02 · DESIGN / Agents/Coder Agent

Coder Agent · 完整 Skill 规范

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)

① 功能需求

  1. 三种调用模式:
    • implement · Unit 首次写代码
    • retry · Tester 红 → 基于 failures + hint + 自己历史 coder.log 反思后重写
    • resume · 崩溃恢复
  2. 读契约写代码:从 SDD 找 data-unit-id={自己} section,读 contract + acceptance。每个 depends_on Unit 只读 contract.json,不读它们 code/。
  3. 遵循 LemonBoss 代码约定:
    • 后端:Express route + service 双层;DB 用 better-sqlite3(参考 memory)
    • 前端:Vue 3 Composition API + vue-i18n(key 复用既有)
    • 路径别名 @server 可用,不要新增别名
    • 禁止重新发明轮子(已有 scheduler / process-guard 等直接用)
  4. 产 contract.json:结构必须与 SDD 描述一致;version 字段 = hash_contract(contract.exports),必须与 units.json 中该 Unit 的 contract_hash 一致。
  5. 反思能力:retry 时显式说明「上次错 X,这次改 Y,因为 Z」,写入 coder.log。不允许重写时忽略 history。
  6. 不写测试:Tester 的活。写测试容易写「永远绿」的 trivial case。
  7. 不读、不动其他 Unit 的代码 / 契约。违反 → 沙箱拒绝。

② 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/templates/devloop-coder.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 · curlcwd 锁 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/Architectagent-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 做归因

⑥ 完整执行样例

# ─────────── 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
}

⑦ 助手工具源码

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

#检查项命令 / 工具通过判定
1contract.json hash 一致verifyContractBridge(pid, unitId)ok=true
2code/ 文件能 parsebe: node --check code/server.js;fe: vite dry-run0 退出
3coder.log 新条目file mtime > call starttrue
4retry 模式 coder.log 含 last_failure / this_fixregex 校验最末条目命中
5沙箱无越界scanSandboxviolations=[]
6不读其他 Unit code/session log review无 ../{xxx-(be|fe|mw|db)}/code 路径出现
7todolist 全 doneread agent-state/coder-todo.json全 done
8timeline 含 unit_codedtail timeline.ndjson命中
9agent-state.coder.json 终态read M-10status=done
10无 Bash inline JSONjsonGuard 中间件实时拦截 (运行时无遗漏)0 拒绝事件

任一不过 → 不输出 ok=true。retry 最多 2 次内必过。否则报 validation_failed。

⑨ 失败模式与降级

场景检测降级
试图写 ../别的 unit/Write 工具沙箱拒绝Coder 收 error,换路径
Bash inline JSONjsonGuard 拦截提示参考 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 时找不到自己的 sectioncheerio grep报 SDD 不一致,触发 L3 让 Architect 重生
memory 注入太多挤爆 promptLoopController 控制 top 5—
npm install 超时Bash 工具超时(10min)retry;持续失败标 infra_flake

⑩ 与其他 Agent 的握手摘要

对方Coder 给的Coder 收的
Architect(无)spec.json + sdd section
上游 Unit Coder(无)contract.json(只读)
Testercode/** + contract.jsonretry 时 failures + hint
下游 Unit Codercontract.json(无)
Integratorcode/** (运行时进程)(无)
BugTriagercode/** + contract.json + coder.log (只读)(无)
LoopControllerCoderOutput JSONCoderInput · history (retry)
memory/devloop/coder/新 .md启动注入

⑪ 实战验证命令

# 前置: Architect 已跑通,units.json 含 todo-be unit, sdd/ 含 todo-be section

# 1. 注册模板
#    system/templates/devloop-coder.md  ← 本页 §2.2
#    system/templates/_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 反思闭环。