refactor(#1789): ADR-1769 Phase 5 — milestoneComplete migration (#1790)

Migrate the STATE.md write path inside cmdMilestoneComplete (milestone.cts)
onto the STATE.md Transition Module substrate (ADR-1769, epic #1769).

- Add {kind: 'milestoneComplete'} to StateTransitionIntent, with
  milestoneCompleteCore in src/state-transition.cts (consulting the
  field-classification table). Owns the closure write: Status
  ('<version> milestone complete'), Last Activity, Last Activity Description,
  a Current Position reset to 'Awaiting next milestone', and an Operator Next
  Steps reset pointing at the next-milestone command.
- Collapse the inline STATE.md transform in cmdMilestoneComplete to a
  transitionCore dispatch. The adapter retains writeStateMd (lock + steady-state
  syncStateFrontmatter post-sync) and resolves the runtime-specific
  next-milestone slash command, injecting it via intent.nextMilestoneCommand.
- The two section resets carry their pre-seam allow-adhoc-markdown waivers
  (regex semantics pinned by existing tests; pending collectSection #1372).
- realClock.today() is byte-identical to milestone.cts's local today
  (both new Date().toISOString().split('T')[0]).
- Characterization tests pin field updates, both section resets (replace +
  insert paths), and frontmatter #1255 parity.

All 66 transition + milestone + state tests pass.

Closes #1789
This commit is contained in:
Tom Boucher
2026-06-27 15:46:54 -04:00
committed by GitHub
parent 91704c9fcf
commit 3ceb83329d
5 changed files with 348 additions and 41 deletions

View File

@@ -16,6 +16,8 @@ import frontmatterMod = require('./frontmatter.cjs');
import stateMod = require('./state.cjs');
import { platformWriteSync, platformEnsureDir, execGit, retryRenameSync } from './shell-command-projection.cjs';
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
import { realClock } from './clock.cjs';
import { transitionCore } from './state-transition.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import ioMod = require('./io.cjs');
const { output, error } = ioMod;
@@ -30,7 +32,7 @@ import coreUtilsMod = require('./core-utils.cjs');
const { extractOneLinerFromBody } = coreUtilsMod;
const { planningPaths } = planningWorkspace;
const { extractFrontmatter } = frontmatterMod;
const { writeStateMd, stateReplaceFieldWithFallback } = stateMod;
const { writeStateMd } = stateMod;
interface MilestoneCompleteOptions {
name?: string;
@@ -311,45 +313,25 @@ function cmdMilestoneComplete(cwd: string, version: string, options: MilestoneCo
platformWriteSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`);
}
// Update STATE.md — keep frontmatter/body semantically aligned after closure
// Update STATE.md — keep frontmatter/body semantically aligned after closure.
// ADR-1769 Phase 5: dispatches to the STATE.md Transition Module. The closure
// write (Status, Last Activity, Last Activity Description, Current Position
// reset, Operator Next Steps reset) is the pure `milestoneCompleteCore` in
// src/state-transition.cts, backed by the field-classification table. The
// runtime-specific next-milestone slash command is resolved here and injected
// via the intent so the core stays pure. writeStateMd still owns the lock and
// the steady-state syncStateFrontmatter post-sync.
if (fs.existsSync(statePath)) {
let stateContent = fs.readFileSync(statePath, 'utf-8');
stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, `${version} milestone complete`);
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity', 'Last activity', today);
stateContent = stateReplaceFieldWithFallback(
stateContent,
'Last Activity Description',
null,
`${version} milestone completed and archived`,
const result = transitionCore(
fs.readFileSync(statePath, 'utf-8'),
{
kind: 'milestoneComplete',
version,
nextMilestoneCommand: formatGsdSlash('new-milestone', resolveRuntime(cwd)) as string,
},
{ clock: realClock, progressProvider: () => null },
);
// Reset Current Position narrative so resume/progress flows do not keep
// pointing at closed-phase execution instructions.
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i; // allow-adhoc-markdown: pre-seam section write-modify in milestone.cts; pending collectSection migration #1372
const closedPositionBody =
`\nPhase: Milestone ${version} complete\n` +
`Plan: —\n` +
`Status: Awaiting next milestone\n` +
`Last activity: ${today} — Milestone ${version} completed and archived\n\n`;
if (positionPattern.test(stateContent)) {
stateContent = stateContent.replace(positionPattern, (_m, header: string) => `${header}${closedPositionBody}`);
} else {
stateContent = `${stateContent.trimEnd()}\n\n## Current Position\n${closedPositionBody}`;
}
// Normalize operator-next-step tails that can become stale after close.
const operatorPattern = /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i; // allow-adhoc-markdown: pre-seam section write-modify in milestone.cts; pending collectSection migration #1372
if (operatorPattern.test(stateContent)) {
stateContent = stateContent.replace(
operatorPattern,
`$1\n- Start the next milestone with ${formatGsdSlash('new-milestone', resolveRuntime(cwd)) as string}\n\n`,
);
} else {
stateContent = `${stateContent.trimEnd()}\n\n## Operator Next Steps\n\n- Start the next milestone with ${formatGsdSlash('new-milestone', resolveRuntime(cwd)) as string}\n`;
}
writeStateMd(statePath, stateContent, cwd);
writeStateMd(statePath, result.content, cwd);
}
// Archive phase directories if requested