Tidy

SkillAI & models

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.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Tidy skill

What this skill tells your AI

The instructions your AI receives, as published by hiroro-work/claude-plugins in skills/tidy/SKILL.md and read by ahel’s review.

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 stops the loop.

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)

  1. 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.
  2. 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).
  3. 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
  4. 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.
  5. 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:

  1. Read the file's full current contents. Hold in main-thread context as the iter-1 snapshot.
  2. Read references/cleanup-checklist.md.
  3. 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 essential 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 provided): 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 applied 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 — they are counted in 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.

  1. 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.
  2. 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"}.
  3. 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
  4. 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 listed in 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 was applied), 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 counted in 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:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
47
Forks
3
Last commit
Sep 2026
Advanced
Item type
skill
Key
tidy-hiroro-work
Source
github.com/hiroro-work/claude-plugins