@tea-agent/loop-agent 0.42.0-next.8 → 0.42.0

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 (161) hide show
  1. package/CHANGELOG.md +56 -50
  2. package/dist/application/dag/run-dag.js +41 -0
  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 +1809 -213
  9. package/dist/executors/pi-extension-resolver.js +14 -2
  10. package/dist/executors/shell-executor.js +178 -73
  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/routes.js +4 -0
  98. package/dist/worker/observe/static/constants.js +22 -22
  99. package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
  100. package/dist/worker/observe/static/dag-history-labels.js +1 -0
  101. package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
  102. package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
  103. package/dist/worker/observe/static/dag-node-purpose.js +10 -10
  104. package/dist/worker/observe/static/index.html +4 -4
  105. package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
  106. package/dist/worker/observe/static/relations.js +5 -5
  107. package/dist/worker/observe/static/styles.css +171 -0
  108. package/dist/worker/observe/static/views/dag-graph.js +1 -1
  109. package/dist/worker/observe/static/views/dag-inspector.js +329 -174
  110. package/dist/worker/observe/static/views/dag.js +16 -12
  111. package/dist/worker/observe/static/views/session-timeline.js +5 -3
  112. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +18 -0
  113. package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
  114. package/dist/workflows/dag/backend-test-scenario-param.js +30 -1
  115. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  116. package/dist/workflows/dag/frontend-contract-facts.js +130 -0
  117. package/dist/workflows/dag/frontend-design-policy.js +101 -16
  118. package/dist/workflows/dag/frontend-implementation-contract.js +410 -41
  119. package/dist/workflows/dag/frontend-plan-render.js +13 -2
  120. package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
  121. package/dist/workflows/dag/frontend-recovery-controller.js +25 -1
  122. package/dist/workflows/dag/frontend-recovery-plan.js +6 -2
  123. package/dist/workflows/dag/frontend-recovery-run.js +169 -15
  124. package/dist/workflows/dag/frontend-risk.js +2 -0
  125. package/dist/workflows/dag/frontend-shadow-dual-write.js +17 -1
  126. package/dist/workflows/dag/frontend-shape.js +16 -6
  127. package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
  128. package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
  129. package/dist/workflows/dag/frontend-verification-trace.js +38 -4
  130. package/dist/workflows/dag/frontend-writer-admission.js +46 -12
  131. package/dist/workflows/dag/init-hybrid.js +59 -29
  132. package/dist/workflows/dag/node-execution.js +277 -85
  133. package/dist/workflows/dag/recovery-lease.js +106 -16
  134. package/dist/workflows/dag/rerun-feedback.js +60 -0
  135. package/dist/workflows/dag/rerun-plan.js +90 -4
  136. package/dist/workflows/dag/rerun-run.js +28 -6
  137. package/dist/workflows/dag/rerun-task.js +224 -15
  138. package/dist/workflows/dag/retry-policy.js +27 -10
  139. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  140. package/dist/workflows/dag/runner.js +238 -29
  141. package/dist/workflows/dag/scheduler.js +21 -6
  142. package/dist/workflows/dag/structured-output-repair.js +4 -1
  143. package/dist/workflows/dag/types.js +4 -3
  144. package/dist/workflows/dag/validate.js +10 -8
  145. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  146. package/docs/operations/README.md +1 -0
  147. package/docs/templates/agent-dag.schema.json +2 -2
  148. package/docs/templates/backend-test-dag.json +6 -4
  149. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  150. package/harness.json +1 -1
  151. package/package.json +4 -5
  152. package/skills/frontend-plan/SKILL.md +14 -1
  153. package/skills/frontend-plan/references/decision-contract.md +101 -5
  154. package/skills/frontend-plan/references/design-decisions.md +32 -0
  155. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
  156. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
  157. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
  158. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
  159. package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
  160. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
  161. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
@@ -6,13 +6,14 @@ import { pathMatchesPattern } from "../../shared/git-progress.js";
6
6
  import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
7
7
  import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
8
8
  import { FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isFrontendWriterAuthorized, readFrontendPrewriteResult, } from "./scheduler.js";
9
+ import { collectVerificationCommandFiles, } from "./frontend-writer-admission.js";
9
10
  import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
10
11
  import { resolveContextPolicy } from "./context-policy.js";
11
12
  import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
12
13
  import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
13
14
  import { materializeDeclaredArtifactFacts } from "./artifact-bindings.js";
