Development record
Directory location is the source of truth for lifecycle state
adracceptedadr-3
Context
After ADR-2 introduced three intent kinds, two field-vs-directory inconsistencies surfaced:
- Disciplines initially carried
status: activein frontmatter. But thedisciplines/directory already encoded that — there is noinactivediscipline state. Field and directory said the same thing twice. - For standalone/bundle-member intents,
statuswas historically a positional state in a sequence (draft/planned/shipped). For disciplines,status: activewas a type predicate doubling for one of two directories (disciplines/vssuperseded/). Different jobs, same field name — a smell. - Superseded intents initially recorded only
superseded_by: <itd-M>. But "superseded" means different things depending on what the intent was: a superseded standalone is a retired capability; a superseded discipline is a retired rule. Without recording the original kind, future readers (and the shape-classification auditor) had to infer it from context.
Two failure modes followed: field-vs-directory drift (an intent could in principle have status: active in the wrong directory and internal/core/lint had no clear contract to assert) and lossy supersession (kind context erased on retirement).
Decision
Directory location is the single source of truth for lifecycle state across all intent kinds.
- Standalone and bundle-member intents derive lifecycle state from
drafts//planned//shipped/. - Disciplines derive state from
disciplines//superseded/. - No intent carries a
status:frontmatter field. Disciplines drop it entirely (it was redundant); standalone/bundle never had astatus:field that disagreed with directory. - Superseded intents preserve
kind_at_supersession: <original-kind>so the shape the intent had when retired stays legible.
internal/core/lint enforces:
- No file in
drafts//planned//shipped/carries akind: disciplinevalue. - No file in
disciplines//superseded/carries astatus:field. - Every file in
superseded/carries bothsuperseded_by:andkind_at_supersession:.
Alternatives Considered
- Keep
status:field as the source of truth; make directory advisory. Rejected: filesystem moves are cheap, atomic, and self-documenting ingit log; field edits are easy to forget and create silent drift. Directory-first matches the existing intent lifecycle (/abcd:intent planalready moved files between directories) — making the field authoritative would have required a second mechanism. - Keep both
status:and directory; require lint to verify they agree. Rejected: redundancy is a smell; the lint rule would only ever catch discrepancies that a single source of truth prevents structurally. The two-source design encourages each writer to update one and forget the other. - Drop
status:only from disciplines; keep it for standalone/bundle. Rejected: the principle is now non-uniform. Two kinds use directory-as-truth, one kind uses field-as-truth. Future readers and tooling must know which kind they're looking at to know which to trust. Uniformity is the gain; partial application loses it.
Consequences
Gains:
- One source of truth per concern.
git logshows lifecycle transitions as file moves; no parallel field history to reconcile. - The principle is uniform across kinds, applies consistently to future kinds, and generalises beyond intents (disembark snapshots, lifeboat artefacts, etc.).
internal/core/lintcontract is simpler (assert directory matches kind, not directory matches field matches kind).
Costs / obligations:
- Manual
mvoperations outside/abcd:intentverbs can put a file in the wrong directory; lint catches at pre-commit but the moment-of-error is the moment of move, not the moment of verb. Mitigated by/abcd:intent reclassifybeing the only sanctioned move path, withreclassification_historywritten in frontmatter. - The
status:field is reserved vocabulary for non-intent surfaces (e.g., RFCs still usestatus: open|resolved-yes|...). Naming registry must distinguish — clarified in02-constraints/04-naming.md.
Downstream consequences:
- ADR-5 (brief is current state) extends the same principle to the brief itself: the live
brief/directory IS the brief; no parallelversion:field, no parallelarchive/<NN>/directories duplicating git history. - The issue ledger (spc-20, itd-4) applies the same pattern: the
.abcd/work/issues/{open,resolved,wontfix}/iss-N-<slug>.mdledger carries nostatus:field; the sub-directory IS the lifecycle state. Awontfix_reason:orresolution:field is required in the corresponding directory (enforced by_issue_lib._validate_invariants, not by schema location alone), keeping per-state context legible.
Worked examples
| Lifecycle | Directories | Required-by-location field(s) | Enforced by |
|---|---|---|---|
| Intents (standalone / bundle-member) | drafts/ / planned/ / shipped/ |
— | internal/core/lint |
| Disciplines | disciplines/ / superseded/ |
superseded_by: + kind_at_supersession: in superseded/ |
internal/core/lint |
| Issue ledger | issues/{open,resolved,wontfix}/ |
resolution: in resolved/; wontfix_reason: in wontfix/ |
_issue_lib._validate_invariants |
Frontmatter
| id | adr-3 |
|---|---|
| type | adr |
| status | accepted |
| date | 2026-05-07 |
.abcd/development/decisions/adrs/0003-directory-as-truth-for-lifecycle.md
open on GitHub ↗ · commit history ↗
References1
- itd-34Intents Promote In Three Flavours — Standalone, Bundle-Member, Disciplinerelated
Referenced by6
- adr-2Three intent kinds (standalone / bundle-member / discipline)related
- adr-5Brief is the current state; no version label, no archive directoryrelated
- adr-26A native minimal spec layer with the companion harness `ccpm` as the primary deeper backendrelated
- adr-30Design-record information architecture — flat artefact-type foldersrelated
- adr-32The issue ledger is working-tier data, not authored recordrelated
- adr-34Intent lifecycle and phase scheduling are orthogonal axesrelated
mentions9
- dcn-26082100000000012026-08-21 — Bug-hunt round 4, branch bughunt-b/round-4. Baseline (make preflight
- iss-388roadmap phases README states itd-88 is in planned/ and unscheduled, but the intent has shipped — a false bucket claim in present-tense current-state prose
- iss-2608211140410761abcd <id> ADR dispatch (record.go describeADR) confirms the frontmatter id with a byte-exact 'fields["id"].Value != id' compare (frontmatter.Field.Value is TrimSpace-only, quotes not stripped), while record-lint schema.go compares parsed handles (a tested tolerance, TestRecordSchemaFilenameIDComparesNumerically) and the citation resolver rebuilds ids from the parsed integer. So an ADR written id: "adr-12" (quoted), adr-0012 (padded), or ADR-12 (cased) is lint-green and citation-resolvable but abcd adr-12 reports 'not found'. A live facet against the shipped corpus: abcd adr-0003 (padded, admitted by IDRe) routes to the file then refuses. Second site under iss-285's quote-normalisation-split principle.
- 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-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.
- itd-4Nothing You Notice Gets Lost
- itd-6RepoPrompt over MCP is one opt-in reviewer route, beside the host's own agents
- spc-20name-banlist
- spc-58Reading records and disposition records: two acts, two writes