@tea-agent/loop-agent 0.44.0-next.11 → 0.44.0-next.13

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.
Files changed (132) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/commands/init.js +1 -0
  4. package/dist/executors/dag-pi-executor.js +11 -6
  5. package/dist/executors/pi-writer-tool-policy.js +60 -4
  6. package/dist/executors/playwright-cli-launcher.js +31 -45
  7. package/dist/executors/shell-executor.js +39 -11
  8. package/dist/executors/shell-write-guard.js +24 -67
  9. package/dist/governance/exec-plans.js +10 -1
  10. package/dist/task/config-types.js +4 -2
  11. package/dist/task/source-prepare/build-draft.js +1 -0
  12. package/dist/task/source-prepare/path-policy.js +17 -7
  13. package/dist/worker/console/chat/browser-automation.js +343 -38
  14. package/dist/worker/console/chat/browser-content.js +74 -0
  15. package/dist/worker/console/chat/browser-diagnostics.js +250 -0
  16. package/dist/worker/console/chat/browser-evaluation.js +58 -0
  17. package/dist/worker/console/chat/browser-operation-queue.js +19 -0
  18. package/dist/worker/console/chat/browser-pick.js +113 -0
  19. package/dist/worker/console/chat/browser-routes.js +33 -5
  20. package/dist/worker/console/chat/browser-screenshot-result.js +47 -0
  21. package/dist/worker/console/chat/browser-storage.js +89 -0
  22. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +125 -174
  23. package/dist/worker/console/chat/pi-runtime.js +1 -1
  24. package/dist/worker/console/chat/repo-browser.js +80 -2
  25. package/dist/worker/console/chat/repo-preview-kind.js +155 -0
  26. package/dist/worker/console/chat/session-mode.js +1 -1
  27. package/dist/worker/console/chat/session-store.js +4 -2
  28. package/dist/worker/console/chat/tools.js +12 -0
  29. package/dist/worker/console/routes.js +5 -0
  30. package/dist/worker/console/static/app-icon.svg +39 -0
  31. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-r1NAPU_5.js → abnfDiagram-N423BO3Z-BA7qCL_r.js} +1 -1
  32. package/dist/worker/console/static/assets/{arc-dT5T4sVL.js → arc-Df09ajwL.js} +1 -1
  33. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP_cOq46.js → architectureDiagram-T3A2C74G-CpF9sw5R.js} +1 -1
  34. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-sToEZxEf.js → blockDiagram-VBNYF7ZC-BtPys8ja.js} +1 -1
  35. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DSxwwi2m.js → c4Diagram-5PPSVZJV-DZPnTWqT.js} +1 -1
  36. package/dist/worker/console/static/assets/channel-DiTNLZUm.js +1 -0
  37. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B9fh0IkF.js → chunk-2GRJ4B5K-BD9D4h1p.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CAUNl-A6.js → chunk-2Q5K7J3B-Svn8yBmn.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BomeqbNX.js → chunk-5RXB4S5H-B8EbQj-S.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BNAuTOLE.js → chunk-5VM5RSS4-tENZpcpF.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DfzjRD5y.js → chunk-6Q2QTUOP-DBx71Lco.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3uE-yc2.js → chunk-GF5L2VYU-1v_QamMz.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-C3sQMMYr.js → chunk-JWPE2WC7-BAMCNNBG.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D_AtEYQL.js → chunk-KBJHAD2P-BvuEury-.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DW4TwhKv.js → chunk-RYQCIY6F-YqFuHp8s.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CTQDmJcp.js → chunk-XXDRQBXY-ouI5T7Mr.js} +1 -1
  47. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgL8aiyZ.js +1 -0
  48. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgL8aiyZ.js +1 -0
  49. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-iMqPuBsL.js → cose-bilkent-JH36ORCC-Bri0_Izk.js} +1 -1
  50. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DktIuDWs.js → cynefin-VYW2F7L2-e8Vhhi4X.js} +1 -1
  51. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BgVnjyEs.js → cynefinDiagram-MW4NZA55-tSIXGQiJ.js} +1 -1
  52. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CR25E4bu.js → dagre-VZM6K2ZE-Rl-sF5pt.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CPM2h5HY.js → diagram-7IWD3JNH-Db6ddPz8.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqF0x2g1.js → diagram-B4RE2ZJO-Jwd_Xw5S.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-G8BYp2eb.js → diagram-LBJQPF4R-CeC_8InC.js} +1 -1
  56. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BbC4kT_D.js → diagram-Q27KOJAE-LKPRaNq8.js} +1 -1
  57. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CKZwZ-G5.js → diagram-UB23O5K3-DlVQytMW.js} +1 -1
  58. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kJDmheia.js → ebnfDiagram-BXEA7PRR-DnfJZwO-.js} +1 -1
  59. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DFTVHyxB.js → erDiagram-JOGREHBK-3axVSXca.js} +1 -1
  60. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqdN5_os.js → flowDiagram-UKHOOZJN-xe04-_L-.js} +1 -1
  61. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-SYDjVakh.js → ganttDiagram-PKOTCBZU-hpmRINiD.js} +1 -1
  62. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js → gitGraphDiagram-DS77QQ5N-6HSNz9Ko.js} +1 -1
  63. package/dist/worker/console/static/assets/index-C_8_gWal.css +1 -0
  64. package/dist/worker/console/static/assets/{index-CTMZsDmh.js → index-DybcVXCu.js} +102 -100
  65. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DIeAmJ45.js → infoDiagram-6WML65LV-BF6ksWTX.js} +1 -1
  66. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-oikqUAlo.js → ishikawaDiagram-WSZJBQD7-DXisdeD7.js} +1 -1
  67. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-mKEF_xYT.js → journeyDiagram-NVQOT4AX-BWu65wmu.js} +1 -1
  68. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdWJHPnN.js → kanban-definition-27J2QSJJ-hfNhHqAI.js} +1 -1
  69. package/dist/worker/console/static/assets/{linear-DGRlieMH.js → linear-D6HTfQcp.js} +1 -1
  70. package/dist/worker/console/static/assets/{mermaid.core-4P9yPC9c.js → mermaid.core-Iv91R1WP.js} +5 -5
  71. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-5PTSkos6.js → mindmap-definition-FAOFIHXS-CfPChwjR.js} +1 -1
  72. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DONIFpG5.js → pegDiagram-VL7TDLO6-B7TcoqHz.js} +1 -1
  73. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D_Z4S70M.js → pieDiagram-7S7Q4E2Y-ouyPKMtQ.js} +1 -1
  74. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CAsjh8iY.js → quadrantDiagram-CIZ2JOQS-B9tuyLbh.js} +1 -1
  75. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cx4Dd6ac.js → railroadDiagram-AXF67PYL-Ck4q9nJs.js} +1 -1
  76. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-cQb7mwyH.js → requirementDiagram-LRYGKXZP-wXLX5Dnp.js} +1 -1
  77. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cd-DhUdf.js → sankeyDiagram-W5VNT64P-D2XWw6_m.js} +1 -1
  78. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-AlKYRgIk.js → sequenceDiagram-SI44F4Z6-DzxKiug4.js} +1 -1
  79. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CUy_ET4e.js → sizeCapture-X5ZJPWSS-DWUqK5Us.js} +1 -1
  80. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CTuVe6Sa.js → stateDiagram-OKZ733FA-D5Pa4O5g.js} +1 -1
  81. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C0QIbqO4.js +1 -0
  82. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ChFLJb-v.js → swimlanes-SLNWSIFB-VH7cRnsJ.js} +2 -2
  83. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BB9CzRHB.js +8 -0
  84. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DZZeiAPr.js → timeline-definition-Z64GVDOM-C4HM5NoZ.js} +1 -1
  85. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-t4O91cZU.js → vennDiagram-T6HMQDX7-Bu-4hOlA.js} +1 -1
  86. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CV5qGHZu.js → wardleyDiagram-T6FBY63Y-CasFEvvl.js} +1 -1
  87. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DyXbytUI.js → xychartDiagram-ELKLHX3M-D-gpwPSA.js} +1 -1
  88. package/dist/worker/console/static/index.html +5 -2
  89. package/dist/worker/console/static/manifest.webmanifest +19 -0
  90. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +57 -9
  91. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +6 -0
  92. package/dist/worker/console/workspace-context.js +1 -0
  93. package/dist/worker/materialize/harness-task-lineage.js +4 -2
  94. package/dist/worker/observe/static/console-theme.css +33 -29
  95. package/dist/worker/observe/static/console-theme.js +38 -0
  96. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +3 -1
  97. package/dist/workflows/dag/backend-test-layout.js +27 -7
  98. package/dist/workflows/dag/backend-test-markdown-workflow.js +3 -1
  99. package/dist/workflows/dag/frontend-repair.js +24 -0
  100. package/dist/workflows/dag/frontend-review-scopes.js +10 -2
  101. package/dist/workflows/dag/frontend-test-case-checklist.js +12 -3
  102. package/dist/workflows/dag/frontend-test-environment-probe.js +0 -2
  103. package/dist/workflows/dag/frontend-test-execution-evidence.js +163 -67
  104. package/dist/workflows/dag/frontend-test-framework-adapters.js +309 -0
  105. package/dist/workflows/dag/frontend-test-layout.js +41 -12
  106. package/dist/workflows/dag/hybrid/sources.js +21 -2
  107. package/dist/workflows/dag/hybrid/templates/backend-test.js +65 -50
  108. package/dist/workflows/dag/hybrid/templates/frontend-test.js +58 -37
  109. package/dist/workflows/dag/test-artifact-namespace.js +73 -0
  110. package/dist/workflows/dag/validate.js +22 -3
  111. package/docs/architecture/dag-execution.md +3 -3
  112. package/docs/templates/README.md +4 -4
  113. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -3
  114. package/docs/templates/backend-test-dag.json +708 -708
  115. package/docs/templates/backend-test-dag.review-cases.prompt.md +3 -3
  116. package/docs/templates/frontend-test-case-checklist.md +2 -2
  117. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +7 -7
  118. package/docs/templates/frontend-test-dag.json +46 -37
  119. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +2 -2
  120. package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
  121. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  122. package/package.json +7 -2
  123. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  124. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  125. package/skills/playwright-cli/SKILL.md +1 -1
  126. package/skills/playwright-cli-case-generator/SKILL.md +4 -4
  127. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +0 -1
  128. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +0 -1
  129. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +0 -1
  130. package/dist/worker/console/static/assets/index-B28Onmfy.css +0 -1
  131. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +0 -1
  132. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +0 -8
