@tea-agent/loop-agent 0.42.0-next.9 → 0.42.1

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 (236) hide show
  1. package/CHANGELOG.md +111 -45
  2. package/dist/application/dag/run-dag.js +8 -2
  3. package/dist/application/evaluation/budget.js +19 -1
  4. package/dist/application/task-lifecycle/advance.js +26 -8
  5. package/dist/application/task-lifecycle/observe.js +43 -29
  6. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/program.js +1 -1
  9. package/dist/commands/dag-rerun-task.js +2 -0
  10. package/dist/commands/task-source-prepare.js +3 -1
  11. package/dist/executors/dag-pi-executor.js +2584 -600
  12. package/dist/executors/pi-executor.js +22 -1
  13. package/dist/executors/pi-extension-resolver.js +14 -2
  14. package/dist/executors/pi-sdk-executor.js +140 -39
  15. package/dist/executors/shell-executor.js +135 -55
  16. package/dist/shared/dag-failure-category.js +6 -0
  17. package/dist/shared/frontend-execution-policy.js +23 -0
  18. package/dist/task/config-types.js +4 -0
  19. package/dist/task/contract/apply.js +36 -2
  20. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  21. package/dist/task/source-prepare/ledger-review.js +6 -9
  22. package/dist/task/source-prepare/parse-intent.js +7 -0
  23. package/dist/task/source-prepare/semantic-intake.js +16 -26
  24. package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
  25. package/dist/task/source-references.js +48 -23
  26. package/dist/worker/console/chat/assistant-content.js +23 -2
  27. package/dist/worker/console/chat/browser-policy.js +143 -0
  28. package/dist/worker/console/chat/browser-routes.js +148 -0
  29. package/dist/worker/console/chat/chat-event-store.js +4 -2
  30. package/dist/worker/console/chat/explore-tools.js +13 -0
  31. package/dist/worker/console/chat/pi-runtime.js +173 -94
  32. package/dist/worker/console/chat/resource-loader.js +4 -1
  33. package/dist/worker/console/chat/routes.js +208 -66
  34. package/dist/worker/console/chat/scm-routes.js +217 -0
  35. package/dist/worker/console/chat/scm-service.js +283 -0
  36. package/dist/worker/console/chat/scm-tools.js +111 -0
  37. package/dist/worker/console/chat/sdd-data-alignment.js +78 -11
  38. package/dist/worker/console/chat/session-catalog.js +32 -0
  39. package/dist/worker/console/chat/session-mode-view.js +48 -0
  40. package/dist/worker/console/chat/session-mode.js +218 -0
  41. package/dist/worker/console/chat/session-store.js +27 -7
  42. package/dist/worker/console/chat/shortcuts.js +6 -0
  43. package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
  44. package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
  45. package/dist/worker/console/chat/subagents/index.js +5 -0
  46. package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
  47. package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
  48. package/dist/worker/console/chat/subagents/types.js +13 -0
  49. package/dist/worker/console/chat/terminal-routes.js +216 -0
  50. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  51. package/dist/worker/console/chat/terminal-tools.js +199 -0
  52. package/dist/worker/console/chat/tool-preview.js +21 -0
  53. package/dist/worker/console/chat/tools.js +13 -1
  54. package/dist/worker/console/chat/turn-process.js +1 -0
  55. package/dist/worker/console/interview/tools.js +1 -0
  56. package/dist/worker/console/server.js +2 -28
  57. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
  58. package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-VJWsxWhB.js} +1 -1
  59. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
  60. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
  61. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
  62. package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
  63. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
  73. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
  74. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
  75. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
  76. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
  77. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
  78. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
  81. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
  82. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
  83. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
  84. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
  85. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
  86. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
  87. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
  88. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
  89. package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
  90. package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
  91. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
  92. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
  93. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
  94. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
  95. package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-G1JR40SX.js} +1 -1
  96. package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-CCgPF0oA.js} +5 -5
  97. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
  98. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
  99. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
  100. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
  101. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
  102. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
  103. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
  104. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
  105. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
  106. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
  107. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
  108. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
  110. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
  111. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
  112. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
  113. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
  114. package/dist/worker/console/static/index.html +2 -2
  115. package/dist/worker/console/static-src/chat-view-types.js +1 -1
  116. package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
  117. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +142 -7
  118. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +44 -0
  119. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  120. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
  121. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  122. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
  123. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  124. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +33 -18
  125. package/dist/worker/console/static-src/operator-chat/useChatStream.js +48 -16
  126. package/dist/worker/console/static-src/operator-chat/useChatThread.js +170 -7
  127. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  128. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  129. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  130. package/dist/worker/console/workspace-context.js +15 -1
  131. package/dist/worker/observe/node-transparency.js +81 -72
  132. package/dist/worker/observe/routes.js +20 -1
  133. package/dist/worker/observe/static/constants.js +22 -22
  134. package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
  135. package/dist/worker/observe/static/dag-history-labels.js +1 -0
  136. package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
  137. package/dist/worker/observe/static/dag-inspector-humanize.js +342 -0
  138. package/dist/worker/observe/static/dag-node-purpose.js +10 -10
  139. package/dist/worker/observe/static/dom.js +20 -1
  140. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  141. package/dist/worker/observe/static/format-pool.js +6 -0
  142. package/dist/worker/observe/static/format.js +7 -0
  143. package/dist/worker/observe/static/index.html +4 -4
  144. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  145. package/dist/worker/observe/static/inspector-submission.js +32 -0
  146. package/dist/worker/observe/static/kpi.js +1 -0
  147. package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
  148. package/dist/worker/observe/static/relations.js +7 -5
  149. package/dist/worker/observe/static/router.js +13 -0
  150. package/dist/worker/observe/static/run-processing.js +2 -0
  151. package/dist/worker/observe/static/shell-chrome.js +36 -3
  152. package/dist/worker/observe/static/state.js +35 -2
  153. package/dist/worker/observe/static/styles.css +431 -39
  154. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  155. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  156. package/dist/worker/observe/static/task-history.js +12 -0
  157. package/dist/worker/observe/static/views/batch.js +6 -13
  158. package/dist/worker/observe/static/views/dag-graph.js +51 -4
  159. package/dist/worker/observe/static/views/dag-inspector.js +1069 -426
  160. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  161. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  162. package/dist/worker/observe/static/views/dag.js +64 -22
  163. package/dist/worker/observe/static/views/dags.js +2 -0
  164. package/dist/worker/observe/static/views/dashboard.js +21 -12
  165. package/dist/worker/observe/static/views/failures.js +21 -11
  166. package/dist/worker/observe/static/views/feature.js +11 -29
  167. package/dist/worker/observe/static/views/pool.js +37 -28
  168. package/dist/worker/observe/static/views/run.js +48 -5
  169. package/dist/worker/observe/static/views/session-timeline.js +194 -243
  170. package/dist/worker/observe/static/views/task.js +81 -62
  171. package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
  172. package/dist/workflows/dag/budget-enforcement.js +53 -3
  173. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  174. package/dist/workflows/dag/frontend-capacity.js +9 -0
  175. package/dist/workflows/dag/frontend-contract-facts.js +130 -0
  176. package/dist/workflows/dag/frontend-design-policy.js +103 -17
  177. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  178. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  179. package/dist/workflows/dag/frontend-implementation-contract.js +423 -41
  180. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  181. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  182. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  183. package/dist/workflows/dag/frontend-plan-render.js +21 -3
  184. package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
  185. package/dist/workflows/dag/frontend-recovery-controller.js +32 -8
  186. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  187. package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
  188. package/dist/workflows/dag/frontend-recovery-run.js +146 -16
  189. package/dist/workflows/dag/frontend-review-scopes.js +139 -0
  190. package/dist/workflows/dag/frontend-risk.js +2 -0
  191. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  192. package/dist/workflows/dag/frontend-shadow-dual-write.js +37 -3
  193. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  194. package/dist/workflows/dag/frontend-shape.js +46 -7
  195. package/dist/workflows/dag/frontend-test-execution-evidence.js +49 -0
  196. package/dist/workflows/dag/frontend-typed-event-store.js +14 -0
  197. package/dist/workflows/dag/frontend-verification-trace.js +38 -4
  198. package/dist/workflows/dag/frontend-writer-admission.js +2 -2
  199. package/dist/workflows/dag/init-hybrid.js +70 -35
  200. package/dist/workflows/dag/node-execution.js +225 -226
  201. package/dist/workflows/dag/prompt.js +4 -0
  202. package/dist/workflows/dag/recovery-lease.js +106 -16
  203. package/dist/workflows/dag/rerun-feedback.js +1 -0
  204. package/dist/workflows/dag/rerun-plan.js +96 -4
  205. package/dist/workflows/dag/rerun-run.js +28 -6
  206. package/dist/workflows/dag/rerun-task.js +221 -18
  207. package/dist/workflows/dag/retry-policy.js +27 -10
  208. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  209. package/dist/workflows/dag/runner.js +246 -19
  210. package/dist/workflows/dag/structured-output-repair.js +4 -1
  211. package/dist/workflows/dag/types.js +10 -3
  212. package/dist/workflows/dag/validate.js +10 -8
  213. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  214. package/docs/operations/README.md +2 -0
  215. package/docs/templates/agent-dag.schema.json +2 -2
  216. package/docs/templates/backend-test-dag.json +6 -4
  217. package/docs/templates/frontend-design-contract.md +4 -4
  218. package/docs/templates/frontend-implementation-contract.schema.json +68 -4
  219. package/docs/templates/frontend-implementation-dag.json +5 -5
  220. package/harness.json +1 -1
  221. package/package.json +8 -6
  222. package/skills/frontend-contract/SKILL.md +2 -1
  223. package/skills/frontend-contract/references/contract-protocol.md +19 -3
  224. package/skills/frontend-design-review/SKILL.md +12 -11
  225. package/skills/frontend-plan/SKILL.md +22 -2
  226. package/skills/frontend-plan/references/decision-contract.md +114 -5
  227. package/skills/frontend-plan/references/design-decisions.md +32 -0
  228. package/skills/frontend-review/SKILL.md +10 -11
  229. package/skills/frontend-scout/references/scout-evidence.md +4 -0
  230. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
  231. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
  232. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
  233. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
  234. package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
  235. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
  236. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
