@tea-agent/loop-agent 0.44.0-next.12 → 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 (122) hide show
  1. package/CHANGELOG.md +15 -2
  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-write-guard.js +24 -67
  8. package/dist/governance/exec-plans.js +10 -1
  9. package/dist/task/config-types.js +4 -2
  10. package/dist/task/source-prepare/build-draft.js +1 -0
  11. package/dist/task/source-prepare/path-policy.js +17 -7
  12. package/dist/worker/console/chat/browser-automation.js +343 -38
  13. package/dist/worker/console/chat/browser-content.js +74 -0
  14. package/dist/worker/console/chat/browser-diagnostics.js +250 -0
  15. package/dist/worker/console/chat/browser-evaluation.js +58 -0
  16. package/dist/worker/console/chat/browser-operation-queue.js +19 -0
  17. package/dist/worker/console/chat/browser-pick.js +113 -0
  18. package/dist/worker/console/chat/browser-routes.js +33 -5
  19. package/dist/worker/console/chat/browser-screenshot-result.js +47 -0
  20. package/dist/worker/console/chat/browser-storage.js +89 -0
  21. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +125 -174
  22. package/dist/worker/console/chat/pi-runtime.js +1 -1
  23. package/dist/worker/console/chat/repo-browser.js +80 -2
  24. package/dist/worker/console/chat/repo-preview-kind.js +155 -0
  25. package/dist/worker/console/chat/session-mode.js +1 -1
  26. package/dist/worker/console/chat/session-store.js +4 -2
  27. package/dist/worker/console/chat/tools.js +12 -0
  28. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-r1NAPU_5.js → abnfDiagram-N423BO3Z-BA7qCL_r.js} +1 -1
  29. package/dist/worker/console/static/assets/{arc-dT5T4sVL.js → arc-Df09ajwL.js} +1 -1
  30. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP_cOq46.js → architectureDiagram-T3A2C74G-CpF9sw5R.js} +1 -1
  31. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-sToEZxEf.js → blockDiagram-VBNYF7ZC-BtPys8ja.js} +1 -1
  32. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DSxwwi2m.js → c4Diagram-5PPSVZJV-DZPnTWqT.js} +1 -1
  33. package/dist/worker/console/static/assets/channel-DiTNLZUm.js +1 -0
  34. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B9fh0IkF.js → chunk-2GRJ4B5K-BD9D4h1p.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CAUNl-A6.js → chunk-2Q5K7J3B-Svn8yBmn.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BomeqbNX.js → chunk-5RXB4S5H-B8EbQj-S.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BNAuTOLE.js → chunk-5VM5RSS4-tENZpcpF.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DfzjRD5y.js → chunk-6Q2QTUOP-DBx71Lco.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3uE-yc2.js → chunk-GF5L2VYU-1v_QamMz.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-C3sQMMYr.js → chunk-JWPE2WC7-BAMCNNBG.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D_AtEYQL.js → chunk-KBJHAD2P-BvuEury-.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DW4TwhKv.js → chunk-RYQCIY6F-YqFuHp8s.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CTQDmJcp.js → chunk-XXDRQBXY-ouI5T7Mr.js} +1 -1
  44. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgL8aiyZ.js +1 -0
  45. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgL8aiyZ.js +1 -0
  46. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-iMqPuBsL.js → cose-bilkent-JH36ORCC-Bri0_Izk.js} +1 -1
  47. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DktIuDWs.js → cynefin-VYW2F7L2-e8Vhhi4X.js} +1 -1
  48. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BgVnjyEs.js → cynefinDiagram-MW4NZA55-tSIXGQiJ.js} +1 -1
  49. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CR25E4bu.js → dagre-VZM6K2ZE-Rl-sF5pt.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CPM2h5HY.js → diagram-7IWD3JNH-Db6ddPz8.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqF0x2g1.js → diagram-B4RE2ZJO-Jwd_Xw5S.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-G8BYp2eb.js → diagram-LBJQPF4R-CeC_8InC.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BbC4kT_D.js → diagram-Q27KOJAE-LKPRaNq8.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CKZwZ-G5.js → diagram-UB23O5K3-DlVQytMW.js} +1 -1
  55. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kJDmheia.js → ebnfDiagram-BXEA7PRR-DnfJZwO-.js} +1 -1
  56. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DFTVHyxB.js → erDiagram-JOGREHBK-3axVSXca.js} +1 -1
  57. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqdN5_os.js → flowDiagram-UKHOOZJN-xe04-_L-.js} +1 -1
  58. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-SYDjVakh.js → ganttDiagram-PKOTCBZU-hpmRINiD.js} +1 -1
  59. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js → gitGraphDiagram-DS77QQ5N-6HSNz9Ko.js} +1 -1
  60. package/dist/worker/console/static/assets/index-C_8_gWal.css +1 -0
  61. package/dist/worker/console/static/assets/{index-CTMZsDmh.js → index-DybcVXCu.js} +102 -100
  62. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DIeAmJ45.js → infoDiagram-6WML65LV-BF6ksWTX.js} +1 -1
  63. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-oikqUAlo.js → ishikawaDiagram-WSZJBQD7-DXisdeD7.js} +1 -1
  64. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-mKEF_xYT.js → journeyDiagram-NVQOT4AX-BWu65wmu.js} +1 -1
  65. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdWJHPnN.js → kanban-definition-27J2QSJJ-hfNhHqAI.js} +1 -1
  66. package/dist/worker/console/static/assets/{linear-DGRlieMH.js → linear-D6HTfQcp.js} +1 -1
  67. package/dist/worker/console/static/assets/{mermaid.core-4P9yPC9c.js → mermaid.core-Iv91R1WP.js} +5 -5
  68. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-5PTSkos6.js → mindmap-definition-FAOFIHXS-CfPChwjR.js} +1 -1
  69. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DONIFpG5.js → pegDiagram-VL7TDLO6-B7TcoqHz.js} +1 -1
  70. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D_Z4S70M.js → pieDiagram-7S7Q4E2Y-ouyPKMtQ.js} +1 -1
  71. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CAsjh8iY.js → quadrantDiagram-CIZ2JOQS-B9tuyLbh.js} +1 -1
  72. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cx4Dd6ac.js → railroadDiagram-AXF67PYL-Ck4q9nJs.js} +1 -1
  73. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-cQb7mwyH.js → requirementDiagram-LRYGKXZP-wXLX5Dnp.js} +1 -1
  74. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cd-DhUdf.js → sankeyDiagram-W5VNT64P-D2XWw6_m.js} +1 -1
  75. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-AlKYRgIk.js → sequenceDiagram-SI44F4Z6-DzxKiug4.js} +1 -1
  76. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CUy_ET4e.js → sizeCapture-X5ZJPWSS-DWUqK5Us.js} +1 -1
  77. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CTuVe6Sa.js → stateDiagram-OKZ733FA-D5Pa4O5g.js} +1 -1
  78. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C0QIbqO4.js +1 -0
  79. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ChFLJb-v.js → swimlanes-SLNWSIFB-VH7cRnsJ.js} +2 -2
  80. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BB9CzRHB.js +8 -0
  81. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DZZeiAPr.js → timeline-definition-Z64GVDOM-C4HM5NoZ.js} +1 -1
  82. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-t4O91cZU.js → vennDiagram-T6HMQDX7-Bu-4hOlA.js} +1 -1
  83. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CV5qGHZu.js → wardleyDiagram-T6FBY63Y-CasFEvvl.js} +1 -1
  84. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DyXbytUI.js → xychartDiagram-ELKLHX3M-D-gpwPSA.js} +1 -1
  85. package/dist/worker/console/static/index.html +2 -2
  86. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +57 -9
  87. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +6 -0
  88. package/dist/worker/console/workspace-context.js +1 -0
  89. package/dist/worker/materialize/harness-task-lineage.js +4 -2
  90. package/dist/worker/observe/static/console-theme.css +33 -29
  91. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +3 -1
  92. package/dist/workflows/dag/backend-test-layout.js +27 -7
  93. package/dist/workflows/dag/backend-test-markdown-workflow.js +3 -1
  94. package/dist/workflows/dag/frontend-test-case-checklist.js +12 -3
  95. package/dist/workflows/dag/frontend-test-environment-probe.js +0 -2
  96. package/dist/workflows/dag/frontend-test-layout.js +41 -12
  97. package/dist/workflows/dag/hybrid/sources.js +21 -2
  98. package/dist/workflows/dag/hybrid/templates/backend-test.js +65 -50
  99. package/dist/workflows/dag/hybrid/templates/frontend-test.js +58 -37
  100. package/dist/workflows/dag/test-artifact-namespace.js +73 -0
  101. package/dist/workflows/dag/validate.js +22 -3
  102. package/docs/templates/README.md +4 -4
  103. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -3
  104. package/docs/templates/backend-test-dag.json +708 -708
  105. package/docs/templates/backend-test-dag.review-cases.prompt.md +3 -3
  106. package/docs/templates/frontend-test-case-checklist.md +2 -2
  107. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +7 -7
  108. package/docs/templates/frontend-test-dag.json +46 -37
  109. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +2 -2
  110. package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
  111. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  112. package/package.json +7 -2
  113. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  114. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  115. package/skills/playwright-cli/SKILL.md +1 -1
  116. package/skills/playwright-cli-case-generator/SKILL.md +4 -4
  117. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +0 -1
  118. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +0 -1
  119. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +0 -1
  120. package/dist/worker/console/static/assets/index-B28Onmfy.css +0 -1
  121. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +0 -1
  122. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +0 -8