14
15
  import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
15
- 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";
16
17
  import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
17
18
  import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
18
19
  import { getStructuredContractValidator } from "./contract-output-registry.js";
@@ -20,6 +21,7 @@ import "./contract-validator-registrations.js";
20
21
  import { computeNormalizedFailureFingerprint } from "./frontend-recovery-lineage.js";
21
22
  import { parseLedgerJson } from "../../task/source-prepare/ledger.js";
22
23
  import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
24
+ import { collectCanonicalStateFlowNames, extractRequiredDeliverablePaths, resolveFrontendContractRequirements, } from "./frontend-contract-facts.js";
23
25
  import { allowedRepairReadPaths, auditRepairAttemptToolUse, buildStructuredOutputRepairPrompt, freezeStructuredOutputRepairContext, hasNonEmptyStructuredCandidate, isFrontendStructuredRepairSchemaId, isStructuredRepairableFailureCategory, persistStructuredAttemptRaw, sessionEventsByteLength, GOVERNANCE_BLOCKED_CATEGORY, STRUCTURED_REPAIR_EXHAUSTED_CATEGORY, } from "./structured-output-repair.js";
24
26
  import { readFrontendCanonicalCandidate } from "./frontend-implementation-contract.js";
25
27
  import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
@@ -255,6 +257,9 @@ function frontendPlanValidationRetryGuidance(reason) {
255
257
  if (/verificationTargets.*uiStates|uiStates.*verificationTargets/i.test(reason)) {
256
258
  guidance.push("For this retry, provide verificationTargets[].uiStates as an array; use [] when the target has no named UI state.");
257
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
+ }
258
263
  return guidance;
259
264
  }
