Sysop a monograph Preface Audience Day one The arc I · Lifecycle II · Parallel III · Quality IV · Convention V · Hooks
Sysop · a self-improving workflow for Claude Code

The workflow
gets quieter
over time.

Twenty-three skills that carry a project from a brain-dump to a merged PR — and beyond it, to the release notes — turning every code review into a permanent convention along the way. Extracted from a real production codebase, dogfooded one phase at a time, and refusing to forget what it has already learned.

23
Skills
431
Phases shipped through
78
Promoted conventions
2
Install paths

Why this page
exists.

Sysop is a self-improving development workflow for Claude Code. Its core idea is simple: when a code review keeps catching the same mistake, that finding stops being one-off feedback and becomes a written convention the workflow consults automatically on every future task. To be precise about the term: promoting a finding to a convention is a human judgment call — what's self-improving is the enforcement, which from that point on is automatic and permanent. Reviews get cheaper over time because the workflow stops re-finding what it already knows. So far that loop has distilled nearly 3,300 review findings into 78 enforceable conventions, all from one project's five-month review archive — and § IV shows what that archive measures, and, just as carefully, what it can't.

I built it to solve a problem. I was shipping a production app solo, generating far more code than I could review line by line. So instead of inspecting every line, I built a system where the review is structural: deterministic gates plus adversarial plan review. Agents, like staff, make mistakes — so you need a process that catches them when the code is written and again before it deploys. Adversarial plan review is handing a colleague your spec and asking them to tear apart your assumptions — a review rigorous enough to come back with the line numbers that need to change. Recurring defects get rarer by design, not because I'm eyeballing every line.

The instinct comes from running a team that shipped real systems: analysts and economists building SQL and front-end tools, plus contract developers. You can't read every line your people write, so you put a process in place that lets you ship anyway.

There's a quieter design constraint underneath all of it: the context window. It turned out to be the most precious resource in this whole system. Even when you stay comfortably inside the limits, an instruction given early in a session has to compete with everything generated since. The model predicts one token at a time, and the more context that has piled up between that instruction and the work happening now, the less it weighs on what comes next. The rule is still in the window; it's just too far from the work to carry much signal. Piling every rule into one standing document makes this worse, not better.

So the workflow is built to nurture the window rather than fill it. Conventions live in glob-keyed maps, and only the entries matching the files a task actually touches get pulled in, at the two moments they matter: during planning and after implementation. Skills load when invoked, not as permanent preamble. Hooks inject their guidance at the instant of the event they handle, not a thousand turns earlier. The principle throughout: don't tell the model everything up front and hope it remembers. Hand it the applicable instruction when and where it applies, so it's actually applied.

What it is,
and why it matters.

Most teams treat code review as a checkpoint — gate-keep the PR, ship it, move on. Sysop treats every review as a data point. When the same finding recurs across PRs, it gets promoted from "feedback on this change" to a written convention that future planning and implementation steps consult automatically.

The shape

A planning front door — /intake (Phase 60) — turns a brain-dump into a populated, validated tasks/index.yml. From there, four lifecycle skills — /next-task, /claim-task, /document-work, /review-close — carry a single task from queued to merged. Two quality skills (/codebase-review, /security-audit) generate work back into the queue; a third (/test-audit) assesses the test suite's standing health. One orchestrator (/auto-build) fans the same lifecycle across N tasks in parallel. A read-side pair answers a project's two standing questions — /roadmap (what's left, in what order) and /daily-summary (what happened) — and /release bundles the merges since the last tag into a changelog entry, a semver bump, and an annotated tag. Two hooks (PermissionDenied, SubagentStop) handle the boring mechanics so the skills can stay short.

The differentiator

Per-pack convention_map.md files are the persistent memory of the workflow. Each entry traces back to the review round where the pattern was first observed. /claim-task consults them before planning and after implementation; /codebase-review uses them as the rubric. Promotion into the map is a human decision; everything after it is enforcement. And the loop reaches past one repo. /contribute-convention takes a locally-promoted convention, strips your project's fingerprints down to placeholder vocabulary, and files it upstream as a pack proposal — one of three give-back channels, alongside /report-issues (friction that became a fix) and /share-wins (what worked well enough to protect from a future change). Where all three file is your call, not the tool's: they resolve a target from § Sysop upstream repo in your own CLAUDE.md, and the shipped default is the public Sysop repo — which is worth knowing before a friction log quoting your production context goes anywhere. They probe the resolved target's visibility and say so before filing, but the check warns; it never redacts, and per-item consent stays the actual gate.

That give-back is the reason the project is open. Every pack convention is a documented, generalized failure mode of an AI coding agent — earned from recurring findings on a real project, not drafted from a style guide — so the corpus can compound across consumers in a way a closed tool structurally can't offer. And because convention prose is read by review agents in privileged contexts, contributions land as issues, never auto-merged: a maintainer authors every map change under a published trust policy (CONTRIBUTING.md). The floor is shared; the curation is not crowdsourced.

What each part is trusted with

The design rule under all of it: where an outcome can be checked, the workflow checks it rather than taking the agent's word for it. An agent reports completion in the same confident register whether or not the work ran. This page's own record has author batteries reporting every mutation killed while an independent battery found survivors (§ I, What that doesn't buy); the close step reads a task's written test decision back against its actual diff rather than accepting it; a claim lock is written by a script rather than by an agent, so it survives a run that failed to follow the orchestration. So responsibilities are divided by what can verify them, and a claim about work gets a second source wherever one is possible.

Interpret and judge
A reasoning model: reading a requirement, planning the change, deciding what a finding means. Probabilistic by nature, so it is never the only witness to its own output.
Produce the change
A coding agent, in its own worktree, against the conventions that match the files it touches.
Critique
An independent agent in a fresh context — adversarial review of the plan before code, of the change before merge — and never the agent that wrote it.
Observe a mechanical condition
A deterministic tool: the check registry, the language server, the diff-coverage gate, lock files and lifecycle markers written by scripts, and the git state itself. The same answer on every run, no model in the loop.
Accept a risk
The human — at the merge gate, at every promotion of a finding to a convention, and on any decision that is consequential or hard to reverse.

What that buys is not correctness: nothing here guarantees generated code is right. It is that the human's attention is spent where no tool can stand in for it, and that every other claim has somewhere to be checked.

Three readers,
and one who should close the tab.

Sysop isn't a productivity skin you sprinkle on any repo. It pays for itself in three specific situations, and asks more than it's worth in a fourth. Name yours before going further.

You, shipping a real product alone. The case the workflow was built for. You're a competent engineer producing more code than you can review line by line, and you can't add reviewers. The review has to become structural — deterministic gates, written conventions, adversarial plan critique — instead of eyeball-by-eyeball. The compounding payoff (§ IV) is the reason a solo builder can hold the line on quality at team velocity.

A team shipping via agentic tools. Agents generate diffs faster than reviewers can read them. The workflow puts gates between generation and merge — convention checks, plan critique, batched cleanup — so autonomous code doesn't reach main unread. The value here is risk posture as much as throughput: same speed, fewer of the predictable failure modes (schema drift, naive auth, silent data loss, half-applied fixes) sneaking through. One boundary to name up front: the coordination Sysop provides is between parallel sessions — locks and worktrees keep concurrent agent work from colliding — under a single human reviewer. Two people sharing one queue (assignment, review handoff, who runs /review-close) is out of scope today: a deliberate non-goal, not an oversight.

A builder growing into the judgment the workflow assumes. Sysop's gates ask for calls — approve this plan, promote this convention, accept this risk — that presume review experience. Guided mode (Phase 76; opt-in via one section in your project's CLAUDE.md) re-shapes those gates for someone still building that judgment: the skill states the decision plainly, stress-tests its own recommendation, and hands you only the calls that are genuinely yours. An honest tradeoff gets pros and cons; a false choice — a security hole dressed as an option — gets collapsed, not offered as a peer; a call you couldn't fairly weigh gets a conservative default and a note explaining why. The review bar doesn't move — the deterministic gates are unchanged. What changes is how much of the reasoning you get to see.

Not a fit: anything you can hold in your head. Side projects, quick scripts, prototypes, weekend tooling — anything where reviewing your own diff costs less than running the process. Sysop assumes the codebase is big enough that structural review pays for itself. If it's small enough to keep in your head, you don't need this; the overhead won't earn its keep.

Before any of this
runs, you need a queue.

Everything in this document — the lifecycle, the parallel orchestrator, the convention loop — assumes a populated tasks/index.yml. For most of Sysop's life you filed that by hand. Phase 60 added /intake: a planning front door that runs the brain-dump-to-queue loop with you, writes the tasks, and validates them — while leaving every judgment call that matters to you.

Ask it more than you think. The strongest move most people miss with these tools is using them upstream of code: as a sounding board for scope, business model, tech stack, schema, pricing — the whole shape of the thing before any of it gets built. The trick is how you ask. Propose an idea and tell the LLM to tear it apart. Read the critique, keep what's real, drop what's reflexive, decide. The LLM can pressure-test the structure of an idea; it can't tell you whether the idea is worth building. That judgment is yours, and the workflow doesn't pretend otherwise — /intake is built around exactly this boundary: it argues its own decomposition calls, but it will not adjudicate whether the venture is worth pursuing. The same adversarial primitive the workflow applies to plans (§ I) you apply to the premise, one layer up.

What /intake does. A few rich turns, not a wizard. You brain-dump; it plays the idea back so you can catch where it misheard you; it acts as a sounding board, arguing the weak spots; then it proposes phases, sorts them by priority, and emits a phase-one slice into tasks/index.yml with body files alongside. Decomposition follows a calibrated rubric (_shared/decomposition-rubric.md, Phase 60b) — one-sentence done-conditions, effort kept independent of blast_radius, depends_on reserved for physical impossibility. The skill is re-enterable: later phases stay coarse until you come back to sharpen them.

Two legs of done. The floor is mechanical — validate_tasks.py enforces the schema (on demand, or wired into pre-commit as an opt-in), so a malformed queue is caught mechanically rather than by eye. The ceiling is human — you read the slice and confirm it's the work you meant. Intent that should outlive a single session lives in two optional, consumer-owned files at the tasks/ root: vision.md (what this is) and decisions.md (what you've ratified and why), so the next /intake pass re-checks itself against them instead of relitigating settled calls.

And when the project predates Sysop entirely — code shipped, a ROADMAP.md or an issue tracker full of backlog, no intent layer — /onboard (Phase 88) runs the adoption: with consent it reads the repo's own evidence and drafts vision.md + decisions.md for you to confirm (observations recorded as fact; inferred rationales explicitly marked, never asserted), and/or imports the existing backlog — a roadmap file or open GitHub issues — into the queue, each task provenance-marked surfaced_by: [imported] so downstream batching treats its effort numbers as the archaeological guesses they are — then hands off to /intake for everything after.

Between planning sessions, single tasks arrive constantly — a bug spotted in a screenshot, an idea mid-review. /add-task (Phase 89) is the thirty-second capture for that moment: one thought in, a validated, deduplicated queue entry out, uncommitted for your sign-off. It never creates phases or touches the intent layer — a phase-shaped thought gets routed back to /intake.

Addendum (2026-09-13). A queue answers what should be worked on. It turned out to be silent on a question that arises constantly and had been resolved ad hoc every time: what to do with the thing you notice while working on something else. Left unstated, it resolves badly in both directions — either everything adjacent gets fixed in the branch, and the change under review quietly stops being the change that was reviewed, or everything gets filed, and the queue fills with one-line corrections nobody will ever schedule. So the decision was written down as three tiers, in the three places an executor actually reads. Fix it here when the file is already in the brief, the fix is mechanical or a documentation correction, a check covers it or you add the one that does, it is small, and it is not a claim about what code means that you have not verified by reading it. Extend an open task in that area. File a new one only past both.

Two details carry the weight. The first is the never-list, and its two halves are built differently on purpose. Most of it is an ordinary enumeration — migrations, prompt files, money-path and auth code, anything that writes to production, and whatever the project's own security-critical file list names. The last member is deliberately not a list: any change that would weaken, disarm, narrow or delete a gate, whatever file it lives in. It is written as a property of the change rather than a lookup because an enumeration rots, and because the tier's own “a check covers it” test is satisfied by the disarming edit itself — the kind of loop a rule has to close explicitly or it will be walked. The second is that a tier-1 fix is declared: the task body gains an ## Also fixed section, and at close every path named there is checked against the branch's own diff, with no line allowed to name something on the never-list. An absent section is never a finding — most branches fix nothing extra — so the check grades what was claimed, not whether anything was claimed.

Tier 3 then needed a bar of its own, because “file it” is the tier that looks free and is not: a filing costs a reader, and a queue of filings nobody can rank is the failure mode the tiers were written to avoid. A new task must name what it blocks — a phase in progress, a planned one, a declared gate, or an open task's stated acceptance — with four kinds filed regardless of that test, because they are not the session's call to make: a design question, anything needing a human action, anything touching a production write, and a falsifiable defect in shipped behaviour or a security finding. Everything else goes to a flat one-line-per-entry tasks/notes.md beside the queue, which nothing routes off and nothing counts. The installer never creates that file; whoever writes the first note creates it, which is protection by absence rather than another thing to configure. Promoting a note is the one carve-out to /add-task's otherwise append-only rule: it files the task and deletes the line in the same run, so a thought cannot be live in two places at once.

Addendum (2026-09-23). That day's re-measurement reversed the default, and this paragraph supersedes the three above. The bound kept a large middle group of real, small findings out of the branch, and the filing bar then sent most of them to the notes file, which nothing reads. In one consumer the open queue grew from 316 to 341 open tasks in sixteen days while 94 closed, at close to three findings per close: the routing moved work between piles without shrinking any of them. Close-time review, meanwhile, caught nearly every in-branch fix that went wrong before it merged. So fixing is now the default. A finding the agent can fix is fixed in the branch, in any module and past a one-liner, as long as the close can still review it, and it is declared under ## Also fixed as before. The never-list shrank to migrations, production writes, and auth or payment logic. A finding that cannot be fixed now becomes a task, and one that is not actually wrong is dropped. When an answer from the human would make a finding fixable, an interactive /document-work, or /review-close for its reviewers' notes, asks first and fixes it on the answer. A subagent executor or an unattended run files the task with the question written in. A /claim-task executor also lists its questions, and /claim-task asks them itself on option A or a --resume, with a second executor fixing the ones answered fix in the same branch (Phase 345). The notes file is retired, and the measure is net open work per close rather than filings per close.

Three phases of a
project's life.

You don't use Sysop once. You use it across the arc of a project — plan, execute, maintain — and the value compounds across all three.

