* 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:
@@ -91,6 +91,7 @@ const {
|
||||
stateExtractField,
|
||||
stateReplaceField,
|
||||
syncStateFrontmatter,
|
||||
applyPostSyncPreservation,
|
||||
withStateLock,
|
||||
updatePerformanceMetricsSection,
|
||||
} = stateMod;
|
||||
@@ -2964,7 +2965,9 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
// this adapter: they are section-table / disk-scan concerns, not
|
||||
// classified fields, and `syncStateFrontmatter` is the post-sync this
|
||||
// transaction needs (it does NOT go through readModifyWriteStateMd
|
||||
// because STATE.md is committed atomically with ROADMAP/REQUIREMENTS).
|
||||
// because STATE.md is committed atomically with ROADMAP/REQUIREMENTS —
|
||||
// the post-sync preservation pass runs via applyPostSyncPreservation
|
||||
// instead, #3374).
|
||||
const nextPhaseDisplayName =
|
||||
phaseDisplayNameFromRoadmap(roadmapContent, nextPhaseNum) ??
|
||||
phaseDisplayNameFromSlug(nextPhaseName);
|
||||
@@ -3018,7 +3021,30 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
current_phase_name: nextPhaseDisplayName,
|
||||
}
|
||||
: undefined;
|
||||
stateContent = syncStateFrontmatter(stateContent, cwd, authoritativeFm);
|
||||
const synced = syncStateFrontmatter(stateContent, cwd, authoritativeFm);
|
||||
// #3374: the direct sync above deliberately bypasses
|
||||
// readModifyWriteStateMd (STATE.md is committed atomically with
|
||||
// ROADMAP/REQUIREMENTS), which also bypassed the #948/#1230
|
||||
// preservation pass every RMW write gets — so a stale body
|
||||
// `Stopped at:` line silently clobbered a fresher frontmatter
|
||||
// stopped_at on every completion. Run the shared post-sync pass:
|
||||
// snapshots from the on-disk pre-image (originalStateContent) and the
|
||||
// transformed content, table-driven applyStatePreservation, then the
|
||||
// #2736 authoritative re-assert (which restores the #3350 pairing
|
||||
// override the preserve-always restore may have reverted). resync=true
|
||||
// is the lifecycle-transition posture (progress recomputed from disk;
|
||||
// only the preserve-when-unchanged deltas apply). Fields the
|
||||
// transition legitimately rewrote (Status, Phase, Stopped At via
|
||||
// completePhaseCore's #3374 continuity line) have changed body
|
||||
// sources, so their deltas do not fire.
|
||||
stateContent = applyPostSyncPreservation(
|
||||
originalStateContent,
|
||||
stateContent,
|
||||
synced,
|
||||
statePath,
|
||||
true,
|
||||
authoritativeFm,
|
||||
);
|
||||
|
||||
writes.push({ filePath: statePath, before: originalStateContent, after: stateContent });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user