@tea-agent/loop-agent 0.41.1-beta.0 → 0.42.0-next.2

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 (277) hide show
  1. package/CHANGELOG.md +78 -206
  2. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  3. package/dist/adapters/context-transfer/pi-session.js +61 -0
  4. package/dist/application/dag/generate-task-dag.js +19 -75
  5. package/dist/application/task-lifecycle/advance.js +5 -24
  6. package/dist/application/task-lifecycle/observe.js +17 -171
  7. package/dist/application/task-lifecycle/plan-transitions.js +7 -42
  8. package/dist/application/task-lifecycle/recommendations.js +2 -13
  9. package/dist/build-stamp.json +3 -3
  10. package/dist/cli/command-definitions.js +12 -7
  11. package/dist/cli/program.js +23 -6
  12. package/dist/commands/client-recovery.js +0 -3
  13. package/dist/commands/dag-artifact.js +284 -0
  14. package/dist/commands/dag-context.js +184 -0
  15. package/dist/commands/dag-rerun.js +206 -1
  16. package/dist/commands/init.js +1 -27
  17. package/dist/commands/task-advance.js +0 -19
  18. package/dist/executors/dag-pi-executor.js +266 -3792
  19. package/dist/executors/pi-executor.js +4 -15
  20. package/dist/executors/pi-sdk-executor.js +3 -0
  21. package/dist/executors/shell-executor.js +217 -945
  22. package/dist/executors/shell-write-guard.js +0 -7
  23. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  24. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  25. package/dist/infrastructure/console/context-export-store.js +160 -0
  26. package/dist/infrastructure/console/dir-lock.js +132 -0
  27. package/dist/infrastructure/console/operation-store.js +197 -0
  28. package/dist/infrastructure/harness/artifact-store.js +10 -1
  29. package/dist/infrastructure/harness/atomic-write.js +12 -2
  30. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  31. package/dist/shared/context-transfer.js +418 -0
  32. package/dist/shared/dag-failure-category.js +0 -12
  33. package/dist/shared/openspec-spec.js +4 -70
  34. package/dist/shared/operator/capabilities.js +14 -7
  35. package/dist/shared/operator/safe-run-summary.js +1 -0
  36. package/dist/shared/path-safety.js +93 -0
  37. package/dist/shared/preview.js +28 -4
  38. package/dist/task/config-types.js +80 -35
  39. package/dist/task/contract/adopt.js +0 -4
  40. package/dist/task/contract/import-revision.js +0 -4
  41. package/dist/task/contract/project.js +0 -3
  42. package/dist/task/contract/schema.js +1 -2
  43. package/dist/task/frontend-project-capability.js +20 -203
  44. package/dist/task/runtime.js +2 -5
  45. package/dist/task/source-prepare/build-draft.js +3 -3
  46. package/dist/task/source-prepare/fragment-inventory.js +9 -15
  47. package/dist/task/source-prepare/prepare.js +1 -89
  48. package/dist/task/source-prepare/semantic-intake.js +2 -6
  49. package/dist/task/source-references.js +1 -22
  50. package/dist/task/task-demand-routing.js +3 -0
  51. package/dist/worker/console/chat/chat-event-store.js +2 -2
  52. package/dist/worker/console/chat/pi-runtime.js +3 -2
  53. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  54. package/dist/worker/console/chat/routes.js +13 -4
  55. package/dist/worker/console/chat/semantic-activity.js +12 -11
  56. package/dist/worker/console/chat/session-stats.js +96 -0
  57. package/dist/worker/console/chat/session-store.js +1 -1
  58. package/dist/worker/console/chat/turn-process.js +28 -4
  59. package/dist/worker/console/chat/user-questions.js +1 -1
  60. package/dist/worker/console/console-update-runtime.js +1 -1
  61. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  62. package/dist/worker/console/dag-confirmation.js +1 -1
  63. package/dist/worker/console/dag-execution-receipt.js +1 -1
  64. package/dist/worker/console/doctor.js +1 -1
  65. package/dist/worker/console/draft-store.js +1 -1
  66. package/dist/worker/console/human-gate-token.js +1 -1
  67. package/dist/worker/console/index.js +3 -6
  68. package/dist/worker/console/interview/assessment.js +1 -1
  69. package/dist/worker/console/interview/session.js +1 -1
  70. package/dist/worker/console/operation-runner.js +45 -4
  71. package/dist/worker/console/operation-sse.js +1 -1
  72. package/dist/worker/console/operation-wait.js +1 -1
  73. package/dist/worker/console/operator-actions.js +107 -168
  74. package/dist/worker/console/operator-surface-health.js +1 -1
  75. package/dist/worker/console/operator-user-error.js +169 -0
  76. package/dist/worker/console/pi-plugins.js +60 -1
  77. package/dist/worker/console/routes.js +637 -8
  78. package/dist/worker/console/security.js +35 -0
  79. package/dist/worker/console/server.js +6 -6
  80. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DZZ8m3PO.js} +1 -1
  81. package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-D6PvaVd-.js} +1 -1
  82. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-B_OTOiI8.js} +1 -1
  83. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-Bv6rqHBg.js} +1 -1
  84. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-B8eHr0oz.js} +1 -1
  85. package/dist/worker/console/static/assets/channel-BU5gOilw.js +1 -0
  86. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-DYwR0im2.js} +1 -1
  87. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-D2WPGqXt.js} +1 -1
  88. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-CQISJ_I7.js} +1 -1
  89. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-C0o2Du1e.js} +1 -1
  90. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-4f8kr-U7.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-D_OWvXzX.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-CasdPz5X.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-Cj8lRrla.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-CUvD1FSd.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-CqLw_eqB.js} +1 -1
  96. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +1 -0
  97. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +1 -0
  98. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-ku-WqJPx.js} +1 -1
  99. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-Bq_PYDhl.js} +1 -1
  100. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-BiGZV641.js} +1 -1
  101. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-C4JRlglF.js} +1 -1
  102. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-B0dNeWYG.js} +1 -1
  103. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cj35YvYM.js} +1 -1
  104. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-uzQoJ2-8.js} +1 -1
  105. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D1a-Buoz.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-XjRrLRSs.js} +1 -1
  107. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-Da_O24cW.js} +1 -1
  108. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BLJ8jrYU.js} +1 -1
  109. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-B9GrLjM0.js} +1 -1
  110. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-CeJ0TqiK.js} +1 -1
  111. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js} +1 -1
  112. package/dist/worker/console/static/assets/index-H9rFJiGL.css +1 -0
  113. package/dist/worker/console/static/assets/index-xwu9GxEc.js +451 -0
  114. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-DB26i3d8.js} +1 -1
  115. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BOGRyeZT.js} +1 -1
  116. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-CZRoBO_6.js} +1 -1
  117. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-qlWhJyoB.js} +1 -1
  118. package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-CfUiDDB3.js} +1 -1
  119. package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-BQe6fpqj.js} +5 -5
  120. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-zFzWHw64.js} +1 -1
  121. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-DFGUqjZO.js} +1 -1
  122. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-BB5i0l8Z.js} +1 -1
  123. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-obbVZ_X5.js} +1 -1
  124. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-BwdaNzc1.js} +1 -1
  125. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CkTJaugh.js} +1 -1
  126. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-BwJ-hIgq.js} +1 -1
  127. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-BzSgmm7w.js} +1 -1
  128. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-Bn3oj5Q1.js} +1 -1
  129. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-dxhGtL8K.js} +1 -1
  130. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +1 -0
  131. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BLpEInya.js} +2 -2
  132. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +8 -0
  133. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-ChJGYcXy.js} +1 -1
  134. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-DK-qjRer.js} +1 -1
  135. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-3qeaWAg-.js} +1 -1
  136. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-CootlyP9.js} +1 -1
  137. package/dist/worker/console/static/index.html +7 -2
  138. package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
  139. package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
  140. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
  141. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  142. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
  143. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  144. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
  145. package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
  146. package/dist/worker/console/workspace-context.js +28 -112
  147. package/dist/worker/console/workspace-registry.js +2 -2
  148. package/dist/worker/continuation/worker-continuation.js +278 -0
  149. package/dist/worker/observability/read-model.js +4 -0
  150. package/dist/worker/observe/health.js +1 -1
  151. package/dist/worker/observe/node-transparency.js +572 -0
  152. package/dist/worker/observe/routes.js +51 -1
  153. package/dist/worker/observe/static/api.js +69 -5
  154. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  155. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  156. package/dist/worker/observe/static/dag-node-purpose.js +0 -5
  157. package/dist/worker/observe/static/state.js +3 -1
  158. package/dist/worker/observe/static/styles.css +1037 -126
  159. package/dist/worker/observe/static/views/dag-inspector.js +1217 -65
  160. package/dist/worker/observe/static/views/session-timeline.js +121 -25
  161. package/dist/worker/outcomes/adapters.js +19 -0
  162. package/dist/worker/outcomes/types.js +1 -0
  163. package/dist/worker/pool/attempt-lease.js +97 -66
  164. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  165. package/dist/worker/pool/reconcile.js +46 -1
  166. package/dist/worker/pool/run-store.js +2 -0
  167. package/dist/worker/pool/state-projection.js +2 -0
  168. package/dist/worker/task-spec/workflow-routing.js +8 -3
  169. package/dist/workflows/dag/artifact-bindings.js +149 -0
  170. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  171. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +99 -4
  172. package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
  173. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  174. package/dist/workflows/dag/backend-test-writer-completeness.js +45 -3
  175. package/dist/workflows/dag/budget-enforcement.js +10 -0
  176. package/dist/workflows/dag/context-receipt.js +305 -0
  177. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  178. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  179. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  180. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  181. package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
  182. package/dist/workflows/dag/frontend-plan-render.js +1 -2
  183. package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
  184. package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
  185. package/dist/workflows/dag/frontend-recovery-run.js +20 -33
  186. package/dist/workflows/dag/frontend-repair.js +432 -1
  187. package/dist/workflows/dag/frontend-review-context.js +15 -261
  188. package/dist/workflows/dag/frontend-verification-trace.js +24 -249
  189. package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
  190. package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
  191. package/dist/workflows/dag/init-hybrid.js +740 -718
  192. package/dist/workflows/dag/interrupt-request.js +0 -7
  193. package/dist/workflows/dag/node-execution.js +136 -759
  194. package/dist/workflows/dag/path-safety.js +1 -0
  195. package/dist/workflows/dag/prompt.js +30 -38
  196. package/dist/workflows/dag/report.js +1 -37
  197. package/dist/workflows/dag/rerun-feedback.js +1 -256
  198. package/dist/workflows/dag/rerun-plan.js +557 -11
  199. package/dist/workflows/dag/rerun-run.js +601 -38
  200. package/dist/workflows/dag/rerun-task.js +0 -29
  201. package/dist/workflows/dag/retry-policy.js +109 -214
  202. package/dist/workflows/dag/runner.js +142 -430
  203. package/dist/workflows/dag/scheduler.js +16 -114
  204. package/dist/workflows/dag/skill-snapshot.js +17 -0
  205. package/dist/workflows/dag/types.js +195 -246
  206. package/dist/workflows/dag/validate.js +12 -28
  207. package/docs/architecture/runtime-boundaries.md +6 -6
  208. package/docs/architecture/worker-and-feature.md +1 -1
  209. package/docs/init-surface.manifest.json +12 -30
  210. package/docs/skills/vetted-skill-registry.md +2 -4
  211. package/docs/templates/README.md +0 -2
  212. package/docs/templates/agent-dag-report.schema.json +2 -8
  213. package/docs/templates/agent-dag.schema.json +33 -1
  214. package/docs/templates/backend-test-dag.json +18 -18
  215. package/docs/templates/frontend-implementation-contract.schema.json +1 -4
  216. package/docs/templates/product-line/task.yaml +1 -1
  217. package/harness.json +3 -3
  218. package/package.json +3 -1
  219. package/skills/frontend-bounded-implement/SKILL.md +14 -15
  220. package/skills/frontend-design-review/SKILL.md +41 -22
  221. package/skills/frontend-implementation/SKILL.md +52 -0
  222. package/skills/frontend-implementation/references/code-standards.md +33 -0
  223. package/skills/frontend-implementation/references/design-spec.md +56 -0
  224. package/skills/frontend-implementation/references/node-contracts.md +31 -0
  225. package/skills/frontend-review/SKILL.md +15 -20
  226. package/skills/frontend-review/references/review-findings.md +7 -6
  227. package/skills/frontend-verification/SKILL.md +1 -1
  228. package/skills/loop-agent/references/command-reference.md +6 -1
  229. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  230. package/dist/commands/dag-follow-up.js +0 -138
  231. package/dist/executors/pi-read-budget-policy.js +0 -239
  232. package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
  233. package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
  234. package/dist/worker/console/operation-store.js +0 -389
  235. package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
  236. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
  237. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
  238. package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
  239. package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
  240. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
  241. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
  242. package/dist/worker/console/static-src/active-run-badge.js +0 -17
  243. package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
  244. package/dist/workflows/dag/dag-retry-schema.js +0 -138
  245. package/dist/workflows/dag/frontend-closeout.js +0 -221
  246. package/dist/workflows/dag/frontend-design-policy.js +0 -400
  247. package/dist/workflows/dag/frontend-human-decision.js +0 -182
  248. package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
  249. package/dist/workflows/dag/frontend-read-budgets.js +0 -12
  250. package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
  251. package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
  252. package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
  253. package/dist/workflows/dag/frontend-review-findings.js +0 -270
  254. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
  255. package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
  256. package/dist/workflows/dag/frontend-shape-facts.js +0 -409
  257. package/dist/workflows/dag/frontend-shape.js +0 -427
  258. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
  259. package/dist/workflows/dag/frontend-split-application-service.js +0 -203
  260. package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
  261. package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
  262. package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
  263. package/dist/workflows/dag/frontend-writer-admission.js +0 -285
  264. package/dist/workflows/dag/frontend-writer-status.js +0 -256
  265. package/dist/workflows/dag/recovery-lease.js +0 -80
  266. package/docs/templates/frontend-implementation-dag.json +0 -89
  267. package/docs/templates/spec-registry.schema.json +0 -45
  268. package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
  269. package/skills/frontend-contract/SKILL.md +0 -23
  270. package/skills/frontend-contract/references/contract-protocol.md +0 -34
  271. package/skills/frontend-plan/SKILL.md +0 -26
  272. package/skills/frontend-plan/references/decision-contract.md +0 -37
  273. package/skills/frontend-plan/references/design-decisions.md +0 -17
  274. package/skills/frontend-scout/SKILL.md +0 -25
  275. package/skills/frontend-scout/references/design-evidence.md +0 -16
  276. package/skills/frontend-scout/references/scout-evidence.md +0 -23
  277. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -1,9 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
