@tea-agent/loop-agent 0.42.0-next.9 → 0.42.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 (236) hide show
  1. package/CHANGELOG.md +111 -45
  2. package/dist/application/dag/run-dag.js +8 -2
  3. package/dist/application/evaluation/budget.js +19 -1
  4. package/dist/application/task-lifecycle/advance.js +26 -8
  5. package/dist/application/task-lifecycle/observe.js +43 -29
  6. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/program.js +1 -1
  9. package/dist/commands/dag-rerun-task.js +2 -0
  10. package/dist/commands/task-source-prepare.js +3 -1
  11. package/dist/executors/dag-pi-executor.js +2584 -600
  12. package/dist/executors/pi-executor.js +22 -1
  13. package/dist/executors/pi-extension-resolver.js +14 -2
  14. package/dist/executors/pi-sdk-executor.js +140 -39
  15. package/dist/executors/shell-executor.js +135 -55
  16. package/dist/shared/dag-failure-category.js +6 -0
  17. package/dist/shared/frontend-execution-policy.js +23 -0
  18. package/dist/task/config-types.js +4 -0
  19. package/dist/task/contract/apply.js +36 -2
  20. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  21. package/dist/task/source-prepare/ledger-review.js +6 -9
  22. package/dist/task/source-prepare/parse-intent.js +7 -0
  23. package/dist/task/source-prepare/semantic-intake.js +16 -26
  24. package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
  25. package/dist/task/source-references.js +48 -23
  26. package/dist/worker/console/chat/assistant-content.js +23 -2
  27. package/dist/worker/console/chat/browser-policy.js +143 -0
  28. package/dist/worker/console/chat/browser-routes.js +148 -0
  29. package/dist/worker/console/chat/chat-event-store.js +4 -2
  30. package/dist/worker/console/chat/explore-tools.js +13 -0
  31. package/dist/worker/console/chat/pi-runtime.js +173 -94
  32. package/dist/worker/console/chat/resource-loader.js +4 -1
  33. package/dist/worker/console/chat/routes.js +208 -66
  34. package/dist/worker/console/chat/scm-routes.js +217 -0
  35. package/dist/worker/console/chat/scm-service.js +283 -0
  36. package/dist/worker/console/chat/scm-tools.js +111 -0
  37. package/dist/worker/console/chat/sdd-data-alignment.js +78 -11
  38. package/dist/worker/console/chat/session-catalog.js +32 -0
  39. package/dist/worker/console/chat/session-mode-view.js +48 -0
  40. package/dist/worker/console/chat/session-mode.js +218 -0
  41. package/dist/worker/console/chat/session-store.js +27 -7
  42. package/dist/worker/console/chat/shortcuts.js +6 -0
  43. package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
  44. package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
  45. package/dist/worker/console/chat/subagents/index.js +5 -0
  46. package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
  47. package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
  48. package/dist/worker/console/chat/subagents/types.js +13 -0
  49. package/dist/worker/console/chat/terminal-routes.js +216 -0
  50. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  51. package/dist/worker/console/chat/terminal-tools.js +199 -0
  52. package/dist/worker/console/chat/tool-preview.js +21 -0
  53. package/dist/worker/console/chat/tools.js +13 -1
  54. package/dist/worker/console/chat/turn-process.js +1 -0
  55. package/dist/worker/console/interview/tools.js +1 -0
  56. package/dist/worker/console/server.js +2 -28
  57. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
  58. package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-VJWsxWhB.js} +1 -1
  59. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
  60. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
  61. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
  62. package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
  63. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
  73. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
  74. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
  75. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
  76. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
  77. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
  78. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
  81. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
  82. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
  83. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
  84. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
  85. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
  86. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
  87. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
  88. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
  89. package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
  90. package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
  91. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
  92. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
  93. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
  94. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
  95. package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-G1JR40SX.js} +1 -1
  96. package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-CCgPF0oA.js} +5 -5
  97. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
  98. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
  99. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
  100. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
  101. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
  102. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
  103. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
  104. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
  105. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
  106. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
  107. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
  108. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
  110. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
  111. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
  112. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
  113. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
  114. package/dist/worker/console/static/index.html +2 -2
  115. package/dist/worker/console/static-src/chat-view-types.js +1 -1
  116. package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
  117. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +142 -7
  118. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +44 -0
  119. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  120. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
  121. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  122. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
  123. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  124. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +33 -18
  125. package/dist/worker/console/static-src/operator-chat/useChatStream.js +48 -16
  126. package/dist/worker/console/static-src/operator-chat/useChatThread.js +170 -7
  127. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  128. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  129. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  130. package/dist/worker/console/workspace-context.js +15 -1
  131. package/dist/worker/observe/node-transparency.js +81 -72
  132. package/dist/worker/observe/routes.js +20 -1
  133. package/dist/worker/observe/static/constants.js +22 -22
  134. package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
  135. package/dist/worker/observe/static/dag-history-labels.js +1 -0
  136. package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
  137. package/dist/worker/observe/static/dag-inspector-humanize.js +342 -0
  138. package/dist/worker/observe/static/dag-node-purpose.js +10 -10
  139. package/dist/worker/observe/static/dom.js +20 -1
  140. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  141. package/dist/worker/observe/static/format-pool.js +6 -0
  142. package/dist/worker/observe/static/format.js +7 -0
  143. package/dist/worker/observe/static/index.html +4 -4
  144. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  145. package/dist/worker/observe/static/inspector-submission.js +32 -0
  146. package/dist/worker/observe/static/kpi.js +1 -0
  147. package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
  148. package/dist/worker/observe/static/relations.js +7 -5
  149. package/dist/worker/observe/static/router.js +13 -0
  150. package/dist/worker/observe/static/run-processing.js +2 -0
  151. package/dist/worker/observe/static/shell-chrome.js +36 -3
  152. package/dist/worker/observe/static/state.js +35 -2
  153. package/dist/worker/observe/static/styles.css +431 -39
  154. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  155. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  156. package/dist/worker/observe/static/task-history.js +12 -0
  157. package/dist/worker/observe/static/views/batch.js +6 -13
  158. package/dist/worker/observe/static/views/dag-graph.js +51 -4
  159. package/dist/worker/observe/static/views/dag-inspector.js +1069 -426
  160. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  161. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  162. package/dist/worker/observe/static/views/dag.js +64 -22
  163. package/dist/worker/observe/static/views/dags.js +2 -0
  164. package/dist/worker/observe/static/views/dashboard.js +21 -12
  165. package/dist/worker/observe/static/views/failures.js +21 -11
  166. package/dist/worker/observe/static/views/feature.js +11 -29
  167. package/dist/worker/observe/static/views/pool.js +37 -28
  168. package/dist/worker/observe/static/views/run.js +48 -5
  169. package/dist/worker/observe/static/views/session-timeline.js +194 -243
  170. package/dist/worker/observe/static/views/task.js +81 -62
  171. package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
  172. package/dist/workflows/dag/budget-enforcement.js +53 -3
  173. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  174. package/dist/workflows/dag/frontend-capacity.js +9 -0
  175. package/dist/workflows/dag/frontend-contract-facts.js +130 -0
  176. package/dist/workflows/dag/frontend-design-policy.js +103 -17
  177. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  178. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  179. package/dist/workflows/dag/frontend-implementation-contract.js +423 -41
  180. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  181. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  182. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  183. package/dist/workflows/dag/frontend-plan-render.js +21 -3
  184. package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
  185. package/dist/workflows/dag/frontend-recovery-controller.js +32 -8
  186. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  187. package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
  188. package/dist/workflows/dag/frontend-recovery-run.js +146 -16
  189. package/dist/workflows/dag/frontend-review-scopes.js +139 -0
  190. package/dist/workflows/dag/frontend-risk.js +2 -0
  191. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  192. package/dist/workflows/dag/frontend-shadow-dual-write.js +37 -3
  193. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  194. package/dist/workflows/dag/frontend-shape.js +46 -7
  195. package/dist/workflows/dag/frontend-test-execution-evidence.js +49 -0
  196. package/dist/workflows/dag/frontend-typed-event-store.js +14 -0
  197. package/dist/workflows/dag/frontend-verification-trace.js +38 -4
  198. package/dist/workflows/dag/frontend-writer-admission.js +2 -2
  199. package/dist/workflows/dag/init-hybrid.js +70 -35
  200. package/dist/workflows/dag/node-execution.js +225 -226
  201. package/dist/workflows/dag/prompt.js +4 -0
  202. package/dist/workflows/dag/recovery-lease.js +106 -16
  203. package/dist/workflows/dag/rerun-feedback.js +1 -0
  204. package/dist/workflows/dag/rerun-plan.js +96 -4
  205. package/dist/workflows/dag/rerun-run.js +28 -6
  206. package/dist/workflows/dag/rerun-task.js +221 -18
  207. package/dist/workflows/dag/retry-policy.js +27 -10
  208. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  209. package/dist/workflows/dag/runner.js +246 -19
  210. package/dist/workflows/dag/structured-output-repair.js +4 -1
  211. package/dist/workflows/dag/types.js +10 -3
  212. package/dist/workflows/dag/validate.js +10 -8
  213. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  214. package/docs/operations/README.md +2 -0
  215. package/docs/templates/agent-dag.schema.json +2 -2
  216. package/docs/templates/backend-test-dag.json +6 -4
  217. package/docs/templates/frontend-design-contract.md +4 -4
  218. package/docs/templates/frontend-implementation-contract.schema.json +68 -4
  219. package/docs/templates/frontend-implementation-dag.json +5 -5
  220. package/harness.json +1 -1
  221. package/package.json +8 -6
  222. package/skills/frontend-contract/SKILL.md +2 -1
  223. package/skills/frontend-contract/references/contract-protocol.md +19 -3
  224. package/skills/frontend-design-review/SKILL.md +12 -11
  225. package/skills/frontend-plan/SKILL.md +22 -2
  226. package/skills/frontend-plan/references/decision-contract.md +114 -5
  227. package/skills/frontend-plan/references/design-decisions.md +32 -0
  228. package/skills/frontend-review/SKILL.md +10 -11
  229. package/skills/frontend-scout/references/scout-evidence.md +4 -0
  230. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
  231. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
  232. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
  233. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
  234. package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
  235. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
  236. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
