Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
30 KiB
ADR-612: Bracket Phase-ID Convention
- Status: Proposed (PR-0 — ADR only; locks the contract PR-1…PR-6 execute against. No production code lands in this PR.)
- Date: 2026-07-11
- Issue: #612 — epic (bracket phase-ID convention,
type: enhancement+approved-enhancement) - Supersedes: nothing (no prior ADR). The
milestone-prefixed(M-NN) convention introduced by #565 is deprecated forward via the migrator — see Decision 5. This is a convention-direction call, not an ADR supersede. - Relationship to prior work: completes #39 (milestone-encoding phase IDs), which #565 partially implemented as M-NN. Sibling of ADR-2121 (#2121) — the phase-identifier parsing consolidation that made
src/phase-id.ctsthe single canonical owner of phase-token grammar; the bracket grammar lands inside that owner (Decision 4). The opt-in gating discipline mirrors ADR-1244's capability boundary without modeling bracket as a capability (Decision 9).
Context
MSD phase identifiers encode up to three numeric dimensions — milestone, phase (with optional dotted sub-phase decomposition), and plan — across four surfaces: human display, on-disk directory names, ROADMAP.md headings, and plan/summary filenames. The same string is read by many subsystems (CLI args, migrator, validator, progress renderer, statusline) and, increasingly, by multiple coordinating LLM sessions across repositories — the motivating use case for #39 and #565.
The hyphen is overloaded under M-NN
The milestone-prefixed (M-NN) convention encodes the milestone by hyphen-joining it to the phase (2-01 = milestone 2, phase 01) and the plan by another hyphen (…-01). That is two separator types (hyphen, dot) for three dimensions, so the hyphen means milestone↔phase in one position and phase↔plan in another. Once a token carries both a dotted sub-phase and a plan, it has no deterministic parse (proven in Decision 3).
The lineage, stated honestly
- #39 proposed milestone-encoding phase IDs.
- #565 implemented that as M-NN. This is a partial resolution: it lifts the milestone into the leading integer but leaves the hyphen overloaded, so the token becomes ambiguous once sub-phases and plans coexist.
- This ADR completes #39's intent with the bracket grammar, which lifts the milestone out of the phase token entirely.
M-NN's current status is first-class, not vestigial. Unlike the situation at the original #612 filing, M-NN is now a shipped, tested convention on next (v1.7.0-rc.5). Three distinct facts must be kept separate — conflating them is the trap:
- The default config value is
null, not M-NN —msd-core/bin/shared/config-defaults.manifest.json:14sets"phase_id_convention": null. A fresh repo speaks no milestone convention. - M-NN is a shipped, first-class convention with live machinery: a deprecation-style migration nudge in
src/roadmap-parser.cts:430(getMilestonePhaseFilter, warning body at:441-445) and the W021 read-path check insrc/verify.cts:1724(phase_id_convention === 'milestone-prefixed'branch, warning at:1733). W021 is pinned by shippedtests/milestone-prefixed-convention.test.cjsand cannot be renumbered. - M-NN is the default and only migrator target today —
src/roadmap-command-router.cts:190defaultsconvention = 'milestone-prefixed'and:204rejects any other--conventionvalue;src/roadmap-upgrade.cts:593writesphase_id_convention = 'milestone-prefixed'.
The deprecation stance in Decision 5 is therefore a forward-migration call (consolidation), not the earlier "nobody adopted M-NN so it can be discarded" argument — that argument is dead.
Decision
1. Adopt the bracket grammar
[MSD.02] 05.03-01
│ │ │ │ └── plan 01 one hyphen — only ever the plan (filename surface only)
│ │ │ └────── subphase 03 dot — optional decomposition
│ │ └───────── phase 05 zero-padded integer
│ └───────────── milestone 02 dot-joined INTO the bracket
└───────────────── project MSD uppercase alpha [A-Z]{1,6}
Three dimensions, two separators, zero reuse: dots are always phase-levels, the single hyphen is always the plan, the milestone always lives in the bracket / dir-prefix.
| Surface | Form |
|---|---|
| Display | [MSD.02] 05.03-01 |
| On-disk dir (Option B, no brackets) | MSD.02-05.03-some-feature/ |
| Plan/Summary file | 05.03-01-PLAN.md (milestone in dir prefix, not filename) |
| ROADMAP phase heading | ### [MSD.02] 05.03: Name |
| ROADMAP milestone heading | ## [MSD.02] Foundation (name, no number) |
Milestone source (READING-B). Under bracket, the milestone comes from the [PROJECT.MM] bracket / dir prefix, never from the phase-token leading integer. This is a deliberate departure from getMilestoneFromPhaseId (src/phase-id.cts:91), which today reads the leading integer (READING-A: stripped.match(/^0*(\d+)-\d/) → v{major}.0). READING-A is correct for M-NN and stays intact for it; READING-B is added only on the bracket path (Decision 6).
Heading discriminator. A phase heading is a bracket followed by a digit-then-colon ([MSD.02] 05:); a milestone heading is a bracket followed by a name ([MSD.02] Foundation). A milestone name that begins with a digit is disambiguated by the trailing colon (phase numbers carry it; names do not). The current parser already tolerates a […] prefix before Phase (src/roadmap-parser.cts heading regexes carry (?:\[[^\]]{1,200}\]\s*)?), which is the porting anchor for PR-2.
2. Opt-in gating — legacy paths byte-untouched
Every bracket emit / display / milestone-detection path is gated on config.phase_id_convention === 'bracket', and never on project_code presence (a repo can carry a project_code without opting into bracket — gating on project_code presence is a latent-bug class caught while prototyping this series). The null (un-migrated) and 'milestone-prefixed' (M-NN) parse/emit paths are byte-identical to today. Reads remain tolerant of all forms during the migration window; read-tolerance is not a second active convention.
3. The plan dimension and the concrete collision (PR-0 anchor)
The plan is the third dimension. In bracket form it is a single trailing hyphen + zero-padded integer that appears only in filenames (05.03-01-PLAN.md); the milestone never shares that hyphen because it lives in the bracket. Under M-NN the same hyphen carries both milestone↔phase and phase↔plan, which is the defect.
The empirical anchor, grounded in the two live regexes of normalizePhaseName (src/phase-id.cts:66):
- Anchored milestone regex,
src/phase-id.cts:71:/^(\d+)((?:-\d+)+)([A-Z]?(?:\.\d+)*)$/i. On2-01.02-01it captures2, then-01, then.02— but the trailing-01(the plan) cannot be consumed by the dot-only sub-phase tail(?:\.\d+)*, so the$anchor fails and the match is rejected. - Unanchored numeric fallback,
src/phase-id.cts:79:/^(\d+)([A-Z])?((?:\.\d+)*)/i. With no$, it matches only the leading2and zero-pads it. Everything after the leading integer is silently dropped.
Result (empirically confirmed against msd-core/bin/lib/phase-id.cjs):
normalizePhaseName('2-01.02-01') === '02' // milestone 2, phase 01, subphase 02, plan 01 → collapses to bare "02"
normalizePhaseName('10-02.03-04') === '10' // same failure class — trailing plan hyphen after ".03" kills the anchor
A fully-specified (milestone, phase, subphase, plan) identity silently collapses to a bare two-digit integer that is indistinguishable from a bare phase. This is the "prove the defect first" artifact the #612 approval requires; it is locked as a green characterization test in tests/adr-612-collision-characterization.test.cjs (see Decision 3's note on green-ness).
The boundary of the defect is instructive — the collapse appears only when all four dimensions coexist:
normalizePhaseName('2-01') === '02-01' // milestone + phase — anchor matches, no collapse
normalizePhaseName('2-01.02') === '02-01.02' // + subphase (dot) — anchor still matches
normalizePhaseName('2-01.02-01') === '02' // + plan (hyphen after dot) — anchor fails, collapse
A second, cross-subsystem ambiguity (semantic, not a parse collapse): the token 02-04 is a valid production in two subsystems with two meanings. normalizePhaseName('02-04') === '02-04' reads it as milestone 02 / phase 04 (src/phase-id.cts:71 matches — no collapse). The same string is the phase-02 / plan-04 token in plan-file notation ({padded_phase}-{NN}-PLAN.md, per msd-core/references/universal-anti-patterns.md and --plan NN-MM in docs/COMMANDS.md). The token alone cannot tell a resolver which subsystem is asking. This is a genuine defect but a different class than the collapse above — it is characterized (asserting the non-collapsing current output) but is not the primary anchor.
The bracket contrast (target contract, expected-fail until PR-1 — prose only, NOT in the PR-0 test): once the grammar lands, parsePhaseId('MSD.02-05.03-01') yields exactly one tuple { project:'MSD', milestone:'02', phase:'05', subphase:'03', plan:'01' }, and renderPhaseId / toDir round-trip it. parsePhaseId does not exist on next, so this contract is documented here as the PR-1 acceptance target and deliberately excluded from the PR-0 test, which is green-only and asserts current behavior exclusively.
4. EMIT/RENDER as one pure function pair with round-trip property tests
PR-1 introduces one pure model — a single parsePhaseId plus two renders sharing one PhaseId shape — added inside src/phase-id.cts, the single canonical owner established by ADR-2121:
type PhaseId = {
project: string; // 'MSD'
milestone: string; // '02' (zero-padded; from bracket/dir prefix — READING-B)
phase: string; // '05' (zero-padded)
subphase?: string; // '03' (optional)
plan?: string; // '01' (filename surface only)
};
function parsePhaseId(input: string): PhaseId; // accepts display OR dir OR bare arg
function renderPhaseId(id: PhaseId): string; // '[MSD.02] 05.03-01'
function toDir(id: PhaseId, slug: string): string; // 'MSD.02-05.03-slug'
The pair is verified with fast-check property tests (fast-check ^4.8.0 is an installed devDependency on this base — package.json:66 — and tests/phase-id.test.cjs already imports it), holding the bijective contract the maintainer requires:
render(parse(x)) === xfor every well-formed display stringx;toDir(parse(display), slug) === dirfor every display/dir pair.
Because the grammar lands in phase-id.cts, it inherits the #2128 single-owner regime: the canonical token source PHASE_NUMBER_TOKEN_SOURCE (src/phase-id.cts:44-54) and the anti-divergence guard scripts/lint-phase-id-drift.cjs (ADR-2121 Decision 7). Design constraint for PR-1: the bracket grammar must be an owner-sanctioned extension inside phase-id.cts — any new phase-token regex literal it introduces elsewhere requires a // phase-id-owner: <reason> sanction, or check:phase-id-drift fails CI. It must never be re-derived in another module. This strengthens, rather than replaces, the centralization the maintainer already enforces.
All existing consumers (phase.cts, roadmap.cts, roadmap-parser.cts, validate.cts, verify.cts, commands.cts) call this pair rather than re-implementing regexes inline.
5. M-NN deprecation is terminal — end state is two conventions, by consolidation
Bracket is the terminal convention. The end state is two conventions — null and bracket — down from the transient three (null, milestone-prefixed, bracket). This is achieved by consolidation, not addition: M-NN is deprecated forward through the migrator, not removed out from under any adopter.
- Going forward the runtime speaks one milestone convention:
bracket(gated onphase_id_convention: 'bracket').nullremains permanently supported for un-migrated / non-milestone repos. - M-NN parse/emit is retained as migration-window read-tolerance and as a migrator source — it is not a second active emit convention.
- The deprecation is grounded in machinery that already exists: the
getMilestonePhaseFilternudge (src/roadmap-parser.cts:441-445) and the W021 hint (src/verify.cts:1733) already tell M-NN repos to run the migrator. The bracket migrator (Decision 7) extends that forward path to carry M-NN → bracket. - Which release carries the cutover is the maintainer's call — release mechanics, not design. No stale rc is named here.
This is the design hinge the maintainer flagged; it is decided here, not deferred.
6. Milestone-detection rewrite behind the opt-in flag; legacy intact
getMilestoneFromPhaseId (src/phase-id.cts:91, single-arg, READING-A today) gains a bracket path that derives the milestone from the {CODE}.{MM}- prefix (READING-B), gated on phase_id_convention === 'bracket'. The null and 'milestone-prefixed' paths keep the current READING-A body byte-untouched. Gating requires either a new parameter or a config read at the call boundary — that is a PR-1 design choice, not decided here. Discriminator test (PR-1): getMilestoneFromPhaseId('MSD.02-05.03') resolves to milestone 2, not milestone 5. Sentinel behavior (0.x / 999.x → milestone null) is preserved (src/phase-id.cts:96).
The convention value is validated by a new enum VALID_PHASE_ID_CONVENTIONS in src/config.cts. Today the value is an un-validated magic literal — config.cts has per-key enum blocks for other keys but none for phase_id_convention. The enum lands in the display/config PR (PR-4/PR-5).
7. Migrator commitments
The bracket migrator (src/roadmap-upgrade.cts) preserves the existing safety triad and extends the source grammar:
- Dry-run by default —
applyMigration(src/roadmap-upgrade.cts:478) setsconst dryRun = options.dryRun !== falseand prints the full plan without mutating (:485-487). - Dirty-tree guard — a non-empty
git status --porcelainthrows before any write (:497-498). - Atomic rollback on failure. The #612 approval names this "HEAD-sha rollback." On this base the mechanism is intentionally different and stronger: a surgical, git-independent reverse-rename + per-file snapshot (
#1542,src/roadmap-upgrade.cts:509-524and the rollback block:598-616). Agit reset --hard+git cleanrestores nothing for a gitignored.planning/(commit_docs:false, the default) and is a whole-repo operation besides; the surgical rollback reverses exactly the renames performed and restores exactly the files snapshotted, correct whether.planning/is tracked or ignored. The requirement's intent — atomic, safe rollback on failure — is met; PR-3 preserves this safer mechanism rather than regressing to a HEAD-sha reset. This is a deliberate deviation from the requirement's literal wording, surfaced here for the maintainer. - Real-world dir-layout fixture corpus with two named invariants (both are failure classes confirmed against real repository layouts while prototyping the migrator):
- Decimal multi-milestone directories keep distinct per-milestone prefixes — a tree with ≥2 milestones in dotted form must not flatten distinct milestones into one phase counter.
- Project-prefixed single-milestone directories don't no-op — a single-milestone legacy tree (
HQ-01,HQ-02…) must derive its milestone (01) from## vN.M/ STATE.md and emit bracket dirs, not exit "already migrated."
- M-NN → bracket lift preserves the milestone integer —
2-01→01,2-04-01→04.01. - HARD-REFUSE when
project_codeis absent — bracket requires a project code; the migrator refuses (throws) rather than emitting a malformed prefix.
8. Single-sourced, generated injection block with a verify parity check (PR-6)
The convention block injected into agent and workflow definitions is generated from one canonical source, and a verify check enforces parity between the source and every injected copy. This keeps the injection machine-uniform across the surfaces the #612 approval scopes — ~34 agent definitions, ~90 workflow files, and 7 phase templates — so PR-6 cannot be reviewed line-by-line and does not silently drift. (On this base the surface counts are: agents/*.md = 34; msd-core/workflows/**/*.md = 114 — 91 top-level command workflows (the ~90 the approval scopes) plus 23 nested mode/step/template fragments under discuss-phase/ (11), execute-phase/ (5), help/ (4), and plan-phase/ (3); the "7 templates" figure is the approval's stated parity scope, corresponding to the phase-emitting subset of msd-core/templates/, e.g. phase-prompt.md, roadmap.md, state.md, planner-subagent-prompt.md, summary.md. The generator, not this ADR, pins the exact set.) There is no existing block-injection pattern in the repo — PR-6 builds it net-new, with the parity check as the acceptance gate.
9. Guardrail — bracket is not a capability
The repo has a capability ecosystem (ADR-1244, ADR-1016, ADR-1143, ADR-1213). Capabilities are add-only descriptors; a capability cannot deprecate M-NN. Bracket is therefore core config-gated behavior, not a capability descriptor. Keep three levels unblurred:
- Capability model = add-only → bracket is not a capability (this guardrail).
- Implementation = additive, gated, legacy byte-untouched (Decision 2) — the code is additive.
- Convention direction = M-NN terminal, end state two (Decision 5) — the convention consolidates.
"Additive implementation" must not contaminate the deprecation stance: the code is additive; the convention consolidates.
Phases
Each phase is an independently-green PR. Tolerant reads (PR-2) ship before emit (PR-4), so a partially-landed series never breaks legacy repos. Module homes are the current src/*.cts names (post-ADR-857 / #1267; core.cts no longer exists).
| PR | Scope | Primary modules |
|---|---|---|
| PR-0 | This ADR + plan-dimension spec + concrete collision characterization test. No behavior change. | docs/adr/612-*.md, tests/adr-612-collision-characterization.test.cjs |
| PR-1 | Core grammar: PhaseId type, parsePhaseId/renderPhaseId/toDir with fast-check round-trip properties, READING-B getMilestoneFromPhaseId (gated), bracket extractPhaseToken branch, comparator, sentinel + slug guards — all inside the single-owner leaf. |
phase-id.cts |
| PR-2 | Read path: bracket heading/dir/checklist tolerance + bracket-coherence checks; ROADMAP phase counting (both total_phases derivations) and the #1514 retirement filter; the write-adjacent enumerator call sites that consume that same counting (milestone complete, state update-progress) now thread the resolved convention explicitly instead of inheriting a lazy default — see the amendment note below the table. |
health-diagnostic-rules/phase-structure.cts, health-diagnostic-rules/roadmap-disk-consistency.cts, health-diagnostic-rules/state-consistency.cts, milestone.cts, phase-id.cts, phase.cts, planning-snapshot.cts, planning-workspace.cts, roadmap-parser.cts, roadmap.cts, state.cts, validate.cts, workstream-inventory.cts, scripts/lint-phase-id-drift.cjs, scripts/lint-phase-enumeration-drift.cjs |
| PR-3 | Migrator: legacy + M-NN → bracket; dry-run / dirty-guard / surgical rollback preserved; HARD-REFUSE on absent project_code; real-layout fixture corpus (two invariants). |
roadmap-upgrade.cts, roadmap-command-router.cts |
| PR-4 | Write path: bracket emit gated on convention; new-project default; STATE.md milestone: frontmatter; VALID_PHASE_ID_CONVENTIONS enum. |
phase.cts, state.cts (emit + milestone: frontmatter only — counting stays in PR-2, above), config.cts |
| PR-5 | Display + card: progress render + stats display_id route through the pure pair; statusline; convention card single-source. |
commands.cts, hooks/msd-statusline.js |
| PR-6 | Generated injection: single-sourced convention block across agents / phase-emitting workflows / templates + verify parity check; canonical reference doc; grep-evidence gate. | msd-core/references/, agents/*.md, msd-core/workflows/**/*.md, msd-core/templates/ |
AMENDED 2026-08-18 — APPROVED AND RATIFIED by maintainer. The PR-2 / PR-4 rows above are amended on three points, scoped to what actually ships in PR-2 (#2761) after the completion-seam work below was split out of it. The boundary between PR-2 and PR-4 is concern, not file: PR-2 owns everything that derives a number or a directory set from the ROADMAP; PR-4 owns everything that emits a bracket identifier.
-
state.ctsread-tolerance is in PR-2, not PR-4. Bothtotal_phasesderivations (buildStateFrontmatter,cmdStateSync) and the #1514 retirement filter are a parse-and-count of the ROADMAP, the same read-path concern PR-2 already owns inroadmap.ctsandvalidate.cts. PR-4 retains onlystate.cts's write concerns: bracket emit, themilestone:frontmatter field, andVALID_PHASE_ID_CONVENTIONS. -
The shared phase-directory enumerator (
listMilestonePhaseDirs) defaults itsphaseIdConventionargument to "not yet resolved — resolve from config" rather than "resolved, and not bracket." That default reaches every call site that does not pass the parameter explicitly, including two commands that write:cmdMilestoneComplete(milestone.cts) archives whatever directory set the enumerator returns, andcmdStateUpdateProgress(state.cts) gates and counts off it. Both are now explicit about it: each resolvesphase_id_conventiononce (viaresolvePhaseIdConvention) and threads it into its own enumerator call, rather than relying on the lazy default to resolve it the same way. On a project that has opted into'bracket', this meansmilestone completearchives the milestone's real bracket-declared phase directories instead of failing to recognize them — the same set the read path already reports.state update-progress's reported and written percent was already independently correct (it comes frombuildStateFrontmatter, which resolves and threads its own convention); the explicit thread at its own enumerator call closes a second, silent, separately-resolved answer to the same question, not a defect in the reported percent — mutation-tested: reverting only this thread leaves every existing percent/completed/total assertion on this command green, because the enumerator's own lazy resolve-from-config default answers the identical question the explicit thread does.cmdMilestoneComplete's enumerated set is pinned by a test (tests/adr-612-bracket-phase-counting.test.cjs, round-11 BLOCKER block) so a future regression to the pass-all-degrade legacy reading cannot silently move whatmilestone completearchives without failing a test.state update-progress's own call site is pinned differently, matching what it actually gates: not the reported percent, but the #3233 zero-plans no-op — a bracket milestone whose declared phases carry no plans on disk stays a no-op only when a directory that plainly does not belong to the milestone window is excluded from this call site's enumerated set; swept in by a pass-all degrade, the no-op stops firing (alsotests/adr-612-bracket-phase-counting.test.cjs, round-11 BLOCKER block, round-12 addition). Anull/milestone-prefixedproject's behavior at both call sites is unchanged either way — its resolved convention is the same value the lazy default would have produced. -
Two modules the table omitted, and one explicitly excluded.
phase-id.ctsis not PR-1-only: PR-2 consumes and extends the owner-sanctioned read sources (BRACKET_PHASE_TOKEN_SOURCE,PHASE_HEADING_PREFIX_SRC) inside the ADR-2121 canonical owner, and the phase-key derivation (phaseKeyFromDir) via an optionalconventionparameter forwarded toextractPhaseToken(additive, defaulted-absent, byte-identical at every existing call site).planning-workspace.ctshad no row at all; it hosts theresolvePhaseIdConventionresolver every gated read (and, per point 2, both write-adjacent call sites above) consults. Both are PR-2 foundations, not scope creep. Explicitly excluded from PR-2: the completion-seam threading ofconventionthroughisPhaseArtifact/scopeToPhaseand the rest of the phase-completion chain (phase-id.cts, ~424 lines) — round-11 review directed this split into a separate, stacked follow-up PR under epic #612, never reviewed as part of PR-2, and it ships independently.
Consequences
- Positive: every token is uniquely parseable; one pure round-trippable model under the single-owner regime; milestone-detection correct under READING-B; legacy (
null/ M-NN) repos byte-untouched; migration is opt-in and reversible; the convention count drops from a transient three to a terminal two. - Negative: a migration window in which reads must tolerate three forms; a second milestone authority under bracket (bracket integer vs STATE.md
milestone:) whose coherence-check teeth are advisory (W021, message-disambiguated per the folded W021 disposition below); thegetMilestoneFromPhaseIdreturn-form under bracket is ratified asvN.0(Decision 3 below), with the archive-dir glob coupled to that form. - Negative (added 2026-08-18): because the tolerant reads land before emit, a repo that opts into
'bracket'between PR-2 and PR-4 sees corrected counters, corrected milestone scoping, and corrected archival in commands that write (state sync,state update-progress,milestone complete) whilemsd phase addstill emits legacy ids. That asymmetry is the deliberate cost of read-before-write sequencing; it closes at PR-4. Repos onnull/milestone-prefixedremain byte-identical throughout. The completion-seam threading (point 3 above) is a separate, later cost: until its follow-up PR lands,isPhaseArtifact/scopeToPhaseand the phase-completion chain do not yet honor a resolved bracket convention, independent of anything PR-2 changes.
Decisions to ratify
Resolved items are normally folded into the body above; ratified items retained below preserve review history. Only items explicitly marked open still gate their named PR:
- Bare
02-04resolution (gates PR-1normalizePhaseName/ PR-2 resolvers). Options: (i) throw a disambiguation error; (ii) keep the current M-NN reading and let only the bracket path reject it; (iii) asurface: 'phase' | 'plan'context hint. The throw is a new behavior —normalizePhaseName('02-04')returns'02-04'today, it does not truncate — so it is not merely a fix. Not committed here. phase_namingvsphase_id_conventionaxis relationship (gates PR-4 build-default). Confirm these are two independent axes before wiring the new-project'bracket'default; the build-default object setsphase_namingbut omitsphase_id_convention.getMilestoneFromPhaseIdreturn-form under bracket + archive-dir naming (gated PR-1/PR-2) — RATIFIED 2026-07-24:vN.0(STATE.md parity, lowest churn), chosen over a bare integer; the archive-dir glob is value-coupled to this form and followsvN.0. PR-1 (#2258) ships this return-form. The separate milestone-identity normalization arc is unaffected.- Convention-card single-source location (gates PR-5/PR-6). Pin the single module/path so the ASCII grammar card renders identically at installer completion, migrator start (dry-run and apply), and in docs.
- PR-2 / PR-4 phase boundary, the write-observability of gated reads, and the completion-seam split (PR-2, i.e. #2761) — RATIFIED by maintainer. The amendment note beneath the Phases table, dated 2026-08-18, is approved. It (a) moves
state.ctsROADMAP-counting and retirement filtering into PR-2 while leaving emit andmilestone:frontmatter in PR-4, (b) states thatmilestone completeandstate update-progressnow explicitly thread the resolved convention into the shared enumerator rather than inheriting its lazy default, pinned by tests, and (c) confirms the completion-seam threading (isPhaseArtifact/scopeToPhase) ships in a separate, stacked follow-up PR under epic #612, not in PR-2. Raised by the #2867 review (PR-2, round-11) and approved by the maintainer in #2867 review5012940978(2026-08-24), which withdrew the ratification blocker in these terms: "Maintainer has approved the amended PR-2 scope... There is no separate ratification process in this repo; maintainer approval is the standard." That review is the receipt for this item; maintainer approval is the repository's ratification standard, so no separate ratification step remains.
Folded (resolved): M-NN deprecation stance → terminal by consolidation (Decision 5). W021 renumber → void: W021 is pinned by tests/milestone-prefixed-convention.test.cjs and is kept, message-disambiguated (the earlier plan to renumber it is dropped). READING-B milestone source → Decision 6.
References
- Issue #612 (this epic) —
approved-enhancement, tracer-bullet sequence PR-0…PR-6. - Issue #39 (milestone-encoding origin), #565 (M-NN implementation).
- ADR-2121 (#2121) — phase-identifier parsing consolidation; single canonical owner
src/phase-id.cts; anti-divergence guard. #2128 is thePHASE_NUMBER_TOKEN_SOURCEconstant (src/phase-id.cts:44-54). - ADR-1244, ADR-1016 — capability model (guardrail, Decision 9).