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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CHANGELOG.md +111 -45
  2. package/dist/application/dag/run-dag.js +8 -2
  3. package/dist/application/evaluation/budget.js +19 -1
  4. package/dist/application/task-lifecycle/advance.js +26 -8
  5. package/dist/application/task-lifecycle/observe.js +43 -29
  6. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/program.js +1 -1
  9. package/dist/commands/dag-rerun-task.js +2 -0
  10. package/dist/commands/task-source-prepare.js +3 -1
  11. package/dist/executors/dag-pi-executor.js +2584 -600
  12. package/dist/executors/pi-executor.js +22 -1
  13. package/dist/executors/pi-extension-resolver.js +14 -2
  14. package/dist/executors/pi-sdk-executor.js +140 -39
  15. package/dist/executors/shell-executor.js +135 -55
  16. package/dist/shared/dag-failure-category.js +6 -0
  17. package/dist/shared/frontend-execution-policy.js +23 -0
  18. package/dist/task/config-types.js +4 -0
  19. package/dist/task/contract/apply.js +36 -2
  20. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  21. package/dist/task/source-prepare/ledger-review.js +6 -9
  22. package/dist/task/source-prepare/parse-intent.js +7 -0
  23. package/dist/task/source-prepare/semantic-intake.js +16 -26
  24. package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
  25. package/dist/task/source-references.js +48 -23
  26. package/dist/worker/console/chat/assistant-content.js +23 -2
  27. package/dist/worker/console/chat/browser-policy.js +143 -0
  28. package/dist/worker/console/chat/browser-routes.js +148 -0
  29. package/dist/worker/console/chat/chat-event-store.js +4 -2
  30. package/dist/worker/console/chat/explore-tools.js +13 -0
  31. package/dist/worker/console/chat/pi-runtime.js +173 -94
  32. package/dist/worker/console/chat/resource-loader.js +4 -1
  33. package/dist/worker/console/chat/routes.js +208 -66
  34. package/dist/worker/console/chat/scm-routes.js +217 -0
  35. package/dist/worker/console/chat/scm-service.js +283 -0
  36. package/dist/worker/console/chat/scm-tools.js +111 -0
  37. package/dist/worker/console/chat/sdd-data-alignment.js +78 -11
  38. package/dist/worker/console/chat/session-catalog.js +32 -0
  39. package/dist/worker/console/chat/session-mode-view.js +48 -0
  40. package/dist/worker/console/chat/session-mode.js +218 -0
  41. package/dist/worker/console/chat/session-store.js +27 -7
  42. package/dist/worker/console/chat/shortcuts.js +6 -0
  43. package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
  44. package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
  45. package/dist/worker/console/chat/subagents/index.js +5 -0
  46. package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
  47. package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
  48. package/dist/worker/console/chat/subagents/types.js +13 -0
  49. package/dist/worker/console/chat/terminal-routes.js +216 -0
  50. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  51. package/dist/worker/console/chat/terminal-tools.js +199 -0
  52. package/dist/worker/console/chat/tool-preview.js +21 -0
  53. package/dist/worker/console/chat/tools.js +13 -1
  54. package/dist/worker/console/chat/turn-process.js +1 -0
  55. package/dist/worker/console/interview/tools.js +1 -0
  56. package/dist/worker/console/server.js +2 -28
  57. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
  58. package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-VJWsxWhB.js} +1 -1
  59. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
  60. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
  61. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
  62. package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
  63. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
  73. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
  74. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
  75. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
  76. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
  77. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
  78. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
  81. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
  82. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
  83. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
  84. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
  85. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
  86. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
  87. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
  88. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
  89. package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
  90. package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
  91. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
  92. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
  93. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
  94. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
  95. package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-G1JR40SX.js} +1 -1
  96. package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-CCgPF0oA.js} +5 -5
  97. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
  98. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
  99. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
  100. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
  101. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
  102. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
  103. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
  104. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
  105. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
  106. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
  107. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
  108. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
  110. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
  111. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
  112. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
  113. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
  114. package/dist/worker/console/static/index.html +2 -2
  115. package/dist/worker/console/static-src/chat-view-types.js +1 -1
  116. package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
  117. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +142 -7
  118. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +44 -0
  119. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  120. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
  121. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  122. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
  123. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  124. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +33 -18
  125. package/dist/worker/console/static-src/operator-chat/useChatStream.js +48 -16
  126. package/dist/worker/console/static-src/operator-chat/useChatThread.js +170 -7
  127. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  128. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  129. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  130. package/dist/worker/console/workspace-context.js +15 -1
  131. package/dist/worker/observe/node-transparency.js +81 -72
  132. package/dist/worker/observe/routes.js +20 -1
  133. package/dist/worker/observe/static/constants.js +22 -22
  134. package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
  135. package/dist/worker/observe/static/dag-history-labels.js +1 -0
  136. package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
  137. package/dist/worker/observe/static/dag-inspector-humanize.js +342 -0
  138. package/dist/worker/observe/static/dag-node-purpose.js +10 -10
  139. package/dist/worker/observe/static/dom.js +20 -1
  140. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  141. package/dist/worker/observe/static/format-pool.js +6 -0
  142. package/dist/worker/observe/static/format.js +7 -0
  143. package/dist/worker/observe/static/index.html +4 -4
  144. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  145. package/dist/worker/observe/static/inspector-submission.js +32 -0
  146. package/dist/worker/observe/static/kpi.js +1 -0
  147. package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
  148. package/dist/worker/observe/static/relations.js +7 -5
  149. package/dist/worker/observe/static/router.js +13 -0
  150. package/dist/worker/observe/static/run-processing.js +2 -0
  151. package/dist/worker/observe/static/shell-chrome.js +36 -3
  152. package/dist/worker/observe/static/state.js +35 -2
  153. package/dist/worker/observe/static/styles.css +431 -39
  154. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  155. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  156. package/dist/worker/observe/static/task-history.js +12 -0
  157. package/dist/worker/observe/static/views/batch.js +6 -13
  158. package/dist/worker/observe/static/views/dag-graph.js +51 -4
  159. package/dist/worker/observe/static/views/dag-inspector.js +1069 -426
  160. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  161. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  162. package/dist/worker/observe/static/views/dag.js +64 -22
  163. package/dist/worker/observe/static/views/dags.js +2 -0
  164. package/dist/worker/observe/static/views/dashboard.js +21 -12
  165. package/dist/worker/observe/static/views/failures.js +21 -11
  166. package/dist/worker/observe/static/views/feature.js +11 -29
  167. package/dist/worker/observe/static/views/pool.js +37 -28
  168. package/dist/worker/observe/static/views/run.js +48 -5
  169. package/dist/worker/observe/static/views/session-timeline.js +194 -243
  170. package/dist/worker/observe/static/views/task.js +81 -62
  171. package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
  172. package/dist/workflows/dag/budget-enforcement.js +53 -3
  173. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  174. package/dist/workflows/dag/frontend-capacity.js +9 -0
  175. package/dist/workflows/dag/frontend-contract-facts.js +130 -0
  176. package/dist/workflows/dag/frontend-design-policy.js +103 -17
  177. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  178. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  179. package/dist/workflows/dag/frontend-implementation-contract.js +423 -41
  180. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  181. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  182. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  183. package/dist/workflows/dag/frontend-plan-render.js +21 -3
  184. package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
  185. package/dist/workflows/dag/frontend-recovery-controller.js +32 -8
  186. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  187. package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
  188. package/dist/workflows/dag/frontend-recovery-run.js +146 -16
  189. package/dist/workflows/dag/frontend-review-scopes.js +139 -0
  190. package/dist/workflows/dag/frontend-risk.js +2 -0
  191. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  192. package/dist/workflows/dag/frontend-shadow-dual-write.js +37 -3
  193. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  194. package/dist/workflows/dag/frontend-shape.js +46 -7
  195. package/dist/workflows/dag/frontend-test-execution-evidence.js +49 -0
  196. package/dist/workflows/dag/frontend-typed-event-store.js +14 -0
  197. package/dist/workflows/dag/frontend-verification-trace.js +38 -4
  198. package/dist/workflows/dag/frontend-writer-admission.js +2 -2
  199. package/dist/workflows/dag/init-hybrid.js +70 -35
  200. package/dist/workflows/dag/node-execution.js +225 -226
  201. package/dist/workflows/dag/prompt.js +4 -0
  202. package/dist/workflows/dag/recovery-lease.js +106 -16
  203. package/dist/workflows/dag/rerun-feedback.js +1 -0
  204. package/dist/workflows/dag/rerun-plan.js +96 -4
  205. package/dist/workflows/dag/rerun-run.js +28 -6
  206. package/dist/workflows/dag/rerun-task.js +221 -18
  207. package/dist/workflows/dag/retry-policy.js +27 -10
  208. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  209. package/dist/workflows/dag/runner.js +246 -19
  210. package/dist/workflows/dag/structured-output-repair.js +4 -1
  211. package/dist/workflows/dag/types.js +10 -3
  212. package/dist/workflows/dag/validate.js +10 -8
  213. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  214. package/docs/operations/README.md +2 -0
  215. package/docs/templates/agent-dag.schema.json +2 -2
  216. package/docs/templates/backend-test-dag.json +6 -4
  217. package/docs/templates/frontend-design-contract.md +4 -4
  218. package/docs/templates/frontend-implementation-contract.schema.json +68 -4
  219. package/docs/templates/frontend-implementation-dag.json +5 -5
  220. package/harness.json +1 -1
  221. package/package.json +8 -6
  222. package/skills/frontend-contract/SKILL.md +2 -1
  223. package/skills/frontend-contract/references/contract-protocol.md +19 -3
  224. package/skills/frontend-design-review/SKILL.md +12 -11
  225. package/skills/frontend-plan/SKILL.md +22 -2
  226. package/skills/frontend-plan/references/decision-contract.md +114 -5
  227. package/skills/frontend-plan/references/design-decisions.md +32 -0
  228. package/skills/frontend-review/SKILL.md +10 -11
  229. package/skills/frontend-scout/references/scout-evidence.md +4 -0
  230. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
  231. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
  232. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
  233. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
  234. package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
  235. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
  236. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
