@sienklogic/plan-build-run 2.0.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 (221) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/CLAUDE.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +247 -0
  5. package/dashboard/bin/cli.js +25 -0
  6. package/dashboard/package.json +34 -0
  7. package/dashboard/public/.gitkeep +0 -0
  8. package/dashboard/public/css/layout.css +406 -0
  9. package/dashboard/public/css/status-colors.css +98 -0
  10. package/dashboard/public/js/htmx-title.js +5 -0
  11. package/dashboard/public/js/sidebar-toggle.js +20 -0
  12. package/dashboard/src/app.js +78 -0
  13. package/dashboard/src/middleware/errorHandler.js +52 -0
  14. package/dashboard/src/middleware/notFoundHandler.js +9 -0
  15. package/dashboard/src/repositories/planning.repository.js +128 -0
  16. package/dashboard/src/routes/events.routes.js +40 -0
  17. package/dashboard/src/routes/index.routes.js +31 -0
  18. package/dashboard/src/routes/pages.routes.js +195 -0
  19. package/dashboard/src/server.js +42 -0
  20. package/dashboard/src/services/dashboard.service.js +222 -0
  21. package/dashboard/src/services/phase.service.js +167 -0
  22. package/dashboard/src/services/project.service.js +57 -0
  23. package/dashboard/src/services/roadmap.service.js +171 -0
  24. package/dashboard/src/services/sse.service.js +58 -0
  25. package/dashboard/src/services/todo.service.js +254 -0
  26. package/dashboard/src/services/watcher.service.js +48 -0
  27. package/dashboard/src/views/coming-soon.ejs +11 -0
  28. package/dashboard/src/views/error.ejs +13 -0
  29. package/dashboard/src/views/index.ejs +5 -0
  30. package/dashboard/src/views/layout.ejs +1 -0
  31. package/dashboard/src/views/partials/dashboard-content.ejs +77 -0
  32. package/dashboard/src/views/partials/footer.ejs +3 -0
  33. package/dashboard/src/views/partials/head.ejs +21 -0
  34. package/dashboard/src/views/partials/header.ejs +12 -0
  35. package/dashboard/src/views/partials/layout-bottom.ejs +15 -0
  36. package/dashboard/src/views/partials/layout-top.ejs +8 -0
  37. package/dashboard/src/views/partials/phase-content.ejs +181 -0
  38. package/dashboard/src/views/partials/phases-content.ejs +117 -0
  39. package/dashboard/src/views/partials/roadmap-content.ejs +142 -0
  40. package/dashboard/src/views/partials/sidebar.ejs +38 -0
  41. package/dashboard/src/views/partials/todo-create-content.ejs +53 -0
  42. package/dashboard/src/views/partials/todo-detail-content.ejs +38 -0
  43. package/dashboard/src/views/partials/todos-content.ejs +53 -0
  44. package/dashboard/src/views/phase-detail.ejs +5 -0
  45. package/dashboard/src/views/phases.ejs +5 -0
  46. package/dashboard/src/views/roadmap.ejs +5 -0
  47. package/dashboard/src/views/todo-create.ejs +5 -0
  48. package/dashboard/src/views/todo-detail.ejs +5 -0
  49. package/dashboard/src/views/todos.ejs +5 -0
  50. package/package.json +57 -0
  51. package/plugins/pbr/.claude-plugin/plugin.json +13 -0
  52. package/plugins/pbr/UI-CONSISTENCY-GAPS.md +61 -0
  53. package/plugins/pbr/agents/codebase-mapper.md +271 -0
  54. package/plugins/pbr/agents/debugger.md +281 -0
  55. package/plugins/pbr/agents/executor.md +407 -0
  56. package/plugins/pbr/agents/general.md +164 -0
  57. package/plugins/pbr/agents/integration-checker.md +141 -0
  58. package/plugins/pbr/agents/plan-checker.md +280 -0
  59. package/plugins/pbr/agents/planner.md +358 -0
  60. package/plugins/pbr/agents/researcher.md +363 -0
  61. package/plugins/pbr/agents/synthesizer.md +230 -0
  62. package/plugins/pbr/agents/verifier.md +454 -0
  63. package/plugins/pbr/commands/begin.md +5 -0
  64. package/plugins/pbr/commands/build.md +5 -0
  65. package/plugins/pbr/commands/config.md +5 -0
  66. package/plugins/pbr/commands/continue.md +5 -0
  67. package/plugins/pbr/commands/debug.md +5 -0
  68. package/plugins/pbr/commands/discuss.md +5 -0
  69. package/plugins/pbr/commands/explore.md +5 -0
  70. package/plugins/pbr/commands/health.md +5 -0
  71. package/plugins/pbr/commands/help.md +5 -0
  72. package/plugins/pbr/commands/import.md +5 -0
  73. package/plugins/pbr/commands/milestone.md +5 -0
  74. package/plugins/pbr/commands/note.md +5 -0
  75. package/plugins/pbr/commands/pause.md +5 -0
  76. package/plugins/pbr/commands/plan.md +5 -0
  77. package/plugins/pbr/commands/quick.md +5 -0
  78. package/plugins/pbr/commands/resume.md +5 -0
  79. package/plugins/pbr/commands/review.md +5 -0
  80. package/plugins/pbr/commands/scan.md +5 -0
  81. package/plugins/pbr/commands/setup.md +5 -0
  82. package/plugins/pbr/commands/status.md +5 -0
  83. package/plugins/pbr/commands/todo.md +5 -0
  84. package/plugins/pbr/contexts/dev.md +27 -0
  85. package/plugins/pbr/contexts/research.md +28 -0
  86. package/plugins/pbr/contexts/review.md +36 -0
  87. package/plugins/pbr/hooks/hooks.json +183 -0
  88. package/plugins/pbr/references/agent-anti-patterns.md +24 -0
  89. package/plugins/pbr/references/agent-interactions.md +134 -0
  90. package/plugins/pbr/references/agent-teams.md +54 -0
  91. package/plugins/pbr/references/checkpoints.md +157 -0
  92. package/plugins/pbr/references/common-bug-patterns.md +13 -0
  93. package/plugins/pbr/references/continuation-format.md +212 -0
  94. package/plugins/pbr/references/deviation-rules.md +112 -0
  95. package/plugins/pbr/references/git-integration.md +226 -0
  96. package/plugins/pbr/references/integration-patterns.md +117 -0
  97. package/plugins/pbr/references/model-profiles.md +99 -0
  98. package/plugins/pbr/references/model-selection.md +31 -0
  99. package/plugins/pbr/references/pbr-rules.md +193 -0
  100. package/plugins/pbr/references/plan-authoring.md +181 -0
  101. package/plugins/pbr/references/plan-format.md +283 -0
  102. package/plugins/pbr/references/planning-config.md +213 -0
  103. package/plugins/pbr/references/questioning.md +214 -0
  104. package/plugins/pbr/references/reading-verification.md +127 -0
  105. package/plugins/pbr/references/stub-patterns.md +160 -0
  106. package/plugins/pbr/references/subagent-coordination.md +119 -0
  107. package/plugins/pbr/references/ui-formatting.md +399 -0
  108. package/plugins/pbr/references/verification-patterns.md +198 -0
  109. package/plugins/pbr/references/wave-execution.md +95 -0
  110. package/plugins/pbr/scripts/auto-continue.js +80 -0
  111. package/plugins/pbr/scripts/check-dangerous-commands.js +136 -0
  112. package/plugins/pbr/scripts/check-doc-sprawl.js +102 -0
  113. package/plugins/pbr/scripts/check-phase-boundary.js +196 -0
  114. package/plugins/pbr/scripts/check-plan-format.js +270 -0
  115. package/plugins/pbr/scripts/check-roadmap-sync.js +252 -0
  116. package/plugins/pbr/scripts/check-skill-workflow.js +262 -0
  117. package/plugins/pbr/scripts/check-state-sync.js +476 -0
  118. package/plugins/pbr/scripts/check-subagent-output.js +144 -0
  119. package/plugins/pbr/scripts/config-schema.json +251 -0
  120. package/plugins/pbr/scripts/context-budget-check.js +287 -0
  121. package/plugins/pbr/scripts/event-handler.js +151 -0
  122. package/plugins/pbr/scripts/event-logger.js +92 -0
  123. package/plugins/pbr/scripts/hook-logger.js +76 -0
  124. package/plugins/pbr/scripts/hooks-schema.json +79 -0
  125. package/plugins/pbr/scripts/log-subagent.js +152 -0
  126. package/plugins/pbr/scripts/log-tool-failure.js +88 -0
  127. package/plugins/pbr/scripts/pbr-tools.js +1301 -0
  128. package/plugins/pbr/scripts/post-write-dispatch.js +66 -0
  129. package/plugins/pbr/scripts/post-write-quality.js +207 -0
  130. package/plugins/pbr/scripts/pre-bash-dispatch.js +56 -0
  131. package/plugins/pbr/scripts/pre-write-dispatch.js +62 -0
  132. package/plugins/pbr/scripts/progress-tracker.js +228 -0
  133. package/plugins/pbr/scripts/session-cleanup.js +254 -0
  134. package/plugins/pbr/scripts/status-line.js +285 -0
  135. package/plugins/pbr/scripts/suggest-compact.js +119 -0
  136. package/plugins/pbr/scripts/task-completed.js +45 -0
  137. package/plugins/pbr/scripts/track-context-budget.js +119 -0
  138. package/plugins/pbr/scripts/validate-commit.js +200 -0
  139. package/plugins/pbr/scripts/validate-plugin-structure.js +172 -0
  140. package/plugins/pbr/skills/begin/SKILL.md +545 -0
  141. package/plugins/pbr/skills/begin/templates/PROJECT.md.tmpl +33 -0
  142. package/plugins/pbr/skills/begin/templates/REQUIREMENTS.md.tmpl +18 -0
  143. package/plugins/pbr/skills/begin/templates/STATE.md.tmpl +49 -0
  144. package/plugins/pbr/skills/begin/templates/config.json.tmpl +63 -0
  145. package/plugins/pbr/skills/begin/templates/researcher-prompt.md.tmpl +19 -0
  146. package/plugins/pbr/skills/begin/templates/roadmap-prompt.md.tmpl +30 -0
  147. package/plugins/pbr/skills/begin/templates/synthesis-prompt.md.tmpl +16 -0
  148. package/plugins/pbr/skills/build/SKILL.md +962 -0
  149. package/plugins/pbr/skills/config/SKILL.md +241 -0
  150. package/plugins/pbr/skills/continue/SKILL.md +127 -0
  151. package/plugins/pbr/skills/debug/SKILL.md +489 -0
  152. package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +16 -0
  153. package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +27 -0
  154. package/plugins/pbr/skills/discuss/SKILL.md +338 -0
  155. package/plugins/pbr/skills/discuss/templates/CONTEXT.md.tmpl +61 -0
  156. package/plugins/pbr/skills/discuss/templates/decision-categories.md +9 -0
  157. package/plugins/pbr/skills/explore/SKILL.md +362 -0
  158. package/plugins/pbr/skills/health/SKILL.md +186 -0
  159. package/plugins/pbr/skills/health/templates/check-pattern.md.tmpl +30 -0
  160. package/plugins/pbr/skills/health/templates/output-format.md.tmpl +63 -0
  161. package/plugins/pbr/skills/help/SKILL.md +140 -0
  162. package/plugins/pbr/skills/import/SKILL.md +490 -0
  163. package/plugins/pbr/skills/milestone/SKILL.md +673 -0
  164. package/plugins/pbr/skills/milestone/templates/audit-report.md.tmpl +48 -0
  165. package/plugins/pbr/skills/milestone/templates/stats-file.md.tmpl +30 -0
  166. package/plugins/pbr/skills/note/SKILL.md +212 -0
  167. package/plugins/pbr/skills/pause/SKILL.md +235 -0
  168. package/plugins/pbr/skills/pause/templates/continue-here.md.tmpl +71 -0
  169. package/plugins/pbr/skills/plan/SKILL.md +628 -0
  170. package/plugins/pbr/skills/plan/decimal-phase-calc.md +98 -0
  171. package/plugins/pbr/skills/plan/templates/checker-prompt.md.tmpl +21 -0
  172. package/plugins/pbr/skills/plan/templates/gap-closure-prompt.md.tmpl +32 -0
  173. package/plugins/pbr/skills/plan/templates/planner-prompt.md.tmpl +38 -0
  174. package/plugins/pbr/skills/plan/templates/researcher-prompt.md.tmpl +19 -0
  175. package/plugins/pbr/skills/plan/templates/revision-prompt.md.tmpl +23 -0
  176. package/plugins/pbr/skills/quick/SKILL.md +335 -0
  177. package/plugins/pbr/skills/resume/SKILL.md +388 -0
  178. package/plugins/pbr/skills/review/SKILL.md +652 -0
  179. package/plugins/pbr/skills/review/templates/debugger-prompt.md.tmpl +60 -0
  180. package/plugins/pbr/skills/review/templates/gap-planner-prompt.md.tmpl +40 -0
  181. package/plugins/pbr/skills/review/templates/verifier-prompt.md.tmpl +115 -0
  182. package/plugins/pbr/skills/scan/SKILL.md +269 -0
  183. package/plugins/pbr/skills/scan/templates/mapper-prompt.md.tmpl +201 -0
  184. package/plugins/pbr/skills/setup/SKILL.md +227 -0
  185. package/plugins/pbr/skills/shared/commit-planning-docs.md +35 -0
  186. package/plugins/pbr/skills/shared/config-loading.md +102 -0
  187. package/plugins/pbr/skills/shared/context-budget.md +40 -0
  188. package/plugins/pbr/skills/shared/context-loader-task.md +86 -0
  189. package/plugins/pbr/skills/shared/digest-select.md +79 -0
  190. package/plugins/pbr/skills/shared/domain-probes.md +125 -0
  191. package/plugins/pbr/skills/shared/error-reporting.md +79 -0
  192. package/plugins/pbr/skills/shared/gate-prompts.md +388 -0
  193. package/plugins/pbr/skills/shared/phase-argument-parsing.md +45 -0
  194. package/plugins/pbr/skills/shared/progress-display.md +53 -0
  195. package/plugins/pbr/skills/shared/revision-loop.md +81 -0
  196. package/plugins/pbr/skills/shared/state-loading.md +62 -0
  197. package/plugins/pbr/skills/shared/state-update.md +161 -0
  198. package/plugins/pbr/skills/shared/universal-anti-patterns.md +33 -0
  199. package/plugins/pbr/skills/status/SKILL.md +353 -0
  200. package/plugins/pbr/skills/todo/SKILL.md +181 -0
  201. package/plugins/pbr/templates/CONTEXT.md.tmpl +52 -0
  202. package/plugins/pbr/templates/INTEGRATION-REPORT.md.tmpl +151 -0
  203. package/plugins/pbr/templates/RESEARCH-SUMMARY.md.tmpl +97 -0
  204. package/plugins/pbr/templates/ROADMAP.md.tmpl +40 -0
  205. package/plugins/pbr/templates/SUMMARY.md.tmpl +81 -0
  206. package/plugins/pbr/templates/VERIFICATION-DETAIL.md.tmpl +116 -0
  207. package/plugins/pbr/templates/codebase/ARCHITECTURE.md.tmpl +98 -0
  208. package/plugins/pbr/templates/codebase/CONCERNS.md.tmpl +93 -0
  209. package/plugins/pbr/templates/codebase/CONVENTIONS.md.tmpl +104 -0
  210. package/plugins/pbr/templates/codebase/INTEGRATIONS.md.tmpl +78 -0
  211. package/plugins/pbr/templates/codebase/STACK.md.tmpl +78 -0
  212. package/plugins/pbr/templates/codebase/STRUCTURE.md.tmpl +80 -0
  213. package/plugins/pbr/templates/codebase/TESTING.md.tmpl +107 -0
  214. package/plugins/pbr/templates/continue-here.md.tmpl +73 -0
  215. package/plugins/pbr/templates/prompt-partials/phase-project-context.md.tmpl +37 -0
  216. package/plugins/pbr/templates/research/ARCHITECTURE.md.tmpl +124 -0
  217. package/plugins/pbr/templates/research/STACK.md.tmpl +71 -0
  218. package/plugins/pbr/templates/research/SUMMARY.md.tmpl +112 -0
  219. package/plugins/pbr/templates/research-outputs/phase-research.md.tmpl +81 -0
  220. package/plugins/pbr/templates/research-outputs/project-research.md.tmpl +99 -0
  221. package/plugins/pbr/templates/research-outputs/synthesis.md.tmpl +36 -0
