@sienklogic/plan-build-run 2.0.0 → 2.0.2

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 (233) hide show
  1. package/CHANGELOG.md +56 -56
  2. package/CLAUDE.md +149 -149
  3. package/LICENSE +21 -21
  4. package/README.md +247 -247
  5. package/dashboard/bin/cli.js +25 -25
  6. package/dashboard/package.json +34 -34
  7. package/dashboard/public/css/layout.css +406 -406
  8. package/dashboard/public/css/status-colors.css +98 -98
  9. package/dashboard/public/js/htmx-title.js +5 -5
  10. package/dashboard/public/js/sidebar-toggle.js +20 -20
  11. package/dashboard/src/app.js +78 -78
  12. package/dashboard/src/middleware/errorHandler.js +52 -52
  13. package/dashboard/src/middleware/notFoundHandler.js +9 -9
  14. package/dashboard/src/repositories/planning.repository.js +128 -128
  15. package/dashboard/src/routes/events.routes.js +40 -40
  16. package/dashboard/src/routes/index.routes.js +31 -31
  17. package/dashboard/src/routes/pages.routes.js +245 -195
  18. package/dashboard/src/server.js +42 -42
  19. package/dashboard/src/services/dashboard.service.js +222 -222
  20. package/dashboard/src/services/phase.service.js +220 -167
  21. package/dashboard/src/services/project.service.js +57 -57
  22. package/dashboard/src/services/roadmap.service.js +171 -171
  23. package/dashboard/src/services/sse.service.js +58 -58
  24. package/dashboard/src/services/todo.service.js +254 -254
  25. package/dashboard/src/services/watcher.service.js +48 -48
  26. package/dashboard/src/views/coming-soon.ejs +11 -11
  27. package/dashboard/src/views/error.ejs +13 -13
  28. package/dashboard/src/views/index.ejs +5 -5
  29. package/dashboard/src/views/layout.ejs +1 -1
  30. package/dashboard/src/views/partials/dashboard-content.ejs +77 -77
  31. package/dashboard/src/views/partials/footer.ejs +3 -3
  32. package/dashboard/src/views/partials/head.ejs +21 -21
  33. package/dashboard/src/views/partials/header.ejs +12 -12
  34. package/dashboard/src/views/partials/layout-bottom.ejs +15 -15
  35. package/dashboard/src/views/partials/layout-top.ejs +8 -8
  36. package/dashboard/src/views/partials/phase-content.ejs +188 -181
  37. package/dashboard/src/views/partials/phase-doc-content.ejs +38 -0
  38. package/dashboard/src/views/partials/phases-content.ejs +117 -117
  39. package/dashboard/src/views/partials/roadmap-content.ejs +142 -142
  40. package/dashboard/src/views/partials/sidebar.ejs +38 -38
  41. package/dashboard/src/views/partials/todo-create-content.ejs +53 -53
  42. package/dashboard/src/views/partials/todo-detail-content.ejs +38 -38
  43. package/dashboard/src/views/partials/todos-content.ejs +53 -53
  44. package/dashboard/src/views/phase-detail.ejs +5 -5
  45. package/dashboard/src/views/phase-doc.ejs +5 -0
  46. package/dashboard/src/views/phases.ejs +5 -5
  47. package/dashboard/src/views/roadmap.ejs +5 -5
  48. package/dashboard/src/views/todo-create.ejs +5 -5
  49. package/dashboard/src/views/todo-detail.ejs +5 -5
  50. package/dashboard/src/views/todos.ejs +5 -5
  51. package/package.json +57 -57
  52. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +22 -0
  53. package/plugins/cursor-pbr/agents/.gitkeep +0 -0
  54. package/plugins/cursor-pbr/assets/.gitkeep +0 -0
  55. package/plugins/cursor-pbr/hooks/hooks.json +11 -0
  56. package/plugins/cursor-pbr/references/.gitkeep +0 -0
  57. package/plugins/cursor-pbr/rules/.gitkeep +0 -0
  58. package/plugins/cursor-pbr/skills/.gitkeep +0 -0
  59. package/plugins/cursor-pbr/templates/.gitkeep +0 -0
  60. package/plugins/pbr/.claude-plugin/plugin.json +13 -13
  61. package/plugins/pbr/UI-CONSISTENCY-GAPS.md +61 -61
  62. package/plugins/pbr/agents/codebase-mapper.md +279 -271
  63. package/plugins/pbr/agents/debugger.md +281 -281
  64. package/plugins/pbr/agents/executor.md +428 -407
  65. package/plugins/pbr/agents/general.md +164 -164
  66. package/plugins/pbr/agents/integration-checker.md +169 -141
  67. package/plugins/pbr/agents/plan-checker.md +296 -280
  68. package/plugins/pbr/agents/planner.md +358 -358
  69. package/plugins/pbr/agents/researcher.md +363 -363
  70. package/plugins/pbr/agents/synthesizer.md +230 -230
  71. package/plugins/pbr/agents/verifier.md +489 -454
  72. package/plugins/pbr/commands/begin.md +5 -5
  73. package/plugins/pbr/commands/build.md +5 -5
  74. package/plugins/pbr/commands/config.md +5 -5
  75. package/plugins/pbr/commands/continue.md +5 -5
  76. package/plugins/pbr/commands/debug.md +5 -5
  77. package/plugins/pbr/commands/discuss.md +5 -5
  78. package/plugins/pbr/commands/explore.md +5 -5
  79. package/plugins/pbr/commands/health.md +5 -5
  80. package/plugins/pbr/commands/help.md +5 -5
  81. package/plugins/pbr/commands/import.md +5 -5
  82. package/plugins/pbr/commands/milestone.md +5 -5
  83. package/plugins/pbr/commands/note.md +5 -5
  84. package/plugins/pbr/commands/pause.md +5 -5
  85. package/plugins/pbr/commands/plan.md +5 -5
  86. package/plugins/pbr/commands/quick.md +5 -5
  87. package/plugins/pbr/commands/resume.md +5 -5
  88. package/plugins/pbr/commands/review.md +5 -5
  89. package/plugins/pbr/commands/scan.md +5 -5
  90. package/plugins/pbr/commands/setup.md +5 -5
  91. package/plugins/pbr/commands/status.md +5 -5
  92. package/plugins/pbr/commands/todo.md +5 -5
  93. package/plugins/pbr/contexts/dev.md +27 -27
  94. package/plugins/pbr/contexts/research.md +28 -28
  95. package/plugins/pbr/contexts/review.md +36 -36
  96. package/plugins/pbr/hooks/hooks.json +183 -183
  97. package/plugins/pbr/references/agent-anti-patterns.md +24 -24
  98. package/plugins/pbr/references/agent-interactions.md +134 -134
  99. package/plugins/pbr/references/agent-teams.md +54 -54
  100. package/plugins/pbr/references/checkpoints.md +157 -157
  101. package/plugins/pbr/references/common-bug-patterns.md +13 -13
  102. package/plugins/pbr/references/config-reference.md +441 -0
  103. package/plugins/pbr/references/continuation-format.md +212 -212
  104. package/plugins/pbr/references/deviation-rules.md +112 -112
  105. package/plugins/pbr/references/git-integration.md +226 -226
  106. package/plugins/pbr/references/integration-patterns.md +117 -117
  107. package/plugins/pbr/references/model-profiles.md +99 -99
  108. package/plugins/pbr/references/model-selection.md +31 -31
  109. package/plugins/pbr/references/pbr-rules.md +193 -193
  110. package/plugins/pbr/references/plan-authoring.md +181 -181
  111. package/plugins/pbr/references/plan-format.md +287 -283
  112. package/plugins/pbr/references/planning-config.md +213 -213
  113. package/plugins/pbr/references/questioning.md +214 -214
  114. package/plugins/pbr/references/reading-verification.md +127 -127
  115. package/plugins/pbr/references/stub-patterns.md +160 -160
  116. package/plugins/pbr/references/subagent-coordination.md +119 -119
  117. package/plugins/pbr/references/ui-formatting.md +461 -399
  118. package/plugins/pbr/references/verification-patterns.md +198 -198
  119. package/plugins/pbr/references/wave-execution.md +95 -95
  120. package/plugins/pbr/scripts/auto-continue.js +80 -80
  121. package/plugins/pbr/scripts/check-dangerous-commands.js +136 -136
  122. package/plugins/pbr/scripts/check-doc-sprawl.js +102 -102
  123. package/plugins/pbr/scripts/check-phase-boundary.js +196 -196
  124. package/plugins/pbr/scripts/check-plan-format.js +270 -270
  125. package/plugins/pbr/scripts/check-roadmap-sync.js +322 -252
  126. package/plugins/pbr/scripts/check-skill-workflow.js +262 -262
  127. package/plugins/pbr/scripts/check-state-sync.js +476 -476
  128. package/plugins/pbr/scripts/check-subagent-output.js +144 -144
  129. package/plugins/pbr/scripts/config-schema.json +251 -251
  130. package/plugins/pbr/scripts/context-budget-check.js +287 -287
  131. package/plugins/pbr/scripts/event-handler.js +151 -151
  132. package/plugins/pbr/scripts/event-logger.js +92 -92
  133. package/plugins/pbr/scripts/hook-logger.js +80 -76
  134. package/plugins/pbr/scripts/hooks-schema.json +79 -79
  135. package/plugins/pbr/scripts/log-subagent.js +164 -152
  136. package/plugins/pbr/scripts/log-tool-failure.js +88 -88
  137. package/plugins/pbr/scripts/pbr-tools.js +1378 -1301
  138. package/plugins/pbr/scripts/post-write-dispatch.js +66 -66
  139. package/plugins/pbr/scripts/post-write-quality.js +207 -207
  140. package/plugins/pbr/scripts/pre-bash-dispatch.js +86 -56
  141. package/plugins/pbr/scripts/pre-write-dispatch.js +97 -62
  142. package/plugins/pbr/scripts/progress-tracker.js +281 -228
  143. package/plugins/pbr/scripts/run-hook.js +92 -0
  144. package/plugins/pbr/scripts/session-cleanup.js +254 -254
  145. package/plugins/pbr/scripts/status-line.js +288 -285
  146. package/plugins/pbr/scripts/suggest-compact.js +119 -119
  147. package/plugins/pbr/scripts/task-completed.js +45 -45
  148. package/plugins/pbr/scripts/track-context-budget.js +149 -119
  149. package/plugins/pbr/scripts/validate-commit.js +200 -200
  150. package/plugins/pbr/scripts/validate-plugin-structure.js +183 -172
  151. package/plugins/pbr/scripts/validate-task.js +106 -0
  152. package/plugins/pbr/skills/begin/SKILL.md +594 -545
  153. package/plugins/pbr/skills/begin/templates/PROJECT.md.tmpl +33 -33
  154. package/plugins/pbr/skills/begin/templates/REQUIREMENTS.md.tmpl +18 -18
  155. package/plugins/pbr/skills/begin/templates/STATE.md.tmpl +49 -49
  156. package/plugins/pbr/skills/begin/templates/config.json.tmpl +64 -63
  157. package/plugins/pbr/skills/begin/templates/researcher-prompt.md.tmpl +19 -19
  158. package/plugins/pbr/skills/begin/templates/roadmap-prompt.md.tmpl +30 -30
  159. package/plugins/pbr/skills/begin/templates/synthesis-prompt.md.tmpl +16 -16
  160. package/plugins/pbr/skills/build/SKILL.md +943 -962
  161. package/plugins/pbr/skills/config/SKILL.md +256 -241
  162. package/plugins/pbr/skills/continue/SKILL.md +164 -127
  163. package/plugins/pbr/skills/debug/SKILL.md +515 -489
  164. package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +16 -16
  165. package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +27 -27
  166. package/plugins/pbr/skills/discuss/SKILL.md +347 -338
  167. package/plugins/pbr/skills/discuss/templates/CONTEXT.md.tmpl +61 -61
  168. package/plugins/pbr/skills/discuss/templates/decision-categories.md +9 -9
  169. package/plugins/pbr/skills/explore/SKILL.md +378 -362
  170. package/plugins/pbr/skills/health/SKILL.md +221 -186
  171. package/plugins/pbr/skills/health/templates/check-pattern.md.tmpl +30 -30
  172. package/plugins/pbr/skills/health/templates/output-format.md.tmpl +63 -63
  173. package/plugins/pbr/skills/help/SKILL.md +155 -140
  174. package/plugins/pbr/skills/import/SKILL.md +504 -490
  175. package/plugins/pbr/skills/milestone/SKILL.md +704 -673
  176. package/plugins/pbr/skills/milestone/templates/audit-report.md.tmpl +48 -48
  177. package/plugins/pbr/skills/milestone/templates/stats-file.md.tmpl +30 -30
  178. package/plugins/pbr/skills/note/SKILL.md +231 -212
  179. package/plugins/pbr/skills/pause/SKILL.md +249 -235
  180. package/plugins/pbr/skills/pause/templates/continue-here.md.tmpl +71 -71
  181. package/plugins/pbr/skills/plan/SKILL.md +685 -628
  182. package/plugins/pbr/skills/plan/decimal-phase-calc.md +98 -98
  183. package/plugins/pbr/skills/plan/templates/checker-prompt.md.tmpl +21 -21
  184. package/plugins/pbr/skills/plan/templates/gap-closure-prompt.md.tmpl +32 -32
  185. package/plugins/pbr/skills/plan/templates/planner-prompt.md.tmpl +38 -38
  186. package/plugins/pbr/skills/plan/templates/researcher-prompt.md.tmpl +19 -19
  187. package/plugins/pbr/skills/plan/templates/revision-prompt.md.tmpl +23 -23
  188. package/plugins/pbr/skills/quick/SKILL.md +354 -335
  189. package/plugins/pbr/skills/resume/SKILL.md +402 -388
  190. package/plugins/pbr/skills/review/SKILL.md +686 -652
  191. package/plugins/pbr/skills/review/templates/debugger-prompt.md.tmpl +60 -60
  192. package/plugins/pbr/skills/review/templates/gap-planner-prompt.md.tmpl +40 -40
  193. package/plugins/pbr/skills/review/templates/verifier-prompt.md.tmpl +115 -115
  194. package/plugins/pbr/skills/scan/SKILL.md +304 -269
  195. package/plugins/pbr/skills/scan/templates/mapper-prompt.md.tmpl +201 -201
  196. package/plugins/pbr/skills/setup/SKILL.md +253 -227
  197. package/plugins/pbr/skills/shared/commit-planning-docs.md +35 -35
  198. package/plugins/pbr/skills/shared/config-loading.md +102 -102
  199. package/plugins/pbr/skills/shared/context-budget.md +40 -40
  200. package/plugins/pbr/skills/shared/context-loader-task.md +86 -86
  201. package/plugins/pbr/skills/shared/digest-select.md +79 -79
  202. package/plugins/pbr/skills/shared/domain-probes.md +125 -125
  203. package/plugins/pbr/skills/shared/error-reporting.md +79 -79
  204. package/plugins/pbr/skills/shared/gate-prompts.md +388 -388
  205. package/plugins/pbr/skills/shared/phase-argument-parsing.md +45 -45
  206. package/plugins/pbr/skills/shared/progress-display.md +53 -53
  207. package/plugins/pbr/skills/shared/revision-loop.md +81 -81
  208. package/plugins/pbr/skills/shared/state-loading.md +62 -62
  209. package/plugins/pbr/skills/shared/state-update.md +161 -161
  210. package/plugins/pbr/skills/shared/universal-anti-patterns.md +33 -33
  211. package/plugins/pbr/skills/status/SKILL.md +367 -353
  212. package/plugins/pbr/skills/todo/SKILL.md +198 -181
  213. package/plugins/pbr/templates/CONTEXT.md.tmpl +52 -52
  214. package/plugins/pbr/templates/INTEGRATION-REPORT.md.tmpl +151 -151
  215. package/plugins/pbr/templates/RESEARCH-SUMMARY.md.tmpl +97 -97
  216. package/plugins/pbr/templates/ROADMAP.md.tmpl +40 -40
  217. package/plugins/pbr/templates/SUMMARY.md.tmpl +81 -81
  218. package/plugins/pbr/templates/VERIFICATION-DETAIL.md.tmpl +116 -116
  219. package/plugins/pbr/templates/codebase/ARCHITECTURE.md.tmpl +98 -98
  220. package/plugins/pbr/templates/codebase/CONCERNS.md.tmpl +93 -93
  221. package/plugins/pbr/templates/codebase/CONVENTIONS.md.tmpl +104 -104
  222. package/plugins/pbr/templates/codebase/INTEGRATIONS.md.tmpl +78 -78
  223. package/plugins/pbr/templates/codebase/STACK.md.tmpl +78 -78
  224. package/plugins/pbr/templates/codebase/STRUCTURE.md.tmpl +80 -80
  225. package/plugins/pbr/templates/codebase/TESTING.md.tmpl +107 -107
  226. package/plugins/pbr/templates/continue-here.md.tmpl +73 -73
  227. package/plugins/pbr/templates/prompt-partials/phase-project-context.md.tmpl +37 -37
  228. package/plugins/pbr/templates/research/ARCHITECTURE.md.tmpl +124 -124
  229. package/plugins/pbr/templates/research/STACK.md.tmpl +71 -71
  230. package/plugins/pbr/templates/research/SUMMARY.md.tmpl +112 -112
  231. package/plugins/pbr/templates/research-outputs/phase-research.md.tmpl +81 -81
  232. package/plugins/pbr/templates/research-outputs/project-research.md.tmpl +99 -99
  233. package/plugins/pbr/templates/research-outputs/synthesis.md.tmpl +36 -36