@@ -1,3 +1,5 @@
1
+ import { collectFrontendExecutionGroups } from "./frontend-execution-groups.js";
2
+ import { reserveDagProviderRequest } from "./budget-enforcement.js";
1
3
  import { createHash } from "node:crypto";
2
4
  import { existsSync } from "node:fs";
3
5
  import { readFile } from "node:fs/promises";
@@ -13,7 +15,7 @@ import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./p
13
15
  import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
14
16
  import { materializeDeclaredArtifactFacts } from "./artifact-bindings.js";
15
17
  import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
16
- import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, projectFrontendNodeProtocolFailureReason, resolveFrontendPlanBackupRoute, resolveFrontendPlanRetryStep, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
18
+ import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, OUTPUT_LIMIT_RETRY_CATEGORY, projectFrontendNodeProtocolFailureReason, resolveFrontendPlanBackupRoute, resolveFrontendPlanRetryStep, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
17
19
  import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
18
20
  import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
19
21
  import { getStructuredContractValidator } from "./contract-output-registry.js";
@@ -21,6 +23,7 @@ import "./contract-validator-registrations.js";
21
23
  import { computeNormalizedFailureFingerprint } from "./frontend-recovery-lineage.js";
22
24
  import { parseLedgerJson } from "../../task/source-prepare/ledger.js";
23
25
  import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
26
+ import { collectCanonicalStateFlowNames, extractRequiredDeliverablePaths, resolveFrontendContractRequirements, } from "./frontend-contract-facts.js";
24
27
  import { allowedRepairReadPaths, auditRepairAttemptToolUse, buildStructuredOutputRepairPrompt, freezeStructuredOutputRepairContext, hasNonEmptyStructuredCandidate, isFrontendStructuredRepairSchemaId, isStructuredRepairableFailureCategory, persistStructuredAttemptRaw, sessionEventsByteLength, GOVERNANCE_BLOCKED_CATEGORY, STRUCTURED_REPAIR_EXHAUSTED_CATEGORY, } from "./structured-output-repair.js";
25
28
  import { readFrontendCanonicalCandidate } from "./frontend-implementation-contract.js";
26
29
  import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
