@sienklogic/plan-build-run 2.34.0 → 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 +663 -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 +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 +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 +259 -0
- 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 -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/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,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/state.js — STATE.md operations for Plan-Build-Run tools.
|
|
3
|
+
*
|
|
4
|
+
* Handles loading, parsing, updating, patching, and advancing STATE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const {
|
|
10
|
+
parseYamlFrontmatter,
|
|
11
|
+
findFiles,
|
|
12
|
+
lockedFileUpdate,
|
|
13
|
+
calculateProgress,
|
|
14
|
+
determinePhaseStatus
|
|
15
|
+
} = require('./core');
|
|
16
|
+
|
|
17
|
+
// --- Parsers ---
|
|
18
|
+
|
|
19
|
+
function parseStateMd(content) {
|
|
20
|
+
const result = {
|
|
21
|
+
current_phase: null,
|
|
22
|
+
phase_name: null,
|
|
23
|
+
progress: null,
|
|
24
|
+
status: null,
|
|
25
|
+
line_count: content.split('\n').length,
|
|
26
|
+
format: 'legacy' // 'legacy' or 'frontmatter'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Check for YAML frontmatter (version 2 format)
|
|
30
|
+
const frontmatter = parseYamlFrontmatter(content);
|
|
31
|
+
if (frontmatter.version === 2 || frontmatter.current_phase !== undefined) {
|
|
32
|
+
result.format = 'frontmatter';
|
|
33
|
+
result.current_phase = frontmatter.current_phase || null;
|
|
34
|
+
result.total_phases = frontmatter.total_phases || null;
|
|
35
|
+
result.phase_name = frontmatter.phase_slug || frontmatter.phase_name || null;
|
|
36
|
+
result.status = frontmatter.status || null;
|
|
37
|
+
result.progress = frontmatter.progress_percent !== undefined ? frontmatter.progress_percent : null;
|
|
38
|
+
result.plans_total = frontmatter.plans_total || null;
|
|
39
|
+
result.plans_complete = frontmatter.plans_complete || null;
|
|
40
|
+
result.last_activity = frontmatter.last_activity || null;
|
|
41
|
+
result.last_command = frontmatter.last_command || null;
|
|
42
|
+
result.blockers = frontmatter.blockers || [];
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Legacy regex-based parsing (version 1 format, no frontmatter)
|
|
47
|
+
// DEPRECATED (2026-02): v1 STATE.md format (no YAML frontmatter) is deprecated.
|
|
48
|
+
// New projects should use v2 (frontmatter) format, generated by /pbr:setup.
|
|
49
|
+
// v1 support will be removed in a future major version.
|
|
50
|
+
process.stderr.write('[pbr] WARNING: STATE.md uses legacy v1 format. Run /pbr:setup to migrate to v2 format.\n');
|
|
51
|
+
// Extract "Phase: N of M"
|
|
52
|
+
const phaseMatch = content.match(/Phase:\s*(\d+)\s+of\s+(\d+)/);
|
|
53
|
+
if (phaseMatch) {
|
|
54
|
+
result.current_phase = parseInt(phaseMatch[1], 10);
|
|
55
|
+
result.total_phases = parseInt(phaseMatch[2], 10);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Extract phase name (line after "Phase:")
|
|
59
|
+
const nameMatch = content.match(/--\s+(.+?)(?:\n|$)/);
|
|
60
|
+
if (nameMatch) {
|
|
61
|
+
result.phase_name = nameMatch[1].trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Extract progress percentage
|
|
65
|
+
const progressMatch = content.match(/(\d+)%/);
|
|
66
|
+
if (progressMatch) {
|
|
67
|
+
result.progress = parseInt(progressMatch[1], 10);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Extract plan status
|
|
71
|
+
const statusMatch = content.match(/Status:\s*(.+?)(?:\n|$)/i);
|
|
72
|
+
if (statusMatch) {
|
|
73
|
+
result.status = statusMatch[1].trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- Mutation helpers ---
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Update a field in legacy (non-frontmatter) STATE.md content.
|
|
83
|
+
* Pure function: content in, content out.
|
|
84
|
+
*/
|
|
85
|
+
function updateLegacyStateField(content, field, value) {
|
|
86
|
+
const lines = content.split('\n');
|
|
87
|
+
|
|
88
|
+
switch (field) {
|
|
89
|
+
case 'current_phase': {
|
|
90
|
+
const idx = lines.findIndex(l => /Phase:\s*\d+\s+of\s+\d+/.test(l));
|
|
91
|
+
if (idx !== -1) {
|
|
92
|
+
lines[idx] = lines[idx].replace(/(Phase:\s*)\d+/, (_, prefix) => `${prefix}${value}`);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case 'status': {
|
|
97
|
+
const idx = lines.findIndex(l => /^Status:/i.test(l));
|
|
98
|
+
if (idx !== -1) {
|
|
99
|
+
lines[idx] = `Status: ${value}`;
|
|
100
|
+
} else {
|
|
101
|
+
const phaseIdx = lines.findIndex(l => /Phase:/.test(l));
|
|
102
|
+
if (phaseIdx !== -1) {
|
|
103
|
+
lines.splice(phaseIdx + 1, 0, `Status: ${value}`);
|
|
104
|
+
} else {
|
|
105
|
+
lines.push(`Status: ${value}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case 'plans_complete': {
|
|
111
|
+
const idx = lines.findIndex(l => /Plan:\s*\d+\s+of\s+\d+/.test(l));
|
|
112
|
+
if (idx !== -1) {
|
|
113
|
+
lines[idx] = lines[idx].replace(/(Plan:\s*)\d+/, (_, prefix) => `${prefix}${value}`);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'last_activity': {
|
|
118
|
+
const idx = lines.findIndex(l => /^Last Activity:/i.test(l));
|
|
119
|
+
if (idx !== -1) {
|
|
120
|
+
lines[idx] = `Last Activity: ${value}`;
|
|
121
|
+
} else {
|
|
122
|
+
const statusIdx = lines.findIndex(l => /^Status:/i.test(l));
|
|
123
|
+
if (statusIdx !== -1) {
|
|
124
|
+
lines.splice(statusIdx + 1, 0, `Last Activity: ${value}`);
|
|
125
|
+
} else {
|
|
126
|
+
lines.push(`Last Activity: ${value}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Update a field in YAML frontmatter content.
|
|
138
|
+
* Pure function: content in, content out.
|
|
139
|
+
*/
|
|
140
|
+
function updateFrontmatterField(content, field, value) {
|
|
141
|
+
const match = content.match(/^(---\s*\n)([\s\S]*?)(\n---)/);
|
|
142
|
+
if (!match) return content;
|
|
143
|
+
|
|
144
|
+
const before = match[1];
|
|
145
|
+
let yaml = match[2];
|
|
146
|
+
const after = match[3];
|
|
147
|
+
const rest = content.slice(match[0].length);
|
|
148
|
+
|
|
149
|
+
// Format value: integers stay bare, strings get quotes
|
|
150
|
+
const isNum = /^\d+$/.test(String(value));
|
|
151
|
+
const formatted = isNum ? value : `"${value}"`;
|
|
152
|
+
|
|
153
|
+
const fieldRegex = new RegExp(`^(${field})\\s*:.*$`, 'm');
|
|
154
|
+
if (fieldRegex.test(yaml)) {
|
|
155
|
+
yaml = yaml.replace(fieldRegex, () => `${field}: ${formatted}`);
|
|
156
|
+
} else {
|
|
157
|
+
yaml = yaml + `\n${field}: ${formatted}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return before + yaml + after + rest;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Commands ---
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Load full project state from .planning/ directory.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} [planningDir] - Path to .planning directory
|
|
169
|
+
* @returns {object} Full state object
|
|
170
|
+
*/
|
|
171
|
+
function stateLoad(planningDir) {
|
|
172
|
+
const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
|
|
173
|
+
const { parseRoadmapMd } = require('./roadmap');
|
|
174
|
+
|
|
175
|
+
const result = {
|
|
176
|
+
exists: false,
|
|
177
|
+
config: null,
|
|
178
|
+
state: null,
|
|
179
|
+
roadmap: null,
|
|
180
|
+
phase_count: 0,
|
|
181
|
+
current_phase: null,
|
|
182
|
+
progress: null
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (!fs.existsSync(dir)) {
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
result.exists = true;
|
|
189
|
+
|
|
190
|
+
// Load config.json
|
|
191
|
+
const configPath = path.join(dir, 'config.json');
|
|
192
|
+
if (fs.existsSync(configPath)) {
|
|
193
|
+
try {
|
|
194
|
+
result.config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
195
|
+
} catch (_) {
|
|
196
|
+
result.config = { _error: 'Failed to parse config.json' };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Load STATE.md
|
|
201
|
+
const statePath = path.join(dir, 'STATE.md');
|
|
202
|
+
if (fs.existsSync(statePath)) {
|
|
203
|
+
const content = fs.readFileSync(statePath, 'utf8');
|
|
204
|
+
result.state = parseStateMd(content);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Load ROADMAP.md
|
|
208
|
+
const roadmapPath = path.join(dir, 'ROADMAP.md');
|
|
209
|
+
if (fs.existsSync(roadmapPath)) {
|
|
210
|
+
const content = fs.readFileSync(roadmapPath, 'utf8');
|
|
211
|
+
result.roadmap = parseRoadmapMd(content);
|
|
212
|
+
result.phase_count = result.roadmap.phases.length;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Extract current phase
|
|
216
|
+
if (result.state && result.state.current_phase) {
|
|
217
|
+
result.current_phase = result.state.current_phase;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Calculate progress
|
|
221
|
+
result.progress = calculateProgress(dir);
|
|
222
|
+
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Recalculate progress from filesystem.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} [planningDir] - Path to .planning directory
|
|
230
|
+
* @returns {object} Progress object
|
|
231
|
+
*/
|
|
232
|
+
function stateCheckProgress(planningDir) {
|
|
233
|
+
const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
|
|
234
|
+
const phasesDir = path.join(dir, 'phases');
|
|
235
|
+
if (!fs.existsSync(phasesDir)) {
|
|
236
|
+
return { phases: [], total_plans: 0, completed_plans: 0, percentage: 0 };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const phases = [];
|
|
240
|
+
let totalPlans = 0;
|
|
241
|
+
let completedPlans = 0;
|
|
242
|
+
|
|
243
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
244
|
+
.filter(e => e.isDirectory())
|
|
245
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
246
|
+
|
|
247
|
+
for (const entry of entries) {
|
|
248
|
+
const phaseDir = path.join(phasesDir, entry.name);
|
|
249
|
+
const plans = findFiles(phaseDir, /-PLAN\.md$/);
|
|
250
|
+
const summaries = findFiles(phaseDir, /^SUMMARY-.*\.md$/);
|
|
251
|
+
const verification = fs.existsSync(path.join(phaseDir, 'VERIFICATION.md'));
|
|
252
|
+
|
|
253
|
+
const completedSummaries = summaries.filter(s => {
|
|
254
|
+
const content = fs.readFileSync(path.join(phaseDir, s), 'utf8');
|
|
255
|
+
return /status:\s*["']?complete/i.test(content);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const phaseInfoObj = {
|
|
259
|
+
directory: entry.name,
|
|
260
|
+
plans: plans.length,
|
|
261
|
+
summaries: summaries.length,
|
|
262
|
+
completed: completedSummaries.length,
|
|
263
|
+
has_verification: verification,
|
|
264
|
+
status: determinePhaseStatus(plans.length, completedSummaries.length, summaries.length, verification, phaseDir)
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
phases.push(phaseInfoObj);
|
|
268
|
+
totalPlans += plans.length;
|
|
269
|
+
completedPlans += completedSummaries.length;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
phases,
|
|
274
|
+
total_plans: totalPlans,
|
|
275
|
+
completed_plans: completedPlans,
|
|
276
|
+
percentage: totalPlans > 0 ? Math.round((completedPlans / totalPlans) * 100) : 0
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Atomically update a field in STATE.md using lockedFileUpdate.
|
|
282
|
+
* Supports both legacy and frontmatter (v2) formats.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} field - One of: current_phase, status, plans_complete, last_activity
|
|
285
|
+
* @param {string} value - New value (use 'now' for last_activity to auto-timestamp)
|
|
286
|
+
* @param {string} [planningDir] - Path to .planning directory
|
|
287
|
+
*/
|
|
288
|
+
function stateUpdate(field, value, planningDir) {
|
|
289
|
+
const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
|
|
290
|
+
const statePath = path.join(dir, 'STATE.md');
|
|
291
|
+
if (!fs.existsSync(statePath)) {
|
|
292
|
+
return { success: false, error: 'STATE.md not found' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const validFields = ['current_phase', 'status', 'plans_complete', 'last_activity'];
|
|
296
|
+
if (!validFields.includes(field)) {
|
|
297
|
+
return { success: false, error: `Invalid field: ${field}. Valid fields: ${validFields.join(', ')}` };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Auto-timestamp
|
|
301
|
+
if (field === 'last_activity' && value === 'now') {
|
|
302
|
+
value = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const result = lockedFileUpdate(statePath, (content) => {
|
|
306
|
+
const fm = parseYamlFrontmatter(content);
|
|
307
|
+
if (fm.version === 2 || fm.current_phase !== undefined) {
|
|
308
|
+
return updateFrontmatterField(content, field, value);
|
|
309
|
+
}
|
|
310
|
+
return updateLegacyStateField(content, field, value);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (result.success) {
|
|
314
|
+
return { success: true, field, value };
|
|
315
|
+
}
|
|
316
|
+
return { success: false, error: result.error };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Batch-update multiple STATE.md fields at once.
|
|
321
|
+
*
|
|
322
|
+
* @param {string} jsonStr - JSON string of field:value pairs
|
|
323
|
+
* @param {string} [planningDir] - Path to .planning directory
|
|
324
|
+
*/
|
|
325
|
+
function statePatch(jsonStr, planningDir) {
|
|
326
|
+
const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
|
|
327
|
+
const statePath = path.join(dir, 'STATE.md');
|
|
328
|
+
if (!fs.existsSync(statePath)) return { success: false, error: "STATE.md not found" };
|
|
329
|
+
let fields;
|
|
330
|
+
try { fields = JSON.parse(jsonStr); } catch (_e) { return { success: false, error: "Invalid JSON" }; }
|
|
331
|
+
const validFields = ["current_phase", "status", "plans_complete", "last_activity", "progress_percent", "phase_slug", "total_phases", "last_command", "blockers"];
|
|
332
|
+
const updates = [], errors = [];
|
|
333
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
334
|
+
if (!validFields.includes(field)) { errors.push("Unknown field: " + field); continue; }
|
|
335
|
+
try { stateUpdate(field, String(value), dir); updates.push(field); } catch (e) { errors.push(field + ": " + e.message); }
|
|
336
|
+
}
|
|
337
|
+
return { success: errors.length === 0, updated: updates, errors: errors.length > 0 ? errors : undefined };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Advance the plan counter in STATE.md by 1.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} [planningDir] - Path to .planning directory
|
|
344
|
+
*/
|
|
345
|
+
function stateAdvancePlan(planningDir) {
|
|
346
|
+
const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
|
|
347
|
+
const statePath = path.join(dir, 'STATE.md');
|
|
348
|
+
if (!fs.existsSync(statePath)) return { success: false, error: "STATE.md not found" };
|
|
349
|
+
const stateContent = fs.readFileSync(statePath, "utf8");
|
|
350
|
+
const planMatch = stateContent.match(/Plan:\s*(\d+)\s+of\s+(\d+)/);
|
|
351
|
+
if (!planMatch) return { success: false, error: "Could not find Plan: N of M in STATE.md" };
|
|
352
|
+
const current = parseInt(planMatch[1], 10), total = parseInt(planMatch[2], 10);
|
|
353
|
+
const next = Math.min(current + 1, total);
|
|
354
|
+
stateUpdate("plans_complete", String(next), dir);
|
|
355
|
+
const progressPct = total > 0 ? Math.round((next / total) * 100) : 0;
|
|
356
|
+
stateUpdate("progress_percent", String(progressPct), dir);
|
|
357
|
+
return { success: true, previous_plan: current, current_plan: next, total_plans: total, progress_percent: progressPct };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Record a session metric in STATE.md + HISTORY.md.
|
|
362
|
+
*
|
|
363
|
+
* @param {string[]} metricArgs - CLI args like ['--duration', '30m', '--plans-completed', '3']
|
|
364
|
+
* @param {string} [planningDir] - Path to .planning directory
|
|
365
|
+
*/
|
|
366
|
+
function stateRecordMetric(metricArgs, planningDir) {
|
|
367
|
+
const dir = planningDir || path.join(process.env.PBR_PROJECT_ROOT || process.cwd(), '.planning');
|
|
368
|
+
const { historyAppend } = require('./history');
|
|
369
|
+
let duration = null, plansCompleted = null;
|
|
370
|
+
for (let i = 0; i < metricArgs.length; i++) {
|
|
371
|
+
if (metricArgs[i] === "--duration" && metricArgs[i + 1]) {
|
|
372
|
+
const match = metricArgs[i + 1].match(/(\d+)(m|s|h)/);
|
|
373
|
+
if (match) { const val = parseInt(match[1], 10); const unit = match[2]; duration = unit === "h" ? val * 60 : unit === "s" ? Math.round(val / 60) : val; }
|
|
374
|
+
i++;
|
|
375
|
+
} else if (metricArgs[i] === "--plans-completed" && metricArgs[i + 1]) {
|
|
376
|
+
plansCompleted = parseInt(metricArgs[i + 1], 10); i++;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const parts = [];
|
|
380
|
+
if (duration !== null) parts.push("duration: " + duration + "m");
|
|
381
|
+
if (plansCompleted !== null) parts.push("plans_completed: " + plansCompleted);
|
|
382
|
+
if (parts.length > 0) historyAppend({ type: "metric", title: "Session metric", body: parts.join(", ") }, dir);
|
|
383
|
+
stateUpdate("last_activity", "now", dir);
|
|
384
|
+
return { success: true, duration_minutes: duration, plans_completed: plansCompleted };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
module.exports = {
|
|
388
|
+
parseStateMd,
|
|
389
|
+
updateLegacyStateField,
|
|
390
|
+
updateFrontmatterField,
|
|
391
|
+
stateLoad,
|
|
392
|
+
stateCheckProgress,
|
|
393
|
+
stateUpdate,
|
|
394
|
+
statePatch,
|
|
395
|
+
stateAdvancePlan,
|
|
396
|
+
stateRecordMetric
|
|
397
|
+
};
|