@tea-agent/loop-agent 0.39.0-next.9 → 0.39.0

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 (252) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +262 -69
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/dag/generate-task-dag.js +9 -0
  6. package/dist/application/task-lifecycle/advance.js +4 -0
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +16 -5
  9. package/dist/cli/program.js +16 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli.js +2 -1
  12. package/dist/commands/dag-rerun.js +55 -1
  13. package/dist/commands/init-model-catalog.js +464 -0
  14. package/dist/commands/init-upgrade.js +265 -97
  15. package/dist/commands/init.js +428 -27
  16. package/dist/commands/inspect-next.js +8 -0
  17. package/dist/commands/task-advance.js +31 -0
  18. package/dist/executors/dag-pi-executor.js +124 -18
  19. package/dist/executors/pi-event-serializer.js +42 -1
  20. package/dist/executors/pi-executor.js +51 -0
  21. package/dist/executors/pi-extension-resolver.js +233 -0
  22. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  23. package/dist/executors/pi-sdk-executor.js +187 -59
  24. package/dist/executors/shell-executor.js +554 -33
  25. package/dist/executors/shell-write-guard.js +7 -0
  26. package/dist/shared/dag-failure-category.js +138 -0
  27. package/dist/shared/dag-prompt-override.js +28 -0
  28. package/dist/shared/operator/capabilities.js +181 -27
  29. package/dist/shared/playwright-cli-command-policy.js +15 -0
  30. package/dist/shared/update/console-notifier.js +100 -0
  31. package/dist/{cli → shared}/update/npm-client.js +41 -8
  32. package/dist/{cli → shared}/update/state.js +41 -13
  33. package/dist/task/config-types.js +28 -0
  34. package/dist/task/contract/constants.js +1 -0
  35. package/dist/task/contract/diff.js +26 -0
  36. package/dist/task/contract/project.js +5 -0
  37. package/dist/task/contract/schema.js +6 -1
  38. package/dist/task/source-prepare/build-draft.js +15 -0
  39. package/dist/task/source-prepare/parse-intent.js +24 -4
  40. package/dist/worker/cli.js +31 -1
  41. package/dist/worker/console/chat/deferred-turn.js +91 -0
  42. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  43. package/dist/worker/console/chat/operation-card.js +8 -1
  44. package/dist/worker/console/chat/pi-console-config.js +81 -10
  45. package/dist/worker/console/chat/pi-runtime.js +165 -83
  46. package/dist/worker/console/chat/repo-walk.js +10 -1
  47. package/dist/worker/console/chat/routes.js +480 -55
  48. package/dist/worker/console/chat/semantic-activity.js +20 -8
  49. package/dist/worker/console/chat/session-store.js +146 -2
  50. package/dist/worker/console/chat/turn-process.js +10 -10
  51. package/dist/worker/console/dag-execution-receipt.js +33 -0
  52. package/dist/worker/console/inspect-split.js +18 -0
  53. package/dist/worker/console/operation-run-facts.js +19 -3
  54. package/dist/worker/console/operator-actions.js +195 -4
  55. package/dist/worker/console/operator-user-error.js +2 -2
  56. package/dist/worker/console/pi-readiness.js +5 -4
  57. package/dist/worker/console/recovery-cta.js +4 -4
  58. package/dist/worker/console/recovery-error-copy.js +4 -4
  59. package/dist/worker/console/routes.js +43 -2
  60. package/dist/worker/console/security.js +63 -9
  61. package/dist/worker/console/server.js +42 -6
  62. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-Br92EGb7.js} +1 -1
  63. package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-oulImtQq.js} +1 -1
  64. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-tyHc6aqO.js} +1 -1
  65. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-CY86BSpT.js} +1 -1
  66. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-BKKsDmNs.js} +1 -1
  67. package/dist/worker/console/static/assets/channel-TY36zt-i.js +1 -0
  68. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-CXZdjl3B.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-B-aIFDQ5.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-BDgAW_B2.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-DABa4UPR.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-aGbysVOx.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-B8MSZDVs.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-CpelV73g.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-BE04ty3z.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-BUxTkaaR.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-CYYBVKfX.js} +1 -1
  78. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +1 -0
  79. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +1 -0
  80. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-CEQF_xmQ.js} +1 -1
  81. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-CqpxN5IT.js} +1 -1
  82. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-B3PzanVi.js} +1 -1
  83. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-BSUGCNx1.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-CdLevU5T.js} +1 -1
  85. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BjpF_Kok.js} +1 -1
  86. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C4I6NJny.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-C8hecCr5.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-Daod6tOS.js} +1 -1
  89. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-BvvmazGv.js} +1 -1
  90. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-DeN_vfV4.js} +1 -1
  91. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-D6dq1t9z.js} +1 -1
  92. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-DziIdNY4.js} +1 -1
  93. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js} +1 -1
  94. package/dist/worker/console/static/assets/index-C3GgHg-g.css +1 -0
  95. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +406 -0
  96. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-Czv5eYx2.js} +1 -1
  97. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js} +1 -1
  98. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-DxGmNj5K.js} +1 -1
  99. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-CYCFqkO_.js} +1 -1
  100. package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-B0YzRwrd.js} +1 -1
  101. package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-t3EZtIGh.js} +5 -5
  102. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-BTXpTv0T.js} +1 -1
  103. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-DU8UkKqA.js} +1 -1
  104. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-DP4y8ohp.js} +1 -1
  105. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-B07vNaYR.js} +1 -1
  106. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-xDWktb2i.js} +1 -1
  107. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-BLNMKqeS.js} +1 -1
  108. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-n0adalxH.js} +1 -1
  109. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-BbnS-2DL.js} +1 -1
  110. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-B4uPpCLu.js} +1 -1
  111. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA--MMbB0fX.js} +1 -1
  112. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +1 -0
  113. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-BXUgp2QY.js} +2 -2
  114. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +8 -0
  115. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-CsaMxki7.js} +1 -1
  116. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-C2IuG9iH.js} +1 -1
  117. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-CmEWOXCW.js} +1 -1
  118. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-D_9LE0MG.js} +1 -1
  119. package/dist/worker/console/static/index.html +2 -2
  120. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  121. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  122. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  123. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  124. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  125. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +74 -26
  126. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  127. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  128. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  129. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  130. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  131. package/dist/worker/console/static-src/operator-chat/turn-submission.js +54 -0
  132. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  133. package/dist/worker/console/static-src/operator-chat/useChatStream.js +237 -11
  134. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  135. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  136. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  137. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  138. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  139. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  140. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  141. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  142. package/dist/worker/observability/read-model.js +22 -0
  143. package/dist/worker/observe/node-input.js +74 -3
  144. package/dist/worker/observe/node-process.js +377 -0
  145. package/dist/worker/observe/routes.js +36 -3
  146. package/dist/worker/observe/shell-handler-keys.js +34 -0
  147. package/dist/worker/observe/static/api.js +66 -0
  148. package/dist/worker/observe/static/constants.js +8 -1
  149. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  150. package/dist/worker/observe/static/format.js +23 -0
  151. package/dist/worker/observe/static/markdown-render.js +20 -1
  152. package/dist/worker/observe/static/operator-chrome.js +4 -0
  153. package/dist/worker/observe/static/state.js +13 -2
  154. package/dist/worker/observe/static/styles.css +73 -0
  155. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  156. package/dist/worker/observe/static/views/dag.js +8 -1
  157. package/dist/worker/observe/static/views/failures.js +5 -2
  158. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  159. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
  160. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  161. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  162. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  163. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  164. package/dist/workflows/dag/backend-test-layout.js +133 -0
  165. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  166. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  167. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  168. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  169. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  170. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  171. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  172. package/dist/workflows/dag/contract-output-registry.js +15 -0
  173. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  174. package/dist/workflows/dag/convergence/controller.js +141 -0
  175. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  176. package/dist/workflows/dag/dynamic-runtime/map.js +25 -9
  177. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  178. package/dist/workflows/dag/failure-category.js +7 -116
  179. package/dist/workflows/dag/frontend-implementation-contract.js +377 -4
  180. package/dist/workflows/dag/frontend-prewrite-gate.js +96 -84
  181. package/dist/workflows/dag/frontend-repair.js +14 -0
  182. package/dist/workflows/dag/frontend-risk.js +15 -2
  183. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  184. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  185. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  186. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  187. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  188. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  189. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  190. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  191. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  192. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  193. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  194. package/dist/workflows/dag/init-hybrid.js +894 -205
  195. package/dist/workflows/dag/node-execution.js +183 -15
  196. package/dist/workflows/dag/prompt.js +60 -1
  197. package/dist/workflows/dag/rerun-feedback.js +212 -0
  198. package/dist/workflows/dag/rerun-plan.js +224 -27
  199. package/dist/workflows/dag/rerun-run.js +61 -6
  200. package/dist/workflows/dag/rerun-task.js +61 -2
  201. package/dist/workflows/dag/retry-policy.js +109 -0
  202. package/dist/workflows/dag/structured-output-repair.js +712 -0
  203. package/dist/workflows/dag/types.js +143 -1
  204. package/dist/workflows/dag/validate.js +64 -20
  205. package/docs/init-surface.manifest.json +5 -0
  206. package/docs/operations/README.md +1 -1
  207. package/docs/operations/local-development-environment.md +1 -5
  208. package/docs/skills/vetted-skill-registry.md +14 -0
  209. package/docs/templates/README.md +1 -1
  210. package/docs/templates/agent-dag.schema.json +44 -0
  211. package/docs/templates/backend-test-dag.json +60 -35
  212. package/docs/templates/frontend-test-case-checklist.md +2 -2
  213. package/docs/templates/frontend-test-dag.json +8 -10
  214. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  215. package/docs/templates/init-managed-agents.md +13 -3
  216. package/harness.json +6 -4
  217. package/package.json +12 -4
  218. package/scripts/next-info.mjs +356 -0
  219. package/scripts/next-publish-gate.mjs +238 -0
  220. package/scripts/release-source-binding.mjs +251 -0
  221. package/skills/analyze-product-requirements/SKILL.md +8 -5
  222. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  223. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  224. package/skills/analyze-product-requirements/references/example.md +50 -0
  225. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  226. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  227. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  228. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  229. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  230. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  231. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  232. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  233. package/skills/codebase-scout/SKILL.md +1 -1
  234. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  235. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  236. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  237. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  238. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  239. package/skills/improve-codebase-architecture/deepening.md +37 -0
  240. package/skills/improve-codebase-architecture/html-report.md +123 -0
  241. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  242. package/skills/improve-codebase-architecture/language.md +53 -0
  243. package/skills/loop-agent/references/command-reference.md +11 -4
  244. package/skills/playwright-cli/SKILL.md +23 -1
  245. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  246. package/dist/worker/console/static/assets/channel-BBOTJHvD.js +0 -1
  247. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cm8Yf8tD.js +0 -1
  248. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cm8Yf8tD.js +0 -1
  249. package/dist/worker/console/static/assets/index-B-qWGThd.js +0 -325
  250. package/dist/worker/console/static/assets/index-C-xRgY_0.css +0 -1
  251. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DZihfdKc.js +0 -1
  252. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CKnXCjHX.js +0 -8