package/CHANGELOG.md CHANGED
@@ -4,9 +4,18 @@
4
4
 
5
5
  ### 新增
6
6
 
7
+ - Console Chat 浏览器控制补齐页面 JavaScript、登录态导入导出、带行上下文的元素查找、Markdown 内容抽取和外部窗口人工点选;条件等待可确认文本、元素或网址变化,多个对话保持各自选中的标签页,旧页面快照不会被误用于其他页面。
8
+ - 浏览器截图可指定标签页和完整页面,作为图片发送给支持视觉的模型,并在工具详情中查看保存的截图;不支持视觉或图片过大时明确提示。右侧网页预览明确标注为独立页面,避免与实际受控窗口的登录态、操作混淆。
9
+ - 登录态文件保存在仓库外,默认不把 cookie 值写入工具结果;导入显式合并 cookies/localStorage,保留未列出的现有数据。页面脚本超时后提示等待恢复或关闭该标签页;现有 Chrome 接管和 frontend-test 自动加载登录态仍为后续能力。
10
+ - Operator Chat 的「预览」面板支持图片预览:`.png` / `.jpg` / `.jpeg` / `.gif` / `.webp` / `.bmp` / `.ico` / `.svg` 直接在预览区展示,宽图按面板宽度缩放、超高图纵向滚动;位图不再误走文本解码、不再提示「binary files cannot be previewed」。SVG 可当图查看,也能切回 XML 源码;新窗口打开图片直接展示原图。
11
+ - 无渲染器的二进制容器(zip/mp4/pdf/docx 等常见格式)在预览区显示「此文件类型无法预览」空态说明,不再报错;未知后缀仍按文本读取,含二进制内容时保持明确报错。
12
+ - 前后端测试工作流的产物默认收进统一的运行时目录:后端测试默认写到运行时目录下的测试区,前端浏览器测试默认写到运行时目录下的测试区,不再混入项目根目录生成 `testcase/`;自定义目录仍可继续使用,已有产物不迁移不删除。新目录随运行时目录一起被 Git 忽略,测试运行仍会逐文件对比哈希来证明真实产出,不会因为目录被忽略而误判为“没有写入”;控制器自己的运行记录不受影响。
13
+ - 项目初始化生成的忽略规则新增 `.playwright-cli/`(浏览器测试工具缓存目录),避免工具缓存被误提交;已初始化的项目按统一更新机制刷新,用户自己写的忽略规则保持不变。 5642d882 (feat(test-workflows): 前后端测试产物默认收进运行时目录,init gitignore 增 .playwright-cli/)
7
14
  - 前端 DAG 的行为验证不再只认 Vitest:测试框架改为可插拔适配层,首批支持 Vitest、Jest 与 Playwright Test。三类项目都能用**真实报告**证明契约绑定的测试文件确实执行且通过,`npm`/`pnpm`/`yarn` 的常用测试入口走同一套识别逻辑,直接命令与 `package.json` 脚本不会再得出不同结论。
