@sienklogic/plan-build-run 2.34.0 → 2.38.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 +683 -0
- package/dashboard/public/css/command-center.css +152 -65
- package/dashboard/public/css/explorer.css +22 -41
- package/dashboard/public/css/layout.css +119 -1
- package/dashboard/public/css/tokens.css +13 -0
- package/dashboard/src/components/Layout.tsx +32 -6
- package/dashboard/src/components/explorer/tabs/PhasesTab.tsx +11 -1
- package/dashboard/src/components/explorer/tabs/TodosTab.tsx +18 -2
- 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 +1 -0
- 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 +129 -16
- package/plugins/copilot-pbr/agents/codebase-mapper.agent.md +49 -1
- package/plugins/copilot-pbr/agents/debugger.agent.md +50 -1
- package/plugins/copilot-pbr/agents/dev-sync.agent.md +23 -0
- package/plugins/copilot-pbr/agents/executor.agent.md +153 -8
- package/plugins/copilot-pbr/agents/general.agent.md +46 -1
- package/plugins/copilot-pbr/agents/integration-checker.agent.md +55 -2
- package/plugins/copilot-pbr/agents/plan-checker.agent.md +50 -2
- package/plugins/copilot-pbr/agents/planner.agent.md +80 -1
- package/plugins/copilot-pbr/agents/researcher.agent.md +50 -2
- package/plugins/copilot-pbr/agents/synthesizer.agent.md +49 -1
- package/plugins/copilot-pbr/agents/verifier.agent.md +114 -13
- package/plugins/copilot-pbr/commands/test.md +5 -0
- 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 +96 -18
- 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/config/SKILL.md +12 -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/health/SKILL.md +13 -5
- 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/skills/setup/SKILL.md +9 -1
- package/plugins/copilot-pbr/skills/shared/context-budget.md +10 -0
- package/plugins/copilot-pbr/skills/shared/universal-anti-patterns.md +6 -0
- package/plugins/copilot-pbr/skills/test/SKILL.md +210 -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 +52 -5
- package/plugins/cursor-pbr/agents/codebase-mapper.md +49 -1
- package/plugins/cursor-pbr/agents/debugger.md +50 -1
- package/plugins/cursor-pbr/agents/dev-sync.md +23 -0
- package/plugins/cursor-pbr/agents/executor.md +153 -8
- package/plugins/cursor-pbr/agents/general.md +46 -1
- package/plugins/cursor-pbr/agents/integration-checker.md +54 -1
- package/plugins/cursor-pbr/agents/plan-checker.md +49 -1
- package/plugins/cursor-pbr/agents/planner.md +80 -1
- package/plugins/cursor-pbr/agents/researcher.md +49 -1
- package/plugins/cursor-pbr/agents/synthesizer.md +49 -1
- package/plugins/cursor-pbr/agents/verifier.md +113 -12
- package/plugins/cursor-pbr/commands/test.md +5 -0
- 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 +96 -18
- 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/config/SKILL.md +12 -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/health/SKILL.md +14 -5
- 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/skills/setup/SKILL.md +9 -1
- package/plugins/cursor-pbr/skills/shared/context-budget.md +10 -0
- package/plugins/cursor-pbr/skills/shared/universal-anti-patterns.md +6 -0
- package/plugins/cursor-pbr/skills/test/SKILL.md +211 -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 +45 -0
- package/plugins/pbr/agents/codebase-mapper.md +48 -0
- package/plugins/pbr/agents/debugger.md +49 -0
- package/plugins/pbr/agents/dev-sync.md +23 -0
- package/plugins/pbr/agents/executor.md +151 -6
- package/plugins/pbr/agents/general.md +45 -0
- package/plugins/pbr/agents/integration-checker.md +53 -0
- package/plugins/pbr/agents/plan-checker.md +48 -0
- package/plugins/pbr/agents/planner.md +78 -1
- package/plugins/pbr/agents/researcher.md +48 -0
- package/plugins/pbr/agents/synthesizer.md +48 -0
- package/plugins/pbr/agents/verifier.md +112 -11
- package/plugins/pbr/commands/test.md +5 -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 +96 -17
- package/plugins/pbr/scripts/check-plan-format.js +13 -1
- 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 +265 -0
- package/plugins/pbr/scripts/lib/config.js +271 -0
- package/plugins/pbr/scripts/lib/core.js +587 -0
- package/plugins/pbr/scripts/lib/history.js +73 -0
- package/plugins/pbr/scripts/lib/init.js +166 -0
- package/plugins/pbr/scripts/lib/migrate.js +169 -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/lib/todo.js +300 -0
- package/plugins/pbr/scripts/pbr-tools.js +425 -1310
- package/plugins/pbr/scripts/post-write-dispatch.js +5 -4
- 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/config/SKILL.md +12 -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/health/SKILL.md +14 -3
- package/plugins/pbr/skills/help/SKILL.md +2 -0
- 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/skills/setup/SKILL.md +9 -1
- package/plugins/pbr/skills/shared/context-budget.md +10 -0
- package/plugins/pbr/skills/shared/universal-anti-patterns.md +6 -0
- package/plugins/pbr/skills/test/SKILL.md +212 -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,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/migrate.js — Schema migration for Plan-Build-Run config.json.
|
|
3
|
+
*
|
|
4
|
+
* Tracks config.json schema version and applies sequential migrations
|
|
5
|
+
* to bring outdated configs up to the current version.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { applyMigrations, CURRENT_SCHEMA_VERSION } = require('./migrate');
|
|
9
|
+
* const result = await applyMigrations(planningDir, { dryRun: false });
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { atomicWrite } = require('./core');
|
|
15
|
+
|
|
16
|
+
/** The current schema version supported by this version of PBR. */
|
|
17
|
+
const CURRENT_SCHEMA_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Migration registry. Each entry describes one schema version step.
|
|
21
|
+
* Migrations MUST be listed in ascending `from` order.
|
|
22
|
+
*
|
|
23
|
+
* @type {Array<{ from: number, to: number, description: string, migrate: function }>}
|
|
24
|
+
*/
|
|
25
|
+
const MIGRATIONS = [
|
|
26
|
+
{
|
|
27
|
+
from: 0,
|
|
28
|
+
to: 1,
|
|
29
|
+
description: 'Add schema_version field',
|
|
30
|
+
migrate(config) {
|
|
31
|
+
config.schema_version = 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Detect the current schema version from a config object.
|
|
38
|
+
* Returns 0 if schema_version is absent or non-numeric.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} config - Parsed config.json object
|
|
41
|
+
* @returns {number} Detected schema version
|
|
42
|
+
*/
|
|
43
|
+
function detectSchemaVersion(config) {
|
|
44
|
+
const v = config && config.schema_version;
|
|
45
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Return the ordered list of migrations needed to go from `fromVersion` to `toVersion`.
|
|
51
|
+
* Returns an empty array if versions are equal.
|
|
52
|
+
* Throws if fromVersion > toVersion (downgrade not supported).
|
|
53
|
+
*
|
|
54
|
+
* @param {number} fromVersion - Current schema version
|
|
55
|
+
* @param {number} toVersion - Target schema version
|
|
56
|
+
* @returns {Array} Ordered migrations to apply
|
|
57
|
+
*/
|
|
58
|
+
function getMigrationPath(fromVersion, toVersion) {
|
|
59
|
+
if (fromVersion > toVersion) {
|
|
60
|
+
throw new Error(`Cannot downgrade schema from version ${fromVersion} to ${toVersion}`);
|
|
61
|
+
}
|
|
62
|
+
if (fromVersion === toVersion) return [];
|
|
63
|
+
return MIGRATIONS.filter(m => m.from >= fromVersion && m.to <= toVersion);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Apply pending migrations to config.json in planningDir.
|
|
68
|
+
*
|
|
69
|
+
* Options:
|
|
70
|
+
* dryRun {boolean} — If true, simulate migration without writing files (default: false)
|
|
71
|
+
* force {boolean} — Reserved for future use (default: false)
|
|
72
|
+
*
|
|
73
|
+
* Returns:
|
|
74
|
+
* { migrated: false, version: N } — already current
|
|
75
|
+
* { migrated: false, message: string } — future version, no-op
|
|
76
|
+
* { migrated: true, fromVersion, toVersion, applied, backupPath } — success
|
|
77
|
+
* { error: string } — failure
|
|
78
|
+
*
|
|
79
|
+
* @param {string} planningDir - Path to .planning directory
|
|
80
|
+
* @param {object} [options] - Options { dryRun, force }
|
|
81
|
+
* @returns {Promise<object>} Result object
|
|
82
|
+
*/
|
|
83
|
+
async function applyMigrations(planningDir, options) {
|
|
84
|
+
const opts = options || {};
|
|
85
|
+
const dryRun = opts.dryRun === true;
|
|
86
|
+
|
|
87
|
+
const configPath = path.join(planningDir, 'config.json');
|
|
88
|
+
|
|
89
|
+
// Load config.json
|
|
90
|
+
if (!fs.existsSync(configPath)) {
|
|
91
|
+
return { error: 'config.json not found in ' + planningDir };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let config;
|
|
95
|
+
try {
|
|
96
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return { error: 'config.json is not valid JSON: ' + e.message };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const currentVersion = detectSchemaVersion(config);
|
|
102
|
+
|
|
103
|
+
// Future version — don't touch it
|
|
104
|
+
if (currentVersion > CURRENT_SCHEMA_VERSION) {
|
|
105
|
+
return {
|
|
106
|
+
migrated: false,
|
|
107
|
+
version: currentVersion,
|
|
108
|
+
message: `config.json schema_version (${currentVersion}) is newer than this PBR version supports (${CURRENT_SCHEMA_VERSION}). No migration applied.`
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Already current
|
|
113
|
+
if (currentVersion === CURRENT_SCHEMA_VERSION) {
|
|
114
|
+
return { migrated: false, version: currentVersion };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Determine migrations to apply
|
|
118
|
+
const migrations = getMigrationPath(currentVersion, CURRENT_SCHEMA_VERSION);
|
|
119
|
+
if (migrations.length === 0) {
|
|
120
|
+
return { migrated: false, version: currentVersion };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Clone config for mutation
|
|
124
|
+
const updatedConfig = JSON.parse(JSON.stringify(config));
|
|
125
|
+
|
|
126
|
+
// Apply each migration in sequence
|
|
127
|
+
const applied = [];
|
|
128
|
+
for (const m of migrations) {
|
|
129
|
+
m.migrate(updatedConfig);
|
|
130
|
+
applied.push(m.description);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (dryRun) {
|
|
134
|
+
return {
|
|
135
|
+
migrated: true,
|
|
136
|
+
fromVersion: currentVersion,
|
|
137
|
+
toVersion: CURRENT_SCHEMA_VERSION,
|
|
138
|
+
applied,
|
|
139
|
+
dryRun: true
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create backup
|
|
144
|
+
const backupDir = path.join(planningDir, '.migration-backup');
|
|
145
|
+
const backupPath = path.join(backupDir, 'config.json.bak');
|
|
146
|
+
if (!fs.existsSync(backupDir)) {
|
|
147
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
fs.copyFileSync(configPath, backupPath);
|
|
150
|
+
|
|
151
|
+
// Write updated config atomically
|
|
152
|
+
atomicWrite(configPath, JSON.stringify(updatedConfig, null, 2));
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
migrated: true,
|
|
156
|
+
fromVersion: currentVersion,
|
|
157
|
+
toVersion: CURRENT_SCHEMA_VERSION,
|
|
158
|
+
applied,
|
|
159
|
+
backupPath
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
CURRENT_SCHEMA_VERSION,
|
|
165
|
+
MIGRATIONS,
|
|
166
|
+
detectSchemaVersion,
|
|
167
|
+
getMigrationPath,
|
|
168
|
+
applyMigrations
|
|
169
|
+
};
|
|
@@ -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
|
+
};
|