@tea-agent/loop-agent 0.39.0-next.3 → 0.39.0-next.30

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 (223) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +267 -65
  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 +118 -14
  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 +8 -1
  23. package/dist/executors/pi-sdk-executor.js +187 -59
  24. package/dist/executors/shell-executor.js +533 -29
  25. package/dist/executors/shell-write-guard.js +7 -0
  26. package/dist/shared/dag-prompt-override.js +28 -0
  27. package/dist/shared/operator/capabilities.js +181 -27
  28. package/dist/shared/update/console-notifier.js +100 -0
  29. package/dist/{cli → shared}/update/npm-client.js +41 -8
  30. package/dist/{cli → shared}/update/state.js +41 -13
  31. package/dist/task/config-types.js +22 -0
  32. package/dist/task/contract/constants.js +1 -0
  33. package/dist/task/contract/diff.js +26 -0
  34. package/dist/task/contract/project.js +5 -0
  35. package/dist/task/contract/schema.js +6 -1
  36. package/dist/task/source-prepare/build-draft.js +15 -0
  37. package/dist/worker/cli.js +31 -1
  38. package/dist/worker/console/chat/deferred-turn.js +91 -0
  39. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  40. package/dist/worker/console/chat/pi-runtime.js +165 -83
  41. package/dist/worker/console/chat/repo-walk.js +10 -1
  42. package/dist/worker/console/chat/routes.js +478 -55
  43. package/dist/worker/console/chat/semantic-activity.js +20 -8
  44. package/dist/worker/console/chat/session-store.js +146 -2
  45. package/dist/worker/console/chat/turn-process.js +17 -30
  46. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  47. package/dist/worker/console/operator-actions.js +244 -4
  48. package/dist/worker/console/operator-user-error.js +19 -0
  49. package/dist/worker/console/pi-readiness.js +12 -5
  50. package/dist/worker/console/recovery-cta.js +4 -4
  51. package/dist/worker/console/recovery-error-copy.js +4 -4
  52. package/dist/worker/console/routes.js +43 -2
  53. package/dist/worker/console/security.js +83 -10
  54. package/dist/worker/console/server.js +42 -6
  55. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-CiGOV5eG.js} +1 -1
  56. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-Ds9Gyfd8.js} +1 -1
  57. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-C6V6uC98.js} +1 -1
  58. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-DezF7Ol9.js} +1 -1
  59. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-BCWk3gCT.js} +1 -1
  60. package/dist/worker/console/static/assets/channel-DMvu1nmi.js +1 -0
  61. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-DbYr-Cxy.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-XSlusW4G.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-BtcKAj9i.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BF57lhXj.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-DJIlR25o.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-DklNc7dm.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-fZNtEdQZ.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-BBgLqV4h.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-Dl9e5n_J.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-B0JK51NT.js} +1 -1
  71. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVzM-02m.js +1 -0
  72. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVzM-02m.js +1 -0
  73. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-Ch-ajv4O.js} +1 -1
  74. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-xn_NxcfB.js} +1 -1
  75. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DRD0fyga.js} +1 -1
  76. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-COtnKp4B.js} +1 -1
  77. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-BQp_CAqG.js} +1 -1
  78. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-x0y-vNgq.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-N2ezwL31.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-CSaxRH6Q.js} +1 -1
  81. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-6Ro7Kpbd.js} +1 -1
  82. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-CrIIQEAI.js} +1 -1
  83. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-DivKcq8g.js} +1 -1
  84. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-BXrfDo8y.js} +1 -1
  85. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-LTTQmMRJ.js} +1 -1
  86. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-DP3o_5qd.js} +1 -1
  87. package/dist/worker/console/static/assets/index-C2jmYBfu.js +404 -0
  88. package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
  89. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-wVfSY1WH.js} +1 -1
  90. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-BRKfFL0I.js} +1 -1
  91. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DEml3jLb.js} +1 -1
  92. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-DTzBmOqN.js} +1 -1
  93. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-Cpy1d873.js} +1 -1
  94. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-C1cSavOy.js} +5 -5
  95. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-BEBSKvYM.js} +1 -1
  96. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-BrLs4Oac.js} +1 -1
  97. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-B47AZcLa.js} +1 -1
  98. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-s0dVeYR2.js} +1 -1
  99. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-lW8aJ7Q6.js} +1 -1
  100. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-BcHJPf6Q.js} +1 -1
  101. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-DdegpUUf.js} +1 -1
  102. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-BxFWqNv8.js} +1 -1
  103. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-cOdUDPbR.js} +1 -1
  104. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-DpxlYl8P.js} +1 -1
  105. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BwecGAkM.js +1 -0
  106. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DQOfurE0.js} +2 -2
  107. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D5WW_LbN.js +8 -0
  108. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-CIp3tC6V.js} +1 -1
  109. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-C-adIPKn.js} +1 -1
  110. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-CiYrar9M.js} +1 -1
  111. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-WHrdTV0-.js} +1 -1
  112. package/dist/worker/console/static/index.html +2 -2
  113. package/dist/worker/console/static-src/app/console-types.js +39 -0
  114. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  115. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  116. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
  117. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  118. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  119. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  120. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  121. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  122. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
  123. package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
  124. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  125. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  126. package/dist/worker/observe/node-input.js +74 -3
  127. package/dist/worker/observe/node-process.js +377 -0
  128. package/dist/worker/observe/routes.js +36 -3
  129. package/dist/worker/observe/shell-handler-keys.js +34 -0
  130. package/dist/worker/observe/static/api.js +66 -0
  131. package/dist/worker/observe/static/constants.js +8 -1
  132. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  133. package/dist/worker/observe/static/format.js +23 -0
  134. package/dist/worker/observe/static/markdown-render.js +20 -1
  135. package/dist/worker/observe/static/operator-chrome.js +4 -0
  136. package/dist/worker/observe/static/state.js +13 -2
  137. package/dist/worker/observe/static/styles.css +73 -0
  138. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  139. package/dist/worker/observe/static/views/dag.js +8 -1
  140. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  141. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
  142. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  143. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  144. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  145. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  146. package/dist/workflows/dag/backend-test-layout.js +133 -0
  147. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  148. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  149. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  150. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  151. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  152. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  153. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  154. package/dist/workflows/dag/contract-output-registry.js +15 -0
  155. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  156. package/dist/workflows/dag/convergence/controller.js +141 -0
  157. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  158. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  159. package/dist/workflows/dag/failure-category.js +12 -0
  160. package/dist/workflows/dag/frontend-implementation-contract.js +493 -2
  161. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  162. package/dist/workflows/dag/frontend-prewrite-gate.js +221 -31
  163. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  164. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  165. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  166. package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
  167. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  168. package/dist/workflows/dag/init-hybrid.js +805 -113
  169. package/dist/workflows/dag/node-execution.js +194 -6
  170. package/dist/workflows/dag/prompt.js +60 -1
  171. package/dist/workflows/dag/rerun-feedback.js +212 -0
  172. package/dist/workflows/dag/rerun-plan.js +224 -27
  173. package/dist/workflows/dag/rerun-run.js +61 -6
  174. package/dist/workflows/dag/rerun-task.js +61 -2
  175. package/dist/workflows/dag/retry-policy.js +113 -0
  176. package/dist/workflows/dag/structured-output-repair.js +712 -0
  177. package/dist/workflows/dag/types.js +157 -1
  178. package/dist/workflows/dag/validate.js +41 -1
  179. package/docs/init-surface.manifest.json +5 -0
  180. package/docs/operations/README.md +1 -1
  181. package/docs/operations/local-development-environment.md +1 -5
  182. package/docs/skills/vetted-skill-registry.md +14 -0
  183. package/docs/templates/README.md +1 -1
  184. package/docs/templates/agent-dag.schema.json +44 -0
  185. package/docs/templates/backend-test-dag.json +60 -35
  186. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  187. package/docs/templates/frontend-test-dag.json +8 -10
  188. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  189. package/docs/templates/init-managed-agents.md +13 -3
  190. package/harness.json +3 -1
  191. package/package.json +12 -4
  192. package/scripts/next-info.mjs +356 -0
  193. package/scripts/next-publish-gate.mjs +238 -0
  194. package/scripts/release-source-binding.mjs +251 -0
  195. package/skills/analyze-product-requirements/SKILL.md +8 -5
  196. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  197. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  198. package/skills/analyze-product-requirements/references/example.md +50 -0
  199. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  200. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  201. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  202. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  203. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  204. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  205. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  206. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  207. package/skills/codebase-scout/SKILL.md +1 -1
  208. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  209. package/skills/frontend-implementation/references/design-spec.md +16 -19
  210. package/skills/frontend-implementation/references/node-contracts.md +9 -6
  211. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  212. package/skills/improve-codebase-architecture/deepening.md +37 -0
  213. package/skills/improve-codebase-architecture/html-report.md +123 -0
  214. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  215. package/skills/improve-codebase-architecture/language.md +53 -0
  216. package/skills/loop-agent/references/command-reference.md +11 -4
  217. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  218. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  219. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  220. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  221. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  222. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  223. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.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,13 @@ 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 { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
30
34
  import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
31
35
  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";
36
+ 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
37
  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";
38
+ import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
35
39
  import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
36
40
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
37
41
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
@@ -48,6 +52,7 @@ async function runBackendScenarioParamPreflight(input) {
48
52
  phase: "initial",
49
53
  repairAttempt: 0,
50
54
  strictScenarioParamGate: input.strictScenarioParamGate,
55
+ layout: input.layout,
51
56
  });
