@sienklogic/plan-build-run 2.33.1 → 2.37.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.
Files changed (140) hide show
  1. package/CHANGELOG.md +678 -0
  2. package/dashboard/public/css/command-center.css +152 -65
  3. package/dashboard/public/css/explorer.css +70 -41
  4. package/dashboard/public/css/layout.css +163 -2
  5. package/dashboard/public/css/settings.css +108 -110
  6. package/dashboard/public/css/timeline.css +2 -1
  7. package/dashboard/public/css/tokens.css +13 -0
  8. package/dashboard/public/js/sidebar-toggle.js +21 -7
  9. package/dashboard/src/components/Layout.tsx +51 -7
  10. package/dashboard/src/components/explorer/tabs/MilestonesTab.tsx +18 -2
  11. package/dashboard/src/components/explorer/tabs/PhasesTab.tsx +11 -1
  12. package/dashboard/src/components/explorer/tabs/TodosTab.tsx +25 -6
  13. package/dashboard/src/components/partials/AttentionPanel.tsx +7 -1
  14. package/dashboard/src/components/partials/CurrentPhaseCard.tsx +26 -24
  15. package/dashboard/src/components/partials/QuickActions.tsx +21 -11
  16. package/dashboard/src/components/partials/StatCardGrid.tsx +67 -0
  17. package/dashboard/src/components/partials/StatusHeader.tsx +2 -1
  18. package/dashboard/src/components/settings/LogEntryList.tsx +43 -5
  19. package/dashboard/src/routes/command-center.routes.tsx +8 -7
  20. package/dashboard/src/routes/index.routes.tsx +32 -29
  21. package/package.json +2 -2
  22. package/plugins/copilot-pbr/agents/audit.agent.md +128 -16
  23. package/plugins/copilot-pbr/agents/codebase-mapper.agent.md +48 -1
  24. package/plugins/copilot-pbr/agents/debugger.agent.md +47 -1
  25. package/plugins/copilot-pbr/agents/executor.agent.md +152 -8
  26. package/plugins/copilot-pbr/agents/general.agent.md +46 -1
  27. package/plugins/copilot-pbr/agents/integration-checker.agent.md +52 -2
  28. package/plugins/copilot-pbr/agents/plan-checker.agent.md +50 -2
  29. package/plugins/copilot-pbr/agents/planner.agent.md +54 -1
  30. package/plugins/copilot-pbr/agents/researcher.agent.md +47 -2
  31. package/plugins/copilot-pbr/agents/synthesizer.agent.md +49 -1
  32. package/plugins/copilot-pbr/agents/verifier.agent.md +86 -2
  33. package/plugins/copilot-pbr/hooks/hooks.json +11 -0
  34. package/plugins/copilot-pbr/plugin.json +1 -1
  35. package/plugins/copilot-pbr/references/agent-contracts.md +27 -0
  36. package/plugins/copilot-pbr/references/checkpoints.md +32 -1
  37. package/plugins/copilot-pbr/references/context-quality-tiers.md +45 -0
  38. package/plugins/copilot-pbr/references/pbr-tools-cli.md +115 -0
  39. package/plugins/copilot-pbr/references/questioning.md +21 -1
  40. package/plugins/copilot-pbr/references/verification-patterns.md +52 -1
  41. package/plugins/copilot-pbr/skills/audit/SKILL.md +19 -3
  42. package/plugins/copilot-pbr/skills/begin/SKILL.md +57 -4
  43. package/plugins/copilot-pbr/skills/build/SKILL.md +39 -2
  44. package/plugins/copilot-pbr/skills/debug/SKILL.md +12 -1
  45. package/plugins/copilot-pbr/skills/explore/SKILL.md +13 -2
  46. package/plugins/copilot-pbr/skills/import/SKILL.md +26 -1
  47. package/plugins/copilot-pbr/skills/milestone/SKILL.md +15 -3
  48. package/plugins/copilot-pbr/skills/plan/SKILL.md +50 -0
  49. package/plugins/copilot-pbr/skills/quick/SKILL.md +21 -0
  50. package/plugins/copilot-pbr/skills/review/SKILL.md +45 -0
  51. package/plugins/copilot-pbr/skills/scan/SKILL.md +20 -0
  52. package/plugins/copilot-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  53. package/plugins/copilot-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
  54. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  55. package/plugins/cursor-pbr/agents/audit.md +51 -5
  56. package/plugins/cursor-pbr/agents/codebase-mapper.md +48 -1
  57. package/plugins/cursor-pbr/agents/debugger.md +47 -1
  58. package/plugins/cursor-pbr/agents/executor.md +152 -8
  59. package/plugins/cursor-pbr/agents/general.md +46 -1
  60. package/plugins/cursor-pbr/agents/integration-checker.md +51 -1
  61. package/plugins/cursor-pbr/agents/plan-checker.md +49 -1
  62. package/plugins/cursor-pbr/agents/planner.md +54 -1
  63. package/plugins/cursor-pbr/agents/researcher.md +46 -1
  64. package/plugins/cursor-pbr/agents/synthesizer.md +49 -1
  65. package/plugins/cursor-pbr/agents/verifier.md +85 -1
  66. package/plugins/cursor-pbr/hooks/hooks.json +9 -0
  67. package/plugins/cursor-pbr/references/agent-contracts.md +27 -0
  68. package/plugins/cursor-pbr/references/checkpoints.md +32 -1
  69. package/plugins/cursor-pbr/references/context-quality-tiers.md +45 -0
  70. package/plugins/cursor-pbr/references/pbr-tools-cli.md +115 -0
  71. package/plugins/cursor-pbr/references/questioning.md +21 -1
  72. package/plugins/cursor-pbr/references/verification-patterns.md +52 -1
  73. package/plugins/cursor-pbr/skills/audit/SKILL.md +19 -3
  74. package/plugins/cursor-pbr/skills/begin/SKILL.md +57 -4
  75. package/plugins/cursor-pbr/skills/build/SKILL.md +37 -2
  76. package/plugins/cursor-pbr/skills/debug/SKILL.md +12 -1
  77. package/plugins/cursor-pbr/skills/explore/SKILL.md +13 -2
  78. package/plugins/cursor-pbr/skills/import/SKILL.md +26 -1
  79. package/plugins/cursor-pbr/skills/milestone/SKILL.md +15 -3
  80. package/plugins/cursor-pbr/skills/plan/SKILL.md +50 -0
  81. package/plugins/cursor-pbr/skills/quick/SKILL.md +21 -0
  82. package/plugins/cursor-pbr/skills/review/SKILL.md +45 -0
  83. package/plugins/cursor-pbr/skills/scan/SKILL.md +20 -0
  84. package/plugins/cursor-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  85. package/plugins/cursor-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
  86. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  87. package/plugins/pbr/agents/audit.md +44 -0
  88. package/plugins/pbr/agents/codebase-mapper.md +47 -0
  89. package/plugins/pbr/agents/debugger.md +46 -0
  90. package/plugins/pbr/agents/executor.md +150 -6
  91. package/plugins/pbr/agents/general.md +45 -0
  92. package/plugins/pbr/agents/integration-checker.md +50 -0
  93. package/plugins/pbr/agents/plan-checker.md +48 -0
  94. package/plugins/pbr/agents/planner.md +51 -0
  95. package/plugins/pbr/agents/researcher.md +45 -0
  96. package/plugins/pbr/agents/synthesizer.md +48 -0
  97. package/plugins/pbr/agents/verifier.md +84 -0
  98. package/plugins/pbr/hooks/hooks.json +9 -0
  99. package/plugins/pbr/references/agent-contracts.md +27 -0
  100. package/plugins/pbr/references/checkpoints.md +32 -0
  101. package/plugins/pbr/references/context-quality-tiers.md +45 -0
  102. package/plugins/pbr/references/pbr-tools-cli.md +115 -0
  103. package/plugins/pbr/references/questioning.md +21 -0
  104. package/plugins/pbr/references/verification-patterns.md +52 -0
  105. package/plugins/pbr/scripts/check-plan-format.js +15 -3
  106. package/plugins/pbr/scripts/check-state-sync.js +26 -7
  107. package/plugins/pbr/scripts/check-subagent-output.js +30 -2
  108. package/plugins/pbr/scripts/config-schema.json +11 -1
  109. package/plugins/pbr/scripts/context-bridge.js +259 -0
  110. package/plugins/pbr/scripts/context-budget-check.js +2 -2
  111. package/plugins/pbr/scripts/lib/config.js +178 -0
  112. package/plugins/pbr/scripts/lib/core.js +578 -0
  113. package/plugins/pbr/scripts/lib/history.js +73 -0
  114. package/plugins/pbr/scripts/lib/init.js +166 -0
  115. package/plugins/pbr/scripts/lib/phase.js +364 -0
  116. package/plugins/pbr/scripts/lib/roadmap.js +175 -0
  117. package/plugins/pbr/scripts/lib/state.js +397 -0
  118. package/plugins/pbr/scripts/pbr-tools.js +346 -1235
  119. package/plugins/pbr/scripts/post-write-dispatch.js +5 -4
  120. package/plugins/pbr/scripts/post-write-quality.js +3 -3
  121. package/plugins/pbr/scripts/pre-write-dispatch.js +1 -1
  122. package/plugins/pbr/scripts/progress-tracker.js +1 -1
  123. package/plugins/pbr/scripts/suggest-compact.js +1 -1
  124. package/plugins/pbr/scripts/track-context-budget.js +53 -2
  125. package/plugins/pbr/scripts/validate-task.js +20 -28
  126. package/plugins/pbr/skills/audit/SKILL.md +19 -3
  127. package/plugins/pbr/skills/begin/SKILL.md +48 -2
  128. package/plugins/pbr/skills/build/SKILL.md +39 -2
  129. package/plugins/pbr/skills/debug/SKILL.md +12 -1
  130. package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +12 -1
  131. package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +12 -5
  132. package/plugins/pbr/skills/explore/SKILL.md +13 -2
  133. package/plugins/pbr/skills/import/SKILL.md +26 -1
  134. package/plugins/pbr/skills/milestone/SKILL.md +15 -3
  135. package/plugins/pbr/skills/plan/SKILL.md +52 -2
  136. package/plugins/pbr/skills/quick/SKILL.md +21 -0
  137. package/plugins/pbr/skills/review/SKILL.md +46 -0
  138. package/plugins/pbr/skills/scan/SKILL.md +20 -0
  139. package/plugins/pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  140. package/plugins/pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * lib/init.js — Compound init commands for Plan-Build-Run tools.
