claude-coder 1.8.3 → 1.9.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/README.md +59 -12
- package/bin/cli.js +20 -37
- package/package.json +4 -1
- package/recipes/_shared/roles/developer.md +11 -0
- package/recipes/_shared/roles/product.md +12 -0
- package/recipes/_shared/roles/tester.md +12 -0
- package/recipes/_shared/test/report-format.md +86 -0
- package/recipes/backend/base.md +27 -0
- package/recipes/backend/components/auth.md +18 -0
- package/recipes/backend/components/crud-api.md +18 -0
- package/recipes/backend/components/file-service.md +15 -0
- package/recipes/backend/manifest.json +20 -0
- package/recipes/backend/test/api-test.md +25 -0
- package/recipes/console/base.md +37 -0
- package/recipes/console/components/modal-form.md +20 -0
- package/recipes/console/components/pagination.md +17 -0
- package/recipes/console/components/search.md +17 -0
- package/recipes/console/components/table-list.md +18 -0
- package/recipes/console/components/tabs.md +14 -0
- package/recipes/console/components/tree.md +15 -0
- package/recipes/console/components/upload.md +15 -0
- package/recipes/console/manifest.json +24 -0
- package/recipes/console/test/crud-e2e.md +47 -0
- package/recipes/h5/base.md +26 -0
- package/recipes/h5/components/animation.md +11 -0
- package/recipes/h5/components/countdown.md +11 -0
- package/recipes/h5/components/share.md +11 -0
- package/recipes/h5/components/swiper.md +11 -0
- package/recipes/h5/manifest.json +21 -0
- package/recipes/h5/test/h5-e2e.md +20 -0
- package/src/commands/auth.js +87 -15
- package/src/commands/setup-modules/helpers.js +4 -3
- package/src/commands/setup-modules/mcp.js +44 -24
- package/src/commands/setup-modules/safety.js +1 -15
- package/src/commands/setup.js +8 -8
- package/src/common/assets.js +10 -1
- package/src/common/config.js +2 -2
- package/src/common/indicator.js +158 -120
- package/src/common/utils.js +60 -8
- package/src/core/coding.js +16 -38
- package/src/core/go.js +31 -77
- package/src/core/hooks.js +56 -89
- package/src/core/init.js +94 -100
- package/src/core/plan.js +85 -223
- package/src/core/prompts.js +36 -16
- package/src/core/repair.js +7 -17
- package/src/core/runner.js +306 -43
- package/src/core/scan.js +38 -34
- package/src/core/session.js +253 -39
- package/src/core/simplify.js +45 -24
- package/src/core/state.js +105 -0
- package/src/index.js +76 -0
- package/templates/codingSystem.md +2 -2
- package/templates/codingUser.md +1 -1
- package/templates/guidance.json +22 -3
- package/templates/planSystem.md +2 -2
- package/templates/scanSystem.md +3 -3
- package/templates/scanUser.md +1 -1
- package/templates/web-testing.md +17 -0
- package/types/index.d.ts +217 -0
- package/src/core/context.js +0 -117
- package/src/core/harness.js +0 -484
- package/src/core/query.js +0 -50
- package/templates/playwright.md +0 -17
package/src/core/go.js
CHANGED
|
@@ -3,20 +3,15 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
-
const { runSession } = require('./session');
|
|
7
|
-
const { buildQueryOptions } = require('./query');
|
|
8
6
|
const { buildSystemPrompt } = require('./prompts');
|
|
9
|
-
const { log
|
|
7
|
+
const { log } = require('../common/config');
|
|
10
8
|
const { assets } = require('../common/assets');
|
|
11
9
|
const { extractResultText } = require('../common/logging');
|
|
12
|
-
const { loadState } = require('./
|
|
10
|
+
const { loadState, saveState } = require('./state');
|
|
11
|
+
const { Session } = require('./session');
|
|
13
12
|
|
|
14
13
|
const GO_DIR_NAME = 'go';
|
|
15
14
|
|
|
16
|
-
function getRecipesDir() {
|
|
17
|
-
return path.join(assets.projectRoot || process.cwd(), '.claude-coder', 'recipes');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
15
|
// ─── Go State (harness_state.json → go section) ──────────
|
|
21
16
|
|
|
22
17
|
function loadGoState() {
|
|
@@ -27,13 +22,13 @@ function loadGoState() {
|
|
|
27
22
|
function saveGoState(goData) {
|
|
28
23
|
const state = loadState();
|
|
29
24
|
state.go = { ...state.go, ...goData };
|
|
30
|
-
|
|
25
|
+
saveState(state);
|
|
31
26
|
}
|
|
32
27
|
|
|
33
28
|
// ─── Prompt Builder ───────────────────────────────────────
|
|
34
29
|
|
|
35
30
|
function buildGoPrompt(instruction, opts = {}) {
|
|
36
|
-
const recipesAbsPath =
|
|
31
|
+
const recipesAbsPath = assets.recipesDir();
|
|
37
32
|
const goState = loadGoState();
|
|
38
33
|
|
|
39
34
|
const inputSection = opts.reqFile
|
|
@@ -173,86 +168,49 @@ function writeGoFile(content) {
|
|
|
173
168
|
return filePath;
|
|
174
169
|
}
|
|
175
170
|
|
|
176
|
-
// ───
|
|
171
|
+
// ─── Main Entry ──────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
async function executeGo(config, input, opts = {}) {
|
|
174
|
+
const instruction = input || '';
|
|
175
|
+
|
|
176
|
+
if (opts.reset) {
|
|
177
|
+
saveGoState({});
|
|
178
|
+
log('ok', 'Go 记忆已重置');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (opts.reqFile) {
|
|
183
|
+
log('info', `需求文件: ${opts.reqFile}`);
|
|
184
|
+
}
|
|
177
185
|
|
|
178
|
-
async function _executeGoSession(instruction, opts = {}) {
|
|
179
186
|
const isAutoMode = !!(instruction || opts.reqFile);
|
|
187
|
+
const mode = isAutoMode ? '自动' : '对话';
|
|
188
|
+
log('info', `Go 模式: ${mode}`);
|
|
189
|
+
|
|
180
190
|
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
181
191
|
|
|
182
|
-
|
|
183
|
-
opts,
|
|
184
|
-
sessionNum: 0,
|
|
192
|
+
const result = await Session.run('go', config, {
|
|
185
193
|
logFileName: `go_${ts}.log`,
|
|
186
194
|
label: isAutoMode ? 'go_auto' : 'go_dialogue',
|
|
187
195
|
|
|
188
|
-
async execute(
|
|
196
|
+
async execute(session) {
|
|
189
197
|
log('info', isAutoMode ? '正在分析需求并组装方案...' : '正在启动对话式需求收集...');
|
|
190
198
|
|
|
191
199
|
const prompt = buildGoPrompt(instruction, opts);
|
|
192
|
-
const queryOpts = buildQueryOptions(
|
|
200
|
+
const queryOpts = session.buildQueryOptions(opts);
|
|
193
201
|
queryOpts.systemPrompt = buildSystemPrompt('go');
|
|
194
202
|
queryOpts.permissionMode = 'plan';
|
|
195
|
-
queryOpts.hooks = ctx.hooks;
|
|
196
|
-
queryOpts.abortController = ctx.abortController;
|
|
197
203
|
|
|
198
204
|
if (isAutoMode) {
|
|
199
205
|
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
200
206
|
}
|
|
201
207
|
|
|
202
|
-
const
|
|
203
|
-
const content = extractGoContent(
|
|
208
|
+
const { messages } = await session.runQuery(prompt, queryOpts);
|
|
209
|
+
const content = extractGoContent(messages);
|
|
204
210
|
|
|
205
|
-
return { content, collected };
|
|
211
|
+
return { content, collected: messages };
|
|
206
212
|
},
|
|
207
213
|
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ─── Main Entry ──────────────────────────────────────────
|
|
211
|
-
|
|
212
|
-
async function run(input, opts = {}) {
|
|
213
|
-
const instruction = input || '';
|
|
214
|
-
|
|
215
|
-
assets.ensureDirs();
|
|
216
|
-
|
|
217
|
-
const config = loadConfig();
|
|
218
|
-
if (!opts.model) {
|
|
219
|
-
opts.model = config.defaultOpus || config.model;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const displayModel = opts.model || config.model || '(default)';
|
|
223
|
-
log('ok', `模型配置已加载: ${config.provider || 'claude'} (go 使用: ${displayModel})`);
|
|
224
|
-
|
|
225
|
-
const recipesDir = getRecipesDir();
|
|
226
|
-
if (!fs.existsSync(recipesDir) || fs.readdirSync(recipesDir).length === 0) {
|
|
227
|
-
log('error', `食谱目录为空或不存在: ${recipesDir}`);
|
|
228
|
-
log('info', '请先运行 claude-coder init 初始化项目(会自动部署食谱)');
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// --reset: 清空 go 记忆
|
|
233
|
-
if (opts.reset) {
|
|
234
|
-
saveGoState({});
|
|
235
|
-
log('ok', 'Go 记忆已重置');
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// -r: 读取需求文件,自动模式
|
|
240
|
-
if (opts.readFile) {
|
|
241
|
-
const reqPath = path.resolve(assets.projectRoot, opts.readFile);
|
|
242
|
-
if (!fs.existsSync(reqPath)) {
|
|
243
|
-
log('error', `文件不存在: ${reqPath}`);
|
|
244
|
-
process.exit(1);
|
|
245
|
-
}
|
|
246
|
-
opts.reqFile = reqPath;
|
|
247
|
-
log('info', `需求文件: ${reqPath}`);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// 确定模式
|
|
251
|
-
const mode = (instruction || opts.reqFile) ? '自动' : '对话';
|
|
252
|
-
log('info', `Go 模式: ${mode}`);
|
|
253
|
-
|
|
254
|
-
// 执行 go session
|
|
255
|
-
const result = await _executeGoSession(instruction, opts);
|
|
256
214
|
|
|
257
215
|
if (!result.content) {
|
|
258
216
|
log('error', '无法从 AI 输出中提取方案内容');
|
|
@@ -260,7 +218,6 @@ async function run(input, opts = {}) {
|
|
|
260
218
|
return;
|
|
261
219
|
}
|
|
262
220
|
|
|
263
|
-
// 预览 + 确认 + 补充
|
|
264
221
|
const { confirmed, supplement } = await previewAndConfirm(result.content);
|
|
265
222
|
if (!confirmed) {
|
|
266
223
|
log('info', '已取消');
|
|
@@ -272,11 +229,9 @@ async function run(input, opts = {}) {
|
|
|
272
229
|
finalContent += `\n\n## 补充要求\n\n${supplement}`;
|
|
273
230
|
}
|
|
274
231
|
|
|
275
|
-
// 写入 .claude-coder/go/
|
|
276
232
|
const filePath = writeGoFile(finalContent);
|
|
277
233
|
log('ok', `方案已保存: ${filePath}`);
|
|
278
234
|
|
|
279
|
-
// 保存记忆到 harness_state.json
|
|
280
235
|
const domain = extractDomainFromContent(finalContent);
|
|
281
236
|
const components = extractComponentsFromContent(finalContent);
|
|
282
237
|
const history = (loadGoState().history || []).slice(-9);
|
|
@@ -295,16 +250,15 @@ async function run(input, opts = {}) {
|
|
|
295
250
|
history,
|
|
296
251
|
});
|
|
297
252
|
|
|
298
|
-
// 询问是否继续到 plan
|
|
299
253
|
console.log('');
|
|
300
254
|
const shouldPlan = await promptProceedToPlan();
|
|
301
255
|
if (shouldPlan) {
|
|
302
256
|
log('info', '开始生成计划并分解任务...');
|
|
303
|
-
const {
|
|
304
|
-
await
|
|
257
|
+
const { executePlan } = require('./plan');
|
|
258
|
+
await executePlan(config, '', { reqFile: filePath });
|
|
305
259
|
} else {
|
|
306
260
|
log('info', `方案已保存,稍后可使用: claude-coder plan -r ${filePath}`);
|
|
307
261
|
}
|
|
308
262
|
}
|
|
309
263
|
|
|
310
|
-
module.exports = {
|
|
264
|
+
module.exports = { executeGo };
|
package/src/core/hooks.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { inferPhaseStep } = require('../common/indicator');
|
|
6
6
|
const { log } = require('../common/config');
|
|
7
|
-
const { EDIT_THRESHOLD
|
|
7
|
+
const { EDIT_THRESHOLD } = require('../common/constants');
|
|
8
8
|
const { createAskUserQuestionHook } = require('../common/interaction');
|
|
9
9
|
const { assets } = require('../common/assets');
|
|
10
10
|
const { localTimestamp } = require('../common/utils');
|
|
@@ -13,13 +13,12 @@ const { localTimestamp } = require('../common/utils');
|
|
|
13
13
|
// ─────────────────────────────────────────────────────────────
|
|
14
14
|
|
|
15
15
|
const DEFAULT_EDIT_THRESHOLD = EDIT_THRESHOLD;
|
|
16
|
-
const SESSION_RESULT_FILENAME = FILES.SESSION_RESULT;
|
|
17
16
|
|
|
18
17
|
// Feature name constants
|
|
19
18
|
const FEATURES = Object.freeze({
|
|
20
19
|
GUIDANCE: 'guidance',
|
|
21
20
|
EDIT_GUARD: 'editGuard',
|
|
22
|
-
|
|
21
|
+
STOP: 'stop',
|
|
23
22
|
STALL: 'stall',
|
|
24
23
|
INTERACTION: 'interaction',
|
|
25
24
|
});
|
|
@@ -249,19 +248,6 @@ function logToolCall(logStream, input) {
|
|
|
249
248
|
}
|
|
250
249
|
}
|
|
251
250
|
|
|
252
|
-
function isSessionResultWrite(toolName, toolInput) {
|
|
253
|
-
if (toolName === 'Write') {
|
|
254
|
-
const target = toolInput?.file_path || toolInput?.path || '';
|
|
255
|
-
return target.endsWith(SESSION_RESULT_FILENAME);
|
|
256
|
-
}
|
|
257
|
-
if (toolName === 'Bash' || toolName === 'Shell') {
|
|
258
|
-
const cmd = toolInput?.command || '';
|
|
259
|
-
if (!cmd.includes(SESSION_RESULT_FILENAME)) return false;
|
|
260
|
-
return />\s*[^\s]*session_result/.test(cmd);
|
|
261
|
-
}
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
251
|
// ─────────────────────────────────────────────────────────────
|
|
266
252
|
// Module Factories
|
|
267
253
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -315,40 +301,18 @@ function createEditGuardModule(options) {
|
|
|
315
301
|
}
|
|
316
302
|
|
|
317
303
|
/**
|
|
318
|
-
* Create stall detection module (
|
|
304
|
+
* Create stall detection module (idle timeout only)
|
|
319
305
|
*/
|
|
320
306
|
function createStallModule(indicator, logStream, options) {
|
|
321
307
|
let stallDetected = false;
|
|
322
308
|
let stallChecker = null;
|
|
323
309
|
const timeoutMs = options.stallTimeoutMs || 1200000;
|
|
324
|
-
const completionTimeoutMs = options.completionTimeoutMs || 300000;
|
|
325
310
|
const abortController = options.abortController;
|
|
326
|
-
let completionDetectedAt = 0;
|
|
327
311
|
|
|
328
312
|
const checkStall = () => {
|
|
329
313
|
const now = Date.now();
|
|
330
314
|
const idleMs = now - indicator.lastActivityTime;
|
|
331
315
|
|
|
332
|
-
// Priority: completion timeout
|
|
333
|
-
if (completionDetectedAt > 0) {
|
|
334
|
-
const sinceCompletion = now - completionDetectedAt;
|
|
335
|
-
if (sinceCompletion > completionTimeoutMs && !stallDetected) {
|
|
336
|
-
stallDetected = true;
|
|
337
|
-
const shortMin = Math.ceil(completionTimeoutMs / 60000);
|
|
338
|
-
const actualMin = Math.floor(sinceCompletion / 60000);
|
|
339
|
-
log('warn', `\nsession_result 已写入 ${actualMin} 分钟,超过 ${shortMin} 分钟上限,自动中断`);
|
|
340
|
-
if (logStream) {
|
|
341
|
-
logStream.write(`\n[${localTimestamp()}] STALL: session_result 写入后 ${actualMin} 分钟(上限 ${shortMin} 分钟),自动中断\n`);
|
|
342
|
-
}
|
|
343
|
-
if (abortController) {
|
|
344
|
-
abortController.abort();
|
|
345
|
-
log('warn', '\n已发送中断信号');
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Normal stall detection
|
|
352
316
|
if (idleMs > timeoutMs && !stallDetected) {
|
|
353
317
|
stallDetected = true;
|
|
354
318
|
const idleMin = Math.floor(idleMs / 60000);
|
|
@@ -366,39 +330,34 @@ function createStallModule(indicator, logStream, options) {
|
|
|
366
330
|
stallChecker = setInterval(checkStall, 30000);
|
|
367
331
|
|
|
368
332
|
return {
|
|
369
|
-
setCompletionDetected: () => { completionDetectedAt = Date.now(); },
|
|
370
|
-
onCompletionDetected: () => {
|
|
371
|
-
completionDetectedAt = Date.now();
|
|
372
|
-
const shortMin = Math.ceil(completionTimeoutMs / 60000);
|
|
373
|
-
indicator.setCompletionDetected(shortMin);
|
|
374
|
-
log('info', '');
|
|
375
|
-
log('info', `检测到 session_result 写入,${shortMin} 分钟内模型未终止将自动中断`);
|
|
376
|
-
if (logStream) {
|
|
377
|
-
logStream.write(`\n[${localTimestamp()}] COMPLETION_DETECTED: session_result.json written, ${shortMin}min grace period\n`);
|
|
378
|
-
}
|
|
379
|
-
},
|
|
380
333
|
cleanup: () => { if (stallChecker) clearInterval(stallChecker); },
|
|
381
334
|
isStalled: () => stallDetected
|
|
382
335
|
};
|
|
383
336
|
}
|
|
384
337
|
|
|
385
338
|
/**
|
|
386
|
-
* Create
|
|
387
|
-
*
|
|
339
|
+
* Create Stop hook — per-turn activity logger.
|
|
340
|
+
* Stop fires on EVERY model response turn (not just session end).
|
|
341
|
+
* For session completion detection, use the result message (SDKResultMessage.subtype).
|
|
388
342
|
*/
|
|
389
|
-
function
|
|
390
|
-
return {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
indicator.updatePhase('thinking');
|
|
394
|
-
indicator.updateStep('');
|
|
395
|
-
indicator.toolTarget = '';
|
|
396
|
-
|
|
397
|
-
if (isSessionResultWrite(input.tool_name, input.tool_input)) {
|
|
398
|
-
stallModule.onCompletionDetected();
|
|
399
|
-
}
|
|
400
|
-
return {};
|
|
343
|
+
function createStopHook(logStream) {
|
|
344
|
+
return async (_input) => {
|
|
345
|
+
if (logStream?.writable) {
|
|
346
|
+
logStream.write(`[${localTimestamp()}] STOP: turn completed\n`);
|
|
401
347
|
}
|
|
348
|
+
return {};
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Create PostToolUse hook — resets tool running state and activity timer.
|
|
354
|
+
* Unified for all session types (replaces the former createCompletionModule).
|
|
355
|
+
*/
|
|
356
|
+
function createEndToolHook(indicator) {
|
|
357
|
+
return async (_input, _toolUseID, _context) => {
|
|
358
|
+
indicator.endTool();
|
|
359
|
+
indicator.updatePhase('thinking');
|
|
360
|
+
return {};
|
|
402
361
|
};
|
|
403
362
|
}
|
|
404
363
|
|
|
@@ -428,13 +387,13 @@ function createLoggingHook(indicator, logStream) {
|
|
|
428
387
|
// ─────────────────────────────────────────────────────────────
|
|
429
388
|
|
|
430
389
|
const FEATURE_MAP = {
|
|
431
|
-
coding: [FEATURES.GUIDANCE, FEATURES.EDIT_GUARD, FEATURES.
|
|
432
|
-
plan: [FEATURES.STALL],
|
|
433
|
-
plan_interactive: [FEATURES.STALL, FEATURES.INTERACTION],
|
|
434
|
-
scan: [FEATURES.STALL],
|
|
435
|
-
add: [FEATURES.STALL],
|
|
436
|
-
simplify: [FEATURES.STALL, FEATURES.INTERACTION],
|
|
437
|
-
go: [FEATURES.STALL, FEATURES.INTERACTION],
|
|
390
|
+
coding: [FEATURES.GUIDANCE, FEATURES.EDIT_GUARD, FEATURES.STOP, FEATURES.STALL],
|
|
391
|
+
plan: [FEATURES.STOP, FEATURES.STALL],
|
|
392
|
+
plan_interactive: [FEATURES.STOP, FEATURES.STALL, FEATURES.INTERACTION],
|
|
393
|
+
scan: [FEATURES.STOP, FEATURES.STALL],
|
|
394
|
+
add: [FEATURES.STOP, FEATURES.STALL],
|
|
395
|
+
simplify: [FEATURES.STOP, FEATURES.STALL, FEATURES.INTERACTION],
|
|
396
|
+
go: [FEATURES.STOP, FEATURES.STALL, FEATURES.INTERACTION],
|
|
438
397
|
custom: null
|
|
439
398
|
};
|
|
440
399
|
|
|
@@ -452,12 +411,12 @@ function createHooks(type, indicator, logStream, options = {}) {
|
|
|
452
411
|
modules.stall = createStallModule(indicator, logStream, options);
|
|
453
412
|
}
|
|
454
413
|
|
|
455
|
-
if (features.includes(FEATURES.
|
|
456
|
-
modules.
|
|
414
|
+
if (features.includes(FEATURES.STOP)) {
|
|
415
|
+
modules.stopHook = createStopHook(logStream);
|
|
457
416
|
}
|
|
458
417
|
|
|
459
|
-
if (features.includes(FEATURES.
|
|
460
|
-
modules.
|
|
418
|
+
if (features.includes(FEATURES.EDIT_GUARD)) {
|
|
419
|
+
modules.editGuard = createEditGuardModule(options);
|
|
461
420
|
}
|
|
462
421
|
|
|
463
422
|
if (features.includes(FEATURES.GUIDANCE)) {
|
|
@@ -484,15 +443,10 @@ function createHooks(type, indicator, logStream, options = {}) {
|
|
|
484
443
|
preToolUseHooks.push(modules.interaction.hook);
|
|
485
444
|
}
|
|
486
445
|
|
|
487
|
-
//
|
|
488
|
-
const
|
|
489
|
-
if (modules.completion) {
|
|
490
|
-
postToolUseHooks.push(modules.completion.hook);
|
|
491
|
-
} else {
|
|
492
|
-
postToolUseHooks.push(createFailureHook(indicator));
|
|
493
|
-
}
|
|
446
|
+
// PostToolUse: unified endTool for all session types
|
|
447
|
+
const endToolHook = createEndToolHook(indicator);
|
|
494
448
|
|
|
495
|
-
// PostToolUseFailure
|
|
449
|
+
// PostToolUseFailure: ensure endTool even on tool errors
|
|
496
450
|
const failureHook = createFailureHook(indicator);
|
|
497
451
|
|
|
498
452
|
// Build hooks object
|
|
@@ -500,11 +454,24 @@ function createHooks(type, indicator, logStream, options = {}) {
|
|
|
500
454
|
if (preToolUseHooks.length > 0) {
|
|
501
455
|
hooks.PreToolUse = [{ matcher: '*', hooks: preToolUseHooks }];
|
|
502
456
|
}
|
|
503
|
-
|
|
504
|
-
hooks.PostToolUse = [{ matcher: '*', hooks: postToolUseHooks }];
|
|
505
|
-
}
|
|
457
|
+
hooks.PostToolUse = [{ matcher: '*', hooks: [endToolHook] }];
|
|
506
458
|
hooks.PostToolUseFailure = [{ matcher: '*', hooks: [failureHook] }];
|
|
507
459
|
|
|
460
|
+
// Stop hook: per-turn activity logger
|
|
461
|
+
if (modules.stopHook) {
|
|
462
|
+
hooks.Stop = [{ hooks: [modules.stopHook] }];
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// SessionStart hook: log query lifecycle
|
|
466
|
+
const sessionStartHook = async (input) => {
|
|
467
|
+
indicator.updateActivity();
|
|
468
|
+
if (logStream?.writable) {
|
|
469
|
+
logStream.write(`[${localTimestamp()}] SESSION_START: source=${input.source || 'unknown'}\n`);
|
|
470
|
+
}
|
|
471
|
+
return {};
|
|
472
|
+
};
|
|
473
|
+
hooks.SessionStart = [{ hooks: [sessionStartHook] }];
|
|
474
|
+
|
|
508
475
|
// Cleanup functions
|
|
509
476
|
const cleanupFns = [];
|
|
510
477
|
if (modules.stall) {
|
|
@@ -514,7 +481,7 @@ function createHooks(type, indicator, logStream, options = {}) {
|
|
|
514
481
|
return {
|
|
515
482
|
hooks,
|
|
516
483
|
cleanup: () => cleanupFns.forEach(fn => fn()),
|
|
517
|
-
isStalled: () => modules.stall?.isStalled() || false
|
|
484
|
+
isStalled: () => modules.stall?.isStalled() || false,
|
|
518
485
|
};
|
|
519
486
|
}
|
|
520
487
|
|
|
@@ -527,8 +494,8 @@ module.exports = {
|
|
|
527
494
|
GuidanceInjector,
|
|
528
495
|
createGuidanceModule,
|
|
529
496
|
createEditGuardModule,
|
|
530
|
-
|
|
497
|
+
createStopHook,
|
|
498
|
+
createEndToolHook,
|
|
531
499
|
createStallModule,
|
|
532
500
|
FEATURES,
|
|
533
|
-
isSessionResultWrite,
|
|
534
501
|
};
|