Development record
every-change-starts-in-its-own-worktree-the-primary-checkout
Summary
Delivers itd-148: the primary checkout of an abcd-managed repo becomes a read-only surface for every session but a declared coordinator, and every change starts in a worktree the store under the home folder holds (itd-2609091014076309, adr-2609091248200336). All behaviour lives in the transport-agnostic core; the CLI and the plugin markdown surface are thin front doors, and the enforcement hooks are thin callers into the binary.
Rewritten on 2026-09-29 to the product thinker's rulings (itd-148
## Decisions). This spec no longer mints abcd worktree new <slug> or
abcd worktree sweep, and it no longer composes a sweep of its own: the
store intent owns adding, listing and clearing away worktrees, and this spec
calls those verbs. It reverses the scope of the 2026-08-26 planning
interview in exactly that respect; the block, the allow-set, the seeding and
the peer visibility stand.
Blocked on
- The store intent's planning interview (itd-2609091014076309). Ruling Q4
of 2026-09-29 plans the store intent first: nothing from the store intent,
the merged-worktree clean-up included, is built before its planning
interview. This spec waits on it because it calls the store's verbs
(ruling Q2), which is what itd-148's
blocked_byon the store intent carries. - The store's add, list and clean-up verbs shipping, because the refusal names the add verb as the route and the seeding runs on the worktrees the store adds.
- A walk of the criteria after the store is planned, which settles how a session declares itself the coordinator (itd-148's open questions).
Scope
- Core package (
internal/core): tree classification (primary vs worktree, resolved via the git common directory, never a path prefix), the coordinator declaration and its check, and the visibility ledger. Worktree creation, listing and removal are the store's, not this package's. - Verb: the hook entry point, a
checksub-verb beside the store's worktree verbs (its final name follows the store's naming, one of that intent's open questions); the exit code carries the block verdict. It performs zero writes. - Hooks: a PreToolUse-shaped hook routing the host's file-edit and shell tools through the check; a git pre-commit backstop installed in the primary checkout via the ahoy defaults; both honour the coordinator exemption.
- Scaffolding: the prepare/ahoy path installs the hooks and the pre-populated private banlist floor into abcd-managed repos.
- Record changes: resolution of iss-213 and iss-2608230847432285 in the shipping changes. The three other issues this spec once bundled (iss-370, iss-2608230957104179, iss-2608210738378295) were resolved by other changes.
Approach, by acceptance criterion
AC 1 — the mutation block with allow-set. The check receives the tool
input (cwd, file path or command) and answers allow/refuse. Refusal only
when: the tree resolves to the primary checkout of an abcd-managed repo, the
session is not the declared coordinator, AND the write targets a tracked
file or the command is history-moving. The allow-set is explicit:
gitignored/untracked paths, .abcd/.work.local/, fetch/fast-forward of the
default branch, git worktree administration, and the capture carve-out (a
new timestamp-named file under .abcd/work/issues/open/ only). Shell
interception is command parsing at the guard-registry rung, stated as
mitigation, not filesystem guarantee; the pre-commit backstop (same check,
commit-time) catches what parsing misses. Refusal messages name the store's
add verb. Tests: table-driven check tests per allow/refuse case; hook wiring
exercised via the smoke harness.
AC 2 — the declared coordinator. A session is the coordinator only by its own declaration, never by inference; the check allows the coordinator at both layers and refuses every other session. The declaration's form and the refusal of a second declaration are settled at the walk named under Blocked on; the lane is security-reviewed, since the declaration lifts a write block.
AC 3 — guard seeding and peer visibility on entry. Seeding runs on detection, not only creation: any hook fire that finds itself in a worktree of an abcd-managed repo whose local tier lacks the name-guard layer seeds the pointer to the primary checkout's store. The floor's four categories (home/user paths, machine hostnames, personal email, real surname) are populated by one-time setup prompt or from the user-level home; values are machine-local, never derived silently, never committed. Entry/exit events append to a visibility ledger in the primary checkout's local tier; peer-session hook fires inject unseen entries (the record-then-inject shape the rules loader already uses). Delivery is at-next-prompt by design.
AC 4 — the route lands in the store. The refusal's route is the store's add verb; a test follows it from a refused write and asserts the worktree is under the store and nothing new sits in or beside the primary checkout.
AC 5 — mint visibility. Record-id mints append a family-and-checkout event to the same visibility ledger, injected to peers at next hook fire (kept or dropped at the walk named under Blocked on; see itd-148's open questions).
AC 6 — zero cost when idle. The check and bare verbs stay zero-write. A repo with no worktrees and no peers takes one tree-classification stat call in the hook path and nothing else. Read-only verbs are untouched by construction (the block sits on write paths only), verified by tests asserting no writes from bare invocations.
Out of scope
Adding, listing and clearing away worktrees (the store intent, itd-2609091014076309, including the merge proof and the abandoned-worktree dossier this spec once carried), coordination claims (itd-33), post-merge remote/branch residue mechanics (itd-118), presence leases (iss-2608220750029993).
Delivery
Staged PRs after the store ships, each preflight-clean with tests watched
fail first: (1) core classification + the check + hooks; (2) the coordinator
declaration (security review); (3) seeding + visibility ledger; (4)
scaffolding + the bundled-issue resolutions with Resolves: trailers in
their fixing changes.
Frontmatter
| id | spc-42 |
|---|---|
| type | spec |
| lifecycle | open |
.abcd/development/specs/open/spc-42-every-change-starts-in-its-own-worktree.md
open on GitHub ↗ · commit history ↗
References1
- itd-148Every change starts in its own worktree in abcd's store: the primary checkout is a read-only surface, and abcd blocks mutations there for every session but a declared coordinatorimplements
mentions17
- adr-2609091248200336A tool never creates directories in user-owned project space; the store's location binds now and its verbs bind when the store ships
- dcn-26092900000000072026-09-29 — The product thinker answered twenty-six owed rulings and the published-history question in one interview, asked one at a time by orchestrator abcd-23 of autonomous run A (recorded by lane recRulings for orchestrator abcd-cb). The answers are kept verbatim in the local-tier file `.abcd/.work.local/scratch/reports/rulings-answered-2609-29.md`, dated 2026-09-29, and are restated here because that file is not committed. Published history is left as it is, with no rewrite. The build loop's fidelity audit runs once, on the lane that closes the spec, over the whole delivery (AI). On the vendor denylist the product thinker answered, verbatim: "I think we need an allow list rather than trying to catch a deny list." The interviewer's reading of that answer, recorded as a reading and not as the product thinker's words, is that the per-provider allowlist is the control and the vendor denylist is not extended to chase spellings, with whether the bundled `anthropic/*` entry stays as a backstop left to the technical facilitator (AA(a)). Only a route the person set up on their own machine may use their paid key, and a repository's own route to it is refused, which replaces the route half of itd-2609081951381895 decision 8 (AA(b)). The live site setup, the API adapter and the decision adapter may each close on tests against a simulated service, with the live paid check recorded as owed to a person (iss-2609291925387011, iss-2609291925393913, iss-2609291925398978). itd-111's last criterion is met by an agent's timing check on macOS plus the Linux CI run, so the intent may be finished. Automated dependency updates are switched on, pushed with a personal or app token the person creates and stores as a repository secret (itd-2609221842494980; creating the token is the person's act, and the lane builds everything else and names the secret it expects). itd-6 adds a Go MCP module, proposed for the person's explicit sign-off before it is fetched; closing itd-6 on a fake server was not ruled in this interview. (A later answer the same day, H1b in the local-tier file `.abcd/.work.local/scratch/reports/rulings-answered-2609-29-b.md`, dated 2026-09-29, rules that it is not: itd-6 is finished only after running once against the real RepoPrompt app, a check owed to a person.) For itd-148: the store under the home folder wins (adr-2609091248200336); the store draft itd-2609091014076309 owns add, list and clean-up and itd-148 uses them; a session that declares itself the coordinator is exempt from the write block in the primary checkout and every other session is blocked; and the store draft is planned first, with nothing from the store draft, the merged-worktree clean-up included, built before its planning interview (Q4). itd-148's own wait is not part of that ruling: it follows from Q2, because itd-148 uses the store's verbs, and is carried by the `blocked_by` below. itd-148 and spc-42 are rewritten to match in this change, and itd-148 is now `blocked_by` the store draft. A release retrospective is seeded from a release, the intents a tag shipped (AD); itd-24 and its spec are rewritten to the release in this change. An unpointed route is refused naming the one setting to add, as built in 35100512e, so AC11 of itd-2609170822093401 is complete (BR1). A missed target becomes `next`, whatever the following release is numbered (BS1). Spec pieces keep "step" and the lane stages are renamed, a breaking change to the JSON fields `step`, `performed` and `refusal.step` that resolves iss-2609291313276243 and corrects the glossary entry `core/step.md` (BU1), and the old command spellings are removed in that same breaking release (BT1). The text board shows Later as a count only while the site and `--json` keep the full list (BV1), and an intent in progress appears only under Now (BV2). A pause of zero is allowed (BW1); drain stays in the agents' help list until it runs end to end (BX1); a managed repository must hold the drain eligibility decision in its own record, and drain refuses there until it does (BX2); a capture without a remedy is refused (BX3), which tightens the one-line capture friction `commands/capture.md` promises, so the lane that builds it reconciles that page, the principle wording and every caller that files a capture; a run's pick reason keeps the shared `pursued:` kind with its marker (BZ1); and an intent whose blocker was superseded waits on the successor, transitively (BZ2). The five itd-48 follow-ups are planned as one intent, drafted as itd-2609291923559186 (BT2), and all warn rules share one shrink-only baseline file in which a new warning fails, drafted as itd-2609291924469783 (BT3); both drafts wait on their planning interviews. Each ruling that contradicts a shipped behaviour or another record is amended in the change that builds it, not here. itd-7's planning interview stays owed.
- iss-213Several agents sharing ONE git worktree silently invalidated a verification result and came close to losing committed work. Observed repeatedly during the 2026-08-11 install-test round, in a repo that is about to run more agents, not fewer.
- iss-370Agent worktrees commit without the private name-guard: .abcd/.work.local/ is per-worktree, so every isolated-worktree agent commit runs with the banlist layer absent — loudly warned, per design, but the isolated-agent pattern now systematically bypasses a protection the main checkout has. Candidate remedies: the worktree-creation path seeds a pointer to the primary checkout's store, or the hook falls back to reading the primary worktree's local tier
- iss-2608210738378295Local record gates validate the WORKING TREE, not the committed/pushed tree, so a partial commit passes them while the committed tree fails CI. Demonstrated live this session: git mv staged a rename (iss-370->377) but the follow-up sed edit to the frontmatter id was left unstaged; git commit captured only the rename, so the committed file had filename iss-377 with frontmatter iss-370 — a record_schema blocker. record-lint had passed at commit time because it reads the working tree (which held the sed edit), and the pre-push make preflight passes for the same reason (the unstaged edit is still present); only CI, checking out the commit, sees the divergence. Fix direction: run the push-time gate against the tree that will actually ship (e.g. a clean worktree of HEAD, or git stash --keep-index before linting), so a working-tree/index divergence cannot pass locally and fail CI. Sibling of iss-147 (guard-load reads guard.json from the working tree).
- iss-2608220750029993Session-presence detection for shared checkouts: each live session leases a marker (session id, intended branch, started-at) in the checkout, and the pre-commit gate warns when another live session's lease is present — the armed-detector rung of the AGENTS.md concurrent-session conventions, which are vigilance-only until this ships. Lease home needs thought: the local tier is per-worktree by design, and the hazard is precisely two sessions in ONE worktree, so the lease belongs to the checkout, not the branch.
- iss-2608230847432285Per-agent worktrees do not isolate a session whose shell cwd reverts to the shared checkout, so the mitigation iss-213 recommended has a hole. This refines iss-213, which recorded several agents sharing one git worktree silently invalidating a verification result, and whose recommended direction was to give each agent its own worktree so the whole class disappears. That direction is now in force via the AGENTS.md Concurrent sessions section, and on 2026-08-23 three concurrent sessions demonstrated it does not hold. A session's shell cwd can be silently reset from its worktree back to the primary working directory, and the notice arrives on the tool result AFTER the command that caused it, so the contamination lands on the NEXT command. It fails toward the shared tree, which is the wrong direction: two sessions wrote into the main checkout while believing they were in their own worktree. One appended to two Go test files, the other filed a capture and edited three record files, and both discovered it only when a third session read git status in the main checkout and asked who owned the diffs. Nobody lost work, because the convention that a diff you did not make is a peer's work held and the owners were asked rather than the files committed. The isolation property did not hold; the coordination convention compensated for it. Two details generalise beyond this instance. First, the failure is silent in BOTH directions: nothing warns the writer, and nothing would have warned a committer using git add -A, because the misplaced files are indistinguishable from that session's own in git status. What stood between this and a bad commit was one session committing with explicit paths and another happening to run git status for an unrelated reason. Detection was not mechanical in any of the three cases. Second, this is the write-side twin of iss-213 rather than a restatement of it: iss-213's dangerous member was a verification result that described no tree in particular, on the read side, while this is work landing in a tree whose HEAD another session is preparing to move. Same root cause, that the checkout is the unit of isolation and nothing enforces which checkout a session is in, on opposite sides of the read/write boundary. The mechanical mitigation is to address the tree explicitly on every git invocation, git -C <abs-path>, and to use absolute paths for file writes, rather than relying on a persisted cd: a cd is a session-global mutation with no scope and no expiry, which is the wrong shape for the mechanism that is supposed to provide isolation. AGENTS.md states that the checkout is the unit of isolation without saying what makes a session stay in its checkout, and that gap is what let three sessions make the same mistake in one day. Decide the routing: an AGENTS.md line under Concurrent sessions is the cheapest rung and matches how the sequential-id caveat was handled, while the durable form is whatever makes a session's tree unambiguous rather than remembered.
- iss-2608230957104179the scan-before-mutating rule enumerates four git operations and misses shared artefacts outside git
- iss-2608271804176759the specs store has no charter and its id namespace is doubly allocated: specs/ is the only durable-tier record family with no README at any level (directory_coverage flags specs/, open/ and closed/ independently), the live store is spc-2..spc-42 while the retired predecessor store minted its own spc-1..spc-83, and above-ceiling references now collide — spc-33 and spc-37 resolve to live specs on different subjects than the retired records the citations meant. Write the three READMEs first (grammar, open-to-closed lifecycle, intent back-link, the no-status-field rule per adr-3, and the mandatory predecessor-store qualifier), then re-scope iss-239 with the corrected live range and the two collisions.
- iss-2609020721142452Worktrees for parallel lanes are created one directory above the repository (../abcd-wt-<lane>, ../abcd-f185, ../abcd-integration), which litters the parent directory with a dozen sibling checkouts named by convention only, leaves them behind when a session dies (21 spent worktrees, 1.4G, were removed by hand on 2026-09-01), and gives nothing a stable place to look for them. Assessment of where they should go: (1) inside the repository under .abcd/.work.local/worktrees/ keeps them with their owner and gitignored, but a worktree inside the main working tree is walked by every tree scan (the payload gate, name-guard, docs-lint), which already flakes during worktree creation (iss-2608261331317889) and would slow every gate; (2) under the user-level home, ~/.abcd/worktrees/<root-sha>/<branch-slug>/, keyed the way the history store already keys a repository by its root commit, keeps them out of every scan, survives a session, and gives an abcd verb a place to list, prune and reattach them; (3) the git default (anywhere) is the status quo. Option 2 fits the existing home layout and the one-canonical-primitive rule (the root-sha keying exists), and it makes the concurrent-sessions convention in AGENTS.md checkable: a session lists its peers' worktrees by reading one directory. It needs: a verb or documented one-liner (abcd worktree add <lane> / list / prune), the AGENTS.md convention updated, the lane brief updated, and a prune rule for worktrees whose branch is merged.
- iss-2610111926302443TestWorktreeStoreRuleNamesTheIntentsActualState (internal/core/lint/conventionsfile_test.go) checks for stale draft wording per blank-line paragraph, and the references list in .abcd/development/brief/04-surfaces/34-build.md is one paragraph naming six intents, so a later '(a draft)' beside a different intent in that list would fail the test naming itd-2609091014076309 falsely.
- iss-2610111926302792Present-state planning prose still calls itd-2609091014076309 'the store draft' although the intent is in planned/: itd-148 (lines 64 and 188), spc-42 (lines 27 and 112) and iss-213 (line 11). The drift fix for iss-2610090744073409 corrected the CONCURRENCY rule, the build brief page and lane.go, and left these planning records as out of its remedy's scope; its ruthless review recorded them.
- iss-2610112237488013Three planning records still treat the worktree-store intent itd-2609091014076309's planning interview as owed, though it was held on 2026-09-30 (its record lists the rulings, and its Open Questions read 'None open'): spc-42's '## Blocked on' lists that interview as the blocker; iss-213's deferral_reason says 'Owed: the store draft's planning interview'; itd-148's Acceptance Criteria note and Open Questions defer to after it; and spc-42's Scope item 2 calls the check verb's name one of the store intent's open questions, though that intent's Decision 5 settled it as a sub-verb of ahoy. Found by the lane for iss-2610111926302792.
- itd-33Multiple Agents Coordinate On Intent And Spec Work Without Duplicating Effort Or Producing Competing Artefacts
- itd-118Merged work leaves no residue: abcd-managed repos delete a PR branch on merge, and abcd tidies the stale local branch, tracking ref, and worktree
- itd-2609091014076309Session and agent worktrees live in a machine-scoped store that abcd lists and reclaims, never beside the user's own projects
- itd-2609091416295622A session sees the records its peers hold before it mints or fixes one