@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.
Files changed (160) hide show
  1. package/CHANGELOG.md +683 -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 +129 -16
  18. package/plugins/copilot-pbr/agents/codebase-mapper.agent.md +49 -1
  19. package/plugins/copilot-pbr/agents/debugger.agent.md +50 -1
  20. package/plugins/copilot-pbr/agents/dev-sync.agent.md +23 -0
  21. package/plugins/copilot-pbr/agents/executor.agent.md +153 -8
  22. package/plugins/copilot-pbr/agents/general.agent.md +46 -1
  23. package/plugins/copilot-pbr/agents/integration-checker.agent.md +55 -2
  24. package/plugins/copilot-pbr/agents/plan-checker.agent.md +50 -2
  25. package/plugins/copilot-pbr/agents/planner.agent.md +80 -1
  26. package/plugins/copilot-pbr/agents/researcher.agent.md +50 -2
  27. package/plugins/copilot-pbr/agents/synthesizer.agent.md +49 -1
  28. package/plugins/copilot-pbr/agents/verifier.agent.md +114 -13
  29. package/plugins/copilot-pbr/commands/test.md +5 -0
  30. package/plugins/copilot-pbr/hooks/hooks.json +11 -0
  31. package/plugins/copilot-pbr/plugin.json +1 -1
  32. package/plugins/copilot-pbr/references/agent-contracts.md +27 -0
  33. package/plugins/copilot-pbr/references/checkpoints.md +32 -1
  34. package/plugins/copilot-pbr/references/context-quality-tiers.md +45 -0
  35. package/plugins/copilot-pbr/references/pbr-tools-cli.md +115 -0
  36. package/plugins/copilot-pbr/references/questioning.md +21 -1
  37. package/plugins/copilot-pbr/references/verification-patterns.md +96 -18
  38. package/plugins/copilot-pbr/skills/audit/SKILL.md +19 -3
  39. package/plugins/copilot-pbr/skills/begin/SKILL.md +57 -4
  40. package/plugins/copilot-pbr/skills/build/SKILL.md +39 -2
  41. package/plugins/copilot-pbr/skills/config/SKILL.md +12 -2
  42. package/plugins/copilot-pbr/skills/debug/SKILL.md +12 -1
  43. package/plugins/copilot-pbr/skills/explore/SKILL.md +13 -2
  44. package/plugins/copilot-pbr/skills/health/SKILL.md +13 -5
  45. package/plugins/copilot-pbr/skills/import/SKILL.md +26 -1
  46. package/plugins/copilot-pbr/skills/milestone/SKILL.md +15 -3
  47. package/plugins/copilot-pbr/skills/plan/SKILL.md +50 -0
  48. package/plugins/copilot-pbr/skills/quick/SKILL.md +21 -0
  49. package/plugins/copilot-pbr/skills/review/SKILL.md +45 -0
  50. package/plugins/copilot-pbr/skills/scan/SKILL.md +20 -0
  51. package/plugins/copilot-pbr/skills/setup/SKILL.md +9 -1
  52. package/plugins/copilot-pbr/skills/shared/context-budget.md +10 -0
  53. package/plugins/copilot-pbr/skills/shared/universal-anti-patterns.md +6 -0
  54. package/plugins/copilot-pbr/skills/test/SKILL.md +210 -0
  55. package/plugins/copilot-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  56. package/plugins/copilot-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
  57. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  58. package/plugins/cursor-pbr/agents/audit.md +52 -5
  59. package/plugins/cursor-pbr/agents/codebase-mapper.md +49 -1
  60. package/plugins/cursor-pbr/agents/debugger.md +50 -1
  61. package/plugins/cursor-pbr/agents/dev-sync.md +23 -0
  62. package/plugins/cursor-pbr/agents/executor.md +153 -8
  63. package/plugins/cursor-pbr/agents/general.md +46 -1
  64. package/plugins/cursor-pbr/agents/integration-checker.md +54 -1
  65. package/plugins/cursor-pbr/agents/plan-checker.md +49 -1
  66. package/plugins/cursor-pbr/agents/planner.md +80 -1
  67. package/plugins/cursor-pbr/agents/researcher.md +49 -1
  68. package/plugins/cursor-pbr/agents/synthesizer.md +49 -1
  69. package/plugins/cursor-pbr/agents/verifier.md +113 -12
  70. package/plugins/cursor-pbr/commands/test.md +5 -0
  71. package/plugins/cursor-pbr/hooks/hooks.json +9 -0
  72. package/plugins/cursor-pbr/references/agent-contracts.md +27 -0
  73. package/plugins/cursor-pbr/references/checkpoints.md +32 -1
  74. package/plugins/cursor-pbr/references/context-quality-tiers.md +45 -0
  75. package/plugins/cursor-pbr/references/pbr-tools-cli.md +115 -0
  76. package/plugins/cursor-pbr/references/questioning.md +21 -1
  77. package/plugins/cursor-pbr/references/verification-patterns.md +96 -18
  78. package/plugins/cursor-pbr/skills/audit/SKILL.md +19 -3
  79. package/plugins/cursor-pbr/skills/begin/SKILL.md +57 -4
  80. package/plugins/cursor-pbr/skills/build/SKILL.md +37 -2
  81. package/plugins/cursor-pbr/skills/config/SKILL.md +12 -2
  82. package/plugins/cursor-pbr/skills/debug/SKILL.md +12 -1
  83. package/plugins/cursor-pbr/skills/explore/SKILL.md +13 -2
  84. package/plugins/cursor-pbr/skills/health/SKILL.md +14 -5
  85. package/plugins/cursor-pbr/skills/import/SKILL.md +26 -1
  86. package/plugins/cursor-pbr/skills/milestone/SKILL.md +15 -3
  87. package/plugins/cursor-pbr/skills/plan/SKILL.md +50 -0
  88. package/plugins/cursor-pbr/skills/quick/SKILL.md +21 -0
  89. package/plugins/cursor-pbr/skills/review/SKILL.md +45 -0
  90. package/plugins/cursor-pbr/skills/scan/SKILL.md +20 -0
  91. package/plugins/cursor-pbr/skills/setup/SKILL.md +9 -1
  92. package/plugins/cursor-pbr/skills/shared/context-budget.md +10 -0
  93. package/plugins/cursor-pbr/skills/shared/universal-anti-patterns.md +6 -0
  94. package/plugins/cursor-pbr/skills/test/SKILL.md +211 -0
  95. package/plugins/cursor-pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  96. package/plugins/cursor-pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
  97. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  98. package/plugins/pbr/agents/audit.md +45 -0
  99. package/plugins/pbr/agents/codebase-mapper.md +48 -0
  100. package/plugins/pbr/agents/debugger.md +49 -0
  101. package/plugins/pbr/agents/dev-sync.md +23 -0
  102. package/plugins/pbr/agents/executor.md +151 -6
  103. package/plugins/pbr/agents/general.md +45 -0
  104. package/plugins/pbr/agents/integration-checker.md +53 -0
  105. package/plugins/pbr/agents/plan-checker.md +48 -0
  106. package/plugins/pbr/agents/planner.md +78 -1
  107. package/plugins/pbr/agents/researcher.md +48 -0
  108. package/plugins/pbr/agents/synthesizer.md +48 -0
  109. package/plugins/pbr/agents/verifier.md +112 -11
  110. package/plugins/pbr/commands/test.md +5 -0
  111. package/plugins/pbr/hooks/hooks.json +9 -0
  112. package/plugins/pbr/references/agent-contracts.md +27 -0
  113. package/plugins/pbr/references/checkpoints.md +32 -0
  114. package/plugins/pbr/references/context-quality-tiers.md +45 -0
  115. package/plugins/pbr/references/pbr-tools-cli.md +115 -0
  116. package/plugins/pbr/references/questioning.md +21 -0
  117. package/plugins/pbr/references/verification-patterns.md +96 -17
  118. package/plugins/pbr/scripts/check-plan-format.js +13 -1
  119. package/plugins/pbr/scripts/check-state-sync.js +26 -7
  120. package/plugins/pbr/scripts/check-subagent-output.js +30 -2
  121. package/plugins/pbr/scripts/config-schema.json +11 -1
  122. package/plugins/pbr/scripts/context-bridge.js +265 -0
  123. package/plugins/pbr/scripts/lib/config.js +271 -0
  124. package/plugins/pbr/scripts/lib/core.js +587 -0
  125. package/plugins/pbr/scripts/lib/history.js +73 -0
  126. package/plugins/pbr/scripts/lib/init.js +166 -0
  127. package/plugins/pbr/scripts/lib/migrate.js +169 -0
  128. package/plugins/pbr/scripts/lib/phase.js +364 -0
  129. package/plugins/pbr/scripts/lib/roadmap.js +175 -0
  130. package/plugins/pbr/scripts/lib/state.js +397 -0
  131. package/plugins/pbr/scripts/lib/todo.js +300 -0
  132. package/plugins/pbr/scripts/pbr-tools.js +425 -1310
  133. package/plugins/pbr/scripts/post-write-dispatch.js +5 -4
  134. package/plugins/pbr/scripts/pre-write-dispatch.js +1 -1
  135. package/plugins/pbr/scripts/progress-tracker.js +1 -1
  136. package/plugins/pbr/scripts/suggest-compact.js +1 -1
  137. package/plugins/pbr/scripts/track-context-budget.js +53 -2
  138. package/plugins/pbr/scripts/validate-task.js +20 -28
  139. package/plugins/pbr/skills/audit/SKILL.md +19 -3
  140. package/plugins/pbr/skills/begin/SKILL.md +48 -2
  141. package/plugins/pbr/skills/build/SKILL.md +39 -2
  142. package/plugins/pbr/skills/config/SKILL.md +12 -2
  143. package/plugins/pbr/skills/debug/SKILL.md +12 -1
  144. package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +12 -1
  145. package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +12 -5
  146. package/plugins/pbr/skills/explore/SKILL.md +13 -2
  147. package/plugins/pbr/skills/health/SKILL.md +14 -3
  148. package/plugins/pbr/skills/help/SKILL.md +2 -0
  149. package/plugins/pbr/skills/import/SKILL.md +26 -1
  150. package/plugins/pbr/skills/milestone/SKILL.md +15 -3
  151. package/plugins/pbr/skills/plan/SKILL.md +52 -2
  152. package/plugins/pbr/skills/quick/SKILL.md +21 -0
  153. package/plugins/pbr/skills/review/SKILL.md +46 -0
  154. package/plugins/pbr/skills/scan/SKILL.md +20 -0
  155. package/plugins/pbr/skills/setup/SKILL.md +9 -1
  156. package/plugins/pbr/skills/shared/context-budget.md +10 -0
  157. package/plugins/pbr/skills/shared/universal-anti-patterns.md +6 -0
  158. package/plugins/pbr/skills/test/SKILL.md +212 -0
  159. package/plugins/pbr/templates/SUMMARY-complex.md.tmpl +95 -0
  160. package/plugins/pbr/templates/SUMMARY-minimal.md.tmpl +48 -0
@@ -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
+ };
@@ -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
+ };