@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,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PostToolUse hook on Read: Tracks cumulative file reads per skill invocation.
5
+ *
6
+ * Maintains a session-scoped counter in .planning/.context-tracker.
7
+ * Warns when reads exceed thresholds (15 reads or 30k chars).
8
+ * Resets when .active-skill changes (new skill invocation).
9
+ *
10
+ * Exit codes:
11
+ * 0 = always (PostToolUse hook, advisory only)
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { logHook } = require('./hook-logger');
17
+
18
+ const READ_THRESHOLD = 20;
19
+ const CHAR_THRESHOLD = 30000;
20
+
21
+ function main() {
22
+ let input = '';
23
+
24
+ process.stdin.setEncoding('utf8');
25
+ process.stdin.on('data', (chunk) => { input += chunk; });
26
+ process.stdin.on('end', () => {
27
+ try {
28
+ const cwd = process.cwd();
29
+ const planningDir = path.join(cwd, '.planning');
30
+ if (!fs.existsSync(planningDir)) {
31
+ process.exit(0);
32
+ }
33
+
34
+ const data = JSON.parse(input);
35
+ const filePath = data.tool_input?.file_path || '';
36
+ if (!filePath) {
37
+ process.exit(0);
38
+ }
39
+
40
+ // Estimate chars read (use limit if provided, otherwise assume ~2000 lines × 40 chars avg)
41
+ const limit = data.tool_input?.limit;
42
+ const estimatedChars = limit ? limit * 40 : 80000;
43
+ // Use actual output length if available
44
+ const actualChars = data.tool_output ? String(data.tool_output).length : estimatedChars;
45
+
46
+ const trackerPath = path.join(planningDir, '.context-tracker');
47
+ const skillPath = path.join(planningDir, '.active-skill');
48
+
49
+ // Check if active skill changed (reset tracker)
50
+ const currentSkill = readFileSafe(skillPath);
51
+ let tracker = loadTracker(trackerPath);
52
+
53
+ if (tracker.skill !== currentSkill) {
54
+ tracker = { skill: currentSkill, reads: 0, total_chars: 0, files: [] };
55
+ }
56
+
57
+ // Update tracker
58
+ tracker.reads += 1;
59
+ tracker.total_chars += actualChars;
60
+ if (!tracker.files.includes(filePath)) {
61
+ tracker.files.push(filePath);
62
+ }
63
+
64
+ // Save tracker
65
+ try {
66
+ fs.writeFileSync(trackerPath, JSON.stringify(tracker), 'utf8');
67
+ } catch (_e) {
68
+ // Best-effort
69
+ }
70
+
71
+ // Check thresholds
72
+ if (tracker.reads >= READ_THRESHOLD || tracker.total_chars >= CHAR_THRESHOLD) {
73
+ const warnings = [];
74
+ if (tracker.reads >= READ_THRESHOLD) {
75
+ warnings.push(`${tracker.reads} file reads (threshold: ${READ_THRESHOLD})`);
76
+ }
77
+ if (tracker.total_chars >= CHAR_THRESHOLD) {
78
+ const kChars = Math.round(tracker.total_chars / 1000);
79
+ warnings.push(`~${kChars}k chars read (threshold: ${CHAR_THRESHOLD / 1000}k)`);
80
+ }
81
+
82
+ logHook('track-context-budget', 'PostToolUse', 'warn', {
83
+ reads: tracker.reads,
84
+ total_chars: tracker.total_chars,
85
+ unique_files: tracker.files.length,
86
+ });
87
+
88
+ const output = {
89
+ additionalContext: `[Context Budget Warning] ${warnings.join(', ')}. ${tracker.files.length} unique files read. Consider delegating remaining reads to a Task() subagent to protect orchestrator context.`
90
+ };
91
+ process.stdout.write(JSON.stringify(output));
92
+ }
93
+
94
+ process.exit(0);
95
+ } catch (_e) {
96
+ // Never block on tracking errors
97
+ process.exit(0);
98
+ }
99
+ });
100
+ }
101
+
102
+ function readFileSafe(filePath) {
103
+ try {
104
+ return fs.readFileSync(filePath, 'utf8').trim();
105
+ } catch (_e) {
106
+ return '';
107
+ }
108
+ }
109
+
110
+ function loadTracker(trackerPath) {
111
+ try {
112
+ const content = fs.readFileSync(trackerPath, 'utf8');
113
+ return JSON.parse(content);
114
+ } catch (_e) {
115
+ return { skill: '', reads: 0, total_chars: 0, files: [] };
116
+ }
117
+ }
118
+
119
+ main();
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PreToolUse hook: Validates git commit message format.
5
+ *
6
+ * Expected format: {type}({phase}-{plan}): {description}
7
+ * Valid types: feat, fix, refactor, test, docs, chore
8
+ *
9
+ * Also accepts:
10
+ * - Merge commits (starts with "Merge")
11
+ * - Quick task commits: {type}(quick-{NNN}): {description}
12
+ * - Planning doc commits: docs(planning): {description}
13
+ * - WIP commits: wip: {description} or wip({area}): {description}
14
+ *
15
+ * Exit codes:
16
+ * 0 = not a commit command or valid format
17
+ * 2 = invalid commit message format (blocks the tool)
18
+ */
19
+
20
+ const path = require('path');
21
+ const { execSync } = require('child_process');
22
+ const { logHook } = require('./hook-logger');
23
+ const { logEvent } = require('./event-logger');
24
+
25
+ const VALID_TYPES = ['feat', 'fix', 'refactor', 'test', 'docs', 'chore', 'wip'];
26
+
27
+ const SENSITIVE_PATTERNS = [
28
+ /^\.env$/, // .env exactly (not .env.example)
29
+ /\.env\.[^.]+$/, // .env.production, .env.local etc (but not .env.example)
30
+ /\.key$/i,
31
+ /\.pem$/i,
32
+ /\.pfx$/i,
33
+ /\.p12$/i,
34
+ /credential/i,
35
+ /secret/i,
36
+ ];
37
+
38
+ const SAFE_PATTERNS = [
39
+ /\.example$/i,
40
+ /\.template$/i,
41
+ /\.sample$/i,
42
+ /^tests?[\\/]/i,
43
+ ];
44
+
45
+ // Pattern: type(scope): description
46
+ // Scope can be: NN-MM (phase-plan), quick-NNN, planning, or any word
47
+ const COMMIT_PATTERN = /^(feat|fix|refactor|test|docs|chore|wip)(\([a-zA-Z0-9._-]+\))?:\s+.+/;
48
+
49
+ // Merge commits are always allowed
50
+ const MERGE_PATTERN = /^Merge\s/;
51
+
52
+ // AI co-author patterns to block
53
+ const AI_COAUTHOR_PATTERN = /Co-Authored-By:.*(?:Claude|Anthropic|noreply@anthropic\.com|OpenAI|Copilot|GPT|AI Assistant)/i;
54
+
55
+ function checkAiCoAuthorResult(command) {
56
+ if (AI_COAUTHOR_PATTERN.test(command)) {
57
+ logHook('validate-commit', 'PreToolUse', 'block-coauthor', { command: command.substring(0, 200) });
58
+ return {
59
+ output: {
60
+ decision: 'block',
61
+ reason: 'Commit blocked: contains AI co-author attribution.\n\nPlan-Build-Run commits must not include Co-Authored-By lines referencing AI tools (Claude, Copilot, GPT, etc.).\n\nRemove the Co-Authored-By line and try again.'
62
+ },
63
+ exitCode: 2
64
+ };
65
+ }
66
+ return null;
67
+ }
68
+
69
+ function checkSensitiveFilesResult() {
70
+ try {
71
+ const output = execSync('git diff --cached --name-only', { encoding: 'utf8' });
72
+ const files = output.trim().split('\n').filter(Boolean);
73
+
74
+ const matched = files.filter((file) => {
75
+ // Skip files matching safe patterns
76
+ if (SAFE_PATTERNS.some((pattern) => pattern.test(file))) return false;
77
+ // Check against sensitive patterns (test basename and full path)
78
+ const basename = path.basename(file);
79
+ return SENSITIVE_PATTERNS.some((pattern) => pattern.test(basename) || pattern.test(file));
80
+ });
81
+
82
+ if (matched.length > 0) {
83
+ logHook('validate-commit', 'PreToolUse', 'block-sensitive', { files: matched });
84
+ return {
85
+ output: {
86
+ decision: 'block',
87
+ reason: `Commit blocked: staged files may contain sensitive data.\n\nFiles: ${matched.join(', ')}\n\nRemove these files from staging with:\n git reset HEAD ${matched.join(' ')}\n\nIf these files are intentionally safe (e.g., test fixtures), rename them to include .example, .template, or .sample.`
88
+ },
89
+ exitCode: 2
90
+ };
91
+ }
92
+ } catch (_e) {
93
+ // Not in a git repo or git not available - silently continue
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * Check a parsed hook data object for commit validation issues.
100
+ * Returns { output, exitCode } if the command should be blocked, or null if allowed.
101
+ * Used by pre-bash-dispatch.js for consolidated hook execution.
102
+ */
103
+ function checkCommit(data) {
104
+ const command = data.tool_input?.command || '';
105
+
106
+ // Only validate git commit commands
107
+ if (!isGitCommit(command)) {
108
+ return null;
109
+ }
110
+
111
+ // Extract the commit message
112
+ const message = extractCommitMessage(command);
113
+ if (!message) {
114
+ // Could not parse message - let it through (might be --amend or other form)
115
+ logHook('validate-commit', 'PreToolUse', 'allow', { reason: 'unparseable message' });
116
+ return null;
117
+ }
118
+
119
+ // Validate format
120
+ if (MERGE_PATTERN.test(message)) {
121
+ logHook('validate-commit', 'PreToolUse', 'allow', { message, reason: 'merge commit' });
122
+ return null;
123
+ }
124
+
125
+ if (!COMMIT_PATTERN.test(message)) {
126
+ logHook('validate-commit', 'PreToolUse', 'block', { message });
127
+ logEvent('workflow', 'commit-validated', { message: message.substring(0, 80), status: 'block' });
128
+ return {
129
+ output: {
130
+ decision: 'block',
131
+ reason: `Invalid commit message format.\n\nExpected: {type}({scope}): {description}\nTypes: ${VALID_TYPES.join(', ')}\nExamples:\n feat(03-01): add user authentication\n fix(02-02): resolve database connection timeout\n docs(planning): update roadmap with phase 4\n wip: save progress on auth middleware\n\nGot: "${message}"`
132
+ },
133
+ exitCode: 2
134
+ };
135
+ }
136
+
137
+ // Valid format
138
+ logHook('validate-commit', 'PreToolUse', 'allow', { message });
139
+ logEvent('workflow', 'commit-validated', { message: message.substring(0, 80), status: 'allow' });
140
+
141
+ // Check AI co-author
142
+ const coAuthorResult = checkAiCoAuthorResult(command);
143
+ if (coAuthorResult) return coAuthorResult;
144
+
145
+ // Check sensitive files
146
+ const sensitiveResult = checkSensitiveFilesResult();
147
+ if (sensitiveResult) return sensitiveResult;
148
+
149
+ return null;
150
+ }
151
+
152
+ function main() {
153
+ let input = '';
154
+
155
+ process.stdin.setEncoding('utf8');
156
+ process.stdin.on('data', (chunk) => { input += chunk; });
157
+ process.stdin.on('end', () => {
158
+ try {
159
+ const data = JSON.parse(input);
160
+ const result = checkCommit(data);
161
+ if (result) {
162
+ process.stdout.write(JSON.stringify(result.output));
163
+ process.exit(result.exitCode);
164
+ }
165
+ process.exit(0);
166
+ } catch (_e) {
167
+ // Parse error - don't block
168
+ process.exit(0);
169
+ }
170
+ });
171
+ }
172
+
173
+ function isGitCommit(command) {
174
+ // Match: git commit anywhere in the command string
175
+ // Handles chained commands like "cd /dir && git commit ..." or "git add . && git commit ..."
176
+ const trimmed = command.trim();
177
+ return /\bgit\s+commit\b/.test(trimmed) && !trimmed.includes('--amend --no-edit');
178
+ }
179
+
180
+ function extractCommitMessage(command) {
181
+ // Try -m "message" or -m 'message'
182
+ const mFlagMatch = command.match(/-m\s+["']([^"']+)["']/);
183
+ if (mFlagMatch) return mFlagMatch[1];
184
+
185
+ // Try -m "message" with escaped quotes
186
+ const mFlagMatch2 = command.match(/-m\s+"([^"]+)"/);
187
+ if (mFlagMatch2) return mFlagMatch2[1];
188
+
189
+ // Try heredoc: -m "$(cat <<'EOF'\n...\nEOF\n)"
190
+ const heredocMatch = command.match(/<<'?EOF'?\s*\n([\s\S]*?)\nEOF/);
191
+ if (heredocMatch) {
192
+ // First line of heredoc is the commit message
193
+ return heredocMatch[1].trim().split('\n')[0].trim();
194
+ }
195
+
196
+ return null;
197
+ }
198
+
199
+ module.exports = { checkCommit };
200
+ if (require.main === module) { main(); }
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Validates the Plan-Build-Run plugin structure:
5
+ * - Every skill directory has SKILL.md
6
+ * - Every agent file has valid YAML frontmatter (name, description)
7
+ * - hooks.json references existing scripts
8
+ * - No broken relative links in markdown files
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const ROOT = path.resolve(__dirname, '..');
15
+ let errors = 0;
16
+ let warnings = 0;
17
+
18
+ function error(msg) {
19
+ console.error(`ERROR: ${msg}`);
20
+ errors++;
21
+ }
22
+
23
+ function warn(msg) {
24
+ console.warn(`WARN: ${msg}`);
25
+ warnings++;
26
+ }
27
+
28
+ function info(msg) {
29
+ console.log(`OK: ${msg}`);
30
+ }
31
+
32
+ // 1. Check plugin.json exists
33
+ const pluginJsonPath = path.join(ROOT, '.claude-plugin', 'plugin.json');
34
+ if (!fs.existsSync(pluginJsonPath)) {
35
+ error('.claude-plugin/plugin.json missing');
36
+ } else {
37
+ try {
38
+ const plugin = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
39
+ if (!plugin.name) error('plugin.json missing "name" field');
40
+ if (!plugin.version) error('plugin.json missing "version" field');
41
+ if (!plugin.description) error('plugin.json missing "description" field');
42
+ info(`Plugin: ${plugin.name} v${plugin.version}`);
43
+ } catch (e) {
44
+ error(`plugin.json is not valid JSON: ${e.message}`);
45
+ }
46
+ }
47
+
48
+ // 2. Check every skill directory has SKILL.md
49
+ const skillsDir = path.join(ROOT, 'skills');
50
+ if (fs.existsSync(skillsDir)) {
51
+ const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
52
+ .filter(d => d.isDirectory() && d.name !== 'shared');
53
+
54
+ for (const dir of skillDirs) {
55
+ const skillMd = path.join(skillsDir, dir.name, 'SKILL.md');
56
+ if (!fs.existsSync(skillMd)) {
57
+ error(`skills/${dir.name}/ missing SKILL.md`);
58
+ } else {
59
+ const content = fs.readFileSync(skillMd, 'utf8');
60
+ if (!content.startsWith('---')) {
61
+ error(`skills/${dir.name}/SKILL.md missing YAML frontmatter`);
62
+ } else {
63
+ const frontmatter = content.split('---')[1];
64
+ if (!frontmatter.includes('name:')) {
65
+ error(`skills/${dir.name}/SKILL.md frontmatter missing "name" field`);
66
+ }
67
+ if (!frontmatter.includes('description:')) {
68
+ error(`skills/${dir.name}/SKILL.md frontmatter missing "description" field`);
69
+ }
70
+ }
71
+ // Check: skills with Task in allowed-tools must have Context Budget section
72
+ const frontmatterBlock = content.split('---')[1] || '';
73
+ const hasTaskTool = /allowed-tools:.*Task/.test(frontmatterBlock);
74
+ if (hasTaskTool && !content.includes('## Context Budget')) {
75
+ warn(`skills/${dir.name}/SKILL.md has Task in allowed-tools but no "## Context Budget" section`);
76
+ }
77
+
78
+ info(`Skill: /pbr:${dir.name}`);
79
+ }
80
+ }
81
+ } else {
82
+ error('skills/ directory missing');
83
+ }
84
+
85
+ // 3. Check every agent file has valid frontmatter
86
+ const agentsDir = path.join(ROOT, 'agents');
87
+ if (fs.existsSync(agentsDir)) {
88
+ const agentFiles = fs.readdirSync(agentsDir)
89
+ .filter(f => f.endsWith('.md'));
90
+
91
+ for (const file of agentFiles) {
92
+ const content = fs.readFileSync(path.join(agentsDir, file), 'utf8');
93
+ if (!content.startsWith('---')) {
94
+ error(`agents/${file} missing YAML frontmatter`);
95
+ } else {
96
+ const frontmatter = content.split('---')[1];
97
+ if (!frontmatter.includes('name:')) {
98
+ error(`agents/${file} frontmatter missing "name" field`);
99
+ }
100
+ if (!frontmatter.includes('description:')) {
101
+ error(`agents/${file} frontmatter missing "description" field`);
102
+ }
103
+ const nameMatch = frontmatter.match(/name:\s*(.+)/);
104
+ info(`Agent: ${nameMatch ? nameMatch[1].trim() : file}`);
105
+ }
106
+ }
107
+ } else {
108
+ error('agents/ directory missing');
109
+ }
110
+
111
+ // 4. Check context files have valid structure
112
+ const contextsDir = path.join(ROOT, 'contexts');
113
+ if (fs.existsSync(contextsDir)) {
114
+ const contextFiles = fs.readdirSync(contextsDir)
115
+ .filter(f => f.endsWith('.md'));
116
+
117
+ for (const file of contextFiles) {
118
+ const content = fs.readFileSync(path.join(contextsDir, file), 'utf8');
119
+ if (!content.startsWith('#')) {
120
+ warn(`contexts/${file} should start with a heading`);
121
+ }
122
+ const name = file.replace('.md', '');
123
+ info(`Context: ${name}`);
124
+ }
125
+ } else {
126
+ warn('contexts/ directory not found (contexts are optional)');
127
+ }
128
+
129
+ // 5. Check hooks.json references existing scripts
130
+ const hooksJsonPath = path.join(ROOT, 'hooks', 'hooks.json');
131
+ if (fs.existsSync(hooksJsonPath)) {
132
+ try {
133
+ const hooksFile = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
134
+
135
+ // Plugin hooks format: { hooks: { EventName: [ { matcher?, hooks: [ { type, command } ] } ] } }
136
+ const hooksObj = hooksFile.hooks || {};
137
+ for (const eventName of Object.keys(hooksObj)) {
138
+ const matcherGroups = hooksObj[eventName];
139
+ if (!Array.isArray(matcherGroups)) continue;
140
+ for (const group of matcherGroups) {
141
+ const handlers = group.hooks || [];
142
+ for (const handler of handlers) {
143
+ if (handler.command) {
144
+ const cmd = handler.command.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, ROOT);
145
+ const parts = cmd.split(' ');
146
+ const scriptPart = parts.find(p => p.endsWith('.js'));
147
+ if (scriptPart) {
148
+ const scriptPath = scriptPart.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, ROOT);
149
+ const resolvedPath = path.isAbsolute(scriptPath) ? scriptPath : path.join(ROOT, scriptPath);
150
+ if (!fs.existsSync(resolvedPath)) {
151
+ error(`hooks.json references missing script: ${scriptPart}`);
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+ info('hooks.json validated');
159
+ } catch (e) {
160
+ error(`hooks.json is not valid JSON: ${e.message}`);
161
+ }
162
+ } else {
163
+ warn('hooks/hooks.json not found (hooks are optional)');
164
+ }
165
+
166
+ // 6. Summary
167
+ console.log('\n---');
168
+ console.log(`Validation complete: ${errors} errors, ${warnings} warnings`);
169
+
170
+ if (errors > 0) {
171
+ process.exit(1);
172
+ }