pan-wizard 3.33.0 → 3.35.0
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/README.md +29 -26
- package/agents/pan-conductor.md +18 -17
- package/agents/pan-counterfactual.md +2 -2
- package/agents/pan-debugger.md +30 -9
- package/agents/pan-design-checker.md +2 -1
- package/agents/pan-designer.md +2 -2
- package/agents/pan-distiller.md +3 -3
- package/agents/pan-document_code.md +14 -14
- package/agents/pan-executor.md +30 -9
- package/agents/pan-experiment-runner.md +9 -8
- package/agents/pan-hardener.md +1 -1
- package/agents/pan-integration-checker.md +2 -2
- package/agents/pan-knowledge.md +1 -1
- package/agents/pan-meta-reviewer.md +4 -4
- package/agents/pan-optimizer.md +39 -43
- package/agents/pan-phase-researcher.md +10 -10
- package/agents/pan-plan-checker.md +37 -34
- package/agents/pan-planner.md +57 -44
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-project-researcher.md +12 -12
- package/agents/pan-release.md +1 -1
- package/agents/pan-research-synthesizer.md +3 -3
- package/agents/pan-reviewer.md +26 -3
- package/agents/pan-roadmapper.md +15 -14
- package/agents/pan-verifier.md +33 -25
- package/bin/install-lib.cjs +176 -38
- package/bin/install.js +81 -63
- package/commands/pan/army.md +15 -11
- package/commands/pan/assumptions.md +2 -2
- package/commands/pan/audit-deployment.md +11 -12
- package/commands/pan/cost.md +8 -8
- package/commands/pan/dashboard.md +0 -3
- package/commands/pan/debug.md +10 -7
- package/commands/pan/design-phase.md +5 -5
- package/commands/pan/discuss-phase.md +2 -0
- package/commands/pan/exec-phase.md +21 -37
- package/commands/pan/experiment.md +9 -8
- package/commands/pan/focus-auto.md +22 -20
- package/commands/pan/focus-design.md +7 -6
- package/commands/pan/focus-doc-audit.md +4 -3
- package/commands/pan/focus-drift-walking.md +4 -3
- package/commands/pan/focus-exec.md +6 -6
- package/commands/pan/focus-plan.md +12 -11
- package/commands/pan/focus-scan.md +5 -3
- package/commands/pan/focus-sync.md +3 -2
- package/commands/pan/git.md +8 -8
- package/commands/pan/health.md +2 -2
- package/commands/pan/hud.md +4 -7
- package/commands/pan/hygiene.md +14 -7
- package/commands/pan/knowledge.md +9 -10
- package/commands/pan/learn.md +10 -7
- package/commands/pan/links.md +4 -1
- package/commands/pan/map-codebase.md +1 -1
- package/commands/pan/mcp-bridge.md +19 -21
- package/commands/pan/milestone-done.md +6 -3
- package/commands/pan/milestone-new.md +1 -1
- package/commands/pan/new-project.md +10 -10
- package/commands/pan/optimize.md +18 -14
- package/commands/pan/patches.md +2 -6
- package/commands/pan/pause.md +1 -1
- package/commands/pan/phase-tests.md +3 -2
- package/commands/pan/plan-phase.md +9 -19
- package/commands/pan/preview.md +2 -3
- package/commands/pan/profile.md +2 -2
- package/commands/pan/quick.md +1 -1
- package/commands/pan/remove-phase.md +1 -1
- package/commands/pan/report.md +3 -7
- package/commands/pan/research-phase.md +16 -15
- package/commands/pan/resume.md +9 -11
- package/commands/pan/retro.md +5 -3
- package/commands/pan/review-deep.md +7 -9
- package/commands/pan/settings.md +1 -1
- package/commands/pan/verify-phase.md +0 -11
- package/commands/pan/what-if.md +3 -4
- package/hooks/dist/pan-context-monitor.js +352 -44
- package/hooks/dist/pan-cost-logger.js +30 -0
- package/hooks/dist/pan-guard.js +244 -0
- package/hooks/dist/pan-statusline.js +73 -24
- package/hooks/dist/pan-stop-guard.js +7 -2
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +44 -6
- package/pan-wizard-core/bin/lib/bridge.cjs +6 -5
- package/pan-wizard-core/bin/lib/campaign.cjs +4 -2
- package/pan-wizard-core/bin/lib/commands-learnings.cjs +19 -2
- package/pan-wizard-core/bin/lib/commands.cjs +57 -6
- package/pan-wizard-core/bin/lib/config.cjs +7 -1
- package/pan-wizard-core/bin/lib/constants.cjs +18 -6
- package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
- package/pan-wizard-core/bin/lib/core.cjs +53 -19
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
- package/pan-wizard-core/bin/lib/cost.cjs +142 -13
- package/pan-wizard-core/bin/lib/distill.cjs +6 -3
- package/pan-wizard-core/bin/lib/doc-lint.cjs +30 -11
- package/pan-wizard-core/bin/lib/findings.cjs +135 -21
- package/pan-wizard-core/bin/lib/focus.cjs +13 -4
- package/pan-wizard-core/bin/lib/frontmatter.cjs +105 -26
- package/pan-wizard-core/bin/lib/hygiene.cjs +184 -17
- package/pan-wizard-core/bin/lib/init.cjs +13 -4
- package/pan-wizard-core/bin/lib/knowledge.cjs +17 -1
- package/pan-wizard-core/bin/lib/learn-index.cjs +111 -10
- package/pan-wizard-core/bin/lib/learn-lint.cjs +30 -3
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +8 -4
- package/pan-wizard-core/bin/lib/memory.cjs +508 -54
- package/pan-wizard-core/bin/lib/milestone.cjs +8 -3
- package/pan-wizard-core/bin/lib/optimize.cjs +97 -17
- package/pan-wizard-core/bin/lib/phase-gate.cjs +174 -0
- package/pan-wizard-core/bin/lib/phase-remove.cjs +85 -21
- package/pan-wizard-core/bin/lib/phase-report.cjs +8 -2
- package/pan-wizard-core/bin/lib/phase.cjs +39 -13
- package/pan-wizard-core/bin/lib/preview.cjs +34 -31
- package/pan-wizard-core/bin/lib/roadmap-compact.cjs +267 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +243 -9
- package/pan-wizard-core/bin/lib/runner.cjs +2 -1
- package/pan-wizard-core/bin/lib/state-compact.cjs +11 -6
- package/pan-wizard-core/bin/lib/state.cjs +43 -28
- package/pan-wizard-core/bin/lib/template.cjs +11 -4
- package/pan-wizard-core/bin/lib/verdict.cjs +140 -10
- package/pan-wizard-core/bin/lib/verify-integrity.cjs +181 -0
- package/pan-wizard-core/bin/lib/verify-plan-set.cjs +230 -0
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -13
- package/pan-wizard-core/bin/lib/verify-retro.cjs +5 -3
- package/pan-wizard-core/bin/lib/verify-scope.cjs +11 -7
- package/pan-wizard-core/bin/lib/verify.cjs +30 -28
- package/pan-wizard-core/bin/lib/whatif.cjs +53 -9
- package/pan-wizard-core/bin/pan-tools.cjs +114 -19
- package/pan-wizard-core/learnings/index.json +5 -5
- package/pan-wizard-core/mcp/merge-gate.cjs +14 -3
- package/pan-wizard-core/mcp/native-tools.cjs +3 -3
- package/pan-wizard-core/opencode/pan-wizard.js +139 -0
- package/pan-wizard-core/references/checkpoints.md +3 -3
- package/pan-wizard-core/references/continuation-format.md +3 -3
- package/pan-wizard-core/references/decimal-phase-calculation.md +2 -3
- package/pan-wizard-core/references/design-methodology.md +1 -1
- package/pan-wizard-core/references/git-integration.md +4 -6
- package/pan-wizard-core/references/git-planning-commit.md +2 -2
- package/pan-wizard-core/references/guardrails.md +2 -2
- package/pan-wizard-core/references/handoff-decisions.md +3 -3
- package/pan-wizard-core/references/model-profile-resolution.md +5 -5
- package/pan-wizard-core/references/model-profiles.md +15 -10
- package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
- package/pan-wizard-core/references/planning-config.md +7 -7
- package/pan-wizard-core/references/tdd.md +10 -1
- package/pan-wizard-core/references/verdict-contract.md +6 -1
- package/pan-wizard-core/references/verification-patterns.md +5 -5
- package/pan-wizard-core/templates/codebase/architecture.md +2 -2
- package/pan-wizard-core/templates/codebase/best-practices.md +2 -2
- package/pan-wizard-core/templates/codebase/concerns.md +3 -3
- package/pan-wizard-core/templates/codebase/integrations.md +6 -6
- package/pan-wizard-core/templates/codebase/relationships.md +2 -2
- package/pan-wizard-core/templates/codebase/stack.md +2 -2
- package/pan-wizard-core/templates/codebase/structure.md +4 -4
- package/pan-wizard-core/templates/codebase/testing.md +4 -4
- package/pan-wizard-core/templates/config.json +2 -6
- package/pan-wizard-core/templates/continue-here.md +1 -1
- package/pan-wizard-core/templates/debug-subagent-prompt.md +5 -3
- package/pan-wizard-core/templates/debug.md +15 -1
- package/pan-wizard-core/templates/milestone-archive.md +6 -6
- package/pan-wizard-core/templates/milestone.md +1 -1
- package/pan-wizard-core/templates/phase-prompt.md +31 -17
- package/pan-wizard-core/templates/planner-subagent-prompt.md +5 -5
- package/pan-wizard-core/templates/preview-report.md +1 -1
- package/pan-wizard-core/templates/research-project/architecture.md +3 -3
- package/pan-wizard-core/templates/research-project/features.md +1 -1
- package/pan-wizard-core/templates/research-project/pitfalls.md +1 -1
- package/pan-wizard-core/templates/research-project/stack.md +3 -3
- package/pan-wizard-core/templates/research.md +5 -5
- package/pan-wizard-core/templates/retrospective.md +1 -1
- package/pan-wizard-core/templates/roadmap.md +15 -15
- package/pan-wizard-core/templates/state.md +4 -4
- package/pan-wizard-core/templates/summary-complex.md +1 -0
- package/pan-wizard-core/templates/summary-minimal.md +1 -0
- package/pan-wizard-core/templates/summary-standard.md +2 -1
- package/pan-wizard-core/templates/summary.md +18 -2
- package/pan-wizard-core/templates/uat.md +3 -3
- package/pan-wizard-core/templates/user-setup.md +8 -8
- package/pan-wizard-core/templates/verification-report.md +53 -8
- package/pan-wizard-core/workflows/add-phase.md +1 -1
- package/pan-wizard-core/workflows/assumptions.md +1 -1
- package/pan-wizard-core/workflows/diagnose-issues.md +4 -4
- package/pan-wizard-core/workflows/discuss-phase.md +10 -8
- package/pan-wizard-core/workflows/exec-phase.md +75 -95
- package/pan-wizard-core/workflows/execute-plan.md +8 -7
- package/pan-wizard-core/workflows/health.md +13 -12
- package/pan-wizard-core/workflows/help.md +8 -9
- package/pan-wizard-core/workflows/learn.md +13 -11
- package/pan-wizard-core/workflows/map-codebase.md +10 -10
- package/pan-wizard-core/workflows/milestone-audit.md +10 -10
- package/pan-wizard-core/workflows/milestone-gaps.md +18 -25
- package/pan-wizard-core/workflows/milestone-new.md +7 -6
- package/pan-wizard-core/workflows/new-project.md +17 -25
- package/pan-wizard-core/workflows/optimize.md +11 -9
- package/pan-wizard-core/workflows/pause.md +6 -1
- package/pan-wizard-core/workflows/phase-tests.md +4 -4
- package/pan-wizard-core/workflows/plan-phase.md +53 -42
- package/pan-wizard-core/workflows/profile.md +2 -2
- package/pan-wizard-core/workflows/progress.md +6 -8
- package/pan-wizard-core/workflows/quick.md +7 -5
- package/pan-wizard-core/workflows/remove-phase.md +1 -1
- package/pan-wizard-core/workflows/research-phase.md +9 -5
- package/pan-wizard-core/workflows/resume-project.md +4 -6
- package/pan-wizard-core/workflows/retro.md +3 -1
- package/pan-wizard-core/workflows/settings.md +16 -24
- package/pan-wizard-core/workflows/todo-check.md +4 -3
- package/pan-wizard-core/workflows/transition.md +16 -12
- package/pan-wizard-core/workflows/update.md +25 -17
- package/pan-wizard-core/workflows/verify-phase.md +22 -20
- package/pan-zcode/KNOWN-BETA-RISKS.md +2 -2
- package/pan-zcode/README.md +7 -7
- package/pan-zcode/bin/install-zcode.js +1 -1
- package/scripts/build-hooks.js +1 -0
- package/scripts/build-plugin.js +3 -1
- package/scripts/generate-skills-docs.py +15 -6
- package/scripts/prompt-lint.cjs +167 -0
- package/scripts/test-surface.cjs +1 -0
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { PHASES_DIR, ROADMAP_FILE, REQUIREMENTS_FILE, isPlanFile, isSummaryFile, isContextFile, isResearchFile, PHASE_HEADER_RE, getPlanId, getSummaryId } = require('./constants.cjs');
|
|
8
|
-
const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, classifyPhaseStatus } = require('./utils.cjs');
|
|
9
|
-
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
|
|
7
|
+
const { PHASES_DIR, ROADMAP_FILE, REQUIREMENTS_FILE, ROADMAP_SLICE_SUFFIX, isPlanFile, isSummaryFile, isContextFile, isResearchFile, PHASE_HEADER_RE, getPlanId, getSummaryId } = require('./constants.cjs');
|
|
8
|
+
const { planningPath, planningRel, phasesPath, filterPlanFiles, filterSummaryFiles, classifyPhaseStatus } = require('./utils.cjs');
|
|
9
|
+
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, safeReadFile, toLf, dominantEol, withEol } = require('./core.cjs');
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Extract a single phase section from roadmap.md including goal and success criteria.
|
|
@@ -21,8 +21,8 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
|
21
21
|
try {
|
|
22
22
|
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
const escapedPhase =
|
|
24
|
+
// Padded or not: `init` hands workflows "05", roadmaps write "Phase 5:".
|
|
25
|
+
const escapedPhase = phaseNumRe(phaseNum);
|
|
26
26
|
|
|
27
27
|
// Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
|
|
28
28
|
const phasePattern = new RegExp(
|
|
@@ -65,7 +65,12 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
|
65
65
|
? headerIndex + nextHeaderMatch.index
|
|
66
66
|
: content.length;
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
let section = content.slice(headerIndex, sectionEnd).trim();
|
|
69
|
+
|
|
70
|
+
// A phase moved out by `roadmap compact` leaves a stub here; its success
|
|
71
|
+
// criteria and the rest of its section are in roadmap-history.md.
|
|
72
|
+
const compacted = require('./roadmap-compact.cjs').compactedPhaseSection(cwd, phaseNum, section);
|
|
73
|
+
if (compacted && compacted.section) section = compacted.section;
|
|
69
74
|
|
|
70
75
|
// Extract goal if present
|
|
71
76
|
const goalMatch = section.match(/(?:\*\*Goal:\*\*|\*\*Goal\*\*:)\s*([^\n]+)/i);
|
|
@@ -85,6 +90,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
|
|
|
85
90
|
goal,
|
|
86
91
|
success_criteria,
|
|
87
92
|
section,
|
|
93
|
+
...(compacted ? { compacted: true, history_path: compacted.history_path, history_found: compacted.history_found } : {}),
|
|
88
94
|
},
|
|
89
95
|
raw,
|
|
90
96
|
section
|
|
@@ -346,7 +352,7 @@ const isSeparator = (line) => /^\s*\|(?:\s*:?-{3,}:?\s*\|)+\s*$/.test(line);
|
|
|
346
352
|
function updateProgressTableRow(content, phaseNum, values) {
|
|
347
353
|
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
|
348
354
|
const lines = content.split(/\r?\n/);
|
|
349
|
-
const phaseRe = new RegExp(`^(?:phase\\s+)?${
|
|
355
|
+
const phaseRe = new RegExp(`^(?:phase\\s+)?${phaseNumRe(phaseNum)}\\.?(?=\\s|:|$)`, 'i');
|
|
350
356
|
let reason = null;
|
|
351
357
|
for (let i = 0; i + 1 < lines.length; i++) {
|
|
352
358
|
if (!isTableLine(lines[i]) || !isSeparator(lines[i + 1])) continue;
|
|
@@ -377,7 +383,7 @@ function updateProgressTableRow(content, phaseNum, values) {
|
|
|
377
383
|
/** Rewrite `**Plans:**` inside the phase's own section only. Pure. */
|
|
378
384
|
function updatePhasePlansLine(content, phaseNum, text) {
|
|
379
385
|
const lines = content.split(/(?<=\n)/);
|
|
380
|
-
const headRe = new RegExp(`^(#{2,4})\\s*Phase\\s+${
|
|
386
|
+
const headRe = new RegExp(`^(#{2,4})\\s*Phase\\s+${phaseNumRe(phaseNum)}(?=[\\s:.]|$)`, 'i');
|
|
381
387
|
const start = lines.findIndex((l) => headRe.test(l));
|
|
382
388
|
if (start < 0) return { content, updated: false };
|
|
383
389
|
const level = lines[start].match(headRe)[1].length;
|
|
@@ -426,7 +432,9 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
|
|
426
432
|
output({ updated: false, reason: 'roadmap.md not found', error: 'roadmap_not_found', plan_count: planCount, summary_count: summaryCount }, raw, 'no roadmap');
|
|
427
433
|
return;
|
|
428
434
|
}
|
|
429
|
-
|
|
435
|
+
// Padded or not: the executor passes init's "05" for a `Phase 5` roadmap, and
|
|
436
|
+
// every lookup below used to miss it while the result still said `updated: true`.
|
|
437
|
+
const phaseEscaped = phaseNumRe(phaseNum);
|
|
430
438
|
|
|
431
439
|
// Progress table row: the Plans, Status and Completed cells, found by header.
|
|
432
440
|
const table = updateProgressTableRow(roadmapContent, phaseNum, {
|
|
@@ -530,14 +538,240 @@ function syncRequirementCheckboxes(cwd, requirementIds) {
|
|
|
530
538
|
return { updated: checked > 0, checked, total: requirementIds.length };
|
|
531
539
|
}
|
|
532
540
|
|
|
541
|
+
// ─── Phase slice (memory optimisation O2) ────────────────────────────────────
|
|
542
|
+
//
|
|
543
|
+
// What work on ONE phase needs from roadmap.md and requirements.md: a one-line
|
|
544
|
+
// index of every phase, this phase's section, the goals of the phases it depends
|
|
545
|
+
// on, and the requirement lines it names. The planner, the plan checker, the
|
|
546
|
+
// researcher, the executors (through the plan's <context>) and the verifier read
|
|
547
|
+
// this instead of the whole files. Measured on the field installs (2026-10-04):
|
|
548
|
+
// 89–98% smaller; a 54-phase project went from ~146k tokens to ~3k, and those
|
|
549
|
+
// whole files had been re-read on every turn of every spawn that listed them.
|
|
550
|
+
|
|
551
|
+
/** Phase numbers compare without zero padding: "03" and "3" are one phase. */
|
|
552
|
+
const unpadPhase = (n) => String(n).trim().replace(/^0+(?=\d)/, '');
|
|
553
|
+
|
|
554
|
+
/** A phase number as a regex source matching it padded or not ("05" finds `Phase 5:` and `Phase 05:`). */
|
|
555
|
+
const phaseNumRe = (n) => `0*${escapeRegex(unpadPhase(n))}`;
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Requirement ids on a phase section's `**Requirements**:` line. Taken by shape,
|
|
559
|
+
* not by splitting on commas: field roadmaps write prose there ("none of its own;
|
|
560
|
+
* pulled forward from Phase 45 (`CI-01`)…", "seeding (replace …), TEST-01 (…)").
|
|
561
|
+
* An id is a `CAT-01`-style token anywhere on the line, or a comma-separated item
|
|
562
|
+
* that is a single token carrying a digit (`R1`, `REQ001`).
|
|
563
|
+
*/
|
|
564
|
+
function phaseRequirementIds(section) {
|
|
565
|
+
const m = String(section || '').match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m)
|
|
566
|
+
|| String(section || '').match(/^\*\*Requirements:\*\*[^\S\n]*([^\n]*)$/m);
|
|
567
|
+
if (!m) return [];
|
|
568
|
+
const line = m[1].replace(/[[\]`]/g, ' ');
|
|
569
|
+
const shaped = line.match(/\b[A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-\d+[A-Za-z]?\b/g) || [];
|
|
570
|
+
const tokens = line.split(',').map(s => s.trim()).filter(s => /^[A-Za-z][A-Za-z0-9._-]*\d[A-Za-z0-9._-]*$/.test(s));
|
|
571
|
+
return [...new Set([...shaped, ...tokens])];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Phase numbers on a section's `**Depends on**:` line ("Phase 2, Phase 2.1" → ['2', '2.1']). */
|
|
575
|
+
function phaseDependencies(section) {
|
|
576
|
+
const m = String(section || '').match(/(?:\*\*Depends on:\*\*|\*\*Depends on\*\*:)\s*([^\n]+)/i);
|
|
577
|
+
if (!m) return [];
|
|
578
|
+
return [...new Set([...m[1].matchAll(/\bPhase\s+(\d+[A-Z]?(?:\.\d+)*)/gi)].map(x => unpadPhase(x[1])))];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** A phase's section in roadmap.md content, or null. Mirrors getRoadmapPhaseInternal. */
|
|
582
|
+
function roadmapPhaseSection(content, phaseNum) {
|
|
583
|
+
const escaped = escapeRegex(unpadPhase(phaseNum));
|
|
584
|
+
const header = content.match(new RegExp(`#{2,4}\\s*Phase\\s+0*${escaped}:\\s*([^\\n]+)`, 'i'));
|
|
585
|
+
if (!header) return null;
|
|
586
|
+
const rest = content.slice(header.index);
|
|
587
|
+
const next = rest.match(/\n#{2,4}\s+Phase\s+\d/i);
|
|
588
|
+
const section = content.slice(header.index, next ? header.index + next.index : content.length).trim();
|
|
589
|
+
return { name: header[1].trim(), section };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* The requirement ids roadmap.md names for one phase: from its own `### Phase N:`
|
|
594
|
+
* section, else from a `**Requirements:**` line under its checklist entry, before
|
|
595
|
+
* the next "Phase <n>" (the layout older roadmaps use). `phase complete` and
|
|
596
|
+
* `validate health --repair` read them here: their own search took the first
|
|
597
|
+
* Requirements line after the first "Phase N" anywhere, which on the template's
|
|
598
|
+
* layout (checklist above the sections) was phase 1's, and matched "05" to nothing.
|
|
599
|
+
*/
|
|
600
|
+
function roadmapRequirementIds(content, phaseNum) {
|
|
601
|
+
const text = String(content || '');
|
|
602
|
+
const sec = roadmapPhaseSection(text, phaseNum);
|
|
603
|
+
if (sec) return phaseRequirementIds(sec.section);
|
|
604
|
+
const start = text.search(new RegExp(`Phase\\s+${phaseNumRe(phaseNum)}(?![\\d.])`, 'i'));
|
|
605
|
+
if (start < 0) return [];
|
|
606
|
+
const rest = text.slice(start);
|
|
607
|
+
const next = rest.slice(1).search(/Phase\s+\d/i);
|
|
608
|
+
const span = next < 0 ? rest : rest.slice(0, next + 1);
|
|
609
|
+
return phaseRequirementIds(span.replace(/^[ \t]+(?=\*\*Requirements)/gm, ''));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Lines of requirements.md that name one of `ids`, each with its indented continuation. */
|
|
613
|
+
function requirementLines(requirements, ids) {
|
|
614
|
+
const lines = String(requirements || '').split('\n');
|
|
615
|
+
const pick = new Set();
|
|
616
|
+
const found = new Set();
|
|
617
|
+
const patterns = ids.map(id => ({ id, re: new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegex(id)}(?![A-Za-z0-9_-])`) }));
|
|
618
|
+
lines.forEach((line, i) => {
|
|
619
|
+
const hits = patterns.filter(p => p.re.test(line));
|
|
620
|
+
if (!hits.length) return;
|
|
621
|
+
for (const h of hits) found.add(h.id);
|
|
622
|
+
pick.add(i);
|
|
623
|
+
// A list item keeps its detail: the lines after it indented deeper than it.
|
|
624
|
+
if (/^\s*[-*]\s/.test(line)) {
|
|
625
|
+
const indent = line.length - line.trimStart().length;
|
|
626
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
627
|
+
const l = lines[j];
|
|
628
|
+
if (!l.trim() || l.length - l.trimStart().length <= indent) break;
|
|
629
|
+
pick.add(j);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
// A picked table row keeps its table's header and separator, so it still reads as a table.
|
|
634
|
+
for (const i of [...pick]) {
|
|
635
|
+
if (!/^\s*\|/.test(lines[i])) continue;
|
|
636
|
+
let top = i;
|
|
637
|
+
while (top > 0 && /^\s*\|/.test(lines[top - 1])) top--;
|
|
638
|
+
if (top < i) pick.add(top);
|
|
639
|
+
if (top + 1 < i && /^\s*\|[\s:|-]+\|\s*$/.test(lines[top + 1])) pick.add(top + 1);
|
|
640
|
+
}
|
|
641
|
+
return { lines: [...pick].sort((a, b) => a - b).map(i => lines[i]), missing: ids.filter(id => !found.has(id)) };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Pure: the slice of `roadmap` and `requirements` for one phase.
|
|
646
|
+
* `history` (roadmap-history.md text, or a function returning it) is consulted only
|
|
647
|
+
* when the phase's section is a `roadmap compact` stub, to put its full section back.
|
|
648
|
+
* @param {{roadmap: string, requirements: string|null, phase: string, planningRel?: string,
|
|
649
|
+
* history?: string|null|(() => string|null)}} args
|
|
650
|
+
* @returns {{found: boolean, phase_number: string, phase_name?: string, content?: string,
|
|
651
|
+
* requirement_ids?: string[], missing_requirement_ids?: string[], depends_on?: string[],
|
|
652
|
+
* tokens?: number, whole_tokens?: number}}
|
|
653
|
+
*/
|
|
654
|
+
function buildRoadmapSlice({ roadmap, requirements, phase, planningRel = '.planning', history = null }) {
|
|
655
|
+
const rm = toLf(roadmap);
|
|
656
|
+
const req = requirements == null ? null : toLf(requirements);
|
|
657
|
+
const target = roadmapPhaseSection(rm, phase);
|
|
658
|
+
if (!target) return { found: false, phase_number: String(phase) };
|
|
659
|
+
const { isCompactedSection, archivedPhaseSection } = require('./roadmap-compact.cjs');
|
|
660
|
+
if (isCompactedSection(target.section)) {
|
|
661
|
+
const full = archivedPhaseSection(typeof history === 'function' ? history() : history, phase);
|
|
662
|
+
if (full) target.section = full;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const num = unpadPhase(phase);
|
|
666
|
+
const phases = enumerateRoadmapPhases(rm);
|
|
667
|
+
// Any one-character box: roadmaps mark work in flight `[~]` or `[-]` as well as `[ ]` / `[x]`.
|
|
668
|
+
const checklist = rm.split('\n').filter(l => /^\s*-\s*\[[^\]]\]\s*\*\*Phase\s+\d/i.test(l)).map(l => l.trim());
|
|
669
|
+
const thisLine = new RegExp(`\\*\\*Phase\\s+0*${escapeRegex(num)}\\s*:`, 'i');
|
|
670
|
+
const index = (checklist.length ? checklist : phases.map(p => `- Phase ${p.number}: ${p.name}`))
|
|
671
|
+
.map(l => (thisLine.test(l) || new RegExp(`^- Phase 0*${escapeRegex(num)}:`).test(l) ? `${l} ← this phase` : l));
|
|
672
|
+
|
|
673
|
+
const deps = phaseDependencies(target.section);
|
|
674
|
+
const depLines = deps.map(d => {
|
|
675
|
+
const p = phases.find(x => unpadPhase(x.number) === d);
|
|
676
|
+
return p ? `- **Phase ${p.number}: ${p.name}** — ${p.goal || 'no goal line in roadmap.md'}` : `- **Phase ${d}** — not found in roadmap.md`;
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const ids = phaseRequirementIds(target.section);
|
|
680
|
+
let reqBody;
|
|
681
|
+
let missing = [];
|
|
682
|
+
if (req == null) reqBody = `_There is no \`${planningRel}/requirements.md\`._`;
|
|
683
|
+
else if (!ids.length) reqBody = '_The roadmap names no requirement ids for this phase._';
|
|
684
|
+
else {
|
|
685
|
+
const r = requirementLines(req, ids);
|
|
686
|
+
missing = r.missing;
|
|
687
|
+
reqBody = (r.lines.length ? r.lines.join('\n') : '') + (missing.length ? `${r.lines.length ? '\n\n' : ''}_Not found in requirements.md: ${missing.join(', ')}._` : '');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const content = [
|
|
691
|
+
`# Roadmap slice — Phase ${num}: ${target.name}`,
|
|
692
|
+
'',
|
|
693
|
+
`Generated by \`pan-tools roadmap slice ${num}\` from \`${planningRel}/roadmap.md\` and \`${planningRel}/requirements.md\`. It holds what work on this phase needs from those files; read the whole files only for something it leaves out.`,
|
|
694
|
+
'',
|
|
695
|
+
'## Every phase',
|
|
696
|
+
'',
|
|
697
|
+
...index,
|
|
698
|
+
'',
|
|
699
|
+
'## This phase',
|
|
700
|
+
'',
|
|
701
|
+
target.section,
|
|
702
|
+
'',
|
|
703
|
+
'## Phases it depends on',
|
|
704
|
+
'',
|
|
705
|
+
...(depLines.length ? depLines : ['_None._']),
|
|
706
|
+
'',
|
|
707
|
+
'## Its requirements',
|
|
708
|
+
'',
|
|
709
|
+
reqBody,
|
|
710
|
+
'',
|
|
711
|
+
].join('\n');
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
found: true,
|
|
715
|
+
phase_number: num,
|
|
716
|
+
phase_name: target.name,
|
|
717
|
+
requirement_ids: ids,
|
|
718
|
+
missing_requirement_ids: missing,
|
|
719
|
+
depends_on: deps,
|
|
720
|
+
content,
|
|
721
|
+
tokens: Math.ceil(content.length / 4),
|
|
722
|
+
whole_tokens: Math.ceil((rm.length + (req || '').length) / 4),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* `roadmap slice <phase> [--write]` — the phase's slice; `--write` puts it at
|
|
728
|
+
* `<phase dir>/<NN>-roadmap-slice.md` (the path the plan template's <context> names)
|
|
729
|
+
* and prints the path and sizes.
|
|
730
|
+
*/
|
|
731
|
+
function cmdRoadmapSlice(cwd, phase, opts, raw) {
|
|
732
|
+
const roadmap = safeReadFile(path.join(planningPath(cwd), ROADMAP_FILE));
|
|
733
|
+
if (roadmap == null) { output({ found: false, error: 'roadmap.md not found' }, raw, ''); return; }
|
|
734
|
+
const requirements = safeReadFile(path.join(planningPath(cwd), REQUIREMENTS_FILE));
|
|
735
|
+
const history = () => safeReadFile(path.join(planningPath(cwd), require('./roadmap-compact.cjs').ROADMAP_HISTORY_FILE));
|
|
736
|
+
const slice = buildRoadmapSlice({ roadmap, requirements, phase, planningRel: planningRel(), history });
|
|
737
|
+
if (!slice.found) { output(slice, raw, ''); return; }
|
|
738
|
+
const summary = {
|
|
739
|
+
found: true, phase_number: slice.phase_number, phase_name: slice.phase_name,
|
|
740
|
+
requirement_ids: slice.requirement_ids, missing_requirement_ids: slice.missing_requirement_ids,
|
|
741
|
+
depends_on: slice.depends_on, tokens: slice.tokens, whole_tokens: slice.whole_tokens,
|
|
742
|
+
smaller_pct: slice.whole_tokens ? Math.round(100 * (1 - slice.tokens / slice.whole_tokens)) : 0,
|
|
743
|
+
};
|
|
744
|
+
if (!opts || !opts.write) { output({ ...summary, content: slice.content }, raw, slice.content); return; }
|
|
745
|
+
|
|
746
|
+
const dir = findPhaseInternal(cwd, phase);
|
|
747
|
+
if (!dir || !dir.directory) {
|
|
748
|
+
output({ ...summary, written: false, reason: 'no phase directory yet — create it first, or read the whole files' }, raw, '');
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const rel = `${dir.directory}/${normalizePhaseName(phase)}${ROADMAP_SLICE_SUFFIX}`;
|
|
752
|
+
const abs = path.join(cwd, rel);
|
|
753
|
+
// Same bytes in the file's own line ending, and no write when nothing changed,
|
|
754
|
+
// so regenerating at each workflow start leaves git alone.
|
|
755
|
+
const existing = safeReadFile(abs);
|
|
756
|
+
const next = withEol(slice.content, existing == null ? '\n' : dominantEol(existing));
|
|
757
|
+
if (existing !== next) fs.writeFileSync(abs, next, 'utf-8');
|
|
758
|
+
output({ ...summary, written: true, changed: existing !== next, path: rel }, raw, rel);
|
|
759
|
+
}
|
|
760
|
+
|
|
533
761
|
module.exports = {
|
|
534
762
|
cmdRoadmapGetPhase,
|
|
535
763
|
cmdRoadmapAnalyze,
|
|
536
764
|
cmdRoadmapUpdatePlanProgress,
|
|
765
|
+
cmdRoadmapSlice,
|
|
537
766
|
syncRequirementCheckboxes,
|
|
538
767
|
// Exported for testability
|
|
539
768
|
enumerateRoadmapPhases,
|
|
540
769
|
enrichPhaseWithDiskStatus,
|
|
541
770
|
extractMilestones,
|
|
542
771
|
computeRoadmapStats,
|
|
772
|
+
buildRoadmapSlice,
|
|
773
|
+
roadmapRequirementIds,
|
|
774
|
+
phaseRequirementIds,
|
|
775
|
+
phaseDependencies,
|
|
776
|
+
ROADMAP_SLICE_SUFFIX,
|
|
543
777
|
};
|
|
@@ -38,7 +38,8 @@ const { getExperimentManifest, PAN_EXPERIMENTS_ROOT_DEFAULT } = require('./exper
|
|
|
38
38
|
* shell: 'win32' — they default to direct spawn, which suits test mocks like
|
|
39
39
|
* `node -e '...'` that are resolvable directly. P-102 fix (v3.7.1).
|
|
40
40
|
*
|
|
41
|
-
* GitHub Copilot CLI
|
|
41
|
+
* GitHub Copilot CLI is null: no adapter is written yet, although `copilot -p`
|
|
42
|
+
* runs a prompt headless (the PAN Harness drives it that way).
|
|
42
43
|
*/
|
|
43
44
|
// P-1302 fix (v3.7.2): autonomous claude/gemini runs default to non-interactive
|
|
44
45
|
// permissions. Without these flags, the CLI prompts for tool approval, which
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
const fs = require('fs');
|
|
28
28
|
const path = require('path');
|
|
29
|
-
const { output, error } = require('./core.cjs');
|
|
29
|
+
const { output, error, dominantEol, withEol } = require('./core.cjs');
|
|
30
30
|
const {
|
|
31
31
|
STATE_FILE,
|
|
32
32
|
STATE_HISTORY_FILE,
|
|
@@ -210,9 +210,13 @@ function planStateCompaction(cwd, opts = {}) {
|
|
|
210
210
|
// Build the post-compaction text for real rather than estimating it. An
|
|
211
211
|
// estimate that ignores pointer overhead can claim a saving on a file where
|
|
212
212
|
// the pointers cost more than the sections they replace — a dry-run that
|
|
213
|
-
// overstates its own benefit is worse than no dry-run.
|
|
213
|
+
// overstates its own benefit is worse than no dry-run. The rebuild comes back
|
|
214
|
+
// LF and writeStateMd restores the file's own ending on disk, so measure it in
|
|
215
|
+
// that ending too: on a CRLF file the LF rebuild otherwise "saved" a byte per
|
|
216
|
+
// line, and a compaction that archived nothing still reported tokens saved.
|
|
214
217
|
const stamp = new Date(now).toISOString().slice(0, 10);
|
|
215
|
-
|
|
218
|
+
const eol = dominantEol(content);
|
|
219
|
+
let bytesAfter = withEol(rebuildState(content, new Set(archivable.map(v => v.title)), stamp), eol).length;
|
|
216
220
|
|
|
217
221
|
// The point of compaction is a smaller re-read on every agent call. On a file
|
|
218
222
|
// whose history sections are tiny, the pointers left behind cost more than the
|
|
@@ -291,14 +295,15 @@ function compactState(cwd, opts = {}) {
|
|
|
291
295
|
const { writeStateMd } = require('./state.cjs');
|
|
292
296
|
writeStateMd(statePath, next, cwd);
|
|
293
297
|
|
|
298
|
+
const onDisk = withEol(next, dominantEol(content));
|
|
294
299
|
return {
|
|
295
300
|
...plan,
|
|
296
301
|
applied: true,
|
|
297
302
|
dry_run: false,
|
|
298
303
|
archived: plan.archivable.map(v => v.title),
|
|
299
|
-
bytes_after:
|
|
300
|
-
tokens_after: Math.ceil(
|
|
301
|
-
tokens_saved_per_call: Math.max(0, Math.ceil((plan.bytes_before -
|
|
304
|
+
bytes_after: onDisk.length,
|
|
305
|
+
tokens_after: Math.ceil(onDisk.length / CHARS_PER_TOKEN),
|
|
306
|
+
tokens_saved_per_call: Math.max(0, Math.ceil((plan.bytes_before - onDisk.length) / CHARS_PER_TOKEN)),
|
|
302
307
|
};
|
|
303
308
|
}
|
|
304
309
|
|
|
@@ -294,22 +294,22 @@ function cmdStateRecordMetric(cwd, options, raw) {
|
|
|
294
294
|
return;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
//
|
|
298
|
-
|
|
297
|
+
// The Performance Metrics section's first table: its rows only, then an optional
|
|
298
|
+
// "None yet" line. Taking the rest of the section as the table body put every row
|
|
299
|
+
// below the template's "Recent Trend" and footer lines, outside the table. The
|
|
300
|
+
// separator is matched on one line: `[-|\s]+` ran on into a `| - | - |` row.
|
|
301
|
+
const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|: \t]+\r?\n)((?:\|[^\n]*(?:\n|$))*)((?:[^\S\n]*None yet\.?[^\S\n]*(?:\n|$))?)/i;
|
|
299
302
|
const metricsMatch = content.match(metricsPattern);
|
|
300
303
|
|
|
301
304
|
if (metricsMatch) {
|
|
302
|
-
let tableBody = metricsMatch[2].trimEnd();
|
|
303
305
|
const cell = escapeTableCell;
|
|
304
306
|
const newRow = `| Phase ${cell(phase)} P${cell(plan)} | ${cell(duration)} | ${cell(tasks || '-')} tasks | ${cell(files || '-')} files |`;
|
|
307
|
+
// A template placeholder row (`| - | - | - | - |`, `| None yet | | | |`) gives way.
|
|
308
|
+
const isPlaceholder = (row) => /None yet/i.test(row) || row.split('|').slice(1, -1).every((c) => /^\s*-?\s*$/.test(c));
|
|
309
|
+
const rows = metricsMatch[2].split('\n').map((r) => r.replace(/\r$/, '')).filter((r) => r.trim() && !isPlaceholder(r));
|
|
310
|
+
rows.push(newRow);
|
|
305
311
|
|
|
306
|
-
|
|
307
|
-
tableBody = newRow;
|
|
308
|
-
} else {
|
|
309
|
-
tableBody = tableBody + '\n' + newRow;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
content = content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
|
|
312
|
+
content = content.replace(metricsPattern, (_match, header) => `${header}${rows.join('\n')}\n`);
|
|
313
313
|
writeStateMd(statePath, content, cwd);
|
|
314
314
|
output({ recorded: true, phase, plan, duration }, raw, 'true');
|
|
315
315
|
} else {
|
|
@@ -432,7 +432,8 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
432
432
|
|
|
433
433
|
if (match) {
|
|
434
434
|
let sectionBody = match[2];
|
|
435
|
-
|
|
435
|
+
// "None yet" first: stripping "None" first left a stray "yet." on the template.
|
|
436
|
+
sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, '').replace(/None\.?\s*\n?/gi, '');
|
|
436
437
|
sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
|
|
437
438
|
content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
|
|
438
439
|
writeStateMd(statePath, content, cwd);
|
|
@@ -538,14 +539,23 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
538
539
|
// --- Snapshot Parsers --------------------------------------------------------
|
|
539
540
|
|
|
540
541
|
/**
|
|
541
|
-
* Parse the
|
|
542
|
-
*
|
|
543
|
-
*
|
|
542
|
+
* Parse the decisions from state.md content: the `- [Phase N]: summary — rationale`
|
|
543
|
+
* bullets `state add-decision` appends under `### Decisions` (the template's
|
|
544
|
+
* heading), or else an older "## Decisions Made" table. Reading only the table
|
|
545
|
+
* left `decisions` empty on every state.md PAN itself writes.
|
|
544
546
|
* @param {string} content - Full state.md content
|
|
545
547
|
* @returns {Array<{phase: string, summary: string, rationale: string}>}
|
|
546
548
|
*/
|
|
547
549
|
function parseDecisionsFromState(content) {
|
|
548
550
|
const decisions = [];
|
|
551
|
+
const section = content.match(/###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n([\s\S]*?)(?=\n###?|\n##[^#]|$)/i);
|
|
552
|
+
for (const line of section ? section[1].split('\n') : []) {
|
|
553
|
+
const m = line.match(/^-\s+\[Phase\s+([^\]]*)\]:\s*(.+?)\s*$/i);
|
|
554
|
+
if (!m) continue;
|
|
555
|
+
const [summary, ...rest] = m[2].split(' — ');
|
|
556
|
+
decisions.push({ phase: m[1].trim(), summary: summary.trim(), rationale: rest.join(' — ').trim() });
|
|
557
|
+
}
|
|
558
|
+
if (decisions.length > 0) return decisions;
|
|
549
559
|
// Match the decisions table body after header row and separator row
|
|
550
560
|
const decisionsMatch = content.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i);
|
|
551
561
|
if (decisionsMatch) {
|
|
@@ -567,14 +577,15 @@ function parseDecisionsFromState(content) {
|
|
|
567
577
|
|
|
568
578
|
/**
|
|
569
579
|
* Parse the Blockers section from state.md content.
|
|
570
|
-
* Extracts bullet-point items (lines starting with "- ") from under
|
|
571
|
-
*
|
|
580
|
+
* Extracts bullet-point items (lines starting with "- ") from under the heading
|
|
581
|
+
* `state add-blocker` writes to: `### Blockers/Concerns` in the template, or
|
|
582
|
+
* `Blockers` / `Concerns`. Matching only "## Blockers" left the list empty.
|
|
572
583
|
* @param {string} content - Full state.md content
|
|
573
584
|
* @returns {string[]} Array of blocker text strings
|
|
574
585
|
*/
|
|
575
586
|
function parseBlockersFromState(content) {
|
|
576
587
|
const blockers = [];
|
|
577
|
-
const blockersMatch = content.match(
|
|
588
|
+
const blockersMatch = content.match(/###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n([\s\S]*?)(?=\n###?|\n##[^#]|$)/i);
|
|
578
589
|
if (blockersMatch) {
|
|
579
590
|
const blockersSection = blockersMatch[1];
|
|
580
591
|
const items = blockersSection.match(/^-\s+(.+)$/gm) || [];
|
|
@@ -585,6 +596,18 @@ function parseBlockersFromState(content) {
|
|
|
585
596
|
return blockers;
|
|
586
597
|
}
|
|
587
598
|
|
|
599
|
+
/**
|
|
600
|
+
* The blockers that are still open: the section's bullets minus a "None" placeholder.
|
|
601
|
+
* `preflight` and `dashboard` read a `## Blockers` heading of their own, which the
|
|
602
|
+
* state template never writes (it writes `### Blockers/Concerns`, where
|
|
603
|
+
* `state add-blocker` puts its bullets), so both reported no blockers with one open.
|
|
604
|
+
* @param {string} content - Full state.md content
|
|
605
|
+
* @returns {string[]}
|
|
606
|
+
*/
|
|
607
|
+
function activeBlockersFromState(content) {
|
|
608
|
+
return parseBlockersFromState(content).filter((b) => b && !/^none(\s+yet)?\.?$/i.test(b));
|
|
609
|
+
}
|
|
610
|
+
|
|
588
611
|
/**
|
|
589
612
|
* Parse the Session section from state.md content.
|
|
590
613
|
* Extracts **Last Date:**, **Stopped At:**, and **Resume File:** bold-field
|
|
@@ -1029,17 +1052,8 @@ function cmdDashboard(cwd, raw) {
|
|
|
1029
1052
|
lastActivity = stateExtractField(stateContent, 'Last Activity');
|
|
1030
1053
|
lastActivityDesc = stateExtractField(stateContent, 'Last Activity Description');
|
|
1031
1054
|
|
|
1032
|
-
// Parse blockers
|
|
1033
|
-
|
|
1034
|
-
if (blockersMatch) {
|
|
1035
|
-
const items = blockersMatch[1].match(/^-\s+(.+)$/gm) || [];
|
|
1036
|
-
for (const item of items) {
|
|
1037
|
-
const text = item.replace(/^-\s+/, '').trim();
|
|
1038
|
-
if (text && !/^none$/i.test(text)) {
|
|
1039
|
-
activeBlockers.push(text);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1055
|
+
// Parse blockers (the template's `### Blockers/Concerns`, or a legacy `## Blockers`)
|
|
1056
|
+
activeBlockers.push(...activeBlockersFromState(stateContent));
|
|
1043
1057
|
blockerCount = activeBlockers.length;
|
|
1044
1058
|
}
|
|
1045
1059
|
|
|
@@ -1111,6 +1125,7 @@ function cmdDashboard(cwd, raw) {
|
|
|
1111
1125
|
|
|
1112
1126
|
module.exports = {
|
|
1113
1127
|
readStateSafe: safeReadFile,
|
|
1128
|
+
activeBlockersFromState,
|
|
1114
1129
|
stateExtractField,
|
|
1115
1130
|
stateReplaceField,
|
|
1116
1131
|
writeStateMd,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const {
|
|
8
|
-
PHASES_DIR, PLAN_SUFFIX, SUMMARY_SUFFIX, VERIFICATION_SUFFIX,
|
|
8
|
+
PHASES_DIR, PLAN_SUFFIX, SUMMARY_SUFFIX, VERIFICATION_SUFFIX, ROADMAP_SLICE_SUFFIX,
|
|
9
9
|
SIMPLE_TASK_THRESHOLD, SIMPLE_FILE_THRESHOLD, COMPLEX_TASK_THRESHOLD, COMPLEX_FILE_THRESHOLD,
|
|
10
10
|
} = require('./constants.cjs');
|
|
11
11
|
const { planningPath, phasesPath } = require('./utils.cjs');
|
|
@@ -86,7 +86,7 @@ function cmdTemplateSelect(cwd, planPath, raw) {
|
|
|
86
86
|
* @param {string} phaseName - Human-readable phase name
|
|
87
87
|
* @param {string} phaseSlug - Phase slug for directory naming
|
|
88
88
|
* @param {string} phaseNum - Raw phase number from options
|
|
89
|
-
* @param {Object} options - Additional options (type, wave, fields)
|
|
89
|
+
* @param {Object} options - Additional options (type, wave, fields, slicePath)
|
|
90
90
|
* @returns {{frontmatter: Object, body: string, fileName: string}}
|
|
91
91
|
*/
|
|
92
92
|
function generatePlanTemplate(phaseId, planId, phaseName, phaseSlug, phaseNum, options) {
|
|
@@ -94,6 +94,10 @@ function generatePlanTemplate(phaseId, planId, phaseName, phaseSlug, phaseNum, o
|
|
|
94
94
|
const wave = parseInt(options.wave, 10) || 1;
|
|
95
95
|
const fields = options.fields || {};
|
|
96
96
|
const padded = phaseId.split('-')[0]; // e.g., "01" from "01-setup-auth"
|
|
97
|
+
// The <context> names the phase's roadmap slice, as templates/phase-prompt.md
|
|
98
|
+
// does (O2): `@.planning/roadmap.md` handed every executor the whole roadmap.
|
|
99
|
+
// cmdTemplateFill passes the phase's real directory.
|
|
100
|
+
const slicePath = options.slicePath || `.planning/phases/${phaseId}/${padded}${ROADMAP_SLICE_SUFFIX}`;
|
|
97
101
|
|
|
98
102
|
const frontmatter = {
|
|
99
103
|
phase: phaseId,
|
|
@@ -121,7 +125,7 @@ function generatePlanTemplate(phaseId, planId, phaseName, phaseSlug, phaseNum, o
|
|
|
121
125
|
'',
|
|
122
126
|
'## Context',
|
|
123
127
|
'@.planning/project.md',
|
|
124
|
-
|
|
128
|
+
`@${slicePath}`,
|
|
125
129
|
'@.planning/state.md',
|
|
126
130
|
'',
|
|
127
131
|
'## Tasks',
|
|
@@ -286,7 +290,10 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
|
|
|
286
290
|
generated = generateSummaryTemplate(phaseId, planNum, phaseName, options.phase, fields);
|
|
287
291
|
break;
|
|
288
292
|
case 'plan':
|
|
289
|
-
generated = generatePlanTemplate(phaseId, planNum, phaseName, phaseSlug, options.phase,
|
|
293
|
+
generated = generatePlanTemplate(phaseId, planNum, phaseName, phaseSlug, options.phase, {
|
|
294
|
+
...options,
|
|
295
|
+
slicePath: toPosix(path.join(phaseInfo.directory, `${padded}${ROADMAP_SLICE_SUFFIX}`)),
|
|
296
|
+
});
|
|
290
297
|
break;
|
|
291
298
|
case 'verification':
|
|
292
299
|
generated = generateVerificationTemplate(phaseId, phaseName, options.phase, fields);
|