@tea-agent/loop-agent 0.41.1-beta.0 → 0.42.0-next.1

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 (273) hide show
  1. package/CHANGELOG.md +61 -207
  2. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  3. package/dist/adapters/context-transfer/pi-session.js +61 -0
  4. package/dist/application/dag/generate-task-dag.js +19 -75
  5. package/dist/application/task-lifecycle/advance.js +5 -24
  6. package/dist/application/task-lifecycle/observe.js +17 -171
  7. package/dist/application/task-lifecycle/plan-transitions.js +7 -42
  8. package/dist/application/task-lifecycle/recommendations.js +2 -13
  9. package/dist/build-stamp.json +3 -3
  10. package/dist/cli/command-definitions.js +12 -7
  11. package/dist/cli/program.js +23 -6
  12. package/dist/commands/client-recovery.js +0 -3
  13. package/dist/commands/dag-artifact.js +284 -0
  14. package/dist/commands/dag-context.js +184 -0
  15. package/dist/commands/dag-rerun.js +206 -1
  16. package/dist/commands/init.js +1 -27
  17. package/dist/commands/task-advance.js +0 -19
  18. package/dist/executors/dag-pi-executor.js +266 -3792
  19. package/dist/executors/pi-executor.js +4 -15
  20. package/dist/executors/pi-sdk-executor.js +3 -0
  21. package/dist/executors/shell-executor.js +217 -945
  22. package/dist/executors/shell-write-guard.js +0 -7
  23. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  24. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  25. package/dist/infrastructure/console/context-export-store.js +160 -0
  26. package/dist/infrastructure/console/dir-lock.js +132 -0
  27. package/dist/infrastructure/console/operation-store.js +197 -0
  28. package/dist/infrastructure/harness/artifact-store.js +10 -1
  29. package/dist/infrastructure/harness/atomic-write.js +12 -2
  30. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  31. package/dist/shared/context-transfer.js +418 -0
  32. package/dist/shared/dag-failure-category.js +0 -12
  33. package/dist/shared/openspec-spec.js +4 -70
  34. package/dist/shared/operator/capabilities.js +14 -7
  35. package/dist/shared/operator/safe-run-summary.js +1 -0
  36. package/dist/shared/path-safety.js +93 -0
  37. package/dist/shared/preview.js +28 -4
  38. package/dist/task/config-types.js +9 -35
  39. package/dist/task/contract/adopt.js +0 -4
  40. package/dist/task/contract/import-revision.js +0 -4
  41. package/dist/task/contract/project.js +0 -3
  42. package/dist/task/contract/schema.js +1 -2
  43. package/dist/task/frontend-project-capability.js +20 -203
  44. package/dist/task/runtime.js +2 -5
  45. package/dist/task/source-prepare/build-draft.js +3 -3
  46. package/dist/task/source-prepare/fragment-inventory.js +9 -15
  47. package/dist/task/source-prepare/prepare.js +1 -89
  48. package/dist/task/source-prepare/semantic-intake.js +2 -6
  49. package/dist/task/source-references.js +1 -22
  50. package/dist/task/task-demand-routing.js +3 -0
  51. package/dist/worker/console/chat/chat-event-store.js +2 -2
  52. package/dist/worker/console/chat/pi-runtime.js +3 -2
  53. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  54. package/dist/worker/console/chat/routes.js +13 -4
  55. package/dist/worker/console/chat/semantic-activity.js +12 -11
  56. package/dist/worker/console/chat/session-stats.js +96 -0
  57. package/dist/worker/console/chat/session-store.js +1 -1
  58. package/dist/worker/console/chat/turn-process.js +28 -4
  59. package/dist/worker/console/chat/user-questions.js +1 -1
  60. package/dist/worker/console/console-update-runtime.js +1 -1
  61. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  62. package/dist/worker/console/dag-confirmation.js +1 -1
  63. package/dist/worker/console/dag-execution-receipt.js +1 -1
  64. package/dist/worker/console/doctor.js +1 -1
  65. package/dist/worker/console/draft-store.js +1 -1
  66. package/dist/worker/console/human-gate-token.js +1 -1
  67. package/dist/worker/console/index.js +3 -6
  68. package/dist/worker/console/interview/assessment.js +1 -1
  69. package/dist/worker/console/interview/session.js +1 -1
  70. package/dist/worker/console/operation-runner.js +45 -4
  71. package/dist/worker/console/operation-sse.js +1 -1
  72. package/dist/worker/console/operation-wait.js +1 -1
  73. package/dist/worker/console/operator-actions.js +107 -168
  74. package/dist/worker/console/operator-surface-health.js +1 -1
  75. package/dist/worker/console/operator-user-error.js +169 -0
  76. package/dist/worker/console/pi-plugins.js +60 -1
  77. package/dist/worker/console/routes.js +637 -8
  78. package/dist/worker/console/security.js +35 -0
  79. package/dist/worker/console/server.js +6 -6
  80. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Bho4we4c.js → abnfDiagram-N423BO3Z-DZZ8m3PO.js} +1 -1
  81. package/dist/worker/console/static/assets/{arc-TBDkTeK0.js → arc-D6PvaVd-.js} +1 -1
  82. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BF_fZrn2.js → architectureDiagram-T3A2C74G-B_OTOiI8.js} +1 -1
  83. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CCnPgUe2.js → blockDiagram-VBNYF7ZC-Bv6rqHBg.js} +1 -1
  84. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCYZJLAT.js → c4Diagram-5PPSVZJV-B8eHr0oz.js} +1 -1
  85. package/dist/worker/console/static/assets/channel-BU5gOilw.js +1 -0
  86. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D8K6adhU.js → chunk-2GRJ4B5K-DYwR0im2.js} +1 -1
  87. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DkT1Q78b.js → chunk-2Q5K7J3B-D2WPGqXt.js} +1 -1
  88. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DuhvpIr7.js → chunk-5RXB4S5H-CQISJ_I7.js} +1 -1
  89. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BnK6dC49.js → chunk-5VM5RSS4-C0o2Du1e.js} +1 -1
  90. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DPz__z7r.js → chunk-6Q2QTUOP-4f8kr-U7.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-0kkppM2j.js → chunk-GF5L2VYU-D_OWvXzX.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BB41iK5t.js → chunk-JWPE2WC7-CasdPz5X.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CiVrxKHU.js → chunk-KBJHAD2P-Cj8lRrla.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C2h9Xje1.js → chunk-RYQCIY6F-CUvD1FSd.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-3iOSHdbM.js → chunk-XXDRQBXY-CqLw_eqB.js} +1 -1
  96. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +1 -0
  97. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +1 -0
  98. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BN3A7tkk.js → cose-bilkent-JH36ORCC-ku-WqJPx.js} +1 -1
  99. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CTuBy0Or.js → cynefin-VYW2F7L2-Bq_PYDhl.js} +1 -1
  100. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-wb6tUlMN.js → cynefinDiagram-MW4NZA55-BiGZV641.js} +1 -1
  101. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-GP1Ivx8B.js → dagre-VZM6K2ZE-C4JRlglF.js} +1 -1
  102. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ejrcK6L0.js → diagram-7IWD3JNH-B0dNeWYG.js} +1 -1
  103. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-YFkCCvUU.js → diagram-B4RE2ZJO-Cj35YvYM.js} +1 -1
  104. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B4rrvIaL.js → diagram-LBJQPF4R-uzQoJ2-8.js} +1 -1
  105. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C-YIZcUf.js → diagram-Q27KOJAE-D1a-Buoz.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPF5wcL1.js → diagram-UB23O5K3-XjRrLRSs.js} +1 -1
  107. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CU5SzXjO.js → ebnfDiagram-BXEA7PRR-Da_O24cW.js} +1 -1
  108. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CpL414DE.js → erDiagram-JOGREHBK-BLJ8jrYU.js} +1 -1
  109. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-MrhLzNau.js → flowDiagram-UKHOOZJN-B9GrLjM0.js} +1 -1
  110. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Bg4qFuUo.js → ganttDiagram-PKOTCBZU-CeJ0TqiK.js} +1 -1
  111. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-ColxSXLR.js → gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js} +1 -1
  112. package/dist/worker/console/static/assets/index-H9rFJiGL.css +1 -0
  113. package/dist/worker/console/static/assets/index-xwu9GxEc.js +451 -0
  114. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czvtm6gc.js → infoDiagram-6WML65LV-DB26i3d8.js} +1 -1
  115. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js → ishikawaDiagram-WSZJBQD7-BOGRyeZT.js} +1 -1
  116. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DWYZcvph.js → journeyDiagram-NVQOT4AX-CZRoBO_6.js} +1 -1
  117. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-KTLZRUjx.js → kanban-definition-27J2QSJJ-qlWhJyoB.js} +1 -1
  118. package/dist/worker/console/static/assets/{linear-_Exn7bfl.js → linear-CfUiDDB3.js} +1 -1
  119. package/dist/worker/console/static/assets/{mermaid.core-Cxg8kX8V.js → mermaid.core-BQe6fpqj.js} +5 -5
  120. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-oKrlo2Bt.js → mindmap-definition-FAOFIHXS-zFzWHw64.js} +1 -1
  121. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-XAeteiNd.js → pegDiagram-VL7TDLO6-DFGUqjZO.js} +1 -1
  122. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BTf1K-lp.js → pieDiagram-7S7Q4E2Y-BB5i0l8Z.js} +1 -1
  123. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js → quadrantDiagram-CIZ2JOQS-obbVZ_X5.js} +1 -1
  124. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BVbKtZhW.js → railroadDiagram-AXF67PYL-BwdaNzc1.js} +1 -1
  125. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C5e4JFcr.js → requirementDiagram-LRYGKXZP-CkTJaugh.js} +1 -1
  126. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cz30hc29.js → sankeyDiagram-W5VNT64P-BwJ-hIgq.js} +1 -1
  127. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DS33n6d0.js → sequenceDiagram-SI44F4Z6-BzSgmm7w.js} +1 -1
  128. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-46w7v-9T.js → sizeCapture-X5ZJPWSS-Bn3oj5Q1.js} +1 -1
  129. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-pbcD5SsZ.js → stateDiagram-OKZ733FA-dxhGtL8K.js} +1 -1
  130. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +1 -0
  131. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DuivYBSj.js → swimlanes-SLNWSIFB-BLpEInya.js} +2 -2
  132. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +8 -0
  133. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cb9VZVIl.js → timeline-definition-Z64GVDOM-ChJGYcXy.js} +1 -1
  134. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-D2-mfOgR.js → vennDiagram-T6HMQDX7-DK-qjRer.js} +1 -1
  135. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DeS2aC76.js → wardleyDiagram-T6FBY63Y-3qeaWAg-.js} +1 -1
  136. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-WGsrFjxH.js → xychartDiagram-ELKLHX3M-CootlyP9.js} +1 -1
  137. package/dist/worker/console/static/index.html +7 -2
  138. package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
  139. package/dist/worker/console/static-src/app/useRunProgress.js +9 -2
  140. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +22 -0
  141. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  142. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -0
  143. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +28 -3
  144. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +106 -0
  145. package/dist/worker/console/static-src/shell/console-update-reload.js +25 -0
  146. package/dist/worker/console/workspace-context.js +28 -112
  147. package/dist/worker/console/workspace-registry.js +2 -2
  148. package/dist/worker/continuation/worker-continuation.js +278 -0
  149. package/dist/worker/observability/read-model.js +4 -0
  150. package/dist/worker/observe/health.js +1 -1
  151. package/dist/worker/observe/node-transparency.js +572 -0
  152. package/dist/worker/observe/routes.js +51 -1
  153. package/dist/worker/observe/static/api.js +69 -5
  154. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  155. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  156. package/dist/worker/observe/static/dag-node-purpose.js +0 -5
  157. package/dist/worker/observe/static/state.js +3 -1
  158. package/dist/worker/observe/static/styles.css +1037 -126
  159. package/dist/worker/observe/static/views/dag-inspector.js +1217 -65
  160. package/dist/worker/observe/static/views/session-timeline.js +121 -25
  161. package/dist/worker/outcomes/adapters.js +19 -0
  162. package/dist/worker/outcomes/types.js +1 -0
  163. package/dist/worker/pool/attempt-lease.js +97 -66
  164. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  165. package/dist/worker/pool/reconcile.js +46 -1
  166. package/dist/worker/pool/run-store.js +2 -0
  167. package/dist/worker/pool/state-projection.js +2 -0
  168. package/dist/worker/task-spec/workflow-routing.js +8 -3
  169. package/dist/workflows/dag/artifact-bindings.js +149 -0
  170. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  171. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  172. package/dist/workflows/dag/budget-enforcement.js +10 -0
  173. package/dist/workflows/dag/context-receipt.js +305 -0
  174. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  175. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  176. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  177. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  178. package/dist/workflows/dag/frontend-implementation-contract.js +162 -932
  179. package/dist/workflows/dag/frontend-plan-render.js +1 -2
  180. package/dist/workflows/dag/frontend-prewrite-gate.js +349 -255
  181. package/dist/workflows/dag/frontend-recovery-plan.js +10 -17
  182. package/dist/workflows/dag/frontend-recovery-run.js +20 -33
  183. package/dist/workflows/dag/frontend-repair.js +432 -1
  184. package/dist/workflows/dag/frontend-review-context.js +15 -261
  185. package/dist/workflows/dag/frontend-verification-trace.js +24 -249
  186. package/dist/workflows/dag/frontend-worktree-diff.js +17 -250
  187. package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
  188. package/dist/workflows/dag/init-hybrid.js +640 -685
  189. package/dist/workflows/dag/interrupt-request.js +0 -7
  190. package/dist/workflows/dag/node-execution.js +116 -759
  191. package/dist/workflows/dag/path-safety.js +1 -0
  192. package/dist/workflows/dag/prompt.js +30 -38
  193. package/dist/workflows/dag/report.js +1 -37
  194. package/dist/workflows/dag/rerun-feedback.js +1 -256
  195. package/dist/workflows/dag/rerun-plan.js +557 -11
  196. package/dist/workflows/dag/rerun-run.js +601 -38
  197. package/dist/workflows/dag/rerun-task.js +0 -29
  198. package/dist/workflows/dag/retry-policy.js +104 -214
  199. package/dist/workflows/dag/runner.js +142 -430
  200. package/dist/workflows/dag/scheduler.js +16 -114
  201. package/dist/workflows/dag/skill-snapshot.js +17 -0
  202. package/dist/workflows/dag/types.js +195 -246
  203. package/dist/workflows/dag/validate.js +12 -28
  204. package/docs/architecture/runtime-boundaries.md +6 -6
  205. package/docs/architecture/worker-and-feature.md +1 -1
  206. package/docs/init-surface.manifest.json +12 -30
  207. package/docs/skills/vetted-skill-registry.md +2 -4
  208. package/docs/templates/README.md +0 -2
  209. package/docs/templates/agent-dag-report.schema.json +2 -8
  210. package/docs/templates/agent-dag.schema.json +33 -1
  211. package/docs/templates/frontend-implementation-contract.schema.json +1 -4
  212. package/docs/templates/product-line/task.yaml +1 -1
  213. package/harness.json +3 -3
  214. package/package.json +3 -1
  215. package/skills/frontend-bounded-implement/SKILL.md +14 -15
  216. package/skills/frontend-design-review/SKILL.md +41 -22
  217. package/skills/frontend-implementation/SKILL.md +52 -0
  218. package/skills/frontend-implementation/references/code-standards.md +33 -0
  219. package/skills/frontend-implementation/references/design-spec.md +56 -0
  220. package/skills/frontend-implementation/references/node-contracts.md +31 -0
  221. package/skills/frontend-review/SKILL.md +15 -20
  222. package/skills/frontend-review/references/review-findings.md +7 -6
  223. package/skills/frontend-verification/SKILL.md +1 -1
  224. package/skills/loop-agent/references/command-reference.md +6 -1
  225. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  226. package/dist/commands/dag-follow-up.js +0 -138
  227. package/dist/executors/pi-read-budget-policy.js +0 -239
  228. package/dist/worker/console/frontend-human-decision-adapter.js +0 -19
  229. package/dist/worker/console/frontend-split-operation-adapter.js +0 -20
  230. package/dist/worker/console/operation-store.js +0 -389
  231. package/dist/worker/console/static/assets/channel-bnXW0U3C.js +0 -1
  232. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +0 -1
  233. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +0 -1
  234. package/dist/worker/console/static/assets/index-D3CC4eUz.css +0 -1
  235. package/dist/worker/console/static/assets/index-DLt4ZFvD.js +0 -437
  236. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +0 -1
  237. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +0 -8
  238. package/dist/worker/console/static-src/active-run-badge.js +0 -17
  239. package/dist/worker/materialize/frontend-split-task-materializer.js +0 -72
  240. package/dist/workflows/dag/dag-retry-schema.js +0 -138
  241. package/dist/workflows/dag/frontend-closeout.js +0 -221
  242. package/dist/workflows/dag/frontend-design-policy.js +0 -400
  243. package/dist/workflows/dag/frontend-human-decision.js +0 -182
  244. package/dist/workflows/dag/frontend-provider-capability-matrix.js +0 -159
  245. package/dist/workflows/dag/frontend-read-budgets.js +0 -12
  246. package/dist/workflows/dag/frontend-recovery-capsule.js +0 -455
  247. package/dist/workflows/dag/frontend-recovery-controller.js +0 -202
  248. package/dist/workflows/dag/frontend-recovery-lineage.js +0 -178
  249. package/dist/workflows/dag/frontend-review-findings.js +0 -270
  250. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -914
  251. package/dist/workflows/dag/frontend-shape-capsule-store.js +0 -191
  252. package/dist/workflows/dag/frontend-shape-facts.js +0 -409
  253. package/dist/workflows/dag/frontend-shape.js +0 -427
  254. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +0 -108
  255. package/dist/workflows/dag/frontend-split-application-service.js +0 -203
  256. package/dist/workflows/dag/frontend-split-orchestrator.js +0 -899
  257. package/dist/workflows/dag/frontend-typed-event-store.js +0 -452
  258. package/dist/workflows/dag/frontend-typed-event-transaction.js +0 -180
  259. package/dist/workflows/dag/frontend-writer-admission.js +0 -285
  260. package/dist/workflows/dag/frontend-writer-status.js +0 -256
  261. package/dist/workflows/dag/recovery-lease.js +0 -80
  262. package/docs/templates/frontend-implementation-dag.json +0 -89
  263. package/docs/templates/spec-registry.schema.json +0 -45
  264. package/skills/frontend-bounded-implement/references/code-standards.md +0 -19
  265. package/skills/frontend-contract/SKILL.md +0 -23
  266. package/skills/frontend-contract/references/contract-protocol.md +0 -34
  267. package/skills/frontend-plan/SKILL.md +0 -26
  268. package/skills/frontend-plan/references/decision-contract.md +0 -37
  269. package/skills/frontend-plan/references/design-decisions.md +0 -17
  270. package/skills/frontend-scout/SKILL.md +0 -25
  271. package/skills/frontend-scout/references/design-evidence.md +0 -16
  272. package/skills/frontend-scout/references/scout-evidence.md +0 -23
  273. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -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,25 +301,27 @@ 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 {
367
307
  }
