@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.10

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 (331) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +248 -95
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +12 -0
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +23 -5
  9. package/dist/cli/program.js +21 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli/update/runtime-activity.js +1 -29
  12. package/dist/cli.js +2 -1
  13. package/dist/commands/client-recovery.js +3 -0
  14. package/dist/commands/dag-follow-up.js +138 -0
  15. package/dist/commands/dag-rerun.js +55 -1
  16. package/dist/commands/init-model-catalog.js +464 -0
  17. package/dist/commands/init-upgrade.js +265 -97
  18. package/dist/commands/init.js +455 -28
  19. package/dist/commands/inspect-next.js +8 -0
  20. package/dist/commands/task-advance.js +19 -0
  21. package/dist/executors/dag-pi-executor.js +2306 -61
  22. package/dist/executors/pi-executor.js +7 -0
  23. package/dist/executors/pi-extension-resolver.js +233 -0
  24. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  25. package/dist/executors/pi-read-budget-policy.js +239 -0
  26. package/dist/executors/pi-sdk-executor.js +180 -33
  27. package/dist/executors/shell-executor.js +1249 -224
  28. package/dist/executors/shell-write-guard.js +14 -0
  29. package/dist/executors/workspace-write-snapshot.js +68 -0
  30. package/dist/infrastructure/harness/atomic-write.js +23 -0
  31. package/dist/shared/dag-failure-category.js +150 -0
  32. package/dist/shared/dag-prompt-override.js +27 -0
  33. package/dist/shared/openspec-spec.js +70 -4
  34. package/dist/shared/operator/capabilities.js +39 -0
  35. package/dist/shared/playwright-cli-command-policy.js +15 -0
  36. package/dist/shared/update/console-notifier.js +100 -0
  37. package/dist/{cli → shared}/update/npm-client.js +70 -15
  38. package/dist/{cli → shared}/update/state.js +41 -13
  39. package/dist/task/config-types.js +50 -5
  40. package/dist/task/contract/apply.js +11 -1
  41. package/dist/task/contract/constants.js +2 -0
  42. package/dist/task/contract/hash.js +3 -0
  43. package/dist/task/contract/observe.js +25 -4
  44. package/dist/task/contract/paths.js +2 -1
  45. package/dist/task/contract/project.js +18 -1
  46. package/dist/task/contract/schema.js +4 -1
  47. package/dist/task/contract/transaction.js +12 -0
  48. package/dist/task/frontend-project-capability.js +199 -18
  49. package/dist/task/source-prepare/completeness.js +188 -0
  50. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  51. package/dist/task/source-prepare/index.js +3 -0
  52. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  53. package/dist/task/source-prepare/ledger-review.js +214 -0
  54. package/dist/task/source-prepare/ledger.js +545 -0
  55. package/dist/task/source-prepare/parse-intent.js +24 -4
  56. package/dist/task/source-prepare/prepare.js +298 -1
  57. package/dist/task/source-prepare/semantic-intake.js +25 -5
  58. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  59. package/dist/task/source-prepare/types.js +22 -0
  60. package/dist/task/source-references.js +22 -1
  61. package/dist/worker/cli.js +18 -2
  62. package/dist/worker/console/chat/chat-event-store.js +62 -4
  63. package/dist/worker/console/chat/context-panel.js +7 -0
  64. package/dist/worker/console/chat/deferred-turn.js +12 -0
  65. package/dist/worker/console/chat/operation-card.js +8 -1
  66. package/dist/worker/console/chat/pi-console-config.js +81 -10
  67. package/dist/worker/console/chat/pi-runtime.js +264 -16
  68. package/dist/worker/console/chat/provider-error.js +98 -0
  69. package/dist/worker/console/chat/routes.js +298 -32
  70. package/dist/worker/console/chat/session-stats.js +179 -0
  71. package/dist/worker/console/chat/session-store.js +93 -63
  72. package/dist/worker/console/chat/todos.js +115 -0
  73. package/dist/worker/console/chat/tool-preview.js +42 -0
  74. package/dist/worker/console/chat/turn-process.js +20 -55
  75. package/dist/worker/console/chat/user-questions.js +266 -0
  76. package/dist/worker/console/chat/workspace-landing.js +14 -0
  77. package/dist/worker/console/console-handoff.js +82 -0
  78. package/dist/worker/console/console-update-and-init.js +115 -0
  79. package/dist/worker/console/console-update-runtime.js +84 -0
  80. package/dist/worker/console/dag-execution-receipt.js +33 -0
  81. package/dist/worker/console/draft-store.js +49 -0
  82. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  83. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  84. package/dist/worker/console/index.js +3 -0
  85. package/dist/worker/console/inspect-split.js +18 -0
  86. package/dist/worker/console/native-directory-picker.js +13 -0
  87. package/dist/worker/console/operation-run-facts.js +19 -3
  88. package/dist/worker/console/operation-store.js +227 -7
  89. package/dist/worker/console/operator-actions.js +388 -0
  90. package/dist/worker/console/operator-surface-health.js +1 -0
  91. package/dist/worker/console/operator-user-error.js +2 -2
  92. package/dist/worker/console/prd-intake-bridge.js +13 -0
  93. package/dist/worker/console/routes.js +355 -3
  94. package/dist/worker/console/security.js +67 -13
  95. package/dist/worker/console/server.js +609 -164
  96. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-BAsjg7ew.js} +1 -1
  97. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-CyIlrwZw.js} +1 -1
  98. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-Bo4tobhH.js} +1 -1
  99. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dfpow9gu.js} +1 -1
  100. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-DPmzG_B4.js} +1 -1
  101. package/dist/worker/console/static/assets/channel-DSrJAn0-.js +1 -0
  102. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-pKQpB1UW.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-5n9YYquZ.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DP3CtA2a.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DYxMJPUy.js} +1 -1
  106. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-CS3D50Io.js} +1 -1
  107. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-gSdDfo5_.js} +1 -1
  108. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-A48q0ZNt.js} +1 -1
  109. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-ow8Mm3DY.js} +1 -1
  110. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-CMKfLWYE.js} +1 -1
  111. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BiTBdQPX.js} +1 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DeMmPl0_.js +1 -0
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DeMmPl0_.js +1 -0
  114. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-Cm7exRz_.js} +1 -1
  115. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-D9O5qGOu.js} +1 -1
  116. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-CkvQnako.js} +1 -1
  117. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-B1rzp7CB.js} +1 -1
  118. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BP07Ipcc.js} +1 -1
  119. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BlPzweQb.js} +1 -1
  120. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-CxPa7n0S.js} +1 -1
  121. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CMTZs53A.js} +1 -1
  122. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-CxG_X52r.js} +1 -1
  123. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D8axtJM0.js} +1 -1
  124. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DIsGzn7b.js} +1 -1
  125. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CU9zkDG0.js} +1 -1
  126. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-DCjTmz5P.js} +1 -1
  127. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BksdmFzg.js} +1 -1
  128. package/dist/worker/console/static/assets/index-B-O5ChYq.css +1 -0
  129. package/dist/worker/console/static/assets/index-CfnY8AQx.js +407 -0
  130. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-CGzzzJtL.js} +1 -1
  131. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BdseB80N.js} +1 -1
  132. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-DM9gsgha.js} +1 -1
  133. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-3q-_9R2b.js} +1 -1
  134. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-B8N-Mxsd.js} +1 -1
  135. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-EPIHUUsu.js} +5 -5
  136. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CxwO12aa.js} +1 -1
  137. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-bPqbVX6Y.js} +1 -1
  138. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-KzqIvzeX.js} +1 -1
  139. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-CyofpWiV.js} +1 -1
  140. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-cTWJvLTP.js} +1 -1
  141. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-BK--q27B.js} +1 -1
  142. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-C0SDOHXK.js} +1 -1
  143. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-Cong6YIo.js} +1 -1
  144. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DUVkgZx8.js} +1 -1
  145. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C7XgxuZg.js} +1 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dzbnd5oX.js +1 -0
  147. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-D9Ayvwcy.js} +2 -2
  148. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BY0hHk6G.js +8 -0
  149. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-Byb000cH.js} +1 -1
  150. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DMLegPt4.js} +1 -1
  151. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-ChMMeOsP.js} +1 -1
  152. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-bEMEPisW.js} +1 -1
  153. package/dist/worker/console/static/index.html +2 -2
  154. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  155. package/dist/worker/console/static-src/app/console-types.js +68 -4
  156. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  157. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  158. package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
  159. package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
  160. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  161. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  162. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  163. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  164. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  165. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  166. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  167. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  168. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  169. package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
  170. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  171. package/dist/worker/console/static-src/operator-chat/useChatStream.js +102 -21
  172. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  173. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  174. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  175. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  176. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  177. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  178. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  179. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  180. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  181. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  182. package/dist/worker/console/workspace-context.js +344 -0
  183. package/dist/worker/console/workspace-registry.js +214 -0
  184. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  185. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  186. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  187. package/dist/worker/observability/init-runtime-activity.js +26 -0
  188. package/dist/worker/observability/read-model.js +22 -0
  189. package/dist/worker/observe/node-input.js +160 -3
  190. package/dist/worker/observe/node-process.js +377 -0
  191. package/dist/worker/observe/routes.js +224 -3
  192. package/dist/worker/observe/server.js +4 -0
  193. package/dist/worker/observe/shell-handler-keys.js +34 -0
  194. package/dist/worker/observe/static/api.js +90 -4
  195. package/dist/worker/observe/static/app.js +18 -70
  196. package/dist/worker/observe/static/constants.js +8 -1
  197. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  198. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  199. package/dist/worker/observe/static/dag-node-purpose.js +208 -0
  200. package/dist/worker/observe/static/format.js +60 -0
  201. package/dist/worker/observe/static/index.html +6 -1
  202. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  203. package/dist/worker/observe/static/markdown-render.js +20 -1
  204. package/dist/worker/observe/static/operator-chrome.css +145 -20
  205. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  206. package/dist/worker/observe/static/operator-chrome.js +330 -96
  207. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  208. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  209. package/dist/worker/observe/static/router.d.ts +46 -0
  210. package/dist/worker/observe/static/router.js +25 -1
  211. package/dist/worker/observe/static/state.d.ts +50 -0
  212. package/dist/worker/observe/static/state.js +184 -1
  213. package/dist/worker/observe/static/styles.css +395 -0
  214. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  215. package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
  216. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  217. package/dist/worker/observe/static/views/dag.js +14 -5
  218. package/dist/worker/observe/static/views/dashboard.js +6 -4
  219. package/dist/worker/observe/static/views/failures.js +5 -2
  220. package/dist/worker/observe/static/views/night.js +2 -2
  221. package/dist/worker/observe/static/views/pool.js +1 -0
  222. package/dist/worker/observe/static/views/run.js +3 -1
  223. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  224. package/dist/worker/observe/static/views/task.js +56 -1
  225. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
  226. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  227. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  228. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  229. package/dist/workflows/dag/contract-output-registry.js +15 -0
  230. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  231. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  232. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  233. package/dist/workflows/dag/failure-category.js +7 -116
  234. package/dist/workflows/dag/frontend-closeout.js +221 -0
  235. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  236. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  237. package/dist/workflows/dag/frontend-implementation-contract.js +538 -166
  238. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  239. package/dist/workflows/dag/frontend-prewrite-gate.js +388 -392
  240. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  241. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  242. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  243. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  244. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  245. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  246. package/dist/workflows/dag/frontend-repair.js +7 -424
  247. package/dist/workflows/dag/frontend-review-context.js +288 -14
  248. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  249. package/dist/workflows/dag/frontend-risk.js +15 -2
  250. package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
  251. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  252. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  253. package/dist/workflows/dag/frontend-shape.js +427 -0
  254. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  255. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  256. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  257. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  258. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  259. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  260. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  261. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  262. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  263. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  264. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  265. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  266. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  267. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  268. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  269. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  270. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  271. package/dist/workflows/dag/frontend-writer-admission.js +272 -0
  272. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  273. package/dist/workflows/dag/init-hybrid.js +984 -613
  274. package/dist/workflows/dag/interrupt-request.js +10 -3
  275. package/dist/workflows/dag/lifecycle.js +3 -3
  276. package/dist/workflows/dag/node-execution.js +468 -58
  277. package/dist/workflows/dag/prompt.js +44 -19
  278. package/dist/workflows/dag/recovery-lease.js +80 -0
  279. package/dist/workflows/dag/report.js +37 -1
  280. package/dist/workflows/dag/rerun-feedback.js +124 -1
  281. package/dist/workflows/dag/rerun-plan.js +144 -5
  282. package/dist/workflows/dag/rerun-run.js +81 -6
  283. package/dist/workflows/dag/rerun-task.js +29 -0
  284. package/dist/workflows/dag/retry-policy.js +318 -8
  285. package/dist/workflows/dag/runner.js +429 -122
  286. package/dist/workflows/dag/scheduler.js +114 -16
  287. package/dist/workflows/dag/structured-output-repair.js +712 -0
  288. package/dist/workflows/dag/types.js +321 -31
  289. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  290. package/dist/workflows/dag/validate.js +92 -32
  291. package/docs/README.md +2 -0
  292. package/docs/architecture/evolution.md +45 -0
  293. package/docs/init-surface.manifest.json +9 -0
  294. package/docs/operations/README.md +1 -1
  295. package/docs/operations/local-development-environment.md +21 -0
  296. package/docs/templates/README.md +2 -1
  297. package/docs/templates/agent-dag-report.schema.json +8 -2
  298. package/docs/templates/agent-dag.schema.json +45 -1
  299. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  300. package/docs/templates/frontend-test-case-checklist.md +2 -2
  301. package/docs/templates/frontend-test-dag.json +8 -10
  302. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  303. package/docs/templates/init-managed-agents.md +13 -3
  304. package/docs/templates/spec-registry.schema.json +45 -0
  305. package/harness.json +3 -3
  306. package/package.json +7 -1
  307. package/scripts/next-info.mjs +356 -0
  308. package/scripts/next-publish-gate.mjs +238 -0
  309. package/scripts/release-source-binding.mjs +251 -0
  310. package/skills/codebase-scout/SKILL.md +1 -1
  311. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  312. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  313. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  314. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  315. package/skills/frontend-design-review/SKILL.md +20 -41
  316. package/skills/frontend-implementation/SKILL.md +3 -4
  317. package/skills/frontend-implementation/references/design-spec.md +15 -10
  318. package/skills/frontend-implementation/references/node-contracts.md +23 -13
  319. package/skills/frontend-review/SKILL.md +22 -14
  320. package/skills/frontend-review/references/review-findings.md +6 -7
  321. package/skills/loop-agent/references/command-reference.md +12 -4
  322. package/skills/loop-agent/references/hybrid-dag.md +8 -1
  323. package/skills/playwright-cli/SKILL.md +23 -1
  324. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  325. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  326. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  327. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  328. package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
  329. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  330. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  331. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -1,8 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
