02 · DESIGN / Agents/BugTriager Agent

BugTriager Agent · 完整 Skill 规范

集成失败的法官。读 Integrator 给的 result.json + http-trace + 涉事 Unit 的 code/contract,决定这是哪类问题、哪些 Unit 要 reset、SDD 是否需要改。归因质量直接决定整个 L2 是否能收敛。

B

BugTriager Agent

集成失败归因 · 4 类分类 · 提议 SDD 补丁 · confidence 评分

Loop L2
Model Opus 4.7(归因质量决定 L2 收敛性)
Modes triage · resume
沙箱 全项目只读 + integration/runs/{runId}/triage.json 写
会话复用 否

① 功能需求

  1. 定位 root_cause:用一句人话说清楚错在哪。禁止「测试失败」「需要修复」式废话。
  2. 4 类归因,必选其一:
    • contract_mismatch · SDD 双方契约不齐 → 必填 proposed_sdd_patch
    • impl_bug · 代码没遵守 SDD → 不改 SDD,只 reset Unit
    • spec_ambiguous · SDD 描述歧义 → 升 L3,等人
    • infra_flake · 端口/进程/网络等环境问题 → 直接重跑
  3. 影响面:affected_units 只列需要重写的 Unit;牵连但不必动的不列。
  4. confidence 评分:0–1。< 0.5 自动升 L3,不允许 LoopController 自动 apply。
  5. Evidence 必须可验证:每条 evidence 来自原文件 grep,extract_evidence 工具校验,造假被拒。
  6. 不动任何文件除 triage.json。SDD 改由 Architect 在 patch 模式干。
  7. 不调 Architect / 其他 Agent。只产 triage.json,决定权交 LoopController。

② Skill 设计

2.1 输出沙箱(极小)

data/companies/{cid}/projects/{pid}/integration/runs/{runId}/
└── triage.json                # 唯一写入文件 ★

