@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.
- package/CHANGELOG.md +678 -0
- package/dashboard/public/css/command-center.css +152 -65
- package/dashboard/public/css/explorer.css +70 -41
- package/dashboard/public/css/layout.css +163 -2
- package/dashboard/public/css/settings.css +108 -110
- package/dashboard/public/css/timeline.css +2 -1
- package/dashboard/public/css/tokens.css +13 -0
- package/dashboard/public/js/sidebar-toggle.js +21 -7
- package/dashboard/src/components/Layout.tsx +51 -7
- package/dashboard/src/components/explorer/tabs/MilestonesTab.tsx +18 -2
- package/dashboard/src/components/explorer/tabs/PhasesTab.tsx +11 -1
- package/dashboard/src/components/explorer/tabs/TodosTab.tsx +25 -6
- package/dashboard/src/components/partials/AttentionPanel.tsx +7 -1
- package/dashboard/src/components/partials/CurrentPhaseCard.tsx +26 -24
- package/dashboard/src/components/partials/QuickActions.tsx +21 -11
- package/dashboard/src/components/partials/StatCardGrid.tsx +67 -0
- package/dashboard/src/components/partials/StatusHeader.tsx +2 -1
- package/dashboard/src/components/settings/LogEntryList.tsx +43 -5
- package/dashboard/src/routes/command-center.routes.tsx +8 -7
- package/dashboard/src/routes/index.routes.tsx +32 -29
- package/package.json +2 -2
- package/plugins/copilot-pbr/agents/audit.agent.md +128 -16
- package/plugins/copilot-pbr/agents/codebase-mapper.agent.md +48 -1
- package/plugins/copilot-pbr/agents/debugger.agent.md +47 -1
- package/plugins/copilot-pbr/agents/executor.agent.md +152 -8
- package/plugins/copilot-pbr/agents/general.agent.md +46 -1
- package/plugins/copilot-pbr/agents/integration-checker.agent.md +52 -2
- package/plugins/copilot-pbr/agents/plan-checker.agent.md +50 -2
- package/plugins/copilot-pbr/agents/planner.agent.md +54 -1
- package/plugins/copilot-pbr/agents/researcher.agent.md +47 -2
- package/plugins/copilot-pbr/agents/synthesizer.agent.md +49 -1
- package/plugins/copilot-pbr/agents/verifier.agent.md +86 -2
- package/plugins/copilot-pbr/hooks/hooks.json +11 -0
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/copilot-pbr/references/agent-contracts.md +27 -0
- package/plugins/copilot-pbr/references/checkpoints.md +32 -1
- package/plugins/copilot-pbr/references/context-quality-tiers.md +45 -0
- package/plugins/copilot-pbr/references/pbr-tools-cli.md +115 -0
- package/plugins/copilot-pbr/references/questioning.md +21 -1
- package/plugins/copilot-pbr/references/verification-patterns.md +52 -1
- package/plugins/copilot-pbr/skills/audit/SKILL.md +19 -3
- package/plugins/copilot-pbr/skills/begin/SKILL.md +57 -4
- package/plugins/copilot-pbr/skills/build/SKILL.md +39 -2
- package/plugins/copilot-pbr/skills/debug/SKILL.md +12 -1
- package/plugins/copilot-pbr/skills/explore/SKILL.md +13 -2
- package/plugins/copilot-pbr/skills/import/SKILL.md +26 -1
- package/plugins/copilot-pbr/skills/milestone/SKILL.md +15 -3
- package/plugins/copilot-pbr/skills/plan/SKILL.md +50 -0
- package/plugins/copilot-pbr/skills/quick/SKILL.md +21 -0
- package/plugins/copilot-pbr/skills/review/SKILL.md +45 -0
- package/plugins/copilot-pbr/skills/scan/SKILL.md +20 -0
- package/plugins/copilot-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
- package/plugins/copilot-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-pbr/agents/audit.md +51 -5
- package/plugins/cursor-pbr/agents/codebase-mapper.md +48 -1
- package/plugins/cursor-pbr/agents/debugger.md +47 -1
- package/plugins/cursor-pbr/agents/executor.md +152 -8
- package/plugins/cursor-pbr/agents/general.md +46 -1
- package/plugins/cursor-pbr/agents/integration-checker.md +51 -1
- package/plugins/cursor-pbr/agents/plan-checker.md +49 -1
- package/plugins/cursor-pbr/agents/planner.md +54 -1
- package/plugins/cursor-pbr/agents/researcher.md +46 -1
- package/plugins/cursor-pbr/agents/synthesizer.md +49 -1
- package/plugins/cursor-pbr/agents/verifier.md +85 -1
- package/plugins/cursor-pbr/hooks/hooks.json +9 -0
- package/plugins/cursor-pbr/references/agent-contracts.md +27 -0
- package/plugins/cursor-pbr/references/checkpoints.md +32 -1
- package/plugins/cursor-pbr/references/context-quality-tiers.md +45 -0
- package/plugins/cursor-pbr/references/pbr-tools-cli.md +115 -0
- package/plugins/cursor-pbr/references/questioning.md +21 -1
- package/plugins/cursor-pbr/references/verification-patterns.md +52 -1
- package/plugins/cursor-pbr/skills/audit/SKILL.md +19 -3
- package/plugins/cursor-pbr/skills/begin/SKILL.md +57 -4
- package/plugins/cursor-pbr/skills/build/SKILL.md +37 -2
- package/plugins/cursor-pbr/skills/debug/SKILL.md +12 -1
- package/plugins/cursor-pbr/skills/explore/SKILL.md +13 -2
- package/plugins/cursor-pbr/skills/import/SKILL.md +26 -1
- package/plugins/cursor-pbr/skills/milestone/SKILL.md +15 -3
- package/plugins/cursor-pbr/skills/plan/SKILL.md +50 -0
- package/plugins/cursor-pbr/skills/quick/SKILL.md +21 -0
- package/plugins/cursor-pbr/skills/review/SKILL.md +45 -0
- package/plugins/cursor-pbr/skills/scan/SKILL.md +20 -0
- package/plugins/cursor-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
- package/plugins/cursor-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/agents/audit.md +44 -0
- package/plugins/pbr/agents/codebase-mapper.md +47 -0
- package/plugins/pbr/agents/debugger.md +46 -0
- package/plugins/pbr/agents/executor.md +150 -6
- package/plugins/pbr/agents/general.md +45 -0
- package/plugins/pbr/agents/integration-checker.md +50 -0
- package/plugins/pbr/agents/plan-checker.md +48 -0
- package/plugins/pbr/agents/planner.md +51 -0
- package/plugins/pbr/agents/researcher.md +45 -0
- package/plugins/pbr/agents/synthesizer.md +48 -0
- package/plugins/pbr/agents/verifier.md +84 -0
- package/plugins/pbr/hooks/hooks.json +9 -0
- package/plugins/pbr/references/agent-contracts.md +27 -0
- package/plugins/pbr/references/checkpoints.md +32 -0
- package/plugins/pbr/references/context-quality-tiers.md +45 -0
- package/plugins/pbr/references/pbr-tools-cli.md +115 -0
- package/plugins/pbr/references/questioning.md +21 -0
- package/plugins/pbr/references/verification-patterns.md +52 -0
- package/plugins/pbr/scripts/check-plan-format.js +15 -3
- package/plugins/pbr/scripts/check-state-sync.js +26 -7
- package/plugins/pbr/scripts/check-subagent-output.js +30 -2
- package/plugins/pbr/scripts/config-schema.json +11 -1
- package/plugins/pbr/scripts/context-bridge.js +259 -0
- package/plugins/pbr/scripts/context-budget-check.js +2 -2
- package/plugins/pbr/scripts/lib/config.js +178 -0
- package/plugins/pbr/scripts/lib/core.js +578 -0
- package/plugins/pbr/scripts/lib/history.js +73 -0
- package/plugins/pbr/scripts/lib/init.js +166 -0
- package/plugins/pbr/scripts/lib/phase.js +364 -0
- package/plugins/pbr/scripts/lib/roadmap.js +175 -0
- package/plugins/pbr/scripts/lib/state.js +397 -0
- package/plugins/pbr/scripts/pbr-tools.js +346 -1235
- package/plugins/pbr/scripts/post-write-dispatch.js +5 -4
- package/plugins/pbr/scripts/post-write-quality.js +3 -3
- package/plugins/pbr/scripts/pre-write-dispatch.js +1 -1
- package/plugins/pbr/scripts/progress-tracker.js +1 -1
- package/plugins/pbr/scripts/suggest-compact.js +1 -1
- package/plugins/pbr/scripts/track-context-budget.js +53 -2
- package/plugins/pbr/scripts/validate-task.js +20 -28
- package/plugins/pbr/skills/audit/SKILL.md +19 -3
- package/plugins/pbr/skills/begin/SKILL.md +48 -2
- package/plugins/pbr/skills/build/SKILL.md +39 -2
- package/plugins/pbr/skills/debug/SKILL.md +12 -1
- package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +12 -1
- package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +12 -5
- package/plugins/pbr/skills/explore/SKILL.md +13 -2
- package/plugins/pbr/skills/import/SKILL.md +26 -1
- package/plugins/pbr/skills/milestone/SKILL.md +15 -3
- package/plugins/pbr/skills/plan/SKILL.md +52 -2
- package/plugins/pbr/skills/quick/SKILL.md +21 -0
- package/plugins/pbr/skills/review/SKILL.md +46 -0
- package/plugins/pbr/skills/scan/SKILL.md +20 -0
- package/plugins/pbr/templates/SUMMARY-complex.md.tmpl +95 -0
- 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
|
+
};
|