3
+ *
4
+ * These aggregate state from multiple sources into single JSON payloads
5
+ * for skill initialization. Each init function returns everything a skill
6
+ * needs to start work without additional file reads.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { stateLoad, stateCheckProgress } = require('./state');
12
+ const { configLoad, resolveDepthProfile } = require('./config');
13
+ const { phaseInfo, planIndex } = require('./phase');
14
+
15
+ /**
16
+ * Initialize context for executing a phase (building plans).
17
+ *
18
+ * @param {string} phaseNum - Phase number
19
+ * @param {string} [planningDir] - Path to .planning directory
20
+ */
21
+ function initExecutePhase(phaseNum, planningDir) {
22
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
23
+ const state = stateLoad(dir);
24
+ if (!state.exists) return { error: "No .planning/ directory found" };
25
+ const phase = phaseInfo(phaseNum, dir);
26
+ if (phase.error) return { error: phase.error };
27
+ const plans = planIndex(phaseNum, dir);
28
+ const config = configLoad(dir) || {};
29
+ const depthProfile = resolveDepthProfile(config);
30
+ const models = config.models || {};
31
+ let gitState = { branch: null, clean: null };
32
+ try {
33
+ const { execSync } = require("child_process");
34
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8", timeout: 5000 }).trim();
35
+ const status = execSync("git status --porcelain", { encoding: "utf8", timeout: 5000 }).trim();
36
+ gitState = { branch, clean: status === "" };
37
+ } catch (_e) { /* not a git repo */ }
38
+ return {
39
+ executor_model: models.executor || "sonnet",
40
+ verifier_model: models.verifier || "sonnet",
41
+ config: { depth: depthProfile.depth, mode: config.mode || "interactive", parallelization: config.parallelization || { enabled: false }, planning: config.planning || {}, gates: config.gates || {}, features: config.features || {} },
42
+ phase: { num: phaseNum, dir: phase.phase, name: phase.name, goal: phase.goal, has_context: phase.has_context, status: phase.filesystem_status, plan_count: phase.plan_count, completed: phase.completed },
43
+ plans: (plans.plans || []).map(p => ({ file: p.file, plan_id: p.plan_id, wave: p.wave, autonomous: p.autonomous, has_summary: p.has_summary, must_haves_count: p.must_haves_count, depends_on: p.depends_on })),
44
+ waves: plans.waves || {},
45
+ branch_name: gitState.branch, git_clean: gitState.clean
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Initialize context for planning a phase.
51
+ *
52
+ * @param {string} phaseNum - Phase number
53
+ * @param {string} [planningDir] - Path to .planning directory
54
+ */
55
+ function initPlanPhase(phaseNum, planningDir) {
56
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
57
+ const state = stateLoad(dir);
58
+ if (!state.exists) return { error: "No .planning/ directory found" };
59
+ const config = configLoad(dir) || {};
60
+ const models = config.models || {};
61
+ const depthProfile = resolveDepthProfile(config);
62
+ const phasesDir = path.join(dir, "phases");
63
+ const paddedPhase = String(phaseNum).padStart(2, "0");
64
+ let existingArtifacts = [], phaseDirName = null;
65
+ if (fs.existsSync(phasesDir)) {
66
+ const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(paddedPhase + "-"));
67
+ if (dirs.length > 0) { phaseDirName = dirs[0]; existingArtifacts = fs.readdirSync(path.join(phasesDir, phaseDirName)).filter(f => f.endsWith(".md")); }
68
+ }
69
+ let phaseGoal = null, phaseDeps = null;
70
+ if (state.roadmap && state.roadmap.phases) {
71
+ const rp = state.roadmap.phases.find(p => p.number === paddedPhase);
72
+ if (rp) { phaseGoal = rp.goal; phaseDeps = rp.depends_on; }
73
+ }
74
+ return {
75
+ researcher_model: models.researcher || "sonnet", planner_model: models.planner || "sonnet", checker_model: models.planner || "sonnet",
76
+ config: { depth: depthProfile.depth, profile: depthProfile.profile, features: config.features || {}, planning: config.planning || {} },
77
+ phase: { num: phaseNum, dir: phaseDirName, goal: phaseGoal, depends_on: phaseDeps },
78
+ existing_artifacts: existingArtifacts,
79
+ workflow: { research_phase: (config.features || {}).research_phase !== false, plan_checking: (config.features || {}).plan_checking !== false }
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Initialize context for a quick task.
85
+ *
86
+ * @param {string} description - Task description
87
+ * @param {string} [planningDir] - Path to .planning directory
88
+ */
89
+ function initQuick(description, planningDir) {
90
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
91
+ const config = configLoad(dir) || {};
92
+ const quickDir = path.join(dir, "quick");
93
+ let nextNum = 1;
94
+ if (fs.existsSync(quickDir)) {
95
+ const dirs = fs.readdirSync(quickDir).filter(d => /^\d{3}-/.test(d)).sort();
96
+ if (dirs.length > 0) { nextNum = parseInt(dirs[dirs.length - 1].substring(0, 3), 10) + 1; }
97
+ }
98
+ const slug = (description || "task").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").substring(0, 30);
99
+ const paddedNum = String(nextNum).padStart(3, "0");
100
+ return {
101
+ next_task_number: paddedNum, slug, dir: path.join(".planning", "quick", paddedNum + "-" + slug),
102
+ dir_name: paddedNum + "-" + slug, timestamp: new Date().toISOString(),
103
+ config: { depth: config.depth || "standard", mode: config.mode || "interactive", models: config.models || {}, planning: config.planning || {} }
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Initialize context for verifying phase work.
109
+ *
110
+ * @param {string} phaseNum - Phase number
111
+ * @param {string} [planningDir] - Path to .planning directory
112
+ */
113
+ function initVerifyWork(phaseNum, planningDir) {
114
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
115
+ const phase = phaseInfo(phaseNum, dir);
116
+ if (phase.error) return { error: phase.error };
117
+ const config = configLoad(dir) || {};
118
+ const models = config.models || {};
119
+ let priorAttempts = 0;
120
+ if (phase.verification) { priorAttempts = parseInt(phase.verification.attempt, 10) || 0; }
121
+ return {
122
+ verifier_model: models.verifier || "sonnet",
123
+ phase: { num: phaseNum, dir: phase.phase, name: phase.name, goal: phase.goal, plan_count: phase.plan_count, completed: phase.completed },
124
+ has_verification: !!phase.verification, prior_attempts: priorAttempts,
125
+ prior_status: phase.verification ? (phase.verification.status || "unknown") : null,
126
+ summaries: phase.summaries || []
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Initialize context for resuming work.
132
+ *
133
+ * @param {string} [planningDir] - Path to .planning directory
134
+ */
135
+ function initResume(planningDir) {
136
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
137
+ const state = stateLoad(dir);
138
+ if (!state.exists) return { error: "No .planning/ directory found" };
139
+ let autoNext = null, continueHere = null, activeSkill = null;
140
+ try { autoNext = fs.readFileSync(path.join(dir, ".auto-next"), "utf8").trim(); } catch (_e) { /* file not found */ }
141
+ try { continueHere = fs.readFileSync(path.join(dir, ".continue-here"), "utf8").trim(); } catch (_e) { /* file not found */ }
142
+ try { activeSkill = fs.readFileSync(path.join(dir, ".active-skill"), "utf8").trim(); } catch (_e) { /* file not found */ }
143
+ return { state: state.state, auto_next: autoNext, continue_here: continueHere, active_skill: activeSkill, current_phase: state.current_phase, progress: state.progress };
144
+ }
145
+
146
+ /**
147
+ * Initialize context for progress display.
148
+ *
149
+ * @param {string} [planningDir] - Path to .planning directory
150
+ */
151
+ function initProgress(planningDir) {
152
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
153
+ const state = stateLoad(dir);
154
+ if (!state.exists) return { error: "No .planning/ directory found" };
155
+ const progress = stateCheckProgress(dir);
156
+ return { current_phase: state.current_phase, total_phases: state.phase_count, status: state.state ? state.state.status : null, phases: progress.phases, total_plans: progress.total_plans, completed_plans: progress.completed_plans, percentage: progress.percentage };
157
+ }
158
+
159
+ module.exports = {
160
+ initExecutePhase,
161
+ initPlanPhase,
162
+ initQuick,
163
+ initVerifyWork,
164
+ initResume,
165
+ initProgress
166
+ };
@@ -0,0 +1,364 @@
1
+ /**
2
+ * lib/phase.js — Phase operations for Plan-Build-Run tools.
3
+ *
4
+ * Handles phase directory management (add/remove/list), plan indexing,
5
+ * must-haves collection, and comprehensive phase info.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const {
11
+ parseYamlFrontmatter,
12
+ findFiles,
13
+ countMustHaves,
14
+ determinePhaseStatus
15
+ } = require('./core');
16
+
17
+ /**
18
+ * Parse a markdown file's YAML frontmatter and return as JSON.
19
+ * Wraps parseYamlFrontmatter().
20
+ *
21
+ * @param {string} filePath - Path to markdown file
22
+ * @returns {object} Parsed frontmatter or error
23
+ */
24
+ function frontmatter(filePath) {
25
+ const resolved = path.resolve(filePath);
26
+ if (!fs.existsSync(resolved)) {
27
+ return { error: `File not found: ${resolved}` };
28
+ }
29
+ const content = fs.readFileSync(resolved, 'utf8');
30
+ return parseYamlFrontmatter(content);
31
+ }
32
+
33
+ /**
34
+ * Plan inventory for a phase, grouped by wave.
35
+ *
36
+ * @param {string} phaseNum - Phase number
37
+ * @param {string} [planningDir] - Path to .planning directory
38
+ * @returns {object} Plan index
39
+ */
40
+ function planIndex(phaseNum, planningDir) {
41
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
42
+ const phasesDir = path.join(dir, 'phases');
43
+ if (!fs.existsSync(phasesDir)) {
44
+ return { error: 'No phases directory found' };
45
+ }
46
+
47
+ // Find phase directory matching the number
48
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true })
49
+ .filter(e => e.isDirectory());
50
+
51
+ const phaseDir = entries.find(e => e.name.startsWith(phaseNum.padStart(2, '0') + '-'));
52
+ if (!phaseDir) {
53
+ return { error: `No phase directory found matching phase ${phaseNum}` };
54
+ }
55
+
56
+ const fullDir = path.join(phasesDir, phaseDir.name);
57
+ const planFiles = findFiles(fullDir, /-PLAN\.md$/);
58
+
59
+ const plans = [];
60
+ const waves = {};
61
+
62
+ for (const file of planFiles) {
63
+ const content = fs.readFileSync(path.join(fullDir, file), 'utf8');
64
+ const fm = parseYamlFrontmatter(content);
65
+
66
+ const plan = {
67
+ file,
68
+ plan_id: fm.plan || file.replace(/-PLAN\.md$/, ''),
69
+ wave: parseInt(fm.wave, 10) || 1,
70
+ type: fm.type || 'unknown',
71
+ autonomous: fm.autonomous !== false,
72
+ depends_on: fm.depends_on || [],
73
+ gap_closure: fm.gap_closure || false,
74
+ has_summary: fs.existsSync(path.join(fullDir, `SUMMARY-${fm.plan || ''}.md`)),
75
+ must_haves_count: countMustHaves(fm.must_haves)
76
+ };
77
+
78
+ plans.push(plan);
79
+
80
+ const waveKey = `wave_${plan.wave}`;
81
+ if (!waves[waveKey]) waves[waveKey] = [];
82
+ waves[waveKey].push(plan.plan_id);
83
+ }
84
+
85
+ return {
86
+ phase: phaseDir.name,
87
+ total_plans: plans.length,
88
+ plans,
89
+ waves
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Collect all must-haves from all PLAN.md files in a phase.
95
+ * Returns per-plan grouping + flat deduplicated list + total count.
96
+ *
97
+ * @param {string} phaseNum - Phase number
98
+ * @param {string} [planningDir] - Path to .planning directory
99
+ */
100
+ function mustHavesCollect(phaseNum, planningDir) {
101
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
102
+ const phasesDir = path.join(dir, 'phases');
103
+ if (!fs.existsSync(phasesDir)) {
104
+ return { error: 'No phases directory found' };
105
+ }
106
+
107
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true })
108
+ .filter(e => e.isDirectory());
109
+ const phaseDir = entries.find(e => e.name.startsWith(phaseNum.padStart(2, '0') + '-'));
110
+ if (!phaseDir) {
111
+ return { error: `No phase directory found matching phase ${phaseNum}` };
112
+ }
113
+
114
+ const fullDir = path.join(phasesDir, phaseDir.name);
115
+ const planFiles = findFiles(fullDir, /-PLAN\.md$/);
116
+
117
+ const perPlan = {};
118
+ const allTruths = new Set();
119
+ const allArtifacts = new Set();
120
+ const allKeyLinks = new Set();
121
+
122
+ for (const file of planFiles) {
123
+ const content = fs.readFileSync(path.join(fullDir, file), 'utf8');
124
+ const fm = parseYamlFrontmatter(content);
125
+ const planId = fm.plan || file.replace(/-PLAN\.md$/, '');
126
+ const mh = fm.must_haves || { truths: [], artifacts: [], key_links: [] };
127
+
128
+ perPlan[planId] = mh;
129
+ (mh.truths || []).forEach(t => allTruths.add(t));
130
+ (mh.artifacts || []).forEach(a => allArtifacts.add(a));
131
+ (mh.key_links || []).forEach(k => allKeyLinks.add(k));
132
+ }
133
+
134
+ const all = {
135
+ truths: [...allTruths],
136
+ artifacts: [...allArtifacts],
137
+ key_links: [...allKeyLinks]
138
+ };
139
+
140
+ return {
141
+ phase: phaseDir.name,
142
+ plans: perPlan,
143
+ all,
144
+ total: all.truths.length + all.artifacts.length + all.key_links.length
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Comprehensive single-phase status combining roadmap, filesystem, and plan data.
150
+ *
151
+ * @param {string} phaseNum - Phase number
152
+ * @param {string} [planningDir] - Path to .planning directory
153
+ */
154
+ function phaseInfo(phaseNum, planningDir) {
155
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
156
+ const { parseRoadmapMd } = require('./roadmap');
157
+ const phasesDir = path.join(dir, 'phases');
158
+ if (!fs.existsSync(phasesDir)) {
159
+ return { error: 'No phases directory found' };
160
+ }
161
+
162
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true })
163
+ .filter(e => e.isDirectory());
164
+ const phaseDir = entries.find(e => e.name.startsWith(phaseNum.padStart(2, '0') + '-'));
165
+ if (!phaseDir) {
166
+ return { error: `No phase directory found matching phase ${phaseNum}` };
167
+ }
168
+
169
+ const fullDir = path.join(phasesDir, phaseDir.name);
170
+
171
+ // Get roadmap info
172
+ let roadmapInfo = null;
173
+ const roadmapPath = path.join(dir, 'ROADMAP.md');
174
+ if (fs.existsSync(roadmapPath)) {
175
+ const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
176
+ const roadmap = parseRoadmapMd(roadmapContent);
177
+ roadmapInfo = roadmap.phases.find(p => p.number === phaseNum.padStart(2, '0')) || null;
178
+ }
179
+
180
+ // Get plan index
181
+ const plans = planIndex(phaseNum, dir);
182
+
183
+ // Check for verification
184
+ const verificationPath = path.join(fullDir, 'VERIFICATION.md');
185
+ let verification = null;
186
+ if (fs.existsSync(verificationPath)) {
187
+ const vContent = fs.readFileSync(verificationPath, 'utf8');
188
+ verification = parseYamlFrontmatter(vContent);
189
+ }
190
+
191
+ // Check summaries
192
+ const summaryFiles = findFiles(fullDir, /^SUMMARY-.*\.md$/);
193
+ const summaries = summaryFiles.map(f => {
194
+ const content = fs.readFileSync(path.join(fullDir, f), 'utf8');
195
+ const fm = parseYamlFrontmatter(content);
196
+ return { file: f, plan: fm.plan || f.replace(/^SUMMARY-|\.md$/g, ''), status: fm.status || 'unknown' };
197
+ });
198
+
199
+ // Determine filesystem status
200
+ const planCount = plans.total_plans || 0;
201
+ const completedCount = summaries.filter(s => s.status === 'complete').length;
202
+ const hasVerification = fs.existsSync(verificationPath);
203
+ const fsStatus = determinePhaseStatus(planCount, completedCount, summaryFiles.length, hasVerification, fullDir);
204
+
205
+ return {
206
+ phase: phaseDir.name,
207
+ name: roadmapInfo ? roadmapInfo.name : phaseDir.name.replace(/^\d+-/, ''),
208
+ goal: roadmapInfo ? roadmapInfo.goal : null,
209
+ roadmap_status: roadmapInfo ? roadmapInfo.status : null,
210
+ filesystem_status: fsStatus,
211
+ plans: plans.plans || [],
212
+ plan_count: planCount,
213
+ summaries,
214
+ completed: completedCount,
215
+ verification,
216
+ has_context: fs.existsSync(path.join(fullDir, 'CONTEXT.md'))
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Add a new phase directory (with renumbering).
222
+ *
223
+ * @param {string} slug - Phase slug
224
+ * @param {string|null} afterPhase - Insert after this phase number
225
+ * @param {string} [planningDir] - Path to .planning directory
226
+ */
227
+ function phaseAdd(slug, afterPhase, planningDir) {
228
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
229
+ const phasesDir = path.join(dir, 'phases');
230
+ if (!fs.existsSync(phasesDir)) {
231
+ fs.mkdirSync(phasesDir, { recursive: true });
232
+ }
233
+
234
+ // Determine next phase number
235
+ const existing = fs.readdirSync(phasesDir)
236
+ .filter(d => /^\d+-/.test(d))
237
+ .map(d => parseInt(d.split('-')[0], 10))
238
+ .sort((a, b) => a - b);
239
+
240
+ let newNum;
241
+ if (afterPhase) {
242
+ const after = parseInt(afterPhase, 10);
243
+ // Find the next number after the specified phase
244
+ const higher = existing.filter(n => n > after);
245
+ if (higher.length > 0) {
246
+ // Need to renumber: insert between after and next
247
+ newNum = after + 1;
248
+ // Renumber all phases >= newNum
249
+ for (const dirName of fs.readdirSync(phasesDir).sort().reverse()) {
250
+ const num = parseInt(dirName.split('-')[0], 10);
251
+ if (num >= newNum) {
252
+ const newName = dirName.replace(/^\d+/, String(num + 1).padStart(2, '0'));
253
+ fs.renameSync(path.join(phasesDir, dirName), path.join(phasesDir, newName));
254
+ }
255
+ }
256
+ } else {
257
+ newNum = after + 1;
258
+ }
259
+ } else {
260
+ newNum = existing.length > 0 ? Math.max(...existing) + 1 : 1;
261
+ }
262
+
263
+ const dirName = `${String(newNum).padStart(2, '0')}-${slug}`;
264
+ const fullPath = path.join(phasesDir, dirName);
265
+ fs.mkdirSync(fullPath, { recursive: true });
266
+
267
+ return {
268
+ phase: newNum,
269
+ slug,
270
+ directory: dirName,
271
+ path: fullPath,
272
+ renumbered: afterPhase ? true : false
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Remove an empty phase directory (with renumbering).
278
+ *
279
+ * @param {string} phaseNum - Phase number to remove
280
+ * @param {string} [planningDir] - Path to .planning directory
281
+ */
282
+ function phaseRemove(phaseNum, planningDir) {
283
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
284
+ const phasesDir = path.join(dir, 'phases');
285
+ const padded = String(phaseNum).padStart(2, '0');
286
+ const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(padded + '-'));
287
+
288
+ if (dirs.length === 0) {
289
+ return { removed: false, error: `Phase ${phaseNum} not found` };
290
+ }
291
+
292
+ const dirName = dirs[0];
293
+ const fullPath = path.join(phasesDir, dirName);
294
+
295
+ // Check if phase has artifacts
296
+ const contents = fs.readdirSync(fullPath);
297
+ if (contents.length > 0) {
298
+ return {
299
+ removed: false,
300
+ error: `Phase ${phaseNum} (${dirName}) has ${contents.length} files. Remove contents first or use --force.`,
301
+ files: contents
302
+ };
303
+ }
304
+
305
+ fs.rmdirSync(fullPath);
306
+
307
+ // Renumber subsequent phases
308
+ const allDirs = fs.readdirSync(phasesDir)
309
+ .filter(d => /^\d+-/.test(d))
310
+ .sort();
311
+
312
+ for (const d of allDirs) {
313
+ const num = parseInt(d.split('-')[0], 10);
314
+ if (num > parseInt(phaseNum, 10)) {
315
+ const newName = d.replace(/^\d+/, String(num - 1).padStart(2, '0'));
316
+ fs.renameSync(path.join(phasesDir, d), path.join(phasesDir, newName));
317
+ }
318
+ }
319
+
320
+ return {
321
+ removed: true,
322
+ directory: dirName,
323
+ renumbered: true
324
+ };
325
+ }
326
+
327
+ /**
328
+ * List all phase directories with status.
329
+ *
330
+ * @param {string} [planningDir] - Path to .planning directory
331
+ */
332
+ function phaseList(planningDir) {
333
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
334
+ const phasesDir = path.join(dir, 'phases');
335
+ if (!fs.existsSync(phasesDir)) {
336
+ return { phases: [] };
337
+ }
338
+
339
+ const phases = fs.readdirSync(phasesDir)
340
+ .filter(d => /^\d+-/.test(d))
341
+ .sort()
342
+ .map(d => {
343
+ const num = parseInt(d.split('-')[0], 10);
344
+ const slug = d.replace(/^\d+-/, '');
345
+ const fullPath = path.join(phasesDir, d);
346
+ const files = fs.readdirSync(fullPath);
347
+ const hasPlan = files.some(f => /^PLAN/i.test(f));
348
+ const hasSummary = files.some(f => /^SUMMARY/i.test(f));
349
+ const hasVerification = files.some(f => /^VERIFICATION/i.test(f));
350
+ return { num, slug, directory: d, files: files.length, hasPlan, hasSummary, hasVerification };
351
+ });
352
+
353
+ return { phases };
354
+ }
355
+
356
+ module.exports = {
357
+ frontmatter,
358
+ planIndex,
359
+ mustHavesCollect,
360
+ phaseInfo,
361
+ phaseAdd,
362
+ phaseRemove,
363
+ phaseList
364
+ };
@@ -0,0 +1,175 @@
1
+ /**
2
+ * lib/roadmap.js — ROADMAP.md operations for Plan-Build-Run tools.
3
+ *
4
+ * Handles parsing and updating the ROADMAP.md phase overview table.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { lockedFileUpdate, validateStatusTransition } = require('./core');
10
+
11
+ // --- Parsers ---
12
+
13
+ function parseRoadmapMd(content) {
14
+ const result = { phases: [], has_progress_table: false };
15
+
16
+ // Find Phase Overview table
17
+ const overviewMatch = content.match(/## Phase Overview[\s\S]*?\|[\s\S]*?(?=\n##|\s*$)/);
18
+ if (overviewMatch) {
19
+ const rows = overviewMatch[0].split('\n').filter(r => r.includes('|'));
20
+ // Skip header and separator rows
21
+ for (let i = 2; i < rows.length; i++) {
22
+ const cols = rows[i].split('|').map(c => c.trim()).filter(Boolean);
23
+ if (cols.length >= 3) {
24
+ result.phases.push({
25
+ number: cols[0],
26
+ name: cols[1],
27
+ goal: cols[2],
28
+ plans: cols[3] || '',
29
+ wave: cols[4] || '',
30
+ status: cols[5] || 'pending'
31
+ });
32
+ }
33
+ }
34
+ }
35
+
36
+ // Check for Progress table
37
+ result.has_progress_table = /## Progress/.test(content);
38
+
39
+ return result;
40
+ }
41
+
42
+ // --- Table helpers ---
43
+
44
+ /**
45
+ * Find the row index of a phase in a ROADMAP.md table.
46
+ * @returns {number} Line index or -1 if not found
47
+ */
48
+ function findRoadmapRow(lines, phaseNum) {
49
+ const paddedPhase = phaseNum.padStart(2, '0');
50
+ for (let i = 0; i < lines.length; i++) {
51
+ if (!lines[i].includes('|')) continue;
52
+ const parts = lines[i].split('|');
53
+ if (parts.length < 3) continue;
54
+ const phaseCol = parts[1] ? parts[1].trim() : '';
55
+ if (phaseCol === paddedPhase) {
56
+ return i;
57
+ }
58
+ }
59
+ return -1;
60
+ }
61
+
62
+ /**
63
+ * Update a specific column in a markdown table row.
64
+ * @param {string} row - The full table row string (e.g., "| 01 | Setup | ... |")
65
+ * @param {number} columnIndex - 0-based column index (Phase=0, Name=1, ..., Status=5)
66
+ * @param {string} newValue - New cell value
67
+ * @returns {string} Updated row
68
+ */
69
+ function updateTableRow(row, columnIndex, newValue) {
70
+ const parts = row.split('|');
71
+ // parts[0] is empty (before first |), data starts at parts[1]
72
+ const partIndex = columnIndex + 1;
73
+ if (partIndex < parts.length) {
74
+ parts[partIndex] = ` ${newValue} `;
75
+ }
76
+ return parts.join('|');
77
+ }
78
+
79
+ // --- Mutation commands ---
80
+
81
+ /**
82
+ * Update the Status column for a phase in ROADMAP.md's Phase Overview table.
83
+ *
84
+ * @param {string} phaseNum - Phase number
85
+ * @param {string} newStatus - New status value
86
+ * @param {string} [planningDir] - Path to .planning directory
87
+ */
88
+ function roadmapUpdateStatus(phaseNum, newStatus, planningDir) {
89
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
90
+ const roadmapPath = path.join(dir, 'ROADMAP.md');
91
+ if (!fs.existsSync(roadmapPath)) {
92
+ return { success: false, error: 'ROADMAP.md not found' };
93
+ }
94
+
95
+ let oldStatus = null;
96
+
97
+ const result = lockedFileUpdate(roadmapPath, (content) => {
98
+ const lines = content.split('\n');
99
+ const rowIdx = findRoadmapRow(lines, phaseNum);
100
+ if (rowIdx === -1) {
101
+ return content; // No matching row found
102
+ }
103
+ const parts = lines[rowIdx].split('|');
104
+ oldStatus = parts[6] ? parts[6].trim() : 'unknown';
105
+ lines[rowIdx] = updateTableRow(lines[rowIdx], 5, newStatus);
106
+ return lines.join('\n');
107
+ });
108
+
109
+ if (!oldStatus) {
110
+ return { success: false, error: `Phase ${phaseNum} not found in ROADMAP.md table` };
111
+ }
112
+
113
+ // Advisory transition validation — warn on suspicious transitions but don't block
114
+ const transition = validateStatusTransition(oldStatus, newStatus);
115
+ if (!transition.valid && transition.warning) {
116
+ process.stderr.write(`[pbr-tools] WARNING: ${transition.warning}\n`);
117
+ }
118
+
119
+ if (result.success) {
120
+ const response = { success: true, old_status: oldStatus, new_status: newStatus };
121
+ if (!transition.valid) {
122
+ response.transition_warning = transition.warning;
123
+ }
124
+ return response;
125
+ }
126
+ return { success: false, error: result.error };
127
+ }
128
+
129
+ /**
130
+ * Update the Plans column for a phase in ROADMAP.md's Phase Overview table.
131
+ *
132
+ * @param {string} phaseNum - Phase number
133
+ * @param {string} complete - Completed plan count
134
+ * @param {string} total - Total plan count
135
+ * @param {string} [planningDir] - Path to .planning directory
136
+ */
137
+ function roadmapUpdatePlans(phaseNum, complete, total, planningDir) {
138
+ const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
139
+ const roadmapPath = path.join(dir, 'ROADMAP.md');
140
+ if (!fs.existsSync(roadmapPath)) {
141
+ return { success: false, error: 'ROADMAP.md not found' };
142
+ }
143
+
144
+ let oldPlans = null;
145
+ const newPlans = `${complete}/${total}`;
146
+
147
+ const result = lockedFileUpdate(roadmapPath, (content) => {
148
+ const lines = content.split('\n');
149
+ const rowIdx = findRoadmapRow(lines, phaseNum);
150
+ if (rowIdx === -1) {
151
+ return content;
152
+ }
153
+ const parts = lines[rowIdx].split('|');
154
+ oldPlans = parts[4] ? parts[4].trim() : 'unknown';
155
+ lines[rowIdx] = updateTableRow(lines[rowIdx], 3, newPlans);
156
+ return lines.join('\n');
157
+ });
158
+
159
+ if (!oldPlans) {
160
+ return { success: false, error: `Phase ${phaseNum} not found in ROADMAP.md table` };
161
+ }
162
+
163
+ if (result.success) {
164
+ return { success: true, old_plans: oldPlans, new_plans: newPlans };
165
+ }
166
+ return { success: false, error: result.error };
167
+ }
168
+
169
+ module.exports = {
170
+ parseRoadmapMd,
171
+ findRoadmapRow,
172
+ updateTableRow,
173
+ roadmapUpdateStatus,
174
+ roadmapUpdatePlans
175
+ };