3
4
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
4
5
  import path from "node:path";
5
- import { writeDagNodeJsonArtifact, writeDagNodeTextArtifact, } from "../infrastructure/harness/artifact-store.js";
6
+ import { writeDagNodeJsonArtifact, writeDagNodeTextArtifact, writeDagRunJsonArtifact, } from "../infrastructure/harness/artifact-store.js";
6
7
  import { captureWorkspaceCheckpoint } from "../workflows/dag/workspace-checkpoint.js";
7
8
  import { truncateOutput } from "../shared/output-truncation.js";
8
9
  import { processTreeSpawnOptions, terminateProcessTree, } from "./process-tree.js";
@@ -14,20 +15,32 @@ import { buildRequirementCoverageGateShellCommand, expandShellPreset, buildVerdi
14
15
  import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend-test-analysis-contract.js";
15
16
  import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
16
17
  import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
18
+ import { assertFrontendSourceBindingFresh, buildContractFromCommittedFacts, deterministicSha256, frontendImplementationContractSchema, serializeDeterministicJson, sha256Hex, writeFrontendImplementationContractArtifact, } from "../workflows/dag/frontend-implementation-contract.js";
19
+ import { checkFrontendSourceFidelityLedger } from "../workflows/dag/frontend-source-fidelity-ledger.js";
17
20
  import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
21
+ import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
22
+ import { probeFrontendTestEnvironment } from "../workflows/dag/frontend-test-environment-probe.js";
18
23
  import { renderFrontendTestL5Report } from "../workflows/dag/frontend-test-l5-report.js";
19
24
  import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-case-checklist.js";
20
25
  import { materializeFrontendTestCaseManifest } from "../workflows/dag/frontend-test-case-manifest.js";
21
26
  import { renderFrontendTestHtmlReport } from "../workflows/dag/frontend-test-html-report.js";
22
27
  import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
23
- import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
24
- import { formatFrontendFailureAssessStdout, formatFrontendRepairContractStdout, runFrontendFailureAssessGate, runFrontendRepairContractGate, } from "../workflows/dag/frontend-repair.js";
25
- import { formatFrontendPrewriteGateStdout, runFrontendPrewriteGate, } from "../workflows/dag/frontend-prewrite-gate.js";
28
+ import { captureFrontendWorktreeBaseline, formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
29
+ import { readTypedEventStoreFromJsonl } from "../workflows/dag/frontend-typed-event-store.js";
30
+ import { classifyFrontendFailure, } from "../workflows/dag/frontend-repair.js";
31
+ const FRONTEND_CONTRACT_MAX_BYTES = 65536;
32
+ import { evaluateFrontendDesignPolicy, } from "../workflows/dag/frontend-design-policy.js";
33
+ import { deriveFrontendWriterAdmissionShell, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, frontendWriterAdmissionResultV1Schema, } from "../workflows/dag/frontend-writer-admission.js";
34
+ import { deriveFrontendIntegrationFacts, buildFrontendTestHandoff, frontendCloseoutFactsSchema, renderFrontendCloseout, } from "../workflows/dag/frontend-closeout.js";
35
+ import { buildFrontendHumanDecisionEnvelope, evaluateHighAdmissionReceipt, routeVerificationConfig, } from "../workflows/dag/frontend-human-decision.js";
36
+ import { classifyFailureOwner, classifyIssueCategoryRestartPhase, classifyRestartPhase, } from "../workflows/dag/frontend-recovery-capsule.js";
37
+ import { pathMatchesPattern } from "../shared/git-progress.js";
26
38
  import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from "../workflows/dag/frontend-review-context.js";
27
39
  import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
28
40
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
29
41
  import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
30
42
  import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
43
+ import { frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
31
44
  import { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
32
45
  import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
33
46
  import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
@@ -38,6 +51,7 @@ import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
38
51
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
39
52
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
40
53
  import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
54
+ import { captureWorkspaceWriteSnapshot, diffWorkspaceWriteSnapshots, } from "./workspace-write-snapshot.js";
41
55
  import { parseAndValidateFinalWriteSetApproval, } from "../workflows/dag/node-execution.js";
42
56
  import { buildShellProcessEnv } from "./shell-verification.js";
43
57
  import { readRunState } from "../workflows/dag/run-store.js";
@@ -121,6 +135,39 @@ function shouldReuseSuccessfulShellCommands(shell) {
121
135
  const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
122
136
  const SUMMARY_STDOUT_MAX = 4_000;
123
137
  const SUMMARY_STDERR_MAX = 2_000;
138
+ const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
139
+ const TEST_TIMEOUT_MARKERS = [
140
+ /Test timed out in \d+ms/i,
141
+ /Timeout of \d+ms exceeded/i,
142
+ ];
143
+ const DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS = [
144
+ /AssertionError/i,
145
+ /expected:?\s/i,
146
+ /received:?\s/i,
147
+ /Snapshot /i,
148
+ /toMatchSnapshot/i,
149
+ /SyntaxError/i,
150
+ /TS\d{4}/,
151
+ /error TS/i,
152
+ /eslint/i,
153
+ /compile error/i,
154
+ /Failed to compile/i,
155
+ ];
156
+ function isIdentifiedTestCommand(command) {
157
+ 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);
158
+ }
159
+ function classifyShellCommandFailure(input) {
160
+ if (input.processCategory !== "nonzero-exit")
161
+ return input.processCategory;
162
+ if (!isIdentifiedTestCommand(input.command))
163
+ return "nonzero-exit";
164
+ const sample = `${input.stdout}\n${input.stderr}`.slice(0, TEST_TIMEOUT_CLASSIFY_MAX_CHARS);
165
+ const hasTimeoutMarker = TEST_TIMEOUT_MARKERS.some((marker) => marker.test(sample));
166
+ if (!hasTimeoutMarker)
167
+ return "nonzero-exit";
168
+ const mixedDeterministicFailure = DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS.some((marker) => marker.test(sample));
169
+ return mixedDeterministicFailure ? "nonzero-exit" : "test-timeout";
170
+ }
124
171
  const NODE_STDOUT_MAX = 12_000;
125
172
  const NODE_STDERR_MAX = 8_000;
126
173
  const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
@@ -327,16 +374,21 @@ export async function executeShellCommand(input) {
327
374
  });
328
375
  });
329
376
  child.on("close", (code) => {
330
- let failureCategory = "nonzero-exit";
377
+ let processCategory = "nonzero-exit";
331
378
  if (timedOut) {
332
- failureCategory = "timeout";
379
+ processCategory = "timeout";
333
380
  }
334
381
  else if (code === 0) {
335
- failureCategory = "success";
382
+ processCategory = "success";
336
383
  }
337
384
  finish({
338
385
  exitCode: code,
339
- failureCategory,
386
+ failureCategory: classifyShellCommandFailure({
387
+ command: input.command,
388
+ processCategory,
389
+ stdout: formatBoundedOutput(stdout),
390
+ stderr: formatBoundedOutput(stderr),
391
+ }),
340
392
  ok: code === 0 && !timedOut,
341
393
  });
342
394
  });
@@ -874,6 +926,7 @@ async function executeBackendTestPipeline(input, meta) {
874
926
  effective = await materializeEffectiveBackendPytestCollection({
875
927
  workspaceRoot: input.cwd,
876
928
  initial,
929
+ layout: backendLayout,
877
930
  });
878
931
  }
879
932
  else if (initial.status === "REPAIRABLE") {
@@ -933,6 +986,7 @@ async function executeBackendTestPipeline(input, meta) {
933
986
  workspaceRoot: input.cwd,
934
987
  initial,
935
988
  final: finalFacts,
989
+ layout: backendLayout,
936
990
  });
937
991
  }
938
992
  else {
@@ -995,6 +1049,7 @@ async function executeBackendTestPipeline(input, meta) {
995
1049
  scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
996
1050
  eligibility,
997
1051
  eligibilityInputHashes,
1052
+ layout: backendLayout,
998
1053
  });
