@sienklogic/plan-build-run 2.0.0 → 2.0.1
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 -56
- package/CLAUDE.md +149 -149
- package/LICENSE +21 -21
- package/README.md +247 -247
- package/dashboard/bin/cli.js +25 -25
- package/dashboard/package.json +34 -34
- package/dashboard/public/css/layout.css +406 -406
- package/dashboard/public/css/status-colors.css +98 -98
- package/dashboard/public/js/htmx-title.js +5 -5
- package/dashboard/public/js/sidebar-toggle.js +20 -20
- package/dashboard/src/app.js +78 -78
- package/dashboard/src/middleware/errorHandler.js +52 -52
- package/dashboard/src/middleware/notFoundHandler.js +9 -9
- package/dashboard/src/repositories/planning.repository.js +128 -128
- package/dashboard/src/routes/events.routes.js +40 -40
- package/dashboard/src/routes/index.routes.js +31 -31
- package/dashboard/src/routes/pages.routes.js +245 -195
- package/dashboard/src/server.js +42 -42
- package/dashboard/src/services/dashboard.service.js +222 -222
- package/dashboard/src/services/phase.service.js +220 -167
- package/dashboard/src/services/project.service.js +57 -57
- package/dashboard/src/services/roadmap.service.js +171 -171
- package/dashboard/src/services/sse.service.js +58 -58
- package/dashboard/src/services/todo.service.js +254 -254
- package/dashboard/src/services/watcher.service.js +48 -48
- package/dashboard/src/views/coming-soon.ejs +11 -11
- package/dashboard/src/views/error.ejs +13 -13
- package/dashboard/src/views/index.ejs +5 -5
- package/dashboard/src/views/layout.ejs +1 -1
- package/dashboard/src/views/partials/dashboard-content.ejs +77 -77
- package/dashboard/src/views/partials/footer.ejs +3 -3
- package/dashboard/src/views/partials/head.ejs +21 -21
- package/dashboard/src/views/partials/header.ejs +12 -12
- package/dashboard/src/views/partials/layout-bottom.ejs +15 -15
- package/dashboard/src/views/partials/layout-top.ejs +8 -8
- package/dashboard/src/views/partials/phase-content.ejs +188 -181
- package/dashboard/src/views/partials/phase-doc-content.ejs +38 -0
- package/dashboard/src/views/partials/phases-content.ejs +117 -117
- package/dashboard/src/views/partials/roadmap-content.ejs +142 -142
- package/dashboard/src/views/partials/sidebar.ejs +38 -38
- package/dashboard/src/views/partials/todo-create-content.ejs +53 -53
- package/dashboard/src/views/partials/todo-detail-content.ejs +38 -38
- package/dashboard/src/views/partials/todos-content.ejs +53 -53
- package/dashboard/src/views/phase-detail.ejs +5 -5
- package/dashboard/src/views/phase-doc.ejs +5 -0
- package/dashboard/src/views/phases.ejs +5 -5
- package/dashboard/src/views/roadmap.ejs +5 -5
- package/dashboard/src/views/todo-create.ejs +5 -5
- package/dashboard/src/views/todo-detail.ejs +5 -5
- package/dashboard/src/views/todos.ejs +5 -5
- package/package.json +57 -57
- package/plugins/pbr/.claude-plugin/plugin.json +13 -13
- package/plugins/pbr/UI-CONSISTENCY-GAPS.md +61 -61
- package/plugins/pbr/agents/codebase-mapper.md +279 -271
- package/plugins/pbr/agents/debugger.md +281 -281
- package/plugins/pbr/agents/executor.md +428 -407
- package/plugins/pbr/agents/general.md +164 -164
- package/plugins/pbr/agents/integration-checker.md +169 -141
- package/plugins/pbr/agents/plan-checker.md +296 -280
- package/plugins/pbr/agents/planner.md +358 -358
- package/plugins/pbr/agents/researcher.md +363 -363
- package/plugins/pbr/agents/synthesizer.md +230 -230
- package/plugins/pbr/agents/verifier.md +489 -454
- package/plugins/pbr/commands/begin.md +5 -5
- package/plugins/pbr/commands/build.md +5 -5
- package/plugins/pbr/commands/config.md +5 -5
- package/plugins/pbr/commands/continue.md +5 -5
- package/plugins/pbr/commands/debug.md +5 -5
- package/plugins/pbr/commands/discuss.md +5 -5
- package/plugins/pbr/commands/explore.md +5 -5
- package/plugins/pbr/commands/health.md +5 -5
- package/plugins/pbr/commands/help.md +5 -5
- package/plugins/pbr/commands/import.md +5 -5
- package/plugins/pbr/commands/milestone.md +5 -5
- package/plugins/pbr/commands/note.md +5 -5
- package/plugins/pbr/commands/pause.md +5 -5
- package/plugins/pbr/commands/plan.md +5 -5
- package/plugins/pbr/commands/quick.md +5 -5
- package/plugins/pbr/commands/resume.md +5 -5
- package/plugins/pbr/commands/review.md +5 -5
- package/plugins/pbr/commands/scan.md +5 -5
- package/plugins/pbr/commands/setup.md +5 -5
- package/plugins/pbr/commands/status.md +5 -5
- package/plugins/pbr/commands/todo.md +5 -5
- package/plugins/pbr/contexts/dev.md +27 -27
- package/plugins/pbr/contexts/research.md +28 -28
- package/plugins/pbr/contexts/review.md +36 -36
- package/plugins/pbr/hooks/hooks.json +183 -183
- package/plugins/pbr/references/agent-anti-patterns.md +24 -24
- package/plugins/pbr/references/agent-interactions.md +134 -134
- package/plugins/pbr/references/agent-teams.md +54 -54
- package/plugins/pbr/references/checkpoints.md +157 -157
- package/plugins/pbr/references/common-bug-patterns.md +13 -13
- package/plugins/pbr/references/config-reference.md +441 -0
- package/plugins/pbr/references/continuation-format.md +212 -212
- package/plugins/pbr/references/deviation-rules.md +112 -112
- package/plugins/pbr/references/git-integration.md +226 -226
- package/plugins/pbr/references/integration-patterns.md +117 -117
- package/plugins/pbr/references/model-profiles.md +99 -99
- package/plugins/pbr/references/model-selection.md +31 -31
- package/plugins/pbr/references/pbr-rules.md +193 -193
- package/plugins/pbr/references/plan-authoring.md +181 -181
- package/plugins/pbr/references/plan-format.md +287 -283
- package/plugins/pbr/references/planning-config.md +213 -213
- package/plugins/pbr/references/questioning.md +214 -214
- package/plugins/pbr/references/reading-verification.md +127 -127
- package/plugins/pbr/references/stub-patterns.md +160 -160
- package/plugins/pbr/references/subagent-coordination.md +119 -119
- package/plugins/pbr/references/ui-formatting.md +461 -399
- package/plugins/pbr/references/verification-patterns.md +198 -198
- package/plugins/pbr/references/wave-execution.md +95 -95
- package/plugins/pbr/scripts/auto-continue.js +80 -80
- package/plugins/pbr/scripts/check-dangerous-commands.js +136 -136
- package/plugins/pbr/scripts/check-doc-sprawl.js +102 -102
- package/plugins/pbr/scripts/check-phase-boundary.js +196 -196
- package/plugins/pbr/scripts/check-plan-format.js +270 -270
- package/plugins/pbr/scripts/check-roadmap-sync.js +322 -252
- package/plugins/pbr/scripts/check-skill-workflow.js +262 -262
- package/plugins/pbr/scripts/check-state-sync.js +476 -476
- package/plugins/pbr/scripts/check-subagent-output.js +144 -144
- package/plugins/pbr/scripts/config-schema.json +251 -251
- package/plugins/pbr/scripts/context-budget-check.js +287 -287
- package/plugins/pbr/scripts/event-handler.js +151 -151
- package/plugins/pbr/scripts/event-logger.js +92 -92
- package/plugins/pbr/scripts/hook-logger.js +80 -76
- package/plugins/pbr/scripts/hooks-schema.json +79 -79
- package/plugins/pbr/scripts/log-subagent.js +164 -152
- package/plugins/pbr/scripts/log-tool-failure.js +88 -88
- package/plugins/pbr/scripts/pbr-tools.js +1378 -1301
- package/plugins/pbr/scripts/post-write-dispatch.js +66 -66
- package/plugins/pbr/scripts/post-write-quality.js +207 -207
- package/plugins/pbr/scripts/pre-bash-dispatch.js +86 -56
- package/plugins/pbr/scripts/pre-write-dispatch.js +97 -62
- package/plugins/pbr/scripts/progress-tracker.js +281 -228
- package/plugins/pbr/scripts/run-hook.js +92 -0
- package/plugins/pbr/scripts/session-cleanup.js +254 -254
- package/plugins/pbr/scripts/status-line.js +288 -285
- package/plugins/pbr/scripts/suggest-compact.js +119 -119
- package/plugins/pbr/scripts/task-completed.js +45 -45
- package/plugins/pbr/scripts/track-context-budget.js +149 -119
- package/plugins/pbr/scripts/validate-commit.js +200 -200
- package/plugins/pbr/scripts/validate-plugin-structure.js +183 -172
- package/plugins/pbr/scripts/validate-task.js +106 -0
- package/plugins/pbr/skills/begin/SKILL.md +594 -545
- package/plugins/pbr/skills/begin/templates/PROJECT.md.tmpl +33 -33
- package/plugins/pbr/skills/begin/templates/REQUIREMENTS.md.tmpl +18 -18
- package/plugins/pbr/skills/begin/templates/STATE.md.tmpl +49 -49
- package/plugins/pbr/skills/begin/templates/config.json.tmpl +64 -63
- package/plugins/pbr/skills/begin/templates/researcher-prompt.md.tmpl +19 -19
- package/plugins/pbr/skills/begin/templates/roadmap-prompt.md.tmpl +30 -30
- package/plugins/pbr/skills/begin/templates/synthesis-prompt.md.tmpl +16 -16
- package/plugins/pbr/skills/build/SKILL.md +943 -962
- package/plugins/pbr/skills/config/SKILL.md +256 -241
- package/plugins/pbr/skills/continue/SKILL.md +164 -127
- package/plugins/pbr/skills/debug/SKILL.md +515 -489
- package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +16 -16
- package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +27 -27
- package/plugins/pbr/skills/discuss/SKILL.md +347 -338
- package/plugins/pbr/skills/discuss/templates/CONTEXT.md.tmpl +61 -61
- package/plugins/pbr/skills/discuss/templates/decision-categories.md +9 -9
- package/plugins/pbr/skills/explore/SKILL.md +378 -362
- package/plugins/pbr/skills/health/SKILL.md +221 -186
- package/plugins/pbr/skills/health/templates/check-pattern.md.tmpl +30 -30
- package/plugins/pbr/skills/health/templates/output-format.md.tmpl +63 -63
- package/plugins/pbr/skills/help/SKILL.md +155 -140
- package/plugins/pbr/skills/import/SKILL.md +504 -490
- package/plugins/pbr/skills/milestone/SKILL.md +704 -673
- package/plugins/pbr/skills/milestone/templates/audit-report.md.tmpl +48 -48
- package/plugins/pbr/skills/milestone/templates/stats-file.md.tmpl +30 -30
- package/plugins/pbr/skills/note/SKILL.md +231 -212
- package/plugins/pbr/skills/pause/SKILL.md +249 -235
- package/plugins/pbr/skills/pause/templates/continue-here.md.tmpl +71 -71
- package/plugins/pbr/skills/plan/SKILL.md +685 -628
- package/plugins/pbr/skills/plan/decimal-phase-calc.md +98 -98
- package/plugins/pbr/skills/plan/templates/checker-prompt.md.tmpl +21 -21
- package/plugins/pbr/skills/plan/templates/gap-closure-prompt.md.tmpl +32 -32
- package/plugins/pbr/skills/plan/templates/planner-prompt.md.tmpl +38 -38
- package/plugins/pbr/skills/plan/templates/researcher-prompt.md.tmpl +19 -19
- package/plugins/pbr/skills/plan/templates/revision-prompt.md.tmpl +23 -23
- package/plugins/pbr/skills/quick/SKILL.md +354 -335
- package/plugins/pbr/skills/resume/SKILL.md +402 -388
- package/plugins/pbr/skills/review/SKILL.md +686 -652
- package/plugins/pbr/skills/review/templates/debugger-prompt.md.tmpl +60 -60
- package/plugins/pbr/skills/review/templates/gap-planner-prompt.md.tmpl +40 -40
- package/plugins/pbr/skills/review/templates/verifier-prompt.md.tmpl +115 -115
- package/plugins/pbr/skills/scan/SKILL.md +304 -269
- package/plugins/pbr/skills/scan/templates/mapper-prompt.md.tmpl +201 -201
- package/plugins/pbr/skills/setup/SKILL.md +253 -227
- package/plugins/pbr/skills/shared/commit-planning-docs.md +35 -35
- package/plugins/pbr/skills/shared/config-loading.md +102 -102
- package/plugins/pbr/skills/shared/context-budget.md +40 -40
- package/plugins/pbr/skills/shared/context-loader-task.md +86 -86
- package/plugins/pbr/skills/shared/digest-select.md +79 -79
- package/plugins/pbr/skills/shared/domain-probes.md +125 -125
- package/plugins/pbr/skills/shared/error-reporting.md +79 -79
- package/plugins/pbr/skills/shared/gate-prompts.md +388 -388
- package/plugins/pbr/skills/shared/phase-argument-parsing.md +45 -45
- package/plugins/pbr/skills/shared/progress-display.md +53 -53
- package/plugins/pbr/skills/shared/revision-loop.md +81 -81
- package/plugins/pbr/skills/shared/state-loading.md +62 -62
- package/plugins/pbr/skills/shared/state-update.md +161 -161
- package/plugins/pbr/skills/shared/universal-anti-patterns.md +33 -33
- package/plugins/pbr/skills/status/SKILL.md +367 -353
- package/plugins/pbr/skills/todo/SKILL.md +198 -181
- package/plugins/pbr/templates/CONTEXT.md.tmpl +52 -52
- package/plugins/pbr/templates/INTEGRATION-REPORT.md.tmpl +151 -151
- package/plugins/pbr/templates/RESEARCH-SUMMARY.md.tmpl +97 -97
- package/plugins/pbr/templates/ROADMAP.md.tmpl +40 -40
- package/plugins/pbr/templates/SUMMARY.md.tmpl +81 -81
- package/plugins/pbr/templates/VERIFICATION-DETAIL.md.tmpl +116 -116
- package/plugins/pbr/templates/codebase/ARCHITECTURE.md.tmpl +98 -98
- package/plugins/pbr/templates/codebase/CONCERNS.md.tmpl +93 -93
- package/plugins/pbr/templates/codebase/CONVENTIONS.md.tmpl +104 -104
- package/plugins/pbr/templates/codebase/INTEGRATIONS.md.tmpl +78 -78
- package/plugins/pbr/templates/codebase/STACK.md.tmpl +78 -78
- package/plugins/pbr/templates/codebase/STRUCTURE.md.tmpl +80 -80
- package/plugins/pbr/templates/codebase/TESTING.md.tmpl +107 -107
- package/plugins/pbr/templates/continue-here.md.tmpl +73 -73
- package/plugins/pbr/templates/prompt-partials/phase-project-context.md.tmpl +37 -37
- package/plugins/pbr/templates/research/ARCHITECTURE.md.tmpl +124 -124
- package/plugins/pbr/templates/research/STACK.md.tmpl +71 -71
- package/plugins/pbr/templates/research/SUMMARY.md.tmpl +112 -112
- package/plugins/pbr/templates/research-outputs/phase-research.md.tmpl +81 -81
- package/plugins/pbr/templates/research-outputs/project-research.md.tmpl +99 -99
- package/plugins/pbr/templates/research-outputs/synthesis.md.tmpl +36 -36
|
@@ -1,270 +1,270 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* PostToolUse hook: Validates PLAN.md and SUMMARY.md structure.
|
|
5
|
-
*
|
|
6
|
-
* PLAN.md checks:
|
|
7
|
-
* - Each task has <name>, <files>, <action>, <verify>, <done> elements
|
|
8
|
-
* - Max 3 tasks per plan
|
|
9
|
-
* - Has YAML frontmatter with required fields (phase, plan, wave, must_haves)
|
|
10
|
-
*
|
|
11
|
-
* SUMMARY.md checks:
|
|
12
|
-
* - Has YAML frontmatter with required fields (phase, plan, status, provides, requires, key_files)
|
|
13
|
-
* - key_files paths exist on disk
|
|
14
|
-
* - Warns if no deferred field in frontmatter
|
|
15
|
-
*
|
|
16
|
-
* Returns decision: "block" for structural errors (forces Claude to fix and retry).
|
|
17
|
-
* Returns message for non-blocking warnings.
|
|
18
|
-
*
|
|
19
|
-
* Exit codes:
|
|
20
|
-
* 0 = always (PostToolUse hook, never blocks via exit code)
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const fs = require('fs');
|
|
24
|
-
const path = require('path');
|
|
25
|
-
const { logHook } = require('./hook-logger');
|
|
26
|
-
const { logEvent } = require('./event-logger');
|
|
27
|
-
|
|
28
|
-
function main() {
|
|
29
|
-
let input = '';
|
|
30
|
-
|
|
31
|
-
process.stdin.setEncoding('utf8');
|
|
32
|
-
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
33
|
-
process.stdin.on('end', () => {
|
|
34
|
-
try {
|
|
35
|
-
const data = JSON.parse(input);
|
|
36
|
-
|
|
37
|
-
// Get the file path that was written/edited
|
|
38
|
-
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
39
|
-
|
|
40
|
-
// Determine file type
|
|
41
|
-
const basename = path.basename(filePath);
|
|
42
|
-
const isPlan = basename.endsWith('PLAN.md');
|
|
43
|
-
const isSummary = basename.includes('SUMMARY') && basename.endsWith('.md');
|
|
44
|
-
|
|
45
|
-
if (!isPlan && !isSummary) {
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (!fs.existsSync(filePath)) {
|
|
50
|
-
process.exit(0);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
54
|
-
const result = isPlan
|
|
55
|
-
? validatePlan(content, filePath)
|
|
56
|
-
: validateSummary(content, filePath);
|
|
57
|
-
|
|
58
|
-
const eventType = isPlan ? 'plan-validated' : 'summary-validated';
|
|
59
|
-
|
|
60
|
-
if (result.errors.length > 0) {
|
|
61
|
-
// Structural errors — block and force correction
|
|
62
|
-
logHook('check-plan-format', 'PostToolUse', 'block', {
|
|
63
|
-
file: basename,
|
|
64
|
-
errors: result.errors
|
|
65
|
-
});
|
|
66
|
-
logEvent('workflow', eventType, {
|
|
67
|
-
file: basename,
|
|
68
|
-
status: 'block',
|
|
69
|
-
errorCount: result.errors.length
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const parts = [`${basename} has structural errors that must be fixed:`];
|
|
73
|
-
parts.push(...result.errors.map(i => ` - ${i}`));
|
|
74
|
-
|
|
75
|
-
if (result.warnings.length > 0) {
|
|
76
|
-
parts.push('');
|
|
77
|
-
parts.push('Warnings (non-blocking):');
|
|
78
|
-
parts.push(...result.warnings.map(i => ` - ${i}`));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const output = {
|
|
82
|
-
decision: 'block',
|
|
83
|
-
reason: parts.join('\n')
|
|
84
|
-
};
|
|
85
|
-
process.stdout.write(JSON.stringify(output));
|
|
86
|
-
} else if (result.warnings.length > 0) {
|
|
87
|
-
// Warnings only — non-blocking feedback
|
|
88
|
-
logHook('check-plan-format', 'PostToolUse', 'warn', {
|
|
89
|
-
file: basename,
|
|
90
|
-
warnings: result.warnings
|
|
91
|
-
});
|
|
92
|
-
logEvent('workflow', eventType, {
|
|
93
|
-
file: basename,
|
|
94
|
-
status: 'warn',
|
|
95
|
-
warningCount: result.warnings.length
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const output = {
|
|
99
|
-
message: `${basename} warnings:\n${result.warnings.map(i => ` - ${i}`).join('\n')}`
|
|
100
|
-
};
|
|
101
|
-
process.stdout.write(JSON.stringify(output));
|
|
102
|
-
} else {
|
|
103
|
-
// Clean pass
|
|
104
|
-
logHook('check-plan-format', 'PostToolUse', 'pass', { file: basename });
|
|
105
|
-
logEvent('workflow', eventType, { file: basename, status: 'pass' });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
process.exit(0);
|
|
109
|
-
} catch (_e) {
|
|
110
|
-
// Don't block on parse errors
|
|
111
|
-
process.exit(0);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function validatePlan(content, _filePath) {
|
|
117
|
-
const errors = [];
|
|
118
|
-
const warnings = [];
|
|
119
|
-
|
|
120
|
-
// Check frontmatter
|
|
121
|
-
if (!content.startsWith('---')) {
|
|
122
|
-
errors.push('Missing YAML frontmatter');
|
|
123
|
-
} else {
|
|
124
|
-
const frontmatterEnd = content.indexOf('---', 3);
|
|
125
|
-
if (frontmatterEnd === -1) {
|
|
126
|
-
errors.push('Unclosed YAML frontmatter');
|
|
127
|
-
} else {
|
|
128
|
-
const frontmatter = content.substring(3, frontmatterEnd);
|
|
129
|
-
const requiredFields = ['phase', 'plan', 'wave'];
|
|
130
|
-
for (const field of requiredFields) {
|
|
131
|
-
if (!frontmatter.includes(`${field}:`)) {
|
|
132
|
-
errors.push(`Frontmatter missing "${field}" field`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (!frontmatter.includes('must_haves:')) {
|
|
136
|
-
errors.push('Frontmatter missing "must_haves" field (truths/artifacts/key_links required)');
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Count tasks
|
|
142
|
-
const taskMatches = content.match(/<task\b[^>]*>/g) || [];
|
|
143
|
-
const taskCount = taskMatches.length;
|
|
144
|
-
|
|
145
|
-
if (taskCount === 0) {
|
|
146
|
-
errors.push('No <task> elements found');
|
|
147
|
-
} else if (taskCount > 3) {
|
|
148
|
-
errors.push(`Too many tasks: ${taskCount} (max 3 per plan)`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Check each task has required elements
|
|
152
|
-
const taskTags = content.match(/<task\b[^>]*>/g) || [];
|
|
153
|
-
const taskBlocks = content.split(/<task\b[^>]*>/).slice(1);
|
|
154
|
-
const requiredElements = ['name', 'files', 'action', 'verify', 'done'];
|
|
155
|
-
|
|
156
|
-
taskBlocks.forEach((block, index) => {
|
|
157
|
-
const taskEnd = block.indexOf('</task>');
|
|
158
|
-
const taskContent = taskEnd !== -1 ? block.substring(0, taskEnd) : block;
|
|
159
|
-
|
|
160
|
-
// Skip checkpoint tasks - they have different required elements
|
|
161
|
-
const taskTag = taskTags[index] || '';
|
|
162
|
-
if (taskTag.includes('checkpoint')) {
|
|
163
|
-
return; // Checkpoint tasks have different structure
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
for (const elem of requiredElements) {
|
|
167
|
-
if (!taskContent.includes(`<${elem}>`) && !taskContent.includes(`<${elem} `)) {
|
|
168
|
-
errors.push(`Task ${index + 1}: missing <${elem}> element`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
return { errors, warnings };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function validateSummary(content, _filePath) {
|
|
177
|
-
const errors = [];
|
|
178
|
-
const warnings = [];
|
|
179
|
-
|
|
180
|
-
// Check frontmatter
|
|
181
|
-
if (!content.startsWith('---')) {
|
|
182
|
-
errors.push('Missing YAML frontmatter');
|
|
183
|
-
} else {
|
|
184
|
-
const frontmatterEnd = content.indexOf('---', 3);
|
|
185
|
-
if (frontmatterEnd === -1) {
|
|
186
|
-
errors.push('Unclosed YAML frontmatter');
|
|
187
|
-
} else {
|
|
188
|
-
const frontmatter = content.substring(3, frontmatterEnd);
|
|
189
|
-
|
|
190
|
-
// Required fields — structural errors
|
|
191
|
-
const requiredFields = ['phase', 'plan', 'status', 'provides', 'requires', 'key_files'];
|
|
192
|
-
for (const field of requiredFields) {
|
|
193
|
-
if (!frontmatter.includes(`${field}:`)) {
|
|
194
|
-
errors.push(`Frontmatter missing "${field}" field`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Optional but encouraged — warnings
|
|
199
|
-
if (!frontmatter.includes('deferred:')) {
|
|
200
|
-
warnings.push('Frontmatter missing "deferred" field (forces executor to consciously record scope creep)');
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Validate key_files paths exist on disk — warning only (files may not exist yet during planning)
|
|
204
|
-
const keyFilesMatch = frontmatter.match(/key_files:\s*\n((?:\s+-\s+.*\n?)*)/);
|
|
205
|
-
if (keyFilesMatch) {
|
|
206
|
-
const lines = keyFilesMatch[1].split('\n').filter(l => l.trim().startsWith('-'));
|
|
207
|
-
for (const line of lines) {
|
|
208
|
-
// Parse "- path: description" or "- path" format
|
|
209
|
-
const entryMatch = line.match(/^\s*-\s+"?([^":]+?)(?::.*)?"?\s*$/);
|
|
210
|
-
if (entryMatch) {
|
|
211
|
-
const filePortion = entryMatch[1].trim();
|
|
212
|
-
if (filePortion && !fs.existsSync(filePortion)) {
|
|
213
|
-
warnings.push(`key_files path not found on disk: ${filePortion}`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return { errors, warnings };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Core plan/summary check logic for use by dispatchers.
|
|
226
|
-
* @param {Object} data - Parsed hook input (tool_input, etc.)
|
|
227
|
-
* @returns {null|{output: Object}} null if pass or not applicable, result otherwise
|
|
228
|
-
*/
|
|
229
|
-
function checkPlanWrite(data) {
|
|
230
|
-
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
231
|
-
const basename = path.basename(filePath);
|
|
232
|
-
const isPlan = basename.endsWith('PLAN.md');
|
|
233
|
-
const isSummary = basename.includes('SUMMARY') && basename.endsWith('.md');
|
|
234
|
-
|
|
235
|
-
if (!isPlan && !isSummary) return null;
|
|
236
|
-
if (!fs.existsSync(filePath)) return null;
|
|
237
|
-
|
|
238
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
239
|
-
const result = isPlan
|
|
240
|
-
? validatePlan(content, filePath)
|
|
241
|
-
: validateSummary(content, filePath);
|
|
242
|
-
|
|
243
|
-
const eventType = isPlan ? 'plan-validated' : 'summary-validated';
|
|
244
|
-
|
|
245
|
-
if (result.errors.length > 0) {
|
|
246
|
-
logHook('check-plan-format', 'PostToolUse', 'block', { file: basename, errors: result.errors });
|
|
247
|
-
logEvent('workflow', eventType, { file: basename, status: 'block', errorCount: result.errors.length });
|
|
248
|
-
|
|
249
|
-
const parts = [`${basename} has structural errors that must be fixed:`];
|
|
250
|
-
parts.push(...result.errors.map(i => ` - ${i}`));
|
|
251
|
-
if (result.warnings.length > 0) {
|
|
252
|
-
parts.push('', 'Warnings (non-blocking):');
|
|
253
|
-
parts.push(...result.warnings.map(i => ` - ${i}`));
|
|
254
|
-
}
|
|
255
|
-
return { output: { decision: 'block', reason: parts.join('\n') } };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (result.warnings.length > 0) {
|
|
259
|
-
logHook('check-plan-format', 'PostToolUse', 'warn', { file: basename, warnings: result.warnings });
|
|
260
|
-
logEvent('workflow', eventType, { file: basename, status: 'warn', warningCount: result.warnings.length });
|
|
261
|
-
return { output: { message: `${basename} warnings:\n${result.warnings.map(i => ` - ${i}`).join('\n')}` } };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
logHook('check-plan-format', 'PostToolUse', 'pass', { file: basename });
|
|
265
|
-
logEvent('workflow', eventType, { file: basename, status: 'pass' });
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
module.exports = { validatePlan, validateSummary, checkPlanWrite };
|
|
270
|
-
if (require.main === module) { main(); }
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PostToolUse hook: Validates PLAN.md and SUMMARY.md structure.
|
|
5
|
+
*
|
|
6
|
+
* PLAN.md checks:
|
|
7
|
+
* - Each task has <name>, <files>, <action>, <verify>, <done> elements
|
|
8
|
+
* - Max 3 tasks per plan
|
|
9
|
+
* - Has YAML frontmatter with required fields (phase, plan, wave, must_haves)
|
|
10
|
+
*
|
|
11
|
+
* SUMMARY.md checks:
|
|
12
|
+
* - Has YAML frontmatter with required fields (phase, plan, status, provides, requires, key_files)
|
|
13
|
+
* - key_files paths exist on disk
|
|
14
|
+
* - Warns if no deferred field in frontmatter
|
|
15
|
+
*
|
|
16
|
+
* Returns decision: "block" for structural errors (forces Claude to fix and retry).
|
|
17
|
+
* Returns message for non-blocking warnings.
|
|
18
|
+
*
|
|
19
|
+
* Exit codes:
|
|
20
|
+
* 0 = always (PostToolUse hook, never blocks via exit code)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { logHook } = require('./hook-logger');
|
|
26
|
+
const { logEvent } = require('./event-logger');
|
|
27
|
+
|
|
28
|
+
function main() {
|
|
29
|
+
let input = '';
|
|
30
|
+
|
|
31
|
+
process.stdin.setEncoding('utf8');
|
|
32
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
33
|
+
process.stdin.on('end', () => {
|
|
34
|
+
try {
|
|
35
|
+
const data = JSON.parse(input);
|
|
36
|
+
|
|
37
|
+
// Get the file path that was written/edited
|
|
38
|
+
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
39
|
+
|
|
40
|
+
// Determine file type
|
|
41
|
+
const basename = path.basename(filePath);
|
|
42
|
+
const isPlan = basename.endsWith('PLAN.md');
|
|
43
|
+
const isSummary = basename.includes('SUMMARY') && basename.endsWith('.md');
|
|
44
|
+
|
|
45
|
+
if (!isPlan && !isSummary) {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!fs.existsSync(filePath)) {
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
54
|
+
const result = isPlan
|
|
55
|
+
? validatePlan(content, filePath)
|
|
56
|
+
: validateSummary(content, filePath);
|
|
57
|
+
|
|
58
|
+
const eventType = isPlan ? 'plan-validated' : 'summary-validated';
|
|
59
|
+
|
|
60
|
+
if (result.errors.length > 0) {
|
|
61
|
+
// Structural errors — block and force correction
|
|
62
|
+
logHook('check-plan-format', 'PostToolUse', 'block', {
|
|
63
|
+
file: basename,
|
|
64
|
+
errors: result.errors
|
|
65
|
+
});
|
|
66
|
+
logEvent('workflow', eventType, {
|
|
67
|
+
file: basename,
|
|
68
|
+
status: 'block',
|
|
69
|
+
errorCount: result.errors.length
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const parts = [`${basename} has structural errors that must be fixed:`];
|
|
73
|
+
parts.push(...result.errors.map(i => ` - ${i}`));
|
|
74
|
+
|
|
75
|
+
if (result.warnings.length > 0) {
|
|
76
|
+
parts.push('');
|
|
77
|
+
parts.push('Warnings (non-blocking):');
|
|
78
|
+
parts.push(...result.warnings.map(i => ` - ${i}`));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const output = {
|
|
82
|
+
decision: 'block',
|
|
83
|
+
reason: parts.join('\n')
|
|
84
|
+
};
|
|
85
|
+
process.stdout.write(JSON.stringify(output));
|
|
86
|
+
} else if (result.warnings.length > 0) {
|
|
87
|
+
// Warnings only — non-blocking feedback
|
|
88
|
+
logHook('check-plan-format', 'PostToolUse', 'warn', {
|
|
89
|
+
file: basename,
|
|
90
|
+
warnings: result.warnings
|
|
91
|
+
});
|
|
92
|
+
logEvent('workflow', eventType, {
|
|
93
|
+
file: basename,
|
|
94
|
+
status: 'warn',
|
|
95
|
+
warningCount: result.warnings.length
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const output = {
|
|
99
|
+
message: `${basename} warnings:\n${result.warnings.map(i => ` - ${i}`).join('\n')}`
|
|
100
|
+
};
|
|
101
|
+
process.stdout.write(JSON.stringify(output));
|
|
102
|
+
} else {
|
|
103
|
+
// Clean pass
|
|
104
|
+
logHook('check-plan-format', 'PostToolUse', 'pass', { file: basename });
|
|
105
|
+
logEvent('workflow', eventType, { file: basename, status: 'pass' });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
process.exit(0);
|
|
109
|
+
} catch (_e) {
|
|
110
|
+
// Don't block on parse errors
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function validatePlan(content, _filePath) {
|
|
117
|
+
const errors = [];
|
|
118
|
+
const warnings = [];
|
|
119
|
+
|
|
120
|
+
// Check frontmatter
|
|
121
|
+
if (!content.startsWith('---')) {
|
|
122
|
+
errors.push('Missing YAML frontmatter');
|
|
123
|
+
} else {
|
|
124
|
+
const frontmatterEnd = content.indexOf('---', 3);
|
|
125
|
+
if (frontmatterEnd === -1) {
|
|
126
|
+
errors.push('Unclosed YAML frontmatter');
|
|
127
|
+
} else {
|
|
128
|
+
const frontmatter = content.substring(3, frontmatterEnd);
|
|
129
|
+
const requiredFields = ['phase', 'plan', 'wave'];
|
|
130
|
+
for (const field of requiredFields) {
|
|
131
|
+
if (!frontmatter.includes(`${field}:`)) {
|
|
132
|
+
errors.push(`Frontmatter missing "${field}" field`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!frontmatter.includes('must_haves:')) {
|
|
136
|
+
errors.push('Frontmatter missing "must_haves" field (truths/artifacts/key_links required)');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Count tasks
|
|
142
|
+
const taskMatches = content.match(/<task\b[^>]*>/g) || [];
|
|
143
|
+
const taskCount = taskMatches.length;
|
|
144
|
+
|
|
145
|
+
if (taskCount === 0) {
|
|
146
|
+
errors.push('No <task> elements found');
|
|
147
|
+
} else if (taskCount > 3) {
|
|
148
|
+
errors.push(`Too many tasks: ${taskCount} (max 3 per plan)`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check each task has required elements
|
|
152
|
+
const taskTags = content.match(/<task\b[^>]*>/g) || [];
|
|
153
|
+
const taskBlocks = content.split(/<task\b[^>]*>/).slice(1);
|
|
154
|
+
const requiredElements = ['name', 'files', 'action', 'verify', 'done'];
|
|
155
|
+
|
|
156
|
+
taskBlocks.forEach((block, index) => {
|
|
157
|
+
const taskEnd = block.indexOf('</task>');
|
|
158
|
+
const taskContent = taskEnd !== -1 ? block.substring(0, taskEnd) : block;
|
|
159
|
+
|
|
160
|
+
// Skip checkpoint tasks - they have different required elements
|
|
161
|
+
const taskTag = taskTags[index] || '';
|
|
162
|
+
if (taskTag.includes('checkpoint')) {
|
|
163
|
+
return; // Checkpoint tasks have different structure
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const elem of requiredElements) {
|
|
167
|
+
if (!taskContent.includes(`<${elem}>`) && !taskContent.includes(`<${elem} `)) {
|
|
168
|
+
errors.push(`Task ${index + 1}: missing <${elem}> element`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return { errors, warnings };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function validateSummary(content, _filePath) {
|
|
177
|
+
const errors = [];
|
|
178
|
+
const warnings = [];
|
|
179
|
+
|
|
180
|
+
// Check frontmatter
|
|
181
|
+
if (!content.startsWith('---')) {
|
|
182
|
+
errors.push('Missing YAML frontmatter');
|
|
183
|
+
} else {
|
|
184
|
+
const frontmatterEnd = content.indexOf('---', 3);
|
|
185
|
+
if (frontmatterEnd === -1) {
|
|
186
|
+
errors.push('Unclosed YAML frontmatter');
|
|
187
|
+
} else {
|
|
188
|
+
const frontmatter = content.substring(3, frontmatterEnd);
|
|
189
|
+
|
|
190
|
+
// Required fields — structural errors
|
|
191
|
+
const requiredFields = ['phase', 'plan', 'status', 'provides', 'requires', 'key_files'];
|
|
192
|
+
for (const field of requiredFields) {
|
|
193
|
+
if (!frontmatter.includes(`${field}:`)) {
|
|
194
|
+
errors.push(`Frontmatter missing "${field}" field`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Optional but encouraged — warnings
|
|
199
|
+
if (!frontmatter.includes('deferred:')) {
|
|
200
|
+
warnings.push('Frontmatter missing "deferred" field (forces executor to consciously record scope creep)');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Validate key_files paths exist on disk — warning only (files may not exist yet during planning)
|
|
204
|
+
const keyFilesMatch = frontmatter.match(/key_files:\s*\n((?:\s+-\s+.*\n?)*)/);
|
|
205
|
+
if (keyFilesMatch) {
|
|
206
|
+
const lines = keyFilesMatch[1].split('\n').filter(l => l.trim().startsWith('-'));
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
// Parse "- path: description" or "- path" format
|
|
209
|
+
const entryMatch = line.match(/^\s*-\s+"?([^":]+?)(?::.*)?"?\s*$/);
|
|
210
|
+
if (entryMatch) {
|
|
211
|
+
const filePortion = entryMatch[1].trim();
|
|
212
|
+
if (filePortion && !fs.existsSync(filePortion)) {
|
|
213
|
+
warnings.push(`key_files path not found on disk: ${filePortion}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { errors, warnings };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Core plan/summary check logic for use by dispatchers.
|
|
226
|
+
* @param {Object} data - Parsed hook input (tool_input, etc.)
|
|
227
|
+
* @returns {null|{output: Object}} null if pass or not applicable, result otherwise
|
|
228
|
+
*/
|
|
229
|
+
function checkPlanWrite(data) {
|
|
230
|
+
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
231
|
+
const basename = path.basename(filePath);
|
|
232
|
+
const isPlan = basename.endsWith('PLAN.md');
|
|
233
|
+
const isSummary = basename.includes('SUMMARY') && basename.endsWith('.md');
|
|
234
|
+
|
|
235
|
+
if (!isPlan && !isSummary) return null;
|
|
236
|
+
if (!fs.existsSync(filePath)) return null;
|
|
237
|
+
|
|
238
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
239
|
+
const result = isPlan
|
|
240
|
+
? validatePlan(content, filePath)
|
|
241
|
+
: validateSummary(content, filePath);
|
|
242
|
+
|
|
243
|
+
const eventType = isPlan ? 'plan-validated' : 'summary-validated';
|
|
244
|
+
|
|
245
|
+
if (result.errors.length > 0) {
|
|
246
|
+
logHook('check-plan-format', 'PostToolUse', 'block', { file: basename, errors: result.errors });
|
|
247
|
+
logEvent('workflow', eventType, { file: basename, status: 'block', errorCount: result.errors.length });
|
|
248
|
+
|
|
249
|
+
const parts = [`${basename} has structural errors that must be fixed:`];
|
|
250
|
+
parts.push(...result.errors.map(i => ` - ${i}`));
|
|
251
|
+
if (result.warnings.length > 0) {
|
|
252
|
+
parts.push('', 'Warnings (non-blocking):');
|
|
253
|
+
parts.push(...result.warnings.map(i => ` - ${i}`));
|
|
254
|
+
}
|
|
255
|
+
return { output: { decision: 'block', reason: parts.join('\n') } };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (result.warnings.length > 0) {
|
|
259
|
+
logHook('check-plan-format', 'PostToolUse', 'warn', { file: basename, warnings: result.warnings });
|
|
260
|
+
logEvent('workflow', eventType, { file: basename, status: 'warn', warningCount: result.warnings.length });
|
|
261
|
+
return { output: { message: `${basename} warnings:\n${result.warnings.map(i => ` - ${i}`).join('\n')}` } };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
logHook('check-plan-format', 'PostToolUse', 'pass', { file: basename });
|
|
265
|
+
logEvent('workflow', eventType, { file: basename, status: 'pass' });
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = { validatePlan, validateSummary, checkPlanWrite };
|
|
270
|
+
if (require.main === module) { main(); }
|