@tea-agent/loop-agent 0.25.2 → 0.25.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,77 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { frontendTestResultContractSchema } from "./frontend-test-result-contract.js";
4
+ function escapeHtml(value) {
5
+ return String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6
+ }
7
+ function statusLabel(status) {
8
+ return status === "passed" ? "通过" : status === "failed" ? "失败" : "阻塞";
9
+ }
10
+ function listMarkdown(items) {
11
+ return items.length ? items.map((item, index) => `${index + 1}. ${item}`).join("\n") : "- 无";
12
+ }
13
+ function listHtml(items) {
14
+ return items.length ? `<ol>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ol>` : "<p>无</p>";
15
+ }
16
+ async function writePairAtomic(markdownPath, markdown, htmlPath, html) {
17
+ await mkdir(path.dirname(markdownPath), { recursive: true });
18
+ const nonce = `${process.pid}-${Date.now()}`;
19
+ const markdownTemp = `${markdownPath}.${nonce}.tmp`;
20
+ const htmlTemp = `${htmlPath}.${nonce}.tmp`;
21
+ try {
22
+ await writeFile(markdownTemp, markdown, "utf8");
23
+ await writeFile(htmlTemp, html, "utf8");
24
+ await rename(markdownTemp, markdownPath);
25
+ await rename(htmlTemp, htmlPath);
26
+ }
27
+ finally {
28
+ await rm(markdownTemp, { force: true });
29
+ await rm(htmlTemp, { force: true });
30
+ }
31
+ }
32
+ export async function renderFrontendTestHtmlReport(input) {
33
+ const resultPath = path.join(input.runDir, "contracts", "frontend-test-result.json");
34
+ const result = frontendTestResultContractSchema.parse(JSON.parse(await readFile(resultPath, "utf8")));
35
+ const outputDir = path.join(input.workspaceRoot, "testcase", "frontend", "reports");
36
+ const markdownPath = path.join(outputDir, "frontend-test-report.md");
37
+ const htmlPath = path.join(outputDir, "frontend-test-report.html");
38
+ const outcomeLabel = result.outcome === "passed" ? "测试通过" : result.outcome === "failed" ? "测试失败" : "测试未完成";
39
+ const markdownCases = result.cases.map((item) => [
40
+ `## ${item.caseId}`,
41
+ "",
42
+ `- 执行结果:${statusLabel(item.status)}`,
43
+ "",
44
+ "### 测试目的",
45
+ "",
46
+ item.caseContent.purpose,
47
+ "",
48
+ "### 前置条件",
49
+ "",
50
+ listMarkdown(item.caseContent.preconditions),
51
+ "",
52
+ "### 操作步骤",
53
+ "",
54
+ listMarkdown(item.caseContent.steps),
55
+ "",
56
+ "### 预期结果",
57
+ "",
58
+ listMarkdown(item.caseContent.expectedResults),
59
+ ...(item.status === "passed" ? [] : ["", "### 错误分析", "", item.errorAnalysis ?? `用例因 ${item.blockedReason ?? "未知原因"} 未能完成。`]),
60
+ ].join("\n")).join("\n\n");
61
+ const markdown = [
62
+ "# 前端功能测试报告",
63
+ "",
64
+ `- 测试结论:${outcomeLabel}`,
65
+ `- 用例总数:${result.totals.cases}`,
66
+ `- 通过:${result.totals.passed}`,
67
+ `- 失败:${result.totals.failed}`,
68
+ `- 阻塞:${result.totals.blocked}`,
69
+ "",
70
+ markdownCases,
71
+ "",
72
+ ].join("\n");
73
+ const htmlCases = result.cases.map((item) => `<section class="case ${item.status}"><header><h2>${escapeHtml(item.caseId)}</h2><span class="status">${statusLabel(item.status)}</span></header><h3>测试目的</h3><p>${escapeHtml(item.caseContent.purpose)}</p><h3>前置条件</h3>${listHtml(item.caseContent.preconditions)}<h3>操作步骤</h3>${listHtml(item.caseContent.steps)}<h3>预期结果</h3>${listHtml(item.caseContent.expectedResults)}${item.status === "passed" ? "" : `<h3>错误分析</h3><p class="error">${escapeHtml(item.errorAnalysis ?? `用例因 ${item.blockedReason ?? "未知原因"} 未能完成。`)}</p>`}</section>`).join("");
74
+ const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><title>前端功能测试报告</title><style>body{font:16px system-ui,"Microsoft YaHei",sans-serif;margin:0;background:#f5f7fb;color:#172033}main{max-width:1100px;margin:auto;padding:32px}.summary,.case{background:#fff;border-radius:14px;padding:22px;margin:16px 0;box-shadow:0 6px 24px rgba(16,24,40,.07)}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.metric{background:#f8fafc;padding:14px;border-radius:10px}.case header{display:flex;justify-content:space-between;gap:16px}.status{font-weight:700}.passed .status{color:#067647}.failed .status,.error{color:#b42318}.blocked .status{color:#946200}li{margin:.45rem 0}@media(max-width:700px){.metrics{grid-template-columns:1fr 1fr}}</style></head><body><main><h1>前端功能测试报告</h1><section class="summary"><h2>${outcomeLabel}</h2><div class="metrics"><div class="metric">用例总数<br><strong>${result.totals.cases}</strong></div><div class="metric">通过<br><strong>${result.totals.passed}</strong></div><div class="metric">失败<br><strong>${result.totals.failed}</strong></div><div class="metric">阻塞<br><strong>${result.totals.blocked}</strong></div></div></section>${htmlCases}</main></body></html>`;
75
+ await writePairAtomic(markdownPath, markdown, htmlPath, html);
76
+ return { markdownPath, htmlPath, outcome: result.outcome, caseCount: result.cases.length };
77
+ }
@@ -33,6 +33,13 @@ export const frontendTestResultContractSchema = z.object({
33
33
  status: caseStatusSchema,
34
34
  evidence: z.array(z.object({ path: safeRelativePathSchema, sha256: sha256Schema }).strict()),
35
35
  blockedReason: z.string().min(1).optional(),
36
+ caseContent: z.object({
37
+ purpose: z.string(),
38
+ preconditions: z.array(z.string()),
39
+ steps: z.array(z.string()),
40
+ expectedResults: z.array(z.string()),
41
+ }).strict().default({ purpose: "未提供测试目的", preconditions: [], steps: [], expectedResults: [] }),
42
+ errorAnalysis: z.string().min(1).optional(),
36
43
  }).strict()).min(1),
