claude-coder 1.5.5 → 1.5.7

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
@@ -97,7 +97,8 @@ your-project/
97
97
  progress.json # 会话历史 + 成本
98
98
  tests.json # 验证记录
99
99
  test.env # 测试凭证(API Key 等,可选)
100
- playwright-auth.json # Playwright 登录状态(可选,auth 命令生成)
100
+ playwright-auth.json # 登录状态快照(备份参考,auth 命令生成)
101
+ browser-profile/ # 持久化浏览器 Profile(MCP 实际使用)
101
102
  .runtime/ # 临时文件
102
103
  logs/ # 每 session 独立日志(含工具调用记录)
103
104
  requirements.md # 需求文档(可选)
@@ -31,7 +31,7 @@ Agent 在单次 session 中应最大化推进任务进度。**任何非致命问
31
31
 
32
32
  `git reset --hard` 是全量回滚,不做部分文件保护。
33
33
 
34
- - 凭证文件(`test.env`、`playwright-auth.json`)应通过 `.gitignore` 排除在 git 之外
34
+ - 凭证文件(`test.env`、`playwright-auth.json`、`browser-profile/`)应通过 `.gitignore` 排除在 git 之外
35
35
  - 如果回滚发生,说明 session 确实失败,代码应全部还原
36
36
  - 不需要 backup/restore 机制 — 这是过度设计
37
37
 
@@ -51,7 +51,8 @@ Agent 在单次 session 中应最大化推进任务进度。**任何非致命问
51
51
  | 文件 | git 状态 | 说明 |
52
52
  |------|---------|------|
53
53
  | `test.env` | .gitignore | Agent 可写入发现的 API Key、测试账号 |
54
- | `playwright-auth.json` | .gitignore | 用户通过 `claude-coder auth` 生成 |
54
+ | `playwright-auth.json` | .gitignore | 登录状态快照备份(`claude-coder auth` 生成) |
55
+ | `browser-profile/` | .gitignore | 持久化浏览器 Profile(MCP 实际使用) |
55
56
  | `session_result.json` | git-tracked | Agent 每次 session 覆盖写入 |
56
57
  | `tasks.json` | git-tracked | Agent 修改 status 字段 |
57
58
 
@@ -207,7 +208,8 @@ templates/
207
208
  | `tasks.json` | 首次扫描 | 任务列表 + 状态跟踪 |
208
209
  | `progress.json` | 每次 session 结束 | 结构化会话日志 + 成本记录 |
209
210
  | `session_result.json` | 每次 session 结束 | 当前 session 结果(扁平格式,向后兼容旧 `current` 包装) |
210
- | `playwright-auth.json` | `claude-coder auth` | Playwright 登录状态(cookies + localStorage) |
211
+ | `playwright-auth.json` | `claude-coder auth` | 登录状态快照(备份参考) |
212
+ | `browser-profile/` | `claude-coder auth` | 持久化浏览器 Profile(MCP 通过 `--user-data-dir` 使用) |
211
213
  | `tests.json` | 首次测试时 | 验证记录(防止反复测试) |
212
214
  | `.runtime/` | 运行时 | 临时文件(phase、step、logs/);工具调用记录合并到 session log |
213
215
 
@@ -265,7 +267,7 @@ flowchart TB
265
267
  | 5 | `docsHint` | profile.existing_docs 非空或 profile 有缺陷 | Step 4:读文档后再编码;profile 缺陷时提示 Agent 在 Step 6 补全 services/docs |
266
268
  | 6 | `taskHint` | tasks.json 存在且有待办任务 | Step 1:跳过读取 tasks.json,harness 已注入当前任务上下文 + 项目绝对路径 |
267
269
  | 6b | `testEnvHint` | 始终注入(内容因 test.env 是否存在而不同) | Step 5:存在时提示加载;不存在时告知可创建 |
268
- | 6c | `playwrightAuthHint` | .claude-coder/playwright-auth.json 存在 | Step 5:提示 Agent 前端测试可使用已认证的浏览器状态 |
270
+ | 6c | `playwrightAuthHint` | .claude-coder/browser-profile/ 存在 | Step 5:提示 Agent MCP 使用持久化浏览器 Profile,首次需手动登录 |
269
271
  | 7 | `memoryHint` | session_result.json 存在(扁平格式) | Step 1:跳过读取 session_result.json,harness 已注入上次会话摘要 |
270
272
  | 8 | `serviceHint` | 始终注入 | Step 6:单次模式停止服务,连续模式保持服务运行 |
271
273
  | 9 | `toolGuidance` | 始终注入 | 全局:工具使用规范(Grep/Glob/Read/LS/MultiEdit/Task 替代 bash 命令),非 Claude 模型必需 |
@@ -406,7 +408,8 @@ Harness 在 `buildCodingPrompt()` 中预读 `session_result.json`,将上次会
406
408
  | `tasks.json` | Agent(仅 `status` 字段) | 修改 `status` | tracked |
407
409
  | `project_profile.json` | Agent(仅扫描阶段) | 扫描时写入 | tracked |
408
410
  | `test.env` | Agent + 用户 | 可追加写入 | .gitignore |
409
- | `playwright-auth.json` | 用户(`claude-coder auth`) | 只读 | .gitignore |
411
+ | `playwright-auth.json` | 用户(`claude-coder auth`) | 快照备份 | .gitignore |
412
+ | `browser-profile/` | 用户(`claude-coder auth`) | MCP 自动维护 | .gitignore |
410
413
 
411
414
  ---
412
415
 
@@ -13,7 +13,7 @@ claude-coder 的核心目标是让 Agent **完全自主测试**,不因凭证
13
13
  **核心原则**:
14
14
  1. **Agent 可自行发现并持久化凭证** — 测试中发现需要的 API Key 或账号,直接写入 `test.env`
15
15
  2. **凭证不受回滚影响** — `git reset --hard` 不会摧毁已保存的凭证
16
- 3. **零手动干预** — 除首次浏览器登录态外,其余由 Agent 自动处理
16
+ 3. **零手动干预** — 首次浏览器登录后,后续由持久化 profile 自动处理
17
17
 
18
18
  ---
19
19
 
@@ -23,7 +23,7 @@ claude-coder 的核心目标是让 Agent **完全自主测试**,不因凭证
23
23
  .claude-coder/
24
24
  .env ← 模型配置(ANTHROPIC_API_KEY 等) [用户配置]
25
25
  test.env ← 测试凭证(API Key、测试账号等) [Agent 可写]
26
- playwright-auth.json ← 浏览器状态(cookies + localStorage) [auth 命令生成]
26
+ playwright-auth.json ← 浏览器登录状态(MCP 每次会话加载) [auth 命令生成]
27
27
  ```
28
28
 
29
29
  ### 文件生命周期
@@ -32,17 +32,26 @@ claude-coder 的核心目标是让 Agent **完全自主测试**,不因凭证
32
32
  |------|--------|--------|----------|----------|
33
33
  | `.env` | `claude-coder setup` | 用户 | 是 | 长期 |
34
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
- 这确保无论回滚多少次,凭证始终保留。
35
+ | `playwright-auth.json` | `claude-coder auth` | auth 命令 | 是 | 长期,MCP 每次会话自动加载;如需更新重新运行 auth |
36
+
37
+ ### 技术实现:为什么用 `--isolated --storage-state`
38
+
39
+ | 维度 | `--user-data-dir`(persistent) | `--isolated --storage-state`(当前方案) |
40
+ |------|--------------------------|--------------------------------------|
41
+ | 上下文类型 | 持久化上下文 | 隔离上下文 |
42
+ | localStorage | **已知 Bug #14949:`launchPersistentContext` 不注入 localStorage** | 从 JSON 可靠注入 |
43
+ | Cookies | Profile 自动续期 | 每次从 JSON 加载(静态) |
44
+ | 状态保持 | 跨会话自动保持 | 每次会话从 JSON 重新加载 |
45
+ | 适用场景 | 需要 cookie 自动续期(Google OAuth) | 需要 localStorage 注入(API Key 等) |
46
+
47
+ > **选择 `--isolated --storage-state` 的原因**:
48
+ > 经实测验证,Playwright 的 `launchPersistentContext` + `storageState` 存在已知缺陷
49
+ >(Issue #14949):localStorage 完全不注入。而 `--isolated` 模式使用 `newContext({ storageState })`,
50
+ > localStorage 可靠注入。claude-coder 的典型场景是注入 API Key(存储在 localStorage),
51
+ > 因此选择 `--isolated --storage-state` 作为默认方案。
52
+ >
53
+ > 如需 cookie 持久化(Google OAuth/SSO),可手动修改 `.mcp.json` 为 `--user-data-dir` 模式,
54
+ > 但需在 MCP 浏览器中手动登录一次。
46
55
 
47
56
  ---
48
57
 
@@ -54,56 +63,46 @@ Harness 在 `git reset --hard` 前备份、后恢复以下文件:
54
63
  Agent 测试 → 发现需要 API Key → 写入 test.env → 下次 session 自动加载
55
64
  ```
