agent-tempo 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (484) hide show
  1. package/CLAUDE.md +213 -0
  2. package/LICENSE +21 -0
  3. package/README.md +289 -0
  4. package/assets/icon-32.png +0 -0
  5. package/assets/icon-512.png +0 -0
  6. package/assets/icon-64.png +0 -0
  7. package/assets/icon-dark-32.png +0 -0
  8. package/assets/icon-dark-64.png +0 -0
  9. package/assets/icon-dark.svg +9 -0
  10. package/assets/icon.svg +9 -0
  11. package/assets/logo-dark.svg +11 -0
  12. package/assets/logo-light.svg +11 -0
  13. package/dashboard/README.md +91 -0
  14. package/dashboard/dist/assets/index-CB78ToNE.css +2 -0
  15. package/dashboard/dist/assets/index-_5jV0Znu.js +62 -0
  16. package/dashboard/dist/assets/index-_5jV0Znu.js.map +1 -0
  17. package/dashboard/dist/index.html +21 -0
  18. package/dashboard/package.json +47 -0
  19. package/dist/activities/hard-terminate.d.ts +32 -0
  20. package/dist/activities/hard-terminate.js +460 -0
  21. package/dist/activities/maestro.d.ts +72 -0
  22. package/dist/activities/maestro.js +254 -0
  23. package/dist/activities/outbox.d.ts +188 -0
  24. package/dist/activities/outbox.js +849 -0
  25. package/dist/activities/resolve.d.ts +64 -0
  26. package/dist/activities/resolve.js +129 -0
  27. package/dist/activities/schedule-fire.d.ts +36 -0
  28. package/dist/activities/schedule-fire.js +147 -0
  29. package/dist/adapters/base.d.ts +426 -0
  30. package/dist/adapters/base.js +1270 -0
  31. package/dist/adapters/claude-api/adapter.d.ts +168 -0
  32. package/dist/adapters/claude-api/adapter.js +797 -0
  33. package/dist/adapters/claude-api/api-error.d.ts +96 -0
  34. package/dist/adapters/claude-api/api-error.js +191 -0
  35. package/dist/adapters/claude-api/index.d.ts +16 -0
  36. package/dist/adapters/claude-api/index.js +21 -0
  37. package/dist/adapters/claude-api/mcp-bridge.d.ts +50 -0
  38. package/dist/adapters/claude-api/mcp-bridge.js +157 -0
  39. package/dist/adapters/claude-code/adapter.d.ts +133 -0
  40. package/dist/adapters/claude-code/adapter.js +274 -0
  41. package/dist/adapters/claude-code/index.d.ts +15 -0
  42. package/dist/adapters/claude-code/index.js +20 -0
  43. package/dist/adapters/claude-code-headless/adapter.d.ts +131 -0
  44. package/dist/adapters/claude-code-headless/adapter.js +710 -0
  45. package/dist/adapters/claude-code-headless/error-mapper.d.ts +107 -0
  46. package/dist/adapters/claude-code-headless/error-mapper.js +281 -0
  47. package/dist/adapters/claude-code-headless/index.d.ts +17 -0
  48. package/dist/adapters/claude-code-headless/index.js +26 -0
  49. package/dist/adapters/claude-code-headless/pre-flight.d.ts +51 -0
  50. package/dist/adapters/claude-code-headless/pre-flight.js +207 -0
  51. package/dist/adapters/claude-code-headless/prompt.d.ts +93 -0
  52. package/dist/adapters/claude-code-headless/prompt.js +79 -0
  53. package/dist/adapters/claude-code-headless/stream-json.d.ts +242 -0
  54. package/dist/adapters/claude-code-headless/stream-json.js +208 -0
  55. package/dist/adapters/claude-code-headless/types.d.ts +28 -0
  56. package/dist/adapters/claude-code-headless/types.js +36 -0
  57. package/dist/adapters/copilot/adapter.d.ts +100 -0
  58. package/dist/adapters/copilot/adapter.js +730 -0
  59. package/dist/adapters/copilot/index.d.ts +15 -0
  60. package/dist/adapters/copilot/index.js +20 -0
  61. package/dist/adapters/index.d.ts +42 -0
  62. package/dist/adapters/index.js +115 -0
  63. package/dist/adapters/opencode/adapter.d.ts +82 -0
  64. package/dist/adapters/opencode/adapter.js +710 -0
  65. package/dist/adapters/opencode/config.d.ts +90 -0
  66. package/dist/adapters/opencode/config.js +137 -0
  67. package/dist/adapters/opencode/helpers.d.ts +40 -0
  68. package/dist/adapters/opencode/helpers.js +144 -0
  69. package/dist/adapters/opencode/index.d.ts +12 -0
  70. package/dist/adapters/opencode/index.js +17 -0
  71. package/dist/adapters/opencode/server-bridge.d.ts +124 -0
  72. package/dist/adapters/opencode/server-bridge.js +216 -0
  73. package/dist/adapters/sdk/base.d.ts +95 -0
  74. package/dist/adapters/sdk/base.js +134 -0
  75. package/dist/adapters/sdk/system-prompt.d.ts +64 -0
  76. package/dist/adapters/sdk/system-prompt.js +78 -0
  77. package/dist/adapters/terminal-error.d.ts +27 -0
  78. package/dist/adapters/terminal-error.js +39 -0
  79. package/dist/channel.d.ts +3 -0
  80. package/dist/channel.js +48 -0
  81. package/dist/cli/commands.d.ts +245 -0
  82. package/dist/cli/commands.js +2438 -0
  83. package/dist/cli/config-command.d.ts +8 -0
  84. package/dist/cli/config-command.js +254 -0
  85. package/dist/cli/daemon-command.d.ts +57 -0
  86. package/dist/cli/daemon-command.js +493 -0
  87. package/dist/cli/daemon.d.ts +217 -0
  88. package/dist/cli/daemon.js +632 -0
  89. package/dist/cli/dashboard-command.d.ts +20 -0
  90. package/dist/cli/dashboard-command.js +241 -0
  91. package/dist/cli/dev-banner.d.ts +107 -0
  92. package/dist/cli/dev-banner.js +190 -0
  93. package/dist/cli/dev-mode-bootstrap.d.ts +29 -0
  94. package/dist/cli/dev-mode-bootstrap.js +36 -0
  95. package/dist/cli/dev-verbs.d.ts +43 -0
  96. package/dist/cli/dev-verbs.js +254 -0
  97. package/dist/cli/help-text.d.ts +1 -0
  98. package/dist/cli/help-text.js +158 -0
  99. package/dist/cli/legacy-migration.d.ts +35 -0
  100. package/dist/cli/legacy-migration.js +335 -0
  101. package/dist/cli/mcp.d.ts +8 -0
  102. package/dist/cli/mcp.js +63 -0
  103. package/dist/cli/output.d.ts +12 -0
  104. package/dist/cli/output.js +37 -0
  105. package/dist/cli/preflight.d.ts +9 -0
  106. package/dist/cli/preflight.js +96 -0
  107. package/dist/cli/removed-verbs.d.ts +9 -0
  108. package/dist/cli/removed-verbs.js +78 -0
  109. package/dist/cli/sa-preflight.d.ts +99 -0
  110. package/dist/cli/sa-preflight.js +183 -0
  111. package/dist/cli/scenarios-command.d.ts +6 -0
  112. package/dist/cli/scenarios-command.js +167 -0
  113. package/dist/cli/startup.d.ts +112 -0
  114. package/dist/cli/startup.js +641 -0
  115. package/dist/cli/upgrade-command.d.ts +5 -0
  116. package/dist/cli/upgrade-command.js +240 -0
  117. package/dist/cli.d.ts +2 -0
  118. package/dist/cli.js +680 -0
  119. package/dist/client/core.d.ts +33 -0
  120. package/dist/client/core.js +1260 -0
  121. package/dist/client/ensure-conductor-spawned.d.ts +35 -0
  122. package/dist/client/ensure-conductor-spawned.js +48 -0
  123. package/dist/client/index.d.ts +32 -0
  124. package/dist/client/index.js +22 -0
  125. package/dist/client/interface.d.ts +461 -0
  126. package/dist/client/interface.js +2 -0
  127. package/dist/client/subscribe.d.ts +108 -0
  128. package/dist/client/subscribe.js +598 -0
  129. package/dist/client/with-spawn.d.ts +27 -0
  130. package/dist/client/with-spawn.js +87 -0
  131. package/dist/config.d.ts +323 -0
  132. package/dist/config.js +593 -0
  133. package/dist/connection.d.ts +7 -0
  134. package/dist/connection.js +46 -0
  135. package/dist/constants.d.ts +50 -0
  136. package/dist/constants.js +74 -0
  137. package/dist/copilot-bridge.d.ts +22 -0
  138. package/dist/copilot-bridge.js +565 -0
  139. package/dist/daemon-adapter-versions.d.ts +52 -0
  140. package/dist/daemon-adapter-versions.js +170 -0
  141. package/dist/daemon.d.ts +275 -0
  142. package/dist/daemon.js +989 -0
  143. package/dist/ensemble/agent-types.d.ts +23 -0
  144. package/dist/ensemble/agent-types.js +132 -0
  145. package/dist/ensemble/loader.d.ts +14 -0
  146. package/dist/ensemble/loader.js +140 -0
  147. package/dist/ensemble/saver.d.ts +49 -0
  148. package/dist/ensemble/saver.js +201 -0
  149. package/dist/ensemble/schema.d.ts +71 -0
  150. package/dist/ensemble/schema.js +3 -0
  151. package/dist/git-info.d.ts +4 -0
  152. package/dist/git-info.js +29 -0
  153. package/dist/http/aggregate.d.ts +319 -0
  154. package/dist/http/aggregate.js +684 -0
  155. package/dist/http/auth.d.ts +67 -0
  156. package/dist/http/auth.js +177 -0
  157. package/dist/http/body.d.ts +71 -0
  158. package/dist/http/body.js +121 -0
  159. package/dist/http/catalog.d.ts +67 -0
  160. package/dist/http/catalog.js +209 -0
  161. package/dist/http/cors.d.ts +42 -0
  162. package/dist/http/cors.js +111 -0
  163. package/dist/http/dashboard-pair.d.ts +94 -0
  164. package/dist/http/dashboard-pair.js +148 -0
  165. package/dist/http/dashboard.d.ts +20 -0
  166. package/dist/http/dashboard.js +160 -0
  167. package/dist/http/event-bus.d.ts +217 -0
  168. package/dist/http/event-bus.js +365 -0
  169. package/dist/http/event-id.d.ts +77 -0
  170. package/dist/http/event-id.js +117 -0
  171. package/dist/http/event-types.d.ts +348 -0
  172. package/dist/http/event-types.js +36 -0
  173. package/dist/http/fixtures/chat-stress.d.ts +8 -0
  174. package/dist/http/fixtures/chat-stress.js +63 -0
  175. package/dist/http/fixtures/conductor-leaving.d.ts +8 -0
  176. package/dist/http/fixtures/conductor-leaving.js +80 -0
  177. package/dist/http/fixtures/constants.d.ts +10 -0
  178. package/dist/http/fixtures/constants.js +13 -0
  179. package/dist/http/fixtures/eight-player-broadcast.d.ts +10 -0
  180. package/dist/http/fixtures/eight-player-broadcast.js +81 -0
  181. package/dist/http/fixtures/empty-ensemble.d.ts +6 -0
  182. package/dist/http/fixtures/empty-ensemble.js +26 -0
  183. package/dist/http/fixtures/index.d.ts +55 -0
  184. package/dist/http/fixtures/index.js +110 -0
  185. package/dist/http/fixtures/single-conductor.d.ts +7 -0
  186. package/dist/http/fixtures/single-conductor.js +46 -0
  187. package/dist/http/fixtures/sse-reconnect.d.ts +8 -0
  188. package/dist/http/fixtures/sse-reconnect.js +77 -0
  189. package/dist/http/index.d.ts +21 -0
  190. package/dist/http/index.js +61 -0
  191. package/dist/http/port-file.d.ts +22 -0
  192. package/dist/http/port-file.js +132 -0
  193. package/dist/http/responses.d.ts +27 -0
  194. package/dist/http/responses.js +40 -0
  195. package/dist/http/ring-buffer.d.ts +41 -0
  196. package/dist/http/ring-buffer.js +80 -0
  197. package/dist/http/server.d.ts +122 -0
  198. package/dist/http/server.js +459 -0
  199. package/dist/http/snapshot.d.ts +85 -0
  200. package/dist/http/snapshot.js +180 -0
  201. package/dist/http/sse-handler.d.ts +87 -0
  202. package/dist/http/sse-handler.js +294 -0
  203. package/dist/http/writes.d.ts +55 -0
  204. package/dist/http/writes.js +240 -0
  205. package/dist/palette/index.d.ts +138 -0
  206. package/dist/palette/index.js +221 -0
  207. package/dist/reconcile/orphans.d.ts +255 -0
  208. package/dist/reconcile/orphans.js +340 -0
  209. package/dist/scripts/258-spotcheck.js +303 -0
  210. package/dist/scripts/check-components-css-sync.js +199 -0
  211. package/dist/scripts/run-shard.js +121 -0
  212. package/dist/scripts/verify-daemon-isolation-guard.js +128 -0
  213. package/dist/server-tools.d.ts +87 -0
  214. package/dist/server-tools.js +146 -0
  215. package/dist/server.d.ts +2 -0
  216. package/dist/server.js +366 -0
  217. package/dist/spawn.d.ts +296 -0
  218. package/dist/spawn.js +747 -0
  219. package/dist/tools/agent-types.d.ts +2 -0
  220. package/dist/tools/agent-types.js +21 -0
  221. package/dist/tools/attachment-info.d.ts +4 -0
  222. package/dist/tools/attachment-info.js +48 -0
  223. package/dist/tools/broadcast.d.ts +4 -0
  224. package/dist/tools/broadcast.js +76 -0
  225. package/dist/tools/cancel-stage.d.ts +3 -0
  226. package/dist/tools/cancel-stage.js +20 -0
  227. package/dist/tools/clear-state.d.ts +3 -0
  228. package/dist/tools/clear-state.js +37 -0
  229. package/dist/tools/coat-check-evict.d.ts +4 -0
  230. package/dist/tools/coat-check-evict.js +43 -0
  231. package/dist/tools/coat-check-get.d.ts +4 -0
  232. package/dist/tools/coat-check-get.js +56 -0
  233. package/dist/tools/coat-check-list.d.ts +4 -0
  234. package/dist/tools/coat-check-list.js +60 -0
  235. package/dist/tools/coat-check-put.d.ts +4 -0
  236. package/dist/tools/coat-check-put.js +53 -0
  237. package/dist/tools/cue.d.ts +44 -0
  238. package/dist/tools/cue.js +201 -0
  239. package/dist/tools/destroy.d.ts +4 -0
  240. package/dist/tools/destroy.js +188 -0
  241. package/dist/tools/detach.d.ts +4 -0
  242. package/dist/tools/detach.js +45 -0
  243. package/dist/tools/encore.d.ts +4 -0
  244. package/dist/tools/encore.js +31 -0
  245. package/dist/tools/ensemble.d.ts +32 -0
  246. package/dist/tools/ensemble.js +198 -0
  247. package/dist/tools/evaluate-gate.d.ts +3 -0
  248. package/dist/tools/evaluate-gate.js +32 -0
  249. package/dist/tools/fetch-state.d.ts +13 -0
  250. package/dist/tools/fetch-state.js +78 -0
  251. package/dist/tools/gates.d.ts +3 -0
  252. package/dist/tools/gates.js +41 -0
  253. package/dist/tools/helpers.d.ts +21 -0
  254. package/dist/tools/helpers.js +25 -0
  255. package/dist/tools/hosts.d.ts +4 -0
  256. package/dist/tools/hosts.js +40 -0
  257. package/dist/tools/listen.d.ts +3 -0
  258. package/dist/tools/listen.js +22 -0
  259. package/dist/tools/load-lineup.d.ts +5 -0
  260. package/dist/tools/load-lineup.js +381 -0
  261. package/dist/tools/migrate.d.ts +4 -0
  262. package/dist/tools/migrate.js +60 -0
  263. package/dist/tools/pause-ensemble.d.ts +4 -0
  264. package/dist/tools/pause-ensemble.js +58 -0
  265. package/dist/tools/pause.d.ts +4 -0
  266. package/dist/tools/pause.js +36 -0
  267. package/dist/tools/play.d.ts +4 -0
  268. package/dist/tools/play.js +57 -0
  269. package/dist/tools/quality-gate.d.ts +3 -0
  270. package/dist/tools/quality-gate.js +26 -0
  271. package/dist/tools/recall.d.ts +3 -0
  272. package/dist/tools/recall.js +32 -0
  273. package/dist/tools/recruit.d.ts +38 -0
  274. package/dist/tools/recruit.js +447 -0
  275. package/dist/tools/release.d.ts +4 -0
  276. package/dist/tools/release.js +98 -0
  277. package/dist/tools/report.d.ts +3 -0
  278. package/dist/tools/report.js +29 -0
  279. package/dist/tools/resolve.d.ts +1 -0
  280. package/dist/tools/resolve.js +7 -0
  281. package/dist/tools/restart.d.ts +35 -0
  282. package/dist/tools/restart.js +131 -0
  283. package/dist/tools/restore.d.ts +4 -0
  284. package/dist/tools/restore.js +107 -0
  285. package/dist/tools/resume-ensemble.d.ts +4 -0
  286. package/dist/tools/resume-ensemble.js +79 -0
  287. package/dist/tools/save-lineup.d.ts +4 -0
  288. package/dist/tools/save-lineup.js +36 -0
  289. package/dist/tools/save-state.d.ts +3 -0
  290. package/dist/tools/save-state.js +57 -0
  291. package/dist/tools/schedule.d.ts +4 -0
  292. package/dist/tools/schedule.js +152 -0
  293. package/dist/tools/schedules.d.ts +4 -0
  294. package/dist/tools/schedules.js +54 -0
  295. package/dist/tools/set-ensemble-description.d.ts +4 -0
  296. package/dist/tools/set-ensemble-description.js +37 -0
  297. package/dist/tools/set-name.d.ts +4 -0
  298. package/dist/tools/set-name.js +45 -0
  299. package/dist/tools/set-part.d.ts +3 -0
  300. package/dist/tools/set-part.js +20 -0
  301. package/dist/tools/shutdown.d.ts +4 -0
  302. package/dist/tools/shutdown.js +54 -0
  303. package/dist/tools/stage.d.ts +3 -0
  304. package/dist/tools/stage.js +28 -0
  305. package/dist/tools/stages.d.ts +3 -0
  306. package/dist/tools/stages.js +35 -0
  307. package/dist/tools/stop.d.ts +4 -0
  308. package/dist/tools/stop.js +29 -0
  309. package/dist/tools/unschedule.d.ts +4 -0
  310. package/dist/tools/unschedule.js +35 -0
  311. package/dist/tools/who-am-i.d.ts +3 -0
  312. package/dist/tools/who-am-i.js +34 -0
  313. package/dist/tools/worktree.d.ts +4 -0
  314. package/dist/tools/worktree.js +181 -0
  315. package/dist/tui/App.d.ts +85 -0
  316. package/dist/tui/App.js +1791 -0
  317. package/dist/tui/bootstrap-types.d.ts +46 -0
  318. package/dist/tui/bootstrap-types.js +7 -0
  319. package/dist/tui/client.d.ts +6 -0
  320. package/dist/tui/client.js +9 -0
  321. package/dist/tui/commands.d.ts +71 -0
  322. package/dist/tui/commands.js +1375 -0
  323. package/dist/tui/components/ActivityLog.d.ts +16 -0
  324. package/dist/tui/components/ActivityLog.js +36 -0
  325. package/dist/tui/components/ChatView.d.ts +35 -0
  326. package/dist/tui/components/ChatView.js +54 -0
  327. package/dist/tui/components/CommandOverlay.d.ts +15 -0
  328. package/dist/tui/components/CommandOverlay.js +34 -0
  329. package/dist/tui/components/CommandPalette.d.ts +21 -0
  330. package/dist/tui/components/CommandPalette.js +67 -0
  331. package/dist/tui/components/ConductorChat.d.ts +16 -0
  332. package/dist/tui/components/ConductorChat.js +32 -0
  333. package/dist/tui/components/ConversationStream.d.ts +114 -0
  334. package/dist/tui/components/ConversationStream.js +307 -0
  335. package/dist/tui/components/CreateEnsembleWizard.d.ts +19 -0
  336. package/dist/tui/components/CreateEnsembleWizard.js +223 -0
  337. package/dist/tui/components/DestroyConfirmModal.d.ts +17 -0
  338. package/dist/tui/components/DestroyConfirmModal.js +62 -0
  339. package/dist/tui/components/EnsembleListView.d.ts +14 -0
  340. package/dist/tui/components/EnsembleListView.js +32 -0
  341. package/dist/tui/components/EnsemblePanel.d.ts +12 -0
  342. package/dist/tui/components/EnsemblePanel.js +40 -0
  343. package/dist/tui/components/ErrorView.d.ts +31 -0
  344. package/dist/tui/components/ErrorView.js +129 -0
  345. package/dist/tui/components/HomeView.d.ts +54 -0
  346. package/dist/tui/components/HomeView.js +306 -0
  347. package/dist/tui/components/InputBar.d.ts +13 -0
  348. package/dist/tui/components/InputBar.js +58 -0
  349. package/dist/tui/components/LoadLineupModal.d.ts +18 -0
  350. package/dist/tui/components/LoadLineupModal.js +79 -0
  351. package/dist/tui/components/MainView.d.ts +21 -0
  352. package/dist/tui/components/MainView.js +107 -0
  353. package/dist/tui/components/NewEnsembleModal.d.ts +9 -0
  354. package/dist/tui/components/NewEnsembleModal.js +73 -0
  355. package/dist/tui/components/Picker.d.ts +23 -0
  356. package/dist/tui/components/Picker.js +70 -0
  357. package/dist/tui/components/PlayerDetailView.d.ts +26 -0
  358. package/dist/tui/components/PlayerDetailView.js +118 -0
  359. package/dist/tui/components/PromptArea.d.ts +50 -0
  360. package/dist/tui/components/PromptArea.js +303 -0
  361. package/dist/tui/components/RecruitWizard.d.ts +17 -0
  362. package/dist/tui/components/RecruitWizard.js +221 -0
  363. package/dist/tui/components/RestoreConfirmModal.d.ts +18 -0
  364. package/dist/tui/components/RestoreConfirmModal.js +71 -0
  365. package/dist/tui/components/ScheduleOverlay.d.ts +13 -0
  366. package/dist/tui/components/ScheduleOverlay.js +113 -0
  367. package/dist/tui/components/ScheduleWizard.d.ts +19 -0
  368. package/dist/tui/components/ScheduleWizard.js +259 -0
  369. package/dist/tui/components/Splash.d.ts +23 -0
  370. package/dist/tui/components/Splash.js +221 -0
  371. package/dist/tui/components/StatusBar.d.ts +48 -0
  372. package/dist/tui/components/StatusBar.js +128 -0
  373. package/dist/tui/components/StatusOverlay.d.ts +15 -0
  374. package/dist/tui/components/StatusOverlay.js +76 -0
  375. package/dist/tui/components/TitleBar.d.ts +10 -0
  376. package/dist/tui/components/TitleBar.js +21 -0
  377. package/dist/tui/components/TopBar.d.ts +12 -0
  378. package/dist/tui/components/TopBar.js +15 -0
  379. package/dist/tui/core-api.d.ts +26 -0
  380. package/dist/tui/core-api.js +67 -0
  381. package/dist/tui/hooks/useEnsembleDiscovery.d.ts +3 -0
  382. package/dist/tui/hooks/useEnsembleDiscovery.js +30 -0
  383. package/dist/tui/hooks/useMaestroPoller.d.ts +3 -0
  384. package/dist/tui/hooks/useMaestroPoller.js +36 -0
  385. package/dist/tui/hooks/useSendCommand.d.ts +7 -0
  386. package/dist/tui/hooks/useSendCommand.js +29 -0
  387. package/dist/tui/index.d.ts +15 -0
  388. package/dist/tui/index.js +156 -0
  389. package/dist/tui/ink-context.d.ts +18 -0
  390. package/dist/tui/ink-context.js +59 -0
  391. package/dist/tui/ink-loader.d.ts +26 -0
  392. package/dist/tui/ink-loader.js +42 -0
  393. package/dist/tui/removed-commands.d.ts +9 -0
  394. package/dist/tui/removed-commands.js +22 -0
  395. package/dist/tui/sse-handler.d.ts +52 -0
  396. package/dist/tui/sse-handler.js +157 -0
  397. package/dist/tui/store.d.ts +598 -0
  398. package/dist/tui/store.js +753 -0
  399. package/dist/tui/utils/format.d.ts +56 -0
  400. package/dist/tui/utils/format.js +155 -0
  401. package/dist/tui/utils/fullscreen.d.ts +23 -0
  402. package/dist/tui/utils/fullscreen.js +71 -0
  403. package/dist/tui/utils/history.d.ts +10 -0
  404. package/dist/tui/utils/history.js +85 -0
  405. package/dist/tui/utils/platform.d.ts +45 -0
  406. package/dist/tui/utils/platform.js +258 -0
  407. package/dist/tui/utils/theme.d.ts +21 -0
  408. package/dist/tui/utils/theme.js +24 -0
  409. package/dist/types.d.ts +1020 -0
  410. package/dist/types.js +39 -0
  411. package/dist/utils/attachment-format.d.ts +22 -0
  412. package/dist/utils/attachment-format.js +32 -0
  413. package/dist/utils/default-part.d.ts +43 -0
  414. package/dist/utils/default-part.js +104 -0
  415. package/dist/utils/duration.d.ts +30 -0
  416. package/dist/utils/duration.js +69 -0
  417. package/dist/utils/ensemble-ops.d.ts +61 -0
  418. package/dist/utils/ensemble-ops.js +77 -0
  419. package/dist/utils/format-hosts.d.ts +21 -0
  420. package/dist/utils/format-hosts.js +73 -0
  421. package/dist/utils/hosts.d.ts +113 -0
  422. package/dist/utils/hosts.js +265 -0
  423. package/dist/utils/parent-death-watchdog.d.ts +1 -0
  424. package/dist/utils/parent-death-watchdog.js +47 -0
  425. package/dist/utils/query-timeout.d.ts +103 -0
  426. package/dist/utils/query-timeout.js +113 -0
  427. package/dist/utils/recall-format.d.ts +78 -0
  428. package/dist/utils/recall-format.js +105 -0
  429. package/dist/utils/restore-format.d.ts +49 -0
  430. package/dist/utils/restore-format.js +91 -0
  431. package/dist/utils/safe-path.d.ts +10 -0
  432. package/dist/utils/safe-path.js +43 -0
  433. package/dist/utils/sdk-probe.d.ts +9 -0
  434. package/dist/utils/sdk-probe.js +45 -0
  435. package/dist/utils/search-attributes.d.ts +76 -0
  436. package/dist/utils/search-attributes.js +86 -0
  437. package/dist/utils/validation.d.ts +113 -0
  438. package/dist/utils/validation.js +163 -0
  439. package/dist/utils/visibility-deadline.d.ts +186 -0
  440. package/dist/utils/visibility-deadline.js +158 -0
  441. package/dist/utils/worktree.d.ts +103 -0
  442. package/dist/utils/worktree.js +327 -0
  443. package/dist/worker.d.ts +14 -0
  444. package/dist/worker.js +146 -0
  445. package/dist/workflows/attachment-math.d.ts +56 -0
  446. package/dist/workflows/attachment-math.js +47 -0
  447. package/dist/workflows/index.d.ts +3 -0
  448. package/dist/workflows/index.js +11 -0
  449. package/dist/workflows/maestro-signals.d.ts +217 -0
  450. package/dist/workflows/maestro-signals.js +155 -0
  451. package/dist/workflows/maestro.d.ts +3 -0
  452. package/dist/workflows/maestro.js +812 -0
  453. package/dist/workflows/scheduler-signals.d.ts +10 -0
  454. package/dist/workflows/scheduler-signals.js +14 -0
  455. package/dist/workflows/scheduler.d.ts +17 -0
  456. package/dist/workflows/scheduler.js +143 -0
  457. package/dist/workflows/session.d.ts +2 -0
  458. package/dist/workflows/session.js +1638 -0
  459. package/dist/workflows/signals.d.ts +297 -0
  460. package/dist/workflows/signals.js +239 -0
  461. package/examples/agents/tempo-composer.md +56 -0
  462. package/examples/agents/tempo-conductor.md +117 -0
  463. package/examples/agents/tempo-critic.md +73 -0
  464. package/examples/agents/tempo-improv.md +74 -0
  465. package/examples/agents/tempo-liner.md +75 -0
  466. package/examples/agents/tempo-roadie.md +61 -0
  467. package/examples/agents/tempo-soloist.md +71 -0
  468. package/examples/agents/tempo-tuner.md +94 -0
  469. package/examples/ensembles/tempo-big-band.yaml +146 -0
  470. package/examples/ensembles/tempo-dev-team.yaml +58 -0
  471. package/examples/ensembles/tempo-headless-jam.yaml +77 -0
  472. package/examples/ensembles/tempo-jam-session.yaml +41 -0
  473. package/examples/ensembles/tempo-mock-jam.yaml +79 -0
  474. package/examples/ensembles/tempo-review-squad.yaml +32 -0
  475. package/package.json +172 -0
  476. package/packaging/launchd/com.agent.tempo.plist +46 -0
  477. package/packaging/systemd/agent-tempo.service +32 -0
  478. package/packaging/windows/install-task.ps1 +71 -0
  479. package/scenarios/conductor-recruit-mock.yaml +33 -0
  480. package/scenarios/echo-roundtrip.yaml +15 -0
  481. package/scenarios/multi-player-handoff.yaml +38 -0
  482. package/scenarios/recruit-cascade.yaml +38 -0
  483. package/scenarios/two-player-conversation.yaml +33 -0
  484. package/workflow-bundle.js +14146 -0
