Drift-check an issue before work begins — fails fast when code has evolved (git-drift, symbol-missing, dep-resolved). Triggers: "recheck" | "is this issue still valid" | "check drift" | "check issue staleness".
Scanned 9/23/2026
Install to Claude Code
npx -y skills add Roxabi/roxabi-plugins --skill recheck --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Recheck?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/roxabi-recheck)More formats (shields.io, HTML) on the badges page.
---
name: R-recheck
argument-hint: '[#N | --issue N]'
description: Drift-check an issue before work begins — fails fast when code has evolved (git-drift, symbol-missing, dep-resolved). Triggers: "recheck" | "is this issue still valid" | "check drift" | "check issue staleness".
version: 0.2.0
allowed-tools: Bash, Read, Grep, Glob, Skill, ToolSearch
---
# Recheck
## Success
I := signals computed ∧ (S == ∅ ∨ informative → auto-proceed) ∨ (S ambiguous → DP presented)
Let:
N := issue number
S := signal set { git-drift, symbol-missing, dep-resolved }
M := mode ∈ { pipeline, standalone }
ambiguous(S) ≔ S contains `symbol-missing` ∨ `dep-resolved`
informative(S) ≔ S ≠ ∅ ∧ ¬ambiguous(S) # git-drift only
Drift-check N against 3 deterministic signals (no LLM). **AQ only when signals are ambiguous** (missing symbols / closed blockers). Pure git-drift is informational — print and auto-proceed.
Standalone-safe: callable without `/R-dev`. Invoked by `/R-dev` between `triage` and `frame`.
## Entry
```
/R-recheck #N standalone — DP only if ambiguous signals (Proceed | Close | Abort)
(invoked by /R-dev) pipeline — same; DP adds Update issue first when ambiguous
```
## Pipeline
| Step | ID | Required | Notes |
|------|---------|----------|------------------------------------------------|
| 0 | parse | ✓ | resolve N, detect M, check `--update-iter` flag |
| 1 | fetch | ✓ | `gh issue view N --json number,title,body,labels,createdAt` |
| 2 | extract | ✓ | cited paths, symbols, blocked-by numbers from body |
| 3 | check | ✓ | run 3 drift checks (parallel, deterministic) |
| 4 | decide | ✓ | S==∅ silent; informative auto-proceed; ambiguous → AQ |
## Step 0 — Parse + Detect Mode
Resolve N from:
- `#N` positional arg → strip `#`
- `--issue N` flag
- `$ARGUMENTS` (bare number)
Detect M:
- `--from-dev` flag present → `pipeline`
- else → `standalone`
Check `--update-iter=2` flag: ∃ → this is the 2nd run after Update; omit **Update issue first** from DP even if signals fire.
Issue N ¬∃ (gh returns 404) → print `Error: issue #N not found.` + exit 1.
## Step 1 — Fetch Issue
```bash
gh issue view N --json number,title,body,labels,createdAt \
--jq '{number:.number, title:.title, body:.body, labels:[.labels[].name], createdAt:.createdAt}'
```
Note: `gh issue view` does not expose `parent` or `blocked-by` as structured JSON fields. Primary approach: grep body for `blocked by #N`, `blocks #N`, `parent: #N`, `closes #N` patterns (Step 2). Secondary: `gh api repos/{owner}/{repo}/issues/N/sub_issues` if sub-issue API is available.
## Step 2 — Extract Refs from Body
From `body`, regex-extract THEN validate. Extraction is loose; **validation is strict** — drop anything that fails.
| Kind | Extraction pattern | Validation (required) | Example match |
|---------------|----------------------------------------------------------|------------------------------------------------------|-----------------------|
| cited paths | `` `path/to/file.ext` `` or bare `path/to/file.ext` | match `^[A-Za-z0-9_./-]+$` ∧ ¬contains `..` segment ∧ ¬absolute (`^/`) — reject otherwise | `src/core/runner.ts` |
| symbols | CamelCase identifiers, `function_names` | match `^[A-Za-z_][A-Za-z0-9_]{0,63}$` — reject anything else (incl. flags, paths, error strings with quotes/spaces) | `WorktreeSetup`, `run_phase` |
| blocked-by Ns | `blocked by #(\d+)`, `blocks #(\d+)`, `parent: #(\d+)`, `closes #(\d+)` | digits only (regex already enforces); dedup; **cap at 20** (warn `dep-drift: capped at 20 blockers (body contained N)` if exceeded) | `#179` |
**Validation rationale:**
- Symbols flow into `grep` arguments — without the allowlist, a body-symbol like `-e '/etc/passwd'` or `$(id)` would inject as a grep flag or command substitution.
- Paths flow into `git log -- <paths>` — without the `..` ban + absolute-path ban, traversal patterns leak outside the repo.
- Blocker numbers flow into `gh issue view <N>` — the `\d+` regex enforces numeric, but the **dedup + cap** prevents an issue body with 200 `closes #N` references from triggering 200 sequential `gh` API calls.
Loose extraction (e.g. picking up an "error string" candidate) then strict validation = silently drop the candidate, do not abort.
∅ validated cited paths → git-drift falls back to entire repo (all commits since createdAt).
∅ validated symbols → symbol-drift is a no-op.
∅ validated blocked-by → dep-drift is a no-op.
## Step 3 — Drift Checks (parallel, deterministic)
Run all 3 checks concurrently. Each emits 0 or more signal entries.
### git-drift
```bash
created_at=$(gh issue view N --json createdAt --jq '.createdAt')
# If cited paths extracted:
git log --since="$created_at" --oneline -- path/to/cited/file.ext ... | wc -l
# If ∅ cited paths (fallback: whole repo):
git log --since="$created_at" --oneline | wc -l
```
Signal fires if count > 0.
`kind: "git-drift"` | `description: "N commits on cited paths since issue created"` | `evidence: [sha list]`
### symbol-drift
```bash
# $sym already validated in Step 2 to match ^[A-Za-z_][A-Za-z0-9_]{0,63}$
# Use grep -F (fixed-string) + -- end-of-options marker for defense-in-depth.
for sym in <validated_symbols>; do
grep -rqF -- "$sym" --include='*.ts' --include='*.tsx' --include='*.js' \
--include='*.py' --include='*.sh' --include='*.md' . \
|| echo "missing: $sym"
done
```
Signal fires per missing symbol.
`kind: "symbol-missing"` | `description: "symbol '$sym' not found in tree"` | `evidence: ["$sym"]`
### dep-drift
```bash
# $blocker already validated in Step 2 to be numeric, deduped, and capped at 20.
# Capture stderr explicitly to distinguish "issue is open" from "API error" —
# silently treating API errors as "not closed" would mask the dep-drift signal.
for blocker in <validated_blockers>; do
if ! out=$(gh issue view "$blocker" --json state --jq '.state' 2>&1); then
echo "dep-drift: error fetching #$blocker — skipped ($out)" >&2
continue
fi
[ "$out" = "CLOSED" ] && echo "closed: #$blocker"
done
```
Signal fires per closed blocker. Semantics: closed blocker = signal regardless of meaning (could be "ready to proceed, re-verify scope" OR "this issue is now moot") — user choice surfaces the ambiguity; user decides.
API failures (auth, network, rate-limit, repo-not-found) are surfaced as warnings on stderr rather than silently swallowed — operators see "skipped" entries and can re-run if needed.
`kind: "dep-resolved"` | `description: "blocker #$blocker is now closed"` | `evidence: ["#$blocker"]`
## Step 4 — Decide
### Severity split (before any AQ)
| Class | Condition | Action |
|-------|-----------|--------|
| clean | S == ∅ | silent return (below) |
| informative | S ≠ ∅ ∧ kinds ⊆ {git-drift} | print signals + **auto-proceed** — ¬AQ |
| ambiguous | ∃ kind ∈ {symbol-missing, dep-resolved} | print signals + **present choice** |
Rationale: git-drift alone means “code moved nearby” — issue still open, symbols found, blockers still open (or none). Default path is always Proceed; asking is pure friction. symbol-missing / dep-resolved can mean the issue is moot or needs rewrite → real decision.
### S == ∅ (no signals)
- **Pipeline mode (M == pipeline):**
Print exactly: `Issue still relevant.`
Return silently. `/R-dev` proceeds to next step.
- **Standalone mode (M == standalone):**
Print richer summary:
```
Issue #N still relevant.
Checks: git-drift (0 commits) | symbol-drift (all found) | dep-drift (no closed blockers)
```
Exit 0.
### informative (git-drift only — auto-proceed)
Render `## Drift Signals` block (same table as below), then print exactly one line and return exit 0:
```
Drift noted (git-drift only) — proceeding. Re-run /R-recheck or update the issue if scope looks stale.
```
¬AQ. Pipeline and standalone share this path. `/R-dev` continues to next step.
### ambiguous (symbol-missing and/or dep-resolved — AQ)
Render `## Drift Signals` block:
```
## Drift Signals — Issue #N
| Kind | Description | Evidence |
|----------------|--------------------------------------|----------------------|
| git-drift | 12 commits on cited paths since... | abc1234, def5678 ... |
| symbol-missing | symbol 'WorktreeSetup' not found | WorktreeSetup |
| dep-resolved | blocker #179 is now closed | #179 |
```
(Include any co-occurring git-drift rows for context.)
Then present user choice:
**Pipeline DP (4 options — M == pipeline ∧ ¬--update-iter=2):**
```
── Decision: Issue #N ambiguous drift ──
Context: symbol-missing and/or dep-resolved (see ## Drift Signals above)
Target: decide whether to continue, update, close, or abort
Path: executed immediately after choice
Options:
1. Proceed anyway — continue, current premise accepted as-is
2. Update issue first — re-invoke /issue-triage, then re-run /R-recheck once
3. Close as resolved/obsolete — gh issue close N --reason completed ; abort /R-dev
4. Abort — exit /R-dev cleanly, no mutation
Recommended: Option 2 if symbols/blockers suggest rewrite; Option 3 if issue is moot
```
**Pipeline DP on 2nd run (M == pipeline ∧ --update-iter=2):**
Same block, but Option 2 (Update issue first) is OMITTED. Forces terminal decision. No infinite loop.
**Standalone DP (3 options — M == standalone):**
```
── Decision: Issue #N ambiguous drift ──
Context: symbol-missing and/or dep-resolved (see ## Drift Signals above)
Target: decide whether to continue, close, or abort
Path: executed immediately after choice
Options:
1. Proceed — continue with current premise
2. Close as resolved/obsolete — gh issue close N --reason completed
3. Abort — exit cleanly, no mutation
Recommended: Option 2 if issue is moot; Option 1 if premise still holds
```
## DP Outcomes — Implementation
| Option | Effect |
|----------------------|-------------------------------------------------------------------------------------------|
| **Proceed anyway** | Return exit 0. In pipeline, `/R-dev` re-scans and continues to next step. |
| **Update issue first** | `Skill: "issue-triage", args: "N"` → then re-run self with `--from-dev --update-iter=2 #N`. On 2nd run, omit Update from DP (loop bound = 1 iteration). **Note on task state:** `/R-dev` has already marked its `triage` task `completed` in the prior Step 8 cycle and does not re-mark it during this in-skill re-invocation. The triage task remains `completed` in `/R-dev`'s task list — by design, since recheck owns the side-channel refinement, not `/R-dev`. The actual triage work is re-executed and its effects (label changes, re-classification) take hold immediately; only the task-list flag is stale, and that has no downstream consumer. |
| **Close as resolved/obsolete** | `gh issue close N --reason completed --comment "Closed by /R-recheck — drift signals indicated issue is no longer applicable."` → exit with abort signal so `/R-dev` marks task cancelled. |
| **Abort** | Exit cleanly. No mutation. `/R-dev` halts cleanly. |
## State
No on-disk artifact. `/R-dev` tracks recheck as Σ_s (session-only) like `validate`, `ci-watch`. Re-running `/R-dev #N` in a new session re-runs `/R-recheck` — acceptable: deterministic checks are cheap and fresh state is more valuable than skip-on-resume.
`RecheckResult` is ephemeral — built during execution, consumed by decide step, then discarded:
- `issue_number: int`
- `signals: Signal[]` — empty on clean path; `Signal` = { `kind`, `description`, `evidence[]` }
- `blocking: bool` — `true` iff `ambiguous(signals)` (symbol-missing ∨ dep-resolved). git-drift-only → `blocking: false`
## Task Integration
- `/R-dev` owns the dev-pipeline task lifecycle externally
- This skill does NOT update its own dev-pipeline task
- Sub-tasks created: none
## Chain Position
- **Phase:** Frame
- **Predecessor:** `/issue-triage`
- **Successor:** `/R-frame` (F-lite, F-full) ∨ `/R-dev-implement` (S, frame skipped)
- **Class:** `adv` — `/R-dev` treats approval-stop separately (`frame|analyze|spec|plan`); all others are `adv` (or verdict/loop). The DP rendered on signal-fire is **skill-internal**: `/R-recheck` self-manages the blocking decision the same way `/R-validate` and `/R-ci-watch` do when they surface failures. From `/R-dev`'s perspective, `/R-recheck` is always `adv`.
## Exit
| Path | Effect |
|-----------------------------------|------------------------------------------------------------------------|
| Via `/R-dev` — clean | Print `Issue still relevant.` → return silently → `/R-dev` continues |
| Via `/R-dev` — informative | Print drift table + proceed line → exit 0 → `/R-dev` continues |
| Via `/R-dev` — Proceed on ambiguous | Return exit 0 → `/R-dev` re-scans → next step |
| Via `/R-dev` — Update issue first | Invoke `issue-triage` → re-run self once (`--update-iter=2`) |
| Via `/R-dev` — Close | `gh issue close N` → exit with abort signal → `/R-dev` marks cancelled |
| Via `/R-dev` — Abort | Exit cleanly, no mutation → `/R-dev` halts |
| Standalone — clean | Print richer summary (counts per check) → exit 0 |
| Standalone — informative | Print drift table + proceed line → exit 0 |
| Standalone — ambiguous | Render `## Drift Signals` + 3-option DP → apply outcome |
## Edge Cases
- ∅ cited paths ∧ ∅ symbols ∧ ∅ blockers in body → git-drift uses entire repo since `createdAt` (commit count); symbol-drift + dep-drift are no-ops.
- Issue ¬∃ (gh 404) → print `Error: issue #N not found.` + exit 1.
- `--update-iter=2` on a clean 2nd run → print `Issue still relevant (re-verified after triage update).` + return.
- `createdAt` parse failure → fallback to `--since="1 year ago"` + warn: `Warning: could not parse issue creation date; using 1-year fallback.`
$ARGUMENTS
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!