claude-coder 1.5.1 → 1.5.2
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/docs/ARCHITECTURE.md +12 -17
- package/package.json +1 -1
- package/src/hooks.js +86 -0
- package/src/prompts.js +11 -1
- package/src/runner.js +25 -55
- package/src/session.js +72 -71
- package/src/tasks.js +17 -0
- package/templates/CLAUDE.md +2 -2
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -85,7 +85,7 @@ flowchart TB
|
|
|
85
85
|
|
|
86
86
|
subgraph SDK["Claude Agent SDK"]
|
|
87
87
|
query["query() 函数"]
|
|
88
|
-
hook_sys["PreToolUse hook<br
|
|
88
|
+
hook_sys["PreToolUse hook<br/>hooks.js 工厂"]
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
subgraph Files["文件系统 (.claude-coder/)"]
|
|
@@ -154,11 +154,12 @@ flowchart LR
|
|
|
154
154
|
## 3. 模块职责
|
|
155
155
|
|
|
156
156
|
```
|
|
157
|
-
bin/cli.js CLI
|
|
157
|
+
bin/cli.js CLI 入口:参数解析、命令路由
|
|
158
158
|
src/
|
|
159
159
|
config.js 配置管理:.env 加载、模型映射、环境变量构建、全局同步
|
|
160
160
|
runner.js 主循环:scan → session → validate → retry/rollback
|
|
161
|
-
session.js SDK 交互:query() 调用、
|
|
161
|
+
session.js SDK 交互:query() 调用、SDK 加载、日志流
|
|
162
|
+
hooks.js Hook 工厂:停顿检测 + 编辑死循环防护(可复用于所有 session 类型)
|
|
162
163
|
prompts.js 提示语构建:系统 prompt 组合 + 条件 hint + 任务分解指导
|
|
163
164
|
init.js 环境初始化:读取 profile 执行依赖安装、服务启动、健康检查
|
|
164
165
|
scanner.js 初始化扫描:调用 runScanSession + 重试
|
|
@@ -184,7 +185,8 @@ templates/
|
|
|
184
185
|
| `bin/cli.js` | CLI 入口 |
|
|
185
186
|
| `src/config.js` | .env 加载、模型映射 |
|
|
186
187
|
| `src/runner.js` | Harness 主循环 |
|
|
187
|
-
| `src/session.js` | SDK query() 封装 +
|
|
188
|
+
| `src/session.js` | SDK query() 封装 + 日志流 |
|
|
189
|
+
| `src/hooks.js` | Hook 工厂(停顿检测 + 编辑防护,可复用于所有 session 类型) |
|
|
188
190
|
| `src/prompts.js` | 提示语构建(系统 prompt + 条件 hint + 任务分解指导) |
|
|
189
191
|
| `src/init.js` | 环境初始化(依赖安装、服务启动) |
|
|
190
192
|
| `src/scanner.js` | 项目初始化扫描 |
|
|
@@ -250,7 +252,7 @@ flowchart TB
|
|
|
250
252
|
|---|---|---|---|
|
|
251
253
|
| **编码** | CLAUDE.md | `buildCodingPrompt()` + 11 个条件 hint | 主循环每次迭代 |
|
|
252
254
|
| **扫描** | CLAUDE.md + SCAN_PROTOCOL.md | `buildScanPrompt()` + 任务分解指导 + profile 质量要求 | 首次运行 |
|
|
253
|
-
| **追加** | CLAUDE.md | `buildAddPrompt()` + 任务分解指导 | `claude-coder add` |
|
|
255
|
+
| **追加** | 精简角色提示(不注入 CLAUDE.md) | `buildAddPrompt()` + 任务分解指导 + session_result 格式 | `claude-coder add` |
|
|
254
256
|
|
|
255
257
|
### 编码 Session 的 11 个条件 Hint
|
|
256
258
|
|
|
@@ -372,19 +374,19 @@ flowchart TD
|
|
|
372
374
|
end
|
|
373
375
|
|
|
374
376
|
subgraph after ["优化后:Harness 注入上下文"]
|
|
375
|
-
B1["Harness 预读文件"] --> B2["注入 Hint
|
|
376
|
-
B1 --> B3["注入 Hint
|
|
377
|
+
B1["Harness 预读文件"] --> B2["注入 Hint 6: 任务上下文"]
|
|
378
|
+
B1 --> B3["注入 Hint 7: 会话记忆"]
|
|
377
379
|
B2 --> B4["Agent prompt 就绪"]
|
|
378
380
|
B3 --> B4
|
|
379
381
|
B4 --> B5["Agent 直接开始编码"]
|
|
380
382
|
end
|
|
381
383
|
```
|
|
382
384
|
|
|
383
|
-
### Hint
|
|
385
|
+
### Hint 6: 任务上下文注入
|
|
384
386
|
|
|
385
387
|
Harness 在 `buildCodingPrompt()` 中预读 `tasks.json`,将下一个待办任务的 id、description、category、steps 数量和整体进度注入 user prompt。Agent 无需自行读取 `tasks.json`。
|
|
386
388
|
|
|
387
|
-
### Hint
|
|
389
|
+
### Hint 7: 会话记忆注入
|
|
388
390
|
|
|
389
391
|
Harness 在 `buildCodingPrompt()` 中预读 `session_result.json`,将上次会话的 task_id、结果和 notes 摘要注入 user prompt。Agent 无需自行读取历史 session 数据。
|
|
390
392
|
|
|
@@ -470,13 +472,6 @@ query({
|
|
|
470
472
|
|
|
471
473
|
### P0 — 近期
|
|
472
474
|
|
|
473
|
-
| 方向 | 说明 |
|
|
474
|
-
|------|------|
|
|
475
|
-
| **文件保护 Deny-list** | PreToolUse hook 拦截对保护文件的写入(比文字规则更硬性) |
|
|
476
|
-
| **成本预算控制** | `.env` 新增 `MAX_COST_USD`,超预算自动停止 |
|
|
477
|
-
|
|
478
|
-
### P1 — 中期
|
|
479
|
-
|
|
480
475
|
| 方向 | 说明 |
|
|
481
476
|
|------|------|
|
|
482
477
|
| **TCR 纪律** | Test && Commit \|\| Revert,可配置 strict/smart/off |
|
|
@@ -484,7 +479,7 @@ query({
|
|
|
484
479
|
| **Reminders 注入** | 用户自定义提醒文件,拼接到编码 prompt |
|
|
485
480
|
| **MCP 工具自动检测** | `claude mcp list` 自动启用已安装工具 |
|
|
486
481
|
|
|
487
|
-
###
|
|
482
|
+
### P1 — 远期
|
|
488
483
|
|
|
489
484
|
| 方向 | 说明 |
|
|
490
485
|
|------|------|
|
package/package.json
CHANGED
package/src/hooks.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { inferPhaseStep } = require('./indicator');
|
|
4
|
+
const { log } = require('./config');
|
|
5
|
+
|
|
6
|
+
const EDIT_THRESHOLD = 5;
|
|
7
|
+
|
|
8
|
+
function logToolCall(logStream, input) {
|
|
9
|
+
if (!logStream) return;
|
|
10
|
+
const target = input.tool_input?.file_path || input.tool_input?.path || '';
|
|
11
|
+
const cmd = input.tool_input?.command || '';
|
|
12
|
+
const pattern = input.tool_input?.pattern || '';
|
|
13
|
+
const detail = target || cmd.slice(0, 200) || (pattern ? `pattern: ${pattern}` : '');
|
|
14
|
+
if (detail) {
|
|
15
|
+
logStream.write(`[${new Date().toISOString()}] ${input.tool_name}: ${detail}\n`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create unified session hooks with configurable stall detection and edit guard.
|
|
21
|
+
* @param {Indicator} indicator
|
|
22
|
+
* @param {WriteStream|null} logStream
|
|
23
|
+
* @param {object} [options]
|
|
24
|
+
* @param {boolean} [options.enableStallDetection=false]
|
|
25
|
+
* @param {number} [options.stallTimeoutMs=1800000]
|
|
26
|
+
* @param {boolean} [options.enableEditGuard=false]
|
|
27
|
+
* @returns {{ hooks: object, cleanup: () => void, isStalled: () => boolean }}
|
|
28
|
+
*/
|
|
29
|
+
function createSessionHooks(indicator, logStream, options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
enableStallDetection = false,
|
|
32
|
+
stallTimeoutMs = 1800000,
|
|
33
|
+
enableEditGuard = false,
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
const editCounts = {};
|
|
37
|
+
let stallDetected = false;
|
|
38
|
+
let stallChecker = null;
|
|
39
|
+
|
|
40
|
+
if (enableStallDetection) {
|
|
41
|
+
stallChecker = setInterval(() => {
|
|
42
|
+
const idleMs = Date.now() - indicator.lastToolTime;
|
|
43
|
+
if (idleMs > stallTimeoutMs && !stallDetected) {
|
|
44
|
+
stallDetected = true;
|
|
45
|
+
const idleMin = Math.floor(idleMs / 60000);
|
|
46
|
+
log('warn', `无新工具调用超过 ${idleMin} 分钟,自动中断 session`);
|
|
47
|
+
if (logStream) {
|
|
48
|
+
logStream.write(`[${new Date().toISOString()}] STALL: 无工具调用 ${idleMin} 分钟,自动中断\n`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, 30000);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const hooks = {
|
|
55
|
+
PreToolUse: [{
|
|
56
|
+
matcher: '*',
|
|
57
|
+
hooks: [async (input) => {
|
|
58
|
+
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
59
|
+
logToolCall(logStream, input);
|
|
60
|
+
|
|
61
|
+
if (enableEditGuard) {
|
|
62
|
+
const target = input.tool_input?.file_path || input.tool_input?.path || '';
|
|
63
|
+
if (['Write', 'Edit', 'MultiEdit'].includes(input.tool_name) && target) {
|
|
64
|
+
editCounts[target] = (editCounts[target] || 0) + 1;
|
|
65
|
+
if (editCounts[target] > EDIT_THRESHOLD) {
|
|
66
|
+
return {
|
|
67
|
+
decision: 'block',
|
|
68
|
+
message: `已对 ${target} 编辑 ${editCounts[target]} 次,疑似死循环。请重新审视方案后再继续。`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {};
|
|
75
|
+
}]
|
|
76
|
+
}]
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
hooks,
|
|
81
|
+
cleanup() { if (stallChecker) clearInterval(stallChecker); },
|
|
82
|
+
isStalled() { return stallDetected; },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { createSessionHooks };
|
package/src/prompts.js
CHANGED
|
@@ -209,6 +209,15 @@ function buildScanPrompt(projectType, requirement) {
|
|
|
209
209
|
].join('\n');
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Build lightweight system prompt for add sessions.
|
|
214
|
+
* Add sessions only decompose requirements — no coding workflow needed.
|
|
215
|
+
* CLAUDE.md is NOT injected to avoid role conflict and save ~2000 tokens.
|
|
216
|
+
*/
|
|
217
|
+
function buildAddSystemPrompt() {
|
|
218
|
+
return '你是一个任务分解专家,擅长将模糊需求拆解为结构化、可执行的原子任务。你只分析需求和分解任务,不实现任何代码。';
|
|
219
|
+
}
|
|
220
|
+
|
|
212
221
|
/**
|
|
213
222
|
* Build user prompt for add sessions.
|
|
214
223
|
* Structure: Role (primacy) → Context → CoT → TaskGuide → Instruction (recency)
|
|
@@ -276,7 +285,7 @@ function buildAddPrompt(instruction) {
|
|
|
276
285
|
'5. 分解任务:每个任务对应一个独立可测试的功能单元,description 简明(40字内),steps 具体可操作',
|
|
277
286
|
'6. 追加到 tasks.json,id 和 priority 从已有最大值递增,status: pending',
|
|
278
287
|
'7. git add -A && git commit -m "chore: add new tasks"',
|
|
279
|
-
'8. 写入 session_result.json',
|
|
288
|
+
'8. 写入 session_result.json(格式:{ "session_result": "success", "task_id": "add-tasks", "status_before": "N/A", "status_after": "N/A", "git_commit": "hash", "tests_passed": false, "notes": "追加了 N 个任务:简述" })',
|
|
280
289
|
'',
|
|
281
290
|
|
|
282
291
|
// --- Quality constraints ---
|
|
@@ -295,5 +304,6 @@ module.exports = {
|
|
|
295
304
|
buildCodingPrompt,
|
|
296
305
|
buildTaskGuide,
|
|
297
306
|
buildScanPrompt,
|
|
307
|
+
buildAddSystemPrompt,
|
|
298
308
|
buildAddPrompt,
|
|
299
309
|
};
|
package/src/runner.js
CHANGED
|
@@ -5,35 +5,15 @@ const path = require('path');
|
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
7
|
const { paths, log, COLOR, loadConfig, ensureLoopDir, getProjectRoot } = require('./config');
|
|
8
|
-
const { loadTasks,
|
|
8
|
+
const { loadTasks, getFeatures, getStats, findNextTask, forceStatus } = require('./tasks');
|
|
9
9
|
const { validate } = require('./validator');
|
|
10
10
|
const { scan } = require('./scanner');
|
|
11
|
-
const { runCodingSession, runAddSession } = require('./session');
|
|
11
|
+
const { loadSDK, runCodingSession, runAddSession } = require('./session');
|
|
12
12
|
|
|
13
13
|
const MAX_RETRY = 3;
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const attempts = [
|
|
18
|
-
() => { require.resolve(pkgName); return true; },
|
|
19
|
-
() => {
|
|
20
|
-
const { createRequire } = require('module');
|
|
21
|
-
createRequire(__filename).resolve(pkgName);
|
|
22
|
-
return true;
|
|
23
|
-
},
|
|
24
|
-
() => {
|
|
25
|
-
const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
|
|
26
|
-
const sdkPath = path.join(prefix, 'lib', 'node_modules', pkgName);
|
|
27
|
-
if (fs.existsSync(sdkPath)) return true;
|
|
28
|
-
throw new Error('not found');
|
|
29
|
-
},
|
|
30
|
-
];
|
|
31
|
-
for (const attempt of attempts) {
|
|
32
|
-
try { if (attempt()) return; } catch { /* try next */ }
|
|
33
|
-
}
|
|
34
|
-
console.error(`错误:未找到 ${pkgName}`);
|
|
35
|
-
console.error(`请先安装:npm install -g ${pkgName}`);
|
|
36
|
-
process.exit(1);
|
|
15
|
+
function sleep(ms) {
|
|
16
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
37
17
|
}
|
|
38
18
|
|
|
39
19
|
function getHead() {
|
|
@@ -77,17 +57,12 @@ function killServicesByProfile() {
|
|
|
77
57
|
} catch { /* ignore profile read errors */ }
|
|
78
58
|
}
|
|
79
59
|
|
|
80
|
-
function
|
|
81
|
-
const end = Date.now() + ms;
|
|
82
|
-
while (Date.now() < end) { /* busy wait */ }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function rollback(headBefore, reason) {
|
|
60
|
+
async function rollback(headBefore, reason) {
|
|
86
61
|
if (!headBefore || headBefore === 'none') return;
|
|
87
62
|
|
|
88
63
|
killServicesByProfile();
|
|
89
64
|
|
|
90
|
-
if (process.platform === 'win32')
|
|
65
|
+
if (process.platform === 'win32') await sleep(1500);
|
|
91
66
|
|
|
92
67
|
const cwd = getProjectRoot();
|
|
93
68
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
@@ -104,7 +79,7 @@ function rollback(headBefore, reason) {
|
|
|
104
79
|
} catch (err) {
|
|
105
80
|
if (attempt === 1) {
|
|
106
81
|
log('warn', `回滚首次失败,等待后重试: ${err.message}`);
|
|
107
|
-
|
|
82
|
+
await sleep(2000);
|
|
108
83
|
} else {
|
|
109
84
|
log('error', `回滚失败: ${err.message}`);
|
|
110
85
|
}
|
|
@@ -123,14 +98,10 @@ function rollback(headBefore, reason) {
|
|
|
123
98
|
function markTaskFailed() {
|
|
124
99
|
const data = loadTasks();
|
|
125
100
|
if (!data) return;
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
f.status = 'failed';
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
101
|
+
const result = forceStatus(data, 'failed');
|
|
102
|
+
if (result) {
|
|
103
|
+
log('warn', `已将任务 ${result.id} 强制标记为 failed`);
|
|
132
104
|
}
|
|
133
|
-
saveTasks(data);
|
|
134
105
|
}
|
|
135
106
|
|
|
136
107
|
function tryPush() {
|
|
@@ -192,20 +163,17 @@ async function run(requirement, opts = {}) {
|
|
|
192
163
|
console.log('============================================');
|
|
193
164
|
console.log('');
|
|
194
165
|
|
|
195
|
-
// Load config
|
|
196
166
|
const config = loadConfig();
|
|
197
167
|
if (config.provider !== 'claude' && config.baseUrl) {
|
|
198
168
|
log('ok', `模型配置已加载: ${config.provider}${config.model ? ` (${config.model})` : ''}`);
|
|
199
169
|
}
|
|
200
170
|
|
|
201
|
-
// Read requirement from requirements.md or CLI
|
|
202
171
|
const reqFile = path.join(projectRoot, 'requirements.md');
|
|
203
172
|
if (fs.existsSync(reqFile) && !requirement) {
|
|
204
173
|
requirement = fs.readFileSync(reqFile, 'utf8');
|
|
205
174
|
log('ok', '已读取需求文件: requirements.md');
|
|
206
175
|
}
|
|
207
176
|
|
|
208
|
-
// Ensure git repo
|
|
209
177
|
try {
|
|
210
178
|
execSync('git rev-parse --is-inside-work-tree', { cwd: projectRoot, stdio: 'ignore' });
|
|
211
179
|
} catch {
|
|
@@ -217,7 +185,6 @@ async function run(requirement, opts = {}) {
|
|
|
217
185
|
});
|
|
218
186
|
}
|
|
219
187
|
|
|
220
|
-
// Initialization (scan) if needed
|
|
221
188
|
if (!fs.existsSync(p.profile) || !fs.existsSync(p.tasksFile)) {
|
|
222
189
|
if (!requirement) {
|
|
223
190
|
log('error', '首次运行需要提供需求描述');
|
|
@@ -238,7 +205,7 @@ async function run(requirement, opts = {}) {
|
|
|
238
205
|
return;
|
|
239
206
|
}
|
|
240
207
|
|
|
241
|
-
await
|
|
208
|
+
await loadSDK();
|
|
242
209
|
const scanResult = await scan(requirement, { projectRoot });
|
|
243
210
|
if (!scanResult.success) {
|
|
244
211
|
console.log('');
|
|
@@ -253,8 +220,7 @@ async function run(requirement, opts = {}) {
|
|
|
253
220
|
printStats();
|
|
254
221
|
}
|
|
255
222
|
|
|
256
|
-
|
|
257
|
-
if (!dryRun) await requireSdk();
|
|
223
|
+
if (!dryRun) await loadSDK();
|
|
258
224
|
log('info', `开始编码循环 (最多 ${maxSessions} 个会话) ...`);
|
|
259
225
|
console.log('');
|
|
260
226
|
|
|
@@ -292,7 +258,6 @@ async function run(requirement, opts = {}) {
|
|
|
292
258
|
const nextTask = findNextTask(taskData);
|
|
293
259
|
const taskId = nextTask?.id || 'unknown';
|
|
294
260
|
|
|
295
|
-
// Run coding session
|
|
296
261
|
const sessionResult = await runCodingSession(session, {
|
|
297
262
|
projectRoot,
|
|
298
263
|
taskId,
|
|
@@ -304,7 +269,7 @@ async function run(requirement, opts = {}) {
|
|
|
304
269
|
if (sessionResult.stalled) {
|
|
305
270
|
log('warn', `Session ${session} 因停顿超时中断,跳过校验直接重试`);
|
|
306
271
|
consecutiveFailures++;
|
|
307
|
-
rollback(headBefore, '停顿超时');
|
|
272
|
+
await rollback(headBefore, '停顿超时');
|
|
308
273
|
if (consecutiveFailures >= MAX_RETRY) {
|
|
309
274
|
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
310
275
|
markTaskFailed();
|
|
@@ -320,7 +285,6 @@ async function run(requirement, opts = {}) {
|
|
|
320
285
|
continue;
|
|
321
286
|
}
|
|
322
287
|
|
|
323
|
-
// Validate
|
|
324
288
|
log('info', '开始 harness 校验 ...');
|
|
325
289
|
const validateResult = await validate(headBefore);
|
|
326
290
|
|
|
@@ -338,7 +302,7 @@ async function run(requirement, opts = {}) {
|
|
|
338
302
|
timestamp: new Date().toISOString(),
|
|
339
303
|
result: 'success',
|
|
340
304
|
cost: sessionResult.cost,
|
|
341
|
-
taskId: validateResult.sessionData?.task_id ||
|
|
305
|
+
taskId: validateResult.sessionData?.task_id || taskId,
|
|
342
306
|
statusAfter: validateResult.sessionData?.status_after || null,
|
|
343
307
|
notes: validateResult.sessionData?.notes || null,
|
|
344
308
|
});
|
|
@@ -347,7 +311,16 @@ async function run(requirement, opts = {}) {
|
|
|
347
311
|
consecutiveFailures++;
|
|
348
312
|
log('error', `Session ${session} 校验失败 (连续失败: ${consecutiveFailures}/${MAX_RETRY})`);
|
|
349
313
|
|
|
350
|
-
|
|
314
|
+
appendProgress({
|
|
315
|
+
session,
|
|
316
|
+
timestamp: new Date().toISOString(),
|
|
317
|
+
result: 'fatal',
|
|
318
|
+
cost: sessionResult.cost,
|
|
319
|
+
taskId,
|
|
320
|
+
reason: validateResult.sessionData?.reason || '校验失败',
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await rollback(headBefore, '校验失败');
|
|
351
324
|
|
|
352
325
|
if (consecutiveFailures >= MAX_RETRY) {
|
|
353
326
|
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
@@ -357,7 +330,6 @@ async function run(requirement, opts = {}) {
|
|
|
357
330
|
}
|
|
358
331
|
}
|
|
359
332
|
|
|
360
|
-
// Periodic pause
|
|
361
333
|
if (pauseEvery > 0 && session % pauseEvery === 0) {
|
|
362
334
|
console.log('');
|
|
363
335
|
printStats();
|
|
@@ -369,10 +341,8 @@ async function run(requirement, opts = {}) {
|
|
|
369
341
|
}
|
|
370
342
|
}
|
|
371
343
|
|
|
372
|
-
// Cleanup: stop services after loop ends
|
|
373
344
|
killServicesByProfile();
|
|
374
345
|
|
|
375
|
-
// Final report
|
|
376
346
|
console.log('');
|
|
377
347
|
console.log('============================================');
|
|
378
348
|
console.log(' 运行结束');
|
|
@@ -382,7 +352,7 @@ async function run(requirement, opts = {}) {
|
|
|
382
352
|
}
|
|
383
353
|
|
|
384
354
|
async function add(instruction, opts = {}) {
|
|
385
|
-
await
|
|
355
|
+
await loadSDK();
|
|
386
356
|
const p = paths();
|
|
387
357
|
const projectRoot = getProjectRoot();
|
|
388
358
|
ensureLoopDir();
|
package/src/session.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { paths, loadConfig, buildEnvVars, getAllowedTools, log } = require('./config');
|
|
6
|
-
const { Indicator
|
|
7
|
-
const {
|
|
6
|
+
const { Indicator } = require('./indicator');
|
|
7
|
+
const { createSessionHooks } = require('./hooks');
|
|
8
|
+
const { buildSystemPrompt, buildCodingPrompt, buildScanPrompt, buildAddSystemPrompt, buildAddPrompt } = require('./prompts');
|
|
9
|
+
|
|
10
|
+
// ── SDK loader (cached, shared across sessions) ──
|
|
8
11
|
|
|
9
12
|
let _sdkModule = null;
|
|
10
13
|
async function loadSDK() {
|
|
@@ -38,6 +41,8 @@ async function loadSDK() {
|
|
|
38
41
|
process.exit(1);
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
// ── Helpers ──
|
|
45
|
+
|
|
41
46
|
function applyEnvConfig(config) {
|
|
42
47
|
Object.assign(process.env, buildEnvVars(config));
|
|
43
48
|
}
|
|
@@ -63,8 +68,9 @@ function extractResult(messages) {
|
|
|
63
68
|
return null;
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
function
|
|
67
|
-
|
|
71
|
+
function writeSessionSeparator(logStream, sessionNum, label) {
|
|
72
|
+
const sep = '='.repeat(60);
|
|
73
|
+
logStream.write(`\n${sep}\n[Session ${sessionNum}] ${label} ${new Date().toISOString()}\n${sep}\n`);
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
function logMessage(message, logStream, indicator) {
|
|
@@ -75,17 +81,29 @@ function logMessage(message, logStream, indicator) {
|
|
|
75
81
|
const statusLine = indicator.getStatusLine();
|
|
76
82
|
process.stderr.write('\r\x1b[K');
|
|
77
83
|
if (statusLine) process.stderr.write(statusLine + '\n');
|
|
78
|
-
if (logStream && statusLine) {
|
|
79
|
-
logStream.write('\n' + stripAnsi(statusLine) + '\n');
|
|
80
|
-
}
|
|
81
84
|
}
|
|
82
85
|
process.stdout.write(block.text);
|
|
83
86
|
if (logStream) logStream.write(block.text);
|
|
84
87
|
}
|
|
88
|
+
if (block.type === 'tool_use' && logStream) {
|
|
89
|
+
logStream.write(`[TOOL_USE] ${block.name}: ${JSON.stringify(block.input).slice(0, 300)}\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (message.type === 'tool_result' && logStream) {
|
|
95
|
+
const isErr = message.is_error || false;
|
|
96
|
+
const content = typeof message.content === 'string'
|
|
97
|
+
? message.content.slice(0, 500)
|
|
98
|
+
: JSON.stringify(message.content).slice(0, 500);
|
|
99
|
+
if (isErr) {
|
|
100
|
+
logStream.write(`[TOOL_ERROR] ${content}\n`);
|
|
85
101
|
}
|
|
86
102
|
}
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
// ── Session runners ──
|
|
106
|
+
|
|
89
107
|
async function runCodingSession(sessionNum, opts = {}) {
|
|
90
108
|
const sdk = await loadSDK();
|
|
91
109
|
const config = loadConfig();
|
|
@@ -101,58 +119,27 @@ async function runCodingSession(sessionNum, opts = {}) {
|
|
|
101
119
|
const logFile = path.join(p.logsDir, `${taskId}_session_${sessionNum}_${dateStr}.log`);
|
|
102
120
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
103
121
|
|
|
104
|
-
|
|
122
|
+
writeSessionSeparator(logStream, sessionNum, `coding task=${taskId}`);
|
|
105
123
|
|
|
106
|
-
const editCounts = {};
|
|
107
|
-
const EDIT_THRESHOLD = 5;
|
|
108
124
|
const stallTimeoutMs = config.stallTimeout * 1000;
|
|
109
|
-
|
|
125
|
+
const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
|
|
126
|
+
enableStallDetection: true,
|
|
127
|
+
stallTimeoutMs,
|
|
128
|
+
enableEditGuard: true,
|
|
129
|
+
});
|
|
110
130
|
|
|
111
|
-
|
|
112
|
-
const idleMs = Date.now() - indicator.lastToolTime;
|
|
113
|
-
if (idleMs > stallTimeoutMs && !stallDetected) {
|
|
114
|
-
stallDetected = true;
|
|
115
|
-
log('warn', `无新工具调用超过 ${Math.floor(idleMs / 60000)} 分钟,自动中断 session`);
|
|
116
|
-
}
|
|
117
|
-
}, 30000);
|
|
131
|
+
indicator.start(sessionNum);
|
|
118
132
|
|
|
119
133
|
try {
|
|
120
134
|
const queryOpts = buildQueryOptions(config, opts);
|
|
121
135
|
queryOpts.systemPrompt = systemPrompt;
|
|
122
|
-
queryOpts.hooks =
|
|
123
|
-
PreToolUse: [{
|
|
124
|
-
matcher: '*',
|
|
125
|
-
hooks: [async (input) => {
|
|
126
|
-
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
127
|
-
|
|
128
|
-
const target = input.tool_input?.file_path || input.tool_input?.path || '';
|
|
129
|
-
const cmd = input.tool_input?.command || '';
|
|
130
|
-
const pattern = input.tool_input?.pattern || '';
|
|
131
|
-
const detail = target || cmd.slice(0, 200) || (pattern ? `pattern: ${pattern}` : '');
|
|
132
|
-
if (detail) {
|
|
133
|
-
logStream.write(`[${new Date().toISOString()}] ${input.tool_name}: ${detail}\n`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (['Write', 'Edit', 'MultiEdit'].includes(input.tool_name) && target) {
|
|
137
|
-
editCounts[target] = (editCounts[target] || 0) + 1;
|
|
138
|
-
if (editCounts[target] > EDIT_THRESHOLD) {
|
|
139
|
-
return {
|
|
140
|
-
decision: 'block',
|
|
141
|
-
message: `已对 ${target} 编辑 ${editCounts[target]} 次,疑似死循环。请重新审视方案后再继续。`,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return {};
|
|
147
|
-
}]
|
|
148
|
-
}]
|
|
149
|
-
};
|
|
136
|
+
queryOpts.hooks = hooks;
|
|
150
137
|
|
|
151
138
|
const session = sdk.query({ prompt, options: queryOpts });
|
|
152
139
|
|
|
153
140
|
const collected = [];
|
|
154
141
|
for await (const message of session) {
|
|
155
|
-
if (
|
|
142
|
+
if (isStalled()) {
|
|
156
143
|
log('warn', '停顿超时,中断消息循环');
|
|
157
144
|
break;
|
|
158
145
|
}
|
|
@@ -160,20 +147,20 @@ async function runCodingSession(sessionNum, opts = {}) {
|
|
|
160
147
|
logMessage(message, logStream, indicator);
|
|
161
148
|
}
|
|
162
149
|
|
|
163
|
-
|
|
150
|
+
cleanup();
|
|
164
151
|
logStream.end();
|
|
165
152
|
indicator.stop();
|
|
166
153
|
|
|
167
154
|
const result = extractResult(collected);
|
|
168
155
|
return {
|
|
169
|
-
exitCode:
|
|
156
|
+
exitCode: isStalled() ? 2 : 0,
|
|
170
157
|
cost: result?.total_cost_usd ?? null,
|
|
171
158
|
tokenUsage: result?.usage ?? null,
|
|
172
159
|
logFile,
|
|
173
|
-
stalled:
|
|
160
|
+
stalled: isStalled(),
|
|
174
161
|
};
|
|
175
162
|
} catch (err) {
|
|
176
|
-
|
|
163
|
+
cleanup();
|
|
177
164
|
logStream.end();
|
|
178
165
|
indicator.stop();
|
|
179
166
|
log('error', `Claude SDK 错误: ${err.message}`);
|
|
@@ -201,40 +188,47 @@ async function runScanSession(requirement, opts = {}) {
|
|
|
201
188
|
const logFile = path.join(p.logsDir, `scan_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
|
|
202
189
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
203
190
|
|
|
191
|
+
writeSessionSeparator(logStream, 0, `scan (${projectType})`);
|
|
192
|
+
|
|
193
|
+
const stallTimeoutMs = config.stallTimeout * 1000;
|
|
194
|
+
const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
|
|
195
|
+
enableStallDetection: true,
|
|
196
|
+
stallTimeoutMs,
|
|
197
|
+
});
|
|
198
|
+
|
|
204
199
|
indicator.start(0);
|
|
205
200
|
log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
|
|
206
201
|
|
|
207
202
|
try {
|
|
208
203
|
const queryOpts = buildQueryOptions(config, opts);
|
|
209
204
|
queryOpts.systemPrompt = systemPrompt;
|
|
210
|
-
queryOpts.hooks =
|
|
211
|
-
PreToolUse: [{
|
|
212
|
-
matcher: '*',
|
|
213
|
-
hooks: [async (input) => {
|
|
214
|
-
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
215
|
-
return {};
|
|
216
|
-
}]
|
|
217
|
-
}]
|
|
218
|
-
};
|
|
205
|
+
queryOpts.hooks = hooks;
|
|
219
206
|
|
|
220
207
|
const session = sdk.query({ prompt, options: queryOpts });
|
|
221
208
|
|
|
222
209
|
const collected = [];
|
|
223
210
|
for await (const message of session) {
|
|
211
|
+
if (isStalled()) {
|
|
212
|
+
log('warn', '扫描停顿超时,中断');
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
224
215
|
collected.push(message);
|
|
225
216
|
logMessage(message, logStream, indicator);
|
|
226
217
|
}
|
|
227
218
|
|
|
219
|
+
cleanup();
|
|
228
220
|
logStream.end();
|
|
229
221
|
indicator.stop();
|
|
230
222
|
|
|
231
223
|
const result = extractResult(collected);
|
|
232
224
|
return {
|
|
233
|
-
exitCode: 0,
|
|
225
|
+
exitCode: isStalled() ? 2 : 0,
|
|
234
226
|
cost: result?.total_cost_usd ?? null,
|
|
235
227
|
logFile,
|
|
228
|
+
stalled: isStalled(),
|
|
236
229
|
};
|
|
237
230
|
} catch (err) {
|
|
231
|
+
cleanup();
|
|
238
232
|
logStream.end();
|
|
239
233
|
indicator.stop();
|
|
240
234
|
log('error', `扫描失败: ${err.message}`);
|
|
@@ -248,39 +242,45 @@ async function runAddSession(instruction, opts = {}) {
|
|
|
248
242
|
applyEnvConfig(config);
|
|
249
243
|
const indicator = new Indicator();
|
|
250
244
|
|
|
251
|
-
const systemPrompt =
|
|
245
|
+
const systemPrompt = buildAddSystemPrompt();
|
|
252
246
|
const prompt = buildAddPrompt(instruction);
|
|
253
247
|
|
|
254
248
|
const p = paths();
|
|
255
249
|
const logFile = path.join(p.logsDir, `add_tasks_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
|
|
256
250
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
257
251
|
|
|
252
|
+
writeSessionSeparator(logStream, 0, 'add tasks');
|
|
253
|
+
|
|
254
|
+
const stallTimeoutMs = config.stallTimeout * 1000;
|
|
255
|
+
const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
|
|
256
|
+
enableStallDetection: true,
|
|
257
|
+
stallTimeoutMs,
|
|
258
|
+
});
|
|
259
|
+
|
|
258
260
|
indicator.start(0);
|
|
259
261
|
log('info', '正在追加任务...');
|
|
260
262
|
|
|
261
263
|
try {
|
|
262
264
|
const queryOpts = buildQueryOptions(config, opts);
|
|
263
265
|
queryOpts.systemPrompt = systemPrompt;
|
|
264
|
-
queryOpts.hooks =
|
|
265
|
-
PreToolUse: [{
|
|
266
|
-
matcher: '*',
|
|
267
|
-
hooks: [async (input) => {
|
|
268
|
-
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
269
|
-
return {};
|
|
270
|
-
}]
|
|
271
|
-
}]
|
|
272
|
-
};
|
|
266
|
+
queryOpts.hooks = hooks;
|
|
273
267
|
|
|
274
268
|
const session = sdk.query({ prompt, options: queryOpts });
|
|
275
269
|
|
|
276
270
|
for await (const message of session) {
|
|
271
|
+
if (isStalled()) {
|
|
272
|
+
log('warn', '追加任务停顿超时,中断');
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
277
275
|
logMessage(message, logStream, indicator);
|
|
278
276
|
}
|
|
279
277
|
|
|
278
|
+
cleanup();
|
|
280
279
|
logStream.end();
|
|
281
280
|
indicator.stop();
|
|
282
281
|
log('ok', '任务追加完成');
|
|
283
282
|
} catch (err) {
|
|
283
|
+
cleanup();
|
|
284
284
|
logStream.end();
|
|
285
285
|
indicator.stop();
|
|
286
286
|
log('error', `任务追加失败: ${err.message}`);
|
|
@@ -304,6 +304,7 @@ function hasCodeFiles(projectRoot) {
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
module.exports = {
|
|
307
|
+
loadSDK,
|
|
307
308
|
runCodingSession,
|
|
308
309
|
runScanSession,
|
|
309
310
|
runAddSession,
|
package/src/tasks.js
CHANGED
|
@@ -67,6 +67,22 @@ function setStatus(data, taskId, newStatus) {
|
|
|
67
67
|
return task;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Harness-level forced status change (bypasses TRANSITIONS validation).
|
|
72
|
+
* Used when harness needs to mark tasks failed after max retries.
|
|
73
|
+
*/
|
|
74
|
+
function forceStatus(data, status) {
|
|
75
|
+
const features = getFeatures(data);
|
|
76
|
+
for (const f of features) {
|
|
77
|
+
if (f.status === 'in_progress') {
|
|
78
|
+
f.status = status;
|
|
79
|
+
saveTasks(data);
|
|
80
|
+
return f;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
function addTask(data, task) {
|
|
71
87
|
if (!data) {
|
|
72
88
|
data = { project: '', created_at: new Date().toISOString().slice(0, 10), features: [] };
|
|
@@ -145,6 +161,7 @@ module.exports = {
|
|
|
145
161
|
getFeatures,
|
|
146
162
|
findNextTask,
|
|
147
163
|
setStatus,
|
|
164
|
+
forceStatus,
|
|
148
165
|
addTask,
|
|
149
166
|
getStats,
|
|
150
167
|
showStatus,
|
package/templates/CLAUDE.md
CHANGED
|
@@ -177,8 +177,8 @@ pending ──→ in_progress ──→ testing ──→ done
|
|
|
177
177
|
### 第一步:恢复上下文
|
|
178
178
|
|
|
179
179
|
1. **检查 prompt 注入的上下文**:
|
|
180
|
-
- 如果 prompt 中包含"任务上下文"(Hint
|
|
181
|
-
- 如果 prompt 中包含"上次会话"(Hint
|
|
180
|
+
- 如果 prompt 中包含"任务上下文"(Hint 6),说明 harness 已注入当前任务信息,**跳过读取 tasks.json**,直接确认任务后进入第二步
|
|
181
|
+
- 如果 prompt 中包含"上次会话"(Hint 7),说明 harness 已注入上次会话摘要,**跳过读取 session_result.json 历史**
|
|
182
182
|
2. 批量读取以下文件(一次工具调用,跳过已注入的):`.claude-coder/project_profile.json`、`.claude-coder/tasks.json`(仅当无 Hint 6 时)
|
|
183
183
|
3. 如果无 Hint 7 且 `session_result.json` 不存在,运行 `git log --oneline -20` 补充上下文
|
|
184
184
|
4. 如果项目根目录存在 `requirements.md`,读取用户的详细需求和偏好(技术约束、样式要求等),作为本次会话的参考依据
|