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
|
@@ -6,7 +6,7 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { PHASES_DIR, MILESTONES_DIR, ROADMAP_FILE, REQUIREMENTS_FILE, STATE_FILE, isPlanFile } = require('./constants.cjs');
|
|
8
8
|
const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, fileAccessible, planningRel } = require('./utils.cjs');
|
|
9
|
-
const { output, error, isGitRepo, execGit, escapeRegex } = require('./core.cjs');
|
|
9
|
+
const { output, error, isGitRepo, isGitIgnored, execGit, escapeRegex, loadConfig } = require('./core.cjs');
|
|
10
10
|
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
11
11
|
const { writeStateMd } = require('./state.cjs');
|
|
12
12
|
|
|
@@ -258,8 +258,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
|
|
258
258
|
result.archive_warnings = archiveResult.warnings;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
// Auto-commit + tag unless --no-commit
|
|
262
|
-
|
|
261
|
+
// Auto-commit + tag unless --no-commit, not a git repo, or the project keeps planning
|
|
262
|
+
// files out of git (`commit_docs: false` or a gitignored planning tree), the rules
|
|
263
|
+
// `phase complete` and `pan-tools commit` follow (this commit used to ignore both).
|
|
264
|
+
const noCommit = options.noCommit
|
|
265
|
+
|| loadConfig(cwd).commit_docs === false
|
|
266
|
+
|| isGitIgnored(cwd, planningRel());
|
|
267
|
+
if (!noCommit && isGitRepo(cwd)) {
|
|
263
268
|
execGit(cwd, ['add', planningRel() + '/']);
|
|
264
269
|
const commitMsg = `docs: milestone ${version} complete`;
|
|
265
270
|
const commitResult = execGit(cwd, ['commit', '-m', commitMsg]);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { output, escapeRegex, execGit } = require('./core.cjs');
|
|
11
|
+
const { output, escapeRegex, execGit, toLf } = require('./core.cjs');
|
|
12
12
|
const { planningPath, planningRel } = require('./utils.cjs');
|
|
13
13
|
|
|
14
14
|
// ─── Storage layout ──────────────────────────────────────────────────────────
|
|
@@ -793,6 +793,27 @@ function revertedIndices(rows) {
|
|
|
793
793
|
return out;
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
+
/** The trace session a report belongs to, from its name (`<session>-opt-report.md`, `<session>-analysis.json`). */
|
|
797
|
+
function reportSessionId(reportPath) {
|
|
798
|
+
const m = path.basename(String(reportPath || '')).match(/^(.+?)-(?:opt-report\.md|analysis\.json)$/);
|
|
799
|
+
return m ? m[1] : null;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* An older report's memory actions (`memory`, `memory_append`, `memory_entry`) still
|
|
804
|
+
* write into `.planning/memory/`, but PAN's workflows no longer load that folder into
|
|
805
|
+
* agents (ADR-0036, amended 2026-10-04). Say so in the result rather than let the
|
|
806
|
+
* apply look like it taught anyone anything.
|
|
807
|
+
*/
|
|
808
|
+
const MEMORY_NOT_LOADED_WARNING = "stored in .planning/memory/, which PAN's workflows do not load into agents (ADR-0036): "
|
|
809
|
+
+ "write a lesson where the agents read — the project's instructions, a test, or the code";
|
|
810
|
+
|
|
811
|
+
function notLoadedWarning(cwd, relPath) {
|
|
812
|
+
const abs = path.resolve(cwd, relPath || '');
|
|
813
|
+
if (path.dirname(abs) !== path.resolve(planningPath(cwd), 'memory')) return {};
|
|
814
|
+
return { warning: MEMORY_NOT_LOADED_WARNING };
|
|
815
|
+
}
|
|
816
|
+
|
|
796
817
|
function applyReportRecommendations(cwd, reportPath) {
|
|
797
818
|
let reportContent;
|
|
798
819
|
try {
|
|
@@ -842,15 +863,23 @@ function applyReportRecommendations(cwd, reportPath) {
|
|
|
842
863
|
const resolvedCwd = path.resolve(cwd);
|
|
843
864
|
actions.forEach((action, i) => {
|
|
844
865
|
try {
|
|
845
|
-
// Containment: action.path on memory writes is report/agent-authored
|
|
846
|
-
//
|
|
847
|
-
//
|
|
866
|
+
// Containment: action.path on memory writes is report/agent-authored. A `../`
|
|
867
|
+
// (or absolute) path escaped the project (M23, ADR audit 2026-08), and a path
|
|
868
|
+
// inside it reached any file: a report's `memory_append` to CLAUDE.md made
|
|
869
|
+
// agent-written text a standing instruction with no review (ADR-0040), and
|
|
870
|
+
// `/pan:learn --apply` runs this on the optimizer's own report. A memory action
|
|
871
|
+
// writes memory: `.planning/memory/` and nothing else.
|
|
848
872
|
if (action.type === 'memory' || action.type === 'memory_append') {
|
|
849
873
|
const abs = path.resolve(cwd, action.path || '');
|
|
874
|
+
const memoryDir = path.resolve(planningPath(cwd), 'memory');
|
|
850
875
|
if (abs !== resolvedCwd && !abs.startsWith(resolvedCwd + path.sep)) {
|
|
851
876
|
skipped.push({ action, reason: 'path escapes project root — skipped' });
|
|
852
877
|
return;
|
|
853
878
|
}
|
|
879
|
+
if (!abs.startsWith(memoryDir + path.sep)) {
|
|
880
|
+
skipped.push({ action, reason: 'a memory action writes only into .planning/memory/ — skipped; make a change to the project\'s instructions yourself' });
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
854
883
|
}
|
|
855
884
|
const earlier = live.get(actionSig(action));
|
|
856
885
|
if (earlier) {
|
|
@@ -867,11 +896,29 @@ function applyReportRecommendations(cwd, reportPath) {
|
|
|
867
896
|
fs.mkdirSync(path.dirname(memPath), { recursive: true });
|
|
868
897
|
fs.writeFileSync(memPath, action.content, 'utf-8');
|
|
869
898
|
records.push({ i, type: action.type, path: rel(memPath), kind: 'created', sha256_after: normalisedHash(fs.readFileSync(memPath, 'utf-8')), action_sig: actionSig(action) });
|
|
870
|
-
applied.push({ action, result: `Written to ${action.path}
|
|
899
|
+
applied.push({ action, result: `Written to ${action.path}`, ...notLoadedWarning(cwd, action.path) });
|
|
871
900
|
}
|
|
872
901
|
} else if (action.type === 'memory_append') {
|
|
873
902
|
appendRecorded(path.join(cwd, action.path), '\n' + action.content, action, i);
|
|
874
|
-
applied.push({ action, result: `Appended to ${action.path}
|
|
903
|
+
applied.push({ action, result: `Appended to ${action.path}`, ...notLoadedWarning(cwd, action.path) });
|
|
904
|
+
} else if (action.type === 'memory_entry') {
|
|
905
|
+
// The gated write path (O6): a lesson drawn from this report's trace session,
|
|
906
|
+
// recorded only if it carries citations that hold and passes `memory record`'s
|
|
907
|
+
// other rules. The optimizer no longer proposes these; an older report's are
|
|
908
|
+
// still recorded, as created/appended so revert works.
|
|
909
|
+
const session = reportSessionId(reportPath);
|
|
910
|
+
if (!session) { skipped.push({ action, reason: 'a memory entry needs the trace session the report came from, and this report\'s name does not carry one' }); return; }
|
|
911
|
+
const { recordLesson } = require('./memory.cjs');
|
|
912
|
+
const logPath = path.join(planningPath(cwd), 'memory', `${String(action.agent || '')}.md`);
|
|
913
|
+
let before = null;
|
|
914
|
+
try { before = fs.readFileSync(logPath, 'utf-8'); } catch { /* a new log */ }
|
|
915
|
+
const r = recordLesson(cwd, String(action.agent || ''), { lesson: action.lesson, trace: session, cites: action.cites });
|
|
916
|
+
if (r.error || !r.recorded) { skipped.push({ action, reason: r.error || `memory record refused it: ${r.reason}` }); return; }
|
|
917
|
+
const after = fs.readFileSync(logPath, 'utf-8');
|
|
918
|
+
records.push(before === null
|
|
919
|
+
? { i, type: action.type, path: rel(logPath), kind: 'created', sha256_after: normalisedHash(after), action_sig: actionSig(action) }
|
|
920
|
+
: { i, type: action.type, path: rel(logPath), kind: 'appended', text: toLf(after).slice(toLf(before).length), sha256_after: normalisedHash(after), action_sig: actionSig(action) });
|
|
921
|
+
applied.push({ action, result: `Recorded for ${r.agent} from ${r.evidence}`, warning: MEMORY_NOT_LOADED_WARNING });
|
|
875
922
|
} else if (action.type === 'note') {
|
|
876
923
|
// Write a human-readable suggestion note
|
|
877
924
|
const entry = `\n## ${new Date().toISOString()}: ${action.description || 'Suggestion'}\n\n${action.content || action.suggestion || ''}\n\n**Target:** ${action.target || 'unspecified'}\n`;
|
|
@@ -992,7 +1039,11 @@ function revertApply(cwd, which) {
|
|
|
992
1039
|
return { apply_id: apply.apply_id, reverted: out.sort((x, y) => x - y), refused, status: remaining === 0 ? 'reverted' : (out.length ? 'partial' : 'refused') };
|
|
993
1040
|
}
|
|
994
1041
|
|
|
995
|
-
//
|
|
1042
|
+
// Where a lesson reaches the next run's agents: they read the project's
|
|
1043
|
+
// instructions, and PAN's workflows no longer load `.planning/memory/` (ADR-0036).
|
|
1044
|
+
const LESSON_TARGET = "the project's instructions (CLAUDE.md or AGENTS.md, outside PAN's section)";
|
|
1045
|
+
|
|
1046
|
+
// Derive suggestion notes from a raw JSON analysis when no optimizer agent
|
|
996
1047
|
// report is available (fallback for /pan:optimize apply on a JSON file).
|
|
997
1048
|
function deriveActionsFromAnalysis(analysis) {
|
|
998
1049
|
const actions = [];
|
|
@@ -1002,9 +1053,9 @@ function deriveActionsFromAnalysis(analysis) {
|
|
|
1002
1053
|
memory_miss_patterns.slice(0, 3).forEach(p => {
|
|
1003
1054
|
actions.push({
|
|
1004
1055
|
type: 'note',
|
|
1005
|
-
description: `
|
|
1006
|
-
content: `
|
|
1007
|
-
target:
|
|
1056
|
+
description: `Missing knowledge: ${p.pattern}`,
|
|
1057
|
+
content: `An agent logged this as missing ${p.count} time(s) during the traced session. Write the answer where the agents read: ${LESSON_TARGET}, or a comment at the code it concerns.`,
|
|
1058
|
+
target: LESSON_TARGET,
|
|
1008
1059
|
});
|
|
1009
1060
|
});
|
|
1010
1061
|
}
|
|
@@ -1016,8 +1067,8 @@ function deriveActionsFromAnalysis(analysis) {
|
|
|
1016
1067
|
actions.push({
|
|
1017
1068
|
type: 'note',
|
|
1018
1069
|
description: `Recurring tool failure: ${p.agent} ${p.tool} (${p.error_class})`,
|
|
1019
|
-
content: `${p.agent}'s ${p.tool} call failed with "${p.message}" in ${p.spawns} spawns across ${p.sessions} session(s) (${p.occurrences} times in all).
|
|
1020
|
-
target:
|
|
1070
|
+
content: `${p.agent}'s ${p.tool} call failed with "${p.message}" in ${p.spawns} spawns across ${p.sessions} session(s) (${p.occurrences} times in all). Write the working form of the call, or the precondition it needs, where ${p.agent} reads it: ${LESSON_TARGET}.`,
|
|
1071
|
+
target: LESSON_TARGET,
|
|
1021
1072
|
});
|
|
1022
1073
|
});
|
|
1023
1074
|
Object.entries(analysis.verdict_stats || {}).filter(([, s]) => s.fail >= 2).forEach(([agent, s]) => {
|
|
@@ -1025,7 +1076,7 @@ function deriveActionsFromAnalysis(analysis) {
|
|
|
1025
1076
|
type: 'note',
|
|
1026
1077
|
description: `Repeated judge failures: ${agent}`,
|
|
1027
1078
|
content: `${agent} failed ${s.fail} time(s) (${s.retries} retr${s.retries === 1 ? 'y' : 'ies'}, ${s.resolved_by_retry} resolved by the retry). Read the recorded findings (\`pan-tools findings list --agent ${agent}\`) for the class that keeps recurring before the next phase.`,
|
|
1028
|
-
target: '.planning/
|
|
1079
|
+
target: '.planning/findings.jsonl',
|
|
1029
1080
|
});
|
|
1030
1081
|
});
|
|
1031
1082
|
|
|
@@ -1034,8 +1085,8 @@ function deriveActionsFromAnalysis(analysis) {
|
|
|
1034
1085
|
actions.push({
|
|
1035
1086
|
type: 'note',
|
|
1036
1087
|
description: `Knowledge gap: ${p.pattern}`,
|
|
1037
|
-
content: `The agent had to infer this ${p.count} time(s). Research and
|
|
1038
|
-
target:
|
|
1088
|
+
content: `The agent had to infer this ${p.count} time(s). Research the answer and write it where the agents read: ${LESSON_TARGET}, or a comment at the code it concerns.`,
|
|
1089
|
+
target: LESSON_TARGET,
|
|
1039
1090
|
});
|
|
1040
1091
|
});
|
|
1041
1092
|
}
|
|
@@ -1286,6 +1337,14 @@ function validatePromoteInputs(pattern, opts) {
|
|
|
1286
1337
|
if (!pattern.id) return 'pattern.id is required';
|
|
1287
1338
|
if (!pattern.summary) return 'pattern.summary is required';
|
|
1288
1339
|
if (!pattern.rule) return 'pattern.rule is required';
|
|
1340
|
+
// A citation names the code the pattern rests on, relative to the tree that
|
|
1341
|
+
// holds the store; it must hold now, and learn lint L-007 keeps checking it.
|
|
1342
|
+
if (pattern.cites !== undefined && !Array.isArray(pattern.cites)) return 'pattern.cites must be a list';
|
|
1343
|
+
const { citationProblem } = require('./memory.cjs');
|
|
1344
|
+
for (const c of pattern.cites || []) {
|
|
1345
|
+
const problem = citationProblem(opts.sourceRoot, c);
|
|
1346
|
+
if (problem) return `citation ${c}: ${problem}`;
|
|
1347
|
+
}
|
|
1289
1348
|
return null;
|
|
1290
1349
|
}
|
|
1291
1350
|
|
|
@@ -1366,7 +1425,8 @@ function classifyPatternKind(pattern) {
|
|
|
1366
1425
|
* always written by us — no general YAML dependency needed).
|
|
1367
1426
|
*/
|
|
1368
1427
|
function readTopicFile(filePath) {
|
|
1369
|
-
|
|
1428
|
+
// LF: an install committed with core.autocrlf=true holds these files as CRLF.
|
|
1429
|
+
const content = toLf(fs.readFileSync(filePath, 'utf-8'));
|
|
1370
1430
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
1371
1431
|
if (!fmMatch) {
|
|
1372
1432
|
return { frontmatter: { topic: '', patterns: [] }, body: content, _raw: content };
|
|
@@ -1429,6 +1489,13 @@ function serializeTopicFile(topic, patterns, body) {
|
|
|
1429
1489
|
fm += ` promoted_at: ${p.promoted_at || ts}\n`;
|
|
1430
1490
|
const srcExps = Array.isArray(p.source_experiments) ? p.source_experiments : [];
|
|
1431
1491
|
fm += ` source_experiments: [${srcExps.join(', ')}]\n`;
|
|
1492
|
+
// Optional fields survive a re-serialisation: promoting into an existing topic
|
|
1493
|
+
// rewrites every pattern's frontmatter, and dropping these lost the supersession
|
|
1494
|
+
// learn lint L-005 reads, and the citations L-007 checks.
|
|
1495
|
+
const cites = Array.isArray(p.cites) ? p.cites : [];
|
|
1496
|
+
if (cites.length) fm += ` cites: [${cites.join(', ')}]\n`;
|
|
1497
|
+
if (p.superseded_by) fm += ` superseded_by: ${p.superseded_by}\n`;
|
|
1498
|
+
if (p.superseded_id) fm += ` superseded_id: ${p.superseded_id}\n`;
|
|
1432
1499
|
}
|
|
1433
1500
|
return `---\n${fm}---\n${body}`;
|
|
1434
1501
|
}
|
|
@@ -1519,6 +1586,7 @@ function promotePattern(pattern, opts) {
|
|
|
1519
1586
|
summary: pattern.summary,
|
|
1520
1587
|
promoted_at: promotedAt,
|
|
1521
1588
|
source_experiments: pattern.source_experiments || [],
|
|
1589
|
+
...(pattern.cites && pattern.cites.length ? { cites: pattern.cites } : {}),
|
|
1522
1590
|
});
|
|
1523
1591
|
|
|
1524
1592
|
const serialized = serializeTopicFile(topic, frontmatter.patterns, body);
|
|
@@ -173,6 +173,74 @@ function renumberIntegerPhases(phasesDir, removedInt) {
|
|
|
173
173
|
return { renamedDirs, renamedFiles };
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Remove one phase's section from roadmap text. The section runs from its `Phase N:` heading
|
|
178
|
+
* to the first line outside a code fence that is another phase heading, a heading at the
|
|
179
|
+
* section's own level or above (`## Progress`, a milestone group), a `</details>` that closes
|
|
180
|
+
* an enclosing block, or a collapsed milestone's `<details>` (its `<summary>` names a version
|
|
181
|
+
* or a phase range). Any other `<details>` block the section opens and closes itself is part
|
|
182
|
+
* of it. Running to the end of the file deleted `## Progress` with the last phase and the next
|
|
183
|
+
* milestone group's heading with a group's last phase; a line-blind bound stopped at a
|
|
184
|
+
* `# comment` or `## Usage` inside a fenced block.
|
|
185
|
+
* @param {string} content - roadmap.md text (LF or CRLF; line endings are kept)
|
|
186
|
+
* @param {string} phaseNum - the phase number as written in its heading
|
|
187
|
+
* @returns {string} the text without that section (unchanged when no heading matches)
|
|
188
|
+
*/
|
|
189
|
+
function removePhaseSection(content, phaseNum) {
|
|
190
|
+
const lines = content.split('\n');
|
|
191
|
+
const fenced = new Array(lines.length).fill(false);
|
|
192
|
+
// A fence line with an info string opens a block, even inside another (a nested example);
|
|
193
|
+
// a bare one closes the innermost block it can close. A block never closed stays unfenced.
|
|
194
|
+
for (let i = 0, stack = []; i < lines.length; i++) {
|
|
195
|
+
const f = lines[i].match(/^\s{0,3}(`{3,}|~{3,})(.*)/);
|
|
196
|
+
if (!f) continue;
|
|
197
|
+
const top = stack[stack.length - 1];
|
|
198
|
+
if (top && !f[2].trim() && f[1][0] === top.fence[0] && f[1].length >= top.fence.length) {
|
|
199
|
+
stack.pop();
|
|
200
|
+
fenced.fill(true, top.open, i + 1);
|
|
201
|
+
} else if (!top || f[2].trim()) stack.push({ fence: f[1], open: i });
|
|
202
|
+
}
|
|
203
|
+
const headRe = new RegExp(`^(#{2,4})\\s*Phase\\s+${escapeRegex(phaseNum)}\\s*:`, 'i');
|
|
204
|
+
const start = lines.findIndex((l, i) => !fenced[i] && headRe.test(l));
|
|
205
|
+
if (start === -1) return content;
|
|
206
|
+
const levelEndRe = new RegExp(`^#{1,${lines[start].match(headRe)[1].length}}\\s`);
|
|
207
|
+
const endsSection = (i) => !fenced[i] && (/^#{2,4}\s*Phase\s+\d/i.test(lines[i]) || levelEndRe.test(lines[i]));
|
|
208
|
+
const opens = (i) => !fenced[i] && /^\s*<details\b/i.test(lines[i]);
|
|
209
|
+
const closes = (i) => !fenced[i] && /^\s*<\/details\b/i.test(lines[i]);
|
|
210
|
+
const oneLine = (i) => /<\/details\s*>/i.test(lines[i]);
|
|
211
|
+
// A collapsed milestone's <summary> names its version or phase range ("v1.0 MVP (Phases 1-4)").
|
|
212
|
+
const milestone = (i) => /<summary>[^<]*\b(?:v\d+\.\d+|phases\s+\d)/i.test(`${lines[i]} ${lines[i + 1] || ''}`);
|
|
213
|
+
// A <details> closed before anything that ends the section is the section's own block.
|
|
214
|
+
const ownBlock = (at) => {
|
|
215
|
+
for (let i = at + 1, d = 0; i < lines.length; i++) {
|
|
216
|
+
if (endsSection(i)) return false;
|
|
217
|
+
if (opens(i) && !oneLine(i)) d++;
|
|
218
|
+
else if (closes(i)) { if (d === 0) return true; d--; }
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
};
|
|
222
|
+
let end = start + 1;
|
|
223
|
+
for (let depth = 0; end < lines.length; end++) {
|
|
224
|
+
if (endsSection(end)) break;
|
|
225
|
+
if (opens(end)) {
|
|
226
|
+
if (depth === 0 && milestone(end)) break;
|
|
227
|
+
if (oneLine(end)) continue;
|
|
228
|
+
if (depth > 0 || ownBlock(end)) { depth++; continue; }
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (closes(end)) { if (depth > 0) { depth--; continue; } break; }
|
|
232
|
+
}
|
|
233
|
+
const toEof = end === lines.length;
|
|
234
|
+
lines.splice(start, end - start);
|
|
235
|
+
let out = lines.join('\n');
|
|
236
|
+
// A section that ran to the end of the file took the final line ending with it.
|
|
237
|
+
if (toEof && content.endsWith('\n')) {
|
|
238
|
+
out = out.replace(/\r$/, '');
|
|
239
|
+
if (!out.endsWith('\n')) out += content.endsWith('\r\n') ? '\r\n' : '\n';
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
|
|
176
244
|
/**
|
|
177
245
|
* Rewrite roadmap.md after a phase is removed: delete the target section,
|
|
178
246
|
* remove checkbox/table references, and renumber subsequent phase references.
|
|
@@ -189,14 +257,8 @@ function updateRoadmapAfterRemoval(cwd, phaseNum, isDecimal, normalized) {
|
|
|
189
257
|
roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
190
258
|
} catch { return; }
|
|
191
259
|
|
|
192
|
-
|
|
193
|
-
// Matches from the phase heading to the next phase heading (or end of file).
|
|
260
|
+
roadmapContent = removePhaseSection(roadmapContent, phaseNum);
|
|
194
261
|
const targetEscaped = escapeRegex(phaseNum);
|
|
195
|
-
const sectionPattern = new RegExp(
|
|
196
|
-
`\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`,
|
|
197
|
-
'i'
|
|
198
|
-
);
|
|
199
|
-
roadmapContent = roadmapContent.replace(sectionPattern, '');
|
|
200
262
|
|
|
201
263
|
// Remove checkbox list items referencing this phase
|
|
202
264
|
const checkboxPattern = new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${targetEscaped}[:\\s][^\\n]*`, 'gi');
|
|
@@ -207,8 +269,9 @@ function updateRoadmapAfterRemoval(cwd, phaseNum, isDecimal, normalized) {
|
|
|
207
269
|
roadmapContent = roadmapContent.replace(tableRowPattern, '');
|
|
208
270
|
|
|
209
271
|
// For integer phase removal, renumber all references to subsequent phases.
|
|
210
|
-
// Walk from
|
|
211
|
-
//
|
|
272
|
+
// Walk UP from removedInt+1, decrementing each by 1. A downward walk re-hits its
|
|
273
|
+
// own output (10->9, then that 9 and the real 9 ->8), so every later phase
|
|
274
|
+
// collapsed onto the removed number.
|
|
212
275
|
if (!isDecimal) {
|
|
213
276
|
const removedInt = parseInt(normalized, 10);
|
|
214
277
|
|
|
@@ -219,35 +282,35 @@ function updateRoadmapAfterRemoval(cwd, phaseNum, isDecimal, normalized) {
|
|
|
219
282
|
// and an unbounded lower end is how `phase remove -1234567890` turned into
|
|
220
283
|
// ~1.2 billion regex passes. The loop can now never exceed maxPhase steps.
|
|
221
284
|
const stopAt = Math.max(removedInt, 0);
|
|
222
|
-
for (let oldNum =
|
|
285
|
+
for (let oldNum = stopAt + 1; oldNum <= maxPhase; oldNum++) {
|
|
223
286
|
const newNum = oldNum - 1;
|
|
224
287
|
const oldStr = String(oldNum);
|
|
225
288
|
const newStr = String(newNum);
|
|
226
289
|
const oldPad = oldStr.padStart(2, '0');
|
|
227
290
|
const newPad = newStr.padStart(2, '0');
|
|
228
291
|
|
|
229
|
-
// Phase headings: ## Phase 18: or ### Phase 18: -> ## Phase 17:
|
|
292
|
+
// Phase headings: ## Phase 18: or ### Phase 18.1: -> ## Phase 17: / ### Phase 17.1:
|
|
230
293
|
roadmapContent = roadmapContent.replace(
|
|
231
|
-
new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(
|
|
294
|
+
new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}((?:\\.\\d+)?\\s*:)`, 'gi'),
|
|
232
295
|
`$1${newStr}$2`
|
|
233
296
|
);
|
|
234
297
|
|
|
235
|
-
// Inline phase references: "Phase 18:" or "Phase 18
|
|
298
|
+
// Inline phase references: "Phase 18:", "Phase 18 " or "Phase 18.1:" -> "Phase 17..."
|
|
236
299
|
roadmapContent = roadmapContent.replace(
|
|
237
|
-
new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'),
|
|
300
|
+
new RegExp(`(Phase\\s+)${oldStr}((?:\\.\\d+)?[:\\s])`, 'g'),
|
|
238
301
|
`$1${newStr}$2`
|
|
239
302
|
);
|
|
240
303
|
|
|
241
|
-
// Plan references in padded form: 18-01 -> 17-01
|
|
304
|
+
// Plan references in padded form: 18-01 / 18.1-01 -> 17-01 / 17.1-01, never inside a date (2026-10-01)
|
|
242
305
|
roadmapContent = roadmapContent.replace(
|
|
243
|
-
new RegExp(
|
|
244
|
-
`${newPad}-$
|
|
306
|
+
new RegExp(`(?<![\\d.-])${oldPad}((?:\\.\\d+)?)-(\\d{2})(?!\\d)`, 'g'),
|
|
307
|
+
`${newPad}$1-$2`
|
|
245
308
|
);
|
|
246
309
|
|
|
247
|
-
// Progress table row numbers: | 18. -> | 17.
|
|
310
|
+
// Progress table row numbers: | 18. or | 18.1. -> | 17. / | 17.1.
|
|
248
311
|
roadmapContent = roadmapContent.replace(
|
|
249
|
-
new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'),
|
|
250
|
-
`$1${newStr}. `
|
|
312
|
+
new RegExp(`(\\|\\s*)${oldStr}((?:\\.\\d+)?)\\.\\s`, 'g'),
|
|
313
|
+
`$1${newStr}$2. `
|
|
251
314
|
);
|
|
252
315
|
|
|
253
316
|
// Depends-on references: "Depends on:** Phase 18" -> "Phase 17"
|
|
@@ -279,7 +342,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
|
|
279
342
|
// PHASE_NUM_RE does not match — so a bad argument used to reach the renumber
|
|
280
343
|
// logic and do real damage, in both cases reporting success and exiting 0:
|
|
281
344
|
// `phase remove 0` renumbered EVERY roadmap heading down to "Phase 0"
|
|
282
|
-
// (the
|
|
345
|
+
// (the renumber loop then ran down from 99 and re-hit
|
|
283
346
|
// its own output, collapsing 3->2->1->0) and renamed
|
|
284
347
|
// every phase directory one lower.
|
|
285
348
|
// `phase remove -1e9` spun that same loop ~1.2 billion times, each pass
|
|
@@ -425,6 +488,7 @@ module.exports = {
|
|
|
425
488
|
collectDirsToRenumber,
|
|
426
489
|
renamePhaseDir,
|
|
427
490
|
renumberIntegerPhases,
|
|
491
|
+
removePhaseSection,
|
|
428
492
|
updateRoadmapAfterRemoval,
|
|
429
493
|
cmdPhaseRemove,
|
|
430
494
|
updateStateAfterPhaseRemoval,
|
|
@@ -96,14 +96,20 @@ function resolvePhase(cwd, phaseNumber) {
|
|
|
96
96
|
function extractPhaseGoal(cwd, phaseNumber) {
|
|
97
97
|
const content = safeReadFile(path.join(planningPath(cwd), ROADMAP_FILE));
|
|
98
98
|
if (!content) return { objective: null, success_criteria: [] };
|
|
99
|
-
|
|
99
|
+
// Padded or not: the number comes from the zero-padded phase directory ("02"),
|
|
100
|
+
// while PAN writes `### Phase 2:` headings, so the report found no goal.
|
|
101
|
+
const unpadded = String(phaseNumber).trim().replace(/^0+(?=\d)/, '');
|
|
102
|
+
const re = new RegExp('#{2,4}\\s*Phase\\s+0*' + escapeRegex(unpadded) + ':\\s*([^\\n]+)', 'i');
|
|
100
103
|
const m = content.match(re);
|
|
101
104
|
if (!m) return { objective: null, success_criteria: [] };
|
|
102
105
|
const start = m.index;
|
|
103
106
|
const rest = content.slice(start + 1);
|
|
104
107
|
const nextH = rest.match(/\n#{2,4}\s+Phase\s+\d/i);
|
|
105
108
|
const end = nextH ? start + 1 + nextH.index : content.length;
|
|
106
|
-
|
|
109
|
+
let section = content.slice(start, end);
|
|
110
|
+
// A phase moved out by `roadmap compact` keeps a stub; its criteria are in the history.
|
|
111
|
+
const compacted = require('./roadmap-compact.cjs').compactedPhaseSection(cwd, phaseNumber, section);
|
|
112
|
+
if (compacted && compacted.section) section = compacted.section;
|
|
107
113
|
const gm = section.match(/(?:\*\*Goal:\*\*|\*\*Goal\*\*:)\s*([^\n]+)/i);
|
|
108
114
|
const objective = gm ? gm[1].trim() : null;
|
|
109
115
|
const cm = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\s*\d+\.\s*[^\n]+\n?)+)/i);
|
|
@@ -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;
|
|
@@ -239,7 +239,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
239
239
|
* @param {boolean} raw - If true, output raw value instead of JSON
|
|
240
240
|
* @returns {void}
|
|
241
241
|
*/
|
|
242
|
-
function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
242
|
+
function cmdPhasePlanIndex(cwd, phase, raw, opts = {}) {
|
|
243
243
|
if (!phase) {
|
|
244
244
|
error('phase required for phase-plan-index');
|
|
245
245
|
}
|
|
@@ -253,7 +253,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
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;
|
|
@@ -280,7 +280,52 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
280
280
|
|
|
281
281
|
const { plans, waves, incomplete, hasCheckpoints } = buildPlanIndex(phaseDir, planFiles, summaryFiles);
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
const result = { phase: normalized, plans, waves, incomplete, has_checkpoints: hasCheckpoints };
|
|
284
|
+
// `--failed <ids>` (market-ideas M28): the plans that must not run after those
|
|
285
|
+
// failed, because a dependency, direct or transitive, is among them.
|
|
286
|
+
if (opts.failed) {
|
|
287
|
+
const failed = String(opts.failed).split(',').map((s) => s.trim()).filter(Boolean);
|
|
288
|
+
result.failed = failed;
|
|
289
|
+
result.blocked = blockedPlans(plans, failed);
|
|
290
|
+
}
|
|
291
|
+
output(result, raw);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Whether a `depends_on` entry names this plan: its full id ("03-02") or its plan number ("02", "2"). */
|
|
295
|
+
function dependencyNames(dep, planId) {
|
|
296
|
+
const d = String(dep).trim();
|
|
297
|
+
if (d === planId) return true;
|
|
298
|
+
const own = planId.split('-').pop();
|
|
299
|
+
const strip = (s) => s.replace(/^0+(?=\d)/, '');
|
|
300
|
+
return /^\d+$/.test(d) && /^\d+$/.test(own) && strip(d) === strip(own);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The plans that must not run because a plan they depend on failed (market-ideas
|
|
305
|
+
* M28). Transitive: a plan waiting on a blocked plan is blocked too. Plans that
|
|
306
|
+
* already have a summary are done and never blocked.
|
|
307
|
+
* @param {Array<{id: string, depends_on: string[], has_summary: boolean}>} plans
|
|
308
|
+
* @param {string[]} failedIds
|
|
309
|
+
* @returns {Array<{id: string, blocked_by: string[]}>} in plan order; blocked_by names the failed plans
|
|
310
|
+
*/
|
|
311
|
+
function blockedPlans(plans, failedIds) {
|
|
312
|
+
const failed = new Set(failedIds);
|
|
313
|
+
const blockedBy = new Map(); // plan id → Set of failed ids behind it
|
|
314
|
+
let changed = true;
|
|
315
|
+
while (changed) {
|
|
316
|
+
changed = false;
|
|
317
|
+
for (const p of plans) {
|
|
318
|
+
if (p.has_summary || failed.has(p.id)) continue;
|
|
319
|
+
const roots = blockedBy.get(p.id) || new Set();
|
|
320
|
+
const before = roots.size;
|
|
321
|
+
for (const dep of p.depends_on || []) {
|
|
322
|
+
for (const f of failed) if (dependencyNames(dep, f)) roots.add(f);
|
|
323
|
+
for (const [b, bRoots] of blockedBy) if (dependencyNames(dep, b)) for (const r of bRoots) roots.add(r);
|
|
324
|
+
}
|
|
325
|
+
if (roots.size > before) { blockedBy.set(p.id, roots); changed = true; }
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return plans.filter((p) => blockedBy.has(p.id)).map((p) => ({ id: p.id, blocked_by: [...blockedBy.get(p.id)].sort() }));
|
|
284
329
|
}
|
|
285
330
|
|
|
286
331
|
/**
|
|
@@ -339,10 +384,14 @@ function buildPlanIndex(phaseDir, planFiles, summaryFiles) {
|
|
|
339
384
|
const hasSummary = completedPlanIds.has(planId);
|
|
340
385
|
if (!hasSummary) incomplete.push(planId);
|
|
341
386
|
|
|
387
|
+
const rawDeps = frontmatter.depends_on || frontmatter['depends-on'];
|
|
388
|
+
const dependsOn = (Array.isArray(rawDeps) ? rawDeps : (rawDeps ? [rawDeps] : [])).map(String).filter((d) => d.trim());
|
|
389
|
+
|
|
342
390
|
plans.push({
|
|
343
391
|
id: planId, wave, autonomous,
|
|
344
392
|
objective,
|
|
345
393
|
files_modified: filesModified,
|
|
394
|
+
depends_on: dependsOn,
|
|
346
395
|
task_count: taskCount,
|
|
347
396
|
has_summary: hasSummary,
|
|
348
397
|
});
|
|
@@ -659,12 +708,10 @@ function markRequirementsCompleteForPhase(cwd, phaseNum, roadmapContent) {
|
|
|
659
708
|
const reqPath = path.join(planningPath(cwd), REQUIREMENTS_FILE);
|
|
660
709
|
try {
|
|
661
710
|
const reqReadContent = fs.readFileSync(reqPath, 'utf-8');
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
);
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
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;
|
|
668
715
|
let reqContent = reqReadContent;
|
|
669
716
|
|
|
670
717
|
for (const reqId of reqIds) {
|
|
@@ -851,8 +898,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw, opts) {
|
|
|
851
898
|
result.roadmap_updated = false;
|
|
852
899
|
}
|
|
853
900
|
|
|
854
|
-
// Auto-commit .planning/ metadata unless --no-commit
|
|
855
|
-
|
|
901
|
+
// Auto-commit .planning/ metadata unless --no-commit, not a git repo, or the project
|
|
902
|
+
// keeps planning files out of git: `commit_docs: false` or a gitignored planning
|
|
903
|
+
// tree, the same rules `pan-tools commit` follows (this commit used to ignore both).
|
|
904
|
+
const noCommit = (opts && opts.noCommit)
|
|
905
|
+
|| loadConfig(cwd).commit_docs === false
|
|
906
|
+
|| isGitIgnored(cwd, planningRel());
|
|
856
907
|
if (!noCommit && isGitRepo(cwd)) {
|
|
857
908
|
const commitMsg = `docs(${normalized}): complete phase — ${phaseInfo.phase_name}`;
|
|
858
909
|
execGit(cwd, ['add', planningRel() + '/']);
|
|
@@ -930,6 +981,7 @@ module.exports = {
|
|
|
930
981
|
cmdPhaseNextDecimal,
|
|
931
982
|
cmdFindPhase,
|
|
932
983
|
cmdPhasePlanIndex,
|
|
984
|
+
blockedPlans,
|
|
933
985
|
cmdPhaseAdd,
|
|
934
986
|
cmdPhaseInsert,
|
|
935
987
|
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,
|
|
@@ -210,7 +211,8 @@ function buildPhaseDependencyGraph(cwd) {
|
|
|
210
211
|
let files = [];
|
|
211
212
|
try { files = fs.readdirSync(fullDir).filter(isPlanFile); } catch { continue; }
|
|
212
213
|
|
|
213
|
-
|
|
214
|
+
// LF, so the frontmatter match below also finds a CRLF plan's depends_on.
|
|
215
|
+
const phaseText = toLf(files.map(f => safeReadFile(path.join(fullDir, f)) || '').join('\n'));
|
|
214
216
|
|
|
215
217
|
// Explicit via frontmatter. Parse depends_on as freeform string (either
|
|
216
218
|
// inline `[phase:1, phase:2]`, or block-list). Extract all digit runs.
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
const fs = require('fs');
|
|
21
21
|
const path = require('path');
|
|
22
|
-
const { output, error, safeReadFile, toPosix } = require('./core.cjs');
|
|
22
|
+
const { output, error, safeReadFile, toPosix, escapeTableCell } = require('./core.cjs');
|
|
23
23
|
const { planningPath } = require('./utils.cjs');
|
|
24
24
|
const { publish } = require('./bus.cjs');
|
|
25
25
|
const { findVerdictBlocks, validateVerdict } = require('./verdict.cjs');
|
|
@@ -235,7 +235,7 @@ function writeDeepReview(cwd, phaseNum, payload, opts) {
|
|
|
235
235
|
const loc = f.file ? `\`${f.file}${f.line ? `:${f.line}` : ''}\`` : '—';
|
|
236
236
|
// Neutralize markdown-table-breaking chars: escape pipes and flatten any
|
|
237
237
|
// newlines so a finding description can't corrupt the table structure.
|
|
238
|
-
const desc =
|
|
238
|
+
const desc = escapeTableCell(f.description);
|
|
239
239
|
lines.push(`| ${f.severity} | ${f.source} | ${f.category} | ${desc} | ${loc} |`);
|
|
240
240
|
}
|
|
241
241
|
lines.push('');
|