* fix(#4186): anchored status vocabulary, record-session arg guard, recount pin Three defects from #4186: 1. normalizeStateStatus ran a first-match-wins SUBSTRING chain over the free-prose body Status field, so prose merely mentioning a status word was silently rewritten to a credible wrong token (a .planning/ path in Italian prose -> status: planning; verifica -> verifying; completezza -> completed). Recognition is now an ANCHORED whole-field match against a declared vocabulary (STATUS_EXACT_TOKENS + STATUS_ANCHORED_PATTERNS, state-document.cts) — case/whitespace-tolerant, branch-order artifacts preserved (Planning complete -> planning; Phase complete — ready for verification -> verifying). The recorded lenient fallback (#3873 row 26) stands: unrecognized prose passes through verbatim. Read-side consumers (W011, statusline) ride the same function. 2. The progress recount skew (stray *-SUMMARY.md inflating completed_plans) is already dead on next via #1988/PR #2016 (countMatchedSummaries pairs summaries to plans) — verified live and pinned with regression rows composed against the #4129/#4359 ratchet. 3. state record-session with no args executed and wrote STATE.md; it now errors like state update (stopped-at or resume-file required), handler- side so SDK callers are covered too. Four tests pinning the bare-call write are updated to the new contract. * fix(#4186): update status pins to the anchored vocabulary contract Bench round 1 follow-ups: - Legacy bare 'Milestone complete' kept as reader-side vocabulary (ADR-2207 removed the writers, not recognition of legacy files). - state.test pins updated: 'Paused at Plan 3' and round-trip 'Executing Plan 5' were pins of the substring guessing itself — the round-trip now uses the real handler form 'Executing Phase 5'. - record-session no-op/no-fields tests repurposed to the usage-error contract (CLI + SDK-level ExitError), byte-unchanged assertions kept. - statusline tests repinned: vocabulary values collapse to keywords; narratives render the documented first-word fallback instead of a guessed token. Hook doc comment updated to match. - docs-guard exempt baseline: state.test.cjs now cites docs/CLI-TOOLS.md. - docs/CLI-TOOLS.md: record-session signature notes the required flag. * fix(#4186): repair a dangling sentence in the schema docstring * test(#4186): bound the completed_plans scan regex (#2128 class) * chore(#4186): backfill changeset PR number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -1921,6 +1921,18 @@ function cmdStateResolveBlocker(cwd: string, text: string, raw: boolean): void {
|
||||
}
|
||||
|
||||
function cmdStateRecordSession(cwd: string, options: StateRecordSessionOptions, raw: boolean): void {
|
||||
// #4186: a bare invocation is a usage error, not a heartbeat write. The
|
||||
// pre-#4186 handler accepted zero arguments and still refreshed
|
||||
// `Last session` / `Last Date` / `last_updated` — a caller probing the
|
||||
// command's signature (the way other subcommands encourage) silently
|
||||
// mutated STATE.md. Mirrors `state update`'s required-arg guard
|
||||
// (cmdStateUpdate: `error('field and value required for state update')`),
|
||||
// including its ordering: validation precedes the STATE.md existence
|
||||
// check. Either flag suffices — `--resume-file` alone carries an explicit
|
||||
// value the handler must persist.
|
||||
if (!options.stopped_at && (options.resume_file === undefined || options.resume_file === null)) {
|
||||
error('stopped-at or resume-file required for state record-session');
|
||||
}
|
||||
const statePath = planningPaths(cwd).state;
|
||||
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw, undefined); return; }
|
||||
|
||||
@@ -3147,19 +3159,19 @@ function buildStateFrontmatter(
|
||||
}
|
||||
|
||||
let normalizedStatus = normalizeStateStatus(status, pausedAt);
|
||||
// #3578: normalizeStateStatus matches 'complete' as a case-insensitive
|
||||
// SUBSTRING, so the phase-completion prose cmdStateCompletePhase writes to
|
||||
// the body (`Phase ${N} complete`) collapses to the milestone-level
|
||||
// 'completed' status even when other phases remain open. Phase-level
|
||||
// prose must never decide milestone-level status — completedPhases /
|
||||
// totalPhases / diskScope, already derived above from a disk scan, are
|
||||
// the authority on whether the MILESTONE is actually done. Only override
|
||||
// when: (a) normalizeStateStatus actually landed on 'completed'; (b) the
|
||||
// raw prose is UNAMBIGUOUSLY phase-completion prose — the anchored
|
||||
// pattern below deliberately excludes "All phases complete" (no `\S+`
|
||||
// phase token) and milestone-close prose like "v1.0 milestone complete"
|
||||
// (no leading "phase"); and (c) the counters are trustworthy (a COMPLETE
|
||||
// disk scope, both counts are finite numbers, and a positive
|
||||
// #3578: the declared status vocabulary (#4186) recognizes
|
||||
// `Phase ${N} complete` (state.cts's own phase-completion write) and maps
|
||||
// it to `completed`, so the phase-completion prose still collapses to the
|
||||
// milestone-level status even when other phases remain open — this guard
|
||||
// demotes it back. Phase-level prose must never decide milestone-level
|
||||
// status — completedPhases / totalPhases / diskScope, already derived above
|
||||
// from a disk scan, are the authority on whether the MILESTONE is actually
|
||||
// done. Only override when: (a) normalizeStateStatus actually landed on
|
||||
// 'completed'; (b) the raw prose is UNAMBIGUOUSLY phase-completion prose —
|
||||
// the anchored pattern below deliberately excludes "All phases complete"
|
||||
// (no `\S+` phase token) and milestone-close prose like "v1.0 milestone
|
||||
// complete" (no leading "phase"); and (c) the counters are trustworthy (a
|
||||
// COMPLETE disk scope, both counts are finite numbers, and a positive
|
||||
// denominator) and affirmatively disagree with 'completed'. In every
|
||||
// other case normalizedStatus is left exactly as normalizeStateStatus
|
||||
// returned it.
|
||||
|
||||
Reference in New Issue
Block a user