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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +187 -92
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/recommendations.js +13 -2
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +429 -28
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/executors/dag-pi-executor.js +1645 -29
  17. package/dist/executors/pi-extension-resolver.js +233 -0
  18. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  19. package/dist/executors/pi-sdk-executor.js +180 -33
  20. package/dist/executors/shell-executor.js +1150 -171
  21. package/dist/executors/shell-write-guard.js +14 -0
  22. package/dist/shared/dag-prompt-override.js +28 -0
  23. package/dist/shared/openspec-spec.js +70 -4
  24. package/dist/shared/operator/capabilities.js +32 -0
  25. package/dist/shared/playwright-cli-command-policy.js +15 -0
  26. package/dist/shared/update/console-notifier.js +100 -0
  27. package/dist/{cli → shared}/update/npm-client.js +41 -8
  28. package/dist/{cli → shared}/update/state.js +41 -13
  29. package/dist/task/config-types.js +19 -5
  30. package/dist/task/contract/project.js +3 -0
  31. package/dist/task/contract/schema.js +2 -1
  32. package/dist/task/frontend-project-capability.js +42 -16
  33. package/dist/task/source-prepare/parse-intent.js +24 -4
  34. package/dist/task/source-prepare/prepare.js +36 -0
  35. package/dist/worker/cli.js +31 -1
  36. package/dist/worker/console/chat/pi-console-config.js +81 -10
  37. package/dist/worker/console/chat/routes.js +1 -0
  38. package/dist/worker/console/dag-execution-receipt.js +33 -0
  39. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  40. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  41. package/dist/worker/console/index.js +3 -0
  42. package/dist/worker/console/operation-store.js +226 -6
  43. package/dist/worker/console/operator-actions.js +263 -0
  44. package/dist/worker/console/operator-user-error.js +2 -2
  45. package/dist/worker/console/routes.js +43 -2
  46. package/dist/worker/console/security.js +63 -9
  47. package/dist/worker/console/server.js +151 -6
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
  81. package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  107. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  108. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  109. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  110. package/dist/worker/observe/node-input.js +74 -3
  111. package/dist/worker/observe/node-process.js +377 -0
  112. package/dist/worker/observe/routes.js +36 -3
  113. package/dist/worker/observe/shell-handler-keys.js +34 -0
  114. package/dist/worker/observe/static/api.js +66 -0
  115. package/dist/worker/observe/static/constants.js +8 -1
  116. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  117. package/dist/worker/observe/static/format.js +23 -0
  118. package/dist/worker/observe/static/markdown-render.js +20 -1
  119. package/dist/worker/observe/static/operator-chrome.js +4 -0
  120. package/dist/worker/observe/static/state.js +13 -2
  121. package/dist/worker/observe/static/styles.css +73 -0
  122. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  123. package/dist/worker/observe/static/views/dag.js +8 -1
  124. package/dist/worker/observe/static/views/failures.js +5 -2
  125. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  127. package/dist/workflows/dag/contract-output-registry.js +15 -0
  128. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  129. package/dist/workflows/dag/failure-category.js +12 -0
  130. package/dist/workflows/dag/frontend-closeout.js +221 -0
  131. package/dist/workflows/dag/frontend-design-policy.js +347 -0
  132. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  133. package/dist/workflows/dag/frontend-implementation-contract.js +378 -150
  134. package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
  135. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  136. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  137. package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
  138. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  139. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  140. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  141. package/dist/workflows/dag/frontend-repair.js +7 -424
  142. package/dist/workflows/dag/frontend-review-context.js +195 -8
  143. package/dist/workflows/dag/frontend-risk.js +15 -2
  144. package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
  145. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  146. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  147. package/dist/workflows/dag/frontend-shape.js +425 -0
  148. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  149. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  150. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  151. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  152. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  153. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  154. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  155. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  156. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  157. package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
  158. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  159. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  160. package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
  161. package/dist/workflows/dag/frontend-writer-admission.js +260 -0
  162. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  163. package/dist/workflows/dag/init-hybrid.js +570 -457
  164. package/dist/workflows/dag/node-execution.js +306 -59
  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 -19
  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";
