claude-coder 1.0.9 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,15 +50,17 @@ claude-coder run "实现用户注册和登录功能"
50
50
  |------|------|
51
51
  | `claude-coder setup` | 交互式模型配置 |
52
52
  | `claude-coder run [需求]` | 自动编码循环 |
53
+ | `claude-coder run --max 1` | 单次执行 |
53
54
  | `claude-coder run --dry-run` | 预览模式 |
54
55
  | `claude-coder init` | 初始化项目环境 |
55
- | `claude-coder view [需求]` | 观测模式(交互式单次) |
56
- | `claude-coder add "指令"` | 追加任务 |
56
+ | `claude-coder add "指令"` | 追加任务(默认用 opus 级模型推理) |
57
+ | `claude-coder add -r [file]` | 从需求文件追加任务 |
58
+ | `claude-coder add "..." --model M` | 指定模型追加任务 |
57
59
  | `claude-coder validate` | 手动校验 |
58
60
  | `claude-coder status` | 查看进度和成本 |
59
61
  | `claude-coder config sync` | 同步配置到 ~/.claude/ |
60
62
 
61
- **选项**:`--max N` 限制 session 数(默认 50),`--pause N` 每 N 个 session 暂停(默认 5)。
63
+ **选项**:`--max N` 限制 session 数(默认 50),`--pause N` 每 N 个 session 暂停确认(默认不暂停)。
62
64
 
63
65
  ## 使用场景
64
66
 
@@ -66,9 +68,9 @@ claude-coder run "实现用户注册和登录功能"
66
68
 
67
69
  **已有项目**:`claude-coder run "新增头像上传功能"` — 先扫描现有代码和技术栈,再增量开发。
68
70
 
69
- **需求文档驱动**:在项目根目录创建 `requirements.md`(可从模板复制),运行 `claude-coder run` — 修改需求后再次运行,自动同步新任务。
71
+ **需求文档驱动**:在项目根目录创建 `requirements.md`,运行 `claude-coder run` — 需求变更后用 `claude-coder add -r` 同步新任务。
70
72
 
71
- **追加任务**:`claude-coder add "新增管理员后台"` — 仅追加到任务列表,下次 run 时执行。
73
+ **追加任务**:`claude-coder add "新增管理员后台"` 或 `claude-coder add -r requirements.md` — 仅追加到任务列表,下次 run 时执行。
72
74
 
73
75
  ## 模型支持
74
76
 
@@ -88,10 +90,11 @@ your-project/
88
90
  .env # 模型配置
89
91
  project_profile.json # 项目扫描结果
90
92
  tasks.json # 任务列表 + 状态
91
- session_result.json # session 结果 + 历史
92
- progress.json # 会话日志 + 成本
93
+ session_result.json # 上次 session 结果(扁平)
94
+ progress.json # 会话历史 + 成本
93
95
  tests.json # 验证记录
94
- .runtime/ # 临时文件
96
+ test.env # 测试凭证(API Key 等,可选)
97
+ .runtime/ # 临时文件(含日志)
95
98
  requirements.md # 需求文档(可选)
