@tea-agent/loop-agent 0.42.0-next.15 → 0.42.0-next.17

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 (97) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/application/task-lifecycle/advance.js +9 -4
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/commands/task-source-prepare.js +3 -1
  5. package/dist/executors/dag-pi-executor.js +825 -65
  6. package/dist/executors/shell-executor.js +103 -35
  7. package/dist/shared/dag-failure-category.js +6 -0
  8. package/dist/task/contract/apply.js +36 -2
  9. package/dist/task/source-prepare/parse-intent.js +7 -0
  10. package/dist/worker/console/chat/chat-event-store.js +4 -2
  11. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CgXb0EVO.js → abnfDiagram-N423BO3Z-B0Q0nClf.js} +1 -1
  12. package/dist/worker/console/static/assets/{arc-DN59MZqN.js → arc-DCPjC19G.js} +1 -1
  13. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BUk3sWpn.js → architectureDiagram-T3A2C74G-CP5n9jmG.js} +1 -1
  14. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-IH-cPBFE.js → blockDiagram-VBNYF7ZC-CmaWi_Wg.js} +1 -1
  15. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BJWf1nGo.js → c4Diagram-5PPSVZJV-Ct5tcMfZ.js} +1 -1
  16. package/dist/worker/console/static/assets/channel-DAS07MdS.js +1 -0
  17. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BCPb5H0y.js → chunk-2GRJ4B5K-C0osm1Zf.js} +1 -1
  18. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Cfs3jeW2.js → chunk-2Q5K7J3B-CDWviED7.js} +1 -1
  19. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BhQI1B_T.js → chunk-5RXB4S5H-mtSk1-j7.js} +1 -1
  20. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-Cc_m4gyV.js → chunk-5VM5RSS4-D5J9PHC7.js} +1 -1
  21. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-JPqDS3IV.js → chunk-6Q2QTUOP-Begg4WAa.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-AeGX6EAg.js → chunk-GF5L2VYU-sF1AEy7T.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-pEyTjekV.js → chunk-JWPE2WC7-BqbsWXZb.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bi5UopKd.js → chunk-KBJHAD2P-BKJrPcJ6.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-FkGFJxgQ.js → chunk-RYQCIY6F-CcwNMRho.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-D-7LGrV2.js → chunk-XXDRQBXY-BEJi3iIs.js} +1 -1
  27. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +1 -0
  28. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +1 -0
  29. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CCEDNDaX.js → cose-bilkent-JH36ORCC-DRLgTvVu.js} +1 -1
  30. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DDd-t7fm.js → cynefin-VYW2F7L2-BoAVG3cs.js} +1 -1
  31. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-LWe4Ikzr.js → cynefinDiagram-MW4NZA55-Bzol06hR.js} +1 -1
  32. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BBCZScc-.js → dagre-VZM6K2ZE-UmliJM77.js} +1 -1
  33. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B6liRVig.js → diagram-7IWD3JNH-BWLcJkFo.js} +1 -1
  34. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DO_hIrpW.js → diagram-B4RE2ZJO-CgApOm-O.js} +1 -1
  35. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-dmCy93uR.js → diagram-LBJQPF4R-Bh3RgTLs.js} +1 -1
  36. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6mFTIxP.js → diagram-Q27KOJAE-Dsh-D5nE.js} +1 -1
  37. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DCteVUfY.js → diagram-UB23O5K3-Dkbbpcpb.js} +1 -1
  38. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DTv530VK.js → ebnfDiagram-BXEA7PRR-3EBaA3t3.js} +1 -1
  39. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DCXDMxCr.js → erDiagram-JOGREHBK-Bq2Dc5ok.js} +1 -1
  40. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BwEBLOJh.js → flowDiagram-UKHOOZJN-De7y55ha.js} +1 -1
  41. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Ck-Vymjm.js → ganttDiagram-PKOTCBZU-fLiDbQRh.js} +1 -1
  42. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Hqs6X_3L.js → gitGraphDiagram-DS77QQ5N-Du617mp1.js} +1 -1
  43. package/dist/worker/console/static/assets/{index-BmMi-Bve.js → index-C0O48S_P.js} +50 -50
  44. package/dist/worker/console/static/assets/index-CzKf4U8P.css +1 -0
  45. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Cjc6M9eg.js → infoDiagram-6WML65LV-BwmlF-XP.js} +1 -1
  46. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CPchYZMl.js → ishikawaDiagram-WSZJBQD7-zsGtS59u.js} +1 -1
  47. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BABdJNNC.js → journeyDiagram-NVQOT4AX-DTTPTe3f.js} +1 -1
  48. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-1oXhbM4j.js → kanban-definition-27J2QSJJ-BkXg9aP-.js} +1 -1
  49. package/dist/worker/console/static/assets/{linear-PTmQ9LkV.js → linear-7U2ue5IE.js} +1 -1
  50. package/dist/worker/console/static/assets/{mermaid.core-B6Lxil0V.js → mermaid.core-BUuGHmWO.js} +5 -5
  51. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BUqjNGEw.js → mindmap-definition-FAOFIHXS-7q6fX0Sv.js} +1 -1
  52. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-8D1N-orJ.js → pegDiagram-VL7TDLO6-CkQPJN55.js} +1 -1
  53. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BlPMN9d9.js → pieDiagram-7S7Q4E2Y-CCal_tfX.js} +1 -1
  54. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BIL5YDes.js → quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js} +1 -1
  55. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Dqmil3ie.js → railroadDiagram-AXF67PYL-CJmww7D-.js} +1 -1
  56. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C_-Dp4mH.js → requirementDiagram-LRYGKXZP-D6ldWJEv.js} +1 -1
  57. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C0s5-bQS.js → sankeyDiagram-W5VNT64P-VGh33I9e.js} +1 -1
  58. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DiVCHPfi.js → sequenceDiagram-SI44F4Z6-CCkDrIjU.js} +1 -1
  59. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CxQ9WVvx.js → sizeCapture-X5ZJPWSS-NC8f5Otb.js} +1 -1
  60. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D9mFHA-Y.js → stateDiagram-OKZ733FA-_92ezZdF.js} +1 -1
  61. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +1 -0
  62. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DDnC5l-M.js → swimlanes-SLNWSIFB-BdUCGtbP.js} +2 -2
  63. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +8 -0
  64. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C8QySPC-.js → timeline-definition-Z64GVDOM-CdD1H8ia.js} +1 -1
  65. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BeYdDiz6.js → vennDiagram-T6HMQDX7-Dkdk3oFo.js} +1 -1
  66. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-D8hjFWLH.js → wardleyDiagram-T6FBY63Y-CKu2uPPY.js} +1 -1
  67. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqG_JGK.js → xychartDiagram-ELKLHX3M-ClqAnuG8.js} +1 -1
  68. package/dist/worker/console/static/index.html +2 -2
  69. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  70. package/dist/workflows/dag/frontend-design-policy.js +1 -1
  71. package/dist/workflows/dag/frontend-risk.js +2 -0
  72. package/dist/workflows/dag/frontend-shadow-dual-write.js +16 -1
  73. package/dist/workflows/dag/frontend-shape.js +16 -6
  74. package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
  75. package/dist/workflows/dag/frontend-verification-trace.js +32 -0
  76. package/dist/workflows/dag/init-hybrid.js +9 -10
  77. package/dist/workflows/dag/node-execution.js +124 -72
  78. package/dist/workflows/dag/rerun-feedback.js +1 -0
  79. package/dist/workflows/dag/rerun-plan.js +90 -4
  80. package/dist/workflows/dag/rerun-run.js +7 -0
  81. package/dist/workflows/dag/retry-policy.js +16 -10
  82. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  83. package/dist/workflows/dag/runner.js +67 -7
  84. package/dist/workflows/dag/structured-output-repair.js +4 -1
  85. package/dist/workflows/dag/validate.js +10 -8
  86. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  87. package/docs/templates/agent-dag.schema.json +2 -2
  88. package/package.json +1 -1
  89. package/skills/frontend-plan/SKILL.md +6 -1
  90. package/skills/frontend-plan/references/decision-contract.md +69 -18
  91. package/skills/frontend-plan/references/design-decisions.md +32 -0
  92. package/dist/worker/console/static/assets/channel-i1DjpDIw.js +0 -1
  93. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgDRdE1V.js +0 -1
  94. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgDRdE1V.js +0 -1
  95. package/dist/worker/console/static/assets/index-Dh0Gh4X0.css +0 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqXQxfFC.js +0 -1
  97. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CXwWNRJl.js +0 -8