8
15
  - 未适配的测试框架(Cypress、`node --test`、Mocha、自定义脚本等)现在明确报「缺少适配能力」,并附上具体命令与原因;此前这类命令会被判为不支持,错误信息还暗示你「必须改用 Vitest」,把工具的能力缺口显示成了用户配置错误。
9
16
  - 行为验证保留「测试确实执行过」的校验:零测试、全部跳过、报告缺失、报告过期、契约绑定文件未执行都判失败,不会退回只看退出码(全部跳过的运行退出码是 0)。
17
+ - Console 对话中的浏览器工具支持原生下拉选择、显式勾选/取消,以及按标签页增量读取控制台日志和网络请求状态;诊断记录有容量上限并提示历史缺口,便于定位页面异常、接口失败与弹窗问题,无需打开额外诊断面板。
18
+ - 前端测试随 loop-agent 安装固定版本的 Playwright CLI,无需另装全局 CLI;运行时使用当前 loop-agent 安装的依赖,避免目标项目或 PATH 中的其他版本干扰。依赖缺失或版本不符时明确失败并提示重新安装;浏览器仍需单独准备,不在安装时自动下载。
10
19
 
11
20
  - main 上的 npm `next` 预发布改为跑默认测试(fast + integration),不再在每次发布时重复跑约 6 分钟的 I/O-heavy 池;完整三池仍由 PR、正式 release 线、本地推送前检查与发布前钩子承担。同时把该预发布任务的 I/O 并发从 1 调到 2,并把 npm 包可见性确认的等待窗口从约 3 分钟拉长到约 10 分钟,避免包已发出却因仓库异步处理未完成而被判失败。
12
21
 
@@ -27,7 +36,7 @@
27
36
 
28
37
  - Console 操作文案改为面向操作者的说明并给出下一步:等待超时不再伪装成「需要核对」,改为提示操作可能仍在后台运行;「源内容已变更」「前端拆分状态被并发修改」等原本直接展示英文原文的错误,改为说明原因与具体处理步骤;恢复页按钮与提示去掉 reconcile / supersede / orphan / Human Gate 等术语。
29
38
 
30
- - Operator Chat 新增对话驱动的真实浏览器自动化:右侧「浏览器」面板同步共享状态,登录态与下载隔离在仓库外;模型可操作同一浏览器完成导航、搜索、结构化快照、元素操作与截图,不读写仓库文件,也不暴露页面脚本执行。
39
+ - Operator Chat 新增对话驱动的真实浏览器自动化:右侧「浏览器」面板同步网址,登录态与下载隔离在仓库外;模型通过外部受控窗口完成导航、搜索、结构化快照、元素操作与截图,页面脚本执行由显式 browser_eval 提供。
31
40
  - 模型驱动的共享浏览器默认改为可见外部窗口,并保留右侧预览;地址、搜索与元素操作不再默认生成大截图,搜索逐字填写并提交真实表单,启动时降低自动化指纹;无法嵌入的页面提供外部打开与聚焦入口,同一持久配置可完成站点安全验证。
32
41
  - 修复观测页「执行历史」把仍在重试的节点误报为「最终失败」的问题:只要节点最后一个已记录的尝试失败,就会显示「N 次 · 最终失败」,即使该节点仍在运行、重试已经排队,会让人误判运行已结束。现在节点处于运行/已启动/暂停状态时改为「N 次 · 最近一次失败,仍在重试」,真正到达终态才显示「最终失败」;修复轮次的多轮标签同样处理。
33
42
 
@@ -141,6 +150,8 @@
141
150
 
142
151
  ### 改进
143
152
 
153
+ - Console「浅色」主题改为 DeepSeek Harness 浅色家族:白底、侧栏冷灰、近黑正文、发送与主操作为 DeepSeek 蓝,用户气泡为淡蓝;默认暖纸和深色主题不变。
154
+ - 对话时间线的「紧凑 / 完整」改为 DSH 文字 Tab:去掉小卡片边框与全局 34px 按钮高度,选中项用品牌色下划线。
144
155
  - DAG 运行时内部按职责拆开:模型配置、领域工具、分段会话、模板族、终态判定与检查点各自独立维护,测试也按行为分成可单独调度的文件。命令、任务流程与用户可见结果不变。本机默认测试并行下整池耗时未证实变快。
145
156
  - Operator Chat 输入框下方的会话统计对齐 DSH:长串静态文字收敛为两个可点击摘要,可分别查看模型/工具用时、TTFT、TPS 与 Token 分项;详情卡互斥并支持外部点击或 Escape 关闭,窄屏打开时不会推动输入框或消息流。