96
99
  ```
97
100
 
@@ -108,6 +111,7 @@ your-project/
108
111
  ## 文档
109
112
 
110
113
  - [技术架构](docs/ARCHITECTURE.md) — 模块职责、提示语注入架构、注意力机制、Hook 数据流、后续优化方向
114
+ - [Playwright 凭证持久化](docs/PLAYWRIGHT_CREDENTIALS.md) — 测试 cookies 和 API Key 管理方案
111
115
 
112
116
  ## License
113
117
 
package/bin/cli.js CHANGED
@@ -7,7 +7,7 @@ const COMMANDS = {
7
7
  run: { desc: '自动编码循环', usage: 'claude-coder run [需求] [--max N] [--pause N] [--dry-run]' },
8
8
  setup: { desc: '交互式模型配置', usage: 'claude-coder setup' },
9
9
  init: { desc: '初始化项目环境', usage: 'claude-coder init' },
10
- add: { desc: '追加任务到 tasks.json', usage: 'claude-coder add "指令"' },
10
+ add: { desc: '追加任务到 tasks.json', usage: 'claude-coder add "指令" [--model M] | add -r [file]' },
11
11
  validate: { desc: '手动校验上次 session', usage: 'claude-coder validate' },
12
12
  status: { desc: '查看任务进度和成本', usage: 'claude-coder status' },
13
13
  config: { desc: '配置管理', usage: 'claude-coder config sync' },
@@ -23,8 +23,12 @@ function showHelp() {
23
23
  console.log('\n示例:');
24
24
  console.log(' claude-coder setup 配置模型和 API Key');
25
25
  console.log(' claude-coder run "实现用户登录" 开始自动编码');
26
- console.log(' claude-coder run --max 1 单次执行(替代旧 view 模式)');
27
- console.log(' claude-coder run --max 5 --dry-run 预览模式');
26
+ console.log(' claude-coder run --max 1 单次执行');
27
+ console.log(' claude-coder run --max 5 --pause 5 每 5 个 session 暂停确认');
28
+ console.log(' claude-coder run --dry-run 预览模式');
29
+ console.log(' claude-coder add "新增搜索功能" 追加任务');
30
+ console.log(' claude-coder add -r 从 requirements.md 追加任务');
31
+ console.log(' claude-coder add "..." --model opus-4 指定模型追加任务');
28
32
  console.log(' claude-coder status 查看进度和成本');
29
33
  console.log(`\n前置条件: npm install -g @anthropic-ai/claude-agent-sdk`);
30
34
  }
@@ -32,7 +36,7 @@ function showHelp() {
32
36
  function parseArgs(argv) {
33
37
  const args = argv.slice(2);
34
38
  const command = args[0];
35
- const opts = { max: 50, pause: 5, dryRun: false };
39
+ const opts = { max: 50, pause: 0, dryRun: false, readFile: null, model: null };
36
40
  const positional = [];
37
41
 
38
42
  for (let i = 1; i < args.length; i++) {
@@ -46,6 +50,19 @@ function parseArgs(argv) {
46
50
  case '--dry-run':
47
51
  opts.dryRun = true;
48
52
  break;
53
+ case '--model':
54
+ opts.model = args[++i] || null;
55
+ break;
56
+ case '-r': {
57
+ const next = args[i + 1];
58
+ if (next && !next.startsWith('-')) {
59
+ opts.readFile = next;
60
+ i++;
61
+ } else {
62
+ opts.readFile = 'requirements.md';
63
+ }
64
+ break;
65
+ }
49
66
  case '--help':
50
67
  case '-h':
51
68
  showHelp();
@@ -92,12 +109,22 @@ async function main() {
92
109
  break;
93
110
  }
94
111
  case 'add': {
95
- if (!positional[0]) {
96
- console.error('用法: claude-coder add "任务描述"');
112
+ let instruction = positional[0] || '';
113
+ if (opts.readFile) {
114
+ const reqPath = require('path').resolve(opts.readFile);
115
+ if (!require('fs').existsSync(reqPath)) {
116
+ console.error(`文件不存在: ${reqPath}`);
117
+ process.exit(1);
118
+ }
119
+ instruction = require('fs').readFileSync(reqPath, 'utf8');
120
+ console.log(`已读取需求文件: ${opts.readFile}`);
121
+ }
122
+ if (!instruction) {
123
+ console.error('用法: claude-coder add "任务描述" 或 claude-coder add -r [requirements.md]');
97
124
  process.exit(1);
98
125
  }
99
126
  const runner = require('../src/runner');
100
- await runner.add(positional[0], opts);
127
+ await runner.add(instruction, opts);
101
128
  break;
102
129
  }
103
130
  case 'validate': {
@@ -191,16 +191,16 @@ flowchart TB
191
191
 
192
192
  | # | Hint | 触发条件 | 影响 |
193
193
  |---|---|---|---|
194
- | 1 | `reqSyncHint` | 需求 hash 变化 | Step 1:追加新任务 |
195
- | 2 | `mcpHint` | MCP_PLAYWRIGHT=true | Step 5:可用 Playwright |
196
- | 3 | `testHint` | tests.json 有记录 | Step 5:避免重复验证 |
197
- | 4 | `docsHint` | profile.existing_docs 非空或 profile 有缺陷 | Step 4:读文档后再编码;profile 缺陷时提示 Agent 在 Step 6 补全 services/docs |
198
- | 5 | `envHint` | 连续成功且 session>1 | Step 2:跳过 init |
199
- | 6 | `retryContext` | 上次校验失败 | 全局:避免同样错误 |
200
- | 7 | `taskHint` | tasks.json 存在且有待办任务 | Step 1:跳过读取 tasks.json,harness 已注入当前任务上下文 + .claude-coder/ 路径提示 |
201
- | 8 | `memoryHint` | session_result.json 存在且有历史记录 | Step 1:跳过读取 session_result.json,harness 已注入上次会话摘要 |
202
- | 9 | `serviceHint` | 始终注入 | Step 6:单次模式停止服务,连续模式保持服务运行 |
203
- | 10 | `toolGuidance` | 始终注入 | 全局:工具使用规范(Grep/Glob/Read/LS/MultiEdit/Task 替代 bash 命令),非 Claude 模型必需 |
194
+ | 1 | `mcpHint` | MCP_PLAYWRIGHT=true | Step 5:可用 Playwright |
195
+ | 2 | `retryContext` | 上次校验失败 | 全局:避免同样错误 |
196
+ | 3 | `envHint` | 连续成功且 session>1 | Step 2:跳过 init |
197
+ | 4 | `testHint` | tests.json 有记录 | Step 5:避免重复验证 |
198
+ | 5 | `docsHint` | profile.existing_docs 非空或 profile 有缺陷 | Step 4:读文档后再编码;profile 缺陷时提示 Agent 在 Step 6 补全 services/docs |
199
+ | 6 | `taskHint` | tasks.json 存在且有待办任务 | Step 1:跳过读取 tasks.json,harness 已注入当前任务上下文 + 项目绝对路径 |
200
+ | 6b | `testEnvHint` | .claude-coder/test.env 存在 | Step 5:提示 Agent 在测试前加载测试环境变量 |
201
+ | 7 | `memoryHint` | session_result.json 存在(扁平格式) | Step 1:跳过读取 session_result.json,harness 已注入上次会话摘要 |
202
+ | 8 | `serviceHint` | 始终注入 | Step 6:单次模式停止服务,连续模式保持服务运行 |
203
+ | 9 | `toolGuidance` | 始终注入 | 全局:工具使用规范(Grep/Glob/Read/LS/MultiEdit/Task 替代 bash 命令),非 Claude 模型必需 |
204
204
 
205
205
  ---
206
206
 
@@ -268,7 +268,7 @@ sequenceDiagram
268
268
  | 维度 | 评分 | 说明 |
269
269
  |------|------|------|
270
270
  | **CLAUDE.md 系统提示** | 8/10 | U 型注意力设计;铁律清晰;状态机和 6 步流程是核心竞争力 |
271
- | **动态 prompt** | 9/10 | 10 个条件 hint 精准注入,含 task/memory 上下文注入 + 服务管理 + 工具使用指导,减少 Agent 冗余操作 |
271
+ | **动态 prompt** | 9/10 | 10 个条件 hint 精准注入,含 task/memory 上下文注入 + cwd 路径 + test.env + 服务管理 + 工具使用指导,减少 Agent 冗余操作 |
272
272
  | **SCAN_PROTOCOL.md** | 8.5/10 | 新旧项目分支完整,profile 格式全面 |
273
273
  | **tests.json 设计** | 7.5/10 | 精简字段,核心目的(防反复测试)明确 |
274
274
  | **注入时机** | 9/10 | 静态规则 vs 动态上下文分离干净 |
@@ -0,0 +1,131 @@
1
+ # Playwright MCP 凭证持久化方案
2
+
3
+ ## 背景
4
+
5
+ 在使用 claude-coder 运行涉及前端测试的任务时,Playwright MCP 可能需要:
6
+ 1. 已登录状态的 cookies(如后台管理页面)
7
+ 2. API Key 等测试凭证(如 AI 生成功能需要真实 API 调用)
8
+
9
+ 本文档描述如何在 claude-coder 工作流中管理这些凭证。
10
+
11
+ ---
12
+
13
+ ## 方案 1: Playwright --storage-state(推荐用于 cookies)
14
+
15
+ ### 原理
16
+
17
+ `@playwright/mcp` 支持 `--storage-state=<path>` 参数,加载预存的浏览器状态(cookies、localStorage)。
18
+
19
+ ### 步骤
20
+
21
+ **1. 手动登录并导出状态**
22
+
23
+ ```bash
24
+ # 启动 Playwright,手动登录后导出
25
+ npx playwright codegen --save-storage=.claude-coder/playwright-auth.json http://localhost:3000
26
+ ```
27
+
28
+ 登录完成后关闭浏览器,状态自动保存到 `playwright-auth.json`。
29
+
30
+ **2. 配置 MCP 使用保存的状态**
31
+
32
+ 在项目的 `.mcp.json`(Claude Code MCP 配置)中:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "playwright": {
38
+ "command": "npx",
39
+ "args": [
40
+ "@playwright/mcp@latest",
41
+ "--storage-state=.claude-coder/playwright-auth.json"
42
+ ]
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ **3. 安全注意事项**
49
+
50
+ ```gitignore
51
+ # .gitignore
52
+ .claude-coder/playwright-auth.json
53
+ ```
54
+
55
+ ### 注意
56
+
57
+ - 状态文件包含敏感 cookies,必须加入 `.gitignore`
58
+ - cookies 有过期时间,需要定期重新导出
59
+ - `--storage-state` 与 `--isolated` 模式配合使用效果最佳
60
+
61
+ ---
62
+
63
+ ## 方案 2: test.env(推荐用于 API Key)
64
+
65
+ ### 原理
66
+
67
+ 在 `.claude-coder/test.env` 中存放测试专用的环境变量(如 API Key)。claude-coder 会自动检测此文件存在,并通过 Hint 提示 Agent 在测试前加载它。
68
+
69
+ ### 步骤
70
+
71
+ **1. 创建 test.env**
72
+
73
+ ```bash
74
+ # .claude-coder/test.env
75
+ OPENAI_API_KEY=sk-xxx
76
+ ZHIPU_API_KEY=xxx.xxx
77
+ TEST_USER_TOKEN=xxx
78
+ ```
79
+
80
+ **2. Agent 自动感知**
81
+
82
+ 当 `.claude-coder/test.env` 存在时,harness 在编码 session 的 prompt 中注入提示:
83
+
84
+ > 测试环境变量在 .claude-coder/test.env(含 API Key 等),测试前用 source .claude-coder/test.env 或 export 加载。
85
+
86
+ Agent 在执行测试时会自动 `source` 该文件。
87
+
88
+ **3. 安全注意事项**
89
+
90
+ ```gitignore
91
+ # .gitignore
92
+ .claude-coder/test.env
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 方案 3: project_profile.json 中声明测试依赖
98
+
99
+ 在扫描阶段或手动编辑 `project_profile.json`,声明哪些测试需要真实 API Key:
100
+
101
+ ```json
102
+ {
103
+ "test_dependencies": {
104
+ "real_api_key": true,
105
+ "required_env_vars": ["OPENAI_API_KEY", "ZHIPU_API_KEY"],
106
+ "env_file": ".claude-coder/test.env"
107
+ }
108
+ }
109
+ ```
110
+
111
+ Agent 在 Step 5 测试时,如果检测到 `preconditions.real_api_key: true`,会先检查环境变量是否可用,不可用则跳过该测试并标记为 `skip`。
112
+
113
+ ---
114
+
115
+ ## 最佳实践
116
+
117
+ | 场景 | 推荐方案 |
118
+ |------|----------|
119
+ | 需要已登录状态测试页面 | 方案 1 (--storage-state) |
120
+ | 需要 API Key 测试后端功能 | 方案 2 (test.env) |
121
+ | 需要区分 mock 测试和集成测试 | 方案 3 (profile 声明) |
122
+ | 以上组合 | 方案 1 + 2 + 3 |
123
+
124
+ ### 工作流示例
125
+
126
+ ```
127
+ 1. claude-coder setup → 配置模型
128
+ 2. 创建 .claude-coder/test.env → 填入 API Key
129
+ 3. npx playwright codegen ... → 导出登录状态
130
+ 4. claude-coder run → Agent 自动使用凭证测试
131
+ ```
package/docs/README.en.md CHANGED
@@ -53,12 +53,14 @@ Each session, the agent autonomously follows 6 steps: restore context → env ch
53
53
  | `claude-coder run --max 1` | Single session (replaces old view mode) |
