claude-coder 1.5.6 → 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.
@@ -23,8 +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 ← 登录状态快照(备份参考) [auth 命令生成]
27
- browser-profile/ ← 持久化浏览器 Profile(MCP 实际使用) [auth 命令创建]
26
+ playwright-auth.json ← 浏览器登录状态(MCP 每次会话加载) [auth 命令生成]
28
27
  ```
29
28
 
30
29
  ### 文件生命周期
@@ -33,25 +32,26 @@ claude-coder 的核心目标是让 Agent **完全自主测试**,不因凭证
33
32
  |------|--------|--------|----------|----------|
34
33
  | `.env` | `claude-coder setup` | 用户 | 是 | 长期 |
35
34
  | `test.env` | Agent 或用户 | Agent + 用户 | 是 | 长期,按需更新 |
36
- | `playwright-auth.json` | `claude-coder auth` | auth 命令 | 是 | 快照备份,不被 MCP 直接使用 |
37
- | `browser-profile/` | `claude-coder auth` | MCP 自动维护 | 是 | 长期,自动续期 |
38
-
39
- ### 技术实现:为什么用 `--user-data-dir` 而不是 `--storage-state`
40
-
41
- | 维度 | `--storage-state`(旧方案) | `--user-data-dir`(当前方案) |
42
- |------|-------------------------|--------------------------|
43
- | 上下文类型 | 隔离上下文(isolated) | 持久化上下文(persistent) |
44
- | 状态保持 | 每次从 JSON 文件加载,会话结束丢弃 | Profile 自动保存,跨会话保持 |
45
- | Cookies 续期 | 不支持(JSON 文件静态) | 支持(浏览器自动刷新的 cookies 被保留) |
46
- | localStorage | JSON 注入(可靠) | 持久保存在 Profile |
47
- | Google OAuth | 每次创建新上下文 → Google 检测到自动化 | 持久 Profile → Google 视为常规浏览器 |
48
- | 长期运行 | 差(cookies 过期后必须重新 auth) | 优(Profile 随浏览器使用自动演进) |
49
-
50
- > **源码依据**:Playwright MCP `contextFactory()` `isolated=false` 时使用 `PersistentContextFactory`,
51
- > 调用 `launchPersistentContext(userDataDir, options)` 创建持久化浏览器上下文。
52
- > `--storage-state` 虽然不强制 `isolated=true`,但官方文档明确描述其为 "load into an isolated browser context",
53
- > 且 `launchPersistentContext` 的 `storageState` 参数存在已知缺陷(Issue #14949):localStorage 不注入,
54
- > cookies 可能覆盖 Profile 中已刷新的 cookies。
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 浏览器中手动登录一次。
55
55
 
56
56
  ---
57
57
 
@@ -68,11 +68,10 @@ Agent 测试 → 发现需要 API Key → 写入 test.env → 下次 session 自
68
68
  ```
69
69
  用户运行 claude-coder auth url
70
70
  → playwright codegen 打开浏览器 → 手动登录 → 关闭浏览器