999
1054
  return {
1000
1055
  ok: true,
@@ -1018,7 +1073,7 @@ async function executeBackendTestPipeline(input, meta) {
1018
1073
  else if (pipeline === "markdown-traceability") {
1019
1074
  let report;
1020
1075
  try {
1021
- report = await validateBackendMarkdownTraceability(input.cwd);
1076
+ report = await validateBackendMarkdownTraceability(input.cwd, backendLayout);
1022
1077
  }
1023
1078
  catch (error) {
1024
1079
  report = `# Backend Test Traceability\n\n## Status\n\nFAIL\n\n## Findings\n\n- Traceability validation could not complete: ${error instanceof Error ? error.message : String(error)}\n`;
@@ -1103,7 +1158,7 @@ async function executeBackendTestPipeline(input, meta) {
1103
1158
  }
1104
1159
  else if (pipeline === "markdown-execute-html") {
1105
1160
  const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
1106
- await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
1161
+ await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness, backendLayout);
1107
1162
  const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
1108
1163
  let eligibleItemIds = executionReadiness.eligibleItemIds;
1109
1164
  // Plan D gap-fill executeScope=affected: when a gap plan exists in
@@ -1678,6 +1733,33 @@ async function executeBackendTestPipeline(input, meta) {
1678
1733
  }
1679
1734
  }
1680
1735
  async function executeBackendTestPipelineWithWriteGuard(input, meta) {
1736
+ if (meta.spec.backendTestWorkspaceControl === "filesystem-only") {
1737
+ const paths = input.task.writeSet ?? input.task.allowedPaths;
1738
+ let beforeSnapshot;
1739
+ try {
1740
+ beforeSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
1741
+ }
1742
+ catch (error) {
1743
+ return { ok: false, stdout: "", stderr: `workspace filesystem baseline unavailable: ${error instanceof Error ? error.message : String(error)}`, failureCategory: "write-guard", durationMs: 0 };
1744
+ }
1745
+ const result = await executeBackendTestPipeline(input, meta);
1746
+ try {
1747
+ const afterSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
1748
+ const guard = validateShellWriteGuard({
1749
+ changedPaths: diffWorkspaceWriteSnapshots(beforeSnapshot, afterSnapshot),
1750
+ writePolicy: input.task.writePolicy,
1751
+ writeSet: input.task.writeSet,
1752
+ allowedPaths: input.task.allowedPaths,
1753
+ forbiddenPaths: input.task.forbiddenPaths,
1754
+ });
1755
+ if (!guard.ok)
1756
+ return { ...result, ok: false, stderr: [result.stderr, `filesystem write guard failed: ${guard.violations.join(", ")}`].filter(Boolean).join("\n\n"), failureCategory: "write-guard" };
1757
+ return result;
1758
+ }
1759
+ catch (error) {
1760
+ return { ...result, ok: false, stderr: [result.stderr, `filesystem write guard failed: ${error instanceof Error ? error.message : String(error)}`].filter(Boolean).join("\n\n"), failureCategory: "write-guard" };
1761
+ }
1762
+ }
1681
1763
  let beforeStatus;
1682
1764
  try {
1683
1765
  beforeStatus = await readGitStatusPorcelain(input.cwd);
@@ -1740,6 +1822,45 @@ async function executePipelineCommands(input, meta, overrideCommands) {
1740
1822
  }
1741
1823
  return results;
1742
1824
  }
1825
+ /** A+B (AC-008): derive unresolved verification entrypoints from committed plan
1826
+ * facts. A verification target is unresolved when its commandLabel does not
1827
+ * match any frozen command label from the verify shell evidence bundle.
1828
+ * Returns `{ ref, digest }` entries; a missing/unreadable plan ledger fails
1829
+ * closed to [] so the router restarts the contract instead of inventing refs. */
1830
+ async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandLabels) {
1831
+ try {
1832
+ const { restorePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
1833
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
1834
+ const patch = restorePlanPatchFromCommittedFacts(records);
1835
+ const targets = patch
1836
+ ?.verificationTargets;
1837
+ if (!Array.isArray(targets))
1838
+ return [];
1839
+ const frozen = new Set(frozenCommandLabels
1840
+ .map((label) => label.trim().toLowerCase())
1841
+ .filter(Boolean));
1842
+ const refs = [];
1843
+ for (const item of targets) {
1844
+ const target = item;
1845
+ const commandLabel = typeof target.commandLabel === "string" ? target.commandLabel : "";
1846
+ if (commandLabel &&
1847
+ frozen.has(commandLabel.trim().toLowerCase())) {
1848
+ continue;
1849
+ }
1850
+ const file = typeof target.file === "string" ? target.file : "";
1851
+ const symbol = typeof target.symbol === "string" ? target.symbol : "";
1852
+ const id = typeof target.id === "string" ? target.id : "";
1853
+ const ref = file || symbol || id;
1854
+ if (!ref)
1855
+ continue;
1856
+ refs.push({ ref, digest: sha256Hex(ref) });
1857
+ }
1858
+ return refs;
1859
+ }
1860
+ catch {
1861
+ return [];
1862
+ }
1863
+ }
1743
1864
  async function executeFrontendVerificationBundle(input, meta) {
1744
1865
  const started = Date.now();
1745
1866
  const shell = input.task.shell;
@@ -1894,75 +2015,505 @@ async function executeFrontendVerificationBundle(input, meta) {
1894
2015
  catch (error) {
1895
2016
  traceError = error instanceof Error ? error : new Error(String(error));
1896
2017
  }
1897
- if (bundle.mode === "repair") {
1898
- if (firstFailure || lintSyntheticFailure || traceError) {
1899
- return {
1900
- ok: false,
1901
- stdout: summarizeCommandResults(results).stdout,
1902
- stderr: firstFailure?.stderr ||
1903
- lintSyntheticFailure?.stderr ||
1904
- traceError?.message ||
1905
- "frontend reverify failed",
1906
- failureCategory: firstFailure?.failureCategory ??
1907
- lintSyntheticFailure?.failureCategory ??
1908
- "invalid-output",
1909
- durationMs: Date.now() - started,
1910
- ...{ commandResults },
1911
- };
2018
+ const verifyFailed = firstFailure !== undefined || lintSyntheticFailure !== undefined || traceError !== undefined;
2019
+ if (verifyFailed) {
2020
+ // M8: verify failure is TERMINAL. There is no same-run repair branch and
2021
+ // no frontend-repair-assessment.json; we only materialize failure
2022
+ // ownership facts (frontend-repair classification + recovery capsule
2023
+ // ownership) for the operation controller / recovery run.
2024
+ const failedNodeId = input.task.id;
2025
+ const failureCategory = firstFailure?.failureCategory ??
2026
+ lintSyntheticFailure?.failureCategory ??
2027
+ (traceError ? "invalid-output" : undefined) ??
2028
+ "invalid-output";
2029
+ const failureClass = classifyFrontendFailure({
2030
+ nodeId: failedNodeId,
2031
+ failureCategory,
2032
+ stdout: firstFailure?.stdout,
2033
+ stderr: firstFailure?.stderr ?? traceError?.message,
2034
+ });
2035
+ // A verify failure means the writer's candidate failed verification, so
2036
+ // the recovery ownership maps to the implement phase (frontend-recovery
2037
+ // capsule classification, fail-closed to implement-pi).
2038
+ const classifiedOwner = classifyFailureOwner({
2039
+ failureSource: "frontend-verify-shell",
2040
+ nodeId: failedNodeId,
2041
+ rawFailureCategory: failureCategory,
2042
+ });
2043
+ const failureOwner = classifiedOwner === "unknown" ? "implement-pi" : classifiedOwner;
2044
+ const restartPhase = classifyRestartPhase(failureOwner);
2045
+ // A+B (AC-008): verification-config routing. Produce structured
2046
+ // unresolved entrypoint refs from committed plan facts and the frozen
2047
+ // command labels; a non-empty unresolved set routes to
2048
+ // verification-config-decision and materializes the frozen
2049
+ // verification-config envelope. An empty/untyped signal fails closed to
2050
+ // contract-restart. The decision is recorded so the recovery controller
2051
+ // never re-classifies the same signal twice.
2052
+ let verificationConfigRoute;
2053
+ if (failureOwner === "verification-config") {
2054
+ const frozenCommandLabels = [
2055
+ ...(bundle.staticEvidence.commandLabels ?? []),
2056
+ ...(bundle.behaviorEvidence.commandLabels ?? []),
2057
+ ...(bundle.mockEvidence?.commandLabels ?? []),
2058
+ ];
2059
+ const unresolvedEntrypointRefs = await deriveUnresolvedVerificationEntrypointRefs(meta.runDir, frozenCommandLabels);
2060
+ verificationConfigRoute = routeVerificationConfig({
2061
+ unresolvedEntrypointRefs: unresolvedEntrypointRefs.map((ref) => ref.ref),
2062
+ });
2063
+ const allowlistedResolvers = [
2064
+ ...new Set(frozenCommandLabels
2065
+ .map((label) => label.trim())
2066
+ .filter(Boolean)),
2067
+ ].sort();
2068
+ if (verificationConfigRoute.decision ===
2069
+ "verification-config-decision" &&
2070
+ unresolvedEntrypointRefs.length > 0 &&
2071
+ allowlistedResolvers.length > 0) {
2072
+ const envelope = buildFrontendHumanDecisionEnvelope({
2073
+ operationId: input.task.id,
2074
+ runId: meta.runId,
2075
+ operationRevision: 1,
2076
+ targetDigest: sha256Hex(frozenCommandLabels.slice().sort().join("\n")),
2077
+ staleOnTargetDigestChange: true,
2078
+ kind: "verification-config",
2079
+ resumeAction: "select-verification-config",
2080
+ payload: {
2081
+ unresolvedEntrypointRefs,
2082
+ allowlistedResolvers,
2083
+ },
2084
+ });
2085
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verification-config-envelope.json", envelope);
2086
+ }
1912
2087
  }
2088
+ const verifyFailure = {
2089
+ schemaVersion: 1,
2090
+ schemaId: "frontend-verify-failure-v1",
2091
+ nodeId: failedNodeId,
2092
+ failureCategory,
2093
+ failureClass,
2094
+ failureOwner,
2095
+ restartPhase,
2096
+ ...(verificationConfigRoute
2097
+ ? { verificationConfigRoute }
2098
+ : {}),
2099
+ commandResults,
2100
+ failedAt: new Date().toISOString(),
2101
+ };
2102
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verify-failure.json", verifyFailure);
1913
2103
  return {
1914
- ok: true,
1915
- stdout: lintAssessment?.status === "baseline-debt"
1916
- ? "Frontend reverify bundle: pass with lint baseline-debt"
1917
- : "Frontend reverify bundle: pass",
1918
- stderr: "",
1919
- failureCategory: "success",
2104
+ ok: false,
2105
+ stdout: summarizeCommandResults(results).stdout,
2106
+ stderr: firstFailure?.stderr ||
2107
+ lintSyntheticFailure?.stderr ||
2108
+ traceError?.message ||
2109
+ "frontend verification failed",
2110
+ failureCategory,
1920
2111
  durationMs: Date.now() - started,
1921
2112
  ...{ commandResults },
1922
2113
  };
1923
2114
  }
1924
- const failureFacts = [];
1925
- if (firstFailure || lintSyntheticFailure) {
1926
- const failureStdout = firstFailure
1927
- ? [firstFailure.command, firstFailure.stdout].filter(Boolean).join("\n")
1928
- : "";
1929
- failureFacts.push({
1930
- nodeId: input.task.id,
1931
- record: {
1932
- status: "FINISHED",
1933
- failureCategory: firstFailure?.failureCategory ??
1934
- lintSyntheticFailure?.failureCategory,
1935
- // Keep classification scoped to the failed command. Aggregate
1936
- // successful output may contain unrelated writeSet-like JSON.
1937
- stdout: failureStdout,
1938
- stderr: firstFailure?.stderr ?? lintSyntheticFailure?.stderr,
1939
- commandResults,
1940
- },
1941
- });
2115
+ // A+B (AC-008): when lint is `require-clean` but the assessment proves
2116
+ // baseline-debt, materialize the frozen `baseline-debt-acceptance` envelope
2117
+ // whose only positive resume is `accept-baseline-debt`. Evidence stays
2118
+ // `baseline-debt` and is never rewritten to `passed`.
2119
+ if (lintAssessment?.status === "baseline-debt") {
2120
+ const baselineEvidenceRefs = (lintAssessment.rawEvidenceRefs ?? [])
2121
+ .filter((ref) => ref?.path && ref?.sha256)
2122
+ .map((ref) => ({ ref: ref.path, digest: ref.sha256 }));
2123
+ if (baselineEvidenceRefs.length > 0) {
2124
+ const envelope = buildFrontendHumanDecisionEnvelope({
2125
+ operationId: input.task.id,
2126
+ runId: meta.runId,
2127
+ operationRevision: 1,
2128
+ targetDigest: lintAssessment.commandIdentity.sha256,
2129
+ staleOnTargetDigestChange: true,
2130
+ kind: "baseline-debt-acceptance",
2131
+ resumeAction: "accept-baseline-debt",
2132
+ payload: {
2133
+ baselineEvidenceRefs,
2134
+ candidateDigest: lintAssessment.commandIdentity.sha256,
2135
+ commandDigest: lintAssessment.commandIdentity.sha256,
2136
+ resultingPolicy: "allow-non-regression",
2137
+ },
2138
+ });
2139
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-baseline-debt-acceptance-envelope.json", envelope);
2140
+ }
1942
2141
  }
1943
- if (traceError) {
1944
- failureFacts.push({
1945
- nodeId: "frontend-verification-trace-shell",
1946
- record: {
1947
- status: "FINISHED",
1948
- failureCategory: "invalid-output",
1949
- stderr: traceError.message,
1950
- },
1951
- });
2142
+ return {
2143
+ ok: true,
2144
+ stdout: lintAssessment?.status === "baseline-debt"
2145
+ ? "Frontend verification bundle: pass with lint baseline-debt"
2146
+ : "Frontend verification bundle: pass",
2147
+ stderr: "",
2148
+ failureCategory: "success",
2149
+ durationMs: Date.now() - started,
2150
+ ...{ commandResults },
2151
+ };
2152
+ }
2153
+ /* ------------------------------------------------------------------ *
2154
+ * M8 deterministic frontend shell nodes: design policy, writer *
2155
+ * admission, closeout. These replace the prewrite-gate authorization *
2156
+ * path and the Pi closeout node. *
2157
+ * ------------------------------------------------------------------ */
2158
+ async function readRunDirJson(runDir, relativePath) {
2159
+ return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
2160
+ }
2161
+ /** Committed typed review terminal kinds from the review node's typed event
2162
+ * store (AC-003: typed terminal tools are the only authoritative verdict). */
2163
+ async function collectReviewTerminalKinds(runDir, reviewNodeId) {
2164
+ try {
2165
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
2166
+ return records
2167
+ .filter((record) => record.phase === "committed")
2168
+ .map((record) => {
2169
+ const kind = record.fact.kind;
2170
+ return typeof kind === "string" ? kind : "";
2171
+ })
2172
+ .filter(Boolean);
2173
+ }
2174
+ catch {
2175
+ return [];
2176
+ }
2177
+ }
2178
+ /** Committed typed design terminal kinds from the design review node's typed
2179
+ * event store (M8: the committed approve_design / request_design_changes fact
2180
+ * is the only authoritative verdict; the legacy first-line VERDICT text is
2181
+ * never read for writer admission). Fail-closed on a missing/unreadable store. */
2182
+ async function collectDesignTerminalKinds(runDir, designNodeId) {
2183
+ try {
2184
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2185
+ return records
2186
+ .filter((record) => record.phase === "committed")
2187
+ .map((record) => {
2188
+ const kind = record.fact.kind;
2189
+ return typeof kind === "string" ? kind : "";
2190
+ })
2191
+ .filter(Boolean);
2192
+ }
2193
+ catch {
2194
+ return [];
2195
+ }
2196
+ }
2197
+ /** A+B (AC-009): deterministic §4.10 restart phase for a committed
2198
+ * `request_review_changes` fact. The typed issueCategory is mapped through the
2199
+ * frozen classifier; `unknown` (or an unreadable store) fails closed to
2200
+ * `undefined` so the runtime never trusts an untyped category. */
2201
+ async function collectReviewRestartPhase(runDir, reviewNodeId) {
2202
+ try {
2203
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
2204
+ const request = records.find((record) => record.phase === "committed" &&
2205
+ record.fact.kind === "request_review_changes");
2206
+ const issueCategory = request?.fact
2207
+ ?.issueCategory;
2208
+ return typeof issueCategory === "string"
2209
+ ? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
2210
+ : undefined;
2211
+ }
2212
+ catch {
2213
+ return undefined;
2214
+ }
2215
+ }
2216
+ /** A+B (AC-009): deterministic §4.10 restart phase for a committed
2217
+ * `request_design_changes` fact. Mirrors the review collector. */
2218
+ async function collectDesignRestartPhase(runDir, designNodeId) {
2219
+ try {
2220
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2221
+ const request = records.find((record) => record.phase === "committed" &&
2222
+ record.fact.kind === "request_design_changes");
2223
+ const issueCategory = request?.fact
2224
+ ?.issueCategory;
2225
+ return typeof issueCategory === "string"
2226
+ ? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
2227
+ : undefined;
2228
+ }
2229
+ catch {
2230
+ return undefined;
2231
+ }
2232
+ }
2233
+ /**
2234
+ * Resolve the deterministic runtime skeleton for the frontend plan patch from
2235
+ * the DAG spec. The plan task carries it via `structuredContractOutput.skeleton`
2236
+ * (the only schema-recognized persistent channel); absence is a fail-closed
2237
+ * generation/configuration defect, never a reason to fall back to text.
2238
+ */
2239
+ function resolveFrontendContractSkeleton(spec, planNodeId) {
2240
+ const planTask = spec.tasks.find((task) => task.id === planNodeId);
2241
+ const skeleton = planTask?.structuredContractOutput?.skeleton;
2242
+ if (!skeleton || Object.keys(skeleton).length === 0) {
2243
+ throw new Error(`frontend design policy blocked: missing runtime contract skeleton for ${planNodeId}`);
2244
+ }
2245
+ return skeleton;
2246
+ }
2247
+ async function collectFrontendSourceFreshness(input) {
2248
+ const entries = [];
2249
+ for (const source of input.sourceBinding?.sources ?? []) {
2250
+ entries.push({ path: source.path, sha256: source.sha256, fresh: true });
2251
+ }
2252
+ for (const evidencePath of input.contract.designEvidence.paths) {
2253
+ if (entries.some((entry) => entry.path === evidencePath))
2254
+ continue;
2255
+ try {
2256
+ const bytes = await readFile(path.join(input.workspaceRoot, evidencePath));
2257
+ entries.push({
2258
+ path: evidencePath,
2259
+ sha256: createHash("sha256").update(bytes).digest("hex"),
2260
+ fresh: true,
2261
+ });
2262
+ }
2263
+ catch {
2264
+ entries.push({
2265
+ path: evidencePath,
2266
+ sha256: "0".repeat(64),
2267
+ fresh: false,
2268
+ });
2269
+ }
2270
+ }
2271
+ return entries;
2272
+ }
2273
+ /** R3/R4 openspec invariants: frozen candidates must exist and content
2274
+ * snapshots must not have drifted. Citation is enforced upstream by the
2275
+ * prewrite gate read-event evidence and ledger specReference conformance. */
2276
+ async function checkFrontendOpenspecFacts(input) {
2277
+ const frozenCandidatePaths = input.config.openspecCandidatePaths ?? [];
2278
+ const candidateRoot = input.workspaceRoot;
2279
+ const missingCandidates = [];
2280
+ for (const candidate of frozenCandidatePaths) {
2281
+ try {
2282
+ await stat(path.join(candidateRoot, candidate));
2283
+ }
2284
+ catch {
2285
+ missingCandidates.push(candidate);
2286
+ }
2287
+ }
2288
+ if (missingCandidates.length > 0) {
2289
+ throw new Error(`frontend design policy blocked: openspec candidate paths frozen at generation time no longer exist on disk: ${missingCandidates.join(", ")}`);
2290
+ }
2291
+ const snapshots = input.config.openspecCandidateSnapshots;
2292
+ if (snapshots && snapshots.length > 0) {
2293
+ for (const snapshot of snapshots) {
2294
+ try {
2295
+ const bytes = await readFile(path.join(candidateRoot, snapshot.path));
2296
+ const actual = createHash("sha256").update(bytes).digest("hex");
2297
+ if (actual !== snapshot.sha256) {
2298
+ throw new Error(`frontend design policy blocked: openspec candidate content hash drifted: ${snapshot.path}`);
2299
+ }
2300
+ }
2301
+ catch (error) {
2302
+ if (error instanceof Error &&
2303
+ error.message.startsWith("frontend design policy blocked")) {
2304
+ throw error;
2305
+ }
2306
+ throw new Error(`frontend design policy blocked: openspec candidate unreadable: ${snapshot.path}`);
2307
+ }
2308
+ }
1952
2309
  }
2310
+ }
2311
+ async function executeFrontendDesignPolicy(input, meta) {
2312
+ const started = Date.now();
2313
+ const shell = input.task.shell;
2314
+ const config = shell.frontendDesignPolicy;
1953
2315
  try {
1954
- const assessment = await runFrontendFailureAssessGate({
2316
+ // 1. Materialize the canonical contract from committed typed facts. After
2317
+ // M9a the plan node's assistantText is a pure Markdown narrative; the
2318
+ // compile authority is the committed plan ledger ⊕ runtime skeleton.
2319
+ // Fail closed on missing committed facts — never fall back to text.
2320
+ const { readCommittedOriginFacts, checkCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
2321
+ // A+B (AC-001/003): fail-closed committed contract/scout typed facts.
2322
+ // Missing/uncommitted facts block here; never fall back to text parsing.
2323
+ const committedContractFacts = await readCommittedOriginFacts(meta.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
2324
+ const contractCheck = checkCommittedOriginFacts({
2325
+ records: committedContractFacts,
2326
+ origin: "contract",
2327
+ });
2328
+ if (!contractCheck.ok) {
2329
+ return {
2330
+ ok: false,
2331
+ stdout: "",
2332
+ stderr: `frontend design policy blocked: ${contractCheck.reason}`,
2333
+ failureCategory: "invalid-output",
2334
+ durationMs: Date.now() - started,
2335
+ };
2336
+ }
2337
+ const committedScoutFacts = await readCommittedOriginFacts(meta.runDir, "frontend-scout-pi", "scout-typed-facts.jsonl");
2338
+ const scoutCheck = checkCommittedOriginFacts({
2339
+ records: committedScoutFacts,
2340
+ origin: "scout",
2341
+ });
2342
+ if (!scoutCheck.ok) {
2343
+ return {
2344
+ ok: false,
2345
+ stdout: "",
2346
+ stderr: `frontend design policy blocked: ${scoutCheck.reason}`,
2347
+ failureCategory: "invalid-output",
2348
+ durationMs: Date.now() - started,
2349
+ };
2350
+ }
2351
+ const skeleton = resolveFrontendContractSkeleton(meta.spec, config.planFromNodeId);
2352
+ const assembled = await buildContractFromCommittedFacts({
2353
+ runDir: meta.runDir,
2354
+ planNodeId: config.planFromNodeId,
2355
+ contractNodeId: "frontend-contract-pi",
2356
+ sourceBinding: meta.spec.sourceBinding,
2357
+ skeleton,
2358
+ });
2359
+ if (!assembled.ok) {
2360
+ return {
2361
+ ok: false,
2362
+ stdout: "",
2363
+ stderr: `frontend design policy blocked: ${assembled.reason}`,
2364
+ failureCategory: "invalid-output",
2365
+ durationMs: Date.now() - started,
2366
+ };
2367
+ }
2368
+ const contract = assembled.canonical;
2369
+ const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(contract), "utf8");
2370
+ if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
2371
+ return {
2372
+ ok: false,
2373
+ stdout: "",
2374
+ stderr: `frontend design policy blocked: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit`,
2375
+ failureCategory: "invalid-output",
2376
+ durationMs: Date.now() - started,
2377
+ };
2378
+ }
2379
+ // 2. Requirement id retention (R11): search the canonical contract
2380
+ // serialization instead of the plan node narrative.
2381
+ const contractText = serializeDeterministicJson(contract);
2382
+ const missingIds = config.requiredRequirementIds.filter((id) => !contractText.includes(id));
2383
+ if (missingIds.length > 0) {
2384
+ return {
2385
+ ok: false,
2386
+ stdout: "",
2387
+ stderr: `frontend design policy blocked: missing requirement ids: ${missingIds.join(", ")}`,
2388
+ failureCategory: "invalid-output",
2389
+ durationMs: Date.now() - started,
2390
+ };
2391
+ }
2392
+ // 3. Mock strategy allowed + frozen command binding (R1).
2393
+ const mockStrategy = contract.mockApi.strategy;
2394
+ if (!config.allowedMockStrategies.includes(mockStrategy)) {
2395
+ return {
2396
+ ok: false,
2397
+ stdout: "",
2398
+ stderr: `frontend contract mock strategy "${mockStrategy}" is incompatible with the generated allowed strategies [${config.allowedMockStrategies.join(", ")}]`,
2399
+ failureCategory: "invalid-output",
2400
+ durationMs: Date.now() - started,
2401
+ };
2402
+ }
2403
+ if (mockStrategy !== "not-needed" && config.mockCommandLabels.length === 0) {
2404
+ return {
2405
+ ok: false,
2406
+ stdout: "",
2407
+ stderr: `frontend design policy blocked: contract selected Mock strategy ${mockStrategy} but the DAG has no authorized Mock verification commands`,
2408
+ failureCategory: "invalid-output",
2409
+ durationMs: Date.now() - started,
2410
+ };
2411
+ }
2412
+ // 4. writeSet containment.
2413
+ if (config.implementationWriteSet) {
2414
+ const writeSet = new Set(config.implementationWriteSet);
2415
+ const uncoveredTargets = contract.targets.files.filter((target) => ![...writeSet].some((pattern) => pathMatchesPattern(target, pattern) || target === pattern));
2416
+ if (uncoveredTargets.length > 0) {
2417
+ return {
2418
+ ok: false,
2419
+ stdout: "",
2420
+ stderr: `frontend design policy blocked: contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`,
2421
+ failureCategory: "invalid-output",
2422
+ durationMs: Date.now() - started,
2423
+ };
2424
+ }
2425
+ const uncoveredVTs = contract.verificationTargets
2426
+ .filter((vt) => vt.type !== "static")
2427
+ .filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
2428
+ if (uncoveredVTs.length > 0) {
2429
+ return {
2430
+ ok: false,
2431
+ stdout: "",
2432
+ stderr: `frontend design policy blocked: verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`,
2433
+ failureCategory: "invalid-output",
2434
+ durationMs: Date.now() - started,
2435
+ };
2436
+ }
2437
+ }
2438
+ // 5. Source freshness (R4) + freshness entries.
2439
+ if (config.requireSourceFreshness) {
2440
+ if (!meta.spec.sourceBinding) {
2441
+ throw new Error("frontend design policy requires sourceBinding for freshness check");
2442
+ }
2443
+ await assertFrontendSourceBindingFresh({
2444
+ workspaceRoot: input.cwd,
2445
+ binding: meta.spec.sourceBinding,
2446
+ });
2447
+ }
2448
+ // Byte freshness is necessary but not sufficient: require the same ledger
2449
+ // semantic checks as the shadow prewrite gate before production writer
2450
+ // admission can be materialized.
2451
+ if (meta.spec.sourceBinding) {
2452
+ const ledgerCheck = await checkFrontendSourceFidelityLedger({
2453
+ workspaceRoot: input.cwd,
2454
+ binding: meta.spec.sourceBinding,
2455
+ contractRequirements: contract.requirements.map((requirement) => ({
2456
+ id: requirement.id,
2457
+ sourceFragmentIds: requirement.sourceFragmentIds,
2458
+ sourceRefs: requirement.sourceRefs,
2459
+ })),
2460
+ });
2461
+ if (!ledgerCheck.ok) {
2462
+ return {
2463
+ ok: false,
2464
+ stdout: "",
2465
+ stderr: `frontend design policy blocked: ${ledgerCheck.failureReason ?? "source fidelity ledger validation failed"}`,
2466
+ failureCategory: "invalid-output",
2467
+ durationMs: Date.now() - started,
2468
+ };
2469
+ }
2470
+ }
2471
+ const sourceFreshness = await collectFrontendSourceFreshness({
2472
+ workspaceRoot: input.cwd,
2473
+ sourceBinding: meta.spec.sourceBinding,
2474
+ contract,
2475
+ });
2476
+ // 6. Openspec invariants (R3): frozen candidates exist and snapshots have
2477
+ // not drifted. Citation is superseded by prewrite-gate read evidence.
2478
+ await checkFrontendOpenspecFacts({
2479
+ workspaceRoot: input.cwd,
2480
+ config,
2481
+ });
2482
+ // 7. Deterministic design policy (M8 D2: designApproval is optional here;
2483
+ // the design verdict is enforced by the writer-admission shell).
2484
+ const policyResult = evaluateFrontendDesignPolicy({
2485
+ contract,
2486
+ allowedMockStrategies: config.allowedMockStrategies,
2487
+ sourceFreshness,
2488
+ componentSpecCandidatePaths: config.openspecCandidatePaths ??
2489
+ config.componentSpecCandidatePaths ??
2490
+ [],
2491
+ allowedDependencies: config.allowedDependencies,
2492
+ writeSetPatterns: config.implementationWriteSet ?? [],
2493
+ });
2494
+ // 8. Write canonical contract + policy result.
2495
+ await writeFrontendImplementationContractArtifact({
1955
2496
  runDir: meta.runDir,
1956
- failureFacts,
2497
+ outputDir: config.outputDir,
2498
+ artifactName: config.artifactName,
2499
+ canonical: contract,
1957
2500
  });
1958
- await runFrontendRepairContractGate({ runDir: meta.runDir });
2501
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-design-policy-result.json", policyResult);
2502
+ if (policyResult.classification === "blocked") {
2503
+ return {
2504
+ ok: false,
2505
+ stdout: "",
2506
+ stderr: `frontend design policy blocked: ${policyResult.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
2507
+ failureCategory: "invalid-output",
2508
+ durationMs: Date.now() - started,
2509
+ };
2510
+ }
1959
2511
  return {
1960
2512
  ok: true,
1961
- stdout: formatFrontendFailureAssessStdout(assessment),
2513
+ stdout: `Frontend design policy: approved findings=0\nContract: ${path.resolve(meta.runDir, config.outputDir, config.artifactName)}`,
1962
2514
  stderr: "",
1963
2515
  failureCategory: "success",
1964
2516
  durationMs: Date.now() - started,
1965
- ...{ commandResults },
1966
2517
  };
1967
2518
  }
1968
2519
  catch (error) {
@@ -1972,30 +2523,163 @@ async function executeFrontendVerificationBundle(input, meta) {
1972
2523
  stderr: error instanceof Error ? error.message : String(error),
1973
2524
  failureCategory: "invalid-output",
1974
2525
  durationMs: Date.now() - started,
1975
- ...{ commandResults },
1976
2526
  };
1977
2527
  }
1978
2528
  }
1979
- async function executeFrontendTestCaseChecklist(input, meta) {
2529
+ async function executeFrontendWriterAdmission(input, meta) {
1980
2530
  const started = Date.now();
2531
+ const shell = input.task.shell;
2532
+ const config = shell.frontendWriterAdmission;
1981
2533
  try {
1982
- const declaredAcIds = (meta.spec.sourceBinding?.requirementIds ?? []).filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
1983
- const result = await validateFrontendCaseChecklist({
2534
+ // 1. Design verdict pass is a hard admission condition (M8 D2). In small
2535
+ // topology the design review node is absent and the check is vacuous.
2536
+ // The verdict is read from the committed typed design terminal fact, not
2537
+ // from the legacy first-line VERDICT text protocol (AC-003/AC-004).
2538
+ let verdictPass = true;
2539
+ let designRestartPhase;
2540
+ if (config.designReviewFromNodeId) {
2541
+ const designKinds = await collectDesignTerminalKinds(meta.runDir, config.designReviewFromNodeId);
2542
+ verdictPass =
2543
+ designKinds.length === 1 && designKinds[0] === "approve_design";
2544
+ // A+B (AC-009): request_design_changes issueCategory maps to the
2545
+ // deterministic §4.10 restart phase; unknown fails closed to undefined.
2546
+ designRestartPhase = verdictPass
2547
+ ? undefined
2548
+ : await collectDesignRestartPhase(meta.runDir, config.designReviewFromNodeId);
2549
+ }
2550
+ // 2. Read the canonical contract + design-policy result.
2551
+ const contractRaw = await readRunDirJson(meta.runDir, "contracts/frontend-implementation-contract.json");
2552
+ const contract = frontendImplementationContractSchema.parse(contractRaw);
2553
+ const policyRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-design-policy-result.json"));
2554
+ if (policyRaw.schemaId !== "frontend-design-policy-v1") {
2555
+ throw new Error("frontend writer admission: design policy result artifact invalid");
2556
+ }
2557
+ const designPolicyResult = {
2558
+ schemaId: "frontend-design-policy-v1",
2559
+ classification: policyRaw.classification === "approved" ? "approved" : "blocked",
2560
+ findings: policyRaw.findings ?? [],
2561
+ digest: "",
2562
+ };
2563
+ // 3. Freeze the pre-writer worktree baseline (cumulative diff baseline).
2564
+ await captureFrontendWorktreeBaseline({
2565
+ runDir: meta.runDir,
1984
2566
  workspaceRoot: input.cwd,
1985
- declaredAcIds,
1986
2567
  });
1987
- if (result.issues.length) {
2568
+ const baselineRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-worktree-baseline.json"));
2569
+ const baseline = (baselineRaw.files ?? [])
2570
+ .map((file) => ({
2571
+ path: file,
2572
+ sha256: baselineRaw.hashes?.[file] ?? null,
2573
+ }))
2574
+ .filter((entry) => entry.sha256 !== null);
2575
+ // 4. Writer-preceding lint baseline (R5) when lint is configured.
2576
+ let lintResults = [];
2577
+ if ((config.lintCommands?.length ?? 0) > 0) {
2578
+ for (const command of config.lintCommands) {
2579
+ const commandNumber = lintResults.length + 1;
2580
+ const result = await executeShellCommand({
2581
+ command,
2582
+ cwd: input.cwd,
2583
+ timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
2584
+ envAllowlist: shell.envAllowlist,
2585
+ dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2586
+ reportActivity: input.reportActivity,
2587
+ outputArtifacts: {
2588
+ stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2589
+ stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
2590
+ },
2591
+ });
2592
+ lintResults.push(result);
2593
+ }
2594
+ await materializeFrontendLintBaseline({
2595
+ runDir: meta.runDir,
2596
+ workspaceRoot: input.cwd,
2597
+ commands: config.lintCommands,
2598
+ results: lintResults,
2599
+ worktreeChanged: false,
2600
+ });
2601
+ }
2602
+ // 5. Derive admission (writeSet + digests + findings). Glob entries in
2603
+ // the contract targets (e.g. `src/**` inherited from the skeleton) are
2604
+ // expanded against the concrete workspace files so the derived writeSet
2605
+ // contains only real paths the writer can touch.
2606
+ const workspaceFiles = await collectWorkspaceFiles(input.cwd);
2607
+ const admission = deriveFrontendWriterAdmissionShell({
2608
+ contract,
2609
+ designPolicyResult,
2610
+ baseline,
2611
+ frozenCommandLabels: config.frozenCommandLabels,
2612
+ allowedMockStrategies: config.allowedMockStrategies,
2613
+ workspaceFiles,
2614
+ });
2615
+ // 6. Design verdict enforcement: request-revision is a blocked admission.
2616
+ const failureSource = policyRaw.classification === "blocked"
2617
+ ? "frontend-design-policy-shell"
2618
+ : "frontend-writer-admission-shell";
2619
+ const findings = verdictPass
2620
+ ? admission.findings
2621
+ : [
2622
+ ...admission.findings,
2623
+ {
2624
+ code: "design-approval-request-revision",
2625
+ message: designRestartPhase !== undefined
2626
+ ? `design review typed terminal verdict is not approve_design (restartPhase=${designRestartPhase})`
2627
+ : "design review typed terminal verdict is not approve_design",
2628
+ },
2629
+ ];
2630
+ const classification = findings.length === 0 && admission.classification === "accepted"
2631
+ ? admission.classification
2632
+ : findings.length === 0 &&
2633
+ admission.classification === "requires-human-approval"
2634
+ ? "requires-human-approval"
2635
+ : "blocked";
2636
+ const result = frontendWriterAdmissionResultV1Schema.parse({
2637
+ schemaVersion: 1,
2638
+ schemaId: "frontend-writer-admission-shell-v1",
2639
+ classification,
2640
+ writeSet: admission.writeSet,
2641
+ writeSetDigest: admission.writeSetDigest,
2642
+ baselineDigest: admission.baselineDigest,
2643
+ admissionDigest: admission.admissionDigest,
2644
+ ...(admission.baselineDebtPolicy !== undefined
2645
+ ? { baselineDebtPolicy: admission.baselineDebtPolicy }
2646
+ : {}),
2647
+ findings,
2648
+ failureSource,
2649
+ });
2650
+ await writeDagRunJsonArtifact(meta.runDir, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, result);
2651
+ // A+B (AC-007): High topology materializes a `high-admission-receipt-v1`
2652
+ // catalog whose candidate digest must match the admission digest before
2653
+ // the gate can transition from `requires-human-approval` to `accepted`.
2654
+ // Any digest drift resolves to `stale` (never silently accepted).
2655
+ if (classification === "requires-human-approval") {
2656
+ const receipt = evaluateHighAdmissionReceipt({
2657
+ candidateDigest: result.admissionDigest,
2658
+ admissionDigest: result.admissionDigest,
2659
+ });
2660
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-high-admission-receipt.json", receipt);
2661
+ if (receipt.status !== "accepted") {
2662
+ return {
2663
+ ok: false,
2664
+ stdout: "",
2665
+ stderr: `frontend writer admission stale: high-admission receipt candidate digest drifted from admission digest`,
2666
+ failureCategory: "invalid-output",
2667
+ durationMs: Date.now() - started,
2668
+ };
2669
+ }
2670
+ }
2671
+ if (classification === "blocked") {
1988
2672
  return {
1989
2673
  ok: false,
1990
2674
  stdout: "",
1991
- stderr: `frontend-test checklist blocked: ${JSON.stringify(result.issues)}`,
2675
+ stderr: `frontend writer admission ${classification}: ${findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
1992
2676
  failureCategory: "invalid-output",
1993
2677
  durationMs: Date.now() - started,
1994
2678
  };
1995
2679
  }
1996
2680
  return {
1997
2681
  ok: true,
1998
- stdout: `frontend-test checklist ok cases=${result.caseCount} source=${path.basename(result.manifestPath)}`,
2682
+ stdout: `Frontend writer admission: ${classification} writeSet=${result.writeSet.length} writeSetDigest=${result.writeSetDigest}`,
1999
2683
  stderr: "",
2000
2684
  failureCategory: "success",
2001
2685
  durationMs: Date.now() - started,
@@ -2011,16 +2695,40 @@ async function executeFrontendTestCaseChecklist(input, meta) {
2011
2695
  };
2012
2696
  }
2013
2697
  }
2014
- async function executeFrontendTestL5Report(input, meta) {
2698
+ async function executeFrontendContractImport(input, meta) {
2015
2699
  const started = Date.now();
2700
+ const shell = input.task.shell;
2701
+ const config = shell.frontendContractImport;
2016
2702
  try {
2017
- const output = await renderFrontendTestL5Report({
2018
- workspaceRoot: input.cwd,
2019
- runDir: meta.runDir,
2020
- });
2703
+ const binding = meta.spec.sourceBinding;
2704
+ if (!binding) {
2705
+ throw new Error("frontend contract-import requires a DAG sourceBinding to re-validate freshness");
2706
+ }
2707
+ if (config.requireSourceFreshness) {
2708
+ await assertFrontendSourceBindingFresh({
2709
+ workspaceRoot: input.cwd,
2710
+ binding,
2711
+ });
2712
+ }
2713
+ const digest = createHash("sha256")
2714
+ .update(JSON.stringify({
2715
+ taskId: binding.taskId,
2716
+ sources: binding.sources,
2717
+ artifactName: config.artifactName,
2718
+ }))
2719
+ .digest("hex");
2720
+ const facts = {
2721
+ schemaVersion: 1,
2722
+ schemaId: "frontend-contract-import-v1",
2723
+ artifactName: config.artifactName,
2724
+ sourceBindingTaskId: binding.taskId,
2725
+ sourceCount: binding.sources.length,
2726
+ fresh: true,
2727
+ digest,
2728
+ };
2021
2729
  return {
2022
2730
  ok: true,
2023
- stdout: `Frontend L-5 report: ${output.htmlPath}\nMarkdown: ${output.markdownPath}\nStatus: ${output.metrics.status}`,
2731
+ stdout: JSON.stringify(facts, null, 2),
2024
2732
  stderr: "",
2025
2733
  failureCategory: "success",
2026
2734
  durationMs: Date.now() - started,
@@ -2036,12 +2744,185 @@ async function executeFrontendTestL5Report(input, meta) {
2036
2744
  };
2037
2745
  }
2038
2746
  }
2039
- async function executeFrontendTestHtmlReport(input, meta) {
2747
+ async function executeFrontendCloseout(input, meta) {
2040
2748
  const started = Date.now();
2749
+ const shell = input.task.shell;
2750
+ const config = shell.frontendCloseout;
2041
2751
  try {
2042
- const output = await renderFrontendTestHtmlReport({
2752
+ const context = (await readRunDirJson(meta.runDir, "contracts/frontend-review-context.json"));
2753
+ const contract = frontendImplementationContractSchema.parse(context.contract);
2754
+ const trace = context.verificationTrace;
2755
+ const verificationTargets = (trace?.targets ?? []).map((target) => ({
2756
+ status: target.status === "ok" ? "ok" : "failed",
2757
+ }));
2758
+ const mockEvidenceBound = contract.mockApi.strategy !== "not-needed" &&
2759
+ typeof trace?.mockCommandNodeId === "string" &&
2760
+ trace.mockCommandNodeId.length > 0 &&
2761
+ Array.isArray(trace?.mockCommandLabels) &&
2762
+ trace.mockCommandLabels.length > 0 &&
2763
+ Array.isArray(trace?.mockCommandTexts) &&
2764
+ trace.mockCommandTexts.length === trace.mockCommandLabels.length;
2765
+ const integrationFacts = deriveFrontendIntegrationFacts({
2766
+ mockStrategy: contract.mockApi.strategy,
2767
+ realApiEvidence: { present: false, passed: false },
2768
+ mockEvidenceBound,
2769
+ verificationTargets,
2770
+ });
2771
+ // Typed review terminal (AC-003) from the committed typed event store.
2772
+ const typedKinds = await collectReviewTerminalKinds(meta.runDir, config.reviewFromNodeId);
2773
+ const typedVerdict = typedKinds.length === 1 &&
2774
+ (typedKinds[0] === "approve_review" || typedKinds[0] === "request_review_changes")
2775
+ ? typedKinds[0]
2776
+ : null;
2777
+ // A+B (AC-009): request_review_changes issueCategory maps to the
2778
+ // deterministic §4.10 restart phase. Recorded as a run-owned audit
2779
+ // artifact so the frozen closeout schema is never widened; `unknown`
2780
+ // fails closed to no restart phase.
2781
+ const reviewRestartPhase = typedVerdict === "request_review_changes"
2782
+ ? await collectReviewRestartPhase(meta.runDir, config.reviewFromNodeId)
2783
+ : undefined;
2784
+ if (reviewRestartPhase !== undefined) {
2785
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-review-restart-phase.json", {
2786
+ schemaVersion: 1,
2787
+ nodeId: config.reviewFromNodeId,
2788
+ restartPhase: reviewRestartPhase,
2789
+ });
2790
+ }
2791
+ const facts = frontendCloseoutFactsSchema.parse({
2792
+ schemaVersion: 1,
2793
+ schemaId: "frontend-closeout-facts-v1",
2794
+ coverageMatrix: verificationTargets.map((target, index) => ({
2795
+ verificationTargetId: trace?.targets?.[index]?.id ?? `vt-${index}`,
2796
+ status: target.status === "ok" ? "passed" : "failed",
2797
+ })),
2798
+ lintStatus: "unavailable",
2799
+ integrationFacts,
2800
+ reviewApproval: {
2801
+ typedVerdict,
2802
+ legacyVerdict: null,
2803
+ match: true,
2804
+ },
2805
+ cumulativeDiff: {
2806
+ cumulativeDigest: (typeof context.verificationTrace === "object" &&
2807
+ context.verificationTrace !== null &&
2808
+ "schemaId" in context.verificationTrace
2809
+ ? deterministicSha256({ verificationTrace: context.verificationTrace })
2810
+ : deterministicSha256({})),
2811
+ empty: false,
2812
+ },
2813
+ notRun: { browser: "not-run", visual: "not-run" },
2814
+ pendingRealIntegration: integrationFacts !== "real-api",
2815
+ });
2816
+ const rendered = renderFrontendCloseout(facts);
2817
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-closeout-facts.json", facts);
2818
+ // A+B (AC-010): materialize the deterministic frontend-test handoff.
2819
+ // browser/visual/runtime-a11y are always `not-run` and the handoff is
2820
+ // always `deferred-to-frontend-test`, so it never blocks implementation
2821
+ // and can never be rewritten into a passed result.
2822
+ const handoffTaskId = meta.spec.sourceBinding?.taskId ?? input.task.id;
2823
+ const sourceDigest = meta.spec.sourceBinding
2824
+ ? createHash("sha256")
2825
+ .update(JSON.stringify({
2826
+ taskId: meta.spec.sourceBinding.taskId,
2827
+ sources: meta.spec.sourceBinding.sources,
2828
+ }))
2829
+ .digest("hex")
2830
+ : deterministicSha256({});
2831
+ const handoff = buildFrontendTestHandoff({
2832
+ taskId: handoffTaskId,
2833
+ sourceDigest,
2834
+ candidateHash: facts.cumulativeDiff.cumulativeDigest,
2835
+ });
2836
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-test-handoff.json", handoff);
2837
+ await writeDagNodeTextArtifact(meta.runDir, input.task.id, "frontend-closeout.md", rendered);
2838
+ return {
2839
+ ok: true,
2840
+ stdout: `Frontend closeout rendered\n${rendered.split("\n").slice(0, 8).join("\n")}`,
2841
+ stderr: "",
2842
+ failureCategory: "success",
2843
+ durationMs: Date.now() - started,
2844
+ };
2845
+ }
2846
+ catch (error) {
2847
+ return {
2848
+ ok: false,
2849
+ stdout: "",
2850
+ stderr: error instanceof Error ? error.message : String(error),
2851
+ failureCategory: "invalid-output",
2852
+ durationMs: Date.now() - started,
2853
+ };
2854
+ }
2855
+ }
2856
+ async function executeFrontendTestCaseChecklist(input, meta) {
2857
+ const started = Date.now();
2858
+ try {
2859
+ const layout = frontendTestLayoutFromSpec(meta.spec);
2860
+ const declaredAcIds = (meta.spec.sourceBinding?.requirementIds ?? []).filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
2861
+ const result = await validateFrontendCaseChecklist({
2862
+ workspaceRoot: input.cwd,
2863
+ declaredAcIds,
2864
+ layout,
2865
+ });
2866
+ if (result.issues.length) {
2867
+ return {
2868
+ ok: false,
2869
+ stdout: "",
2870
+ stderr: `frontend-test checklist blocked: ${JSON.stringify(result.issues)}`,
2871
+ failureCategory: "invalid-output",
2872
+ durationMs: Date.now() - started,
2873
+ };
2874
+ }
2875
+ return {
2876
+ ok: true,
2877
+ stdout: `frontend-test checklist ok cases=${result.caseCount} source=${path.basename(result.manifestPath)}`,
2878
+ stderr: "",
2879
+ failureCategory: "success",
2880
+ durationMs: Date.now() - started,
2881
+ };
2882
+ }
2883
+ catch (error) {
2884
+ return {
2885
+ ok: false,
2886
+ stdout: "",
2887
+ stderr: error instanceof Error ? error.message : String(error),
2888
+ failureCategory: "invalid-output",
2889
+ durationMs: Date.now() - started,
2890
+ };
2891
+ }
2892
+ }
2893
+ async function executeFrontendTestL5Report(input, meta) {
2894
+ const started = Date.now();
2895
+ try {
2896
+ const output = await renderFrontendTestL5Report({
2043
2897
  workspaceRoot: input.cwd,
2044
2898
  runDir: meta.runDir,
2899
+ layout: frontendTestLayoutFromSpec(meta.spec),
2900
+ });
2901
+ return {
2902
+ ok: true,
2903
+ stdout: `Frontend L-5 report: ${output.htmlPath}\nMarkdown: ${output.markdownPath}\nStatus: ${output.metrics.status}`,
2904
+ stderr: "",
2905
+ failureCategory: "success",
2906
+ durationMs: Date.now() - started,
2907
+ };
2908
+ }
2909
+ catch (error) {
2910
+ return {
2911
+ ok: false,
2912
+ stdout: "",
2913
+ stderr: error instanceof Error ? error.message : String(error),
2914
+ failureCategory: "invalid-output",
2915
+ durationMs: Date.now() - started,
2916
+ };
2917
+ }
2918
+ }
2919
+ async function executeFrontendTestHtmlReport(input, meta) {
2920
+ const started = Date.now();
2921
+ try {
2922
+ const output = await renderFrontendTestHtmlReport({
2923
+ workspaceRoot: input.cwd,
2924
+ runDir: meta.runDir,
2925
+ layout: frontendTestLayoutFromSpec(meta.spec),
2045
2926
  });
2046
2927
  return {
2047
2928
  ok: true,
@@ -2061,11 +2942,60 @@ async function executeFrontendTestHtmlReport(input, meta) {
2061
2942
  };
2062
2943
  }
2063
2944
  }
2064
- async function executeFrontendTestEvidenceValidation(input) {
2945
+ async function executeFrontendTestStandardScenarios(input, meta) {
2946
+ const started = Date.now();
2947
+ try {
2948
+ const result = await copyFrontendTestStandardScenarios({
2949
+ workspaceRoot: input.cwd,
2950
+ layout: frontendTestLayoutFromSpec(meta.spec),
2951
+ });
2952
+ return {
2953
+ ok: true,
2954
+ stdout: JSON.stringify(result),
2955
+ stderr: "",
2956
+ failureCategory: "success",
2957
+ durationMs: Date.now() - started,
2958
+ };
2959
+ }
2960
+ catch (error) {
2961
+ return {
2962
+ ok: false,
2963
+ stdout: "",
2964
+ stderr: error instanceof Error ? error.message : String(error),
2965
+ failureCategory: "invalid-output",
2966
+ durationMs: Date.now() - started,
2967
+ };
2968
+ }
2969
+ }
2970
+ async function executeFrontendTestEnvironmentProbe(input, meta) {
2971
+ const started = Date.now();
2972
+ const result = await probeFrontendTestEnvironment({
2973
+ workspaceRoot: input.cwd,
2974
+ layout: frontendTestLayoutFromSpec(meta.spec),
2975
+ });
2976
+ if (result.ok) {
2977
+ return {
2978
+ ok: true,
2979
+ stdout: result.stdout,
2980
+ stderr: "",
2981
+ failureCategory: "success",
2982
+ durationMs: Date.now() - started,
2983
+ };
2984
+ }
2985
+ return {
2986
+ ok: false,
2987
+ stdout: "",
2988
+ stderr: result.stderr,
2989
+ failureCategory: "nonzero-exit",
2990
+ durationMs: Date.now() - started,
2991
+ };
2992
+ }
2993
+ async function executeFrontendTestEvidenceValidation(input, meta) {
2065
2994
  const started = Date.now();
2066
2995
  try {
2067
2996
  const result = await validateFrontendCaseEvidence({
2068
2997
  workspaceRoot: input.cwd,
2998
+ layout: frontendTestLayoutFromSpec(meta.spec),
2069
2999
  });
2070
3000
  const output = `frontend case evidence validation cases=${result.cases} findings=${result.issues.length}${result.issues.length ? ` issues=${JSON.stringify(result.issues)}` : ""}`;
2071
3001
  if (result.hardFail) {
@@ -2104,6 +3034,7 @@ async function executeFrontendTestCaseManifest(input, meta) {
2104
3034
  const checklist = await validateFrontendCaseChecklist({
2105
3035
  workspaceRoot: input.cwd,
2106
3036
  declaredAcIds,
3037
+ layout: frontendTestLayoutFromSpec(meta.spec),
2107
3038
  });
2108
3039
  if (checklist.issues.length) {
2109
3040
  return {
@@ -2118,6 +3049,7 @@ async function executeFrontendTestCaseManifest(input, meta) {
2118
3049
  workspaceRoot: input.cwd,
2119
3050
  maxCases: gate.maxCases,
2120
3051
  declaredAcIds,
3052
+ layout: frontendTestLayoutFromSpec(meta.spec),
2121
3053
  });
2122
3054
  return {
2123
3055
  ok: true,
@@ -2156,6 +3088,7 @@ async function executeFrontendTestResultFinalize(input, meta) {
2156
3088
  try {
2157
3089
  const evidence = await validateFrontendCaseEvidence({
2158
3090
  workspaceRoot: input.cwd,
3091
+ layout: frontendTestLayoutFromSpec(meta.spec),
2159
3092
  });
2160
3093
  advisoryStdout = `frontend case evidence validation cases=${evidence.cases} findings=${evidence.issues.length}${evidence.issues.length ? ` issues=${JSON.stringify(evidence.issues)}` : ""}`;
2161
3094
  // Advisory findings must not block materialize; only hardFail (path escape) does.
@@ -2174,6 +3107,7 @@ async function executeFrontendTestResultFinalize(input, meta) {
2174
3107
  artifactName: "frontend-test-result.json",
2175
3108
  outputDir: "contracts",
2176
3109
  sourceBinding: meta.spec.sourceBinding,
3110
+ layout: frontendTestLayoutFromSpec(meta.spec),
2177
3111
  });
2178
3112
  return {
2179
3113
  ok: true,
@@ -2212,12 +3146,14 @@ async function executeFrontendTestReports(input, meta) {
2212
3146
  const l5 = await renderFrontendTestL5Report({
2213
3147
  workspaceRoot: input.cwd,
2214
3148
  runDir: meta.runDir,
3149
+ layout: frontendTestLayoutFromSpec(meta.spec),
2215
3150
  });
2216
3151
  lines.push(`Frontend L-5 report: ${l5.htmlPath}\nMarkdown: ${l5.markdownPath}\nStatus: ${l5.metrics.status}`);
2217
3152
  }
2218
3153
  const report = await renderFrontendTestHtmlReport({
2219
3154
  workspaceRoot: input.cwd,
2220
3155
  runDir: meta.runDir,
3156
+ layout: frontendTestLayoutFromSpec(meta.spec),
2221
3157
  });
2222
3158
  lines.push(`Frontend test report: ${report.htmlPath}\nMarkdown: ${report.markdownPath}\nOutcome: ${report.outcome}\nCases: ${report.caseCount}`);
2223
3159
  return {
@@ -2238,69 +3174,76 @@ async function executeFrontendTestReports(input, meta) {
2238
3174
  };
2239
3175
  }
2240
3176
  }
2241
- async function executeFrontendLintBaseline(input, meta) {
2242
- const started = Date.now();
2243
- const shell = input.task.shell;
2244
- const baseline = shell.frontendLintBaseline;
2245
- const cwd = resolveShellCwd(input.cwd, shell.cwd);
2246
- let beforeStatus;
2247
- try {
2248
- beforeStatus = await readGitStatusPorcelain(input.cwd);
2249
- }
2250
- catch {
2251
- beforeStatus = undefined;
3177
+ async function writeShellPresetProcessReceipt(input) {
3178
+ await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
3179
+ schemaVersion: 1,
3180
+ kind: "shell-preset",
3181
+ preset: input.preset,
3182
+ ok: input.ok,
3183
+ durationMs: input.durationMs,
3184
+ steps: input.steps,
3185
+ });
3186
+ }
3187
+ async function ensureShellPresetProcessReceipt(input) {
3188
+ const receiptPath = path.join(input.runDir, input.nodeId, "process-receipt.json");
3189
+ if (existsSync(receiptPath))
3190
+ return;
3191
+ if (existsSync(path.join(input.runDir, input.nodeId, "shell-command-receipt.json"))) {
3192
+ return;
2252
3193
  }
2253
- const results = [];
2254
- for (const command of baseline.lintCommands) {
2255
- const commandNumber = results.length + 1;
2256
- results.push(await executeShellCommand({
2257
- command,
2258
- cwd,
2259
- timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
2260
- envAllowlist: shell.envAllowlist,
2261
- dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2262
- outputArtifacts: {
2263
- stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2264
- stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
3194
+ const detail = input.result.ok ? input.result.stdout : input.result.stderr;
3195
+ await writeShellPresetProcessReceipt({
3196
+ runDir: input.runDir,
3197
+ nodeId: input.nodeId,
3198
+ preset: input.preset,
3199
+ ok: input.result.ok,
3200
+ durationMs: input.result.durationMs,
3201
+ steps: [
3202
+ {
3203
+ id: "run",
3204
+ label: input.preset,
3205
+ ok: input.result.ok,
3206
+ detail: detail?.slice(0, 8 * 1024),
3207
+ durationMs: input.result.durationMs,
2265
3208
  },
2266
- }));
2267
- }
2268
- let afterStatus;
2269
- try {
2270
- afterStatus = await readGitStatusPorcelain(input.cwd);
2271
- }
2272
- catch {
2273
- afterStatus = undefined;
2274
- }
2275
- try {
2276
- const materialized = await materializeFrontendLintBaseline({
2277
- runDir: meta.runDir,
2278
- workspaceRoot: input.cwd,
2279
- commands: baseline.lintCommands,
2280
- results,
2281
- worktreeChanged: beforeStatus === undefined ||
2282
- afterStatus === undefined ||
2283
- beforeStatus !== afterStatus,
2284
- });
2285
- return {
2286
- ok: true,
2287
- stdout: `Frontend lint baseline: ${materialized.artifact.status}\nArtifact: ${materialized.ref.path}\nDiagnostics: ${materialized.artifact.diagnostics.length}`,
2288
- stderr: materialized.artifact.reason ?? "",
2289
- failureCategory: "success",
2290
- durationMs: Date.now() - started,
2291
- };
2292
- }
2293
- catch (error) {
2294
- return {
2295
- ok: false,
2296
- stdout: "",
2297
- stderr: error instanceof Error ? error.message : String(error),
2298
- failureCategory: "invalid-output",
2299
- durationMs: Date.now() - started,
2300
- };
2301
- }
3209
+ ],
3210
+ });
2302
3211
  }
2303
- export async function executeDagShellNode(input, meta) {
3212
+ function namedShellHandler(shell) {
3213
+ if (!shell)
3214
+ return null;
3215
+ const record = shell;
3216
+ for (const key of [
3217
+ "finalWriteSetApprovalGate",
3218
+ "frontendTestStandardScenarios",
3219
+ "frontendTestEnvironmentProbe",
3220
+ "frontendBrowserToolPreflight",
3221
+ "frontendPrewriteGate",
3222
+ "frontendLintBaseline",
3223
+ "frontendVerificationBundle",
3224
+ "frontendReviewContext",
3225
+ "frontendTestCaseChecklist",
3226
+ "frontendTestCaseManifest",
3227
+ "frontendTestEvidenceValidation",
3228
+ "frontendTestResultFinalize",
3229
+ "frontendTestL5Report",
3230
+ "frontendTestHtmlReport",
3231
+ "frontendTestReports",
3232
+ "backendTestPipeline",
3233
+ "jsonArtifactGate",
3234
+ "projectGovernanceGate",
3235
+ "requirementCoverageGate",
3236
+ "verdictGate",
3237
+ "verifyEvidence",
3238
+ "repairArtifactGate",
3239
+ "jacocoCoverage",
3240
+ ]) {
3241
+ if (record[key] != null)
3242
+ return key;
3243
+ }
3244
+ return null;
3245
+ }
3246
+ async function executeDagShellNodeInner(input, meta) {
2304
3247
  const shell = input.task.shell;
2305
3248
  if (shell?.finalWriteSetApprovalGate) {
2306
3249
  const started = Date.now();
@@ -2339,30 +3282,91 @@ export async function executeDagShellNode(input, meta) {
2339
3282
  };
2340
3283
  }
2341
3284
  }
3285
+ if (shell?.frontendTestStandardScenarios) {
3286
+ return executeFrontendTestStandardScenarios(input, meta);
3287
+ }
3288
+ if (shell?.frontendTestEnvironmentProbe) {
3289
+ return executeFrontendTestEnvironmentProbe(input, meta);
3290
+ }
2342
3291
  if (shell?.frontendBrowserToolPreflight) {
2343
3292
  const started = Date.now();
3293
+ const steps = [];
3294
+ const finish = async (result) => {
3295
+ await writeShellPresetProcessReceipt({
3296
+ runDir: meta.runDir,
3297
+ nodeId: input.task.id,
3298
+ preset: "frontendBrowserToolPreflight",
3299
+ ok: result.ok,
3300
+ durationMs: result.durationMs,
3301
+ steps: steps.length > 0
3302
+ ? steps
3303
+ : [
3304
+ {
3305
+ id: "run",
3306
+ label: "浏览器工具能力探测",
3307
+ ok: result.ok,
3308
+ detail: result.ok ? result.stdout : result.stderr,
3309
+ durationMs: result.durationMs,
3310
+ },
3311
+ ],
3312
+ });
3313
+ return result;
3314
+ };
2344
3315
  try {
2345
3316
  if (resolvePiBackend() === "cli-only") {
3317
+ steps.push({
3318
+ id: "pi-backend",
3319
+ label: "Pi SDK 后端可用",
3320
+ ok: false,
3321
+ detail: "CODE_AGENT_PI_BACKEND=cli-only",
3322
+ });
2346
3323
  throw new Error("browser-command-capability-unavailable: CODE_AGENT_PI_BACKEND=cli-only rejects SDK-only playwright_cli");
2347
3324
  }
3325
+ steps.push({
3326
+ id: "pi-backend",
3327
+ label: "Pi SDK 后端可用",
3328
+ ok: true,
3329
+ });
2348
3330
  const sdkCapability = await checkPiSdkCustomToolCapability(input.cwd);
2349
3331
  if (!sdkCapability.ok) {
3332
+ steps.push({
3333
+ id: "sdk-custom-tool",
3334
+ label: "Pi SDK custom-tool 能力",
3335
+ ok: false,
3336
+ detail: sdkCapability.detail,
3337
+ });
2350
3338
  throw new Error(`browser-command-capability-unavailable: ${sdkCapability.detail}`);
2351
3339
  }
3340
+ steps.push({
3341
+ id: "sdk-custom-tool",
3342
+ label: "Pi SDK custom-tool 能力",
3343
+ ok: true,
3344
+ });
3345
+ const layout = frontendTestLayoutFromSpec(meta.spec);
2352
3346
  const toolProbe = await createPlaywrightCliTool({
2353
3347
  repoRoot: input.cwd,
2354
3348
  runDir: meta.runDir,
2355
3349
  nodeId: input.task.id,
2356
3350
  caseId: "frontend-browser-preflight",
2357
- evidenceDir: "testcase/frontend/evidence/frontend-browser-preflight",
2358
- // Capability-only registration probe. The tested URL is selected later by
2359
- // retrieve-frontend-test-context-pi and validated by the environment shell.
3351
+ evidenceDir: `${layout.evidenceDir}/frontend-browser-preflight`,
2360
3352
  baseUrl: "http://localhost:5173/",
2361
- inputRoot: "testcase/frontend/fixtures",
3353
+ inputRoot: layout.fixturesDir,
3354
+ layout,
2362
3355
  });
2363
3356
  if (toolProbe.name !== "playwright_cli") {
3357
+ steps.push({
3358
+ id: "structured-tool",
3359
+ label: "注册 playwright_cli",
3360
+ ok: false,
3361
+ detail: toolProbe.name,
3362
+ });
2364
3363
  throw new Error("browser-command-capability-unavailable: structured playwright_cli tool registration failed");
2365
3364
  }
3365
+ steps.push({
3366
+ id: "structured-tool",
3367
+ label: "注册 playwright_cli",
3368
+ ok: true,
3369
+ });
2366
3370
  const launcher = resolvePlaywrightCliLauncher(input.cwd);
2367
3371
  const result = await defaultPlaywrightCliCommandRunner({
2368
3372
  executable: launcher.executable,
@@ -2375,8 +3379,19 @@ export async function executeDagShellNode(input, meta) {
2375
3379
  if (result.exitCode !== 0 ||
2376
3380
  result.timedOut ||
2377
3381
  !["open", "close", "find", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
3382
+ steps.push({
3383
+ id: "playwright-cli-help",
3384
+ label: "playwright-cli --help 合同",
3385
+ ok: false,
3386
+ detail: help.slice(0, 8 * 1024),
3387
+ });
2378
3388
  throw new Error("playwright-cli-contract-incompatible");
2379
3389
  }
3390
+ steps.push({
3391
+ id: "playwright-cli-help",
3392
+ label: "playwright-cli --help 合同",
3393
+ ok: true,
3394
+ });
2380
3395
  const capability = {
2381
3396
  schemaVersion: 1,
2382
3397
  status: "ready",
@@ -2387,54 +3402,35 @@ export async function executeDagShellNode(input, meta) {
2387
3402
  targetUrlAuthority: "retrieve-context-and-environment-probe",
2388
3403
  };
2389
3404
  await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "frontend-browser-capability.json", capability);
2390
- return {
3405
+ steps.push({
3406
+ id: "capability-artifact",
3407
+ label: "写出 frontend-browser-capability.json",
3408
+ ok: true,
3409
+ detail: JSON.stringify(capability, null, 2),
3410
+ });
3411
+ return await finish({
2391
3412
  ok: true,
2392
3413
  stdout: JSON.stringify(capability),
2393
3414
  stderr: "",
2394
3415
  failureCategory: "success",
2395
3416
  durationMs: Date.now() - started,
2396
- };
3417
+ });
2397
3418
  }
2398
3419
  catch (error) {
2399
- return {
3420
+ return await finish({
2400
3421
  ok: false,
2401
3422
  stdout: "",
2402
3423
  stderr: `playwright-cli-unavailable: ${error instanceof Error ? error.message : String(error)}`,
2403
3424
  failureCategory: "tool-policy",
2404
3425
  durationMs: Date.now() - started,
2405
- };
2406
- }
2407
- }
2408
- if (shell?.frontendPrewriteGate) {
2409
- const started = Date.now();
2410
- try {
2411
- const result = await runFrontendPrewriteGate({
2412
- runDir: meta.runDir,
2413
- config: shell.frontendPrewriteGate,
2414
- sourceBinding: meta.spec.sourceBinding,
2415
- workspaceRoot: input.cwd,
2416
- repoRoot: input.cwd,
2417
3426
  });
2418
- return {
2419
- ok: true,
2420
- stdout: formatFrontendPrewriteGateStdout(result),
2421
- stderr: "",
2422
- failureCategory: "success",
2423
- durationMs: Date.now() - started,
2424
- };
2425
- }
2426
- catch (error) {
2427
- return {
2428
- ok: false,
2429
- stdout: "",
2430
- stderr: error instanceof Error ? error.message : String(error),
2431
- failureCategory: "invalid-output",
2432
- durationMs: Date.now() - started,
2433
- };
2434
3427
  }
2435
3428
  }
2436
- if (shell?.frontendLintBaseline) {
2437
- return executeFrontendLintBaseline(input, meta);
3429
+ if (shell?.frontendDesignPolicy) {
3430
+ return executeFrontendDesignPolicy(input, meta);
3431
+ }
3432
+ if (shell?.frontendWriterAdmission) {
3433
+ return executeFrontendWriterAdmission(input, meta);
2438
3434
  }
2439
3435
  if (shell?.frontendVerificationBundle) {
2440
3436
  return executeFrontendVerificationBundle(input, meta);
@@ -2465,6 +3461,12 @@ export async function executeDagShellNode(input, meta) {
2465
3461
  };
2466
3462
  }
2467
3463
  }
3464
+ if (shell?.frontendContractImport) {
3465
+ return executeFrontendContractImport(input, meta);
3466
+ }
3467
+ if (shell?.frontendCloseout) {
3468
+ return executeFrontendCloseout(input, meta);
3469
+ }
2468
3470
  if (shell?.frontendTestCaseChecklist) {
2469
3471
  return executeFrontendTestCaseChecklist(input, meta);
2470
3472
  }
@@ -2472,7 +3474,7 @@ export async function executeDagShellNode(input, meta) {
2472
3474
  return executeFrontendTestCaseManifest(input, meta);
2473
3475
  }
2474
3476
  if (shell?.frontendTestEvidenceValidation) {
2475
- return executeFrontendTestEvidenceValidation(input);
3477
+ return executeFrontendTestEvidenceValidation(input, meta);
2476
3478
  }
2477
3479
  if (shell?.frontendTestResultFinalize) {
2478
3480
  return executeFrontendTestResultFinalize(input, meta);
@@ -2579,6 +3581,7 @@ export async function executeDagShellNode(input, meta) {
2579
3581
  artifactName: gate.artifactName,
2580
3582
  outputDir: gate.outputDir,
2581
3583
  sourceBinding: meta.spec.sourceBinding,
3584
+ layout: frontendTestLayoutFromSpec(meta.spec),
2582
3585
  });
2583
3586
  break;
2584
3587
  default:
@@ -2686,53 +3689,23 @@ export async function executeDagShellNode(input, meta) {
2686
3689
  }
2687
3690
  if (shell?.commands?.length === 1 &&
2688
3691
  shell.commands[0] === "frontend-failure-assess-gate") {
2689
- const started = Date.now();
2690
- try {
2691
- const result = await runFrontendFailureAssessGate({
2692
- runDir: meta.runDir,
2693
- });
2694
- return {
2695
- ok: true,
2696
- stdout: formatFrontendFailureAssessStdout(result),
2697
- stderr: "",
2698
- failureCategory: "success",
2699
- durationMs: Date.now() - started,
2700
- };
2701
- }
2702
- catch (error) {
2703
- return {
2704
- ok: false,
2705
- stdout: "",
2706
- stderr: error instanceof Error ? error.message : String(error),
2707
- failureCategory: "invalid-output",
2708
- durationMs: Date.now() - started,
2709
- };
2710
- }
3692
+ return {
3693
+ ok: false,
3694
+ stdout: "",
3695
+ stderr: "frontend-failure-assess-gate legacy command removed in M8: verify failure is terminal and materializes failure ownership facts in the verification bundle itself",
3696
+ failureCategory: "invalid-output",
3697
+ durationMs: 0,
3698
+ };
2711
3699
  }
2712
3700
  if (shell?.commands?.length === 1 &&
2713
3701
  shell.commands[0] === "frontend-repair-contract-gate") {
2714
- const started = Date.now();
2715
- try {
2716
- const result = await runFrontendRepairContractGate({
2717
- runDir: meta.runDir,
2718
- });
2719
- return {
2720
- ok: true,
2721
- stdout: formatFrontendRepairContractStdout(result.assessment),
2722
- stderr: "",
2723
- failureCategory: "success",
2724
- durationMs: Date.now() - started,
2725
- };
2726
- }
2727
- catch (error) {
2728
- return {
2729
- ok: false,
2730
- stdout: "",
2731
- stderr: error instanceof Error ? error.message : String(error),
2732
- failureCategory: "invalid-output",
2733
- durationMs: Date.now() - started,
2734
- };
2735
- }
3702
+ return {
3703
+ ok: false,
3704
+ stdout: "",
3705
+ stderr: "frontend-repair-contract-gate legacy command removed in M8: no same-run repair branch exists in the new topology",
3706
+ failureCategory: "invalid-output",
3707
+ durationMs: 0,
3708
+ };
2736
3709
  }
2737
3710
  if (shell?.projectGovernanceGate) {
2738
3711
  const started = Date.now();
@@ -2824,6 +3797,7 @@ export async function executeDagShellNode(input, meta) {
2824
3797
  const cwd = resolveShellCwd(input.cwd, shell.cwd);
2825
3798
  const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
2826
3799
  const failFast = shell.failFast !== false;
3800
+ const attemptNumber = input.attempt ?? 1;
2827
3801
  const results = [];
2828
3802
  const executedCommandKeys = new Set();
2829
3803
  let beforeStatus;
@@ -2919,8 +3893,8 @@ export async function executeDagShellNode(input, meta) {
2919
3893
  dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2920
3894
  reportActivity: input.reportActivity,
2921
3895
  outputArtifacts: {
2922
- stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2923
- stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
3896
+ stdoutPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stdout.txt`),
3897
+ stderrPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stderr.txt`),
2924
3898
  },
2925
3899
  });
2926
3900
  results.push(result);
@@ -2979,6 +3953,7 @@ export async function executeDagShellNode(input, meta) {
2979
3953
  schemaVersion: 1,
2980
3954
  runId: meta.runId,
2981
3955
  nodeId: input.task.id,
3956
+ attempt: attemptNumber,
2982
3957
  workspaceFingerprint: workspaceFingerprint ?? null,
2983
3958
  failFast,
2984
3959
  commands: results.map((result) => ({
@@ -2997,6 +3972,7 @@ export async function executeDagShellNode(input, meta) {
2997
3972
  subchecks: parseCheckRepoSubcheckDurations(result),
2998
3973
  })),
2999
3974
  };
3975
+ await writeDagNodeJsonArtifact(meta.runDir, input.task.id, path.posix.join(`attempt-${attemptNumber}`, "shell-command-receipt.json"), receipt);
3000
3976
  await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "shell-command-receipt.json", receipt);
3001
3977
  const summaryMarkdown = buildShellResultSummaryMarkdown({
3002
3978
  nodeId: input.task.id,
@@ -3048,8 +4024,57 @@ export async function executeDagShellNode(input, meta) {
3048
4024
  stdout: `${recordPrefix}${reuseNote}${aggregate.stdout}`,
3049
4025
  stderr: stderrParts.filter(Boolean).join("\n\n"),
3050
4026
  failureCategory,
4027
+ ...(firstFailure?.failureCategory
4028
+ ? { rawFailureCategory: firstFailure.failureCategory }
4029
+ : {}),
3051
4030
  durationMs: Date.now() - started,
3052
4031
  // extra field consumed by node-execution if supported; also embedded in stdout marker
3053
4032
  ...{ commandResults },
3054
4033
  };
3055
4034
  }
4035
+ /**
4036
+ * Enumerate the workspace's concrete file paths for glob writeSet expansion.
4037
+ *
4038
+ * Uses `git ls-files -co --exclude-standard` (tracked + untracked, excluding
4039
+ * gitignored) so the expanded writeSet reflects real files the writer can
4040
+ * touch. Falls back to [] when the workspace is not a git repository or git
4041
+ * fails — in that case glob entries fail closed as non-concrete paths.
4042
+ */
4043
+ async function collectWorkspaceFiles(workspaceRoot) {
4044
+ try {
4045
+ const files = await new Promise((resolve, reject) => {
4046
+ const child = spawn("git", ["ls-files", "-co", "--exclude-standard", "-z"], { cwd: workspaceRoot, stdio: ["ignore", "pipe", "ignore"] });
4047
+ const chunks = [];
4048
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
4049
+ child.on("error", reject);
4050
+ child.on("close", (code) => {
4051
+ if (code === 0) {
4052
+ resolve(Buffer.concat(chunks)
4053
+ .toString("utf8")
4054
+ .split("\0")
4055
+ .filter(Boolean));
4056
+ }
4057
+ else {
4058
+ reject(new Error(`git ls-files exited ${String(code)}`));
4059
+ }
4060
+ });
4061
+ });
4062
+ return files.map((file) => file.replace(/\\/g, "/")).sort();
4063
+ }
4064
+ catch {
4065
+ return [];
4066
+ }
4067
+ }
4068
+ export async function executeDagShellNode(input, meta) {
4069
+ const handler = namedShellHandler(input.task.shell);
4070
+ const result = await executeDagShellNodeInner(input, meta);
4071
+ if (handler) {
4072
+ await ensureShellPresetProcessReceipt({
4073
+ runDir: meta.runDir,
4074
+ nodeId: input.task.id,
4075
+ preset: handler,
4076
+ result,
4077
+ });
4078
+ }
4079
+ return result;
4080
+ }