54
54
  | `claude-coder run --dry-run` | Preview mode |
55
55
  | `claude-coder init` | Initialize project environment |
56
- | `claude-coder add "instruction"` | Append tasks |
56
+ | `claude-coder add "instruction"` | Append tasks (defaults to opus-class model) |
57
+ | `claude-coder add -r [file]` | Append tasks from requirements file |
58
+ | `claude-coder add "..." --model M` | Append tasks with specific model |
57
59
  | `claude-coder validate` | Manually validate last session |
58
60
  | `claude-coder status` | View progress and costs |
59
61
  | `claude-coder config sync` | Sync config to ~/.claude/ |
60
62
 
61
- **Options**: `--max N` limit sessions (default 50), `--pause N` pause every N sessions (default 5).
63
+ **Options**: `--max N` limit sessions (default 50), `--pause N` pause every N sessions (default: no pause).
62
64
 
63
65
  ## Model Support
64
66
 
@@ -78,16 +80,18 @@ your-project/
78
80
  .env # Model config
79
81
  project_profile.json # Project scan results
80
82
  tasks.json # Task list + status
81
- session_result.json # Session results + history
82
- progress.json # Session log + costs
83
+ session_result.json # Last session result (flat)
84
+ progress.json # Session history + costs
83
85
  tests.json # Verification records