368
308
  }
369
309
  export async function runDagContinuation(opts) {
310
+ assertValidDagSpec(opts.spec);
311
+ assertFrozenBudget(opts.spec.budget, opts.state.budget, opts.state.runId);
312
+ if (opts.spec.budget) {
313
+ const ledger = opts.state.budgetLedger;
314
+ if (!ledger ||
315
+ ledger.mode !== opts.spec.budget.mode ||
316
+ JSON.stringify(ledger.limits) !== JSON.stringify(opts.spec.budget.limits)) {
317
+ throw new Error(`dag continuation ${opts.state.runId} is missing a matching cumulative budget ledger`);
318
+ }
319
+ }
320
+ const { assertDagSpecAllowsNewWriterExecution, assertTaskContractBindingConsistent, } = await import("./task-contract-binding.js");
321
+ assertDagSpecAllowsNewWriterExecution(opts.spec);
322
+ await assertTaskContractBindingConsistent({ repoRoot: opts.cwd, spec: opts.spec });
370
323
  const { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), opts.spec.budget);
371
324
  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
325
  state.status = "running";
383
326
  const resumedAt = new Date().toISOString();
384
327
  state.runner = {
@@ -398,12 +341,12 @@ export async function runDagContinuation(opts) {
398
341
  cwd: opts.cwd,
399
342
  maxConcurrent,
400
343
  executeNode: opts.executeNode,
344
+ beforeTerminalArchive: opts.beforeTerminalArchive,
401
345
  observer: opts.observer,
402
346
  abortSignal: opts.abortSignal,
403
347
  activeRunDir,
404
348
  completedRunDir,
405
349
  pausedRunDir,
406
- recovery: opts.recovery,
407
350
  });
