@tea-agent/loop-agent 0.43.0-next.17 → 0.43.0-next.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +68 -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/cursor-prompt.js +2 -0
  12. package/dist/commands/dag-approve.js +2 -0
  13. package/dist/commands/dag-final-verification.js +2 -0
  14. package/dist/commands/dag-init-hybrid.js +2 -0
  15. package/dist/commands/dag-reconcile-tasks.js +2 -0
  16. package/dist/commands/dag-reject.js +2 -0
  17. package/dist/commands/dag-report.js +2 -0
  18. package/dist/commands/dag-rerun.js +2 -0
  19. package/dist/commands/dag-resume.js +2 -0
  20. package/dist/commands/dag-workflow-compile.js +2 -0
  21. package/dist/commands/dag-workflow-plan.js +2 -0
  22. package/dist/commands/dag-workflow-validate.js +2 -0
  23. package/dist/commands/delegate.js +2 -0
  24. package/dist/commands/examples.js +2 -0
  25. package/dist/commands/import-prd.js +2 -0
  26. package/dist/commands/init-model-catalog.js +2 -0
  27. package/dist/commands/init.js +19 -13
  28. package/dist/commands/instructions.js +2 -0
  29. package/dist/commands/knowledge.js +2 -0
  30. package/dist/commands/loop-benchmark.js +2 -0
  31. package/dist/commands/pi-prompt.js +2 -0
  32. package/dist/commands/pi-reuse-benchmark.js +2 -0
  33. package/dist/commands/promote-run.js +2 -0
  34. package/dist/commands/stats.js +1 -1
  35. package/dist/commands/study-init.js +2 -0
  36. package/dist/commands/task-advance.js +34 -0
  37. package/dist/commands/task-source-prepare.js +2 -0
  38. package/dist/commands/worktree-create.js +2 -0
  39. package/dist/commands/worktree-remove.js +2 -0
  40. package/dist/executors/dag-pi-executor.js +1640 -194
  41. package/dist/executors/pi-executor.js +85 -3
  42. package/dist/executors/pi-sdk-executor.js +18 -0
  43. package/dist/executors/shell-executor.js +92 -7
  44. package/dist/executors/shell-verification.js +3 -0
  45. package/dist/governance/exec-plans.js +6 -3
  46. package/dist/infrastructure/console/app-data.js +6 -0
  47. package/dist/shared/artifacts-core.js +2 -0
  48. package/dist/shared/backend-dogfood-preflight.js +47 -0
  49. package/dist/shared/dag-failure-category.js +3 -0
  50. package/dist/shared/git-progress.js +2 -0
  51. package/dist/shared/one-shot-prompt-args.js +2 -0
  52. package/dist/shared/operator/capabilities.js +180 -0
  53. package/dist/shared/package-metadata.js +6 -4
  54. package/dist/shared/pi-context-pressure/sift-bridge.js +1 -1
  55. package/dist/shared/reference-context.js +2 -0
  56. package/dist/task/config-types.js +29 -0
  57. package/dist/task/contract/project.js +9 -0
  58. package/dist/task/contract/recover.js +6 -0
  59. package/dist/task/contract/schema.js +17 -0
  60. package/dist/task/source-prepare/artifact-meta.js +9 -2
  61. package/dist/task/source-prepare/build-draft.js +60 -0
  62. package/dist/task/source-prepare/fragment-inventory.js +4 -1
  63. package/dist/task/source-prepare/parse-intent.js +36 -11
  64. package/dist/task/task-demand-routing.js +5 -2
  65. package/dist/worker/console/chat/browser-automation.js +468 -308
  66. package/dist/worker/console/chat/browser-routes.js +175 -57
  67. package/dist/worker/console/chat/explore-tools.js +5 -2
  68. package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
  69. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +115 -20
  70. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +24 -16
  71. package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
  72. package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
  73. package/dist/worker/console/chat/pi-runtime.js +95 -28
  74. package/dist/worker/console/chat/routes.js +20 -1
  75. package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
  76. package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
  77. package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
  78. package/dist/worker/console/chat/sdd-data-alignment.js +1 -1
  79. package/dist/worker/console/chat/session-mode.js +8 -4
  80. package/dist/worker/console/chat/session-store.js +42 -9
  81. package/dist/worker/console/chat/tool-preview.js +115 -0
  82. package/dist/worker/console/chat/tools.js +7 -1
  83. package/dist/worker/console/chat/turn-order.js +13 -0
  84. package/dist/worker/console/chat/turn-process.js +32 -30
  85. package/dist/worker/console/operator-actions.js +50 -0
  86. package/dist/worker/console/prd-intake-bridge.js +54 -1
  87. package/dist/worker/console/scheduled-goal-host.js +98 -0
  88. package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
  89. package/dist/worker/console/server.js +15 -0
  90. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4NXyedy.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
  91. package/dist/worker/console/static/assets/{arc-NZ3qXvBI.js → arc-eoQiMvuk.js} +1 -1
  92. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-COZSRvml.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
  93. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Ck0Gxi9C.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
  94. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BX5HQ4Ox.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
  95. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
  96. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CoviemSL.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DbOdOkbZ.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BsG2pmdQ.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DGdvrNm8.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
  100. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAy7Yd6y.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
  101. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D9UA7hUb.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
  102. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DPz7SZ8V.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BmuEOIh7.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CIhBWnd8.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CScR6mDD.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
  106. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
  107. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
  108. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BjpfWaUr.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
  109. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BFQohJgT.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
  110. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D3gzRJnS.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
  111. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-5eojkhoA.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
  112. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CpD8LWu_.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
  113. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B8_4qJ2a.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
  114. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-HOsXf0eu.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
  115. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BqwRwiWP.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
  116. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BRVCNZe1.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
  117. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Cvb1TgGh.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
  118. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DS0O0qG9.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
  119. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BHIRvRr3.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
  120. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DcCJ1JWx.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
  121. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-B9ia5ony.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
  122. package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
  123. package/dist/worker/console/static/assets/{index-wXlQohNs.js → index-Cdkvw_H6.js} +97 -97
  124. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-3ogpgegn.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
  125. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-4tgpQ5SL.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
  126. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DNfgHqFx.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
  127. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Dz7ngC8z.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
  128. package/dist/worker/console/static/assets/{linear-DI7Jv62E.js → linear-BDZ9riWi.js} +1 -1
  129. package/dist/worker/console/static/assets/{mermaid.core-CQA4Nzmo.js → mermaid.core-7pKqYtpZ.js} +5 -5
  130. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CtsMc8Mf.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
  131. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-ByppE9yH.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
  132. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-B5PtgFgt.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
  133. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cq4p0k2K.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
  134. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DjfkyljN.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
  135. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DzynNRSl.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
  136. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cj7TCpTJ.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
  137. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C73C6ykA.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
  138. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BiNLgN2x.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
  139. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-UwvjS64-.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
  140. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
  141. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CC7DH98r.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
  142. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
  143. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-LSBXTR70.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
  144. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BAuxWwGP.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
  145. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y--E5_gjZx.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
  146. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-dJWNmyzF.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
  147. package/dist/worker/console/static/index.html +2 -2
  148. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
  149. package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
  150. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +15 -5
  151. package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
  152. package/dist/worker/console/static-src/operator-chat/useChatThread.js +2 -5
  153. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +2 -0
  154. package/dist/worker/console/static-src/prd-file-import.js +3 -1
  155. package/dist/worker/console/workspace-context.js +17 -1
  156. package/dist/worker/delivery/git-transaction.js +2 -0
  157. package/dist/worker/feature/fullstack-validate.js +2 -0
  158. package/dist/worker/loop-agent/loop-agent-client.js +8 -2
  159. package/dist/worker/materialize/harness-task-materializer.js +4 -1
  160. package/dist/worker/metrics/projector.js +1 -1
  161. package/dist/worker/observability/event-store.js +4 -1
  162. package/dist/worker/observability/read-model.js +4 -1
  163. package/dist/worker/observe/node-transparency.js +23 -1
  164. package/dist/worker/observe/routes.js +4 -0
  165. package/dist/worker/observe/static/operator-chrome.js +3 -1
  166. package/dist/worker/outcomes/declared-artifacts.js +2 -0
  167. package/dist/worker/preflight.js +3 -0
  168. package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
  169. package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
  170. package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
  171. package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
  172. package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
  173. package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
  174. package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
  175. package/dist/worker/task-spec/validate.js +4 -1
  176. package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
  177. package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
  178. package/dist/workflows/dag/dag-retry-schema.js +11 -0
  179. package/dist/workflows/dag/decision-envelope.js +2 -0
  180. package/dist/workflows/dag/dynamic-runtime/shared.js +2 -2
  181. package/dist/workflows/dag/frontend-closeout.js +4 -2
  182. package/dist/workflows/dag/frontend-committed-facts.js +461 -0
  183. package/dist/workflows/dag/frontend-durable-tools.js +15 -3
  184. package/dist/workflows/dag/frontend-implementation-contract.js +365 -8
  185. package/dist/workflows/dag/frontend-plan-canary.js +53 -0
  186. package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
  187. package/dist/workflows/dag/frontend-plan-render.js +0 -2
  188. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  189. package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
  190. package/dist/workflows/dag/frontend-recovery-run.js +57 -0
  191. package/dist/workflows/dag/frontend-review-context.js +43 -70
  192. package/dist/workflows/dag/frontend-risk.js +92 -10
  193. package/dist/workflows/dag/frontend-session-budget.js +117 -3
  194. package/dist/workflows/dag/frontend-shape.js +11 -55
  195. package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
  196. package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
  197. package/dist/workflows/dag/frontend-verification-trace.js +25 -12
  198. package/dist/workflows/dag/frontend-writer-admission.js +3 -47
  199. package/dist/workflows/dag/frontend-writer-status.js +0 -23
  200. package/dist/workflows/dag/init-hybrid.js +202 -111
  201. package/dist/workflows/dag/interrupt-request.js +2 -0
  202. package/dist/workflows/dag/lifecycle.js +11 -2
  203. package/dist/workflows/dag/node-execution.js +50 -13
  204. package/dist/workflows/dag/reconcile-run.js +2 -0
  205. package/dist/workflows/dag/repair-artifact.js +3 -1
  206. package/dist/workflows/dag/report.js +2 -0
  207. package/dist/workflows/dag/rerun-plan.js +10 -0
  208. package/dist/workflows/dag/rerun-task.js +79 -1
  209. package/dist/workflows/dag/retry-policy.js +18 -0
  210. package/dist/workflows/dag/scheduler.js +2 -4
  211. package/dist/workflows/dag/types.js +44 -6
  212. package/dist/workflows/dag/validate.js +4 -0
  213. package/docs/README.md +1 -0
  214. package/docs/governance/README.md +1 -0
  215. package/docs/init-surface.manifest.json +1 -0
  216. package/docs/operations/README.md +2 -0
  217. package/docs/templates/README.md +1 -1
  218. package/docs/templates/agent-dag.schema.json +2 -2
  219. package/docs/templates/backend-test-dag.json +14 -10
  220. package/docs/templates/frontend-implementation-contract.schema.json +0 -7
  221. package/docs/templates/frontend-implementation-dag.json +8 -9
  222. package/package.json +8 -3
  223. package/skills/frontend-bounded-implement/SKILL.md +3 -4
  224. package/skills/frontend-design-review/SKILL.md +7 -12
  225. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  226. package/skills/frontend-plan/SKILL.md +8 -18
  227. package/skills/frontend-plan/references/decision-contract.md +19 -26
  228. package/skills/frontend-review/SKILL.md +21 -25
  229. package/skills/frontend-review/references/review-findings.md +48 -24
  230. package/dist/worker/console/static/assets/channel-BxdTlb_5.js +0 -1
  231. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-_ByA1Nf8.js +0 -1
  232. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-_ByA1Nf8.js +0 -1
  233. package/dist/worker/console/static/assets/index-B9goN3S5.css +0 -1
  234. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-nLgMZ0D3.js +0 -1
  235. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BcYqTady.js +0 -8
  236. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