84
- .runtime/ # Temp files
86
+ test.env # Test credentials (API keys, optional)
87
+ .runtime/ # Temp files (logs)
85
88
  requirements.md # Requirements (optional)
86
89
  ```
87
90
 
88
91
  ## Documentation
89
92
 
90
93
  - [Architecture](ARCHITECTURE.md) — Module responsibilities, prompt injection architecture, attention mechanism, hook data flow, future roadmap
94
+ - [Playwright Credentials](PLAYWRIGHT_CREDENTIALS.md) — Test cookies and API key management
91
95
 
92
96
  ## License
93
97
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-coder",
3
- "version": "1.0.9",
3
+ "version": "1.2.0",
4
4
  "description": "Claude Coder — Autonomous coding agent harness powered by Claude Code SDK. Scan, plan, code, validate, git-commit in a loop.",
5
5
  "bin": {
6
6
  "claude-coder": "bin/cli.js"
package/src/config.js CHANGED
@@ -55,8 +55,7 @@ function paths() {
55
55
  sessionResult: path.join(loopDir, 'session_result.json'),
56
56
  profile: path.join(loopDir, 'project_profile.json'),
57
57
  testsFile: path.join(loopDir, 'tests.json'),
58
- syncState: path.join(loopDir, 'sync_state.json'),
59
- reqHashFile: path.join(loopDir, 'requirements_hash.current'),
58
+ testEnvFile: path.join(loopDir, 'test.env'),
60
59
  claudeMd: getTemplatePath('CLAUDE.md'),
61
60
  scanProtocol: getTemplatePath('SCAN_PROTOCOL.md'),
62
61
  runtime,
package/src/indicator.js CHANGED
@@ -59,6 +59,12 @@ class Indicator {
59
59
  }
60
60
 
61
61
  getStatusLine() {
62
+ const now = new Date();
63
+ const hh = String(now.getHours()).padStart(2, '0');
64
+ const mi = String(now.getMinutes()).padStart(2, '0');
65
+ const sc = String(now.getSeconds()).padStart(2, '0');
66
+ const clock = `${hh}:${mi}:${sc}`;
67
+
62
68
  const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
63
69
  const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
64
70
  const ss = String(elapsed % 60).padStart(2, '0');
@@ -68,7 +74,7 @@ class Indicator {
68
74
  ? `${COLOR.yellow}思考中${COLOR.reset}`
69
75
  : `${COLOR.green}编码中${COLOR.reset}`;
70
76
 
71
- let line = `${spinner} [Session ${this.sessionNum}] ${phaseLabel} ${mm}:${ss}`;
77
+ let line = `${spinner} [Session ${this.sessionNum}] ${clock} ${phaseLabel} ${mm}:${ss}`;
72
78
  if (this.step) line += ` | ${this.step}`;
73
79
  return line;
74
80
  }
package/src/prompts.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
- const { paths, loadConfig, getRequirementsHash } = require('./config');
4
+ const { paths, loadConfig, getProjectRoot } = require('./config');
5
5
  const { loadTasks, findNextTask, getStats } = require('./tasks');
6
6
 
7
7
  /**
@@ -26,40 +26,24 @@ function buildCodingPrompt(sessionNum, opts = {}) {
26
26
  const config = loadConfig();
27
27
  const consecutiveFailures = opts.consecutiveFailures || 0;
28
28
 
29
- // Hint 1: Requirements change detection
30
- const reqHash = getRequirementsHash();
31
- let reqSyncHint = '';
32
- if (reqHash) {
33
- fs.writeFileSync(p.reqHashFile, reqHash, 'utf8');
34
- let lastHash = '';
35
- if (fs.existsSync(p.syncState)) {
36
- try { lastHash = JSON.parse(fs.readFileSync(p.syncState, 'utf8')).last_requirements_hash || ''; } catch { /* ignore */ }
37
- }
38
- if (lastHash !== reqHash) {
39
- reqSyncHint = '需求已变更:第一步中请读取 requirements.md,将新增需求追加为 pending 任务到 tasks.json。';
40
- }
41
- } else if (fs.existsSync(p.reqHashFile)) {
42
- fs.unlinkSync(p.reqHashFile);
43
- }
44
-
45
- // Hint 2: Playwright MCP availability
29
+ // Hint 1: Playwright MCP availability
46
30
  const mcpHint = config.mcpPlaywright
47
31
  ? '前端/全栈任务可用 Playwright MCP(browser_navigate、browser_snapshot、browser_click 等)做端到端测试。'
48
32
  : '';
49
33
 
50
- // Hint 3: Retry context from previous failures
34
+ // Hint 2: Retry context from previous failures
51
35
  let retryContext = '';
52
36
  if (consecutiveFailures > 0 && opts.lastValidateLog) {
53
37
  retryContext = `\n注意:上次会话校验失败,原因:${opts.lastValidateLog}。请避免同样的问题。`;
54
38
  }
55
39
 
56
- // Hint 4: Environment readiness
40
+ // Hint 3: Environment readiness
57
41
  let envHint = '';
58
42
  if (consecutiveFailures === 0 && sessionNum > 1) {
59
43
  envHint = '环境已就绪,第二步可跳过 claude-coder init,仅确认服务存活。涉及新依赖时仍需运行 claude-coder init。';
60
44
  }
61
45
 
62
- // Hint 5: Existing test records
46
+ // Hint 4: Existing test records
63
47
  let testHint = '';
64
48
  if (fs.existsSync(p.testsFile)) {
65
49
  try {
@@ -68,7 +52,7 @@ function buildCodingPrompt(sessionNum, opts = {}) {
68
52
  } catch { /* ignore */ }
69
53
  }
70
54
 
71
- // Hint 6: Project documentation awareness + profile quality check
55
+ // Hint 5: Project documentation awareness + profile quality check
72
56
  let docsHint = '';
73
57
  if (fs.existsSync(p.profile)) {
74
58
  try {
@@ -87,9 +71,10 @@ function buildCodingPrompt(sessionNum, opts = {}) {
87
71
  } catch { /* ignore */ }
88
72
  }
89
73
 
90
- // Hint 7: Task context (harness pre-read, saves Agent 2-3 Read calls)
74
+ // Hint 6: Task context (harness pre-read, saves Agent 2-3 Read calls)
91
75
  let taskHint = '';
92
76
  try {
77
+ const projectRoot = getProjectRoot();
93
78
  const taskData = loadTasks();
94
79
  if (taskData) {
95
80
  const next = findNextTask(taskData);
@@ -98,32 +83,38 @@ function buildCodingPrompt(sessionNum, opts = {}) {
98
83
  taskHint = `任务上下文: ${next.id} "${next.description}" (${next.status}), ` +
99
84
  `category=${next.category}, steps=${next.steps.length}步。` +
100
85
  `进度: ${stats.done}/${stats.total} done, ${stats.failed} failed。` +
101
- `运行时目录: .claude-coder/(隐藏目录,ls -a 可见,所有 tasks.json/profile 等文件均在此目录下)。` +
86
+ `项目绝对路径: ${projectRoot}。运行时目录: ${projectRoot}/.claude-coder/(隐藏目录)。` +
102
87
  `第一步无需读取 tasks.json(已注入),直接确认任务后进入 Step 2。`;
103
88
  }
104
89
  }
105
90
  } catch { /* ignore */ }
106
91
 
107
- // Hint 8: Session memory (last session summary, recency zone for attention)
92
+ // Hint 6b: Test environment variables
93
+ let testEnvHint = '';
94
+ const testEnvFile = paths().testEnvFile;
95
+ if (testEnvFile && fs.existsSync(testEnvFile)) {
96
+ testEnvHint = '测试环境变量在 .claude-coder/test.env(含 API Key 等),测试前用 source .claude-coder/test.env 或 export 加载。';
97
+ }
98
+
99
+ // Hint 7: Session memory (read flat session_result.json)
108
100
  let memoryHint = '';
109
101
  if (fs.existsSync(p.sessionResult)) {
110
102
  try {
111
103
  const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
112
- const last = sr.current || (sr.history?.length ? sr.history[sr.history.length - 1] : null);
113
- if (last?.task_id) {
114
- memoryHint = `上次会话: ${last.task_id} ${last.status_after || last.session_result}` +
115
- (last.notes ? `, 要点: ${last.notes.slice(0, 100)}` : '') + '。';
104
+ if (sr?.task_id) {
105
+ memoryHint = `上次会话: ${sr.task_id} → ${sr.status_after || sr.session_result}` +
106
+ (sr.notes ? `, 要点: ${sr.notes.slice(0, 100)}` : '') + '。';
116
107
  }
117
108
  } catch { /* ignore */ }
118
109
  }
119
110
 
120
- // Hint 9: Service management (continuous vs single-shot mode)
111
+ // Hint 8: Service management (continuous vs single-shot mode)
121
112
  const maxSessions = opts.maxSessions || 50;
122
113
  const serviceHint = maxSessions === 1
123
114
  ? '单次模式:收尾时停止所有后台服务。'
124
115
  : '连续模式:收尾时不要停止后台服务,保持服务运行以便下个 session 继续使用。';
125
116
 
126
- // Hint 10: Tool usage guidance (critical for non-Claude models)
117
+ // Hint 9: Tool usage guidance (critical for non-Claude models)
127
118
  const toolGuidance = [
128
119
  '可用工具与使用规范(严格遵守):',
129
120
  '- 搜索文件名: Glob(如 **/*.ts),禁止 bash find',
@@ -139,12 +130,12 @@ function buildCodingPrompt(sessionNum, opts = {}) {
139
130
  return [
140
131
  `Session ${sessionNum}。执行 6 步流程。`,
141
132
  '效率要求:先规划后编码,完成全部编码后再统一测试,禁止编码-测试反复跳转。后端任务用 curl 验证,不启动浏览器。',
142
- reqSyncHint,
143
133
  mcpHint,
144
134
  testHint,
145
135
  docsHint,
146
136
  envHint,
147
137
  taskHint,
138
+ testEnvHint,
148
139
  memoryHint,
149
140
  serviceHint,
150
141
  toolGuidance,
package/src/runner.js CHANGED
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const readline = require('readline');
6
6
  const { execSync } = require('child_process');
7
- const { paths, log, COLOR, loadConfig, ensureLoopDir, getProjectRoot, getRequirementsHash } = require('./config');
7
+ const { paths, log, COLOR, loadConfig, ensureLoopDir, getProjectRoot } = require('./config');
8
8
  const { loadTasks, saveTasks, getFeatures, getStats, findNextTask } = require('./tasks');
9
9
  const { validate } = require('./validator');
10
10
  const { scan } = require('./scanner');
@@ -52,22 +52,71 @@ function allTasksDone() {
52
52
  return features.every(f => f.status === 'done');
53
53
  }
54
54
 
55
+ function killServicesByProfile() {
56
+ const p = paths();
57
+ if (!fs.existsSync(p.profile)) return;
58
+ try {
59
+ const profile = JSON.parse(fs.readFileSync(p.profile, 'utf8'));
60
+ const services = profile.services || [];
61
+ const ports = services.map(s => s.port).filter(Boolean);
62
+ if (ports.length === 0) return;
63
+
64
+ const isWin = process.platform === 'win32';
65
+ for (const port of ports) {
66
+ try {
67
+ if (isWin) {
68
+ const out = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: 'utf8', stdio: 'pipe' }).trim();
69
+ const pids = [...new Set(out.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(Boolean))];
70
+ for (const pid of pids) { try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' }); } catch { /* ignore */ } }
71
+ } else {
72
+ execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, { stdio: 'pipe' });
73
+ }
74
+ } catch { /* no process on port */ }
75
+ }
76
+ log('info', `已停止端口 ${ports.join(', ')} 上的服务`);
77
+ } catch { /* ignore profile read errors */ }
78
+ }
79
+
80
+ function sleepSync(ms) {
81
+ const end = Date.now() + ms;
82
+ while (Date.now() < end) { /* busy wait */ }
83
+ }
84
+
55
85
  function rollback(headBefore, reason) {
56
86
  if (!headBefore || headBefore === 'none') return;
87
+
88
+ killServicesByProfile();
89
+
90
+ if (process.platform === 'win32') sleepSync(1500);
91
+
92
+ const cwd = getProjectRoot();
93
+ const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
94
+
57
95
  log('warn', `回滚到 ${headBefore} ...`);
58
- try {
59
- execSync(`git reset --hard ${headBefore}`, { cwd: getProjectRoot(), stdio: 'inherit' });
60
- log('ok', '回滚完成');
61
- } catch (err) {
62
- log('error', `回滚失败: ${err.message}`);
96
+
97
+ let success = false;
98
+ for (let attempt = 1; attempt <= 2; attempt++) {
99
+ try {
100
+ execSync(`git reset --hard ${headBefore}`, { cwd, stdio: 'pipe', env: gitEnv });
101
+ log('ok', '回滚完成');
102
+ success = true;
103
+ break;
104
+ } catch (err) {
105
+ if (attempt === 1) {
106
+ log('warn', `回滚首次失败,等待后重试: ${err.message}`);
107
+ sleepSync(2000);
108
+ } else {
109
+ log('error', `回滚失败: ${err.message}`);
110
+ }
111
+ }
63
112
  }
64
113
 
65
- // Record failure in progress.json
66
114
  appendProgress({
67
115
  type: 'rollback',
68
116
  timestamp: new Date().toISOString(),
69
117
  reason: reason || 'harness 校验失败',
70
118
  rollbackTo: headBefore,
119
+ success,
71
120
  });
72
121
  }
73
122
 
@@ -110,36 +159,6 @@ function appendProgress(entry) {
110
159
  fs.writeFileSync(p.progressFile, JSON.stringify(progress, null, 2) + '\n', 'utf8');
111
160
  }
112
161
 
113
- function updateSessionHistory(sessionData, sessionNum) {
114
- const p = paths();
115
- let sr = { current: null, history: [] };
116
- if (fs.existsSync(p.sessionResult)) {
117
- try {
118
- const text = fs.readFileSync(p.sessionResult, 'utf8');
119
- sr = JSON.parse(text);
120
- } catch { /* reset */ }
121
- if (!sr.history && sr.session_result) {
122
- sr = { current: sr, history: [] };
123
- }
124
- }
125
-
126
- // Move current to history
127
- if (sr.current) {
128
- sr.history.push({
129
- session: sessionNum - 1,
130
- timestamp: new Date().toISOString(),
131
- ...sr.current,
132
- });
133
- sr.current = null;
134
- }
135
-
136
- if (sessionData) {
137
- sr.current = sessionData;
138
- }
139
-
140
- fs.writeFileSync(p.sessionResult, JSON.stringify(sr, null, 2) + '\n', 'utf8');
141
- }
142
-
143
162
  function printStats() {
144
163
  const data = loadTasks();
145
164
  if (!data) return;
@@ -270,10 +289,13 @@ async function run(requirement, opts = {}) {
270
289
  }
271
290
 
272
291
  const headBefore = getHead();
292
+ const nextTask = findNextTask(taskData);
293
+ const taskId = nextTask?.id || 'unknown';
273
294
 
274
295
  // Run coding session
275
296
  const sessionResult = await runCodingSession(session, {
276
297
  projectRoot,
298
+ taskId,
277
299
  consecutiveFailures,
278
300
  maxSessions,
279
301
  lastValidateLog: consecutiveFailures > 0 ? '上次校验失败' : '',
@@ -288,19 +310,6 @@ async function run(requirement, opts = {}) {
288
310
  tryPush();
289
311
  consecutiveFailures = 0;
290
312
 
291
- // Update session history
292
- updateSessionHistory(validateResult.sessionData, session);
293
-
294
- // Update sync_state.json if requirements exist
295
- const reqHash = getRequirementsHash();
296
- if (reqHash) {
297
- fs.writeFileSync(p.syncState, JSON.stringify({
298
- last_requirements_hash: reqHash,
299
- last_synced_at: new Date().toISOString(),
300
- }, null, 2) + '\n', 'utf8');
301
- }
302
-
303
- // Append to progress.json
304
313
  appendProgress({
305
314
  session,
306
315
  timestamp: new Date().toISOString(),
@@ -308,6 +317,7 @@ async function run(requirement, opts = {}) {
308
317
  cost: sessionResult.cost,
309
318
  taskId: validateResult.sessionData?.task_id || null,
310
319
  statusAfter: validateResult.sessionData?.status_after || null,
320
+ notes: validateResult.sessionData?.notes || null,
311
321
  });
312
322
 
313
323
  } else {
@@ -325,7 +335,7 @@ async function run(requirement, opts = {}) {
325
335
  }
326
336
 
327
337
  // Periodic pause
328
- if (session % pauseEvery === 0) {
338
+ if (pauseEvery > 0 && session % pauseEvery === 0) {
329
339
  console.log('');
330
340
  printStats();
331
341
  const shouldContinue = await promptContinue();
@@ -336,6 +346,9 @@ async function run(requirement, opts = {}) {
336
346
  }
337
347
  }
338
348
 
349
+ // Cleanup: stop services after loop ends
350
+ killServicesByProfile();
351
+
339
352
  // Final report
340
353
  console.log('');
341
354
  console.log('============================================');
@@ -351,6 +364,19 @@ async function add(instruction, opts = {}) {
351
364
  const projectRoot = getProjectRoot();
352
365
  ensureLoopDir();
353
366
 
367
+ const config = loadConfig();
368
+
369
+ if (!opts.model) {
370
+ if (config.defaultOpus) {
371
+ opts.model = config.defaultOpus;
372
+ } else if (config.provider === 'claude' || !config.baseUrl) {
373
+ opts.model = 'claude-sonnet-4-20250514';
374
+ }
375
+ }
376
+
377
+ const displayModel = opts.model || config.model || '(default)';
378
+ log('ok', `模型配置已加载: ${config.provider || 'claude'} (add 使用: ${displayModel})`);
379
+
354
380
  if (!fs.existsSync(p.profile) || !fs.existsSync(p.tasksFile)) {
355
381
  log('error', 'add 需要先完成初始化(至少运行一次 claude-coder run)');
356
382
  process.exit(1);
package/src/session.js CHANGED
@@ -51,7 +51,8 @@ function buildQueryOptions(config, opts = {}) {
51
51
  env: buildEnvVars(config),
52
52
  settingSources: ['project'],
53
53
  };
54
- if (config.model) base.model = config.model;
54
+ if (opts.model) base.model = opts.model;
55
+ else if (config.model) base.model = config.model;
55
56
  return base;
56
57
  }
57
58
 
@@ -62,6 +63,10 @@ function extractResult(messages) {
62
63
  return null;
63
64
  }
64
65
 
66
+ function stripAnsi(str) {
67
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
68
+ }
69
+
65
70
  function logMessage(message, logStream, indicator) {
66
71
  if (message.type === 'assistant' && message.message?.content) {
67
72
  for (const block of message.message.content) {
@@ -70,6 +75,9 @@ function logMessage(message, logStream, indicator) {
70
75
  const statusLine = indicator.getStatusLine();
71
76
  process.stderr.write('\r\x1b[K');
72
77
  if (statusLine) process.stderr.write(statusLine + '\n');
78
+ if (logStream && statusLine) {
79
+ logStream.write('\n' + stripAnsi(statusLine) + '\n');
80
+ }
73
81
  }
74
82
  process.stdout.write(block.text);
75
83
  if (logStream) logStream.write(block.text);
@@ -88,7 +96,9 @@ async function runCodingSession(sessionNum, opts = {}) {
88
96
  const systemPrompt = buildSystemPrompt(false);
89
97
 
90
98
  const p = paths();
91
- const logFile = path.join(p.logsDir, `session_${sessionNum}_${Date.now()}.log`);
99
+ const taskId = opts.taskId || 'unknown';
100
+ const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
101
+ const logFile = path.join(p.logsDir, `${taskId}_session_${sessionNum}_${dateStr}.log`);
92
102
  const logStream = fs.createWriteStream(logFile, { flags: 'a' });
93
103
 
94
104
  indicator.start(sessionNum);
@@ -164,7 +174,7 @@ async function runScanSession(requirement, opts = {}) {
164
174
  const systemPrompt = buildSystemPrompt(true);
165
175
 
166
176
  const p = paths();
167
- const logFile = path.join(p.logsDir, `scan_${Date.now()}.log`);
177
+ const logFile = path.join(p.logsDir, `scan_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
168
178
  const logStream = fs.createWriteStream(logFile, { flags: 'a' });