@@ -132,16 +136,26 @@ export function buildNodePrompt(spec, task, upstream, options) {
132
136
  function frontendStructuredArtifactRetryGuidance(schemaId) {
133
137
  if (schemaId === "frontend-implementation-contract-plan-patch-v1") {
134
138
  return [
135
- "Return exactly two blocks and nothing else: one fenced json block containing only the editable RFC 7386 plan patch, immediately followed by one fenced openspec-citations block.",
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.",
136
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.",
137
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
+ "```",
138
147
  ];
139
148
  }
140
149
  if (schemaId === "frontend-implementation-contract-revision-patch-v1") {
141
150
  return [
142
- "Return exactly two blocks and nothing else: one fenced json block containing the RFC 7386 merge-patch delta, immediately followed by one fenced openspec-citations block.",
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.",
143
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.",
144
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
+ "```",
145
159
  ];
146
160
  }
147
161
  return [
@@ -149,30 +163,62 @@ function frontendStructuredArtifactRetryGuidance(schemaId) {
149
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.",
150
164
  ];
151
165
  }
152
- function isFrontendPlanArtifactSchema(schemaId) {
153
- return (schemaId === "frontend-implementation-contract-plan-patch-v1" ||
154
- 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;
155
169
  }
156
- function buildFrontendPlanArtifactRecoveryPrompt(options) {
157
- const cause = options.failure === "truncated"
158
- ? "Previous response was truncated by the provider (stopReason=length)."
159
- : options.failure === "too-large"
160
- ? "Previous response could not be retained by the provider."
161
- : "Previous response failed structured validation.";
162
- return [
163
- "<retry_instruction>",
164
- cause,
165
- options.reason ? `Validation error: ${options.reason}` : "",
166
- ...frontendStructuredArtifactRetryGuidance(options.schemaId),
167
- "Use concise values and reference paths/IDs instead of copying source prose. Do not omit required semantics.",
168
- "</retry_instruction>",
169
- ]
170
- .filter(Boolean)
171
- .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 };
172
186
  }
173
- function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics) {
187
+ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics, frontendPlanRetryStep) {
174
188
  if (attemptNumber <= 1)
175
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
+ }
176
222
  if (previousFailureCategory === "protocol-invalid" &&
177
223
  task.outputProtocol &&
178
224
  previousProtocolReason) {
@@ -185,12 +231,23 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
185
231
  if (previousFailureCategory === "invalid-output" &&
186
232
  task.structuredContractOutput &&
187
233
  previousProtocolReason) {
188
- if (isFrontendPlanArtifactSchema(task.structuredContractOutput.schemaId)) {
189
- return buildFrontendPlanArtifactRecoveryPrompt({
190
- failure: "invalid",
191
- reason: previousProtocolReason,
192
- schemaId: task.structuredContractOutput.schemaId,
193
- });
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");
194
251
  }
195
252
  return [
196
253
  basePrompt,
@@ -205,13 +262,17 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
205
262
  }
206
263
  if (previousFailureCategory === "structured-output-truncated" &&
207
264
  task.structuredContractOutput) {
208
- const schemaId = task.structuredContractOutput.schemaId;
209
- if (isFrontendPlanArtifactSchema(schemaId)) {
210
- return buildFrontendPlanArtifactRecoveryPrompt({
211
- failure: "truncated",
212
- reason: previousProtocolReason,
213
- schemaId,
214
- });
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");
215
276
  }
216
277
  return [
217
278
  basePrompt,
@@ -284,16 +345,32 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
284
345
  "</retry_instruction>",
285
346
  ].join("\n");
286
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
+ }
287
358
  if (previousFailureCategory !== "output-too-large") {
288
359
  return basePrompt;
289
360
  }
290
361
  if (task.outputMode === "structured-required") {
291
362
  if (task.structuredContractOutput) {
292
- if (isFrontendPlanArtifactSchema(task.structuredContractOutput.schemaId)) {
293
- return buildFrontendPlanArtifactRecoveryPrompt({
294
- failure: "too-large",
295
- schemaId: task.structuredContractOutput.schemaId,
296
- });
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");
297
374
  }
298
375
  return [
299
376
  basePrompt,
@@ -715,7 +792,7 @@ export async function executeDagNode(input) {
715
792
  }
716
793
  node.resolvedSkills = resolvedSkills;
717
794
  await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
718
- const model = resolveModelForTask(task, spec.executorModels);
795
+ let model = resolveModelForTask(task, spec.executorModels);
719
796
  let thinking;
720
797
  if (task.executor === "pi") {
721
798
  try {
@@ -726,16 +803,25 @@ export async function executeDagNode(input) {
726
803
  // Invalid or missing harness preserves the previous no-override behavior.
727
804
  }
728
805
  }
729
- const retryPolicy = task.retryPolicy &&
806
+ const specRetryPolicy = task.retryPolicy &&
730
807
  (isSafeReadOnlyPiRetryCandidate(task) ||
731
808
  isWriterEmptyDiffRetryCandidate(task) ||
732
809
  isWriterTransportRetryCandidate(task))
733
810
  ? task.retryPolicy
734
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;
735
818
  const maxAttempts = retryPolicy?.maxAttempts ?? 1;
736
819
  const attempts = [];
737
820
  let totalAttemptWallDurationMs = 0;
738
821
  let totalBackoffMs = 0;
822
+ const frontendPlanLadderEnabled = isFrontendPlanLadderTask(task);
823
+ let frontendPlanRetryStep = "normal";
824
+ const frontendPriorFingerprints = [];
739
825
  const livenessPolicy = resolveLivenessPolicy(spec.defaults?.livenessPolicy, task.livenessPolicy);
740
826
  /**
741
827
  * Attempt-fenced, throttled activity sink. Late events from a previous
@@ -773,10 +859,24 @@ export async function executeDagNode(input) {
773
859
  let terminalResult;
774
860
  let previousFailureCategory;
775
861
  let previousProtocolReason;
862
+ let pendingStructuredArtifact;
863
+ let lastRepairAllowlist = [];
776
864
  for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
777
865
  const attemptStartedAt = new Date().toISOString();
778
866
  node.currentAttempt = attemptNumber;
779
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);
780
880
  let result;
781
881
  try {
782
882
  validateRepairArtifactGateBeforeShell({
@@ -799,12 +899,60 @@ export async function executeDagNode(input) {
799
899
  (acc[i.path] ??= []).push(i.detail);
800
900
  return acc;
801
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
+ }
802
950
  result = await executeNode({
803
951
  task,
804
952
  cwd,
805
953
  model,
806
954
  ...(thinking ? { thinking } : {}),
807
- prompt: buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics),
955
+ prompt: attemptPrompt,
808
956
  attempt: attemptNumber,
809
957
  reportActivity,
810
958
  timeoutMs: livenessPolicy.absoluteMaxWallClockMs,
@@ -820,6 +968,35 @@ export async function executeDagNode(input) {
820
968
  durationMs: 0,
821
969
  };
822
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
+ }
823
1000
  // R0: executor ok=true still fails closed when outputProtocol is violated.
824
1001
  // Valid semantic results (e.g. VERDICT: request-revision) pass validation.
825
1002
  if (result.ok && task.outputProtocol) {
@@ -856,16 +1033,17 @@ export async function executeDagNode(input) {
856
1033
  structuredContractOutput: task.structuredContractOutput,
857
1034
  });
858
1035
  if (!contractCheck.ok) {
859
- // A provider stopReason=length means the response was cut before the
860
- // JSON contract could complete; separate it from an ordinary bad
861
- // contract so the retry switches to a JSON-only output strategy.
862
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";
863
1043
  result = {
864
1044
  ...result,
865
1045
  ok: false,
866
- failureCategory: attemptStopReason === "length"
867
- ? "structured-output-truncated"
868
- : "invalid-output",
1046
+ failureCategory,
869
1047
  stderr: [result.stderr, contractCheck.reason]
870
1048
  .filter(Boolean)
871
1049
  .join("\n"),
@@ -873,14 +1051,6 @@ export async function executeDagNode(input) {
873
1051
  previousProtocolReason = contractCheck.reason;
874
1052
  }
875
1053
  else {
876
- if (contractCheck.structuredArtifact) {
877
- node.structuredArtifactPath = contractCheck.structuredArtifact.path;
878
- node.structuredArtifactSha256 = contractCheck.structuredArtifact.sha256;
879
- node.structuredArtifactSchemaId = contractCheck.structuredArtifact.schemaId;
880
- }
881
- if (contractCheck.frontendPlanOutput) {
882
- node.frontendPlanOutput = contractCheck.frontendPlanOutput;
883
- }
884
1054
  if (contractCheck.normalizedText) {
885
1055
  result = {
886
1056
  ...result,
@@ -888,6 +1058,9 @@ export async function executeDagNode(input) {
888
1058
  stdout: contractCheck.normalizedText,
889
1059
  };
890
1060
  }
1061
+ if (contractCheck.artifact) {
1062
+ pendingStructuredArtifact = contractCheck.artifact;
1063
+ }
891
1064
  previousProtocolReason = undefined;
892
1065
  }
893
1066
  }
@@ -900,6 +1073,7 @@ export async function executeDagNode(input) {
900
1073
  const attemptFinishedAt = new Date().toISOString();
901
1074
  const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
902
1075
  totalAttemptWallDurationMs += attemptWallDurationMs;
1076
+ const rawFailureCategory = result.rawFailureCategory ?? result.failureCategory;
903
1077
  const attemptRecord = {
904
1078
  attempt: attemptNumber,
905
1079
  startedAt: attemptStartedAt,
@@ -911,6 +1085,7 @@ export async function executeDagNode(input) {
911
1085
  stderr: result.stderr,
912
1086
  assistantText: result.assistantText,
913
1087
  failureCategory: result.failureCategory,
1088
+ ...(rawFailureCategory ? { rawFailureCategory } : {}),
914
1089
  backend: result.backend,
915
1090
  sdkAttempted: result.sdkAttempted,
916
1091
  tokensUsed: result.tokensUsed,
@@ -925,6 +1100,60 @@ export async function executeDagNode(input) {
925
1100
  await writeDagNodeJsonArtifact(runDir, nodeId, `attempt-${attemptNumber}.json`, attemptRecord);
926
1101
  node.attempts = attempts;
927
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
+ }
928
1157
  // Reflect the latest attempt on the node so progress is observable,
929
1158
  // but keep node.status RUNNING while retry is still possible.
930
1159
  node.durationMs = durationBetween(node.startedAt, attemptFinishedAt);
@@ -958,13 +1187,26 @@ export async function executeDagNode(input) {
958
1187
  break;
959
1188
  const canRetry = retryPolicy !== undefined && attemptNumber < maxAttempts;
960
1189
  const retryable = retryPolicy !== undefined &&
1190
+ result.failureCategory !== GOVERNANCE_BLOCKED_CATEGORY &&
961
1191
  (isRetryablePiFailureCategory(result.failureCategory, {
962
1192
  retryCategories: retryPolicy.retryCategories,
963
1193
  }) ||
964
1194
  (result.failureCategory === "protocol-invalid" &&
965
1195
  task.outputProtocol?.retryOnInvalid === true));
966
- 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
+ }
967
1208
  break;
1209
+ }
968
1210
  const delayMs = computeBackoffDelayMs(attemptNumber + 1, retryPolicy);
969
1211
  if (delayMs > 0) {
970
1212
  const backoffStartedAt = new Date().toISOString();
@@ -1113,6 +1355,11 @@ export async function executeDagNode(input) {
1113
1355
  node.structuredArtifactSchemaId = "backend-test-result-v1";
1114
1356
  }
1115
1357
  }
1358
+ else if (pendingStructuredArtifact) {
1359
+ node.structuredArtifactPath = pendingStructuredArtifact.path;
1360
+ node.structuredArtifactSha256 = pendingStructuredArtifact.sha256;
1361
+ node.structuredArtifactSchemaId = pendingStructuredArtifact.schemaId;
1362
+ }
1116
1363
  }
1117
1364
  const finishedAt = new Date().toISOString();
1118
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)