2
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
4
3
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
5
4
  import path from "node:path";
6
- import { writeDagNodeJsonArtifact, writeDagNodeTextArtifact, writeDagRunJsonArtifact, } from "../infrastructure/harness/artifact-store.js";
5
+ import { writeDagNodeJsonArtifact, writeDagNodeTextArtifact, } from "../infrastructure/harness/artifact-store.js";
7
6
  import { captureWorkspaceCheckpoint } from "../workflows/dag/workspace-checkpoint.js";
8
7
  import { truncateOutput } from "../shared/output-truncation.js";
9
8
  import { processTreeSpawnOptions, terminateProcessTree, } from "./process-tree.js";
@@ -15,8 +14,6 @@ import { buildRequirementCoverageGateShellCommand, expandShellPreset, buildVerdi
15
14
  import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend-test-analysis-contract.js";
16
15
  import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
17
16
  import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
18
- import { assertFrontendSourceBindingFresh, buildContractFromCommittedFacts, deterministicSha256, frontendImplementationContractSchema, serializeCompactDeterministicJson, sha256Hex, writeFrontendImplementationContractArtifact, } from "../workflows/dag/frontend-implementation-contract.js";
19
- import { checkFrontendSourceFidelityLedger } from "../workflows/dag/frontend-source-fidelity-ledger.js";
20
17
  import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
21
18
  import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
22
19
  import { probeFrontendTestEnvironment } from "../workflows/dag/frontend-test-environment-probe.js";
@@ -25,15 +22,9 @@ import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-ca
25
22
  import { materializeFrontendTestCaseManifest } from "../workflows/dag/frontend-test-case-manifest.js";
26
23
  import { renderFrontendTestHtmlReport } from "../workflows/dag/frontend-test-html-report.js";
27
24
  import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.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
- import { evaluateFrontendDesignPolicy, } from "../workflows/dag/frontend-design-policy.js";
32
- import { deriveFrontendWriterAdmissionShell, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, frontendWriterAdmissionResultV1Schema, } from "../workflows/dag/frontend-writer-admission.js";
33
- import { deriveFrontendIntegrationFacts, buildFrontendTestHandoff, frontendCloseoutFactsSchema, renderFrontendCloseout, } from "../workflows/dag/frontend-closeout.js";
34
- import { buildFrontendHumanDecisionEnvelope, evaluateHighAdmissionReceipt, routeVerificationConfig, } from "../workflows/dag/frontend-human-decision.js";
35
- import { classifyFailureOwner, classifyIssueCategoryRestartPhase, classifyRestartPhase, } from "../workflows/dag/frontend-recovery-capsule.js";
36
- import { pathMatchesPattern } from "../shared/git-progress.js";
25
+ import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
26
+ import { formatFrontendFailureAssessStdout, formatFrontendRepairContractStdout, runFrontendFailureAssessGate, runFrontendRepairContractGate, } from "../workflows/dag/frontend-repair.js";
27
+ import { formatFrontendPrewriteGateStdout, runFrontendPrewriteGate, } from "../workflows/dag/frontend-prewrite-gate.js";
37
28
  import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from "../workflows/dag/frontend-review-context.js";
38
29
  import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
39
30
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
@@ -1239,7 +1230,33 @@ async function executeBackendTestPipeline(input, meta) {
1239
1230
  const pytestTargetsRelative = "reports/backend-test-pytest-targets.txt";
1240
1231
  const pytestTargetsAbsolute = path.join(meta.runDir, pytestTargetsRelative);
1241
1232
  await writeFile(pytestTargetsAbsolute, `${eligibleItemIds.join("\n")}\n`, "utf8");
1242
- const pytestTargetsArg = `@\"\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}\"`;
1233
+ const pytestLauncherRelative = "reports/backend-test-pytest-launcher.py";
1234
+ const pytestLauncherAbsolute = path.join(meta.runDir, pytestLauncherRelative);
1235
+ await writeFile(pytestLauncherAbsolute, [
1236
+ "import os",
1237
+ "import pathlib",
1238
+ "import sys",
1239
+ "",
1240
+ "sys.path[0] = os.getcwd()",
1241
+ "import pytest",
1242
+ "",
1243
+ "if len(sys.argv) != 3:",
1244
+ " print('usage: backend-test-pytest-launcher.py <targets-file> <html-file>', file=sys.stderr)",
1245
+ " raise SystemExit(4)",
1246
+ "targets_path = pathlib.Path(sys.argv[1])",
1247
+ "html_path = pathlib.Path(sys.argv[2])",
1248
+ "try:",
1249
+ " targets = targets_path.read_text(encoding='utf-8').splitlines()",
1250
+ "except (OSError, UnicodeError) as error:",
1251
+ " print(f'backend-test targets unreadable: {error}', file=sys.stderr)",
1252
+ " raise SystemExit(4)",
1253
+ "if not targets or any(not target or target != target.strip() for target in targets):",
1254
+ " print('backend-test targets must contain non-empty exact pytest node IDs', file=sys.stderr)",
1255
+ " raise SystemExit(4)",
1256
+ "pytest_args = [*targets, '-v', '-p', 'no:cacheprovider', f'--html={html_path}', '--self-contained-html']",
1257
+ "raise SystemExit(pytest.main(pytest_args))",
1258
+ "",
1259
+ ].join("\n"), "utf8");
1243
1260
  // Split into short bash -c commands (aligned with markdown-environment).
1244
1261
  // A single ultra-long compound command has been observed on Windows to
1245
1262
  // exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
@@ -1316,10 +1333,11 @@ async function executeBackendTestPipeline(input, meta) {
1316
1333
  'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
1317
1334
  'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
1318
1335
  'echo "STEP=pytest-run"',
1319
- `echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
1336
+ `echo "- pytestCommand: controller launcher -> pytest.main(<${eligibleItemIds.length} eligibility-authorized-items>, -v, -p no:cacheprovider, --html=reports/backend-test.html, --self-contained-html)" >> "\${DIAG_FILE}"`,
1320
1337
  `echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
1321
1338
  `echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
1322
- `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargetsArg} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
1339
+ 'rm -f "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-native.html"',
1340
+ `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" "\${HARNESS_DAG_RUN_DIR}/${pytestLauncherRelative}" "\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}" "\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html"`,
1323
1341
  "STATUS=$?",
1324
1342
  'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
1325
1343
  'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
@@ -1877,45 +1895,6 @@ async function executePipelineCommands(input, meta, overrideCommands) {
1877
1895
  }
1878
1896
  return results;
1879
1897
  }
1880
- /** A+B (AC-008): derive unresolved verification entrypoints from committed plan
1881
- * facts. A verification target is unresolved when its commandLabel does not
1882
- * match any frozen command label from the verify shell evidence bundle.
1883
- * Returns `{ ref, digest }` entries; a missing/unreadable plan ledger fails
1884
- * closed to [] so the router restarts the contract instead of inventing refs. */
1885
- async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandLabels) {
1886
- try {
1887
- const { restorePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
1888
- const records = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
1889
- const patch = restorePlanPatchFromCommittedFacts(records);
1890
- const targets = patch
1891
- ?.verificationTargets;
1892
- if (!Array.isArray(targets))
1893
- return [];
1894
- const frozen = new Set(frozenCommandLabels
1895
- .map((label) => label.trim().toLowerCase())
1896
- .filter(Boolean));
1897
- const refs = [];
1898
- for (const item of targets) {
1899
- const target = item;
1900
- const commandLabel = typeof target.commandLabel === "string" ? target.commandLabel : "";
1901
- if (commandLabel &&
1902
- frozen.has(commandLabel.trim().toLowerCase())) {
1903
- continue;
1904
- }
1905
- const file = typeof target.file === "string" ? target.file : "";
1906
- const symbol = typeof target.symbol === "string" ? target.symbol : "";
1907
- const id = typeof target.id === "string" ? target.id : "";
1908
- const ref = file || symbol || id;
1909
- if (!ref)
1910
- continue;
1911
- refs.push({ ref, digest: sha256Hex(ref) });
1912
- }
1913
- return refs;
1914
- }
1915
- catch {
1916
- return [];
1917
- }
1918
- }
1919
1898
  async function executeFrontendVerificationBundle(input, meta) {
1920
1899
  const started = Date.now();
1921
1900
  const shell = input.task.shell;
@@ -2070,861 +2049,75 @@ async function executeFrontendVerificationBundle(input, meta) {
2070
2049
  catch (error) {
2071
2050
  traceError = error instanceof Error ? error : new Error(String(error));
2072
2051
  }
2073
- const verifyFailed = firstFailure !== undefined || lintSyntheticFailure !== undefined || traceError !== undefined;
2074
- if (verifyFailed) {
2075
- // M8: verify failure is TERMINAL. There is no same-run repair branch and
2076
- // no frontend-repair-assessment.json; we only materialize failure
2077
- // ownership facts (frontend-repair classification + recovery capsule
2078
- // ownership) for the operation controller / recovery run.
2079
- const failedNodeId = input.task.id;
2080
- const failureCategory = firstFailure?.failureCategory ??
2081
- lintSyntheticFailure?.failureCategory ??
2082
- (traceError ? "invalid-output" : undefined) ??
2083
- "invalid-output";
2084
- const failureClass = classifyFrontendFailure({
2085
- nodeId: failedNodeId,
2086
- failureCategory,
2087
- stdout: firstFailure?.stdout,
2088
- stderr: firstFailure?.stderr ?? traceError?.message,
2089
- });
2090
- // A verify failure means the writer's candidate failed verification, so
2091
- // the recovery ownership maps to the implement phase (frontend-recovery
2092
- // capsule classification, fail-closed to implement-pi).
2093
- const classifiedOwner = classifyFailureOwner({
2094
- failureSource: "frontend-verify-shell",
2095
- nodeId: failedNodeId,
2096
- rawFailureCategory: failureCategory,
2097
- });
2098
- const failureOwner = classifiedOwner === "unknown" ? "implement-pi" : classifiedOwner;
2099
- const restartPhase = classifyRestartPhase(failureOwner);
2100
- // A+B (AC-008): verification-config routing. Produce structured
2101
- // unresolved entrypoint refs from committed plan facts and the frozen
2102
- // command labels; a non-empty unresolved set routes to
2103
- // verification-config-decision and materializes the frozen
2104
- // verification-config envelope. An empty/untyped signal fails closed to
2105
- // contract-restart. The decision is recorded so the recovery controller
2106
- // never re-classifies the same signal twice.
2107
- let verificationConfigRoute;
2108
- if (failureOwner === "verification-config") {
2109
- const frozenCommandLabels = [
2110
- ...(bundle.staticEvidence.commandLabels ?? []),
2111
- ...(bundle.behaviorEvidence.commandLabels ?? []),
2112
- ...(bundle.mockEvidence?.commandLabels ?? []),
2113
- ];
2114
- const unresolvedEntrypointRefs = await deriveUnresolvedVerificationEntrypointRefs(meta.runDir, frozenCommandLabels);
2115
- verificationConfigRoute = routeVerificationConfig({
2116
- unresolvedEntrypointRefs: unresolvedEntrypointRefs.map((ref) => ref.ref),
2117
- });
2118
- const allowlistedResolvers = [
2119
- ...new Set(frozenCommandLabels
2120
- .map((label) => label.trim())
2121
- .filter(Boolean)),
2122
- ].sort();
2123
- if (verificationConfigRoute.decision ===
2124
- "verification-config-decision" &&
2125
- unresolvedEntrypointRefs.length > 0 &&
2126
- allowlistedResolvers.length > 0) {
2127
- const envelope = buildFrontendHumanDecisionEnvelope({
2128
- operationId: input.task.id,
2129
- runId: meta.runId,
2130
- operationRevision: 1,
2131
- targetDigest: sha256Hex(frozenCommandLabels.slice().sort().join("\n")),
2132
- staleOnTargetDigestChange: true,
2133
- kind: "verification-config",
2134
- resumeAction: "select-verification-config",
2135
- payload: {
2136
- unresolvedEntrypointRefs,
2137
- allowlistedResolvers,
2138
- },
2139
- });
2140
- await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verification-config-envelope.json", envelope);
2141
- }
2142
- }
2143
- const verifyFailure = {
2144
- schemaVersion: 1,
2145
- schemaId: "frontend-verify-failure-v1",
2146
- nodeId: failedNodeId,
2147
- failureCategory,
2148
- failureClass,
2149
- failureOwner,
2150
- restartPhase,
2151
- ...(verificationConfigRoute
2152
- ? { verificationConfigRoute }
2153
- : {}),
2154
- commandResults,
2155
- failedAt: new Date().toISOString(),
2156
- };
2157
- await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verify-failure.json", verifyFailure);
2158
- return {
2159
- ok: false,
2160
- stdout: summarizeCommandResults(results).stdout,
2161
- stderr: firstFailure?.stderr ||
2162
- lintSyntheticFailure?.stderr ||
2163
- traceError?.message ||
2164
- "frontend verification failed",
2165
- failureCategory,
2166
- durationMs: Date.now() - started,
2167
- ...{ commandResults },
2168
- };
2169
- }
2170
- // A+B (AC-008): when lint is `require-clean` but the assessment proves
2171
- // baseline-debt, materialize the frozen `baseline-debt-acceptance` envelope
2172
- // whose only positive resume is `accept-baseline-debt`. Evidence stays
2173
- // `baseline-debt` and is never rewritten to `passed`.
2174
- if (lintAssessment?.status === "baseline-debt") {
2175
- const baselineEvidenceRefs = (lintAssessment.rawEvidenceRefs ?? [])
2176
- .filter((ref) => ref?.path && ref?.sha256)
2177
- .map((ref) => ({ ref: ref.path, digest: ref.sha256 }));
2178
- if (baselineEvidenceRefs.length > 0) {
2179
- const envelope = buildFrontendHumanDecisionEnvelope({
2180
- operationId: input.task.id,
2181
- runId: meta.runId,
2182
- operationRevision: 1,
2183
- targetDigest: lintAssessment.commandIdentity.sha256,
2184
- staleOnTargetDigestChange: true,
2185
- kind: "baseline-debt-acceptance",
2186
- resumeAction: "accept-baseline-debt",
2187
- payload: {
2188
- baselineEvidenceRefs,
2189
- candidateDigest: lintAssessment.commandIdentity.sha256,
2190
- commandDigest: lintAssessment.commandIdentity.sha256,
2191
- resultingPolicy: "allow-non-regression",
2192
- },
2193
- });
2194
- await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-baseline-debt-acceptance-envelope.json", envelope);
2195
- }
2196
- }
2197
- return {
2198
- ok: true,
2199
- stdout: lintAssessment?.status === "baseline-debt"
2200
- ? "Frontend verification bundle: pass with lint baseline-debt"
2201
- : "Frontend verification bundle: pass",
2202
- stderr: "",
2203
- failureCategory: "success",
2204
- durationMs: Date.now() - started,
2205
- ...{ commandResults },
2206
- };
2207
- }
2208
- /* ------------------------------------------------------------------ *
2209
- * M8 deterministic frontend shell nodes: design policy, writer *
2210
- * admission, closeout. These replace the prewrite-gate authorization *
2211
- * path and the Pi closeout node. *
2212
- * ------------------------------------------------------------------ */
2213
- async function readRunDirJson(runDir, relativePath) {
2214
- return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
2215
- }
2216
- /** Committed typed review terminal kinds from the review node's typed event
2217
- * store (AC-003: typed terminal tools are the only authoritative verdict). */
2218
- async function collectReviewTerminalKinds(runDir, reviewNodeId) {
2219
- try {
2220
- const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
2221
- return records
2222
- .filter((record) => record.phase === "committed")
2223
- .map((record) => {
2224
- const kind = record.fact.kind;
2225
- return typeof kind === "string" ? kind : "";
2226
- })
2227
- .filter(Boolean);
2228
- }
2229
- catch {
2230
- return [];
2231
- }
2232
- }
2233
- /** Committed typed design terminal kinds from the design review node's typed
2234
- * event store (M8: the committed approve_design / request_design_changes fact
2235
- * is the only authoritative verdict; the legacy first-line VERDICT text is
2236
- * never read for writer admission). Fail-closed on a missing/unreadable store. */
2237
- async function collectDesignTerminalKinds(runDir, designNodeId) {
2238
- try {
2239
- const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2240
- return records
2241
- .filter((record) => record.phase === "committed")
2242
- .map((record) => {
2243
- const kind = record.fact.kind;
2244
- return typeof kind === "string" ? kind : "";
2245
- })
2246
- .filter(Boolean);
2247
- }
2248
- catch {
2249
- return [];
2250
- }
2251
- }
2252
- /** A+B (AC-009): deterministic §4.10 restart phase for a committed
2253
- * `request_review_changes` fact. The typed issueCategory is mapped through the
2254
- * frozen classifier; `unknown` (or an unreadable store) fails closed to
2255
- * `undefined` so the runtime never trusts an untyped category. */
2256
- async function collectReviewRestartPhase(runDir, reviewNodeId) {
2257
- try {
2258
- const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
2259
- const request = records.find((record) => record.phase === "committed" &&
2260
- record.fact.kind === "request_review_changes");
2261
- const issueCategory = request?.fact
2262
- ?.issueCategory;
2263
- return typeof issueCategory === "string"
2264
- ? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
2265
- : undefined;
2266
- }
2267
- catch {
2268
- return undefined;
2269
- }
2270
- }
2271
- /** A+B (AC-009): deterministic §4.10 restart phase for a committed
2272
- * `request_design_changes` fact. Mirrors the review collector. */
2273
- /** Number of findings attached to the committed request_design_changes
2274
- * terminal fact (0 when absent) — used for the admission advisory. */
2275
- async function collectDesignReviewFindingCount(runDir, designNodeId) {
2276
- try {
2277
- const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2278
- const request = records.find((record) => record.phase === "committed" &&
2279
- record.fact.kind ===
2280
- "request_design_changes");
2281
- const findings = (request?.fact).findings;
2282
- return Array.isArray(findings) ? findings.length : 0;
2283
- }
2284
- catch {
2285
- return 0;
2286
- }
2287
- }
2288
- async function collectDesignRestartPhase(runDir, designNodeId) {
2289
- try {
2290
- const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2291
- const request = records.find((record) => record.phase === "committed" &&
2292
- record.fact.kind === "request_design_changes");
2293
- const issueCategory = request?.fact
2294
- ?.issueCategory;
2295
- return typeof issueCategory === "string"
2296
- ? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
2297
- : undefined;
2298
- }
2299
- catch {
2300
- return undefined;
2301
- }
2302
- }
2303
- /**
2304
- * Resolve the deterministic runtime skeleton for the frontend plan patch from
2305
- * the DAG spec. The plan task carries it via `structuredContractOutput.skeleton`
2306
- * (the only schema-recognized persistent channel); absence is a fail-closed
2307
- * generation/configuration defect, never a reason to fall back to text.
2308
- */
2309
- function resolveFrontendContractSkeleton(spec, planNodeId) {
2310
- const planTask = spec.tasks.find((task) => task.id === planNodeId);
2311
- const skeleton = planTask?.structuredContractOutput?.skeleton;
2312
- if (!skeleton || Object.keys(skeleton).length === 0) {
2313
- throw new Error(`frontend design policy blocked: missing runtime contract skeleton for ${planNodeId}`);
2314
- }
2315
- return skeleton;
2316
- }
2317
- async function collectFrontendSourceFreshness(input) {
2318
- const entries = [];
2319
- for (const source of input.sourceBinding?.sources ?? []) {
2320
- entries.push({ path: source.path, sha256: source.sha256, fresh: true });
2321
- }
2322
- for (const evidencePath of input.contract.designEvidence.paths) {
2323
- if (entries.some((entry) => entry.path === evidencePath))
2324
- continue;
2325
- try {
2326
- const bytes = await readFile(path.join(input.workspaceRoot, evidencePath));
2327
- entries.push({
2328
- path: evidencePath,
2329
- sha256: createHash("sha256").update(bytes).digest("hex"),
2330
- fresh: true,
2331
- });
2332
- }
2333
- catch {
2334
- entries.push({
2335
- path: evidencePath,
2336
- sha256: "0".repeat(64),
2337
- fresh: false,
2338
- });
2339
- }
2340
- }
2341
- return entries;
2342
- }
2343
- /** R3/R4 openspec invariants: frozen candidates must exist and content
2344
- * snapshots must not have drifted. Citation is enforced upstream by the
2345
- * prewrite gate read-event evidence and ledger specReference conformance. */
2346
- async function checkFrontendOpenspecFacts(input) {
2347
- const frozenCandidatePaths = input.config.openspecCandidatePaths ?? [];
2348
- const candidateRoot = input.workspaceRoot;
2349
- const missingCandidates = [];
2350
- for (const candidate of frozenCandidatePaths) {
2351
- try {
2352
- await stat(path.join(candidateRoot, candidate));
2353
- }
2354
- catch {
2355
- missingCandidates.push(candidate);
2356
- }
2357
- }
2358
- if (missingCandidates.length > 0) {
2359
- throw new Error(`frontend design policy blocked: openspec candidate paths frozen at generation time no longer exist on disk: ${missingCandidates.join(", ")}`);
2360
- }
2361
- const snapshots = input.config.openspecCandidateSnapshots;
2362
- if (snapshots && snapshots.length > 0) {
2363
- for (const snapshot of snapshots) {
2364
- try {
2365
- const bytes = await readFile(path.join(candidateRoot, snapshot.path));
2366
- const actual = createHash("sha256").update(bytes).digest("hex");
2367
- if (actual !== snapshot.sha256) {
2368
- throw new Error(`frontend design policy blocked: openspec candidate content hash drifted: ${snapshot.path}`);
2369
- }
2370
- }
2371
- catch (error) {
2372
- if (error instanceof Error &&
2373
- error.message.startsWith("frontend design policy blocked")) {
2374
- throw error;
2375
- }
2376
- throw new Error(`frontend design policy blocked: openspec candidate unreadable: ${snapshot.path}`);
2377
- }
2378
- }
2379
- }
2380
- }
2381
- async function executeFrontendDesignPolicy(input, meta) {
2382
- const started = Date.now();
2383
- const shell = input.task.shell;
2384
- const config = shell.frontendDesignPolicy;
2385
- try {
2386
- // 1. Materialize the canonical contract from committed typed facts. After
2387
- // M9a the plan node's assistantText is a pure Markdown narrative; the
2388
- // compile authority is the committed plan ledger ⊕ runtime skeleton.
2389
- // Fail closed on missing committed facts — never fall back to text.
2390
- const { readCommittedOriginFacts, checkCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
2391
- // A+B (AC-001/003): fail-closed committed contract/scout typed facts.
2392
- // Missing/uncommitted facts block here; never fall back to text parsing.
2393
- const committedContractFacts = await readCommittedOriginFacts(meta.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
2394
- const contractCheck = checkCommittedOriginFacts({
2395
- records: committedContractFacts,
2396
- origin: "contract",
2397
- });
2398
- if (!contractCheck.ok) {
2052
+ if (bundle.mode === "repair") {
2053
+ if (firstFailure || lintSyntheticFailure || traceError) {
2399
2054
  return {
2400
2055
  ok: false,
2401
- stdout: "",
2402
- stderr: `frontend design policy blocked: ${contractCheck.reason}`,
2403
- failureCategory: "invalid-output",
2404
- durationMs: Date.now() - started,
2405
- };
2406
- }
2407
- const committedScoutFacts = await readCommittedOriginFacts(meta.runDir, "frontend-scout-pi", "scout-typed-facts.jsonl");
2408
- const scoutCheck = checkCommittedOriginFacts({
2409
- records: committedScoutFacts,
2410
- origin: "scout",
2411
- });
2412
- if (!scoutCheck.ok) {
2413
- return {
2414
- ok: false,
2415
- stdout: "",
2416
- stderr: `frontend design policy blocked: ${scoutCheck.reason}`,
2417
- failureCategory: "invalid-output",
2418
- durationMs: Date.now() - started,
2419
- };
2420
- }
2421
- const skeleton = resolveFrontendContractSkeleton(meta.spec, config.planFromNodeId);
2422
- const assembled = await buildContractFromCommittedFacts({
2423
- runDir: meta.runDir,
2424
- planNodeId: config.planFromNodeId,
2425
- contractNodeId: "frontend-contract-pi",
2426
- sourceBinding: meta.spec.sourceBinding,
2427
- skeleton,
2428
- });
2429
- if (!assembled.ok) {
2430
- return {
2431
- ok: false,
2432
- stdout: "",
2433
- stderr: `frontend design policy blocked: ${assembled.reason}`,
2434
- failureCategory: "invalid-output",
2435
- durationMs: Date.now() - started,
2436
- };
2437
- }
2438
- const contract = assembled.canonical;
2439
- // 2. Requirement id retention (R11): search the canonical contract
2440
- // serialization instead of the plan node narrative.
2441
- const contractText = serializeCompactDeterministicJson(contract);
2442
- const missingIds = config.requiredRequirementIds.filter((id) => !contractText.includes(id));
2443
- if (missingIds.length > 0) {
2444
- return {
2445
- ok: false,
2446
- stdout: "",
2447
- stderr: `frontend design policy blocked: missing requirement ids: ${missingIds.join(", ")}`,
2448
- failureCategory: "invalid-output",
2449
- durationMs: Date.now() - started,
2450
- };
2451
- }
2452
- // 3. Mock strategy allowed + frozen command binding (R1).
2453
- const mockStrategy = contract.mockApi.strategy;
2454
- if (!config.allowedMockStrategies.includes(mockStrategy)) {
2455
- return {
2456
- ok: false,
2457
- stdout: "",
2458
- stderr: `frontend contract mock strategy "${mockStrategy}" is incompatible with the generated allowed strategies [${config.allowedMockStrategies.join(", ")}]`,
2459
- failureCategory: "invalid-output",
2460
- durationMs: Date.now() - started,
2461
- };
2462
- }
2463
- if (mockStrategy !== "not-needed" && config.mockCommandLabels.length === 0) {
2464
- return {
2465
- ok: false,
2466
- stdout: "",
2467
- stderr: `frontend design policy blocked: contract selected Mock strategy ${mockStrategy} but the DAG has no authorized Mock verification commands`,
2468
- failureCategory: "invalid-output",
2469
- durationMs: Date.now() - started,
2470
- };
2471
- }
2472
- // 4. writeSet containment.
2473
- if (config.implementationWriteSet) {
2474
- const writeSet = new Set(config.implementationWriteSet);
2475
- const uncoveredTargets = contract.targets.files.filter((target) => ![...writeSet].some((pattern) => pathMatchesPattern(target, pattern) || target === pattern));
2476
- if (uncoveredTargets.length > 0) {
2477
- return {
2478
- ok: false,
2479
- stdout: "",
2480
- stderr: `frontend design policy blocked: contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`,
2481
- failureCategory: "invalid-output",
2482
- durationMs: Date.now() - started,
2483
- };
2484
- }
2485
- const uncoveredVTs = contract.verificationTargets
2486
- .filter((vt) => vt.type !== "static")
2487
- .filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
2488
- if (uncoveredVTs.length > 0) {
2489
- return {
2490
- ok: false,
2491
- stdout: "",
2492
- stderr: `frontend design policy blocked: verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`,
2493
- failureCategory: "invalid-output",
2494
- durationMs: Date.now() - started,
2495
- };
2496
- }
2497
- }
2498
- // 5. Source freshness (R4) + freshness entries.
2499
- if (config.requireSourceFreshness) {
2500
- if (!meta.spec.sourceBinding) {
2501
- throw new Error("frontend design policy requires sourceBinding for freshness check");
2502
- }
2503
- await assertFrontendSourceBindingFresh({
2504
- workspaceRoot: input.cwd,
2505
- binding: meta.spec.sourceBinding,
2506
- });
2507
- }
2508
- // Byte freshness is necessary but not sufficient: require the same ledger
2509
- // semantic checks as the shadow prewrite gate before production writer
2510
- // admission can be materialized.
2511
- if (meta.spec.sourceBinding) {
2512
- const ledgerCheck = await checkFrontendSourceFidelityLedger({
2513
- workspaceRoot: input.cwd,
2514
- binding: meta.spec.sourceBinding,
2515
- contractRequirements: contract.requirements.map((requirement) => ({
2516
- id: requirement.id,
2517
- sourceFragmentIds: requirement.sourceFragmentIds,
2518
- sourceRefs: requirement.sourceRefs,
2519
- })),
2520
- });
2521
- if (!ledgerCheck.ok) {
2522
- return {
2523
- ok: false,
2524
- stdout: "",
2525
- stderr: `frontend design policy blocked: ${ledgerCheck.failureReason ?? "source fidelity ledger validation failed"}`,
2526
- failureCategory: "invalid-output",
2527
- durationMs: Date.now() - started,
2528
- };
2529
- }
2530
- }
2531
- const sourceFreshness = await collectFrontendSourceFreshness({
2532
- workspaceRoot: input.cwd,
2533
- sourceBinding: meta.spec.sourceBinding,
2534
- contract,
2535
- });
2536
- // 6. Openspec invariants (R3): frozen candidates exist and snapshots have
2537
- // not drifted. Citation is superseded by prewrite-gate read evidence.
2538
- await checkFrontendOpenspecFacts({
2539
- workspaceRoot: input.cwd,
2540
- config,
2541
- });
2542
- // 7. Deterministic design policy (M8 D2: designApproval is optional here;
2543
- // the design verdict is enforced by the writer-admission shell).
2544
- const policyResult = evaluateFrontendDesignPolicy({
2545
- contract,
2546
- allowedMockStrategies: config.allowedMockStrategies,
2547
- sourceFreshness,
2548
- componentSpecCandidatePaths: config.openspecCandidatePaths ??
2549
- config.componentSpecCandidatePaths ??
2550
- [],
2551
- allowedDependencies: config.allowedDependencies,
2552
- writeSetPatterns: config.implementationWriteSet ?? [],
2553
- });
2554
- // 8. Write canonical contract + policy result.
2555
- const contractArtifact = await writeFrontendImplementationContractArtifact({
2556
- runDir: meta.runDir,
2557
- outputDir: config.outputDir,
2558
- artifactName: config.artifactName,
2559
- canonical: contract,
2560
- });
2561
- await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-design-policy-result.json", policyResult);
2562
- if (policyResult.classification === "blocked") {
2563
- return {
2564
- ok: false,
2565
- stdout: "",
2566
- stderr: `frontend design policy blocked: ${policyResult.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
2567
- failureCategory: "invalid-output",
2056
+ stdout: summarizeCommandResults(results).stdout,
2057
+ stderr: firstFailure?.stderr ||
2058
+ lintSyntheticFailure?.stderr ||
2059
+ traceError?.message ||
2060
+ "frontend reverify failed",
2061
+ failureCategory: firstFailure?.failureCategory ??
2062
+ lintSyntheticFailure?.failureCategory ??
2063
+ "invalid-output",
2568
2064
  durationMs: Date.now() - started,
2065
+ ...{ commandResults },
2569
2066
  };
2570
2067
  }
2571
2068
  return {
2572
2069
  ok: true,
2573
- stdout: [
2574
- "Frontend design policy: approved findings=0",
2575
- `Contract: ${contractArtifact.path}`,
2576
- `Contract index: ${contractArtifact.reviewIndexPath}`,
2577
- `Capacity diagnostic: ${contractArtifact.capacityDiagnosticPath} (non-blocking)`,
2578
- ].join("\n"),
2070
+ stdout: lintAssessment?.status === "baseline-debt"
2071
+ ? "Frontend reverify bundle: pass with lint baseline-debt"
2072
+ : "Frontend reverify bundle: pass",
2579
2073
  stderr: "",
2580
2074
  failureCategory: "success",
2581
2075
  durationMs: Date.now() - started,
2076
+ ...{ commandResults },
2582
2077
  };
2583
2078
  }
2584
- catch (error) {
2585
- return {
2586
- ok: false,
2587
- stdout: "",
2588
- stderr: error instanceof Error ? error.message : String(error),
2589
- failureCategory: "invalid-output",
2590
- durationMs: Date.now() - started,
2591
- };
2592
- }
2593
- }
2594
- async function executeFrontendWriterAdmission(input, meta) {
2595
- const started = Date.now();
2596
- const shell = input.task.shell;
2597
- const config = shell.frontendWriterAdmission;
2598
- try {
2599
- // 1. Design verdict handling (r12 policy change). The typed terminal
2600
- // verdict is read from the committed typed design terminal fact, not
2601
- // from the legacy first-line VERDICT text protocol (AC-003/AC-004).
2602
- // A request_design_changes verdict no longer blocks writer admission:
2603
- // it is downgraded to an advisory that rides along in the admission
2604
- // receipt (and reaches implement via the review node's upstream
2605
- // context). Blocking is owned by the deterministic gates (prewrite,
2606
- // write guard) and the final code review. A missing typed terminal
2607
- // verdict still fails closed. In small topology the design review node
2608
- // is absent and the check is vacuous.
2609
- let verdictPass = true;
2610
- let designAdvisory;
2611
- if (config.designReviewFromNodeId) {
2612
- const designKinds = await collectDesignTerminalKinds(meta.runDir, config.designReviewFromNodeId);
2613
- if (designKinds.length !== 1) {
2614
- verdictPass = false;
2615
- }
2616
- else if (designKinds[0] !== "approve_design") {
2617
- designAdvisory = {
2618
- verdict: designKinds[0],
2619
- restartPhase: await collectDesignRestartPhase(meta.runDir, config.designReviewFromNodeId),
2620
- findingCount: await collectDesignReviewFindingCount(meta.runDir, config.designReviewFromNodeId),
2621
- };
2622
- }
2623
- }
2624
- // 2. Read the canonical contract + design-policy result.
2625
- const contractRaw = await readRunDirJson(meta.runDir, "contracts/frontend-implementation-contract.json");
2626
- const contract = frontendImplementationContractSchema.parse(contractRaw);
2627
- const policyRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-design-policy-result.json"));
2628
- if (policyRaw.schemaId !== "frontend-design-policy-v1") {
2629
- throw new Error("frontend writer admission: design policy result artifact invalid");
2630
- }
2631
- const designPolicyResult = {
2632
- schemaId: "frontend-design-policy-v1",
2633
- classification: policyRaw.classification === "approved" ? "approved" : "blocked",
2634
- findings: policyRaw.findings ?? [],
2635
- digest: "",
2636
- };
2637
- // 3. Freeze the pre-writer worktree baseline (cumulative diff baseline).
2638
- await captureFrontendWorktreeBaseline({
2639
- runDir: meta.runDir,
2640
- workspaceRoot: input.cwd,
2641
- });
2642
- const baselineRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-worktree-baseline.json"));
2643
- const baseline = (baselineRaw.files ?? [])
2644
- .map((file) => ({
2645
- path: file,
2646
- sha256: baselineRaw.hashes?.[file] ?? null,
2647
- }))
2648
- .filter((entry) => entry.sha256 !== null);
2649
- // 4. Writer-preceding lint baseline (R5) when lint is configured.
2650
- let lintResults = [];
2651
- if ((config.lintCommands?.length ?? 0) > 0) {
2652
- for (const command of config.lintCommands) {
2653
- const commandNumber = lintResults.length + 1;
2654
- const result = await executeShellCommand({
2655
- command,
2656
- cwd: input.cwd,
2657
- timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
2658
- envAllowlist: shell.envAllowlist,
2659
- dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2660
- reportActivity: input.reportActivity,
2661
- outputArtifacts: {
2662
- stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2663
- stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
2664
- },
2665
- });
2666
- lintResults.push(result);
2667
- }
2668
- await materializeFrontendLintBaseline({
2669
- runDir: meta.runDir,
2670
- workspaceRoot: input.cwd,
2671
- commands: config.lintCommands,
2672
- results: lintResults,
2673
- worktreeChanged: false,
2674
- });
2675
- }
2676
- // 5. Derive admission (writeSet + digests + findings). Glob entries in
2677
- // the contract targets (e.g. `src/**` inherited from the skeleton) are
2678
- // expanded against the concrete workspace files so the derived writeSet
2679
- // contains only real paths the writer can touch.
2680
- const workspaceFiles = await collectWorkspaceFiles(input.cwd);
2681
- const admission = deriveFrontendWriterAdmissionShell({
2682
- contract,
2683
- designPolicyResult,
2684
- baseline,
2685
- frozenCommandLabels: config.frozenCommandLabels,
2686
- allowedMockStrategies: config.allowedMockStrategies,
2687
- workspaceFiles,
2688
- });
2689
- // 6. Design verdict enforcement: request-revision is a blocked admission.
2690
- const failureSource = policyRaw.classification === "blocked"
2691
- ? "frontend-design-policy-shell"
2692
- : "frontend-writer-admission-shell";
2693
- const findings = verdictPass
2694
- ? admission.findings
2695
- : [
2696
- ...admission.findings,
2697
- {
2698
- code: "design-approval-missing",
2699
- message: "design review typed terminal verdict missing or ambiguous (fail closed)",
2700
- },
2701
- ];
2702
- const classification = findings.length === 0 && admission.classification === "accepted"
2703
- ? admission.classification
2704
- : findings.length === 0 &&
2705
- admission.classification === "requires-human-approval"
2706
- ? "requires-human-approval"
2707
- : "blocked";
2708
- const result = frontendWriterAdmissionResultV1Schema.parse({
2709
- schemaVersion: 1,
2710
- schemaId: "frontend-writer-admission-shell-v1",
2711
- classification,
2712
- writeSet: admission.writeSet,
2713
- writeSetDigest: admission.writeSetDigest,
2714
- baselineDigest: admission.baselineDigest,
2715
- admissionDigest: admission.admissionDigest,
2716
- ...(admission.baselineDebtPolicy !== undefined
2717
- ? { baselineDebtPolicy: admission.baselineDebtPolicy }
2718
- : {}),
2719
- findings,
2720
- ...(designAdvisory ? { designReviewAdvisory: designAdvisory } : {}),
2721
- failureSource,
2079
+ const failureFacts = [];
2080
+ if (firstFailure || lintSyntheticFailure) {
2081
+ const failureStdout = firstFailure
2082
+ ? [firstFailure.command, firstFailure.stdout].filter(Boolean).join("\n")
2083
+ : "";
2084
+ failureFacts.push({
2085
+ nodeId: input.task.id,
2086
+ record: {
2087
+ status: "FINISHED",
2088
+ failureCategory: firstFailure?.failureCategory ??
2089
+ lintSyntheticFailure?.failureCategory,
2090
+ // Keep classification scoped to the failed command. Aggregate
2091
+ // successful output may contain unrelated writeSet-like JSON.
2092
+ stdout: failureStdout,
2093
+ stderr: firstFailure?.stderr ?? lintSyntheticFailure?.stderr,
2094
+ commandResults,
2095
+ },
2722
2096
  });
2723
- await writeDagRunJsonArtifact(meta.runDir, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, result);
2724
- // A+B (AC-007): High topology materializes a `high-admission-receipt-v1`
2725
- // catalog whose candidate digest must match the admission digest before
2726
- // the gate can transition from `requires-human-approval` to `accepted`.
2727
- // Any digest drift resolves to `stale` (never silently accepted).
2728
- if (classification === "requires-human-approval") {
2729
- const receipt = evaluateHighAdmissionReceipt({
2730
- candidateDigest: result.admissionDigest,
2731
- admissionDigest: result.admissionDigest,
2732
- });
2733
- await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-high-admission-receipt.json", receipt);
2734
- if (receipt.status !== "accepted") {
2735
- return {
2736
- ok: false,
2737
- stdout: "",
2738
- stderr: `frontend writer admission stale: high-admission receipt candidate digest drifted from admission digest`,
2739
- failureCategory: "invalid-output",
2740
- durationMs: Date.now() - started,
2741
- };
2742
- }
2743
- }
2744
- if (classification === "blocked") {
2745
- return {
2746
- ok: false,
2747
- stdout: "",
2748
- stderr: `frontend writer admission ${classification}: ${findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
2749
- failureCategory: "invalid-output",
2750
- durationMs: Date.now() - started,
2751
- };
2752
- }
2753
- return {
2754
- ok: true,
2755
- stdout: `Frontend writer admission: ${classification} writeSet=${result.writeSet.length} writeSetDigest=${result.writeSetDigest}`,
2756
- stderr: "",
2757
- failureCategory: "success",
2758
- durationMs: Date.now() - started,
2759
- };
2760
- }
2761
- catch (error) {
2762
- return {
2763
- ok: false,
2764
- stdout: "",
2765
- stderr: error instanceof Error ? error.message : String(error),
2766
- failureCategory: "invalid-output",
2767
- durationMs: Date.now() - started,
2768
- };
2769
- }
2770
- }
2771
- async function executeFrontendContractImport(input, meta) {
2772
- const started = Date.now();
2773
- const shell = input.task.shell;
2774
- const config = shell.frontendContractImport;
2775
- try {
2776
- const binding = meta.spec.sourceBinding;
2777
- if (!binding) {
2778
- throw new Error("frontend contract-import requires a DAG sourceBinding to re-validate freshness");
2779
- }
2780
- if (config.requireSourceFreshness) {
2781
- await assertFrontendSourceBindingFresh({
2782
- workspaceRoot: input.cwd,
2783
- binding,
2784
- });
2785
- }
2786
- const digest = createHash("sha256")
2787
- .update(JSON.stringify({
2788
- taskId: binding.taskId,
2789
- sources: binding.sources,
2790
- artifactName: config.artifactName,
2791
- }))
2792
- .digest("hex");
2793
- const facts = {
2794
- schemaVersion: 1,
2795
- schemaId: "frontend-contract-import-v1",
2796
- artifactName: config.artifactName,
2797
- sourceBindingTaskId: binding.taskId,
2798
- sourceCount: binding.sources.length,
2799
- fresh: true,
2800
- digest,
2801
- };
2802
- return {
2803
- ok: true,
2804
- stdout: JSON.stringify(facts, null, 2),
2805
- stderr: "",
2806
- failureCategory: "success",
2807
- durationMs: Date.now() - started,
2808
- };
2809
- }
2810
- catch (error) {
2811
- return {
2812
- ok: false,
2813
- stdout: "",
2814
- stderr: error instanceof Error ? error.message : String(error),
2815
- failureCategory: "invalid-output",
2816
- durationMs: Date.now() - started,
2817
- };
2818
2097
  }
2819
- }
2820
- async function executeFrontendCloseout(input, meta) {
2821
- const started = Date.now();
2822
- const shell = input.task.shell;
2823
- const config = shell.frontendCloseout;
2824
- try {
2825
- const context = (await readRunDirJson(meta.runDir, "contracts/frontend-review-context.json"));
2826
- if (context.contractRef?.path !==
2827
- "contracts/frontend-implementation-contract.json") {
2828
- throw new Error("frontend closeout missing canonical contract reference");
2829
- }
2830
- const contractRaw = await readFile(path.join(meta.runDir, context.contractRef.path), "utf8");
2831
- const contractSha256 = createHash("sha256")
2832
- .update(contractRaw)
2833
- .digest("hex");
2834
- if (contractSha256 !== context.contractRef.sha256) {
2835
- throw new Error("frontend closeout canonical contract hash mismatch");
2836
- }
2837
- const contract = frontendImplementationContractSchema.parse(JSON.parse(contractRaw));
2838
- const trace = context.verificationTrace;
2839
- const verificationTargets = (trace?.targets ?? []).map((target) => ({
2840
- status: target.status === "ok" ? "ok" : "failed",
2841
- }));
2842
- const mockEvidenceBound = contract.mockApi.strategy !== "not-needed" &&
2843
- typeof trace?.mockCommandNodeId === "string" &&
2844
- trace.mockCommandNodeId.length > 0 &&
2845
- Array.isArray(trace?.mockCommandLabels) &&
2846
- trace.mockCommandLabels.length > 0 &&
2847
- Array.isArray(trace?.mockCommandTexts) &&
2848
- trace.mockCommandTexts.length === trace.mockCommandLabels.length;
2849
- const integrationFacts = deriveFrontendIntegrationFacts({
2850
- mockStrategy: contract.mockApi.strategy,
2851
- realApiEvidence: { present: false, passed: false },
2852
- mockEvidenceBound,
2853
- verificationTargets,
2854
- });
2855
- // Typed review terminal (AC-003) from the committed typed event store.
2856
- const typedKinds = await collectReviewTerminalKinds(meta.runDir, config.reviewFromNodeId);
2857
- const typedVerdict = typedKinds.length === 1 &&
2858
- (typedKinds[0] === "approve_review" || typedKinds[0] === "request_review_changes")
2859
- ? typedKinds[0]
2860
- : null;
2861
- // A+B (AC-009): request_review_changes issueCategory maps to the
2862
- // deterministic §4.10 restart phase. Recorded as a run-owned audit
2863
- // artifact so the frozen closeout schema is never widened; `unknown`
2864
- // fails closed to no restart phase.
2865
- const reviewRestartPhase = typedVerdict === "request_review_changes"
2866
- ? await collectReviewRestartPhase(meta.runDir, config.reviewFromNodeId)
2867
- : undefined;
2868
- if (reviewRestartPhase !== undefined) {
2869
- await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-review-restart-phase.json", {
2870
- schemaVersion: 1,
2871
- nodeId: config.reviewFromNodeId,
2872
- restartPhase: reviewRestartPhase,
2873
- });
2874
- }
2875
- const facts = frontendCloseoutFactsSchema.parse({
2876
- schemaVersion: 1,
2877
- schemaId: "frontend-closeout-facts-v1",
2878
- coverageMatrix: verificationTargets.map((target, index) => ({
2879
- verificationTargetId: trace?.targets?.[index]?.id ?? `vt-${index}`,
2880
- status: target.status === "ok" ? "passed" : "failed",
2881
- })),
2882
- lintStatus: "unavailable",
2883
- integrationFacts,
2884
- reviewApproval: {
2885
- typedVerdict,
2886
- legacyVerdict: null,
2887
- match: true,
2888
- },
2889
- cumulativeDiff: {
2890
- cumulativeDigest: (typeof context.verificationTrace === "object" &&
2891
- context.verificationTrace !== null &&
2892
- "schemaId" in context.verificationTrace
2893
- ? deterministicSha256({ verificationTrace: context.verificationTrace })
2894
- : deterministicSha256({})),
2895
- empty: false,
2098
+ if (traceError) {
2099
+ failureFacts.push({
2100
+ nodeId: "frontend-verification-trace-shell",
2101
+ record: {
2102
+ status: "FINISHED",
2103
+ failureCategory: "invalid-output",
2104
+ stderr: traceError.message,
2896
2105
  },
2897
- notRun: { browser: "not-run", visual: "not-run" },
2898
- pendingRealIntegration: integrationFacts !== "real-api",
2899
2106
  });
2900
- const rendered = renderFrontendCloseout(facts);
2901
- await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-closeout-facts.json", facts);
2902
- // A+B (AC-010): materialize the deterministic frontend-test handoff.
2903
- // browser/visual/runtime-a11y are always `not-run` and the handoff is
2904
- // always `deferred-to-frontend-test`, so it never blocks implementation
2905
- // and can never be rewritten into a passed result.
2906
- const handoffTaskId = meta.spec.sourceBinding?.taskId ?? input.task.id;
2907
- const sourceDigest = meta.spec.sourceBinding
2908
- ? createHash("sha256")
2909
- .update(JSON.stringify({
2910
- taskId: meta.spec.sourceBinding.taskId,
2911
- sources: meta.spec.sourceBinding.sources,
2912
- }))
2913
- .digest("hex")
2914
- : deterministicSha256({});
2915
- const handoff = buildFrontendTestHandoff({
2916
- taskId: handoffTaskId,
2917
- sourceDigest,
2918
- candidateHash: facts.cumulativeDiff.cumulativeDigest,
2107
+ }
2108
+ try {
2109
+ const assessment = await runFrontendFailureAssessGate({
2110
+ runDir: meta.runDir,
2111
+ failureFacts,
2919
2112
  });
2920
- await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-test-handoff.json", handoff);
2921
- await writeDagNodeTextArtifact(meta.runDir, input.task.id, "frontend-closeout.md", rendered);
2113
+ await runFrontendRepairContractGate({ runDir: meta.runDir });
2922
2114
  return {
2923
2115
  ok: true,
2924
- stdout: `Frontend closeout rendered\n${rendered.split("\n").slice(0, 8).join("\n")}`,
2116
+ stdout: formatFrontendFailureAssessStdout(assessment),
2925
2117
  stderr: "",
2926
2118
  failureCategory: "success",
2927
2119
  durationMs: Date.now() - started,
2120
+ ...{ commandResults },
2928
2121
  };
2929
2122
  }
2930
2123
  catch (error) {
@@ -2934,6 +2127,7 @@ async function executeFrontendCloseout(input, meta) {
2934
2127
  stderr: error instanceof Error ? error.message : String(error),
2935
2128
  failureCategory: "invalid-output",
2936
2129
  durationMs: Date.now() - started,
2130
+ ...{ commandResults },
2937
2131
  };
2938
2132
  }
2939
2133
  }
@@ -3258,6 +2452,68 @@ async function executeFrontendTestReports(input, meta) {
3258
2452
  };
3259
2453
  }
3260
2454
  }
2455
+ async function executeFrontendLintBaseline(input, meta) {
2456
+ const started = Date.now();
2457
+ const shell = input.task.shell;
2458
+ const baseline = shell.frontendLintBaseline;
2459
+ const cwd = resolveShellCwd(input.cwd, shell.cwd);
2460
+ let beforeStatus;
2461
+ try {
2462
+ beforeStatus = await readGitStatusPorcelain(input.cwd);
2463
+ }
2464
+ catch {
2465
+ beforeStatus = undefined;
2466
+ }
2467
+ const results = [];
2468
+ for (const command of baseline.lintCommands) {
2469
+ const commandNumber = results.length + 1;
2470
+ results.push(await executeShellCommand({
2471
+ command,
2472
+ cwd,
2473
+ timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
2474
+ envAllowlist: shell.envAllowlist,
2475
+ dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2476
+ outputArtifacts: {
2477
+ stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2478
+ stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
2479
+ },
2480
+ }));
2481
+ }
2482
+ let afterStatus;
2483
+ try {
2484
+ afterStatus = await readGitStatusPorcelain(input.cwd);
2485
+ }
2486
+ catch {
2487
+ afterStatus = undefined;
2488
+ }
2489
+ try {
2490
+ const materialized = await materializeFrontendLintBaseline({
2491
+ runDir: meta.runDir,
2492
+ workspaceRoot: input.cwd,
2493
+ commands: baseline.lintCommands,
2494
+ results,
2495
+ worktreeChanged: beforeStatus === undefined ||
2496
+ afterStatus === undefined ||
2497
+ beforeStatus !== afterStatus,
2498
+ });
2499
+ return {
2500
+ ok: true,
2501
+ stdout: `Frontend lint baseline: ${materialized.artifact.status}\nArtifact: ${materialized.ref.path}\nDiagnostics: ${materialized.artifact.diagnostics.length}`,
2502
+ stderr: materialized.artifact.reason ?? "",
2503
+ failureCategory: "success",
2504
+ durationMs: Date.now() - started,
2505
+ };
2506
+ }
2507
+ catch (error) {
2508
+ return {
2509
+ ok: false,
2510
+ stdout: "",
2511
+ stderr: error instanceof Error ? error.message : String(error),
2512
+ failureCategory: "invalid-output",
2513
+ durationMs: Date.now() - started,
2514
+ };
2515
+ }
2516
+ }
3261
2517
  async function writeShellPresetProcessReceipt(input) {
3262
2518
  await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
3263
2519
  schemaVersion: 1,
@@ -3510,11 +2766,36 @@ async function executeDagShellNodeInner(input, meta) {
3510
2766
  });
3511
2767
  }
3512
2768
  }
3513
- if (shell?.frontendDesignPolicy) {
3514
- return executeFrontendDesignPolicy(input, meta);
2769
+ if (shell?.frontendPrewriteGate) {
2770
+ const started = Date.now();
2771
+ try {
2772
+ const result = await runFrontendPrewriteGate({
2773
+ runDir: meta.runDir,
2774
+ config: shell.frontendPrewriteGate,
2775
+ sourceBinding: meta.spec.sourceBinding,
2776
+ workspaceRoot: input.cwd,
2777
+ repoRoot: input.cwd,
2778
+ });
2779
+ return {
2780
+ ok: true,
2781
+ stdout: formatFrontendPrewriteGateStdout(result),
2782
+ stderr: "",
2783
+ failureCategory: "success",
2784
+ durationMs: Date.now() - started,
2785
+ };
2786
+ }
2787
+ catch (error) {
2788
+ return {
2789
+ ok: false,
2790
+ stdout: "",
2791
+ stderr: error instanceof Error ? error.message : String(error),
2792
+ failureCategory: "invalid-output",
2793
+ durationMs: Date.now() - started,
2794
+ };
2795
+ }
3515
2796
  }
3516
- if (shell?.frontendWriterAdmission) {
3517
- return executeFrontendWriterAdmission(input, meta);
2797
+ if (shell?.frontendLintBaseline) {
2798
+ return executeFrontendLintBaseline(input, meta);
3518
2799
  }
3519
2800
  if (shell?.frontendVerificationBundle) {
3520
2801
  return executeFrontendVerificationBundle(input, meta);
@@ -3545,12 +2826,6 @@ async function executeDagShellNodeInner(input, meta) {
3545
2826
  };
3546
2827
  }
3547
2828
  }
3548
- if (shell?.frontendContractImport) {
3549
- return executeFrontendContractImport(input, meta);
3550
- }
3551
- if (shell?.frontendCloseout) {
3552
- return executeFrontendCloseout(input, meta);
3553
- }
3554
2829
  if (shell?.frontendTestCaseChecklist) {
3555
2830
  return executeFrontendTestCaseChecklist(input, meta);
3556
2831
  }
@@ -3773,23 +3048,53 @@ async function executeDagShellNodeInner(input, meta) {
3773
3048
  }
3774
3049
  if (shell?.commands?.length === 1 &&
3775
3050
  shell.commands[0] === "frontend-failure-assess-gate") {
3776
- return {
3777
- ok: false,
3778
- stdout: "",
3779
- stderr: "frontend-failure-assess-gate legacy command removed in M8: verify failure is terminal and materializes failure ownership facts in the verification bundle itself",
3780
- failureCategory: "invalid-output",
3781
- durationMs: 0,
3782
- };
3051
+ const started = Date.now();
3052
+ try {
3053
+ const result = await runFrontendFailureAssessGate({
3054
+ runDir: meta.runDir,
3055
+ });
3056
+ return {
3057
+ ok: true,
3058
+ stdout: formatFrontendFailureAssessStdout(result),
3059
+ stderr: "",
3060
+ failureCategory: "success",
3061
+ durationMs: Date.now() - started,
3062
+ };
3063
+ }
3064
+ catch (error) {
3065
+ return {
3066
+ ok: false,
3067
+ stdout: "",
3068
+ stderr: error instanceof Error ? error.message : String(error),
3069
+ failureCategory: "invalid-output",
3070
+ durationMs: Date.now() - started,
3071
+ };
3072
+ }
3783
3073
  }
3784
3074
  if (shell?.commands?.length === 1 &&
3785
3075
  shell.commands[0] === "frontend-repair-contract-gate") {
3786
- return {
3787
- ok: false,
3788
- stdout: "",
3789
- stderr: "frontend-repair-contract-gate legacy command removed in M8: no same-run repair branch exists in the new topology",
3790
- failureCategory: "invalid-output",
3791
- durationMs: 0,
3792
- };
3076
+ const started = Date.now();
3077
+ try {
3078
+ const result = await runFrontendRepairContractGate({
3079
+ runDir: meta.runDir,
3080
+ });
3081
+ return {
3082
+ ok: true,
3083
+ stdout: formatFrontendRepairContractStdout(result.assessment),
3084
+ stderr: "",
3085
+ failureCategory: "success",
3086
+ durationMs: Date.now() - started,
3087
+ };
3088
+ }
3089
+ catch (error) {
3090
+ return {
3091
+ ok: false,
3092
+ stdout: "",
3093
+ stderr: error instanceof Error ? error.message : String(error),
3094
+ failureCategory: "invalid-output",
3095
+ durationMs: Date.now() - started,
3096
+ };
3097
+ }
3793
3098
  }
3794
3099
  if (shell?.projectGovernanceGate) {
3795
3100
  const started = Date.now();
@@ -4116,39 +3421,6 @@ async function executeDagShellNodeInner(input, meta) {
4116
3421
  ...{ commandResults },
4117
3422
  };
4118
3423
  }
4119
- /**
4120
- * Enumerate the workspace's concrete file paths for glob writeSet expansion.
4121
- *
4122
- * Uses `git ls-files -co --exclude-standard` (tracked + untracked, excluding
4123
- * gitignored) so the expanded writeSet reflects real files the writer can
4124
- * touch. Falls back to [] when the workspace is not a git repository or git
4125
- * fails — in that case glob entries fail closed as non-concrete paths.
4126
- */
4127
- async function collectWorkspaceFiles(workspaceRoot) {
4128
- try {
4129
- const files = await new Promise((resolve, reject) => {
4130
- const child = spawn("git", ["ls-files", "-co", "--exclude-standard", "-z"], { cwd: workspaceRoot, stdio: ["ignore", "pipe", "ignore"] });
4131
- const chunks = [];
4132
- child.stdout.on("data", (chunk) => chunks.push(chunk));
4133
- child.on("error", reject);
4134
- child.on("close", (code) => {
4135
- if (code === 0) {
4136
- resolve(Buffer.concat(chunks)
4137
- .toString("utf8")
4138
- .split("\0")
4139
- .filter(Boolean));
4140
- }
4141
- else {
4142
- reject(new Error(`git ls-files exited ${String(code)}`));
4143
- }
4144
- });
4145
- });
4146
- return files.map((file) => file.replace(/\\/g, "/")).sort();
4147
- }
4148
- catch {
4149
- return [];
4150
- }
4151
- }
4152
3424
  export async function executeDagShellNode(input, meta) {
4153
3425
  const handler = namedShellHandler(input.task.shell);
4154
3426
  const result = await executeDagShellNodeInner(input, meta);