@tea-agent/loop-agent 0.41.1-beta.0 → 0.41.2

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 (209) hide show
  1. package/CHANGELOG.md +61 -267
  2. package/dist/application/dag/generate-task-dag.js +19 -75
  3. package/dist/application/task-lifecycle/advance.js +5 -24
  4. package/dist/application/task-lifecycle/observe.js +17 -171
  5. package/dist/application/task-lifecycle/plan-transitions.js +7 -42
  6. package/dist/application/task-lifecycle/recommendations.js +2 -13
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +0 -7
  9. package/dist/cli/program.js +0 -5
  10. package/dist/commands/client-recovery.js +0 -3
  11. package/dist/commands/init.js +1 -27
  12. package/dist/commands/task-advance.js +0 -19
  13. package/dist/executors/dag-pi-executor.js +231 -3784
  14. package/dist/executors/pi-executor.js +4 -15
  15. package/dist/executors/pi-extension-resolver.js +14 -2
  16. package/dist/executors/shell-executor.js +217 -945
  17. package/dist/executors/shell-write-guard.js +0 -7
  18. package/dist/shared/dag-failure-category.js +0 -12
  19. package/dist/shared/openspec-spec.js +4 -70
  20. package/dist/shared/operator/capabilities.js +0 -7
  21. package/dist/task/config-types.js +7 -34
  22. package/dist/task/contract/adopt.js +0 -4
  23. package/dist/task/contract/import-revision.js +0 -4
  24. package/dist/task/contract/project.js +0 -3
  25. package/dist/task/contract/schema.js +1 -2
  26. package/dist/task/frontend-project-capability.js +20 -203
  27. package/dist/task/runtime.js +2 -5
  28. package/dist/task/source-prepare/build-draft.js +3 -3
  29. package/dist/task/source-prepare/fragment-inventory.js +9 -15
  30. package/dist/task/source-prepare/prepare.js +1 -89
  31. package/dist/task/source-prepare/semantic-intake.js +2 -6
  32. package/dist/task/source-references.js +1 -22
  33. package/dist/worker/console/chat/chat-ui-policy.js +4 -2
  34. package/dist/worker/console/chat/pi-runtime.js +235 -13
  35. package/dist/worker/console/chat/repo-browser.js +10 -3
  36. package/dist/worker/console/chat/routes.js +86 -44
  37. package/dist/worker/console/chat/semantic-activity.js +12 -11
  38. package/dist/worker/console/chat/session-stats.js +96 -0
  39. package/dist/worker/console/chat/session-store.js +133 -70
  40. package/dist/worker/console/chat/shortcuts.js +1 -1
  41. package/dist/worker/console/chat/turn-process.js +139 -60
  42. package/dist/worker/console/index.js +0 -3
  43. package/dist/worker/console/operation-store.js +6 -226
  44. package/dist/worker/console/operator-actions.js +7 -169
  45. package/dist/worker/console/pi-plugins.js +3 -1
  46. package/dist/worker/console/server.js +19 -4
  47. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DQ5jlYVU.js} +1 -1
  48. package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-BeTVCzH9.js} +1 -1
  49. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-DassREBi.js} +1 -1
  50. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-DyeNX8Ek.js} +1 -1
  51. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-BXb_zxGx.js} +1 -1
  52. package/dist/worker/console/static/assets/channel-B_lofstT.js +1 -0
  53. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-B9F9gxGC.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-Byd5zHEQ.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-BqoxMJk9.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-CT7d6YZw.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-CdiNyN0N.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-BGw9bpVc.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-DiDVlVB1.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-BhHf-4yK.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-B79pEgwv.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-Cb70EcUV.js} +1 -1
  63. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-ieUI0-eY.js +1 -0
  64. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-ieUI0-eY.js +1 -0
  65. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-DBHLnAf1.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-BT_JRsOH.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-Bss-msa3.js} +1 -1
  68. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-B8Gr24pt.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-BrjU70Jw.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cp_JpPGd.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-BWKICEP-.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D3z_UNR_.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-_Ywm390x.js} +1 -1
  74. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-D9QYQQMB.js} +1 -1
  75. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BNgXNKAr.js} +1 -1
  76. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-QRLnkPCg.js} +1 -1
  77. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-B9fOduNi.js} +1 -1
  78. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-FSEdtPN2.js} +1 -1
  79. package/dist/worker/console/static/assets/index-Cs9JUSPs.css +1 -0
  80. package/dist/worker/console/static/assets/index-DtPrwu7M.js +451 -0
  81. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-D5Bjg9dm.js} +1 -1
  82. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BlNRkWvf.js} +1 -1
  83. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-BdJiw3SL.js} +1 -1
  84. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-CsDNlYht.js} +1 -1
  85. package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-8g9dbEmX.js} +1 -1
  86. package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-ChyQH_ut.js} +5 -5
  87. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-DwYoCTYt.js} +1 -1
  88. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-T_b2rS17.js} +1 -1
  89. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-1Cr7WMhH.js} +1 -1
  90. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-Bq4qM59o.js} +1 -1
  91. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-ytFgR1RL.js} +1 -1
  92. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CnUdUVYX.js} +1 -1
  93. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-_5rJqG6R.js} +1 -1
  94. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-D5imyGbt.js} +1 -1
  95. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-DvgDn3wL.js} +1 -1
  96. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-BViu7duc.js} +1 -1
  97. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-kNxnG4_w.js +1 -0
  98. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BcULKg2P.js} +2 -2
  99. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DyTzlcWH.js +8 -0
  100. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-DmPveIjt.js} +1 -1
  101. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-9r-iFy0R.js} +1 -1
  102. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-D3fIXdvM.js} +1 -1
  103. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-BCSS9SBg.js} +1 -1
  104. package/dist/worker/console/static/index.html +2 -2
  105. package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
  106. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
  107. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
  108. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
  109. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  110. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
  111. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  112. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
  113. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  114. package/dist/worker/console/workspace-context.js +1 -111
  115. package/dist/worker/observability/read-model.js +4 -0
  116. package/dist/worker/observe/routes.js +4 -0
  117. package/dist/worker/observe/static/dag-node-purpose.js +0 -5
  118. package/dist/worker/observe/static/views/dag-inspector.js +2 -7
  119. package/dist/worker/observe/static/views/session-timeline.js +46 -15
  120. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  121. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  122. package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
  123. package/dist/workflows/dag/frontend-plan-render.js +1 -2
  124. package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
  125. package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
  126. package/dist/workflows/dag/frontend-recovery-run.js +20 -33
  127. package/dist/workflows/dag/frontend-repair.js +432 -1
  128. package/dist/workflows/dag/frontend-review-context.js +15 -261
  129. package/dist/workflows/dag/frontend-verification-trace.js +24 -249
  130. package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
  131. package/dist/workflows/dag/init-hybrid.js +494 -685
  132. package/dist/workflows/dag/interrupt-request.js +0 -7
  133. package/dist/workflows/dag/node-execution.js +4 -732
  134. package/dist/workflows/dag/prompt.js +1 -34
  135. package/dist/workflows/dag/report.js +1 -37
  136. package/dist/workflows/dag/rerun-feedback.js +1 -256
  137. package/dist/workflows/dag/rerun-plan.js +2 -10
  138. package/dist/workflows/dag/rerun-run.js +0 -23
  139. package/dist/workflows/dag/rerun-task.js +0 -29
  140. package/dist/workflows/dag/retry-policy.js +104 -214
  141. package/dist/workflows/dag/runner.js +122 -429
  142. package/dist/workflows/dag/scheduler.js +16 -114
  143. package/dist/workflows/dag/types.js +12 -239
  144. package/dist/workflows/dag/validate.js +12 -28
  145. package/docs/init-surface.manifest.json +12 -30
  146. package/docs/skills/vetted-skill-registry.md +2 -4
  147. package/docs/templates/README.md +0 -2
  148. package/docs/templates/agent-dag-report.schema.json +2 -8
  149. package/docs/templates/agent-dag.schema.json +1 -1
  150. package/docs/templates/frontend-implementation-contract.schema.json +1 -4
  151. package/harness.json +3 -3
  152. package/package.json +1 -1
  153. package/skills/frontend-bounded-implement/SKILL.md +14 -15
  154. package/skills/frontend-design-review/SKILL.md +41 -22
  155. package/skills/frontend-implementation/SKILL.md +52 -0
  156. package/skills/frontend-implementation/references/code-standards.md +33 -0
  157. package/skills/frontend-implementation/references/design-spec.md +56 -0
  158. package/skills/frontend-implementation/references/node-contracts.md +31 -0
  159. package/skills/frontend-review/SKILL.md +15 -20
  160. package/skills/frontend-review/references/review-findings.md +7 -6
  161. package/skills/frontend-verification/SKILL.md +1 -1
  162. package/skills/loop-agent/references/command-reference.md +0 -1
  163. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  164. package/dist/commands/dag-follow-up.js +0 -138
  165. package/dist/executors/pi-read-budget-policy.js +0 -239
  166. package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
  167. package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
  168. package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
  169. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
  170. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
  171. package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
  172. package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
  173. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
  174. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
  175. package/dist/worker/console/static-src/active-run-badge.js +0 -17
  176. package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
  177. package/dist/workflows/dag/dag-retry-schema.js +0 -138
  178. package/dist/workflows/dag/frontend-closeout.js +0 -221
  179. package/dist/workflows/dag/frontend-design-policy.js +0 -400
  180. package/dist/workflows/dag/frontend-human-decision.js +0 -182
  181. package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
  182. package/dist/workflows/dag/frontend-read-budgets.js +0 -12
  183. package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
  184. package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
  185. package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
  186. package/dist/workflows/dag/frontend-review-findings.js +0 -270
  187. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
  188. package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
  189. package/dist/workflows/dag/frontend-shape-facts.js +0 -409
  190. package/dist/workflows/dag/frontend-shape.js +0 -427
  191. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
  192. package/dist/workflows/dag/frontend-split-application-service.js +0 -203
  193. package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
  194. package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
  195. package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
  196. package/dist/workflows/dag/frontend-writer-admission.js +0 -285
  197. package/dist/workflows/dag/frontend-writer-status.js +0 -256
  198. package/dist/workflows/dag/recovery-lease.js +0 -80
  199. package/docs/templates/frontend-implementation-dag.json +0 -89
  200. package/docs/templates/spec-registry.schema.json +0 -45
  201. package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
  202. package/skills/frontend-contract/SKILL.md +0 -23
  203. package/skills/frontend-contract/references/contract-protocol.md +0 -34
  204. package/skills/frontend-plan/SKILL.md +0 -26
  205. package/skills/frontend-plan/references/decision-contract.md +0 -37
  206. package/skills/frontend-plan/references/design-decisions.md +0 -17
  207. package/skills/frontend-scout/SKILL.md +0 -25
  208. package/skills/frontend-scout/references/design-evidence.md +0 -16
  209. package/skills/frontend-scout/references/scout-evidence.md +0 -23
