agentxchain 2.155.26 → 2.155.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/status.js +37 -0
- package/src/lib/continuous-run.js +147 -3
- package/src/lib/vision-reader.js +184 -0
package/package.json
CHANGED
package/src/commands/status.js
CHANGED
|
@@ -23,6 +23,7 @@ import { findCurrentHumanEscalation } from '../lib/human-escalations.js';
|
|
|
23
23
|
import { getDashboardPid, getDashboardSession } from './dashboard.js';
|
|
24
24
|
import { readPreemptionMarker, validatePreemptionMarker, findPendingApprovedIntents } from '../lib/intake.js';
|
|
25
25
|
import { readContinuousSession } from '../lib/continuous-run.js';
|
|
26
|
+
import { deriveRoadmapCandidates, detectRoadmapExhaustedVisionOpen, resolveVisionPath } from '../lib/vision-reader.js';
|
|
26
27
|
import { readAllDispatchProgress } from '../lib/dispatch-progress.js';
|
|
27
28
|
import { readCoordinatorWarnings } from '../lib/coordinator-warnings.js';
|
|
28
29
|
import { reconcileStaleTurns } from '../lib/stale-turn-watchdog.js';
|
|
@@ -155,6 +156,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
155
156
|
const preemptionMarker = validatePreemptionMarker(root);
|
|
156
157
|
const pendingIntents = findPendingApprovedIntents(root, { run_id: stateRunId || null });
|
|
157
158
|
const continuousSession = readContinuousSession(root);
|
|
159
|
+
appendRoadmapOpenWorkNextAction(root, state, continuousSession, nextActions);
|
|
158
160
|
const gateActionAttempt = state?.pending_phase_transition
|
|
159
161
|
? summarizeLatestGateActionAttempt(root, 'phase_transition', state.pending_phase_transition.gate)
|
|
160
162
|
: state?.pending_run_completion
|
|
@@ -698,6 +700,41 @@ function renderGovernedStatus(context, opts) {
|
|
|
698
700
|
console.log('');
|
|
699
701
|
}
|
|
700
702
|
|
|
703
|
+
function appendRoadmapOpenWorkNextAction(root, state, continuousSession, nextActions) {
|
|
704
|
+
const status = state?.status || null;
|
|
705
|
+
const activeTurns = getActiveTurnCount(state);
|
|
706
|
+
const hasPendingGate = Boolean(state?.pending_phase_transition || state?.pending_run_completion);
|
|
707
|
+
const sessionTerminal = ['completed', 'idle_exit', 'vision_exhausted', 'vision_expansion_exhausted'].includes(continuousSession?.status);
|
|
708
|
+
const runTerminal = status === 'completed' || status === 'idle' || status === null;
|
|
709
|
+
|
|
710
|
+
if (activeTurns > 0 || hasPendingGate || (!runTerminal && !sessionTerminal)) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const roadmap = deriveRoadmapCandidates(root);
|
|
715
|
+
if (roadmap.ok && roadmap.candidates.length > 0) {
|
|
716
|
+
const candidate = roadmap.candidates[0];
|
|
717
|
+
const command = 'agentxchain run --continuous --vision .planning/VISION.md';
|
|
718
|
+
const reason = `Unchecked roadmap work remains (${candidate.section} line ${candidate.line}): ${candidate.goal}`;
|
|
719
|
+
if (!nextActions.some((action) => action.command === command && action.reason === reason)) {
|
|
720
|
+
nextActions.push({ command, reason, type: 'roadmap_open_work_detected' });
|
|
721
|
+
}
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// BUG-77: When roadmap has no unchecked work, check if roadmap is exhausted
|
|
726
|
+
// but VISION still has unplanned scope requiring PM roadmap-replenishment.
|
|
727
|
+
const visionPath = resolveVisionPath(root, continuousSession?.vision_path || '.planning/VISION.md');
|
|
728
|
+
const exhaustion = detectRoadmapExhaustedVisionOpen(root, visionPath);
|
|
729
|
+
if (exhaustion.open) {
|
|
730
|
+
const command = 'agentxchain run --continuous --vision .planning/VISION.md';
|
|
731
|
+
const reason = `Roadmap exhausted (${exhaustion.total_milestones} milestones checked through ${exhaustion.latest_milestone}). VISION.md has unplanned scope: ${exhaustion.unplanned_sections.join(', ')}`;
|
|
732
|
+
if (!nextActions.some((action) => action.type === 'roadmap_exhausted_vision_open')) {
|
|
733
|
+
nextActions.push({ command, reason, type: 'roadmap_exhausted_vision_open' });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
701
738
|
function renderConnectorHealthStatus(connectorHealth) {
|
|
702
739
|
const connectors = Array.isArray(connectorHealth?.connectors)
|
|
703
740
|
? connectorHealth.connectors
|
|
@@ -15,6 +15,8 @@ import { randomUUID } from 'node:crypto';
|
|
|
15
15
|
import {
|
|
16
16
|
resolveVisionPath,
|
|
17
17
|
deriveVisionCandidates,
|
|
18
|
+
deriveRoadmapCandidates,
|
|
19
|
+
detectRoadmapExhaustedVisionOpen,
|
|
18
20
|
captureVisionHeadingsSnapshot,
|
|
19
21
|
computeVisionContentSha,
|
|
20
22
|
buildSourceManifest,
|
|
@@ -644,12 +646,139 @@ function reconcileContinuousStartupState(context, session, contOpts, log) {
|
|
|
644
646
|
* @returns {{ ok: boolean, intentId?: string, section?: string, goal?: string, error?: string, idle?: boolean }}
|
|
645
647
|
*/
|
|
646
648
|
export function seedFromVision(root, visionPath, options = {}) {
|
|
649
|
+
const roadmapResult = deriveRoadmapCandidates(root);
|
|
650
|
+
if (!roadmapResult.ok) {
|
|
651
|
+
return { ok: false, error: roadmapResult.error };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (roadmapResult.candidates.length > 0) {
|
|
655
|
+
const candidate = roadmapResult.candidates[0];
|
|
656
|
+
const eventResult = recordEvent(root, {
|
|
657
|
+
source: 'vision_scan',
|
|
658
|
+
category: 'roadmap_open_work_detected',
|
|
659
|
+
signal: {
|
|
660
|
+
description: `${candidate.section}: ${candidate.goal}`,
|
|
661
|
+
roadmap_milestone: candidate.section,
|
|
662
|
+
roadmap_path: candidate.roadmap_path,
|
|
663
|
+
roadmap_line: candidate.line,
|
|
664
|
+
derived: true,
|
|
665
|
+
},
|
|
666
|
+
evidence: [
|
|
667
|
+
{ type: 'file', value: `${candidate.roadmap_path}:${candidate.line}` },
|
|
668
|
+
{ type: 'text', value: `Unchecked roadmap work: ${candidate.section} — ${candidate.goal}` },
|
|
669
|
+
],
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
if (!eventResult.ok) {
|
|
673
|
+
if (eventResult.deduplicated) {
|
|
674
|
+
return { ok: true, idle: true };
|
|
675
|
+
}
|
|
676
|
+
return { ok: false, error: `intake record failed: ${eventResult.error}` };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (eventResult.deduplicated) {
|
|
680
|
+
return { ok: true, idle: true };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const intentId = eventResult.intent.intent_id;
|
|
684
|
+
const triageResult = triageIntent(root, intentId, {
|
|
685
|
+
priority: candidate.priority,
|
|
686
|
+
template: 'generic',
|
|
687
|
+
charter: `[roadmap] ${candidate.section}: ${candidate.goal}`,
|
|
688
|
+
acceptance_contract: [
|
|
689
|
+
`Roadmap milestone addressed: ${candidate.section}`,
|
|
690
|
+
`Unchecked roadmap item completed: ${candidate.goal}`,
|
|
691
|
+
`Evidence source: ${candidate.roadmap_path}:${candidate.line}`,
|
|
692
|
+
],
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
if (!triageResult.ok) {
|
|
696
|
+
return { ok: false, error: `triage failed: ${triageResult.error}` };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const triageApproval = options.triageApproval || 'auto';
|
|
700
|
+
if (triageApproval === 'auto') {
|
|
701
|
+
const approveResult = approveIntent(root, intentId, {
|
|
702
|
+
approver: 'continuous_loop',
|
|
703
|
+
reason: 'roadmap-open-work auto-approval',
|
|
704
|
+
});
|
|
705
|
+
if (!approveResult.ok) {
|
|
706
|
+
return { ok: false, error: `approve failed: ${approveResult.error}` };
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
ok: true,
|
|
712
|
+
idle: false,
|
|
713
|
+
intentId,
|
|
714
|
+
section: candidate.section,
|
|
715
|
+
goal: candidate.goal,
|
|
716
|
+
source: candidate.source,
|
|
717
|
+
roadmap_path: candidate.roadmap_path,
|
|
718
|
+
roadmap_line: candidate.line,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
647
722
|
const result = deriveVisionCandidates(root, visionPath);
|
|
648
723
|
if (!result.ok) {
|
|
649
724
|
return { ok: false, error: result.error };
|
|
650
725
|
}
|
|
651
726
|
|
|
652
727
|
if (result.candidates.length === 0) {
|
|
728
|
+
// BUG-77: Before declaring idle, check if roadmap is exhausted but VISION
|
|
729
|
+
// has unplanned scope. If so, seed a PM roadmap-replenishment intent to
|
|
730
|
+
// derive the next bounded milestone instead of idle-exiting.
|
|
731
|
+
const exhaustion = detectRoadmapExhaustedVisionOpen(root, visionPath);
|
|
732
|
+
if (exhaustion.open) {
|
|
733
|
+
const sectionNames = exhaustion.unplanned_sections.join(', ');
|
|
734
|
+
const replenishmentEvent = recordEvent(root, {
|
|
735
|
+
source: 'vision_scan',
|
|
736
|
+
category: 'roadmap_exhausted_vision_open',
|
|
737
|
+
signal: {
|
|
738
|
+
description: `Roadmap exhausted (${exhaustion.total_milestones} milestones checked through ${exhaustion.latest_milestone}). VISION.md has unplanned scope: ${sectionNames}`,
|
|
739
|
+
unplanned_sections: exhaustion.unplanned_sections,
|
|
740
|
+
latest_milestone: exhaustion.latest_milestone,
|
|
741
|
+
derived: true,
|
|
742
|
+
},
|
|
743
|
+
evidence: [
|
|
744
|
+
{ type: 'text', value: `All ${exhaustion.total_milestones} roadmap milestones checked. VISION sections not yet planned: ${sectionNames}` },
|
|
745
|
+
],
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
if (replenishmentEvent.ok && !replenishmentEvent.deduplicated) {
|
|
749
|
+
const replenishmentIntentId = replenishmentEvent.intent.intent_id;
|
|
750
|
+
const triageResult = triageIntent(root, replenishmentIntentId, {
|
|
751
|
+
priority: 'p1',
|
|
752
|
+
template: 'generic',
|
|
753
|
+
charter: `[roadmap-replenishment] Derive next bounded roadmap increment from VISION.md. Unplanned scope: ${sectionNames}. Current roadmap checked through ${exhaustion.latest_milestone}. Read .planning/VISION.md and .planning/ROADMAP.md to select the next testable milestone. Produce concrete unchecked M${exhaustion.total_milestones + 1} items. Do not re-verify previous completed milestones.`,
|
|
754
|
+
acceptance_contract: [
|
|
755
|
+
`New unchecked milestone items added to .planning/ROADMAP.md`,
|
|
756
|
+
`Milestone scope derived from VISION.md sections: ${sectionNames}`,
|
|
757
|
+
`Milestone is bounded, testable, and does not duplicate existing checked milestones`,
|
|
758
|
+
],
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
if (triageResult.ok) {
|
|
762
|
+
const triageApproval = options.triageApproval || 'auto';
|
|
763
|
+
if (triageApproval === 'auto') {
|
|
764
|
+
approveIntent(root, replenishmentIntentId, {
|
|
765
|
+
approver: 'continuous_loop',
|
|
766
|
+
reason: 'roadmap-replenishment auto-approval (BUG-77)',
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
ok: true,
|
|
772
|
+
idle: false,
|
|
773
|
+
intentId: replenishmentIntentId,
|
|
774
|
+
section: 'Roadmap replenishment',
|
|
775
|
+
goal: `Derive next increment from: ${sectionNames}`,
|
|
776
|
+
source: 'roadmap_replenishment',
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
// If deduplicated or triage failed, fall through to idle
|
|
781
|
+
}
|
|
653
782
|
return { ok: true, idle: true };
|
|
654
783
|
}
|
|
655
784
|
|
|
@@ -714,6 +843,7 @@ export function seedFromVision(root, visionPath, options = {}) {
|
|
|
714
843
|
intentId,
|
|
715
844
|
section: candidate.section,
|
|
716
845
|
goal: candidate.goal,
|
|
846
|
+
source: 'vision_scan',
|
|
717
847
|
};
|
|
718
848
|
}
|
|
719
849
|
|
|
@@ -1443,6 +1573,7 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
1443
1573
|
: findNextDispatchableIntent(root, { run_id: session.current_run_id });
|
|
1444
1574
|
let targetIntentId = null;
|
|
1445
1575
|
let visionObjective = null;
|
|
1576
|
+
let seededSource = null;
|
|
1446
1577
|
|
|
1447
1578
|
if (queued.ok) {
|
|
1448
1579
|
targetIntentId = queued.intentId;
|
|
@@ -1478,13 +1609,24 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
1478
1609
|
|
|
1479
1610
|
targetIntentId = seeded.intentId;
|
|
1480
1611
|
visionObjective = `${seeded.section}: ${seeded.goal}`;
|
|
1612
|
+
seededSource = seeded.source || 'vision_scan';
|
|
1481
1613
|
session.idle_cycles = 0;
|
|
1482
|
-
|
|
1614
|
+
if (seeded.source === 'roadmap_open_work') {
|
|
1615
|
+
log(`Roadmap-derived: ${visionObjective}`);
|
|
1616
|
+
} else if (seeded.source === 'roadmap_replenishment') {
|
|
1617
|
+
log(`Roadmap-replenishment (roadmap exhausted, vision open): ${visionObjective}`);
|
|
1618
|
+
} else {
|
|
1619
|
+
log(`Vision-derived: ${visionObjective}`);
|
|
1620
|
+
}
|
|
1483
1621
|
}
|
|
1484
1622
|
|
|
1485
1623
|
// Prepare intent through intake lifecycle
|
|
1486
1624
|
const provenance = buildContinuousProvenance(targetIntentId, {
|
|
1487
|
-
trigger: visionObjective
|
|
1625
|
+
trigger: visionObjective
|
|
1626
|
+
? (seededSource === 'roadmap_open_work' ? 'roadmap_open_work'
|
|
1627
|
+
: seededSource === 'roadmap_replenishment' ? 'roadmap_replenishment'
|
|
1628
|
+
: 'vision_scan')
|
|
1629
|
+
: 'intake',
|
|
1488
1630
|
triggerReason: visionObjective || readIntent(root, targetIntentId)?.charter || null,
|
|
1489
1631
|
});
|
|
1490
1632
|
const preparedIntent = prepareIntentForDispatch(root, targetIntentId, {
|
|
@@ -1519,7 +1661,9 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
1519
1661
|
next_objective: nextObjective,
|
|
1520
1662
|
next_intent_id: targetIntentId,
|
|
1521
1663
|
runs_completed: session.runs_completed || 0,
|
|
1522
|
-
trigger: visionObjective
|
|
1664
|
+
trigger: visionObjective
|
|
1665
|
+
? (seededSource === 'roadmap_open_work' ? 'roadmap_open_work' : 'vision_scan')
|
|
1666
|
+
: 'intake',
|
|
1523
1667
|
},
|
|
1524
1668
|
});
|
|
1525
1669
|
}
|
package/src/lib/vision-reader.js
CHANGED
|
@@ -217,6 +217,68 @@ export function deriveVisionCandidates(root, visionPath) {
|
|
|
217
217
|
return { ok: true, candidates };
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Derive concrete candidates from unchecked roadmap milestones.
|
|
222
|
+
*
|
|
223
|
+
* This is intentionally narrower than VISION.md derivation: a roadmap item is
|
|
224
|
+
* already PM-shaped product scope, so continuous mode must treat it as work
|
|
225
|
+
* before falling back to broad vision inference.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} root - Project root
|
|
228
|
+
* @param {string} [roadmapPath] - Project-relative roadmap path
|
|
229
|
+
* @returns {{ ok: boolean, candidates: Array<{ section: string, goal: string, priority: string, source: string, roadmap_path: string, line: number }>, error?: string }}
|
|
230
|
+
*/
|
|
231
|
+
export function deriveRoadmapCandidates(root, roadmapPath = '.planning/ROADMAP.md') {
|
|
232
|
+
const absPath = isAbsolute(roadmapPath) ? roadmapPath : pathResolve(root, roadmapPath);
|
|
233
|
+
if (!existsSync(absPath)) {
|
|
234
|
+
return { ok: true, candidates: [] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let content;
|
|
238
|
+
try {
|
|
239
|
+
content = readFileSync(absPath, 'utf8');
|
|
240
|
+
} catch (err) {
|
|
241
|
+
return { ok: false, candidates: [], error: `Cannot read ROADMAP.md: ${err.message}` };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const activeSignals = loadActiveIntentSignals(root);
|
|
245
|
+
const completedSignals = loadCompletedIntentSignals(root);
|
|
246
|
+
const allSignals = [...activeSignals, ...completedSignals];
|
|
247
|
+
|
|
248
|
+
const candidates = [];
|
|
249
|
+
let currentMilestone = null;
|
|
250
|
+
const lines = content.split('\n');
|
|
251
|
+
|
|
252
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
253
|
+
const line = lines[i];
|
|
254
|
+
const headingMatch = line.match(/^#{2,6}\s+(M\d+\b.*)$/i);
|
|
255
|
+
if (headingMatch) {
|
|
256
|
+
currentMilestone = headingMatch[1].trim();
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const uncheckedMatch = line.match(/^\s*[-*]\s+\[\s\]\s+(.+?)\s*$/);
|
|
261
|
+
if (!uncheckedMatch || !currentMilestone) continue;
|
|
262
|
+
|
|
263
|
+
const goal = uncheckedMatch[1].trim();
|
|
264
|
+
const combinedGoal = `${currentMilestone}: ${goal}`;
|
|
265
|
+
if (isGoalAddressed(combinedGoal, allSignals) || isGoalAddressed(goal, allSignals)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
candidates.push({
|
|
270
|
+
section: currentMilestone,
|
|
271
|
+
goal,
|
|
272
|
+
priority: 'p1',
|
|
273
|
+
source: 'roadmap_open_work',
|
|
274
|
+
roadmap_path: roadmapPath,
|
|
275
|
+
line: i + 1,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { ok: true, candidates };
|
|
280
|
+
}
|
|
281
|
+
|
|
220
282
|
// ---------------------------------------------------------------------------
|
|
221
283
|
// Vision snapshot capture (BUG-60 Slice 3)
|
|
222
284
|
// ---------------------------------------------------------------------------
|
|
@@ -380,6 +442,128 @@ function truncatePreview(content, capBytes) {
|
|
|
380
442
|
return head + marker + tail;
|
|
381
443
|
}
|
|
382
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Detect whether the roadmap is exhausted but VISION.md still has unplanned scope.
|
|
447
|
+
*
|
|
448
|
+
* BUG-77: When all ROADMAP.md milestones are checked and no M<n+1> exists,
|
|
449
|
+
* but VISION.md has sections with goals that are NOT mapped to any roadmap
|
|
450
|
+
* milestone, continuous mode should dispatch PM to derive the next bounded
|
|
451
|
+
* roadmap increment — not declare idle_exit or vision_exhausted.
|
|
452
|
+
*
|
|
453
|
+
* @param {string} root - Project root
|
|
454
|
+
* @param {string} visionPath - Absolute path to VISION.md
|
|
455
|
+
* @param {string} [roadmapPath] - Project-relative roadmap path
|
|
456
|
+
* @returns {{ open: boolean, reason: string, unplanned_sections?: string[], mapped_sections?: string[], total_milestones?: number, latest_milestone?: string, evidence_map?: Array<{ milestone: string, status: string }> }}
|
|
457
|
+
*/
|
|
458
|
+
export function detectRoadmapExhaustedVisionOpen(root, visionPath, roadmapPath = '.planning/ROADMAP.md') {
|
|
459
|
+
const absRoadmap = isAbsolute(roadmapPath) ? roadmapPath : pathResolve(root, roadmapPath);
|
|
460
|
+
const absVision = isAbsolute(visionPath) ? visionPath : pathResolve(root, visionPath);
|
|
461
|
+
|
|
462
|
+
// If no roadmap, cannot be "exhausted"
|
|
463
|
+
if (!existsSync(absRoadmap)) {
|
|
464
|
+
return { open: false, reason: 'no_roadmap' };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let roadmapContent;
|
|
468
|
+
try {
|
|
469
|
+
roadmapContent = readFileSync(absRoadmap, 'utf8');
|
|
470
|
+
} catch {
|
|
471
|
+
return { open: false, reason: 'roadmap_unreadable' };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Parse milestone headings and check for unchecked items
|
|
475
|
+
const milestoneHeadings = [];
|
|
476
|
+
let currentMilestone = null;
|
|
477
|
+
let hasUnchecked = false;
|
|
478
|
+
|
|
479
|
+
for (const line of roadmapContent.split('\n')) {
|
|
480
|
+
const hm = line.match(/^#{2,6}\s+(M\d+\b.*)$/i);
|
|
481
|
+
if (hm) {
|
|
482
|
+
currentMilestone = hm[1].trim();
|
|
483
|
+
milestoneHeadings.push(currentMilestone);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (currentMilestone && /^\s*[-*]\s+\[\s\]/.test(line)) {
|
|
487
|
+
hasUnchecked = true;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Not exhausted if unchecked work exists (BUG-76 handles this)
|
|
492
|
+
if (hasUnchecked) {
|
|
493
|
+
return { open: false, reason: 'has_unchecked' };
|
|
494
|
+
}
|
|
495
|
+
// No milestones at all — not a milestone-based roadmap
|
|
496
|
+
if (milestoneHeadings.length === 0) {
|
|
497
|
+
return { open: false, reason: 'no_milestones' };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Roadmap IS exhausted (all milestones checked, none unchecked). Check VISION.
|
|
501
|
+
if (!existsSync(absVision)) {
|
|
502
|
+
return {
|
|
503
|
+
open: false,
|
|
504
|
+
reason: 'no_vision',
|
|
505
|
+
evidence_map: milestoneHeadings.map(m => ({ milestone: m, status: 'completed' })),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
let visionContent;
|
|
510
|
+
try {
|
|
511
|
+
visionContent = readFileSync(absVision, 'utf8');
|
|
512
|
+
} catch {
|
|
513
|
+
return { open: false, reason: 'vision_unreadable' };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const { sections } = parseVisionDocument(visionContent);
|
|
517
|
+
const sectionsWithGoals = sections.filter(s => s.goals.length > 0);
|
|
518
|
+
|
|
519
|
+
if (sectionsWithGoals.length === 0) {
|
|
520
|
+
return {
|
|
521
|
+
open: false,
|
|
522
|
+
reason: 'vision_no_actionable_scope',
|
|
523
|
+
evidence_map: milestoneHeadings.map(m => ({ milestone: m, status: 'completed' })),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Check which VISION sections are NOT mapped to any roadmap milestone heading
|
|
528
|
+
const unplanned = [];
|
|
529
|
+
const mapped = [];
|
|
530
|
+
|
|
531
|
+
for (const section of sectionsWithGoals) {
|
|
532
|
+
const sectionWords = extractSignificantWords(section.heading);
|
|
533
|
+
const isMapped = milestoneHeadings.some(m => {
|
|
534
|
+
const milestoneWords = extractSignificantWords(m);
|
|
535
|
+
if (sectionWords.length === 0 || milestoneWords.length === 0) return false;
|
|
536
|
+
const overlap = sectionWords.filter(w =>
|
|
537
|
+
milestoneWords.some(mw => mw.includes(w) || w.includes(mw)),
|
|
538
|
+
).length;
|
|
539
|
+
return overlap / sectionWords.length >= 0.4;
|
|
540
|
+
});
|
|
541
|
+
if (isMapped) {
|
|
542
|
+
mapped.push(section.heading);
|
|
543
|
+
} else {
|
|
544
|
+
unplanned.push(section.heading);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (unplanned.length > 0) {
|
|
549
|
+
return {
|
|
550
|
+
open: true,
|
|
551
|
+
reason: 'roadmap_exhausted_vision_open',
|
|
552
|
+
unplanned_sections: unplanned,
|
|
553
|
+
mapped_sections: mapped,
|
|
554
|
+
total_milestones: milestoneHeadings.length,
|
|
555
|
+
latest_milestone: milestoneHeadings[milestoneHeadings.length - 1],
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
open: false,
|
|
561
|
+
reason: 'vision_fully_mapped',
|
|
562
|
+
evidence_map: milestoneHeadings.map(m => ({ milestone: m, status: 'completed' })),
|
|
563
|
+
mapped_sections: mapped,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
383
567
|
/**
|
|
384
568
|
* Resolve a vision path relative to the project root.
|
|
385
569
|
*
|