@@ -1,10 +1,13 @@
1
1
  import { copyFile, mkdir, readdir, readFile, rm, access } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
3
4
  import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
4
5
  import { sha256Hex } from "./frontend-implementation-contract.js";
5
6
  import { FRONTEND_RECOVERY_IMPORT_MANIFEST_REL_PATH, FRONTEND_RECOVERY_INTENT_REL_DIR, FRONTEND_RECOVERY_STAGING_PREFIX, computeFrontendRecoveryPlan, computeFrontendRecoveryPlanForSource, } from "./frontend-recovery-plan.js";
6
- import { dagRunDirExists, getDagRunDir, readDagRunSpec, readDagRunState, transferDagRunDir, writeDagRunState, } from "./lifecycle.js";
7
+ import { dagRunDirExists, getDagRunDir, locateDagRun, readDagRunSpec, readDagRunState, transferDagRunDir, writeDagRunState, } from "./lifecycle.js";
7
8
  import { buildDagRunId } from "./runner.js";
9
+ import { buildFrontendTopologyBinding } from "./frontend-shape.js";
10
+ import { FRONTEND_SHAPE_BOUNDARIES, isCanonicalFrontendShapeDag, } from "./frontend-shape-facts.js";
8
11
  import { prepareRunDir, writeNodeRecord } from "./run-store.js";
