@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,228 +1,281 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * SessionStart hook: Auto-detects .planning/ directory and injects
5
- * project state as additionalContext.
6
- *
7
- * If no .planning/ directory exists, exits silently (non-Plan-Build-Run project).
8
- * If STATE.md exists, reads and outputs a concise summary.
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const os = require('os');
14
- const { execSync } = require('child_process');
15
- const { logHook } = require('./hook-logger');
16
- const { logEvent } = require('./event-logger');
17
- const { configLoad } = require('./pbr-tools');
18
-
19
- function main() {
20
- const cwd = process.cwd();
21
- const planningDir = path.join(cwd, '.planning');
22
- const stateFile = path.join(planningDir, 'STATE.md');
23
-
24
- // Not a Plan-Build-Run project
25
- if (!fs.existsSync(planningDir)) {
26
- process.exit(0);
27
- }
28
-
29
- // Reset compaction counter for new session
30
- const { resetCounter } = require('./suggest-compact');
31
- resetCounter(planningDir);
32
-
33
- const context = buildContext(planningDir, stateFile);
34
-
35
- if (context) {
36
- const output = {
37
- additionalContext: context
38
- };
39
- process.stdout.write(JSON.stringify(output));
40
- logHook('progress-tracker', 'SessionStart', 'injected', { hasState: true });
41
- logEvent('workflow', 'session-start', { hasState: true });
42
- } else {
43
- logHook('progress-tracker', 'SessionStart', 'skipped', { hasState: false });
44
- logEvent('workflow', 'session-start', { hasState: false });
45
- }
46
-
47
- process.exit(0);
48
- }
49
-
50
- function buildContext(planningDir, stateFile) {
51
- const parts = [];
52
-
53
- parts.push('[Plan-Build-Run Project Detected]');
54
-
55
- // Git context
56
- try {
57
- const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', timeout: 3000 }).trim();
58
- const porcelain = execSync('git status --porcelain', { encoding: 'utf8', timeout: 3000 }).trim();
59
- const uncommitted = porcelain ? porcelain.split('\n').length : 0;
60
- const recentCommits = execSync('git log -5 --oneline', { encoding: 'utf8', timeout: 3000 }).trim();
61
- parts.push(`\nGit: ${branch} (${uncommitted} uncommitted file${uncommitted !== 1 ? 's' : ''})`);
62
- if (recentCommits) {
63
- parts.push(`Recent commits:\n${recentCommits}`);
64
- }
65
- } catch (_e) {
66
- // Not a git repo or git not available — skip
67
- }
68
-
69
- // Read STATE.md if it exists
70
- if (fs.existsSync(stateFile)) {
71
- const state = fs.readFileSync(stateFile, 'utf8');
72
-
73
- // Extract key sections
74
- const position = extractSection(state, 'Current Position');
75
- if (position) {
76
- parts.push(`\nPosition:\n${position}`);
77
- }
78
-
79
- const blockers = extractSection(state, 'Blockers/Concerns');
80
- if (blockers && !blockers.includes('None')) {
81
- parts.push(`\nBlockers:\n${blockers}`);
82
- }
83
-
84
- const continuity = extractSection(state, 'Session Continuity');
85
- if (continuity) {
86
- parts.push(`\nLast Session:\n${continuity}`);
87
- }
88
- } else {
89
- parts.push('\nNo STATE.md found. Run /pbr:begin to initialize or /pbr:status to check.');
90
- }
91
-
92
- // Check for .continue-here.md files
93
- const phasesDir = path.join(planningDir, 'phases');
94
- if (fs.existsSync(phasesDir)) {
95
- const continueFiles = findContinueFiles(phasesDir);
96
- if (continueFiles.length > 0) {
97
- parts.push(`\nPaused work found: ${continueFiles.join(', ')}`);
98
- parts.push('Run /pbr:resume to pick up where you left off.');
99
- }
100
- }
101
-
102
- // Check for config and validate
103
- const config = configLoad(planningDir);
104
- if (config) {
105
- parts.push(`\nConfig: depth=${config.depth || 'standard'}, mode=${config.mode || 'interactive'}`);
106
-
107
- // Validate config against schema (reuse already-loaded config)
108
- const schemaPath = path.join(__dirname, 'config-schema.json');
109
- if (fs.existsSync(schemaPath)) {
110
- const { configValidate } = require('./pbr-tools');
111
- const validation = configValidate(config);
112
- if (validation.warnings.length > 0) {
113
- parts.push(`\nConfig warnings: ${validation.warnings.join('; ')}`);
114
- }
115
- if (validation.errors.length > 0) {
116
- parts.push(`\nConfig errors: ${validation.errors.join('; ')}`);
117
- }
118
- }
119
- }
120
-
121
- // Check for quick notes
122
- const projectNotesFile = path.join(planningDir, 'NOTES.md');
123
- const globalNotesFile = path.join(os.homedir(), '.claude', 'notes.md');
124
- const projectNoteCount = countNotes(projectNotesFile);
125
- const globalNoteCount = countNotes(globalNotesFile);
126
- if (projectNoteCount > 0 || globalNoteCount > 0) {
127
- const noteParts = [];
128
- if (projectNoteCount > 0) noteParts.push(`${projectNoteCount} project`);
129
- if (globalNoteCount > 0) noteParts.push(`${globalNoteCount} global`);
130
- parts.push(`\nNotes: ${noteParts.join(', ')}. \`/pbr:note list\` to review.`);
131
- }
132
-
133
- // Check ROADMAP/STATE sync (S>M-2)
134
- const roadmapFile = path.join(planningDir, 'ROADMAP.md');
135
- if (fs.existsSync(stateFile) && fs.existsSync(roadmapFile)) {
136
- try {
137
- const roadmap = fs.readFileSync(roadmapFile, 'utf8');
138
- const state = fs.readFileSync(stateFile, 'utf8');
139
-
140
- // Extract current phase from STATE.md
141
- const phaseMatch = state.match(/Phase:\s*(\d+)\s+of\s+\d+/);
142
- if (phaseMatch) {
143
- const currentPhase = parseInt(phaseMatch[1], 10);
144
- // Check if ROADMAP shows this phase as already verified/complete
145
- const progressTable = roadmap.match(/## Progress[\s\S]*?\|[\s\S]*?(?=\n##|\s*$)/);
146
- if (progressTable) {
147
- const rows = progressTable[0].split('\n').filter(r => r.includes('|'));
148
- for (const row of rows) {
149
- const cols = row.split('|').map(c => c.trim()).filter(Boolean);
150
- if (cols.length >= 4) {
151
- const phaseNum = parseInt(cols[0], 10);
152
- const status = cols[3] ? cols[3].toLowerCase() : '';
153
- if (phaseNum === currentPhase && (status === 'verified' || status === 'complete')) {
154
- parts.push(`\nWarning: STATE.md may be outdated — ROADMAP.md shows phase ${currentPhase} as ${status}.`);
155
- }
156
- }
157
- }
158
- }
159
- }
160
- } catch (_e) {
161
- // Ignore parse errors
162
- }
163
- }
164
-
165
- // Check for stale .auto-next signal (S>M-9)
166
- const autoNextFile = path.join(planningDir, '.auto-next');
167
- if (fs.existsSync(autoNextFile)) {
168
- try {
169
- const stats = fs.statSync(autoNextFile);
170
- const ageMs = Date.now() - stats.mtimeMs;
171
- const ageMinutes = Math.floor(ageMs / 60000);
172
- if (ageMinutes > 10) {
173
- parts.push(`\nWarning: Stale .auto-next signal found (${ageMinutes} minutes old). This may trigger an unexpected command. Consider deleting .planning/.auto-next.`);
174
- logHook('progress-tracker', 'SessionStart', 'stale-auto-next', { ageMinutes });
175
- }
176
- } catch (_e) {
177
- // Ignore errors
178
- }
179
- }
180
-
181
- parts.push('\nAvailable commands: /pbr:status, /pbr:plan, /pbr:build, /pbr:review, /pbr:help');
182
-
183
- return parts.join('\n');
184
- }
185
-
186
- function extractSection(content, heading) {
187
- const regex = new RegExp(`##\\s+${escapeRegex(heading)}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`);
188
- const match = content.match(regex);
189
- if (!match) return null;
190
- const section = match[1].trim();
191
- // Return first 5 lines max
192
- return section.split('\n').slice(0, 5).join('\n');
193
- }
194
-
195
- function escapeRegex(str) {
196
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
197
- }
198
-
199
- function findContinueFiles(dir) {
200
- const results = [];
201
- try {
202
- const entries = fs.readdirSync(dir, { withFileTypes: true });
203
- for (const entry of entries) {
204
- const fullPath = path.join(dir, entry.name);
205
- if (entry.isDirectory()) {
206
- results.push(...findContinueFiles(fullPath));
207
- } else if (entry.name.includes('.continue-here')) {
208
- results.push(path.relative(dir, fullPath));
209
- }
210
- }
211
- } catch (_e) {
212
- // Ignore permission errors
213
- }
214
- return results;
215
- }
216
-
217
- function countNotes(filePath) {
218
- try {
219
- if (!fs.existsSync(filePath)) return 0;
220
- const content = fs.readFileSync(filePath, 'utf8');
221
- const lines = content.split('\n');
222
- return lines.filter(l => /^- \[/.test(l) && !l.includes('[promoted]')).length;
223
- } catch (_e) {
224
- return 0;
225
- }
226
- }
227
-
228
- main();
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SessionStart hook: Auto-detects .planning/ directory and injects
5
+ * project state as additionalContext.
6
+ *
7
+ * If no .planning/ directory exists, exits silently (non-Plan-Build-Run project).
8
+ * If STATE.md exists, reads and outputs a concise summary.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+ const { execSync } = require('child_process');
15
+ const { logHook } = require('./hook-logger');
16
+ const { logEvent } = require('./event-logger');
17
+ const { configLoad } = require('./pbr-tools');
18
+
19
+ function main() {
20
+ const cwd = process.cwd();
21
+ const planningDir = path.join(cwd, '.planning');
22
+ const stateFile = path.join(planningDir, 'STATE.md');
23
+
24
+ // Not a Plan-Build-Run project
25
+ if (!fs.existsSync(planningDir)) {
26
+ process.exit(0);
27
+ }
28
+
29
+ // Reset compaction counter for new session
30
+ const { resetCounter } = require('./suggest-compact');
31
+ resetCounter(planningDir);
32
+
33
+ const context = buildContext(planningDir, stateFile);
34
+
35
+ if (context) {
36
+ const output = {
37
+ additionalContext: context
38
+ };
39
+ process.stdout.write(JSON.stringify(output));
40
+ logHook('progress-tracker', 'SessionStart', 'injected', { hasState: true });
41
+ logEvent('workflow', 'session-start', { hasState: true });
42
+ } else {
43
+ logHook('progress-tracker', 'SessionStart', 'skipped', { hasState: false });
44
+ logEvent('workflow', 'session-start', { hasState: false });
45
+ }
46
+
47
+ process.exit(0);
48
+ }
49
+
50
+ function buildContext(planningDir, stateFile) {
51
+ const parts = [];
52
+
53
+ parts.push('[Plan-Build-Run Project Detected]');
54
+
55
+ // Git context
56
+ try {
57
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', timeout: 3000 }).trim();
58
+ const porcelain = execSync('git status --porcelain', { encoding: 'utf8', timeout: 3000 }).trim();
59
+ const uncommitted = porcelain ? porcelain.split('\n').length : 0;
60
+ const recentCommits = execSync('git log -5 --oneline', { encoding: 'utf8', timeout: 3000 }).trim();
61
+ parts.push(`\nGit: ${branch} (${uncommitted} uncommitted file${uncommitted !== 1 ? 's' : ''})`);
62
+ if (recentCommits) {
63
+ parts.push(`Recent commits:\n${recentCommits}`);
64
+ }
65
+ } catch (_e) {
66
+ // Not a git repo or git not available — skip
67
+ }
68
+
69
+ // Read STATE.md if it exists
70
+ if (fs.existsSync(stateFile)) {
71
+ const state = fs.readFileSync(stateFile, 'utf8');
72
+
73
+ // Extract key sections
74
+ const position = extractSection(state, 'Current Position');
75
+ if (position) {
76
+ parts.push(`\nPosition:\n${position}`);
77
+ }
78
+
79
+ const blockers = extractSection(state, 'Blockers/Concerns');
80
+ if (blockers && !blockers.includes('None')) {
81
+ parts.push(`\nBlockers:\n${blockers}`);
82
+ }
83
+
84
+ const continuity = extractSection(state, 'Session Continuity');
85
+ if (continuity) {
86
+ parts.push(`\nLast Session:\n${continuity}`);
87
+ }
88
+ } else {
89
+ parts.push('\nNo STATE.md found. Run /pbr:begin to initialize or /pbr:status to check.');
90
+ }
91
+
92
+ // Check for .continue-here.md files
93
+ const phasesDir = path.join(planningDir, 'phases');
94
+ if (fs.existsSync(phasesDir)) {
95
+ const continueFiles = findContinueFiles(phasesDir);
96
+ if (continueFiles.length > 0) {
97
+ parts.push(`\nPaused work found: ${continueFiles.join(', ')}`);
98
+ parts.push('Run /pbr:resume to pick up where you left off.');
99
+ }
100
+ }
101
+
102
+ // Check for config and validate
103
+ const config = configLoad(planningDir);
104
+ if (config) {
105
+ parts.push(`\nConfig: depth=${config.depth || 'standard'}, mode=${config.mode || 'interactive'}`);
106
+
107
+ // Validate config against schema (reuse already-loaded config)
108
+ const schemaPath = path.join(__dirname, 'config-schema.json');
109
+ if (fs.existsSync(schemaPath)) {
110
+ const { configValidate } = require('./pbr-tools');
111
+ const validation = configValidate(config);
112
+ if (validation.warnings.length > 0) {
113
+ parts.push(`\nConfig warnings: ${validation.warnings.join('; ')}`);
114
+ }
115
+ if (validation.errors.length > 0) {
116
+ parts.push(`\nConfig errors: ${validation.errors.join('; ')}`);
117
+ }
118
+ }
119
+ }
120
+
121
+ // Check for quick notes
122
+ const projectNotesFile = path.join(planningDir, 'NOTES.md');
123
+ const globalNotesFile = path.join(os.homedir(), '.claude', 'notes.md');
124
+ const projectNoteCount = countNotes(projectNotesFile);
125
+ const globalNoteCount = countNotes(globalNotesFile);
126
+ if (projectNoteCount > 0 || globalNoteCount > 0) {
127
+ const noteParts = [];
128
+ if (projectNoteCount > 0) noteParts.push(`${projectNoteCount} project`);
129
+ if (globalNoteCount > 0) noteParts.push(`${globalNoteCount} global`);
130
+ parts.push(`\nNotes: ${noteParts.join(', ')}. \`/pbr:note list\` to review.`);
131
+ }
132
+
133
+ // Check ROADMAP/STATE sync (S>M-2)
134
+ const roadmapFile = path.join(planningDir, 'ROADMAP.md');
135
+ if (fs.existsSync(stateFile) && fs.existsSync(roadmapFile)) {
136
+ try {
137
+ const roadmap = fs.readFileSync(roadmapFile, 'utf8');
138
+ const state = fs.readFileSync(stateFile, 'utf8');
139
+
140
+ // Extract current phase from STATE.md
141
+ const phaseMatch = state.match(/Phase:\s*(\d+)\s+of\s+\d+/);
142
+ if (phaseMatch) {
143
+ const currentPhase = parseInt(phaseMatch[1], 10);
144
+ // Check if ROADMAP shows this phase as already verified/complete
145
+ const progressTable = roadmap.match(/## Progress[\s\S]*?\|[\s\S]*?(?=\n##|\s*$)/);
146
+ if (progressTable) {
147
+ const rows = progressTable[0].split('\n').filter(r => r.includes('|'));
148
+ for (const row of rows) {
149
+ const cols = row.split('|').map(c => c.trim()).filter(Boolean);
150
+ if (cols.length >= 4) {
151
+ const phaseNum = parseInt(cols[0], 10);
152
+ const status = cols[3] ? cols[3].toLowerCase() : '';
153
+ if (phaseNum === currentPhase && (status === 'verified' || status === 'complete')) {
154
+ parts.push(`\nWarning: STATE.md may be outdated — ROADMAP.md shows phase ${currentPhase} as ${status}.`);
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ } catch (_e) {
161
+ // Ignore parse errors
162
+ }
163
+ }
164
+
165
+ // Check for stale .auto-next signal (S>M-9)
166
+ const autoNextFile = path.join(planningDir, '.auto-next');
167
+ if (fs.existsSync(autoNextFile)) {
168
+ try {
169
+ const stats = fs.statSync(autoNextFile);
170
+ const ageMs = Date.now() - stats.mtimeMs;
171
+ const ageMinutes = Math.floor(ageMs / 60000);
172
+ if (ageMinutes > 10) {
173
+ parts.push(`\nWarning: Stale .auto-next signal found (${ageMinutes} minutes old). This may trigger an unexpected command. Consider deleting .planning/.auto-next.`);
174
+ logHook('progress-tracker', 'SessionStart', 'stale-auto-next', { ageMinutes });
175
+ }
176
+ } catch (_e) {
177
+ // Ignore errors
178
+ }
179
+ }
180
+
181
+ // Hook health summary from recent log entries
182
+ const hookHealth = getHookHealthSummary(planningDir);
183
+ if (hookHealth) {
184
+ parts.push(`\n${hookHealth}`);
185
+ }
186
+
187
+ parts.push('\nAvailable commands: /pbr:status, /pbr:plan, /pbr:build, /pbr:review, /pbr:help');
188
+
189
+ return parts.join('\n');
190
+ }
191
+
192
+ function extractSection(content, heading) {
193
+ const regex = new RegExp(`##\\s+${escapeRegex(heading)}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`);
194
+ const match = content.match(regex);
195
+ if (!match) return null;
196
+ const section = match[1].trim();
197
+ // Return first 5 lines max
198
+ return section.split('\n').slice(0, 5).join('\n');
199
+ }
200
+
201
+ function escapeRegex(str) {
202
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
203
+ }
204
+
205
+ function findContinueFiles(dir) {
206
+ const results = [];
207
+ try {
208
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
209
+ for (const entry of entries) {
210
+ const fullPath = path.join(dir, entry.name);
211
+ if (entry.isDirectory()) {
212
+ results.push(...findContinueFiles(fullPath));
213
+ } else if (entry.name.includes('.continue-here')) {
214
+ results.push(path.relative(dir, fullPath));
215
+ }
216
+ }
217
+ } catch (_e) {
218
+ // Ignore permission errors
219
+ }
220
+ return results;
221
+ }
222
+
223
+ function countNotes(filePath) {
224
+ try {
225
+ if (!fs.existsSync(filePath)) return 0;
226
+ const content = fs.readFileSync(filePath, 'utf8');
227
+ const lines = content.split('\n');
228
+ return lines.filter(l => /^- \[/.test(l) && !l.includes('[promoted]')).length;
229
+ } catch (_e) {
230
+ return 0;
231
+ }
232
+ }
233
+
234
+ const FAILURE_DECISIONS = /^(block|error|warn|warning|block-coauthor|block-sensitive|unlink-failed)$/;
235
+ const HOOK_HEALTH_MAX_ENTRIES = 50;
236
+
237
+ function getHookHealthSummary(planningDir) {
238
+ const logPath = path.join(planningDir, 'logs', 'hooks.jsonl');
239
+ try {
240
+ if (!fs.existsSync(logPath)) return null;
241
+ const content = fs.readFileSync(logPath, 'utf8').trim();
242
+ if (!content) return null;
243
+
244
+ const lines = content.split('\n');
245
+ // Take only the last N entries
246
+ const recent = lines.slice(-HOOK_HEALTH_MAX_ENTRIES);
247
+
248
+ const failuresByHook = {};
249
+ let totalFailures = 0;
250
+
251
+ for (const line of recent) {
252
+ try {
253
+ const entry = JSON.parse(line);
254
+ if (entry.decision && FAILURE_DECISIONS.test(entry.decision)) {
255
+ const hookName = entry.hook || 'unknown';
256
+ failuresByHook[hookName] = (failuresByHook[hookName] || 0) + 1;
257
+ totalFailures++;
258
+ }
259
+ } catch (_e) {
260
+ // Skip malformed lines
261
+ }
262
+ }
263
+
264
+ if (totalFailures === 0) return null;
265
+
266
+ // Sort hooks by failure count descending
267
+ const sorted = Object.entries(failuresByHook)
268
+ .sort((a, b) => b[1] - a[1])
269
+ .map(([hook, count]) => `${hook}: ${count}`)
270
+ .join(', ');
271
+
272
+ return `Hook health: ${totalFailures} failure${totalFailures !== 1 ? 's' : ''} in last ${recent.length} entries (${sorted})`;
273
+ } catch (_e) {
274
+ return null;
275
+ }
276
+ }
277
+
278
+ // Exported for testing
279
+ module.exports = { getHookHealthSummary, FAILURE_DECISIONS, HOOK_HEALTH_MAX_ENTRIES };
280
+
281
+ main();
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Hook wrapper that normalizes CLAUDE_PLUGIN_ROOT paths on Windows.
5
+ *
6
+ * Problem: On Windows with Git Bash, ${CLAUDE_PLUGIN_ROOT} expands to
7
+ * a MSYS-style path like /d/Repos/project/plugins/pbr. Node.js on Windows
8
+ * interprets this as D:\d\Repos\... (relative to drive root),
9
+ * causing MODULE_NOT_FOUND errors.
10
+ *
11
+ * Invocation from hooks.json (bootstrap pattern):
12
+ * "node -e \"require(require('path').resolve(
13
+ * (function(r){var m=r.match(/^\\/([a-zA-Z])\\/(.*)/);
14
+ * return m?m[1]+':'+m[2].replace(/\\//g,'\\\\'):r})
15
+ * (process.env.CLAUDE_PLUGIN_ROOT||''),
16
+ * 'scripts','run-hook.js'))(process.argv[1])\" <script> [args...]"
17
+ *
18
+ * Or directly (when CLAUDE_PLUGIN_ROOT resolves correctly):
19
+ * "node ${CLAUDE_PLUGIN_ROOT}/scripts/run-hook.js <script-name> [args...]"
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const path = require('path');
25
+
26
+ /**
27
+ * Fix MSYS-style paths on Windows.
28
+ * Converts /d/Repos/... to D:\Repos\...
29
+ */
30
+ function fixMsysPath(p) {
31
+ if (!p) return p;
32
+ const match = p.match(/^\/([a-zA-Z])\/(.*)/);
33
+ if (match) {
34
+ return match[1].toUpperCase() + ':\\' + match[2].replace(/\//g, '\\');
35
+ }
36
+ return p;
37
+ }
38
+
39
+ // Fix CLAUDE_PLUGIN_ROOT in environment
40
+ const pluginRoot = fixMsysPath(process.env.CLAUDE_PLUGIN_ROOT || '');
41
+
42
+ // When invoked via `node -e "..." scriptName`, process.argv is:
43
+ // [node, scriptName, ...extra]
44
+ // When invoked via `node run-hook.js scriptName`, process.argv is:
45
+ // [node, run-hook.js, scriptName, ...extra]
46
+ // Detect which case we're in:
47
+ const invokedViaEval = !process.argv[1] ||
48
+ !process.argv[1].endsWith('run-hook.js');
49
+
50
+ let scriptName, scriptArgs;
51
+ if (invokedViaEval) {
52
+ // Called as module: exports a function, or check argv[1]
53
+ scriptName = process.argv[1] || null;
54
+ scriptArgs = process.argv.slice(2);
55
+ } else {
56
+ // Called directly: node run-hook.js <script> [args...]
57
+ scriptName = process.argv[2] || null;
58
+ scriptArgs = process.argv.slice(3);
59
+ }
60
+
61
+ // When required as a module from -e bootstrap, export a runner function
62
+ if (typeof module !== 'undefined' && module.exports) {
63
+ module.exports = runScript;
64
+ }
65
+
66
+ // If we have a script name, run it immediately
67
+ if (scriptName) {
68
+ runScript(scriptName, scriptArgs);
69
+ }
70
+
71
+ function runScript(name, args) {
72
+ args = args || [];
73
+ // Try __dirname first, then pluginRoot
74
+ const candidates = [
75
+ path.resolve(__dirname, name),
76
+ pluginRoot ? path.resolve(pluginRoot, 'scripts', name) : null
77
+ ].filter(Boolean);
78
+
79
+ for (const candidate of candidates) {
80
+ try {
81
+ process.argv = [process.argv[0], candidate, ...args];
82
+ require(candidate);
83
+ return;
84
+ } catch (err) {
85
+ if (err.code !== 'MODULE_NOT_FOUND') throw err;
86
+ }
87
+ }
88
+
89
+ process.stderr.write(`run-hook: cannot find script: ${name}\n`);
90
+ process.stderr.write(` searched: ${candidates.join(', ')}\n`);
91
+ process.exit(1);
92
+ }