@@ -1,5 +1,5 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
- import { createHash } from "node:crypto";
2
+ import { randomUUID } from "node:crypto";
3
3
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
4
4
  import { hostname } from "node:os";
5
5
  import path from "node:path";
@@ -7,7 +7,7 @@ import { isHardBudgetBreached, resolveEffectiveMaxConcurrent, } from "../../appl
7
7
  import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
8
8
  import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
9
9
  import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
10
- import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
10
+ import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readDagRunSpec, readDagRunState, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
11
11
  import { markInterruptNeedsReconcile, mergeAbortSignals, readInterruptRequest, runnerIdentityFromState, settleDagInterrupt, startInterruptWatcher, } from "./interrupt-request.js";
12
12
  import { moveToCompletedRunDir, moveToPausedRunDir, prepareRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
13
13
  import { evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
@@ -26,12 +26,7 @@ import { runConvergencePassController, } from "./convergence/controller.js";
26
26
  import { executeDagRanksOnce, FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isConditionSkippedReason, readFrontendPrewriteResult, } from "./scheduler.js";
27
27
  import { topoSortToRanks } from "./topo.js";
28
28
  import { isFrontendWriterTransientPartialWrite } from "./frontend-writer-recovery.js";
29
- import { classifyReviewIssueCategoryFailureOwner, readCommittedDesignRequestFact, readCommittedReviewRequestFact, writeFrontendReviewFindingsPrd, } from "./frontend-review-findings.js";
30
- import { bindFrontendShapeStateToRunTopology, commitFrontendShapeTransitionCas, commitRuntimeFrontendShapeFact, isCanonicalFrontendShapeDag, } from "./frontend-shape-facts.js";
31
- import { assertFrontendTopologyBinding, buildFrontendTopologyBinding, computeFrontendShapeSourceDigest, } from "./frontend-shape.js";
32
- import { ensureFrontendShapeCapsuleRecord } from "./frontend-shape-capsule-store.js";
33
29
  import { parseDagSpec, resolveModelForTask, } from "./types.js";
34
- import { acquireDagRecoveryLease, bindDagRecoveryLeaseExecution, } from "./recovery-lease.js";
35
30
  import { executeDynamicCondition } from "./dynamic-runtime/condition.js";
36
31
  import { executeDynamicLoopUntil } from "./dynamic-runtime/loop-until.js";
37
32
  import { executeDynamicMapExpansion } from "./dynamic-runtime/map.js";
@@ -51,51 +46,6 @@ export function toKebabSlug(text, maxLen = 40) {
51
46
  .replace(/^-|-$/g, "")
52
47
  .slice(0, maxLen) || "dag");
53
48
  }
54
- export async function loadCanonicalFrontendTaskSourceBinding(cwd, spec) {
55
- const binding = spec.sourceBinding;
56
- if (!binding?.taskId)
57
- throw new Error("canonical frontend DAG requires sourceBinding.taskId");
58
- const requirement = binding.sources.find((source) => source.kind === "requirement");
59
- if (!requirement)
60
- throw new Error("canonical frontend DAG requires a bound requirement source");
61
- const constraint = binding.sources.find((source) => source.kind === "constraint");
62
- const readFreshSource = async (source) => {
63
- let bytes;
64
- try {
65
- bytes = await readFile(path.resolve(cwd, source.path));
66
- }
67
- catch (error) {
68
- throw new Error(`canonical frontend ${source.kind} source freshness read failed: ${error instanceof Error ? error.message : String(error)}`);
69
- }
70
- const actualSha256 = createHash("sha256").update(bytes).digest("hex");
71
- if (actualSha256 !== source.sha256) {
72
- throw new Error(`canonical frontend ${source.kind} source sha256 drifted: expected ${source.sha256}, received ${actualSha256}`);
73
- }
74
- return bytes.toString("utf8");
75
- };
76
- const requirementMarkdown = await readFreshSource(requirement);
77
- const constraintMarkdown = constraint ? await readFreshSource(constraint) : "";
78
- return {
79
- taskId: binding.taskId,
80
- requirementMarkdown,
81
- constraintMarkdown,
82
- sourceDigest: computeFrontendShapeSourceDigest({ requirementMarkdown, constraintMarkdown }),
83
- };
84
- }
85
- export async function ensureFreshFrontendShapeCapsuleRecord(input) {
86
- const freshSource = await loadCanonicalFrontendTaskSourceBinding(input.cwd, input.spec);
87
- if (freshSource.taskId !== input.canonicalSource.taskId ||
88
- freshSource.sourceDigest !== input.canonicalSource.sourceDigest) {
89
- throw new Error("canonical frontend task source freshness binding drifted before transition commit");
90
- }
91
- await (input.ensureRecord ?? ensureFrontendShapeCapsuleRecord)({
92
- repoRoot: input.cwd,
93
- taskId: input.canonicalSource.taskId,
94
- parentRunId: input.parentRunId,
95
- terminalOutcome: input.terminalOutcome,
96
- capsule: input.capsule,
97
- });
98
- }
99
49
  export function extractTaskIdFromDagSpec(spec) {
100
50
  if (spec.objective) {
101
51
  const match = spec.objective.match(TASK_ID_IN_TEXT_PATTERN);
@@ -230,15 +180,6 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
230
180
  ...(opts.workerAssociation
231
181
  ? { workerAssociation: structuredClone(opts.workerAssociation) }
232
182
  : {}),
233
- ...(isCanonicalFrontendShapeDag(new Map(spec.tasks.map((task) => [task.id, task])))
234
- ? {
235
- frontendShapeState: {
236
- frozenTopology: buildFrontendTopologyBinding(spec.tasks.map((task) => task.id)),
237
- revision: 0,
238
- committedFacts: {},
239
- },
240
- }
241
- : {}),
242
183
  };
243
184
  initRunBudgetLedger(state, spec.budget);
244
185
  return state;
@@ -360,7 +301,6 @@ export async function runDag(spec, opts) {
360
301
  activeRunDir,
361
302
  completedRunDir,
362
303
  pausedRunDir,
363
- recovery: opts.recovery,
364
304
  });
365
305
  }