71
- → cookies + localStorage 保存为快照备份(playwright-auth.json
72
- 创建 browser-profile/ 目录
73
- 更新 .mcp.json(--user-data-dir 指向 browser-profile/)
74
- 首次 MCP 会话时在浏览器窗口中登录一次
75
- → 之后 MCP 自动使用持久化 Profile,无需再次登录
71
+ → cookies + localStorage 保存到 playwright-auth.json
72
+ 更新 .mcp.json(--isolated --storage-state 指向 playwright-auth.json)
73
+ 每次 MCP 会话自动从 JSON 加载状态(无需手动登录)
74
+ 如需更新状态,重新运行 claude-coder auth
76
75
  ```
77
76
 
78
77
  ### 流程 3:用户预配置 API Key
@@ -98,10 +97,10 @@ claude-coder auth http://testyapi.example.com/group/2245
98
97
  ```
99
98
 
100
99
  **自动完成**:
101
- 1. 创建 `.claude-coder/browser-profile/` 持久化 Profile 目录
102
- 2. 启动 `playwright codegen`,用户手动登录后关闭浏览器(保存快照备份)
103
- 3. 创建/更新 `.mcp.json`,配置 `--user-data-dir=.claude-coder/browser-profile`
104
- 4. 添加 `.gitignore` 条目(`playwright-auth.json` + `browser-profile/`)
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`)
105
104
  5. 启用 `.claude-coder/.env` 中 `MCP_PLAYWRIGHT=true`
106
105
 
107
106
  ---
@@ -117,36 +116,30 @@ OPENAI_API_KEY=sk-xxx
117
116
  EOF
118
117
  claude-coder auth http://localhost:3000
119
118
  claude-coder run
120
- # 首次 MCP 访问需登录的页面时,在浏览器窗口登录一次
121
- # 之后所有 session 自动保持登录状态
119
+ # MCP 每次会话自动从 playwright-auth.json 加载 localStorage 和 cookies
122
120
  ```
123
121
 
124
122
  ### 场景 2:内部系统(Google OAuth / SSO)
125
123
 
126
124
  ```bash
127
125
  claude-coder auth http://testyapi.example.com/group/2245
128
- # 在弹出的浏览器中完成 Google 登录(快照备份)
129
- # 首次 MCP 会话时在浏览器窗口中再次登录
130
- # 之后持久化 Profile 自动保持 Google 登录状态
126
+ # 在弹出的浏览器中完成登录,关闭后状态保存到 JSON
127
+ # MCP 每次会话自动加载此状态
131
128
  ```
132
129
 
133
- > **关于 Google OAuth 检测**:使用 `--user-data-dir` 持久化 Profile 后,Google 将浏览器视为
134
- > "回访用户"而非"新自动化会话",大幅降低重复登录要求。如果仍被检测,可在 `.mcp.json`
135
- > 添加 `--browser chrome` 使用真实 Chrome 进一步降低检测率。
130
+ > **关于 Google OAuth**:`--isolated` 模式每次创建新上下文,Google 可能要求重新验证。
131
+ > 如需 cookie 持久化,可手动修改 `.mcp.json` 为 `--user-data-dir` 模式(但 localStorage 不会注入)。
136
132
 
137
- ### 场景 3:长期运行(核心优势)
133
+ ### 场景 3:更新登录状态
138
134
 
139
135
  ```bash
140
- claude-coder run --max-sessions 50
141
- # 浏览器 Profile 自动保持:
142
- # - cookies 被网站刷新时,新 cookies 自动写入 Profile
143
- # - 不依赖静态 JSON 文件,状态持续演进
144
- # - 即使中途重启 claude-coder,Profile 中的登录状态仍然有效
136
+ claude-coder auth http://localhost:3000
137
+ # 重新登录,覆盖 playwright-auth.json
145
138
  ```
146
139
 
147
140
  ### 场景 4:清除登录状态
148
141
 
149
142
  ```bash
150
- rm -rf .claude-coder/browser-profile/
151
- # 下次 MCP 会话时将创建全新 Profile,需重新登录
143
+ rm .claude-coder/playwright-auth.json
144
+ # 下次运行 claude-coder auth 重新配置
152
145
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-coder",
3
- "version": "1.5.6",
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(browserProfileDir) {
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(browserProfileDir) {
31
31
 
32
32
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
33
33
 
34
- const relProfileDir = path.relative(getProjectRoot(), browserProfileDir);
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
- `--user-data-dir=${relProfileDir}`,
39
+ '--isolated',
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 (user-data-dir: ${relProfileDir})`);
45
+ log('ok', `.mcp.json 已配置 Playwright MCP (isolated + storage-state: ${relAuthPath})`);
45
46
  }
46
47
 