52
57
  const initialArtifacts = await writeBackendScenarioParamArtifacts({
53
58
  runDir: input.runDir,
@@ -73,6 +78,7 @@ async function runBackendScenarioParamPreflight(input) {
73
78
  phase: "final",
74
79
  repairAttempt: 1,
75
80
  strictScenarioParamGate: input.strictScenarioParamGate,
81
+ layout: input.layout,
76
82
  });
77
83
  finalFacts = reassessed.facts;
78
84
  finalMarkdown = reassessed.markdown;
@@ -117,6 +123,39 @@ function shouldReuseSuccessfulShellCommands(shell) {
117
123
  const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
118
124
  const SUMMARY_STDOUT_MAX = 4_000;
119
125
  const SUMMARY_STDERR_MAX = 2_000;
126
+ const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
127
+ const TEST_TIMEOUT_MARKERS = [
128
+ /Test timed out in \d+ms/i,
129
+ /Timeout of \d+ms exceeded/i,
130
+ ];
131
+ const DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS = [
132
+ /AssertionError/i,
133
+ /expected:?\s/i,
134
+ /received:?\s/i,
135
+ /Snapshot /i,
136
+ /toMatchSnapshot/i,
137
+ /SyntaxError/i,
138
+ /TS\d{4}/,
139
+ /error TS/i,
140
+ /eslint/i,
141
+ /compile error/i,
142
+ /Failed to compile/i,
143
+ ];
144
+ function isIdentifiedTestCommand(command) {
145
+ 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);
146
+ }
147
+ function classifyShellCommandFailure(input) {
148
+ if (input.processCategory !== "nonzero-exit")
149
+ return input.processCategory;
150
+ if (!isIdentifiedTestCommand(input.command))
151
+ return "nonzero-exit";
152
+ const sample = `${input.stdout}\n${input.stderr}`.slice(0, TEST_TIMEOUT_CLASSIFY_MAX_CHARS);
153
+ const hasTimeoutMarker = TEST_TIMEOUT_MARKERS.some((marker) => marker.test(sample));
154
+ if (!hasTimeoutMarker)
155
+ return "nonzero-exit";
156
+ const mixedDeterministicFailure = DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS.some((marker) => marker.test(sample));
157
+ return mixedDeterministicFailure ? "nonzero-exit" : "test-timeout";
158
+ }
120
159
  const NODE_STDOUT_MAX = 12_000;
121
160
  const NODE_STDERR_MAX = 8_000;
122
161
  const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
@@ -323,16 +362,21 @@ export async function executeShellCommand(input) {
323
362
  });
324
363
  });
