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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +187 -92
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/recommendations.js +13 -2
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +429 -28
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/executors/dag-pi-executor.js +1691 -29
  17. package/dist/executors/pi-extension-resolver.js +233 -0
  18. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  19. package/dist/executors/pi-sdk-executor.js +180 -33
  20. package/dist/executors/shell-executor.js +1152 -171
  21. package/dist/executors/shell-write-guard.js +14 -0
  22. package/dist/shared/dag-prompt-override.js +28 -0
  23. package/dist/shared/openspec-spec.js +70 -4
  24. package/dist/shared/operator/capabilities.js +32 -0
  25. package/dist/shared/playwright-cli-command-policy.js +15 -0
  26. package/dist/shared/update/console-notifier.js +100 -0
  27. package/dist/{cli → shared}/update/npm-client.js +41 -8
  28. package/dist/{cli → shared}/update/state.js +41 -13
  29. package/dist/task/config-types.js +19 -5
  30. package/dist/task/contract/project.js +3 -0
  31. package/dist/task/contract/schema.js +2 -1
  32. package/dist/task/frontend-project-capability.js +60 -17
  33. package/dist/task/source-prepare/parse-intent.js +24 -4
  34. package/dist/task/source-prepare/prepare.js +36 -0
  35. package/dist/worker/cli.js +31 -1
  36. package/dist/worker/console/chat/pi-console-config.js +81 -10
  37. package/dist/worker/console/chat/routes.js +1 -0
  38. package/dist/worker/console/dag-execution-receipt.js +33 -0
  39. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  40. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  41. package/dist/worker/console/index.js +3 -0
  42. package/dist/worker/console/operation-store.js +226 -6
  43. package/dist/worker/console/operator-actions.js +263 -0
  44. package/dist/worker/console/operator-user-error.js +2 -2
  45. package/dist/worker/console/routes.js +43 -2
  46. package/dist/worker/console/security.js +63 -9
  47. package/dist/worker/console/server.js +151 -6
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
  81. package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  107. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  108. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  109. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  110. package/dist/worker/observe/node-input.js +74 -3
  111. package/dist/worker/observe/node-process.js +377 -0
  112. package/dist/worker/observe/routes.js +36 -3
  113. package/dist/worker/observe/shell-handler-keys.js +34 -0
  114. package/dist/worker/observe/static/api.js +66 -0
  115. package/dist/worker/observe/static/constants.js +8 -1
  116. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  117. package/dist/worker/observe/static/format.js +23 -0
  118. package/dist/worker/observe/static/markdown-render.js +20 -1
  119. package/dist/worker/observe/static/operator-chrome.js +4 -0
  120. package/dist/worker/observe/static/state.js +13 -2
  121. package/dist/worker/observe/static/styles.css +73 -0
  122. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  123. package/dist/worker/observe/static/views/dag.js +8 -1
  124. package/dist/worker/observe/static/views/failures.js +5 -2
  125. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  127. package/dist/workflows/dag/contract-output-registry.js +15 -0
  128. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  129. package/dist/workflows/dag/failure-category.js +12 -0
  130. package/dist/workflows/dag/frontend-closeout.js +221 -0
  131. package/dist/workflows/dag/frontend-design-policy.js +366 -0
  132. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  133. package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
  134. package/dist/workflows/dag/frontend-prewrite-gate.js +343 -391
  135. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  136. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  137. package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
  138. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  139. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  140. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  141. package/dist/workflows/dag/frontend-repair.js +7 -424
  142. package/dist/workflows/dag/frontend-review-context.js +195 -8
  143. package/dist/workflows/dag/frontend-risk.js +15 -2
  144. package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
  145. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  146. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  147. package/dist/workflows/dag/frontend-shape.js +425 -0
  148. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  149. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  150. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  151. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  152. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  153. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  154. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  155. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  156. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  157. package/dist/workflows/dag/frontend-typed-event-store.js +439 -0
  158. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  159. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  160. package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
  161. package/dist/workflows/dag/frontend-writer-admission.js +260 -0
  162. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  163. package/dist/workflows/dag/init-hybrid.js +572 -500
  164. package/dist/workflows/dag/node-execution.js +307 -70
  165. package/dist/workflows/dag/prompt.js +10 -18
  166. package/dist/workflows/dag/rerun-plan.js +144 -5
  167. package/dist/workflows/dag/rerun-run.js +58 -6
  168. package/dist/workflows/dag/retry-policy.js +228 -4
  169. package/dist/workflows/dag/runner.js +314 -119
  170. package/dist/workflows/dag/scheduler.js +114 -16
  171. package/dist/workflows/dag/structured-output-repair.js +712 -0
  172. package/dist/workflows/dag/types.js +235 -23
  173. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  174. package/dist/workflows/dag/validate.js +60 -10
  175. package/docs/init-surface.manifest.json +5 -0
  176. package/docs/operations/README.md +1 -1
  177. package/docs/templates/agent-dag.schema.json +44 -0
  178. package/docs/templates/frontend-test-dag.json +8 -10
  179. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  180. package/docs/templates/init-managed-agents.md +13 -3
  181. package/harness.json +3 -3
  182. package/package.json +5 -1
  183. package/scripts/next-info.mjs +356 -0
  184. package/scripts/next-publish-gate.mjs +238 -0
  185. package/scripts/release-source-binding.mjs +251 -0
  186. package/skills/codebase-scout/SKILL.md +1 -1
  187. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  188. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  189. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  190. package/skills/frontend-implementation/SKILL.md +4 -4
  191. package/skills/frontend-implementation/references/design-spec.md +11 -7
  192. package/skills/frontend-implementation/references/node-contracts.md +20 -11
  193. package/skills/loop-agent/references/command-reference.md +11 -4
  194. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  195. package/skills/playwright-cli/SKILL.md +22 -0
  196. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  197. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  198. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  199. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  200. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  201. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  202. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -10,11 +10,15 @@ import { resolveContextPolicy } from "./context-policy.js";