The arc is also the map — for this document, and for every shorter telling of Sysop. Here, Day one is plan; the lifecycle (§ I) and the orchestrator (§ II) are execute; the quality skills (§ III) and the convention loop (§ IV) are maintain; the hooks (§ V) are plumbing under all three. The landing page's six lifecycle moves nest inside the same three phases, and the hands-on tutorial walks the concrete path through the first two — /intake to plan, then /claim-task → /document-work → /review-close to execute one task. Wherever you meet the workflow, you're looking at this arc at a different altitude.

PLAN what gets built — /intake · brain-dump → queue — /roadmap · order of attack — a populated, validated queue tasks/index.yml tasks/open/<ID>.md EXECUTE how it gets built — single: /next-task → close — parallel: /auto-build batch — convention checks gate both slower than reckless — faster than reckless + fallout MAINTAIN what stays clean — /codebase-review · /security-audit — /test-audit · /pr-dependabot — /auto-fix · /auto-judge review_tasks.md parallel batch · --merge sequential new findings feed back into the queue — the cycle continues
Fig. 1 The arc of a project. The maintenance loop returns new work to execution; the workflow repeats with sharper conventions each time.
Planyou · spec'd intent
The judgment Sysop can't replace. You carry the product intent, the scope, the priorities; the queue is the artifact those choices produce. See Day one for the procedure.
Executeopus · sonnet · haiku
The lifecycle (§ I) runs single tasks; the orchestrator (§ II) runs batches. Both consult the convention_map.md (§ IV) before and after every implementation, so the gates aren't friction — they're how the workflow ships clean without a human reading every line.
Maintainopus + parallel agents
Periodic /codebase-review and /security-audit generate batches in review_tasks.md. /auto-fix handles mechanical fixes in parallel (default 4 agents); /auto-judge handles flagged batches with reasoning-tier judgment. Both accept a --merge flag that runs overlapping batches sequentially — same-file work waits its turn so the second batch sees the first batch's fixes already in place. /pr-dependabot (§ III) handles dependency upkeep on the same cadence, and /test-audit assesses the standing health of the test suite itself — coverage the diff-scoped gates never look at, and existing tests worth retiring.
Report & releaseread-side · person-gated tag
The arc's backward glance. /daily-summary reads the git log and the queue and answers what happened — yesterday in detail, the week in shape. /roadmap answers what's left and in what order, computing up to four orderings from the signals in both queues — roadmap tasks and review batches alike, so open review work is surfaced in the same view as feature work rather than in a second queue nobody sequences. One honest limit, stated in the skill and worth repeating here: the portable read path reads whichever tree it is invoked in, so a run from a task worktree reports that branch's queues, not the main checkout's. When enough has merged, /release bundles the commits since the last tag into a proposed semver bump, a changelog entry, and an annotated tag — dry-run by default, person-gated like every merge.

One task, four skills,
one merge.

The happy path for a developer working on a single feature or fix. Each skill hands off to the next via the filesystem (tasks/index.yml, sysop/runtime/locks/, sysop/runtime/pending-docs/) so any step can be re-run independently.

tasks/index.yml — source of truth for the queue — /next-task role · quick (haiku) pick claimable /claim-task role · reasoning (opus) lock + plan + review + execute /document-work role · reasoning (opus) commit + pending-doc /review-close role · reasoning (opus) verify + merge + archive three cold sub-agents planner → reviewer → executor the orchestrator classifies · phase 171 sysop/runtime/pending-docs/ handoff to senior review sysop/runtime/locks/ prevents double-claim
Fig. 2 The single-task lifecycle. Solid arrows: skill handoff. Dashed: filesystem coordination. Skills declare roles — reasoning, planner, plan-review, executor, convention-gate, security-gate, mechanical, quick — not models (Phase 69); the parenthesized model is the default mapping, remappable per project (§ VI). Since Phase 349 every skill carries one (a pinned model that is unavailable still falls back to the session model).
/next-taskhaiku · selection
Single claimable task with effort + blockers. Anchored on current_focus: true, ranked unblocker-first — how many other tasks it unblocks, then effort ascending, then id (Phase 74). --avoid-inflight prepends a collision tier, so an exact-path clash with work already running sorts out of the way even ahead of an unblocker (Phase 103).
/claim-taskopus · orchestration
Adjudicates the claim before it creates anything — --entry-state answers claimable / resumable / held / closed / absent, and resumable stops and asks rather than resuming (Phase 159b). Then creates lock + worktree and orchestrates — it never implements, never enters plan mode, and holds no Edit tool: a planner writes the plan, an independent reviewer that did not write it looks for what is wrong, the orchestrator classifies those findings itself, and an executor implements, verifies and commits. Three cold contexts, four when the human answers an executor's question fix (Phase 345), and a file left behind at each seam in sysop/runtime/claim/<id>/<run>/ — the pre-plan SHAs, the plan, the planner's integrity check, the review, the classification, the outcome, the executor's questions and the human's answers, and on option C a marker recording that the plan was written back and the claim released — so a review that ran and a review that was skipped stop producing identical evidence (Phase 171). The reviewed plan itself goes somewhere else on purpose: into the task body in the main checkout, the one write in the skill that is deliberately not a worktree write. Those files are not just a record: --resume reads the stage to re-enter off them, which is why the set grew past the three the reshape first specified. How much you are in the loop is settled before anything is claimed — ## Plan review in your CLAUDE.md, or the --review-plan / --no-review-plan / --plan-only flags — and resolves to one of three options: A puts a human gate between the review and the executor, B runs straight through, and C (--plan-only, Phase 238) stops after the review, writes the reviewed plan and its sealed verdict back into the task body, and releases the claim — the review is the deliverable there, since nothing is implemented. The reviewer runs on all three; only what follows differs. C is roadmap-only and refused up front for a review batch, which has no body file to persist a plan into. A run that cannot finish parks rather than unwinding: a marker under sysop/runtime/parked/ names the reason, the branch, and the --resume <RUN_ID> line that re-enters it — and the stage to re-enter is read off the artifacts on disk rather than remembered, which is what lets the same claimant pick the run back up. A run whose record never reached the branch does not hand off (Phase 249): at Step 8 a deterministic probe reads the branch tip — not the worktree, and not the main checkout, both of which can hold a ## Test decision the branch does not — and halts the claim before /document-work when the record is missing or still the schema template. The remedy is to write the decision 7a already made; composing a fresh one from the diff is forbidden there, because that substitutes an unreviewed judgment for a reviewed one. The arm that matters is the orchestrator's: the failure being closed is an executor skipping a sequence item, and the remedy for that cannot be another sequence item in the same prompt for the same agent.
/document-workopus · narration
Boy-scout simplify pass, follow-up-stub HARD-FAIL gate, then writes sysop/runtime/pending-docs/<branch>.md for senior review (or a numbered file beside it while a close is holding that one back). The doc's frontmatter records the branch_tip: it was written against, stamped last, after the branch's final commit (Phase 282) — which is what lets the close path tell a current narration from one the branch has since moved past.
/review-closeopus · gate
Worktree-state classification; project pre-merge gates, run twice on two trees — a cheap fail-fast before the merges and the authoritative one after them, on the assembled result, because a gate that runs before the merge can only ever describe the tree it stood on (Phase 170) — including a crown-jewel coverage gate (Phase 61) that fails the merge when a critical path loses test coverage, with no baseline escape hatch; a verify-the-record check (Phase 59) that reads back the task's ## Test decision and confirms it against the actual diff; the manual-smoke gate; the merge; archive the task; clean up branches. How it merges is the project's call — § Merge policy in your CLAUDE.md selects direct (push main) or pr, which never writes main directly — work routes through an integration branch and one squash PR, making GitHub the sole serialized writer (Phase 63; PR-state-as-verdict and PR reuse, Phase 151). It also reports what each claim left behind (Step 2e, Phase 237): for every branch under review it prints which of plan.md, planner-integrity.md, review.md, classification.md and outcome.md exist, plus any hook envelopes, resolving the branch to a claim id from the lock rather than the branch name. Five, not the three the pipeline first shipped — planner-integrity.md and outcome.md are what answer was the plan re-gated? and did the executor already run? An /auto-build cycle is judged against the three files it writes — plan.md, classification.md and revised-plan.md — so its absent list names only those. Step 7-pre's resume table routes on nine, and four of them are the gap worth knowing about: option C's marker, which tells a re-entry the run is finished, and the three that record the executor's questions and the human's answers (Phase 345). Step 2e reports none of the four. That report never moves a verdict — a rejecting form of it was built and disqualified by its own round — and an absent artifact renders as unknown, never as “did not run”, because sysop/runtime/ is gitignored and the hook is Claude Code only. At close it reaps them (Phase 236): Step 4c removes a roadmap claim's artifact directory, lock and park marker; close_batch.sh's remove_claim_artifacts() does the batch half, sited there because Step 4c's id list is built from roadmap_ids alone and no batch id ever reaches it. Only the batch half waits for the close to land on main: under the pr policy Step 4c runs on the integration branch before the PR merges, so a roadmap claim's directory and park marker are already gone if that merge never lands, and its lock too unless the close is a --request-review hand-off, which keeps locks until it lands. The promoted checks now run here too (Phase 247). Until then the checks.yml registry, the Semgrep rules, the language-server and coverage stages ran at audit time and nowhere else — which is after the branch that violated one had already merged, so the violation surfaced on the next review round as a new-vs-baseline finding. The pre-scan runs at 4a-post against the assembled merge target, in addition to whatever your ## Pre-merge verification section names rather than as one more source in a chain that stops at the first match — offering it as a fourth source would have made it invisible to exactly the consumers whose CLAUDE.md an --update preserves. It de-duplicates on whether a resolved command actually reaches the pre-scan — directly or through a project wrapper of your own — and deliberately not on whether the command mentions run_checks.sh, because the one consumer already enforcing these checks at its merge gate does it through a wrapper, which a name match would miss while charging you a second whole-tree scan. A flagless invocation does not de-duplicate at all: without --fail-on-blocking the scan reports and never refuses. Flags never enter the decision otherwise — a consumer who narrowed the scan did it on purpose. The merge gate names the ref it is landing in (Phase 248): close_batch.sh --merge-target <ref>, with the operand-free default read from § Merge policy — main under direct, and under pr deliberately unresolved, so a hand-run without the flag is refused in a message that names the flag rather than guessing at a target. Identity is compared by resolved ref name, not SHA: an --ff-only merge legitimately leaves target and branch sharing a SHA, which is why a SHA test refused correctly-merged work on the dominant single-branch shape. Two Phase 249 corrections ride along: the worktree classification identifies the primary checkout by device and inode against --git-common-dir rather than by asking which worktree is on main — the same worktree only while the primary happens to be on main — and the verify-the-record check gains a fourth disposition, record not owed, which is withheld whenever a claim lock says the record was owed — the lock is what stops the new disposition becoming a quieter waiver for exactly the misses the Step 8 read-back exists to surface. A lock is written by a script rather than an agent, so it survives a run that failed to follow the orchestration; its absence stays cannot tell rather than proof, because a release removes locks too, and only then is the disposition offered at all.

Addendum (2026-09-13). The narration a branch hands to review is a claim about a tree, and nothing had ever checked that the tree still looked like that. /document-work writes the pending doc; commits then keep landing on the branch, because review found something or the author kept working. The doc now carries the revision it was written against, stamped after the branch's last commit, and the close path counts the commits that arrived since. Where any did, it refuses to collect: nothing is moved, the main checkout is left exactly as it was, and the branch is skipped with its worktree intact so the narration can simply be re-run. Where the stamp is missing or unreadable — an older doc, a hand-cut branch — the doc is collected and the fact that staleness could not be measured is reported rather than treated as a failure, which is the direction this page keeps arguing for: say the check did not run, do not let its silence read as a pass.

One phase later that gate was found to be checking the wrong population, and the shape of the miss is the familiar one. The refusal had been written inside the loop that walks the worktree's incoming docs — but the close path has a second, entirely legitimate route where that directory does not exist at all, because the doc was authored in the main checkout, and that route returns before the loop. So the one class of doc the earlier fix had gone out of its way to keep supported was the class never compared against anything. Worse, the two arms disagreed about the identical stale document: an absent directory refused, an empty one passed. And the remedy printed by the refusal creates the directory — so an operator following the instruction moved their next close onto the arm that does not look. The gate turned itself off, correctly, by the book. What ships now lifts the comparison out of that loop entirely, so it runs on both routes and over two populations — the documents arriving from the branch's workspace, and the ones already sitting in the main checkout that this run will not overwrite, each measured in the repository it actually lives in. Measuring the second from the first had been succeeding and reporting no drift: measured-and-clean over a record never measured.

The third thread here is the one worth carrying furthest, because it is about recoveries. Three separate documented ways out of a bad collect were each destroying the records they existed to protect. Hand the collect the wrong directory — the one it is writing into — and it failed in a way whose printed disposition said run the rollback; the rollback deletes, by provenance, every doc in the main checkout claiming that branch. An operand mistake was routed by the skill's own prescribed recovery into deleting the work. That prescription is gone. What replaced it is a step that undoes itself: before overwriting anything it preserves what was there under a randomly-named temporary directory, and on any failure it puts back what it displaced and removes only what it created — restoring rather than unlinking, because a delete-based undo destroys the record the overwrite displaced. The rollback was then found to have the same population bug as the staleness gate, one level over: it took its list of documents from the worktree alone, so on the route where that directory is absent it looped zero times over a main checkout that was holding the branch's doc, and printed ROLLED BACK: none at exit zero. It now takes the union of both sides, and where the main checkout holds the only readable copy it leaves the document in place and says so, under its own exit code, rather than achieving a clean report by removing the last copy. A recovery path is code that runs on the worst day, and it is the least-exercised code in any system. All three of these were written down as remedies before any of them was run.

Addendum (2026-10-10) — closing with nobody at the keyboard. A consumer running the lifecycle from an autopilot asked for the last human step to come out of the loop, and most of it now has. /auto-build --unattended claims and builds without its confirmation prompt. An abort while claiming stops the claiming; if it leaves the claim state unverified it also ends the run and writes a halt marker, after which no unattended run claims anything until a human has reconciled it, and otherwise the tasks already claimed go on to build. --agent <name> writes an owner into each lock, and /sitrep --json is a versioned contract a script can read. On the close side, /review-close --prepare runs the gather, review and pre-merge checks for real, asks nothing, writes its questions into a run directory, and stops short of collecting or merging anything; --resume <run-id> takes the answers, checks what has changed since, and re-runs only what went stale. The close is also split where it merges: --request-review opens the pull request, asks the reviewer named under ## Second reader for a verdict, and stops; --land <run-id> reads that verdict on the head it recorded and then merges exactly that head, waits, or parks. With --agent, the request half closes only the branches that agent owns and that fall inside a declared ## Unattended close class — allowed paths, a blast-radius ceiling, no manual-smoke, solo or human-action flag — and sets the rest aside for a person.

