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.
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.
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.
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.
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.
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.
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.
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.
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./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 Opus 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./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.
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.
/document-work) inherit the session model.
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).--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, and a file left behind at each seam in sysop/runtime/claim/<id>/<run>/ — the plan, the planner's integrity check, the review, the classification, the outcome, 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.sysop/runtime/pending-docs/<branch>.md for senior review.## 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? Step 7-pre's resume table routes on six, and the sixth is the gap worth knowing about: option C's marker is what tells a re-entry the run is finished, and Step 2e does not report it. 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. Both are gated on the close having landed on main, so a pr-policy close sitting on an integration branch defers rather than destroys. 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.
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).
/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.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.
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.
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.
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.
/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.
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).
convention_map.md, files findings as batched tasks (Batch 1 / Batch 2 / …) with effort + blast_radius pre-populated. Drained by /auto-fix.security_map.md. Pack-aware (python / postgres / nextjs-react / llm) so findings cite the right CWE and remediation pattern.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./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.--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.--merge shape, but each sub-agent is Opus 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.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. Eight shapes of that silence turned up in real runs across July and August 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 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 shape 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.
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.
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.
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. Two reviewers on distinct lenses by default. A third when the change ships behaviour and its record makes numeric claims a reviewer could falsify — both terms defined, and when it is unclear whether the condition holds, it does not. 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.
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.
Phase 53 added the workflow's first GitHub-touching skill — the exception to this section's title, scoped to bot-authored dependency PRs only.
--execute to act. A client-side replacement for native auto-merge, which Free-plan private repos don't get. Classifier validated against ~50 real PRs.
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.
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 is attributed to the review round that surfaced it — and that attribution is load-bearing: the cross-round recurrence gate reads it, and when an entry later turns out to be wrong, you can find the review that promoted it and learn what changed. 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.
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.
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.
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.
!-prefix is a user-side control the model cannot emit.
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.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, retiring the earlier timing caveat — the parser no longer depends on winning a race against the sub-agent's final message.
/plugin marketplace add getsysop/sysop then
/plugin install sysop@sysop plus any packs. Delivers namespaced slash commands like
/sysop:claim-task.
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.
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.
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.
Since Phase 69, skills that pin a model declare a role — reasoning, convention-gate, mechanical, or
quick — rather than a model id (skills without a pin simply inherit the session model). Install resolves roles through
.claude/served_models.yml (default mapping: reasoning → opus, mechanical → sonnet,
quick → haiku, with convention-gate following reasoning until you separate them);
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).
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.
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 plan + review 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/) │ │ └── 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