fix(#4023): preserve decimal phase ids in init progress ordering and smart-entry output (#4110)

* test(#4023): reproduce decimal phase-id coercions

* fix(#4023): preserve decimal phase ids in progress signals

* test(#4023): align phase token contract expectations

* chore(#4023): point the changeset at PR #4110

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
BeeHiggs
2026-09-01 11:58:20 -06:00
committed by GitHub
parent 80d2236ca5
commit 41466e8e88
6 changed files with 95 additions and 17 deletions

View File

@@ -92,7 +92,7 @@ const {
extractCurrentMilestone,
} = roadmapParser;
const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
const { normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase } = phaseId;
const { comparePhaseNum, normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase } = phaseId;
const { pruneOrphanedWorktrees } = worktreeSafety;
const {
@@ -3175,9 +3175,7 @@ function cmdInitProgress(cwd: string, raw: boolean, options: Record<string, unkn
}
}
phases.sort(
(a, b) => parseInt(a['number'] as string, 10) - parseInt(b['number'] as string, 10),
);
phases.sort((a, b) => comparePhaseNum(a['number'], b['number']));
// #3581: the frontier is ROADMAP ORDER, not artifact presence. The disk loop
// above could claim nextPhase from a stray out-of-order artifact directory

View File

@@ -54,7 +54,7 @@ import stateDocument = require('./state-document.cjs');
const { stateFieldValue } = stateDocument;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import phaseId = require('./phase-id.cjs');
const { comparePhaseNum, extractPhaseToken, matchPhaseDirs, normalizePhaseName, stripProjectCodePrefix } = phaseId;
const { comparePhaseNum, extractPhaseToken, matchPhaseDirs, normalizePhaseName, parsePhaseFromProse, stripProjectCodePrefix } = phaseId;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import stateMod = require('./state.cjs');
const { readStateHeadFreshness } = stateMod;
@@ -87,7 +87,7 @@ export interface SmartEntryAction {
}
export interface SmartEntrySignals {
current_phase: number | null;
current_phase: string | null;
total_phases: number | null;
status: string;
progress: number | null;
@@ -306,9 +306,7 @@ function readGitSignals(cwd: string): GitSignals {
/** Leading numeric phase token from a STATE.md scalar or body `Phase:` value. */
function phaseTokenFromState(raw: string | null): string | null {
if (!raw?.trim()) return null;
const match = raw.trim().match(/^(\d+(?:[A-Z])?(?:\.\d+)*)/i);
return match ? match[1] : null;
return parsePhaseFromProse(raw).phase;
}
/**
@@ -513,7 +511,7 @@ export function detectSignals(cwd: string, now: () => number = Date.now): SmartE
const freshness = readStateHeadFreshness(cwd, stateHeadRaw);
return {
current_phase: parseIntOrNull(currentPhaseRaw),
current_phase: phaseTokenFromState(currentPhaseRaw),
total_phases: parseIntOrNull(totalPhasesRaw),
status: (statusRaw || '').toLowerCase(),
progress: parseIntOrNull(progressRaw),
@@ -555,10 +553,11 @@ function isComplete(s: SmartEntrySignals): boolean {
if (s.roadmap_total_phases === 0) return false;
if (s.roadmap_completed_phases < s.roadmap_total_phases) return false;
} else {
// Legacy path: STATE.md comparison. Still subject to the two-scale bug,
// but only fires when ROADMAP.md is absent or has no Progress table.
// Legacy path: STATE.md comparison. It only fires when ROADMAP.md is
// absent or has no Progress table, and uses canonical phase-id ordering
// so dotted phase ids are never coerced to JavaScript numbers.
if (s.total_phases === null || s.current_phase === null) return false;
if (s.current_phase < s.total_phases) return false;
if (comparePhaseNum(s.current_phase, s.total_phases) < 0) return false;
}
// Status regex: require milestone-level completion language. The pre-fix
// regex matched any "shipped" / "done" substring (per-phase language).