@@ -1,3 +1,6 @@
1
+ import { deliverScheduledGoalResult } from "./chat/scheduled-goal-delivery.js";
2
+ import { ScheduledGoalHost } from "./scheduled-goal-host.js";
3
+ import { ScheduledGoalStore } from "../scheduler/scheduled-goal-store.js";
1
4
  /**
2
5
  * Repo-scoped Workspace Context (2026-08-22 multi-workspace foundation, W1).
3
6
  *
@@ -298,7 +301,11 @@ export async function createWorkspaceContext(record, deps) {
298
301
  // Workspace-scoped git source control shared by the model tool (scm) and
299
302
  // the Console UI SCM panel (HTTP API).
300
303
  const scmService = new ScmService(repoRoot);
301
- const browserAutomation = new BrowserAutomationService();
304
+ const browserAutomation = new BrowserAutomationService({
305
+ profileDir: appData.browserProfile,
306
+ downloadsDir: appData.browserDownloads,
307
+ screenshotsDir: appData.browserScreenshots,
308
+ });
302
309
  const chatRuntime = new ConsolePiRuntime({
303
310
  cwd: repoRoot,
304
311
  sessionDir: appData.chatSessions,
@@ -336,6 +343,12 @@ export async function createWorkspaceContext(record, deps) {
336
343
  detachSession: (sessionId) => registry.detachSession(record.workspaceId, sessionId),
337
344
  };
338
345
  const nightAuxTicker = new NightAuxTicker({ repoRoot, client });
346
+ const scheduledGoals = new ScheduledGoalStore(repoRoot);
347
+ const scheduledGoalHost = new ScheduledGoalHost({
348
+ goals: scheduledGoals, client, operations, events,
349
+ deliver: (goal) => deliverScheduledGoalResult(goal, scheduledGoals, chatStore),
350
+ onError: (goalId, error) => process.stderr.write(`[scheduled-goal] ${goalId ?? "wake"}: ${error instanceof Error ? error.message : String(error)}\n`),
351
+ });
339
352
  const observeRouteContext = createObserveRouteContext(repoRoot, {
340
353
  staticDir: deps.inspectStaticDir,
341
354
  });
@@ -370,6 +383,7 @@ export async function createWorkspaceContext(record, deps) {
370
383
  if (disposed)
371
384
  return;
372
385
  disposed = true;
386
+ scheduledGoalHost.stop();
373
387
  try {
374
388
  goalRoundDriver.dispose();
375
389
  }
@@ -388,6 +402,7 @@ export async function createWorkspaceContext(record, deps) {
388
402
  catch {
389
403
  // best-effort
390
404
  }
405
+ void browserAutomation.dispose();
391
406
  try {
392
407
  nightAuxTicker.dispose();
393
408
  }
@@ -396,6 +411,7 @@ export async function createWorkspaceContext(record, deps) {
396
411
  }
397
412
  },
398
413
  };
414
+ scheduledGoalHost.start();
399
415
  return context;
400
416
  }
401
417
  /** Inspect-facing readiness/capability summary for a registry record. */