@@ -256,6 +259,9 @@ function frontendPlanValidationRetryGuidance(reason) {
256
259
  if (/verificationTargets.*uiStates|uiStates.*verificationTargets/i.test(reason)) {
257
260
  guidance.push("For this retry, provide verificationTargets[].uiStates as an array; use [] when the target has no named UI state.");
258
261
  }
262
+ if (/duplicate|already recorded/i.test(reason)) {
263
+ guidance.push("For this retry, re-commit the corrected entry with the same id and replace=true; the ledger compiles the latest submission as the full replacement. A duplicate receipt is a correction invitation, not a prohibition, and backfilling a cited fact's reverse reference in-node does not re-derive committed facts.");
264
+ }
259
265
  return guidance;
260
266
  }
261
267
  function compactRetryText(text, maxChars) {
@@ -293,6 +299,12 @@ export function buildContextOverflowRetryPrompt(task, basePrompt) {
293
299
  compactRetrySection(basePrompt, "task", COMPACT_RETRY_TASK_MAX_CHARS),
294
300
  "</task>",
295
301
  ].join("\n"),
302
+ // Plan still needs the complete frozen obligations for ledger ownership
303
+ // and scoped projection. Its executor packs this block before dispatch;
304
+ // dropping or character-truncating it prevents safe progress recovery.
305
+ ...(isFrontendPlanLadderTask(task)
306
+ ? [basePrompt.match(/<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/)?.[0] ?? ""]
307
+ : []),
296
308
  ].join("\n\n");
297
309
  }
