From f850952332dde0c47ecebea0adb97ede4365f639 Mon Sep 17 00:00:00 2001 From: j2h4u <39818683+j2h4u@users.noreply.github.com> Date: Sat, 21 Mar 2026 00:33:13 +0500 Subject: [PATCH] fix(state): preserve frontmatter status when body Status field is missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit syncStateFrontmatter() rebuilds YAML frontmatter from the body on every writeStateMd() call. If an agent removes or omits the Status: field from the body, buildStateFrontmatter() defaults to 'unknown', overwriting a previously valid status (e.g., 'executing'). Fix: read existing frontmatter before stripping, and preserve its status value when the body-derived status would be 'unknown'. This makes frontmatter self-healing — once a status is set, it persists even if the body loses the field. Co-Authored-By: Claude Opus 4.6 (1M context) --- get-shit-done/bin/lib/state.cjs | 15 +++++++++++++-- tests/state.test.cjs | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index dd3845c3b..d93f2e2ac 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -733,9 +733,20 @@ function stripFrontmatter(content) { } function syncStateFrontmatter(content, cwd) { + // Read existing frontmatter BEFORE stripping — it may contain values + // that the body no longer has (e.g., Status field removed by an agent). + const existingFm = extractFrontmatter(content); const body = stripFrontmatter(content); - const fm = buildStateFrontmatter(body, cwd); - const yamlStr = reconstructFrontmatter(fm); + const derivedFm = buildStateFrontmatter(body, cwd); + + // Preserve existing frontmatter status when body-derived status is 'unknown'. + // This prevents a missing Status: field in the body from overwriting a + // previously valid status (e.g., 'executing' → 'unknown'). + if (derivedFm.status === 'unknown' && existingFm.status && existingFm.status !== 'unknown') { + derivedFm.status = existingFm.status; + } + + const yamlStr = reconstructFrontmatter(derivedFm); return `---\n${yamlStr}\n---\n\n${body}`; } diff --git a/tests/state.test.cjs b/tests/state.test.cjs index 7f86f25fc..82023c347 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -475,6 +475,30 @@ describe('STATE.md frontmatter sync', () => { assert.ok(content.includes('status: paused'), 'frontmatter should reflect latest status'); }); + test('preserves frontmatter status when body Status field is missing', () => { + // Simulate: frontmatter has status: executing, but body lost Status: field + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `--- +status: executing +milestone: v1.0 +--- + +# Project State + +**Current Phase:** 03 +**Current Plan:** 03-02 +` + ); + + // Any writeStateMd triggers syncStateFrontmatter — use state update on a field that exists + runGsdTools('state update "Current Plan" "03-03"', tmpDir); + + const content = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(content.includes('status: executing'), 'should preserve existing status, not overwrite with unknown'); + assert.ok(!content.includes('status: unknown'), 'should not contain unknown status'); + }); + test('round-trip: write then read via state json', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'STATE.md'),