claude-coder 1.5.6 → 1.6.2

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/src/auth.js CHANGED
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const path = require('path');
5
6
  const { execSync } = require('child_process');
6
- const { paths, log, getProjectRoot, ensureLoopDir } = require('./config');
7
+ const { paths, loadConfig, log, getProjectRoot, ensureLoopDir } = require('./config');
7
8
 
8
9
  function updateGitignore(entry) {
9
10
  const gitignorePath = path.join(getProjectRoot(), '.gitignore');
@@ -18,30 +19,35 @@ function updateGitignore(entry) {
18
19
  log('ok', `.gitignore 已添加: ${entry}`);
19
20
  }
20
21
 
21
- function updateMcpConfig(browserProfileDir) {
22
- const p = paths();
22
+ function updateMcpConfig(p, mode) {
23
23
  let mcpConfig = {};
24
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
- }
25
+ try { mcpConfig = JSON.parse(fs.readFileSync(p.mcpConfig, 'utf8')); } catch {}
30
26
  }
31
27
 
32
28
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
33
29
 
34
- const relProfileDir = path.relative(getProjectRoot(), browserProfileDir);
35
- mcpConfig.mcpServers.playwright = {
36
- command: 'npx',
37
- args: [
38
- '@playwright/mcp@latest',
39
- `--user-data-dir=${relProfileDir}`,
40
- ],
41
- };
30
+ const args = ['@playwright/mcp@latest'];
42
31
 
32
+ switch (mode) {
33
+ case 'persistent': {
34
+ const relProfile = path.relative(getProjectRoot(), p.browserProfile).split(path.sep).join('/');
35
+ args.push(`--user-data-dir=${relProfile}`);
36
+ break;
37
+ }
38
+ case 'isolated': {
39
+ const relAuth = path.relative(getProjectRoot(), p.playwrightAuth).split(path.sep).join('/');
40
+ args.push('--isolated', `--storage-state=${relAuth}`);
41
+ break;
42
+ }
43
+ case 'extension':
44
+ args.push('--extension');
45
+ break;
46
+ }
47
+
48
+ mcpConfig.mcpServers.playwright = { command: 'npx', args };
43
49
  fs.writeFileSync(p.mcpConfig, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf8');
44
- log('ok', `.mcp.json 已配置 Playwright MCP (user-data-dir: ${relProfileDir})`);
50
+ log('ok', `.mcp.json 已配置 Playwright MCP (${mode} 模式)`);
45
51
  }
46
52
 
47
53
  function enableMcpPlaywrightEnv() {
@@ -59,33 +65,108 @@ function enableMcpPlaywrightEnv() {
59
65
  log('ok', '.claude-coder/.env 已设置 MCP_PLAYWRIGHT=true');
60
66
  }
61
67
 
62
- async function auth(url) {
63
- ensureLoopDir();
64
- const p = paths();
65
- const targetUrl = url || 'http://localhost:3000';
68
+ // ── persistent 模式:启动持久化浏览器让用户登录 ──
66
69
 
67
- if (!fs.existsSync(p.browserProfile))
68
- fs.mkdirSync(p.browserProfile, { recursive: true });
70
+ async function authPersistent(url, p) {
71
+ const profileDir = p.browserProfile;
72
+ if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
69
73
 
70
- log('info', '启动 Playwright 浏览器,请手动登录...');
71
- log('info', `目标 URL: ${targetUrl}`);
72
- log('info', `浏览器配置将持久化到: ${p.browserProfile}`);
74
+ const lockFile = path.join(profileDir, 'SingletonLock');
75
+ if (fs.existsSync(lockFile)) {
76
+ fs.unlinkSync(lockFile);
77
+ log('warn', '已清理残留的 SingletonLock(上次浏览器未正常关闭)');
78
+ }
79
+
80
+ console.log('操作步骤:');
81
+ console.log(' 1. 浏览器将自动打开,请手动完成登录');
82
+ console.log(' 2. 登录成功后关闭浏览器窗口');
83
+ console.log(' 3. 登录状态将保存在持久化配置中');
84
+ console.log(' 4. MCP 后续会话自动复用此登录状态');
73
85
  console.log('');
86
+
87
+ const scriptContent = [
88
+ `let chromium;`,
89
+ `try { chromium = require('playwright').chromium; } catch {`,
90
+ ` try { chromium = require('@playwright/test').chromium; } catch {`,
91
+ ` console.error('错误: 未找到 playwright 模块');`,
92
+ ` console.error('请安装: npx playwright install chromium');`,
93
+ ` process.exit(1);`,
94
+ ` }`,
95
+ `}`,
96
+ `(async () => {`,
97
+ ` const ctx = await chromium.launchPersistentContext(${JSON.stringify(profileDir)}, { headless: false });`,
98
+ ` const page = ctx.pages()[0] || await ctx.newPage();`,
99
+ ` try { await page.goto(${JSON.stringify(url)}); } catch {}`,
100
+ ` console.log('请在浏览器中完成登录后关闭窗口...');`,
101
+ ` await new Promise(r => {`,
102
+ ` ctx.on('close', r);`,
103
+ ` const t = setInterval(() => { try { if (!ctx.pages().length) { clearInterval(t); r(); } } catch { clearInterval(t); r(); } }, 2000);`,
104
+ ` });`,
105
+ ` try { await ctx.close(); } catch {}`,
106
+ `})().then(() => process.exit(0)).catch(() => process.exit(0));`,
107
+ ].join('\n');
108
+
109
+ const tmpScript = path.join(os.tmpdir(), `pw-auth-${Date.now()}.js`);
110
+ fs.writeFileSync(tmpScript, scriptContent);
111
+
112
+ const helperModules = path.join(__dirname, '..', 'node_modules');
113
+ const existingNodePath = process.env.NODE_PATH || '';
114
+ const nodePath = existingNodePath ? `${helperModules}:${existingNodePath}` : helperModules;
115
+
116
+ let scriptOk = false;
117
+ try {
118
+ execSync(`node "${tmpScript}"`, {
119
+ stdio: 'inherit',
120
+ cwd: getProjectRoot(),
121
+ env: { ...process.env, NODE_PATH: nodePath },
122
+ });
123
+ scriptOk = true;
124
+ } catch {
125
+ // 浏览器关闭时可能返回非零退出码,只要 profile 目录有内容就认为成功
126
+ const profileFiles = fs.readdirSync(profileDir);
127
+ scriptOk = profileFiles.length > 2;
128
+ if (!scriptOk) {
129
+ log('error', 'Playwright 启动失败,且未检测到有效的浏览器配置');
130
+ log('info', '请确保已安装 Chromium: npx playwright install chromium');
131
+ try { fs.unlinkSync(tmpScript); } catch {}
132
+ return;
133
+ }
134
+ log('warn', '浏览器退出码非零,但已检测到有效配置,继续...');
135
+ }
136
+
137
+ try { fs.unlinkSync(tmpScript); } catch {}
138
+
139
+ log('ok', '登录状态已保存到持久化配置');
140
+ updateMcpConfig(p, 'persistent');
141
+ updateGitignore('.claude-coder/.runtime/browser-profile');
142
+ enableMcpPlaywrightEnv();
143
+
144
+ console.log('');
145
+ log('ok', '配置完成!');
146
+ const relProfile = path.relative(getProjectRoot(), profileDir);
147
+ log('info', `MCP 使用 persistent 模式 (user-data-dir: ${relProfile})`);
148
+ log('info', '如需更新登录状态,重新运行 claude-coder auth');
149
+ }
150
+
151
+ // ── isolated 模式:使用 codegen 录制 storage-state ──
152
+
153
+ async function authIsolated(url, p) {
74
154
  console.log('操作步骤:');
75
155
  console.log(' 1. 浏览器将自动打开,请手动完成登录');
76
156
  console.log(' 2. 登录成功后关闭浏览器窗口');
77
- console.log(' 3. 登录状态(cookies + localStorage)将保存为快照备份');
157
+ console.log(' 3. 登录状态(cookies + localStorage)将保存到 playwright-auth.json');
158
+ console.log(' 4. MCP 每次会话自动从此文件加载初始状态');
78
159
  console.log('');
79
160
 
80
161
  try {
81
162
  execSync(
82
- `npx playwright codegen --save-storage="${p.playwrightAuth}" "${targetUrl}"`,
163
+ `npx playwright codegen --save-storage="${p.playwrightAuth}" "${url}"`,
83
164
  { stdio: 'inherit', cwd: getProjectRoot() }
84
165
  );
85
166
  } catch (err) {
86
167
  if (!fs.existsSync(p.playwrightAuth)) {
87
168
  log('error', `Playwright 登录状态导出失败: ${err.message}`);
88
- log('info', '请确保已安装 playwright: npx playwright install');
169
+ log('info', '请确保已安装: npx playwright install chromium');
89
170
  return;
90
171
  }
91
172
  }
@@ -95,18 +176,70 @@ async function auth(url) {
95
176
  return;
96
177
  }
97
178
 
98
- log('ok', '登录状态快照已保存(备份参考)');
99
-
100
- updateMcpConfig(p.browserProfile);
179
+ log('ok', '登录状态已保存到 playwright-auth.json');
180
+ updateMcpConfig(p, 'isolated');
101
181
  updateGitignore('.claude-coder/playwright-auth.json');
102
- updateGitignore('.claude-coder/browser-profile/');
103
182
  enableMcpPlaywrightEnv();
104
183
 
105
184
  console.log('');
106
- log('ok', '持久化浏览器配置完成!');
107
- log('info', 'MCP 使用 --user-data-dir 持久化模式,登录状态跨会话保持');
108
- log('info', '首次 MCP 会话时需在浏览器窗口中登录一次,之后永久保持');
109
- log('info', 'cookies 自动续期,无需手动重新运行 claude-coder auth');
185
+ log('ok', '配置完成!');
186
+ log('info', 'MCP 使用 isolated 模式 (storage-state)');
187
+ log('info', 'cookies localStorage 每次会话自动从 playwright-auth.json 加载');
188
+ log('info', '如需更新登录状态,重新运行 claude-coder auth');
189
+ }
190
+
191
+ // ── extension 模式:连接真实浏览器 ──
192
+
193
+ function authExtension(p) {
194
+ console.log('Extension 模式说明:');
195
+ console.log('');
196
+ console.log(' 此模式通过 Chrome 扩展连接到您正在运行的浏览器。');
197
+ console.log(' MCP 将直接使用浏览器中已有的登录态和扩展。');
198
+ console.log('');
199
+ console.log(' 前置条件:');
200
+ console.log(' 1. 安装 "Playwright MCP Bridge" Chrome/Edge 扩展');
201
+ console.log(' https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm');
202
+ console.log(' 2. 确保浏览器已启动且扩展已启用');
203
+ console.log(' 3. 无需额外认证操作,您的浏览器登录态将自动可用');
204
+ console.log('');
205
+
206
+ updateMcpConfig(p, 'extension');
207
+ enableMcpPlaywrightEnv();
208
+
209
+ console.log('');
210
+ log('ok', '配置完成!');
211
+ log('info', 'MCP 使用 extension 模式(连接真实浏览器)');
212
+ log('info', '确保 Chrome/Edge 已运行且 Playwright MCP Bridge 扩展已启用');
213
+ }
214
+
215
+ // ── 主入口 ──
216
+
217
+ async function auth(url) {
218
+ ensureLoopDir();
219
+ const config = loadConfig();
220
+ const p = paths();
221
+ const mode = config.playwrightMode;
222
+ const targetUrl = url || 'http://localhost:3000';
223
+
224
+ log('info', `Playwright 模式: ${mode}`);
225
+ log('info', `目标 URL: ${targetUrl}`);
226
+ console.log('');
227
+
228
+ switch (mode) {
229
+ case 'persistent':
230
+ await authPersistent(targetUrl, p);
231
+ break;
232
+ case 'isolated':
233
+ await authIsolated(targetUrl, p);
234
+ break;
235
+ case 'extension':
236
+ authExtension(p);
237
+ break;
238
+ default:
239
+ log('error', `未知的 Playwright 模式: ${mode}`);
240
+ log('info', '请运行 claude-coder setup 重新配置');
241
+ return;
242
+ }
110
243
  }
111
244
 
112
- module.exports = { auth };
245
+ module.exports = { auth, updateMcpConfig };
package/src/config.js CHANGED
@@ -57,13 +57,12 @@ 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'),
60
+ browserProfile: path.join(runtime, 'browser-profile'),
61
61
  mcpConfig: path.join(getProjectRoot(), '.mcp.json'),
62
62
  claudeMd: getTemplatePath('CLAUDE.md'),
63
63
  scanProtocol: getTemplatePath('SCAN_PROTOCOL.md'),
64
+ testRuleTemplate: getTemplatePath('test_rule.md'),
64
65
  runtime,
65
- phaseFile: path.join(runtime, 'phase'),
66
- stepFile: path.join(runtime, 'step'),
67
66
  logsDir: path.join(runtime, 'logs'),
68
67
  };
69
68
  }
@@ -99,7 +98,7 @@ function loadConfig() {
99
98
  timeoutMs: parseInt(env.API_TIMEOUT_MS, 10) || 3000000,
100
99
  mcpToolTimeout: parseInt(env.MCP_TOOL_TIMEOUT, 10) || 30000,
101
100
  mcpPlaywright: env.MCP_PLAYWRIGHT === 'true',
102
- debug: env.CLAUDE_DEBUG || '',
101
+ playwrightMode: env.MCP_PLAYWRIGHT_MODE || 'persistent',
103
102
  disableNonessential: env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC || '',
104
103
  effortLevel: env.CLAUDE_CODE_EFFORT_LEVEL || '',
105
104
  smallFastModel: env.ANTHROPIC_SMALL_FAST_MODEL || '',
@@ -108,6 +107,7 @@ function loadConfig() {
108
107
  defaultHaiku: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '',
109
108
  thinkingBudget: env.ANTHROPIC_THINKING_BUDGET || '',
110
109
  stallTimeout: parseInt(env.SESSION_STALL_TIMEOUT, 10) || 1800,
110
+ editThreshold: parseInt(env.EDIT_THRESHOLD, 10) || 15,
111
111
  raw: env,
112
112
  };
113
113
 
@@ -191,6 +191,21 @@ function syncToGlobal() {
191
191
  log('ok', `已同步配置到 ${settingsPath}`);
192
192
  }
193
193
 
194
+ function updateEnvVar(key, value) {
195
+ const p = paths();
196
+ if (!fs.existsSync(p.envFile)) return false;
197
+ let content = fs.readFileSync(p.envFile, 'utf8');
198
+ const regex = new RegExp(`^${key}=.*$`, 'm');
199
+ if (regex.test(content)) {
200
+ content = content.replace(regex, `${key}=${value}`);
201
+ } else {
202
+ const suffix = content.endsWith('\n') ? '' : '\n';
203
+ content += `${suffix}${key}=${value}\n`;
204
+ }
205
+ fs.writeFileSync(p.envFile, content, 'utf8');
206
+ return true;
207
+ }
208
+
194
209
  module.exports = {
195
210
  COLOR,
196
211
  log,
@@ -204,4 +219,5 @@ module.exports = {
204
219
  buildEnvVars,
205
220
  getAllowedTools,
206
221
  syncToGlobal,
222
+ updateEnvVar,
207
223
  };
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]} 次,疑似死循环。请重新审视方案后再继续。`,
@@ -73,7 +74,16 @@ function createSessionHooks(indicator, logStream, options = {}) {
73
74
 
74
75
  return {};
75
76
  }]
76
- }]
77
+ }],
78
+ PostToolUse: [{
79
+ matcher: '*',
80
+ hooks: [async () => {
81
+ indicator.updatePhase('thinking');
82
+ indicator.updateStep('');
83
+ indicator.toolTarget = '';
84
+ return {};
85
+ }]
86
+ }],
77
87
  };
78
88
 
79
89
  return {
package/src/indicator.js CHANGED
@@ -1,9 +1,8 @@
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
- const SPINNERS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
+ const SPINNERS = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
7
6
 
8
7
  class Indicator {
9
8
  constructor() {
@@ -16,15 +15,14 @@ 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;
18
+ this.stallTimeoutMin = 30;
21
19
  }
22
20
 
23
- start(sessionNum) {
21
+ start(sessionNum, stallTimeoutMin) {
24
22
  this.sessionNum = sessionNum;
25
23
  this.startTime = Date.now();
26
- this._lastRenderTime = Date.now();
27
- this.timer = setInterval(() => this._render(), 500);
24
+ if (stallTimeoutMin > 0) this.stallTimeoutMin = stallTimeoutMin;
25
+ this.timer = setInterval(() => this._render(), 1000);
28
26
  }
29
27
 
30
28
  stop() {
@@ -37,26 +35,16 @@ class Indicator {
37
35
 
38
36
  updatePhase(phase) {
39
37
  this.phase = phase;
40
- this._writePhaseFile();
41
38
  }
42
39
 
43
40
  updateStep(step) {
44
41
  this.step = step;
45
- this._writeStepFile();
46
42
  }
47
43
 
48
44
  appendActivity(toolName, summary) {
49
45
  this.lastActivity = `${toolName}: ${summary}`;
50
46
  }
51
47
 
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
48
  getStatusLine() {
61
49
  const now = new Date();
62
50
  const hh = String(now.getHours()).padStart(2, '0');
@@ -78,32 +66,26 @@ class Indicator {
78
66
 
79
67
  let line = `${spinner} [Session ${this.sessionNum}] ${clock} ${phaseLabel} ${mm}:${ss}`;
80
68
  if (idleMin >= 2) {
81
- line += ` | ${COLOR.red}${idleMin}分无工具调用${COLOR.reset}`;
69
+ line += ` | ${COLOR.red}${idleMin}分无工具调用(等待模型响应, ${this.stallTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
82
70
  }
83
71
  if (this.step) {
84
72
  line += ` | ${this.step}`;
85
- if (this.toolTarget) line += `: ${this.toolTarget}`;
73
+ if (this.toolTarget) {
74
+ const cols = process.stderr.columns || 80;
75
+ const usedWidth = line.replace(/\x1b\[[^m]*m/g, '').length;
76
+ const availWidth = Math.max(15, cols - usedWidth - 4);
77
+ const target = this.toolTarget.length > availWidth
78
+ ? '…' + this.toolTarget.slice(-(availWidth - 1))
79
+ : this.toolTarget;
80
+ line += `: ${target}`;
81
+ }
86
82
  }
87
83
  return line;
88
84
  }
89
85
 
90
86
  _render() {
91
87
  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
- 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}`);
88
+ process.stderr.write(`\r\x1b[K${this.getStatusLine()}`);
107
89
  }
108
90
  }
109
91
 
@@ -112,7 +94,7 @@ function extractFileTarget(toolInput) {
112
94
  ? (toolInput.file_path || toolInput.path || '')
113
95
  : '';
114
96
  if (!raw) return '';
115
- return raw.split('/').slice(-2).join('/').slice(0, 40);
97
+ return raw.split('/').slice(-2).join('/');
116
98
  }
117
99
 
118
100
  function extractBashLabel(cmd) {
@@ -126,7 +108,7 @@ function extractBashLabel(cmd) {
126
108
  function extractBashTarget(cmd) {
127
109
  let clean = cmd.replace(/^(?:cd\s+\S+\s*&&\s*)+/g, '').trim();
128
110
  clean = clean.split(/\s*(?:\|{1,2}|;|&&|2>&1|>\s*\/dev\/null)\s*/)[0].trim();
129
- return clean.slice(0, 40);
111
+ return clean;
130
112
  }
131
113
 
132
114
  function inferPhaseStep(indicator, toolName, toolInput) {
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,23 @@ 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 mode awareness
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 (config.mcpPlaywright) {
104
+ const mode = config.playwrightMode;
105
+ switch (mode) {
106
+ case 'persistent':
107
+ playwrightAuthHint = 'Playwright MCP 使用 persistent 模式(user-data-dir),浏览器登录状态持久保存在本地配置中,无需额外登录操作。';
108
+ break;
109
+ case 'isolated':
110
+ playwrightAuthHint = fs.existsSync(p.playwrightAuth)
111
+ ? `Playwright MCP 使用 isolated 模式,已检测到登录状态文件(playwright-auth.json),每次会话自动加载 cookies 和 localStorage。`
112
+ : 'Playwright MCP 使用 isolated 模式,但未检测到登录状态文件。如目标页面需要登录,请先运行 claude-coder auth <URL>。';
113
+ break;
114
+ case 'extension':
115
+ playwrightAuthHint = 'Playwright MCP 使用 extension 模式,已连接用户真实浏览器,直接复用浏览器已有的登录态和扩展。注意:操作会影响用户正在使用的浏览器。';
116
+ break;
117
+ }
106
118
  }
107
119
 
108
120
  // Hint 7: Session memory (read flat session_result.json)
@@ -110,9 +122,9 @@ function buildCodingPrompt(sessionNum, opts = {}) {
110
122
  if (fs.existsSync(p.sessionResult)) {
111
123
  try {
112
124
  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)}` : '') + '。';
125
+ if (sr?.session_result) {
126
+ memoryHint = `上次会话: ${sr.session_result}(${sr.status_before || '?'} → ${sr.status_after || '?'})` +
127
+ (sr.notes ? `, 要点: ${sr.notes.slice(0, 150)}` : '') + '。';
116
128
  }
117
129
  } catch { /* ignore */ }
118
130
  }
