@tea-agent/loop-agent 0.43.0-next.17 → 0.43.0-next.3

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 (187) hide show
  1. package/CHANGELOG.md +9 -140
  2. package/dist/application/task-lifecycle/advance.js +4 -17
  3. package/dist/application/task-lifecycle/observe.js +29 -43
  4. package/dist/application/task-lifecycle/plan-transitions.js +4 -5
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/commands/client-recovery.js +55 -20
  7. package/dist/commands/new-task.js +5 -0
  8. package/dist/commands/status.js +57 -0
  9. package/dist/commands/task-contract.js +269 -0
  10. package/dist/executors/dag-pi-executor.js +230 -269
  11. package/dist/executors/pi-executor.js +0 -2
  12. package/dist/executors/pi-sdk-executor.js +39 -77
  13. package/dist/executors/shell-executor.js +2 -3
  14. package/dist/executors/shell-write-guard.js +32 -27
  15. package/dist/shared/pi-context-pressure/extension.js +16 -26
  16. package/dist/shared/pi-retry-settings.js +0 -40
  17. package/dist/shared/prompts.js +26 -0
  18. package/dist/task/config.js +2 -0
  19. package/dist/task/contract/validate-draft.js +106 -0
  20. package/dist/task/goal.js +3 -0
  21. package/dist/task/index.js +12 -0
  22. package/dist/task/lifecycle.js +1 -0
  23. package/dist/task/operator/capabilities.js +6 -0
  24. package/dist/task/operator/envelope.js +2 -0
  25. package/dist/task/operator/index.js +5 -0
  26. package/dist/task/operator/registry.js +2 -0
  27. package/dist/task/operator/types.js +1 -0
  28. package/dist/task/paths.js +1 -0
  29. package/dist/task/source-prepare/source-fidelity-pi.js +2 -2
  30. package/dist/task/source-references.js +23 -48
  31. package/dist/task/source-state.js +1 -0
  32. package/dist/task/subagent-guidance.js +1 -0
  33. package/dist/worker/console/chat/assistant-content.js +2 -23
  34. package/dist/worker/console/chat/explore-tools.js +0 -13
  35. package/dist/worker/console/chat/pi-runtime.js +1182 -221
  36. package/dist/worker/console/chat/routes.js +71 -213
  37. package/dist/worker/console/chat/sdd-data-alignment.js +9 -65
  38. package/dist/worker/console/chat/session-catalog.js +0 -32
  39. package/dist/worker/console/chat/session-store.js +6 -22
  40. package/dist/worker/console/chat/shortcuts.js +0 -6
  41. package/dist/worker/console/chat/sift-bridge.js +1 -0
  42. package/dist/worker/console/chat/tool-preview.js +0 -11
  43. package/dist/worker/console/chat/tools.js +0 -6
  44. package/dist/worker/console/console-update-and-init.js +19 -1
  45. package/dist/worker/console/console-update-runtime.js +20 -5
  46. package/dist/worker/console/index.js +20 -0
  47. package/dist/worker/console/operator-actions.js +1 -13
  48. package/dist/worker/console/routes.js +0 -8
  49. package/dist/worker/console/server.js +29 -11
  50. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4NXyedy.js → abnfDiagram-N423BO3Z-hUmM_GRf.js} +1 -1
  51. package/dist/worker/console/static/assets/{arc-NZ3qXvBI.js → arc-Bt-xuieW.js} +1 -1
  52. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-COZSRvml.js → architectureDiagram-T3A2C74G-DTqkL4DJ.js} +1 -1
  53. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Ck0Gxi9C.js → blockDiagram-VBNYF7ZC-Cy0MNnzV.js} +1 -1
  54. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BX5HQ4Ox.js → c4Diagram-5PPSVZJV-DDdlT0Pd.js} +1 -1
  55. package/dist/worker/console/static/assets/channel-BpzRnZmU.js +1 -0
  56. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CoviemSL.js → chunk-2GRJ4B5K-3JawwNZ6.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DbOdOkbZ.js → chunk-2Q5K7J3B-DoTp7Cri.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BsG2pmdQ.js → chunk-5RXB4S5H-CcirhZaS.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DGdvrNm8.js → chunk-5VM5RSS4-VrG21gnP.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAy7Yd6y.js → chunk-6Q2QTUOP-DDOgIS70.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D9UA7hUb.js → chunk-GF5L2VYU-CBjmgvJ_.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DPz7SZ8V.js → chunk-JWPE2WC7-CCQSht8M.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BmuEOIh7.js → chunk-KBJHAD2P-BpGMkUUV.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CIhBWnd8.js → chunk-RYQCIY6F-DNDT41QL.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CScR6mDD.js → chunk-XXDRQBXY-euXnDFbn.js} +1 -1
  66. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +1 -0
  67. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +1 -0
  68. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BjpfWaUr.js → cose-bilkent-JH36ORCC-BJ6NAeql.js} +1 -1
  69. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BFQohJgT.js → cynefin-VYW2F7L2-F5JKIvIq.js} +1 -1
  70. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D3gzRJnS.js → cynefinDiagram-MW4NZA55-Cwmkl_Gi.js} +1 -1
  71. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-5eojkhoA.js → dagre-VZM6K2ZE-BmCZ8815.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CpD8LWu_.js → diagram-7IWD3JNH-zWA3hUnX.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8_4qJ2a.js → diagram-B4RE2ZJO-BzA-888F.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-HOsXf0eu.js → diagram-LBJQPF4R-tPNheoch.js} +1 -1
  75. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BqwRwiWP.js → diagram-Q27KOJAE-Cdi7Fyqp.js} +1 -1
  76. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BRVCNZe1.js → diagram-UB23O5K3-DFK9VBf1.js} +1 -1
  77. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Cvb1TgGh.js → ebnfDiagram-BXEA7PRR-xCUr1kpg.js} +1 -1
  78. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DS0O0qG9.js → erDiagram-JOGREHBK-BANQHY_A.js} +1 -1
  79. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BHIRvRr3.js → flowDiagram-UKHOOZJN-VHJYEMkP.js} +1 -1
  80. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DcCJ1JWx.js → ganttDiagram-PKOTCBZU-DiyZQ0C6.js} +1 -1
  81. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-B9ia5ony.js → gitGraphDiagram-DS77QQ5N-CZtIeBBa.js} +1 -1
  82. package/dist/worker/console/static/assets/index-BlN6ytLr.css +1 -0
  83. package/dist/worker/console/static/assets/index-CA-jfWlE.js +449 -0
  84. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-3ogpgegn.js → infoDiagram-6WML65LV-BuSspC0X.js} +1 -1
  85. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-4tgpQ5SL.js → ishikawaDiagram-WSZJBQD7-DRJZyPFy.js} +1 -1
  86. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DNfgHqFx.js → journeyDiagram-NVQOT4AX-DIfXIVuE.js} +1 -1
  87. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Dz7ngC8z.js → kanban-definition-27J2QSJJ-CT10_7Lt.js} +1 -1
  88. package/dist/worker/console/static/assets/{linear-DI7Jv62E.js → linear-ufemj74H.js} +1 -1
  89. package/dist/worker/console/static/assets/{mermaid.core-CQA4Nzmo.js → mermaid.core-DEQe8-_m.js} +5 -5
  90. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CtsMc8Mf.js → mindmap-definition-FAOFIHXS-BRLUrbrC.js} +1 -1
  91. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-ByppE9yH.js → pegDiagram-VL7TDLO6-BgibKtiF.js} +1 -1
  92. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B5PtgFgt.js → pieDiagram-7S7Q4E2Y-D3eyfdV9.js} +1 -1
  93. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq4p0k2K.js → quadrantDiagram-CIZ2JOQS-DXeTk5Px.js} +1 -1
  94. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DjfkyljN.js → railroadDiagram-AXF67PYL-BgXdFEe7.js} +1 -1
  95. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DzynNRSl.js → requirementDiagram-LRYGKXZP-CseC2A7R.js} +1 -1
  96. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cj7TCpTJ.js → sankeyDiagram-W5VNT64P-D80PFzfJ.js} +1 -1
  97. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C73C6ykA.js → sequenceDiagram-SI44F4Z6-BWzHW7LS.js} +1 -1
  98. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BiNLgN2x.js → sizeCapture-X5ZJPWSS-xWQYTLAn.js} +1 -1
  99. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-UwvjS64-.js → stateDiagram-OKZ733FA-CLxtpZeL.js} +1 -1
  100. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +1 -0
  101. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CC7DH98r.js → swimlanes-SLNWSIFB-DwugtmWV.js} +2 -2
  102. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +8 -0
  103. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-LSBXTR70.js → timeline-definition-Z64GVDOM-DHEOwHIe.js} +1 -1
  104. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BAuxWwGP.js → vennDiagram-T6HMQDX7-pmbZKlBK.js} +1 -1
  105. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y--E5_gjZx.js → wardleyDiagram-T6FBY63Y-CivHoM5q.js} +1 -1
  106. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-dJWNmyzF.js → xychartDiagram-ELKLHX3M-BfHIE7o6.js} +1 -1
  107. package/dist/worker/console/static/index.html +2 -2
  108. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +10 -122
  109. package/dist/worker/console/static-src/operator-chat/compaction-message.js +0 -7
  110. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +15 -35
  111. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +62 -136
  112. package/dist/worker/console/static-src/operator-chat/useChatStream.js +68 -159
  113. package/dist/worker/console/static-src/operator-chat/useChatThread.js +56 -75
  114. package/dist/worker/console/static-src/operator-chat/useComposer.js +78 -206
  115. package/dist/worker/console/workspace-context.js +1 -21
  116. package/dist/worker/observe/static/router.js +1 -1
  117. package/dist/worker/observe/static/styles.css +4 -7
  118. package/dist/worker/observe/static/views/dag-inspector.js +19 -12
  119. package/dist/worker/observe/static/views/dag.js +2 -2
  120. package/dist/worker/scheduler/index.js +25 -0
  121. package/dist/workflows/dag/facts.js +4 -0
  122. package/dist/workflows/dag/frontend-closeout.js +1 -1
  123. package/dist/workflows/dag/frontend-design-policy.js +1 -2
  124. package/dist/workflows/dag/frontend-implementation-contract.js +4 -8
  125. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  126. package/dist/workflows/dag/frontend-repair.js +76 -55
  127. package/dist/workflows/dag/frontend-review-context.js +1 -2
  128. package/dist/workflows/dag/frontend-review-scopes.js +12 -34
  129. package/dist/workflows/dag/frontend-shape-facts.js +2 -12
  130. package/dist/workflows/dag/frontend-verification-trace.js +42 -8
  131. package/dist/workflows/dag/index.js +6 -0
  132. package/dist/workflows/dag/init-hybrid.js +160 -97
  133. package/dist/workflows/dynamic/index.js +7 -0
  134. package/docs/architecture/runtime-boundaries.md +3 -3
  135. package/docs/templates/frontend-implementation-dag.json +8 -9
  136. package/package.json +3 -7
  137. package/skills/frontend-bounded-implement/SKILL.md +3 -4
  138. package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
  139. package/skills/frontend-contract/references/contract-protocol.md +1 -3
  140. package/skills/frontend-design-review/SKILL.md +7 -12
  141. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  142. package/skills/frontend-plan/SKILL.md +0 -19
  143. package/skills/frontend-plan/references/decision-contract.md +19 -26
  144. package/skills/frontend-review/SKILL.md +18 -12
  145. package/skills/frontend-review/references/review-findings.md +43 -24
  146. package/dist/shared/pi-provider-recovery.js +0 -182
  147. package/dist/worker/console/chat/browser-automation.js +0 -363
  148. package/dist/worker/console/chat/browser-policy.js +0 -113
  149. package/dist/worker/console/chat/browser-routes.js +0 -162
  150. package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +0 -44
  151. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +0 -89
  152. package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +0 -202
  153. package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +0 -79
  154. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +0 -86
  155. package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +0 -38
  156. package/dist/worker/console/chat/pi-runtime/inspection-types.js +0 -28
  157. package/dist/worker/console/chat/pi-runtime/inspection.js +0 -439
  158. package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +0 -153
  159. package/dist/worker/console/chat/pi-runtime/sdk-session.js +0 -5
  160. package/dist/worker/console/chat/scm-routes.js +0 -217
  161. package/dist/worker/console/chat/scm-service.js +0 -283
  162. package/dist/worker/console/chat/scm-tools.js +0 -111
  163. package/dist/worker/console/chat/session-mode-view.js +0 -49
  164. package/dist/worker/console/chat/session-mode.js +0 -220
  165. package/dist/worker/console/chat/terminal-routes.js +0 -216
  166. package/dist/worker/console/chat/terminal-sessions.js +0 -348
  167. package/dist/worker/console/chat/terminal-tools.js +0 -211
  168. package/dist/worker/console/static/assets/channel-BxdTlb_5.js +0 -1
  169. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-_ByA1Nf8.js +0 -1
  170. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-_ByA1Nf8.js +0 -1
  171. package/dist/worker/console/static/assets/index-B9goN3S5.css +0 -1
  172. package/dist/worker/console/static/assets/index-wXlQohNs.js +0 -469
  173. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-nLgMZ0D3.js +0 -1
  174. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BcYqTady.js +0 -8
  175. package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +0 -10
  176. package/dist/worker/console/static-src/operator-chat/chat-link.js +0 -94
  177. package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +0 -24
  178. package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +0 -192
  179. package/dist/worker/console/static-src/operator-chat/process-label.js +0 -41
  180. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +0 -13
  181. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +0 -10
  182. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +0 -51
  183. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +0 -21
  184. package/dist/worker/console/workspace-initialization.js +0 -209
  185. package/dist/workflows/dag/frontend-capacity.js +0 -9
  186. package/dist/workflows/dag/frontend-plan-completeness.js +0 -186
  187. package/dist/workflows/dag/frontend-plan-recovery-policy.js +0 -18
