@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +187 -92
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/recommendations.js +13 -2
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +429 -28
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/executors/dag-pi-executor.js +1645 -29
  17. package/dist/executors/pi-extension-resolver.js +233 -0
  18. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  19. package/dist/executors/pi-sdk-executor.js +180 -33
  20. package/dist/executors/shell-executor.js +1150 -171
  21. package/dist/executors/shell-write-guard.js +14 -0
  22. package/dist/shared/dag-prompt-override.js +28 -0
  23. package/dist/shared/openspec-spec.js +70 -4
  24. package/dist/shared/operator/capabilities.js +32 -0
  25. package/dist/shared/playwright-cli-command-policy.js +15 -0
  26. package/dist/shared/update/console-notifier.js +100 -0
  27. package/dist/{cli → shared}/update/npm-client.js +41 -8
  28. package/dist/{cli → shared}/update/state.js +41 -13
  29. package/dist/task/config-types.js +19 -5
  30. package/dist/task/contract/project.js +3 -0
  31. package/dist/task/contract/schema.js +2 -1
  32. package/dist/task/frontend-project-capability.js +42 -16
  33. package/dist/task/source-prepare/parse-intent.js +24 -4
  34. package/dist/task/source-prepare/prepare.js +36 -0
  35. package/dist/worker/cli.js +31 -1
  36. package/dist/worker/console/chat/pi-console-config.js +81 -10
  37. package/dist/worker/console/chat/routes.js +1 -0
  38. package/dist/worker/console/dag-execution-receipt.js +33 -0
  39. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  40. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  41. package/dist/worker/console/index.js +3 -0
  42. package/dist/worker/console/operation-store.js +226 -6
  43. package/dist/worker/console/operator-actions.js +263 -0
  44. package/dist/worker/console/operator-user-error.js +2 -2
  45. package/dist/worker/console/routes.js +43 -2
  46. package/dist/worker/console/security.js +63 -9
  47. package/dist/worker/console/server.js +151 -6
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
  81. package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  107. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  108. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  109. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  110. package/dist/worker/observe/node-input.js +74 -3
  111. package/dist/worker/observe/node-process.js +377 -0
  112. package/dist/worker/observe/routes.js +36 -3
  113. package/dist/worker/observe/shell-handler-keys.js +34 -0
  114. package/dist/worker/observe/static/api.js +66 -0
  115. package/dist/worker/observe/static/constants.js +8 -1
  116. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  117. package/dist/worker/observe/static/format.js +23 -0
  118. package/dist/worker/observe/static/markdown-render.js +20 -1
  119. package/dist/worker/observe/static/operator-chrome.js +4 -0
  120. package/dist/worker/observe/static/state.js +13 -2
  121. package/dist/worker/observe/static/styles.css +73 -0
  122. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  123. package/dist/worker/observe/static/views/dag.js +8 -1
  124. package/dist/worker/observe/static/views/failures.js +5 -2
  125. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  127. package/dist/workflows/dag/contract-output-registry.js +15 -0
  128. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  129. package/dist/workflows/dag/failure-category.js +12 -0
  130. package/dist/workflows/dag/frontend-closeout.js +221 -0
  131. package/dist/workflows/dag/frontend-design-policy.js +347 -0
  132. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  133. package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
  134. package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
  135. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  136. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  137. package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
  138. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  139. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  140. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  141. package/dist/workflows/dag/frontend-repair.js +7 -424
  142. package/dist/workflows/dag/frontend-review-context.js +195 -8
  143. package/dist/workflows/dag/frontend-risk.js +15 -2
  144. package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
  145. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  146. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  147. package/dist/workflows/dag/frontend-shape.js +425 -0
  148. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  149. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  150. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  151. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  152. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  153. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  154. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  155. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  156. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  157. package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
  158. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  159. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  160. package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
  161. package/dist/workflows/dag/frontend-writer-admission.js +260 -0
  162. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  163. package/dist/workflows/dag/init-hybrid.js +572 -500
  164. package/dist/workflows/dag/node-execution.js +307 -70
  165. package/dist/workflows/dag/prompt.js +10 -18
  166. package/dist/workflows/dag/rerun-plan.js +144 -5
  167. package/dist/workflows/dag/rerun-run.js +58 -6
  168. package/dist/workflows/dag/retry-policy.js +228 -4
  169. package/dist/workflows/dag/runner.js +314 -119
  170. package/dist/workflows/dag/scheduler.js +114 -16
  171. package/dist/workflows/dag/structured-output-repair.js +712 -0
  172. package/dist/workflows/dag/types.js +235 -23
  173. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  174. package/dist/workflows/dag/validate.js +60 -10
  175. package/docs/init-surface.manifest.json +5 -0
  176. package/docs/operations/README.md +1 -1
  177. package/docs/templates/agent-dag.schema.json +44 -0
  178. package/docs/templates/frontend-test-dag.json +8 -10
  179. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  180. package/docs/templates/init-managed-agents.md +13 -3
  181. package/harness.json +3 -3
  182. package/package.json +5 -1
  183. package/scripts/next-info.mjs +356 -0
  184. package/scripts/next-publish-gate.mjs +238 -0
  185. package/scripts/release-source-binding.mjs +251 -0
  186. package/skills/codebase-scout/SKILL.md +1 -1
  187. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  188. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  189. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  190. package/skills/frontend-implementation/SKILL.md +4 -4
  191. package/skills/frontend-implementation/references/design-spec.md +11 -7
  192. package/skills/frontend-implementation/references/node-contracts.md +20 -11
  193. package/skills/loop-agent/references/command-reference.md +11 -4
  194. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  195. package/skills/playwright-cli/SKILL.md +22 -0
  196. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  197. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  198. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  199. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  200. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  201. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  202. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import { assertTaskContractBindingFresh } from "./task-contract-binding.js";
