@tea-agent/loop-agent 0.43.0-next.9 → 0.43.0

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 (287) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +31 -2309
  3. package/README.md +7 -0
  4. package/dist/application/dag/args.js +8 -0
  5. package/dist/application/task-lifecycle/advance.js +38 -7
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +2 -0
  8. package/dist/cli/program.js +3 -1
  9. package/dist/cli/update/init-surface-notifier.js +2 -0
  10. package/dist/cli/update/policy.js +2 -0
  11. package/dist/commands/client-recovery.js +20 -55
  12. package/dist/commands/cursor-prompt.js +2 -0
  13. package/dist/commands/dag-approve.js +2 -0
  14. package/dist/commands/dag-final-verification.js +2 -0
  15. package/dist/commands/dag-init-hybrid.js +2 -0
  16. package/dist/commands/dag-reconcile-tasks.js +2 -0
  17. package/dist/commands/dag-reject.js +2 -0
  18. package/dist/commands/dag-report.js +2 -0
  19. package/dist/commands/dag-rerun.js +2 -0
  20. package/dist/commands/dag-resume.js +2 -0
  21. package/dist/commands/dag-workflow-compile.js +2 -0
  22. package/dist/commands/dag-workflow-plan.js +2 -0
  23. package/dist/commands/dag-workflow-validate.js +2 -0
  24. package/dist/commands/delegate.js +2 -0
  25. package/dist/commands/examples.js +2 -0
  26. package/dist/commands/import-prd.js +2 -0
  27. package/dist/commands/init-model-catalog.js +2 -0
  28. package/dist/commands/init.js +19 -13
  29. package/dist/commands/instructions.js +2 -0
  30. package/dist/commands/knowledge.js +2 -0
  31. package/dist/commands/loop-benchmark.js +2 -0
  32. package/dist/commands/pi-prompt.js +2 -0
  33. package/dist/commands/pi-reuse-benchmark.js +2 -0
  34. package/dist/commands/promote-run.js +2 -0
  35. package/dist/commands/stats.js +1 -1
  36. package/dist/commands/study-init.js +2 -0
  37. package/dist/commands/task-advance.js +34 -0
  38. package/dist/commands/task-source-prepare.js +2 -0
  39. package/dist/commands/worktree-create.js +2 -0
  40. package/dist/commands/worktree-remove.js +2 -0
  41. package/dist/executors/dag-pi-executor.js +1650 -198
  42. package/dist/executors/pi-executor.js +85 -3
  43. package/dist/executors/pi-sdk-executor.js +18 -0
  44. package/dist/executors/shell-executor.js +93 -7
  45. package/dist/executors/shell-verification.js +3 -0
  46. package/dist/executors/shell-write-guard.js +27 -32
  47. package/dist/governance/exec-plans.js +6 -3
  48. package/dist/infrastructure/console/app-data.js +6 -0
  49. package/dist/shared/artifacts-core.js +2 -0
  50. package/dist/shared/backend-dogfood-preflight.js +47 -0
  51. package/dist/shared/dag-failure-category.js +3 -0
  52. package/dist/shared/git-progress.js +2 -0
  53. package/dist/shared/one-shot-prompt-args.js +2 -0
  54. package/dist/shared/operator/capabilities.js +180 -0
  55. package/dist/shared/package-metadata.js +6 -4
  56. package/dist/shared/pi-context-pressure/extension.js +26 -16
  57. package/dist/shared/pi-context-pressure/sift-bridge.js +1 -1
  58. package/dist/shared/pi-provider-recovery.js +182 -0
  59. package/dist/shared/pi-retry-settings.js +40 -0
  60. package/dist/shared/reference-context.js +2 -0
  61. package/dist/task/config-types.js +29 -0
  62. package/dist/task/contract/project.js +9 -0
  63. package/dist/task/contract/recover.js +6 -0
  64. package/dist/task/contract/schema.js +17 -0
  65. package/dist/task/source-prepare/artifact-meta.js +9 -2
  66. package/dist/task/source-prepare/build-draft.js +60 -0
  67. package/dist/task/source-prepare/fragment-inventory.js +4 -1
  68. package/dist/task/source-prepare/parse-intent.js +36 -11
  69. package/dist/task/task-demand-routing.js +5 -2
  70. package/dist/worker/console/chat/browser-automation.js +523 -0
  71. package/dist/worker/console/chat/browser-policy.js +6 -36
  72. package/dist/worker/console/chat/browser-routes.js +188 -56
  73. package/dist/worker/console/chat/explore-tools.js +5 -2
  74. package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
  75. package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
  76. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +184 -0
  77. package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
  78. package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
  79. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +94 -0
  80. package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
  81. package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
  82. package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
  83. package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
  84. package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
  85. package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
  86. package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
  87. package/dist/worker/console/chat/pi-runtime.js +208 -1138
  88. package/dist/worker/console/chat/routes.js +27 -7
  89. package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
  90. package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
  91. package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
  92. package/dist/worker/console/chat/sdd-data-alignment.js +1 -1
  93. package/dist/worker/console/chat/session-mode-view.js +5 -4
  94. package/dist/worker/console/chat/session-mode.js +18 -12
  95. package/dist/worker/console/chat/session-store.js +42 -9
  96. package/dist/worker/console/chat/tool-preview.js +115 -0
  97. package/dist/worker/console/chat/tools.js +10 -0
  98. package/dist/worker/console/chat/turn-order.js +13 -0
  99. package/dist/worker/console/chat/turn-process.js +32 -30
  100. package/dist/worker/console/console-update-and-init.js +1 -19
  101. package/dist/worker/console/console-update-runtime.js +5 -20
  102. package/dist/worker/console/operator-actions.js +63 -1
  103. package/dist/worker/console/prd-intake-bridge.js +54 -1
  104. package/dist/worker/console/routes.js +8 -0
  105. package/dist/worker/console/scheduled-goal-host.js +98 -0
  106. package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
  107. package/dist/worker/console/server.js +24 -1
  108. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-D_fNbWc_.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
  109. package/dist/worker/console/static/assets/{arc-_HuqLW6m.js → arc-eoQiMvuk.js} +1 -1
  110. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Djy2qY1m.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
  111. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-bB-1TSyY.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
  112. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5Hmghrz.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
  113. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
  114. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Duyl39oy.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
  115. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-x6mdnSyf.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
  116. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-WlBuFj7W.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
  117. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CTF429HX.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
  118. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dwd6r-SF.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
  119. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C-Br82D1.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
  120. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CuGp0DeE.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
  121. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CJhFX6Lh.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
  122. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DpGOa_jT.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
  123. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DXJaTynd.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
  124. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
  125. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
  126. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CRBZWjE4.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
  127. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BdXcxtlb.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
  128. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-07UKsOts.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
  129. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-D38p9__S.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
  130. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-nSiBNNjH.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
  131. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BFOTot5D.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
  132. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DypKaZ6k.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
  133. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-B6yoOQeW.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
  134. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BHYS6viX.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
  135. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-D2VD5Snj.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
  136. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-E98mWxN1.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
  137. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CT1zxhqP.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
  138. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-6GcngNcy.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
  139. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCL6NvKe.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
  140. package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
  141. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +469 -0
  142. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C0627e-9.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
  143. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
  144. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DPDPv9HL.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
  145. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D3OHdWiO.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
  146. package/dist/worker/console/static/assets/{linear-DI88zD7n.js → linear-BDZ9riWi.js} +1 -1
  147. package/dist/worker/console/static/assets/{mermaid.core-p2l8IrWY.js → mermaid.core-7pKqYtpZ.js} +5 -5
  148. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-B1bAX-oS.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
  149. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DwOnWxdD.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
  150. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-xRkTXnJC.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
  151. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-_9UC5Zio.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
  152. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CgEUirWJ.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
  153. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Czbpwiyz.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
  154. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-_j7_KiqY.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
  155. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BNJa6Hyi.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
  156. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DedpKtOc.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
  157. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BBvthr7c.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
  158. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
  159. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Ds7umgEM.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
  160. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
  161. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKa-p1nF.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
  162. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LoUNR56o.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
  163. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bqbl6ym7.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
  164. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B8dUg4lf.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
  165. package/dist/worker/console/static/index.html +2 -2
  166. package/dist/worker/console/static-src/operator-chat/chat-scroll-position.js +24 -0
  167. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
  168. package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
  169. package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +192 -0
  170. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +9 -1
  171. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +15 -5
  172. package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
  173. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +92 -38
  174. package/dist/worker/console/static-src/operator-chat/useChatStream.js +145 -70
  175. package/dist/worker/console/static-src/operator-chat/useChatThread.js +38 -55
  176. package/dist/worker/console/static-src/operator-chat/useComposer.js +182 -69
  177. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +2 -0
  178. package/dist/worker/console/static-src/prd-file-import.js +3 -1
  179. package/dist/worker/console/workspace-context.js +22 -0
  180. package/dist/worker/console/workspace-initialization.js +209 -0
  181. package/dist/worker/delivery/git-transaction.js +2 -0
  182. package/dist/worker/feature/fullstack-validate.js +2 -0
  183. package/dist/worker/loop-agent/loop-agent-client.js +8 -2
  184. package/dist/worker/materialize/harness-task-materializer.js +4 -1
  185. package/dist/worker/metrics/projector.js +1 -1
  186. package/dist/worker/observability/event-store.js +4 -1
  187. package/dist/worker/observability/read-model.js +4 -1
  188. package/dist/worker/observe/node-transparency.js +23 -1
  189. package/dist/worker/observe/routes.js +4 -0
  190. package/dist/worker/observe/static/operator-chrome.js +3 -1
  191. package/dist/worker/observe/static/styles.css +7 -4
  192. package/dist/worker/observe/static/views/dag-inspector.js +5 -19
  193. package/dist/worker/outcomes/declared-artifacts.js +2 -0
  194. package/dist/worker/preflight.js +3 -0
  195. package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
  196. package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
  197. package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
  198. package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
  199. package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
  200. package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
  201. package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
  202. package/dist/worker/task-spec/validate.js +4 -1
  203. package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
  204. package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
  205. package/dist/workflows/dag/dag-retry-schema.js +11 -0
  206. package/dist/workflows/dag/decision-envelope.js +2 -0
  207. package/dist/workflows/dag/dynamic-runtime/shared.js +2 -2
  208. package/dist/workflows/dag/frontend-closeout.js +3 -1
  209. package/dist/workflows/dag/frontend-committed-facts.js +461 -0
  210. package/dist/workflows/dag/frontend-durable-tools.js +15 -3
  211. package/dist/workflows/dag/frontend-implementation-contract.js +369 -12
  212. package/dist/workflows/dag/frontend-plan-canary.js +53 -0
  213. package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
  214. package/dist/workflows/dag/frontend-plan-render.js +0 -2
  215. package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
  216. package/dist/workflows/dag/frontend-recovery-run.js +57 -0
  217. package/dist/workflows/dag/frontend-repair.js +55 -76
  218. package/dist/workflows/dag/frontend-review-context.js +42 -68
  219. package/dist/workflows/dag/frontend-review-scopes.js +2 -2
  220. package/dist/workflows/dag/frontend-risk.js +92 -10
  221. package/dist/workflows/dag/frontend-session-budget.js +117 -3
  222. package/dist/workflows/dag/frontend-shape.js +11 -55
  223. package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
  224. package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
  225. package/dist/workflows/dag/frontend-verification-trace.js +33 -54
  226. package/dist/workflows/dag/frontend-writer-admission.js +3 -47
  227. package/dist/workflows/dag/frontend-writer-status.js +0 -23
  228. package/dist/workflows/dag/init-hybrid.js +47 -19
  229. package/dist/workflows/dag/interrupt-request.js +2 -0
  230. package/dist/workflows/dag/lifecycle.js +11 -2
  231. package/dist/workflows/dag/node-execution.js +50 -13
  232. package/dist/workflows/dag/reconcile-run.js +2 -0
  233. package/dist/workflows/dag/repair-artifact.js +3 -1
  234. package/dist/workflows/dag/report.js +2 -0
  235. package/dist/workflows/dag/rerun-plan.js +10 -0
  236. package/dist/workflows/dag/rerun-task.js +79 -1
  237. package/dist/workflows/dag/retry-policy.js +18 -0
  238. package/dist/workflows/dag/scheduler.js +2 -4
  239. package/dist/workflows/dag/types.js +44 -6
  240. package/dist/workflows/dag/validate.js +4 -0
  241. package/docs/README.md +1 -0
  242. package/docs/architecture/runtime-boundaries.md +3 -3
  243. package/docs/governance/README.md +1 -0
  244. package/docs/init-surface.manifest.json +1 -0
  245. package/docs/operations/README.md +2 -0
  246. package/docs/templates/README.md +1 -1
  247. package/docs/templates/agent-dag.schema.json +2 -2
  248. package/docs/templates/backend-test-dag.json +14 -10
  249. package/docs/templates/frontend-implementation-contract.schema.json +0 -7
  250. package/package.json +9 -3
  251. package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
  252. package/skills/frontend-contract/references/contract-protocol.md +3 -1
  253. package/skills/frontend-plan/SKILL.md +8 -6
  254. package/skills/frontend-plan/references/decision-contract.md +3 -3
  255. package/skills/frontend-review/SKILL.md +18 -28
  256. package/skills/frontend-review/references/review-findings.md +8 -3
  257. package/dist/commands/new-task.js +0 -5
  258. package/dist/commands/status.js +0 -57
  259. package/dist/commands/task-contract.js +0 -269
  260. package/dist/shared/prompts.js +0 -26
  261. package/dist/task/config.js +0 -2
  262. package/dist/task/contract/validate-draft.js +0 -106
  263. package/dist/task/goal.js +0 -3
  264. package/dist/task/index.js +0 -12
  265. package/dist/task/lifecycle.js +0 -1
  266. package/dist/task/operator/capabilities.js +0 -6
  267. package/dist/task/operator/envelope.js +0 -2
  268. package/dist/task/operator/index.js +0 -5
  269. package/dist/task/operator/registry.js +0 -2
  270. package/dist/task/operator/types.js +0 -1
  271. package/dist/task/paths.js +0 -1
  272. package/dist/task/source-state.js +0 -1
  273. package/dist/task/subagent-guidance.js +0 -1
  274. package/dist/worker/console/chat/sift-bridge.js +0 -1
  275. package/dist/worker/console/index.js +0 -20
  276. package/dist/worker/console/static/assets/channel-C0A6UEIa.js +0 -1
  277. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +0 -1
  278. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +0 -1
  279. package/dist/worker/console/static/assets/index-24jp3aBh.js +0 -468
  280. package/dist/worker/console/static/assets/index-CY6y6I8M.css +0 -1
  281. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +0 -1
  282. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +0 -8
  283. package/dist/worker/scheduler/index.js +0 -25
  284. package/dist/workflows/dag/facts.js +0 -4
  285. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
  286. package/dist/workflows/dag/index.js +0 -6
  287. package/dist/workflows/dynamic/index.js +0 -7
