@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,11 +1,12 @@
1
1
  import { deterministicSha256, } from "./frontend-implementation-contract.js";
2
2
  import { pathMatchesPattern } from "../../shared/git-progress.js";
3
+ import { FRONTEND_MAX_IMPLEMENTATION_FILES } from "./frontend-capacity.js";
3
4
  const SHA256_HEX = /^[a-f0-9]{64}$/;
4
5
  const NPM_PACKAGE_NAME = /^(?:@[a-zA-Z0-9_.-]+\/)?[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
5
6
  /** Max concrete implementation files a single writeSet may carry before the
6
7
  * plan is asked to split. Kept at 15: beyond this, a single implement node's
7
8
  * read/edit loop inflates provider context toward overflow. */
8
- const MAX_WRITE_SET_FILES = 15;
9
+ const MAX_WRITE_SET_FILES = FRONTEND_MAX_IMPLEMENTATION_FILES;
9
10
  function normalizeRelativePath(value) {
10
11
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
11
12
  }
@@ -28,31 +29,37 @@ function sortFindings(findings) {
28
29
  * visual UI-state dimension (it cannot cover behavioural interactions). */
29
30
  export function checkUiDesignCoverage(contract) {
30
31
  const findings = [];
31
- const purposes = new Set((contract.uiComponentChoices ?? []).map((choice) => choice.purpose));
32
- const hasStyling = Boolean(contract.stylingStrategy?.trim());
33
- // Reuse of an existing component/pattern covers behavioural interactions
34
- // without forcing one uiComponentChoices row per interaction: the model
35
- // should not enumerate every click/hover on a reused Button. Only
36
- // interactions with no reuse and no explicit purpose are flagged.
37
- const hasReuseChoice = (contract.uiComponentChoices ?? []).some((choice) => choice.decision === "reuse-existing");
32
+ // Explicit per-id coverage (dogfood r-plan-quality): one choice covers an
33
+ // interaction/state when its purpose equals the name OR the choice lists
34
+ // the name in covers. The old wildcards are gone — a single reuse-existing
35
+ // row no longer passes every interaction, and a non-empty stylingStrategy
36
+ // no longer passes every state.
37
+ const covered = new Set();
38
+ for (const choice of contract.uiComponentChoices ?? []) {
39
+ if (choice.purpose.trim())
40
+ covered.add(choice.purpose);
41
+ for (const id of choice.covers ?? []) {
42
+ if (id.trim())
43
+ covered.add(id);
44
+ }
45
+ }
38
46
  for (const state of contract.uiStates) {
39
47
  if (!state.applicable)
40
48
  continue;
41
- const covered = purposes.has(state.name) || hasStyling;
42
- if (!covered) {
49
+ if (!covered.has(state.name)) {
43
50
  findings.push({
44
51
  code: "ui-design-coverage-missing",
45
- message: `applicable UI state "${state.name}" has no matching uiComponentChoices.purpose or stylingStrategy`,
52
+ message: `applicable UI state "${state.name}" is not covered by any uiComponentChoices purpose or covers entry`,
46
53
  path: state.name,
47
54
  });
48
55
  }
49
56
  }
50
57
  for (const interaction of contract.interactions) {
51
- if (purposes.has(interaction.name) || hasReuseChoice)
58
+ if (covered.has(interaction.name))
52
59
  continue;
53
60
  findings.push({
54
61
  code: "ui-design-coverage-missing",
55
- message: `interaction "${interaction.name}" has no matching uiComponentChoices.purpose and no reuse-existing choice covering it`,
62
+ message: `interaction "${interaction.name}" is not covered by any uiComponentChoices purpose or covers entry`,
56
63
  path: interaction.name,
57
64
  });
58
65
  }
@@ -158,7 +165,7 @@ export function checkMockStrategy(contract, allowedMockStrategies) {
158
165
  * A component specReference must still not point into clearly non-component
159
166
  * spec areas — templates and the ai_workspace governance subtree cannot be the
160
167
  * authoritative component definition. */
161
- export function checkComponentChoice(contract, componentSpecCandidatePaths) {
168
+ export function checkComponentChoice(contract, componentSpecCandidatePaths, options) {
162
169
  const findings = [];
163
170
  const candidates = new Set(componentSpecCandidatePaths.map(normalizeRelativePath));
164
171
  const isNonComponentSpecArea = (path) => {
@@ -200,6 +207,23 @@ export function checkComponentChoice(contract, componentSpecCandidatePaths) {
200
207
  });
201
208
  }
202
209
  }
210
+ else if (choice.decision === "reuse-existing") {
211
+ if (!(choice.evidencePath && choice.evidencePath.trim().length > 0)) {
212
+ findings.push({
213
+ code: "component-reuse-evidence-missing",
214
+ message: `reuse-existing component choice #${index} is missing evidencePath naming the existing repo file that proves the reuse; a greenfield path must use decision=new`,
215
+ path: choice.purpose,
216
+ });
217
+ }
218
+ else if (options?.hasFreshPathEvidence &&
219
+ !options.hasFreshPathEvidence(choice.evidencePath)) {
220
+ findings.push({
221
+ code: "component-reuse-path-not-fresh",
222
+ message: `reuse-existing component choice #${index} evidencePath "${choice.evidencePath}" has no fresh existence evidence (path not found); reuse requires an existing file — use decision=new for greenfield paths`,
223
+ path: choice.evidencePath,
224
+ });
225
+ }
226
+ }
203
227
  else if (choice.decision === "new") {
204
228
  if (!choice.specReference || !choice.specReference.path) {
205
229
  findings.push({
@@ -236,7 +260,7 @@ export function checkTargetPaths(contract, writeSetPatterns) {
236
260
  if (contract.targets.files.length > MAX_WRITE_SET_FILES) {
237
261
  findings.push({
238
262
  code: "write-set-too-large",
239
- message: `implementation writeSet has ${contract.targets.files.length} files (limit ${MAX_WRITE_SET_FILES}); split the task (record_split_proposal) so no single implement node exceeds the bound`,
263
+ message: `implementation writeSet has ${contract.targets.files.length} files (limit ${MAX_WRITE_SET_FILES}); split the task at the Contract boundary so no single implement node exceeds the bound; preserve all required files`,
240
264
  });
241
265
  }
242
266
  for (const file of contract.targets.files) {
@@ -249,7 +273,7 @@ export function checkTargetPaths(contract, writeSetPatterns) {
249
273
  }
250
274
  }
251
275
  for (const target of contract.verificationTargets) {
252
- if (target.type === "static")
276
+ if (target.mode === "static")
253
277
  continue;
254
278
  if (!matches(target.file)) {
255
279
  findings.push({
@@ -371,6 +395,64 @@ export function checkDesignApproval(designApproval) {
371
395
  }
372
396
  return [];
373
397
  }
398
+ /**
399
+ * UX vocabulary collisions (item ③, dogfood dag-1788504923861-0f7b17a9):
400
+ * prioritize-planner-task / prioritizePlannerTask / prioritize-planner-tasks
401
+ * are one behavior renamed three ways. Case/separator-insensitive duplicates
402
+ * inside interactions or inside uiStates are blocking.
403
+ */
404
+ export function checkUxNameCollisions(contract) {
405
+ const findings = [];
406
+ const normalize = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
407
+ const collisions = (names, kind) => {
408
+ const byNormalized = new Map();
409
+ const found = [];
410
+ for (const name of names) {
411
+ const key = normalize(name);
412
+ if (!key)
413
+ continue;
414
+ const existing = byNormalized.get(key);
415
+ if (existing !== undefined && existing !== name) {
416
+ found.push({
417
+ code: "ux-name-collision",
418
+ message: `"${name}" and "${existing}" are the same ${kind} name up to case/separator — one behavior must keep one stable name`,
419
+ path: name,
420
+ });
421
+ }
422
+ else {
423
+ byNormalized.set(key, name);
424
+ }
425
+ }
426
+ return found;
427
+ };
428
+ findings.push(...collisions(contract.interactions.map((interaction) => interaction.name), "interaction"));
429
+ findings.push(...collisions(contract.uiStates.map((state) => state.name), "UI state"));
430
+ return findings;
431
+ }
432
+ /**
433
+ * Deliverable completeness (item ⑦): every file explicitly declared by Contract
434
+ * from frozen source fragments (projected into targets.requiredDeliverables)
435
+ * must be planned — bound by some requirement's implementationTargets or a
436
+ * verification target's file, or listed in targets.files. allowedPaths is a
437
+ * write boundary and never implies coverage.
438
+ */
439
+ export function checkRequiredDeliverables(contract) {
440
+ const required = contract.targets.requiredDeliverables ?? [];
441
+ if (required.length === 0)
442
+ return [];
443
+ const planned = new Set([
444
+ ...contract.targets.files,
445
+ ...contract.requirements.flatMap((requirement) => requirement.implementationTargets),
446
+ ...contract.verificationTargets.map((target) => target.file),
447
+ ]);
448
+ return required
449
+ .filter((path) => !planned.has(path))
450
+ .map((path) => ({
451
+ code: "required-deliverable-unplanned",
452
+ message: `Contract declares required deliverable "${path}" but no requirement implementationTarget, verification target, or targets.files entry plans it`,
453
+ path,
454
+ }));
455
+ }
374
456
  /** Aggregate the deterministic design-policy checks into a single
375
457
  * deterministic result. Any finding blocks; an empty finding list approves. */
376
458
  export function evaluateFrontendDesignPolicy(input) {
@@ -379,10 +461,14 @@ export function evaluateFrontendDesignPolicy(input) {
379
461
  ...checkUiDesignCoverage(input.contract),
380
462
  ...checkSourceFreshness(input.contract, input.sourceFreshness),
381
463
  ...checkMockStrategy(input.contract, input.allowedMockStrategies),
382
- ...checkComponentChoice(input.contract, input.componentSpecCandidatePaths),
464
+ ...checkComponentChoice(input.contract, input.componentSpecCandidatePaths, input.hasFreshPathEvidence
465
+ ? { hasFreshPathEvidence: input.hasFreshPathEvidence }
466
+ : undefined),
383
467
  ...checkTargetPaths(input.contract, input.writeSetPatterns),
384
468
  ...checkDependencies(input.contract, input.allowedDependencies),
385
469
  ...checkUiStateAttribution(input.contract),
470
+ ...checkUxNameCollisions(input.contract),
471
+ ...checkRequiredDeliverables(input.contract),
386
472
  ]);
387
473
  const classification = findings.length > 0 ? "blocked" : "approved";
388
474
  const result = {
@@ -0,0 +1,193 @@
1
+ import { mkdir, open, rename, rm } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import path from "node:path";
4
+ import { Check, Errors } from "typebox/value";
5
+ import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
6
+ import { assertHarnessWriteAllowed } from "../../infrastructure/harness/completed-facts-guard.js";
7
+ import { readTypedEventStoreFromJsonl, typedEventPayloadSha256, hasCommittedTerminalEvent } from "./frontend-typed-event-store.js";
8
+ const queues = new Map();
9
+ const progressByTool = new WeakMap();
10
+ /** Process-local telemetry only; durable facts remain the authority. */
11
+ export function readFrontendToolProgress(tools) {
12
+ const counters = [...new Set((tools ?? []).flatMap(tool => tool && typeof tool === "object" && progressByTool.has(tool) ? [progressByTool.get(tool)] : []))];
13
+ if (!counters.length)
14
+ return undefined;
15
+ return counters.reduce((sum, item) => ({ failedRecords: sum.failedRecords + item.failedRecords, duplicateRecords: sum.duplicateRecords + item.duplicateRecords, durableSubmissions: sum.durableSubmissions + item.durableSubmissions, durableReplacements: sum.durableReplacements + item.durableReplacements }), { failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 });
16
+ }
17
+ async function exclusive(key, work) {
18
+ const previous = queues.get(key) ?? Promise.resolve();
19
+ const current = previous.catch(() => { }).then(work);
20
+ queues.set(key, current);
21
+ try {
22
+ return await current;
23
+ }
24
+ finally {
25
+ if (queues.get(key) === current)
26
+ queues.delete(key);
27
+ }
28
+ }
29
+ const hash = (value) => sha256OfCanonicalJson(JSON.parse(JSON.stringify(value, (_key, v) => v instanceof Map ? [...v.entries()].sort(([a], [b]) => String(a).localeCompare(String(b))) : v)));
30
+ const ledgerError = (code, message) => Object.assign(new Error(message), { code });
31
+ const failure = (code, error) => ({ content: [{ type: "text", text: JSON.stringify({ ok: false, code, error }) }], details: { ok: false, code, error } });
32
+ /** A failed rename must never fall back to copying over a live ledger. */
33
+ async function persist(file, records) {
34
+ assertHarnessWriteAllowed(file);
35
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
36
+ const temporary = `${file}.${randomUUID()}.tmp`;
37
+ try {
38
+ const handle = await open(temporary, "wx", 0o600);
39
+ try {
40
+ await handle.writeFile(records.map(r => JSON.stringify(r)).join("\n") + (records.length ? "\n" : ""));
41
+ await handle.sync();
42
+ }
43
+ finally {
44
+ await handle.close();
45
+ }
46
+ await rename(temporary, file);
47
+ }
48
+ finally {
49
+ await rm(temporary, { force: true }).catch(() => { });
50
+ }
51
+ }
52
+ /** One queue covers schema checks, semantic checks, all facts in a tool call,
53
+ * atomic persistence, and acknowledgement. The model never sees a draft as saved. */
54
+ export async function createDurableFrontendTools(input) {
55
+ const file = path.resolve(input.file);
56
+ const tools = input.tools.map(tool => {
57
+ const schema = tool.parameters;
58
+ return { ...tool, parameters: { ...schema, properties: { ...schema.properties, replace: { type: "boolean", description: "Explicitly revise the current fact with the same identity. Omit for new facts or identical replay." } } } };
59
+ });
60
+ const bindingSha256 = hash({ version: 1, attemptId: input.attemptId, binding: input.binding, schemas: tools.map(t => ({ name: t.name, parameters: t.parameters })) });
61
+ const publish = (store) => { input.store.records = store.records; input.store.revision = store.revision; input.setWorkingStore(input.store); };
62
+ const validate = async (records) => {
63
+ let revision = 0;
64
+ let terminal = false;
65
+ const ids = new Set();
66
+ const requests = new Set();
67
+ for (const r of records) {
68
+ if (r.attemptId !== input.attemptId || r.submission?.bindingSha256 !== bindingSha256)
69
+ throw ledgerError("FRONTEND_LEDGER_BINDING_MISMATCH", "frontend ledger binding/scope mismatch; preserve evidence and restart its owning phase");
70
+ if (r.phase !== "committed" || r.payloadSha256 !== typedEventPayloadSha256(r.fact) || r.revision <= revision || ids.has(r.eventId) || requests.has(r.requestId) || terminal)
71
+ throw ledgerError("FRONTEND_LEDGER_INTEGRITY_INVALID", "frontend ledger integrity/revision mismatch");
72
+ ids.add(r.eventId);
73
+ requests.add(r.requestId);
74
+ revision = r.revision;
75
+ terminal = hasCommittedTerminalEvent({ records: [r], revision }, input.attemptId);
76
+ }
77
+ await input.validateRestored?.(records);
78
+ };
79
+ const restore = async () => {
80
+ const records = await readTypedEventStoreFromJsonl(file);
81
+ if (records.length) {
82
+ await validate(records);
83
+ publish({ records, revision: records.at(-1).revision });
84
+ }
85
+ else if (input.store.records.some(r => r.submission))
86
+ throw ledgerError("FRONTEND_LEDGER_MISSING", "frontend durable ledger disappeared or became empty; refusing an unproven acknowledgement");
87
+ };
88
+ await exclusive(file, restore);
89
+ const progress = { failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 };
90
+ const replay = (receipt) => { progress.duplicateRecords++; return receipt; };
91
+ const wrapped = tools.map(tool => ({ ...tool, execute: (...args) => exclusive(file, async () => {
92
+ const [callId, params] = args;
93
+ const valid = Check(tool.parameters, params);
94
+ if (!valid) {
95
+ const errors = [...Errors(tool.parameters, params)];
96
+ return failure("TOOL_SCHEMA_INVALID", `${tool.name}: ${errors.slice(0, 8).map(e => `${e.instancePath || "/"}: ${e.message}`).join("; ")}${errors.length > 8 ? `; ${errors.length - 8} more fields` : ""}`);
97
+ }
98
+ try {
99
+ await restore();
100
+ const paramsSha256 = hash(params);
101
+ const singleton = new Set(["record_route_selection", "record_state_registry", "record_mock_api", "record_data_flow", "record_dependency", "record_design_deviation", "record_required_deliverables", "record_target_surface", "finalize_contract", "finalize_plan"]);
102
+ const naturalId = params?.id ?? params?.scopeId ?? params?.entry?.id ?? params?.requirementId ?? params?.choice?.purpose ?? (tool.name === "record_openspec_selection" ? params?.path : undefined) ?? (params?.endpoint ? `${params.endpoint.method}:${params.endpoint.path}` : undefined) ?? (singleton.has(tool.name) ? "singleton" : undefined);
103
+ const identity = `${tool.name}:${typeof naturalId === "string" ? naturalId : "incremental"}`;
104
+ const submissions = input.store.records.flatMap(r => r.submission?.receipt ? [r.submission] : []);
105
+ const previousCall = submissions.find(s => s.callId === callId);
106
+ if (previousCall)
107
+ return previousCall.tool === tool.name && previousCall.paramsSha256 === paramsSha256 ? replay(previousCall.receipt) : failure("REQUEST_ID_REUSE_CONFLICT", `${tool.name}: call id already acknowledged with different parameters`);
108
+ const previousFact = submissions.filter(s => s.identity === identity).at(-1);
109
+ if (tool.name !== "record_plan_group_coverage" && previousFact?.paramsSha256 === paramsSha256)
110
+ return replay(previousFact.receipt);
111
+ if (previousFact && naturalId !== undefined && params?.replace !== true && !["record_evidence_expectation", "record_required_deliverables", "record_data_flow"].includes(tool.name))
112
+ return failure("FACT_IDENTITY_CONFLICT", `${identity}: changing a saved fact requires explicit replacement (replace:true)`);
113
+ if (input.store.records.some(r => ["contract-finalized", "finalize_plan", "approve_review", "request_review_changes", "approve_design", "request_design_changes"].includes(String(r.fact.kind))))
114
+ return failure("TERMINAL_CONFLICT", "A successful terminal is already durable; only identical acknowledged submissions may be replayed");
115
+ const draft = structuredClone(input.store);
116
+ input.setWorkingStore(draft);
117
+ let result;
118
+ try {
119
+ // Protocol controls must not be spread into domain facts by permissive callers.
120
+ const original = input.tools.find(t => t.name === tool.name);
121
+ const domainParams = { ...params };
122
+ if (!original.parameters.properties?.replace)
123
+ delete domainParams.replace;
124
+ result = await tool.execute(args[0], domainParams, ...args.slice(2));
125
+ }
126
+ finally {
127
+ input.setWorkingStore(input.store);
128
+ }
129
+ if (result.details?.ok !== true)
130
+ return result;
131
+ const newRecords = draft.records.filter(r => r.phase === "committed" && r.revision > input.store.revision);
132
+ if (!newRecords.length)
133
+ return replay(result);
134
+ for (const record of draft.records) {
135
+ record.submission ??= { version: 1, bindingSha256, tool: "bootstrap", callId: record.requestId, identity: record.eventId, paramsSha256: record.payloadSha256 };
136
+ }
137
+ for (const record of newRecords)
138
+ record.submission = { version: 1, bindingSha256, tool: tool.name, callId, identity, paramsSha256 };
139
+ newRecords.at(-1).submission.receipt = JSON.parse(JSON.stringify(result));
140
+ const committed = draft.records.filter(r => r.phase === "committed");
141
+ await validate(committed);
142
+ await persist(file, committed);
143
+ publish(draft);
144
+ progress.durableSubmissions++;
145
+ if (previousFact)
146
+ progress.durableReplacements++;
147
+ return result;
148
+ }
149
+ catch (error) {
150
+ input.setWorkingStore(input.store);
151
+ return failure(typeof error?.code === "string" ? error.code : "FRONTEND_COMMIT_FAILED", error instanceof Error ? error.message : String(error));
152
+ }
153
+ }).then(result => { if (result.details?.ok !== true)
154
+ progress.failedRecords++; return result; }) }));
155
+ for (const tool of wrapped)
156
+ progressByTool.set(tool, progress);
157
+ return {
158
+ customTools: wrapped,
159
+ commitExternal: (work) => exclusive(file, async () => {
160
+ await restore();
161
+ const draft = structuredClone(input.store);
162
+ input.setWorkingStore(draft);
163
+ try {
164
+ await work();
165
+ const newRecords = draft.records.filter(r => r.phase === "committed" && r.revision > input.store.revision);
166
+ for (const record of draft.records) {
167
+ record.submission ??= {
168
+ version: 1,
169
+ bindingSha256,
170
+ tool: "external-adoption",
171
+ callId: record.requestId,
172
+ identity: record.eventId,
173
+ paramsSha256: record.payloadSha256,
174
+ };
175
+ }
176
+ const committed = draft.records.filter(r => r.phase === "committed");
177
+ await validate(committed);
178
+ await persist(file, committed);
179
+ publish(draft);
180
+ progress.durableSubmissions += newRecords.length;
181
+ }
182
+ finally {
183
+ input.setWorkingStore(input.store);
184
+ }
185
+ }),
186
+ flush: () => exclusive(file, async () => {
187
+ await restore();
188
+ const committed = input.store.records.filter(r => r.phase === "committed");
189
+ await validate(committed);
190
+ await persist(file, committed);
191
+ }),
192
+ };
193
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ /** Model-authored execution ownership; never replaces a source obligation. */
3
+ export const frontendExecutionSchema = z.object({
4
+ groupId: z.string().trim().min(1),
5
+ kind: z.enum(["behavior", "constraint", "exclusion"]),
6
+ summary: z.string().trim().min(1),
7
+ }).strict();
8
+ /** Latest canonical member wins; joining a group requires consistent metadata.
9
+ * No lexical/semantic similarity heuristic can erase a source ID or condition. */
10
+ export function collectFrontendExecutionGroups(units) {
11
+ const groups = new Map();
12
+ for (const unit of new Map(units.map(unit => [unit.id, unit])).values()) {
13
+ const execution = unit.execution === undefined ? undefined : frontendExecutionSchema.parse(unit.execution);
14
+ const key = execution ? `group:${execution.groupId}` : `requirement:${unit.id}`;
15
+ const previous = groups.get(key);
16
+ if (previous && execution && (previous.kind !== execution.kind || previous.summary !== execution.summary))
17
+ throw Error(`EXECUTION_GROUP_CONFLICT: ${execution.groupId}; preserve differing permissions, thresholds and error paths as separate obligations/groups`);
18
+ if (previous)
19
+ previous.requirementIds.push(unit.id);
20
+ else
21
+ groups.set(key, { id: execution?.groupId ?? unit.id, kind: execution?.kind ?? "unclassified", summary: execution?.summary ?? `Independent requirement ${unit.id}`, requirementIds: [unit.id] });
22
+ }
23
+ return [...groups.values()];
24
+ }