fix(#1255): advance frontmatter status for pipe-table STATE.md on begin/complete-phase (#1256)

* fix(#1255): advance frontmatter status for pipe-table STATE.md on begin/complete-phase

state begin-phase/complete-phase called stateReplaceField on the FULL file content, so its case-insensitive ^Status: plain-pattern matched the YAML frontmatter status: line first (no g flag) and never updated the body pipe-table Status cell; syncStateFrontmatter then re-derived the stale status from the unchanged body, freezing the frontmatter status. Fix: strip frontmatter before the body-field replacements (operate on body only), reassemble with frontmatter preserved, so the body Status cell updates and the frontmatter derives correctly — for inline AND pipe-table body formats. Also corrects the Current Position pipe-table else-branches (Status/Phase/Last-activity) to write bare, consistent cell values. Pipe-table Status is a supported body format (not rewritten to inline).

Closes #1255

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1255): add changeset for pipe-table state status fix (#1256)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#1255): fold pipe-table regression into state.test.cjs + Windows-portable frontmatter regex

Per the 2026-06 audit, new tests/bug-NNNN-*.test.cjs files are banned (lint-regression-test-names) — folded the 7 #1255 regressions into tests/state.test.cjs and removed the standalone file + its lint-test-file-count allowlist entry. Also fixed the frontmatter assertions' /^---\n/ anchors to /^---\r?\n/ (windows-test-parity-guard frontmatterAnchorLiteralNewline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-14 22:42:28 -04:00
committed by GitHub
parent 3556450b0d
commit 2668fbbeb0
3 changed files with 406 additions and 33 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 1256
---
**`state begin-phase` / `complete-phase` now advance the frontmatter `status` for pipe-table `STATE.md`, not only inline `Status:` files.** The status update matched the YAML frontmatter `status:` line first and never updated a body `| Status | … |` cell, so the frontmatter `status` froze (e.g. stuck at `planning`); it now transitions correctly (`planning → executing → completed`) regardless of whether the body `Status` is inline or pipe-table. (#1255)

View File

@@ -1749,63 +1749,81 @@ function cmdStateBeginPhase(cwd: string, phaseNumber: string | number, phaseName
const updated: string[] = [];
readModifyWriteStateMd(statePath, (content) => {
// Bug #1255: all body-field replacements must operate on the body only
// (frontmatter stripped), not on the full content. When the full content is
// passed to stateReplaceField the YAML `status: planning` key matches the
// plain-text pattern (`^Status:\s*`) before the body pipe-table row, so the
// pipe-table `| Status | Planning |` is never updated and syncStateFrontmatter
// re-derives 'planning' from the unchanged body — the status never advances.
const existingFm = extractFrontmatter(content) as Record<string, unknown>;
const hasFrontmatter = Object.keys(existingFm).length > 0;
let body = stripFrontmatter(content);
// Helper to reassemble content for field-replacement checks; callers that
// only need to test/replace body fields use `body` directly, and the final
// return reassembles the frontmatter block with the updated body.
const reassemble = (b: string) =>
hasFrontmatter ? `---\n${reconstructFrontmatter(existingFm as unknown as Frontmatter)}\n---\n\n${b}` : b;
// Idempotency guard (#3127): if the phase is already mid-flight, do NOT
// overwrite execution-progress fields (Current Plan, plan body line,
// Last Activity Description). Only update fields that are safe to
// refresh on resume (Last Activity date, Status if inconsistent).
// A phase is considered mid-flight when Status contains 'Executing Phase N'
// for the current phase number.
const currentStatus = stateExtractField(content, 'Status') || '';
// #1255: extract from body (not full content) so the YAML `status:` key
// cannot shadow the body Status field.
const currentStatus = stateExtractField(body, 'Status') || '';
const isAlreadyExecuting = new RegExp(`Executing Phase\\s+${escapeRegex(String(phaseNumber))}\\b`, 'i').test(currentStatus);
// Update Status field
// Update Status field (body only — #1255)
const statusValue = `Executing Phase ${phaseNumber}`;
let result = stateReplaceField(content, 'Status', statusValue);
if (result) { content = result; updated.push('Status'); }
let result = stateReplaceField(body, 'Status', statusValue);
if (result) { body = result; updated.push('Status'); }
// Update Last Activity (safe to update on resume — tracks when execute-phase ran)
result = stateReplaceField(content, 'Last Activity', today);
if (result) { content = result; updated.push('Last Activity'); }
result = stateReplaceField(body, 'Last Activity', today);
if (result) { body = result; updated.push('Last Activity'); }
if (!isAlreadyExecuting) {
// First-time execution: set all progress fields
// Update Last Activity Description
const activityDesc = `Phase ${phaseNumber} execution started`;
result = stateReplaceField(content, 'Last Activity Description', activityDesc);
if (result) { content = result; updated.push('Last Activity Description'); }
result = stateReplaceField(body, 'Last Activity Description', activityDesc);
if (result) { body = result; updated.push('Last Activity Description'); }
// Update Current Phase
result = stateReplaceField(content, 'Current Phase', String(phaseNumber));
if (result) { content = result; updated.push('Current Phase'); }
result = stateReplaceField(body, 'Current Phase', String(phaseNumber));
if (result) { body = result; updated.push('Current Phase'); }
// Update Current Phase Name
if (phaseName) {
result = stateReplaceField(content, 'Current Phase Name', phaseName);
if (result) { content = result; updated.push('Current Phase Name'); }
result = stateReplaceField(body, 'Current Phase Name', phaseName);
if (result) { body = result; updated.push('Current Phase Name'); }
}
// Update Current Plan to 1 (starting from the first plan)
result = stateReplaceField(content, 'Current Plan', '1');
if (result) { content = result; updated.push('Current Plan'); }
result = stateReplaceField(body, 'Current Plan', '1');
if (result) { body = result; updated.push('Current Plan'); }
// Update Total Plans in Phase
if (planCount) {
result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
if (result) { content = result; updated.push('Total Plans in Phase'); }
result = stateReplaceField(body, 'Total Plans in Phase', String(planCount));
if (result) { body = result; updated.push('Total Plans in Phase'); }
}
// Update **Current focus:** body text line (#1104)
const focusLabel = phaseName ? `Phase ${phaseNumber} — ${phaseName}` : `Phase ${phaseNumber}`;
const focusPattern = /(\*\*Current focus:\*\*\s*).*/i;
if (focusPattern.test(content)) {
content = content.replace(focusPattern, (_match, prefix: string) => `${prefix}${focusLabel}`);
if (focusPattern.test(body)) {
body = body.replace(focusPattern, (_match, prefix: string) => `${prefix}${focusLabel}`);
updated.push('Current focus');
}
// Update ## Current Position section (#1104, #1365)
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
const positionMatch = content.match(positionPattern);
const positionMatch = body.match(positionPattern);
if (positionMatch) {
const header = positionMatch[1];
let posBody = positionMatch[2];
@@ -1830,35 +1848,55 @@ function cmdStateBeginPhase(cwd: string, phaseNumber: string | number, phaseName
const newStatus = `Status: Executing Phase ${phaseNumber}`;
if (/^Status:/m.test(posBody)) {
posBody = posBody.replace(/^Status:.*$/m, newStatus);
} else {
// Pipe-table format in Current Position (#1255)
const replaced = stateReplaceField(posBody, 'Status', `Executing Phase ${phaseNumber}`);
if (replaced !== null) posBody = replaced;
}
// Update Last activity line if present
const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
if (/^Last activity:/im.test(posBody)) {
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
} else {
// Pipe-table format in Current Position (#1255)
// Value must match the inline branch (date + narrative), not bare date.
const activityValue = `${today} -- Phase ${phaseNumber} execution started`;
const replaced = stateReplaceField(posBody, 'Last Activity', activityValue)
?? stateReplaceField(posBody, 'Last activity', activityValue);
if (replaced !== null) posBody = replaced;
}
content = content.replace(positionPattern, () => `${header}${posBody}`);
body = body.replace(positionPattern, () => `${header}${posBody}`);
updated.push('Current Position');
}
} else {
// Resume path: only update Last activity timestamp in Current Position
// (do not touch Plan:, stopped_at, progress.percent, or plan counter)
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
const positionMatch = content.match(positionPattern);
const positionMatch = body.match(positionPattern);
if (positionMatch) {
const header = positionMatch[1];
let posBody = positionMatch[2];
const resumeActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution resumed (wave continue)`;
if (/^Last activity:/im.test(posBody)) {
posBody = posBody.replace(/^Last activity:.*$/im, resumeActivity);
content = content.replace(positionPattern, () => `${header}${posBody}`);
body = body.replace(positionPattern, () => `${header}${posBody}`);
updated.push('Last activity (resume)');
} else {
// Pipe-table format in Current Position (#1255)
const replaced = stateReplaceField(posBody, 'Last Activity', resumeActivity)
?? stateReplaceField(posBody, 'Last activity', resumeActivity);
if (replaced !== null) {
posBody = replaced;
body = body.replace(positionPattern, () => `${header}${posBody}`);
updated.push('Last activity (resume)');
}
}
}
}
return content;
return reassemble(body);
}, cwd);
output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false');
@@ -2522,23 +2560,32 @@ function cmdStateCompletePhase(cwd: string, raw: boolean, overridePhase?: string
readModifyWriteStateMd(statePath, (content) => {
const currentPhase = resolvedPhase;
// Update Status field
// Bug #1255: operate on body only so the YAML frontmatter `status:` key
// cannot shadow the body Status field (pipe-table or inline).
const existingFm = extractFrontmatter(content) as Record<string, unknown>;
const hasFrontmatter = Object.keys(existingFm).length > 0;
let body = stripFrontmatter(content);
const reassemble = (b: string) =>
hasFrontmatter ? `---\n${reconstructFrontmatter(existingFm as unknown as Frontmatter)}\n---\n\n${b}` : b;
// Update Status field (body only — #1255)
const statusValue = `Phase ${currentPhase} complete`;
let result = stateReplaceField(content, 'Status', statusValue);
if (result) { content = result; updated.push('Status'); }
let result = stateReplaceField(body, 'Status', statusValue);
if (result) { body = result; updated.push('Status'); }
// Update Last Activity date
result = stateReplaceField(content, 'Last Activity', today);
if (result) { content = result; updated.push('Last Activity'); }
result = stateReplaceField(body, 'Last Activity', today);
if (result) { body = result; updated.push('Last Activity'); }
// Update Last Activity Description
const activityDesc = `Phase ${currentPhase} marked complete`;
result = stateReplaceField(content, 'Last Activity Description', activityDesc);
if (result) { content = result; updated.push('Last Activity Description'); }
result = stateReplaceField(body, 'Last Activity Description', activityDesc);
if (result) { body = result; updated.push('Last Activity Description'); }
// Update ## Current Position section
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
const positionMatch = content.match(positionPattern);
const positionMatch = body.match(positionPattern);
if (positionMatch) {
const header = positionMatch[1];
let posBody = positionMatch[2];
@@ -2547,25 +2594,41 @@ function cmdStateCompletePhase(cwd: string, raw: boolean, overridePhase?: string
const newPhase = `Phase: ${currentPhase} — COMPLETE`;
if (/^Phase:/m.test(posBody)) {
posBody = posBody.replace(/^Phase:.*$/m, newPhase);
} else {
// Pipe-table format in Current Position (#1255)
// Value cell must be bare (no "Phase:" label prefix) — the column header already provides the label.
const replaced = stateReplaceField(posBody, 'Phase', `${currentPhase} — COMPLETE`);
if (replaced !== null) posBody = replaced;
}
// Update Status line if present
const newStatus = `Status: Phase ${currentPhase} complete`;
if (/^Status:/m.test(posBody)) {
posBody = posBody.replace(/^Status:.*$/m, newStatus);
} else {
// Pipe-table format in Current Position (#1255)
const replaced = stateReplaceField(posBody, 'Status', `Phase ${currentPhase} complete`);
if (replaced !== null) posBody = replaced;
}
// Update Last activity line if present
const newActivity = `Last activity: ${today} -- Phase ${currentPhase} marked complete`;
if (/^Last activity:/im.test(posBody)) {
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
} else {
// Pipe-table format in Current Position (#1255)
// Value must match the inline branch (date + narrative), not bare date.
const activityValue = `${today} -- Phase ${currentPhase} marked complete`;
const replaced = stateReplaceField(posBody, 'Last Activity', activityValue)
?? stateReplaceField(posBody, 'Last activity', activityValue);
if (replaced !== null) posBody = replaced;
}
content = content.replace(positionPattern, () => `${header}${posBody}`);
body = body.replace(positionPattern, () => `${header}${posBody}`);
updated.push('Current Position');
}
return content;
return reassemble(body);
}, cwd);
output(

View File

@@ -9,6 +9,7 @@
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const { createFixture } = require('./fixtures/index.cjs');
@@ -3655,3 +3656,307 @@ describe('regressions: table-format STATE.md (#1162) — updateCurrentPositionFi
);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// #1255 — begin/complete-phase advance status for pipe-table STATE.md
// ─────────────────────────────────────────────────────────────────────────────
/**
* Regression tests for bug #1255.
*
* `state begin-phase` / `state complete-phase` do not advance the frontmatter
* `status` when the body `Status` field is expressed as a pipe-table row
* (`| Status | Planning |`) instead of an inline key-value pair
* (`Status: Planning`).
*
* Root cause: `stateReplaceField(content, 'Status', ...)` is called with the
* full file content (frontmatter + body). The plain-text pattern
* (`^Status:\s*(.+)` with /im flag) matches `status: planning` in the YAML
* frontmatter block rather than the body pipe-table row. The pipe-table row
* is never updated. `syncStateFrontmatter` then re-derives from the body (which
* still says 'Planning') and the #1230 delta heuristic preserves the old
* frontmatter value ('planning'), so the status never advances to 'executing'.
*
* Fix: strip frontmatter before all body-field replacements in
* `cmdStateBeginPhase` and `cmdStateCompletePhase`, then reassemble.
*
* Additional bugs fixed (#1255 follow-up):
* 1. complete-phase Phase table cell had label-duplication: `Phase: 1 — COMPLETE`
* instead of bare `1 — COMPLETE`.
* 2. begin-phase and complete-phase Last-activity table branches wrote bare date
* instead of date + narrative (inconsistent with inline branch).
*/
function make1255TempProject(stateContent) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1255-'));
const planningDir = path.join(dir, '.planning');
fs.mkdirSync(planningDir, { recursive: true });
// Minimal ROADMAP so buildStateFrontmatter can resolve phase counts
fs.writeFileSync(path.join(planningDir, 'ROADMAP.md'), [
'# ROADMAP',
'',
'## Phase 1: setup:',
'- [ ] Step 1',
'',
].join('\n'), 'utf8');
fs.writeFileSync(path.join(planningDir, 'STATE.md'), stateContent, 'utf8');
return dir;
}
// STATE.md where Status lives entirely in pipe-table rows (no inline "Status: ..." anywhere)
// This is the form a hand-edited or legacy STATE.md might use, and is a
// supported body format (do NOT silently rewrite to inline).
const TABLE_STATUS_PLANNING_1255 = `---
gsd_state_version: '1.0'
status: planning
---
# Project State
## Configuration
| Current Phase | 1 |
| Current Phase Name | setup |
| Total Plans in Phase | 3 |
| Current Plan | 1 |
| Status | Planning |
| Last Activity | 2026-06-01 |
| Last Activity Description | Roadmap created |
## Current Position
| Phase | 1 (setup) |
| Plan | 1 of 3 |
| Status | Planning |
| Last activity | 2026-06-01 |
`;
// STATE.md with Status as pipe-table but execution already in progress (complete-phase scenario)
const TABLE_STATUS_EXECUTING_1255 = `---
gsd_state_version: '1.0'
status: executing
---
# Project State
## Configuration
| Current Phase | 1 |
| Current Phase Name | setup |
| Total Plans in Phase | 3 |
| Current Plan | 3 |
| Status | Executing Phase 1 |
| Last Activity | 2026-06-01 |
| Last Activity Description | Phase 1 execution started |
## Current Position
| Phase | 1 (setup) |
| Plan | 3 of 3 |
| Status | Executing Phase 1 |
| Last activity | 2026-06-01 |
`;
describe('#1255 — begin/complete-phase advance status for pipe-table STATE.md', () => {
// begin-phase: planning → executing
test('begin-phase advances frontmatter status planning→executing when body Status is pipe-table', () => {
const dir = make1255TempProject(TABLE_STATUS_PLANNING_1255);
try {
const result = runGsdTools(
['state', 'begin-phase', '--phase', '1', '--name', 'setup', '--plans', '3'],
dir
);
assert.ok(result.success, `begin-phase failed: ${result.error || result.output}`);
const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8');
// Primary assertion: frontmatter status must advance to 'executing'
const fmMatch = after.match(/^---\r?\n([\s\S]*?)\r?\n---/);
assert.ok(fmMatch, 'STATE.md must have YAML frontmatter after begin-phase');
const fm = fmMatch[1];
assert.ok(
/^status:\s*executing\s*$/m.test(fm),
`frontmatter status must be 'executing' after begin-phase on pipe-table STATUS; got frontmatter:\n${fm}`
);
} finally {
cleanup(dir);
}
});
// begin-phase: body pipe-table row must also be updated
test('begin-phase updates body pipe-table Status cell to Executing Phase N', () => {
const dir = make1255TempProject(TABLE_STATUS_PLANNING_1255);
try {
runGsdTools(
['state', 'begin-phase', '--phase', '1', '--name', 'setup', '--plans', '3'],
dir
);
const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8');
// The pipe-table Status cell in the Configuration table must be updated
assert.ok(
/\|\s*Status\s*\|\s*Executing Phase 1\s*\|/i.test(after),
`body pipe-table Status cell must be updated to 'Executing Phase 1'; got:\n${after}`
);
} finally {
cleanup(dir);
}
});
// begin-phase: Current Position table cells — exact cell values
test('begin-phase updates Current Position pipe-table Status and Last activity cells correctly', () => {
const dir = make1255TempProject(TABLE_STATUS_PLANNING_1255);
try {
runGsdTools(
['state', 'begin-phase', '--phase', '1', '--name', 'setup', '--plans', '3'],
dir
);
const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8');
// Extract the ## Current Position section only, to avoid matching Configuration rows
const cpMatch = after.match(/##\s*Current Position\s*\n([\s\S]*?)(?=\n##|$)/i);
assert.ok(cpMatch, '## Current Position section must exist');
const cpSection = cpMatch[1];
// Status cell in Current Position: bare value, not prefixed
assert.ok(
/\|\s*Status\s*\|\s*Executing Phase 1\s*\|/i.test(cpSection),
`Current Position Status cell must be 'Executing Phase 1'; got Current Position:\n${cpSection}`
);
// Last activity cell must include date + narrative (not bare date)
assert.ok(
/\|\s*Last activity\s*\|[^|]*--\s*Phase 1 execution started\s*\|/i.test(cpSection),
`Current Position Last activity cell must include narrative '-- Phase 1 execution started'; got Current Position:\n${cpSection}`
);
} finally {
cleanup(dir);
}
});
// complete-phase: executing → completed
test('complete-phase advances frontmatter status executing→completed when body Status is pipe-table', () => {
const dir = make1255TempProject(TABLE_STATUS_EXECUTING_1255);
try {
const result = runGsdTools(
['state', 'complete-phase', '--phase', '1'],
dir
);
assert.ok(result.success, `complete-phase failed: ${result.error || result.output}`);
const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8');
// Primary assertion: frontmatter status must be 'completed'
const fmMatch = after.match(/^---\r?\n([\s\S]*?)\r?\n---/);
assert.ok(fmMatch, 'STATE.md must have YAML frontmatter after complete-phase');
const fm = fmMatch[1];
assert.ok(
/^status:\s*completed\s*$/m.test(fm),
`frontmatter status must be 'completed' after complete-phase on pipe-table STATUS; got frontmatter:\n${fm}`
);
} finally {
cleanup(dir);
}
});
// complete-phase: body pipe-table row must also be updated
test('complete-phase updates body pipe-table Status cell to Phase N complete', () => {
const dir = make1255TempProject(TABLE_STATUS_EXECUTING_1255);
try {
runGsdTools(
['state', 'complete-phase', '--phase', '1'],
dir
);
const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8');
assert.ok(
/\|\s*Status\s*\|\s*Phase 1 complete\s*\|/i.test(after),
`body pipe-table Status cell must be updated to 'Phase 1 complete'; got:\n${after}`
);
} finally {
cleanup(dir);
}
});
// complete-phase: Current Position table cells — exact cell values (catches bugs 1 and 2)
test('complete-phase updates Current Position pipe-table Phase/Status/Last-activity cells correctly', () => {
const dir = make1255TempProject(TABLE_STATUS_EXECUTING_1255);
try {
runGsdTools(
['state', 'complete-phase', '--phase', '1'],
dir
);
const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8');
// Extract the ## Current Position section only, to avoid matching Configuration rows
const cpMatch = after.match(/##\s*Current Position\s*\n([\s\S]*?)(?=\n##|$)/i);
assert.ok(cpMatch, '## Current Position section must exist');
const cpSection = cpMatch[1];
// Bug 1: Phase cell must be bare '1 — COMPLETE', NOT 'Phase: 1 — COMPLETE'
assert.ok(
/\|\s*Phase\s*\|\s*1\s*—\s*COMPLETE\s*\|/.test(cpSection),
`Current Position Phase cell must be '1 — COMPLETE' (no 'Phase:' prefix in cell value); got Current Position:\n${cpSection}`
);
assert.ok(
!/\|\s*Phase\s*\|\s*Phase:\s*1/.test(cpSection),
`Current Position Phase cell must NOT contain 'Phase: 1' (label-duplication bug); got Current Position:\n${cpSection}`
);
// Status cell in Current Position: bare value
assert.ok(
/\|\s*Status\s*\|\s*Phase 1 complete\s*\|/i.test(cpSection),
`Current Position Status cell must be 'Phase 1 complete'; got Current Position:\n${cpSection}`
);
// Bug 2: Last activity cell must include date + narrative (not bare date)
assert.ok(
/\|\s*Last activity\s*\|[^|]*--\s*Phase 1 marked complete\s*\|/i.test(cpSection),
`Current Position Last activity cell must include narrative '-- Phase 1 marked complete'; got Current Position:\n${cpSection}`
);
} finally {
cleanup(dir);
}
});
// Regression guard: inline Status format must still work (existing behavior unchanged)
test('begin-phase still works correctly with inline Status: format (regression guard)', () => {
const inlineStateMd = `---
gsd_state_version: '1.0'
status: planning
---
# Project State
Current Phase: 1
Current Phase Name: setup
Total Plans in Phase: 3
Current Plan: 1
Status: Planning
Last Activity: 2026-06-01
Last Activity Description: Roadmap created
## Current Position
Phase: 1 (setup)
Plan: 1 of 3
Status: Planning
Last activity: 2026-06-01 -- Roadmap created
`;
const dir = make1255TempProject(inlineStateMd);
try {
const result = runGsdTools(
['state', 'begin-phase', '--phase', '1', '--name', 'setup', '--plans', '3'],
dir
);
assert.ok(result.success, `begin-phase failed on inline format: ${result.error || result.output}`);
const after = fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf8');
const fmMatch = after.match(/^---\r?\n([\s\S]*?)\r?\n---/);
assert.ok(fmMatch, 'must have frontmatter');
const fm = fmMatch[1];
assert.ok(
/^status:\s*executing\s*$/m.test(fm),
`inline Status: format: frontmatter status must be 'executing'; got:\n${fm}`
);
} finally {
cleanup(dir);
}
});
});