* fix(#500): stop state planned-phase corrupting milestone progress.* counters Two independent defects combined to corrupt STATE.md progress.* on a plan-phase run: RC1 — cmdStatePlannedPhase wrote via writeStateMd, which unconditionally runs syncStateFrontmatter and rebuilds progress.* (total/completed plans+phases) from a half-planned disk snapshot, trampling curated counters. It now routes through readModifyWriteStateMd(..., { resync:false }), the same body-only-write guard state.update uses — per-phase body fields are updated, milestone progress.* is preserved. RC2 — isRootPlanFile's loose /PLAN/i fallback matched legacy `<N>-PLAN-<NN>-SUMMARY.md` names (they contain "PLAN"), double-counting summaries as plans (a 4-plan/4-summary phase scanned as planCount:8, completed:false). isRootPlanFile now rejects isRootSummaryFile before the fallback, so summaries are never counted as plans. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#500): add changeset for planned-phase progress fix 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:
@@ -24,6 +24,11 @@ function isRootPlanFile(fileName) {
|
||||
return false;
|
||||
if (fileName.endsWith('-PLAN.md') || fileName === 'PLAN.md')
|
||||
return true;
|
||||
// A summary is never a plan. Reject summaries before the loose /PLAN/i
|
||||
// fallback so legacy `<N>-PLAN-<NN>-SUMMARY.md` names (which contain the
|
||||
// substring "PLAN") are not double-counted as plans. (#500 RC2)
|
||||
if (isRootSummaryFile(fileName))
|
||||
return false;
|
||||
return /\.md$/i.test(fileName) && /PLAN/i.test(fileName);
|
||||
}
|
||||
|
||||
|
||||
@@ -1392,45 +1392,48 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(statePath, 'utf-8');
|
||||
const today = realClock.today();
|
||||
const updated = [];
|
||||
|
||||
const statusDefaults = KNOWN_TEMPLATE_DEFAULTS['Status'];
|
||||
const lastActivityDefaults = KNOWN_TEMPLATE_DEFAULTS['Last Activity'];
|
||||
|
||||
// Update Status — only when the existing value is a known template default
|
||||
// (Knuth invariant: preserve executor-authored values).
|
||||
const newContent = stateReplaceFieldIfTemplate(content, 'Status', statusDefaults, 'Ready to execute');
|
||||
if (newContent !== content) { content = newContent; updated.push('Status'); }
|
||||
// plan-phase updates per-phase body fields only. It must NOT resync the
|
||||
// milestone-wide progress.* frontmatter from a half-planned disk snapshot —
|
||||
// doing so tramples curated/known-good counters. Route through the body-only
|
||||
// write contract (resync:false), the same guard state.update uses. (#500 RC1)
|
||||
readModifyWriteStateMd(statePath, (content) => {
|
||||
// Update Status — only when the existing value is a known template default
|
||||
// (Knuth invariant: preserve executor-authored values).
|
||||
const newContent = stateReplaceFieldIfTemplate(content, 'Status', statusDefaults, 'Ready to execute');
|
||||
if (newContent !== content) { content = newContent; updated.push('Status'); }
|
||||
|
||||
// Update Total Plans in Phase
|
||||
if (planCount !== null && planCount !== undefined) {
|
||||
const result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
|
||||
if (result) { content = result; updated.push('Total Plans in Phase'); }
|
||||
}
|
||||
// Update Total Plans in Phase
|
||||
if (planCount !== null && planCount !== undefined) {
|
||||
const result = stateReplaceField(content, 'Total Plans in Phase', String(planCount));
|
||||
if (result) { content = result; updated.push('Total Plans in Phase'); }
|
||||
}
|
||||
|
||||
// Update Last Activity — only when the existing value is a known template default
|
||||
{
|
||||
const after = stateReplaceFieldIfTemplate(content, 'Last Activity', lastActivityDefaults, today);
|
||||
if (after !== content) { content = after; updated.push('Last Activity'); }
|
||||
}
|
||||
// Update Last Activity — only when the existing value is a known template default
|
||||
{
|
||||
const after = stateReplaceFieldIfTemplate(content, 'Last Activity', lastActivityDefaults, today);
|
||||
if (after !== content) { content = after; updated.push('Last Activity'); }
|
||||
}
|
||||
|
||||
// Update Last Activity Description
|
||||
{
|
||||
const result = stateReplaceField(content, 'Last Activity Description', `Phase ${phaseNumber} planning complete — ${planCount || '?'} plans ready`);
|
||||
if (result) { content = result; updated.push('Last Activity Description'); }
|
||||
}
|
||||
// Update Last Activity Description
|
||||
{
|
||||
const result = stateReplaceField(content, 'Last Activity Description', `Phase ${phaseNumber} planning complete — ${planCount || '?'} plans ready`);
|
||||
if (result) { content = result; updated.push('Last Activity Description'); }
|
||||
}
|
||||
|
||||
// Update Current Position section
|
||||
content = updateCurrentPositionFields(content, {
|
||||
status: 'Ready to execute',
|
||||
lastActivity: `${today} -- Phase ${phaseNumber} planning complete`,
|
||||
});
|
||||
// Update Current Position section
|
||||
content = updateCurrentPositionFields(content, {
|
||||
status: 'Ready to execute',
|
||||
lastActivity: `${today} -- Phase ${phaseNumber} planning complete`,
|
||||
});
|
||||
|
||||
if (updated.length > 0) {
|
||||
writeStateMd(statePath, content, cwd);
|
||||
}
|
||||
return content;
|
||||
}, cwd, { resync: false });
|
||||
|
||||
output({ updated, phase: phaseNumber, plan_count: planCount }, raw, updated.length > 0 ? 'true' : 'false');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user