pan-wizard 3.32.0 → 3.34.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 +23 -20
- package/agents/pan-conductor.md +18 -17
- package/agents/pan-counterfactual.md +2 -2
- package/agents/pan-debugger.md +27 -8
- package/agents/pan-design-checker.md +1 -1
- package/agents/pan-designer.md +2 -2
- package/agents/pan-distiller.md +3 -3
- package/agents/pan-document_code.md +16 -16
- package/agents/pan-executor.md +15 -11
- package/agents/pan-experiment-runner.md +15 -17
- package/agents/pan-hardener.md +15 -15
- package/agents/pan-integration-checker.md +2 -2
- package/agents/pan-knowledge.md +2 -2
- package/agents/pan-meta-reviewer.md +5 -5
- package/agents/pan-optimizer.md +40 -44
- package/agents/pan-phase-researcher.md +10 -10
- package/agents/pan-plan-checker.md +36 -30
- package/agents/pan-planner.md +77 -53
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-project-researcher.md +21 -78
- package/agents/pan-release.md +1 -1
- package/agents/pan-research-synthesizer.md +2 -2
- package/agents/pan-reviewer.md +24 -2
- package/agents/pan-roadmapper.md +20 -62
- package/agents/pan-verifier.md +53 -30
- package/bin/install-lib.cjs +228 -38
- package/bin/install.js +86 -15
- package/commands/pan/army.md +16 -12
- package/commands/pan/assumptions.md +2 -2
- package/commands/pan/audit-deployment.md +11 -12
- package/commands/pan/cost.md +8 -4
- package/commands/pan/debug.md +10 -7
- package/commands/pan/design-phase.md +3 -3
- package/commands/pan/discuss-phase.md +2 -0
- package/commands/pan/exec-phase.md +21 -37
- package/commands/pan/experiment.md +11 -10
- package/commands/pan/focus-auto.md +33 -27
- package/commands/pan/focus-design.md +9 -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 +7 -7
- package/commands/pan/focus-plan.md +12 -11
- package/commands/pan/focus-scan.md +7 -10
- 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 +3 -2
- package/commands/pan/hygiene.md +14 -7
- package/commands/pan/knowledge.md +9 -9
- package/commands/pan/learn.md +10 -7
- package/commands/pan/links.md +4 -1
- package/commands/pan/map-codebase.md +5 -5
- package/commands/pan/mcp-bridge.md +18 -18
- package/commands/pan/milestone-done.md +7 -5
- package/commands/pan/milestone-new.md +1 -1
- package/commands/pan/new-project.md +10 -10
- package/commands/pan/optimize.md +16 -14
- package/commands/pan/patches.md +2 -6
- package/commands/pan/pause.md +1 -1
- package/commands/pan/phase-tests.md +4 -3
- package/commands/pan/plan-phase.md +9 -19
- package/commands/pan/preview.md +1 -1
- package/commands/pan/profile.md +4 -4
- package/commands/pan/quick.md +1 -1
- package/commands/pan/remove-phase.md +1 -1
- package/commands/pan/report.md +2 -2
- package/commands/pan/research-phase.md +14 -15
- package/commands/pan/resume.md +9 -11
- package/commands/pan/retro.md +5 -3
- package/commands/pan/review-deep.md +10 -10
- package/commands/pan/settings.md +1 -1
- package/commands/pan/todo-add.md +2 -2
- package/commands/pan/todo-check.md +2 -2
- package/commands/pan/verify-phase.md +0 -11
- package/commands/pan/what-if.md +2 -2
- package/hooks/dist/pan-context-monitor.js +343 -44
- package/hooks/dist/pan-cost-logger.js +30 -0
- package/hooks/dist/pan-state-reinject.js +140 -15
- package/hooks/dist/pan-statusline.js +68 -22
- package/hooks/dist/pan-stop-guard.js +7 -2
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +48 -7
- 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 +126 -5
- package/pan-wizard-core/bin/lib/config.cjs +112 -1
- package/pan-wizard-core/bin/lib/constants.cjs +29 -15
- package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
- package/pan-wizard-core/bin/lib/core.cjs +77 -23
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
- package/pan-wizard-core/bin/lib/cost.cjs +149 -10
- 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 +15 -4
- 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 +178 -17
- package/pan-wizard-core/bin/lib/init.cjs +12 -5
- 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 +84 -16
- 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 +67 -15
- package/pan-wizard-core/bin/lib/preview.cjs +3 -1
- package/pan-wizard-core/bin/lib/review-deep.cjs +2 -2
- 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 +47 -31
- package/pan-wizard-core/bin/lib/template.cjs +11 -4
- package/pan-wizard-core/bin/lib/verdict.cjs +54 -4
- package/pan-wizard-core/bin/lib/verify-drift.cjs +14 -5
- 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 +74 -4
- package/pan-wizard-core/bin/lib/verify.cjs +117 -35
- package/pan-wizard-core/bin/lib/whatif.cjs +5 -3
- package/pan-wizard-core/bin/pan-tools.cjs +111 -17
- package/pan-wizard-core/learnings/index.json +5 -5
- package/pan-wizard-core/mcp/server.cjs +18 -9
- package/pan-wizard-core/opencode/pan-wizard.js +111 -0
- package/pan-wizard-core/references/checkpoints.md +7 -5
- 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 +14 -9
- package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
- package/pan-wizard-core/references/planning-config.md +4 -4
- package/pan-wizard-core/references/tdd.md +10 -1
- package/pan-wizard-core/references/verdict-contract.md +2 -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/continue-here.md +1 -1
- package/pan-wizard-core/templates/debug-subagent-prompt.md +3 -2
- package/pan-wizard-core/templates/debug.md +15 -1
- package/pan-wizard-core/templates/discovery.md +1 -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 -18
- package/pan-wizard-core/templates/planner-subagent-prompt.md +3 -5
- 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/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 +54 -4
- package/pan-wizard-core/workflows/add-phase.md +1 -1
- package/pan-wizard-core/workflows/assumptions.md +1 -1
- package/pan-wizard-core/workflows/discuss-phase.md +9 -8
- package/pan-wizard-core/workflows/exec-phase.md +34 -60
- package/pan-wizard-core/workflows/execute-plan.md +8 -7
- package/pan-wizard-core/workflows/health.md +13 -11
- 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 +17 -24
- package/pan-wizard-core/workflows/milestone-new.md +7 -6
- package/pan-wizard-core/workflows/new-project.md +10 -10
- 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 +31 -34
- package/pan-wizard-core/workflows/profile.md +2 -2
- package/pan-wizard-core/workflows/progress.md +5 -7
- package/pan-wizard-core/workflows/quick.md +10 -8
- 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 +1 -1
- package/pan-wizard-core/workflows/transition.md +12 -12
- package/pan-wizard-core/workflows/update.md +25 -17
- package/pan-wizard-core/workflows/verify-phase.md +24 -17
- 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-agent-plugin.js +19 -5
- package/scripts/build-plugin.js +3 -1
- package/scripts/generate-skills-docs.py +13 -4
- package/scripts/test-surface.cjs +4 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* roadmap compact — move shipped phases' detail out of roadmap.md (memory optimisation O3).
|
|
5
|
+
*
|
|
6
|
+
* roadmap.md only grows: every phase adds a detail section (goal, success criteria,
|
|
7
|
+
* plan list, notes) and nothing ever takes one away. A 54-phase field project's
|
|
8
|
+
* roadmap reached 476 KB, about 121k tokens. Phase agents now read a slice of it
|
|
9
|
+
* (`roadmap slice`, O2), but the roadmapper, the milestone workflows and people
|
|
10
|
+
* still read it whole. This bounds it the way `state compact` bounds state.md
|
|
11
|
+
* (ADR-0044).
|
|
12
|
+
*
|
|
13
|
+
* Safety, in order of importance:
|
|
14
|
+
* 1. Nothing is deleted. Sections are appended to roadmap-history.md FIRST, and
|
|
15
|
+
* only then shortened in roadmap.md, so an interruption can only duplicate.
|
|
16
|
+
* 2. Only a shipped phase moves: its checklist line is ticked, it is not the
|
|
17
|
+
* current phase in state.md, and it is not one of the `keep` most recently
|
|
18
|
+
* shipped phases (default 2), which the next phase's planning may lean on.
|
|
19
|
+
* 3. A stub stays: the heading, the Goal, Depends on and Requirements lines and
|
|
20
|
+
* any `<!-- model_tier: … -->` override, so goal lookups, the dependency goals
|
|
21
|
+
* in a slice and `roadmap analyze` still read the phase. Readers that need the
|
|
22
|
+
* rest (`roadmap get-phase`, `phase report`, a slice of that phase) find the
|
|
23
|
+
* stub and read the section from roadmap-history.md.
|
|
24
|
+
* 4. A section already compacted is never compacted again.
|
|
25
|
+
* 5. Dry-run by default; `--apply` writes. Declines when nothing would shrink.
|
|
26
|
+
* 6. Parsed on LF and written back in the file's own line ending.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const { ROADMAP_FILE, STATE_FILE, CHARS_PER_TOKEN } = require('./constants.cjs');
|
|
32
|
+
const { planningPath, planningRel } = require('./utils.cjs');
|
|
33
|
+
const { output, safeReadFile, toLf, dominantEol, withEol } = require('./core.cjs');
|
|
34
|
+
|
|
35
|
+
const ROADMAP_HISTORY_FILE = 'roadmap-history.md';
|
|
36
|
+
const ROADMAP_COMPACT_KEEP = 2;
|
|
37
|
+
/** The line a stub carries in place of the section's body. */
|
|
38
|
+
const STUB_MARK = '_Shipped; the full section is in [roadmap-history.md]';
|
|
39
|
+
|
|
40
|
+
const PHASE_HEADING_RE = /^(#{2,4})\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*(.+?)\s*$/;
|
|
41
|
+
const KEPT_LINE_RE = /^(?:\*\*(Goal|Depends on|Requirements)(?::\*\*|\*\*:)|<!--\s*(model_tier):)/i;
|
|
42
|
+
/** The line each compaction writes into the history ahead of the sections it moved. */
|
|
43
|
+
const HISTORY_BATCH_MARK = `<!-- compacted from ${ROADMAP_FILE} on `;
|
|
44
|
+
const unpad = (n) => String(n).replace(/^0+(?=\d)/, '');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The phase detail sections of roadmap.md (LF text): each runs from its heading to
|
|
48
|
+
* the next phase heading or the next heading at its level or above, or to an HTML
|
|
49
|
+
* block boundary such as a milestone's `</details>`, so a trailing progress table
|
|
50
|
+
* never rides along. In roadmap-history.md a section also ends at the next
|
|
51
|
+
* compaction's batch line. Trailing blank lines stay outside the section.
|
|
52
|
+
*/
|
|
53
|
+
function phaseSections(lines) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (let i = 0; i < lines.length; i++) {
|
|
56
|
+
const m = lines[i].match(PHASE_HEADING_RE);
|
|
57
|
+
if (!m) continue;
|
|
58
|
+
const level = m[1].length;
|
|
59
|
+
let end = i + 1;
|
|
60
|
+
for (; end < lines.length; end++) {
|
|
61
|
+
const h = lines[end].match(/^(#{1,6})\s/);
|
|
62
|
+
if (h && (h[1].length <= level || PHASE_HEADING_RE.test(lines[end]))) break;
|
|
63
|
+
if (/^\s*<\/?details\b/i.test(lines[end])) break;
|
|
64
|
+
if (lines[end].startsWith(HISTORY_BATCH_MARK)) break;
|
|
65
|
+
}
|
|
66
|
+
let last = end;
|
|
67
|
+
while (last > i + 1 && !lines[last - 1].trim()) last--;
|
|
68
|
+
out.push({ start: i, end: last, number: unpad(m[2]), title: m[3], heading: lines[i] });
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Phase numbers whose checklist line is ticked (`- [x] **Phase 3: …`). */
|
|
74
|
+
function shippedPhases(lines) {
|
|
75
|
+
const s = new Set();
|
|
76
|
+
for (const l of lines) {
|
|
77
|
+
const m = l.match(/^\s*-\s*\[[xX]\]\s*\*{0,2}Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/);
|
|
78
|
+
if (m) s.add(unpad(m[1]));
|
|
79
|
+
}
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The current phase named in state.md, or null. */
|
|
84
|
+
function currentPhase(stateText) {
|
|
85
|
+
const t = toLf(stateText || '');
|
|
86
|
+
const m = t.match(/\*\*Current Phase:\*\*\s*([0-9]+[A-Z]?(?:\.[0-9]+)*)/i) || t.match(/^current_phase:\s*"?([0-9]+[A-Z]?(?:\.[0-9]+)*)/m);
|
|
87
|
+
return m ? unpad(m[1]) : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The stub left in roadmap.md for an archived section. */
|
|
91
|
+
function stubFor(sectionLines, stamp) {
|
|
92
|
+
const kept = [];
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
for (const l of sectionLines.slice(1)) {
|
|
95
|
+
const m = l.match(KEPT_LINE_RE);
|
|
96
|
+
const key = m && (m[1] || m[2]).toLowerCase();
|
|
97
|
+
if (m && !seen.has(key)) { seen.add(key); kept.push(l); }
|
|
98
|
+
}
|
|
99
|
+
return [sectionLines[0], ...kept, '', `${STUB_MARK}(${ROADMAP_HISTORY_FILE}) (compacted ${stamp})._`];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pure: plan (and build) a compaction of roadmap.md text.
|
|
104
|
+
* @param {string} roadmap - roadmap.md content
|
|
105
|
+
* @param {{keep?: number, current?: string|null, now?: number}} [opts]
|
|
106
|
+
* @returns {{sections: Array, archivable: Array, content: string, archivedText: string, bytes_before: number, bytes_after: number}}
|
|
107
|
+
*/
|
|
108
|
+
function planRoadmapCompaction(roadmap, opts = {}) {
|
|
109
|
+
const keep = Number.isInteger(opts.keep) && opts.keep >= 0 ? opts.keep : ROADMAP_COMPACT_KEEP;
|
|
110
|
+
const eol = dominantEol(roadmap);
|
|
111
|
+
const lines = toLf(roadmap).split('\n');
|
|
112
|
+
const shipped = shippedPhases(lines);
|
|
113
|
+
const sections = phaseSections(lines);
|
|
114
|
+
const stamp = new Date(opts.now || Date.now()).toISOString().slice(0, 10);
|
|
115
|
+
|
|
116
|
+
const isStub = (s) => lines.slice(s.start, s.end).some(l => l.startsWith(STUB_MARK));
|
|
117
|
+
const candidates = sections.filter(s => shipped.has(s.number) && !isStub(s) && s.number !== opts.current);
|
|
118
|
+
const keepSet = new Set(candidates.slice(Math.max(0, candidates.length - keep)).map(s => s.start));
|
|
119
|
+
|
|
120
|
+
const verdicts = sections.map(s => {
|
|
121
|
+
let archive = false;
|
|
122
|
+
let reason;
|
|
123
|
+
if (isStub(s)) reason = 'already compacted';
|
|
124
|
+
else if (!shipped.has(s.number)) reason = 'not shipped — its checklist line is not ticked';
|
|
125
|
+
else if (s.number === opts.current) reason = 'the current phase in state.md';
|
|
126
|
+
else if (keepSet.has(s.start)) reason = `one of the ${keep} most recently shipped phases`;
|
|
127
|
+
else { archive = true; reason = 'shipped'; }
|
|
128
|
+
return { phase: s.number, title: s.title, bytes: lines.slice(s.start, s.end).join('\n').length, archive, reason, start: s.start, end: s.end };
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const toArchive = verdicts.filter(v => v.archive);
|
|
132
|
+
const next = [];
|
|
133
|
+
let cursor = 0;
|
|
134
|
+
for (const v of toArchive) {
|
|
135
|
+
next.push(...lines.slice(cursor, v.start), ...stubFor(lines.slice(v.start, v.end), stamp));
|
|
136
|
+
cursor = v.end;
|
|
137
|
+
}
|
|
138
|
+
next.push(...lines.slice(cursor));
|
|
139
|
+
const content = withEol(next.join('\n'), eol);
|
|
140
|
+
const archivedText = toArchive.map(v => lines.slice(v.start, v.end).join('\n')).join('\n\n');
|
|
141
|
+
|
|
142
|
+
let archivable = toArchive.map(({ start, end, ...rest }) => rest);
|
|
143
|
+
let after = content.length;
|
|
144
|
+
// A stub on a tiny section can cost more than the section: decline rather than
|
|
145
|
+
// churn roadmap.md to make it bigger.
|
|
146
|
+
if (archivable.length && after >= roadmap.length) {
|
|
147
|
+
for (const v of verdicts) if (v.archive) { v.archive = false; v.reason = 'archiving it would not shrink roadmap.md'; }
|
|
148
|
+
archivable = [];
|
|
149
|
+
after = roadmap.length;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
sections: verdicts.map(({ start, end, ...rest }) => rest),
|
|
153
|
+
archivable,
|
|
154
|
+
content: archivable.length ? content : roadmap,
|
|
155
|
+
archivedText: archivable.length ? archivedText : '',
|
|
156
|
+
bytes_before: roadmap.length,
|
|
157
|
+
bytes_after: after,
|
|
158
|
+
stamp,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Plan or apply `roadmap compact` for a project. Archive is written BEFORE
|
|
164
|
+
* roadmap.md is rewritten, so an interruption can only ever leave a duplicate.
|
|
165
|
+
* @param {string} cwd
|
|
166
|
+
* @param {{apply?: boolean, keep?: number, now?: number}} [opts]
|
|
167
|
+
*/
|
|
168
|
+
function compactRoadmap(cwd, opts = {}) {
|
|
169
|
+
const roadmapPath = planningPath(cwd, ROADMAP_FILE);
|
|
170
|
+
const roadmap = safeReadFile(roadmapPath);
|
|
171
|
+
if (roadmap == null) return { found: false, path: planningRel(ROADMAP_FILE), archivable: [], applied: false };
|
|
172
|
+
const current = currentPhase(safeReadFile(planningPath(cwd, STATE_FILE)));
|
|
173
|
+
const plan = planRoadmapCompaction(roadmap, { keep: opts.keep, current, now: opts.now });
|
|
174
|
+
const result = {
|
|
175
|
+
found: true,
|
|
176
|
+
path: planningRel(ROADMAP_FILE),
|
|
177
|
+
history_path: planningRel(ROADMAP_HISTORY_FILE),
|
|
178
|
+
keep: Number.isInteger(opts.keep) && opts.keep >= 0 ? opts.keep : ROADMAP_COMPACT_KEEP,
|
|
179
|
+
current_phase: current,
|
|
180
|
+
sections: plan.sections,
|
|
181
|
+
archivable: plan.archivable,
|
|
182
|
+
bytes_before: plan.bytes_before,
|
|
183
|
+
bytes_after: plan.bytes_after,
|
|
184
|
+
tokens_before: Math.ceil(plan.bytes_before / CHARS_PER_TOKEN),
|
|
185
|
+
tokens_after: Math.ceil(plan.bytes_after / CHARS_PER_TOKEN),
|
|
186
|
+
applied: false,
|
|
187
|
+
dry_run: !opts.apply,
|
|
188
|
+
};
|
|
189
|
+
if (!opts.apply || !plan.archivable.length) return result;
|
|
190
|
+
|
|
191
|
+
// 1. History first. The preamble is created with an exclusive open (no
|
|
192
|
+
// check-then-write race); appends follow the history file's own line ending.
|
|
193
|
+
const historyPath = planningPath(cwd, ROADMAP_HISTORY_FILE);
|
|
194
|
+
const preamble = `# Roadmap history\n\nPhase sections compacted out of ${ROADMAP_FILE} by \`pan-tools roadmap compact\`.\n`
|
|
195
|
+
+ 'They are kept verbatim; roadmap.md keeps a stub with each heading, goal, dependencies and requirements.\n';
|
|
196
|
+
try {
|
|
197
|
+
fs.writeFileSync(historyPath, preamble, { flag: 'wx', encoding: 'utf-8' });
|
|
198
|
+
} catch (e) {
|
|
199
|
+
if (e.code !== 'EEXIST') throw e;
|
|
200
|
+
}
|
|
201
|
+
const histEol = dominantEol(safeReadFile(historyPath) || '');
|
|
202
|
+
fs.appendFileSync(historyPath, withEol(`\n\n${HISTORY_BATCH_MARK}${plan.stamp} -->\n${plan.archivedText}\n`, histEol), 'utf-8');
|
|
203
|
+
|
|
204
|
+
// 2. Only now shorten roadmap.md.
|
|
205
|
+
fs.writeFileSync(roadmapPath, plan.content, 'utf-8');
|
|
206
|
+
return { ...result, applied: true, dry_run: false, archived: plan.archivable.map(v => v.phase) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** True when a phase section (LF or CRLF text) is a compaction stub. */
|
|
210
|
+
function isCompactedSection(section) {
|
|
211
|
+
return String(section || '').split('\n').some(l => l.trimStart().startsWith(STUB_MARK));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A compacted phase's full section from roadmap-history.md text, or null. The last
|
|
216
|
+
* copy wins: each compaction appends.
|
|
217
|
+
*/
|
|
218
|
+
function archivedPhaseSection(historyText, phase) {
|
|
219
|
+
if (!historyText) return null;
|
|
220
|
+
const want = unpad(phase);
|
|
221
|
+
const lines = toLf(historyText).split('\n');
|
|
222
|
+
let hit = null;
|
|
223
|
+
for (const s of phaseSections(lines)) if (s.number === want) hit = s;
|
|
224
|
+
return hit ? lines.slice(hit.start, hit.end).join('\n') : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* For a reader that found a stub where a phase's section should be: the full
|
|
229
|
+
* section from roadmap-history.md. Returns null when `section` is not a stub, so
|
|
230
|
+
* the history is read only for a compacted phase.
|
|
231
|
+
* @returns {{section: string|null, compacted: true, history_path: string, history_found: boolean}|null}
|
|
232
|
+
*/
|
|
233
|
+
function compactedPhaseSection(cwd, phase, section) {
|
|
234
|
+
if (!isCompactedSection(section)) return null;
|
|
235
|
+
const full = archivedPhaseSection(safeReadFile(planningPath(cwd, ROADMAP_HISTORY_FILE)), phase);
|
|
236
|
+
return { section: full, compacted: true, history_path: planningRel(ROADMAP_HISTORY_FILE), history_found: full != null };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** How many sections a compaction would archive now — hygiene offers the fix only then. */
|
|
240
|
+
function roadmapCompactionAvailable(cwd) {
|
|
241
|
+
try { return compactRoadmap(cwd).archivable.length; } catch { return 0; }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** `roadmap compact [--apply] [--keep N]` */
|
|
245
|
+
function cmdRoadmapCompact(cwd, opts, raw) {
|
|
246
|
+
const r = compactRoadmap(cwd, opts);
|
|
247
|
+
if (!r.found) { output(r, raw, 'roadmap.md not found'); return; }
|
|
248
|
+
const saved = Math.max(0, r.tokens_before - r.tokens_after);
|
|
249
|
+
const summary = r.applied
|
|
250
|
+
? `archived ${r.archived.length} phase section(s) to ${r.history_path}; roadmap.md ~${saved} tokens smaller`
|
|
251
|
+
: r.archivable.length
|
|
252
|
+
? `would archive ${r.archivable.length} phase section(s) (phases ${r.archivable.map(v => v.phase).join(', ')}); roadmap.md ~${saved} tokens smaller — rerun with --apply`
|
|
253
|
+
: 'nothing to compact — no shipped phase past the most recent ones';
|
|
254
|
+
output(r, raw, summary);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
ROADMAP_HISTORY_FILE,
|
|
259
|
+
ROADMAP_COMPACT_KEEP,
|
|
260
|
+
isCompactedSection,
|
|
261
|
+
archivedPhaseSection,
|
|
262
|
+
compactedPhaseSection,
|
|
263
|
+
planRoadmapCompaction,
|
|
264
|
+
compactRoadmap,
|
|
265
|
+
roadmapCompactionAvailable,
|
|
266
|
+
cmdRoadmapCompact,
|
|
267
|
+
};
|
|
@@ -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
|
|