@@ -15,6 +15,8 @@ import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend
15
15
  import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
16
16
  import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
17
17
  import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
18
+ import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
19
+ import { probeFrontendTestEnvironment } from "../workflows/dag/frontend-test-environment-probe.js";
18
20
  import { renderFrontendTestL5Report } from "../workflows/dag/frontend-test-l5-report.js";
19
21
  import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-case-checklist.js";
20
22
  import { materializeFrontendTestCaseManifest } from "../workflows/dag/frontend-test-case-manifest.js";
@@ -27,11 +29,14 @@ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from
27
29
  import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
28
30
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
29
31
  import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
32
+ import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
33
+ import { frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
34
+ import { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
30
35
  import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
31
36
  import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
32
- import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
37
+ import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, markdownFiles, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
33
38
  import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
34
- import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
39
+ import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
35
40
  import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
36
41
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
37
42
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
@@ -48,6 +53,7 @@ async function runBackendScenarioParamPreflight(input) {
48
53
  phase: "initial",
49
54
  repairAttempt: 0,
50
55
  strictScenarioParamGate: input.strictScenarioParamGate,
56
+ layout: input.layout,
51
57
  });
52
58
  const initialArtifacts = await writeBackendScenarioParamArtifacts({
53
59
  runDir: input.runDir,
@@ -73,6 +79,7 @@ async function runBackendScenarioParamPreflight(input) {
73
79
  phase: "final",
74
80
  repairAttempt: 1,
75
81
  strictScenarioParamGate: input.strictScenarioParamGate,
82
+ layout: input.layout,
76
83
  });
77
84
  finalFacts = reassessed.facts;
78
85
  finalMarkdown = reassessed.markdown;
@@ -117,6 +124,39 @@ function shouldReuseSuccessfulShellCommands(shell) {
117
124
  const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
118
125
  const SUMMARY_STDOUT_MAX = 4_000;
119
126
  const SUMMARY_STDERR_MAX = 2_000;
127
+ const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
128
+ const TEST_TIMEOUT_MARKERS = [
129
+ /Test timed out in \d+ms/i,
130
+ /Timeout of \d+ms exceeded/i,
131
+ ];
132
+ const DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS = [
133
+ /AssertionError/i,
134
+ /expected:?\s/i,
135
+ /received:?\s/i,
136
+ /Snapshot /i,
137
+ /toMatchSnapshot/i,
138
+ /SyntaxError/i,
139
+ /TS\d{4}/,
140
+ /error TS/i,
141
+ /eslint/i,
142
+ /compile error/i,
143
+ /Failed to compile/i,
144
+ ];
145
+ function isIdentifiedTestCommand(command) {
146
+ return /(?:^|[\s"'`])(?:npx\s+)?vitest(?:\s|$)|(?:^|[\s"'`])npm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])pnpm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])yarn(?:\.cmd)?\s+test(?:\s|$)/i.test(command);
147
+ }
148
+ function classifyShellCommandFailure(input) {
149
+ if (input.processCategory !== "nonzero-exit")
150
+ return input.processCategory;
151
+ if (!isIdentifiedTestCommand(input.command))
152
+ return "nonzero-exit";
153
+ const sample = `${input.stdout}\n${input.stderr}`.slice(0, TEST_TIMEOUT_CLASSIFY_MAX_CHARS);
154
+ const hasTimeoutMarker = TEST_TIMEOUT_MARKERS.some((marker) => marker.test(sample));
155
+ if (!hasTimeoutMarker)
156
+ return "nonzero-exit";
157
+ const mixedDeterministicFailure = DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS.some((marker) => marker.test(sample));
158
+ return mixedDeterministicFailure ? "nonzero-exit" : "test-timeout";
159
+ }
120
160
  const NODE_STDOUT_MAX = 12_000;
121
161
  const NODE_STDERR_MAX = 8_000;
122
162
  const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
@@ -323,16 +363,21 @@ export async function executeShellCommand(input) {
323
363
  });
324
364
  });
325
365
  child.on("close", (code) => {
326
- let failureCategory = "nonzero-exit";
366
+ let processCategory = "nonzero-exit";
327
367
  if (timedOut) {
328
- failureCategory = "timeout";
368
+ processCategory = "timeout";
329
369
  }
330
370
  else if (code === 0) {
331
- failureCategory = "success";
371
+ processCategory = "success";
332
372
  }
333
373
  finish({
334
374
  exitCode: code,
335
- failureCategory,
375
+ failureCategory: classifyShellCommandFailure({
376
+ command: input.command,
377
+ processCategory,
378
+ stdout: formatBoundedOutput(stdout),
379
+ stderr: formatBoundedOutput(stderr),
380
+ }),
336
381
  ok: code === 0 && !timedOut,
337
382
  });
338
383
  });
@@ -492,11 +537,114 @@ async function readAdvisoryRunReport(reportsDir, filename, title) {
492
537
  async function executeBackendTestPipeline(input, meta) {
493
538
  const pipeline = input.task.shell?.backendTestPipeline;
494
539
  const started = Date.now();
540
+ // Plan A: prefer the frozen layout carried by the compiled spec; fall back
541
+ // to the historical default for hand-written legacy DAGs.
542
+ const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
543
+ ? {
544
+ testRoot: meta.spec.backendTestLayout.testRoot,
545
+ markdownRel: meta.spec.backendTestLayout.markdownRel,
546
+ scriptRel: meta.spec.backendTestLayout.scriptRel,
547
+ }
548
+ : undefined);
495
549
  try {
496
550
  if (!pipeline)
497
551
  throw new Error("missing backend-test pipeline id");
498
552
  const outputs = [];
499
- if (pipeline === "markdown-environment") {
553
+ if (pipeline === "ingest-backend-test-gap") {
554
+ // Plan D: deterministic gap ingest. Identity (taskId + requirement
555
+ // sha256) is fail-closed; a mismatched plan blocks every writer.
556
+ const gapDocRel = meta.spec.globalConstraints
557
+ ?.map((item) => /^backendTestGapDoc=(.+)$/i.exec(item.trim())?.[1])
558
+ .find(Boolean);
559
+ const binding = meta.spec.sourceBinding;
560
+ const requirementSha256 = binding?.sources.find((s) => s.kind === "requirement")?.sha256 ?? "";
561
+ if (!gapDocRel || !binding || !requirementSha256) {
562
+ return {
563
+ ok: false,
564
+ stdout: outputs.join("\n"),
565
+ stderr: "backend-test gap ingest requires the bound gap document path and the task source binding with a requirement sha256",
566
+ failureCategory: "invalid-output",
567
+ durationMs: Date.now() - started,
568
+ };
569
+ }
570
+ const gapDocAbs = path.join(input.cwd, ...gapDocRel.split("/"));
571
+ let parsed;
572
+ try {
573
+ parsed = JSON.parse(await readFile(gapDocAbs, "utf8"));
574
+ }
575
+ catch (error) {
576
+ return {
577
+ ok: false,
578
+ stdout: outputs.join("\n"),
579
+ stderr: `backend-test gap document is unreadable JSON at ${gapDocRel}: ${error instanceof Error ? error.message : String(error)}`,
580
+ failureCategory: "invalid-output",
581
+ durationMs: Date.now() - started,
582
+ };
583
+ }
584
+ const doc = backendTestGapDocSchema.safeParse(parsed);
585
+ if (!doc.success) {
586
+ return {
587
+ ok: false,
588
+ stdout: outputs.join("\n"),
589
+ stderr: `invalid backend-test-gap-v1 document: ${doc.error.issues
590
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
591
+ .join("; ")}`,
592
+ failureCategory: "invalid-output",
593
+ durationMs: Date.now() - started,
594
+ };
595
+ }
596
+ // previous N6 facts (may be absent on a fresh checkout)
597
+ const factsPath = path.join(input.cwd, "contracts", "backend-test-case-coverage-facts.json");
598
+ let previousMissingSlots;
599
+ let previousCoveredSlots;
600
+ try {
601
+ const facts = JSON.parse(await readFile(factsPath, "utf8"));
602
+ previousMissingSlots = facts.scenarioPartitions?.missingSlots;
603
+ previousCoveredSlots = (facts.rules ?? []).flatMap((rule) => rule.coveredTestPoints ?? []);
604
+ }
605
+ catch {
606
+ // no previous facts in the workspace: the gap doc alone drives the plan
607
+ }
608
+ // module index comes from the README on disk
609
+ let existingModuleStems = [];
610
+ try {
611
+ const files = await markdownFiles(input.cwd, backendLayout);
612
+ existingModuleStems = files
613
+ .map((file) => path.basename(file, ".md"))
614
+ .filter((stem) => stem.toLowerCase() !== "readme");
615
+ }
616
+ catch {
617
+ // no markdown tree: every slot becomes a conflict
618
+ }
619
+ const plan = ingestBackendTestGap({
620
+ gapDoc: doc.data,
621
+ previousMissingSlots,
622
+ previousCoveredSlots,
623
+ existingModuleStems,
624
+ sharedSetupPath: meta.spec.backendTestSharedSetup?.path ?? null,
625
+ layout: backendLayout,
626
+ taskId: binding.taskId,
627
+ requirementSha256,
628
+ });
629
+ const contractsDir = path.join(meta.runDir, "contracts");
630
+ await mkdir(contractsDir, { recursive: true });
631
+ const planPath = path.join(contractsDir, "backend-test-gap-plan-v1.json");
632
+ await writeFile(planPath, JSON.stringify(plan, null, 2), "utf8");
633
+ outputs.push(`gapPlan=${planPath}`, `gapPlanStatus=${plan.status}`);
634
+ if (plan.status === "error") {
635
+ return {
636
+ ok: false,
637
+ stdout: outputs.join("\n"),
638
+ stderr: [
639
+ "backend-test gap ingest failed closed (identity/conflicts):",
640
+ ...plan.conflicts,
641
+ ].join("\n"),
642
+ failureCategory: "invalid-output",
643
+ durationMs: Date.now() - started,
644
+ };
645
+ }
646
+ }
647
+ else if (pipeline === "markdown-environment") {
500
648
  const results = await executePipelineCommands(input, meta);
501
649
  if (!results.every((result) => result.ok)) {
502
650
  const failure = results.find((result) => !result.ok);
@@ -530,6 +678,8 @@ async function executeBackendTestPipeline(input, meta) {
530
678
  environmentMarkdown,
531
679
  requiredRequirementIds,
532
680
  sourceBinding: meta.spec.sourceBinding,
681
+ layout: backendLayout,
682
+ sharedSetup: meta.spec.backendTestSharedSetup ?? null,
533
683
  });
534
684
  }
535
685
  catch (error) {
@@ -583,6 +733,7 @@ async function executeBackendTestPipeline(input, meta) {
583
733
  const coverage = await analyzeBackendTestCaseCoverage({
584
734
  workspaceRoot: input.cwd,
585
735
  sourceBinding: normalizedBinding,
736
+ layout: backendLayout,
586
737
  });
587
738
  const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage.markdown);
588
739
  const contractsDir = path.join(meta.runDir, "contracts");
@@ -590,6 +741,23 @@ async function executeBackendTestPipeline(input, meta) {
590
741
  const factsPath = path.join(contractsDir, "backend-test-case-coverage-facts.json");
591
742
  await writeFile(factsPath, JSON.stringify(coverage.facts, null, 2), "utf8");
592
743
  outputs.push(`caseCoverage=${coveragePath}`, `caseCoverageFacts=${factsPath}`, coverage.markdown);
744
+ // Plan C: scenario-partition slot coverage is fail-closed. General
745
+ // coverage findings stay advisory, but a declared filter axis missing
746
+ // an each-value/omitted slot or its complement slot must block pytest
747
+ // generation until the Markdown declares the slot.
748
+ const partitionBlocking = coverage.facts.findings.filter((finding) => /^(?:MISSING_PARTITION|MISSING_PARTITION_SLOT|MISSING_COMPLEMENT_SLOT|MISSING_PARTITION_TABLE|INVALID_PARTITION_DOMAIN|NO_FINITE_DOMAIN|PARTITION_SOURCE_MISMATCH)/.test(finding));
749
+ if (partitionBlocking.length > 0) {
750
+ return {
751
+ ok: false,
752
+ stdout: outputs.join("\n\n"),
753
+ stderr: [
754
+ "backend-test scenario partitions are incomplete (fail-closed):",
755
+ ...partitionBlocking,
756
+ ].join("\n"),
757
+ failureCategory: "invalid-output",
758
+ durationMs: Date.now() - started,
759
+ };
760
+ }
593
761
  }
594
762
  catch (error) {
595
763
  const message = error instanceof Error ? error.message : String(error);
@@ -606,9 +774,9 @@ async function executeBackendTestPipeline(input, meta) {
606
774
  }
607
775
  }
608
776
  else if (pipeline === "markdown-collection-assess") {
609
- const resolution = await resolveBackendTestMappedPytestScripts(input.cwd);
777
+ const resolution = await resolveBackendTestMappedPytestScripts(input.cwd, backendLayout);
610
778
  let facts;
611
- const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
779
+ const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
612
780
  const priorityOnlyFacts = assessPriorityOnlyBackendPytestModules({
613
781
  phase: "initial",
614
782
  mappedScripts: resolution.expectedScripts,
@@ -618,7 +786,7 @@ async function executeBackendTestPipeline(input, meta) {
618
786
  facts = priorityOnlyFacts;
619
787
  }
620
788
  else if (resolution.missingScripts.length > 0) {
621
- const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
789
+ const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
622
790
  facts = assessMissingBackendPytestScripts({
623
791
  mappedScripts: resolution.expectedScripts,
624
792
  existingMappedScripts: resolution.existingScripts,
@@ -632,10 +800,11 @@ async function executeBackendTestPipeline(input, meta) {
632
800
  const scenario = await runBackendScenarioParamPreflight({
633
801
  workspaceRoot: input.cwd,
634
802
  runDir: meta.runDir,
803
+ layout: backendLayout,
635
804
  strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
636
805
  });
637
806
  outputs.push(...scenario.outputs);
638
- const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
807
+ const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
639
808
  const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
640
809
  const targets = mappedScripts.map(shellQuote).join(" ");
641
810
  const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
@@ -669,6 +838,7 @@ async function executeBackendTestPipeline(input, meta) {
669
838
  fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
670
839
  }
671
840
  facts = assessBackendPytestCollection({
841
+ layout: backendLayout,
672
842
  phase: "initial",
673
843
  mappedScripts,
674
844
  inventory,
@@ -678,6 +848,43 @@ async function executeBackendTestPipeline(input, meta) {
678
848
  fixtureResolution,
679
849
  });
680
850
  }
851
+ try {
852
+ const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
853
+ workspaceRoot: input.cwd,
854
+ layout: backendLayout,
855
+ taskId: meta.spec.sourceBinding?.taskId ?? "unbound",
856
+ });
857
+ const correspondenceReportPath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence-initial.md", correspondence.markdown);
858
+ const contractsDir = path.join(meta.runDir, "contracts");
859
+ await mkdir(contractsDir, { recursive: true });
860
+ const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
861
+ await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
862
+ outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
863
+ if (correspondence.facts.status === "FAIL" && facts.status === "PASS") {
864
+ const correspondenceRepairPaths = Array.from(new Set(correspondence.facts.entries
865
+ .filter((entry) => entry.status !== "EXACT_1_TO_1")
866
+ .flatMap((entry) => [entry.expectedScript, ...entry.actualScripts])
867
+ .filter((candidate) => candidate.startsWith("testcase/") && candidate.endsWith(".py")))).sort();
868
+ if (correspondenceRepairPaths.length > 0) {
869
+ facts.status = "REPAIRABLE";
870
+ facts.repairEligible = true;
871
+ facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
872
+ facts.findings.push({
873
+ kind: "markdown-pytest-correspondence",
874
+ classification: "test-asset-defect",
875
+ repairability: "repairable",
876
+ detail: correspondence.facts.entries
877
+ .filter((entry) => entry.status !== "EXACT_1_TO_1")
878
+ .flatMap((entry) => entry.findings)
879
+ .join("; ")
880
+ .slice(0, 12_000) || "Markdown-to-pytest correspondence is incomplete",
881
+ });
882
+ }
883
+ }
884
+ }
885
+ catch (error) {
886
+ outputs.push(`initialCorrespondence=UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
887
+ }
681
888
  const artifacts = await writeBackendPytestCollectionArtifacts({
682
889
  runDir: meta.runDir,
683
890
  stem: "initial",
@@ -711,14 +918,15 @@ async function executeBackendTestPipeline(input, meta) {
711
918
  });
712
919
  }
713
920
  else if (initial.status === "REPAIRABLE") {
714
- const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
921
+ const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
715
922
  const scenario = await runBackendScenarioParamPreflight({
716
923
  workspaceRoot: input.cwd,
717
924
  runDir: meta.runDir,
925
+ layout: backendLayout,
718
926
  strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
719
927
  });
720
928
  outputs.push(...scenario.outputs);
721
- const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
929
+ const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
722
930
  const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
723
931
  const targets = mappedScripts.map(shellQuote).join(" ");
724
932
  const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
@@ -753,6 +961,7 @@ async function executeBackendTestPipeline(input, meta) {
753
961
  fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
754
962
  }
755
963
  const finalFacts = assessBackendPytestCollection({
964
+ layout: backendLayout,
756
965
  phase: "final",
757
966
  mappedScripts,
758
967
  inventory,
@@ -776,12 +985,57 @@ async function executeBackendTestPipeline(input, meta) {
776
985
  facts: effective,
777
986
  });
778
987
  const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
988
+ const correspondence = meta.spec.sourceBinding
989
+ ? await analyzeBackendTestMarkdownPytestCorrespondence({
990
+ workspaceRoot: input.cwd,
991
+ layout: backendLayout,
992
+ taskId: meta.spec.sourceBinding.taskId,
993
+ })
994
+ : undefined;
995
+ const eligibility = correspondence
996
+ ? buildBackendTestItemEligibility(effective.collectedItemIds, {
997
+ correspondenceEntries: correspondence.facts.entries,
998
+ scenarioEntries: scenarioFacts.entries,
999
+ })
1000
+ : { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
1001
+ const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
1002
+ const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
1003
+ await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
1004
+ await writeFile(eligibilityFactsPath, `${JSON.stringify({
1005
+ schemaId: "backend-test-execution-eligibility-v1",
1006
+ status: eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
1007
+ collectedItemCount: effective.collectedItemIds.length,
1008
+ eligibleItemCount: eligibility.eligibleItemIds.length,
1009
+ excludedItemCount: eligibility.excludedItems.length,
1010
+ eligibleItemIds: eligibility.eligibleItemIds,
1011
+ excludedItems: eligibility.excludedItems,
1012
+ inputHashes: eligibilityInputHashes,
1013
+ }, null, 2)}\n`, "utf8");
1014
+ const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
1015
+ "# Backend Test Execution Eligibility",
1016
+ "",
1017
+ "## Status",
1018
+ "",
1019
+ eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
1020
+ "",
1021
+ `- Collected items: ${effective.collectedItemIds.length}`,
1022
+ `- Eligible items: ${eligibility.eligibleItemIds.length}`,
1023
+ `- Excluded items: ${eligibility.excludedItems.length}`,
1024
+ "",
1025
+ "## Excluded Items",
1026
+ "",
1027
+ ...(eligibility.excludedItems.length > 0 ? eligibility.excludedItems.map((item) => `- \`${item.itemId}\` (${item.caseId ?? "unknown Case"}): ${item.reasons.join("; ")}`) : ["- None"]),
1028
+ "",
1029
+ ].join("\n"));
1030
+ outputs.push(`executionEligibility=${eligibilityReportPath}`, `executionEligibilityFacts=${eligibilityFactsPath}`);
779
1031
  const readiness = await materializeBackendTestExecutionReadiness({
780
1032
  runDir: meta.runDir,
781
1033
  workspaceRoot: input.cwd,
782
1034
  effective,
783
1035
  scenarioParamStatus: scenarioFacts.overallStatus,
784
1036
  scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
1037
+ eligibility,
1038
+ eligibilityInputHashes,
785
1039
  });
786
1040
  return {
787
1041
  ok: true,
@@ -792,6 +1046,8 @@ async function executeBackendTestPipeline(input, meta) {
792
1046
  collectedItemCount: effective.collectedItemCount,
793
1047
  fixtureResolutionStatus: effective.fixtureResolutionStatus,
794
1048
  executionReadinessStatus: readiness.status,
1049
+ eligibleItemCount: readiness.eligibleItemIds.length,
1050
+ excludedItemCount: readiness.excludedItems.length,
795
1051
  factsPath: "contracts/backend-test-pytest-collection-effective.json",
796
1052
  readinessPath: "contracts/backend-test-execution-readiness.json",
797
1053
  reportPath: artifacts.reportPath,
@@ -826,6 +1082,7 @@ async function executeBackendTestPipeline(input, meta) {
826
1082
  try {
827
1083
  const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
828
1084
  workspaceRoot: input.cwd,
1085
+ layout: backendLayout,
829
1086
  taskId: sourceBinding.taskId,
830
1087
  });
831
1088
  const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence.markdown);
@@ -888,9 +1145,32 @@ async function executeBackendTestPipeline(input, meta) {
888
1145
  else if (pipeline === "markdown-execute-html") {
889
1146
  const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
890
1147
  await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
891
- const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
892
- const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
893
- const pytestTargets = mappedScripts.map(shellQuote).join(" ");
1148
+ const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
1149
+ let eligibleItemIds = executionReadiness.eligibleItemIds;
1150
+ // Plan D gap-fill executeScope=affected: when a gap plan exists in
1151
+ // this run, prefer the newly planned items (pytest node ids contain
1152
+ // the TP slot id) plus every collected item of the patched modules;
1153
+ // scope=all keeps the full eligible set.
1154
+ const affectedScope = meta.spec.globalConstraints?.some((item) => /^backendTestExecuteScope=affected$/i.test(item.trim())) ?? false;
1155
+ if (affectedScope) {
1156
+ try {
1157
+ const plan = JSON.parse(await readFile(path.join(meta.runDir, "contracts", "backend-test-gap-plan-v1.json"), "utf8"));
1158
+ const newPoints = new Set(plan.newTestPoints ?? []);
1159
+ const modules = new Set((plan.targetModules ?? []).map((stem) => `test_${stem}.py`));
1160
+ const affected = eligibleItemIds.filter((id) => modules.size === 0 ||
1161
+ [...modules].some((script) => id.includes(script)) ||
1162
+ [...newPoints].some((point) => id.includes(point)));
1163
+ if (affected.length > 0)
1164
+ eligibleItemIds = affected;
1165
+ }
1166
+ catch {
1167
+ // no gap plan in this run: execute the full eligible set
1168
+ }
1169
+ }
1170
+ const pytestTargetsRelative = "reports/backend-test-pytest-targets.txt";
1171
+ const pytestTargetsAbsolute = path.join(meta.runDir, pytestTargetsRelative);
1172
+ await writeFile(pytestTargetsAbsolute, `${eligibleItemIds.join("\n")}\n`, "utf8");
1173
+ const pytestTargetsArg = `@\"\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}\"`;
894
1174
  // Split into short bash -c commands (aligned with markdown-environment).
895
1175
  // A single ultra-long compound command has been observed on Windows to
896
1176
  // exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
@@ -967,8 +1247,10 @@ async function executeBackendTestPipeline(input, meta) {
967
1247
  'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
968
1248
  'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
969
1249
  'echo "STEP=pytest-run"',
970
- `echo "- pytestCommand: python -m pytest ${mappedScripts.join(" ")} -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
971
- `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargets} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
1250
+ `echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
1251
+ `echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
1252
+ `echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
1253
+ `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargetsArg} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
972
1254
  "STATUS=$?",
973
1255
  'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
974
1256
  'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
@@ -1053,11 +1335,13 @@ async function executeBackendTestPipeline(input, meta) {
1053
1335
  htmlContent: pytestHtmlContent,
1054
1336
  pytestExitCode,
1055
1337
  });
1056
- const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
1338
+ const cases = await collectBackendTestHumanCaseCatalog(input.cwd, backendLayout);
1057
1339
  const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
1058
1340
  const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
1059
1341
  const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
1060
- const correspondenceSummary = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
1342
+ const correspondenceSummaryBase = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
1343
+ const eligibilitySummary = await readAdvisoryRunReport(reportsDir, "backend-test-execution-eligibility.md", "Backend Test Execution Eligibility");
1344
+ const correspondenceSummary = `${correspondenceSummaryBase.trim()}\n\n${eligibilitySummary.trim()}\n`;
1061
1345
  const htmlContent = renderBackendTestHtml({
1062
1346
  title: meta.spec.title,
1063
1347
  parsed,
@@ -1245,6 +1529,7 @@ async function executeBackendTestPipeline(input, meta) {
1245
1529
  fromNodeId: "backend-test-execution-envelope",
1246
1530
  artifactName: "backend-test-execution.json",
1247
1531
  outputDir: "contracts",
1532
+ allowedTestRoot: backendLayout.testRoot,
1248
1533
  });
1249
1534
  outputs.push(`analysis=${analysis.path}`, `execution=${execution.path}`);
1250
1535
  }
@@ -1360,6 +1645,7 @@ async function executeBackendTestPipeline(input, meta) {
1360
1645
  artifactName: "backend-test-result-initial.json",
1361
1646
  outputDir: "contracts",
1362
1647
  junitRelativePath: "reports/backend-test-initial-junit.xml",
1648
+ testRoot: backendLayout.testRoot,
1363
1649
  });
1364
1650
  outputs.push(...results.map((result) => result.stdout), `result=${artifact.path}`);
1365
1651
  }
@@ -1734,10 +2020,12 @@ async function executeFrontendVerificationBundle(input, meta) {
1734
2020
  async function executeFrontendTestCaseChecklist(input, meta) {
1735
2021
  const started = Date.now();
1736
2022
  try {
2023
+ const layout = frontendTestLayoutFromSpec(meta.spec);
1737
2024
  const declaredAcIds = (meta.spec.sourceBinding?.requirementIds ?? []).filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
1738
2025
  const result = await validateFrontendCaseChecklist({
1739
2026
  workspaceRoot: input.cwd,
1740
2027
  declaredAcIds,
2028
+ layout,
1741
2029
  });
1742
2030
  if (result.issues.length) {
1743
2031
  return {
@@ -1772,6 +2060,7 @@ async function executeFrontendTestL5Report(input, meta) {
1772
2060
  const output = await renderFrontendTestL5Report({
1773
2061
  workspaceRoot: input.cwd,
1774
2062
  runDir: meta.runDir,
2063
+ layout: frontendTestLayoutFromSpec(meta.spec),
1775
2064
  });
1776
2065
  return {
1777
2066
  ok: true,
@@ -1797,6 +2086,7 @@ async function executeFrontendTestHtmlReport(input, meta) {
1797
2086
  const output = await renderFrontendTestHtmlReport({
1798
2087
  workspaceRoot: input.cwd,
1799
2088
  runDir: meta.runDir,
2089
+ layout: frontendTestLayoutFromSpec(meta.spec),
1800
2090
  });
1801
2091
  return {
1802
2092
  ok: true,
@@ -1816,11 +2106,60 @@ async function executeFrontendTestHtmlReport(input, meta) {
1816
2106
  };
1817
2107
  }
1818
2108
  }
1819
- async function executeFrontendTestEvidenceValidation(input) {
2109
+ async function executeFrontendTestStandardScenarios(input, meta) {
2110
+ const started = Date.now();
2111
+ try {
2112
+ const result = await copyFrontendTestStandardScenarios({
2113
+ workspaceRoot: input.cwd,
2114
+ layout: frontendTestLayoutFromSpec(meta.spec),
2115
+ });
2116
+ return {
2117
+ ok: true,
2118
+ stdout: JSON.stringify(result),
2119
+ stderr: "",
2120
+ failureCategory: "success",
2121
+ durationMs: Date.now() - started,
2122
+ };
2123
+ }
2124
+ catch (error) {
2125
+ return {
2126
+ ok: false,
2127
+ stdout: "",
2128
+ stderr: error instanceof Error ? error.message : String(error),
2129
+ failureCategory: "invalid-output",
2130
+ durationMs: Date.now() - started,
2131
+ };
2132
+ }
2133
+ }
2134
+ async function executeFrontendTestEnvironmentProbe(input, meta) {
2135
+ const started = Date.now();
2136
+ const result = await probeFrontendTestEnvironment({
2137
+ workspaceRoot: input.cwd,
2138
+ layout: frontendTestLayoutFromSpec(meta.spec),
2139
+ });
2140
+ if (result.ok) {
2141
+ return {
2142
+ ok: true,
2143
+ stdout: result.stdout,
2144
+ stderr: "",
2145
+ failureCategory: "success",
2146
+ durationMs: Date.now() - started,
2147
+ };
2148
+ }
2149
+ return {
2150
+ ok: false,
2151
+ stdout: "",
2152
+ stderr: result.stderr,
2153
+ failureCategory: "nonzero-exit",
2154
+ durationMs: Date.now() - started,
2155
+ };
2156
+ }
2157
+ async function executeFrontendTestEvidenceValidation(input, meta) {
1820
2158
  const started = Date.now();
1821
2159
  try {
1822
2160
  const result = await validateFrontendCaseEvidence({
1823
2161
  workspaceRoot: input.cwd,
2162
+ layout: frontendTestLayoutFromSpec(meta.spec),
1824
2163
  });
1825
2164
  const output = `frontend case evidence validation cases=${result.cases} findings=${result.issues.length}${result.issues.length ? ` issues=${JSON.stringify(result.issues)}` : ""}`;
1826
2165
  if (result.hardFail) {
@@ -1859,6 +2198,7 @@ async function executeFrontendTestCaseManifest(input, meta) {
1859
2198
  const checklist = await validateFrontendCaseChecklist({
1860
2199
  workspaceRoot: input.cwd,
1861
2200
  declaredAcIds,
2201
+ layout: frontendTestLayoutFromSpec(meta.spec),
1862
2202
  });
1863
2203
  if (checklist.issues.length) {
1864
2204
  return {
@@ -1873,6 +2213,7 @@ async function executeFrontendTestCaseManifest(input, meta) {
1873
2213
  workspaceRoot: input.cwd,
1874
2214
  maxCases: gate.maxCases,
1875
2215
  declaredAcIds,
2216
+ layout: frontendTestLayoutFromSpec(meta.spec),
1876
2217
  });
1877
2218
  return {
1878
2219
  ok: true,
@@ -1911,6 +2252,7 @@ async function executeFrontendTestResultFinalize(input, meta) {
1911
2252
  try {
1912
2253
  const evidence = await validateFrontendCaseEvidence({
1913
2254
  workspaceRoot: input.cwd,
2255
+ layout: frontendTestLayoutFromSpec(meta.spec),
1914
2256
  });
1915
2257
  advisoryStdout = `frontend case evidence validation cases=${evidence.cases} findings=${evidence.issues.length}${evidence.issues.length ? ` issues=${JSON.stringify(evidence.issues)}` : ""}`;
1916
2258
  // Advisory findings must not block materialize; only hardFail (path escape) does.
@@ -1929,6 +2271,7 @@ async function executeFrontendTestResultFinalize(input, meta) {
1929
2271
  artifactName: "frontend-test-result.json",
1930
2272
  outputDir: "contracts",
1931
2273
  sourceBinding: meta.spec.sourceBinding,
2274
+ layout: frontendTestLayoutFromSpec(meta.spec),
1932
2275
  });
1933
2276
  return {
1934
2277
  ok: true,
@@ -1967,12 +2310,14 @@ async function executeFrontendTestReports(input, meta) {
1967
2310
  const l5 = await renderFrontendTestL5Report({
1968
2311
  workspaceRoot: input.cwd,
1969
2312
  runDir: meta.runDir,
2313
+ layout: frontendTestLayoutFromSpec(meta.spec),
1970
2314
  });
1971
2315
  lines.push(`Frontend L-5 report: ${l5.htmlPath}\nMarkdown: ${l5.markdownPath}\nStatus: ${l5.metrics.status}`);
1972
2316
  }
1973
2317
  const report = await renderFrontendTestHtmlReport({
1974
2318
  workspaceRoot: input.cwd,
1975
2319
  runDir: meta.runDir,
2320
+ layout: frontendTestLayoutFromSpec(meta.spec),
1976
2321
  });
1977
2322
  lines.push(`Frontend test report: ${report.htmlPath}\nMarkdown: ${report.markdownPath}\nOutcome: ${report.outcome}\nCases: ${report.caseCount}`);
1978
2323
  return {
@@ -2055,7 +2400,76 @@ async function executeFrontendLintBaseline(input, meta) {
2055
2400
  };
2056
2401
  }
2057
2402
  }
2058
- export async function executeDagShellNode(input, meta) {
2403
+ async function writeShellPresetProcessReceipt(input) {
2404
+ await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
2405
+ schemaVersion: 1,
2406
+ kind: "shell-preset",
2407
+ preset: input.preset,
2408
+ ok: input.ok,
2409
+ durationMs: input.durationMs,
2410
+ steps: input.steps,
2411
+ });
2412
+ }
2413
+ async function ensureShellPresetProcessReceipt(input) {
2414
+ const receiptPath = path.join(input.runDir, input.nodeId, "process-receipt.json");
2415
+ if (existsSync(receiptPath))
2416
+ return;
2417
+ if (existsSync(path.join(input.runDir, input.nodeId, "shell-command-receipt.json"))) {
2418
+ return;
2419
+ }
2420
+ const detail = input.result.ok ? input.result.stdout : input.result.stderr;
2421
+ await writeShellPresetProcessReceipt({
2422
+ runDir: input.runDir,
2423
+ nodeId: input.nodeId,
2424
+ preset: input.preset,
2425
+ ok: input.result.ok,
2426
+ durationMs: input.result.durationMs,
2427
+ steps: [
2428
+ {
2429
+ id: "run",
2430
+ label: input.preset,
2431
+ ok: input.result.ok,
2432
+ detail: detail?.slice(0, 8 * 1024),
2433
+ durationMs: input.result.durationMs,
2434
+ },
2435
+ ],
2436
+ });
2437
+ }
2438
+ function namedShellHandler(shell) {
2439
+ if (!shell)
2440
+ return null;
2441
+ const record = shell;
2442
+ for (const key of [
2443
+ "finalWriteSetApprovalGate",
2444
+ "frontendTestStandardScenarios",
2445
+ "frontendTestEnvironmentProbe",
2446
+ "frontendBrowserToolPreflight",
2447
+ "frontendPrewriteGate",
2448
+ "frontendLintBaseline",
2449
+ "frontendVerificationBundle",
2450
+ "frontendReviewContext",
2451
+ "frontendTestCaseChecklist",
2452
+ "frontendTestCaseManifest",
2453
+ "frontendTestEvidenceValidation",
2454
+ "frontendTestResultFinalize",
2455
+ "frontendTestL5Report",
2456
+ "frontendTestHtmlReport",
2457
+ "frontendTestReports",
2458
+ "backendTestPipeline",
2459
+ "jsonArtifactGate",
2460
+ "projectGovernanceGate",
2461
+ "requirementCoverageGate",
2462
+ "verdictGate",
2463
+ "verifyEvidence",
2464
+ "repairArtifactGate",
2465
+ "jacocoCoverage",
2466
+ ]) {
2467
+ if (record[key] != null)
2468
+ return key;
2469
+ }
2470
+ return null;
2471
+ }
2472
+ async function executeDagShellNodeInner(input, meta) {
2059
2473
  const shell = input.task.shell;
2060
2474
  if (shell?.finalWriteSetApprovalGate) {
2061
2475
  const started = Date.now();
@@ -2094,30 +2508,91 @@ export async function executeDagShellNode(input, meta) {
2094
2508
  };
2095
2509
  }
2096
2510
  }
2511
+ if (shell?.frontendTestStandardScenarios) {
2512
+ return executeFrontendTestStandardScenarios(input, meta);
2513
+ }
2514
+ if (shell?.frontendTestEnvironmentProbe) {
2515
+ return executeFrontendTestEnvironmentProbe(input, meta);
2516
+ }
2097
2517
  if (shell?.frontendBrowserToolPreflight) {
2098
2518
  const started = Date.now();
2519
+ const steps = [];
2520
+ const finish = async (result) => {
2521
+ await writeShellPresetProcessReceipt({
2522
+ runDir: meta.runDir,
2523
+ nodeId: input.task.id,
2524
+ preset: "frontendBrowserToolPreflight",
2525
+ ok: result.ok,
2526
+ durationMs: result.durationMs,
2527
+ steps: steps.length > 0
2528
+ ? steps
2529
+ : [
2530
+ {
2531
+ id: "run",
2532
+ label: "浏览器工具能力探测",
2533
+ ok: result.ok,
2534
+ detail: result.ok ? result.stdout : result.stderr,
2535
+ durationMs: result.durationMs,
2536
+ },
2537
+ ],
2538
+ });
2539
+ return result;
2540
+ };
2099
2541
  try {
2100
2542
  if (resolvePiBackend() === "cli-only") {
2543
+ steps.push({
2544
+ id: "pi-backend",
2545
+ label: "Pi SDK 后端可用",
2546
+ ok: false,
2547
+ detail: "CODE_AGENT_PI_BACKEND=cli-only",
2548
+ });
2101
2549
  throw new Error("browser-command-capability-unavailable: CODE_AGENT_PI_BACKEND=cli-only rejects SDK-only playwright_cli");
2102
2550
  }
2551
+ steps.push({
2552
+ id: "pi-backend",
2553
+ label: "Pi SDK 后端可用",
2554
+ ok: true,
2555
+ });
2103
2556
  const sdkCapability = await checkPiSdkCustomToolCapability(input.cwd);
2104
2557
  if (!sdkCapability.ok) {
2558
+ steps.push({
2559
+ id: "sdk-custom-tool",
2560
+ label: "Pi SDK custom-tool 能力",
2561
+ ok: false,
2562
+ detail: sdkCapability.detail,
2563
+ });
2105
2564
  throw new Error(`browser-command-capability-unavailable: ${sdkCapability.detail}`);
2106
2565
  }
2566
+ steps.push({
2567
+ id: "sdk-custom-tool",
2568
+ label: "Pi SDK custom-tool 能力",
2569
+ ok: true,
2570
+ });
2571
+ const layout = frontendTestLayoutFromSpec(meta.spec);
2107
2572
  const toolProbe = await createPlaywrightCliTool({
2108
2573
  repoRoot: input.cwd,
2109
2574
  runDir: meta.runDir,
2110
2575
  nodeId: input.task.id,
2111
2576
  caseId: "frontend-browser-preflight",
2112
- evidenceDir: "testcase/frontend/evidence/frontend-browser-preflight",
2113
- // Capability-only registration probe. The tested URL is selected later by
2114
- // retrieve-frontend-test-context-pi and validated by the environment shell.
2577
+ evidenceDir: `${layout.evidenceDir}/frontend-browser-preflight`,
2115
2578
  baseUrl: "http://localhost:5173/",
2116
- inputRoot: "testcase/frontend/fixtures",
2579
+ inputRoot: layout.fixturesDir,
2580
+ layout,
2117
2581
  });
2118
2582
  if (toolProbe.name !== "playwright_cli") {
2583
+ steps.push({
2584
+ id: "structured-tool",
2585
+ label: "注册 playwright_cli",
2586
+ ok: false,
2587
+ detail: toolProbe.name,
2588
+ });
2119
2589
  throw new Error("browser-command-capability-unavailable: structured playwright_cli tool registration failed");
2120
2590
  }
2591
+ steps.push({
2592
+ id: "structured-tool",
2593
+ label: "注册 playwright_cli",
2594
+ ok: true,
2595
+ });
2121
2596
  const launcher = resolvePlaywrightCliLauncher(input.cwd);
2122
2597
  const result = await defaultPlaywrightCliCommandRunner({
2123
2598
  executable: launcher.executable,
@@ -2130,8 +2605,19 @@ export async function executeDagShellNode(input, meta) {
2130
2605
  if (result.exitCode !== 0 ||
2131
2606
  result.timedOut ||
2132
2607
  !["open", "close", "find", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
2608
+ steps.push({
2609
+ id: "playwright-cli-help",
2610
+ label: "playwright-cli --help 合同",
2611
+ ok: false,
2612
+ detail: help.slice(0, 8 * 1024),
2613
+ });
2133
2614
  throw new Error("playwright-cli-contract-incompatible");
2134
2615
  }
2616
+ steps.push({
2617
+ id: "playwright-cli-help",
2618
+ label: "playwright-cli --help 合同",
2619
+ ok: true,
2620
+ });
2135
2621
  const capability = {
2136
2622
  schemaVersion: 1,
2137
2623
  status: "ready",
@@ -2142,22 +2628,28 @@ export async function executeDagShellNode(input, meta) {
2142
2628
  targetUrlAuthority: "retrieve-context-and-environment-probe",
2143
2629
  };
2144
2630
  await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "frontend-browser-capability.json", capability);
2145
- return {
2631
+ steps.push({
2632
+ id: "capability-artifact",
2633
+ label: "写出 frontend-browser-capability.json",
2634
+ ok: true,
2635
+ detail: JSON.stringify(capability, null, 2),
2636
+ });
2637
+ return await finish({
2146
2638
  ok: true,
2147
2639
  stdout: JSON.stringify(capability),
2148
2640
  stderr: "",
2149
2641
  failureCategory: "success",
2150
2642
  durationMs: Date.now() - started,
2151
- };
2643
+ });
2152
2644
  }
2153
2645
  catch (error) {
2154
- return {
2646
+ return await finish({
2155
2647
  ok: false,
2156
2648
  stdout: "",
2157
2649
  stderr: `playwright-cli-unavailable: ${error instanceof Error ? error.message : String(error)}`,
2158
2650
  failureCategory: "tool-policy",
2159
2651
  durationMs: Date.now() - started,
2160
- };
2652
+ });
2161
2653
  }
2162
2654
  }
2163
2655
  if (shell?.frontendPrewriteGate) {
@@ -2227,7 +2719,7 @@ export async function executeDagShellNode(input, meta) {
2227
2719
  return executeFrontendTestCaseManifest(input, meta);
2228
2720
  }
2229
2721
  if (shell?.frontendTestEvidenceValidation) {
2230
- return executeFrontendTestEvidenceValidation(input);
2722
+ return executeFrontendTestEvidenceValidation(input, meta);
2231
2723
  }
2232
2724
  if (shell?.frontendTestResultFinalize) {
2233
2725
  return executeFrontendTestResultFinalize(input, meta);
@@ -2246,6 +2738,13 @@ export async function executeDagShellNode(input, meta) {
2246
2738
  }
2247
2739
  if (shell?.jsonArtifactGate) {
2248
2740
  const started = Date.now();
2741
+ const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
2742
+ ? {
2743
+ testRoot: meta.spec.backendTestLayout.testRoot,
2744
+ markdownRel: meta.spec.backendTestLayout.markdownRel,
2745
+ scriptRel: meta.spec.backendTestLayout.scriptRel,
2746
+ }
2747
+ : undefined);
2249
2748
  try {
2250
2749
  const gate = shell.jsonArtifactGate;
2251
2750
  const sourceNodeId = gate.fallbackFromNodeIds?.length
@@ -2273,6 +2772,7 @@ export async function executeDagShellNode(input, meta) {
2273
2772
  fromNodeId: sourceNodeId,
2274
2773
  artifactName: gate.artifactName,
2275
2774
  outputDir: gate.outputDir,
2775
+ allowedTestRoot: backendLayout.testRoot,
2276
2776
  });
2277
2777
  break;
2278
2778
  case "backend-test-result-v1":
@@ -2282,6 +2782,7 @@ export async function executeDagShellNode(input, meta) {
2282
2782
  artifactName: gate.artifactName,
2283
2783
  outputDir: gate.outputDir,
2284
2784
  junitRelativePath: gate.junitRelativePath,
2785
+ testRoot: backendLayout.testRoot,
2285
2786
  });
2286
2787
  break;
2287
2788
  case "backend-test-classification-v1":
@@ -2325,6 +2826,7 @@ export async function executeDagShellNode(input, meta) {
2325
2826
  artifactName: gate.artifactName,
2326
2827
  outputDir: gate.outputDir,
2327
2828
  sourceBinding: meta.spec.sourceBinding,
2829
+ layout: frontendTestLayoutFromSpec(meta.spec),
2328
2830
  });
2329
2831
  break;
2330
2832
  default:
@@ -2570,6 +3072,7 @@ export async function executeDagShellNode(input, meta) {
2570
3072
  const cwd = resolveShellCwd(input.cwd, shell.cwd);
2571
3073
  const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
2572
3074
  const failFast = shell.failFast !== false;
3075
+ const attemptNumber = input.attempt ?? 1;
2573
3076
  const results = [];
2574
3077
  const executedCommandKeys = new Set();
2575
3078
  let beforeStatus;
@@ -2665,8 +3168,8 @@ export async function executeDagShellNode(input, meta) {
2665
3168
  dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2666
3169
  reportActivity: input.reportActivity,
2667
3170
  outputArtifacts: {
2668
- stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2669
- stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
3171
+ stdoutPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stdout.txt`),
3172
+ stderrPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stderr.txt`),
2670
3173
  },
2671
3174
  });
2672
3175
  results.push(result);
@@ -2725,6 +3228,7 @@ export async function executeDagShellNode(input, meta) {
2725
3228
  schemaVersion: 1,
2726
3229
  runId: meta.runId,
2727
3230
  nodeId: input.task.id,
3231
+ attempt: attemptNumber,
2728
3232
  workspaceFingerprint: workspaceFingerprint ?? null,
2729
3233
  failFast,
2730
3234
  commands: results.map((result) => ({
@@ -2743,6 +3247,7 @@ export async function executeDagShellNode(input, meta) {
2743
3247
  subchecks: parseCheckRepoSubcheckDurations(result),
2744
3248
  })),
2745
3249
  };
3250
+ await writeDagNodeJsonArtifact(meta.runDir, input.task.id, path.posix.join(`attempt-${attemptNumber}`, "shell-command-receipt.json"), receipt);
2746
3251
  await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "shell-command-receipt.json", receipt);
2747
3252
  const summaryMarkdown = buildShellResultSummaryMarkdown({
2748
3253
  nodeId: input.task.id,
@@ -2794,8 +3299,24 @@ export async function executeDagShellNode(input, meta) {
2794
3299
  stdout: `${recordPrefix}${reuseNote}${aggregate.stdout}`,
2795
3300
  stderr: stderrParts.filter(Boolean).join("\n\n"),
2796
3301
  failureCategory,
3302
+ ...(firstFailure?.failureCategory
3303
+ ? { rawFailureCategory: firstFailure.failureCategory }
3304
+ : {}),
2797
3305
  durationMs: Date.now() - started,
2798
3306
  // extra field consumed by node-execution if supported; also embedded in stdout marker
2799
3307
  ...{ commandResults },
2800
3308
  };
2801
3309
  }
3310
+ export async function executeDagShellNode(input, meta) {
3311
+ const handler = namedShellHandler(input.task.shell);
3312
+ const result = await executeDagShellNodeInner(input, meta);
3313
+ if (handler) {
3314
+ await ensureShellPresetProcessReceipt({
3315
+ runDir: meta.runDir,
3316
+ nodeId: input.task.id,
3317
+ preset: handler,
3318
+ result,
3319
+ });
3320
+ }
3321
+ return result;
3322
+ }