fix(#3374): phase.complete stops harvesting stale body stopped_at (#3491)

* 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:
Tom Boucher
2026-08-14 12:38:23 -04:00
committed by GitHub
parent 27cad971d0
commit be9329b10b
7 changed files with 551 additions and 134 deletions

View File

@@ -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 });
}