10
10
  import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
11
11
  import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
12
12
  import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
13
- import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
13
+ import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, projectFrontendNodeProtocolFailureReason, resolveFrontendPlanBackupRoute, resolveFrontendPlanRetryStep, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
14
14
  import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
15
15
  import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
16
16
  import { getStructuredContractValidator } from "./contract-output-registry.js";
17
17
  import "./contract-validator-registrations.js";
18
+ import { computeNormalizedFailureFingerprint } from "./frontend-recovery-lineage.js";
19
+ import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
20
+ import { allowedRepairReadPaths, auditRepairAttemptToolUse, buildStructuredOutputRepairPrompt, freezeStructuredOutputRepairContext, hasNonEmptyStructuredCandidate, isFrontendStructuredRepairSchemaId, isStructuredRepairableFailureCategory, persistStructuredAttemptRaw, sessionEventsByteLength, GOVERNANCE_BLOCKED_CATEGORY, STRUCTURED_REPAIR_EXHAUSTED_CATEGORY, } from "./structured-output-repair.js";
21
+ import { readFrontendCanonicalCandidate } from "./frontend-implementation-contract.js";
18
22
  import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
19
23
  import { buildProjectGovernanceContext, readCompletedWriterChangeManifests, writeProjectGovernanceContext, } from "./project-governance-context.js";
20
24
  import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
@@ -129,26 +133,29 @@ export function buildNodePrompt(spec, task, upstream, options) {
129
133
  convergenceFeedback: options?.convergenceFeedback,
130
134
  });
131
135
  }
