@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.11

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 (338) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +349 -97
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +12 -0
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +23 -5
  9. package/dist/cli/program.js +21 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli/update/runtime-activity.js +1 -29
  12. package/dist/cli.js +2 -1
  13. package/dist/commands/client-recovery.js +3 -0
  14. package/dist/commands/dag-follow-up.js +138 -0
  15. package/dist/commands/dag-rerun.js +55 -1
  16. package/dist/commands/init-model-catalog.js +464 -0
  17. package/dist/commands/init-upgrade.js +265 -97
  18. package/dist/commands/init.js +455 -28
  19. package/dist/commands/inspect-next.js +8 -0
  20. package/dist/commands/task-advance.js +19 -0
  21. package/dist/executors/dag-pi-executor.js +2370 -64
  22. package/dist/executors/pi-executor.js +11 -2
  23. package/dist/executors/pi-extension-resolver.js +233 -0
  24. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  25. package/dist/executors/pi-read-budget-policy.js +239 -0
  26. package/dist/executors/pi-sdk-executor.js +225 -33
  27. package/dist/executors/pi-writer-tool-policy.js +57 -1
  28. package/dist/executors/shell-executor.js +1258 -224
  29. package/dist/executors/shell-write-guard.js +14 -0
  30. package/dist/executors/workspace-write-snapshot.js +68 -0
  31. package/dist/infrastructure/harness/atomic-write.js +23 -0
  32. package/dist/shared/dag-failure-category.js +150 -0
  33. package/dist/shared/dag-prompt-override.js +27 -0
  34. package/dist/shared/openspec-spec.js +70 -4
  35. package/dist/shared/operator/capabilities.js +39 -0
  36. package/dist/shared/playwright-cli-command-policy.js +15 -0
  37. package/dist/shared/update/console-notifier.js +100 -0
  38. package/dist/{cli → shared}/update/npm-client.js +70 -15
  39. package/dist/{cli → shared}/update/state.js +41 -13
  40. package/dist/task/config-types.js +50 -5
  41. package/dist/task/contract/apply.js +11 -1
  42. package/dist/task/contract/constants.js +2 -0
  43. package/dist/task/contract/hash.js +3 -0
  44. package/dist/task/contract/observe.js +25 -4
  45. package/dist/task/contract/paths.js +2 -1
  46. package/dist/task/contract/project.js +18 -1
  47. package/dist/task/contract/schema.js +4 -1
  48. package/dist/task/contract/transaction.js +12 -0
  49. package/dist/task/frontend-project-capability.js +199 -18
  50. package/dist/task/source-prepare/completeness.js +188 -0
  51. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  52. package/dist/task/source-prepare/index.js +3 -0
  53. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  54. package/dist/task/source-prepare/ledger-review.js +214 -0
  55. package/dist/task/source-prepare/ledger.js +545 -0
  56. package/dist/task/source-prepare/parse-intent.js +24 -4
  57. package/dist/task/source-prepare/prepare.js +298 -1
  58. package/dist/task/source-prepare/semantic-intake.js +25 -5
  59. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  60. package/dist/task/source-prepare/types.js +22 -0
  61. package/dist/task/source-references.js +22 -1
  62. package/dist/worker/cli.js +18 -2
  63. package/dist/worker/console/chat/chat-event-store.js +75 -5
  64. package/dist/worker/console/chat/context-panel.js +7 -0
  65. package/dist/worker/console/chat/deferred-turn.js +12 -0
  66. package/dist/worker/console/chat/operation-card.js +8 -1
  67. package/dist/worker/console/chat/pi-console-config.js +81 -10
  68. package/dist/worker/console/chat/pi-runtime.js +331 -18
  69. package/dist/worker/console/chat/provider-error.js +98 -0
  70. package/dist/worker/console/chat/routes.js +395 -59
  71. package/dist/worker/console/chat/session-stats.js +179 -0
  72. package/dist/worker/console/chat/session-store.js +93 -63
  73. package/dist/worker/console/chat/todos.js +115 -0
  74. package/dist/worker/console/chat/tool-preview.js +42 -0
  75. package/dist/worker/console/chat/turn-process.js +20 -55
  76. package/dist/worker/console/chat/user-questions.js +266 -0
  77. package/dist/worker/console/chat/workspace-landing.js +79 -0
  78. package/dist/worker/console/console-handoff.js +82 -0
  79. package/dist/worker/console/console-update-and-init.js +115 -0
  80. package/dist/worker/console/console-update-runtime.js +84 -0
  81. package/dist/worker/console/dag-execution-receipt.js +33 -0
  82. package/dist/worker/console/draft-store.js +49 -0
  83. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  84. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  85. package/dist/worker/console/index.js +3 -0
  86. package/dist/worker/console/inspect-split.js +18 -0
  87. package/dist/worker/console/native-directory-picker.js +13 -0
  88. package/dist/worker/console/operation-run-facts.js +19 -3
  89. package/dist/worker/console/operation-store.js +227 -7
  90. package/dist/worker/console/operator-actions.js +388 -0
  91. package/dist/worker/console/operator-surface-health.js +1 -0
  92. package/dist/worker/console/operator-user-error.js +2 -2
  93. package/dist/worker/console/prd-intake-bridge.js +13 -0
  94. package/dist/worker/console/routes.js +355 -3
  95. package/dist/worker/console/security.js +67 -13
  96. package/dist/worker/console/server.js +609 -164
  97. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-CXj_GnSb.js} +1 -1
  98. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BZp6JAp7.js} +1 -1
  99. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-DfpcEuYU.js} +1 -1
  100. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-_Gf0xadb.js} +1 -1
  101. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-Ct2QPCmv.js} +1 -1
  102. package/dist/worker/console/static/assets/channel-3TxJgYaH.js +1 -0
  103. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-BfklkzKl.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-Dy25vZJV.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-BFlCRZep.js} +1 -1
  106. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-op3oVxIE.js} +1 -1
  107. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-C0R7rzF2.js} +1 -1
  108. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-Bt44TCGy.js} +1 -1
  109. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-_uEE_XFx.js} +1 -1
  110. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-C3TOYGZ9.js} +1 -1
  111. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-Cz60oBPV.js} +1 -1
  112. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-8Bik0qis.js} +1 -1
  113. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BHIkXpp3.js +1 -0
  114. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BHIkXpp3.js +1 -0
  115. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-C2QIOA4a.js} +1 -1
  116. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-CSH_yUUd.js} +1 -1
  117. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-BDLw2XFM.js} +1 -1
  118. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-CcD9ZtF4.js} +1 -1
  119. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-DqwtkBsI.js} +1 -1
  120. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BWVEcqsC.js} +1 -1
  121. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-M-WAbtQi.js} +1 -1
  122. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-DKW7SixP.js} +1 -1
  123. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-PHHPPtrj.js} +1 -1
  124. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-BQD-B4RX.js} +1 -1
  125. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-BFvULb51.js} +1 -1
  126. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-Bw-aXTCb.js} +1 -1
  127. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BGKw04Qx.js} +1 -1
  128. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-RqKaPR-I.js} +1 -1
  129. package/dist/worker/console/static/assets/index-B_D8rbWc.js +407 -0
  130. package/dist/worker/console/static/assets/index-BdNx6fj0.css +1 -0
  131. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-YO5dnzrJ.js} +1 -1
  132. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-Bi1VZHMq.js} +1 -1
  133. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-Bszls1DD.js} +1 -1
  134. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-PhzeaZ09.js} +1 -1
  135. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-BsjbDoXi.js} +1 -1
  136. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-0B7NnWKk.js} +5 -5
  137. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BJr4Fj-q.js} +1 -1
  138. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-moC4fpGB.js} +1 -1
  139. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-BEw37-2c.js} +1 -1
  140. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-Cq6LyasU.js} +1 -1
  141. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-DUCMcK0D.js} +1 -1
  142. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-C3upTZm7.js} +1 -1
  143. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-BI_gMsCW.js} +1 -1
  144. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-YFOIRzfN.js} +1 -1
  145. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-dOnB7UDD.js} +1 -1
  146. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-BXUniaIh.js} +1 -1
  147. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cdi6UhLa.js +1 -0
  148. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-2tA4wTNu.js} +2 -2
  149. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D-RJBbb0.js +8 -0
  150. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-DO0HkJXC.js} +1 -1
  151. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DvIOzixv.js} +1 -1
  152. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-DXDZ0cTj.js} +1 -1
  153. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-B32Ark0D.js} +1 -1
  154. package/dist/worker/console/static/index.html +2 -2
  155. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  156. package/dist/worker/console/static-src/app/console-types.js +68 -4
  157. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  158. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  159. package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
  160. package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
  161. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  162. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  163. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  164. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
  165. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  166. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  167. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  168. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  169. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  170. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  171. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  172. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  173. package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
  174. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
  175. package/dist/worker/console/static-src/operator-chat/useChatStream.js +183 -48
  176. package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
  177. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  178. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  179. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  180. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  181. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  182. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  183. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  184. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  185. package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
  186. package/dist/worker/console/workspace-context.js +344 -0
  187. package/dist/worker/console/workspace-registry.js +214 -0
  188. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  189. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  190. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  191. package/dist/worker/observability/init-runtime-activity.js +26 -0
  192. package/dist/worker/observability/read-model.js +22 -0
  193. package/dist/worker/observe/node-input.js +160 -3
  194. package/dist/worker/observe/node-process.js +377 -0
  195. package/dist/worker/observe/routes.js +224 -3
  196. package/dist/worker/observe/server.js +4 -0
  197. package/dist/worker/observe/shell-handler-keys.js +34 -0
  198. package/dist/worker/observe/static/api.js +90 -4
  199. package/dist/worker/observe/static/app.js +18 -70
  200. package/dist/worker/observe/static/constants.js +8 -1
  201. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  202. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  203. package/dist/worker/observe/static/dag-node-purpose.js +208 -0
  204. package/dist/worker/observe/static/format.js +60 -0
  205. package/dist/worker/observe/static/index.html +6 -1
  206. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  207. package/dist/worker/observe/static/markdown-render.js +20 -1
  208. package/dist/worker/observe/static/operator-chrome.css +145 -20
  209. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  210. package/dist/worker/observe/static/operator-chrome.js +330 -96
  211. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  212. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  213. package/dist/worker/observe/static/router.d.ts +46 -0
  214. package/dist/worker/observe/static/router.js +25 -1
  215. package/dist/worker/observe/static/state.d.ts +50 -0
  216. package/dist/worker/observe/static/state.js +184 -1
  217. package/dist/worker/observe/static/styles.css +395 -0
  218. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  219. package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
  220. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  221. package/dist/worker/observe/static/views/dag.js +14 -5
  222. package/dist/worker/observe/static/views/dashboard.js +6 -4
  223. package/dist/worker/observe/static/views/failures.js +5 -2
  224. package/dist/worker/observe/static/views/night.js +2 -2
  225. package/dist/worker/observe/static/views/pool.js +1 -0
  226. package/dist/worker/observe/static/views/run.js +3 -1
  227. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  228. package/dist/worker/observe/static/views/task.js +56 -1
  229. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
  230. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  231. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  232. package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
  233. package/dist/workflows/dag/contract-output-registry.js +15 -0
  234. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  235. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  236. package/dist/workflows/dag/dynamic-runtime/map.js +2 -1
  237. package/dist/workflows/dag/failure-category.js +7 -116
  238. package/dist/workflows/dag/frontend-closeout.js +221 -0
  239. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  240. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  241. package/dist/workflows/dag/frontend-implementation-contract.js +538 -166
  242. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  243. package/dist/workflows/dag/frontend-prewrite-gate.js +388 -392
  244. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  245. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  246. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  247. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  248. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  249. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  250. package/dist/workflows/dag/frontend-repair.js +7 -424
  251. package/dist/workflows/dag/frontend-review-context.js +288 -14
  252. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  253. package/dist/workflows/dag/frontend-risk.js +15 -2
  254. package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
  255. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  256. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  257. package/dist/workflows/dag/frontend-shape.js +427 -0
  258. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  259. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  260. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  261. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  262. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  263. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  264. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  265. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  266. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  267. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  268. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  269. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  270. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  271. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  272. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  273. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  274. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  275. package/dist/workflows/dag/frontend-writer-admission.js +272 -0
  276. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  277. package/dist/workflows/dag/init-hybrid.js +1072 -660
  278. package/dist/workflows/dag/interrupt-request.js +10 -3
  279. package/dist/workflows/dag/lifecycle.js +3 -3
  280. package/dist/workflows/dag/node-execution.js +552 -58
  281. package/dist/workflows/dag/prompt.js +44 -19
  282. package/dist/workflows/dag/recovery-lease.js +80 -0
  283. package/dist/workflows/dag/report.js +37 -1
  284. package/dist/workflows/dag/rerun-feedback.js +124 -1
  285. package/dist/workflows/dag/rerun-plan.js +144 -5
  286. package/dist/workflows/dag/rerun-run.js +81 -6
  287. package/dist/workflows/dag/rerun-task.js +29 -0
  288. package/dist/workflows/dag/retry-policy.js +318 -8
  289. package/dist/workflows/dag/runner.js +429 -122
  290. package/dist/workflows/dag/scheduler.js +114 -16
  291. package/dist/workflows/dag/structured-output-repair.js +712 -0
  292. package/dist/workflows/dag/types.js +338 -31
  293. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  294. package/dist/workflows/dag/validate.js +92 -32
  295. package/docs/README.md +2 -0
  296. package/docs/architecture/evolution.md +45 -0
  297. package/docs/init-surface.manifest.json +9 -0
  298. package/docs/operations/README.md +1 -1
  299. package/docs/operations/local-development-environment.md +21 -0
  300. package/docs/templates/README.md +2 -1
  301. package/docs/templates/agent-dag-report.schema.json +8 -2
  302. package/docs/templates/agent-dag.schema.json +63 -1
  303. package/docs/templates/backend-test-dag.json +265 -237
  304. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  305. package/docs/templates/frontend-test-case-checklist.md +2 -2
  306. package/docs/templates/frontend-test-dag.json +8 -10
  307. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  308. package/docs/templates/init-managed-agents.md +13 -3
  309. package/docs/templates/spec-registry.schema.json +45 -0
  310. package/harness.json +1 -1
  311. package/package.json +7 -1
  312. package/scripts/next-info.mjs +356 -0
  313. package/scripts/next-publish-gate.mjs +238 -0
  314. package/scripts/release-source-binding.mjs +251 -0
  315. package/skills/codebase-scout/SKILL.md +1 -1
  316. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  317. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  318. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  319. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  320. package/skills/frontend-design-review/SKILL.md +20 -41
  321. package/skills/frontend-implementation/SKILL.md +3 -4
  322. package/skills/frontend-implementation/references/design-spec.md +15 -10
  323. package/skills/frontend-implementation/references/node-contracts.md +23 -13
  324. package/skills/frontend-review/SKILL.md +22 -14
  325. package/skills/frontend-review/references/review-findings.md +6 -7
  326. package/skills/loop-agent/references/command-reference.md +12 -4
  327. package/skills/loop-agent/references/hybrid-dag.md +8 -1
  328. package/skills/playwright-cli/SKILL.md +23 -1
  329. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  330. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  331. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  332. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  333. package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
  334. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  335. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  336. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
  337. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  338. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { pathMatchesPattern } from "../../shared/git-progress.js";
