Tidy
The cleanup walk runs in a fresh host-provided reviewer per iteration when reviewer dispatch is available; Edit application stays in the main thread. The skill loops the dispatch + apply cycle until the reviewer returns no more mechanical_edits, max iterations is reached, or a safety rail trips.
Scope: invocation targets are arbitrary source files (application code, config, SKILL.md, any text). The skill does not restrict the changed-file set to a fixed directory prefix — any path in the diff is reviewed unless caught by the exclusion list (Step 1 step 3).
Invocation contract
The caller passes these fields in natural language (the skill extracts them from the invocation text):
Base ref (optional, default <working-tree-vs-HEAD>) — git ref to diff against. When omitted, the skill looks at the working tree's uncommitted + staged + untracked changes (the default scope for a post-implementation cleanup pass). When specified (e.g. Base ref: main), the skill switches to git diff <Base ref> semantics — useful for callers that want to review a stack of already-committed changes between a base branch and HEAD.
Untracked-file scope asymmetry: the default mode includes untracked new files (collected via git status --porcelain=v1 --untracked-files=all -z), but the explicit Base ref mode reads committed history only and excludes untracked files. A caller passing Base ref: HEAD~5 expecting "everything since base" will silently miss new files added since HEAD~5 that were never committed. If you need untracked files in scope, omit Base ref and rely on the working-tree default.
Max iterations (optional, default 3) — upper bound on the refinement loop.
Custom instructions (optional) — free-form text injected into the dispatch payload as additional constraints alongside the cleanup checklist.
Model (optional) — model id applied as the model parameter on the reviewer Agent dispatch in Step 3 (a). An independent optional field (not part of a fixed-arity mode gate); a caller such as dev-workflow passes it to run the cleanup reviewer on a specific model. The same value is applied to every iteration's reviewer dispatch. Effective only on the Claude Code Agent-dispatch path; on the reviewer-dispatch-unavailable inline fallback (Step 3 (a)) the executing agent's own model governs. Validity predicate: a value is valid only if it is one of the model ids the current Agent tool's model parameter accepts — check the tool's live schema loaded in the current session; a full claude-* id (e.g. claude-sonnet-5) is outside that parameter's accepted aliases and is therefore invalid too. An absent field or an invalid value → no override; the reviewer Agent inherits the session model.
The caller must not stage changes while this skill is running. The skill reads the working tree; staged content would mix into the diff and corrupt the verdict. (The Base ref mode reads committed history vs the ref, so staging interference applies only to the default working-tree mode.)
Dispatch authorization
This skill's procedure dispatches subagents, so invoking the skill is the request to use that mechanism: an ambient instruction allowing subagent dispatch only when the user asked for it — a permission-shaped restriction — is already satisfied by this invocation. Do not ask the user to re-confirm the dispatch, and do not silently substitute inline execution for a dispatch this procedure specifies. Only two things justify that substitution: technical availability (the dispatch tool is not present and callable on the current tool surface), and an explicit contract term from the caller bounding this skill to its own thread. A permission-shaped restriction is neither.
Process
Step 1 — Detect changed files (main thread)
- Parse
Base ref, Max iterations, Custom instructions, and the optional Model value from the invocation text per § Invocation contract. Hold Model for the Step 3 (a) reviewer dispatch; absent or invalid → no override (inherit). See § Invocation contract's Model field for the validity predicate.
- Compute the changed-file set based on
Base ref:
- Default mode (no
Base ref provided):
git diff --name-only for unstaged tracked changes
git diff --name-only --cached for staged tracked changes
git status --porcelain=v1 --untracked-files=all -z and collect entries prefixed ?? for untracked files
- Explicit
Base ref mode (e.g. Base ref: main): git diff --name-only <Base ref> only. Untracked files are out of scope (see § Invocation contract).
- Apply the exclusion list — drop any path matching:
- Lockfiles / dependency manifests:
package-lock.json, pnpm-lock.yaml, yarn.lock, Gemfile.lock, Cargo.lock, poetry.lock, uv.lock, Pipfile.lock, go.sum, any *.lock
- Generated / build artifacts: paths under
dist/, build/, node_modules/, target/, vendor/, .next/, .nuxt/
- Binary files (extension-based):
*.png, *.jpg, *.jpeg, *.gif, *.webp, *.ico, *.pdf, *.zip, *.tar, *.gz, *.so, *.dylib, *.exe, *.bin
- Hold the filtered set in main-thread context as
changed_files (the scope-check baseline for Step 3 (c)). Additionally retain the subset of paths that arrived as ??-prefixed entries (untracked files) as untracked_paths — Step 3 (c)'s frontmatter rail uses it for the HEAD-absent check.
- If
changed_files is empty, emit the verdict {"status": "no-actionable-findings", "iterations_used": 0, "applied_edits_count": 0, "notes_remaining_count": 0, "reverted_paths": [], "reason": null} per § Return contract and stop. iterations_used: 0 because the iteration loop never runs.
Step 2 — Gather review inputs (main thread)
For each entry in changed_files, in the main thread:
Read the file's full current contents. Hold in main-thread context as the iter-1 snapshot.
Read references/cleanup-checklist.md.
- Pre-capture each changed file's
git diff:
- Default mode: for staged-only changes use
git diff --cached -- <file>, for working-tree-only changes use git diff -- <file>, for mixed staged + unstaged edits concatenate them (staged section first) into one unified diff payload. For untracked files there is no diff (the file did not exist at base) — present the full file contents as the "new file" hunk in the dispatch payload.
- Explicit
Base ref mode: git diff <Base ref> -- <file> (committed history vs the ref).
Step 3 — Iteration loop (i = 1 .. Max iterations)
Pre-register iteration tasks — before entering the loop, TaskCreate one task per iteration with subject iteration 1, iteration 2, ..., iteration <Max iterations>. Mark in_progress (via TaskUpdate) before each dispatch, completed after parse + apply (a converged iteration marks completed immediately after parsing). On early convergence (no mechanical_edits returned) or safety-rail-triggered exit, mark remaining iteration tasks completed with the skip note recorded in the task's description field as — skipped: converged / — skipped: <reason>. Pre-registration is load-bearing when the host supports it: without it, executor-driven loops tend to stop at the first iteration that looks acceptable.
Task tools unavailable fallback (e.g. the VSCode extension, a Codex host, or a nested subagent context where progress-tracking tools were not surfaced): skip the pre-registration step and hold iteration state (current i, cumulative applied_edits_count, notes_remaining_count, accumulated out_of_scope, reverted_paths) in main-thread context instead. Progress tracking is not correctness-critical — the loop semantics in (a)–(c) are unaffected.
(a) Dispatch reviewer
On i == 1, use the snapshot from Step 2. On i ≥ 2, only re-Read the subset of changed_files whose path appeared in a successfully-applied mechanical_edits entry during iter i - 1 (untouched files keep their iter-1 snapshot). For that same re-read subset, also re-run the per-file git diff so the diff payload reflects edits that landed in prior iterations; files outside the subset keep their iter-1 diff.
Dispatch a fresh reviewer through the current host's reviewer-dispatch mechanism. In Claude Code, use the Agent tool when it is exposed and callable and no caller-imposed nesting bound applies (see the Reviewer-dispatch unavailable fallback paragraph below) — passing the parsed Model value (§ Invocation contract) as the Agent model parameter only when it resolved to a valid override, and omitting it otherwise (absent or invalid → inherit). Apply the same Model decision on every iteration's dispatch. In Codex, use the exposed subagent / delegation mechanism when available. Assemble the dispatch prompt from the five sections below, each framed with a clear --- LABEL --- fence:
--- CLEANUP CHECKLIST ---: the full content of references/cleanup-checklist.md
--- CHANGED FILES ---: each changed file's path, full content, and unified diff (one block per file, separated by ### <path> sub-headings; for untracked files present "(new file — no prior contents)" before the full current contents)
--- CUSTOM INSTRUCTIONS ---: the value of Custom instructions from the invocation, or the literal (none) placeholder when the field is empty
--- REVIEWER PROMPT ---: the reviewer prompt below (verbatim)
--- RESPONSE FORMAT ---: the response format and constraints below (verbatim)
Reviewer prompt (include verbatim in the dispatch):
You are a fresh reviewer of changed code. You have not seen prior conversation context — only the CLEANUP CHECKLIST, CHANGED FILES, and CUSTOM INSTRUCTIONS below. Walk the CLEANUP CHECKLIST against each CHANGED FILE.
Preserve functionality (hard constraint): never propose a change that alters observable behavior — features, outputs, error conditions, or side-effect ordering — regardless of which checklist item the finding matches. Read § Preserve functionality at the top of the CLEANUP CHECKLIST as the binding rule. If a candidate fix could plausibly change behavior, downgrade it to a structural_note; when behavior preservation is clear from the diff and the fix matches § Behavior-preserving structural improvements, a mechanical_edit is correct.
Only the changed lines and their immediately surrounding context are in-scope (lines added or modified, plus surrounding code where the change participates in a structural cleanup opportunity). Do not audit pre-existing untouched code in sibling regions. Project conventions under .claude/rules/ and CLAUDE.md override the checklist where they conflict — defer to them for language-specific / framework-specific standards (import style, function-declaration form, return-type annotation conventions, error-handling patterns, component / module structure).
Classify each finding:
- mechanical_edit: a fix that can be applied as a textual replacement — removing a redundant narration comment, deleting a dead branch, replacing a defensive guard on an already-safe path, collapsing a needless local helper, expanding a nested ternary into an
if/else, or a behavior-preserving structural improvement that meets the CLEANUP CHECKLIST's § Behavior-preserving structural improvements conditions. Return as a {file, old_string, new_string, rationale} Edit
- structural_note: a fix that requires moving content between files, deleting sections, rewriting large portions, or carries any risk of behavior change. Return as a
{file, description} note. These will not be applied automatically — the caller surfaces them via notes_remaining_count
old_string must match exactly one location in the current file. Include 1–3 lines of surrounding context so the snippet is unique.
If CUSTOM INSTRUCTIONS is non-empty, treat its text as additional constraints — apply alongside (not in place of) the checklist.
Balance rails (required): a candidate mechanical_edit that would violate § Balance rails — anti-over-simplification at the bottom of the CLEANUP CHECKLIST is not actionable as a mechanical fix even when it matches one of items 1–10. Either downgrade to structural_note or skip the finding entirely. § Balance rails and § Preserve functionality are the negative gates (what must not be mechanical); § Behavior-preserving structural improvements is the positive gate a finding reaches only after passing them.
Overlap resolution (required): when a finding could match more than one checklist item, apply the Overlap handling rules listed in the CLEANUP CHECKLIST and emit only the preferred classification (first-match-wins per finding — never split one finding across multiple items).
Gate reachability rule (required): when there are no actionable mechanical findings on this iteration, you must return mechanical_edits: []. Do not emit speculative or "nice to have" edits — mechanical_edits == [] is the convergence signal and must be empty when no apply work remains. Continuing to flag issues only as structural_notes is fine; that is the documented exit path for non-mechanical findings.
Response format (include verbatim in the dispatch):
Write your reasoning and per-file findings in natural language, then end your response with a single fenced JSON block matching this schema:
```json
{
"mechanical_edits": [
{"file": "<path>", "old_string": "<unique 1-3 line snippet>", "new_string": "<replacement>", "rationale": "<short reason>"}
],
"structural_notes": [
{"file": "<path>", "description": "<short description>"}
]
}
```
Reviewer-dispatch unavailable fallback: detect availability by inspecting the current tool surface; do not attempt a speculative call just to probe availability. When no host-provided reviewer dispatch is available — the Agent tool is absent from the tool surface (e.g. this skill runs inside a nested subagent context where nested Agent is not surfaced), or the invoking request explicitly bounds this skill to its own thread (a caller-imposed nesting bound — see § Dispatch authorization, which excludes permission-shaped restrictions but not an explicit contract term from the caller) — walk the cleanup checklist over each changed file inline-sequentially in the main thread once per iteration. Being invoked as a sub-skill (e.g. via Skill() on the main thread) does not by itself trigger this path (see § Dispatch authorization): decide by whether Agent is exposed and callable and whether a caller-imposed nesting bound applies. Under the fallback, construct the same fenced JSON block defined above so step (b)'s parser handles both paths identically. Output scope: the per-iteration JSON produced under fallback is internal state for step (b)'s parser — hold it in main-thread context only; do not write it into the user-visible response. Only the terminal verdict JSON from § Return contract appears in the response (see the uniqueness clause there).
(b) Parse & apply — evaluate in this order, first match wins
Dispatch failures (reviewer-dispatch tool error / timeout / empty response) are handled in § Dispatch failure before this parse step runs and do not enter sub-cases 1–4 below.
- Verdict missing or malformed — no fenced JSON block found, or JSON parse fails → exit loop with terminal
{"status": "error", "iterations_used": <i>, "applied_edits_count": <cumulative>, "notes_remaining_count": 0, "reverted_paths": [], "reason": "verdict parse failure"}. Do not consume remaining iter slots.
- Schema violation — required keys (
mechanical_edits, structural_notes) are missing, values are not arrays, or any entry fails its expected per-entry shape (mechanical_edits entries must have non-empty string file, old_string, new_string; structural_notes entries must have non-empty string file, description) → exit loop with terminal {"status": "error", "iterations_used": <i>, "applied_edits_count": <cumulative>, "notes_remaining_count": 0, "reverted_paths": [], "reason": "verdict schema violation"}.
- No more apply work —
mechanical_edits == [] → exit loop. Determine the terminal status from cumulative state and the current iter's structural_notes:
- cumulative
applied_edits_count == 0 AND structural_notes == [] → no-actionable-findings
- cumulative
applied_edits_count > 0 AND structural_notes == [] → applied-edits (notes count = 0)
structural_notes != [] (regardless of cumulative count) → if cumulative > 0 then applied-edits (with notes_remaining_count > 0), else notes-left
- Otherwise — apply
mechanical_edits in order:
- For each entry, verify
file ∈ changed_files; if not, record the path in an out_of_scope list and skip the entry without calling Edit. The out_of_scope list is later surfaced via reverted_paths in the terminal verdict if non-empty.
- For each in-scope entry, re-
Read the target file (so old_string matches the current contents after any earlier edit landed), then call Edit.
- If an
old_string is not found, skip that entry and continue with the next. This is expected when the reviewer emits multiple edits from a single snapshot and a later edit overlaps a region an earlier one already rewrote — the skip is a no-op fallback, not an error.
- Increment
applied_edits_count only for entries whose Edit call succeeded — skipped entries do not count.
- After the edits (applied or skipped), if at least one Edit succeeded, run the safety rails in (c), then continue to iteration
i + 1. If all entries skipped, also continue (the next iter will re-dispatch with the current file state).
(c) Per-iteration safety rails — run only if at least one edit was applied
Frontmatter integrity — for each edited file that begins with a ----delimited YAML frontmatter block, re-Read and parse the frontmatter. If parsing fails:
git checkout HEAD -- <file>
Exit loop with terminal {"status": "error", "iterations_used": <i>, "applied_edits_count": <surviving on disk>, "notes_remaining_count": 0, "reverted_paths": [<file>], "reason": "frontmatter broken"}. applied_edits_count reflects edits still on disk after the revert (the revert wipes this file's prior-iter contributions; cross-file edits to other paths in earlier iters survive). Files without a frontmatter block skip this rail.
Untracked-path specialization: if the offending file is in untracked_paths (i.e. HEAD-absent — the iteration just edited an untracked new file), skip the git checkout for HEAD-absent paths; the file is left in its post-edit state, applied_edits_count retains all surviving on-disk edits (no revert occurred), and reverted_paths still carries the offending path as an informational entry so the caller sees what could not be reverted.
Scope — the per-edit pre-check in (b) step 4 already skips out-of-scope writes, so no global git diff --name-only revert is needed. If the out_of_scope list accumulated by (b) step 4 is non-empty for this iteration, exit loop with terminal {"status": "error", "iterations_used": <i>, "applied_edits_count": <cumulative>, "notes_remaining_count": 0, "reverted_paths": <out_of_scope>, "reason": "scope violation"} (no actual git checkout runs — the pre-check prevented the write).
Step 4 — Max iterations reached without convergence
If the loop runs all Max iterations without (b) sub-case 3 firing (i.e. the reviewer kept emitting mechanical_edits to the end, but apply progress stalled — typically all entries skipping because old_string collisions), determine the terminal status from cumulative state and the last iter's structural_notes:
- cumulative
applied_edits_count > 0 → applied-edits (notes_remaining = last-iter structural_notes count)
- cumulative
applied_edits_count == 0 AND last-iter structural_notes == [] → no-actionable-findings
- cumulative
applied_edits_count == 0 AND last-iter structural_notes != [] → notes-left
Step 5 — Emit verdict
End your response with a single fenced JSON block matching the schema in § Return contract. See § Sub-skill caller directive for the caller-side no-stall discipline that applies when this skill is invoked as a sub-skill.
Scope
- Only review files that have uncommitted changes (default mode) or that appear in the
Base ref-vs-HEAD diff — diff-scoped, not a full audit
- Project conventions (
.claude/rules/, CLAUDE.md) override the checklist where they conflict
- Don't chase perfection — fix real cleanup wins, note structural ones, move on
- Sub-skill scope note (caller-side): when this skill runs as a sub-skill, structural changes are surfaced via
notes_remaining_count rather than applied. The caller decides whether and how to act on them. See § Sub-skill caller directive for the no-stall discipline that applies on the sub-skill invocation path.
Return contract
The skill emits a single fenced JSON block at the very end of the invocation. Only one fenced JSON block must appear in the user-visible response — the verdict block. Intermediate structured outputs that the skill constructs internally for its own parser (e.g., the per-iteration reviewer JSON synthesized under the Agent-unavailable fallback path) are held in main-thread context and do not enter the response stream.
Emit a single fenced JSON block at the end of the response, matching the schema:
{
"status": "no-actionable-findings|applied-edits|notes-left|error",
"iterations_used": N,
"applied_edits_count": N,
"notes_remaining_count": N,
"reverted_paths": ["<path>", "..."],
"reason": "verdict parse failure|verdict schema violation|frontmatter broken|scope violation|dispatch error|null"
}
The |null token at the end of the reason enum means JSON null (not the string "null").
Field semantics:
status:
no-actionable-findings: the iteration loop converged with cumulative applied_edits_count == 0 AND no structural_notes outstanding
applied-edits: at least one mechanical fix was applied across the iteration loop (cumulative applied_edits_count > 0); notes_remaining_count may be 0 (clean convergence) or > 0 (notes alongside applied edits)
notes-left: cumulative applied_edits_count == 0 AND notes_remaining_count > 0 (only structural changes were flagged, surfaced via notes_remaining_count)
error: an internal error occurred — see reason
iterations_used: number of iterations whose reviewer dispatch returned a verdict, including the iteration whose verdict triggered convergence. Step 1 early returns (no changed files after exclusion) count as 0
applied_edits_count: non-negative integer count of Edit calls whose result is still on disk at the time the verdict is emitted. For verdict parse failure / verdict schema violation / dispatch error / scope violation, this is the cumulative count of successful Edit calls across earlier iterations of the same invocation (none of these recovery paths revert in-scope edits — scope violation only flags the offending out-of-scope paths informationally). The exception is frontmatter broken: its recovery reverts the edited file itself (or skips the revert for an untracked HEAD-absent path), so the count drops accordingly
notes_remaining_count: non-negative integer. Count of structural / still-actionable items flagged in the terminal iteration but not applied. Always 0 for no-actionable-findings and any error status
reverted_paths: array of <path> strings. Empty array [] for no-actionable-findings / applied-edits / notes-left / verdict parse failure / verdict schema violation / dispatch error / Step 1 early returns. For frontmatter broken: contains the offending file path in both branches (HEAD-present revert: path was reverted; HEAD-absent: path retained as informational entry since the rail skipped git checkout). For scope violation: contains the out_of_scope paths the pre-check rejected (no git checkout ran; the entries are informational rejected-write records). (Source of truth: the JSON shapes in Step 1 early return, Step 3 (b) sub-cases 1/2, Step 3 (c) frontmatter/scope rails, and § Dispatch failure; keep this enumeration in sync when status conditions are added.)
reason: enum string only when status == "error", otherwise JSON null. Keep reason payloads to the listed enum tokens — no free-form text, newlines, or control characters — so the verdict stays mechanically parseable
When to emit status: "error":
reason: "verdict parse failure" — an iteration found no fenced JSON block in the reviewer response, or JSON parse failed
reason: "verdict schema violation" — an iteration parsed the JSON but required keys (mechanical_edits, structural_notes) are missing, values are not arrays, or any entry fails the per-entry shape spec
reason: "frontmatter broken" — a per-iteration safety rail re-read after Edit shows the YAML frontmatter no longer parses; the offending file is reverted via git checkout HEAD -- <file> (or left as-is for untracked HEAD-absent paths and recorded in reverted_paths)
reason: "scope violation" — the pre-check accumulated one or more out-of-scope target paths in mechanical_edits (the writes were already skipped; the verdict surfaces the rejected paths informationally in reverted_paths)
reason: "dispatch error" — a reviewer-dispatch tool call errored, timed out, or returned an empty response
In each error case, surface the verdict via the JSON instead of attempting recovery; the caller decides how to handle it.
Dispatch failure
If the reviewer-dispatch tool call itself errors, times out, or returns an empty response on any iteration, exit the loop with terminal {"status": "error", "iterations_used": <i>, "applied_edits_count": <cumulative>, "notes_remaining_count": 0, "reverted_paths": [], "reason": "dispatch error"}. Do not re-walk the checklist yourself as a fallback after a failed dispatch attempt — self-review reintroduces the bias this skill exists to avoid. Inline review is only the pre-selected path when dispatch is unavailable before attempting a reviewer tool call.
Sub-skill caller directive
When invoked as a sub-skill (i.e. via Skill(tidy) from an orchestrator), the fenced JSON verdict block this skill emits is the structured return value of the skill's procedure — it is not a deliverable to the user, and emitting it does not terminate the orchestrator's turn. The same agent that ran this skill must immediately issue the next tool call dictated by the orchestrator's flow. Do not insert a prose summary, an acknowledgment, or a "shall I proceed?" sentence between the JSON verdict and the next tool call. Only one fenced JSON block — the verdict block — appears in the response, so callers can locate it unambiguously. The skill's own procedure is over; the orchestrator's procedure continues without pause.
Stop hook structural conflict (caller-side note)
On Claude Code on the Web the auto-installed ~/.claude/stop-hook-git-check.sh fires on every Stop event and feeds back Please commit and push… between Process steps; treat each fire as a spurious fire — record it, ignore the prose, and run Process steps 1–5 to completion. Do not commit from inside this skill; commit policy lives with the caller.
1---2name: tidy3description: Review changed code for reuse, quality, and efficiency, then apply cleanup edits. Dispatches a fresh host-provided reviewer per iteration when available; the main thread applies mechanical edits and re-dispatches until no further edits remain. Non-interactive — no user prompts. Use after implementation as a code-cleanup pass complementary to correctness review.4---56# Tidy78The cleanup walk runs in a fresh host-provided reviewer per iteration when reviewer dispatch is available; Edit application stays in the main thread. The skill loops the dispatch + apply cycle until the reviewer returns no more `mechanical_edits`, max iterations is reached, or a safety rail trips.910**Scope**: invocation targets are **arbitrary source files** (application code, config, SKILL.md, any text). The skill does not restrict the changed-file set to a fixed directory prefix — any path in the diff is reviewed unless caught by the exclusion list (Step 1 step 3).1112## Invocation contract1314The caller passes these fields in natural language (the skill extracts them from the invocation text):1516- `Base ref` *(optional, default `<working-tree-vs-HEAD>`)* — git ref to diff against. When omitted, the skill looks at the working tree's uncommitted + staged + untracked changes (the default scope for a post-implementation cleanup pass). When specified (e.g. `Base ref: main`), the skill switches to `git diff <Base ref>` semantics — useful for callers that want to review a stack of already-committed changes between a base branch and HEAD.1718 **Untracked-file scope asymmetry**: the default mode includes untracked new files (collected via `git status --porcelain=v1 --untracked-files=all -z`), but the explicit `Base ref` mode reads committed history only and **excludes untracked files**. A caller passing `Base ref: HEAD~5` expecting "everything since base" will silently miss new files added since `HEAD~5` that were never committed. If you need untracked files in scope, omit `Base ref` and rely on the working-tree default.1920- `Max iterations` *(optional, default `3`)* — upper bound on the refinement loop.2122- `Custom instructions` *(optional)* — free-form text injected into the dispatch payload as additional constraints alongside the cleanup checklist.2324- `Model` *(optional)* — model id applied as the `model` parameter on the reviewer `Agent` dispatch in Step 3 (a). An independent optional field (not part of a fixed-arity mode gate); a caller such as `dev-workflow` passes it to run the cleanup reviewer on a specific model. The same value is applied to **every** iteration's reviewer dispatch. Effective **only on the Claude Code `Agent`-dispatch path**; on the reviewer-dispatch-unavailable inline fallback (Step 3 (a)) the executing agent's own model governs. **Validity predicate**: a value is valid only if it is one of the model ids the current `Agent` tool's `model` parameter accepts — check the tool's live schema loaded in the current session; a full `claude-*` id (e.g. `claude-sonnet-5`) is outside that parameter's accepted aliases and is therefore invalid too. An absent field or an invalid value → no override; the reviewer `Agent` inherits the session model.2526The caller must **not** stage changes while this skill is running. The skill reads the working tree; staged content would mix into the diff and corrupt the verdict. (The `Base ref` mode reads committed history vs the ref, so staging interference applies only to the default working-tree mode.)2728## Dispatch authorization2930This skill's procedure dispatches subagents, so invoking the skill **is** the request to use that mechanism: an ambient instruction allowing subagent dispatch only when the user asked for it — a **permission-shaped restriction** — is already satisfied by this invocation. Do not ask the user to re-confirm the dispatch, and do not silently substitute inline execution for a dispatch this procedure specifies. Only two things justify that substitution: **technical availability** (the dispatch tool is not present and callable on the current tool surface), and an **explicit contract term from the caller** bounding this skill to its own thread. A permission-shaped restriction is neither.3132## Process3334### Step 1 — Detect changed files (main thread)35361. Parse `Base ref`, `Max iterations`, `Custom instructions`, and the optional `Model` value from the invocation text per `§ Invocation contract`. Hold `Model` for the Step 3 (a) reviewer dispatch; absent or invalid → no override (inherit). See `§ Invocation contract`'s `Model` field for the validity predicate.372. Compute the changed-file set based on `Base ref`:38 - **Default mode** (no `Base ref` provided):39 - `git diff --name-only` for unstaged tracked changes40 - `git diff --name-only --cached` for staged tracked changes41 - `git status --porcelain=v1 --untracked-files=all -z` and collect entries prefixed `??` for untracked files42 - **Explicit `Base ref` mode** (e.g. `Base ref: main`): `git diff --name-only <Base ref>` only. Untracked files are out of scope (see `§ Invocation contract`).433. Apply the exclusion list — drop any path matching:44 - **Lockfiles / dependency manifests**: `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `Gemfile.lock`, `Cargo.lock`, `poetry.lock`, `uv.lock`, `Pipfile.lock`, `go.sum`, any `*.lock`45 - **Generated / build artifacts**: paths under `dist/`, `build/`, `node_modules/`, `target/`, `vendor/`, `.next/`, `.nuxt/`46 - **Binary files** (extension-based): `*.png`, `*.jpg`, `*.jpeg`, `*.gif`, `*.webp`, `*.ico`, `*.pdf`, `*.zip`, `*.tar`, `*.gz`, `*.so`, `*.dylib`, `*.exe`, `*.bin`474. Hold the filtered set in main-thread context as `changed_files` (the scope-check baseline for Step 3 (c)). Additionally retain the subset of paths that arrived as `??`-prefixed entries (untracked files) as `untracked_paths` — Step 3 (c)'s frontmatter rail uses it for the HEAD-absent check.485. If `changed_files` is empty, emit the verdict `{"status": "no-actionable-findings", "iterations_used": 0, "applied_edits_count": 0, "notes_remaining_count": 0, "reverted_paths": [], "reason": null}` per `§ Return contract` and stop. `iterations_used: 0` because the iteration loop never runs.4950### Step 2 — Gather review inputs (main thread)5152For each entry in `changed_files`, in the main thread:53541. `Read` the file's full current contents. Hold in main-thread context as the iter-1 snapshot.552. `Read` `references/cleanup-checklist.md`.563. Pre-capture each changed file's `git diff`:57 - **Default mode**: for staged-only changes use `git diff --cached -- <file>`, for working-tree-only changes use `git diff -- <file>`, for mixed staged + unstaged edits concatenate them (staged section first) into one unified diff payload. For **untracked** files there is no diff (the file did not exist at base) — present the full file contents as the "new file" hunk in the dispatch payload.58 - **Explicit `Base ref` mode**: `git diff <Base ref> -- <file>` (committed history vs the ref).5960### Step 3 — Iteration loop (i = 1 .. Max iterations)6162**Pre-register iteration tasks** — before entering the loop, `TaskCreate` one task per iteration with subject `iteration 1`, `iteration 2`, ..., `iteration <Max iterations>`. Mark `in_progress` (via `TaskUpdate`) before each dispatch, `completed` after parse + apply (a converged iteration marks `completed` immediately after parsing). On early convergence (no `mechanical_edits` returned) or safety-rail-triggered exit, mark remaining iteration tasks `completed` with the skip note recorded in the task's `description` field as `— skipped: converged` / `— skipped: <reason>`. Pre-registration is load-bearing when the host supports it: without it, executor-driven loops tend to stop at the first iteration that looks acceptable.6364**Task tools unavailable fallback** (e.g. the VSCode extension, a Codex host, or a nested subagent context where progress-tracking tools were not surfaced): skip the pre-registration step and hold iteration state (current `i`, cumulative `applied_edits_count`, `notes_remaining_count`, accumulated `out_of_scope`, `reverted_paths`) in main-thread context instead. Progress tracking is not correctness-critical — the loop semantics in (a)–(c) are unaffected.6566#### (a) Dispatch reviewer6768On `i == 1`, use the snapshot from Step 2. On `i ≥ 2`, only re-`Read` the subset of `changed_files` whose path appeared in a successfully-applied `mechanical_edits` entry during iter `i - 1` (untouched files keep their iter-1 snapshot). For that same re-read subset, also re-run the per-file `git diff` so the diff payload reflects edits that landed in prior iterations; files outside the subset keep their iter-1 diff.6970Dispatch a fresh reviewer through the current host's reviewer-dispatch mechanism. In Claude Code, use the `Agent` tool when it is exposed and callable and no caller-imposed nesting bound applies (see the **Reviewer-dispatch unavailable fallback** paragraph below) — passing the parsed `Model` value (§ Invocation contract) as the `Agent` `model` parameter only when it resolved to a valid override, and omitting it otherwise (absent **or** invalid → inherit). Apply the same `Model` decision on every iteration's dispatch. In Codex, use the exposed subagent / delegation mechanism when available. Assemble the dispatch prompt from the five sections below, each framed with a clear `--- LABEL ---` fence:7172- `--- CLEANUP CHECKLIST ---`: the full content of `references/cleanup-checklist.md`73- `--- CHANGED FILES ---`: each changed file's path, full content, and unified diff (one block per file, separated by `### <path>` sub-headings; for untracked files present "(new file — no prior contents)" before the full current contents)74- `--- CUSTOM INSTRUCTIONS ---`: the value of `Custom instructions` from the invocation, or the literal `(none)` placeholder when the field is empty75- `--- REVIEWER PROMPT ---`: the reviewer prompt below (verbatim)76- `--- RESPONSE FORMAT ---`: the response format and constraints below (verbatim)7778**Reviewer prompt (include verbatim in the dispatch):**7980> You are a fresh reviewer of changed code. You have **not** seen prior conversation context — only the CLEANUP CHECKLIST, CHANGED FILES, and CUSTOM INSTRUCTIONS below. Walk the CLEANUP CHECKLIST against each CHANGED FILE.81>82> **Preserve functionality (hard constraint)**: never propose a change that alters observable behavior — features, outputs, error conditions, or side-effect ordering — regardless of which checklist item the finding matches. Read § Preserve functionality at the top of the CLEANUP CHECKLIST as the binding rule. If a candidate fix could plausibly change behavior, downgrade it to a `structural_note`; when behavior preservation is clear from the diff and the fix matches § Behavior-preserving structural improvements, a `mechanical_edit` is correct.83>84> Only the **changed lines and their immediately surrounding context** are in-scope (lines added or modified, plus surrounding code where the change participates in a structural cleanup opportunity). Do not audit pre-existing untouched code in sibling regions. Project conventions under `.claude/rules/` and `CLAUDE.md` override the checklist where they conflict — defer to them for language-specific / framework-specific standards (import style, function-declaration form, return-type annotation conventions, error-handling patterns, component / module structure).85>86> Classify each finding:87>88> - **mechanical_edit**: a fix that can be applied as a textual replacement — removing a redundant narration comment, deleting a dead branch, replacing a defensive guard on an already-safe path, collapsing a needless local helper, expanding a nested ternary into an `if`/`else`, or a behavior-preserving structural improvement that meets the CLEANUP CHECKLIST's § Behavior-preserving structural improvements conditions. Return as a `{file, old_string, new_string, rationale}` Edit89> - **structural_note**: a fix that requires moving content between files, deleting sections, rewriting large portions, or carries any risk of behavior change. Return as a `{file, description}` note. These will **not** be applied automatically — the caller surfaces them via `notes_remaining_count`90>91> `old_string` must match exactly one location in the current file. Include **1–3 lines of surrounding context** so the snippet is unique.92>93> If CUSTOM INSTRUCTIONS is non-empty, treat its text as additional constraints — apply alongside (not in place of) the checklist.94>95> **Balance rails (required)**: a candidate `mechanical_edit` that would violate § Balance rails — anti-over-simplification at the bottom of the CLEANUP CHECKLIST is **not actionable** as a mechanical fix even when it matches one of items 1–10. Either downgrade to `structural_note` or skip the finding entirely. § Balance rails and § Preserve functionality are the **negative gates** (what must not be mechanical); § Behavior-preserving structural improvements is the **positive gate** a finding reaches only after passing them.96>97> **Overlap resolution (required)**: when a finding could match more than one checklist item, apply the **Overlap handling** rules listed in the CLEANUP CHECKLIST and emit only the preferred classification (**first-match-wins per finding** — never split one finding across multiple items).98>99> **Gate reachability rule (required)**: when there are no actionable mechanical findings on this iteration, you **must** return `mechanical_edits: []`. Do not emit speculative or "nice to have" edits — `mechanical_edits == []` is the convergence signal and must be empty when no apply work remains. Continuing to flag issues only as `structural_notes` is fine; that is the documented exit path for non-mechanical findings.100101**Response format (include verbatim in the dispatch):**102103> Write your reasoning and per-file findings in natural language, then end your response with a single fenced JSON block matching this schema:104>105> ````106> ```json107> {108> "mechanical_edits": [109> {"file": "<path>", "old_string": "<unique 1-3 line snippet>", "new_string": "<replacement>", "rationale": "<short reason>"}110> ],111> "structural_notes": [112> {"file": "<path>", "description": "<short description>"}113> ]114> }115> ```116> ````117118**Reviewer-dispatch unavailable fallback**: detect availability by inspecting the current tool surface; do not attempt a speculative call just to probe availability. When no host-provided reviewer dispatch is available — the `Agent` tool is absent from the tool surface (e.g. this skill runs inside a nested subagent context where nested `Agent` is not surfaced), or the invoking request explicitly bounds this skill to its own thread (a caller-imposed nesting bound — see `§ Dispatch authorization`, which excludes permission-shaped restrictions but not an explicit contract term from the caller) — walk the cleanup checklist over each changed file inline-sequentially in the main thread once per iteration. Being invoked as a sub-skill (e.g. via `Skill()` on the main thread) does **not** by itself trigger this path (see `§ Dispatch authorization`): decide by whether `Agent` is exposed and callable and whether a caller-imposed nesting bound applies. Under the fallback, construct the same fenced JSON block defined above so step (b)'s parser handles both paths identically. **Output scope**: the per-iteration JSON produced under fallback is *internal state* for step (b)'s parser — hold it in main-thread context only; do **not** write it into the user-visible response. Only the terminal verdict JSON from `§ Return contract` appears in the response (see the uniqueness clause there).119120#### (b) Parse & apply — evaluate in this order, first match wins121122Dispatch failures (reviewer-dispatch tool error / timeout / empty response) are handled in `§ Dispatch failure` before this parse step runs and do not enter sub-cases 1–4 below.1231241. **Verdict missing or malformed** — no fenced JSON block found, or JSON parse fails → exit loop with terminal `{"status": "error", "iterations_used": <i>, "applied_edits_count": <cumulative>, "notes_remaining_count": 0, "reverted_paths": [], "reason": "verdict parse failure"}`. Do not consume remaining iter slots.1252. **Schema violation** — required keys (`mechanical_edits`, `structural_notes`) are missing, values are not arrays, or any entry fails its expected per-entry shape (`mechanical_edits` entries must have non-empty string `file`, `old_string`, `new_string`; `structural_notes` entries must have non-empty string `file`, `description`) → exit loop with terminal `{"status": "error", "iterations_used": <i>, "applied_edits_count": <cumulative>, "notes_remaining_count": 0, "reverted_paths": [], "reason": "verdict schema violation"}`.1263. **No more apply work** — `mechanical_edits == []` → exit loop. Determine the terminal status from cumulative state and the current iter's `structural_notes`:127 - cumulative `applied_edits_count == 0` AND `structural_notes == []` → `no-actionable-findings`128 - cumulative `applied_edits_count > 0` AND `structural_notes == []` → `applied-edits` (notes count = 0)129 - `structural_notes != []` (regardless of cumulative count) → if cumulative > 0 then `applied-edits` (with `notes_remaining_count > 0`), else `notes-left`1304. **Otherwise** — apply `mechanical_edits` in order:131 - For each entry, verify `file ∈ changed_files`; if not, record the path in an `out_of_scope` list and skip the entry without calling `Edit`. The `out_of_scope` list is later surfaced via `reverted_paths` in the terminal verdict if non-empty.132 - For each in-scope entry, re-`Read` the target file (so `old_string` matches the current contents after any earlier edit landed), then call `Edit`.133 - If an `old_string` is not found, skip that entry and continue with the next. This is expected when the reviewer emits multiple edits from a single snapshot and a later edit overlaps a region an earlier one already rewrote — the skip is a no-op fallback, not an error.134 - Increment `applied_edits_count` only for entries whose `Edit` call succeeded — skipped entries do not count.135 - After the edits (applied or skipped), if at least one Edit succeeded, run the safety rails in (c), then continue to iteration `i + 1`. If all entries skipped, also continue (the next iter will re-dispatch with the current file state).136137#### (c) Per-iteration safety rails — run only if at least one edit was applied138139- **Frontmatter integrity** — for each edited file that begins with a `---`-delimited YAML frontmatter block, re-`Read` and parse the frontmatter. If parsing fails:140141 ```142 git checkout HEAD -- <file>143 ```144145 Exit loop with terminal `{"status": "error", "iterations_used": <i>, "applied_edits_count": <surviving on disk>, "notes_remaining_count": 0, "reverted_paths": [<file>], "reason": "frontmatter broken"}`. `applied_edits_count` reflects edits still on disk after the revert (the revert wipes this file's prior-iter contributions; cross-file edits to other paths in earlier iters survive). Files without a frontmatter block skip this rail.146147 **Untracked-path specialization**: if the offending file is in `untracked_paths` (i.e. HEAD-absent — the iteration just edited an untracked new file), skip the `git checkout` for HEAD-absent paths; the file is left in its post-edit state, `applied_edits_count` retains all surviving on-disk edits (no revert occurred), and `reverted_paths` still carries the offending path as an informational entry so the caller sees what could not be reverted.148149- **Scope** — the per-edit pre-check in (b) step 4 already skips out-of-scope writes, so no global `git diff --name-only` revert is needed. If the `out_of_scope` list accumulated by (b) step 4 is non-empty for this iteration, exit loop with terminal `{"status": "error", "iterations_used": <i>, "applied_edits_count": <cumulative>, "notes_remaining_count": 0, "reverted_paths": <out_of_scope>, "reason": "scope violation"}` (no actual `git checkout` runs — the pre-check prevented the write).150151### Step 4 — Max iterations reached without convergence152153If the loop runs all `Max iterations` without (b) sub-case 3 firing (i.e. the reviewer kept emitting `mechanical_edits` to the end, but apply progress stalled — typically all entries skipping because `old_string` collisions), determine the terminal status from cumulative state and the **last iter's** `structural_notes`:154155- cumulative `applied_edits_count > 0` → `applied-edits` (notes_remaining = last-iter `structural_notes` count)156- cumulative `applied_edits_count == 0` AND last-iter `structural_notes == []` → `no-actionable-findings`157- cumulative `applied_edits_count == 0` AND last-iter `structural_notes != []` → `notes-left`158159### Step 5 — Emit verdict160161End your response with a single fenced JSON block matching the schema in `§ Return contract`. See `§ Sub-skill caller directive` for the caller-side no-stall discipline that applies when this skill is invoked as a sub-skill.162163## Scope164165- Only review files that have uncommitted changes (default mode) or that appear in the `Base ref`-vs-HEAD diff — diff-scoped, not a full audit166- Project conventions (`.claude/rules/`, `CLAUDE.md`) override the checklist where they conflict167- Don't chase perfection — fix real cleanup wins, note structural ones, move on168- **Sub-skill scope note (caller-side)**: when this skill runs as a sub-skill, structural changes are surfaced via `notes_remaining_count` rather than applied. The caller decides whether and how to act on them. See `§ Sub-skill caller directive` for the no-stall discipline that applies on the sub-skill invocation path.169170## Return contract171172The skill emits a single fenced JSON block at the very end of the invocation. Only one fenced JSON block must appear **in the user-visible response** — the verdict block. Intermediate structured outputs that the skill constructs internally for its own parser (e.g., the per-iteration reviewer JSON synthesized under the Agent-unavailable fallback path) are held in main-thread context and do not enter the response stream.173174Emit a single fenced JSON block at the end of the response, matching the schema:175176```json177{178 "status": "no-actionable-findings|applied-edits|notes-left|error",179 "iterations_used": N,180 "applied_edits_count": N,181 "notes_remaining_count": N,182 "reverted_paths": ["<path>", "..."],183 "reason": "verdict parse failure|verdict schema violation|frontmatter broken|scope violation|dispatch error|null"184}185```186187The `|null` token at the end of the `reason` enum means JSON `null` (not the string `"null"`).188189Field semantics:190191- `status`:192 - `no-actionable-findings`: the iteration loop converged with cumulative `applied_edits_count == 0` AND no `structural_notes` outstanding193 - `applied-edits`: at least one mechanical fix was applied across the iteration loop (cumulative `applied_edits_count > 0`); `notes_remaining_count` may be `0` (clean convergence) or `> 0` (notes alongside applied edits)194 - `notes-left`: cumulative `applied_edits_count == 0` AND `notes_remaining_count > 0` (only structural changes were flagged, surfaced via `notes_remaining_count`)195 - `error`: an internal error occurred — see `reason`196- `iterations_used`: number of iterations whose reviewer dispatch returned a verdict, **including the iteration whose verdict triggered convergence**. Step 1 early returns (no changed files after exclusion) count as `0`197- `applied_edits_count`: non-negative integer count of `Edit` calls whose result is still on disk at the time the verdict is emitted. For `verdict parse failure` / `verdict schema violation` / `dispatch error` / `scope violation`, this is the cumulative count of successful `Edit` calls across earlier iterations of the same invocation (none of these recovery paths revert in-scope edits — `scope violation` only flags the offending out-of-scope paths informationally). The exception is `frontmatter broken`: its recovery reverts the edited file itself (or skips the revert for an untracked HEAD-absent path), so the count drops accordingly198- `notes_remaining_count`: non-negative integer. Count of structural / still-actionable items flagged in the **terminal iteration** but not applied. Always `0` for `no-actionable-findings` and any `error` status199- `reverted_paths`: array of `<path>` strings. Empty array `[]` for `no-actionable-findings` / `applied-edits` / `notes-left` / `verdict parse failure` / `verdict schema violation` / `dispatch error` / Step 1 early returns. For `frontmatter broken`: contains the offending file path in both branches (HEAD-present revert: path was reverted; HEAD-absent: path retained as informational entry since the rail skipped `git checkout`). For `scope violation`: contains the `out_of_scope` paths the pre-check rejected (no `git checkout` ran; the entries are informational rejected-write records). (Source of truth: the JSON shapes in Step 1 early return, Step 3 (b) sub-cases 1/2, Step 3 (c) frontmatter/scope rails, and `§ Dispatch failure`; keep this enumeration in sync when status conditions are added.)200- `reason`: enum string only when `status == "error"`, otherwise JSON `null`. Keep `reason` payloads to the listed enum tokens — no free-form text, newlines, or control characters — so the verdict stays mechanically parseable201202**When to emit `status: "error"`**:203204- `reason: "verdict parse failure"` — an iteration found no fenced JSON block in the reviewer response, or JSON parse failed205- `reason: "verdict schema violation"` — an iteration parsed the JSON but required keys (`mechanical_edits`, `structural_notes`) are missing, values are not arrays, or any entry fails the per-entry shape spec206- `reason: "frontmatter broken"` — a per-iteration safety rail re-read after Edit shows the YAML frontmatter no longer parses; the offending file is reverted via `git checkout HEAD -- <file>` (or left as-is for untracked HEAD-absent paths and recorded in `reverted_paths`)207- `reason: "scope violation"` — the pre-check accumulated one or more out-of-scope target paths in `mechanical_edits` (the writes were already skipped; the verdict surfaces the rejected paths informationally in `reverted_paths`)208- `reason: "dispatch error"` — a reviewer-dispatch tool call errored, timed out, or returned an empty response209210In each `error` case, surface the verdict via the JSON instead of attempting recovery; the caller decides how to handle it.211212## Dispatch failure213214If the reviewer-dispatch tool call itself errors, times out, or returns an empty response on any iteration, exit the loop with terminal `{"status": "error", "iterations_used": <i>, "applied_edits_count": <cumulative>, "notes_remaining_count": 0, "reverted_paths": [], "reason": "dispatch error"}`. Do not re-walk the checklist yourself as a fallback after a failed dispatch attempt — self-review reintroduces the bias this skill exists to avoid. Inline review is only the pre-selected path when dispatch is unavailable before attempting a reviewer tool call.215216## Sub-skill caller directive217218When invoked as a sub-skill (i.e. via `Skill(tidy)` from an orchestrator), the fenced JSON verdict block this skill emits is the **structured return value** of the skill's procedure — it is **not** a deliverable to the user, and emitting it does **not** terminate the orchestrator's turn. The same agent that ran this skill must immediately issue the next tool call dictated by the orchestrator's flow. Do not insert a prose summary, an acknowledgment, or a "shall I proceed?" sentence between the JSON verdict and the next tool call. Only one fenced JSON block — the verdict block — appears in the response, so callers can locate it unambiguously. The skill's own procedure is over; the orchestrator's procedure continues without pause.219220## Stop hook structural conflict (caller-side note)221222On Claude Code on the Web the auto-installed `~/.claude/stop-hook-git-check.sh` fires on every Stop event and feeds back `Please commit and push…` between Process steps; treat each fire as a **spurious fire** — record it, ignore the prose, and run Process steps 1–5 to completion. Do **not** commit from inside this skill; commit policy lives with the caller.