56
65
 
57
- Agent 在 CLAUDE.md Step 5 中被指导:测试中发现的凭证追加到 `.claude-coder/test.env`。Harness 在每次 session 的 prompt 中注入 hint,告知 Agent `test.env` 的存在和用法。
58
-
59
66
  ### 流程 2:用户预配置浏览器登录态
60
67
 
61
68
  ```
62
- 用户运行 claude-coder auth → 手动登录 → 状态自动保存 → Agent 测试时使用
69
+ 用户运行 claude-coder auth url
70
+ → playwright codegen 打开浏览器 → 手动登录 → 关闭浏览器
71
+ → cookies + localStorage 保存到 playwright-auth.json
72
+ → 更新 .mcp.json(--isolated --storage-state 指向 playwright-auth.json)
73
+ → 每次 MCP 会话自动从 JSON 加载状态(无需手动登录)
74
+ → 如需更新状态,重新运行 claude-coder auth
63
75
  ```
64
76
 
65
- 适用于需要已登录状态才能测试的前端页面(如后台管理、需要 cookie 的 SPA)。
66
-
67
77
  ### 流程 3:用户预配置 API Key
68
78
 
69
79
  ```
70
80
  用户编辑 test.env → 填入 API Key → Agent 测试前 source 加载
71
81
  ```
72
82
 
73
- 适用于后端功能依赖真实 API 调用的场景。
74
-
75
83
  ---
76
84
 
77
85
  ## CLI 命令
78
86
 
79
87
  ### `claude-coder auth [url]`
80
88
 
81
- 一键导出浏览器登录态:
89
+ 配置持久化浏览器认证:
82
90
 
83
91
  ```bash
84
92
  # 默认打开 http://localhost:3000
85
93
  claude-coder auth
86
94
 
87
- # 指定 URL
88
- claude-coder auth http://localhost:8080/admin
95
+ # 指定 URL(如内部 API 文档平台)
96
+ claude-coder auth http://testyapi.example.com/group/2245
89
97
  ```
90
98
 
91
99
  **自动完成**:
92
- 1. 启动 Playwright 浏览器,用户手动登录后关闭
93
- 2. 保存 cookies + localStorage `.claude-coder/playwright-auth.json`
94
- 3. 创建/更新 `.mcp.json`,配置 `--storage-state`
95
- 4. 添加 `.gitignore` 条目
100
+ 1. 启动 `playwright codegen`,用户手动登录后关闭浏览器
101
+ 2. cookies + localStorage 保存到 `.claude-coder/playwright-auth.json`
102
+ 3. 创建/更新 `.mcp.json`,配置 `--isolated --storage-state=.claude-coder/playwright-auth.json`
103
+ 4. 添加 `.gitignore` 条目(`playwright-auth.json`)
96
104
  5. 启用 `.claude-coder/.env` 中 `MCP_PLAYWRIGHT=true`
97
105
 
98
- ### `claude-coder setup`(相关)
99
-
100
- 配置模型时可启用 Playwright MCP:
101
-
102
- ```bash
103
- claude-coder setup
104
- # 选择启用 MCP_PLAYWRIGHT=true
105
- ```
106
-
107
106
  ---
108
107
 
109
108
  ## 场景示例
@@ -111,51 +110,36 @@ claude-coder setup
111
110
  ### 场景 1:全栈项目首次测试
112
111
 
113
112
  ```bash
114
- # 1. 配置模型
115
113
  claude-coder setup
116
-
117
- # 2. 填入后端测试需要的 API Key
118
114
  cat >> .claude-coder/test.env << 'EOF'
119
115
  OPENAI_API_KEY=sk-xxx
120
- ZHIPU_API_KEY=xxx.xxx
121
116
  EOF
122
-
123
- # 3. 导出前端登录态(可选,Agent 也能用 Playwright MCP 自动登录)
124
117
  claude-coder auth http://localhost:3000
125
-
126
- # 4. 开始自动编码和测试
127
118
  claude-coder run
119
+ # MCP 每次会话自动从 playwright-auth.json 加载 localStorage 和 cookies
128
120
  ```
129
121
 
130
- ### 场景 2:Agent 自主发现并处理凭证缺失
131
-
132
- Agent 在测试 feat-005(AI 内容生成)时发现需要 `OPENAI_API_KEY`:
122
+ ### 场景 2:内部系统(Google OAuth / SSO)
133
123
 
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。
124
+ ```bash
125
+ claude-coder auth http://testyapi.example.com/group/2245
126
+ # 在弹出的浏览器中完成登录,关闭后状态保存到 JSON
127
+ # MCP 每次会话自动加载此状态
128
+ ```
141
129
 
142
- ### 场景 3:前端 localStorage 配置持久化
130
+ > **关于 Google OAuth**:`--isolated` 模式每次创建新上下文,Google 可能要求重新验证。
131
+ > 如需 cookie 持久化,可手动修改 `.mcp.json` 为 `--user-data-dir` 模式(但 localStorage 不会注入)。
143
132
 
144
- 项目的前端将 LLM 服务商配置存储在 localStorage 中:
133
+ ### 场景 3:更新登录状态
145
134
 
146
135
  ```bash
147
- # 启动前后端服务
148
- # 运行 auth,手动在页面中配置 LLM 设置
149
136
  claude-coder auth http://localhost:3000
150
-
151
- # playwright-auth.json 中已包含 localStorage 数据
152
- # 后续 Agent 使用 Playwright MCP 测试时自动加载这些配置
137
+ # 重新登录,覆盖 playwright-auth.json
153
138
  ```
154
139
 
155
- ### 场景 4:cookies 过期后刷新
140
+ ### 场景 4:清除登录状态
156
141
 
157
142
  ```bash
158
- # 重新运行 auth 即可
159
- claude-coder auth http://localhost:3000
160
- # 新的 cookies 覆盖旧文件,立即生效
143
+ rm .claude-coder/playwright-auth.json
144
+ # 下次运行 claude-coder auth 重新配置
161
145
  ```
package/docs/README.en.md CHANGED
@@ -85,7 +85,8 @@ your-project/
85
85
  progress.json # Session history + costs
86
86
  tests.json # Verification records
87
87
  test.env # Test credentials (API keys, optional)
88
- playwright-auth.json # Playwright login state (optional, via auth command)
88
+ playwright-auth.json # Login state snapshot (backup, via auth command)
89
+ browser-profile/ # Persistent browser profile (used by MCP)
89
90
  .runtime/ # Temp files
90
91
  logs/ # Per-session logs (with tool call traces)
91
92
  requirements.md # Requirements (optional)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-coder",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
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 CHANGED
@@ -18,7 +18,7 @@ function updateGitignore(entry) {
18
18
  log('ok', `.gitignore 已添加: ${entry}`);
19
19
  }
20
20
 