260
265
  function compactRetryText(text, maxChars) {
@@ -324,6 +329,8 @@ const FRONTEND_CONTRACT_RECORD_TOOL_NAMES_LOCAL = new Set([
324
329
  "record_handoff_intent",
325
330
  "record_open_question",
326
331
  "record_split_proposal",
332
+ "record_ui_state",
333
+ "record_required_deliverables",
327
334
  ]);
328
335
  async function countContractRecordSubmissions(runDir, nodeId) {
329
336
  const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
@@ -552,8 +559,9 @@ export function renderFrontendPlanInputContext(input) {
552
559
  : []);
553
560
  const contractFacts = committedFacts(input.contractRecords);
554
561
  const scoutFacts = committedFacts(input.scoutRecords);
555
- const requirements = contractFacts
556
- .filter((fact) => fact.kind === "requirement" && fact.origin === "contract")
562
+ const requirementFacts = resolveFrontendContractRequirements(contractFacts);
563
+ const requiredDeliverables = extractRequiredDeliverablePaths(contractFacts);
564
+ const requirements = requirementFacts
557
565
  .map((fact) => ({
558
566
  id: planInputText(fact.id, 80),
559
567
  text: planInputText(fact.text),
@@ -579,19 +587,28 @@ export function renderFrontendPlanInputContext(input) {
579
587
  paths: fact.paths,
580
588
  conflicts: fact.conflicts,
581
589
  }));
590
+ // Authoritative UI states (contract-declared): the source's UI-state table
591
+ // extracted by the contract node. The planner binds these ids instead of
592
+ // inventing list-visibility variants.
593
+ const declaredUiStates = contractFacts
594
+ .filter((fact) => fact.kind === "ui-state-declaration" && fact.origin === "contract")
595
+ .map((fact) => ({
596
+ id: planInputText(fact.id, 80),
597
+ trigger: planInputText(fact.trigger),
598
+ observableOutcome: planInputText(fact.observableOutcome),
599
+ }))
600
+ .filter((state) => state.id !== undefined);
601
+ // Replay registry edits and state-flow removals/additions in commit order.
602
+ const committedUxNames = collectCanonicalStateFlowNames(input.planRecords ?? [], true);
603
+ const committedUiStateNames = [...committedUxNames.uiStateNames];
604
+ const committedInteractionNames = [...committedUxNames.interactionNames];
582
605
  // Reviewer-rubric scaffold: the design reviewer re-runs the design-policy
583
606
  // checks on the committed facts, so publish the checklist to the producer.
584
607
  // Requirements whose contract evidence expects behavioural verification are
585
608
  // enumerated explicitly — those are the slots the reviewer finds missing
586
609
  // when the plan models interactions ad hoc (r8/r9 findings).
587
- const behaviorRequiredIds = requirements
588
- .filter((requirement) => {
589
- const fact = contractFacts.find((candidate) => candidate.kind === "requirement" &&
590
- candidate.origin === "contract" &&
591
- candidate.id === requirement.id);
592
- const evidence = fact?.evidence;
593
- return evidence?.behavior === "required";
594
- })
610
+ const behaviorRequiredIds = requirementFacts
611
+ .filter((requirement) => requirement.evidence.behavior === "required")
595
612
  .map((requirement) => requirement.id);
596
613
  const serializeAtCap = (cap) => JSON.stringify({
597
614
  requirements: requirements.map((requirement) => ({
@@ -599,6 +616,7 @@ export function renderFrontendPlanInputContext(input) {
599
616
  text: planInputText(requirement.text, cap.text),
600
617
  sourceFragmentIds: planInputStrings(requirement.sourceFragmentIds).slice(0, cap.array),
601
618
  })),
619
+ requiredDeliverables,
602
620
  targetSurface: targetSurface.map((surface) => ({
603
621
  completeness: planInputText(surface.completeness, 32),
604
622
  entrypoint: planInputText(surface.entrypoint, cap.text),
@@ -614,6 +632,18 @@ export function renderFrontendPlanInputContext(input) {
614
632
  paths: planInputStrings(evidence.paths).slice(0, cap.array),
615
633
  conflicts: planInputStrings(evidence.conflicts).slice(0, cap.array),
616
634
  })),
635
+ declaredUiStates: declaredUiStates.map((state) => ({
636
+ id: state.id,
637
+ trigger: planInputText(state.trigger, cap.text),
638
+ observableOutcome: planInputText(state.observableOutcome, cap.text),
639
+ })),
640
+ committedUx: committedUiStateNames.length > 0 ||
641
+ committedInteractionNames.length > 0
642
+ ? {
643
+ uiStateNames: committedUiStateNames,
644
+ interactionNames: committedInteractionNames,
645
+ }
646
+ : undefined,
617
647
  });
618
648
  let serialized = serializeAtCap(FRONTEND_PLAN_INPUT_CAP_LADDER[0]);
619
649
  for (const cap of FRONTEND_PLAN_INPUT_CAP_LADDER.slice(1)) {
@@ -627,6 +657,7 @@ export function renderFrontendPlanInputContext(input) {
627
657
  // to the minimum, and declare the degradation instead of corrupting JSON.
628
658
  let fallback = {
629
659
  degraded: "requirement-texts-truncated",
660
+ requiredDeliverables,
630
661
  requirements: requirements.map((requirement) => ({
631
662
  id: requirement.id,
632
663
  text: "(truncated)",
@@ -651,20 +682,20 @@ export function renderFrontendPlanInputContext(input) {
651
682
  serialized = bounded;
652
683
  }
653
684
  const checklistLines = [
654
- "1. Every interaction you record needs a uiComponentChoices entry whose purpose equals the interaction name, or one decision=reuse-existing choice covering behavioural interactions.",
655
- "2. Every applicable UI state needs a purpose-matching component choice or a stylingStrategy.",
656
- "3. Every requirement marked (behavior) below needs modelled interactions plus at least one verification target that references it.",
657
- "4. targets.files must name the concrete deliverable files; never leave the scope broader than the frozen requirements state.",
685
+ "1. Record the GLOBAL UX vocabulary with record_state_registry BEFORE any record_state_flow: one stable behavior-domain name per state/interaction (e.g. planner-task-edit), never one name per AC number, and never a rename of an already-recorded concept. Coverage slices by AC; UX does not.",
686
+ "2. Every recorded interaction/uiState name must be in that registry, and uiState names must use the contract's declared authoritative ids (declaredUiStates below) when present.",
687
+ "3. Every interaction and every applicable UI state must be covered by a uiComponentChoices entry: its purpose equals the name, or the choice lists the name in covers (one choice may cover many ids). A reuse-existing decision without evidencePath is rejected; stylingStrategy alone covers nothing.",
688
+ "4. Every requirement marked (behavior) below needs modelled interactions plus at least one verification target that references it.",
658
689
  "5. Verification targets may only reference UI states and requirements you actually recorded (the record_* tools reject unknown references).",
659
690
  `Requirements requiring behavioural coverage: ${behaviorRequiredIds.length > 0 ? behaviorRequiredIds.join(", ") : "(none)"}`,
660
- "6. A decision=new component must declare sourceRequirementIds and pass sourceFragmentId for the frozen PRD fragment whose section matches the component's purpose — the runtime validates that binding and derives specReference (path/section/line); the reviewer checks purpose↔citation consistency.",
691
+ "6. A decision=new component must declare sourceRequirementIds and pass sourceFragmentId for the frozen PRD fragment whose section matches the component's purpose — the runtime validates that binding and derives specReference (path/section/line); the reviewer checks purpose↔citation consistency. A decision=reuse-existing component must pass evidencePath pointing at the existing repo file that proves the reuse.",
661
692
  ...[...input.componentSourceCitations ?? []]
662
693
  .filter(([id]) => behaviorRequiredIds.includes(id))
663
694
  .flatMap(([id, citations]) => citations.map((citation) => ` ${id} + ${citation.fragmentId} → ${citation.section}${citation.line ? ` (line ${citation.line})` : ""}`)),
664
695
  ];
665
696
  return [
666
697
  "<frontend_plan_input>",
667
- "Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence.",
698
+ "Committed Contract/Scout facts, compiled by the runner. Treat them as the complete planning evidence. declaredUiStates are the authoritative UI states from the task source; committedUx (retry attempts) is the UX vocabulary already recorded — reuse those names, never re-invent them.",
668
699
  serialized,
669
700
  "Do not read upstream artifacts, task sources, or repository files. If this input cannot support a decision, record a genuine evidence gap.",
670
701
  "</frontend_plan_input>",
@@ -691,6 +722,16 @@ export async function buildFrontendPlanInputContext(runDir, componentSourceCitat
691
722
  // forbids reading anything.
692
723
  throw new Error(`frontend-plan-input-unavailable: cannot read committed typed facts (${error instanceof Error ? error.message : String(error)})`);
693
724
  }
725
+ // Retry-attempt continuity (UX slice visibility): the plan node's own
726
+ // committed facts are absent on the first attempt and present on retries;
727
+ // a missing file is normal there, not a broken pipeline.
728
+ let planRecords = [];
729
+ try {
730
+ planRecords = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
731
+ }
732
+ catch {
733
+ planRecords = [];
734
+ }
694
735
  const committedCount = [...contractRecords, ...scoutRecords].filter((record) => record.phase === "committed").length;
695
736
  if (committedCount === 0) {
696
737
  throw new Error(`frontend-plan-input-unavailable: no committed Contract/Scout facts in ${contractFactsPath} / ${scoutFactsPath}`);
@@ -698,6 +739,7 @@ export async function buildFrontendPlanInputContext(runDir, componentSourceCitat
698
739
  return renderFrontendPlanInputContext({
699
740
  contractRecords,
700
741
  scoutRecords,
742
+ planRecords,
701
743
  componentSourceCitations,
702
744
  });
703
745
  }
@@ -720,77 +762,27 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
720
762
  "</retry_instruction>",
721
763
  ].join("\n");
722
764
  }
723
- if (frontendPlanRetryStep === "compact-terminal-first") {
724
- return [
725
- basePrompt,
726
- "",
727
- "<retry_instruction>",
728
- "Frontend plan retry ladder step: compact-terminal-first.",
729
- "Do NOT rewrite the narrative. Only adopt already staged/quarantined fresh facts, fill the missing required facts, then call finalize_plan exactly once.",
730
- "Keep the existing committed ledger intact; do not re-derive already committed facts.",
731
- "</retry_instruction>",
732
- ].join("\n");
733
- }
734
- if (frontendPlanRetryStep === "bounded-tool-only") {
735
- return [
736
- basePrompt,
737
- "",
738
- "<retry_instruction>",
739
- "Frontend plan retry ladder step: bounded-tool-only.",
740
- "Reduce reasoning and wall-clock time. Use only read / fact / terminal tools and only for the necessary missing facts, then call finalize_plan exactly once.",
741
- "Do not expand scope or re-derive already committed facts.",
742
- "</retry_instruction>",
743
- ].join("\n");
744
- }
745
- if (frontendPlanRetryStep === "backup-model") {
746
- return [
747
- basePrompt,
748
- "",
749
- "<retry_instruction>",
750
- "Frontend plan retry ladder step: backup-model.",
751
- "A backup provider/model route was selected after repeated non-converging failures. Re-derive only the missing committed facts, then call finalize_plan exactly once; do not repeat the failed strategy.",
752
- "</retry_instruction>",
753
- ].join("\n");
754
- }
755
- if (previousFailureCategory === "protocol-invalid" &&
756
- task.outputProtocol &&
757
- previousProtocolReason) {
758
- return [
759
- basePrompt,
760
- "",
761
- buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
762
- ].join("\n");
763
- }
764
- if (previousFailureCategory === "review-terminal-missing") {
765
- return [
766
- basePrompt,
767
- "",
768
- "<retry_instruction>",
769
- "The review emitted a verdict in response text but never committed the authoritative typed terminal tool call (approve_review / request_review_changes). The response text is NOT the authority: no branch or gate reads it.",
770
- "Call exactly one typed terminal tool to finish: approve_review (implementation passes, no Critical/Important findings) or request_review_changes (with typed issueCategory, at least one evidenceRef, and non-empty findings). Do not repeat the review analysis; commit the terminal tool once and stop.",
771
- "</retry_instruction>",
772
- ].join("\n");
773
- }
774
- if (previousFailureCategory === "read-burst") {
775
- if (task.id !== FRONTEND_PLAN_NODE_ID) {
776
- return [
777
- basePrompt,
778
- "",
779
- "<retry_instruction>",
780
- "The previous attempt exceeded its read budget. Start a fresh, evidence-minimal pass: do not re-read task sources, upstream stdout, skills, or full diff artifacts already represented by a canonical context artifact.",
781
- "Read the smallest relevant summary first, then only the specific source file or diff fragment needed for the required decision. Finish the required terminal/output protocol as soon as evidence is sufficient.",
782
- "</retry_instruction>",
783
- ].join("\n");
784
- }
765
+ if (task.id === "generate-backend-md-plan-pi" &&
766
+ (previousFailureCategory === "output-too-large" ||
767
+ previousFailureCategory === "invalid-output")) {
785
768
  return [
786
769
  basePrompt,
787
770
  "",
788
771
  "<retry_instruction>",
789
- "Previous plan attempt issued too many read-only tool calls (read/grep/ls/find) and blew up the context window. Trust the upstream frontend-contract-pi typed requirement facts and frontend-scout-pi target surface already provided — do NOT re-read contract/scout stdout, PRD/source files, or component sources you already inspected.",
790
- "Minimize discovery reads: only read what you genuinely need, once. Commit record_plan_requirement / record_plan_verification_target / record_* facts directly from the facts already in context (one tool call per message), then call finalize_plan exactly once.",
772
+ "The previous backend-test plan attempt was truncated or failed its mandatory Markdown protocol.",
773
+ previousProtocolReason ?? "The previous plan artifact was incomplete.",
774
+ "Return the final Markdown artifact immediately. Do not output analysis, reasoning, source summaries, or planning narration.",
775
+ "Emit the complete section skeleton first, including exactly one ## Coverage Scope, exactly one ## Coverage Matrix, optional ## Scenario Partitions only when applicable, and exactly one ## Module Index with its required 8-column table and at least one module row.",
776
+ "After the complete skeleton exists, fill only concise table rows within the remaining output budget. Do not use code fences.",
791
777
  "</retry_instruction>",
792
778
  ].join("\n");
793
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.
794
786
  if (previousFailureCategory === "invalid-output" &&
795
787
  task.structuredContractOutput &&
796
788
  previousProtocolReason) {
@@ -804,7 +796,7 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
804
796
  const splitGuidance = /write-set-too-large|split the task/.test(previousProtocolReason ?? "")
805
797
  ? [
806
798
  "",
807
- "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.",
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.",
808
800
  ]
809
801
  : [];
810
802
  return [
@@ -866,8 +858,96 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
866
858
  "</retry_instruction>",
867
859
  ].join("\n");
868
860
  }
861
+ if (frontendPlanRetryStep === "compact-terminal-first") {
862
+ return [
863
+ basePrompt,
864
+ "",
865
+ "<retry_instruction>",
866
+ "Frontend plan retry ladder step: compact-terminal-first.",
867
+ "Do NOT rewrite the narrative. Only adopt already staged/quarantined fresh facts, fill the missing required facts, then call finalize_plan exactly once.",
868
+ "Keep the existing committed ledger intact; do not re-derive already committed facts.",
869
+ "</retry_instruction>",
870
+ ].join("\n");
871
+ }
872
+ if (frontendPlanRetryStep === "bounded-tool-only") {
873
+ return [
874
+ basePrompt,
875
+ "",
876
+ "<retry_instruction>",
877
+ "Frontend plan retry ladder step: bounded-tool-only.",
878
+ "Reduce reasoning and wall-clock time. Use only read / fact / terminal tools and only for the necessary missing facts, then call finalize_plan exactly once.",
879
+ "Do not expand scope or re-derive already committed facts.",
880
+ "</retry_instruction>",
881
+ ].join("\n");
882
+ }
883
+ if (frontendPlanRetryStep === "backup-model") {
884
+ return [
885
+ basePrompt,
886
+ "",
887
+ "<retry_instruction>",
888
+ "Frontend plan retry ladder step: backup-model.",
889
+ "A backup provider/model route was selected after repeated non-converging failures. Re-derive only the missing committed facts, then call finalize_plan exactly once; do not repeat the failed strategy.",
890
+ "</retry_instruction>",
891
+ ].join("\n");
892
+ }
893
+ if (previousFailureCategory === "protocol-invalid" &&
894
+ task.outputProtocol &&
895
+ previousProtocolReason) {
896
+ return [
897
+ basePrompt,
898
+ "",
899
+ buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
900
+ ].join("\n");
901
+ }
902
+ if (previousFailureCategory === "review-terminal-missing") {
903
+ return [
904
+ basePrompt,
905
+ "",
906
+ "<retry_instruction>",
907
+ "The review emitted a verdict in response text but never committed the authoritative typed terminal tool call (approve_review / request_review_changes). The response text is NOT the authority: no branch or gate reads it.",
908
+ "Call exactly one typed terminal tool to finish: approve_review (implementation passes, no Critical/Important findings) or request_review_changes (with typed issueCategory, at least one evidenceRef, and non-empty findings). Do not repeat the review analysis; commit the terminal tool once and stop.",
909
+ "</retry_instruction>",
910
+ ].join("\n");
911
+ }
912
+ if (previousFailureCategory === "read-burst") {
913
+ if (task.id !== FRONTEND_PLAN_NODE_ID) {
914
+ return [
915
+ basePrompt,
916
+ "",
917
+ "<retry_instruction>",
918
+ "The previous attempt exceeded its read budget. Start a fresh, evidence-minimal pass: do not re-read task sources, upstream stdout, skills, or full diff artifacts already represented by a canonical context artifact.",
919
+ "Read the smallest relevant summary first, then only the specific source file or diff fragment needed for the required decision. Finish the required terminal/output protocol as soon as evidence is sufficient.",
920
+ "</retry_instruction>",
921
+ ].join("\n");
922
+ }
923
+ return [
924
+ basePrompt,
925
+ "",
926
+ "<retry_instruction>",
927
+ "Previous plan attempt issued too many read-only tool calls (read/grep/ls/find) and blew up the context window. Trust the upstream frontend-contract-pi typed requirement facts and frontend-scout-pi target surface already provided — do NOT re-read contract/scout stdout, PRD/source files, or component sources you already inspected.",
928
+ "Minimize discovery reads: only read what you genuinely need, once. Commit record_plan_requirement / record_plan_verification_target / record_* facts directly from the facts already in context (one tool call per message), then call finalize_plan exactly once.",
929
+ "</retry_instruction>",
930
+ ].join("\n");
931
+ }
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) {
938
+ return [
939
+ basePrompt,
940
+ "",
941
+ "<retry_instruction>",
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.",
946
+ "</retry_instruction>",
947
+ ].join("\n");
948
+ }
869
949
  if (previousFailureCategory === "writer-empty-diff") {
870
- const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
950
+ const maxAttempts = task.retryPolicy?.maxAttempts ?? 5;
871
951
  // When a completeness progress exists for this writer, fold the concrete
872
952
  // target paths into the empty-diff retry so the model does not guess and
873
953
  // does not need to read a forbidden `.harness/**` evidence file.
@@ -895,7 +975,7 @@ export function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFail
895
975
  ].join("\n");
896
976
  }
897
977
  if (previousFailureCategory === "incomplete-write-set") {
898
- const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
978
+ const maxAttempts = task.retryPolicy?.maxAttempts ?? 5;
899
979
  const bindingOnly = task.id?.startsWith("generate-backend-md-case-") === true &&
900
980
  (recoveryTargetPaths?.length ?? 0) === 1 &&
901
981
  Object.values(recoveryDiagnostics ?? {}).flat().some((detail) => /(?:unclassified Test Points|duplicate Test Point bindings)/i.test(detail));
@@ -1282,6 +1362,52 @@ export async function executeDagNode(input) {
1282
1362
  await skipFrontendWriter(record);
1283
1363
  return;
1284
1364
  }
1365
+ // The admission artifact is the effective authorization boundary. Never
1366
+ // leave the writer using the broad task glob after the shell has frozen a
1367
+ // concrete set: doing so makes the receipt auditable but unenforceable.
1368
+ // Files referenced by frozen verification commands are unioned in: the
1369
+ // plan's verification targets do not always name verification
1370
+ // infrastructure, yet verify-shell cannot run without it and the writer
1371
+ // must be authorized to create it.
1372
+ const admissionWriteSetEntries = admission.result.writeSet.map((entry) => entry.trim().replace(/\\/g, "/").replace(/^\.\//, ""));
1373
+ const frozenVerificationBundle = spec.tasks.find((specTask) => specTask.shell?.frontendVerificationBundle)?.shell?.frontendVerificationBundle;
1374
+ const verificationCommandFiles = frozenVerificationBundle
1375
+ ? collectVerificationCommandFiles([
1376
+ ...(frozenVerificationBundle.staticCommands ?? []),
1377
+ ...(frozenVerificationBundle.behaviorCommands ?? []),
1378
+ ...(frozenVerificationBundle.mockCommands ?? []),
1379
+ ...(frozenVerificationBundle.lintCommands ?? []),
1380
+ ])
1381
+ : [];
1382
+ const extraVerificationFiles = verificationCommandFiles.filter((file) => !admissionWriteSetEntries.includes(file) &&
1383
+ file &&
1384
+ !file.includes("*") &&
1385
+ !file.includes("?") &&
1386
+ !file.split("/").some((segment) => segment === "..") &&
1387
+ task.allowedPaths.some((allowed) => pathMatchesPattern(file, allowed)) &&
1388
+ !task.forbiddenPaths.some((forbidden) => pathMatchesPattern(file, forbidden)));
1389
+ const admittedWriteSet = [
1390
+ ...new Set([...admissionWriteSetEntries, ...extraVerificationFiles]),
1391
+ ];
1392
+ if (admittedWriteSet.length === 0 ||
1393
+ new Set(admittedWriteSet).size !== admittedWriteSet.length ||
1394
+ admittedWriteSet.some((entry) => !entry ||
1395
+ entry.includes("*") ||
1396
+ entry.includes("?") ||
1397
+ entry.split("/").some((segment) => segment === "..") ||
1398
+ !task.allowedPaths.some((allowed) => pathMatchesPattern(entry, allowed)) ||
1399
+ task.forbiddenPaths.some((forbidden) => pathMatchesPattern(entry, forbidden)))) {
1400
+ await failBeforePrompt(new Error("frontend writer admission contains an invalid effective writeSet"), "final-write-set-approval-invalid");
1401
+ return;
1402
+ }
1403
+ task = { ...task, writeSet: admittedWriteSet };
1404
+ node.runtimeWriteAuthorization = {
1405
+ schemaVersion: 1,
1406
+ status: "validated",
1407
+ approvalSourceNodeId: FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT,
1408
+ approvalDigest: admission.result.admissionDigest,
1409
+ effectiveWriteSet: [...admittedWriteSet],
1410
+ };
1285
1411
  node.frontendWriterAdmission = record;
1286
1412
  }
1287
1413
  let projectGovernanceContext;
@@ -1457,6 +1583,26 @@ export async function executeDagNode(input) {
1457
1583
  return;
1458
1584
  }
1459
1585
  }
1586
+ if (FRONTEND_WRITER_NODE_IDS.includes(nodeId)) {
1587
+ // Design-review findings are not reliable in the provider's prose output
1588
+ // (typed terminal nodes commonly return an empty assistant message). Inject
1589
+ // the bounded admission capsule explicitly so an authorized retry has the
1590
+ // reviewer's concrete issue/evidence context.
1591
+ try {
1592
+ const admission = await readFrontendPrewriteResult(runDir);
1593
+ const advisory = admission.ok ? admission.result.designReviewAdvisory : undefined;
1594
+ if (advisory?.findings?.length || advisory?.evidenceRefs?.length) {
1595
+ prompt = `${prompt}\n\n<design_review_findings>\n${JSON.stringify({
1596
+ verdict: advisory.verdict,
1597
+ findings: advisory.findings.slice(0, 16),
1598
+ evidenceRefs: advisory.evidenceRefs.slice(0, 16),
1599
+ })}\n</design_review_findings>\nAddress every Critical/Important finding before writing.`;
1600
+ }
1601
+ }
1602
+ catch {
1603
+ // Admission is already enforced above; prompt enrichment is best effort.
1604
+ }
1605
+ }
1460
1606
  node.resolvedSkills = resolvedSkills;
1461
1607
  await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
1462
1608
  let model = resolveModelForTask(task, spec.executorModels);
@@ -1568,6 +1714,9 @@ export async function executeDagNode(input) {
1568
1714
  return acc;
1569
1715
  }, {});
1570
1716
  let attemptPrompt = buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep);
1717
+ if (task.id === "frontend-scout-pi" && attemptNumber > 1) {
1718
+ attemptPrompt = `${attemptPrompt}\n\nSCOUT RETRY (reuse existing evidence): preserve all committed target-surface/design-evidence facts and do not re-read files already covered by the prior attempt. Inspect only unresolvedPaths or missing target-surface fields, then commit the minimal correction. If the existing facts are complete, commit the same canonical facts without broad rediscovery.`;
1719
+ }
1571
1720
  if (attemptNumber > 1 &&
1572
1721
  // The plan node (frontend-plan-pi) is a typed-facts ladder task:
1573
1722
  // its retry is driven by the §5.1 ladder (compact-terminal-first
@@ -1642,6 +1791,43 @@ export async function executeDagNode(input) {
1642
1791
  durationMs: 0,
1643
1792
  };
1644
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
+ }
1820
+ if (task.id === "generate-backend-md-plan-pi" &&
1821
+ !result.ok &&
1822
+ (result.failureCategory === "output-too-large" ||
1823
+ result.failureCategory === "invalid-output")) {
1824
+ const marker = "backend-test Markdown plan";
1825
+ const markerIndex = result.stderr?.lastIndexOf(marker) ?? -1;
1826
+ previousProtocolReason =
1827
+ markerIndex >= 0
1828
+ ? result.stderr.slice(markerIndex, markerIndex + 4_000).trim()
1829
+ : `backend-test Markdown plan attempt failed with ${result.failureCategory}`;
1830
+ }
1645
1831
  if (isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId)) {
1646
1832
  const rawText = canonicalNodeOutput(result);
1647
1833
  if (rawText.trim().length > 0) {
@@ -1764,7 +1950,7 @@ export async function executeDagNode(input) {
1764
1950
  !result.ok &&
1765
1951
  retryPolicy !== undefined) {
1766
1952
  const submissions = await countContractRecordSubmissions(runDir, nodeId);
1767
- if (submissions === 0) {
1953
+ if (submissions === 0 && result.stopReason !== "length") {
1768
1954
  result = {
1769
1955
  ...result,
1770
1956
  failureCategory: "empty-output",
@@ -1793,6 +1979,9 @@ export async function executeDagNode(input) {
1793
1979
  sdkAttempted: result.sdkAttempted,
1794
1980
  tokensUsed: result.tokensUsed,
1795
1981
  parsedEvents: result.parsedEvents,
1982
+ stopReason: result.stopReason,
1983
+ thinkingObserved: result.thinkingObserved,
1984
+ writeToolCallCount: result.writeToolCallCount,
1796
1985
  artifactPath: `${nodeId}/attempt-${attemptNumber}.json`,
1797
1986
  };
1798
1987
  if (retryPolicy !== undefined) {
@@ -1874,6 +2063,9 @@ export async function executeDagNode(input) {
1874
2063
  retryPolicy === undefined
1875
2064
  ? result.parsedEvents
1876
2065
  : sumAttemptMetric(attempts, (attempt) => attempt.parsedEvents);
2066
+ node.stopReason = result.stopReason;
2067
+ node.thinkingObserved = result.thinkingObserved;
2068
+ node.writeToolCallCount = result.writeToolCallCount;
1877
2069
  node.lastActivityAt = attemptFinishedAt;
1878
2070
  if (result.failureCategory === "termination-unconfirmed") {
1879
2071
  node.needsAttentionReason = "attempt-termination-unconfirmed";