169
179
 
170
180
  indicator.start(0);
@@ -212,28 +222,43 @@ async function runAddSession(instruction, opts = {}) {
212
222
  const sdk = await loadSDK();
213
223
  const config = loadConfig();
214
224
  applyEnvConfig(config);
225
+ const indicator = new Indicator();
215
226
 
216
227
  const systemPrompt = buildSystemPrompt(false);
217
228
  const prompt = buildAddPrompt(instruction);
218
229
 
219
230
  const p = paths();
220
- const logFile = path.join(p.logsDir, `add_tasks_${Date.now()}.log`);
231
+ const logFile = path.join(p.logsDir, `add_tasks_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.log`);
221
232
  const logStream = fs.createWriteStream(logFile, { flags: 'a' });
222
233
 
234
+ indicator.start(0);
235
+ log('info', '正在追加任务...');
236
+
223
237
  try {
224
238
  const queryOpts = buildQueryOptions(config, opts);
225
239
  queryOpts.systemPrompt = systemPrompt;
240
+ queryOpts.hooks = {
241
+ PreToolUse: [{
242
+ matcher: '*',
243
+ hooks: [async (input) => {
244
+ inferPhaseStep(indicator, input.tool_name, input.tool_input);
245
+ return {};
246
+ }]
247
+ }]
248
+ };
226
249
 
227
250
  const session = sdk.query({ prompt, options: queryOpts });
228
251
 
229
252
  for await (const message of session) {
230
- logMessage(message, logStream);
253
+ logMessage(message, logStream, indicator);
231
254
  }
232
255
 
233
256
  logStream.end();
257
+ indicator.stop();
234
258
  log('ok', '任务追加完成');
235
259
  } catch (err) {
236
260
  logStream.end();
261
+ indicator.stop();
237
262
  log('error', `任务追加失败: ${err.message}`);
238
263
  }
239
264
  }
