Protocol 2.0: CodeCrew Moves Out of Your Repository's Root (2026-09-07)
CodeCrew shipped v2.0.0 on 6 September — its first protocol major — and v2.0.1 a day later. In between, one operator moved a fleet of repositories onto the new layout in an evening; every fix in the second release came out of that migration and of working the new verbs on a milestone of their own. This is the field report: what broke, why it was allowed to break, what you run if you are already on CodeCrew, and what deliberately stayed exactly where it was.
Why a protocol major¶
Until 2.0, CodeCrew wrote its operational files into the root of whatever repository it was installed in: a .codecrew.yml pointer, and a roles/ directory holding the role contracts. roles/ is not CodeCrew's name to take. It is Ansible's, for one, and common enough besides that the framework was quietly competing for a directory name in repositories it does not own. Moving those files changes paths that adopters' tooling and every dispatched agent session resolve directly, and under the spec a change that invalidates existing pointers is a protocol major.
A major is also the one moment a break like that is cheap, and the milestone that carried it says so in as many words: "about a dozen repos, one operator". So before shipping, the operator commissioned two fresh-context scans of the protocol's public surface — one by Codex, one by Claude — with a single brief: what else would force a 3.0 within weeks if 2.0 shipped as the layout move alone. Both sets of findings are attached to that issue verbatim, and the operator's Decision on them records which were adopted and which were not.
The decision that shapes the upgrade, though, is the one about compatibility: there is no dual-read and no shim anywhere. A 2.0 binary meeting a 1.x repository refuses, names the verb that moves it forward, and stops. The reasoning is the essay this project grew out of, which — assessing a heavier process framework from inside a run of it — found that most of the machinery was "portability scaffolding and scar tissue", guards numbered after the incidents that produced them. A compatibility shim is scar tissue by construction: written once, depended on quietly, removable only at the next major. Against a dozen repositories and one operator, refusing outright cost an evening. The shim would have cost forever.
What changed¶
Five breaks. The migration resolves every one of them except the fourth, which is not about files at all.
The layout. Every CodeCrew-owned operational file now lives under .codecrew/: the pointer at .codecrew/config.yml, the contracts and their local extensions at .codecrew/roles/, and the agent entry point at .codecrew/AGENTS.md — hub and spoke alike. A repository still on the old shape raises refused[LAYOUT_LEGACY], naming what it found and gh codecrew migrate.
Identities are typed. A row in the routing table names its holder as ~ for the operator, app:<slug>, user:<login> or team:<org>/<slug>; a bare string is refused IDENTITY_UNTYPED. Under 1.0 the CLI inferred the kind of principal from the string, which it cannot reliably do — a GitHub App is written one way in a config file and another in the account that acts for it. The grammar types the GitHub principal, not who is at the keyboard.
Routing fails closed. A spoke resolves its seats by fetching the hub's table over the API. Under 1.0, a fetch that failed degraded to the spoke's own empty table — and an empty table resolves every seat to the operator, which silently turned task finish's holder-review gate into "any non-author approved" and milestone close's verdict count into "anyone commented". Those are the two gates the protocol exists to enforce, and they were failing open on a 404. The layout move would have guaranteed that 404 for the length of a migration window, in both directions of skew, which is how the scan found it. A spoke that cannot read its hub now refuses HUB_UNREADABLE; a hub that reads fine and declares no table is a different condition, and still legitimately the operator everywhere.
The record grammar tightened. This is the one break that is not about files, because it reclassifies text already written on GitHub. A gate is read per paragraph rather than per comment; only a **Gate resolved:** comment resolves one; verdict supersession is per comment, latest wins; and a requirement ID under a milestone's ## Requirements must carry that milestone's own number. Code spans, fenced blocks and four-space indented blocks are stripped before any of those scans run, so a verdict quoted as an example no longer counts as a verdict — which it did, and which is exactly the shape a paste picks up when nobody reaches for backticks.
The 1.0 shims are gone. Three of them, each a behaviour an adopter could have depended on, which is why they go at a major: the pre-1.0 codecrew: "0.1" pointer, a coordinator row inferred when a declared table omits one, and a task's first assignee standing in for a start record. The last had teeth — it handed every assigned-but-never-started task an implicit owner for the ownership gate — so task finish on a task nothing records a start for now refuses NOT_OWNER. 2.0 also wrote down the machine contract that had only ever been implied: every failure exits 1, the refused[CODE]: detail line on stderr is the channel a caller branches on, and every refusal code the CLI promises is now catalogued in one table in the spec's CLI section — forty-three of them today. Silence about a contract is what makes a later change breaking.
What an adopter does¶
Straight from the changelog's 2.0.0 entry:
gh extension upgrade codecrew(orgh extension install radiusred/gh-codecrew), once per machine.- In each repository,
gh codecrew migrate --dry-runto see every step, thengh codecrew migrate— hubs before their spokes, because a spoke resolves its routing by reading the hub's.codecrew/config.ymland refusesHUB_UNREADABLEwhile the hub is still on 1.x.git show— read the commit. The files move bygit mv, so their history follows them; the part worth reading is the pointer's rewrite, which types every identity in the routing table.git push -u origin HEAD, then open the pull request.migratenever pushes: landing the move is the operator's act.- If the output ends with an
action neededblock, paste the lines it prints into each rootAGENTS.mdorCLAUDE.mdit names. A 1.x root entry point holds the old instructions and does not reach.codecrew/AGENTS.md;migratenever edits a file the project owns. A root entry point the repo does not have is written for you, from the same scaffoldinituses (2.0.1, #306).
That last sentence is the newest of them, and it is there because of the migration described below. The move also brings the repository's cc: labels to the protocol's defaults, the one step that needs GitHub at all: a repository the verb cannot reach gets a note, the migration still stands, and a rerun finishes the job. A repository already on the 2.0 layout has no migration step to take — upgrade the extension and carry on.
What deliberately did not change¶
The scans looked at the rest of the protocol's public surface too, and the other half of the operator's Decision is the list it blesses as permanent rather than changing: the M<n>: title prefix that gives a milestone its identity, the cc: label prefix, task/<n>-<slug> branch names, and ROADMAP.md at the repository root with docs/milestones/ beneath docs/. The roadmap and the milestone records are the human-facing product of the protocol, and they stay where readers look for them. AGENTS.md and CLAUDE.md stay at the root as well — they now point at .codecrew/AGENTS.md rather than holding the instructions themselves, so a harness that reads either one still lands in the right place.
Nothing about a 1.x project's history changes either. The issues, the labels, the branches, the recorded comments and the roadmap are untouched by the move; what changes is where CodeCrew's own files live and what the pointer says.
What the migration taught¶
The fleet ran the same evening the release was cut, and the M14 record has the trail: a migration record of eleven repositories, ten of them migrated in two commits each, and one — a protocol-0.1 sandbox — refused MIGRATION_UNSUPPORTED by design. That refusal was the first time any of migrate's conservative guards met a repository nobody had built to test it. A twelfth repository named on the checklist appears in neither the record nor the fleet; the QA seat found the gap and returned the requirement not satisfied, and the operator's Decision settled it as retired rather than migrated, rejecting "migrating an empty project for the sake of the count".
Running the new verbs on a milestone of their own, and moving ten repositories with them, then raised seven captures — and every one was an implementation correction rather than a protocol change. task start was making an assignment call that could only ever fail, because GitHub does not accept an App as an issue assignee, and printing an error-shaped note about it on every run (#287). Every listing read stopped at GitHub's first hundred rows, so a milestone issue past a hundred comments lost its newest QA verdicts and refused to close (#264). An issue body the GitHub web editor had saved with CRLF defeated the line-anchored record scans entirely — a task edited in the browser yielded no adoptions at all (#296), and nothing asserted that task new applied the label every downstream gate reads (#297). migrate left five of the migrated repositories without the root entry points init would have written (#301). A skipped stale-branch sweep was invisible until the next milestone close (#295). And a pull request body's example prose handed GitHub two closing references nobody intended (#303).
v2.0.1 fixes all seven, under a milestone whose own goal names them one by one. The protocol stays at 2.0 through it: nothing already recorded on GitHub is reclassified, and a repository on the 2.0 layout has nothing to run.
That is the shape of the thing, and it is the argument for keeping the record in the first place. The reasoning above is not reconstructed from memory — it is on the issues, in Decision comments written at the moment each call was made, and this post is a compilation of them by the seat whose job that is. The milestone documents are where the long versions live, including the parts that went wrong.