@@ -5,6 +5,7 @@ import { assertValidDagSpec } from "../../validate.js";
5
5
  import { DEFAULT_DAG_EXECUTOR_MODELS, DEFAULT_DAG_OUTPUT_LANGUAGE, parseDagSpec } from "../../types.js";
6
6
  import { pathMatchesPattern } from "../../../../shared/git-progress.js";
7
7
  import { applyFrontendTestLayoutToText, resolveFrontendTestLayout } from "../../frontend-test-layout.js";
8
+ import { isHarnessNamespacePath } from "../../test-artifact-namespace.js";
8
9
  import { buildFrontendTestOutcomeGateShellSnippet } from "../../frontend-test-result-contract.js";
9
10
  import { GENERATED_DAG_RUNTIME_CONTRACT, HYBRID_DEFAULTS, STANDARD_GLOBAL_CONSTRAINTS, buildDagSourceBinding, buildSourceContextBlock, extractObjective, extractSuccessCriteria, resolveDagVerifyStrategy } from "../sources.js";
10
11
  import { applyDefaultReadOnlyRetryPolicy, commonForbiddenPaths, commonReadOnlyPaths, stampGeneratedArtifactBindings, } from "./shared.js";
@@ -62,7 +63,7 @@ export function buildFrontendTestHybridDag(sources) {
62
63
  const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
63
64
  const checklistScript = [
64
65
  "const fs=require('fs'),path=require('path');",
65
- "const root='testcase/frontend/cases';",
66
+ `const root='${layout.casesDir}';`,
66
67
  "const draft=path.join(root,'manifest.draft.json');",
67
68
  "const final=path.join(root,'manifest.json');",
68
69
  "const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
@@ -84,8 +85,8 @@ export function buildFrontendTestHybridDag(sources) {
84
85
  " if(typeof c.caseId==='string'&&/^AC-/i.test(c.caseId))issues.push({ruleId:'case-id-is-ac',caseId:id,detail:'do not use acceptance id as caseId; put AC-FE-* only in acIds'});",
85
86
  " const casePath=typeof c.casePath==='string'?c.casePath:null;",
86
87
  " if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
87
- " if(typeof c.caseId==='string'&&casePath!=='testcase/frontend/cases/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal testcase/frontend/cases/'+c.caseId+'.md'});",
88
- " const body=fs.readFileSync(casePath,'utf8');",
88
+ " if(typeof c.caseId==='string'&&casePath!=='${layout.casesDir}/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal ${layout.casesDir}/'+c.caseId+'.md'});",
89
+ " const body=fs.readFileSync(casePath,'utf8').replace(/\\bplaywright_cli\\b/g,'playwright-cli');",
89
90
  " if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome <absolute-url>; playwright-cli is strongly recommended for browser execution'});",
90
91
  ,
91
92
  " const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+(https?:\\/\\/\\S+)/i);",
@@ -117,7 +118,7 @@ export function buildFrontendTestHybridDag(sources) {
117
118
  JSON.stringify([
118
119
  "const fs=require('fs'),path=require('path');",
119
120
  "function fail(ruleId,detail){ const msg=JSON.stringify({ruleId:ruleId,detail:String(detail||'')}); console.error('frontend-test manifest blocked: '+msg); throw new Error('frontend-test manifest blocked: '+ruleId+(detail?(': '+detail):'')); }",
120
- "const draft='testcase/frontend/cases/manifest.draft.json',file='testcase/frontend/cases/manifest.json',tmp=file+'.tmp';",
121
+ `const draft='${layout.casesDir}/manifest.draft.json',file='${layout.casesDir}/manifest.json',tmp=file+'.tmp';`,
121
122
  "if(!fs.existsSync(draft)) fail('draft-missing','missing '+draft);",
122
123
  "let manifest; try{manifest=JSON.parse(fs.readFileSync(draft,'utf8'));}catch(e){fail('draft-invalid-json',e&&e.message||e);}",
123
124
  "if(manifest.schemaVersion!==1) fail('draft-schema','schemaVersion must be 1');",
@@ -138,8 +139,8 @@ export function buildFrontendTestHybridDag(sources) {
138
139
  " if(typeof c.dimension!=='string'||!dims.has(c.dimension)) fail('invalid-dimension',String(c.dimension));",
139
140
  " if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) fail('ac-mapping','invalid acIds for '+c.caseId);",
140
141
  " for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))) fail('ac-id-truncated','acIds entry is a truncated acceptance id (missing feature prefix or sequence number): '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
141
- " c.casePath='testcase/frontend/cases/'+c.caseId+'.md';",
142
- " c.evidenceDir='testcase/frontend/evidence/'+c.caseId+'/';",
142
+ ` c.casePath='${layout.casesDir}/'+c.caseId+'.md';`,
143
+ ` c.evidenceDir='${layout.evidenceDir}/'+c.caseId+'/';`,
143
144
  " for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) fail('unsafe-path',k+': '+String(v)); }",
144
145
  " if(!fs.existsSync(c.casePath)) fail('case-file-missing','missing case file '+c.casePath+' (filename must equal caseId.md)');",
145
146
  " if(seenCasePath.has(c.casePath)) fail('duplicate-case-path',c.casePath); seenCasePath.add(c.casePath);",
@@ -158,7 +159,7 @@ export function buildFrontendTestHybridDag(sources) {
158
159
  "const runDir=process.env.HARNESS_DAG_RUN_DIR; if(!runDir)throw new Error('missing HARNESS_DAG_RUN_DIR');",
159
160
  "const resultPath=path.join(runDir,'contracts','frontend-test-result.json'); if(!fs.existsSync(resultPath))throw new Error('missing '+resultPath);",
160
161
  "const r=JSON.parse(fs.readFileSync(resultPath,'utf8')); const findings=Array.isArray(r.advisoryFindings)?r.advisoryFindings:[];",
161
- "const reports='testcase/frontend/reports'; fs.mkdirSync(reports,{recursive:true});",
162
+ `const reports='${layout.reportsDir}'; fs.mkdirSync(reports,{recursive:true});`,
162
163
  "const esc=v=>String(v??'').replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('\\\"','&quot;');",
163
164
  "const labels={cases:'用例总数',passed:'通过',failed:'失败',blocked:'阻塞'}; const totals=r.totals||{}; const missing=r.acceptanceCoverage&&Array.isArray(r.acceptanceCoverage.missing)?r.acceptanceCoverage.missing:[];",
164
165
  "const md=['# 前端测试质量建议报告','','> Advisory:以下 finding 用于改进测试资产和证据质量,不阻塞后续流程。','','## 执行摘要','',...Object.entries(labels).map(([k,v])=>'- '+v+': '+Number(totals[k]||0)),'- 执行结果: '+String(r.outcome||'unknown'),'- 缺失 AC: '+(missing.join(', ')||'无'),'','## 建议项','',...(findings.length?findings.map(f=>'- ['+f.ruleId+']'+(f.caseId?' '+f.caseId:'')+': '+f.detail):['- 未发现建议项。']),''];",
@@ -197,7 +198,7 @@ export function buildFrontendTestHybridDag(sources) {
197
198
  writeSet: ragWriteSet,
198
199
  allowedPaths: [...ragWriteSet],
199
200
  forbiddenPaths: forbidden,
200
- outputContract: "Write testcase/frontend/rag/standard-scenarios.v1.json for generate-time standard scenario coverage. Copy docs/templates or harness.json governanceRoot templates (including ai_workspace/loop-agent/templates) when present; otherwise write the minimal STD-FE-SMOKE-ENTRY fallback.",
201
+ outputContract: `Write ${layout.ragDir}/standard-scenarios.v1.json for generate-time standard scenario coverage. Copy docs/templates or harness.json governanceRoot templates (including ai_workspace/loop-agent/templates) when present; otherwise write the minimal STD-FE-SMOKE-ENTRY fallback.`,
201
202
  subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package from docs/templates, governanceRoot/templates, or the init-projected ai_workspace/loop-agent/templates path.",
202
203
  shell: {
203
204
  commands: [],
@@ -218,10 +219,10 @@ export function buildFrontendTestHybridDag(sources) {
218
219
  writeSet: ragWriteSet,
219
220
  allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
220
221
  forbiddenPaths: forbidden,
221
- outputContract: "Write short testcase/frontend/rag/context.md and coverage-map.md with a model-resolved non-production baseUrl/baseUrlSource from task source config, environmentProbe=pending, and capability notes.",
222
+ outputContract: `Write short ${layout.ragDir}/context.md and coverage-map.md with a model-resolved non-production baseUrl/baseUrlSource from task source config, environmentProbe=pending, and capability notes.`,
222
223
  subtask_prompt: [
223
224
  "Build the frontend test RAG package (keep it short).",
224
- "Read task source, routes/components/API or Mock facts, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.",
225
+ `Read task source, routes/components/API or Mock facts, and execution contract. Write only ${layout.ragDir}/context.md and coverage-map.md.`,
225
226
  "Prefer fixed fields: baseUrl, baseUrlSource, environmentProbe, AC table, capability matrix (backend real/mock, pagination data, HTTP observation, error injection), risks, forbidden hosts. Do not paste large implementation dumps.",
226
227
  "Base URL resolution (required): read the bound task source and config/environment references. Prefer TARGET_URL/targetUrl as the concrete tested entry, then BASE_URL/baseUrl or FRONTEND_BASE_URL/frontendUrl, then LOGIN_URL/loginUrl; accept common casing and underscore/kebab/space variants. Choose one absolute non-production http(s) URL, never API-only or production hosts. If no usable URL exists, use http://localhost:5173. Write `baseUrl: <url>`, `baseUrlSource: <bound source path>|default-localhost-5173`, and `environmentProbe: pending`. Include exact start prefix: playwright-cli open --browser=chrome <resolved-base-url> with the concrete selected URL.",
227
228
  buildSourceContextBlock(sources),
@@ -238,7 +239,7 @@ export function buildFrontendTestHybridDag(sources) {
238
239
  allowedPaths: [...ragWriteSet],
239
240
  forbiddenPaths: forbidden,
240
241
  outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable with errorClass (connection-refused / dns-unresolved / connect-timeout / http-N / curl-exit-N). Node ERROR so generate/map do not run. Does not start the app.",
241
- subtask_prompt: "Parse the resolved absolute baseUrl from testcase/frontend/rag/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. Connection refused (curl 7) records errorClass=connection-refused and tells the operator to start the local app (scripts/serve.sh or npm start) then rerun from this node. 4xx/5xx/DNS/timeout/TLS => blockedReason frontend-base-url-unreachable with a distinct errorClass. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.",
242
+ subtask_prompt: `Parse the resolved absolute baseUrl from ${layout.ragDir}/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. Connection refused (curl 7) records errorClass=connection-refused and tells the operator to start the local app (scripts/serve.sh or npm start) then rerun from this node. 4xx/5xx/DNS/timeout/TLS => blockedReason frontend-base-url-unreachable with a distinct errorClass. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.`,
242
243
  shell: {
243
244
  commands: [],
244
245
  frontendTestEnvironmentProbe: {},
@@ -261,22 +262,22 @@ export function buildFrontendTestHybridDag(sources) {
261
262
  outputContract: "Write executable Markdown frontend cases, index.md, and manifest.draft.json schemaVersion 1 only; the exclusive shell materializer promotes the validated draft to manifest.json. Do not write manifest.json or test source code.",
262
263
  subtask_prompt: [
263
264
  "Use skill playwright-cli-case-generator.",
264
- "Read testcase/frontend/rag/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.",
265
+ `Read ${layout.ragDir}/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.`,
265
266
  ...(uiAnchorsLedgerInstruction ? [uiAnchorsLedgerInstruction] : []),
266
- "Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and the draft case paths testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/manifest.draft.json. Write only those same draft paths. Do not write testcase/frontend/cases/manifest.json.",
267
+ `Read only ${layout.ragDir}/context.md, ${layout.ragDir}/coverage-map.md, and the draft case paths ${layout.casesDir}/FE-*.md, ${layout.casesDir}/index.md, and ${layout.casesDir}/manifest.draft.json. Write only those same draft paths. Do not write ${layout.casesDir}/manifest.json.`,
267
268
  "Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
268
269
  "HARD ID CONTRACT (do not confuse these):",
269
270
  "- caseId / filename MUST be FE-<FEATURE>-<NNN>-<dimension> (example FE-LOGIN-001-core). NEVER use AC-FE-* as caseId or filename.",
270
271
  "- acIds MUST list acceptance criteria only: AC-FE-* / AC-* from the declared task list (example AC-FE-001). NEVER put FE-* case ids into acIds.",
271
- "- casePath MUST equal testcase/frontend/cases/<caseId>.md; evidenceDir MUST equal testcase/frontend/evidence/<caseId>/. Materialize will rewrite paths, but files must already use caseId filenames.",
272
+ `- casePath MUST equal ${layout.casesDir}/<caseId>.md; evidenceDir MUST equal ${layout.evidenceDir}/<caseId>/. Materialize will rewrite paths, but files must already use caseId filenames.`,
272
273
  `Declared acceptance ids for this task (use only these in acIds when non-empty): ${declaredAcIds.length > 0 ? declaredAcIds.join(", ") : "(none extracted - still use AC-* shape, never FE-* case ids)"}.`,
273
274
  "dimensions: core|boundary|flow|backend only.",
274
275
  "Prefer a small smoke suite (default max roughly 4-8 cases unless task frontendTest.maxCasesPerBatch is higher). Never invent unavailable API fields or credentials. Do not create pytest or Playwright source.",
275
276
  "HARD playwright-cli-only: every browser step must use repo skill playwright-cli declared commands only. Forbidden: bare `playwright`, `npx playwright`, `playwright test`, `@playwright/test`, Node Playwright API, or generating Playwright/Pytest source. No fallback when playwright-cli is unavailable - case must instruct blocked evidence playwright-cli-unavailable.",
276
- "Copy the resolved absolute baseUrl from testcase/frontend/rag/context.md after the environment probe has marked it reachable. Every browser start command must be exactly: playwright-cli open --browser=chrome <resolved-base-url-from-context.md> with that concrete URL. Never leave a base-url placeholder. Use default browser session only; never write -s=<case-id>.",
277
+ `Copy the resolved absolute baseUrl from ${layout.ragDir}/context.md after the environment probe has marked it reachable. Every browser start command must be exactly: playwright-cli open --browser=chrome <resolved-base-url-from-context.md> with that concrete URL. Never leave a base-url placeholder. Use default browser session only; never write -s=<case-id>.`,
277
278
  "HARD dynamic refs: executable playwright-cli lines must never contain an angle-bracket token such as <fresh-ref> or descriptive <...> placeholder. Use only shell-safe documentation placeholders `eX`, `eY`, ...; each means the real `eNN` ref parsed from the immediately preceding latest `snapshot`. Write a fresh snapshot before every element reference. eX/eY are never literal structured-tool arguments; a later snapshot invalidates prior refs, so never reuse stale refs.",
278
279
  "HARD file-output argv: use canonical `--filename` only. Screenshot uses `playwright-cli screenshot --filename final.png` (a real ref may precede the flag); PDF uses `playwright-cli pdf --filename final.pdf`; snapshot without filename is response-only and a snapshot file uses `playwright-cli snapshot --filename snapshot.txt`. Never generate `playwright-cli screenshot <path>`, use `--path`, `--output`, or `--file`, or pass an output path as a positional target.",
279
- "Each case must be independently reproducible with fixture/reset, UI reset, snapshot-before-ref, evidence write point under testcase/frontend/evidence/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.",
280
+ `Each case must be independently reproducible with fixture/reset, UI reset, snapshot-before-ref, evidence write point under ${layout.evidenceDir}/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.`,
280
281
  "U/D data ownership (required for modify/delete): (1) Only mutate data whose ownership is proven by current login identity + observable UI/API owner fields - never by name/guessed id/list order alone. (2) If current user has no data, create tagged cleanable data in current-user context, then U/D, then cleanup+verify. (3) Else only task-authorized Mock, labeled as Mock (not real backend proof). (4) If ownership unverifiable and create/Mock unavailable: write blocked with blockedReason current-user-data-unavailable | data-ownership-unverifiable | safe-test-data-setup-unavailable - do not risk cross-user data. (5) Never touch other users, shared fixtures, production, or non-cleanable data.",
281
282
  ].join("\n\n"),
282
283
  },
@@ -307,7 +308,7 @@ export function buildFrontendTestHybridDag(sources) {
307
308
  allowedPaths: [...ragWriteSet, ...caseDraftWriteSet],
308
309
  forbiddenPaths: forbidden,
309
310
  outputContract: "Apply the one permitted frontend case revision to FE-*.md, index.md, and manifest.draft.json only; the exclusive shell materializer remains the sole writer of manifest.json. No browser execution or evidence writes.",
310
- subtask_prompt: "This is the only permitted case revision. Read the first review findings and the RAG package. Revise only testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/manifest.draft.json; preserve traceable AC mappings. Preserve the dynamic-ref contract: executable playwright-cli lines use only eX/eY-style shell-safe documentation placeholders, never <...>; each placeholder is resolved from the immediately preceding latest snapshot and stale refs are not reused. Do not write testcase/frontend/cases/manifest.json, execute a browser, or write evidence. HARD: Never delete case files; only edit in place or add missing cases. Preserve the full planned suite, index.md, and manifest.draft.json.",
311
+ subtask_prompt: `This is the only permitted case revision. Read the first review findings and the RAG package. Revise only ${layout.casesDir}/FE-*.md, ${layout.casesDir}/index.md, and ${layout.casesDir}/manifest.draft.json; preserve traceable AC mappings. Preserve the dynamic-ref contract: executable playwright-cli lines use only eX/eY-style shell-safe documentation placeholders, never <...>; each placeholder is resolved from the immediately preceding latest snapshot and stale refs are not reused. Do not write ${layout.casesDir}/manifest.json, execute a browser, or write evidence. HARD: Never delete case files; only edit in place or add missing cases. Preserve the full planned suite, index.md, and manifest.draft.json.`,
311
312
  }, {
312
313
  id: "review-frontend-cases-final-pi",
313
314
  depends_on: ["revise-frontend-cases-pi"],
@@ -411,9 +412,9 @@ export function buildFrontendTestHybridDag(sources) {
411
412
  writePolicy: "exclusive",
412
413
  writeGuardPolicy: "tools-only",
413
414
  allowedPaths: [
414
- "testcase/frontend/cases/{{case.caseId}}.md",
415
- "testcase/frontend/rag/context.md",
416
- "testcase/frontend/rag/coverage-map.md",
415
+ `${layout.casesDir}/{{case.caseId}}.md`,
416
+ `${layout.ragDir}/context.md`,
417
+ `${layout.ragDir}/coverage-map.md`,
417
418
  `${evidenceRoot}/{{case.caseId}}/**`,
418
419
  ],
419
420
  forbiddenPaths: forbidden,
@@ -422,7 +423,7 @@ export function buildFrontendTestHybridDag(sources) {
422
423
  subtaskPromptTemplate: [
423
424
  "Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
424
425
  "Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
425
- "1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. Unique-control early stop: when an AC names a specific control that is absent from the first post-navigation snapshot after reaching the required state (and no setup-required gate appeared), do ONE find with the case's literal as evidence; if it returns 0 matches, record status=blocked with blockedReason unique-control-unavailable citing that single find - do NOT re-explore with alternative literals, repeated snapshots, navigation detours, or rerun loops. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
426
+ `1) Read the concrete baseUrl from ${layout.ragDir}/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal \`eX\`/\`eY\`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. Unique-control early stop: when an AC names a specific control that is absent from the first post-navigation snapshot after reaching the required state (and no setup-required gate appeared), do ONE find with the case's literal as evidence; if it returns 0 matches, record status=blocked with blockedReason unique-control-unavailable citing that single find - do NOT re-explore with alternative literals, repeated snapshots, navigation detours, or rerun loops. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.`,
426
427
  "Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
427
428
  "Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
428
429
  ].join("\n\n"),
@@ -442,11 +443,11 @@ export function buildFrontendTestHybridDag(sources) {
442
443
  : `rerun-candidates-round${round}.json`;
443
444
  const selectCommand = [
444
445
  "const fs=require('fs'),path=require('path');",
445
- "const manifestPath='testcase/frontend/cases/manifest.json';",
446
+ `const manifestPath='${layout.casesDir}/manifest.json';`,
446
447
  "if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
447
448
  "const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
448
- "for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(r.status==='failed')reason='failed-retry';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||reason==='failed-retry'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
449
- `fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
449
+ `for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('${layout.evidenceDir}/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(r.status==='failed')reason='failed-retry';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||reason==='failed-retry'||missing)&&attempt<` + String(round) + `;if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('${layout.casesDir}/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}`,
450
+ `fs.mkdirSync('${layout.evidenceDir}',{recursive:true});fs.writeFileSync('${layout.evidenceDir}/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
450
451
  ].join("");
451
452
  tasks.push({
452
453
  id: selectorId,
@@ -455,8 +456,8 @@ export function buildFrontendTestHybridDag(sources) {
455
456
  executor: "shell",
456
457
  complexity: "LOW",
457
458
  writePolicy: "exclusive",
458
- writeSet: ["testcase/frontend/evidence/**"],
459
- allowedPaths: [...casesWriteSet, "testcase/frontend/evidence/**"],
459
+ writeSet: [`${evidenceRoot}/**`],
460
+ allowedPaths: [...casesWriteSet, `${evidenceRoot}/**`],
460
461
  forbiddenPaths: forbidden,
461
462
  outputContract: `Stdout JSON {cases} for blocked or missing-result-file cases eligible for rerun round ${round}/${maxRerunAttempts}.`,
462
463
  subtask_prompt: "Select frontend-test cases eligible for this bounded rerun round.",
@@ -501,9 +502,9 @@ export function buildFrontendTestHybridDag(sources) {
501
502
  writePolicy: "exclusive",
502
503
  writeGuardPolicy: "tools-only",
503
504
  allowedPaths: [
504
- "testcase/frontend/cases/{{case.caseId}}.md",
505
- "testcase/frontend/rag/context.md",
506
- "testcase/frontend/rag/coverage-map.md",
505
+ `${layout.casesDir}/{{case.caseId}}.md`,
506
+ `${layout.ragDir}/context.md`,
507
+ `${layout.ragDir}/coverage-map.md`,
507
508
  `${evidenceRoot}/{{case.caseId}}/**`,
508
509
  ],
509
510
  forbiddenPaths: forbidden,
@@ -512,7 +513,7 @@ export function buildFrontendTestHybridDag(sources) {
512
513
  subtaskPromptTemplate: [
513
514
  "RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite the authoritative execution.md and case-result.json; its final status replaces the earlier case result.",
514
515
  "Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
515
- "Read the concrete baseUrl from testcase/frontend/rag/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.",
516
+ `Read the concrete baseUrl from ${layout.ragDir}/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.`,
516
517
  "Unique-control early stop: if the first post-navigation snapshot lacks the control an AC names (and no setup-required gate appeared), one find returning 0 matches is sufficient evidence - write status=blocked blockedReason unique-control-unavailable instead of exploratory retries.",
517
518
  "Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}. Write fixed sections `### 执行摘要` and `### 实际执行步骤` to execution.md when available.",
518
519
  ].join("\n\n"),
@@ -531,7 +532,7 @@ export function buildFrontendTestHybridDag(sources) {
531
532
  complexity: "LOW",
532
533
  writePolicy: "exclusive",
533
534
  writeSet: [`${evidenceRoot}/**`],
534
- allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
535
+ allowedPaths: [`${layout.casesDir}/**`, `${evidenceRoot}/**`],
535
536
  forbiddenPaths: forbidden,
536
537
  outputContract: "Evidence validation (advisory missing/malformed does not fail the node; hard-fail only path escape) then hash-bound frontend-test-result-v1. Node success means result-v1 was written, not that all cases passed.",
537
538
  subtask_prompt: "Validate case evidence then materialize authoritative frontend-test-result-v1. Missing/malformed case evidence is advisory; only unsafe evidence paths hard-fail. Do not use Pi prose as input.",
@@ -549,8 +550,8 @@ export function buildFrontendTestHybridDag(sources) {
549
550
  executor: "shell",
550
551
  complexity: "LOW",
551
552
  writePolicy: "exclusive",
552
- writeSet: ["testcase/frontend/reports/**"],
553
- allowedPaths: ["testcase/frontend/**"],
553
+ writeSet: [`${layout.reportsDir}/**`],
554
+ allowedPaths: [`${layout.testRoot}/**`],
554
555
  forbiddenPaths: forbidden,
555
556
  outputContract: "Deterministic L-5 (optional) plus main frontend-test-report.md/html from frontend-test-result-v1. Pipeline acceptance = result-v1 + main HTML.",
556
557
  subtask_prompt: "Render operational reports from frontend-test-result-v1 only. Do not invent coverage. Main HTML is required; L-5 follows frontendTest.reports.l5.",
@@ -596,10 +597,10 @@ export function buildFrontendTestHybridDag(sources) {
596
597
  complexity: "MED",
597
598
  writePolicy: "exclusive",
598
599
  writeGuardPolicy: "tools-only",
599
- writeSet: ["testcase/frontend/reports/**"],
600
- allowedPaths: ["testcase/frontend/**"],
600
+ writeSet: [`${layout.reportsDir}/**`],
601
+ allowedPaths: [`${layout.testRoot}/**`],
601
602
  forbiddenPaths: forbidden,
602
- outputContract: "Optional retrospective under testcase/frontend/reports/frontend-test-retrospect-<date>.md. Not required for pipeline acceptance.",
603
+ outputContract: `Optional retrospective under ${layout.reportsDir}/frontend-test-retrospect-<date>.md. Not required for pipeline acceptance.`,
603
604
  subtask_prompt: "Write optional frontend-test retrospective after result + reports. Do not recompute L-5. Pipeline success does not require this file.",
604
605
  });
605
606
  }
@@ -617,7 +618,7 @@ export function buildFrontendTestHybridDag(sources) {
617
618
  "Environment preflight must parse the retrieve-authored context.md baseUrl, reject unsafe/non-production violations, and curl-probe it before generate; unreachable or curl-unavailable ends preflight as blocked (frontend-base-url-unreachable|curl-unavailable) so generate/map do not run.",
618
619
  "U/D cases must prove current-user data ownership or create cleanable current-user data or authorized Mock; otherwise blocked (current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable) without cross-user mutation.",
619
620
  "Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
620
- "Pipeline acceptance for frontend-test is frontend-test-result-v1 plus the main report under testcase/frontend/reports/frontend-test-report.html; case pass rate and outcome=passed are quality signals; retrospect is opt-in (frontendTest.reports.retrospect).",
621
+ `Pipeline acceptance for frontend-test is frontend-test-result-v1 plus the main report under ${layout.reportsDir}/frontend-test-report.html; case pass rate and outcome=passed are quality signals; retrospect is opt-in (frontendTest.reports.retrospect).`,
621
622
  blockingReview
622
623
  ? "frontendTest.reviewMode=blocking: a frontend case review must emit VERDICT: pass before checklist/manifest materialization; request-revision blocks browser execution."
623
624
  : "frontendTest.reviewMode is off|advisory by default: mechanical checklist-shell gates materialize/execute; LLM review is not a hard browser gate.",
@@ -650,12 +651,32 @@ export function buildFrontendTestHybridDag(sources) {
650
651
  tasks,
651
652
  };
652
653
  applyFrontendTestLayoutToDagSpec(spec, layout);
654
+ applyFrontendTestHarnessRootGuard(spec, layout);
653
655
  applyDefaultReadOnlyRetryPolicy(spec);
654
656
  stampGeneratedArtifactBindings(spec);
655
657
  parseDagSpec(spec);
656
658
  assertValidDagSpec(spec);
657
659
  return spec;
658
660
  }
661
+ /**
662
+ * D4: when the frozen frontend-test root lives under `.harness/` the whole tree
663
+ * is Git-ignored, so Git-status write guards would see empty diffs for real
664
+ * writes. Flip every file-writing node (and the dynamic browser child) from
665
+ * tools-only to the bounded filesystem snapshot channel instead.
666
+ */
667
+ export function applyFrontendTestHarnessRootGuard(spec, layout) {
668
+ if (!isHarnessNamespacePath(layout.testRoot))
669
+ return;
670
+ for (const task of spec.tasks) {
671
+ if (task.writeGuardPolicy === "tools-only") {
672
+ task.writeGuardPolicy = "filesystem-only";
673
+ }
674
+ const child = task.dynamicExpansion?.childTask;
675
+ if (child?.writeGuardPolicy === "tools-only") {
676
+ child.writeGuardPolicy = "filesystem-only";
677
+ }
678
+ }
679
+ }
659
680
  export function applyFrontendTestLayoutToDagSpec(spec, layout) {
660
681
  if (layout.isDefault) {
661
682
  spec.frontendTestLayout = layout;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Shared test-artifact namespace policy (D3).
3
+ *
4
+ * Both dedicated test workflows (backend-test, frontend-test) default to a
5
+ * canonical root under `.harness/testcase/`. That root is the ONLY `.harness`
6
+ * subtree those workflows may write; every other `.harness/**` path stays
7
+ * fail-closed for controller-owned facts.
8
+ *
9
+ * This module is the single source of truth shared by:
10
+ * - frontend/backend layout resolvers (canonical default roots)
11
+ * - task source-prepare path assessment (precise ancestor-gate exception)
12
+ * - DAG validators / writeSet gates / writer tool policies / shell guards
13
+ *
14
+ * It must stay dependency-free (pure functions + constants only) so both
15
+ * `src/task` and `src/workflows` can import it without cycles.
16
+ */
17
+ /** Canonical backend-test artifact root (repo-relative, posix). */
18
+ export const HARNESS_TESTCASE_ROOT = ".harness/testcase";
19
+ /** Canonical frontend-test artifact root (repo-relative, posix). */
20
+ export const HARNESS_TESTCASE_FRONTEND_ROOT = `${HARNESS_TESTCASE_ROOT}/frontend`;
21
+ /** taskKinds whose dedicated DAGs own the test-artifact namespace. */
22
+ export const TEST_ARTIFACT_TASK_KINDS = [
23
+ "backend-test",
24
+ "frontend-test",
25
+ ];
26
+ export function isTestArtifactTaskKind(value) {
27
+ return (typeof value === "string" &&
28
+ TEST_ARTIFACT_TASK_KINDS.includes(value));
29
+ }
30
+ /**
31
+ * Canonical test-artifact root for a dedicated test taskKind.
32
+ * backend-test -> `.harness/testcase`; frontend-test -> `.harness/testcase/frontend`.
33
+ */
34
+ export function canonicalTestArtifactRoot(kind) {
35
+ return kind === "frontend-test"
36
+ ? HARNESS_TESTCASE_FRONTEND_ROOT
37
+ : HARNESS_TESTCASE_ROOT;
38
+ }
39
+ /**
40
+ * True when a repo-relative posix path is covered by the test-artifact
41
+ * allowance for the given kind (exact canonical root or beneath it).
42
+ */
43
+ export function isTestArtifactPath(kind, repoRelativePosix) {
44
+ const root = canonicalTestArtifactRoot(kind);
45
+ return (repoRelativePosix === root ||
46
+ repoRelativePosix.startsWith(`${root}/`));
47
+ }
48
+ /**
49
+ * True when a repo-relative posix path is covered by EITHER dedicated test
50
+ * namespace (both kinds). Used by guards that authorize the exception but do
51
+ * not know which workflow owns the path.
52
+ */
53
+ export function isAnyTestArtifactPath(repoRelativePosix) {
54
+ return (isTestArtifactPath("backend-test", repoRelativePosix) ||
55
+ isTestArtifactPath("frontend-test", repoRelativePosix));
56
+ }
57
+ /**
58
+ * True when a repo-relative posix path sits anywhere beneath `.harness/`
59
+ * (controller/runtime namespace) — regardless of test-artifact allowance.
60
+ */
61
+ export function isHarnessNamespacePath(repoRelativePosix) {
62
+ return (repoRelativePosix === ".harness" ||
63
+ repoRelativePosix.startsWith(".harness/"));
64
+ }
65
+ /**
66
+ * True only for the baseline harness guard patterns (`.harness`, `.harness/**`).
67
+ * Deeper `.harness/<sub>/...` forbidden patterns are treated as explicit
68
+ * operator choices and stay authoritative over the test-artifact exception.
69
+ */
70
+ export function isHarnessBaselineForbiddenPattern(pattern) {
71
+ const normalized = pattern.trim().replace(/\\/g, "/");
72
+ return normalized === ".harness" || normalized === ".harness/**";
73
+ }
@@ -5,6 +5,7 @@ import { resolveRepairTaskForGate } from "./repair-artifact.js";
5
5
  import { topoSortToRanks } from "./topo.js";
6
6
  import { isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, OUTPUT_LIMIT_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, CONTEXT_OVERFLOW_RETRY_CATEGORY, } from "./retry-policy.js";
7
7
  import { isBackendTestCompletenessRetryCandidate } from "./backend-test-writer-completeness.js";
8
+ import { isAnyTestArtifactPath, isHarnessBaselineForbiddenPattern, } from "./test-artifact-namespace.js";
8
9
  import { defaultFrontendTestLayout, frontendTestLayoutFromSpec, } from "./frontend-test-layout.js";
9
10
  const GOVERNANCE_WARNING_TYPES = new Set([
10
11
  "read-only-missing-artifacts-forbidden",
@@ -58,9 +59,27 @@ function writeSetEntryWithinAllowed(entry, allowedPaths) {
58
59
  const probes = probePathsForWriteSetEntry(entry);
59
60
  return probes.some((probe) => allowedPaths.some((allowed) => pathMatchesPattern(probe, allowed)));
60
61
  }
61
- function writeSetEntryHitsForbidden(entry, forbiddenPaths) {
62
+ function writeSetEntryHitsForbidden(entry, forbiddenPaths, spec) {
62
63
  const probes = probePathsForWriteSetEntry(entry);
63
- return probes.some((probe) => forbiddenPaths.some((forbidden) => pathMatchesPattern(probe, forbidden)));
64
+ // D3 test-artifact exception: entries covered by a frozen dedicated-test
65
+ // layout root under `.harness/testcase/` are the only `.harness` writes a
66
+ // test DAG may declare. The exception is exact-namespace (canonical roots
67
+ // only), so `.harness/foo` or `.harness/dag-runs/x` stays forbidden.
68
+ const frozenTestRoots = [
69
+ spec?.backendTestLayout?.testRoot,
70
+ spec?.frontendTestLayout?.testRoot,
71
+ ].filter((root) => typeof root === "string" && isAnyTestArtifactPath(root));
72
+ return probes.some((probe) => forbiddenPaths.some((forbidden) => {
73
+ if (frozenTestRoots.some((root) => probe === root || probe.startsWith(`${root}/`)) &&
74
+ pathMatchesPattern(probe, forbidden)) {
75
+ // probe is inside a canonical frozen test root: the BASELINE
76
+ // harness guard patterns (`.harness` / `.harness/**`) are
77
+ // exempted; any explicit deeper or custom matching pattern
78
+ // still wins.
79
+ return !isHarnessBaselineForbiddenPattern(forbidden);
80
+ }
81
+ return pathMatchesPattern(probe, forbidden);
82
+ }));
64
83
  }
65
84
  function forbiddenPathsExcludeRootArtifacts(forbiddenPaths) {
66
85
  return ROOT_ARTIFACT_PATH_PROBES.some((probe) => forbiddenPaths.some((forbidden) => pathMatchesPattern(probe, forbidden)));
@@ -381,7 +400,7 @@ function validateTaskWritePolicy(task, spec, issues) {
381
400
  continue;
382
401
  }
383
402
  if (forbiddenPaths.length > 0 &&
384
- writeSetEntryHitsForbidden(entry, forbiddenPaths)) {
403
+ writeSetEntryHitsForbidden(entry, forbiddenPaths, spec)) {
385
404
  issues.push({
386
405
  type: "write-set-forbidden",
387
406
  message: `task ${task.id} writeSet entry "${entry}" matches forbiddenPaths`,
@@ -9,7 +9,7 @@
9
9
  ```text
10
10
  src/commands/dag-run-task.ts runDagRunTask
11
11
  → src/application/dag/generate-task-dag.ts generateTaskDagUseCase
12
- └─ src/workflows/dag/init-hybrid.ts initHybridDagFromTask (生成 DagSpec)
12
+ └─ src/workflows/dag/init-hybrid.ts initHybridDagFromTask (生成 DagSpec;模板族在 hybrid/templates/,输入准备在 hybrid/types.ts + hybrid/sources.ts)
13
13
  └─ src/application/dag/validate-dag.ts validateDagUseCase (候选 + 最终校验)
14
14
  └─ assertSafeForExecution (执行前安全检查)
15
15
  └─ src/application/dag/run-dag.ts runDagUseCase (执行)
@@ -43,7 +43,7 @@ src/commands/dag-validate.ts runDagValidate
43
43
 
44
44
  ### 专用 taskKind 与模板选择(架构摘要)
45
45
 
46
- - `initHybridDagFromTask` 按 `task.json.taskKind`(及前端需求自动分类)选择专用拓扑:`frontend-implementation`、`frontend-test`、`backend-test`、`knowledge-sync`、`knowledge-graph-bootstrap` 等;默认 `standard` 走 governance profile 通用实现链。
46
+ - `initHybridDagFromTask` 按 `task.json.taskKind`(及前端需求自动分类)选择专用拓扑:`frontend-implementation`、`frontend-test`、`backend-test`、`knowledge-sync`、`knowledge-graph-bootstrap` 等;默认 `standard` 走 governance profile 通用实现链。入口只做分发与治理装饰,六个模板 builder 在 `src/workflows/dag/hybrid/templates/`。
47
47
  - **backend-test(0.17.x)**:Markdown-first 固定短链(环境硬门 → Markdown 用例/Review → 单次 pytest + HTML → 报告与 L-5);不在本页展开节点清单,见 `docs/runtime/backend-test-workflow.md` 与 completed markdown-first plan。
48
48
  - 专用模板不是新的 governance profile:风险等级仍由 profile 规则判断;写边界仍受 `allowedPaths` / `forbiddenPaths` / `writeSet` 约束。
49
49
 
@@ -73,7 +73,7 @@ src/commands/dag-validate.ts runDagValidate
73
73
  - schema:`src/workflows/dag/types.ts`
74
74
  `dagNodeExecutorSchema = z.enum(["pi","shell","static"])`;DagNodeExecutor 默认 `"pi"`(`executor: dagNodeExecutorSchema.default("pi")`)。
75
75
  - `executor: "cursor"` 在 schema refine 阶段抛 `CURSOR_DAG_EXECUTOR_REMOVED_ERROR`(Pi-only = ADR 0001)。
76
- - Pi handler 先 `requireModel(input)` 校验已解析 model,再委托 `executeDagPiNode`(`src/executors/dag-pi-executor.ts`)。
76
+ - Pi handler 先 `requireModel(input)` 校验已解析 model,再委托 `executeDagPiNode`(`src/executors/dag-pi-executor.ts`;领域工具、plan ledger、分段会话在 `src/executors/dag-pi/`)。
77
77
  - shell handler = `executeDagShellNode`(`src/executors/shell-executor.ts`);static handler = `executeDagStaticNode`(`src/executors/dag-static-executor.ts`)。
78
78
 
79
79
  Executor 不得依赖 commands 或 CLI formatting;Cursor 不在受治理路径(`runtime-boundaries.md` §Executors)。
@@ -42,15 +42,15 @@
42
42
  - `frontend-test-dag.json` — frontend-test DAG 模板;其 deterministic checklist 会拒绝 alternative executable instructions,仅允许 allowlisted `playwright-cli` command。Pi generator/reviser 的 runtime writeSet 仅授权下列目标项目生成路径(不是本仓库文档索引目标):
43
43
 
44
44
  ~~~text
45
- testcase/frontend/cases/FE-*.md
46
- testcase/frontend/cases/index.md
47
- testcase/frontend/cases/manifest.draft.json
45
+ .harness/testcase/frontend/cases/FE-*.md
46
+ .harness/testcase/frontend/cases/index.md
47
+ .harness/testcase/frontend/cases/manifest.draft.json
48
48
  ~~~
49
49
 
50
50
  Pi writer writeSet 不包含 final manifest(文件名 manifest.json)。依赖 checklist 的 exclusive shell materializer 是 final manifest 的唯一写入者,以 temp+rename 原子生成 final,后续 map 只消费该 materializer 输出。
51
51
  - `frontend-test-dag.retrieve-context.prompt.md`、`frontend-test-dag.generate-cases.prompt.md`、`frontend-test-dag.review-cases.prompt.md`、`frontend-test-dag.review-execution.prompt.md`、`frontend-test-dag.retrospect.prompt.md` — 上下文、用例、审查、执行审查和复盘提示。
52
52
  - `frontend-test-case-checklist.md` — 前端测试用例检查清单。
53
- - `frontend-test-standard-scenarios.v1.json` — frontend-test 标准场景规范(generate 覆盖 must 场景;运行时可物化到目标项目 testcase/frontend/rag 下的 standard-scenarios.v1.json)。
53
+ - `frontend-test-standard-scenarios.v1.json` — frontend-test 标准场景规范(generate 覆盖 must 场景;运行时可物化到目标项目 .harness/testcase/frontend/rag 下的 standard-scenarios.v1.json)。
54
54
 
55
55
  ## 知识同步与配置
56
56
 
@@ -6,9 +6,9 @@
6
6
 
7
7
  ## 当前合同
8
8
 
9
- - 输入:最终 `testcase/md/**`、run-owned 环境报告与 Markdown 校验报告,以及有界的 pytest config / `conftest.py`。
9
+ - 输入:最终 `.harness/testcase/md/**`、run-owned 环境报告与 Markdown 校验报告,以及有界的 pytest config / `conftest.py`。
10
10
  - 禁止重新读取 `source/**`、新增测试场景、重新分配 AC、执行 pytest、修改生产代码/配置或输出业务 JSON。
11
- - 写入范围:`testcase/**/test_*.py`、`testcase/**/helpers/**`、`testcase/**/factories/**`。
11
+ - 写入范围:`.harness/testcase/**/test_*.py`、`.harness/testcase/**/helpers/**`、`.harness/testcase/**/factories/**`。
12
12
  - pytest 可以使用模块级 `test_*` 函数,也可以使用 pytest 测试类中的 `test_*` 方法;不得为了追溯强迫目标项目放弃现有 class-based 风格。
13
13
  - 每个最终 Markdown Case ID 必须出现在至少一个真实测试函数/方法区域中,优先同时出现在函数/方法名和 docstring:
14
14
 
@@ -26,7 +26,7 @@ class TestOrderApi:
26
26
  - 禁止 `skip` / `xfail`、吞断言、宽异常静默通过、mock 替代真实目标、删除用例或弱化断言。
27
27
  - best-effort 清理只能捕获所选 HTTP client 实际抛出的窄 transport exception,例如 `requests.RequestException` 或 `urllib.error.URLError`;禁止 `except:`、`except Exception`、`except BaseException` 后 `pass`。
28
28
  - 同一 Case ID 可以由多个 pytest 函数覆盖;额外映射会进入 traceability 报告,但不能伪造未在 Markdown 中定义的业务场景。
29
- - 生成 pytest 文件名必须与 Markdown 模块一一对应:`testcase/md/<module>.md` → `testcase/test_<module>.py`(`<module>` 为文件名去 `.md` 后小写、非字母数字转 `_`)。即使 Markdown `自动化映射` 写了别的路径,也必须写模块 stem 路径,不得额外发明 `test_be_*` 前缀。
29
+ - 生成 pytest 文件名必须与 Markdown 模块一一对应:`.harness/testcase/md/<module>.md` → `.harness/testcase/test_<module>.py`(`<module>` 为文件名去 `.md` 后小写、非字母数字转 `_`)。即使 Markdown `自动化映射` 写了别的路径,也必须写模块 stem 路径,不得额外发明 `test_be_*` 前缀。
30
30
  - 测试失败必须诚实保留,后续节点只执行一次 pytest,并从同一 pytest-html 报告生成 HTML/facts。
31
31
 
32
32
  ## 推荐输出