132
- function frontendStructuredArtifactRetryGuidance(schemaId, citationsRequired = true) {
133
- const format = citationsRequired
134
- ? "one fenced json block immediately followed by one fenced openspec-citations block"
135
- : "one fenced json block and no openspec-citations block";
136
+ function frontendStructuredArtifactRetryGuidance(schemaId) {
136
137
  if (schemaId === "frontend-implementation-contract-plan-patch-v1") {
137
138
  return [
138
- citationsRequired
139
- ? "Return exactly one fenced json block containing only the editable RFC 7386 plan patch, immediately followed by one fenced openspec-citations block, and nothing else."
140
- : `Return exactly ${format} and nothing else; the json block contains only the editable RFC 7386 plan patch.`,
139
+ "Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing only the editable RFC 7386 plan patch, immediately followed by exactly one fenced openspec-citations block.",
141
140
  "The runtime applies your patch to its protected contract skeleton. Do not emit schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff; do not emit the full contract.",
142
141
  "Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
142
+ "The openspec-citations fenced block contains one JSON object per line, each with path/section/line — for example:",
143
+ "```openspec-citations",
144
+ '{"path":".harness/tasks/<task>/source/需求.md","section":"Objective","line":5}',
145
+ '{"path":".harness/tasks/<task>/source/references/product-requirement.md","section":"FR-1","line":16}',
146
+ "```",
143
147
  ];
144
148
  }
145
149
  if (schemaId === "frontend-implementation-contract-revision-patch-v1") {
146
150
  return [
147
- citationsRequired
148
- ? "Return exactly one fenced json block containing the RFC 7386 merge-patch delta, immediately followed by one fenced openspec-citations block, and nothing else."
149
- : `Return exactly ${format} and nothing else; the json block contains the RFC 7386 merge-patch delta.`,
151
+ "Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing the RFC 7386 merge-patch delta, immediately followed by exactly one fenced openspec-citations block.",
150
152
  "The delta contains only the fields you change; null deletes a key, arrays and scalars replace, and plain objects merge recursively. Do not emit a full contract or a schemaVersion/targets section.",
151
153
  "Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
154
+ "The openspec-citations fenced block contains one JSON object per line, each with path/section/line — for example:",
155
+ "```openspec-citations",
156
+ '{"path":".harness/tasks/<task>/source/需求.md","section":"Objective","line":5}',
157
+ '{"path":".harness/tasks/<task>/source/references/product-requirement.md","section":"FR-1","line":16}',
158
+ "```",
152
159
  ];
153
160
  }
154
161
  return [
@@ -156,30 +163,62 @@ function frontendStructuredArtifactRetryGuidance(schemaId, citationsRequired = t
156
163
  "The contract JSON fields are the complete implementation plan. Do not emit a separate plan document, Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
157
164
  ];
158
165
  }
159
- function isFrontendPlanArtifactSchema(schemaId) {
160
- return (schemaId === "frontend-implementation-contract-plan-patch-v1" ||
161
- schemaId === "frontend-implementation-contract-revision-patch-v1");
166
+ const FRONTEND_PLAN_NODE_ID = "frontend-plan-pi";
167
+ function isFrontendPlanLadderTask(task) {
168
+ return task.id === FRONTEND_PLAN_NODE_ID;
162
169
  }
163
- function buildFrontendPlanArtifactRecoveryPrompt(options) {
164
- const cause = options.failure === "truncated"
165
- ? "Previous response was truncated by the provider (stopReason=length)."
166
- : options.failure === "too-large"
167
- ? "Previous response could not be retained by the provider."
168
- : "Previous response failed structured validation.";
169
- return [
170
- "<retry_instruction>",
171
- cause,
172
- options.reason ? `Validation error: ${options.reason}` : "",
173
- ...frontendStructuredArtifactRetryGuidance(options.schemaId, options.citationsRequired),
174
- "Use concise values and reference paths/IDs instead of copying source prose. Do not omit required semantics.",
175
- "</retry_instruction>",
176
- ]
177
- .filter(Boolean)
178
- .join("\n");
170
+ /**
171
+ * Read the committed plan ledger snapshot for the ladder's "no new committed
172
+ * fact" check. The digest is over the sorted committed payload hashes, and
173
+ * `hasTerminalFact` reflects a committed `finalize_plan` terminal fact.
174
+ */
175
+ async function readFrontendPlanCommittedSnapshot(runDir, nodeId) {
176
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, nodeId, "plan-typed-facts.jsonl"));
177
+ const committed = records.filter((record) => record.phase === "committed");
178
+ const digest = createHash("sha256")
179
+ .update(committed
180
+ .map((record) => record.payloadSha256)
181
+ .sort()
182
+ .join("\n"))
183
+ .digest("hex");
184
+ const hasTerminalFact = committed.some((record) => record.fact.kind === "finalize_plan");
185
+ return { digest, hasTerminalFact };
179
186
  }
180
- function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics) {
187
+ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep) {
181
188
  if (attemptNumber <= 1)
182
189
  return basePrompt;
190
+ if (frontendPlanRetryStep === "compact-terminal-first") {
191
+ return [
192
+ basePrompt,
193
+ "",
194
+ "<retry_instruction>",
195
+ "Frontend plan retry ladder step: compact-terminal-first.",
196
+ "Do NOT rewrite the narrative. Only adopt already staged/quarantined fresh facts, fill the missing required facts, then call finalize_plan exactly once.",
197
+ "Keep the existing committed ledger intact; do not re-derive already committed facts.",
198
+ "</retry_instruction>",
199
+ ].join("\n");
200
+ }
201
+ if (frontendPlanRetryStep === "bounded-tool-only") {
202
+ return [
203
+ basePrompt,
204
+ "",
205
+ "<retry_instruction>",
206
+ "Frontend plan retry ladder step: bounded-tool-only.",
207
+ "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.",
208
+ "Do not expand scope or re-derive already committed facts.",
209
+ "</retry_instruction>",
210
+ ].join("\n");
211
+ }
212
+ if (frontendPlanRetryStep === "backup-model") {
213
+ return [
214
+ basePrompt,
215
+ "",
216
+ "<retry_instruction>",
217
+ "Frontend plan retry ladder step: backup-model.",
218
+ "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.",
219
+ "</retry_instruction>",
220
+ ].join("\n");
221
+ }
183
222
  if (previousFailureCategory === "protocol-invalid" &&
184
223
  task.outputProtocol &&
185
224
  previousProtocolReason) {
@@ -192,13 +231,23 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
192
231
  if (previousFailureCategory === "invalid-output" &&
193
232
  task.structuredContractOutput &&
194
233
  previousProtocolReason) {
195
- if (isFrontendPlanArtifactSchema(task.structuredContractOutput.schemaId)) {
196
- return buildFrontendPlanArtifactRecoveryPrompt({
197
- failure: "invalid",
198
- reason: previousProtocolReason,
199
- schemaId: task.structuredContractOutput.schemaId,
200
- citationsRequired: task.structuredContractOutput.citationsRequired,
201
- });
234
+ // The frontend plan node's compile authority is the committed typed
235
+ // ledger, not a fenced JSON text artifact: its retry guidance must
236
+ // direct the model to re-commit corrected record_* facts and
237
+ // finalize_plan. The legacy full-contract JSON guidance below applies
238
+ // only to nodes whose authority is still a text contract artifact.
239
+ if (task.structuredContractOutput.schemaId ===
240
+ "frontend-implementation-contract-plan-patch-v1") {
241
+ return [
242
+ basePrompt,
243
+ "",
244
+ "<retry_instruction>",
245
+ "Previous plan ledger facts failed canonical contract validation:",
246
+ previousProtocolReason,
247
+ "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.",
248
+ "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.",
249
+ "</retry_instruction>",
250
+ ].join("\n");
202
251
  }
203
252
  return [
204
253
  basePrompt,
@@ -213,14 +262,17 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
213
262
  }
214
263
  if (previousFailureCategory === "structured-output-truncated" &&
215
264
  task.structuredContractOutput) {
216
- const schemaId = task.structuredContractOutput.schemaId;
217
- if (isFrontendPlanArtifactSchema(schemaId)) {
218
- return buildFrontendPlanArtifactRecoveryPrompt({
219
- failure: "truncated",
220
- reason: previousProtocolReason,
221
- schemaId,
222
- citationsRequired: task.structuredContractOutput.citationsRequired,
223
- });
265
+ if (task.structuredContractOutput.schemaId ===
266
+ "frontend-implementation-contract-plan-patch-v1") {
267
+ return [
268
+ basePrompt,
269
+ "",
270
+ "<retry_instruction>",
271
+ "Previous attempt was truncated by the provider (stopReason=length) before the plan facts were fully committed.",
272
+ "Re-commit the missing record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority.",
273
+ "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.",
274
+ "</retry_instruction>",
275
+ ].join("\n");
224
276
  }
225
277
  return [
226
278
  basePrompt,
@@ -293,17 +345,32 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
293
345
  "</retry_instruction>",
294
346
  ].join("\n");
295
347
  }
348
+ if (previousFailureCategory === "writer-clean-timeout") {
349
+ return [
350
+ basePrompt,
351
+ "",
352
+ "<retry_instruction>",
353
+ "Previous attempt ended in a clean provider/transport failure with zero attributed workspace writes and zero write/edit tool calls.",
354
+ "Do not invent existing implementation progress. Continue from the original task only if the workspace is still unchanged.",
355
+ "</retry_instruction>",
356
+ ].join("\n");
357
+ }
296
358
  if (previousFailureCategory !== "output-too-large") {
297
359
  return basePrompt;
298
360
  }
299
361
  if (task.outputMode === "structured-required") {
300
362
  if (task.structuredContractOutput) {
301
- if (isFrontendPlanArtifactSchema(task.structuredContractOutput.schemaId)) {
302
- return buildFrontendPlanArtifactRecoveryPrompt({
303
- failure: "too-large",
304
- schemaId: task.structuredContractOutput.schemaId,
305
- citationsRequired: task.structuredContractOutput.citationsRequired,
306
- });
363
+ if (task.structuredContractOutput.schemaId ===
364
+ "frontend-implementation-contract-plan-patch-v1") {
365
+ return [
366
+ basePrompt,
367
+ "",
368
+ "<retry_instruction>",
369
+ "Previous attempt exceeded the structured output size limit.",
370
+ "Re-commit corrected record_* facts and call finalize_plan exactly once; the committed typed ledger is the only compile authority.",
371
+ "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.",
372
+ "</retry_instruction>",
373
+ ].join("\n");
307
374
  }
308
375
  return [
309
376
  basePrompt,
@@ -725,7 +792,7 @@ export async function executeDagNode(input) {
725
792
  }
726
793
  node.resolvedSkills = resolvedSkills;
727
794
  await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
728
- const model = resolveModelForTask(task, spec.executorModels);
795
+ let model = resolveModelForTask(task, spec.executorModels);
729
796
  let thinking;
730
797
  if (task.executor === "pi") {
731
798
  try {
@@ -736,16 +803,25 @@ export async function executeDagNode(input) {
736
803
  // Invalid or missing harness preserves the previous no-override behavior.
737
804
  }
738
805
  }
739
- const retryPolicy = task.retryPolicy &&
806
+ const specRetryPolicy = task.retryPolicy &&
740
807
  (isSafeReadOnlyPiRetryCandidate(task) ||
741
808
  isWriterEmptyDiffRetryCandidate(task) ||
742
809
  isWriterTransportRetryCandidate(task))
743
810
  ? task.retryPolicy
744
811
  : undefined;
812
+ const implicitVerifyRetryPolicy = specRetryPolicy === undefined &&
813
+ isCanonicalFinalVerifyShellRetryCandidate(task) &&
814
+ isTargetTemplateTransientRetryNode(task)
815
+ ? VERIFY_SHELL_RETRY_POLICY
816
+ : undefined;
817
+ const retryPolicy = specRetryPolicy ?? implicitVerifyRetryPolicy;
745
818
  const maxAttempts = retryPolicy?.maxAttempts ?? 1;
746
819
  const attempts = [];
747
820
  let totalAttemptWallDurationMs = 0;
748
821
  let totalBackoffMs = 0;
822
+ const frontendPlanLadderEnabled = isFrontendPlanLadderTask(task);
823
+ let frontendPlanRetryStep = "normal";
824
+ const frontendPriorFingerprints = [];
749
825
  const livenessPolicy = resolveLivenessPolicy(spec.defaults?.livenessPolicy, task.livenessPolicy);
750
826
  /**
751
827
  * Attempt-fenced, throttled activity sink. Late events from a previous
@@ -783,10 +859,24 @@ export async function executeDagNode(input) {
783
859
  let terminalResult;
784
860
  let previousFailureCategory;
785
861
  let previousProtocolReason;
862
+ let pendingStructuredArtifact;
863
+ let lastRepairAllowlist = [];
786
864
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
787
865
  const attemptStartedAt = new Date().toISOString();
788
866
  node.currentAttempt = attemptNumber;
789
867
  node.livenessStatus = "active";
868
+ const beforeCommittedDigest = frontendPlanLadderEnabled
869
+ ? (await readFrontendPlanCommittedSnapshot(runDir, nodeId)).digest
870
+ : undefined;
871
+ if (frontendPlanLadderEnabled &&
872
+ frontendPlanRetryStep === "backup-model") {
873
+ model = resolveModelForTask(task, spec.executorModels);
874
+ }
875
+ // Capture the session-events.jsonl length BEFORE the executor appends this
876
+ // attempt's events, so the post-attempt repair tool audit can be scoped to
877
+ // exactly this attempt's segment (B1: earlier attempts legitimately use
878
+ // ls/find and must not be attributed to the repair attempt).
879
+ const attemptSessionEventsOffset = await sessionEventsByteLength(runDir, nodeId);
790
880
  let result;
791
881
  try {
792
882
  validateRepairArtifactGateBeforeShell({
@@ -809,12 +899,60 @@ export async function executeDagNode(input) {
809
899
  (acc[i.path] ??= []).push(i.detail);
810
900
  return acc;
811
901
  }, {});
902
+ let attemptPrompt = buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep);
903
+ if (attemptNumber > 1 &&
904
+ // The plan node (frontend-plan-pi) is a typed-facts ladder task:
905
+ // its retry is driven by the §5.1 ladder (compact-terminal-first
906
+ // / backup-model) and its compile authority is the committed
907
+ // ledger — never the legacy frozen fenced-JSON repair. The
908
+ // frozen repair path below applies only to non-ladder
909
+ // structured nodes whose output authority is a text artifact.
910
+ !frontendPlanLadderEnabled &&
911
+ isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId) &&
912
+ isStructuredRepairableFailureCategory(previousFailureCategory) &&
913
+ (await hasNonEmptyStructuredCandidate({
914
+ runDir,
915
+ nodeId,
916
+ beforeAttempt: attemptNumber,
917
+ }))) {
918
+ const originalCanonical = task.structuredContractOutput?.schemaId ===
919
+ "frontend-implementation-contract-revision-patch-v1"
920
+ ? (await readFrontendCanonicalCandidate(runDir, "frontend-plan-pi"))
921
+ : undefined;
922
+ const frozen = await freezeStructuredOutputRepairContext({
923
+ runDir,
924
+ runId: state.runId,
925
+ taskId: spec.sourceBinding?.taskId ?? task.id,
926
+ nodeId,
927
+ nodeAttempt: attemptNumber,
928
+ targetSchemaId: task.structuredContractOutput.schemaId,
929
+ skeleton: task.structuredContractOutput?.skeleton,
930
+ originalCanonical,
931
+ latestStopReason: previousFailureCategory === "structured-output-truncated"
932
+ ? "length"
933
+ : previousFailureCategory,
934
+ });
935
+ lastRepairAllowlist = allowedRepairReadPaths({
936
+ runDir,
937
+ context: frozen.context,
938
+ contextPath: frozen.relativePath,
939
+ });
940
+ attemptPrompt = await buildStructuredOutputRepairPrompt({
941
+ runDir,
942
+ context: frozen.context,
943
+ contextPath: frozen.relativePath,
944
+ contextSha256: frozen.sha256,
945
+ maxAttempts,
946
+ schemaGuidance: frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
947
+ previousReason: previousProtocolReason,
948
+ });
949
+ }
812
950
  result = await executeNode({
813
951
  task,
814
952
  cwd,
815
953
  model,
816
954
  ...(thinking ? { thinking } : {}),
817
- prompt: buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics),
955
+ prompt: attemptPrompt,
818
956
  attempt: attemptNumber,
819
957
  reportActivity,
820
958
  timeoutMs: livenessPolicy.absoluteMaxWallClockMs,
@@ -830,6 +968,35 @@ export async function executeDagNode(input) {
830
968
  durationMs: 0,
831
969
  };
832
970
  }
971
+ if (isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId)) {
972
+ const rawText = canonicalNodeOutput(result);
973
+ if (rawText.trim().length > 0) {
974
+ await persistStructuredAttemptRaw({
975
+ runDir,
976
+ nodeId,
977
+ attempt: attemptNumber,
978
+ text: rawText,
979
+ });
980
+ }
981
+ if (attemptNumber > 1 && lastRepairAllowlist.length > 0) {
982
+ const audit = await auditRepairAttemptToolUse({
983
+ runDir,
984
+ nodeId,
985
+ allowedPaths: lastRepairAllowlist,
986
+ fromByteOffset: attemptSessionEventsOffset,
987
+ });
988
+ if (!audit.ok) {
989
+ result = {
990
+ ...result,
991
+ ok: false,
992
+ failureCategory: GOVERNANCE_BLOCKED_CATEGORY,
993
+ stderr: [result.stderr, audit.reason]
994
+ .filter(Boolean)
995
+ .join("\n"),
996
+ };
997
+ }
998
+ }
999
+ }
833
1000
  // R0: executor ok=true still fails closed when outputProtocol is violated.
834
1001
  // Valid semantic results (e.g. VERDICT: request-revision) pass validation.
835
1002
  if (result.ok && task.outputProtocol) {
@@ -866,16 +1033,17 @@ export async function executeDagNode(input) {
866
1033
  structuredContractOutput: task.structuredContractOutput,
867
1034
  });
868
1035
  if (!contractCheck.ok) {
869
- // A provider stopReason=length means the response was cut before the
870
- // JSON contract could complete; separate it from an ordinary bad
871
- // contract so the retry switches to a JSON-only output strategy.
872
1036
  const attemptStopReason = result.stopReason;
1037
+ const failureCategory = contractCheck.classification === "governance-blocked"
1038
+ ? GOVERNANCE_BLOCKED_CATEGORY
1039
+ : attemptStopReason === "length" ||
1040
+ contractCheck.classification === "truncated"
1041
+ ? "structured-output-truncated"
1042
+ : "invalid-output";
873
1043
  result = {
874
1044
  ...result,
875
1045
  ok: false,
876
- failureCategory: attemptStopReason === "length"
877
- ? "structured-output-truncated"
878
- : "invalid-output",
1046
+ failureCategory,
879
1047
  stderr: [result.stderr, contractCheck.reason]
880
1048
  .filter(Boolean)
881
1049
  .join("\n"),
@@ -883,14 +1051,6 @@ export async function executeDagNode(input) {
883
1051
  previousProtocolReason = contractCheck.reason;
884
1052
  }
885
1053
  else {
886
- if (contractCheck.structuredArtifact) {
887
- node.structuredArtifactPath = contractCheck.structuredArtifact.path;
888
- node.structuredArtifactSha256 = contractCheck.structuredArtifact.sha256;
889
- node.structuredArtifactSchemaId = contractCheck.structuredArtifact.schemaId;
890
- }
891
- if (contractCheck.frontendPlanOutput) {
892
- node.frontendPlanOutput = contractCheck.frontendPlanOutput;
893
- }
894
1054
  if (contractCheck.normalizedText) {
895
1055
  result = {
896
1056
  ...result,
@@ -898,6 +1058,9 @@ export async function executeDagNode(input) {
898
1058
  stdout: contractCheck.normalizedText,
899
1059
  };
900
1060
  }
1061
+ if (contractCheck.artifact) {
1062
+ pendingStructuredArtifact = contractCheck.artifact;
1063
+ }
901
1064
  previousProtocolReason = undefined;
902
1065
  }
903
1066
  }
@@ -910,6 +1073,7 @@ export async function executeDagNode(input) {
910
1073
  const attemptFinishedAt = new Date().toISOString();
911
1074
  const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
912
1075
  totalAttemptWallDurationMs += attemptWallDurationMs;
1076
+ const rawFailureCategory = result.rawFailureCategory ?? result.failureCategory;
913
1077
  const attemptRecord = {
914
1078
  attempt: attemptNumber,
915
1079
  startedAt: attemptStartedAt,
@@ -921,6 +1085,7 @@ export async function executeDagNode(input) {
921
1085
  stderr: result.stderr,
922
1086
  assistantText: result.assistantText,
923
1087
  failureCategory: result.failureCategory,
1088
+ ...(rawFailureCategory ? { rawFailureCategory } : {}),
924
1089
  backend: result.backend,
925
1090
  sdkAttempted: result.sdkAttempted,
926
1091
  tokensUsed: result.tokensUsed,
@@ -935,6 +1100,60 @@ export async function executeDagNode(input) {
935
1100
  await writeDagNodeJsonArtifact(runDir, nodeId, `attempt-${attemptNumber}.json`, attemptRecord);
936
1101
  node.attempts = attempts;
937
1102
  }
1103
+ // Frontend plan retry ladder: project the 7-value protocol failure
1104
+ // reason, detect new committed facts, and escalate instead of re-running.
1105
+ if (frontendPlanLadderEnabled && !result.ok) {
1106
+ const afterSnapshot = await readFrontendPlanCommittedSnapshot(runDir, nodeId);
1107
+ const hasNewCommittedFact = beforeCommittedDigest !== undefined &&
1108
+ afterSnapshot.digest !== beforeCommittedDigest;
1109
+ const protocolReason = projectFrontendNodeProtocolFailureReason({
1110
+ failureCategory: result.failureCategory,
1111
+ stopReason: result.stopReason,
1112
+ assistantText: result.assistantText ?? result.stdout,
1113
+ hasTerminalFact: afterSnapshot.hasTerminalFact,
1114
+ });
1115
+ const fingerprint = computeNormalizedFailureFingerprint({
1116
+ failureOwner: "plan",
1117
+ protocolFailureReason: protocolReason ?? "",
1118
+ });
1119
+ const backupRoute = resolveFrontendPlanBackupRoute(protocolReason);
1120
+ const nextStep = resolveFrontendPlanRetryStep({
1121
+ currentStep: frontendPlanRetryStep,
1122
+ protocolFailureReason: protocolReason,
1123
+ hasNewCommittedFact,
1124
+ fingerprint,
1125
+ priorFingerprints: frontendPriorFingerprints,
1126
+ backupRouteAvailable: backupRoute.ok,
1127
+ });
1128
+ frontendPriorFingerprints.push(fingerprint);
1129
+ frontendPlanRetryStep = nextStep;
1130
+ if (nextStep === "unsupported-provider-capability") {
1131
+ result = {
1132
+ ...result,
1133
+ ok: false,
1134
+ failureCategory: "unsupported-provider-capability",
1135
+ stderr: [
1136
+ result.stderr,
1137
+ "frontend plan retry ladder: no compatible provider capability route for the protocol failure reason",
1138
+ ]
1139
+ .filter(Boolean)
1140
+ .join("\n"),
1141
+ };
1142
+ }
1143
+ else if (nextStep === "non-converging") {
1144
+ result = {
1145
+ ...result,
1146
+ ok: false,
1147
+ failureCategory: "non-converging",
1148
+ stderr: [
1149
+ result.stderr,
1150
+ "frontend plan retry ladder: repeated failure fingerprint with no new committed fact",
1151
+ ]
1152
+ .filter(Boolean)
1153
+ .join("\n"),
1154
+ };
1155
+ }
1156
+ }
938
1157
  // Reflect the latest attempt on the node so progress is observable,
939
1158
  // but keep node.status RUNNING while retry is still possible.
940
1159
  node.durationMs = durationBetween(node.startedAt, attemptFinishedAt);
@@ -968,13 +1187,26 @@ export async function executeDagNode(input) {
968
1187
  break;
969
1188
  const canRetry = retryPolicy !== undefined && attemptNumber < maxAttempts;
970
1189
  const retryable = retryPolicy !== undefined &&
1190
+ result.failureCategory !== GOVERNANCE_BLOCKED_CATEGORY &&
971
1191
  (isRetryablePiFailureCategory(result.failureCategory, {
972
1192
  retryCategories: retryPolicy.retryCategories,
973
1193
  }) ||
974
1194
  (result.failureCategory === "protocol-invalid" &&
975
1195
  task.outputProtocol?.retryOnInvalid === true));
976
- if (!canRetry || !retryable)
1196
+ if (!canRetry || !retryable) {
1197
+ if (!canRetry &&
1198
+ isFrontendStructuredRepairSchemaId(task.structuredContractOutput?.schemaId) &&
1199
+ isStructuredRepairableFailureCategory(result.failureCategory)) {
1200
+ result = {
1201
+ ...result,
1202
+ failureCategory: STRUCTURED_REPAIR_EXHAUSTED_CATEGORY,
1203
+ };
1204
+ terminalResult = result;
1205
+ previousFailureCategory = result.failureCategory;
1206
+ node.failureCategory = result.failureCategory;
1207
+ }
977
1208
  break;
1209
+ }
978
1210
  const delayMs = computeBackoffDelayMs(attemptNumber + 1, retryPolicy);
979
1211
  if (delayMs > 0) {
980
1212
  const backoffStartedAt = new Date().toISOString();
@@ -1123,6 +1355,11 @@ export async function executeDagNode(input) {
1123
1355
  node.structuredArtifactSchemaId = "backend-test-result-v1";
1124
1356
  }
1125
1357
  }
1358
+ else if (pendingStructuredArtifact) {
1359
+ node.structuredArtifactPath = pendingStructuredArtifact.path;
1360
+ node.structuredArtifactSha256 = pendingStructuredArtifact.sha256;
1361
+ node.structuredArtifactSchemaId = pendingStructuredArtifact.schemaId;
1362
+ }
1126
1363
  }
1127
1364
  const finishedAt = new Date().toISOString();
1128
1365
  node.finishedAt = finishedAt;
@@ -67,7 +67,6 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
67
67
  const toleratedFailures = new Set(task.failureAwareDependsOn ?? []);
68
68
  for (const depId of task.depends_on) {
69
69
  const record = upstream[depId];
70
- const artifactFirst = task.artifactFirstUpstreamNodeIds?.includes(depId);
71
70
  const stdout = record?.stdout?.trim() ? record.stdout : "";
72
71
  const assistantText = record?.assistantText?.trim()
73
72
  ? record.assistantText
@@ -119,23 +118,16 @@ export function buildUpstreamContext(task, upstream, maxChars = MAX_UPSTREAM_CHA
119
118
  }
120
119
  if (!record || record.status !== "FINISHED")
121
120
  continue;
122
- if (artifactFirst) {
123
- const lines = [
124
- `## Upstream artifact: ${depId}`,
125
- "Artifact-first input: do not infer fields from a preview. Read the hash-bound payload only when a field is required; do not edit runner evidence.",
126
- ];
127
- if (record.structuredArtifactPath &&
128
- record.structuredArtifactSha256 &&
129
- record.structuredArtifactSchemaId) {
130
- lines.push(`- payload: ${record.structuredArtifactPath}`, `- schema: ${record.structuredArtifactSchemaId}`, `- sha256: ${record.structuredArtifactSha256}`);
131
- if (record.frontendPlanOutput) {
132
- lines.push(`- payload kind: ${record.frontendPlanOutput.payloadKind}`, `- citations: ${record.frontendPlanOutput.citationsArtifactPath}`, `- citations sha256: ${record.frontendPlanOutput.citationsArtifactSha256}`);
133
- }
134
- }
135
- else {
136
- lines.push("- artifact: unavailable (do not reconstruct the contract from node prose; treat this as an upstream artifact integrity failure)");
137
- }
138
- sections.push(lines.join("\n"));
121
+ if (record.structuredArtifactPath &&
122
+ record.structuredArtifactSchemaId ===
123
+ "frontend-implementation-contract-v1") {
124
+ sections.push([
125
+ `## Upstream output: ${depId}`,
126
+ "Validated structured artifact (read-only runner evidence — use read tool to fetch; do not edit). Do not infer the contract from stdout.",
127
+ `- path: ${record.structuredArtifactPath}`,
128
+ `- schema: ${record.structuredArtifactSchemaId}`,
129
+ `- sha256: ${record.structuredArtifactSha256 ?? "unknown"}`,
130
+ ].join("\n"));
139
131
  continue;
140
132
  }
141
133
  if (!upstreamText)