@@ -0,0 +1,476 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PostToolUse hook: Auto-sync STATE.md and ROADMAP.md when SUMMARY or
5
+ * VERIFICATION files are written.
6
+ *
7
+ * Bridges the gap between build artifacts (SUMMARY/VERIFICATION) and
8
+ * tracking files (STATE.md/ROADMAP.md) so the status line stays current
9
+ * even when the orchestrator skips update steps.
10
+ *
11
+ * Trigger:
12
+ * - SUMMARY*.md or *SUMMARY*.md writes inside .planning/phases/
13
+ * - VERIFICATION.md writes inside .planning/phases/
14
+ *
15
+ * Guards:
16
+ * - Skips STATE.md / ROADMAP.md writes (prevents circular trigger)
17
+ * - Skips files outside .planning/phases/
18
+ * - Skips gracefully when tracking files don't exist
19
+ *
20
+ * Updates:
21
+ * - ROADMAP.md Progress table: Plans Complete, Status, Completed date
22
+ * - STATE.md Current Position: Plan count, Status, Last activity, Progress bar
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const { logHook } = require('./hook-logger');
28
+ const { logEvent } = require('./event-logger');
29
+ const { atomicWrite } = require('./pbr-tools');
30
+
31
+ /**
32
+ * Extract phase number from a phase directory name.
33
+ * E.g., "35-agent-output-budgets" → "35", "02-auth" → "02"
34
+ *
35
+ * @param {string} dirName - Directory name like "35-agent-output-budgets"
36
+ * @returns {string|null} Phase number string or null
37
+ */
38
+ function extractPhaseNum(dirName) {
39
+ const match = dirName.match(/^(\d+)-/);
40
+ return match ? match[1] : null;
41
+ }
42
+
43
+ /**
44
+ * Count PLAN and SUMMARY files in a phase directory.
45
+ *
46
+ * @param {string} phaseDir - Absolute path to the phase directory
47
+ * @returns {{ plans: number, summaries: number, completeSummaries: number }}
48
+ */
49
+ function countPhaseArtifacts(phaseDir) {
50
+ try {
51
+ const files = fs.readdirSync(phaseDir);
52
+ const plans = files.filter(f => /-PLAN\.md$/.test(f));
53
+ const summaries = files.filter(f => /SUMMARY.*\.md$/.test(f) || /.*SUMMARY.*\.md$/.test(f));
54
+
55
+ // Filter for summaries that have status: complete in frontmatter
56
+ let completeSummaries = 0;
57
+ for (const s of summaries) {
58
+ try {
59
+ const content = fs.readFileSync(path.join(phaseDir, s), 'utf8');
60
+ if (/status:\s*["']?complete/i.test(content)) {
61
+ completeSummaries++;
62
+ }
63
+ } catch (_e) {
64
+ // Skip unreadable files
65
+ }
66
+ }
67
+
68
+ return { plans: plans.length, summaries: summaries.length, completeSummaries };
69
+ } catch (_e) {
70
+ return { plans: 0, summaries: 0, completeSummaries: 0 };
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Update the Progress table in ROADMAP.md content.
76
+ *
77
+ * Progress table format:
78
+ * | Phase | Plans Complete | Status | Completed |
79
+ * |-------|----------------|--------|-----------|
80
+ * | 01. Project Scaffolding | 2/2 | Complete | 2026-02-08 |
81
+ *
82
+ * Phase column contains "NN. Name" — we match on the leading number.
83
+ *
84
+ * @param {string} content - Full ROADMAP.md content
85
+ * @param {string} phaseNum - Phase number (e.g., "35")
86
+ * @param {string} plansComplete - Plans complete string (e.g., "2/3")
87
+ * @param {string} status - New status (e.g., "Complete", "In progress")
88
+ * @param {string|null} completedDate - ISO date or null (sets Completed column)
89
+ * @returns {string} Updated content
90
+ */
91
+ function updateProgressTable(content, phaseNum, plansComplete, status, completedDate) {
92
+ const lines = content.split('\n');
93
+ const paddedPhase = phaseNum.padStart(2, '0');
94
+
95
+ // Find the Progress table by looking for a header row with "Plans Complete"
96
+ let inProgressTable = false;
97
+
98
+ for (let i = 0; i < lines.length; i++) {
99
+ const line = lines[i];
100
+
101
+ if (!inProgressTable) {
102
+ if (line.includes('|') && /Plans\s*Complete/i.test(line)) {
103
+ inProgressTable = true;
104
+ }
105
+ continue;
106
+ }
107
+
108
+ // Skip separator row
109
+ if (/^\s*\|[\s-:|]+\|\s*$/.test(line)) continue;
110
+
111
+ // Non-table line ends the table
112
+ if (!line.includes('|')) break;
113
+
114
+ // Check if this row matches our phase number
115
+ const parts = line.split('|');
116
+ if (parts.length < 5) continue; // Need at least: empty | Phase | Plans | Status | Completed | empty
117
+
118
+ const phaseCol = (parts[1] || '').trim();
119
+ const phaseMatch = phaseCol.match(/^(\d+)\./);
120
+ if (!phaseMatch) continue;
121
+
122
+ if (phaseMatch[1] === paddedPhase || String(parseInt(phaseMatch[1], 10)) === String(parseInt(phaseNum, 10))) {
123
+ // Update this row
124
+ parts[2] = ` ${plansComplete} `;
125
+ parts[3] = ` ${status} `;
126
+ if (completedDate !== undefined && completedDate !== null) {
127
+ parts[4] = ` ${completedDate} `;
128
+ }
129
+ lines[i] = parts.join('|');
130
+
131
+ return lines.join('\n');
132
+ }
133
+ }
134
+
135
+ // Phase not found in Progress table — return unchanged
136
+ return content;
137
+ }
138
+
139
+ /**
140
+ * Update the Current Position section in STATE.md.
141
+ *
142
+ * Handles the legacy (non-frontmatter) format:
143
+ * ## Current Position
144
+ * Phase: 1 of 10 (Setup)
145
+ * Plan: 0 of 2 in current phase
146
+ * Status: Ready to plan
147
+ * Last activity: 2026-02-08 -- Project initialized
148
+ * Progress: [████░░░░░░░░░░░░░░░░] 20%
149
+ *
150
+ * @param {string} content - Full STATE.md content
151
+ * @param {object} updates - Fields to update
152
+ * @param {string} [updates.planLine] - New Plan: line value (e.g., "2 of 3 in current phase")
153
+ * @param {string} [updates.status] - New Status: value (e.g., "Building")
154
+ * @param {string} [updates.lastActivity] - New Last activity: value
155
+ * @param {number} [updates.progressPct] - New progress percentage (0-100)
156
+ * @returns {string} Updated content
157
+ */
158
+ function updateStatePosition(content, updates) {
159
+ const lines = content.split('\n');
160
+
161
+ for (let i = 0; i < lines.length; i++) {
162
+ const line = lines[i];
163
+
164
+ if (updates.planLine !== undefined && /^Plan:\s/.test(line)) {
165
+ lines[i] = `Plan: ${updates.planLine}`;
166
+ }
167
+
168
+ if (updates.status !== undefined && /^Status:\s/.test(line)) {
169
+ lines[i] = `Status: ${updates.status}`;
170
+ }
171
+
172
+ if (updates.lastActivity !== undefined && /^Last activity:\s/i.test(line)) {
173
+ lines[i] = `Last activity: ${updates.lastActivity}`;
174
+ }
175
+
176
+ if (updates.progressPct !== undefined && /^Progress:\s/.test(line)) {
177
+ lines[i] = `Progress: ${buildProgressBar(updates.progressPct)}`;
178
+ }
179
+ }
180
+
181
+ // Also update frontmatter fields if present
182
+ if (content.startsWith('---')) {
183
+ const fmEnd = content.indexOf('---', 3);
184
+ if (fmEnd !== -1) {
185
+ let fm = content.substring(0, fmEnd + 3);
186
+ const body = content.substring(fmEnd + 3);
187
+
188
+ if (updates.fmPlansComplete !== undefined) {
189
+ fm = fm.replace(/^(plans_complete:\s*).*/m, `$1${updates.fmPlansComplete}`);
190
+ }
191
+ if (updates.fmStatus !== undefined) {
192
+ fm = fm.replace(/^(status:\s*).*/m, `$1"${updates.fmStatus}"`);
193
+ }
194
+ if (updates.fmLastActivity !== undefined) {
195
+ fm = fm.replace(/^(last_activity:\s*).*/m, `$1"${updates.fmLastActivity}"`);
196
+ }
197
+ if (updates.fmProgressPct !== undefined) {
198
+ fm = fm.replace(/^(progress_percent:\s*).*/m, `$1${updates.fmProgressPct}`);
199
+ }
200
+
201
+ // Reconstruct with updated frontmatter + body with line updates
202
+ const updatedBody = updateStatePositionBody(body, updates);
203
+ return fm + updatedBody;
204
+ }
205
+ }
206
+
207
+ return lines.join('\n');
208
+ }
209
+
210
+ /**
211
+ * Update only the body (after frontmatter) of STATE.md.
212
+ */
213
+ function updateStatePositionBody(body, updates) {
214
+ const lines = body.split('\n');
215
+
216
+ for (let i = 0; i < lines.length; i++) {
217
+ const line = lines[i];
218
+
219
+ if (updates.planLine !== undefined && /^Plan:\s/.test(line)) {
220
+ lines[i] = `Plan: ${updates.planLine}`;
221
+ }
222
+ if (updates.status !== undefined && /^Status:\s/.test(line)) {
223
+ lines[i] = `Status: ${updates.status}`;
224
+ }
225
+ if (updates.lastActivity !== undefined && /^Last activity:\s/i.test(line)) {
226
+ lines[i] = `Last activity: ${updates.lastActivity}`;
227
+ }
228
+ if (updates.progressPct !== undefined && /^Progress:\s/.test(line)) {
229
+ lines[i] = `Progress: ${buildProgressBar(updates.progressPct)}`;
230
+ }
231
+ }
232
+
233
+ return lines.join('\n');
234
+ }
235
+
236
+ /**
237
+ * Build a text progress bar: [████░░░░░░░░░░░░░░░░] 20%
238
+ * @param {number} pct - Percentage 0-100
239
+ * @returns {string}
240
+ */
241
+ function buildProgressBar(pct) {
242
+ const width = 20;
243
+ const filled = Math.round((pct / 100) * width);
244
+ const empty = width - filled;
245
+ return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${pct}%`;
246
+ }
247
+
248
+ /**
249
+ * Calculate overall progress percentage from all phase directories.
250
+ * Counts completed summaries vs total plans across all phases.
251
+ *
252
+ * @param {string} phasesDir - Path to .planning/phases/
253
+ * @returns {number} Percentage 0-100
254
+ */
255
+ function calculateOverallProgress(phasesDir) {
256
+ try {
257
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true })
258
+ .filter(e => e.isDirectory());
259
+
260
+ let totalPlans = 0;
261
+ let completedPlans = 0;
262
+
263
+ for (const entry of entries) {
264
+ const dir = path.join(phasesDir, entry.name);
265
+ const artifacts = countPhaseArtifacts(dir);
266
+ totalPlans += artifacts.plans;
267
+ completedPlans += artifacts.completeSummaries;
268
+ }
269
+
270
+ return totalPlans > 0 ? Math.round((completedPlans / totalPlans) * 100) : 0;
271
+ } catch (_e) {
272
+ return 0;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Core state-sync check logic for use by dispatchers.
278
+ *
279
+ * @param {Object} data - Parsed hook input (tool_input, etc.)
280
+ * @returns {null|{output: Object}} null if not applicable, result with message otherwise
281
+ */
282
+ function checkStateSync(data) {
283
+ const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
284
+ const basename = path.basename(filePath);
285
+
286
+ // Guard: skip STATE.md and ROADMAP.md writes (prevents circular trigger)
287
+ if (basename === 'STATE.md' || basename === 'ROADMAP.md') return null;
288
+
289
+ // Determine if this is a SUMMARY or VERIFICATION write
290
+ const isSummary = basename.includes('SUMMARY') && basename.endsWith('.md');
291
+ const isVerification = basename === 'VERIFICATION.md';
292
+
293
+ if (!isSummary && !isVerification) return null;
294
+
295
+ // Guard: must be inside .planning/phases/
296
+ const normalizedPath = filePath.replace(/\\/g, '/');
297
+ if (!normalizedPath.includes('.planning/phases/')) return null;
298
+
299
+ // Extract phase directory
300
+ const phaseDir = path.dirname(filePath);
301
+ const phaseDirName = path.basename(phaseDir);
302
+ const phaseNum = extractPhaseNum(phaseDirName);
303
+
304
+ if (!phaseNum) {
305
+ logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'could not extract phase number', dir: phaseDirName });
306
+ return null;
307
+ }
308
+
309
+ const cwd = process.cwd();
310
+ const planningDir = path.join(cwd, '.planning');
311
+ const roadmapPath = path.join(planningDir, 'ROADMAP.md');
312
+ const statePath = path.join(planningDir, 'STATE.md');
313
+ const phasesDir = path.join(planningDir, 'phases');
314
+
315
+ // Count artifacts in this phase
316
+ const artifacts = countPhaseArtifacts(phaseDir);
317
+
318
+ if (artifacts.plans === 0) {
319
+ logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'no plans in phase', phase: phaseNum });
320
+ return null;
321
+ }
322
+
323
+ const today = new Date().toISOString().slice(0, 10);
324
+ const messages = [];
325
+
326
+ if (isSummary) {
327
+ const plansComplete = `${artifacts.completeSummaries}/${artifacts.plans}`;
328
+ const allComplete = artifacts.completeSummaries >= artifacts.plans;
329
+ const newStatus = allComplete ? 'Complete' : 'In progress';
330
+ const completedDate = allComplete ? today : null;
331
+
332
+ // Update ROADMAP.md Progress table
333
+ if (fs.existsSync(roadmapPath)) {
334
+ try {
335
+ const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
336
+ const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, newStatus, completedDate);
337
+ if (updatedRoadmap !== roadmapContent) {
338
+ atomicWrite(roadmapPath, updatedRoadmap);
339
+ messages.push(`ROADMAP.md: Phase ${phaseNum} → ${plansComplete} plans, ${newStatus}`);
340
+ }
341
+ } catch (e) {
342
+ logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'ROADMAP.md update failed', error: e.message });
343
+ }
344
+ }
345
+
346
+ // Update STATE.md
347
+ if (fs.existsSync(statePath)) {
348
+ try {
349
+ const stateContent = fs.readFileSync(statePath, 'utf8');
350
+ const overallPct = calculateOverallProgress(phasesDir);
351
+ const stateUpdates = {
352
+ planLine: `${artifacts.completeSummaries} of ${artifacts.plans} in current phase`,
353
+ status: allComplete ? 'Built' : 'Building',
354
+ lastActivity: `${today} -- Phase ${phaseNum} plan completed`,
355
+ progressPct: overallPct,
356
+ fmPlansComplete: artifacts.completeSummaries,
357
+ fmStatus: allComplete ? 'built' : 'building',
358
+ fmLastActivity: today,
359
+ fmProgressPct: overallPct
360
+ };
361
+ const updatedState = updateStatePosition(stateContent, stateUpdates);
362
+ if (updatedState !== stateContent) {
363
+ atomicWrite(statePath, updatedState);
364
+ messages.push(`STATE.md: ${artifacts.completeSummaries}/${artifacts.plans} plans, ${overallPct}%`);
365
+ }
366
+ } catch (e) {
367
+ logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'STATE.md update failed', error: e.message });
368
+ }
369
+ }
370
+ }
371
+
372
+ if (isVerification) {
373
+ // Read VERIFICATION.md frontmatter for status
374
+ let verStatus = null;
375
+ try {
376
+ if (fs.existsSync(filePath)) {
377
+ const vContent = fs.readFileSync(filePath, 'utf8');
378
+ const statusMatch = vContent.match(/status:\s*["']?(\w+)/i);
379
+ if (statusMatch) {
380
+ verStatus = statusMatch[1].toLowerCase();
381
+ }
382
+ }
383
+ } catch (_e) {
384
+ // Skip if unreadable
385
+ }
386
+
387
+ if (!verStatus) {
388
+ logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'no status in VERIFICATION.md', phase: phaseNum });
389
+ return null;
390
+ }
391
+
392
+ const isPassed = verStatus === 'passed';
393
+ const roadmapStatus = isPassed ? 'Complete' : 'Needs fixes';
394
+ const stateStatus = isPassed ? 'Verified' : 'Needs fixes';
395
+ const completedDate = isPassed ? today : null;
396
+ const plansComplete = `${artifacts.completeSummaries}/${artifacts.plans}`;
397
+
398
+ // Update ROADMAP.md Progress table
399
+ if (fs.existsSync(roadmapPath)) {
400
+ try {
401
+ const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
402
+ const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, roadmapStatus, completedDate);
403
+ if (updatedRoadmap !== roadmapContent) {
404
+ atomicWrite(roadmapPath, updatedRoadmap);
405
+ messages.push(`ROADMAP.md: Phase ${phaseNum} → ${roadmapStatus}`);
406
+ }
407
+ } catch (e) {
408
+ logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'ROADMAP.md update failed', error: e.message });
409
+ }
410
+ }
411
+
412
+ // Update STATE.md
413
+ if (fs.existsSync(statePath)) {
414
+ try {
415
+ const stateContent = fs.readFileSync(statePath, 'utf8');
416
+ const overallPct = calculateOverallProgress(phasesDir);
417
+ const stateUpdates = {
418
+ status: stateStatus,
419
+ lastActivity: `${today} -- Phase ${phaseNum} ${isPassed ? 'verified' : 'needs fixes'}`,
420
+ progressPct: overallPct,
421
+ fmStatus: isPassed ? 'verified' : 'needs_fixes',
422
+ fmLastActivity: today,
423
+ fmProgressPct: overallPct
424
+ };
425
+ const updatedState = updateStatePosition(stateContent, stateUpdates);
426
+ if (updatedState !== stateContent) {
427
+ atomicWrite(statePath, updatedState);
428
+ messages.push(`STATE.md: ${stateStatus}, ${overallPct}%`);
429
+ }
430
+ } catch (e) {
431
+ logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'STATE.md update failed', error: e.message });
432
+ }
433
+ }
434
+ }
435
+
436
+ if (messages.length > 0) {
437
+ const msg = `Auto-synced tracking files: ${messages.join('; ')}`;
438
+ logHook('check-state-sync', 'PostToolUse', 'sync', { phase: phaseNum, updates: messages });
439
+ logEvent('workflow', 'state-sync', { phase: phaseNum, trigger: isSummary ? 'summary' : 'verification', updates: messages });
440
+ return { output: { message: msg } };
441
+ }
442
+
443
+ logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'no tracking files to update', phase: phaseNum });
444
+ return null;
445
+ }
446
+
447
+ // Standalone mode
448
+ function main() {
449
+ let input = '';
450
+
451
+ process.stdin.setEncoding('utf8');
452
+ process.stdin.on('data', (chunk) => { input += chunk; });
453
+ process.stdin.on('end', () => {
454
+ try {
455
+ const data = JSON.parse(input);
456
+ const result = checkStateSync(data);
457
+ if (result) {
458
+ process.stdout.write(JSON.stringify(result.output));
459
+ }
460
+ process.exit(0);
461
+ } catch (_e) {
462
+ process.exit(0);
463
+ }
464
+ });
465
+ }
466
+
467
+ if (require.main === module) { main(); }
468
+ module.exports = {
469
+ extractPhaseNum,
470
+ countPhaseArtifacts,
471
+ updateProgressTable,
472
+ updateStatePosition,
473
+ buildProgressBar,
474
+ calculateOverallProgress,
475
+ checkStateSync
476
+ };
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PostToolUse hook on Task: Validates that subagent outputs exist.
5
+ *
6
+ * Maps agent types to expected output files and warns if they're missing
7
+ * after the agent completes. This catches silent agent failures early
8
+ * rather than discovering them during verification.
9
+ *
10
+ * Agent → Expected output mapping:
11
+ * executor → SUMMARY-{plan_id}.md (or SUMMARY.md) in the phase directory
12
+ * planner → PLAN-{MM}.md in the phase directory
13
+ * verifier → VERIFICATION.md in the phase directory
14
+ * researcher → RESEARCH.md (or domain-specific .md) in research/
15
+ *
16
+ * Exit codes:
17
+ * 0 = always (informational hook, never blocks — PostToolUse can only warn)
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { logHook } = require('./hook-logger');
23
+
24
+ // Agent type → expected output patterns
25
+ const AGENT_OUTPUTS = {
26
+ 'pbr:executor': {
27
+ description: 'SUMMARY.md in the phase directory',
28
+ check: (planningDir) => findInPhaseDir(planningDir, /^SUMMARY.*\.md$/i)
29
+ },
30
+ 'pbr:planner': {
31
+ description: 'PLAN.md in the phase directory',
32
+ check: (planningDir) => findInPhaseDir(planningDir, /^PLAN.*\.md$/i)
33
+ },
34
+ 'pbr:verifier': {
35
+ description: 'VERIFICATION.md in the phase directory',
36
+ check: (planningDir) => findInPhaseDir(planningDir, /^VERIFICATION\.md$/i)
37
+ },
38
+ 'pbr:researcher': {
39
+ description: 'research file in .planning/research/',
40
+ check: (planningDir) => {
41
+ const researchDir = path.join(planningDir, 'research');
42
+ if (!fs.existsSync(researchDir)) return [];
43
+ try {
44
+ return fs.readdirSync(researchDir)
45
+ .filter(f => f.endsWith('.md'))
46
+ .map(f => path.join('research', f));
47
+ } catch (_e) {
48
+ return [];
49
+ }
50
+ }
51
+ }
52
+ };
53
+
54
+ function findInPhaseDir(planningDir, pattern) {
55
+ const matches = [];
56
+ const phasesDir = path.join(planningDir, 'phases');
57
+ if (!fs.existsSync(phasesDir)) return matches;
58
+
59
+ try {
60
+ // Find the active phase from STATE.md
61
+ const stateFile = path.join(planningDir, 'STATE.md');
62
+ if (!fs.existsSync(stateFile)) return matches;
63
+
64
+ const stateContent = fs.readFileSync(stateFile, 'utf8');
65
+ const phaseMatch = stateContent.match(/Phase:\s*(\d+)\s+of\s+\d+/);
66
+ if (!phaseMatch) return matches;
67
+
68
+ const currentPhase = phaseMatch[1].padStart(2, '0');
69
+ const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase));
70
+ if (dirs.length === 0) return matches;
71
+
72
+ const phaseDir = path.join(phasesDir, dirs[0]);
73
+ const files = fs.readdirSync(phaseDir);
74
+ for (const file of files) {
75
+ if (pattern.test(file)) {
76
+ // Check it's non-empty
77
+ const filePath = path.join(phaseDir, file);
78
+ const stat = fs.statSync(filePath);
79
+ if (stat.size > 0) {
80
+ matches.push(path.join('phases', dirs[0], file));
81
+ }
82
+ }
83
+ }
84
+ } catch (_e) {
85
+ // best-effort
86
+ }
87
+ return matches;
88
+ }
89
+
90
+ function readStdin() {
91
+ try {
92
+ const input = fs.readFileSync(0, 'utf8').trim();
93
+ if (input) return JSON.parse(input);
94
+ } catch (_e) {
95
+ // empty or non-JSON stdin
96
+ }
97
+ return {};
98
+ }
99
+
100
+ function main() {
101
+ const data = readStdin();
102
+ const cwd = process.cwd();
103
+ const planningDir = path.join(cwd, '.planning');
104
+
105
+ // Only relevant for Plan-Build-Run projects
106
+ if (!fs.existsSync(planningDir)) {
107
+ process.exit(0);
108
+ }
109
+
110
+ // Extract agent type from the Task completion data
111
+ const agentType = data.tool_input?.subagent_type || data.subagent_type || '';
112
+
113
+ // Only check known Plan-Build-Run agent types
114
+ const outputSpec = AGENT_OUTPUTS[agentType];
115
+ if (!outputSpec) {
116
+ process.exit(0);
117
+ }
118
+
119
+ // Check for expected outputs
120
+ const found = outputSpec.check(planningDir);
121
+
122
+ if (found.length === 0) {
123
+ logHook('check-subagent-output', 'PostToolUse', 'warning', {
124
+ agent_type: agentType,
125
+ expected: outputSpec.description,
126
+ found: 'none'
127
+ });
128
+
129
+ const output = {
130
+ additionalContext: `Warning: Agent ${agentType} completed but no ${outputSpec.description} was found. The agent may have failed silently. Check agent output for errors.`
131
+ };
132
+ process.stdout.write(JSON.stringify(output));
133
+ } else {
134
+ logHook('check-subagent-output', 'PostToolUse', 'verified', {
135
+ agent_type: agentType,
136
+ found: found
137
+ });
138
+ }
139
+
140
+ process.exit(0);
141
+ }
142
+
143
+ module.exports = { AGENT_OUTPUTS, findInPhaseDir };
144
+ if (require.main === module) { main(); }