* fix(#3395): own the phase line in planned-phase and persist --name * fix(#3395): backfill changeset pr 3490 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -46,7 +46,7 @@ interface StateModule {
|
||||
cmdStateBeginPhase(cwd: string, phase: string | null | undefined, name: string | null | undefined, plans: number | null, raw: boolean): void;
|
||||
cmdSignalWaiting(cwd: string, type: string | null | undefined, question: string | null | undefined, options: string | null | undefined, phase: string | null | undefined, raw: boolean): void;
|
||||
cmdSignalResume(cwd: string, raw: boolean): void;
|
||||
cmdStatePlannedPhase(cwd: string, phase: string | null | undefined, plans: number | null, raw: boolean): void;
|
||||
cmdStatePlannedPhase(cwd: string, phase: string | null | undefined, name: string | null | undefined, plans: number | null, raw: boolean): void;
|
||||
cmdStateValidate(cwd: string, raw: boolean): void;
|
||||
cmdStateSync(cwd: string, opts: { verify: string | boolean | null | undefined }, raw: boolean): void;
|
||||
cmdStatePrune(cwd: string, opts: { keepRecent: string; dryRun: boolean }, raw: boolean): void;
|
||||
@@ -180,7 +180,11 @@ function routeStateCommand({ state, args, cwd, raw, error }: RouteStateCommandOp
|
||||
'signal-resume': () => state.cmdSignalResume(cwd, raw),
|
||||
'planned-phase': () => {
|
||||
const a = parseNamedArgs(args, ['phase', 'name', 'plans']);
|
||||
state.cmdStatePlannedPhase(cwd, strArg(a, 'phase'), parsePlans(strArg(a, 'plans')), raw);
|
||||
// #3395: --name was parsed here but never forwarded (the StateModule
|
||||
// signature had no channel for it), so the argument was silently
|
||||
// dropped. It now persists into the Current Position `Phase:` line and
|
||||
// the authoritative current_phase_name, mirroring begin-phase.
|
||||
state.cmdStatePlannedPhase(cwd, strArg(a, 'phase'), strArg(a, 'name'), parsePlans(strArg(a, 'plans')), raw);
|
||||
},
|
||||
validate: () => state.cmdStateValidate(cwd, raw),
|
||||
sync: () => {
|
||||
|
||||
@@ -462,7 +462,7 @@ export type StateTransitionIntent =
|
||||
planCount: number;
|
||||
summaryCount: number;
|
||||
}
|
||||
| { kind: 'plannedPhase'; phaseNumber: string | number; planCount: number | null }
|
||||
| { kind: 'plannedPhase'; phaseNumber: string | number; phaseName: string | null; planCount: number | null }
|
||||
| { kind: 'milestoneSwitch'; version: string; name: string }
|
||||
| {
|
||||
kind: 'milestoneComplete';
|
||||
@@ -845,7 +845,7 @@ function mutateCurrentPositionResume(
|
||||
*/
|
||||
function mutateCurrentPositionForAdvance(
|
||||
content: string,
|
||||
fields: { status?: string; lastActivity?: string; plan?: string },
|
||||
fields: { phase?: string; status?: string; lastActivity?: string; plan?: string },
|
||||
statusDefaults: string[] | null | undefined,
|
||||
lastActivityDefaults: string[] | null | undefined,
|
||||
): string {
|
||||
@@ -854,6 +854,22 @@ function mutateCurrentPositionForAdvance(
|
||||
let sectionBody = content.slice(span.start, span.end);
|
||||
let mutated = false;
|
||||
|
||||
// #3395: Phase is always replaced when a caller passes it — system-derived,
|
||||
// not executor-authored (same rule as Plan below). plannedPhaseCore uses
|
||||
// this so the transition that declares phase N planned also owns the `Phase:`
|
||||
// line the frontmatter resync and `state json` re-derive current_phase from;
|
||||
// before, the line survived stale from a previous phase and every
|
||||
// body-derived consumer kept reading it (#948 class).
|
||||
if (fields.phase) {
|
||||
if (/^Phase:/m.test(sectionBody)) {
|
||||
sectionBody = sectionBody.replace(/^Phase:.*$/m, `Phase: ${fields.phase}`);
|
||||
mutated = true;
|
||||
} else {
|
||||
const replaced = stateReplaceField(sectionBody, 'Phase', fields.phase);
|
||||
if (replaced !== null) { sectionBody = replaced; mutated = true; }
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.status) {
|
||||
const replaced = stateReplaceFieldIfTemplate(sectionBody, 'Status', statusDefaults, fields.status);
|
||||
if (replaced !== null && replaced !== sectionBody) { sectionBody = replaced; mutated = true; }
|
||||
@@ -1198,7 +1214,10 @@ function completePhaseCore(
|
||||
* per-phase body fields after plan-phase runs: Status (template-aware — only
|
||||
* replaces handler-generated values, preserving executor-authored ones),
|
||||
* Total Plans in Phase, Last Activity (template-aware), Last Activity
|
||||
* Description, and the ## Current Position section. The adapter wraps this in
|
||||
* Description, and the ## Current Position section — including its `Phase:`
|
||||
* line, which this transition owns (#3395: the line is the body source
|
||||
* `current_phase` re-derives from, so it must not survive stale from a
|
||||
* previous phase). The adapter wraps this in
|
||||
* `readModifyWriteStateMd({ resync: false })` so the milestone-wide progress.*
|
||||
* frontmatter is NOT re-derived from a half-planned disk snapshot (#500 RC1).
|
||||
*
|
||||
@@ -1208,7 +1227,7 @@ function completePhaseCore(
|
||||
*/
|
||||
function plannedPhaseCore(
|
||||
content: string,
|
||||
intent: { kind: 'plannedPhase'; phaseNumber: string | number; planCount: number | null },
|
||||
intent: { kind: 'plannedPhase'; phaseNumber: string | number; phaseName: string | null; planCount: number | null },
|
||||
deps: StateTransitionDeps,
|
||||
): StateTransitionResult {
|
||||
const updated: string[] = [];
|
||||
@@ -1270,11 +1289,22 @@ function plannedPhaseCore(
|
||||
updated.push('Last Activity Description');
|
||||
}
|
||||
|
||||
// ## Current Position section — Status + Last activity (template-aware).
|
||||
// ## Current Position section — Phase + Status + Last activity.
|
||||
// #3395: plannedPhaseCore owns the `Phase:` line for the same reason
|
||||
// beginPhaseCore/completePhaseCore do — it is the body source the frontmatter
|
||||
// resync and `state json` re-derive `current_phase` from. Before, a stale
|
||||
// line from a previous phase survived this transition and every
|
||||
// body-derived consumer kept reading it (the write path was already
|
||||
// protected by the #3258 preserve-when-unchanged row; the source itself was
|
||||
// never refreshed). The label mirrors beginPhaseCore's `N (Name) — EXECUTING`
|
||||
// convention with this transition's status vocabulary ("Ready to execute").
|
||||
// Phase is system-derived, always replaced (Knuth invariant does not apply);
|
||||
// Status / Last activity stay template-aware.
|
||||
const beforePos = body;
|
||||
body = mutateCurrentPositionForAdvance(
|
||||
body,
|
||||
{
|
||||
phase: `${intent.phaseNumber}${intent.phaseName ? ` (${intent.phaseName})` : ''} — READY TO EXECUTE`,
|
||||
status: 'Ready to execute',
|
||||
lastActivity: `${today} — Phase ${intent.phaseNumber} planning complete`,
|
||||
},
|
||||
|
||||
@@ -3268,7 +3268,7 @@ function updatePerformanceMetricsSection(content: string, cwd: string, phaseNum:
|
||||
* Gate 3a: Record state after plan-phase completes.
|
||||
* Updates Status to "Ready to execute", Total Plans, Last Activity.
|
||||
*/
|
||||
function cmdStatePlannedPhase(cwd: string, phaseNumber: string | number, planCount: number | null | undefined, raw: boolean): void {
|
||||
function cmdStatePlannedPhase(cwd: string, phaseNumber: string | number, phaseName: string | null | undefined, planCount: number | null | undefined, raw: boolean): void {
|
||||
const statePath = planningPaths(cwd).state;
|
||||
if (!fs.existsSync(statePath)) {
|
||||
output({ error: 'STATE.md not found' }, raw, undefined);
|
||||
@@ -3286,6 +3286,7 @@ function cmdStatePlannedPhase(cwd: string, phaseNumber: string | number, planCou
|
||||
const intent: StateTransitionIntent = {
|
||||
kind: 'plannedPhase',
|
||||
phaseNumber,
|
||||
phaseName: phaseName ?? null,
|
||||
planCount: planCount ?? null,
|
||||
};
|
||||
const deps: StateTransitionDeps = {
|
||||
@@ -3293,12 +3294,23 @@ function cmdStatePlannedPhase(cwd: string, phaseNumber: string | number, planCou
|
||||
sourcePath: statePath,
|
||||
};
|
||||
|
||||
// #3395 / #2736: the transition holds the exact display name. plannedPhaseCore
|
||||
// writes it into the Current Position `Phase: N (Name) — READY TO EXECUTE`
|
||||
// line, and the prose re-derivation of current_phase_name truncates names
|
||||
// that themselves contain a parenthetical — the authoritative override keeps
|
||||
// the exact value, exactly as cmdStateBeginPhase does for its EXECUTING line.
|
||||
const rmwOptions: ReadModifyWriteOptions = {
|
||||
resync: false,
|
||||
deriveProgressKeys: true,
|
||||
authoritativeFm: intent.phaseName ? { current_phase_name: intent.phaseName } : undefined,
|
||||
};
|
||||
|
||||
let updated: string[] = [];
|
||||
readModifyWriteStateMd(statePath, (content) => {
|
||||
const result = transitionCore(content, intent, deps);
|
||||
updated = result.updated;
|
||||
return result.content;
|
||||
}, cwd, { resync: false, deriveProgressKeys: true });
|
||||
}, cwd, rmwOptions);
|
||||
|
||||
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.).' }
|
||||
|
||||
Reference in New Issue
Block a user