@tea-agent/loop-agent 0.41.1 → 0.42.0-next.10

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 (302) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +397 -5
  3. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  4. package/dist/adapters/context-transfer/pi-session.js +61 -0
  5. package/dist/application/dag/generate-task-dag.js +75 -19
  6. package/dist/application/dag/run-dag.js +41 -0
  7. package/dist/application/task-lifecycle/advance.js +24 -5
  8. package/dist/application/task-lifecycle/observe.js +171 -17
  9. package/dist/application/task-lifecycle/plan-transitions.js +42 -7
  10. package/dist/application/task-lifecycle/recommendations.js +13 -2
  11. package/dist/build-stamp.json +3 -3
  12. package/dist/cli/command-definitions.js +19 -0
  13. package/dist/cli/program.js +29 -2
  14. package/dist/commands/client-recovery.js +3 -0
  15. package/dist/commands/dag-artifact.js +284 -0
  16. package/dist/commands/dag-context.js +184 -0
  17. package/dist/commands/dag-follow-up.js +138 -0
  18. package/dist/commands/dag-rerun-task.js +2 -0
  19. package/dist/commands/dag-rerun.js +206 -1
  20. package/dist/commands/init.js +27 -1
  21. package/dist/commands/task-advance.js +19 -0
  22. package/dist/executors/dag-pi-executor.js +4013 -86
  23. package/dist/executors/pi-executor.js +15 -4
  24. package/dist/executors/pi-read-budget-policy.js +239 -0
  25. package/dist/executors/pi-sdk-executor.js +3 -0
  26. package/dist/executors/shell-executor.js +978 -188
  27. package/dist/executors/shell-write-guard.js +7 -0
  28. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  29. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  30. package/dist/infrastructure/console/context-export-store.js +160 -0
  31. package/dist/infrastructure/console/dir-lock.js +132 -0
  32. package/dist/infrastructure/console/operation-store.js +417 -0
  33. package/dist/infrastructure/harness/artifact-store.js +10 -1
  34. package/dist/infrastructure/harness/atomic-write.js +12 -2
  35. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  36. package/dist/shared/context-transfer.js +418 -0
  37. package/dist/shared/dag-failure-category.js +12 -0
  38. package/dist/shared/openspec-spec.js +70 -4
  39. package/dist/shared/operator/capabilities.js +21 -0
  40. package/dist/shared/operator/safe-run-summary.js +1 -0
  41. package/dist/shared/path-safety.js +93 -0
  42. package/dist/shared/preview.js +28 -4
  43. package/dist/task/config-types.js +107 -6
  44. package/dist/task/contract/adopt.js +4 -0
  45. package/dist/task/contract/import-revision.js +4 -0
  46. package/dist/task/contract/project.js +3 -0
  47. package/dist/task/contract/schema.js +2 -1
  48. package/dist/task/frontend-project-capability.js +203 -20
  49. package/dist/task/runtime.js +5 -2
  50. package/dist/task/source-prepare/build-draft.js +3 -3
  51. package/dist/task/source-prepare/fragment-inventory.js +64 -25
  52. package/dist/task/source-prepare/prepare.js +126 -21
  53. package/dist/task/source-prepare/semantic-intake.js +6 -2
  54. package/dist/task/source-references.js +22 -1
  55. package/dist/task/task-demand-routing.js +3 -0
  56. package/dist/worker/console/chat/chat-event-store.js +2 -2
  57. package/dist/worker/console/chat/model-resolver.js +7 -4
  58. package/dist/worker/console/chat/pi-runtime.js +86 -5
  59. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  60. package/dist/worker/console/chat/routes.js +136 -53
  61. package/dist/worker/console/chat/sdd-data-alignment.js +222 -0
  62. package/dist/worker/console/chat/session-catalog.js +2 -0
  63. package/dist/worker/console/chat/session-store.js +43 -1
  64. package/dist/worker/console/chat/shortcuts.js +18 -2
  65. package/dist/worker/console/chat/user-questions.js +1 -1
  66. package/dist/worker/console/console-update-runtime.js +1 -1
  67. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  68. package/dist/worker/console/dag-confirmation.js +1 -1
  69. package/dist/worker/console/dag-execution-receipt.js +1 -1
  70. package/dist/worker/console/doctor.js +1 -1
  71. package/dist/worker/console/draft-store.js +1 -1
  72. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  73. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  74. package/dist/worker/console/human-gate-token.js +1 -1
  75. package/dist/worker/console/index.js +6 -3
  76. package/dist/worker/console/interview/assessment.js +1 -1
  77. package/dist/worker/console/interview/session.js +1 -1
  78. package/dist/worker/console/operation-runner.js +45 -4
  79. package/dist/worker/console/operation-sse.js +1 -1
  80. package/dist/worker/console/operation-wait.js +1 -1
  81. package/dist/worker/console/operator-actions.js +273 -10
  82. package/dist/worker/console/operator-surface-health.js +1 -1
  83. package/dist/worker/console/operator-user-error.js +169 -0
  84. package/dist/worker/console/pi-plugins.js +57 -0
  85. package/dist/worker/console/pi-readiness.js +3 -2
  86. package/dist/worker/console/routes.js +637 -8
  87. package/dist/worker/console/security.js +35 -0
  88. package/dist/worker/console/server.js +29 -4
  89. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-D7oU983t.js → abnfDiagram-N423BO3Z-CcS17TBr.js} +1 -1
  90. package/dist/worker/console/static/assets/{arc-BrKSRdFS.js → arc-COptKq2S.js} +1 -1
  91. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CiXV7L56.js → architectureDiagram-T3A2C74G-h4LKMHjP.js} +1 -1
  92. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D_Hs_gIG.js → blockDiagram-VBNYF7ZC-COA1MOH0.js} +1 -1
  93. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DbM-Lqqh.js → c4Diagram-5PPSVZJV-DJUf0QPm.js} +1 -1
  94. package/dist/worker/console/static/assets/channel-DdBCaOJ6.js +1 -0
  95. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D1M8uqJw.js → chunk-2GRJ4B5K-mtWfKrUX.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-4D3RFHT6.js → chunk-2Q5K7J3B-B8pXsxDQ.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CdwpY_n9.js → chunk-5RXB4S5H-ipKzByl1.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BLeLZ9PG.js → chunk-5VM5RSS4-DYi3Ald_.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CeRNEMvc.js → chunk-6Q2QTUOP-DQtGYoty.js} +1 -1
  100. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjkHP8Vy.js → chunk-GF5L2VYU-BU0qS2YV.js} +1 -1
  101. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-B54ZUQY-.js → chunk-JWPE2WC7-DVH9dIGE.js} +1 -1
  102. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-4bKaMVmx.js → chunk-KBJHAD2P-Dt60SmgM.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Cyjfznu_.js → chunk-RYQCIY6F-BoaUGuEY.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CDN-zGFV.js → chunk-XXDRQBXY-_WHFDTp2.js} +1 -1
  105. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DZFra1GO.js +1 -0
  106. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DZFra1GO.js +1 -0
  107. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-HxJmz411.js → cose-bilkent-JH36ORCC-BxkbTIRd.js} +1 -1
  108. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DtSt5K2w.js → cynefin-VYW2F7L2-Bf2UVnoG.js} +1 -1
  109. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-VtkSpRmG.js → cynefinDiagram-MW4NZA55-Bo23q1J_.js} +1 -1
  110. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bj4DT4du.js → dagre-VZM6K2ZE-Dd_UU49i.js} +1 -1
  111. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Bpuk6BmP.js → diagram-7IWD3JNH-ebUa1a9y.js} +1 -1
  112. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BHYGv_yO.js → diagram-B4RE2ZJO-BucthU8r.js} +1 -1
  113. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DEx3WwnT.js → diagram-LBJQPF4R-BgqNpAkm.js} +1 -1
  114. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cj0dS5e8.js → diagram-Q27KOJAE-DC4q5NGa.js} +1 -1
  115. package/dist/worker/console/static/assets/{diagram-UB23O5K3-B97h8l21.js → diagram-UB23O5K3-CBqCaMeb.js} +1 -1
  116. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DMSFAhBd.js → ebnfDiagram-BXEA7PRR-CGtnbQ3-.js} +1 -1
  117. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DhVOCbt6.js → erDiagram-JOGREHBK-CG3LUao5.js} +1 -1
  118. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-GlakITWL.js → flowDiagram-UKHOOZJN-D2ZVgoFS.js} +1 -1
  119. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-VxPpu9eQ.js → ganttDiagram-PKOTCBZU-DoYFZiKt.js} +1 -1
  120. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bs_e5DRT.js → gitGraphDiagram-DS77QQ5N-CKoP1s6j.js} +1 -1
  121. package/dist/worker/console/static/assets/index-CAZ2fC_X.css +1 -0
  122. package/dist/worker/console/static/assets/index-vbTcFnFs.js +449 -0
  123. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BXYuU4Uw.js → infoDiagram-6WML65LV-Duofv8p2.js} +1 -1
  124. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C34erCbU.js → ishikawaDiagram-WSZJBQD7-D2nlCkA1.js} +1 -1
  125. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DZCAiOU3.js → journeyDiagram-NVQOT4AX-Dd4IHdus.js} +1 -1
  126. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C9gaTdUm.js → kanban-definition-27J2QSJJ-Bi20AOdt.js} +1 -1
  127. package/dist/worker/console/static/assets/{linear-CvI0Z17U.js → linear-D59YJ9kB.js} +1 -1
  128. package/dist/worker/console/static/assets/{mermaid.core-DMMSIx7j.js → mermaid.core-Bl13LpNa.js} +5 -5
  129. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BKKP0EO6.js → mindmap-definition-FAOFIHXS-C6DLP5eY.js} +1 -1
  130. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-C3LiFbl5.js → pegDiagram-VL7TDLO6-BjO4cy64.js} +1 -1
  131. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Bb3Om2Dz.js → pieDiagram-7S7Q4E2Y-D7i2qZTG.js} +1 -1
  132. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DNthMjzc.js → quadrantDiagram-CIZ2JOQS-Dx8o_h0y.js} +1 -1
  133. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-miFsr4Tv.js → railroadDiagram-AXF67PYL-B55orI_t.js} +1 -1
  134. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-yXUncmn6.js → requirementDiagram-LRYGKXZP-zWaehp6f.js} +1 -1
  135. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DLeWKLak.js → sankeyDiagram-W5VNT64P-CcA-pjvD.js} +1 -1
  136. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CI3he1R3.js → sequenceDiagram-SI44F4Z6-BOFbzNHI.js} +1 -1
  137. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B7jfDkS5.js → sizeCapture-X5ZJPWSS-BjAejah1.js} +1 -1
  138. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BmrZod7J.js → stateDiagram-OKZ733FA-DXYwxJwZ.js} +1 -1
  139. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Clg3V9t1.js +1 -0
  140. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CDOlHVSY.js → swimlanes-SLNWSIFB-OYiOch8n.js} +2 -2
  141. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DX1dxAAW.js +8 -0
  142. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cz-XX1gy.js → timeline-definition-Z64GVDOM-cZfH3nmU.js} +1 -1
  143. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-caV4cI8C.js → vennDiagram-T6HMQDX7-BDE7b3E1.js} +1 -1
  144. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-dImPlnIO.js → wardleyDiagram-T6FBY63Y-kyyGy9WJ.js} +1 -1
  145. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CtRYVJcn.js → xychartDiagram-ELKLHX3M-CJj6VTog.js} +1 -1
  146. package/dist/worker/console/static/index.html +7 -2
  147. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  148. package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
  149. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
  150. package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
  151. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
  152. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +258 -23
  153. package/dist/worker/console/static-src/operator-chat/useChatThread.js +112 -4
  154. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
  155. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
  156. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
  157. package/dist/worker/console/static-src/shell/console-update-reload.js +52 -0
  158. package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
  159. package/dist/worker/console/workspace-context.js +142 -2
  160. package/dist/worker/console/workspace-registry.js +2 -2
  161. package/dist/worker/continuation/worker-continuation.js +278 -0
  162. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  163. package/dist/worker/observe/health.js +1 -1
  164. package/dist/worker/observe/node-transparency.js +572 -0
  165. package/dist/worker/observe/routes.js +55 -1
  166. package/dist/worker/observe/static/api.js +69 -5
  167. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  168. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  169. package/dist/worker/observe/static/dag-helpers.js +4 -2
  170. package/dist/worker/observe/static/dag-node-purpose.js +5 -0
  171. package/dist/worker/observe/static/inspect-workspace.js +23 -0
  172. package/dist/worker/observe/static/kpi.js +2 -2
  173. package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
  174. package/dist/worker/observe/static/operator-chrome.js +37 -23
  175. package/dist/worker/observe/static/relations.js +2 -2
  176. package/dist/worker/observe/static/router.d.ts +12 -1
  177. package/dist/worker/observe/static/router.js +48 -6
  178. package/dist/worker/observe/static/run-processing.js +4 -2
  179. package/dist/worker/observe/static/shell-chrome.js +2 -2
  180. package/dist/worker/observe/static/state.js +8 -1
  181. package/dist/worker/observe/static/styles.css +1092 -86
  182. package/dist/worker/observe/static/views/dag-inspector.js +1228 -64
  183. package/dist/worker/observe/static/views/dag.js +10 -2
  184. package/dist/worker/observe/static/views/dags.js +3 -1
  185. package/dist/worker/observe/static/views/dashboard.js +14 -6
  186. package/dist/worker/observe/static/views/pool.js +7 -2
  187. package/dist/worker/observe/static/views/run.js +12 -3
  188. package/dist/worker/observe/static/views/session-timeline.js +142 -18
  189. package/dist/worker/observe/static/views/task.js +7 -2
  190. package/dist/worker/outcomes/adapters.js +19 -0
  191. package/dist/worker/outcomes/types.js +1 -0
  192. package/dist/worker/pool/attempt-lease.js +97 -66
  193. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  194. package/dist/worker/pool/reconcile.js +46 -1
  195. package/dist/worker/pool/run-store.js +2 -0
  196. package/dist/worker/pool/state-projection.js +2 -0
  197. package/dist/worker/task-spec/workflow-routing.js +8 -3
  198. package/dist/workflows/dag/artifact-bindings.js +149 -0
  199. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  200. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +215 -28
  201. package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
  202. package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
  203. package/dist/workflows/dag/backend-test-scenario-param.js +172 -25
  204. package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
  205. package/dist/workflows/dag/backend-test-writer-completeness.js +104 -12
  206. package/dist/workflows/dag/budget-enforcement.js +10 -0
  207. package/dist/workflows/dag/context-receipt.js +305 -0
  208. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  209. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  210. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  211. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  212. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  213. package/dist/workflows/dag/frontend-closeout.js +221 -0
  214. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  215. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  216. package/dist/workflows/dag/frontend-implementation-contract.js +1237 -192
  217. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  218. package/dist/workflows/dag/frontend-prewrite-gate.js +256 -350
  219. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  220. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  221. package/dist/workflows/dag/frontend-recovery-controller.js +226 -0
  222. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  223. package/dist/workflows/dag/frontend-recovery-plan.js +21 -10
  224. package/dist/workflows/dag/frontend-recovery-run.js +166 -34
  225. package/dist/workflows/dag/frontend-repair.js +1 -432
  226. package/dist/workflows/dag/frontend-review-context.js +261 -15
  227. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  228. package/dist/workflows/dag/frontend-shadow-dual-write.js +941 -0
  229. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  230. package/dist/workflows/dag/frontend-shape-facts.js +419 -0
  231. package/dist/workflows/dag/frontend-shape.js +435 -0
  232. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  233. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  234. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  235. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  236. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  237. package/dist/workflows/dag/frontend-verification-trace.js +252 -25
  238. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  239. package/dist/workflows/dag/frontend-writer-admission.js +319 -0
  240. package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
  241. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  242. package/dist/workflows/dag/init-hybrid.js +1174 -562
  243. package/dist/workflows/dag/interrupt-request.js +7 -0
  244. package/dist/workflows/dag/node-execution.js +940 -31
  245. package/dist/workflows/dag/path-safety.js +1 -0
  246. package/dist/workflows/dag/prompt.js +99 -7
  247. package/dist/workflows/dag/recovery-lease.js +170 -0
  248. package/dist/workflows/dag/report.js +37 -1
  249. package/dist/workflows/dag/rerun-feedback.js +315 -1
  250. package/dist/workflows/dag/rerun-plan.js +568 -3
  251. package/dist/workflows/dag/rerun-run.js +642 -18
  252. package/dist/workflows/dag/rerun-task.js +251 -13
  253. package/dist/workflows/dag/retry-policy.js +219 -104
  254. package/dist/workflows/dag/runner.js +596 -121
  255. package/dist/workflows/dag/scheduler.js +133 -20
  256. package/dist/workflows/dag/skill-snapshot.js +17 -0
  257. package/dist/workflows/dag/types.js +426 -22
  258. package/dist/workflows/dag/validate.js +28 -12
  259. package/docs/architecture/runtime-boundaries.md +6 -6
  260. package/docs/architecture/worker-and-feature.md +1 -1
  261. package/docs/examples/README.md +5 -0
  262. package/docs/init-surface.manifest.json +30 -12
  263. package/docs/skills/vetted-skill-registry.md +4 -2
  264. package/docs/templates/README.md +2 -0
  265. package/docs/templates/agent-dag-report.schema.json +8 -2
  266. package/docs/templates/agent-dag.schema.json +33 -1
  267. package/docs/templates/backend-test-dag.json +23 -20
  268. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  269. package/docs/templates/frontend-implementation-dag.json +89 -0
  270. package/docs/templates/product-line/task.yaml +1 -1
  271. package/docs/templates/spec-registry.schema.json +45 -0
  272. package/harness.json +1 -1
  273. package/package.json +6 -3
  274. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  275. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  276. package/skills/frontend-contract/SKILL.md +23 -0
  277. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  278. package/skills/frontend-design-review/SKILL.md +22 -41
  279. package/skills/frontend-plan/SKILL.md +26 -0
  280. package/skills/frontend-plan/references/decision-contract.md +37 -0
  281. package/skills/frontend-plan/references/design-decisions.md +17 -0
  282. package/skills/frontend-review/SKILL.md +20 -15
  283. package/skills/frontend-review/references/review-findings.md +6 -7
  284. package/skills/frontend-scout/SKILL.md +25 -0
  285. package/skills/frontend-scout/references/design-evidence.md +16 -0
  286. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  287. package/skills/frontend-verification/SKILL.md +1 -1
  288. package/skills/loop-agent/references/command-reference.md +7 -0
  289. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  290. package/dist/worker/console/operation-store.js +0 -169
  291. package/dist/worker/console/static/assets/channel-D1xajGpe.js +0 -1
  292. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BmuGTfRr.js +0 -1
  293. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BmuGTfRr.js +0 -1
  294. package/dist/worker/console/static/assets/index-BLzcAykx.js +0 -451
  295. package/dist/worker/console/static/assets/index-Cs9JUSPs.css +0 -1
  296. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-D9ykKEWP.js +0 -1
  297. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BvPOXALT.js +0 -8
  298. package/skills/frontend-implementation/SKILL.md +0 -52
  299. package/skills/frontend-implementation/references/code-standards.md +0 -33
  300. package/skills/frontend-implementation/references/design-spec.md +0 -56
  301. package/skills/frontend-implementation/references/node-contracts.md +0 -31
  302. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -0,0 +1,132 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { lstat, mkdir, readFile, rename, rm, utimes, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { ensureSecureDir } from "./app-data.js";
5
+ const OWNER_FILE = "owner";
6
+ const DEFAULT_STALE_MS = 30_000;
7
+ function delay(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+ function freshToken() {
11
+ return randomBytes(16).toString("hex");
12
+ }
13
+ function ownerPath(lockDir) {
14
+ return path.join(lockDir, OWNER_FILE);
15
+ }
16
+ /**
17
+ * A lock's freshness is the owner-file heartbeat, not the containing
18
+ * directory mtime. Creating or editing a file does not refresh the directory
19
+ * mtime, so using the latter could make a healthy long-running lock appear
20
+ * stale and permit a second holder into its critical section.
21
+ */
22
+ async function lockTimestamp(lockDir) {
23
+ const ownerStat = await lstat(ownerPath(lockDir)).catch(() => undefined);
24
+ if (ownerStat?.isFile())
25
+ return ownerStat.mtimeMs;
26
+ return (await lstat(lockDir).catch(() => undefined))?.mtimeMs;
27
+ }
28
+ export async function isDirLockStale(lockDir, staleMs, nowMs = Date.now()) {
29
+ const timestamp = await lockTimestamp(lockDir);
30
+ return timestamp === undefined || nowMs - timestamp > staleMs;
31
+ }
32
+ async function refreshDirLock(lockDir, token) {
33
+ const owner = await readFile(ownerPath(lockDir), "utf-8").catch(() => undefined);
34
+ if (owner !== token)
35
+ return;
36
+ const now = new Date();
37
+ await utimes(ownerPath(lockDir), now, now).catch(() => { });
38
+ }
39
+ /**
40
+ * Cross-process exclusive directory lock (mkdir-based). Ownership is
41
+ * token-guarded: each holder writes a unique owner file inside the lock
42
+ * directory and only ever removes a lock whose owner token matches its own.
43
+ *
44
+ * Release protocol (B2): verify owner AND freshness BEFORE touching the lock
45
+ * path. Takeover only ever targets a stale lock, so a holder that releases a
46
+ * lock it still owns (fresh) cannot have been taken over between the verify
47
+ * and the rename — it moves its own lock, never a successor's. A stale lock is
48
+ * deliberately left alone (a takeover may be in flight); only the post-rename
49
+ * tombstone check restores a lock moved by an unexpected race, never deletes a
50
+ * successor's live lock.
51
+ */
52
+ export async function acquireDirLock(lockDir, options) {
53
+ const token = options?.token ?? freshToken();
54
+ const staleMs = options?.staleMs ?? DEFAULT_STALE_MS;
55
+ ensureSecureDir(path.dirname(lockDir));
56
+ for (;;) {
57
+ try {
58
+ await mkdir(lockDir);
59
+ }
60
+ catch (error) {
61
+ const code = error?.code;
62
+ if (code !== "EEXIST")
63
+ throw error;
64
+ const stat = await lstat(lockDir).catch(() => undefined);
65
+ if (stat?.isDirectory() && await isDirLockStale(lockDir, staleMs)) {
66
+ const tombstone = `${lockDir}.${freshToken()}.tomb`;
67
+ try {
68
+ await rename(lockDir, tombstone);
69
+ await rm(tombstone, { recursive: true, force: true });
70
+ }
71
+ catch {
72
+ // another process won the takeover; retry the loop
73
+ }
74
+ continue;
75
+ }
76
+ await delay(20);
77
+ continue;
78
+ }
79
+ try {
80
+ await writeFile(ownerPath(lockDir), token, {
81
+ flag: "wx",
82
+ mode: 0o600,
83
+ });
84
+ }
85
+ catch {
86
+ await rm(lockDir, { recursive: true, force: true });
87
+ continue;
88
+ }
89
+ break;
90
+ }
91
+ const heartbeat = setInterval(() => { void refreshDirLock(lockDir, token); }, Math.max(10, Math.floor(staleMs / 3)));
92
+ heartbeat.unref();
93
+ let released = false;
94
+ const release = async () => {
95
+ if (released)
96
+ return;
97
+ released = true;
98
+ clearInterval(heartbeat);
99
+ // Verify the lock is still ours AND fresh before renaming it away. If the
100
+ // owner no longer matches, a successor holds the lock — never touch it.
101
+ const stat = await lstat(lockDir).catch(() => undefined);
102
+ if (!stat?.isDirectory())
103
+ return;
104
+ if (await isDirLockStale(lockDir, staleMs)) {
105
+ // Our lock went stale: a takeover may be in flight or imminent. Leave
106
+ // it alone — removing it now could delete a successor's live lock.
107
+ return;
108
+ }
109
+ const owner = await readFile(ownerPath(lockDir), "utf-8").catch(() => undefined);
110
+ if (owner !== token)
111
+ return;
112
+ const tombstone = `${lockDir}.${token}.release`;
113
+ try {
114
+ await rename(lockDir, tombstone);
115
+ }
116
+ catch {
117
+ // lock path already gone (a takeover removed it); nothing of ours.
118
+ return;
119
+ }
120
+ const movedOwner = await readFile(path.join(tombstone, OWNER_FILE), "utf-8").catch(() => undefined);
121
+ if (movedOwner !== token) {
122
+ // Raced with a takeover in the microsecond window between the fresh
123
+ // verify and the rename: restore the moved lock if the path is still
124
+ // free; otherwise leave the tombstone for reconcile — never delete a
125
+ // successor's live lock.
126
+ await rename(tombstone, lockDir).catch(() => { });
127
+ return;
128
+ }
129
+ await rm(tombstone, { recursive: true, force: true }).catch(() => { });
130
+ };
131
+ return { token, release };
132
+ }
@@ -0,0 +1,417 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import path from "node:path";
3
+ import { assertFrontendSplitProjectionSuccessor, frontendSplitProjectionSchema, } from "../../workflows/dag/frontend-split-application-service.js";
4
+ import { buildFrontendHumanDecisionProjection, consumeFrontendHumanDecision, materializeFrontendHumanDecision, parseFrontendHumanDecisionProjection, parseFrontendHumanDecisionReceipt, } from "../../workflows/dag/frontend-human-decision.js";
5
+ import { buildFrontendSplitAggregateCloseout, computeFrontendSplitDigest, finalizeFrontendSplitAggregate, frontendCommittedFactRefSchema, frontendSplitChildCloseoutFactsSchema, initializeFrontendSplitParent, } from "../../workflows/dag/frontend-split-orchestrator.js";
6
+ import { frontendCloseoutFactsSchema, frontendTestHandoffSchema, } from "../../workflows/dag/frontend-closeout.js";
7
+ import { listJsonFiles, readJsonIfExists, sha256Json, writeSecureJson, } from "./app-data.js";
8
+ export const TERMINAL_OPERATION_STATES = new Set(["succeeded", "failed", "timed-out", "restarting"]);
9
+ function operationFile(appData, operationId) {
10
+ return path.join(appData.operations, `${operationId}.json`);
11
+ }
12
+ function requestIndexFile(appData, clientRequestId) {
13
+ const safe = clientRequestId.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 180);
14
+ return path.join(appData.operations, `req-${safe}.json`);
15
+ }
16
+ export function computeActionPayloadHash(action, actionParams) {
17
+ return sha256Json({ action, actionParams });
18
+ }
19
+ export function newOperationId() {
20
+ return `op_${randomBytes(12).toString("hex")}`;
21
+ }
22
+ const sharedWriteChains = new Map();
23
+ export class OperationStore {
24
+ appData;
25
+ /** Shared per-store-root, per-key write lock. Multiple OperationStore instances
26
+ * in one worker process must observe the same CAS critical section. */
27
+ writeChains;
28
+ constructor(appData) {
29
+ this.appData = appData;
30
+ const root = path.resolve(appData.operations);
31
+ let chains = sharedWriteChains.get(root);
32
+ if (!chains) {
33
+ chains = new Map();
34
+ sharedWriteChains.set(root, chains);
35
+ }
36
+ this.writeChains = chains;
37
+ }
38
+ serialize(key, op) {
39
+ const prev = this.writeChains.get(key) ?? Promise.resolve();
40
+ const next = prev.then(op, op);
41
+ this.writeChains.set(key, next.then(() => undefined, () => undefined));
42
+ return next;
43
+ }
44
+ async create(input) {
45
+ const clientRequestId = input.clientRequestId.trim();
46
+ if (!clientRequestId) {
47
+ throw new Error("clientRequestId is required for mutation operations");
48
+ }
49
+ const payloadHash = computeActionPayloadHash(input.action, input.actionParams);
50
+ return this.serialize(`req:${clientRequestId}`, async () => {
51
+ const indexPath = requestIndexFile(this.appData, clientRequestId);
52
+ const existingIndex = await readJsonIfExists(indexPath);
53
+ if (existingIndex) {
54
+ const existing = await this.get(existingIndex.operationId);
55
+ if (existing) {
56
+ if (existing.payloadHash === payloadHash) {
57
+ return { kind: "idempotent", operation: existing };
58
+ }
59
+ return {
60
+ kind: "conflict",
61
+ code: "REQUEST_ID_REUSE_CONFLICT",
62
+ message: `clientRequestId ${clientRequestId} was reused with a different payload`,
63
+ existing,
64
+ };
65
+ }
66
+ }
67
+ const operationId = newOperationId();
68
+ const now = new Date().toISOString();
69
+ const operation = {
70
+ schemaVersion: 1,
71
+ operationId,
72
+ clientRequestId,
73
+ action: input.action,
74
+ actionParams: input.actionParams,
75
+ payloadHash,
76
+ state: "queued",
77
+ createdAt: now,
78
+ revision: 1,
79
+ taskId: input.taskId,
80
+ cliArgs: input.cliArgs,
81
+ ...(input.externalCommand
82
+ ? { externalCommand: input.externalCommand }
83
+ : {}),
84
+ };
85
+ await writeSecureJson(operationFile(this.appData, operationId), operation);
86
+ await writeSecureJson(indexPath, { operationId, payloadHash });
87
+ return { kind: "created", operation };
88
+ });
89
+ }
90
+ /**
91
+ * Idempotently materialize a split child/integration operation with a
92
+ * caller-specified deterministic operationId (from the split manifest).
93
+ * Unlike create() (random op_<hex> id) and save/update (no create), this is
94
+ * the durable cross-restart idempotency boundary for the production
95
+ * FrontendSplitTaskMaterializer backend: an existing operation file with the
96
+ * same taskId + payloadHash is returned as {created:false}; any drift fails
97
+ * closed instead of overwriting or silently diverging.
98
+ */
99
+ async ensureSplitMaterializedOperation(input) {
100
+ return this.serialize(input.operationId, async () => {
101
+ const existing = await this.get(input.operationId);
102
+ if (existing) {
103
+ if (existing.taskId !== input.taskId ||
104
+ existing.payloadHash !== input.payloadHash) {
105
+ throw new Error("frontend split operation materialization payload drift");
106
+ }
107
+ return { operation: existing, created: false };
108
+ }
109
+ const now = new Date().toISOString();
110
+ const operation = {
111
+ schemaVersion: 1,
112
+ operationId: input.operationId,
113
+ clientRequestId: `split:${input.operationId}`,
114
+ action: input.action,
115
+ actionParams: input.actionParams,
116
+ payloadHash: input.payloadHash,
117
+ state: "queued",
118
+ createdAt: now,
119
+ revision: 1,
120
+ taskId: input.taskId,
121
+ ...(input.cliArgs ? { cliArgs: input.cliArgs } : {}),
122
+ };
123
+ await writeSecureJson(operationFile(this.appData, input.operationId), operation);
124
+ return { operation, created: true };
125
+ });
126
+ }
127
+ async get(operationId) {
128
+ return readJsonIfExists(operationFile(this.appData, operationId));
129
+ }
130
+ async save(operation) {
131
+ return this.serialize(operation.operationId, async () => {
132
+ const current = await this.get(operation.operationId);
133
+ if (current?.revision !== undefined && operation.revision !== current.revision)
134
+ throw new Error("operation revision may not be changed by save");
135
+ if (operation.frontendHumanDecision !== undefined || current?.frontendHumanDecision !== undefined) {
136
+ if (!current || JSON.stringify(operation) !== JSON.stringify(current))
137
+ throw new Error("human-decision projection may only be mutated by canonical CAS");
138
+ return current;
139
+ }
140
+ if (operation.frontendSplit !== undefined || current?.frontendSplit !== undefined) {
141
+ if (!current || JSON.stringify(operation) !== JSON.stringify(current))
142
+ throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
143
+ return current;
144
+ }
145
+ await writeSecureJson(operationFile(this.appData, operation.operationId), operation);
146
+ return operation;
147
+ });
148
+ }
149
+ async update(operationId, patch) {
150
+ return this.serialize(operationId, async () => {
151
+ const current = await this.get(operationId);
152
+ if (!current) {
153
+ throw new Error(`operation not found: ${operationId}`);
154
+ }
155
+ if ("frontendHumanDecision" in patch || (current.frontendHumanDecision !== undefined && ("state" in patch || "frontendExecution" in patch)))
156
+ throw new Error("human-decision projection may only be mutated by canonical CAS");
157
+ if ("frontendSplit" in patch || "revision" in patch || (current.frontendSplit !== undefined && ("state" in patch || "frontendExecution" in patch)))
158
+ throw new Error("split projection may only be mutated by compareAndSwapFrontendSplit");
159
+ const next = {
160
+ ...current,
161
+ ...patch,
162
+ operationId: current.operationId,
163
+ schemaVersion: 1,
164
+ clientRequestId: current.clientRequestId,
165
+ payloadHash: current.payloadHash,
166
+ action: current.action,
167
+ createdAt: current.createdAt,
168
+ };
169
+ await writeSecureJson(operationFile(this.appData, operationId), next);
170
+ return next;
171
+ });
172
+ }
173
+ async materializeFrontendHumanDecision(input) {
174
+ return this.serialize(input.operationId, async () => {
175
+ const current = await this.get(input.operationId);
176
+ if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision) {
177
+ const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.envelope.runId, revision: Math.max(1, input.expectedRecordRevision) });
178
+ return { outcome: "rejected", reason: "record revision conflict or legacy record", projection: fallback };
179
+ }
180
+ const projection = current.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: current.operationId, runId: input.envelope.runId, revision: current.revision });
181
+ const result = materializeFrontendHumanDecision({ projection, envelope: input.envelope, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest });
182
+ if (result.outcome !== "applied")
183
+ return result;
184
+ const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection, state: "running" };
185
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
186
+ return result;
187
+ });
188
+ }
189
+ async consumeFrontendHumanDecision(input) {
190
+ return this.serialize(input.operationId, async () => {
191
+ const current = await this.get(input.operationId);
192
+ if (!current || current.revision === undefined || !current.frontendHumanDecision) {
193
+ const fallback = buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
194
+ return { outcome: "rejected", reason: "record has no mutable human-decision projection", projection: fallback };
195
+ }
196
+ const projection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
197
+ if (current.revision !== input.expectedRecordRevision)
198
+ return { outcome: "rejected", reason: "record revision conflict", projection };
199
+ const result = consumeFrontendHumanDecision({ projection, receipt: input.receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
200
+ if (result.outcome !== "applied")
201
+ return result;
202
+ const record = { ...current, revision: result.projection.revision, frontendHumanDecision: result.projection };
203
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
204
+ return result;
205
+ });
206
+ }
207
+ async initializeFrontendHumanApprovedSplit(input) {
208
+ return this.serialize(input.operationId, async () => {
209
+ const current = await this.get(input.operationId);
210
+ const fallback = current?.frontendHumanDecision ?? buildFrontendHumanDecisionProjection({ status: "active", operationId: input.operationId, runId: input.receipt.runId, revision: Math.max(1, input.expectedRecordRevision) });
211
+ if (!current || current.revision === undefined || current.revision !== input.expectedRecordRevision || !current.frontendHumanDecision || !current.frontendExecution || current.frontendSplit)
212
+ return { outcome: "rejected", reason: "record is not an authoritative pending single-task split", projection: fallback };
213
+ const humanProjection = parseFrontendHumanDecisionProjection(current.frontendHumanDecision);
214
+ if (humanProjection.status !== "blocked-human" || humanProjection.envelope.kind !== "split-materialization-approval" || humanProjection.digest !== input.expectedProjectionDigest)
215
+ return { outcome: "rejected", reason: "pending split envelope conflict", projection: humanProjection };
216
+ const receipt = parseFrontendHumanDecisionReceipt(input.receipt);
217
+ if (receipt.kind !== "split-materialization-approval")
218
+ return { outcome: "rejected", reason: "receipt is not a split approval", projection: humanProjection };
219
+ if (JSON.stringify(current.frontendExecution) !== JSON.stringify(input.execution) || input.execution.kind !== "single-task")
220
+ return { outcome: "rejected", reason: "single-task execution conflict", projection: humanProjection };
221
+ const consumed = consumeFrontendHumanDecision({ projection: humanProjection, receipt, expectedRecordRevision: input.expectedRecordRevision, expectedProjectionDigest: input.expectedProjectionDigest, currentTargetDigest: input.currentTargetDigest, now: input.now });
222
+ if (consumed.outcome !== "applied")
223
+ return consumed;
224
+ const initialized = initializeFrontendSplitParent({
225
+ execution: input.execution,
226
+ manifest: input.manifest,
227
+ expectedOperationRevision: input.execution.operationRevision,
228
+ successorOperationRevision: consumed.projection.revision,
229
+ envelopeTargetDigest: humanProjection.envelope.targetDigest,
230
+ activeWriterOperationId: input.activeWriterOperationId,
231
+ activeProviderOperationId: input.activeProviderOperationId,
232
+ authorization: { mode: "human", envelope: humanProjection.envelope, receipt },
233
+ });
234
+ if (initialized.outcome !== "applied")
235
+ return { outcome: "rejected", reason: initialized.reason, projection: humanProjection };
236
+ const splitProjection = frontendSplitProjectionSchema.parse({ revision: consumed.projection.revision, execution: initialized.execution, manifest: input.manifest, parent: initialized.parent });
237
+ const { frontendExecution: _singleTask, ...withoutSingleTask } = current;
238
+ const record = { ...withoutSingleTask, revision: consumed.projection.revision, frontendHumanDecision: consumed.projection, frontendSplit: splitProjection, state: "running" };
239
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
240
+ return { outcome: "applied", projection: splitProjection, humanDecision: consumed.projection };
241
+ });
242
+ }
243
+ async compareAndSwapFrontendSplit(input) {
244
+ return this.serialize(input.operationId, async () => {
245
+ const current = await this.get(input.operationId);
246
+ if (!current || current.revision === undefined || current.revision !== input.expectedRevision)
247
+ return { outcome: "conflict" };
248
+ const currentProjection = current.frontendSplit;
249
+ if (currentProjection) {
250
+ if (currentProjection.execution.digest !== input.expectedExecutionDigest || currentProjection.parent.digest !== input.expectedParentDigest)
251
+ return { outcome: "conflict" };
252
+ }
253
+ else if (input.expectedExecutionDigest || input.expectedParentDigest)
254
+ return { outcome: "conflict" };
255
+ const next = currentProjection
256
+ ? assertFrontendSplitProjectionSuccessor(currentProjection, input.next)
257
+ : frontendSplitProjectionSchema.parse(input.next);
258
+ if (next.revision !== current.revision + 1 || next.parent.operationId !== input.operationId)
259
+ throw new Error("split CAS next revision or operation binding mismatch");
260
+ const state = next.parent.operationState === "success" ? "succeeded" : next.parent.operationTerminal !== undefined ? "failed" : current.state;
261
+ const record = { ...current, revision: next.revision, frontendSplit: next, state };
262
+ await writeSecureJson(operationFile(this.appData, input.operationId), record);
263
+ return { outcome: "applied", projection: next };
264
+ });
265
+ }
266
+ async loadChildCloseoutFacts(input) {
267
+ const factsDir = input.closeoutRef.ref;
268
+ const facts = await readJsonIfExists(path.join(factsDir, "frontend-closeout-facts.json"));
269
+ if (facts === undefined)
270
+ return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: [], frontendTestHandoffRef: undefined });
271
+ const parsedFacts = frontendCloseoutFactsSchema.parse(facts);
272
+ if (computeFrontendSplitDigest(parsedFacts) !== input.closeoutRef.digest)
273
+ throw new Error(`child closeout facts digest mismatch: ${input.childId}`);
274
+ const handoff = await readJsonIfExists(path.join(factsDir, "frontend-test-handoff.json"));
275
+ const frontendTestHandoffRef = handoff === undefined ? undefined : computeFrontendSplitDigest(frontendTestHandoffSchema.parse(handoff));
276
+ return frontendSplitChildCloseoutFactsSchema.parse({ childId: input.childId, residualRisks: parsedFacts.residualRisks ?? [], frontendTestHandoffRef });
277
+ }
278
+ async settleFrontendSplitAggregate(input) {
279
+ return this.serialize(input.operationId, async () => {
280
+ const record = await this.get(input.operationId);
281
+ if (!record || record.revision === undefined || record.revision !== input.expectedRevision || !record.frontendSplit)
282
+ return { outcome: "conflict" };
283
+ const authoritative = frontendSplitProjectionSchema.parse(record.frontendSplit);
284
+ if (authoritative.parent.operationId !== input.operationId
285
+ || authoritative.revision !== input.expectedRevision
286
+ || authoritative.execution.digest !== input.expectedExecutionDigest
287
+ || authoritative.parent.digest !== input.expectedParentDigest
288
+ || authoritative.manifest.digest !== input.expectedManifestDigest
289
+ || authoritative.candidateBinding?.digest !== input.expectedCandidateBindingDigest
290
+ || authoritative.integrationBinding?.digest !== input.expectedIntegrationBindingDigest)
291
+ return { outcome: "conflict" };
292
+ if (!authoritative.candidateBinding)
293
+ return { outcome: "conflict" };
294
+ const expectedAggregate = buildFrontendSplitAggregateCloseout(authoritative.parent, authoritative.manifest, authoritative.candidateBinding, authoritative.integrationBinding, input.childCloseoutFacts);
295
+ if (JSON.stringify(expectedAggregate) !== JSON.stringify(input.aggregate))
296
+ return { outcome: "conflict" };
297
+ const transaction = await input.prepareAggregate({
298
+ operationId: authoritative.parent.operationId,
299
+ aggregate: expectedAggregate,
300
+ });
301
+ try {
302
+ const committedRef = frontendCommittedFactRefSchema.parse(transaction.committedRef);
303
+ const finalized = finalizeFrontendSplitAggregate(authoritative.parent, expectedAggregate, committedRef);
304
+ const next = assertFrontendSplitProjectionSuccessor(authoritative, { ...authoritative, revision: authoritative.revision + 1, parent: finalized.parent, execution: finalized.execution, aggregateCloseout: { value: expectedAggregate, committedRef } });
305
+ const nextRecord = { ...record, revision: next.revision, frontendSplit: next, state: "succeeded" };
306
+ await transaction.commit({
307
+ parentRecordPath: operationFile(this.appData, record.operationId),
308
+ parentRecord: nextRecord,
309
+ });
310
+ return { outcome: "applied", projection: next };
311
+ }
312
+ catch (error) {
313
+ await transaction.rollback();
314
+ throw error;
315
+ }
316
+ });
317
+ }
318
+ async initializeFrontendSplit(expectedRevision, projectionValue) {
319
+ const projection = frontendSplitProjectionSchema.parse(projectionValue);
320
+ return this.serialize(projection.parent.operationId, async () => {
321
+ const current = await this.get(projection.parent.operationId);
322
+ if (!current || current.revision === undefined || current.revision !== expectedRevision || current.frontendSplit !== undefined || projection.revision !== expectedRevision + 1)
323
+ return { outcome: "conflict" };
324
+ const record = { ...current, revision: projection.revision, frontendSplit: projection };
325
+ await writeSecureJson(operationFile(this.appData, current.operationId), record);
326
+ return { outcome: "applied", projection };
327
+ });
328
+ }
329
+ async list() {
330
+ const files = await listJsonFiles(this.appData.operations);
331
+ const ops = [];
332
+ for (const file of files) {
333
+ const base = path.basename(file);
334
+ if (base.startsWith("req-"))
335
+ continue;
336
+ const rec = await readJsonIfExists(file);
337
+ if (rec?.operationId)
338
+ ops.push(rec);
339
+ }
340
+ ops.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
341
+ return ops;
342
+ }
343
+ /**
344
+ * On Console boot: any non-terminal operation that cannot be proven still
345
+ * running becomes needs-reconcile (never invent succeeded/failed).
346
+ */
347
+ async reconcileOnBoot(options) {
348
+ const isAlive = options?.isProcessAlive ??
349
+ ((pid) => {
350
+ try {
351
+ process.kill(pid, 0);
352
+ return true;
353
+ }
354
+ catch {
355
+ return false;
356
+ }
357
+ });
358
+ const changed = [];
359
+ for (const op of await this.list()) {
360
+ if (TERMINAL_OPERATION_STATES.has(op.state))
361
+ continue;
362
+ // A consoleUpdateAndInit whose record already proves a requested
363
+ // handoff died before writing its terminal state: the update and
364
+ // restart themselves succeeded — the booting process IS the
365
+ // handoff target. Mark it succeeded instead of surfacing the
366
+ // generic needs-reconcile failure copy to the operator.
367
+ if (op.action === "consoleUpdateAndInit" &&
368
+ op.state === "needs-reconcile" &&
369
+ isProvenConsoleUpdateHandoff(op.result)) {
370
+ changed.push(await this.update(op.operationId, {
371
+ state: "succeeded",
372
+ errorCode: undefined,
373
+ errorMessage: undefined,
374
+ }));
375
+ continue;
376
+ }
377
+ if (op.state === "needs-reconcile")
378
+ continue;
379
+ let next;
380
+ if (op.state === "running" &&
381
+ typeof op.pid === "number" &&
382
+ isAlive(op.pid, op.processStartIdentity)) {
383
+ // still running — leave as-is
384
+ continue;
385
+ }
386
+ // queued/starting/running without proven process → needs-reconcile
387
+ next = await this.update(op.operationId, {
388
+ state: "needs-reconcile",
389
+ errorCode: "OPERATION_NEEDS_RECONCILE",
390
+ errorMessage: "Console restart or lost process evidence; operation needs reconcile (not cancelled)",
391
+ });
392
+ changed.push(next);
393
+ }
394
+ return changed;
395
+ }
396
+ }
397
+ export function isTerminalOperationState(state) {
398
+ return TERMINAL_OPERATION_STATES.has(state);
399
+ }
400
+ /**
401
+ * True when a consoleUpdateAndInit result proves the handoff was requested
402
+ * (install verified + init clean + new Console spawned). Used by boot
403
+ * reconcile to close the record as succeeded even though the old process
404
+ * died before writing its terminal state.
405
+ */
406
+ function isProvenConsoleUpdateHandoff(result) {
407
+ if (!result || typeof result !== "object")
408
+ return false;
409
+ const shape = result;
410
+ return (shape.state === "restarting" &&
411
+ shape.maintenance?.handoff?.status === "requested");
412
+ }
413
+ export function clipPreview(text, max = 4096) {
414
+ if (text.length <= max)
415
+ return text;
416
+ return `${text.slice(0, max)}\n…[truncated ${text.length - max} chars]`;
417
+ }
@@ -1,6 +1,6 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { writeJsonAtomic, writeTextAtomic, } from "./atomic-write.js";
3
+ import { writeJsonAtomic, writeBufferAtomic, writeTextAtomic, } from "./atomic-write.js";
4
4
  function resolveRepoRootFromDagRunDir(runDir) {
5
5
  const normalized = path.resolve(runDir);
6
6
  const lifecycleDir = path.dirname(normalized);
@@ -26,12 +26,16 @@ function dagWriteOptions(runDir, options) {
26
26
  return {
27
27
  repoRoot: options?.repoRoot ?? resolveRepoRootFromDagRunDir(runDir),
28
28
  allowCompletedFactsWrite: options?.allowCompletedFactsWrite,
29
+ mode: options?.mode,
30
+ directoryMode: options?.directoryMode,
29
31
  };
30
32
  }
31
33
  function pathWriteOptions(targetPath, options) {
32
34
  return {
33
35
  repoRoot: options?.repoRoot ?? resolveRepoRootFromHarnessPath(targetPath),
34
36
  allowCompletedFactsWrite: options?.allowCompletedFactsWrite,
37
+ mode: options?.mode,
38
+ directoryMode: options?.directoryMode,
35
39
  };
36
40
  }
37
41
  function completedRelocationWriteOptions(runDir) {
@@ -48,6 +52,11 @@ export async function writeDagNodeTextArtifact(runDir, nodeId, fileName, content
48
52
  await writeTextAtomic(targetPath, content, dagWriteOptions(runDir, options));
49
53
  return targetPath;
50
54
  }
55
+ export async function writeDagNodeBufferArtifact(runDir, nodeId, fileName, bytes, options) {
56
+ const targetPath = path.join(runDir, nodeId, fileName);
57
+ await writeBufferAtomic(targetPath, bytes, dagWriteOptions(runDir, options));
58
+ return targetPath;
59
+ }
51
60
  export async function writeDagNodeJsonArtifact(runDir, nodeId, fileName, value, options) {
52
61
  const targetPath = path.join(runDir, nodeId, fileName);
53
62
  await writeJsonAtomic(targetPath, value, dagWriteOptions(runDir, options));
@@ -30,10 +30,17 @@ async function writeAtomic(targetPath, content, options) {
30
30
  const resolved = options?.repoRoot && !path.isAbsolute(targetPath)
31
31
  ? path.resolve(options.repoRoot, targetPath)
32
32
  : path.resolve(targetPath);
33
- await mkdir(path.dirname(resolved), { recursive: true });
33
+ await mkdir(path.dirname(resolved), {
34
+ recursive: true,
35
+ ...(options?.directoryMode !== undefined
36
+ ? { mode: options.directoryMode }
37
+ : {}),
38
+ });
34
39
  const tempPath = `${resolved}.${process.pid}.${(atomicWriteSequence += 1)}.tmp`;
35
40
  try {
36
- await writeFile(tempPath, content, "utf-8");
41
+ await writeFile(tempPath, content, {
42
+ ...(options?.mode !== undefined ? { mode: options.mode } : {}),
43
+ });
37
44
  try {
38
45
  await rename(tempPath, resolved);
39
46
  }
@@ -54,6 +61,9 @@ export async function writeJsonAtomic(targetPath, value, options) {
54
61
  export async function writeTextAtomic(targetPath, text, options) {
55
62
  await writeAtomic(targetPath, text, options);
56
63
  }
64
+ export async function writeBufferAtomic(targetPath, bytes, options) {
65
+ await writeAtomic(targetPath, bytes, options);
66
+ }
57
67
  export function serializeJsonl(entries) {
58
68
  if (entries.length === 0)
59
69
  return "";