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 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
- .runtime/ # 临时文件
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) — 模块职责、提示语注入架构、注意力机制、Hook 数据流、后续优化方向
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 = require('path').resolve(opts.readFile);
111
- if (!require('fs').existsSync(reqPath)) {
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 = require('fs').readFileSync(reqPath, 'utf8');
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();
@@ -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/>fail → rollback"| coding
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 -->|fail| rollback["git reset --hard"]
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 校验引擎:session_result 结构校验 + git 检查 + 测试覆盖
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 结束 | 当前 + 历史 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()` + 9 个条件 hint | 主循环每次迭代 |
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 的 9 个条件 Hint
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 已注入当前任务上下文 + .claude-coder/ 路径提示 |
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 | 9 个条件 hint 精准注入,含 task/memory 上下文注入 + 服务管理 + 工具使用指导,减少 Agent 冗余操作 |
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
- | `sync_state.json` | Harness | 只读 |
324
- | `session_result.json` | Agent `current`,Harness 归档到 `history` | `current` |
325
- | `tasks.json` | Agent(仅 `status` 字段) | 修改 `status` |
326
- | `project_profile.json` | Agent(仅扫描阶段) | 扫描时写入 |
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. **Agent 自治**:Agent 通过 CLAUDE.md 协议自主决策,harness 只负责调度和校验
423
- 4. **幂等设计**:所有入口可重复执行,不产生副作用
424
- 5. **跨平台**:纯 Node.js + `child_process` 调用 git,无平台特定脚本
425
- 6. **运行时隔离**:每个项目的 `.claude-coder/` 独立,不同项目互不干扰
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
- .runtime/ # Temp files
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-coder",
3
- "version": "1.1.0",
3
+ "version": "1.4.0",
4
4
  "description": "Claude Coder — Autonomous coding agent harness powered by Claude Code SDK. Scan, plan, code, validate, git-commit in a loop.",
5
5
  "bin": {
6
6
  "claude-coder": "bin/cli.js"
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
- `运行时目录: .claude-coder/(隐藏目录,ls -a 可见,所有 tasks.json/profile 等文件均在此目录下)。` +
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
- try {
87
- execSync(`git reset --hard ${headBefore}`, { cwd: getProjectRoot(), stdio: 'pipe' });
88
- log('ok', '回滚完成');
89
- } catch (err) {
90
- log('error', `回滚失败: ${err.message}`);
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 || 5;
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
- log('ok', `Session ${session} 校验通过`);
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
- if (config.provider !== 'claude' && config.baseUrl) {
344
- log('ok', `模型配置已加载: ${config.provider}${config.model ? ` (${config.model})` : ''}`);
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 (config.model) base.model = config.model;
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 logFile = path.join(p.logsDir, `session_${sessionNum}_${Date.now()}.log`);
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.now()}.log`);
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.now()}.log`);
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('error', `session_result.json 缺少字段: ${missing.join(', ')}`);
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
- const srResult = validateSessionResult();
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', '========== 校验失败 (致命) ==========');
@@ -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`)