@@ -1,252 +1,322 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * PostToolUse hook (async): Checks that ROADMAP.md phase status
5
- * stays in sync with STATE.md after state updates.
6
- *
7
- * When STATE.md is written/edited and contains a phase status
8
- * (planned, built, partial, verified), this hook checks if the
9
- * ROADMAP.md Phase Overview table has a matching status for that
10
- * phase. If not, it warns Claude to update ROADMAP.md.
11
- *
12
- * Runs asynchronously (non-blocking). Issues are reported but
13
- * don't prevent saving.
14
- */
15
-
16
- const fs = require('fs');
17
- const path = require('path');
18
- const { logHook } = require('./hook-logger');
19
- const { logEvent } = require('./event-logger');
20
-
21
- const LIFECYCLE_STATUSES = ['planned', 'built', 'partial', 'verified'];
22
-
23
- function main() {
24
- let input = '';
25
-
26
- process.stdin.setEncoding('utf8');
27
- process.stdin.on('data', (chunk) => { input += chunk; });
28
- process.stdin.on('end', () => {
29
- try {
30
- const data = JSON.parse(input);
31
- const filePath = data.tool_input?.file_path || '';
32
-
33
- if (!filePath.endsWith('STATE.md')) {
34
- process.exit(0);
35
- }
36
-
37
- const cwd = process.cwd();
38
- const planningDir = path.join(cwd, '.planning');
39
- const roadmapPath = path.join(planningDir, 'ROADMAP.md');
40
-
41
- if (!fs.existsSync(filePath) || !fs.existsSync(roadmapPath)) {
42
- process.exit(0);
43
- }
44
-
45
- const stateContent = fs.readFileSync(filePath, 'utf8');
46
- const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
47
-
48
- const stateInfo = parseState(stateContent);
49
- if (!stateInfo || !stateInfo.phase || !stateInfo.status) {
50
- logHook('check-roadmap-sync', 'PostToolUse', 'skip', { reason: 'could not parse STATE.md' });
51
- process.exit(0);
52
- }
53
-
54
- if (!LIFECYCLE_STATUSES.includes(stateInfo.status)) {
55
- logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
56
- reason: `status "${stateInfo.status}" not a lifecycle status`
57
- });
58
- process.exit(0);
59
- }
60
-
61
- const roadmapStatus = getRoadmapPhaseStatus(roadmapContent, stateInfo.phase);
62
- if (!roadmapStatus) {
63
- logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
64
- reason: `phase ${stateInfo.phase} not found in ROADMAP.md table`
65
- });
66
- process.exit(0);
67
- }
68
-
69
- if (roadmapStatus.toLowerCase() !== stateInfo.status) {
70
- logHook('check-roadmap-sync', 'PostToolUse', 'warn', {
71
- phase: stateInfo.phase,
72
- stateStatus: stateInfo.status,
73
- roadmapStatus: roadmapStatus
74
- });
75
- logEvent('workflow', 'roadmap-sync', {
76
- phase: stateInfo.phase,
77
- stateStatus: stateInfo.status,
78
- roadmapStatus: roadmapStatus,
79
- status: 'out-of-sync'
80
- });
81
-
82
- const output = {
83
- message: `ROADMAP.md out of sync: Phase ${stateInfo.phase} is "${roadmapStatus}" in ROADMAP.md but "${stateInfo.status}" in STATE.md. Update the Phase Overview table in ROADMAP.md to match.`
84
- };
85
- process.stdout.write(JSON.stringify(output));
86
- } else {
87
- logHook('check-roadmap-sync', 'PostToolUse', 'pass', {
88
- phase: stateInfo.phase,
89
- status: stateInfo.status
90
- });
91
- logEvent('workflow', 'roadmap-sync', {
92
- phase: stateInfo.phase,
93
- status: 'in-sync'
94
- });
95
- }
96
-
97
- process.exit(0);
98
- } catch (_e) {
99
- process.exit(0);
100
- }
101
- });
102
- }
103
-
104
- /**
105
- * Extract current phase number and status from STATE.md.
106
- * Handles common formats:
107
- * "**Phase**: 03 - slug-name"
108
- * "Phase: 3"
109
- * "Current phase: 03-slug-name"
110
- * "**Status**: planned"
111
- * "Phase status: built"
112
- */
113
- function parseState(content) {
114
- const phaseMatch = content.match(
115
- /\*{0,2}(?:Current\s+)?Phase\*{0,2}:\s*(\d+(?:\.\d+)?)/i
116
- );
117
-
118
- const statusMatch = content.match(
119
- /\*{0,2}(?:Phase\s+)?Status\*{0,2}:\s*["']?(\w+)["']?/i
120
- );
121
-
122
- if (!phaseMatch || !statusMatch) return null;
123
-
124
- return {
125
- phase: normalizePhaseNum(phaseMatch[1]),
126
- status: statusMatch[1].toLowerCase()
127
- };
128
- }
129
-
130
- /**
131
- * Find the status for a given phase in ROADMAP.md's Phase Overview table.
132
- * Table format:
133
- * | Phase | Name | Goal | Plans | Wave | Status |
134
- * |-------|------|------|-------|------|--------|
135
- * | 01 | ... | ... | ... | ... | pending |
136
- */
137
- function getRoadmapPhaseStatus(content, phaseNum) {
138
- const lines = content.split('\n');
139
-
140
- let statusColIndex = -1;
141
- let phaseColIndex = -1;
142
- let inTable = false;
143
-
144
- for (const line of lines) {
145
- if (!inTable) {
146
- if (line.includes('|') && /Phase/i.test(line) && /Status/i.test(line)) {
147
- const cols = splitTableRow(line);
148
- phaseColIndex = cols.findIndex(c => /^Phase$/i.test(c));
149
- statusColIndex = cols.findIndex(c => /^Status$/i.test(c));
150
- if (phaseColIndex !== -1 && statusColIndex !== -1) {
151
- inTable = true;
152
- }
153
- }
154
- continue;
155
- }
156
-
157
- // Skip separator row
158
- if (/^\s*\|[\s-:|]+\|\s*$/.test(line)) continue;
159
-
160
- // Non-table line ends the table
161
- if (!line.includes('|')) break;
162
-
163
- const cols = splitTableRow(line);
164
- if (cols.length <= Math.max(phaseColIndex, statusColIndex)) continue;
165
-
166
- const rowPhase = normalizePhaseNum(cols[phaseColIndex]);
167
- if (rowPhase === phaseNum) {
168
- return cols[statusColIndex];
169
- }
170
- }
171
-
172
- return null;
173
- }
174
-
175
- /** Split a markdown table row into trimmed cell values. */
176
- function splitTableRow(line) {
177
- return line.split('|').map(c => c.trim()).filter(Boolean);
178
- }
179
-
180
- /**
181
- * Extract and normalize a phase number from various formats:
182
- * "03" → "3"
183
- * "3.1" → "3.1"
184
- * "01. Project Scaffolding" → "1"
185
- * "Phase 02" → "2"
186
- */
187
- function normalizePhaseNum(raw) {
188
- const match = raw.match(/(?:Phase\s+)?0*(\d+(?:\.\d+)?)/i);
189
- return match ? match[1] : raw.trim();
190
- }
191
-
192
- /**
193
- * Core roadmap sync check logic for use by dispatchers.
194
- * @param {Object} data - Parsed hook input (tool_input, etc.)
195
- * @returns {null|{output: Object}} null if pass or not applicable, result otherwise
196
- */
197
- function checkSync(data) {
198
- const filePath = data.tool_input?.file_path || '';
199
-
200
- if (!filePath.endsWith('STATE.md')) return null;
201
-
202
- const cwd = process.cwd();
203
- const planningDir = path.join(cwd, '.planning');
204
- const roadmapPath = path.join(planningDir, 'ROADMAP.md');
205
-
206
- if (!fs.existsSync(filePath) || !fs.existsSync(roadmapPath)) return null;
207
-
208
- const stateContent = fs.readFileSync(filePath, 'utf8');
209
- const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
210
-
211
- const stateInfo = parseState(stateContent);
212
- if (!stateInfo || !stateInfo.phase || !stateInfo.status) {
213
- logHook('check-roadmap-sync', 'PostToolUse', 'skip', { reason: 'could not parse STATE.md' });
214
- return null;
215
- }
216
-
217
- if (!LIFECYCLE_STATUSES.includes(stateInfo.status)) {
218
- logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
219
- reason: `status "${stateInfo.status}" not a lifecycle status`
220
- });
221
- return null;
222
- }
223
-
224
- const roadmapStatus = getRoadmapPhaseStatus(roadmapContent, stateInfo.phase);
225
- if (!roadmapStatus) {
226
- logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
227
- reason: `phase ${stateInfo.phase} not found in ROADMAP.md table`
228
- });
229
- return null;
230
- }
231
-
232
- if (roadmapStatus.toLowerCase() !== stateInfo.status) {
233
- logHook('check-roadmap-sync', 'PostToolUse', 'warn', {
234
- phase: stateInfo.phase, stateStatus: stateInfo.status, roadmapStatus
235
- });
236
- logEvent('workflow', 'roadmap-sync', {
237
- phase: stateInfo.phase, stateStatus: stateInfo.status, roadmapStatus, status: 'out-of-sync'
238
- });
239
- return {
240
- output: {
241
- message: `ROADMAP.md out of sync: Phase ${stateInfo.phase} is "${roadmapStatus}" in ROADMAP.md but "${stateInfo.status}" in STATE.md. Update the Phase Overview table in ROADMAP.md to match.`
242
- }
243
- };
244
- }
245
-
246
- logHook('check-roadmap-sync', 'PostToolUse', 'pass', { phase: stateInfo.phase, status: stateInfo.status });
247
- logEvent('workflow', 'roadmap-sync', { phase: stateInfo.phase, status: 'in-sync' });
248
- return null;
249
- }
250
-
251
- module.exports = { parseState, getRoadmapPhaseStatus, checkSync };
252
- if (require.main === module) { main(); }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PostToolUse hook (async): Checks that ROADMAP.md phase status
5
+ * stays in sync with STATE.md after state updates.
6
+ *
7
+ * When STATE.md is written/edited and contains a phase status
8
+ * (planned, built, partial, verified), this hook checks if the
9
+ * ROADMAP.md Phase Overview table has a matching status for that
10
+ * phase. If not, it warns Claude to update ROADMAP.md.
11
+ *
12
+ * Runs asynchronously (non-blocking). Issues are reported but
13
+ * don't prevent saving.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { logHook } = require('./hook-logger');
19
+ const { logEvent } = require('./event-logger');
20
+
21
+ const LIFECYCLE_STATUSES = ['planned', 'built', 'partial', 'verified'];
22
+
23
+ function main() {
24
+ let input = '';
25
+
26
+ process.stdin.setEncoding('utf8');
27
+ process.stdin.on('data', (chunk) => { input += chunk; });
28
+ process.stdin.on('end', () => {
29
+ try {
30
+ const data = JSON.parse(input);
31
+ const filePath = data.tool_input?.file_path || '';
32
+
33
+ if (!filePath.endsWith('STATE.md')) {
34
+ process.exit(0);
35
+ }
36
+
37
+ const cwd = process.cwd();
38
+ const planningDir = path.join(cwd, '.planning');
39
+ const roadmapPath = path.join(planningDir, 'ROADMAP.md');
40
+
41
+ if (!fs.existsSync(filePath) || !fs.existsSync(roadmapPath)) {
42
+ process.exit(0);
43
+ }
44
+
45
+ const stateContent = fs.readFileSync(filePath, 'utf8');
46
+ const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
47
+
48
+ const stateInfo = parseState(stateContent);
49
+ if (!stateInfo || !stateInfo.phase || !stateInfo.status) {
50
+ logHook('check-roadmap-sync', 'PostToolUse', 'skip', { reason: 'could not parse STATE.md' });
51
+ process.exit(0);
52
+ }
53
+
54
+ if (!LIFECYCLE_STATUSES.includes(stateInfo.status)) {
55
+ logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
56
+ reason: `status "${stateInfo.status}" not a lifecycle status`
57
+ });
58
+ process.exit(0);
59
+ }
60
+
61
+ const roadmapStatus = getRoadmapPhaseStatus(roadmapContent, stateInfo.phase);
62
+ if (!roadmapStatus) {
63
+ logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
64
+ reason: `phase ${stateInfo.phase} not found in ROADMAP.md table`
65
+ });
66
+ process.exit(0);
67
+ }
68
+
69
+ if (roadmapStatus.toLowerCase() !== stateInfo.status) {
70
+ logHook('check-roadmap-sync', 'PostToolUse', 'warn', {
71
+ phase: stateInfo.phase,
72
+ stateStatus: stateInfo.status,
73
+ roadmapStatus: roadmapStatus
74
+ });
75
+ logEvent('workflow', 'roadmap-sync', {
76
+ phase: stateInfo.phase,
77
+ stateStatus: stateInfo.status,
78
+ roadmapStatus: roadmapStatus,
79
+ status: 'out-of-sync'
80
+ });
81
+
82
+ const output = {
83
+ message: `ROADMAP.md out of sync: Phase ${stateInfo.phase} is "${roadmapStatus}" in ROADMAP.md but "${stateInfo.status}" in STATE.md. Update the Phase Overview table in ROADMAP.md to match.`
84
+ };
85
+ process.stdout.write(JSON.stringify(output));
86
+ } else {
87
+ logHook('check-roadmap-sync', 'PostToolUse', 'pass', {
88
+ phase: stateInfo.phase,
89
+ status: stateInfo.status
90
+ });
91
+ logEvent('workflow', 'roadmap-sync', {
92
+ phase: stateInfo.phase,
93
+ status: 'in-sync'
94
+ });
95
+ }
96
+
97
+ process.exit(0);
98
+ } catch (_e) {
99
+ process.exit(0);
100
+ }
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Extract current phase number and status from STATE.md.
106
+ * Handles common formats:
107
+ * "**Phase**: 03 - slug-name"
108
+ * "Phase: 3"
109
+ * "Current phase: 03-slug-name"
110
+ * "**Status**: planned"
111
+ * "Phase status: built"
112
+ */
113
+ function parseState(content) {
114
+ const phaseMatch = content.match(
115
+ /\*{0,2}(?:Current\s+)?Phase\*{0,2}:\s*(\d+(?:\.\d+)?)/i
116
+ );
117
+
118
+ const statusMatch = content.match(
119
+ /\*{0,2}(?:Phase\s+)?Status\*{0,2}:\s*["']?(\w+)["']?/i
120
+ );
121
+
122
+ if (!phaseMatch || !statusMatch) return null;
123
+
124
+ return {
125
+ phase: normalizePhaseNum(phaseMatch[1]),
126
+ status: statusMatch[1].toLowerCase()
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Find the status for a given phase in ROADMAP.md's Phase Overview table.
132
+ * Table format:
133
+ * | Phase | Name | Goal | Plans | Wave | Status |
134
+ * |-------|------|------|-------|------|--------|
135
+ * | 01 | ... | ... | ... | ... | pending |
136
+ */
137
+ function getRoadmapPhaseStatus(content, phaseNum) {
138
+ const lines = content.split('\n');
139
+
140
+ let statusColIndex = -1;
141
+ let phaseColIndex = -1;
142
+ let inTable = false;
143
+
144
+ for (const line of lines) {
145
+ if (!inTable) {
146
+ if (line.includes('|') && /Phase/i.test(line) && /Status/i.test(line)) {
147
+ const cols = splitTableRow(line);
148
+ phaseColIndex = cols.findIndex(c => /^Phase$/i.test(c));
149
+ statusColIndex = cols.findIndex(c => /^Status$/i.test(c));
150
+ if (phaseColIndex !== -1 && statusColIndex !== -1) {
151
+ inTable = true;
152
+ }
153
+ }
154
+ continue;
155
+ }
156
+
157
+ // Skip separator row
158
+ if (/^\s*\|[\s-:|]+\|\s*$/.test(line)) continue;
159
+
160
+ // Non-table line ends the table
161
+ if (!line.includes('|')) break;
162
+
163
+ const cols = splitTableRow(line);
164
+ if (cols.length <= Math.max(phaseColIndex, statusColIndex)) continue;
165
+
166
+ const rowPhase = normalizePhaseNum(cols[phaseColIndex]);
167
+ if (rowPhase === phaseNum) {
168
+ return cols[statusColIndex];
169
+ }
170
+ }
171
+
172
+ return null;
173
+ }
174
+
175
+ /** Split a markdown table row into trimmed cell values. */
176
+ function splitTableRow(line) {
177
+ return line.split('|').map(c => c.trim()).filter(Boolean);
178
+ }
179
+
180
+ /**
181
+ * Extract and normalize a phase number from various formats:
182
+ * "03" → "3"
183
+ * "3.1" → "3.1"
184
+ * "01. Project Scaffolding" → "1"
185
+ * "Phase 02" → "2"
186
+ */
187
+ function normalizePhaseNum(raw) {
188
+ const match = raw.match(/(?:Phase\s+)?0*(\d+(?:\.\d+)?)/i);
189
+ return match ? match[1] : raw.trim();
190
+ }
191
+
192
+ /**
193
+ * Core roadmap sync check logic for use by dispatchers.
194
+ * @param {Object} data - Parsed hook input (tool_input, etc.)
195
+ * @returns {null|{output: Object}} null if pass or not applicable, result otherwise
196
+ */
197
+ function checkSync(data) {
198
+ const filePath = data.tool_input?.file_path || '';
199
+
200
+ if (!filePath.endsWith('STATE.md')) return null;
201
+
202
+ const cwd = process.cwd();
203
+ const planningDir = path.join(cwd, '.planning');
204
+ const roadmapPath = path.join(planningDir, 'ROADMAP.md');
205
+
206
+ if (!fs.existsSync(filePath) || !fs.existsSync(roadmapPath)) return null;
207
+
208
+ const stateContent = fs.readFileSync(filePath, 'utf8');
209
+ const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
210
+
211
+ const stateInfo = parseState(stateContent);
212
+ if (!stateInfo || !stateInfo.phase || !stateInfo.status) {
213
+ logHook('check-roadmap-sync', 'PostToolUse', 'skip', { reason: 'could not parse STATE.md' });
214
+ return null;
215
+ }
216
+
217
+ if (!LIFECYCLE_STATUSES.includes(stateInfo.status)) {
218
+ logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
219
+ reason: `status "${stateInfo.status}" not a lifecycle status`
220
+ });
221
+ return null;
222
+ }
223
+
224
+ const roadmapStatus = getRoadmapPhaseStatus(roadmapContent, stateInfo.phase);
225
+ if (!roadmapStatus) {
226
+ logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
227
+ reason: `phase ${stateInfo.phase} not found in ROADMAP.md table`
228
+ });
229
+ return null;
230
+ }
231
+
232
+ if (roadmapStatus.toLowerCase() !== stateInfo.status) {
233
+ logHook('check-roadmap-sync', 'PostToolUse', 'warn', {
234
+ phase: stateInfo.phase, stateStatus: stateInfo.status, roadmapStatus
235
+ });
236
+ logEvent('workflow', 'roadmap-sync', {
237
+ phase: stateInfo.phase, stateStatus: stateInfo.status, roadmapStatus, status: 'out-of-sync'
238
+ });
239
+ return {
240
+ output: {
241
+ message: `ROADMAP.md out of sync: Phase ${stateInfo.phase} is "${roadmapStatus}" in ROADMAP.md but "${stateInfo.status}" in STATE.md. Update the Phase Overview table in ROADMAP.md to match.`
242
+ }
243
+ };
244
+ }
245
+
246
+ logHook('check-roadmap-sync', 'PostToolUse', 'pass', { phase: stateInfo.phase, status: stateInfo.status });
247
+ logEvent('workflow', 'roadmap-sync', { phase: stateInfo.phase, status: 'in-sync' });
248
+ return null;
249
+ }
250
+
251
+ /**
252
+ * Parse all phase directory slugs referenced in ROADMAP.md.
253
+ * Looks for NN-slug patterns in the Phase Overview table or
254
+ * phase reference lines like "## Phase 01-setup" or "01-setup".
255
+ * Returns an array of unique directory names, e.g. ["01-setup", "02-auth"].
256
+ */
257
+ function parseRoadmapPhases(content) {
258
+ const phases = new Set();
259
+ const lines = content.split('\n');
260
+
261
+ for (const line of lines) {
262
+ // Match NN-slug patterns (at least two-digit prefix with hyphen and slug)
263
+ const matches = line.match(/\b(\d{2,}-[a-zA-Z][a-zA-Z0-9-]*)\b/g);
264
+ if (matches) {
265
+ for (const m of matches) {
266
+ phases.add(m);
267
+ }
268
+ }
269
+ }
270
+
271
+ return Array.from(phases);
272
+ }
273
+
274
+ /**
275
+ * Check for drift between ROADMAP.md phase references and actual
276
+ * phase directories on disk under .planning/phases/.
277
+ *
278
+ * Returns an array of warning strings. Empty array means no drift.
279
+ *
280
+ * @param {string} roadmapContent - Contents of ROADMAP.md
281
+ * @param {string} phasesDir - Absolute path to .planning/phases/
282
+ * @returns {string[]} warnings
283
+ */
284
+ function checkFilesystemDrift(roadmapContent, phasesDir) {
285
+ const warnings = [];
286
+
287
+ if (!fs.existsSync(phasesDir)) {
288
+ return warnings;
289
+ }
290
+
291
+ const roadmapPhases = parseRoadmapPhases(roadmapContent);
292
+
293
+ // Check that each ROADMAP.md phase has a directory on disk
294
+ for (const phase of roadmapPhases) {
295
+ const dirPath = path.join(phasesDir, phase);
296
+ if (!fs.existsSync(dirPath)) {
297
+ warnings.push(`Phase directory missing: .planning/phases/${phase} (referenced in ROADMAP.md)`);
298
+ }
299
+ }
300
+
301
+ // Check for orphaned directories not referenced in ROADMAP.md
302
+ let entries;
303
+ try {
304
+ entries = fs.readdirSync(phasesDir, { withFileTypes: true });
305
+ } catch (_e) {
306
+ return warnings;
307
+ }
308
+
309
+ for (const entry of entries) {
310
+ if (!entry.isDirectory()) continue;
311
+ // Only consider NN-slug directories
312
+ if (!/^\d{2,}-[a-zA-Z]/.test(entry.name)) continue;
313
+ if (!roadmapPhases.includes(entry.name)) {
314
+ warnings.push(`Orphaned phase directory: .planning/phases/${entry.name} (not referenced in ROADMAP.md)`);
315
+ }
316
+ }
317
+
318
+ return warnings;
319
+ }
320
+
321
+ module.exports = { parseState, getRoadmapPhaseStatus, checkSync, parseRoadmapPhases, checkFilesystemDrift };
322
+ if (require.main === module) { main(); }