claude-coder 1.5.0 → 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/README.md +1 -1
- package/docs/ARCHITECTURE.md +14 -19
- package/docs/README.en.md +1 -1
- package/package.json +1 -1
- package/src/hooks.js +86 -0
- package/src/indicator.js +2 -10
- package/src/prompts.js +77 -9
- package/src/runner.js +25 -55
- package/src/session.js +72 -70
- package/src/tasks.js +17 -0
- package/templates/CLAUDE.md +2 -2
package/README.md
CHANGED
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` | 项目初始化扫描 |
|
|
@@ -207,7 +209,7 @@ templates/
|
|
|
207
209
|
| `session_result.json` | 每次 session 结束 | 当前 session 结果(扁平格式,向后兼容旧 `current` 包装) |
|
|
208
210
|
| `playwright-auth.json` | `claude-coder auth` | Playwright 登录状态(cookies + localStorage) |
|
|
209
211
|
| `tests.json` | 首次测试时 | 验证记录(防止反复测试) |
|
|
210
|
-
| `.runtime/` | 运行时 | 临时文件(phase、step、logs
|
|
212
|
+
| `.runtime/` | 运行时 | 临时文件(phase、step、logs/);工具调用记录合并到 session log |
|
|
211
213
|
|
|
212
214
|
---
|
|
213
215
|
|
|
@@ -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
|
|
|
@@ -450,7 +452,7 @@ query({
|
|
|
450
452
|
### V2 迁移条件(等待稳定后)
|
|
451
453
|
|
|
452
454
|
1. V2 去掉 `unstable_` 前缀,正式发布
|
|
453
|
-
2. V2 支持 Hooks(当前项目依赖 PreToolUse 做 spinner
|
|
455
|
+
2. V2 支持 Hooks(当前项目依赖 PreToolUse 做 spinner 和日志记录)
|
|
454
456
|
3. V2 支持 Subagents(未来可能用于扫描 Agent / 编码 Agent 分离)
|
|
455
457
|
|
|
456
458
|
### 可利用但尚未使用的 V1 特性
|
|
@@ -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/docs/README.en.md
CHANGED
|
@@ -87,7 +87,7 @@ your-project/
|
|
|
87
87
|
test.env # Test credentials (API keys, optional)
|
|
88
88
|
playwright-auth.json # Playwright login state (optional, via auth command)
|
|
89
89
|
.runtime/ # Temp files
|
|
90
|
-
logs/ # Per-session logs
|
|
90
|
+
logs/ # Per-session logs (with tool call traces)
|
|
91
91
|
requirements.md # Requirements (optional)
|
|
92
92
|
```
|
|
93
93
|
|
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
|
@@ -18,9 +18,8 @@ class Indicator {
|
|
|
18
18
|
this.startTime = Date.now();
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
start(sessionNum
|
|
21
|
+
start(sessionNum) {
|
|
22
22
|
this.sessionNum = sessionNum;
|
|
23
|
-
this.activityLogPath = activityLogPath || null;
|
|
24
23
|
this.startTime = Date.now();
|
|
25
24
|
this.timer = setInterval(() => this._render(), 500);
|
|
26
25
|
}
|
|
@@ -44,14 +43,7 @@ class Indicator {
|
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
appendActivity(toolName, summary) {
|
|
47
|
-
|
|
48
|
-
const entry = `[${ts}] ${toolName}: ${summary}`;
|
|
49
|
-
this.lastActivity = entry;
|
|
50
|
-
try {
|
|
51
|
-
if (this.activityLogPath) {
|
|
52
|
-
fs.appendFileSync(this.activityLogPath, entry + '\n', 'utf8');
|
|
53
|
-
}
|
|
54
|
-
} catch { /* ignore */ }
|
|
46
|
+
this.lastActivity = `${toolName}: ${summary}`;
|
|
55
47
|
}
|
|
56
48
|
|
|
57
49
|
_writePhaseFile() {
|
package/src/prompts.js
CHANGED
|
@@ -209,27 +209,94 @@ 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.
|
|
223
|
+
* Structure: Role (primacy) → Context → CoT → TaskGuide → Instruction (recency)
|
|
214
224
|
*/
|
|
215
225
|
function buildAddPrompt(instruction) {
|
|
226
|
+
const p = paths();
|
|
227
|
+
const projectRoot = getProjectRoot();
|
|
216
228
|
const taskGuide = buildTaskGuide();
|
|
229
|
+
|
|
230
|
+
// --- Context injection: pre-read project state ---
|
|
231
|
+
let profileContext = '';
|
|
232
|
+
if (fs.existsSync(p.profile)) {
|
|
233
|
+
try {
|
|
234
|
+
const profile = JSON.parse(fs.readFileSync(p.profile, 'utf8'));
|
|
235
|
+
const stack = profile.tech_stack || {};
|
|
236
|
+
const parts = [];
|
|
237
|
+
if (stack.backend?.framework) parts.push(`后端: ${stack.backend.framework}`);
|
|
238
|
+
if (stack.frontend?.framework) parts.push(`前端: ${stack.frontend.framework}`);
|
|
239
|
+
if (stack.backend?.language) parts.push(`语言: ${stack.backend.language}`);
|
|
240
|
+
if (parts.length) profileContext = `项目技术栈: ${parts.join(', ')}`;
|
|
241
|
+
} catch { /* ignore */ }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let taskContext = '';
|
|
245
|
+
let recentExamples = '';
|
|
246
|
+
try {
|
|
247
|
+
const taskData = loadTasks();
|
|
248
|
+
if (taskData) {
|
|
249
|
+
const stats = getStats(taskData);
|
|
250
|
+
const features = taskData.features || [];
|
|
251
|
+
const maxId = features.length ? features[features.length - 1].id : 'feat-000';
|
|
252
|
+
const maxPriority = features.length ? Math.max(...features.map(f => f.priority || 0)) : 0;
|
|
253
|
+
const categories = [...new Set(features.map(f => f.category))].join(', ');
|
|
254
|
+
|
|
255
|
+
taskContext = `已有 ${stats.total} 个任务(${stats.done} done, ${stats.pending} pending, ${stats.failed} failed)。` +
|
|
256
|
+
`最大 id: ${maxId}, 最大 priority: ${maxPriority}。已有 category: ${categories}。`;
|
|
257
|
+
|
|
258
|
+
const recent = features.slice(-3);
|
|
259
|
+
if (recent.length) {
|
|
260
|
+
recentExamples = '已有任务格式参考(保持一致性):\n' +
|
|
261
|
+
recent.map(f => ` ${f.id}: "${f.description}" (category=${f.category}, steps=${f.steps.length}步, depends_on=[${f.depends_on.join(',')}])`).join('\n');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} catch { /* ignore */ }
|
|
265
|
+
|
|
217
266
|
return [
|
|
218
|
-
|
|
267
|
+
// --- Primacy zone: role + identity ---
|
|
268
|
+
'你是资深需求分析师,擅长将模糊需求分解为可执行的原子任务。',
|
|
269
|
+
'这是任务追加 session,不是编码 session。你只分解任务,不实现代码。',
|
|
219
270
|
'',
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
271
|
+
|
|
272
|
+
// --- Context layer ---
|
|
273
|
+
profileContext,
|
|
274
|
+
taskContext,
|
|
275
|
+
recentExamples,
|
|
276
|
+
`项目绝对路径: ${projectRoot}`,
|
|
277
|
+
'',
|
|
278
|
+
|
|
279
|
+
// --- CoT: explicit thinking steps ---
|
|
280
|
+
'执行步骤(按顺序,不可跳过):',
|
|
281
|
+
'1. 读取 .claude-coder/tasks.json 和 .claude-coder/project_profile.json,全面了解项目现状',
|
|
282
|
+
'2. 分析用户指令:识别核心功能点,判断是单任务还是需要拆分为多任务',
|
|
283
|
+
'3. 检查重复:对比已有任务,避免功能重叠',
|
|
284
|
+
'4. 确定依赖:新任务的 depends_on 引用已有或新增任务的 id,形成 DAG',
|
|
285
|
+
'5. 分解任务:每个任务对应一个独立可测试的功能单元,description 简明(40字内),steps 具体可操作',
|
|
286
|
+
'6. 追加到 tasks.json,id 和 priority 从已有最大值递增,status: pending',
|
|
287
|
+
'7. git add -A && git commit -m "chore: add new tasks"',
|
|
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 个任务:简述" })',
|
|
224
289
|
'',
|
|
290
|
+
|
|
291
|
+
// --- Quality constraints ---
|
|
225
292
|
taskGuide,
|
|
226
293
|
'',
|
|
227
|
-
'
|
|
228
|
-
'git add -A && git commit -m "chore: add new tasks"',
|
|
229
|
-
'写入 session_result.json',
|
|
294
|
+
'不修改已有任务,不实现代码。',
|
|
230
295
|
'',
|
|
296
|
+
|
|
297
|
+
// --- Recency zone: user instruction (highest attention) ---
|
|
231
298
|
`用户指令:${instruction}`,
|
|
232
|
-
].join('\n');
|
|
299
|
+
].filter(Boolean).join('\n');
|
|
233
300
|
}
|
|
234
301
|
|
|
235
302
|
module.exports = {
|
|
@@ -237,5 +304,6 @@ module.exports = {
|
|
|
237
304
|
buildCodingPrompt,
|
|
238
305
|
buildTaskGuide,
|
|
239
306
|
buildScanPrompt,
|
|
307
|
+
buildAddSystemPrompt,
|
|
240
308
|
buildAddPrompt,
|
|
241
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();
|
|
@@ -99,59 +117,29 @@ async function runCodingSession(sessionNum, opts = {}) {
|
|
|
99
117
|
const taskId = opts.taskId || 'unknown';
|
|
100
118
|
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
101
119
|
const logFile = path.join(p.logsDir, `${taskId}_session_${sessionNum}_${dateStr}.log`);
|
|
102
|
-
const activityLogFile = path.join(p.logsDir, `session_${sessionNum}.activity.log`);
|
|
103
120
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
104
121
|
|
|
105
|
-
|
|
122
|
+
writeSessionSeparator(logStream, sessionNum, `coding task=${taskId}`);
|
|
106
123
|
|
|
107
|
-
const editCounts = {};
|
|
108
|
-
const EDIT_THRESHOLD = 5;
|
|
109
124
|
const stallTimeoutMs = config.stallTimeout * 1000;
|
|
110
|
-
|
|
125
|
+
const { hooks, cleanup, isStalled } = createSessionHooks(indicator, logStream, {
|
|
126
|
+
enableStallDetection: true,
|
|
127
|
+
stallTimeoutMs,
|
|
128
|
+
enableEditGuard: true,
|
|
129
|
+
});
|
|
111
130
|
|
|
112
|
-
|
|
113
|
-
const idleMs = Date.now() - indicator.lastToolTime;
|
|
114
|
-
if (idleMs > stallTimeoutMs && !stallDetected) {
|
|
115
|
-
stallDetected = true;
|
|
116
|
-
log('warn', `无新工具调用超过 ${Math.floor(idleMs / 60000)} 分钟,自动中断 session`);
|
|
117
|
-
}
|
|
118
|
-
}, 30000);
|
|
131
|
+
indicator.start(sessionNum);
|
|
119
132
|
|
|
120
133
|
try {
|
|
121
134
|
const queryOpts = buildQueryOptions(config, opts);
|
|
122
135
|
queryOpts.systemPrompt = systemPrompt;
|
|
123
|
-
queryOpts.hooks =
|
|
124
|
-
PreToolUse: [{
|
|
125
|
-
matcher: '*',
|
|
126
|
-
hooks: [async (input) => {
|
|
127
|
-
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
128
|
-
|
|
129
|
-
const target = input.tool_input?.file_path || input.tool_input?.path || '';
|
|
130
|
-
const toolSummary = target ? target.split('/').slice(-2).join('/') : '';
|
|
131
|
-
if (toolSummary) {
|
|
132
|
-
logStream.write(`[${new Date().toISOString()}] ${input.tool_name}: ${toolSummary}\n`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (['Write', 'Edit', 'MultiEdit'].includes(input.tool_name) && target) {
|
|
136
|
-
editCounts[target] = (editCounts[target] || 0) + 1;
|
|
137
|
-
if (editCounts[target] > EDIT_THRESHOLD) {
|
|
138
|
-
return {
|
|
139
|
-
decision: 'block',
|
|
140
|
-
message: `已对 ${target} 编辑 ${editCounts[target]} 次,疑似死循环。请重新审视方案后再继续。`,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return {};
|
|
146
|
-
}]
|
|
147
|
-
}]
|
|
148
|
-
};
|
|
136
|
+
queryOpts.hooks = hooks;
|
|
149
137
|
|
|
150
138
|
const session = sdk.query({ prompt, options: queryOpts });
|
|
151
139
|
|
|
152
140
|
const collected = [];
|
|
153
141
|
for await (const message of session) {
|
|
154
|
-
if (
|
|
142
|
+
if (isStalled()) {
|
|
155
143
|
log('warn', '停顿超时,中断消息循环');
|
|
156
144
|
break;
|
|
157
145
|
}
|
|
@@ -159,20 +147,20 @@ async function runCodingSession(sessionNum, opts = {}) {
|
|
|
159
147
|
logMessage(message, logStream, indicator);
|
|
160
148
|
}
|
|
161
149
|
|
|
162
|
-
|
|
150
|
+
cleanup();
|
|
163
151
|
logStream.end();
|
|
164
152
|
indicator.stop();
|
|
165
153
|
|
|
166
154
|
const result = extractResult(collected);
|
|
167
155
|
return {
|
|
168
|
-
exitCode:
|
|
156
|
+
exitCode: isStalled() ? 2 : 0,
|
|
169
157
|
cost: result?.total_cost_usd ?? null,
|
|
170
158
|
tokenUsage: result?.usage ?? null,
|
|
171
159
|
logFile,
|
|
172
|
-
stalled:
|
|
160
|
+
stalled: isStalled(),
|
|
173
161
|
};
|
|
174
162
|
} catch (err) {
|
|
175
|
-
|
|
163
|
+
cleanup();
|
|
176
164
|
logStream.end();
|
|
177
165
|
indicator.stop();
|
|
178
166
|
log('error', `Claude SDK 错误: ${err.message}`);
|
|
@@ -200,40 +188,47 @@ async function runScanSession(requirement, opts = {}) {
|
|
|
200
188
|
const logFile = path.join(p.logsDir, `scan_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
|
|
201
189
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
202
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
|
+
|
|
203
199
|
indicator.start(0);
|
|
204
200
|
log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
|
|
205
201
|
|
|
206
202
|
try {
|
|
207
203
|
const queryOpts = buildQueryOptions(config, opts);
|
|
208
204
|
queryOpts.systemPrompt = systemPrompt;
|
|
209
|
-
queryOpts.hooks =
|
|
210
|
-
PreToolUse: [{
|
|
211
|
-
matcher: '*',
|
|
212
|
-
hooks: [async (input) => {
|
|
213
|
-
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
214
|
-
return {};
|
|
215
|
-
}]
|
|
216
|
-
}]
|
|
217
|
-
};
|
|
205
|
+
queryOpts.hooks = hooks;
|
|
218
206
|
|
|
219
207
|
const session = sdk.query({ prompt, options: queryOpts });
|
|
220
208
|
|
|
221
209
|
const collected = [];
|
|
222
210
|
for await (const message of session) {
|
|
211
|
+
if (isStalled()) {
|
|
212
|
+
log('warn', '扫描停顿超时,中断');
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
223
215
|
collected.push(message);
|
|
224
216
|
logMessage(message, logStream, indicator);
|
|
225
217
|
}
|
|
226
218
|
|
|
219
|
+
cleanup();
|
|
227
220
|
logStream.end();
|
|
228
221
|
indicator.stop();
|
|
229
222
|
|
|
230
223
|
const result = extractResult(collected);
|
|
231
224
|
return {
|
|
232
|
-
exitCode: 0,
|
|
225
|
+
exitCode: isStalled() ? 2 : 0,
|
|
233
226
|
cost: result?.total_cost_usd ?? null,
|
|
234
227
|
logFile,
|
|
228
|
+
stalled: isStalled(),
|
|
235
229
|
};
|
|
236
230
|
} catch (err) {
|
|
231
|
+
cleanup();
|
|
237
232
|
logStream.end();
|
|
238
233
|
indicator.stop();
|
|
239
234
|
log('error', `扫描失败: ${err.message}`);
|
|
@@ -247,39 +242,45 @@ async function runAddSession(instruction, opts = {}) {
|
|
|
247
242
|
applyEnvConfig(config);
|
|
248
243
|
const indicator = new Indicator();
|
|
249
244
|
|
|
250
|
-
const systemPrompt =
|
|
245
|
+
const systemPrompt = buildAddSystemPrompt();
|
|
251
246
|
const prompt = buildAddPrompt(instruction);
|
|
252
247
|
|
|
253
248
|
const p = paths();
|
|
254
249
|
const logFile = path.join(p.logsDir, `add_tasks_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
|
|
255
250
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
256
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
|
+
|
|
257
260
|
indicator.start(0);
|
|
258
261
|
log('info', '正在追加任务...');
|
|
259
262
|
|
|
260
263
|
try {
|
|
261
264
|
const queryOpts = buildQueryOptions(config, opts);
|
|
262
265
|
queryOpts.systemPrompt = systemPrompt;
|
|
263
|
-
queryOpts.hooks =
|
|
264
|
-
PreToolUse: [{
|
|
265
|
-
matcher: '*',
|
|
266
|
-
hooks: [async (input) => {
|
|
267
|
-
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
268
|
-
return {};
|
|
269
|
-
}]
|
|
270
|
-
}]
|
|
271
|
-
};
|
|
266
|
+
queryOpts.hooks = hooks;
|
|
272
267
|
|
|
273
268
|
const session = sdk.query({ prompt, options: queryOpts });
|
|
274
269
|
|
|
275
270
|
for await (const message of session) {
|
|
271
|
+
if (isStalled()) {
|
|
272
|
+
log('warn', '追加任务停顿超时,中断');
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
276
275
|
logMessage(message, logStream, indicator);
|
|
277
276
|
}
|
|
278
277
|
|
|
278
|
+
cleanup();
|
|
279
279
|
logStream.end();
|
|
280
280
|
indicator.stop();
|
|
281
281
|
log('ok', '任务追加完成');
|
|
282
282
|
} catch (err) {
|
|
283
|
+
cleanup();
|
|
283
284
|
logStream.end();
|
|
284
285
|
indicator.stop();
|
|
285
286
|
log('error', `任务追加失败: ${err.message}`);
|
|
@@ -303,6 +304,7 @@ function hasCodeFiles(projectRoot) {
|
|
|
303
304
|
}
|
|
304
305
|
|
|
305
306
|
module.exports = {
|
|
307
|
+
loadSDK,
|
|
306
308
|
runCodingSession,
|
|
307
309
|
runScanSession,
|
|
308
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`,读取用户的详细需求和偏好(技术约束、样式要求等),作为本次会话的参考依据
|