claude-coder 1.2.0 → 1.5.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
@@ -53,9 +53,10 @@ claude-coder run "实现用户注册和登录功能"
53
53
  | `claude-coder run --max 1` | 单次执行 |
54
54
  | `claude-coder run --dry-run` | 预览模式 |
55
55
  | `claude-coder init` | 初始化项目环境 |
56
- | `claude-coder add "指令"` | 追加任务(默认用 opus 级模型推理) |
56
+ | `claude-coder add "指令"` | 追加任务 |
57
57
  | `claude-coder add -r [file]` | 从需求文件追加任务 |
58
58
  | `claude-coder add "..." --model M` | 指定模型追加任务 |
59
+ | `claude-coder auth [url]` | 导出 Playwright 登录状态 |
59
60
  | `claude-coder validate` | 手动校验 |
60
61
  | `claude-coder status` | 查看进度和成本 |
61
62
  | `claude-coder config sync` | 同步配置到 ~/.claude/ |
@@ -72,6 +73,8 @@ claude-coder run "实现用户注册和登录功能"
72
73
 
73
74
  **追加任务**:`claude-coder add "新增管理员后台"` 或 `claude-coder add -r requirements.md` — 仅追加到任务列表,下次 run 时执行。
74
75
 
76
+ **自动测试 + 凭证持久化**:`claude-coder auth http://localhost:3000` — 导出浏览器登录态(cookies + localStorage),Agent 测试时自动使用。缺 API Key 时 Agent 会自行记录到 `test.env` 并继续推进,不会停工。详见 [测试凭证持久化方案](docs/PLAYWRIGHT_CREDENTIALS.md)。
77
+
75
78
  ## 模型支持
76
79
 
77
80
  | 提供商 | 说明 |
@@ -94,7 +97,9 @@ your-project/
94
97
  progress.json # 会话历史 + 成本
95
98
  tests.json # 验证记录
96
99
  test.env # 测试凭证(API Key 等,可选)
97
- .runtime/ # 临时文件(含日志)
100
+ playwright-auth.json # Playwright 登录状态(可选,auth 命令生成)
101
+ .runtime/ # 临时文件
102
+ logs/ # 每 session 独立日志 + activity log
98
103
  requirements.md # 需求文档(可选)
99
104
  ```
100
105
 
@@ -104,14 +109,16 @@ your-project/
104
109
 
105
110
  **中断恢复**:直接重新运行 `claude-coder run`,会从上次中断处继续。
106
111
 
112
+ **长时间无响应**:模型处理复杂文件时可能出现 10-20 分钟的思考间隔(spinner 会显示红色警告),这是正常行为。超过 30 分钟无工具调用时 Harness 会自动中断并重试。可通过 `.env` 中 `SESSION_STALL_TIMEOUT=秒数` 调整阈值。
113
+
107
114
  **跳过任务**:将 `.claude-coder/tasks.json` 中该任务的 `status` 改为 `done`。
108
115
 
109
116
  **Windows 支持**:完全支持,纯 Node.js 实现。
110
117
 
111
118
  ## 文档
112
119
 
113
- - [技术架构](docs/ARCHITECTURE.md) — 模块职责、提示语注入架构、注意力机制、Hook 数据流、后续优化方向
114
- - [Playwright 凭证持久化](docs/PLAYWRIGHT_CREDENTIALS.md) — 测试 cookies 和 API Key 管理方案
120
+ - [技术架构](docs/ARCHITECTURE.md) — 核心设计规则、模块职责、提示语注入架构、注意力机制、Hook 数据流
121
+ - [测试凭证持久化方案](docs/PLAYWRIGHT_CREDENTIALS.md) — 自动测试的凭证管理:Playwright 登录态导出、API Key 持久化、Agent 缺凭证时的行为策略
115
122
 
116
123
  ## License
117
124
 
package/bin/cli.js CHANGED
@@ -8,6 +8,7 @@ const COMMANDS = {
8
8
  setup: { desc: '交互式模型配置', usage: 'claude-coder setup' },
9
9
  init: { desc: '初始化项目环境', usage: 'claude-coder init' },
10
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' },
@@ -29,6 +30,8 @@ function showHelp() {
29
30
  console.log(' claude-coder add "新增搜索功能" 追加任务');
30
31
  console.log(' claude-coder add -r 从 requirements.md 追加任务');
31
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');
32
35
  console.log(' claude-coder status 查看进度和成本');
33
36
  console.log(`\n前置条件: npm install -g @anthropic-ai/claude-agent-sdk`);
34
37
  }
@@ -109,14 +112,16 @@ async function main() {
109
112
  break;
110
113
  }
111
114
  case 'add': {
115
+ const fs = require('fs');
116
+ const nodePath = require('path');
112
117
  let instruction = positional[0] || '';
113
118
  if (opts.readFile) {
114
- const reqPath = require('path').resolve(opts.readFile);
115
- if (!require('fs').existsSync(reqPath)) {
119
+ const reqPath = nodePath.resolve(opts.readFile);
120
+ if (!fs.existsSync(reqPath)) {
116
121
  console.error(`文件不存在: ${reqPath}`);
117
122
  process.exit(1);
118
123
  }
119
- instruction = require('fs').readFileSync(reqPath, 'utf8');
124
+ instruction = fs.readFileSync(reqPath, 'utf8');
120
125
  console.log(`已读取需求文件: ${opts.readFile}`);
121
126
  }
122
127
  if (!instruction) {
@@ -127,6 +132,11 @@ async function main() {
127
132
  await runner.add(instruction, opts);
128
133
  break;
129
134
  }
135
+ case 'auth': {
136
+ const { auth } = require('../src/auth');
137
+ await auth(positional[0] || null);
138
+ break;
139
+ }
130
140
  case 'validate': {
131
141
  const validator = require('../src/validator');
132
142
  const result = await validator.validate();
@@ -10,6 +10,67 @@
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
+ - **长时间思考是正常行为**:模型处理大文件(如 500+ 行的代码文件)时可能出现 10-20 分钟的思考间隔,不代表卡死
25
+
26
+ **反面案例**:Agent 因 `OPENAI_API_KEY` 缺失直接标记任务 `failed` → 浪费整个 session
27
+
28
+ > Harness 兜底机制:当工具调用间隔超过 `SESSION_STALL_TIMEOUT`(默认 30 分钟)时自动中断 session 并触发 rollback + 重试。此阈值设计为远超正常思考时长,仅捕捉真正的卡死场景。
29
+
30
+ ### 规则 2:回滚即彻底回滚
31
+
32
+ `git reset --hard` 是全量回滚,不做部分文件保护。
33
+
34
+ - 凭证文件(`test.env`、`playwright-auth.json`)应通过 `.gitignore` 排除在 git 之外
35
+ - 如果回滚发生,说明 session 确实失败,代码应全部还原
36
+ - 不需要 backup/restore 机制 — 这是过度设计
37
+
38
+ ### 规则 3:分层校验(fatal / recoverable / pass)
39
+
40
+ 不是所有校验失败都需要回滚:
41
+
42
+ | 情况 | 有新 commit | 处理 |
43
+ |------|------------|------|
44
+ | session_result.json 格式异常 | 是 | **warn** — 代码已提交且可能正确,不回滚 |
45
+ | session_result.json 格式异常 | 否 | **fatal** — 无进展,回滚 |
46
+ | 代码结构性错误 | — | **fatal** — 回滚 |
47
+ | 全部通过 | — | **pass** — 推送 |
48
+
49
+ ### 规则 4:凭证与代码分离
50
+
51
+ | 文件 | git 状态 | 说明 |
52
+ |------|---------|------|
53
+ | `test.env` | .gitignore | Agent 可写入发现的 API Key、测试账号 |
54
+ | `playwright-auth.json` | .gitignore | 用户通过 `claude-coder auth` 生成 |
55
+ | `session_result.json` | git-tracked | Agent 每次 session 覆盖写入 |
56
+ | `tasks.json` | git-tracked | Agent 修改 status 字段 |
57
+
58
+ ### 规则 5:Harness 准备上下文,Agent 直接执行
59
+
60
+ Agent 不应浪费工具调用读取 harness 已知的数据。所有可预读的上下文通过 prompt hint 注入(见第 5 节 Prompt 注入架构)。
61
+
62
+ ### 规则 6:停顿检测 — 模型卡死自动恢复
63
+
64
+ 模型可能长时间「思考」但不调用工具(20+ 分钟无进展)。Harness 通过 PreToolUse hook 追踪最后一次工具调用时间:
65
+
66
+ - 无工具调用 > `SESSION_STALL_TIMEOUT`(默认 1800 秒 / 30 分钟) → 自动中断 session
67
+ - 中断后进入 runner 的重试逻辑(连续失败 ≥ 3 次 → 标记 task failed)
68
+ - Spinner 在无工具调用 > 2 分钟时显示红色警告
69
+
70
+ 配置方式:`.claude-coder/.env` 中设置 `SESSION_STALL_TIMEOUT=1800`(秒)
71
+
72
+ ---
73
+
13
74
  ## 1. 核心架构
14
75
 
15
76
  ```mermaid
