@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.
Files changed (221) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/CLAUDE.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +247 -0
  5. package/dashboard/bin/cli.js +25 -0
  6. package/dashboard/package.json +34 -0
  7. package/dashboard/public/.gitkeep +0 -0
  8. package/dashboard/public/css/layout.css +406 -0
  9. package/dashboard/public/css/status-colors.css +98 -0
  10. package/dashboard/public/js/htmx-title.js +5 -0
  11. package/dashboard/public/js/sidebar-toggle.js +20 -0
  12. package/dashboard/src/app.js +78 -0
  13. package/dashboard/src/middleware/errorHandler.js +52 -0
  14. package/dashboard/src/middleware/notFoundHandler.js +9 -0
  15. package/dashboard/src/repositories/planning.repository.js +128 -0
  16. package/dashboard/src/routes/events.routes.js +40 -0
  17. package/dashboard/src/routes/index.routes.js +31 -0
  18. package/dashboard/src/routes/pages.routes.js +195 -0
  19. package/dashboard/src/server.js +42 -0
  20. package/dashboard/src/services/dashboard.service.js +222 -0
  21. package/dashboard/src/services/phase.service.js +167 -0
  22. package/dashboard/src/services/project.service.js +57 -0
  23. package/dashboard/src/services/roadmap.service.js +171 -0
  24. package/dashboard/src/services/sse.service.js +58 -0
  25. package/dashboard/src/services/todo.service.js +254 -0
  26. package/dashboard/src/services/watcher.service.js +48 -0
  27. package/dashboard/src/views/coming-soon.ejs +11 -0
  28. package/dashboard/src/views/error.ejs +13 -0
  29. package/dashboard/src/views/index.ejs +5 -0
  30. package/dashboard/src/views/layout.ejs +1 -0
  31. package/dashboard/src/views/partials/dashboard-content.ejs +77 -0
  32. package/dashboard/src/views/partials/footer.ejs +3 -0
  33. package/dashboard/src/views/partials/head.ejs +21 -0
  34. package/dashboard/src/views/partials/header.ejs +12 -0
  35. package/dashboard/src/views/partials/layout-bottom.ejs +15 -0
  36. package/dashboard/src/views/partials/layout-top.ejs +8 -0
  37. package/dashboard/src/views/partials/phase-content.ejs +181 -0
  38. package/dashboard/src/views/partials/phases-content.ejs +117 -0
  39. package/dashboard/src/views/partials/roadmap-content.ejs +142 -0
  40. package/dashboard/src/views/partials/sidebar.ejs +38 -0
  41. package/dashboard/src/views/partials/todo-create-content.ejs +53 -0
  42. package/dashboard/src/views/partials/todo-detail-content.ejs +38 -0
  43. package/dashboard/src/views/partials/todos-content.ejs +53 -0
  44. package/dashboard/src/views/phase-detail.ejs +5 -0
  45. package/dashboard/src/views/phases.ejs +5 -0
  46. package/dashboard/src/views/roadmap.ejs +5 -0
  47. package/dashboard/src/views/todo-create.ejs +5 -0
  48. package/dashboard/src/views/todo-detail.ejs +5 -0
  49. package/dashboard/src/views/todos.ejs +5 -0
  50. package/package.json +57 -0
  51. package/plugins/pbr/.claude-plugin/plugin.json +13 -0
  52. package/plugins/pbr/UI-CONSISTENCY-GAPS.md +61 -0
  53. package/plugins/pbr/agents/codebase-mapper.md +271 -0
  54. package/plugins/pbr/agents/debugger.md +281 -0
  55. package/plugins/pbr/agents/executor.md +407 -0
  56. package/plugins/pbr/agents/general.md +164 -0
  57. package/plugins/pbr/agents/integration-checker.md +141 -0
  58. package/plugins/pbr/agents/plan-checker.md +280 -0
  59. package/plugins/pbr/agents/planner.md +358 -0
  60. package/plugins/pbr/agents/researcher.md +363 -0
  61. package/plugins/pbr/agents/synthesizer.md +230 -0
  62. package/plugins/pbr/agents/verifier.md +454 -0
  63. package/plugins/pbr/commands/begin.md +5 -0
  64. package/plugins/pbr/commands/build.md +5 -0
  65. package/plugins/pbr/commands/config.md +5 -0
  66. package/plugins/pbr/commands/continue.md +5 -0
  67. package/plugins/pbr/commands/debug.md +5 -0
  68. package/plugins/pbr/commands/discuss.md +5 -0
  69. package/plugins/pbr/commands/explore.md +5 -0
  70. package/plugins/pbr/commands/health.md +5 -0
  71. package/plugins/pbr/commands/help.md +5 -0
  72. package/plugins/pbr/commands/import.md +5 -0
  73. package/plugins/pbr/commands/milestone.md +5 -0
  74. package/plugins/pbr/commands/note.md +5 -0
  75. package/plugins/pbr/commands/pause.md +5 -0
  76. package/plugins/pbr/commands/plan.md +5 -0
  77. package/plugins/pbr/commands/quick.md +5 -0
  78. package/plugins/pbr/commands/resume.md +5 -0
  79. package/plugins/pbr/commands/review.md +5 -0
  80. package/plugins/pbr/commands/scan.md +5 -0
  81. package/plugins/pbr/commands/setup.md +5 -0
  82. package/plugins/pbr/commands/status.md +5 -0
  83. package/plugins/pbr/commands/todo.md +5 -0
  84. package/plugins/pbr/contexts/dev.md +27 -0
  85. package/plugins/pbr/contexts/research.md +28 -0
  86. package/plugins/pbr/contexts/review.md +36 -0
  87. package/plugins/pbr/hooks/hooks.json +183 -0
  88. package/plugins/pbr/references/agent-anti-patterns.md +24 -0
  89. package/plugins/pbr/references/agent-interactions.md +134 -0
  90. package/plugins/pbr/references/agent-teams.md +54 -0
  91. package/plugins/pbr/references/checkpoints.md +157 -0
  92. package/plugins/pbr/references/common-bug-patterns.md +13 -0
  93. package/plugins/pbr/references/continuation-format.md +212 -0
  94. package/plugins/pbr/references/deviation-rules.md +112 -0
  95. package/plugins/pbr/references/git-integration.md +226 -0
  96. package/plugins/pbr/references/integration-patterns.md +117 -0
  97. package/plugins/pbr/references/model-profiles.md +99 -0
  98. package/plugins/pbr/references/model-selection.md +31 -0
  99. package/plugins/pbr/references/pbr-rules.md +193 -0
  100. package/plugins/pbr/references/plan-authoring.md +181 -0
  101. package/plugins/pbr/references/plan-format.md +283 -0
  102. package/plugins/pbr/references/planning-config.md +213 -0
  103. package/plugins/pbr/references/questioning.md +214 -0
  104. package/plugins/pbr/references/reading-verification.md +127 -0
  105. package/plugins/pbr/references/stub-patterns.md +160 -0
  106. package/plugins/pbr/references/subagent-coordination.md +119 -0
  107. package/plugins/pbr/references/ui-formatting.md +399 -0
  108. package/plugins/pbr/references/verification-patterns.md +198 -0
  109. package/plugins/pbr/references/wave-execution.md +95 -0
  110. package/plugins/pbr/scripts/auto-continue.js +80 -0
  111. package/plugins/pbr/scripts/check-dangerous-commands.js +136 -0
  112. package/plugins/pbr/scripts/check-doc-sprawl.js +102 -0
  113. package/plugins/pbr/scripts/check-phase-boundary.js +196 -0
  114. package/plugins/pbr/scripts/check-plan-format.js +270 -0
  115. package/plugins/pbr/scripts/check-roadmap-sync.js +252 -0
  116. package/plugins/pbr/scripts/check-skill-workflow.js +262 -0
  117. package/plugins/pbr/scripts/check-state-sync.js +476 -0
  118. package/plugins/pbr/scripts/check-subagent-output.js +144 -0
  119. package/plugins/pbr/scripts/config-schema.json +251 -0
  120. package/plugins/pbr/scripts/context-budget-check.js +287 -0
  121. package/plugins/pbr/scripts/event-handler.js +151 -0
  122. package/plugins/pbr/scripts/event-logger.js +92 -0
  123. package/plugins/pbr/scripts/hook-logger.js +76 -0
  124. package/plugins/pbr/scripts/hooks-schema.json +79 -0
  125. package/plugins/pbr/scripts/log-subagent.js +152 -0
  126. package/plugins/pbr/scripts/log-tool-failure.js +88 -0
  127. package/plugins/pbr/scripts/pbr-tools.js +1301 -0
  128. package/plugins/pbr/scripts/post-write-dispatch.js +66 -0
  129. package/plugins/pbr/scripts/post-write-quality.js +207 -0
  130. package/plugins/pbr/scripts/pre-bash-dispatch.js +56 -0
  131. package/plugins/pbr/scripts/pre-write-dispatch.js +62 -0
  132. package/plugins/pbr/scripts/progress-tracker.js +228 -0
  133. package/plugins/pbr/scripts/session-cleanup.js +254 -0
  134. package/plugins/pbr/scripts/status-line.js +285 -0
  135. package/plugins/pbr/scripts/suggest-compact.js +119 -0
  136. package/plugins/pbr/scripts/task-completed.js +45 -0
  137. package/plugins/pbr/scripts/track-context-budget.js +119 -0
  138. package/plugins/pbr/scripts/validate-commit.js +200 -0
  139. package/plugins/pbr/scripts/validate-plugin-structure.js +172 -0
  140. package/plugins/pbr/skills/begin/SKILL.md +545 -0
  141. package/plugins/pbr/skills/begin/templates/PROJECT.md.tmpl +33 -0
  142. package/plugins/pbr/skills/begin/templates/REQUIREMENTS.md.tmpl +18 -0
  143. package/plugins/pbr/skills/begin/templates/STATE.md.tmpl +49 -0
  144. package/plugins/pbr/skills/begin/templates/config.json.tmpl +63 -0
  145. package/plugins/pbr/skills/begin/templates/researcher-prompt.md.tmpl +19 -0
  146. package/plugins/pbr/skills/begin/templates/roadmap-prompt.md.tmpl +30 -0
  147. package/plugins/pbr/skills/begin/templates/synthesis-prompt.md.tmpl +16 -0
  148. package/plugins/pbr/skills/build/SKILL.md +962 -0
  149. package/plugins/pbr/skills/config/SKILL.md +241 -0
  150. package/plugins/pbr/skills/continue/SKILL.md +127 -0
  151. package/plugins/pbr/skills/debug/SKILL.md +489 -0
  152. package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +16 -0
  153. package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +27 -0
  154. package/plugins/pbr/skills/discuss/SKILL.md +338 -0
  155. package/plugins/pbr/skills/discuss/templates/CONTEXT.md.tmpl +61 -0
  156. package/plugins/pbr/skills/discuss/templates/decision-categories.md +9 -0
  157. package/plugins/pbr/skills/explore/SKILL.md +362 -0
  158. package/plugins/pbr/skills/health/SKILL.md +186 -0
  159. package/plugins/pbr/skills/health/templates/check-pattern.md.tmpl +30 -0
  160. package/plugins/pbr/skills/health/templates/output-format.md.tmpl +63 -0
  161. package/plugins/pbr/skills/help/SKILL.md +140 -0
  162. package/plugins/pbr/skills/import/SKILL.md +490 -0
  163. package/plugins/pbr/skills/milestone/SKILL.md +673 -0
  164. package/plugins/pbr/skills/milestone/templates/audit-report.md.tmpl +48 -0
  165. package/plugins/pbr/skills/milestone/templates/stats-file.md.tmpl +30 -0
  166. package/plugins/pbr/skills/note/SKILL.md +212 -0
  167. package/plugins/pbr/skills/pause/SKILL.md +235 -0
  168. package/plugins/pbr/skills/pause/templates/continue-here.md.tmpl +71 -0
  169. package/plugins/pbr/skills/plan/SKILL.md +628 -0
  170. package/plugins/pbr/skills/plan/decimal-phase-calc.md +98 -0
  171. package/plugins/pbr/skills/plan/templates/checker-prompt.md.tmpl +21 -0
  172. package/plugins/pbr/skills/plan/templates/gap-closure-prompt.md.tmpl +32 -0
  173. package/plugins/pbr/skills/plan/templates/planner-prompt.md.tmpl +38 -0
  174. package/plugins/pbr/skills/plan/templates/researcher-prompt.md.tmpl +19 -0
  175. package/plugins/pbr/skills/plan/templates/revision-prompt.md.tmpl +23 -0
  176. package/plugins/pbr/skills/quick/SKILL.md +335 -0
  177. package/plugins/pbr/skills/resume/SKILL.md +388 -0
  178. package/plugins/pbr/skills/review/SKILL.md +652 -0
  179. package/plugins/pbr/skills/review/templates/debugger-prompt.md.tmpl +60 -0
  180. package/plugins/pbr/skills/review/templates/gap-planner-prompt.md.tmpl +40 -0
  181. package/plugins/pbr/skills/review/templates/verifier-prompt.md.tmpl +115 -0
  182. package/plugins/pbr/skills/scan/SKILL.md +269 -0
  183. package/plugins/pbr/skills/scan/templates/mapper-prompt.md.tmpl +201 -0
  184. package/plugins/pbr/skills/setup/SKILL.md +227 -0
  185. package/plugins/pbr/skills/shared/commit-planning-docs.md +35 -0
  186. package/plugins/pbr/skills/shared/config-loading.md +102 -0
  187. package/plugins/pbr/skills/shared/context-budget.md +40 -0
  188. package/plugins/pbr/skills/shared/context-loader-task.md +86 -0
  189. package/plugins/pbr/skills/shared/digest-select.md +79 -0
  190. package/plugins/pbr/skills/shared/domain-probes.md +125 -0
  191. package/plugins/pbr/skills/shared/error-reporting.md +79 -0
  192. package/plugins/pbr/skills/shared/gate-prompts.md +388 -0
  193. package/plugins/pbr/skills/shared/phase-argument-parsing.md +45 -0
  194. package/plugins/pbr/skills/shared/progress-display.md +53 -0
  195. package/plugins/pbr/skills/shared/revision-loop.md +81 -0
  196. package/plugins/pbr/skills/shared/state-loading.md +62 -0
  197. package/plugins/pbr/skills/shared/state-update.md +161 -0
  198. package/plugins/pbr/skills/shared/universal-anti-patterns.md +33 -0
  199. package/plugins/pbr/skills/status/SKILL.md +353 -0
  200. package/plugins/pbr/skills/todo/SKILL.md +181 -0
  201. package/plugins/pbr/templates/CONTEXT.md.tmpl +52 -0
  202. package/plugins/pbr/templates/INTEGRATION-REPORT.md.tmpl +151 -0
  203. package/plugins/pbr/templates/RESEARCH-SUMMARY.md.tmpl +97 -0
  204. package/plugins/pbr/templates/ROADMAP.md.tmpl +40 -0
  205. package/plugins/pbr/templates/SUMMARY.md.tmpl +81 -0
  206. package/plugins/pbr/templates/VERIFICATION-DETAIL.md.tmpl +116 -0
  207. package/plugins/pbr/templates/codebase/ARCHITECTURE.md.tmpl +98 -0
  208. package/plugins/pbr/templates/codebase/CONCERNS.md.tmpl +93 -0
  209. package/plugins/pbr/templates/codebase/CONVENTIONS.md.tmpl +104 -0
  210. package/plugins/pbr/templates/codebase/INTEGRATIONS.md.tmpl +78 -0
  211. package/plugins/pbr/templates/codebase/STACK.md.tmpl +78 -0
  212. package/plugins/pbr/templates/codebase/STRUCTURE.md.tmpl +80 -0
  213. package/plugins/pbr/templates/codebase/TESTING.md.tmpl +107 -0
  214. package/plugins/pbr/templates/continue-here.md.tmpl +73 -0
  215. package/plugins/pbr/templates/prompt-partials/phase-project-context.md.tmpl +37 -0
  216. package/plugins/pbr/templates/research/ARCHITECTURE.md.tmpl +124 -0
  217. package/plugins/pbr/templates/research/STACK.md.tmpl +71 -0
  218. package/plugins/pbr/templates/research/SUMMARY.md.tmpl +112 -0
  219. package/plugins/pbr/templates/research-outputs/phase-research.md.tmpl +81 -0
  220. package/plugins/pbr/templates/research-outputs/project-research.md.tmpl +99 -0
  221. package/plugins/pbr/templates/research-outputs/synthesis.md.tmpl +36 -0
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SessionEnd cleanup hook.
5
+ *
6
+ * Removes stale planning artifacts that shouldn't persist across sessions:
7
+ * - .planning/.auto-next (prevents confusion on next session start)
8
+ * - .planning/.active-operation (stale operation lock)
9
+ * - .planning/.active-skill (stale skill tracking)
10
+ *
11
+ * Additional cleanup:
12
+ * - Removes stale .checkpoint-manifest.json files (>24h old)
13
+ * - Rotates hooks.jsonl when >200KB (moves to hooks.jsonl.1)
14
+ * - Warns about orphaned .PROGRESS-* files (executor crash artifacts)
15
+ * - Writes session summary to logs/sessions.jsonl
16
+ *
17
+ * Logs session end with reason to hook-log.
18
+ * Non-blocking — best-effort cleanup, fails silently.
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { logHook } = require('./hook-logger');
24
+ const { tailLines } = require('./pbr-tools');
25
+
26
+ function readStdin() {
27
+ try {
28
+ const input = fs.readFileSync(0, 'utf8').trim();
29
+ if (input) return JSON.parse(input);
30
+ } catch (_e) {
31
+ // empty or non-JSON stdin
32
+ }
33
+ return {};
34
+ }
35
+
36
+ function tryRemove(filePath) {
37
+ try {
38
+ if (fs.existsSync(filePath)) {
39
+ fs.unlinkSync(filePath);
40
+ return true;
41
+ }
42
+ } catch (_e) {
43
+ // best-effort — don't fail the hook
44
+ }
45
+ return false;
46
+ }
47
+
48
+ const STALE_CHECKPOINT_MS = 24 * 60 * 60 * 1000; // 24 hours
49
+ const MAX_HOOKS_LOG_BYTES = 200 * 1024; // 200KB
50
+
51
+ function cleanStaleCheckpoints(planningDir) {
52
+ const removed = [];
53
+ try {
54
+ const phasesDir = path.join(planningDir, 'phases');
55
+ if (!fs.existsSync(phasesDir)) return removed;
56
+
57
+ const dirs = fs.readdirSync(phasesDir);
58
+ for (const dir of dirs) {
59
+ const manifestPath = path.join(phasesDir, dir, '.checkpoint-manifest.json');
60
+ if (!fs.existsSync(manifestPath)) continue;
61
+
62
+ const stat = fs.statSync(manifestPath);
63
+ const ageMs = Date.now() - stat.mtimeMs;
64
+ if (ageMs > STALE_CHECKPOINT_MS) {
65
+ fs.unlinkSync(manifestPath);
66
+ removed.push(path.join('phases', dir, '.checkpoint-manifest.json'));
67
+ }
68
+ }
69
+ } catch (_e) {
70
+ // best-effort
71
+ }
72
+ return removed;
73
+ }
74
+
75
+ function rotateHooksLog(planningDir) {
76
+ try {
77
+ const logsDir = path.join(planningDir, 'logs');
78
+ const hooksLog = path.join(logsDir, 'hooks.jsonl');
79
+ if (!fs.existsSync(hooksLog)) return false;
80
+
81
+ const stat = fs.statSync(hooksLog);
82
+ if (stat.size <= MAX_HOOKS_LOG_BYTES) return false;
83
+
84
+ const rotatedPath = hooksLog + '.1';
85
+ // Overwrite any existing .1 file
86
+ fs.renameSync(hooksLog, rotatedPath);
87
+ return true;
88
+ } catch (_e) {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ function findOrphanedProgressFiles(planningDir) {
94
+ const orphans = [];
95
+ try {
96
+ const phasesDir = path.join(planningDir, 'phases');
97
+ if (!fs.existsSync(phasesDir)) return orphans;
98
+
99
+ const dirs = fs.readdirSync(phasesDir);
100
+ for (const dir of dirs) {
101
+ const phaseDir = path.join(phasesDir, dir);
102
+ const files = fs.readdirSync(phaseDir);
103
+ for (const file of files) {
104
+ if (file.startsWith('.PROGRESS-')) {
105
+ orphans.push(path.join('phases', dir, file));
106
+ }
107
+ }
108
+ }
109
+ } catch (_e) {
110
+ // best-effort
111
+ }
112
+ return orphans;
113
+ }
114
+
115
+ const MAX_SESSION_ENTRIES = 100;
116
+
117
+ function writeSessionHistory(planningDir, data) {
118
+ try {
119
+ const logsDir = path.join(planningDir, 'logs');
120
+ if (!fs.existsSync(logsDir)) {
121
+ fs.mkdirSync(logsDir, { recursive: true });
122
+ }
123
+
124
+ const sessionsFile = path.join(logsDir, 'sessions.jsonl');
125
+
126
+ // Mine existing logs for session stats
127
+ const hooksLog = path.join(logsDir, 'hooks.jsonl');
128
+ const eventsLog = path.join(logsDir, 'events.jsonl');
129
+
130
+ let agentsSpawned = 0;
131
+ let commitsCreated = 0;
132
+ const commandsRun = [];
133
+ let sessionStart = null;
134
+
135
+ // Count agents from hooks log (SubagentStart entries)
136
+ // Hooks log is capped at 200 entries; read last 200 to cover the full session
137
+ const hookLines = tailLines(hooksLog, 200);
138
+ for (const line of hookLines) {
139
+ try {
140
+ const entry = JSON.parse(line);
141
+ if (entry.event === 'SubagentStart' && entry.decision === 'spawned') {
142
+ agentsSpawned++;
143
+ }
144
+ // Track earliest timestamp as session start
145
+ if (entry.ts && (!sessionStart || entry.ts < sessionStart)) {
146
+ sessionStart = entry.ts;
147
+ }
148
+ } catch (_e) { /* skip malformed lines */ }
149
+ }
150
+
151
+ // Count commits and commands from events log
152
+ // Read last 200 entries — sufficient for a single session's events
153
+ const eventLines = tailLines(eventsLog, 200);
154
+ for (const line of eventLines) {
155
+ try {
156
+ const entry = JSON.parse(line);
157
+ if (entry.event === 'commit-validated' && entry.status === 'allow') {
158
+ commitsCreated++;
159
+ }
160
+ if (entry.cat === 'workflow' && entry.event) {
161
+ if (!commandsRun.includes(entry.event)) {
162
+ commandsRun.push(entry.event);
163
+ }
164
+ }
165
+ if (entry.ts && (!sessionStart || entry.ts < sessionStart)) {
166
+ sessionStart = entry.ts;
167
+ }
168
+ } catch (_e) { /* skip malformed lines */ }
169
+ }
170
+
171
+ const sessionEnd = new Date().toISOString();
172
+ let durationMinutes = null;
173
+ if (sessionStart) {
174
+ durationMinutes = Math.round((new Date(sessionEnd) - new Date(sessionStart)) / 60000);
175
+ }
176
+
177
+ const summary = {
178
+ start: sessionStart || sessionEnd,
179
+ end: sessionEnd,
180
+ duration_minutes: durationMinutes,
181
+ reason: data.reason || null,
182
+ agents_spawned: agentsSpawned,
183
+ commits_created: commitsCreated,
184
+ commands_run: commandsRun
185
+ };
186
+
187
+ // Append to sessions.jsonl, cap at MAX_SESSION_ENTRIES
188
+ let lines = [];
189
+ if (fs.existsSync(sessionsFile)) {
190
+ const content = fs.readFileSync(sessionsFile, 'utf8').trim();
191
+ if (content) {
192
+ lines = content.split('\n');
193
+ }
194
+ }
195
+ lines.push(JSON.stringify(summary));
196
+ if (lines.length > MAX_SESSION_ENTRIES) {
197
+ lines = lines.slice(lines.length - MAX_SESSION_ENTRIES);
198
+ }
199
+ fs.writeFileSync(sessionsFile, lines.join('\n') + '\n', 'utf8');
200
+ } catch (_e) {
201
+ // Best-effort — don't fail the hook
202
+ }
203
+ }
204
+
205
+ function main() {
206
+ const data = readStdin();
207
+ const cwd = process.cwd();
208
+ const planningDir = path.join(cwd, '.planning');
209
+
210
+ if (!fs.existsSync(planningDir)) {
211
+ process.exit(0);
212
+ }
213
+
214
+ const cleaned = [];
215
+
216
+ if (tryRemove(path.join(planningDir, '.auto-next'))) {
217
+ cleaned.push('.auto-next');
218
+ }
219
+ if (tryRemove(path.join(planningDir, '.active-operation'))) {
220
+ cleaned.push('.active-operation');
221
+ }
222
+ if (tryRemove(path.join(planningDir, '.active-skill'))) {
223
+ cleaned.push('.active-skill');
224
+ }
225
+ if (tryRemove(path.join(planningDir, '.active-plan'))) {
226
+ cleaned.push('.active-plan');
227
+ }
228
+
229
+ // Clean stale checkpoint manifests (>24h old)
230
+ const staleCheckpoints = cleanStaleCheckpoints(planningDir);
231
+ cleaned.push(...staleCheckpoints);
232
+
233
+ // Rotate hooks.jsonl if >200KB
234
+ const rotated = rotateHooksLog(planningDir);
235
+
236
+ // Detect orphaned .PROGRESS-* files (executor crash artifacts)
237
+ const orphans = findOrphanedProgressFiles(planningDir);
238
+
239
+ // Write session history log
240
+ writeSessionHistory(planningDir, data);
241
+
242
+ const decision = cleaned.length > 0 ? 'cleaned' : 'nothing';
243
+ logHook('session-cleanup', 'SessionEnd', decision, {
244
+ reason: data.reason || null,
245
+ removed: cleaned,
246
+ log_rotated: rotated,
247
+ orphaned_progress_files: orphans.length > 0 ? orphans : undefined
248
+ });
249
+
250
+ process.exit(0);
251
+ }
252
+
253
+ module.exports = { writeSessionHistory, tryRemove, cleanStaleCheckpoints, rotateHooksLog, findOrphanedProgressFiles };
254
+ if (require.main === module) { main(); }
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Status line: Updates Claude Code status bar with phase progress and
5
+ * context usage bar.
6
+ *
7
+ * Reads STATE.md for project position. Receives session JSON on stdin
8
+ * from Claude Code (context_window, model, cost, etc.).
9
+ *
10
+ * Output: plain text with ANSI color codes to stdout.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const cp = require('child_process');
16
+ const { logHook } = require('./hook-logger');
17
+ const { configLoad } = require('./pbr-tools');
18
+
19
+ // ANSI color codes
20
+ const c = {
21
+ reset: '\x1b[0m',
22
+ bold: '\x1b[1m',
23
+ dim: '\x1b[2m',
24
+ cyan: '\x1b[36m',
25
+ green: '\x1b[32m',
26
+ yellow: '\x1b[33m',
27
+ red: '\x1b[31m',
28
+ blue: '\x1b[34m',
29
+ magenta: '\x1b[35m',
30
+ white: '\x1b[37m',
31
+ boldCyan: '\x1b[1;36m',
32
+ boldGreen: '\x1b[1;32m',
33
+ boldYellow: '\x1b[1;33m',
34
+ boldRed: '\x1b[1;31m',
35
+ };
36
+
37
+ // Default status_line config — works out of the box with zero config
38
+ const DEFAULTS = {
39
+ sections: ['phase', 'plan', 'status', 'git', 'context'],
40
+ brand_text: '\u25C6 Plan-Build-Run',
41
+ max_status_length: 50,
42
+ context_bar: {
43
+ width: 10,
44
+ thresholds: { green: 70, yellow: 90 },
45
+ chars: { filled: '\u2588', empty: '\u2591' }
46
+ }
47
+ };
48
+
49
+ /**
50
+ * Load status_line config from .planning/config.json, merged with defaults.
51
+ * Returns DEFAULTS if no config exists or no status_line section is present.
52
+ */
53
+ function loadStatusLineConfig(planningDir) {
54
+ const config = configLoad(planningDir);
55
+ if (!config || !config.status_line) return DEFAULTS;
56
+
57
+ const sl = config.status_line;
58
+ return {
59
+ sections: Array.isArray(sl.sections) ? sl.sections : DEFAULTS.sections,
60
+ brand_text: typeof sl.brand_text === 'string' ? sl.brand_text : DEFAULTS.brand_text,
61
+ max_status_length: typeof sl.max_status_length === 'number' ? sl.max_status_length : DEFAULTS.max_status_length,
62
+ context_bar: {
63
+ width: (sl.context_bar && typeof sl.context_bar.width === 'number') ? sl.context_bar.width : DEFAULTS.context_bar.width,
64
+ thresholds: {
65
+ green: (sl.context_bar && sl.context_bar.thresholds && typeof sl.context_bar.thresholds.green === 'number') ? sl.context_bar.thresholds.green : DEFAULTS.context_bar.thresholds.green,
66
+ yellow: (sl.context_bar && sl.context_bar.thresholds && typeof sl.context_bar.thresholds.yellow === 'number') ? sl.context_bar.thresholds.yellow : DEFAULTS.context_bar.thresholds.yellow
67
+ },
68
+ chars: {
69
+ filled: (sl.context_bar && sl.context_bar.chars && typeof sl.context_bar.chars.filled === 'string') ? sl.context_bar.chars.filled : DEFAULTS.context_bar.chars.filled,
70
+ empty: (sl.context_bar && sl.context_bar.chars && typeof sl.context_bar.chars.empty === 'string') ? sl.context_bar.chars.empty : DEFAULTS.context_bar.chars.empty
71
+ }
72
+ }
73
+ };
74
+ }
75
+
76
+ function readStdin() {
77
+ try {
78
+ const input = fs.readFileSync(0, 'utf8').trim();
79
+ if (input) return JSON.parse(input);
80
+ } catch (_e) {
81
+ // stdin may be empty or not JSON — that's fine
82
+ }
83
+ return {};
84
+ }
85
+
86
+ function getContextPercent(stdinData) {
87
+ // Claude Code statusLine sends context_window.used_percentage (0-100)
88
+ if (stdinData.context_window && stdinData.context_window.used_percentage != null) {
89
+ return Math.round(stdinData.context_window.used_percentage);
90
+ }
91
+ // Legacy field name
92
+ if (stdinData.context_usage_fraction != null) {
93
+ return Math.round(stdinData.context_usage_fraction * 100);
94
+ }
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * Build a horizontal bar using Unicode block characters.
100
+ * Width is in character cells. Color shifts green -> yellow -> red.
101
+ *
102
+ * @param {number} percent - Usage percentage (0-100)
103
+ * @param {number} width - Bar width in characters
104
+ * @param {object} [opts] - Optional config overrides
105
+ * @param {object} [opts.thresholds] - { green: number, yellow: number }
106
+ * @param {object} [opts.chars] - { filled: string, empty: string }
107
+ */
108
+ function buildContextBar(percent, width, opts) {
109
+ if (width < 1) return '';
110
+ const thresholds = (opts && opts.thresholds) || DEFAULTS.context_bar.thresholds;
111
+ const chars = (opts && opts.chars) || DEFAULTS.context_bar.chars;
112
+
113
+ const filled = Math.round((percent / 100) * width);
114
+ const empty = width - filled;
115
+
116
+ // Color based on usage threshold
117
+ let barColor;
118
+ if (percent >= thresholds.yellow) barColor = c.boldRed;
119
+ else if (percent >= thresholds.green) barColor = c.boldYellow;
120
+ else barColor = c.boldGreen;
121
+
122
+ const filledStr = chars.filled.repeat(filled);
123
+ const emptyStr = chars.empty.repeat(empty);
124
+
125
+ return `${barColor}${filledStr}${c.dim}${emptyStr}${c.reset}`;
126
+ }
127
+
128
+ /**
129
+ * Pick a color for the phase status keyword.
130
+ */
131
+ function statusColor(statusText) {
132
+ const lower = statusText.toLowerCase();
133
+ if (lower.includes('complete') || lower.includes('verified')) return c.green;
134
+ if (lower.includes('progress') || lower.includes('building') || lower.includes('executing')) return c.yellow;
135
+ if (lower.includes('planned') || lower.includes('ready')) return c.cyan;
136
+ if (lower.includes('blocked') || lower.includes('failed')) return c.red;
137
+ return c.white;
138
+ }
139
+
140
+ /**
141
+ * Get current git branch and dirty status.
142
+ * Returns null if not in a git repo or git is unavailable.
143
+ */
144
+ function getGitInfo() {
145
+ try {
146
+ const branch = cp.execSync('git branch --show-current', {
147
+ timeout: 500, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
148
+ }).trim();
149
+ if (!branch) return null;
150
+ const porcelain = cp.execSync('git status --porcelain', {
151
+ timeout: 500, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
152
+ }).trim();
153
+ return { branch, dirty: porcelain.length > 0 };
154
+ } catch (_e) {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Format milliseconds into human-readable duration (e.g. "12m", "1h23m").
161
+ */
162
+ function formatDuration(ms) {
163
+ const totalSeconds = Math.floor(ms / 1000);
164
+ const hours = Math.floor(totalSeconds / 3600);
165
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
166
+ if (hours > 0) return `${hours}h${minutes > 0 ? minutes + 'm' : ''}`;
167
+ return `${minutes}m`;
168
+ }
169
+
170
+ function main() {
171
+ const stdinData = readStdin();
172
+ const cwd = process.cwd();
173
+ const planningDir = path.join(cwd, '.planning');
174
+ const stateFile = path.join(planningDir, 'STATE.md');
175
+
176
+ if (!fs.existsSync(stateFile)) {
177
+ process.exit(0);
178
+ }
179
+
180
+ try {
181
+ const slConfig = loadStatusLineConfig(planningDir);
182
+ const content = fs.readFileSync(stateFile, 'utf8');
183
+ const ctxPercent = getContextPercent(stdinData);
184
+ const status = buildStatusLine(content, ctxPercent, slConfig, stdinData);
185
+
186
+ if (status) {
187
+ process.stdout.write(status);
188
+ logHook('status-line', 'StatusLine', 'updated', { ctxPercent });
189
+ }
190
+ } catch (_e) {
191
+ logHook('status-line', 'StatusLine', 'error', { error: _e.message });
192
+ }
193
+
194
+ process.exit(0);
195
+ }
196
+
197
+ function buildStatusLine(content, ctxPercent, cfg, stdinData) {
198
+ const config = cfg || DEFAULTS;
199
+ const sections = config.sections || DEFAULTS.sections;
200
+ const brandText = config.brand_text || DEFAULTS.brand_text;
201
+ const maxLen = config.max_status_length || DEFAULTS.max_status_length;
202
+ const barCfg = config.context_bar || DEFAULTS.context_bar;
203
+ const sd = stdinData || {};
204
+
205
+ const parts = [];
206
+
207
+ // Phase section (always includes brand text)
208
+ if (sections.includes('phase')) {
209
+ const phaseMatch = content.match(/Phase:\s*(\d+)\s*of\s*(\d+)\s*(?:\(([^)]+)\))?/);
210
+ if (phaseMatch) {
211
+ parts.push(`${c.boldCyan}${brandText}${c.reset} ${c.bold}Phase ${phaseMatch[1]}/${phaseMatch[2]}${c.reset}`);
212
+ if (phaseMatch[3]) {
213
+ parts.push(`${c.magenta}${phaseMatch[3]}${c.reset}`);
214
+ }
215
+ } else {
216
+ parts.push(`${c.boldCyan}${brandText}${c.reset}`);
217
+ }
218
+ }
219
+
220
+ // Plan section
221
+ if (sections.includes('plan')) {
222
+ const planMatch = content.match(/Plan:\s*(\d+)\s*of\s*(\d+)/);
223
+ if (planMatch) {
224
+ const done = parseInt(planMatch[1], 10);
225
+ const total = parseInt(planMatch[2], 10);
226
+ const planColor = done === total ? c.green : c.white;
227
+ parts.push(`${planColor}Plan ${done}/${total}${c.reset}`);
228
+ }
229
+ }
230
+
231
+ // Status section
232
+ if (sections.includes('status')) {
233
+ const statusMatch = content.match(/Status:\s*(.+)/);
234
+ if (statusMatch) {
235
+ const text = statusMatch[1].trim();
236
+ const short = text.length > maxLen ? text.slice(0, maxLen - 3) + '...' : text;
237
+ parts.push(`${statusColor(text)}${short}${c.reset}`);
238
+ }
239
+ }
240
+
241
+ // Git section — branch name + dirty indicator
242
+ if (sections.includes('git')) {
243
+ const gitInfo = getGitInfo();
244
+ if (gitInfo) {
245
+ const dirtyMark = gitInfo.dirty ? `${c.yellow}*${c.reset}` : '';
246
+ parts.push(`${c.cyan}${gitInfo.branch}${c.reset}${dirtyMark}`);
247
+ }
248
+ }
249
+
250
+ // Model section — current model display name from stdin
251
+ if (sections.includes('model') && sd.model && sd.model.display_name) {
252
+ parts.push(`${c.dim}${sd.model.display_name}${c.reset}`);
253
+ }
254
+
255
+ // Cost section — session cost from stdin
256
+ if (sections.includes('cost') && sd.cost && sd.cost.total_cost_usd != null) {
257
+ const cost = sd.cost.total_cost_usd;
258
+ const costStr = `$${cost.toFixed(2)}`;
259
+ let costColor = c.dim;
260
+ if (cost > 5) costColor = c.red;
261
+ else if (cost > 1) costColor = c.yellow;
262
+ parts.push(`${costColor}${costStr}${c.reset}`);
263
+ }
264
+
265
+ // Duration section — session wall-clock time from stdin
266
+ if (sections.includes('duration') && sd.cost && sd.cost.total_duration_ms != null) {
267
+ parts.push(`${c.dim}${formatDuration(sd.cost.total_duration_ms)}${c.reset}`);
268
+ }
269
+
270
+ // Context bar section
271
+ if (sections.includes('context') && ctxPercent != null) {
272
+ const bar = buildContextBar(ctxPercent, barCfg.width || DEFAULTS.context_bar.width, {
273
+ thresholds: barCfg.thresholds || DEFAULTS.context_bar.thresholds,
274
+ chars: barCfg.chars || DEFAULTS.context_bar.chars
275
+ });
276
+ parts.push(`${bar} ${c.dim}${ctxPercent}%${c.reset}`);
277
+ }
278
+
279
+ if (parts.length === 0) return null;
280
+
281
+ return parts.join(` ${c.dim}\u2502${c.reset} `);
282
+ }
283
+
284
+ if (require.main === module) { main(); }
285
+ module.exports = { buildStatusLine, buildContextBar, getContextPercent, getGitInfo, formatDuration, loadStatusLineConfig, DEFAULTS };
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PostToolUse hook on Write|Edit: Tracks tool call count per session
5
+ * and suggests /compact when approaching context limits.
6
+ *
7
+ * Counter stored in .planning/.compact-counter (JSON).
8
+ * Threshold configurable via config.json hooks.compactThreshold (default: 50).
9
+ * After first suggestion, re-suggests every 25 calls.
10
+ * Counter resets on SessionStart (via progress-tracker.js).
11
+ *
12
+ * Exit codes:
13
+ * 0 = always (advisory only)
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { logHook } = require('./hook-logger');
19
+ const { configLoad } = require('./pbr-tools');
20
+
21
+ const DEFAULT_THRESHOLD = 50;
22
+ const REMINDER_INTERVAL = 25;
23
+
24
+ function main() {
25
+ process.stdin.setEncoding('utf8');
26
+ process.stdin.resume();
27
+ process.stdin.on('end', () => {
28
+ try {
29
+ const cwd = process.cwd();
30
+ const planningDir = path.join(cwd, '.planning');
31
+ if (!fs.existsSync(planningDir)) {
32
+ process.exit(0);
33
+ }
34
+
35
+ const result = checkCompaction(planningDir, cwd);
36
+ if (result) {
37
+ process.stdout.write(JSON.stringify(result));
38
+ }
39
+ process.exit(0);
40
+ } catch (_e) {
41
+ process.exit(0);
42
+ }
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Increment tool call counter and return a suggestion if threshold is reached.
48
+ * @param {string} planningDir - Path to .planning/ directory
49
+ * @param {string} cwd - Current working directory (for config loading)
50
+ * @returns {Object|null} Hook output with additionalContext, or null
51
+ */
52
+ function checkCompaction(planningDir, cwd) {
53
+ const counterPath = path.join(planningDir, '.compact-counter');
54
+ const counter = loadCounter(counterPath);
55
+ const threshold = getThreshold(cwd);
56
+
57
+ counter.count += 1;
58
+ saveCounter(counterPath, counter);
59
+
60
+ if (counter.count < threshold) return null;
61
+
62
+ const isFirstSuggestion = !counter.lastSuggested;
63
+ const callsSinceSuggestion = counter.count - (counter.lastSuggested || 0);
64
+
65
+ if (isFirstSuggestion || callsSinceSuggestion >= REMINDER_INTERVAL) {
66
+ counter.lastSuggested = counter.count;
67
+ saveCounter(counterPath, counter);
68
+
69
+ logHook('suggest-compact', 'PostToolUse', 'suggest', {
70
+ count: counter.count,
71
+ threshold
72
+ });
73
+
74
+ return {
75
+ additionalContext: `[Context Budget] ${counter.count} tool calls this session (threshold: ${threshold}). Consider running /compact to free context space before quality degrades.`
76
+ };
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ function loadCounter(counterPath) {
83
+ try {
84
+ const content = fs.readFileSync(counterPath, 'utf8');
85
+ const data = JSON.parse(content);
86
+ return { count: data.count || 0, lastSuggested: data.lastSuggested || 0 };
87
+ } catch (_e) {
88
+ return { count: 0, lastSuggested: 0 };
89
+ }
90
+ }
91
+
92
+ function saveCounter(counterPath, counter) {
93
+ try {
94
+ fs.writeFileSync(counterPath, JSON.stringify(counter), 'utf8');
95
+ } catch (_e) {
96
+ // Best-effort
97
+ }
98
+ }
99
+
100
+ function getThreshold(cwd) {
101
+ const planningDir = path.join(cwd, '.planning');
102
+ const config = configLoad(planningDir);
103
+ if (!config) return DEFAULT_THRESHOLD;
104
+ return config.hooks?.compactThreshold || DEFAULT_THRESHOLD;
105
+ }
106
+
107
+ function resetCounter(planningDir) {
108
+ const counterPath = path.join(planningDir, '.compact-counter');
109
+ try {
110
+ if (fs.existsSync(counterPath)) {
111
+ fs.unlinkSync(counterPath);
112
+ }
113
+ } catch (_e) {
114
+ // Best-effort
115
+ }
116
+ }
117
+
118
+ module.exports = { checkCompaction, loadCounter, saveCounter, getThreshold, resetCounter, DEFAULT_THRESHOLD, REMINDER_INTERVAL };
119
+ if (require.main === module) { main(); }
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * TaskCompleted hook: Logs agent task completion with output summary.
5
+ *
6
+ * Fires when a Task() sub-agent finishes (distinct from SubagentStop).
7
+ * Logs the completion event and agent type for workflow tracking.
8
+ *
9
+ * Non-blocking — exits 0 always.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const { logHook } = require('./hook-logger');
14
+ const { logEvent } = require('./event-logger');
15
+
16
+ function readStdin() {
17
+ try {
18
+ const input = fs.readFileSync(0, 'utf8').trim();
19
+ if (input) return JSON.parse(input);
20
+ } catch (_e) {
21
+ // empty or non-JSON stdin
22
+ }
23
+ return {};
24
+ }
25
+
26
+ function main() {
27
+ const data = readStdin();
28
+
29
+ logHook('task-completed', 'TaskCompleted', 'completed', {
30
+ agent_type: data.agent_type || data.subagent_type || null,
31
+ agent_id: data.agent_id || null,
32
+ duration_ms: data.duration_ms || null
33
+ });
34
+
35
+ logEvent('agent', 'task-completed', {
36
+ agent_type: data.agent_type || data.subagent_type || null,
37
+ agent_id: data.agent_id || null,
38
+ duration_ms: data.duration_ms || null
39
+ });
40
+
41
+ process.exit(0);
42
+ }
43
+
44
+ if (require.main === module) { main(); }
45
+ module.exports = { main };