Coder Agent · 完整 Skill 规范
Shell-stripped: preserves full prompt content for Agent system templates
DevLoop 的代码主力。读 SDD 内对应 Unit 的
<pre data-contract>+ 上游 Unit 的contract.json,产units/{id}/code/**+contract.json+coder.log。是 L1 内被调用频率最高的 Agent。
C
Coder Agent
写前后端代码 · 暴露契约 · 自己留决策日志
Loop L1
Model Sonnet 4.6
Modes implement · retry · resume
沙箱 units/{unitId}/ 内 code/ + contract.json + coder.log
会话复用 是(SessionPool key = coder.cid.pid.unitId)
① 功能需求
- 三种调用模式:
implement· Unit 首次写代码 2.retry· Tester 红 → 基于 failures + hint + 自己历史 coder.log 反思后重写 3.resume· 崩溃恢复 4. 读契约写代码:从 SDD 找data-unit-id={自己}section,读 contract + acceptance。每个 depends_on Unit 只读contract.json,不读它们 code/。 5. 遵循 LemonBoss 代码约定:- 后端:Express route + service 双层;DB 用
better-sqlite3(参考 memory) 6. 前端:Vue 3 Composition API + vue-i18n(key 复用既有) 7. 路径别名@server可用,不要新增别名 8. 禁止重新发明轮子(已有scheduler / process-guard等直接用) 9. 产contract.json:结构必须与 SDD 描述一致;version字段 =hash_contract(contract.exports),必须与 units.json 中该 Unit 的 contract_hash 一致。 10. 反思能力:retry 时显式说明「上次错 X,这次改 Y,因为 Z」,写入 coder.log。不允许重写时忽略 history。 11. 不写测试:Tester 的活。写测试容易写「永远绿」的 trivial case。 12. 不读、不动其他 Unit 的代码 / 契约。违反 → 沙箱拒绝。
- 后端:Express route + service 双层;DB 用
② Skill 设计
2.1 输出沙箱(严格)
data/companies/{cid}/projects/{pid}/units/{unitId}/ ├── spec.json # 只读 (Architect 写) ├── code/ # Coder 写 ★ │ ├── server.js # 后端入口 (be 类) │ ├── service.js # 业务逻辑 │ ├── schema.sql # DB DDL (含 db Unit 时) │ └── ... (Coder 自由组织) ├── contract.json # Coder 写 ★ 必须与 SDD 一致 └── coder.log # Coder 写 ★ append-only 决策日志 # Coder 可读路径(只读): sdd/mvcp/*.html # 仅自己 unit 的 section ../{depends_on Unit}/contract.json # 上游 contract,不读 code/ units.json # 只读 contract_hash 对比
2.2 完整 System Prompt(可拷贝直用)
落 system/agents/devloop-coder/prompt.md。LoopController 注入时头部补 ## Memory + ## Unit Spec(含 sdd section 节选 + 上游 contract 摘要),尾部补 ## Retry History(retry 模式时)。
# ROLE 你是 LemonBoss DevLoop 的 Coder Agent。本次任务:实现 Unit {unitId}。 读 SDD 内的 Unit spec + contract,写代码到 ./code/,写 contract.json, 记录自己的决策到 coder.log。 # MODES - "implement" Unit 首次实现 - "retry" 上次 Tester 红了,基于 history 反思后重写 - "resume" 从 resume_token 继续 # SANDBOX (强约束 - 越界自动拒绝) 你**只能**写: ./code/** ./contract.json ./coder.log 禁止写: ./tests/** (Tester 的领地) ../{别的 unit}/** (其他 Unit 的领地) ../../sdd/** (Architect 的领地) ../../prd/** (PM 的领地) ../../units.json (Architect/UnitRegistry 的) 你可读: ./spec.json (Architect 写的本 Unit 元数据) ../../sdd/mvcp/*.html (只看自己 unit 的 section) ../{depends_on}/contract.json (上游 contract,不读它们 code/) ./coder.log (retry 时反思自己历史) ./runs/{最新}/result.json (retry 时看 Tester failures) # MANDATORY WORKFLOW ## step 0 · 读上下文 - implement: 读 spec.json + sdd section + 全部上游 contract.json - retry: 上面全部 + 读自己 coder.log + 最近 runs/{}/result.json - resume: 由 LoopController 注入 resume context ## step 1 · write_todolist ### implement 典型 (6 项): step-001 解析 SDD 的 Unit spec + 上游 contracts step-002 设计 code/ 文件结构 (后端: server + service + (schema if db);前端: component + composable) step-003 实现核心逻辑 step-004 写 contract.json (结构匹配 SDD) step-005 调 hash_contract → 验证 version 与 units.json 中 contract_hash 一致 step-006 append coder.log 决策摘要 + 自检 sandbox ### retry 典型 (5 项): step-001 读 failures + hint + 自己 coder.log step-002 反思: 上次错 X, 真正原因 Y, 这次改 Z step-003 精准 patch code/(改最小范围) step-004 如有契约调整 → 同步更新 contract.json + 重新 hash step-005 append coder.log 新决策 (explicit:"上次 X,这次 Y,因 Z") # RULES (硬约束) 1. 任何 curl/带 JSON body 的 Bash 命令 → 先写 /tmp/*.json 再 -d @file 参见 CLAUDE.md L91-99。jsonGuard 中间件会拦截 inline JSON 自动拒绝。 反例(禁): curl -d '{"k":"v"}' http://... 正例: echo '{"k":"v"}' > /tmp/x.json && curl -d @/tmp/x.json http://... 2. contract.json 必须先调 hash_contract 工具计算 version。 不要手算 sha256。算出来的 version 必须 === units.json 中本 Unit 的 contract_hash。 如不一致 → 你产的 contract 跟 SDD 不匹配,必须改 code/ 让它匹配。 3. retry 时 coder.log 必须含 "上次 X 这次 Y 因 Z" 结构,不允许只写"修了 bug"。 4. 不读其他 Unit 的 code/。只读 contract.json。 反例(禁): cat ../profile-be/code/server.js 正例: cat ../profile-be/contract.json 5. 后端 Unit 必须用 better-sqlite3 (memory 一致)。不要 mysql/postgres/mongodb。 6. 前端 Unit 用 Vue 3 Composition API + script setup,不用 Options API。 7. 写完每个 step 立即 update_todo_item, 不要批处理。 8. 不调 call_sub_agent。Coder 不分派。 # 反思模板 (retry 时强制用) 在 coder.log 写新条目时,用这个模板: --- attempt: 2 ts: 2026-05-13T10:32:08Z last_failure: login.invalid-pwd 期望 401 收 500 last_cause: 没处理 user 不存在,直接 bcrypt.compare(null,...) 抛 TypeError this_fix: service.js login() 先 SELECT user, user 缺失或 pwd 错统一 return 401 rationale: 401 是"凭据无效"的语义,不该用 500 (服务器错误) 暴露用户不存在 changed_files: [code/auth-service.js] --- # SELF-CHECK (完工前必跑) 1. contract.json hash 与 units.json[unitId].contract_hash 一致 2. code/ 下文件能 node --check 通过 (be); 前端能 vite build dry-run 通过 (fe) 3. coder.log 这次有新一条决策 (implement 1 条, retry 模板格式) 4. 没有写 code/ 沙箱外的文件 5. todolist 全 done # OUTPUT VOICE - 代码风格遵循 LemonBoss 既有 (扫 server/services/* 仿) - 不写大段注释,只在 WHY 非显然时一行 - 不写"完美""强大",数字胜过形容词 - coder.log 用人话不是 emoji
2.3 工具集(含签名)
| 工具 | 签名 | 用途 | 访问边界 | | --- | --- | --- | --- | | Read | (path) | 读 spec/sdd/上游 contract/历史 log | 沙箱白名单 | | Edit | (path, old, new) | 精准修改(retry 时常用) | code/** + contract.json | | Write | (path, content) | 新文件 | code/** + contract.json + coder.log | | Bash | (cmd) | 跑 npm install · lint · syntax check · curl | cwd 锁 units/{unitId}/, jsonGuard 强制 | | hash_contract NEW | (contract) → "sha256:..." | 同 Architect 工具,复用 | — | | contract_bridge NEW | (pid, unitId) → {ok, expected, actual} | 对比 contract.json 与 units.json 中 hash | — | | sandbox_check NEW | (pid, unitId) → {ok, violations[]} | 扫所有本 Unit 文件确认无越界 | — | | append_coder_log NEW | (decision: object) | 结构化追加 coder.log(含 attempt/last_failure/this_fix 字段) | coder.log only | | write_todolist | (items) | 同 PM/Architect | agent-state/ | | update_todo_item | (id, status, output_ref?) | 同上 | — | | update_runtime_state | (step, total) | 同上 | — | | timeline_append | (kind, ref, summary) | 同上 · 典型 kind: unit_coded · code_patched | — | | record_memory | (topic, content, relevance) | 同上 · memory/devloop/coder/ | memory/devloop/coder/ |
2.4 禁用的工具 / 行为
- 不允许 call_sub_agent — Coder 不分派
- 不允许 inline JSON Bash — jsonGuard 强制拦截
- 不允许写:tests/ · ../别的 unit/ · sdd/ · prd/ · units.json · integration/
- 不允许读其他 Unit 的 code/(仅 contract.json)
- 不允许跳过反思 — retry 模式 coder.log 必含 "last_failure/this_fix" 字段
③ 持久化集成
与 PM/Architect 同模式:开工前 LoopController 注入 memory,结束前可写新 memory(少用,Coder memory 通常是代码风格偏好)。todolist + runtime + timeline 同步。
retry 模式的特殊性:
history字段由 LoopController 注入到 prompt 尾部,含上一次 failure + hint + 旧 coder.log 摘要。Coder 不主动读 history,从 prompt 拿。Coder memory 典型话题:
- "never use callback style, prefer async/await"
- "401 vs 500: 凭据问题 401, 服务器内部错 500"
- "sqlite better-sqlite3 同步 API, 不要 await db.query"
④ I/O 契约
const CoderInput = Type.Object({ pid: Type.String(), unit_id: Type.String(), mode: Type.Union([Type.Literal('implement'), Type.Literal('retry'), Type.Literal('resume')]), // retry 模式由 LoopController 注入 history: Type.Optional(Type.Array(Type.Object({ attempt: Type.Number(), my_decision: Type.String(), // 上次的反思条目 tester_said: Type.Array(Type.Any()), // 上次 failures }))), reflect_hint: Type.Optional(Type.String()), // Tester 给的 hint resume_token: Type.Optional(Type.String()), call_id: Type.String(), company_id: Type.String(), }); const CoderOutput = Type.Object({ ok: Type.Boolean(), mode: Type.String(), unit_id: Type.String(), attempt: Type.Number(), files_written: Type.Array(Type.String()), contract_hash: Type.String(), contract_match_sdd: Type.Boolean(), decision: Type.Object({ // 本次 coder.log 主条目摘要 last_failure: Type.Optional(Type.String()), this_fix: Type.String(), rationale: Type.String(), changed_files: Type.Array(Type.String()), }), sandbox_violations: Type.Array(Type.String()), // 必为 [] duration_ms: Type.Number(), tokens_used: Type.Number(), });⑤ 上下游传递
⬆ 上游
- Architect · 通过 spec.json + sdd section
- 上游 Unit 的 Coder(已完成)· 通过 ../{dep}/contract.json
- LoopController · 注入 input + memory + (retry 时) history
- Tester(retry 时间接)· 通过 runs/{}/result.json failures
⬇ 下游
- Tester · 读 code/ + contract.json 写测试
- 下游 Unit 的 Coder · 读我的 contract.json(不读 code)
- Integrator(L2 时)· 起 code/ 真实进程跑 e2e
- UnitRegistry · 接 status: coding → testing
- BugTriager(L2 失败时)· 读 code/ + contract.json + coder.log 做归因
⑥ 完整执行样例
Sample A · implement (todo-be) Sample B · retry (反思后重写) Sample C · resume (崩溃恢复)
# ─────────── INPUT ─────────── { "pid": "p-test001", "unit_id": "todo-be", "mode": "implement", "call_id": "call-c-001", "company_id": "c-test" } # INJECTED MEMORY ## Memory (from prior projects) ### sqlite-conventions (relevance: 0.93) - better-sqlite3 是同步 API,不要 await - prepared statement 优先于字符串拼接 - 写在 service 层,不直接在 route 用 ### http-status (relevance: 0.88) - 400 客户端语法错; 401 凭据无效; 404 不存在; 409 冲突 - 201 Created 含 Location header # INJECTED Unit Spec (摘自 sdd/mvcp/control.html) ## Unit Spec: todo-be contract: POST /api/todos body: { title: string (1..200) } → 201 { id, title, created_at } → 400 { error: "title_required" } | { error: "title_too_long" } GET /api/todos → 200 { items: [{ id, title, created_at }, ...] } acceptance: - 创建后能列出 - 空 title 返 400 - 200+ 字符 title 返 400 # ─────────── 内部执行 ─────────── # step 1 · write_todolist (implement 6 项) # step 2 · 设计文件结构 # code/server.js Express app + route # code/service.js 业务逻辑 + DB # code/schema.sql DDL (schema 简单,内联在 service) # step 3 · Write code/service.js const Database = require('better-sqlite3'); const db = new Database(':memory:'); db.exec(`CREATE TABLE todos (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP)`); const insertStmt = db.prepare('INSERT INTO todos (title) VALUES (?)'); const listStmt = db.prepare('SELECT id, title, created_at FROM todos ORDER BY id DESC'); function create(title) { if (!title || title.length === 0) return { error: 'title_required' }; if (title.length > 200) return { error: 'title_too_long' }; const r = insertStmt.run(title); return listStmt.get(r.lastInsertRowid) || { id: r.lastInsertRowid, title }; } module.exports = { create, list: () => ({ items: listStmt.all() }) }; # step 3 · Write code/server.js const express = require('express'); const svc = require('./service'); const app = express(); app.use(express.json()); app.post('/api/todos', (req, res) => { const r = svc.create(req.body.title); if (r.error) return res.status(400).json(r); res.status(201).json(r); }); app.get('/api/todos', (req, res) => res.json(svc.list())); module.exports = app; # step 4 · Write contract.json { "unit_id": "todo-be", "version": "sha256:4a7c8e2f9d1b3a5e", "exports": { "http": [ { "method":"POST", "path":"/api/todos", "request":{"title":"string"}, "responses":{ "201":{"id":"number","title":"string","created_at":"string"}, "400":{"error":"\"title_required\" | \"title_too_long\""} } }, { "method":"GET", "path":"/api/todos", ... } ] }, "consumes": [] } # step 5 · contract_bridge → { ok: true, expected: "sha256:4a7c8e2f9d1b3a5e", actual: 同 } # step 6 · append_coder_log append_coder_log({ attempt: 1, this_fix: "首次实现 todo-be:Express + better-sqlite3 内存库 + 双端点", rationale: "按 SDD 拆 server/service 双层; 内存库便于 Tester 跑 vitest", changed_files: ["code/server.js", "code/service.js"] }) # OUTPUT { "ok": true, "mode": "implement", "unit_id": "todo-be", "attempt": 1, "files_written": ["code/server.js", "code/service.js", "contract.json", "coder.log"], "contract_hash": "sha256:4a7c8e2f9d1b3a5e", "contract_match_sdd": true, "decision": { "this_fix": "首次实现...", ... }, "sandbox_violations": [], "duration_ms": 38_412, "tokens_used": 5_120 }# ─────────── INPUT (retry mode) ─────────── { "pid": "p-test001", "unit_id": "todo-be", "mode": "retry", "history": [ { "attempt": 1, "my_decision": "用 better-sqlite3 内存库 + 双端点", "tester_said": [ { "test":"empty title", "expected":400, "got":500, "stack":"TypeError: title.length undefined" } ] } ], "reflect_hint": "title 字段缺失时 req.body.title 是 undefined,先判存在性", ... } # INJECTED Retry History (LoopController 拼到 prompt 尾部) ## Retry History attempt 1 决策: "用 better-sqlite3 内存库 + 双端点" attempt 1 失败: - test "empty title" 期望 400 收 500 - stack: TypeError: title.length undefined Tester 提示: "title 字段缺失时 req.body.title 是 undefined,先判存在性" # step 1 · todolist 5 项 (retry 模板) # step 2 · 反思 # 上次错: service.create(title) 内 if (!title || title.length === 0) # 当 title=undefined 时 title.length 抛 TypeError # 真正原因: undefined 没短路到 !title (空字符串才会), # 但 || 链对 undefined 是 truthy 之前先取 .length 抛错 # 错!实际上 !undefined 是 true → 短路返 title_required # 重新看 stack: route 层先解构 body.title,如果 body 整个缺失则报错 # ✓ 真正错: req.body 是 undefined (express.json() 没匹配 content-type) # step 3 · Edit code/server.js # - 兜底: const { title } = req.body || {}; # step 5 · append_coder_log append_coder_log({ attempt: 2, ts: "2026-05-13T10:32:08Z", last_failure: "empty title 期望 400 收 500", last_cause: "req.body 为 undefined 时解构 title 抛 TypeError", this_fix: "route 层 req.body || {} 兜底,再传入 service", rationale: "express.json() 对非 application/json 请求不解析,body 是 undefined", changed_files: ["code/server.js"] }) # OUTPUT { "ok": true, "mode": "retry", "attempt": 2, "files_written": ["code/server.js", "coder.log"], "contract_hash": "sha256:4a7c8e2f9d1b3a5e", // hash 不变 (只改实现) "contract_match_sdd": true, "decision": { "last_failure": "empty title 期望 400 收 500", "this_fix": "route 层 req.body || {} 兜底", "rationale": "express.json() 对非 JSON 请求 body 是 undefined", "changed_files": ["code/server.js"] }, ... }# ─────────── INPUT (resume) ─────────── { "pid": "p-test001", "unit_id": "todo-be", "mode": "resume", "resume_token": "rt-coder-...-step3", ... } # LoopController 已注入 agent-state/coder.json + coder-todo.json # 上次进度: step-001/002/003 done, step-004 doing 时崩 # code/server.js, code/service.js 已写 # contract.json 未写 # Coder 自动续: update_runtime_state(current_step=3, total_steps=6) # 从 step-004 继续 # step-004: 写 contract.json # step-005: contract_bridge 验证 # step-006: append_coder_log + sandbox check # OUTPUT (同 Sample A 但 files_written 不含已写的)⑦ 助手工具源码
7.1
contract-bridge.js· contract.json ↔ units.json 一致性// server/services/agent-tools/contract-bridge.js · ~50 行 const fs = require('fs'); const path = require('path'); const { hashContract } = require('./hash-contract'); function verifyContractBridge(pid, unitId) { const projDir = projectsDir(pid); const contractPath = path.join(projDir, 'units', unitId, 'contract.json'); const unitsPath = path.join(projDir, 'units.json'); if (!fs.existsSync(contractPath)) return { ok: false, reason: 'contract.json missing' }; const contract = JSON.parse(fs.readFileSync(contractPath, 'utf8')); const units = JSON.parse(fs.readFileSync(unitsPath, 'utf8')); const unit = units.units.find(u => u.id === unitId); if (!unit) return { ok: false, reason: 'unit not in units.json' }; const expected = unit.contract_hash; const recomputed = hashContract(contract); // 还检查 contract.version 自报与重算一致 if (contract.version !== recomputed) { return { ok: false, reason: 'contract.version self-inconsistent', claimed: contract.version, actual: recomputed, }; } if (recomputed !== expected) { return { ok: false, reason: 'contract does not match SDD-declared hash', expected, actual: recomputed, hint: '你的 contract 与 SDD 描述不一致,改 code 让契约对齐', }; } return { ok: true, hash: recomputed }; } module.exports = { verifyContractBridge };7.2
sandbox-check.js· 沙箱越界扫描// server/services/agent-tools/sandbox-check.js · ~60 行 const fs = require('fs'); const path = require('path'); /** * 给定 (pid, unitId, since:timestamp) → 扫描自 since 起被 mtime 修改过的文件, * 任何在沙箱外的写盘 → violations。 * 由 LoopController 在 callAgent 结束后调用。 */ function scanSandbox({ pid, agent, unitId, since }) { const projDir = projectsDir(pid); const allowed = [ path.join(projDir, 'units', unitId, 'code'), path.join(projDir, 'units', unitId, 'contract.json'), path.join(projDir, 'units', unitId, 'coder.log'), path.join(projDir, 'agent-state'), // runtime/todolist 写允许 path.join(projDir, 'timeline.ndjson'), ]; const violations = []; walk(projDir, (f) => { const st = fs.statSync(f); if (st.mtimeMs < since) return; const within = allowed.some(a => f.startsWith(a)); if (!within) violations.push(f); }); return { ok: violations.length === 0, violations }; } function walk(dir, cb) { for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const p = path.join(dir, e.name); if (e.isDirectory()) walk(p, cb); else cb(p); } } module.exports = { scanSandbox };7.3
append-coder-log.js· 结构化决策日志// server/services/agent-tools/append-coder-log.js · ~30 行 const fs = require('fs'); const path = require('path'); function appendCoderLog(pid, unitId, decision) { const required = ['attempt', 'this_fix', 'rationale', 'changed_files']; if (decision.attempt > 1) required.push('last_failure', 'last_cause'); const missing = required.filter(k => !decision[k]); if (missing.length) throw new Error(`coder.log decision missing fields: ${missing.join(',')}`); const p = path.join(projectsDir(pid), 'units', unitId, 'coder.log'); const block = '---\n' + Object.entries(decision) .map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`) .join('\n') + '\n---\n\n'; fs.appendFileSync(p, block); return { ok: true }; } module.exports = { appendCoderLog };⑧ 自检 Checklist
# 检查项 命令 / 工具 通过判定 1 contract.json hash 一致 verifyContractBridge(pid, unitId)ok=true 2 code/ 文件能 parse be: node --check code/server.js;fe: vite dry-run0 退出 3 coder.log 新条目 file mtime > call start true 4 retry 模式 coder.log 含 last_failure / this_fix regex 校验最末条目 命中 5 沙箱无越界 scanSandboxviolations=[] 6 不读其他 Unit code/ session log review 无 ../{xxx-(be 7 todolist 全 done read agent-state/coder-todo.json 全 done 8 timeline 含 unit_coded tail timeline.ndjson 命中 9 agent-state.coder.json 终态 read M-10 status=done 10 无 Bash inline JSON jsonGuard 中间件实时拦截 (运行时无遗漏) 0 拒绝事件 任一不过 → 不输出 ok=true。retry 最多 2 次内必过。否则报 validationfailed。_
⑨ 失败模式与降级
场景 检测 降级 试图写 ../别的 unit/ Write 工具沙箱拒绝 Coder 收 error,换路径 Bash inline JSON jsonGuard 拦截 提示参考 CLAUDE.md L91-99 contract.json hash 与 SDD 不匹配 contract_bridge 视为 fail;Coder 必须改 code 让契约对齐 反复同样错误 ≥ 3 次 retry_budget 累计 UnitRegistry.block + SSE 求人 code/ 文件 parse 失败 self-check 步骤 retry 一次重写 retry 模式 coder.log 缺反思字段 appendCoderLog 内置校验 抛 error,Coder 必须补 读取 sdd 时找不到自己的 section cheerio grep 报 SDD 不一致,触发 L3 让 Architect 重生 memory 注入太多挤爆 prompt LoopController 控制 top 5 — npm install 超时 Bash 工具超时(10min) retry;持续失败标 infra_flake ⑩ 与其他 Agent 的握手摘要
对方 Coder 给的 Coder 收的 Architect (无) spec.json + sdd section 上游 Unit Coder (无) contract.json(只读) Tester code/** + contract.json retry 时 failures + hint 下游 Unit Coder contract.json (无) Integrator code/** (运行时进程) (无) BugTriager code/** + contract.json + coder.log (只读) (无) LoopController CoderOutput JSON CoderInput · history (retry) memory/devloop/coder/ 新 .md 启动注入 ⑪ 实战验证命令
# 前置: Architect 已跑通,units.json 含 todo-be unit, sdd/ 含 todo-be section # 1. 注册模板 # system/agents/devloop-coder/prompt.md ← 本页 §2.2 # system/agents/_registry.json ← 加 'devloop-coder' # 2. 准备输入 echo '{"pid":"p-test001","unit_id":"todo-be","mode":"implement","call_id":"call-coder-1","company_id":"c-test"}' > /tmp/coder-impl.json # 3. 调 Coder (implement) curl -X POST http://localhost:3010/api/devloop/_test/run-agent/coder \ -H "Content-Type: application/json" \ -d @/tmp/coder-impl.json # 4. 期望:30-60 秒内 # data/companies/c-test/projects/p-test001/units/todo-be/ # ├── code/server.js, code/service.js # ├── contract.json (hash 与 units.json 一致) # └── coder.log (1 个 --- block) # 5. 三方验证 node -e "console.log(require('./server/services/agent-tools/contract-bridge').verifyContractBridge('p-test001', 'todo-be'))" # 期望: { ok: true, hash: 'sha256:...' } node -e "console.log(require('./server/services/agent-tools/sandbox-check').scanSandbox({pid:'p-test001',unitId:'todo-be',since:Date.now()-300000}))" # 期望: { ok: true, violations: [] } node --check data/companies/c-test/projects/p-test001/units/todo-be/code/server.js # 期望: 0 退出码 # 6. 模拟 retry (假装 Tester 已经红了) echo '{"pid":"p-test001","unit_id":"todo-be","mode":"retry","history":[{"attempt":1,"my_decision":"...","tester_said":[{"test":"x","expected":400,"got":500,"stack":"TypeError..."}]}],"reflect_hint":"req.body 可能 undefined","call_id":"call-coder-2","company_id":"c-test"}' > /tmp/coder-retry.json curl -X POST http://localhost:3010/api/devloop/_test/run-agent/coder -d @/tmp/coder-retry.json # 期望: coder.log 多一个 block,含 last_failure / this_fix / rationale 字段 # code/server.js 被 Edit 过 (mtime > 上次) # contract_hash 不变 (除非真的契约改了)这一组全过 = Coder skill 验证通过。下一步 Tester Agent 跑同 Unit 检测能不能让代码红/绿,关上 L1 反思闭环。