@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,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
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PostToolUse hook on Task: Validates that subagent outputs exist.
|
|
5
|
+
*
|
|
6
|
+
* Maps agent types to expected output files and warns if they're missing
|
|
7
|
+
* after the agent completes. This catches silent agent failures early
|
|
8
|
+
* rather than discovering them during verification.
|
|
9
|
+
*
|
|
10
|
+
* Agent → Expected output mapping:
|
|
11
|
+
* executor → SUMMARY-{plan_id}.md (or SUMMARY.md) in the phase directory
|
|
12
|
+
* planner → PLAN-{MM}.md in the phase directory
|
|
13
|
+
* verifier → VERIFICATION.md in the phase directory
|
|
14
|
+
* researcher → RESEARCH.md (or domain-specific .md) in research/
|
|
15
|
+
*
|
|
16
|
+
* Exit codes:
|
|
17
|
+
* 0 = always (informational hook, never blocks — PostToolUse can only warn)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { logHook } = require('./hook-logger');
|
|
23
|
+
|
|
24
|
+
// Agent type → expected output patterns
|
|
25
|
+
const AGENT_OUTPUTS = {
|
|
26
|
+
'pbr:executor': {
|
|
27
|
+
description: 'SUMMARY.md in the phase directory',
|
|
28
|
+
check: (planningDir) => findInPhaseDir(planningDir, /^SUMMARY.*\.md$/i)
|
|
29
|
+
},
|
|
30
|
+
'pbr:planner': {
|
|
31
|
+
description: 'PLAN.md in the phase directory',
|
|
32
|
+
check: (planningDir) => findInPhaseDir(planningDir, /^PLAN.*\.md$/i)
|
|
33
|
+
},
|
|
34
|
+
'pbr:verifier': {
|
|
35
|
+
description: 'VERIFICATION.md in the phase directory',
|
|
36
|
+
check: (planningDir) => findInPhaseDir(planningDir, /^VERIFICATION\.md$/i)
|
|
37
|
+
},
|
|
38
|
+
'pbr:researcher': {
|
|
39
|
+
description: 'research file in .planning/research/',
|
|
40
|
+
check: (planningDir) => {
|
|
41
|
+
const researchDir = path.join(planningDir, 'research');
|
|
42
|
+
if (!fs.existsSync(researchDir)) return [];
|
|
43
|
+
try {
|
|
44
|
+
return fs.readdirSync(researchDir)
|
|
45
|
+
.filter(f => f.endsWith('.md'))
|
|
46
|
+
.map(f => path.join('research', f));
|
|
47
|
+
} catch (_e) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function findInPhaseDir(planningDir, pattern) {
|
|
55
|
+
const matches = [];
|
|
56
|
+
const phasesDir = path.join(planningDir, 'phases');
|
|
57
|
+
if (!fs.existsSync(phasesDir)) return matches;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Find the active phase from STATE.md
|
|
61
|
+
const stateFile = path.join(planningDir, 'STATE.md');
|
|
62
|
+
if (!fs.existsSync(stateFile)) return matches;
|
|
63
|
+
|
|
64
|
+
const stateContent = fs.readFileSync(stateFile, 'utf8');
|
|
65
|
+
const phaseMatch = stateContent.match(/Phase:\s*(\d+)\s+of\s+\d+/);
|
|
66
|
+
if (!phaseMatch) return matches;
|
|
67
|
+
|
|
68
|
+
const currentPhase = phaseMatch[1].padStart(2, '0');
|
|
69
|
+
const dirs = fs.readdirSync(phasesDir).filter(d => d.startsWith(currentPhase));
|
|
70
|
+
if (dirs.length === 0) return matches;
|
|
71
|
+
|
|
72
|
+
const phaseDir = path.join(phasesDir, dirs[0]);
|
|
73
|
+
const files = fs.readdirSync(phaseDir);
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
if (pattern.test(file)) {
|
|
76
|
+
// Check it's non-empty
|
|
77
|
+
const filePath = path.join(phaseDir, file);
|
|
78
|
+
const stat = fs.statSync(filePath);
|
|
79
|
+
if (stat.size > 0) {
|
|
80
|
+
matches.push(path.join('phases', dirs[0], file));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (_e) {
|
|
85
|
+
// best-effort
|
|
86
|
+
}
|
|
87
|
+
return matches;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readStdin() {
|
|
91
|
+
try {
|
|
92
|
+
const input = fs.readFileSync(0, 'utf8').trim();
|
|
93
|
+
if (input) return JSON.parse(input);
|
|
94
|
+
} catch (_e) {
|
|
95
|
+
// empty or non-JSON stdin
|
|
96
|
+
}
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function main() {
|
|
101
|
+
const data = readStdin();
|
|
102
|
+
const cwd = process.cwd();
|
|
103
|
+
const planningDir = path.join(cwd, '.planning');
|
|
104
|
+
|
|
105
|
+
// Only relevant for Plan-Build-Run projects
|
|
106
|
+
if (!fs.existsSync(planningDir)) {
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Extract agent type from the Task completion data
|
|
111
|
+
const agentType = data.tool_input?.subagent_type || data.subagent_type || '';
|
|
112
|
+
|
|
113
|
+
// Only check known Plan-Build-Run agent types
|
|
114
|
+
const outputSpec = AGENT_OUTPUTS[agentType];
|
|
115
|
+
if (!outputSpec) {
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check for expected outputs
|
|
120
|
+
const found = outputSpec.check(planningDir);
|
|
121
|
+
|
|
122
|
+
if (found.length === 0) {
|
|
123
|
+
logHook('check-subagent-output', 'PostToolUse', 'warning', {
|
|
124
|
+
agent_type: agentType,
|
|
125
|
+
expected: outputSpec.description,
|
|
126
|
+
found: 'none'
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const output = {
|
|
130
|
+
additionalContext: `Warning: Agent ${agentType} completed but no ${outputSpec.description} was found. The agent may have failed silently. Check agent output for errors.`
|
|
131
|
+
};
|
|
132
|
+
process.stdout.write(JSON.stringify(output));
|
|
133
|
+
} else {
|
|
134
|
+
logHook('check-subagent-output', 'PostToolUse', 'verified', {
|
|
135
|
+
agent_type: agentType,
|
|
136
|
+
found: found
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = { AGENT_OUTPUTS, findInPhaseDir };
|
|
144
|
+
if (require.main === module) { main(); }
|