agents-triager.md 23.3 KB

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

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 类归因,必选其一:
  3. contract_mismatch · SDD 双方契约不齐 → 必填 proposed_sdd_patch 3. impl_bug · 代码没遵守 SDD → 不改 SDD,只 reset Unit 4. spec_ambiguous · SDD 描述歧义 → 升 L3,等人 5. infra_flake · 端口/进程/网络等环境问题 → 直接重跑 6. 影响面:affected_units 只列需要重写的 Unit;牵连但不必动的不列。 7. confidence 评分:0–1。< 0.5 自动升 L3,不允许 LoopController 自动 apply。 8. Evidence 必须可验证:每条 evidence 来自原文件 grep,extract_evidence 工具校验,造假被拒。 9. 不动任何文件除 triage.json。SDD 改由 Architect 在 patch 模式干。 10. 不调 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/agents/devloop-triager/prompt.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
    • BugTracker(间接)· LoopController 改 bug.category 字段(unknown → 实际值)

    ⑥ 完整执行样例

    Sample A · contract_mismatch Sample B · impl_bug Sample C · spec_ambiguous (升 L3)

    # 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":
    #    <pre data-contract>... 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.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='specambiguous' 的兜底版升 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 的实施。