claude-coder 1.0.3 → 1.0.5

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.
@@ -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,7 +280,59 @@ sequenceDiagram
278
280
 
279
281
  ---
280
282
 
281
- ## 9. Claude Agent SDK V1/V2 对比与迁移计划
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 对比与迁移计划
282
336
 
283
337
  当前使用 **V1 稳定 API**(`query()`),V2 为 preview 状态(`unstable_` 前缀)。
284
338
 
@@ -336,14 +390,13 @@ query({
336
390
 
337
391
  ---
338
392
 
339
- ## 10. 后续优化方向
393
+ ## 11. 后续优化方向
340
394
 
341
395
  ### P0 — 近期
342
396
 
343
397
  | 方向 | 说明 |
344
398
  |------|------|
345
399
  | **文件保护 Deny-list** | PreToolUse hook 拦截对保护文件的写入(比文字规则更硬性) |
346
- | **TUI 终端监控** | 基于 ANSI 的全屏界面,替代单行 spinner |
347
400
  | **成本预算控制** | `.env` 新增 `MAX_COST_USD`,超预算自动停止 |
348
401
 
349
402
  ### P1 — 中期
@@ -359,6 +412,7 @@ query({
359
412
 
360
413
  | 方向 | 说明 |
361
414
  |------|------|
415
+ | **TUI 终端监控** | 基于 ANSI 的全屏界面,替代单行 spinner |
362
416
  | **Web UI 监控** | 可选插件包 `@claude-coder/web-ui` |
363
417
  | **PR/CI 集成** | Session 完成后自动创建 PR、监控 CI |
364
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.3",
3
+ "version": "1.0.5",
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,17 @@
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 safeJsonParse(text) {
8
+ try {
9
+ return JSON.parse(text);
10
+ } catch {
11
+ return JSON.parse(
12
+ text.replace(/[\u201c\u201d]/g, '"').replace(/[\u2018\u2019]/g, "'")
13
+ );
14
+ }
15
+ }
5
16
 
6
17
  /**
7
18
  * Build system prompt by combining template files.
@@ -79,6 +90,35 @@ function buildCodingPrompt(sessionNum, opts = {}) {
79
90
  } catch { /* ignore */ }
80
91
  }
81
92
 
93
+ // Hint 7: Task context (harness pre-read, saves Agent 2-3 Read calls)
94
+ let taskHint = '';
95
+ try {
96
+ const taskData = loadTasks();
97
+ if (taskData) {
98
+ const next = findNextTask(taskData);
99
+ const stats = getStats(taskData);
100
+ if (next) {
101
+ taskHint = `任务上下文: ${next.id} "${next.description}" (${next.status}), ` +
102
+ `category=${next.category}, steps=${next.steps.length}步。` +
103
+ `进度: ${stats.done}/${stats.total} done, ${stats.failed} failed。` +
104
+ `第一步无需读取 tasks.json(已注入),直接确认任务后进入 Step 2。`;
105
+ }
106
+ }
107
+ } catch { /* ignore */ }
108
+
109
+ // Hint 8: Session memory (last session summary, recency zone for attention)
110
+ let memoryHint = '';
111
+ if (fs.existsSync(p.sessionResult)) {
112
+ try {
113
+ const sr = safeJsonParse(fs.readFileSync(p.sessionResult, 'utf8'));
114
+ const last = sr.current || (sr.history?.length ? sr.history[sr.history.length - 1] : null);
115
+ if (last?.task_id) {
116
+ memoryHint = `上次会话: ${last.task_id} → ${last.status_after || last.session_result}` +
117
+ (last.notes ? `, 要点: ${last.notes.slice(0, 100)}` : '') + '。';
118
+ }
119
+ } catch { /* ignore */ }
120
+ }
121
+
82
122
  return [
83
123
  `Session ${sessionNum}。执行 6 步流程。`,
84
124
  '效率要求:先规划后编码,完成全部编码后再统一测试,禁止编码-测试反复跳转。后端任务用 curl 验证,不启动浏览器。',
@@ -87,6 +127,8 @@ function buildCodingPrompt(sessionNum, opts = {}) {
87
127
  testHint,
88
128
  docsHint,
89
129
  envHint,
130
+ taskHint,
131
+ memoryHint,
90
132
  `完成后写入 session_result.json。${retryContext}`,
91
133
  ].filter(Boolean).join('\n');
92
134
  }
package/src/runner.js CHANGED
@@ -100,7 +100,10 @@ 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').replace(/[\u201c\u201d]/g, '"')); } catch { /* reset */ }
103
+ try {
104
+ const text = fs.readFileSync(p.progressFile, 'utf8');
105
+ try { progress = JSON.parse(text); } catch { progress = JSON.parse(text.replace(/[\u201c\u201d]/g, '"')); }
106
+ } catch { /* reset */ }
104
107
  }
105
108
  if (!Array.isArray(progress.sessions)) progress.sessions = [];
106
109
  progress.sessions.push(entry);
@@ -111,7 +114,10 @@ function updateSessionHistory(sessionData, sessionNum) {
111
114
  const p = paths();
112
115
  let sr = { current: null, history: [] };
113
116
  if (fs.existsSync(p.sessionResult)) {
114
- try { sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8').replace(/[\u201c\u201d]/g, '"')); } catch { /* reset */ }
117
+ try {
118
+ const text = fs.readFileSync(p.sessionResult, 'utf8');
119
+ try { sr = JSON.parse(text); } catch { sr = JSON.parse(text.replace(/[\u201c\u201d]/g, '"')); }
120
+ } catch { /* reset */ }
115
121
  if (!sr.history && sr.session_result) {
116
122
  sr = { current: sr, history: [] };
117
123
  }
package/src/session.js CHANGED
@@ -89,6 +89,9 @@ async function runCodingSession(sessionNum, opts = {}) {
89
89
 
90
90
  indicator.start(sessionNum);
91
91
 
92
+ const editCounts = {};
93
+ const EDIT_THRESHOLD = 5;
94
+
92
95
  try {
93
96
  const queryOpts = buildQueryOptions(config, opts);
94
97
  queryOpts.systemPrompt = systemPrompt;
@@ -97,6 +100,18 @@ async function runCodingSession(sessionNum, opts = {}) {
97
100
  matcher: '*',
98
101
  hooks: [async (input) => {
99
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
+
100
115
  return {};
101
116
  }]
102
117
  }]
package/src/tasks.js CHANGED
@@ -13,16 +13,20 @@ 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, "'");
16
+ function safeJsonParse(text) {
17
+ try {
18
+ return JSON.parse(text);
19
+ } catch {
20
+ return JSON.parse(
21
+ text.replace(/[\u201c\u201d]/g, '"').replace(/[\u2018\u2019]/g, "'")
22
+ );
23
+ }
20
24
  }
21
25
 
22
26
  function loadTasks() {
23
27
  const p = paths();
24
28
  if (!fs.existsSync(p.tasksFile)) return null;
25
- return JSON.parse(normalizeJson(fs.readFileSync(p.tasksFile, 'utf8')));
29
+ return safeJsonParse(fs.readFileSync(p.tasksFile, 'utf8'));
26
30
  }
27
31
 
28
32
  function saveTasks(data) {
package/src/validator.js CHANGED
@@ -4,8 +4,14 @@ 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, "'");
7
+ function safeJsonParse(text) {
8
+ try {
9
+ return JSON.parse(text);
10
+ } catch {
11
+ return JSON.parse(
12
+ text.replace(/[\u201c\u201d]/g, '"').replace(/[\u2018\u2019]/g, "'")
13
+ );
14
+ }
9
15
  }
10
16
 
11
17
  function validateSessionResult() {
@@ -18,7 +24,7 @@ function validateSessionResult() {
18
24
 
19
25
  let data;
20
26
  try {
21
- data = JSON.parse(normalizeJson(fs.readFileSync(p.sessionResult, 'utf8')));
27
+ data = safeJsonParse(fs.readFileSync(p.sessionResult, 'utf8'));
22
28
  } catch {
23
29
  log('error', 'session_result.json JSON 格式错误');
24
30
  return { valid: false, fatal: true, reason: 'JSON 格式错误' };
@@ -90,9 +96,9 @@ function checkTestCoverage() {
90
96
  if (!fs.existsSync(p.testsFile) || !fs.existsSync(p.sessionResult)) return;
91
97
 
92
98
  try {
93
- const sr = JSON.parse(normalizeJson(fs.readFileSync(p.sessionResult, 'utf8')));
99
+ const sr = safeJsonParse(fs.readFileSync(p.sessionResult, 'utf8'));
94
100
  const current = sr.current || sr;
95
- const tests = JSON.parse(normalizeJson(fs.readFileSync(p.testsFile, 'utf8')));
101
+ const tests = safeJsonParse(fs.readFileSync(p.testsFile, 'utf8'));
96
102
 
97
103
  const taskId = current.task_id || '';
98
104
  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