* fix(#3374): phase.complete stops harvesting stale body stopped_at Variant A: cmdPhaseComplete's adapter calls syncStateFrontmatter directly (deliberately - STATE.md commits atomically with ROADMAP/REQUIREMENTS), which also bypassed the #948/#1230 preservation pass every RMW write gets. A stale body 'Stopped at:' line then silently clobbered a fresher frontmatter stopped_at on every phase completion, with warnings: []. Three layers close it without reversing #3517's refresh expectation: - completePhaseCore now refreshes the body continuity line it implies ('Phase N complete, ready to plan Phase N+1'; ADR-2207 phrasing on the last phase), session-scoped via the new stateReplaceFieldInSession seam so a decoy bold Stopped-at line in an unrelated section cannot absorb the refresh. Replace-only - a layout with no session line keeps its shape and its frontmatter value survives via the preservation delta. - the RMW post-sync preservation chunk (snapshots + table-driven applyStatePreservation + #2736 re-assert, full bodyDeltas wired) is extracted into the shared applyPostSyncPreservation helper; the phase.complete adapter and writeStateMd (milestone complete / state sync - the gap the closed PR #3442 review flagged) now run it too. - cmdStateRecordSession pushed 'Stopped At' onto updated[] on any label MATCH, including a value already on disk - reporting a write that never changed a byte. It now reports only on real change, and the match is tracked separately so an identical value does not arm the #944 DWIM section rewrite (which would reset an executor-authored resume file to None). * docs(#3374): backfill changeset pr field to 3491 * fix(#3374): drop the writeStateMd preservation pass - state sync's #905 contract is body-wins CI on this PR caught what the closed PR #3442 review's MAJOR remediation option (a) would have broken: state sync's #905 contract ('body annotation beats existing frontmatter when both are present') is the opposite by design - sync exists to re-derive frontmatter from the body. A blanket applyStatePreservation pass on writeStateMd re-locked stale frontmatter (current_phase 3 over the body's 5) on every sync. Take the review's sanctioned option (b) instead: the scope claim is accurate (phase.complete only) and the milestone complete / state sync exposure is tracked as follow-up issue #3492. --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import frontmatter = require('./frontmatter.cjs');
|
||||
import { stateReplaceField, stateExtractField, stateReplaceFieldIfTemplate, stateReplaceFieldWithFallback } from './state-document.cjs';
|
||||
import { stateReplaceField, stateExtractField, stateReplaceFieldIfTemplate, stateReplaceFieldWithFallback, stateReplaceFieldInSession } from './state-document.cjs';
|
||||
import { KNOWN_TEMPLATE_DEFAULTS } from './state-document.cjs';
|
||||
import { tokenizeHeadings } from './markdown-sectionizer.cjs';
|
||||
import type { HeadingToken } from './markdown-sectionizer.cjs';
|
||||
@@ -1057,6 +1057,7 @@ function completePhaseCore(
|
||||
'current_plan',
|
||||
'last_activity',
|
||||
'last_activity_desc',
|
||||
'stopped_at',
|
||||
'progress',
|
||||
]) {
|
||||
const cls = getFieldClassification(fmKey);
|
||||
@@ -1157,6 +1158,29 @@ function completePhaseCore(
|
||||
updated.push('Last Activity Description');
|
||||
}
|
||||
|
||||
// Stopped At — #3374: write the continuity line this transition implies.
|
||||
// The frontmatter `stopped_at` is a projection of this body line
|
||||
// (source: 'body' in FIELD_CLASSIFICATION), and phase completion is exactly
|
||||
// the event the line describes — leaving it stale made the post-sync harvest
|
||||
// overwrite a fresher frontmatter value with pre-completion prose on every
|
||||
// completion (#3374), and left the workflow's later prose refresh as a
|
||||
// divergence source. Session-SCOPED replace (stateReplaceFieldInSession):
|
||||
// the harvest reads only the session section, so the write must target the
|
||||
// same scope — a whole-body replace let a decoy `**Stopped at:**` line in an
|
||||
// unrelated section absorb the refresh. Replace-only (no insertion): a
|
||||
// STATE.md with no session continuity line keeps its shape, and the
|
||||
// unchanged body source then lets the preservation delta keep an existing
|
||||
// frontmatter value. Last-phase wording reuses the ADR-2207 status phrase;
|
||||
// milestone termination wording stays owned by milestoneCompleteCore.
|
||||
const stoppedAtLine = intent.isLastPhase
|
||||
? `Phase ${intent.phaseNum} complete — all phases complete`
|
||||
: `Phase ${intent.phaseNum} complete${intent.nextPhaseNum ? `, ready to plan Phase ${intent.nextPhaseNum}` : ''}`;
|
||||
const stoppedAfter = stateReplaceFieldInSession(body, 'Stopped At', 'Stopped at', stoppedAtLine);
|
||||
if (stoppedAfter !== body) {
|
||||
body = stoppedAfter;
|
||||
updated.push('Stopped At');
|
||||
}
|
||||
|
||||
// Progress block — re-derive completed/total phases from the roadmap when
|
||||
// available (milestone-wide source of truth), then recompute the percent.
|
||||
// Only runs when a Completed Phases field exists (the existing guard).
|
||||
|
||||
Reference in New Issue
Block a user