Development record
captureRoot (`internal/surface/cli/cli.go:3794`) is the same collapse the rules
captureRoot (internal/surface/cli/cli.go:3794) is the same collapse the rules
root just had: it asks git for rev-parse --show-toplevel and falls back to cwd
on any error, so a repository git will not answer for (an ownership refusal under
the isolated env, git absent from the host's PATH, a corrupt .git) is treated as
no repository at all. The sibling three-state walk already exists as
gitutil.RepoShapedRoot; capture.discoverRepoRoot and site.checkoutRoot both
do this correctly. That much is unchanged and is still worth fixing.
RE-GRADED 2026-09-09, major -> minor, on reachability. The stated harm path is GATED and the record did not say so.
The mechanism is real where it is reached: internal/core/history/history.go:140
calls scanner.New(repoRoot) with the root captureRoot returned, and the scanner
resolves the per-repo redaction override at <root>/.abcd/config/pii.json
without walking up — so a wrong root does silently redact with defaults only,
which is the B12 failure.
But the history verbs never reach it in the failure modes the record names.
abcd history capture (cli.go:3557) and abcd history drain (cli.go:3693)
each call repoRootSHA() FIRST and return its error, and repoRootSHA
(cli.go:3822) fails whenever det.RootSHA == "". RootSHA comes from
gitutil.RootCommit (internal/core/ahoy/store.go:61), which runs
rev-list -n 1 --max-parents=0 HEAD through the SAME isolated-git environment
gitutil.Run uses (internal/gitutil/repo.go:300, :345). Every failure mode
the record names — the ownership refusal under GIT_CONFIG_GLOBAL=/dev/null,
git absent from PATH, a corrupt .git — fails both calls, so the verb refuses
before captureRoot is consulted. The asymmetry does not run the other way
either: rev-list HEAD is the stricter call, so there is no state where
--show-toplevel fails and RootSHA still resolves. The hook session-start
drain (cli.go:1283-1284) is gated identically, by ahoy.Detect succeeding AND
det.RootSHA != "".
The one currently-ungated caller set is internal/surface/cli/reading.go — the
cold-reading verbs, at :56 (reading.Describe), :121 (reading.Assemble)
and :237 (reading.Ingest). None is preceded by a root-SHA gate, and the
ingest path reaches a scanner the same way: internal/core/reading/ingest.go:601
calls newPayloadField(repoRoot), which is scanner.New(repoRoot) at
internal/core/reading/redact.go:51. So the reachable exposure today is abcd reading ingest/assemble run from a subdirectory of a repository git will not
answer for. That is the cold-reading workstream, which is not being carried here.
So: a real latent collapse, correctly identified, whose named harm path cannot
currently be walked, and whose one walkable path belongs to another workstream.
minor rather than major. The fix is unchanged and still wanted — three-state
through gitutil.RepoShapedRoot like the two siblings that already do — and it
should land before any new caller is added without a root-SHA gate in front of
it, because the gate is what is holding this closed, not the function.
AMENDED 2026-09-09. The adjacent front-door defect this record was read against — iss-2609090951291524, every capture verb taking the working directory as the repo root — is fixed, and captureRoot gained no new callers in the fixing change. The capture verbs resolve through capture.LedgerRoot, which asks git and refuses the two states captureRoot collapses onto cwd, deliberately rather than reusing this function while it still collapses them. So the condition this record sets is intact: the reading verbs at reading.go:56, :121 and :237 remain the one ungated caller set, and the fix wanted here — three-state through gitutil.RepoShapedRoot, like the two siblings that already do it — is unchanged and still wanted before the next ungated caller is added.
Grounds
- pursued: from a subdirectory of a repository git cannot answer for, captureRoot names the checkout, not the subdirectory; shown wrong if TestCaptureRootBoundsARepositoryGitWillNotAnswerFor returns the subdirectory
Frontmatter
| id | iss-2609020224230967 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-2609020224230967-captureroot-in-internal-surface-cli-cli.md
open on GitHub ↗ · commit history ↗
mentions1
- 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.