From 2bcfaa2e27447e1f59c479231f5a4f9b07a4020c Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 23 Jul 2026 07:38:21 -0400 Subject: [PATCH] fix(#2400): warn on planned-phase no-op + sync progress.total_plans (#2552) * fix(#2400): warn on planned-phase no-op + sync progress.total_plans Bug A: When STATE.md Current Position has no recognized labels (narrative prose), emit a warning field so the workflow detects the no-op instead of continuing with stale state. Bug B: Sync progress.total_plans in the YAML frontmatter when a plan count is provided, preventing contradictory state between frontmatter (0) and body (actual count). This writes the explicitly-provided count, not a re-derivation from disk (#500 safe). Closes #2400 * docs(#2400): backfill changeset PR number (2552) --- .changeset/2400-planned-phase-empty-transition.md | 5 +++++ src/state-transition.cts | 13 +++++++++++++ src/state.cts | 5 ++++- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .changeset/2400-planned-phase-empty-transition.md diff --git a/.changeset/2400-planned-phase-empty-transition.md b/.changeset/2400-planned-phase-empty-transition.md new file mode 100644 index 000000000..a17e85740 --- /dev/null +++ b/.changeset/2400-planned-phase-empty-transition.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2552 +--- +**`state.planned-phase` now warns on no-op transitions and syncs `progress.total_plans`** — when STATE.md's Current Position has no recognized labels (narrative prose), the command emits a `warning` field so the workflow can detect the no-op instead of continuing with stale state. When a plan count is provided, `progress.total_plans` in the YAML frontmatter is updated alongside the body `Total Plans in Phase` field, preventing contradictory state between the two representations. Previously, the command silently returned success with an empty `updated` array and zero bytes written, and left `progress.total_plans` at 0 while the body reported the actual count. (#2400) diff --git a/src/state-transition.cts b/src/state-transition.cts index 8ee445a60..f45f04274 100644 --- a/src/state-transition.cts +++ b/src/state-transition.cts @@ -1117,6 +1117,19 @@ function plannedPhaseCore( ); if (body !== beforePos) updated.push('Current Position'); + // #2400 Bug B: sync progress.total_plans to the frontmatter when a plan count + // is given. This writes the explicitly-provided count — it is NOT a re-derivation + // from disk (#500 RC1 is about deriving from a half-planned snapshot, not about + // refusing to write an explicitly-passed argument). + if (intent.planCount !== null && intent.planCount !== undefined && hasFrontmatter) { + const fmProgress = (existingFm['progress'] as Record | undefined) || {}; + if (fmProgress['total_plans'] !== intent.planCount) { + fmProgress['total_plans'] = intent.planCount; + existingFm['progress'] = fmProgress; + updated.push('progress.total_plans'); + } + } + return { content: reassemble(body), updated }; } diff --git a/src/state.cts b/src/state.cts index 720d72033..bafb9e1f3 100644 --- a/src/state.cts +++ b/src/state.cts @@ -2529,7 +2529,10 @@ function cmdStatePlannedPhase(cwd: string, phaseNumber: string | number, planCou return result.content; }, cwd, { resync: false, deriveProgressKeys: true }); - output({ updated, phase: phaseNumber, plan_count: planCount }, raw, updated.length > 0 ? 'true' : 'false'); + const result = updated.length === 0 + ? { updated, phase: phaseNumber, plan_count: planCount, warning: 'STATE.md Current Position has no recognized labels — transition was a no-op. Verify STATE.md uses the canonical labeled format (Status:, Total Plans in Phase:, etc.).' } + : { updated, phase: phaseNumber, plan_count: planCount }; + output(result, raw, updated.length > 0 ? 'true' : 'false'); } /**