@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.
Files changed (133) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/dashboard/public/css/command-center.css +152 -65
  3. package/dashboard/public/css/explorer.css +22 -41
  4. package/dashboard/public/css/layout.css +119 -1
  5. package/dashboard/public/css/tokens.css +13 -0
  6. package/dashboard/src/components/Layout.tsx +32 -6
  7. package/dashboard/src/components/explorer/tabs/PhasesTab.tsx +11 -1
  8. package/dashboard/src/components/explorer/tabs/TodosTab.tsx +18 -2
  9. package/dashboard/src/components/partials/AttentionPanel.tsx +7 -1
  10. package/dashboard/src/components/partials/CurrentPhaseCard.tsx +26 -24
  11. package/dashboard/src/components/partials/QuickActions.tsx +21 -11
  12. package/dashboard/src/components/partials/StatCardGrid.tsx +67 -0
  13. package/dashboard/src/components/partials/StatusHeader.tsx +1 -0
  14. package/dashboard/src/routes/command-center.routes.tsx +8 -7
  15. package/dashboard/src/routes/index.routes.tsx +32 -29
  16. package/package.json +2 -2
  17. package/plugins/copilot-pbr/agents/audit.agent.md +128 -16
  18. package/plugins/copilot-pbr/agents/codebase-mapper.agent.md +48 -1
  19. package/plugins/copilot-pbr/agents/debugger.agent.md +47 -1
  20. package/plugins/copilot-pbr/agents/executor.agent.md +152 -8
  21. package/plugins/copilot-pbr/agents/general.agent.md +46 -1
  22. package/plugins/copilot-pbr/agents/integration-checker.agent.md +52 -2
  23. package/plugins/copilot-pbr/agents/plan-checker.agent.md +50 -2
  24. package/plugins/copilot-pbr/agents/planner.agent.md +54 -1
  25. package/plugins/copilot-pbr/agents/researcher.agent.md +47 -2
  26. package/plugins/copilot-pbr/agents/synthesizer.agent.md +49 -1
  27. package/plugins/copilot-pbr/agents/verifier.agent.md +86 -2
  28. package/plugins/copilot-pbr/hooks/hooks.json +11 -0
  29. package/plugins/copilot-pbr/plugin.json +1 -1
  30. package/plugins/copilot-pbr/references/agent-contracts.md +27 -0
  31. package/plugins/copilot-pbr/references/checkpoints.md +32 -1
  32. package/plugins/copilot-pbr/references/context-quality-tiers.md +45 -0
  33. package/plugins/copilot-pbr/references/pbr-tools-cli.md +115 -0
  34. package/plugins/copilot-pbr/references/questioning.md +21 -1
  35. package/plugins/copilot-pbr/references/verification-patterns.md +52 -1
  36. package/plugins/copilot-pbr/skills/audit/SKILL.md +19 -3
  37. package/plugins/copilot-pbr/skills/begin/SKILL.md +57 -4
  38. package/plugins/copilot-pbr/skills/build/SKILL.md +39 -2
  39. package/plugins/copilot-pbr/skills/debug/SKILL.md +12 -1
  40. package/plugins/copilot-pbr/skills/explore/SKILL.md +13 -2
  41. package/plugins/copilot-pbr/skills/import/SKILL.md +26 -1
  42. package/plugins/copilot-pbr/skills/milestone/SKILL.md +15 -3
  43. package/plugins/copilot-pbr/skills/plan/SKILL.md +50 -0
  44. package/plugins/copilot-pbr/skills/quick/SKILL.md +21 -0
  45. package/plugins/copilot-pbr/skills/review/SKILL.md +45 -0
  46. package/plugins/copilot-pbr/skills/scan/SKILL.md +20 -0
  47. package/plugins/copilot-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  48. package/plugins/copilot-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
  49. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  50. package/plugins/cursor-pbr/agents/audit.md +51 -5
  51. package/plugins/cursor-pbr/agents/codebase-mapper.md +48 -1
  52. package/plugins/cursor-pbr/agents/debugger.md +47 -1
  53. package/plugins/cursor-pbr/agents/executor.md +152 -8
  54. package/plugins/cursor-pbr/agents/general.md +46 -1
  55. package/plugins/cursor-pbr/agents/integration-checker.md +51 -1
  56. package/plugins/cursor-pbr/agents/plan-checker.md +49 -1
  57. package/plugins/cursor-pbr/agents/planner.md +54 -1
  58. package/plugins/cursor-pbr/agents/researcher.md +46 -1
  59. package/plugins/cursor-pbr/agents/synthesizer.md +49 -1
  60. package/plugins/cursor-pbr/agents/verifier.md +85 -1
  61. package/plugins/cursor-pbr/hooks/hooks.json +9 -0
  62. package/plugins/cursor-pbr/references/agent-contracts.md +27 -0
  63. package/plugins/cursor-pbr/references/checkpoints.md +32 -1
  64. package/plugins/cursor-pbr/references/context-quality-tiers.md +45 -0
  65. package/plugins/cursor-pbr/references/pbr-tools-cli.md +115 -0
  66. package/plugins/cursor-pbr/references/questioning.md +21 -1
  67. package/plugins/cursor-pbr/references/verification-patterns.md +52 -1
  68. package/plugins/cursor-pbr/skills/audit/SKILL.md +19 -3
  69. package/plugins/cursor-pbr/skills/begin/SKILL.md +57 -4
  70. package/plugins/cursor-pbr/skills/build/SKILL.md +37 -2
  71. package/plugins/cursor-pbr/skills/debug/SKILL.md +12 -1
  72. package/plugins/cursor-pbr/skills/explore/SKILL.md +13 -2
  73. package/plugins/cursor-pbr/skills/import/SKILL.md +26 -1
  74. package/plugins/cursor-pbr/skills/milestone/SKILL.md +15 -3
  75. package/plugins/cursor-pbr/skills/plan/SKILL.md +50 -0
  76. package/plugins/cursor-pbr/skills/quick/SKILL.md +21 -0
  77. package/plugins/cursor-pbr/skills/review/SKILL.md +45 -0
  78. package/plugins/cursor-pbr/skills/scan/SKILL.md +20 -0
  79. package/plugins/cursor-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  80. package/plugins/cursor-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
  81. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  82. package/plugins/pbr/agents/audit.md +44 -0
  83. package/plugins/pbr/agents/codebase-mapper.md +47 -0
  84. package/plugins/pbr/agents/debugger.md +46 -0
  85. package/plugins/pbr/agents/executor.md +150 -6
  86. package/plugins/pbr/agents/general.md +45 -0
  87. package/plugins/pbr/agents/integration-checker.md +50 -0
  88. package/plugins/pbr/agents/plan-checker.md +48 -0
  89. package/plugins/pbr/agents/planner.md +51 -0
  90. package/plugins/pbr/agents/researcher.md +45 -0
  91. package/plugins/pbr/agents/synthesizer.md +48 -0
  92. package/plugins/pbr/agents/verifier.md +84 -0
  93. package/plugins/pbr/hooks/hooks.json +9 -0
  94. package/plugins/pbr/references/agent-contracts.md +27 -0
  95. package/plugins/pbr/references/checkpoints.md +32 -0
  96. package/plugins/pbr/references/context-quality-tiers.md +45 -0
  97. package/plugins/pbr/references/pbr-tools-cli.md +115 -0
  98. package/plugins/pbr/references/questioning.md +21 -0
  99. package/plugins/pbr/references/verification-patterns.md +52 -0
  100. package/plugins/pbr/scripts/check-plan-format.js +13 -1
  101. package/plugins/pbr/scripts/check-state-sync.js +26 -7
  102. package/plugins/pbr/scripts/check-subagent-output.js +30 -2
  103. package/plugins/pbr/scripts/config-schema.json +11 -1
  104. package/plugins/pbr/scripts/context-bridge.js +259 -0
  105. package/plugins/pbr/scripts/lib/config.js +178 -0
  106. package/plugins/pbr/scripts/lib/core.js +578 -0
  107. package/plugins/pbr/scripts/lib/history.js +73 -0
  108. package/plugins/pbr/scripts/lib/init.js +166 -0
  109. package/plugins/pbr/scripts/lib/phase.js +364 -0
  110. package/plugins/pbr/scripts/lib/roadmap.js +175 -0
  111. package/plugins/pbr/scripts/lib/state.js +397 -0
  112. package/plugins/pbr/scripts/pbr-tools.js +346 -1310
  113. package/plugins/pbr/scripts/post-write-dispatch.js +5 -4
  114. package/plugins/pbr/scripts/pre-write-dispatch.js +1 -1
  115. package/plugins/pbr/scripts/progress-tracker.js +1 -1
  116. package/plugins/pbr/scripts/suggest-compact.js +1 -1
  117. package/plugins/pbr/scripts/track-context-budget.js +53 -2
  118. package/plugins/pbr/scripts/validate-task.js +20 -28
  119. package/plugins/pbr/skills/audit/SKILL.md +19 -3
  120. package/plugins/pbr/skills/begin/SKILL.md +48 -2
  121. package/plugins/pbr/skills/build/SKILL.md +39 -2
  122. package/plugins/pbr/skills/debug/SKILL.md +12 -1
  123. package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +12 -1
  124. package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +12 -5
  125. package/plugins/pbr/skills/explore/SKILL.md +13 -2
  126. package/plugins/pbr/skills/import/SKILL.md +26 -1
  127. package/plugins/pbr/skills/milestone/SKILL.md +15 -3
  128. package/plugins/pbr/skills/plan/SKILL.md +52 -2
  129. package/plugins/pbr/skills/quick/SKILL.md +21 -0
  130. package/plugins/pbr/skills/review/SKILL.md +46 -0
  131. package/plugins/pbr/skills/scan/SKILL.md +20 -0
  132. package/plugins/pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  133. 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
+ };