21
- function updateMcpConfig(authFilePath) {
21
+ function updateMcpConfig() {
22
22
  const p = paths();
23
23
  let mcpConfig = {};
24
24
  if (fs.existsSync(p.mcpConfig)) {
@@ -31,17 +31,18 @@ function updateMcpConfig(authFilePath) {
31
31
 
32
32
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
33
33
 
34
- const relAuthPath = path.relative(getProjectRoot(), authFilePath);
34
+ const relAuthPath = path.relative(getProjectRoot(), p.playwrightAuth).split(path.sep).join('/');
35
35
  mcpConfig.mcpServers.playwright = {
36
36
  command: 'npx',
37
37
  args: [
38
38
  '@playwright/mcp@latest',
39
+ '--isolated',
39
40
  `--storage-state=${relAuthPath}`,
40
41
  ],
41
42
  };
42
43
 
43
44
  fs.writeFileSync(p.mcpConfig, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
44
- log('ok', `.mcp.json 已配置 Playwright MCP (storage-state: ${relAuthPath})`);
45
+ log('ok', `.mcp.json 已配置 Playwright MCP (isolated + storage-state: ${relAuthPath})`);
45
46
  }
46
47
 
47
48
  function enableMcpPlaywrightEnv() {
@@ -66,12 +67,12 @@ async function auth(url) {
66
67
 
67
68
  log('info', '启动 Playwright 浏览器,请手动登录...');
68
69
  log('info', `目标 URL: ${targetUrl}`);
69
- log('info', `登录状态将保存到: ${p.playwrightAuth}`);
70
70
  console.log('');
71
71
  console.log('操作步骤:');
72
72
  console.log(' 1. 浏览器将自动打开,请手动完成登录');
73
73
  console.log(' 2. 登录成功后关闭浏览器窗口');
74
- console.log(' 3. 登录状态(cookies + localStorage)将自动保存');
74
+ console.log(' 3. 登录状态(cookies + localStorage)将保存到 playwright-auth.json');
75
+ console.log(' 4. MCP 每次会话自动加载此状态(isolated 模式)');
75
76
  console.log('');
76
77
 
77
78
  try {
@@ -92,16 +93,17 @@ async function auth(url) {
92
93
  return;
93
94
  }
94
95
 
95
- log('ok', '登录状态已保存');
96
+ log('ok', '登录状态已保存到 playwright-auth.json');
96
97
 
97
- updateMcpConfig(p.playwrightAuth);
98
+ updateMcpConfig();
98
99
  updateGitignore('.claude-coder/playwright-auth.json');
99
100
  enableMcpPlaywrightEnv();
100
101
 
101
102
  console.log('');
102
- log('ok', 'Playwright 凭证配置完成!');
103
- log('info', '后续运行 claude-coder run 时,Agent 的前端测试将自动使用已认证状态');
104
- log('info', '注意: cookies 有过期时间,需要定期重新运行 claude-coder auth 更新');
103
+ log('ok', '配置完成!');
104
+ log('info', 'MCP 使用 --isolated --storage-state 模式');
105
+ log('info', 'localStorage cookies 每次会话自动从 playwright-auth.json 加载');
106
+ log('info', '如需更新登录状态,重新运行 claude-coder auth');
105
107
  }
106
108
 
107
109
  module.exports = { auth };
package/src/config.js CHANGED
@@ -60,9 +60,8 @@ function paths() {
60
60
  mcpConfig: path.join(getProjectRoot(), '.mcp.json'),
61
61
  claudeMd: getTemplatePath('CLAUDE.md'),
62
62
  scanProtocol: getTemplatePath('SCAN_PROTOCOL.md'),
63
+ testRuleTemplate: getTemplatePath('test_rule.md'),
63
64
  runtime,
64
- phaseFile: path.join(runtime, 'phase'),
65
- stepFile: path.join(runtime, 'step'),
66
65
  logsDir: path.join(runtime, 'logs'),
67
66
  };
68
67
  }
@@ -107,6 +106,7 @@ function loadConfig() {
107
106
  defaultHaiku: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '',
108
107
  thinkingBudget: env.ANTHROPIC_THINKING_BUDGET || '',
109
108
  stallTimeout: parseInt(env.SESSION_STALL_TIMEOUT, 10) || 1800,
109
+ editThreshold: parseInt(env.EDIT_THRESHOLD, 10) || 15,
110
110
  raw: env,
111
111
  };
112
112
 
package/src/hooks.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const { inferPhaseStep } = require('./indicator');
4
4
  const { log } = require('./config');
5
5
 
6
- const EDIT_THRESHOLD = 5;
6
+ const DEFAULT_EDIT_THRESHOLD = 30;
7
7
 
8
8
  function logToolCall(logStream, input) {
9
9
  if (!logStream) return;
@@ -31,6 +31,7 @@ function createSessionHooks(indicator, logStream, options = {}) {
31
31
  enableStallDetection = false,
32
32
  stallTimeoutMs = 1800000,
33
33
  enableEditGuard = false,
34
+ editThreshold = DEFAULT_EDIT_THRESHOLD,
34
35
  } = options;
35
36
 
36
37
  const editCounts = {};
@@ -62,7 +63,7 @@ function createSessionHooks(indicator, logStream, options = {}) {
62
63
  const target = input.tool_input?.file_path || input.tool_input?.path || '';
63
64
  if (['Write', 'Edit', 'MultiEdit'].includes(input.tool_name) && target) {
64
65
  editCounts[target] = (editCounts[target] || 0) + 1;
65
- if (editCounts[target] > EDIT_THRESHOLD) {
66
+ if (editCounts[target] > editThreshold) {
66
67
  return {
67
68
  decision: 'block',
68
69
  message: `已对 ${target} 编辑 ${editCounts[target]} 次,疑似死循环。请重新审视方案后再继续。`,
package/src/indicator.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
4
- const { paths, COLOR } = require('./config');
3
+ const { COLOR } = require('./config');
5
4
 
6
5
  const SPINNERS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
7
6
 
@@ -16,14 +15,11 @@ class Indicator {
16
15
  this.lastToolTime = Date.now();
17
16
  this.sessionNum = 0;
18
17
  this.startTime = Date.now();
19
- this._lastContentKey = '';
20
- this._lastRenderTime = 0;
21
18
  }
22
19
 
23
20
  start(sessionNum) {
24
21
  this.sessionNum = sessionNum;
25
22
  this.startTime = Date.now();
26
- this._lastRenderTime = Date.now();
27
23
  this.timer = setInterval(() => this._render(), 500);
28
24
  }
29
25
 
@@ -37,26 +33,16 @@ class Indicator {
37
33
 
38
34
  updatePhase(phase) {
39
35
  this.phase = phase;
40
- this._writePhaseFile();
41
36
  }
42
37
 
43
38
  updateStep(step) {
44
39
  this.step = step;
45
- this._writeStepFile();
46
40
  }
47
41
 
48
42
  appendActivity(toolName, summary) {
49
43
  this.lastActivity = `${toolName}: ${summary}`;
50
44
  }
51
45
 
52
- _writePhaseFile() {
53
- try { fs.writeFileSync(paths().phaseFile, this.phase, 'utf8'); } catch { /* ignore */ }
54
- }
55
-
56
- _writeStepFile() {
57
- try { fs.writeFileSync(paths().stepFile, this.step, 'utf8'); } catch { /* ignore */ }
58
- }
59
-
60
46
  getStatusLine() {
61
47
  const now = new Date();
62
48
  const hh = String(now.getHours()).padStart(2, '0');
@@ -82,28 +68,23 @@ class Indicator {
82
68
  }
83
69
  if (this.step) {
84
70
  line += ` | ${this.step}`;
85
- if (this.toolTarget) line += `: ${this.toolTarget}`;
71
+ if (this.toolTarget) {
72
+ const cols = process.stderr.columns || 80;
73
+ const usedWidth = line.replace(/\x1b\[[^m]*m/g, '').length;
74
+ const availWidth = Math.max(15, cols - usedWidth - 4);
75
+ const target = this.toolTarget.length > availWidth
76
+ ? '…' + this.toolTarget.slice(-(availWidth - 1))
77
+ : this.toolTarget;
78
+ line += `: ${target}`;
79
+ }
86
80
  }
87
81
  return line;
88
82
  }
89
83
 
90
84
  _render() {
91
85
  this.spinnerIndex++;
92
- const contentKey = `${this.phase}|${this.step}|${this.toolTarget}`;
93
- const now = Date.now();
94
- const contentChanged = contentKey !== this._lastContentKey;
95
-
96
- if (!contentChanged && now - this._lastRenderTime < 3000) {
97
- return;
98
- }
99
- this._lastContentKey = contentKey;
100
- this._lastRenderTime = now;
101
-
102
86
  const line = this.getStatusLine();
103
- const maxWidth = process.stderr.columns || 80;
104
- const truncated = line.length > maxWidth + 20 ? line.slice(0, maxWidth + 20) : line;
105
-
106
- process.stderr.write(`\r\x1b[K${truncated}`);
87
+ process.stderr.write(`\r\x1b[K${line}`);
107
88
  }
108
89
  }
109
90
 
@@ -112,7 +93,7 @@ function extractFileTarget(toolInput) {
112
93
  ? (toolInput.file_path || toolInput.path || '')
113
94
  : '';
114
95
  if (!raw) return '';
115
- return raw.split('/').slice(-2).join('/').slice(0, 40);
96
+ return raw.split('/').slice(-2).join('/');
116
97
  }
117
98
 
118
99
  function extractBashLabel(cmd) {
package/src/prompts.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const path = require('path');
4
5
  const { paths, loadConfig, getProjectRoot } = require('./config');
5
6
  const { loadTasks, findNextTask, getStats } = require('./tasks');
6
7
 
@@ -97,10 +98,10 @@ function buildCodingPrompt(sessionNum, opts = {}) {
97
98
  testEnvHint = `如需持久化测试凭证(API Key、测试账号密码等),写入 ${projectRoot}/.claude-coder/test.env(KEY=value 格式,每行一个)。后续 session 会自动感知。`;
98
99
  }
99
100
 
100
- // Hint 6c: Playwright authenticated state
101
+ // Hint 6c: Playwright auth state
101
102
  let playwrightAuthHint = '';
102
- if (p.playwrightAuth && fs.existsSync(p.playwrightAuth)) {
103
- playwrightAuthHint = `已检测到 Playwright 登录状态(${projectRoot}/.claude-coder/playwright-auth.json),前端/全栈测试将使用已认证的浏览器会话(含 cookieslocalStorage)。`;
103
+ if (fs.existsSync(p.playwrightAuth)) {
104
+ playwrightAuthHint = `已检测到 Playwright 登录状态(${projectRoot}/.claude-coder/playwright-auth.json),MCP 使用 --isolated --storage-state 模式,每次会话自动加载 localStorage cookies。`;
104
105
  }
105
106
 
106
107
  // Hint 7: Session memory (read flat session_result.json)
@@ -108,9 +109,9 @@ function buildCodingPrompt(sessionNum, opts = {}) {
108
109
  if (fs.existsSync(p.sessionResult)) {
109
110
  try {
110
111
  const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
111
- if (sr?.task_id) {
112
- memoryHint = `上次会话: ${sr.task_id} → ${sr.status_after || sr.session_result}` +
113
- (sr.notes ? `, 要点: ${sr.notes.slice(0, 100)}` : '') + '。';
112
+ if (sr?.session_result) {
113
+ memoryHint = `上次会话: ${sr.session_result}(${sr.status_before || '?'} → ${sr.status_after || '?'})` +
114
+ (sr.notes ? `, 要点: ${sr.notes.slice(0, 150)}` : '') + '。';
114
115
  }
115
116
  } catch { /* ignore */ }
116
117
  }
@@ -198,7 +199,7 @@ function buildScanPrompt(projectType, requirement) {
198
199
  'profile 质量要求(必须遵守,harness 会校验):',
199
200
  '- services 数组必须包含所有可启动服务(command、port、health_check),不得为空',
200
201
  '- existing_docs 必须列出所有实际存在的文档路径',
201
- '- 前后端分离项目必须生成 docs/ARCHITECTURE.md(模块职责、数据流、API 路由),并加入 existing_docs',
202
+ '- 检查 .claude/CLAUDE.md 是否存在,若无则生成(WHAT/WHY/HOW 格式:技术栈、关键决策、开发命令、关键路径、编码规则),并加入 existing_docs',
202
203
  '- scan_files_checked 必须列出所有实际扫描过的文件',
203
204
  '',
204
205
  '步骤 3:根据以下指导分解任务到 tasks.json(格式见 CLAUDE.md):',
@@ -263,6 +264,21 @@ function buildAddPrompt(instruction) {
263
264
  }
264
265
  } catch { /* ignore */ }
265
266
 
267
+ // --- Conditional: Playwright test rule hint ---
268
+ let testRuleHint = '';
269
+ const testRulePath = path.join(p.loopDir, 'test_rule.md');
270
+ const hasMcp = fs.existsSync(p.mcpConfig);
271
+ if (fs.existsSync(testRulePath) && hasMcp) {
272
+ testRuleHint = [
273
+ '【Playwright 测试规则】项目已配置 Playwright MCP(.mcp.json),' +
274
+ '`.claude-coder/test_rule.md` 中包含通用测试指导规则(Smart Snapshot、Token 预算控制、三步测试方法论、等待策略等)。',
275
+ '当任务涉及端到端测试时:',
276
+ ' - 在 steps 中第一步加入「阅读 .claude-coder/test_rule.md 了解测试规范和成本控制」',
277
+ ' - 测试步骤按 test_rule.md 中的 tasks.json 模板格式编写(含环境检查、优先级标注、预算控制)',
278
+ ' - 设定合理的 test_tier(unit/smoke/regression/full_e2e)',
279
+ ].join('\n');
280
+ }
281
+
266
282
  return [
267
283
  // --- Primacy zone: role + identity ---
268
284
  '你是资深需求分析师,擅长将模糊需求分解为可执行的原子任务。',
@@ -285,12 +301,13 @@ function buildAddPrompt(instruction) {
285
301
  '5. 分解任务:每个任务对应一个独立可测试的功能单元,description 简明(40字内),steps 具体可操作',
286
302
  '6. 追加到 tasks.json,id 和 priority 从已有最大值递增,status: pending',
287
303
  '7. git add -A && git commit -m "chore: add new tasks"',
288
- '8. 写入 session_result.json(格式:{ "session_result": "success", "task_id": "add-tasks", "status_before": "N/A", "status_after": "N/A", "git_commit": "hash", "tests_passed": false, "notes": "追加了 N 个任务:简述" })',
304
+ '8. 写入 session_result.json(格式:{ "session_result": "success", "status_before": "N/A", "status_after": "N/A", "notes": "追加了 N 个任务:简述" })',
289
305
  '',
290
306
 
291
307
  // --- Quality constraints ---
292
308
  taskGuide,
293
309
  '',
310
+ testRuleHint,
294
311
  '不修改已有任务,不实现代码。',
295
312
  '',
296
313
 
package/src/runner.js CHANGED
@@ -286,7 +286,7 @@ async function run(requirement, opts = {}) {
286
286
  }
287
287
 
288
288
  log('info', '开始 harness 校验 ...');
289
- const validateResult = await validate(headBefore);
289
+ const validateResult = await validate(headBefore, taskId);
290
290
 
291
291
  if (!validateResult.fatal) {
292
292
  if (validateResult.hasWarnings) {
@@ -302,7 +302,7 @@ async function run(requirement, opts = {}) {
302
302
  timestamp: new Date().toISOString(),
303
303
  result: 'success',
304
304
  cost: sessionResult.cost,
305
- taskId: validateResult.sessionData?.task_id || taskId,
305
+ taskId,
306
306
  statusAfter: validateResult.sessionData?.status_after || null,
307
307
  notes: validateResult.sessionData?.notes || null,
308
308
  });
@@ -375,8 +375,20 @@ async function add(instruction, opts = {}) {
375
375
  process.exit(1);
376
376
  }
377
377
 
378
+ deployTestRule(p);
379
+
378
380
  await runAddSession(instruction, { projectRoot, ...opts });
379
381
  printStats();
380
382
  }
381
383
 
384
+ function deployTestRule(p) {
385
+ const dest = path.join(p.loopDir, 'test_rule.md');
386
+ if (fs.existsSync(dest)) return;
387
+ if (!fs.existsSync(p.testRuleTemplate)) return;
388
+ try {
389
+ fs.copyFileSync(p.testRuleTemplate, dest);
390
+ log('ok', '已部署测试指导规则 → .claude-coder/test_rule.md');
391
+ } catch { /* ignore */ }
392
+ }
393
+
382
394
  module.exports = { run, add };
package/src/session.js CHANGED
@@ -132,6 +132,7 @@ async function runCodingSession(sessionNum, opts = {}) {
132
132
  enableStallDetection: true,
133
133
  stallTimeoutMs,
134
134
  enableEditGuard: true,
135
+ editThreshold: config.editThreshold,
135
136
  });
136
137
 
137
138
  indicator.start(sessionNum);
package/src/validator.js CHANGED
@@ -43,10 +43,6 @@ function validateSessionResult() {
43
43
  return { valid: false, fatal: true, recoverable: false, reason: `无效 status_after: ${data.status_after}` };
44
44
  }
45
45
 
46
- if (!data.task_id) {
47
- log('warn', 'session_result.json 缺少 task_id (建议包含)');
48
- }
49
-
50
46
  if (data.session_result === 'success') {
51
47
  log('ok', 'session_result.json 合法 (success)');
52
48
  } else {
@@ -83,7 +79,7 @@ function checkGitProgress(headBefore) {
83
79
  return { hasCommit: true, warning: false };
84
80
  }
85
81
 
86
- function checkTestCoverage() {
82
+ function checkTestCoverage(taskId) {
87
83
  const p = paths();
88
84
 
89
85
  if (!fs.existsSync(p.testsFile) || !fs.existsSync(p.sessionResult)) return;
@@ -91,11 +87,9 @@ function checkTestCoverage() {
91
87
  try {
92
88
  const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
93
89
  const tests = JSON.parse(fs.readFileSync(p.testsFile, 'utf8'));
94
-
95
- const taskId = sr.task_id || '';
96
90
  const testCases = tests.test_cases || [];
97
91
 
98
- if (sr.status_after === 'done' && sr.tests_passed) {
92
+ if (sr.status_after === 'done' && taskId) {
99
93
  const taskTests = testCases.filter(t => t.feature_id === taskId);
100
94
  if (taskTests.length > 0) {
101
95
  const failed = taskTests.filter(t => t.last_result === 'fail');
@@ -109,7 +103,7 @@ function checkTestCoverage() {
109
103
  } catch { /* ignore */ }
110
104
  }
111
105
 
112
- async function validate(headBefore) {
106
+ async function validate(headBefore, taskId) {
113
107
  log('info', '========== 开始校验 ==========');
114
108
 
115
109
  let srResult = validateSessionResult();
@@ -123,7 +117,7 @@ async function validate(headBefore) {
123
117
  srResult.fatal = true;
124
118
  }
125
119
 
126
- checkTestCoverage();
120
+ checkTestCoverage(taskId);
127
121
 
128
122
  const fatal = srResult.fatal;
129
123
  const hasWarnings = gitResult.warning || srResult.recoverable;
@@ -51,7 +51,7 @@
51
51
  | `.claude-coder/session_result.json` | 本次会话的结构化输出 | 每次会话结束时覆盖写入 |
52
52
  | `.claude-coder/tests.json` | 功能验证记录(轻量) | 可新增和更新;仅当功能涉及 API 或核心逻辑时记录 |
53
53
  | `.claude-coder/test.env` | 测试凭证(API Key、测试账号等) | **可追加写入**;发现测试需要的凭证时持久化到此文件 |
54
- | `.claude-coder/playwright-auth.json` | 浏览器登录状态(cookies + localStorage) | 只读;由用户通过 `claude-coder auth` 预配置 |
54
+ | `.claude-coder/playwright-auth.json` | 浏览器登录状态(cookies + localStorage) | 只读;由 `claude-coder auth` 生成,MCP 每次会话自动加载 |
55
55
 
56
56
  ### requirements.md 处理原则
57
57
 
@@ -97,12 +97,9 @@
97
97
  ```json
98
98
  {
99
99
  "session_result": "success | failed",
100
- "task_id": "feat-xxx",
101
100
  "status_before": "pending | failed",
102
101
  "status_after": "done | failed | in_progress | testing",
103
- "git_commit": "abc1234 null",
104
- "tests_passed": true | false,
105
- "notes": "本次会话的简要说明"
102
+ "notes": "本次做了什么 + 遇到的问题 + 给下一个会话的提醒"
106
103
  }
107
104
  ```
108
105
 
@@ -137,38 +134,17 @@
137
134
 
138
135
  ## 任务状态机(严格遵守)
139
136
 
140
- 每个任务在 `tasks.json` 中有一个 `status` 字段,合法状态和迁移规则如下:
137
+ 每个任务在 `tasks.json` 中有一个 `status` 字段,合法迁移路径如下:
141
138
 
142
- ```
143
- pending ──→ in_progress ──→ testing ──→ done
144
-
145
-
146
- failed ──→ in_progress(重试)
147
- ```
148
-
149
- ### 状态说明
150
-
151
- | 状态 | 含义 | 何时设置 |
139
+ | 当前状态 | 可迁移至 | 触发条件 |
152
140
  |---|---|---|
153
- | `pending` | 未开始 | 初始状态 |
154
- | `in_progress` | 正在实现 | 你开始编码时 |
155
- | `testing` | 代码已写完,正在测试 | 代码完成、开始验证时 |
156
- | `done` | 测试通过,功能完成 | 端到端测试通过后 |
157
- | `failed` | 测试失败或实现有问题 | 测试未通过时 |
158
-
159
- ### 迁移规则(铁律)
160
-
161
- - `pending` → `in_progress`:开始工作
162
- - `in_progress` → `testing`:代码写完,开始验证
163
- - `testing` → `done`:所有测试通过
164
- - `testing` → `failed`:测试未通过
165
- - `failed` → `in_progress`:重试修复
166
-
167
- **禁止的迁移**:
168
- - `pending` → `done`(不允许跳步)
169
- - `pending` → `testing`(必须先写代码)
170
- - `in_progress` → `done`(必须先测试)
171
- - 任何状态 → `pending`(不允许回退到未开始)
141
+ | `pending` | `in_progress` | 开始编码 |
142
+ | `in_progress` | `testing` | 代码写完,开始验证 |
143
+ | `testing` | `done` | 所有测试通过 |
144
+ | `testing` | `failed` | 测试未通过 |
145
+ | `failed` | `in_progress` | 重试修复 |
146
+
147
+ **禁止**:跳步(如 `pending` → `done`)、回退到 `pending`、未测试直接 `done`
172
148
 
173
149
  ---
174
150
 
@@ -247,18 +223,15 @@ pending ──→ in_progress ──→ testing ──→ done
247
223
  1. **后台服务管理**:根据 prompt 提示决定——单次模式(`--max 1`)时停止所有后台服务(`lsof -ti :端口 | xargs kill`);连续模式时保持服务运行,下个 session 继续使用
248
224
  2. **按需更新文档和 profile**:
249
225
  - **README / 用户文档**:仅当对外行为变化(新增功能、API 变更、使用方式变化)时更新
250
- - **架构 / API 文档**:如果本次新增了模块、改变了模块职责或新增了 API 端点,更新 `existing_docs` 中对应的架构或 API 文档。同时更新 `project_profile.json` 的 `existing_docs` 列表(若新增了文档文件)
226
+ - **项目指令文件**:如果本次新增了模块、改变了模块职责或新增了 API 端点,更新 `.claude/CLAUDE.md`。同时确保 `project_profile.json` 的 `existing_docs` 列表包含此文件
251
227
  - **profile 补全**:如果 prompt 中提示 `project_profile.json` 有缺陷(如 services 为空、existing_docs 为空),在此步骤补全。Harness 依赖 profile 做环境初始化和上下文注入
252
228
  3. **Git 提交**:`git add -A && git commit -m "feat(task-id): 功能描述"`
253
229
  4. **写入 session_result.json**(notes 要充分记录上下文供下次恢复):
254
230
  ```json
255
231
  {
256
232
  "session_result": "success 或 failed",
257
- "task_id": "当前任务 ID",
258
233
  "status_before": "任务开始时的状态",
259
234
  "status_after": "任务结束时的状态",
260
- "git_commit": "本次提交的 hash",
261
- "tests_passed": true 或 false,
262
235
  "notes": "本次做了什么 + 遇到的问题 + 给下一个会话的提醒"
263
236
  }
264
237
  ```
@@ -20,8 +20,8 @@
20
20
 
21
21
  **文档标准(按优先级)**:
22
22
  1. **README.md**(必须有):项目简介、技术栈、目录结构、如何运行。若缺失或过于简略,先补充
23
- 2. **架构文档**(推荐有):如果 `docs/` 中没有架构概述,生成一份简要的架构文档(如 `docs/ARCHITECTURE.md`),包含:模块职责、核心数据流、关键 API 路由。格式用结构化标题,方便 AI 快速检索
24
- 3. **API 文档**:如果项目有 API 且无文档,在 docs/ README 中补充主要端点列表
23
+ 2. **`.claude/CLAUDE.md`**(推荐有):检查 `.claude/` 目录下是否已有 `CLAUDE.md`。若无,生成一份项目指令文件,采用 WHAT/WHY/HOW 格式:WHAT(项目是什么、技术栈)、WHY(关键技术决策)、HOW(开发命令、测试命令、关键路径表、编码规则)。此文件会被 Claude Code 自动加载为项目上下文
24
+ 3. **API 文档**:如果项目有 API 且无文档,在 `.claude/CLAUDE.md` HOW 部分或 README 中补充主要端点列表
25
25
 
26
26
  按顺序检查以下文件,**存在则读取**,不存在则跳过:
27
27
 
@@ -42,7 +42,7 @@
42
42
  2. 根据需求(`requirements.md` 或 harness 传入的需求文本),设计技术架构
43
43
  3. 创建项目目录结构和基础文件(入口文件、配置文件、依赖文件等)
44
44
  4. 生成 `README.md`(项目用途、技术栈、如何运行)
45
- 5. 如果项目包含 2 个以上模块或前后端分离,生成简要架构文档 `docs/ARCHITECTURE.md`(模块职责、数据流、API 路由)
45
+ 5. 如果 `.claude/CLAUDE.md` 不存在,生成项目指令文件(WHAT/WHY/HOW 格式),包含模块职责、数据流、API 路由、开发和测试命令
46
46
  6. 初始化包管理(`npm init` / `pip freeze` 等)
47
47
  7. 完成后,执行**步骤 2A 的扫描流程**生成 `project_profile.json`
48
48
 
@@ -100,7 +100,7 @@
100
100
  "python_env": "conda:env_name | venv | system",
101
101
  "node_version": "20 | 18 | none"
102
102
  },
103
- "existing_docs": ["README.md", "docs/api.md"],
103
+ "existing_docs": ["README.md", ".claude/CLAUDE.md"],
104
104
  "has_tests": false,
105
105
  "has_docker": false,
106
106
  "mcp_tools": {
@@ -0,0 +1,157 @@
1
+ # Playwright 自动化测试通用规则 v0.0.1
2
+
3
+ ## 一、四条铁律
4
+
5
+ 1. **真实操作** — 必须通过 Playwright MCP 产生浏览器交互,代码审查不等于测试
6
+ 2. **测试业务** — 断言基于用户可见结果(页面文本、按钮状态),非内部变量
7
+ 3. **独立可重复** — 每个场景不依赖其他测试结果
8
+ 4. **先调查再修复** — 失败先分析根因,不要修改测试让它通过
9
+
10
+ ## 二、三步测试方法论
11
+
12
+ 任何 Web 项目的端到端测试遵循三步走:
13
+
14
+ ### Step 1: 功能验证(Happy Path)
15
+
16
+ 核心用户流程能走通,每个步骤对应一个 Playwright MCP 工具调用:
17
+
18
+ ```
19
+ 1. browser_navigate → [页面URL]
20
+ 2. browser_snapshot → 确认页面加载,定位关键元素 ref
21
+ 3. browser_fill_form / browser_type → 输入测试数据
22
+ 4. browser_click → 提交操作
23
+ 5. browser_wait_for → 等待结果出现
24
+ 6. browser_snapshot → 验证预期结果
25
+ ```
26
+
27
+ ### Step 2: 错误场景(Unhappy Path)
28
+
29
+ | 类别 | 典型场景 |
30
+ |------|---------|
31
+ | 输入验证 | 空提交、超长输入、特殊字符、非法格式 |
32
+ | 认证权限 | 未登录访问、过期凭证、无效 API Key |
33
+ | 网络服务 | 后端宕机、慢响应、API 500 |
34
+ | 状态边界 | 空数据、大数据量、重复提交、浏览器后退 |
35
+
36
+ ### Step 3: 探索性测试
37
+
38
+ 以目标用户角色自由使用系统,关注可发现性、可理解性、响应速度、错误恢复、视觉一致性。
39
+
40
+ ## 三、Playwright MCP 工具速查
41
+
42
+ ### 导航与观察
43
+
44
+ | 工具 | 用途 | 关键参数 |
45
+ |------|------|---------|
46
+ | `browser_navigate` | 打开页面 | `url` |
47
+ | `browser_snapshot` | 获取页面可访问性快照 | 无 |
48
+ | `browser_console_messages` | 检查控制台 | `level` |
49
+ | `browser_network_requests` | 网络请求日志 | 无 |
50
+
51
+ ### 交互操作
52
+
53
+ | 工具 | 用途 | 关键参数 |
54
+ |------|------|---------|
55
+ | `browser_click` | 点击元素 | `ref`, `element` |
56
+ | `browser_type` | 逐字符输入 | `ref`, `text`, `submit` |
57
+ | `browser_fill_form` | 批量填写表单 | `fields[]` |
58
+ | `browser_select_option` | 选择下拉项 | `ref`, `values[]` |
59
+ | `browser_press_key` | 按键 | `key` |
60
+ | `browser_file_upload` | 上传文件 | `paths[]` |
61
+ | `browser_handle_dialog` | 处理弹窗 | `accept` |
62
+
63
+ ### 等待与控制
64
+
65
+ | 工具 | 用途 | 关键参数 |
66
+ |------|------|---------|
67
+ | `browser_wait_for` | 等待元素/文本出现 | `text`, `ref`, `timeout` |
68
+ | `browser_evaluate` | 执行 JS | `function` |
69
+ | `browser_close` | 关闭页面 | 无 |
70
+
71
+ ## 四、Smart Snapshot 策略(节省 40-60% Token)
72
+
73
+ 每次 `browser_snapshot` 消耗 3,000-8,000 tokens。分级控制:
74
+
75
+ | 级别 | 何时 snapshot | 示例 |
76
+ |------|-------------|------|
77
+ | **必须** | 首次加载页面 | navigate 后确认页面正确 |
78
+ | **必须** | 关键断言点 | 验证操作结果出现 |
79
+ | **必须** | 操作失败时 | 调查页面状态 |
80
+ | **可选** | 中间操作后 | fill 后确认文字填入 |
81
+ | **跳过** | 连续同类操作间 | 连续选择多个下拉框 |
82
+ | **跳过** | 等待循环中 | 改用 `browser_wait_for` |
83
+
84
+ **高效模式**:navigate → snapshot → fill → select → click → wait_for → snapshot(**2 次**)
85
+ **低效模式**:navigate → snapshot → fill → snapshot → select → snapshot → click → snapshot(**4 次**)
86
+
87
+ ## 五、等待策略
88
+
89
+ ### 按操作类型选择
90
+
91
+ | 操作类型 | 策略 | Token 消耗 |
92
+ |---------|------|-----------|
93
+ | 瞬时(导航、点击) | 直接操作,不等待 | 极低 |
94
+ | 短等(表单提交) | `browser_wait_for text="成功" timeout=10000` | ~5K |
95
+ | 长等(AI 生成、文件处理) | 指数退避轮询 | ~20K |
96
+ | 超长等(批量处理) | Shell 端 API 检查 + 最终 1 次 snapshot | ~5.5K |
97
+
98
+ ### 指数退避轮询模式(长操作)
99
+
100
+ - 每步 snapshot → 合并 2-3 操作后再 snapshot
101
+ - MCP 做 20+ 步 → 长流程用 Playwright CLI
102
+ - 反复 navigate 同一页面 → 在同一页面完成
103
+ - 失败后盲目重试 → 先 `browser_console_messages` 分析
104
+
105
+ ### 优先级映射
106
+
107
+ P0(核心流程)必测 → P1(错误处理)必测 → P2(次要功能)按需 → P3 低优先
108
+
109
+ 预算 >200K: P0+P1+P2 | 100-200K: P0+P1 | <100K: 仅 P0
110
+
111
+ ## 六、凭证管理
112
+
113
+ `.mcp.json` 配置 `--isolated --storage-state=path/to/auth.json`。
114
+
115
+ **关键**: `--storage-state` **必须**配合 `--isolated`,否则 localStorage 不注入。
116
+
117
+ 凭证失效时:不修改 auth 文件,报告中标注,提示用户运行 `claude-coder auth [URL]`。
118
+
119
+ ## 七、失败处理
120
+
121
+ **阻断性**(立即停止): 服务未启动、500 错误、凭证缺失、页面空白
122
+
123
+ **非阻断性**(记录继续): 样式异常、console warning、慢响应
124
+
125
+ 失败时: snapshot(记录状态)→ console_messages(错误日志)→ 停止该场景 → 继续下一个
126
+
127
+ ## 八、tasks.json 测试步骤模板
128
+
129
+ ```json
130
+ {
131
+ "steps": [
132
+ "【规则】阅读 .claude-coder/test_rule.md",
133
+ "【环境】curl [后端]/health && curl [前端](失败则停止)",
134
+ "【P0】Playwright MCP 执行核心 Happy Path(Smart Snapshot)",
135
+ "【P1】错误场景:空输入、无效凭证",
136
+ "【记录】结果写入 record/",
137
+ "【预算】消耗 >80% 时跳过低优先级,记录 session_result.json"
138
+ ]
139
+ }
140
+ ```
141
+
142
+ ## 九、测试报告格式
143
+
144
+ ```markdown
145
+ # E2E 测试报告
146
+ **日期**: YYYY-MM-DD | **环境**: 前端 [URL] / 后端 [URL]
147
+
148
+ | 场景 | 结果 | 备注 |
149
+ |------|------|------|
150
+ | [名称] | PASS/FAIL | [简要] |
151
+
152
+ ## 发现的问题
153
+ ### [P0/P1/P2] 标题
154
+ - **复现**: [Playwright 动作序列]
155
+ - **预期/实际**: ...
156
+ - **根因**: [代码分析]
157
+ ```
@@ -1,325 +0,0 @@
1
- # 分阶段提示语注入 — 技术调研与方向探讨
2
-
3
- > 状态:调研阶段,仅探讨,未实现
4
- > 日期:2026-03-04
5
- > 背景:当前所有 10 个 Hint 在 session 开始前一次性注入 user prompt。本文探讨利用 Hook 的 `additionalContext` 能力,将提示语拆分到不同阶段按需注入。
6
-
7
- ---
8
-
9
- ## 1. 当前架构
10
-
11
- ### 提示语注入时机
12
-
13
- ```mermaid
14
- sequenceDiagram
15
- participant H as Harness
16
- participant SDK as Claude Agent SDK
17
- participant Agent as Agent (Model)
18
-
19
- H->>H: buildSystemPrompt()<br/>CLAUDE.md (~260行)
20
- H->>H: buildCodingPrompt()<br/>10 个 Hint 全部拼接
21
- H->>SDK: query({ prompt, options })
22
- Note over SDK,Agent: 所有提示语一次性加载
23
-
24
- loop Agent 自主运行
25
- Agent->>SDK: 工具调用 (Read/Edit/Bash...)
26
- SDK->>H: PreToolUse hook 回调
27
- H->>H: inferPhaseStep() 更新 spinner
28
- H->>H: 检查编辑循环
29
- H-->>SDK: return {} (放行)
30
- SDK->>Agent: 工具结果
31
- end
32
- ```
33
-
34
- ### 问题
35
-
36
- | 问题 | 说明 |
37
- |------|------|
38
- | **Token 浪费** | 10 个 Hint 全部注入 user prompt,但大部分 Hint 仅在特定阶段有用(如 testHint 仅 Step 5 需要) |
39
- | **注意力稀释** | 一次性注入大量指令,模型在真正需要某条指令时可能已"忘记"(context rot) |
40
- | **时机错位** | 工具使用指导(Hint 10)在 Agent 还没开始读文件时就注入了,但 Agent 在 Step 4 编码阶段才真正需要这些规则 |
41
- | **无法纠正** | 当前 Hook 仅用于监控和死循环拦截,无法在 Agent 做出低效工具选择时即时纠正 |
42
-
43
- ---
44
-
45
- ## 2. Hook 能力盘点
46
-
47
- ### SDK 内联 Hook(当前使用方式)
48
-
49
- 通过 `query()` 的 `options.hooks` 定义,进程内回调:
50
-
51
- ```javascript
52
- sdk.query({
53
- prompt,
54
- options: {
55
- hooks: {
56
- PreToolUse: [{ matcher: '*', hooks: [async (input) => { ... }] }],
57
- PostToolUse: [{ matcher: '*', hooks: [async (input) => { ... }] }],
58
- }
59
- }
60
- });
61
- ```
62
-
63
- | Hook 事件 | SDK 内联支持 | 能力 |
64
- |-----------|-------------|------|
65
- | `PreToolUse` | 是 | `permissionDecision` (allow/deny/ask), `message`, **`additionalContext`** (v2.1.9+), `updatedInput` |
66
- | `PostToolUse` | 是 | `decision` (block), `reason`, **`additionalContext`** |
67
- | `UserPromptSubmit` | 是 | `decision` (block), `reason`, `additionalContext` |
68
- | `Stop` | 是 | `decision` (block), `reason` |
69
- | `SessionStart` | **否** (仅 CLI 声明式) | 不适用 |
70
- | `SessionEnd` | **否** (仅 CLI 声明式) | 不适用 |
71
-
72
- ### `additionalContext` 关键特性
73
-
74
- - **作用**: 将文本注入 Agent 的 context window,Agent 在后续推理中可以看到并遵循
75
- - **注入位置**: 作为工具调用的附加上下文出现,紧邻工具结果
76
- - **注意力**: 因为紧跟当前工具调用,处于模型注意力的高峰区域(recency zone)
77
- - **限制**: 2026年1月新增,可能存在边缘 bug
78
-
79
- ### `decision: 'block'` + `message`(当前已在用)
80
-
81
- - **作用**: 阻止工具调用,`message` 作为错误反馈传回模型
82
- - **注意力**: 模型会将其视为"操作失败"信息,遵循率高
83
- - **适用场景**: 拦截不当操作并引导替代方案
84
-
85
- ---
86
-
87
- ## 3. 提议架构:分阶段注入
88
-
89
- ### 核心思想
90
-
91
- **按 Agent 的工作阶段,在 Hook 中按需注入对应阶段的提示语。** 初始 prompt 仅包含最核心的内容(身份、任务、约束),其余指导在 Agent 进入相应阶段时即时注入。
92
-
93
- ```mermaid
94
- sequenceDiagram
95
- participant H as Harness
96
- participant SDK as Claude Agent SDK
97
- participant Agent as Agent
98
-
99
- H->>SDK: query({ prompt: 精简版 })
100
- Note over H: 仅注入: 身份 + 任务上下文 + 约束
101
-
102
- rect rgb(200, 230, 255)
103
- Note over Agent: Phase 1: 恢复上下文
104
- Agent->>SDK: Read(.claude-coder/profile.json)
105
- SDK->>H: PreToolUse
106
- H-->>SDK: additionalContext: 路径提示 + 文档指引
107
- end
108
-
109
- rect rgb(200, 255, 200)
110
- Note over Agent: Phase 2: 编码阶段
111
- Agent->>SDK: Edit(src/app.ts)
112
- SDK->>H: PreToolUse
113
- H-->>SDK: additionalContext: 工具使用规范 + MultiEdit提示
114
- end
115
-
116
- rect rgb(255, 230, 200)
117
- Note over Agent: Phase 3: 测试阶段
118
- Agent->>SDK: Bash(curl ...)
119
- SDK->>H: PreToolUse
120
- H-->>SDK: additionalContext: 测试效率规则
121
- end
122
-
123
- rect rgb(230, 200, 255)
124
- Note over Agent: Phase 4: 收尾阶段
125
- Agent->>SDK: Bash(git commit)
126
- SDK->>H: PreToolUse
127
- H-->>SDK: additionalContext: 服务管理 + session_result 格式
128
- end
129
- ```
130
-
131
- ### Hint 拆分方案
132
-
133
- | # | Hint | 当前位置 | 建议注入时机 | 注入方式 |
134
- |---|------|----------|-------------|----------|
135
- | 1 | `reqSyncHint` | user prompt | **保留在 user prompt** | 需求变更需要在 Step 1 就知道 |
136
- | 7 | `taskHint` | user prompt | **保留在 user prompt** | 任务上下文是 Agent 开始工作的前提 |
137
- | 8 | `memoryHint` | user prompt | **保留在 user prompt** | 上次会话记忆需要一开始就有 |
138
- | 5 | `envHint` | user prompt | **保留在 user prompt** | Step 2 环境检查需要一开始就知道 |
139
- | 2 | `mcpHint` | user prompt | PreToolUse (Bash: curl/test) | 测试时才需要知道 Playwright 可用 |
140
- | 3 | `testHint` | user prompt | PreToolUse (Bash: curl/test) | 测试时才需要避免重复验证 |
141
- | 4 | `docsHint` | user prompt | PreToolUse (Read: 首次读文件) | 读文件时提醒先读文档 |
142
- | 6 | `retryContext` | user prompt | **保留在 user prompt** | 重试上下文需要一开始就有 |
143
- | 9 | `serviceHint` | user prompt | PreToolUse (Bash: git) | 收尾时才需要知道是否停服务 |
144
- | 10 | `toolGuidance` | user prompt | PreToolUse (首次工具调用) | 开始使用工具时注入 |
145
-
146
- **结论**: 10 个 Hint 中,5 个适合保留在初始 prompt(1, 5, 6, 7, 8),5 个适合延迟注入到 Hook(2, 3, 4, 9, 10)。
147
-
148
- ### 实现草案
149
-
150
- ```javascript
151
- // session.js - PreToolUse hook 增强版(概念代码,仅供讨论)
152
- const injected = new Set(); // 跟踪已注入的 Hint,每个仅注入一次
153
-
154
- hooks: {
155
- PreToolUse: [{
156
- matcher: '*',
157
- hooks: [async (input) => {
158
- const name = input.tool_name;
159
- const toolInput = input.tool_input || {};
160
- let additionalContext = '';
161
-
162
- // --- Phase: 读取文件 → 注入文档指引 ---
163
- if (['Read', 'Glob', 'Grep', 'LS'].includes(name) && !injected.has('docs')) {
164
- additionalContext += docsHint; // Hint 4
165
- injected.add('docs');
166
- }
167
-
168
- // --- Phase: 首次工具调用 → 注入工具使用规范 ---
169
- if (!injected.has('toolGuide')) {
170
- additionalContext += '\n' + toolGuidance; // Hint 10
171
- injected.add('toolGuide');
172
- }
173
-
174
- // --- Phase: 测试阶段 → 注入测试规则 ---
175
- if (name === 'Bash') {
176
- const cmd = toolInput.command || '';
177
- if ((cmd.includes('curl') || cmd.includes('test') || cmd.includes('pytest'))
178
- && !injected.has('test')) {
179
- additionalContext += '\n' + testHint; // Hint 3
180
- additionalContext += '\n' + mcpHint; // Hint 2
181
- injected.add('test');
182
- }
183
-
184
- // --- Phase: Git 操作 → 注入收尾提示 ---
185
- if (cmd.includes('git ') && !injected.has('service')) {
186
- additionalContext += '\n' + serviceHint; // Hint 9
187
- injected.add('service');
188
- }
189
- }
190
-
191
- // --- Bash 命令纠正(进阶) ---
192
- if (name === 'Bash') {
193
- const cmd = toolInput.command || '';
194
- if (/\bgrep\b/.test(cmd) && !cmd.includes('rg ')) {
195
- return {
196
- permissionDecision: 'deny',
197
- permissionDecisionReason: '请使用 Grep 工具替代 bash grep,效率更高且结果格式化更好。',
198
- };
199
- }
200
- if (/\bfind\b/.test(cmd)) {
201
- return {
202
- permissionDecision: 'deny',
203
- permissionDecisionReason: '请使用 Glob 工具替代 bash find。',
204
- };
205
- }
206
- if (/\bcat\b/.test(cmd) && !cmd.includes('<<')) {
207
- return {
208
- permissionDecision: 'deny',
209
- permissionDecisionReason: '请使用 Read 工具替代 bash cat。',
210
- };
211
- }
212
- }
213
-
214
- // --- 编辑循环检测(已有功能) ---
215
- // ... existing loop detection code ...
216
-
217
- // 注入上下文
218
- if (additionalContext.trim()) {
219
- return { additionalContext: additionalContext.trim() };
220
- }
221
- return {};
222
- }]
223
- }]
224
- }
225
- ```
226
-
227
- ---
228
-
229
- ## 4. Bash 命令拦截:工具纠正的最短路径
230
-
231
- 在完整的分阶段注入之前,有一个**低成本高收益**的中间步骤:在 PreToolUse hook 中拦截 Agent 使用 Bash 执行低效命令(grep/find/cat/ls/head/tail),引导其使用专用工具。
232
-
233
- ### 行为矩阵
234
-
235
- | Agent 执行 | Hook 行为 | 反馈给 Agent |
236
- |------------|----------|-------------|
237
- | `Bash: grep -r "pattern" .` | **deny** | "请使用 Grep 工具替代 bash grep" |
238
- | `Bash: find . -name "*.ts"` | **deny** | "请使用 Glob 工具替代 bash find" |
239
- | `Bash: cat src/app.ts` | **deny** | "请使用 Read 工具替代 bash cat" |
240
- | `Bash: ls -la` | **deny** | "请使用 LS 工具替代 bash ls" |
241
- | `Bash: head -20 file.txt` | **deny** | "请使用 Read 工具(支持 offset/limit)替代 bash head" |
242
- | `Bash: npm test` | allow | -- |
243
- | `Bash: git commit` | allow + additionalContext | 注入收尾提示 |
244
-
245
- ### 优势
246
-
247
- - **确定性**: Hook 拦截是确定性的,不依赖模型是否"记住"了 prompt 中的工具规则
248
- - **即时纠正**: 在 Agent 犯错的那一刻就纠正,而不是等它浪费完 context
249
- - **渐进式**: 可以先实现拦截(deny + message),后续再加 additionalContext
250
- - **非 Claude 模型必需**: qwen/deepseek 等模型对 prompt 的遵循率不如 Claude,但 deny 是硬性拦截,模型无法绕过
251
-
252
- ### 风险
253
-
254
- | 风险 | 缓解方案 |
255
- |------|----------|
256
- | 误拦截合法 Bash 命令(如 `cat <<EOF` heredoc) | 正则匹配需要排除 heredoc、管道等场景 |
257
- | 某些 grep 用法没有 Grep 工具替代(如 `grep -c`) | 只拦截简单模式,复杂 grep 放行 |
258
- | 过度拦截导致 Agent 陷入循环 | 每种拦截最多触发 2 次,第 3 次放行 |
259
-
260
- ---
261
-
262
- ## 5. 与现有方案的对比
263
-
264
- | 维度 | 当前方案 | 分阶段注入 | Bash 拦截纠正 |
265
- |------|---------|-----------|-------------|
266
- | 实现复杂度 | 低 | 高 | 中 |
267
- | Token 效率 | 低(全量注入) | 高(按需注入) | 不变(不影响初始 prompt) |
268
- | 注意力效果 | 中(U型优化) | 高(时机精准) | 高(即时纠正,deny 不可忽略) |
269
- | 非 Claude 模型支持 | 中(靠 prompt) | 高(时机 + prompt) | **最高(硬性拦截)** |
270
- | 风险 | 低 | 中(additionalContext 较新) | 低(deny 已验证) |
271
- | 依赖 SDK 版本 | 无 | v2.1.9+(additionalContext) | 无(deny + message 已有) |
272
-
273
- ---
274
-
275
- ## 6. 建议路线图
276
-
277
- ### P0 — 立即可做(不依赖新 SDK 特性)
278
-
279
- **Bash 命令拦截纠正**
280
-
281
- 在现有 PreToolUse hook 中增加 bash 命令检测,对 `grep`/`find`/`cat`/`ls`/`head`/`tail` 返回 `deny + message` 引导使用专用工具。这是最短路径、最高确定性的优化。
282
-
283
- ### P1 — 短期(需要验证 additionalContext)
284
-
285
- **工具使用指导延迟注入**
286
-
287
- 将 Hint 10(toolGuidance)从初始 prompt 移到 PreToolUse hook 的 `additionalContext`,在 Agent 首次使用工具时注入。验证 `additionalContext` 在非 Claude 模型上的效果。
288
-
289
- ### P2 — 中期
290
-
291
- **测试/收尾阶段指导延迟注入**
292
-
293
- 将 Hint 2/3/9 移到 PreToolUse hook,按阶段(test/git)触发注入。
294
-
295
- ### P3 — 远期
296
-
297
- **完整分阶段注入**
298
-
299
- 所有可延迟的 Hint 通过 Hook 按需注入。初始 prompt 仅保留身份、任务、约束。配合 `additionalContext` 的 PostToolUse 版本,实现"编码后注入代码审查提示"等高级场景。
300
-
301
- ---
302
-
303
- ## 7. 学术/行业参考
304
-
305
- | 来源 | 核心概念 | 与本方案的关联 |
306
- |------|----------|---------------|
307
- | Anthropic Context Engineering (2025) | Context 是有限资源,需精心管理 | 按需注入减少 context 浪费 |
308
- | Claude Code System Prompt (gist) | 每个工具都有 "when to use / when NOT to use" 指导 | Hint 10 和 Bash 拦截复现这一设计 |
309
- | SWE-Agent (2024) ACI | Agent-Computer Interface 设计应优化工具发现和使用 | Hook 即时纠正是 ACI 的运行时优化 |
310
- | Anthropic "Writing effective tools for agents" (2025) | 工具设计影响 Agent 行为,工具在 context 中很显眼 | 扩展 allowedTools 让工具自然出现在模型视野 |
311
- | ContextBench (2025) | 复杂脚手架边际收益递减 | 不过度设计分阶段注入,先做确定性拦截 |
312
-
313
- ---
314
-
315
- ## 8. 结论
316
-
317
- 当前 harness 的提示语架构已经相当成熟(U型注意力 + 10个条件Hint + recency zone 注入)。下一步优化的核心方向是**从"一次性全量注入"向"按需分阶段注入"演进**,但需要渐进式推进:
318
-
319
- 1. **先做 Bash 命令拦截**(P0)— 零风险,最高确定性,不依赖新 SDK 特性
320
- 2. **验证 `additionalContext`**(P1)— 确认非 Claude 模型是否能看到并遵循
321
- 3. **逐步迁移 Hint**(P2-P3)— 每次迁移一个 Hint,A/B 测试效果
322
-
323
- **核心原则:确定性拦截(Hook deny)> 即时注入(additionalContext)> 初始 prompt 指导(Hint)> 系统 prompt 规则(CLAUDE.md)**
324
-
325
- 这个优先级排序体现了一个关键洞察:**越靠近行为发生的时刻,指导的遵循率越高**。