408
351
  }
409
352
  export async function resumeDagRun(opts) {
@@ -506,13 +449,9 @@ export async function resumeDagRun(opts) {
506
449
  }
507
450
  async function executeDagCheckpoint(input) {
508
451
  const { spec, state, ranks, runDir: initialRunDir, cwd, maxConcurrent, completedRunDir, pausedRunDir, } = input;
509
- bindFrontendShapeStateToRunTopology(state);
510
452
  let runDir = initialRunDir;
511
453
  let stateWriteQueue = Promise.resolve();
512
- let shapeTransitionBarrier;
513
454
  const persistState = async (options) => {
514
- if (shapeTransitionBarrier)
515
- await shapeTransitionBarrier;
516
455
  stateWriteQueue = stateWriteQueue.then(() => writeRunState(runDir, state, options));
517
456
  await stateWriteQueue;
518
457
  };
@@ -637,50 +576,12 @@ async function executeDagCheckpoint(input) {
637
576
  .map((rank) => rank.filter((nodeId) => nodeId === "closeout-pi"))
638
577
  .filter((rank) => rank.length > 0)
639
578
  : [];
640
- const canonicalFrontendSource = isCanonicalFrontendShapeDag(tasksById)
641
- ? await loadCanonicalFrontendTaskSourceBinding(cwd, spec)
642
- : undefined;
643
579
  const executeRanks = (selectedRanks) => executeDagRanksOnce({
644
580
  state,
645
581
  ranks: selectedRanks,
646
582
  tasksById,
647
583
  maxConcurrent,
648
584
  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
585
  runDir,
685
586
  abortSignal: input.abortSignal,
686
587
  createExecuteNodeForRank: (rankWriterNodeIds) => buildRankAwareExecuteNode({
@@ -716,18 +617,6 @@ async function executeDagCheckpoint(input) {
716
617
  });
717
618
  const node = state.nodes[nodeId];
718
619
  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
620
  recordFinishedNodeBudget(state, node);
732
621
  await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
733
622
  await persistState();
@@ -737,9 +626,6 @@ async function executeDagCheckpoint(input) {
737
626
  let pausedByNodeId;
738
627
  while (true) {
739
628
  pausedByNodeId = await executeRanks(convergenceRanks);
740
- if (state.frontendShapeState?.terminalOutcome) {
741
- break;
742
- }
743
629
  if (pausedByNodeId) {
744
630
  break;
745
631
  }
@@ -795,7 +681,7 @@ async function executeDagCheckpoint(input) {
795
681
  runDir = await moveToPausedRunDir(runDir, pausedRunDir);
796
682
  }
797
683
  else {
798
- const { recoveryPending, recovery: recoveryTrigger } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd, spec.frontendRecovery?.maxContinuations ?? 1);
684
+ const { recoveryPending } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
799
685
  await finalizeRunOwnedInterrupt({
800
686
  runDir,
801
687
  state,
@@ -803,65 +689,56 @@ async function executeDagCheckpoint(input) {
803
689
  interruptAborted: interruptController.signal.aborted,
804
690
  });
805
691
  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,
692
+ if (recoveryPending) {
693
+ // Recovery coordinator (phase 3c AC-2/AC-3): materialize the reserved
694
+ // child and run it synchronously. Dynamic import avoids a static
695
+ // runner ↔ frontend-recovery-run module cycle. stageRecoveryChild
696
+ // locates the parent via the canonical active/ layout, so recovery
697
+ // only fires when this run is still the canonical active run dir
698
+ // (direct runDagContinuation callers using an arbitrary runDir keep
699
+ // the previous scheduler-layer skip semantics).
700
+ const canonicalActiveDir = getDagRunDir(cwd, "active", state.runId);
701
+ if (path.resolve(runDir) === path.resolve(canonicalActiveDir)) {
702
+ const { stageRecoveryChild } = await import("./frontend-recovery-run.js");
703
+ const staged = await stageRecoveryChild({
704
+ cwd,
839
705
  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
706
  });
849
- state.frontendRecoveryResult = buildRecoveryResultForTrigger(state, recoveryTrigger, result.terminalReason === "succeeded"
850
- ? "recovered"
851
- : "auto-recovery-blocked");
707
+ // stageRecoveryChild CAS-advanced the parent's on-disk lineage
708
+ // (child-staging → child-running + childRunId). Refresh the in-memory
709
+ // copy so the terminal persistState below does not regress the parent
710
+ // back to stale child-staging.
711
+ const parentOnDisk = await readDagRunState(runDir);
712
+ state.frontendRecoveryState = parentOnDisk.frontendRecoveryState;
713
+ const childState = await readDagRunState(staged.childRunDir);
714
+ const childSpec = await readDagRunSpec(staged.childRunDir);
715
+ await runDagContinuation({
716
+ cwd,
717
+ spec: childSpec,
718
+ state: childState,
719
+ runDir: staged.childRunDir,
720
+ maxConcurrent,
721
+ executeNode: input.executeNode,
722
+ observer: input.observer,
723
+ abortSignal,
724
+ });
852
725
  }
853
726
  }
854
727
  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)}`);
728
+ if (!recoveryPending) {
729
+ if (input.beforeTerminalArchive) {
730
+ await input.beforeTerminalArchive({ runDir, state });
731
+ await persistState();
732
+ }
733
+ try {
734
+ const terminalCheckpoint = await captureWorkspaceCheckpoint(cwd);
735
+ await writeWorkspaceCheckpoint(runDir, WORKSPACE_CHECKPOINT_TERMINAL_REL, terminalCheckpoint);
736
+ }
737
+ catch (error) {
738
+ console.warn(`[run-dag] warning: failed to write workspace terminal checkpoint: ${error instanceof Error ? error.message : String(error)}`);
739
+ }
740
+ runDir = await moveToCompletedRunDir(runDir, completedRunDir);
863
741
  }
864
- runDir = await moveToCompletedRunDir(runDir, completedRunDir);
865
742
  }
866
743
  await relocateRunArtifactPaths({
867
744
  runDir,
@@ -994,21 +871,39 @@ function isSuccessfulConvergenceTerminal(state) {
994
871
  return true;
995
872
  return SUCCESSFUL_CONVERGENCE_TERMINAL_REASONS.has(reason);
996
873
  }
874
+ function buildFrontendRecoveryIntent(state, failureSource) {
875
+ const existing = state.frontendRecoveryState;
876
+ const requestId = existing?.requestId ?? randomUUID();
877
+ return {
878
+ schemaVersion: 1,
879
+ phase: "child-staging",
880
+ requestId,
881
+ recoveryRootRunId: existing?.recoveryRootRunId ?? state.runId,
882
+ parentRunId: state.runId,
883
+ ...(existing?.childRunId ? { childRunId: existing.childRunId } : {}),
884
+ attemptId: existing?.attemptId ?? `recovery-${requestId}`,
885
+ attemptIndex: existing?.attemptIndex ?? 0,
886
+ continuationCount: existing?.continuationCount ?? 0,
887
+ ...(failureSource ? { failureSource } : {}),
888
+ revision: (existing?.revision ?? 0) + 1,
889
+ };
890
+ }
997
891
  function buildCandidateContractInvalidResult(state, result, artifactHash) {
892
+ const recovery = state.frontendRecoveryState;
998
893
  return {
999
894
  schemaVersion: 1,
1000
895
  outcome: "candidate-contract-invalid",
1001
896
  origin: {
1002
897
  kind: "frontend-prewrite-gate",
1003
- parentRunId: state.runId,
1004
- requestId: state.runId,
1005
- failedNodeId: result.failureSource ?? "frontend-plan-pi",
898
+ parentRunId: recovery?.parentRunId ?? state.runId,
899
+ ...(recovery?.childRunId ? { childRunId: recovery.childRunId } : {}),
900
+ requestId: recovery?.requestId ?? "",
901
+ failedNodeId: result.selectedPlanNodeId,
1006
902
  },
1007
903
  failureClass: {
1008
904
  code: "candidate-contract-invalid",
1009
905
  classification: result.classification,
1010
- reason: result.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ") ??
1011
- "candidate contract invalid",
906
+ reason: result.failureReason ?? "candidate contract invalid",
1012
907
  },
1013
908
  evidenceRefs: [
1014
909
  {
@@ -1020,13 +915,15 @@ function buildCandidateContractInvalidResult(state, result, artifactHash) {
1020
915
  };
1021
916
  }
1022
917
  function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
918
+ const recovery = state.frontendRecoveryState;
1023
919
  return {
1024
920
  schemaVersion: 1,
1025
921
  outcome: "auto-recovery-blocked",
1026
922
  origin: {
1027
923
  kind: "frontend-writer",
1028
- parentRunId: state.runId,
1029
- requestId: state.runId,
924
+ parentRunId: recovery?.parentRunId ?? state.runId,
925
+ ...(recovery?.childRunId ? { childRunId: recovery.childRunId } : {}),
926
+ requestId: recovery?.requestId ?? "",
1030
927
  failedNodeId,
1031
928
  },
1032
929
  failureClass: {
@@ -1037,115 +934,64 @@ function buildWriterPartialWriteBlockedResult(state, failedNodeId, reason) {
1037
934
  evidenceRefs: [],
1038
935
  };
1039
936
  }
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
- };
937
+ function settleFrontendRecovery(state, outcome) {
938
+ const recovery = state.frontendRecoveryState;
939
+ if (!recovery)
940
+ return;
941
+ recovery.phase = "settled";
942
+ if (!state.frontendRecoveryResult) {
943
+ state.frontendRecoveryResult = {
944
+ schemaVersion: 1,
945
+ outcome,
946
+ origin: {
947
+ kind: "frontend-prewrite-gate",
948
+ parentRunId: recovery.parentRunId,
949
+ ...(recovery.childRunId ? { childRunId: recovery.childRunId } : {}),
950
+ requestId: recovery.requestId,
951
+ failedNodeId: "",
952
+ },
953
+ evidenceRefs: [],
954
+ };
955
+ }
1054
956
  }
1055
- export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, frontendRecoveryQuota = 1) {
957
+ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
1056
958
  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
959
  // Terminal override: a denied frontend writer must never surface as a
1063
960
  // mechanical partial_failed just because prewrite FINISHED while the writer
1064
- // was SKIPPED. Fail closed on blocked before aggregation.
1065
- let recoveryTrigger;
1066
- const skipFrontendRecovery = state.failureCategory === "controller-interrupted";
961
+ // was SKIPPED. Fail closed on retryable-invalid/blocked before aggregation.
962
+ let recoveryPending = false;
963
+ const skipFrontendRecovery = state.failureCategory === "controller-interrupted" ||
964
+ state.continuation?.writerTransaction !== undefined;
1067
965
  const hasFrontendWriter = Object.keys(state.nodes).some((id) => FRONTEND_WRITER_NODE_IDS.includes(id));
1068
966
  if (hasFrontendWriter && !skipFrontendRecovery) {
1069
967
  const admission = await readFrontendPrewriteResult(runDir);
1070
968
  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) {
969
+ if (admission.result.classification === "blocked") {
1095
970
  state.status = "failed";
1096
971
  return { recoveryPending: false };
1097
972
  }
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) {
973
+ if (admission.result.classification === "retryable-invalid") {
1122
974
  const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1123
- if (continuationCount >= frontendRecoveryQuota) {
1124
- // Continuation quota exhausted: a second contract-invalid candidate
975
+ if (continuationCount >= 1) {
976
+ // Continuation quota exhausted: a second retryable-invalid candidate
1125
977
  // is authoritative candidate-contract-invalid → forced failed.
1126
978
  state.frontendRecoveryResult = buildCandidateContractInvalidResult(state, admission.result, admission.artifactHash);
1127
979
  state.status = "failed";
1128
980
  return { recoveryPending: false };
1129
981
  }
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
- };
982
+ // Root continuation still available: record the recovery intent and let
983
+ // mechanical aggregation settle partial_failed/failed. The parent keeps
984
+ // phase != settled (recovery in progress) and no final root manifest.
985
+ state.frontendRecoveryState = buildFrontendRecoveryIntent(state);
986
+ recoveryPending = true;
1140
987
  }
1141
988
  }
1142
989
  }
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) {
990
+ // Phase 5: writer transient partial write → rollback + recovery intent. Only
991
+ // for frontend-implementation writers with a remaining root continuation, and
992
+ // only when the rollback journal (captured before the provider call) restores
993
+ // cleanly; otherwise the run stays failed with auto-recovery-blocked.
994
+ if (hasFrontendWriter && !recoveryPending && !skipFrontendRecovery) {
1149
995
  const writerNodeId = FRONTEND_WRITER_NODE_IDS.find((id) => {
1150
996
  const node = state.nodes[id];
1151
997
  return (node &&
@@ -1156,20 +1002,15 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
1156
1002
  });
1157
1003
  if (writerNodeId) {
1158
1004
  const continuationCount = state.frontendRecoveryState?.continuationCount ?? 0;
1159
- if (continuationCount < frontendRecoveryQuota) {
1005
+ if (continuationCount < 1) {
1160
1006
  const { rollbackFrontendWriter } = await import("./frontend-writer-recovery.js");
1161
1007
  const rollback = await rollbackFrontendWriter({
1162
1008
  cwd: repoRoot,
1163
1009
  parentRunId: state.runId,
1164
1010
  });
1165
1011
  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
- };
1012
+ state.frontendRecoveryState = buildFrontendRecoveryIntent(state, "frontend-implement-pi");
1013
+ recoveryPending = true;
1173
1014
  }
1174
1015
  else {
1175
1016
  state.frontendRecoveryResult = buildWriterPartialWriteBlockedResult(state, writerNodeId, rollback.reason);
@@ -1179,64 +1020,6 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
1179
1020
  }
1180
1021
  }
1181
1022
  }
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
1023
  const finishedCount = Object.values(state.nodes).filter((n) => n.status === "FINISHED").length;
1241
1024
  const { supersededIds, supersededFailures } = collectSupersededIntermediateFailures(state);
1242
1025
  if (state.convergence && supersededFailures.length > 0) {
@@ -1252,13 +1035,7 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
1252
1035
  if (effectiveErrorCount === 0 &&
1253
1036
  successTerminal &&
1254
1037
  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";
1038
+ state.status = "finished";
1262
1039
  }
1263
1040
  else if (finishedCount > 0) {
1264
1041
  state.status = "partial_failed";
@@ -1266,84 +1043,19 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd, f
1266
1043
  else {
1267
1044
  state.status = "failed";
1268
1045
  }
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
- });
1046
+ // Terminal recovery bookkeeping for a child attempt: a child that finished
1047
+ // recovered settles its own lineage; any other child terminal settles as
1048
+ // auto-recovery-blocked (phase 3 performs no further recovery).
1049
+ const recovery = state.frontendRecoveryState;
1050
+ if (recovery && recovery.attemptIndex >= 1) {
1051
+ if (state.status === "finished") {
1052
+ settleFrontendRecovery(state, "recovered");
1053
+ }
1054
+ else if (!state.frontendRecoveryResult) {
1055
+ settleFrontendRecovery(state, "auto-recovery-blocked");
1056
+ }
1345
1057
  }
1346
- return created;
1058
+ return { recoveryPending };
1347
1059
  }
1348
1060
  function concurrentSiblingWriteSetsForNode(rankWriterNodeIds, nodeId, tasksById) {
1349
1061
  if (rankWriterNodeIds.length <= 1)