@tea-agent/loop-agent 0.44.0-next.1 → 0.44.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +379 -416
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
  22. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
  23. package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
  24. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  25. package/dist/executors/dag-pi/plan/record-tools.js +861 -0
  26. package/dist/executors/dag-pi/plan/schema.js +129 -0
  27. package/dist/executors/dag-pi/plan/session.js +1 -0
  28. package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
  29. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  30. package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
  31. package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
  32. package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
  33. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  34. package/dist/executors/dag-pi-executor.js +398 -3384
  35. package/dist/executors/pi-executor.js +6 -1
  36. package/dist/executors/pi-sdk-executor.js +112 -19
  37. package/dist/executors/shell-executor.js +88 -12
  38. package/dist/executors/shell-presets.js +1 -1
  39. package/dist/executors/shell-write-guard.js +26 -3
  40. package/dist/infrastructure/console/operation-store.js +48 -14
  41. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  42. package/dist/shared/file-identity.js +23 -0
  43. package/dist/shared/git-workspace-status.js +482 -0
  44. package/dist/shared/operator/capabilities.js +10 -5
  45. package/dist/shared/path-safety.js +2 -2
  46. package/dist/shared/wizard-local-telemetry.js +51 -0
  47. package/dist/task/config-types.js +22 -8
  48. package/dist/task/contract/project.js +13 -0
  49. package/dist/task/contract/schema.js +2 -7
  50. package/dist/task/frontend-preflight.js +1 -1
  51. package/dist/task/source-prepare/parse-intent.js +124 -5
  52. package/dist/task/source-prepare/prepare.js +6 -1
  53. package/dist/task/source-prepare/semantic-intake.js +18 -3
  54. package/dist/task/source-prepare/source-execution.js +65 -0
  55. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  56. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  57. package/dist/worker/console/chat/assistant-content.js +5 -34
  58. package/dist/worker/console/chat/chat-event-store.js +62 -10
  59. package/dist/worker/console/chat/context-insights.js +723 -0
  60. package/dist/worker/console/chat/conversation-nodes.js +383 -0
  61. package/dist/worker/console/chat/db-connections.js +854 -0
  62. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  63. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
  64. package/dist/worker/console/chat/pi-runtime.js +445 -36
  65. package/dist/worker/console/chat/provider-error.js +0 -12
  66. package/dist/worker/console/chat/repo-browser.js +52 -4
  67. package/dist/worker/console/chat/routes.js +577 -72
  68. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  69. package/dist/worker/console/chat/session-store.js +147 -19
  70. package/dist/worker/console/chat/shortcuts.js +7 -0
  71. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  72. package/dist/worker/console/chat/tools.js +34 -0
  73. package/dist/worker/console/chat/turn-process.js +237 -66
  74. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  75. package/dist/worker/console/console-update-runtime.js +2 -2
  76. package/dist/worker/console/dag-confirmation.js +3 -0
  77. package/dist/worker/console/dag-execution-receipt.js +3 -0
  78. package/dist/worker/console/operator-actions.js +32 -3
  79. package/dist/worker/console/operator-user-error.js +28 -1
  80. package/dist/worker/console/pi-readiness.js +4 -4
  81. package/dist/worker/console/prd-intake-bridge.js +5 -12
  82. package/dist/worker/console/recovery-cta.js +14 -14
  83. package/dist/worker/console/server.js +5 -1
  84. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  85. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
  86. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  87. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  88. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  89. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  90. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  100. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  101. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  102. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  103. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  104. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  105. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  111. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  112. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  113. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  114. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  115. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  116. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  117. package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
  118. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  119. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  120. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  121. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  122. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
  123. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
  124. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  125. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  126. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  127. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  128. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  129. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  130. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  131. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  132. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  133. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  134. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  135. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  136. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  137. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  138. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  139. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  140. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  141. package/dist/worker/console/static/index.html +2 -2
  142. package/dist/worker/console/static-src/app/console-types.js +9 -1
  143. package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
  144. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  145. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
  146. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  147. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  148. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  149. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  150. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  151. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  152. package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
  153. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  154. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  155. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
  156. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  157. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  158. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  159. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
  160. package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
  161. package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
  162. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  163. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  164. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  165. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  166. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  167. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  168. package/dist/worker/console/workspace-context.js +2 -0
  169. package/dist/worker/console/workspace-initialization.js +3 -3
  170. package/dist/worker/observability/read-model.js +23 -2
  171. package/dist/worker/observe/node-context-insights.js +479 -0
  172. package/dist/worker/observe/static/console-theme.css +174 -0
  173. package/dist/worker/observe/static/console-theme.js +36 -0
  174. package/dist/worker/observe/static/operator-chrome.css +85 -0
  175. package/dist/worker/observe/static/operator-chrome.js +51 -0
  176. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  177. package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
  178. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  179. package/dist/workflows/dag/budget-enforcement.js +31 -2
  180. package/dist/workflows/dag/frontend-closeout.js +3 -1
  181. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  182. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  183. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  184. package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
  185. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  186. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  187. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  188. package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
  189. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  190. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  191. package/dist/workflows/dag/frontend-review-context.js +29 -1
  192. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  193. package/dist/workflows/dag/frontend-risk.js +8 -3
  194. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  195. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  196. package/dist/workflows/dag/frontend-session-context.js +10 -0
  197. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  198. package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
  199. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  200. package/dist/workflows/dag/frontend-writer-admission.js +3 -1
  201. package/dist/workflows/dag/init-hybrid.js +108 -37
  202. package/dist/workflows/dag/node-execution.js +11 -11
  203. package/dist/workflows/dag/output-protocol.js +1 -0
  204. package/dist/workflows/dag/rerun-feedback.js +35 -4
  205. package/dist/workflows/dag/rerun-task.js +62 -66
  206. package/dist/workflows/dag/runner.js +58 -12
  207. package/dist/workflows/dag/types.js +15 -8
  208. package/dist/workflows/loop/benchmark.js +11 -8
  209. package/docs/README.md +1 -1
  210. package/docs/architecture/runtime-boundaries.md +1 -1
  211. package/docs/governance/harness-methodology-verification.md +11 -0
  212. package/docs/templates/backend-test-dag.json +5 -4
  213. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  214. package/package.json +6 -4
  215. package/skills/frontend-design-review/SKILL.md +8 -11
  216. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  217. package/skills/frontend-review/SKILL.md +5 -1
  218. package/skills/loop-agent/references/command-reference.md +8 -0
  219. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  220. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  221. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  222. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  223. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  224. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  225. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  226. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -0,0 +1,861 @@