@@ -1,3 +1,4 @@
1
+ import { frontendExecutionSchema, collectFrontendExecutionGroups } from "./frontend-execution-groups.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { readFileSync } from "node:fs";
3
4
  import { readFile, stat } from "node:fs/promises";
@@ -9,10 +10,12 @@ import { findPackageRoot } from "../../shared/package-metadata.js";
9
10
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
11
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
11
12
  import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
13
+ import { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
14
+ export { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
12
15
  // Cycle-safe value imports: the check functions are hoisted declarations used
13
16
  // only at call time, so the contract module and the policy module can depend
14
17
  // on each other without module-init ordering issues.
15
- import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
18
+ import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, checkRequiredDeliverables, checkUxNameCollisions, } from "./frontend-design-policy.js";
16
19
  import { isDagSourceBindingV2 } from "./types.js";
17
20
  import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
18
21
  import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
@@ -25,6 +28,7 @@ export const frontendNormalizationActionSchema = z.enum([
25
28
  "drop-unresolved-verification-symbols",
26
29
  "inject-source-binding",
27
30
  "compact-canonical-contract",
31
+ "resolve-verification-command-ids",
28
32
  ]);
29
33
  export function sha256Hex(input) {
30
34
  return createHash("sha256").update(input, "utf8").digest("hex");
@@ -95,20 +99,200 @@ export async function writeCompactDeterministicJsonArtifact(runDir, relativePath
95
99
  return { path: targetPath, sha256: sha256Hex(json) };
96
100
  }
97
101
  /**
98
- * Extract frozen command labels from the DAG run spec (run.json).
99
- * The run spec is written before any node executes, so it is always available
100
- * when the prewrite gate materializes the contract.
102
+ * Verification mode v2 (AC-V2-001..004).
103
+ *
104
+ * The mode is owned by the runtime, never by the model: plan tools submit a
105
+ * `commandId` referencing the generation-time frozen command directory, and
106
+ * materialization resolves `mode` + `commandLabel` from that directory. This
107
+ * makes contradictory combinations such as `type: component` bound to
108
+ * `npm run build` unrepresentable instead of failing late at the writer
109
+ * focused-check.
110
+ */
111
+ export const frontendVerificationModeSchema = z.enum(["static", "behavior"]);
112
+ /**
113
+ * Optional display-only test scope. It never participates in control flow:
114
+ * trace checks, writeSet derivation and behavior-required evaluation read
115
+ * only the runtime-derived `mode`.
116
+ */
117
+ export const frontendVerificationScopeSchema = z.enum([
118
+ "unit",
119
+ "component",
120
+ "integration",
121
+ ]);
122
+ function laneToVerificationMode(lane) {
123
+ return lane === "static" || lane === "lint" ? "static" : "behavior";
124
+ }
125
+ const VERIFY_RUNNER_RE = /\b(vitest|jest|mocha|ava|tap|playwright|cypress|testcafe|nightwatch|webdriverio|pytest|go test)\b/i;
126
+ const VERIFY_E2E_RE = /(^|[\s:"'=/-])e2e([\s"'/-]|$)/i;
127
+ const VERIFY_STATIC_SCRIPT_WORDS = new Set([
128
+ "typecheck",
129
+ "check",
130
+ "lint",
131
+ "eslint",
132
+ "tsc",
133
+ "build",
134
+ ]);
135
+ // A script segment carrying test semantics wins over static words:
136
+ // `test:build` builds nothing observable by itself — it runs tests, and
137
+ // `check:unit` is a unit-test entrypoint, not a project-wide check.
138
+ const VERIFY_BEHAVIOR_SCRIPT_WORDS = new Set([
139
+ "test",
140
+ "tests",
141
+ "unit",
142
+ "spec",
143
+ "specs",
144
+ "e2e",
145
+ "integration",
146
+ "coverage",
147
+ ]);
148
+ function extractRunScriptName(command) {
149
+ // Skip package-manager flags between the binary and the script, e.g.
150
+ // `npm --prefix website run build` executes the `build` script.
151
+ const tokens = command.match(/(?:npm|pnpm|yarn|bun)(?:\.cmd|\.ps1)?\s+(.+)/i)?.[1];
152
+ if (!tokens)
153
+ return undefined;
154
+ const parts = tokens.split(/\s+/).filter(Boolean);
155
+ let index = 0;
156
+ while (index < parts.length) {
157
+ const part = parts[index].replace(/["']/g, "");
158
+ if (part === "--prefix" ||
159
+ part === "--cwd" ||
160
+ part === "-C" ||
161
+ part === "--workspace") {
162
+ index += 2;
163
+ continue;
164
+ }
165
+ if (part.startsWith("-")) {
166
+ index += 1;
167
+ continue;
168
+ }
169
+ if (/^(run|exec)$/i.test(part)) {
170
+ index += 1;
171
+ continue;
172
+ }
173
+ return part;
174
+ }
175
+ return undefined;
176
+ }
177
+ /**
178
+ * Whether a command carries known verification semantics. Markdown intake
179
+ * uses this before lane classification so prose cannot authorize an arbitrary
180
+ * package script such as `npm run deploy`. Explicit task verifyCommands remain
181
+ * operator-authorized and intentionally do not use this admission check.
182
+ */
183
+ export function isRecognizedFrontendVerifyCommandText(command) {
184
+ if (VERIFY_RUNNER_RE.test(command) || VERIFY_E2E_RE.test(command))
185
+ return true;
186
+ const scriptName = extractRunScriptName(command);
187
+ if (scriptName) {
188
+ const segments = scriptName.toLowerCase().split(/[:/_-]+/);
189
+ return segments.some((segment) => VERIFY_BEHAVIOR_SCRIPT_WORDS.has(segment) ||
190
+ VERIFY_STATIC_SCRIPT_WORDS.has(segment));
191
+ }
192
+ const head = command.trim().split(/\s+/, 1)[0] ?? "";
193
+ const binary = head.split(/[\\/]/).pop() ?? "";
194
+ return (/^(tsc|eslint|stylelint|lint|typecheck|check|build)$/i.test(binary) ||
195
+ /\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
196
+ /\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command));
197
+ }
198
+ /**
199
+ * Classify a raw verification command text into the runtime-owned mode.
200
+ * Shared single implementation used by DAG generation (lane split) and
201
+ * documented here so generation and validation can never disagree.
202
+ *
203
+ * Order matters: test runners and e2e harnesses always prove behavior, even
204
+ * when their arguments mention build/check/lint paths (e.g.
205
+ * `npx vitest run test/build.test.ts`). For npm scripts, a segment with
206
+ * test semantics (`test:build`, `check:unit`) wins over static words; pure
207
+ * static scripts (`npm run check`, `check:types`) and leading binaries
208
+ * (`tsc`, `eslint`) stay static. Package-manager flags (`npm --prefix
209
+ * website run build`) are skipped when locating the script.
210
+ */
211
+ export function classifyFrontendVerifyCommandText(command) {
212
+ if (VERIFY_RUNNER_RE.test(command) || VERIFY_E2E_RE.test(command)) {
213
+ return "behavior";
214
+ }
215
+ const scriptName = extractRunScriptName(command);
216
+ if (scriptName) {
217
+ const segments = scriptName.toLowerCase().split(/[:/_-]+/);
218
+ if (segments.some((segment) => VERIFY_BEHAVIOR_SCRIPT_WORDS.has(segment))) {
219
+ return "behavior";
220
+ }
221
+ return segments.some((segment) => VERIFY_STATIC_SCRIPT_WORDS.has(segment))
222
+ ? "static"
223
+ : "behavior";
224
+ }
225
+ const head = command.trim().split(/\s+/, 1)[0] ?? "";
226
+ const binary = head.split(/[\\/]/).pop() ?? "";
227
+ if (/^(tsc|eslint|stylelint|lint|typecheck|check|build)$/i.test(binary) ||
228
+ /\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
229
+ /\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command)) {
230
+ return "static";
231
+ }
232
+ return "behavior";
233
+ }
234
+ export function slugFrontendVerifyCommandId(label, taken) {
235
+ const slug = label
236
+ .toLowerCase()
237
+ .replace(/[^a-z0-9]+/g, "-")
238
+ .replace(/^-+|-+$/g, "")
239
+ .slice(0, 48) || "verify";
240
+ let candidate = `verify-${slug}`;
241
+ let counter = 2;
242
+ while (taken.has(candidate)) {
243
+ candidate = `verify-${slug}-${counter++}`;
244
+ }
245
+ taken.add(candidate);
246
+ return candidate;
247
+ }
248
+ /**
249
+ * Build the generation-time frozen command directory. Lanes come from the
250
+ * DAG generation split (init-hybrid classifies each verify command once);
251
+ * labels stay human-readable while `commandId` is the stable machine key
252
+ * the plan references. Mock/lint lanes keep their provenance in `lane` but
253
+ * map to the behavior/static mode the trace gate enforces. Command texts
254
+ * (parallel to labels; usually from the bundle evidence) bind each id to
255
+ * the exact frozen command so same-label entries across lanes cannot drift
256
+ * into the same identity silently.
257
+ */
258
+ export function buildFrontendVerifyCommandDirectory(lanes) {
259
+ const taken = new Set();
260
+ const entries = [];
261
+ const push = (labels, lane, texts) => {
262
+ labels.forEach((label, index) => {
263
+ const trimmed = label.trim();
264
+ if (!trimmed)
265
+ return;
266
+ entries.push({
267
+ commandId: slugFrontendVerifyCommandId(trimmed, taken),
268
+ label: trimmed,
269
+ mode: laneToVerificationMode(lane),
270
+ lane,
271
+ command: texts?.[index]?.trim() ?? "",
272
+ });
273
+ });
274
+ };
275
+ push(lanes.staticLabels, "static", lanes.staticCommandTexts);
276
+ push(lanes.lintLabels ?? [], "lint", lanes.lintCommandTexts);
277
+ push(lanes.behaviorLabels, "behavior", lanes.behaviorCommandTexts);
278
+ push(lanes.mockLabels ?? [], "mock", lanes.mockCommandTexts);
279
+ return entries;
280
+ }
281
+ /**
282
+ * Derive the frozen command directory from the DAG run spec (run.json),
283
+ * reading the per-lane evidence of the `frontend-verify-shell` bundle. The
284
+ * run spec is written before any node executes, so it is always available
285
+ * when the prewrite gate materializes the contract. When absent (e.g. test
286
+ * fixtures), there is no directory to validate against and callers skip
287
+ * directory validation downstream — mirroring the legacy label-set behavior.
101
288
  */
102
- async function deriveFrozenCommandLabelsFromRun(runDir) {
289
+ export async function deriveFrontendVerifyCommandDirectoryFromRun(runDir) {
103
290
  const specPath = path.join(runDir, "run.json");
104
291
  let raw;
105
292
  try {
106
293
  raw = await readFile(specPath, "utf8");
107
294
  }
108
295
  catch {
109
- // run.json is guaranteed to exist in production (DAG runner writes it
110
- // before any node executes). When absent (e.g. test fixtures), there is
111
- // no frozen set to validate against and the check is skipped downstream.
112
296
  return [];
113
297
  }
114
298
  let spec;
@@ -116,21 +300,90 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
116
300
  spec = JSON.parse(raw);
117
301
  }
118
302
  catch (error) {
119
- throw new Error(`cannot derive frozen command labels: run.json is not valid JSON at ${specPath}: ${error.message}`);
120
- }
121
- const labels = new Set();
303
+ throw new Error(`cannot derive frontend verify command directory: run.json is not valid JSON at ${specPath}: ${error.message}`);
304
+ }
305
+ const lanes = {
306
+ staticLabels: [],
307
+ behaviorLabels: [],
308
+ mockLabels: [],
309
+ lintLabels: [],
310
+ staticCommandTexts: [],
311
+ behaviorCommandTexts: [],
312
+ mockCommandTexts: [],
313
+ lintCommandTexts: [],
314
+ };
122
315
  for (const task of spec.tasks ?? []) {
123
- const cmdLabels = task.shell?.verifyEvidence?.commandLabels;
124
- if (cmdLabels) {
125
- for (const label of cmdLabels)
126
- labels.add(label);
316
+ const bundle = task.shell?.frontendVerificationBundle;
317
+ if (!bundle)
318
+ continue;
319
+ const lanePairs = [
320
+ ["static", bundle.staticEvidence],
321
+ ["behavior", bundle.behaviorEvidence],
322
+ ["mock", bundle.mockEvidence],
323
+ ["lint", bundle.lintEvidence],
324
+ ];
325
+ for (const [lane, evidence] of lanePairs) {
326
+ const labels = evidence?.commandLabels ?? [];
327
+ const texts = evidence?.commandTexts ?? [];
328
+ // run.json is untrusted input: labels and texts must zip 1:1.
329
+ // A lane that violates the alignment fails closed instead of
330
+ // binding ids to the wrong commands.
331
+ if (texts.length > 0 && texts.length !== labels.length) {
332
+ throw new Error(`cannot derive frontend verify command directory: ${lane} evidence commandTexts length (${texts.length}) does not match commandLabels length (${labels.length}) in ${specPath}`);
333
+ }
334
+ lanes[`${lane}Labels`].push(...labels);
335
+ lanes[`${lane}CommandTexts`].push(...texts);
127
336
  }
128
337
  }
129
- return [...labels];
338
+ if (lanes.staticLabels.length === 0 &&
339
+ lanes.behaviorLabels.length === 0 &&
340
+ lanes.mockLabels.length === 0 &&
341
+ lanes.lintLabels.length === 0) {
342
+ return [];
343
+ }
344
+ return buildFrontendVerifyCommandDirectory(lanes);
345
+ }
346
+ /**
347
+ * A behavior verification target must bind a test file: the trace gate
348
+ * proves behavior targets by finding the target id in a real
349
+ * describe/it/test title inside `file`. Enforced at the plan boundary and
350
+ * at materialization (path shape only — the writer may create the file).
351
+ *
352
+ * Only code files count (the gate reads titles from source): fixtures and
353
+ * data files such as `test/fixtures/data.json` are rejected even under a
354
+ * test directory, as are helper/support/setup files (`cypress/fixtures/*`,
355
+ * `tests/support/*`, `setup.*`) that never carry describe/it/test titles.
356
+ * Cypress (`*.cy.*`, `cypress/`) and e2e (`e2e/`) entries are accepted —
357
+ * the repo supports those harnesses.
358
+ */
359
+ export function isFrontendTestFilePath(filePath) {
360
+ const normalized = filePath.replace(/\\/g, "/");
361
+ const base = normalized.split("/").pop() ?? "";
362
+ if (!/\.(c|m)?[jt]sx?$/.test(base))
363
+ return false;
364
+ const segments = normalized.toLowerCase().split("/");
365
+ // `helpers` is deliberately NOT excluded: source helper directories
366
+ // (e.g. `src/helpers/format.test.ts`) legitimately contain unit tests.
367
+ if (segments.some((segment) => segment === "fixtures" || segment === "support")) {
368
+ return false;
369
+ }
370
+ if (/^setup\.[^/]+$/.test(base))
371
+ return false;
372
+ if (/\.cy\.[^/]+$/.test(base))
373
+ return true;
374
+ if (/(^|\/)__tests__\//.test(normalized))
375
+ return true;
376
+ if (/(^|\/)(tests?|e2e|cypress)\//.test(normalized))
377
+ return true;
378
+ if (/\.test\.[^/]+$/.test(normalized))
379
+ return true;
380
+ if (/\.spec\.[^/]+$/.test(normalized))
381
+ return true;
382
+ return false;
130
383
  }
131
384
  /**
132
385
  * Extract the design-policy allowedDependencies set from the DAG run spec
133
- * (run.json), mirroring `deriveFrozenCommandLabelsFromRun`. Used by the plan
386
+ * (run.json), mirroring `deriveFrontendVerifyCommandDirectoryFromRun`. Used by the plan
134
387
  * node's front-loaded policy pre-check so dependency declarations can be
135
388
  * corrected through the retry ladder before the policy shell (the authority)
136
389
  * re-runs the same check on committed facts.
@@ -350,12 +603,30 @@ const uiComponentChoiceSchema = z
350
603
  decision: z.enum(["specified", "reuse-existing", "new"]),
351
604
  specReference: uiComponentSpecReferenceSchema.nullable().optional(),
352
605
  // Optional: the decision enum already carries the semantic (specified →
353
- // specReference, new → task-source traceability, reuse-existing → no
354
- // rationale needed).
355
- // Omit for reuse-existing to keep plan output small.
606
+ // specReference, new → task-source traceability, reuse-existing →
607
+ // evidencePath). Omit for reuse-existing to keep plan output small.
356
608
  rationale: z.string().min(1).optional(),
609
+ // Multi-id coverage (dogfood r-plan-quality): ONE choice may cover many
610
+ // UI states/interactions explicitly instead of the old implicit
611
+ // "reuse-existing covers everything" wildcard.
612
+ covers: z.array(z.string().min(1)).optional(),
613
+ // reuse-existing evidence (dogfood dag-1788504923861-0f7b17a9: 18
614
+ // reuse-existing rows cited conventions in a not-yet-written
615
+ // src/planner.ts): a repo-relative path to the existing file that
616
+ // proves the reuse. Enforced by superRefine + fresh-existence checks.
617
+ evidencePath: safePath.optional(),
357
618
  })
358
- .strict();
619
+ .strict()
620
+ .superRefine((choice, ctx) => {
621
+ if (choice.decision === "reuse-existing" &&
622
+ !(choice.evidencePath && choice.evidencePath.trim().length > 0)) {
623
+ ctx.addIssue({
624
+ code: z.ZodIssueCode.custom,
625
+ path: ["evidencePath"],
626
+ message: "decision=reuse-existing requires evidencePath naming the existing repo file that proves the reuse; use decision=new for greenfield paths",
627
+ });
628
+ }
629
+ });
359
630
  const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
360
631
  /**
361
632
  * Normalize unstable model emissions before the strict schema sees them:
@@ -430,12 +701,18 @@ export const frontendImplementationContractSchema = z
430
701
  files: z.array(safePath).min(1),
431
702
  routes: z.array(z.string().startsWith("/")).optional(),
432
703
  publicApiChanges: z.array(z.string().min(1)).optional(),
704
+ // Runtime-owned deliverable contract (dogfood r-plan-quality):
705
+ // explicit, source-bound file inventory committed by Contract.
706
+ // The design-policy completeness gate checks ONLY these —
707
+ // allowedPaths is a write boundary, not a coverage obligation.
708
+ requiredDeliverables: z.array(safePath).optional(),
433
709
  })
434
710
  .strict(),
435
711
  requirements: z.array(z
436
712
  .object({
437
713
  id,
438
714
  expectedOutcome: z.string().min(1),
715
+ execution: frontendExecutionSchema.optional(),
439
716
  implementationTargets: z.array(safePath),
440
717
  verificationTargetIds: z.array(z.string().min(1)),
441
718
  evidenceGap: gap.optional(),
@@ -507,12 +784,18 @@ export const frontendImplementationContractSchema = z
507
784
  verificationTargets: z.array(z
508
785
  .object({
509
786
  id: z.string().min(1),
510
- type: z.enum(["static", "unit", "component", "integration", "mock"]),
787
+ // Runtime-owned verification mode, resolved from commandId
788
+ // against the frozen command directory at materialization.
789
+ mode: frontendVerificationModeSchema,
790
+ // Stable machine key into the frozen command directory.
791
+ commandId: z.string().min(1),
511
792
  commandLabel: z.string().min(1),
512
793
  file: safePath,
513
794
  symbol: z.preprocess((value) => (value === "" || value === null ? undefined : value), z.string().min(1).optional()),
514
795
  requirementIds: z.array(id),
515
796
  uiStates: z.array(z.string().min(1)),
797
+ // Display-only test scope; never read by control flow.
798
+ scope: frontendVerificationScopeSchema.optional(),
516
799
  })
517
800
  .strict()).min(1),
518
801
  evidenceGaps: z.array(gap).optional(),
@@ -525,6 +808,12 @@ export const frontendImplementationContractSchema = z
525
808
  })
526
809
  .strict()
527
810
  .superRefine((value, ctx) => {
811
+ try {
812
+ collectFrontendExecutionGroups(value.requirements);
813
+ }
814
+ catch (error) {
815
+ ctx.addIssue({ code: "custom", path: ["requirements"], message: String(error) });
816
+ }
528
817
  const stableIdentities = [
529
818
  {
530
819
  path: "requirements",
@@ -587,6 +876,27 @@ export const frontendImplementationContractSchema = z
587
876
  });
588
877
  }
589
878
  }
879
+ // Behavior evidence must be anchored to be machine-traceable: a
880
+ // behavior target that names no requirement and no UI state produces
881
+ // coverage the trace gate and review cannot verify. Contracts that
882
+ // declare no UI state and no interaction (pure-logic) are exempt from
883
+ // the UI-state half — the schema allows `uiStates: []` there.
884
+ if (target.mode === "behavior" && target.requirementIds.length === 0) {
885
+ ctx.addIssue({
886
+ code: "custom",
887
+ message: `behavior verification target ${target.id} must reference at least one requirement`,
888
+ path: ["verificationTargets"],
889
+ });
890
+ }
891
+ if (target.mode === "behavior" &&
892
+ target.uiStates.length === 0 &&
893
+ (value.uiStates.length > 0 || value.interactions.length > 0)) {
894
+ ctx.addIssue({
895
+ code: "custom",
896
+ message: `behavior verification target ${target.id} must reference at least one declared UI state`,
897
+ path: ["verificationTargets"],
898
+ });
899
+ }
590
900
  }
591
901
  for (const requirement of value.requirements) {
592
902
  if (!known.has(requirement.id))
@@ -1357,7 +1667,12 @@ export function compactFrontendImplementationContract(value) {
1357
1667
  const targets = asRecord(record.targets);
1358
1668
  if (targets) {
1359
1669
  const compactTargets = { ...targets };
1360
- for (const field of ["files", "routes", "publicApiChanges"]) {
1670
+ for (const field of [
1671
+ "files",
1672
+ "routes",
1673
+ "publicApiChanges",
1674
+ "requiredDeliverables",
1675
+ ]) {
1361
1676
  if (Object.hasOwn(compactTargets, field)) {
1362
1677
  compactTargets[field] = dedupeJsonArray(compactTargets[field]);
1363
1678
  }
@@ -1666,13 +1981,32 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1666
1981
  for (const [index, item] of rawVerification.entries()) {
1667
1982
  const vt = asRecord(item) ?? {};
1668
1983
  const id = asString(vt.id) || `VT-${String(index + 1).padStart(3, "0")}`;
1669
- const typeRaw = asString(vt.type) || asString(vt.phase) || "unit";
1670
- const type = ["static", "unit", "component", "integration", "mock"].includes(typeRaw)
1671
- ? typeRaw
1672
- : typeRaw.includes("type")
1984
+ // Legacy compatibility input may still carry a model-written `type`
1985
+ // and a bare `commandLabel`. Map them onto the v2 shape: the five
1986
+ // legacy types collapse to the runtime mode (only `static` stays
1987
+ // static), the commandId is slugged from the label, and a legacy
1988
+ // unit/component/integration type is preserved as display-only scope.
1989
+ // Production plan facts already carry commandId and are resolved
1990
+ // against the frozen directory during materialization instead.
1991
+ const legacyType = asString(vt.type) || asString(vt.phase);
1992
+ const mode = asString(vt.mode) === "static" ||
1993
+ asString(vt.mode) === "behavior"
1994
+ ? asString(vt.mode)
1995
+ : legacyType === "static"
1673
1996
  ? "static"
1674
- : "unit";
1997
+ : "behavior";
1675
1998
  const commandLabel = asString(vt.commandLabel) || asString(vt.command);
1999
+ const commandId = asString(vt.commandId) ||
2000
+ slugFrontendVerifyCommandId(commandLabel || id, new Set());
2001
+ const scope = asString(vt.scope) === "unit" ||
2002
+ asString(vt.scope) === "component" ||
2003
+ asString(vt.scope) === "integration"
2004
+ ? asString(vt.scope)
2005
+ : legacyType === "unit" ||
2006
+ legacyType === "component" ||
2007
+ legacyType === "integration"
2008
+ ? legacyType
2009
+ : undefined;
1676
2010
  const file = asString(vt.file) ||
1677
2011
  (Array.isArray(vt.symbols) ? targetFiles.find((p) => p.includes("__tests__")) : "") ||
1678
2012
  targetFiles.find((p) => p.includes("__tests__")) ||
@@ -1681,10 +2015,12 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1681
2015
  const uiStateNames = asStringArray(vt.uiStates);
1682
2016
  verificationTargets.push({
1683
2017
  id,
1684
- type,
2018
+ mode,
2019
+ commandId,
1685
2020
  commandLabel,
1686
2021
  file,
1687
2022
  symbol: asString(vt.symbol) || undefined,
2023
+ ...(scope ? { scope } : {}),
1688
2024
  requirementIds: requirementIds.length > 0
1689
2025
  ? requirementIds
1690
2026
  : [...canonicalBinding.requirementIds],
@@ -1735,6 +2071,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1735
2071
  ? { sourceFragmentIds: req.sourceFragmentIds }
1736
2072
  : {}),
1737
2073
  ...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
2074
+ ...(req.execution !== undefined ? { execution: req.execution } : {}),
1738
2075
  };
1739
2076
  // Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
1740
2077
  if (gapText) {
@@ -2008,19 +2345,42 @@ export async function analyzeFrontendImplementationContract(input) {
2008
2345
  catch (error) {
2009
2346
  fail("blocked", error instanceof Error ? error.message : String(error), candidateJsonSha256);
2010
2347
  }
2011
- // Validate verificationTarget commandLabels against frozen command set.
2012
- // The frozen set is derived from DAG verification shell task verifyEvidence.
2013
- const frozenLabels = await deriveFrozenCommandLabelsFromRun(input.runDir);
2014
- if (frozenLabels.length > 0) {
2015
- const frozen = new Set(frozenLabels);
2016
- const parsedVt = asRecord(parsed)?.verificationTargets;
2348
+ // Resolve verificationTarget commandIds against the frozen command
2349
+ // directory (per-lane evidence of the frontend-verify-shell bundle in
2350
+ // run.json). The directory is the only authority for mode and label: a
2351
+ // plan that references an unknown commandId, or binds a behavior command
2352
+ // to a non-test file, is rejected here with plan-attributable codes
2353
+ // instead of failing late at the writer focused-check.
2354
+ const verifyDirectory = await deriveFrontendVerifyCommandDirectoryFromRun(input.runDir);
2355
+ if (verifyDirectory.length > 0) {
2356
+ const byCommandId = new Map(verifyDirectory.map((entry) => [entry.commandId, entry]));
2357
+ const available = verifyDirectory
2358
+ .map((entry) => `${entry.commandId} (${entry.mode}: ${entry.label})`)
2359
+ .join(", ");
2360
+ const parsedRecord = asRecord(parsed);
2361
+ const parsedVt = parsedRecord?.verificationTargets;
2017
2362
  if (Array.isArray(parsedVt)) {
2018
2363
  for (const vt of parsedVt) {
2019
- const label = asString(asRecord(vt)?.commandLabel);
2020
- if (label && !frozen.has(label)) {
2021
- fail("blocked", `invalid-output: verificationTarget commandLabel "${label}" is not in the frozen command set [${[...frozen].join(", ")}]`, candidateJsonSha256);
2364
+ const record = asRecord(vt);
2365
+ if (!record)
2366
+ continue;
2367
+ const vtId = asString(record.id) || "(missing id)";
2368
+ const commandId = asString(record.commandId);
2369
+ if (!commandId) {
2370
+ fail("retryable-invalid", `invalid-output: verification target "${vtId}" must reference a frozen command: set commandId to one of [${available}]`, candidateJsonSha256);
2371
+ }
2372
+ const entry = byCommandId.get(commandId);
2373
+ if (!entry) {
2374
+ fail("retryable-invalid", `invalid-output: verification-target-unknown-command: verification target "${vtId}" references unknown commandId "${commandId}"; available frozen commands: [${available}]`, candidateJsonSha256);
2375
+ }
2376
+ record.mode = entry.mode;
2377
+ record.commandLabel = entry.label;
2378
+ const file = asString(record.file);
2379
+ if (entry.mode === "behavior" && file && !isFrontendTestFilePath(file)) {
2380
+ fail("retryable-invalid", `invalid-output: verification-target-phase-mismatch: verification target "${vtId}" binds behavior command "${entry.label}" (${entry.commandId}) to non-test file "${file}"; behavior targets must bind a test file (__tests__/, tests?/, e2e/, cypress/, *.test.*, *.spec.*, *.cy.*) so the trace gate can match the target id in a real describe/it/test title`, candidateJsonSha256);
2022
2381
  }
2023
2382
  }
2383
+ pushAction("resolve-verification-command-ids");
2024
2384
  }
2025
2385
  }
2026
2386
  const parsedTargets = asRecord(parsed)?.targets;
@@ -2104,6 +2464,16 @@ export async function analyzeFrontendImplementationContract(input) {
2104
2464
  files: referencedConcretePaths,
2105
2465
  };
2106
2466
  }
2467
+ // Both Plan precheck and policy materialization consume this exact inventory.
2468
+ // Never trust a Plan patch or infer delivery obligations from prose.
2469
+ const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.contractNodeId ?? "frontend-contract-pi", "contract-typed-facts.jsonl"));
2470
+ const requiredDeliverables = extractRequiredDeliverablePaths(contractRecords
2471
+ .filter((record) => record.phase === "committed")
2472
+ .map((record) => record.fact));
2473
+ delete result.data.targets.requiredDeliverables;
2474
+ if (requiredDeliverables.length > 0) {
2475
+ result.data.targets.requiredDeliverables = requiredDeliverables;
2476
+ }
2107
2477
  const blockingGaps = [
2108
2478
  ...(result.data.evidenceGaps ?? []),
2109
2479
  ...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
@@ -2139,6 +2509,10 @@ function splitFrontendContractReviewGroup(canonical, fields) {
2139
2509
  if (!Object.hasOwn(canonical, field))
2140
2510
  continue;
2141
2511
  const fieldValue = canonical[field];
2512
+ // JSON omits undefined object properties; the index must describe the
2513
+ // persisted contract, not phantom keys retained by normalization.
2514
+ if (fieldValue === undefined)
2515
+ continue;
2142
2516
  if (!Array.isArray(fieldValue)) {
2143
2517
  const candidate = { ...data, [field]: fieldValue };
2144
2518
  if (Object.keys(data).length > 0 &&
@@ -2311,6 +2685,7 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
2311
2685
  "/sourceBinding",
2312
2686
  "/riskLevel",
2313
2687
  "/targets/files",
2688
+ "/targets/requiredDeliverables",
2314
2689
  "/mockApi/productionDefaultOff",
2315
2690
  ];
2316
2691
  const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
@@ -2359,6 +2734,8 @@ function frontendPlanPatchProtectedPathViolations(patch) {
2359
2734
  const targets = asRecord(record.targets);
2360
2735
  if (targets && Object.hasOwn(targets, "files"))
2361
2736
  violations.push("/targets/files");
2737
+ if (targets && Object.hasOwn(targets, "requiredDeliverables"))
2738
+ violations.push("/targets/requiredDeliverables");
2362
2739
  const mockApi = asRecord(record.mockApi);
2363
2740
  if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
2364
2741
  violations.push("/mockApi/productionDefaultOff");
@@ -2395,7 +2772,7 @@ async function dropUnresolvedVerificationSymbols(input) {
2395
2772
  // it. This is the root fix for the recurring fabricated-symbol error
2396
2773
  // class: the field was allowed for static, the prompt did not forbid
2397
2774
  // it, and models habitually fill it; the compiler removes it instead.
2398
- if (target.type === "static") {
2775
+ if (target.mode === "static") {
2399
2776
  if (symbol) {
2400
2777
  delete target.symbol;
2401
2778
  if (!actions.includes("strip-static-verification-symbols"))
@@ -2543,6 +2920,8 @@ export async function analyzeFrontendPlanPatchCandidate(input) {
2543
2920
  const policyPreFindings = [
2544
2921
  ...checkUiDesignCoverage(analysis.canonical),
2545
2922
  ...checkUiStateAttribution(analysis.canonical),
2923
+ ...checkRequiredDeliverables(analysis.canonical),
2924
+ ...checkUxNameCollisions(analysis.canonical),
2546
2925
  ...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
2547
2926
  ...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
2548
2927
  ];
@@ -2761,9 +3140,11 @@ export async function buildContractFromCommittedFacts(input) {
2761
3140
  }
2762
3141
  // b. Committed contract terminal fact (contract-finalized).
2763
3142
  const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
2764
- const contractKinds = new Set(contractRecords
3143
+ const committedContractFacts = contractRecords
2765
3144
  .filter((record) => record.phase === "committed")
2766
- .map((record) => record.fact.kind)
3145
+ .map((record) => record.fact);
3146
+ const contractKinds = new Set(committedContractFacts
3147
+ .map((fact) => fact?.kind)
2767
3148
  .filter((kind) => typeof kind === "string"));
2768
3149
  if (!contractKinds.has("contract-finalized")) {
2769
3150
  return {
@@ -2818,6 +3199,7 @@ export async function buildContractFromCommittedFacts(input) {
2818
3199
  runDir: input.runDir,
2819
3200
  rawContractText: serializeDeterministicJson(merged),
2820
3201
  sourceBinding: input.sourceBinding,
3202
+ contractNodeId,
2821
3203
  });
2822
3204
  return {
2823
3205
  ok: true,