@namzu/cli 29.0.1 → 29.1.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 (384) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +20 -0
  3. package/dist/browser/commands.d.ts +29 -0
  4. package/dist/browser/commands.d.ts.map +1 -0
  5. package/dist/browser/commands.js +361 -0
  6. package/dist/browser/commands.js.map +1 -0
  7. package/dist/browser/control.d.ts +79 -0
  8. package/dist/browser/control.d.ts.map +1 -0
  9. package/dist/browser/control.js +140 -0
  10. package/dist/browser/control.js.map +1 -0
  11. package/dist/browser/describe.d.ts +12 -0
  12. package/dist/browser/describe.d.ts.map +1 -0
  13. package/dist/browser/describe.js +27 -0
  14. package/dist/browser/describe.js.map +1 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +29 -2
  17. package/dist/cli.js.map +1 -1
  18. package/dist/commands/acp.d.ts.map +1 -1
  19. package/dist/commands/acp.js +1 -0
  20. package/dist/commands/acp.js.map +1 -1
  21. package/dist/commands/browser.d.ts +7 -0
  22. package/dist/commands/browser.d.ts.map +1 -0
  23. package/dist/commands/browser.js +16 -0
  24. package/dist/commands/browser.js.map +1 -0
  25. package/dist/commands/drain.d.ts +2 -1
  26. package/dist/commands/drain.d.ts.map +1 -1
  27. package/dist/commands/drain.js +6 -3
  28. package/dist/commands/drain.js.map +1 -1
  29. package/dist/commands/exec-flags.d.ts +2 -1
  30. package/dist/commands/exec-flags.d.ts.map +1 -1
  31. package/dist/commands/exec-flags.js +6 -3
  32. package/dist/commands/exec-flags.js.map +1 -1
  33. package/dist/commands/exec-json.d.ts.map +1 -1
  34. package/dist/commands/exec-json.js +2 -1
  35. package/dist/commands/exec-json.js.map +1 -1
  36. package/dist/commands/exec.d.ts.map +1 -1
  37. package/dist/commands/exec.js +2 -1
  38. package/dist/commands/exec.js.map +1 -1
  39. package/dist/commands/host-queries.d.ts.map +1 -1
  40. package/dist/commands/host-queries.js +10 -3
  41. package/dist/commands/host-queries.js.map +1 -1
  42. package/dist/commands/schedule.d.ts +9 -0
  43. package/dist/commands/schedule.d.ts.map +1 -0
  44. package/dist/commands/schedule.js +126 -0
  45. package/dist/commands/schedule.js.map +1 -0
  46. package/dist/commands/serve.d.ts +3 -1
  47. package/dist/commands/serve.d.ts.map +1 -1
  48. package/dist/commands/serve.js +5 -3
  49. package/dist/commands/serve.js.map +1 -1
  50. package/dist/commands/skills.d.ts +10 -0
  51. package/dist/commands/skills.d.ts.map +1 -1
  52. package/dist/commands/skills.js +23 -7
  53. package/dist/commands/skills.js.map +1 -1
  54. package/dist/commands/upgrade.d.ts +5 -0
  55. package/dist/commands/upgrade.d.ts.map +1 -1
  56. package/dist/commands/upgrade.js +11 -0
  57. package/dist/commands/upgrade.js.map +1 -1
  58. package/dist/config/load.d.ts +39 -0
  59. package/dist/config/load.d.ts.map +1 -1
  60. package/dist/config/load.js +232 -1
  61. package/dist/config/load.js.map +1 -1
  62. package/dist/config/schema.d.ts +78 -0
  63. package/dist/config/schema.d.ts.map +1 -1
  64. package/dist/config/schema.js.map +1 -1
  65. package/dist/config/user-config.d.ts +23 -0
  66. package/dist/config/user-config.d.ts.map +1 -0
  67. package/dist/config/user-config.js +48 -0
  68. package/dist/config/user-config.js.map +1 -0
  69. package/dist/context/capabilities.d.ts +1 -1
  70. package/dist/context/capabilities.d.ts.map +1 -1
  71. package/dist/context/capabilities.js +1 -0
  72. package/dist/context/capabilities.js.map +1 -1
  73. package/dist/doctor/checks/browser.d.ts +15 -0
  74. package/dist/doctor/checks/browser.d.ts.map +1 -0
  75. package/dist/doctor/checks/browser.js +63 -0
  76. package/dist/doctor/checks/browser.js.map +1 -0
  77. package/dist/doctor/checks/index.d.ts +4 -1
  78. package/dist/doctor/checks/index.d.ts.map +1 -1
  79. package/dist/doctor/checks/index.js +8 -1
  80. package/dist/doctor/checks/index.js.map +1 -1
  81. package/dist/doctor/checks/scheduler.d.ts +10 -0
  82. package/dist/doctor/checks/scheduler.d.ts.map +1 -0
  83. package/dist/doctor/checks/scheduler.js +74 -0
  84. package/dist/doctor/checks/scheduler.js.map +1 -0
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +1 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/integrations/notifications/desktop/freedesktop.d.ts +10 -0
  90. package/dist/integrations/notifications/desktop/freedesktop.d.ts.map +1 -0
  91. package/dist/integrations/notifications/desktop/freedesktop.js +33 -0
  92. package/dist/integrations/notifications/desktop/freedesktop.js.map +1 -0
  93. package/dist/integrations/notifications/desktop/macos.d.ts +6 -0
  94. package/dist/integrations/notifications/desktop/macos.d.ts.map +1 -0
  95. package/dist/integrations/notifications/desktop/macos.js +17 -0
  96. package/dist/integrations/notifications/desktop/macos.js.map +1 -0
  97. package/dist/integrations/notifications/desktop/sanitize.d.ts +16 -0
  98. package/dist/integrations/notifications/desktop/sanitize.d.ts.map +1 -0
  99. package/dist/integrations/notifications/desktop/sanitize.js +49 -0
  100. package/dist/integrations/notifications/desktop/sanitize.js.map +1 -0
  101. package/dist/integrations/notifications/desktop/throttle.d.ts +10 -0
  102. package/dist/integrations/notifications/desktop/throttle.d.ts.map +1 -0
  103. package/dist/integrations/notifications/desktop/throttle.js +39 -0
  104. package/dist/integrations/notifications/desktop/throttle.js.map +1 -0
  105. package/dist/integrations/notifications/desktop/windows-toast.d.ts +23 -0
  106. package/dist/integrations/notifications/desktop/windows-toast.d.ts.map +1 -0
  107. package/dist/integrations/notifications/desktop/windows-toast.js +56 -0
  108. package/dist/integrations/notifications/desktop/windows-toast.js.map +1 -0
  109. package/dist/integrations/notifications/desktop.d.ts +82 -0
  110. package/dist/integrations/notifications/desktop.d.ts.map +1 -0
  111. package/dist/integrations/notifications/desktop.js +198 -0
  112. package/dist/integrations/notifications/desktop.js.map +1 -0
  113. package/dist/integrations/resident/session-step.d.ts.map +1 -1
  114. package/dist/integrations/resident/session-step.js +1 -0
  115. package/dist/integrations/resident/session-step.js.map +1 -1
  116. package/dist/integrations/subagents/runtime.d.ts +6 -0
  117. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  118. package/dist/integrations/subagents/runtime.js +14 -5
  119. package/dist/integrations/subagents/runtime.js.map +1 -1
  120. package/dist/permissions/browser-sites.d.ts +95 -0
  121. package/dist/permissions/browser-sites.d.ts.map +1 -0
  122. package/dist/permissions/browser-sites.js +270 -0
  123. package/dist/permissions/browser-sites.js.map +1 -0
  124. package/dist/schedule/build.d.ts +79 -0
  125. package/dist/schedule/build.d.ts.map +1 -0
  126. package/dist/schedule/build.js +246 -0
  127. package/dist/schedule/build.js.map +1 -0
  128. package/dist/schedule/changes.d.ts +22 -0
  129. package/dist/schedule/changes.d.ts.map +1 -0
  130. package/dist/schedule/changes.js +55 -0
  131. package/dist/schedule/changes.js.map +1 -0
  132. package/dist/schedule/commands/add.d.ts +24 -0
  133. package/dist/schedule/commands/add.d.ts.map +1 -0
  134. package/dist/schedule/commands/add.js +459 -0
  135. package/dist/schedule/commands/add.js.map +1 -0
  136. package/dist/schedule/commands/args.d.ts +22 -0
  137. package/dist/schedule/commands/args.d.ts.map +1 -0
  138. package/dist/schedule/commands/args.js +80 -0
  139. package/dist/schedule/commands/args.js.map +1 -0
  140. package/dist/schedule/commands/confirm-prompt.d.ts +6 -0
  141. package/dist/schedule/commands/confirm-prompt.d.ts.map +1 -0
  142. package/dist/schedule/commands/confirm-prompt.js +21 -0
  143. package/dist/schedule/commands/confirm-prompt.js.map +1 -0
  144. package/dist/schedule/commands/lifecycle.d.ts +39 -0
  145. package/dist/schedule/commands/lifecycle.d.ts.map +1 -0
  146. package/dist/schedule/commands/lifecycle.js +585 -0
  147. package/dist/schedule/commands/lifecycle.js.map +1 -0
  148. package/dist/schedule/commands/list.d.ts +33 -0
  149. package/dist/schedule/commands/list.d.ts.map +1 -0
  150. package/dist/schedule/commands/list.js +202 -0
  151. package/dist/schedule/commands/list.js.map +1 -0
  152. package/dist/schedule/commands/logs.d.ts +7 -0
  153. package/dist/schedule/commands/logs.d.ts.map +1 -0
  154. package/dist/schedule/commands/logs.js +106 -0
  155. package/dist/schedule/commands/logs.js.map +1 -0
  156. package/dist/schedule/commands/service.d.ts +16 -0
  157. package/dist/schedule/commands/service.d.ts.map +1 -0
  158. package/dist/schedule/commands/service.js +458 -0
  159. package/dist/schedule/commands/service.js.map +1 -0
  160. package/dist/schedule/daemon/daemon.d.ts +156 -0
  161. package/dist/schedule/daemon/daemon.d.ts.map +1 -0
  162. package/dist/schedule/daemon/daemon.js +1248 -0
  163. package/dist/schedule/daemon/daemon.js.map +1 -0
  164. package/dist/schedule/daemon/endpoint.d.ts +38 -0
  165. package/dist/schedule/daemon/endpoint.d.ts.map +1 -0
  166. package/dist/schedule/daemon/endpoint.js +158 -0
  167. package/dist/schedule/daemon/endpoint.js.map +1 -0
  168. package/dist/schedule/daemon/exit.d.ts +8 -0
  169. package/dist/schedule/daemon/exit.d.ts.map +1 -0
  170. package/dist/schedule/daemon/exit.js +8 -0
  171. package/dist/schedule/daemon/exit.js.map +1 -0
  172. package/dist/schedule/daemon/log.d.ts +12 -0
  173. package/dist/schedule/daemon/log.d.ts.map +1 -0
  174. package/dist/schedule/daemon/log.js +53 -0
  175. package/dist/schedule/daemon/log.js.map +1 -0
  176. package/dist/schedule/daemon/notify.d.ts +31 -0
  177. package/dist/schedule/daemon/notify.d.ts.map +1 -0
  178. package/dist/schedule/daemon/notify.js +89 -0
  179. package/dist/schedule/daemon/notify.js.map +1 -0
  180. package/dist/schedule/daemon/retention.d.ts +16 -0
  181. package/dist/schedule/daemon/retention.d.ts.map +1 -0
  182. package/dist/schedule/daemon/retention.js +46 -0
  183. package/dist/schedule/daemon/retention.js.map +1 -0
  184. package/dist/schedule/daemon/sessions.d.ts +23 -0
  185. package/dist/schedule/daemon/sessions.d.ts.map +1 -0
  186. package/dist/schedule/daemon/sessions.js +52 -0
  187. package/dist/schedule/daemon/sessions.js.map +1 -0
  188. package/dist/schedule/env.d.ts +28 -0
  189. package/dist/schedule/env.d.ts.map +1 -0
  190. package/dist/schedule/env.js +99 -0
  191. package/dist/schedule/env.js.map +1 -0
  192. package/dist/schedule/fire/browser-preflight.d.ts +41 -0
  193. package/dist/schedule/fire/browser-preflight.d.ts.map +1 -0
  194. package/dist/schedule/fire/browser-preflight.js +73 -0
  195. package/dist/schedule/fire/browser-preflight.js.map +1 -0
  196. package/dist/schedule/fire/fire.d.ts +78 -0
  197. package/dist/schedule/fire/fire.d.ts.map +1 -0
  198. package/dist/schedule/fire/fire.js +482 -0
  199. package/dist/schedule/fire/fire.js.map +1 -0
  200. package/dist/schedule/fire/result.d.ts +13 -0
  201. package/dist/schedule/fire/result.d.ts.map +1 -0
  202. package/dist/schedule/fire/result.js +18 -0
  203. package/dist/schedule/fire/result.js.map +1 -0
  204. package/dist/schedule/fire/unattended-note.d.ts +22 -0
  205. package/dist/schedule/fire/unattended-note.d.ts.map +1 -0
  206. package/dist/schedule/fire/unattended-note.js +46 -0
  207. package/dist/schedule/fire/unattended-note.js.map +1 -0
  208. package/dist/schedule/floor.d.ts +116 -0
  209. package/dist/schedule/floor.d.ts.map +1 -0
  210. package/dist/schedule/floor.js +1051 -0
  211. package/dist/schedule/floor.js.map +1 -0
  212. package/dist/schedule/folder.d.ts +34 -0
  213. package/dist/schedule/folder.d.ts.map +1 -0
  214. package/dist/schedule/folder.js +84 -0
  215. package/dist/schedule/folder.js.map +1 -0
  216. package/dist/schedule/paths.d.ts +49 -0
  217. package/dist/schedule/paths.d.ts.map +1 -0
  218. package/dist/schedule/paths.js +53 -0
  219. package/dist/schedule/paths.js.map +1 -0
  220. package/dist/schedule/policy.d.ts +141 -0
  221. package/dist/schedule/policy.d.ts.map +1 -0
  222. package/dist/schedule/policy.js +378 -0
  223. package/dist/schedule/policy.js.map +1 -0
  224. package/dist/schedule/resume-command.d.ts +36 -0
  225. package/dist/schedule/resume-command.d.ts.map +1 -0
  226. package/dist/schedule/resume-command.js +72 -0
  227. package/dist/schedule/resume-command.js.map +1 -0
  228. package/dist/schedule/service/detect.d.ts +22 -0
  229. package/dist/schedule/service/detect.d.ts.map +1 -0
  230. package/dist/schedule/service/detect.js +29 -0
  231. package/dist/schedule/service/detect.js.map +1 -0
  232. package/dist/schedule/service/index.d.ts +48 -0
  233. package/dist/schedule/service/index.d.ts.map +1 -0
  234. package/dist/schedule/service/index.js +352 -0
  235. package/dist/schedule/service/index.js.map +1 -0
  236. package/dist/schedule/service/launchd.d.ts +24 -0
  237. package/dist/schedule/service/launchd.d.ts.map +1 -0
  238. package/dist/schedule/service/launchd.js +84 -0
  239. package/dist/schedule/service/launchd.js.map +1 -0
  240. package/dist/schedule/service/manifest.d.ts +44 -0
  241. package/dist/schedule/service/manifest.d.ts.map +1 -0
  242. package/dist/schedule/service/manifest.js +16 -0
  243. package/dist/schedule/service/manifest.js.map +1 -0
  244. package/dist/schedule/service/names.d.ts +9 -0
  245. package/dist/schedule/service/names.d.ts.map +1 -0
  246. package/dist/schedule/service/names.js +22 -0
  247. package/dist/schedule/service/names.js.map +1 -0
  248. package/dist/schedule/service/quote.d.ts +24 -0
  249. package/dist/schedule/service/quote.d.ts.map +1 -0
  250. package/dist/schedule/service/quote.js +64 -0
  251. package/dist/schedule/service/quote.js.map +1 -0
  252. package/dist/schedule/service/runner.d.ts +17 -0
  253. package/dist/schedule/service/runner.d.ts.map +1 -0
  254. package/dist/schedule/service/runner.js +31 -0
  255. package/dist/schedule/service/runner.js.map +1 -0
  256. package/dist/schedule/service/systemd.d.ts +37 -0
  257. package/dist/schedule/service/systemd.d.ts.map +1 -0
  258. package/dist/schedule/service/systemd.js +96 -0
  259. package/dist/schedule/service/systemd.js.map +1 -0
  260. package/dist/schedule/service/windows-task.d.ts +51 -0
  261. package/dist/schedule/service/windows-task.d.ts.map +1 -0
  262. package/dist/schedule/service/windows-task.js +134 -0
  263. package/dist/schedule/service/windows-task.js.map +1 -0
  264. package/dist/schedule/service/wsl.d.ts +57 -0
  265. package/dist/schedule/service/wsl.d.ts.map +1 -0
  266. package/dist/schedule/service/wsl.js +111 -0
  267. package/dist/schedule/service/wsl.js.map +1 -0
  268. package/dist/schedule/store/atomic.d.ts +43 -0
  269. package/dist/schedule/store/atomic.d.ts.map +1 -0
  270. package/dist/schedule/store/atomic.js +135 -0
  271. package/dist/schedule/store/atomic.js.map +1 -0
  272. package/dist/schedule/store/claims.d.ts +26 -0
  273. package/dist/schedule/store/claims.d.ts.map +1 -0
  274. package/dist/schedule/store/claims.js +57 -0
  275. package/dist/schedule/store/claims.js.map +1 -0
  276. package/dist/schedule/store/digest.d.ts +18 -0
  277. package/dist/schedule/store/digest.d.ts.map +1 -0
  278. package/dist/schedule/store/digest.js +139 -0
  279. package/dist/schedule/store/digest.js.map +1 -0
  280. package/dist/schedule/store/history.d.ts +21 -0
  281. package/dist/schedule/store/history.d.ts.map +1 -0
  282. package/dist/schedule/store/history.js +114 -0
  283. package/dist/schedule/store/history.js.map +1 -0
  284. package/dist/schedule/store/jobs.d.ts +61 -0
  285. package/dist/schedule/store/jobs.d.ts.map +1 -0
  286. package/dist/schedule/store/jobs.js +210 -0
  287. package/dist/schedule/store/jobs.js.map +1 -0
  288. package/dist/schedule/store/state.d.ts +19 -0
  289. package/dist/schedule/store/state.d.ts.map +1 -0
  290. package/dist/schedule/store/state.js +38 -0
  291. package/dist/schedule/store/state.js.map +1 -0
  292. package/dist/schedule/types.d.ts +256 -0
  293. package/dist/schedule/types.d.ts.map +1 -0
  294. package/dist/schedule/types.js +9 -0
  295. package/dist/schedule/types.js.map +1 -0
  296. package/dist/skills/catalog.d.ts +96 -0
  297. package/dist/skills/catalog.d.ts.map +1 -0
  298. package/dist/skills/catalog.js +226 -0
  299. package/dist/skills/catalog.js.map +1 -0
  300. package/dist/skills/save.d.ts +148 -0
  301. package/dist/skills/save.d.ts.map +1 -0
  302. package/dist/skills/save.js +344 -0
  303. package/dist/skills/save.js.map +1 -0
  304. package/dist/skills/store.d.ts +103 -16
  305. package/dist/skills/store.d.ts.map +1 -1
  306. package/dist/skills/store.js +227 -30
  307. package/dist/skills/store.js.map +1 -1
  308. package/dist/tui/App.d.ts.map +1 -1
  309. package/dist/tui/App.js +591 -72
  310. package/dist/tui/App.js.map +1 -1
  311. package/dist/tui/LiveActivity.d.ts +8 -1
  312. package/dist/tui/LiveActivity.d.ts.map +1 -1
  313. package/dist/tui/LiveActivity.js +20 -4
  314. package/dist/tui/LiveActivity.js.map +1 -1
  315. package/dist/tui/PermissionOverlay.d.ts +24 -4
  316. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  317. package/dist/tui/PermissionOverlay.js +113 -15
  318. package/dist/tui/PermissionOverlay.js.map +1 -1
  319. package/dist/tui/SaveSkillOverlay.d.ts +33 -0
  320. package/dist/tui/SaveSkillOverlay.d.ts.map +1 -0
  321. package/dist/tui/SaveSkillOverlay.js +118 -0
  322. package/dist/tui/SaveSkillOverlay.js.map +1 -0
  323. package/dist/tui/agent.d.ts +185 -5
  324. package/dist/tui/agent.d.ts.map +1 -1
  325. package/dist/tui/agent.js +325 -54
  326. package/dist/tui/agent.js.map +1 -1
  327. package/dist/tui/browser-notices.d.ts +33 -0
  328. package/dist/tui/browser-notices.d.ts.map +1 -0
  329. package/dist/tui/browser-notices.js +102 -0
  330. package/dist/tui/browser-notices.js.map +1 -0
  331. package/dist/tui/permission-review.d.ts.map +1 -1
  332. package/dist/tui/permission-review.js +53 -4
  333. package/dist/tui/permission-review.js.map +1 -1
  334. package/dist/tui/schedule/host-commands.d.ts +34 -0
  335. package/dist/tui/schedule/host-commands.d.ts.map +1 -0
  336. package/dist/tui/schedule/host-commands.js +309 -0
  337. package/dist/tui/schedule/host-commands.js.map +1 -0
  338. package/dist/tui/schedule/integration.d.ts +62 -0
  339. package/dist/tui/schedule/integration.d.ts.map +1 -0
  340. package/dist/tui/schedule/integration.js +145 -0
  341. package/dist/tui/schedule/integration.js.map +1 -0
  342. package/dist/tui/schedule/loop-host.d.ts +42 -0
  343. package/dist/tui/schedule/loop-host.d.ts.map +1 -0
  344. package/dist/tui/schedule/loop-host.js +153 -0
  345. package/dist/tui/schedule/loop-host.js.map +1 -0
  346. package/dist/tui/schedule/resume.d.ts +122 -0
  347. package/dist/tui/schedule/resume.d.ts.map +1 -0
  348. package/dist/tui/schedule/resume.js +324 -0
  349. package/dist/tui/schedule/resume.js.map +1 -0
  350. package/dist/tui/schedule/startup.d.ts +11 -0
  351. package/dist/tui/schedule/startup.d.ts.map +1 -0
  352. package/dist/tui/schedule/startup.js +121 -0
  353. package/dist/tui/schedule/startup.js.map +1 -0
  354. package/dist/tui/schedule/tool-host.d.ts +44 -0
  355. package/dist/tui/schedule/tool-host.d.ts.map +1 -0
  356. package/dist/tui/schedule/tool-host.js +294 -0
  357. package/dist/tui/schedule/tool-host.js.map +1 -0
  358. package/dist/tui/skills/learning.d.ts +74 -0
  359. package/dist/tui/skills/learning.d.ts.map +1 -0
  360. package/dist/tui/skills/learning.js +131 -0
  361. package/dist/tui/skills/learning.js.map +1 -0
  362. package/dist/tui/skills/suggest-setting.d.ts +22 -0
  363. package/dist/tui/skills/suggest-setting.d.ts.map +1 -0
  364. package/dist/tui/skills/suggest-setting.js +55 -0
  365. package/dist/tui/skills/suggest-setting.js.map +1 -0
  366. package/dist/tui/skills/suggestion-ledger.d.ts +35 -0
  367. package/dist/tui/skills/suggestion-ledger.d.ts.map +1 -0
  368. package/dist/tui/skills/suggestion-ledger.js +52 -0
  369. package/dist/tui/skills/suggestion-ledger.js.map +1 -0
  370. package/dist/tui/slashCommands.d.ts +20 -0
  371. package/dist/tui/slashCommands.d.ts.map +1 -1
  372. package/dist/tui/slashCommands.js +76 -2
  373. package/dist/tui/slashCommands.js.map +1 -1
  374. package/dist/tui/turn-interruption.d.ts +20 -1
  375. package/dist/tui/turn-interruption.d.ts.map +1 -1
  376. package/dist/tui/turn-interruption.js +43 -0
  377. package/dist/tui/turn-interruption.js.map +1 -1
  378. package/dist/tui/types.d.ts +9 -1
  379. package/dist/tui/types.d.ts.map +1 -1
  380. package/package.json +6 -4
  381. package/skills/README.md +15 -0
  382. package/skills/browser-automation/SKILL.md +84 -0
  383. package/skills/schedule-task/SKILL.md +140 -0
  384. package/skills/skill-creator/SKILL.md +101 -0