366
306
  finally {
@@ -369,16 +309,6 @@ export async function runDag(spec, opts) {
369
309
  export async function runDagContinuation(opts) {
370
310
  const { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), opts.spec.budget);
371
311
  const state = opts.state;
372
- const canonicalFrontend = isCanonicalFrontendShapeDag(new Map(opts.spec.tasks.map((task) => [task.id, task])));
373
- if (canonicalFrontend) {
374
- assertFrontendTopologyBinding(state.frontendShapeState?.frozenTopology, opts.spec.tasks.map((task) => task.id));
375
- }
376
- else if (state.frontendShapeState) {
377
- throw new Error("non-frontend DAG unexpectedly contains frontend topology state");
378
- }
379
- if (state.frontendShapeState?.terminalOutcome) {
380
- throw new Error(`dag run ${state.runId} is terminal due to frontend shape ${state.frontendShapeState.terminalOutcome}`);
381
- }
382
312
  state.status = "running";
383
313
  const resumedAt = new Date().toISOString();
384
314
  state.runner = {
@@ -403,7 +333,6 @@ export async function runDagContinuation(opts) {
403
333
  activeRunDir,
404
334
  completedRunDir,
405
335
  pausedRunDir,
406
- recovery: opts.recovery,
407
336
  });
408
337
  }
409
338
  export async function resumeDagRun(opts) {
@@ -506,13 +435,9 @@ export async function resumeDagRun(opts) {
506
435
  }
507
436
  async function executeDagCheckpoint(input) {
508
437
  const { spec, state, ranks, runDir: initialRunDir, cwd, maxConcurrent, completedRunDir, pausedRunDir, } = input;
509
- bindFrontendShapeStateToRunTopology(state);
510
438
  let runDir = initialRunDir;
511
439
  let stateWriteQueue = Promise.resolve();
512
- let shapeTransitionBarrier;
513
440
  const persistState = async (options) => {
514
- if (shapeTransitionBarrier)
515
- await shapeTransitionBarrier;
516
441
  stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, state, options));
517
442
  await stateWriteQueue;
518
443
  };
@@ -637,50 +562,12 @@ async function executeDagCheckpoint(input) {
637
562
  .map((rank) => rank.filter((nodeId) => nodeId === "closeout-pi"))
638
563
  .filter((rank) => rank.length > 0)
639
564
  : [];
640
- const canonicalFrontendSource = isCanonicalFrontendShapeDag(tasksById)
641
- ? await loadCanonicalFrontendTaskSourceBinding(cwd, spec)
642
- : undefined;
643
565
  const executeRanks = (selectedRanks) => executeDagRanksOnce({
644
566
  state,
645
567
  ranks: selectedRanks,
646
568
  tasksById,
647
569
  maxConcurrent,
648
570
  persistState,
649
- commitFrontendShapeTransition: async (request) => {
650
- let releaseBarrier;
651
- shapeTransitionBarrier = new Promise((resolve) => {
652
- releaseBarrier = resolve;
653
- });
654
- try {
655
- if (!canonicalFrontendSource) {
656
- throw new Error("frontend shape transition requires canonical task source binding");
657
- }
658
- return await commitFrontendShapeTransitionCas({
659
- state,
660
- tasksById,
661
- request,
662
- taskSourceDigest: canonicalFrontendSource.sourceDigest,
663
- ensureRecord: async (capsule, terminalOutcome) => {
664
- await ensureFreshFrontendShapeCapsuleRecord({
665
- cwd,
666
- spec,
667
- canonicalSource: canonicalFrontendSource,
668
- parentRunId: state.runId,
669
- terminalOutcome,
670
- capsule,
671
- });
672
- },
673
- persist: async (nextState) => {
674
- stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, nextState));
675
- await stateWriteQueue;
676
- },
677
- });
678
- }
679
- finally {
680
- releaseBarrier();
681
- shapeTransitionBarrier = undefined;
682
- }
683
- },
684
571
  runDir,
685
572
  abortSignal: input.abortSignal,
686
573
  createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
@@ -716,18 +603,6 @@ async function executeDagCheckpoint(input) {
716
603
  });
717
604
  const node = state.nodes[nodeId];
718
605
  if (node && (node.status === "FINISHED" || node.status === "ERROR")) {
719
- if (node.status === "FINISHED") {
720
- if (canonicalFrontendSource) {
721
- await commitRuntimeFrontendShapeFact({
722
- state,
723
- tasksById,
724
- sourceNodeId: nodeId,
725
- taskId: canonicalFrontendSource.taskId,
726
- requirementMarkdown: canonicalFrontendSource.requirementMarkdown,
727
- constraintMarkdown: canonicalFrontendSource.constraintMarkdown,
728
- });
729
- }
730
- }
731
606
  recordFinishedNodeBudget(state, node);
732
607
  await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
733
608
  await persistState();
@@ -737,9 +612,6 @@ async function executeDagCheckpoint(input) {
737
612
  let pausedByNodeId;
738
613
  while (true) {
739
614
  pausedByNodeId = await executeRanks(convergenceRanks);
740
- if (state.frontendShapeState?.terminalOutcome) {
741
- break;
742
- }
743
615
  if (pausedByNodeId) {
744
616
  break;
745
617
  }
@@ -795,7 +667,7 @@ async function executeDagCheckpoint(input) {
795
667
  runDir = await moveToPausedRunDir(runDir, pausedRunDir);
796
668
  }
797
669
  else {
798
- const { recoveryPending, recovery: recoveryTrigger } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd, spec.frontendRecovery?.maxContinuations ?? 1);
670
+ const { recoveryPending } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
799
671
  await finalizeRunOwnedInterrupt({
800
672
  runDir,
801
673
  state,
@@ -803,65 +675,52 @@ async function executeDagCheckpoint(input) {
803
675
  interruptAborted: interruptController.signal.aborted,
804
676
  });
805
677
  await persistState();
806
- if (recoveryPending && input.recovery && recoveryTrigger) {
807
- // M6 recovery operation controller path: construct/reuse one recovery
808
- // operation keyed by a deterministic clientRequestId and supervise it
809
- // to terminal, instead of staging an in-process child run.
810
- const created = await createRecoveryOperation({
811
- cwd,
812
- runDir,
813
- spec,
814
- state,
815
- trigger: recoveryTrigger,
816
- deps: input.recovery,
817
- });
818
- if (created.kind === "conflict") {
819
- // Same clientRequestId with a different payload → fail closed; never
820
- // create a second recovery operation (AC-004).
821
- state.status = "failed";
822
- state.terminalReason =
823
- "frontend recovery request-id reuse conflict";
824
- state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, "auto-recovery-blocked");
825
- }
826
- else if (created.kind === "lease-held") {
827
- // A different recovery entrance has already durably claimed this
828
- // parent. Do not "help" by running its operation: that would reopen
829
- // the exact two-child-DAG race the lease prevents.
830
- state.status = "failed";
831
- state.terminalReason =
832
- "frontend recovery parent lease already held";
833
- state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, "auto-recovery-blocked");
834
- }
835
- else {
836
- const { runRecoveryOperationController } = await import("./frontend-recovery-controller.js");
837
- const result = await runRecoveryOperationController({
838
- operationId: created.operation.operationId,
678
+ if (recoveryPending) {
679
+ // Recovery coordinator (phase 3c AC-2/AC-3): materialize the reserved
680
+ // child and run it synchronously. Dynamic import avoids a static
681
+ // runner ↔ frontend-recovery-run module cycle. stageRecoveryChild
682
+ // locates the parent via the canonical active/ layout, so recovery
683
+ // only fires when this run is still the canonical active run dir
684
+ // (direct runDagContinuation callers using an arbitrary runDir keep
685
+ // the previous scheduler-layer skip semantics).
686
+ const canonicalActiveDir = getDagRunDir(cwd, "active", state.runId);
687
+ if (path.resolve(runDir) === path.resolve(canonicalActiveDir)) {
688
+ const { stageRecoveryChild } = await import("./frontend-recovery-run.js");
689
+ const staged = await stageRecoveryChild({
690
+ cwd,
839
691
  parentRunId: state.runId,
840
- deps: {
841
- ...input.recovery,
842
- failureOwner: input.recovery.failureOwner ??
843
- recoveryTrigger.failureOwner,
844
- protocolFailureReason: input.recovery.protocolFailureReason ??
845
- recoveryTrigger.protocolFailureReason,
846
- recoveryMode: input.recovery.recoveryMode ?? recoveryTrigger.recoveryMode,
847
- },
848
692
  });
849
- state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, result.terminalReason === "succeeded"
850
- ? "recovered"
851
- : "auto-recovery-blocked");
693
+ // stageRecoveryChild CAS-advanced the parent's on-disk lineage
694
+ // (child-staging → child-running + childRunId). Refresh the in-memory
695
+ // copy so the terminal persistState below does not regress the parent
696
+ // back to stale child-staging.
697
+ const parentOnDisk = await readDagRunState(runDir);
698
+ state.frontendRecoveryState = parentOnDisk.frontendRecoveryState;
699
+ const childState = await readDagRunState(staged.childRunDir);
700
+ const childSpec = await readDagRunSpec(staged.childRunDir);
701
+ await runDagContinuation({
702
+ cwd,
703
+ spec: childSpec,
704
+ state: childState,
705
+ runDir: staged.childRunDir,
706
+ maxConcurrent,
707
+ executeNode: input.executeNode,
708
+ observer: input.observer,
709
+ abortSignal,
710
+ });
852
711
  }
853
712
  }
854
713
  await notifyRunObserver(input.observer, "onRunFinish", state);
855
- // Parent is always archived to completed/ — no active/ retention for an
856
- // in-process child CAS anymore.
857
- try {
858
- const terminalCheckpoint = await captureWorkspaceCheckpoint(cwd);
859
- await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL, terminalCheckpoint);
860
- }
861
- catch (error) {
862
- console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
714
+ if (!recoveryPending) {
715
+ try {
716
+ const terminalCheckpoint = await captureWorkspaceCheckpoint(cwd);
717
+ await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL, terminalCheckpoint);
718
+ }
719
+ catch (error) {
720
+ console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
721
+ }
722
+ runDir = await moveToCompletedRunDir(runDir, completedRunDir);
863
723
  }
864
- runDir = await moveToCompletedRunDir(runDir, completedRunDir);
865
724
  }
866
725
  await relocateRunArtifactPaths({
867
726
  runDir,
@@ -994,21 +853,39 @@ function isSuccessfulConvergenceTerminal(state) {
994
853
  return true;
995
854
  return SUCCESSFUL_CONVERGENCE_TERMINAL_REASONS.has(reason);
996
855
  }
856
+ function buildFrontendRecoveryIntent(state, failureSource) {
857
+ const existing = state.frontendRecoveryState;
858
+ const requestId = existing?.requestId ?? randomUUID();
859
+ return {
860
+ schemaVersion: 1,
861
+ phase: "child-staging",
862
+ requestId,
863
+ recoveryRootRunId: existing?.recoveryRootRunId ?? state.runId,
864
+ parentRunId: state.runId,
865
+ ...(existing?.childRunId ? { childRunId: existing.childRunId } : {}),
866
+ attemptId: existing?.attemptId ?? `recovery-${requestId}`,
867
+ attemptIndex: existing?.attemptIndex ?? 0,
868
+ continuationCount: existing?.continuationCount ?? 0,
869
+ ...(failureSource ? { failureSource } : {}),
870
+ revision: (existing?.revision ?? 0) + 1,
871
+ };
872
+ }
997
873
  function buildCandidateContractInvalidResult(state, result, artifactHash) {
874
+ const recovery = state.frontendRecoveryState;
998
875
  return {
999
876
  schemaVersion: 1,
1000
877
  outcome: "candidate-contract-invalid",
1001
878
  origin: {
1002
879
  kind: "frontend-prewrite-gate",
1003
- parentRunId: state.runId,
1004
- requestId: state.runId,
1005
- failedNodeId: result.failureSource ?? "frontend-plan-pi",
880
+ parentRunId: recovery?.parentRunId ?? state.runId,
881
+ ...(recovery?.childRunId ? { childRunId: recovery.childRunId } : {}),
882
+ requestId: recovery?.requestId ?? "",
883
+ failedNodeId: result.selectedPlanNodeId,
1006
884
  },
1007
885
  failureClass: {
1008
886
  code: "candidate-contract-invalid",
1009
887
  classification: result.classification,
1010
- reason: result.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ") ??
1011
- "candidate contract invalid",
888
+ reason: result.failureReason ?? "candidate contract invalid",
1012
889
  },
1013
890
  evidenceRefs: [
1014
891
  {
@@ -1020,13 +897,15 @@ function buildCandidateContractInvalidResult(state, result, artifactHash) {
1020
897
  };
1021
898
  }
1022
899
  function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
900
+ const recovery = state.frontendRecoveryState;
1023
901
  return {
1024
902
  schemaVersion: 1,
1025
903
  outcome: "auto-recovery-blocked",
1026
904
  origin: {
1027
905
  kind: "frontend-writer",
1028
- parentRunId: state.runId,
1029
- requestId: state.runId,
906
+ parentRunId: recovery?.parentRunId ?? state.runId,
907
+ ...(recovery?.childRunId ? { childRunId: recovery.childRunId } : {}),
908
+ requestId: recovery?.requestId ?? "",
1030
909
  failedNodeId,
1031
910
  },
1032
911
  failureClass: {
@@ -1037,115 +916,63 @@ function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
1037
916
  evidenceRefs: [],
1038
917
  };
1039
918
  }
1040
- function buildRecoveryResultForTrigger(state, trigger, outcome) {
1041
- return {
1042
- schemaVersion: 1,
1043
- outcome,
1044
- origin: {
1045
- kind: trigger.failureSource === "frontend-implement-pi"
1046
- ? "frontend-writer"
1047
- : "frontend-prewrite-gate",
1048
- parentRunId: state.runId,
1049
- requestId: trigger.requestId,
1050
- failedNodeId: trigger.failureSource,
1051
- },
1052
- evidenceRefs: [],
1053
- };
919
+ function settleFrontendRecovery(state, outcome) {
920
+ const recovery = state.frontendRecoveryState;
921
+ if (!recovery)
922
+ return;
923
+ recovery.phase = "settled";
924
+ if (!state.frontendRecoveryResult) {
925
+ state.frontendRecoveryResult = {
926
+ schemaVersion: 1,
927
+ outcome,
928
+ origin: {
929
+ kind: "frontend-prewrite-gate",
930
+ parentRunId: recovery.parentRunId,
931
+ ...(recovery.childRunId ? { childRunId: recovery.childRunId } : {}),
932
+ requestId: recovery.requestId,
933
+ failedNodeId: "",
934
+ },
935
+ evidenceRefs: [],
936
+ };
937
+ }
1054
938
  }
1055
- export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, frontendRecoveryQuota = 1) {
939
+ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
1056
940
  const repoRoot = cwd ?? path.resolve(runDir, "..", "..", "..", "..");
1057
- if (state.frontendShapeState?.terminalOutcome &&
1058
- state.frontendShapeState.transitionCapsule) {
1059
- state.status = "failed";
1060
- return { recoveryPending: false };
1061
- }
1062
941
  // Terminal override: a denied frontend writer must never surface as a
1063
942
  // mechanical partial_failed just because prewrite FINISHED while the writer
1064
- // was SKIPPED. Fail closed on blocked before aggregation.
1065
- let recoveryTrigger;
943
+ // was SKIPPED. Fail closed on retryable-invalid/blocked before aggregation.
944
+ let recoveryPending = false;
1066
945
  const skipFrontendRecovery = state.failureCategory === "controller-interrupted";
1067
946
  const hasFrontendWriter = Object.keys(state.nodes).some((id) => FRONTEND_WRITER_NODE_IDS.includes(id));
1068
947
  if (hasFrontendWriter && !skipFrontendRecovery) {
1069
948
  const admission = await readFrontendPrewriteResult(runDir);
1070
949
  if (admission.ok) {
1071
- // M8: admission has only admitted|blocked. A blocked admission whose
1072
- // failureSource is frontend-plan-pi is the contract-invalid analog of
1073
- // the old retryable-invalid path (candidate continuation may re-plan);
1074
- // every other blocked admission is terminal.
1075
- const planContractInvalid = admission.result.classification === "blocked" &&
1076
- admission.result.failureSource === "frontend-plan-pi";
1077
- // A blocked admission caused by the design review committing
1078
- // `request_design_changes` is a definite finding (not a protocol or
1079
- // environment error): the design was reviewed and rejected. It is the
1080
- // design-review analog of review-request-changes — not terminal, but
1081
- // re-enters the next plan round with the committed design findings.
1082
- // The failureSource may be frontend-writer-admission-shell or
1083
- // frontend-design-policy-shell; the committed design fact is the
1084
- // authoritative signal.
1085
- const designRequestFact = await readCommittedDesignRequestFact({
1086
- runDir,
1087
- });
1088
- const designRejected = admission.result.classification === "blocked" &&
1089
- designRequestFact !== undefined;
1090
- const terminalAdmissionDenial = (admission.result.classification === "blocked" ||
1091
- admission.result.classification === "stale") &&
1092
- !planContractInvalid &&
1093
- !designRejected;
1094
- if (terminalAdmissionDenial) {
950
+ if (admission.result.classification === "blocked") {
1095
951
  state.status = "failed";
1096
952
  return { recoveryPending: false };
1097
953
  }
1098
- if (designRejected && designRequestFact) {
1099
- state.terminalReason = "design-request-changes";
1100
- try {
1101
- await writeFrontendReviewFindingsPrd({
1102
- runDir,
1103
- parentRunId: state.runId,
1104
- fact: designRequestFact,
1105
- });
1106
- }
1107
- catch (error) {
1108
- console.warn(`[run-dag] warning: failed to write frontend design findings PRD: ${error instanceof Error ? error.message : String(error)}`);
1109
- }
1110
- const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1111
- if (continuationCount < frontendRecoveryQuota) {
1112
- recoveryTrigger = {
1113
- requestId: state.runId,
1114
- failureSource: "frontend-design-review-pi",
1115
- failureOwner: classifyReviewIssueCategoryFailureOwner(designRequestFact.issueCategory),
1116
- protocolFailureReason: `frontend design review request_design_changes (issueCategory=${designRequestFact.issueCategory})`,
1117
- recoveryMode: "new-dag",
1118
- };
1119
- }
1120
- }
1121
- if (planContractInvalid) {
954
+ if (admission.result.classification === "retryable-invalid") {
1122
955
  const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1123
- if (continuationCount >= frontendRecoveryQuota) {
1124
- // Continuation quota exhausted: a second contract-invalid candidate
956
+ if (continuationCount >= 1) {
957
+ // Continuation quota exhausted: a second retryable-invalid candidate
1125
958
  // is authoritative candidate-contract-invalid → forced failed.
1126
959
  state.frontendRecoveryResult = buildCandidateContractInvalidResult(state, admission.result, admission.artifactHash);
1127
960
  state.status = "failed";
1128
961
  return { recoveryPending: false };
1129
962
  }
1130
- // Root continuation still available: emit a recovery trigger for the
1131
- // M6 operation controller. The parent no longer writes
1132
- // frontendRecoveryState or stays in active/ for a child CAS.
1133
- recoveryTrigger = {
1134
- requestId: state.runId,
1135
- failureSource: admission.result.failureSource ?? "frontend-plan-pi",
1136
- failureOwner: "plan",
1137
- protocolFailureReason: "frontend candidate contract invalid",
1138
- recoveryMode: "new-dag",
1139
- };
963
+ // Root continuation still available: record the recovery intent and let
964
+ // mechanical aggregation settle partial_failed/failed. The parent keeps
965
+ // phase != settled (recovery in progress) and no final root manifest.
966
+ state.frontendRecoveryState = buildFrontendRecoveryIntent(state);
967
+ recoveryPending = true;
1140
968
  }
1141
969
  }
1142
970
  }
1143
- // Phase 5: writer transient partial write → rollback + recovery trigger.
1144
- // Only for frontend-implementation writers with a remaining root
1145
- // continuation, and only when the rollback journal (captured before the
1146
- // provider call) restores cleanly; otherwise the run stays failed with
1147
- // auto-recovery-blocked.
1148
- if (hasFrontendWriter && !recoveryTrigger && !skipFrontendRecovery) {
971
+ // Phase 5: writer transient partial write → rollback + recovery intent. Only
972
+ // for frontend-implementation writers with a remaining root continuation, and
973
+ // only when the rollback journal (captured before the provider call) restores
974
+ // cleanly; otherwise the run stays failed with auto-recovery-blocked.
975
+ if (hasFrontendWriter && !recoveryPending && !skipFrontendRecovery) {
1149
976
  const writerNodeId = FRONTEND_WRITER_NODE_IDS.find((id) => {
1150
977
  const node = state.nodes[id];
1151
978
  return (node &&
@@ -1156,20 +983,15 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
1156
983
  });
1157
984
  if (writerNodeId) {
1158
985
  const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1159
- if (continuationCount < frontendRecoveryQuota) {
986
+ if (continuationCount < 1) {
1160
987
  const { rollbackFrontendWriter } = await import("./frontend-writer-recovery.js");
1161
988
  const rollback = await rollbackFrontendWriter({
1162
989
  cwd: repoRoot,
1163
990
  parentRunId: state.runId,
1164
991
  });
1165
992
  if (rollback.ok) {
1166
- recoveryTrigger = {
1167
- requestId: state.runId,
1168
- failureSource: "frontend-implement-pi",
1169
- failureOwner: "implementation",
1170
- protocolFailureReason: "frontend writer transient partial write",
1171
- recoveryMode: "continuation",
1172
- };
993
+ state.frontendRecoveryState = buildFrontendRecoveryIntent(state, "frontend-implement-pi");
994
+ recoveryPending = true;
1173
995
  }
1174
996
  else {
1175
997
  state.frontendRecoveryResult = buildWriterPartialWriteBlockedResult(state, writerNodeId, rollback.reason);
@@ -1179,64 +1001,6 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
1179
1001
  }
1180
1002
  }
1181
1003
  }
1182
- // Phase 6: verification failure → automatic recovery (re-implement +
1183
- // re-verify). The verify shell is terminal by design so the failed run's
1184
- // evidence stays clean; the M6 recovery operation re-runs implement and
1185
- // verify in a fresh continuation, guarded by the same continuation quota,
1186
- // budget fold and failure fingerprint as the other recovery paths.
1187
- if (hasFrontendWriter && !recoveryTrigger && !skipFrontendRecovery) {
1188
- const verifyFailed = state.nodes["frontend-verify-shell"]?.status === "ERROR";
1189
- if (verifyFailed) {
1190
- const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1191
- if (continuationCount < frontendRecoveryQuota) {
1192
- recoveryTrigger = {
1193
- requestId: state.runId,
1194
- failureSource: "frontend-verify-shell",
1195
- failureOwner: "implementation",
1196
- protocolFailureReason: "frontend verification failure",
1197
- recoveryMode: "continuation",
1198
- };
1199
- }
1200
- }
1201
- }
1202
- // Phase 7: frontend review "request changes" → not passed. When the review
1203
- // node FINISHED but committed `request_review_changes` (typed terminal
1204
- // verdict), the implementation was reviewed and rejected. The run must not
1205
- // surface as finished: terminal aggregation below stays partial_failed, a
1206
- // terminal reason is recorded, the committed findings are rendered into a
1207
- // downstream-task PRD draft (run-owned), and a recovery trigger carries the
1208
- // issueCategory-derived owner so a later DAG round consumes the findings
1209
- // instead of repeating the previous flow.
1210
- const reviewRequestFact = await readCommittedReviewRequestFact({
1211
- runDir,
1212
- });
1213
- if (reviewRequestFact) {
1214
- state.terminalReason = "review-request-changes";
1215
- try {
1216
- await writeFrontendReviewFindingsPrd({
1217
- runDir,
1218
- parentRunId: state.runId,
1219
- fact: reviewRequestFact,
1220
- });
1221
- }
1222
- catch (error) {
1223
- // The PRD draft is additive; a write failure must not mask the
1224
- // not-passed verdict. The terminal aggregation below still applies.
1225
- console.warn(`[run-dag] warning: failed to write frontend review findings PRD: ${error instanceof Error ? error.message : String(error)}`);
1226
- }
1227
- if (!recoveryTrigger && !skipFrontendRecovery) {
1228
- const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1229
- if (continuationCount < frontendRecoveryQuota) {
1230
- recoveryTrigger = {
1231
- requestId: state.runId,
1232
- failureSource: "frontend-review-pi",
1233
- failureOwner: classifyReviewIssueCategoryFailureOwner(reviewRequestFact.issueCategory),
1234
- protocolFailureReason: `frontend review request_review_changes (issueCategory=${reviewRequestFact.issueCategory})`,
1235
- recoveryMode: "new-dag",
1236
- };
1237
- }
1238
- }
1239
- }
1240
1004
  const finishedCount = Object.values(state.nodes).filter((n) => n.status === "FINISHED").length;
1241
1005
  const { supersededIds, supersededFailures } = collectSupersededIntermediateFailures(state);
1242
1006
  if (state.convergence && supersededFailures.length > 0) {
@@ -1252,13 +1016,7 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
1252
1016
  if (effectiveErrorCount === 0 &&
1253
1017
  successTerminal &&
1254
1018
  finishedCount + conditionSkippedCount + supersededErrorCount === taskCount) {
1255
- // Phase 7 guard: a committed `request_review_changes` means the review
1256
- // rejected the implementation; the run must never surface as finished
1257
- // even though every node technically FINISHED.
1258
- state.status =
1259
- state.terminalReason === "review-request-changes"
1260
- ? "partial_failed"
1261
- : "finished";
1019
+ state.status = "finished";
1262
1020
  }
1263
1021
  else if (finishedCount > 0) {
1264
1022
  state.status = "partial_failed";
@@ -1266,84 +1024,19 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
1266
1024
  else {
1267
1025
  state.status = "failed";
1268
1026
  }
1269
- return recoveryTrigger
1270
- ? { recoveryPending: true, recovery: recoveryTrigger }
1271
- : { recoveryPending: false };
1272
- }
1273
- /**
1274
- * Construct (or reuse) the recovery operation for a failed frontend run.
1275
- *
1276
- * A parent-run atomic lease is acquired before `OperationStore.create`, then
1277
- * its operation id is persisted into that lease before the controller may run.
1278
- * `clientRequestId` remains a useful store-level replay key, but is not a
1279
- * cross-process recovery admission boundary. The continuation DagSpec/reset
1280
- * partition is frozen via `computeFrontendRecoveryPlanForSource` and
1281
- * serialized under the run dir's bounded `.runtime/` namespace. `planHash` is
1282
- * the second idempotency dimension of `actionParams`.
1283
- */
1284
- async function createRecoveryOperation(input) {
1285
- const { cwd, runDir, spec, state, trigger } = input;
1286
- const recoveryLease = await acquireDagRecoveryLease({
1287
- cwd,
1288
- parentRunId: state.runId,
1289
- requestId: trigger.requestId,
1290
- kind: "auto-frontend-recovery",
1291
- });
1292
- if (recoveryLease.kind === "held") {
1293
- return { kind: "lease-held", lease: recoveryLease.lease };
1294
- }
1295
- const { computeFrontendRecoveryPlanForSource } = await import("./frontend-recovery-plan.js");
1296
- const plan = computeFrontendRecoveryPlanForSource({
1297
- spec,
1298
- parentRunId: state.runId,
1299
- requestId: trigger.requestId,
1300
- failureSource: trigger.failureSource,
1301
- });
1302
- const planHash = createHash("sha256")
1303
- .update(JSON.stringify(plan))
1304
- .digest("hex");
1305
- const continuationPath = path.join(runDir, ".runtime", "frontend-recovery", `${trigger.requestId}.json`);
1306
- await writeJsonAtomic(continuationPath, {
1307
- schemaVersion: 1,
1308
- parentRunId: state.runId,
1309
- requestId: trigger.requestId,
1310
- failureSource: trigger.failureSource,
1311
- recoveryMode: trigger.recoveryMode,
1312
- plan,
1313
- });
1314
- const created = await input.deps.store.create({
1315
- clientRequestId: `dag-recovery-${state.runId}`,
1316
- action: "dagRunTask",
1317
- actionParams: {
1318
- parentRunId: state.runId,
1319
- requestId: trigger.requestId,
1320
- failureSource: trigger.failureSource,
1321
- failureOwner: trigger.failureOwner,
1322
- protocolFailureReason: trigger.protocolFailureReason,
1323
- recoveryMode: trigger.recoveryMode,
1324
- planHash,
1325
- },
1326
- cliArgs: [
1327
- "dag",
1328
- "execute",
1329
- "--dag",
1330
- continuationPath,
1331
- "--cwd",
1332
- cwd,
1333
- "--json",
1334
- ],
1335
- });
1336
- if (created.kind !== "conflict") {
1337
- // The durable lease must name the operation before controller execution.
1338
- // A crash in this narrow window leaves a safely blocked reservation rather
1339
- // than a second process that can launch another recovery DAG.
1340
- await bindDagRecoveryLeaseExecution({
1341
- leasePath: recoveryLease.leasePath,
1342
- lease: recoveryLease.lease,
1343
- executionRef: created.operation.operationId,
1344
- });
1027
+ // Terminal recovery bookkeeping for a child attempt: a child that finished
1028
+ // recovered settles its own lineage; any other child terminal settles as
1029
+ // auto-recovery-blocked (phase 3 performs no further recovery).
1030
+ const recovery = state.frontendRecoveryState;
1031
+ if (recovery && recovery.attemptIndex >= 1) {
1032
+ if (state.status === "finished") {
1033
+ settleFrontendRecovery(state, "recovered");
1034
+ }
1035
+ else if (!state.frontendRecoveryResult) {
1036
+ settleFrontendRecovery(state, "auto-recovery-blocked");
1037
+ }
1345
1038
  }
1346
- return created;
1039
+ return { recoveryPending };
1347
1040
  }
1348
1041
  function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
1349
1042
  if (rankWriterNodeIds.length <= 1)