@@ -31,7 +92,7 @@ flowchart TB
31
92
  direction TB
32
93
  profile["project_profile.json<br/>tasks.json"]
33
94
  runtime["session_result.json<br/>progress.json"]
34
- phase[".runtime/<br/>phase / step / activity.log"]
95
+ phase[".runtime/<br/>phase / step / logs/"]
35
96
  end
36
97
 
37
98
  scan -->|"systemPrompt =<br/>CLAUDE.md + SCAN_PROTOCOL.md"| query
@@ -42,13 +103,14 @@ flowchart TB
42
103
 
43
104
  query -->|Agent 工具调用| Files
44
105
  validate -->|读取| runtime
45
- validate -->|"pass → 下一 session<br/>fail → rollback"| coding
106
+ validate -->|"pass → 下一 session<br/>fatal → rollback<br/>recoverable + commit → warn"| coding
46
107
  ```
47
108
 
48
109
  **核心特征:**
49
110
  - **项目无关**:项目信息由 Agent 扫描后存入 `project_profile.json`,harness 不含项目特定逻辑
50
111
  - **可恢复**:通过 `session_result.json` 跨会话记忆,任意 session 可断点续跑
51
- - **可观测**:SDK 内联 `PreToolUse` hook 实时显示 Agent 当前步骤和工具调用
112
+ - **可观测**:SDK 内联 `PreToolUse` hook 实时显示当前工具、操作目标和停顿警告
113
+ - **自愈**:编辑死循环检测 + 停顿超时自动中断 + runner 重试机制
52
114
  - **跨平台**:纯 Node.js 实现,macOS / Linux / Windows 通用
53
115
  - **零依赖**:`dependencies` 为空,Claude Agent SDK 作为 peerDependency
54
116
 
@@ -80,7 +142,7 @@ flowchart LR
80
142
  pause_check -->|继续| session
81
143
  done_check -->|是| finish([完成])
82
144
 
83
- val -->|fail| rollback["git reset --hard"]
145
+ val -->|fatal| rollback["git reset --hard"]
84
146
  rollback --> retry_check{连续失败<br/>≥3次?}
85
147
  retry_check -->|否| session
86
148
  retry_check -->|是| mark_failed["标记 task failed"]
@@ -96,13 +158,14 @@ bin/cli.js CLI 入口:参数解析、命令路由、SDK peerDep 检
96
158
  src/
97
159
  config.js 配置管理:.env 加载、模型映射、环境变量构建、全局同步
98
160
  runner.js 主循环:scan → session → validate → retry/rollback
99
- session.js SDK 交互:query() 调用、hook 绑定、日志流
161
+ session.js SDK 交互:query() 调用、hook 绑定、停顿检测、日志流
100
162
  prompts.js 提示语构建:系统 prompt 组合 + 条件 hint + 任务分解指导
101
163
  init.js 环境初始化:读取 profile 执行依赖安装、服务启动、健康检查
102
164
  scanner.js 初始化扫描:调用 runScanSession + 重试
103
- validator.js 校验引擎:session_result 结构校验 + git 检查 + 测试覆盖
165
+ validator.js 校验引擎:分层校验(fatal/recoverable/pass)+ git 检查 + 测试覆盖
104
166
  tasks.js 任务管理:CRUD + 状态机 + 进度展示
105
- indicator.js 进度指示:终端 spinner + phase/step 文件写入
167
+ auth.js Playwright 凭证:导出登录状态 + MCP 配置 + gitignore
168
+ indicator.js 进度指示:终端 spinner + 工具目标显示 + 停顿警告 + phase/step 文件写入
106
169
  setup.js 交互式配置:模型选择、API Key、MCP 工具
107
170
  templates/
108
171
  CLAUDE.md Agent 协议(注入为 systemPrompt)
@@ -125,8 +188,9 @@ templates/
125
188
  | `src/prompts.js` | 提示语构建(系统 prompt + 条件 hint + 任务分解指导) |
126
189
  | `src/init.js` | 环境初始化(依赖安装、服务启动) |
127
190
  | `src/scanner.js` | 项目初始化扫描 |
128
- | `src/validator.js` | 校验引擎 |
191
+ | `src/validator.js` | 校验引擎(分层校验) |
129
192
  | `src/tasks.js` | 任务 CRUD + 状态机 |
193
+ | `src/auth.js` | Playwright 凭证持久化 |
130
194
  | `src/indicator.js` | 终端进度指示器 |
131
195
  | `src/setup.js` | 交互式配置向导 |
132
196
  | `templates/CLAUDE.md` | Agent 协议 |
@@ -140,9 +204,10 @@ templates/
140
204
  | `project_profile.json` | 首次扫描 | 项目元数据 |
141
205
  | `tasks.json` | 首次扫描 | 任务列表 + 状态跟踪 |
142
206
  | `progress.json` | 每次 session 结束 | 结构化会话日志 + 成本记录 |
143
- | `session_result.json` | 每次 session 结束 | 当前 + 历史 session 结果 |
207
+ | `session_result.json` | 每次 session 结束 | 当前 session 结果(扁平格式,向后兼容旧 `current` 包装) |
208
+ | `playwright-auth.json` | `claude-coder auth` | Playwright 登录状态(cookies + localStorage) |
144
209
  | `tests.json` | 首次测试时 | 验证记录(防止反复测试) |
145
- | `.runtime/` | 运行时 | 临时文件(phase、step、activity.log、logs/) |
210
+ | `.runtime/` | 运行时 | 临时文件(phase、step、logs/session_N.activity.log |
146
211
 
147
212
  ---
148
213
 
@@ -183,11 +248,11 @@ flowchart TB
183
248
 
184
249
  | Session 类型 | systemPrompt | user prompt | 触发条件 |
185
250
  |---|---|---|---|
186
- | **编码** | CLAUDE.md | `buildCodingPrompt()` + 10 个条件 hint | 主循环每次迭代 |
251
+ | **编码** | CLAUDE.md | `buildCodingPrompt()` + 11 个条件 hint | 主循环每次迭代 |
187
252
  | **扫描** | CLAUDE.md + SCAN_PROTOCOL.md | `buildScanPrompt()` + 任务分解指导 + profile 质量要求 | 首次运行 |
188
253
  | **追加** | CLAUDE.md | `buildAddPrompt()` + 任务分解指导 | `claude-coder add` |
189
254
 
190
- ### 编码 Session 的 10 个条件 Hint
255
+ ### 编码 Session 的 11 个条件 Hint
191
256
 
192
257
  | # | Hint | 触发条件 | 影响 |
193
258
  |---|---|---|---|
@@ -197,7 +262,8 @@ flowchart TB
197
262
  | 4 | `testHint` | tests.json 有记录 | Step 5:避免重复验证 |
198
263
  | 5 | `docsHint` | profile.existing_docs 非空或 profile 有缺陷 | Step 4:读文档后再编码;profile 缺陷时提示 Agent 在 Step 6 补全 services/docs |
199
264
  | 6 | `taskHint` | tasks.json 存在且有待办任务 | Step 1:跳过读取 tasks.json,harness 已注入当前任务上下文 + 项目绝对路径 |
200
- | 6b | `testEnvHint` | .claude-coder/test.env 存在 | Step 5:提示 Agent 在测试前加载测试环境变量 |
265
+ | 6b | `testEnvHint` | 始终注入(内容因 test.env 是否存在而不同) | Step 5:存在时提示加载;不存在时告知可创建 |
266
+ | 6c | `playwrightAuthHint` | .claude-coder/playwright-auth.json 存在 | Step 5:提示 Agent 前端测试可使用已认证的浏览器状态 |
201
267
  | 7 | `memoryHint` | session_result.json 存在(扁平格式) | Step 1:跳过读取 session_result.json,harness 已注入上次会话摘要 |
202
268
  | 8 | `serviceHint` | 始终注入 | Step 6:单次模式停止服务,连续模式保持服务运行 |
203
269
  | 9 | `toolGuidance` | 始终注入 | 全局:工具使用规范(Grep/Glob/Read/LS/MultiEdit/Task 替代 bash 命令),非 Claude 模型必需 |
@@ -247,17 +313,27 @@ sequenceDiagram
247
313
  participant SDK as Claude Agent SDK
248
314
  participant Hook as inferPhaseStep()
249
315
  participant Ind as Indicator (setInterval)
316
+ participant Stall as stallChecker (30s)
250
317
  participant Term as 终端
251
318
 
252
319
  SDK->>Hook: PreToolUse 回调<br/>{tool_name: "Edit", tool_input: {path: "src/app.tsx"}}
253
320
  Hook->>Hook: 推断阶段: Edit → coding
254
321
  Hook->>Ind: updatePhase("coding")
322
+ Hook->>Ind: lastToolTime = now
323
+ Hook->>Ind: toolTarget = "src/app.tsx"
255
324
  Hook->>Ind: appendActivity("Edit", "src/app.tsx")
256
325
 
257
326
  Note over SDK,Hook: 同步回调,return {decision: "allow"}
258
327
 
259
328
  loop 每 500ms
260
- Ind->>Term: ⠋ [Session 3] 编码中 02:15 | Git 操作
329
+ Ind->>Term: ⠋ [Session 3] 编码中 02:15 | 读取文件: ppt_generator.py
330
+ end
331
+
332
+ loop 每 30s
333
+ Stall->>Ind: 检查 now - lastToolTime
334
+ alt 超过 STALL_TIMEOUT
335
+ Stall->>SDK: stallDetected = true → break for-await
336
+ end
261
337
  end
262
338
  ```