@@ -0,0 +1,1248 @@
1
+ /**
2
+ * The scheduler daemon: one per `NAMZU_HOME`, started by a user service.
3
+ *
4
+ * ## One owner
5
+ *
6
+ * Ownership is a fenced lease in `schedule/daemon/` — the session lease's own
7
+ * store, `lease.<fence>.json` published with `link`, the highest fence owning,
8
+ * renewed every 30 s with a 90 s expiry. A second daemon does not exit (a
9
+ * supervisor would restart it in a loop): it waits on standby and takes a new
10
+ * fence when the owner's expires. Before every claim the owner checks its
11
+ * fence is still the highest, and one that was fenced out stops dispatching at
12
+ * once. The occurrence claim (`claims/`, also `link`) is the final guarantee
13
+ * that nothing starts twice, lease or no lease.
14
+ *
15
+ * ## The loop
16
+ *
17
+ * Every tick (at most 30 s, sooner when something is due, a job file changes
18
+ * or a run ends): evaluate each job with the SDK's pure evaluator, append what
19
+ * it decided to history, queue what is due, and dispatch the queue up to the
20
+ * concurrency cap — one run at a time per folder for jobs that can write.
21
+ * An occurrence is claimed when its run STARTS, never when it is queued, so a
22
+ * restart re-derives a waiting run instead of losing it.
23
+ *
24
+ * ## Runs
25
+ *
26
+ * A run is a child process, `schedule __fire`, whose stdout and stderr are a
27
+ * log FILE rather than pipes, so it outlives a daemon restart without dying of
28
+ * EPIPE. It enforces its own wall clock. A run this daemon did not start (it
29
+ * was started before a restart) is adopted: finalised from its result file,
30
+ * or from its session log once its lease lapses. Nothing is ever killed by a
31
+ * pid read from disk.
32
+ *
33
+ * ## Upgrades
34
+ *
35
+ * Every tick compares the installed CLI with the one this daemon started
36
+ * from. On a change it stops dispatching, waits for the runs it started and
37
+ * exits 0; the supervisor starts the new code.
38
+ */
39
+ import { spawn } from 'node:child_process';
40
+ import { closeSync, existsSync, openSync, rmSync, statSync, watch, writeFileSync, } from 'node:fs';
41
+ import { dirname, join } from 'node:path';
42
+ import { DiskSessionLeaseStore, evaluateJob, generateScheduleRunId, } from '@namzu/sdk';
43
+ import { admitNotification } from '../../integrations/notifications/desktop/throttle.js';
44
+ import { childEnvironment } from '../env.js';
45
+ import { isFinal, readRunResult } from '../fire/result.js';
46
+ import { allowsWrites } from '../policy.js';
47
+ import { resumeCommand } from '../resume-command.js';
48
+ import { PRIVATE_FILE_MODE, ensureDir, writeJsonAtomic } from '../store/atomic.js';
49
+ import { claimOccurrence, isClaimed, pruneClaims } from '../store/claims.js';
50
+ import { appendHistory, compactHistory } from '../store/history.js';
51
+ import { confirmationHolds, jobSecurityDigest, listJobs, readJob, updateJob, } from '../store/jobs.js';
52
+ import { readState, withoutUndefined, writeState } from '../store/state.js';
53
+ import { startEndpoint } from './endpoint.js';
54
+ import { EXIT_STOP_REQUESTED } from './exit.js';
55
+ import { pruneDaemonLogs } from './log.js';
56
+ import { failureSignature, noticeText } from './notify.js';
57
+ import { archiveOldRuns } from './retention.js';
58
+ import { abandonParkedTurn, sessionFacts, sessionLeaseLive, turnOutcome } from './sessions.js';
59
+ export { EXIT_STOP_REQUESTED };
60
+ export const LEASE_TTL_MS = 90_000;
61
+ export const LEASE_RENEW_MS = 30_000;
62
+ export const TICK_MS = 30_000;
63
+ /** A run with no result and no session this long after it started is gone. */
64
+ const START_GRACE_MS = 120_000;
65
+ const CLAIM_RETENTION_EXTRA_MS = 24 * 60 * 60 * 1000;
66
+ const HOUSEKEEPING_MS = 24 * 60 * 60 * 1000;
67
+ const GAP_MS = 60_000;
68
+ export class ScheduleDaemon {
69
+ #o;
70
+ #now;
71
+ #mono;
72
+ #leaseStore;
73
+ #lease = null;
74
+ #leaseRenewedAt = 0;
75
+ #endpoint;
76
+ #watcher;
77
+ /** Runs in progress: ours (with a handle) and adopted ones (without). */
78
+ #running = new Map();
79
+ #stopping = false;
80
+ /** The stop came from `schedule stop`'s file, not a signal or the endpoint. */
81
+ #stopRequestedByFile = false;
82
+ #draining = false;
83
+ #standby = false;
84
+ #wake;
85
+ #lastWall = 0;
86
+ #lastMono = 0;
87
+ #startedAt = 0;
88
+ #observedGap;
89
+ #startFingerprint = '';
90
+ #lastHousekeeping = 0;
91
+ #manual = [];
92
+ /** Why a queued run is waiting, for its run record once it starts. */
93
+ #delayReasons = new Map();
94
+ #finalizing = [];
95
+ constructor(options) {
96
+ this.#o = options;
97
+ this.#now = options.now ?? Date.now;
98
+ this.#mono = options.monotonic ?? (() => performance.now());
99
+ this.#leaseStore = new DiskSessionLeaseStore(options.paths.daemon);
100
+ }
101
+ get standby() {
102
+ return this.#standby;
103
+ }
104
+ get lease() {
105
+ return this.#lease;
106
+ }
107
+ /** Ask the loop to stop at its next wake. Running children are left running. */
108
+ stop() {
109
+ this.#stopping = true;
110
+ this.#wake?.();
111
+ }
112
+ /** Stop dispatching, wait for our own runs, then stop (an upgrade restart). */
113
+ drainAndRestart() {
114
+ this.#draining = true;
115
+ this.#persistManual();
116
+ this.#wake?.();
117
+ }
118
+ wake() {
119
+ this.#wake?.();
120
+ }
121
+ /** Try once to own the home (for a caller driving {@link tick} itself). */
122
+ async claimOwnership() {
123
+ this.#startFingerprint ||= this.#o.fingerprint();
124
+ this.#startedAt ||= this.#now();
125
+ const lease = await this.#leaseStore.claim({
126
+ holder: this.#o.epoch,
127
+ ttlMs: this.#o.leaseTtlMs ?? LEASE_TTL_MS,
128
+ now: this.#now(),
129
+ });
130
+ if (lease) {
131
+ this.#lease = lease;
132
+ this.#leaseRenewedAt = this.#now();
133
+ }
134
+ return lease !== null;
135
+ }
136
+ /** Give the home up (a caller driving {@link tick} itself). */
137
+ async releaseOwnership() {
138
+ if (this.#lease)
139
+ await this.#leaseStore.release(this.#lease);
140
+ this.#lease = null;
141
+ }
142
+ /** Run until stopped. Returns the process exit code. */
143
+ async run() {
144
+ ensureDir(this.#o.paths.daemon);
145
+ this.#startedAt = this.#now();
146
+ this.#startFingerprint = this.#o.fingerprint();
147
+ for (;;) {
148
+ const acquired = await this.#acquire();
149
+ if (acquired === 'exit75')
150
+ return 75;
151
+ if (acquired === 'stopped')
152
+ return this.#stopRequestedByFile ? EXIT_STOP_REQUESTED : 0;
153
+ const outcome = await this.#own();
154
+ if (outcome === 'standby')
155
+ continue;
156
+ return this.#stopRequestedByFile ? EXIT_STOP_REQUESTED : 0;
157
+ }
158
+ }
159
+ async #acquire() {
160
+ let announced = false;
161
+ for (;;) {
162
+ if (this.#stopRequested())
163
+ this.#stopping = true;
164
+ if (this.#stopping)
165
+ return 'stopped';
166
+ const lease = await this.#leaseStore.claim({
167
+ holder: this.#o.epoch,
168
+ ttlMs: this.#o.leaseTtlMs ?? LEASE_TTL_MS,
169
+ now: this.#now(),
170
+ });
171
+ if (lease) {
172
+ this.#lease = lease;
173
+ this.#leaseRenewedAt = this.#now();
174
+ this.#standby = false;
175
+ this.#o.log.info('scheduler owns this home', {
176
+ 'namzu.schedule.epoch': this.#o.epoch,
177
+ 'namzu.schedule.fence': lease.fence,
178
+ });
179
+ return 'owned';
180
+ }
181
+ if (this.#o.onceOrExit) {
182
+ this.#o.log.warn('another scheduler owns this home', {
183
+ 'namzu.schedule.epoch': this.#o.epoch,
184
+ });
185
+ return 'exit75';
186
+ }
187
+ this.#standby = true;
188
+ if (!announced) {
189
+ announced = true;
190
+ this.#o.log.info('scheduler on standby', { 'namzu.schedule.epoch': this.#o.epoch });
191
+ }
192
+ await this.#sleep(this.#o.standbyPollMs ?? LEASE_RENEW_MS);
193
+ }
194
+ }
195
+ async #own() {
196
+ this.#endpoint = await startEndpoint(this.#o.paths.endpoint, this.#o.epoch, {
197
+ status: () => this.status(),
198
+ reload: () => this.wake(),
199
+ runNow: (jobId) => this.requestRunNow(jobId),
200
+ stop: () => this.stop(),
201
+ drainAndRestart: () => this.drainAndRestart(),
202
+ });
203
+ if (this.#o.watchJobs !== false) {
204
+ try {
205
+ ensureDir(this.#o.paths.jobs);
206
+ this.#watcher = watch(this.#o.paths.jobs, () => this.wake());
207
+ this.#watcher.on('error', () => { });
208
+ }
209
+ catch { }
210
+ }
211
+ this.#lastWall = this.#now();
212
+ this.#lastMono = this.#mono();
213
+ try {
214
+ for (;;) {
215
+ if (this.#stopRequested())
216
+ this.#stopping = true;
217
+ if (this.#stopping)
218
+ return 'stopped';
219
+ if (!(await this.#holdLease())) {
220
+ this.#o.log.warn('scheduler lost its lease; standing by', {
221
+ 'namzu.schedule.epoch': this.#o.epoch,
222
+ });
223
+ return 'standby';
224
+ }
225
+ await this.tick();
226
+ if (this.#draining && this.#ownRuns() === 0) {
227
+ this.#o.log.info('scheduler drained for a restart', {
228
+ 'namzu.schedule.epoch': this.#o.epoch,
229
+ });
230
+ this.#stopping = true;
231
+ return 'stopped';
232
+ }
233
+ await this.#sleep(this.#nextSleep());
234
+ }
235
+ }
236
+ finally {
237
+ this.#watcher?.close();
238
+ await this.#endpoint?.close();
239
+ this.#endpoint = undefined;
240
+ await Promise.allSettled(this.#finalizing);
241
+ if (this.#stopping)
242
+ this.#persistManual();
243
+ if (this.#lease && this.#stopping) {
244
+ await this.#leaseStore.release(this.#lease).catch(() => undefined);
245
+ this.#lease = null;
246
+ }
247
+ this.#heartbeat();
248
+ }
249
+ }
250
+ /**
251
+ * `schedule stop` and `uninstall` leave `daemon/stop.json`; `start` and
252
+ * `install` remove it. It reaches a daemon the service manager cannot: one
253
+ * on standby (it has no endpoint), or one under WSL, whose Windows task
254
+ * ending does not end the Linux process. A daemon that finds it exits
255
+ * {@link EXIT_STOP_REQUESTED}.
256
+ */
257
+ #stopRequested() {
258
+ if (!existsSync(stopRequestPath(this.#o.paths)))
259
+ return false;
260
+ this.#stopRequestedByFile = true;
261
+ this.#o.log.info('scheduler stop requested', { 'namzu.schedule.epoch': this.#o.epoch });
262
+ return true;
263
+ }
264
+ #ownRuns() {
265
+ let n = 0;
266
+ for (const t of this.#running.values())
267
+ if (t.run)
268
+ n++;
269
+ return n;
270
+ }
271
+ /** Renew when due; true while this daemon's fence is still the highest. */
272
+ async #holdLease() {
273
+ const lease = this.#lease;
274
+ if (!lease)
275
+ return false;
276
+ if (this.#now() - this.#leaseRenewedAt >= LEASE_RENEW_MS) {
277
+ const renewed = await this.#leaseStore.claim({ holder: this.#o.epoch, ttlMs: this.#o.leaseTtlMs ?? LEASE_TTL_MS, now: this.#now() }, { renew: lease });
278
+ if (!renewed) {
279
+ this.#lease = null;
280
+ return false;
281
+ }
282
+ this.#lease = renewed;
283
+ this.#leaseRenewedAt = this.#now();
284
+ }
285
+ return this.#stillOwner();
286
+ }
287
+ async #stillOwner() {
288
+ const lease = this.#lease;
289
+ return lease !== null && (await this.#leaseStore.fence()) === lease.fence;
290
+ }
291
+ #sleep(ms) {
292
+ return new Promise((resolve) => {
293
+ const timer = setTimeout(() => {
294
+ this.#wake = undefined;
295
+ resolve();
296
+ }, Math.max(10, ms));
297
+ this.#wake = () => {
298
+ clearTimeout(timer);
299
+ this.#wake = undefined;
300
+ resolve();
301
+ };
302
+ });
303
+ }
304
+ #nextSleep() {
305
+ const tick = this.#o.tickMs ?? TICK_MS;
306
+ let soonest = this.#now() + tick;
307
+ for (const job of listJobs(this.#o.paths).jobs) {
308
+ try {
309
+ const next = readState(this.#o.paths, job.id).nextFireAt;
310
+ if (next)
311
+ soonest = Math.min(soonest, Date.parse(next) + 50);
312
+ }
313
+ catch { }
314
+ }
315
+ return Math.max(50, Math.min(tick, soonest - this.#now()));
316
+ }
317
+ /** What the endpoint's `status` reports. */
318
+ status() {
319
+ return {
320
+ epoch: this.#o.epoch,
321
+ pid: process.pid,
322
+ version: this.#o.version,
323
+ fence: this.#lease?.fence,
324
+ standby: this.#standby,
325
+ draining: this.#draining,
326
+ runs: [...this.#running.values()].map((t) => ({
327
+ jobId: t.jobId,
328
+ runId: t.runId,
329
+ adopted: t.run === undefined,
330
+ })),
331
+ notifications: this.#o.notificationBackend ?? 'unknown',
332
+ };
333
+ }
334
+ /** Queue a manual run (`schedule run-now`). Refused while the job has a run. */
335
+ requestRunNow(jobId) {
336
+ const job = readJob(this.#o.paths, jobId);
337
+ if (!job)
338
+ return { ok: false, message: `no job ${jobId}` };
339
+ if (job.state !== 'active' || !confirmationHolds(job)) {
340
+ return {
341
+ ok: false,
342
+ message: `${job.name} is ${job.state === 'active' ? 'awaiting confirmation' : job.state}`,
343
+ };
344
+ }
345
+ const state = readState(this.#o.paths, job.id);
346
+ if (state.activeRun || state.queued || this.#manual.some((m) => m.jobId === job.id)) {
347
+ return { ok: false, message: `${job.name} already has a run` };
348
+ }
349
+ this.#manual.push({ jobId: job.id, runId: generateScheduleRunId() });
350
+ if (this.#draining) {
351
+ // This daemon starts nothing more and its memory goes with it: the run
352
+ // is left queued on disk, for the daemon that takes over.
353
+ this.#persistManual();
354
+ return {
355
+ ok: true,
356
+ message: `${job.name} queued; the scheduler is restarting for an upgrade and starts it once it is back`,
357
+ };
358
+ }
359
+ this.wake();
360
+ return { ok: true, message: `${job.name} queued` };
361
+ }
362
+ /**
363
+ * Move every manual run still waiting in memory (deferred by the cap or a
364
+ * busy folder lane) to `state.queued` on disk. A daemon that stops
365
+ * dispatching (a drain) or exits keeps no memory, and the CLI has already
366
+ * told the operator the run is queued; the daemon that takes over starts
367
+ * it from disk under the same run id.
368
+ */
369
+ #persistManual() {
370
+ for (const manual of this.#manual.splice(0)) {
371
+ try {
372
+ const state = readState(this.#o.paths, manual.jobId);
373
+ if (state.activeRun || state.queued) {
374
+ this.#o.log.warn('manual run dropped; the job already has a run', {
375
+ 'namzu.schedule.job_id': manual.jobId,
376
+ 'namzu.schedule.run_id': manual.runId,
377
+ });
378
+ continue;
379
+ }
380
+ const at = new Date(this.#now()).toISOString();
381
+ writeState(this.#o.paths, {
382
+ ...state,
383
+ queued: {
384
+ key: `${MANUAL_KEY_PREFIX}${manual.runId}`,
385
+ scheduledFor: at,
386
+ trigger: 'manual',
387
+ queuedAt: at,
388
+ },
389
+ });
390
+ }
391
+ catch (error) {
392
+ this.#o.log.error('manual run could not be queued on disk', {
393
+ 'namzu.schedule.job_id': manual.jobId,
394
+ 'namzu.schedule.run_id': manual.runId,
395
+ 'exception.message': error instanceof Error ? error.message : String(error),
396
+ });
397
+ }
398
+ }
399
+ }
400
+ #heartbeat() {
401
+ try {
402
+ writeJsonAtomic(this.#o.paths.heartbeat, {
403
+ v: 1,
404
+ kind: 'schedule-heartbeat',
405
+ at: new Date(this.#now()).toISOString(),
406
+ pid: process.pid,
407
+ epoch: this.#o.epoch,
408
+ version: this.#o.version,
409
+ standby: this.#standby,
410
+ draining: this.#draining,
411
+ runsInFlight: this.#running.size,
412
+ notifications: this.#o.notificationBackend ?? 'unknown',
413
+ });
414
+ }
415
+ catch { }
416
+ }
417
+ #observeClock() {
418
+ const wall = this.#now();
419
+ const mono = this.#mono();
420
+ const wallDelta = wall - this.#lastWall;
421
+ const monoDelta = mono - this.#lastMono;
422
+ if (wallDelta - monoDelta > GAP_MS) {
423
+ this.#observedGap = {
424
+ from: new Date(this.#lastWall),
425
+ to: new Date(wall),
426
+ kind: monoDelta < wallDelta / 2 ? 'asleep' : 'clock-forward',
427
+ };
428
+ this.#o.log.info('scheduler observed a clock gap', {
429
+ 'namzu.schedule.gap_ms': wallDelta - monoDelta,
430
+ 'namzu.schedule.gap_kind': this.#observedGap.kind,
431
+ });
432
+ }
433
+ else if (monoDelta - wallDelta > GAP_MS) {
434
+ this.#observedGap = {
435
+ from: new Date(this.#lastWall),
436
+ to: new Date(wall),
437
+ kind: 'clock-backward',
438
+ };
439
+ this.#o.log.info('scheduler observed the clock going back', {
440
+ 'namzu.schedule.gap_ms': monoDelta - wallDelta,
441
+ });
442
+ }
443
+ this.#lastWall = wall;
444
+ this.#lastMono = mono;
445
+ }
446
+ /** One pass: every job evaluated, the queue dispatched. Public for tests. */
447
+ async tick() {
448
+ this.#observeClock();
449
+ if (!this.#draining && this.#o.fingerprint() !== this.#startFingerprint) {
450
+ this.#o.log.info('installed CLI changed; draining for a restart', {
451
+ 'namzu.schedule.epoch': this.#o.epoch,
452
+ });
453
+ this.#draining = true;
454
+ this.#persistManual();
455
+ }
456
+ const now = this.#now();
457
+ const { jobs, errors } = listJobs(this.#o.paths);
458
+ for (const error of errors) {
459
+ this.#o.log.warn('scheduled job file unreadable', {
460
+ 'namzu.schedule.path': error.path,
461
+ 'exception.message': error.message,
462
+ });
463
+ }
464
+ for (const job of jobs) {
465
+ try {
466
+ await this.#tickJob(job, now);
467
+ }
468
+ catch (error) {
469
+ this.#o.log.error('scheduled job evaluation failed', {
470
+ 'namzu.schedule.job_id': job.id,
471
+ 'exception.message': error instanceof Error ? error.message : String(error),
472
+ });
473
+ }
474
+ }
475
+ this.#observedGap = undefined;
476
+ if (!this.#draining)
477
+ await this.#dispatch(now);
478
+ // A dispatch that was under way when the drain began may have handed a
479
+ // deferred manual run back to memory.
480
+ if (this.#draining)
481
+ this.#persistManual();
482
+ if (now - this.#lastHousekeeping > HOUSEKEEPING_MS) {
483
+ this.#lastHousekeeping = now;
484
+ for (const job of jobs) {
485
+ try {
486
+ compactHistory(this.#o.paths, job.id, now);
487
+ pruneClaims(this.#o.paths, job.id, job.catchUp.windowMs + CLAIM_RETENTION_EXTRA_MS, now);
488
+ }
489
+ catch { }
490
+ }
491
+ pruneDaemonLogs(this.#o.paths.daemonLog, new Date(now));
492
+ }
493
+ this.#heartbeat();
494
+ }
495
+ async #notice(kind, job, extra) {
496
+ if (!this.#o.notifications)
497
+ return;
498
+ const flags = job.notify;
499
+ const wanted = kind === 'finished'
500
+ ? flags.finished
501
+ : kind === 'awaiting-approval'
502
+ ? flags.awaitingApproval
503
+ : kind === 'catch-up'
504
+ ? flags.finished || flags.failed
505
+ : flags.failed || kind === 'held' || kind === 'needs-confirmation';
506
+ if (!wanted)
507
+ return;
508
+ // What needs the person is never throttled: each happens at most once
509
+ // per run or per change, and a dropped one is never sent again. A park
510
+ // right after a catch-up notice would otherwise sit unannounced until
511
+ // its approval expired, with every later occurrence skipped.
512
+ const always = kind === 'held' ||
513
+ kind === 'needs-confirmation' ||
514
+ kind === 'auto-paused' ||
515
+ kind === 'awaiting-approval' ||
516
+ kind === 'approval-expired';
517
+ if (!always &&
518
+ !admitNotification(join(this.#o.paths.daemon, 'notify.json'), job.id, this.#now()))
519
+ return;
520
+ const text = noticeText(kind, job, extra);
521
+ await this.#o.notify({ jobId: job.id, ...text });
522
+ this.#o.log.info('scheduled run notification', {
523
+ 'namzu.schedule.job_id': job.id,
524
+ 'namzu.schedule.notice': kind,
525
+ });
526
+ }
527
+ async #tickJob(loaded, now) {
528
+ const paths = this.#o.paths;
529
+ let job = loaded;
530
+ let state = readState(paths, job.id);
531
+ // A confirmed job whose file no longer matches its confirmation was
532
+ // edited behind the CLI's back: hold it, say so once.
533
+ if (job.state === 'active' && !confirmationHolds(job)) {
534
+ job = updateJob(paths, job.id, job.revision, (j) => ({ ...j, state: 'pending-confirmation' }));
535
+ appendHistory(paths, job.id, {
536
+ v: 1,
537
+ kind: 'job',
538
+ at: new Date(now).toISOString(),
539
+ action: 'tampered',
540
+ by: 'daemon',
541
+ detail: 'the job file changed outside namzu; it waits for confirmation',
542
+ });
543
+ state = { ...state, lastHoldNotice: jobSecurityDigest(job) };
544
+ writeState(paths, state);
545
+ await this.#notice('held', job, { at: new Date(now) });
546
+ }
547
+ else if (job.state === 'pending-confirmation') {
548
+ const digest = jobSecurityDigest(job);
549
+ if (state.lastHoldNotice !== digest) {
550
+ state = { ...state, lastHoldNotice: digest };
551
+ writeState(paths, state);
552
+ await this.#notice('needs-confirmation', job, { at: new Date(now) });
553
+ }
554
+ }
555
+ state = await this.#reconcile(job, state, now);
556
+ job = readJob(paths, job.id) ?? job;
557
+ // A draining daemon does not evaluate. Evaluating would move
558
+ // `lastEvaluatedAt` past an occurrence it cannot queue, so the daemon
559
+ // that takes over would never see it: no run, no `missed`, and a
560
+ // one-shot left active with nothing to fire. Left alone, the successor
561
+ // evaluates from where this daemon stopped and runs it (late, as a
562
+ // scheduled or catch-up run under the job's own policy).
563
+ if (this.#draining)
564
+ return;
565
+ const evaluationState = {
566
+ ...(state.lastEvaluatedAt ? { lastEvaluatedAt: state.lastEvaluatedAt } : {}),
567
+ ...(state.jobRevision !== undefined ? { jobRevision: state.jobRevision } : {}),
568
+ ...(state.activeRun
569
+ ? { activeRun: { status: state.activeRun.status } }
570
+ : state.queued || this.#manual.some((m) => m.jobId === job.id)
571
+ ? { activeRun: { status: 'queued' } }
572
+ : {}),
573
+ ...(state.quotaHoldUntil ? { quotaHoldUntil: state.quotaHoldUntil } : {}),
574
+ };
575
+ const decision = evaluateJob({
576
+ job: {
577
+ spec: job.schedule,
578
+ state: job.state,
579
+ catchUp: job.catchUp,
580
+ createdAt: job.createdAt,
581
+ updatedAt: job.updatedAt,
582
+ revision: job.revision,
583
+ },
584
+ state: evaluationState,
585
+ now: new Date(now),
586
+ daemon: {
587
+ startedAt: new Date(this.#startedAt),
588
+ ...(this.#observedGap ? { observedGap: this.#observedGap } : {}),
589
+ },
590
+ isClaimed: (key) => isClaimed(paths, job.id, key),
591
+ });
592
+ const at = new Date(now).toISOString();
593
+ for (const skip of decision.skip) {
594
+ appendHistory(paths, job.id, {
595
+ v: 1,
596
+ kind: 'skip',
597
+ at,
598
+ scheduledFor: skip.scheduledFor.toISOString(),
599
+ reason: skip.reason,
600
+ count: skip.count,
601
+ });
602
+ }
603
+ if (decision.missed) {
604
+ appendHistory(paths, job.id, {
605
+ v: 1,
606
+ kind: 'missed',
607
+ at,
608
+ from: decision.missed.from.toISOString(),
609
+ to: decision.missed.to.toISOString(),
610
+ count: decision.missed.count,
611
+ ...(decision.missed.capped ? { capped: true } : {}),
612
+ reason: decision.missed.reason,
613
+ ...(decision.fire ? { caughtUpBy: decision.fire.key } : {}),
614
+ });
615
+ }
616
+ let next = withoutUndefined({
617
+ ...state,
618
+ lastEvaluatedAt: decision.nextState.lastEvaluatedAt.toISOString(),
619
+ jobRevision: decision.nextState.jobRevision,
620
+ nextFireAt: decision.nextState.nextFireAt?.toISOString(),
621
+ quotaHoldUntil: state.quotaHoldUntil && Date.parse(state.quotaHoldUntil) > now
622
+ ? state.quotaHoldUntil
623
+ : undefined,
624
+ });
625
+ if (decision.fire) {
626
+ next = {
627
+ ...next,
628
+ queued: {
629
+ key: decision.fire.key,
630
+ scheduledFor: decision.fire.scheduledFor.toISOString(),
631
+ trigger: decision.fire.trigger,
632
+ queuedAt: at,
633
+ },
634
+ };
635
+ if (decision.fire.trigger === 'catch-up') {
636
+ await this.#notice('catch-up', job, {
637
+ at: new Date(now),
638
+ scheduledFor: decision.fire.scheduledFor,
639
+ ...(decision.missed ? { missed: decision.missed.count } : {}),
640
+ });
641
+ }
642
+ }
643
+ writeState(paths, next);
644
+ if (decision.jobTransition && decision.jobTransition !== job.state) {
645
+ const transition = decision.jobTransition;
646
+ // A one-shot that fired completes once its run is dispatched; one
647
+ // that expired is done now.
648
+ if (transition === 'expired' || !decision.fire) {
649
+ updateJob(paths, job.id, job.revision, (j) => ({ ...j, state: transition }));
650
+ appendHistory(paths, job.id, { v: 1, kind: 'job', at, action: transition, by: 'daemon' });
651
+ }
652
+ }
653
+ }
654
+ /**
655
+ * Record how a run this daemon did not start has ended — a foreground
656
+ * `schedule run-now` — exactly as a run it started is recorded. False,
657
+ * and nothing written, when the job's state no longer names that run (a
658
+ * daemon that adopted it recorded it first).
659
+ */
660
+ async finalizeRun(jobId, runId, result) {
661
+ const job = readJob(this.#o.paths, jobId);
662
+ const run = readState(this.#o.paths, jobId).activeRun;
663
+ if (!job || run?.runId !== runId)
664
+ return false;
665
+ this.#finalize(job, run, result, this.#now());
666
+ await this.settled();
667
+ return true;
668
+ }
669
+ /**
670
+ * Settle the job's run in progress if its process is gone, as a tick
671
+ * would, without evaluating the schedule. For a foreground `run-now`
672
+ * finding a run no scheduler is left to settle.
673
+ */
674
+ async reconcileJob(jobId) {
675
+ const job = readJob(this.#o.paths, jobId);
676
+ if (!job)
677
+ return undefined;
678
+ const state = await this.#reconcile(job, readState(this.#o.paths, jobId), this.#now());
679
+ await this.settled();
680
+ return state;
681
+ }
682
+ /** Finalise or adopt the job's run in progress, if it has one. */
683
+ async #reconcile(job, state, now) {
684
+ // A run adopted earlier that the job no longer names was recorded by
685
+ // someone else (a foreground `run-now` finishing): stop counting it.
686
+ for (const [runId, tracked] of this.#running) {
687
+ if (tracked.jobId === job.id && !tracked.run && state.activeRun?.runId !== runId)
688
+ this.#running.delete(runId);
689
+ }
690
+ const run = state.activeRun;
691
+ if (!run)
692
+ return state;
693
+ const tracked = this.#running.get(run.runId);
694
+ if (tracked?.run)
695
+ return state;
696
+ const result = readRunResult(this.#o.paths, job.id, run.runId);
697
+ if (run.status === 'running') {
698
+ if (isFinal(result))
699
+ return this.#finalize(job, run, result, now);
700
+ const slug = result?.projectSlug ?? run.projectSlug;
701
+ const sessionId = result?.sessionId ?? run.sessionId;
702
+ const ref = slug && sessionId ? { home: this.#o.paths.home, projectSlug: slug, sessionId } : undefined;
703
+ if (ref && (await sessionLeaseLive(ref, now))) {
704
+ this.#running.set(run.runId, {
705
+ jobId: job.id,
706
+ runId: run.runId,
707
+ folder: job.folder.canonical,
708
+ writes: allowsWrites(job.permissions),
709
+ });
710
+ return state;
711
+ }
712
+ if (!ref && now - Date.parse(run.startedAt) < START_GRACE_MS) {
713
+ this.#running.set(run.runId, {
714
+ jobId: job.id,
715
+ runId: run.runId,
716
+ folder: job.folder.canonical,
717
+ writes: allowsWrites(job.permissions),
718
+ });
719
+ return state;
720
+ }
721
+ this.#running.delete(run.runId);
722
+ // The process is gone. The session log says how far the turn got.
723
+ const facts = ref ? await sessionFacts(ref) : null;
724
+ if (facts?.activeTurn?.paused) {
725
+ return this.#finalize(job, run, {
726
+ ...(result ?? this.#emptyResult(job, run)),
727
+ status: 'awaiting-approval',
728
+ exitCode: 0,
729
+ turnId: facts.activeTurn.turnId,
730
+ ...(ref ? { sessionId: ref.sessionId, projectSlug: ref.projectSlug } : {}),
731
+ }, now);
732
+ }
733
+ const outcome = facts && !facts.activeTurn ? turnOutcome(facts, run.turnId ?? result?.turnId) : undefined;
734
+ return this.#finalize(job, run, {
735
+ ...(result ?? this.#emptyResult(job, run)),
736
+ status: outcome?.status ?? 'interrupted',
737
+ exitCode: outcome?.status === 'completed' ? 0 : 1,
738
+ reason: outcome?.reason ?? 'the run stopped without recording a result (its process ended)',
739
+ }, now);
740
+ }
741
+ // Parked on a decision.
742
+ if (!run.sessionId || !run.projectSlug) {
743
+ return this.#finalize(job, run, { ...this.#emptyResult(job, run), status: 'interrupted', exitCode: 1 }, now);
744
+ }
745
+ const ref = { home: this.#o.paths.home, projectSlug: run.projectSlug, sessionId: run.sessionId };
746
+ const facts = await sessionFacts(ref);
747
+ if (!facts) {
748
+ return this.#finalize(job, run, {
749
+ ...this.#emptyResult(job, run),
750
+ status: 'interrupted',
751
+ exitCode: 1,
752
+ reason: 'the parked session is gone',
753
+ }, now);
754
+ }
755
+ const active = facts.activeTurn;
756
+ if (active && active.turnId === run.turnId) {
757
+ if (!active.paused)
758
+ return state; // someone resumed it; wait for it to settle
759
+ const parkedAt = Date.parse(run.parkedAt ?? run.startedAt);
760
+ if (now - parkedAt > job.approvalTtlMs && !(await sessionLeaseLive(ref, now))) {
761
+ try {
762
+ await abandonParkedTurn(ref, active.turnId, 'Scheduled run: approval expired');
763
+ }
764
+ catch (error) {
765
+ this.#o.log.warn('could not abandon an expired park', {
766
+ 'namzu.schedule.job_id': job.id,
767
+ 'exception.message': error instanceof Error ? error.message : String(error),
768
+ });
769
+ return state;
770
+ }
771
+ const expired = this.#finalize(job, run, {
772
+ ...this.#emptyResult(job, run),
773
+ status: 'approval-expired',
774
+ exitCode: 1,
775
+ reason: 'nobody answered the approval in time; the turn was abandoned',
776
+ }, now);
777
+ await this.#notice('approval-expired', job, { at: new Date(now) });
778
+ return expired;
779
+ }
780
+ return state;
781
+ }
782
+ if (active)
783
+ return state;
784
+ const outcome = turnOutcome(facts, run.turnId);
785
+ if (!outcome)
786
+ return state;
787
+ return this.#finalize(job, run, {
788
+ ...this.#emptyResult(job, run),
789
+ status: outcome.status,
790
+ exitCode: outcome.status === 'completed' ? 0 : 1,
791
+ ...(outcome.reason ? { reason: outcome.reason } : {}),
792
+ }, now);
793
+ }
794
+ #emptyResult(job, run) {
795
+ return {
796
+ v: 1,
797
+ kind: 'schedule-run-result',
798
+ runId: run.runId,
799
+ jobId: job.id,
800
+ status: 'interrupted',
801
+ exitCode: 1,
802
+ startedAt: run.startedAt,
803
+ ...(run.sessionId ? { sessionId: run.sessionId } : {}),
804
+ ...(run.projectSlug ? { projectSlug: run.projectSlug } : {}),
805
+ ...(run.turnId ? { turnId: run.turnId } : {}),
806
+ };
807
+ }
808
+ /** Record how a run ended, update counters, notify. Returns the new state (already written). */
809
+ #finalize(job, run, result, now) {
810
+ const paths = this.#o.paths;
811
+ const at = new Date(now).toISOString();
812
+ const status = result.status === 'running' ? 'interrupted' : result.status;
813
+ this.#running.delete(run.runId);
814
+ appendRunRecord(paths, job.id, run, result, at);
815
+ const current = readState(paths, job.id);
816
+ const failed = status === 'failed' ||
817
+ status === 'blocked-config' ||
818
+ status === 'timed-out' ||
819
+ status === 'interrupted' ||
820
+ status === 'approval-expired';
821
+ const counters = {
822
+ runs: current.counters.runs + (status === 'awaiting-approval' ? 0 : 1),
823
+ failures: current.counters.failures + (failed ? 1 : 0),
824
+ failureStreak: failed
825
+ ? current.counters.failureStreak + 1
826
+ : status === 'completed'
827
+ ? 0
828
+ : current.counters.failureStreak,
829
+ };
830
+ const quotaHold = status === 'failed' &&
831
+ result.retryAfterMs !== undefined &&
832
+ result.retryAfterMs > job.budget.waitForProviderMs
833
+ ? new Date(now + result.retryAfterMs).toISOString()
834
+ : current.quotaHoldUntil;
835
+ let next;
836
+ if (status === 'awaiting-approval') {
837
+ next = withoutUndefined({
838
+ ...current,
839
+ counters,
840
+ activeRun: withoutUndefined({
841
+ ...run,
842
+ status: 'awaiting-approval',
843
+ sessionId: result.sessionId ?? run.sessionId,
844
+ projectSlug: result.projectSlug ?? run.projectSlug,
845
+ turnId: result.turnId ?? run.turnId,
846
+ parkedAt: run.parkedAt ?? at,
847
+ handoff: result.handoff ? { reason: result.handoff.reason } : undefined,
848
+ }),
849
+ });
850
+ }
851
+ else {
852
+ next = withoutUndefined({
853
+ ...current,
854
+ counters,
855
+ activeRun: undefined,
856
+ quotaHoldUntil: quotaHold,
857
+ lastRun: withoutUndefined({
858
+ runId: run.runId,
859
+ scheduledFor: run.scheduledFor,
860
+ status,
861
+ endedAt: result.endedAt ?? at,
862
+ sessionId: result.sessionId,
863
+ }),
864
+ });
865
+ }
866
+ // The same failure is told once; a success clears it.
867
+ let incident = next.lastIncident;
868
+ let tell;
869
+ if (status === 'completed') {
870
+ incident = undefined;
871
+ tell = 'finished';
872
+ }
873
+ else if (status === 'awaiting-approval') {
874
+ tell = 'awaiting-approval';
875
+ }
876
+ else if (failed && status !== 'approval-expired') {
877
+ const signature = failureSignature(status, result.reason);
878
+ if (incident?.signature !== signature) {
879
+ tell =
880
+ status === 'blocked-config'
881
+ ? 'blocked-config'
882
+ : status === 'timed-out'
883
+ ? 'timed-out'
884
+ : status === 'interrupted'
885
+ ? 'interrupted'
886
+ : 'failed';
887
+ incident = { signature, at };
888
+ }
889
+ }
890
+ next = withoutUndefined({ ...next, lastIncident: incident });
891
+ writeState(paths, next);
892
+ // A failing job pauses itself rather than failing every tick forever.
893
+ const pauseAfter = job.failurePolicy.pauseAfterFailures;
894
+ let autoPaused = false;
895
+ if (failed && pauseAfter > 0 && counters.failureStreak >= pauseAfter) {
896
+ const latest = readJob(paths, job.id);
897
+ if (latest && latest.state === 'active') {
898
+ updateJob(paths, job.id, latest.revision, (j) => ({
899
+ ...j,
900
+ state: 'paused',
901
+ pausedAt: at,
902
+ pausedBy: 'auto-failure-streak',
903
+ }));
904
+ appendHistory(paths, job.id, {
905
+ v: 1,
906
+ kind: 'job',
907
+ at,
908
+ action: 'paused',
909
+ by: 'auto-failure-streak',
910
+ detail: `${counters.failureStreak} failed runs in a row`,
911
+ });
912
+ autoPaused = true;
913
+ }
914
+ }
915
+ // A one-shot that ran is complete.
916
+ if (job.schedule.kind === 'at' && status !== 'awaiting-approval' && run.trigger !== 'manual') {
917
+ const latest = readJob(paths, job.id);
918
+ if (latest && latest.state === 'active') {
919
+ updateJob(paths, job.id, latest.revision, (j) => ({ ...j, state: 'completed' }));
920
+ appendHistory(paths, job.id, { v: 1, kind: 'job', at, action: 'completed', by: 'daemon' });
921
+ }
922
+ }
923
+ const notices = [];
924
+ if (tell) {
925
+ notices.push(this.#notice(tell, job, {
926
+ at: new Date(now),
927
+ ...(result.summary ? { summary: result.summary } : {}),
928
+ ...(tell === 'awaiting-approval' && next.activeRun?.sessionId
929
+ ? { resumeCommand: resumeCommand(job, next.activeRun.sessionId) }
930
+ : {}),
931
+ ...(tell === 'awaiting-approval' && result.handoff
932
+ ? { handoff: result.handoff.reason }
933
+ : {}),
934
+ }));
935
+ }
936
+ if (autoPaused)
937
+ notices.push(this.#notice('auto-paused', job, { at: new Date(now), failures: counters.failureStreak }));
938
+ if (status === 'completed') {
939
+ notices.push(archiveOldRuns(paths, job, next).then((archived) => {
940
+ if (archived.length > 0) {
941
+ const latest = readState(paths, job.id);
942
+ writeState(paths, {
943
+ ...latest,
944
+ archivedSessions: [...(latest.archivedSessions ?? []), ...archived].slice(-200),
945
+ });
946
+ }
947
+ }, () => undefined));
948
+ }
949
+ this.#finalizing.push(Promise.allSettled(notices).then(() => undefined));
950
+ this.#o.log.info('scheduled run recorded', {
951
+ 'namzu.schedule.job_id': job.id,
952
+ 'namzu.schedule.run_id': run.runId,
953
+ 'namzu.schedule.status': status,
954
+ });
955
+ return next;
956
+ }
957
+ /** Start queued runs, oldest first, within the cap and the folder lanes. */
958
+ async #dispatch(now) {
959
+ const paths = this.#o.paths;
960
+ const queue = [];
961
+ for (const job of listJobs(paths).jobs) {
962
+ const state = readState(paths, job.id);
963
+ if (state.queued)
964
+ queue.push({ job, state });
965
+ }
966
+ queue.sort((a, b) => (a.state.queued?.queuedAt ?? '').localeCompare(b.state.queued?.queuedAt ?? ''));
967
+ for (const manual of this.#manual.splice(0)) {
968
+ const job = readJob(paths, manual.jobId);
969
+ if (job)
970
+ queue.push({ job, state: readState(paths, job.id), manual: manual.runId });
971
+ }
972
+ for (const entry of queue) {
973
+ const { job } = entry;
974
+ const queued = entry.state.queued;
975
+ if (!(await this.#stillOwner())) {
976
+ this.#o.log.warn('scheduler fenced out; not dispatching', {
977
+ 'namzu.schedule.epoch': this.#o.epoch,
978
+ });
979
+ return;
980
+ }
981
+ const latest = readJob(paths, job.id);
982
+ if (!latest || latest.state !== 'active' || !confirmationHolds(latest)) {
983
+ if (queued)
984
+ writeState(paths, withoutUndefined({ ...entry.state, queued: undefined }));
985
+ continue;
986
+ }
987
+ const writes = allowsWrites(latest.permissions);
988
+ const busyLane = writes &&
989
+ [...this.#running.values()].some((t) => t.writes && t.folder === latest.folder.canonical);
990
+ if (this.#running.size >= this.#o.maxConcurrentRuns || busyLane) {
991
+ this.#delayReasons.set(job.id, busyLane ? 'folder-busy' : 'concurrency-cap');
992
+ if (entry.manual)
993
+ this.#manual.push({ jobId: job.id, runId: entry.manual });
994
+ continue;
995
+ }
996
+ const delayReason = this.#delayReasons.get(job.id);
997
+ this.#delayReasons.delete(job.id);
998
+ // A manual run queued on disk (during a drain) keeps the run id its key names.
999
+ const queuedManual = !entry.manual && queued?.trigger === 'manual' && queued.key.startsWith(MANUAL_KEY_PREFIX)
1000
+ ? queued.key.slice(MANUAL_KEY_PREFIX.length)
1001
+ : undefined;
1002
+ const runId = entry.manual ?? queuedManual ?? generateScheduleRunId();
1003
+ const key = entry.manual ? `${MANUAL_KEY_PREFIX}${runId}` : queued?.key;
1004
+ const trigger = entry.manual ? 'manual' : (queued?.trigger ?? 'scheduled');
1005
+ if (!claimOccurrence(paths, {
1006
+ jobId: job.id,
1007
+ key,
1008
+ runId,
1009
+ daemonEpoch: this.#o.epoch,
1010
+ at: new Date(now).toISOString(),
1011
+ })) {
1012
+ // Somebody already started this occurrence.
1013
+ if (queued)
1014
+ writeState(paths, withoutUndefined({ ...readState(paths, job.id), queued: undefined }));
1015
+ continue;
1016
+ }
1017
+ const delayedMs = queued && trigger !== 'manual' ? Math.max(0, now - Date.parse(queued.queuedAt)) : 0;
1018
+ const run = withoutUndefined({
1019
+ runId,
1020
+ key,
1021
+ trigger,
1022
+ scheduledFor: trigger === 'manual' ? undefined : queued?.scheduledFor,
1023
+ startedAt: new Date(now).toISOString(),
1024
+ daemonEpoch: this.#o.epoch,
1025
+ status: 'running',
1026
+ delayedMs: delayedMs >= 1_000 ? delayedMs : undefined,
1027
+ delayReason: delayedMs >= 1_000 ? (delayReason ?? 'concurrency-cap') : undefined,
1028
+ });
1029
+ writeState(paths, withoutUndefined({ ...readState(paths, job.id), queued: undefined, activeRun: run }));
1030
+ appendHistory(paths, job.id, {
1031
+ v: 1,
1032
+ kind: 'run',
1033
+ at: run.startedAt,
1034
+ runId,
1035
+ key,
1036
+ trigger,
1037
+ ...(run.scheduledFor ? { scheduledFor: run.scheduledFor } : {}),
1038
+ startedAt: run.startedAt,
1039
+ ...(run.delayedMs
1040
+ ? { delayedMs: run.delayedMs, delayReason: run.delayReason ?? 'concurrency-cap' }
1041
+ : {}),
1042
+ status: 'running',
1043
+ });
1044
+ let spawned;
1045
+ try {
1046
+ spawned = this.#o.spawnFire({
1047
+ job: latest,
1048
+ runId,
1049
+ key,
1050
+ trigger,
1051
+ ...(run.scheduledFor ? { scheduledFor: run.scheduledFor } : {}),
1052
+ epoch: this.#o.epoch,
1053
+ });
1054
+ }
1055
+ catch (error) {
1056
+ this.#finalize(latest, run, {
1057
+ ...this.#emptyResult(latest, run),
1058
+ status: 'failed',
1059
+ exitCode: 1,
1060
+ reason: `the run could not be started: ${error instanceof Error ? error.message : String(error)}`,
1061
+ }, now);
1062
+ continue;
1063
+ }
1064
+ this.#running.set(runId, {
1065
+ jobId: job.id,
1066
+ runId,
1067
+ folder: latest.folder.canonical,
1068
+ writes,
1069
+ run: spawned,
1070
+ });
1071
+ this.#o.log.info('scheduled run started', {
1072
+ 'namzu.schedule.job_id': job.id,
1073
+ 'namzu.schedule.run_id': runId,
1074
+ 'namzu.schedule.trigger': trigger,
1075
+ });
1076
+ void spawned.exited.then(() => {
1077
+ const tracked = this.#running.get(runId);
1078
+ if (tracked)
1079
+ this.#running.set(runId, { ...tracked, run: undefined });
1080
+ const jobNow = readJob(paths, job.id) ?? latest;
1081
+ const state = readState(paths, job.id);
1082
+ if (state.activeRun?.runId === runId) {
1083
+ const result = readRunResult(paths, job.id, runId);
1084
+ this.#finalize(jobNow, state.activeRun, isFinal(result)
1085
+ ? result
1086
+ : {
1087
+ ...this.#emptyResult(jobNow, state.activeRun),
1088
+ ...(result ?? {}),
1089
+ status: 'interrupted',
1090
+ exitCode: 1,
1091
+ reason: 'the run ended without recording a result',
1092
+ }, this.#now());
1093
+ }
1094
+ else {
1095
+ this.#running.delete(runId);
1096
+ // The job was removed (`remove --force`) while this run went on:
1097
+ // nothing will ever settle it, so its end is written here, where
1098
+ // `prune` finds it.
1099
+ if (!readJob(paths, job.id)) {
1100
+ const result = readRunResult(paths, job.id, runId);
1101
+ appendRunRecord(paths, job.id, run, isFinal(result)
1102
+ ? result
1103
+ : {
1104
+ ...this.#emptyResult(latest, run),
1105
+ ...(result ?? {}),
1106
+ status: 'interrupted',
1107
+ exitCode: 1,
1108
+ reason: 'the run ended without recording a result',
1109
+ }, new Date(this.#now()).toISOString());
1110
+ }
1111
+ }
1112
+ this.wake();
1113
+ });
1114
+ }
1115
+ }
1116
+ /** For tests: wait until notifications and archiving started by finalisation are done. */
1117
+ async settled() {
1118
+ await Promise.allSettled(this.#finalizing.splice(0));
1119
+ }
1120
+ }
1121
+ /** A manual run's occurrence key is this and its run id. */
1122
+ export const MANUAL_KEY_PREFIX = 'manual-';
1123
+ /**
1124
+ * Append the record of how a run ended (or parked) to its job's history. A
1125
+ * run still `running` is recorded `interrupted`: the record is final.
1126
+ */
1127
+ export function appendRunRecord(paths, jobId, run, result, at) {
1128
+ const status = result.status === 'running' ? 'interrupted' : result.status;
1129
+ appendHistory(paths, jobId, {
1130
+ v: 1,
1131
+ kind: 'run',
1132
+ at,
1133
+ runId: run.runId,
1134
+ key: run.key,
1135
+ trigger: run.trigger,
1136
+ ...(run.scheduledFor ? { scheduledFor: run.scheduledFor } : {}),
1137
+ startedAt: run.startedAt,
1138
+ ...(status === 'awaiting-approval' ? {} : { endedAt: result.endedAt ?? at }),
1139
+ ...(run.delayedMs
1140
+ ? { delayedMs: run.delayedMs, delayReason: run.delayReason ?? 'concurrency-cap' }
1141
+ : {}),
1142
+ ...(result.sessionId ? { sessionId: result.sessionId } : {}),
1143
+ status,
1144
+ ...(result.reason ? { reason: result.reason } : {}),
1145
+ exitCode: result.exitCode,
1146
+ ...(result.summary ? { summary: result.summary } : {}),
1147
+ ...(result.usage ? { usage: result.usage } : {}),
1148
+ ...(result.warnings ? { warnings: result.warnings } : {}),
1149
+ });
1150
+ }
1151
+ /** Where `schedule stop` leaves its request. See `ScheduleDaemon#stopRequested`. */
1152
+ export function stopRequestPath(paths) {
1153
+ return join(paths.daemon, 'stop.json');
1154
+ }
1155
+ /** Ask every daemon of this home, owner or standby, to stop; or clear the request. */
1156
+ export function requestStop(paths, stop) {
1157
+ if (stop)
1158
+ writeJsonAtomic(stopRequestPath(paths), {
1159
+ v: 1,
1160
+ kind: 'schedule-stop',
1161
+ at: new Date().toISOString(),
1162
+ });
1163
+ else
1164
+ rmSync(stopRequestPath(paths), { force: true });
1165
+ }
1166
+ /** Spawn `node <bin> schedule __fire …` with its output going to a log file, not a pipe. */
1167
+ export function spawnFireProcess(options) {
1168
+ return (request) => {
1169
+ const logPath = options.paths.runLog(request.job.id, request.runId);
1170
+ ensureDir(dirname(logPath));
1171
+ writeFileSync(logPath, '', { flag: 'a', mode: PRIVATE_FILE_MODE });
1172
+ const fd = openSync(logPath, 'a', PRIVATE_FILE_MODE);
1173
+ let child;
1174
+ try {
1175
+ child = spawn(options.node, [
1176
+ options.bin,
1177
+ 'schedule',
1178
+ '__fire',
1179
+ '--home',
1180
+ options.paths.home,
1181
+ '--job',
1182
+ request.job.id,
1183
+ '--run',
1184
+ request.runId,
1185
+ '--key',
1186
+ request.key,
1187
+ '--revision',
1188
+ String(request.job.revision),
1189
+ '--trigger',
1190
+ request.trigger,
1191
+ ...(request.scheduledFor ? ['--scheduled-for', request.scheduledFor] : []),
1192
+ '--epoch',
1193
+ request.epoch,
1194
+ ], {
1195
+ cwd: request.job.folder.canonical,
1196
+ env: childEnvironment(options.env, options.paths.home),
1197
+ stdio: ['ignore', fd, fd],
1198
+ detached: process.platform !== 'win32',
1199
+ windowsHide: true,
1200
+ });
1201
+ }
1202
+ finally {
1203
+ closeSync(fd);
1204
+ }
1205
+ const exited = new Promise((resolve) => {
1206
+ child.once('exit', (code) => resolve(code));
1207
+ child.once('error', () => resolve(null));
1208
+ });
1209
+ child.unref();
1210
+ return {
1211
+ exited,
1212
+ terminate: () => {
1213
+ try {
1214
+ child.kill('SIGTERM');
1215
+ }
1216
+ catch { }
1217
+ },
1218
+ };
1219
+ };
1220
+ }
1221
+ /**
1222
+ * The CLI as installed, cheaply: its version file, its entry point and the
1223
+ * scheduler's own modules, by size and time. An npm install rewrites every
1224
+ * file; a rebuild rewrites the modules that changed.
1225
+ */
1226
+ export function installedFingerprint(bin) {
1227
+ const dist = dirname(bin);
1228
+ const watched = [
1229
+ join(dist, '..', 'package.json'),
1230
+ bin,
1231
+ join(dist, 'schedule', 'daemon', 'daemon.js'),
1232
+ join(dist, 'schedule', 'fire', 'fire.js'),
1233
+ ];
1234
+ return () => {
1235
+ const parts = [];
1236
+ for (const path of watched) {
1237
+ try {
1238
+ const st = statSync(path);
1239
+ parts.push(`${st.size}:${st.mtimeMs}`);
1240
+ }
1241
+ catch {
1242
+ parts.push('missing');
1243
+ }
1244
+ }
1245
+ return parts.join('|');
1246
+ };
1247
+ }
1248
+ //# sourceMappingURL=daemon.js.map