@@ -1,6 +1,3 @@
1
- import { collectFrontendPlanMissingFacts, collectFrontendPlanPhaseMissingFacts, committedFactFromPlanRecord, planFactStringList, planFactScopeIntersects } from "../workflows/dag/frontend-plan-completeness.js";
2
- export { collectFrontendPlanMissingFacts, collectFrontendPlanPhaseMissingFacts } from "../workflows/dag/frontend-plan-completeness.js";
3
- import { classifyFrontendPlanRecovery } from "../workflows/dag/frontend-plan-recovery-policy.js";
4
1
  import { collectFrontendExecutionGroups, frontendExecutionSchema } from "../workflows/dag/frontend-execution-groups.js";
5
2
  import { FRONTEND_SCOPE_TARGET_BYTES, packFrontendInputUnits, parseFrontendInputBlock, projectFrontendContractPrompt, projectFrontendInputScope } from "../workflows/dag/frontend-input-projection.js";
6
3
  import { createDurableFrontendTools } from "../workflows/dag/frontend-durable-tools.js";
@@ -561,7 +558,6 @@ export function resolveDagPiToolNames(task) {
561
558
  if (isFrontendReviewTypedTerminalNode(task)) {
562
559
  return [
563
560
  ...DAG_PI_READONLY_TOOLS,
564
- "complete_review_scope",
565
561
  "record_review_finding",
566
562
  "approve_review",
567
563
  "request_review_changes",
@@ -570,7 +566,6 @@ export function resolveDagPiToolNames(task) {
570
566
  if (isFrontendDesignTypedTerminalNode(task)) {
571
567
  return [
572
568
  ...DAG_PI_READONLY_TOOLS,
573
- "complete_review_scope",
574
569
  "record_design_finding",
575
570
  "approve_design",
576
571
  "request_design_changes",
@@ -597,7 +592,6 @@ export function resolveDagPiToolNames(task) {
597
592
  ...FRONTEND_PLAN_RECORD_TOOL_NAMES,
598
593
  ...FRONTEND_PLAN_TERMINAL_TOOL_NAMES,
599
594
  ...FRONTEND_PLAN_ADOPT_TOOL_NAMES,
600
- "read_plan_facts",
601
595
  ];
602
596
  }
603
597
  if ((task.readSet?.length ?? 0) > 0) {
@@ -1817,44 +1811,6 @@ export async function createFrontendPlanLedgerTools(input) {
1817
1811
  });
1818
1812
  }
1819
1813
  }
1820
- // Named entries are last-wins at assembly. A scoped correction may
1821
- // replace its own bindings, but must retain other scopes' coverage.
1822
- const scope = new Set(scopedRequirementIds());
1823
- const committed = readCommittedEvents(store, attemptId).map(event => event.fact);
1824
- const targetOwners = new Map();
1825
- for (const fact of committed) {
1826
- if (fact.kind === "plan-verification-target" && isRecordObject(fact.entry) && typeof fact.entry.id === "string") {
1827
- targetOwners.set(fact.entry.id, stringList(fact.entry.requirementIds));
1828
- }
1829
- }
1830
- for (const [field, removals] of [["uiStates", removeUiStateNames], ["interactions", removeInteractionNames]]) {
1831
- const live = new Map();
1832
- for (const fact of committed) {
1833
- if (fact.kind !== "state-flow")
1834
- continue;
1835
- for (const name of stringList(fact[field === "uiStates" ? "removeUiStateNames" : "removeInteractionNames"]))
1836
- live.delete(name);
1837
- for (const entry of Array.isArray(fact[field]) ? fact[field] : []) {
1838
- if (isRecordObject(entry) && typeof entry.name === "string")
1839
- live.set(entry.name, entry);
1840
- }
1841
- }
1842
- const foreignBindings = (entry) => scope.size === 0 ? [] : stringList(entry.verificationTargetIds).filter(id => {
1843
- const owners = targetOwners.get(id);
1844
- return !owners?.length || owners.some(owner => !scope.has(owner));
1845
- });
1846
- for (const name of removals) {
1847
- const previous = live.get(name);
1848
- if (previous && foreignBindings(previous).length)
1849
- return planToolReceipt({ ok: false, kind: "state-flow",
1850
- error: `Cannot remove shared ${name} from this requirement scope; retain it and update its scoped bindings instead` });
1851
- }
1852
- for (const entry of field === "uiStates" ? uiStates : interactions) {
1853
- const previous = live.get(String(entry.name));
1854
- if (previous)
1855
- entry.verificationTargetIds = [...new Set([...foreignBindings(previous), ...stringList(entry.verificationTargetIds)])];
1856
- }
1857
- }
1858
1814
  const result = await adoptPlanFact("state-flow", `${attemptId}:record_state_flow:${randomUUID()}`, {
1859
1815
  kind: "state-flow",
1860
1816
  origin: "plan",
@@ -2070,7 +2026,7 @@ export async function createFrontendPlanLedgerTools(input) {
2070
2026
  const recordPlanVerificationTargetTool = defineTool({
2071
2027
  name: "record_plan_verification_target",
2072
2028
  label: "record_plan_verification_target",
2073
- 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\": []}}" +
2029
+ 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 need a test file whose describe/it/test title contains the target id). 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 behavior target id is the stable trace token that implementation must place in a real describe/it/test title. 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\": []}}" +
2074
2030
  (input.canonicalVerificationTargetIds &&
2075
2031
  input.canonicalVerificationTargetIds.length > 0
2076
2032
  ? ` Frozen canonical behavior target ids (use exactly for behavior targets): ${input.canonicalVerificationTargetIds.join(", ")}.`
@@ -2286,13 +2242,9 @@ export async function createFrontendPlanLedgerTools(input) {
2286
2242
  const finalizePlanTool = defineTool({
2287
2243
  name: "finalize_plan",
2288
2244
  label: "finalize_plan",
2289
- description: "Commit the finalize_plan terminal. Requirements, verification targets, and evidence gaps (optional) were already committed incrementally through record_plan_requirement / record_plan_verification_target / record_plan_evidence_gap; finalize_plan requires implementationSteps: [{requirementIds, action}] covering every frozen requirement (shared steps allowed), and assembles them with the ledger and optional remaining fields, publishes the canonical editable patch on a target-surface fact, and commits the terminal. A successful terminal commit occurs exactly once. If validation fails, correct only the reported facts and retry finalize.",
2290
- promptSnippet: "Commit finalize_plan with implementationSteps: [{requirementIds, action}] covering every frozen requirement; optional residualRisks / realIntegrationGap.",
2245
+ description: "Commit the finalize_plan terminal. Requirements, verification targets, and evidence gaps (optional) were already committed incrementally through record_plan_requirement / record_plan_verification_target / record_plan_evidence_gap; finalize_plan assembles them from the ledger together with these optional remaining fields, publishes the canonical editable patch on a target-surface fact, and commits the terminal. A successful terminal commit occurs exactly once. If validation fails, correct only the reported facts and retry finalize.",
2246
+ promptSnippet: "Commit the finalize_plan terminal (ledger fields + optional residualRisks / realIntegrationGap).",
2291
2247
  parameters: Type.Object({
2292
- implementationSteps: Type.Optional(Type.Array(Type.Object({
2293
- requirementIds: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
2294
- action: Type.String({ minLength: 1 }),
2295
- }, { additionalProperties: false }))),
2296
2248
  residualRisks: optionalStringArray,
2297
2249
  realIntegrationGap: optionalString,
2298
2250
  }, { additionalProperties: false }),
@@ -2309,23 +2261,8 @@ export async function createFrontendPlanLedgerTools(input) {
2309
2261
  if (missingData.length)
2310
2262
  return planToolReceipt({ ok: false, kind: "finalize_plan", code: "PLAN_DATA_FLOW_INCOMPLETE", error: missingData.map(f => f.reason).join("; ") });
2311
2263
  const fragment = assemblePlanPatchFromCommittedFacts(committed, contractInheritance) ?? {};
2312
- const requiredIds = new Set([
2313
- ...(input.requirementIds ?? []),
2314
- ...contractInheritance.keys(),
2315
- ...(Array.isArray(fragment.requirements) ? fragment.requirements.flatMap((entry) => isRecordObject(entry) && typeof entry.id === "string" ? [entry.id] : []) : []),
2316
- ]);
2317
- const steps = params?.implementationSteps ?? [];
2318
- const coveredIds = new Set(steps.flatMap(step => step.requirementIds));
2319
- const missingRequirementIds = [...requiredIds].filter(id => !coveredIds.has(id));
2320
- const unknownRequirementIds = [...coveredIds].filter(id => !requiredIds.has(id));
2321
- if (missingRequirementIds.length || unknownRequirementIds.length || steps.some(step => !step.action.trim())) {
2322
- return planToolReceipt({ ok: false, kind: "finalize_plan", code: "PLAN_IMPLEMENTATION_STEPS_INCOMPLETE",
2323
- error: `Submit finalize_plan with non-empty implementationSteps: [{requirementIds: [exact frozen IDs], action: "what to change and where"}]. Cover every requirement; shared steps may cover several IDs. Missing: ${missingRequirementIds.join(", ") || "none"}; unknown: ${unknownRequirementIds.join(", ") || "none"}. Actions must not be blank. Do not add a test plan.`,
2324
- });
2325
- }
2326
2264
  const patch = {
2327
2265
  ...fragment,
2328
- ...(steps.length ? { implementationSteps: steps.map(step => `[${[...new Set(step.requirementIds)].join(", ")}] ${step.action.trim()}`) } : {}),
2329
2266
  ...(params?.residualRisks
2330
2267
  ? { residualRisks: params.residualRisks }
2331
2268
  : {}),
@@ -2478,74 +2415,6 @@ export async function createFrontendPlanLedgerTools(input) {
2478
2415
  }
2479
2416
  },
2480
2417
  });
2481
- const readPlanFactsTool = defineTool({
2482
- name: "read_plan_facts",
2483
- label: "read_plan_facts",
2484
- description: "Read the LIVE committed plan ledger without repository access. Filter by kind and optional entryId or eventId. Use field (dot path, e.g. entry.requirementIds or uiStates) to page large arrays/strings. offset/limit paginate results; follow nextOffset until null before replacing any full-array fact. expectedRevision pins all pages to one ledger revision; restart if it changes. No writes.",
2485
- parameters: Type.Object({
2486
- kind: Type.String(), entryId: Type.Optional(Type.String()), eventId: Type.Optional(Type.String()),
2487
- field: Type.Optional(Type.String()), offset: Type.Optional(Type.Integer({ minimum: 0 })),
2488
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
2489
- stringOffset: Type.Optional(Type.Integer({ minimum: 0 })),
2490
- expectedRevision: Type.Optional(Type.Integer({ minimum: 0 })),
2491
- }, { additionalProperties: false }),
2492
- async execute(_id, params) {
2493
- const reply = (details) => ({ content: [{ type: "text", text: JSON.stringify(details) }], details });
2494
- if (params.expectedRevision !== undefined && params.expectedRevision !== store.revision) {
2495
- return reply({ ok: false, revision: store.revision, error: "ledger revision changed; restart pagination" });
2496
- }
2497
- let records = readCommittedEvents(store, attemptId).filter(record => {
2498
- const fact = record.fact;
2499
- return fact.kind === params.kind && (!params.eventId || record.eventId === params.eventId) &&
2500
- (!params.entryId || fact.entry?.id === params.entryId);
2501
- });
2502
- // Full-entry corrections and registries are last-wins. Other kinds
2503
- // remain an ordered event log (state removals must remain visible).
2504
- if (params.entryId || params.kind === "state-registry")
2505
- records = records.slice(-1);
2506
- let values = records.map(record => ({ eventId: record.eventId, ...record.fact }));
2507
- if (params.field) {
2508
- values = records.flatMap(record => {
2509
- let value = record.fact;
2510
- for (const key of params.field.split(".")) {
2511
- if (!value || typeof value !== "object" || !Object.hasOwn(value, key))
2512
- return [];
2513
- value = value[key];
2514
- }
2515
- return Array.isArray(value) ? value : [value];
2516
- });
2517
- }
2518
- const offset = params.offset ?? 0;
2519
- if (params.stringOffset !== undefined) {
2520
- const value = values[offset];
2521
- if (typeof value !== "string")
2522
- return reply({ ok: false, error: "stringOffset requires a string field item" });
2523
- // Unicode code points prevent a page boundary splitting a surrogate pair.
2524
- const characters = Array.from(value);
2525
- const chunk = characters.slice(params.stringOffset, params.stringOffset + 1000).join("");
2526
- const next = params.stringOffset + Array.from(chunk).length;
2527
- return reply({ ok: true, revision: store.revision, offset, stringOffset: params.stringOffset,
2528
- chunk, totalCharacters: characters.length, nextStringOffset: next < characters.length ? next : null });
2529
- }
2530
- const items = [];
2531
- let bytes = 0;
2532
- for (const value of values.slice(offset, offset + (params.limit ?? 10))) {
2533
- const size = Buffer.byteLength(JSON.stringify(value));
2534
- if (bytes + size > 12_000)
2535
- break;
2536
- bytes += size;
2537
- items.push(value);
2538
- }
2539
- if (!items.length && offset < values.length)
2540
- return reply({
2541
- ok: false, revision: store.revision, total: values.length, offset,
2542
- error: "item exceeds the page budget; select an eventId/entryId and a narrower field path; for a string item set stringOffset: 0 and follow nextStringOffset",
2543
- fields: typeof values[offset] === "object" && values[offset] !== null ? Object.keys(values[offset]) : [],
2544
- });
2545
- return reply({ ok: true, revision: store.revision, total: values.length, offset, items,
2546
- nextOffset: offset + items.length < values.length ? offset + items.length : null });
2547
- },
2548
- });
2549
2418
  const durable = await createDurableFrontendTools({
2550
2419
  file: path.join(input.runDir, input.nodeId, "plan-typed-facts.jsonl"), attemptId, store: input.store,
2551
2420
  setWorkingStore: next => { store = next; },
@@ -2569,7 +2438,7 @@ export async function createFrontendPlanLedgerTools(input) {
2569
2438
  ],
2570
2439
  });
2571
2440
  return {
2572
- customTools: [...durable.customTools, { ...readPlanFactsTool, execute: async (...args) => { await durable.flush(); return readPlanFactsTool.execute(...args); } }],
2441
+ customTools: durable.customTools,
2573
2442
  adoptCommittedFacts: async (records) => durable.commitExternal(async () => {
2574
2443
  for (const record of records) {
2575
2444
  if (record.phase !== "committed")
@@ -3763,7 +3632,7 @@ const FRONTEND_PLAN_SEGMENTS = [
3763
3632
  ]),
3764
3633
  instruction: [
3765
3634
  "PLAN PHASE — requirement coverage only.",
3766
- "Your ONLY job: for every frozen requirement, emit record_plan_requirement (requirement → implementation files) and record_plan_verification_target facts (verification target bound to requirement ids and files). Group related requirements under one non-static behavior target when one observable test behavior proves them together; do not mechanically create one target per requirement. Target ids identify contract entries, not test-title markers. Reuse affected existing test files and their names; do not add tests or rename titles just to carry generated ids. Never submit prose as a symbol. Do NOT record components, UI states, mock, dependency, or routes — a follow-up session owns those.",
3635
+ "Your ONLY job: for every frozen requirement, emit record_plan_requirement (requirement → implementation files) and record_plan_verification_target facts (verification target bound to requirement ids and files). Group related requirements under one non-static behavior target when one observable test behavior proves them together; do not mechanically create one target per requirement. A non-static target id is the stable machine trace token; never submit prose as a symbol. Do NOT record components, UI states, mock, dependency, or routes — a follow-up session owns those.",
3767
3636
  "A coverage session is complete only when EVERY requirement assigned to this session (the full inventory, or the exact COVERAGE BATCH / shard list when present) has committed coverage facts: a record_plan_requirement entry plus verification targets, or a committed evidence gap. Keep committing in batches of up to 4 record_* calls per assistant message until then; do not write a concluding summary while any assigned requirement is still uncommitted — an early stop strands the remainder into a MISSING-FACT repair session and doubles the sessions needed.",
3768
3637
  "If a requirement genuinely cannot have a verification target, record a non-empty record_plan_evidence_gap. Do not call finalize_plan; it is not available in this phase.",
3769
3638
  ].join(" "),
@@ -3821,10 +3690,178 @@ const FRONTEND_PLAN_SEGMENTS = [
3821
3690
  toolNames: null,
3822
3691
  instruction: [
3823
3692
  "PLAN PHASE — finalize.",
3824
- "All record_* tools are available only for a narrowly named correction if the finalize receipt reports missing or invalid facts. Otherwise call finalize_plan with implementationSteps: [{requirementIds, action}] covering every frozen requirement. Describe concrete changes within approved paths, including what constraints preserve; shared steps may cover related IDs. Do not add tests or a test plan.",
3693
+ "All record_* tools are available only for a narrowly named correction if the finalize receipt reports missing or invalid facts. Otherwise call finalize_plan exactly once with no extra fields.",
3825
3694
  ].join(" "),
3826
3695
  },
3827
3696
  ];
3697
+ function committedFactFromPlanRecord(value) {
3698
+ if (!value || typeof value !== "object" || Array.isArray(value))
3699
+ return undefined;
3700
+ const record = value;
3701
+ if (record.phase !== undefined && record.phase !== "committed")
3702
+ return undefined;
3703
+ const fact = record.fact;
3704
+ return fact && typeof fact === "object" && !Array.isArray(fact)
3705
+ ? fact
3706
+ : typeof record.kind === "string"
3707
+ ? record
3708
+ : undefined;
3709
+ }
3710
+ function planFactStringList(value) {
3711
+ if (!Array.isArray(value))
3712
+ return [];
3713
+ return value.filter((item) => typeof item === "string" && item.trim().length > 0);
3714
+ }
3715
+ function planFactScopeIntersects(fact, requirementIds) {
3716
+ return planFactStringList(fact.scopeRequirementIds).some((id) => requirementIds.has(id));
3717
+ }
3718
+ /** Compute the authoritative coverage queue from the committed plan ledger. */
3719
+ export function collectFrontendPlanMissingFacts(input) {
3720
+ const requirements = new Map();
3721
+ const standaloneEvidenceGaps = new Set();
3722
+ const verificationTargetIds = new Set();
3723
+ const verificationTargetRequirements = new Map();
3724
+ for (const value of input.committedFacts) {
3725
+ const fact = committedFactFromPlanRecord(value);
3726
+ if (!fact || fact.origin !== "plan")
3727
+ continue;
3728
+ if (fact.kind === "plan-requirement" && fact.entry && typeof fact.entry === "object") {
3729
+ const entry = fact.entry;
3730
+ if (typeof entry.id === "string" && entry.id.trim())
3731
+ requirements.set(entry.id, entry);
3732
+ }
3733
+ if (fact.kind === "plan-verification-target" && fact.entry && typeof fact.entry === "object") {
3734
+ const entry = fact.entry;
3735
+ const id = entry.id;
3736
+ if (typeof id === "string" && id.trim()) {
3737
+ verificationTargetIds.add(id);
3738
+ verificationTargetRequirements.set(id, new Set(Array.isArray(entry.requirementIds)
3739
+ ? entry.requirementIds.filter((value) => typeof value === "string")
3740
+ : []));
3741
+ }
3742
+ }
3743
+ if (fact.kind === "plan-evidence-gap" && fact.entry && typeof fact.entry === "object") {
3744
+ const entry = fact.entry;
3745
+ const requirementId = entry.requirementId;
3746
+ const description = entry.description;
3747
+ if (typeof requirementId === "string" && requirementId.trim() && typeof description === "string" && description.trim()) {
3748
+ standaloneEvidenceGaps.add(requirementId);
3749
+ }
3750
+ }
3751
+ }
3752
+ const missing = [];
3753
+ for (const id of input.requirementIds) {
3754
+ const entry = requirements.get(id);
3755
+ if (!entry) {
3756
+ missing.push({
3757
+ kind: "plan-requirement",
3758
+ id,
3759
+ requirementIds: [id],
3760
+ reason: `requirement ${id} has no committed plan-requirement fact`,
3761
+ });
3762
+ continue;
3763
+ }
3764
+ const targetIds = Array.isArray(entry.verificationTargetIds)
3765
+ ? entry.verificationTargetIds.filter((value) => typeof value === "string" && value.trim().length > 0)
3766
+ : [];
3767
+ const gap = entry.evidenceGap && typeof entry.evidenceGap === "object"
3768
+ ? entry.evidenceGap
3769
+ : undefined;
3770
+ const hasEvidenceGap = (typeof gap?.description === "string" && gap.description.trim().length > 0) ||
3771
+ standaloneEvidenceGaps.has(id);
3772
+ if (targetIds.length === 0 && !hasEvidenceGap) {
3773
+ missing.push({
3774
+ kind: "plan-verification-target",
3775
+ requirementIds: [id],
3776
+ reason: `requirement ${id} declares neither a verification target nor a non-empty evidenceGap`,
3777
+ });
3778
+ continue;
3779
+ }
3780
+ for (const targetId of targetIds) {
3781
+ if (!verificationTargetIds.has(targetId) ||
3782
+ !verificationTargetRequirements.get(targetId)?.has(id)) {
3783
+ missing.push({
3784
+ kind: "plan-verification-target",
3785
+ id: targetId,
3786
+ requirementIds: [id],
3787
+ reason: `requirement ${id} references verification target ${targetId}, but that target is not committed`,
3788
+ });
3789
+ }
3790
+ }
3791
+ }
3792
+ return missing;
3793
+ }
3794
+ /** Completeness checks for phases whose facts are committed incrementally. */
3795
+ export function collectFrontendPlanPhaseMissingFacts(input) {
3796
+ const facts = input.committedFacts
3797
+ .map(committedFactFromPlanRecord)
3798
+ .filter((fact) => Boolean(fact && fact.origin === "plan"));
3799
+ if (input.phase === "ux-registry") {
3800
+ return facts.some((fact) => fact.kind === "state-registry")
3801
+ ? []
3802
+ : [
3803
+ {
3804
+ kind: "state-registry",
3805
+ requirementIds: [...input.requirementIds],
3806
+ reason: "global UX vocabulary phase has no committed state-registry fact",
3807
+ },
3808
+ ];
3809
+ }
3810
+ if (input.phase === "ux-local") {
3811
+ const needsUx = input.requirementIds.some((id) => input.behaviorRequiredRequirementIds?.includes(id));
3812
+ if (!needsUx)
3813
+ return [];
3814
+ const requirementSlice = new Set(input.requirementIds);
3815
+ const scopedFacts = facts.filter((fact) => planFactScopeIntersects(fact, requirementSlice));
3816
+ const hasChoice = scopedFacts.some((fact) => fact.kind === "component-choice" &&
3817
+ Array.isArray(fact.uiComponentChoices) &&
3818
+ fact.uiComponentChoices.length > 0);
3819
+ const canonicalStateFlow = collectCanonicalStateFlowNames(scopedFacts);
3820
+ const hasStateFlow = canonicalStateFlow.uiStateNames.size > 0 ||
3821
+ canonicalStateFlow.interactionNames.size > 0;
3822
+ const missing = [];
3823
+ if (!hasChoice) {
3824
+ missing.push({
3825
+ kind: "component-choice",
3826
+ requirementIds: [...input.requirementIds],
3827
+ reason: "behaviour-required UX slice has no committed component-choice fact",
3828
+ });
3829
+ }
3830
+ if (!hasStateFlow) {
3831
+ missing.push({
3832
+ kind: "state-flow",
3833
+ requirementIds: [...input.requirementIds],
3834
+ reason: "behaviour-required UX slice has no committed state-flow fact",
3835
+ });
3836
+ }
3837
+ return missing;
3838
+ }
3839
+ const hasMockApi = facts.some((fact) => fact.kind === "mock-api");
3840
+ const allInteractions = collectCanonicalStateFlowNames(input.committedFacts).interactionNames;
3841
+ const liveInteractions = new Set([...collectCanonicalStateFlowNames(facts.filter(f => !planFactStringList(f.scopeRequirementIds).length || planFactScopeIntersects(f, new Set(input.requirementIds)))).interactionNames].filter(name => allInteractions.has(name)));
3842
+ const coveredInteractions = new Set(facts
3843
+ .filter((fact) => fact.kind === "data-flow")
3844
+ .flatMap((fact) => planFactStringList(fact.interactions)));
3845
+ const missing = [];
3846
+ if (!hasMockApi) {
3847
+ missing.push({
3848
+ kind: "mock-api",
3849
+ requirementIds: [...input.requirementIds],
3850
+ reason: "global Mock/data phase has no committed mock-api fact",
3851
+ });
3852
+ }
3853
+ for (const interaction of liveInteractions) {
3854
+ if (coveredInteractions.has(interaction))
3855
+ continue;
3856
+ missing.push({
3857
+ kind: "data-flow",
3858
+ id: interaction,
3859
+ requirementIds: [...input.requirementIds],
3860
+ reason: `interaction ${interaction} has no committed data-flow fact`,
3861
+ });
3862
+ }
3863
+ return missing;
3864
+ }
3828
3865
  /** Estimate calls conservatively: requirement + one VT, with a second VT
3829
3866
  * reserved for behaviour-required requirements. Explicit declarations win. */
3830
3867
  export function estimateFrontendPlanRequirementRecordCalls(fact) {
@@ -3866,8 +3903,9 @@ export function batchFrontendPlanRequirements(input) {
3866
3903
  }
3867
3904
  const FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS = 10;
3868
3905
  const FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY = 4;
3869
- // A single registry owns global names; large UX work uses bounded serial
3870
- // scopes against that registry. Keep a safety bound for adaptive retries
3906
+ // A large requirement set creates parallel coverage shards; UX remains one
3907
+ // global decision session so behavior names and component/state facts are not
3908
+ // reinvented per AC batch. Keep a safety bound for adaptive coverage retries
3871
3909
  // without letting the old 32-session ceiling skip finalize.
3872
3910
  const FRONTEND_PLAN_BATCH_MAX_SESSIONS = 128;
3873
3911
  const FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS = 8;
@@ -4008,28 +4046,26 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
4008
4046
  if (!value || typeof value !== "object" || Array.isArray(value))
4009
4047
  return [];
4010
4048
  const target = value;
4011
- if (options.verificationTargetIds && !options.verificationTargetIds.includes(String(target.id)))
4012
- return [];
4013
- const targetRequirementIds = planFactStringList(target.requirementIds);
4049
+ const targetRequirementIds = compactPromptStringArray(target.requirementIds, 20, 80);
4014
4050
  const relatedRequirementIds = targetRequirementIds.filter((id) => sliceSet.has(id));
4015
4051
  if (relatedRequirementIds.length === 0)
4016
4052
  return [];
4017
4053
  return [
4018
4054
  {
4019
- ...(typeof target.id === "string"
4020
- ? { id: target.id }
4055
+ ...(compactPromptString(target.id, 80)
4056
+ ? { id: compactPromptString(target.id, 80) }
4021
4057
  : {}),
4022
- ...(typeof target.commandId === "string"
4023
- ? { commandId: target.commandId }
4058
+ ...(compactPromptString(target.commandId, 80)
4059
+ ? { commandId: compactPromptString(target.commandId, 80) }
4024
4060
  : {}),
4025
4061
  ...(compactPromptString(target.commandLabel, 180)
4026
4062
  ? { commandLabel: compactPromptString(target.commandLabel, 180) }
4027
4063
  : {}),
4028
- ...(typeof target.file === "string"
4029
- ? { file: target.file }
4064
+ ...(compactPromptString(target.file, 180)
4065
+ ? { file: compactPromptString(target.file, 180) }
4030
4066
  : {}),
4031
4067
  requirementIds: relatedRequirementIds,
4032
- uiStates: planFactStringList(target.uiStates),
4068
+ uiStates: compactPromptStringArray(target.uiStates, 12, 100),
4033
4069
  },
4034
4070
  ];
4035
4071
  })
@@ -4081,7 +4117,7 @@ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, require
4081
4117
  if (!value || typeof value !== "object" || Array.isArray(value))
4082
4118
  return [];
4083
4119
  const state = value;
4084
- const id = typeof state.id === "string" ? state.id : undefined;
4120
+ const id = compactPromptString(state.id, 80);
4085
4121
  if (!id)
4086
4122
  return [];
4087
4123
  return [{
@@ -4181,9 +4217,9 @@ function compactFrontendPlanLedgerContext(input) {
4181
4217
  compactFacts.push({
4182
4218
  kind: fact.kind,
4183
4219
  entry: {
4184
- id: entry.id,
4185
- implementationTargets: planFactStringList(entry.implementationTargets).slice(0, 12),
4186
- verificationTargetIds: planFactStringList(entry.verificationTargetIds).slice(0, 12),
4220
+ id: compactPromptString(entry.id, 80),
4221
+ implementationTargets: compactPromptStringArray(entry.implementationTargets, 12, 180),
4222
+ verificationTargetIds: compactPromptStringArray(entry.verificationTargetIds, 12, 80),
4187
4223
  ...(compactPromptString(entry.expectedOutcome, 240)
4188
4224
  ? { expectedOutcome: compactPromptString(entry.expectedOutcome, 240) }
4189
4225
  : {}),
@@ -4197,19 +4233,17 @@ function compactFrontendPlanLedgerContext(input) {
4197
4233
  if (fact.kind === "plan-verification-target") {
4198
4234
  if (!entry)
4199
4235
  continue;
4200
- // Scope against the complete canonical binding before compacting.
4201
- // A shared target may bind a late requirement beyond the preview cap.
4202
- const requirementIds = planFactStringList(entry.requirementIds).filter((id) => slice.has(id));
4203
- if (requirementIds.length === 0)
4236
+ const requirementIds = compactPromptStringArray(entry.requirementIds, 20, 80);
4237
+ if (!requirementIds.some((id) => slice.has(id)))
4204
4238
  continue;
4205
4239
  compactFacts.push({
4206
4240
  kind: fact.kind,
4207
4241
  entry: {
4208
- id: entry.id,
4209
- commandId: entry.commandId,
4210
- file: entry.file,
4242
+ id: compactPromptString(entry.id, 80),
4243
+ commandId: compactPromptString(entry.commandId, 80),
4244
+ file: compactPromptString(entry.file, 180),
4211
4245
  requirementIds,
4212
- uiStates: planFactStringList(entry.uiStates).slice(0, 12),
4246
+ uiStates: compactPromptStringArray(entry.uiStates, 12, 100),
4213
4247
  },
4214
4248
  });
4215
4249
  continue;
@@ -4236,8 +4270,8 @@ function compactFrontendPlanLedgerContext(input) {
4236
4270
  if (fact.kind === "state-registry") {
4237
4271
  compactFacts.push({
4238
4272
  kind: fact.kind,
4239
- uiStateNames: planFactStringList(fact.uiStateNames).slice(0, 40),
4240
- interactionNames: planFactStringList(fact.interactionNames).slice(0, 60),
4273
+ uiStateNames: compactPromptStringArray(fact.uiStateNames, 40, 100),
4274
+ interactionNames: compactPromptStringArray(fact.interactionNames, 60, 120),
4241
4275
  });
4242
4276
  continue;
4243
4277
  }
@@ -4289,21 +4323,13 @@ function compactFrontendPlanLedgerContext(input) {
4289
4323
  return "";
4290
4324
  const priorityFacts = compactFacts.filter((fact) => fact.kind === "plan-requirement" || fact.kind === "plan-verification-target");
4291
4325
  const otherFacts = compactFacts.filter((fact) => fact.kind !== "plan-requirement" && fact.kind !== "plan-verification-target");
4292
- let previewBytes = 0;
4293
4326
  const boundedFacts = [
4294
- ...priorityFacts.slice(0, 64),
4295
- ...otherFacts.slice(-32),
4296
- ].slice(0, 96).filter(fact => {
4297
- const bytes = Buffer.byteLength(JSON.stringify(fact));
4298
- if (previewBytes + bytes > 24_000)
4299
- return false;
4300
- previewBytes += bytes;
4301
- return true;
4302
- });
4327
+ ...priorityFacts,
4328
+ ...otherFacts,
4329
+ ];
4303
4330
  return [
4304
4331
  "<frontend_plan_ledger>",
4305
- "Preview of committed plan facts; not a replacement payload. Fields and lists may be omitted or shortened. Use read_plan_facts with kind, entryId/eventId and optional field to retrieve complete values in bounded pages before corrections; preserve all other bindings. Absence from this preview is not absence from the ledger.",
4306
- JSON.stringify({ complete: false, matchingFacts: compactFacts.length, omittedFacts: compactFacts.length - boundedFacts.length }),
4332
+ "Committed plan facts from earlier sessions. Treat these as authoritative; correct them only with the allowed replacement/removal fields.",
4307
4333
  JSON.stringify(boundedFacts),
4308
4334
  "</frontend_plan_ledger>",
4309
4335
  ].join("\n");
@@ -4337,14 +4363,12 @@ export async function runFrontendReviewSegmentedSessions(input) {
4337
4363
  index--;
4338
4364
  continue;
4339
4365
  }
4340
- const completionInstruction = `${item.repairs ? "REPAIR: the prior session did not commit all required checkpoints/verdict. Do not repeat the review narrative. " : ""}Complete these exact runtime scope IDs using complete_review_scope: ${JSON.stringify(scopes.map(scope => scope.id))}. These are scope IDs, not node IDs. ${finalScope ? `After those checkpoints succeed, call exactly one terminal tool: ${terminalKinds.join(" or ")}. A prose conclusion is not a committed verdict.` : "More scopes remain; do not submit an overall verdict yet."}`;
4341
- const userMessage = `${input.sessionOptions.userMessage}\n\n${completionInstruction}`;
4342
4366
  if (++calls > 128)
4343
4367
  return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_RECOVERY_EXHAUSTED: session quota reached" };
4344
- last = await observeFrontendSession({ ...input.observation, phase: `${input.phase}/scope`, scopeIds: scopes.map(s => s.id), prompt, userMessage, customTools,
4368
+ last = await observeFrontendSession({ ...input.observation, phase: `${input.phase}/scope`, scopeIds: scopes.map(s => s.id), prompt, userMessage: input.sessionOptions.userMessage, customTools,
4345
4369
  artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${calls}.json` : undefined,
4346
4370
  committedCount: () => protocol.committedFacts().length, durableCommittedCount: () => protocol.committedFacts().length,
4347
- }, observer => input.piStepFn({ ...input.sessionOptions, prompt, userMessage, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
4371
+ }, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
4348
4372
  await input.tools.flush();
4349
4373
  await input.inventory.validate();
4350
4374
  if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
@@ -4533,18 +4557,16 @@ function buildFrontendPlanWorkload(input) {
4533
4557
  const workCost = (ids) => 1 + ids.reduce((total, id) => total + Math.max(1, (input.requirementCosts?.get(id) ?? 2) - 1), 0);
4534
4558
  const policy = input.sessionOptions.frontendExecutionPolicy;
4535
4559
  const targetBytes = policy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
4560
+ const userMessageBytes = Buffer.byteLength(input.sessionOptions.userMessage ?? "");
4536
4561
  const buildWorkBatches = (ids) => {
4537
4562
  const work = workGroups.flatMap((g, index) => {
4538
4563
  const members = g.requirementIds.filter(id => ids.includes(id));
4539
4564
  return members.length ? [{ id: `${index}:${g.id}`, requirementIds: members,
4540
4565
  requirements: members.map(id => fullUnits.get(id) ?? { id }), estimatedCalls: workCost(members) }] : [];
4541
4566
  });
4542
- // Pack complete work units, not the repeated request scaffold. Deducting
4543
- // fixed context/tools can leave a one-byte budget and force one AC per
4544
- // session without reducing that overhead. The SDK checks the actual
4545
- // request against model capacity; capacity recovery splits unfinished work.
4567
+ const scaffoldBytes = Buffer.byteLength(input.basePrompt.replace(/<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/, JSON.stringify({ ...compiledInput, requirements: [] }))) + userMessageBytes + Buffer.byteLength(JSON.stringify(input.coverageTools));
4546
4568
  return packFrontendInputUnits(work, {
4547
- targetBytes,
4569
+ targetBytes: Math.max(1, targetBytes - scaffoldBytes),
4548
4570
  maxUnits: policy?.maxScopeUnits ?? 4,
4549
4571
  maxCost: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS, cost: g => g.estimatedCalls,
4550
4572
  }).map(batch => batch.flatMap(g => g.requirementIds));
@@ -4556,6 +4578,7 @@ function buildFrontendPlanWorkload(input) {
4556
4578
  frozenInput, workGroups, buildWorkBatches,
4557
4579
  compactEligible: input.requirementCosts !== undefined &&
4558
4580
  workGroups.length <= FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS &&
4581
+ Buffer.byteLength(input.basePrompt) + userMessageBytes + Buffer.byteLength(JSON.stringify(input.allTools)) <= targetBytes * 2 &&
4559
4582
  workGroups.reduce((total, group) => total + workCost(group.requirementIds), 0) <= FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS &&
4560
4583
  countFrontendPlanTargetSurfaces(input.basePrompt) === 1,
4561
4584
  };
@@ -4628,7 +4651,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
4628
4651
  const buildPhasePrompt = (segment, missing = [], scopeIds = allRequirementIds) => {
4629
4652
  const compact = allRequirementIds.length > 0
4630
4653
  ? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, scopeIds, {
4631
- includeRequirementText: segment.id === "global-mock-data" || segment.id === "finalize",
4654
+ includeRequirementText: segment.id === "global-mock-data",
4632
4655
  includeVerificationTargets: false,
4633
4656
  includeDesignEvidence: segment.id === "global-dependency-deviation" || segment.id === "finalize",
4634
4657
  includeChecklist: false,
@@ -4684,7 +4707,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
4684
4707
  "component-choice",
4685
4708
  "state-flow",
4686
4709
  ],
4687
- // Include shared state bindings from other scopes; page full values before correcting.
4710
+ scopedKinds: ["state-flow"],
4688
4711
  })
4689
4712
  : "";
4690
4713
  return [
@@ -4807,6 +4830,8 @@ export async function runFrontendPlanSegmentedSessions(input) {
4807
4830
  const { workGroups, buildWorkBatches, compactEligible } = buildFrontendPlanWorkload({
4808
4831
  basePrompt: input.basePrompt, requirementIds: allRequirementIds,
4809
4832
  requirementCosts: input.requirementCosts, sessionOptions: input.sessionOptions,
4833
+ coverageTools: input.segmentCustomTools(coverageSegment.toolNames),
4834
+ allTools: input.segmentCustomTools(null),
4810
4835
  });
4811
4836
  // Small, single-surface requests do not benefit from six isolated Pi
4812
4837
  // sessions. Keep the typed ledger as the authority, but let one local
@@ -4901,27 +4926,6 @@ export async function runFrontendPlanSegmentedSessions(input) {
4901
4926
  let lastDurableCount = input.committedFactCount();
4902
4927
  while (index < queue.length) {
4903
4928
  const session = queue[index];
4904
- if (input.attempt > 1 && input.committedFacts && session.id === "ux-registry" &&
4905
- collectFrontendPlanPhaseMissingFacts({ phase: "ux-registry", requirementIds: allRequirementIds, committedFacts: input.committedFacts() }).length === 0) {
4906
- index += 1;
4907
- continue;
4908
- }
4909
- // Reuse complete UX scopes on node retry. Require explicit per-requirement
4910
- // ownership; an unrelated/global fact must not prove a slice complete.
4911
- if (input.attempt > 1 && input.committedFacts && session.id.startsWith("ux-local-") && session.requirementSlice?.length) {
4912
- const facts = input.committedFacts();
4913
- const ownedIds = new Set(facts.flatMap(value => {
4914
- const fact = committedFactFromPlanRecord(value);
4915
- return fact ? planFactStringList(fact.scopeRequirementIds) : [];
4916
- }));
4917
- const complete = session.requirementSlice.every(id => ownedIds.has(id)) &&
4918
- collectFrontendPlanPhaseMissingFacts({ phase: "ux-local", requirementIds: session.requirementSlice,
4919
- committedFacts: facts, behaviorRequiredRequirementIds: input.behaviorRequiredRequirementIds }).length === 0;
4920
- if (complete) {
4921
- index += 1;
4922
- continue;
4923
- }
4924
- }
4925
4929
  const remaining = (session.coverageOnly ? session.coverageSlice ?? [] : []).filter((id) => !input.committedRequirementIds?.().has(id));
4926
4930
  const preexistingMissing = session.coverageOnly && session.coverageSlice && input.committedFacts
4927
4931
  ? collectFrontendPlanMissingFacts({
@@ -4967,30 +4971,23 @@ export async function runFrontendPlanSegmentedSessions(input) {
4967
4971
  : buildPhasePrompt(segment, session.missingFacts, session.requirementSlice ?? allRequirementIds);
4968
4972
  }
4969
4973
  }
4970
- const atomicFocus = session.atomicRecovery ? session.missingFacts?.[0] : undefined;
4971
- if (atomicFocus) {
4972
- prompt = [
4973
- compactFrontendPlanPromptForRequirementSlice(input.basePrompt, atomicFocus.requirementIds, {
4974
- includeChecklist: false,
4975
- ...(atomicFocus.kind === "plan-verification-target" && atomicFocus.id
4976
- ? { verificationTargetIds: [atomicFocus.id] } : {}),
4977
- }),
4978
- `ATOMIC FACT: ${atomicFocus.kind}${atomicFocus.id ? ` ${atomicFocus.id}` : ""}`,
4979
- atomicFocus.reason,
4980
- "Commit ONLY this missing fact with one record_* call, then end this session. Other facts are queued separately. Preserve canonical IDs and shared bindings; use read_plan_facts for existing values. Do not summarize or plan the entire requirement. Capacity exhaustion is not evidence of a requirement gap.",
4981
- ].join("\n\n");
4982
- }
4983
4974
  const committedBefore = input.committedFactCount();
4984
4975
  input.setActiveRequirementScope?.(session.coverageOnly ? session.coverageSlice ?? [] : session.requirementSlice ?? []);
4985
4976
  if (invocationCount >= FRONTEND_PLAN_BATCH_MAX_SESSIONS)
4986
4977
  break;
4987
4978
  invocationCount += 1;
4988
- const atomicTools = {
4989
- "plan-requirement": "record_plan_requirement", "plan-verification-target": "record_plan_verification_target",
4990
- "state-registry": "record_state_registry", "component-choice": "record_component_choice",
4991
- "state-flow": "record_state_flow", "data-flow": "record_data_flow", "mock-api": "record_mock_api",
4992
- };
4993
- const customTools = input.segmentCustomTools(atomicFocus ? new Set([atomicTools[atomicFocus.kind]]) : session.toolNames);
4979
+ const customTools = input.segmentCustomTools(session.toolNames);
4980
+ const scopeForPacking = session.coverageSlice ?? session.requirementSlice;
4981
+ const envelopeBytes = Buffer.byteLength(prompt) + Buffer.byteLength(input.sessionOptions.userMessage ?? "") + Buffer.byteLength(JSON.stringify(customTools));
4982
+ if (envelopeBytes > (input.sessionOptions.frontendExecutionPolicy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES) && scopeForPacking && session.id !== "compact-local") {
4983
+ const groups = workGroups.map(g => g.requirementIds.filter(id => scopeForPacking.includes(id))).filter(g => g.length);
4984
+ if (groups.length > 1) {
4985
+ const half = Math.ceil(groups.length / 2);
4986
+ queue.splice(index, 1, ...[groups.slice(0, half).flat(), groups.slice(half).flat()].map(slice => ({ ...session, ...(session.coverageOnly ? { coverageSlice: slice } : { requirementSlice: slice }) })));
4987
+ invocationCount -= 1;
4988
+ continue;
4989
+ }
4990
+ }
4994
4991
  const result = await observeFrontendSession({
4995
4992
  ...input.observation, phase: `plan/${session.id}`, scopeIds: session.requirementSlice ?? session.coverageSlice ?? allRequirementIds,
4996
4993
  prompt, userMessage: input.sessionOptions.userMessage, customTools, committedCount: input.committedFactCount, durableCommittedCount: () => lastDurableCount,
@@ -5108,47 +5105,9 @@ export async function runFrontendPlanSegmentedSessions(input) {
5108
5105
  : isUxLocalSession
5109
5106
  ? buildUxPrompt(session.requirementSlice ?? [], missing)
5110
5107
  : buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
5111
- const recovery = classifyFrontendPlanRecovery({ ...result, stopReason: readWriterThinkingExhaustionEvidence(result).stopReason });
5112
- if (recovery === "stop")
5108
+ if (result.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(result.failureCategory))
5113
5109
  return { ...result, ok: false };
5114
- if ((recovery === "output" || (session.atomicRecovery && result.ok)) && input.committedFacts &&
5115
- !(missingPhaseFacts.length === 1 && missingPhaseFacts[0].kind === "plan-requirement") &&
5116
- (session.coverageOnly || isUxRegistrySession || isUxLocalSession || session.id.startsWith("global-mock-data"))) {
5117
- if (missingPhaseFacts.length === 0) {
5118
- // A complete validated slice does not need a successful prose turn.
5119
- // This never accepts finalize or bypasses the final contract validator.
5120
- accumulated = { ...accumulated, ok: true, failureCategory: "success" };
5121
- index += 1;
5122
- continue;
5123
- }
5124
- const missingIds = [...new Set(missingPhaseFacts.flatMap(item => item.requirementIds))];
5125
- const scope = session.coverageSlice ?? session.requirementSlice;
5126
- if (scope && missingIds.length < scope.length) {
5127
- queue[index] = { ...session, missingFacts: missingPhaseFacts,
5128
- ...(session.coverageOnly ? { coverageSlice: missingIds } : { requirementSlice: missingIds }),
5129
- retryCount: 0 };
5130
- continue;
5131
- }
5132
- if (scope && missingIds.length > 1 && (session.coverageOnly || isUxLocalSession)) {
5133
- const half = Math.ceil(missingIds.length / 2);
5134
- queue.splice(index, 1, ...[missingIds.slice(0, half), missingIds.slice(half)].map(ids => ({
5135
- ...session, retryCount: 0,
5136
- ...(session.coverageOnly ? { coverageSlice: ids } : { requirementSlice: ids }),
5137
- missingFacts: missingPhaseFacts.filter(item => item.requirementIds.some(id => ids.includes(id))),
5138
- })));
5139
- continue;
5140
- }
5141
- const focusStillMissing = atomicFocus && missingPhaseFacts.some(item => item.kind === atomicFocus.kind && item.id === atomicFocus.id &&
5142
- item.requirementIds.join("\0") === atomicFocus.requirementIds.join("\0"));
5143
- const retries = focusStillMissing ? (session.retryCount ?? 0) + 1 : 0;
5144
- if (retries < 2) {
5145
- queue[index] = { ...session, atomicRecovery: true, missingFacts: missingPhaseFacts, retryCount: retries };
5146
- continue;
5147
- }
5148
- return { ...accumulated, ok: false, failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
5149
- stderr: `${accumulated.stderr}\nfrontend plan capacity recovery exhausted: preserve committed facts; phase=${session.id}; scope=${missingIds.join(",")}; strategy=atomic-fact; missing=${JSON.stringify(missingPhaseFacts)}`.trim() };
5150
- }
5151
- const capacityExhausted = recovery === "output" || recovery === "context";
5110
+ const capacityExhausted = readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory);
5152
5111
  if (capacityExhausted && !result.timedOut) {
5153
5112
  const failure = () => mapPlannerExhaustion({ ...result, ok: false, stderr: `${result.stderr}\nFRONTEND_INPUT_UNIT_TOO_LARGE: ${session.id}; no smaller complete scope can finish; unchanged retries are disabled` }, committedAfter > committedBefore);
5154
5113
  const missingCoverageIds = input.committedFacts ? [...new Set(collectFrontendPlanMissingFacts({ requirementIds: session.coverageSlice ?? session.requirementSlice ?? allRequirementIds, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds))] : [...(session.coverageSlice ?? session.requirementSlice ?? allRequirementIds)];
@@ -5254,7 +5213,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
5254
5213
  : undefined;
5255
5214
  const zeroProgressBurn = coverageSlice !== undefined &&
5256
5215
  coverageSlice.length > 1 &&
5257
- (capacityExhausted || (["empty-output", "unknown"].includes(result.failureCategory) && committedAfter === committedBefore && !(result.assistantText ?? "").trim() && !result.stderr.trim())) &&
5216
+ (readWriterThinkingExhaustionEvidence(result).stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory) || (committedAfter === committedBefore && !(result.assistantText ?? "").trim() && !result.stderr.trim())) &&
5258
5217
  !result.timedOut;
5259
5218
  if (zeroProgressBurn && coverageSlice) {
5260
5219
  const missingIds = input.committedFacts ? new Set(collectFrontendPlanMissingFacts({ requirementIds: coverageSlice, committedFacts: input.committedFacts() }).flatMap(f => f.requirementIds)) : undefined;
@@ -5974,7 +5933,6 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
5974
5933
  frontendExecutionPolicy: meta.spec?.frontendExecutionPolicy,
5975
5934
  attachedFiles: [],
5976
5935
  modelConfig,
5977
- outputLimitRecovery: { terminalToolNames: ["finalize_plan", "finalize_contract", "approve_design", "request_design_changes", "approve_review", "request_review_changes"] },
5978
5936
  repoRoot: input.cwd,
5979
5937
  step,
5980
5938
  toolNames,
@@ -6093,13 +6051,16 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
6093
6051
  ? options.ledgerTools.customTools
6094
6052
  : options.ledgerTools.customTools.filter((tool) => typeof tool === "object" &&
6095
6053
  tool !== null &&
6096
- (toolNames.has(tool.name) || tool.name === "read_plan_facts")),
6054
+ toolNames.has(tool.name)),
6097
6055
  flushLedger: () => options.ledgerTools.flush(),
6098
6056
  });
6099
6057
  if (planRequirementIds.length > 1) {
6058
+ const coverageTools = FRONTEND_PLAN_SEGMENTS.find(segment => segment.id === "coverage").toolNames;
6100
6059
  const workload = buildFrontendPlanWorkload({
6101
6060
  basePrompt: input.prompt, requirementIds: planRequirementIds,
6102
6061
  requirementCosts: planRequirementCosts, sessionOptions: piSessionOptions,
6062
+ allTools: planLedgerTools.customTools,
6063
+ coverageTools: planLedgerTools.customTools.filter(tool => isRecordObject(tool) && coverageTools.has(String(tool.name))),
6103
6064
  });
6104
6065
  let coverageBatches;
6105
6066
  try {