claude-coder 1.1.0 → 1.4.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 +9 -2
- package/bin/cli.js +19 -5
- package/docs/ARCHITECTURE.md +77 -23
- package/docs/PLAYWRIGHT_CREDENTIALS.md +161 -0
- package/docs/README.en.md +6 -1
- package/package.json +1 -1
- package/src/auth.js +107 -0
- package/src/config.js +3 -11
- package/src/prompts.js +19 -2
- package/src/runner.js +46 -9
- package/src/session.js +14 -4
- package/src/validator.js +23 -9
- package/templates/CLAUDE.md +4 -0
package/README.md
CHANGED
|
@@ -55,6 +55,8 @@ claude-coder run "实现用户注册和登录功能"
|
|
|
55
55
|
| `claude-coder init` | 初始化项目环境 |
|
|
56
56
|
| `claude-coder add "指令"` | 追加任务 |
|
|
57
57
|
| `claude-coder add -r [file]` | 从需求文件追加任务 |
|
|
58
|
+
| `claude-coder add "..." --model M` | 指定模型追加任务 |
|
|
59
|
+
| `claude-coder auth [url]` | 导出 Playwright 登录状态 |
|
|
58
60
|
| `claude-coder validate` | 手动校验 |
|
|
59
61
|
| `claude-coder status` | 查看进度和成本 |
|
|
60
62
|
| `claude-coder config sync` | 同步配置到 ~/.claude/ |
|
|
@@ -71,6 +73,8 @@ claude-coder run "实现用户注册和登录功能"
|
|
|
71
73
|
|
|
72
74
|
**追加任务**:`claude-coder add "新增管理员后台"` 或 `claude-coder add -r requirements.md` — 仅追加到任务列表,下次 run 时执行。
|
|
73
75
|
|
|
76
|
+
**自动测试 + 凭证持久化**:`claude-coder auth http://localhost:3000` — 导出浏览器登录态(cookies + localStorage),Agent 测试时自动使用。缺 API Key 时 Agent 会自行记录到 `test.env` 并继续推进,不会停工。详见 [测试凭证持久化方案](docs/PLAYWRIGHT_CREDENTIALS.md)。
|
|
77
|
+
|
|
74
78
|
## 模型支持
|
|
75
79
|
|
|
76
80
|
| 提供商 | 说明 |
|
|
@@ -92,7 +96,9 @@ your-project/
|
|
|
92
96
|
session_result.json # 上次 session 结果(扁平)
|
|
93
97
|
progress.json # 会话历史 + 成本
|
|
94
98
|
tests.json # 验证记录
|
|
95
|
-
.
|
|
99
|
+
test.env # 测试凭证(API Key 等,可选)
|
|
100
|
+
playwright-auth.json # Playwright 登录状态(可选,auth 命令生成)
|
|
101
|
+
.runtime/ # 临时文件(含日志)
|
|
96
102
|
requirements.md # 需求文档(可选)
|
|
97
103
|
```
|
|
98
104
|
|
|
@@ -108,7 +114,8 @@ your-project/
|
|
|
108
114
|
|
|
109
115
|
## 文档
|
|
110
116
|
|
|
111
|
-
- [技术架构](docs/ARCHITECTURE.md) —
|
|
117
|
+
- [技术架构](docs/ARCHITECTURE.md) — 核心设计规则、模块职责、提示语注入架构、注意力机制、Hook 数据流
|
|
118
|
+
- [测试凭证持久化方案](docs/PLAYWRIGHT_CREDENTIALS.md) — 自动测试的凭证管理:Playwright 登录态导出、API Key 持久化、Agent 缺凭证时的行为策略
|
|
112
119
|
|
|
113
120
|
## License
|
|
114
121
|
|
package/bin/cli.js
CHANGED
|
@@ -7,7 +7,8 @@ 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 "指令" | add -r [file]' },
|
|
10
|
+
add: { desc: '追加任务到 tasks.json', usage: 'claude-coder add "指令" [--model M] | add -r [file]' },
|
|
11
|
+
auth: { desc: '导出 Playwright 登录状态', usage: 'claude-coder auth [url]' },
|
|
11
12
|
validate: { desc: '手动校验上次 session', usage: 'claude-coder validate' },
|
|
12
13
|
status: { desc: '查看任务进度和成本', usage: 'claude-coder status' },
|
|
13
14
|
config: { desc: '配置管理', usage: 'claude-coder config sync' },
|
|
@@ -28,6 +29,9 @@ function showHelp() {
|
|
|
28
29
|
console.log(' claude-coder run --dry-run 预览模式');
|
|
29
30
|
console.log(' claude-coder add "新增搜索功能" 追加任务');
|
|
30
31
|
console.log(' claude-coder add -r 从 requirements.md 追加任务');
|
|
32
|
+
console.log(' claude-coder add "..." --model opus-4 指定模型追加任务');
|
|
33
|
+
console.log(' claude-coder auth 导出 Playwright 登录状态');
|
|
34
|
+
console.log(' claude-coder auth http://localhost:8080 指定登录 URL');
|
|
31
35
|
console.log(' claude-coder status 查看进度和成本');
|
|
32
36
|
console.log(`\n前置条件: npm install -g @anthropic-ai/claude-agent-sdk`);
|
|
33
37
|
}
|
|
@@ -35,7 +39,7 @@ function showHelp() {
|
|
|
35
39
|
function parseArgs(argv) {
|
|
36
40
|
const args = argv.slice(2);
|
|
37
41
|
const command = args[0];
|
|
38
|
-
const opts = { max: 50, pause: 0, dryRun: false, readFile: null };
|
|
42
|
+
const opts = { max: 50, pause: 0, dryRun: false, readFile: null, model: null };
|
|
39
43
|
const positional = [];
|
|
40
44
|
|
|
41
45
|
for (let i = 1; i < args.length; i++) {
|
|
@@ -49,6 +53,9 @@ function parseArgs(argv) {
|
|
|
49
53
|
case '--dry-run':
|
|
50
54
|
opts.dryRun = true;
|
|
51
55
|
break;
|
|
56
|
+
case '--model':
|
|
57
|
+
opts.model = args[++i] || null;
|
|
58
|
+
break;
|
|
52
59
|
case '-r': {
|
|
53
60
|
const next = args[i + 1];
|
|
54
61
|
if (next && !next.startsWith('-')) {
|
|
@@ -105,14 +112,16 @@ async function main() {
|
|
|
105
112
|
break;
|
|
106
113
|
}
|
|
107
114
|
case 'add': {
|
|
115
|
+
const fs = require('fs');
|
|
116
|
+
const nodePath = require('path');
|
|
108
117
|
let instruction = positional[0] || '';
|
|
109
118
|
if (opts.readFile) {
|
|
110
|
-
const reqPath =
|
|
111
|
-
if (!
|
|
119
|
+
const reqPath = nodePath.resolve(opts.readFile);
|
|
120
|
+
if (!fs.existsSync(reqPath)) {
|
|
112
121
|
console.error(`文件不存在: ${reqPath}`);
|
|
113
122
|
process.exit(1);
|
|
114
123
|
}
|
|
115
|
-
instruction =
|
|
124
|
+
instruction = fs.readFileSync(reqPath, 'utf8');
|
|
116
125
|
console.log(`已读取需求文件: ${opts.readFile}`);
|
|
117
126
|
}
|
|
118
127
|
if (!instruction) {
|
|
@@ -123,6 +132,11 @@ async function main() {
|
|
|
123
132
|
await runner.add(instruction, opts);
|
|
124
133
|
break;
|
|
125
134
|
}
|
|
135
|
+
case 'auth': {
|
|
136
|
+
const { auth } = require('../src/auth');
|
|
137
|
+
await auth(positional[0] || null);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
126
140
|
case 'validate': {
|
|
127
141
|
const validator = require('../src/validator');
|
|
128
142
|
const result = await validator.validate();
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -10,6 +10,54 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## 0. 核心设计规则(MUST READ)
|
|
14
|
+
|
|
15
|
+
> 以下规则按重要性排序(注意力 primacy zone),所有代码修改和架构决策必须遵循。
|
|
16
|
+
|
|
17
|
+
### 规则 1:长 Session 不停工
|
|
18
|
+
|
|
19
|
+
Agent 在单次 session 中应最大化推进任务进度。**任何非致命问题都不应中断 session**。
|
|
20
|
+
|
|
21
|
+
- 缺少 API Key → 用 mock 或代码逻辑验证替代,记录到 `test.env`,继续推进
|
|
22
|
+
- 测试环境未就绪 → 跳过该测试步骤,完成其余可验证的步骤
|
|
23
|
+
- 服务启动失败 → 尝试排查修复,无法修复则记录问题后推进代码实现
|
|
24
|
+
|
|
25
|
+
**反面案例**:Agent 因 `OPENAI_API_KEY` 缺失直接标记任务 `failed` → 浪费整个 session
|
|
26
|
+
|
|
27
|
+
### 规则 2:回滚即彻底回滚
|
|
28
|
+
|
|
29
|
+
`git reset --hard` 是全量回滚,不做部分文件保护。
|
|
30
|
+
|
|
31
|
+
- 凭证文件(`test.env`、`playwright-auth.json`)应通过 `.gitignore` 排除在 git 之外
|
|
32
|
+
- 如果回滚发生,说明 session 确实失败,代码应全部还原
|
|
33
|
+
- 不需要 backup/restore 机制 — 这是过度设计
|
|
34
|
+
|
|
35
|
+
### 规则 3:分层校验(fatal / recoverable / pass)
|
|
36
|
+
|
|
37
|
+
不是所有校验失败都需要回滚:
|
|
38
|
+
|
|
39
|
+
| 情况 | 有新 commit | 处理 |
|
|
40
|
+
|------|------------|------|
|
|
41
|
+
| session_result.json 格式异常 | 是 | **warn** — 代码已提交且可能正确,不回滚 |
|
|
42
|
+
| session_result.json 格式异常 | 否 | **fatal** — 无进展,回滚 |
|
|
43
|
+
| 代码结构性错误 | — | **fatal** — 回滚 |
|
|
44
|
+
| 全部通过 | — | **pass** — 推送 |
|
|
45
|
+
|
|
46
|
+
### 规则 4:凭证与代码分离
|
|
47
|
+
|
|
48
|
+
| 文件 | git 状态 | 说明 |
|
|
49
|
+
|------|---------|------|
|
|
50
|
+
| `test.env` | .gitignore | Agent 可写入发现的 API Key、测试账号 |
|
|
51
|
+
| `playwright-auth.json` | .gitignore | 用户通过 `claude-coder auth` 生成 |
|
|
52
|
+
| `session_result.json` | git-tracked | Agent 每次 session 覆盖写入 |
|
|
53
|
+
| `tasks.json` | git-tracked | Agent 修改 status 字段 |
|
|
54
|
+
|
|
55
|
+
### 规则 5:Harness 准备上下文,Agent 直接执行
|
|
56
|
+
|
|
57
|
+
Agent 不应浪费工具调用读取 harness 已知的数据。所有可预读的上下文通过 prompt hint 注入(见第 5 节 Prompt 注入架构)。
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
13
61
|
## 1. 核心架构
|
|
14
62
|
|
|
15
63
|
```mermaid
|
|
@@ -42,7 +90,7 @@ flowchart TB
|
|
|
42
90
|
|
|
43
91
|
query -->|Agent 工具调用| Files
|
|
44
92
|
validate -->|读取| runtime
|
|
45
|
-
validate -->|"pass → 下一 session<br/>
|
|
93
|
+
validate -->|"pass → 下一 session<br/>fatal → rollback<br/>recoverable + commit → warn"| coding
|
|
46
94
|
```
|
|
47
95
|
|
|
48
96
|
**核心特征:**
|
|
@@ -80,7 +128,7 @@ flowchart LR
|
|
|
80
128
|
pause_check -->|继续| session
|
|
81
129
|
done_check -->|是| finish([完成])
|
|
82
130
|
|
|
83
|
-
val -->|
|
|
131
|
+
val -->|fatal| rollback["git reset --hard"]
|
|
84
132
|
rollback --> retry_check{连续失败<br/>≥3次?}
|
|
85
133
|
retry_check -->|否| session
|
|
86
134
|
retry_check -->|是| mark_failed["标记 task failed"]
|
|
@@ -100,8 +148,9 @@ src/
|
|
|
100
148
|
prompts.js 提示语构建:系统 prompt 组合 + 条件 hint + 任务分解指导
|
|
101
149
|
init.js 环境初始化:读取 profile 执行依赖安装、服务启动、健康检查
|
|
102
150
|
scanner.js 初始化扫描:调用 runScanSession + 重试
|
|
103
|
-
validator.js
|
|
151
|
+
validator.js 校验引擎:分层校验(fatal/recoverable/pass)+ git 检查 + 测试覆盖
|
|
104
152
|
tasks.js 任务管理:CRUD + 状态机 + 进度展示
|
|
153
|
+
auth.js Playwright 凭证:导出登录状态 + MCP 配置 + gitignore
|
|
105
154
|
indicator.js 进度指示:终端 spinner + phase/step 文件写入
|
|
106
155
|
setup.js 交互式配置:模型选择、API Key、MCP 工具
|
|
107
156
|
templates/
|
|
@@ -125,8 +174,9 @@ templates/
|
|
|
125
174
|
| `src/prompts.js` | 提示语构建(系统 prompt + 条件 hint + 任务分解指导) |
|
|
126
175
|
| `src/init.js` | 环境初始化(依赖安装、服务启动) |
|
|
127
176
|
| `src/scanner.js` | 项目初始化扫描 |
|
|
128
|
-
| `src/validator.js` |
|
|
177
|
+
| `src/validator.js` | 校验引擎(分层校验) |
|
|
129
178
|
| `src/tasks.js` | 任务 CRUD + 状态机 |
|
|
179
|
+
| `src/auth.js` | Playwright 凭证持久化 |
|
|
130
180
|
| `src/indicator.js` | 终端进度指示器 |
|
|
131
181
|
| `src/setup.js` | 交互式配置向导 |
|
|
132
182
|
| `templates/CLAUDE.md` | Agent 协议 |
|
|
@@ -140,7 +190,8 @@ templates/
|
|
|
140
190
|
| `project_profile.json` | 首次扫描 | 项目元数据 |
|
|
141
191
|
| `tasks.json` | 首次扫描 | 任务列表 + 状态跟踪 |
|
|
142
192
|
| `progress.json` | 每次 session 结束 | 结构化会话日志 + 成本记录 |
|
|
143
|
-
| `session_result.json` | 每次 session 结束 | 当前
|
|
193
|
+
| `session_result.json` | 每次 session 结束 | 当前 session 结果(扁平格式,向后兼容旧 `current` 包装) |
|
|
194
|
+
| `playwright-auth.json` | `claude-coder auth` | Playwright 登录状态(cookies + localStorage) |
|
|
144
195
|
| `tests.json` | 首次测试时 | 验证记录(防止反复测试) |
|
|
145
196
|
| `.runtime/` | 运行时 | 临时文件(phase、step、activity.log、logs/) |
|
|
146
197
|
|
|
@@ -183,11 +234,11 @@ flowchart TB
|
|
|
183
234
|
|
|
184
235
|
| Session 类型 | systemPrompt | user prompt | 触发条件 |
|
|
185
236
|
|---|---|---|---|
|
|
186
|
-
| **编码** | CLAUDE.md | `buildCodingPrompt()` +
|
|
237
|
+
| **编码** | CLAUDE.md | `buildCodingPrompt()` + 11 个条件 hint | 主循环每次迭代 |
|
|
187
238
|
| **扫描** | CLAUDE.md + SCAN_PROTOCOL.md | `buildScanPrompt()` + 任务分解指导 + profile 质量要求 | 首次运行 |
|
|
188
239
|
| **追加** | CLAUDE.md | `buildAddPrompt()` + 任务分解指导 | `claude-coder add` |
|
|
189
240
|
|
|
190
|
-
### 编码 Session 的
|
|
241
|
+
### 编码 Session 的 11 个条件 Hint
|
|
191
242
|
|
|
192
243
|
| # | Hint | 触发条件 | 影响 |
|
|
193
244
|
|---|---|---|---|
|
|
@@ -196,7 +247,9 @@ flowchart TB
|
|
|
196
247
|
| 3 | `envHint` | 连续成功且 session>1 | Step 2:跳过 init |
|
|
197
248
|
| 4 | `testHint` | tests.json 有记录 | Step 5:避免重复验证 |
|
|
198
249
|
| 5 | `docsHint` | profile.existing_docs 非空或 profile 有缺陷 | Step 4:读文档后再编码;profile 缺陷时提示 Agent 在 Step 6 补全 services/docs |
|
|
199
|
-
| 6 | `taskHint` | tasks.json 存在且有待办任务 | Step 1:跳过读取 tasks.json,harness 已注入当前任务上下文 +
|
|
250
|
+
| 6 | `taskHint` | tasks.json 存在且有待办任务 | Step 1:跳过读取 tasks.json,harness 已注入当前任务上下文 + 项目绝对路径 |
|
|
251
|
+
| 6b | `testEnvHint` | 始终注入(内容因 test.env 是否存在而不同) | Step 5:存在时提示加载;不存在时告知可创建 |
|
|
252
|
+
| 6c | `playwrightAuthHint` | .claude-coder/playwright-auth.json 存在 | Step 5:提示 Agent 前端测试可使用已认证的浏览器状态 |
|
|
200
253
|
| 7 | `memoryHint` | session_result.json 存在(扁平格式) | Step 1:跳过读取 session_result.json,harness 已注入上次会话摘要 |
|
|
201
254
|
| 8 | `serviceHint` | 始终注入 | Step 6:单次模式停止服务,连续模式保持服务运行 |
|
|
202
255
|
| 9 | `toolGuidance` | 始终注入 | 全局:工具使用规范(Grep/Glob/Read/LS/MultiEdit/Task 替代 bash 命令),非 Claude 模型必需 |
|
|
@@ -267,7 +320,7 @@ sequenceDiagram
|
|
|
267
320
|
| 维度 | 评分 | 说明 |
|
|
268
321
|
|------|------|------|
|
|
269
322
|
| **CLAUDE.md 系统提示** | 8/10 | U 型注意力设计;铁律清晰;状态机和 6 步流程是核心竞争力 |
|
|
270
|
-
| **动态 prompt** | 9/10 |
|
|
323
|
+
| **动态 prompt** | 9/10 | 10 个条件 hint 精准注入,含 task/memory 上下文注入 + cwd 路径 + test.env + 服务管理 + 工具使用指导,减少 Agent 冗余操作 |
|
|
271
324
|
| **SCAN_PROTOCOL.md** | 8.5/10 | 新旧项目分支完整,profile 格式全面 |
|
|
272
325
|
| **tests.json 设计** | 7.5/10 | 精简字段,核心目的(防反复测试)明确 |
|
|
273
326
|
| **注入时机** | 9/10 | 静态规则 vs 动态上下文分离干净 |
|
|
@@ -317,13 +370,14 @@ PreToolUse hook 中追踪每个文件的编辑次数。当同一文件被 Write/
|
|
|
317
370
|
|
|
318
371
|
### 文件权限模型
|
|
319
372
|
|
|
320
|
-
| 文件 | 写入方 | Agent 权限 |
|
|
321
|
-
|
|
322
|
-
| `progress.json` | Harness | 只读 |
|
|
323
|
-
| `
|
|
324
|
-
| `
|
|
325
|
-
| `
|
|
326
|
-
| `
|
|
373
|
+
| 文件 | 写入方 | Agent 权限 | git 状态 |
|
|
374
|
+
|------|--------|-----------|---------|
|
|
375
|
+
| `progress.json` | Harness | 只读 | tracked |
|
|
376
|
+
| `session_result.json` | Agent 每次 session 覆盖写入(扁平格式) | 写入 | tracked |
|
|
377
|
+
| `tasks.json` | Agent(仅 `status` 字段) | 修改 `status` | tracked |
|
|
378
|
+
| `project_profile.json` | Agent(仅扫描阶段) | 扫描时写入 | tracked |
|
|
379
|
+
| `test.env` | Agent + 用户 | 可追加写入 | .gitignore |
|
|
380
|
+
| `playwright-auth.json` | 用户(`claude-coder auth`) | 只读 | .gitignore |
|
|
327
381
|
|
|
328
382
|
---
|
|
329
383
|
|
|
@@ -415,16 +469,16 @@ query({
|
|
|
415
469
|
|
|
416
470
|
---
|
|
417
471
|
|
|
418
|
-
##
|
|
472
|
+
## 实现原则
|
|
473
|
+
|
|
474
|
+
> 核心设计规则见 Section 0(primacy zone),以下为实现层面的补充原则。
|
|
419
475
|
|
|
420
476
|
1. **SDK 原生集成**:通过 `query()` 调用 Claude,内联 hooks,原生 cost tracking
|
|
421
477
|
2. **零硬依赖**:Claude Agent SDK 作为 peerDependency
|
|
422
|
-
3.
|
|
423
|
-
4.
|
|
424
|
-
5.
|
|
425
|
-
6.
|
|
426
|
-
7. **Prompt 架构分离**:静态规则在 `templates/`,动态上下文在 `src/prompts.js`
|
|
427
|
-
8. **文档即上下文**:文档在 harness 中分两层角色——Blueprint(`project_profile.json`,给 harness 的结构化元数据)和 Context Docs(`docs/ARCHITECTURE.md` 等,给 Agent 的人类可读文档)。Harness 通过 Hint 6 动态提醒 Agent 读取相关文档,并在 profile 有缺陷时提示补全
|
|
478
|
+
3. **幂等设计**:所有入口可重复执行,不产生副作用
|
|
479
|
+
4. **跨平台**:纯 Node.js + `child_process` 调用 git,无平台特定脚本
|
|
480
|
+
5. **运行时隔离**:每个项目的 `.claude-coder/` 独立,不同项目互不干扰
|
|
481
|
+
6. **文档即上下文**:Blueprint(`project_profile.json`)给 harness,Context Docs 给 Agent。Hint 6 动态提醒 Agent 读取相关文档
|
|
428
482
|
|
|
429
483
|
### 文档架构的学术依据
|
|
430
484
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# 测试凭证持久化方案
|
|
2
|
+
|
|
3
|
+
## 设计思想
|
|
4
|
+
|
|
5
|
+
claude-coder 的核心目标是让 Agent **完全自主测试**,不因凭证缺失而中断。测试中涉及三类凭证:
|
|
6
|
+
|
|
7
|
+
| 类型 | 示例 | 特点 |
|
|
8
|
+
|------|------|------|
|
|
9
|
+
| 浏览器状态 | 登录 cookies、localStorage 中的用户配置 | 有过期时间,跨 session 需要持久化 |
|
|
10
|
+
| API Key | OPENAI_API_KEY、ZHIPU_API_KEY | 长期有效,需安全存储 |
|
|
11
|
+
| 测试账号 | 注册的测试用户名密码、生成的 token | 可能是 Agent 自己创建的,需跨 session 传递 |
|
|
12
|
+
|
|
13
|
+
**核心原则**:
|
|
14
|
+
1. **Agent 可自行发现并持久化凭证** — 测试中发现需要的 API Key 或账号,直接写入 `test.env`
|
|
15
|
+
2. **凭证不受回滚影响** — `git reset --hard` 不会摧毁已保存的凭证
|
|
16
|
+
3. **零手动干预** — 除首次浏览器登录态外,其余由 Agent 自动处理
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 持久化架构
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
.claude-coder/
|
|
24
|
+
.env ← 模型配置(ANTHROPIC_API_KEY 等) [用户配置]
|
|
25
|
+
test.env ← 测试凭证(API Key、测试账号等) [Agent 可写]
|
|
26
|
+
playwright-auth.json ← 浏览器状态(cookies + localStorage) [auth 命令生成]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 文件生命周期
|
|
30
|
+
|
|
31
|
+
| 文件 | 创建方 | 写入方 | 回滚保护 | 生命周期 |
|
|
32
|
+
|------|--------|--------|----------|----------|
|
|
33
|
+
| `.env` | `claude-coder setup` | 用户 | 是 | 长期 |
|
|
34
|
+
| `test.env` | Agent 或用户 | Agent + 用户 | 是 | 长期,按需更新 |
|
|
35
|
+
| `playwright-auth.json` | `claude-coder auth` | auth 命令 | 是 | 中期,cookies 过期后需刷新 |
|
|
36
|
+
|
|
37
|
+
### 回滚保护机制
|
|
38
|
+
|
|
39
|
+
Harness 在 `git reset --hard` 前备份、后恢复以下文件:
|
|
40
|
+
- `session_result.json` — 会话结果
|
|
41
|
+
- `progress.json` — 历史记录
|
|
42
|
+
- `test.env` — 测试凭证
|
|
43
|
+
- `playwright-auth.json` — 浏览器状态
|
|
44
|
+
|
|
45
|
+
这确保无论回滚多少次,凭证始终保留。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 核心流程
|
|
50
|
+
|
|
51
|
+
### 流程 1:Agent 自动发现凭证
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Agent 测试 → 发现需要 API Key → 写入 test.env → 下次 session 自动加载
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Agent 在 CLAUDE.md Step 5 中被指导:测试中发现的凭证追加到 `.claude-coder/test.env`。Harness 在每次 session 的 prompt 中注入 hint,告知 Agent `test.env` 的存在和用法。
|
|
58
|
+
|
|
59
|
+
### 流程 2:用户预配置浏览器登录态
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
用户运行 claude-coder auth → 手动登录 → 状态自动保存 → Agent 测试时使用
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
适用于需要已登录状态才能测试的前端页面(如后台管理、需要 cookie 的 SPA)。
|
|
66
|
+
|
|
67
|
+
### 流程 3:用户预配置 API Key
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
用户编辑 test.env → 填入 API Key → Agent 测试前 source 加载
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
适用于后端功能依赖真实 API 调用的场景。
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## CLI 命令
|
|
78
|
+
|
|
79
|
+
### `claude-coder auth [url]`
|
|
80
|
+
|
|
81
|
+
一键导出浏览器登录态:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# 默认打开 http://localhost:3000
|
|
85
|
+
claude-coder auth
|
|
86
|
+
|
|
87
|
+
# 指定 URL
|
|
88
|
+
claude-coder auth http://localhost:8080/admin
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**自动完成**:
|
|
92
|
+
1. 启动 Playwright 浏览器,用户手动登录后关闭
|
|
93
|
+
2. 保存 cookies + localStorage 到 `.claude-coder/playwright-auth.json`
|
|
94
|
+
3. 创建/更新 `.mcp.json`,配置 `--storage-state`
|
|
95
|
+
4. 添加 `.gitignore` 条目
|
|
96
|
+
5. 启用 `.claude-coder/.env` 中 `MCP_PLAYWRIGHT=true`
|
|
97
|
+
|
|
98
|
+
### `claude-coder setup`(相关)
|
|
99
|
+
|
|
100
|
+
配置模型时可启用 Playwright MCP:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
claude-coder setup
|
|
104
|
+
# 选择启用 MCP_PLAYWRIGHT=true
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 场景示例
|
|
110
|
+
|
|
111
|
+
### 场景 1:全栈项目首次测试
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# 1. 配置模型
|
|
115
|
+
claude-coder setup
|
|
116
|
+
|
|
117
|
+
# 2. 填入后端测试需要的 API Key
|
|
118
|
+
cat >> .claude-coder/test.env << 'EOF'
|
|
119
|
+
OPENAI_API_KEY=sk-xxx
|
|
120
|
+
ZHIPU_API_KEY=xxx.xxx
|
|
121
|
+
EOF
|
|
122
|
+
|
|
123
|
+
# 3. 导出前端登录态(可选,Agent 也能用 Playwright MCP 自动登录)
|
|
124
|
+
claude-coder auth http://localhost:3000
|
|
125
|
+
|
|
126
|
+
# 4. 开始自动编码和测试
|
|
127
|
+
claude-coder run
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 场景 2:Agent 自主发现并处理凭证缺失
|
|
131
|
+
|
|
132
|
+
Agent 在测试 feat-005(AI 内容生成)时发现需要 `OPENAI_API_KEY`:
|
|
133
|
+
|
|
134
|
+
1. Agent 尝试调用 API → 报错 "API key required"
|
|
135
|
+
2. Agent **不中断任务**,改用替代验证方式(如 mock 响应、检查代码逻辑是否正确、验证接口可达性)
|
|
136
|
+
3. Agent 将凭证需求写入 `test.env`:`echo 'OPENAI_API_KEY=需要配置' >> .claude-coder/test.env`
|
|
137
|
+
4. Agent 在 `session_result.json` 的 notes 中记录:"AI 内容生成功能已实现,但需要真实 OPENAI_API_KEY 才能完整测试,已记录到 test.env"
|
|
138
|
+
5. Agent 完成其他可验证的步骤后标记任务为 `done`(功能已实现)或 `testing`(等待凭证后完整验证)
|
|
139
|
+
|
|
140
|
+
**核心原则**:缺少凭证不等于任务失败。Agent 应最大化推进,将凭证问题记录为后续补充项,而非阻塞整个 session。
|
|
141
|
+
|
|
142
|
+
### 场景 3:前端 localStorage 配置持久化
|
|
143
|
+
|
|
144
|
+
项目的前端将 LLM 服务商配置存储在 localStorage 中:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# 启动前后端服务
|
|
148
|
+
# 运行 auth,手动在页面中配置 LLM 设置
|
|
149
|
+
claude-coder auth http://localhost:3000
|
|
150
|
+
|
|
151
|
+
# playwright-auth.json 中已包含 localStorage 数据
|
|
152
|
+
# 后续 Agent 使用 Playwright MCP 测试时自动加载这些配置
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 场景 4:cookies 过期后刷新
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# 重新运行 auth 即可
|
|
159
|
+
claude-coder auth http://localhost:3000
|
|
160
|
+
# 新的 cookies 覆盖旧文件,立即生效
|
|
161
|
+
```
|
package/docs/README.en.md
CHANGED
|
@@ -55,6 +55,8 @@ Each session, the agent autonomously follows 6 steps: restore context → env ch
|
|
|
55
55
|
| `claude-coder init` | Initialize project environment |
|
|
56
56
|
| `claude-coder add "instruction"` | Append tasks |
|
|
57
57
|
| `claude-coder add -r [file]` | Append tasks from requirements file |
|
|
58
|
+
| `claude-coder add "..." --model M` | Append tasks with specific model |
|
|
59
|
+
| `claude-coder auth [url]` | Export Playwright login state |
|
|
58
60
|
| `claude-coder validate` | Manually validate last session |
|
|
59
61
|
| `claude-coder status` | View progress and costs |
|
|
60
62
|
| `claude-coder config sync` | Sync config to ~/.claude/ |
|
|
@@ -82,13 +84,16 @@ your-project/
|
|
|
82
84
|
session_result.json # Last session result (flat)
|
|
83
85
|
progress.json # Session history + costs
|
|
84
86
|
tests.json # Verification records
|
|
85
|
-
.
|
|
87
|
+
test.env # Test credentials (API keys, optional)
|
|
88
|
+
playwright-auth.json # Playwright login state (optional, via auth command)
|
|
89
|
+
.runtime/ # Temp files (logs)
|
|
86
90
|
requirements.md # Requirements (optional)
|
|
87
91
|
```
|
|
88
92
|
|
|
89
93
|
## Documentation
|
|
90
94
|
|
|
91
95
|
- [Architecture](ARCHITECTURE.md) — Module responsibilities, prompt injection architecture, attention mechanism, hook data flow, future roadmap
|
|
96
|
+
- [Playwright Credentials](PLAYWRIGHT_CREDENTIALS.md) — Test cookies and API key management
|
|
92
97
|
|
|
93
98
|
## License
|
|
94
99
|
|
package/package.json
CHANGED
package/src/auth.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const { paths, log, getProjectRoot, ensureLoopDir } = require('./config');
|
|
7
|
+
|
|
8
|
+
function updateGitignore(entry) {
|
|
9
|
+
const gitignorePath = path.join(getProjectRoot(), '.gitignore');
|
|
10
|
+
let content = '';
|
|
11
|
+
if (fs.existsSync(gitignorePath)) {
|
|
12
|
+
content = fs.readFileSync(gitignorePath, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
if (content.includes(entry)) return;
|
|
15
|
+
|
|
16
|
+
const suffix = content.endsWith('\n') || content === '' ? '' : '\n';
|
|
17
|
+
fs.appendFileSync(gitignorePath, `${suffix}${entry}\n`, 'utf8');
|
|
18
|
+
log('ok', `.gitignore 已添加: ${entry}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function updateMcpConfig(authFilePath) {
|
|
22
|
+
const p = paths();
|
|
23
|
+
let mcpConfig = {};
|
|
24
|
+
if (fs.existsSync(p.mcpConfig)) {
|
|
25
|
+
try {
|
|
26
|
+
mcpConfig = JSON.parse(fs.readFileSync(p.mcpConfig, 'utf8'));
|
|
27
|
+
} catch {
|
|
28
|
+
log('warn', '.mcp.json 解析失败,将覆盖');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
33
|
+
|
|
34
|
+
const relAuthPath = path.relative(getProjectRoot(), authFilePath);
|
|
35
|
+
mcpConfig.mcpServers.playwright = {
|
|
36
|
+
command: 'npx',
|
|
37
|
+
args: [
|
|
38
|
+
'@playwright/mcp@latest',
|
|
39
|
+
`--storage-state=${relAuthPath}`,
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
fs.writeFileSync(p.mcpConfig, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
|
|
44
|
+
log('ok', `.mcp.json 已配置 Playwright MCP (storage-state: ${relAuthPath})`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function enableMcpPlaywrightEnv() {
|
|
48
|
+
const p = paths();
|
|
49
|
+
if (!fs.existsSync(p.envFile)) return;
|
|
50
|
+
|
|
51
|
+
let content = fs.readFileSync(p.envFile, 'utf8');
|
|
52
|
+
if (/^MCP_PLAYWRIGHT=/m.test(content)) {
|
|
53
|
+
content = content.replace(/^MCP_PLAYWRIGHT=.*/m, 'MCP_PLAYWRIGHT=true');
|
|
54
|
+
} else {
|
|
55
|
+
const suffix = content.endsWith('\n') ? '' : '\n';
|
|
56
|
+
content += `${suffix}MCP_PLAYWRIGHT=true\n`;
|
|
57
|
+
}
|
|
58
|
+
fs.writeFileSync(p.envFile, content, 'utf8');
|
|
59
|
+
log('ok', '.claude-coder/.env 已设置 MCP_PLAYWRIGHT=true');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function auth(url) {
|
|
63
|
+
ensureLoopDir();
|
|
64
|
+
const p = paths();
|
|
65
|
+
const targetUrl = url || 'http://localhost:3000';
|
|
66
|
+
|
|
67
|
+
log('info', '启动 Playwright 浏览器,请手动登录...');
|
|
68
|
+
log('info', `目标 URL: ${targetUrl}`);
|
|
69
|
+
log('info', `登录状态将保存到: ${p.playwrightAuth}`);
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log('操作步骤:');
|
|
72
|
+
console.log(' 1. 浏览器将自动打开,请手动完成登录');
|
|
73
|
+
console.log(' 2. 登录成功后关闭浏览器窗口');
|
|
74
|
+
console.log(' 3. 登录状态(cookies + localStorage)将自动保存');
|
|
75
|
+
console.log('');
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
execSync(
|
|
79
|
+
`npx playwright codegen --save-storage="${p.playwrightAuth}" "${targetUrl}"`,
|
|
80
|
+
{ stdio: 'inherit', cwd: getProjectRoot() }
|
|
81
|
+
);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (!fs.existsSync(p.playwrightAuth)) {
|
|
84
|
+
log('error', `Playwright 登录状态导出失败: ${err.message}`);
|
|
85
|
+
log('info', '请确保已安装 playwright: npx playwright install');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!fs.existsSync(p.playwrightAuth)) {
|
|
91
|
+
log('error', '未检测到导出的登录状态文件');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
log('ok', '登录状态已保存');
|
|
96
|
+
|
|
97
|
+
updateMcpConfig(p.playwrightAuth);
|
|
98
|
+
updateGitignore('.claude-coder/playwright-auth.json');
|
|
99
|
+
enableMcpPlaywrightEnv();
|
|
100
|
+
|
|
101
|
+
console.log('');
|
|
102
|
+
log('ok', 'Playwright 凭证配置完成!');
|
|
103
|
+
log('info', '后续运行 claude-coder run 时,Agent 的前端测试将自动使用已认证状态');
|
|
104
|
+
log('info', '注意: cookies 有过期时间,需要定期重新运行 claude-coder auth 更新');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { auth };
|
package/src/config.js
CHANGED
|
@@ -55,6 +55,9 @@ 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
|
+
testEnvFile: path.join(loopDir, 'test.env'),
|
|
59
|
+
playwrightAuth: path.join(loopDir, 'playwright-auth.json'),
|
|
60
|
+
mcpConfig: path.join(getProjectRoot(), '.mcp.json'),
|
|
58
61
|
claudeMd: getTemplatePath('CLAUDE.md'),
|
|
59
62
|
scanProtocol: getTemplatePath('SCAN_PROTOCOL.md'),
|
|
60
63
|
runtime,
|
|
@@ -187,16 +190,6 @@ function syncToGlobal() {
|
|
|
187
190
|
log('ok', `已同步配置到 ${settingsPath}`);
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
// --------------- Requirements hash ---------------
|
|
191
|
-
|
|
192
|
-
function getRequirementsHash() {
|
|
193
|
-
const crypto = require('crypto');
|
|
194
|
-
const reqFile = path.join(getProjectRoot(), 'requirements.md');
|
|
195
|
-
if (!fs.existsSync(reqFile)) return '';
|
|
196
|
-
const content = fs.readFileSync(reqFile, 'utf8');
|
|
197
|
-
return crypto.createHash('sha256').update(content).digest('hex');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
193
|
module.exports = {
|
|
201
194
|
COLOR,
|
|
202
195
|
log,
|
|
@@ -210,5 +203,4 @@ module.exports = {
|
|
|
210
203
|
buildEnvVars,
|
|
211
204
|
getAllowedTools,
|
|
212
205
|
syncToGlobal,
|
|
213
|
-
getRequirementsHash,
|
|
214
206
|
};
|
package/src/prompts.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const { paths, loadConfig } = require('./config');
|
|
4
|
+
const { paths, loadConfig, getProjectRoot } = require('./config');
|
|
5
5
|
const { loadTasks, findNextTask, getStats } = require('./tasks');
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -72,6 +72,7 @@ function buildCodingPrompt(sessionNum, opts = {}) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// Hint 6: Task context (harness pre-read, saves Agent 2-3 Read calls)
|
|
75
|
+
const projectRoot = getProjectRoot();
|
|
75
76
|
let taskHint = '';
|
|
76
77
|
try {
|
|
77
78
|
const taskData = loadTasks();
|
|
@@ -82,12 +83,26 @@ function buildCodingPrompt(sessionNum, opts = {}) {
|
|
|
82
83
|
taskHint = `任务上下文: ${next.id} "${next.description}" (${next.status}), ` +
|
|
83
84
|
`category=${next.category}, steps=${next.steps.length}步。` +
|
|
84
85
|
`进度: ${stats.done}/${stats.total} done, ${stats.failed} failed。` +
|
|
85
|
-
|
|
86
|
+
`项目绝对路径: ${projectRoot}。运行时目录: ${projectRoot}/.claude-coder/(隐藏目录)。` +
|
|
86
87
|
`第一步无需读取 tasks.json(已注入),直接确认任务后进入 Step 2。`;
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
} catch { /* ignore */ }
|
|
90
91
|
|
|
92
|
+
// Hint 6b: Test environment variables (readable + writable by Agent)
|
|
93
|
+
let testEnvHint = '';
|
|
94
|
+
if (p.testEnvFile && fs.existsSync(p.testEnvFile)) {
|
|
95
|
+
testEnvHint = `测试凭证文件: ${projectRoot}/.claude-coder/test.env(含 API Key、测试账号等),测试前用 source ${projectRoot}/.claude-coder/test.env 加载。发现新凭证需求时可追加写入(KEY=value 格式)。`;
|
|
96
|
+
} else {
|
|
97
|
+
testEnvHint = `如需持久化测试凭证(API Key、测试账号密码等),写入 ${projectRoot}/.claude-coder/test.env(KEY=value 格式,每行一个)。后续 session 会自动感知。`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Hint 6c: Playwright authenticated state
|
|
101
|
+
let playwrightAuthHint = '';
|
|
102
|
+
if (p.playwrightAuth && fs.existsSync(p.playwrightAuth)) {
|
|
103
|
+
playwrightAuthHint = `已检测到 Playwright 登录状态(${projectRoot}/.claude-coder/playwright-auth.json),前端/全栈测试将使用已认证的浏览器会话(含 cookies 和 localStorage)。`;
|
|
104
|
+
}
|
|
105
|
+
|
|
91
106
|
// Hint 7: Session memory (read flat session_result.json)
|
|
92
107
|
let memoryHint = '';
|
|
93
108
|
if (fs.existsSync(p.sessionResult)) {
|
|
@@ -127,6 +142,8 @@ function buildCodingPrompt(sessionNum, opts = {}) {
|
|
|
127
142
|
docsHint,
|
|
128
143
|
envHint,
|
|
129
144
|
taskHint,
|
|
145
|
+
testEnvHint,
|
|
146
|
+
playwrightAuthHint,
|
|
130
147
|
memoryHint,
|
|
131
148
|
serviceHint,
|
|
132
149
|
toolGuidance,
|
package/src/runner.js
CHANGED
|
@@ -77,17 +77,38 @@ function killServicesByProfile() {
|
|
|
77
77
|
} catch { /* ignore profile read errors */ }
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function sleepSync(ms) {
|
|
81
|
+
const end = Date.now() + ms;
|
|
82
|
+
while (Date.now() < end) { /* busy wait */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
80
85
|
function rollback(headBefore, reason) {
|
|
81
86
|
if (!headBefore || headBefore === 'none') return;
|
|
82
87
|
|
|
83
88
|
killServicesByProfile();
|
|
84
89
|
|
|
90
|
+
if (process.platform === 'win32') sleepSync(1500);
|
|
91
|
+
|
|
92
|
+
const cwd = getProjectRoot();
|
|
93
|
+
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
94
|
+
|
|
85
95
|
log('warn', `回滚到 ${headBefore} ...`);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
|
|
97
|
+
let success = false;
|
|
98
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
99
|
+
try {
|
|
100
|
+
execSync(`git reset --hard ${headBefore}`, { cwd, stdio: 'pipe', env: gitEnv });
|
|
101
|
+
log('ok', '回滚完成');
|
|
102
|
+
success = true;
|
|
103
|
+
break;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (attempt === 1) {
|
|
106
|
+
log('warn', `回滚首次失败,等待后重试: ${err.message}`);
|
|
107
|
+
sleepSync(2000);
|
|
108
|
+
} else {
|
|
109
|
+
log('error', `回滚失败: ${err.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
91
112
|
}
|
|
92
113
|
|
|
93
114
|
appendProgress({
|
|
@@ -95,6 +116,7 @@ function rollback(headBefore, reason) {
|
|
|
95
116
|
timestamp: new Date().toISOString(),
|
|
96
117
|
reason: reason || 'harness 校验失败',
|
|
97
118
|
rollbackTo: headBefore,
|
|
119
|
+
success,
|
|
98
120
|
});
|
|
99
121
|
}
|
|
100
122
|
|
|
@@ -161,7 +183,7 @@ async function run(requirement, opts = {}) {
|
|
|
161
183
|
ensureLoopDir();
|
|
162
184
|
|
|
163
185
|
const maxSessions = opts.max || 50;
|
|
164
|
-
const pauseEvery = opts.pause
|
|
186
|
+
const pauseEvery = opts.pause ?? 0;
|
|
165
187
|
const dryRun = opts.dryRun || false;
|
|
166
188
|
|
|
167
189
|
console.log('');
|
|
@@ -267,10 +289,13 @@ async function run(requirement, opts = {}) {
|
|
|
267
289
|
}
|
|
268
290
|
|
|
269
291
|
const headBefore = getHead();
|
|
292
|
+
const nextTask = findNextTask(taskData);
|
|
293
|
+
const taskId = nextTask?.id || 'unknown';
|
|
270
294
|
|
|
271
295
|
// Run coding session
|
|
272
296
|
const sessionResult = await runCodingSession(session, {
|
|
273
297
|
projectRoot,
|
|
298
|
+
taskId,
|
|
274
299
|
consecutiveFailures,
|
|
275
300
|
maxSessions,
|
|
276
301
|
lastValidateLog: consecutiveFailures > 0 ? '上次校验失败' : '',
|
|
@@ -281,7 +306,11 @@ async function run(requirement, opts = {}) {
|
|
|
281
306
|
const validateResult = await validate(headBefore);
|
|
282
307
|
|
|
283
308
|
if (!validateResult.fatal) {
|
|
284
|
-
|
|
309
|
+
if (validateResult.hasWarnings) {
|
|
310
|
+
log('warn', `Session ${session} 校验通过 (有自动修复或警告)`);
|
|
311
|
+
} else {
|
|
312
|
+
log('ok', `Session ${session} 校验通过`);
|
|
313
|
+
}
|
|
285
314
|
tryPush();
|
|
286
315
|
consecutiveFailures = 0;
|
|
287
316
|
|
|
@@ -340,10 +369,18 @@ async function add(instruction, opts = {}) {
|
|
|
340
369
|
ensureLoopDir();
|
|
341
370
|
|
|
342
371
|
const config = loadConfig();
|
|
343
|
-
|
|
344
|
-
|
|
372
|
+
|
|
373
|
+
if (!opts.model) {
|
|
374
|
+
if (config.defaultOpus) {
|
|
375
|
+
opts.model = config.defaultOpus;
|
|
376
|
+
} else if (config.model) {
|
|
377
|
+
opts.model = config.model;
|
|
378
|
+
}
|
|
345
379
|
}
|
|
346
380
|
|
|
381
|
+
const displayModel = opts.model || config.model || '(default)';
|
|
382
|
+
log('ok', `模型配置已加载: ${config.provider || 'claude'} (add 使用: ${displayModel})`);
|
|
383
|
+
|
|
347
384
|
if (!fs.existsSync(p.profile) || !fs.existsSync(p.tasksFile)) {
|
|
348
385
|
log('error', 'add 需要先完成初始化(至少运行一次 claude-coder run)');
|
|
349
386
|
process.exit(1);
|
package/src/session.js
CHANGED
|
@@ -51,7 +51,8 @@ function buildQueryOptions(config, opts = {}) {
|
|
|
51
51
|
env: buildEnvVars(config),
|
|
52
52
|
settingSources: ['project'],
|
|
53
53
|
};
|
|
54
|
-
if (
|
|
54
|
+
if (opts.model) base.model = opts.model;
|
|
55
|
+
else if (config.model) base.model = config.model;
|
|
55
56
|
return base;
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -62,6 +63,10 @@ function extractResult(messages) {
|
|
|
62
63
|
return null;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
function stripAnsi(str) {
|
|
67
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
function logMessage(message, logStream, indicator) {
|
|
66
71
|
if (message.type === 'assistant' && message.message?.content) {
|
|
67
72
|
for (const block of message.message.content) {
|
|
@@ -70,6 +75,9 @@ function logMessage(message, logStream, indicator) {
|
|
|
70
75
|
const statusLine = indicator.getStatusLine();
|
|
71
76
|
process.stderr.write('\r\x1b[K');
|
|
72
77
|
if (statusLine) process.stderr.write(statusLine + '\n');
|
|
78
|
+
if (logStream && statusLine) {
|
|
79
|
+
logStream.write('\n' + stripAnsi(statusLine) + '\n');
|
|
80
|
+
}
|
|
73
81
|
}
|
|
74
82
|
process.stdout.write(block.text);
|
|
75
83
|
if (logStream) logStream.write(block.text);
|
|
@@ -88,7 +96,9 @@ async function runCodingSession(sessionNum, opts = {}) {
|
|
|
88
96
|
const systemPrompt = buildSystemPrompt(false);
|
|
89
97
|
|
|
90
98
|
const p = paths();
|
|
91
|
-
const
|
|
99
|
+
const taskId = opts.taskId || 'unknown';
|
|
100
|
+
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
101
|
+
const logFile = path.join(p.logsDir, `${taskId}_session_${sessionNum}_${dateStr}.log`);
|
|
92
102
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
93
103
|
|
|
94
104
|
indicator.start(sessionNum);
|
|
@@ -164,7 +174,7 @@ async function runScanSession(requirement, opts = {}) {
|
|
|
164
174
|
const systemPrompt = buildSystemPrompt(true);
|
|
165
175
|
|
|
166
176
|
const p = paths();
|
|
167
|
-
const logFile = path.join(p.logsDir, `scan_${Date.
|
|
177
|
+
const logFile = path.join(p.logsDir, `scan_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
|
|
168
178
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
169
179
|
|
|
170
180
|
indicator.start(0);
|
|
@@ -218,7 +228,7 @@ async function runAddSession(instruction, opts = {}) {
|
|
|
218
228
|
const prompt = buildAddPrompt(instruction);
|
|
219
229
|
|
|
220
230
|
const p = paths();
|
|
221
|
-
const logFile = path.join(p.logsDir, `add_tasks_${Date.
|
|
231
|
+
const logFile = path.join(p.logsDir, `add_tasks_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
|
|
222
232
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
223
233
|
|
|
224
234
|
indicator.start(0);
|
package/src/validator.js
CHANGED
|
@@ -9,7 +9,7 @@ function validateSessionResult() {
|
|
|
9
9
|
|
|
10
10
|
if (!fs.existsSync(p.sessionResult)) {
|
|
11
11
|
log('error', 'Agent 未生成 session_result.json');
|
|
12
|
-
return { valid: false, fatal: true, reason: 'session_result.json 不存在' };
|
|
12
|
+
return { valid: false, fatal: true, recoverable: false, reason: 'session_result.json 不存在' };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
let data;
|
|
@@ -17,25 +17,30 @@ function validateSessionResult() {
|
|
|
17
17
|
data = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
|
|
18
18
|
} catch (err) {
|
|
19
19
|
log('error', `session_result.json 解析失败: ${err.message}`);
|
|
20
|
-
return { valid: false, fatal: true, reason: `JSON 解析失败: ${err.message}` };
|
|
20
|
+
return { valid: false, fatal: true, recoverable: false, reason: `JSON 解析失败: ${err.message}` };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Backward compat: unwrap legacy { current: {...} } format
|
|
24
|
+
if (data.current && typeof data.current === 'object') {
|
|
25
|
+
data = data.current;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
const required = ['session_result', 'status_after'];
|
|
24
29
|
const missing = required.filter(k => !(k in data));
|
|
25
30
|
if (missing.length > 0) {
|
|
26
|
-
log('
|
|
27
|
-
return { valid: false, fatal: true, reason: `缺少字段: ${missing.join(', ')}` };
|
|
31
|
+
log('warn', `session_result.json 缺少字段: ${missing.join(', ')}`);
|
|
32
|
+
return { valid: false, fatal: false, recoverable: true, reason: `缺少字段: ${missing.join(', ')}` };
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
if (!['success', 'failed'].includes(data.session_result)) {
|
|
31
36
|
log('error', `session_result 必须是 success 或 failed,实际是: ${data.session_result}`);
|
|
32
|
-
return { valid: false, fatal: true, reason: `无效 session_result: ${data.session_result}` };
|
|
37
|
+
return { valid: false, fatal: true, recoverable: false, reason: `无效 session_result: ${data.session_result}` };
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
const validStatuses = ['pending', 'in_progress', 'testing', 'done', 'failed'];
|
|
36
41
|
if (!validStatuses.includes(data.status_after)) {
|
|
37
42
|
log('error', `status_after 不合法: ${data.status_after}`);
|
|
38
|
-
return { valid: false, fatal: true, reason: `无效 status_after: ${data.status_after}` };
|
|
43
|
+
return { valid: false, fatal: true, recoverable: false, reason: `无效 status_after: ${data.status_after}` };
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
if (!data.task_id) {
|
|
@@ -48,7 +53,7 @@ function validateSessionResult() {
|
|
|
48
53
|
log('warn', 'session_result.json 合法,但 Agent 报告失败 (failed)');
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
return { valid: true, fatal: false, data };
|
|
56
|
+
return { valid: true, fatal: false, recoverable: false, data };
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
function checkGitProgress(headBefore) {
|
|
@@ -107,12 +112,21 @@ function checkTestCoverage() {
|
|
|
107
112
|
async function validate(headBefore) {
|
|
108
113
|
log('info', '========== 开始校验 ==========');
|
|
109
114
|
|
|
110
|
-
|
|
115
|
+
let srResult = validateSessionResult();
|
|
111
116
|
const gitResult = checkGitProgress(headBefore);
|
|
117
|
+
|
|
118
|
+
// Tiered: has commit + session_result issue → warn, don't rollback good code
|
|
119
|
+
if (srResult.recoverable && gitResult.hasCommit) {
|
|
120
|
+
log('warn', 'session_result.json 格式异常,但有新提交,降级为警告(不回滚代码)');
|
|
121
|
+
} else if (srResult.recoverable && !gitResult.hasCommit) {
|
|
122
|
+
log('error', '无新提交且 session_result.json 格式错误,视为致命');
|
|
123
|
+
srResult.fatal = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
112
126
|
checkTestCoverage();
|
|
113
127
|
|
|
114
128
|
const fatal = srResult.fatal;
|
|
115
|
-
const hasWarnings = gitResult.warning;
|
|
129
|
+
const hasWarnings = gitResult.warning || srResult.recoverable;
|
|
116
130
|
|
|
117
131
|
if (fatal) {
|
|
118
132
|
log('error', '========== 校验失败 (致命) ==========');
|
package/templates/CLAUDE.md
CHANGED
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
| `.claude-coder/progress.json` | 跨会话记忆日志(外部循环自动维护) | 只读 |
|
|
51
51
|
| `.claude-coder/session_result.json` | 本次会话的结构化输出 | 每次会话结束时覆盖写入 |
|
|
52
52
|
| `.claude-coder/tests.json` | 功能验证记录(轻量) | 可新增和更新;仅当功能涉及 API 或核心逻辑时记录 |
|
|
53
|
+
| `.claude-coder/test.env` | 测试凭证(API Key、测试账号等) | **可追加写入**;发现测试需要的凭证时持久化到此文件 |
|
|
54
|
+
| `.claude-coder/playwright-auth.json` | 浏览器登录状态(cookies + localStorage) | 只读;由用户通过 `claude-coder auth` 预配置 |
|
|
53
55
|
|
|
54
56
|
### requirements.md 处理原则
|
|
55
57
|
|
|
@@ -232,6 +234,8 @@ pending ──→ in_progress ──→ testing ──→ done
|
|
|
232
234
|
|
|
233
235
|
6. **记录验证命令**:如果本功能涉及 API 或核心逻辑,在 `tests.json` 中追加一条记录(含 `last_run_session` 为当前 session 编号)。纯配置 / 纯样式 / 改动 < 100 行的任务无需记录
|
|
234
236
|
|
|
237
|
+
7. **凭证持久化**:测试中发现需要的凭证(API Key、测试账号密码等),追加写入 `.claude-coder/test.env`,格式为 `KEY=value`(每行一个)。后续 session 会自动感知该文件。确保 `test.env` 已在 `.gitignore` 中(不被 git 追踪)
|
|
238
|
+
|
|
235
239
|
**禁止**:
|
|
236
240
|
- 后端任务启动浏览器测试
|
|
237
241
|
- 创建独立测试文件(`test-*.js` / `test-*.html`)
|