package/dist/daemon.js ADDED
@@ -0,0 +1,989 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.writePidFileAtomic = writePidFileAtomic;
38
+ exports.warnIfDevNamespaceDrift = warnIfDevNamespaceDrift;
39
+ exports.ensureDevNamespace = ensureDevNamespace;
40
+ exports.computeHostProfile = computeHostProfile;
41
+ exports.scrubHostProfile = scrubHostProfile;
42
+ exports.advertiseHostProfile = advertiseHostProfile;
43
+ exports.runDaemonBoot = runDaemonBoot;
44
+ exports.reconcileOnBoot = reconcileOnBoot;
45
+ exports.formatMemoryUsage = formatMemoryUsage;
46
+ exports.startMemoryReporter = startMemoryReporter;
47
+ exports.selectStaleDetachedOrphans = selectStaleDetachedOrphans;
48
+ exports.cleanupLoop = cleanupLoop;
49
+ exports.startCleanupLoop = startCleanupLoop;
50
+ /**
51
+ * Daemon entry point — runs Temporal workers in a detached background process.
52
+ *
53
+ * Started by `startDaemon()` in `src/cli/daemon.ts`.
54
+ * Config is passed via environment variables set by the parent.
55
+ *
56
+ * Writes its PID to ~/.agent-tempo/daemon.pid on startup and removes it
57
+ * on graceful shutdown (SIGTERM/SIGINT).
58
+ */
59
+ const fs = __importStar(require("fs"));
60
+ const os = __importStar(require("os"));
61
+ const path = __importStar(require("path"));
62
+ const promises_1 = require("timers/promises");
63
+ const client_1 = require("@temporalio/client");
64
+ const client_2 = require("@temporalio/client");
65
+ const config_1 = require("./config");
66
+ const dev_banner_1 = require("./cli/dev-banner");
67
+ const worker_1 = require("./worker");
68
+ const connection_1 = require("./connection");
69
+ const daemon_1 = require("./cli/daemon");
70
+ const client_3 = require("./client");
71
+ const orphans_1 = require("./reconcile/orphans");
72
+ const agent_types_1 = require("./ensemble/agent-types");
73
+ const pre_flight_1 = require("./adapters/claude-code-headless/pre-flight");
74
+ const daemon_adapter_versions_1 = require("./daemon-adapter-versions");
75
+ const log = (...args) => console.error(`[agent-tempo:daemon ${new Date().toISOString()}]`, ...args);
76
+ /**
77
+ * Daemon process start time, captured at module load. Issue #399 Q5.3b
78
+ * advertises this on every `hostProfile` signal as
79
+ * {@link HostProfile.daemonStartedAt} so the dashboard's Hosts table
80
+ * can render `now - daemonStartedAt` as the daemon-process uptime.
81
+ *
82
+ * Captured here (top-of-module) rather than inside `computeHostProfile`
83
+ * so a refresh-host-profile invocation later in the daemon's lifetime
84
+ * still advertises the original boot time. Module load happens once
85
+ * per daemon process; the value is effectively the daemon's birth time.
86
+ */
87
+ const DAEMON_STARTED_AT = Date.now();
88
+ /**
89
+ * Atomically write the daemon PID file via `writeFile(tmp) + rename(tmp, final)`.
90
+ *
91
+ * A racing reader (a CLI invocation that happens to poll during startup) will
92
+ * either see the previous file or the new one — never a half-written one.
93
+ *
94
+ * Windows sometimes fails the rename with EPERM/EBUSY/EACCES if an antivirus
95
+ * scanner or the previous handle is still active. We retry with short backoffs
96
+ * before giving up so a transient scanner doesn't crash startup.
97
+ *
98
+ * Exported for unit testing.
99
+ */
100
+ async function writePidFileAtomic(pidFilePath, pid) {
101
+ const tmp = `${pidFilePath}.tmp.${process.pid}`;
102
+ fs.writeFileSync(tmp, String(pid));
103
+ const retryCodes = new Set(['EPERM', 'EBUSY', 'EACCES']);
104
+ const backoffs = [50, 100, 200, 400]; // ms — total ≤ 750ms, bounded for startup
105
+ let lastErr;
106
+ for (let attempt = 0; attempt <= backoffs.length; attempt++) {
107
+ try {
108
+ fs.renameSync(tmp, pidFilePath);
109
+ return;
110
+ }
111
+ catch (err) {
112
+ lastErr = err;
113
+ const code = err.code;
114
+ if (!code || !retryCodes.has(code) || attempt === backoffs.length) {
115
+ try {
116
+ fs.unlinkSync(tmp);
117
+ }
118
+ catch { /* ignore */ }
119
+ throw err;
120
+ }
121
+ await (0, promises_1.setTimeout)(backoffs[attempt]);
122
+ }
123
+ }
124
+ // Unreachable — loop either returns or throws.
125
+ throw lastErr;
126
+ }
127
+ // ── Dev profile (ADR 0014 §6.2) ──
128
+ /**
129
+ * Runtime drift detector — #423 PR-A Fix 3. When dev mode is active but
130
+ * the resolved namespace is NOT the dev default, an explicit override is
131
+ * in play (CLI `--namespace`, `~/.agent-tempo-dev/config.json`, or — if
132
+ * the env-var carve-out from Fix 1 ever regressed — a leaked shell var).
133
+ * Either way, the operator deserves a load-bearing diagnostic so the
134
+ * "banner says X, daemon connects to Y" drift doesn't recur silently.
135
+ *
136
+ * Pure function over the resolved Config + an injected log sink so the
137
+ * unit test can capture the message without a live daemon process.
138
+ * Returns whether a warning fired so callers (and tests) can assert on
139
+ * the branch directly.
140
+ *
141
+ * The check is intentionally loose: any namespace mismatch in dev mode
142
+ * triggers the warning, even for intentional overrides. We can't tell
143
+ * a typo'd `config.json` entry from a deliberate one — the warning is
144
+ * cheap, and an operator who overrode the namespace on purpose can
145
+ * grep-skip a single line.
146
+ */
147
+ function warnIfDevNamespaceDrift(config, logFn = log) {
148
+ if (!(0, config_1.isDevMode)())
149
+ return false;
150
+ if (config.temporalNamespace === config_1.DEV_TEMPORAL_NAMESPACE)
151
+ return false;
152
+ logFn(`[dev-mode] WARNING: namespace drift — connecting to "${config.temporalNamespace}" ` +
153
+ `instead of dev default "${config_1.DEV_TEMPORAL_NAMESPACE}". An explicit override is in ` +
154
+ `play (CLI flag, dev config.json). Drop the override to restore dev profile isolation ` +
155
+ `(ADR 0014 §5.1).`);
156
+ return true;
157
+ }
158
+ /**
159
+ * Auto-create the dev profile's Temporal namespace on dev daemon boot
160
+ * (ADR 0014 §6.2). Idempotent — calling it on every boot is correct and
161
+ * cheap.
162
+ *
163
+ * - `ALREADY_EXISTS`: the steady state after the first boot. Happy path.
164
+ * - `PERMISSION_DENIED`: e.g. managed Temporal Cloud where `RegisterNamespace`
165
+ * isn't granted. Log + return; the subsequent worker bootstrap fails
166
+ * loudly with `Namespace not found` and the operator can run
167
+ * `temporal operator namespace create -n agent-tempo-dev` themselves.
168
+ * - any other error: same fall-through; daemon stays alive without
169
+ * mutating state.
170
+ *
171
+ * Production daemons never call this — guarded by `isDevMode()` at the
172
+ * single callsite in `main()` below. Exported for direct unit testing
173
+ * with an injected stub workflow service.
174
+ */
175
+ async function ensureDevNamespace(connection, namespace, logFn = log) {
176
+ const wfService = connection.workflowService;
177
+ try {
178
+ await wfService.registerNamespace({
179
+ namespace,
180
+ // 1-day retention is generous for dev scratch state and keeps the
181
+ // namespace tidy without aggressive cleanup pressure. The proto's
182
+ // `seconds` field is typed as `Long` (int64), but the gRPC layer
183
+ // accepts a plain number and coerces internally — same shape used
184
+ // by Temporal's own examples. Cast keeps the call site readable
185
+ // without dragging `long.js` into our direct dep graph.
186
+ workflowExecutionRetentionPeriod: { seconds: 86_400 },
187
+ description: 'agent-tempo dev profile — auto-created. Safe to drop.',
188
+ });
189
+ logFn(`[dev-mode] registered Temporal namespace "${namespace}"`);
190
+ return { ok: true, status: 'created' };
191
+ }
192
+ catch (err) {
193
+ const message = err instanceof Error ? err.message : String(err);
194
+ const code = err?.code
195
+ ?? err?.details?.code;
196
+ // ALREADY_EXISTS — happy path on every boot after the first. The
197
+ // gRPC code is 6; the Temporal SDK also surfaces it as a string in
198
+ // some transports, so check both shapes plus a substring fallback.
199
+ if (code === 'ALREADY_EXISTS' || code === 6 || /already.?exists/i.test(message)) {
200
+ logFn(`[dev-mode] Temporal namespace "${namespace}" already registered`);
201
+ return { ok: true, status: 'already-exists' };
202
+ }
203
+ // PERMISSION_DENIED — e.g. managed Temporal Cloud without RegisterNamespace.
204
+ // Log a hint so operators know what to do.
205
+ if (code === 'PERMISSION_DENIED' || code === 7 || /permission/i.test(message)) {
206
+ logFn(`[dev-mode] could not register namespace "${namespace}" — permission denied. ` +
207
+ `Run \`temporal operator namespace create -n ${namespace}\` (or grant RegisterNamespace) once.`);
208
+ return { ok: false, status: 'permission-denied', message };
209
+ }
210
+ logFn(`[dev-mode] could not register namespace "${namespace}" (continuing; worker may fail with a clearer error):`, message);
211
+ return { ok: false, status: 'error', message };
212
+ }
213
+ }
214
+ /**
215
+ * Ensure the global Maestro workflow is running.
216
+ * Uses USE_EXISTING conflict policy so it's safe to call on every daemon start.
217
+ */
218
+ async function ensureGlobalMaestro(config) {
219
+ try {
220
+ const connection = await (0, connection_1.createTemporalConnection)(config);
221
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
222
+ const input = {};
223
+ await client.workflow.start('agentGlobalMaestroWorkflow', {
224
+ workflowId: config_1.GLOBAL_MAESTRO_WORKFLOW_ID,
225
+ taskQueue: config.taskQueue,
226
+ args: [input],
227
+ workflowIdConflictPolicy: client_2.WorkflowIdConflictPolicy.USE_EXISTING,
228
+ });
229
+ log(`Global Maestro ensured (id: ${config_1.GLOBAL_MAESTRO_WORKFLOW_ID})`);
230
+ }
231
+ catch (err) {
232
+ // Non-fatal — the global maestro is optional for basic operation
233
+ log('Failed to ensure global Maestro (non-fatal):', err instanceof Error ? err.message : String(err));
234
+ }
235
+ }
236
+ // ────────────────────────────────────────────────────────────────────────
237
+ // #274 — host capability profile: compute → scrub → signal
238
+ // ────────────────────────────────────────────────────────────────────────
239
+ /**
240
+ * Daemon package version, lazily read from `package.json` so the test
241
+ * build (which compiles daemon.ts into `dist-test/src/` where
242
+ * `../package.json` doesn't resolve) can exercise daemon-boot logic
243
+ * without MODULE_NOT_FOUND. Tests that exercise `computeHostProfile`
244
+ * pass a stubbed version via `HostProfile.version` on the input.
245
+ */
246
+ function daemonVersion() {
247
+ try {
248
+ const { version } = require('../package.json');
249
+ return version;
250
+ }
251
+ catch {
252
+ return 'unknown';
253
+ }
254
+ }
255
+ /**
256
+ * Build the daemon's capability profile from its config + runtime env.
257
+ * Result is NOT scrubbed — call `scrubHostProfile` before signaling.
258
+ *
259
+ * Exported for testability; production callers go through
260
+ * `runDaemonBoot(client, deps)` which provides this as the default
261
+ * `computeHostProfile` dep.
262
+ */
263
+ function computeHostProfile(config, deps = {}) {
264
+ const resolveCopilotSync = deps.resolveCopilotSdkVersionSync ?? daemon_adapter_versions_1.resolveCopilotSdkVersionSync;
265
+ const agentTypes = (() => {
266
+ try {
267
+ return (0, agent_types_1.listAgentTypes)().map((a) => a.name);
268
+ }
269
+ catch {
270
+ // listAgentTypes reads the filesystem; treat any failure as "no
271
+ // discoverable types" rather than crashing boot.
272
+ return [];
273
+ }
274
+ })();
275
+ // Build the available-agents array. Always includes the configured
276
+ // default. #520 — additionally probe whether `claude` is installed AND
277
+ // logged in; if both pass, advertise `'claude-code-headless'` so
278
+ // cross-host recruit pre-flight can reject early on hosts without the
279
+ // CLI configured. Bounded by the probe timeouts (3s + 5s = ≤8s worst
280
+ // case) — acceptable boot cost for a one-shot probe.
281
+ const availableAgentTypes = [config.defaultAgent];
282
+ try {
283
+ const binProbe = (0, pre_flight_1.probeClaudeBinary)(config.claudeBin ?? 'claude');
284
+ if (binProbe.ok) {
285
+ const authProbe = (0, pre_flight_1.probeClaudeAuth)(config.claudeBin ?? 'claude');
286
+ if (authProbe.loggedIn && !availableAgentTypes.includes('claude-code-headless')) {
287
+ availableAgentTypes.push('claude-code-headless');
288
+ }
289
+ }
290
+ }
291
+ catch {
292
+ // Probe machinery should never throw, but guard anyway — host-profile
293
+ // computation is on the critical boot path.
294
+ }
295
+ // #532 PR-2 — copilot probe. Reuses the same sync require-source-of-
296
+ // truth as `defaultResolveCopilotSdkVersion` (which it delegates to);
297
+ // resolves to a version string when `@github/copilot-sdk` is
298
+ // installed, or `undefined` when missing. Closes the gap where
299
+ // cross-host recruit of `agent: 'copilot'` was rejected with a
300
+ // misleading "host cannot run copilot" message even on hosts where
301
+ // the SDK was installed and Copilot was logged in. Pattern mirrors
302
+ // the claude-code-headless block above.
303
+ try {
304
+ const copilotVersion = resolveCopilotSync();
305
+ if (copilotVersion && !availableAgentTypes.includes('copilot')) {
306
+ availableAgentTypes.push('copilot');
307
+ }
308
+ }
309
+ catch {
310
+ // Defensive — `resolveCopilotSdkVersionSync` already swallows
311
+ // require failures, but the boot path must not crash on any
312
+ // surprise here.
313
+ }
314
+ return {
315
+ hostname: os.hostname(),
316
+ version: daemonVersion(),
317
+ defaultAgent: config.defaultAgent,
318
+ // #520 + #532 PR-2 — was: `[config.defaultAgent]`. Now grows when
319
+ // the optional probes pass: `claude-code-headless` (when `claude`
320
+ // is on PATH AND logged in), `copilot` (when `@github/copilot-sdk`
321
+ // is installed). Future PRs can extend the same pattern for
322
+ // `claude-api` (probe `@anthropic-ai/sdk` install +
323
+ // ANTHROPIC_API_KEY env) and `opencode` (probe `@opencode-ai/sdk`
324
+ // install + `opencode` binary on PATH). Recording as an array
325
+ // keeps the wire shape forward-compatible.
326
+ availableAgentTypes,
327
+ availablePlayerTypes: agentTypes,
328
+ claudeBin: config.claudeBin,
329
+ platform: process.platform,
330
+ capabilities: [],
331
+ daemonStartedAt: DAEMON_STARTED_AT,
332
+ // adapterVersions is populated at runDaemonBoot time after the
333
+ // parallel probe (see runDaemonBoot below). computeHostProfile
334
+ // intentionally returns the immediate fields only.
335
+ };
336
+ }
337
+ /**
338
+ * #274 AC5c / M10 — HARD REQUIREMENT privacy scrub.
339
+ *
340
+ * Strips absolute paths and file extensions from every `HostProfile` field
341
+ * before the payload crosses the signal boundary. The global maestro is
342
+ * namespace-wide; a multi-tenant or multi-ensemble corporate setup would
343
+ * leak username-containing paths across ensembles if this is ever
344
+ * violated. Unit-tested in `test/daemon-boot.test.ts` with a dedicated
345
+ * "no `/` or `\\` in any string" invariant assertion against pathological
346
+ * inputs.
347
+ *
348
+ * Contract per architect AC5c:
349
+ * - `claudeBin` — basename only (e.g. `claude`), never absolute
350
+ * - `availableAgentTypes` — names only, never paths
351
+ * - `availablePlayerTypes` — names only, never paths
352
+ * - No env var values, no `workDir`, no user directories in any field
353
+ *
354
+ * The scrub is defense-in-depth: production callers (`computeHostProfile`)
355
+ * already produce clean inputs from `listAgentTypes().map(a => a.name)`.
356
+ * If a future code path accidentally passes a path, this function catches
357
+ * it before the workflow handler ever sees it.
358
+ */
359
+ function scrubHostProfile(raw) {
360
+ const stripPath = (s) => {
361
+ // Platform-independent basename: `path.basename` is runtime-bound —
362
+ // on POSIX it doesn't recognise `\` as a separator, so a Windows
363
+ // daemon's signal leaking `'C:\Users\alice\bin\claude.exe'` into
364
+ // a Linux-hosted global maestro would bypass the scrub entirely
365
+ // (CI caught exactly this on Ubuntu shard-2). Normalize first,
366
+ // then use `path.posix.basename` explicitly so the scrub is
367
+ // deterministic regardless of where the daemon or maestro runs.
368
+ //
369
+ // Also strip a single trailing `.md` — player-type files are
370
+ // shipped as e.g. `tempo-soloist.md` but the name should be just
371
+ // `tempo-soloist` on the wire.
372
+ const normalized = s.replace(/\\/g, '/');
373
+ const base = path.posix.basename(normalized);
374
+ return base.endsWith('.md') ? base.slice(0, -3) : base;
375
+ };
376
+ const scrubList = (list) => list?.map(stripPath);
377
+ // Issue #399 — pass-through fields with no privacy concern.
378
+ // `daemonStartedAt` is a number; `adapterVersions` keys are adapter
379
+ // NAMES and values are version strings. Neither carries paths,
380
+ // env vars, or user-home directories, so the AC5c scrub doesn't
381
+ // need to touch them. We conditionally splice them in only when
382
+ // they're defined on the input, so the scrub output stays
383
+ // shape-equivalent to a clean input that omits them — the
384
+ // already-clean round-trip test (`scrubHostProfile(clean) === clean`)
385
+ // continues to hold.
386
+ const out = {
387
+ hostname: raw.hostname,
388
+ version: raw.version,
389
+ defaultAgent: raw.defaultAgent,
390
+ availableAgentTypes: scrubList(raw.availableAgentTypes),
391
+ availablePlayerTypes: scrubList(raw.availablePlayerTypes),
392
+ claudeBin: raw.claudeBin ? stripPath(raw.claudeBin) : undefined,
393
+ platform: raw.platform,
394
+ capabilities: raw.capabilities,
395
+ };
396
+ if (raw.daemonStartedAt !== undefined)
397
+ out.daemonStartedAt = raw.daemonStartedAt;
398
+ if (raw.adapterVersions !== undefined)
399
+ out.adapterVersions = raw.adapterVersions;
400
+ return out;
401
+ }
402
+ /** Production default: signal the global maestro with the profile. */
403
+ async function realSendHostProfileSignal(client, profile) {
404
+ const handle = client.workflow.getHandle(config_1.GLOBAL_MAESTRO_WORKFLOW_ID);
405
+ await handle.signal('hostProfile', profile);
406
+ }
407
+ /**
408
+ * Signal `hostProfile` with bounded retry (AC5b / M11).
409
+ *
410
+ * Default backoff: `[0, 5000, 15000]` ms → 3 attempts, ≤20 s wall-clock,
411
+ * well under the 30 s budget. Tests override to `[0, 0, 0]` for fast
412
+ * execution. On total failure, logs a warning and returns — the daemon
413
+ * stays alive without its profile advertised.
414
+ *
415
+ * Exported for reuse by the Phase 5 `agent-tempo refresh-host-profile`
416
+ * CLI subcommand, which re-signals without needing the full
417
+ * `runDaemonBoot` sequence (the global maestro is already up).
418
+ */
419
+ async function advertiseHostProfile(client, profile, opts = {}) {
420
+ const backoffs = opts.retryBackoffsMs ?? [0, 5000, 15000];
421
+ const logFn = opts.log ?? log;
422
+ const send = opts.sendSignal ?? realSendHostProfileSignal;
423
+ let lastError;
424
+ for (let attempt = 0; attempt < backoffs.length; attempt++) {
425
+ const delay = backoffs[attempt];
426
+ if (delay > 0)
427
+ await (0, promises_1.setTimeout)(delay);
428
+ try {
429
+ await send(client, profile);
430
+ logFn(`Advertised host profile for "${profile.hostname}" (attempt ${attempt + 1}/${backoffs.length})`);
431
+ return { ok: true, attempts: attempt + 1 };
432
+ }
433
+ catch (err) {
434
+ lastError = err;
435
+ logFn(`hostProfile signal attempt ${attempt + 1}/${backoffs.length} failed:`, err instanceof Error ? err.message : err);
436
+ }
437
+ }
438
+ logFn(`Failed to advertise host profile after ${backoffs.length} attempts (non-fatal; daemon stays alive):`, lastError instanceof Error ? lastError.message : lastError);
439
+ return { ok: false, attempts: backoffs.length, lastError };
440
+ }
441
+ /**
442
+ * #274 M14 — daemon boot sequence: ensure global maestro is running,
443
+ * then advertise the (scrubbed) capability profile with bounded retry.
444
+ *
445
+ * Ordering is load-bearing (AC5a / M11): the `hostProfile` signal MUST
446
+ * NOT fire until `ensureGlobalMaestro` has resolved. Otherwise the
447
+ * signal races the workflow-start and gets silently dropped by Temporal
448
+ * (WorkflowNotFound on an unknown workflow id).
449
+ *
450
+ * Hard-failure behavior (AC5b): if `ensureGlobalMaestro` rejects, the
451
+ * daemon stays alive WITHOUT advertising its profile. Next opportunity
452
+ * is the next daemon restart OR a manual `agent-tempo refresh-host-profile`
453
+ * invocation (Phase 5).
454
+ *
455
+ * Tests in `test/daemon-boot.test.ts` exercise:
456
+ * - ensure-before-signal ordering via deferred promises
457
+ * - retry success on 3rd attempt
458
+ * - all-retries-exhausted stays alive
459
+ * - ensure-fails-stays-alive
460
+ */
461
+ async function runDaemonBoot(client, deps) {
462
+ const logFn = deps.log ?? log;
463
+ const raw = deps.computeHostProfile();
464
+ // Issue #399 Q5.4 — probe adapter versions in parallel with the
465
+ // global-maestro ensure. The probe is best-effort and never throws;
466
+ // settled-result handling makes the boot path tolerant of either
467
+ // succeeding without the other. Ordering invariant (AC5a / M11) —
468
+ // the host-profile signal still gates on `ensureGlobalMaestro`
469
+ // resolving — is preserved by awaiting both before signaling.
470
+ const probeFn = deps.probeAdapterVersions ?? (() => Promise.resolve({}));
471
+ const [ensureResult, probeResult] = await Promise.allSettled([
472
+ deps.ensureGlobalMaestro(),
473
+ probeFn(),
474
+ ]);
475
+ if (ensureResult.status === 'rejected') {
476
+ logFn('ensureGlobalMaestro failed (non-fatal); host profile not advertised this boot:', ensureResult.reason instanceof Error ? ensureResult.reason.message : ensureResult.reason);
477
+ return;
478
+ }
479
+ const adapterVersions = probeResult.status === 'fulfilled' ? probeResult.value : {};
480
+ if (probeResult.status === 'rejected') {
481
+ // probeAdapterVersions is contracted to never throw, but guard the
482
+ // fallthrough for defense-in-depth — a thrown probe shouldn't
483
+ // block profile advertisement.
484
+ logFn('probeAdapterVersions threw (non-fatal); advertising profile without adapter versions:', probeResult.reason instanceof Error ? probeResult.reason.message : probeResult.reason);
485
+ }
486
+ // Merge probe result into the profile. We mutate `raw` rather than
487
+ // re-call computeHostProfile because the probe and compute are
488
+ // logically two halves of the same boot snapshot.
489
+ const profile = scrubHostProfile({
490
+ ...raw,
491
+ adapterVersions: Object.keys(adapterVersions).length > 0 ? adapterVersions : undefined,
492
+ });
493
+ await advertiseHostProfile(client, profile, {
494
+ retryBackoffsMs: deps.retryBackoffsMs,
495
+ log: logFn,
496
+ sendSignal: deps.sendHostProfileSignal,
497
+ });
498
+ }
499
+ // ── Reconcile-on-boot (PR-E §10.1) ──
500
+ /**
501
+ * PR-E reconcile-on-boot — design §10.1.
502
+ *
503
+ * Called once during daemon startup, after workers are running but before
504
+ * the main run loop blocks. Queries for orphaned sessions owned by this
505
+ * host and applies the effective {@link DaemonConfig.restorePolicy}:
506
+ *
507
+ * - `auto`: call `restart` on each orphan inside the allowlist + age
508
+ * window. `AttachmentConflict` is caught silently — another process
509
+ * may have restored concurrently.
510
+ * - `prompt`: log the orphan list and leave the restore to the CLI
511
+ * `agent-tempo restore` command. No automatic action.
512
+ * - `never`: silent no-op.
513
+ *
514
+ * All three branches exit in bounded time — never blocks worker startup.
515
+ * Non-fatal: any failure is logged and reconcile bails without crashing
516
+ * the daemon (worker loop takes over and the user can re-run the query
517
+ * via the CLI).
518
+ */
519
+ async function reconcileOnBoot(client, daemonConfig, hostname = os.hostname(),
520
+ // Injectable clock — default to wall-clock at call time. Exposed so tests can pass a
521
+ // pinned reference time alongside fixtures that use ISO strings derived from that time
522
+ // (otherwise the 24h age filter below vs. a hardcoded test NOW drifts out of sync as
523
+ // calendar days roll over; matches the pattern used by the cleanup path below).
524
+ now = Date.now()) {
525
+ if (daemonConfig.restorePolicy === 'never') {
526
+ log(`reconcile: restorePolicy="never" — skipping orphan scan`);
527
+ return;
528
+ }
529
+ log(`reconcile: scanning for orphans on host="${hostname}" (policy=${daemonConfig.restorePolicy})`);
530
+ // #93 / #285: the decision loop (cross-host filter, age window, allowlist,
531
+ // restart via outbox) was extracted to `restoreOrphansOnce` so the CLI
532
+ // resume flow (`up` option 2, `conduct --resume`) shares the same
533
+ // behavior. Pass `invokerPlayerId: 'daemon'` to preserve the previous
534
+ // operator identity, and inject `now` as a closure over the pinned ref
535
+ // time so the existing rebuild-reboot tests keep their fixture semantics.
536
+ const summary = await (0, orphans_1.restoreOrphansOnce)(client, {
537
+ hostname,
538
+ invokerPlayerId: 'daemon',
539
+ policy: daemonConfig.restorePolicy,
540
+ autoRestoreMaxAgeHours: daemonConfig.autoRestoreMaxAgeHours,
541
+ autoRestoreEnsembles: daemonConfig.autoRestoreEnsembles,
542
+ now: () => now,
543
+ }, log);
544
+ const total = summary.reattached + summary.skipped + summary.failed;
545
+ if (total === 0) {
546
+ log('reconcile: no orphans found');
547
+ return;
548
+ }
549
+ log(`reconcile: ${summary.reattached} reattached, ` +
550
+ `${summary.skipped} skipped, ${summary.failed} failed ` +
551
+ `(scanned ${total})`);
552
+ if (daemonConfig.restorePolicy === 'prompt' && summary.skipped > 0) {
553
+ log('reconcile: [prompt] run `agent-tempo restore` to restore interactively');
554
+ }
555
+ }
556
+ // ── Memory reporter (#336) ──
557
+ /** Default cadence for the periodic memory log. */
558
+ const MEMORY_REPORT_INTERVAL_MS = 5 * 60 * 1000;
559
+ /**
560
+ * Pure formatter — turns a `process.memoryUsage()` snapshot into a single
561
+ * space-separated `key=NNNmb` string suitable for grepping out of the log.
562
+ *
563
+ * Exported for unit testing without a live process.
564
+ */
565
+ function formatMemoryUsage(usage) {
566
+ const mb = (n) => Math.round(n / (1024 * 1024));
567
+ return (`rss=${mb(usage.rss)}mb ` +
568
+ `heapUsed=${mb(usage.heapUsed)}mb ` +
569
+ `heapTotal=${mb(usage.heapTotal)}mb ` +
570
+ `external=${mb(usage.external)}mb ` +
571
+ `arrayBuffers=${mb(usage.arrayBuffers)}mb`);
572
+ }
573
+ /**
574
+ * #336 — schedule a periodic `[agent-tempo:daemon ...] memory: ...` log
575
+ * line so the next memory-leak investigation has a baseline + growth curve
576
+ * directly in the daemon log instead of needing a debugger attach.
577
+ *
578
+ * Returns a stop function the daemon's shutdown handler invokes.
579
+ *
580
+ * `unref()` on the timer handle so memory reporting alone never keeps the
581
+ * daemon alive — workers + the HTTP listener are the only legitimate
582
+ * long-lived references.
583
+ */
584
+ function startMemoryReporter(intervalMs = MEMORY_REPORT_INTERVAL_MS, logFn = log, sample = () => process.memoryUsage()) {
585
+ const tick = () => {
586
+ try {
587
+ logFn(`memory: ${formatMemoryUsage(sample())}`);
588
+ }
589
+ catch (err) {
590
+ // `process.memoryUsage()` can't realistically throw, but if a custom
591
+ // sampler does we don't want to take the daemon down.
592
+ logFn('memory: sample failed (non-fatal):', err instanceof Error ? err.message : err);
593
+ }
594
+ };
595
+ // Emit immediately so the first log line is the baseline (otherwise an
596
+ // operator polling early sees nothing for `intervalMs`).
597
+ tick();
598
+ const timer = setInterval(tick, intervalMs);
599
+ timer.unref();
600
+ return () => clearInterval(timer);
601
+ }
602
+ // ── Cleanup loop (PR-E §13.4) ──
603
+ /** Hardcoded cleanup loop period per PR-E §8 answer 2. */
604
+ const CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
605
+ /**
606
+ * Filter a set of orphan candidates to those that exceed the
607
+ * `detachedMaxAgeDays` retention threshold. Exported for unit testing the
608
+ * retention math without a live Temporal connection.
609
+ */
610
+ function selectStaleDetachedOrphans(orphans, detachedMaxAgeDays, now = Date.now()) {
611
+ const thresholdMs = detachedMaxAgeDays * 24 * 60 * 60 * 1000;
612
+ return orphans.filter((o) => {
613
+ if (o.info.phase !== 'detached')
614
+ return false;
615
+ if (!o.summary.detachedSince)
616
+ return false;
617
+ const detachedAt = Date.parse(o.summary.detachedSince);
618
+ if (!Number.isFinite(detachedAt))
619
+ return false;
620
+ return now - detachedAt > thresholdMs;
621
+ });
622
+ }
623
+ /**
624
+ * PR-E cleanup loop — design §13.4 regression row 1.
625
+ *
626
+ * Runs on a 6-hour timer (hardcoded per §8 answer 2). Destroys detached
627
+ * orphans older than `detachedMaxAgeDays` via `TempoClient.destroy` so the
628
+ * workflow completes and eventually falls out of the namespace.
629
+ *
630
+ * Never touches `Running` workflows that still hold a live attachment
631
+ * (filter is explicit on `phase === 'detached'`).
632
+ *
633
+ * **Note (#144)**: an earlier revision included a "pass 2" that tried to
634
+ * `terminate()` already-Completed workflows as belt-and-suspenders retention.
635
+ * `terminate()` throws on Completed workflows in every Temporal namespace
636
+ * setting, so the pass was dead code masked by a swallowing catch. It was
637
+ * removed: namespace retention (Temporal Cloud default 30d, self-hosted
638
+ * configurable) is the authoritative reaper for Completed workflows.
639
+ */
640
+ async function cleanupLoop(client, daemonConfig, hostname = os.hostname()) {
641
+ const tempo = (0, client_3.createTempoClient)(client);
642
+ const now = Date.now();
643
+ try {
644
+ const orphans = await (0, orphans_1.queryOrphanedSessions)(client, { hostname }, log);
645
+ const stale = selectStaleDetachedOrphans(orphans, daemonConfig.cleanupPolicy.detachedMaxAgeDays, now);
646
+ for (const o of stale) {
647
+ const { ensemble, playerId } = o.summary;
648
+ try {
649
+ await tempo.destroy(ensemble, playerId, `detached >${daemonConfig.cleanupPolicy.detachedMaxAgeDays}d`);
650
+ log(`cleanup: [detached] destroyed ${o.workflowId} (detachedSince=${o.summary.detachedSince})`);
651
+ }
652
+ catch (err) {
653
+ log(`cleanup: [detached] destroy failed for ${o.workflowId}: ${err instanceof Error ? err.message : String(err)}`);
654
+ }
655
+ }
656
+ }
657
+ catch (err) {
658
+ log('cleanup: failed (non-fatal):', err instanceof Error ? err.message : String(err));
659
+ }
660
+ }
661
+ /**
662
+ * Schedule {@link cleanupLoop} to run every 6 hours. Returns a clearer
663
+ * function that cancels the timer — called during shutdown.
664
+ */
665
+ function startCleanupLoop(client, daemonConfig, hostname = os.hostname()) {
666
+ let timer = null;
667
+ const tick = () => {
668
+ cleanupLoop(client, daemonConfig, hostname).catch((err) => {
669
+ log('cleanup: tick failed:', err instanceof Error ? err.message : String(err));
670
+ });
671
+ timer = setTimeout(tick, CLEANUP_INTERVAL_MS);
672
+ timer.unref();
673
+ };
674
+ // Run first tick after the initial interval (not immediately — startup is
675
+ // busy enough). The retention math is idempotent so a delayed first run is
676
+ // always safe.
677
+ timer = setTimeout(tick, CLEANUP_INTERVAL_MS);
678
+ timer.unref();
679
+ return () => {
680
+ if (timer) {
681
+ clearTimeout(timer);
682
+ timer = null;
683
+ }
684
+ };
685
+ }
686
+ async function main() {
687
+ // ADR 0014 §5.4 / gate 4 — dev daemon log self-identifies. Banner fires
688
+ // first so it lands at the top of `~/.agent-tempo-dev/daemon.log` for
689
+ // grep-friendly identification regardless of subsequent log volume.
690
+ (0, dev_banner_1.emitDevBannerIfActive)();
691
+ // Ensure daemon directory exists. AGENT_TEMPO_HOME already resolves to
692
+ // `~/.agent-tempo-dev/` in dev mode (ADR 0014 §5.3), so this lands in
693
+ // the right place without a per-callsite branch.
694
+ fs.mkdirSync(config_1.AGENT_TEMPO_HOME, { recursive: true });
695
+ // Write PID file — the parent polls for this to confirm startup.
696
+ // Atomic write: tmp + rename so a racing reader never sees a half-written
697
+ // file. Retries on Windows EPERM/EBUSY/EACCES (see #182).
698
+ await writePidFileAtomic(daemon_1.DAEMON_PID_PATH, process.pid);
699
+ log(`Daemon started (pid ${process.pid})`);
700
+ log(`PID file: ${daemon_1.DAEMON_PID_PATH}`);
701
+ log(`Log file: ${daemon_1.DAEMON_LOG_PATH}`);
702
+ // Create the heartbeat file synchronously so the first `daemon status`
703
+ // invocation after startup never races the first interval tick. The
704
+ // subsequent interval only has to refresh the mtime — no branching on
705
+ // file-existence each tick (#157 PR B).
706
+ try {
707
+ fs.writeFileSync(daemon_1.DAEMON_HEARTBEAT_PATH, '');
708
+ }
709
+ catch (err) {
710
+ // Non-fatal — the daemon still runs, `daemon status` just reports
711
+ // `heartbeatAge: null`. Log loudly so operators notice.
712
+ log('Failed to create heartbeat file (non-fatal):', err?.message ?? err);
713
+ }
714
+ const heartbeatInterval = setInterval(() => {
715
+ try {
716
+ const now = Date.now() / 1000; // `fs.utimes` takes seconds since epoch
717
+ fs.utimesSync(daemon_1.DAEMON_HEARTBEAT_PATH, now, now);
718
+ }
719
+ catch {
720
+ // Swallow — transient fs errors shouldn't take down the daemon.
721
+ }
722
+ }, daemon_1.HEARTBEAT_INTERVAL_MS);
723
+ heartbeatInterval.unref();
724
+ // Get config from env vars (passed by startDaemon via spawn env)
725
+ const config = (0, config_1.getConfig)({});
726
+ // #423 PR-A Fix 3 — load-bearing drift detector. The `[DEV MODE]` banner
727
+ // (gate 4) and the daemon's actual Temporal connection MUST agree on the
728
+ // namespace. Fix 1's env-var carve-out plus Fix 2's source-annotated
729
+ // banner make a silent disagreement impossible at the resolution layer,
730
+ // but a future regression — or an operator who hand-edits
731
+ // `~/.agent-tempo-dev/config.json` to a non-dev namespace by mistake —
732
+ // would still slip through. The warning fires once at boot and lands at
733
+ // the top of `daemon.log` so an operator chasing weird coordination bugs
734
+ // sees the override on first inspection.
735
+ warnIfDevNamespaceDrift(config);
736
+ // ADR 0014 §6.2 — dev daemon auto-creates its Temporal namespace before
737
+ // the worker bootstrap. Production daemons skip this; namespaces are
738
+ // operator-managed there.
739
+ //
740
+ // Idempotent on `ALREADY_EXISTS` (every boot after the first), non-fatal
741
+ // on `PERMISSION_DENIED`. If creation fails for an unexpected reason the
742
+ // worker bootstrap below fails loudly with `Namespace not found`, which
743
+ // is the clearer error from the operator's perspective.
744
+ if ((0, config_1.isDevMode)() && config.temporalNamespace === config_1.DEV_TEMPORAL_NAMESPACE) {
745
+ try {
746
+ const provisionConn = await (0, connection_1.createTemporalConnection)(config);
747
+ try {
748
+ await ensureDevNamespace(provisionConn, config.temporalNamespace);
749
+ }
750
+ finally {
751
+ await provisionConn.close();
752
+ }
753
+ }
754
+ catch (err) {
755
+ // Connection itself failed — log + fall through. createWorkers() will
756
+ // surface the same error with its own context.
757
+ log('[dev-mode] namespace pre-create skipped — Temporal connection failed:', err instanceof Error ? err.message : String(err));
758
+ }
759
+ }
760
+ // PR-3 of the v1.0 rebrand — fail fast if the `AgentTempo*` search
761
+ // attributes aren't registered on the target namespace. The actionable
762
+ // error message includes the exact `temporal operator search-attribute
763
+ // create` commands operators need to paste. Probe failure (Temporal CLI
764
+ // missing, namespace unreachable) is downgraded to a warning — the
765
+ // createWorkers() call below will surface the connection error with
766
+ // better context. The hard-stop is only "namespace reached, but SAs
767
+ // missing".
768
+ {
769
+ const { verifySearchAttributes } = await Promise.resolve().then(() => __importStar(require('./cli/sa-preflight')));
770
+ const result = await verifySearchAttributes({
771
+ temporalAddress: config.temporalAddress,
772
+ temporalNamespace: config.temporalNamespace,
773
+ });
774
+ if (!result.ok && !result.probeError) {
775
+ process.stderr.write('ERROR: ' + result.message + '\n');
776
+ log('Daemon refused to boot — search attributes missing on namespace ' + config.temporalNamespace);
777
+ try {
778
+ fs.unlinkSync(daemon_1.DAEMON_PID_PATH);
779
+ }
780
+ catch { /* ignore */ }
781
+ try {
782
+ fs.unlinkSync(daemon_1.DAEMON_HEARTBEAT_PATH);
783
+ }
784
+ catch { /* ignore */ }
785
+ process.exit(1);
786
+ }
787
+ else if (result.probeError) {
788
+ log('search-attribute preflight probe failed (non-fatal — createWorkers will surface the real error):', result.probeError);
789
+ }
790
+ }
791
+ // Use mutable refs so signal handlers can be registered before workers
792
+ // are created — closes the narrow window where a SIGTERM during
793
+ // createWorkers() would be missed.
794
+ let sharedWorker = null;
795
+ let hostWorker = null;
796
+ // Register signal handlers first — idempotent, drain-only (no process.exit).
797
+ let shuttingDown = false;
798
+ const hardExit = () => {
799
+ log('Shutdown timeout — forcing exit');
800
+ try {
801
+ fs.unlinkSync(daemon_1.DAEMON_PID_PATH);
802
+ }
803
+ catch { /* ignore */ }
804
+ try {
805
+ fs.unlinkSync(daemon_1.DAEMON_HEARTBEAT_PATH);
806
+ }
807
+ catch { /* ignore */ }
808
+ process.exit(1);
809
+ };
810
+ // Mutable ref so the reconcile/cleanup init below can register its
811
+ // cancellation with shutdown (declared after signal handlers to preserve
812
+ // the existing signal-handler-first safety ordering).
813
+ let stopCleanupLoopRef = null;
814
+ // #336 — memory reporter. Started unconditionally below; shutdown
815
+ // clears the interval so the daemon can drain cleanly.
816
+ let stopMemoryReporterRef = null;
817
+ // #94/#95 PR-1 — HTTP server handle. Started after workers are up
818
+ // (so handlers calling into TempoClient hit a live worker), drained
819
+ // here on shutdown. Mutable ref because `startHttpServer` is awaited
820
+ // below the `shutdown` declaration.
821
+ let httpServerHandle = null;
822
+ // #94/#95 PR-2 — aggregate poll loop + per-ensemble buses. Owned by
823
+ // the daemon process; `close()` drains every per-ensemble bus.
824
+ let aggregateRunner = null;
825
+ const shutdown = () => {
826
+ if (shuttingDown)
827
+ return;
828
+ shuttingDown = true;
829
+ log('Shutting down (draining in-flight activities)...');
830
+ // Safety net: force exit if workers don't stop within 15s
831
+ const timer = setTimeout(hardExit, 15_000);
832
+ timer.unref();
833
+ stopCleanupLoopRef?.();
834
+ stopMemoryReporterRef?.();
835
+ clearInterval(heartbeatInterval);
836
+ try {
837
+ fs.unlinkSync(daemon_1.DAEMON_HEARTBEAT_PATH);
838
+ }
839
+ catch { /* ignore */ }
840
+ // HTTP server closes ahead of workers. The HTTP `close()` itself
841
+ // returns a Promise that resolves only after live SSE sockets
842
+ // drain (5 s) — by which point the listener is already refusing
843
+ // new connections AND the port file has been unlinked. The fire-
844
+ // and-forget `.catch()` here means we don't await drain, so the
845
+ // worker shutdown below races the HTTP drain. That's intentional:
846
+ // the worker drain budget is 15 s (`hardExit`), which exceeds
847
+ // HTTP's 5 s drain window — so a polling CLI sees ECONNREFUSED
848
+ // either at the listener level (if it polled after `close()`
849
+ // returned) OR is force-disconnected (if it was inside the drain
850
+ // window and the worker drain pulled the rug). Both signals mean
851
+ // "daemon is going away," which is the contract.
852
+ //
853
+ // The aggregate runner is closed first so per-ensemble buses stop
854
+ // pushing events while the SSE handler is still draining its
855
+ // sockets — preventing wasted work in the drain window.
856
+ aggregateRunner?.close();
857
+ httpServerHandle?.close().catch((err) => log('http close error (non-fatal):', err instanceof Error ? err.message : err));
858
+ sharedWorker?.shutdown();
859
+ hostWorker?.shutdown();
860
+ };
861
+ process.on('SIGTERM', shutdown);
862
+ process.on('SIGINT', shutdown);
863
+ // Create workers (signal handlers already active via mutable refs)
864
+ log(`Connecting to Temporal at ${config.temporalAddress} (namespace: ${config.temporalNamespace})`);
865
+ const workers = await (0, worker_1.createWorkers)(config);
866
+ sharedWorker = workers.sharedWorker;
867
+ hostWorker = workers.hostWorker;
868
+ log('Workers created — processing tasks');
869
+ // #336 — start the periodic memory reporter alongside the workers. The
870
+ // first sample lands in the log immediately as a baseline; subsequent
871
+ // samples fire every MEMORY_REPORT_INTERVAL_MS and let operators spot
872
+ // unbounded growth without attaching a debugger.
873
+ stopMemoryReporterRef = startMemoryReporter();
874
+ // #274 — daemon boot sequence: ensure the global maestro is running,
875
+ // then advertise this host's capability profile with bounded retry.
876
+ // Fire-and-forget from main's perspective (the workers above are
877
+ // already polling tasks; we don't block the run loop on maestro
878
+ // ensure + profile signaling). Ordering INSIDE runDaemonBoot is
879
+ // load-bearing — see M11 / AC5a — so the outer `.catch` only
880
+ // handles unexpected throws (both ensure and signal paths log +
881
+ // return gracefully on their own).
882
+ (async () => {
883
+ try {
884
+ const bootConnection = await (0, connection_1.createTemporalConnection)(config);
885
+ const bootClient = new client_1.Client({ connection: bootConnection, namespace: config.temporalNamespace });
886
+ await runDaemonBoot(bootClient, {
887
+ ensureGlobalMaestro: () => ensureGlobalMaestro(config),
888
+ sendHostProfileSignal: realSendHostProfileSignal,
889
+ computeHostProfile: () => computeHostProfile(config),
890
+ // Issue #399 Q5.4 — probe upstream tool versions in parallel
891
+ // with the global-maestro ensure. Production uses real spawns
892
+ // / package.json reads; tests inject canned maps via
893
+ // `DaemonBootDeps.probeAdapterVersions`.
894
+ probeAdapterVersions: () => (0, daemon_adapter_versions_1.probeAdapterVersions)(),
895
+ });
896
+ }
897
+ catch (err) {
898
+ log('runDaemonBoot background error:', err);
899
+ }
900
+ })();
901
+ // PR-E reconcile-on-boot + cleanup loop (design §10, §13.4). Both run
902
+ // against their own Temporal Client, not the worker connection — they
903
+ // call `workflow.list` + `workflow.getHandle().query(...)` which are
904
+ // client-side operations. Non-fatal: any failure is logged and the
905
+ // daemon continues running.
906
+ let reconcileClient = null;
907
+ try {
908
+ const daemonConfig = (0, config_1.loadDaemonConfig)();
909
+ const reconcileConnection = await (0, connection_1.createTemporalConnection)(config);
910
+ reconcileClient = new client_1.Client({ connection: reconcileConnection, namespace: config.temporalNamespace });
911
+ // Fire-and-forget reconcile; the daemon must not block on this.
912
+ reconcileOnBoot(reconcileClient, daemonConfig).catch((err) => {
913
+ log('reconcileOnBoot background error:', err);
914
+ });
915
+ // Schedule the 6-hour cleanup loop (hardcoded per §8 answer 2).
916
+ stopCleanupLoopRef = startCleanupLoop(reconcileClient, daemonConfig);
917
+ log(`cleanup loop scheduled (every ${CLEANUP_INTERVAL_MS / 3_600_000}h)`);
918
+ }
919
+ catch (err) {
920
+ log('reconcile/cleanup init failed (non-fatal):', err instanceof Error ? err.message : String(err));
921
+ }
922
+ // #94/#95 PR-1 — HTTP snapshot endpoints. Reuses the reconcile client
923
+ // (already long-lived; the snapshot handlers fan out the same
924
+ // visibility queries that reconcile/cleanup do). Non-fatal: any
925
+ // listener error logs and the daemon stays alive — Temporal workers
926
+ // are the durable concern.
927
+ if (reconcileClient) {
928
+ try {
929
+ const { startHttpServer } = await Promise.resolve().then(() => __importStar(require('./http')));
930
+ const { createTempoClient } = await Promise.resolve().then(() => __importStar(require('./client')));
931
+ const { AggregateRunner } = await Promise.resolve().then(() => __importStar(require('./http/aggregate')));
932
+ // #437 — pass the daemon's polling task queue through. `listHosts`
933
+ // (called by `/v1/hosts`, snapshot.hostProfiles, dashboard, TUI,
934
+ // AggregateRunner) defaults to `'agent-tempo'` and silently
935
+ // returns `[]` in dev mode without this. Both `namespace` (already
936
+ // baked into `reconcileClient.options.namespace`) and `taskQueue`
937
+ // must match the daemon for poller discovery to find this host.
938
+ const httpClient = createTempoClient(reconcileClient, { taskQueue: config.taskQueue });
939
+ // Single shared bootEpoch — every bus the daemon constructs uses
940
+ // this same value, frozen for the process lifetime per §5.
941
+ const bootEpoch = Date.now();
942
+ aggregateRunner = new AggregateRunner({ client: httpClient, bootEpoch });
943
+ aggregateRunner.start();
944
+ httpServerHandle = await startHttpServer({
945
+ client: httpClient,
946
+ namespace: config.temporalNamespace,
947
+ taskQueue: config.taskQueue,
948
+ version: daemonVersion(),
949
+ aggregate: aggregateRunner,
950
+ });
951
+ log(`HTTP listening on http://${httpServerHandle.bindAddr}:${httpServerHandle.port}`);
952
+ log(`Aggregate poll loop running (bootEpoch=${bootEpoch})`);
953
+ }
954
+ catch (err) {
955
+ log('http server init failed (non-fatal):', err instanceof Error ? err.message : String(err));
956
+ }
957
+ }
958
+ else {
959
+ log('http server skipped: no Temporal client available');
960
+ }
961
+ // Run both workers — blocks until shutdown + drain completes
962
+ try {
963
+ await Promise.all([sharedWorker.run(), hostWorker.run()]);
964
+ }
965
+ catch (err) {
966
+ log('Worker error:', err);
967
+ }
968
+ // Workers have stopped — clean up PID file and exit
969
+ try {
970
+ fs.unlinkSync(daemon_1.DAEMON_PID_PATH);
971
+ }
972
+ catch { /* ignore */ }
973
+ log('Daemon stopped');
974
+ process.exit(0);
975
+ }
976
+ // Only run `main()` when this file is invoked directly (e.g. via
977
+ // `node dist/daemon.js` or `npx ts-node src/daemon.ts`). Tests that
978
+ // import `reconcileOnBoot` / `cleanupLoop` / `selectStaleDetachedOrphans`
979
+ // must not trigger the worker-bootstrap path as a module side-effect.
980
+ if (require.main === module) {
981
+ main().catch((err) => {
982
+ log('Fatal error:', err);
983
+ try {
984
+ fs.unlinkSync(daemon_1.DAEMON_PID_PATH);
985
+ }
986
+ catch { /* ignore */ }
987
+ process.exit(1);
988
+ });
989
+ }