@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,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 };
|