diff --git a/.changeset/sharp-badgers-squeak.md b/.changeset/sharp-badgers-squeak.md new file mode 100644 index 000000000..1b3e2f596 --- /dev/null +++ b/.changeset/sharp-badgers-squeak.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3275 +--- +state-snapshot no longer returns wrong status and other fields when STATE.md body contains a Markdown table cell with bold field syntax (e.g. **Status:** in a task history row) — YAML frontmatter values now take precedence over body extraction for all canonical scalar fields. diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 8195b76d4..16f7448b0 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -648,17 +648,36 @@ function cmdStateSnapshot(cwd, raw) { const content = fs.readFileSync(statePath, 'utf-8'); - // Extract basic fields - const currentPhase = stateExtractField(content, 'Current Phase'); - const currentPhaseName = stateExtractField(content, 'Current Phase Name'); - const totalPhasesRaw = stateExtractField(content, 'Total Phases'); - const currentPlan = stateExtractField(content, 'Current Plan'); - const totalPlansRaw = stateExtractField(content, 'Total Plans in Phase'); - const status = stateExtractField(content, 'Status'); - const progressRaw = stateExtractField(content, 'Progress'); - const lastActivity = stateExtractField(content, 'Last Activity'); - const lastActivityDesc = stateExtractField(content, 'Last Activity Description'); - const pausedAt = stateExtractField(content, 'Paused At'); + // Bug #3265: prefer YAML frontmatter for canonical scalar fields so that a + // body table cell containing **Status:** Y cannot shadow the authoritative + // frontmatter value. Mirrors the fix in sdk/src/query/state.ts. + const fm = extractFrontmatter(content); + const body = stripFrontmatter(content); + + // Helper: return frontmatter scalar value when present and non-empty. + // Accepts strings, numbers, and booleans — coercing non-string primitives to + // their string representation so callers always receive string | null. + // Returns null for missing, null/undefined, or empty-after-trim values so + // the caller falls back to body extraction. + const fmScalar = (key) => { + const v = fm[key]; + if (v === null || v === undefined) return null; + if (typeof v === 'string') return v.trim() || null; + if (typeof v === 'number' || typeof v === 'boolean') return String(v); + return null; + }; + + // Extract basic fields — frontmatter keys take precedence over body + const currentPhase = fmScalar('current_phase') ?? stateExtractField(body, 'Current Phase'); + const currentPhaseName = fmScalar('current_phase_name') ?? stateExtractField(body, 'Current Phase Name'); + const totalPhasesRaw = fmScalar('total_phases') ?? stateExtractField(body, 'Total Phases'); + const currentPlan = fmScalar('current_plan') ?? stateExtractField(body, 'Current Plan'); + const totalPlansRaw = fmScalar('total_plans_in_phase') ?? stateExtractField(body, 'Total Plans in Phase'); + const status = fmScalar('status') ?? stateExtractField(body, 'Status'); + const progressRaw = fmScalar('progress') ?? stateExtractField(body, 'Progress'); + const lastActivity = fmScalar('last_activity') ?? stateExtractField(body, 'Last Activity'); + const lastActivityDesc = fmScalar('last_activity_desc') ?? stateExtractField(body, 'Last Activity Description'); + const pausedAt = fmScalar('paused_at') ?? stateExtractField(body, 'Paused At'); // Parse numeric fields const totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null; @@ -667,7 +686,7 @@ function cmdStateSnapshot(cwd, raw) { // Extract decisions table const decisions = []; - const decisionsMatch = content.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i); + const decisionsMatch = body.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i); if (decisionsMatch) { const tableBody = decisionsMatch[1]; const rows = tableBody.trim().split('\n').filter(r => r.includes('|')); @@ -685,7 +704,7 @@ function cmdStateSnapshot(cwd, raw) { // Extract blockers list const blockers = []; - const blockersMatch = content.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i); + const blockersMatch = body.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i); if (blockersMatch) { const blockersSection = blockersMatch[1]; const items = blockersSection.match(/^-\s+(.+)$/gm) || []; @@ -701,7 +720,7 @@ function cmdStateSnapshot(cwd, raw) { resume_file: null, }; - const sessionMatch = content.match(/##\s*Session\s*\n([\s\S]*?)(?=\n##|$)/i); + const sessionMatch = body.match(/##\s*Session\s*\n([\s\S]*?)(?=\n##|$)/i); if (sessionMatch) { const sessionSection = sessionMatch[1]; const lastDateMatch = sessionSection.match(/\*\*Last Date:\*\*\s*(.+)/i) diff --git a/sdk/src/query/state.test.ts b/sdk/src/query/state.test.ts index 09c7eeb04..a97eac3b6 100644 --- a/sdk/src/query/state.test.ts +++ b/sdk/src/query/state.test.ts @@ -346,6 +346,126 @@ describe('stateSnapshot', () => { }); }); +// ─── Regression: #3265 — frontmatter wins over bold-body cell ───────────── + +describe('stateSnapshot — bug #3265 frontmatter precedence', () => { + it('returns frontmatter status, not **Status:** value embedded in a body table cell', async () => { + // Reproduce the collision: frontmatter says "executing", but the body + // contains a Markdown table cell with "**Status:** to ✅ COMPLETE ..." + // which stateExtractField (bold pattern) would match before the YAML line. + const stateContent = [ + '---', + 'gsd_state_version: 1.0', + 'status: executing', + 'current_plan: 19.5-05', + '---', + '', + '# Project State', + '', + '## Recent Quick Tasks', + '', + '| Date | Task | Notes |', + '|------|------|-------|', + '| 2026-05-01 | Reopened Plan 19.5-05. **Status:** to ✅ COMPLETE | done |', + '', + '**Current Phase:** 19', + '**Current Plan:** archived-lane', + '', + ].join('\n'); + + const localDir = await mkdtemp(join(tmpdir(), 'gsd-3265-')); + await mkdir(join(localDir, '.planning'), { recursive: true }); + await writeFile(join(localDir, '.planning', 'STATE.md'), stateContent); + + const result = await stateSnapshot([], localDir); + const data = result.data as Record; + + // Frontmatter status must win + expect(data.status).toBe('executing'); + + await rm(localDir, { recursive: true, force: true }); + }); + + it('returns frontmatter current_plan, not bold body value when both present', async () => { + const stateContent = [ + '---', + 'gsd_state_version: 1.0', + 'status: executing', + 'current_plan: 19.5-05', + '---', + '', + '# Project State', + '', + '**Current Phase:** 19', + '**Current Plan:** archived-lane', + '', + ].join('\n'); + + const localDir = await mkdtemp(join(tmpdir(), 'gsd-3265b-')); + await mkdir(join(localDir, '.planning'), { recursive: true }); + await writeFile(join(localDir, '.planning', 'STATE.md'), stateContent); + + const result = await stateSnapshot([], localDir); + const data = result.data as Record; + + // Frontmatter current_plan must win over body bold value + expect(data.current_plan).toBe('19.5-05'); + + await rm(localDir, { recursive: true, force: true }); + }); + + it('falls back to body extraction when no frontmatter block is present', async () => { + const stateContent = [ + '# Project State', + '', + '**Current Phase:** 07', + '**Status:** paused', + '', + ].join('\n'); + + const localDir = await mkdtemp(join(tmpdir(), 'gsd-3265c-')); + await mkdir(join(localDir, '.planning'), { recursive: true }); + await writeFile(join(localDir, '.planning', 'STATE.md'), stateContent); + + const result = await stateSnapshot([], localDir); + const data = result.data as Record; + + // No frontmatter — body extraction must still work + expect(data.status).toBe('paused'); + expect(data.current_phase).toBe('07'); + + await rm(localDir, { recursive: true, force: true }); + }); + + it('falls back to body extractor for a field absent from frontmatter', async () => { + // Frontmatter has status but no current_plan — snapshot must body-extract current_plan + const stateContent = [ + '---', + 'gsd_state_version: 1.0', + 'status: planning', + '---', + '', + '# Project State', + '', + '**Current Plan:** 05-03', + '', + ].join('\n'); + + const localDir = await mkdtemp(join(tmpdir(), 'gsd-3265d-')); + await mkdir(join(localDir, '.planning'), { recursive: true }); + await writeFile(join(localDir, '.planning', 'STATE.md'), stateContent); + + const result = await stateSnapshot([], localDir); + const data = result.data as Record; + + expect(data.status).toBe('planning'); + // current_plan absent from frontmatter — must come from body + expect(data.current_plan).toBe('05-03'); + + await rm(localDir, { recursive: true, force: true }); + }); +}); + // ─── Regression: --ws propagation (#2618 gap 1) ──────────────────────────── describe('stateJson with --ws workstream', () => { @@ -379,3 +499,90 @@ Status: planning expect(data.status).toBe('planning'); }); }); + +// ─── Regression: #3275 CR — fmScalar handles numeric/boolean YAML scalars ─── + +describe('stateSnapshot — CR #3275 fmScalar non-string scalar coercion', () => { + it('treats numeric current_phase as string "19", not missing', async () => { + // A real YAML parser (e.g. js-yaml) would parse `current_phase: 19` as + // the number 19, not the string "19". fmScalar must coerce it so the + // frontmatter value wins over the body's bold field. + const stateContent = [ + '---', + 'gsd_state_version: 1.0', + 'current_phase: 19', + '---', + '', + '# Project State', + '', + '**Current Phase:** 03', + '**Status:** executing', + '', + ].join('\n'); + + const localDir = await mkdtemp(join(tmpdir(), 'gsd-3275a-')); + await mkdir(join(localDir, '.planning'), { recursive: true }); + await writeFile(join(localDir, '.planning', 'STATE.md'), stateContent); + + const result = await stateSnapshot([], localDir); + const data = result.data as Record; + + // Frontmatter wins: current_phase must be "19", not "03" (from body) + expect(data.current_phase).toBe('19'); + + await rm(localDir, { recursive: true, force: true }); + }); + + it('treats numeric total_phases in frontmatter as string, not missing', async () => { + const stateContent = [ + '---', + 'gsd_state_version: 1.0', + 'total_phases: 7', + '---', + '', + '# Project State', + '', + '**Total Phases:** 3', + '**Status:** executing', + '', + ].join('\n'); + + const localDir = await mkdtemp(join(tmpdir(), 'gsd-3275b-')); + await mkdir(join(localDir, '.planning'), { recursive: true }); + await writeFile(join(localDir, '.planning', 'STATE.md'), stateContent); + + const result = await stateSnapshot([], localDir); + const data = result.data as Record; + + // total_phases is parsed as int downstream: frontmatter 7 must win over body 3 + expect(data.total_phases).toBe(7); + + await rm(localDir, { recursive: true, force: true }); + }); + + it('treats numeric total_plans_in_phase in frontmatter as string, not missing', async () => { + const stateContent = [ + '---', + 'gsd_state_version: 1.0', + 'total_plans_in_phase: 5', + '---', + '', + '# Project State', + '', + '**Total Plans in Phase:** 2', + '**Status:** executing', + '', + ].join('\n'); + + const localDir = await mkdtemp(join(tmpdir(), 'gsd-3275c-')); + await mkdir(join(localDir, '.planning'), { recursive: true }); + await writeFile(join(localDir, '.planning', 'STATE.md'), stateContent); + + const result = await stateSnapshot([], localDir); + const data = result.data as Record; + + expect(data.total_plans_in_phase).toBe(5); + + await rm(localDir, { recursive: true, force: true }); + }); +}); diff --git a/sdk/src/query/state.ts b/sdk/src/query/state.ts index 7d31ac725..e184eafc2 100644 --- a/sdk/src/query/state.ts +++ b/sdk/src/query/state.ts @@ -337,17 +337,38 @@ export const stateSnapshot: QueryHandler = async (_args, projectDir, workstream) return { data: { error: 'STATE.md not found' } }; } - // Extract basic fields - const currentPhase = stateExtractField(content, 'Current Phase'); - const currentPhaseName = stateExtractField(content, 'Current Phase Name'); - const totalPhasesRaw = stateExtractField(content, 'Total Phases'); - const currentPlan = stateExtractField(content, 'Current Plan'); - const totalPlansRaw = stateExtractField(content, 'Total Plans in Phase'); - const status = stateExtractField(content, 'Status'); - const progressRaw = stateExtractField(content, 'Progress'); - const lastActivity = stateExtractField(content, 'Last Activity'); - const lastActivityDesc = stateExtractField(content, 'Last Activity Description'); - const pausedAt = stateExtractField(content, 'Paused At'); + // Bug #3265: prefer YAML frontmatter for canonical scalar fields so that a + // body table cell containing **Status:** Y cannot shadow the authoritative + // frontmatter value. Matches the precedent set by buildStateFrontmatter + // (see state.ts:92 Bug #2613 comment). + const fm = extractFrontmatter(content); + const body = stripFrontmatter(content); + + // Helper: return frontmatter scalar value when present and non-empty. + // Accepts strings, numbers, and booleans — coercing non-string primitives to + // their string representation so callers always receive string | null. + // Returns null for missing, null/undefined, or empty-after-trim values so + // the caller falls back to body extractor (covers STATE.md files that have + // no frontmatter at all, or frontmatter that lacks the specific key). + const fmScalar = (key: string): string | null => { + const v = fm[key]; + if (v === null || v === undefined) return null; + if (typeof v === 'string') return v.trim() || null; + if (typeof v === 'number' || typeof v === 'boolean') return String(v); + return null; + }; + + // Extract basic fields — frontmatter keys take precedence over body + const currentPhase = fmScalar('current_phase') ?? stateExtractField(body, 'Current Phase'); + const currentPhaseName = fmScalar('current_phase_name') ?? stateExtractField(body, 'Current Phase Name'); + const totalPhasesRaw = fmScalar('total_phases') ?? stateExtractField(body, 'Total Phases'); + const currentPlan = fmScalar('current_plan') ?? stateExtractField(body, 'Current Plan'); + const totalPlansRaw = fmScalar('total_plans_in_phase') ?? stateExtractField(body, 'Total Plans in Phase'); + const status = fmScalar('status') ?? stateExtractField(body, 'Status'); + const progressRaw = fmScalar('progress') ?? stateExtractField(body, 'Progress'); + const lastActivity = fmScalar('last_activity') ?? stateExtractField(body, 'Last Activity'); + const lastActivityDesc = fmScalar('last_activity_desc') ?? stateExtractField(body, 'Last Activity Description'); + const pausedAt = fmScalar('paused_at') ?? stateExtractField(body, 'Paused At'); // Parse numeric fields const totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null; @@ -361,7 +382,7 @@ export const stateSnapshot: QueryHandler = async (_args, projectDir, workstream) // Extract decisions table const decisions: Array<{ phase: string; summary: string; rationale: string }> = []; - const decisionsMatch = content.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i); + const decisionsMatch = body.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i); if (decisionsMatch) { const tableBody = decisionsMatch[1]; const rows = tableBody.trim().split('\n').filter(r => r.includes('|')); @@ -379,7 +400,7 @@ export const stateSnapshot: QueryHandler = async (_args, projectDir, workstream) // Extract blockers list const blockers: string[] = []; - const blockersMatch = content.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i); + const blockersMatch = body.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i); if (blockersMatch) { const blockersSection = blockersMatch[1]; const items = blockersSection.match(/^-\s+(.+)$/gm) || []; @@ -395,7 +416,7 @@ export const stateSnapshot: QueryHandler = async (_args, projectDir, workstream) resume_file: null, }; - const sessionMatch = content.match(/##\s*Session\s*\n([\s\S]*?)(?=\n##|$)/i); + const sessionMatch = body.match(/##\s*Session\s*\n([\s\S]*?)(?=\n##|$)/i); if (sessionMatch) { const sessionSection = sessionMatch[1]; const lastDateMatch = sessionSection.match(/\*\*Last Date:\*\*\s*(.+)/i) diff --git a/tests/bug-3275-fmstr-non-string-scalars.test.cjs b/tests/bug-3275-fmstr-non-string-scalars.test.cjs new file mode 100644 index 000000000..0fe89a45c --- /dev/null +++ b/tests/bug-3275-fmstr-non-string-scalars.test.cjs @@ -0,0 +1,146 @@ +/** + * GSD Tools Tests — Bug #3275 (CR finding) + * + * Regression guard: `state-snapshot` must prefer YAML frontmatter scalar + * values even when those scalars are numeric (e.g. current_phase: 19) or + * boolean — not just when they are strings. + * + * Prior to the fix, `fmStr` checked `typeof v === 'string'`, so a numeric + * frontmatter value like `current_phase: 19` was treated as missing and the + * snapshot fell back to body extraction, which could return a stale or + * incorrect value. + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); + +describe('state-snapshot: fmStr accepts non-string YAML scalars (#3275 CR)', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('numeric current_phase in frontmatter wins over body extraction', () => { + // YAML parses bare integers as numbers, not strings. + // fmStr must not drop the frontmatter value when it is a number. + const stateMd = [ + '---', + 'gsd_state_version: 1.0', + 'current_phase: 19', + '---', + '', + '# Project State', + '', + '**Current Phase:** 03', + '**Status:** executing', + '', + ].join('\n'); + + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // Frontmatter numeric value must win over bold-body value + assert.strictEqual(output.current_phase, '19', 'numeric frontmatter current_phase must be used'); + }); + + test('numeric total_phases in frontmatter wins over body extraction', () => { + const stateMd = [ + '---', + 'gsd_state_version: 1.0', + 'total_phases: 7', + '---', + '', + '# Project State', + '', + '**Total Phases:** 3', + '**Status:** executing', + '', + ].join('\n'); + + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // Frontmatter says 7, body says 3 — frontmatter must win + assert.strictEqual(output.total_phases, 7, 'numeric frontmatter total_phases must be used'); + }); + + test('numeric total_plans_in_phase in frontmatter wins over body extraction', () => { + const stateMd = [ + '---', + 'gsd_state_version: 1.0', + 'total_plans_in_phase: 5', + '---', + '', + '# Project State', + '', + '**Total Plans in Phase:** 2', + '**Status:** executing', + '', + ].join('\n'); + + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.total_plans_in_phase, 5, 'numeric frontmatter total_plans_in_phase must be used'); + }); + + test('string current_phase in frontmatter still works (no regression)', () => { + const stateMd = [ + '---', + 'gsd_state_version: 1.0', + "current_phase: '19'", + '---', + '', + '# Project State', + '', + '**Current Phase:** 03', + '**Status:** executing', + '', + ].join('\n'); + + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.current_phase, '19', 'string frontmatter current_phase still works'); + }); + + test('no-frontmatter file still extracts from body (no regression)', () => { + const stateMd = [ + '# Project State', + '', + '**Current Phase:** 05', + '**Total Phases:** 8', + '**Status:** paused', + '', + ].join('\n'); + + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.current_phase, '05', 'body extraction still works without frontmatter'); + assert.strictEqual(output.total_phases, 8, 'numeric body total_phases still extracted'); + }); +}); diff --git a/tests/state.test.cjs b/tests/state.test.cjs index e21b46375..6e1433df0 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -191,6 +191,98 @@ describe('state-snapshot command', () => { }); }); +// ─── Regression: #3265 — frontmatter wins over bold-body cell ───────────── + +describe('state-snapshot — bug #3265 frontmatter precedence', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('returns frontmatter status, not **Status:** value embedded in a body table cell', () => { + // Reproduce the collision: frontmatter says "executing", but the body + // contains a Markdown table cell with "**Status:** to ✅ COMPLETE ..." + // which stateExtractField (bold pattern) would match before the YAML line. + const stateContent = [ + '---', + 'gsd_state_version: 1.0', + 'status: executing', + 'current_plan: 19.5-05', + '---', + '', + '# Project State', + '', + '## Recent Quick Tasks', + '', + '| Date | Task | Notes |', + '|------|------|-------|', + '| 2026-05-01 | Reopened Plan 19.5-05. **Status:** to ✅ COMPLETE | done |', + '', + '**Current Phase:** 19', + '**Current Plan:** archived-lane', + '', + ].join('\n'); + + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateContent); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // Frontmatter status must win over the table cell's **Status:** match + assert.strictEqual(output.status, 'executing', 'frontmatter status beats body table cell'); + }); + + test('returns frontmatter current_plan, not bold body value when both present', () => { + const stateContent = [ + '---', + 'gsd_state_version: 1.0', + 'status: executing', + 'current_plan: 19.5-05', + '---', + '', + '# Project State', + '', + '**Current Phase:** 19', + '**Current Plan:** archived-lane', + '', + ].join('\n'); + + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateContent); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.current_plan, '19.5-05', 'frontmatter current_plan beats body bold value'); + }); + + test('falls back to body extraction when no frontmatter block is present', () => { + const stateContent = [ + '# Project State', + '', + '**Current Phase:** 07', + '**Status:** paused', + '', + ].join('\n'); + + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateContent); + + const result = runGsdTools('state-snapshot', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + // No frontmatter — body extraction must still work + assert.strictEqual(output.status, 'paused', 'body extraction works without frontmatter'); + assert.strictEqual(output.current_phase, '07', 'body extraction works without frontmatter'); + }); +}); + describe('state mutation commands', () => { let tmpDir;