gh codecrew migrate: the one-shot move from the 1.x layout to 2.0 (#256) - #280
Conversation
There was a problem hiding this comment.
PR #280 Review Verdict
Verdict: request changes.
Finding:
internal/cli/migrate.go:203falls back from anygit mvfailure to
os.Rename, andinternal/cli/migrate.go:207then renames onto the
destination path. That is only valid when the source is untracked. If a
target path such as.codecrew/roles/qa.mdalready exists while
.codecrew/config.ymldoes not,git mvfails because the destination
exists, the fallback overwrites the existing 2.0 file, and the old tracked
source deletion is left outside the migration commit. This violates M13-R2's
"one pathspec commit" and "every refusal before anything is written"
requirements. The fix should preflight destination conflicts, or only use
the filesystem fallback after proving the source is untracked; add a test
for an existing destination file.
Verification:
go test ./...failed in this workspace before the migrate-specific tests:
TestLoadConfigRefusesTheLegacyLayoutandTestLoadRefusesTheLegacyLayout
saw/tmp/.gitas the repository root fort.TempDir()and reported
"no .codecrew/config.yml found" instead of the legacy-layout refusal.go test ./internal/cli -run 'TestMigrate|TestRewritePointer|TestTypeIdentity' -count=1passed.go vet ./...passed.gofmt -l .was clean after removing generated Go cache files from the
in-repo cache directories requested for the run.go build -o ./gh-codecrew ./cmd/codecrewpassed.- A scratch old-layout repo under the workspace confirmed
migrate --dry-run
wrote nothing; a live scratch run made one local commit, preserved unrelated
staged/unstaged work, and had no remote configured.
Record:
…untracked source (#256) `applyMoves` fell back from any `git mv` failure to `os.Rename`. The fallback exists for one case — a 1.x file git does not track, whose path cannot go in the commit's pathspec — but "destination exists" is also a `git mv` failure, so a repo holding `.codecrew/roles/qa.md` with no `.codecrew/config.yml` above it had its 2.0 file renamed over and the tracked source's deletion left outside the migration commit, breaking both of M13-R2's guarantees at once. Two rules now. Every destination is stat'd before the first move and any that exists refuses `BOTH_LAYOUTS` naming it and the file that would have moved there: the same fact the pointer check catches one level up, and the same act asked of the operator. And the filesystem fallback is guarded by `git ls-files --error-unmatch`, so git refusing to move a file it tracks is returned rather than worked around. SPEC §6, the introduction's `BOTH_LAYOUTS` entry and the CHANGELOG widen to the two shapes. Tests: an existing destination refuses with the 2.0 file untouched and a clean index, and an untracked pointer still moves and commits. checky's finding on PR #280. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
…256) `gitRoot` judges the 1.x layout at the nearest ancestor holding a `.git` entry, so `TestLoadRefusesTheLegacyLayout` and `TestLoadConfigRefusesTheLegacyLayout` inherited whatever sat above the machine's temp root: a stray `/tmp/.git` made both report the not-a-CodeCrew-repo error instead of the layout refusal, and the suite passed or failed by machine. Each case now runs in a repository of its own — `gitRepo(t)` in `internal/cli`, and a `repoDir(t)` in `internal/config` that makes the `.git` directory `gitRoot` stats for, no subprocess from that package's tests. Reproduced with TMPDIR set to a directory holding a `.git`: both tests failed with the reported message before, and pass after. checky's verification note on PR #280; the tests are #255's. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
|
Both findings fixed; new head The blocking finding is right and the bug was real. Two rules now, both of which you named. Every destination is stat'd before the first move, and any that exists refuses before anything is written. And the filesystem fallback is reached only after The preflight reuses Two tests added. Your verification note is taken in this PR, per the coordinator. I reproduced it with |
`gh codecrew migrate` moves a protocol 1.0 hub or spoke to the 2.0 layout in one local commit and never pushes. Exempt from the pointer check as `init` is — the repos it exists for are exactly the ones every other verb refuses — and, like `init`, it runs only at the repository root. It git mv's `.codecrew.yml` to `.codecrew/config.yml` and the ten recognised names out of `roles/` — the five contracts and their `.local.md` extensions — into `.codecrew/roles/`, removes the emptied directory, and rewrites the pointer in place: `codecrew: "2.0"`, a `coordinator` row when the table has none, and every identity typed per M13-R4. The rewrite is a yaml node round-trip, so the file's comments, blank lines and key order survive the move. `roles/` is read at all only when it holds one of the ten: a project's own `roles/` is never touched, which is the collision the layout move exists to end. When the directory is CodeCrew's, an entry outside the ten refuses `FOREIGN_ROLES_DIR` rather than guessing. A bare identity is typed by asking GitHub — `users/<value>`, then `users/<value>[bot]`, since a 1.0 table wrote an App as its bare slug — and a value nothing or two things answer to refuses `IDENTITY_UNRESOLVED`. Both layouts at once refuses `BOTH_LAYOUTS`; a pointer that is not protocol 1.x refuses `MIGRATION_UNSUPPORTED`. A repo already on 2.0 says so and writes nothing, and `--dry-run` prints every step and writes nothing. `commitScaffold`'s pathspec commit is extracted into `pathspecCommit`, the machinery both one-shot verbs share, and `config` exports the 1.x layout names and the five role names it already held. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
SPEC §6 gains the verb's row — what it moves, what it rewrites, what it refuses, and that the commit is local — and §10's 2.0 paragraph names `gh codecrew migrate` and says the move ends in a commit the operator reads and pushes. `docs/introduction.md` gains a `migrate` block in the refusal catalogue with its four codes (`BOTH_LAYOUTS`, `FOREIGN_ROLES_DIR`, `MIGRATION_UNSUPPORTED`, `IDENTITY_UNRESOLVED`), taking the count from thirty-five to thirty-nine with the README's index of it, and the verb joins the install-and-use block. The CHANGELOG gains the verb's own entry, and the protocol 2.0 entry gains the steps an adopter runs: upgrade the extension, `migrate --dry-run`, `migrate`, read the commit, push and open the PR. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
…untracked source (#256) `applyMoves` fell back from any `git mv` failure to `os.Rename`. The fallback exists for one case — a 1.x file git does not track, whose path cannot go in the commit's pathspec — but "destination exists" is also a `git mv` failure, so a repo holding `.codecrew/roles/qa.md` with no `.codecrew/config.yml` above it had its 2.0 file renamed over and the tracked source's deletion left outside the migration commit, breaking both of M13-R2's guarantees at once. Two rules now. Every destination is stat'd before the first move and any that exists refuses `BOTH_LAYOUTS` naming it and the file that would have moved there: the same fact the pointer check catches one level up, and the same act asked of the operator. And the filesystem fallback is guarded by `git ls-files --error-unmatch`, so git refusing to move a file it tracks is returned rather than worked around. SPEC §6, the introduction's `BOTH_LAYOUTS` entry and the CHANGELOG widen to the two shapes. Tests: an existing destination refuses with the 2.0 file untouched and a clean index, and an untracked pointer still moves and commits. checky's finding on PR #280. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
…256) `gitRoot` judges the 1.x layout at the nearest ancestor holding a `.git` entry, so `TestLoadRefusesTheLegacyLayout` and `TestLoadConfigRefusesTheLegacyLayout` inherited whatever sat above the machine's temp root: a stray `/tmp/.git` made both report the not-a-CodeCrew-repo error instead of the layout refusal, and the suite passed or failed by machine. Each case now runs in a repository of its own — `gitRepo(t)` in `internal/cli`, and a `repoDir(t)` in `internal/config` that makes the `.git` directory `gitRoot` stats for, no subprocess from that package's tests. Reproduced with TMPDIR set to a directory holding a `.git`: both tests failed with the reported message before, and pass after. checky's verification note on PR #280; the tests are #255's. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
… value's (#256) #259 landed `GH_UNREACHABLE` while this branch was open: GitHub out of reach is its own condition, never reported as something else. Typing a bare 1.0 identity is a question only GitHub can answer, so migrate's lookup now classifies the failure through the shared `unreachable` helper and keeps `IDENTITY_UNRESOLVED` for what it should always have meant — GitHub answered and the answer did not settle the value. SPEC §6's row, the introduction's migrate block and the CHANGELOG follow. The refusal-code count is unchanged at forty-two: `GH_UNREACHABLE` is #259's, reused rather than added. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
937b4af to
0503914
Compare
|
Rebased onto Conflicts, all resolved keeping both sides. The refusal-code count is forty-two, recounted against One behavioural change the rebase asked for, as its own commit. #259 landed
One thing I did not do, flagged rather than decided: a migrated 1.x hub ends up with no |
A migrated 1.x repo landed on the 2.0 layout with no 2.0 entry point: its instructions still lived in the root `AGENTS.md`, describing `roles/` and `.codecrew.yml`, paths the migration had just removed. R2 moves a 1.0 repo to the 2.0 layout and R3 says what that layout's entry point is, so the move is not finished without it (the operator's call, 2026-09-06). migrate now writes `.codecrew/AGENTS.md` from the same scaffold `init` writes, when the repo has none, in the migration's own pathspec commit; and it treats the root `AGENTS.md` and `CLAUDE.md` exactly as `init` treats a kept one, through the same `reachesInstructions` — a file that already arrives asks for nothing, one that does not is named under an `action needed` heading with the exact lines to paste, and neither is ever rewritten, being the project's. `--dry-run` lists the write and prints the block. `init`'s block is extracted into a shared `entryPointAction` printing `entryPointLines` verbatim, so the two verbs cannot drift into telling an operator to paste different lines; `init`'s own wording is unchanged. SPEC §6's row, the introduction's migrate block and the CHANGELOG's migration steps say what the operator will see. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
|
The AGENTS.md gap is taken, on the operator's call. New head The scope reading that makes it R2's: R2 moves a protocol 1.0 repo to the 2.0 layout, R3 says what that layout's entry point is, and a migration that produces every other 2.0 file but not The shape is
Two tests as asked, plus the dry run. A 1.x hub whose root SPEC §6's row, the introduction's migrate block — which now opens with a short non-refusal note, since the |
There was a problem hiding this comment.
Request changes.
Finding:
internal/cli/migrate.go:454 starts rewriting any roles: table it finds:
roles := mapValue(root, "roles")
That misses the rebased 2.0 rule from #279/M13-R5: a spoke pointer carrying a roles: block is refused as SPOKE_ROUTING. A legacy spoke like:
codecrew: "1.0"
hub: radiusred/gh-codecrew
roles:
reviewer: { identity: radiusred-checky }is accepted by migrate; it commits .codecrew/config.yml with hub: radiusred/gh-codecrew and a typed roles: table, then this same binary immediately refuses gh codecrew status with refused[SPOKE_ROUTING]. That is not a completed one-shot move to the 2.0 layout.
Fix: before writing anything, detect hub != self plus a roles: mapping and refuse (reusing/recording the appropriate existing code, likely SPOKE_ROUTING after #279), or get an operator Decision for a different migration rule. Add a test that migrates a 1.x spoke with a roles: block and proves the live verb does not commit an invalid 2.0 pointer.
Verification performed:
go test ./...withGOCACHE=$PWD/../gocache GOMODCACHE=$PWD/../gomodcache: pass.go vet ./...: pass.gofmt -l .: no output.- Hostile
TMPDIRholding a.git:go test ./internal/cli ./internal/configpass. - Mutation check in a separate worktree with the destination preflight removed:
TestMigrateRefusesAnExistingDestinationfails, so the preflight is covered. - Manual scratch 1.x hub:
migrate --dry-runleft HEAD andgit statusunchanged; livemigratemade one local pathspec commit of the moved/written files and did not push. - Refusal-code source count: 42 distinct
refuse("CODE"values in non-testinternal/clisource; #279'sHUB_UNREADABLE,SPOKE_ROUTING,GH_UNREACHABLEare present, and this PR adds four migrate-specific codes. - Rebase check: #278 entry-point and #279 routing/fail-closed SPEC and CHANGELOG sections are still present.
#256) Protocol 1.0 let a spoke's pointer carry a routing table; 2.0 does not (M13-R5, #259). Migrating one forward wrote a `.codecrew/config.yml` with `hub: owner/repo` and a typed `roles:` table — a pointer this same binary refuses `SPOKE_ROUTING` on the very next verb, so the one-shot move had not landed. The check runs before anything is written, beside the protocol-major one: a `hub` that is not `self` plus a `roles:` mapping with at least one row is refused, naming the hub and the rows in sorted order, as `config.Parse` names them. An empty `roles:` key is not a table, the same reading `Parse` takes. The block is not dropped on the operator's behalf: it is routing they wrote, and whether it moves to the hub or goes away is a question only they can answer. The refusal says both, and names `.codecrew.yml` — the file they have to edit — rather than the path it would have moved to. SPEC §6's row, the introduction's migrate block and the CHANGELOG follow. The refusal-code count is unchanged at forty-two: `SPOKE_ROUTING` is #259's, reused rather than added. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
|
Fixed; new head The finding is right, and it is the sharper version of the same rule the first round caught: the test of a one-shot move is whether the repo it leaves behind is one this binary reads. Migrating a 1.x spoke's The check now runs beside the protocol-major one, before anything is written: a I did not drop the block. It is routing the operator wrote, and whether those rows belong in the hub or belong nowhere is the one question migrate cannot answer for them — so the refusal offers both and names Three test changes. SPEC §6's row, the introduction's migrate block and the CHANGELOG's refusal bullet all carry it. |
There was a problem hiding this comment.
Approved. Round three verified the remaining spoke-routing fix: a 1.x spoke pointer with hub != self and a non-empty roles: table now refuses SPOKE_ROUTING before destination preflight, before identity rewriting, and before any git write. I confirmed the order in code and with a scratch repo that also had an existing destination clash: the observed refusal was SPOKE_ROUTING, HEAD stayed on the fixture commit, .codecrew.yml remained, .codecrew/config.yml and .codecrew/AGENTS.md were not written, and git status stayed clean.\n\nThe targeted regression test fails when checkSpokeRouting is disabled, and the ordinary spoke path without a roles block still migrates and commits locally. Full verification also passed: gofmt -l ., go test ./..., go vet ./..., go build -o ./gh-codecrew ./cmd/codecrew, plus read-only migrate --dry-run on the PR checkout. The #256 Decision records refuse-rather-than-drop, docs/changelog/help are in step, and the PR body closes #256 and adopted capture #281.
…untracked source (#256) `applyMoves` fell back from any `git mv` failure to `os.Rename`. The fallback exists for one case — a 1.x file git does not track, whose path cannot go in the commit's pathspec — but "destination exists" is also a `git mv` failure, so a repo holding `.codecrew/roles/qa.md` with no `.codecrew/config.yml` above it had its 2.0 file renamed over and the tracked source's deletion left outside the migration commit, breaking both of M13-R2's guarantees at once. Two rules now. Every destination is stat'd before the first move and any that exists refuses `BOTH_LAYOUTS` naming it and the file that would have moved there: the same fact the pointer check catches one level up, and the same act asked of the operator. And the filesystem fallback is guarded by `git ls-files --error-unmatch`, so git refusing to move a file it tracks is returned rather than worked around. SPEC §6, the introduction's `BOTH_LAYOUTS` entry and the CHANGELOG widen to the two shapes. Tests: an existing destination refuses with the 2.0 file untouched and a clean index, and an untracked pointer still moves and commits. checky's finding on PR #280. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
…256) `gitRoot` judges the 1.x layout at the nearest ancestor holding a `.git` entry, so `TestLoadRefusesTheLegacyLayout` and `TestLoadConfigRefusesTheLegacyLayout` inherited whatever sat above the machine's temp root: a stray `/tmp/.git` made both report the not-a-CodeCrew-repo error instead of the layout refusal, and the suite passed or failed by machine. Each case now runs in a repository of its own — `gitRepo(t)` in `internal/cli`, and a `repoDir(t)` in `internal/config` that makes the `.git` directory `gitRoot` stats for, no subprocess from that package's tests. Reproduced with TMPDIR set to a directory holding a `.git`: both tests failed with the reported message before, and pass after. checky's verification note on PR #280; the tests are #255's. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr
What this does
M13-R2 in full:
gh codecrew migrate, the one-shot move from the protocol1.0 layout to 2.0. It reads no pointer, so it is exempt from the protocol
check the way
initis — the repos it exists for are exactly the onesevery other verb now refuses
LAYOUT_LEGACYon — and, likeinit, it runsonly at the repository root. Everything it does lands in one pathspec
commit on the current branch, which it never pushes.
What it moves.
.codecrew.yml→.codecrew/config.yml, and out of aroot
roles/only the ten names CodeCrew owns — the five contracts andtheir
<role>.local.mdextensions — into.codecrew/roles/, bygit mvso the history follows; the emptied directory is removed. A root
roles/is read at all only when it already holds one of the ten, so a project's
own
roles/(Ansible's — the collision the layout move exists to end) isnever touched. Once the directory is CodeCrew's, an entry outside the ten
refuses
FOREIGN_ROLES_DIRnaming it.What it rewrites. The pointer, in place:
codecrew: "2.0", acoordinatorrow when the table declares none, and every identity typedper M13-R4 —
~and already-typed values pass through, a slash meansteam:, and a bare login is resolved by asking GitHub (users/<login>,then
users/<login>[bot]). The rewrite is ayaml.Noderound-trip, so thefile's comments, blank lines and key order survive; the added
coordinatorrow takes the style its siblings use.
What it refuses, all before anything is written:
BOTH_LAYOUTS,FOREIGN_ROLES_DIR,MIGRATION_UNSUPPORTED(a pointer whose protocolmajor is not 1),
IDENTITY_UNRESOLVED(a bare value that answers tonothing, to both a user and an App, or to an organization). A repo already
on 2.0 says so, writes nothing and exits 0.
--dry-runprints every stepand writes nothing.
Run against a real copy of this hub's own pre-#276 1.0 pointer, with the
live users API:
The live run committed those seven renames as renames, and the migrated
pointer loads under this binary with its comments intact.
Requirements
gh codecrew migrate, in full.Commits
feat(migrate)— the verb (internal/cli/migrate.go), its tests, thecli.gowiring,commitScaffold's pathspec commit extracted into theshared
pathspecCommit, andconfig's 1.x layout names and role namesexported (
LegacyPointer,LegacyRolesDir,RoleNames).docs— SPEC §6 and §10,docs/introduction.md, the README's count,the CHANGELOG.
fix(migrate)— checky's finding: an existing destination refusesbefore anything is written, and the filesystem fallback is reached only
after
git ls-files --error-unmatchproves the source untracked.test(config)— the twoLegacyLayoutcases get a repository of theirown, so a stray
.gitabove the machine's temp root cannot decide wherethe layout is judged.
fix(migrate)— after the rebase onto The entry point: .codecrew/AGENTS.md, root pointer, and a line to add when AGENTS.md exists #257 and Routing fails closed: HUB_UNREADABLE, protocol skew, spoke roles refused, offline named #259: an unreachableGitHub is named by
GH_UNREACHABLE, Routing fails closed: HUB_UNREADABLE, protocol skew, spoke roles refused, offline named #259's code, instead of beingfolded into
IDENTITY_UNRESOLVED.feat(migrate)— the 2.0 entry point rides the migration:.codecrew/AGENTS.mdwritten when absent, the rootAGENTS.mdandCLAUDE.mdreported throughinit's ownreachesInstructionsandnever rewritten.
fix(migrate)— checky's second-round finding: a 1.x spoke pointercarrying a
roles:block refusesSPOKE_ROUTINGbefore anything iswritten, rather than migrating into a pointer the next verb refuses.
Tests
internal/cli/migrate_test.go, temp repositories withgit initand theusers API behind an injectable
lookupAccount: a full 1.x hub (all tenrole files) moved whole with the pointer rewritten, typed, the coordinator
row added,
roles/gone, one commit, and the operator's own staged andunstaged work still there afterwards; an older scaffold (four contracts, no
extensions) moved with nothing invented; a foreign entry beside CodeCrew's
own refused by name with nothing written; a project-owned
roles/leftuntouched while the pointer moves; both layouts →
BOTH_LAYOUTSnamingboth; a spoke → the pointer and its extension, no coordinator row invented;
already 2.0 → exit 0, no commit; a
0.1pointer →MIGRATION_UNSUPPORTEDnaming the version; contracts with no pointer, and no layout at all →
plain errors;
--dry-runprinting every step and writing nothing; the yamlround-trip keeping comments, blank lines and the table's style; and
typeIdentityacross every form including the ambiguous and unresolvableones.
go test ./...,go vet ./...,gofmt -l .clean.Decisions
The
roles/rule — why an unrecognised entry refuses rather than being left and reported, why the directory is read only when it already holds one of ours, and why the ten names are the same in a hub and a spoke.The four refusal codes — their names and meanings, and why the three non-protocol stops stay plain errors.
Resolving a bare identity, and the yaml round-trip — why
users/<login>[bot]is probed second (without it every App-held seat in every 1.0 hub would refuse), why a value both answer to is refused, and the trade-off the round-trip makes.The destination preflight and the test-independence fix — why an existing destination is
BOTH_LAYOUTSrather than a new code, why the filesystem fallback is now guarded, and why the twoLegacyLayouttests were fixed in this PR.No deviations.
For the reviewer
doc list, while the verb has its own
###entry at the top of[Unreleased]per the standing convention. The one cross-referencebetween them is deliberate rather than duplicating the command block.
migrateis not in theplan/--dry-rungate machinery thattask finishandmilestone closeuse. Its checks are not gates anagent clears one at a time — every one of them is fatal and none is
"not applicable" — so it computes the whole move first and then either
prints it or performs it, which is the same single-code-path property
the
plantype exists for.pathspecCommitkeepscommitScaffold's strings byte-identical (thenoun is a parameter), so
init's existing tests still pin them.and left out of the commit's pathspec, since naming it there would fail —
and that is now the only
git mvfailure the fallback answers, provedwith
git ls-files --error-unmatch. Every other one is returned.origin/mainat1004651(The entry point: .codecrew/AGENTS.md, root pointer, and a line to add when AGENTS.md exists #257 and Routing fails closed: HUB_UNREADABLE, protocol skew, spoke roles refused, offline named #259). Conflictsresolved keeping both sides:
cli.go's help (init's new line plus themigrateline) and its verb wiring;config.go(The entry point: .codecrew/AGENTS.md, root pointer, and a line to add when AGENTS.md exists #257'sAgentsFile,Routing fails closed: HUB_UNREADABLE, protocol skew, spoke roles refused, offline named #259's
SpokeRoutingErrorand this branch's exports auto-merged);SPEC §6's
initrow kept frommainwith themigraterow added afterit; the CHANGELOG keeping every section with this one newest-first and
the migration steps still inside the 2.0 entry; the refusal-code count
recounted against
refuse("in non-testinternal/cli— forty-two(main's thirty-eight plus this branch's four; nothing overlaps).
note, because the
action neededblock is the one thing the verbleaves for a human and the section is otherwise a code catalogue. The
count is unchanged at forty-two.
SPOKE_ROUTINGis reused too, not added — feat(routing)!: the routing table fails closed — HUB_UNREADABLE, skew, spoke roles, offline (#259) #279's code, for the one1.0 shape 2.0 does not allow. The block is never dropped on the
operator's behalf: whether those rows move to the hub or go away is
theirs to decide, so the refusal names both and points at
.codecrew.yml, the file they have to edit, not the path it would havemoved to.
TestMigrateSpokenow also asserts that the migrated spokeconfig.Loads, which is the property the finding was really about.GH_UNREACHABLEis reused, not added. Routing fails closed: HUB_UNREADABLE, protocol skew, spoke roles refused, offline named #259 landed the code whilethis branch was open, so migrate's identity lookup now classifies through
the same
unreachablehelper: GitHub out of reach is the network'scondition, and
IDENTITY_UNRESOLVEDkeeps the meaning it should alwayshave had — GitHub answered and the answer did not settle the value.
of 2026-09-06 (I had flagged the gap rather than deciding it). A migrated
1.x repo was landing on the 2.0 layout with no
.codecrew/AGENTS.md, itsinstructions still in the root
AGENTS.mddescribing paths the migrationhad just removed.
migratewrites the file from the same scaffoldinitwrites, inside the migration's own commit, and treats the two root files
exactly as
inittreats a kept one — samereachesInstructions, sameprinted lines, never a rewrite.
init's block is extracted into a sharedentryPointActionso the two verbs cannot drift into telling an operatorto paste different lines;
init's own wording and its test areuntouched.
BOTH_LAYOUTScovers two shapes after the review: a 2.0 pointerbeside the 1.x one, and a 2.0 file already sitting where a 1.x one would
move. Same fact, same act asked of the operator, so the detail rather
than the code says which.
gh api users/<login>is called at most twice per bare row, tencalls for a five-row table, and in
--dry-runtoo — it is a read, and apreview that skipped it would not be a preview.
Closes #256
Closes #281
🤖 Generated with Claude Code
https://claude.ai/code/session_017Zu94NTC5DJskcpMTSofzr