263
339
 
@@ -312,19 +388,23 @@ Harness 在 `buildCodingPrompt()` 中预读 `tasks.json`,将下一个待办任
312
388
 
313
389
  Harness 在 `buildCodingPrompt()` 中预读 `session_result.json`,将上次会话的 task_id、结果和 notes 摘要注入 user prompt。Agent 无需自行读取历史 session 数据。
314
390
 
315
- ### Loop Detection(编辑死循环检测)
391
+ ### 自愈机制
392
+
393
+ **编辑死循环检测**:PreToolUse hook 追踪每个文件的编辑次数,同一文件 Write/Edit 超 5 次 → `decision: "block"`。
316
394
 
317
- PreToolUse hook 中追踪每个文件的编辑次数。当同一文件被 Write/Edit 超过 5 次时,hook 返回 `decision: "block"` 阻止操作并提示 Agent 重新审视方案。
395
+ **停顿超时检测**:每 30 秒检查 `indicator.lastToolTime`,若距上次工具调用超过 `SESSION_STALL_TIMEOUT`(默认 1800 / 30 分钟),自动 `break` 退出并触发 rollback + 重试。
396
+ > 注意:模型在处理复杂文件时可能出现 10-20 分钟的长时间思考,这是正常行为。超时设为 30 分钟以避免误杀正常思考。可通过 `.env` 中 `SESSION_STALL_TIMEOUT=秒数` 自定义。
318
397
 