146
157
  - Operator Chat 长会话新增完整轮次轨道与按需历史定位,可从任意用户消息精确新建会话;历史图片刷新后仍可预览,仓库文件链接可直接定位到行,最终回答显示单轮 token/耗时,并支持可持久化的紧凑/完整阅读模式。图片文件与消息记录分离保存,删除、分叉和失败回滚会同步清理,附件读取重新校验类型与引用。
@@ -154,7 +165,9 @@
154
165
 
155
166
  ### 修复
156
167
 
157
- - 前端 DAG 的验证失败不再把配置问题算到业务代码头上:报告缺失或过期、报告协议错误、命令或脚本哈希漂移、框架未适配都归为验证配置问题,不再触发一轮无意义的实现重写;测试断言失败仍归实现问题。
168
+ - Console Chat 的浏览器搜索工具在更多真实站点上可用:现代搜索框(`textarea`、`role=searchbox`、`name=search`)不再报“找不到搜索框”;对只在页面内更新结果、不发生跳转的搜索表单也不会超时失败;提交时只在搜索框所属表单内查找提交按钮,不再误点页面上无关的按钮导致“已输入但未提交”。
169
+ - 浏览器导航在页面于加载完成后才发生跳转(如搜索引擎跳转到区域站点)时,不再把后续操作带入上一页;跳转后会等待短暂稳定再返回。
170
+ - 浏览器搜索的默认搜索引擎改为 Bing(可从参数改写)。此前默认的百度对自动化已不可用(新版为文本框加验证码),模型不指定引擎时搜索会直接失败。
158
171
  - 已经自带 `--reporter`/`--outputFile` 的测试命令或脚本不会被悄悄覆盖;无法安全注入报告参数时在生成阶段直接报错,要求改用可注入的入口。
159
172
  - 修复 Playwright 项目在 Review 阶段被误判失败的问题:审查重读报告时没有带上协议,仍按 Vitest 解析,导致 Playwright 报告因缺少 `testResults` 报错,即使执行与 trace 已经通过。report 引用现在记录协议,review 按同一协议解析并校验一致性。
160
173
  - 修复 Playwright 报告路径按 `testDir` 解析错误的问题:报告中的文件路径相对 Playwright 自身的 `config.rootDir`,此前按进程 cwd 解析,配了 `testDir: "./tests"` 的项目会得到 `orders.spec.js` 而非 `tests/orders.spec.js`,契约目标永远无法匹配。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.44.0-next.12",
4
- "gitSha": "91181001a8cbed75a00a8a6d8e76ae5fab889b36",
5
- "builtAt": "2026-09-17T10:04:11.141Z"
3
+ "version": "0.44.0-next.13",
4
+ "gitSha": "03e91c507b0ab0f87e6dd376bb7bb735b8e5590e",
5
+ "builtAt": "2026-09-18T01:29:28.992Z"
6
6
  }