@@ -360,6 +360,8 @@ function matchesGlob(filePath, glob) {
360
360
  let pattern = "^";
361
361
  for (let index = 0; index < normalizedGlob.length; index += 1) {
362
362
  const char = normalizedGlob[index];
363
+ if (char === undefined)
364
+ continue;
363
365
  if (char === "*" && normalizedGlob[index + 1] === "*") {
364
366
  pattern += ".*";
365
367
  index += 1;
@@ -204,6 +204,8 @@ export function validateFullstackStructure(input) {
204
204
  for (let j = i + 1; j < writerNodes.length; j++) {
205
205
  const a = writerNodes[i];
206
206
  const b = writerNodes[j];
207
+ if (!a || !b)
208
+ continue;
207
209
  const aPaths = collectWritePaths(taskSpecs.get(a.id));
208
210
  const bPaths = collectWritePaths(taskSpecs.get(b.id));
209
211
  if (!writeSetsOverlap(aPaths, bPaths))
@@ -310,7 +310,10 @@ function findSourceScript(baseArgs, cwd) {
310
310
  // Look for the last .ts/.js/.mjs/.cjs file in baseArgs
311
311
  let scriptIdx = -1;
312
312
  for (let i = baseArgs.length - 1; i >= 0; i--) {
313
- const ext = path.extname(baseArgs[i]).toLowerCase();
313
+ const candidate = baseArgs[i];
314
+ if (candidate === undefined)
315
+ continue;
316
+ const ext = path.extname(candidate).toLowerCase();
314
317
  if (ext === ".ts" || ext === ".js" || ext === ".mjs" || ext === ".cjs") {
315
318
  scriptIdx = i;
316
319
  break;
@@ -318,7 +321,10 @@ function findSourceScript(baseArgs, cwd) {
318
321
  }
319
322
  if (scriptIdx < 0)
320
323
  return undefined;
321
- const scriptPath = path.resolve(cwd, baseArgs[scriptIdx]);
324
+ const scriptArg = baseArgs[scriptIdx];
325
+ if (scriptArg === undefined)
326
+ return undefined;
327
+ const scriptPath = path.resolve(cwd, scriptArg);
322
328
  try {
323
329
  const s = statSync(scriptPath);
324
330
  if (s.isFile())
@@ -551,7 +551,10 @@ export function indexAcceptanceItems(raw) {
551
551
  if (!match)
552
552
  continue;
553
553
  const id = match[1];
554
- const summary = compactSummary(match[2]);
554
+ const body = match[2];
555
+ if (id === undefined || body === undefined)
556
+ continue;
557
+ const summary = compactSummary(body);
555
558
  if (!map.has(id))
556
559
  map.set(id, summary);
557
560
  }
@@ -110,7 +110,7 @@ export async function projectMonthlyMetrics(input) {
110
110
  }
111
111
  function ratio(numerator, denominator, missingData) { return { numerator, denominator, value: denominator ? numerator / denominator : null, sampleSize: denominator, missingData }; }
112
112
  function median(values) { if (!values.length)
113
- return null; const middle = Math.floor(values.length / 2); return values.length % 2 ? values[middle] : (values[middle - 1] + values[middle]) / 2; }
113
+ return null; const middle = Math.floor(values.length / 2); return values.length % 2 ? (values[middle] ?? 0) : ((values[middle - 1] ?? 0) + (values[middle] ?? 0)) / 2; }
114
114
  function inWindow(value, start, end) { const time = new Date(value).getTime(); return time >= start.getTime() && time < end.getTime(); }
115
115
  function dedupeRuns(runs) { return [...new Map(runs.map((run) => [run.workerRunId, run])).values()]; }
116
116
  async function countBoundaryInterceptions(root, start, end) { let interceptions = 0; let audited = 0; try {
@@ -11,7 +11,10 @@ function serializeEvent(event) {
11
11
  }
12
12
  /** Identifiers used as event-store path segments must not alter the route. */
13
13
  export function isSafeObservabilityIdentifier(value) {
14
- return value !== "." && value !== ".." && !/[\\/\0]/.test(value);
14
+ return (typeof value === "string" &&
15
+ value !== "." &&
16
+ value !== ".." &&
17
+ !/[\\/\0]/.test(value));
15
18
  }
16
19
  export function createWorkerEventStore(jsonlPath) {
17
20
  return {
@@ -1444,8 +1444,11 @@ async function loadFailureHandoffs(repoRoot) {
1444
1444
  const match = /^(.+)-failure-closeout-draft\.md$/.exec(entry.name);
1445
1445
  if (!match)
1446
1446
  continue;
1447
+ const workerRunId = match[1];
1448
+ if (workerRunId === undefined)
1449
+ continue;
1447
1450
  handoffs.push({
1448
- workerRunId: match[1],
1451
+ workerRunId,
1449
1452
  closeoutDraftPath: path.join(handoffDir, entry.name),
1450
1453
  });
1451
1454
  }
@@ -106,6 +106,12 @@ export async function loadDagNodeContextSummary(repoRoot, dagRunId, nodeId, atte
106
106
  body: { error: "Invalid node identifier", reason: "invalid-id" },
107
107
  };
108
108
  }
109
+ if (!isSafeObservabilityIdentifier(dagRunId)) {
110
+ return {
111
+ status: 400,
112
+ body: { error: "Invalid DAG run identifier", reason: "invalid-id" },
113
+ };
114
+ }
109
115
  const loaded = await loadRunFacts(repoRoot, dagRunId);
110
116
  if (!loaded.ok)
111
117
  return loaded.failure;
@@ -242,6 +248,12 @@ export async function loadDagNodeArtifactInventory(repoRoot, dagRunId, nodeId) {
242
248
  body: { error: "Invalid node identifier", reason: "invalid-id" },
243
249
  };
244
250
  }
251
+ if (!isSafeObservabilityIdentifier(dagRunId)) {
252
+ return {
253
+ status: 400,
254
+ body: { error: "Invalid DAG run identifier", reason: "invalid-id" },
255
+ };
256
+ }
245
257
  const loaded = await loadRunFacts(repoRoot, dagRunId);
246
258
  if (!loaded.ok)
247
259
  return loaded.failure;
@@ -436,6 +448,8 @@ async function loadVerifiedArtifactBytes(repoRoot, dagRunId, nodeId, artifactId)
436
448
  }
437
449
  }
438
450
  export async function loadDagNodeArtifactPreview(repoRoot, dagRunId, nodeId, artifactId) {
451
+ if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId))
452
+ return { status: 400, body: { error: "Invalid DAG run or node identifier", reason: "invalid-id" } };
439
453
  const verified = await loadVerifiedArtifactBytes(repoRoot, dagRunId, nodeId, artifactId);
440
454
  if (verified.status !== 200)
441
455
  return verified;
@@ -448,6 +462,8 @@ export async function loadDagNodeArtifactPreview(repoRoot, dagRunId, nodeId, art
448
462
  }
449
463
  /** Full text uses the same binding and safety checks as the bounded preview. */
450
464
  export async function loadDagNodeArtifactDownload(repoRoot, dagRunId, nodeId, artifactId) {
465
+ if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId))
466
+ return { status: 400, body: { error: "Invalid DAG run or node identifier", reason: "invalid-id" } };
451
467
  const verified = await loadVerifiedArtifactBytes(repoRoot, dagRunId, nodeId, artifactId);
452
468
  if (verified.status !== 200)
453
469
  return verified;
@@ -469,7 +485,7 @@ async function readValidatedContextReceipt(boundaryRoot, targetPath) {
469
485
  return effectiveNodeContextV1Schema.safeParse(null);
470
486
  }
471
487
  export async function loadDagNodeContextComponent(repoRoot, dagRunId, nodeId, attempt, componentIndex) {
472
- if (!isSafeObservabilityIdentifier(nodeId) || !Number.isSafeInteger(attempt) || attempt < 1 || !Number.isSafeInteger(componentIndex) || componentIndex < 0)
488
+ if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId) || !Number.isSafeInteger(attempt) || attempt < 1 || !Number.isSafeInteger(componentIndex) || componentIndex < 0)
473
489
  return { status: 400, body: { error: "Invalid context selection", reason: "invalid-id" } };
474
490
  const loaded = await loadRunFacts(repoRoot, dagRunId);
475
491
  if (!loaded.ok)
@@ -502,6 +518,12 @@ function stringField(value, key) {
502
518
  return typeof value?.[key] === "string" ? value[key] : undefined;
503
519
  }
504
520
  export async function loadDagRunLineage(repoRoot, dagRunId) {
521
+ if (!isSafeObservabilityIdentifier(dagRunId)) {
522
+ return {
523
+ status: 400,
524
+ body: { error: "Invalid DAG run identifier", reason: "invalid-id" },
525
+ };
526
+ }
505
527
  const loaded = await loadRunFacts(repoRoot, dagRunId);
506
528
  if (!loaded.ok)
507
529
  return loaded.failure;
@@ -835,6 +835,10 @@ async function handleDagNodeExecutionOutput(_req, res, match, ctx) {
835
835
  const dagRunId = match.params.id;
836
836
  const nodeId = match.params.sub;
837
837
  const key = match.query.get("key");
838
+ if (dagRunId === undefined || nodeId === undefined) {
839
+ sendJson(res, 400, { error: "Invalid DAG node execution output selection", reason: "invalid-id" });
840
+ return;
841
+ }
838
842
  // Reject path-shaped inputs even if smuggled via key/query aliases.
839
843
  if (match.query.has("path") || match.query.has("artifactPath")) {
840
844
  sendJson(res, 400, {
@@ -26,6 +26,7 @@ export const INSPECT_TABS = Object.freeze([
26
26
  { id: "dashboard", label: "总览", href: "/#/inspect" },
27
27
  { id: "pool", label: "资源池", href: "/#/inspect/pool" },
28
28
  { id: "failures", label: "异常 Task", href: "/#/inspect/failures" },
29
+ { id: "scheduled-goals", label: "定时任务", href: "/#/inspect/scheduled-goals" },
29
30
  ]);
30
31
 
31
32
  export const READINESS_LABELS = Object.freeze({
@@ -92,7 +93,7 @@ export function parseInspectObjectView(hashValue) {
92
93
 
93
94
  /**
94
95
  * Inspect 当前 hash 应高亮的工作区 tab href。
95
- * 三个一级集合视图映射到各自 tab;下钻/对象页(dag/dags/run/night 等)
96
+ * 一级集合视图映射到各自 tab;下钻/对象页(dag/dags/run/night 等)
96
97
  * 不高亮任何 tab——位置由面包屑与上下文 chip 表达(`#/dags` 自 0.33.0
97
98
  * 起是独立历史分页页,不再是总览滚动别名)。
98
99
  */
@@ -106,6 +107,7 @@ export function activeInspectTabHref(hashValue) {
106
107
  return "/#/inspect/pool";
107
108
  }
108
109
  if (internal === "/failures") return "/#/inspect/failures";
110
+ if (internal === "/scheduled-goals") return "/#/inspect/scheduled-goals";
109
111
  if (internal.startsWith("/feature/")) return "/#/inspect";
110
112
  return null;
111
113
  }
@@ -12,6 +12,8 @@ export function matchesPathGlob(filePath, glob) {
12
12
  let pattern = "^";
13
13
  for (let index = 0; index < normalizedGlob.length; index += 1) {
14
14
  const char = normalizedGlob[index];
15
+ if (char === undefined)
16
+ continue;
15
17
  if (char === "*" && normalizedGlob[index + 1] === "*") {
16
18
  pattern += ".*";
17
19
  index += 1;
@@ -120,6 +120,9 @@ export async function preflightTargetRepo(input) {
120
120
  }
121
121
  if (input.runCheckRepo) {
122
122
  const [command, ...args] = input.checkRepoCommand ?? ["bash", "scripts/check-repo.sh"];
123
+ if (command === undefined) {
124
+ throw new Error("check-repo command requires a binary");
125
+ }
123
126
  const checkRepo = await input.client.runExternal(command, args, {
124
127
  cwd: input.repoRoot,
125
128
  artifactName: "check-repo",
@@ -0,0 +1,117 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { readFile, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ import { getTaskPaths } from "../../task/runtime.js";
6
+ import { computeTaskConfigSha256 } from "../../task/contract/hash.js";
7
+ import { assessAutonomousExecutionEligibility, deriveEligibilityFactsFromDagSpec } from "../console/dag-execution-receipt.js";
8
+ import { scheduledGoalDraftSchema } from "./scheduled-goal-types.js";
9
+ const bindingSchema = z.object({
10
+ taskId: z.string(), revision: z.number().int().positive(), canonicalHash: z.string(),
11
+ taskConfigSha256: z.string(), projectionVersion: z.number(), canonicalizerVersion: z.number(), taskConfigSchemaVersion: z.number(),
12
+ });
13
+ const statusSchema = z.object({
14
+ taskId: z.string(), lifecycleState: z.literal("awaiting-write-set-approval"),
15
+ contract: z.object({ effectiveStatus: z.literal("managed"), revision: z.number().int().positive(),
16
+ canonicalHash: z.string(), observedCanonicalHash: z.string(), transactionIncomplete: z.literal(false),
17
+ taskConfigSha256: z.string(), projectionVersion: z.number(), canonicalizerVersion: z.number(), taskConfigSchemaVersion: z.number(), }),
18
+ dagDraft: z.object({ exists: z.literal(true), strictValidated: z.literal(true), specHash: z.string() }),
19
+ gate: z.object({ id: z.string().min(1), digest: z.string().min(1), type: z.literal("human-approval"),
20
+ bound: z.object({ taskId: z.string(), contractRevision: z.number(), contractCanonicalHash: z.string(), dagSpecHash: z.string() }), }),
21
+ activeRun: z.unknown().nullable().optional(), blockers: z.array(z.unknown()).length(0),
22
+ });
23
+ /** Freeze only a CLI-prepared managed task. This read-only intake never approves or starts a DAG. */
24
+ export async function prepareScheduledGoalDraft(input, client) {
25
+ // Validate caller-controlled identity before using it in CLI arguments or paths.
26
+ const parsed = scheduledGoalDraftSchema.omit({ prepared: true, controllerRef: true }).parse(input);
27
+ const identity = client.getIdentity();
28
+ if (!identity)
29
+ throw new Error("scheduled goal requires a pinned controller");
30
+ const root = await realpath(parsed.worktreeRef);
31
+ const paths = getTaskPaths(root, parsed.managedTaskId);
32
+ const readBound = async (file) => {
33
+ const resolved = await realpath(file);
34
+ const relative = path.relative(root, resolved);
35
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
36
+ throw new Error("scheduled goal prepared artifact escapes worktree");
37
+ return readFile(resolved, "utf8");
38
+ };
39
+ const status = await client.run(["task", "status", parsed.managedTaskId, "--json"], { cwd: root, expectJson: true, artifactName: `scheduled-goal-admission-${parsed.managedTaskId}-${randomUUID()}`, timeoutMs: 30_000 });
40
+ if (!status.ok || status.exitCode !== 0)
41
+ throw new Error("scheduled goal task status failed");
42
+ const envelope = z.object({ ok: z.literal(true), result: statusSchema }).parse(status.json);
43
+ const snapshot = envelope.result;
44
+ if (snapshot.taskId !== parsed.managedTaskId || snapshot.activeRun != null ||
45
+ snapshot.contract.canonicalHash !== snapshot.contract.observedCanonicalHash ||
46
+ snapshot.gate.bound.taskId !== parsed.managedTaskId ||
47
+ snapshot.gate.bound.contractRevision !== snapshot.contract.revision ||
48
+ snapshot.gate.bound.contractCanonicalHash !== snapshot.contract.canonicalHash)
49
+ throw new Error("scheduled goal managed contract binding mismatch");
50
+ const [dagBytes, configBytes] = await Promise.all([readBound(paths.dagDraftPath), readBound(paths.taskConfigPath)]);
51
+ const dagSpecHash = `sha256:${createHash("sha256").update(dagBytes).digest("hex")}`;
52
+ if (snapshot.dagDraft.specHash !== dagSpecHash || snapshot.gate.bound.dagSpecHash !== dagSpecHash)
53
+ throw new Error("scheduled goal validated DAG drift");
54
+ const dag = JSON.parse(dagBytes);
55
+ const binding = bindingSchema.parse(dag.taskContractBinding);
56
+ const expectedBinding = bindingSchema.parse({ ...snapshot.contract, taskId: parsed.managedTaskId });
57
+ if (dag.version !== 4 || JSON.stringify(binding) !== JSON.stringify(expectedBinding))
58
+ throw new Error("scheduled goal DAG contract binding mismatch");
59
+ const configJson = JSON.parse(configBytes);
60
+ if (computeTaskConfigSha256(configJson) !== binding.taskConfigSha256)
61
+ throw new Error("scheduled goal task configuration drift");
62
+ const config = z.object({ taskKind: z.string(), allowedPaths: z.array(z.string()), forbiddenPaths: z.array(z.string()) }).parse(configJson);
63
+ if (config.taskKind !== parsed.workflow || JSON.stringify(config.allowedPaths) !== JSON.stringify(parsed.allowedPaths) ||
64
+ JSON.stringify(config.forbiddenPaths) !== JSON.stringify(parsed.forbiddenPaths))
65
+ throw new Error("scheduled goal task scope or workflow mismatch");
66
+ const assessment = assessAutonomousExecutionEligibility(deriveEligibilityFactsFromDagSpec({
67
+ dagSpec: dag, allowedPaths: parsed.allowedPaths, forbiddenPaths: parsed.forbiddenPaths,
68
+ }));
69
+ if (!assessment.ok)
70
+ throw new Error(`scheduled goal ineligible: ${assessment.reason}`);
71
+ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
72
+ return scheduledGoalDraftSchema.parse({ ...parsed, controllerRef: identity.packageFingerprint.value,
73
+ prepared: { gateToken: `${snapshot.gate.id}:${snapshot.gate.digest}`,
74
+ dagPath: path.relative(root, paths.dagDraftPath), dagHash: digest(dagBytes),
75
+ contractPath: path.relative(root, paths.taskConfigPath), contractHash: digest(configBytes) }, });
76
+ }
77
+ /** Materialize only contract/runtime inputs; execution remains owned by the operation host. */
78
+ export async function prepareDerivedScheduledGoalTask(goalId, ownerToken, goals, client) {
79
+ const { mkdir, writeFile } = await import("node:fs/promises");
80
+ const goal = await goals.get(goalId);
81
+ const entry = goal.supervisions.at(-1);
82
+ if (goal.state !== "admitted" || !entry || entry.ownerToken !== ownerToken || entry.state !== "preparing" || !entry.proposal)
83
+ throw new Error("derived preparation not authorized");
84
+ if (client.getIdentity()?.packageFingerprint.value !== goal.frozen.controllerRef)
85
+ throw new Error("derived controller drift");
86
+ const directory = path.join(goal.frozen.worktreeRef, ".harness", "scheduled-goal-inputs");
87
+ await mkdir(directory, { recursive: true, mode: 0o700 });
88
+ const prdPath = path.join(directory, `${entry.managedTaskId}.md`);
89
+ const content = ["# Scheduled goal continuation", "## Goal", goal.frozen.originalGoal,
90
+ "## Observed gap", entry.proposal.summary, "## Next task", entry.proposal.instructions,
91
+ "## Acceptance criteria", ...goal.frozen.acceptance.criteria.map((ac) => `- ${ac.acId}: ${ac.description}\n Verification: ${ac.command}`),
92
+ "## Scope", `Workflow: ${goal.frozen.workflow}; acceptance policy: ${goal.frozen.acceptance.policy}.`,
93
+ `Allowed paths: ${JSON.stringify(goal.frozen.allowedPaths)}. Forbidden paths: ${JSON.stringify(goal.frozen.forbiddenPaths)}.`,
94
+ "Preserve the current worktree changes. Do not change acceptance, dependencies, Git history, controller or runtime configuration. Do not push or merge.",
95
+ `Parent evidence: ${entry.parentEvidenceId}; tree: ${entry.treeHash}.`, ""].join("\n\n");
96
+ await writeFile(prdPath, content, { flag: "wx", mode: 0o600 });
97
+ const boundaryArgs = goal.frozen.allowedPaths.flatMap((ref) => ["--allowed-path", ref])
98
+ .concat(goal.frozen.forbiddenPaths.flatMap((ref) => ["--forbidden-path", ref]))
99
+ .concat(goal.frozen.acceptance.criteria.flatMap((ac) => ["--verify", `${ac.acId}:${ac.command}`]));
100
+ for (const [step, args] of [
101
+ ["create", ["task", "advance", entry.managedTaskId, `Scheduled goal continuation ${entry.taskRevision}`, "--prd", prdPath, "--task-kind", goal.frozen.workflow, ...boundaryArgs, "--json"]],
102
+ ["prepare", ["task", "advance", entry.managedTaskId, "--task-kind", goal.frozen.workflow, ...boundaryArgs, "--json"]],
103
+ ]) {
104
+ const current = await goals.get(goalId);
105
+ if (current.state !== "admitted" || current.supervisions.at(-1)?.ownerToken !== ownerToken)
106
+ throw new Error("derived preparation authorization lost");
107
+ const remaining = Math.min(Date.parse(goal.frozen.time.stopAtUtc), Date.parse(goal.frozen.time.expiresAt)) - Date.now();
108
+ if (remaining <= 0)
109
+ throw new Error("derived preparation expired");
110
+ const result = await client.run([...args], { cwd: goal.frozen.worktreeRef, expectJson: true,
111
+ artifactName: `${entry.managedTaskId}-${step}`, timeoutMs: Math.min(remaining, 300000) });
112
+ if (!result.ok || result.exitCode !== 0 || result.timedOut)
113
+ throw new Error(`derived task ${step} failed or unknown`);
114
+ }
115
+ const { prepared: _prepared, controllerRef: _controller, ...input } = goal.frozen;
116
+ return prepareScheduledGoalDraft({ ...input, managedTaskId: entry.managedTaskId }, client);
117
+ }
@@ -0,0 +1,167 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { lstat, readFile, readlink, realpath } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { pathMatchesPattern } from "../../shared/git-progress.js";
7
+ import { scheduledGoalDraftSchema } from "./scheduled-goal-types.js";
8
+ const execFileAsync = promisify(execFile);
9
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
10
+ /** Run only frozen AC commands. A timeout is uncertain execution, never a passing or repairable failure. */
11
+ export async function evaluateScheduledGoalCriteria(frozen, options = {}) {
12
+ frozen = scheduledGoalDraftSchema.pick({ worktreeRef: true, frozenBase: true, allowedPaths: true, forbiddenPaths: true, acceptance: true, time: true }).strip().parse(frozen);
13
+ if (options.commandTimeoutMs !== undefined && (!Number.isFinite(options.commandTimeoutMs) || options.commandTimeoutMs <= 0))
14
+ throw new Error("invalid acceptance command timeout");
15
+ const now = options.now ?? (() => new Date());
16
+ const startedAt = now().toISOString();
17
+ const capture = () => captureScheduledGoalTree(frozen.worktreeRef, frozen.frozenBase, frozen.allowedPaths, frozen.forbiddenPaths);
18
+ const before = await capture();
19
+ const criteria = [];
20
+ let stopped = before.scopeViolations.length ? "scope_violation" : undefined;
21
+ let drift = false;
22
+ let executionUnknown = false;
23
+ for (const criterion of frozen.acceptance.criteria) {
24
+ if (options.mayRun && !await options.mayRun())
25
+ stopped ??= "assessment_cancelled";
26
+ const current = await capture();
27
+ if (current.treeHash !== before.treeHash) {
28
+ drift = true;
29
+ stopped = "tree_changed_during_acceptance";
30
+ }
31
+ const remaining = Math.min(Date.parse(frozen.time.stopAtUtc), Date.parse(frozen.time.expiresAt)) - now().getTime();
32
+ if (remaining <= 0)
33
+ stopped ??= "authorization_expired";
34
+ const result = { acId: criterion.acId, command: criterion.command,
35
+ status: "pending", exitCode: null, stdout: "", stderr: "", outputHash: sha256(JSON.stringify({ stdout: "", stderr: "", exitCode: null })), };
36
+ if (stopped)
37
+ result.reason = stopped;
38
+ else {
39
+ try {
40
+ const output = await execFileAsync("bash", ["-lc", criterion.command], {
41
+ cwd: frozen.worktreeRef, encoding: "utf8", maxBuffer: 1024 * 1024,
42
+ timeout: Math.max(1, Math.min(remaining, options.commandTimeoutMs ?? 120_000)),
43
+ });
44
+ result.stdout = output.stdout;
45
+ result.stderr = output.stderr;
46
+ result.exitCode = 0;
47
+ result.status = "succeeded";
48
+ }
49
+ catch (error) {
50
+ const failure = error;
51
+ result.stdout = failure.stdout ?? "";
52
+ result.stderr = failure.stderr ?? "";
53
+ if (typeof failure.code === "number" && !failure.killed && !failure.signal) {
54
+ result.exitCode = failure.code;
55
+ result.status = "failed";
56
+ }
57
+ else {
58
+ executionUnknown = true;
59
+ result.reason = "command_execution_unknown";
60
+ stopped = result.reason;
61
+ }
62
+ }
63
+ result.outputHash = sha256(JSON.stringify({ stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode }));
64
+ const verifiedTree = await capture();
65
+ if (verifiedTree.treeHash !== before.treeHash) {
66
+ drift = true;
67
+ stopped = "tree_changed_during_acceptance";
68
+ }
69
+ }
70
+ criteria.push(result);
71
+ }
72
+ const after = await capture();
73
+ drift ||= before.treeHash !== after.treeHash;
74
+ if (drift)
75
+ for (const result of criteria) {
76
+ if (result.reason === "command_execution_unknown")
77
+ continue;
78
+ result.status = "stale";
79
+ result.reason = "tree_changed_during_acceptance";
80
+ }
81
+ const finishedAt = now().toISOString();
82
+ const expired = Date.parse(finishedAt) >= Math.min(Date.parse(frozen.time.stopAtUtc), Date.parse(frozen.time.expiresAt));
83
+ const status = executionUnknown ? "pending" : drift ? "stale" : before.scopeViolations.length || after.scopeViolations.length ? "failed"
84
+ : expired ? "pending"
85
+ : criteria.some((result) => result.status === "pending") ? "pending"
86
+ : criteria.every((result) => result.status === "succeeded") ? "succeeded" : "failed";
87
+ return { before, after, criteria, status, executionUnknown, startedAt, finishedAt };
88
+ }
89
+ /** Hash actual delivery bytes, including nonignored untracked files; staging is not a tree change. */
90
+ export async function captureScheduledGoalTree(worktree, base, allowedPaths, forbiddenPaths) {
91
+ if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(base))
92
+ throw new Error("invalid frozen base");
93
+ const root = await realpath(worktree);
94
+ const git = async (...args) => (await execFileAsync("git", ["-C", root, ...args], {
95
+ encoding: "utf8", timeout: 30_000, maxBuffer: 16 * 1024 * 1024,
96
+ })).stdout;
97
+ const [head, top, tracked, untracked, baseline, format] = await Promise.all([
98
+ git("rev-parse", "HEAD"), git("rev-parse", "--show-toplevel"),
99
+ git("ls-files", "-z", "--cached"), git("ls-files", "-z", "--others", "--exclude-standard"),
100
+ git("ls-tree", "-r", "-z", base), git("rev-parse", "--show-object-format"),
101
+ ]);
102
+ if (await realpath(top.trim()) !== root || head.trim() !== base)
103
+ throw new Error("scheduled goal worktree/base drift");
104
+ const entries = (value) => value.split("\0").filter(Boolean);
105
+ const objectFormat = format.trim();
106
+ if (objectFormat !== "sha1" && objectFormat !== "sha256")
107
+ throw new Error("unsupported Git object format");
108
+ const baseEntries = new Map(entries(baseline).map((entry) => {
109
+ const separator = entry.indexOf("\t");
110
+ const [mode, kind, objectId] = entry.slice(0, separator).split(" ");
111
+ if (separator < 0 || !mode || !kind || !objectId)
112
+ throw new Error("invalid Git tree entry");
113
+ return [entry.slice(separator + 1), { mode, kind, objectId }];
114
+ }));
115
+ const blobId = (bytes) => createHash(objectFormat).update(`blob ${bytes.length}\0`).update(bytes).digest("hex");
116
+ const names = [...new Set([...entries(tracked), ...entries(untracked), ...baseEntries.keys()])].sort();
117
+ const deliveryNames = new Set(names);
118
+ const files = [];
119
+ const changedPaths = [];
120
+ for (const name of names) {
121
+ const expected = baseEntries.get(name);
122
+ let actualId;
123
+ let actualMode;
124
+ const target = path.resolve(root, name);
125
+ const relative = path.relative(root, target);
126
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
127
+ throw new Error("tree path escapes worktree");
128
+ try {
129
+ const parent = await realpath(path.dirname(target));
130
+ if (parent !== root && !parent.startsWith(`${root}${path.sep}`))
131
+ throw new Error("tree parent escapes worktree");
132
+ const stat = await lstat(target);
133
+ if (stat.isSymbolicLink()) {
134
+ const link = await readlink(target);
135
+ const resolved = await realpath(target).catch(() => { throw new Error(`unresolved delivery symlink: ${name}`); });
136
+ if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`))
137
+ throw new Error(`delivery symlink escapes worktree: ${name}`);
138
+ if (!deliveryNames.has(path.relative(root, resolved)))
139
+ throw new Error(`delivery symlink target is not fingerprinted: ${name}`);
140
+ files.push({ name, kind: "symlink", hash: sha256(link) });
141
+ actualId = blobId(Buffer.from(link));
142
+ actualMode = "120000";
143
+ }
144
+ else if (stat.isFile()) {
145
+ const bytes = await readFile(target);
146
+ files.push({ name, kind: "file", hash: sha256(bytes), executable: Boolean(stat.mode & 0o111) });
147
+ actualId = blobId(bytes);
148
+ actualMode = stat.mode & 0o111 ? "100755" : "100644";
149
+ }
150
+ else
151
+ throw new Error(`unsupported delivery entry: ${name}`);
152
+ }
153
+ catch (error) {
154
+ if (error.code === "ENOENT") {
155
+ if (expected)
156
+ files.push({ name, kind: "missing" });
157
+ }
158
+ else
159
+ throw error;
160
+ }
161
+ if (expected ? expected.objectId !== actualId || expected.mode !== actualMode : actualId !== undefined)
162
+ changedPaths.push(name);
163
+ }
164
+ const scopeViolations = changedPaths.filter((name) => !allowedPaths.some((pattern) => pathMatchesPattern(name, pattern))
165
+ || forbiddenPaths.some((pattern) => pathMatchesPattern(name, pattern)));
166
+ return { base, head: head.trim(), treeHash: sha256(JSON.stringify(files)), changedPaths, scopeViolations };
167
+ }
@@ -0,0 +1,62 @@
1
+ import { captureScheduledGoalTree } from "./scheduled-goal-evidence.js";
2
+ // ESRCH establishes absence only; permission errors and PID reuse never grant takeover.
3
+ function ownerMissing(pid) {
4
+ if (pid === undefined)
5
+ return true;
6
+ try {
7
+ process.kill(pid, 0);
8
+ return false;
9
+ }
10
+ catch (error) {
11
+ return error.code === "ESRCH";
12
+ }
13
+ }
14
+ /** Read-only result projection. Reopening a result never authorizes another execution. */
15
+ export async function readScheduledGoalResult(goalId, goals) {
16
+ const goal = await goals.get(goalId);
17
+ const evidence = goal.evidence.at(-1);
18
+ let evidenceCurrent = false;
19
+ let treeReadError = false;
20
+ if (evidence) {
21
+ try {
22
+ const tree = await captureScheduledGoalTree(goal.frozen.worktreeRef, goal.frozen.frozenBase, goal.frozen.allowedPaths, goal.frozen.forbiddenPaths);
23
+ evidenceCurrent = evidence.intentId === goal.intents.at(-1)?.intentId && tree.treeHash === evidence.committedTree.treeHash &&
24
+ tree.treeHash === evidence.assessment.after.treeHash && tree.scopeViolations.length === 0;
25
+ }
26
+ catch {
27
+ treeReadError = true;
28
+ }
29
+ }
30
+ const missingOwner = goal.intents.some((intent) => intent.state === "dispatching" && ownerMissing(intent.dispatchOwnerPid)) || Boolean(goal.assessmentAttempt && ownerMissing(goal.assessmentAttempt.ownerPid)) ||
31
+ goal.supervisions.some((entry) => ["requested", "preparing"].includes(entry.state) && ownerMissing(entry.ownerPid));
32
+ const uncertain = missingOwner || goal.state === "unknown" || goal.intents.some((intent) => intent.state === "unknown") ||
33
+ goal.supervisions.some((entry) => entry.state === "unknown") || Boolean(goal.assessmentAttempt && goal.acceptanceState === "pending");
34
+ const acceptance = evidence && !evidenceCurrent && ["accepted", "failed"].includes(goal.acceptanceState) ? "stale" : goal.acceptanceState;
35
+ return {
36
+ goalId, goalRevision: goal.goalRevision, stateVersion: goal.stateVersion,
37
+ state: goal.state, reasonCode: treeReadError ? "tree_unreadable" : acceptance === "stale" ? "evidence_drift" : goal.reasonCode,
38
+ acceptance, delivery: goal.deliveryState, evidenceCurrent,
39
+ budget: { limits: goal.frozen.budget, used: goal.used, tokenUsage: "unknown" },
40
+ supervision: goal.supervisions.map((entry) => ({ state: entry.state, taskRevision: entry.taskRevision, usageTokens: entry.usageTokens ?? null })),
41
+ execution: goal.intents.map((intent) => ({ taskRevision: intent.taskRevision, turnSeq: intent.turnSeq,
42
+ state: intent.state, operationId: intent.operationId, runId: intent.runId })),
43
+ evidence: goal.evidence.map((item) => ({ evidenceId: item.evidenceId, taskRevision: item.taskRevision,
44
+ treeHash: item.committedTree.treeHash, status: item.assessment.status, recordedAt: item.recordedAt,
45
+ criteria: item.assessment.criteria.map((ac) => ({ acId: ac.acId, status: ac.status, exitCode: ac.exitCode,
46
+ outputHash: ac.outputHash, evidenceRef: `${item.evidenceId}#${ac.acId}` })) })),
47
+ reconciliation: uncertain ? {
48
+ required: true,
49
+ reason: missingOwner ? "reservation_owner_missing" : goal.reasonCode ?? "execution_unknown",
50
+ instructions: "Inspect the associated operation/run and all descendant processes. Preserve the ledger and worktree. Do not delete ownership locks or automatically repeat an unknown action; an operator must reconcile execution before resources can be released.",
51
+ } : null,
52
+ };
53
+ }
54
+ /** Resolve a hash-bound output reference without exposing dispatch/owner capabilities. */
55
+ export async function readScheduledGoalEvidence(goalId, evidenceId, acId, goals) {
56
+ const goal = await goals.get(goalId);
57
+ const evidence = goal.evidence.find((entry) => entry.evidenceId === evidenceId);
58
+ const criterion = evidence?.assessment.criteria.find((entry) => entry.acId === acId);
59
+ if (!evidence || !criterion)
60
+ throw new Error("scheduled goal evidence not found");
61
+ return { evidenceId, taskRevision: evidence.taskRevision, treeHash: evidence.committedTree.treeHash, ...criterion };
62
+ }