# BugTriager Agent · 完整 Skill 规范 > _Shell-stripped: preserves full prompt content for Agent system templates_ > 集成失败的法官。读 Integrator 给的 result.json + http-trace + 涉事 Unit 的 code/contract,决定**这是哪类问题、哪些 Unit 要 reset、SDD 是否需要改**。归因质量直接决定整个 L2 是否能收敛。 B
... 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:{...} })
```
# INPUT 同样,但实际场景不同
# 读发现:
# sdd 明确写: "user 不存在返 404 with {error:'user_not_found'}"
# contract.json 也写: responses: { "200": ..., "404": ... } ✓
# code/server.js: user 查不到时 throw new Error('not found') → 500
# 这是**实现没遵守 SDD**,不是契约不齐
# triage.json
{
"run_id": "...",
"root_cause": "profile-be 在 user 不存在时抛 throw 错误返 500,SDD 已规定该返 404 user_not_found",
"category": "impl_bug", // 不是 contract_mismatch
"affected_units": ["profile-be"],
"proposed_sdd_patch": null, // impl_bug 不改 SDD
"confidence": 0.93, // 证据非常清晰
"evidence": [
"sdd/mvcp/control.html: '404 with {error:user_not_found}'",
"profile-be/contract.json responses 404 已声明",
"profile-be/code/server.js:41: throw new Error('not found')",
"http-trace step 1: got 500"
]
}
# LoopController → 不改 SDD, 只 unitRegistry.reset(['profile-be'])
# → L1 重跑 profile-be (Coder 反思: SDD 写 404 我做了 throw, 这次改)
```
```
# INPUT 同样
# 读发现:
# 失败 case: token 过期 fe 应自动刷新, 但 SDD 没说
# sdd 关于 token 续期: 没找到 → 翻 prd: 也没说
# auth-fe/code: 实现了自动刷新, 但没说收到 401 后等多久重试
# auth-be/code: 接收 refresh_token 也行, 没接收也行 (双解)
# 这种"双方都说得通,SDD 没规定"的情况 = spec_ambiguous
# triage.json
{
"run_id": "...",
"root_cause": "SDD 未规定 token 过期后 fe 是否自动刷新及刷新策略,auth-fe 实现刷新 1 次后放弃,auth-be 同时接收 refresh_token 与重新登录两种方式",
"category": "spec_ambiguous",
"affected_units": [], // 等人决策后才知道
"proposed_sdd_patch": null,
"confidence": 0.42, // < 0.5 → 升 L3
"evidence": [
"sdd/mvcp/control.html: 搜 'refresh' 0 命中",
"prd/product/scope.html: 同上 0 命中",
"auth-fe/code/auth-service.js:67: setTimeout 重试 1 次",
"auth-be/code/server.js:34: 接收 grant_type=refresh_token"
],
"l3_question": "Token 过期后:(A) fe 自动刷新 ≤3 次 (B) 提示用户重新登录 (C) 静默续期不感知"
}
# LoopController: confidence<0.5 + category=spec_ambiguous
# → SSE human_intervention_needed with l3_question
# → 不动 Unit,不改 SDD,等人
```
## ⑦ 助手工具源码
### 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
| # | 检查项 | 命令 | 判定 |
| --- | --- | --- | --- |
| 1 | triage.json typebox 合规 | schema check (M-9) | true |
| 2 | root_cause ≥ 20 字 | length | true |
| 3 | root_cause 含行为动词(返/抛/收/写/读/超时之一) | regex | 命中 |
| 4 | 所有 evidence grep 命中 | `verifyAll(pid, evidence)` | ok=true |
| 5 | category × proposed_sdd_patch null 一致 | 逻辑 | contract_mismatch ⇔ patch≠null |
| 6 | category × confidence 合理 | spec_ambiguous <0.6, impl_bug typically >0.7 | 无明显错配 |
| 7 | affected_units ⊆ units.json | set 包含 | true |
| 8 | category 非 unknown | 必为 4 选 1 | true |
| 9 | todolist 全 done | read | true |
| 10 | 没写 triage.json 之外的文件 | scanSandbox | violations=[] |
> _任一不过 → 不写 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_section | Architect 应用时报错,Triager 重生 |
| 读 process.log 时间窗口太大(> 10MB) | extract_evidence 工具截取 failure 时间 ± 30s |
| 同 run 已有 triage.json | 视为 resume 模式(取代旧版,归档到 history/) |
| Triager 自己崩 | LoopController 写一个最小 spec_ambiguous triage 兜底 |
## ⑩ 握手摘要
| 对方 | 给 | 收 |
| --- | --- | --- |
| **LoopController** | TriagerOutput | TriagerInput |
| **Integrator** | (无) | result.json + process.log + http-trace |
| **Architect** | triage.json(间接 via LoopController) | sdd/ (只读) |
| **UnitRegistry** | affected_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/agents/devloop-triager/prompt.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 的实施。