@@ -0,0 +1,699 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, open, readFile, realpath, rename, unlink } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import { evaluateScheduledGoalWake } from "./scheduled-goal-time.js";
8
+ import { captureScheduledGoalTree } from "./scheduled-goal-evidence.js";
9
+ import { scheduledGoalDraftSchema, scheduledGoalRecordSchema, scheduledGoalOperationObservationSchema, scheduledGoalAssessmentSchema, scheduledGoalProposalSchema, } from "./scheduled-goal-types.js";
10
+ const stateSchema = z.object({ schemaVersion: z.literal(1), goals: z.array(scheduledGoalRecordSchema) }).strict();
11
+ const execFileAsync = promisify(execFile);
12
+ const holdsWriterSlot = (intent) => ["dispatching", "associated", "unknown"].includes(intent.state);
13
+ const holdsResource = (goal) => Boolean(goal.assessmentAttempt) || goal.intents.some(holdsWriterSlot) || goal.supervisions.some((entry) => ["requested", "proposed", "preparing", "unknown"].includes(entry.state));
14
+ async function syncDirectory(directoryPath) {
15
+ const directory = await open(directoryPath, "r");
16
+ try {
17
+ await directory.sync();
18
+ }
19
+ finally {
20
+ await directory.close();
21
+ }
22
+ }
23
+ async function assertInitialFrozenTree(frozen) {
24
+ const tree = await captureScheduledGoalTree(frozen.worktreeRef, frozen.frozenBase, frozen.allowedPaths, frozen.forbiddenPaths);
25
+ if (tree.changedPaths.length)
26
+ throw new Error(`initial worktree differs from frozen base: ${tree.changedPaths.join(", ")}`);
27
+ }
28
+ async function assertExecutionWorkspace(repoRef, worktreeRef, base) {
29
+ const git = async (cwd, args) => (await execFileAsync("git", ["-C", cwd, ...args], { timeout: 30_000, encoding: "utf8" })).stdout.trim();
30
+ try {
31
+ const [top, common, controlCommon, gitDir, head] = await Promise.all([
32
+ git(worktreeRef, ["rev-parse", "--show-toplevel"]),
33
+ git(worktreeRef, ["rev-parse", "--path-format=absolute", "--git-common-dir"]),
34
+ git(repoRef, ["rev-parse", "--path-format=absolute", "--git-common-dir"]),
35
+ git(worktreeRef, ["rev-parse", "--absolute-git-dir"]),
36
+ git(worktreeRef, ["rev-parse", "HEAD"]),
37
+ ]);
38
+ if (await realpath(top) !== worktreeRef || await realpath(common) !== await realpath(controlCommon)
39
+ || (worktreeRef !== repoRef && await realpath(gitDir) === await realpath(common)) || head !== base)
40
+ throw new Error("worktree identity/base mismatch");
41
+ }
42
+ catch (error) {
43
+ throw new Error(`scheduled goal requires the current repository or its linked git worktree at frozen base: ${String(error)}`);
44
+ }
45
+ }
46
+ export function scheduledGoalHash(value) {
47
+ const canonical = (item) => Array.isArray(item) ? item.map(canonical)
48
+ : item !== null && typeof item === "object"
49
+ ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, canonical(entry)])) : item;
50
+ return createHash("sha256").update(JSON.stringify(canonical(value))).digest("hex");
51
+ }
52
+ /** Canonical IDs are required by workflow lineage stores, not just the CLI.
53
+ * Use the frozen UTC date so a later wake never changes an intent's identity. */
54
+ function scheduledGoalRunId(goal, key) {
55
+ const day = goal.frozen.time.executeAtUtc.slice(0, 10).replaceAll("-", "");
56
+ return `${day}-sg-${key.slice(0, 32)}`;
57
+ }
58
+ /** Stable delivery identity excludes delivery bookkeeping and wake-only state versions. */
59
+ export function scheduledGoalDeliveryKey(goal) {
60
+ return scheduledGoalHash([goal.goalId, goal.goalRevision, goal.state, goal.acceptanceState,
61
+ goal.reasonCode ?? null, goal.evidence.at(-1)?.evidenceId ?? null, goal.intents.at(-1)?.state ?? null]);
62
+ }
63
+ /**
64
+ * One repository admission ledger: root envelope, budget, intent and journal are
65
+ * one atomic commit. The cross-process mutex is never automatically stolen.
66
+ * A crash leaving it behind requires explicit reconciliation, not a second writer.
67
+ */
68
+ export class ScheduledGoalStore {
69
+ repoRoot;
70
+ root;
71
+ constructor(repoRoot) {
72
+ this.repoRoot = repoRoot;
73
+ this.root = path.resolve(repoRoot, ".harness/scheduled-goals");
74
+ }
75
+ async get(goalId) {
76
+ return this.requireGoal(await this.read(), goalId);
77
+ }
78
+ async recordDelivery(goalId, key, delivery) {
79
+ await this.transaction((state) => {
80
+ const goal = this.requireGoal(state, goalId);
81
+ if (scheduledGoalDeliveryKey(goal) !== key)
82
+ throw new Error("delivery result changed");
83
+ if (goal.deliveryKey === key && goal.deliveryState === delivery)
84
+ return;
85
+ goal.deliveryKey = key;
86
+ goal.deliveryState = delivery;
87
+ this.event(goal, `result_delivery:${delivery}`, new Date());
88
+ });
89
+ }
90
+ async list() { return (await this.read()).goals; }
91
+ async createDraft(input, now) {
92
+ const frozen = scheduledGoalDraftSchema.parse(input);
93
+ const repoRef = await realpath(this.repoRoot);
94
+ frozen.worktreeRef = await realpath(frozen.worktreeRef);
95
+ await assertExecutionWorkspace(repoRef, frozen.worktreeRef, frozen.frozenBase);
96
+ await assertInitialFrozenTree(frozen);
97
+ const resourceKey = scheduledGoalHash([repoRef, frozen.managedTaskId]);
98
+ return this.transaction((state) => {
99
+ now ??= new Date();
100
+ if (state.goals.some((goal) => goal.resourceKey === resourceKey && !["cancelled", "accepted"].includes(goal.state)))
101
+ throw new Error("managed task already reserved by a scheduled goal");
102
+ const goal = {
103
+ schemaVersion: 1, goalId: `sg-${randomUUID()}`, goalRevision: 1, stateVersion: 1,
104
+ repoRef, resourceKey, envelopeHash: scheduledGoalHash({ repoRef, frozen, goalRevision: 1 }), frozen,
105
+ state: "planned", used: { turns: 0, dagRuns: 0, supervisorCalls: 0 }, intents: [],
106
+ acceptanceState: "not_started", evidence: [], supervisions: [], deliveryState: "not_requested",
107
+ journal: [{ version: 1, at: now.toISOString(), event: "draft_created" }],
108
+ };
109
+ state.goals.push(goal);
110
+ return goal;
111
+ });
112
+ }
113
+ /** Human-facing adapter supplies the exact reviewed hash and immutable approval reference. */
114
+ async approve(goalId, expectedVersion, envelopeHash, approvalRef, now) {
115
+ const before = await this.get(goalId);
116
+ await assertExecutionWorkspace(before.repoRef, before.frozen.worktreeRef, before.frozen.frozenBase);
117
+ await assertInitialFrozenTree(before.frozen);
118
+ return this.transaction((state) => {
119
+ now ??= new Date();
120
+ const goal = this.requireGoal(state, goalId);
121
+ if (goal.stateVersion !== expectedVersion || goal.state !== "planned" || goal.envelopeHash !== envelopeHash || !approvalRef.trim())
122
+ throw new Error("approval binding mismatch");
123
+ if (now.toISOString() >= goal.frozen.time.expiresAt)
124
+ throw new Error("authorization expired");
125
+ goal.approval = { approvalRef, approvedAt: now.toISOString(), envelopeHash, approvedVersion: expectedVersion };
126
+ goal.state = "authorized";
127
+ this.event(goal, "human_approved", now);
128
+ return goal;
129
+ });
130
+ }
131
+ /** Durable cancellation blocks admission; it does not assert that a child has stopped. */
132
+ async cancel(goalId, expectedVersion, requestId, reason, now) {
133
+ if (!requestId.trim() || !reason.trim())
134
+ throw new Error("cancellation reference and reason required");
135
+ return this.transaction((state) => {
136
+ const goal = this.requireGoal(state, goalId);
137
+ if (goal.cancellation?.requestId === requestId) {
138
+ if (goal.cancellation.reason !== reason)
139
+ throw new Error("cancellation request payload mismatch");
140
+ return goal;
141
+ }
142
+ if (goal.stateVersion !== expectedVersion)
143
+ throw new Error("cancellation version mismatch");
144
+ if (goal.state === "accepted")
145
+ throw new Error("accepted goal cannot be cancelled");
146
+ if (goal.cancellation)
147
+ throw new Error("goal already cancelled");
148
+ now ??= new Date();
149
+ goal.cancellation = { requestId, reason, requestedAt: now.toISOString() };
150
+ goal.state = "cancelled";
151
+ goal.reasonCode = "user_cancelled";
152
+ // Prepared has never granted dispatch. In-flight/unknown intents retain
153
+ // their writer slot until an exact operation/run terminal observation.
154
+ for (const intent of goal.intents)
155
+ if (intent.state === "prepared")
156
+ intent.state = "terminal";
157
+ for (const entry of goal.supervisions)
158
+ if (entry.state === "proposed")
159
+ entry.state = "stopped";
160
+ this.event(goal, "cancel_requested", now);
161
+ return goal;
162
+ });
163
+ }
164
+ async claim(goalId, now) {
165
+ return this.transaction((state) => {
166
+ now ??= new Date();
167
+ const goal = this.requireGoal(state, goalId);
168
+ if (!goal.approval || goal.approval.envelopeHash !== goal.envelopeHash)
169
+ throw new Error("scheduled goal not authorized");
170
+ if (["cancelled", "blocked", "budget_exhausted", "accepted", "unknown"].includes(goal.state))
171
+ return { kind: "blocked", reason: goal.reasonCode ?? goal.state };
172
+ const existing = goal.intents.find((intent) => intent.state !== "terminal");
173
+ if (existing)
174
+ return { kind: "existing", intent: existing };
175
+ const wake = evaluateScheduledGoalWake(goal.frozen.time, now);
176
+ if (wake.kind !== "due") {
177
+ goal.state = wake.kind === "waiting" ? "waiting" : "blocked";
178
+ if (wake.kind === "blocked")
179
+ goal.reasonCode = wake.reason;
180
+ this.event(goal, wake.kind === "waiting" ? "waiting_for_window" : wake.reason, now);
181
+ return wake;
182
+ }
183
+ // Unknown and cancelled-but-in-flight intents continue holding the writer slot.
184
+ if (state.goals.some(holdsResource))
185
+ return { kind: "waiting" };
186
+ if (goal.used.turns >= goal.frozen.budget.maxTurns || goal.used.dagRuns >= goal.frozen.budget.maxDagRuns) {
187
+ goal.state = "budget_exhausted";
188
+ goal.reasonCode = "budget_exhausted";
189
+ this.event(goal, "budget_exhausted", now);
190
+ return { kind: "blocked", reason: "budget_exhausted" };
191
+ }
192
+ if (goal.frozen.budget.maxTokens !== undefined) {
193
+ goal.state = "blocked";
194
+ goal.reasonCode = "token_usage_unknown";
195
+ this.event(goal, "token_usage_unknown", now);
196
+ return { kind: "blocked", reason: "token_usage_unknown" };
197
+ }
198
+ // Initial admission only; Derived admission requires
199
+ // the separate parent-evidence transaction; never silently start a second task.
200
+ if (goal.intents.length)
201
+ throw new Error("derived task requires validated parent evidence");
202
+ const key = scheduledGoalHash([goal.goalId, goal.goalRevision, 1, 1, "execute", goal.envelopeHash]);
203
+ const intent = {
204
+ intentId: `intent-${key}`, taskRevision: 1, turnSeq: 1, epoch: 1, ownerToken: randomUUID(),
205
+ idempotencyKey: key, state: "prepared", operationId: `op_${key.slice(0, 32)}`,
206
+ runId: scheduledGoalRunId(goal, key), createdAt: now.toISOString(), observations: [],
207
+ };
208
+ goal.used.turns++;
209
+ goal.used.dagRuns++;
210
+ goal.intents.push(intent);
211
+ goal.state = "admitted";
212
+ this.event(goal, "intent_prepared", now);
213
+ return { kind: "claimed", intent };
214
+ });
215
+ }
216
+ /** Returns true exactly once; callers must never spawn on false or uncertainty. */
217
+ async beginDispatch(goalId, intentId, ownerToken, now) {
218
+ const before = await this.get(goalId);
219
+ await assertExecutionWorkspace(before.repoRef, before.frozen.worktreeRef, before.frozen.frozenBase);
220
+ return this.transaction(async (state) => {
221
+ const goal = this.requireGoal(state, goalId);
222
+ const intent = goal.intents.find((item) => item.intentId === intentId);
223
+ if (!intent || intent.ownerToken !== ownerToken)
224
+ throw new Error("stale_owner");
225
+ if (!["admitted", "waiting"].includes(goal.state) || intent.state !== "prepared")
226
+ return false;
227
+ if (intent.derived) {
228
+ const tree = await captureScheduledGoalTree(goal.frozen.worktreeRef, goal.frozen.frozenBase, goal.frozen.allowedPaths, goal.frozen.forbiddenPaths);
229
+ if (goal.evidence.at(-1)?.evidenceId !== intent.derived.parentEvidenceId || tree.treeHash !== intent.derived.treeHash || tree.scopeViolations.length)
230
+ throw new Error("derived dispatch evidence drift");
231
+ }
232
+ if (!intent.derived) {
233
+ try {
234
+ await assertInitialFrozenTree(goal.frozen);
235
+ }
236
+ catch {
237
+ goal.state = "blocked";
238
+ goal.reasonCode = "initial_tree_drift";
239
+ intent.state = "terminal";
240
+ this.event(goal, "not_dispatched:initial_tree_drift", now ?? new Date());
241
+ return false;
242
+ }
243
+ }
244
+ now ??= new Date();
245
+ const wake = evaluateScheduledGoalWake(goal.frozen.time, now);
246
+ if (wake.kind !== "due") {
247
+ if (wake.kind === "waiting") {
248
+ goal.state = "waiting";
249
+ this.event(goal, "waiting_for_window", now);
250
+ }
251
+ else {
252
+ goal.state = "blocked";
253
+ goal.reasonCode = wake.reason;
254
+ // This intent never obtained dispatch permission; no child can exist.
255
+ intent.state = "terminal";
256
+ this.event(goal, `not_dispatched:${wake.reason}`, now);
257
+ }
258
+ return false;
259
+ }
260
+ if (state.goals.some(holdsResource))
261
+ return false;
262
+ goal.state = "admitted";
263
+ intent.state = "dispatching";
264
+ intent.dispatchedAt = now.toISOString();
265
+ intent.dispatchOwnerPid = process.pid;
266
+ this.event(goal, "dispatch_intent_committed", now);
267
+ return true;
268
+ });
269
+ }
270
+ /** Only the trusted operation adapter calls this; a run success is not AC acceptance. */
271
+ async observeOperation(goalId, intentId, ownerToken, input) {
272
+ const observation = scheduledGoalOperationObservationSchema.parse(input);
273
+ await this.transaction((state) => {
274
+ const goal = this.requireGoal(state, goalId);
275
+ const intent = goal.intents.find((item) => item.intentId === intentId);
276
+ if (!intent || intent.ownerToken !== ownerToken)
277
+ throw new Error("stale_owner");
278
+ if (intent.state === "prepared")
279
+ throw new Error("operation observation before dispatch");
280
+ if (intent.observations.some((entry) => scheduledGoalHash(entry) === scheduledGoalHash(observation)))
281
+ return;
282
+ intent.observations.push(observation);
283
+ const bound = observation.operationId === intent.operationId && observation.runId === intent.runId;
284
+ if (intent.state === "terminal") {
285
+ this.event(goal, "late_operation_observation", new Date(observation.observedAt));
286
+ return;
287
+ }
288
+ if (!bound || observation.state === "unknown") {
289
+ intent.state = "unknown";
290
+ if (goal.state !== "cancelled") {
291
+ goal.state = "unknown";
292
+ goal.reasonCode = "operation_unknown";
293
+ }
294
+ }
295
+ else {
296
+ intent.state = observation.state === "running" ? "associated" : "terminal";
297
+ if (goal.state === "unknown") {
298
+ goal.state = "admitted";
299
+ delete goal.reasonCode;
300
+ }
301
+ }
302
+ this.event(goal, `operation_observed:${intent.state}`, new Date(observation.observedAt));
303
+ });
304
+ }
305
+ async beginAssessment(goalId, expectedVersion, now = new Date()) {
306
+ return this.transaction((state) => {
307
+ const goal = this.requireGoal(state, goalId);
308
+ const intent = goal.intents.at(-1);
309
+ if (goal.stateVersion !== expectedVersion || goal.state !== "admitted" || !goal.approval)
310
+ throw new Error("assessment authorization/version mismatch");
311
+ if (!intent || intent.state !== "terminal" || !intent.observations.some((observation) => observation.operationId === intent.operationId && observation.runId === intent.runId && ["succeeded", "failed"].includes(observation.state)))
312
+ throw new Error("assessment requires an associated terminal operation");
313
+ if (now.getTime() >= Math.min(Date.parse(goal.frozen.time.stopAtUtc), Date.parse(goal.frozen.time.expiresAt)))
314
+ throw new Error("assessment authorization expired");
315
+ if (state.goals.some(holdsResource))
316
+ throw new Error("assessment resource busy");
317
+ goal.assessmentAttempt = { intentId: intent.intentId, ownerToken: randomUUID(), ownerPid: process.pid, startedAt: now.toISOString() };
318
+ goal.acceptanceState = "assessing";
319
+ this.event(goal, "assessment_started", now);
320
+ return goal.assessmentAttempt;
321
+ });
322
+ }
323
+ async refreshAcceptance(goalId) {
324
+ return this.transaction(async (state) => {
325
+ const goal = this.requireGoal(state, goalId);
326
+ if (!["accepted", "admitted"].includes(goal.state) || !["accepted", "failed"].includes(goal.acceptanceState))
327
+ return goal;
328
+ const evidence = goal.evidence.at(-1);
329
+ const tree = await captureScheduledGoalTree(goal.frozen.worktreeRef, goal.frozen.frozenBase, goal.frozen.allowedPaths, goal.frozen.forbiddenPaths);
330
+ if (!evidence || evidence.intentId !== goal.intents.at(-1)?.intentId || tree.treeHash !== evidence.assessment.after.treeHash || tree.scopeViolations.length) {
331
+ goal.state = "admitted";
332
+ goal.acceptanceState = "stale";
333
+ goal.reasonCode = "evidence_drift";
334
+ this.event(goal, "acceptance_invalidated:tree_changed", new Date());
335
+ }
336
+ return goal;
337
+ });
338
+ }
339
+ async failAssessment(goalId, ownerToken, now = new Date()) {
340
+ await this.transaction((state) => {
341
+ const goal = this.requireGoal(state, goalId);
342
+ if (goal.assessmentAttempt?.ownerToken !== ownerToken)
343
+ throw new Error("stale_owner");
344
+ goal.acceptanceState = "pending";
345
+ if (goal.state !== "cancelled") {
346
+ goal.state = "unknown";
347
+ goal.reasonCode = "assessment_execution_unknown";
348
+ }
349
+ this.event(goal, "assessment_execution_unknown", now);
350
+ });
351
+ }
352
+ async finishAssessment(goalId, ownerToken, input, now) {
353
+ const assessment = scheduledGoalAssessmentSchema.parse(input);
354
+ return this.transaction(async (state) => {
355
+ const goal = this.requireGoal(state, goalId);
356
+ const attempt = goal.assessmentAttempt;
357
+ if (!attempt || attempt.ownerToken !== ownerToken)
358
+ throw new Error("stale_owner");
359
+ if (goal.evidence.some((entry) => entry.intentId === attempt.intentId && entry.assessment.executionUnknown))
360
+ throw new Error("assessment requires explicit reconciliation");
361
+ const intent = goal.intents.find((entry) => entry.intentId === attempt.intentId);
362
+ const expected = goal.frozen.acceptance.criteria;
363
+ if (assessment.criteria.length !== expected.length || assessment.criteria.some((entry, index) => entry.acId !== expected[index].acId || entry.command !== expected[index].command ||
364
+ (entry.status === "succeeded" && entry.exitCode !== 0) ||
365
+ entry.outputHash !== createHash("sha256").update(JSON.stringify({ stdout: entry.stdout, stderr: entry.stderr, exitCode: entry.exitCode })).digest("hex")))
366
+ throw new Error("assessment frozen AC/output binding mismatch");
367
+ if (assessment.before.base !== goal.frozen.frozenBase || assessment.after.base !== goal.frozen.frozenBase ||
368
+ Date.parse(assessment.startedAt) < Date.parse(attempt.startedAt) || Date.parse(assessment.finishedAt) < Date.parse(assessment.startedAt))
369
+ throw new Error("assessment base/time binding mismatch");
370
+ const current = await captureScheduledGoalTree(goal.frozen.worktreeRef, goal.frozen.frozenBase, goal.frozen.allowedPaths, goal.frozen.forbiddenPaths);
371
+ now ??= new Date();
372
+ if (Date.parse(assessment.finishedAt) > now.getTime())
373
+ throw new Error("assessment finish time is in the future");
374
+ const uncertain = assessment.executionUnknown || assessment.criteria.some((entry) => entry.reason === "command_execution_unknown");
375
+ assessment.executionUnknown = uncertain;
376
+ const stale = assessment.status === "stale" || assessment.before.treeHash !== assessment.after.treeHash || current.treeHash !== assessment.after.treeHash;
377
+ const expired = now.getTime() >= Math.min(Date.parse(goal.frozen.time.stopAtUtc), Date.parse(goal.frozen.time.expiresAt));
378
+ const scopeFailure = current.scopeViolations.length > 0 || assessment.before.scopeViolations.length > 0;
379
+ assessment.status = uncertain ? "pending" : stale ? "stale" : scopeFailure ? "failed" : expired ? "pending"
380
+ : assessment.criteria.every((entry) => entry.status === "succeeded" && entry.exitCode === 0) ? "succeeded"
381
+ : assessment.criteria.some((entry) => entry.status === "pending") ? "pending" : "failed";
382
+ const payload = { goalId: goal.goalId, goalRevision: goal.goalRevision, envelopeHash: goal.envelopeHash, intentId: intent.intentId,
383
+ taskRevision: intent.taskRevision, turnSeq: intent.turnSeq, operationId: intent.operationId, runId: intent.runId,
384
+ assessment, committedTree: current, recordedAt: now.toISOString() };
385
+ goal.evidence.push({ evidenceId: scheduledGoalHash(payload), ...payload });
386
+ goal.acceptanceState = goal.state === "cancelled" ? "pending" : assessment.status === "succeeded" ? "accepted" : assessment.status;
387
+ if (!uncertain)
388
+ delete goal.assessmentAttempt;
389
+ if (goal.state !== "cancelled") {
390
+ if (uncertain) {
391
+ goal.state = "unknown";
392
+ goal.reasonCode = "assessment_execution_unknown";
393
+ }
394
+ else if (scopeFailure) {
395
+ goal.state = "blocked";
396
+ goal.reasonCode = "scope_violation";
397
+ }
398
+ else if (assessment.status === "succeeded") {
399
+ goal.state = "accepted";
400
+ goal.deliveryState = "pending";
401
+ }
402
+ }
403
+ this.event(goal, `assessment_recorded:${assessment.status}`, now);
404
+ return goal;
405
+ });
406
+ }
407
+ /** Settle only idle work. In-flight/unknown ownership is never released by a clock tick. */
408
+ async settleIdleGoal(goalId, now) {
409
+ return this.transaction(async (state) => {
410
+ const goal = this.requireGoal(state, goalId);
411
+ if (!goal.approval || ["cancelled", "blocked", "budget_exhausted", "accepted", "unknown"].includes(goal.state))
412
+ return goal;
413
+ if (goal.assessmentAttempt || goal.intents.some(holdsWriterSlot) ||
414
+ goal.supervisions.some((entry) => ["requested", "preparing", "unknown"].includes(entry.state)))
415
+ return goal;
416
+ const latest = goal.evidence.at(-1);
417
+ let noProgress = false;
418
+ if (latest && goal.acceptanceState === "failed" && latest.intentId === goal.intents.at(-1)?.intentId) {
419
+ const tree = await captureScheduledGoalTree(goal.frozen.worktreeRef, goal.frozen.frozenBase, goal.frozen.allowedPaths, goal.frozen.forbiddenPaths);
420
+ if (tree.treeHash !== latest.committedTree.treeHash || tree.scopeViolations.length) {
421
+ goal.acceptanceState = "stale";
422
+ goal.reasonCode = "evidence_drift";
423
+ this.event(goal, "acceptance_invalidated:tree_changed", now ?? new Date());
424
+ return goal;
425
+ }
426
+ const previous = [...goal.evidence].reverse().find((item) => item.intentId !== latest.intentId);
427
+ const outcomes = (item) => item.assessment.criteria.map((ac) => [ac.acId, ac.status, ac.exitCode]);
428
+ noProgress = Boolean(previous && previous.assessment.status === "failed" && latest.assessment.status === "failed" &&
429
+ previous.committedTree.treeHash === latest.committedTree.treeHash && scheduledGoalHash(outcomes(previous)) === scheduledGoalHash(outcomes(latest)));
430
+ }
431
+ now ??= new Date();
432
+ const wake = evaluateScheduledGoalWake(goal.frozen.time, now);
433
+ const prepared = goal.intents.some((intent) => intent.state === "prepared");
434
+ const needsAdmission = prepared || !goal.intents.length || goal.acceptanceState === "failed";
435
+ // latestStart/windows constrain a new run, not acceptance of a run already finished.
436
+ let reason = now.getTime() >= Date.parse(goal.frozen.time.expiresAt) ? "authorization_expired"
437
+ : now.getTime() >= Date.parse(goal.frozen.time.stopAtUtc) ? "deadline_reached"
438
+ : needsAdmission && wake.kind === "blocked" ? wake.reason : undefined;
439
+ if (!reason && noProgress)
440
+ reason = "no_progress";
441
+ const needsSupervision = goal.acceptanceState === "failed" && !goal.supervisions.some((entry) => entry.state === "proposed");
442
+ if (!reason && needsAdmission && !prepared && (goal.used.turns >= goal.frozen.budget.maxTurns || goal.used.dagRuns >= goal.frozen.budget.maxDagRuns ||
443
+ (needsSupervision && goal.used.supervisorCalls >= goal.frozen.budget.maxSupervisorCalls)))
444
+ reason = "budget_exhausted";
445
+ if (!reason)
446
+ return goal;
447
+ goal.state = reason === "budget_exhausted" ? "budget_exhausted" : "blocked";
448
+ goal.reasonCode = reason;
449
+ for (const intent of goal.intents)
450
+ if (intent.state === "prepared")
451
+ intent.state = "terminal";
452
+ for (const entry of goal.supervisions)
453
+ if (entry.state === "proposed")
454
+ entry.state = "stopped";
455
+ this.event(goal, reason, now);
456
+ return goal;
457
+ });
458
+ }
459
+ /** Reserve before calling a tool-less model; a crash never repeats the call. */
460
+ async beginSupervision(goalId, expectedVersion, now) {
461
+ return this.transaction(async (state) => {
462
+ const goal = this.requireGoal(state, goalId);
463
+ if (goal.stateVersion !== expectedVersion || goal.state !== "admitted" || goal.acceptanceState !== "failed")
464
+ throw new Error("supervision requires current failed acceptance");
465
+ if (state.goals.some(holdsResource))
466
+ throw new Error("supervision resource busy");
467
+ const evidence = goal.evidence.at(-1);
468
+ const parent = goal.intents.at(-1);
469
+ if (!goal.approval || !evidence || !parent || parent.state !== "terminal" || evidence.intentId !== parent.intentId ||
470
+ evidence.assessment.status !== "failed" || evidence.assessment.executionUnknown)
471
+ throw new Error("supervision parent binding mismatch");
472
+ if (goal.used.supervisorCalls >= goal.frozen.budget.maxSupervisorCalls || goal.used.turns >= goal.frozen.budget.maxTurns ||
473
+ goal.used.dagRuns >= goal.frozen.budget.maxDagRuns)
474
+ throw new Error("supervision budget exhausted");
475
+ // No reliable aggregate DAG token bound exists yet. Never treat its unknown usage as zero.
476
+ if (goal.frozen.budget.maxTokens !== undefined)
477
+ throw new Error("token_usage_unknown");
478
+ const tree = await captureScheduledGoalTree(goal.frozen.worktreeRef, goal.frozen.frozenBase, goal.frozen.allowedPaths, goal.frozen.forbiddenPaths);
479
+ now ??= new Date();
480
+ if (evaluateScheduledGoalWake(goal.frozen.time, now).kind !== "due")
481
+ throw new Error("supervision outside authorization window");
482
+ if (tree.treeHash !== evidence.committedTree.treeHash || tree.scopeViolations.length)
483
+ throw new Error("supervision evidence drift");
484
+ const taskRevision = parent.taskRevision + 1;
485
+ const turnSeq = parent.turnSeq + 1;
486
+ const supervisionId = scheduledGoalHash([goalId, goal.goalRevision, evidence.evidenceId, taskRevision, turnSeq]);
487
+ const entry = { supervisionId, ownerToken: randomUUID(), ownerPid: process.pid, parentEvidenceId: evidence.evidenceId, treeHash: tree.treeHash,
488
+ managedTaskId: `${goalId}-r${taskRevision}`, taskRevision, turnSeq, state: "requested", startedAt: now.toISOString() };
489
+ goal.used.supervisorCalls++;
490
+ goal.supervisions.push(entry);
491
+ this.event(goal, "supervision_reserved", now);
492
+ return entry;
493
+ });
494
+ }
495
+ async recordSupervisionResponse(goalId, ownerToken, text, modelRef, usageTokens) {
496
+ if (text.length > 64000 || !modelRef.trim() || (usageTokens !== null && (!Number.isSafeInteger(usageTokens) || usageTokens < 0)))
497
+ throw new Error("invalid supervision response");
498
+ return this.transaction((state) => {
499
+ const goal = this.requireGoal(state, goalId);
500
+ const entry = goal.supervisions.at(-1);
501
+ if (!entry || entry.ownerToken !== ownerToken || entry.state !== "requested")
502
+ throw new Error("stale_owner");
503
+ const response = { text, hash: scheduledGoalHash(text), modelRef };
504
+ if (entry.response && scheduledGoalHash(entry.response) !== scheduledGoalHash(response))
505
+ throw new Error("supervision response mismatch");
506
+ entry.response = response;
507
+ entry.usageTokens = usageTokens;
508
+ this.event(goal, "supervision_response_recorded", new Date());
509
+ });
510
+ }
511
+ async finishSupervision(goalId, ownerToken, input, usageTokens, now = new Date()) {
512
+ const proposal = scheduledGoalProposalSchema.parse(input);
513
+ if (usageTokens !== null && (!Number.isSafeInteger(usageTokens) || usageTokens < 0))
514
+ throw new Error("invalid model usage");
515
+ return this.transaction((state) => {
516
+ const goal = this.requireGoal(state, goalId);
517
+ const entry = goal.supervisions.at(-1);
518
+ if (!entry || entry.ownerToken !== ownerToken || entry.state !== "requested")
519
+ throw new Error("stale_owner");
520
+ const evidence = goal.evidence.find((item) => item.evidenceId === entry.parentEvidenceId);
521
+ const failedIds = evidence.assessment.criteria.filter((item) => item.status === "failed").map((item) => item.acId);
522
+ if (proposal.action === "continue" && (!proposal.instructions.trim() || !proposal.gapAcIds.length ||
523
+ (!proposal.gapAcIds.some((id) => failedIds.includes(id)) || proposal.gapAcIds.some((id) => !goal.frozen.acceptance.criteria.some((ac) => ac.acId === id)))))
524
+ throw new Error("proposal must address frozen failed AC");
525
+ entry.proposal = proposal;
526
+ entry.usageTokens = usageTokens;
527
+ entry.finishedAt = now.toISOString();
528
+ entry.state = goal.state === "cancelled" || proposal.action === "stop" ? "stopped" : "proposed";
529
+ if (proposal.action === "stop" && goal.state !== "cancelled") {
530
+ goal.state = "blocked";
531
+ goal.reasonCode = "supervisor_stopped";
532
+ }
533
+ this.event(goal, `supervision_${entry.state}`, now);
534
+ return entry;
535
+ });
536
+ }
537
+ async beginDerivedPreparation(goalId, ownerToken, now = new Date()) {
538
+ return this.transaction((state) => {
539
+ const goal = this.requireGoal(state, goalId);
540
+ const entry = goal.supervisions.at(-1);
541
+ if (!entry || entry.ownerToken !== ownerToken)
542
+ throw new Error("stale_owner");
543
+ if (goal.state !== "admitted" || entry.state !== "proposed")
544
+ return false;
545
+ if (evaluateScheduledGoalWake(goal.frozen.time, now).kind !== "due")
546
+ return false;
547
+ entry.state = "preparing";
548
+ entry.ownerPid = process.pid;
549
+ this.event(goal, "derived_preparation_started", now);
550
+ return true;
551
+ });
552
+ }
553
+ async failSupervision(goalId, ownerToken, now = new Date()) {
554
+ return this.transaction((state) => {
555
+ const goal = this.requireGoal(state, goalId);
556
+ const entry = goal.supervisions.at(-1);
557
+ if (!entry || entry.ownerToken !== ownerToken || !["requested", "proposed", "preparing"].includes(entry.state))
558
+ throw new Error("stale_owner");
559
+ entry.state = "unknown";
560
+ if (goal.state !== "cancelled") {
561
+ goal.state = "unknown";
562
+ goal.reasonCode = "supervision_unknown";
563
+ }
564
+ this.event(goal, "supervision_unknown", now);
565
+ });
566
+ }
567
+ /** The caller validates the new managed gate through the pinned CLI before admission. */
568
+ async admitDerivedTask(goalId, ownerToken, input, now) {
569
+ const draft = scheduledGoalDraftSchema.parse(input);
570
+ return this.transaction(async (state) => {
571
+ const goal = this.requireGoal(state, goalId);
572
+ const entry = goal.supervisions.at(-1);
573
+ if (!entry || entry.ownerToken !== ownerToken)
574
+ throw new Error("stale_owner");
575
+ const existing = goal.intents.find((intent) => intent.derived?.supervisionId === entry.supervisionId);
576
+ if (existing) {
577
+ if (scheduledGoalHash(existing.derived?.prepared) !== scheduledGoalHash(draft.prepared) || existing.derived?.managedTaskId !== draft.managedTaskId)
578
+ throw new Error("derived request mismatch");
579
+ return existing;
580
+ }
581
+ if (goal.state !== "admitted" || !goal.approval || entry.state !== "preparing" || entry.proposal?.action !== "continue")
582
+ throw new Error("derived admission not authorized");
583
+ const envelope = ({ managedTaskId: _task, prepared: _prepared, ...rest }) => rest;
584
+ if (draft.managedTaskId !== entry.managedTaskId || scheduledGoalHash(envelope(draft)) !== scheduledGoalHash(envelope(goal.frozen)) ||
585
+ draft.prepared.gateToken === goal.frozen.prepared.gateToken)
586
+ throw new Error("derived envelope/gate mismatch");
587
+ if (state.goals.some((item) => item.goalId !== goalId && holdsResource(item)) || goal.assessmentAttempt || goal.intents.some((item) => item.state !== "terminal"))
588
+ throw new Error("derived resource busy");
589
+ if (goal.used.turns >= goal.frozen.budget.maxTurns || goal.used.dagRuns >= goal.frozen.budget.maxDagRuns)
590
+ throw new Error("derived budget exhausted");
591
+ if (goal.evidence.at(-1)?.evidenceId !== entry.parentEvidenceId)
592
+ throw new Error("derived parent evidence drift");
593
+ const tree = await captureScheduledGoalTree(goal.frozen.worktreeRef, goal.frozen.frozenBase, goal.frozen.allowedPaths, goal.frozen.forbiddenPaths);
594
+ now ??= new Date();
595
+ if (evaluateScheduledGoalWake(goal.frozen.time, now).kind !== "due")
596
+ throw new Error("derived outside authorization window");
597
+ if (tree.treeHash !== entry.treeHash || tree.scopeViolations.length)
598
+ throw new Error("derived tree drift");
599
+ const key = scheduledGoalHash([goalId, goal.goalRevision, entry.taskRevision, entry.turnSeq, "execute", goal.envelopeHash, entry.parentEvidenceId]);
600
+ const intent = {
601
+ intentId: `intent-${key}`, taskRevision: entry.taskRevision, turnSeq: entry.turnSeq, epoch: 1, ownerToken: randomUUID(),
602
+ idempotencyKey: key, state: "prepared", operationId: `op_${key.slice(0, 32)}`, runId: scheduledGoalRunId(goal, key),
603
+ createdAt: now.toISOString(), observations: [], derived: { supervisionId: entry.supervisionId,
604
+ parentEvidenceId: entry.parentEvidenceId, treeHash: tree.treeHash, managedTaskId: draft.managedTaskId, prepared: draft.prepared },
605
+ };
606
+ goal.intents.push(intent);
607
+ goal.used.turns++;
608
+ goal.used.dagRuns++;
609
+ entry.state = "admitted";
610
+ goal.acceptanceState = "not_started";
611
+ this.event(goal, "derived_intent_prepared", now);
612
+ return intent;
613
+ });
614
+ }
615
+ requireGoal(state, goalId) {
616
+ const goal = state.goals.find((item) => item.goalId === goalId);
617
+ if (!goal)
618
+ throw new Error("scheduled goal not found");
619
+ const hash = scheduledGoalHash({ repoRef: goal.repoRef, frozen: goal.frozen, goalRevision: goal.goalRevision });
620
+ if (hash !== goal.envelopeHash)
621
+ throw new Error("scheduled goal envelope drift");
622
+ for (const entry of goal.supervisions)
623
+ if (entry.response && scheduledGoalHash(entry.response.text) !== entry.response.hash)
624
+ throw new Error("supervision response drift");
625
+ for (const { evidenceId, ...payload } of goal.evidence)
626
+ if (scheduledGoalHash(payload) !== evidenceId)
627
+ throw new Error("scheduled goal evidence drift");
628
+ return goal;
629
+ }
630
+ event(goal, event, now) {
631
+ goal.stateVersion++;
632
+ goal.journal.push({ version: goal.stateVersion, at: now.toISOString(), event });
633
+ }
634
+ async read() {
635
+ let bytes;
636
+ try {
637
+ bytes = await readFile(path.join(this.root, "state.json"), "utf8");
638
+ }
639
+ catch (error) {
640
+ if (error.code === "ENOENT")
641
+ return { schemaVersion: 1, goals: [] };
642
+ throw error;
643
+ }
644
+ const state = stateSchema.parse(JSON.parse(bytes));
645
+ const repoRef = await realpath(this.repoRoot);
646
+ for (const goal of state.goals) {
647
+ if (goal.repoRef !== repoRef || goal.resourceKey !== scheduledGoalHash([repoRef, goal.frozen.managedTaskId]))
648
+ throw new Error("scheduled goal repository binding mismatch");
649
+ this.requireGoal(state, goal.goalId);
650
+ }
651
+ return state;
652
+ }
653
+ async transaction(mutate) {
654
+ await mkdir(this.root, { recursive: true, mode: 0o700 });
655
+ // The first ledger commit must also persist newly created .harness and
656
+ // scheduled-goals directory entries, not only state.json's rename.
657
+ await syncDirectory(path.dirname(this.root));
658
+ await syncDirectory(path.dirname(path.dirname(this.root)));
659
+ const lockPath = path.join(this.root, "mutation.lock");
660
+ const lock = await open(lockPath, "wx", 0o600).catch((error) => {
661
+ if (error.code === "EEXIST")
662
+ throw new Error("scheduled goal store locked: retry or reconcile owner; never steal");
663
+ throw error;
664
+ });
665
+ let committed = false;
666
+ let mutationStarted = false;
667
+ try {
668
+ await lock.writeFile(JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString() }));
669
+ await lock.sync();
670
+ const state = await this.read();
671
+ const result = await mutate(state);
672
+ const bytes = JSON.stringify(stateSchema.parse(state));
673
+ mutationStarted = true;
674
+ await this.persist(bytes);
675
+ committed = true;
676
+ return result;
677
+ }
678
+ finally {
679
+ await lock.close();
680
+ // Ambiguous filesystem failure retains the lock and durable facts for reconcile.
681
+ if (committed || !mutationStarted)
682
+ await unlink(lockPath);
683
+ }
684
+ }
685
+ async persist(bytes) {
686
+ const temp = path.join(this.root, `state-${randomUUID()}.tmp`);
687
+ const file = await open(temp, "wx", 0o600);
688
+ try {
689
+ await file.writeFile(bytes);
690
+ await file.sync();
691
+ }
692
+ finally {
693
+ await file.close();
694
+ }
695
+ await rename(temp, path.join(this.root, "state.json"));
696
+ // fsync directory makes the rename durable on supported local POSIX filesystems.
697
+ await syncDirectory(this.root);
698
+ }
699
+ }