@@ -200,7 +212,7 @@ function buildScanPrompt(projectType, requirement) {
200
212
  'profile 质量要求(必须遵守,harness 会校验):',
201
213
  '- services 数组必须包含所有可启动服务(command、port、health_check),不得为空',
202
214
  '- existing_docs 必须列出所有实际存在的文档路径',
203
- '- 前后端分离项目必须生成 docs/ARCHITECTURE.md(模块职责、数据流、API 路由),并加入 existing_docs',
215
+ '- 检查 .claude/CLAUDE.md 是否存在,若无则生成(WHAT/WHY/HOW 格式:技术栈、关键决策、开发命令、关键路径、编码规则),并加入 existing_docs',
204
216
  '- scan_files_checked 必须列出所有实际扫描过的文件',
205
217
  '',
206
218
  '步骤 3:根据以下指导分解任务到 tasks.json(格式见 CLAUDE.md):',
@@ -265,6 +277,21 @@ function buildAddPrompt(instruction) {
265
277
  }
266
278
  } catch { /* ignore */ }
267
279
 
280
+ // --- Conditional: Playwright test rule hint ---
281
+ let testRuleHint = '';
282
+ const testRulePath = path.join(p.loopDir, 'test_rule.md');
283
+ const hasMcp = fs.existsSync(p.mcpConfig);
284
+ if (fs.existsSync(testRulePath) && hasMcp) {
285
+ testRuleHint = [
286
+ '【Playwright 测试规则】项目已配置 Playwright MCP(.mcp.json),' +
287
+ '`.claude-coder/test_rule.md` 中包含通用测试指导规则(Smart Snapshot、Token 预算控制、三步测试方法论、等待策略等)。',
288
+ '当任务涉及端到端测试时:',
289
+ ' - 在 steps 中第一步加入「阅读 .claude-coder/test_rule.md 了解测试规范和成本控制」',
290
+ ' - 测试步骤按 test_rule.md 中的 tasks.json 模板格式编写(含环境检查、优先级标注、预算控制)',
291
+ ' - 设定合理的 test_tier(unit/smoke/regression/full_e2e)',
292
+ ].join('\n');
293
+ }
294
+
268
295
  return [
269
296
  // --- Primacy zone: role + identity ---
270
297
  '你是资深需求分析师,擅长将模糊需求分解为可执行的原子任务。',
@@ -287,12 +314,13 @@ function buildAddPrompt(instruction) {
287
314
  '5. 分解任务:每个任务对应一个独立可测试的功能单元,description 简明(40字内),steps 具体可操作',
288
315
  '6. 追加到 tasks.json,id 和 priority 从已有最大值递增,status: pending',
289
316
  '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 个任务:简述" })',
317
+ '8. 写入 session_result.json(格式:{ "session_result": "success", "status_before": "N/A", "status_after": "N/A", "notes": "追加了 N 个任务:简述" })',
291
318
  '',
292
319
 
293
320
  // --- Quality constraints ---
294
321
  taskGuide,
295
322
  '',
323
+ testRuleHint,
296
324
  '不修改已有任务,不实现代码。',
297
325
  '',
298
326
 
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,9 +132,11 @@ 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
- indicator.start(sessionNum);
138
+ const stallTimeoutMin = Math.floor(stallTimeoutMs / 60000);
139
+ indicator.start(sessionNum, stallTimeoutMin);
138
140
 
139
141
  try {
140
142
  const queryOpts = buildQueryOptions(config, opts);
@@ -202,7 +204,7 @@ async function runScanSession(requirement, opts = {}) {
202
204
  stallTimeoutMs,
203
205
  });
204
206
 
205
- indicator.start(0);
207
+ indicator.start(0, Math.floor(stallTimeoutMs / 60000));
206
208
  log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
207
209
 
208
210
  try {
@@ -263,7 +265,7 @@ async function runAddSession(instruction, opts = {}) {
263
265
  stallTimeoutMs,
264
266
  });
265
267
 
266
- indicator.start(0);
268
+ indicator.start(0, Math.floor(stallTimeoutMs / 60000));
267
269
  log('info', '正在追加任务...');
268
270
 
269
271
  try {