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:
#Npositional arg → strip#--issue Nflag$ARGUMENTS(bare number)
Detect M:
--from-devflag 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
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
greparguments — 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 200closes #Nreferences from triggering 200 sequentialghAPI 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
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
# $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
# $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-devproceeds 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: intsignals: Signal[]— empty on clean path;Signal= {kind,description,evidence[]}blocking: bool—trueiffambiguous(signals)(symbol-missing ∨ dep-resolved). git-drift-only →blocking: false
Task Integration
/R-devowns 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-devtreats approval-stop separately (frame|analyze|spec|plan); all others areadv(or verdict/loop). The DP rendered on signal-fire is skill-internal:/R-recheckself-manages the blocking decision the same way/R-validateand/R-ci-watchdo when they surface failures. From/R-dev's perspective,/R-recheckis alwaysadv.
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=2on a clean 2nd run → printIssue still relevant (re-verified after triage update).+ return.createdAtparse failure → fallback to--since="1 year ago"+ warn:Warning: could not parse issue creation date; using 1-year fallback.
$ARGUMENTS