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,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, loadConfig, output, error, toPosix, isGitRepo, execGit } = require('./core.cjs');
|
|
7
|
+
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, loadConfig, output, error, toPosix, isGitRepo, isGitIgnored, execGit } = require('./core.cjs');
|
|
8
8
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
9
9
|
const { writeStateMd, readStateSafe } = require('./state.cjs');
|
|
10
|
-
const { enumerateRoadmapPhases } = require('./roadmap.cjs');
|
|
10
|
+
const { enumerateRoadmapPhases, roadmapRequirementIds } = require('./roadmap.cjs');
|
|
11
11
|
const { PHASES_DIR, ROADMAP_FILE, REQUIREMENTS_FILE, STATE_FILE, isPlanFile, isSummaryFile, getPlanId, PHASE_DIR_RE, ARCHIVE_DIR_RE } = require('./constants.cjs');
|
|
12
12
|
const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, parsePhaseDir, fileAccessible, planningRel } = require('./utils.cjs');
|
|
13
13
|
// Phase removal lives in phase-remove.cjs; re-exported below so consumers of
|
|
@@ -63,7 +63,7 @@ function cmdPhasesList(cwd, options, raw) {
|
|
|
63
63
|
// If filtering by phase number
|
|
64
64
|
if (phase) {
|
|
65
65
|
const normalized = normalizePhaseName(phase);
|
|
66
|
-
const match = dirs.find(dir => dir.startsWith(normalized));
|
|
66
|
+
const match = dirs.find(dir => dir === normalized || dir.startsWith(normalized + '-'));
|
|
67
67
|
if (!match) {
|
|
68
68
|
output({ files: [], count: 0, phase_dir: null, error: 'Phase not found' }, raw, '');
|
|
69
69
|
return;
|
|
@@ -200,7 +200,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
200
200
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
201
201
|
const dirs = entries.filter(entry => entry.isDirectory()).map(entry => entry.name).sort((left, right) => comparePhaseNum(left, right));
|
|
202
202
|
|
|
203
|
-
const match = dirs.find(dir => dir.startsWith(normalized));
|
|
203
|
+
const match = dirs.find(dir => dir === normalized || dir.startsWith(normalized + '-'));
|
|
204
204
|
if (!match) {
|
|
205
205
|
output(notFound, raw, '');
|
|
206
206
|
return;
|
|
@@ -253,7 +253,7 @@ function cmdPhasePlanIndex(cwd, phase, raw, opts = {}) {
|
|
|
253
253
|
try {
|
|
254
254
|
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
255
255
|
const dirs = entries.filter(entry => entry.isDirectory()).map(entry => entry.name).sort((left, right) => comparePhaseNum(left, right));
|
|
256
|
-
const match = dirs.find(dir => dir.startsWith(normalized));
|
|
256
|
+
const match = dirs.find(dir => dir === normalized || dir.startsWith(normalized + '-'));
|
|
257
257
|
if (match) {
|
|
258
258
|
phaseDir = path.join(phasesDir, match);
|
|
259
259
|
phaseDirName = match;
|
|
@@ -708,12 +708,10 @@ function markRequirementsCompleteForPhase(cwd, phaseNum, roadmapContent) {
|
|
|
708
708
|
const reqPath = path.join(planningPath(cwd), REQUIREMENTS_FILE);
|
|
709
709
|
try {
|
|
710
710
|
const reqReadContent = fs.readFileSync(reqPath, 'utf-8');
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
);
|
|
714
|
-
if (
|
|
715
|
-
|
|
716
|
-
const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(id => id.trim()).filter(Boolean);
|
|
711
|
+
// The phase's own requirements, matched padded or not (roadmapRequirementIds): a
|
|
712
|
+
// bare `Phase N … Requirements` search ticked phase 1's when completing phase 5.
|
|
713
|
+
const reqIds = roadmapRequirementIds(roadmapContent, phaseNum);
|
|
714
|
+
if (reqIds.length === 0) return;
|
|
717
715
|
let reqContent = reqReadContent;
|
|
718
716
|
|
|
719
717
|
for (const reqId of reqIds) {
|
|
@@ -825,6 +823,26 @@ function cmdPhaseComplete(cwd, phaseNum, raw, opts) {
|
|
|
825
823
|
error(`Phase ${phaseNum} not found`);
|
|
826
824
|
}
|
|
827
825
|
|
|
826
|
+
// The phase gate (V1). In `enforce` mode a refusal stops here unless --force
|
|
827
|
+
// --reason, which records a decision; in `warn` mode the reasons travel with the
|
|
828
|
+
// result. A --force without a reason is refused in either mode.
|
|
829
|
+
const { phaseGate, recordGateOverride } = require('./phase-gate.cjs');
|
|
830
|
+
const force = !!(opts && opts.force);
|
|
831
|
+
const forceReason = opts && typeof opts.reason === 'string' ? opts.reason.trim() : '';
|
|
832
|
+
if (force && !forceReason) {
|
|
833
|
+
output({ error: 'reason_required', reason: 'phase complete --force needs --reason "<why the gate is being overridden>"' }, raw);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const gate = phaseGate(cwd, phaseNum);
|
|
837
|
+
let gateOverride = null;
|
|
838
|
+
if (!gate.error && !gate.ok) {
|
|
839
|
+
if (gate.mode === 'enforce' && !force) {
|
|
840
|
+
output({ error: 'phase_gate_refused', reason: gate.reasons.map((r) => `${r.code}: ${r.detail}`).join(' | '), gate }, raw);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (force) gateOverride = recordGateOverride(cwd, gate, forceReason);
|
|
844
|
+
}
|
|
845
|
+
|
|
828
846
|
const planCount = phaseInfo.plans.length;
|
|
829
847
|
const summaryCount = phaseInfo.summaries.length;
|
|
830
848
|
|
|
@@ -888,6 +906,8 @@ function cmdPhaseComplete(cwd, phaseNum, raw, opts) {
|
|
|
888
906
|
date: today,
|
|
889
907
|
roadmap_updated: fileAccessible(roadmapPath),
|
|
890
908
|
state_updated: fileAccessible(statePath),
|
|
909
|
+
gate: gate.error ? { error: gate.error } : { ok: gate.ok, mode: gate.mode, reasons: gate.reasons, not_checked: gate.not_checked },
|
|
910
|
+
...(gateOverride ? { gate_override: gateOverride } : {}),
|
|
891
911
|
};
|
|
892
912
|
if (roadmapResult && roadmapResult.requirements_warning) {
|
|
893
913
|
result.requirements_warning = roadmapResult.requirements_warning;
|
|
@@ -900,8 +920,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw, opts) {
|
|
|
900
920
|
result.roadmap_updated = false;
|
|
901
921
|
}
|
|
902
922
|
|
|
903
|
-
// Auto-commit .planning/ metadata unless --no-commit
|
|
904
|
-
|
|
923
|
+
// Auto-commit .planning/ metadata unless --no-commit, not a git repo, or the project
|
|
924
|
+
// keeps planning files out of git: `commit_docs: false` or a gitignored planning
|
|
925
|
+
// tree, the same rules `pan-tools commit` follows (this commit used to ignore both).
|
|
926
|
+
const noCommit = (opts && opts.noCommit)
|
|
927
|
+
|| loadConfig(cwd).commit_docs === false
|
|
928
|
+
|| isGitIgnored(cwd, planningRel());
|
|
905
929
|
if (!noCommit && isGitRepo(cwd)) {
|
|
906
930
|
const commitMsg = `docs(${normalized}): complete phase — ${phaseInfo.phase_name}`;
|
|
907
931
|
execGit(cwd, ['add', planningRel() + '/']);
|
|
@@ -980,6 +1004,8 @@ module.exports = {
|
|
|
980
1004
|
cmdFindPhase,
|
|
981
1005
|
cmdPhasePlanIndex,
|
|
982
1006
|
blockedPlans,
|
|
1007
|
+
buildPlanIndex,
|
|
1008
|
+
dependencyNames,
|
|
983
1009
|
cmdPhaseAdd,
|
|
984
1010
|
cmdPhaseInsert,
|
|
985
1011
|
cmdPhaseRemove,
|
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
findPhaseInternal,
|
|
21
21
|
getRoadmapPhaseInternal,
|
|
22
22
|
toPosix,
|
|
23
|
+
toLf,
|
|
23
24
|
} = require('./core.cjs');
|
|
24
25
|
const {
|
|
25
26
|
ROADMAP_FILE,
|
|
@@ -160,7 +161,8 @@ function buildPhasePreview(cwd, phaseNum) {
|
|
|
160
161
|
* Produce a dependency graph + mermaid source + parallel-batch recommendation.
|
|
161
162
|
*
|
|
162
163
|
* Dependency detection:
|
|
163
|
-
* -
|
|
164
|
+
* - the roadmap's `**Depends on:** Phase N` line (explicit)
|
|
165
|
+
* - plan frontmatter `depends_on: [phase:NN, phase:MM]` (explicit; bare plan ids are not phases)
|
|
164
166
|
* - mentions of prior phases in plan text (heuristic, flagged as "hidden")
|
|
165
167
|
*
|
|
166
168
|
* @param {string} cwd - Project root
|
|
@@ -194,6 +196,13 @@ function buildPhaseDependencyGraph(cwd) {
|
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
// Graph keys are the roadmap checklist's numbers; a dependency written "01" or
|
|
200
|
+
// "Phase 1" resolves to the key whose unpadded form matches.
|
|
201
|
+
const unpad = (n) => String(n).trim().replace(/^0+(?=\d)/, '');
|
|
202
|
+
const keyByUnpadded = new Map(phaseList.map(p => [unpad(p.num), p.num]));
|
|
203
|
+
const keyOf = (n) => keyByUnpadded.get(unpad(n)) || unpad(n);
|
|
204
|
+
const { phaseDependencies } = require('./roadmap.cjs');
|
|
205
|
+
|
|
197
206
|
for (const p of phaseList) {
|
|
198
207
|
graph[p.num] = {
|
|
199
208
|
num: p.num,
|
|
@@ -203,6 +212,15 @@ function buildPhaseDependencyGraph(cwd) {
|
|
|
203
212
|
hidden_deps: [],
|
|
204
213
|
};
|
|
205
214
|
|
|
215
|
+
const addExplicit = (d) => {
|
|
216
|
+
const key = keyOf(d);
|
|
217
|
+
if (key !== keyOf(p.num) && !graph[p.num].explicit_deps.includes(key)) graph[p.num].explicit_deps.push(key);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Explicit via the roadmap: the phase's `**Depends on:** Phase N` line.
|
|
221
|
+
const roadmapPhase = getRoadmapPhaseInternal(cwd, p.num);
|
|
222
|
+
for (const d of phaseDependencies(roadmapPhase && roadmapPhase.section)) addExplicit(d);
|
|
223
|
+
|
|
206
224
|
// Read plan files for depends_on frontmatter + prior-phase mentions.
|
|
207
225
|
const dir = phaseDirs[p.num];
|
|
208
226
|
if (!dir) continue;
|
|
@@ -210,35 +228,19 @@ function buildPhaseDependencyGraph(cwd) {
|
|
|
210
228
|
let files = [];
|
|
211
229
|
try { files = fs.readdirSync(fullDir).filter(isPlanFile); } catch { continue; }
|
|
212
230
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
// Also support block-list form:
|
|
229
|
-
// depends_on:
|
|
230
|
-
// - phase:1
|
|
231
|
-
// - phase:2
|
|
232
|
-
const blockMatch = fmMatch[1].match(/^depends_on:\s*\n((?:\s*-\s*.+\n?)*)/m);
|
|
233
|
-
if (blockMatch && !depsLine) {
|
|
234
|
-
const items = blockMatch[1].match(/^\s*-\s*(.+)$/gm) || [];
|
|
235
|
-
for (const item of items) {
|
|
236
|
-
const d = item.match(/\d+(?:\.\d+)?/);
|
|
237
|
-
if (d && d[0] !== p.num && !graph[p.num].explicit_deps.includes(d[0])) {
|
|
238
|
-
graph[p.num].explicit_deps.push(d[0]);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
231
|
+
// LF, so the frontmatter match below also finds a CRLF plan's depends_on.
|
|
232
|
+
const planTexts = files.map(f => toLf(safeReadFile(path.join(fullDir, f)) || ''));
|
|
233
|
+
const phaseText = planTexts.join('\n');
|
|
234
|
+
|
|
235
|
+
// Explicit via each plan's frontmatter. A plan's `depends_on` names plan ids
|
|
236
|
+
// inside its own phase (`["01"]`), so only `phase:N` entries are phase
|
|
237
|
+
// dependencies, inline (`[phase:1, phase:2]`) or as a block list.
|
|
238
|
+
for (const text of planTexts) {
|
|
239
|
+
const fmMatch = text.match(/^---\n([\s\S]*?)\n---/);
|
|
240
|
+
if (!fmMatch) continue;
|
|
241
|
+
const depsBlock = fmMatch[1].match(/^depends_on:[^\n]*(?:\n[ \t]*-[^\n]*)*/m);
|
|
242
|
+
if (!depsBlock) continue;
|
|
243
|
+
for (const m of depsBlock[0].matchAll(/\bphase[:\s]*(\d+(?:\.\d+)?)/gi)) addExplicit(m[1]);
|
|
242
244
|
}
|
|
243
245
|
|
|
244
246
|
// Hidden via mentions: "phase N", "from phase N", "as in phase N".
|
|
@@ -246,7 +248,8 @@ function buildPhaseDependencyGraph(cwd) {
|
|
|
246
248
|
let mm;
|
|
247
249
|
const mentions = new Set();
|
|
248
250
|
while ((mm = mentionRe.exec(phaseText)) !== null) {
|
|
249
|
-
|
|
251
|
+
const k = keyOf(mm[1]);
|
|
252
|
+
if (k !== keyOf(p.num)) mentions.add(k);
|
|
250
253
|
}
|
|
251
254
|
graph[p.num].hidden_deps = [...mentions].filter(d => !graph[p.num].explicit_deps.includes(d));
|
|
252
255
|
}
|
|
@@ -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
|
+
};
|