@@ -2559,8 +2559,8 @@ function resolveFrontendMockContextBlock(sources) {
2559
2559
  if (mode === "not-required") {
2560
2560
  parts.push("Generation-time evidence does not require Mock. The assessment must still use contract/scout evidence: select not-needed when Mock is intentionally skipped, or select a safe Mock strategy if project evidence supports one.");
2561
2561
  if (frontendMockStrategyMustBeNotNeeded(sources)) {
2562
- parts.push('Auto mode has no confirmed project Mock capability or no deterministic Mock verification command. The structured contract must set mockApi.strategy to "not-needed". Do not add Mock files or dependencies; keep the real request path as default and record any unproved backend behavior as Real Integration Gap.');
2563
- parts.push('HARD CONSTRAINT (frozen at generation time): this DAG allows only mockApi.strategy "not-needed"; the prewrite gate rejects any other strategy. If project governance (openspec / ai_workspace / decision records, e.g. a DEC rule requiring native) demands Mock-backed verification, that is a generation-time contract gap, not a plan-revision defect: declare frontendMock.verifyCommands (or policy: "required") in task.json and regenerate the DAG. Do not emit any mockApi.strategy outside the allowlist and do not add Mock files or dependencies within this run.');
2562
+ parts.push('Auto mode has no confirmed project Mock capability or no deterministic Mock verification command. The structured contract must set mockApi.strategy to "not-needed". Keep the real request path as the default, record any unproved backend behavior as Real Integration Gap, and do not add Mock files or dependencies within this run.');
2563
+ parts.push('HARD CONSTRAINT (frozen at generation time): this DAG allows only mockApi.strategy "not-needed"; the prewrite gate rejects any other strategy. If project governance (openspec / ai_workspace / decision records, e.g. a DEC rule requiring native) demands Mock-backed verification, that is a generation-time contract gap, not a plan-revision defect: declare frontendMock.verifyCommands (or policy: "required") in task.json and regenerate the DAG.');
2564
2564
  }
2565
2565
  }
2566
2566
  if (mode === "blocked") {
@@ -3226,9 +3226,7 @@ async function buildFrontendHybridDagFromTask(sources) {
3226
3226
  "Reference frozen commands ONLY by commandId (record_plan_verification_target entry.commandId). The runtime resolves mode and label; never invent a mode or type.",
3227
3227
  ...frontendVerifyDirectory.map((entry) => ` - ${entry.commandId} [${entry.mode}]: ${JSON.stringify(entry.label)}`),
3228
3228
  `- Static command source: ${staticVerifyEvidence.commandSource}`,
3229
- ...staticVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
3230
3229
  `- Behavior command source: ${behaviorVerifyEvidence.commandSource}`,
3231
- ...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
3232
3230
  ].join("\n");
3233
3231
  const advisories = [];
3234
3232
  if (!hasDeclaredFrontendVerification &&
@@ -3394,7 +3392,10 @@ async function buildFrontendHybridDagFromTask(sources) {
3394
3392
  depends_on: ["frontend-contract-pi", "frontend-scout-pi"],
3395
3393
  role: "planner",
3396
3394
  executor: "pi",
3397
- complexity: "MED",
3395
+ // Small topology has already proven a concentrated, no-remote scope;
3396
+ // keep its bounded plan on the LOW model tier. Standard/High retain
3397
+ // MED for broader contract-to-surface decisions.
3398
+ complexity: frontendTaskShape.shape === "small" ? "LOW" : "MED",
3398
3399
  writePolicy: "read-only",
3399
3400
  retryPolicy: FRONTEND_PLAN_LADDER_RETRY_POLICY,
3400
3401
  allowedPaths: readOnlyPaths,
@@ -3503,18 +3504,16 @@ async function buildFrontendHybridDagFromTask(sources) {
3503
3504
  "Your authoritative terminal verdict is exactly one committed typed tool call: approve_design or request_design_changes. Call exactly one of them; after calling one, do not call the other.",
3504
3505
  "request_design_changes must carry a typed issueCategory, at least one evidenceRef, and non-empty findings.",
3505
3506
  "Your verdict is consumed as deterministic data input by frontend-writer-admission-shell. approve_design permits admission; request_design_changes blocks writer admission until a recovery plan incorporates every Critical/Important finding.",
3506
- "Request design changes when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence. A static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint. not-needed otherwise requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case the plan must preserve the real request path and record the Real Integration Gap.",
3507
+ "Request design changes when the Mock strategy is blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence; a static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint; not-needed requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists, in which case the plan must preserve the real request path and record the Real Integration Gap.",
3507
3508
  "Also request design changes for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
3508
- "Component selection conformance is a hard blocking condition: request_design_changes when the frontend spec (component/theme/rule.components bucket) already defines a component for a purpose but the plan selects another or self-invents one without a declared deviation; when uiComponentChoices is missing/empty for UI-visible work while the frozen component/theme bucket is non-empty; when a decision=specified specReference.path is missing a ledger OpenSpec reference or successful read event; or when a decision=new component lacks a traceable task-source/PRD specReference. A PRD reference for decision=new is not an OpenSpec citation and must not be rejected merely for lacking an OpenSpec read event.",
3509
- "For uiComponentChoices, purpose is the stable coverage key and must match interaction.name or uiState.name. Responsibility is expressed by the matched expectedBehavior plus rationale; you must not reject it merely for matching an interaction or component identifier.",
3510
- "You must NOT make authoritative assertions about the execution result of frozen verification commands (typecheck/test/build/lint/etc.). Predicting that a command will necessarily pass or fail, or declaring an acceptance criterion unreachable on that basis, is out of your authority: command results are deterministically established by frontend-verify-shell. Any concern about verification feasibility must be recorded only as a non-blocking verification concern in findings (severity must not be Critical, and it must never be the sole fatal basis for request_design_changes). Only semantic design defects (component selection, state flow, interaction contract, or conflicts with the specification) may be Critical. A pure command-will-fail prediction must not be classified as contract-requirement-gap.",
3509
+ "Component selection conformance is a hard blocking condition: request_design_changes when the frontend spec (component/theme/rule.components bucket) already defines a component for a purpose but the plan selects another or self-invents one without a declared deviation; when uiComponentChoices is missing/empty for UI-visible work while the frozen component/theme bucket is non-empty; when a decision=specified specReference.path is missing a ledger OpenSpec reference or successful read event; or when a decision=new component lacks a traceable task-source/PRD specReference. A PRD reference for decision=new is not an OpenSpec citation and must not be rejected merely for lacking an OpenSpec read event. For uiComponentChoices, purpose is the stable coverage key and must match interaction.name or uiState.name; responsibility is expressed by the matched expectedBehavior plus rationale, and you must not reject it merely for matching an interaction or component identifier.",
3510
+ "You must NOT make authoritative assertions about the execution result of frozen verification commands: command results are deterministically established by frontend-verify-shell. Record a verification-feasibility concern only as a non-blocking finding (severity must not be Critical, and it must never be the sole fatal basis for request_design_changes). Only semantic design defects (component selection, state flow, interaction contract, or conflicts with the specification) may be Critical; a pure command-will-fail prediction must not be classified as contract-requirement-gap.",
3511
3511
  "Read-only: do not modify repository files.",
3512
3512
  "LARGE-FILE AUDIT (avoid full reads): style/theme audit files can be large (e.g. styles.css is often hundreds of KB). Prefer grep to locate the exact rules/variables you must verify (e.g. grep the oc- class, is-* modifier, or --oc- theme variables with their line numbers), then read only the narrow line range when surrounding context is needed. Do not read a large style/test file in full — a single full read can exhaust the read budget and fail the attempt.",
3513
3513
  "Canonical contract reading: frontend-design-policy-shell prints absolute paths for Contract, Contract index, and the non-blocking Capacity diagnostic. Read the capacity diagnostic first. When it recommends full-contract, read the exact Contract path. When it recommends indexed-sections, read the Contract index and its hash-bound section files instead of opening the full contract. Never resolve a bare contracts/... path against the repository root or hunt for substitutes. Implementation target files inside the writeSet are created later by the implement node: do not read them and do not treat their absence as a design defect.",
3514
3514
  fixedVerificationContext,
3515
3515
  sourceContexts.designReview,
3516
3516
  scopedOpenspecContext,
3517
- frontendContractFieldSummary,
3518
3517
  mockContextBlock,
3519
3518
  ].join("\n\n"),
3520
3519
  },
@@ -13,7 +13,7 @@ import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./p
13
13
  import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
14
14
  import { materializeDeclaredArtifactFacts } from "./artifact-bindings.js";
15
15
  import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
16
- import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, projectFrontendNodeProtocolFailureReason, resolveFrontendPlanBackupRoute, resolveFrontendPlanRetryStep, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
16
+ import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, OUTPUT_LIMIT_RETRY_CATEGORY, projectFrontendNodeProtocolFailureReason, resolveFrontendPlanBackupRoute, resolveFrontendPlanRetryStep, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
17
17
  import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
18
18
  import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
19
19
  import { getStructuredContractValidator } from "./contract-output-registry.js";
@@ -257,6 +257,9 @@ function frontendPlanValidationRetryGuidance(reason) {
257
257
  if (/verificationTargets.*uiStates|uiStates.*verificationTargets/i.test(reason)) {
258
258
  guidance.push("For this retry, provide verificationTargets[].uiStates as an array; use [] when the target has no named UI state.");
259
259
  }
260
+ if (/duplicate|already recorded/i.test(reason)) {
261
+ guidance.push("For this retry, re-commit the corrected entry with the same id and replace=true; the ledger compiles the latest submission as the full replacement. A duplicate receipt is a correction invitation, not a prohibition, and backfilling a cited fact's reverse reference in-node does not re-derive committed facts.");
262
+ }
260
263
  return guidance;
261
264
  }
262
265
  function compactRetryText(text, maxChars) {
@@ -774,6 +777,87 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
774
777
  "</retry_instruction>",
775
778
  ].join("\n");
776
779
  }
780
+ // Repair-category guidance must outrank the retry ladder position: the
781
+ // ladder advances monotonically on transport failures (e.g. length →
782
+ // compact-terminal-first), and its "keep the committed ledger intact"
783
+ // instruction directly contradicts the repair action for invalid-output /
784
+ // truncated ledger facts (re-commit corrected record_* facts). When both
785
+ // apply, the model receives the repair instruction, not the rung script.
786
+ if (previousFailureCategory === "invalid-output" &&
787
+ task.structuredContractOutput &&
788
+ previousProtocolReason) {
789
+ // The frontend plan node's compile authority is the committed typed
790
+ // ledger, not a fenced JSON text artifact: its retry guidance must
791
+ // direct the model to re-commit corrected record_* facts and
792
+ // finalize_plan. The legacy full-contract JSON guidance below applies
793
+ // only to nodes whose authority is still a text contract artifact.
794
+ if (task.structuredContractOutput.schemaId ===
795
+ "frontend-implementation-contract-plan-patch-v1") {
796
+ const splitGuidance = /write-set-too-large|split the task/.test(previousProtocolReason ?? "")
797
+ ? [
798
+ "",
799
+ "The writeSet is too large for one implement node. Preserve the complete requirement and file coverage. This needs a Contract-level task split, not a Plan formatting repair. Report the write-set-too-large finding and the affected paths for the controller to resume Contract/split orchestration. Plan cannot change protected targets.files or call Contract-only tools; do not invent a split tool or discard required files.",
800
+ ]
801
+ : [];
802
+ return [
803
+ basePrompt,
804
+ "",
805
+ "<retry_instruction>",
806
+ "Previous plan ledger facts failed canonical contract validation:",
807
+ previousProtocolReason,
808
+ "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.",
809
+ "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.",
810
+ ...frontendPlanValidationRetryGuidance(previousProtocolReason),
811
+ ...splitGuidance,
812
+ "</retry_instruction>",
813
+ ].join("\n");
814
+ }
815
+ return [
816
+ basePrompt,
817
+ "",
818
+ "<retry_instruction>",
819
+ "Previous attempt produced an invalid frontend implementation contract:",
820
+ previousProtocolReason,
821
+ ...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
822
+ "Fix every reported field violation: do not emit null for optional fields, do not misspell field names, and match the required types exactly.",
823
+ "</retry_instruction>",
824
+ ].join("\n");
825
+ }
826
+ if (previousFailureCategory === "invalid-output" &&
827
+ task.id === "frontend-scout-pi") {
828
+ return [
829
+ basePrompt,
830
+ "",
831
+ "<retry_instruction>",
832
+ "The previous Scout attempt did not commit a complete, runtime-evidenced target surface.",
833
+ "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, unless the task source explicitly declares a greenfield target: in that case every future path must be source-declared by the runtime-enriched fact. If ownership truly cannot be established, record completeness=blocked with each unresolved path; do not make Plan discover it.",
834
+ "</retry_instruction>",
835
+ ].join("\n");
836
+ }
837
+ if (previousFailureCategory === "structured-output-truncated" &&
838
+ task.structuredContractOutput) {
839
+ if (task.structuredContractOutput.schemaId ===
840
+ "frontend-implementation-contract-plan-patch-v1") {
841
+ return [
842
+ basePrompt,
843
+ "",
844
+ "<retry_instruction>",
845
+ "Previous attempt was truncated by the provider (stopReason=length) before the plan facts were fully committed.",
846
+ "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.",
847
+ "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.",
848
+ "</retry_instruction>",
849
+ ].join("\n");
850
+ }
851
+ return [
852
+ basePrompt,
853
+ "",
854
+ "<retry_instruction>",
855
+ "Previous attempt was truncated by the provider (stopReason=length) before the JSON contract was completed.",
856
+ ...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
857
+ "The JSON artifact must be complete; omit evidence excerpts and duplicated upstream context.",
858
+ "</retry_instruction>",
859
+ ].join("\n");
860
+ }
777
861
  if (frontendPlanRetryStep === "compact-terminal-first") {
778
862
  return [
779
863
  basePrompt,
@@ -845,83 +929,25 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
845
929
  "</retry_instruction>",
846
930
  ].join("\n");
847
931
  }
848
- if (previousFailureCategory === "invalid-output" &&
849
- task.structuredContractOutput &&
850
- previousProtocolReason) {
851
- // The frontend plan node's compile authority is the committed typed
852
- // ledger, not a fenced JSON text artifact: its retry guidance must
853
- // direct the model to re-commit corrected record_* facts and
854
- // finalize_plan. The legacy full-contract JSON guidance below applies
855
- // only to nodes whose authority is still a text contract artifact.
856
- if (task.structuredContractOutput.schemaId ===
857
- "frontend-implementation-contract-plan-patch-v1") {
858
- const splitGuidance = /write-set-too-large|split the task/.test(previousProtocolReason ?? "")
859
- ? [
860
- "",
861
- "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.",
862
- ]
863
- : [];
864
- return [
865
- basePrompt,
866
- "",
867
- "<retry_instruction>",
868
- "Previous plan ledger facts failed canonical contract validation:",
869
- previousProtocolReason,
870
- "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.",
871
- "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.",
872
- ...frontendPlanValidationRetryGuidance(previousProtocolReason),
873
- ...splitGuidance,
874
- "</retry_instruction>",
875
- ].join("\n");
876
- }
932
+ // Generic output-limit fallback. Every branch above this one carries a
933
+ // more precise instruction for the same capacity signal (contract repair
934
+ // reasons, ladder rungs — compact-terminal-first is the reason-mandated
935
+ // rung for length-before-terminal —, protocol/review/read-burst repair),
936
+ // so output-limit must not shadow them.
937
+ if (previousFailureCategory === OUTPUT_LIMIT_RETRY_CATEGORY) {
877
938
  return [
878
939
  basePrompt,
879
940
  "",
880
941
  "<retry_instruction>",
881
- "Previous attempt produced an invalid frontend implementation contract:",
882
- previousProtocolReason,
883
- ...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
884
- "Fix every reported field violation: do not emit null for optional fields, do not misspell field names, and match the required types exactly.",
885
- "</retry_instruction>",
886
- ].join("\n");
887
- }
888
- if (previousFailureCategory === "invalid-output" &&
889
- task.id === "frontend-scout-pi") {
890
- return [
891
- basePrompt,
892
- "",
893
- "<retry_instruction>",
894
- "The previous Scout attempt did not commit a complete, runtime-evidenced target surface.",
895
- "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, unless the task source explicitly declares a greenfield target: in that case every future path must be source-declared by the runtime-enriched fact. If ownership truly cannot be established, record completeness=blocked with each unresolved path; do not make Plan discover it.",
896
- "</retry_instruction>",
897
- ].join("\n");
898
- }
899
- if (previousFailureCategory === "structured-output-truncated" &&
900
- task.structuredContractOutput) {
901
- if (task.structuredContractOutput.schemaId ===
902
- "frontend-implementation-contract-plan-patch-v1") {
903
- return [
904
- basePrompt,
905
- "",
906
- "<retry_instruction>",
907
- "Previous attempt was truncated by the provider (stopReason=length) before the plan facts were fully committed.",
908
- "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.",
909
- "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.",
910
- "</retry_instruction>",
911
- ].join("\n");
912
- }
913
- return [
914
- basePrompt,
915
- "",
916
- "<retry_instruction>",
917
- "Previous attempt was truncated by the provider (stopReason=length) before the JSON contract was completed.",
918
- ...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
919
- "The JSON artifact must be complete; omit evidence excerpts and duplicated upstream context.",
942
+ "The previous turn ended with stopReason=length before the required output was complete. This is output-capacity truncation, not empty output.",
943
+ "Continue incrementally from already committed typed facts and current in-scope workspace files. Do not repeat completed discovery, decisions, facts, or writes.",
944
+ "Generate the smallest unfinished unit next, validate its required format immediately, repair any format error in this node, and only then continue to the next unfinished unit or terminal tool.",
945
+ "Keep prose minimal and finish the required terminal/output protocol as soon as the remaining work is valid.",
920
946
  "</retry_instruction>",
921
947
  ].join("\n");
922
948
  }
923
949
  if (previousFailureCategory === "writer-empty-diff") {
924
- const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
950
+ const maxAttempts = task.retryPolicy?.maxAttempts ?? 5;
925
951
  // When a completeness progress exists for this writer, fold the concrete
926
952
  // target paths into the empty-diff retry so the model does not guess and
927
953
  // does not need to read a forbidden `.harness/**` evidence file.
@@ -949,7 +975,7 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
949
975
  ].join("\n");
950
976
  }
951
977
  if (previousFailureCategory === "incomplete-write-set") {
952
- const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
978
+ const maxAttempts = task.retryPolicy?.maxAttempts ?? 5;
953
979
  const bindingOnly = task.id?.startsWith("generate-backend-md-case-") === true &&
954
980
  (recoveryTargetPaths?.length ?? 0) === 1 &&
955
981
  Object.values(recoveryDiagnostics ?? {}).flat().some((detail) => /(?:unclassified Test Points|duplicate Test Point bindings)/i.test(detail));
@@ -1765,6 +1791,32 @@ export async function executeDagNode(input) {
1765
1791
  durationMs: 0,
1766
1792
  };
1767
1793
  }
1794
+ // stopReason=length on top of a bare empty-output verdict is a provider
1795
+ // capacity signal, not a true empty response: reroute it through the
1796
+ // dedicated output-limit retry path while keeping the raw category for
1797
+ // diagnostics. Bare empty-output and transport aliases (network,
1798
+ // nonzero-exit, unknown) are rerouted — categories that already carry a
1799
+ // precise repair instruction (output-too-large, invalid-output,
1800
+ // protocol-invalid, structured-output-truncated via the validators below,
1801
+ // writer categories) keep their classification so their exact retry
1802
+ // guidance still reaches the model.
1803
+ if (task.executor === "pi" &&
1804
+ !result.ok &&
1805
+ result.stopReason === "length" &&
1806
+ (result.failureCategory === undefined ||
1807
+ ["empty-output", "network", "nonzero-exit", "unknown"].includes(result.failureCategory))) {
1808
+ result = {
1809
+ ...result,
1810
+ rawFailureCategory: result.rawFailureCategory ?? result.failureCategory,
1811
+ failureCategory: OUTPUT_LIMIT_RETRY_CATEGORY,
1812
+ stderr: [
1813
+ result.stderr,
1814
+ "output-limit: stopReason=length; preserve completed work and retry only the unfinished output",
1815
+ ]
1816
+ .filter(Boolean)
1817
+ .join("\n"),
1818
+ };
1819
+ }
1768
1820
  if (task.id === "generate-backend-md-plan-pi" &&
1769
1821
  !result.ok &&
1770
1822
  (result.failureCategory === "output-too-large" ||
@@ -1898,7 +1950,7 @@ export async function executeDagNode(input) {
1898
1950
  !result.ok &&
1899
1951
  retryPolicy !== undefined) {
1900
1952
  const submissions = await countContractRecordSubmissions(runDir, nodeId);
1901
- if (submissions === 0) {
1953
+ if (submissions === 0 && result.stopReason !== "length") {
1902
1954
  result = {
1903
1955
  ...result,
1904
1956
  failureCategory: "empty-output",
@@ -408,6 +408,7 @@ export async function deriveDagRerunFeedback(input) {
408
408
  return (node?.status === "ERROR" &&
409
409
  [
410
410
  "empty-output",
411
+ "output-limit",
411
412
  "invalid-output",
412
413
  "writer-thinking-exhausted",
413
414
  "writer-budget-exhausted",
@@ -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");
@@ -784,6 +784,13 @@ async function stageContinuationRun(input) {
784
784
  if (!parentRecord) {
785
785
  throw new Error(`parent node record missing for imported node: ${nodeId}`);
786
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
+ }
787
794
  const { manifestNode, importedRecord } = await importNodeFacts({
788
795
  parentRunDir: input.parentRunDir,
789
796
  newRunDir: runDir,
@@ -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,
@@ -125,7 +125,10 @@ export const WRITER_EMPTY_DIFF_RETRY_POLICY = {
125
125
  backoff: "exponential",
126
126
  initialDelayMs: 2000,
127
127
  maxDelayMs: 30000,
128
- retryCategories: [WRITER_EMPTY_DIFF_RETRY_CATEGORY],
128
+ retryCategories: [
129
+ WRITER_EMPTY_DIFF_RETRY_CATEGORY,
130
+ OUTPUT_LIMIT_RETRY_CATEGORY,
131
+ ],
129
132
  };
130
133
  /**
131
134
  * Bounded transport retry for standard exclusive implementers when a provider
@@ -140,6 +143,7 @@ export const WRITER_TRANSPORT_RETRY_POLICY = {
140
143
  retryCategories: [
141
144
  WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
142
145
  CONTEXT_OVERFLOW_RETRY_CATEGORY,
146
+ OUTPUT_LIMIT_RETRY_CATEGORY,
143
147
  ],
144
148
  };
145
149
  /**
@@ -175,6 +179,7 @@ export const TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY = {
175
179
  retryCategories: [
176
180
  WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY,
177
181
  CONTEXT_OVERFLOW_RETRY_CATEGORY,
182
+ OUTPUT_LIMIT_RETRY_CATEGORY,
178
183
  ],
179
184
  };
180
185
  /**
@@ -182,13 +187,14 @@ export const TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY = {
182
187
  * recovery attempts driven by Completeness Gate (missing/broken target files).
183
188
  */
184
189
  export const BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY = {
185
- maxAttempts: 3,
190
+ maxAttempts: 5,
186
191
  backoff: "exponential",
187
192
  initialDelayMs: 2000,
188
193
  maxDelayMs: 30000,
189
194
  retryCategories: [
190
195
  WRITER_EMPTY_DIFF_RETRY_CATEGORY,
191
196
  INCOMPLETE_WRITE_SET_RETRY_CATEGORY,
197
+ OUTPUT_LIMIT_RETRY_CATEGORY,
192
198
  ],
193
199
  };
194
200
  /** Markdown shard writers use one full attempt plus at most one bounded binding repair. */
@@ -472,9 +478,9 @@ function reasonMandatedFrontendPlanRetryStep(reason) {
472
478
  }
473
479
  }
474
480
  /**
475
- * Opt-in retry policy mounted on the frontend-plan-pi node. maxAttempts=3
476
- * bounds plan retry cost: the four-rung ladder is only partially traversable
477
- * 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 →
478
484
  * compact-terminal-first → backup-model). `invalid-output` (typed-fact schema
479
485
  * violations, e.g. verification targets referencing undeclared UI states)
480
486
  * retries at the `normal` rung with the contractCheck reason injected via
@@ -485,7 +491,7 @@ export const FRONTEND_PLAN_LADDER_RETRY_POLICY = {
485
491
  // incremental record_* calls; dense call bursts hit provider rate limits
486
492
  // whose windows exceed the old 30s cap. Longer backoff gives the limit
487
493
  // window time to expire before the next attempt.
488
- maxAttempts: 3,
494
+ maxAttempts: 5,
489
495
  backoff: "exponential",
490
496
  initialDelayMs: 5000,
491
497
  maxDelayMs: 60000,