The last step, landing with no human at all, was first built on the client and switched off. Three review rounds on it each found a way past the gate, mostly in the previous round's fix. The pattern is the one this page keeps returning to: the gate ran on the agent's own machine and read state the agent could shape, so every patch to it was one more thing to route around. The redesign moves the trust to the server; its gate ships unarmed for a human to arm, and the land now relies on it: it checks that the server is set up, then turns on auto-merge and leaves the merge to GitHub. The autopilot acts as its own GitHub identity; code owners require the human's review on every path outside the class; a check signed by a dedicated GitHub App decides what paths cannot; and the agent's only power over the merge is to enable auto-merge, which GitHub will not complete until both are satisfied. The split is stated plainly: the server enforces the paths and the merge result against an agent that is compromised, while the task flags and the reviewer's verdict still bind only an agent that is honest.

Eight GitHub behaviours the design rests on were not settled by the documentation, so they were run against a scratch repository before anything was built. None broke the design; three changed it. A setting that requires the last push to be approved by someone else refuses an unapproved merge even at an approval count of zero, so it stays off. GitHub's squash merge does not detect directory renames and ignores a repository's merge=union attribute, so the gate's local simulation of the merge is configured to do the same — otherwise it would certify a tree GitHub never produces. And a collaborator with write access can edit the app's review comments, so the gate reads a comment's edit history rather than its current text. The land was turned on after its round re-ran the experiments, because GitHub can change any of them.

Adjacent skills

Two skills that sit next to the lifecycle rather than inside it — useful for ad-hoc planning and for surveying state when multiple branches are in flight (typically after an /auto-build batch — § II).

/plan-reviewopus · adversarial pass
Adversarial pass on an ad-hoc plan from conversation or a file. Uses the same prompt template as /claim-task Step 7 (_shared/adversarial-review.md); surfaces findings, presents a revised plan for approval before any execution — unless a finding classifies blocker, which halts instead: no revised plan, no ExitPlanMode, the question goes to the human (Phase 176). Phase 58a added a source-verification dimension: external SDK or framework calls with no in-repo precedent get flagged unverified rather than trusted on faith. Phase 140 added decompose-before-reject — the same rule § III applies at review merges, here at its source: refuting one clause of a compound finding never rejects the finding, and a High or security rejection owes a second independent pass. Where the shape can't spawn one (an executor leaf, which must not nest; a consumer agent that doesn't nest at all), the rejection is never silently absorbed — the per-clause rationale is sealed into the plan for the next independent reader.
/sitrephaiku · orientation
Read-only survey across worktrees, locks, branches, the index, and pending review batches. Reports lifecycle position per task — queued, claimed, in-progress, code committed with docs pending, parked, awaiting approval, documented, awaiting close — and recommends a single next move. The parked and awaiting approval states arrived with Phase 237 and are what stops a stalled claim and an unstarted one producing identical evidence; review batches have their own vocabulary but share these two states and parked, work in progress since Phases 248 and 252. Later states cover what the close leaves in between: a pending doc the branch has moved past (Phase 347), a branch merged with its close still to run (Phase 352), and a --request-review hand-off awaiting --land or parked for a human (Phases 365–366). Phase 40 introduced the Doc-Work: git trailer that lets it classify documented vs undocumented branches without grepping prose. Phase 54 hardened the read-only intent with a disallowed-tools guard (Claude Code 2.1.152) on both /sitrep and /next-task — partial by design, as both skill files state: it covers the dedicated write tools (Edit, Write, NotebookEdit) while Bash stays allowed for the survey commands, and non-Claude-Code harnesses ignore the key.

One orchestrator,
N tasks, three phases each.

When the queue holds several independent tasks, /auto-build runs the same lifecycle in batched parallelism. Architectural tasks solo, migrations solo, cross-module capped (≤ 2 per batch), complexity-sum ceiling, and a heavy-solo invariant (any single task with weight > K = 12 runs alone, never alongside siblings — Phase 42a; the K = 6 → 12 ceiling ramp landed 2026-07). Default N = 4.

/auto-build orchestrator · phase 29 Step 5 · sequential pre-claim on main · yaml round-trip + claim_task.sh --lock/--commit-claim Task A Task B Task C 6a · plan-only opus 6a · plan-only opus 6a · plan-only opus 6b · adversarial review 6b · adversarial review 6b · adversarial review 6c · orchestrator classifies — halt on blocker, or absorb fixable findings 6e · execute + commit 6e · execute + commit 6e · execute + commit — human runs one /review-close over the batch; the merge remains person-gated —
Fig. 3 Batched fan-out. Steel-blue phases are sub-agent work; orange phases execute and commit. The orchestrator never recurses; it owns the classification seam.

Fig. 3 is the process. The question it doesn't answer — the one every newcomer eventually asks — is where all of that runs, and what you can safely do beside it. The build never happens on main: your clone stays put while each task gets its own worktree, and two coordination files (sysop/runtime/locks/, tasks/index.yml) keep every session honest about what's claimed.

Primary checkout ~/Projects/app/ · main sysop/runtime/locks/ — who owns what tasks/index.yml open · in_progress · done orchestrator runs here · merges land here worktrees — isolated build dirs app-feat-a/ · feat/feat-a agent builds here app-tech-b/ · tech/tech-b agent builds here /claim-task · /auto-build /review-close From another session, while a build runs — /add-task appends to the queue · /claim-task starts new work in its own worktree never edit main by hand — it skips the lock and can hijack a running build's HEAD
Fig. 8 Where the work lives. One clone stays on main (holding sysop/runtime/locks/ and the task index); each task builds in its own sibling worktree. /claim-task and /auto-build create worktrees; /review-close merges them back. Adding tasks or claiming new work from another session only ever creates more worktrees — but those operations also write shared tracker files on this clone, and which of those writes a lock covers has changed twice since this figure was drawn. See the 2026-09-03 and 2026-09-08 addenda below.

So a second session is free to brainstorm, /add-task into the queue, or /claim-task fresh work — all of it lands in new worktrees, never on top of a running build. The one rule: start work through /claim-task, never by editing main by hand.

The coordination goes one level past the lock. Before you claim, /claim-task checks whether the task's likely files overlap what's already building, and the selectors — /next-task, /auto-build, /roadmap — can weigh that same overlap when they pick the next task. It's advisory, not a gate: worktree isolation makes a collision recoverable rework at review time, not a corrupt tree, so the workflow warns and lets you decide rather than refusing the claim.

Addendum (2026-09-03). The paragraph above, and the figure's caption, were true about the workspaces and wrong about the bookkeeping — and the distinction is the finding. Each task really does build in its own worktree, so no two builds ever write the same source file. But every claim and every close also writes the shared tracker files that live on the primary checkout, and those writes were not serialized against each other. Two claims running at once share one checkout, so the second one's rewrite of the tracker silently includes the first one's uncommitted edit. Whichever commits first therefore commits both flips. The other's commit then has nothing left to commit, fails, and tells the operator the claim was rolled back and nothing was claimed — while that claim's status change is in fact already recorded, under someone else's commit, with no lock to match it. The harm is not a lost write so much as a contradiction: a run reporting failure over work that landed. Measured rather than reasoned about: under a harness driving the real script, the losing edit disappeared in 40 of 40 trials on one sample and 97 of 100 on a larger one. The difference between those two figures is the whole reason this needed measuring rather than reasoning about: three runs in a hundred come out clean, so the failure is intermittent — the kind a team argues about and works around rather than fixes, and the kind a single successful test run certifies as absent. Isolation of the workspace had been quietly read as isolation of the whole operation, including by this page.

The fix is a mutex the claim and close paths both take before they touch the tracker, and its most visible property is a refusal rather than a recovery. A lock left behind by a killed process is not broken automatically: the next claim or close stops, names the holder, and prints the rm -f for a human to run after checking that the holder is really gone. That looks user-hostile until you notice that a claim's critical section contains a network git pull, so a live holder on a slow remote is indistinguishable from a dead one by any test the script can make — and breaking the lock blind reintroduces exactly the lost update the lock exists to prevent. A refusal that names its cause is the honest outcome; an automatic break would be the bug wearing a fix's clothes.

Two members of the same class were known and open when this was written, and saying so is the point of writing this down. The task queue's own index is written by the claim path with the identical read-modify-write-commit shape and no mutex over it — the write lives in skill prose rather than in a script, so it cannot simply take the same lock and needs its own design. And the archiver is a third writer of the review tracker that the new mutex does not cover; its exposure is genuinely lower, because it is a discrete command an operator runs between closes rather than something the loop invokes. Neither is fixed, neither has been measured, and both are filed. The first of the two has since been fixed and measured — see the 2026-09-08 addendum below. The reason to publish that rather than wait for it to be tidy is the same reason the rejected mechanisms above are on this page: a claim that concurrent use is safe is worth exactly as much as the last thing that was checked.

Addendum (2026-09-08). The first of those two is closed, and the forecast above is the interesting part of how. It said the task index “cannot simply take the same lock and needs its own design” — because the write lived in skill prose rather than in a script. The design turned out to be moving the write into a script that could take the lock: the claim path's commit became a mode of a script, and that script took the tracker mutex the batch writers already used — it had been taking no part in that mutex before, though it had always held its own per-task claim lock, which is a different lock protecting a different thing — and a batch loop's own claim became a caller of that same mode two phases later rather than keeping a second copy. Measured before and after on the real script, not reasoned about: two claims started together, with nothing between the index edit and its commit, left a task open at HEAD while its own process exited 0 in 6 to 19 of 100 trials; the same pair with the worktree and validator steps in between — the window an operator's own typing opens — measured 0 of 100. After, 0 of 100 in both. That spread is the reason the number was worth taking: a defect whose common timing is benign is the kind a single green run certifies as absent. What remains is narrower and still stated, in two parts. Three other writers of the index take no mutex — all three write atomically — and the archiver on the review tracker is unchanged from the paragraph above. Separately, the claim-side edit that precedes the commit is still unlocked in both paths, by design: its lost flip is repaired downstream by the commit's re-flip rather than prevented, which is a different property from serialization and is worth naming because it is the very edit the paragraph above describes being clobbered. The class is not closed; two ceilings were raised.

The isolation is only worth what the recovery instructions are worth, and this project shipped a counterexample for a while. Three skill sites offered the same rollback for a failed claim: a cleanup script's --force. That script takes no path operand — --force loops every worktree except the main one and removes each, without even the active-work skip its gentler sibling applies. So the prescribed recovery from one orphaned claim was, as written, capable of destroying every concurrent claim's uncommitted work; and it did not clear the lock it told the operator it cleared, leaving the task held with no workspace to return to. That is a capability verified by running the pre-fix script, not a recorded incident — no such incident exists here. The fix is the more interesting half: not a single replacement command, because the lock is written last, so the correct recovery differs either side of that write. After the lock exists, the lock-aware single-target release is exactly right; before it — the very case the worst of the three sites was conditioned on — that same command refuses, and only a plain worktree removal applies. The rollback prose had to split in two, and a documentation row had to stop licensing the wholesale form.

Addendum (2026-09-08). There is a question one step earlier than recovery, and it went unasked for longer: not how do I get out of this workspace, but whose workspace is this. Both the single-task claim and the batch loop build at a computed path, and both, finding something already there, would adopt it rather than fail — where “already there” had been checked by asking whether it was a directory. It is the wrong question. A second checkout of the same project, parked beside the first, computes the same path; adopting it silently wires two sessions into one tree, and the lock then records a workspace that is not the one being built in. The check is now an identity comparison against the repository itself, and it had to be made twice in the second script, because on both occasions the report named only the first. The identity half the fixing phase caught itself, by asking whether the class was wider than its brief; the branch half, a phase later, only its review round did. Two narrower cases followed the same way: a workspace that really is ours but sits on a different branch than the lock claims, and a cloned workspace whose origin points at a different repository altogether. The second is refused outright; the first is put right where it can be — the workspace is moved onto the branch the lock names — and refused only when it cannot be, which is the shape worth copying: verify, correct if that is possible, refuse if it is not. The general shape is the one this section keeps arriving at — isolation is an assumption about a specific directory, and an assumption about a directory is worth what the check that identifies it is worth.

Addendum (2026-09-13). A gap sat in this path for a long time without producing a single error, because what it produced was a question for a human. § I describes the test decision as recorded at plan time and verified at close. On the autonomous path, nothing wrote it — not occasionally, ever. Every batch branch therefore arrived at the close gate with the record classed missing, and because a batch claims its tasks through the lock the ownership probe answered owed, which is precisely the state that withholds the “no record was required” disposition. So every close of every autonomous branch put a person in front of a waive-or-hold with no third answer available, and the workflow read as working: a prompt appeared, somebody decided, the branch merged. The planner now emits the decision as part of its plan and the executor persists it into the branch's copy of the task body, replacing rather than appending where one already exists — every reader downstream takes the first match, so an append is how you end up grading the wrong one.

The fix needed a second layer, and the reason generalises past this project. The executor writes the record and the executor reports its own success; an agent that skipped the write, or wrote it into the main checkout instead of the branch, returns an envelope indistinguishable from a compliant one, and no check that agent runs can see either mistake. So the orchestrator re-reads the record at the branch tip before counting the task executed, using the same block the single-task path already ran, pinned identical by a test because a second copy that drifts is worse than no second copy. It is explicitly forbidden from composing the record itself out of the plan text it is holding — which would turn a verification into a fabrication — and a failure fails that task rather than halting the batch. The failure being closed is an agent skipping a step in its own instructions. The remedy for that cannot be another step in the same instructions, given to the same agent.

One detail from the same work is worth keeping for how small it is. The reader that finds the record walks the body looking for fence markers, so it knows whether a heading it sees is real or quoted inside an example. It had two of the three rules a fence has; the missing one is that a closing fence never carries a language tag. Without it, a tagged fence nested inside a plain one reads as a close, the walker believes it has left the example, and the ## Test decision heading quoted in the plan is certified as the branch's record. The check runs, finds what it is looking for, and passes — on text that was never a record at all. What ships is the third property, written down once as a named predicate both readers call rather than as a rule each of them restates: same character, at least as long, and no language tag.

Reviews and audits
write tasks,
they don't open PRs.

/codebase-review and /security-audit are the workflow's task generators. Their output isn't a comment thread or a checklist — it's structured entries in review_tasks.md. /triage classifies each batch as auto or flag; /auto-fix and /auto-judge drain the two pools in parallel. Findings have to survive the same gates as feature work.

