claude-coder 1.0.2 → 1.0.4

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/bin/cli.js CHANGED
@@ -46,6 +46,9 @@ function parseArgs(argv) {
46
46
  case '--dry-run':
47
47
  opts.dryRun = true;
48
48
  break;
49
+ case '--view':
50
+ opts.viewMode = true;
51
+ break;
49
52
  case '--help':
50
53
  case '-h':
51
54
  showHelp();
@@ -78,7 +81,11 @@ async function main() {
78
81
  switch (command) {
79
82
  case 'run': {
80
83
  const runner = require('../src/runner');
81
- await runner.run(positional[0] || null, opts);
84
+ if (opts.viewMode) {
85
+ await runner.view(positional[0] || null, opts);
86
+ } else {
87
+ await runner.run(positional[0] || null, opts);
88
+ }
82
89
  break;
83
90
  }
84
91
  case 'setup': {
@@ -188,21 +188,23 @@ flowchart TB
188
188
 
189
189
  | Session 类型 | systemPrompt | user prompt | 触发条件 |
190
190
  |---|---|---|---|
191
- | **编码** | CLAUDE.md | `buildCodingPrompt()` + 6 个条件 hint | 主循环每次迭代 |
191
+ | **编码** | CLAUDE.md | `buildCodingPrompt()` + 8 个条件 hint | 主循环每次迭代 |
192
192
  | **扫描** | CLAUDE.md + SCAN_PROTOCOL.md | `buildScanPrompt()` + 任务分解指导 | 首次运行 |
193
193
  | **观测** | CLAUDE.md (± SCAN_PROTOCOL.md) | `buildViewPrompt()` | `claude-coder view` |
194
194
  | **追加** | CLAUDE.md | `buildAddPrompt()` + 任务分解指导 | `claude-coder add` |
195
195
 
196
- ### 编码 Session 的 6 个条件 Hint
196
+ ### 编码 Session 的 8 个条件 Hint
197
197
 
198
- | Hint | 触发条件 | 影响 |
199
- |---|---|---|
200
- | `reqSyncHint` | 需求 hash 变化 | Step 1:追加新任务 |
201
- | `mcpHint` | MCP_PLAYWRIGHT=true | Step 5:可用 Playwright |
202
- | `testHint` | tests.json 有记录 | Step 5:避免重复验证 |
203
- | `docsHint` | profile.existing_docs 非空 | Step 4:读文档后再编码,完成后更新文档 |
204
- | `envHint` | 连续成功且 session>1 | Step 2:跳过 init |
205
- | `retryContext` | 上次校验失败 | 全局:避免同样错误 |
198
+ | # | Hint | 触发条件 | 影响 |
199
+ |---|---|---|---|
200
+ | 1 | `reqSyncHint` | 需求 hash 变化 | Step 1:追加新任务 |
201
+ | 2 | `mcpHint` | MCP_PLAYWRIGHT=true | Step 5:可用 Playwright |
202
+ | 3 | `testHint` | tests.json 有记录 | Step 5:避免重复验证 |
203
+ | 4 | `docsHint` | profile.existing_docs 非空 | Step 4:读文档后再编码,完成后更新文档 |
204
+ | 5 | `envHint` | 连续成功且 session>1 | Step 2:跳过 init |
205
+ | 6 | `retryContext` | 上次校验失败 | 全局:避免同样错误 |
206
+ | 7 | `taskHint` | tasks.json 存在且有待办任务 | Step 1:跳过读取 tasks.json,harness 已注入当前任务上下文 |
207
+ | 8 | `memoryHint` | session_result.json 存在且有历史记录 | Step 1:跳过读取 session_result.json,harness 已注入上次会话摘要 |
206
208
 
207
209
  ---
208
210
 
@@ -270,7 +272,7 @@ sequenceDiagram
270
272
  | 维度 | 评分 | 说明 |
271
273
  |------|------|------|
272
274
  | **CLAUDE.md 系统提示** | 8/10 | U 型注意力设计;铁律清晰;状态机和 6 步流程是核心竞争力 |
273
- | **动态 prompt** | 8/10 | 5 个条件 hint 精准注入,不浪费 token |
275
+ | **动态 prompt** | 8.5/10 | 8 个条件 hint 精准注入,含 task/memory 上下文注入,减少 Agent 冗余 Read 调用 |
274
276
  | **SCAN_PROTOCOL.md** | 8.5/10 | 新旧项目分支完整,profile 格式全面 |
275
277
  | **tests.json 设计** | 7.5/10 | 精简字段,核心目的(防反复测试)明确 |
276
278
  | **注入时机** | 9/10 | 静态规则 vs 动态上下文分离干净 |
@@ -278,14 +280,123 @@ sequenceDiagram
278
280
 
279
281
  ---
280
282
 
281
- ## 9. 后续优化方向
283
+ ## 9. Context Injection 架构(v1.0.4+)
284
+
285
+ ### 设计原则
286
+
287
+ **Harness 准备上下文,Agent 直接执行。** Agent 不应浪费工具调用读取 harness 已知的数据。
288
+
289
+ ### 优化前后对比
290
+
291
+ ```mermaid
292
+ flowchart TD
293
+ subgraph before ["优化前:Agent 自行读取"]
294
+ A1[Agent starts] --> A2["Read tasks.json"]
295
+ A2 --> A3["Read profile.json"]
296
+ A3 --> A4["Read session_result.json"]
297
+ A4 --> A5["Read requirements.md"]
298
+ A5 --> A6["Read tests.json"]
299
+ A6 --> A7["开始编码(5+ Read 调用浪费)"]
300
+ end
301
+
302
+ subgraph after ["优化后:Harness 注入上下文"]
303
+ B1["Harness 预读文件"] --> B2["注入 Hint 7: 任务上下文"]
304
+ B1 --> B3["注入 Hint 8: 会话记忆"]
305
+ B2 --> B4["Agent prompt 就绪"]
306
+ B3 --> B4
307
+ B4 --> B5["Agent 直接开始编码"]
308
+ end
309
+ ```
310
+
311
+ ### Hint 7: 任务上下文注入
312
+
313
+ Harness 在 `buildCodingPrompt()` 中预读 `tasks.json`,将下一个待办任务的 id、description、category、steps 数量和整体进度注入 user prompt。Agent 无需自行读取 `tasks.json`。
314
+
315
+ ### Hint 8: 会话记忆注入
316
+
317
+ Harness 在 `buildCodingPrompt()` 中预读 `session_result.json`,将上次会话的 task_id、结果和 notes 摘要注入 user prompt。Agent 无需自行读取历史 session 数据。
318
+
319
+ ### Loop Detection(编辑死循环检测)
320
+
321
+ PreToolUse hook 中追踪每个文件的编辑次数。当同一文件被 Write/Edit 超过 5 次时,hook 返回 `decision: "block"` 阻止操作并提示 Agent 重新审视方案。
322
+
323
+ ### 文件权限模型
324
+
325
+ | 文件 | 写入方 | Agent 权限 |
326
+ |------|--------|-----------|
327
+ | `progress.json` | Harness | 只读 |
328
+ | `sync_state.json` | Harness | 只读 |
329
+ | `session_result.json` | Agent 写 `current`,Harness 归档到 `history` | 写 `current` |
330
+ | `tasks.json` | Agent(仅 `status` 字段) | 修改 `status` |
331
+ | `project_profile.json` | Agent(仅扫描阶段) | 扫描时写入 |
332
+
333
+ ---
334
+
335
+ ## 10. Claude Agent SDK V1/V2 对比与迁移计划
336
+
337
+ 当前使用 **V1 稳定 API**(`query()`),V2 为 preview 状态(`unstable_` 前缀)。
338
+
339
+ ### V1 vs V2 API 对比
340
+
341
+ | 维度 | V1 `query()` | V2 `send()/stream()` |
342
+ |------|-------------|---------------------|
343
+ | **状态** | 稳定,生产可用 | `unstable_` 前缀,preview |
344
+ | **入口函数** | `query({ prompt, options })` | `unstable_v2_createSession(opts)` / `unstable_v2_prompt()` |
345
+ | **多轮会话** | 需手动管理 AsyncGenerator | `session.send()` + `session.stream()`,更简洁 |
346
+ | **会话恢复** | `options.resume: sessionId` | `unstable_v2_resumeSession(id)` |
347
+ | **Hooks** | `options.hooks: { PreToolUse, PostToolUse, ... }` | 未支持 |
348
+ | **Subagents** | `options.agents: { name: AgentDefinition }` | 未支持 |
349
+ | **Session Fork** | `options.forkSession: true` | 未支持 |
350
+ | **Plugins** | `options.plugins: [{ type, path }]` | 未支持 |
351
+ | **结构化输出** | `options.outputFormat: { type: 'json_schema', schema }` | 支持 |
352
+ | **文件检查点** | `options.enableFileCheckpointing + rewindFiles()` | 未明确 |
353
+ | **Cost Tracking** | `SDKResultMessage.total_cost_usd` | `SDKResultMessage.total_cost_usd` |
354
+ | **权限控制** | `canUseTool`, `permissionMode`, `allowedTools`, `disallowedTools` | 继承 |
355
+
356
+ ### 当前实现使用的 V1 特性
357
+
358
+ ```javascript
359
+ query({
360
+ prompt,
361
+ options: {
362
+ systemPrompt, // 注入 CLAUDE.md
363
+ allowedTools, // 工具白名单
364
+ permissionMode: 'bypassPermissions',
365
+ allowDangerouslySkipPermissions: true,
366
+ model, // 从 .env 传入
367
+ env, // 环境变量透传
368
+ settingSources: ['project'], // 加载项目 CLAUDE.md
369
+ hooks: { PreToolUse: [...] }, // 实时 spinner 监控
370
+ }
371
+ })
372
+ ```
373
+
374
+ ### V2 迁移条件(等待稳定后)
375
+
376
+ 1. V2 去掉 `unstable_` 前缀,正式发布
377
+ 2. V2 支持 Hooks(当前项目依赖 PreToolUse 做 spinner 和 activity log)
378
+ 3. V2 支持 Subagents(未来可能用于扫描 Agent / 编码 Agent 分离)
379
+
380
+ ### 可利用但尚未使用的 V1 特性
381
+
382
+ | 特性 | 说明 | 优先级 |
383
+ |------|------|--------|
384
+ | `maxBudgetUsd` | SDK 内置成本上限,替代自研追踪 | P0 |
385
+ | `effort` | 控制思考深度(`low`/`medium`/`high`/`max`) | P1 |
386
+ | `enableFileCheckpointing` | 文件操作检查点,比 git reset 更精细 | P1 |
387
+ | `outputFormat` | 结构化输出,让 Agent 直接输出 JSON 格式 | P1 |
388
+ | `agents` | 定义子 Agent,不同模型/工具集 | P2 |
389
+ | `betas` | 扩展上下文窗口 | P2 |
390
+
391
+ ---
392
+
393
+ ## 11. 后续优化方向
282
394
 
283
395
  ### P0 — 近期
284
396
 
285
397
  | 方向 | 说明 |
286
398
  |------|------|
287
399
  | **文件保护 Deny-list** | PreToolUse hook 拦截对保护文件的写入(比文字规则更硬性) |
288
- | **TUI 终端监控** | 基于 ANSI 的全屏界面,替代单行 spinner |
289
400
  | **成本预算控制** | `.env` 新增 `MAX_COST_USD`,超预算自动停止 |
290
401
 
291
402
  ### P1 — 中期
@@ -301,6 +412,7 @@ sequenceDiagram
301
412
 
302
413
  | 方向 | 说明 |
303
414
  |------|------|
415
+ | **TUI 终端监控** | 基于 ANSI 的全屏界面,替代单行 spinner |
304
416
  | **Web UI 监控** | 可选插件包 `@claude-coder/web-ui` |
305
417
  | **PR/CI 集成** | Session 完成后自动创建 PR、监控 CI |
306
418
  | **Prompt A/B 测试** | 多版本 CLAUDE.md 并行对比效果 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-coder",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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/prompts.js CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const { paths, loadConfig, getRequirementsHash } = require('./config');
5
+ const { loadTasks, findNextTask, getStats } = require('./tasks');
6
+
7
+ function normalizeJson(text) {
8
+ return text.replace(/[\u201c\u201d]/g, '"').replace(/[\u2018\u2019]/g, "'");
9
+ }
5
10
 
6
11
  /**
7
12
  * Build system prompt by combining template files.
@@ -79,6 +84,35 @@ function buildCodingPrompt(sessionNum, opts = {}) {
79
84
  } catch { /* ignore */ }
80
85
  }
81
86
 
87
+ // Hint 7: Task context (harness pre-read, saves Agent 2-3 Read calls)
88
+ let taskHint = '';
89
+ try {
90
+ const taskData = loadTasks();
91
+ if (taskData) {
92
+ const next = findNextTask(taskData);
93
+ const stats = getStats(taskData);
94
+ if (next) {
95
+ taskHint = `任务上下文: ${next.id} "${next.description}" (${next.status}), ` +
96
+ `category=${next.category}, steps=${next.steps.length}步。` +
97
+ `进度: ${stats.done}/${stats.total} done, ${stats.failed} failed。` +
98
+ `第一步无需读取 tasks.json(已注入),直接确认任务后进入 Step 2。`;
99
+ }
100
+ }
101
+ } catch { /* ignore */ }
102
+
103
+ // Hint 8: Session memory (last session summary, recency zone for attention)
104
+ let memoryHint = '';
105
+ if (fs.existsSync(p.sessionResult)) {
106
+ try {
107
+ const sr = JSON.parse(normalizeJson(fs.readFileSync(p.sessionResult, 'utf8')));
108
+ const last = sr.current || (sr.history?.length ? sr.history[sr.history.length - 1] : null);
109
+ if (last?.task_id) {
110
+ memoryHint = `上次会话: ${last.task_id} → ${last.status_after || last.session_result}` +
111
+ (last.notes ? `, 要点: ${last.notes.slice(0, 100)}` : '') + '。';
112
+ }
113
+ } catch { /* ignore */ }
114
+ }
115
+
82
116
  return [
83
117
  `Session ${sessionNum}。执行 6 步流程。`,
84
118
  '效率要求:先规划后编码,完成全部编码后再统一测试,禁止编码-测试反复跳转。后端任务用 curl 验证,不启动浏览器。',
@@ -87,6 +121,8 @@ function buildCodingPrompt(sessionNum, opts = {}) {
87
121
  testHint,
88
122
  docsHint,
89
123
  envHint,
124
+ taskHint,
125
+ memoryHint,
90
126
  `完成后写入 session_result.json。${retryContext}`,
91
127
  ].filter(Boolean).join('\n');
92
128
  }
package/src/runner.js CHANGED
@@ -100,7 +100,7 @@ function appendProgress(entry) {
100
100
  const p = paths();
101
101
  let progress = { sessions: [] };
102
102
  if (fs.existsSync(p.progressFile)) {
103
- try { progress = JSON.parse(fs.readFileSync(p.progressFile, 'utf8')); } catch { /* reset */ }
103
+ try { progress = JSON.parse(fs.readFileSync(p.progressFile, 'utf8').replace(/[\u201c\u201d]/g, '"')); } catch { /* reset */ }
104
104
  }
105
105
  if (!Array.isArray(progress.sessions)) progress.sessions = [];
106
106
  progress.sessions.push(entry);
@@ -111,7 +111,7 @@ function updateSessionHistory(sessionData, sessionNum) {
111
111
  const p = paths();
112
112
  let sr = { current: null, history: [] };
113
113
  if (fs.existsSync(p.sessionResult)) {
114
- try { sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8')); } catch { /* reset */ }
114
+ try { sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8').replace(/[\u201c\u201d]/g, '"')); } catch { /* reset */ }
115
115
  if (!sr.history && sr.session_result) {
116
116
  sr = { current: sr, history: [] };
117
117
  }
package/src/session.js CHANGED
@@ -62,10 +62,11 @@ function extractResult(messages) {
62
62
  return null;
63
63
  }
64
64
 
65
- function logMessage(message, logStream) {
65
+ function logMessage(message, logStream, indicator) {
66
66
  if (message.type === 'assistant' && message.message?.content) {
67
67
  for (const block of message.message.content) {
68
68
  if (block.type === 'text' && block.text) {
69
+ if (indicator) process.stderr.write('\r\x1b[K');
69
70
  process.stdout.write(block.text);
70
71
  if (logStream) logStream.write(block.text);
71
72
  }
@@ -88,6 +89,9 @@ async function runCodingSession(sessionNum, opts = {}) {
88
89
 
89
90
  indicator.start(sessionNum);
90
91
 
92
+ const editCounts = {};
93
+ const EDIT_THRESHOLD = 5;
94
+
91
95
  try {
92
96
  const queryOpts = buildQueryOptions(config, opts);
93
97
  queryOpts.systemPrompt = systemPrompt;
@@ -96,6 +100,18 @@ async function runCodingSession(sessionNum, opts = {}) {
96
100
  matcher: '*',
97
101
  hooks: [async (input) => {
98
102
  inferPhaseStep(indicator, input.tool_name, input.tool_input);
103
+
104
+ const filePath = input.tool_input?.file_path || input.tool_input?.path || '';
105
+ if (['Write', 'Edit', 'MultiEdit'].includes(input.tool_name) && filePath) {
106
+ editCounts[filePath] = (editCounts[filePath] || 0) + 1;
107
+ if (editCounts[filePath] > EDIT_THRESHOLD) {
108
+ return {
109
+ decision: 'block',
110
+ message: `已对 ${filePath} 编辑 ${editCounts[filePath]} 次,疑似死循环。请重新审视方案后再继续。`,
111
+ };
112
+ }
113
+ }
114
+
99
115
  return {};
100
116
  }]
101
117
  }]
@@ -106,7 +122,7 @@ async function runCodingSession(sessionNum, opts = {}) {
106
122
  const collected = [];
107
123
  for await (const message of session) {
108
124
  collected.push(message);
109
- logMessage(message, logStream);
125
+ logMessage(message, logStream, indicator);
110
126
  }
111
127
 
112
128
  logStream.end();
@@ -168,7 +184,7 @@ async function runScanSession(requirement, opts = {}) {
168
184
  const collected = [];
169
185
  for await (const message of session) {
170
186
  collected.push(message);
171
- logMessage(message, logStream);
187
+ logMessage(message, logStream, indicator);
172
188
  }
173
189
 
174
190
  logStream.end();
package/src/tasks.js CHANGED
@@ -13,10 +13,16 @@ const TRANSITIONS = {
13
13
  done: [],
14
14
  };
15
15
 
16
+ function normalizeJson(text) {
17
+ return text
18
+ .replace(/[\u201c\u201d]/g, '"')
19
+ .replace(/[\u2018\u2019]/g, "'");
20
+ }
21
+
16
22
  function loadTasks() {
17
23
  const p = paths();
18
24
  if (!fs.existsSync(p.tasksFile)) return null;
19
- return JSON.parse(fs.readFileSync(p.tasksFile, 'utf8'));
25
+ return JSON.parse(normalizeJson(fs.readFileSync(p.tasksFile, 'utf8')));
20
26
  }
21
27
 
22
28
  function saveTasks(data) {
package/src/validator.js CHANGED
@@ -4,6 +4,10 @@ const fs = require('fs');
4
4
  const { execSync } = require('child_process');
5
5
  const { paths, log, getProjectRoot } = require('./config');
6
6
 
7
+ function normalizeJson(text) {
8
+ return text.replace(/[\u201c\u201d]/g, '"').replace(/[\u2018\u2019]/g, "'");
9
+ }
10
+
7
11
  function validateSessionResult() {
8
12
  const p = paths();
9
13
 
@@ -14,7 +18,7 @@ function validateSessionResult() {
14
18
 
15
19
  let data;
16
20
  try {
17
- data = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
21
+ data = JSON.parse(normalizeJson(fs.readFileSync(p.sessionResult, 'utf8')));
18
22
  } catch {
19
23
  log('error', 'session_result.json JSON 格式错误');
20
24
  return { valid: false, fatal: true, reason: 'JSON 格式错误' };
@@ -86,9 +90,9 @@ function checkTestCoverage() {
86
90
  if (!fs.existsSync(p.testsFile) || !fs.existsSync(p.sessionResult)) return;
87
91
 
88
92
  try {
89
- const sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8'));
93
+ const sr = JSON.parse(normalizeJson(fs.readFileSync(p.sessionResult, 'utf8')));
90
94
  const current = sr.current || sr;
91
- const tests = JSON.parse(fs.readFileSync(p.testsFile, 'utf8'));
95
+ const tests = JSON.parse(normalizeJson(fs.readFileSync(p.testsFile, 'utf8')));
92
96
 
93
97
  const taskId = current.task_id || '';
94
98
  const testCases = tests.test_cases || [];
@@ -175,10 +175,13 @@ pending ──→ in_progress ──→ testing ──→ done
175
175
 
176
176
  ### 第一步:恢复上下文
177
177
 
178
- 1. 批量读取以下文件(一次工具调用):`.claude-coder/project_profile.json`、`.claude-coder/tasks.json`、`.claude-coder/session_result.json`
179
- 2. 如果 `session_result.json` 不存在或 history 为空,运行 `git log --oneline -20` 补充上下文
180
- 3. 如果项目根目录存在 `requirements.md`,读取用户的详细需求和偏好(技术约束、样式要求等),作为本次会话的参考依据
181
- 4. **需求同步(条件触发)**:如果 prompt 中提示"需求已变更",读取 `requirements.md`,对比 `tasks.json`,将新增需求追加为 `pending` 任务。未提示则跳过
178
+ 1. **检查 prompt 注入的上下文**:
179
+ - 如果 prompt 中包含"任务上下文"(Hint 7),说明 harness 已注入当前任务信息,**跳过读取 tasks.json**,直接确认任务后进入第二步
180
+ - 如果 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` 补充上下文
183
+ 4. 如果项目根目录存在 `requirements.md`,读取用户的详细需求和偏好(技术约束、样式要求等),作为本次会话的参考依据
184
+ 5. **需求同步(条件触发)**:如果 prompt 中提示"需求已变更",读取 `requirements.md`,对比 `tasks.json`,将新增需求追加为 `pending` 任务。未提示则跳过
182
185
 
183
186
  ### 第二步:环境与健康检查
184
187