@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,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(); }
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PostToolUse hook (async): Checks that ROADMAP.md phase status
|
|
5
|
+
* stays in sync with STATE.md after state updates.
|
|
6
|
+
*
|
|
7
|
+
* When STATE.md is written/edited and contains a phase status
|
|
8
|
+
* (planned, built, partial, verified), this hook checks if the
|
|
9
|
+
* ROADMAP.md Phase Overview table has a matching status for that
|
|
10
|
+
* phase. If not, it warns Claude to update ROADMAP.md.
|
|
11
|
+
*
|
|
12
|
+
* Runs asynchronously (non-blocking). Issues are reported but
|
|
13
|
+
* don't prevent saving.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { logHook } = require('./hook-logger');
|
|
19
|
+
const { logEvent } = require('./event-logger');
|
|
20
|
+
|
|
21
|
+
const LIFECYCLE_STATUSES = ['planned', 'built', 'partial', 'verified'];
|
|
22
|
+
|
|
23
|
+
function main() {
|
|
24
|
+
let input = '';
|
|
25
|
+
|
|
26
|
+
process.stdin.setEncoding('utf8');
|
|
27
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
28
|
+
process.stdin.on('end', () => {
|
|
29
|
+
try {
|
|
30
|
+
const data = JSON.parse(input);
|
|
31
|
+
const filePath = data.tool_input?.file_path || '';
|
|
32
|
+
|
|
33
|
+
if (!filePath.endsWith('STATE.md')) {
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
const planningDir = path.join(cwd, '.planning');
|
|
39
|
+
const roadmapPath = path.join(planningDir, 'ROADMAP.md');
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(filePath) || !fs.existsSync(roadmapPath)) {
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const stateContent = fs.readFileSync(filePath, 'utf8');
|
|
46
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
|
|
47
|
+
|
|
48
|
+
const stateInfo = parseState(stateContent);
|
|
49
|
+
if (!stateInfo || !stateInfo.phase || !stateInfo.status) {
|
|
50
|
+
logHook('check-roadmap-sync', 'PostToolUse', 'skip', { reason: 'could not parse STATE.md' });
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!LIFECYCLE_STATUSES.includes(stateInfo.status)) {
|
|
55
|
+
logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
|
|
56
|
+
reason: `status "${stateInfo.status}" not a lifecycle status`
|
|
57
|
+
});
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const roadmapStatus = getRoadmapPhaseStatus(roadmapContent, stateInfo.phase);
|
|
62
|
+
if (!roadmapStatus) {
|
|
63
|
+
logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
|
|
64
|
+
reason: `phase ${stateInfo.phase} not found in ROADMAP.md table`
|
|
65
|
+
});
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (roadmapStatus.toLowerCase() !== stateInfo.status) {
|
|
70
|
+
logHook('check-roadmap-sync', 'PostToolUse', 'warn', {
|
|
71
|
+
phase: stateInfo.phase,
|
|
72
|
+
stateStatus: stateInfo.status,
|
|
73
|
+
roadmapStatus: roadmapStatus
|
|
74
|
+
});
|
|
75
|
+
logEvent('workflow', 'roadmap-sync', {
|
|
76
|
+
phase: stateInfo.phase,
|
|
77
|
+
stateStatus: stateInfo.status,
|
|
78
|
+
roadmapStatus: roadmapStatus,
|
|
79
|
+
status: 'out-of-sync'
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const output = {
|
|
83
|
+
message: `ROADMAP.md out of sync: Phase ${stateInfo.phase} is "${roadmapStatus}" in ROADMAP.md but "${stateInfo.status}" in STATE.md. Update the Phase Overview table in ROADMAP.md to match.`
|
|
84
|
+
};
|
|
85
|
+
process.stdout.write(JSON.stringify(output));
|
|
86
|
+
} else {
|
|
87
|
+
logHook('check-roadmap-sync', 'PostToolUse', 'pass', {
|
|
88
|
+
phase: stateInfo.phase,
|
|
89
|
+
status: stateInfo.status
|
|
90
|
+
});
|
|
91
|
+
logEvent('workflow', 'roadmap-sync', {
|
|
92
|
+
phase: stateInfo.phase,
|
|
93
|
+
status: 'in-sync'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
process.exit(0);
|
|
98
|
+
} catch (_e) {
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract current phase number and status from STATE.md.
|
|
106
|
+
* Handles common formats:
|
|
107
|
+
* "**Phase**: 03 - slug-name"
|
|
108
|
+
* "Phase: 3"
|
|
109
|
+
* "Current phase: 03-slug-name"
|
|
110
|
+
* "**Status**: planned"
|
|
111
|
+
* "Phase status: built"
|
|
112
|
+
*/
|
|
113
|
+
function parseState(content) {
|
|
114
|
+
const phaseMatch = content.match(
|
|
115
|
+
/\*{0,2}(?:Current\s+)?Phase\*{0,2}:\s*(\d+(?:\.\d+)?)/i
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const statusMatch = content.match(
|
|
119
|
+
/\*{0,2}(?:Phase\s+)?Status\*{0,2}:\s*["']?(\w+)["']?/i
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (!phaseMatch || !statusMatch) return null;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
phase: normalizePhaseNum(phaseMatch[1]),
|
|
126
|
+
status: statusMatch[1].toLowerCase()
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Find the status for a given phase in ROADMAP.md's Phase Overview table.
|
|
132
|
+
* Table format:
|
|
133
|
+
* | Phase | Name | Goal | Plans | Wave | Status |
|
|
134
|
+
* |-------|------|------|-------|------|--------|
|
|
135
|
+
* | 01 | ... | ... | ... | ... | pending |
|
|
136
|
+
*/
|
|
137
|
+
function getRoadmapPhaseStatus(content, phaseNum) {
|
|
138
|
+
const lines = content.split('\n');
|
|
139
|
+
|
|
140
|
+
let statusColIndex = -1;
|
|
141
|
+
let phaseColIndex = -1;
|
|
142
|
+
let inTable = false;
|
|
143
|
+
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
if (!inTable) {
|
|
146
|
+
if (line.includes('|') && /Phase/i.test(line) && /Status/i.test(line)) {
|
|
147
|
+
const cols = splitTableRow(line);
|
|
148
|
+
phaseColIndex = cols.findIndex(c => /^Phase$/i.test(c));
|
|
149
|
+
statusColIndex = cols.findIndex(c => /^Status$/i.test(c));
|
|
150
|
+
if (phaseColIndex !== -1 && statusColIndex !== -1) {
|
|
151
|
+
inTable = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Skip separator row
|
|
158
|
+
if (/^\s*\|[\s-:|]+\|\s*$/.test(line)) continue;
|
|
159
|
+
|
|
160
|
+
// Non-table line ends the table
|
|
161
|
+
if (!line.includes('|')) break;
|
|
162
|
+
|
|
163
|
+
const cols = splitTableRow(line);
|
|
164
|
+
if (cols.length <= Math.max(phaseColIndex, statusColIndex)) continue;
|
|
165
|
+
|
|
166
|
+
const rowPhase = normalizePhaseNum(cols[phaseColIndex]);
|
|
167
|
+
if (rowPhase === phaseNum) {
|
|
168
|
+
return cols[statusColIndex];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Split a markdown table row into trimmed cell values. */
|
|
176
|
+
function splitTableRow(line) {
|
|
177
|
+
return line.split('|').map(c => c.trim()).filter(Boolean);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Extract and normalize a phase number from various formats:
|
|
182
|
+
* "03" → "3"
|
|
183
|
+
* "3.1" → "3.1"
|
|
184
|
+
* "01. Project Scaffolding" → "1"
|
|
185
|
+
* "Phase 02" → "2"
|
|
186
|
+
*/
|
|
187
|
+
function normalizePhaseNum(raw) {
|
|
188
|
+
const match = raw.match(/(?:Phase\s+)?0*(\d+(?:\.\d+)?)/i);
|
|
189
|
+
return match ? match[1] : raw.trim();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Core roadmap sync check logic for use by dispatchers.
|
|
194
|
+
* @param {Object} data - Parsed hook input (tool_input, etc.)
|
|
195
|
+
* @returns {null|{output: Object}} null if pass or not applicable, result otherwise
|
|
196
|
+
*/
|
|
197
|
+
function checkSync(data) {
|
|
198
|
+
const filePath = data.tool_input?.file_path || '';
|
|
199
|
+
|
|
200
|
+
if (!filePath.endsWith('STATE.md')) return null;
|
|
201
|
+
|
|
202
|
+
const cwd = process.cwd();
|
|
203
|
+
const planningDir = path.join(cwd, '.planning');
|
|
204
|
+
const roadmapPath = path.join(planningDir, 'ROADMAP.md');
|
|
205
|
+
|
|
206
|
+
if (!fs.existsSync(filePath) || !fs.existsSync(roadmapPath)) return null;
|
|
207
|
+
|
|
208
|
+
const stateContent = fs.readFileSync(filePath, 'utf8');
|
|
209
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
|
|
210
|
+
|
|
211
|
+
const stateInfo = parseState(stateContent);
|
|
212
|
+
if (!stateInfo || !stateInfo.phase || !stateInfo.status) {
|
|
213
|
+
logHook('check-roadmap-sync', 'PostToolUse', 'skip', { reason: 'could not parse STATE.md' });
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!LIFECYCLE_STATUSES.includes(stateInfo.status)) {
|
|
218
|
+
logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
|
|
219
|
+
reason: `status "${stateInfo.status}" not a lifecycle status`
|
|
220
|
+
});
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const roadmapStatus = getRoadmapPhaseStatus(roadmapContent, stateInfo.phase);
|
|
225
|
+
if (!roadmapStatus) {
|
|
226
|
+
logHook('check-roadmap-sync', 'PostToolUse', 'skip', {
|
|
227
|
+
reason: `phase ${stateInfo.phase} not found in ROADMAP.md table`
|
|
228
|
+
});
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (roadmapStatus.toLowerCase() !== stateInfo.status) {
|
|
233
|
+
logHook('check-roadmap-sync', 'PostToolUse', 'warn', {
|
|
234
|
+
phase: stateInfo.phase, stateStatus: stateInfo.status, roadmapStatus
|
|
235
|
+
});
|
|
236
|
+
logEvent('workflow', 'roadmap-sync', {
|
|
237
|
+
phase: stateInfo.phase, stateStatus: stateInfo.status, roadmapStatus, status: 'out-of-sync'
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
output: {
|
|
241
|
+
message: `ROADMAP.md out of sync: Phase ${stateInfo.phase} is "${roadmapStatus}" in ROADMAP.md but "${stateInfo.status}" in STATE.md. Update the Phase Overview table in ROADMAP.md to match.`
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
logHook('check-roadmap-sync', 'PostToolUse', 'pass', { phase: stateInfo.phase, status: stateInfo.status });
|
|
247
|
+
logEvent('workflow', 'roadmap-sync', { phase: stateInfo.phase, status: 'in-sync' });
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = { parseState, getRoadmapPhaseStatus, checkSync };
|
|
252
|
+
if (require.main === module) { main(); }
|