Neither generator opens with the model. A review's first pass is deterministic — the shared check registry (checks.yml): grep rules for the mechanical convention violations, a language-server pass (pyright/tsc diagnostics), Semgrep AST rules for the patterns grep can't express, lint and dependency-audit stages riding the same plumbing. One finding shape, one baseline, one blocking gate — those findings file themselves, no model in the loop. The model reviews on top of that floor, and the language server arms it a second time on the way out: each novel finding is amplified across the codebase, symbol-anchored ones through find-references — which resolves imports, aliases, and renames — so a promoted convention lands as a task at every call site, not just the one place an agent happened to look. Judgment gets spent where tools can't see; enumeration goes to the tools.

/codebase-review reuse · quality · drift /security-audit OWASP-aligned threat scan review_tasks.md batched, threat-categorized /triage auto vs flag · Phase 44 /auto-fix parallel · mechanical fixes /auto-judge parallel · reasoning judgment findings become work — work goes through the same gates as feature PRs — or /claim-task for one-off manual review —
Fig. 4 Quality skills funnel into review_tasks.md; /triage splits the pile into auto and flag; /auto-fix and /auto-judge drain the two pools in parallel (or sequentially via --merge when batches share files).
/codebase-reviewopus · rubric pass
Runs the deterministic pre-scan, then walks the codebase against convention_map.md, files findings as batched tasks (Batch 1 / Batch 2 / …) with effort + blast_radius pre-populated. Drained by /auto-fix.
/security-auditopus · OWASP pass
Threat-categorized OWASP pass against security_map.md. Pack-aware (python / postgres / nextjs-react / llm) so findings cite the right CWE and remediation pattern.
/test-auditopus · suite health
Phase 80 — the standing counterpart to the diff-scoped coverage gate. Crown-jewel-first assessment of the whole test suite: modules that never show up in a diff but carry no tests, covered-but-hollow tests a coverage number can't see, and existing tests worth retiring (provably dead, redundant, brittle). Two signals decide what gets audited hardest — the critical_path globs behind the coverage gate, and any surface security_map.md calls out (Phase 137 — the rubric had cited that second signal since Phase 80 while the skill never opened the file; it is worth what your localized sections are worth, since a fresh install's app-layer sections are still placeholder globs). Every recommendation carries the same [verified]/[reported] provenance marker the two review skills use — it is universal, not a fan-out artifact. Read-only by contract: it ranks recommendations and offers to route the accepted ones to /intake, and it never writes a test or claims a task. The frontmatter behind that removes the dedicated write tools, and only when a harness invokes the skill — never on a path-based invocation, which is the only option off Claude Code — and never Bash. So even at its strongest the guard is partial, and the contract carries the rest.
/triageopus · classifier
Phase 44 split this out of /auto-fix: read each pending batch, classify as auto (fully mechanical) or flag (needs judgment), persist the verdict back into review_tasks.md. Two keys, deliberately kept separate (Phase 181): Flag: is the pool predicate the drainers select on, and Triaged: is the verdict record — when the decision was made, which way it went, and which tasks it covered. Deliberately not who: the shipped contract says the line records when a decision was made, not which program made it, so hand-editing it is ordinary rather than an exception. Re-running is safe because the second key says a run happened; presence of the first never did, which is the whole reason there are two. Prerequisite for the two drainers below.
/auto-fixparallel · mechanical
Drains the auto pool. Claims each batch via an isolated parallel sub-agent (default 4), fixes, verifies, pushes. --merge handles file-overlapping batches sequentially so the second batch sees the first's fixes. A batch claim writes a real lock — sysop/runtime/locks/BATCH-<N>.lock, same directory and shape as a task's (Phase 156) — which is what keeps /next-task, /sitrep and the collision advisories from handing the same batch to a second agent.
/auto-judgeopus · judgment
Drains the flag pool. Same parallel + --merge shape, but each sub-agent runs on the reasoning role and can FIX, DROP, or FAIL a task after adversarial re-reading. A FAIL now writes an indented > Failed: line under the task and close_batch.sh leaves that task open — until Phase 157 a FAIL existed only as terminal output that died with the session, and the close checked the box anyway. The two drainers run concurrently against disjoint pools.

The failure that looks like a clean result

Everything above assumes the review ran. The hardest defect class in a review system is the one where it didn't — because a review that dies produces the same artifact as a review that found nothing: no findings, no error, a queue that looks drained. Nine shapes of that silence turned up in real runs between July and September 2026, and each got its own answer.

A stage that died. The pre-scan used to report how many checks it selected. In a cross-harness trial, one install reported 890 finding(s) from 25 checks while five of those twenty-five had actually executed; a security-mode run on the same install reported 0 findings from 13 checks with every stage dead — output byte-indistinguishable from a genuinely clean scan. Every selected check now ends in exactly one of five terminal states: executed (ran to completion — a zero here is a real zero), skipped (a precondition was absent, so the tool never ran), failed (it started and broke), degraded (it ran, but over less than its declared inputs), or unroutable (it declares no form this runner can execute). The first three are always reported; the other two appear only when non-zero, so the header stays legible on an ordinary run. Each non-executed stage is named with its reason. A failed blocking check fails the gate outright: a crashed tool produces zero findings, and a green gate on top of that is a lie. Skipped is loud but never fatal, and warns only when the check is actually armed — a warning that fires from the first install forever just teaches people to ignore warnings. The gate is a backstop here rather than the fix: every shipped semgrep check is advisory — blocking is earned per-project, after a rule proves quiet on your tree — so the crashed semgrep stage that motivated all this would not have tripped it. (Five shipped grep checks do arrive blocking: true, four of them security checks — though their shipped paths are placeholders, so their gates arm only once a consumer localizes them; on a localized install, a dead grep stage would trip it.) What closes that case is the summary contract itself, plus both review skills now being told to read it and carry every dead stage into the round. Where the cause is a missing trust store — the crash that motivated all this — the runner now resolves an existing CA bundle for the scanner's subprocess rather than only reporting the death, supplying the trust anchors it needs without ever weakening certificate verification.

A runner that never started. Same trial, different agent: it read the runner script, inferred that the tools needed installing, declined on a standing no-install instruction, and hand-rolled grep instead — reporting 24 tasks where the reference run reported 372. The capability had been there the whole time; the runner resolves its own interpreter and tool path from the project's virtualenv. That is a documentation defect and it is fixed as one: both review skills now state that at the point of invocation — along with the fact that running the script installs nothing, and that not having run it is a coverage gap rather than a clean scan.

Addendum (2026-08-23). The three-state vocabulary above was not enough, and the case it missed is the one this section is really about. A scanner handed a directory does its own target discovery, against a built-in ignore list that quietly drops test trees — and files excluded at discovery never appear in the tool's own “skipped” report. So the stage ran, exited cleanly, and reported zero findings over surface it had never read: executed, by every definition the runner had. The fix was not a fourth check but a fourth and fifth state — degraded, for a stage that ran over less than its declared inputs, and unroutable, for one that declares no form the runner can execute. The first attempt at the underlying repair, enumerating every file instead of naming the directory, was built and then withdrawn by its own review round, which named seven ways it was worse than the defect.

The sharper finding is where the same silence turned up next. This page holds the deterministic layer up as the part you can trust — one finding shape, one baseline, one blocking gate, no model in the loop. That layer had two instances of exactly this failure inside it. The baseline that records accepted findings was keyed by position rather than by identity, so editing an accepted line silently excused whatever replaced it, and for a whole-stage producer like a dependency audit a single accepted entry suppressed every future finding from that stage — new critical advisories included. Separately, a punctuation character in an absolute path truncated every path in one branch of the runner, the containment guard then rejected all of them, and the branch returned zero findings while the accounting reported it as executed. Both are fixed. The point of recording them here is that the argument this section makes — a miss and a real zero are the same output — was demonstrated inside the one layer the page had offered as the exception to it.

A round that never finished. On one frontier non-Claude model, /security-audit was refused outright on repeated attempts — once immediately, once after a minute of genuine work. A round now opens a marker when it starts and clears it only once findings are written, so a round that dies partway leaves a trace the next pre-scan surfaces. The shape that can't leave a trace — a refusal before anything runs — is caught from outside instead, by self_check.sh noticing an asymmetric history: quality rounds recorded, no security round ever completed. That one is advisory on purpose. It is indistinguishable from "you haven't run the audit here yet," and a check that reddens every mid-adoption project is a check people switch off.

A subtree nothing looks at. The maps in § IV are authored, not derived — so a top-level directory no map section names is not reviewed, is never reported as unreviewed, and its silence is indistinguishable from a clean result on every round, forever. A head-to-head against Anthropic's claude-security plugin — 2026-07-23, one production codebase at one pinned commit — made that concrete: three of the six findings only the plugin found sat in subtrees the security map did not cover at all. Those misses were a map problem, not a reasoning problem. Step 2a in both review skills now opens with an inventory check that enumerates the repository independently of the maps — every top-level entry holding tracked code must be covered by a section, excluded with a stated reason, or — if neither — recorded as a named gap, the case where a placeholder section plausibly covers it but has not been localized yet. That third disposition is not a third kind of coverage — and getting the wording right took a second pass, because the first one read as though a placeholder discharged the obligation. A placeholder section's globs match nothing on a fresh install, so its Skip: list excludes nothing and authorizes no skip on any file; the entry stays on an Unlocalized report line and on the localization backlog until those globs resolve. It composes with the map-driven coverage audit rather than replacing it: that one finds files no section matches inside territory some section already reaches; this one finds the territory nothing reaches. A third check, added later, closes the gap between them — it asks whether the dispatch reaches the map. In the quality skill dispatch runs off a hand-authored table rather than being computed from the maps; in the security skill it is keyed on the categories a matching section lists, so a section whose categories no agent owns is matched and unaudited. Either way, a file matched by a section that no reviewer reaches is matched — so both of the other two checks count it covered while nobody reads it.

And then the version that unmakes the unit itself. All three checks above are keyed on map sections, and Phase 231 measured what that assumes: 30 of the 36 shipped security_map sections are all-placeholder — ## <api module>/routes/**/*.py — binding nothing until a consumer localizes them, and markdown is never token-substituted at install time. So on a fresh install the six sections that do bind are all infrastructure — shell scripts, Dockerfile, .gitignore, CI workflows, skill markdown, pyrightconfig.json. The audit reaches those and not the application code. Measured on a realistic four-pack install: 6 of the consumer's 14 files owned, 8 unowned — and all eight were the API server, its routes, auth, the pipeline, a utility module, a frontend component, a SQL migration and the tests. Section-keyed ownership assigns a real consumer's routes and auth to nobody, and every check above this one certifies that tree as covered. The fix inverts the unit. security_partition.py does not partition the map; it partitions the manifest — always real, always complete — into bounded, directory-coherent assignments, and demotes the map to what it can be when most of its globs are placeholders: a rule source, not a coverage source. Mapped files keep the category dispatch untouched; the residual is a set difference, and it is largest exactly when the map is least localized, which is the case that was silently empty before. It is advisory and never a gate — every degraded path exits 0 with a stated reason, because a partial audit beats a refused one. What it will not do is conflate assignment with coverage: packing a 486-file residual into four agents and calling that Full because every file landed in a bucket is the laundering the Full/Sampled arithmetic exists to prevent. Each agent is credited with at most its per-agent budget and the rest is reported as a shortfall, with the agent count Full would have needed. That check is why the file has the shape it does: the first cut reported Full over a 533-file repo it had reviewed a quarter of.

A round nobody ran — by an agent that knew it should. The fifth shape is the least comfortable, because this project found it in itself. The rule that a phase is not finished until an adversarial round has run existed, and was loaded into every session, as a standing note to the assistant. It failed three times anyway: once by being skipped outright, and twice — this is the interesting part — by firing correctly. The agent raised the round, said out loud that it had not yet run, and then waited for permission the convention had already granted. So the diagnosis that matters is not that anyone forgot. A reminder cannot fix a case where the reminder worked; what was missing was authority. The rule was promoted out of a standing note and into the project's own always-loaded instructions, rewritten to say that running the round is not a decision to bring to the human, and that skipping is per-phase and must be recorded with its reason — no standing exemptions, no size threshold, because the small changes are exactly where a wrong claim ships unchallenged. The same class shows up from outside too: in a scale trial, two ordinary requests against a 1,561-file repository opened 13 and 27 files — roughly 0.8% and 1.7% — while reporting 1,477 and 955 files as their scope. Neither run failed. Both reported a scope they never read.

A gate that ran, and reported green anyway. The sixth shape is the same silence one layer down — not the review that never ran, but the check that ran on half its subject. Three phases in quick succession found one each, and the family resemblance is the point. A grep check anchored its pattern on a literal double quote; Python, JavaScript and SQL take either quote, so it scanned half the code and reported the other half clean — at the registry's top severity, shipped that way in every consumer carrying that pack since the packs were populated. Its sibling was worse: a positional check that skips the whole file when its anchor fails to match, which on one real corpus meant it was silently disarmed on 59 of the 182 files that set the variable at all, rather than missing a line in each. Then a merge-time gate whose skip rule read “while the list still contains a code file” — vacuously false on a documentation-only change, making that the one route through the gate that executes nothing and still reports success, with no word in its report vocabulary for ran nothing. Then a plan reviewer whose error, refusal or rewritten answer yielded nothing classifiable, so the zero-findings branch fired and stamped the plan “adversarial review: no blockers found” — a line whose entire purpose is telling the next reader that the step ran clean. A close relative is the gate that reads the wrong revision: the verify-the-record check in § I resolved each task's body as a plain relative path, and it runs before anything merges — so for its entire life it read main's copy of a section that is only ever written on the feature branch, classified the record missing for every task on every code-touching branch on every run, and halted on it. (Documentation-only branches are skipped before the read, which is the one case it never reached.) A gate that only ever reported the state of a revision it was not gating.

A router that trusted a field nobody wrote. The seventh shape reaches furthest, because what it degraded was the review loop's own dispatcher. A one-line Flag: tag on a review batch was read by six shipped surfaces and written by exactly one, with nothing anywhere saying so — and the load-bearing inference (“a batch carrying this was already judged by a prior run, so don't re-analyze it”) is not something a line of plain markdown can support. On a real queue, an emitting agent pattern-matched the metadata of batches already in the file and produced 19 Flag: lines about its own findings. The classifier read them as prior verdicts, skipped 18 of 25 pending batches unread, looked at 41 of 385 tasks, and printed a confident, complete-looking classification. The fix was to stop overloading the field rather than to police it: provenance moved to a second key — Triaged:, carrying the date, the verdict and the tasks it covers — while Flag: keeps its exact prior shape and meaning as the pool predicate. All six readers changed; not one had to change how it parses Flag:.