1
+ import { nonEmptyString, planToolReceipt, stringList } from "./tool-helpers.js";
2
+ import { collectCanonicalStateFlowNames } from "../../../workflows/dag/frontend-contract-facts.js";
3
+ import { pathMatchesPattern } from "../../../shared/git-progress.js";
4
+ import { isRecordObject } from "../guards.js";
5
+ import { randomUUID } from "node:crypto";
6
+ import { stat } from "node:fs/promises";
7
+ import path from "node:path";
8
+ export function buildPlanRecordTools(ctx) {
9
+ const { Type, defineTool, session, schemas, input, readCommittedEvents, scopedRequirementIds, executionGroups, adoptPlanFact } = ctx;
10
+ const { stringArray, optionalString, requirementSchema, uiStateSchema, interactionSchema, mockEndpointSchema, mockApiSchema, designEvidenceSchema, verificationTargetSchema, evidenceGapSchema, uiComponentChoiceSchema } = ctx.schemas;
11
+ const recordRouteSelectionTool = defineTool({
12
+ name: "record_route_selection",
13
+ label: "record_route_selection",
14
+ description: "Record the route selection needed by this plan. Repository target surface and file ownership belong to Scout/runtime. Example: {\"routes\": [\"/<route>\"]}",
15
+ promptSnippet: "Record the selected routes.",
16
+ parameters: Type.Object({ routes: stringArray }, { additionalProperties: false }),
17
+ async execute(_toolCallId, params) {
18
+ const routes = stringList(params?.routes);
19
+ const result = await adoptPlanFact("target-surface", `${session.attemptId}:record_route_selection:${randomUUID()}`, { kind: "target-surface", origin: "plan", routes });
20
+ return planToolReceipt(result);
21
+ },
22
+ });
23
+ const recordComponentChoiceTool = defineTool({
24
+ name: "record_component_choice",
25
+ label: "record_component_choice",
26
+ description: "Record ONE component choice (origin=plan component-choice fact). One choice may cover MULTIPLE UI states/interactions via covers: [\"<state-or-interaction name>\", ...]; do not emit one row per interaction. For decision=new, pass sourceRequirementIds containing the frozen requirement ID(s) that mandate the component and sourceFragmentId selecting one frozen citation listed in the plan checklist; the runtime validates the relation and derives the exact PRD specReference. Do not read the PRD or invent a path/line. decision=reuse-existing is only for components that already exist in the repo and REQUIRES evidencePath: a repo-relative path to the existing file that proves the reuse — the runtime verifies the file exists (fresh evidence); a path with no existing file is greenfield and must use decision=new instead. Call up to 5 component choices per assistant message; never more than 5 per message. Optionally include stylingStrategy (set it once, on the first call). Example: {\"choice\": {\"purpose\": \"<interaction or UI state name>\", \"component\": \"<component name>\", \"decision\": \"new\", \"covers\": [\"<other interaction names this component also serves>\"]}, \"sourceRequirementIds\": [\"<AC-XXX mandating this component>\"], \"sourceFragmentId\": \"<REQ-SRC-...>\"} or {\"choice\": {\"purpose\": \"FocusQueuePanel\", \"component\": \"FocusQueuePanel\", \"decision\": \"reuse-existing\", \"evidencePath\": \"src/journal.ts\"}}",
27
+ promptSnippet: "Record 1-5 component choices (up to 5 per message).",
28
+ parameters: Type.Object({
29
+ choice: uiComponentChoiceSchema,
30
+ sourceRequirementIds: Type.Optional(stringArray),
31
+ sourceFragmentId: optionalString,
32
+ stylingStrategy: optionalString,
33
+ }, { additionalProperties: false }),
34
+ async execute(_toolCallId, params) {
35
+ const rawChoice = params?.choice;
36
+ if (!isRecordObject(rawChoice)) {
37
+ return planToolReceipt({
38
+ ok: false,
39
+ kind: "component-choice",
40
+ error: "record_component_choice requires a non-empty choice object",
41
+ });
42
+ }
43
+ const sourceRequirementIds = stringList(params?.sourceRequirementIds);
44
+ const sourceFragmentId = nonEmptyString(params?.sourceFragmentId);
45
+ const choice = { ...rawChoice };
46
+ if (choice.decision === "new") {
47
+ if (sourceRequirementIds.length === 0) {
48
+ return planToolReceipt({
49
+ ok: false,
50
+ kind: "component-choice",
51
+ error: "decision=new requires sourceRequirementIds so runtime can materialize the task-source specReference",
52
+ });
53
+ }
54
+ if (!sourceFragmentId) {
55
+ return planToolReceipt({
56
+ ok: false,
57
+ kind: "component-choice",
58
+ error: "decision=new requires sourceFragmentId selecting a frozen task-source citation",
59
+ });
60
+ }
61
+ const citation = sourceRequirementIds
62
+ .flatMap((id) => input.componentNewSourceReferences?.get(id) ?? [])
63
+ .find((candidate) => candidate.fragmentId === sourceFragmentId);
64
+ if (!citation) {
65
+ return planToolReceipt({
66
+ ok: false,
67
+ kind: "component-choice",
68
+ error: `decision=new sourceFragmentId ${sourceFragmentId} is not bound to sourceRequirementIds ${sourceRequirementIds.join(", ")}`,
69
+ });
70
+ }
71
+ choice.specReference = {
72
+ path: citation.path,
73
+ section: citation.section,
74
+ ...(citation.line !== undefined ? { line: citation.line } : {}),
75
+ };
76
+ }
77
+ if (choice.decision === "reuse-existing") {
78
+ // reuse-existing must cite a file that actually exists NOW (fresh
79
+ // existence evidence, same semantics as Scout pathEvidence.fresh).
80
+ // A path with no file on disk is greenfield: decision=new is the
81
+ // only honest choice (dogfood run dag-1788504923861-0f7b17a9
82
+ // claimed 18 reuse-existing conventions in a not-yet-written
83
+ // src/planner.ts and every gate let it through).
84
+ const evidencePath = typeof choice.evidencePath === "string"
85
+ ? choice.evidencePath.trim()
86
+ : "";
87
+ if (!evidencePath) {
88
+ return planToolReceipt({
89
+ ok: false,
90
+ kind: "component-choice",
91
+ error: "decision=reuse-existing requires evidencePath naming the existing repo file that proves the reuse; if the file does not exist yet, use decision=new",
92
+ });
93
+ }
94
+ if (input.workspaceRoot) {
95
+ const absolute = path.resolve(input.workspaceRoot, evidencePath);
96
+ const workspaceRoot = path.resolve(input.workspaceRoot);
97
+ const contained = absolute === workspaceRoot ||
98
+ absolute.startsWith(`${workspaceRoot}${path.sep}`);
99
+ let exists = false;
100
+ if (contained) {
101
+ try {
102
+ exists = (await stat(absolute)).isFile();
103
+ }
104
+ catch {
105
+ exists = false;
106
+ }
107
+ }
108
+ if (!exists) {
109
+ return planToolReceipt({
110
+ ok: false,
111
+ kind: "component-choice",
112
+ error: `decision=reuse-existing evidencePath "${evidencePath}" has no fresh existence evidence (file not found in the workspace); reuse requires an existing file — use decision=new for greenfield paths`,
113
+ });
114
+ }
115
+ }
116
+ }
117
+ const components = typeof choice.component === "string" ? [choice.component] : [];
118
+ const result = await adoptPlanFact("component-choice", `${session.attemptId}:record_component_choice:${randomUUID()}`, {
119
+ kind: "component-choice",
120
+ origin: "plan",
121
+ scopeRequirementIds: scopedRequirementIds(),
122
+ components,
123
+ uiComponentChoices: [choice],
124
+ ...(params?.stylingStrategy
125
+ ? { stylingStrategy: params.stylingStrategy }
126
+ : {}),
127
+ });
128
+ // Echo the frozen citation the runtime derived: the model sees the
129
+ // purpose↔citation mapping it just committed and can re-record the
130
+ // choice (last-wins per purpose at compile) when it mismatches.
131
+ const echo = {
132
+ ...result,
133
+ ...(choice.specReference
134
+ ? { derivedSpecReference: choice.specReference }
135
+ : {}),
136
+ };
137
+ return planToolReceipt(echo);
138
+ },
139
+ });
140
+ // Global UX vocabulary: one compact registry committed BEFORE any state
141
+ // flow details. Coverage may slice by AC; UX must not — the registry is
142
+ // the anti-duplication anchor that keeps every later slice on the same
143
+ // named concepts instead of re-inventing them per AC chunk.
144
+ const recordStateRegistryTool = defineTool({
145
+ name: "record_state_registry",
146
+ label: "record_state_registry",
147
+ description: "Commit the GLOBAL UX vocabulary (origin=plan state-registry fact) BEFORE any record_state_flow call: uiStateNames (use the contract's declared authoritative state ids when provided) and interactionNames (stable behavior-domain kebab-case names, e.g. planner-task-edit / focus-queue-move — one name per behavior domain, never one per AC). Empty arrays explicitly mean the request has no UI state or interaction vocabulary. Re-record the full vocabulary to correct it; the latest commit wins, but it may not remove names still referenced by committed state-flow facts. Example: {\"uiStateNames\": [\"planner-empty\"], \"interactionNames\": [\"planner-task-create\", \"focus-queue-move\"]}",
148
+ promptSnippet: "Record the global UI-state/interaction vocabulary once, before any state flow.",
149
+ parameters: Type.Object({
150
+ uiStateNames: stringArray,
151
+ interactionNames: stringArray,
152
+ }, { additionalProperties: false }),
153
+ async execute(_toolCallId, params) {
154
+ const uiStateNames = [...new Set(stringList(params?.uiStateNames).map((name) => name.trim()))];
155
+ const interactionNames = [
156
+ ...new Set(stringList(params?.interactionNames).map((name) => name.trim())),
157
+ ];
158
+ if (uiStateNames.some((name) => name.length === 0) || interactionNames.some((name) => name.length === 0)) {
159
+ return planToolReceipt({
160
+ ok: false,
161
+ kind: "state-registry",
162
+ error: "record_state_registry names must be non-empty strings",
163
+ });
164
+ }
165
+ const invalidInteractionNames = interactionNames.filter((name) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name));
166
+ if (invalidInteractionNames.length > 0) {
167
+ return planToolReceipt({
168
+ ok: false,
169
+ kind: "state-registry",
170
+ error: `record_state_registry interactionNames must be stable kebab-case behavior-domain names: ${invalidInteractionNames.join(", ")}`,
171
+ });
172
+ }
173
+ if (input.declaredUiStateIds &&
174
+ input.declaredUiStateIds.length > 0) {
175
+ const declared = new Set(input.declaredUiStateIds);
176
+ const undeclared = uiStateNames.filter((name) => !declared.has(name));
177
+ if (undeclared.length > 0) {
178
+ return planToolReceipt({
179
+ ok: false,
180
+ kind: "state-registry",
181
+ error: `record_state_registry uiStateNames are not declared by the contract's authoritative UI-state table: ${undeclared.join(", ")}; use the declared ids (${input.declaredUiStateIds.join(", ")})`,
182
+ });
183
+ }
184
+ const missing = input.declaredUiStateIds.filter((name) => !uiStateNames.includes(name));
185
+ if (missing.length > 0) {
186
+ return planToolReceipt({
187
+ ok: false,
188
+ kind: "state-registry",
189
+ error: `record_state_registry must include every state from the contract's authoritative UI-state table; missing: ${missing.join(", ")}`,
190
+ });
191
+ }
192
+ }
193
+ // A correction is deterministic only when the new last-wins registry
194
+ // still contains every live name already committed by state-flow facts.
195
+ // This permits adding a missed concept, while preventing a registry edit
196
+ // from retroactively orphaning earlier slices.
197
+ const liveNames = collectCanonicalStateFlowNames(readCommittedEvents(session.store, session.attemptId));
198
+ const orphanedUiStates = [...liveNames.uiStateNames].filter((name) => !uiStateNames.includes(name));
199
+ const orphanedInteractions = [...liveNames.interactionNames].filter((name) => !interactionNames.includes(name));
200
+ if (orphanedUiStates.length > 0 || orphanedInteractions.length > 0) {
201
+ return planToolReceipt({
202
+ ok: false,
203
+ kind: "state-registry",
204
+ error: `record_state_registry cannot remove names still referenced by committed state-flow facts (uiStates: ${orphanedUiStates.join(", ") || "none"}; interactions: ${orphanedInteractions.join(", ") || "none"}); first correct/remove those state-flow entries, then re-record the full registry`,
205
+ });
206
+ }
207
+ const result = await adoptPlanFact("state-registry", `${session.attemptId}:record_state_registry:${randomUUID()}`, {
208
+ kind: "state-registry",
209
+ origin: "plan",
210
+ uiStateNames,
211
+ interactionNames,
212
+ });
213
+ return planToolReceipt(result);
214
+ },
215
+ });
216
+ const recordStateFlowTool = defineTool({
217
+ name: "record_state_flow",
218
+ label: "record_state_flow",
219
+ description: "Record UI states and interactions as an origin=plan state-flow fact. REQUIRES a committed record_state_registry vocabulary first, and every name here must be in that registry; uiState names must also be contract-declared authoritative ids when the contract declares them. To correct stale named entries from an earlier UX batch, include removeUiStateNames and/or removeInteractionNames. Example: {\"uiStates\": [{\"name\": \"<state>\", \"applicable\": true, \"expectedBehavior\": \"<behavior>\", \"implementationTargets\": [\"<file>\"], \"verificationTargetIds\": [\"<VT-XXX>\"]}], \"interactions\": [{\"name\": \"<interaction>\", \"trigger\": \"<user event>\", \"expectedBehavior\": \"<behavior>\", \"implementationTargets\": [\"<file>\"], \"verificationTargetIds\": [\"<VT-XXX>\"]}]}",
220
+ promptSnippet: "Record the plan state-flow fact.",
221
+ parameters: Type.Object({
222
+ uiStates: Type.Array(uiStateSchema),
223
+ interactions: Type.Array(interactionSchema),
224
+ removeUiStateNames: Type.Optional(stringArray),
225
+ removeInteractionNames: Type.Optional(stringArray),
226
+ }, { additionalProperties: false }),
227
+ async execute(_toolCallId, params) {
228
+ const rawUiStates = Array.isArray(params?.uiStates)
229
+ ? params.uiStates
230
+ : [];
231
+ const rawInteractions = Array.isArray(params?.interactions)
232
+ ? params.interactions
233
+ : [];
234
+ const removeUiStateNames = stringList(params?.removeUiStateNames);
235
+ const removeInteractionNames = stringList(params?.removeInteractionNames);
236
+ const uiStates = [];
237
+ for (const rawState of rawUiStates) {
238
+ if (!isRecordObject(rawState)) {
239
+ return planToolReceipt({
240
+ ok: false,
241
+ kind: "state-flow",
242
+ error: "record_state_flow uiStates entries must be objects",
243
+ });
244
+ }
245
+ const { reason, notApplicableReason, ...state } = rawState;
246
+ const canonicalReason = nonEmptyString(notApplicableReason);
247
+ const aliasReason = nonEmptyString(reason);
248
+ if (canonicalReason !== undefined &&
249
+ aliasReason !== undefined &&
250
+ canonicalReason !== aliasReason) {
251
+ return planToolReceipt({
252
+ ok: false,
253
+ kind: "state-flow",
254
+ error: "record_state_flow uiState has conflicting reason and notApplicableReason values",
255
+ });
256
+ }
257
+ const resolvedReason = canonicalReason ?? aliasReason;
258
+ if (state.applicable === false && !resolvedReason) {
259
+ return planToolReceipt({
260
+ ok: false,
261
+ kind: "state-flow",
262
+ error: "record_state_flow requires non-empty notApplicableReason when uiState.applicable is false",
263
+ });
264
+ }
265
+ uiStates.push({
266
+ ...state,
267
+ ...(resolvedReason
268
+ ? { notApplicableReason: resolvedReason }
269
+ : {}),
270
+ });
271
+ }
272
+ const interactions = [];
273
+ for (const rawInteraction of rawInteractions) {
274
+ if (!isRecordObject(rawInteraction)) {
275
+ return planToolReceipt({
276
+ ok: false,
277
+ kind: "state-flow",
278
+ error: "record_state_flow interactions entries must be objects",
279
+ });
280
+ }
281
+ const { id, name, ...interaction } = rawInteraction;
282
+ const canonicalName = nonEmptyString(name);
283
+ const aliasName = nonEmptyString(id);
284
+ if (canonicalName !== undefined &&
285
+ aliasName !== undefined &&
286
+ canonicalName !== aliasName) {
287
+ return planToolReceipt({
288
+ ok: false,
289
+ kind: "state-flow",
290
+ error: "record_state_flow interaction has conflicting id and name values",
291
+ });
292
+ }
293
+ const resolvedName = canonicalName ?? aliasName;
294
+ if (!resolvedName) {
295
+ return planToolReceipt({
296
+ ok: false,
297
+ kind: "state-flow",
298
+ error: "record_state_flow requires non-empty interaction.name (id is accepted only as a legacy alias)",
299
+ });
300
+ }
301
+ // Interaction -> VT forward references are legal only against
302
+ // already-committed VT facts. In the segmented flow every VT
303
+ // commits in the coverage segment before state flows run, so a
304
+ // dangling reference here is a real defect (r20: *-BEHAVIOR
305
+ // refs reached the final review untraceable).
306
+ const interactionVtIds = stringList(interaction.verificationTargetIds);
307
+ const committedVtIds = new Set(readCommittedEvents(session.store, session.attemptId)
308
+ .map((event) => event.fact)
309
+ .filter((fact) => fact.kind === "plan-verification-target")
310
+ .map((fact) => fact.entry
311
+ ?.id)
312
+ .filter((id) => typeof id === "string"));
313
+ const unknownVtIds = interactionVtIds.filter((id) => !committedVtIds.has(id));
314
+ if (unknownVtIds.length > 0) {
315
+ return planToolReceipt({
316
+ ok: false,
317
+ kind: "state-flow",
318
+ error: `record_state_flow interaction "${resolvedName}" references verification targets that are not recorded yet: ${unknownVtIds.join(", ")}; record them with record_plan_verification_target first, then re-record this state flow`,
319
+ });
320
+ }
321
+ interactions.push({ ...interaction, name: resolvedName });
322
+ }
323
+ // UX vocabulary gate: every recorded state/interaction name must be
324
+ // declared in the committed global registry first. This keeps UX out
325
+ // of AC-number slicing — the model commits one compact vocabulary
326
+ // (record_state_registry), then fills behavior-domain details, and a
327
+ // later slice cannot silently rename an earlier concept (dogfood
328
+ // dag-1788504923861-0f7b17a9: 5 renames of "prioritize" + 7 of
329
+ // "focus queue" across AC chunks).
330
+ const states = uiStates
331
+ .map((state) => (typeof state?.name === "string" ? state.name : ""))
332
+ .filter(Boolean);
333
+ const committedRegistry = readCommittedEvents(session.store, session.attemptId)
334
+ .map((event) => event.fact)
335
+ .filter((fact) => isRecordObject(fact) && fact.kind === "state-registry")
336
+ .at(-1);
337
+ if (!committedRegistry) {
338
+ return planToolReceipt({
339
+ ok: false,
340
+ kind: "state-flow",
341
+ error: "record_state_flow requires a committed UX registry first: call record_state_registry with the full uiStateNames/interactionNames vocabulary, then record state flows against it",
342
+ });
343
+ }
344
+ const registryUiStateNames = new Set(stringList(committedRegistry.uiStateNames));
345
+ const registryInteractionNames = new Set(stringList(committedRegistry.interactionNames));
346
+ const unknownStates = states.filter((name) => !registryUiStateNames.has(name));
347
+ if (unknownStates.length > 0) {
348
+ return planToolReceipt({
349
+ ok: false,
350
+ kind: "state-flow",
351
+ error: `record_state_flow uiState names not in the committed registry: ${unknownStates.join(", ")}; re-record record_state_registry with the complete vocabulary first (registered: ${[...registryUiStateNames].join(", ") || "(none)"})`,
352
+ });
353
+ }
354
+ const unknownInteractions = interactions
355
+ .map((interaction) => typeof interaction?.name === "string" ? interaction.name : "")
356
+ .filter((name) => name && !registryInteractionNames.has(name));
357
+ if (unknownInteractions.length > 0) {
358
+ return planToolReceipt({
359
+ ok: false,
360
+ kind: "state-flow",
361
+ error: `record_state_flow interaction names not in the committed registry: ${unknownInteractions.join(", ")}; re-record record_state_registry with the complete vocabulary first (registered: ${[...registryInteractionNames].join(", ") || "(none)"})`,
362
+ });
363
+ }
364
+ // Authoritative UI states: when the contract node declared the
365
+ // source's UI-state table, planner states must bind those ids — no
366
+ // invented variants (planner-empty/create-invalid/no-results/
367
+ // editing/focus-full/storage-unavailable, not "planner-list").
368
+ if (input.declaredUiStateIds &&
369
+ input.declaredUiStateIds.length > 0) {
370
+ const declared = new Set(input.declaredUiStateIds);
371
+ const undeclaredStates = states.filter((name) => !declared.has(name));
372
+ if (undeclaredStates.length > 0) {
373
+ return planToolReceipt({
374
+ ok: false,
375
+ kind: "state-flow",
376
+ error: `record_state_flow uiState names are not declared by the contract's authoritative UI-state table: ${undeclaredStates.join(", ")}; use the declared ids (${input.declaredUiStateIds.join(", ")}), or record a design deviation if the source table is genuinely incomplete`,
377
+ });
378
+ }
379
+ }
380
+ // Named entries are last-wins at assembly. A scoped correction may
381
+ // replace its own bindings, but must retain other scopes' coverage.
382
+ const scope = new Set(scopedRequirementIds());
383
+ const committed = readCommittedEvents(session.store, session.attemptId).map(event => event.fact);
384
+ const targetOwners = new Map();
385
+ for (const fact of committed) {
386
+ if (fact.kind === "plan-verification-target" && isRecordObject(fact.entry) && typeof fact.entry.id === "string") {
387
+ targetOwners.set(fact.entry.id, stringList(fact.entry.requirementIds));
388
+ }
389
+ }
390
+ for (const [field, removals] of [["uiStates", removeUiStateNames], ["interactions", removeInteractionNames]]) {
391
+ const live = new Map();
392
+ for (const fact of committed) {
393
+ if (fact.kind !== "state-flow")
394
+ continue;
395
+ for (const name of stringList(fact[field === "uiStates" ? "removeUiStateNames" : "removeInteractionNames"]))
396
+ live.delete(name);
397
+ for (const entry of Array.isArray(fact[field]) ? fact[field] : []) {
398
+ if (isRecordObject(entry) && typeof entry.name === "string")
399
+ live.set(entry.name, entry);
400
+ }
401
+ }
402
+ const foreignBindings = (entry) => scope.size === 0 ? [] : stringList(entry.verificationTargetIds).filter(id => {
403
+ const owners = targetOwners.get(id);
404
+ return !owners?.length || owners.some(owner => !scope.has(owner));
405
+ });
406
+ for (const name of removals) {
407
+ const previous = live.get(name);
408
+ if (previous && foreignBindings(previous).length)
409
+ return planToolReceipt({ ok: false, kind: "state-flow",
410
+ error: `Cannot remove shared ${name} from this requirement scope; retain it and update its scoped bindings instead` });
411
+ }
412
+ for (const entry of field === "uiStates" ? uiStates : interactions) {
413
+ const previous = live.get(String(entry.name));
414
+ if (previous)
415
+ entry.verificationTargetIds = [...new Set([...foreignBindings(previous), ...stringList(entry.verificationTargetIds)])];
416
+ }
417
+ }
418
+ const result = await adoptPlanFact("state-flow", `${session.attemptId}:record_state_flow:${randomUUID()}`, {
419
+ kind: "state-flow",
420
+ origin: "plan",
421
+ scopeRequirementIds: scopedRequirementIds(),
422
+ states,
423
+ uiStates,
424
+ interactions,
425
+ removeUiStateNames,
426
+ removeInteractionNames,
427
+ });
428
+ return planToolReceipt(result);
429
+ },
430
+ });
431
+ const recordDataFlowTool = defineTool({
432
+ name: "record_data_flow",
433
+ label: "record_data_flow",
434
+ description: "Record interaction/endpoint data flow as an origin=plan data-flow fact. Example: {\"interactions\": [\"<interaction name>\"], \"endpoints\": [\"GET <path>\"]}",
435
+ promptSnippet: "Record the plan data-flow fact.",
436
+ parameters: Type.Object({ interactions: stringArray, endpoints: stringArray, replace: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
437
+ async execute(_toolCallId, params) {
438
+ const previous = params.replace ? undefined : readCommittedEvents(session.store, session.attemptId).filter(r => r.fact.kind === "data-flow").at(-1)?.fact;
439
+ const result = await adoptPlanFact("data-flow", `${session.attemptId}:record_data_flow:${randomUUID()}`, {
440
+ kind: "data-flow",
441
+ origin: "plan",
442
+ interactions: [...new Set([...stringList(previous?.interactions), ...stringList(params?.interactions)])],
443
+ endpoints: [...new Set([...stringList(previous?.endpoints), ...stringList(params?.endpoints)])],
444
+ });
445
+ return planToolReceipt(result);
446
+ },
447
+ });
448
+ const recordMockApiTool = defineTool({
449
+ name: "record_mock_api",
450
+ label: "record_mock_api",
451
+ description: "Record the Mock/API strategy as an origin=plan mock-api fact. Example: {\"mockApi\": {\"strategy\": \"not-needed\", \"activation\": \"n/a\", \"endpoints\": []}}",
452
+ promptSnippet: "Record the plan mock-api fact.",
453
+ parameters: Type.Object({ mockApi: mockApiSchema }, { additionalProperties: false }),
454
+ async execute(_toolCallId, params) {
455
+ const mockApi = params?.mockApi ?? {
456
+ strategy: "not-needed",
457
+ activation: "",
458
+ endpoints: [],
459
+ };
460
+ const endpoints = stringList((Array.isArray(mockApi?.endpoints) ? mockApi.endpoints : []).map((endpoint) => `${typeof endpoint?.method === "string" ? endpoint.method : ""} ${typeof endpoint?.path === "string" ? endpoint.path : ""}`.trim()));
461
+ const result = await adoptPlanFact("mock-api", `${session.attemptId}:record_mock_api:${randomUUID()}`, {
462
+ kind: "mock-api",
463
+ origin: "plan",
464
+ strategy: typeof mockApi?.strategy === "string" ? mockApi.strategy : "",
465
+ endpoints,
466
+ mockApi,
467
+ });
468
+ return planToolReceipt(result);
469
+ },
470
+ });
471
+ const recordMockEndpointTool = defineTool({
472
+ name: "record_mock_endpoint", label: "record_mock_endpoint",
473
+ description: "Record one Mock/API endpoint. First record_mock_api with the policy and endpoints: []; then submit each endpoint separately. Never regenerate the whole endpoint collection. Use replace:true to revise an existing method/path, or replace:true plus remove:true to withdraw it.",
474
+ parameters: Type.Object({ endpoint: mockEndpointSchema, remove: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
475
+ async execute(_callId, params) {
476
+ if (!readCommittedEvents(session.store, session.attemptId).some(r => r.fact.kind === "mock-api"))
477
+ return planToolReceipt({ ok: false, kind: "mock-endpoint", code: "MOCK_POLICY_MISSING", error: "Record the mock policy before its endpoints" });
478
+ return planToolReceipt(await adoptPlanFact("mock-endpoint", `${session.attemptId}:endpoint:${randomUUID()}`, { kind: "mock-endpoint", origin: "plan", endpoint: params.endpoint, ...(params.remove ? { removed: true } : {}) }));
479
+ },
480
+ });
481
+ const recordDesignDeviationTool = defineTool({
482
+ name: "record_design_deviation",
483
+ label: "record_design_deviation",
484
+ description: "Record design evidence conflicts as an origin=plan design-deviation fact. Example: {\"designEvidence\": {\"source\": \"<source>\", \"paths\": [\"<file>\"], \"conflicts\": [\"<conflicting requirement id>\"]}}",
485
+ promptSnippet: "Record the plan design-deviation fact.",
486
+ parameters: Type.Object({ designEvidence: designEvidenceSchema }, { additionalProperties: false }),
487
+ async execute(_toolCallId, params) {
488
+ const conflicts = stringList(params?.designEvidence?.conflicts);
489
+ const result = await adoptPlanFact("design-deviation", `${session.attemptId}:record_design_deviation:${randomUUID()}`, { kind: "design-deviation", origin: "plan", conflicts });
490
+ return planToolReceipt(result);
491
+ },
492
+ });
493
+ const recordDependencyTool = defineTool({
494
+ name: "record_dependency",
495
+ label: "record_dependency",
496
+ description: "Record the dependency policy as an origin=plan dependency fact. Example: {\"policy\": \"<dependency policy statement>\"}",
497
+ promptSnippet: "Record the plan dependency fact.",
498
+ parameters: Type.Object({ policy: Type.String({}) }, { additionalProperties: false }),
499
+ async execute(_toolCallId, params) {
500
+ const result = await adoptPlanFact("dependency", `${session.attemptId}:record_dependency:${randomUUID()}`, {
501
+ kind: "dependency",
502
+ origin: "plan",
503
+ policy: typeof params?.policy === "string" ? params.policy : "",
504
+ });
505
+ return planToolReceipt(result);
506
+ },
507
+ });
508
+ // Incremental plan payload records (one entry per call) so requirements,
509
+ // verification targets, evidence gaps, and implementation steps never have
510
+ // to be emitted as one large array inside a single finalize_plan call —
511
+ // they aggregate from the ledger in commit order. Mirrors the contract
512
+ // node's record_requirement pattern to stay within any model output budget.
513
+ const recordPlanRequirementTool = defineTool({
514
+ name: "record_plan_requirement",
515
+ label: "record_plan_requirement",
516
+ description: "Commit one plan requirement entry (origin=plan plan-requirement fact). Call once per requirement. Verification targets are authoritative in record_plan_verification_target; omit verificationTargetIds here unless repairing legacy input. For a correction, re-submit the same id with replace=true; the ledger compiles the latest replacement. Entry carries id, implementationTargets, and optional expectedOutcome (omit it — the runtime derives the outcome text from the contract requirement). IMPORTANT: batch up to 4 record_* calls per assistant message; never batch more than 4 — a larger single message risks output truncation under a small output window. Example: {\"entry\": {\"id\": \"<AC-XXX>\", \"implementationTargets\": [\"<deliverable file>\"]}}",
517
+ promptSnippet: "Commit 1-4 plan requirement entries (up to 4 per message).",
518
+ parameters: Type.Object({
519
+ entry: requirementSchema,
520
+ replace: Type.Optional(Type.Boolean()),
521
+ }, { additionalProperties: false }),
522
+ async execute(_toolCallId, params) {
523
+ const rawEntry = params?.entry;
524
+ if (!isRecordObject(rawEntry)) {
525
+ return planToolReceipt({
526
+ ok: false,
527
+ kind: "plan-requirement",
528
+ error: "record_plan_requirement requires a non-empty entry object",
529
+ });
530
+ }
531
+ let entry = rawEntry;
532
+ // An embedded evidenceGap with a blank description means "no gap":
533
+ // small-output models emit the slot defensively with description ""
534
+ // on every requirement. The canonical contract schema requires a
535
+ // non-empty gap description (min 1 char), so passing the empty slot
536
+ // through would deterministically fail the plan compile with
537
+ // invalid-output and burn every retry. Drop the empty slot — the
538
+ // field is optional and the runtime derives real blocking gaps when
539
+ // a requirement has no proof.
540
+ const rawGap = isRecordObject(entry.evidenceGap)
541
+ ? entry.evidenceGap
542
+ : undefined;
543
+ if (rawGap &&
544
+ typeof rawGap.description === "string" &&
545
+ rawGap.description.trim() === "") {
546
+ const { evidenceGap: _omittedGap, ...rest } = entry;
547
+ void _omittedGap;
548
+ entry = rest;
549
+ }
550
+ // Canonical-identity check: a requirement id outside the frozen
551
+ // canonical list (e.g. a BR-* business rule picked up from the PRD
552
+ // prose) would commit an immutable fact that finalize's
553
+ // canonical-coverage gate rejects with no in-node cure. Reject here
554
+ // and name the allowed ids.
555
+ const id = typeof entry.id === "string" ? entry.id : "";
556
+ if (session.activeRequirementScope.length && !session.activeRequirementScope.includes(id))
557
+ return planToolReceipt({ ok: false, kind: "plan-requirement", code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: `Requirement ${id} is outside this session` });
558
+ if (id &&
559
+ input.requirementIds &&
560
+ input.requirementIds.length > 0 &&
561
+ !input.requirementIds.includes(id)) {
562
+ return planToolReceipt({
563
+ ok: false,
564
+ kind: "plan-requirement",
565
+ error: `record_plan_requirement id "${id}" is not a frozen canonical requirement; canonical ids are: ${input.requirementIds.join(", ")}`,
566
+ });
567
+ }
568
+ // A requirement id is a canonical identity: recording it twice would
569
+ // compile a duplicate requirements[] entry and fail design review.
570
+ // Reject duplicates by default; replace=true is the explicit in-node
571
+ // correction path and the compiler keeps the latest replacement.
572
+ if (id && params?.replace !== true) {
573
+ const existing = readCommittedEvents(session.store, session.attemptId).find((event) => {
574
+ const fact = event.fact;
575
+ if (!fact || fact.kind !== "plan-requirement")
576
+ return false;
577
+ const entryFact = fact.entry;
578
+ return (typeof entryFact?.id === "string" &&
579
+ entryFact.id === id);
580
+ });
581
+ if (existing) {
582
+ return planToolReceipt({
583
+ ok: false,
584
+ kind: "plan-requirement",
585
+ error: `record_plan_requirement duplicate: requirement ${id} is already recorded; do not record the same requirement id twice`,
586
+ });
587
+ }
588
+ }
589
+ if (id &&
590
+ session.activeRequirementScope.length > 0 &&
591
+ !session.activeRequirementScope.includes(id)) {
592
+ return planToolReceipt({
593
+ ok: false,
594
+ kind: "plan-requirement",
595
+ error: `record_plan_requirement id "${id}" is outside this session's requirement scope [${session.activeRequirementScope.join(", ")}]`,
596
+ });
597
+ }
598
+ const result = await adoptPlanFact("plan-requirement", `${session.attemptId}:record_plan_requirement:${randomUUID()}`, {
599
+ kind: "plan-requirement",
600
+ origin: "plan",
601
+ entry,
602
+ ...(params?.replace === true ? { replaces: id } : {}),
603
+ });
604
+ return planToolReceipt(result);
605
+ },
606
+ });
607
+ const recordPlanGroupCoverageTool = defineTool({
608
+ name: "record_plan_group_coverage", label: "record_plan_group_coverage",
609
+ description: "Submit shared implementation/verification references for one declared execution group. Runtime expands to every canonical member and retains its full outcome and source bindings. A shared VT must actually verify each independent condition. Use per-requirement records for differences; never create UI for constraints or exclusions. replace:true explicitly revises the group.",
610
+ parameters: Type.Object({ id: Type.String({ minLength: 1 }), implementationTargets: stringArray, verificationTargetIds: Type.Optional(stringArray), replace: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
611
+ async execute(callId, params, signal, onUpdate, ctx) {
612
+ const group = executionGroups.find(g => g.id === params.id && g.kind !== "unclassified");
613
+ if (!group || (session.activeRequirementScope.length && group.requirementIds.some(id => !session.activeRequirementScope.includes(id))))
614
+ return planToolReceipt({ ok: false, kind: "plan-requirement", code: "EXECUTION_GROUP_SCOPE_INVALID", error: "A known complete group must be present in this session; submit individual member records when the group spans scopes" });
615
+ let last;
616
+ for (const id of group.requirementIds) {
617
+ const existing = readCommittedEvents(session.store, session.attemptId).filter(r => r.fact.kind === "plan-requirement" && r.fact.entry?.id === id).at(-1)?.fact.entry;
618
+ if (existing && !params.replace) {
619
+ if (JSON.stringify(existing.implementationTargets) !== JSON.stringify(params.implementationTargets))
620
+ return planToolReceipt({ ok: false, kind: "plan-requirement", code: "FACT_IDENTITY_CONFLICT", error: `${id}: existing coverage differs; use replace:true to revise explicitly` });
621
+ continue;
622
+ }
623
+ last = await recordPlanRequirementTool.execute(`${callId}:${id}`, { entry: { id, implementationTargets: params.implementationTargets, ...(params.verificationTargetIds ? { verificationTargetIds: params.verificationTargetIds } : {}) }, ...(params.replace ? { replace: true } : {}) }, signal, onUpdate, ctx);
624
+ if (!last.details?.ok)
625
+ return last;
626
+ }
627
+ return planToolReceipt(await adoptPlanFact("plan-group-coverage", `${session.attemptId}:group:${randomUUID()}`, { kind: "plan-group-coverage", origin: "plan", id: group.id, requirementIds: group.requirementIds }));
628
+ },
629
+ });
630
+ const recordPlanVerificationTargetTool = defineTool({
631
+ name: "record_plan_verification_target",
632
+ label: "record_plan_verification_target",
633
+ description: "Commit one plan verification target entry (origin=plan plan-verification-target fact). Reference a frozen verification command by commandId (see the frozen command directory in your prompt: static commands are project-wide checks traced by file and command only; behavior commands bind an existing affected test file; preserve its test names). For behavior targets, call once per distinct behavior, not mechanically once per requirement: one target may cover multiple related requirementIds. For a correction, re-submit the same id with replace=true; the ledger compiles the latest replacement. A target id identifies a contract entry only; never require it in test names. Results bind by frozen command and test file. Entry carries id, commandId, file, requirementIds, and uiStates; optional scope (unit | component | integration) is display-only. Free-form symbol text is not accepted. IMPORTANT: batch up to 4 record_* calls per assistant message; never batch more than 4 — a larger single message risks output truncation under a small output window. Example: {\"entry\": {\"id\": \"VT-DASHBOARD-SHELL\", \"commandId\": \"<frozen behavior command id>\", \"file\": \"<test file>\", \"requirementIds\": [\"AC-001\", \"AC-002\"], \"uiStates\": []}}" +
634
+ (input.canonicalVerificationTargetIds &&
635
+ input.canonicalVerificationTargetIds.length > 0
636
+ ? ` Frozen canonical behavior target ids (use exactly for behavior targets): ${input.canonicalVerificationTargetIds.join(", ")}.`
637
+ : ""),
638
+ promptSnippet: "Commit 1-4 plan verification target entries (up to 4 per message).",
639
+ parameters: Type.Object({
640
+ entry: verificationTargetSchema,
641
+ replace: Type.Optional(Type.Boolean()),
642
+ }, { additionalProperties: false }),
643
+ async execute(_toolCallId, params) {
644
+ const rawEntry = params?.entry;
645
+ if (!isRecordObject(rawEntry)) {
646
+ return planToolReceipt({
647
+ ok: false,
648
+ kind: "plan-verification-target",
649
+ error: "record_plan_verification_target requires a non-empty entry object",
650
+ });
651
+ }
652
+ // Contract v2: the plan references a frozen command by commandId;
653
+ // mode and label are resolved by the runtime from the frozen
654
+ // command directory (run.json frontend-verify-shell bundle lanes).
655
+ // Reject unknown ids and behavior commands bound to non-test
656
+ // files here so the model fixes them in-node instead of the
657
+ // attempt dying at materialization or at the writer focused-check.
658
+ const verificationCommandId = typeof rawEntry.commandId === "string"
659
+ ? rawEntry.commandId.trim()
660
+ : "";
661
+ if (!verificationCommandId) {
662
+ return planToolReceipt({
663
+ ok: false,
664
+ kind: "plan-verification-target",
665
+ error: `record_plan_verification_target entry.commandId is required (received ${JSON.stringify(rawEntry.commandId ?? null)}); pick one id from the frozen command directory in your prompt`,
666
+ });
667
+ }
668
+ const { deriveFrontendVerifyCommandDirectoryFromRun, isFrontendTestFilePath, } = await import("../../../workflows/dag/frontend-implementation-contract.js");
669
+ const verifyDirectory = await deriveFrontendVerifyCommandDirectoryFromRun(input.runDir);
670
+ if (verifyDirectory.length > 0) {
671
+ const directoryEntry = verifyDirectory.find((entry) => entry.commandId === verificationCommandId);
672
+ if (!directoryEntry) {
673
+ return planToolReceipt({
674
+ ok: false,
675
+ kind: "plan-verification-target",
676
+ error: `record_plan_verification_target verification-target-unknown-command: unknown commandId "${verificationCommandId}"; available frozen commands: [${verifyDirectory.map((entry) => `${entry.commandId} (${entry.mode}: ${entry.label})`).join(", ")}]`,
677
+ });
678
+ }
679
+ if (directoryEntry.mode === "behavior" &&
680
+ typeof rawEntry.file === "string" &&
681
+ !isFrontendTestFilePath(rawEntry.file)) {
682
+ return planToolReceipt({
683
+ ok: false,
684
+ kind: "plan-verification-target",
685
+ error: `record_plan_verification_target verification-target-phase-mismatch: behavior command "${directoryEntry.label}" (${directoryEntry.commandId}) must bind a test file (__tests__/, tests?/, e2e/, cypress/, *.test.*, *.spec.*, *.cy.*); received file "${rawEntry.file}"`,
686
+ });
687
+ }
688
+ // Canonical-identity check for behavior targets: the PRD freezes
689
+ // the exact behavior verification-target ids (e.g.
690
+ // VT-SMOKE-COUNTER-BEHAVIOR). A committed non-canonical id is
691
+ // immutable and design review rejects it as a
692
+ // contract-requirement gap, so reject invented ids here.
693
+ if (directoryEntry.mode === "behavior" &&
694
+ input.canonicalVerificationTargetIds &&
695
+ input.canonicalVerificationTargetIds.length > 0) {
696
+ const canonicalTargetId = typeof rawEntry.id === "string" ? rawEntry.id.trim() : "";
697
+ if (canonicalTargetId &&
698
+ !input.canonicalVerificationTargetIds.includes(canonicalTargetId)) {
699
+ return planToolReceipt({
700
+ ok: false,
701
+ kind: "plan-verification-target",
702
+ error: `record_plan_verification_target id "${canonicalTargetId}" is not a frozen canonical behavior verification target; canonical ids are: ${input.canonicalVerificationTargetIds.join(", ")}`,
703
+ });
704
+ }
705
+ }
706
+ }
707
+ // Duplicate-id rejection: committed typed facts are immutable, so
708
+ // re-recording the same VT id would deadlock the compile by default.
709
+ // replace=true is the explicit in-node correction path.
710
+ const vtId = typeof rawEntry.id === "string" ? rawEntry.id : "";
711
+ if (vtId &&
712
+ params?.replace !== true &&
713
+ readCommittedEvents(session.store, session.attemptId).some((event) => {
714
+ const fact = event.fact;
715
+ if (!fact || fact.kind !== "plan-verification-target")
716
+ return false;
717
+ const entryFact = fact.entry;
718
+ return entryFact?.id === vtId;
719
+ })) {
720
+ return planToolReceipt({
721
+ ok: false,
722
+ kind: "plan-verification-target",
723
+ error: `record_plan_verification_target duplicate: verification target ${vtId} is already recorded; use replace=true for an in-node correction`,
724
+ });
725
+ }
726
+ // WriteSet containment at the boundary: a committed VT fact whose
727
+ // file is outside the task writeSet is immutable, and the finalize
728
+ // pre-validation would then fail the whole attempt with no in-node
729
+ // cure (r17 post-merge). Reject here with the allowed patterns.
730
+ if (typeof rawEntry.file === "string" &&
731
+ input.writeSetPatterns &&
732
+ input.writeSetPatterns.length > 0 &&
733
+ !input.writeSetPatterns.some((pattern) => pathMatchesPattern(rawEntry.file, pattern))) {
734
+ return planToolReceipt({
735
+ ok: false,
736
+ kind: "plan-verification-target",
737
+ error: `record_plan_verification_target file is outside the writeSet patterns [${input.writeSetPatterns.join(", ")}]: ${rawEntry.file}; verification targets must point inside the task writeSet`,
738
+ });
739
+ }
740
+ // uiStates: [] means this verification target is intentionally not
741
+ // bound to a named UI state. Keep that canonical representation even
742
+ // when a model omits the optional tool-boundary field.
743
+ const entryWithoutSymbol = { ...rawEntry };
744
+ delete entryWithoutSymbol.symbol;
745
+ const entry = {
746
+ ...entryWithoutSymbol,
747
+ uiStates: stringList(rawEntry.uiStates),
748
+ };
749
+ // Cross-reference integrity at the boundary: the compile gate
750
+ // rejects verification targets referencing UI states or
751
+ // requirements that were never declared. Validate against the
752
+ // facts already committed in this attempt so the model fixes the
753
+ // reference in-node instead of burning the attempt at compile time
754
+ // (r7: one full attempt lost to a single unknown UI state name).
755
+ const committedEvents = readCommittedEvents(session.store, session.attemptId);
756
+ const declaredUiStateNames = collectCanonicalStateFlowNames(committedEvents).uiStateNames;
757
+ const unknownUiStates = entry.uiStates.filter((name) => !declaredUiStateNames.has(name));
758
+ if (unknownUiStates.length > 0) {
759
+ return planToolReceipt({
760
+ ok: false,
761
+ kind: "plan-verification-target",
762
+ error: `record_plan_verification_target references UI states that were never declared: ${unknownUiStates.join(", ")}; declare every referenced UI state with record_state_flow first, or pass uiStates: [] for intentionally unbound targets`,
763
+ });
764
+ }
765
+ const declaredRequirementIds = new Set(committedEvents.flatMap((event) => {
766
+ const fact = event.fact;
767
+ if (!fact || fact.kind !== "plan-requirement")
768
+ return [];
769
+ const requirementEntry = fact.entry;
770
+ return typeof requirementEntry?.id === "string"
771
+ ? [requirementEntry.id]
772
+ : [];
773
+ }));
774
+ const unknownRequirementIds = stringList(rawEntry.requirementIds).filter((id) => !declaredRequirementIds.has(id));
775
+ const outOfScopeRequirementIds = stringList(rawEntry.requirementIds).filter((id) => session.activeRequirementScope.length > 0 &&
776
+ !session.activeRequirementScope.includes(id));
777
+ if (outOfScopeRequirementIds.length > 0) {
778
+ return planToolReceipt({
779
+ ok: false,
780
+ kind: "plan-verification-target",
781
+ error: `record_plan_verification_target references requirements outside this session's scope [${session.activeRequirementScope.join(", ")}]: ${outOfScopeRequirementIds.join(", ")}`,
782
+ });
783
+ }
784
+ if (unknownRequirementIds.length > 0) {
785
+ return planToolReceipt({
786
+ ok: false,
787
+ kind: "plan-verification-target",
788
+ error: `record_plan_verification_target references unknown requirement ids: ${unknownRequirementIds.join(", ")}; record every referenced requirement with record_plan_requirement first`,
789
+ });
790
+ }
791
+ const result = await adoptPlanFact("plan-verification-target", `${session.attemptId}:record_plan_verification_target:${randomUUID()}`, {
792
+ kind: "plan-verification-target",
793
+ origin: "plan",
794
+ entry,
795
+ ...(params?.replace === true ? { replaces: vtId } : {}),
796
+ });
797
+ return planToolReceipt(result);
798
+ },
799
+ });
800
+ const recordPlanEvidenceGapTool = defineTool({
801
+ name: "record_plan_evidence_gap",
802
+ label: "record_plan_evidence_gap",
803
+ description: "Commit one plan evidence gap entry (origin=plan plan-evidence-gap fact). Call once per gap; entry carries requirementId, description, blocking. IMPORTANT: batch up to 4 record_* calls per assistant message; never batch more than 4 — a larger single message risks output truncation under a small output window. Example: {\"entry\": {\"requirementId\": \"<AC-XXX>\", \"description\": \"<what evidence is missing and why>\", \"blocking\": false}}",
804
+ promptSnippet: "Commit 1-4 plan evidence gap entries (up to 4 per message).",
805
+ parameters: Type.Object({
806
+ entry: evidenceGapSchema,
807
+ }, { additionalProperties: false }),
808
+ async execute(_toolCallId, params) {
809
+ const rawEntry = params?.entry;
810
+ if (!isRecordObject(rawEntry)) {
811
+ return planToolReceipt({
812
+ ok: false,
813
+ kind: "plan-evidence-gap",
814
+ error: "record_plan_evidence_gap requires a non-empty entry object",
815
+ });
816
+ }
817
+ const entry = rawEntry;
818
+ // A standalone evidence gap IS the gap statement: a blank description
819
+ // would fail the canonical contract schema (min 1 char) after the
820
+ // whole attempt finished. Reject at the boundary so the model writes
821
+ // a real description in-node instead of burning the attempt.
822
+ if (typeof entry.description === "string" &&
823
+ entry.description.trim() === "") {
824
+ return planToolReceipt({
825
+ ok: false,
826
+ kind: "plan-evidence-gap",
827
+ error: "record_plan_evidence_gap requires a non-empty description describing the gap",
828
+ });
829
+ }
830
+ const requirementId = typeof entry.requirementId === "string"
831
+ ? entry.requirementId
832
+ : undefined;
833
+ if (requirementId &&
834
+ session.activeRequirementScope.length > 0 &&
835
+ !session.activeRequirementScope.includes(requirementId)) {
836
+ return planToolReceipt({
837
+ ok: false,
838
+ kind: "plan-evidence-gap",
839
+ error: `record_plan_evidence_gap requirementId "${requirementId}" is outside this session's requirement scope [${session.activeRequirementScope.join(", ")}]`,
840
+ });
841
+ }
842
+ const result = await adoptPlanFact("plan-evidence-gap", `${session.attemptId}:record_plan_evidence_gap:${randomUUID()}`, { kind: "plan-evidence-gap", origin: "plan", entry });
843
+ return planToolReceipt(result);
844
+ },
845
+ });
846
+ return {
847
+ recordRouteSelectionTool,
848
+ recordComponentChoiceTool,
849
+ recordStateRegistryTool,
850
+ recordStateFlowTool,
851
+ recordDataFlowTool,
852
+ recordMockApiTool,
853
+ recordMockEndpointTool,
854
+ recordDesignDeviationTool,
855
+ recordDependencyTool,
856
+ recordPlanRequirementTool,
857
+ recordPlanGroupCoverageTool,
858
+ recordPlanVerificationTargetTool,
859
+ recordPlanEvidenceGapTool,
860
+ };
861
+ }