@sienklogic/plan-build-run 2.61.1 → 2.62.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 +12 -0
- package/package.json +2 -2
- package/plugins/copilot-pbr/hooks/hooks.json +24 -24
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-pbr/hooks/hooks.json +12 -12
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/hooks/hooks.json +12 -12
- package/plugins/pbr/scripts/check-config-change.js +26 -1
- package/plugins/pbr/scripts/check-subagent-output.js +89 -1
- package/plugins/pbr/scripts/config-schema.json +24 -0
- package/plugins/pbr/scripts/context-bridge.js +44 -0
- package/plugins/pbr/scripts/context-budget-check.js +65 -1
- package/plugins/pbr/scripts/event-handler.js +49 -1
- package/plugins/pbr/scripts/hook-server-client.js +213 -0
- package/plugins/pbr/scripts/hook-server.js +334 -0
- package/plugins/pbr/scripts/instructions-loaded.js +32 -1
- package/plugins/pbr/scripts/log-subagent.js +75 -1
- package/plugins/pbr/scripts/log-tool-failure.js +37 -0
- package/plugins/pbr/scripts/post-bash-triage.js +20 -1
- package/plugins/pbr/scripts/post-write-dispatch.js +117 -88
- package/plugins/pbr/scripts/progress-tracker.js +112 -3
- package/plugins/pbr/scripts/session-cleanup.js +36 -1
- package/plugins/pbr/scripts/task-completed.js +35 -1
- package/plugins/pbr/scripts/track-context-budget.js +165 -115
- package/plugins/pbr/scripts/worktree-create.js +49 -1
- package/plugins/pbr/scripts/worktree-remove.js +46 -1
|
@@ -28,6 +28,165 @@ const UNIQUE_FILE_MILESTONE = 10; // warn every 10 unique files
|
|
|
28
28
|
const CHAR_MILESTONE = 50000; // warn every 50k chars
|
|
29
29
|
const LARGE_FILE_THRESHOLD = 5000; // warn if single read > 5k chars
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Core event processing logic for track-context-budget.
|
|
33
|
+
* Extracted so it can be called directly by hook-server.js (HTTP mode)
|
|
34
|
+
* or via stdin in command mode.
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} data - Hook event data (tool_input, tool_output, etc.)
|
|
37
|
+
* @param {string} planningDir - Absolute path to .planning/ directory
|
|
38
|
+
* @param {Object} [opts] - Optional overrides
|
|
39
|
+
* @param {string} [opts.pluginRoot] - Override for CLAUDE_PLUGIN_ROOT
|
|
40
|
+
* @returns {{ additionalContext: string }|null} Warning output or null
|
|
41
|
+
*/
|
|
42
|
+
function processEvent(data, planningDir, opts) {
|
|
43
|
+
const filePath = data.tool_input?.file_path || '';
|
|
44
|
+
if (!filePath) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Skip plugin-internal files — these are loaded by the plugin system,
|
|
49
|
+
// not by the orchestrator, so they shouldn't count against context budget
|
|
50
|
+
const pluginRoot = (opts && opts.pluginRoot != null) ? opts.pluginRoot : (process.env.CLAUDE_PLUGIN_ROOT || '');
|
|
51
|
+
if (pluginRoot) {
|
|
52
|
+
const normalizedFile = path.resolve(filePath);
|
|
53
|
+
const normalizedPlugin = path.resolve(pluginRoot);
|
|
54
|
+
if (normalizedFile.startsWith(normalizedPlugin + path.sep) || normalizedFile === normalizedPlugin) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Estimate chars read from actual output or limit, with a conservative default.
|
|
60
|
+
// Previous default of 80k (2000 lines × 40 chars) caused every read to cross
|
|
61
|
+
// the 50k milestone, flooding logs with warnings on every single Read call.
|
|
62
|
+
const limit = data.tool_input?.limit;
|
|
63
|
+
const estimatedChars = limit ? limit * 40 : 8000;
|
|
64
|
+
// Use actual output length if available
|
|
65
|
+
const actualChars = data.tool_output ? String(data.tool_output).length : estimatedChars;
|
|
66
|
+
|
|
67
|
+
const trackerPath = path.join(planningDir, '.context-tracker');
|
|
68
|
+
const skillPath = path.join(planningDir, '.active-skill');
|
|
69
|
+
|
|
70
|
+
// Check if active skill changed (reset tracker)
|
|
71
|
+
const currentSkill = readFileSafe(skillPath);
|
|
72
|
+
let tracker = loadTracker(trackerPath);
|
|
73
|
+
|
|
74
|
+
if (tracker.skill !== currentSkill) {
|
|
75
|
+
tracker = { skill: currentSkill, reads: 0, total_chars: 0, files: [] };
|
|
76
|
+
} else if (tracker.files.length > 200) {
|
|
77
|
+
logHook('track-context-budget', 'PostToolUse', 'warn', {
|
|
78
|
+
reason: 'tracker reset at 200 files',
|
|
79
|
+
reads: tracker.reads,
|
|
80
|
+
total_chars: tracker.total_chars,
|
|
81
|
+
unique_files: tracker.files.length,
|
|
82
|
+
});
|
|
83
|
+
const prevCharsTotal = tracker.total_chars;
|
|
84
|
+
const resetWarning = {
|
|
85
|
+
additionalContext: `[Context Budget] Tracker reset: ${tracker.files.length} unique files read (~${Math.round(tracker.total_chars / 1000)}k chars). File list cleared but char total preserved. Consider delegating remaining work to a Task() subagent.`
|
|
86
|
+
};
|
|
87
|
+
tracker = { skill: currentSkill, reads: 0, total_chars: prevCharsTotal, files: [] };
|
|
88
|
+
// Save reset tracker before returning the warning
|
|
89
|
+
try {
|
|
90
|
+
const tmpPath = trackerPath + '.' + process.pid;
|
|
91
|
+
fs.writeFileSync(tmpPath, JSON.stringify(tracker), 'utf8');
|
|
92
|
+
fs.renameSync(tmpPath, trackerPath);
|
|
93
|
+
} catch (_e) {
|
|
94
|
+
try { fs.unlinkSync(trackerPath + '.' + process.pid); } catch (_e2) { /* best-effort cleanup */ }
|
|
95
|
+
}
|
|
96
|
+
return resetWarning;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Update tracker
|
|
100
|
+
const prevFileCount = tracker.files.length;
|
|
101
|
+
tracker.reads += 1;
|
|
102
|
+
tracker.total_chars += actualChars;
|
|
103
|
+
if (!tracker.files.includes(filePath)) {
|
|
104
|
+
tracker.files.push(filePath);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Save tracker (atomic write to avoid corruption from concurrent hooks)
|
|
108
|
+
try {
|
|
109
|
+
const tmpPath = trackerPath + '.' + process.pid;
|
|
110
|
+
fs.writeFileSync(tmpPath, JSON.stringify(tracker), 'utf8');
|
|
111
|
+
fs.renameSync(tmpPath, trackerPath);
|
|
112
|
+
} catch (_e) {
|
|
113
|
+
// Best-effort — clean up temp file if rename failed
|
|
114
|
+
try { fs.unlinkSync(trackerPath + '.' + process.pid); } catch (_e2) { /* best-effort cleanup */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check bridge file for tier-based context warnings
|
|
118
|
+
const bridgeTier = checkBridge(planningDir);
|
|
119
|
+
if (bridgeTier) {
|
|
120
|
+
// Bridge is fresh and providing tier warnings — skip heuristic milestones
|
|
121
|
+
// (the bridge's context-bridge.js already handles tier debounce)
|
|
122
|
+
logHook('track-context-budget', 'PostToolUse', 'bridge-active', {
|
|
123
|
+
reads: tracker.reads,
|
|
124
|
+
total_chars: tracker.total_chars,
|
|
125
|
+
tier: bridgeTier,
|
|
126
|
+
});
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check thresholds — only warn at milestone crossings, not every read
|
|
131
|
+
const warnings = [];
|
|
132
|
+
|
|
133
|
+
// Milestone: unique files read crosses a multiple of UNIQUE_FILE_MILESTONE
|
|
134
|
+
const curUniqueFiles = tracker.files.length;
|
|
135
|
+
if (curUniqueFiles >= UNIQUE_FILE_MILESTONE &&
|
|
136
|
+
Math.floor(curUniqueFiles / UNIQUE_FILE_MILESTONE) > Math.floor(prevFileCount / UNIQUE_FILE_MILESTONE)) {
|
|
137
|
+
warnings.push(`${curUniqueFiles} unique files read (milestone: every ${UNIQUE_FILE_MILESTONE})`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Milestone: total chars crosses a multiple of CHAR_MILESTONE
|
|
141
|
+
const prevChars = tracker.total_chars - actualChars;
|
|
142
|
+
if (tracker.total_chars >= CHAR_MILESTONE &&
|
|
143
|
+
Math.floor(tracker.total_chars / CHAR_MILESTONE) > Math.floor(prevChars / CHAR_MILESTONE)) {
|
|
144
|
+
const kChars = Math.round(tracker.total_chars / 1000);
|
|
145
|
+
warnings.push(`~${kChars}k chars read (milestone: every ${CHAR_MILESTONE / 1000}k)`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Single large file warning
|
|
149
|
+
if (actualChars >= LARGE_FILE_THRESHOLD) {
|
|
150
|
+
const kChars = Math.round(actualChars / 1000);
|
|
151
|
+
warnings.push(`large file read (~${kChars}k chars): ${path.basename(filePath)}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (warnings.length > 0) {
|
|
155
|
+
logHook('track-context-budget', 'PostToolUse', 'warn', {
|
|
156
|
+
reads: tracker.reads,
|
|
157
|
+
total_chars: tracker.total_chars,
|
|
158
|
+
unique_files: tracker.files.length,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
additionalContext: `[Context Budget Warning] ${warnings.join(', ')}. ${tracker.files.length} unique files read. Consider delegating remaining reads to a Task() subagent to protect orchestrator context.`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* HTTP handler for hook-server.js.
|
|
171
|
+
* Called directly instead of spawning a subprocess.
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} reqBody - Full hook request body { event, tool, data, planningDir, cache }
|
|
174
|
+
* @param {Object} _cache - Server in-memory cache (unused by this handler)
|
|
175
|
+
* @returns {{ additionalContext: string }|null}
|
|
176
|
+
*/
|
|
177
|
+
function handleHttp(reqBody, _cache) {
|
|
178
|
+
try {
|
|
179
|
+
const planningDir = reqBody.planningDir;
|
|
180
|
+
const data = reqBody.data || {};
|
|
181
|
+
if (!planningDir || !fs.existsSync(planningDir)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return processEvent(data, planningDir, {});
|
|
185
|
+
} catch (_e) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
31
190
|
function main() {
|
|
32
191
|
let input = '';
|
|
33
192
|
|
|
@@ -42,121 +201,12 @@ function main() {
|
|
|
42
201
|
}
|
|
43
202
|
|
|
44
203
|
const data = JSON.parse(input);
|
|
45
|
-
const
|
|
46
|
-
if (!filePath) {
|
|
47
|
-
process.exit(0);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Skip plugin-internal files — these are loaded by the plugin system,
|
|
51
|
-
// not by the orchestrator, so they shouldn't count against context budget
|
|
52
|
-
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || '';
|
|
53
|
-
if (pluginRoot) {
|
|
54
|
-
const normalizedFile = path.resolve(filePath);
|
|
55
|
-
const normalizedPlugin = path.resolve(pluginRoot);
|
|
56
|
-
if (normalizedFile.startsWith(normalizedPlugin + path.sep) || normalizedFile === normalizedPlugin) {
|
|
57
|
-
process.exit(0);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Estimate chars read from actual output or limit, with a conservative default.
|
|
62
|
-
// Previous default of 80k (2000 lines × 40 chars) caused every read to cross
|
|
63
|
-
// the 50k milestone, flooding logs with warnings on every single Read call.
|
|
64
|
-
const limit = data.tool_input?.limit;
|
|
65
|
-
const estimatedChars = limit ? limit * 40 : 8000;
|
|
66
|
-
// Use actual output length if available
|
|
67
|
-
const actualChars = data.tool_output ? String(data.tool_output).length : estimatedChars;
|
|
68
|
-
|
|
69
|
-
const trackerPath = path.join(planningDir, '.context-tracker');
|
|
70
|
-
const skillPath = path.join(planningDir, '.active-skill');
|
|
71
|
-
|
|
72
|
-
// Check if active skill changed (reset tracker)
|
|
73
|
-
const currentSkill = readFileSafe(skillPath);
|
|
74
|
-
let tracker = loadTracker(trackerPath);
|
|
75
|
-
|
|
76
|
-
if (tracker.skill !== currentSkill) {
|
|
77
|
-
tracker = { skill: currentSkill, reads: 0, total_chars: 0, files: [] };
|
|
78
|
-
} else if (tracker.files.length > 200) {
|
|
79
|
-
logHook('track-context-budget', 'PostToolUse', 'warn', {
|
|
80
|
-
reason: 'tracker reset at 200 files',
|
|
81
|
-
reads: tracker.reads,
|
|
82
|
-
total_chars: tracker.total_chars,
|
|
83
|
-
unique_files: tracker.files.length,
|
|
84
|
-
});
|
|
85
|
-
const prevCharsTotal = tracker.total_chars;
|
|
86
|
-
// Emit user-visible warning before resetting (was previously silent)
|
|
87
|
-
const resetWarning = {
|
|
88
|
-
additionalContext: `[Context Budget] Tracker reset: ${tracker.files.length} unique files read (~${Math.round(tracker.total_chars / 1000)}k chars). File list cleared but char total preserved. Consider delegating remaining work to a Task() subagent.`
|
|
89
|
-
};
|
|
90
|
-
process.stdout.write(JSON.stringify(resetWarning));
|
|
91
|
-
tracker = { skill: currentSkill, reads: 0, total_chars: prevCharsTotal, files: [] };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Update tracker
|
|
95
|
-
const prevFileCount = tracker.files.length;
|
|
96
|
-
tracker.reads += 1;
|
|
97
|
-
tracker.total_chars += actualChars;
|
|
98
|
-
if (!tracker.files.includes(filePath)) {
|
|
99
|
-
tracker.files.push(filePath);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Save tracker (atomic write to avoid corruption from concurrent hooks)
|
|
103
|
-
try {
|
|
104
|
-
const tmpPath = trackerPath + '.' + process.pid;
|
|
105
|
-
fs.writeFileSync(tmpPath, JSON.stringify(tracker), 'utf8');
|
|
106
|
-
fs.renameSync(tmpPath, trackerPath);
|
|
107
|
-
} catch (_e) {
|
|
108
|
-
// Best-effort — clean up temp file if rename failed
|
|
109
|
-
try { fs.unlinkSync(trackerPath + '.' + process.pid); } catch (_e2) { /* best-effort cleanup */ }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Check bridge file for tier-based context warnings
|
|
113
|
-
const bridgeTier = checkBridge(planningDir);
|
|
114
|
-
if (bridgeTier) {
|
|
115
|
-
// Bridge is fresh and providing tier warnings — skip heuristic milestones
|
|
116
|
-
// (the bridge's context-bridge.js already handles tier debounce)
|
|
117
|
-
logHook('track-context-budget', 'PostToolUse', 'bridge-active', {
|
|
118
|
-
reads: tracker.reads,
|
|
119
|
-
total_chars: tracker.total_chars,
|
|
120
|
-
tier: bridgeTier,
|
|
121
|
-
});
|
|
122
|
-
process.exit(0);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Check thresholds — only warn at milestone crossings, not every read
|
|
126
|
-
const warnings = [];
|
|
127
|
-
|
|
128
|
-
// Milestone: unique files read crosses a multiple of UNIQUE_FILE_MILESTONE
|
|
129
|
-
const curUniqueFiles = tracker.files.length;
|
|
130
|
-
if (curUniqueFiles >= UNIQUE_FILE_MILESTONE &&
|
|
131
|
-
Math.floor(curUniqueFiles / UNIQUE_FILE_MILESTONE) > Math.floor(prevFileCount / UNIQUE_FILE_MILESTONE)) {
|
|
132
|
-
warnings.push(`${curUniqueFiles} unique files read (milestone: every ${UNIQUE_FILE_MILESTONE})`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Milestone: total chars crosses a multiple of CHAR_MILESTONE
|
|
136
|
-
const prevChars = tracker.total_chars - actualChars;
|
|
137
|
-
if (tracker.total_chars >= CHAR_MILESTONE &&
|
|
138
|
-
Math.floor(tracker.total_chars / CHAR_MILESTONE) > Math.floor(prevChars / CHAR_MILESTONE)) {
|
|
139
|
-
const kChars = Math.round(tracker.total_chars / 1000);
|
|
140
|
-
warnings.push(`~${kChars}k chars read (milestone: every ${CHAR_MILESTONE / 1000}k)`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Single large file warning
|
|
144
|
-
if (actualChars >= LARGE_FILE_THRESHOLD) {
|
|
145
|
-
const kChars = Math.round(actualChars / 1000);
|
|
146
|
-
warnings.push(`large file read (~${kChars}k chars): ${path.basename(filePath)}`);
|
|
147
|
-
}
|
|
204
|
+
const result = processEvent(data, planningDir, {});
|
|
148
205
|
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
unique_files: tracker.files.length,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const output = {
|
|
157
|
-
additionalContext: `[Context Budget Warning] ${warnings.join(', ')}. ${tracker.files.length} unique files read. Consider delegating remaining reads to a Task() subagent to protect orchestrator context.`
|
|
158
|
-
};
|
|
159
|
-
process.stdout.write(JSON.stringify(output));
|
|
206
|
+
if (result) {
|
|
207
|
+
// In command mode: write reset warnings that were previously written inline
|
|
208
|
+
// processEvent returns them all, so just output here
|
|
209
|
+
process.stdout.write(JSON.stringify(result));
|
|
160
210
|
}
|
|
161
211
|
|
|
162
212
|
process.exit(0);
|
|
@@ -214,6 +264,6 @@ function checkBridge(planningDir) {
|
|
|
214
264
|
}
|
|
215
265
|
}
|
|
216
266
|
|
|
217
|
-
module.exports = { checkBridge, BRIDGE_STALENESS_MS };
|
|
267
|
+
module.exports = { checkBridge, BRIDGE_STALENESS_MS, processEvent, handleHttp };
|
|
218
268
|
|
|
219
269
|
if (require.main === module || process.argv[1] === __filename) { main(); }
|
|
@@ -89,5 +89,53 @@ initialized: ${new Date().toISOString()}
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* handleHttp — hook-server.js interface.
|
|
94
|
+
* reqBody = { event, tool, data, planningDir, cache }
|
|
95
|
+
* Returns { additionalContext: "..." } or null. Never calls process.exit().
|
|
96
|
+
*/
|
|
97
|
+
function handleHttp(reqBody) {
|
|
98
|
+
try {
|
|
99
|
+
const data = (reqBody && reqBody.data) || {};
|
|
100
|
+
const worktreePath = data.worktree_path || process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
101
|
+
const parentRoot = data.project_root || process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
102
|
+
|
|
103
|
+
const planningDir = path.join(worktreePath, '.planning');
|
|
104
|
+
const parentPlanningDir = path.join(parentRoot, '.planning');
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(parentPlanningDir)) {
|
|
107
|
+
logHook('worktree-create', 'WorktreeCreate', 'skip-no-parent', { worktree_path: worktreePath });
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (fs.existsSync(planningDir)) {
|
|
112
|
+
logHook('worktree-create', 'WorktreeCreate', 'skip-exists', { worktree_path: worktreePath });
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fs.mkdirSync(planningDir, { recursive: true });
|
|
117
|
+
fs.mkdirSync(path.join(planningDir, 'logs'), { recursive: true });
|
|
118
|
+
|
|
119
|
+
const stateMd = `# STATE\n\n## Current Position\nphase: (none)\nstatus: Worktree initialized — run /pbr:resume or /pbr:status for project state.\n\n## Source\nparent: ${parentRoot}\ninitialized: ${new Date().toISOString()}\n`;
|
|
120
|
+
fs.writeFileSync(path.join(planningDir, 'STATE.md'), stateMd, 'utf8');
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const srcConfig = path.join(parentPlanningDir, 'config.json');
|
|
124
|
+
const destConfig = path.join(planningDir, 'config.json');
|
|
125
|
+
if (fs.existsSync(srcConfig)) fs.copyFileSync(srcConfig, destConfig);
|
|
126
|
+
} catch (_e) { /* non-fatal */ }
|
|
127
|
+
|
|
128
|
+
logHook('worktree-create', 'WorktreeCreate', 'initialized', {
|
|
129
|
+
worktree_path: worktreePath,
|
|
130
|
+
parent_root: parentRoot
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return { additionalContext: '[Plan-Build-Run] Worktree .planning/ initialized. Run /pbr:status to see project state.' };
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logHook('worktree-create', 'WorktreeCreate', 'error', { error: err.message });
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
92
140
|
if (require.main === module || process.argv[1] === __filename) { main(); }
|
|
93
|
-
module.exports = { main };
|
|
141
|
+
module.exports = { main, handleHttp };
|
|
@@ -79,5 +79,50 @@ function main() {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* handleHttp — hook-server.js interface.
|
|
84
|
+
* reqBody = { event, tool, data, planningDir, cache }
|
|
85
|
+
* Returns null. Never calls process.exit().
|
|
86
|
+
*/
|
|
87
|
+
function handleHttp(reqBody) {
|
|
88
|
+
try {
|
|
89
|
+
const data = (reqBody && reqBody.data) || {};
|
|
90
|
+
const worktreePath = data.worktree_path || process.env.PBR_PROJECT_ROOT || process.cwd();
|
|
91
|
+
const planningDir = path.join(worktreePath, '.planning');
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(planningDir)) {
|
|
94
|
+
logHook('worktree-remove', 'WorktreeRemove', 'skip-no-planning', { worktree_path: worktreePath });
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const stateMdPath = path.join(planningDir, 'STATE.md');
|
|
99
|
+
if (fs.existsSync(stateMdPath)) {
|
|
100
|
+
const stateContent = fs.readFileSync(stateMdPath, 'utf8');
|
|
101
|
+
if (!stateContent.includes('parent:')) {
|
|
102
|
+
logHook('worktree-remove', 'WorktreeRemove', 'skip-not-worktree', { worktree_path: worktreePath });
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sessionFiles = ['.session.json', '.session-start', '.active-skill', '.auto-next'];
|
|
108
|
+
for (const filename of sessionFiles) {
|
|
109
|
+
try {
|
|
110
|
+
const filePath = path.join(planningDir, filename);
|
|
111
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
112
|
+
} catch (_e) { /* non-fatal */ }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
logHook('worktree-remove', 'WorktreeRemove', 'cleaned', {
|
|
116
|
+
worktree_path: worktreePath,
|
|
117
|
+
files_removed: ['session files']
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
logHook('worktree-remove', 'WorktreeRemove', 'error', { error: err.message });
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
82
127
|
if (require.main === module || process.argv[1] === __filename) { main(); }
|
|
83
|
-
module.exports = { main };
|
|
128
|
+
module.exports = { main, handleHttp };
|