Development record
The checkout-root defect that decide carried is a pattern the surface shares, not two instances of it. iss-2609090951291524 fixed it for the seven capture verbs and iss-2609091707224329 for decide; the same raw shape — os.Getwd() handed to a core that joins a repo-relative store path onto whatever it is given — remains at the intent, spec, ideate and memory front doors, which between them own the intent store, the spec store, the ideate verdict record and the memory substrate. Reproduced against a scratch git checkout holding one intent draft: abcd intent run from internal/core reports drafts 0, planned 0, shipped 0 while the identical binary at the root reports the draft, and abcd intent "<text>" run from the same directory mints a second store at internal/core/.abcd/development/intents/drafts and reports a repo-relative path that reads exactly like the checkout store's. Run in a plain directory that is not a repository at all it exits 0 and lays the full intent skeleton there. spec and memory show the read half of the same thing from a subdirectory. The cost is the one the two closed records already argue: a record filed where no gate, no release cut and no reader looks, written with every appearance of success, and for the intent store an intent whose spec can never be closed against it. Fix direction: the resolution is already shared and already generalised — gitutil.CheckoutRoot takes git's toplevel, refuses a repo-shaped tree git will not answer for, and refuses when nothing repo-shaped sits above, with the store's noun as its only parameter — so each remaining front door needs a resolving step of the captureLedgerRoot and decideStoreRoot shape and the strayStoreNotes call that reports what an unresolved door already deposited, not a new resolver. Detector, per family: the verb run from a subdirectory addresses the checkout's store, the verb run outside a repository refuses with exit 2 and writes nothing, the verb run at the root is unchanged, and a static pass over the surface package refuses a request built from an unresolved working directory.
The checkout-root defect that decide carried is a pattern the surface shares, not two instances of it. iss-2609090951291524 fixed it for the seven capture verbs and iss-2609091707224329 for decide; the same raw shape — os.Getwd() handed to a core that joins a repo-relative store path onto whatever it is given — remains at the intent, spec, ideate and memory front doors, which between them own the intent store, the spec store, the ideate verdict record and the memory substrate. Reproduced against a scratch git checkout holding one intent draft: abcd intent run from internal/core reports drafts 0, planned 0, shipped 0 while the identical binary at the root reports the draft, and abcd intent "<text>" run from the same directory mints a second store at internal/core/.abcd/development/intents/drafts and reports a repo-relative path that reads exactly like the checkout store's. Run in a plain directory that is not a repository at all it exits 0 and lays the full intent skeleton there. spec and memory show the read half of the same thing from a subdirectory. The cost is the one the two closed records already argue: a record filed where no gate, no release cut and no reader looks, written with every appearance of success, and for the intent store an intent whose spec can never be closed against it. Fix direction: the resolution is already shared and already generalised — gitutil.CheckoutRoot takes git's toplevel, refuses a repo-shaped tree git will not answer for, and refuses when nothing repo-shaped sits above, with the store's noun as its only parameter — so each remaining front door needs a resolving step of the captureLedgerRoot and decideStoreRoot shape and the strayStoreNotes call that reports what an unresolved door already deposited, not a new resolver. Detector, per family: the verb run from a subdirectory addresses the checkout's store, the verb run outside a repository refuses with exit 2 and writes nothing, the verb run at the root is unchanged, and a static pass over the surface package refuses a request built from an unresolved working directory.
Two of the four families are closed on 2026-09-09: spec and memory now
resolve the checkout root through gitutil.CheckoutRoot before they build a
request, at all six call sites (bare spec, spec close, bare memory,
memory ingest, memory ask, memory lint), each with the family's own
detectors and the strayStoreNotes report. The record stays open for intent and
ideate record, which are a sibling's half.
Amended 2026-09-09 on what the sweep found while closing them, because the
sentence above under-describes the memory family. "spec and memory show the read
half of the same thing" is right about spec and wrong about memory. Memory's
write paths were live and silent: memory ingest from a subdirectory exited 0
and laid a COMPLETE second substrate — pages, index, registry, log — beneath the
caller, and in a plain directory that is no repository at all it did the same
there; memory ask --file-back writes through the same core; and memory lint
read the store that was not there, reported 0 blockers over pages it never
opened, and wrote its run-log tree to <caller>/.abcd/.work.local/logs/memory/.
The spec family's write path, spec close, is the one that stayed a refusal
rather than a misplaced write — spec.Load returns an empty store rather than an
error, so Reconcile refused with "spec spc-N not found" for a record sitting in
the checkout, which is a true-shaped statement about the wrong store and not a
lost record. The call-site line numbers the sweep brief carried (cli.go:1918,
1958 for spec and 3386, 3426, 3482, 3518 for memory) predate the decide fix; the
sites were at 1925, 1965, 3444, 3484, 3540 and 3576 when this change started.
Two of the four halves are closed. intent and ideate record now resolve the checkout root before they read or write, through intentStoreRoot and ideateStoreRoot — the captureLedgerRoot/decideStoreRoot shape the fix direction above asked for, over the same gitutil.CheckoutRoot, with the stray-store note on the ancestor chain. All eight intent call sites (Status, CreateFromText via createIntentFromText, Plan, RecordGrounds, Ready, Link, ReEmitAudit, IngestVerdict) and the one ideate call site take the resolved root; the bare intent render stays read-only, and the two remaining refusal states exit 2 having done nothing. Per-family detectors are in internal/surface/cli/intent_root_test.go and ideate_root_test.go, including a static pass that derives the covered core functions from the core packages themselves. The record understated the ideate half in one respect: from a subdirectory the verb did not merely misaddress the store, it reported the grill's cited ids as records that "do not exist in this repository" — the operator's grill blamed for the verb's own misaddressing — and where the caller's directory carried a decision log it wrote the verdict and its pointer there and exited 0. spec (spec.Load and intent.Reconcile at the spec-close door) and memory are still open, and this record stays open for them.
Grounds
- pursued: we expect a verb that writes or reads a record to address the checkout's store from anywhere inside it, and to refuse rather than guess outside one, so a record is never written where nobody will look and never reported absent when it is present; a verb reaching a store beneath the caller, or a plausible empty answer from a subdirectory, would show it wrong.
Frontmatter
| id | iss-2609091729516940 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | major |
.abcd/work/issues/resolved/iss-2609091729516940-the-checkout-root-defect-that-decide.md
open on GitHub ↗ · commit history ↗
mentions3
- iss-2609090951291524Every capture verb hands the caller's working directory to the core as the repo root, verbatim and unresolved: the status, list, resolve, promote, wontfix and disposition commands each build their request with the working directory, and the fast-path capture does the same, so the core takes the explicit-root branch and never consults the git-aware discovery helper that sits behind it. A verb run anywhere but the checkout root therefore addresses a ledger that is not there. Reproduced: from a package directory two levels down the status render reports open 0, resolved 0 and wontfix 0, while the identical binary run from the checkout root reports open 365, and neither run says anything is wrong. The write half is worse than a wrong answer, because the directory tree is created on demand: a capture run from a subdirectory mints a second ledger under it, writes the record there, and reports success with a path the author will not read as unusual, so the record is filed where nothing looks for it and a stray dot-abcd tree appears in a source directory. Confirmed by running the shipped binary in a plain directory that was not a repository at all, where it created the full ledger skeleton and wrote thirteen records into it. The reading verbs get this right and resolve the toplevel first, which is what makes this an omission rather than a design. One further limb is real but currently inert here: the ledger redactor builds its scanner from the root it is handed and the scanner reads the per-repo pattern override two directories down without walking up, so a capture from a subdirectory is redacted with the built-in defaults alone; this repository carries no such override today, so nothing is under-redacted now, and any managed repository that adds one is. iss-2609020224230967 is adjacent and states that the capture package does this correctly, which is true of the core helper and false of the front door that never calls it. Fix direction: resolve the checkout root once at the surface, the way the reading verbs already do, or let the core discover it by passing no root and hardening the helper that then runs. Detector: a capture verb invoked from a subdirectory must address the checkout root ledger, and must refuse rather than silently mint a new store when there is no ledger and no repository.
- iss-2609091707224329The decide verb hands its working directory straight to the record writer without resolving the checkout root, so it mints an architecture decision record relative to wherever the shell happens to be. Run outside any repository it exits zero, reports a repo-relative path, and creates a full decisions store in a plain directory; run from a subdirectory of a real checkout it mints into a second store beneath that subdirectory rather than into the repository's own. This is the same defect the capture verbs carried until this release, and the same remedy applies: every front door resolves the checkout root before it builds a request, and refuses rather than guessing when there is no repository to address. The evidence is not hypothetical. A checker of this release's own semantic gate, running in a scratch directory, left behind an ADR store containing one record whose slug says it was written outside any repository; nothing in the verb, its help, its surface chapter or its plugin page warned that this could happen. The cost is a decision record that no gate reads, no release cut sees and no reader finds, written with every appearance of success, and the durable record family is the one where a lost entry is least recoverable because the decision it holds was never anywhere else. Fix: resolve the root the way the capture verbs now do, refuse outside a repository with nothing written, and report a store found below the checkout root rather than adding to it. Detector: decide run outside a repository refuses with exit 2 and creates nothing, decide run from a subdirectory mints into the checkout's own store, and decide run at the root is unchanged.
- iss-2610142032594765ideate record's stray-store note now looks for a stray research/ideas store below the checkout root, so a stray research/notes store that the long-fixed subdirectory defect (iss-2609091729516940) left below a root is no longer reported when ideate runs. Raised by the Opus reviewer of PR #991.