6
+ import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
6
7
  import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
7
8
  import { FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isFrontendWriterAuthorized, readFrontendPrewriteResult, } from "./scheduler.js";
8
9
  import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
@@ -10,11 +11,15 @@ import { resolveContextPolicy } from "./context-policy.js";
10
11
  import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
11
12
  import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
12
13
  import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
13
- import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
14
+ import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, projectFrontendNodeProtocolFailureReason, resolveFrontendPlanBackupRoute, resolveFrontendPlanRetryStep, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
14
15
  import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
15
16
  import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
16
17
  import { getStructuredContractValidator } from "./contract-output-registry.js";
17
18
  import "./contract-validator-registrations.js";
19
+ import { computeNormalizedFailureFingerprint } from "./frontend-recovery-lineage.js";
20
+ import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
21
+ import { allowedRepairReadPaths, auditRepairAttemptToolUse, buildStructuredOutputRepairPrompt, freezeStructuredOutputRepairContext, hasNonEmptyStructuredCandidate, isFrontendStructuredRepairSchemaId, isStructuredRepairableFailureCategory, persistStructuredAttemptRaw, sessionEventsByteLength, GOVERNANCE_BLOCKED_CATEGORY, STRUCTURED_REPAIR_EXHAUSTED_CATEGORY, } from "./structured-output-repair.js";
22
+ import { readFrontendCanonicalCandidate } from "./frontend-implementation-contract.js";
18
23
  import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
