@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,80 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Stop hook: Auto-continue via signal files.
5
+ *
6
+ * When enabled (features.auto_continue: true in config.json),
7
+ * reads .planning/.auto-next signal file on session stop.
8
+ * If present, reads the next command and injects it.
9
+ * Signal file is ONE-SHOT: read and delete to prevent infinite loops.
10
+ *
11
+ * Hard stops (signal file NOT written):
12
+ * - Milestone completion
13
+ * - human_needed flag set
14
+ * - Execution errors
15
+ * - Gap closure attempted 3+ times
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { logHook } = require('./hook-logger');
21
+ const { configLoad } = require('./pbr-tools');
22
+
23
+ function main() {
24
+ try {
25
+ const cwd = process.cwd();
26
+ const planningDir = path.join(cwd, '.planning');
27
+ const signalPath = path.join(planningDir, '.auto-next');
28
+
29
+ // Check if auto-continue is enabled
30
+ const config = configLoad(planningDir);
31
+ if (!config || !config.features || !config.features.auto_continue) {
32
+ process.exit(0);
33
+ }
34
+
35
+ // Check for signal file
36
+ if (!fs.existsSync(signalPath)) {
37
+ logHook('auto-continue', 'Stop', 'no-signal', {});
38
+ process.exit(0);
39
+ }
40
+
41
+ // Read and DELETE the signal file (one-shot)
42
+ const nextCommand = fs.readFileSync(signalPath, 'utf8').trim();
43
+ // Retry unlink with exponential backoff for Windows file locking (antivirus/indexer)
44
+ for (let attempt = 0; attempt < 3; attempt++) {
45
+ try {
46
+ fs.unlinkSync(signalPath);
47
+ break;
48
+ } catch (unlinkErr) {
49
+ if (attempt === 2) {
50
+ logHook('auto-continue', 'Stop', 'unlink-failed', { error: unlinkErr.message });
51
+ } else {
52
+ // Exponential backoff: 100ms, 200ms
53
+ const delay = 100 * Math.pow(2, attempt);
54
+ const start = Date.now();
55
+ while (Date.now() - start < delay) { /* busy-wait */ }
56
+ }
57
+ }
58
+ }
59
+
60
+ if (!nextCommand) {
61
+ logHook('auto-continue', 'Stop', 'empty-signal', {});
62
+ process.exit(0);
63
+ }
64
+
65
+ logHook('auto-continue', 'Stop', 'continue', { next: nextCommand });
66
+
67
+ // Output the next command for Claude Code to execute
68
+ const output = {
69
+ message: `Auto-continuing with: ${nextCommand}`,
70
+ command: nextCommand
71
+ };
72
+ process.stdout.write(JSON.stringify(output));
73
+ process.exit(0);
74
+ } catch (_e) {
75
+ // Don't block on errors
76
+ process.exit(0);
77
+ }
78
+ }
79
+
80
+ main();
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PreToolUse hook: Blocks dangerous Bash commands that could destroy
5
+ * planning state or repository integrity.
6
+ *
7
+ * Patterns blocked (exit 2):
8
+ * - rm -rf .planning (or any rm that targets .planning/)
9
+ * - git reset --hard
10
+ * - git push --force / -f to main/master
11
+ * - git clean -fd / -fxd (removes untracked files including .planning/)
12
+ *
13
+ * Patterns warned (exit 0, additionalContext):
14
+ * - Large rm operations (rm -rf on project directories)
15
+ * - git checkout -- . (discards all unstaged changes)
16
+ *
17
+ * Exit codes:
18
+ * 0 = not a dangerous command, or a warning-only match
19
+ * 2 = blocked (destructive command detected)
20
+ */
21
+
22
+ const { logHook } = require('./hook-logger');
23
+
24
+ // Commands that are outright blocked
25
+ const BLOCK_PATTERNS = [
26
+ {
27
+ pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*r[a-zA-Z]*)\s+.*\.planning\b/,
28
+ reason: 'rm -rf targeting .planning/ directory — this would destroy all project state.'
29
+ },
30
+ {
31
+ pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*r[a-zA-Z]*)\s+.*\.planning[/\\]/,
32
+ reason: 'rm -rf targeting files inside .planning/ — this would destroy project state.'
33
+ },
34
+ {
35
+ pattern: /\bgit\s+reset\s+--hard\b/,
36
+ reason: 'git reset --hard discards all uncommitted changes. Use git stash or git checkout for specific files instead.'
37
+ },
38
+ {
39
+ pattern: /\bgit\s+push\s+.*(-f|--force)\b.*\b(main|master)\b/,
40
+ reason: 'Force-pushing to main/master can destroy shared history. This is almost never what you want.'
41
+ },
42
+ {
43
+ pattern: /\bgit\s+push\s+.*\b(main|master)\b.*(-f|--force)\b/,
44
+ reason: 'Force-pushing to main/master can destroy shared history. This is almost never what you want.'
45
+ },
46
+ {
47
+ pattern: /\bgit\s+clean\s+(-[a-zA-Z]*f[a-zA-Z]*d|-[a-zA-Z]*d[a-zA-Z]*f)\b/,
48
+ reason: 'git clean -fd removes untracked files including .planning/ contents. Use specific file paths instead.'
49
+ }
50
+ ];
51
+
52
+ // Commands that produce warnings but are not blocked
53
+ const WARN_PATTERNS = [
54
+ {
55
+ pattern: /\bgit\s+checkout\s+--\s+\.\s*$/,
56
+ message: 'git checkout -- . discards ALL unstaged changes. Consider targeting specific files.'
57
+ },
58
+ {
59
+ pattern: /\bgit\s+push\s+.*(-f|--force)\b/,
60
+ message: 'Force-pushing can overwrite remote history. Ensure this is intentional.'
61
+ }
62
+ ];
63
+
64
+ /**
65
+ * Check a parsed hook data object for dangerous commands.
66
+ * Returns { output, exitCode } if the command should be blocked/warned, or null if allowed.
67
+ * Used by pre-bash-dispatch.js for consolidated hook execution.
68
+ */
69
+ function checkDangerous(data) {
70
+ const command = data.tool_input?.command || '';
71
+
72
+ // Skip empty commands
73
+ if (!command.trim()) {
74
+ return null;
75
+ }
76
+
77
+ // Check block patterns
78
+ for (const { pattern, reason } of BLOCK_PATTERNS) {
79
+ if (pattern.test(command)) {
80
+ logHook('check-dangerous-commands', 'PreToolUse', 'block', {
81
+ command: command.substring(0, 200),
82
+ reason
83
+ });
84
+ return {
85
+ output: {
86
+ decision: 'block',
87
+ reason: `Dangerous command blocked.\n\n${reason}\n\nCommand: ${command.substring(0, 150)}`
88
+ },
89
+ exitCode: 2
90
+ };
91
+ }
92
+ }
93
+
94
+ // Check warn patterns
95
+ for (const { pattern, message } of WARN_PATTERNS) {
96
+ if (pattern.test(command)) {
97
+ logHook('check-dangerous-commands', 'PreToolUse', 'warn', {
98
+ command: command.substring(0, 200),
99
+ warning: message
100
+ });
101
+ return {
102
+ output: {
103
+ additionalContext: `Warning: ${message}`
104
+ },
105
+ exitCode: 0
106
+ };
107
+ }
108
+ }
109
+
110
+ // No match — allow
111
+ return null;
112
+ }
113
+
114
+ function main() {
115
+ let input = '';
116
+
117
+ process.stdin.setEncoding('utf8');
118
+ process.stdin.on('data', (chunk) => { input += chunk; });
119
+ process.stdin.on('end', () => {
120
+ try {
121
+ const data = JSON.parse(input);
122
+ const result = checkDangerous(data);
123
+ if (result) {
124
+ process.stdout.write(JSON.stringify(result.output));
125
+ process.exit(result.exitCode);
126
+ }
127
+ process.exit(0);
128
+ } catch (_e) {
129
+ // Parse error — don't block
130
+ process.exit(0);
131
+ }
132
+ });
133
+ }
134
+
135
+ module.exports = { BLOCK_PATTERNS, WARN_PATTERNS, checkDangerous };
136
+ if (require.main === module) { main(); }
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PreToolUse check: Blocks creation of new .md/.txt documentation files
5
+ * outside a known allowlist, preventing doc sprawl during builds.
6
+ *
7
+ * Opt-in via .planning/config.json: { "hooks": { "blockDocSprawl": true } }
8
+ *
9
+ * Allowlist:
10
+ * - README.md, CLAUDE.md, CONTRIBUTING.md, CHANGELOG.md, LICENSE.md, LICENSE, LICENSE.txt
11
+ * - Any file under .planning/, .claude/, node_modules/, .git/
12
+ * - Any file that already exists (edits to existing docs are always allowed)
13
+ *
14
+ * Called by pre-write-dispatch.js — not wired directly in hooks.json.
15
+ *
16
+ * Exit codes (when standalone):
17
+ * 0 = pass (allowed)
18
+ * 2 = block (not on allowlist)
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { logHook } = require('./hook-logger');
24
+
25
+ const ALLOWED_DOC_BASENAMES = new Set([
26
+ 'readme.md',
27
+ 'claude.md',
28
+ 'contributing.md',
29
+ 'changelog.md',
30
+ 'license.md',
31
+ 'license',
32
+ 'license.txt',
33
+ ]);
34
+
35
+ const ALLOWED_DIR_SEGMENTS = [
36
+ '.planning',
37
+ '.claude',
38
+ 'node_modules',
39
+ '.git',
40
+ ];
41
+
42
+ const DOC_EXTENSIONS = new Set(['.md', '.txt']);
43
+
44
+ /**
45
+ * Check if a Write operation would create a disallowed documentation file.
46
+ * @param {Object} data - Parsed hook input (tool_input, etc.)
47
+ * @param {string} [cwd] - Working directory override (defaults to process.cwd())
48
+ * @returns {null|{exitCode: number, output: Object}} null if allowed, block result otherwise
49
+ */
50
+ function checkDocSprawl(data, cwd) {
51
+ const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
52
+ if (!filePath) return null;
53
+
54
+ const ext = path.extname(filePath).toLowerCase();
55
+ if (!DOC_EXTENSIONS.has(ext)) return null;
56
+
57
+ // Only block NEW file creation — existing docs can always be edited
58
+ if (fs.existsSync(filePath)) return null;
59
+
60
+ // Check config — disabled by default
61
+ const effectiveCwd = cwd || process.cwd();
62
+ if (!isBlockDocSprawlEnabled(effectiveCwd)) return null;
63
+
64
+ // Check basename allowlist
65
+ const basename = path.basename(filePath).toLowerCase();
66
+ if (ALLOWED_DOC_BASENAMES.has(basename)) return null;
67
+
68
+ // Check if file is in an allowed directory
69
+ const normalized = filePath.replace(/\\/g, '/');
70
+ for (const seg of ALLOWED_DIR_SEGMENTS) {
71
+ if (normalized.includes(`/${seg}/`)) return null;
72
+ }
73
+
74
+ logHook('check-doc-sprawl', 'PreToolUse', 'block', {
75
+ file: path.basename(filePath),
76
+ ext
77
+ });
78
+
79
+ return {
80
+ exitCode: 2,
81
+ output: {
82
+ decision: 'block',
83
+ reason: `[Doc Sprawl] Blocked creation of ${path.basename(filePath)}. ` +
84
+ 'Only known docs (README.md, CLAUDE.md, CONTRIBUTING.md, CHANGELOG.md, LICENSE.md) ' +
85
+ 'and .planning/ files are allowed when blockDocSprawl is enabled. ' +
86
+ 'Add content to an existing file instead, or disable hooks.blockDocSprawl in config.'
87
+ }
88
+ };
89
+ }
90
+
91
+ function isBlockDocSprawlEnabled(cwd) {
92
+ try {
93
+ const configPath = path.join(cwd, '.planning', 'config.json');
94
+ if (!fs.existsSync(configPath)) return false;
95
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
96
+ return !!config.hooks?.blockDocSprawl;
97
+ } catch (_e) {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ module.exports = { checkDocSprawl, isBlockDocSprawlEnabled, ALLOWED_DOC_BASENAMES, ALLOWED_DIR_SEGMENTS, DOC_EXTENSIONS };
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+
3
+ // PreToolUse hook (Write|Edit): Warns or blocks when editing files
4
+ // outside the current active phase.
5
+ //
6
+ // Reads STATE.md for current phase number. If the file being written
7
+ // is under .planning/phases/NN-<slug>/ and NN does not match the
8
+ // current phase, issues a warning or blocks depending on config.
9
+ //
10
+ // Config: safety.enforce_phase_boundaries
11
+ // - true = block cross-phase writes (exit 2)
12
+ // - false = warn only (default)
13
+ // - absent = warn only
14
+ //
15
+ // Files outside .planning/phases/ are always allowed (source code,
16
+ // config files, etc.).
17
+ //
18
+ // Exit codes:
19
+ // 0 = allowed or not applicable
20
+ // 2 = blocked (only when enforce_phase_boundaries is true)
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const { logHook } = require('./hook-logger');
25
+ const { logEvent } = require('./event-logger');
26
+
27
+ function main() {
28
+ let input = '';
29
+
30
+ process.stdin.setEncoding('utf8');
31
+ process.stdin.on('data', (chunk) => { input += chunk; });
32
+ process.stdin.on('end', () => {
33
+ try {
34
+ const data = JSON.parse(input);
35
+ const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
36
+
37
+ if (!filePath) {
38
+ process.exit(0);
39
+ }
40
+
41
+ const cwd = process.cwd();
42
+ const planningDir = path.join(cwd, '.planning');
43
+
44
+ // Check if the file is under .planning/phases/ using marker matching
45
+ // instead of absolute path comparison (avoids macOS /var -> /private/var symlink issues)
46
+ const normalizedPath = filePath.replace(/\\/g, '/');
47
+ const phasesMarker = '.planning/phases/';
48
+ const markerIdx = normalizedPath.indexOf(phasesMarker);
49
+
50
+ if (markerIdx === -1) {
51
+ process.exit(0);
52
+ }
53
+
54
+ // Extract phase number from path after the marker
55
+ // Path pattern: .planning/phases/NN-slug/...
56
+ const afterMarker = normalizedPath.substring(markerIdx + phasesMarker.length);
57
+ const phaseMatch = afterMarker.match(/^(\d+)-/);
58
+ if (!phaseMatch) {
59
+ process.exit(0);
60
+ }
61
+ const filePhase = parseInt(phaseMatch[1], 10);
62
+
63
+ // Get current phase from STATE.md
64
+ const stateFile = path.join(planningDir, 'STATE.md');
65
+ if (!fs.existsSync(stateFile)) {
66
+ process.exit(0);
67
+ }
68
+
69
+ const state = fs.readFileSync(stateFile, 'utf8');
70
+ const currentPhaseMatch = state.match(/Phase:\s*(\d+)\s+of\s+\d+/);
71
+ if (!currentPhaseMatch) {
72
+ process.exit(0);
73
+ }
74
+ const currentPhase = parseInt(currentPhaseMatch[1], 10);
75
+
76
+ // Same phase — always allowed
77
+ if (filePhase === currentPhase) {
78
+ process.exit(0);
79
+ }
80
+
81
+ // Cross-phase write detected — check config
82
+ const enforce = getEnforceSetting(planningDir);
83
+
84
+ logHook('check-phase-boundary', 'PreToolUse', enforce ? 'block' : 'warn', {
85
+ filePhase,
86
+ currentPhase,
87
+ file: path.basename(filePath)
88
+ });
89
+ logEvent('workflow', 'phase-boundary', {
90
+ filePhase,
91
+ currentPhase,
92
+ file: path.basename(filePath),
93
+ action: enforce ? 'block' : 'warn'
94
+ });
95
+
96
+ if (enforce) {
97
+ const output = {
98
+ decision: 'block',
99
+ reason: `Cross-phase write blocked: editing phase ${filePhase} file but current phase is ${currentPhase}.\n\nFile: ${filePath}\n\nIf this is intentional, either:\n 1. Update STATE.md to reflect the correct phase\n 2. Set safety.enforce_phase_boundaries: false in config.json`
100
+ };
101
+ process.stdout.write(JSON.stringify(output));
102
+ process.exit(2);
103
+ } else {
104
+ const output = {
105
+ hookSpecificOutput: {
106
+ hookEventName: 'PreToolUse',
107
+ additionalContext: `Warning: editing phase ${filePhase} file but current phase is ${currentPhase}. Ensure this cross-phase edit is intentional.`
108
+ }
109
+ };
110
+ process.stdout.write(JSON.stringify(output));
111
+ }
112
+
113
+ process.exit(0);
114
+ } catch (_e) {
115
+ // Don't block on errors
116
+ process.exit(0);
117
+ }
118
+ });
119
+ }
120
+
121
+ function getEnforceSetting(planningDir) {
122
+ const configFile = path.join(planningDir, 'config.json');
123
+ if (!fs.existsSync(configFile)) return false;
124
+
125
+ try {
126
+ const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
127
+ return !!(config.safety && config.safety.enforce_phase_boundaries);
128
+ } catch (_e) {
129
+ return false;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Core boundary check logic for use by dispatchers.
135
+ * @param {Object} data - Parsed hook input (tool_input, etc.)
136
+ * @returns {null|{exitCode: number, output: Object}} null if pass, result otherwise
137
+ */
138
+ function checkBoundary(data) {
139
+ const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
140
+ if (!filePath) return null;
141
+
142
+ const cwd = process.cwd();
143
+ const planningDir = path.join(cwd, '.planning');
144
+
145
+ const normalizedPath = filePath.replace(/\\/g, '/');
146
+ const phasesMarker = '.planning/phases/';
147
+ const markerIdx = normalizedPath.indexOf(phasesMarker);
148
+ if (markerIdx === -1) return null;
149
+
150
+ const afterMarker = normalizedPath.substring(markerIdx + phasesMarker.length);
151
+ const phaseMatch = afterMarker.match(/^(\d+)-/);
152
+ if (!phaseMatch) return null;
153
+ const filePhase = parseInt(phaseMatch[1], 10);
154
+
155
+ const stateFile = path.join(planningDir, 'STATE.md');
156
+ if (!fs.existsSync(stateFile)) return null;
157
+
158
+ const state = fs.readFileSync(stateFile, 'utf8');
159
+ const currentPhaseMatch = state.match(/Phase:\s*(\d+)\s+of\s+\d+/);
160
+ if (!currentPhaseMatch) return null;
161
+ const currentPhase = parseInt(currentPhaseMatch[1], 10);
162
+
163
+ if (filePhase === currentPhase) return null;
164
+
165
+ const enforce = getEnforceSetting(planningDir);
166
+
167
+ logHook('check-phase-boundary', 'PreToolUse', enforce ? 'block' : 'warn', {
168
+ filePhase, currentPhase, file: path.basename(filePath)
169
+ });
170
+ logEvent('workflow', 'phase-boundary', {
171
+ filePhase, currentPhase, file: path.basename(filePath), action: enforce ? 'block' : 'warn'
172
+ });
173
+
174
+ if (enforce) {
175
+ return {
176
+ exitCode: 2,
177
+ output: {
178
+ decision: 'block',
179
+ reason: `Cross-phase write blocked: editing phase ${filePhase} file but current phase is ${currentPhase}.\n\nFile: ${filePath}\n\nIf this is intentional, either:\n 1. Update STATE.md to reflect the correct phase\n 2. Set safety.enforce_phase_boundaries: false in config.json`
180
+ }
181
+ };
182
+ }
183
+
184
+ return {
185
+ exitCode: 0,
186
+ output: {
187
+ hookSpecificOutput: {
188
+ hookEventName: 'PreToolUse',
189
+ additionalContext: `Warning: editing phase ${filePhase} file but current phase is ${currentPhase}. Ensure this cross-phase edit is intentional.`
190
+ }
191
+ }
192
+ };
193
+ }
194
+
195
+ module.exports = { getEnforceSetting, checkBoundary };
196
+ if (require.main === module) { main(); }