A gate that is green, correct, and blind. The eighth shape is the one this project found by turning the question on its own publication step. The snapshot published to the public repository passes a set of leak gates before it ships. They ask whether the tree contains a blocked identifier, whether an excluded file slipped in, whether an excluded class is still tracked, and whether the old project names have left new residue. Each of them works at the question it asks — though two turned out to be asking it over less than the whole tree, which is the addendum below. (A sixth pass counts known provenance classes and is deliberately not a gate — mechanizing a judgement call as pass/fail would manufacture the very confidence this section is about.) None of them asks the question a reader actually depends on: is this sentence true of the code beside it? On one cut the gates were green on the first build and would have stayed green with three false statements in the tree — a resolution order stated backwards, a line-number citation the same change had invalidated by moving the text it pointed at, and an asserted permission rule that does not exist. Everything that cut caught was caught by two agents reading the snapshot, which is not a gate at all. What came out of taking that seriously is a smaller claim than “check the prose”, and the smallness is the point: one half of it is decidable — whether shipped prose points a public reader at a path the mirror strips — and that half is now a pass, which on its first run found 19 defects where the filing had estimated 9. Whether a sentence is true is not decidable by pattern, and no amount of gate-writing changes that. Mechanize the decidable half; say out loud that an independent reader owns the rest. A green gate that implies a coverage it was never asking about is the same silence as the other seven, one level further out.

Addendum (2026-08-23). Two things about this paragraph were wrong, and the second is the one worth carrying. First, the gates themselves: one of them read a whole tree with a text pipeline that drops binary files, so a blocked identifier inside a binary read clean; another tested for an excluded directory with a pattern that needs a trailing slash, so a plain file of the same name would have published. Both were found by execution against planted fixtures, not by reading, and both are closed. Second, and larger: the residue left over after mechanization was never only the undecidable half. Every pass here reads file content and file names. Nothing read a commit header — and the snapshot commits had been carrying a private author address, on published commits, for weeks, reading green at every cut. That is not a judgement call a gate cannot make. It is a decidable field nobody had thought to ask about, which is a different and more uncomfortable failure than the one this paragraph was written to describe: the gate was not blind to something hard, it was blind to something easy that sat outside the frame.

The repair to that is a scanner over commit headers, which now exists. The other half — what to do about commits already published — produced the more transferable result. A history rewrite was built and verified byte-for-byte: every tree hash identical, every message identical, dates unchanged, independently reproduced by a second reader from a fresh clone. It was then refused. A force-push does not remove a commit from a hosting provider: the pull-request refs still carry it, the orphaned commits still resolve by SHA, and the automation run records keep their own copy. The rewrite would have cleared exactly one of those surfaces — a fresh clone of the main branch — and left the same commits exposed on the other three, while permanently destroying the provider's signature verification on nearly every commit in the repository. The reason to refuse it was not that it was expensive. It was that it would have made the repository look clean — which is the one property this entire page argues against, arriving as a remedy rather than as a bug.

Addendum (2026-09-03). The same shape recurred one field over, which is the part worth reporting. The repair above is a scanner over commit headers — and it reads the author and committer email addresses, because that is what the leak had been. It does not read the commit message. Nothing did: every other pass reads a tree or a path, so the message was outside the frame twice, and the second time the frame had just been widened on purpose. What makes it worse than a repetition is where the message comes from. The snapshot commit's own message is generated by the build script and is never at risk; the text that actually reaches published history is the pull request's title and body, typed by hand at the moment of publication, folded into the commit by the squash-merge, and immutable from then on — the same immutability that made the rewrite above unwinnable. The natural thing to paste into that box is the phase's own summary, which routinely names maintainer-side paths and internal tracker numbers.

Two details of the fix generalise past this project. The first is that the filed remedy was wrong and running it is what showed that: “scan the commit about to be pushed” gates a machine-generated string, because the text at risk never exists as a local commit at all. The gate had to move to where the text is authored — the body goes in a file, and the scanner is chained to the publish command with && so a finding stops the pull request instead of annotating it. That chaining is not style: a refusing arm that prints a warning and returns, followed by the push on the next line, is a defect this same block already carries and had already been filed for. The second detail is the measurement. Run against real published history, the new arm reported findings on 33 of 35 commits — every one of them the hosting provider's own (#N) merge trailer, a number that resolves perfectly in the repository being scanned. A gate wrong that often does not get tightened, it gets switched off, so the exemption is exactly one token in exactly one position, and it is disabled for the authoring half where no such trailer exists yet. The rule this page keeps arriving at from different directions: a gate is worth what its last execution proved, and the execution is where the design gets corrected.

A round that ran, on the wrong tree. The ninth shape is this project's own adversarial round, which is this section's argument turned on the instrument the rest of the page relies on to catch everything else. A reviewer is spawned with its own checkout so that reading the tree cannot disturb it. That checkout is created from the repository's default branch — not from the commit the spawning session is sitting on. Under a pull-request merge policy every round runs from a branch that has not merged yet, so this is not a risk, it is a certainty: every lens is handed the pre-phase tree, deterministically. And a lens reading the parent commit finds nothing to report, which is indistinguishable from a clean pass — or worse, reports real defects about the wrong revision, which read exactly like defects in the change under review. Re-measured directly rather than inherited: a probe spawned from a branch one commit ahead of the default branch reported the default branch's tip, and a marker string that branch's own commit had introduced was absent from its checkout.

The uncomfortable part is not the mechanism, it is the record. The remedy was already written down, in the same file the round procedure lives in, with the mechanics spelled out — and had been for a month; a warning had preceded it by eighteen days carrying the wrong diagnosis, calling the symptom non-determinism, and a wrong diagnosis can only ask for vigilance. The maintainer's own always-loaded note was worse than silent: it prescribed the broken call, with no pin at all. It lapsed anyway: at least thirteen reviewers across four consecutive phases were placed on the pre-phase tree, caught every time only because somebody happened to look. So the fix could not be a third copy of the rule. What ships is an invoker and a receipt: the requirement moved up into the body of the section a spawner actually reads at the moment it spawns an agent, out of a subsection three levels down filed under where the harness offers it; and the round ledger's rows must now name the commit their lenses stood on, which a test enforces. The requirement is two halves, and the second is what makes the first checkable — place it, by creating the checkout at the commit under review and naming that revision in the prompt, and make it echo, by having the reviewer print the revision it actually landed on before its first finding. It binds every spawn that creates a checkout, not only the ad-hoc round — the scope line said “this is the ad-hoc round” for two phases, which excluded the close path's own convention reviewers by its own terms and so bound nothing where it was needed most. The carve-out is the narrow one it always was: where an earlier step already built the checkout, there is no separate commit under review to place anyone at, and the rule says so rather than reading as universal. The echo is where the care is: it has to be read from the path the reviewer was handed, because a bare query answers about the shell's own working directory, so a lens can echo the right revision while reading the wrong tree. That was demonstrated by a lens reviewing this very rule. A reminder is not a mechanism; neither is a correct rule filed where nobody is standing at the moment they need it.

A companion finding from the phase before it is worth recording beside that one, because it is the same consequence reached through something far smaller. Several shipped prompts hand a reviewing agent a loop over files, with the file named by a variable. In zsh — the default shell on macOS — the name path is tied to $PATH. An agent that picks the obvious loop variable therefore replaces its own command search path on the first iteration — it does not empty it, and if the value happens to name a real binary directory the command may even still resolve, which is part of why this is hard to spot. Usually it does not. What the agent has already scanned still produced findings; everything after returns nothing, and when the output is captured rather than shown it returns nothing quietly, so the run ends with an approval over a remainder it never read. The fix is one word, repeated in every shipped prompt that hands out that loop — name the variable entry — and the durable half is what it forced next door: a classification step that had been reading empty output as this file has no such section now has to account for the fact that two different failed reads return exactly zero bytes — this clobber when its output is captured, and a quieter one that returns nothing at exit 0. Neither is an ordinary success: a body that genuinely lacks the section returns its content, which is not empty. Empty output therefore identifies nothing on its own, so the exit status became part of the report and the absent-section verdict is reachable only from output you know you read. Every entry in this section is a variation on one sentence: nothing came back is not an answer, it is the absence of one.

A miss and a real zero are the same output. That is the whole class, and it is why the fixes here are never “check harder”: they are to derive the population from the source of truth rather than trusting an anchor to find it, and to give the reporting vocabulary a word for nothing ran so the difference reaches a human. Neither fix makes a check correct. They make a broken one visible, which is the only property a reader downstream can act on.

Addendum (2026-09-08, Phases 271–272). The sharpest instance of that rule since arrived from a direction this section did not anticipate: the instrument wrong about its population was a test, and it was wrong four times before anyone measured it. A guard listed the writers of one defect class — a temp file named from its target, so two concurrent writers collide on one name — and it found them by matching a single spelling of the assignment. Grepping for the class instead turned up seven more writers spelled another way, across four files, none of which that pattern could match at all; a review lens then found an eighth spelling in two shipped skills. Each discovery had been read, at the time, as the list being one entry short.

So the guard stopped enumerating spellings and asked a structural question instead — does an assignment's right-hand side end in a temp extension, whatever built it. That is the fix this section prescribes, and it was still not enough, because the detector then had a population problem of its own. Measured rather than reviewed: it counted parentheses before stripping comments and never reset, so one ( in a comment or a shell regex put the remainder of a file behind its skip — 4,881 of the 51,110 lines it reported scanning were never examined, and five files were blind from partway down. The rewrite for that was measured too, and had moved the hole rather than closed it: its own reach guard probed only column 0, where the blindness was zero, while 650 of 20,199 interior positions were still invisible. The third version measures 0 of 21,589, and ships that probe as a test rather than the claim as a sentence.

Two things are worth taking from it. The population found is byte-for-byte identical across all three versions, which is why nothing downstream ever looked wrong — a blind instrument and a complete one returned the same answer, which is this section's whole subject arriving one level up, inside the check rather than the scan. And the number exists only because a reviewer wrote a probe instead of reading the code. Every version of that detector was read carefully first, by people looking for exactly this, and reading found none of it.

Addendum (2026-09-18). Three phases in a row then found the same silence in the report of a defect rather than in the defect. A filed finding carries three separable claims — that the defect exists, that it lives where the filing says, and that the fix the filing names is the right one — and the last of the three is the one nobody checks, because it arrives sounding like work already done.

One filing said its remedy needed no new research and named an existing constant to reuse. Reusing it would have suppressed the single live entry the report exists to surface on the only real consumer, while omitting the one file that genuinely belonged — wrong in both directions at once, and confidently. A second filing named two live instances of a defect in a shared text helper; neither was a call site of that helper at all, and the one place the helper really does truncate appears in none of the four measurements the finding's blast radius had accumulated before somebody opened the files. Its published figures did not survive re-measurement either. The third is the subtlest, because the instrument that agreed was a reproduction. A close-path defect silently reverted a task's status flip; the fixture built to characterise it passed cleanly on half its cells, and the reason had nothing to do with the code under test. Where a cherry-pick inherits its original's committer date, git produces a byte-identical commit — same tree, same parent, same author, same message, same SHA — so the history never diverges and the flip survives. Every one of those rows reports success. Run the same matrix with distinct dates and every row reverts. A harness can be green because the defect is fixed, or green because the conditions that expose it were never assembled, and the two are indistinguishable from the exit code — which is this section's own thesis, arriving inside the test written to prove a fix.

What the three share is that each was caught by doing the cheapest possible thing and doing it first: opening the file the filing cites, and running the command the filing prescribes, before building anything on top of either. Neither is review. Both are execution, and both preceded the design rather than checking it afterwards.

One writer, many readers, and a contract written down nowhere

Those silences are not independent accidents, and a run of phases through August 2026 kept landing on the same cause. Some value — a metadata field, a message layout, a merge state, the name of an interpreter — is produced in one place and consumed in several, and the agreement binding them is either unwritten or written in a file the consumer never opens. Nothing then breaks, because every reader is doing something locally reasonable with what it found. The failure is that they were reasoning about different things.

The dispatcher above is the clearest instance: six readers, one writer, and the inference one of those readers drew — that a tag meant a prior run had already judged the batch — was never a property of the field. The other five were reading it correctly, as a pool predicate, which is exactly why the fix did not have to touch what any of them parse. But the shape recurs at every altitude. A contract can be stated correctly and still be invisible — the required layout for a review sub-agent's reply was written down accurately in the shared review partial, in a section that is not part of the prompt template the reviewer is actually handed, so a reviewer reading top to bottom met a contradiction that was not in the requirements. Its sibling was worse in the same way: the close-time gate explained that it reads the record at the branch tip because the executor writes it there during implementation — a confident description of a mechanism the claiming skill had never implemented. And a contract can be absent altogether while three readers act as though it holds — a branch that one step deliberately skipped was then treated as merged by three later ones, not because they checked anything and got the wrong answer but because nothing checked at all: it was still approved, was not classified dirty, was not rejected, so it fell through every carve-out. The remedy written for that in the same change is its own cautionary note. A containment check was added, and it could never return its pass value — it tested ancestry, and a squash merge destroys ancestry, so it scored a correctly merged branch and a skipped one alike. An inert cleanup that reported every clean close as suspect, deleted rather than repaired, and a self-initiated addition on top of a carve-out that had already shipped correctly. Alongside it, an unquoted ^HEAD — a negated glob under one shell's non-default extended globbing, which turns genuinely merged branches into not-merged for the operators who have that option on.

The purest case is the contract with the executing agent itself. A skill is markdown an agent runs, and nothing carries from one fenced block to the next — a named $VAR that a block does not itself assign is the empty string by the time the command runs. Positional parameters fail in the opposite direction, which is worse: the harness rewrites a bare, in-range $1 or $2 into a word from the invocation before bash sees the line, fenced code included, so an awk field reference in a skill body quietly becomes something else and prints a plausible wrong answer rather than an empty one. The shipped rule is that a skill body contains no positionals at all. Two audits of the lifecycle skills found thirty-one such references, and the second audit's own review round found the last two, in a fifth file nobody had looked at. They are quiet in the worst way, because the shell does not object: git -C "" rev-parse HEAD does not fail, it runs in the current directory and prints a perfectly real commit. So an integrity check comparing that against an unset “before” value compares a real SHA to an empty string, is true whatever happened, and parks the entire batch reporting a contract violation that did not occur; an empty subset variable, elsewhere, quietly widened a deliberately narrowed selection back to the whole queue. What is measured here is the mechanism, not the frequency — prose further down the same file did tell the operator to hold the value in context, so an agent that substituted it anyway was fine, and no run logs exist either way. The first filing counted twenty sites in two files; the class was thirty-three in five, and six of the extras had been read and cleared by an earlier sweep working from a rule that licensed them.

