@tea-agent/loop-agent 0.42.0-next.1 → 0.42.0-next.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 (238) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +319 -1
  3. package/dist/application/dag/generate-task-dag.js +75 -19
  4. package/dist/application/dag/run-dag.js +41 -0
  5. package/dist/application/task-lifecycle/advance.js +24 -5
  6. package/dist/application/task-lifecycle/observe.js +171 -17
  7. package/dist/application/task-lifecycle/plan-transitions.js +42 -7
  8. package/dist/application/task-lifecycle/recommendations.js +13 -2
  9. package/dist/build-stamp.json +3 -3
  10. package/dist/cli/command-definitions.js +7 -0
  11. package/dist/cli/program.js +6 -1
  12. package/dist/commands/client-recovery.js +3 -0
  13. package/dist/commands/dag-follow-up.js +138 -0
  14. package/dist/commands/dag-rerun-task.js +2 -0
  15. package/dist/commands/init.js +27 -1
  16. package/dist/commands/task-advance.js +19 -0
  17. package/dist/executors/dag-pi-executor.js +3976 -76
  18. package/dist/executors/pi-executor.js +15 -4
  19. package/dist/executors/pi-read-budget-policy.js +239 -0
  20. package/dist/executors/shell-executor.js +978 -188
  21. package/dist/executors/shell-write-guard.js +7 -0
  22. package/dist/infrastructure/console/operation-store.js +226 -6
  23. package/dist/shared/dag-failure-category.js +12 -0
  24. package/dist/shared/openspec-spec.js +70 -4
  25. package/dist/shared/operator/capabilities.js +7 -0
  26. package/dist/task/config-types.js +105 -5
  27. package/dist/task/contract/adopt.js +4 -0
  28. package/dist/task/contract/import-revision.js +4 -0
  29. package/dist/task/contract/project.js +3 -0
  30. package/dist/task/contract/schema.js +2 -1
  31. package/dist/task/frontend-project-capability.js +203 -20
  32. package/dist/task/runtime.js +5 -2
  33. package/dist/task/source-prepare/build-draft.js +3 -3
  34. package/dist/task/source-prepare/fragment-inventory.js +64 -25
  35. package/dist/task/source-prepare/prepare.js +126 -21
  36. package/dist/task/source-prepare/semantic-intake.js +6 -2
  37. package/dist/task/source-references.js +22 -1
  38. package/dist/worker/console/chat/chat-ui-policy.js +4 -2
  39. package/dist/worker/console/chat/model-resolver.js +7 -4
  40. package/dist/worker/console/chat/pi-runtime.js +258 -11
  41. package/dist/worker/console/chat/repo-browser.js +10 -3
  42. package/dist/worker/console/chat/routes.js +165 -71
  43. package/dist/worker/console/chat/sdd-data-alignment.js +222 -0
  44. package/dist/worker/console/chat/session-catalog.js +2 -0
  45. package/dist/worker/console/chat/session-store.js +159 -70
  46. package/dist/worker/console/chat/shortcuts.js +19 -3
  47. package/dist/worker/console/chat/turn-process.js +111 -56
  48. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  49. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  50. package/dist/worker/console/index.js +3 -0
  51. package/dist/worker/console/operator-actions.js +169 -7
  52. package/dist/worker/console/pi-readiness.js +3 -2
  53. package/dist/worker/console/server.js +23 -1
  54. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CcS17TBr.js} +1 -1
  55. package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-COptKq2S.js} +1 -1
  56. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-h4LKMHjP.js} +1 -1
  57. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-COA1MOH0.js} +1 -1
  58. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-DJUf0QPm.js} +1 -1
  59. package/dist/worker/console/static/assets/channel-DdBCaOJ6.js +1 -0
  60. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-mtWfKrUX.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-B8pXsxDQ.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-ipKzByl1.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-DYi3Ald_.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DQtGYoty.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-BU0qS2YV.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-DVH9dIGE.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-Dt60SmgM.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-BoaUGuEY.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-_WHFDTp2.js} +1 -1
  70. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DZFra1GO.js +1 -0
  71. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DZFra1GO.js +1 -0
  72. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-BxkbTIRd.js} +1 -1
  73. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-Bf2UVnoG.js} +1 -1
  74. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-Bo23q1J_.js} +1 -1
  75. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-Dd_UU49i.js} +1 -1
  76. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-ebUa1a9y.js} +1 -1
  77. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-BucthU8r.js} +1 -1
  78. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-BgqNpAkm.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-DC4q5NGa.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-CBqCaMeb.js} +1 -1
  81. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-CGtnbQ3-.js} +1 -1
  82. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-CG3LUao5.js} +1 -1
  83. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-D2ZVgoFS.js} +1 -1
  84. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DoYFZiKt.js} +1 -1
  85. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-CKoP1s6j.js} +1 -1
  86. package/dist/worker/console/static/assets/index-CAZ2fC_X.css +1 -0
  87. package/dist/worker/console/static/assets/index-vbTcFnFs.js +449 -0
  88. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-Duofv8p2.js} +1 -1
  89. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-D2nlCkA1.js} +1 -1
  90. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-Dd4IHdus.js} +1 -1
  91. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-Bi20AOdt.js} +1 -1
  92. package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-D59YJ9kB.js} +1 -1
  93. package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-Bl13LpNa.js} +5 -5
  94. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-C6DLP5eY.js} +1 -1
  95. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-BjO4cy64.js} +1 -1
  96. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-D7i2qZTG.js} +1 -1
  97. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-Dx8o_h0y.js} +1 -1
  98. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-B55orI_t.js} +1 -1
  99. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-zWaehp6f.js} +1 -1
  100. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-CcA-pjvD.js} +1 -1
  101. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-BOFbzNHI.js} +1 -1
  102. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-BjAejah1.js} +1 -1
  103. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-DXYwxJwZ.js} +1 -1
  104. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Clg3V9t1.js +1 -0
  105. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-OYiOch8n.js} +2 -2
  106. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DX1dxAAW.js +8 -0
  107. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-cZfH3nmU.js} +1 -1
  108. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-BDE7b3E1.js} +1 -1
  109. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-kyyGy9WJ.js} +1 -1
  110. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CJj6VTog.js} +1 -1
  111. package/dist/worker/console/static/index.html +2 -2
  112. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  113. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
  114. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
  115. package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
  116. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
  117. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
  118. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +258 -23
  119. package/dist/worker/console/static-src/operator-chat/useChatThread.js +112 -4
  120. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
  121. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
  122. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
  123. package/dist/worker/console/static-src/shell/console-update-reload.js +36 -9
  124. package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
  125. package/dist/worker/console/workspace-context.js +115 -1
  126. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  127. package/dist/worker/observe/routes.js +4 -0
  128. package/dist/worker/observe/static/dag-helpers.js +4 -2
  129. package/dist/worker/observe/static/dag-node-purpose.js +5 -0
  130. package/dist/worker/observe/static/inspect-workspace.js +23 -0
  131. package/dist/worker/observe/static/kpi.js +2 -2
  132. package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
  133. package/dist/worker/observe/static/operator-chrome.js +37 -23
  134. package/dist/worker/observe/static/relations.js +2 -2
  135. package/dist/worker/observe/static/router.d.ts +12 -1
  136. package/dist/worker/observe/static/router.js +48 -6
  137. package/dist/worker/observe/static/run-processing.js +4 -2
  138. package/dist/worker/observe/static/shell-chrome.js +2 -2
  139. package/dist/worker/observe/static/state.js +5 -0
  140. package/dist/worker/observe/static/styles.css +95 -0
  141. package/dist/worker/observe/static/views/dag-inspector.js +14 -7
  142. package/dist/worker/observe/static/views/dag.js +10 -2
  143. package/dist/worker/observe/static/views/dags.js +3 -1
  144. package/dist/worker/observe/static/views/dashboard.js +14 -6
  145. package/dist/worker/observe/static/views/pool.js +7 -2
  146. package/dist/worker/observe/static/views/run.js +12 -3
  147. package/dist/worker/observe/static/views/session-timeline.js +69 -10
  148. package/dist/worker/observe/static/views/task.js +7 -2
  149. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +215 -28
  150. package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
  151. package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
  152. package/dist/workflows/dag/backend-test-scenario-param.js +172 -25
  153. package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
  154. package/dist/workflows/dag/backend-test-writer-completeness.js +104 -12
  155. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  156. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  157. package/dist/workflows/dag/frontend-closeout.js +221 -0
  158. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  159. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  160. package/dist/workflows/dag/frontend-implementation-contract.js +1237 -192
  161. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  162. package/dist/workflows/dag/frontend-prewrite-gate.js +256 -350
  163. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  164. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  165. package/dist/workflows/dag/frontend-recovery-controller.js +226 -0
  166. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  167. package/dist/workflows/dag/frontend-recovery-plan.js +21 -10
  168. package/dist/workflows/dag/frontend-recovery-run.js +166 -34
  169. package/dist/workflows/dag/frontend-repair.js +1 -432
  170. package/dist/workflows/dag/frontend-review-context.js +261 -15
  171. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  172. package/dist/workflows/dag/frontend-shadow-dual-write.js +941 -0
  173. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  174. package/dist/workflows/dag/frontend-shape-facts.js +419 -0
  175. package/dist/workflows/dag/frontend-shape.js +435 -0
  176. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  177. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  178. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  179. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  180. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  181. package/dist/workflows/dag/frontend-verification-trace.js +252 -25
  182. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  183. package/dist/workflows/dag/frontend-writer-admission.js +319 -0
  184. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  185. package/dist/workflows/dag/init-hybrid.js +1028 -562
  186. package/dist/workflows/dag/interrupt-request.js +7 -0
  187. package/dist/workflows/dag/node-execution.js +828 -4
  188. package/dist/workflows/dag/prompt.js +70 -3
  189. package/dist/workflows/dag/recovery-lease.js +170 -0
  190. package/dist/workflows/dag/report.js +37 -1
  191. package/dist/workflows/dag/rerun-feedback.js +315 -1
  192. package/dist/workflows/dag/rerun-plan.js +17 -6
  193. package/dist/workflows/dag/rerun-run.js +44 -6
  194. package/dist/workflows/dag/rerun-task.js +251 -13
  195. package/dist/workflows/dag/retry-policy.js +219 -104
  196. package/dist/workflows/dag/runner.js +580 -124
  197. package/dist/workflows/dag/scheduler.js +133 -20
  198. package/dist/workflows/dag/types.js +244 -16
  199. package/dist/workflows/dag/validate.js +28 -12
  200. package/docs/examples/README.md +5 -0
  201. package/docs/init-surface.manifest.json +30 -12
  202. package/docs/skills/vetted-skill-registry.md +4 -2
  203. package/docs/templates/README.md +2 -0
  204. package/docs/templates/agent-dag-report.schema.json +8 -2
  205. package/docs/templates/agent-dag.schema.json +1 -1
  206. package/docs/templates/backend-test-dag.json +23 -20
  207. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  208. package/docs/templates/frontend-implementation-dag.json +89 -0
  209. package/docs/templates/spec-registry.schema.json +45 -0
  210. package/harness.json +1 -1
  211. package/package.json +4 -3
  212. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  213. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  214. package/skills/frontend-contract/SKILL.md +23 -0
  215. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  216. package/skills/frontend-design-review/SKILL.md +22 -41
  217. package/skills/frontend-plan/SKILL.md +26 -0
  218. package/skills/frontend-plan/references/decision-contract.md +37 -0
  219. package/skills/frontend-plan/references/design-decisions.md +17 -0
  220. package/skills/frontend-review/SKILL.md +20 -15
  221. package/skills/frontend-review/references/review-findings.md +6 -7
  222. package/skills/frontend-scout/SKILL.md +25 -0
  223. package/skills/frontend-scout/references/design-evidence.md +16 -0
  224. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  225. package/skills/frontend-verification/SKILL.md +1 -1
  226. package/skills/loop-agent/references/command-reference.md +1 -0
  227. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  228. package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
  229. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
  230. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
  231. package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
  232. package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
  233. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
  234. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
  235. package/skills/frontend-implementation/SKILL.md +0 -52
  236. package/skills/frontend-implementation/references/code-standards.md +0 -33
  237. package/skills/frontend-implementation/references/design-spec.md +0 -56
  238. package/skills/frontend-implementation/references/node-contracts.md +0 -31