# 只读 (无沙箱限制,需要看全部上下文做归因):
integration/runs/{runId}/result.json
integration/runs/{runId}/process.log
integration/runs/{runId}/http-trace.ndjson
units.json
sdd/mvcp/**.html
units/*/code/**
units/*/contract.json
units/*/coder.log               # 看 Coder 之前怎么想的
units/*/runs/*/result.json      # Tester 历史
bugs.ndjson                     # 查相似历史 bug

2.2 完整 System Prompt

落 system/templates/devloop-triager.md。LoopController 注入时头部补 ## Memory + ## Failure Context(含 result.json 节选 + 涉事 Unit 列表)。

# ROLE
你是 LemonBoss DevLoop 的 BugTriager Agent。本次任务:分析集成测试失败 {runId}。
读 Integrator 写的 result.json + process.log + http-trace,加上涉事 Unit 的
code/contract/coder.log,产出 triage.json,决定下一步动作。

你的归因是 L2 自愈的核心判断。错了会让项目陷入死循环或修错地方,
所以宁可输出低 confidence 让人介入,也不要硬猜。

# OUTPUT (唯一)
integration/runs/{runId}/triage.json (M-9 schema)

字段:
  run_id              来自输入
  root_cause          string, ≥20 字, 含具体行为
  category            "contract_mismatch" | "impl_bug" | "spec_ambiguous" | "infra_flake"
  affected_units      string[], 只列需要 reset 的
  proposed_sdd_patch  object | null (仅 contract_mismatch 必填)
  confidence          number 0..1
  evidence            string[], 来自原文件的引文 (extract_evidence 校验)

# MANDATORY WORKFLOW

## step 0 · 收集证据
  - 读 result.json failures[0] 的 stack/got/expected/involves
  - 读 http-trace.ndjson 失败 case 完整调用链
  - 读 process.log 失败时间窗口前后 50 行 (用 extract_evidence 工具)
  - 对每个 involves Unit, 读 contract.json + coder.log + sdd section

## step 1 · 写 todolist (4 项)
  step-001 收集 4 类证据 (Integrator/SDD/code/历史)
  step-002 用 category_classifier 做粗判 → 自己再审一遍
  step-003 确定 affected_units (基于 involves + root_cause 精修)
  step-004 (如 contract_mismatch) 起草 proposed_sdd_patch
  step-005 写 triage.json + 自检 evidence grep-able

# CATEGORY 判定流程图

  start
    │
    ▼
  stderr 含 EADDRINUSE/ECONNREFUSED/timeout? ──yes──> infra_flake
    │ no
    ▼
  SDD 中相关行为有明确规定? ──no──> spec_ambiguous (升 L3)
    │ yes
    ▼
  code 实现与 SDD 描述一致? ──no──> impl_bug
    │ yes
    ▼
  involves 中两个 Unit 对同一契约理解不一? ──yes──> contract_mismatch
    │ no
    ▼
  unknown → 升 L3 (confidence ≤ 0.4)

# CATEGORY 详细判定准则

[contract_mismatch]
  特征: A Unit 的 contract.json 与 B Unit 的 contract.json (或 SDD) 对同一
        endpoint 的 status/body 表达不一
  典型: auth-be contract 写 "401 invalid_credentials"
        但 SDD 描述写 "403 forbidden"
        auth-fe 期望 403,所以 401 时不工作
  必填: proposed_sdd_patch (统一双方,通常修正 SDD)

[impl_bug]
  特征: SDD 明确写了 X,code 没遵守 (做了 Y 或漏做)
  典型: SDD 写 "user 不存在返 404", code 写 throw new Error()
  proposed_sdd_patch: null (不改 SDD)
  affected_units: 仅那一个

[spec_ambiguous]
  特征: SDD 没写、写得模糊、双解
  典型: SDD 没规定 token 过期后是否自动刷新
  proposed_sdd_patch: null (Architect 不该自己猜)
  confidence: 通常 ≤ 0.5 → 升 L3

[infra_flake]
  特征: 端口冲突、网络超时、进程未起、磁盘满
  典型: stderr 含 EADDRINUSE / ETIMEDOUT
  proposed_sdd_patch: null
  affected_units: [] (直接重跑就行)

# RULES (硬约束)

1. 永远不动任何文件,除 triage.json。Architect 才有改 SDD 的权限。

2. root_cause 必须含具体行为。反例(禁):
     ✗ "测试失败"
     ✗ "auth-be 有问题"
     ✗ "需要修复"
   正例:
     ✓ "auth-be 在 user 不存在时抛 TypeError 返 500,
         但 auth-fe 期望 401 (SDD 规定)"

3. evidence 列表的每条必须能在原文件 grep 到。extract_evidence 工具校验,
   命中失败 → 拒收 triage.json。

4. confidence:
     0.9+ : 证据非常清晰
     0.7-0.9: 主要证据齐,有少量推断
     0.5-0.7: 几条解释都说得通,选一个最可能的
     <0.5: 不确定,自动升 L3 (LoopController 不 auto-apply)

5. category=contract_mismatch 必填 proposed_sdd_patch,
   = impl_bug / spec_ambiguous / infra_flake 必为 null。

6. affected_units:
     contract_mismatch: 通常 ≥2 个 (双方都要重写)
     impl_bug: 通常 1 个
     spec_ambiguous: [] (等 L3 决策后才知道)
     infra_flake: []

7. 不调 call_sub_agent,不写 timeline (LoopController 接管后写)。

# SELF-CHECK
1. triage.json 通过 typebox (M-9)
2. root_cause ≥ 20 字 + 含具体行为词 (返/抛/收/写/读 之一)
3. evidence 每条用 extract_evidence 工具校验 → 全 ok=true
4. category 与 proposed_sdd_patch null 性匹配 (见 §RULES 5)
5. confidence 与 category 合理 (impl_bug 通常 >0.7, spec_ambiguous <0.6)
6. affected_units 引用都在 units.json 中

2.3 工具集

工具签名用途访问
Read(path)读全项目(只读)项目内
Grep(pattern, path)在 code/ / log 中找证据项目内
Write(path, content)仅 triage.json白名单
extract_evidence NEW(pid, evidence: string[]) → {ok, results[], failed[]}验证每条 evidence 是否能在 "{file_path}: 引文" 格式中 grep 命中—
category_classifier NEW(context) → {category, confidence, hints}启发式粗判(基于 stderr 关键词 + SDD 一致性扫描)—
diff_contract_vs_impl NEW(pid, unitId) → {matches, divergences[]}code 实际行为 vs contract.json 声明的差异—
find_similar_bug NEW(pid, signature) → Bug[]在 bugs.ndjson 中查相似历史,提升判断准度—
write_todolist · update_todo_item · update_runtime_state · timeline_append · record_memory同上持久化agent-state/

2.4 禁用

  • 不允许 Edit / Write 任何文件 除 triage.json
  • 不允许 Bash — 全 Read/Grep 即可,不需要跑命令
  • 不允许 call_sub_agent
  • 不允许造假 evidence — 必须能 grep 命中原文
  • 不允许在 root_cause 用废话(「测试失败」等被自检拒)

③ 持久化集成

同模式但会话不复用(每次新归因独立)。memory 是 BugTriager 最有价值的累积资产,典型话题:

  • "EADDRINUSE 永远是 infra_flake 不是 impl_bug"
  • "401 vs 403:401 是凭据无效,403 是有凭据但权限不够"
  • "impl_bug 出现 ≥2 次 → 升级为 spec_ambiguous(SDD 描述不够清晰)"
  • "vitest 退出码 1 但 raw.json 空 → 测试框架本身崩 = infra_flake"

④ I/O 契约

const TriagerInput = Type.Object({
  pid:        Type.String(),
  run_id:     Type.String(),       // 哪一轮集成失败
  mode:       Type.Union([Type.Literal('triage'), Type.Literal('resume')]),
  resume_token: Type.Optional(Type.String()),
  call_id:    Type.String(),
  company_id: Type.String(),
});

const TriagerOutput = Type.Object({
  ok:           Type.Boolean(),     // 总能产出,除非崩;ok=false 仅指 triage 本身有缺陷
  run_id:       Type.String(),
  triage_ref:   Type.String(),     // path to triage.json

  category:     Type.String(),
  confidence:   Type.Number(),
  affected_units: Type.Array(Type.String()),
  will_auto_apply: Type.Boolean(),  // true 当 confidence≥0.5 且 category 允许

  evidence_verified: Type.Boolean(),   // 所有 evidence grep 命中
  duration_ms:  Type.Number(),
  tokens_used:  Type.Number(),
});

⑤ 上下游传递

⬆ 上游

  • Integrator · result.json · process.log · http-trace.ndjson
  • Architect(间接)· sdd/ · units.json · integration/spec.json
  • 所有 Coder(间接)· code/** · contract.json · coder.log
  • Tester(间接)· 历史 result.json
  • BugTracker(间接)· bugs.ndjson 历史
  • LoopController · TriagerInput

⬇ 下游

  • LoopController · 读 triage.json 决定 ① apply patch 走 Architect ② 仅 reset Unit ③ 重跑 Integrator ④ 升 L3
  • Architect(如果 will_auto_apply=true)· mode=patch + triage.json 输入
  • UnitRegistry · reset affected_units
  • Human (L3)(如 confidence<0.5)· SSE human_intervention_needed
  • BugTracker(间接)· LoopController 改 bug.category 字段(unknown → 实际值)

⑥ 完整执行样例

# INPUT
{
  "pid": "p-test001",
  "run_id": "run-int-20260513-1230",
  "mode": "triage", ...
}

# 读 result.json:
#   failure: case "login-then-profile" step 1 fail
#   involves: ["auth-be","profile-be"]
#   got: { status: 200, body: { user_id: 'demo' } }
#   expected: { status: 401 }

# step 1 · 收证据
#  从 http-trace:
#    step 0: POST /api/auth/login → 200 { token: "expired-jwt..." }
#            (注意:token 是测试 fixture 写死,故意过期)
#    step 1: GET /api/profile/me with expired token → 200  ✗ 应 401

#  读 sdd/mvcp/control.html data-unit-id="profile-be":
#    
... 401 if token expired ...
# 读 profile-be/contract.json: # responses: { "200": ..., "404": ... } ← 没有 401! # 读 profile-be/code/server.js: # 没有 JWT 校验,直接信任 Authorization header,从 token 取 user_id # step 2 · category_classifier category_classifier(...) → { category: "contract_mismatch", confidence: 0.82 } // 但等等:这是 contract_mismatch 还是 impl_bug? // SDD 说要 401, contract.json 没说 → contract 与 SDD 不齐 → contract_mismatch // (而非 impl_bug:实现没遵守 contract 的话才是 impl_bug) # step 3 · affected_units = ["profile-be"] # auth-be 不动:它的契约本来就对 (返 token) # step 4 · 起草 patch proposed_sdd_patch = { target_section: "#unit-profile-be", op: "update_contract", text: "明确:接收 Authorization 头,验证 JWT,失败返 401 invalid_token" } # step 5 · 写 triage.json { "run_id": "run-int-20260513-1230", "root_cause": "profile-be 没校验 JWT 直接信任 Authorization 头,过期 token 收到 200 而非 SDD 规定的 401", "category": "contract_mismatch", "affected_units": ["profile-be"], "proposed_sdd_patch": { "target_section": "#unit-profile-be", "op": "update_contract", "text": "明确:验证 JWT,过期返 401 invalid_token" }, "confidence": 0.82, "evidence": [ "sdd/mvcp/control.html: '401 if token expired'", "profile-be/contract.json: responses 仅含 200/404,无 401", "profile-be/code/server.js:23: 无 jwt.verify 调用", "http-trace step 1: expired token returns 200 not 401" ] } # OUTPUT { "ok": true, "category": "contract_mismatch", "confidence": 0.82, "affected_units": ["profile-be"], "will_auto_apply": true, "evidence_verified": true, "duration_ms": 28_412, "tokens_used": 9_823 } # LoopController → callAgent('architect', { mode:'patch', triage:{...} })

⑦ 助手工具源码

7.1 extract-evidence.js · evidence 真伪验证

// server/services/agent-tools/extract-evidence.js · ~60 行
const fs   = require('fs');
const path = require('path');

/**
 * 验证一条 evidence 是否能在 sources 中 grep 命中。
 * evidence 格式约定: "{file_path}: {引文}"
 * 例如: "sdd/mvcp/control.html: '401 if token expired'"
 */
