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".
9 stars
0 votes
0 copies
2 views
Added September 23, 2026
ai-agentsgobashgitapi
Works with
terminal
api
Security analysis
B75/100
criticalAccesses sensitive system or user directories
Installs into .claude/skills of the current project.
Are you the author of Recheck?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/roxabi-recheck)
---
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