319
398
  ### 文件权限模型
320
399
 
321
- | 文件 | 写入方 | Agent 权限 |
322
- |------|--------|-----------|
323
- | `progress.json` | Harness | 只读 |
324
- | `sync_state.json` | Harness | 只读 |
325
- | `session_result.json` | Agent `current`,Harness 归档到 `history` | `current` |
326
- | `tasks.json` | Agent(仅 `status` 字段) | 修改 `status` |
327
- | `project_profile.json` | Agent(仅扫描阶段) | 扫描时写入 |
400
+ | 文件 | 写入方 | Agent 权限 | git 状态 |
401
+ |------|--------|-----------|---------|
402
+ | `progress.json` | Harness | 只读 | tracked |
403
+ | `session_result.json` | Agent 每次 session 覆盖写入(扁平格式) | 写入 | tracked |
404
+ | `tasks.json` | Agent(仅 `status` 字段) | 修改 `status` | tracked |
405
+ | `project_profile.json` | Agent(仅扫描阶段) | 扫描时写入 | tracked |
406
+ | `test.env` | Agent + 用户 | 可追加写入 | .gitignore |
407
+ | `playwright-auth.json` | 用户(`claude-coder auth`) | 只读 | .gitignore |
328
408
 
329
409
  ---
330
410
 