@@ -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,6 +15,8 @@ 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, serializeCompactDeterministicJson, sha256Hex, writeFrontendImplementationContractArtifact, buildFrontendVerifyCommandDirectory, } 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";
18
21
  import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
19
22
  import { probeFrontendTestEnvironment } from "../workflows/dag/frontend-test-environment-probe.js";
@@ -22,9 +25,15 @@ import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-ca
22
25
  import { materializeFrontendTestCaseManifest } from "../workflows/dag/frontend-test-case-manifest.js";
23
26
  import { renderFrontendTestHtmlReport } from "../workflows/dag/frontend-test-html-report.js";
24
27
  import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.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";
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";
28
37
  import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from "../workflows/dag/frontend-review-context.js";
29
38
  import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
30
39
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
@@ -49,6 +58,7 @@ import { readProjectGovernanceContext } from "../workflows/dag/project-governanc
49
58
  import { assertMavenPlanFresh, MavenPlanStaleError, } from "../verification/maven/index.js";
50
59
  async function runBackendScenarioParamPreflight(input) {
51
60
  const outputs = [];
61
+ const allowRepair = input.allowRepair !== false;
52
62
  const initial = await assessBackendScenarioParamConsistency({
53
63
  workspaceRoot: input.workspaceRoot,
54
64
  phase: "initial",
@@ -64,7 +74,7 @@ async function runBackendScenarioParamPreflight(input) {
64
74
  outputs.push(`scenarioParamInitial=${initialArtifacts.reportPath}`);
65
75
  let finalFacts;
66
76
  let finalMarkdown;
67
- if (initial.facts.repairEligible) {
77
+ if (allowRepair && initial.facts.repairEligible) {
68
78
  const repair = await applyDeterministicScenarioParamRepairs({
69
79
  workspaceRoot: input.workspaceRoot,
70
80
  facts: initial.facts,
@@ -970,6 +980,7 @@ async function executeBackendTestPipeline(input, meta) {
970
980
  workspaceRoot: input.cwd,
971
981
  runDir: meta.runDir,
972
982
  layout: backendLayout,
983
+ allowRepair: false,
973
984
  strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
974
985
  });
975
986
  outputs.push(...scenario.outputs);
@@ -1895,6 +1906,44 @@ async function executePipelineCommands(input, meta, overrideCommands) {
1895
1906
  }
1896
1907
  return results;
1897
1908
  }
1909
+ /** A+B (AC-008): derive unresolved verification entrypoints from committed plan
1910
+ * facts. A verification target is unresolved when its commandId does not
1911
+ * match any frozen command directory entry derived from the verify shell
1912
+ * evidence bundle. Returns `{ ref, digest }` entries; a missing/unreadable
1913
+ * plan ledger fails closed to [] so the router restarts the contract instead
1914
+ * of inventing refs. */
1915
+ async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandIds) {
1916
+ try {
1917
+ const { restorePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
1918
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
1919
+ const patch = restorePlanPatchFromCommittedFacts(records);
1920
+ const targets = patch
1921
+ ?.verificationTargets;
1922
+ if (!Array.isArray(targets))
1923
+ return [];
1924
+ const frozen = new Set(frozenCommandIds.filter(Boolean));
1925
+ const refs = [];
1926
+ for (const item of targets) {
1927
+ const target = item;
1928
+ // v2 plan facts carry commandId as the stable directory key.
1929
+ const commandId = typeof target.commandId === "string" ? target.commandId : "";
1930
+ if (commandId && frozen.has(commandId)) {
1931
+ continue;
1932
+ }
1933
+ const file = typeof target.file === "string" ? target.file : "";
1934
+ const symbol = typeof target.symbol === "string" ? target.symbol : "";
1935
+ const id = typeof target.id === "string" ? target.id : "";
1936
+ const ref = file || symbol || id;
1937
+ if (!ref)
1938
+ continue;
1939
+ refs.push({ ref, digest: sha256Hex(ref) });
1940
+ }
1941
+ return refs;
1942
+ }
1943
+ catch {
1944
+ return [];
1945
+ }
1946
+ }
1898
1947
  async function executeFrontendVerificationBundle(input, meta) {
1899
1948
  const started = Date.now();
1900
1949
  const shell = input.task.shell;
@@ -2049,75 +2098,895 @@ async function executeFrontendVerificationBundle(input, meta) {
2049
2098
  catch (error) {
2050
2099
  traceError = error instanceof Error ? error : new Error(String(error));
2051
2100
  }
2052
- if (bundle.mode === "repair") {
2053
- if (firstFailure || lintSyntheticFailure || traceError) {
2101
+ const verifyFailed = firstFailure !== undefined || lintSyntheticFailure !== undefined || traceError !== undefined;
2102
+ if (verifyFailed) {
2103
+ // M8: verify failure is TERMINAL. There is no same-run repair branch and
2104
+ // no frontend-repair-assessment.json; we only materialize failure
2105
+ // ownership facts (frontend-repair classification + recovery capsule
2106
+ // ownership) for the operation controller / recovery run.
2107
+ const failedNodeId = input.task.id;
2108
+ const failureCategory = firstFailure?.failureCategory ??
2109
+ lintSyntheticFailure?.failureCategory ??
2110
+ (traceError ? "invalid-output" : undefined) ??
2111
+ "invalid-output";
2112
+ const failureClass = classifyFrontendFailure({
2113
+ nodeId: failedNodeId,
2114
+ failureCategory,
2115
+ stdout: firstFailure?.stdout,
2116
+ stderr: firstFailure?.stderr ?? traceError?.message,
2117
+ });
2118
+ // A verify failure means the writer's candidate failed verification, so
2119
+ // the recovery ownership maps to the implement phase (frontend-recovery
2120
+ // capsule classification, fail-closed to implement-pi).
2121
+ const classifiedOwner = classifyFailureOwner({
2122
+ failureSource: "frontend-verify-shell",
2123
+ nodeId: failedNodeId,
2124
+ rawFailureCategory: failureCategory,
2125
+ });
2126
+ const failureOwner = classifiedOwner === "unknown" ? "implement-pi" : classifiedOwner;
2127
+ const restartPhase = classifyRestartPhase(failureOwner);
2128
+ // A+B (AC-008): verification-config routing. Produce structured
2129
+ // unresolved entrypoint refs from committed plan facts and the frozen
2130
+ // command labels; a non-empty unresolved set routes to
2131
+ // verification-config-decision and materializes the frozen
2132
+ // verification-config envelope. An empty/untyped signal fails closed to
2133
+ // contract-restart. The decision is recorded so the recovery controller
2134
+ // never re-classifies the same signal twice.
2135
+ let verificationConfigRoute;
2136
+ if (failureOwner === "verification-config") {
2137
+ const verifyDirectory = buildFrontendVerifyCommandDirectory({
2138
+ staticLabels: bundle.staticEvidence.commandLabels ?? [],
2139
+ behaviorLabels: bundle.behaviorEvidence.commandLabels ?? [],
2140
+ mockLabels: bundle.mockEvidence?.commandLabels ?? [],
2141
+ lintLabels: bundle.lintEvidence?.commandLabels ?? [],
2142
+ staticCommandTexts: bundle.staticEvidence.commandTexts ?? [],
2143
+ behaviorCommandTexts: bundle.behaviorEvidence.commandTexts ?? [],
2144
+ mockCommandTexts: bundle.mockEvidence?.commandTexts ?? [],
2145
+ lintCommandTexts: bundle.lintEvidence?.commandTexts ?? [],
2146
+ });
2147
+ const frozenCommandLabels = verifyDirectory.map((entry) => entry.label);
2148
+ const unresolvedEntrypointRefs = await deriveUnresolvedVerificationEntrypointRefs(meta.runDir, verifyDirectory.map((entry) => entry.commandId));
2149
+ verificationConfigRoute = routeVerificationConfig({
2150
+ unresolvedEntrypointRefs: unresolvedEntrypointRefs.map((ref) => ref.ref),
2151
+ });
2152
+ const allowlistedResolvers = [
2153
+ ...new Set(frozenCommandLabels
2154
+ .map((label) => label.trim())
2155
+ .filter(Boolean)),
2156
+ ].sort();
2157
+ if (verificationConfigRoute.decision ===
2158
+ "verification-config-decision" &&
2159
+ unresolvedEntrypointRefs.length > 0 &&
2160
+ allowlistedResolvers.length > 0) {
2161
+ const envelope = buildFrontendHumanDecisionEnvelope({
2162
+ operationId: input.task.id,
2163
+ runId: meta.runId,
2164
+ operationRevision: 1,
2165
+ targetDigest: sha256Hex(frozenCommandLabels.slice().sort().join("\n")),
2166
+ staleOnTargetDigestChange: true,
2167
+ kind: "verification-config",
2168
+ resumeAction: "select-verification-config",
2169
+ payload: {
2170
+ unresolvedEntrypointRefs,
2171
+ allowlistedResolvers,
2172
+ },
2173
+ });
2174
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verification-config-envelope.json", envelope);
2175
+ }
2176
+ }
2177
+ const verifyFailure = {
2178
+ schemaVersion: 1,
2179
+ schemaId: "frontend-verify-failure-v1",
2180
+ nodeId: failedNodeId,
2181
+ failureCategory,
2182
+ failureClass,
2183
+ failureOwner,
2184
+ restartPhase,
2185
+ ...(verificationConfigRoute
2186
+ ? { verificationConfigRoute }
2187
+ : {}),
2188
+ commandResults,
2189
+ failedAt: new Date().toISOString(),
2190
+ };
2191
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verify-failure.json", verifyFailure);
2192
+ return {
2193
+ ok: false,
2194
+ stdout: summarizeCommandResults(results).stdout,
2195
+ stderr: firstFailure?.stderr ||
2196
+ lintSyntheticFailure?.stderr ||
2197
+ traceError?.message ||
2198
+ "frontend verification failed",
2199
+ failureCategory,
2200
+ durationMs: Date.now() - started,
2201
+ ...{ commandResults },
2202
+ };
2203
+ }
2204
+ // A+B (AC-008): when lint is `require-clean` but the assessment proves
2205
+ // baseline-debt, materialize the frozen `baseline-debt-acceptance` envelope
2206
+ // whose only positive resume is `accept-baseline-debt`. Evidence stays
2207
+ // `baseline-debt` and is never rewritten to `passed`.
2208
+ if (lintAssessment?.status === "baseline-debt") {
2209
+ const baselineEvidenceRefs = (lintAssessment.rawEvidenceRefs ?? [])
2210
+ .filter((ref) => ref?.path && ref?.sha256)
2211
+ .map((ref) => ({ ref: ref.path, digest: ref.sha256 }));
2212
+ if (baselineEvidenceRefs.length > 0) {
2213
+ const envelope = buildFrontendHumanDecisionEnvelope({
2214
+ operationId: input.task.id,
2215
+ runId: meta.runId,
2216
+ operationRevision: 1,
2217
+ targetDigest: lintAssessment.commandIdentity.sha256,
2218
+ staleOnTargetDigestChange: true,
2219
+ kind: "baseline-debt-acceptance",
2220
+ resumeAction: "accept-baseline-debt",
2221
+ payload: {
2222
+ baselineEvidenceRefs,
2223
+ candidateDigest: lintAssessment.commandIdentity.sha256,
2224
+ commandDigest: lintAssessment.commandIdentity.sha256,
2225
+ resultingPolicy: "allow-non-regression",
2226
+ },
2227
+ });
2228
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-baseline-debt-acceptance-envelope.json", envelope);
2229
+ }
2230
+ }
2231
+ return {
2232
+ ok: true,
2233
+ stdout: lintAssessment?.status === "baseline-debt"
2234
+ ? "Frontend verification bundle: pass with lint baseline-debt"
2235
+ : "Frontend verification bundle: pass",
2236
+ stderr: "",
2237
+ failureCategory: "success",
2238
+ durationMs: Date.now() - started,
2239
+ ...{ commandResults },
2240
+ };
2241
+ }
2242
+ /* ------------------------------------------------------------------ *
2243
+ * M8 deterministic frontend shell nodes: design policy, writer *
2244
+ * admission, closeout. These replace the prewrite-gate authorization *
2245
+ * path and the Pi closeout node. *
2246
+ * ------------------------------------------------------------------ */
2247
+ async function readRunDirJson(runDir, relativePath) {
2248
+ return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
2249
+ }
2250
+ /** Committed typed review terminal kinds from the review node's typed event
2251
+ * store (AC-003: typed terminal tools are the only authoritative verdict). */
2252
+ async function collectReviewTerminalKinds(runDir, reviewNodeId) {
2253
+ try {
2254
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
2255
+ return records
2256
+ .filter((record) => record.phase === "committed")
2257
+ .map((record) => {
2258
+ const kind = record.fact.kind;
2259
+ return typeof kind === "string" ? kind : "";
2260
+ })
2261
+ .filter(Boolean);
2262
+ }
2263
+ catch {
2264
+ return [];
2265
+ }
2266
+ }
2267
+ /** Committed typed design terminal kinds from the design review node's typed
2268
+ * event store (M8: the committed approve_design / request_design_changes fact
2269
+ * is the only authoritative verdict; the legacy first-line VERDICT text is
2270
+ * never read for writer admission). Fail-closed on a missing/unreadable store. */
2271
+ async function collectDesignTerminalKinds(runDir, designNodeId) {
2272
+ try {
2273
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2274
+ return records
2275
+ .filter((record) => record.phase === "committed")
2276
+ .map((record) => {
2277
+ const kind = record.fact.kind;
2278
+ return typeof kind === "string" ? kind : "";
2279
+ })
2280
+ .filter(Boolean);
2281
+ }
2282
+ catch {
2283
+ return [];
2284
+ }
2285
+ }
2286
+ /** A+B (AC-009): deterministic §4.10 restart phase for a committed
2287
+ * `request_review_changes` fact. The typed issueCategory is mapped through the
2288
+ * frozen classifier; `unknown` (or an unreadable store) fails closed to
2289
+ * `undefined` so the runtime never trusts an untyped category. */
2290
+ async function collectReviewRestartPhase(runDir, reviewNodeId) {
2291
+ try {
2292
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
2293
+ const request = records.find((record) => record.phase === "committed" &&
2294
+ record.fact.kind === "request_review_changes");
2295
+ const issueCategory = request?.fact
2296
+ ?.issueCategory;
2297
+ return typeof issueCategory === "string"
2298
+ ? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
2299
+ : undefined;
2300
+ }
2301
+ catch {
2302
+ return undefined;
2303
+ }
2304
+ }
2305
+ /** A+B (AC-009): deterministic §4.10 restart phase for a committed
2306
+ * `request_design_changes` fact. Mirrors the review collector. */
2307
+ /** Read the committed design request as a bounded structured capsule. */
2308
+ async function collectDesignReviewFindings(runDir, designNodeId) {
2309
+ try {
2310
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2311
+ const request = records.find((record) => record.phase === "committed" &&
2312
+ record.fact.kind ===
2313
+ "request_design_changes");
2314
+ const fact = request?.fact;
2315
+ const findings = Array.isArray(fact?.findings)
2316
+ ? fact.findings.flatMap((finding) => {
2317
+ if (!finding || typeof finding !== "object")
2318
+ return [];
2319
+ const value = finding;
2320
+ if (!(["Critical", "Important", "Minor", "Info"].includes(value.severity) &&
2321
+ typeof value.issue === "string" && value.issue.trim().length > 0))
2322
+ return [];
2323
+ return [{
2324
+ severity: value.severity,
2325
+ ...(typeof value.file === "string" && value.file.trim() ? { file: value.file } : {}),
2326
+ ...(typeof value.line === "number" && Number.isInteger(value.line) && value.line > 0 ? { line: value.line } : {}),
2327
+ issue: value.issue,
2328
+ ...(typeof value.requiredChange === "string" && value.requiredChange.trim() ? { requiredChange: value.requiredChange } : {}),
2329
+ }];
2330
+ }).slice(0, 16)
2331
+ : [];
2332
+ const evidenceRefs = Array.isArray(fact?.evidenceRefs)
2333
+ ? fact.evidenceRefs.filter((ref) => typeof ref === "string" && ref.trim().length > 0).slice(0, 16)
2334
+ : [];
2335
+ return { findings, evidenceRefs };
2336
+ }
2337
+ catch {
2338
+ return { findings: [], evidenceRefs: [] };
2339
+ }
2340
+ }
2341
+ async function collectDesignRestartPhase(runDir, designNodeId) {
2342
+ try {
2343
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2344
+ const request = records.find((record) => record.phase === "committed" &&
2345
+ record.fact.kind === "request_design_changes");
2346
+ const issueCategory = request?.fact
2347
+ ?.issueCategory;
2348
+ return typeof issueCategory === "string"
2349
+ ? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
2350
+ : undefined;
2351
+ }
2352
+ catch {
2353
+ return undefined;
2354
+ }
2355
+ }
2356
+ /**
2357
+ * Resolve the deterministic runtime skeleton for the frontend plan patch from
2358
+ * the DAG spec. The plan task carries it via `structuredContractOutput.skeleton`
2359
+ * (the only schema-recognized persistent channel); absence is a fail-closed
2360
+ * generation/configuration defect, never a reason to fall back to text.
2361
+ */
2362
+ function resolveFrontendContractSkeleton(spec, planNodeId) {
2363
+ const planTask = spec.tasks.find((task) => task.id === planNodeId);
2364
+ const skeleton = planTask?.structuredContractOutput?.skeleton;
2365
+ if (!skeleton || Object.keys(skeleton).length === 0) {
2366
+ throw new Error(`frontend design policy blocked: missing runtime contract skeleton for ${planNodeId}`);
2367
+ }
2368
+ return skeleton;
2369
+ }
2370
+ async function collectFrontendSourceFreshness(input) {
2371
+ const entries = [];
2372
+ for (const source of input.sourceBinding?.sources ?? []) {
2373
+ entries.push({ path: source.path, sha256: source.sha256, fresh: true });
2374
+ }
2375
+ for (const evidencePath of input.contract.designEvidence.paths) {
2376
+ if (entries.some((entry) => entry.path === evidencePath))
2377
+ continue;
2378
+ try {
2379
+ const bytes = await readFile(path.join(input.workspaceRoot, evidencePath));
2380
+ entries.push({
2381
+ path: evidencePath,
2382
+ sha256: createHash("sha256").update(bytes).digest("hex"),
2383
+ fresh: true,
2384
+ });
2385
+ }
2386
+ catch {
2387
+ entries.push({
2388
+ path: evidencePath,
2389
+ sha256: "0".repeat(64),
2390
+ fresh: false,
2391
+ });
2392
+ }
2393
+ }
2394
+ return entries;
2395
+ }
2396
+ /** R3/R4 openspec invariants: frozen candidates must exist and content
2397
+ * snapshots must not have drifted. Citation is enforced upstream by the
2398
+ * prewrite gate read-event evidence and ledger specReference conformance. */
2399
+ async function checkFrontendOpenspecFacts(input) {
2400
+ const frozenCandidatePaths = input.config.openspecCandidatePaths ?? [];
2401
+ const candidateRoot = input.workspaceRoot;
2402
+ const missingCandidates = [];
2403
+ for (const candidate of frozenCandidatePaths) {
2404
+ try {
2405
+ await stat(path.join(candidateRoot, candidate));
2406
+ }
2407
+ catch {
2408
+ missingCandidates.push(candidate);
2409
+ }
2410
+ }
2411
+ if (missingCandidates.length > 0) {
2412
+ throw new Error(`frontend design policy blocked: openspec candidate paths frozen at generation time no longer exist on disk: ${missingCandidates.join(", ")}`);
2413
+ }
2414
+ const snapshots = input.config.openspecCandidateSnapshots;
2415
+ if (snapshots && snapshots.length > 0) {
2416
+ for (const snapshot of snapshots) {
2417
+ try {
2418
+ const bytes = await readFile(path.join(candidateRoot, snapshot.path));
2419
+ const actual = createHash("sha256").update(bytes).digest("hex");
2420
+ if (actual !== snapshot.sha256) {
2421
+ throw new Error(`frontend design policy blocked: openspec candidate content hash drifted: ${snapshot.path}`);
2422
+ }
2423
+ }
2424
+ catch (error) {
2425
+ if (error instanceof Error &&
2426
+ error.message.startsWith("frontend design policy blocked")) {
2427
+ throw error;
2428
+ }
2429
+ throw new Error(`frontend design policy blocked: openspec candidate unreadable: ${snapshot.path}`);
2430
+ }
2431
+ }
2432
+ }
2433
+ }
2434
+ async function executeFrontendDesignPolicy(input, meta) {
2435
+ const started = Date.now();
2436
+ const shell = input.task.shell;
2437
+ const config = shell.frontendDesignPolicy;
2438
+ try {
2439
+ // 1. Materialize the canonical contract from committed typed facts. After
2440
+ // M9a the plan node's assistantText is a pure Markdown narrative; the
2441
+ // compile authority is the committed plan ledger ⊕ runtime skeleton.
2442
+ // Fail closed on missing committed facts — never fall back to text.
2443
+ const { readCommittedOriginFacts, checkCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
2444
+ // A+B (AC-001/003): fail-closed committed contract/scout typed facts.
2445
+ // Missing/uncommitted facts block here; never fall back to text parsing.
2446
+ const committedContractFacts = await readCommittedOriginFacts(meta.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
2447
+ const contractCheck = checkCommittedOriginFacts({
2448
+ records: committedContractFacts,
2449
+ origin: "contract",
2450
+ });
2451
+ if (!contractCheck.ok) {
2452
+ return {
2453
+ ok: false,
2454
+ stdout: "",
2455
+ stderr: `frontend design policy blocked: ${contractCheck.reason}`,
2456
+ failureCategory: "invalid-output",
2457
+ durationMs: Date.now() - started,
2458
+ };
2459
+ }
2460
+ const committedScoutFacts = await readCommittedOriginFacts(meta.runDir, "frontend-scout-pi", "scout-typed-facts.jsonl");
2461
+ const scoutCheck = checkCommittedOriginFacts({
2462
+ records: committedScoutFacts,
2463
+ origin: "scout",
2464
+ });
2465
+ if (!scoutCheck.ok) {
2466
+ return {
2467
+ ok: false,
2468
+ stdout: "",
2469
+ stderr: `frontend design policy blocked: ${scoutCheck.reason}`,
2470
+ failureCategory: "invalid-output",
2471
+ durationMs: Date.now() - started,
2472
+ };
2473
+ }
2474
+ const skeleton = resolveFrontendContractSkeleton(meta.spec, config.planFromNodeId);
2475
+ const assembled = await buildContractFromCommittedFacts({
2476
+ runDir: meta.runDir,
2477
+ planNodeId: config.planFromNodeId,
2478
+ contractNodeId: "frontend-contract-pi",
2479
+ sourceBinding: meta.spec.sourceBinding,
2480
+ skeleton,
2481
+ });
2482
+ if (!assembled.ok) {
2483
+ return {
2484
+ ok: false,
2485
+ stdout: "",
2486
+ stderr: `frontend design policy blocked: ${assembled.reason}`,
2487
+ failureCategory: "invalid-output",
2488
+ durationMs: Date.now() - started,
2489
+ };
2490
+ }
2491
+ const contract = assembled.canonical;
2492
+ // 2. Requirement id retention (R11): search the canonical contract
2493
+ // serialization instead of the plan node narrative.
2494
+ const contractText = serializeCompactDeterministicJson(contract);
2495
+ const missingIds = config.requiredRequirementIds.filter((id) => !contractText.includes(id));
2496
+ if (missingIds.length > 0) {
2497
+ return {
2498
+ ok: false,
2499
+ stdout: "",
2500
+ stderr: `frontend design policy blocked: missing requirement ids: ${missingIds.join(", ")}`,
2501
+ failureCategory: "invalid-output",
2502
+ durationMs: Date.now() - started,
2503
+ };
2504
+ }
2505
+ // 3. Mock strategy allowed + frozen command binding (R1).
2506
+ const mockStrategy = contract.mockApi.strategy;
2507
+ if (!config.allowedMockStrategies.includes(mockStrategy)) {
2508
+ return {
2509
+ ok: false,
2510
+ stdout: "",
2511
+ stderr: `frontend contract mock strategy "${mockStrategy}" is incompatible with the generated allowed strategies [${config.allowedMockStrategies.join(", ")}]`,
2512
+ failureCategory: "invalid-output",
2513
+ durationMs: Date.now() - started,
2514
+ };
2515
+ }
2516
+ if (mockStrategy !== "not-needed" && config.mockCommandLabels.length === 0) {
2054
2517
  return {
2055
2518
  ok: false,
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",
2519
+ stdout: "",
2520
+ stderr: `frontend design policy blocked: contract selected Mock strategy ${mockStrategy} but the DAG has no authorized Mock verification commands`,
2521
+ failureCategory: "invalid-output",
2522
+ durationMs: Date.now() - started,
2523
+ };
2524
+ }
2525
+ // 4. writeSet containment.
2526
+ if (config.implementationWriteSet) {
2527
+ const writeSet = new Set(config.implementationWriteSet);
2528
+ const uncoveredTargets = contract.targets.files.filter((target) => ![...writeSet].some((pattern) => pathMatchesPattern(target, pattern) || target === pattern));
2529
+ if (uncoveredTargets.length > 0) {
2530
+ return {
2531
+ ok: false,
2532
+ stdout: "",
2533
+ stderr: `frontend design policy blocked: contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`,
2534
+ failureCategory: "invalid-output",
2535
+ durationMs: Date.now() - started,
2536
+ };
2537
+ }
2538
+ const uncoveredVTs = contract.verificationTargets
2539
+ .filter((vt) => vt.mode !== "static")
2540
+ .filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
2541
+ if (uncoveredVTs.length > 0) {
2542
+ return {
2543
+ ok: false,
2544
+ stdout: "",
2545
+ stderr: `frontend design policy blocked: verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`,
2546
+ failureCategory: "invalid-output",
2547
+ durationMs: Date.now() - started,
2548
+ };
2549
+ }
2550
+ }
2551
+ // 5. Source freshness (R4) + freshness entries.
2552
+ if (config.requireSourceFreshness) {
2553
+ if (!meta.spec.sourceBinding) {
2554
+ throw new Error("frontend design policy requires sourceBinding for freshness check");
2555
+ }
2556
+ await assertFrontendSourceBindingFresh({
2557
+ workspaceRoot: input.cwd,
2558
+ binding: meta.spec.sourceBinding,
2559
+ });
2560
+ }
2561
+ // Byte freshness is necessary but not sufficient: require the same ledger
2562
+ // semantic checks as the shadow prewrite gate before production writer
2563
+ // admission can be materialized.
2564
+ if (meta.spec.sourceBinding) {
2565
+ const ledgerCheck = await checkFrontendSourceFidelityLedger({
2566
+ workspaceRoot: input.cwd,
2567
+ binding: meta.spec.sourceBinding,
2568
+ contractRequirements: contract.requirements.map((requirement) => ({
2569
+ id: requirement.id,
2570
+ sourceFragmentIds: requirement.sourceFragmentIds,
2571
+ sourceRefs: requirement.sourceRefs,
2572
+ })),
2573
+ });
2574
+ if (!ledgerCheck.ok) {
2575
+ return {
2576
+ ok: false,
2577
+ stdout: "",
2578
+ stderr: `frontend design policy blocked: ${ledgerCheck.failureReason ?? "source fidelity ledger validation failed"}`,
2579
+ failureCategory: "invalid-output",
2580
+ durationMs: Date.now() - started,
2581
+ };
2582
+ }
2583
+ }
2584
+ const sourceFreshness = await collectFrontendSourceFreshness({
2585
+ workspaceRoot: input.cwd,
2586
+ sourceBinding: meta.spec.sourceBinding,
2587
+ contract,
2588
+ });
2589
+ // 6. Openspec invariants (R3): frozen candidates exist and snapshots have
2590
+ // not drifted. Citation is superseded by prewrite-gate read evidence.
2591
+ await checkFrontendOpenspecFacts({
2592
+ workspaceRoot: input.cwd,
2593
+ config,
2594
+ });
2595
+ // 7. Deterministic design policy (M8 D2: designApproval is optional here;
2596
+ // the design verdict is enforced by the writer-admission shell).
2597
+ const policyResult = evaluateFrontendDesignPolicy({
2598
+ contract,
2599
+ allowedMockStrategies: config.allowedMockStrategies,
2600
+ sourceFreshness,
2601
+ componentSpecCandidatePaths: config.openspecCandidatePaths ??
2602
+ config.componentSpecCandidatePaths ??
2603
+ [],
2604
+ allowedDependencies: config.allowedDependencies,
2605
+ writeSetPatterns: config.implementationWriteSet ?? [],
2606
+ });
2607
+ // 8. Write canonical contract + policy result.
2608
+ const contractArtifact = await writeFrontendImplementationContractArtifact({
2609
+ runDir: meta.runDir,
2610
+ outputDir: config.outputDir,
2611
+ artifactName: config.artifactName,
2612
+ canonical: contract,
2613
+ });
2614
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-design-policy-result.json", policyResult);
2615
+ if (policyResult.classification === "blocked") {
2616
+ return {
2617
+ ok: false,
2618
+ stdout: "",
2619
+ stderr: `frontend design policy blocked: ${policyResult.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
2620
+ failureCategory: "invalid-output",
2064
2621
  durationMs: Date.now() - started,
2065
- ...{ commandResults },
2066
2622
  };
2067
2623
  }
2068
2624
  return {
2069
2625
  ok: true,
2070
- stdout: lintAssessment?.status === "baseline-debt"
2071
- ? "Frontend reverify bundle: pass with lint baseline-debt"
2072
- : "Frontend reverify bundle: pass",
2626
+ stdout: [
2627
+ "Frontend design policy: approved findings=0",
2628
+ `Contract: ${contractArtifact.path}`,
2629
+ `Contract index: ${contractArtifact.reviewIndexPath}`,
2630
+ `Capacity diagnostic: ${contractArtifact.capacityDiagnosticPath} (non-blocking)`,
2631
+ ].join("\n"),
2073
2632
  stderr: "",
2074
2633
  failureCategory: "success",
2075
2634
  durationMs: Date.now() - started,
2076
- ...{ commandResults },
2077
2635
  };
2078
2636
  }
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
- },
2096
- });
2637
+ catch (error) {
2638
+ return {
2639
+ ok: false,
2640
+ stdout: "",
2641
+ stderr: error instanceof Error ? error.message : String(error),
2642
+ failureCategory: "invalid-output",
2643
+ durationMs: Date.now() - started,
2644
+ };
2097
2645
  }
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,
2105
- },
2646
+ }
2647
+ async function executeFrontendWriterAdmission(input, meta) {
2648
+ const started = Date.now();
2649
+ const shell = input.task.shell;
2650
+ const config = shell.frontendWriterAdmission;
2651
+ try {
2652
+ // 1. Design verdict handling. The typed terminal
2653
+ // verdict is read from the committed typed design terminal fact, not
2654
+ // from the legacy first-line VERDICT text protocol (AC-003/AC-004).
2655
+ // A request_design_changes verdict blocks writer admission and is copied
2656
+ // into the receipt as a bounded advisory capsule for recovery. A missing
2657
+ // typed terminal verdict still fails closed. In small topology the design
2658
+ // review node is absent and the check is vacuous.
2659
+ let verdictPass = true;
2660
+ let designRequested = false;
2661
+ let designAdvisory;
2662
+ if (config.designReviewFromNodeId) {
2663
+ const designKinds = await collectDesignTerminalKinds(meta.runDir, config.designReviewFromNodeId);
2664
+ if (designKinds.length !== 1) {
2665
+ verdictPass = false;
2666
+ }
2667
+ else if (designKinds[0] !== "approve_design") {
2668
+ designRequested = true;
2669
+ const designFindings = await collectDesignReviewFindings(meta.runDir, config.designReviewFromNodeId);
2670
+ designAdvisory = {
2671
+ verdict: "request_design_changes",
2672
+ restartPhase: await collectDesignRestartPhase(meta.runDir, config.designReviewFromNodeId),
2673
+ findingCount: designFindings.findings.length,
2674
+ findings: designFindings.findings,
2675
+ evidenceRefs: designFindings.evidenceRefs,
2676
+ };
2677
+ verdictPass = false;
2678
+ }
2679
+ }
2680
+ // 2. Read the canonical contract + design-policy result.
2681
+ const contractRaw = await readRunDirJson(meta.runDir, "contracts/frontend-implementation-contract.json");
2682
+ const contract = frontendImplementationContractSchema.parse(contractRaw);
2683
+ const policyRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-design-policy-result.json"));
2684
+ if (policyRaw.schemaId !== "frontend-design-policy-v1") {
2685
+ throw new Error("frontend writer admission: design policy result artifact invalid");
2686
+ }
2687
+ const designPolicyResult = {
2688
+ schemaId: "frontend-design-policy-v1",
2689
+ classification: policyRaw.classification === "approved" ? "approved" : "blocked",
2690
+ findings: policyRaw.findings ?? [],
2691
+ digest: "",
2692
+ };
2693
+ // 3. Freeze the pre-writer worktree baseline (cumulative diff baseline).
2694
+ await captureFrontendWorktreeBaseline({
2695
+ runDir: meta.runDir,
2696
+ workspaceRoot: input.cwd,
2106
2697
  });
2698
+ const baselineRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-worktree-baseline.json"));
2699
+ const baseline = (baselineRaw.files ?? [])
2700
+ .map((file) => ({
2701
+ path: file,
2702
+ sha256: baselineRaw.hashes?.[file] ?? null,
2703
+ }))
2704
+ .filter((entry) => entry.sha256 !== null);
2705
+ // 4. Writer-preceding lint baseline (R5) when lint is configured.
2706
+ let lintResults = [];
2707
+ if ((config.lintCommands?.length ?? 0) > 0) {
2708
+ for (const command of config.lintCommands) {
2709
+ const commandNumber = lintResults.length + 1;
2710
+ const result = await executeShellCommand({
2711
+ command,
2712
+ cwd: input.cwd,
2713
+ timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
2714
+ envAllowlist: shell.envAllowlist,
2715
+ dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2716
+ reportActivity: input.reportActivity,
2717
+ outputArtifacts: {
2718
+ stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2719
+ stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
2720
+ },
2721
+ });
2722
+ lintResults.push(result);
2723
+ }
2724
+ await materializeFrontendLintBaseline({
2725
+ runDir: meta.runDir,
2726
+ workspaceRoot: input.cwd,
2727
+ commands: config.lintCommands,
2728
+ results: lintResults,
2729
+ worktreeChanged: false,
2730
+ });
2731
+ }
2732
+ // 5. Derive admission (writeSet + digests + findings). Glob entries in
2733
+ // the contract targets (e.g. `src/**` inherited from the skeleton) are
2734
+ // expanded against the concrete workspace files so the derived writeSet
2735
+ // contains only real paths the writer can touch.
2736
+ const workspaceFiles = await collectWorkspaceFiles(input.cwd);
2737
+ const admission = deriveFrontendWriterAdmissionShell({
2738
+ contract,
2739
+ designPolicyResult,
2740
+ baseline,
2741
+ frozenCommandLabels: config.frozenCommandLabels,
2742
+ allowedMockStrategies: config.allowedMockStrategies,
2743
+ workspaceFiles,
2744
+ });
2745
+ // 6. Design verdict enforcement: request_design_changes is a blocked
2746
+ // admission. This keeps the writer fail-closed until the recovery plan
2747
+ // has incorporated the reviewer's concrete findings.
2748
+ const failureSource = policyRaw.classification === "blocked"
2749
+ ? "frontend-design-policy-shell"
2750
+ : "frontend-writer-admission-shell";
2751
+ const findings = verdictPass
2752
+ ? admission.findings
2753
+ : [
2754
+ ...admission.findings,
2755
+ {
2756
+ code: designRequested
2757
+ ? "design-review-requested"
2758
+ : "design-approval-missing",
2759
+ message: designRequested
2760
+ ? "design review requested changes; writer admission is blocked until recovery incorporates the committed findings"
2761
+ : "design review typed terminal verdict missing or ambiguous (fail closed)",
2762
+ },
2763
+ ];
2764
+ const classification = findings.length === 0 && admission.classification === "accepted"
2765
+ ? admission.classification
2766
+ : findings.length === 0 &&
2767
+ admission.classification === "requires-human-approval"
2768
+ ? "requires-human-approval"
2769
+ : "blocked";
2770
+ const result = frontendWriterAdmissionResultV1Schema.parse({
2771
+ schemaVersion: 1,
2772
+ schemaId: "frontend-writer-admission-shell-v1",
2773
+ classification,
2774
+ writeSet: admission.writeSet,
2775
+ writeSetDigest: admission.writeSetDigest,
2776
+ baselineDigest: admission.baselineDigest,
2777
+ admissionDigest: admission.admissionDigest,
2778
+ ...(admission.baselineDebtPolicy !== undefined
2779
+ ? { baselineDebtPolicy: admission.baselineDebtPolicy }
2780
+ : {}),
2781
+ findings,
2782
+ ...(designAdvisory ? { designReviewAdvisory: designAdvisory } : {}),
2783
+ failureSource,
2784
+ });
2785
+ await writeDagRunJsonArtifact(meta.runDir, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, result);
2786
+ // A+B (AC-007): High topology materializes a `high-admission-receipt-v1`
2787
+ // catalog whose candidate digest must match the admission digest before
2788
+ // the gate can transition from `requires-human-approval` to `accepted`.
2789
+ // Any digest drift resolves to `stale` (never silently accepted).
2790
+ if (classification === "requires-human-approval") {
2791
+ const receipt = evaluateHighAdmissionReceipt({
2792
+ candidateDigest: result.admissionDigest,
2793
+ admissionDigest: result.admissionDigest,
2794
+ });
2795
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-high-admission-receipt.json", receipt);
2796
+ if (receipt.status !== "accepted") {
2797
+ return {
2798
+ ok: false,
2799
+ stdout: "",
2800
+ stderr: `frontend writer admission stale: high-admission receipt candidate digest drifted from admission digest`,
2801
+ failureCategory: "invalid-output",
2802
+ durationMs: Date.now() - started,
2803
+ };
2804
+ }
2805
+ }
2806
+ if (classification === "blocked") {
2807
+ return {
2808
+ ok: false,
2809
+ stdout: "",
2810
+ stderr: `frontend writer admission ${classification}: ${findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
2811
+ failureCategory: "invalid-output",
2812
+ durationMs: Date.now() - started,
2813
+ };
2814
+ }
2815
+ return {
2816
+ ok: true,
2817
+ stdout: `Frontend writer admission: ${classification} writeSet=${result.writeSet.length} writeSetDigest=${result.writeSetDigest}`,
2818
+ stderr: "",
2819
+ failureCategory: "success",
2820
+ durationMs: Date.now() - started,
2821
+ };
2822
+ }
2823
+ catch (error) {
2824
+ return {
2825
+ ok: false,
2826
+ stdout: "",
2827
+ stderr: error instanceof Error ? error.message : String(error),
2828
+ failureCategory: "invalid-output",
2829
+ durationMs: Date.now() - started,
2830
+ };
2107
2831
  }
2832
+ }
2833
+ async function executeFrontendContractImport(input, meta) {
2834
+ const started = Date.now();
2835
+ const shell = input.task.shell;
2836
+ const config = shell.frontendContractImport;
2108
2837
  try {
2109
- const assessment = await runFrontendFailureAssessGate({
2110
- runDir: meta.runDir,
2111
- failureFacts,
2838
+ const binding = meta.spec.sourceBinding;
2839
+ if (!binding) {
2840
+ throw new Error("frontend contract-import requires a DAG sourceBinding to re-validate freshness");
2841
+ }
2842
+ if (config.requireSourceFreshness) {
2843
+ await assertFrontendSourceBindingFresh({
2844
+ workspaceRoot: input.cwd,
2845
+ binding,
2846
+ });
2847
+ }
2848
+ const digest = createHash("sha256")
2849
+ .update(JSON.stringify({
2850
+ taskId: binding.taskId,
2851
+ sources: binding.sources,
2852
+ artifactName: config.artifactName,
2853
+ }))
2854
+ .digest("hex");
2855
+ const facts = {
2856
+ schemaVersion: 1,
2857
+ schemaId: "frontend-contract-import-v1",
2858
+ artifactName: config.artifactName,
2859
+ sourceBindingTaskId: binding.taskId,
2860
+ sourceCount: binding.sources.length,
2861
+ fresh: true,
2862
+ digest,
2863
+ };
2864
+ return {
2865
+ ok: true,
2866
+ stdout: JSON.stringify(facts, null, 2),
2867
+ stderr: "",
2868
+ failureCategory: "success",
2869
+ durationMs: Date.now() - started,
2870
+ };
2871
+ }
2872
+ catch (error) {
2873
+ return {
2874
+ ok: false,
2875
+ stdout: "",
2876
+ stderr: error instanceof Error ? error.message : String(error),
2877
+ failureCategory: "invalid-output",
2878
+ durationMs: Date.now() - started,
2879
+ };
2880
+ }
2881
+ }
2882
+ async function executeFrontendCloseout(input, meta) {
2883
+ const started = Date.now();
2884
+ const shell = input.task.shell;
2885
+ const config = shell.frontendCloseout;
2886
+ try {
2887
+ const context = (await readRunDirJson(meta.runDir, "contracts/frontend-review-context.json"));
2888
+ if (context.contractRef?.path !==
2889
+ "contracts/frontend-implementation-contract.json") {
2890
+ throw new Error("frontend closeout missing canonical contract reference");
2891
+ }
2892
+ const contractRaw = await readFile(path.join(meta.runDir, context.contractRef.path), "utf8");
2893
+ const contractSha256 = createHash("sha256")
2894
+ .update(contractRaw)
2895
+ .digest("hex");
2896
+ if (contractSha256 !== context.contractRef.sha256) {
2897
+ throw new Error("frontend closeout canonical contract hash mismatch");
2898
+ }
2899
+ const contract = frontendImplementationContractSchema.parse(JSON.parse(contractRaw));
2900
+ const trace = context.verificationTrace;
2901
+ const verificationTargets = (trace?.targets ?? []).map((target) => ({
2902
+ status: target.status === "ok" ? "ok" : "failed",
2903
+ }));
2904
+ const mockEvidenceBound = contract.mockApi.strategy !== "not-needed" &&
2905
+ typeof trace?.mockCommandNodeId === "string" &&
2906
+ trace.mockCommandNodeId.length > 0 &&
2907
+ Array.isArray(trace?.mockCommandLabels) &&
2908
+ trace.mockCommandLabels.length > 0 &&
2909
+ Array.isArray(trace?.mockCommandTexts) &&
2910
+ trace.mockCommandTexts.length === trace.mockCommandLabels.length;
2911
+ const integrationFacts = deriveFrontendIntegrationFacts({
2912
+ mockStrategy: contract.mockApi.strategy,
2913
+ realApiEvidence: { present: false, passed: false },
2914
+ mockEvidenceBound,
2915
+ verificationTargets,
2112
2916
  });
2113
- await runFrontendRepairContractGate({ runDir: meta.runDir });
2917
+ // Typed review terminal (AC-003) from the committed typed event store.
2918
+ const typedKinds = await collectReviewTerminalKinds(meta.runDir, config.reviewFromNodeId);
2919
+ const typedVerdict = typedKinds.length === 1 &&
2920
+ (typedKinds[0] === "approve_review" || typedKinds[0] === "request_review_changes")
2921
+ ? typedKinds[0]
2922
+ : null;
2923
+ // A+B (AC-009): request_review_changes issueCategory maps to the
2924
+ // deterministic §4.10 restart phase. Recorded as a run-owned audit
2925
+ // artifact so the frozen closeout schema is never widened; `unknown`
2926
+ // fails closed to no restart phase.
2927
+ const reviewRestartPhase = typedVerdict === "request_review_changes"
2928
+ ? await collectReviewRestartPhase(meta.runDir, config.reviewFromNodeId)
2929
+ : undefined;
2930
+ if (reviewRestartPhase !== undefined) {
2931
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-review-restart-phase.json", {
2932
+ schemaVersion: 1,
2933
+ nodeId: config.reviewFromNodeId,
2934
+ restartPhase: reviewRestartPhase,
2935
+ });
2936
+ }
2937
+ const facts = frontendCloseoutFactsSchema.parse({
2938
+ schemaVersion: 1,
2939
+ schemaId: "frontend-closeout-facts-v1",
2940
+ coverageMatrix: verificationTargets.map((target, index) => ({
2941
+ verificationTargetId: trace?.targets?.[index]?.id ?? `vt-${index}`,
2942
+ status: target.status === "ok" ? "passed" : "failed",
2943
+ })),
2944
+ lintStatus: "unavailable",
2945
+ integrationFacts,
2946
+ reviewApproval: {
2947
+ typedVerdict,
2948
+ legacyVerdict: null,
2949
+ match: true,
2950
+ },
2951
+ cumulativeDiff: {
2952
+ cumulativeDigest: (typeof context.verificationTrace === "object" &&
2953
+ context.verificationTrace !== null &&
2954
+ "schemaId" in context.verificationTrace
2955
+ ? deterministicSha256({ verificationTrace: context.verificationTrace })
2956
+ : deterministicSha256({})),
2957
+ empty: false,
2958
+ },
2959
+ notRun: { browser: "not-run", visual: "not-run" },
2960
+ pendingRealIntegration: integrationFacts !== "real-api",
2961
+ });
2962
+ const rendered = renderFrontendCloseout(facts);
2963
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-closeout-facts.json", facts);
2964
+ // A+B (AC-010): materialize the deterministic frontend-test handoff.
2965
+ // browser/visual/runtime-a11y are always `not-run` and the handoff is
2966
+ // always `deferred-to-frontend-test`, so it never blocks implementation
2967
+ // and can never be rewritten into a passed result.
2968
+ const handoffTaskId = meta.spec.sourceBinding?.taskId ?? input.task.id;
2969
+ const sourceDigest = meta.spec.sourceBinding
2970
+ ? createHash("sha256")
2971
+ .update(JSON.stringify({
2972
+ taskId: meta.spec.sourceBinding.taskId,
2973
+ sources: meta.spec.sourceBinding.sources,
2974
+ }))
2975
+ .digest("hex")
2976
+ : deterministicSha256({});
2977
+ const handoff = buildFrontendTestHandoff({
2978
+ taskId: handoffTaskId,
2979
+ sourceDigest,
2980
+ candidateHash: facts.cumulativeDiff.cumulativeDigest,
2981
+ });
2982
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-test-handoff.json", handoff);
2983
+ await writeDagNodeTextArtifact(meta.runDir, input.task.id, "frontend-closeout.md", rendered);
2114
2984
  return {
2115
2985
  ok: true,
2116
- stdout: formatFrontendFailureAssessStdout(assessment),
2986
+ stdout: `Frontend closeout rendered\n${rendered.split("\n").slice(0, 8).join("\n")}`,
2117
2987
  stderr: "",
2118
2988
  failureCategory: "success",
2119
2989
  durationMs: Date.now() - started,
2120
- ...{ commandResults },
2121
2990
  };
2122
2991
  }
2123
2992
  catch (error) {
@@ -2127,7 +2996,6 @@ async function executeFrontendVerificationBundle(input, meta) {
2127
2996
  stderr: error instanceof Error ? error.message : String(error),
2128
2997
  failureCategory: "invalid-output",
2129
2998
  durationMs: Date.now() - started,
2130
- ...{ commandResults },
2131
2999
  };
2132
3000
  }
2133
3001
  }
@@ -2452,68 +3320,6 @@ async function executeFrontendTestReports(input, meta) {
2452
3320
  };
2453
3321
  }
2454
3322
  }
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
- }
2517
3323
  async function writeShellPresetProcessReceipt(input) {
2518
3324
  await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
2519
3325
  schemaVersion: 1,
@@ -2766,36 +3572,11 @@ async function executeDagShellNodeInner(input, meta) {
2766
3572
  });
2767
3573
  }
2768
3574
  }
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
- }
3575
+ if (shell?.frontendDesignPolicy) {
3576
+ return executeFrontendDesignPolicy(input, meta);
2796
3577
  }
2797
- if (shell?.frontendLintBaseline) {
2798
- return executeFrontendLintBaseline(input, meta);
3578
+ if (shell?.frontendWriterAdmission) {
3579
+ return executeFrontendWriterAdmission(input, meta);
2799
3580
  }
2800
3581
  if (shell?.frontendVerificationBundle) {
2801
3582
  return executeFrontendVerificationBundle(input, meta);
@@ -2826,6 +3607,12 @@ async function executeDagShellNodeInner(input, meta) {
2826
3607
  };
2827
3608
  }
2828
3609
  }
3610
+ if (shell?.frontendContractImport) {
3611
+ return executeFrontendContractImport(input, meta);
3612
+ }
3613
+ if (shell?.frontendCloseout) {
3614
+ return executeFrontendCloseout(input, meta);
3615
+ }
2829
3616
  if (shell?.frontendTestCaseChecklist) {
2830
3617
  return executeFrontendTestCaseChecklist(input, meta);
2831
3618
  }
@@ -3048,53 +3835,23 @@ async function executeDagShellNodeInner(input, meta) {
3048
3835
  }
3049
3836
  if (shell?.commands?.length === 1 &&
3050
3837
  shell.commands[0] === "frontend-failure-assess-gate") {
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
- }
3838
+ return {
3839
+ ok: false,
3840
+ stdout: "",
3841
+ stderr: "frontend-failure-assess-gate legacy command removed in M8: verify failure is terminal and materializes failure ownership facts in the verification bundle itself",
3842
+ failureCategory: "invalid-output",
3843
+ durationMs: 0,
3844
+ };
3073
3845
  }
3074
3846
  if (shell?.commands?.length === 1 &&
3075
3847
  shell.commands[0] === "frontend-repair-contract-gate") {
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
- }
3848
+ return {
3849
+ ok: false,
3850
+ stdout: "",
3851
+ stderr: "frontend-repair-contract-gate legacy command removed in M8: no same-run repair branch exists in the new topology",
3852
+ failureCategory: "invalid-output",
3853
+ durationMs: 0,
3854
+ };
3098
3855
  }
3099
3856
  if (shell?.projectGovernanceGate) {
3100
3857
  const started = Date.now();
@@ -3421,6 +4178,39 @@ async function executeDagShellNodeInner(input, meta) {
3421
4178
  ...{ commandResults },
3422
4179
  };
3423
4180
  }
4181
+ /**
4182
+ * Enumerate the workspace's concrete file paths for glob writeSet expansion.
4183
+ *
4184
+ * Uses `git ls-files -co --exclude-standard` (tracked + untracked, excluding
4185
+ * gitignored) so the expanded writeSet reflects real files the writer can
4186
+ * touch. Falls back to [] when the workspace is not a git repository or git
4187
+ * fails — in that case glob entries fail closed as non-concrete paths.
4188
+ */
4189
+ async function collectWorkspaceFiles(workspaceRoot) {
4190
+ try {
4191
+ const files = await new Promise((resolve, reject) => {
4192
+ const child = spawn("git", ["ls-files", "-co", "--exclude-standard", "-z"], { cwd: workspaceRoot, stdio: ["ignore", "pipe", "ignore"] });
4193
+ const chunks = [];
4194
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
4195
+ child.on("error", reject);
4196
+ child.on("close", (code) => {
4197
+ if (code === 0) {
4198
+ resolve(Buffer.concat(chunks)
4199
+ .toString("utf8")
4200
+ .split("\0")
4201
+ .filter(Boolean));
4202
+ }
4203
+ else {
4204
+ reject(new Error(`git ls-files exited ${String(code)}`));
4205
+ }
4206
+ });
4207
+ });
4208
+ return files.map((file) => file.replace(/\\/g, "/")).sort();
4209
+ }
4210
+ catch {
4211
+ return [];
4212
+ }
4213
+ }
3424
4214
  export async function executeDagShellNode(input, meta) {
3425
4215
  const handler = namedShellHandler(input.task.shell);
3426
4216
  const result = await executeDagShellNodeInner(input, meta);