refactor(#1782): ADR-1769 Phase 2 — advancePlan migration onto Transition Module (#1783)

Migrates cmdStateAdvancePlan (~80-line RMW callback) onto the
transitionCore dispatch established in Phase 1 (#1775):

- src/state-transition.cts:
  - Add {kind: 'advancePlan'} to StateTransitionIntent union
  - Extend StateTransitionResult with optional data field for
    intent-specific output (advanced, currentPlan, totalPlans)
  - advancePlanCore: parses legacy + compound plan formats, handles
    advance vs phase-complete branching, strips frontmatter before
    body mutation (#1255 pattern — codex Phase 2 HIGH finding),
    uses stateReplaceFieldIfTemplate for template-default-aware
    field replacement (Knuth invariant), mutates ## Current Position
    section via mutateCurrentPositionForAdvance
  - mutateCurrentPositionForAdvance: inlined section mutation (avoids
    circular dep with state.cjs's updateCurrentPositionFields)

- src/state.cts:cmdStateAdvancePlan: collapsed to 30-line dispatch

- tests/state-transition.test.cjs: 5 characterization tests
  (advance, phase-complete, error, compound format, frontmatter #1255)

Codex gpt-5.5/high review: 1 HIGH blocking (frontmatter strip — fixed),
1 medium follow-up (StateTransitionResult.data shape discrimination —
noted for Phases 3-7).

gsd-test: 21893/21893 PASS (Linux docker).

Closes #1782
This commit is contained in:
Tom Boucher
2026-06-27 14:22:14 -04:00
committed by GitHub
parent 744bb7aaee
commit 1512e6f415
4 changed files with 442 additions and 69 deletions

View File

@@ -21,6 +21,7 @@ exports.transitionCore = transitionCore;
// eslint-disable-next-line @typescript-eslint/no-require-imports
const frontmatter = require("./frontmatter.cjs");
const state_document_cjs_1 = require("./state-document.cjs");
const state_document_cjs_2 = require("./state-document.cjs");
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const phaseIdMod = require("./phase-id.cjs");
@@ -113,6 +114,8 @@ function transitionCore(content, intent, deps) {
switch (intent.kind) {
case 'beginPhase':
return beginPhaseCore(content, intent, deps);
case 'advancePlan':
return advancePlanCore(content, deps);
}
}
// ----------------------------------------------------------------------------
@@ -337,3 +340,152 @@ function stripFrontmatter(content) {
}
return result;
}
/**
* Update fields within the ## Current Position section for advancePlan.
* Mirrors `updateCurrentPositionFields` (state.cts:496) byte-for-behaviour:
* only replaces Status / Last Activity when the existing value is a known
* template default (Knuth invariant: preserve executor-authored values).
* Plan is always replaced (system-derived, never executor-authored).
*
* Cannot import `updateCurrentPositionFields` from state.cjs directly (circular
* dep: state.cjs → state-transition.cjs → state.cjs), so the mutation is
* inlined here using the same primitives.
*/
function mutateCurrentPositionForAdvance(content, fields, statusDefaults, lastActivityDefaults) {
const span = locateCurrentPosition(content);
if (span === null)
return content;
let sectionBody = content.slice(span.start, span.end);
let mutated = false;
if (fields.status) {
const replaced = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(sectionBody, 'Status', statusDefaults, fields.status);
if (replaced !== null && replaced !== sectionBody) {
sectionBody = replaced;
mutated = true;
}
}
if (fields.lastActivity) {
const replaced = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(sectionBody, 'Last Activity', lastActivityDefaults, fields.lastActivity) ??
(0, state_document_cjs_1.stateReplaceFieldIfTemplate)(sectionBody, 'Last activity', lastActivityDefaults, fields.lastActivity);
if (replaced !== null && replaced !== sectionBody) {
sectionBody = replaced;
mutated = true;
}
}
if (fields.plan) {
// Plan is always replaced — system-derived, not executor-authored.
if (/^Plan:/m.test(sectionBody)) {
sectionBody = sectionBody.replace(/^Plan:.*$/m, `Plan: ${fields.plan}`);
mutated = true;
}
else {
const replaced = (0, state_document_cjs_1.stateReplaceField)(sectionBody, 'Plan', fields.plan);
if (replaced !== null) {
sectionBody = replaced;
mutated = true;
}
}
}
if (!mutated)
return content;
return content.slice(0, span.start) + sectionBody + content.slice(span.end);
}
// ----------------------------------------------------------------------------
// advancePlan — intent implementation (Phase 2)
// ----------------------------------------------------------------------------
/**
* Apply an `advancePlan` transition to STATE.md content.
*
* Parses Current Plan / Total Plans (legacy separate fields or compound
* "Plan: X of Y" format), increments the plan number, updates body fields
* and the ## Current Position section. When currentPlan >= totalPlans,
* takes the phase-complete branch (sets Status to "Phase complete — ready
* for verification") instead of advancing.
*
* Uses `stateReplaceFieldIfTemplate` (template-default-aware) to preserve
* executor-authored field values (Knuth invariant from cmdStateAdvancePlan).
*
* Returns `data.advanced` / `data.currentPlan` / `data.totalPlans` for the
* adapter to construct CLI output.
*/
function advancePlanCore(content, deps) {
const today = deps.clock.today();
// #1255: body-field replacements operate on body only (frontmatter stripped),
// not on the full content. The YAML `status:` key matches `^Status:\s*`
// before the body field if full content is passed (codex Phase 2 review:
// HIGH blocking finding — same pattern beginPhaseCore already handles).
const existingFm = extractFrontmatter(content);
const hasFrontmatter = Object.keys(existingFm).length > 0;
let body = stripFrontmatter(content);
const reassemble = (b) => hasFrontmatter
? `---\n${reconstructFrontmatter(existingFm)}\n---\n\n${b}`
: b;
// Parse plan number — legacy first, then compound.
const legacyPlan = (0, state_document_cjs_1.stateExtractField)(content, 'Current Plan');
const legacyTotal = (0, state_document_cjs_1.stateExtractField)(content, 'Total Plans in Phase');
const planField = (0, state_document_cjs_1.stateExtractField)(content, 'Plan');
let currentPlan;
let totalPlans;
let useCompoundFormat = false;
if (legacyPlan && legacyTotal) {
currentPlan = parseInt(legacyPlan, 10);
totalPlans = parseInt(legacyTotal, 10);
}
else if (planField) {
currentPlan = parseInt(planField, 10);
const ofMatch = planField.match(/of\s+(\d+)/);
totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
useCompoundFormat = true;
}
else {
currentPlan = NaN;
totalPlans = NaN;
}
if (isNaN(currentPlan) || isNaN(totalPlans)) {
return { content: reassemble(body), updated: [], data: { error: true } };
}
const updated = [];
const statusDefaults = state_document_cjs_2.KNOWN_TEMPLATE_DEFAULTS['Status'];
const lastActivityDefaults = state_document_cjs_2.KNOWN_TEMPLATE_DEFAULTS['Last Activity'];
if (currentPlan >= totalPlans) {
// Phase-complete branch.
body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Status', statusDefaults, 'Phase complete — ready for verification') || body;
body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Last Activity', lastActivityDefaults, today) || body;
body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Last activity', lastActivityDefaults, today) || body;
body = mutateCurrentPositionForAdvance(body, {
status: 'Phase complete — ready for verification',
lastActivity: today,
}, statusDefaults, lastActivityDefaults);
updated.push('Status', 'Last Activity', 'Current Position');
return {
content: reassemble(body),
updated,
data: { advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' },
};
}
// Normal advance branch.
const newPlan = currentPlan + 1;
let planDisplayValue;
if (useCompoundFormat) {
planDisplayValue = planField.replace(/^\d+/, String(newPlan));
body = (0, state_document_cjs_1.stateReplaceField)(body, 'Plan', planDisplayValue) || body;
}
else {
planDisplayValue = `${newPlan} of ${totalPlans}`;
body = (0, state_document_cjs_1.stateReplaceField)(body, 'Current Plan', String(newPlan)) || body;
}
body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Status', statusDefaults, 'Ready to execute') || body;
body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Last Activity', lastActivityDefaults, today) || body;
body = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(body, 'Last activity', lastActivityDefaults, today) || body;
body = mutateCurrentPositionForAdvance(body, {
status: 'Ready to execute',
lastActivity: today,
plan: planDisplayValue,
}, statusDefaults, lastActivityDefaults);
updated.push('Current Plan', 'Status', 'Last Activity', 'Current Position');
return {
content: reassemble(body),
updated,
data: { advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans },
};
}

View File

@@ -16,7 +16,8 @@
// eslint-disable-next-line @typescript-eslint/no-require-imports
import frontmatter = require('./frontmatter.cjs');
import { stateReplaceField, stateExtractField } from './state-document.cjs';
import { stateReplaceField, stateExtractField, stateReplaceFieldIfTemplate } from './state-document.cjs';
import { KNOWN_TEMPLATE_DEFAULTS } from './state-document.cjs';
import { tokenizeHeadings } from './markdown-sectionizer.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import phaseIdMod = require('./phase-id.cjs');
@@ -149,10 +150,17 @@ export type StateTransitionDeps = {
};
export type StateTransitionIntent =
| { kind: 'beginPhase'; phaseNumber: string | number; phaseName: string | null; planCount: number | null };
// Phases 2–7 add the remaining 9 intent kinds to this discriminated union.
| { kind: 'beginPhase'; phaseNumber: string | number; phaseName: string | null; planCount: number | null }
| { kind: 'advancePlan' };
// Phases 3–7 add the remaining 8 intent kinds to this discriminated union.
export type StateTransitionResult = { content: string; updated: string[] };
export type StateTransitionResult = {
content: string;
updated: string[];
/** Intent-specific output (e.g. advancePlan returns {advanced, currentPlan, totalPlans}).
* Adapters read this to construct the CLI output shape. */
data?: Record<string, unknown>;
};
// ----------------------------------------------------------------------------
// transitionCore — pure dispatch (ADR-1769 §3)
@@ -175,6 +183,8 @@ export function transitionCore(
switch (intent.kind) {
case 'beginPhase':
return beginPhaseCore(content, intent, deps);
case 'advancePlan':
return advancePlanCore(content, deps);
}
}
@@ -434,3 +444,161 @@ function stripFrontmatter(content: string): string {
}
return result;
}
/**
* Update fields within the ## Current Position section for advancePlan.
* Mirrors `updateCurrentPositionFields` (state.cts:496) byte-for-behaviour:
* only replaces Status / Last Activity when the existing value is a known
* template default (Knuth invariant: preserve executor-authored values).
* Plan is always replaced (system-derived, never executor-authored).
*
* Cannot import `updateCurrentPositionFields` from state.cjs directly (circular
* dep: state.cjs → state-transition.cjs → state.cjs), so the mutation is
* inlined here using the same primitives.
*/
function mutateCurrentPositionForAdvance(
content: string,
fields: { status?: string; lastActivity?: string; plan?: string },
statusDefaults: string[] | null | undefined,
lastActivityDefaults: string[] | null | undefined,
): string {
const span = locateCurrentPosition(content);
if (span === null) return content;
let sectionBody = content.slice(span.start, span.end);
let mutated = false;
if (fields.status) {
const replaced = stateReplaceFieldIfTemplate(sectionBody, 'Status', statusDefaults, fields.status);
if (replaced !== null && replaced !== sectionBody) { sectionBody = replaced; mutated = true; }
}
if (fields.lastActivity) {
const replaced =
stateReplaceFieldIfTemplate(sectionBody, 'Last Activity', lastActivityDefaults, fields.lastActivity) ??
stateReplaceFieldIfTemplate(sectionBody, 'Last activity', lastActivityDefaults, fields.lastActivity);
if (replaced !== null && replaced !== sectionBody) { sectionBody = replaced; mutated = true; }
}
if (fields.plan) {
// Plan is always replaced — system-derived, not executor-authored.
if (/^Plan:/m.test(sectionBody)) {
sectionBody = sectionBody.replace(/^Plan:.*$/m, `Plan: ${fields.plan}`);
mutated = true;
} else {
const replaced = stateReplaceField(sectionBody, 'Plan', fields.plan);
if (replaced !== null) { sectionBody = replaced; mutated = true; }
}
}
if (!mutated) return content;
return content.slice(0, span.start) + sectionBody + content.slice(span.end);
}
// ----------------------------------------------------------------------------
// advancePlan — intent implementation (Phase 2)
// ----------------------------------------------------------------------------
/**
* Apply an `advancePlan` transition to STATE.md content.
*
* Parses Current Plan / Total Plans (legacy separate fields or compound
* "Plan: X of Y" format), increments the plan number, updates body fields
* and the ## Current Position section. When currentPlan >= totalPlans,
* takes the phase-complete branch (sets Status to "Phase complete — ready
* for verification") instead of advancing.
*
* Uses `stateReplaceFieldIfTemplate` (template-default-aware) to preserve
* executor-authored field values (Knuth invariant from cmdStateAdvancePlan).
*
* Returns `data.advanced` / `data.currentPlan` / `data.totalPlans` for the
* adapter to construct CLI output.
*/
function advancePlanCore(content: string, deps: StateTransitionDeps): StateTransitionResult {
const today = deps.clock.today();
// #1255: body-field replacements operate on body only (frontmatter stripped),
// not on the full content. The YAML `status:` key matches `^Status:\s*`
// before the body field if full content is passed (codex Phase 2 review:
// HIGH blocking finding — same pattern beginPhaseCore already handles).
const existingFm = extractFrontmatter(content) as Record<string, unknown>;
const hasFrontmatter = Object.keys(existingFm).length > 0;
let body = stripFrontmatter(content);
const reassemble = (b: string): string =>
hasFrontmatter
? `---\n${reconstructFrontmatter(existingFm as unknown as Frontmatter)}\n---\n\n${b}`
: b;
// Parse plan number — legacy first, then compound.
const legacyPlan = stateExtractField(content, 'Current Plan');
const legacyTotal = stateExtractField(content, 'Total Plans in Phase');
const planField = stateExtractField(content, 'Plan');
let currentPlan: number;
let totalPlans: number;
let useCompoundFormat = false;
if (legacyPlan && legacyTotal) {
currentPlan = parseInt(legacyPlan, 10);
totalPlans = parseInt(legacyTotal, 10);
} else if (planField) {
currentPlan = parseInt(planField, 10);
const ofMatch = planField.match(/of\s+(\d+)/);
totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
useCompoundFormat = true;
} else {
currentPlan = NaN;
totalPlans = NaN;
}
if (isNaN(currentPlan) || isNaN(totalPlans)) {
return { content: reassemble(body), updated: [], data: { error: true } };
}
const updated: string[] = [];
const statusDefaults = KNOWN_TEMPLATE_DEFAULTS['Status'];
const lastActivityDefaults = KNOWN_TEMPLATE_DEFAULTS['Last Activity'];
if (currentPlan >= totalPlans) {
// Phase-complete branch.
body = stateReplaceFieldIfTemplate(body, 'Status', statusDefaults, 'Phase complete — ready for verification') || body;
body = stateReplaceFieldIfTemplate(body, 'Last Activity', lastActivityDefaults, today) || body;
body = stateReplaceFieldIfTemplate(body, 'Last activity', lastActivityDefaults, today) || body;
body = mutateCurrentPositionForAdvance(body, {
status: 'Phase complete — ready for verification',
lastActivity: today,
}, statusDefaults, lastActivityDefaults);
updated.push('Status', 'Last Activity', 'Current Position');
return {
content: reassemble(body),
updated,
data: { advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' },
};
}
// Normal advance branch.
const newPlan = currentPlan + 1;
let planDisplayValue: string;
if (useCompoundFormat) {
planDisplayValue = (planField as string).replace(/^\d+/, String(newPlan));
body = stateReplaceField(body, 'Plan', planDisplayValue) || body;
} else {
planDisplayValue = `${newPlan} of ${totalPlans}`;
body = stateReplaceField(body, 'Current Plan', String(newPlan)) || body;
}
body = stateReplaceFieldIfTemplate(body, 'Status', statusDefaults, 'Ready to execute') || body;
body = stateReplaceFieldIfTemplate(body, 'Last Activity', lastActivityDefaults, today) || body;
body = stateReplaceFieldIfTemplate(body, 'Last activity', lastActivityDefaults, today) || body;
body = mutateCurrentPositionForAdvance(body, {
status: 'Ready to execute',
lastActivity: today,
plan: planDisplayValue,
}, statusDefaults, lastActivityDefaults);
updated.push('Current Plan', 'Status', 'Last Activity', 'Current Position');
return {
content: reassemble(body),
updated,
data: { advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans },
};
}

View File

@@ -591,80 +591,33 @@ function cmdStateAdvancePlan(cwd: string, raw: boolean): void {
const statePath = planningPaths(cwd).state;
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw, undefined); return; }
const today = realClock.today();
let result: Record<string, unknown> | null = null;
// ADR-1769 Phase 2: dispatches to the STATE.md Transition Module. The
// ~80-line RMW callback that used to live here (plan parsing, advance vs
// phase-complete branching, template-default-aware field replacement,
// Current Position section mutation) is now the pure `advancePlanCore`
// function in src/state-transition.cts.
const intent: StateTransitionIntent = { kind: 'advancePlan' };
const deps: StateTransitionDeps = {
clock: realClock,
progressProvider: () => null,
};
let resultData: Record<string, unknown> | undefined;
readModifyWriteStateMd(statePath, (content) => {
// Try legacy separate fields first, then compound "Plan: X of Y" format
const legacyPlan = stateExtractField(content, 'Current Plan');
const legacyTotal = stateExtractField(content, 'Total Plans in Phase');
const planField = stateExtractField(content, 'Plan');
let currentPlan: number, totalPlans: number;
let useCompoundFormat = false;
if (legacyPlan && legacyTotal) {
currentPlan = parseInt(legacyPlan, 10);
totalPlans = parseInt(legacyTotal, 10);
} else if (planField) {
// Compound format: "2 of 6 in current phase" or "2 of 6"
currentPlan = parseInt(planField, 10);
const ofMatch = planField.match(/of\s+(\d+)/);
totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
useCompoundFormat = true;
} else {
currentPlan = NaN;
totalPlans = NaN;
}
if (isNaN(currentPlan) || isNaN(totalPlans)) {
result = { error: true };
return content;
}
const statusDefaults = KNOWN_TEMPLATE_DEFAULTS['Status'];
const lastActivityDefaults = KNOWN_TEMPLATE_DEFAULTS['Last Activity'];
if (currentPlan >= totalPlans) {
// Phase-complete branch — only replace Status/Last Activity when the existing
// value is a known template default (Knuth invariant: preserve executor-authored).
content = stateReplaceFieldIfTemplate(content, 'Status', statusDefaults, 'Phase complete — ready for verification');
content = stateReplaceFieldIfTemplate(content, 'Last Activity', lastActivityDefaults, today);
// stateReplaceFieldWithFallback tries 'Last activity' alias too
content = stateReplaceFieldIfTemplate(content, 'Last activity', lastActivityDefaults, today);
content = updateCurrentPositionFields(content, { status: 'Phase complete — ready for verification', lastActivity: today });
result = { advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' };
} else {
const newPlan = currentPlan + 1;
let planDisplayValue: string;
if (useCompoundFormat) {
// Preserve compound format: "X of Y in current phase" → replace X only
planDisplayValue = (planField as string).replace(/^\d+/, String(newPlan));
content = stateReplaceField(content, 'Plan', planDisplayValue) || content;
} else {
planDisplayValue = `${newPlan} of ${totalPlans}`;
content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
}
// Normal advance — only replace Status/Last Activity when the existing value is
// a known template default (Knuth invariant: preserve executor-authored).
content = stateReplaceFieldIfTemplate(content, 'Status', statusDefaults, 'Ready to execute');
content = stateReplaceFieldIfTemplate(content, 'Last Activity', lastActivityDefaults, today);
content = stateReplaceFieldIfTemplate(content, 'Last activity', lastActivityDefaults, today);
content = updateCurrentPositionFields(content, { status: 'Ready to execute', lastActivity: today, plan: planDisplayValue });
result = { advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans };
}
return content;
const result = transitionCore(content, intent, deps);
resultData = result.data;
return result.content;
}, cwd);
if (!result || (result as Record<string, unknown>)['error']) {
if (!resultData || resultData['error']) {
output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw, undefined);
return;
}
if ((result as Record<string, unknown>)['advanced'] === false) {
output(result, raw, 'false');
if (resultData['advanced'] === false) {
output(resultData, raw, 'false');
} else {
output(result, raw, 'true');
output(resultData, raw, 'true');
}
}

View File

@@ -415,3 +415,103 @@ describe('ADR-1769 Phase 1: property tests (RULESET.TESTS.property-based-testing
);
});
});
describe('ADR-1769 Phase 2: advancePlan transition', () => {
const deps = { clock: fixedClock, progressProvider: noProgress };
test('advances Current Plan from N to N+1 (legacy format)', () => {
const input = [
'# Project State',
'',
'**Current Plan:** 02',
'**Total Plans in Phase:** 05',
'**Status:** Executing Phase 3',
'**Last Activity:** 2026-06-26',
'',
'## Current Position',
'',
'Plan: 2 of 5',
'Status: Executing Phase 3',
'',
].join('\n');
const result = transitionCore(input, { kind: 'advancePlan' }, deps);
assert.strictEqual(stateExtractField(result.content, 'Current Plan'), '3');
assert.strictEqual(result.data && result.data.advanced, true);
assert.strictEqual(result.data && result.data.current_plan, 3);
assert.strictEqual(result.data && result.data.total_plans, 5);
});
test('phase-complete branch when currentPlan >= totalPlans', () => {
const input = [
'# Project State',
'',
'**Current Plan:** 05',
'**Total Plans in Phase:** 05',
'**Status:** Executing Phase 3',
'**Last Activity:** 2026-06-26',
'',
].join('\n');
const result = transitionCore(input, { kind: 'advancePlan' }, deps);
assert.strictEqual(result.data && result.data.advanced, false);
assert.strictEqual(result.data && result.data.reason, 'last_plan');
assert.strictEqual(result.data && result.data.status, 'ready_for_verification');
});
test('error when plan fields are unparseable', () => {
const input = '# Project State\n\nNo plan fields here.\n';
const result = transitionCore(input, { kind: 'advancePlan' }, deps);
assert.strictEqual(result.data && result.data.error, true);
assert.deepStrictEqual(result.updated, []);
});
test('compound format: "Plan: 2 of 6" preserves compound shape', () => {
const input = [
'# Project State',
'',
'**Plan:** 2 of 6',
'**Status:** Executing Phase 3',
'**Last Activity:** 2026-06-26',
'',
].join('\n');
const result = transitionCore(input, { kind: 'advancePlan' }, deps);
const plan = stateExtractField(result.content, 'Plan');
assert.ok(/3 of 6/.test(plan || ''), `Plan should be "3 of 6"; got ${JSON.stringify(plan)}`);
assert.strictEqual(result.data && result.data.advanced, true);
});
});
describe('ADR-1769 Phase 2: advancePlan with frontmatter (#1255 pattern — codex review)', () => {
const deps = { clock: fixedClock, progressProvider: noProgress };
test('advances plan correctly when STATE.md has YAML frontmatter (body Status not YAML status)', () => {
const input = [
'---',
'status: Executing Phase 3',
'current_phase: "03"',
'---',
'',
'# Project State',
'',
'**Current Plan:** 02',
'**Total Plans in Phase:** 05',
'**Status:** Executing Phase 3',
'**Last Activity:** 2026-06-26',
'',
'## Current Position',
'',
'Plan: 2 of 5',
'Status: Executing Phase 3',
'',
].join('\n');
const result = transitionCore(input, { kind: 'advancePlan' }, deps);
// Body Current Plan must advance to 3.
assert.strictEqual(stateExtractField(result.content, 'Current Plan'), '3');
// Body Status must be updated (not the YAML status key).
const bodyStatus = stateExtractField(result.content, 'Status');
assert.ok(
/Ready to execute/.test(bodyStatus || ''),
`body Status should be "Ready to execute"; got ${JSON.stringify(bodyStatus)}`,
);
assert.strictEqual(result.data && result.data.advanced, true);
});
});