37
44
  advisoryFindings: z.array(z.object({
38
45
  ruleId: z.string().min(1),
@@ -176,6 +183,32 @@ export async function validateFrontendCaseEvidence(input) {
176
183
  function sha256(content) {
177
184
  return createHash("sha256").update(content).digest("hex");
178
185
  }
186
+ function markdownSection(body, names) {
187
+ const escaped = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
188
+ const marker = new RegExp(`^###\\s+(?:${escaped})\\s*$`, "mi");
189
+ const hit = marker.exec(body);
190
+ if (!hit)
191
+ return "";
192
+ const rest = body.slice(hit.index + hit[0].length);
193
+ const next = /^###\s+/m.exec(rest);
194
+ return rest.slice(0, next?.index ?? rest.length).trim();
195
+ }
196
+ function markdownList(value) {
197
+ const items = value.split(/\r?\n/).map((line) => line.replace(/^\s*(?:\d+[.)]|[-*+])\s+/, "").trim()).filter(Boolean);
198
+ return items;
199
+ }
200
+ async function readFrontendCaseContent(workspaceRoot, casePath) {
201
+ if (!safeRelativePathSchema.safeParse(casePath).success || !casePath.startsWith("testcase/frontend/cases/")) {
202
+ throw new Error(`unsafe frontend case path: ${casePath}`);
203
+ }
204
+ const body = await readFile(path.resolve(workspaceRoot, casePath), "utf8");
205
+ return {
206
+ purpose: markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) || "未提供测试目的",
207
+ preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件"])),
208
+ steps: markdownList(markdownSection(body, ["Steps", "操作步骤"])),
209
+ expectedResults: markdownList(markdownSection(body, ["Expected Results", "预期结果"])),
210
+ };
211
+ }
179
212
  function assertInside(root, candidate, label) {
180
213
  const relative = path.relative(root, candidate);
181
214
  if (relative.startsWith("..") || path.isAbsolute(relative)) {
@@ -285,7 +318,17 @@ export async function materializeFrontendTestResult(input) {
285
318
  }
286
319
  if (status === "passed" && !evidence.some((entry) => /\.(png|jpg|jpeg|webp|zip|har|webm|mp4|ya?ml|md)$/i.test(entry.path)))
287
320
  advisoryFindings.push({ ruleId: "passed-without-browser-evidence", caseId: item.caseId, detail: "passed case has no browser evidence" });
288
- cases.push({ caseId: item.caseId, acIds: item.acIds, status, evidence, ...(status === "blocked" && typeof resultRaw.blockedReason === "string" && resultRaw.blockedReason.trim() ? { blockedReason: resultRaw.blockedReason } : {}) });
321
+ const caseContent = await readFrontendCaseContent(input.workspaceRoot, item.casePath);
322
+ const blockedReason = status === "blocked" && typeof resultRaw.blockedReason === "string" && resultRaw.blockedReason.trim() ? resultRaw.blockedReason.trim() : undefined;
323
+ const explicitAnalysis = typeof resultRaw.errorAnalysis === "string" && resultRaw.errorAnalysis.trim()
324
+ ? resultRaw.errorAnalysis.trim()
325
+ : typeof resultRaw.errorSummary === "string" && resultRaw.errorSummary.trim()
326
+ ? resultRaw.errorSummary.trim()
327
+ : undefined;
328
+ const errorAnalysis = status === "passed"
329
+ ? undefined
330
+ : explicitAnalysis ?? (status === "blocked" ? `用例因 ${blockedReason ?? "未知原因"} 未能完成执行。` : "用例执行失败,但执行结果未提供详细错误分析。");
331
+ cases.push({ caseId: item.caseId, acIds: item.acIds, status, evidence, caseContent, ...(blockedReason ? { blockedReason } : {}), ...(errorAnalysis ? { errorAnalysis } : {}) });
289
332
  }
290
333
  const missing = [...requiredIds].filter((id) => !covered.has(id));
291
334
  const totals = {
@@ -3470,7 +3470,7 @@ async function buildBackendTestHybridDag(sources) {
3470
3470
  "Read the upstream environment report. Generate a Markdown-first backend test strategy and cases under testcase/md/**.",
3471
3471
  "Write human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.",
3472
3472
  "Create testcase/md/README.md as the concise entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in README.",
3473
- "Write each module as readable case cards. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. The only sections required by the deterministic validator are `### 前置条件`, `### 操作步骤`, and `### 预期结果` (legacy English aliases remain accepted). Add `测试目的`, `验收标准`, `需求依据`, `测试数据`, and `自动化映射` when useful for human readability; every automatable case should explicitly name its target pytest script under `自动化映射` so traceability can scan only that script.",
3473
+ "Write each module as readable case cards. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`) and never a bare number. The only sections required by the deterministic validator are `### 前置条件`, `### 操作步骤`, and `### 预期结果` (legacy English aliases remain accepted). Add `测试目的`, `验收标准`, `需求依据`, `测试数据`, and `自动化映射` when useful for human readability; every automatable case should explicitly name its target pytest script under `自动化映射` so traceability can scan only that script.",
3474
3474
  "Name each module file with a stable lowercase business stem such as `testcase/md/health.md` or `testcase/md/resource_notes.md`. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
3475
3475
  "Place steps and their expected results in a compact readable table when that improves clarity; otherwise keep numbered executable steps and numbered/bulleted independently assertable results. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.",
3476
3476
  "In `自动化映射`, record the planned script path and pytest function name when known, and keep the script path identical to the module one-to-one path above. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
@@ -3487,7 +3487,7 @@ async function buildBackendTestHybridDag(sources) {
3487
3487
  subtask_prompt: [
3488
3488
  "Independently review generated Markdown cases against the task requirements and environment evidence. Treat the files as human-facing test documentation: require clear preconditions, executable steps and assertable expected results; improve names, purpose, metadata and automation mapping where useful while preserving exact machine IDs and technical literals.",
3489
3489
  "Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), and missing or drifted script/function mapping where it can be derived.",
3490
- "Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, fix automation mappings so each case points at `testcase/test_<module>.py` derived from that module filename, merge duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations exact. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
3490
+ "Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`) consistently across headings/index/mappings, fix automation mappings so each case points at `testcase/test_<module>.py` derived from that module filename, merge duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
3491
3491
  "Read only precise referenced source paths plus requirement sections needed for uncovered ACs. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
3492
3492
  intake.boundedSourceContext, "## Authoritative reference index", JSON.stringify(intake.referenceIndex, null, 2),
3493
3493
  "For each index entry, use `readPath` for Pi read-tool calls and keep `path` as the exact Markdown Source References citation. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.",
@@ -3515,7 +3515,7 @@ async function buildBackendTestHybridDag(sources) {
3515
3515
  'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
3516
3516
  'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
3517
3517
  ].join("; ");
3518
- const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html plus final Markdown case metadata without rerun. Keep 测试结论, quality status, failure overview, and a polished per-case result card with concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md, a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON), and an optional contracts/code-coverage-v1.json when jacocoCoverage is configured (JaCoCo TCP dump → jacoco.xml → parsed; failure-safe); exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
3518
+ const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html plus final Markdown case metadata without rerun. Keep 测试结论 and quality status; make node 4 Markdown validation and node 6 Markdown-to-pytest traceability expandable to their full escaped details; show each failure overview item with its original pytest message plus deterministic evidence-based reason analysis; list failure/error case cards before the remaining cases while preserving stable order. Each polished per-case result card includes concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md, a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON), and an optional contracts/code-coverage-v1.json when jacocoCoverage is configured (JaCoCo TCP dump → jacoco.xml → parsed; failure-safe); exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
3519
3519
  if (execute.shell) {
3520
3520
  execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
3521
3521
  }
@@ -3729,7 +3729,7 @@ function buildFrontendTestHybridDag(sources) {
3729
3729
  commands: [
3730
3730
  [
3731
3731
  "node -e",
3732
- JSON.stringify("const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const patterns=[/baseUrl\\s*[:=]\\s*(https?:\\/\\/\\S+)/i,/base[- ]url\\s*[:=]\\s*(https?:\\/\\/\\S+)/i,/playwright-cli open --browser=chrome --headed\\s+(https?:\\/\\/\\S+)/i,/(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\}\\],\\\"']*)/i];let baseUrl=null;for(const re of patterns){const m=s.match(re);if(m){baseUrl=m[1];break;}}if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl (prefer config.md; default http://localhost:5173)');baseUrl=baseUrl.replace(/[)\\}\\],.\\\"']+$/,'');if(!/^https?:\\/\\//i.test(baseUrl))throw new Error('baseUrl must be absolute http(s): '+baseUrl);if(/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i.test(baseUrl))throw new Error('production URL forbidden: '+baseUrl);const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrlRedacted:safe,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated baseUrl='+safe+' probe=reachable method='+used+' httpStatus='+statusNum);"),
3732
+ JSON.stringify("const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const patterns=[/baseUrl\\s*[:=]\\s*(https?:\\/\\/\\S+)/i,/base[- ]url\\s*[:=]\\s*(https?:\\/\\/\\S+)/i,/playwright-cli open --browser=chrome --headed\\s+(https?:\\/\\/\\S+)/i,/(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\}\\],\\\"']*)/i];let baseUrl=null;for(const re of patterns){const m=s.match(re);if(m){baseUrl=m[1];break;}}if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl (prefer config.md; default http://localhost:5173)');baseUrl=baseUrl.replace(/[)\\}\\],.\\\"'\\x60]+$/,'');if(!/^https?:\\/\\//i.test(baseUrl))throw new Error('baseUrl must be absolute http(s): '+baseUrl);if(/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i.test(baseUrl))throw new Error('production URL forbidden: '+baseUrl);const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrlRedacted:safe,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated baseUrl='+safe+' probe=reachable method='+used+' httpStatus='+statusNum);"),
3733
3733
  ].join(" "),
3734
3734
  ],
3735
3735
  cwd: ".",
@@ -3847,7 +3847,7 @@ function buildFrontendTestHybridDag(sources) {
3847
3847
  forbiddenPaths: forbidden,
3848
3848
  outputContract: "Mechanical checklist: manifest/case paths, Case ID, non-production playwright-cli open prefix, and AC mapping; alternative executable tool commands are not inspected or blocked.",
3849
3849
  subtask_prompt: "Scan generated cases/manifest for structural and safety rules only. Strongly recommend playwright-cli for browser execution, but do not inspect or reject alternative executable tool commands and do not use free-form LLM verdicts.",
3850
- shell: { commands: [checklistValidation], cwd: ".", timeoutMs: 120000 },
3850
+ shell: { commands: [], frontendTestCaseChecklist: {}, cwd: ".", timeoutMs: 120000 },
3851
3851
  }, {
3852
3852
  id: "materialize-frontend-case-manifest-shell",
3853
3853
  depends_on: ["frontend-case-checklist-shell"],
@@ -3981,7 +3981,7 @@ function buildFrontendTestHybridDag(sources) {
3981
3981
  subtask_prompt: "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization (do not wait for outcome=pass). Combine AC→case→browser-evidence review with the closeout report: coverage, passed/failed/blocked (including token-budget-exhausted / executor-auth-unavailable), evidence gaps, browser anomalies, residual risks, and A/B/C/D rating. Passed cases need assertion plus screenshot or equivalent evidence when available; failed/blocked need explicit reasons. Blocked cases never count as passed. Do not replace browser evidence with model conclusions. Do not write docs/**. Pipeline success is report production, not case full green.",
3982
3982
  });
3983
3983
  tasks.push({
3984
- id: "frontend-test-quality-report-html-shell",
3984
+ id: "frontend-test-html-report-shell",
3985
3985
  depends_on: ["frontend-test-retrospect-pi"],
3986
3986
  role: "verifier",
3987
3987
  executor: "shell",
@@ -3990,9 +3990,9 @@ function buildFrontendTestHybridDag(sources) {
3990
3990
  writeSet: ["testcase/frontend/reports/**"],
3991
3991
  allowedPaths: ["testcase/frontend/**"],
3992
3992
  forbiddenPaths: forbidden,
3993
- outputContract: "Write a user-readable HTML advisory report after the final Markdown retrospective; findings never block the workflow.",
3994
- subtask_prompt: "Generate the frontend case quality advisory Markdown and HTML report after the final test report. Record content findings only and never fail because findings exist.",
3995
- shell: { commands: [frontendCaseQualityAdvisory], cwd: ".", timeoutMs: 120000 },
3993
+ outputContract: "Write testcase/frontend/reports/frontend-test-report.md and frontend-test-report.html from frontend-test-result-v1, containing only case execution results, case content, and failed/blocked error analysis.",
3994
+ subtask_prompt: "Render the formal frontend test Markdown and HTML report from the current run frontend-test-result-v1. Do not include evidence chains, evidence paths or hashes, advisory findings, quality suggestions, improvement suggestions, or ratings.",
3995
+ shell: { commands: [], frontendTestHtmlReport: {}, cwd: ".", timeoutMs: 120000 },
3996
3996
  });
3997
3997
  const globalConstraints = [
3998
3998
  ...sources.taskConfig.hardConstraints,
@@ -281,6 +281,8 @@ export const dagBackendTestPipelineSchema = z.enum([
281
281
  "markdown-execute-html",
282
282
  ]);
283
283
  export const dagFrontendTestEvidenceValidationSchema = z.object({}).strict();
284
+ export const dagFrontendTestCaseChecklistSchema = z.object({}).strict();
285
+ export const dagFrontendTestHtmlReportSchema = z.object({}).strict();
284
286
  export const dagMavenWorkspaceManifestSchema = z.object({
285
287
  files: z.array(z.object({
286
288
  path: z.string(),
@@ -325,6 +327,8 @@ export const dagShellConfigSchema = z.object({
325
327
  frontendVerificationBundle: dagFrontendVerificationBundleSchema.optional(),
326
328
  frontendReviewContext: dagFrontendReviewContextSchema.optional(),
327
329
  frontendTestEvidenceValidation: dagFrontendTestEvidenceValidationSchema.optional(),
330
+ frontendTestCaseChecklist: dagFrontendTestCaseChecklistSchema.optional(),
331
+ frontendTestHtmlReport: dagFrontendTestHtmlReportSchema.optional(),
328
332
  backendTestPipeline: dagBackendTestPipelineSchema.optional(),
329
333
  /** JaCoCo coverage collection for backend-test (Java services). When set, node 7 dumps coverage over TCP from a JaCoCo tcpserver agent and feeds it to the L-5 dashboard. */
330
334
  jacocoCoverage: z.object({
@@ -462,7 +462,9 @@ function validateShellTaskConfig(task, spec, issues) {
462
462
  !shell.frontendPrewriteGate &&
463
463
  !shell.frontendVerificationBundle &&
464
464
  !shell.frontendReviewContext &&
465
- !shell.frontendTestEvidenceValidation) {
465
+ !shell.frontendTestCaseChecklist &&
466
+ !shell.frontendTestEvidenceValidation &&
467
+ !shell.frontendTestHtmlReport) {
466
468
  issues.push({
467
469
  type: "missing-shell-commands",
468
470
  message: `shell task ${task.id} requires a supported shell operation or non-empty shell.commands`,
@@ -190,6 +190,19 @@ bash scripts/check-skill-entry.sh
190
190
 
191
191
  Runtime 变更另需 `npm run typecheck` 及对应 targeted Vitest(见 exec plan 各 Phase 验证关口)。
192
192
 
193
+ ## Client session 瞬态恢复边界
194
+
195
+ 主会话模型偶尔会返回非标准 502 / `LLMRequestError` / 网络抖动 / 超时响应;OpenCode 可能先解析为 `TypeValidationError` 再落成 `UnknownError`,导致内置 APIError 重试不命中。loop-agent 通过 **项目级 OpenCode 插件补偿** 与 **Pi 用户级配置显式启用** 处理该缺口,不修改 OpenCode/Pi 上游,也不引入外部监督器。
196
+
197
+ | 面 | 路径 / 入口 | 边界 |
198
+ |---|---|---|
199
+ | OpenCode 项目插件 | `.opencode/plugins/loop-agent-transient-retry.js`(init surface `generated`) | 直接返回真实 `Hooks.event`,分发 `session.error` / `session.status` / `message.updated`;从 `client.session.status()` 的 session map 判断内置 retry,以 `client.session.promptAsync()` 续接同一 session;只有成功完成的 assistant message 清零连续失败计数。认证/权限/配额/上下文溢出/取消/业务错误走 `plugin-ignore-permanent-error` |
200
+ | Pi 用户配置 | `~/.pi/agent/settings.json` | **不**进入项目 `.harness/init-surface.json` hash;仅 `--client-recovery=user` 可字段级补缺并原子写;`auto`/`project`/`off` 与 `check-update` 默认零写 home;读取时只有 `ENOENT` 视为缺文件,其他 I/O 错误 fail closed |
201
+ | CLI mode | `--client-recovery=auto\|project\|user\|off`(默认 `auto`) | `auto`/`project` 只装项目插件;`user` = 项目插件 + 显式 Pi 合并;`off` 全跳过 |
202
+ | Ownership | recorded sha256 + apply-safe | 插件缺失可补、与 recorded hash 一致可升级;用户改过 → model merge / human decision,禁止静默覆盖 |
203
+
204
+ 实现落点:`src/commands/client-recovery.ts`(纯逻辑与生成器)+ `src/commands/init.ts`(编排)。
205
+
193
206
  ## 演进里程碑
194
207
 
195
208
  | Phase | 边界变化 |
@@ -164,7 +164,8 @@
164
164
  "docs/templates/backend-test-analysis.schema.json",
165
165
  "docs/templates/backend-test-execution.schema.json",
166
166
  "docs/templates/backend-test-result.schema.json",
167
- "docs/templates/backend-test-case-manifest.schema.json"
167
+ "docs/templates/backend-test-case-manifest.schema.json",
168
+ ".opencode/plugins/loop-agent-transient-retry.js"
168
169
  ],
169
170
  "initSurface": {
170
171
  "README.md": "managed-block",
@@ -244,7 +245,8 @@
244
245
  "docs/templates/backend-test-analysis.schema.json": "copied",
245
246
  "docs/templates/backend-test-execution.schema.json": "copied",
246
247
  "docs/templates/backend-test-result.schema.json": "copied",
247
- "docs/templates/backend-test-case-manifest.schema.json": "copied"
248
+ "docs/templates/backend-test-case-manifest.schema.json": "copied",
249
+ ".opencode/plugins/loop-agent-transient-retry.js": "generated"
248
250
  },
249
251
  "packageExcluded": [
250
252
  "docs/progress/20*.md",
@@ -264,6 +266,7 @@
264
266
  "harness.json",
265
267
  "package.json",
266
268
  "src/commands/init.ts",
269
+ "src/commands/client-recovery.ts",
267
270
  "src/cli.ts",
268
271
  "src/cli/update/init-surface-notifier.ts",
269
272
  "src/cli/update/policy.ts",
@@ -287,6 +290,7 @@
287
290
  "description": "Changes that may alter target-project initialization behavior, default DAG role skills, skill resolution, or package/init contracts.",
288
291
  "patterns": [
289
292
  "src/commands/init.ts",
293
+ "src/commands/client-recovery.ts",
290
294
  "src/cli.ts",
291
295
  "src/cli/update/init-surface-notifier.ts",
292
296
  "src/cli/update/policy.ts",
@@ -350,6 +350,9 @@
350
350
  "required": ["schemaVersion", "requireBaseline"],
351
351
  "properties": { "schemaVersion": { "const": 1 }, "requireBaseline": { "const": true } }
352
352
  },
353
+ "frontendTestCaseChecklist": { "type": "object", "additionalProperties": false },
354
+ "frontendTestEvidenceValidation": { "type": "object", "additionalProperties": false },
355
+ "frontendTestHtmlReport": { "type": "object", "additionalProperties": false },
353
356
  "backendTestPipeline": {
354
357
  "enum": ["contracts", "semantic-initial", "execute-parse-initial", "classification-result-context"]
355
358
  },
@@ -374,6 +377,9 @@
374
377
  { "required": ["frontendPrewriteGate"] },
375
378
  { "required": ["frontendVerificationBundle"] },
376
379
  { "required": ["frontendReviewContext"] },
380
+ { "required": ["frontendTestCaseChecklist"] },
381
+ { "required": ["frontendTestEvidenceValidation"] },
382
+ { "required": ["frontendTestHtmlReport"] },
377
383
  { "required": ["backendTestPipeline"] }
378
384
  ]
379
385
  },
@@ -125,7 +125,7 @@
125
125
  "artifacts/**"
126
126
  ],
127
127
  "outputContract": "Write a Chinese, human-readable testcase/md/README.md plus module Markdown case cards using BE-<MODULE>-<NNN>; keep machine IDs/literals exact and do not execute pytest or modify production code/config.",
128
- "subtask_prompt": "Read the upstream environment report. Generate a Markdown-first backend test strategy and cases under testcase/md/**.\n\nWrite human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.\n\nCreate testcase/md/README.md as the concise entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in README.\n\nWrite each module as readable case cards. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. The only sections required by the deterministic validator are `### 前置条件`, `### 操作步骤`, and `### 预期结果` (legacy English aliases remain accepted). Add `测试目的`, `验收标准`, `需求依据`, `测试数据`, and `自动化映射` when useful for human readability; every automatable case should explicitly name its target pytest script under `自动化映射` so traceability can scan only that script.\n\nName each module file with a stable lowercase business stem such as `testcase/md/health.md` or `testcase/md/resource_notes.md`. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.\n\nPlace steps and their expected results in a compact readable table when that improves clarity; otherwise keep numbered executable steps and numbered/bulleted independently assertable results. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.\n\nIn `自动化映射`, record the planned script path and pytest function name when known, and keep the script path identical to the module one-to-one path above. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and copy `path` exactly into Markdown Source References. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.\n\nRead only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text."
128
+ "subtask_prompt": "Read the upstream environment report. Generate a Markdown-first backend test strategy and cases under testcase/md/**.\n\nWrite human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.\n\nCreate testcase/md/README.md as the concise entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in README.\n\nWrite each module as readable case cards. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. `<NNN>` is exactly three zero-padded digits (`001`, `002`, ...), never two digits (`01`) and never a bare number. The only sections required by the deterministic validator are `### 前置条件`, `### 操作步骤`, and `### 预期结果` (legacy English aliases remain accepted). Add `测试目的`, `验收标准`, `需求依据`, `测试数据`, and `自动化映射` when useful for human readability; every automatable case should explicitly name its target pytest script under `自动化映射` so traceability can scan only that script.\n\nName each module file with a stable lowercase business stem such as `testcase/md/health.md` or `testcase/md/resource_notes.md`. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.\n\nPlace steps and their expected results in a compact readable table when that improves clarity; otherwise keep numbered executable steps and numbered/bulleted independently assertable results. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.\n\nIn `自动化映射`, record the planned script path and pytest function name when known, and keep the script path identical to the module one-to-one path above. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and copy `path` exactly into Markdown Source References. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.\n\nRead only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text."
129
129
  },
130
130
  {
131
131
  "id": "review-and-revise-backend-md-cases-pi",
@@ -149,7 +149,7 @@
149
149
  "artifacts/**"
150
150
  ],
151
151
  "outputContract": "Review source fidelity and directly revise only testcase/md/**; return concise Markdown, never JSON.",
152
- "subtask_prompt": "Independently review generated Markdown cases against the task requirements and environment evidence. Treat the files as human-facing test documentation: require clear preconditions, executable steps and assertable expected results; improve names, purpose, metadata and automation mapping where useful while preserving exact machine IDs and technical literals.\n\nCheck AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), and missing or drifted script/function mapping where it can be derived.\n\nCorrect testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, fix automation mappings so each case points at `testcase/test_<module>.py` derived from that module filename, merge duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations exact. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.\n\nRead only precise referenced source paths plus requirement sections needed for uncovered ACs. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and keep `path` as the exact Markdown Source References citation. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails."
152
+ "subtask_prompt": "Independently review generated Markdown cases against the task requirements and environment evidence. Treat the files as human-facing test documentation: require clear preconditions, executable steps and assertable expected results; improve names, purpose, metadata and automation mapping where useful while preserving exact machine IDs and technical literals.\n\nCheck AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), and missing or drifted script/function mapping where it can be derived.\n\nCorrect testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, normalize every Case ID to hyphen-separated module segments plus exactly three zero-padded digits (`BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`) consistently across headings/index/mappings, fix automation mappings so each case points at `testcase/test_<module>.py` derived from that module filename, merge duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations as exact machine-readable identifiers; only normalize Case ID separator/sequence formatting as specified above. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.\n\nRead only precise referenced source paths plus requirement sections needed for uncovered ACs. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.\n\n## Derived task contract: 需求.md\n\n# Backend test\n- AC-001 proof\n\n## Authoritative reference index\n\n[]\n\nFor each index entry, use `readPath` for Pi read-tool calls and keep `path` as the exact Markdown Source References citation. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails."
153
153
  },
154
154
  {
155
155
  "id": "validate-backend-md-cases-shell",
@@ -251,7 +251,7 @@
251
251
  "artifacts/**"
252
252
  ],
253
253
  "outputContract": "One scoped pytest execution over Markdown-mapped scripts producing a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html, reports/backend-test.md, reports/backend-test-facts.md, a deterministic self-contained reports/backend-test-l5-dashboard.html (machine-computed L-5 metrics, no JSON), and an optional contracts/code-coverage-v1.json when jacocoCoverage is configured (JaCoCo TCP dump → jacoco.xml → parsed; failure-safe); exit 0/1 with valid evidence continues.",
254
- "subtask_prompt": "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html plus final Markdown case metadata without rerun. Keep 测试结论, quality status, failure overview, and a polished per-case result card with concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.",
254
+ "subtask_prompt": "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html plus final Markdown case metadata without rerun. Keep 测试结论 and quality status; make node 4 Markdown validation and node 6 Markdown-to-pytest traceability expandable to their full escaped details; show each failure overview item with its original pytest message plus deterministic evidence-based reason analysis; list failure/error case cards before the remaining cases while preserving stable order. Each polished per-case result card includes concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.",
255
255
  "shell": {
256
256
  "commands": [
257
257
  "mkdir -p \"${HARNESS_DAG_RUN_DIR}/reports\"; echo \"pytest targets are resolved at runtime from final Markdown 自动化映射\""
@@ -11,7 +11,7 @@
11
11
  ### 阅读体验
12
12
 
13
13
  - `testcase/md/README.md` 是简洁入口,包含测试目标、环境、隔离/清理策略、模块汇总和可跳转的用例索引。
14
- - 模块文件采用中文用例卡片;每条以 `## BE-<MODULE>-<NNN>|<中文用例名称>` 开始。
14
+ - 模块文件采用中文用例卡片;每条以 `## BE-<MODULE>-<NNN>|<中文用例名称>` 开始;`<NNN>` 必须是三位补零序号(`001`,禁止 `01`)。Reviewer 发现两位序号或模块下划线时,必须在标题、README 索引与自动化映射中一致规范化(如 `BE-RESOURCE_NOTES-01` → `BE-RESOURCE-NOTES-001`)。
15
15
  - 新文档优先使用:`测试目的`、`验收标准`、`需求依据`、`前置条件`、`测试数据`、`操作步骤`、`预期结果`、`自动化映射`。
16
16
  - validator 同时接受上述中文分节和历史英文分节;机器 ID、HTTP 方法、路径、字段、枚举、文件名、函数名与 source citation 必须保持精确。
17
17
  - 步骤和预期可以用紧凑表格,也可以分别使用编号/项目列表;必须可执行、可独立断言。
@@ -251,11 +251,10 @@
251
251
  "artifacts/**"
252
252
  ],
253
253
  "outputContract": "Mechanical checklist: manifest/case paths, Case ID, non-production playwright-cli open prefix, and AC mapping; strongly recommend playwright-cli without blocking alternative executable tool commands.",
254
- "subtask_prompt": "Scan generated cases/manifest against the shared blocking checklist. Runtime hybrid embeds the authoritative script.",
254
+ "subtask_prompt": "Run the native deterministic frontend case checklist without spawning Bash, PowerShell, or node -e.",
255
255
  "shell": {
256
- "commands": [
257
- "node -e \"console.log('template placeholder: runtime hybrid embeds checklist')\""
258
- ],
256
+ "commands": [],
257
+ "frontendTestCaseChecklist": {},
259
258
  "cwd": ".",
260
259
  "timeoutMs": 120000
261
260
  }
@@ -448,6 +447,34 @@
448
447
  ],
449
448
  "outputContract": "Write testcase/frontend/reports/frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, execution evidence review, risks, findings, and A/B/C/D rating — even when outcome is failed/incomplete.",
450
449
  "subtask_prompt": "Write the frontend test retrospective under testcase/frontend/reports/ after result materialization (do not wait for outcome=pass). Combine AC→case→browser-evidence review with the closeout report. Blocked cases never count as passed. Do not write docs/**."
450
+ },
451
+ {
452
+ "id": "frontend-test-html-report-shell",
453
+ "depends_on": [
454
+ "frontend-test-retrospect-pi"
455
+ ],
456
+ "executor": "shell",
457
+ "role": "verifier",
458
+ "complexity": "LOW",
459
+ "writePolicy": "exclusive",
460
+ "writeSet": [
461
+ "testcase/frontend/reports/**"
462
+ ],
463
+ "allowedPaths": [
464
+ "testcase/frontend/**"
465
+ ],
466
+ "forbiddenPaths": [
467
+ ".harness/**",
468
+ "artifacts/**"
469
+ ],
470
+ "outputContract": "Write frontend-test-report.md and frontend-test-report.html containing only case execution results, case content, and failed/blocked error analysis.",
471
+ "subtask_prompt": "Render the formal frontend test report from frontend-test-result-v1 without evidence chains, advisory findings, suggestions, or ratings.",
472
+ "shell": {
473
+ "commands": [],
474
+ "frontendTestHtmlReport": {},
475
+ "cwd": ".",
476
+ "timeoutMs": 120000
477
+ }
451
478
  }
452
479
  ]
453
480
  }
package/harness.json CHANGED
@@ -58,8 +58,9 @@
58
58
  "executors": {
59
59
  "pi": {
60
60
  "description": "Pi planning, review, diagnosis, and bounded writing when DAG toolProfile=write",
61
- "LOW": "grok-4.5",
62
- "MED": "grok-4.5",
61
+ "defaultModel": "minimax-m3",
62
+ "LOW": "minimax-m3",
63
+ "MED": "glm-5.2",
63
64
  "HIGH": "gpt-5.6-sol"
64
65
  }
65
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.25.2",
3
+ "version": "0.25.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -56,9 +56,10 @@ loop-agent run-dag --dag .harness/tasks/<task-id>/dag.json --cwd <repo-root>
56
56
  6. 主会话不绕过 CLI 直接写业务代码;失败只走 doctor/reconcile/human gate/重跑。
57
57
  7. DAG `pi` executor read-only unless `toolProfile: "write"`;completed run facts read-only;不得从 read-only DAG/sidecar 写 root `artifacts/`。
58
58
  8. No hidden state in chat only;verify before completion.
59
+ 9. Client recovery:`init --client-recovery=auto|project|user|off`;只有 `user` 写 Pi settings;check/update 遵守 ownership。
59
60
 
60
61
  ## References
61
62
 
62
63
  Required(按需 inline):`references/harness-policy.md`、`references/hybrid-dag.md`、`references/verification-and-failure-handling.md`
63
64
 
64
- Optional:`references/command-reference.md`(operator commands、`agent-worker`)、`references/long-running-loop.md`、`references/orchestrator-and-interventions.md`、`references/task-workflow.md`、`references/pi-prompt.md`、`references/one-shot-runs.md`、`references/pi-subagent-assisted-mode.md`、`references/model-routing.md`、`references/multi-worktree.md`、`references/post-implementation-and-patterns.md`、`references/docs-converge.md`
65
+ Optional 索引见 `references/README.md`;常用:`command-reference.md`、`long-running-loop.md`、`orchestrator-and-interventions.md`、`docs-converge.md`。
@@ -294,7 +294,8 @@ loop-agent dag validate --dag <temp-dir>/hybrid-dag.json --strict-governance --s
294
294
  loop-agent dag validate --dag ai_workspace/loop-agent/templates/agent-dag.supervised-implementation.json --strict-models --strict-governance # role=supervisor + write-set-gate topology
295
295
  cp ai_workspace/loop-agent/templates/agent-dag.supervised-implementation.json <temp-dir>/supervised-dag.json
296
296
  (npx vitest run test/dag-supervised-template.test.ts test/dag-validate.test.ts test/dag-shell-executor.test.ts --reporter=dot) # supervised template + shell.verdictGate runtime
297
- loop-agent run-dag --dag <temp-dir>/hybrid-dag.json --cwd <repo-root> # 执行 Agent DAG
297
+ loop-agent run-dag --dag <temp-dir>/hybrid-dag.json --cwd <repo-root> # 执行 Agent DAG;stderr 默认输出节点进度与 30s 心跳,stdout 保持最终 JSON
298
+ loop-agent run-dag --dag <temp-dir>/hybrid-dag.json --cwd <repo-root> --progress-interval-ms 60000 # 调整心跳;--quiet 可禁用
298
299
  loop-agent run-dag --dag <temp-dir>/hybrid-dag.json --init-only --canvas-path <temp-dir>/hybrid-dag.canvas.tsx # 可选 derived Canvas view
299
300
  bash scripts/run-dag-safe.sh --dag <temp-dir>/hybrid-dag.json --cwd <repo-root> [--timeout-secs 7200] # 后台运行 + 轮询,避免外层 bash timeout 杀进程(见 agent-dag-runner.md §Adaptive liveness)
300
301
  loop-agent dag init-hybrid <task-id> # 生成可审阅的 DAG draft