@sienklogic/plan-build-run 2.0.0 → 2.0.2
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/cursor-pbr/.cursor-plugin/plugin.json +22 -0
- package/plugins/cursor-pbr/agents/.gitkeep +0 -0
- package/plugins/cursor-pbr/assets/.gitkeep +0 -0
- package/plugins/cursor-pbr/hooks/hooks.json +11 -0
- package/plugins/cursor-pbr/references/.gitkeep +0 -0
- package/plugins/cursor-pbr/rules/.gitkeep +0 -0
- package/plugins/cursor-pbr/skills/.gitkeep +0 -0
- package/plugins/cursor-pbr/templates/.gitkeep +0 -0
- 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,252 +1,322 @@
|
|
|
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
|
-
|
|
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
|
+
/**
|
|
252
|
+
* Parse all phase directory slugs referenced in ROADMAP.md.
|
|
253
|
+
* Looks for NN-slug patterns in the Phase Overview table or
|
|
254
|
+
* phase reference lines like "## Phase 01-setup" or "01-setup".
|
|
255
|
+
* Returns an array of unique directory names, e.g. ["01-setup", "02-auth"].
|
|
256
|
+
*/
|
|
257
|
+
function parseRoadmapPhases(content) {
|
|
258
|
+
const phases = new Set();
|
|
259
|
+
const lines = content.split('\n');
|
|
260
|
+
|
|
261
|
+
for (const line of lines) {
|
|
262
|
+
// Match NN-slug patterns (at least two-digit prefix with hyphen and slug)
|
|
263
|
+
const matches = line.match(/\b(\d{2,}-[a-zA-Z][a-zA-Z0-9-]*)\b/g);
|
|
264
|
+
if (matches) {
|
|
265
|
+
for (const m of matches) {
|
|
266
|
+
phases.add(m);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return Array.from(phases);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check for drift between ROADMAP.md phase references and actual
|
|
276
|
+
* phase directories on disk under .planning/phases/.
|
|
277
|
+
*
|
|
278
|
+
* Returns an array of warning strings. Empty array means no drift.
|
|
279
|
+
*
|
|
280
|
+
* @param {string} roadmapContent - Contents of ROADMAP.md
|
|
281
|
+
* @param {string} phasesDir - Absolute path to .planning/phases/
|
|
282
|
+
* @returns {string[]} warnings
|
|
283
|
+
*/
|
|
284
|
+
function checkFilesystemDrift(roadmapContent, phasesDir) {
|
|
285
|
+
const warnings = [];
|
|
286
|
+
|
|
287
|
+
if (!fs.existsSync(phasesDir)) {
|
|
288
|
+
return warnings;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const roadmapPhases = parseRoadmapPhases(roadmapContent);
|
|
292
|
+
|
|
293
|
+
// Check that each ROADMAP.md phase has a directory on disk
|
|
294
|
+
for (const phase of roadmapPhases) {
|
|
295
|
+
const dirPath = path.join(phasesDir, phase);
|
|
296
|
+
if (!fs.existsSync(dirPath)) {
|
|
297
|
+
warnings.push(`Phase directory missing: .planning/phases/${phase} (referenced in ROADMAP.md)`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check for orphaned directories not referenced in ROADMAP.md
|
|
302
|
+
let entries;
|
|
303
|
+
try {
|
|
304
|
+
entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
305
|
+
} catch (_e) {
|
|
306
|
+
return warnings;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const entry of entries) {
|
|
310
|
+
if (!entry.isDirectory()) continue;
|
|
311
|
+
// Only consider NN-slug directories
|
|
312
|
+
if (!/^\d{2,}-[a-zA-Z]/.test(entry.name)) continue;
|
|
313
|
+
if (!roadmapPhases.includes(entry.name)) {
|
|
314
|
+
warnings.push(`Orphaned phase directory: .planning/phases/${entry.name} (not referenced in ROADMAP.md)`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return warnings;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = { parseState, getRoadmapPhaseStatus, checkSync, parseRoadmapPhases, checkFilesystemDrift };
|
|
322
|
+
if (require.main === module) { main(); }
|