The portability instance is the least glamorous and the most instructive, because it is where the project's own rule was already correct and still insufficient. Shipped skills hardcoded a virtualenv-relative interpreter — .venv/bin/python3 — as the literal command word. On a consumer using poetry, conda, a plain venv/ layout, or a system Python where installing into it is an error (which is now most Linux distributions and Homebrew macOS), that word is simply command not found; one skill's only command was dead there outright. The rule that would have caught it — run the commands your change prescribes, against a fixture built from the source of truth rather than from your own memory of it — had shipped fifteen phases earlier. The class was still live at sixteen sites, because a rule about new commands does not sweep old ones, and because three of those sites were the documentation still teaching the idiom.

What actually works here is dull and mechanical. Name the writer and the readers in the file the actor reads, not in the one that happens to be correct. When a field acquires a second meaning, add a second key rather than overloading the first — that one decision is why the dispatcher fix touched six readers without changing what any of them parse. Derive the population of affected sites from the source of truth instead of from a summary of it — across five phases scattered through this stretch, the filed estimate came in low every time, by factors between 1.6 and 15. Not uniformly: one phase in the same run talked itself into a correction its own filing had gotten right, so the discipline is to re-derive, not to assume the filing is an undercount. The honest limit: stating a contract does not make the readers obey it, and none of this is detectable by a test that does not already know the contract exists. What it buys is that the next person to touch it meets the agreement instead of reconstructing it.

What it takes to file a finding — and to kill one

Every finding any of the three generators files carries its provenance in the row: [verified] — its author opened the cited file:line and confirmed it against source — or [reported], asserted from a grep, a pattern, a pre-scan, or an upstream claim without opening it. The marker is universal, single-agent /test-audit included. It is a self-declared honesty label, not a machine-checked guarantee, and it says so everywhere it appears. What it buys is a routing decision for the reader and an obligation for the fixer: re-read before applying, never act on a [reported] row blind.

A second tier applies wherever a review fans out — dispatching sub-agents across slices of the codebase and merging what comes back. The measured failure in that merge: one sub-agent reported a completed review of its whole assigned surface having opened eight of its eighty-two assigned files. Merged blind, that ships a clean verdict over a real hole. So each sub-agent returns an evidence footer — files opened versus assigned, plus the tool mix — and that is the load-bearing half, because opened 8 / assigned 82 is falsifiable in a way that "reviewed everything" is not. The opened figure is not a bare count: the footer must enumerate the paths, so the claim's writing cost grows with the claim. An earlier revision let the set be named by reference to the findings instead, which made two of the checks reading it unsatisfiable by construction — dead checks that read as live. Provenance on a merged row is then written from the orchestrator's vantage, so a sub-agent's own [verified] is never copied through unchallenged.

Both of those tiers measure work inside the dispatch path, which leaves the obvious hole: a round that never dispatched is never measured. Worse, it looks better than an honest one — a solo agent's findings are all legitimately [verified], while merge discipline forces [reported] defaults on a real fan-out. The cleaner provenance split is the tell. So a tier sits underneath both, and it fires on every round, solo or not: one line in the durable round header reading Coverage: <kind> · manifest <N> · opened <M> · grepped <G> · workers <K>. Manifest is the round's own declared scope after exclusions, never the repository total; opened counts bodies actually read, with grepped kept separate so the two can never be quietly summed; workers 0 means solo, and solo becomes a declared decision with a stated reason rather than a default nobody notices. A round that cannot open its whole scope is Sampled, not Full — and that label is an exemption, not only a confession: every surface that computes the low-look ratio gates it on Full, so calling a marginal round Sampled switches off the tier's one threshold test. The field-presence and workers 0 checks sit outside that gate on purpose, so a bare Sampled cannot turn the tier off wholesale. The numbers are still self-reported — nothing counts file reads for you — and the honest claim is narrow: what this buys is that the count must now exist and sit beside its denominator, where a reader can see opened 13 against manifest 1,477 and ask.

Killing a finding is the harder direction, deliberately. A blind grading pass over the pooled findings from that same cross-harness comparison found that a same-family adversarial reviewer systematically under-catches compound findings: one clause of a multi-part finding gets refuted, the remaining clauses are silently dropped, and the whole finding is dismissed. The error is directional — it only ever turns a real finding into an apparent false positive, never the reverse. Of six audited dismissals, half concealed a real finding, one of them a High-severity security issue. Hence decompose-before-reject: refuting one clause does not reject the finding, surviving clauses classify on their own merits, and a rejection has to name every clause. It binds on downgrades and de-duplication too — a High quietly demoted to Low on one refuted clause is the same laundering as a drop.

The adjudication rule underneath is asymmetric, and the asymmetry is the point. A falsified premise refutes a finding outright — that is the ordinary death, and it is what keeps a fabricated finding from being unkillable. Beyond that: kill or downgrade only on a mitigation you located and read at a file:line; keep or escalate only on a path you actually traced; and when neither can be established, the finding survives, with the gap recorded. That inverts the default-to-false-positive stance the plugin's own verifier takes, for a reason specific to this queue — a filed task gets another reader (the fixer, the reviewer, the merge gate), while a dismissal gets none. An unverified finding costs a review cycle. An unverified dismissal is silent and permanent.

That rule has a sharp edge on dependency advisories, where the gap between installed and reachable is where audits go wrong. In the same round, three of five dependency narratives were materially wrong — two inflating severity by asserting a call chain that did not exist, one understating how many call sites were affected. So: trace the chain and cite it, or file Reachability: unassessed at the advisory's own severity. The finding is still filed; only the asserted chain is the defect. Alongside it sits a remediation-blast-radius rule, earned the hard way — one refuted finding's fix, applied exactly as filed, would have caused a production outage, and it was an ignore-file change rather than a version bump.

How many reviewers, and when a round ends

A convention that says “use several reviewers” ratchets. Nobody ever decides to spend more; each round is sized under local pressure to be thorough, and the number only moves one way. In this project's own log it was never chosen at all — it drifted from one reviewer to a mode of two, then to three, with excursions to four, and one phase ran three rounds of three. Every one of those calls was defensible on the day. None of them was a decision.

The obvious correction — cut back — is wrong, and the measurement says so. On one four-lens round the overlap between lenses was about 15%, and each of the four produced its sharpest finding alone: one found a remedy that was inert on the maintainer's own machine, one found the authoring habits that walk straight through a line-scoped guard, one found that the phase's central framing was false, and one found a false sentence in the phase's own new code. Extra lenses were not buying redundancy. So the question was never “are they worth it” but who gets to decide, and the answer had to be written down somewhere a session under deadline could not quietly reinterpret.

What shipped is a governor, not a cap. It sizes the round by the kind of change: no round for a docs-only or record-only change, the author battery plus one execution lens for a one-site fix with executable tests, two lenses for behaviour across sites or in a shared file, and a third only on the maintainer's call. (That is Phase 381's resizing, on the maintainer's ruling. The first version set two lenses by default and a third when a change shipped behaviour and its record made numeric claims a reviewer could falsify.) Beyond that, the count is the maintainer's decision, made outside the session that wants it and with the round yield so far in hand. One round is the default, and it ends when every verified finding is dispositioned; what warrants a second is one thing only — that the fixes themselves produced substantial new material no fresh reader has seen. Verifying a bounded fix is the caller's job, not a new round's.

The piece that does the most work is a third disposition. A finding used to have two exits: fix it, or it was not really a finding. That is what turns a round into a loop, because a confirmed-but-unreachable defect spawns a fix, the fix spawns verification, and the marginal product becomes guards on guards. So a finding may now be recorded as latent — verified real, with no live path reaching it today, demonstrated rather than asserted, filed where another reader will meet it along with the condition that would revisit it. “Conceptually possible but unobserved” deliberately fails that test, since every pre-consumer defect is unobserved. A latent filing does not count toward warranting another round.

The honest limit: the session that wants a round is the same session that judges whether it needs one, and no rule fixes that. What the rule buys is that the judgement gets written down where the next reader can dispute it — and, maintainer-side, that each round's reviewer count, findings and dispositions land in a ledger, so the next revision of these numbers can be argued from data rather than from impressions. The subsection is the most contested text in the file it lives in, which is the expected outcome: the numbers it names are exactly the ones a session under pressure wants to raise. Its own review round disqualified the first version of it.

Addendum (2026-09-08, Phases 271–272). What a round buys got a sharper demonstration than the overlap number above, and it came from the lens that executes rather than the one that reads. A phase set out to fix a real defect in a rollback path: the bare git checkout <path> restores from the index, not from the commit, so a rollback over a change that was already staged prints Updated 0 paths from the index, exits 0, and leaves the thing it was asked to undo exactly where it was. The fix it reached for was git checkout HEAD -- <path> — not an invention, but the idiom this project's own workflow spec already carried as house style.

Its round refuted that by running it. Where the staged content belongs to a second session, HEAD -- reverts the index too: measured, one session's rollback silently reset another session's claimed task from in_progress back to open — exit 0, no output. And the one state in which the staged change is reliably your own is the state both skills already tell you not to roll back in at all, so wherever the documented idiom actually ran, the content it discarded was somebody else's. The fix had traded a false report for silent data loss, and the documentation is what licensed it. What ships instead does not choose between the two forms — it verifies the postcondition. The bare, precise checkout stays and gains a check that nothing is staged for that path, which detects the no-op the original defect was about and destroys nothing. The round's product was the disqualification, not the fix.

Then the same shape closed on the author. One phase later, checking a mutation by hand, a session restored a file with the bare git checkout -- <path> and discarded that phase's own uncommitted work, because the index still held the pre-change content — which is precisely the sentence the phase before it had just written down. The lesson had been recorded, in prose, by the person who then failed it. That is the argument this page keeps making about prose, arriving once more at the expense of the people making it.

The second round measured worse than the first. The governor warrants a second round on one ground: that the fixes themselves produced substantial new material. They had — the round-1 fixes rewrote the guards rather than patching them. The second round put 54 mutations through the rewritten guards and 42 survived, against 39 of 62 through the ones they replaced. The honest reading of that is the modest one, and it is the one the record itself reaches: freshly rewritten material measures like freshly rewritten material. It is evidence that the condition selects for unreviewed work, which is what a condition is for; it is not evidence that two rounds are better than one, and nothing here establishes that. What the second round did buy is concrete: it found that two controls the first round had just pinned to a literal commit would fail under CI, whose checkout is shallow by default and does not have that commit — a red required check on every push, introduced by a fix.

Folding in an outside scanner

The same head-to-head produced a sharper result than the coverage gap. On one production codebase at one pinned commit, the two tools' findings did not overlap at all: six of the audit's nine were pass-through of machinery already promoted into the deterministic layer, while all six of the plugin's were novel code reasoning that no promoted rule would have caught at commit time. That is one run on one codebase — not a general property. But it points somewhere useful. An outside scanner finds what a convention loop structurally can't, precisely because the loop's deterministic floor is built from patterns somebody already ratified.

So /security-audit reads one in. If a claude-security report is sitting on disk, the audit folds its in-scope findings into review_tasks.md under their own batch, buckets the out-of-scope ones visibly rather than dropping them, and says so loudly when the scan it is reading was truncated or was taken at an older commit. Three properties are worth naming. The first was locked before any code was written and survived the adversarial pass; the other two are corrections it forced.

It reads; it never launches. Sysop cannot run the plugin — the orchestration tool the plugin's own agent depends on is stripped from every sub-agent, with no override — and reading a file off disk needs no harness detection, so nothing in the ingest's mechanism depends on which harness you are running. The human runs the scanner on their own cadence; the audit folds in whatever report is present.

The parser is a sanitization boundary. Finding text is a model's summary of scanned code, which makes it attacker-reachable. Written verbatim into a task file it could forge a provenance tag or plant an instruction for the automated fixer that later drains the queue. Nothing from the report reaches the queue unsanitized — and the attacker-controlled source snippet is never emitted at all.

Ingested findings stay [reported]. The plugin runs its own verification panel; that verdict is recorded in the row as data and never promotes onto the provenance tag. Somebody local still has to open the file.

The honest ceiling: these are code-reasoning findings, so most will never become a checks.yml rule — they fail the review's systemic-pattern and cross-round recurrence gates, landing as a prose map entry at best rather than a mechanical one. What the ingest buys is that they become tracked work instead of evaporating with the scanner session that found them.

The one sweep that does touch PRs

Phase 53 added the workflow's first GitHub-touching skill — the exception to this section's title, scoped to bot-authored dependency PRs only.

/pr-dependabotopus · github sweep
Sweeps open Dependabot PRs: classifies each by ecosystem + semver, merges patch/minor updates on green CI, holds majors (even from trusted publishers), closes pip and open-set-superseded PRs. Dry-run by default; --execute to act. A client-side replacement for native auto-merge, which Free-plan private repos don't get. It reads the repo's setting first, and asks before acting as a second merge actor. Classifier validated against ~50 real PRs.

The loop that makes it
self-improving.

This is the thing. Every other section is plumbing for this one. When a review finding recurs across two rounds, the review skill that filed it — /codebase-review or /security-audit, in the same session, with your approval on every promotion — promotes it into the project's convention_map.md. From that moment on, /claim-task consults it during planning and after implementation, and every future review round uses it as a rubric. The workflow forgets nothing.

convention_map.md your project's map · base + .project overlay pack maps ship the same rules, generalized /codebase-review files finding round N · or /security-audit fix in this PR noted in review_tasks.md same pattern again round N+1 · N+2 promoted to map attributed to round N /claim-task step 7a · pre-impl check step 7e · post-impl re-check /codebase-review uses map as rubric apply repeat? promote consult enforce next round
Fig. 5 — centerpiece The convention loop. Findings travel clockwise; the map ages forward, the patterns get cheaper to enforce.

What's in a map entry

Each entry is a glob pattern and a one-line rule, plus provenance. In the map the loop maintains in your project, a promoted entry's provenance is the commit that promoted it — docs: promote … from Round <N> — and that is load-bearing: when an entry later turns out to be wrong, you can find the review that promoted it and learn what changed. (The cross-round recurrence gate reads the review rounds themselves, not the map.) The shipped pack entries are those same rules with the project-specific detail generalized out — placeholder globs you localize, a pointer to the paired mechanical check, provenance kept inline where it still explains the rule — with their full round history living in the source archive behind Fig. 7 rather than as inline tags. One section of the real file, trimmed:

## `<scripts dir>/*.py`, `<datajobs entrypoint>` — CLI Scripts and Job Entrypoints
  ⋮ (seven of the section's ten bullets elided)
- Env var validation: Security-critical env vars validated at startup; prod/staging → fatal; dev/test → warning
- Logger formatting: Use `%s` not f-strings
- YAML safe load: Always use `yaml.safe_load()` — never `yaml.load()` or `yaml.full_load()`. PyYAML's default
  loader instantiates arbitrary Python objects from `!!python/object` tags (OWASP A03 deserialization). Codified
  when YAML became load-bearing for `tasks/index.yml` (Phase 16). Paired check: `yaml-unsafe-load`. (… trimmed)

> AST-backed equivalents: `semgrep-logger-fstring`, `semgrep-recompile-inside-def` (in this pack's `semgrep/` directory)

The promotion history is the workflow's audit trail.

The flip side of forgetting nothing: entries can become wrong. A convention that was right at round 12 can be stale by round 60 — the codebase moved, the library fixed the footgun, the rule outlived its reason. The round attribution is the first concession to that: a bad entry traces back to the review that promoted it, so it can be retired deliberately rather than worked around forever. The second is a backward sweep that runs every review round, alongside the forward coverage audit: it flags the statically detectable staleness — a map section whose glob no longer matches any file (a removed category), and a bullet citing a helper that has since been renamed or deleted (a dead reference) — at the moment the code moves out from under the rule. What stays manual is the decision: both halves surface candidates, a human adjudicates them. And the harder, judgment-driven demotion — a blocking rule that keeps firing false positives long after it stopped catching real ones — is now built too: each false positive a reviewer triages away is recorded as a stale-verdict in a per-rule ledger, and once a rule accrues them across two or more rounds, the review session prompts to retire it, demote it to advisory, tighten it, or keep it. Anyone who has maintained a lint config for five years knows how that debt accrues; the map differs in keeping the receipts and, now, in both noticing out loud when an entry's referent disappears and retiring the rules that have quietly gone wrong.

Does it actually get quieter?

Measured, not asserted — 71 review rounds and 3,298 findings from the upstream project's review archive, February through July 2026. The honest version first: raw findings per round go up, not down. The rubric grows with every promoted convention, and later rounds file one task per call site where early rounds bundled. Many findings are process-induced — total findings track the rubric's size, not the code's health.

What falls is severity — and then it stays down. The cleanest metrics need no denominator at all: the critical share of findings fell from 21% in February to 5% in April — the last full month before Round 70's retro-application sweep — and then, that sweep aside, held in the low single digits through partial July (17% in May during the sweep, 0% in June, 5% in July) while the rubric kept growing. It is not a monotonic decline; the point of the claim is that the severe share dropped from roughly one-in-five to the low single digits and has stayed there. The share of low-severity findings mirrors it, rising from 22% in February to a 75% peak in June, 65% in July. Activity-normalized rates agree on direction: criticals per 10,000 effective lines changed fell roughly 3× from the March peak to May, lower still through June and partial July. Reviews don't get smaller; they stop reporting things that matter — and the archive cannot distinguish “not there” from “not seen,” which is why the limits below name the reviewer itself as an instrument, and why every round is now required to carry a coverage ledger.

Two confounds were checked rather than assumed. Commit size shrank ~5× across the interior of the period (median 65 → 13 lines per commit by May), which inflates any per-commit rate — hence the headline rests on denominator-free shares, corroborated per-line. And severity labels are the reviewer's own; the rubric is impact-categorical (security, data integrity, crash potential), and the late rounds labeled map-enforcement findings critical rather than discounting them as known — but the direction of any labeling drift can't be fully pinned, so treat the shares as the primary evidence and the rates as the check.

And the limits, plainly — stated because they bound how much the chart can carry. This is one project, and the loop's author ran it, ran the reviews, and adjudicated every severity — n = 1 and self-graded, which no re-analysis of the same archive can fix. The labels have moved at least once: June's lone critical was reclassified out during a later archive consolidation, so June's 0% is one bookkeeping decision away from ~1.5%. The tail is thin — 68 findings in June, 37 in partial July — so a single bad round would move the late shares by double digits. And the strongest rival explanation is ordinary maturation: young codebases surface their worst findings early and harden over time with or without a convention loop, so some of the 21% → 5% fall was coming anyway. Nothing in Fig. 7 separates the two hypotheses. One more instrument belongs in this list: the reviewing model itself. Every finding in the archive was filed by the same era of model, and a review is a sample, not a census — when two frontier reviewers later scanned one identical tree, their finding sets mostly did not overlap. Just past this chart's window (August 2026), the measurement arrived: the first full round run under a newer model generation, with a scan scope derived from the repository rather than from the convention map, filed 358 findings against the 23–45 a single round filed in the months before — and an unrelated second vendor on the identical baseline filed 132, itself several times the “usual,” confirming the backlog was real rather than the new reviewer over-filing. Most of the surge was scope and lens growth, and the mechanized-class signal held through it (zero new blocking findings; the promoted baselines unmoved) — but it means the thin tail above was thin partly because of what that era's reviewer looked at and could see. “Held low” is also three review rounds — June's two and July's one — not three months of scrutiny; and without a coverage ledger, a declining total is unauditable, the reason every round is now required to carry one.

What would separate them is specified in the method notes (docs/analysis/REPORT.md) and has now been run (Phase 112): partition findings into classes covered by an already-promoted convention at the time of the round versus classes nothing covered, separate new-code findings from retro-application backfill (Round 70 files covered-class findings by design), and track both per line of diff. Maturation depresses both classes together; the loop selectively suppresses the covered ones. That analysis needs the upstream per-finding archive, which is private — the round-level dataset that ships here cannot answer it. The result is mixed and leaves this section's claim unchanged: the rate comparison is inconclusive (the review instrument — the scan tooling — changed twice mid-history, inflating then collapsing every class), the mechanized-convention signal confirms the claim but is partly true by construction, and prose-only conventions show no separable effect from maturation. The claim this section makes stays the narrow, defensible one: the severe share fell early and stayed low while the rubric grew, and where a convention is mechanized, its machine check makes silent recurrence of that finding class structurally harder — a prose-only convention's gate is the reviewer re-armed with the map. What would falsify it: covered classes recurring in new code at the uncovered classes' rate, or a fresh project that starts with the map pre-loaded and still produces February's one-in-five critical share.

critical share of findings (%) share of low-severity findings 21% 11% 5% 17% * 0% 5% 22% 29% 59% 43% 75% 65% Feb 2026 Mar Apr May Jun Jul (partial) * May's bump is Round 70, the first broad sweep after /codebase-review became language-server-driven (LSP): ~25 of its 45 criticals are promoted conventions amplified one task per call site — a sharper instrument, not a Feb-level defect rate.
Fig. 7 Severity fell, then held low, while the rubric grew — a correlation on one self-graded project; limits in the text. Bars are denominator-free shares; activity-normalized rates (per-commit, per-line) agree on direction — see docs/analysis/REPORT.md. Mined from the upstream project's review_tasks archive (71 rounds, 3,298 findings) and its convention history (78 entries, all dated). The mined dataset ships alongside (docs/analysis/gdp_review_metrics.json), and the miner that produced it is included for provenance — re-mining needs the upstream repo's private archive, so verify against the dataset.

Boring mechanics,
handled outside the skills.

Claude Code 2.1.x added two hook points that Sysop leans on heavily. The hooks let skill prose stay short — instead of writing escape instructions inline for every possible permission denial, the hook injects them automatically. Neither hook can authorize anything. The real gate is the human invoking the skill and, for protected operations, typing the escape — the hooks only remove the re-explaining.

PERMISSIONDENIED · 2.1.89 skill emits Bash call e.g. git push origin main classifier blocks protected-branch policy permission_denied_hook.py matches pattern · emits ! escape via additionalContext model relays the escape to the human ask user to type: ! git push origin main human types it — push succeeds SUBAGENTSTOP · 2.1.47 planner, reviewer or executor finishes envelope: last block, or a handback SubagentStop fires last_assistant_message, else transcript parse_subagent_envelope.py finds last yaml block w/ task+status writes subagent-envelopes/<id>[.<phase>].json parent skill reads JSON regex fallback · defense in depth structured handoff · no regex on prose
Fig. 6 Two hook points. The accent flow is the hook itself; the rest is the surrounding mechanism. Note the final actor on the left: the push lands only when the human types the escape — the !-prefix is a user-side control the model cannot emit.
PermissionDeniedclaude code · 2.1.89
Hook can't bypass the classifier — it injects additionalContext showing the exact !-shell-escape for the human to type at the next prompt. It grants nothing; the person typing the escape remains the actor — and is expected to read the command before typing it; the relay is a request, not an instruction. Scoped to three matchers (protected-branch push, --delete push, protected-branch commit), each one a command already in the settings.json allow-list that the classifier overrides anyway; unmatched denials pass through untouched. Collapsed four paragraphs of /review-close escape prose into pointers.
SubagentStopclaude code · 2.1.47
Fires when a sub-agent finishes. Parses the structured envelope (TASK: / STATUS: / WORKTREE: / …) into JSON so the parent skill doesn't have to grep the return text. Phase 54 added an agent_transcript_path fallback and message_source provenance. Separately, it retired the caveat about the hook's own timing — retired not by those additions but by the hooks docs coming to document the SubagentStop lifecycle: the hook runs synchronously on sub-agent completion, so the JSON is written before the parent reads it. A second race outlived that fix until Phase 319. The fallback ran only when last_assistant_message was absent, and an agent that hands its envelope back through the harness's handback tool leaves a one-line summary in that field — so the envelope was discarded and the run recorded unparseable, with the fallback never reached. Phase 319 made the fallback fire whenever the field carries no envelope, and made selection envelope-first across sources: on all three live transcripts measured, the agent emitted a one-liner after its handback, so last-wins would have thrown the envelope away a second time.

Addendum (2026-09-13). The same instinct — keep the text the runner reads short, put the rest where it is paid for once — produced a second mechanism, and the honest report is that it is one step in, not finished. A skill body is two documents wearing one file: procedure for the agent executing it, and, threaded through that, the reasons a given sentence is worded the way it is. The second audience is an editor, and it is the smaller one. So the reasons moved out. SKILL.md stays the runner; a sibling REFERENCE.md carries, per rule, what the rule protects, what its loss or softening would change, why it is easy to soften, and which phase bought it. Claude Code does not load a skill's sibling files automatically, so the split costs the runner nothing.

What makes it more than filing is the pin. Rules that earn a declaration — the record can name what their loss would change — are reproduced verbatim and compared by a required check, and the roster is machine-read three ways at once: the ids listed in the reference, the sections that define them, and a constant inside the test module must be the same set, so retiring a rule cannot happen as a side effect of an edit. The reference then says the thing such a document usually omits: eight rules are declared, all of them in one step, and every other step and the whole tail of the skill ride unprotected by construction — do not read the file's existence as coverage of the skill. Which is the same sentence this page keeps writing about gates, applied to the document that describes them.

The worked instance the method ships with is not the one it was drafted with, and the round is why. The step first chosen to demonstrate the pin was picked for being the right size — and the pin, as it then stood, compared prose with fenced blocks stripped out first, while in that step roughly seven-tenths of the text sits inside fences, because that is where the commands live and the commands are where the rules are. Measured on the characters a pin could actually hold, it was under a third of the size it appeared to offer, and every rule a reader would name in it was invisible to the mechanism. The round disqualified it before anything shipped; the step that did ship was chosen by that measurement rather than by eye.

Addendum (2026-09-14). The paragraph above was written while the fence question was still open, and it was answered within the day. The canonicaliser now tags a wrapper instead of deleting it — a fenced block survives to the character, bracketed so a pin can tell a command from prose — and the two rules the reference had listed under a heading called Blocked were declared instead. That heading is now empty and says so in place of the entries, because a section that explains why it emptied is worth more than a deleted one. What followed is not, on the record, what that settlement bought: thinning this step never needed a new pin, and the blocker the project actually names is a different one — that a re-wrap changing no words still breaks guards which were never meant to constrain formatting. Once that was priced, rationale moved out of the runner and into the reference across four steps — forty-four sections of it — and eighteen blocks were left whole: most often because a rule's reason is what makes it followable, and otherwise because a guard or a pin still reads the text where it sits, because the verdict could not be decided, or because moving the block would have meant re-authoring far more than it freed.

Addendum (2026-09-18) — the campaign is frozen, and the honest report is that the method could not reach the prize. Those four steps are where it stopped. On 2026-09-16 the campaign was closed by decision rather than by completion: four sections taken, thirteen never reached, and every later bundle cancelled. The arithmetic is the reason, and it was re-derived against the merged tree rather than quoted from the plan that proposed it. The runner lost 26,315 bytes, 5.6%, across ten phases — seven of which touched the file, six of which changed its size, and one of which grew it — while the reference it fed grew from 20,213 bytes to 70,244 over the same span. The target the campaign was opened against needed roughly three quarters of the file gone. A census of what could be cleanly relocated found 8.0%, against a ceiling of 35.8%, and the two sections measured had 56% and 70% of every split re-authored straight back into the runner. For scale in the other direction: four ordinary phases in the run-up to it had added 42,341 bytes, so the whole campaign did not undo four phases of routine accretion.

Three things are worth stating about a freeze rather than a finish. The first is that this is a fact about the instrument, not about the problem — the two levers that do reach the prize were costed and deliberately left unopened, because the cheap one was measured and found insufficient before the expensive ones were started. The second is that the ratchet stays armed and changes job: the check that bounded editor-facing mass is now a growth guard, refusing re-accretion rather than tracking progress toward anything. The third is the reopen condition, written down in advance so the decision is not re-argued from whoever is under pressure — a second consumer, or a close cadence high enough that per-close cost compounds. What replaced the campaign is narrower than a continuation of it, and the distinction is worth stating precisely because the file has kept growing. A standing review dimension now asks of every change whether a block of prose tells the runner what to do or tells an editor why the rule exists, and routes the second kind to the reference as it is written. That is prevention, not relocation. The one phase to exercise it since the freeze was an ordinary defect fix: it added three reference sections — forty-seven across five steps today, against the campaign's forty-four across four — but those sections are newly authored rationale for that fix, not text lifted out of the runner. The runner grew by 1,896 bytes in that phase, and the editor-facing mass the ratchet measures fell by thirty-one characters. The campaign's own roster still lists that step as never taken, correctly. So the honest summary is not that thinning continues at a smaller scale: it is that the push is over, and what survives it only keeps new rationale from landing in the runner in the first place.

Addendum (2026-10-21) — the campaign reopened, as a split. The reopen condition fired from outside. A one-week field report on a full install, and a 30-day measurement of the busiest consumer's spend (Sysop's own process at no less than 37% of it, spawned agents starting near 99,000 tokens of context), turned the per-close cost into a number, and the campaign reopened on 2026-10-12 in a different shape. The cheap lever had already been measured insufficient, so this time the runner itself is split. An arm that only some closes take now lives in a step file under review-close/steps/; a close loads it only when the arm is due, records each load in the close's receipt, and checks that receipt before it reports. The longest inline programs became shipped scripts. The ratchet described above was retired, and a byte budget on the runner core replaced it, lowered as each step moves out. At this snapshot the core is 367,128 bytes against 579,071 at the previous one, 37% smaller, with eight step files beside it. The scoping put a realistic core near 42,600 tokens, not the 20,000 first asked for, and the split is still moving one step per phase, so the paragraph above is right about its instrument and no longer right that the push is over.

Addendum (2026-10-28) — the split's planned steps are done. Every arm the scoping ranked has moved into a step file or been measured and left in place. At this snapshot the core is 287,120 bytes against 367,128 at the previous one, 22% smaller, and half the 579,071 it was at the last snapshot before the split began, with twenty-four step files beside it. That is still well above the 170,309 characters the scoping projected for the core. What remains is mostly text every close reads, with a few arms measured and left in place, and whether to go further is an open decision rather than a schedule. The same weeks taught the close to run with no one at the keyboard: its prepare, its resume and its land each ran a headless night under Claude Code’s dontAsk mode with no permission denied.

Addendum (2026-10-30) — the split stops here. The open decision above was taken on 2026-10-29: nothing more moves out of the runner. Its core is 286,503 bytes, with twenty-four step files beside it, and the byte budget stays as a guard against the core growing back rather than a target to shrink toward.

Two install paths,
one source of truth.

Claude Code plugin

/plugin marketplace add getsysop/sysop then /plugin install sysop@sysop plus any packs. Delivers namespaced slash commands like /sysop:claim-task.

core python postgres nextjs-react llm beancount streamlit pandas mcp-server kotlin swift-ios flutter

Bash installer

bash install.sh <target> --packs python,postgres — the installer and its companion scripts run with any agent, or none; how far the skills travel is measured, with named limits, below. Or --packs auto to detect the stack from the target's own markers (Phase 75). Writes scripts, git hooks, permission settings, and per-pack convention maps. Updates via bash sysop/scripts/sysop-update.sh from the consumer side once $SYSOP_SRC is set.

Two install modes: full and loop

One more axis: --mode chooses how much of Sysop lands. The default, full, is everything this page describes. --mode loop installs only the convention-loop slice — § III's review and audit skills (/codebase-review, /security-audit, /test-audit, plus the give-back pair /report-issues and /contribute-convention), the convention and security maps, § IV's promotion machinery, and the compiled checks — into a repo whose owner keeps their own planning, branching, and merge workflow. No task queue, no worktrees, no merge gate: enforcement moves to where a bring-your-own-workflow repo expects it — the pre-commit hook's check slots, and CI running run_checks. Packs are entirely loop-side content, so every pack applies in full. The lock records the mode, and --update --mode full later grows a loop install into the full workflow, purely additively. The day-one walkthrough is loop mode.

Addendum (2026-09-13). That lock is the installer's memory, and it turned out to be reading it the way this page keeps finding things read: treating I could not open it as it is not there. The two are one line apart in the code and nothing alike in consequence. An absent lock means a fresh install, so the absent branch writes both timestamps from scratch — which, reached by mistake on a machine that simply ran out of file descriptors under a parallel load, silently overwrites the record of when the project first installed Sysop and of when it last actually changed. The failure mode is not an error; it is a no-op update that reports itself as a real one. Three places on that path stopped conflating the two, in two different shapes. The two that read the lock now split three ways: a genuine absence is absence, a hand-mangled lock stays tolerated because a mangled file must not abort an install with a traceback, and every other read failure refuses, before anything is written, naming what it could not read. The third reads the installed-from revision rather than the lock, and splits two: no repository at all is an honest unknown — that is a tarball install — while a repository whose revision will not read refuses outright, because substituting the unknown there changes the recorded revision, and a changed revision is precisely what makes a no-op install report itself as an update. The generalisation is the one the catalogue of silent failures in § III is built on. An empty result has at least two causes, and a system that cannot tell them apart will eventually pick the wrong one at the worst moment — here, on the only path whose whole job is remembering.

Addendum (2026-09-18). The lock's other job is saying which tree you have, and two ways it could not were found on a live consumer install rather than reasoned about. The first: without --ref, an update copies your Sysop clone's working tree while the lock records that clone's HEAD. The two are the same file only when the clone is clean, and a maintainer's clone routinely is not — so a consumer can be running a line of code that exists in no commit, under a SHA whose tree does not contain it, and a maintainer triaging against that SHA reproduces the wrong thing. The run now says so at the moment it happens and the lock carries the fact, because the alternative — refusing — would break the ordinary way the tool is developed. The second: --update deliberately leaves its result uncommitted, so running it twice before committing is not an accident, it is Tuesday. The second run's pre-update snapshot then stamps itself with the commit being moved to, over a tree already holding part of that move — so the one commit whose entire purpose is answering where was I before this answers with where you were going.

A companion pair closed the same week is about migrations rather than revisions, and it is the more ordinary failure of the two. When a vendored directory moves, the installer scans the consumer for references to the old location — and that scan had been reporting files whose only mention of the old path was a record of the migration itself, which is append-only and correct. A report that is mostly noise is a report nobody reads, and the instinct was to reuse an exclusion list already sitting in the same file. It answers a different question, and it would have suppressed the one live reference a maintainer could act on. What ships is a purpose-named predicate of three entries, which is less clever and is the only kind that can be checked. The other half is the hook layer: an armed git hook still pointing at a pre-migration path runs nothing and says nothing, so the health check now reads every armed hook and fails on one, and the arming script names a hook it ships no template for instead of skipping it in silence.

One concrete step off Claude Code

The companion layer — checks, hooks, maps, scripts, workflow docs — has always been plain files with no Claude dependency. The skills are the part that was Claude-Code-shaped, and portability there was design intent rather than a track record. Phase 142 made two of them natively selectable in a second harness: on install, Sysop registers the review skills with Codex as two relative symlinks.

.agents/skills/codebase-review -> ../../.claude/skills/codebase-review
.agents/skills/security-audit  -> ../../.claude/skills/security-audit

Links, not copies and not conversions — one source of truth per skill and no drift surface, plus the native selectors no instruction-file prose can provide. In a verification run, a fresh isolated Codex session given an ordinary request — no Sysop filename, no kickoff phrasing — selected the right installed workflow on its own. That run was against a loop-mode install; full mode links the same two paths, so the same behavior is expected rather than measured. The ownership boundary is narrow on purpose: Sysop owns exactly two directory entries, so a consumer's root AGENTS.md, other .agents/ content, and sibling skills all survive install, update, and uninstall untouched. A path Sysop does not own is never adopted, a collision is a hard error rather than a silent skip, and --no-codex-links opts out permanently (the choice is recorded in the lock, so it survives later updates).

The honest limit, stated here as it is in the README: one step is not a track record. Claude Code is where this has been driven day to day, and portability has at least one measured hard edge — the security-audit refusal § III describes. Treat the two review skills as portable to different degrees, not equally.

Model roles, not model pins

Since Phase 69, skills that pin a model declare a role — reasoning, planner, plan-review, executor, convention-gate, security-gate, mechanical, or quick — rather than a model id (since Phase 349 every skill pins one; a pinned model that is unavailable falls back to the session model). Install resolves roles through .claude/served_models.yml (default mapping: reasoning → opus, mechanical → sonnet, quick → haiku, with planner and plan-review following reasoning until you separate them; since Phase 381 convention-gate and executor default to sonnet; security-gate shipped on sonnet for five days and follows reasoning again since Phase 389, after a replay of eight blocked closes reproduced 3 of 8 on the cheaper model; an efforts: map sets a level per role, high for the reasoning tier, the executor and the convention gate); a consumer remaps any role in served_models.local.yml, which is never overwritten and layered on top — so an upstream model sunset and a local preference both survive the next update. A valid preference, at least: since Phase 223 the installer validates the mapping before it applies it, and refuses one it cannot resolve to a model the skills are able to pin — loudly, and leaving the skills at Sysop's shipped defaults rather than writing a mapping that would fail later, mid-skill. The default served: allowlist also pre-lists fable — nothing maps to it by default, but opting the deep-reasoning tier into Claude Fable 5 is a one-key local override (reasoning: fable).

And, deliberately, no setting for the default branch

The mirror image of that indirection is worth stating, because the instinct to add a second config key is strong and it was wrong here. This page says main throughout, and until Phase 252 so did the lifecycle scripts — hard-coded at ten behavioural sites. (The skill bodies took two further phases, and one shipped script outlasted both.) On a master-default repository every one of them failed quietly. git merge-base --is-ancestor X main on a repo with no main exits 128 with a fatal: on stderr, where a genuine “not merged” exits 1 in silence — but the call sites tested truthiness with stderr redirected to /dev/null, which collapses the two into one answer. So a claim left a lock with no status flip, a close skipped every batch as unmerged, and a cleanup reclaimed nothing — each of them reporting success.

The fix is a resolver, and the interesting part is what it refuses to do. It reads the remote's own declaration first, falls back to whichever of main/master exists when there is exactly one, and otherwise refuses with the git command that settles it rather than guessing. There is no Sysop-side setting for the answer, on purpose: git already owns this fact, and a second place to declare it is a second place for it to disagree. Contrast § Merge policy, which is a config key — because how you want work to land is a preference no tool can read off the repository, while what the default branch is called is not a preference at all. A config key is the right answer only when the fact is genuinely yours to state.

Addendum (2026-09-08). A key that passes that test arrived shortly after, and it is useful precisely because it is the contrast case. Workspaces were built at a fixed offset from the repository — its parent directory — which is a sensible default and not a fact git owns. It becomes a problem under a sandboxed harness, where the agent is handed an explicit list of writable paths: a workspace in the repository's parent forces that list to include the parent, and with it every unrelated checkout sitting beside it. So the parent is now nameable — WORKTREE_ROOT, honoured by both the single-task claim and the batch loop — defaulting to exactly what it was before. Where you want scratch workspaces to live is a preference no tool can read off the repository — which is the whole of the argument above, arriving at the opposite answer because the fact is a different kind of fact. It refuses rather than repairs at every arm, because each failure is otherwise silent: a root that does not exist is refused rather than created, since a typo would otherwise scatter directories and succeed; one that is not writable is refused before a claim comes to depend on it; one containing a newline is refused because the lock's record of it is line-anchored; and a root inside any git working tree is refused — widened a phase later from “this repository's” once the reason was stated plainly, since a workspace there is untracked content the checkout's own cleanup paths would sweep, and that reason does not care which checkout it is.

Where things live in a consumer repo

consumer-repo/
├── tasks/                          # queue lives here
│   ├── index.yml                  # source of truth · schema-validated by validate_tasks.py
│   ├── vision.md                  # optional intent layer · /intake-authored, consumer-owned (Phase 60a)
│   ├── decisions.md               # ratified product decisions · re-checked on re-intake (Phase 60a)
│   ├── open/                      # task body files for status: open
│   └── archive/                   # completed task bodies (git mv from open/)
├── .claude/
│   ├── settings.json              # permission allow-list + hooks
│   ├── convention_map.md          # core + per-pack content concatenated
│   ├── security_map.md            # OWASP map · pack-extended
│   ├── checks.yml                 # concat of pack check fragments
│   ├── served_models.yml          # role → model mapping · override via served_models.local.yml (Phase 69)
│   └── *.project.{md,yml}          # consumer-authored extensions (Phase 24a) · promotion writes land here (Phase 78)
├── .agents/skills/                # Phase 142 · two relative symlinks into .claude/skills/
│   ├── codebase-review            # → ../../.claude/skills/codebase-review
│   └── security-audit             # → ../../.claude/skills/security-audit
├── sysop/                         # the vendor dir — everything Sysop owns (Phase 128)
│   ├── scripts/
│   │   ├── claim_task.sh          # lock + worktree + branch creation
│   │   ├── close_batch.sh         # review_tasks.md batch closure
│   │   ├── validate_tasks.py      # schema enforcement · on demand / opt-in pre-commit
│   │   ├── self_check.sh          # Phase 133 · install health probes · Phase 143 abandoned-round probe · Phase 149 coverage ledger
│   │   ├── ingest_security_report.py  # Phase 144 · claude-security parse + sanitize boundary
│   │   ├── permission_denied_hook.py  # Phase 36 · classifier escape injection
│   │   ├── parse_subagent_envelope.py # Phase 37 · structured envelope parsing
│   │   ├── run_checks/            # Phase 49 split · Phase 61 coverage.py · Phase 135 accounting.py
│   │   └── sysop-update.sh        # the consumer-side update entry point
│   ├── docs/                      # WORKFLOW.md, WORKFLOW_GUIDE.md
│   ├── runtime/                   # gitignored orchestration state — one ignore entry covers it (Phase 133)
│   │   ├── locks/                 # lives in main repo · git-common-dir resolved (Phase 32)
│   │   ├── pending-docs/          # /document-work writes here · /review-close consumes
│   │   ├── pending-rounds/        # Phase 143 · round-open markers · a survivor means a round died
│   │   ├── round-receipts/        # Phase 149 · closed rounds' Tier-0 coverage ledgers · newest 50
│   │   ├── auto-build/            # /auto-build per-worktree park-verdict scratch
│   │   ├── claim/                 # Phase 171 · <CLAIM_ID>/<RUN_ID>/ · one file per orchestration seam
│   │   ├── parked/                # why a claim stopped — /auto-build archives plan + verdict, /claim-task a --resume pointer (Phase 65a; 159b moved it out of auto-build/)
│   │   ├── preserved/             # Phase 354 · gitignored files only a worktree held, copied out before /review-close removes it
│   │   ├── close/                 # Phases 363–366 · one run directory per --prepare / --request-review close · holds/ for an unattended night (Phase 380)
│   │   ├── close-arms/            # Phase 388 · one receipt per close: the step files it required and the ones that loaded
│   │   ├── gate-verdicts/         # Phase 394 · a gate’s APPROVED, reused while nothing it read has moved
│   │   ├── pins/ · scratch/       # Phase 421 · each placed reviewer’s detached checkout and its own scratch directory
│   │   ├── giveback/              # Phase 377 · bodies the give-back skills stage for gh / git tag
│   │   ├── auto-build-halt.md     # Phase 361 · an unattended run stopped on a claim it could not verify · a human deletes it
│   │   └── subagent-envelopes/    # Phase 37 · SubagentStop hook writes JSON here · Phase 159a keys it per stage
│   └── SYSOP_ISSUES.md            # friction log · seeded once, project-owned · /report-issues files upstream