claude-coder 1.0.9 → 1.1.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 +7 -6
- package/bin/cli.js +30 -7
- package/docs/ARCHITECTURE.md +12 -13
- package/docs/README.en.md +4 -3
- package/package.json +1 -1
- package/src/config.js +0 -2
- package/src/indicator.js +7 -1
- package/src/prompts.js +13 -31
- package/src/runner.js +40 -47
- package/src/session.js +16 -1
- package/src/validator.js +12 -15
- package/templates/CLAUDE.md +2 -4
package/README.md
CHANGED
|
@@ -50,15 +50,16 @@ 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
56
|
| `claude-coder add "指令"` | 追加任务 |
|
|
57
|
+
| `claude-coder add -r [file]` | 从需求文件追加任务 |
|
|
57
58
|
| `claude-coder validate` | 手动校验 |
|
|
58
59
|
| `claude-coder status` | 查看进度和成本 |
|
|
59
60
|
| `claude-coder config sync` | 同步配置到 ~/.claude/ |
|
|
60
61
|
|
|
61
|
-
**选项**:`--max N` 限制 session 数(默认 50),`--pause N` 每 N 个 session
|
|
62
|
+
**选项**:`--max N` 限制 session 数(默认 50),`--pause N` 每 N 个 session 暂停确认(默认不暂停)。
|
|
62
63
|
|
|
63
64
|
## 使用场景
|
|
64
65
|
|
|
@@ -66,9 +67,9 @@ claude-coder run "实现用户注册和登录功能"
|
|
|
66
67
|
|
|
67
68
|
**已有项目**:`claude-coder run "新增头像上传功能"` — 先扫描现有代码和技术栈,再增量开发。
|
|
68
69
|
|
|
69
|
-
**需求文档驱动**:在项目根目录创建 `requirements.md
|
|
70
|
+
**需求文档驱动**:在项目根目录创建 `requirements.md`,运行 `claude-coder run` — 需求变更后用 `claude-coder add -r` 同步新任务。
|
|
70
71
|
|
|
71
|
-
**追加任务**:`claude-coder add "新增管理员后台"` — 仅追加到任务列表,下次 run 时执行。
|
|
72
|
+
**追加任务**:`claude-coder add "新增管理员后台"` 或 `claude-coder add -r requirements.md` — 仅追加到任务列表,下次 run 时执行。
|
|
72
73
|
|
|
73
74
|
## 模型支持
|
|
74
75
|
|
|
@@ -88,8 +89,8 @@ your-project/
|
|
|
88
89
|
.env # 模型配置
|
|
89
90
|
project_profile.json # 项目扫描结果
|
|
90
91
|
tasks.json # 任务列表 + 状态
|
|
91
|
-
session_result.json # session
|
|
92
|
-
progress.json #
|
|
92
|
+
session_result.json # 上次 session 结果(扁平)
|
|
93
|
+
progress.json # 会话历史 + 成本
|
|
93
94
|
tests.json # 验证记录
|
|
94
95
|
.runtime/ # 临时文件
|
|
95
96
|
requirements.md # 需求文档(可选)
|
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 "指令" | 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,11 @@ 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
|
|
27
|
-
console.log(' claude-coder run --max 5 --
|
|
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 追加任务');
|
|
28
31
|
console.log(' claude-coder status 查看进度和成本');
|
|
29
32
|
console.log(`\n前置条件: npm install -g @anthropic-ai/claude-agent-sdk`);
|
|
30
33
|
}
|
|
@@ -32,7 +35,7 @@ function showHelp() {
|
|
|
32
35
|
function parseArgs(argv) {
|
|
33
36
|
const args = argv.slice(2);
|
|
34
37
|
const command = args[0];
|
|
35
|
-
const opts = { max: 50, pause:
|
|
38
|
+
const opts = { max: 50, pause: 0, dryRun: false, readFile: null };
|
|
36
39
|
const positional = [];
|
|
37
40
|
|
|
38
41
|
for (let i = 1; i < args.length; i++) {
|
|
@@ -46,6 +49,16 @@ function parseArgs(argv) {
|
|
|
46
49
|
case '--dry-run':
|
|
47
50
|
opts.dryRun = true;
|
|
48
51
|
break;
|
|
52
|
+
case '-r': {
|
|
53
|
+
const next = args[i + 1];
|
|
54
|
+
if (next && !next.startsWith('-')) {
|
|
55
|
+
opts.readFile = next;
|
|
56
|
+
i++;
|
|
57
|
+
} else {
|
|
58
|
+
opts.readFile = 'requirements.md';
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
49
62
|
case '--help':
|
|
50
63
|
case '-h':
|
|
51
64
|
showHelp();
|
|
@@ -92,12 +105,22 @@ async function main() {
|
|
|
92
105
|
break;
|
|
93
106
|
}
|
|
94
107
|
case 'add': {
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
let instruction = positional[0] || '';
|
|
109
|
+
if (opts.readFile) {
|
|
110
|
+
const reqPath = require('path').resolve(opts.readFile);
|
|
111
|
+
if (!require('fs').existsSync(reqPath)) {
|
|
112
|
+
console.error(`文件不存在: ${reqPath}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
instruction = require('fs').readFileSync(reqPath, 'utf8');
|
|
116
|
+
console.log(`已读取需求文件: ${opts.readFile}`);
|
|
117
|
+
}
|
|
118
|
+
if (!instruction) {
|
|
119
|
+
console.error('用法: claude-coder add "任务描述" 或 claude-coder add -r [requirements.md]');
|
|
97
120
|
process.exit(1);
|
|
98
121
|
}
|
|
99
122
|
const runner = require('../src/runner');
|
|
100
|
-
await runner.add(
|
|
123
|
+
await runner.add(instruction, opts);
|
|
101
124
|
break;
|
|
102
125
|
}
|
|
103
126
|
case 'validate': {
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -183,24 +183,23 @@ flowchart TB
|
|
|
183
183
|
|
|
184
184
|
| Session 类型 | systemPrompt | user prompt | 触发条件 |
|
|
185
185
|
|---|---|---|---|
|
|
186
|
-
| **编码** | CLAUDE.md | `buildCodingPrompt()` +
|
|
186
|
+
| **编码** | CLAUDE.md | `buildCodingPrompt()` + 9 个条件 hint | 主循环每次迭代 |
|
|
187
187
|
| **扫描** | CLAUDE.md + SCAN_PROTOCOL.md | `buildScanPrompt()` + 任务分解指导 + profile 质量要求 | 首次运行 |
|
|
188
188
|
| **追加** | CLAUDE.md | `buildAddPrompt()` + 任务分解指导 | `claude-coder add` |
|
|
189
189
|
|
|
190
|
-
### 编码 Session 的
|
|
190
|
+
### 编码 Session 的 9 个条件 Hint
|
|
191
191
|
|
|
192
192
|
| # | Hint | 触发条件 | 影响 |
|
|
193
193
|
|---|---|---|---|
|
|
194
|
-
| 1 | `
|
|
195
|
-
| 2 | `
|
|
196
|
-
| 3 | `
|
|
197
|
-
| 4 | `
|
|
198
|
-
| 5 | `
|
|
199
|
-
| 6 | `
|
|
200
|
-
| 7 | `
|
|
201
|
-
| 8 | `
|
|
202
|
-
| 9 | `
|
|
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 已注入当前任务上下文 + .claude-coder/ 路径提示 |
|
|
200
|
+
| 7 | `memoryHint` | session_result.json 存在(扁平格式) | Step 1:跳过读取 session_result.json,harness 已注入上次会话摘要 |
|
|
201
|
+
| 8 | `serviceHint` | 始终注入 | Step 6:单次模式停止服务,连续模式保持服务运行 |
|
|
202
|
+
| 9 | `toolGuidance` | 始终注入 | 全局:工具使用规范(Grep/Glob/Read/LS/MultiEdit/Task 替代 bash 命令),非 Claude 模型必需 |
|
|
204
203
|
|
|
205
204
|
---
|
|
206
205
|
|
|
@@ -268,7 +267,7 @@ sequenceDiagram
|
|
|
268
267
|
| 维度 | 评分 | 说明 |
|
|
269
268
|
|------|------|------|
|
|
270
269
|
| **CLAUDE.md 系统提示** | 8/10 | U 型注意力设计;铁律清晰;状态机和 6 步流程是核心竞争力 |
|
|
271
|
-
| **动态 prompt** | 9/10 |
|
|
270
|
+
| **动态 prompt** | 9/10 | 9 个条件 hint 精准注入,含 task/memory 上下文注入 + 服务管理 + 工具使用指导,减少 Agent 冗余操作 |
|
|
272
271
|
| **SCAN_PROTOCOL.md** | 8.5/10 | 新旧项目分支完整,profile 格式全面 |
|
|
273
272
|
| **tests.json 设计** | 7.5/10 | 精简字段,核心目的(防反复测试)明确 |
|
|
274
273
|
| **注入时机** | 9/10 | 静态规则 vs 动态上下文分离干净 |
|
package/docs/README.en.md
CHANGED
|
@@ -54,11 +54,12 @@ Each session, the agent autonomously follows 6 steps: restore context → env ch
|
|
|
54
54
|
| `claude-coder run --dry-run` | Preview mode |
|
|
55
55
|
| `claude-coder init` | Initialize project environment |
|
|
56
56
|
| `claude-coder add "instruction"` | Append tasks |
|
|
57
|
+
| `claude-coder add -r [file]` | Append tasks from requirements file |
|
|
57
58
|
| `claude-coder validate` | Manually validate last session |
|
|
58
59
|
| `claude-coder status` | View progress and costs |
|
|
59
60
|
| `claude-coder config sync` | Sync config to ~/.claude/ |
|
|
60
61
|
|
|
61
|
-
**Options**: `--max N` limit sessions (default 50), `--pause N` pause every N sessions (default
|
|
62
|
+
**Options**: `--max N` limit sessions (default 50), `--pause N` pause every N sessions (default: no pause).
|
|
62
63
|
|
|
63
64
|
## Model Support
|
|
64
65
|
|
|
@@ -78,8 +79,8 @@ your-project/
|
|
|
78
79
|
.env # Model config
|
|
79
80
|
project_profile.json # Project scan results
|
|
80
81
|
tasks.json # Task list + status
|
|
81
|
-
session_result.json #
|
|
82
|
-
progress.json # Session
|
|
82
|
+
session_result.json # Last session result (flat)
|
|
83
|
+
progress.json # Session history + costs
|
|
83
84
|
tests.json # Verification records
|
|
84
85
|
.runtime/ # Temp files
|
|
85
86
|
requirements.md # Requirements (optional)
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -55,8 +55,6 @@ 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'),
|
|
60
58
|
claudeMd: getTemplatePath('CLAUDE.md'),
|
|
61
59
|
scanProtocol: getTemplatePath('SCAN_PROTOCOL.md'),
|
|
62
60
|
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
|
|
4
|
+
const { paths, loadConfig } = 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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
55
|
+
// Hint 5: Project documentation awareness + profile quality check
|
|
72
56
|
let docsHint = '';
|
|
73
57
|
if (fs.existsSync(p.profile)) {
|
|
74
58
|
try {
|
|
@@ -87,7 +71,7 @@ function buildCodingPrompt(sessionNum, opts = {}) {
|
|
|
87
71
|
} catch { /* ignore */ }
|
|
88
72
|
}
|
|
89
73
|
|
|
90
|
-
// Hint
|
|
74
|
+
// Hint 6: Task context (harness pre-read, saves Agent 2-3 Read calls)
|
|
91
75
|
let taskHint = '';
|
|
92
76
|
try {
|
|
93
77
|
const taskData = loadTasks();
|
|
@@ -104,26 +88,25 @@ function buildCodingPrompt(sessionNum, opts = {}) {
|
|
|
104
88
|
}
|
|
105
89
|
} catch { /* ignore */ }
|
|
106
90
|
|
|
107
|
-
// Hint
|
|
91
|
+
// Hint 7: Session memory (read flat session_result.json)
|
|
108
92
|
let memoryHint = '';
|
|
109
93
|
if (fs.existsSync(p.sessionResult)) {
|
|
110
94
|
try {
|
|
111
95
|
const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
(last.notes ? `, 要点: ${last.notes.slice(0, 100)}` : '') + '。';
|
|
96
|
+
if (sr?.task_id) {
|
|
97
|
+
memoryHint = `上次会话: ${sr.task_id} → ${sr.status_after || sr.session_result}` +
|
|
98
|
+
(sr.notes ? `, 要点: ${sr.notes.slice(0, 100)}` : '') + '。';
|
|
116
99
|
}
|
|
117
100
|
} catch { /* ignore */ }
|
|
118
101
|
}
|
|
119
102
|
|
|
120
|
-
// Hint
|
|
103
|
+
// Hint 8: Service management (continuous vs single-shot mode)
|
|
121
104
|
const maxSessions = opts.maxSessions || 50;
|
|
122
105
|
const serviceHint = maxSessions === 1
|
|
123
106
|
? '单次模式:收尾时停止所有后台服务。'
|
|
124
107
|
: '连续模式:收尾时不要停止后台服务,保持服务运行以便下个 session 继续使用。';
|
|
125
108
|
|
|
126
|
-
// Hint
|
|
109
|
+
// Hint 9: Tool usage guidance (critical for non-Claude models)
|
|
127
110
|
const toolGuidance = [
|
|
128
111
|
'可用工具与使用规范(严格遵守):',
|
|
129
112
|
'- 搜索文件名: Glob(如 **/*.ts),禁止 bash find',
|
|
@@ -139,7 +122,6 @@ function buildCodingPrompt(sessionNum, opts = {}) {
|
|
|
139
122
|
return [
|
|
140
123
|
`Session ${sessionNum}。执行 6 步流程。`,
|
|
141
124
|
'效率要求:先规划后编码,完成全部编码后再统一测试,禁止编码-测试反复跳转。后端任务用 curl 验证,不启动浏览器。',
|
|
142
|
-
reqSyncHint,
|
|
143
125
|
mcpHint,
|
|
144
126
|
testHint,
|
|
145
127
|
docsHint,
|
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
|
|
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,17 +52,44 @@ 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
|
+
|
|
55
80
|
function rollback(headBefore, reason) {
|
|
56
81
|
if (!headBefore || headBefore === 'none') return;
|
|
82
|
+
|
|
83
|
+
killServicesByProfile();
|
|
84
|
+
|
|
57
85
|
log('warn', `回滚到 ${headBefore} ...`);
|
|
58
86
|
try {
|
|
59
|
-
execSync(`git reset --hard ${headBefore}`, { cwd: getProjectRoot(), stdio: '
|
|
87
|
+
execSync(`git reset --hard ${headBefore}`, { cwd: getProjectRoot(), stdio: 'pipe' });
|
|
60
88
|
log('ok', '回滚完成');
|
|
61
89
|
} catch (err) {
|
|
62
90
|
log('error', `回滚失败: ${err.message}`);
|
|
63
91
|
}
|
|
64
92
|
|
|
65
|
-
// Record failure in progress.json
|
|
66
93
|
appendProgress({
|
|
67
94
|
type: 'rollback',
|
|
68
95
|
timestamp: new Date().toISOString(),
|
|
@@ -110,36 +137,6 @@ function appendProgress(entry) {
|
|
|
110
137
|
fs.writeFileSync(p.progressFile, JSON.stringify(progress, null, 2) + '\n', 'utf8');
|
|
111
138
|
}
|
|
112
139
|
|
|
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
140
|
function printStats() {
|
|
144
141
|
const data = loadTasks();
|
|
145
142
|
if (!data) return;
|
|
@@ -288,19 +285,6 @@ async function run(requirement, opts = {}) {
|
|
|
288
285
|
tryPush();
|
|
289
286
|
consecutiveFailures = 0;
|
|
290
287
|
|
|
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
288
|
appendProgress({
|
|
305
289
|
session,
|
|
306
290
|
timestamp: new Date().toISOString(),
|
|
@@ -308,6 +292,7 @@ async function run(requirement, opts = {}) {
|
|
|
308
292
|
cost: sessionResult.cost,
|
|
309
293
|
taskId: validateResult.sessionData?.task_id || null,
|
|
310
294
|
statusAfter: validateResult.sessionData?.status_after || null,
|
|
295
|
+
notes: validateResult.sessionData?.notes || null,
|
|
311
296
|
});
|
|
312
297
|
|
|
313
298
|
} else {
|
|
@@ -325,7 +310,7 @@ async function run(requirement, opts = {}) {
|
|
|
325
310
|
}
|
|
326
311
|
|
|
327
312
|
// Periodic pause
|
|
328
|
-
if (session % pauseEvery === 0) {
|
|
313
|
+
if (pauseEvery > 0 && session % pauseEvery === 0) {
|
|
329
314
|
console.log('');
|
|
330
315
|
printStats();
|
|
331
316
|
const shouldContinue = await promptContinue();
|
|
@@ -336,6 +321,9 @@ async function run(requirement, opts = {}) {
|
|
|
336
321
|
}
|
|
337
322
|
}
|
|
338
323
|
|
|
324
|
+
// Cleanup: stop services after loop ends
|
|
325
|
+
killServicesByProfile();
|
|
326
|
+
|
|
339
327
|
// Final report
|
|
340
328
|
console.log('');
|
|
341
329
|
console.log('============================================');
|
|
@@ -351,6 +339,11 @@ async function add(instruction, opts = {}) {
|
|
|
351
339
|
const projectRoot = getProjectRoot();
|
|
352
340
|
ensureLoopDir();
|
|
353
341
|
|
|
342
|
+
const config = loadConfig();
|
|
343
|
+
if (config.provider !== 'claude' && config.baseUrl) {
|
|
344
|
+
log('ok', `模型配置已加载: ${config.provider}${config.model ? ` (${config.model})` : ''}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
354
347
|
if (!fs.existsSync(p.profile) || !fs.existsSync(p.tasksFile)) {
|
|
355
348
|
log('error', 'add 需要先完成初始化(至少运行一次 claude-coder run)');
|
|
356
349
|
process.exit(1);
|
package/src/session.js
CHANGED
|
@@ -212,6 +212,7 @@ async function runAddSession(instruction, opts = {}) {
|
|
|
212
212
|
const sdk = await loadSDK();
|
|
213
213
|
const config = loadConfig();
|
|
214
214
|
applyEnvConfig(config);
|
|
215
|
+
const indicator = new Indicator();
|
|
215
216
|
|
|
216
217
|
const systemPrompt = buildSystemPrompt(false);
|
|
217
218
|
const prompt = buildAddPrompt(instruction);
|
|
@@ -220,20 +221,34 @@ async function runAddSession(instruction, opts = {}) {
|
|
|
220
221
|
const logFile = path.join(p.logsDir, `add_tasks_${Date.now()}.log`);
|
|
221
222
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
222
223
|
|
|
224
|
+
indicator.start(0);
|
|
225
|
+
log('info', '正在追加任务...');
|
|
226
|
+
|
|
223
227
|
try {
|
|
224
228
|
const queryOpts = buildQueryOptions(config, opts);
|
|
225
229
|
queryOpts.systemPrompt = systemPrompt;
|
|
230
|
+
queryOpts.hooks = {
|
|
231
|
+
PreToolUse: [{
|
|
232
|
+
matcher: '*',
|
|
233
|
+
hooks: [async (input) => {
|
|
234
|
+
inferPhaseStep(indicator, input.tool_name, input.tool_input);
|
|
235
|
+
return {};
|
|
236
|
+
}]
|
|
237
|
+
}]
|
|
238
|
+
};
|
|
226
239
|
|
|
227
240
|
const session = sdk.query({ prompt, options: queryOpts });
|
|
228
241
|
|
|
229
242
|
for await (const message of session) {
|
|
230
|
-
logMessage(message, logStream);
|
|
243
|
+
logMessage(message, logStream, indicator);
|
|
231
244
|
}
|
|
232
245
|
|
|
233
246
|
logStream.end();
|
|
247
|
+
indicator.stop();
|
|
234
248
|
log('ok', '任务追加完成');
|
|
235
249
|
} catch (err) {
|
|
236
250
|
logStream.end();
|
|
251
|
+
indicator.stop();
|
|
237
252
|
log('error', `任务追加失败: ${err.message}`);
|
|
238
253
|
}
|
|
239
254
|
}
|
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
|
|
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(
|
|
33
|
-
log('error', `session_result 必须是 success 或 failed,实际是: ${
|
|
34
|
-
return { valid: false, fatal: true, reason: `无效 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(
|
|
39
|
-
log('error', `status_after 不合法: ${
|
|
40
|
-
return { valid: false, fatal: true, reason: `无效 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 (!
|
|
41
|
+
if (!data.task_id) {
|
|
44
42
|
log('warn', 'session_result.json 缺少 task_id (建议包含)');
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
if (
|
|
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
|
|
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 =
|
|
90
|
+
const taskId = sr.task_id || '';
|
|
94
91
|
const testCases = tests.test_cases || [];
|
|
95
92
|
|
|
96
|
-
if (
|
|
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');
|
package/templates/CLAUDE.md
CHANGED
|
@@ -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
|
|
182
|
-
3.
|
|
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
|
|