@tea-agent/loop-agent 0.42.0-next.9 → 0.43.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +65 -59
  2. package/dist/application/dag/run-dag.js +3 -2
  3. package/dist/application/task-lifecycle/advance.js +9 -4
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/program.js +1 -1
  6. package/dist/commands/dag-rerun-task.js +2 -0
  7. package/dist/commands/task-source-prepare.js +3 -1
  8. package/dist/executors/dag-pi-executor.js +1514 -175
  9. package/dist/executors/pi-extension-resolver.js +14 -2
  10. package/dist/executors/shell-executor.js +131 -54
  11. package/dist/shared/dag-failure-category.js +6 -0
  12. package/dist/task/contract/apply.js +36 -2
  13. package/dist/task/source-prepare/parse-intent.js +7 -0
  14. package/dist/worker/console/chat/chat-event-store.js +4 -2
  15. package/dist/worker/console/chat/pi-runtime.js +45 -2
  16. package/dist/worker/console/chat/resource-loader.js +4 -1
  17. package/dist/worker/console/chat/routes.js +28 -12
  18. package/dist/worker/console/chat/sdd-data-alignment.js +13 -2
  19. package/dist/worker/console/chat/session-store.js +5 -1
  20. package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
  21. package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
  22. package/dist/worker/console/chat/subagents/index.js +5 -0
  23. package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
  24. package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
  25. package/dist/worker/console/chat/subagents/types.js +13 -0
  26. package/dist/worker/console/chat/tool-preview.js +10 -0
  27. package/dist/worker/console/chat/tools.js +11 -1
  28. package/dist/worker/console/chat/turn-process.js +1 -0
  29. package/dist/worker/console/interview/tools.js +1 -0
  30. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-B0Q0nClf.js} +1 -1
  31. package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-DCPjC19G.js} +1 -1
  32. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-CP5n9jmG.js} +1 -1
  33. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-CmaWi_Wg.js} +1 -1
  34. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Ct5tcMfZ.js} +1 -1
  35. package/dist/worker/console/static/assets/channel-DAS07MdS.js +1 -0
  36. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-C0osm1Zf.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-CDWviED7.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-mtSk1-j7.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-D5J9PHC7.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-Begg4WAa.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-sF1AEy7T.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-BqbsWXZb.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BKJrPcJ6.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-CcwNMRho.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-BEJi3iIs.js} +1 -1
  46. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +1 -0
  47. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +1 -0
  48. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-DRLgTvVu.js} +1 -1
  49. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-BoAVG3cs.js} +1 -1
  50. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-Bzol06hR.js} +1 -1
  51. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-UmliJM77.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BWLcJkFo.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CgApOm-O.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-Bh3RgTLs.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-Dsh-D5nE.js} +1 -1
  56. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-Dkbbpcpb.js} +1 -1
  57. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-3EBaA3t3.js} +1 -1
  58. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-Bq2Dc5ok.js} +1 -1
  59. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-De7y55ha.js} +1 -1
  60. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-fLiDbQRh.js} +1 -1
  61. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-Du617mp1.js} +1 -1
  62. package/dist/worker/console/static/assets/index-C0O48S_P.js +449 -0
  63. package/dist/worker/console/static/assets/index-CzKf4U8P.css +1 -0
  64. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-BwmlF-XP.js} +1 -1
  65. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-zsGtS59u.js} +1 -1
  66. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-DTTPTe3f.js} +1 -1
  67. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BkXg9aP-.js} +1 -1
  68. package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-7U2ue5IE.js} +1 -1
  69. package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-BUuGHmWO.js} +5 -5
  70. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-7q6fX0Sv.js} +1 -1
  71. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-CkQPJN55.js} +1 -1
  72. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-CCal_tfX.js} +1 -1
  73. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js} +1 -1
  74. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-CJmww7D-.js} +1 -1
  75. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-D6ldWJEv.js} +1 -1
  76. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-VGh33I9e.js} +1 -1
  77. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CCkDrIjU.js} +1 -1
  78. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-NC8f5Otb.js} +1 -1
  79. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-_92ezZdF.js} +1 -1
  80. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +1 -0
  81. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-BdUCGtbP.js} +2 -2
  82. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +8 -0
  83. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-CdD1H8ia.js} +1 -1
  84. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-Dkdk3oFo.js} +1 -1
  85. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-CKu2uPPY.js} +1 -1
  86. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-ClqAnuG8.js} +1 -1
  87. package/dist/worker/console/static/index.html +2 -2
  88. package/dist/worker/console/static-src/chat-view-types.js +1 -1
  89. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +23 -0
  90. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +32 -0
  91. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
  92. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
  93. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  94. package/dist/worker/console/static-src/operator-chat/useChatStream.js +30 -14
  95. package/dist/worker/console/static-src/operator-chat/useChatThread.js +133 -5
  96. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  97. package/dist/worker/observe/static/constants.js +22 -22
  98. package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
  99. package/dist/worker/observe/static/dag-history-labels.js +1 -0
  100. package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
  101. package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
  102. package/dist/worker/observe/static/dag-node-purpose.js +10 -10
  103. package/dist/worker/observe/static/index.html +4 -4
  104. package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
  105. package/dist/worker/observe/static/relations.js +5 -5
  106. package/dist/worker/observe/static/styles.css +171 -0
  107. package/dist/worker/observe/static/views/dag-graph.js +1 -1
  108. package/dist/worker/observe/static/views/dag-inspector.js +329 -174
  109. package/dist/worker/observe/static/views/dag.js +16 -12
  110. package/dist/worker/observe/static/views/session-timeline.js +5 -3
  111. package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
  112. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  113. package/dist/workflows/dag/frontend-contract-facts.js +130 -0
  114. package/dist/workflows/dag/frontend-design-policy.js +101 -16
  115. package/dist/workflows/dag/frontend-implementation-contract.js +410 -41
  116. package/dist/workflows/dag/frontend-plan-render.js +13 -2
  117. package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
  118. package/dist/workflows/dag/frontend-recovery-controller.js +25 -1
  119. package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
  120. package/dist/workflows/dag/frontend-recovery-run.js +142 -16
  121. package/dist/workflows/dag/frontend-risk.js +2 -0
  122. package/dist/workflows/dag/frontend-shadow-dual-write.js +17 -1
  123. package/dist/workflows/dag/frontend-shape.js +16 -6
  124. package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
  125. package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
  126. package/dist/workflows/dag/frontend-verification-trace.js +38 -4
  127. package/dist/workflows/dag/frontend-writer-admission.js +2 -2
  128. package/dist/workflows/dag/init-hybrid.js +57 -28
  129. package/dist/workflows/dag/node-execution.js +201 -85
  130. package/dist/workflows/dag/recovery-lease.js +106 -16
  131. package/dist/workflows/dag/rerun-feedback.js +1 -0
  132. package/dist/workflows/dag/rerun-plan.js +90 -4
  133. package/dist/workflows/dag/rerun-run.js +28 -6
  134. package/dist/workflows/dag/rerun-task.js +221 -18
  135. package/dist/workflows/dag/retry-policy.js +27 -10
  136. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  137. package/dist/workflows/dag/runner.js +220 -18
  138. package/dist/workflows/dag/structured-output-repair.js +4 -1
  139. package/dist/workflows/dag/types.js +4 -3
  140. package/dist/workflows/dag/validate.js +10 -8
  141. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  142. package/docs/operations/README.md +1 -0
  143. package/docs/templates/agent-dag.schema.json +2 -2
  144. package/docs/templates/backend-test-dag.json +6 -4
  145. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  146. package/harness.json +1 -1
  147. package/package.json +4 -5
  148. package/skills/frontend-plan/SKILL.md +14 -1
  149. package/skills/frontend-plan/references/decision-contract.md +101 -5
  150. package/skills/frontend-plan/references/design-decisions.md +32 -0
  151. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
  152. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
  153. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
  154. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
  155. package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
  156. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
  157. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { z } from "zod";