298
310
  function isFrontendPlanLadderTask(task) {
@@ -325,6 +337,8 @@ const FRONTEND_CONTRACT_RECORD_TOOL_NAMES_LOCAL = new Set([
325
337
  "record_handoff_intent",
326
338
  "record_open_question",
327
339
  "record_split_proposal",
340
+ "record_ui_state",
341
+ "record_required_deliverables",
328
342
  ]);
329
343
  async function countContractRecordSubmissions(runDir, nodeId) {
330
344
  const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
@@ -374,27 +388,14 @@ async function readFrontendPlanCommittedSnapshot(runDir, nodeId) {
374
388
  function planInputRecord(value) {
375
389
  return typeof value === "object" && value !== null && !Array.isArray(value);
376
390
  }
377
- function planInputText(value, maxChars = 240) {
378
- if (typeof value !== "string" || value.trim().length === 0)
379
- return undefined;
380
- const normalized = value.trim();
381
- return normalized.length <= maxChars
382
- ? normalized
383
- : `${normalized.slice(0, maxChars - 1)}…`;
391
+ function planInputText(value, _maxChars) {
392
+ return typeof value === "string" && value.trim().length ? value.trim() : undefined;
384
393
  }
385
394
  function planInputStrings(value) {
386
395
  return Array.isArray(value)
387
396
  ? value.filter((item) => typeof item === "string")
388
397
  : [];
389
398
  }
390
- /** Input bound for the planner evidence block; protects the model input budget. */
391
- const FRONTEND_PLAN_INPUT_MAX_CHARS = 12_000;
392
- const FRONTEND_PLAN_INPUT_CAP_LADDER = [
393
- { text: 240, array: 40 },
394
- { text: 120, array: 20 },
395
- { text: 60, array: 10 },
396
- { text: 24, array: 4 },
397
- ];
398
399
  /**
399
400
  * Frozen requirement→PRD citation map for the plan review checklist: lets the
400
401
  * model declare sourceRequirementIds whose section/line match the component
@@ -440,78 +441,19 @@ async function resolveComponentSourceCitations(spec, cwd) {
440
441
  return new Map();
441
442
  }
442
443
  }
443
- /** Input bound for the contract node's compiled ledger block. */
444
- const FRONTEND_CONTRACT_INPUT_MAX_CHARS = 12_000;
445
444
  /**
446
- * Render the contract node's complete-but-bounded ledger handoff. The
445
+ * Render the contract node's complete semantic ledger handoff. The
447
446
  * source-fidelity ledger already extracted canonical requirements with source
448
447
  * spans; the contract node confirms and commits them incrementally through
449
448
  * record_requirement instead of re-reading the raw source (extreme-environment:
450
449
  * a small output window cannot absorb a full source re-read).
451
450
  *
452
- * Same shape guarantees as the plan input block: always valid JSON under the
453
- * char bound, ids never drop, texts degrade through the cap ladder.
451
+ * Whole obligations are retained here; the executor selects complete scopes
452
+ * before dispatch. References and conditions are never clipped.
454
453
  */
455
454
  export function renderFrontendContractInputContext(input) {
456
- // Fragment bindings are ids, not prose: they are the one thing the contract
457
- // must never lose. r6 regression — the last-resort degradation dropped
458
- // sourceFragmentIds entirely, the model (correctly refusing to invent ids)
459
- // committed empty bindings, and the plan compile failed the ledger-binding
460
- // gate for every requirement. Bindings therefore bypass the cap ladder and
461
- // every degradation level; only requirement TEXTS and fragment CONTEXT
462
- // (path/headingPath) may degrade. Fragment context is rendered only for
463
- // fragments actually referenced by a requirement and shrinks first.
464
- const referencedFragmentIds = new Set(input.canonicalRequirements.flatMap((requirement) => planInputStrings(requirement.sourceFragmentIds)));
465
- const referencedFragments = input.fragments.filter((fragment) => referencedFragmentIds.has(fragment.id));
466
- const serializeAtCap = (cap) => JSON.stringify({
467
- requirements: input.canonicalRequirements.map((requirement) => ({
468
- id: requirement.id,
469
- text: planInputText(requirement.text, cap.text),
470
- sourceFragmentIds: planInputStrings(requirement.sourceFragmentIds),
471
- })),
472
- fragments: referencedFragments.map((fragment) => ({
473
- id: fragment.id,
474
- path: planInputText(fragment.path, 200),
475
- headingPath: planInputText(fragment.headingPath, 120),
476
- lineRange: fragment.lineRange,
477
- })),
478
- });
479
- let serialized = serializeAtCap(FRONTEND_PLAN_INPUT_CAP_LADDER[0]);
480
- for (const cap of FRONTEND_PLAN_INPUT_CAP_LADDER.slice(1)) {
481
- if (serialized.length <= FRONTEND_CONTRACT_INPUT_MAX_CHARS)
482
- break;
483
- serialized = serializeAtCap(cap);
484
- }
485
- if (serialized.length > FRONTEND_CONTRACT_INPUT_MAX_CHARS) {
486
- // Last resort: keep every requirement id AND its fragment bindings,
487
- // degrade texts, and shrink referenced fragment context first (halve,
488
- // then drop context fields, then drop the fragment list entirely).
489
- // Requirement ids and sourceFragmentIds are never dropped.
490
- let fragments = referencedFragments.map((fragment) => ({
491
- id: fragment.id,
492
- path: planInputText(fragment.path, 120),
493
- }));
494
- let requirements = input.canonicalRequirements.map((requirement) => {
495
- const sourceFragmentIds = planInputStrings(requirement.sourceFragmentIds);
496
- return {
497
- id: requirement.id,
498
- text: "(truncated)",
499
- // Empty bindings carry no information; omit them so the payload
500
- // stays inside the char bound when no requirement is bound.
501
- ...(sourceFragmentIds.length > 0 ? { sourceFragmentIds } : {}),
502
- };
503
- });
504
- let bounded = JSON.stringify({ degraded: "requirement-texts-truncated", requirements, fragments });
505
- while (bounded.length > FRONTEND_CONTRACT_INPUT_MAX_CHARS && fragments.length > 0) {
506
- fragments = fragments.slice(0, Math.floor(fragments.length / 2));
507
- bounded = JSON.stringify({
508
- degraded: "requirement-texts-truncated",
509
- requirements,
510
- fragments,
511
- });
512
- }
513
- serialized = bounded;
514
- }
455
+ const referenced = new Set(input.canonicalRequirements.flatMap(r => r.sourceFragmentIds));
456
+ const serialized = JSON.stringify({ requirements: input.canonicalRequirements, fragments: input.fragments.filter(f => referenced.has(f.id)) });
515
457
  return [
516
458
  "<frontend_contract_input>",
517
459
  "Canonical requirements extracted by the source-fidelity ledger, compiled by the runner. Treat them as the authoritative requirement inventory: confirm and commit each requirement through record_requirement (one per tool call); the ledger already binds source fragments, so do NOT re-read the raw source files.",
@@ -536,16 +478,13 @@ export async function buildFrontendContractInputContext(input) {
536
478
  });
537
479
  }
538
480
  /**
539
- * Render the planner's complete-but-bounded evidence handoff from committed
481
+ * Render the planner's complete semantic evidence handoff from committed
540
482
  * typed facts. It deliberately excludes upstream response prose and artifact
541
483
  * paths: Contract and Scout have already established these facts, so Plan
542
484
  * should decide and commit rather than spend another model turn reading them.
543
485
  *
544
- * The block is always valid JSON under the char bound: field texts shrink
545
- * through a cap ladder before any fact is dropped, and the last-resort
546
- * fallback keeps every requirement id (with `text: "(truncated)"`) while
547
- * declaring the degradation, so the planner records targeted evidence gaps
548
- * instead of receiving a silently corrupted tail.
486
+ * Whole obligations are retained. Session packing and scoped projection happen
487
+ * in the executor before dispatch, without dropping text or source identities.
549
488
  */
550
489
  export function renderFrontendPlanInputContext(input) {
551
490
  const committedFacts = (records) => records.flatMap((record) => record.phase === "committed" && planInputRecord(record.fact)
@@ -553,8 +492,9 @@ export function renderFrontendPlanInputContext(input) {
553
492
  : []);
554
493
  const contractFacts = committedFacts(input.contractRecords);
555
494
  const scoutFacts = committedFacts(input.scoutRecords);
556
- const requirements = contractFacts
557
- .filter((fact) => fact.kind === "requirement" && fact.origin === "contract")
495
+ const requirementFacts = resolveFrontendContractRequirements(contractFacts);
496
+ const requiredDeliverables = extractRequiredDeliverablePaths(contractFacts);
497
+ const requirements = requirementFacts
558
498
  .map((fact) => ({
559
499
  id: planInputText(fact.id, 80),
560
500
  text: planInputText(fact.text),
@@ -580,92 +520,50 @@ export function renderFrontendPlanInputContext(input) {
580
520
  paths: fact.paths,
581
521
  conflicts: fact.conflicts,
582
522
  }));
523
+ // Authoritative UI states (contract-declared): the source's UI-state table
524
+ // extracted by the contract node. The planner binds these ids instead of
525
+ // inventing list-visibility variants.
526
+ const declaredUiStates = contractFacts
527
+ .filter((fact) => fact.kind === "ui-state-declaration" && fact.origin === "contract")
528
+ .map((fact) => ({
529
+ id: planInputText(fact.id, 80),
530
+ trigger: planInputText(fact.trigger),
531
+ observableOutcome: planInputText(fact.observableOutcome),
532
+ }))
533
+ .filter((state) => state.id !== undefined);
534
+ // Replay registry edits and state-flow removals/additions in commit order.
535
+ const committedUxNames = collectCanonicalStateFlowNames(input.planRecords ?? [], true);
536
+ const committedUiStateNames = [...committedUxNames.uiStateNames];
537
+ const committedInteractionNames = [...committedUxNames.interactionNames];
583
538
  // Reviewer-rubric scaffold: the design reviewer re-runs the design-policy
584
539
  // checks on the committed facts, so publish the checklist to the producer.
585
540
  // Requirements whose contract evidence expects behavioural verification are
586
541
  // enumerated explicitly — those are the slots the reviewer finds missing
587
542
  // when the plan models interactions ad hoc (r8/r9 findings).
588
- const behaviorRequiredIds = requirements
589
- .filter((requirement) => {
590
- const fact = contractFacts.find((candidate) => candidate.kind === "requirement" &&
591
- candidate.origin === "contract" &&
592
- candidate.id === requirement.id);
593
- const evidence = fact?.evidence;
594
- return evidence?.behavior === "required";
595
- })
543
+ const behaviorRequiredIds = requirementFacts
544
+ .filter((requirement) => requirement.evidence.behavior === "required")
596
545
  .map((requirement) => requirement.id);
597
- const serializeAtCap = (cap) => JSON.stringify({
598
- requirements: requirements.map((requirement) => ({
599
- id: requirement.id,
600
- text: planInputText(requirement.text, cap.text),
601
- sourceFragmentIds: planInputStrings(requirement.sourceFragmentIds).slice(0, cap.array),
602
- })),
603
- targetSurface: targetSurface.map((surface) => ({
604
- completeness: planInputText(surface.completeness, 32),
605
- entrypoint: planInputText(surface.entrypoint, cap.text),
606
- routeOrMount: planInputText(surface.routeOrMount, cap.text),
607
- implementationPaths: planInputStrings(surface.implementationPaths).slice(0, cap.array),
608
- testPaths: planInputStrings(surface.testPaths).slice(0, cap.array),
609
- dataSource: planInputText(surface.dataSource, cap.text),
610
- allowedPathConflicts: planInputStrings(surface.allowedPathConflicts).slice(0, cap.array),
611
- unresolvedPaths: planInputStrings(surface.unresolvedPaths).slice(0, cap.array),
612
- })),
613
- designEvidence: designEvidence.map((evidence) => ({
614
- source: planInputText(evidence.source, cap.text),
615
- paths: planInputStrings(evidence.paths).slice(0, cap.array),
616
- conflicts: planInputStrings(evidence.conflicts).slice(0, cap.array),
617
- })),
546
+ const serialized = JSON.stringify({
547
+ requirements, requiredDeliverables, targetSurface, designEvidence, declaredUiStates,
548
+ executionGroups: collectFrontendExecutionGroups(contractFacts.filter(f => f.kind === "requirement").map(f => ({ id: String(f.id), execution: f.execution }))),
549
+ constraints: contractFacts.filter(f => ["constraint", "open-question", "split-proposal", "handoff-intent"].includes(String(f.kind))),
550
+ committedUx: committedUiStateNames.length || committedInteractionNames.length ? { uiStateNames: committedUiStateNames, interactionNames: committedInteractionNames } : undefined,
618
551
  });
619
- let serialized = serializeAtCap(FRONTEND_PLAN_INPUT_CAP_LADDER[0]);
620
- for (const cap of FRONTEND_PLAN_INPUT_CAP_LADDER.slice(1)) {
621
- if (serialized.length <= FRONTEND_PLAN_INPUT_MAX_CHARS)
622
- break;
623
- serialized = serializeAtCap(cap);
624
- }
625
- if (serialized.length > FRONTEND_PLAN_INPUT_MAX_CHARS) {
626
- // Last resort: keep every requirement id (ids are short and the plan
627
- // prompt separately lists them) but drop their texts, shrink scout facts
628
- // to the minimum, and declare the degradation instead of corrupting JSON.
629
- let fallback = {
630
- degraded: "requirement-texts-truncated",
631
- requirements: requirements.map((requirement) => ({
632
- id: requirement.id,
633
- text: "(truncated)",
634
- })),
635
- targetSurface: targetSurface.map((surface) => ({
636
- completeness: planInputText(surface.completeness, 32),
637
- implementationPaths: planInputStrings(surface.implementationPaths).slice(0, FRONTEND_PLAN_INPUT_CAP_LADDER[3].array),
638
- })),
639
- designEvidence: [],
640
- };
641
- let bounded = JSON.stringify(fallback);
642
- let keep = fallback.requirements.length;
643
- while (bounded.length > FRONTEND_PLAN_INPUT_MAX_CHARS &&
644
- keep > 0) {
645
- keep = Math.max(0, Math.floor(keep / 2));
646
- fallback = { ...fallback, requirements: fallback.requirements.slice(0, keep) };
647
- bounded = JSON.stringify({
648
- ...fallback,
649
- requirementIdsTruncated: keep < requirements.length,
650
- });
651
- }
652
- serialized = bounded;
653
- }
654
552
  const checklistLines = [
655
- "1. Every interaction you record needs a uiComponentChoices entry whose purpose equals the interaction name, or one decision=reuse-existing choice covering behavioural interactions.",
656
- "2. Every applicable UI state needs a purpose-matching component choice or a stylingStrategy.",
657
- "3. Every requirement marked (behavior) below needs modelled interactions plus at least one verification target that references it.",
658
- "4. targets.files must name the concrete deliverable files; never leave the scope broader than the frozen requirements state.",
553
+ "1. Record the GLOBAL UX vocabulary with record_state_registry BEFORE any record_state_flow: one stable behavior-domain name per state/interaction (e.g. planner-task-edit), never one name per AC number, and never a rename of an already-recorded concept. Coverage slices by AC; UX does not.",
554
+ "2. Every recorded interaction/uiState name must be in that registry, and uiState names must use the contract's declared authoritative ids (declaredUiStates below) when present.",
555
+ "3. Every interaction and every applicable UI state must be covered by a uiComponentChoices entry: its purpose equals the name, or the choice lists the name in covers (one choice may cover many ids). A reuse-existing decision without evidencePath is rejected; stylingStrategy alone covers nothing.",
556
+ "4. Every requirement marked (behavior) below needs modelled interactions plus at least one verification target that references it.",
659
557
  "5. Verification targets may only reference UI states and requirements you actually recorded (the record_* tools reject unknown references).",
660
558
  `Requirements requiring behavioural coverage: ${behaviorRequiredIds.length > 0 ? behaviorRequiredIds.join(", ") : "(none)"}`,
661
- "6. A decision=new component must declare sourceRequirementIds and pass sourceFragmentId for the frozen PRD fragment whose section matches the component's purpose — the runtime validates that binding and derives specReference (path/section/line); the reviewer checks purpose↔citation consistency.",
559
+ "6. A decision=new component must declare sourceRequirementIds and pass sourceFragmentId for the frozen PRD fragment whose section matches the component's purpose — the runtime validates that binding and derives specReference (path/section/line); the reviewer checks purpose↔citation consistency. A decision=reuse-existing component must pass evidencePath pointing at the existing repo file that proves the reuse.",
662
560
  ...[...input.componentSourceCitations ?? []]
663
561
  .filter(([id]) => behaviorRequiredIds.includes(id))
664
562
  .flatMap(([id, citations]) => citations.map((citation) => ` ${id} + ${citation.fragmentId} → ${citation.section}${citation.line ? ` (line ${citation.line})` : ""}`)),
665
563
  ];
666
564
  return [
667
565
  "<frontend_plan_input>",
668
- "Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence.",
566
+ "Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence. declaredUiStates are the authoritative UI states from the task source; committedUx (retry attempts) is the UX vocabulary already recorded — reuse those names, never re-invent them.",
669
567
  serialized,
670
568
  "Do not read upstream artifacts, task sources, or repository files. If this input cannot support a decision, record a genuine evidence gap.",
671
569
  "</frontend_plan_input>",
@@ -692,6 +590,16 @@ export async function buildFrontendPlanInputContext(runDir, componentSourceCitat
692
590
  // forbids reading anything.
693
591
  throw new Error(`frontend-plan-input-unavailable: cannot read committed typed facts (${error instanceof Error ? error.message : String(error)})`);
694
592
  }
593
+ // Retry-attempt continuity (UX slice visibility): the plan node's own
594
+ // committed facts are absent on the first attempt and present on retries;
595
+ // a missing file is normal there, not a broken pipeline.
596
+ let planRecords = [];
597
+ try {
598
+ planRecords = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
599
+ }
600
+ catch {
601
+ planRecords = [];
602
+ }
695
603
  const committedCount = [...contractRecords, ...scoutRecords].filter((record) => record.phase === "committed").length;
696
604
  if (committedCount === 0) {
697
605
  throw new Error(`frontend-plan-input-unavailable: no committed Contract/Scout facts in ${contractFactsPath} / ${scoutFactsPath}`);
@@ -699,6 +607,7 @@ export async function buildFrontendPlanInputContext(runDir, componentSourceCitat
699
607
  return renderFrontendPlanInputContext({
700
608
  contractRecords,
701
609
  scoutRecords,
610
+ planRecords,
702
611
  componentSourceCitations,
703
612
  });
704
613
  }
@@ -721,77 +630,27 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
721
630
  "</retry_instruction>",
722
631
  ].join("\n");
723
632
  }
724
- if (frontendPlanRetryStep === "compact-terminal-first") {
725
- return [
726
- basePrompt,
727
- "",
728
- "<retry_instruction>",
729
- "Frontend plan retry ladder step: compact-terminal-first.",
730
- "Do NOT rewrite the narrative. Only adopt already staged/quarantined fresh facts, fill the missing required facts, then call finalize_plan exactly once.",
731
- "Keep the existing committed ledger intact; do not re-derive already committed facts.",
732
- "</retry_instruction>",
733
- ].join("\n");
734
- }
735
- if (frontendPlanRetryStep === "bounded-tool-only") {
633
+ if (task.id === "generate-backend-md-plan-pi" &&
634
+ (previousFailureCategory === "output-too-large" ||
635
+ previousFailureCategory === "invalid-output")) {
736
636
  return [
737
637
  basePrompt,
738
638
  "",
739
639
  "<retry_instruction>",
740
- "Frontend plan retry ladder step: bounded-tool-only.",
741
- "Reduce reasoning and wall-clock time. Use only read / fact / terminal tools and only for the necessary missing facts, then call finalize_plan exactly once.",
742
- "Do not expand scope or re-derive already committed facts.",
743
- "</retry_instruction>",
744
- ].join("\n");
745
- }
746
- if (frontendPlanRetryStep === "backup-model") {
747
- return [
748
- basePrompt,
749
- "",
750
- "<retry_instruction>",
751
- "Frontend plan retry ladder step: backup-model.",
752
- "A backup provider/model route was selected after repeated non-converging failures. Re-derive only the missing committed facts, then call finalize_plan exactly once; do not repeat the failed strategy.",
753
- "</retry_instruction>",
754
- ].join("\n");
755
- }
756
- if (previousFailureCategory === "protocol-invalid" &&
757
- task.outputProtocol &&
758
- previousProtocolReason) {
759
- return [
760
- basePrompt,
761
- "",
762
- buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
763
- ].join("\n");
764
- }
765
- if (previousFailureCategory === "review-terminal-missing") {
766
- return [
767
- basePrompt,
768
- "",
769
- "<retry_instruction>",
770
- "The review emitted a verdict in response text but never committed the authoritative typed terminal tool call (approve_review / request_review_changes). The response text is NOT the authority: no branch or gate reads it.",
771
- "Call exactly one typed terminal tool to finish: approve_review (implementation passes, no Critical/Important findings) or request_review_changes (with typed issueCategory, at least one evidenceRef, and non-empty findings). Do not repeat the review analysis; commit the terminal tool once and stop.",
772
- "</retry_instruction>",
773
- ].join("\n");
774
- }
775
- if (previousFailureCategory === "read-burst") {
776
- if (task.id !== FRONTEND_PLAN_NODE_ID) {
777
- return [
778
- basePrompt,
779
- "",
780
- "<retry_instruction>",
781
- "The previous attempt exceeded its read budget. Start a fresh, evidence-minimal pass: do not re-read task sources, upstream stdout, skills, or full diff artifacts already represented by a canonical context artifact.",
782
- "Read the smallest relevant summary first, then only the specific source file or diff fragment needed for the required decision. Finish the required terminal/output protocol as soon as evidence is sufficient.",
783
- "</retry_instruction>",
784
- ].join("\n");
785
- }
786
- return [
787
- basePrompt,
788
- "",
789
- "<retry_instruction>",
790
- "Previous plan attempt issued too many read-only tool calls (read/grep/ls/find) and blew up the context window. Trust the upstream frontend-contract-pi typed requirement facts and frontend-scout-pi target surface already provided — do NOT re-read contract/scout stdout, PRD/source files, or component sources you already inspected.",
791
- "Minimize discovery reads: only read what you genuinely need, once. Commit record_plan_requirement / record_plan_verification_target / record_* facts directly from the facts already in context (one tool call per message), then call finalize_plan exactly once.",
640
+ "The previous backend-test plan attempt was truncated or failed its mandatory Markdown protocol.",
641
+ previousProtocolReason ?? "The previous plan artifact was incomplete.",
642
+ "Return the final Markdown artifact immediately. Do not output analysis, reasoning, source summaries, or planning narration.",
643
+ "Emit the complete section skeleton first, including exactly one ## Coverage Scope, exactly one ## Coverage Matrix, optional ## Scenario Partitions only when applicable, and exactly one ## Module Index with its required 8-column table and at least one module row.",
644
+ "After the complete skeleton exists, fill only concise table rows within the remaining output budget. Do not use code fences.",
792
645
  "</retry_instruction>",
793
646
  ].join("\n");
794
647
  }
648
+ // Repair-category guidance must outrank the retry ladder position: the
649
+ // ladder advances monotonically on transport failures (e.g. length →
650
+ // compact-terminal-first), and its "keep the committed ledger intact"
651
+ // instruction directly contradicts the repair action for invalid-output /
652
+ // truncated ledger facts (re-commit corrected record_* facts). When both
653
+ // apply, the model receives the repair instruction, not the rung script.
795
654
  if (previousFailureCategory === "invalid-output" &&
796
655
  task.structuredContractOutput &&
797
656
  previousProtocolReason) {
@@ -805,7 +664,7 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
805
664
  const splitGuidance = /write-set-too-large|split the task/.test(previousProtocolReason ?? "")
806
665
  ? [
807
666
  "",
808
- "The writeSet is too large for one implement node. Do NOT delete implementation files to squeeze under the limit — that drops required work. Split the task via record_split_proposal (or narrow targets.files to a genuine subset) so each implement node stays bounded; the full file set must remain covered across the split.",
667
+ "The writeSet is too large for one implement node. Preserve the complete requirement and file coverage. This needs a Contract-level task split, not a Plan formatting repair. Report the write-set-too-large finding and the affected paths for the controller to resume Contract/split orchestration. Plan cannot change protected targets.files or call Contract-only tools; do not invent a split tool or discard required files.",
809
668
  ]
810
669
  : [];
811
670
  return [
@@ -867,8 +726,96 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
867
726
  "</retry_instruction>",
868
727
  ].join("\n");
869
728
  }
729
+ if (frontendPlanRetryStep === "compact-terminal-first") {
730
+ return [
731
+ basePrompt,
732
+ "",
733
+ "<retry_instruction>",
734
+ "Frontend plan retry ladder step: compact-terminal-first.",
735
+ "Do NOT rewrite the narrative. Only adopt already staged/quarantined fresh facts, fill the missing required facts, then call finalize_plan exactly once.",
736
+ "Keep the existing committed ledger intact; do not re-derive already committed facts.",
737
+ "</retry_instruction>",
738
+ ].join("\n");
739
+ }
740
+ if (frontendPlanRetryStep === "bounded-tool-only") {
741
+ return [
742
+ basePrompt,
743
+ "",
744
+ "<retry_instruction>",
745
+ "Frontend plan retry ladder step: bounded-tool-only.",
746
+ "Reduce reasoning and wall-clock time. Use only read / fact / terminal tools and only for the necessary missing facts, then call finalize_plan exactly once.",
747
+ "Do not expand scope or re-derive already committed facts.",
748
+ "</retry_instruction>",
749
+ ].join("\n");
750
+ }
751
+ if (frontendPlanRetryStep === "backup-model") {
752
+ return [
753
+ basePrompt,
754
+ "",
755
+ "<retry_instruction>",
756
+ "Frontend plan retry ladder step: backup-model.",
757
+ "A backup provider/model route was selected after repeated non-converging failures. Re-derive only the missing committed facts, then call finalize_plan exactly once; do not repeat the failed strategy.",
758
+ "</retry_instruction>",
759
+ ].join("\n");
760
+ }
761
+ if (previousFailureCategory === "protocol-invalid" &&
762
+ task.outputProtocol &&
763
+ previousProtocolReason) {
764
+ return [
765
+ basePrompt,
766
+ "",
767
+ buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
768
+ ].join("\n");
769
+ }
770
+ if (previousFailureCategory === "review-terminal-missing") {
771
+ return [
772
+ basePrompt,
773
+ "",
774
+ "<retry_instruction>",
775
+ "The review emitted a verdict in response text but never committed the authoritative typed terminal tool call (approve_review / request_review_changes). The response text is NOT the authority: no branch or gate reads it.",
776
+ "Call exactly one typed terminal tool to finish: approve_review (implementation passes, no Critical/Important findings) or request_review_changes (with typed issueCategory, at least one evidenceRef, and non-empty findings). Do not repeat the review analysis; commit the terminal tool once and stop.",
777
+ "</retry_instruction>",
778
+ ].join("\n");
779
+ }
780
+ if (previousFailureCategory === "read-burst") {
781
+ if (task.id !== FRONTEND_PLAN_NODE_ID) {
782
+ return [
783
+ basePrompt,
784
+ "",
785
+ "<retry_instruction>",
786
+ "The previous attempt exceeded its read budget. Start a fresh, evidence-minimal pass: do not re-read task sources, upstream stdout, skills, or full diff artifacts already represented by a canonical context artifact.",
787
+ "Read the smallest relevant summary first, then only the specific source file or diff fragment needed for the required decision. Finish the required terminal/output protocol as soon as evidence is sufficient.",
788
+ "</retry_instruction>",
789
+ ].join("\n");
790
+ }
791
+ return [
792
+ basePrompt,
793
+ "",
794
+ "<retry_instruction>",
795
+ "Previous plan attempt issued too many read-only tool calls (read/grep/ls/find) and blew up the context window. Trust the upstream frontend-contract-pi typed requirement facts and frontend-scout-pi target surface already provided — do NOT re-read contract/scout stdout, PRD/source files, or component sources you already inspected.",
796
+ "Minimize discovery reads: only read what you genuinely need, once. Commit record_plan_requirement / record_plan_verification_target / record_* facts directly from the facts already in context (one tool call per message), then call finalize_plan exactly once.",
797
+ "</retry_instruction>",
798
+ ].join("\n");
799
+ }
800
+ // Generic output-limit fallback. Every branch above this one carries a
801
+ // more precise instruction for the same capacity signal (contract repair
802
+ // reasons, ladder rungs — compact-terminal-first is the reason-mandated
803
+ // rung for length-before-terminal —, protocol/review/read-burst repair),
804
+ // so output-limit must not shadow them.
805
+ if (previousFailureCategory === OUTPUT_LIMIT_RETRY_CATEGORY) {
806
+ return [
807
+ basePrompt,
808
+ "",
809
+ "<retry_instruction>",
810
+ "The previous turn ended with stopReason=length before the required output was complete. This is output-capacity truncation, not empty output.",
811
+ "Continue incrementally from already committed typed facts and current in-scope workspace files. Do not repeat completed discovery, decisions, facts, or writes.",
812
+ "Generate the smallest unfinished unit next, validate its required format immediately, repair any format error in this node, and only then continue to the next unfinished unit or terminal tool.",
813
+ "Keep prose minimal and finish the required terminal/output protocol as soon as the remaining work is valid.",
814
+ "</retry_instruction>",
815
+ ].join("\n");
816
+ }
870
817
  if (previousFailureCategory === "writer-empty-diff") {
871
- const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
818
+ const maxAttempts = task.retryPolicy?.maxAttempts ?? 5;
872
819
  // When a completeness progress exists for this writer, fold the concrete
873
820
  // target paths into the empty-diff retry so the model does not guess and
874
821
  // does not need to read a forbidden `.harness/**` evidence file.
@@ -896,7 +843,7 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
896
843
  ].join("\n");
897
844
  }
898
845
  if (previousFailureCategory === "incomplete-write-set") {
899
- const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
846
+ const maxAttempts = task.retryPolicy?.maxAttempts ?? 5;
900
847
  const bindingOnly = task.id?.startsWith("generate-backend-md-case-") === true &&
901
848
  (recoveryTargetPaths?.length ?? 0) === 1 &&
902
849
  Object.values(recoveryDiagnostics ?? {}).flat().some((detail) => /(?:unclassified Test Points|duplicate Test Point bindings)/i.test(detail));
@@ -1504,6 +1451,20 @@ export async function executeDagNode(input) {
1504
1451
  return;
1505
1452
  }
1506
1453
  }
1454
+ if (task.id === "frontend-scout-pi") {
1455
+ try {
1456
+ const { readTypedEventStoreFromJsonl } = await import("./frontend-typed-event-store.js");
1457
+ const facts = (await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-contract-pi", "contract-typed-facts.jsonl"))).filter(r => r.phase === "committed").map(r => r.fact);
1458
+ const requirements = [...new Map(facts.filter(f => f.kind === "requirement").map(f => [String(f.id), f])).values()];
1459
+ if (!requirements.length)
1460
+ throw Error("FRONTEND_INPUT_MISSING: Scout needs committed Contract obligations");
1461
+ prompt += `\n<frontend_scout_input>\n${JSON.stringify({ requirements, sharedFacts: facts.filter(f => !["requirement", "contract-scope-completed", "contract-finalized"].includes(String(f.kind))) })}\n</frontend_scout_input>`;
1462
+ }
1463
+ catch (error) {
1464
+ await failBeforePrompt(error, "frontend-scout-input-unavailable");
1465
+ return;
1466
+ }
1467
+ }
1507
1468
  if (FRONTEND_WRITER_NODE_IDS.includes(nodeId)) {
1508
1469
  // Design-review findings are not reliable in the provider's prose output
1509
1470
  // (typed terminal nodes commonly return an empty assistant message). Inject
@@ -1691,6 +1652,7 @@ export async function executeDagNode(input) {
1691
1652
  attemptPrompt,
1692
1653
  });
1693
1654
  result = await executeNode({
1655
+ ...(state.budgetLedger?.mode === "hard" && state.budgetLedger.limits.maxProviderRequests !== undefined ? { reserveProviderRequest: () => reserveDagProviderRequest({ state, nodeId, attempt: attemptNumber, persist: input.persistState }) } : {}),
1694
1656
  task,
1695
1657
  cwd,
1696
1658
  model,
@@ -1712,6 +1674,43 @@ export async function executeDagNode(input) {
1712
1674
  durationMs: 0,
1713
1675
  };
1714
1676
  }
1677
+ // stopReason=length on top of a bare empty-output verdict is a provider
1678
+ // capacity signal, not a true empty response: reroute it through the
1679
+ // dedicated output-limit retry path while keeping the raw category for
1680
+ // diagnostics. Bare empty-output and transport aliases (network,
1681
+ // nonzero-exit, unknown) are rerouted — categories that already carry a
1682
+ // precise repair instruction (output-too-large, invalid-output,
1683
+ // protocol-invalid, structured-output-truncated via the validators below,
1684
+ // writer categories) keep their classification so their exact retry
1685
+ // guidance still reaches the model.
1686
+ if (task.executor === "pi" &&
1687
+ !result.ok &&
1688
+ result.stopReason === "length" &&
1689
+ (result.failureCategory === undefined ||
1690
+ ["empty-output", "network", "nonzero-exit", "unknown"].includes(result.failureCategory))) {
1691
+ result = {
1692
+ ...result,
1693
+ rawFailureCategory: result.rawFailureCategory ?? result.failureCategory,
1694
+ failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
1695
+ stderr: [
1696
+ result.stderr,
1697
+ "output-limit: stopReason=length; preserve completed work and retry only the unfinished output",
1698
+ ]
1699
+ .filter(Boolean)
1700
+ .join("\n"),
1701
+ };
1702
+ }
1703
+ if (task.id === "generate-backend-md-plan-pi" &&
1704
+ !result.ok &&
1705
+ (result.failureCategory === "output-too-large" ||
1706
+ result.failureCategory === "invalid-output")) {
1707
+ const marker = "backend-test Markdown plan";
1708
+ const markerIndex = result.stderr?.lastIndexOf(marker) ?? -1;
1709
+ previousProtocolReason =
1710
+ markerIndex >= 0
1711
+ ? result.stderr.slice(markerIndex, markerIndex + 4_000).trim()
1712
+ : `backend-test Markdown plan attempt failed with ${result.failureCategory}`;
1713
+ }
1715
1714
  if (isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId)) {
1716
1715
  const rawText = canonicalNodeOutput(result);
1717
1716
  if (rawText.trim().length > 0) {
@@ -1834,7 +1833,7 @@ export async function executeDagNode(input) {
1834
1833
  !result.ok &&
1835
1834
  retryPolicy !== undefined) {
1836
1835
  const submissions = await countContractRecordSubmissions(runDir, nodeId);
1837
- if (submissions === 0) {
1836
+ if (submissions === 0 && result.stopReason !== "length") {
1838
1837
  result = {
1839
1838
  ...result,
1840
1839
  failureCategory: "empty-output",