claude-coder 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/bin/cli.js +30 -7
- package/docs/ARCHITECTURE.md +12 -13
- package/docs/PHASE_INJECTION_RESEARCH.md +325 -0
- package/docs/README.en.md +4 -3
- package/package.json +1 -1
- package/src/config.js +0 -2
- package/src/indicator.js +7 -1
- package/src/prompts.js +14 -42
- package/src/runner.js +47 -48
- package/src/session.js +16 -1
- package/src/tasks.js +6 -11
- package/src/validator.js +18 -31
- package/templates/CLAUDE.md +2 -4
package/README.md
CHANGED
|
@@ -50,15 +50,16 @@ claude-coder run "实现用户注册和登录功能"
|
|
|
50
50
|
|------|------|
|
|
51
51
|
| `claude-coder setup` | 交互式模型配置 |
|
|
52
52
|
| `claude-coder run [需求]` | 自动编码循环 |
|
|
53
|
+
| `claude-coder run --max 1` | 单次执行 |
|
|
53
54
|
| `claude-coder run --dry-run` | 预览模式 |
|
|
54
55
|
| `claude-coder init` | 初始化项目环境 |
|
|
55
|
-
| `claude-coder view [需求]` | 观测模式(交互式单次) |
|
|
56
56
|
| `claude-coder add "指令"` | 追加任务 |
|
|
57
|
+
| `claude-coder add -r [file]` | 从需求文件追加任务 |
|
|
57
58
|
| `claude-coder validate` | 手动校验 |
|
|
58
59
|
| `claude-coder status` | 查看进度和成本 |
|
|
59
60
|
| `claude-coder config sync` | 同步配置到 ~/.claude/ |
|
|
60
61
|
|
|
61
|
-
**选项**:`--max N` 限制 session 数(默认 50),`--pause N` 每 N 个 session
|
|
62
|
+
**选项**:`--max N` 限制 session 数(默认 50),`--pause N` 每 N 个 session 暂停确认(默认不暂停)。
|
|
62
63
|
|
|
63
64
|
## 使用场景
|
|
64
65
|
|
|
@@ -66,9 +67,9 @@ claude-coder run "实现用户注册和登录功能"
|
|
|
66
67
|
|
|
67
68
|
**已有项目**:`claude-coder run "新增头像上传功能"` — 先扫描现有代码和技术栈,再增量开发。
|
|
68
69
|
|
|
69
|
-
**需求文档驱动**:在项目根目录创建 `requirements.md
|
|
70
|
+
**需求文档驱动**:在项目根目录创建 `requirements.md`,运行 `claude-coder run` — 需求变更后用 `claude-coder add -r` 同步新任务。
|
|
70
71
|
|
|
71
|
-
**追加任务**:`claude-coder add "新增管理员后台"` — 仅追加到任务列表,下次 run 时执行。
|
|
72
|
+
**追加任务**:`claude-coder add "新增管理员后台"` 或 `claude-coder add -r requirements.md` — 仅追加到任务列表,下次 run 时执行。
|
|
72
73
|
|
|
73
74
|
## 模型支持
|
|
74
75
|
|
|
@@ -88,8 +89,8 @@ your-project/
|
|
|
88
89
|
.env # 模型配置
|
|
89
90
|
project_profile.json # 项目扫描结果
|
|
90
91
|
tasks.json # 任务列表 + 状态
|
|
91
|
-
session_result.json # session
|
|
92
|
-
progress.json #
|
|
92
|
+
session_result.json # 上次 session 结果(扁平)
|
|
93
|
+
progress.json # 会话历史 + 成本
|
|
93
94
|
tests.json # 验证记录
|
|
94
95
|
.runtime/ # 临时文件
|
|
95
96
|
requirements.md # 需求文档(可选)
|
package/bin/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ const COMMANDS = {
|
|
|
7
7
|
run: { desc: '自动编码循环', usage: 'claude-coder run [需求] [--max N] [--pause N] [--dry-run]' },
|
|
8
8
|
setup: { desc: '交互式模型配置', usage: 'claude-coder setup' },
|
|
9
9
|
init: { desc: '初始化项目环境', usage: 'claude-coder init' },
|
|
10
|
-
add: { desc: '追加任务到 tasks.json', usage: 'claude-coder add "指令"' },
|
|
10
|
+
add: { desc: '追加任务到 tasks.json', usage: 'claude-coder add "指令" | add -r [file]' },
|
|
11
11
|
validate: { desc: '手动校验上次 session', usage: 'claude-coder validate' },
|
|
12
12
|
status: { desc: '查看任务进度和成本', usage: 'claude-coder status' },
|
|
13
13
|
config: { desc: '配置管理', usage: 'claude-coder config sync' },
|
|
@@ -23,8 +23,11 @@ function showHelp() {
|
|
|
23
23
|
console.log('\n示例:');
|
|
24
24
|
console.log(' claude-coder setup 配置模型和 API Key');
|
|
25
25
|
console.log(' claude-coder run "实现用户登录" 开始自动编码');
|
|
26
|
-
console.log(' claude-coder run --max 1
|
|
27
|
-
console.log(' claude-coder run --max 5 --
|
|
26
|
+
console.log(' claude-coder run --max 1 单次执行');
|
|
27
|
+
console.log(' claude-coder run --max 5 --pause 5 每 5 个 session 暂停确认');
|
|
28
|
+
console.log(' claude-coder run --dry-run 预览模式');
|
|
29
|
+
console.log(' claude-coder add "新增搜索功能" 追加任务');
|
|
30
|
+
console.log(' claude-coder add -r 从 requirements.md 追加任务');
|
|
28
31
|
console.log(' claude-coder status 查看进度和成本');
|
|
29
32
|
console.log(`\n前置条件: npm install -g @anthropic-ai/claude-agent-sdk`);
|
|
30
33
|
}
|
|
@@ -32,7 +35,7 @@ function showHelp() {
|
|
|
32
35
|
function parseArgs(argv) {
|
|
33
36
|
const args = argv.slice(2);
|
|
34
37
|
const command = args[0];
|
|
35
|
-
const opts = { max: 50, pause:
|
|
38
|
+
const opts = { max: 50, pause: 0, dryRun: false, readFile: null };
|
|
36
39
|
const positional = [];
|
|
37
40
|
|
|
38
41
|
for (let i = 1; i < args.length; i++) {
|
|
@@ -46,6 +49,16 @@ function parseArgs(argv) {
|
|
|
46
49
|
case '--dry-run':
|
|
47
50
|
opts.dryRun = true;
|
|
48
51
|
break;
|
|
52
|
+
case '-r': {
|
|
53
|
+
const next = args[i + 1];
|
|
54
|
+
if (next && !next.startsWith('-')) {
|
|
55
|
+
opts.readFile = next;
|
|
56
|
+
i++;
|
|
57
|
+
} else {
|
|
58
|
+
opts.readFile = 'requirements.md';
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
49
62
|
case '--help':
|
|
50
63
|
case '-h':
|
|
51
64
|
showHelp();
|
|
@@ -92,12 +105,22 @@ async function main() {
|
|
|
92
105
|
break;
|
|
93
106
|
}
|
|
94
107
|
case 'add': {
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
let instruction = positional[0] || '';
|
|
109
|
+
if (opts.readFile) {
|
|
110
|
+
const reqPath = require('path').resolve(opts.readFile);
|
|
111
|
+
if (!require('fs').existsSync(reqPath)) {
|
|
112
|
+
console.error(`文件不存在: ${reqPath}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
instruction = require('fs').readFileSync(reqPath, 'utf8');
|
|
116
|
+
console.log(`已读取需求文件: ${opts.readFile}`);
|
|
117
|
+
}
|
|
118
|
+
if (!instruction) {
|
|
119
|
+
console.error('用法: claude-coder add "任务描述" 或 claude-coder add -r [requirements.md]');
|
|
97
120
|
process.exit(1);
|
|
98
121
|
}
|
|
99
122
|
const runner = require('../src/runner');
|
|
100
|
-
await runner.add(
|
|
123
|
+
await runner.add(instruction, opts);
|
|
101
124
|
break;
|
|
102
125
|
}
|
|
103
126
|
case 'validate': {
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -183,24 +183,23 @@ flowchart TB
|
|
|
183
183
|
|
|
184
184
|
| Session 类型 | systemPrompt | user prompt | 触发条件 |
|
|
185
185
|
|---|---|---|---|
|
|
186
|
-
| **编码** | CLAUDE.md | `buildCodingPrompt()` +
|
|
186
|
+
| **编码** | CLAUDE.md | `buildCodingPrompt()` + 9 个条件 hint | 主循环每次迭代 |
|
|
187
187
|
| **扫描** | CLAUDE.md + SCAN_PROTOCOL.md | `buildScanPrompt()` + 任务分解指导 + profile 质量要求 | 首次运行 |
|
|
188
188
|
| **追加** | CLAUDE.md | `buildAddPrompt()` + 任务分解指导 | `claude-coder add` |
|
|
189
189
|
|
|
190
|
-
### 编码 Session 的
|
|
190
|
+
### 编码 Session 的 9 个条件 Hint
|
|
191
191
|
|
|
192
192
|
| # | Hint | 触发条件 | 影响 |
|
|
193
193
|
|---|---|---|---|
|
|
194
|
-
| 1 | `
|
|
195
|
-
| 2 | `
|
|
196
|
-
| 3 | `
|
|
197
|
-
| 4 | `
|
|
198
|
-
| 5 | `
|
|
199
|
-
| 6 | `
|
|
200
|
-
| 7 | `
|
|
201
|
-
| 8 | `
|
|
202
|
-
| 9 | `
|
|
203
|
-
| 10 | `toolGuidance` | 始终注入 | 全局:工具使用规范(Grep/Glob/Read/LS/MultiEdit/Task 替代 bash 命令),非 Claude 模型必需 |
|
|
194
|
+
| 1 | `mcpHint` | MCP_PLAYWRIGHT=true | Step 5:可用 Playwright |
|
|
195
|
+
| 2 | `retryContext` | 上次校验失败 | 全局:避免同样错误 |
|
|
196
|
+
| 3 | `envHint` | 连续成功且 session>1 | Step 2:跳过 init |
|
|
197
|
+
| 4 | `testHint` | tests.json 有记录 | Step 5:避免重复验证 |
|
|
198
|
+
| 5 | `docsHint` | profile.existing_docs 非空或 profile 有缺陷 | Step 4:读文档后再编码;profile 缺陷时提示 Agent 在 Step 6 补全 services/docs |
|
|
199
|
+
| 6 | `taskHint` | tasks.json 存在且有待办任务 | Step 1:跳过读取 tasks.json,harness 已注入当前任务上下文 + .claude-coder/ 路径提示 |
|
|
200
|
+
| 7 | `memoryHint` | session_result.json 存在(扁平格式) | Step 1:跳过读取 session_result.json,harness 已注入上次会话摘要 |
|
|
201
|
+
| 8 | `serviceHint` | 始终注入 | Step 6:单次模式停止服务,连续模式保持服务运行 |
|
|
202
|
+
| 9 | `toolGuidance` | 始终注入 | 全局:工具使用规范(Grep/Glob/Read/LS/MultiEdit/Task 替代 bash 命令),非 Claude 模型必需 |
|
|
204
203
|
|
|
205
204
|
---
|
|
206
205
|
|
|
@@ -268,7 +267,7 @@ sequenceDiagram
|
|
|
268
267
|
| 维度 | 评分 | 说明 |
|
|
269
268
|
|------|------|------|
|
|
270
269
|
| **CLAUDE.md 系统提示** | 8/10 | U 型注意力设计;铁律清晰;状态机和 6 步流程是核心竞争力 |
|
|
271
|
-
| **动态 prompt** | 9/10 |
|
|
270
|
+
| **动态 prompt** | 9/10 | 9 个条件 hint 精准注入,含 task/memory 上下文注入 + 服务管理 + 工具使用指导,减少 Agent 冗余操作 |
|
|
272
271
|
| **SCAN_PROTOCOL.md** | 8.5/10 | 新旧项目分支完整,profile 格式全面 |
|
|
273
272
|
| **tests.json 设计** | 7.5/10 | 精简字段,核心目的(防反复测试)明确 |
|
|
274
273
|
| **注入时机** | 9/10 | 静态规则 vs 动态上下文分离干净 |
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# 分阶段提示语注入 — 技术调研与方向探讨
|
|
2
|
+
|
|
3
|
+
> 状态:调研阶段,仅探讨,未实现
|
|
4
|
+
> 日期:2026-03-04
|
|
5
|
+
> 背景:当前所有 10 个 Hint 在 session 开始前一次性注入 user prompt。本文探讨利用 Hook 的 `additionalContext` 能力,将提示语拆分到不同阶段按需注入。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 当前架构
|
|
10
|
+
|
|
11
|
+
### 提示语注入时机
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
sequenceDiagram
|
|
15
|
+
participant H as Harness
|
|
16
|
+
participant SDK as Claude Agent SDK
|
|
17
|
+
participant Agent as Agent (Model)
|
|
18
|
+
|
|
19
|
+
H->>H: buildSystemPrompt()<br/>CLAUDE.md (~260行)
|
|
20
|
+
H->>H: buildCodingPrompt()<br/>10 个 Hint 全部拼接
|
|
21
|
+
H->>SDK: query({ prompt, options })
|
|
22
|
+
Note over SDK,Agent: 所有提示语一次性加载
|
|
23
|
+
|
|
24
|
+
loop Agent 自主运行
|
|
25
|
+
Agent->>SDK: 工具调用 (Read/Edit/Bash...)
|
|
26
|
+
SDK->>H: PreToolUse hook 回调
|
|
27
|
+
H->>H: inferPhaseStep() 更新 spinner
|
|
28
|
+
H->>H: 检查编辑循环
|
|
29
|
+
H-->>SDK: return {} (放行)
|
|
30
|
+
SDK->>Agent: 工具结果
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 问题
|
|
35
|
+
|
|
36
|
+
| 问题 | 说明 |
|
|
37
|
+
|------|------|
|
|
38
|
+
| **Token 浪费** | 10 个 Hint 全部注入 user prompt,但大部分 Hint 仅在特定阶段有用(如 testHint 仅 Step 5 需要) |
|
|
39
|
+
| **注意力稀释** | 一次性注入大量指令,模型在真正需要某条指令时可能已"忘记"(context rot) |
|
|
40
|
+
| **时机错位** | 工具使用指导(Hint 10)在 Agent 还没开始读文件时就注入了,但 Agent 在 Step 4 编码阶段才真正需要这些规则 |
|
|
41
|
+
| **无法纠正** | 当前 Hook 仅用于监控和死循环拦截,无法在 Agent 做出低效工具选择时即时纠正 |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Hook 能力盘点
|
|
46
|
+
|
|
47
|
+
### SDK 内联 Hook(当前使用方式)
|
|
48
|
+
|
|
49
|
+
通过 `query()` 的 `options.hooks` 定义,进程内回调:
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
sdk.query({
|
|
53
|
+
prompt,
|
|
54
|
+
options: {
|
|
55
|
+
hooks: {
|
|
56
|
+
PreToolUse: [{ matcher: '*', hooks: [async (input) => { ... }] }],
|
|
57
|
+
PostToolUse: [{ matcher: '*', hooks: [async (input) => { ... }] }],
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Hook 事件 | SDK 内联支持 | 能力 |
|
|
64
|
+
|-----------|-------------|------|
|
|
65
|
+
| `PreToolUse` | 是 | `permissionDecision` (allow/deny/ask), `message`, **`additionalContext`** (v2.1.9+), `updatedInput` |
|
|
66
|
+
| `PostToolUse` | 是 | `decision` (block), `reason`, **`additionalContext`** |
|
|
67
|
+
| `UserPromptSubmit` | 是 | `decision` (block), `reason`, `additionalContext` |
|
|
68
|
+
| `Stop` | 是 | `decision` (block), `reason` |
|
|
69
|
+
| `SessionStart` | **否** (仅 CLI 声明式) | 不适用 |
|
|
70
|
+
| `SessionEnd` | **否** (仅 CLI 声明式) | 不适用 |
|
|
71
|
+
|
|
72
|
+
### `additionalContext` 关键特性
|
|
73
|
+
|
|
74
|
+
- **作用**: 将文本注入 Agent 的 context window,Agent 在后续推理中可以看到并遵循
|
|
75
|
+
- **注入位置**: 作为工具调用的附加上下文出现,紧邻工具结果
|
|
76
|
+
- **注意力**: 因为紧跟当前工具调用,处于模型注意力的高峰区域(recency zone)
|
|
77
|
+
- **限制**: 2026年1月新增,可能存在边缘 bug
|
|
78
|
+
|
|
79
|
+
### `decision: 'block'` + `message`(当前已在用)
|
|
80
|
+
|
|
81
|
+
- **作用**: 阻止工具调用,`message` 作为错误反馈传回模型
|
|
82
|
+
- **注意力**: 模型会将其视为"操作失败"信息,遵循率高
|
|
83
|
+
- **适用场景**: 拦截不当操作并引导替代方案
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 3. 提议架构:分阶段注入
|
|
88
|
+
|
|
89
|
+
### 核心思想
|
|
90
|
+
|
|
91
|
+
**按 Agent 的工作阶段,在 Hook 中按需注入对应阶段的提示语。** 初始 prompt 仅包含最核心的内容(身份、任务、约束),其余指导在 Agent 进入相应阶段时即时注入。
|
|
92
|
+
|
|
93
|
+
```mermaid
|
|
94
|
+
sequenceDiagram
|
|
95
|
+
participant H as Harness
|
|
96
|
+
participant SDK as Claude Agent SDK
|
|
97
|
+
participant Agent as Agent
|
|
98
|
+
|
|
99
|
+
H->>SDK: query({ prompt: 精简版 })
|
|
100
|
+
Note over H: 仅注入: 身份 + 任务上下文 + 约束
|
|
101
|
+
|
|
102
|
+
rect rgb(200, 230, 255)
|
|
103
|
+
Note over Agent: Phase 1: 恢复上下文
|
|
104
|
+
Agent->>SDK: Read(.claude-coder/profile.json)
|
|
105
|
+
SDK->>H: PreToolUse
|
|
106
|
+
H-->>SDK: additionalContext: 路径提示 + 文档指引
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
rect rgb(200, 255, 200)
|
|
110
|
+
Note over Agent: Phase 2: 编码阶段
|
|
111
|
+
Agent->>SDK: Edit(src/app.ts)
|
|
112
|
+
SDK->>H: PreToolUse
|
|
113
|
+
H-->>SDK: additionalContext: 工具使用规范 + MultiEdit提示
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
rect rgb(255, 230, 200)
|
|
117
|
+
Note over Agent: Phase 3: 测试阶段
|
|
118
|
+
Agent->>SDK: Bash(curl ...)
|
|
119
|
+
SDK->>H: PreToolUse
|
|
120
|
+
H-->>SDK: additionalContext: 测试效率规则
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
rect rgb(230, 200, 255)
|
|
124
|
+
Note over Agent: Phase 4: 收尾阶段
|
|
125
|
+
Agent->>SDK: Bash(git commit)
|
|
126
|
+
SDK->>H: PreToolUse
|
|
127
|
+
H-->>SDK: additionalContext: 服务管理 + session_result 格式
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Hint 拆分方案
|
|
132
|
+
|
|
133
|
+
| # | Hint | 当前位置 | 建议注入时机 | 注入方式 |
|
|
134
|
+
|---|------|----------|-------------|----------|
|
|
135
|
+
| 1 | `reqSyncHint` | user prompt | **保留在 user prompt** | 需求变更需要在 Step 1 就知道 |
|
|
136
|
+
| 7 | `taskHint` | user prompt | **保留在 user prompt** | 任务上下文是 Agent 开始工作的前提 |
|
|
137
|
+
| 8 | `memoryHint` | user prompt | **保留在 user prompt** | 上次会话记忆需要一开始就有 |
|
|
138
|
+
| 5 | `envHint` | user prompt | **保留在 user prompt** | Step 2 环境检查需要一开始就知道 |
|
|
139
|
+
| 2 | `mcpHint` | user prompt | PreToolUse (Bash: curl/test) | 测试时才需要知道 Playwright 可用 |
|
|
140
|
+
| 3 | `testHint` | user prompt | PreToolUse (Bash: curl/test) | 测试时才需要避免重复验证 |
|
|
141
|
+
| 4 | `docsHint` | user prompt | PreToolUse (Read: 首次读文件) | 读文件时提醒先读文档 |
|
|
142
|
+
| 6 | `retryContext` | user prompt | **保留在 user prompt** | 重试上下文需要一开始就有 |
|
|
143
|
+
| 9 | `serviceHint` | user prompt | PreToolUse (Bash: git) | 收尾时才需要知道是否停服务 |
|
|
144
|
+
| 10 | `toolGuidance` | user prompt | PreToolUse (首次工具调用) | 开始使用工具时注入 |
|
|
145
|
+
|
|
146
|
+
**结论**: 10 个 Hint 中,5 个适合保留在初始 prompt(1, 5, 6, 7, 8),5 个适合延迟注入到 Hook(2, 3, 4, 9, 10)。
|
|
147
|
+
|
|
148
|
+
### 实现草案
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
// session.js - PreToolUse hook 增强版(概念代码,仅供讨论)
|
|
152
|
+
const injected = new Set(); // 跟踪已注入的 Hint,每个仅注入一次
|
|
153
|
+
|
|
154
|
+
hooks: {
|
|
155
|
+
PreToolUse: [{
|
|
156
|
+
matcher: '*',
|
|
157
|
+
hooks: [async (input) => {
|
|
158
|
+
const name = input.tool_name;
|
|
159
|
+
const toolInput = input.tool_input || {};
|
|
160
|
+
let additionalContext = '';
|
|
161
|
+
|
|
162
|
+
// --- Phase: 读取文件 → 注入文档指引 ---
|
|
163
|
+
if (['Read', 'Glob', 'Grep', 'LS'].includes(name) && !injected.has('docs')) {
|
|
164
|
+
additionalContext += docsHint; // Hint 4
|
|
165
|
+
injected.add('docs');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- Phase: 首次工具调用 → 注入工具使用规范 ---
|
|
169
|
+
if (!injected.has('toolGuide')) {
|
|
170
|
+
additionalContext += '\n' + toolGuidance; // Hint 10
|
|
171
|
+
injected.add('toolGuide');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- Phase: 测试阶段 → 注入测试规则 ---
|
|
175
|
+
if (name === 'Bash') {
|
|
176
|
+
const cmd = toolInput.command || '';
|
|
177
|
+
if ((cmd.includes('curl') || cmd.includes('test') || cmd.includes('pytest'))
|
|
178
|
+
&& !injected.has('test')) {
|
|
179
|
+
additionalContext += '\n' + testHint; // Hint 3
|
|
180
|
+
additionalContext += '\n' + mcpHint; // Hint 2
|
|
181
|
+
injected.add('test');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- Phase: Git 操作 → 注入收尾提示 ---
|
|
185
|
+
if (cmd.includes('git ') && !injected.has('service')) {
|
|
186
|
+
additionalContext += '\n' + serviceHint; // Hint 9
|
|
187
|
+
injected.add('service');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- Bash 命令纠正(进阶) ---
|
|
192
|
+
if (name === 'Bash') {
|
|
193
|
+
const cmd = toolInput.command || '';
|
|
194
|
+
if (/\bgrep\b/.test(cmd) && !cmd.includes('rg ')) {
|
|
195
|
+
return {
|
|
196
|
+
permissionDecision: 'deny',
|
|
197
|
+
permissionDecisionReason: '请使用 Grep 工具替代 bash grep,效率更高且结果格式化更好。',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (/\bfind\b/.test(cmd)) {
|
|
201
|
+
return {
|
|
202
|
+
permissionDecision: 'deny',
|
|
203
|
+
permissionDecisionReason: '请使用 Glob 工具替代 bash find。',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (/\bcat\b/.test(cmd) && !cmd.includes('<<')) {
|
|
207
|
+
return {
|
|
208
|
+
permissionDecision: 'deny',
|
|
209
|
+
permissionDecisionReason: '请使用 Read 工具替代 bash cat。',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --- 编辑循环检测(已有功能) ---
|
|
215
|
+
// ... existing loop detection code ...
|
|
216
|
+
|
|
217
|
+
// 注入上下文
|
|
218
|
+
if (additionalContext.trim()) {
|
|
219
|
+
return { additionalContext: additionalContext.trim() };
|
|
220
|
+
}
|
|
221
|
+
return {};
|
|
222
|
+
}]
|
|
223
|
+
}]
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 4. Bash 命令拦截:工具纠正的最短路径
|
|
230
|
+
|
|
231
|
+
在完整的分阶段注入之前,有一个**低成本高收益**的中间步骤:在 PreToolUse hook 中拦截 Agent 使用 Bash 执行低效命令(grep/find/cat/ls/head/tail),引导其使用专用工具。
|
|
232
|
+
|
|
233
|
+
### 行为矩阵
|
|
234
|
+
|
|
235
|
+
| Agent 执行 | Hook 行为 | 反馈给 Agent |
|
|
236
|
+
|------------|----------|-------------|
|
|
237
|
+
| `Bash: grep -r "pattern" .` | **deny** | "请使用 Grep 工具替代 bash grep" |
|
|
238
|
+
| `Bash: find . -name "*.ts"` | **deny** | "请使用 Glob 工具替代 bash find" |
|
|
239
|
+
| `Bash: cat src/app.ts` | **deny** | "请使用 Read 工具替代 bash cat" |
|
|
240
|
+
| `Bash: ls -la` | **deny** | "请使用 LS 工具替代 bash ls" |
|
|
241
|
+
| `Bash: head -20 file.txt` | **deny** | "请使用 Read 工具(支持 offset/limit)替代 bash head" |
|
|
242
|
+
| `Bash: npm test` | allow | -- |
|
|
243
|
+
| `Bash: git commit` | allow + additionalContext | 注入收尾提示 |
|
|
244
|
+
|
|
245
|
+
### 优势
|
|
246
|
+
|
|
247
|
+
- **确定性**: Hook 拦截是确定性的,不依赖模型是否"记住"了 prompt 中的工具规则
|
|
248
|
+
- **即时纠正**: 在 Agent 犯错的那一刻就纠正,而不是等它浪费完 context
|
|
249
|
+
- **渐进式**: 可以先实现拦截(deny + message),后续再加 additionalContext
|
|
250
|
+
- **非 Claude 模型必需**: qwen/deepseek 等模型对 prompt 的遵循率不如 Claude,但 deny 是硬性拦截,模型无法绕过
|
|
251
|
+
|
|
252
|
+
### 风险
|
|
253
|
+
|
|
254
|
+
| 风险 | 缓解方案 |
|
|
255
|
+
|------|----------|
|
|
256
|
+
| 误拦截合法 Bash 命令(如 `cat <<EOF` heredoc) | 正则匹配需要排除 heredoc、管道等场景 |
|
|
257
|
+
| 某些 grep 用法没有 Grep 工具替代(如 `grep -c`) | 只拦截简单模式,复杂 grep 放行 |
|
|
258
|
+
| 过度拦截导致 Agent 陷入循环 | 每种拦截最多触发 2 次,第 3 次放行 |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 5. 与现有方案的对比
|
|
263
|
+
|
|
264
|
+
| 维度 | 当前方案 | 分阶段注入 | Bash 拦截纠正 |
|
|
265
|
+
|------|---------|-----------|-------------|
|
|
266
|
+
| 实现复杂度 | 低 | 高 | 中 |
|
|
267
|
+
| Token 效率 | 低(全量注入) | 高(按需注入) | 不变(不影响初始 prompt) |
|
|
268
|
+
| 注意力效果 | 中(U型优化) | 高(时机精准) | 高(即时纠正,deny 不可忽略) |
|
|
269
|
+
| 非 Claude 模型支持 | 中(靠 prompt) | 高(时机 + prompt) | **最高(硬性拦截)** |
|
|
270
|
+
| 风险 | 低 | 中(additionalContext 较新) | 低(deny 已验证) |
|
|
271
|
+
| 依赖 SDK 版本 | 无 | v2.1.9+(additionalContext) | 无(deny + message 已有) |
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 6. 建议路线图
|
|
276
|
+
|
|
277
|
+
### P0 — 立即可做(不依赖新 SDK 特性)
|
|
278
|
+
|
|
279
|
+
**Bash 命令拦截纠正**
|
|
280
|
+
|
|
281
|
+
在现有 PreToolUse hook 中增加 bash 命令检测,对 `grep`/`find`/`cat`/`ls`/`head`/`tail` 返回 `deny + message` 引导使用专用工具。这是最短路径、最高确定性的优化。
|
|
282
|
+
|
|
283
|
+
### P1 — 短期(需要验证 additionalContext)
|
|
284
|
+
|
|
285
|
+
**工具使用指导延迟注入**
|
|
286
|
+
|
|
287
|
+
将 Hint 10(toolGuidance)从初始 prompt 移到 PreToolUse hook 的 `additionalContext`,在 Agent 首次使用工具时注入。验证 `additionalContext` 在非 Claude 模型上的效果。
|
|
288
|
+
|
|
289
|
+
### P2 — 中期
|
|
290
|
+
|
|
291
|
+
**测试/收尾阶段指导延迟注入**
|
|
292
|
+
|
|
293
|
+
将 Hint 2/3/9 移到 PreToolUse hook,按阶段(test/git)触发注入。
|
|
294
|
+
|
|
295
|
+
### P3 — 远期
|
|
296
|
+
|
|
297
|
+
**完整分阶段注入**
|
|
298
|
+
|
|
299
|
+
所有可延迟的 Hint 通过 Hook 按需注入。初始 prompt 仅保留身份、任务、约束。配合 `additionalContext` 的 PostToolUse 版本,实现"编码后注入代码审查提示"等高级场景。
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 7. 学术/行业参考
|
|
304
|
+
|
|
305
|
+
| 来源 | 核心概念 | 与本方案的关联 |
|
|
306
|
+
|------|----------|---------------|
|
|
307
|
+
| Anthropic Context Engineering (2025) | Context 是有限资源,需精心管理 | 按需注入减少 context 浪费 |
|
|
308
|
+
| Claude Code System Prompt (gist) | 每个工具都有 "when to use / when NOT to use" 指导 | Hint 10 和 Bash 拦截复现这一设计 |
|
|
309
|
+
| SWE-Agent (2024) ACI | Agent-Computer Interface 设计应优化工具发现和使用 | Hook 即时纠正是 ACI 的运行时优化 |
|
|
310
|
+
| Anthropic "Writing effective tools for agents" (2025) | 工具设计影响 Agent 行为,工具在 context 中很显眼 | 扩展 allowedTools 让工具自然出现在模型视野 |
|
|
311
|
+
| ContextBench (2025) | 复杂脚手架边际收益递减 | 不过度设计分阶段注入,先做确定性拦截 |
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## 8. 结论
|
|
316
|
+
|
|
317
|
+
当前 harness 的提示语架构已经相当成熟(U型注意力 + 10个条件Hint + recency zone 注入)。下一步优化的核心方向是**从"一次性全量注入"向"按需分阶段注入"演进**,但需要渐进式推进:
|
|
318
|
+
|
|
319
|
+
1. **先做 Bash 命令拦截**(P0)— 零风险,最高确定性,不依赖新 SDK 特性
|
|
320
|
+
2. **验证 `additionalContext`**(P1)— 确认非 Claude 模型是否能看到并遵循
|
|
321
|
+
3. **逐步迁移 Hint**(P2-P3)— 每次迁移一个 Hint,A/B 测试效果
|
|
322
|
+
|
|
323
|
+
**核心原则:确定性拦截(Hook deny)> 即时注入(additionalContext)> 初始 prompt 指导(Hint)> 系统 prompt 规则(CLAUDE.md)**
|
|
324
|
+
|
|
325
|
+
这个优先级排序体现了一个关键洞察:**越靠近行为发生的时刻,指导的遵循率越高**。
|
package/docs/README.en.md
CHANGED
|
@@ -54,11 +54,12 @@ Each session, the agent autonomously follows 6 steps: restore context → env ch
|
|
|
54
54
|
| `claude-coder run --dry-run` | Preview mode |
|
|
55
55
|
| `claude-coder init` | Initialize project environment |
|
|
56
56
|
| `claude-coder add "instruction"` | Append tasks |
|
|
57
|
+
| `claude-coder add -r [file]` | Append tasks from requirements file |
|
|
57
58
|
| `claude-coder validate` | Manually validate last session |
|
|
58
59
|
| `claude-coder status` | View progress and costs |
|
|
59
60
|
| `claude-coder config sync` | Sync config to ~/.claude/ |
|
|
60
61
|
|
|
61
|
-
**Options**: `--max N` limit sessions (default 50), `--pause N` pause every N sessions (default
|
|
62
|
+
**Options**: `--max N` limit sessions (default 50), `--pause N` pause every N sessions (default: no pause).
|
|
62
63
|
|
|
63
64
|
## Model Support
|
|
64
65
|
|
|
@@ -78,8 +79,8 @@ your-project/
|
|
|
78
79
|
.env # Model config
|
|
79
80
|
project_profile.json # Project scan results
|
|
80
81
|
tasks.json # Task list + status
|
|
81
|
-
session_result.json #
|
|
82
|
-
progress.json # Session
|
|
82
|
+
session_result.json # Last session result (flat)
|
|
83
|
+
progress.json # Session history + costs
|
|
83
84
|
tests.json # Verification records
|
|
84
85
|
.runtime/ # Temp files
|
|
85
86
|
requirements.md # Requirements (optional)
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -55,8 +55,6 @@ function paths() {
|
|
|
55
55
|
sessionResult: path.join(loopDir, 'session_result.json'),
|
|
56
56
|
profile: path.join(loopDir, 'project_profile.json'),
|
|
57
57
|
testsFile: path.join(loopDir, 'tests.json'),
|
|
58
|
-
syncState: path.join(loopDir, 'sync_state.json'),
|
|
59
|
-
reqHashFile: path.join(loopDir, 'requirements_hash.current'),
|
|
60
58
|
claudeMd: getTemplatePath('CLAUDE.md'),
|
|
61
59
|
scanProtocol: getTemplatePath('SCAN_PROTOCOL.md'),
|
|
62
60
|
runtime,
|
package/src/indicator.js
CHANGED
|
@@ -59,6 +59,12 @@ class Indicator {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
getStatusLine() {
|
|
62
|
+
const now = new Date();
|
|
63
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
64
|
+
const mi = String(now.getMinutes()).padStart(2, '0');
|
|
65
|
+
const sc = String(now.getSeconds()).padStart(2, '0');
|
|
66
|
+
const clock = `${hh}:${mi}:${sc}`;
|
|
67
|
+
|
|
62
68
|
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
63
69
|
const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
64
70
|
const ss = String(elapsed % 60).padStart(2, '0');
|
|
@@ -68,7 +74,7 @@ class Indicator {
|
|
|
68
74
|
? `${COLOR.yellow}思考中${COLOR.reset}`
|
|
69
75
|
: `${COLOR.green}编码中${COLOR.reset}`;
|
|
70
76
|
|
|
71
|
-
let line = `${spinner} [Session ${this.sessionNum}] ${phaseLabel} ${mm}:${ss}`;
|
|
77
|
+
let line = `${spinner} [Session ${this.sessionNum}] ${clock} ${phaseLabel} ${mm}:${ss}`;
|
|
72
78
|
if (this.step) line += ` | ${this.step}`;
|
|
73
79
|
return line;
|
|
74
80
|
}
|
package/src/prompts.js
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const { paths, loadConfig
|
|
4
|
+
const { paths, loadConfig } = require('./config');
|
|
5
5
|
const { loadTasks, findNextTask, getStats } = require('./tasks');
|
|
6
6
|
|
|
7
|
-
function safeJsonParse(text) {
|
|
8
|
-
try {
|
|
9
|
-
return JSON.parse(text);
|
|
10
|
-
} catch {
|
|
11
|
-
return JSON.parse(
|
|
12
|
-
text.replace(/[\u201c\u201d]/g, '"').replace(/[\u2018\u2019]/g, "'")
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
7
|
/**
|
|
18
8
|
* Build system prompt by combining template files.
|
|
19
9
|
* @param {boolean} includeScanProtocol - Whether to append SCAN_PROTOCOL.md
|
|
@@ -36,40 +26,24 @@ function buildCodingPrompt(sessionNum, opts = {}) {
|
|
|
36
26
|
const config = loadConfig();
|
|
37
27
|
const consecutiveFailures = opts.consecutiveFailures || 0;
|
|
38
28
|
|
|
39
|
-
// Hint 1:
|
|
40
|
-
const reqHash = getRequirementsHash();
|
|
41
|
-
let reqSyncHint = '';
|
|
42
|
-
if (reqHash) {
|
|
43
|
-
fs.writeFileSync(p.reqHashFile, reqHash, 'utf8');
|
|
44
|
-
let lastHash = '';
|
|
45
|
-
if (fs.existsSync(p.syncState)) {
|
|
46
|
-
try { lastHash = JSON.parse(fs.readFileSync(p.syncState, 'utf8')).last_requirements_hash || ''; } catch { /* ignore */ }
|
|
47
|
-
}
|
|
48
|
-
if (lastHash !== reqHash) {
|
|
49
|
-
reqSyncHint = '需求已变更:第一步中请读取 requirements.md,将新增需求追加为 pending 任务到 tasks.json。';
|
|
50
|
-
}
|
|
51
|
-
} else if (fs.existsSync(p.reqHashFile)) {
|
|
52
|
-
fs.unlinkSync(p.reqHashFile);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Hint 2: Playwright MCP availability
|
|
29
|
+
// Hint 1: Playwright MCP availability
|
|
56
30
|
const mcpHint = config.mcpPlaywright
|
|
57
31
|
? '前端/全栈任务可用 Playwright MCP(browser_navigate、browser_snapshot、browser_click 等)做端到端测试。'
|
|
58
32
|
: '';
|
|
59
33
|
|
|
60
|
-
// Hint
|
|
34
|
+
// Hint 2: Retry context from previous failures
|
|
61
35
|
let retryContext = '';
|
|
62
36
|
if (consecutiveFailures > 0 && opts.lastValidateLog) {
|
|
63
37
|
retryContext = `\n注意:上次会话校验失败,原因:${opts.lastValidateLog}。请避免同样的问题。`;
|
|
64
38
|
}
|
|
65
39
|
|
|
66
|
-
// Hint
|
|
40
|
+
// Hint 3: Environment readiness
|
|
67
41
|
let envHint = '';
|
|
68
42
|
if (consecutiveFailures === 0 && sessionNum > 1) {
|
|
69
43
|
envHint = '环境已就绪,第二步可跳过 claude-coder init,仅确认服务存活。涉及新依赖时仍需运行 claude-coder init。';
|
|
70
44
|
}
|
|
71
45
|
|
|
72
|
-
// Hint
|
|
46
|
+
// Hint 4: Existing test records
|
|
73
47
|
let testHint = '';
|
|
74
48
|
if (fs.existsSync(p.testsFile)) {
|
|
75
49
|
try {
|
|
@@ -78,7 +52,7 @@ function buildCodingPrompt(sessionNum, opts = {}) {
|
|
|
78
52
|
} catch { /* ignore */ }
|
|
79
53
|
}
|
|
80
54
|
|
|
81
|
-
// Hint
|
|
55
|
+
// Hint 5: Project documentation awareness + profile quality check
|
|
82
56
|
let docsHint = '';
|
|
83
57
|
if (fs.existsSync(p.profile)) {
|
|
84
58
|
try {
|
|
@@ -97,7 +71,7 @@ function buildCodingPrompt(sessionNum, opts = {}) {
|
|
|
97
71
|
} catch { /* ignore */ }
|
|
98
72
|
}
|
|
99
73
|
|
|
100
|
-
// Hint
|
|
74
|
+
// Hint 6: Task context (harness pre-read, saves Agent 2-3 Read calls)
|
|
101
75
|
let taskHint = '';
|
|
102
76
|
try {
|
|
103
77
|
const taskData = loadTasks();
|
|
@@ -114,26 +88,25 @@ function buildCodingPrompt(sessionNum, opts = {}) {
|
|
|
114
88
|
}
|
|
115
89
|
} catch { /* ignore */ }
|
|
116
90
|
|
|
117
|
-
// Hint
|
|
91
|
+
// Hint 7: Session memory (read flat session_result.json)
|
|
118
92
|
let memoryHint = '';
|
|
119
93
|
if (fs.existsSync(p.sessionResult)) {
|
|
120
94
|
try {
|
|
121
|
-
const sr =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
(last.notes ? `, 要点: ${last.notes.slice(0, 100)}` : '') + '。';
|
|
95
|
+
const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
|
|
96
|
+
if (sr?.task_id) {
|
|
97
|
+
memoryHint = `上次会话: ${sr.task_id} → ${sr.status_after || sr.session_result}` +
|
|
98
|
+
(sr.notes ? `, 要点: ${sr.notes.slice(0, 100)}` : '') + '。';
|
|
126
99
|
}
|
|
127
100
|
} catch { /* ignore */ }
|
|
128
101
|
}
|
|
129
102
|
|
|
130
|
-
// Hint
|
|
103
|
+
// Hint 8: Service management (continuous vs single-shot mode)
|
|
131
104
|
const maxSessions = opts.maxSessions || 50;
|
|
132
105
|
const serviceHint = maxSessions === 1
|
|
133
106
|
? '单次模式:收尾时停止所有后台服务。'
|
|
134
107
|
: '连续模式:收尾时不要停止后台服务,保持服务运行以便下个 session 继续使用。';
|
|
135
108
|
|
|
136
|
-
// Hint
|
|
109
|
+
// Hint 9: Tool usage guidance (critical for non-Claude models)
|
|
137
110
|
const toolGuidance = [
|
|
138
111
|
'可用工具与使用规范(严格遵守):',
|
|
139
112
|
'- 搜索文件名: Glob(如 **/*.ts),禁止 bash find',
|
|
@@ -149,7 +122,6 @@ function buildCodingPrompt(sessionNum, opts = {}) {
|
|
|
149
122
|
return [
|
|
150
123
|
`Session ${sessionNum}。执行 6 步流程。`,
|
|
151
124
|
'效率要求:先规划后编码,完成全部编码后再统一测试,禁止编码-测试反复跳转。后端任务用 curl 验证,不启动浏览器。',
|
|
152
|
-
reqSyncHint,
|
|
153
125
|
mcpHint,
|
|
154
126
|
testHint,
|
|
155
127
|
docsHint,
|
package/src/runner.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
|
-
const { paths, log, COLOR, loadConfig, ensureLoopDir, getProjectRoot
|
|
7
|
+
const { paths, log, COLOR, loadConfig, ensureLoopDir, getProjectRoot } = require('./config');
|
|
8
8
|
const { loadTasks, saveTasks, getFeatures, getStats, findNextTask } = require('./tasks');
|
|
9
9
|
const { validate } = require('./validator');
|
|
10
10
|
const { scan } = require('./scanner');
|
|
@@ -52,17 +52,44 @@ function allTasksDone() {
|
|
|
52
52
|
return features.every(f => f.status === 'done');
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function killServicesByProfile() {
|
|
56
|
+
const p = paths();
|
|
57
|
+
if (!fs.existsSync(p.profile)) return;
|
|
58
|
+
try {
|
|
59
|
+
const profile = JSON.parse(fs.readFileSync(p.profile, 'utf8'));
|
|
60
|
+
const services = profile.services || [];
|
|
61
|
+
const ports = services.map(s => s.port).filter(Boolean);
|
|
62
|
+
if (ports.length === 0) return;
|
|
63
|
+
|
|
64
|
+
const isWin = process.platform === 'win32';
|
|
65
|
+
for (const port of ports) {
|
|
66
|
+
try {
|
|
67
|
+
if (isWin) {
|
|
68
|
+
const out = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
69
|
+
const pids = [...new Set(out.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
|
|
70
|
+
for (const pid of pids) { try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' }); } catch { /* ignore */ } }
|
|
71
|
+
} else {
|
|
72
|
+
execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
|
|
73
|
+
}
|
|
74
|
+
} catch { /* no process on port */ }
|
|
75
|
+
}
|
|
76
|
+
log('info', `已停止端口 ${ports.join(', ')} 上的服务`);
|
|
77
|
+
} catch { /* ignore profile read errors */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
function rollback(headBefore, reason) {
|
|
56
81
|
if (!headBefore || headBefore === 'none') return;
|
|
82
|
+
|
|
83
|
+
killServicesByProfile();
|
|
84
|
+
|
|
57
85
|
log('warn', `回滚到 ${headBefore} ...`);
|
|
58
86
|
try {
|
|
59
|
-
execSync(`git reset --hard ${headBefore}`, { cwd: getProjectRoot(), stdio: '
|
|
87
|
+
execSync(`git reset --hard ${headBefore}`, { cwd: getProjectRoot(), stdio: 'pipe' });
|
|
60
88
|
log('ok', '回滚完成');
|
|
61
89
|
} catch (err) {
|
|
62
90
|
log('error', `回滚失败: ${err.message}`);
|
|
63
91
|
}
|
|
64
92
|
|
|
65
|
-
// Record failure in progress.json
|
|
66
93
|
appendProgress({
|
|
67
94
|
type: 'rollback',
|
|
68
95
|
timestamp: new Date().toISOString(),
|
|
@@ -102,7 +129,7 @@ function appendProgress(entry) {
|
|
|
102
129
|
if (fs.existsSync(p.progressFile)) {
|
|
103
130
|
try {
|
|
104
131
|
const text = fs.readFileSync(p.progressFile, 'utf8');
|
|
105
|
-
|
|
132
|
+
progress = JSON.parse(text);
|
|
106
133
|
} catch { /* reset */ }
|
|
107
134
|
}
|
|
108
135
|
if (!Array.isArray(progress.sessions)) progress.sessions = [];
|
|
@@ -110,36 +137,6 @@ function appendProgress(entry) {
|
|
|
110
137
|
fs.writeFileSync(p.progressFile, JSON.stringify(progress, null, 2) + '\n', 'utf8');
|
|
111
138
|
}
|
|
112
139
|
|
|
113
|
-
function updateSessionHistory(sessionData, sessionNum) {
|
|
114
|
-
const p = paths();
|
|
115
|
-
let sr = { current: null, history: [] };
|
|
116
|
-
if (fs.existsSync(p.sessionResult)) {
|
|
117
|
-
try {
|
|
118
|
-
const text = fs.readFileSync(p.sessionResult, 'utf8');
|
|
119
|
-
try { sr = JSON.parse(text); } catch { sr = JSON.parse(text.replace(/[\u201c\u201d]/g, '"')); }
|
|
120
|
-
} catch { /* reset */ }
|
|
121
|
-
if (!sr.history && sr.session_result) {
|
|
122
|
-
sr = { current: sr, history: [] };
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Move current to history
|
|
127
|
-
if (sr.current) {
|
|
128
|
-
sr.history.push({
|
|
129
|
-
session: sessionNum - 1,
|
|
130
|
-
timestamp: new Date().toISOString(),
|
|
131
|
-
...sr.current,
|
|
132
|
-
});
|
|
133
|
-
sr.current = null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (sessionData) {
|
|
137
|
-
sr.current = sessionData;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
fs.writeFileSync(p.sessionResult, JSON.stringify(sr, null, 2) + '\n', 'utf8');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
140
|
function printStats() {
|
|
144
141
|
const data = loadTasks();
|
|
145
142
|
if (!data) return;
|
|
@@ -247,6 +244,12 @@ async function run(requirement, opts = {}) {
|
|
|
247
244
|
log('info', `Session ${session} / ${maxSessions}`);
|
|
248
245
|
console.log('--------------------------------------------');
|
|
249
246
|
|
|
247
|
+
const taskData = loadTasks();
|
|
248
|
+
if (!taskData) {
|
|
249
|
+
log('error', 'tasks.json 无法读取,终止循环');
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
|
|
250
253
|
if (allTasksDone()) {
|
|
251
254
|
console.log('');
|
|
252
255
|
log('ok', '所有任务已完成!');
|
|
@@ -282,19 +285,6 @@ async function run(requirement, opts = {}) {
|
|
|
282
285
|
tryPush();
|
|
283
286
|
consecutiveFailures = 0;
|
|
284
287
|
|
|
285
|
-
// Update session history
|
|
286
|
-
updateSessionHistory(validateResult.sessionData, session);
|
|
287
|
-
|
|
288
|
-
// Update sync_state.json if requirements exist
|
|
289
|
-
const reqHash = getRequirementsHash();
|
|
290
|
-
if (reqHash) {
|
|
291
|
-
fs.writeFileSync(p.syncState, JSON.stringify({
|
|
292
|
-
last_requirements_hash: reqHash,
|
|
293
|
-
last_synced_at: new Date().toISOString(),
|
|
294
|
-
}, null, 2) + '\n', 'utf8');
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Append to progress.json
|
|
298
288
|
appendProgress({
|
|
299
289
|
session,
|
|
300
290
|
timestamp: new Date().toISOString(),
|
|
@@ -302,6 +292,7 @@ async function run(requirement, opts = {}) {
|
|
|
302
292
|
cost: sessionResult.cost,
|
|
303
293
|
taskId: validateResult.sessionData?.task_id || null,
|
|
304
294
|
statusAfter: validateResult.sessionData?.status_after || null,
|
|
295
|
+
notes: validateResult.sessionData?.notes || null,
|
|
305
296
|
});
|
|
306
297
|
|
|
307
298
|
} else {
|
|
@@ -319,7 +310,7 @@ async function run(requirement, opts = {}) {
|
|
|
319
310
|
}
|
|
320
311
|
|
|
321
312
|
// Periodic pause
|
|
322
|
-
if (session % pauseEvery === 0) {
|
|
313
|
+
if (pauseEvery > 0 && session % pauseEvery === 0) {
|
|
323
314
|
console.log('');
|
|
324
315
|
printStats();
|
|
325
316
|
const shouldContinue = await promptContinue();
|
|
@@ -330,6 +321,9 @@ async function run(requirement, opts = {}) {
|
|
|
330
321
|
}
|
|
331
322
|
}
|
|
332
323
|
|
|
324
|
+
// Cleanup: stop services after loop ends
|
|
325
|
+
killServicesByProfile();
|
|
326
|
+
|
|
333
327
|
// Final report
|
|
334
328
|
console.log('');
|
|
335
329
|
console.log('============================================');
|
|
@@ -345,6 +339,11 @@ async function add(instruction, opts = {}) {
|
|
|
345
339
|
const projectRoot = getProjectRoot();
|
|
346
340
|
ensureLoopDir();
|
|
347
341
|
|
|
342
|
+
const config = loadConfig();
|
|
343
|
+
if (config.provider !== 'claude' && config.baseUrl) {
|
|
344
|
+
log('ok', `模型配置已加载: ${config.provider}${config.model ? ` (${config.model})` : ''}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
348
347
|
if (!fs.existsSync(p.profile) || !fs.existsSync(p.tasksFile)) {
|
|
349
348
|
log('error', 'add 需要先完成初始化(至少运行一次 claude-coder run)');
|
|
350
349
|
process.exit(1);
|
package/src/session.js
CHANGED
|
@@ -212,6 +212,7 @@ async function runAddSession(instruction, opts = {}) {
|
|
|
212
212
|
const sdk = await loadSDK();
|
|
213
213
|
const config = loadConfig();
|
|
214
214
|
applyEnvConfig(config);
|
|
215
|
+
const indicator = new Indicator();
|
|
215
216
|
|
|
216
217
|
const systemPrompt = buildSystemPrompt(false);
|
|
217
218
|
const prompt = buildAddPrompt(instruction);
|
|
@@ -220,20 +221,34 @@ async function runAddSession(instruction, opts = {}) {
|
|
|
220
221
|
const logFile = path.join(p.logsDir, `add_tasks_${Date.now()}.log`);
|
|
221
222
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
222
223
|
|
|
224
|
+
indicator.start(0);
|
|
225
|
+
log('info', '正在追加任务...');
|
|
226
|
+
|
|
223
227
|
try {
|
|
224
228
|
const queryOpts = buildQueryOptions(config, opts);
|
|
225
229
|
queryOpts.systemPrompt = systemPrompt;
|
|
230
|
+
queryOpts.hooks = {
|
|
231
|
+
PreToolUse: [{
|
|
232
|
+
matcher: '*',
|
|
233
|
+
hooks: [async (input) => {
|
|
234
|
+
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
235
|
+
return {};
|
|
236
|
+
}]
|
|
237
|
+
}]
|
|
238
|
+
};
|
|
226
239
|
|
|
227
240
|
const session = sdk.query({ prompt, options: queryOpts });
|
|
228
241
|
|
|
229
242
|
for await (const message of session) {
|
|
230
|
-
logMessage(message, logStream);
|
|
243
|
+
logMessage(message, logStream, indicator);
|
|
231
244
|
}
|
|
232
245
|
|
|
233
246
|
logStream.end();
|
|
247
|
+
indicator.stop();
|
|
234
248
|
log('ok', '任务追加完成');
|
|
235
249
|
} catch (err) {
|
|
236
250
|
logStream.end();
|
|
251
|
+
indicator.stop();
|
|
237
252
|
log('error', `任务追加失败: ${err.message}`);
|
|
238
253
|
}
|
|
239
254
|
}
|
package/src/tasks.js
CHANGED
|
@@ -13,20 +13,15 @@ const TRANSITIONS = {
|
|
|
13
13
|
done: [],
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
function safeJsonParse(text) {
|
|
17
|
-
try {
|
|
18
|
-
return JSON.parse(text);
|
|
19
|
-
} catch {
|
|
20
|
-
return JSON.parse(
|
|
21
|
-
text.replace(/[\u201c\u201d]/g, '"').replace(/[\u2018\u2019]/g, "'")
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
16
|
function loadTasks() {
|
|
27
17
|
const p = paths();
|
|
28
18
|
if (!fs.existsSync(p.tasksFile)) return null;
|
|
29
|
-
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(p.tasksFile, 'utf8'));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
log('error', `tasks.json 解析失败: ${err.message}`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
function saveTasks(data) {
|
package/src/validator.js
CHANGED
|
@@ -4,16 +4,6 @@ const fs = require('fs');
|
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
5
|
const { paths, log, getProjectRoot } = require('./config');
|
|
6
6
|
|
|
7
|
-
function safeJsonParse(text) {
|
|
8
|
-
try {
|
|
9
|
-
return JSON.parse(text);
|
|
10
|
-
} catch {
|
|
11
|
-
return JSON.parse(
|
|
12
|
-
text.replace(/[\u201c\u201d]/g, '"').replace(/[\u2018\u2019]/g, "'")
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
7
|
function validateSessionResult() {
|
|
18
8
|
const p = paths();
|
|
19
9
|
|
|
@@ -24,43 +14,41 @@ function validateSessionResult() {
|
|
|
24
14
|
|
|
25
15
|
let data;
|
|
26
16
|
try {
|
|
27
|
-
data =
|
|
28
|
-
} catch {
|
|
29
|
-
log('error',
|
|
30
|
-
return { valid: false, fatal: true, reason:
|
|
17
|
+
data = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
log('error', `session_result.json 解析失败: ${err.message}`);
|
|
20
|
+
return { valid: false, fatal: true, reason: `JSON 解析失败: ${err.message}` };
|
|
31
21
|
}
|
|
32
22
|
|
|
33
|
-
const sr = data.current || data;
|
|
34
|
-
|
|
35
23
|
const required = ['session_result', 'status_after'];
|
|
36
|
-
const missing = required.filter(k => !(k in
|
|
24
|
+
const missing = required.filter(k => !(k in data));
|
|
37
25
|
if (missing.length > 0) {
|
|
38
26
|
log('error', `session_result.json 缺少字段: ${missing.join(', ')}`);
|
|
39
27
|
return { valid: false, fatal: true, reason: `缺少字段: ${missing.join(', ')}` };
|
|
40
28
|
}
|
|
41
29
|
|
|
42
|
-
if (!['success', 'failed'].includes(
|
|
43
|
-
log('error', `session_result 必须是 success 或 failed,实际是: ${
|
|
44
|
-
return { valid: false, fatal: true, reason: `无效 session_result: ${
|
|
30
|
+
if (!['success', 'failed'].includes(data.session_result)) {
|
|
31
|
+
log('error', `session_result 必须是 success 或 failed,实际是: ${data.session_result}`);
|
|
32
|
+
return { valid: false, fatal: true, reason: `无效 session_result: ${data.session_result}` };
|
|
45
33
|
}
|
|
46
34
|
|
|
47
35
|
const validStatuses = ['pending', 'in_progress', 'testing', 'done', 'failed'];
|
|
48
|
-
if (!validStatuses.includes(
|
|
49
|
-
log('error', `status_after 不合法: ${
|
|
50
|
-
return { valid: false, fatal: true, reason: `无效 status_after: ${
|
|
36
|
+
if (!validStatuses.includes(data.status_after)) {
|
|
37
|
+
log('error', `status_after 不合法: ${data.status_after}`);
|
|
38
|
+
return { valid: false, fatal: true, reason: `无效 status_after: ${data.status_after}` };
|
|
51
39
|
}
|
|
52
40
|
|
|
53
|
-
if (!
|
|
41
|
+
if (!data.task_id) {
|
|
54
42
|
log('warn', 'session_result.json 缺少 task_id (建议包含)');
|
|
55
43
|
}
|
|
56
44
|
|
|
57
|
-
if (
|
|
45
|
+
if (data.session_result === 'success') {
|
|
58
46
|
log('ok', 'session_result.json 合法 (success)');
|
|
59
47
|
} else {
|
|
60
48
|
log('warn', 'session_result.json 合法,但 Agent 报告失败 (failed)');
|
|
61
49
|
}
|
|
62
50
|
|
|
63
|
-
return { valid: true, fatal: false, data
|
|
51
|
+
return { valid: true, fatal: false, data };
|
|
64
52
|
}
|
|
65
53
|
|
|
66
54
|
function checkGitProgress(headBefore) {
|
|
@@ -96,14 +84,13 @@ function checkTestCoverage() {
|
|
|
96
84
|
if (!fs.existsSync(p.testsFile) || !fs.existsSync(p.sessionResult)) return;
|
|
97
85
|
|
|
98
86
|
try {
|
|
99
|
-
const sr =
|
|
100
|
-
const
|
|
101
|
-
const tests = safeJsonParse(fs.readFileSync(p.testsFile, 'utf8'));
|
|
87
|
+
const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
|
|
88
|
+
const tests = JSON.parse(fs.readFileSync(p.testsFile, 'utf8'));
|
|
102
89
|
|
|
103
|
-
const taskId =
|
|
90
|
+
const taskId = sr.task_id || '';
|
|
104
91
|
const testCases = tests.test_cases || [];
|
|
105
92
|
|
|
106
|
-
if (
|
|
93
|
+
if (sr.status_after === 'done' && sr.tests_passed) {
|
|
107
94
|
const taskTests = testCases.filter(t => t.feature_id === taskId);
|
|
108
95
|
if (taskTests.length > 0) {
|
|
109
96
|
const failed = taskTests.filter(t => t.last_result === 'fail');
|
package/templates/CLAUDE.md
CHANGED
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
| `.claude-coder/tasks.json` | 功能任务列表,带状态跟踪 | 只能修改 `status` 字段 |
|
|
50
50
|
| `.claude-coder/progress.json` | 跨会话记忆日志(外部循环自动维护) | 只读 |
|
|
51
51
|
| `.claude-coder/session_result.json` | 本次会话的结构化输出 | 每次会话结束时覆盖写入 |
|
|
52
|
-
| `.claude-coder/sync_state.json` | 需求同步状态(外部循环 session 成功后自动更新) | Agent 无需读写 |
|
|
53
52
|
| `.claude-coder/tests.json` | 功能验证记录(轻量) | 可新增和更新;仅当功能涉及 API 或核心逻辑时记录 |
|
|
54
53
|
|
|
55
54
|
### requirements.md 处理原则
|
|
@@ -178,10 +177,9 @@ pending ──→ in_progress ──→ testing ──→ done
|
|
|
178
177
|
1. **检查 prompt 注入的上下文**:
|
|
179
178
|
- 如果 prompt 中包含"任务上下文"(Hint 7),说明 harness 已注入当前任务信息,**跳过读取 tasks.json**,直接确认任务后进入第二步
|
|
180
179
|
- 如果 prompt 中包含"上次会话"(Hint 8),说明 harness 已注入上次会话摘要,**跳过读取 session_result.json 历史**
|
|
181
|
-
2. 批量读取以下文件(一次工具调用,跳过已注入的):`.claude-coder/project_profile.json`、`.claude-coder/tasks.json`(仅当无 Hint
|
|
182
|
-
3.
|
|
180
|
+
2. 批量读取以下文件(一次工具调用,跳过已注入的):`.claude-coder/project_profile.json`、`.claude-coder/tasks.json`(仅当无 Hint 6 时)
|
|
181
|
+
3. 如果无 Hint 7 且 `session_result.json` 不存在,运行 `git log --oneline -20` 补充上下文
|
|
183
182
|
4. 如果项目根目录存在 `requirements.md`,读取用户的详细需求和偏好(技术约束、样式要求等),作为本次会话的参考依据
|
|
184
|
-
5. **需求同步(条件触发)**:如果 prompt 中提示"需求已变更",读取 `requirements.md`,对比 `tasks.json`,将新增需求追加为 `pending` 任务。未提示则跳过
|
|
185
183
|
|
|
186
184
|
### 第二步:环境与健康检查
|
|
187
185
|
|