function verifyOne(pid, evidence) {
  const sep = evidence.indexOf(': ');
  if (sep < 0) return { ok: false, reason: 'malformed: need "path: quote"' };

  const relPath = evidence.slice(0, sep).replace(/^['"]|['"]$/g, '');
  const quote   = evidence.slice(sep + 2).replace(/^['"]|['"]$/g, '');
  const abs     = path.join(projectsDir(pid), relPath);

  if (!fs.existsSync(abs))
    return { ok: false, reason: 'file not found: ' + relPath };

  const content = fs.readFileSync(abs, 'utf8');

  // 模糊匹配:去掉过多空白后 includes 即可
  const norm = (s) => s.replace(/\s+/g, ' ').toLowerCase();
  if (norm(content).includes(norm(quote)))
    return { ok: true, hit_file: relPath };

  // 行号格式 "path:lineno: 引文" 也支持
  const lineMatch = relPath.match(/^(.+):(\d+)$/);
  if (lineMatch) {
    const lines = content.split('\n');
    const line  = lines[parseInt(lineMatch[2],10)-1] || '';
    if (norm(line).includes(norm(quote)))
      return { ok: true, hit_file: lineMatch[1], line: lineMatch[2] };
  }

  return { ok: false, reason: 'quote not found in file', quote, file: relPath };
}

function verifyAll(pid, evidenceList) {
  const results = evidenceList.map(e => ({ evidence: e, ...verifyOne(pid, e) }));
  return {
    ok: results.every(r => r.ok),
    results,
    failed: results.filter(r => !r.ok),
  };
}

module.exports = { verifyOne, verifyAll };

7.2 category-classifier.js · 启发式粗判(Agent 还要审)

// server/services/agent-tools/category-classifier.js · ~80 行
const fs = require('fs');
const path = require('path');

function classify({ resultJson, processLog, sddText, codeTexts }) {
  const hints = [];
  let score = { contract_mismatch:0, impl_bug:0, spec_ambiguous:0, infra_flake:0 };

  // 1) infra_flake 信号 (强)
  const infraSignals = [
    /EADDRINUSE/, /ECONNREFUSED/, /ETIMEDOUT/,
    /EAI_AGAIN/, /Cannot find module/,
    /port already in use/i, /health check timeout/i,
  ];
  if (infraSignals.some(re => re.test(processLog))) {
    score.infra_flake += 3;
    hints.push('infra signal in process.log');
  }

  // 2) spec_ambiguous 信号
  //    失败行为相关关键词在 sdd 中找不到 → 强信号
  const failTokens = extractKeyTokens(resultJson);   // "refresh", "token expire", etc
  const sddLc = sddText.toLowerCase();
  const tokensNotInSdd = failTokens.filter(t => !sddLc.includes(t.toLowerCase()));
  if (tokensNotInSdd.length >= 2) {
    score.spec_ambiguous += 2;
    hints.push('key tokens not found in SDD: ' + tokensNotInSdd.join(','));
  }

  // 3) contract_mismatch 信号: sdd 说一个 status, contract.json 说另一个
  //    (实现细节略)

  // 4) impl_bug 信号: sdd 明确 + contract 明确 + code 不符
  //    用 5xx in got + 4xx in expected 是强信号
  const got    = resultJson?.failures?.[0]?.got;
  const expect = resultJson?.failures?.[0]?.expect;
  if (got?.status >= 500 && expect?.status >= 400 && expect?.status < 500) {
    score.impl_bug += 2;
    hints.push('5xx got vs 4xx expected → impl_bug');
  }

  // 选最高分
  const sorted = Object.entries(score).sort(([,a],[,b]) => b-a);
  const [topCat, topScore] = sorted[0];

  // confidence 启发式
  let confidence;
  if (topScore >= 3)      confidence = 0.85;
  else if (topScore === 2) confidence = 0.65;
  else if (topScore === 1) confidence = 0.45;
  else                  confidence = 0.30;

  return { category: topCat, confidence, hints, score };
}

function extractKeyTokens(rj) {
  const txt = JSON.stringify(rj || {});
  // 去掉 stack 中的 path/random 字符,取 4 字以上小写单词
  return [...new Set(txt.toLowerCase().match(/[a-z]{4,}/g) || [])]
    .filter(t => !['function','expected','received','stack','message'].includes(t));
}

module.exports = { classify };

这是「启发式建议」工具,Agent 必须自己审一遍再写 triage.json。category_classifier 给的 confidence 仅是参考,Agent 输出的 confidence 是基于自己 reasoning 的最终判断。

⑧ 自检 Checklist

#检查项命令判定
1triage.json typebox 合规schema check (M-9)true
2root_cause ≥ 20 字lengthtrue
3root_cause 含行为动词(返/抛/收/写/读/超时之一)regex命中
4所有 evidence grep 命中verifyAll(pid, evidence)ok=true
5category × proposed_sdd_patch null 一致逻辑contract_mismatch ⇔ patch≠null
6category × confidence 合理spec_ambiguous <0.6, impl_bug typically >0.7无明显错配
7affected_units ⊆ units.jsonset 包含true
8category 非 unknown必为 4 选 1true
9todolist 全 donereadtrue
10没写 triage.json 之外的文件scanSandboxviolations=[]

任一不过 → 不写 triage.json,retry 1 次。仍不过 → 写一个 confidence=0.3 + category='spec_ambiguous' 的兜底版升 L3。

⑨ 失败模式

场景处置
evidence 引文找不到原文extract_evidence 拒;Triager 重生这条 evidence
category 选错(后续 L2 再次失败)第 2 次 triage 自动升 spec_ambiguous(避免死循环)
root_cause 是「测试失败」类废话self-check 3 拒
affected_units 空 + category=impl_bug不合法,拒;Triager 必须列至少 1 个
proposed_sdd_patch 写错 target_sectionArchitect 应用时报错,Triager 重生
读 process.log 时间窗口太大(> 10MB)extract_evidence 工具截取 failure 时间 ± 30s
同 run 已有 triage.json视为 resume 模式(取代旧版,归档到 history/)
Triager 自己崩LoopController 写一个最小 spec_ambiguous triage 兜底

⑩ 握手摘要

对方给收
LoopControllerTriagerOutputTriagerInput
Integrator(无)result.json + process.log + http-trace
Architecttriage.json(间接 via LoopController)sdd/ (只读)
UnitRegistryaffected_units(间接)—
Human (L3)spec_ambiguous 时 l3_question 字段—
BugTracker(LoopController 据 category 改 bug.category)历史相似 bug
memory/devloop/bug-triager/新归因经验启动注入

⑪ 实战验证

# 前置: Integrator 跑过一次 fail,有 integration/runs/{runId}/result.json

# 1. 注册模板
#    system/templates/devloop-triager.md ← 本页 §2.2

# 2. 准备输入
echo '{"pid":"p-test001","run_id":"run-int-20260513-1230","mode":"triage","call_id":"call-b-1","company_id":"c-test"}' > /tmp/tri.json

# 3. 跑 BugTriager
curl -X POST http://localhost:3010/api/devloop/_test/run-agent/bug-triager -d @/tmp/tri.json

# 4. 期望 20-60 秒内
#    data/companies/c-test/projects/p-test001/integration/runs/{runId}/triage.json

# 5. 验证 schema + evidence
node -e "
const fs = require('fs');
const path = 'data/companies/c-test/projects/p-test001/integration/runs/run-int-20260513-1230/triage.json';
const t = JSON.parse(fs.readFileSync(path, 'utf8'));
console.log('category:', t.category);
console.log('confidence:', t.confidence);
console.log('affected:', t.affected_units);
const ev = require('./server/services/agent-tools/extract-evidence');
console.log('evidence verify:', ev.verifyAll('p-test001', t.evidence));
"

# 6. 测三种典型场景
# 场景 A · contract_mismatch:故意让 SDD 与 contract.json 不一致 → 期望 patch 非空
# 场景 B · impl_bug:故意让 code 实现违反 SDD → 期望 patch=null
# 场景 C · infra_flake:杀掉一个 be 进程,Integrator 报 ECONNREFUSED → 期望 affected=[]

# 验证决策合理性 (人工抽查 3-5 个 triage.json)
# 关键看: 同样的 result.json 反复跑 BugTriager,category 应稳定 (Opus 4.7 的判断不能 flaky)
BugTriager §⑪ 通过 = L2 完整闭环验证完成。四个 Agent 全部 §⑪ 通过 = 整个 DevLoop 系统可执行性证毕。下一步是在真实代码里跑 P1 + P2 + P3 + P4 的实施。