claude-coder 1.8.4 → 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/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, loadConfig } = require('../common/config');
7
+ const { log } = require('../common/config');
10
8
  const { assets } = require('../common/assets');
11
9
  const { extractResultText } = require('../common/logging');
12
- const { loadState } = require('./harness');
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
- assets.writeJson('harnessState', state);
25
+ saveState(state);
31
26
  }
32
27
 
33
28
  // ─── Prompt Builder ───────────────────────────────────────
34
29
 
35
30
  function buildGoPrompt(instruction, opts = {}) {
36
- const recipesAbsPath = getRecipesDir();
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
- // ─── Session Execution ───────────────────────────────────
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
- return runSession('go', {
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(sdk, ctx) {
196
+ async execute(session) {
189
197
  log('info', isAutoMode ? '正在分析需求并组装方案...' : '正在启动对话式需求收集...');
190
198
 
191
199
  const prompt = buildGoPrompt(instruction, opts);
192
- const queryOpts = buildQueryOptions(ctx.config, opts);
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 collected = await ctx.runQuery(sdk, prompt, queryOpts);
203
- const content = extractGoContent(collected);
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 { run: planRun } = require('./plan');
304
- await planRun('', { reqFile: filePath, ...opts });
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 = { run };
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, FILES } = require('../common/constants');
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
- COMPLETION: 'completion',
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 (includes completion timeout handling)
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 completion detection module (PostToolUse hook)
387
- * endTool() resets toolRunning and refreshes lastActivityTime (countdown reset)
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 createCompletionModule(indicator, stallModule) {
390
- return {
391
- hook: async (input, _toolUseID, _context) => {
392
- indicator.endTool();
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.COMPLETION, FEATURES.STALL],
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.EDIT_GUARD)) {
456
- modules.editGuard = createEditGuardModule(options);
414
+ if (features.includes(FEATURES.STOP)) {
415
+ modules.stopHook = createStopHook(logStream);
457
416
  }
458
417
 
459
- if (features.includes(FEATURES.COMPLETION) && modules.stall) {
460
- modules.completion = createCompletionModule(indicator, modules.stall);
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
- // Assemble PostToolUse hooks (always include endTool for countdown reset)
488
- const postToolUseHooks = [];
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 hook: ensure endTool even on tool errors
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
- if (postToolUseHooks.length > 0) {
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
- createCompletionModule,
497
+ createStopHook,
498
+ createEndToolHook,
531
499
  createStallModule,
532
500
  FEATURES,
533
- isSessionResultWrite,
534
501
  };