package/src/validator.js CHANGED
@@ -20,37 +20,35 @@ function validateSessionResult() {
20
20
  return { valid: false, fatal: true, reason: `JSON 解析失败: ${err.message}` };
21
21
  }
22
22
 
23
- const sr = data.current || data;
24
-
25
23
  const required = ['session_result', 'status_after'];
26
- const missing = required.filter(k => !(k in sr));
24
+ const missing = required.filter(k => !(k in data));
27
25
  if (missing.length > 0) {
28
26
  log('error', `session_result.json 缺少字段: ${missing.join(', ')}`);
29
27
  return { valid: false, fatal: true, reason: `缺少字段: ${missing.join(', ')}` };
30
28
  }
31
29
 
32
- if (!['success', 'failed'].includes(sr.session_result)) {
33
- log('error', `session_result 必须是 success 或 failed,实际是: ${sr.session_result}`);
34
- return { valid: false, fatal: true, reason: `无效 session_result: ${sr.session_result}` };
30
+ if (!['success', 'failed'].includes(data.session_result)) {
31
+ log('error', `session_result 必须是 success 或 failed,实际是: ${data.session_result}`);
32
+ return { valid: false, fatal: true, reason: `无效 session_result: ${data.session_result}` };
35
33
  }
36
34
 
37
35
  const validStatuses = ['pending', 'in_progress', 'testing', 'done', 'failed'];
38
- if (!validStatuses.includes(sr.status_after)) {
39
- log('error', `status_after 不合法: ${sr.status_after}`);
40
- return { valid: false, fatal: true, reason: `无效 status_after: ${sr.status_after}` };
36
+ if (!validStatuses.includes(data.status_after)) {
37
+ log('error', `status_after 不合法: ${data.status_after}`);
38
+ return { valid: false, fatal: true, reason: `无效 status_after: ${data.status_after}` };
41
39
  }
42
40
 
43
- if (!sr.task_id) {
41
+ if (!data.task_id) {
44
42
  log('warn', 'session_result.json 缺少 task_id (建议包含)');
45
43
  }
46
44
 
47
- if (sr.session_result === 'success') {
45
+ if (data.session_result === 'success') {
48
46
  log('ok', 'session_result.json 合法 (success)');
49
47
  } else {
50
48
  log('warn', 'session_result.json 合法,但 Agent 报告失败 (failed)');
51
49
  }
52
50
 
53
- return { valid: true, fatal: false, data: sr };
51
+ return { valid: true, fatal: false, data };
54
52
  }
55
53
 
56
54
  function checkGitProgress(headBefore) {
@@ -87,13 +85,12 @@ function checkTestCoverage() {
87
85
 
88
86
  try {
89
87
  const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
90
- const current = sr.current || sr;
91
88
  const tests = JSON.parse(fs.readFileSync(p.testsFile, 'utf8'));
92
89
 
93
- const taskId = current.task_id || '';
90
+ const taskId = sr.task_id || '';
94
91
  const testCases = tests.test_cases || [];
95
92
 
96
- if (current.status_after === 'done' && current.tests_passed) {
93
+ if (sr.status_after === 'done' && sr.tests_passed) {
97
94
  const taskTests = testCases.filter(t => t.feature_id === taskId);
98
95
  if (taskTests.length > 0) {
99
96
  const failed = taskTests.filter(t => t.last_result === 'fail');
@@ -49,7 +49,6 @@
49
49
  | `.claude-coder/tasks.json` | 功能任务列表,带状态跟踪 | 只能修改 `status` 字段 |
50
50
  | `.claude-coder/progress.json` | 跨会话记忆日志(外部循环自动维护) | 只读 |
51
51
  | `.claude-coder/session_result.json` | 本次会话的结构化输出 | 每次会话结束时覆盖写入 |
52
- | `.claude-coder/sync_state.json` | 需求同步状态(外部循环 session 成功后自动更新) | Agent 无需读写 |
53
52
  | `.claude-coder/tests.json` | 功能验证记录(轻量) | 可新增和更新;仅当功能涉及 API 或核心逻辑时记录 |
54
53
 
55
54
  ### requirements.md 处理原则
@@ -178,10 +177,9 @@ pending ──→ in_progress ──→ testing ──→ done
178
177
  1. **检查 prompt 注入的上下文**:
179
178
  - 如果 prompt 中包含"任务上下文"(Hint 7),说明 harness 已注入当前任务信息,**跳过读取 tasks.json**,直接确认任务后进入第二步
180
179
  - 如果 prompt 中包含"上次会话"(Hint 8),说明 harness 已注入上次会话摘要,**跳过读取 session_result.json 历史**
181
- 2. 批量读取以下文件(一次工具调用,跳过已注入的):`.claude-coder/project_profile.json`、`.claude-coder/tasks.json`(仅当无 Hint 7 时)、`.claude-coder/session_result.json`(仅当无 Hint 8 时)
182
- 3. 如果 `session_result.json` 不存在或 history 为空且无 Hint 8,运行 `git log --oneline -20` 补充上下文
180
+ 2. 批量读取以下文件(一次工具调用,跳过已注入的):`.claude-coder/project_profile.json`、`.claude-coder/tasks.json`(仅当无 Hint 6 时)
181
+ 3. 如果无 Hint 7 且 `session_result.json` 不存在,运行 `git log --oneline -20` 补充上下文
183
182
  4. 如果项目根目录存在 `requirements.md`,读取用户的详细需求和偏好(技术约束、样式要求等),作为本次会话的参考依据
184
- 5. **需求同步(条件触发)**:如果 prompt 中提示"需求已变更",读取 `requirements.md`,对比 `tasks.json`,将新增需求追加为 `pending` 任务。未提示则跳过
185
183
 
186
184
  ### 第二步:环境与健康检查
187
185