@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.
- package/CHANGELOG.md +56 -0
- package/CLAUDE.md +149 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/dashboard/bin/cli.js +25 -0
- package/dashboard/package.json +34 -0
- package/dashboard/public/.gitkeep +0 -0
- package/dashboard/public/css/layout.css +406 -0
- package/dashboard/public/css/status-colors.css +98 -0
- package/dashboard/public/js/htmx-title.js +5 -0
- package/dashboard/public/js/sidebar-toggle.js +20 -0
- package/dashboard/src/app.js +78 -0
- package/dashboard/src/middleware/errorHandler.js +52 -0
- package/dashboard/src/middleware/notFoundHandler.js +9 -0
- package/dashboard/src/repositories/planning.repository.js +128 -0
- package/dashboard/src/routes/events.routes.js +40 -0
- package/dashboard/src/routes/index.routes.js +31 -0
- package/dashboard/src/routes/pages.routes.js +195 -0
- package/dashboard/src/server.js +42 -0
- package/dashboard/src/services/dashboard.service.js +222 -0
- package/dashboard/src/services/phase.service.js +167 -0
- package/dashboard/src/services/project.service.js +57 -0
- package/dashboard/src/services/roadmap.service.js +171 -0
- package/dashboard/src/services/sse.service.js +58 -0
- package/dashboard/src/services/todo.service.js +254 -0
- package/dashboard/src/services/watcher.service.js +48 -0
- package/dashboard/src/views/coming-soon.ejs +11 -0
- package/dashboard/src/views/error.ejs +13 -0
- package/dashboard/src/views/index.ejs +5 -0
- package/dashboard/src/views/layout.ejs +1 -0
- package/dashboard/src/views/partials/dashboard-content.ejs +77 -0
- package/dashboard/src/views/partials/footer.ejs +3 -0
- package/dashboard/src/views/partials/head.ejs +21 -0
- package/dashboard/src/views/partials/header.ejs +12 -0
- package/dashboard/src/views/partials/layout-bottom.ejs +15 -0
- package/dashboard/src/views/partials/layout-top.ejs +8 -0
- package/dashboard/src/views/partials/phase-content.ejs +181 -0
- package/dashboard/src/views/partials/phases-content.ejs +117 -0
- package/dashboard/src/views/partials/roadmap-content.ejs +142 -0
- package/dashboard/src/views/partials/sidebar.ejs +38 -0
- package/dashboard/src/views/partials/todo-create-content.ejs +53 -0
- package/dashboard/src/views/partials/todo-detail-content.ejs +38 -0
- package/dashboard/src/views/partials/todos-content.ejs +53 -0
- package/dashboard/src/views/phase-detail.ejs +5 -0
- package/dashboard/src/views/phases.ejs +5 -0
- package/dashboard/src/views/roadmap.ejs +5 -0
- package/dashboard/src/views/todo-create.ejs +5 -0
- package/dashboard/src/views/todo-detail.ejs +5 -0
- package/dashboard/src/views/todos.ejs +5 -0
- package/package.json +57 -0
- package/plugins/pbr/.claude-plugin/plugin.json +13 -0
- package/plugins/pbr/UI-CONSISTENCY-GAPS.md +61 -0
- package/plugins/pbr/agents/codebase-mapper.md +271 -0
- package/plugins/pbr/agents/debugger.md +281 -0
- package/plugins/pbr/agents/executor.md +407 -0
- package/plugins/pbr/agents/general.md +164 -0
- package/plugins/pbr/agents/integration-checker.md +141 -0
- package/plugins/pbr/agents/plan-checker.md +280 -0
- package/plugins/pbr/agents/planner.md +358 -0
- package/plugins/pbr/agents/researcher.md +363 -0
- package/plugins/pbr/agents/synthesizer.md +230 -0
- package/plugins/pbr/agents/verifier.md +454 -0
- package/plugins/pbr/commands/begin.md +5 -0
- package/plugins/pbr/commands/build.md +5 -0
- package/plugins/pbr/commands/config.md +5 -0
- package/plugins/pbr/commands/continue.md +5 -0
- package/plugins/pbr/commands/debug.md +5 -0
- package/plugins/pbr/commands/discuss.md +5 -0
- package/plugins/pbr/commands/explore.md +5 -0
- package/plugins/pbr/commands/health.md +5 -0
- package/plugins/pbr/commands/help.md +5 -0
- package/plugins/pbr/commands/import.md +5 -0
- package/plugins/pbr/commands/milestone.md +5 -0
- package/plugins/pbr/commands/note.md +5 -0
- package/plugins/pbr/commands/pause.md +5 -0
- package/plugins/pbr/commands/plan.md +5 -0
- package/plugins/pbr/commands/quick.md +5 -0
- package/plugins/pbr/commands/resume.md +5 -0
- package/plugins/pbr/commands/review.md +5 -0
- package/plugins/pbr/commands/scan.md +5 -0
- package/plugins/pbr/commands/setup.md +5 -0
- package/plugins/pbr/commands/status.md +5 -0
- package/plugins/pbr/commands/todo.md +5 -0
- package/plugins/pbr/contexts/dev.md +27 -0
- package/plugins/pbr/contexts/research.md +28 -0
- package/plugins/pbr/contexts/review.md +36 -0
- package/plugins/pbr/hooks/hooks.json +183 -0
- package/plugins/pbr/references/agent-anti-patterns.md +24 -0
- package/plugins/pbr/references/agent-interactions.md +134 -0
- package/plugins/pbr/references/agent-teams.md +54 -0
- package/plugins/pbr/references/checkpoints.md +157 -0
- package/plugins/pbr/references/common-bug-patterns.md +13 -0
- package/plugins/pbr/references/continuation-format.md +212 -0
- package/plugins/pbr/references/deviation-rules.md +112 -0
- package/plugins/pbr/references/git-integration.md +226 -0
- package/plugins/pbr/references/integration-patterns.md +117 -0
- package/plugins/pbr/references/model-profiles.md +99 -0
- package/plugins/pbr/references/model-selection.md +31 -0
- package/plugins/pbr/references/pbr-rules.md +193 -0
- package/plugins/pbr/references/plan-authoring.md +181 -0
- package/plugins/pbr/references/plan-format.md +283 -0
- package/plugins/pbr/references/planning-config.md +213 -0
- package/plugins/pbr/references/questioning.md +214 -0
- package/plugins/pbr/references/reading-verification.md +127 -0
- package/plugins/pbr/references/stub-patterns.md +160 -0
- package/plugins/pbr/references/subagent-coordination.md +119 -0
- package/plugins/pbr/references/ui-formatting.md +399 -0
- package/plugins/pbr/references/verification-patterns.md +198 -0
- package/plugins/pbr/references/wave-execution.md +95 -0
- package/plugins/pbr/scripts/auto-continue.js +80 -0
- package/plugins/pbr/scripts/check-dangerous-commands.js +136 -0
- package/plugins/pbr/scripts/check-doc-sprawl.js +102 -0
- package/plugins/pbr/scripts/check-phase-boundary.js +196 -0
- package/plugins/pbr/scripts/check-plan-format.js +270 -0
- package/plugins/pbr/scripts/check-roadmap-sync.js +252 -0
- package/plugins/pbr/scripts/check-skill-workflow.js +262 -0
- package/plugins/pbr/scripts/check-state-sync.js +476 -0
- package/plugins/pbr/scripts/check-subagent-output.js +144 -0
- package/plugins/pbr/scripts/config-schema.json +251 -0
- package/plugins/pbr/scripts/context-budget-check.js +287 -0
- package/plugins/pbr/scripts/event-handler.js +151 -0
- package/plugins/pbr/scripts/event-logger.js +92 -0
- package/plugins/pbr/scripts/hook-logger.js +76 -0
- package/plugins/pbr/scripts/hooks-schema.json +79 -0
- package/plugins/pbr/scripts/log-subagent.js +152 -0
- package/plugins/pbr/scripts/log-tool-failure.js +88 -0
- package/plugins/pbr/scripts/pbr-tools.js +1301 -0
- package/plugins/pbr/scripts/post-write-dispatch.js +66 -0
- package/plugins/pbr/scripts/post-write-quality.js +207 -0
- package/plugins/pbr/scripts/pre-bash-dispatch.js +56 -0
- package/plugins/pbr/scripts/pre-write-dispatch.js +62 -0
- package/plugins/pbr/scripts/progress-tracker.js +228 -0
- package/plugins/pbr/scripts/session-cleanup.js +254 -0
- package/plugins/pbr/scripts/status-line.js +285 -0
- package/plugins/pbr/scripts/suggest-compact.js +119 -0
- package/plugins/pbr/scripts/task-completed.js +45 -0
- package/plugins/pbr/scripts/track-context-budget.js +119 -0
- package/plugins/pbr/scripts/validate-commit.js +200 -0
- package/plugins/pbr/scripts/validate-plugin-structure.js +172 -0
- package/plugins/pbr/skills/begin/SKILL.md +545 -0
- package/plugins/pbr/skills/begin/templates/PROJECT.md.tmpl +33 -0
- package/plugins/pbr/skills/begin/templates/REQUIREMENTS.md.tmpl +18 -0
- package/plugins/pbr/skills/begin/templates/STATE.md.tmpl +49 -0
- package/plugins/pbr/skills/begin/templates/config.json.tmpl +63 -0
- package/plugins/pbr/skills/begin/templates/researcher-prompt.md.tmpl +19 -0
- package/plugins/pbr/skills/begin/templates/roadmap-prompt.md.tmpl +30 -0
- package/plugins/pbr/skills/begin/templates/synthesis-prompt.md.tmpl +16 -0
- package/plugins/pbr/skills/build/SKILL.md +962 -0
- package/plugins/pbr/skills/config/SKILL.md +241 -0
- package/plugins/pbr/skills/continue/SKILL.md +127 -0
- package/plugins/pbr/skills/debug/SKILL.md +489 -0
- package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +16 -0
- package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +27 -0
- package/plugins/pbr/skills/discuss/SKILL.md +338 -0
- package/plugins/pbr/skills/discuss/templates/CONTEXT.md.tmpl +61 -0
- package/plugins/pbr/skills/discuss/templates/decision-categories.md +9 -0
- package/plugins/pbr/skills/explore/SKILL.md +362 -0
- package/plugins/pbr/skills/health/SKILL.md +186 -0
- package/plugins/pbr/skills/health/templates/check-pattern.md.tmpl +30 -0
- package/plugins/pbr/skills/health/templates/output-format.md.tmpl +63 -0
- package/plugins/pbr/skills/help/SKILL.md +140 -0
- package/plugins/pbr/skills/import/SKILL.md +490 -0
- package/plugins/pbr/skills/milestone/SKILL.md +673 -0
- package/plugins/pbr/skills/milestone/templates/audit-report.md.tmpl +48 -0
- package/plugins/pbr/skills/milestone/templates/stats-file.md.tmpl +30 -0
- package/plugins/pbr/skills/note/SKILL.md +212 -0
- package/plugins/pbr/skills/pause/SKILL.md +235 -0
- package/plugins/pbr/skills/pause/templates/continue-here.md.tmpl +71 -0
- package/plugins/pbr/skills/plan/SKILL.md +628 -0
- package/plugins/pbr/skills/plan/decimal-phase-calc.md +98 -0
- package/plugins/pbr/skills/plan/templates/checker-prompt.md.tmpl +21 -0
- package/plugins/pbr/skills/plan/templates/gap-closure-prompt.md.tmpl +32 -0
- package/plugins/pbr/skills/plan/templates/planner-prompt.md.tmpl +38 -0
- package/plugins/pbr/skills/plan/templates/researcher-prompt.md.tmpl +19 -0
- package/plugins/pbr/skills/plan/templates/revision-prompt.md.tmpl +23 -0
- package/plugins/pbr/skills/quick/SKILL.md +335 -0
- package/plugins/pbr/skills/resume/SKILL.md +388 -0
- package/plugins/pbr/skills/review/SKILL.md +652 -0
- package/plugins/pbr/skills/review/templates/debugger-prompt.md.tmpl +60 -0
- package/plugins/pbr/skills/review/templates/gap-planner-prompt.md.tmpl +40 -0
- package/plugins/pbr/skills/review/templates/verifier-prompt.md.tmpl +115 -0
- package/plugins/pbr/skills/scan/SKILL.md +269 -0
- package/plugins/pbr/skills/scan/templates/mapper-prompt.md.tmpl +201 -0
- package/plugins/pbr/skills/setup/SKILL.md +227 -0
- package/plugins/pbr/skills/shared/commit-planning-docs.md +35 -0
- package/plugins/pbr/skills/shared/config-loading.md +102 -0
- package/plugins/pbr/skills/shared/context-budget.md +40 -0
- package/plugins/pbr/skills/shared/context-loader-task.md +86 -0
- package/plugins/pbr/skills/shared/digest-select.md +79 -0
- package/plugins/pbr/skills/shared/domain-probes.md +125 -0
- package/plugins/pbr/skills/shared/error-reporting.md +79 -0
- package/plugins/pbr/skills/shared/gate-prompts.md +388 -0
- package/plugins/pbr/skills/shared/phase-argument-parsing.md +45 -0
- package/plugins/pbr/skills/shared/progress-display.md +53 -0
- package/plugins/pbr/skills/shared/revision-loop.md +81 -0
- package/plugins/pbr/skills/shared/state-loading.md +62 -0
- package/plugins/pbr/skills/shared/state-update.md +161 -0
- package/plugins/pbr/skills/shared/universal-anti-patterns.md +33 -0
- package/plugins/pbr/skills/status/SKILL.md +353 -0
- package/plugins/pbr/skills/todo/SKILL.md +181 -0
- package/plugins/pbr/templates/CONTEXT.md.tmpl +52 -0
- package/plugins/pbr/templates/INTEGRATION-REPORT.md.tmpl +151 -0
- package/plugins/pbr/templates/RESEARCH-SUMMARY.md.tmpl +97 -0
- package/plugins/pbr/templates/ROADMAP.md.tmpl +40 -0
- package/plugins/pbr/templates/SUMMARY.md.tmpl +81 -0
- package/plugins/pbr/templates/VERIFICATION-DETAIL.md.tmpl +116 -0
- package/plugins/pbr/templates/codebase/ARCHITECTURE.md.tmpl +98 -0
- package/plugins/pbr/templates/codebase/CONCERNS.md.tmpl +93 -0
- package/plugins/pbr/templates/codebase/CONVENTIONS.md.tmpl +104 -0
- package/plugins/pbr/templates/codebase/INTEGRATIONS.md.tmpl +78 -0
- package/plugins/pbr/templates/codebase/STACK.md.tmpl +78 -0
- package/plugins/pbr/templates/codebase/STRUCTURE.md.tmpl +80 -0
- package/plugins/pbr/templates/codebase/TESTING.md.tmpl +107 -0
- package/plugins/pbr/templates/continue-here.md.tmpl +73 -0
- package/plugins/pbr/templates/prompt-partials/phase-project-context.md.tmpl +37 -0
- package/plugins/pbr/templates/research/ARCHITECTURE.md.tmpl +124 -0
- package/plugins/pbr/templates/research/STACK.md.tmpl +71 -0
- package/plugins/pbr/templates/research/SUMMARY.md.tmpl +112 -0
- package/plugins/pbr/templates/research-outputs/phase-research.md.tmpl +81 -0
- package/plugins/pbr/templates/research-outputs/project-research.md.tmpl +99 -0
- 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
|
+
}
|