claude-coder 1.5.1 → 1.5.3
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/indicator.js +46 -25
- package/src/prompts.js +11 -1
- package/src/runner.js +25 -55
- package/src/session.js +79 -72
- 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/indicator.js
CHANGED
|
@@ -16,6 +16,8 @@ class Indicator {
|
|
|
16
16
|
this.lastToolTime = Date.now();
|
|
17
17
|
this.sessionNum = 0;
|
|
18
18
|
this.startTime = Date.now();
|
|
19
|
+
this._lastContentKey = '';
|
|
20
|
+
this._lastRenderTime = 0;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
start(sessionNum) {
|
|
@@ -86,8 +88,16 @@ class Indicator {
|
|
|
86
88
|
|
|
87
89
|
_render() {
|
|
88
90
|
this.spinnerIndex++;
|
|
89
|
-
const
|
|
91
|
+
const contentKey = `${this.phase}|${this.step}|${this.toolTarget}`;
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
|
|
94
|
+
if (contentKey === this._lastContentKey && now - this._lastRenderTime < 5000) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this._lastContentKey = contentKey;
|
|
98
|
+
this._lastRenderTime = now;
|
|
90
99
|
|
|
100
|
+
const line = this.getStatusLine();
|
|
91
101
|
const maxWidth = process.stderr.columns || 80;
|
|
92
102
|
const truncated = line.length > maxWidth + 20 ? line.slice(0, maxWidth + 20) : line;
|
|
93
103
|
|
|
@@ -95,41 +105,60 @@ class Indicator {
|
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
|
|
98
|
-
|
|
108
|
+
function extractFileTarget(toolInput) {
|
|
109
|
+
const raw = typeof toolInput === 'object'
|
|
110
|
+
? (toolInput.file_path || toolInput.path || '')
|
|
111
|
+
: '';
|
|
112
|
+
if (!raw) return '';
|
|
113
|
+
return raw.split('/').slice(-2).join('/').slice(0, 40);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function extractBashLabel(cmd) {
|
|
117
|
+
if (cmd.includes('git ')) return 'Git 操作';
|
|
118
|
+
if (cmd.includes('npm ') || cmd.includes('pip ') || cmd.includes('pnpm ') || cmd.includes('yarn ')) return '安装依赖';
|
|
119
|
+
if (cmd.includes('curl') || cmd.includes('pytest') || cmd.includes('jest') || /\btest\b/.test(cmd)) return '测试验证';
|
|
120
|
+
if (cmd.includes('python ') || cmd.includes('node ')) return '执行脚本';
|
|
121
|
+
return '执行命令';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function extractBashTarget(cmd) {
|
|
125
|
+
let clean = cmd.replace(/^(?:cd\s+\S+\s*&&\s*)+/g, '').trim();
|
|
126
|
+
clean = clean.split(/\s*(?:\|{1,2}|;|&&|2>&1|>\s*\/dev\/null)\s*/)[0].trim();
|
|
127
|
+
return clean.slice(0, 40);
|
|
128
|
+
}
|
|
129
|
+
|
|
99
130
|
function inferPhaseStep(indicator, toolName, toolInput) {
|
|
100
131
|
const name = (toolName || '').toLowerCase();
|
|
101
132
|
|
|
102
133
|
indicator.lastToolTime = Date.now();
|
|
103
134
|
|
|
104
|
-
const rawTarget = typeof toolInput === 'object'
|
|
105
|
-
? (toolInput.file_path || toolInput.path || toolInput.command || toolInput.pattern || '')
|
|
106
|
-
: String(toolInput || '');
|
|
107
|
-
const shortTarget = rawTarget.split('/').slice(-2).join('/').slice(0, 40);
|
|
108
|
-
indicator.toolTarget = shortTarget;
|
|
109
|
-
|
|
110
135
|
if (name === 'write' || name === 'edit' || name === 'multiedit' || name === 'str_replace_editor' || name === 'strreplace') {
|
|
111
136
|
indicator.updatePhase('coding');
|
|
137
|
+
indicator.updateStep('编辑文件');
|
|
138
|
+
indicator.toolTarget = extractFileTarget(toolInput);
|
|
112
139
|
} else if (name === 'bash' || name === 'shell') {
|
|
113
140
|
const cmd = typeof toolInput === 'object' ? (toolInput.command || '') : String(toolInput || '');
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} else if (cmd.includes('test') || cmd.includes('curl') || cmd.includes('pytest')) {
|
|
119
|
-
indicator.updateStep('测试验证');
|
|
120
|
-
indicator.updatePhase('coding');
|
|
121
|
-
} else {
|
|
141
|
+
const label = extractBashLabel(cmd);
|
|
142
|
+
indicator.updateStep(label);
|
|
143
|
+
indicator.toolTarget = extractBashTarget(cmd);
|
|
144
|
+
if (label === '测试验证' || label === '执行脚本' || label === '执行命令') {
|
|
122
145
|
indicator.updatePhase('coding');
|
|
123
146
|
}
|
|
124
147
|
} else if (name === 'read' || name === 'glob' || name === 'grep' || name === 'ls') {
|
|
125
148
|
indicator.updatePhase('thinking');
|
|
126
149
|
indicator.updateStep('读取文件');
|
|
150
|
+
indicator.toolTarget = extractFileTarget(toolInput);
|
|
127
151
|
} else if (name === 'task') {
|
|
128
152
|
indicator.updatePhase('thinking');
|
|
129
153
|
indicator.updateStep('子 Agent 搜索');
|
|
154
|
+
indicator.toolTarget = '';
|
|
130
155
|
} else if (name === 'websearch' || name === 'webfetch') {
|
|
131
156
|
indicator.updatePhase('thinking');
|
|
132
157
|
indicator.updateStep('查阅文档');
|
|
158
|
+
indicator.toolTarget = '';
|
|
159
|
+
} else {
|
|
160
|
+
indicator.updateStep('工具调用');
|
|
161
|
+
indicator.toolTarget = '';
|
|
133
162
|
}
|
|
134
163
|
|
|
135
164
|
let summary;
|
|
@@ -137,15 +166,7 @@ function inferPhaseStep(indicator, toolName, toolInput) {
|
|
|
137
166
|
const target = toolInput.file_path || toolInput.path || '';
|
|
138
167
|
const cmd = toolInput.command || '';
|
|
139
168
|
const pattern = toolInput.pattern || '';
|
|
140
|
-
|
|
141
|
-
summary = target;
|
|
142
|
-
} else if (cmd) {
|
|
143
|
-
summary = cmd.slice(0, 200);
|
|
144
|
-
} else if (pattern) {
|
|
145
|
-
summary = `pattern: ${pattern}`;
|
|
146
|
-
} else {
|
|
147
|
-
summary = JSON.stringify(toolInput).slice(0, 200);
|
|
148
|
-
}
|
|
169
|
+
summary = target || (cmd ? cmd.slice(0, 200) : '') || (pattern ? `pattern: ${pattern}` : JSON.stringify(toolInput).slice(0, 200));
|
|
149
170
|
} else {
|
|
150
171
|
summary = String(toolInput || '').slice(0, 200);
|
|
151
172
|
}
|
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,29 +68,48 @@ 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
|
|
|
76
|
+
let _lastPrintedStatusKey = '';
|
|
77
|
+
|
|
70
78
|
function logMessage(message, logStream, indicator) {
|
|
71
79
|
if (message.type === 'assistant' && message.message?.content) {
|
|
72
80
|
for (const block of message.message.content) {
|
|
73
81
|
if (block.type === 'text' && block.text) {
|
|
74
82
|
if (indicator) {
|
|
75
|
-
const statusLine = indicator.getStatusLine();
|
|
76
83
|
process.stderr.write('\r\x1b[K');
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
84
|
+
const contentKey = `${indicator.phase}|${indicator.step}|${indicator.toolTarget}`;
|
|
85
|
+
if (contentKey !== _lastPrintedStatusKey) {
|
|
86
|
+
_lastPrintedStatusKey = contentKey;
|
|
87
|
+
const statusLine = indicator.getStatusLine();
|
|
88
|
+
if (statusLine) process.stderr.write(statusLine + '\n');
|
|
80
89
|
}
|
|
81
90
|
}
|
|
82
91
|
process.stdout.write(block.text);
|
|
83
92
|
if (logStream) logStream.write(block.text);
|
|
84
93
|
}
|
|
94
|
+
if (block.type === 'tool_use' && logStream) {
|
|
95
|
+
logStream.write(`[TOOL_USE] ${block.name}: ${JSON.stringify(block.input).slice(0, 300)}\n`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (message.type === 'tool_result' && logStream) {
|
|
101
|
+
const isErr = message.is_error || false;
|
|
102
|
+
const content = typeof message.content === 'string'
|
|
103
|
+
? message.content.slice(0, 500)
|
|
104
|
+
: JSON.stringify(message.content).slice(0, 500);
|
|
105
|
+
if (isErr) {
|
|
106
|
+
logStream.write(`[TOOL_ERROR] ${content}\n`);
|
|
85
107
|
}
|
|
86
108
|
}
|
|
87
109
|
}
|
|
88
110
|
|
|
111
|
+
// ── Session runners ──
|
|
112
|
+
|
|
89
113
|
async function runCodingSession(sessionNum, opts = {}) {
|
|
90
114
|
const sdk = await loadSDK();
|
|
91
115
|
const config = loadConfig();
|
|
@@ -101,58 +125,27 @@ async function runCodingSession(sessionNum, opts = {}) {
|
|
|
101
125
|
const logFile = path.join(p.logsDir, `${taskId}_session_${sessionNum}_${dateStr}.log`);
|
|
102
126
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
103
127
|
|
|
104
|
-
|
|
128
|
+
writeSessionSeparator(logStream, sessionNum, `coding task=${taskId}`);
|
|
105
129
|
|
|
106
|
-
const editCounts = {};
|
|
107
|
-
const EDIT_THRESHOLD = 5;
|
|
108
130
|
const stallTimeoutMs = config.stallTimeout * 1000;
|
|
109
|
-
|
|
131
|
+
const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
|
|
132
|
+
enableStallDetection: true,
|
|
133
|
+
stallTimeoutMs,
|
|
134
|
+
enableEditGuard: true,
|
|
135
|
+
});
|
|
110
136
|
|
|
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);
|
|
137
|
+
indicator.start(sessionNum);
|
|
118
138
|
|
|
119
139
|
try {
|
|
120
140
|
const queryOpts = buildQueryOptions(config, opts);
|
|
121
141
|
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
|
-
};
|
|
142
|
+
queryOpts.hooks = hooks;
|
|
150
143
|
|
|
151
144
|
const session = sdk.query({ prompt, options: queryOpts });
|
|
152
145
|
|
|
153
146
|
const collected = [];
|
|
154
147
|
for await (const message of session) {
|
|
155
|
-
if (
|
|
148
|
+
if (isStalled()) {
|
|
156
149
|
log('warn', '停顿超时,中断消息循环');
|
|
157
150
|
break;
|
|
158
151
|
}
|
|
@@ -160,20 +153,20 @@ async function runCodingSession(sessionNum, opts = {}) {
|
|
|
160
153
|
logMessage(message, logStream, indicator);
|
|
161
154
|
}
|
|
162
155
|
|
|
163
|
-
|
|
156
|
+
cleanup();
|
|
164
157
|
logStream.end();
|
|
165
158
|
indicator.stop();
|
|
166
159
|
|
|
167
160
|
const result = extractResult(collected);
|
|
168
161
|
return {
|
|
169
|
-
exitCode:
|
|
162
|
+
exitCode: isStalled() ? 2 : 0,
|
|
170
163
|
cost: result?.total_cost_usd ?? null,
|
|
171
164
|
tokenUsage: result?.usage ?? null,
|
|
172
165
|
logFile,
|
|
173
|
-
stalled:
|
|
166
|
+
stalled: isStalled(),
|
|
174
167
|
};
|
|
175
168
|
} catch (err) {
|
|
176
|
-
|
|
169
|
+
cleanup();
|
|
177
170
|
logStream.end();
|
|
178
171
|
indicator.stop();
|
|
179
172
|
log('error', `Claude SDK 错误: ${err.message}`);
|
|
@@ -201,40 +194,47 @@ async function runScanSession(requirement, opts = {}) {
|
|
|
201
194
|
const logFile = path.join(p.logsDir, `scan_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
|
|
202
195
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
203
196
|
|
|
197
|
+
writeSessionSeparator(logStream, 0, `scan (${projectType})`);
|
|
198
|
+
|
|
199
|
+
const stallTimeoutMs = config.stallTimeout * 1000;
|
|
200
|
+
const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
|
|
201
|
+
enableStallDetection: true,
|
|
202
|
+
stallTimeoutMs,
|
|
203
|
+
});
|
|
204
|
+
|
|
204
205
|
indicator.start(0);
|
|
205
206
|
log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
|
|
206
207
|
|
|
207
208
|
try {
|
|
208
209
|
const queryOpts = buildQueryOptions(config, opts);
|
|
209
210
|
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
|
-
};
|
|
211
|
+
queryOpts.hooks = hooks;
|
|
219
212
|
|
|
220
213
|
const session = sdk.query({ prompt, options: queryOpts });
|
|
221
214
|
|
|
222
215
|
const collected = [];
|
|
223
216
|
for await (const message of session) {
|
|
217
|
+
if (isStalled()) {
|
|
218
|
+
log('warn', '扫描停顿超时,中断');
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
224
221
|
collected.push(message);
|
|
225
222
|
logMessage(message, logStream, indicator);
|
|
226
223
|
}
|
|
227
224
|
|
|
225
|
+
cleanup();
|
|
228
226
|
logStream.end();
|
|
229
227
|
indicator.stop();
|
|
230
228
|
|
|
231
229
|
const result = extractResult(collected);
|
|
232
230
|
return {
|
|
233
|
-
exitCode: 0,
|
|
231
|
+
exitCode: isStalled() ? 2 : 0,
|
|
234
232
|
cost: result?.total_cost_usd ?? null,
|
|
235
233
|
logFile,
|
|
234
|
+
stalled: isStalled(),
|
|
236
235
|
};
|
|
237
236
|
} catch (err) {
|
|
237
|
+
cleanup();
|
|
238
238
|
logStream.end();
|
|
239
239
|
indicator.stop();
|
|
240
240
|
log('error', `扫描失败: ${err.message}`);
|
|
@@ -248,39 +248,45 @@ async function runAddSession(instruction, opts = {}) {
|
|
|
248
248
|
applyEnvConfig(config);
|
|
249
249
|
const indicator = new Indicator();
|
|
250
250
|
|
|
251
|
-
const systemPrompt =
|
|
251
|
+
const systemPrompt = buildAddSystemPrompt();
|
|
252
252
|
const prompt = buildAddPrompt(instruction);
|
|
253
253
|
|
|
254
254
|
const p = paths();
|
|
255
255
|
const logFile = path.join(p.logsDir, `add_tasks_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
|
|
256
256
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
257
257
|
|
|
258
|
+
writeSessionSeparator(logStream, 0, 'add tasks');
|
|
259
|
+
|
|
260
|
+
const stallTimeoutMs = config.stallTimeout * 1000;
|
|
261
|
+
const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
|
|
262
|
+
enableStallDetection: true,
|
|
263
|
+
stallTimeoutMs,
|
|
264
|
+
});
|
|
265
|
+
|
|
258
266
|
indicator.start(0);
|
|
259
267
|
log('info', '正在追加任务...');
|
|
260
268
|
|
|
261
269
|
try {
|
|
262
270
|
const queryOpts = buildQueryOptions(config, opts);
|
|
263
271
|
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
|
-
};
|
|
272
|
+
queryOpts.hooks = hooks;
|
|
273
273
|
|
|
274
274
|
const session = sdk.query({ prompt, options: queryOpts });
|
|
275
275
|
|
|
276
276
|
for await (const message of session) {
|
|
277
|
+
if (isStalled()) {
|
|
278
|
+
log('warn', '追加任务停顿超时,中断');
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
277
281
|
logMessage(message, logStream, indicator);
|
|
278
282
|
}
|
|
279
283
|
|
|
284
|
+
cleanup();
|
|
280
285
|
logStream.end();
|
|
281
286
|
indicator.stop();
|
|
282
287
|
log('ok', '任务追加完成');
|
|
283
288
|
} catch (err) {
|
|
289
|
+
cleanup();
|
|
284
290
|
logStream.end();
|
|
285
291
|
indicator.stop();
|
|
286
292
|
log('error', `任务追加失败: ${err.message}`);
|
|
@@ -304,6 +310,7 @@ function hasCodeFiles(projectRoot) {
|
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
module.exports = {
|
|
313
|
+
loadSDK,
|
|
307
314
|
runCodingSession,
|
|
308
315
|
runScanSession,
|
|
309
316
|
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`,读取用户的详细需求和偏好(技术约束、样式要求等),作为本次会话的参考依据
|