@@ -872,6 +872,7 @@ export function buildManagedGitignoreBlock(governanceRoot = DEFAULT_GOVERNANCE_R
872
872
  GITIGNORE_BLOCK_START,
873
873
  "# loop-agent init surface: rebuildable outputs; AGENTS.md and README.md stay shared",
874
874
  ".harness/",
875
+ ".playwright-cli/",
875
876
  ".agents/",
876
877
  ".task-pool/",
877
878
  ".worktrees/",
@@ -38,7 +38,7 @@ import { isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, is
38
38
  import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestCollectionRepairOutcomeRecoveryCandidate, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
39
39
  import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
40
40
  import { assessBackendTestPlanProtocol } from "../workflows/dag/backend-test-plan-protocol.js";
41
- import { frontendTestLayoutFromSpec } from "../workflows/dag/frontend-test-layout.js";
41
+ import { frontendTestContextRel, frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
42
42
  import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
43
43
  import { writeEffectiveContextReceipt } from "../workflows/dag/context-receipt.js";
44
44
  import { estimateFrontendPlanRequirementRecordCalls, isWriterThinkingExhausted, readWriterThinkingExhaustionEvidence, WRITER_BUDGET_EXHAUSTED_CATEGORY, WRITER_TOKEN_BUDGET, FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY, mapWithConcurrency, buildFrontendPlanWorkload, loadOrCreateFrontendPlanCoverageLayout, collectFrontendVerificationCommandFiles, aggregateParallelPiResults, combineSequentialPiResults, runFrontendScoutParallelSessions, runFrontendContractSegmentedSessions, runFrontendPlanSegmentedSessions, runFrontendReviewSegmentedSessions, runFrontendScoutSegmentedSessions, } from "./dag-pi/sessions/index.js";
@@ -1552,7 +1552,7 @@ async function writePiExtensionsArtifact(nodeArtifactsDir, requested, resolved,
1552
1552
  // best-effort run fact; node execution continues
1553
1553
  }
1554
1554
  }
1555
- async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir) {
1555
+ async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir, layout) {
1556
1556
  try {
1557
1557
  const capability = JSON.parse(await readFile(path.join(runDir, "preflight-frontend-browser-tool-shell", "frontend-browser-capability.json"), "utf8"));
1558
1558
  if (capability.status !== "ready" ||
@@ -1563,7 +1563,7 @@ async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir)
1563
1563
  catch {
1564
1564
  throw new Error("browser-command-capability-unavailable: missing or invalid frontend browser capability artifact");
1565
1565
  }
1566
- const contextPath = path.join(workspaceRoot, "testcase", "frontend", "rag", "context.md");
1566
+ const contextPath = path.join(workspaceRoot, ...frontendTestContextRel(layout).split("/"));
1567
1567
  let markdown;
1568
1568
  try {
1569
1569
  markdown = await readFile(contextPath, "utf8");
@@ -1759,8 +1759,13 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
1759
1759
  let beforeStatus;
1760
1760
  let beforeWorkspaceSnapshot;
1761
1761
  let beforePathFingerprints;
1762
- /** tools-only and filesystem-only keep the tool sandbox but skip Git baseline. */
1763
- const filesystemOnly = isWriteTask && meta.spec.backendTestWorkspaceControl === "filesystem-only";
1762
+ /** tools-only and filesystem-only keep the tool sandbox but skip Git baseline.
1763
+ * filesystem-only applies when the node froze it in writeGuardPolicy (D4:
1764
+ * any dedicated test writer under an ignored root) OR the backend-test spec
1765
+ * explicitly selected workspaceControl=filesystem-only. */
1766
+ const filesystemOnly = isWriteTask &&
1767
+ (input.task.writeGuardPolicy === "filesystem-only" ||
1768
+ meta.spec.backendTestWorkspaceControl === "filesystem-only");
1764
1769
  const skipGitWriteGuard = isWriteTask && (input.task.writeGuardPolicy === "tools-only" || filesystemOnly);
1765
1770
  if (isWriteTask && filesystemOnly) {
1766
1771
  try {
@@ -1880,7 +1885,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
1880
1885
  nodeId: input.task.id,
1881
1886
  caseId,
1882
1887
  evidenceDir,
1883
- baseUrl: await resolveValidatedFrontendBaseUrlFromContext(input.cwd, meta.runDir),
1888
+ baseUrl: await resolveValidatedFrontendBaseUrlFromContext(input.cwd, meta.runDir, layout),
1884
1889
  inputRoot: layout.fixturesDir,
1885
1890
  layout,
1886
1891
  };
@@ -1,6 +1,7 @@
1
1
  import { access as fsAccess, lstat, mkdir as fsMkdir, readFile as fsReadFile, realpath, writeFile as fsWriteFile, } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { pathMatchesPattern } from "../shared/git-progress.js";
4
+ import { isAnyTestArtifactPath, isHarnessBaselineForbiddenPattern, } from "../workflows/dag/test-artifact-namespace.js";
4
5
  export class PiWriterPathDeniedError extends Error {
5
6
  code;
6
7
  targetPath;
@@ -105,12 +106,60 @@ function repoRelativePath(root, absoluteTarget, raw, allowRoot) {
105
106
  }
106
107
  return relative;
107
108
  }
109
+ /**
110
+ * D3 test-artifact exception at the tool layer: a write target inside the
111
+ * canonical `.harness/testcase` namespace that the task's exclusive writeSet
112
+ * (or, for mkdir, a writeSet entry at/above it) explicitly owns is exempt
113
+ * from the BASELINE harness guard patterns (`.harness` / `.harness/**`) only.
114
+ * Explicit deeper forbidden patterns and the hard controller-facts guard stay
115
+ * authoritative, matching the DAG validator and the post-hoc write guard.
116
+ */
117
+ function testArtifactWriteExempt(relative, ctx) {
118
+ const forbidden = ctx.forbiddenPaths ?? [];
119
+ let baselineHit = false;
120
+ for (const pattern of forbidden) {
121
+ if (!matchesAnyPattern(relative, [pattern]))
122
+ continue;
123
+ if (!isHarnessBaselineForbiddenPattern(pattern))
124
+ return false;
125
+ baselineHit = true;
126
+ }
127
+ if (!baselineHit)
128
+ return false;
129
+ if (!isAnyTestArtifactPath(relative))
130
+ return false;
131
+ const writeSet = ctx.writeSet ?? [];
132
+ if ((ctx.writePolicy ?? "exclusive") === "exclusive") {
133
+ return matchesAnyPattern(relative, writeSet);
134
+ }
135
+ return writeSet.length === 0 || matchesAnyPattern(relative, writeSet);
136
+ }
137
+ function testArtifactMkdirExempt(relative, ctx) {
138
+ const forbidden = ctx.forbiddenPaths ?? [];
139
+ let baselineHit = false;
140
+ for (const pattern of forbidden) {
141
+ if (!pathMatchesPattern(relative, pattern))
142
+ continue;
143
+ if (!isHarnessBaselineForbiddenPattern(pattern))
144
+ return false;
145
+ baselineHit = true;
146
+ }
147
+ if (!baselineHit)
148
+ return false;
149
+ if (!isAnyTestArtifactPath(relative))
150
+ return false;
151
+ const writeSet = (ctx.writeSet ?? []).map(normalizeRepoRelative);
152
+ return writeSet.some((candidate) => isPathAtOrBelowDirectory(candidate, relative) ||
153
+ (relative.length > 0 && pathMatchesPattern(relative, candidate)));
154
+ }
108
155
  function assertWritableRelative(relative, ctx) {
109
156
  if (isHardForbidden(relative)) {
110
157
  throw new PiWriterPathDeniedError("hard-forbidden", relative, "writes under .harness/tasks/** or .harness/dag-runs/** are never allowed for Pi writers");
111
158
  }
112
159
  const forbidden = ctx.forbiddenPaths ?? [];
113
- if (forbidden.length > 0 && matchesAnyPattern(relative, forbidden)) {
160
+ if (forbidden.length > 0 &&
161
+ matchesAnyPattern(relative, forbidden) &&
162
+ !testArtifactWriteExempt(relative, ctx)) {
114
163
  throw new PiWriterPathDeniedError("forbidden", relative);
115
164
  }
116
165
  const writeSet = ctx.writeSet ?? [];
@@ -130,7 +179,8 @@ function assertMkdirRelative(relative, ctx) {
130
179
  const forbidden = ctx.forbiddenPaths ?? [];
131
180
  if (relative &&
132
181
  forbidden.length > 0 &&
133
- matchesAnyPattern(relative, forbidden)) {
182
+ matchesAnyPattern(relative, forbidden) &&
183
+ !testArtifactMkdirExempt(relative, ctx)) {
134
184
  throw new PiWriterPathDeniedError("forbidden", relative);
135
185
  }
136
186
  const writeSetCandidates = (ctx.writeSet ?? [])
@@ -149,8 +199,14 @@ function assertMkdirRelative(relative, ctx) {
149
199
  if (nonHardForbiddenCandidates.length === 0) {
150
200
  throw new PiWriterPathDeniedError("hard-forbidden", relative || ".");
151
201
  }
152
- if (forbidden.length > 0 &&
153
- nonHardForbiddenCandidates.every((candidate) => matchesAnyPattern(candidate, forbidden))) {
202
+ // D3 test-artifact exception: when the target directory is a canonical
203
+ // namespace root owned by the writeSet, baseline harness guard patterns do
204
+ // not disqualify allowedPaths candidates either.
205
+ const effectiveForbidden = testArtifactMkdirExempt(relative, ctx)
206
+ ? forbidden.filter((pattern) => !isHarnessBaselineForbiddenPattern(pattern))
207
+ : forbidden;
208
+ if (effectiveForbidden.length > 0 &&
209
+ nonHardForbiddenCandidates.every((candidate) => matchesAnyPattern(candidate, effectiveForbidden))) {
154
210
  throw new PiWriterPathDeniedError("forbidden", relative || ".");
155
211
  }
156
212
  }
@@ -1,57 +1,43 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createRequire } from "node:module";
4
4
  const PACKAGE_NAME = "@playwright/cli";
5
+ const PACKAGE_VERSION = "0.1.20";
5
6
  function launcherFromManifest(manifestPath) {
6
- try {
7
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
8
- if (manifest.name !== PACKAGE_NAME)
9
- return undefined;
10
- const bin = typeof manifest.bin === "string"
11
- ? manifest.bin
12
- : manifest.bin && typeof manifest.bin === "object"
13
- ? manifest.bin["playwright-cli"]
14
- : undefined;
15
- if (typeof bin !== "string" || !bin || path.isAbsolute(bin) || bin.split(/[\\/]+/).includes(".."))
16
- return undefined;
17
- const packageRoot = path.dirname(manifestPath);
18
- const entryPath = path.resolve(packageRoot, bin);
19
- const relative = path.relative(packageRoot, entryPath);
20
- if (relative.startsWith("..") || path.isAbsolute(relative) || !existsSync(entryPath))
21
- return undefined;
22
- return { executable: process.execPath, argvPrefix: [entryPath], entryPath };
23
- }
24
- catch {
7
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
8
+ if (manifest.name !== PACKAGE_NAME)
25
9
  return undefined;
10
+ if (manifest.version !== PACKAGE_VERSION) {
11
+ throw new Error(`expected ${PACKAGE_NAME}@${PACKAGE_VERSION}, found ${String(manifest.version)}`);
26
12
  }
13
+ const bin = typeof manifest.bin === "string"
14
+ ? manifest.bin
15
+ : manifest.bin && typeof manifest.bin === "object"
16
+ ? manifest.bin["playwright-cli"]
17
+ : undefined;
18
+ if (typeof bin !== "string" || !bin || path.isAbsolute(bin) || path.win32.isAbsolute(bin) || bin.split(/[\\/]+/).includes(".."))
19
+ return undefined;
20
+ if (!/\.(?:c|m)?js$/i.test(bin))
21
+ return undefined;
22
+ const packageRoot = realpathSync(path.dirname(manifestPath));
23
+ const entryPath = realpathSync(path.resolve(packageRoot, bin));
24
+ const relative = path.relative(packageRoot, entryPath);
25
+ if (relative.startsWith("..") || path.isAbsolute(relative) || !statSync(entryPath).isFile())
26
+ return undefined;
27
+ return { executable: process.execPath, argvPrefix: [entryPath], entryPath };
27
28
  }
28
- function candidateManifestPaths(cwd) {
29
- const paths = new Set();
30
- for (const root of [cwd, process.cwd()]) {
31
- try {
32
- paths.add(createRequire(path.join(path.resolve(root), "package.json")).resolve(`${PACKAGE_NAME}/package.json`));
33
- }
34
- catch {
35
- // Keep searching controller-known module roots.
36
- }
37
- }
38
- // Npm's Windows/global shim is paired with node_modules in either the shim
39
- // directory or its parent (.bin). We read package metadata directly; the
40
- // .cmd file itself is never executed or parsed as a command string.
41
- for (const binDir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
42
- paths.add(path.join(binDir, "node_modules", ...PACKAGE_NAME.split("/"), "package.json"));
43
- paths.add(path.join(binDir, "..", ...PACKAGE_NAME.split("/"), "package.json"));
44
- }
45
- return [...paths];
46
- }
47
- /** Resolve only a package-declared entry; throws instead of falling back to a PATH executable. */
48
- export function resolvePlaywrightCliLauncher(cwd) {
49
- for (const manifestPath of candidateManifestPaths(cwd)) {
29
+ /** Resolve from this installation, independently of the target project's cwd/PATH. */
30
+ export function resolvePlaywrightCliLauncher(_cwd) {
31
+ try {
32
+ const manifestPath = createRequire(import.meta.url).resolve(`${PACKAGE_NAME}/package.json`);
50
33
  const launcher = launcherFromManifest(manifestPath);
51
- if (launcher)
52
- return launcher;
34
+ if (!launcher)
35
+ throw new Error("invalid package manifest or JavaScript entry");
36
+ return launcher;
37
+ }
38
+ catch (error) {
39
+ throw new Error(`playwright-cli-unavailable: bundled ${PACKAGE_NAME} installation is missing or invalid; reinstall loop-agent. ${error instanceof Error ? error.message : String(error)}`);
53
40
  }
54
- throw new Error("playwright-cli-unavailable: verified @playwright/cli JavaScript entry was not found");
55
41
  }
56
42
  export function checkPlaywrightCliLauncher(cwd) {
57
43
  try {
@@ -1,11 +1,11 @@
1
- import { spawn } from "node:child_process";
2
1
  import { createHash } from "node:crypto";
3
- import { constants, createReadStream } from "node:fs";
4
- import { access, lstat, readlink, unlink } from "node:fs/promises";
2
+ import { createReadStream } from "node:fs";
3
+ import { lstat, readlink, unlink } from "node:fs/promises";
5
4
  import path from "node:path";
6
5
  import { pathMatchesPattern } from "../shared/git-progress.js";
7
6
  import { expandWorkspaceStatus, hasGitmodulesFile, synthesizeExpandedPorcelain, } from "../shared/git-workspace-status.js";
8
7
  import { ResilientGitCommandError, runResilientGitCommand, } from "../shared/resilient-git.js";
8
+ import { isAnyTestArtifactPath, isHarnessBaselineForbiddenPattern, } from "../workflows/dag/test-artifact-namespace.js";
9
9
  async function sha256File(filePath) {
10
10
  const hash = createHash("sha256");
11
11
  for await (const chunk of createReadStream(filePath)) {
@@ -149,7 +149,27 @@ export function validateShellWriteGuard(input) {
149
149
  continue;
150
150
  }
151
151
  if (matchesAnyPattern(filePath, forbiddenPaths)) {
152
- violations.push(filePath);
152
+ // D3 test-artifact exception: a changed file inside the canonical
153
+ // `.harness/testcase` namespace that the task's exclusive writeSet
154
+ // explicitly owns is exempt from the BASELINE harness guard patterns
155
+ // (`.harness` / `.harness/**`) only — spec-level validation gates such
156
+ // writeSets behind a frozen dedicated-test layout. Any explicit
157
+ // (deeper or out-of-tree) matching forbidden pattern still wins.
158
+ const baselinePatternHit = forbiddenPaths.some((forbidden) => isHarnessBaselineForbiddenPattern(forbidden) &&
159
+ pathMatchesPattern(filePath, forbidden));
160
+ const explicitPatternHit = forbiddenPaths.some((forbidden) => !isHarnessBaselineForbiddenPattern(forbidden) &&
161
+ pathMatchesPattern(filePath, forbidden));
162
+ const ownedByWriteSet = writePolicy === "exclusive" &&
163
+ writeSet.length > 0 &&
164
+ matchesAnyPattern(filePath, writeSet);
165
+ if (explicitPatternHit ||
166
+ !baselinePatternHit ||
167
+ !isAnyTestArtifactPath(filePath) ||
168
+ !ownedByWriteSet) {
169
+ violations.push(filePath);
170
+ continue;
171
+ }
172
+ // Exempted: canonical test-artifact file owned by the writeSet.
153
173
  continue;
154
174
  }
155
175
  if (writePolicy === "read-only" || writePolicy === "none") {
@@ -380,37 +400,6 @@ export function deriveSameInstallationGitCandidates(primary) {
380
400
  }
381
401
  return Array.from(new Set([normalized, ...(fallback ? [fallback] : [])]));
382
402
  }
383
- async function resolveGitExecutableCandidates(platform, env) {
384
- if (platform !== "win32")
385
- return ["git"];
386
- const pathValue = env.PATH ?? env.Path ?? "";
387
- for (const rawEntry of pathValue.split(path.delimiter)) {
388
- const entry = rawEntry.trim().replace(/^"|"$/g, "");
389
- if (!entry)
390
- continue;
391
- const candidate = path.win32.join(entry, "git.exe");
392
- try {
393
- await access(candidate, constants.X_OK);
394
- const sameInstallation = deriveSameInstallationGitCandidates(candidate);
395
- const existing = [];
396
- for (const executable of sameInstallation) {
397
- try {
398
- await access(executable, constants.X_OK);
399
- existing.push(executable);
400
- }
401
- catch {
402
- // Optional same-installation fallback is absent.
403
- }
404
- }
405
- if (existing.length > 0)
406
- return existing;
407
- }
408
- catch {
409
- // Keep searching PATH entries.
410
- }
411
- }
412
- return ["git"];
413
- }
414
403
  export async function readGitStatusPorcelain(cwd, options = {}) {
415
404
  let parentPorcelain;
416
405
  try {
@@ -510,38 +499,6 @@ export async function readGitStatusPorcelainWithDependencies(cwd, options, depen
510
499
  attempts: attemptDiagnostics,
511
500
  });
512
501
  }
513
- function readGitStatusPorcelainOnce(cwd, executable) {
514
- return new Promise((resolve, reject) => {
515
- const child = spawn(executable, ["status", "--porcelain=v1", "--untracked-files=all"], {
516
- cwd,
517
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
518
- stdio: ["ignore", "pipe", "pipe"],
519
- windowsHide: true,
520
- });
521
- let stdout = "";
522
- let stderr = "";
523
- child.stdout.setEncoding("utf-8");
524
- child.stderr.setEncoding("utf-8");
525
- child.stdout.on("data", (chunk) => {
526
- stdout += chunk;
527
- });
528
- child.stderr.on("data", (chunk) => {
529
- stderr += chunk;
530
- });
531
- child.on("error", reject);
532
- child.on("close", (code, signal) => {
533
- if (code === 0) {
534
- resolve(stdout);
535
- return;
536
- }
537
- const detail = stderr.trim() || stdout.trim() || "no stderr/stdout";
538
- const error = new Error(`git status failed: ${detail}`);
539
- error.exitCode = code;
540
- error.signal = signal;
541
- reject(error);
542
- });
543
- });
544
- }
545
502
  export async function runPostRunWriteGuard(input) {
546
503
  const afterStatus = await readGitStatusPorcelain(input.rootCwd);
547
504
  const beforeSnapshot = snapshotGitStatusPorcelain(input.beforeStatus);
@@ -191,12 +191,21 @@ function extractIndexReferences(indexContent) {
191
191
  // `(${base})` and `` `base` ``. Bare mentions are intentionally NOT
192
192
  // treated as references to avoid false positives from partial matches.
193
193
  const linkMatches = [...line.matchAll(/\[[^\]]*\]\(([^)]+\.md)\)/g)];
194
+ // Link spans are blanked before the backtick scan: the index convention
195
+ // `[`archived.md`](../completed/archived.md)` puts a bare basename in the
196
+ // link TEXT, which is not an entry for this directory. Reading it as one
197
+ // reported every archived link as a stale active entry. Blanking (rather
198
+ // than filtering by content) keeps `[`local.md`](local.md)`, standalone
199
+ // `local.md` mentions, and mixed lines working, because those still
200
+ // contribute through their link target or an untouched backtick span.
201
+ let remainder = line;
194
202
  for (const match of linkMatches) {
195
203
  const reference = match[1] ? localIndexReference(match[1]) : null;
196
204
  if (reference)
197
205
  references.add(reference);
206
+ remainder = remainder.replace(match[0], " ".repeat(match[0].length));
198
207
  }
199
- const backtickMatches = [...line.matchAll(/`([^`]+\.md)`/g)];
208
+ const backtickMatches = [...remainder.matchAll(/`([^`]+\.md)`/g)];
200
209
  for (const match of backtickMatches) {
201
210
  const reference = match[1] ? localIndexReference(match[1]) : null;
202
211
  if (reference)
@@ -126,7 +126,8 @@ export const frontendOpenspecConfigSchema = z.object({
126
126
  export const frontendTestConfigSchema = z.object({
127
127
  /**
128
128
  * Repository-relative frontend-test artifact root.
129
- * Default "testcase/frontend". Semantic validation lives in
129
+ * Default ".harness/testcase/frontend" (canonical harness test-artifact
130
+ * namespace). Semantic validation lives in
130
131
  * resolveFrontendTestLayout (fail-closed), not in this schema.
131
132
  */
132
133
  testRoot: z.string().min(1).optional(),
@@ -239,7 +240,8 @@ const backendTestModuleLayoutSchema = z
239
240
  /** Backend-test artifact layout and pipeline mode configuration (plan A/B/D). */
240
241
  export const backendTestConfigSchema = z
241
242
  .object({
242
- /** Repository-relative test root. Default "testcase". */
243
+ /** Repository-relative test root. Default ".harness/testcase" (canonical
244
+ * harness test-artifact namespace). */
243
245
  testRoot: z.string().min(1).optional(),
244
246
  /** Markdown subdirectory relative to testRoot. Default "md". */
245
247
  markdownRel: z.string().min(1).optional(),
@@ -233,6 +233,7 @@ export function buildPrepareDraft(input) {
233
233
  allowedPaths,
234
234
  forbiddenPaths: finalForbidden,
235
235
  noDefaultForbiddenPaths: flags.noDefaultForbiddenPaths,
236
+ taskKind,
236
237
  });
237
238
  const draft = {
238
239
  schemaVersion: TASK_CONTRACT_DRAFT_SCHEMA_VERSION,
@@ -1,5 +1,6 @@
1
1
  import { canonicalizePath } from "../contract/canonicalize.js";
2
2
  import { PREPARE_DEFAULT_FORBIDDEN_PATHS } from "./types.js";
3
+ import { isAnyTestArtifactPath, isTestArtifactTaskKind, } from "../../workflows/dag/test-artifact-namespace.js";
3
4
  function hasNul(value) {
4
5
  return value.includes("\0");
5
6
  }
@@ -133,6 +134,7 @@ export function assessPreparePaths(input) {
133
134
  forbidden.push(result.value);
134
135
  }
135
136
  const allowedSet = new Set(allowed);
137
+ const testArtifactTaskKind = isTestArtifactTaskKind(input.taskKind);
136
138
  for (const f of forbidden) {
137
139
  if (allowedSet.has(f)) {
138
140
  blocking.push({
@@ -145,14 +147,22 @@ export function assessPreparePaths(input) {
145
147
  }
146
148
  for (const a of allowed) {
147
149
  for (const f of forbidden) {
148
- if (forbiddenAncestorCoversAllowed(a, f)) {
149
- blocking.push({
150
- code: "ALLOWED_UNDER_FORBIDDEN_ANCESTOR",
151
- level: "blocking",
152
- message: `allowed path ${a} is fully covered by forbidden ancestor ${f}`,
153
- path: a,
154
- });
150
+ if (!forbiddenAncestorCoversAllowed(a, f))
151
+ continue;
152
+ // D3 test-artifact exception: a dedicated test task may allow exactly
153
+ // the canonical `.harness/testcase` roots under the default `.harness/**`
154
+ // forbidden ancestor; every other overlap stays blocking.
155
+ if (testArtifactTaskKind &&
156
+ f === ".harness/**" &&
157
+ isAnyTestArtifactPath(a)) {
158
+ continue;
155
159
  }
160
+ blocking.push({
161
+ code: "ALLOWED_UNDER_FORBIDDEN_ANCESTOR",
162
+ level: "blocking",
163
+ message: `allowed path ${a} is fully covered by forbidden ancestor ${f}`,
164
+ path: a,
165
+ });
156
166
  }
157
167
  }
158
168
  for (const a of allowed) {