9
12
  import { FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, readFrontendWriterAdmissionResult, } from "./scheduler.js";
10
13
  import { topoSortToRanks } from "./topo.js";
@@ -273,34 +276,52 @@ async function casUpdateFrontendRecoveryState(parentRunDir, expectedRevision, de
273
276
  await deps.writeParentState(parentRunDir, state);
274
277
  }
275
278
  async function materializeChildStaging(input) {
276
- const { cwd, parentRunDir, parentRunId, parentSpec, parentState, recovery, requestId, childRunId, stagingDir, createdAt, } = input;
279
+ const { cwd, parentRunDir, parentRunId, parentSpec, parentState, recovery, recoveryPlan, requestId, childRunId, stagingDir, createdAt, } = input;
277
280
  await prepareRunDir(stagingDir);
278
281
  // Design-review rejection must be visible to the reset planner. Recovery
279
282
  // children otherwise copy the old spec and only rerun design review, which
280
283
  // deterministically re-rejects the unchanged plan. Bind the parent's typed
281
284
  // findings into the child spec as ordinary rerun feedback.
282
285
  let childSpec = parentSpec;
283
- if (recovery.failureSource === "frontend-plan-pi") {
286
+ if (recovery.failureSource === "frontend-plan-pi" ||
287
+ recovery.failureSource === "frontend-review-pi") {
288
+ const designRecovery = parentState.terminalReason === "design-request-changes";
289
+ const reviewRecovery = recovery.failureSource === "frontend-review-pi" ||
290
+ parentState.terminalReason === "review-request-changes";
291
+ let feedback;
284
292
  try {
285
293
  const { deriveDagRerunFeedback } = await import("./rerun-feedback.js");
286
294
  const taskId = parentSpec.taskContractBinding?.taskId ??
287
295
  parentSpec.sourceBinding?.taskId ??
288
296
  parentRunId;
289
- const feedback = await deriveDagRerunFeedback({
297
+ feedback = await deriveDagRerunFeedback({
290
298
  parentRunId,
291
299
  taskId,
292
300
  runDir: parentRunDir,
293
301
  spec: parentSpec,
294
302
  state: parentState,
295
303
  });
296
- if (feedback)
297
- childSpec = { ...parentSpec, rerunFeedback: feedback };
304
+ if (reviewRecovery &&
305
+ feedback &&
306
+ feedback.sourceNodeId !== "frontend-review-pi") {
307
+ // Do not accidentally carry an older design/verify finding when a
308
+ // review-triggered continuation has lost its typed review fact.
309
+ feedback = undefined;
310
+ }
298
311
  }
299
312
  catch (error) {
300
- // A design rejection cannot be safely retried without its typed
313
+ if (designRecovery || reviewRecovery) {
314
+ throw new Error(`frontend recovery feedback unavailable: ${error instanceof Error ? error.message : String(error)}`);
315
+ }
316
+ }
317
+ if (feedback) {
318
+ childSpec = { ...parentSpec, rerunFeedback: feedback };
319
+ }
320
+ else if (designRecovery || reviewRecovery) {
321
+ // A design/review rejection cannot be safely retried without its typed
301
322
  // findings. Fail closed instead of deterministically re-running the
302
323
  // unchanged plan and producing another rejection.
303
- throw new Error(`frontend recovery feedback unavailable: ${error instanceof Error ? error.message : String(error)}`);
324
+ throw new Error(`frontend recovery feedback unavailable: no typed ${reviewRecovery ? "review" : "design"} findings were found for recovery`);
304
325
  }
305
326
  }
306
327
  await writeJsonVerified(stagingDir, "run.json", childSpec);
@@ -308,7 +329,10 @@ async function materializeChildStaging(input) {
308
329
  // (frontend-implement-pi) instead of the candidate producer; everything
309
330
  // upstream is imported as verified parent facts.
310
331
  let plan;
311
- if (recovery.failureSource) {
332
+ if (recoveryPlan) {
333
+ plan = recoveryPlan;
334
+ }
335
+ else if (recovery.failureSource) {
312
336
  plan = computeFrontendRecoveryPlanForSource({
313
337
  spec: parentSpec,
314
338
  parentRunId,
@@ -373,7 +397,25 @@ async function materializeChildStaging(input) {
373
397
  newRunDir: stagingDir,
374
398
  relativePath: FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT,
375
399
  });
400
+ // The scheduler gates every shape boundary by its committed fact once the
401
+ // boundary's source node is FINISHED. Imported nodes stay FINISHED in the
402
+ // child, so their boundaries must inherit the parent's committed facts or
403
+ // the child hard-blocks at the first gated rank. Reset boundaries re-commit
404
+ // their own facts when their source nodes re-run.
405
+ const inheritedCommittedFacts = {};
406
+ if (isCanonicalFrontendShapeDag(new Map(childSpec.tasks.map((task) => [task.id, task])))) {
407
+ for (const entry of FRONTEND_SHAPE_BOUNDARIES) {
408
+ if (!importedSet.has(entry.sourceNodeId))
409
+ continue;
410
+ const parentFact = parentState.frontendShapeState?.committedFacts[entry.boundary];
411
+ if (parentFact) {
412
+ inheritedCommittedFacts[entry.boundary] = structuredClone(parentFact);
413
+ }
414
+ }
415
+ }
376
416
  const childState = {
417
+ ...(parentState.budget ? { budget: structuredClone(parentState.budget) } : {}),
418
+ ...(parentState.budgetLedger ? { budgetLedger: structuredClone(parentState.budgetLedger) } : {}),
377
419
  version: 1,
378
420
  title: parentSpec.title,
379
421
  runId: childRunId,
@@ -382,7 +424,20 @@ async function materializeChildStaging(input) {
382
424
  status: "pending",
383
425
  ranks,
384
426
  nodes,
427
+ // Mirror createInitialRunState: a canonical frontend child must carry its
428
+ // own frozen topology binding or executeDag's binding assert rejects the
429
+ // staged child before its first node runs.
430
+ ...(isCanonicalFrontendShapeDag(new Map(childSpec.tasks.map((task) => [task.id, task])))
431
+ ? {
432
+ frontendShapeState: {
433
+ frozenTopology: buildFrontendTopologyBinding(childSpec.tasks.map((task) => task.id)),
434
+ revision: 0,
435
+ committedFacts: inheritedCommittedFacts,
436
+ },
437
+ }
438
+ : {}),
385
439
  frontendRecoveryState: {
440
+ ...(parentState.budgetLedger ? { budgetLineage: { parentLedgerSha256: sha256OfCanonicalJson(parentState.budgetLedger), inheritedAt: createdAt, inheritedConsumed: structuredClone(parentState.budgetLedger.consumed) } } : {}),
386
441
  schemaVersion: 1,
387
442
  phase: "child-running",
388
443
  requestId,
@@ -390,8 +445,11 @@ async function materializeChildStaging(input) {
390
445
  parentRunId,
391
446
  childRunId,
392
447
  attemptId: recovery.attemptId,
393
- attemptIndex: 1,
394
- continuationCount: 1,
448
+ attemptIndex: recovery.attemptIndex + 1,
449
+ continuationCount: recovery.continuationCount + 1,
450
+ ...(recovery.failureSource
451
+ ? { failureSource: recovery.failureSource }
452
+ : {}),
395
453
  revision: 0,
396
454
  },
397
455
  ...(parentState.evaluation
@@ -460,7 +518,41 @@ export async function stageRecoveryChild(options) {
460
518
  const parentRunDir = getDagRunDir(cwd, "active", parentRunId);
461
519
  const parentState = await deps.readParentState(parentRunDir);
462
520
  const parentSpec = await readDagRunSpec(parentRunDir);
463
- const recovery = parentState.frontendRecoveryState;
521
+ let recovery = parentState.frontendRecoveryState;
522
+ if (options.recoveryIntent) {
523
+ const intent = options.recoveryIntent;
524
+ if (intent.parentRunId !== parentRunId ||
525
+ intent.requestId.length === 0 ||
526
+ intent.phase !== "child-staging" ||
527
+ intent.childRunId !== undefined ||
528
+ intent.revision !== 0) {
529
+ throw new Error("frontend recovery intent binding mismatch");
530
+ }
531
+ if (recovery?.requestId === intent.requestId) {
532
+ if (recovery.parentRunId !== intent.parentRunId ||
533
+ recovery.recoveryRootRunId !== intent.recoveryRootRunId ||
534
+ recovery.attemptIndex !== intent.attemptIndex ||
535
+ recovery.continuationCount !== intent.continuationCount ||
536
+ recovery.failureSource !== intent.failureSource) {
537
+ throw new Error("frontend recovery intent replay binding mismatch");
538
+ }
539
+ }
540
+ else {
541
+ const previousAttemptIndex = recovery?.attemptIndex ?? 0;
542
+ const previousContinuationCount = recovery?.continuationCount ?? 0;
543
+ const expectedRoot = recovery?.recoveryRootRunId ?? parentRunId;
544
+ if (intent.recoveryRootRunId !== expectedRoot ||
545
+ intent.attemptIndex !== previousAttemptIndex ||
546
+ intent.continuationCount !== previousContinuationCount) {
547
+ throw new Error("frontend recovery intent lineage mismatch");
548
+ }
549
+ recovery = structuredClone(intent);
550
+ await deps.writeParentState(parentRunDir, {
551
+ ...parentState,
552
+ frontendRecoveryState: recovery,
553
+ });
554
+ }
555
+ }
464
556
  if (!recovery) {
465
557
  throw new Error(`parent run ${parentRunId} has no frontend recovery intent`);
466
558
  }
@@ -468,17 +560,49 @@ export async function stageRecoveryChild(options) {
468
560
  if (!requestId) {
469
561
  throw new Error(`parent run ${parentRunId} recovery intent has no requestId`);
470
562
  }
563
+ if (options.recoveryPlan) {
564
+ if (options.recoveryPlan.parentRunId !== parentRunId ||
565
+ options.recoveryPlan.requestId !== requestId ||
566
+ options.recoveryPlan.failureSource !== recovery.failureSource) {
567
+ throw new Error("frontend recovery plan binding mismatch");
568
+ }
569
+ const expectedPlan = computeFrontendRecoveryPlanForSource({
570
+ spec: parentSpec,
571
+ parentRunId,
572
+ requestId,
573
+ failureSource: recovery.failureSource,
574
+ });
575
+ if (JSON.stringify(expectedPlan.resetNodeIds) !==
576
+ JSON.stringify(options.recoveryPlan.resetNodeIds) ||
577
+ JSON.stringify(expectedPlan.importedNodeIds) !==
578
+ JSON.stringify(options.recoveryPlan.importedNodeIds)) {
579
+ throw new Error("frontend recovery plan drifted from parent DAG");
580
+ }
581
+ }
471
582
  // Idempotent short-circuit (D4.2): reserved childRunId + activated marker +
472
583
  // existing child dir means the request was already fully committed.
473
584
  if (recovery.childRunId) {
474
585
  const existingMarker = await readFrontendRecoveryMarker(parentRunDir, requestId);
475
586
  if (existingMarker) {
476
- const existingChildDir = getDagRunDir(cwd, "active", existingMarker.childRunId);
477
- if (await dagRunDirExists(existingChildDir)) {
587
+ const existingChild = await locateDagRun(cwd, existingMarker.childRunId);
588
+ if (existingChild &&
589
+ (existingChild.lifecycle === "active" ||
590
+ existingChild.lifecycle === "paused" ||
591
+ existingChild.lifecycle === "completed")) {
592
+ await verifyThreeWayConsistency({
593
+ parentRunDir,
594
+ activeChildDir: existingChild.runDir,
595
+ marker: existingMarker,
596
+ parentRunId,
597
+ requestId,
598
+ recoveryRootRunId: recovery.recoveryRootRunId,
599
+ childRunId: existingMarker.childRunId,
600
+ deps,
601
+ });
478
602
  return {
479
603
  ok: true,
480
604
  childRunId: existingMarker.childRunId,
481
- childRunDir: existingChildDir,
605
+ childRunDir: existingChild.runDir,
482
606
  marker: existingMarker,
483
607
  idempotentReplay: true,
484
608
  };
@@ -489,8 +613,13 @@ export async function stageRecoveryChild(options) {
489
613
  // buildDagRunId is deterministic for the same task title on the same day. A
490
614
  // recovery child must never reuse its still-active parent directory, or the
491
615
  // staging path aliases the parent and the required import manifest is absent.
616
+ // The default builder must receive `{ cwd }` (its options object), not the
617
+ // bare string: only the options form enables the collision-safe `-rN`
618
+ // suffix across active/completed/paused runs.
492
619
  const childRunId = recovery.childRunId ?? await (async () => {
493
- const generatedChildRunId = await (options.buildChildRunId ?? buildDagRunId)(parentSpec, cwd);
620
+ const generatedChildRunId = options.buildChildRunId
621
+ ? await options.buildChildRunId(parentSpec, cwd)
622
+ : await buildDagRunId(parentSpec, { cwd });
494
623
  return generatedChildRunId === parentRunId
495
624
  ? `${generatedChildRunId}-recovery-${requestId.slice(0, 8)}`
496
625
  : generatedChildRunId;
@@ -528,6 +657,7 @@ export async function stageRecoveryChild(options) {
528
657
  parentSpec,
529
658
  parentState,
530
659
  recovery,
660
+ recoveryPlan: options.recoveryPlan,
531
661
  requestId,
532
662
  childRunId,
533
663
  stagingDir,
@@ -0,0 +1,139 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { readFile, realpath, lstat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
5
+ import { readCommittedEvents } from "./frontend-typed-event-store.js";
6
+ import { stageTypedEventFact, adoptTypedEventFact } from "./frontend-typed-event-transaction.js";
7
+ const invalidScope = (message) => Object.assign(new Error(`FRONTEND_REVIEW_SCOPE_INVALID: ${message}`), { code: "FRONTEND_REVIEW_SCOPE_INVALID" });
8
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
9
+ const isArtifactReference = (value) => isRecord(value)
10
+ && typeof value.path === "string" && value.path.length > 0
11
+ && typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256);
12
+ const hash = (text) => createHash("sha256").update(text).digest("hex");
13
+ /** The section index is navigation; reconstruct coverage against the canonical source before using it. */
14
+ export async function loadFrontendReviewScopes(runDir, phase, targetBytes) {
15
+ const root = await realpath(runDir);
16
+ const files = new Map();
17
+ async function read(relative, expected) {
18
+ const absolute = path.resolve(root, relative);
19
+ if (!absolute.startsWith(root + path.sep) || (await lstat(absolute)).isSymbolicLink() || !(await realpath(absolute)).startsWith(root + path.sep))
20
+ throw invalidScope("artifact escaped run");
21
+ const text = await readFile(absolute, "utf8");
22
+ const sha256 = hash(text);
23
+ if (expected && sha256 !== expected)
24
+ throw Error(`FRONTEND_REVIEW_SCOPE_DRIFT: ${relative}`);
25
+ files.set(relative, sha256);
26
+ return { id: relative, text, sha256 };
27
+ }
28
+ let indexArtifact;
29
+ try {
30
+ indexArtifact = await read("contracts/frontend-implementation-contract.index.json");
31
+ }
32
+ catch (error) {
33
+ if (error.code === "ENOENT")
34
+ return undefined;
35
+ throw error;
36
+ }
37
+ const index = JSON.parse(indexArtifact.text);
38
+ if (!isRecord(index) || !isArtifactReference(index.contract) || !Array.isArray(index.sections))
39
+ throw invalidScope("malformed contract index");
40
+ const canonicalArtifact = await read(index.contract.path, index.contract.sha256);
41
+ const canonical = JSON.parse(canonicalArtifact.text);
42
+ if (!isRecord(canonical))
43
+ throw invalidScope("canonical contract must be an object");
44
+ const covered = new Map();
45
+ const sections = [];
46
+ const ids = new Set();
47
+ for (const entry of index.sections) {
48
+ if (!isRecord(entry) || !isArtifactReference(entry) || !Array.isArray(entry.slices))
49
+ throw invalidScope("malformed section index entry");
50
+ if (ids.has(entry.path))
51
+ throw invalidScope("duplicate section identity");
52
+ ids.add(entry.path);
53
+ const section = await read(entry.path, entry.sha256);
54
+ const payload = JSON.parse(section.text);
55
+ if (!isRecord(payload) || !Array.isArray(payload.slices) || !isRecord(payload.data))
56
+ throw invalidScope(`malformed section: ${entry.path}`);
57
+ if (payload.contractSha256 !== canonicalArtifact.sha256 || sha256OfCanonicalJson(payload.slices) !== sha256OfCanonicalJson(entry.slices))
58
+ throw invalidScope("unbound section");
59
+ for (const slice of payload.slices) {
60
+ if (!isRecord(slice) || typeof slice.field !== "string" || !Object.hasOwn(canonical, slice.field) || !Object.hasOwn(payload.data, slice.field))
61
+ throw invalidScope(`missing or invalid field reference in ${entry.path}: ${String(slice?.field)}`);
62
+ const value = canonical[slice.field];
63
+ const positions = covered.get(slice.field) ?? new Set();
64
+ if (Array.isArray(value)) {
65
+ if (!Number.isInteger(slice.start) || !Number.isInteger(slice.end) || slice.start < 0 || slice.end < slice.start || slice.end > value.length || slice.total !== value.length)
66
+ throw invalidScope("invalid array coverage");
67
+ if ((value.length === 0 && covered.has(slice.field)) || (value.length > 0 && slice.start === slice.end))
68
+ throw invalidScope(`empty or duplicate array coverage: ${slice.field}`);
69
+ for (let i = slice.start; i < slice.end; i++) {
70
+ if (positions.has(i))
71
+ throw invalidScope("overlapping coverage");
72
+ positions.add(i);
73
+ }
74
+ }
75
+ else {
76
+ if (["start", "end", "total"].some(key => Object.hasOwn(slice, key)))
77
+ throw invalidScope(`scalar field has array range: ${slice.field}`);
78
+ if (positions.size)
79
+ throw invalidScope("duplicate field");
80
+ positions.add(0);
81
+ }
82
+ const range = Array.isArray(value) ? value.slice(slice.start, slice.end) : value;
83
+ if (sha256OfCanonicalJson(range) !== sha256OfCanonicalJson(payload.data[slice.field]))
84
+ throw invalidScope(`section differs from canonical obligation: ${slice.field}`);
85
+ covered.set(slice.field, positions);
86
+ }
87
+ const sectionFields = new Set(payload.slices.map(slice => slice.field));
88
+ if (Object.keys(payload.data).some(field => !sectionFields.has(field)))
89
+ throw invalidScope(`unindexed section data: ${entry.path}`);
90
+ sections.push(section);
91
+ }
92
+ if (Object.keys(canonical).some(field => !covered.has(field) || covered.get(field).size !== (Array.isArray(canonical[field]) ? canonical[field].length : 1)))
93
+ throw invalidScope("canonical field coverage incomplete");
94
+ const scopes = Buffer.byteLength(canonicalArtifact.text) <= targetBytes ? [canonicalArtifact] : sections;
95
+ if (phase === "review") {
96
+ const context = await read("contracts/frontend-review-context.json");
97
+ const payload = JSON.parse(context.text);
98
+ if (payload.contractRef.sha256 !== canonicalArtifact.sha256 || payload.contractRef.indexSha256 !== indexArtifact.sha256)
99
+ throw Error("FRONTEND_REVIEW_SCOPE_DRIFT: review context contract binding");
100
+ const diff = await read(payload.diff.reviewIndexPath);
101
+ const diffIndex = JSON.parse(diff.text);
102
+ await read(payload.diff.patchPath, payload.diff.patchSha256);
103
+ if (diffIndex.patchSha256 !== payload.diff.patchSha256)
104
+ throw Error("FRONTEND_REVIEW_SCOPE_DRIFT: review diff binding");
105
+ scopes.push(context);
106
+ for (const hunk of diffIndex.hunks)
107
+ scopes.push(await read(hunk.patchPath, hunk.sha256));
108
+ }
109
+ return { scopes, digest: sha256OfCanonicalJson([...files]), validate: async () => { for (const [relative, expected] of files)
110
+ await read(relative, expected); } };
111
+ }
112
+ /** Checkpoints attest explicit model completion of already supplied, hash-bound scopes. */
113
+ export function createFrontendReviewScopeProtocol(input) {
114
+ const kind = input.phase === "design" ? "design-scope-completed" : "review-scope-completed";
115
+ let active = new Set();
116
+ const completedScopeIds = () => new Set(readCommittedEvents(input.getStore(), input.attemptId).filter(r => r.fact.kind === kind).map(r => String(r.fact.id)));
117
+ const receipt = (details) => ({ content: [{ type: "text", text: JSON.stringify(details) }], details });
118
+ return {
119
+ setActiveScope(ids) { active = new Set(ids); },
120
+ completedScopeIds,
121
+ committedFacts: () => readCommittedEvents(input.getStore(), input.attemptId),
122
+ assertComplete() { const missing = input.inventory?.scopes.filter(s => !completedScopeIds().has(s.id)) ?? []; if (missing.length)
123
+ throw Object.assign(Error(`FRONTEND_REVIEW_SCOPE_INCOMPLETE: ${missing.map(s => s.id).join(", ")}`), { code: "FRONTEND_REVIEW_SCOPE_INCOMPLETE" }); },
124
+ customTools: !input.inventory ? [] : [{ name: "complete_review_scope", label: "complete_review_scope", description: "After reviewing the supplied complete scope and saving its findings, checkpoint its exact runtime id. This does not approve the implementation or design.", parameters: { type: "object", properties: { id: { type: "string", minLength: 1 } }, required: ["id"], additionalProperties: false },
125
+ async execute(callId, params) {
126
+ const scope = input.inventory.scopes.find(s => s.id === params.id);
127
+ if (!scope || !active.has(params.id))
128
+ return receipt({ ok: false, code: "FRONTEND_INPUT_SCOPE_VIOLATION", error: "Only the supplied active review scope may be completed" });
129
+ await input.inventory.validate();
130
+ const store = input.getStore();
131
+ const requestId = `${input.attemptId}:${callId}:${randomUUID()}`;
132
+ const fact = { kind, id: scope.id, sha256: scope.sha256 };
133
+ const staged = stageTypedEventFact({ store, requestId, attemptId: input.attemptId, fact });
134
+ const committed = await adoptTypedEventFact({ store, requestId, attemptId: input.attemptId, fact, eventId: staged.eventId, expectedRevision: store.revision });
135
+ return receipt({ ok: true, kind, revision: committed.revision });
136
+ },
137
+ }],
138
+ };
139
+ }
@@ -70,8 +70,10 @@ function blobOf(input) {
70
70
  ].join("\n");
71
71
  }
72
72
  function pathSpreadScore(paths) {
73
+ const auxiliaryRoot = /^(?:test|tests|__tests__|e2e|cypress|spec|specs|docs|documentation)$/i;
73
74
  const tops = new Set(paths
74
75
  .map((p) => p.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
76
+ .filter((root) => !auxiliaryRoot.test(root))
75
77
  .filter(Boolean));
76
78
  return tops.size;
77
79
  }