diff --git a/gsd-core/bin/lib/state-transition.cjs b/gsd-core/bin/lib/state-transition.cjs index ca8572b2d..773677b1c 100644 --- a/gsd-core/bin/lib/state-transition.cjs +++ b/gsd-core/bin/lib/state-transition.cjs @@ -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 }, + }; +} diff --git a/src/state-transition.cts b/src/state-transition.cts index 3c968d23f..eeae05143 100644 --- a/src/state-transition.cts +++ b/src/state-transition.cts @@ -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; +}; // ---------------------------------------------------------------------------- // 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; + 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 }, + }; +} diff --git a/src/state.cts b/src/state.cts index d909939e9..3f9979986 100644 --- a/src/state.cts +++ b/src/state.cts @@ -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 | 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 | 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)['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)['advanced'] === false) { - output(result, raw, 'false'); + if (resultData['advanced'] === false) { + output(resultData, raw, 'false'); } else { - output(result, raw, 'true'); + output(resultData, raw, 'true'); } } diff --git a/tests/state-transition.test.cjs b/tests/state-transition.test.cjs index 2bb548674..7654eec14 100644 --- a/tests/state-transition.test.cjs +++ b/tests/state-transition.test.cjs @@ -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); + }); +});