@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,476 +1,476 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* PostToolUse hook: Auto-sync STATE.md and ROADMAP.md when SUMMARY or
|
|
5
|
-
* VERIFICATION files are written.
|
|
6
|
-
*
|
|
7
|
-
* Bridges the gap between build artifacts (SUMMARY/VERIFICATION) and
|
|
8
|
-
* tracking files (STATE.md/ROADMAP.md) so the status line stays current
|
|
9
|
-
* even when the orchestrator skips update steps.
|
|
10
|
-
*
|
|
11
|
-
* Trigger:
|
|
12
|
-
* - SUMMARY*.md or *SUMMARY*.md writes inside .planning/phases/
|
|
13
|
-
* - VERIFICATION.md writes inside .planning/phases/
|
|
14
|
-
*
|
|
15
|
-
* Guards:
|
|
16
|
-
* - Skips STATE.md / ROADMAP.md writes (prevents circular trigger)
|
|
17
|
-
* - Skips files outside .planning/phases/
|
|
18
|
-
* - Skips gracefully when tracking files don't exist
|
|
19
|
-
*
|
|
20
|
-
* Updates:
|
|
21
|
-
* - ROADMAP.md Progress table: Plans Complete, Status, Completed date
|
|
22
|
-
* - STATE.md Current Position: Plan count, Status, Last activity, Progress bar
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
const fs = require('fs');
|
|
26
|
-
const path = require('path');
|
|
27
|
-
const { logHook } = require('./hook-logger');
|
|
28
|
-
const { logEvent } = require('./event-logger');
|
|
29
|
-
const { atomicWrite } = require('./pbr-tools');
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Extract phase number from a phase directory name.
|
|
33
|
-
* E.g., "35-agent-output-budgets" → "35", "02-auth" → "02"
|
|
34
|
-
*
|
|
35
|
-
* @param {string} dirName - Directory name like "35-agent-output-budgets"
|
|
36
|
-
* @returns {string|null} Phase number string or null
|
|
37
|
-
*/
|
|
38
|
-
function extractPhaseNum(dirName) {
|
|
39
|
-
const match = dirName.match(/^(\d+)-/);
|
|
40
|
-
return match ? match[1] : null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Count PLAN and SUMMARY files in a phase directory.
|
|
45
|
-
*
|
|
46
|
-
* @param {string} phaseDir - Absolute path to the phase directory
|
|
47
|
-
* @returns {{ plans: number, summaries: number, completeSummaries: number }}
|
|
48
|
-
*/
|
|
49
|
-
function countPhaseArtifacts(phaseDir) {
|
|
50
|
-
try {
|
|
51
|
-
const files = fs.readdirSync(phaseDir);
|
|
52
|
-
const plans = files.filter(f => /-PLAN\.md$/.test(f));
|
|
53
|
-
const summaries = files.filter(f => /SUMMARY.*\.md$/.test(f) || /.*SUMMARY.*\.md$/.test(f));
|
|
54
|
-
|
|
55
|
-
// Filter for summaries that have status: complete in frontmatter
|
|
56
|
-
let completeSummaries = 0;
|
|
57
|
-
for (const s of summaries) {
|
|
58
|
-
try {
|
|
59
|
-
const content = fs.readFileSync(path.join(phaseDir, s), 'utf8');
|
|
60
|
-
if (/status:\s*["']?complete/i.test(content)) {
|
|
61
|
-
completeSummaries++;
|
|
62
|
-
}
|
|
63
|
-
} catch (_e) {
|
|
64
|
-
// Skip unreadable files
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { plans: plans.length, summaries: summaries.length, completeSummaries };
|
|
69
|
-
} catch (_e) {
|
|
70
|
-
return { plans: 0, summaries: 0, completeSummaries: 0 };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Update the Progress table in ROADMAP.md content.
|
|
76
|
-
*
|
|
77
|
-
* Progress table format:
|
|
78
|
-
* | Phase | Plans Complete | Status | Completed |
|
|
79
|
-
* |-------|----------------|--------|-----------|
|
|
80
|
-
* | 01. Project Scaffolding | 2/2 | Complete | 2026-02-08 |
|
|
81
|
-
*
|
|
82
|
-
* Phase column contains "NN. Name" — we match on the leading number.
|
|
83
|
-
*
|
|
84
|
-
* @param {string} content - Full ROADMAP.md content
|
|
85
|
-
* @param {string} phaseNum - Phase number (e.g., "35")
|
|
86
|
-
* @param {string} plansComplete - Plans complete string (e.g., "2/3")
|
|
87
|
-
* @param {string} status - New status (e.g., "Complete", "In progress")
|
|
88
|
-
* @param {string|null} completedDate - ISO date or null (sets Completed column)
|
|
89
|
-
* @returns {string} Updated content
|
|
90
|
-
*/
|
|
91
|
-
function updateProgressTable(content, phaseNum, plansComplete, status, completedDate) {
|
|
92
|
-
const lines = content.split('\n');
|
|
93
|
-
const paddedPhase = phaseNum.padStart(2, '0');
|
|
94
|
-
|
|
95
|
-
// Find the Progress table by looking for a header row with "Plans Complete"
|
|
96
|
-
let inProgressTable = false;
|
|
97
|
-
|
|
98
|
-
for (let i = 0; i < lines.length; i++) {
|
|
99
|
-
const line = lines[i];
|
|
100
|
-
|
|
101
|
-
if (!inProgressTable) {
|
|
102
|
-
if (line.includes('|') && /Plans\s*Complete/i.test(line)) {
|
|
103
|
-
inProgressTable = true;
|
|
104
|
-
}
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Skip separator row
|
|
109
|
-
if (/^\s*\|[\s-:|]+\|\s*$/.test(line)) continue;
|
|
110
|
-
|
|
111
|
-
// Non-table line ends the table
|
|
112
|
-
if (!line.includes('|')) break;
|
|
113
|
-
|
|
114
|
-
// Check if this row matches our phase number
|
|
115
|
-
const parts = line.split('|');
|
|
116
|
-
if (parts.length < 5) continue; // Need at least: empty | Phase | Plans | Status | Completed | empty
|
|
117
|
-
|
|
118
|
-
const phaseCol = (parts[1] || '').trim();
|
|
119
|
-
const phaseMatch = phaseCol.match(/^(\d+)\./);
|
|
120
|
-
if (!phaseMatch) continue;
|
|
121
|
-
|
|
122
|
-
if (phaseMatch[1] === paddedPhase || String(parseInt(phaseMatch[1], 10)) === String(parseInt(phaseNum, 10))) {
|
|
123
|
-
// Update this row
|
|
124
|
-
parts[2] = ` ${plansComplete} `;
|
|
125
|
-
parts[3] = ` ${status} `;
|
|
126
|
-
if (completedDate !== undefined && completedDate !== null) {
|
|
127
|
-
parts[4] = ` ${completedDate} `;
|
|
128
|
-
}
|
|
129
|
-
lines[i] = parts.join('|');
|
|
130
|
-
|
|
131
|
-
return lines.join('\n');
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Phase not found in Progress table — return unchanged
|
|
136
|
-
return content;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Update the Current Position section in STATE.md.
|
|
141
|
-
*
|
|
142
|
-
* Handles the legacy (non-frontmatter) format:
|
|
143
|
-
* ## Current Position
|
|
144
|
-
* Phase: 1 of 10 (Setup)
|
|
145
|
-
* Plan: 0 of 2 in current phase
|
|
146
|
-
* Status: Ready to plan
|
|
147
|
-
* Last activity: 2026-02-08 -- Project initialized
|
|
148
|
-
* Progress: [████░░░░░░░░░░░░░░░░] 20%
|
|
149
|
-
*
|
|
150
|
-
* @param {string} content - Full STATE.md content
|
|
151
|
-
* @param {object} updates - Fields to update
|
|
152
|
-
* @param {string} [updates.planLine] - New Plan: line value (e.g., "2 of 3 in current phase")
|
|
153
|
-
* @param {string} [updates.status] - New Status: value (e.g., "Building")
|
|
154
|
-
* @param {string} [updates.lastActivity] - New Last activity: value
|
|
155
|
-
* @param {number} [updates.progressPct] - New progress percentage (0-100)
|
|
156
|
-
* @returns {string} Updated content
|
|
157
|
-
*/
|
|
158
|
-
function updateStatePosition(content, updates) {
|
|
159
|
-
const lines = content.split('\n');
|
|
160
|
-
|
|
161
|
-
for (let i = 0; i < lines.length; i++) {
|
|
162
|
-
const line = lines[i];
|
|
163
|
-
|
|
164
|
-
if (updates.planLine !== undefined && /^Plan:\s/.test(line)) {
|
|
165
|
-
lines[i] = `Plan: ${updates.planLine}`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (updates.status !== undefined && /^Status:\s/.test(line)) {
|
|
169
|
-
lines[i] = `Status: ${updates.status}`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (updates.lastActivity !== undefined && /^Last activity:\s/i.test(line)) {
|
|
173
|
-
lines[i] = `Last activity: ${updates.lastActivity}`;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (updates.progressPct !== undefined && /^Progress:\s/.test(line)) {
|
|
177
|
-
lines[i] = `Progress: ${buildProgressBar(updates.progressPct)}`;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Also update frontmatter fields if present
|
|
182
|
-
if (content.startsWith('---')) {
|
|
183
|
-
const fmEnd = content.indexOf('---', 3);
|
|
184
|
-
if (fmEnd !== -1) {
|
|
185
|
-
let fm = content.substring(0, fmEnd + 3);
|
|
186
|
-
const body = content.substring(fmEnd + 3);
|
|
187
|
-
|
|
188
|
-
if (updates.fmPlansComplete !== undefined) {
|
|
189
|
-
fm = fm.replace(/^(plans_complete:\s*).*/m, `$1${updates.fmPlansComplete}`);
|
|
190
|
-
}
|
|
191
|
-
if (updates.fmStatus !== undefined) {
|
|
192
|
-
fm = fm.replace(/^(status:\s*).*/m, `$1"${updates.fmStatus}"`);
|
|
193
|
-
}
|
|
194
|
-
if (updates.fmLastActivity !== undefined) {
|
|
195
|
-
fm = fm.replace(/^(last_activity:\s*).*/m, `$1"${updates.fmLastActivity}"`);
|
|
196
|
-
}
|
|
197
|
-
if (updates.fmProgressPct !== undefined) {
|
|
198
|
-
fm = fm.replace(/^(progress_percent:\s*).*/m, `$1${updates.fmProgressPct}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Reconstruct with updated frontmatter + body with line updates
|
|
202
|
-
const updatedBody = updateStatePositionBody(body, updates);
|
|
203
|
-
return fm + updatedBody;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return lines.join('\n');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Update only the body (after frontmatter) of STATE.md.
|
|
212
|
-
*/
|
|
213
|
-
function updateStatePositionBody(body, updates) {
|
|
214
|
-
const lines = body.split('\n');
|
|
215
|
-
|
|
216
|
-
for (let i = 0; i < lines.length; i++) {
|
|
217
|
-
const line = lines[i];
|
|
218
|
-
|
|
219
|
-
if (updates.planLine !== undefined && /^Plan:\s/.test(line)) {
|
|
220
|
-
lines[i] = `Plan: ${updates.planLine}`;
|
|
221
|
-
}
|
|
222
|
-
if (updates.status !== undefined && /^Status:\s/.test(line)) {
|
|
223
|
-
lines[i] = `Status: ${updates.status}`;
|
|
224
|
-
}
|
|
225
|
-
if (updates.lastActivity !== undefined && /^Last activity:\s/i.test(line)) {
|
|
226
|
-
lines[i] = `Last activity: ${updates.lastActivity}`;
|
|
227
|
-
}
|
|
228
|
-
if (updates.progressPct !== undefined && /^Progress:\s/.test(line)) {
|
|
229
|
-
lines[i] = `Progress: ${buildProgressBar(updates.progressPct)}`;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return lines.join('\n');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Build a text progress bar: [████░░░░░░░░░░░░░░░░] 20%
|
|
238
|
-
* @param {number} pct - Percentage 0-100
|
|
239
|
-
* @returns {string}
|
|
240
|
-
*/
|
|
241
|
-
function buildProgressBar(pct) {
|
|
242
|
-
const width = 20;
|
|
243
|
-
const filled = Math.round((pct / 100) * width);
|
|
244
|
-
const empty = width - filled;
|
|
245
|
-
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${pct}%`;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Calculate overall progress percentage from all phase directories.
|
|
250
|
-
* Counts completed summaries vs total plans across all phases.
|
|
251
|
-
*
|
|
252
|
-
* @param {string} phasesDir - Path to .planning/phases/
|
|
253
|
-
* @returns {number} Percentage 0-100
|
|
254
|
-
*/
|
|
255
|
-
function calculateOverallProgress(phasesDir) {
|
|
256
|
-
try {
|
|
257
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
258
|
-
.filter(e => e.isDirectory());
|
|
259
|
-
|
|
260
|
-
let totalPlans = 0;
|
|
261
|
-
let completedPlans = 0;
|
|
262
|
-
|
|
263
|
-
for (const entry of entries) {
|
|
264
|
-
const dir = path.join(phasesDir, entry.name);
|
|
265
|
-
const artifacts = countPhaseArtifacts(dir);
|
|
266
|
-
totalPlans += artifacts.plans;
|
|
267
|
-
completedPlans += artifacts.completeSummaries;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return totalPlans > 0 ? Math.round((completedPlans / totalPlans) * 100) : 0;
|
|
271
|
-
} catch (_e) {
|
|
272
|
-
return 0;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Core state-sync check logic for use by dispatchers.
|
|
278
|
-
*
|
|
279
|
-
* @param {Object} data - Parsed hook input (tool_input, etc.)
|
|
280
|
-
* @returns {null|{output: Object}} null if not applicable, result with message otherwise
|
|
281
|
-
*/
|
|
282
|
-
function checkStateSync(data) {
|
|
283
|
-
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
284
|
-
const basename = path.basename(filePath);
|
|
285
|
-
|
|
286
|
-
// Guard: skip STATE.md and ROADMAP.md writes (prevents circular trigger)
|
|
287
|
-
if (basename === 'STATE.md' || basename === 'ROADMAP.md') return null;
|
|
288
|
-
|
|
289
|
-
// Determine if this is a SUMMARY or VERIFICATION write
|
|
290
|
-
const isSummary = basename.includes('SUMMARY') && basename.endsWith('.md');
|
|
291
|
-
const isVerification = basename === 'VERIFICATION.md';
|
|
292
|
-
|
|
293
|
-
if (!isSummary && !isVerification) return null;
|
|
294
|
-
|
|
295
|
-
// Guard: must be inside .planning/phases/
|
|
296
|
-
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
297
|
-
if (!normalizedPath.includes('.planning/phases/')) return null;
|
|
298
|
-
|
|
299
|
-
// Extract phase directory
|
|
300
|
-
const phaseDir = path.dirname(filePath);
|
|
301
|
-
const phaseDirName = path.basename(phaseDir);
|
|
302
|
-
const phaseNum = extractPhaseNum(phaseDirName);
|
|
303
|
-
|
|
304
|
-
if (!phaseNum) {
|
|
305
|
-
logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'could not extract phase number', dir: phaseDirName });
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const cwd = process.cwd();
|
|
310
|
-
const planningDir = path.join(cwd, '.planning');
|
|
311
|
-
const roadmapPath = path.join(planningDir, 'ROADMAP.md');
|
|
312
|
-
const statePath = path.join(planningDir, 'STATE.md');
|
|
313
|
-
const phasesDir = path.join(planningDir, 'phases');
|
|
314
|
-
|
|
315
|
-
// Count artifacts in this phase
|
|
316
|
-
const artifacts = countPhaseArtifacts(phaseDir);
|
|
317
|
-
|
|
318
|
-
if (artifacts.plans === 0) {
|
|
319
|
-
logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'no plans in phase', phase: phaseNum });
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
324
|
-
const messages = [];
|
|
325
|
-
|
|
326
|
-
if (isSummary) {
|
|
327
|
-
const plansComplete = `${artifacts.completeSummaries}/${artifacts.plans}`;
|
|
328
|
-
const allComplete = artifacts.completeSummaries >= artifacts.plans;
|
|
329
|
-
const newStatus = allComplete ? 'Complete' : 'In progress';
|
|
330
|
-
const completedDate = allComplete ? today : null;
|
|
331
|
-
|
|
332
|
-
// Update ROADMAP.md Progress table
|
|
333
|
-
if (fs.existsSync(roadmapPath)) {
|
|
334
|
-
try {
|
|
335
|
-
const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
|
|
336
|
-
const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, newStatus, completedDate);
|
|
337
|
-
if (updatedRoadmap !== roadmapContent) {
|
|
338
|
-
atomicWrite(roadmapPath, updatedRoadmap);
|
|
339
|
-
messages.push(`ROADMAP.md: Phase ${phaseNum} → ${plansComplete} plans, ${newStatus}`);
|
|
340
|
-
}
|
|
341
|
-
} catch (e) {
|
|
342
|
-
logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'ROADMAP.md update failed', error: e.message });
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Update STATE.md
|
|
347
|
-
if (fs.existsSync(statePath)) {
|
|
348
|
-
try {
|
|
349
|
-
const stateContent = fs.readFileSync(statePath, 'utf8');
|
|
350
|
-
const overallPct = calculateOverallProgress(phasesDir);
|
|
351
|
-
const stateUpdates = {
|
|
352
|
-
planLine: `${artifacts.completeSummaries} of ${artifacts.plans} in current phase`,
|
|
353
|
-
status: allComplete ? 'Built' : 'Building',
|
|
354
|
-
lastActivity: `${today} -- Phase ${phaseNum} plan completed`,
|
|
355
|
-
progressPct: overallPct,
|
|
356
|
-
fmPlansComplete: artifacts.completeSummaries,
|
|
357
|
-
fmStatus: allComplete ? 'built' : 'building',
|
|
358
|
-
fmLastActivity: today,
|
|
359
|
-
fmProgressPct: overallPct
|
|
360
|
-
};
|
|
361
|
-
const updatedState = updateStatePosition(stateContent, stateUpdates);
|
|
362
|
-
if (updatedState !== stateContent) {
|
|
363
|
-
atomicWrite(statePath, updatedState);
|
|
364
|
-
messages.push(`STATE.md: ${artifacts.completeSummaries}/${artifacts.plans} plans, ${overallPct}%`);
|
|
365
|
-
}
|
|
366
|
-
} catch (e) {
|
|
367
|
-
logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'STATE.md update failed', error: e.message });
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (isVerification) {
|
|
373
|
-
// Read VERIFICATION.md frontmatter for status
|
|
374
|
-
let verStatus = null;
|
|
375
|
-
try {
|
|
376
|
-
if (fs.existsSync(filePath)) {
|
|
377
|
-
const vContent = fs.readFileSync(filePath, 'utf8');
|
|
378
|
-
const statusMatch = vContent.match(/status:\s*["']?(\w+)/i);
|
|
379
|
-
if (statusMatch) {
|
|
380
|
-
verStatus = statusMatch[1].toLowerCase();
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
} catch (_e) {
|
|
384
|
-
// Skip if unreadable
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (!verStatus) {
|
|
388
|
-
logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'no status in VERIFICATION.md', phase: phaseNum });
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const isPassed = verStatus === 'passed';
|
|
393
|
-
const roadmapStatus = isPassed ? 'Complete' : 'Needs fixes';
|
|
394
|
-
const stateStatus = isPassed ? 'Verified' : 'Needs fixes';
|
|
395
|
-
const completedDate = isPassed ? today : null;
|
|
396
|
-
const plansComplete = `${artifacts.completeSummaries}/${artifacts.plans}`;
|
|
397
|
-
|
|
398
|
-
// Update ROADMAP.md Progress table
|
|
399
|
-
if (fs.existsSync(roadmapPath)) {
|
|
400
|
-
try {
|
|
401
|
-
const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
|
|
402
|
-
const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, roadmapStatus, completedDate);
|
|
403
|
-
if (updatedRoadmap !== roadmapContent) {
|
|
404
|
-
atomicWrite(roadmapPath, updatedRoadmap);
|
|
405
|
-
messages.push(`ROADMAP.md: Phase ${phaseNum} → ${roadmapStatus}`);
|
|
406
|
-
}
|
|
407
|
-
} catch (e) {
|
|
408
|
-
logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'ROADMAP.md update failed', error: e.message });
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Update STATE.md
|
|
413
|
-
if (fs.existsSync(statePath)) {
|
|
414
|
-
try {
|
|
415
|
-
const stateContent = fs.readFileSync(statePath, 'utf8');
|
|
416
|
-
const overallPct = calculateOverallProgress(phasesDir);
|
|
417
|
-
const stateUpdates = {
|
|
418
|
-
status: stateStatus,
|
|
419
|
-
lastActivity: `${today} -- Phase ${phaseNum} ${isPassed ? 'verified' : 'needs fixes'}`,
|
|
420
|
-
progressPct: overallPct,
|
|
421
|
-
fmStatus: isPassed ? 'verified' : 'needs_fixes',
|
|
422
|
-
fmLastActivity: today,
|
|
423
|
-
fmProgressPct: overallPct
|
|
424
|
-
};
|
|
425
|
-
const updatedState = updateStatePosition(stateContent, stateUpdates);
|
|
426
|
-
if (updatedState !== stateContent) {
|
|
427
|
-
atomicWrite(statePath, updatedState);
|
|
428
|
-
messages.push(`STATE.md: ${stateStatus}, ${overallPct}%`);
|
|
429
|
-
}
|
|
430
|
-
} catch (e) {
|
|
431
|
-
logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'STATE.md update failed', error: e.message });
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (messages.length > 0) {
|
|
437
|
-
const msg = `Auto-synced tracking files: ${messages.join('; ')}`;
|
|
438
|
-
logHook('check-state-sync', 'PostToolUse', 'sync', { phase: phaseNum, updates: messages });
|
|
439
|
-
logEvent('workflow', 'state-sync', { phase: phaseNum, trigger: isSummary ? 'summary' : 'verification', updates: messages });
|
|
440
|
-
return { output: { message: msg } };
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'no tracking files to update', phase: phaseNum });
|
|
444
|
-
return null;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Standalone mode
|
|
448
|
-
function main() {
|
|
449
|
-
let input = '';
|
|
450
|
-
|
|
451
|
-
process.stdin.setEncoding('utf8');
|
|
452
|
-
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
453
|
-
process.stdin.on('end', () => {
|
|
454
|
-
try {
|
|
455
|
-
const data = JSON.parse(input);
|
|
456
|
-
const result = checkStateSync(data);
|
|
457
|
-
if (result) {
|
|
458
|
-
process.stdout.write(JSON.stringify(result.output));
|
|
459
|
-
}
|
|
460
|
-
process.exit(0);
|
|
461
|
-
} catch (_e) {
|
|
462
|
-
process.exit(0);
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (require.main === module) { main(); }
|
|
468
|
-
module.exports = {
|
|
469
|
-
extractPhaseNum,
|
|
470
|
-
countPhaseArtifacts,
|
|
471
|
-
updateProgressTable,
|
|
472
|
-
updateStatePosition,
|
|
473
|
-
buildProgressBar,
|
|
474
|
-
calculateOverallProgress,
|
|
475
|
-
checkStateSync
|
|
476
|
-
};
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PostToolUse hook: Auto-sync STATE.md and ROADMAP.md when SUMMARY or
|
|
5
|
+
* VERIFICATION files are written.
|
|
6
|
+
*
|
|
7
|
+
* Bridges the gap between build artifacts (SUMMARY/VERIFICATION) and
|
|
8
|
+
* tracking files (STATE.md/ROADMAP.md) so the status line stays current
|
|
9
|
+
* even when the orchestrator skips update steps.
|
|
10
|
+
*
|
|
11
|
+
* Trigger:
|
|
12
|
+
* - SUMMARY*.md or *SUMMARY*.md writes inside .planning/phases/
|
|
13
|
+
* - VERIFICATION.md writes inside .planning/phases/
|
|
14
|
+
*
|
|
15
|
+
* Guards:
|
|
16
|
+
* - Skips STATE.md / ROADMAP.md writes (prevents circular trigger)
|
|
17
|
+
* - Skips files outside .planning/phases/
|
|
18
|
+
* - Skips gracefully when tracking files don't exist
|
|
19
|
+
*
|
|
20
|
+
* Updates:
|
|
21
|
+
* - ROADMAP.md Progress table: Plans Complete, Status, Completed date
|
|
22
|
+
* - STATE.md Current Position: Plan count, Status, Last activity, Progress bar
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const { logHook } = require('./hook-logger');
|
|
28
|
+
const { logEvent } = require('./event-logger');
|
|
29
|
+
const { atomicWrite } = require('./pbr-tools');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract phase number from a phase directory name.
|
|
33
|
+
* E.g., "35-agent-output-budgets" → "35", "02-auth" → "02"
|
|
34
|
+
*
|
|
35
|
+
* @param {string} dirName - Directory name like "35-agent-output-budgets"
|
|
36
|
+
* @returns {string|null} Phase number string or null
|
|
37
|
+
*/
|
|
38
|
+
function extractPhaseNum(dirName) {
|
|
39
|
+
const match = dirName.match(/^(\d+)-/);
|
|
40
|
+
return match ? match[1] : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Count PLAN and SUMMARY files in a phase directory.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} phaseDir - Absolute path to the phase directory
|
|
47
|
+
* @returns {{ plans: number, summaries: number, completeSummaries: number }}
|
|
48
|
+
*/
|
|
49
|
+
function countPhaseArtifacts(phaseDir) {
|
|
50
|
+
try {
|
|
51
|
+
const files = fs.readdirSync(phaseDir);
|
|
52
|
+
const plans = files.filter(f => /-PLAN\.md$/.test(f));
|
|
53
|
+
const summaries = files.filter(f => /SUMMARY.*\.md$/.test(f) || /.*SUMMARY.*\.md$/.test(f));
|
|
54
|
+
|
|
55
|
+
// Filter for summaries that have status: complete in frontmatter
|
|
56
|
+
let completeSummaries = 0;
|
|
57
|
+
for (const s of summaries) {
|
|
58
|
+
try {
|
|
59
|
+
const content = fs.readFileSync(path.join(phaseDir, s), 'utf8');
|
|
60
|
+
if (/status:\s*["']?complete/i.test(content)) {
|
|
61
|
+
completeSummaries++;
|
|
62
|
+
}
|
|
63
|
+
} catch (_e) {
|
|
64
|
+
// Skip unreadable files
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { plans: plans.length, summaries: summaries.length, completeSummaries };
|
|
69
|
+
} catch (_e) {
|
|
70
|
+
return { plans: 0, summaries: 0, completeSummaries: 0 };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Update the Progress table in ROADMAP.md content.
|
|
76
|
+
*
|
|
77
|
+
* Progress table format:
|
|
78
|
+
* | Phase | Plans Complete | Status | Completed |
|
|
79
|
+
* |-------|----------------|--------|-----------|
|
|
80
|
+
* | 01. Project Scaffolding | 2/2 | Complete | 2026-02-08 |
|
|
81
|
+
*
|
|
82
|
+
* Phase column contains "NN. Name" — we match on the leading number.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} content - Full ROADMAP.md content
|
|
85
|
+
* @param {string} phaseNum - Phase number (e.g., "35")
|
|
86
|
+
* @param {string} plansComplete - Plans complete string (e.g., "2/3")
|
|
87
|
+
* @param {string} status - New status (e.g., "Complete", "In progress")
|
|
88
|
+
* @param {string|null} completedDate - ISO date or null (sets Completed column)
|
|
89
|
+
* @returns {string} Updated content
|
|
90
|
+
*/
|
|
91
|
+
function updateProgressTable(content, phaseNum, plansComplete, status, completedDate) {
|
|
92
|
+
const lines = content.split('\n');
|
|
93
|
+
const paddedPhase = phaseNum.padStart(2, '0');
|
|
94
|
+
|
|
95
|
+
// Find the Progress table by looking for a header row with "Plans Complete"
|
|
96
|
+
let inProgressTable = false;
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < lines.length; i++) {
|
|
99
|
+
const line = lines[i];
|
|
100
|
+
|
|
101
|
+
if (!inProgressTable) {
|
|
102
|
+
if (line.includes('|') && /Plans\s*Complete/i.test(line)) {
|
|
103
|
+
inProgressTable = true;
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Skip separator row
|
|
109
|
+
if (/^\s*\|[\s-:|]+\|\s*$/.test(line)) continue;
|
|
110
|
+
|
|
111
|
+
// Non-table line ends the table
|
|
112
|
+
if (!line.includes('|')) break;
|
|
113
|
+
|
|
114
|
+
// Check if this row matches our phase number
|
|
115
|
+
const parts = line.split('|');
|
|
116
|
+
if (parts.length < 5) continue; // Need at least: empty | Phase | Plans | Status | Completed | empty
|
|
117
|
+
|
|
118
|
+
const phaseCol = (parts[1] || '').trim();
|
|
119
|
+
const phaseMatch = phaseCol.match(/^(\d+)\./);
|
|
120
|
+
if (!phaseMatch) continue;
|
|
121
|
+
|
|
122
|
+
if (phaseMatch[1] === paddedPhase || String(parseInt(phaseMatch[1], 10)) === String(parseInt(phaseNum, 10))) {
|
|
123
|
+
// Update this row
|
|
124
|
+
parts[2] = ` ${plansComplete} `;
|
|
125
|
+
parts[3] = ` ${status} `;
|
|
126
|
+
if (completedDate !== undefined && completedDate !== null) {
|
|
127
|
+
parts[4] = ` ${completedDate} `;
|
|
128
|
+
}
|
|
129
|
+
lines[i] = parts.join('|');
|
|
130
|
+
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Phase not found in Progress table — return unchanged
|
|
136
|
+
return content;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Update the Current Position section in STATE.md.
|
|
141
|
+
*
|
|
142
|
+
* Handles the legacy (non-frontmatter) format:
|
|
143
|
+
* ## Current Position
|
|
144
|
+
* Phase: 1 of 10 (Setup)
|
|
145
|
+
* Plan: 0 of 2 in current phase
|
|
146
|
+
* Status: Ready to plan
|
|
147
|
+
* Last activity: 2026-02-08 -- Project initialized
|
|
148
|
+
* Progress: [████░░░░░░░░░░░░░░░░] 20%
|
|
149
|
+
*
|
|
150
|
+
* @param {string} content - Full STATE.md content
|
|
151
|
+
* @param {object} updates - Fields to update
|
|
152
|
+
* @param {string} [updates.planLine] - New Plan: line value (e.g., "2 of 3 in current phase")
|
|
153
|
+
* @param {string} [updates.status] - New Status: value (e.g., "Building")
|
|
154
|
+
* @param {string} [updates.lastActivity] - New Last activity: value
|
|
155
|
+
* @param {number} [updates.progressPct] - New progress percentage (0-100)
|
|
156
|
+
* @returns {string} Updated content
|
|
157
|
+
*/
|
|
158
|
+
function updateStatePosition(content, updates) {
|
|
159
|
+
const lines = content.split('\n');
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < lines.length; i++) {
|
|
162
|
+
const line = lines[i];
|
|
163
|
+
|
|
164
|
+
if (updates.planLine !== undefined && /^Plan:\s/.test(line)) {
|
|
165
|
+
lines[i] = `Plan: ${updates.planLine}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (updates.status !== undefined && /^Status:\s/.test(line)) {
|
|
169
|
+
lines[i] = `Status: ${updates.status}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (updates.lastActivity !== undefined && /^Last activity:\s/i.test(line)) {
|
|
173
|
+
lines[i] = `Last activity: ${updates.lastActivity}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (updates.progressPct !== undefined && /^Progress:\s/.test(line)) {
|
|
177
|
+
lines[i] = `Progress: ${buildProgressBar(updates.progressPct)}`;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Also update frontmatter fields if present
|
|
182
|
+
if (content.startsWith('---')) {
|
|
183
|
+
const fmEnd = content.indexOf('---', 3);
|
|
184
|
+
if (fmEnd !== -1) {
|
|
185
|
+
let fm = content.substring(0, fmEnd + 3);
|
|
186
|
+
const body = content.substring(fmEnd + 3);
|
|
187
|
+
|
|
188
|
+
if (updates.fmPlansComplete !== undefined) {
|
|
189
|
+
fm = fm.replace(/^(plans_complete:\s*).*/m, `$1${updates.fmPlansComplete}`);
|
|
190
|
+
}
|
|
191
|
+
if (updates.fmStatus !== undefined) {
|
|
192
|
+
fm = fm.replace(/^(status:\s*).*/m, `$1"${updates.fmStatus}"`);
|
|
193
|
+
}
|
|
194
|
+
if (updates.fmLastActivity !== undefined) {
|
|
195
|
+
fm = fm.replace(/^(last_activity:\s*).*/m, `$1"${updates.fmLastActivity}"`);
|
|
196
|
+
}
|
|
197
|
+
if (updates.fmProgressPct !== undefined) {
|
|
198
|
+
fm = fm.replace(/^(progress_percent:\s*).*/m, `$1${updates.fmProgressPct}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Reconstruct with updated frontmatter + body with line updates
|
|
202
|
+
const updatedBody = updateStatePositionBody(body, updates);
|
|
203
|
+
return fm + updatedBody;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Update only the body (after frontmatter) of STATE.md.
|
|
212
|
+
*/
|
|
213
|
+
function updateStatePositionBody(body, updates) {
|
|
214
|
+
const lines = body.split('\n');
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < lines.length; i++) {
|
|
217
|
+
const line = lines[i];
|
|
218
|
+
|
|
219
|
+
if (updates.planLine !== undefined && /^Plan:\s/.test(line)) {
|
|
220
|
+
lines[i] = `Plan: ${updates.planLine}`;
|
|
221
|
+
}
|
|
222
|
+
if (updates.status !== undefined && /^Status:\s/.test(line)) {
|
|
223
|
+
lines[i] = `Status: ${updates.status}`;
|
|
224
|
+
}
|
|
225
|
+
if (updates.lastActivity !== undefined && /^Last activity:\s/i.test(line)) {
|
|
226
|
+
lines[i] = `Last activity: ${updates.lastActivity}`;
|
|
227
|
+
}
|
|
228
|
+
if (updates.progressPct !== undefined && /^Progress:\s/.test(line)) {
|
|
229
|
+
lines[i] = `Progress: ${buildProgressBar(updates.progressPct)}`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return lines.join('\n');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Build a text progress bar: [████░░░░░░░░░░░░░░░░] 20%
|
|
238
|
+
* @param {number} pct - Percentage 0-100
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
function buildProgressBar(pct) {
|
|
242
|
+
const width = 20;
|
|
243
|
+
const filled = Math.round((pct / 100) * width);
|
|
244
|
+
const empty = width - filled;
|
|
245
|
+
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${pct}%`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Calculate overall progress percentage from all phase directories.
|
|
250
|
+
* Counts completed summaries vs total plans across all phases.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} phasesDir - Path to .planning/phases/
|
|
253
|
+
* @returns {number} Percentage 0-100
|
|
254
|
+
*/
|
|
255
|
+
function calculateOverallProgress(phasesDir) {
|
|
256
|
+
try {
|
|
257
|
+
const entries = fs.readdirSync(phasesDir, { withFileTypes: true })
|
|
258
|
+
.filter(e => e.isDirectory());
|
|
259
|
+
|
|
260
|
+
let totalPlans = 0;
|
|
261
|
+
let completedPlans = 0;
|
|
262
|
+
|
|
263
|
+
for (const entry of entries) {
|
|
264
|
+
const dir = path.join(phasesDir, entry.name);
|
|
265
|
+
const artifacts = countPhaseArtifacts(dir);
|
|
266
|
+
totalPlans += artifacts.plans;
|
|
267
|
+
completedPlans += artifacts.completeSummaries;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return totalPlans > 0 ? Math.round((completedPlans / totalPlans) * 100) : 0;
|
|
271
|
+
} catch (_e) {
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Core state-sync check logic for use by dispatchers.
|
|
278
|
+
*
|
|
279
|
+
* @param {Object} data - Parsed hook input (tool_input, etc.)
|
|
280
|
+
* @returns {null|{output: Object}} null if not applicable, result with message otherwise
|
|
281
|
+
*/
|
|
282
|
+
function checkStateSync(data) {
|
|
283
|
+
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
284
|
+
const basename = path.basename(filePath);
|
|
285
|
+
|
|
286
|
+
// Guard: skip STATE.md and ROADMAP.md writes (prevents circular trigger)
|
|
287
|
+
if (basename === 'STATE.md' || basename === 'ROADMAP.md') return null;
|
|
288
|
+
|
|
289
|
+
// Determine if this is a SUMMARY or VERIFICATION write
|
|
290
|
+
const isSummary = basename.includes('SUMMARY') && basename.endsWith('.md');
|
|
291
|
+
const isVerification = basename === 'VERIFICATION.md';
|
|
292
|
+
|
|
293
|
+
if (!isSummary && !isVerification) return null;
|
|
294
|
+
|
|
295
|
+
// Guard: must be inside .planning/phases/
|
|
296
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
297
|
+
if (!normalizedPath.includes('.planning/phases/')) return null;
|
|
298
|
+
|
|
299
|
+
// Extract phase directory
|
|
300
|
+
const phaseDir = path.dirname(filePath);
|
|
301
|
+
const phaseDirName = path.basename(phaseDir);
|
|
302
|
+
const phaseNum = extractPhaseNum(phaseDirName);
|
|
303
|
+
|
|
304
|
+
if (!phaseNum) {
|
|
305
|
+
logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'could not extract phase number', dir: phaseDirName });
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const cwd = process.cwd();
|
|
310
|
+
const planningDir = path.join(cwd, '.planning');
|
|
311
|
+
const roadmapPath = path.join(planningDir, 'ROADMAP.md');
|
|
312
|
+
const statePath = path.join(planningDir, 'STATE.md');
|
|
313
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
314
|
+
|
|
315
|
+
// Count artifacts in this phase
|
|
316
|
+
const artifacts = countPhaseArtifacts(phaseDir);
|
|
317
|
+
|
|
318
|
+
if (artifacts.plans === 0) {
|
|
319
|
+
logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'no plans in phase', phase: phaseNum });
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
324
|
+
const messages = [];
|
|
325
|
+
|
|
326
|
+
if (isSummary) {
|
|
327
|
+
const plansComplete = `${artifacts.completeSummaries}/${artifacts.plans}`;
|
|
328
|
+
const allComplete = artifacts.completeSummaries >= artifacts.plans;
|
|
329
|
+
const newStatus = allComplete ? 'Complete' : 'In progress';
|
|
330
|
+
const completedDate = allComplete ? today : null;
|
|
331
|
+
|
|
332
|
+
// Update ROADMAP.md Progress table
|
|
333
|
+
if (fs.existsSync(roadmapPath)) {
|
|
334
|
+
try {
|
|
335
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
|
|
336
|
+
const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, newStatus, completedDate);
|
|
337
|
+
if (updatedRoadmap !== roadmapContent) {
|
|
338
|
+
atomicWrite(roadmapPath, updatedRoadmap);
|
|
339
|
+
messages.push(`ROADMAP.md: Phase ${phaseNum} → ${plansComplete} plans, ${newStatus}`);
|
|
340
|
+
}
|
|
341
|
+
} catch (e) {
|
|
342
|
+
logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'ROADMAP.md update failed', error: e.message });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Update STATE.md
|
|
347
|
+
if (fs.existsSync(statePath)) {
|
|
348
|
+
try {
|
|
349
|
+
const stateContent = fs.readFileSync(statePath, 'utf8');
|
|
350
|
+
const overallPct = calculateOverallProgress(phasesDir);
|
|
351
|
+
const stateUpdates = {
|
|
352
|
+
planLine: `${artifacts.completeSummaries} of ${artifacts.plans} in current phase`,
|
|
353
|
+
status: allComplete ? 'Built' : 'Building',
|
|
354
|
+
lastActivity: `${today} -- Phase ${phaseNum} plan completed`,
|
|
355
|
+
progressPct: overallPct,
|
|
356
|
+
fmPlansComplete: artifacts.completeSummaries,
|
|
357
|
+
fmStatus: allComplete ? 'built' : 'building',
|
|
358
|
+
fmLastActivity: today,
|
|
359
|
+
fmProgressPct: overallPct
|
|
360
|
+
};
|
|
361
|
+
const updatedState = updateStatePosition(stateContent, stateUpdates);
|
|
362
|
+
if (updatedState !== stateContent) {
|
|
363
|
+
atomicWrite(statePath, updatedState);
|
|
364
|
+
messages.push(`STATE.md: ${artifacts.completeSummaries}/${artifacts.plans} plans, ${overallPct}%`);
|
|
365
|
+
}
|
|
366
|
+
} catch (e) {
|
|
367
|
+
logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'STATE.md update failed', error: e.message });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (isVerification) {
|
|
373
|
+
// Read VERIFICATION.md frontmatter for status
|
|
374
|
+
let verStatus = null;
|
|
375
|
+
try {
|
|
376
|
+
if (fs.existsSync(filePath)) {
|
|
377
|
+
const vContent = fs.readFileSync(filePath, 'utf8');
|
|
378
|
+
const statusMatch = vContent.match(/status:\s*["']?(\w+)/i);
|
|
379
|
+
if (statusMatch) {
|
|
380
|
+
verStatus = statusMatch[1].toLowerCase();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch (_e) {
|
|
384
|
+
// Skip if unreadable
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!verStatus) {
|
|
388
|
+
logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'no status in VERIFICATION.md', phase: phaseNum });
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const isPassed = verStatus === 'passed';
|
|
393
|
+
const roadmapStatus = isPassed ? 'Complete' : 'Needs fixes';
|
|
394
|
+
const stateStatus = isPassed ? 'Verified' : 'Needs fixes';
|
|
395
|
+
const completedDate = isPassed ? today : null;
|
|
396
|
+
const plansComplete = `${artifacts.completeSummaries}/${artifacts.plans}`;
|
|
397
|
+
|
|
398
|
+
// Update ROADMAP.md Progress table
|
|
399
|
+
if (fs.existsSync(roadmapPath)) {
|
|
400
|
+
try {
|
|
401
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf8');
|
|
402
|
+
const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, roadmapStatus, completedDate);
|
|
403
|
+
if (updatedRoadmap !== roadmapContent) {
|
|
404
|
+
atomicWrite(roadmapPath, updatedRoadmap);
|
|
405
|
+
messages.push(`ROADMAP.md: Phase ${phaseNum} → ${roadmapStatus}`);
|
|
406
|
+
}
|
|
407
|
+
} catch (e) {
|
|
408
|
+
logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'ROADMAP.md update failed', error: e.message });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Update STATE.md
|
|
413
|
+
if (fs.existsSync(statePath)) {
|
|
414
|
+
try {
|
|
415
|
+
const stateContent = fs.readFileSync(statePath, 'utf8');
|
|
416
|
+
const overallPct = calculateOverallProgress(phasesDir);
|
|
417
|
+
const stateUpdates = {
|
|
418
|
+
status: stateStatus,
|
|
419
|
+
lastActivity: `${today} -- Phase ${phaseNum} ${isPassed ? 'verified' : 'needs fixes'}`,
|
|
420
|
+
progressPct: overallPct,
|
|
421
|
+
fmStatus: isPassed ? 'verified' : 'needs_fixes',
|
|
422
|
+
fmLastActivity: today,
|
|
423
|
+
fmProgressPct: overallPct
|
|
424
|
+
};
|
|
425
|
+
const updatedState = updateStatePosition(stateContent, stateUpdates);
|
|
426
|
+
if (updatedState !== stateContent) {
|
|
427
|
+
atomicWrite(statePath, updatedState);
|
|
428
|
+
messages.push(`STATE.md: ${stateStatus}, ${overallPct}%`);
|
|
429
|
+
}
|
|
430
|
+
} catch (e) {
|
|
431
|
+
logHook('check-state-sync', 'PostToolUse', 'error', { reason: 'STATE.md update failed', error: e.message });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (messages.length > 0) {
|
|
437
|
+
const msg = `Auto-synced tracking files: ${messages.join('; ')}`;
|
|
438
|
+
logHook('check-state-sync', 'PostToolUse', 'sync', { phase: phaseNum, updates: messages });
|
|
439
|
+
logEvent('workflow', 'state-sync', { phase: phaseNum, trigger: isSummary ? 'summary' : 'verification', updates: messages });
|
|
440
|
+
return { output: { message: msg } };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
logHook('check-state-sync', 'PostToolUse', 'skip', { reason: 'no tracking files to update', phase: phaseNum });
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Standalone mode
|
|
448
|
+
function main() {
|
|
449
|
+
let input = '';
|
|
450
|
+
|
|
451
|
+
process.stdin.setEncoding('utf8');
|
|
452
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
453
|
+
process.stdin.on('end', () => {
|
|
454
|
+
try {
|
|
455
|
+
const data = JSON.parse(input);
|
|
456
|
+
const result = checkStateSync(data);
|
|
457
|
+
if (result) {
|
|
458
|
+
process.stdout.write(JSON.stringify(result.output));
|
|
459
|
+
}
|
|
460
|
+
process.exit(0);
|
|
461
|
+
} catch (_e) {
|
|
462
|
+
process.exit(0);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (require.main === module) { main(); }
|
|
468
|
+
module.exports = {
|
|
469
|
+
extractPhaseNum,
|
|
470
|
+
countPhaseArtifacts,
|
|
471
|
+
updateProgressTable,
|
|
472
|
+
updateStatePosition,
|
|
473
|
+
buildProgressBar,
|
|
474
|
+
calculateOverallProgress,
|
|
475
|
+
checkStateSync
|
|
476
|
+
};
|