4
4
  import { assertTaskContractBindingFresh } from "./task-contract-binding.js";
5
+ import { workspaceDriftedPaths, } from "./workspace-checkpoint.js";
5
6
  import { isSafeReadOnlyPiRetryCandidate, isTransportCrashedExclusiveWriter } from "./retry-policy.js";
6
7
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
7
8
  import { composeDagPromptOverridePrompt, MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS, } from "../../shared/dag-prompt-override.js";
@@ -1110,6 +1111,46 @@ function deriveSuggestedAction(input) {
1110
1111
  }
1111
1112
  return "manual";
1112
1113
  }
1114
+ /**
1115
+ * Gitignore-style-lite matcher for readSet/writeSet path patterns in scoped
1116
+ * workspace-drift evaluation: literal paths match exactly; `**` spans path
1117
+ * segments (a trailing `/**` also matches when nothing follows), `*` stays
1118
+ * within one segment, `?` is one non-separator char.
1119
+ */
1120
+ function pathMatchesWorkspacePattern(pathValue, pattern) {
1121
+ const normalizedPath = pathValue.replace(/\\/g, "/").replace(/^\.\//, "");
1122
+ const trimmed = pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "");
1123
+ if (!trimmed || trimmed === "/")
1124
+ return false;
1125
+ if (trimmed === normalizedPath)
1126
+ return true;
1127
+ let source = "";
1128
+ for (let index = 0; index < trimmed.length; index += 1) {
1129
+ const char = trimmed[index];
1130
+ if (char === "*") {
1131
+ if (trimmed[index + 1] === "*") {
1132
+ if (trimmed[index + 2] === "/") {
1133
+ source += "(?:.*/)?";
1134
+ index += 2;
1135
+ }
1136
+ else {
1137
+ source += ".*";
1138
+ index += 1;
1139
+ }
1140
+ }
1141
+ else {
1142
+ source += "[^/]*";
1143
+ }
1144
+ }
1145
+ else if (char === "?") {
1146
+ source += "[^/]";
1147
+ }
1148
+ else {
1149
+ source += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1150
+ }
1151
+ }
1152
+ return new RegExp(`^${source}$`).test(normalizedPath);
1153
+ }
1113
1154
  function deriveRisk(reasonCodes, resetCount) {
1114
1155
  if (reasonCodes.includes("restart-subgraph-contains-writer") ||
1115
1156
  reasonCodes.includes("restart-subgraph-contains-unsafe-shell") ||
@@ -1213,7 +1254,14 @@ export async function evaluateDagRerunPlan(input) {
1213
1254
  blockedReasons.push("parent-lifecycle-ineligible");
1214
1255
  }
1215
1256
  else if (input.parentState.status !== "failed" &&
1216
- input.parentState.status !== "partial_failed") {
1257
+ input.parentState.status !== "partial_failed" &&
1258
+ // A superseded parent is a terminal operator judgment about the run as
1259
+ // a whole, not about its deterministic read-only compute: its FINISHED
1260
+ // upstream nodes are still valid fact donors for the imported set, and
1261
+ // every unsafe case (writers in the reset subgraph, decision gates,
1262
+ // stale checkpoints, unresolved ERROR nodes outside the closure) keeps
1263
+ // its own dedicated block below.
1264
+ input.parentState.status !== "superseded") {
1217
1265
  reasonCodes.push("parent-lifecycle-ineligible");
1218
1266
  blockedReasons.push("parent-lifecycle-ineligible");
1219
1267
  }
@@ -1241,8 +1289,36 @@ export async function evaluateDagRerunPlan(input) {
1241
1289
  else if (input.currentWorkspace?.fingerprint &&
1242
1290
  input.parentTerminalWorkspace.fingerprint !==
1243
1291
  input.currentWorkspace.fingerprint) {
1244
- reasonCodes.push("workspace-drift");
1245
- blockedReasons.push("workspace-drift");
1292
+ // Whole-fingerprint drift is refined to content-level paths: the rerun
1293
+ // re-derives (readSet) and rewrites (writeSet) the reset subgraph's own
1294
+ // paths, so drift confined to those paths is recomputed by the rerun
1295
+ // itself and must not force a full standalone rerun. Drift anywhere
1296
+ // else — especially inputs of imported fact-reuse nodes — keeps the
1297
+ // strict block. Indeterminate checkpoints fail closed.
1298
+ const driftedPaths = workspaceDriftedPaths(input.parentTerminalWorkspace, input.currentWorkspace);
1299
+ const importedReadPaths = importedNodeIds.flatMap((nodeId) => {
1300
+ const task = tasksByIdForPlan.get(nodeId);
1301
+ return task?.readSet ?? [];
1302
+ });
1303
+ const scopedDrift = driftedPaths !== undefined &&
1304
+ driftedPaths.length > 0 &&
1305
+ driftedPaths.every((driftPath) => !importedReadPaths.some((pattern) => pathMatchesWorkspacePattern(driftPath, pattern)) &&
1306
+ resetNodeIds.some((nodeId) => {
1307
+ const task = tasksByIdForPlan.get(nodeId);
1308
+ if (!task)
1309
+ return false;
1310
+ return [
1311
+ ...(task.writeSet ?? []),
1312
+ ...(task.readSet ?? []),
1313
+ ].some((pattern) => pathMatchesWorkspacePattern(driftPath, pattern));
1314
+ }));
1315
+ if (scopedDrift) {
1316
+ reasonCodes.push("workspace-drift-scoped");
1317
+ }
1318
+ else {
1319
+ reasonCodes.push("workspace-drift");
1320
+ blockedReasons.push("workspace-drift");
1321
+ }
1246
1322
  }
1247
1323
  else if (!input.currentWorkspace?.fingerprint) {
1248
1324
  reasonCodes.push("workspace-checkpoint-missing");
@@ -1293,8 +1369,18 @@ export async function evaluateDagRerunPlan(input) {
1293
1369
  }
1294
1370
  for (const nodeId of importedNodeIds) {
1295
1371
  const record = input.parentState.nodes[nodeId];
1296
- if (!record)
1372
+ if (!record) {
1373
+ reasonCodes.push("parent-facts-invalid");
1374
+ blockedReasons.push("parent-facts-invalid");
1375
+ blockingNodes.push({ nodeId, reasonCode: "parent-facts-invalid" });
1297
1376
  continue;
1377
+ }
1378
+ if (record.status === "PENDING" || record.status === "RUNNING") {
1379
+ reasonCodes.push("parent-facts-invalid");
1380
+ blockedReasons.push("parent-facts-invalid");
1381
+ blockingNodes.push({ nodeId, reasonCode: "parent-facts-invalid" });
1382
+ continue;
1383
+ }
1298
1384
  if (record.status === "ERROR") {
1299
1385
  reasonCodes.push("parent-facts-invalid");
1300
1386
  blockedReasons.push("parent-facts-invalid");
@@ -88,11 +88,18 @@ const operatorRequestEnvelopeSchema = z
88
88
  })
89
89
  .strict();
90
90
  async function readExistingOperatorRequest(cwd, requestId) {
91
+ const requestPath = operatorRequestPath(cwd, requestId);
91
92
  try {
92
- const raw = JSON.parse(await readFile(operatorRequestPath(cwd, requestId), "utf-8"));
93
+ const raw = JSON.parse(await readFile(requestPath, "utf-8"));
93
94
  const parsed = operatorRequestEnvelopeSchema.safeParse(raw);
94
- if (!parsed.success)
95
- return undefined;
95
+ if (!parsed.success) {
96
+ throw new Error("REQUEST_ID_REUSE_CONFLICT: persisted operator request envelope is invalid or belongs to a different request kind");
97
+ }
98
+ if (parsed.data.requestId !== requestId ||
99
+ parsed.data.parentRunId !== parsed.data.result.plan.parentRunId ||
100
+ parsed.data.newRunId !== parsed.data.result.newRunId) {
101
+ throw new Error("REQUEST_ID_REUSE_CONFLICT: persisted operator request envelope binding mismatch");
102
+ }
96
103
  const result = parsed.data.result;
97
104
  return {
98
105
  ...result,
@@ -100,10 +107,18 @@ async function readExistingOperatorRequest(cwd, requestId) {
100
107
  idempotentReplay: true,
101
108
  };
102
109
  }
103
- catch {
104
- // not found or unreadable
110
+ catch (error) {
111
+ if (error &&
112
+ typeof error === "object" &&
113
+ "code" in error &&
114
+ error.code === "ENOENT") {
115
+ return undefined;
116
+ }
117
+ if (error instanceof Error && error.message.startsWith("REQUEST_ID_REUSE_CONFLICT:")) {
118
+ throw error;
119
+ }
120
+ throw new Error(`REQUEST_ID_REUSE_CONFLICT: invalid persisted operator request envelope: ${error instanceof Error ? error.message : String(error)}`);
105
121
  }
106
- return undefined;
107
122
  }
108
123
  /**
109
124
  * Idempotent replay of a previously persisted operator request, if any.
@@ -769,6 +784,13 @@ async function stageContinuationRun(input) {
769
784
  if (!parentRecord) {
770
785
  throw new Error(`parent node record missing for imported node: ${nodeId}`);
771
786
  }
787
+ if (parentRecord.status !== "FINISHED" &&
788
+ !(parentRecord.status === "SKIPPED" &&
789
+ (parentRecord.skippedReason?.includes("condition") ||
790
+ parentRecord.skippedReason?.includes("runIf") ||
791
+ parentRecord.skippedReason?.includes("run-if")))) {
792
+ throw new Error(`parent node is not a settled fact donor for imported node: ${nodeId}`);
793
+ }
772
794
  const { manifestNode, importedRecord } = await importNodeFacts({
773
795
  parentRunDir: input.parentRunDir,
774
796
  newRunDir: runDir,
@@ -1,6 +1,10 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
2
3
  import path from "node:path";
3
4
  import { generateTaskDagUseCase } from "../../application/dag/generate-task-dag.js";
5
+ import { buildDefaultFrontendRecoveryDeps, } from "../../application/dag/run-dag.js";
6
+ import { runDagContinuation, } from "./runner.js";
7
+ import { stageRecoveryChild } from "./frontend-recovery-run.js";
4
8
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
5
9
  import { DAG_RUNS_DIR, isTerminalDagRunStatus, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
6
10
  import { deriveDagRerunFeedback } from "./rerun-feedback.js";
@@ -9,7 +13,7 @@ import { parseDagSpec, } from "./types.js";
9
13
  export const RERUN_TASK_LINEAGE_REL_PATH = ".runtime/rerun-task-lineage.json";
10
14
  export function parseDagRerunTaskArgs(args) {
11
15
  if (args.length === 0) {
12
- throw new Error("usage: dag rerun-task --run-id <id> --reason <text> --request-id <key> [--profile auto|minimal|standard|reviewed|supervised] [--task-id <id>] [--json]");
16
+ throw new Error("usage: dag rerun-task --run-id <id> --reason <text> --request-id <key> [--profile auto|minimal|standard|reviewed|supervised] [--task-id <id>] [--frontend-recovery --recovery-operation-id <id>] [--json]");
13
17
  }
14
18
  let parentRunId;
15
19
  let reason;
@@ -18,6 +22,8 @@ export function parseDagRerunTaskArgs(args) {
18
22
  let profileExplicit = false;
19
23
  let taskIdOverride;
20
24
  let execute = true;
25
+ let frontendRecovery = false;
26
+ let recoveryOperationId;
21
27
  for (let index = 0; index < args.length; index += 1) {
22
28
  const arg = args[index];
23
29
  if (arg === "--run-id")
@@ -48,6 +54,12 @@ export function parseDagRerunTaskArgs(args) {
48
54
  continue;
49
55
  else if (arg === "--no-execute")
50
56
  execute = false;
57
+ else if (arg === "--frontend-recovery")
58
+ frontendRecovery = true;
59
+ else if (arg === "--recovery-operation-id")
60
+ recoveryOperationId = args[++index];
61
+ else if (arg.startsWith("--recovery-operation-id="))
62
+ recoveryOperationId = arg.slice("--recovery-operation-id=".length);
51
63
  else if (arg.startsWith("-"))
52
64
  throw new Error(`unknown dag rerun-task flag: ${arg}`);
53
65
  else
@@ -70,6 +82,48 @@ export function parseDagRerunTaskArgs(args) {
70
82
  profileExplicit,
71
83
  ...(taskIdOverride?.trim() ? { taskIdOverride: taskIdOverride.trim() } : {}),
72
84
  execute,
85
+ frontendRecovery,
86
+ ...(recoveryOperationId ? { recoveryOperationId } : {}),
87
+ };
88
+ }
89
+ const frontendRecoveryDescriptorPath = (runDir, requestId) => path.join(runDir, ".runtime", "frontend-recovery", `${requestId}.json`);
90
+ async function readFrontendRecoveryDescriptor(runDir, parentRunId, requestId) {
91
+ const raw = JSON.parse(await readFile(frontendRecoveryDescriptorPath(runDir, requestId), "utf8"));
92
+ const plan = raw.plan;
93
+ if (raw.schemaVersion !== 1 ||
94
+ raw.parentRunId !== parentRunId ||
95
+ raw.requestId !== requestId ||
96
+ typeof raw.failureSource !== "string" ||
97
+ typeof raw.recoveryMode !== "string" ||
98
+ typeof raw.planHash !== "string" ||
99
+ !plan ||
100
+ plan.schemaVersion !== 1 ||
101
+ plan.parentRunId !== parentRunId ||
102
+ plan.requestId !== requestId ||
103
+ !Array.isArray(plan.resetNodeIds) ||
104
+ !Array.isArray(plan.importedNodeIds)) {
105
+ throw new Error("frontend recovery descriptor is invalid or unbound");
106
+ }
107
+ const expectedHash = createHash("sha256")
108
+ .update(JSON.stringify(plan))
109
+ .digest("hex");
110
+ if (raw.planHash !== expectedHash) {
111
+ throw new Error("frontend recovery descriptor plan hash mismatch");
112
+ }
113
+ return raw;
114
+ }
115
+ function buildFrontendRecoveryIntent(input) {
116
+ return {
117
+ schemaVersion: 1,
118
+ phase: "child-staging",
119
+ requestId: input.requestId,
120
+ recoveryRootRunId: input.parentRecovery?.recoveryRootRunId ?? input.parentRunId,
121
+ parentRunId: input.parentRunId,
122
+ attemptId: input.requestId,
123
+ attemptIndex: input.parentRecovery?.attemptIndex ?? 0,
124
+ continuationCount: input.parentRecovery?.continuationCount ?? 0,
125
+ failureSource: input.failureSource,
126
+ revision: 0,
73
127
  };
74
128
  }
75
129
  function parseDagRerunTaskProfile(value) {
@@ -136,6 +190,21 @@ function resolveTaskIdForRerun(spec, taskIdOverride) {
136
190
  function operatorRequestPath(cwd, requestId) {
137
191
  return path.join(cwd, DAG_RUNS_DIR, ".operator-requests", `${requestId}.json`);
138
192
  }
193
+ function computeRerunTaskRequestHash(input) {
194
+ return createHash("sha256")
195
+ .update(JSON.stringify({
196
+ parentRunId: input.parentRunId,
197
+ reason: input.reason,
198
+ requestId: input.requestId,
199
+ profile: input.profile ?? "auto",
200
+ profileExplicit: input.profileExplicit ?? false,
201
+ taskIdOverride: input.taskIdOverride ?? null,
202
+ execute: input.execute ?? true,
203
+ frontendRecovery: input.frontendRecovery ?? false,
204
+ recoveryOperationId: input.recoveryOperationId ?? null,
205
+ }))
206
+ .digest("hex");
207
+ }
139
208
  async function fileExists(filePath) {
140
209
  try {
141
210
  await access(filePath);
@@ -185,23 +254,45 @@ export async function assessGeneratedOutputConflict(spec, cwd) {
185
254
  }
186
255
  return conflictPaths;
187
256
  }
188
- async function readExistingOperatorRequest(cwd, requestId) {
257
+ async function readExistingOperatorRequest(cwd, requestId, expected) {
258
+ let raw;
189
259
  try {
190
- const raw = JSON.parse(await readFile(operatorRequestPath(cwd, requestId), "utf-8"));
191
- if (raw.result && typeof raw.result === "object") {
192
- return { ...raw.result, idempotentReplay: true };
193
- }
194
- }
195
- catch {
196
- // not found or unreadable
260
+ raw = JSON.parse(await readFile(operatorRequestPath(cwd, requestId), "utf-8"));
197
261
  }
198
- return undefined;
262
+ catch (error) {
263
+ if (error &&
264
+ typeof error === "object" &&
265
+ "code" in error &&
266
+ error.code === "ENOENT") {
267
+ return undefined;
268
+ }
269
+ throw new Error(`invalid persisted rerun-task operator request: ${error instanceof Error ? error.message : String(error)}`);
270
+ }
271
+ if (raw.schemaVersion !== 1 ||
272
+ raw.kind !== "standalone-task-rerun-request" ||
273
+ raw.requestId !== requestId) {
274
+ throw new Error("REQUEST_ID_REUSE_CONFLICT: operator request id belongs to a different request kind");
275
+ }
276
+ if (raw.parentRunId !== expected.parentRunId ||
277
+ raw.requestHash !== expected.requestHash) {
278
+ throw new Error("REQUEST_ID_REUSE_CONFLICT: rerun-task operator request payload drifted");
279
+ }
280
+ const result = raw.result;
281
+ if (!result ||
282
+ typeof result !== "object" ||
283
+ Array.isArray(result) ||
284
+ result.parentRunId !== expected.parentRunId ||
285
+ result.requestId !== requestId) {
286
+ throw new Error("invalid persisted rerun-task operator request result");
287
+ }
288
+ return { ...result, idempotentReplay: true };
199
289
  }
200
- async function persistOperatorRequest(cwd, result) {
290
+ async function persistOperatorRequest(cwd, result, requestHash) {
201
291
  await writeJsonAtomic(operatorRequestPath(cwd, result.requestId), {
202
292
  schemaVersion: 1,
203
293
  kind: "standalone-task-rerun-request",
204
294
  parentRunId: result.parentRunId,
295
+ requestHash,
205
296
  newRunId: result.newRunId,
206
297
  taskId: result.taskId,
207
298
  reason: result.reason,
@@ -264,8 +355,106 @@ function extractNewRunId(generateResult) {
264
355
  const runSummary = extractRunSummary(generateResult);
265
356
  return runSummary?.runId;
266
357
  }
358
+ async function executeFrontendRecoveryContinuation(input) {
359
+ const located = await locateDagRun(input.cwd, input.parentRunId);
360
+ if (!located || located.lifecycle !== "active") {
361
+ throw new Error(`frontend recovery parent run must remain active: ${input.parentRunId}`);
362
+ }
363
+ const lease = await acquireDagRecoveryLease({
364
+ cwd: input.cwd,
365
+ parentRunId: input.parentRunId,
366
+ requestId: input.requestId,
367
+ kind: "standalone-task-rerun",
368
+ });
369
+ if (lease.kind !== "held" ||
370
+ lease.lease.kind !== "auto-frontend-recovery" ||
371
+ lease.lease.requestId !== input.requestId ||
372
+ !input.recoveryOperationId ||
373
+ lease.lease.executionRef !== input.recoveryOperationId) {
374
+ return blockedResult({
375
+ parentRunId: input.parentRunId,
376
+ reason: input.reason,
377
+ requestId: input.requestId,
378
+ blockedReason: "recovery-lease-held",
379
+ note: "frontend recovery operation is not the owner of the parent recovery lease",
380
+ });
381
+ }
382
+ const descriptor = await readFrontendRecoveryDescriptor(located.runDir, input.parentRunId, input.requestId);
383
+ const parentState = await readDagRunState(located.runDir);
384
+ const staged = await stageRecoveryChild({
385
+ cwd: input.cwd,
386
+ parentRunId: input.parentRunId,
387
+ recoveryIntent: buildFrontendRecoveryIntent({
388
+ parentRunId: input.parentRunId,
389
+ requestId: input.requestId,
390
+ failureSource: descriptor.failureSource,
391
+ parentRecovery: parentState.frontendRecoveryState,
392
+ }),
393
+ recoveryPlan: descriptor.plan,
394
+ });
395
+ const locatedChild = await locateDagRun(input.cwd, staged.childRunId);
396
+ if (!locatedChild || locatedChild.runDir !== staged.childRunDir) {
397
+ throw new Error(`frontend recovery child lifecycle drift: ${staged.childRunId}`);
398
+ }
399
+ const childSpec = await readDagRunSpec(locatedChild.runDir);
400
+ const childState = await readDagRunState(locatedChild.runDir);
401
+ let runSummary;
402
+ if (locatedChild.lifecycle === "completed" ||
403
+ locatedChild.lifecycle === "paused") {
404
+ runSummary = {
405
+ title: childState.title,
406
+ runId: childState.runId,
407
+ status: childState.status,
408
+ ranks: childState.ranks,
409
+ nodes: childState.nodes,
410
+ runDir: locatedChild.runDir,
411
+ };
412
+ }
413
+ else {
414
+ const childRecovery = childSpec.tasks.some((task) => task.id === "frontend-implement-pi")
415
+ ? input.recovery ?? await buildDefaultFrontendRecoveryDeps(input.cwd)
416
+ : undefined;
417
+ runSummary = await runDagContinuation({
418
+ cwd: input.cwd,
419
+ spec: childSpec,
420
+ state: childState,
421
+ runDir: locatedChild.runDir,
422
+ ...(childRecovery ? { recovery: childRecovery } : {}),
423
+ });
424
+ }
425
+ const settledChild = await locateDagRun(input.cwd, staged.childRunId);
426
+ const settledChildState = settledChild
427
+ ? await readDagRunState(settledChild.runDir)
428
+ : childState;
429
+ const feedback = summarizeFeedback(childSpec.rerunFeedback, "no-unresolved-verify-review-feedback");
430
+ const result = {
431
+ ok: runSummary.status === "finished" ||
432
+ settledChildState.frontendRecoveryResult?.outcome === "recovered",
433
+ parentRunId: input.parentRunId,
434
+ newRunId: staged.childRunId,
435
+ taskId: childSpec.taskContractBinding?.taskId ??
436
+ childSpec.sourceBinding?.taskId,
437
+ reason: input.reason,
438
+ requestId: input.requestId,
439
+ mode: "generate+validate+execute",
440
+ feedback,
441
+ runSummary,
442
+ };
443
+ await writeRerunTaskLineage(input.cwd, staged.childRunId, {
444
+ parentRunId: input.parentRunId,
445
+ reason: input.reason,
446
+ requestId: input.requestId,
447
+ feedback,
448
+ });
449
+ await persistOperatorRequest(input.cwd, result, input.requestHash);
450
+ return result;
451
+ }
267
452
  export async function rerunStandaloneTask(input) {
268
- const existing = await readExistingOperatorRequest(input.cwd, input.requestId);
453
+ const requestHash = computeRerunTaskRequestHash(input);
454
+ const existing = await readExistingOperatorRequest(input.cwd, input.requestId, {
455
+ parentRunId: input.parentRunId,
456
+ requestHash,
457
+ });
269
458
  if (existing) {
270
459
  return existing;
271
460
  }
@@ -298,6 +487,17 @@ export async function rerunStandaloneTask(input) {
298
487
  note: eligibility.note,
299
488
  });
300
489
  }
490
+ if (input.frontendRecovery) {
491
+ return executeFrontendRecoveryContinuation({
492
+ cwd: input.cwd,
493
+ parentRunId: input.parentRunId,
494
+ reason: input.reason,
495
+ requestId: input.requestId,
496
+ requestHash,
497
+ recoveryOperationId: input.recoveryOperationId,
498
+ recovery: input.recovery,
499
+ });
500
+ }
301
501
  let parentFeedback;
302
502
  let missingFeedbackReason = "no-unresolved-verify-review-feedback";
303
503
  try {
@@ -318,6 +518,9 @@ export async function rerunStandaloneTask(input) {
318
518
  // eligible full rerun.
319
519
  missingFeedbackReason = "feedback-unavailable";
320
520
  }
521
+ if (state.terminalReason === "design-request-changes" && !parentFeedback) {
522
+ throw new Error("frontend design recovery feedback unavailable: no typed findings were found");
523
+ }
321
524
  const execute = input.execute ?? true;
322
525
  let effectiveFeedback;
323
526
  let expectedRerunFeedbackDigest;
@@ -381,10 +584,7 @@ export async function rerunStandaloneTask(input) {
381
584
  kind: "standalone-task-rerun",
382
585
  })
383
586
  : undefined;
384
- const nestedAutoRecoveryLease = recoveryLease?.kind === "held" &&
385
- recoveryLease.lease.kind === "auto-frontend-recovery" &&
386
- recoveryLease.lease.requestId === input.requestId;
387
- if (recoveryLease?.kind === "held" && !nestedAutoRecoveryLease) {
587
+ if (recoveryLease?.kind === "held") {
388
588
  return blockedResult({
389
589
  parentRunId: input.parentRunId,
390
590
  reason: input.reason,
@@ -455,10 +655,13 @@ export async function rerunStandaloneTask(input) {
455
655
  ...(runSummary ? { runSummary } : {}),
456
656
  };
457
657
  if (await fileExists(operatorRequestPath(input.cwd, input.requestId))) {
458
- const replay = await readExistingOperatorRequest(input.cwd, input.requestId);
658
+ const replay = await readExistingOperatorRequest(input.cwd, input.requestId, {
659
+ parentRunId: input.parentRunId,
660
+ requestHash,
661
+ });
459
662
  if (replay)
460
663
  return replay;
461
664
  }
462
- await persistOperatorRequest(input.cwd, result);
665
+ await persistOperatorRequest(input.cwd, result, requestHash);
463
666
  return result;
464
667
  }
@@ -7,8 +7,8 @@ import { TYPED_EVENT_FACT_KINDS } from "./frontend-typed-event-store.js";
7
7
  * is re-exported here unchanged so retry-policy stays the single import
8
8
  * surface for retry policies and helpers.
9
9
  */
10
- export { ALL_DAG_RETRY_CATEGORIES, CONTEXT_OVERFLOW_RETRY_CATEGORY, DEFAULT_DAG_RETRY_CATEGORIES, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, PROTOCOL_AWARE_DAG_RETRY_CATEGORIES, PROTOCOL_INVALID_RETRY_CATEGORY, READ_BURST_RETRY_CATEGORY, REVIEW_TERMINAL_MISSING_RETRY_CATEGORY, STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY, STRUCTURED_OUTPUT_RETRY_CATEGORY, STRUCTURED_OUTPUT_TRUNCATED_RETRY_CATEGORY, STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, dagRetryBackoffSchema, dagRetryCategorySchema, dagRetryPolicySchema, } from "./dag-retry-schema.js";
11
- import { CONTEXT_OVERFLOW_RETRY_CATEGORY, DEFAULT_DAG_RETRY_CATEGORIES, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, PROTOCOL_AWARE_DAG_RETRY_CATEGORIES, READ_BURST_RETRY_CATEGORY, REVIEW_TERMINAL_MISSING_RETRY_CATEGORY, STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY, STRUCTURED_OUTPUT_RETRY_CATEGORY, STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "./dag-retry-schema.js";
10
+ export { ALL_DAG_RETRY_CATEGORIES, CONTEXT_OVERFLOW_RETRY_CATEGORY, DEFAULT_DAG_RETRY_CATEGORIES, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, OUTPUT_LIMIT_RETRY_CATEGORY, PROTOCOL_AWARE_DAG_RETRY_CATEGORIES, PROTOCOL_INVALID_RETRY_CATEGORY, READ_BURST_RETRY_CATEGORY, REVIEW_TERMINAL_MISSING_RETRY_CATEGORY, STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY, STRUCTURED_OUTPUT_RETRY_CATEGORY, STRUCTURED_OUTPUT_TRUNCATED_RETRY_CATEGORY, STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, dagRetryBackoffSchema, dagRetryCategorySchema, dagRetryPolicySchema, } from "./dag-retry-schema.js";
11
+ import { CONTEXT_OVERFLOW_RETRY_CATEGORY, DEFAULT_DAG_RETRY_CATEGORIES, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, OUTPUT_LIMIT_RETRY_CATEGORY, PROTOCOL_AWARE_DAG_RETRY_CATEGORIES, READ_BURST_RETRY_CATEGORY, REVIEW_TERMINAL_MISSING_RETRY_CATEGORY, STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY, STRUCTURED_OUTPUT_RETRY_CATEGORY, STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "./dag-retry-schema.js";
12
12
  const RETRY_SAFE_PI_ROLES = new Set([
13
13
  "planner",
14
14
  "scout",
@@ -19,7 +19,7 @@ const RETRY_SAFE_PI_ROLES = new Set([
19
19
  ]);
20
20
  /**
21
21
  * The default retry policy applied to safe generated read-only Pi nodes.
22
- * Total attempts: 3, exponential backoff with cap.
22
+ * Total attempts: 5, exponential backoff with cap.
23
23
  *
24
24
  * Includes `context-overflow`: a read-only node that blew the context window
25
25
  * (400 request too large, e.g. a review node accumulating too many reads) is a
@@ -29,7 +29,7 @@ const RETRY_SAFE_PI_ROLES = new Set([
29
29
  * design-review/review fell straight to ERROR without a retry attempt.
30
30
  */
31
31
  export const DEFAULT_READ_ONLY_PI_RETRY_POLICY = {
32
- maxAttempts: 3,
32
+ maxAttempts: 5,
33
33
  backoff: "exponential",
34
34
  initialDelayMs: 2000,
35
35
  maxDelayMs: 30000,
@@ -80,6 +80,17 @@ export const PLANNER_OUTPUT_LIMIT_RETRY_POLICY = {
80
80
  CONTEXT_OVERFLOW_RETRY_CATEGORY,
81
81
  ],
82
82
  };
83
+ /** Backend-test N2 emits a mandatory run-owned Markdown protocol, so both
84
+ * provider length truncation and deterministic protocol validation failures
85
+ * are safe producer-local retries. */
86
+ export const BACKEND_TEST_MD_PLAN_RETRY_POLICY = {
87
+ ...STRUCTURED_REQUIRED_PI_RETRY_POLICY,
88
+ retryCategories: [
89
+ ...STRUCTURED_REQUIRED_DAG_RETRY_CATEGORIES,
90
+ READ_BURST_RETRY_CATEGORY,
91
+ CONTEXT_OVERFLOW_RETRY_CATEGORY,
92
+ ],
93
+ };
83
94
  /**
84
95
  * Default retry for reviewer / recovery nodes that declare outputProtocol.
85
96
  * Includes protocol-invalid so missing VERDICT lines are corrected in-node.
@@ -114,7 +125,10 @@ export const WRITER_EMPTY_DIFF_RETRY_POLICY = {
114
125
  backoff: "exponential",
115
126
  initialDelayMs: 2000,
116
127
  maxDelayMs: 30000,
117
- retryCategories: [WRITER_EMPTY_DIFF_RETRY_CATEGORY],
128
+ retryCategories: [
129
+ WRITER_EMPTY_DIFF_RETRY_CATEGORY,
130
+ OUTPUT_LIMIT_RETRY_CATEGORY,
131
+ ],
118
132
  };
119
133
  /**
120
134
  * Bounded transport retry for standard exclusive implementers when a provider
@@ -129,6 +143,7 @@ export const WRITER_TRANSPORT_RETRY_POLICY = {
129
143
  retryCategories: [
130
144
  WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
131
145
  CONTEXT_OVERFLOW_RETRY_CATEGORY,
146
+ OUTPUT_LIMIT_RETRY_CATEGORY,
132
147
  ],
133
148
  };
134
149
  /**
@@ -164,6 +179,7 @@ export const TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY = {
164
179
  retryCategories: [
165
180
  WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
166
181
  CONTEXT_OVERFLOW_RETRY_CATEGORY,
182
+ OUTPUT_LIMIT_RETRY_CATEGORY,
167
183
  ],
168
184
  };
169
185
  /**
@@ -171,13 +187,14 @@ export const TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY = {
171
187
  * recovery attempts driven by Completeness Gate (missing/broken target files).
172
188
  */
173
189
  export const BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY = {
174
- maxAttempts: 3,
190
+ maxAttempts: 5,
175
191
  backoff: "exponential",
176
192
  initialDelayMs: 2000,
177
193
  maxDelayMs: 30000,
178
194
  retryCategories: [
179
195
  WRITER_EMPTY_DIFF_RETRY_CATEGORY,
180
196
  INCOMPLETE_WRITE_SET_RETRY_CATEGORY,
197
+ OUTPUT_LIMIT_RETRY_CATEGORY,
181
198
  ],
182
199
  };
183
200
  /** Markdown shard writers use one full attempt plus at most one bounded binding repair. */
@@ -461,9 +478,9 @@ function reasonMandatedFrontendPlanRetryStep(reason) {
461
478
  }
462
479
  }
463
480
  /**
464
- * Opt-in retry policy mounted on the frontend-plan-pi node. maxAttempts=3
465
- * bounds plan retry cost: the four-rung ladder is only partially traversable
466
- * before the run fails non-converging (rungs: normal → bounded-tool-only →
481
+ * Opt-in retry policy mounted on the frontend-plan-pi node. maxAttempts=5
482
+ * lets the producer validate and repair each generated increment while still
483
+ * bounding the four-rung ladder (normal → bounded-tool-only →
467
484
  * compact-terminal-first → backup-model). `invalid-output` (typed-fact schema
468
485
  * violations, e.g. verification targets referencing undeclared UI states)
469
486
  * retries at the `normal` rung with the contractCheck reason injected via
@@ -474,7 +491,7 @@ export const FRONTEND_PLAN_LADDER_RETRY_POLICY = {
474
491
  // incremental record_* calls; dense call bursts hit provider rate limits
475
492
  // whose windows exceed the old 30s cap. Longer backoff gives the limit
476
493
  // window time to expire before the next attempt.
477
- maxAttempts: 3,
494
+ maxAttempts: 5,
478
495
  backoff: "exponential",
479
496
  initialDelayMs: 5000,
480
497
  maxDelayMs: 60000,