Development record
folder-classification
Summary
spc-5 is record catch-up plus a small report cut: itd-40's classification
engine shipped long before this spec existed. Bare abcd ahoy was already a
strictly read-only classification pass (ahoy.DryRun → Detect →
FolderKind: managed-repo / unmanaged-repo / unmanaged-folder), install
already bootstrapped and registered ~/.abcd/history/index.json keyed on the
root-commit SHA, and doctor already resolved the central/host location by
reading that index. What was missing was verified here AC-by-AC against the
built binary, and the two genuine gaps — both in the human report, not the
engine — were closed test-first: the unmanaged-repo report now names
/abcd:ahoy install as the adoption path, and the unmanaged-folder report now
states there is nothing to act on.
Approach
Verification-first, per the run plan's definition of done (the binary is
checked before the record is trusted): each AC was exercised against
bin/abcd-darwin-arm64 in hermetic scratch dirs (temp HOME, plugin root,
and PATH symlink target — the real ~/.abcd untouched), and only the
behaviours the binary could not demonstrate became code. The one production
edit is confined to newAhoyCommand's bare-ahoy render closure in
internal/surface/cli/cli.go — the classification core
(internal/core/ahoy/detect.go) needed no change, keeping bare invocation's
read-only contract untouched.
Two ACs were behaviourally met but untested; they received characterization
tests rather than code: AC4's registration half (the pre-existing idempotency
test used a bare .git mkdir with an empty root SHA, so registration by real
root-commit SHA was never exercised) and AC5's index-resolved location (no
prior test drove the auditGaps/doctor path).
Deviation: none in behaviour, one in provenance
Implementation for this spec was delegated to a sub-agent worker (manual test
B of the run protocol) with the orchestrator re-running the gate; the
watched-fail evidence below is quoted from the worker's captured runs, and
the run findings log records the delegation experiment
(.abcd/development/plans/2026-07-16-run-findings.md, burst 2).
Milestones as delivered
- AC-by-AC verification of the shipped classification engine against the binary (no engine changes required).
- Bare-ahoy report cut: adoption-path hint for
unmanaged-repo, nothing-to-act-on line forunmanaged-folder(internal/surface/cli/cli.go), user-facing CHANGELOG entry. - Test corpus closing the coverage gaps: two watched-fail report tests plus
two characterization tests for the already-met ACs
(
internal/surface/cli/cli_test.go).
Acceptance-criteria satisfaction
AC as ordered in itd-40 → covering evidence (tests in
internal/surface/cli/cli_test.go unless noted):
- Marker block +
.abcd/→managed-repo, mutates nothing — met by the shipped engine;internal/core/ahoy/detect_test.go(TestClassifyManagedRepoByAbcdDir,TestClassifyManagedRepoByMarker); read-only by construction (DryRundelegates toDetect, which has no writer). - Unmanaged git repo →
unmanaged-repo, names/abcd:ahoy install, without adopting — gap filled here, watched-fail:TestAhoyBareUnmanagedRepoNamesAdoptPath(asserts the hint and that.abcd//CLAUDE.mddo not appear afterwards); failing output captured before the change showed the report ending at the kind line. - Plain folder →
unmanaged-folder, nothing to act on, mutates nothing — gap filled here, watched-fail:TestAhoyBareUnmanagedFolderReportsNothingToActOn; mutates-nothing also covered byinternal/core/ahoy/detect_test.go(TestDetectUnmanagedFolderShortCircuits). - First install bootstraps
~/.abcd/history/+ registers by root-commit SHA — met by the shipped engine; characterization test added (TestAhoyInstallBootstrapsAndRegistersByRootSHA, real git repo so the root SHA is non-empty) over the pre-existing bootstrap coverage (internal/core/ahoy/idempotency_test.go,TestInstallThenReinstallIsExactNoOp). - Central/host location resolved by reading
index.json, never a hardcoded path or walk — met by the shipped engine; characterization test added (TestAhoyDoctorResolvesCentralLocationFromIndex: doctor flags a hand-relocatedpathand quotes the value it read from the index, proving the lookup source).
Out-of-scope confirmations: bare ahoy never adopts (AC2's test asserts the
absence of installation artefacts); no grouping layer was added; iss-101's
index read-modify-write concurrency was deliberately left untouched (another
workstream owns it).
Frontmatter
| id | spc-5 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-5-folder-classification.md
open on GitHub ↗ · commit history ↗
References1
- itd-40abcd Knows What Kind of Folder You're Inimplements
mentions6
- iss-101registerRepo does an unlocked load-modify-write of ~/.abcd/history/index.json (loadHistoryIndex -> mutate -> writeHistoryIndex, apply.go), and bootstrapHistory is check-then-act. WriteFileAtomic prevents torn files but not lost updates (last rename wins), so two concurrent 'abcd ahoy install' runs from different worktrees can drop a repo registration or clobber the registry with an empty Repos list, erasing lineage/supersedes links set on explicit user confirmation. Deferred from the multi-agent bug hunt (was B30): a correct fix needs an inter-process O_EXCL/flock lock in ~/.abcd/history held across the load-mutate-write, with a re-load inside the lock and bootstrapHistory creating index.json with O_EXCL. The registerRepo sequence spans an interactive prompter.Confirm (re-founding lineage), so the lock must NOT be held across that prompt (risk of blocking other processes on user input). Low severity, self-heals on next install. Needs a dedicated change.
- iss-2609240227150859itd-6's Implementation status and its post-spc-5 Status paragraph say spc-5 built a typed RPUnavailable error in internal/core and a concrete MCPBridge (the ADR-02 spawn implementation and the ADR-03 host-reuse path), and its Resolved sections route failures through oracle.py. None of it is in the binary: grep for MCPBridge, RPUnavailable and RepoPrompt over internal/ and cmd/ finds only the scanner's RepoPrompt sessionKey pattern (internal/adapter/scanner/patterns.go) and a guard corpus line, and go.mod carries no MCP dependency. The sections describe an earlier Python lineage, so a planner or an implementer reading the planned record is told a foundation exists that does not; the re-filed scope (spc-2609211950427074) needs that foundation built from nothing.
- iss-2609290448510918The predecessor-store qualifier sweep reaches past the six intents iss-2609261536147903 named: outside drafts/ and those six, 192 citation sites across about forty intents name a spc-N at or below spc-70 that is not the citing intent's own spec, with no '(predecessor store)' on the line. Some are live cross-references (the cold-reading family's intents cite each other's live specs), and some are the predecessor store's (itd-4, itd-6, itd-29, itd-47 and itd-49 describe pre-rebuild work in its terms), so each site needs the same reading against the live spec it collides with; the specs charter's Two spc-N Namespaces section is the rule. Found while sweeping the pattern of iss-2609261536147903 in lane drainDrift3; a census script over intents/{planned,shipped,disciplines} reproduces the count.
- itd-6RepoPrompt over MCP is one opt-in reviewer route, beside the host's own agents
- itd-17Pick the Right Oracle for the Job, Automatically
- itd-2609091034175565Before an agent starts on an item, it reserves it on the shared remote, and everyone who works on the project sees the reservation