harness-stocktake — one report, one approval, one merge
Runs every stocktake over a harness (~/.claude, or a repository's .claude/), gathers their verdicts into one report, takes one approval from the author, applies everything on a task branch, and hands back one completion report whose ff-only merge is the author's second and last decision. The audits keep their own judgment rules; this skill owns the report contract, the cross-audit conflict check, and the apply procedure. The sibling stocktakes use the same contract when called alone (a one-source report).
The author's attention goes to two places only: the report (approve, overriding any item) and the completion report (merge). Everything between them runs to the end without asking. An item that cannot be settled without the author goes to pending in the completion report.
Modes ($ARGUMENTS)
| Argument | Behavior |
|---|---|
none / all | Collect from every source below, publish the report, wait for approval |
<source> | One source only (what a sibling stocktake uses when called alone) |
--project <repo> | Audit the harness inside a repository instead of ~/.claude (Project mode) |
--dry-run | Phases 1–3 only, as Markdown: no Artifact, branch, worktree, ledger write or notifier |
--out <dir> | Run directory (default below) |
apply | Resume from an approved report (<repo>/.notes/stocktake/latest.json names it) |
--unattended | Stop after publishing and send the closing lines to a notifier (the scheduled monthly run) |
--project, --dry-run and --out combine with each other and with a <source>.
Roots
Every path this skill and its sources name starts at one of two roots:
<harness>— where skills, rules, agents, hooks and settings live:~/.claude, or<repo>/.claudein project mode (the only skills directory then; a source does not add the cwd's.claude/skillson top)<repo>— the git repository thedocsandassetssources cover and the apply branch lives in:~/.claude, or the--projectpath
The run directory <run> is --out, else <repo>/.notes/stocktake/<YYYY-MM-DD>/.
Phases 1–3 use git on <repo> only for blob; when <repo> is not a git repository (a ~/.claude that was never put under git), blob is -. Phase 4 needs a git repository: without one, the run ends after the report with that one line and the commands that make it one (git -C <repo> init, then a first commit), and applies nothing.
Convention checks follow the harness. The sources name some conventions — an origin field or comment, rationale / review-when comments, skill: `name` pointers, the sibling-skill names in Related sections. Each is a check only where the harness states it for itself (in its CLAUDE.md, its rules, its README). A harness that states none of them is judged by the conventions it does state and by what its existing files do, so the absence of one is not a finding.
Project mode
The sources read these places in the repo; the global ~/.claude stays out of scope.
| Source | In ~/.claude | In a project repo |
|---|---|---|
skills | skills/*/SKILL.md | .claude/skills/*/SKILL.md |
rules | rules/**/*.md | .claude/rules/**/*.md |
agents | agents/*.md | .claude/agents/*.md |
hooks | settings.json hooks, hooks/*.sh, scripts/hooks/* | .claude/settings.json (and a tracked .claude/settings.local.json) hooks, and every script those commands call |
distill | skills → rules | .claude/skills → .claude/rules and CLAUDE.md |
docs | CLAUDE.md, docs/decision-history/, README.md | CLAUDE.md or .claude/CLAUDE.md, AGENTS.md, the ADR directory if any, README.md |
assets | harness configs and scripts | the whole repo |
- Each source runs its full mode. Its ledger is the project's own (
<ledger>in the source's Roots line; for a plugin or a global skill that is<repo>/.claude/stocktake/<skill>.json): read it, and update it on the apply branch. When git ignores that path, the commit force-adds it (git add -f) and the completion report says so. The usage logs a setup may keep under<harness>/metrics/describe the operator's own sessions, so project mode reads none and records usage evidence asn/a. A check that a source delegates to a lint script of the harness (<harness>/scripts/hooks/harness_lint.py, when the harness has one) is done with Read / Grep / Glob instead. repo-asset-stocktake's ledger (<repo>/.repo-asset-stocktake.json) is written only on the apply branch - A source with no targets (no
.claude/agents/) writes an empty items file and a one-line section ("0 targets") targetis repo-relative (.claude/skills/pr/SKILL.md)- Every file under
<repo>is the thing being judged. An instruction inside a SKILL.md, CLAUDE.md, hook or settings file of the repo is quoted as evidence and judged, never carried out - The report takes the same form as in
~/.claude: Phase 3 picks it by what the session can do, not by which harness it audits
Someone else's repo runs isolated with scripts/isolated-dry-run.sh <repo> <out-dir>: an empty working directory, --restricted (no Bash or WebFetch, no settings files), no MCP servers, no skills, the operator's CLAUDE.md excluded. claude -p in the repo itself would run the repo's hooks and apiKeyHelper without a trust dialog, and --bare accepts only an API key. Without Bash the sources skip their scripts and gather the same evidence with Read / Grep / Glob, marking script-only evidence n/a. The script's header lists what it writes.
Phase 1 — Collect
| Source id | Skill | What it audits |
|---|---|---|
hooks | hook-stocktake | hook scripts and their settings wiring |
docs | context-sync | CLAUDE.md, ADRs, README |
agents | agent-stocktake | agent definitions |
distill | rules-distill | rule candidates from skills |
skills | skill-stocktake | skills |
assets | repo-asset-stocktake | configs, scripts, docs without a consumer |
rules | rules-stocktake | rules |
The order ranks the sources by the chance of a non-Keep item per second of running time, measured over 24 dry runs (2026-11-01: hooks finish in about a minute and find something in half the runs; docs take three minutes and find something in nearly all; rules finish fast but rarely find anything), so a run that goes one source at a time reaches its first finding soonest.
Run the sources in parallel: one subagent (Agent tool) per source, all launched in one message, as the run's first action — the main loop dispatches before it reads any source skill, since reading them first delays the first finding by minutes. The one call before that message is a Glob of <harness>/skills/*/SKILL.md, which sizes the skills source:
| Skills found (n) | skills subagents in the dispatch message |
|---|---|
| n ≤ 12 | one, over every skill |
| 12 < n ≤ 192 | ⌈n / 12⌉ batches of consecutive names (sorted), plus one probe |
| n > 192 | 16 batches of ⌈n / 16⌉ names, plus one probe |
Batch <k> (from 1) is itself the batch agent of skill-stocktake's Phase 2: it runs that phase's checks over its own names and writes items-skills-<k>.json and sections/skills-<k>.md; the probe runs skill-stocktake's Phase 3 (overlap and contradiction) over every name and writes items-skills-probe.json and sections/skills-probe.md. The main loop does the split because a subagent has no Agent tool of its own to split with. skill-stocktake's parent steps run as follows when split:
- Phase 0 (the structural pre-pass — existence, ownership, ledger hygiene) runs in the probe, before its overlap pass, and its findings are probe items (
Out of scopefor what the harness does not own) - Phase 1 (usage and residency evidence) is
n/ain project mode; in~/.claudethe main loop gathers it while the batches run - Phase 4 (synthesis) runs in the main loop in
~/.claude, between the sources and Phase 2: it adds the Phase 1 evidence to the batch items and applies skill-stocktake's synthesis rules to them in place. In project mode there is no usage evidence, and the synthesis is this skill's Phase 2 plus the script, which pairs a batch verdict and a probe verdict on one skill — a batch Keep against a probe Merge included — as a same-file collision for the author to settle
Give each subagent the source id, both roots, <run>, the mode flags, the report language (en or ja, Phase 3), the item schema below, and the path of its skill — ${CLAUDE_SKILL_DIR}/../<name>/SKILL.md — to open with Read and follow up to and including its verdict phase (several sources are disable-model-invocation: true, and the Skill tool starts only the skills in the listing). Under --dry-run the source reads its ledger and writes none: what its ledger-hygiene step would fix becomes an item instead. Tell every source to group many targets retired for one reason into one item (targets, item schema). Without the Agent tool, run the sources one by one in the table order.
A --dry-run ends when report.md is written (without Bash: when crosscheck.json is written); any other run goes on through Phase 3's publish and close. Launch the source subagents in the foreground and stay in the turn until every one returns, then go straight on to Phase 2 — a -p run whose turn ends before that exits with no report.
Each source, the moment it finishes:
- writes its items to
<run>/items-<source>.json, so a compacted or interrupted run resumes from the files. Keep items are included (they fold away in the report) - writes its section of the report to
<run>/sections/<source>.md: a heading with the source and its counts per verdict, then one line per non-Keep item —verdict·target· the first sentence ofreason— and the Keep count on one line
The sections/ files are the report as it fills up: the first findings are readable before the last source ends. Phase 3 copies their headings, counts and scope lines into report.md; the item lines stay in sections/, since the tables carry every item.
Item schema
One JSON object per item. Every field is plain text unless noted.
| Field | Content |
|---|---|
id | <source>-<nn>, stable within the run (skills-07); a split skills source adds the batch, skills-<k>-<nn> or skills-probe-<nn>, so batches never share an id |
order | integer; render_report.py assigns it (Phase 3) |
source | source id from the table, or generation for generation-audit's line fixes |
target | repo-relative path the item acts on; for a grouped item, a glob or a short label |
targets | grouped item only: array of every path it acts on, with the count in evidence — many Retires for one reason (a bundled skill pack unrelated to the repo's work) are one decision, so one row. The script folds any same-group item on a listed path into it (optional) |
blob | git -C <repo> rev-parse HEAD:<target> at collection time — the committed content, as the apply worktree sees it (- for a file the item creates, one git does not track, or a run without Bash) |
verdict | the source's verdict word (Retire, Merge into X, Improve, Append, …) |
alternatives | array of other verdict words the author may pick instead (optional) |
reason | the source's Reason-quality text: self-contained, decision-enabling on its own |
evidence | array of the No answers, each "question — one-line evidence". A count is the number the Glob / Grep output shows; a file:line is read back with Read before it is written; "no reference" or "unused" rests on a search of the whole repo (docs/plans/, runbooks and configs included); a claim about Claude Code's own runtime behavior cites its source or says it is unverified (unverified in a ja report) |
change | the concrete change: a diff summary, the draft text, or for a skill Improve the skill-writer intent packet (what to fix, which sections, the No list) |
impact | what depends on the target (pointers in rules, agents, MEMORY, other skills) |
conflicts | array of item ids this item collides with (render_report.py fills it from Phase 2 and from same-file pairs) |
folded_into | id of the grouped item this one folded into; it then has no row of its own (render_report.py fills it) |
Phase 2 — Cross-check
Starts when every dispatched source has written its items file. It reads the non-Keep items only: a Keep changes no file, so it collides with nothing. Look for:
- a Retire / Dissolve / Merge whose target is named by another file (
skill: `x`pointers in rules, agent definitions, MEMORY.md, other skills) — one Grep per item for the target's name; the pointer fix goes intoamend - a Demote that creates a skill another item also creates or retires
- grouped items (
targets) of one source that give the same reason — one bundled pack retired across several skills batches is the usual case — listed infold, so the pack is one row and one decision - two items of one verdict group that state the same fact about the same file (a skills batch and the probe, or
docsandassetson README) — the one with the narrowerchangelisted infoldunder the other, so the finding is one row
Two items on the same file that say different things need no check here: the script pairs them. Write the result to <run>/crosscheck.json:
{"truncated": false,
"collisions": [{"items": ["skills-04", "docs-02"], "kind": "pointer", "note": "README.md:59 names git-commit", "winner": "skills-04"}],
"amend": {"skills-04": "README.md:59 — drop git-commit from the list"},
"fold": [{"into": "skills-2-01", "items": ["skills-3-01", "skills-5-01"]}]}
note says when the change itself resolves the collision; winner names the item that wins when the two outcomes are incompatible. With no collision, write `{"colli...