47
48
  function enableMcpPlaywrightEnv() {
@@ -64,17 +65,14 @@ async function auth(url) {
64
65
  const p = paths();
65
66
  const targetUrl = url || 'http://localhost:3000';
66
67
 
67
- if (!fs.existsSync(p.browserProfile))
68
- fs.mkdirSync(p.browserProfile, { recursive: true });
69
-
70
68
  log('info', '启动 Playwright 浏览器,请手动登录...');
71
69
  log('info', `目标 URL: ${targetUrl}`);
72
- log('info', `浏览器配置将持久化到: ${p.browserProfile}`);
73
70
  console.log('');
74
71
  console.log('操作步骤:');
75
72
  console.log(' 1. 浏览器将自动打开,请手动完成登录');
76
73
  console.log(' 2. 登录成功后关闭浏览器窗口');
77
- console.log(' 3. 登录状态(cookies + localStorage)将保存为快照备份');
74
+ console.log(' 3. 登录状态(cookies + localStorage)将保存到 playwright-auth.json');
75
+ console.log(' 4. MCP 每次会话自动加载此状态(isolated 模式)');
78
76
  console.log('');
79
77
 
80
78
  try {
@@ -95,18 +93,17 @@ async function auth(url) {
95
93
  return;
96
94
  }
97
95
 
98
- log('ok', '登录状态快照已保存(备份参考)');
96
+ log('ok', '登录状态已保存到 playwright-auth.json');
99
97
 
100
- updateMcpConfig(p.browserProfile);
98
+ updateMcpConfig();
101
99
  updateGitignore('.claude-coder/playwright-auth.json');
102
- updateGitignore('.claude-coder/browser-profile/');
103
100
  enableMcpPlaywrightEnv();
104
101
 
105
102
  console.log('');
106
- log('ok', '持久化浏览器配置完成!');
107
- log('info', 'MCP 使用 --user-data-dir 持久化模式,登录状态跨会话保持');
108
- log('info', '首次 MCP 会话时需在浏览器窗口中登录一次,之后永久保持');
109
- 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');
110
107
  }
111
108
 
112
109
  module.exports = { auth };
package/src/config.js CHANGED
@@ -57,13 +57,11 @@ function paths() {
57
57
  testsFile: path.join(loopDir, 'tests.json'),
58
58
  testEnvFile: path.join(loopDir, 'test.env'),
59
59
  playwrightAuth: path.join(loopDir, 'playwright-auth.json'),
60
- browserProfile: path.join(loopDir, 'browser-profile'),
61
60
  mcpConfig: path.join(getProjectRoot(), '.mcp.json'),
62
61
  claudeMd: getTemplatePath('CLAUDE.md'),
63
62
  scanProtocol: getTemplatePath('SCAN_PROTOCOL.md'),
63
+ testRuleTemplate: getTemplatePath('test_rule.md'),
64
64
  runtime,
65
- phaseFile: path.join(runtime, 'phase'),
66
- stepFile: path.join(runtime, 'step'),
67
65
  logsDir: path.join(runtime, 'logs'),
68
66
  };
69
67
  }
@@ -108,6 +106,7 @@ function loadConfig() {
108
106
  defaultHaiku: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '',
109
107
  thinkingBudget: env.ANTHROPIC_THINKING_BUDGET || '',
110
108
  stallTimeout: parseInt(env.SESSION_STALL_TIMEOUT, 10) || 1800,
109
+ editThreshold: parseInt(env.EDIT_THRESHOLD, 10) || 15,
111
110
  raw: env,
112
111
  };
113
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,12 +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 persistent browser profile
101
+ // Hint 6c: Playwright auth state
101
102
  let playwrightAuthHint = '';
102
- if (fs.existsSync(p.browserProfile)) {
103
- playwrightAuthHint = `已检测到持久化浏览器配置(${projectRoot}/.claude-coder/browser-profile/),MCP 使用 --user-data-dir 模式,登录状态跨会话自动保持。首次访问需登录的页面时,用户需在浏览器窗口中手动完成登录。`;
104
- } else if (fs.existsSync(p.playwrightAuth)) {
105
- playwrightAuthHint = `已检测到 Playwright 登录状态快照(${projectRoot}/.claude-coder/playwright-auth.json),建议运行 claude-coder auth 升级到持久化浏览器模式以获得更好的登录保持效果。`;
103
+ if (fs.existsSync(p.playwrightAuth)) {
104
+ playwrightAuthHint = `已检测到 Playwright 登录状态(${projectRoot}/.claude-coder/playwright-auth.json),MCP 使用 --isolated --storage-state 模式,每次会话自动加载 localStorage 和 cookies。`;
106
105
  }
107
106
 
108
107
  // Hint 7: Session memory (read flat session_result.json)
@@ -110,9 +109,9 @@ function buildCodingPrompt(sessionNum, opts = {}) {
110
109
  if (fs.existsSync(p.sessionResult)) {
111
110
  try {
112
111
  const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
113
- if (sr?.task_id) {
114
- memoryHint = `上次会话: ${sr.task_id} → ${sr.status_after || sr.session_result}` +
115
- (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)}` : '') + '。';
116
115
  }
117
116
  } catch { /* ignore */ }
118
117
  }
@@ -200,7 +199,7 @@ function buildScanPrompt(projectType, requirement) {
200
199
  'profile 质量要求(必须遵守,harness 会校验):',
201
200
  '- services 数组必须包含所有可启动服务(command、port、health_check),不得为空',
202
201
  '- existing_docs 必须列出所有实际存在的文档路径',
203
- '- 前后端分离项目必须生成 docs/ARCHITECTURE.md(模块职责、数据流、API 路由),并加入 existing_docs',
202
+ '- 检查 .claude/CLAUDE.md 是否存在,若无则生成(WHAT/WHY/HOW 格式:技术栈、关键决策、开发命令、关键路径、编码规则),并加入 existing_docs',
204
203
  '- scan_files_checked 必须列出所有实际扫描过的文件',
205
204
  '',
206
205
  '步骤 3:根据以下指导分解任务到 tasks.json(格式见 CLAUDE.md):',
@@ -265,6 +264,21 @@ function buildAddPrompt(instruction) {
265
264
  }
266
265
  } catch { /* ignore */ }
267
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
+
268
282
  return [
269
283
  // --- Primacy zone: role + identity ---
270
284
  '你是资深需求分析师,擅长将模糊需求分解为可执行的原子任务。',
@@ -287,12 +301,13 @@ function buildAddPrompt(instruction) {
287
301
  '5. 分解任务:每个任务对应一个独立可测试的功能单元,description 简明(40字内),steps 具体可操作',
288
302
  '6. 追加到 tasks.json,id 和 priority 从已有最大值递增,status: pending',
289
303
  '7. git add -A && git commit -m "chore: add new tasks"',
290
- '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 个任务:简述" })',
291
305
  '',
292
306
 
293
307
  // --- Quality constraints ---
294
308
  taskGuide,
295
309
  '',
310
+ testRuleHint,
296
311
  '不修改已有任务,不实现代码。',
297
312
  '',
298
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,8 +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` | 登录状态快照(备份参考) | 只读;由 `claude-coder auth` 生成 |
55
- | `.claude-coder/browser-profile/` | 持久化浏览器 Profile | MCP 自动维护;首次需手动登录,之后永久保持 |
54
+ | `.claude-coder/playwright-auth.json` | 浏览器登录状态(cookies + localStorage) | 只读;由 `claude-coder auth` 生成,MCP 每次会话自动加载 |
56
55
 
57
56
  ### requirements.md 处理原则
58
57
 
@@ -98,12 +97,9 @@
98
97
  ```json
99
98
  {
100
99
  "session_result": "success | failed",
101
- "task_id": "feat-xxx",
102
100
  "status_before": "pending | failed",
103
101
  "status_after": "done | failed | in_progress | testing",
104
- "git_commit": "abc1234 null",
105
- "tests_passed": true | false,
106
- "notes": "本次会话的简要说明"
102
+ "notes": "本次做了什么 + 遇到的问题 + 给下一个会话的提醒"
107
103
  }
108
104
  ```
109
105
 
@@ -138,38 +134,17 @@
138
134
 
139
135
  ## 任务状态机(严格遵守)
140
136
 
141
- 每个任务在 `tasks.json` 中有一个 `status` 字段,合法状态和迁移规则如下:
137
+ 每个任务在 `tasks.json` 中有一个 `status` 字段,合法迁移路径如下:
142
138
 
143
- ```
144
- pending ──→ in_progress ──→ testing ──→ done
145
-
146
-
147
- failed ──→ in_progress(重试)
148
- ```
149
-
150
- ### 状态说明
151
-
152
- | 状态 | 含义 | 何时设置 |
139
+ | 当前状态 | 可迁移至 | 触发条件 |
153
140
  |---|---|---|
154
- | `pending` | 未开始 | 初始状态 |
155
- | `in_progress` | 正在实现 | 你开始编码时 |
156
- | `testing` | 代码已写完,正在测试 | 代码完成、开始验证时 |
157
- | `done` | 测试通过,功能完成 | 端到端测试通过后 |
158
- | `failed` | 测试失败或实现有问题 | 测试未通过时 |
159
-
160
- ### 迁移规则(铁律)
161
-
162
- - `pending` → `in_progress`:开始工作
163
- - `in_progress` → `testing`:代码写完,开始验证
164
- - `testing` → `done`:所有测试通过
165
- - `testing` → `failed`:测试未通过
166
- - `failed` → `in_progress`:重试修复
167
-
168
- **禁止的迁移**:
169
- - `pending` → `done`(不允许跳步)
170
- - `pending` → `testing`(必须先写代码)
171
- - `in_progress` → `done`(必须先测试)
172
- - 任何状态 → `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`
173
148
 
174
149
  ---
175
150
 
@@ -248,18 +223,15 @@ pending ──→ in_progress ──→ testing ──→ done
248
223
  1. **后台服务管理**:根据 prompt 提示决定——单次模式(`--max 1`)时停止所有后台服务(`lsof -ti :端口 | xargs kill`);连续模式时保持服务运行,下个 session 继续使用
249
224
  2. **按需更新文档和 profile**:
250
225
  - **README / 用户文档**:仅当对外行为变化(新增功能、API 变更、使用方式变化)时更新
251
- - **架构 / API 文档**:如果本次新增了模块、改变了模块职责或新增了 API 端点,更新 `existing_docs` 中对应的架构或 API 文档。同时更新 `project_profile.json` 的 `existing_docs` 列表(若新增了文档文件)
226
+ - **项目指令文件**:如果本次新增了模块、改变了模块职责或新增了 API 端点,更新 `.claude/CLAUDE.md`。同时确保 `project_profile.json` 的 `existing_docs` 列表包含此文件
252
227
  - **profile 补全**:如果 prompt 中提示 `project_profile.json` 有缺陷(如 services 为空、existing_docs 为空),在此步骤补全。Harness 依赖 profile 做环境初始化和上下文注入
253
228
  3. **Git 提交**:`git add -A && git commit -m "feat(task-id): 功能描述"`
254
229
  4. **写入 session_result.json**(notes 要充分记录上下文供下次恢复):
255
230
  ```json
256
231
  {
257
232
  "session_result": "success 或 failed",
258
- "task_id": "当前任务 ID",
259
233
  "status_before": "任务开始时的状态",
260
234
  "status_after": "任务结束时的状态",
261
- "git_commit": "本次提交的 hash",
262
- "tests_passed": true 或 false,
263
235
  "notes": "本次做了什么 + 遇到的问题 + 给下一个会话的提醒"
264
236
  }
265
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
- 这个优先级排序体现了一个关键洞察:**越靠近行为发生的时刻,指导的遵循率越高**。