@@ -416,16 +496,16 @@ query({
416
496
 
417
497
  ---
418
498
 
419
- ## 设计原则
499
+ ## 实现原则
500
+
501
+ > 核心设计规则见 Section 0(primacy zone),以下为实现层面的补充原则。
420
502
 
421
503
  1. **SDK 原生集成**:通过 `query()` 调用 Claude,内联 hooks,原生 cost tracking
422
504
  2. **零硬依赖**:Claude Agent SDK 作为 peerDependency
423
- 3. **Agent 自治**:Agent 通过 CLAUDE.md 协议自主决策,harness 只负责调度和校验
424
- 4. **幂等设计**:所有入口可重复执行,不产生副作用
425
- 5. **跨平台**:纯 Node.js + `child_process` 调用 git,无平台特定脚本
426
- 6. **运行时隔离**:每个项目的 `.claude-coder/` 独立,不同项目互不干扰
427
- 7. **Prompt 架构分离**:静态规则在 `templates/`,动态上下文在 `src/prompts.js`
428
- 8. **文档即上下文**:文档在 harness 中分两层角色——Blueprint(`project_profile.json`,给 harness 的结构化元数据)和 Context Docs(`docs/ARCHITECTURE.md` 等,给 Agent 的人类可读文档)。Harness 通过 Hint 6 动态提醒 Agent 读取相关文档,并在 profile 有缺陷时提示补全
505
+ 3. **幂等设计**:所有入口可重复执行,不产生副作用
506
+ 4. **跨平台**:纯 Node.js + `child_process` 调用 git,无平台特定脚本
507
+ 5. **运行时隔离**:每个项目的 `.claude-coder/` 独立,不同项目互不干扰
508
+ 6. **文档即上下文**:Blueprint(`project_profile.json`)给 harness,Context Docs 给 Agent。Hint 6 动态提醒 Agent 读取相关文档
429
509
 
430
510
  ### 文档架构的学术依据
431
511
 
@@ -1,131 +1,161 @@
1
- # Playwright MCP 凭证持久化方案
1
+ # 测试凭证持久化方案
2
2
 
3
- ## 背景
3
+ ## 设计思想
4
4
 
5
- 在使用 claude-coder 运行涉及前端测试的任务时,Playwright MCP 可能需要:
6
- 1. 已登录状态的 cookies(如后台管理页面)
7
- 2. API Key 等测试凭证(如 AI 生成功能需要真实 API 调用)
5
+ claude-coder 的核心目标是让 Agent **完全自主测试**,不因凭证缺失而中断。测试中涉及三类凭证:
8
6
 
9
- 本文档描述如何在 claude-coder 工作流中管理这些凭证。
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 自动处理
10
17
 
11
18
  ---
12
19
 
13
- ## 方案 1: Playwright --storage-state(推荐用于 cookies)
20
+ ## 持久化架构
14
21
 
15
- ### 原理
22
+ ```
23
+ .claude-coder/
24
+ .env ← 模型配置(ANTHROPIC_API_KEY 等) [用户配置]
25
+ test.env ← 测试凭证(API Key、测试账号等) [Agent 可写]
26
+ playwright-auth.json ← 浏览器状态(cookies + localStorage) [auth 命令生成]
27
+ ```
16
28
 
17
- `@playwright/mcp` 支持 `--storage-state=<path>` 参数,加载预存的浏览器状态(cookies、localStorage)。
29
+ ### 文件生命周期
18
30
 
19
- ### 步骤
31
+ | 文件 | 创建方 | 写入方 | 回滚保护 | 生命周期 |
32
+ |------|--------|--------|----------|----------|
33
+ | `.env` | `claude-coder setup` | 用户 | 是 | 长期 |
34
+ | `test.env` | Agent 或用户 | Agent + 用户 | 是 | 长期,按需更新 |
35
+ | `playwright-auth.json` | `claude-coder auth` | auth 命令 | 是 | 中期,cookies 过期后需刷新 |
20
36
 
21
- **1. 手动登录并导出状态**
37
+ ### 回滚保护机制
22
38
 
23
- ```bash
24
- # 启动 Playwright,手动登录后导出
25
- npx playwright codegen --save-storage=.claude-coder/playwright-auth.json http://localhost:3000
26
- ```
39
+ Harness 在 `git reset --hard` 前备份、后恢复以下文件:
40
+ - `session_result.json` — 会话结果
41
+ - `progress.json` — 历史记录
42
+ - `test.env` — 测试凭证
43
+ - `playwright-auth.json` — 浏览器状态
27
44
 
28
- 登录完成后关闭浏览器,状态自动保存到 `playwright-auth.json`。
45
+ 这确保无论回滚多少次,凭证始终保留。
29
46
 
30
- **2. 配置 MCP 使用保存的状态**
47
+ ---
48
+
49
+ ## 核心流程
31
50
 
32
- 在项目的 `.mcp.json`(Claude Code MCP 配置)中:
51
+ ### 流程 1:Agent 自动发现凭证
33
52
 
34
- ```json
35
- {
36
- "mcpServers": {
37
- "playwright": {
38
- "command": "npx",
39
- "args": [
40
- "@playwright/mcp@latest",
41
- "--storage-state=.claude-coder/playwright-auth.json"
42
- ]
43
- }
44
- }
45
- }
46
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` 的存在和用法。
47
58
 
48
- **3. 安全注意事项**
59
+ ### 流程 2:用户预配置浏览器登录态
49
60
 
50
- ```gitignore
51
- # .gitignore
52
- .claude-coder/playwright-auth.json
61
+ ```
62
+ 用户运行 claude-coder auth → 手动登录 → 状态自动保存 → Agent 测试时使用
53
63
  ```
54
64
 
55
- ### 注意
65
+ 适用于需要已登录状态才能测试的前端页面(如后台管理、需要 cookie 的 SPA)。
56
66
 
57
- - 状态文件包含敏感 cookies,必须加入 `.gitignore`
58
- - cookies 有过期时间,需要定期重新导出
59
- - `--storage-state` 与 `--isolated` 模式配合使用效果最佳
67
+ ### 流程 3:用户预配置 API Key
60
68
 
61
- ---
69
+ ```
70
+ 用户编辑 test.env → 填入 API Key → Agent 测试前 source 加载
71
+ ```
62
72
 
63
- ## 方案 2: test.env(推荐用于 API Key)
73
+ 适用于后端功能依赖真实 API 调用的场景。
64
74
 
65
- ### 原理
75
+ ---
66
76
 
67
- `.claude-coder/test.env` 中存放测试专用的环境变量(如 API Key)。claude-coder 会自动检测此文件存在,并通过 Hint 提示 Agent 在测试前加载它。
77
+ ## CLI 命令
68
78
 
69
- ### 步骤
79
+ ### `claude-coder auth [url]`
70
80
 
71
- **1. 创建 test.env**
81
+ 一键导出浏览器登录态:
72
82
 
73
83
  ```bash
74
- # .claude-coder/test.env
75
- OPENAI_API_KEY=sk-xxx
76
- ZHIPU_API_KEY=xxx.xxx
77
- TEST_USER_TOKEN=xxx
78
- ```
84
+ # 默认打开 http://localhost:3000
85
+ claude-coder auth
79
86
 
80
- **2. Agent 自动感知**
81
-
82
- 当 `.claude-coder/test.env` 存在时,harness 在编码 session 的 prompt 中注入提示:
87
+ # 指定 URL
88
+ claude-coder auth http://localhost:8080/admin
89
+ ```
83
90
 
84
- > 测试环境变量在 .claude-coder/test.env(含 API Key 等),测试前用 source .claude-coder/test.env 或 export 加载。
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`
85
97
 
86
- Agent 在执行测试时会自动 `source` 该文件。
98
+ ### `claude-coder setup`(相关)
87
99
 
88
- **3. 安全注意事项**
100
+ 配置模型时可启用 Playwright MCP:
89
101
 
90
- ```gitignore
91
- # .gitignore
92
- .claude-coder/test.env
102
+ ```bash
103
+ claude-coder setup
104
+ # 选择启用 MCP_PLAYWRIGHT=true
93
105
  ```
94
106
 
95
107
  ---
96
108
 
97
- ## 方案 3: project_profile.json 中声明测试依赖
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
98
122
 
99
- 在扫描阶段或手动编辑 `project_profile.json`,声明哪些测试需要真实 API Key:
123
+ # 3. 导出前端登录态(可选,Agent 也能用 Playwright MCP 自动登录)
124
+ claude-coder auth http://localhost:3000
100
125
 
101
- ```json
102
- {
103
- "test_dependencies": {
104
- "real_api_key": true,
105
- "required_env_vars": ["OPENAI_API_KEY", "ZHIPU_API_KEY"],
106
- "env_file": ".claude-coder/test.env"
107
- }
108
- }
126
+ # 4. 开始自动编码和测试
127
+ claude-coder run
109
128
  ```
110
129
 
111
- Agent Step 5 测试时,如果检测到 `preconditions.real_api_key: true`,会先检查环境变量是否可用,不可用则跳过该测试并标记为 `skip`。
130
+ ### 场景 2:Agent 自主发现并处理凭证缺失
112
131
 
113
- ---
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。
114
141
 
115
- ## 最佳实践
142
+ ### 场景 3:前端 localStorage 配置持久化
116
143
 
117
- | 场景 | 推荐方案 |
118
- |------|----------|
119
- | 需要已登录状态测试页面 | 方案 1 (--storage-state) |
120
- | 需要 API Key 测试后端功能 | 方案 2 (test.env) |
121
- | 需要区分 mock 测试和集成测试 | 方案 3 (profile 声明) |
122
- | 以上组合 | 方案 1 + 2 + 3 |
144
+ 项目的前端将 LLM 服务商配置存储在 localStorage 中:
123
145
 
124
- ### 工作流示例
146
+ ```bash
147
+ # 启动前后端服务
148
+ # 运行 auth,手动在页面中配置 LLM 设置
149
+ claude-coder auth http://localhost:3000
125
150
 
151
+ # playwright-auth.json 中已包含 localStorage 数据
152
+ # 后续 Agent 使用 Playwright MCP 测试时自动加载这些配置
126
153
  ```
127
- 1. claude-coder setup → 配置模型
128
- 2. 创建 .claude-coder/test.env → 填入 API Key
129
- 3. npx playwright codegen ... → 导出登录状态
130
- 4. claude-coder run → Agent 自动使用凭证测试
154
+
155
+ ### 场景 4:cookies 过期后刷新
156
+
157
+ ```bash
158
+ # 重新运行 auth 即可
159
+ claude-coder auth http://localhost:3000
160
+ # 新的 cookies 覆盖旧文件,立即生效
131
161
  ```
package/docs/README.en.md CHANGED
@@ -53,9 +53,10 @@ Each session, the agent autonomously follows 6 steps: restore context → env ch
53
53
  | `claude-coder run --max 1` | Single session (replaces old view mode) |
54
54
  | `claude-coder run --dry-run` | Preview mode |
55
55
  | `claude-coder init` | Initialize project environment |
56
- | `claude-coder add "instruction"` | Append tasks (defaults to opus-class model) |
56
+ | `claude-coder add "instruction"` | Append tasks |
57
57
  | `claude-coder add -r [file]` | Append tasks from requirements file |
58
58
  | `claude-coder add "..." --model M` | Append tasks with specific model |
59
+ | `claude-coder auth [url]` | Export Playwright login state |
59
60
  | `claude-coder validate` | Manually validate last session |
60
61
  | `claude-coder status` | View progress and costs |
61
62
  | `claude-coder config sync` | Sync config to ~/.claude/ |
@@ -84,7 +85,9 @@ your-project/
84
85
  progress.json # Session history + costs
85
86
  tests.json # Verification records
86
87
  test.env # Test credentials (API keys, optional)
87
- .runtime/ # Temp files (logs)
88
+ playwright-auth.json # Playwright login state (optional, via auth command)
89
+ .runtime/ # Temp files
90
+ logs/ # Per-session logs + activity logs
88
91
  requirements.md # Requirements (optional)
89
92
  ```
90
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-coder",
3
- "version": "1.2.0",
3
+ "version": "1.5.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
@@ -56,12 +56,13 @@ function paths() {
56
56
  profile: path.join(loopDir, 'project_profile.json'),
57
57
  testsFile: path.join(loopDir, 'tests.json'),
58
58
  testEnvFile: path.join(loopDir, 'test.env'),
59
+ playwrightAuth: path.join(loopDir, 'playwright-auth.json'),
60
+ mcpConfig: path.join(getProjectRoot(), '.mcp.json'),
59
61
  claudeMd: getTemplatePath('CLAUDE.md'),
60
62
  scanProtocol: getTemplatePath('SCAN_PROTOCOL.md'),
61
63
  runtime,
62
64
  phaseFile: path.join(runtime, 'phase'),
63
65
  stepFile: path.join(runtime, 'step'),
64
- activityLog: path.join(runtime, 'activity.log'),
65
66
  logsDir: path.join(runtime, 'logs'),
66
67
  };
67
68
  }
@@ -105,6 +106,7 @@ function loadConfig() {
105
106
  defaultSonnet: env.ANTHROPIC_DEFAULT_SONNET_MODEL || '',
106
107
  defaultHaiku: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '',
107
108
  thinkingBudget: env.ANTHROPIC_THINKING_BUDGET || '',
109
+ stallTimeout: parseInt(env.SESSION_STALL_TIMEOUT, 10) || 1800,
108
110
  raw: env,
109
111
  };
110
112
 
@@ -188,16 +190,6 @@ function syncToGlobal() {
188
190
  log('ok', `已同步配置到 ${settingsPath}`);
189
191
  }
190
192
 
191
- // --------------- Requirements hash ---------------
192
-
193
- function getRequirementsHash() {
194
- const crypto = require('crypto');
195
- const reqFile = path.join(getProjectRoot(), 'requirements.md');
196
- if (!fs.existsSync(reqFile)) return '';
197
- const content = fs.readFileSync(reqFile, 'utf8');
198
- return crypto.createHash('sha256').update(content).digest('hex');
199
- }
200
-
201
193
  module.exports = {
202
194
  COLOR,
203
195
  log,
@@ -211,5 +203,4 @@ module.exports = {
211
203
  buildEnvVars,
212
204
  getAllowedTools,
213
205
  syncToGlobal,
214
- getRequirementsHash,
215
206
  };
package/src/indicator.js CHANGED
@@ -9,15 +9,18 @@ class Indicator {
9
9
  constructor() {
10
10
  this.phase = 'thinking';
11
11
  this.step = '';
12
+ this.toolTarget = '';
12
13
  this.spinnerIndex = 0;
13
14
  this.timer = null;
14
15
  this.lastActivity = '';
16
+ this.lastToolTime = Date.now();
15
17
  this.sessionNum = 0;
16
18
  this.startTime = Date.now();
17
19
  }
18
20
 
19
- start(sessionNum) {
21
+ start(sessionNum, activityLogPath) {
20
22
  this.sessionNum = sessionNum;
23
+ this.activityLogPath = activityLogPath || null;
21
24
  this.startTime = Date.now();
22
25
  this.timer = setInterval(() => this._render(), 500);
23
26
  }
@@ -45,8 +48,9 @@ class Indicator {
45
48
  const entry = `[${ts}] ${toolName}: ${summary}`;
46
49
  this.lastActivity = entry;
47
50
  try {
48
- const p = paths();
49
- fs.appendFileSync(p.activityLog, entry + '\n', 'utf8');
51
+ if (this.activityLogPath) {
52
+ fs.appendFileSync(this.activityLogPath, entry + '\n', 'utf8');
53
+ }
50
54
  } catch { /* ignore */ }
51
55
  }
52
56
 
@@ -74,8 +78,17 @@ class Indicator {
74
78
  ? `${COLOR.yellow}思考中${COLOR.reset}`
75
79
  : `${COLOR.green}编码中${COLOR.reset}`;
76
80
 
81
+ const idleMs = Date.now() - this.lastToolTime;
82
+ const idleMin = Math.floor(idleMs / 60000);
83
+
77
84
  let line = `${spinner} [Session ${this.sessionNum}] ${clock} ${phaseLabel} ${mm}:${ss}`;
78
- if (this.step) line += ` | ${this.step}`;
85
+ if (idleMin >= 2) {
86
+ line += ` | ${COLOR.red}${idleMin}分无工具调用${COLOR.reset}`;
87
+ }
88
+ if (this.step) {
89
+ line += ` | ${this.step}`;
90
+ if (this.toolTarget) line += `: ${this.toolTarget}`;
91
+ }
79
92
  return line;
80
93
  }
81
94
 
@@ -94,6 +107,14 @@ class Indicator {
94
107
  function inferPhaseStep(indicator, toolName, toolInput) {
95
108
  const name = (toolName || '').toLowerCase();
96
109
 
110
+ indicator.lastToolTime = Date.now();
111
+
112
+ const rawTarget = typeof toolInput === 'object'
113
+ ? (toolInput.file_path || toolInput.path || toolInput.command || toolInput.pattern || '')
114
+ : String(toolInput || '');
115
+ const shortTarget = rawTarget.split('/').slice(-2).join('/').slice(0, 40);
116
+ indicator.toolTarget = shortTarget;
117
+
97
118
  if (name === 'write' || name === 'edit' || name === 'multiedit' || name === 'str_replace_editor' || name === 'strreplace') {
98
119
  indicator.updatePhase('coding');
99
120
  } else if (name === 'bash' || name === 'shell') {
@@ -119,9 +140,23 @@ function inferPhaseStep(indicator, toolName, toolInput) {
119
140
  indicator.updateStep('查阅文档');
120
141
  }
121
142
 
122
- const summary = typeof toolInput === 'object'
123
- ? (toolInput.path || toolInput.command || toolInput.pattern || JSON.stringify(toolInput).slice(0, 80))
124
- : String(toolInput || '').slice(0, 80);
143
+ let summary;
144
+ if (typeof toolInput === 'object') {
145
+ const target = toolInput.file_path || toolInput.path || '';
146
+ const cmd = toolInput.command || '';
147
+ const pattern = toolInput.pattern || '';
148
+ if (target) {
149
+ summary = target;
150
+ } else if (cmd) {
151
+ summary = cmd.slice(0, 200);
152
+ } else if (pattern) {
153
+ summary = `pattern: ${pattern}`;
154
+ } else {
155
+ summary = JSON.stringify(toolInput).slice(0, 200);
156
+ }
157
+ } else {
158
+ summary = String(toolInput || '').slice(0, 200);
159
+ }
125
160
  indicator.appendActivity(toolName, summary);
126
161
  }
127
162
 
package/src/prompts.js CHANGED
@@ -72,9 +72,9 @@ 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
- const projectRoot = getProjectRoot();
78
78
  const taskData = loadTasks();
79
79
  if (taskData) {
80
80
  const next = findNextTask(taskData);
@@ -89,11 +89,18 @@ function buildCodingPrompt(sessionNum, opts = {}) {
89
89
  }
90
90
  } catch { /* ignore */ }
91
91
 
92
- // Hint 6b: Test environment variables
92
+ // Hint 6b: Test environment variables (readable + writable by Agent)
93
93
  let testEnvHint = '';
94
- const testEnvFile = paths().testEnvFile;
95
- if (testEnvFile && fs.existsSync(testEnvFile)) {
96
- testEnvHint = '测试环境变量在 .claude-coder/test.env(含 API Key 等),测试前用 source .claude-coder/test.env 或 export 加载。';
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)。`;
97
104
  }
98
105
 
99
106
  // Hint 7: Session memory (read flat session_result.json)
@@ -136,6 +143,7 @@ function buildCodingPrompt(sessionNum, opts = {}) {
136
143
  envHint,
137
144
  taskHint,
138
145
  testEnvHint,
146
+ playwrightAuthHint,
139
147
  memoryHint,
140
148
  serviceHint,
141
149
  toolGuidance,
package/src/runner.js CHANGED
@@ -183,7 +183,7 @@ async function run(requirement, opts = {}) {
183
183
  ensureLoopDir();
184
184
 
185
185
  const maxSessions = opts.max || 50;
186
- const pauseEvery = opts.pause || 5;
186
+ const pauseEvery = opts.pause ?? 0;
187
187
  const dryRun = opts.dryRun || false;
188
188
 
189
189
  console.log('');
@@ -301,12 +301,35 @@ async function run(requirement, opts = {}) {
301
301
  lastValidateLog: consecutiveFailures > 0 ? '上次校验失败' : '',
302
302
  });
303
303
 
304
+ if (sessionResult.stalled) {
305
+ log('warn', `Session ${session} 因停顿超时中断,跳过校验直接重试`);
306
+ consecutiveFailures++;
307
+ rollback(headBefore, '停顿超时');
308
+ if (consecutiveFailures >= MAX_RETRY) {
309
+ log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
310
+ markTaskFailed();
311
+ consecutiveFailures = 0;
312
+ }
313
+ appendProgress({
314
+ session,
315
+ timestamp: new Date().toISOString(),
316
+ result: 'stalled',
317
+ cost: sessionResult.cost,
318
+ taskId,
319
+ });
320
+ continue;
321
+ }
322
+
304
323
  // Validate
305
324
  log('info', '开始 harness 校验 ...');
306
325
  const validateResult = await validate(headBefore);
307
326
 
308
327
  if (!validateResult.fatal) {
309
- log('ok', `Session ${session} 校验通过`);
328
+ if (validateResult.hasWarnings) {
329
+ log('warn', `Session ${session} 校验通过 (有自动修复或警告)`);
330
+ } else {
331
+ log('ok', `Session ${session} 校验通过`);
332
+ }
310
333
  tryPush();
311
334
  consecutiveFailures = 0;
312
335
 
@@ -369,8 +392,8 @@ async function add(instruction, opts = {}) {
369
392
  if (!opts.model) {
370
393
  if (config.defaultOpus) {
371
394
  opts.model = config.defaultOpus;
372
- } else if (config.provider === 'claude' || !config.baseUrl) {
373
- opts.model = 'claude-sonnet-4-20250514';
395
+ } else if (config.model) {
396
+ opts.model = config.model;
374
397
  }
375
398
  }
376
399
 
package/src/session.js CHANGED
@@ -99,12 +99,23 @@ async function runCodingSession(sessionNum, opts = {}) {
99
99
  const taskId = opts.taskId || 'unknown';
100
100
  const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
101
101
  const logFile = path.join(p.logsDir, `${taskId}_session_${sessionNum}_${dateStr}.log`);
102
+ const activityLogFile = path.join(p.logsDir, `session_${sessionNum}.activity.log`);
102
103
  const logStream = fs.createWriteStream(logFile, { flags: 'a' });
103
104
 
104
- indicator.start(sessionNum);
105
+ indicator.start(sessionNum, activityLogFile);
105
106
 
106
107
  const editCounts = {};
107
108
  const EDIT_THRESHOLD = 5;
109
+ const stallTimeoutMs = config.stallTimeout * 1000;
110
+ let stallDetected = false;
111
+
112
+ const stallChecker = setInterval(() => {
113
+ const idleMs = Date.now() - indicator.lastToolTime;
114
+ if (idleMs > stallTimeoutMs && !stallDetected) {
115
+ stallDetected = true;
116
+ log('warn', `无新工具调用超过 ${Math.floor(idleMs / 60000)} 分钟,自动中断 session`);
117
+ }
118
+ }, 30000);
108
119
 
109
120
  try {
110
121
  const queryOpts = buildQueryOptions(config, opts);
@@ -115,13 +126,18 @@ async function runCodingSession(sessionNum, opts = {}) {
115
126
  hooks: [async (input) => {
116
127
  inferPhaseStep(indicator, input.tool_name, input.tool_input);
117
128
 
118
- const filePath = input.tool_input?.file_path || input.tool_input?.path || '';
119
- if (['Write', 'Edit', 'MultiEdit'].includes(input.tool_name) && filePath) {
120
- editCounts[filePath] = (editCounts[filePath] || 0) + 1;
121
- if (editCounts[filePath] > EDIT_THRESHOLD) {
129
+ const target = input.tool_input?.file_path || input.tool_input?.path || '';
130
+ const toolSummary = target ? target.split('/').slice(-2).join('/') : '';
131
+ if (toolSummary) {
132
+ logStream.write(`[${new Date().toISOString()}] ${input.tool_name}: ${toolSummary}\n`);
133
+ }
134
+
135
+ if (['Write', 'Edit', 'MultiEdit'].includes(input.tool_name) && target) {
136
+ editCounts[target] = (editCounts[target] || 0) + 1;
137
+ if (editCounts[target] > EDIT_THRESHOLD) {
122
138
  return {
123
139
  decision: 'block',
124
- message: `已对 ${filePath} 编辑 ${editCounts[filePath]} 次,疑似死循环。请重新审视方案后再继续。`,
140
+ message: `已对 ${target} 编辑 ${editCounts[target]} 次,疑似死循环。请重新审视方案后再继续。`,
125
141
  };
126
142
  }
127
143
  }
@@ -135,21 +151,28 @@ async function runCodingSession(sessionNum, opts = {}) {
135
151
 
136
152
  const collected = [];
137
153
  for await (const message of session) {
154
+ if (stallDetected) {
155
+ log('warn', '停顿超时,中断消息循环');
156
+ break;
157
+ }
138
158
  collected.push(message);
139
159
  logMessage(message, logStream, indicator);
140
160
  }
141
161
 
162
+ clearInterval(stallChecker);
142
163
  logStream.end();
143
164
  indicator.stop();
144
165
 
145
166
  const result = extractResult(collected);
146
167
  return {
147
- exitCode: 0,
168
+ exitCode: stallDetected ? 2 : 0,
148
169
  cost: result?.total_cost_usd ?? null,
149
170
  tokenUsage: result?.usage ?? null,
150
171
  logFile,
172
+ stalled: stallDetected,
151
173
  };
152
174
  } catch (err) {
175
+ clearInterval(stallChecker);
153
176
  logStream.end();
154
177
  indicator.stop();
155
178
  log('error', `Claude SDK 错误: ${err.message}`);
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`)