4
4
  import { isSafeReadOnlyPiRetryCandidate, isTransportCrashedExclusiveWriter } from "./retry-policy.js";
5
5
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
6
+ import { composeDagPromptOverridePrompt, MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS, } from "../../shared/dag-prompt-override.js";
6
7
  const REVIEW_VERDICT_RECOVERY_NODE_ID = "review-verdict-recovery-pi";
7
8
  const SAFE_SHELL_GATE_KEYS = [
8
9
  "verdictGate",
@@ -159,12 +160,16 @@ function isCanonicalGeneratedVerifyShell(task) {
159
160
  return false;
160
161
  if (shell.projectGovernanceGate)
161
162
  return false;
162
- if (shell.frontendPrewriteGate)
163
+ if (shell.frontendDesignPolicy)
164
+ return false;
165
+ if (shell.frontendWriterAdmission)
163
166
  return false;
164
167
  if (shell.frontendVerificationBundle)
165
168
  return false;
166
169
  if (shell.frontendReviewContext)
167
170
  return false;
171
+ if (shell.frontendCloseout)
172
+ return false;
168
173
  if (shell.backendTestPipeline)
169
174
  return false;
170
175
  if (shell.nonZeroExitPolicy === "record")
@@ -190,12 +195,16 @@ function isSafeGateOnlyShell(task) {
190
195
  return false;
191
196
  if (shell.preset)
192
197
  return false;
193
- if (shell.frontendPrewriteGate)
198
+ if (shell.frontendDesignPolicy)
199
+ return false;
200
+ if (shell.frontendWriterAdmission)
194
201
  return false;
195
202
  if (shell.frontendVerificationBundle)
196
203
  return false;
197
204
  if (shell.frontendReviewContext)
198
205
  return false;
206
+ if (shell.frontendCloseout)
207
+ return false;
199
208
  if (shell.backendTestPipeline)
200
209
  return false;
201
210
  if (shell.verifyEvidence)
@@ -306,6 +315,9 @@ export function computeResetClosure(spec, effectiveFromNodeId) {
306
315
  ...collectReachableDescendants(spec, effectiveFromNodeId),
307
316
  ].sort();
308
317
  }
318
+ function isNeverStartedNodeStatus(status) {
319
+ return status === undefined || status === "SKIPPED" || status === "PENDING";
320
+ }
309
321
  export function assessResetClosureSafety(spec, resetNodeIds, options = {}) {
310
322
  const tasks = taskById(spec);
311
323
  const blockingNodes = [];
@@ -314,9 +326,17 @@ export function assessResetClosureSafety(spec, resetNodeIds, options = {}) {
314
326
  const task = tasks.get(nodeId);
315
327
  if (!task)
316
328
  continue;
329
+ if (options.parentNodes !== undefined &&
330
+ isNeverStartedNodeStatus(options.parentNodes[nodeId]?.status)) {
331
+ continue;
332
+ }
317
333
  if (isWriterTask(task, spec)) {
318
334
  if (nodeId === options.allowedTransportWriterNodeId)
319
335
  continue;
336
+ if (nodeId === options.allowedExclusiveShellFromNodeId &&
337
+ task.executor === "shell") {
338
+ continue;
339
+ }
320
340
  blockingNodes.push({
321
341
  nodeId,
322
342
  reasonCode: "restart-subgraph-contains-writer",
@@ -370,6 +390,100 @@ export function assessResetClosureSafety(spec, resetNodeIds, options = {}) {
370
390
  reasonCodes: [...new Set(reasonCodes)],
371
391
  };
372
392
  }
393
+ /**
394
+ * Experimental prompt-override admission (fail-closed):
395
+ * - requires the internal switch (see shared/dag-prompt-override.ts);
396
+ * - the target must be a failed (ERROR) node inside the reset closure, so an
397
+ * override can only adjust how an already-failed node retries — never a
398
+ * finished, skipped, or not-yet-started node;
399
+ * - only `subtask_prompt` semantics change: "append" keeps the frozen base
400
+ * and adds a marked addendum block, "replace" swaps the whole prompt;
401
+ * base/final sha256 are recorded for provenance and bound into planHash.
402
+ */
403
+ function assessPromptOverrides(input) {
404
+ const blockingNodes = [];
405
+ const reasonCodes = [];
406
+ const tasks = taskById(input.spec);
407
+ const resetSet = new Set(input.resetNodeIds);
408
+ const seen = new Set();
409
+ const entries = [];
410
+ if (!input.enabled) {
411
+ reasonCodes.push("prompt-override-disabled");
412
+ }
413
+ for (const override of input.overrides) {
414
+ const nodeId = override.nodeId?.trim() ?? "";
415
+ if (seen.has(nodeId)) {
416
+ blockingNodes.push({
417
+ nodeId,
418
+ reasonCode: "prompt-override-duplicate-node",
419
+ });
420
+ reasonCodes.push("prompt-override-duplicate-node");
421
+ continue;
422
+ }
423
+ seen.add(nodeId);
424
+ const task = tasks.get(nodeId);
425
+ if (!task) {
426
+ blockingNodes.push({
427
+ nodeId,
428
+ reasonCode: "prompt-override-node-missing",
429
+ });
430
+ reasonCodes.push("prompt-override-node-missing");
431
+ continue;
432
+ }
433
+ if (!resetSet.has(nodeId)) {
434
+ blockingNodes.push({
435
+ nodeId,
436
+ reasonCode: "prompt-override-outside-reset-closure",
437
+ });
438
+ reasonCodes.push("prompt-override-outside-reset-closure");
439
+ continue;
440
+ }
441
+ if (input.state.nodes[nodeId]?.status !== "ERROR") {
442
+ blockingNodes.push({
443
+ nodeId,
444
+ reasonCode: "prompt-override-node-not-failed",
445
+ });
446
+ reasonCodes.push("prompt-override-node-not-failed");
447
+ continue;
448
+ }
449
+ const mode = override.mode === "append" ? "append" : "replace";
450
+ const prompt = override.prompt ?? "";
451
+ if (!prompt.trim() ||
452
+ prompt.length > MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS) {
453
+ blockingNodes.push({
454
+ nodeId,
455
+ reasonCode: "prompt-override-invalid-prompt",
456
+ });
457
+ reasonCodes.push("prompt-override-invalid-prompt");
458
+ continue;
459
+ }
460
+ const basePromptSha256 = createHash("sha256")
461
+ .update(task.subtask_prompt, "utf8")
462
+ .digest("hex");
463
+ const finalPrompt = composeDagPromptOverridePrompt({
464
+ basePrompt: task.subtask_prompt,
465
+ text: prompt,
466
+ mode,
467
+ });
468
+ const promptSha256 = createHash("sha256")
469
+ .update(finalPrompt, "utf8")
470
+ .digest("hex");
471
+ if (promptSha256 === basePromptSha256) {
472
+ blockingNodes.push({
473
+ nodeId,
474
+ reasonCode: "prompt-override-unchanged",
475
+ });
476
+ reasonCodes.push("prompt-override-unchanged");
477
+ continue;
478
+ }
479
+ entries.push({ nodeId, mode, basePromptSha256, promptSha256 });
480
+ }
481
+ return {
482
+ entries: entries.length > 0 ? entries : undefined,
483
+ blockingNodes,
484
+ reasonCodes: [...new Set(reasonCodes)],
485
+ };
486
+ }
373
487
  export function computePlanHash(planWithoutHash) {
374
488
  const canonical = JSON.stringify(sortKeysDeep(planWithoutHash));
375
489
  return createHash("sha256").update(canonical, "utf8").digest("hex");
@@ -594,9 +708,17 @@ export async function evaluateDagRerunPlan(input) {
594
708
  blockedReasons.push("ambiguous-skipped-rewrite");
595
709
  }
596
710
  }
597
- const closureSafety = assessResetClosureSafety(input.parentSpec, resetNodeIds, transportWriterRestart && effectiveFromNodeId
598
- ? { allowedTransportWriterNodeId: effectiveFromNodeId }
599
- : {});
711
+ const closureSafety = assessResetClosureSafety(input.parentSpec, resetNodeIds, {
712
+ ...(transportWriterRestart && effectiveFromNodeId
713
+ ? { allowedTransportWriterNodeId: effectiveFromNodeId }
714
+ : {}),
715
+ parentNodes: input.parentState.nodes,
716
+ ...(effectiveFromNodeId &&
717
+ effectiveTask?.executor === "shell" &&
718
+ effectiveRecord?.status === "ERROR"
719
+ ? { allowedExclusiveShellFromNodeId: effectiveFromNodeId }
720
+ : {}),
721
+ });
600
722
  blockingNodes.push(...closureSafety.blockingNodes);
601
723
  for (const code of closureSafety.reasonCodes) {
602
724
  reasonCodes.push(code);
@@ -629,6 +751,22 @@ export async function evaluateDagRerunPlan(input) {
629
751
  blockingNodes.push({ nodeId, reasonCode: "parent-facts-invalid" });
630
752
  }
631
753
  }
754
+ let promptOverrideEntries;
755
+ if (input.promptOverrides !== undefined && input.promptOverrides.length > 0) {
756
+ const overrideAssessment = assessPromptOverrides({
757
+ spec: input.parentSpec,
758
+ state: input.parentState,
759
+ resetNodeIds,
760
+ overrides: input.promptOverrides,
761
+ enabled: input.promptOverrideEnabled === true,
762
+ });
763
+ blockingNodes.push(...overrideAssessment.blockingNodes);
764
+ for (const code of overrideAssessment.reasonCodes) {
765
+ reasonCodes.push(code);
766
+ blockedReasons.push(code);
767
+ }
768
+ promptOverrideEntries = overrideAssessment.entries;
769
+ }
632
770
  const uniqueReasonCodes = [...new Set(reasonCodes)];
633
771
  const uniqueBlockedReasons = [...new Set(blockedReasons)];
634
772
  const eligible = uniqueBlockedReasons.length === 0 &&
@@ -672,6 +810,7 @@ export async function evaluateDagRerunPlan(input) {
672
810
  parentLifecycle: input.parentLifecycle,
673
811
  workerManaged: input.workerManaged,
674
812
  }),
813
+ ...(promptOverrideEntries ? { promptOverrides: promptOverrideEntries } : {}),
675
814
  };
676
815
  return {
677
816
  ...planWithoutHash,
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { access, copyFile, mkdir, readdir, readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
5
+ import { composeDagPromptOverridePrompt, isDagRerunPromptOverrideEnabled, } from "../../shared/dag-prompt-override.js";
5
6
  import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
6
7
  import { readControllerIdentityArtifact, captureControllerIdentity } from "./controller-identity.js";
7
8
  import { DAG_RUNS_DIR, getDagRunDir, isDagNonExecutionLifecycle, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
@@ -190,6 +191,8 @@ export async function planDagRerun(input) {
190
191
  parentSpec: nonCompletedSpec,
191
192
  parentState: nonCompletedState,
192
193
  parentLifecycle,
194
+ promptOverrides: input.promptOverrides,
195
+ promptOverrideEnabled: isDagRerunPromptOverrideEnabled(),
193
196
  workerManaged: nonCompletedWorkerHints.workerManaged,
194
197
  workerAssociation: nonCompletedWorkerHints.workerManaged
195
198
  ? {
@@ -229,6 +232,8 @@ export async function planDagRerun(input) {
229
232
  parentTerminalWorkspace,
230
233
  currentWorkspace,
231
234
  skillSnapshotOk,
235
+ promptOverrides: input.promptOverrides,
236
+ promptOverrideEnabled: isDagRerunPromptOverrideEnabled(),
232
237
  workerManaged: workerHints.workerManaged,
233
238
  workerAssociation: workerHints.workerManaged
234
239
  ? {
@@ -384,15 +389,57 @@ function buildPendingNodeRecord(task) {
384
389
  ...(task.outputMode ? { outputMode: task.outputMode } : {}),
385
390
  };
386
391
  }
392
+ /**
393
+ * Stage the continuation spec with operator prompt overrides applied. The
394
+ * parent spec stays frozen; every override must match the plan entry (sha256
395
+ * of the composed final prompt and of the parent base prompt) so the
396
+ * two-phase planHash remains the single source of truth for what changed.
397
+ * "append" entries keep the frozen base and append the operator addendum in a
398
+ * marked block; "replace" entries swap the whole prompt.
399
+ */
400
+ function applyPromptOverridesToSpec(input) {
401
+ const planned = new Map((input.plan.promptOverrides ?? []).map((entry) => [entry.nodeId, entry]));
402
+ const spec = structuredClone(input.spec);
403
+ for (const override of input.overrides) {
404
+ const entry = planned.get(override.nodeId);
405
+ if (!entry) {
406
+ throw new Error(`prompt override is not present in the rerun plan: ${override.nodeId}`);
407
+ }
408
+ const task = spec.tasks.find((candidate) => candidate.id === override.nodeId);
409
+ if (!task) {
410
+ throw new Error(`prompt override node missing from spec: ${override.nodeId}`);
411
+ }
412
+ if (sha256Hex(task.subtask_prompt) !== entry.basePromptSha256) {
413
+ throw new Error(`prompt override base prompt drifted from plan: ${override.nodeId}`);
414
+ }
415
+ const finalPrompt = composeDagPromptOverridePrompt({
416
+ basePrompt: task.subtask_prompt,
417
+ text: override.prompt,
418
+ mode: override.mode === "append" ? "append" : "replace",
419
+ });
420
+ if (sha256Hex(finalPrompt) !== entry.promptSha256) {
421
+ throw new Error(`prompt override text does not match the planned sha256: ${override.nodeId}`);
422
+ }
423
+ task.subtask_prompt = finalPrompt;
424
+ }
425
+ return spec;
426
+ }
387
427
  async function stageContinuationRun(input) {
388
- const { ranks } = topoSortToRanks(input.parentSpec);
389
- const runId = await buildDagRunId(input.parentSpec, { cwd: input.cwd });
428
+ const specForRun = input.promptOverrides !== undefined && input.promptOverrides.length > 0
429
+ ? applyPromptOverridesToSpec({
430
+ spec: input.parentSpec,
431
+ plan: input.plan,
432
+ overrides: input.promptOverrides,
433
+ })
434
+ : input.parentSpec;
435
+ const { ranks } = topoSortToRanks(specForRun);
436
+ const runId = await buildDagRunId(specForRun, { cwd: input.cwd });
390
437
  const runDir = getDagRunDir(input.cwd, "active", runId);
391
438
  await prepareActiveRunDir(runDir);
392
- await writeRunSpec(runDir, input.parentSpec);
439
+ await writeRunSpec(runDir, specForRun);
393
440
  const resetSet = new Set(input.plan.resetNodeIds);
394
441
  const importedSet = new Set(input.plan.importedNodeIds);
395
- const tasksById = new Map(input.parentSpec.tasks.map((task) => [task.id, task]));
442
+ const tasksById = new Map(specForRun.tasks.map((task) => [task.id, task]));
396
443
  const importManifestNodes = [];
397
444
  const nodes = {};
398
445
  for (const nodeId of input.plan.importedNodeIds) {
@@ -433,6 +480,9 @@ async function stageContinuationRun(input) {
433
480
  reason: input.reason,
434
481
  requestId: input.requestId,
435
482
  planHash: input.plan.planHash,
483
+ ...(input.plan.promptOverrides
484
+ ? { promptOverrides: input.plan.promptOverrides }
485
+ : {}),
436
486
  createdAt,
437
487
  };
438
488
  const state = {
@@ -482,7 +532,7 @@ async function stageContinuationRun(input) {
482
532
  await writeJsonAtomic(path.join(runDir, IMPORT_MANIFEST_REL_PATH), importManifest);
483
533
  await writeJsonAtomic(path.join(runDir, RERUN_PLAN_REL_PATH), input.plan);
484
534
  await writeRunState(runDir, state);
485
- return { runId, runDir, state };
535
+ return { runId, runDir, state, spec: specForRun };
486
536
  }
487
537
  export async function executeDagRerun(input) {
488
538
  const existing = await readExistingOperatorRequest(input.cwd, input.requestId);
@@ -493,6 +543,7 @@ export async function executeDagRerun(input) {
493
543
  cwd: input.cwd,
494
544
  parentRunId: input.parentRunId,
495
545
  selectedNodeId: input.selectedNodeId,
546
+ promptOverrides: input.promptOverrides,
496
547
  });
497
548
  if (plan.planHash !== input.planHash) {
498
549
  return {
@@ -530,10 +581,11 @@ export async function executeDagRerun(input) {
530
581
  plan,
531
582
  reason: input.reason,
532
583
  requestId: input.requestId,
584
+ promptOverrides: input.promptOverrides,
533
585
  });
534
586
  const continuationOptions = {
535
587
  cwd: input.cwd,
536
- spec: parentSpec,
588
+ spec: staged.spec,
537
589
  state: staged.state,
538
590
  runDir: staged.runDir,
539
591
  ...(input.maxConcurrent !== undefined
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import { projectProtocolFailureReason, } from "./frontend-recovery-capsule.js";
3
+ import { TYPED_EVENT_FACT_KINDS } from "./frontend-typed-event-store.js";
2
4
  /**
3
5
  * Failure categories that are safe to auto-retry for read-only Pi nodes.
4
6
  *
@@ -165,6 +167,36 @@ export const WRITER_TRANSPORT_RETRY_POLICY = {
165
167
  maxDelayMs: 30000,
166
168
  retryCategories: [WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY],
167
169
  };
170
+ /**
171
+ * Implicit runtime retry for canonical final verify shells. Not a DagRetryPolicy:
172
+ * `spawn-error` / `test-timeout` are runtime command categories and must not enter
173
+ * `dagRetryPolicySchema` / `ALL_DAG_RETRY_CATEGORIES`.
174
+ */
175
+ export const VERIFY_SHELL_RETRY_CATEGORIES = [
176
+ "timeout",
177
+ "spawn-error",
178
+ "test-timeout",
179
+ ];
180
+ export const VERIFY_SHELL_RETRY_POLICY = {
181
+ maxAttempts: 2,
182
+ backoff: "exponential",
183
+ initialDelayMs: 2000,
184
+ maxDelayMs: 30000,
185
+ retryCategories: VERIFY_SHELL_RETRY_CATEGORIES,
186
+ };
187
+ export const TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE = "standard-reviewed-supervised-v1";
188
+ /**
189
+ * Target-template exclusive implement-pi transport retry. Same schema category
190
+ * as frontend (`writer-clean-timeout`) so DagSpec validation stays unchanged;
191
+ * only the delay cap is wider. Do not mutate {@link WRITER_TRANSPORT_RETRY_POLICY}.
192
+ */
193
+ export const TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY = {
194
+ maxAttempts: 2,
195
+ backoff: "exponential",
196
+ initialDelayMs: 5000,
197
+ maxDelayMs: 60000,
198
+ retryCategories: [WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY],
199
+ };
168
200
  /**
169
201
  * Backend-test generation writers: empty-diff once, plus bounded incomplete-write-set
170
202
  * recovery attempts driven by Completeness Gate (missing/broken target files).
@@ -229,15 +261,77 @@ export function isWriterEmptyDiffRetryCandidate(task) {
229
261
  task.writerOutcomePolicy?.requireChangedFiles === true);
230
262
  }
231
263
  /**
232
- * Standard exclusive implementers (implementation-outcome writers) may take a
233
- * single clean-timeout transport retry. Backend completeness writers already
234
- * have their own policy and are excluded here.
264
+ * Standard exclusive implementers (implementation-outcome and frontend-facts
265
+ * writers) may take a single clean-timeout transport retry. Backend
266
+ * completeness writers already have their own policy and are excluded here.
235
267
  */
236
268
  export function isWriterTransportRetryCandidate(task) {
237
269
  return (isExclusivePiImplementer(task) &&
238
- task.writerOutcomePolicy?.type === "implementation-outcome-v1" &&
270
+ (task.writerOutcomePolicy?.type === "implementation-outcome-v1" ||
271
+ task.writerOutcomePolicy?.type === "frontend-facts-v1") &&
239
272
  task.writerOutcomePolicy.requireChangedFiles !== true);
240
273
  }
274
+ /**
275
+ * Canonical standard exclusive `implement-pi` only. Frontend designers and
276
+ * completeness writers stay on the existing timeout-only transport candidate.
277
+ */
278
+ export function isTargetTemplateImplementPi(task) {
279
+ return isWriterTransportRetryCandidate(task) && task.id === "implement-pi";
280
+ }
281
+ export function hasTargetTemplateTransientRetryProfile(task) {
282
+ return (task?.transientRetryProfile === TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE);
283
+ }
284
+ /**
285
+ * Dual-condition eligibility: canonical shape AND generated-path stamp.
286
+ * Custom / other-template nodes that only match id or shape stay ineligible.
287
+ */
288
+ export function isTargetTemplateTransientRetryNode(task) {
289
+ if (!hasTargetTemplateTransientRetryProfile(task))
290
+ return false;
291
+ return (isCanonicalFinalVerifyShellRetryCandidate(task) ||
292
+ isTargetTemplateImplementPi(task));
293
+ }
294
+ /**
295
+ * Shape-based eligibility for implicit canonical final verify-shell retry.
296
+ * Does not consult node id or a template field.
297
+ */
298
+ export function isCanonicalFinalVerifyShellRetryCandidate(task) {
299
+ if (task.executor !== "shell")
300
+ return false;
301
+ if (task.role !== "verifier")
302
+ return false;
303
+ if (task.writePolicy !== undefined &&
304
+ task.writePolicy !== "read-only" &&
305
+ task.writePolicy !== "none") {
306
+ return false;
307
+ }
308
+ if ((task.writeSet?.length ?? 0) > 0)
309
+ return false;
310
+ const shell = task.shell;
311
+ if (!shell || (shell.commands?.length ?? 0) === 0)
312
+ return false;
313
+ const evidence = shell.verifyEvidence;
314
+ if (!evidence)
315
+ return false;
316
+ if (evidence.phase !== "final")
317
+ return false;
318
+ if (evidence.quota !== "full")
319
+ return false;
320
+ if (evidence.commandSource !== "adapter")
321
+ return false;
322
+ if (evidence.finalFullRequired !== true)
323
+ return false;
324
+ if (shell.frontendPrewriteGate ||
325
+ shell.frontendVerificationBundle ||
326
+ shell.frontendBrowserToolPreflight ||
327
+ shell.backendTestPipeline ||
328
+ shell.jacocoCoverage ||
329
+ shell.repairArtifactGate ||
330
+ shell.verdictGate) {
331
+ return false;
332
+ }
333
+ return true;
334
+ }
241
335
  export function isSafeReadOnlyPiRetryCandidate(task) {
242
336
  if (task.executor !== "pi")
243
337
  return false;
@@ -279,3 +373,133 @@ export function computeBackoffDelayMs(attemptNumber, policy) {
279
373
  const raw = policy.initialDelayMs * 2 ** exponent;
280
374
  return Math.min(raw, policy.maxDelayMs);
281
375
  }
376
+ /**
377
+ * A+B (AC-001/AC-005): project raw executor signals to the canonical 7-value
378
+ * protocol failure reason. Specific signals win first, then the existing
379
+ * alias/enum passthrough; unknown signals fail closed to `undefined`.
380
+ */
381
+ export function projectFrontendNodeProtocolFailureReason(input) {
382
+ // 1. length stop reason without a terminal → length-before-terminal.
383
+ if (input.stopReason === "length" && input.hasTerminalFact === false) {
384
+ return "length-before-terminal";
385
+ }
386
+ // 2. timeout with reasoning-side evidence → reasoning-timeout.
387
+ if (input.failureCategory === "timeout" &&
388
+ /(?:reasoning|thinking|thought)/i.test(input.assistantText ?? "")) {
389
+ return "reasoning-timeout";
390
+ }
391
+ // 3. empty output without a terminal → provider-ended-without-terminal.
392
+ if (input.failureCategory === "empty-output" &&
393
+ input.hasTerminalFact === false) {
394
+ return "provider-ended-without-terminal";
395
+ }
396
+ // 4. Alias/enum passthrough; unknown fails closed to undefined.
397
+ return projectProtocolFailureReason(input.failureCategory ?? "");
398
+ }
399
+ export const FRONTEND_PLAN_LADDER = [
400
+ "normal",
401
+ "compact-terminal-first",
402
+ "bounded-tool-only",
403
+ "backup-model",
404
+ ];
405
+ /**
406
+ * Map a 7-value protocol failure reason to the closest provider capability
407
+ * fact kind for backup-model routing. `unsupported-tool-capability` is a
408
+ * protocol reason, not a provider capability kind, so it is deliberately NOT
409
+ * a valid route (fail-closed to `unsupported-provider-capability`).
410
+ */
411
+ export function frontendPlanProtocolReasonToCapabilityKind(reason) {
412
+ switch (reason) {
413
+ case "length-before-terminal":
414
+ return "length-before-terminal";
415
+ case "reasoning-timeout":
416
+ return "reasoning-timeout";
417
+ case "provider-ended-without-terminal":
418
+ return "provider-ended-without-terminal";
419
+ case "terminal-event-missing":
420
+ case "terminal-conflict":
421
+ return "terminal-commit";
422
+ case "typed-fact-invalid":
423
+ return "read-only-fact-channel";
424
+ case "unsupported-tool-capability":
425
+ return "unsupported-tool-capability";
426
+ default:
427
+ return undefined;
428
+ }
429
+ }
430
+ /**
431
+ * A+B (AC-003): resolve a backup-model route against the frozen seven-kind
432
+ * provider capability catalog (the exact set `routeFrontendProviderCapability`
433
+ * consults). `unsupported-tool-capability` is a protocol reason, not a
434
+ * provider capability kind, so it (and any unmapped reason) fails closed to
435
+ * `unsupported-provider-capability` — never swapped for the 7-value reason.
436
+ */
437
+ export function resolveFrontendPlanBackupRoute(reason) {
438
+ const factKind = frontendPlanProtocolReasonToCapabilityKind(reason);
439
+ if (!factKind) {
440
+ return { ok: false, reason: "unsupported-provider-capability" };
441
+ }
442
+ return TYPED_EVENT_FACT_KINDS.includes(factKind)
443
+ ? { ok: true, factKind }
444
+ : { ok: false, reason: "unsupported-provider-capability" };
445
+ }
446
+ /**
447
+ * A+B (AC-002): deterministic ladder resolution. Reason-mandated upgrades
448
+ * (length → compact-terminal-first; terminal/timeout/empty → bounded-tool-only)
449
+ * never regress below the current rung. A repeated failure fingerprint with no
450
+ * new committed fact escalates one rung instead of re-running the same step;
451
+ * at `backup-model` that becomes `non-converging`, and an unavailable backup
452
+ * provider route fails closed to `unsupported-provider-capability`.
453
+ */
454
+ export function resolveFrontendPlanRetryStep(input) {
455
+ if (input.currentStep === "non-converging" ||
456
+ input.currentStep === "unsupported-provider-capability") {
457
+ return input.currentStep;
458
+ }
459
+ const currentIndex = FRONTEND_PLAN_LADDER.indexOf(input.currentStep);
460
+ const reasonStep = reasonMandatedFrontendPlanRetryStep(input.protocolFailureReason);
461
+ const reasonIndex = reasonStep
462
+ ? FRONTEND_PLAN_LADDER.indexOf(reasonStep)
463
+ : -1;
464
+ let nextIndex = Math.max(currentIndex, reasonIndex);
465
+ const repeated = input.priorFingerprints.includes(input.fingerprint);
466
+ if (repeated && !input.hasNewCommittedFact) {
467
+ nextIndex = Math.max(nextIndex, currentIndex + 1);
468
+ }
469
+ const nextStep = nextIndex >= FRONTEND_PLAN_LADDER.length
470
+ ? "non-converging"
471
+ : (FRONTEND_PLAN_LADDER[nextIndex] ?? "non-converging");
472
+ if (nextStep === "backup-model" && input.backupRouteAvailable === false) {
473
+ return "unsupported-provider-capability";
474
+ }
475
+ return nextStep;
476
+ }
477
+ function reasonMandatedFrontendPlanRetryStep(reason) {
478
+ switch (reason) {
479
+ case "length-before-terminal":
480
+ return "compact-terminal-first";
481
+ case "terminal-event-missing":
482
+ case "reasoning-timeout":
483
+ case "provider-ended-without-terminal":
484
+ return "bounded-tool-only";
485
+ default:
486
+ return undefined;
487
+ }
488
+ }
489
+ /**
490
+ * Opt-in retry policy mounted on the frontend-plan-pi node. maxAttempts=5 is
491
+ * the schema cap, matching the four-rung ladder plus one non-converging stop.
492
+ * `invalid-output` (typed-fact schema violations, e.g. verification targets
493
+ * referencing undeclared UI states) retries at the `normal` rung with the
494
+ * contractCheck reason injected via `<retry_instruction>` (§5.1 ladder).
495
+ */
496
+ export const FRONTEND_PLAN_LADDER_RETRY_POLICY = {
497
+ maxAttempts: 5,
498
+ backoff: "exponential",
499
+ initialDelayMs: 2000,
500
+ maxDelayMs: 30000,
501
+ retryCategories: [
502
+ ...DEFAULT_DAG_RETRY_CATEGORIES,
503
+ STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
504
+ ],
505
+ };