19
24
  import { buildProjectGovernanceContext, readCompletedWriterChangeManifests, writeProjectGovernanceContext, } from "./project-governance-context.js";
20
25
  import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
@@ -132,16 +137,26 @@ export function buildNodePrompt(spec, task, upstream, options) {
132
137
  function frontendStructuredArtifactRetryGuidance(schemaId) {
133
138
  if (schemaId === "frontend-implementation-contract-plan-patch-v1") {
134
139
  return [
135
- "Return exactly two blocks and nothing else: one fenced json block containing only the editable RFC 7386 plan patch, immediately followed by one fenced openspec-citations block.",
140
+ "Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing only the editable RFC 7386 plan patch, immediately followed by exactly one fenced openspec-citations block.",
136
141
  "The runtime applies your patch to its protected contract skeleton. Do not emit schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff; do not emit the full contract.",
137
142
  "Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
143
+ "The openspec-citations fenced block contains one JSON object per line, each with path/section/line — for example:",
144
+ "```openspec-citations",
145
+ '{"path":".harness/tasks/<task>/source/需求.md","section":"Objective","line":5}',
146
+ '{"path":".harness/tasks/<task>/source/references/product-requirement.md","section":"FR-1","line":16}',
147
+ "```",
138
148
  ];
139
149
  }
140
150
  if (schemaId === "frontend-implementation-contract-revision-patch-v1") {
141
151
  return [
142
- "Return exactly two blocks and nothing else: one fenced json block containing the RFC 7386 merge-patch delta, immediately followed by one fenced openspec-citations block.",
152
+ "Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing the RFC 7386 merge-patch delta, immediately followed by exactly one fenced openspec-citations block.",
143
153
  "The delta contains only the fields you change; null deletes a key, arrays and scalars replace, and plain objects merge recursively. Do not emit a full contract or a schemaVersion/targets section.",
144
154
  "Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
155
+ "The openspec-citations fenced block contains one JSON object per line, each with path/section/line — for example:",
156
+ "```openspec-citations",
157
+ '{"path":".harness/tasks/<task>/source/需求.md","section":"Objective","line":5}',
158
+ '{"path":".harness/tasks/<task>/source/references/product-requirement.md","section":"FR-1","line":16}',
159
+ "```",
145
160
  ];
146
161
  }
147
162
  return [
@@ -149,30 +164,210 @@ function frontendStructuredArtifactRetryGuidance(schemaId) {
149
164
  "The contract JSON fields are the complete implementation plan. Do not emit a separate plan document, Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
150
165
  ];
151
166
  }
152
- function isFrontendPlanArtifactSchema(schemaId) {
153
- return (schemaId === "frontend-implementation-contract-plan-patch-v1" ||
154
- schemaId === "frontend-implementation-contract-revision-patch-v1");
167
+ const FRONTEND_PLAN_NODE_ID = "frontend-plan-pi";
168
+ const COMPACT_RETRY_SECTION_MAX_CHARS = 1_200;
169
+ const COMPACT_RETRY_TASK_MAX_CHARS = 4_800;
170
+ /** Keep alias/field-shape remediation out of the initial Plan prompt. */
171
+ function frontendPlanValidationRetryGuidance(reason) {
172
+ const guidance = [];
173
+ if (/notApplicableReason|uiState.*reason/i.test(reason)) {
174
+ guidance.push("For this retry, use uiStates[].notApplicableReason for a non-applicable state; omit expectedBehavior instead of supplying an empty value.");
175
+ }
176
+ if (/interaction.*(?:\bid\b|\bname\b)|requires non-empty interaction\.name/i.test(reason)) {
177
+ guidance.push("For this retry, use interactions[].name with non-empty trigger and expectedBehavior.");
178
+ }
179
+ if (/verificationTargets.*uiStates|uiStates.*verificationTargets/i.test(reason)) {
180
+ guidance.push("For this retry, provide verificationTargets[].uiStates as an array; use [] when the target has no named UI state.");
181
+ }
182
+ return guidance;
155
183
  }
156
- function buildFrontendPlanArtifactRecoveryPrompt(options) {
157
- const cause = options.failure === "truncated"
158
- ? "Previous response was truncated by the provider (stopReason=length)."
159
- : options.failure === "too-large"
160
- ? "Previous response could not be retained by the provider."
161
- : "Previous response failed structured validation.";
184
+ function compactRetryText(text, maxChars) {
185
+ if (text.length <= maxChars)
186
+ return text;
187
+ const headChars = Math.floor(maxChars * 0.78);
188
+ const tailChars = Math.max(0, maxChars - headChars - 52);
189
+ return `${text.slice(0, headChars)}\n… [omitted after context-overflow] …\n${text.slice(-tailChars)}`;
190
+ }
191
+ function compactRetrySection(prompt, name, maxChars) {
192
+ const match = prompt.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`));
193
+ return compactRetryText(match?.[1]?.trim() ?? "(not available)", maxChars);
194
+ }
195
+ /**
196
+ * A context-overflow retry must not resend the full resolved skills, governance
197
+ * payload, repeated source excerpts, and upstream previews that caused the
198
+ * previous session to overflow. Keep the immutable node contract plus bounded
199
+ * objective/criteria/task excerpts; canonical artifacts remain readable by
200
+ * pointer from the retained task instructions.
201
+ */
202
+ export function buildContextOverflowRetryPrompt(task, basePrompt) {
162
203
  return [
163
- "<retry_instruction>",
164
- cause,
165
- options.reason ? `Validation error: ${options.reason}` : "",
166
- ...frontendStructuredArtifactRetryGuidance(options.schemaId),
167
- "Use concise values and reference paths/IDs instead of copying source prose. Do not omit required semantics.",
168
- "</retry_instruction>",
169
- ]
170
- .filter(Boolean)
171
- .join("\n");
204
+ "<compact_retry_context>",
205
+ "This is a fresh, compact retry after provider context-overflow. The original full prompt remains audit evidence but is intentionally not resent.",
206
+ "Do not re-read task sources, upstream stdout, skills, or full diff artifacts that are already represented by canonical artifacts. Start from the smallest retained artifact and expand only the exact file or diff fragment needed.",
207
+ "</compact_retry_context>",
208
+ `<dag_objective>\n${compactRetrySection(basePrompt, "dag_objective", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</dag_objective>`,
209
+ `<success_criteria>\n${compactRetrySection(basePrompt, "success_criteria", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</success_criteria>`,
210
+ `<global_constraints>\n${compactRetrySection(basePrompt, "global_constraints", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</global_constraints>`,
211
+ `<node_contract>\n${compactRetrySection(basePrompt, "node_contract", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</node_contract>`,
212
+ `<upstream_context>\n${compactRetrySection(basePrompt, "upstream_context", COMPACT_RETRY_SECTION_MAX_CHARS)}\n</upstream_context>`,
213
+ [
214
+ "<task>",
215
+ `Original task id: ${task.id}`,
216
+ compactRetrySection(basePrompt, "task", COMPACT_RETRY_TASK_MAX_CHARS),
217
+ "</task>",
218
+ ].join("\n"),
219
+ ].join("\n\n");
220
+ }
221
+ function isFrontendPlanLadderTask(task) {
222
+ return task.id === FRONTEND_PLAN_NODE_ID;
223
+ }
224
+ /**
225
+ * Read the committed plan ledger snapshot for the ladder's "no new committed
226
+ * fact" check. The digest is over the sorted committed payload hashes, and
227
+ * `hasTerminalFact` reflects a committed `finalize_plan` terminal fact.
228
+ */
229
+ async function readFrontendPlanCommittedSnapshot(runDir, nodeId) {
230
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, nodeId, "plan-typed-facts.jsonl"));
231
+ const committed = records.filter((record) => record.phase === "committed");
232
+ const digest = createHash("sha256")
233
+ .update(committed
234
+ .map((record) => record.payloadSha256)
235
+ .sort()
236
+ .join("\n"))
237
+ .digest("hex");
238
+ const hasTerminalFact = committed.some((record) => record.fact.kind === "finalize_plan");
239
+ return { digest, hasTerminalFact };
240
+ }
241
+ function planInputRecord(value) {
242
+ return typeof value === "object" && value !== null && !Array.isArray(value);
243
+ }
244
+ function planInputText(value, maxChars = 240) {
245
+ if (typeof value !== "string" || value.trim().length === 0)
246
+ return undefined;
247
+ const normalized = value.trim();
248
+ return normalized.length <= maxChars
249
+ ? normalized
250
+ : `${normalized.slice(0, maxChars - 1)}…`;
251
+ }
252
+ function planInputStrings(value) {
253
+ return Array.isArray(value)
254
+ ? value.filter((item) => typeof item === "string")
255
+ : [];
256
+ }
257
+ /**
258
+ * Render the planner's complete-but-bounded evidence handoff from committed
259
+ * typed facts. It deliberately excludes upstream response prose and artifact
260
+ * paths: Contract and Scout have already established these facts, so Plan
261
+ * should decide and commit rather than spend another model turn reading them.
262
+ */
263
+ export function renderFrontendPlanInputContext(input) {
264
+ const committedFacts = (records) => records.flatMap((record) => record.phase === "committed" && planInputRecord(record.fact)
265
+ ? [record.fact]
266
+ : []);
267
+ const contractFacts = committedFacts(input.contractRecords);
268
+ const scoutFacts = committedFacts(input.scoutRecords);
269
+ const requirements = contractFacts
270
+ .filter((fact) => fact.kind === "requirement" && fact.origin === "contract")
271
+ .map((fact) => ({
272
+ id: planInputText(fact.id, 80),
273
+ text: planInputText(fact.text),
274
+ sourceFragmentIds: planInputStrings(fact.sourceFragmentIds),
275
+ }))
276
+ .filter((fact) => fact.id !== undefined);
277
+ const targetSurface = scoutFacts
278
+ .filter((fact) => fact.kind === "target-surface" && fact.origin === "scout")
279
+ .map((fact) => ({
280
+ completeness: planInputText(fact.completeness, 32),
281
+ entrypoint: planInputText(fact.entrypoint, 240),
282
+ routeOrMount: planInputText(fact.routeOrMount, 240),
283
+ implementationPaths: planInputStrings(fact.implementationPaths),
284
+ testPaths: planInputStrings(fact.testPaths),
285
+ dataSource: planInputText(fact.dataSource),
286
+ allowedPathConflicts: planInputStrings(fact.allowedPathConflicts),
287
+ unresolvedPaths: planInputStrings(fact.unresolvedPaths),
288
+ }));
289
+ const designEvidence = scoutFacts
290
+ .filter((fact) => fact.kind === "design-evidence" && fact.origin === "scout")
291
+ .map((fact) => ({
292
+ source: planInputText(fact.source),
293
+ paths: planInputStrings(fact.paths),
294
+ conflicts: planInputStrings(fact.conflicts),
295
+ }));
296
+ const serialized = JSON.stringify({ requirements, targetSurface, designEvidence });
297
+ const bounded = serialized.length <= 12_000
298
+ ? serialized
299
+ : `${serialized.slice(0, 11_999)}…`;
300
+ return [
301
+ "<frontend_plan_input>",
302
+ "Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence.",
303
+ bounded,
304
+ "Do not read upstream artifacts, task sources, or repository files. If this input cannot support a decision, record a genuine evidence gap.",
305
+ "</frontend_plan_input>",
306
+ ].join("\n");
307
+ }
308
+ async function buildFrontendPlanInputContext(runDir) {
309
+ try {
310
+ const [contractRecords, scoutRecords] = await Promise.all([
311
+ readTypedEventStoreFromJsonl(path.join(runDir, "frontend-contract-pi", "contract-typed-facts.jsonl")),
312
+ readTypedEventStoreFromJsonl(path.join(runDir, "frontend-scout-pi", "scout-typed-facts.jsonl")),
313
+ ]);
314
+ return renderFrontendPlanInputContext({ contractRecords, scoutRecords });
315
+ }
316
+ catch {
317
+ return "";
318
+ }
172
319
  }
173
- function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics) {
320
+ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep) {
174
321
  if (attemptNumber <= 1)
175
322
  return basePrompt;
323
+ // Context overflow is a transport/session failure, not a plan-protocol
324
+ // failure. It must take precedence over the plan retry ladder so every
325
+ // retry, including a repeated plan overflow, starts from the compact
326
+ // envelope rather than re-sending the original prompt.
327
+ if (previousFailureCategory === "context-overflow") {
328
+ return [
329
+ buildContextOverflowRetryPrompt(task, basePrompt),
330
+ "",
331
+ "<retry_instruction>",
332
+ "The previous attempt exceeded the provider context window. This retry starts a fresh session: keep the evidence surface narrow.",
333
+ "Do not re-read task sources, upstream stdout, skills, or full diff artifacts that are already represented by the canonical contract/review context. Read the smallest relevant artifact first, then only the specific source file or diff fragment needed to decide a finding.",
334
+ "Do not read artifacts/diff_patch.patch in full. For frontend review, read the bounded diff summary and then at most the specific per-file diff fragments it identifies, in part order.",
335
+ "Use concise tool calls and finish the required terminal/output protocol as soon as the evidence is sufficient.",
336
+ "</retry_instruction>",
337
+ ].join("\n");
338
+ }
339
+ if (frontendPlanRetryStep === "compact-terminal-first") {
340
+ return [
341
+ basePrompt,
342
+ "",
343
+ "<retry_instruction>",
344
+ "Frontend plan retry ladder step: compact-terminal-first.",
345
+ "Do NOT rewrite the narrative. Only adopt already staged/quarantined fresh facts, fill the missing required facts, then call finalize_plan exactly once.",
346
+ "Keep the existing committed ledger intact; do not re-derive already committed facts.",
347
+ "</retry_instruction>",
348
+ ].join("\n");
349
+ }
350
+ if (frontendPlanRetryStep === "bounded-tool-only") {
351
+ return [
352
+ basePrompt,
353
+ "",
354
+ "<retry_instruction>",
355
+ "Frontend plan retry ladder step: bounded-tool-only.",
356
+ "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.",
357
+ "Do not expand scope or re-derive already committed facts.",
358
+ "</retry_instruction>",
359
+ ].join("\n");
360
+ }
361
+ if (frontendPlanRetryStep === "backup-model") {
362
+ return [
363
+ basePrompt,
364
+ "",
365
+ "<retry_instruction>",
366
+ "Frontend plan retry ladder step: backup-model.",
367
+ "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.",
368
+ "</retry_instruction>",
369
+ ].join("\n");
370
+ }
176
371
  if (previousFailureCategory === "protocol-invalid" &&
177
372
  task.outputProtocol &&
178
373
  previousProtocolReason) {
@@ -182,15 +377,64 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
182
377
  buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
183
378
  ].join("\n");
184
379
  }
380
+ if (previousFailureCategory === "review-terminal-missing") {
381
+ return [
382
+ basePrompt,
383
+ "",
384
+ "<retry_instruction>",
385
+ "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.",
386
+ "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.",
387
+ "</retry_instruction>",
388
+ ].join("\n");
389
+ }
390
+ if (previousFailureCategory === "read-burst") {
391
+ if (task.id !== FRONTEND_PLAN_NODE_ID) {
392
+ return [
393
+ basePrompt,
394
+ "",
395
+ "<retry_instruction>",
396
+ "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.",
397
+ "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.",
398
+ "</retry_instruction>",
399
+ ].join("\n");
400
+ }
401
+ return [
402
+ basePrompt,
403
+ "",
404
+ "<retry_instruction>",
405
+ "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.",
406
+ "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.",
407
+ "</retry_instruction>",
408
+ ].join("\n");
409
+ }
185
410
  if (previousFailureCategory === "invalid-output" &&
186
411
  task.structuredContractOutput &&
187
412
  previousProtocolReason) {
188
- if (isFrontendPlanArtifactSchema(task.structuredContractOutput.schemaId)) {
189
- return buildFrontendPlanArtifactRecoveryPrompt({
190
- failure: "invalid",
191
- reason: previousProtocolReason,
192
- schemaId: task.structuredContractOutput.schemaId,
193
- });
413
+ // The frontend plan node's compile authority is the committed typed
414
+ // ledger, not a fenced JSON text artifact: its retry guidance must
415
+ // direct the model to re-commit corrected record_* facts and
416
+ // finalize_plan. The legacy full-contract JSON guidance below applies
417
+ // only to nodes whose authority is still a text contract artifact.
418
+ if (task.structuredContractOutput.schemaId ===
419
+ "frontend-implementation-contract-plan-patch-v1") {
420
+ const splitGuidance = /write-set-too-large|split the task/.test(previousProtocolReason ?? "")
421
+ ? [
422
+ "",
423
+ "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.",
424
+ ]
425
+ : [];
426
+ return [
427
+ basePrompt,
428
+ "",
429
+ "<retry_instruction>",
430
+ "Previous plan ledger facts failed canonical contract validation:",
431
+ previousProtocolReason,
432
+ "Fix the reported violations by re-committing corrected record_* facts and calling finalize_plan exactly once. The committed typed ledger is the only compile authority.",
433
+ "Do not emit a fenced JSON contract, protected skeleton fields, or an openspec-citations block; narrative JSON is not validated and wastes the output budget.",
434
+ ...frontendPlanValidationRetryGuidance(previousProtocolReason),
435
+ ...splitGuidance,
436
+ "</retry_instruction>",
437
+ ].join("\n");
194
438
  }
195
439
  return [
196
440
  basePrompt,
@@ -203,15 +447,30 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
203
447
  "</retry_instruction>",
204
448
  ].join("\n");
205
449
  }
450
+ if (previousFailureCategory === "invalid-output" &&
451
+ task.id === "frontend-scout-pi") {
452
+ return [
453
+ basePrompt,
454
+ "",
455
+ "<retry_instruction>",
456
+ "The previous Scout attempt did not commit a complete, runtime-evidenced target surface.",
457
+ "Search the repository only as needed to establish the real entrypoint, implementation ownership, and applicable test path. Commit record_target_surface with completeness=complete and unresolvedPaths=[] only after at least one named target has fresh runtime evidence. If ownership truly cannot be established, record completeness=blocked with each unresolved path; do not make Plan discover it.",
458
+ "</retry_instruction>",
459
+ ].join("\n");
460
+ }
206
461
  if (previousFailureCategory === "structured-output-truncated" &&
207
462
  task.structuredContractOutput) {
208
- const schemaId = task.structuredContractOutput.schemaId;
209
- if (isFrontendPlanArtifactSchema(schemaId)) {
210
- return buildFrontendPlanArtifactRecoveryPrompt({
211
- failure: "truncated",
212
- reason: previousProtocolReason,
213
- schemaId,
214
- });
463
+ if (task.structuredContractOutput.schemaId ===
464
+ "frontend-implementation-contract-plan-patch-v1") {
465
+ return [
466
+ basePrompt,
467
+ "",
468
+ "<retry_instruction>",
469
+ "Previous attempt was truncated by the provider (stopReason=length) before the plan facts were fully committed.",
470
+ "Re-commit the missing record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority. Do NOT re-read contract/scout outputs or source files — use the facts already in context. Commit record_* facts one tool call per message, then finalize_plan immediately.",
471
+ "Do not emit a fenced JSON contract, protected skeleton fields, or an openspec-citations block; narrative JSON is not validated and wastes the output budget.",
472
+ "</retry_instruction>",
473
+ ].join("\n");
215
474
  }
216
475
  return [
217
476
  basePrompt,
@@ -284,16 +543,32 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
284
543
  "</retry_instruction>",
285
544
  ].join("\n");
286
545
  }
546
+ if (previousFailureCategory === "writer-clean-timeout") {
547
+ return [
548
+ basePrompt,
549
+ "",
550
+ "<retry_instruction>",
551
+ "Previous attempt ended in a clean provider/transport failure with zero attributed workspace writes and zero write/edit tool calls.",
552
+ "Do not invent existing implementation progress. Continue from the original task only if the workspace is still unchanged.",
553
+ "</retry_instruction>",
554
+ ].join("\n");
555
+ }
287
556
  if (previousFailureCategory !== "output-too-large") {
288
557
  return basePrompt;
289
558
  }
290
559
  if (task.outputMode === "structured-required") {
291
560
  if (task.structuredContractOutput) {
292
- if (isFrontendPlanArtifactSchema(task.structuredContractOutput.schemaId)) {
293
- return buildFrontendPlanArtifactRecoveryPrompt({
294
- failure: "too-large",
295
- schemaId: task.structuredContractOutput.schemaId,
296
- });
561
+ if (task.structuredContractOutput.schemaId ===
562
+ "frontend-implementation-contract-plan-patch-v1") {
563
+ return [
564
+ basePrompt,
565
+ "",
566
+ "<retry_instruction>",
567
+ "Previous attempt exceeded the structured output size limit.",
568
+ "Re-commit corrected record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority.",
569
+ "Do not emit a fenced JSON contract, protected skeleton fields, or an openspec-citations block; narrative JSON is not validated and wastes the output budget.",
570
+ "</retry_instruction>",
571
+ ].join("\n");
297
572
  }
298
573
  return [
299
574
  basePrompt,
@@ -326,6 +601,35 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
326
601
  "</retry_instruction>",
327
602
  ].join("\n");
328
603
  }
604
+ const PROMPT_RESTART_CANDIDATE_MAX_BYTES = 64 * 1024;
605
+ function promptRestartCandidateForAttempt(input) {
606
+ if (input.attemptPrompt === input.baseRuntimePrompt) {
607
+ return input.taskPrompt;
608
+ }
609
+ const appendedPrefix = `${input.baseRuntimePrompt}\n\n`;
610
+ if (input.attemptPrompt.startsWith(appendedPrefix)) {
611
+ return [
612
+ input.taskPrompt,
613
+ input.attemptPrompt.slice(appendedPrefix.length),
614
+ ]
615
+ .filter(Boolean)
616
+ .join("\n\n");
617
+ }
618
+ return input.attemptPrompt;
619
+ }
620
+ async function persistFailedPromptRestartCandidate(input) {
621
+ const redacted = redactSecrets(input.prompt);
622
+ const preview = truncateUtf8Preview(redacted, PROMPT_RESTART_CANDIDATE_MAX_BYTES);
623
+ await writeDagNodeJsonArtifact(input.runDir, input.nodeId, `prompt-candidate-attempt-${input.attempt}.json`, {
624
+ schemaVersion: 1,
625
+ attempt: input.attempt,
626
+ failureCategory: input.failureCategory ?? "unknown",
627
+ prompt: preview,
628
+ truncated: preview !== redacted,
629
+ maxBytes: PROMPT_RESTART_CANDIDATE_MAX_BYTES,
630
+ sha256: createHash("sha256").update(redacted).digest("hex"),
631
+ });
632
+ }
329
633
  export async function buildNodePromptWithResolvedSkillInstructions(spec, task, upstream, cwd, options) {
330
634
  const policy = resolveContextPolicy(spec);
331
635
  const skillNames = policy.resolveSkills(spec, task);
@@ -713,9 +1017,14 @@ export async function executeDagNode(input) {
713
1017
  await failSkillSnapshot(error);
714
1018
  return;
715
1019
  }
1020
+ if (isFrontendPlanLadderTask(task)) {
1021
+ const planInputContext = await buildFrontendPlanInputContext(runDir);
1022
+ if (planInputContext)
1023
+ prompt = `${prompt}\n\n${planInputContext}`;
1024
+ }
716
1025
  node.resolvedSkills = resolvedSkills;
717
1026
  await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
718
- const model = resolveModelForTask(task, spec.executorModels);
1027
+ let model = resolveModelForTask(task, spec.executorModels);
719
1028
  let thinking;
720
1029
  if (task.executor === "pi") {
721
1030
  try {
@@ -726,16 +1035,25 @@ export async function executeDagNode(input) {
726
1035
  // Invalid or missing harness preserves the previous no-override behavior.
727
1036
  }
728
1037
  }
729
- const retryPolicy = task.retryPolicy &&
1038
+ const specRetryPolicy = task.retryPolicy &&
730
1039
  (isSafeReadOnlyPiRetryCandidate(task) ||
731
1040
  isWriterEmptyDiffRetryCandidate(task) ||
732
1041
  isWriterTransportRetryCandidate(task))
733
1042
  ? task.retryPolicy
734
1043
  : undefined;
1044
+ const implicitVerifyRetryPolicy = specRetryPolicy === undefined &&
1045
+ isCanonicalFinalVerifyShellRetryCandidate(task) &&
1046
+ isTargetTemplateTransientRetryNode(task)
1047
+ ? VERIFY_SHELL_RETRY_POLICY
1048
+ : undefined;
1049
+ const retryPolicy = specRetryPolicy ?? implicitVerifyRetryPolicy;
735
1050
  const maxAttempts = retryPolicy?.maxAttempts ?? 1;
736
1051
  const attempts = [];
737
1052
  let totalAttemptWallDurationMs = 0;
738
1053
  let totalBackoffMs = 0;
1054
+ const frontendPlanLadderEnabled = isFrontendPlanLadderTask(task);
1055
+ let frontendPlanRetryStep = "normal";
1056
+ const frontendPriorFingerprints = [];
739
1057
  const livenessPolicy = resolveLivenessPolicy(spec.defaults?.livenessPolicy, task.livenessPolicy);
740
1058
  /**
741
1059
  * Attempt-fenced, throttled activity sink. Late events from a previous
@@ -773,10 +1091,25 @@ export async function executeDagNode(input) {
773
1091
  let terminalResult;
774
1092
  let previousFailureCategory;
775
1093
  let previousProtocolReason;
1094
+ let pendingStructuredArtifact;
1095
+ let lastRepairAllowlist = [];
776
1096
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
777
1097
  const attemptStartedAt = new Date().toISOString();
778
1098
  node.currentAttempt = attemptNumber;
779
1099
  node.livenessStatus = "active";
1100
+ const beforeCommittedDigest = frontendPlanLadderEnabled
1101
+ ? (await readFrontendPlanCommittedSnapshot(runDir, nodeId)).digest
1102
+ : undefined;
1103
+ if (frontendPlanLadderEnabled &&
1104
+ frontendPlanRetryStep === "backup-model") {
1105
+ model = resolveModelForTask(task, spec.executorModels);
1106
+ }
1107
+ // Capture the session-events.jsonl length BEFORE the executor appends this
1108
+ // attempt's events, so the post-attempt repair tool audit can be scoped to
1109
+ // exactly this attempt's segment (B1: earlier attempts legitimately use
1110
+ // ls/find and must not be attributed to the repair attempt).
1111
+ const attemptSessionEventsOffset = await sessionEventsByteLength(runDir, nodeId);
1112
+ let promptRestartCandidate = task.subtask_prompt;
780
1113
  let result;
781
1114
  try {
782
1115
  validateRepairArtifactGateBeforeShell({
@@ -799,12 +1132,65 @@ export async function executeDagNode(input) {
799
1132
  (acc[i.path] ??= []).push(i.detail);
800
1133
  return acc;
801
1134
  }, {});
1135
+ let attemptPrompt = buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep);
1136
+ if (attemptNumber > 1 &&
1137
+ // The plan node (frontend-plan-pi) is a typed-facts ladder task:
1138
+ // its retry is driven by the §5.1 ladder (compact-terminal-first
1139
+ // / backup-model) and its compile authority is the committed
1140
+ // ledger — never the legacy frozen fenced-JSON repair. The
1141
+ // frozen repair path below applies only to non-ladder
1142
+ // structured nodes whose output authority is a text artifact.
1143
+ !frontendPlanLadderEnabled &&
1144
+ isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId) &&
1145
+ isStructuredRepairableFailureCategory(previousFailureCategory) &&
1146
+ (await hasNonEmptyStructuredCandidate({
1147
+ runDir,
1148
+ nodeId,
1149
+ beforeAttempt: attemptNumber,
1150
+ }))) {
1151
+ const originalCanonical = task.structuredContractOutput?.schemaId ===
1152
+ "frontend-implementation-contract-revision-patch-v1"
1153
+ ? (await readFrontendCanonicalCandidate(runDir, "frontend-plan-pi"))
1154
+ : undefined;
1155
+ const frozen = await freezeStructuredOutputRepairContext({
1156
+ runDir,
1157
+ runId: state.runId,
1158
+ taskId: spec.sourceBinding?.taskId ?? task.id,
1159
+ nodeId,
1160
+ nodeAttempt: attemptNumber,
1161
+ targetSchemaId: task.structuredContractOutput.schemaId,
1162
+ skeleton: task.structuredContractOutput?.skeleton,
1163
+ originalCanonical,
1164
+ latestStopReason: previousFailureCategory === "structured-output-truncated"
1165
+ ? "length"
1166
+ : previousFailureCategory,
1167
+ });
1168
+ lastRepairAllowlist = allowedRepairReadPaths({
1169
+ runDir,
1170
+ context: frozen.context,
1171
+ contextPath: frozen.relativePath,
1172
+ });
1173
+ attemptPrompt = await buildStructuredOutputRepairPrompt({
1174
+ runDir,
1175
+ context: frozen.context,
1176
+ contextPath: frozen.relativePath,
1177
+ contextSha256: frozen.sha256,
1178
+ maxAttempts,
1179
+ schemaGuidance: frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
1180
+ previousReason: previousProtocolReason,
1181
+ });
1182
+ }
1183
+ promptRestartCandidate = promptRestartCandidateForAttempt({
1184
+ taskPrompt: task.subtask_prompt,
1185
+ baseRuntimePrompt: prompt,
1186
+ attemptPrompt,
1187
+ });
802
1188
  result = await executeNode({
803
1189
  task,
804
1190
  cwd,
805
1191
  model,
806
1192
  ...(thinking ? { thinking } : {}),
807
- prompt: buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics),
1193
+ prompt: attemptPrompt,
808
1194
  attempt: attemptNumber,
809
1195
  reportActivity,
810
1196
  timeoutMs: livenessPolicy.absoluteMaxWallClockMs,
@@ -820,6 +1206,35 @@ export async function executeDagNode(input) {
820
1206
  durationMs: 0,
821
1207
  };
822
1208
  }
1209
+ if (isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId)) {
1210
+ const rawText = canonicalNodeOutput(result);
1211
+ if (rawText.trim().length > 0) {
1212
+ await persistStructuredAttemptRaw({
1213
+ runDir,
1214
+ nodeId,
1215
+ attempt: attemptNumber,
1216
+ text: rawText,
1217
+ });
1218
+ }
1219
+ if (attemptNumber > 1 && lastRepairAllowlist.length > 0) {
1220
+ const audit = await auditRepairAttemptToolUse({
1221
+ runDir,
1222
+ nodeId,
1223
+ allowedPaths: lastRepairAllowlist,
1224
+ fromByteOffset: attemptSessionEventsOffset,
1225
+ });
1226
+ if (!audit.ok) {
1227
+ result = {
1228
+ ...result,
1229
+ ok: false,
1230
+ failureCategory: GOVERNANCE_BLOCKED_CATEGORY,
1231
+ stderr: [result.stderr, audit.reason]
1232
+ .filter(Boolean)
1233
+ .join("\n"),
1234
+ };
1235
+ }
1236
+ }
1237
+ }
823
1238
  // R0: executor ok=true still fails closed when outputProtocol is violated.
824
1239
  // Valid semantic results (e.g. VERDICT: request-revision) pass validation.
825
1240
  if (result.ok && task.outputProtocol) {
@@ -856,16 +1271,17 @@ export async function executeDagNode(input) {
856
1271
  structuredContractOutput: task.structuredContractOutput,
857
1272
  });
858
1273
  if (!contractCheck.ok) {
859
- // A provider stopReason=length means the response was cut before the
860
- // JSON contract could complete; separate it from an ordinary bad
861
- // contract so the retry switches to a JSON-only output strategy.
862
1274
  const attemptStopReason = result.stopReason;
1275
+ const failureCategory = contractCheck.classification === "governance-blocked"
1276
+ ? GOVERNANCE_BLOCKED_CATEGORY
1277
+ : attemptStopReason === "length" ||
1278
+ contractCheck.classification === "truncated"
1279
+ ? "structured-output-truncated"
1280
+ : "invalid-output";
863
1281
  result = {
864
1282
  ...result,
865
1283
  ok: false,
866
- failureCategory: attemptStopReason === "length"
867
- ? "structured-output-truncated"
868
- : "invalid-output",
1284
+ failureCategory,
869
1285
  stderr: [result.stderr, contractCheck.reason]
870
1286
  .filter(Boolean)
871
1287
  .join("\n"),
@@ -873,14 +1289,6 @@ export async function executeDagNode(input) {
873
1289
  previousProtocolReason = contractCheck.reason;
874
1290
  }
875
1291
  else {
876
- if (contractCheck.structuredArtifact) {
877
- node.structuredArtifactPath = contractCheck.structuredArtifact.path;
878
- node.structuredArtifactSha256 = contractCheck.structuredArtifact.sha256;
879
- node.structuredArtifactSchemaId = contractCheck.structuredArtifact.schemaId;
880
- }
881
- if (contractCheck.frontendPlanOutput) {
882
- node.frontendPlanOutput = contractCheck.frontendPlanOutput;
883
- }
884
1292
  if (contractCheck.normalizedText) {
885
1293
  result = {
886
1294
  ...result,
@@ -888,6 +1296,9 @@ export async function executeDagNode(input) {
888
1296
  stdout: contractCheck.normalizedText,
889
1297
  };
890
1298
  }
1299
+ if (contractCheck.artifact) {
1300
+ pendingStructuredArtifact = contractCheck.artifact;
1301
+ }
891
1302
  previousProtocolReason = undefined;
892
1303
  }
893
1304
  }
@@ -900,6 +1311,16 @@ export async function executeDagNode(input) {
900
1311
  const attemptFinishedAt = new Date().toISOString();
901
1312
  const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
902
1313
  totalAttemptWallDurationMs += attemptWallDurationMs;
1314
+ const rawFailureCategory = result.rawFailureCategory ?? result.failureCategory;
1315
+ if (task.executor === "pi" && !result.ok) {
1316
+ await persistFailedPromptRestartCandidate({
1317
+ runDir,
1318
+ nodeId,
1319
+ attempt: attemptNumber,
1320
+ failureCategory: result.failureCategory,
1321
+ prompt: promptRestartCandidate,
1322
+ });
1323
+ }
903
1324
  const attemptRecord = {
904
1325
  attempt: attemptNumber,
905
1326
  startedAt: attemptStartedAt,
@@ -911,6 +1332,7 @@ export async function executeDagNode(input) {
911
1332
  stderr: result.stderr,
912
1333
  assistantText: result.assistantText,
913
1334
  failureCategory: result.failureCategory,
1335
+ ...(rawFailureCategory ? { rawFailureCategory } : {}),
914
1336
  backend: result.backend,
915
1337
  sdkAttempted: result.sdkAttempted,
916
1338
  tokensUsed: result.tokensUsed,
@@ -925,6 +1347,60 @@ export async function executeDagNode(input) {
925
1347
  await writeDagNodeJsonArtifact(runDir, nodeId, `attempt-${attemptNumber}.json`, attemptRecord);
926
1348
  node.attempts = attempts;
927
1349
  }
1350
+ // Frontend plan retry ladder: project the 7-value protocol failure
1351
+ // reason, detect new committed facts, and escalate instead of re-running.
1352
+ if (frontendPlanLadderEnabled && !result.ok) {
1353
+ const afterSnapshot = await readFrontendPlanCommittedSnapshot(runDir, nodeId);
1354
+ const hasNewCommittedFact = beforeCommittedDigest !== undefined &&
1355
+ afterSnapshot.digest !== beforeCommittedDigest;
1356
+ const protocolReason = projectFrontendNodeProtocolFailureReason({
1357
+ failureCategory: result.failureCategory,
1358
+ stopReason: result.stopReason,
1359
+ assistantText: result.assistantText ?? result.stdout,
1360
+ hasTerminalFact: afterSnapshot.hasTerminalFact,
1361
+ });
1362
+ const fingerprint = computeNormalizedFailureFingerprint({
1363
+ failureOwner: "plan",
1364
+ protocolFailureReason: protocolReason ?? "",
1365
+ });
1366
+ const backupRoute = resolveFrontendPlanBackupRoute(protocolReason);
1367
+ const nextStep = resolveFrontendPlanRetryStep({
1368
+ currentStep: frontendPlanRetryStep,
1369
+ protocolFailureReason: protocolReason,
1370
+ hasNewCommittedFact,
1371
+ fingerprint,
1372
+ priorFingerprints: frontendPriorFingerprints,
1373
+ backupRouteAvailable: backupRoute.ok,
1374
+ });
1375
+ frontendPriorFingerprints.push(fingerprint);
1376
+ frontendPlanRetryStep = nextStep;
1377
+ if (nextStep === "unsupported-provider-capability") {
1378
+ result = {
1379
+ ...result,
1380
+ ok: false,
1381
+ failureCategory: "unsupported-provider-capability",
1382
+ stderr: [
1383
+ result.stderr,
1384
+ "frontend plan retry ladder: no compatible provider capability route for the protocol failure reason",
1385
+ ]
1386
+ .filter(Boolean)
1387
+ .join("\n"),
1388
+ };
1389
+ }
1390
+ else if (nextStep === "non-converging") {
1391
+ result = {
1392
+ ...result,
1393
+ ok: false,
1394
+ failureCategory: "non-converging",
1395
+ stderr: [
1396
+ result.stderr,
1397
+ "frontend plan retry ladder: repeated failure fingerprint with no new committed fact",
1398
+ ]
1399
+ .filter(Boolean)
1400
+ .join("\n"),
1401
+ };
1402
+ }
1403
+ }
928
1404
  // Reflect the latest attempt on the node so progress is observable,
929
1405
  // but keep node.status RUNNING while retry is still possible.
930
1406
  node.durationMs = durationBetween(node.startedAt, attemptFinishedAt);
@@ -958,13 +1434,26 @@ export async function executeDagNode(input) {
958
1434
  break;
959
1435
  const canRetry = retryPolicy !== undefined && attemptNumber < maxAttempts;
960
1436
  const retryable = retryPolicy !== undefined &&
1437
+ result.failureCategory !== GOVERNANCE_BLOCKED_CATEGORY &&
961
1438
  (isRetryablePiFailureCategory(result.failureCategory, {
962
1439
  retryCategories: retryPolicy.retryCategories,
963
1440
  }) ||
964
1441
  (result.failureCategory === "protocol-invalid" &&
965
1442
  task.outputProtocol?.retryOnInvalid === true));
966
- if (!canRetry || !retryable)
1443
+ if (!canRetry || !retryable) {
1444
+ if (!canRetry &&
1445
+ isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId) &&
1446
+ isStructuredRepairableFailureCategory(result.failureCategory)) {
1447
+ result = {
1448
+ ...result,
1449
+ failureCategory: STRUCTURED_REPAIR_EXHAUSTED_CATEGORY,
1450
+ };
1451
+ terminalResult = result;
1452
+ previousFailureCategory = result.failureCategory;
1453
+ node.failureCategory = result.failureCategory;
1454
+ }
967
1455
  break;
1456
+ }
968
1457
  const delayMs = computeBackoffDelayMs(attemptNumber + 1, retryPolicy);
969
1458
  if (delayMs > 0) {
970
1459
  const backoffStartedAt = new Date().toISOString();
@@ -1113,6 +1602,11 @@ export async function executeDagNode(input) {
1113
1602
  node.structuredArtifactSchemaId = "backend-test-result-v1";
1114
1603
  }
1115
1604
  }
1605
+ else if (pendingStructuredArtifact) {
1606
+ node.structuredArtifactPath = pendingStructuredArtifact.path;
1607
+ node.structuredArtifactSha256 = pendingStructuredArtifact.sha256;
1608
+ node.structuredArtifactSchemaId = pendingStructuredArtifact.schemaId;
1609
+ }
1116
1610
  }
1117
1611
  const finishedAt = new Date().toISOString();
1118
1612
  node.finishedAt = finishedAt;