325
364
  child.on("close", (code) => {
326
- let failureCategory = "nonzero-exit";
365
+ let processCategory = "nonzero-exit";
327
366
  if (timedOut) {
328
- failureCategory = "timeout";
367
+ processCategory = "timeout";
329
368
  }
330
369
  else if (code === 0) {
331
- failureCategory = "success";
370
+ processCategory = "success";
332
371
  }
333
372
  finish({
334
373
  exitCode: code,
335
- failureCategory,
374
+ failureCategory: classifyShellCommandFailure({
375
+ command: input.command,
376
+ processCategory,
377
+ stdout: formatBoundedOutput(stdout),
378
+ stderr: formatBoundedOutput(stderr),
379
+ }),
336
380
  ok: code === 0 && !timedOut,
337
381
  });
338
382
  });
@@ -492,11 +536,114 @@ async function readAdvisoryRunReport(reportsDir, filename, title) {
492
536
  async function executeBackendTestPipeline(input, meta) {
493
537
  const pipeline = input.task.shell?.backendTestPipeline;
494
538
  const started = Date.now();
539
+ // Plan A: prefer the frozen layout carried by the compiled spec; fall back
540
+ // to the historical default for hand-written legacy DAGs.
541
+ const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
542
+ ? {
543
+ testRoot: meta.spec.backendTestLayout.testRoot,
544
+ markdownRel: meta.spec.backendTestLayout.markdownRel,
545
+ scriptRel: meta.spec.backendTestLayout.scriptRel,
546
+ }
547
+ : undefined);
495
548
  try {
496
549
  if (!pipeline)
497
550
  throw new Error("missing backend-test pipeline id");
498
551
  const outputs = [];
499
- if (pipeline === "markdown-environment") {
552
+ if (pipeline === "ingest-backend-test-gap") {
553
+ // Plan D: deterministic gap ingest. Identity (taskId + requirement
554
+ // sha256) is fail-closed; a mismatched plan blocks every writer.
555
+ const gapDocRel = meta.spec.globalConstraints
556
+ ?.map((item) => /^backendTestGapDoc=(.+)$/i.exec(item.trim())?.[1])
557
+ .find(Boolean);
558
+ const binding = meta.spec.sourceBinding;
559
+ const requirementSha256 = binding?.sources.find((s) => s.kind === "requirement")?.sha256 ?? "";
560
+ if (!gapDocRel || !binding || !requirementSha256) {
561
+ return {
562
+ ok: false,
563
+ stdout: outputs.join("\n"),
564
+ stderr: "backend-test gap ingest requires the bound gap document path and the task source binding with a requirement sha256",
565
+ failureCategory: "invalid-output",
566
+ durationMs: Date.now() - started,
567
+ };
568
+ }
569
+ const gapDocAbs = path.join(input.cwd, ...gapDocRel.split("/"));
570
+ let parsed;
571
+ try {
572
+ parsed = JSON.parse(await readFile(gapDocAbs, "utf8"));
573
+ }
574
+ catch (error) {
575
+ return {
576
+ ok: false,
577
+ stdout: outputs.join("\n"),
578
+ stderr: `backend-test gap document is unreadable JSON at ${gapDocRel}: ${error instanceof Error ? error.message : String(error)}`,
579
+ failureCategory: "invalid-output",
580
+ durationMs: Date.now() - started,
581
+ };
582
+ }
583
+ const doc = backendTestGapDocSchema.safeParse(parsed);
584
+ if (!doc.success) {
585
+ return {
586
+ ok: false,
587
+ stdout: outputs.join("\n"),
588
+ stderr: `invalid backend-test-gap-v1 document: ${doc.error.issues
589
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
590
+ .join("; ")}`,
591
+ failureCategory: "invalid-output",
592
+ durationMs: Date.now() - started,
593
+ };
594
+ }
595
+ // previous N6 facts (may be absent on a fresh checkout)
596
+ const factsPath = path.join(input.cwd, "contracts", "backend-test-case-coverage-facts.json");
597
+ let previousMissingSlots;
598
+ let previousCoveredSlots;
599
+ try {
600
+ const facts = JSON.parse(await readFile(factsPath, "utf8"));
601
+ previousMissingSlots = facts.scenarioPartitions?.missingSlots;
602
+ previousCoveredSlots = (facts.rules ?? []).flatMap((rule) => rule.coveredTestPoints ?? []);
603
+ }
604
+ catch {
605
+ // no previous facts in the workspace: the gap doc alone drives the plan
606
+ }
607
+ // module index comes from the README on disk
608
+ let existingModuleStems = [];
609
+ try {
610
+ const files = await markdownFiles(input.cwd, backendLayout);
611
+ existingModuleStems = files
612
+ .map((file) => path.basename(file, ".md"))
613
+ .filter((stem) => stem.toLowerCase() !== "readme");
614
+ }
615
+ catch {
616
+ // no markdown tree: every slot becomes a conflict
617
+ }
618
+ const plan = ingestBackendTestGap({
619
+ gapDoc: doc.data,
620
+ previousMissingSlots,
621
+ previousCoveredSlots,
622
+ existingModuleStems,
623
+ sharedSetupPath: meta.spec.backendTestSharedSetup?.path ?? null,
624
+ layout: backendLayout,
625
+ taskId: binding.taskId,
626
+ requirementSha256,
627
+ });
628
+ const contractsDir = path.join(meta.runDir, "contracts");
629
+ await mkdir(contractsDir, { recursive: true });
630
+ const planPath = path.join(contractsDir, "backend-test-gap-plan-v1.json");
631
+ await writeFile(planPath, JSON.stringify(plan, null, 2), "utf8");
632
+ outputs.push(`gapPlan=${planPath}`, `gapPlanStatus=${plan.status}`);
633
+ if (plan.status === "error") {
634
+ return {
635
+ ok: false,
636
+ stdout: outputs.join("\n"),
637
+ stderr: [
638
+ "backend-test gap ingest failed closed (identity/conflicts):",
639
+ ...plan.conflicts,
640
+ ].join("\n"),
641
+ failureCategory: "invalid-output",
642
+ durationMs: Date.now() - started,
643
+ };
644
+ }
645
+ }
646
+ else if (pipeline === "markdown-environment") {
500
647
  const results = await executePipelineCommands(input, meta);
501
648
  if (!results.every((result) => result.ok)) {
502
649
  const failure = results.find((result) => !result.ok);
@@ -530,6 +677,8 @@ async function executeBackendTestPipeline(input, meta) {
530
677
  environmentMarkdown,
531
678
  requiredRequirementIds,
532
679
  sourceBinding: meta.spec.sourceBinding,
680
+ layout: backendLayout,
681
+ sharedSetup: meta.spec.backendTestSharedSetup ?? null,
533
682
  });
534
683
  }
535
684
  catch (error) {
@@ -583,6 +732,7 @@ async function executeBackendTestPipeline(input, meta) {
583
732
  const coverage = await analyzeBackendTestCaseCoverage({
584
733
  workspaceRoot: input.cwd,
585
734
  sourceBinding: normalizedBinding,
735
+ layout: backendLayout,
586
736
  });
587
737
  const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage.markdown);
588
738
  const contractsDir = path.join(meta.runDir, "contracts");
@@ -590,6 +740,23 @@ async function executeBackendTestPipeline(input, meta) {
590
740
  const factsPath = path.join(contractsDir, "backend-test-case-coverage-facts.json");
591
741
  await writeFile(factsPath, JSON.stringify(coverage.facts, null, 2), "utf8");
592
742
  outputs.push(`caseCoverage=${coveragePath}`, `caseCoverageFacts=${factsPath}`, coverage.markdown);
743
+ // Plan C: scenario-partition slot coverage is fail-closed. General
744
+ // coverage findings stay advisory, but a declared filter axis missing
745
+ // an each-value/omitted slot or its complement slot must block pytest
746
+ // generation until the Markdown declares the slot.
747
+ 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));
748
+ if (partitionBlocking.length > 0) {
749
+ return {
750
+ ok: false,
751
+ stdout: outputs.join("\n\n"),
752
+ stderr: [
753
+ "backend-test scenario partitions are incomplete (fail-closed):",
754
+ ...partitionBlocking,
755
+ ].join("\n"),
756
+ failureCategory: "invalid-output",
757
+ durationMs: Date.now() - started,
758
+ };
759
+ }
593
760
  }
594
761
  catch (error) {
595
762
  const message = error instanceof Error ? error.message : String(error);
@@ -606,9 +773,9 @@ async function executeBackendTestPipeline(input, meta) {
606
773
  }
607
774
  }
608
775
  else if (pipeline === "markdown-collection-assess") {
609
- const resolution = await resolveBackendTestMappedPytestScripts(input.cwd);
776
+ const resolution = await resolveBackendTestMappedPytestScripts(input.cwd, backendLayout);
610
777
  let facts;
611
- const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
778
+ const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
612
779
  const priorityOnlyFacts = assessPriorityOnlyBackendPytestModules({
613
780
  phase: "initial",
614
781
  mappedScripts: resolution.expectedScripts,
@@ -618,7 +785,7 @@ async function executeBackendTestPipeline(input, meta) {
618
785
  facts = priorityOnlyFacts;
619
786
  }
620
787
  else if (resolution.missingScripts.length > 0) {
621
- const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
788
+ const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
622
789
  facts = assessMissingBackendPytestScripts({
623
790
  mappedScripts: resolution.expectedScripts,
624
791
  existingMappedScripts: resolution.existingScripts,
@@ -632,10 +799,11 @@ async function executeBackendTestPipeline(input, meta) {
632
799
  const scenario = await runBackendScenarioParamPreflight({
633
800
  workspaceRoot: input.cwd,
634
801
  runDir: meta.runDir,
802
+ layout: backendLayout,
635
803
  strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
636
804
  });
637
805
  outputs.push(...scenario.outputs);
638
- const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
806
+ const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
639
807
  const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
640
808
  const targets = mappedScripts.map(shellQuote).join(" ");
641
809
  const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
@@ -669,6 +837,7 @@ async function executeBackendTestPipeline(input, meta) {
669
837
  fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
670
838
  }
671
839
  facts = assessBackendPytestCollection({
840
+ layout: backendLayout,
672
841
  phase: "initial",
673
842
  mappedScripts,
674
843
  inventory,
@@ -678,6 +847,43 @@ async function executeBackendTestPipeline(input, meta) {
678
847
  fixtureResolution,
679
848
  });
680
849
  }
850
+ try {
851
+ const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
852
+ workspaceRoot: input.cwd,
853
+ layout: backendLayout,
854
+ taskId: meta.spec.sourceBinding?.taskId ?? "unbound",
855
+ });
856
+ const correspondenceReportPath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence-initial.md", correspondence.markdown);
857
+ const contractsDir = path.join(meta.runDir, "contracts");
858
+ await mkdir(contractsDir, { recursive: true });
859
+ const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
860
+ await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
861
+ outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
862
+ if (correspondence.facts.status === "FAIL" && facts.status === "PASS") {
863
+ const correspondenceRepairPaths = Array.from(new Set(correspondence.facts.entries
864
+ .filter((entry) => entry.status !== "EXACT_1_TO_1")
865
+ .flatMap((entry) => [entry.expectedScript, ...entry.actualScripts])
866
+ .filter((candidate) => candidate.startsWith("testcase/") && candidate.endsWith(".py")))).sort();
867
+ if (correspondenceRepairPaths.length > 0) {
868
+ facts.status = "REPAIRABLE";
869
+ facts.repairEligible = true;
870
+ facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
871
+ facts.findings.push({
872
+ kind: "markdown-pytest-correspondence",
873
+ classification: "test-asset-defect",
874
+ repairability: "repairable",
875
+ detail: correspondence.facts.entries
876
+ .filter((entry) => entry.status !== "EXACT_1_TO_1")
877
+ .flatMap((entry) => entry.findings)
878
+ .join("; ")
879
+ .slice(0, 12_000) || "Markdown-to-pytest correspondence is incomplete",
880
+ });
881
+ }
882
+ }
883
+ }
884
+ catch (error) {
885
+ outputs.push(`initialCorrespondence=UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
886
+ }
681
887
  const artifacts = await writeBackendPytestCollectionArtifacts({
682
888
  runDir: meta.runDir,
683
889
  stem: "initial",
@@ -711,14 +917,15 @@ async function executeBackendTestPipeline(input, meta) {
711
917
  });
712
918
  }
713
919
  else if (initial.status === "REPAIRABLE") {
714
- const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
920
+ const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
715
921
  const scenario = await runBackendScenarioParamPreflight({
716
922
  workspaceRoot: input.cwd,
717
923
  runDir: meta.runDir,
924
+ layout: backendLayout,
718
925
  strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
719
926
  });
720
927
  outputs.push(...scenario.outputs);
721
- const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
928
+ const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
722
929
  const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
723
930
  const targets = mappedScripts.map(shellQuote).join(" ");
724
931
  const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
@@ -753,6 +960,7 @@ async function executeBackendTestPipeline(input, meta) {
753
960
  fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
754
961
  }
755
962
  const finalFacts = assessBackendPytestCollection({
963
+ layout: backendLayout,
756
964
  phase: "final",
757
965
  mappedScripts,
758
966
  inventory,
@@ -776,12 +984,57 @@ async function executeBackendTestPipeline(input, meta) {
776
984
  facts: effective,
777
985
  });
778
986
  const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
987
+ const correspondence = meta.spec.sourceBinding
988
+ ? await analyzeBackendTestMarkdownPytestCorrespondence({
989
+ workspaceRoot: input.cwd,
990
+ layout: backendLayout,
991
+ taskId: meta.spec.sourceBinding.taskId,
992
+ })
993
+ : undefined;
994
+ const eligibility = correspondence
995
+ ? buildBackendTestItemEligibility(effective.collectedItemIds, {
996
+ correspondenceEntries: correspondence.facts.entries,
997
+ scenarioEntries: scenarioFacts.entries,
998
+ })
999
+ : { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
1000
+ const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
1001
+ const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
1002
+ await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
1003
+ await writeFile(eligibilityFactsPath, `${JSON.stringify({
1004
+ schemaId: "backend-test-execution-eligibility-v1",
1005
+ status: eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
1006
+ collectedItemCount: effective.collectedItemIds.length,
1007
+ eligibleItemCount: eligibility.eligibleItemIds.length,
1008
+ excludedItemCount: eligibility.excludedItems.length,
1009
+ eligibleItemIds: eligibility.eligibleItemIds,
1010
+ excludedItems: eligibility.excludedItems,
1011
+ inputHashes: eligibilityInputHashes,
1012
+ }, null, 2)}\n`, "utf8");
1013
+ const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
1014
+ "# Backend Test Execution Eligibility",
1015
+ "",
1016
+ "## Status",
1017
+ "",
1018
+ eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
1019
+ "",
1020
+ `- Collected items: ${effective.collectedItemIds.length}`,
1021
+ `- Eligible items: ${eligibility.eligibleItemIds.length}`,
1022
+ `- Excluded items: ${eligibility.excludedItems.length}`,
1023
+ "",
1024
+ "## Excluded Items",
1025
+ "",
1026
+ ...(eligibility.excludedItems.length > 0 ? eligibility.excludedItems.map((item) => `- \`${item.itemId}\` (${item.caseId ?? "unknown Case"}): ${item.reasons.join("; ")}`) : ["- None"]),
1027
+ "",
1028
+ ].join("\n"));
1029
+ outputs.push(`executionEligibility=${eligibilityReportPath}`, `executionEligibilityFacts=${eligibilityFactsPath}`);
779
1030
  const readiness = await materializeBackendTestExecutionReadiness({
780
1031
  runDir: meta.runDir,
781
1032
  workspaceRoot: input.cwd,
782
1033
  effective,
783
1034
  scenarioParamStatus: scenarioFacts.overallStatus,
784
1035
  scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
1036
+ eligibility,
1037
+ eligibilityInputHashes,
785
1038
  });
786
1039
  return {
787
1040
  ok: true,
@@ -792,6 +1045,8 @@ async function executeBackendTestPipeline(input, meta) {
792
1045
  collectedItemCount: effective.collectedItemCount,
793
1046
  fixtureResolutionStatus: effective.fixtureResolutionStatus,
794
1047
  executionReadinessStatus: readiness.status,
1048
+ eligibleItemCount: readiness.eligibleItemIds.length,
1049
+ excludedItemCount: readiness.excludedItems.length,
795
1050
  factsPath: "contracts/backend-test-pytest-collection-effective.json",
796
1051
  readinessPath: "contracts/backend-test-execution-readiness.json",
797
1052
  reportPath: artifacts.reportPath,
@@ -826,6 +1081,7 @@ async function executeBackendTestPipeline(input, meta) {
826
1081
  try {
827
1082
  const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
828
1083
  workspaceRoot: input.cwd,
1084
+ layout: backendLayout,
829
1085
  taskId: sourceBinding.taskId,
830
1086
  });
831
1087
  const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence.markdown);
@@ -888,9 +1144,32 @@ async function executeBackendTestPipeline(input, meta) {
888
1144
  else if (pipeline === "markdown-execute-html") {
889
1145
  const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
890
1146
  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(" ");
1147
+ const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
1148
+ let eligibleItemIds = executionReadiness.eligibleItemIds;
1149
+ // Plan D gap-fill executeScope=affected: when a gap plan exists in
1150
+ // this run, prefer the newly planned items (pytest node ids contain
1151
+ // the TP slot id) plus every collected item of the patched modules;
1152
+ // scope=all keeps the full eligible set.
1153
+ const affectedScope = meta.spec.globalConstraints?.some((item) => /^backendTestExecuteScope=affected$/i.test(item.trim())) ?? false;
1154
+ if (affectedScope) {
1155
+ try {
1156
+ const plan = JSON.parse(await readFile(path.join(meta.runDir, "contracts", "backend-test-gap-plan-v1.json"), "utf8"));
1157
+ const newPoints = new Set(plan.newTestPoints ?? []);
1158
+ const modules = new Set((plan.targetModules ?? []).map((stem) => `test_${stem}.py`));
1159
+ const affected = eligibleItemIds.filter((id) => modules.size === 0 ||
1160
+ [...modules].some((script) => id.includes(script)) ||
1161
+ [...newPoints].some((point) => id.includes(point)));
1162
+ if (affected.length > 0)
1163
+ eligibleItemIds = affected;
1164
+ }
1165
+ catch {
1166
+ // no gap plan in this run: execute the full eligible set
1167
+ }
1168
+ }
1169
+ const pytestTargetsRelative = "reports/backend-test-pytest-targets.txt";
1170
+ const pytestTargetsAbsolute = path.join(meta.runDir, pytestTargetsRelative);
1171
+ await writeFile(pytestTargetsAbsolute, `${eligibleItemIds.join("\n")}\n`, "utf8");
1172
+ const pytestTargetsArg = `@\"\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}\"`;
894
1173
  // Split into short bash -c commands (aligned with markdown-environment).
895
1174
  // A single ultra-long compound command has been observed on Windows to
896
1175
  // exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
@@ -967,8 +1246,10 @@ async function executeBackendTestPipeline(input, meta) {
967
1246
  'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
968
1247
  'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
969
1248
  '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`,
1249
+ `echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
1250
+ `echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
1251
+ `echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
1252
+ `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
1253
  "STATUS=$?",
973
1254
  'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
974
1255
  'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
@@ -1053,11 +1334,13 @@ async function executeBackendTestPipeline(input, meta) {
1053
1334
  htmlContent: pytestHtmlContent,
1054
1335
  pytestExitCode,
1055
1336
  });
1056
- const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
1337
+ const cases = await collectBackendTestHumanCaseCatalog(input.cwd, backendLayout);
1057
1338
  const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
1058
1339
  const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
1059
1340
  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");
1341
+ const correspondenceSummaryBase = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
1342
+ const eligibilitySummary = await readAdvisoryRunReport(reportsDir, "backend-test-execution-eligibility.md", "Backend Test Execution Eligibility");
1343
+ const correspondenceSummary = `${correspondenceSummaryBase.trim()}\n\n${eligibilitySummary.trim()}\n`;
1061
1344
  const htmlContent = renderBackendTestHtml({
1062
1345
  title: meta.spec.title,
1063
1346
  parsed,
@@ -1245,6 +1528,7 @@ async function executeBackendTestPipeline(input, meta) {
1245
1528
  fromNodeId: "backend-test-execution-envelope",
1246
1529
  artifactName: "backend-test-execution.json",
1247
1530
  outputDir: "contracts",
1531
+ allowedTestRoot: backendLayout.testRoot,
1248
1532
  });
1249
1533
  outputs.push(`analysis=${analysis.path}`, `execution=${execution.path}`);
1250
1534
  }
@@ -1360,6 +1644,7 @@ async function executeBackendTestPipeline(input, meta) {
1360
1644
  artifactName: "backend-test-result-initial.json",
1361
1645
  outputDir: "contracts",
1362
1646
  junitRelativePath: "reports/backend-test-initial-junit.xml",
1647
+ testRoot: backendLayout.testRoot,
1363
1648
  });
1364
1649
  outputs.push(...results.map((result) => result.stdout), `result=${artifact.path}`);
1365
1650
  }
@@ -1816,6 +2101,52 @@ async function executeFrontendTestHtmlReport(input, meta) {
1816
2101
  };
1817
2102
  }
1818
2103
  }
2104
+ async function executeFrontendTestStandardScenarios(input) {
2105
+ const started = Date.now();
2106
+ try {
2107
+ const result = await copyFrontendTestStandardScenarios({
2108
+ workspaceRoot: input.cwd,
2109
+ });
2110
+ return {
2111
+ ok: true,
2112
+ stdout: JSON.stringify(result),
2113
+ stderr: "",
2114
+ failureCategory: "success",
2115
+ durationMs: Date.now() - started,
2116
+ };
2117
+ }
2118
+ catch (error) {
2119
+ return {
2120
+ ok: false,
2121
+ stdout: "",
2122
+ stderr: error instanceof Error ? error.message : String(error),
2123
+ failureCategory: "invalid-output",
2124
+ durationMs: Date.now() - started,
2125
+ };
2126
+ }
2127
+ }
2128
+ async function executeFrontendTestEnvironmentProbe(input) {
2129
+ const started = Date.now();
2130
+ const result = await probeFrontendTestEnvironment({
2131
+ workspaceRoot: input.cwd,
2132
+ });
2133
+ if (result.ok) {
2134
+ return {
2135
+ ok: true,
2136
+ stdout: result.stdout,
2137
+ stderr: "",
2138
+ failureCategory: "success",
2139
+ durationMs: Date.now() - started,
2140
+ };
2141
+ }
2142
+ return {
2143
+ ok: false,
2144
+ stdout: "",
2145
+ stderr: result.stderr,
2146
+ failureCategory: "nonzero-exit",
2147
+ durationMs: Date.now() - started,
2148
+ };
2149
+ }
1819
2150
  async function executeFrontendTestEvidenceValidation(input) {
1820
2151
  const started = Date.now();
1821
2152
  try {
@@ -2055,7 +2386,76 @@ async function executeFrontendLintBaseline(input, meta) {
2055
2386
  };
2056
2387
  }
2057
2388
  }
2058
- export async function executeDagShellNode(input, meta) {
2389
+ async function writeShellPresetProcessReceipt(input) {
2390
+ await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
2391
+ schemaVersion: 1,
2392
+ kind: "shell-preset",
2393
+ preset: input.preset,
2394
+ ok: input.ok,
2395
+ durationMs: input.durationMs,
2396
+ steps: input.steps,
2397
+ });
2398
+ }
2399
+ async function ensureShellPresetProcessReceipt(input) {
2400
+ const receiptPath = path.join(input.runDir, input.nodeId, "process-receipt.json");
2401
+ if (existsSync(receiptPath))
2402
+ return;
2403
+ if (existsSync(path.join(input.runDir, input.nodeId, "shell-command-receipt.json"))) {
2404
+ return;
2405
+ }
2406
+ const detail = input.result.ok ? input.result.stdout : input.result.stderr;
2407
+ await writeShellPresetProcessReceipt({
2408
+ runDir: input.runDir,
2409
+ nodeId: input.nodeId,
2410
+ preset: input.preset,
2411
+ ok: input.result.ok,
2412
+ durationMs: input.result.durationMs,
2413
+ steps: [
2414
+ {
2415
+ id: "run",
2416
+ label: input.preset,
2417
+ ok: input.result.ok,
2418
+ detail: detail?.slice(0, 8 * 1024),
2419
+ durationMs: input.result.durationMs,
2420
+ },
2421
+ ],
2422
+ });
2423
+ }
2424
+ function namedShellHandler(shell) {
2425
+ if (!shell)
2426
+ return null;
2427
+ const record = shell;
2428
+ for (const key of [
2429
+ "finalWriteSetApprovalGate",
2430
+ "frontendTestStandardScenarios",
2431
+ "frontendTestEnvironmentProbe",
2432
+ "frontendBrowserToolPreflight",
2433
+ "frontendPrewriteGate",
2434
+ "frontendLintBaseline",
2435
+ "frontendVerificationBundle",
2436
+ "frontendReviewContext",
2437
+ "frontendTestCaseChecklist",
2438
+ "frontendTestCaseManifest",
2439
+ "frontendTestEvidenceValidation",
2440
+ "frontendTestResultFinalize",
2441
+ "frontendTestL5Report",
2442
+ "frontendTestHtmlReport",
2443
+ "frontendTestReports",
2444
+ "backendTestPipeline",
2445
+ "jsonArtifactGate",
2446
+ "projectGovernanceGate",
2447
+ "requirementCoverageGate",
2448
+ "verdictGate",
2449
+ "verifyEvidence",
2450
+ "repairArtifactGate",
2451
+ "jacocoCoverage",
2452
+ ]) {
2453
+ if (record[key] != null)
2454
+ return key;
2455
+ }
2456
+ return null;
2457
+ }
2458
+ async function executeDagShellNodeInner(input, meta) {
2059
2459
  const shell = input.task.shell;
2060
2460
  if (shell?.finalWriteSetApprovalGate) {
2061
2461
  const started = Date.now();
@@ -2094,30 +2494,89 @@ export async function executeDagShellNode(input, meta) {
2094
2494
  };
2095
2495
  }
2096
2496
  }
2497
+ if (shell?.frontendTestStandardScenarios) {
2498
+ return executeFrontendTestStandardScenarios(input);
2499
+ }
2500
+ if (shell?.frontendTestEnvironmentProbe) {
2501
+ return executeFrontendTestEnvironmentProbe(input);
2502
+ }
2097
2503
  if (shell?.frontendBrowserToolPreflight) {
2098
2504
  const started = Date.now();
2505
+ const steps = [];
2506
+ const finish = async (result) => {
2507
+ await writeShellPresetProcessReceipt({
2508
+ runDir: meta.runDir,
2509
+ nodeId: input.task.id,
2510
+ preset: "frontendBrowserToolPreflight",
2511
+ ok: result.ok,
2512
+ durationMs: result.durationMs,
2513
+ steps: steps.length > 0
2514
+ ? steps
2515
+ : [
2516
+ {
2517
+ id: "run",
2518
+ label: "浏览器工具能力探测",
2519
+ ok: result.ok,
2520
+ detail: result.ok ? result.stdout : result.stderr,
2521
+ durationMs: result.durationMs,
2522
+ },
2523
+ ],
2524
+ });
2525
+ return result;
2526
+ };
2099
2527
  try {
2100
2528
  if (resolvePiBackend() === "cli-only") {
2529
+ steps.push({
2530
+ id: "pi-backend",
2531
+ label: "Pi SDK 后端可用",
2532
+ ok: false,
2533
+ detail: "CODE_AGENT_PI_BACKEND=cli-only",
2534
+ });
2101
2535
  throw new Error("browser-command-capability-unavailable: CODE_AGENT_PI_BACKEND=cli-only rejects SDK-only playwright_cli");
2102
2536
  }
2537
+ steps.push({
2538
+ id: "pi-backend",
2539
+ label: "Pi SDK 后端可用",
2540
+ ok: true,
2541
+ });
2103
2542
  const sdkCapability = await checkPiSdkCustomToolCapability(input.cwd);
2104
2543
  if (!sdkCapability.ok) {
2544
+ steps.push({
2545
+ id: "sdk-custom-tool",
2546
+ label: "Pi SDK custom-tool 能力",
2547
+ ok: false,
2548
+ detail: sdkCapability.detail,
2549
+ });
2105
2550
  throw new Error(`browser-command-capability-unavailable: ${sdkCapability.detail}`);
2106
2551
  }
2552
+ steps.push({
2553
+ id: "sdk-custom-tool",
2554
+ label: "Pi SDK custom-tool 能力",
2555
+ ok: true,
2556
+ });
2107
2557
  const toolProbe = await createPlaywrightCliTool({
2108
2558
  repoRoot: input.cwd,
2109
2559
  runDir: meta.runDir,
2110
2560
  nodeId: input.task.id,
2111
2561
  caseId: "frontend-browser-preflight",
2112
2562
  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.
2115
2563
  baseUrl: "http://localhost:5173/",
2116
2564
  inputRoot: "testcase/frontend/fixtures",
2117
2565
  });
2118
2566
  if (toolProbe.name !== "playwright_cli") {
2567
+ steps.push({
2568
+ id: "structured-tool",
2569
+ label: "注册 playwright_cli",
2570
+ ok: false,
2571
+ detail: toolProbe.name,
2572
+ });
2119
2573
  throw new Error("browser-command-capability-unavailable: structured playwright_cli tool registration failed");
2120
2574
  }
2575
+ steps.push({
2576
+ id: "structured-tool",
2577
+ label: "注册 playwright_cli",
2578
+ ok: true,
2579
+ });
2121
2580
  const launcher = resolvePlaywrightCliLauncher(input.cwd);
2122
2581
  const result = await defaultPlaywrightCliCommandRunner({
2123
2582
  executable: launcher.executable,
@@ -2130,8 +2589,19 @@ export async function executeDagShellNode(input, meta) {
2130
2589
  if (result.exitCode !== 0 ||
2131
2590
  result.timedOut ||
2132
2591
  !["open", "close", "find", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
2592
+ steps.push({
2593
+ id: "playwright-cli-help",
2594
+ label: "playwright-cli --help 合同",
2595
+ ok: false,
2596
+ detail: help.slice(0, 8 * 1024),
2597
+ });
2133
2598
  throw new Error("playwright-cli-contract-incompatible");
2134
2599
  }
2600
+ steps.push({
2601
+ id: "playwright-cli-help",
2602
+ label: "playwright-cli --help 合同",
2603
+ ok: true,
2604
+ });
2135
2605
  const capability = {
2136
2606
  schemaVersion: 1,
2137
2607
  status: "ready",
@@ -2142,22 +2612,28 @@ export async function executeDagShellNode(input, meta) {
2142
2612
  targetUrlAuthority: "retrieve-context-and-environment-probe",
2143
2613
  };
2144
2614
  await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "frontend-browser-capability.json", capability);
2145
- return {
2615
+ steps.push({
2616
+ id: "capability-artifact",
2617
+ label: "写出 frontend-browser-capability.json",
2618
+ ok: true,
2619
+ detail: JSON.stringify(capability, null, 2),
2620
+ });
2621
+ return await finish({
2146
2622
  ok: true,
2147
2623
  stdout: JSON.stringify(capability),
2148
2624
  stderr: "",
2149
2625
  failureCategory: "success",
2150
2626
  durationMs: Date.now() - started,
2151
- };
2627
+ });
2152
2628
  }
2153
2629
  catch (error) {
2154
- return {
2630
+ return await finish({
2155
2631
  ok: false,
2156
2632
  stdout: "",
2157
2633
  stderr: `playwright-cli-unavailable: ${error instanceof Error ? error.message : String(error)}`,
2158
2634
  failureCategory: "tool-policy",
2159
2635
  durationMs: Date.now() - started,
2160
- };
2636
+ });
2161
2637
  }
2162
2638
  }
2163
2639
  if (shell?.frontendPrewriteGate) {
@@ -2246,6 +2722,13 @@ export async function executeDagShellNode(input, meta) {
2246
2722
  }
2247
2723
  if (shell?.jsonArtifactGate) {
2248
2724
  const started = Date.now();
2725
+ const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
2726
+ ? {
2727
+ testRoot: meta.spec.backendTestLayout.testRoot,
2728
+ markdownRel: meta.spec.backendTestLayout.markdownRel,
2729
+ scriptRel: meta.spec.backendTestLayout.scriptRel,
2730
+ }
2731
+ : undefined);
2249
2732
  try {
2250
2733
  const gate = shell.jsonArtifactGate;
2251
2734
  const sourceNodeId = gate.fallbackFromNodeIds?.length
@@ -2273,6 +2756,7 @@ export async function executeDagShellNode(input, meta) {
2273
2756
  fromNodeId: sourceNodeId,
2274
2757
  artifactName: gate.artifactName,
2275
2758
  outputDir: gate.outputDir,
2759
+ allowedTestRoot: backendLayout.testRoot,
2276
2760
  });
2277
2761
  break;
2278
2762
  case "backend-test-result-v1":
@@ -2282,6 +2766,7 @@ export async function executeDagShellNode(input, meta) {
2282
2766
  artifactName: gate.artifactName,
2283
2767
  outputDir: gate.outputDir,
2284
2768
  junitRelativePath: gate.junitRelativePath,
2769
+ testRoot: backendLayout.testRoot,
2285
2770
  });
2286
2771
  break;
2287
2772
  case "backend-test-classification-v1":
@@ -2570,6 +3055,7 @@ export async function executeDagShellNode(input, meta) {
2570
3055
  const cwd = resolveShellCwd(input.cwd, shell.cwd);
2571
3056
  const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
2572
3057
  const failFast = shell.failFast !== false;
3058
+ const attemptNumber = input.attempt ?? 1;
2573
3059
  const results = [];
2574
3060
  const executedCommandKeys = new Set();
2575
3061
  let beforeStatus;
@@ -2665,8 +3151,8 @@ export async function executeDagShellNode(input, meta) {
2665
3151
  dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2666
3152
  reportActivity: input.reportActivity,
2667
3153
  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`),
3154
+ stdoutPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stdout.txt`),
3155
+ stderrPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stderr.txt`),
2670
3156
  },
2671
3157
  });
2672
3158
  results.push(result);
@@ -2725,6 +3211,7 @@ export async function executeDagShellNode(input, meta) {
2725
3211
  schemaVersion: 1,
2726
3212
  runId: meta.runId,
2727
3213
  nodeId: input.task.id,
3214
+ attempt: attemptNumber,
2728
3215
  workspaceFingerprint: workspaceFingerprint ?? null,
2729
3216
  failFast,
2730
3217
  commands: results.map((result) => ({
@@ -2743,6 +3230,7 @@ export async function executeDagShellNode(input, meta) {
2743
3230
  subchecks: parseCheckRepoSubcheckDurations(result),
2744
3231
  })),
2745
3232
  };
3233
+ await writeDagNodeJsonArtifact(meta.runDir, input.task.id, path.posix.join(`attempt-${attemptNumber}`, "shell-command-receipt.json"), receipt);
2746
3234
  await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "shell-command-receipt.json", receipt);
2747
3235
  const summaryMarkdown = buildShellResultSummaryMarkdown({
2748
3236
  nodeId: input.task.id,
@@ -2794,8 +3282,24 @@ export async function executeDagShellNode(input, meta) {
2794
3282
  stdout: `${recordPrefix}${reuseNote}${aggregate.stdout}`,
2795
3283
  stderr: stderrParts.filter(Boolean).join("\n\n"),
2796
3284
  failureCategory,
3285
+ ...(firstFailure?.failureCategory
3286
+ ? { rawFailureCategory: firstFailure.failureCategory }
3287
+ : {}),
2797
3288
  durationMs: Date.now() - started,
2798
3289
  // extra field consumed by node-execution if supported; also embedded in stdout marker
2799
3290
  ...{ commandResults },
2800
3291
  };
2801
3292
  }
3293
+ export async function executeDagShellNode(input, meta) {
3294
+ const handler = namedShellHandler(input.task.shell);
3295
+ const result = await executeDagShellNodeInner(input, meta);
3296
+ if (handler) {
3297
+ await ensureShellPresetProcessReceipt({
3298
+ runDir: meta.runDir,
3299
+ nodeId: input.task.id,
3300
+ preset: handler,
3301
+ result,
3302
+ });
3303
+ }
3304
+ return result;
3305
+ }