fix(#1913): derive workstream progress status from shipped signals (#1916)

workstream progress trusted the mutable STATE.md `Status` field, so a
shipped/archived milestone whose field was left at `executing` was reported
as executing — a stale hand-maintained field became the source of truth
instead of the authoritative archive/tag/ROADMAP signals.

Derive status in the inventory builder from a milestoneShipped signal
(archived milestone snapshot under milestones/, or a SHIPPED marker in the
workstream ROADMAP), collected by inspectWorkstream. The inventory now
reports `status_source` (field|derived) and `status_conflict` (true when
the derived value disagrees with the stale field), and a shipped workstream
is never reported executing.

- src/workstream-inventory-builder.cts: milestoneShipped input + status_source/status_conflict outputs + derivation
- src/workstream-inventory.cts: workstreamMilestoneShipped() signal detector wired into inspectWorkstream
- tests/workstream-inventory.test.cjs: regression (builder unit + inspectWorkstream integration + negative)

Closes #1913
This commit is contained in:
Tom Boucher
2026-07-02 10:24:41 -04:00
committed by GitHub
parent 9d7625c4ad
commit 05cd55d43b
4 changed files with 158 additions and 1 deletions

View File

@@ -60,6 +60,14 @@ export interface BuildWorkstreamInventoryInputs {
roadmapPhaseCount: number;
stateProjection: StateProjection;
filesExist: WorkstreamFilesExist;
/**
* True when an authoritative shipped signal is present for this workstream
* (an archived milestone snapshot under milestones/, or a SHIPPED marker in
* the workstream ROADMAP). When true, the inventory status is DERIVED as
* "milestone complete" regardless of the mutable STATE.md `Status` field,
* so a stale field can never report a shipped workstream as executing (#1913).
*/
milestoneShipped: boolean;
}
export interface WorkstreamInventory {
@@ -68,6 +76,10 @@ export interface WorkstreamInventory {
active: boolean;
files: WorkstreamFilesExist;
status: string;
/** Whether `status` was derived from an authoritative signal ("derived") or taken verbatim from the STATE.md field ("field"). */
status_source: 'field' | 'derived';
/** True when the derived status disagrees with the STATE.md `Status` field (the field is stale). */
status_conflict: boolean;
current_phase: string | null | undefined;
last_activity: string | null | undefined;
phases: PhaseStatus[];
@@ -90,6 +102,7 @@ export function buildWorkstreamInventory(inputs: BuildWorkstreamInventoryInputs)
roadmapPhaseCount,
stateProjection,
filesExist,
milestoneShipped,
} = inputs;
// Index counts by directory for O(1) lookup during sort/iteration
@@ -122,6 +135,15 @@ export function buildWorkstreamInventory(inputs: BuildWorkstreamInventoryInputs)
});
}
// #1913: derive status from authoritative shipped signals rather than trusting
// the mutable STATE.md `Status` field. When a shipped signal is present, the
// workstream is "milestone complete" regardless of a stale field value.
const fieldStatus = stateProjection.status;
const useDerived = milestoneShipped;
const status = useDerived ? 'milestone complete' : fieldStatus;
const status_source: 'field' | 'derived' = useDerived ? 'derived' : 'field';
const status_conflict = useDerived && !isCompletedInventory(fieldStatus);
return {
name,
path: toPosixPath(path.relative(projectDir, workstreamDir)),
@@ -131,7 +153,9 @@ export function buildWorkstreamInventory(inputs: BuildWorkstreamInventoryInputs)
state: filesExist.state,
requirements: filesExist.requirements,
},
status: stateProjection.status,
status,
status_source,
status_conflict,
current_phase: stateProjection.current_phase,
last_activity: stateProjection.last_activity,
phases,

View File

@@ -84,6 +84,30 @@ function readStateProjection(statePath: string): StateProjection {
}
}
/**
* #1913: detect an authoritative shipped signal for a workstream so the
* inventory status is never trusted from the mutable STATE.md `Status` field
* alone. Returns true when EITHER an archived milestone snapshot is present
* under `<planningBase>/milestones/` OR the workstream ROADMAP carries a
* SHIPPED marker — both are hard to desync, unlike the hand-maintained field.
*/
function workstreamMilestoneShipped(roadmapPath: string, planningBase: string): boolean {
try {
const milestonesDir = path.join(planningBase, 'milestones');
for (const entry of fs.readdirSync(milestonesDir, { withFileTypes: true })) {
if (entry.isFile() && /-ROADMAP\.md$/i.test(entry.name)) return true;
}
} catch {
/* no milestones archive dir */
}
try {
if (/SHIPPED/i.test(fs.readFileSync(roadmapPath, 'utf-8'))) return true;
} catch {
/* no roadmap */
}
return false;
}
function sortWorkstreamInventories(inventories: WorkstreamInventory[], activeWorkstreamName: string | null): WorkstreamInventory[] {
return [...inventories].sort((a, b) => {
const aActive = a.name === activeWorkstreamName ? 1 : 0;
@@ -123,6 +147,7 @@ function inspectWorkstream(cwd: string, name: string, options: InspectWorkstream
state: fs.existsSync(p.state),
requirements: fs.existsSync(p.requirements),
},
milestoneShipped: workstreamMilestoneShipped(p.roadmap, p.planning),
});
}