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
@@ -0,0 +1,2438 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.status = status;
37
+ exports.init = init;
38
+ exports.server = server;
39
+ exports.up = up;
40
+ exports.formatScheduleRecurrence = formatScheduleRecurrence;
41
+ exports.lineupScheduleToEntry = lineupScheduleToEntry;
42
+ exports.stopTemporalServer = stopTemporalServer;
43
+ exports.down = down;
44
+ exports.agentTypesCommand = agentTypesCommand;
45
+ exports.broadcast = broadcast;
46
+ exports.verbClient = verbClient;
47
+ exports.destroy = destroy;
48
+ exports.attachmentInfo = attachmentInfo;
49
+ exports.hosts = hosts;
50
+ exports.refreshHostProfile = refreshHostProfile;
51
+ exports.recall = recall;
52
+ exports.restore = restore;
53
+ exports.ensembleCommand = ensembleCommand;
54
+ exports.release = release;
55
+ const readline = __importStar(require("readline"));
56
+ const fs_1 = require("fs");
57
+ const path_1 = require("path");
58
+ const child_process_1 = require("child_process");
59
+ const os_1 = require("os");
60
+ const crypto_1 = require("crypto");
61
+ const croner_1 = require("croner");
62
+ const client_1 = require("@temporalio/client");
63
+ const spawn_1 = require("../spawn");
64
+ const config_1 = require("../config");
65
+ const git_info_1 = require("../git-info");
66
+ const connection_1 = require("../connection");
67
+ const signals_1 = require("../workflows/signals");
68
+ const scheduler_signals_1 = require("../workflows/scheduler-signals");
69
+ const maestro_signals_1 = require("../workflows/maestro-signals");
70
+ const duration_1 = require("../utils/duration");
71
+ const attachment_format_1 = require("../utils/attachment-format");
72
+ const default_part_1 = require("../utils/default-part");
73
+ const preflight_1 = require("./preflight");
74
+ const mcp_1 = require("./mcp");
75
+ const loader_1 = require("../ensemble/loader");
76
+ const saver_1 = require("../ensemble/saver");
77
+ const agent_types_1 = require("../ensemble/agent-types");
78
+ const validation_1 = require("../utils/validation");
79
+ const search_attributes_1 = require("../utils/search-attributes");
80
+ const daemon_1 = require("./daemon");
81
+ const client_2 = require("../client");
82
+ const constants_1 = require("../constants");
83
+ const recall_format_1 = require("../utils/recall-format");
84
+ const out = __importStar(require("./output"));
85
+ /** Package root is two levels up from dist/cli/ */
86
+ const PACKAGE_ROOT = (0, path_1.resolve)(__dirname, '..', '..');
87
+ /**
88
+ * Ensure the Maestro workflow is running for the given ensemble.
89
+ * Idempotent — uses USE_EXISTING conflict policy.
90
+ */
91
+ async function ensureMaestroWorkflow(client, config, ensemble) {
92
+ const wfId = (0, config_1.maestroWorkflowId)(ensemble);
93
+ try {
94
+ await client.workflow.start('agentMaestroWorkflow', {
95
+ workflowId: wfId,
96
+ taskQueue: config.taskQueue,
97
+ args: [{ ensemble }],
98
+ workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
99
+ searchAttributes: {
100
+ AgentTempoEnsemble: [ensemble],
101
+ },
102
+ });
103
+ }
104
+ catch {
105
+ // Maestro is non-critical — log but don't fail
106
+ }
107
+ }
108
+ /**
109
+ * Resolve a conductor's session name. MUST equal both the spawned process's
110
+ * `ENV.PLAYER_NAME` and the workflow metadata's `playerId` — a mismatch
111
+ * confuses `who_am_i`, reports, and operator-run `restart`/`detach` by name
112
+ * (issue #172).
113
+ */
114
+ function resolveConductorName(opts, lineup) {
115
+ if (lineup) {
116
+ return opts.name
117
+ || lineup.conductor?.name
118
+ || (opts.agent === 'copilot' ? `${opts.ensemble}-conductor` : 'conductor');
119
+ }
120
+ // No lineup: preserve legacy `copilot-${Date.now()}` for ad-hoc copilot
121
+ // conductors (changing that is out of scope for #172).
122
+ return opts.name || (opts.agent === 'copilot' ? `copilot-${Date.now()}` : 'conductor');
123
+ }
124
+ /** Resolve a player's session name. Returns undefined for claude, where
125
+ * Claude Code auto-assigns on spawn. */
126
+ function resolvePlayerName(opts) {
127
+ return opts.name || (opts.agent === 'copilot' ? `copilot-${Date.now()}` : undefined);
128
+ }
129
+ /**
130
+ * Issue #172 (v0.26): pre-create the conductor workflow, optionally with
131
+ * lineup-seeded `messages[]`. MUST run BEFORE the conductor process spawns
132
+ * — otherwise `USE_EXISTING` silently drops the seeded input if the spawned
133
+ * Claude Code MCP client registers the workflow first.
134
+ *
135
+ * Seeded messages (only when `lineup` is provided):
136
+ * 1. lineup instructions (`from: 'lineup'`) — role/phase/convention brief
137
+ * 2. banner + "wait for user, call `resume_ensemble` first" directive
138
+ * (`from: 'system'`), only on `initialStartup: true`
139
+ *
140
+ * When `lineup` is undefined (plain `up` / `conduct`), the workflow is still
141
+ * pre-created with empty seeded messages — this matches the prior inline
142
+ * behavior that held signals safely before the Claude Code MCP client
143
+ * connected.
144
+ */
145
+ async function seedConductorWorkflow(args) {
146
+ const { client, config, ensemble, lineup, initialStartup, conductorName } = args;
147
+ const conductorWfId = (0, config_1.conductorWorkflowId)(ensemble);
148
+ const { gitRoot: conductorGitRoot, gitBranch: conductorGitBranch } = (0, git_info_1.getGitInfo)(process.cwd());
149
+ const conductorSessionId = (0, crypto_1.randomUUID)();
150
+ const resolvedConductorType = lineup?.conductor?.type ? (0, agent_types_1.resolveAgentType)(lineup.conductor.type) : null;
151
+ // Issue #172 follow-up: seed the `from: 'system'` directive BEFORE the
152
+ // lineup's role/phase brief. Earlier messages carry more weight with the
153
+ // LLM — putting the "call resume_ensemble + release FIRST" framing ahead
154
+ // of the lineup instructions reduces the chance the model skims past it
155
+ // and broadcasts directly.
156
+ const seededMessages = [];
157
+ if (initialStartup && lineup) {
158
+ seededMessages.push({
159
+ id: (0, crypto_1.randomUUID)(),
160
+ from: 'system',
161
+ text: (0, constants_1.ensembleReadyDirective)(lineup.name, lineup.players.length),
162
+ timestamp: new Date().toISOString(),
163
+ delivered: false,
164
+ });
165
+ }
166
+ if (lineup?.conductor?.instructions) {
167
+ seededMessages.push({
168
+ id: (0, crypto_1.randomUUID)(),
169
+ from: 'lineup',
170
+ text: lineup.conductor.instructions,
171
+ timestamp: new Date().toISOString(),
172
+ delivered: false,
173
+ });
174
+ }
175
+ const conductorInput = {
176
+ metadata: {
177
+ playerId: conductorName,
178
+ ensemble,
179
+ hostname: (0, os_1.hostname)(),
180
+ workDir: process.cwd(),
181
+ gitRoot: conductorGitRoot,
182
+ gitBranch: conductorGitBranch,
183
+ isConductor: true,
184
+ agentType: args.conductorAgent,
185
+ sessionId: conductorSessionId,
186
+ ...(resolvedConductorType ? { playerType: resolvedConductorType.name, playerTypeDescription: resolvedConductorType.description || '' } : {}),
187
+ },
188
+ // Issue #450 — derive default `part` from the resolved player type so
189
+ // a typed conductor reads as `'<Role> session'` (still falls back to
190
+ // `'Conductor session'` when no type is resolved).
191
+ autoSummary: (0, default_part_1.defaultPart)({
192
+ playerType: resolvedConductorType?.name,
193
+ isConductor: true,
194
+ workDir: process.cwd(),
195
+ adapterType: args.conductorAgent,
196
+ }),
197
+ disableStaleDetection: true,
198
+ temporalConfig: {
199
+ temporalAddress: config.temporalAddress,
200
+ temporalNamespace: config.temporalNamespace,
201
+ taskQueue: config.taskQueue,
202
+ },
203
+ ...(seededMessages.length > 0 ? { messages: seededMessages } : {}),
204
+ };
205
+ await client.workflow.start('agentSessionWorkflow', {
206
+ workflowId: conductorWfId,
207
+ taskQueue: config.taskQueue,
208
+ args: [conductorInput],
209
+ workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
210
+ searchAttributes: {
211
+ ...(conductorGitRoot ? { AgentTempoGitRoot: [conductorGitRoot] } : {}),
212
+ AgentTempoHostname: [(0, os_1.hostname)()],
213
+ AgentTempoEnsemble: [ensemble],
214
+ AgentTempoPlayerId: [conductorName],
215
+ },
216
+ });
217
+ }
218
+ /**
219
+ * Issue #172 (v0.26): pre-create player workflows (warm hold on initial
220
+ * startup), spawn their processes, create scheduled entries, and pause the
221
+ * whole ensemble. Called AFTER the conductor spawn so the conductor tab
222
+ * opens first. Does NOT pre-create the conductor workflow — that must
223
+ * already exist (via `seedConductorWorkflow`).
224
+ */
225
+ async function applyLineupPlayersAndSchedules(args) {
226
+ const { client, config, ensemble, lineup, initialStartup, conductorName } = args;
227
+ // Pre-create and spawn players.
228
+ if (lineup.players.length > 0) {
229
+ console.log();
230
+ out.log(`Recruiting ${lineup.players.length} player${lineup.players.length !== 1 ? 's' : ''} from lineup...`);
231
+ }
232
+ for (const player of lineup.players) {
233
+ // ADR 0014 §4 — `agent: "mock"` is dev-only. Reject up-front rather than
234
+ // letting the spawn fail downstream so operators get a clear hint.
235
+ if (player.agent === 'mock' && !(0, config_1.isDevMode)()) {
236
+ out.warn(`Skipping player "${player.name}" — agent: "mock" requires dev mode. ` +
237
+ `Re-run with --dev to enable.`);
238
+ continue;
239
+ }
240
+ const playerAgent = player.agent === 'copilot' ? 'copilot' :
241
+ player.agent === 'claude' ? 'claude' :
242
+ player.agent === 'mock' ? 'mock' :
243
+ args.conductorAgent;
244
+ const playerWorkDir = player.workDir || process.cwd();
245
+ const resolvedPlayerType = player.type ? (0, agent_types_1.resolveAgentType)(player.type) : null;
246
+ const playerSessionId = (0, crypto_1.randomUUID)();
247
+ const playerWfId = (0, config_1.sessionWorkflowId)(ensemble, player.name);
248
+ const { gitRoot: playerGitRoot, gitBranch: playerGitBranch } = (0, git_info_1.getGitInfo)(playerWorkDir);
249
+ const playerInput = {
250
+ metadata: {
251
+ playerId: player.name,
252
+ ensemble,
253
+ hostname: (0, os_1.hostname)(),
254
+ workDir: playerWorkDir,
255
+ gitRoot: playerGitRoot,
256
+ gitBranch: playerGitBranch,
257
+ isConductor: false,
258
+ agentType: playerAgent,
259
+ sessionId: playerSessionId,
260
+ recruitedBy: conductorName,
261
+ ...(resolvedPlayerType ? { playerType: resolvedPlayerType.name, playerTypeDescription: resolvedPlayerType.description || '' } : {}),
262
+ },
263
+ // Issue #450 — derive default `part` from the resolved player type
264
+ // so a freshly recruited lineup player reads as e.g.
265
+ // `'Engineer session'` instead of the role-agnostic
266
+ // `'Session in <basename>'` placeholder.
267
+ autoSummary: (0, default_part_1.defaultPart)({
268
+ playerType: resolvedPlayerType?.name,
269
+ isConductor: false,
270
+ workDir: (0, path_1.resolve)(playerWorkDir),
271
+ adapterType: playerAgent,
272
+ }),
273
+ disableStaleDetection: true,
274
+ temporalConfig: {
275
+ temporalAddress: config.temporalAddress,
276
+ temporalNamespace: config.temporalNamespace,
277
+ taskQueue: config.taskQueue,
278
+ },
279
+ ...(initialStartup
280
+ ? {
281
+ outboxLocked: true,
282
+ ...(player.instructions ? { heldMessage: player.instructions } : {}),
283
+ }
284
+ : (player.instructions ? {
285
+ messages: [{
286
+ id: (0, crypto_1.randomUUID)(),
287
+ from: 'lineup',
288
+ text: player.instructions,
289
+ timestamp: new Date().toISOString(),
290
+ delivered: false,
291
+ }],
292
+ } : {})),
293
+ };
294
+ try {
295
+ await client.workflow.start('agentSessionWorkflow', {
296
+ workflowId: playerWfId,
297
+ taskQueue: config.taskQueue,
298
+ args: [playerInput],
299
+ workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
300
+ searchAttributes: {
301
+ ...(playerGitRoot ? { AgentTempoGitRoot: [playerGitRoot] } : {}),
302
+ AgentTempoHostname: [(0, os_1.hostname)()],
303
+ AgentTempoEnsemble: [ensemble],
304
+ AgentTempoPlayerId: [player.name],
305
+ },
306
+ });
307
+ }
308
+ catch (err) {
309
+ out.warn(`Could not pre-create workflow for "${player.name}": ${err}`);
310
+ continue;
311
+ }
312
+ // Spawn the player process.
313
+ try {
314
+ if (playerAgent === 'mock') {
315
+ // PR-3 — `--scenario` CLI override wins over per-player lineup
316
+ // `mockScenario`. Forces `mockMode: scripted` because that's the
317
+ // only mode that consumes a scenario. Per-player `mockMode` (silent /
318
+ // chaos / echo) is preserved when no override is set.
319
+ const effectiveMode = args.scenarioOverride ? 'scripted' : (player.mockMode ?? 'echo');
320
+ const effectiveScenario = args.scenarioOverride ?? player.mockScenario;
321
+ (0, spawn_1.spawnMockAdapter)({
322
+ name: player.name,
323
+ ensemble,
324
+ temporalAddress: config.temporalAddress,
325
+ temporalNamespace: config.temporalNamespace,
326
+ temporalApiKey: config.temporalApiKey,
327
+ temporalTlsCertPath: config.temporalTlsCertPath,
328
+ temporalTlsKeyPath: config.temporalTlsKeyPath,
329
+ isConductor: false,
330
+ workDir: playerWorkDir,
331
+ mockMode: effectiveMode,
332
+ ...(effectiveScenario ? { mockScenario: effectiveScenario } : {}),
333
+ });
334
+ }
335
+ else if (playerAgent === 'copilot') {
336
+ (0, spawn_1.spawnCopilotBridge)({
337
+ name: player.name,
338
+ ensemble,
339
+ temporalAddress: config.temporalAddress,
340
+ temporalNamespace: config.temporalNamespace,
341
+ temporalApiKey: config.temporalApiKey,
342
+ temporalTlsCertPath: config.temporalTlsCertPath,
343
+ temporalTlsKeyPath: config.temporalTlsKeyPath,
344
+ isConductor: false,
345
+ workDir: playerWorkDir,
346
+ });
347
+ }
348
+ else {
349
+ const claudeArgs = [
350
+ '--dangerously-skip-permissions',
351
+ '--dangerously-load-development-channels', 'server:agent-tempo',
352
+ // ENSEMBLE_SENTINEL_FLAG carries the ensemble name into the spawned
353
+ // claude.exe's CommandLine so hard-terminate can scope `destroy --all`
354
+ // kills by ensemble (#180, #259). Mirrors src/activities/outbox.ts.
355
+ constants_1.ENSEMBLE_SENTINEL_FLAG, ensemble,
356
+ '-n', player.name,
357
+ ...(resolvedPlayerType?.nativeResolvable ? ['--agent', resolvedPlayerType.name] :
358
+ resolvedPlayerType ? ['--system-prompt', resolvedPlayerType.path] : []),
359
+ ];
360
+ const playerEnvVars = {
361
+ ...args.temporalEnvVars,
362
+ [config_1.ENV.ENSEMBLE]: ensemble,
363
+ [config_1.ENV.CONDUCTOR]: '',
364
+ [config_1.ENV.PLAYER_NAME]: player.name,
365
+ };
366
+ if (resolvedPlayerType) {
367
+ playerEnvVars[config_1.ENV.PLAYER_TYPE] = resolvedPlayerType.name;
368
+ }
369
+ (0, spawn_1.spawnInTerminal)(claudeArgs, playerWorkDir, playerEnvVars, { claudeBin: config.claudeBin });
370
+ }
371
+ out.log(` ${out.green('ok')} ${out.bold(player.name)} in ${playerWorkDir}`);
372
+ }
373
+ catch (err) {
374
+ out.warn(`Could not spawn "${player.name}": ${err}`);
375
+ }
376
+ }
377
+ // Create schedules (independent of hold state).
378
+ if (lineup.schedules && lineup.schedules.length > 0) {
379
+ console.log();
380
+ out.log(`Creating ${lineup.schedules.length} schedule${lineup.schedules.length !== 1 ? 's' : ''}...`);
381
+ for (const sched of lineup.schedules) {
382
+ try {
383
+ const entry = lineupScheduleToEntry(sched);
384
+ const schedulerWfId = (0, config_1.schedulerWorkflowId)(ensemble);
385
+ try {
386
+ const handle = client.workflow.getHandle(schedulerWfId);
387
+ await handle.describe();
388
+ await handle.signal(scheduler_signals_1.addScheduleSignal, entry);
389
+ }
390
+ catch {
391
+ await client.workflow.start('agentSchedulerWorkflow', {
392
+ workflowId: schedulerWfId,
393
+ taskQueue: config.taskQueue,
394
+ args: [{ ensemble, entries: [entry] }],
395
+ workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
396
+ searchAttributes: {
397
+ AgentTempoEnsemble: [ensemble],
398
+ },
399
+ });
400
+ }
401
+ out.check(sched.name, true, `→ ${sched.target}`);
402
+ }
403
+ catch (err) {
404
+ out.warn(`Could not create schedule "${sched.name}": ${err}`);
405
+ }
406
+ }
407
+ }
408
+ // Issue #172 (v0.26): on initial-startup, pause the whole ensemble so the
409
+ // scheduler, per-session outbox dispatch, and maestro all stay quiet while
410
+ // we wait for the user's first message. The system directive baked into
411
+ // the conductor's messages[] tells the LLM to call `resume_ensemble` before
412
+ // taking any action once the user speaks.
413
+ if (initialStartup) {
414
+ await setPausedState(client, ensemble, true);
415
+ }
416
+ }
417
+ /**
418
+ * #288: no longer exported — the `start`/`conduct` CLI verbs were removed.
419
+ * Still invoked internally by `up()` for its post-provisioning conductor +
420
+ * player spawn. Slice 7 will replace this internal usage with the new
421
+ * auto-provision flow, after which this function can be deleted outright.
422
+ */
423
+ async function start(opts) {
424
+ const config = (0, config_1.getConfig)(opts);
425
+ const workDir = opts.dir || process.cwd();
426
+ if (!opts.skipPreflight) {
427
+ const result = await (0, preflight_1.runPreflight)({
428
+ dir: workDir,
429
+ ...opts,
430
+ });
431
+ for (const w of result.warnings)
432
+ out.warn(w);
433
+ if (!result.ok) {
434
+ for (const e of result.errors)
435
+ out.error(e);
436
+ process.exit(1);
437
+ }
438
+ }
439
+ const role = opts.conductor ? 'conductor' : 'player';
440
+ // Check if a conductor workflow already exists for this ensemble
441
+ if (opts.conductor) {
442
+ try {
443
+ const connection = await (0, connection_1.createTemporalConnection)(config);
444
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
445
+ const conductorWfId = (0, config_1.conductorWorkflowId)(opts.ensemble);
446
+ const handle = client.workflow.getHandle(conductorWfId);
447
+ const desc = await handle.describe();
448
+ if (desc.status.name === 'RUNNING') {
449
+ if (opts.replace) {
450
+ out.log(`Stopping existing conductor for ensemble "${opts.ensemble}"...`);
451
+ try {
452
+ // PR-C commit 4: V2 `destroy` update — explicit operator termination.
453
+ await handle.executeUpdate(signals_1.destroyUpdate, { args: [{ reason: 'conductor replace via CLI' }] });
454
+ // Wait briefly for graceful shutdown
455
+ for (let i = 0; i < 10; i++) {
456
+ await new Promise(r => setTimeout(r, 500));
457
+ const check = await handle.describe();
458
+ if (check.status.name !== 'RUNNING')
459
+ break;
460
+ }
461
+ }
462
+ catch {
463
+ // Force cancel if destroy fails (workflow may be stuck/corrupt)
464
+ try {
465
+ await handle.cancel();
466
+ }
467
+ catch { /* already gone */ }
468
+ }
469
+ out.success('Existing conductor stopped');
470
+ }
471
+ else if (opts.resume) {
472
+ out.log(`Resuming conductor for ensemble "${opts.ensemble}" — reconnecting to existing workflow state.\n`);
473
+ }
474
+ else {
475
+ out.error(`A conductor is already running for ensemble "${opts.ensemble}".`);
476
+ out.log(` ${out.dim('agent-tempo conduct --resume')} Reconnect a new session to the existing workflow`);
477
+ out.log(` ${out.dim('agent-tempo conduct --replace')} Stop the existing conductor and start fresh`);
478
+ await connection.close();
479
+ process.exit(1);
480
+ }
481
+ }
482
+ await connection.close();
483
+ }
484
+ catch {
485
+ // No existing conductor — proceed normally
486
+ }
487
+ }
488
+ // Issue #172: `conduct --lineup <name>` loads the lineup with the initial-
489
+ // startup semantics. Resolved here so a bad name/path fails before we spawn
490
+ // the process. Non-conductor `start` ignores `--lineup` (only `up` /
491
+ // `conduct` create ensembles from scratch). `--resume` also ignores it:
492
+ // reconnecting to an existing conductor must NOT re-seed messages (a no-op
493
+ // under `USE_EXISTING`), re-recruit players, or re-pause the ensemble.
494
+ let startLineup;
495
+ if (opts.conductor && opts.lineup) {
496
+ if (opts.resume) {
497
+ out.warn('`--lineup` is ignored with `--resume` — reconnecting to existing conductor without re-applying lineup.');
498
+ }
499
+ else {
500
+ try {
501
+ const resolution = (0, loader_1.resolveLineupPath)(opts.lineup);
502
+ startLineup = (0, loader_1.loadLineup)(resolution.path);
503
+ }
504
+ catch (err) {
505
+ out.error(err.message);
506
+ process.exit(1);
507
+ }
508
+ }
509
+ }
510
+ else if (!opts.conductor && opts.lineup) {
511
+ // Plain `start --lineup` silently dropped the flag; surface a warning so
512
+ // users notice the mistake.
513
+ out.warn('`--lineup` is only meaningful with `conduct` or `up`, not `start` — ignored.');
514
+ }
515
+ const startInitialStartup = Boolean(startLineup) && !opts.noHold;
516
+ out.log(`Starting ${out.bold(role)} in ensemble ${out.cyan(opts.ensemble)}${opts.agent === 'copilot' ? out.dim(' (copilot)') : ''}`);
517
+ // Always forward all resolved Temporal settings to child processes.
518
+ // Don't skip defaults — child processes may not have access to the same config file.
519
+ const temporalEnvVars = {
520
+ [config_1.ENV.TEMPORAL_ADDRESS]: config.temporalAddress,
521
+ [config_1.ENV.TEMPORAL_NAMESPACE]: config.temporalNamespace,
522
+ };
523
+ if (config.temporalApiKey)
524
+ temporalEnvVars[config_1.ENV.TEMPORAL_API_KEY] = config.temporalApiKey;
525
+ if (config.temporalTlsCertPath)
526
+ temporalEnvVars[config_1.ENV.TEMPORAL_TLS_CERT_PATH] = config.temporalTlsCertPath;
527
+ if (config.temporalTlsKeyPath)
528
+ temporalEnvVars[config_1.ENV.TEMPORAL_TLS_KEY_PATH] = config.temporalTlsKeyPath;
529
+ if (config.claudeBin)
530
+ temporalEnvVars[config_1.ENV.CLAUDE_BIN] = config.claudeBin;
531
+ // Resolve the session name ONCE so the spawn env var and the workflow
532
+ // metadata's `playerId` match. Conductor path requires a stable name;
533
+ // player path may leave it undefined for claude auto-assignment.
534
+ const sessionName = opts.conductor
535
+ ? resolveConductorName(opts, startLineup)
536
+ : resolvePlayerName(opts);
537
+ // Pre-seed the conductor workflow BEFORE spawning the Claude Code / copilot
538
+ // process. If the spawned process's MCP client wins the race and registers
539
+ // the workflow first, `USE_EXISTING` silently drops our seeded messages.
540
+ let conductorClient;
541
+ let conductorConnection;
542
+ if (opts.conductor) {
543
+ try {
544
+ conductorConnection = await (0, connection_1.createTemporalConnection)(config);
545
+ conductorClient = new client_1.Client({ connection: conductorConnection, namespace: config.temporalNamespace });
546
+ try {
547
+ await ensureMaestroWorkflow(conductorClient, config, opts.ensemble);
548
+ }
549
+ catch (err) {
550
+ if (process.env.DEBUG) {
551
+ console.error('[agent-tempo:conduct] ensureMaestroWorkflow failed:', err);
552
+ }
553
+ }
554
+ if (startLineup) {
555
+ try {
556
+ await seedConductorWorkflow({
557
+ client: conductorClient,
558
+ config,
559
+ ensemble: opts.ensemble,
560
+ lineup: startLineup,
561
+ initialStartup: startInitialStartup,
562
+ conductorName: sessionName,
563
+ conductorAgent: opts.agent,
564
+ });
565
+ }
566
+ catch (err) {
567
+ out.warn(`Conductor workflow pre-seed failed: ${err instanceof Error ? err.message : String(err)}`);
568
+ }
569
+ }
570
+ }
571
+ catch (err) {
572
+ // Couldn't even connect — let the spawn proceed; the conductor's MCP
573
+ // client will surface a clearer error. Lineup seeding is lost though.
574
+ if (startLineup) {
575
+ out.warn(`Could not connect to Temporal to pre-seed lineup: ${err instanceof Error ? err.message : String(err)}`);
576
+ }
577
+ }
578
+ }
579
+ if (opts.agent === 'copilot') {
580
+ const { pid } = (0, spawn_1.spawnCopilotBridge)({
581
+ name: sessionName,
582
+ ensemble: opts.ensemble,
583
+ temporalAddress: config.temporalAddress,
584
+ temporalNamespace: config.temporalNamespace,
585
+ temporalApiKey: config.temporalApiKey,
586
+ temporalTlsCertPath: config.temporalTlsCertPath,
587
+ temporalTlsKeyPath: config.temporalTlsKeyPath,
588
+ isConductor: opts.conductor,
589
+ workDir,
590
+ });
591
+ out.success(`Launched copilot bridge "${sessionName}" (pid ${pid ?? 'unknown'})`);
592
+ }
593
+ else {
594
+ const claudeArgs = [
595
+ '--dangerously-skip-permissions',
596
+ '--dangerously-load-development-channels', 'server:agent-tempo',
597
+ // ENSEMBLE_SENTINEL_FLAG carries the ensemble name into the spawned
598
+ // claude.exe's CommandLine so hard-terminate can scope `destroy --all`
599
+ // kills by ensemble (#180, #259). Mirrors src/activities/outbox.ts.
600
+ constants_1.ENSEMBLE_SENTINEL_FLAG, opts.ensemble,
601
+ ];
602
+ if (opts.resume && sessionName) {
603
+ // Resume the previous Claude Code conversation by name
604
+ claudeArgs.push('--resume', sessionName);
605
+ }
606
+ else if (sessionName) {
607
+ claudeArgs.push('-n', sessionName);
608
+ }
609
+ const envVars = {
610
+ ...temporalEnvVars,
611
+ [config_1.ENV.ENSEMBLE]: opts.ensemble,
612
+ [config_1.ENV.CONDUCTOR]: opts.conductor ? 'true' : '',
613
+ [config_1.ENV.PLAYER_NAME]: sessionName || '',
614
+ };
615
+ const { pid } = (0, spawn_1.spawnInTerminal)(claudeArgs, workDir, envVars, { claudeBin: config.claudeBin });
616
+ out.success(`Launched ${role} session${sessionName ? ` "${sessionName}"` : ''} (pid ${pid ?? 'unknown'})`);
617
+ }
618
+ out.log(` Ensemble: ${opts.ensemble}`);
619
+ out.log(` Directory: ${workDir}`);
620
+ // Post-spawn: pre-create players, create schedules, pause ensemble.
621
+ // The conductor tab is already open so the user sees it first.
622
+ if (opts.conductor && startLineup && conductorClient) {
623
+ try {
624
+ await applyLineupPlayersAndSchedules({
625
+ client: conductorClient,
626
+ config,
627
+ ensemble: opts.ensemble,
628
+ lineup: startLineup,
629
+ initialStartup: startInitialStartup,
630
+ conductorName: sessionName,
631
+ temporalEnvVars,
632
+ conductorAgent: opts.agent,
633
+ });
634
+ }
635
+ catch (err) {
636
+ out.warn(`Lineup player/schedule setup encountered errors: ${err instanceof Error ? err.message : String(err)}`);
637
+ }
638
+ }
639
+ // #93: resume flow — after the conductor is spawned, scan for orphaned
640
+ // player workflows on this host and enqueue `restart` entries on their
641
+ // outboxes so the daemon re-attaches. Reuses the same helper the daemon
642
+ // calls at boot (`reconcileOnBoot`). Only fires when the user explicitly
643
+ // chose the resume path (`--resume` or `up` option 2).
644
+ if (opts.conductor && opts.resume && conductorClient) {
645
+ try {
646
+ const { restoreOrphansOnce, formatRestoreOutcome } = await Promise.resolve().then(() => __importStar(require('../reconcile/orphans')));
647
+ const summary = await restoreOrphansOnce(conductorClient, { hostname: (0, os_1.hostname)(), invokerPlayerId: 'cli', policy: 'auto' });
648
+ if (summary.details.length > 0) {
649
+ console.log();
650
+ out.heading('Orphaned players');
651
+ for (const d of summary.details) {
652
+ const text = `${d.playerId} — ${formatRestoreOutcome(d.outcome)}`;
653
+ switch (d.outcome.kind) {
654
+ case 'queued':
655
+ out.success(text);
656
+ break;
657
+ case 'failed':
658
+ out.warn(text);
659
+ break;
660
+ case 'skipped':
661
+ out.log(` ${out.dim(text)}`);
662
+ break;
663
+ }
664
+ }
665
+ out.log(`${summary.reattached} reattached, ${summary.skipped} skipped, ${summary.failed} failed.`);
666
+ }
667
+ }
668
+ catch (err) {
669
+ out.warn(`Orphan restore scan failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
670
+ }
671
+ }
672
+ if (conductorConnection) {
673
+ try {
674
+ await conductorConnection.close();
675
+ }
676
+ catch { /* best effort */ }
677
+ }
678
+ out.log(`\nCheck status: ${out.dim('agent-tempo status ' + opts.ensemble)}`);
679
+ if (startLineup && startInitialStartup) {
680
+ console.log();
681
+ out.log(` ${(0, constants_1.ensembleReadyBanner)(startLineup.name, startLineup.players.length)}`);
682
+ }
683
+ }
684
+ async function status(opts) {
685
+ const config = (0, config_1.getConfig)(opts);
686
+ let connection;
687
+ try {
688
+ connection = await Promise.race([
689
+ (0, connection_1.createTemporalConnection)(config),
690
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
691
+ ]);
692
+ }
693
+ catch {
694
+ out.error(`Cannot connect to Temporal at ${config.temporalAddress}`);
695
+ out.log(` Run: ${out.dim('temporal server start-dev')}`);
696
+ process.exit(1);
697
+ return; // unreachable, helps TS
698
+ }
699
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
700
+ // List all running session workflows, filter by ensemble using metadata queries.
701
+ // This avoids depending on custom search attributes which are eventually consistent.
702
+ const query = 'WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running"';
703
+ const sessions = [];
704
+ for await (const wf of client.workflow.list({ query })) {
705
+ try {
706
+ const handle = client.workflow.getHandle(wf.workflowId);
707
+ const [metadata, part] = await Promise.all([
708
+ handle.query('getMetadata').catch(() => ({})),
709
+ handle.query('getPart').catch(() => ''),
710
+ ]);
711
+ const meta = metadata;
712
+ const ensemble = meta.ensemble || '?';
713
+ // Filter by ensemble if specified
714
+ if (opts.ensemble && ensemble !== opts.ensemble)
715
+ continue;
716
+ // Attachment phase lives on the `AgentTempoAttachmentState` search attribute (post-#175).
717
+ const phase = (0, search_attributes_1.getAttachmentPhase)(wf);
718
+ sessions.push({
719
+ id: wf.workflowId,
720
+ name: meta.playerId || wf.workflowId.split('-').pop() || '?',
721
+ part: part || '',
722
+ ensemble,
723
+ workDir: meta.workDir || '?',
724
+ branch: meta.gitBranch || '',
725
+ host: meta.hostname || '',
726
+ conductor: meta.isConductor || false,
727
+ agentType: meta.agentType || 'claude',
728
+ phase,
729
+ });
730
+ }
731
+ catch {
732
+ // workflow may have closed between list and query
733
+ }
734
+ }
735
+ // Query scheduler workflows for active schedules. #586 — using
736
+ // `ScheduleEntry` directly (the wire shape) so the display formatter
737
+ // sees the canonical `'once' | 'interval' | 'cron'` discriminator and
738
+ // can pick up `cronExpression` / `timezone` for cron entries instead
739
+ // of falling through to "one-shot".
740
+ const schedulesByEnsemble = new Map();
741
+ const schedulerQuery = 'WorkflowType = "agentSchedulerWorkflow" AND ExecutionStatus = "Running"';
742
+ for await (const wf of client.workflow.list({ query: schedulerQuery })) {
743
+ try {
744
+ const handle = client.workflow.getHandle(wf.workflowId);
745
+ const entries = await handle.query('getSchedules');
746
+ if (entries.length > 0) {
747
+ // Extract ensemble from workflow ID: agent-scheduler-{ensemble}
748
+ const ensemble = wf.workflowId.replace('agent-scheduler-', '');
749
+ if (opts.ensemble && ensemble !== opts.ensemble)
750
+ continue;
751
+ schedulesByEnsemble.set(ensemble, entries);
752
+ }
753
+ }
754
+ catch {
755
+ // scheduler may have just completed
756
+ }
757
+ }
758
+ await connection.close();
759
+ if (sessions.length === 0 && schedulesByEnsemble.size === 0) {
760
+ out.log(opts.ensemble
761
+ ? `No active sessions in ensemble "${opts.ensemble}".`
762
+ : 'No active sessions found.');
763
+ return;
764
+ }
765
+ // Group by ensemble
766
+ const byEnsemble = new Map();
767
+ for (const s of sessions) {
768
+ const list = byEnsemble.get(s.ensemble) || [];
769
+ list.push(s);
770
+ byEnsemble.set(s.ensemble, list);
771
+ }
772
+ for (const [ensemble, members] of byEnsemble) {
773
+ out.heading(`Ensemble: ${ensemble}`);
774
+ out.log(` ${out.dim(`${members.length} active session${members.length !== 1 ? 's' : ''}`)}`);
775
+ console.log();
776
+ // Sort: conductor first, then alphabetical
777
+ members.sort((a, b) => {
778
+ if (a.conductor !== b.conductor)
779
+ return a.conductor ? -1 : 1;
780
+ return a.name.localeCompare(b.name);
781
+ });
782
+ // Option-B phase → tag mapping (see #176 PR):
783
+ // booting → (pending); attached/processing/awaiting → no tag;
784
+ // draining/detached → (disconnected); gone → (gone).
785
+ const phaseLabel = (phase) => {
786
+ if (phase === 'booting')
787
+ return out.dim(' (pending)');
788
+ if (phase === 'draining' || phase === 'detached')
789
+ return out.yellow(' (disconnected)');
790
+ if (phase === 'gone')
791
+ return out.dim(' (gone)');
792
+ return '';
793
+ };
794
+ for (const s of members) {
795
+ const role = s.conductor ? out.yellow(' (conductor)') : '';
796
+ const agent = s.agentType === 'copilot' ? out.dim(' [copilot]') : '';
797
+ const statusLabel = phaseLabel(s.phase);
798
+ // Show PID info for copilot bridge sessions
799
+ const pidInfo = s.agentType === 'copilot' ? getBridgePidInfo(s.name) : '';
800
+ const name = out.bold(s.name);
801
+ out.log(` ${name}${role}${statusLabel}${agent}${pidInfo}`);
802
+ if (s.part)
803
+ out.log(` ${out.dim(s.part)}`);
804
+ const details = [s.workDir, s.branch, s.host].filter(Boolean).join(' ');
805
+ if (details)
806
+ out.log(` ${out.dim(details)}`);
807
+ }
808
+ // Show schedules for this ensemble
809
+ const ensembleSchedules = schedulesByEnsemble.get(ensemble);
810
+ if (ensembleSchedules && ensembleSchedules.length > 0) {
811
+ console.log();
812
+ out.log(` ${out.dim(`${ensembleSchedules.length} active schedule${ensembleSchedules.length !== 1 ? 's' : ''}`)}`);
813
+ for (const sched of ensembleSchedules) {
814
+ const recur = formatScheduleRecurrence(sched);
815
+ const next = new Date(sched.nextFireAt).toLocaleTimeString();
816
+ const bounds = [];
817
+ if (sched.remainingCount != null)
818
+ bounds.push(`${sched.firedCount}/${sched.firedCount + sched.remainingCount} fired`);
819
+ const boundsStr = bounds.length ? ` (${bounds.join(', ')})` : '';
820
+ out.log(` ${out.bold(sched.name)} → ${sched.target} | ${recur}${boundsStr} | next: ${next}`);
821
+ }
822
+ }
823
+ }
824
+ console.log();
825
+ }
826
+ async function init(opts) {
827
+ if (opts.project) {
828
+ // Per-project .mcp.json mode
829
+ return initProject(opts.dir);
830
+ }
831
+ // Default: global install via `claude mcp add`
832
+ if ((0, mcp_1.isGlobalMcpRegistered)() || (0, mcp_1.isMcpConfigured)(opts.dir)) {
833
+ out.success('agent-tempo already registered');
834
+ out.log(` ${out.dim('claude mcp list -s user')}`);
835
+ return;
836
+ }
837
+ const claudePath = (0, spawn_1.resolveClaudePath)();
838
+ if (claudePath === 'claude') {
839
+ out.warn('claude binary not found — falling back to project-level .mcp.json');
840
+ return initProject(opts.dir);
841
+ }
842
+ if ((0, mcp_1.addGlobalMcp)()) {
843
+ out.success('Registered agent-tempo globally (user scope)');
844
+ out.log(` ${out.dim('Available in all Claude Code sessions')}`);
845
+ }
846
+ else {
847
+ out.warn('Failed to register globally — falling back to project-level .mcp.json');
848
+ return initProject(opts.dir);
849
+ }
850
+ out.log(`\nNext steps:`);
851
+ out.log(` 1. Start Temporal: ${out.dim('temporal server start-dev')}`);
852
+ out.log(` 2. Start conductor: ${out.dim('agent-tempo conduct')}`);
853
+ }
854
+ /** Per-project .mcp.json install (legacy, used with --project flag). */
855
+ function initProject(dir) {
856
+ const mcpPath = (0, path_1.join)(dir, '.mcp.json');
857
+ const entry = {
858
+ command: 'agent-tempo-server',
859
+ };
860
+ if ((0, fs_1.existsSync)(mcpPath)) {
861
+ try {
862
+ const existing = JSON.parse((0, fs_1.readFileSync)(mcpPath, 'utf8'));
863
+ // Backward-compat: detect either the new (`agent-tempo`) or legacy
864
+ // (`agent-tempo`) registration. Skip the rewrite if either is present —
865
+ // the migration verb is the one path that upgrades the key.
866
+ if (existing?.mcpServers?.['agent-tempo'] || existing?.mcpServers?.['agent-tempo']) {
867
+ out.success('.mcp.json already has an agent-tempo entry');
868
+ out.log(` ${out.dim(mcpPath)}`);
869
+ return;
870
+ }
871
+ existing.mcpServers = existing.mcpServers || {};
872
+ existing.mcpServers['agent-tempo'] = entry;
873
+ (0, fs_1.writeFileSync)(mcpPath, JSON.stringify(existing, null, 2) + '\n');
874
+ out.success('Added agent-tempo to existing .mcp.json');
875
+ }
876
+ catch {
877
+ out.error(`Failed to parse ${mcpPath}. Fix the JSON or delete it and re-run.`);
878
+ process.exit(1);
879
+ }
880
+ }
881
+ else {
882
+ const config = {
883
+ mcpServers: {
884
+ 'agent-tempo': entry,
885
+ },
886
+ };
887
+ (0, fs_1.writeFileSync)(mcpPath, JSON.stringify(config, null, 2) + '\n');
888
+ out.success('Created .mcp.json with agent-tempo config');
889
+ }
890
+ out.log(` ${out.dim(mcpPath)}`);
891
+ out.log(`\nNext steps:`);
892
+ out.log(` 1. Start Temporal: ${out.dim('temporal server start-dev')}`);
893
+ out.log(` 2. Start conductor: ${out.dim('agent-tempo conduct')}`);
894
+ }
895
+ // --- Temporal server management ---
896
+ const DEFAULT_DB_PATH = (0, path_1.join)(config_1.AGENT_TEMPO_HOME, 'temporal-data.db');
897
+ // Source of truth lives in `sa-preflight.ts` (REQUIRED_SEARCH_ATTRIBUTES) —
898
+ // avoid drifting a second copy here.
899
+ const sa_preflight_1 = require("./sa-preflight");
900
+ async function isTemporalReachable(config) {
901
+ try {
902
+ const conn = await (0, connection_1.createTemporalConnection)(config);
903
+ try {
904
+ // Verify namespace is ready — a gRPC connection alone doesn't guarantee the server can serve requests
905
+ const client = new client_1.Client({ connection: conn, namespace: config.temporalNamespace || 'default' });
906
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
907
+ for await (const _ of client.workflow.list({ query: 'WorkflowId = "__readiness_probe__"' })) {
908
+ break;
909
+ }
910
+ }
911
+ finally {
912
+ await conn.close();
913
+ }
914
+ return true;
915
+ }
916
+ catch {
917
+ return false;
918
+ }
919
+ }
920
+ function temporalCliExists() {
921
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
922
+ try {
923
+ (0, child_process_1.execFileSync)(cmd, ['temporal'], { stdio: 'ignore' });
924
+ return true;
925
+ }
926
+ catch {
927
+ return false;
928
+ }
929
+ }
930
+ function registerSearchAttributes(temporalAddress, namespace = 'default') {
931
+ let failed = 0;
932
+ for (const attr of sa_preflight_1.REQUIRED_SEARCH_ATTRIBUTES) {
933
+ const r = (0, sa_preflight_1.registerSearchAttribute)(attr, temporalAddress, namespace);
934
+ switch (r.status) {
935
+ case 'created':
936
+ out.success(`Registered search attribute: ${attr.name}`);
937
+ break;
938
+ case 'already-exists':
939
+ out.dim(` ${attr.name} (already registered)`);
940
+ break;
941
+ case 'failed':
942
+ // Surface the real error — pre-#605 this branch was silently
943
+ // labeled "already exists" and the operator only discovered the
944
+ // problem hours later when workflow start failed with
945
+ // INVALID_ARGUMENT. Most common cause on the SQLite dev server is
946
+ // the 10-Keyword-per-namespace cap (often hit when a namespace
947
+ // accumulates both old + new wire-rename attribute families).
948
+ failed++;
949
+ out.warn(`Failed to register ${attr.name}: ${r.detail}`);
950
+ break;
951
+ }
952
+ }
953
+ if (failed > 0) {
954
+ out.warn(`${failed} search attribute${failed === 1 ? '' : 's'} not registered — ` +
955
+ `workflow starts will fail. Resolve the errors above before continuing.`);
956
+ }
957
+ return { failed };
958
+ }
959
+ async function server(opts) {
960
+ const config = (0, config_1.getConfig)(opts);
961
+ if (!temporalCliExists()) {
962
+ out.error('temporal CLI not found on PATH');
963
+ out.log(` Install: ${out.dim('https://docs.temporal.io/cli')}`);
964
+ process.exit(1);
965
+ }
966
+ // Check if already running
967
+ const alreadyRunning = await isTemporalReachable(config);
968
+ if (alreadyRunning) {
969
+ out.success(`Temporal already running at ${config.temporalAddress}`);
970
+ out.log(' Registering search attributes...');
971
+ registerSearchAttributes(config.temporalAddress, config.temporalNamespace);
972
+ return;
973
+ }
974
+ // Ensure data directory exists
975
+ (0, fs_1.mkdirSync)(config_1.AGENT_TEMPO_HOME, { recursive: true });
976
+ const port = config.temporalAddress.split(':')[1] || '7233';
977
+ const args = [
978
+ 'server', 'start-dev',
979
+ '--port', port,
980
+ '--db-filename', DEFAULT_DB_PATH,
981
+ ];
982
+ out.log(`Starting Temporal dev server on port ${port}...`);
983
+ out.log(` Data: ${out.dim(DEFAULT_DB_PATH)}`);
984
+ if (opts.background) {
985
+ const child = (0, child_process_1.spawn)('temporal', args, {
986
+ detached: true,
987
+ stdio: 'ignore',
988
+ });
989
+ child.unref();
990
+ out.success(`Temporal started in background (pid ${child.pid})`);
991
+ // Wait for it to be ready
992
+ for (let i = 0; i < 20; i++) {
993
+ await new Promise(r => setTimeout(r, 500));
994
+ if (await isTemporalReachable(config))
995
+ break;
996
+ }
997
+ }
998
+ else {
999
+ // Foreground — register attributes after startup, then hand over stdio
1000
+ const child = (0, child_process_1.spawn)('temporal', args, {
1001
+ stdio: ['ignore', 'pipe', 'pipe'],
1002
+ });
1003
+ // Wait for ready, then register attributes
1004
+ const waitForReady = async () => {
1005
+ for (let i = 0; i < 20; i++) {
1006
+ await new Promise(r => setTimeout(r, 500));
1007
+ if (await isTemporalReachable(config)) {
1008
+ out.success(`Temporal running at ${config.temporalAddress}`);
1009
+ out.log(' Registering search attributes...');
1010
+ registerSearchAttributes(config.temporalAddress, config.temporalNamespace);
1011
+ out.log(`\n ${out.dim('Press Ctrl+C to stop')}\n`);
1012
+ return;
1013
+ }
1014
+ }
1015
+ out.warn('Temporal started but not responding — search attributes not registered');
1016
+ };
1017
+ waitForReady();
1018
+ // Pipe output through
1019
+ child.stdout?.pipe(process.stdout);
1020
+ child.stderr?.pipe(process.stderr);
1021
+ // Forward signals for clean shutdown
1022
+ const forward = (sig) => { child.kill(sig); };
1023
+ process.on('SIGINT', () => forward('SIGINT'));
1024
+ process.on('SIGTERM', () => forward('SIGTERM'));
1025
+ await new Promise((resolve) => {
1026
+ child.on('exit', (code) => {
1027
+ if (code && code !== 0)
1028
+ out.error(`Temporal exited with code ${code}`);
1029
+ resolve();
1030
+ });
1031
+ });
1032
+ }
1033
+ // Register search attributes (for background mode — foreground does it inline)
1034
+ if (opts.background) {
1035
+ out.log(' Registering search attributes...');
1036
+ registerSearchAttributes(config.temporalAddress, config.temporalNamespace);
1037
+ out.success('Temporal ready');
1038
+ }
1039
+ }
1040
+ async function up(opts) {
1041
+ const config = (0, config_1.getConfig)(opts);
1042
+ out.heading('agent-tempo setup');
1043
+ // Step 1: Check temporal CLI
1044
+ if (!temporalCliExists()) {
1045
+ out.error('temporal CLI not found');
1046
+ out.log(`\n Install the Temporal CLI first:`);
1047
+ out.log(` ${out.dim('https://docs.temporal.io/cli')}\n`);
1048
+ process.exit(1);
1049
+ }
1050
+ out.check('temporal CLI installed', true);
1051
+ // Step 2: Start Temporal if needed
1052
+ const temporalUp = await isTemporalReachable(config);
1053
+ if (temporalUp) {
1054
+ out.check('Temporal running', true, config.temporalAddress);
1055
+ }
1056
+ else {
1057
+ out.log(` ${out.dim('...')} Starting Temporal dev server...`);
1058
+ (0, fs_1.mkdirSync)(config_1.AGENT_TEMPO_HOME, { recursive: true });
1059
+ const port = config.temporalAddress.split(':')[1] || '7233';
1060
+ const child = (0, child_process_1.spawn)('temporal', [
1061
+ 'server', 'start-dev',
1062
+ '--port', port,
1063
+ '--db-filename', DEFAULT_DB_PATH,
1064
+ ], { detached: true, stdio: 'ignore' });
1065
+ child.unref();
1066
+ // Wait for ready
1067
+ let ready = false;
1068
+ for (let i = 0; i < 20; i++) {
1069
+ await new Promise(r => setTimeout(r, 500));
1070
+ if (await isTemporalReachable(config)) {
1071
+ ready = true;
1072
+ break;
1073
+ }
1074
+ }
1075
+ if (!ready) {
1076
+ out.error('Temporal did not start within 10 seconds');
1077
+ process.exit(1);
1078
+ }
1079
+ out.check('Temporal started', true, `pid ${child.pid}, data in ~/.agent-tempo/`);
1080
+ }
1081
+ // Step 3: Register search attributes
1082
+ registerSearchAttributes(config.temporalAddress, config.temporalNamespace);
1083
+ // Step 3.5: Install shipped agent types to ~/.claude/agents/ (if not already there)
1084
+ const userAgentsDir = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'agents');
1085
+ const shippedAgentsPath = (0, path_1.join)(PACKAGE_ROOT, 'examples', 'agents');
1086
+ if ((0, fs_1.existsSync)(shippedAgentsPath)) {
1087
+ (0, fs_1.mkdirSync)(userAgentsDir, { recursive: true });
1088
+ const shipped = (0, fs_1.readdirSync)(shippedAgentsPath).filter(f => f.endsWith('.md'));
1089
+ let installed = 0;
1090
+ for (const file of shipped) {
1091
+ const dest = (0, path_1.join)(userAgentsDir, file);
1092
+ if (!(0, fs_1.existsSync)(dest)) {
1093
+ (0, fs_1.copyFileSync)((0, path_1.join)(shippedAgentsPath, file), dest);
1094
+ installed++;
1095
+ }
1096
+ }
1097
+ if (installed > 0) {
1098
+ out.success(`Installed ${installed} agent type${installed !== 1 ? 's' : ''} to ~/.claude/agents/`);
1099
+ }
1100
+ else {
1101
+ out.dim(` Agent types already installed (${shipped.length} in ~/.claude/agents/)`);
1102
+ }
1103
+ }
1104
+ // Step 3.7: Start worker daemon if not already running
1105
+ if ((0, daemon_1.isDaemonRunning)()) {
1106
+ const daemonStatus = (0, daemon_1.getDaemonStatus)();
1107
+ out.check('Worker daemon running', true, `pid ${daemonStatus.pid}`);
1108
+ }
1109
+ else {
1110
+ out.log(` ${out.dim('...')} Starting worker daemon...`);
1111
+ try {
1112
+ const daemonPid = await (0, daemon_1.startDaemon)(config);
1113
+ out.check('Worker daemon started', true, `pid ${daemonPid}`);
1114
+ }
1115
+ catch (err) {
1116
+ out.error(`Failed to start worker daemon: ${err.message || err}`);
1117
+ out.log(` ${out.dim('You can start it manually: agent-tempo daemon start')}`);
1118
+ process.exit(1);
1119
+ }
1120
+ }
1121
+ // Step 4: Register MCP server if needed
1122
+ if ((0, mcp_1.isMcpConfigured)(process.cwd())) {
1123
+ out.check('MCP configured', true);
1124
+ }
1125
+ else {
1126
+ await init({ dir: process.cwd() });
1127
+ out.check('MCP configured', true);
1128
+ }
1129
+ // Always forward all resolved Temporal settings to child processes.
1130
+ const temporalEnvVars = {
1131
+ [config_1.ENV.TEMPORAL_ADDRESS]: config.temporalAddress,
1132
+ [config_1.ENV.TEMPORAL_NAMESPACE]: config.temporalNamespace,
1133
+ };
1134
+ if (config.temporalApiKey)
1135
+ temporalEnvVars[config_1.ENV.TEMPORAL_API_KEY] = config.temporalApiKey;
1136
+ if (config.temporalTlsCertPath)
1137
+ temporalEnvVars[config_1.ENV.TEMPORAL_TLS_CERT_PATH] = config.temporalTlsCertPath;
1138
+ if (config.temporalTlsKeyPath)
1139
+ temporalEnvVars[config_1.ENV.TEMPORAL_TLS_KEY_PATH] = config.temporalTlsKeyPath;
1140
+ // Load lineup if --lineup is provided
1141
+ let lineup;
1142
+ const lineupArg = opts.lineup;
1143
+ if (lineupArg) {
1144
+ try {
1145
+ const resolution = (0, loader_1.resolveLineupPath)(lineupArg);
1146
+ lineup = (0, loader_1.loadLineup)(resolution.path);
1147
+ }
1148
+ catch (err) {
1149
+ out.error(err.message);
1150
+ process.exit(1);
1151
+ }
1152
+ }
1153
+ if (lineup) {
1154
+ out.check('Lineup loaded', true, lineup.name);
1155
+ }
1156
+ // Issue #172: initial-startup behavior is on by default when a lineup is
1157
+ // loaded. `--no-hold` opts out and preserves legacy immediate-start. No
1158
+ // lineup ⇒ the flag is a no-op (nothing to defer).
1159
+ const initialStartup = Boolean(lineup) && !opts.noHold;
1160
+ // Resolve conductor agent from lineup or CLI flags.
1161
+ // `agent: "mock"` is dev-only — silently fall back to the CLI default
1162
+ // outside dev mode so a mis-configured lineup doesn't spawn a real session
1163
+ // unexpectedly (mirrors the player-level guard at ~line 209).
1164
+ const conductorAgent = lineup?.conductor?.agent === 'copilot' ? 'copilot' :
1165
+ lineup?.conductor?.agent === 'mock' && (0, config_1.isDevMode)() ? 'mock' :
1166
+ opts.agent;
1167
+ // Step 5: Connect to Temporal and check for existing conductor
1168
+ console.log();
1169
+ const connection = await (0, connection_1.createTemporalConnection)(config);
1170
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
1171
+ const conductorWfId = (0, config_1.conductorWorkflowId)(opts.ensemble);
1172
+ // Check if a conductor is already running
1173
+ try {
1174
+ const existingHandle = client.workflow.getHandle(conductorWfId);
1175
+ const desc = await existingHandle.describe();
1176
+ if (desc.status.name === 'RUNNING') {
1177
+ if (!process.stdin.isTTY) {
1178
+ out.error(`A conductor is already running for ensemble "${opts.ensemble}".`);
1179
+ out.log(` Use ${out.dim('--resume')} to reconnect, or ${out.dim('agent-tempo start')} to join as a player.`);
1180
+ process.exit(1);
1181
+ }
1182
+ out.warn(`A conductor is already running for ensemble "${opts.ensemble}".`);
1183
+ console.log();
1184
+ out.log(` 1) Join as a new player session`);
1185
+ out.log(` 2) Reconnect to the existing conductor (--resume)`);
1186
+ out.log(` 3) Tear down and start fresh`);
1187
+ out.log(` 4) Cancel`);
1188
+ console.log();
1189
+ const choice = await new Promise((res) => {
1190
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1191
+ rl.question(` ${out.cyan('?')} Choose an option [1-4]: `, (answer) => {
1192
+ rl.close();
1193
+ res(answer.trim());
1194
+ });
1195
+ });
1196
+ switch (choice) {
1197
+ case '1':
1198
+ // Join as a player — delegate to start()
1199
+ console.log();
1200
+ out.log('Joining as a player session...');
1201
+ await start({
1202
+ ensemble: opts.ensemble,
1203
+ conductor: false,
1204
+ name: opts.name,
1205
+ skipPreflight: true, // infrastructure already verified above
1206
+ agent: opts.agent,
1207
+ dir: process.cwd(),
1208
+ });
1209
+ return;
1210
+ case '2':
1211
+ // Reconnect to existing conductor
1212
+ console.log();
1213
+ out.log('Reconnecting to existing conductor...');
1214
+ await start({
1215
+ ensemble: opts.ensemble,
1216
+ conductor: true,
1217
+ resume: true,
1218
+ name: opts.name,
1219
+ skipPreflight: true,
1220
+ agent: opts.agent,
1221
+ dir: process.cwd(),
1222
+ });
1223
+ return;
1224
+ case '3':
1225
+ // Terminate existing workflows, then fall through to normal up flow
1226
+ console.log();
1227
+ try {
1228
+ await client.workflow.getHandle(conductorWfId).terminate('up: fresh start');
1229
+ }
1230
+ catch { /* may not exist */ }
1231
+ try {
1232
+ await client.workflow.getHandle((0, config_1.schedulerWorkflowId)(opts.ensemble)).terminate('up: fresh start');
1233
+ }
1234
+ catch { /* may not exist */ }
1235
+ try {
1236
+ await client.workflow.getHandle((0, config_1.maestroWorkflowId)(opts.ensemble)).terminate('up: fresh start');
1237
+ }
1238
+ catch { /* may not exist */ }
1239
+ out.success('Existing ensemble torn down');
1240
+ // Fall through to normal up flow below
1241
+ break;
1242
+ case '4':
1243
+ default:
1244
+ out.log('Cancelled.');
1245
+ process.exit(0);
1246
+ }
1247
+ }
1248
+ }
1249
+ catch {
1250
+ // No existing conductor — proceed normally
1251
+ }
1252
+ out.log(`Launching conductor in ensemble ${out.cyan(opts.ensemble)}${conductorAgent === 'copilot' ? out.dim(' (copilot)') : ''}...`);
1253
+ const sessionName = resolveConductorName({ ...opts, agent: conductorAgent }, lineup);
1254
+ // Legacy `lineup.conductor.agent` (string form, e.g. path to a system prompt)
1255
+ // is passed through to the spawn CLI below — not to the workflow metadata.
1256
+ const conductorType = lineup?.conductor?.agent && lineup.conductor.agent !== 'default' && lineup.conductor.agent !== 'copilot'
1257
+ ? lineup.conductor.agent
1258
+ : undefined;
1259
+ const conductorTypeName = lineup?.conductor?.type;
1260
+ const resolvedConductorType = conductorTypeName ? (0, agent_types_1.resolveAgentType)(conductorTypeName) : null;
1261
+ await seedConductorWorkflow({
1262
+ client,
1263
+ config,
1264
+ ensemble: opts.ensemble,
1265
+ lineup,
1266
+ initialStartup,
1267
+ conductorName: sessionName,
1268
+ conductorAgent,
1269
+ });
1270
+ out.check('Conductor workflow pre-created', true);
1271
+ // Spawn the conductor process
1272
+ let pid;
1273
+ if (conductorAgent === 'mock') {
1274
+ // Dev-mode mock conductor — mirrors the player mock-spawn path in
1275
+ // applyLineupPlayersAndSchedules. isConductor: true so the mock
1276
+ // adapter registers the session as the ensemble conductor.
1277
+ const effectiveMode = lineup?.conductor?.mockMode ?? 'echo';
1278
+ const effectiveScenario = lineup?.conductor?.mockScenario;
1279
+ ({ pid } = (0, spawn_1.spawnMockAdapter)({
1280
+ name: sessionName,
1281
+ ensemble: opts.ensemble,
1282
+ temporalAddress: config.temporalAddress,
1283
+ temporalNamespace: config.temporalNamespace,
1284
+ temporalApiKey: config.temporalApiKey,
1285
+ temporalTlsCertPath: config.temporalTlsCertPath,
1286
+ temporalTlsKeyPath: config.temporalTlsKeyPath,
1287
+ isConductor: true,
1288
+ workDir: process.cwd(),
1289
+ mockMode: effectiveMode,
1290
+ ...(effectiveScenario ? { mockScenario: effectiveScenario } : {}),
1291
+ }));
1292
+ }
1293
+ else if (conductorAgent === 'copilot') {
1294
+ ({ pid } = (0, spawn_1.spawnCopilotBridge)({
1295
+ name: sessionName,
1296
+ ensemble: opts.ensemble,
1297
+ temporalAddress: config.temporalAddress,
1298
+ temporalNamespace: config.temporalNamespace,
1299
+ temporalApiKey: config.temporalApiKey,
1300
+ temporalTlsCertPath: config.temporalTlsCertPath,
1301
+ temporalTlsKeyPath: config.temporalTlsKeyPath,
1302
+ isConductor: true,
1303
+ workDir: process.cwd(),
1304
+ }));
1305
+ }
1306
+ else {
1307
+ const claudeArgs = [
1308
+ '--dangerously-skip-permissions',
1309
+ '--dangerously-load-development-channels', 'server:agent-tempo',
1310
+ // ENSEMBLE_SENTINEL_FLAG carries the ensemble name into the spawned
1311
+ // claude.exe's CommandLine so hard-terminate can scope `destroy --all`
1312
+ // kills by ensemble (#180, #259). Mirrors src/activities/outbox.ts.
1313
+ constants_1.ENSEMBLE_SENTINEL_FLAG, opts.ensemble,
1314
+ '-n', sessionName,
1315
+ ...(resolvedConductorType?.nativeResolvable ? ['--agent', resolvedConductorType.name] :
1316
+ resolvedConductorType ? ['--system-prompt', resolvedConductorType.path] :
1317
+ conductorType ? ['--system-prompt', conductorType] : []),
1318
+ ];
1319
+ const conductorEnvVars = {
1320
+ ...temporalEnvVars,
1321
+ [config_1.ENV.ENSEMBLE]: opts.ensemble,
1322
+ [config_1.ENV.CONDUCTOR]: 'true',
1323
+ [config_1.ENV.PLAYER_NAME]: sessionName,
1324
+ };
1325
+ if (resolvedConductorType || conductorTypeName) {
1326
+ conductorEnvVars[config_1.ENV.PLAYER_TYPE] = resolvedConductorType?.name || conductorTypeName || '';
1327
+ }
1328
+ ({ pid } = (0, spawn_1.spawnInTerminal)(claudeArgs, process.cwd(), conductorEnvVars, { claudeBin: config.claudeBin }));
1329
+ }
1330
+ out.success(`Conductor launched (pid ${pid ?? 'unknown'})`);
1331
+ // Step 6: If lineup provided, recruit players, create schedules, and
1332
+ // pause the ensemble for initial-startup — same code path as
1333
+ // `conduct --lineup` via the shared helper.
1334
+ if (lineup) {
1335
+ await ensureMaestroWorkflow(client, config, opts.ensemble);
1336
+ if (lineup.conductor?.instructions) {
1337
+ out.check('Conductor instructions baked into workflow', true);
1338
+ }
1339
+ await applyLineupPlayersAndSchedules({
1340
+ client,
1341
+ config,
1342
+ ensemble: opts.ensemble,
1343
+ lineup,
1344
+ initialStartup,
1345
+ conductorName: sessionName,
1346
+ temporalEnvVars,
1347
+ conductorAgent,
1348
+ ...(opts.scenario ? { scenarioOverride: opts.scenario } : {}),
1349
+ });
1350
+ }
1351
+ await connection.close();
1352
+ console.log();
1353
+ out.success('You\'re all set!');
1354
+ out.log(` Ensemble: ${out.cyan(opts.ensemble)}`);
1355
+ if (!lineup) {
1356
+ out.log(`\n ${out.bold('What next?')}`);
1357
+ out.log(` ${out.dim('agent-tempo start ' + opts.ensemble)} Add a player session`);
1358
+ out.log(` ${out.dim('agent-tempo status ' + opts.ensemble)} See who\'s active`);
1359
+ out.log(` Or ask the conductor to ${out.dim('recruit')} players for you`);
1360
+ }
1361
+ else {
1362
+ out.log(` Lineup: ${out.dim(lineup.name)}`);
1363
+ out.log(` Players: ${lineup.players.length}`);
1364
+ if (lineup.schedules?.length)
1365
+ out.log(` Schedules: ${lineup.schedules.length}`);
1366
+ out.log(`\n ${out.dim('agent-tempo status ' + opts.ensemble)} See who\'s active`);
1367
+ }
1368
+ // Issue #172: print the canonical "ensemble ready" banner on stdout so the
1369
+ // user sees the same wording in their terminal, the conductor's tab, and
1370
+ // the TUI. On `--no-hold` the legacy wording is preserved implicitly since
1371
+ // nothing is deferred — we only surface the banner on initial-startup paths.
1372
+ if (lineup && initialStartup) {
1373
+ console.log();
1374
+ out.log(` ${(0, constants_1.ensembleReadyBanner)(lineup.name, lineup.players.length)}`);
1375
+ }
1376
+ console.log();
1377
+ }
1378
+ /**
1379
+ * Format a `ScheduleEntry` recurrence for the `status` display.
1380
+ *
1381
+ * #586 — cron entries previously rendered as `'one-shot'` because the inline
1382
+ * formatter only checked `sched.interval`. The display formatter now mirrors
1383
+ * the wire-type triplet (`'once' | 'interval' | 'cron'`) from
1384
+ * `ScheduleEntry.type` so cron schedules from the lineup loader (and the
1385
+ * MCP `load_lineup` path) read correctly in `agent-tempo status`.
1386
+ *
1387
+ * Exported for unit tests.
1388
+ */
1389
+ function formatScheduleRecurrence(sched) {
1390
+ if (sched.type === 'cron' && sched.cronExpression) {
1391
+ const tz = sched.timezone && sched.timezone !== 'UTC' ? ` ${sched.timezone}` : '';
1392
+ return `cron: ${sched.cronExpression}${tz}`;
1393
+ }
1394
+ if (sched.interval) {
1395
+ return `every ${(0, duration_1.formatDurationMs)(sched.interval)}`;
1396
+ }
1397
+ return 'one-shot';
1398
+ }
1399
+ /**
1400
+ * Convert a lineup schedule definition to a ScheduleEntry for the scheduler
1401
+ * workflow.
1402
+ *
1403
+ * #586 — cron entries now produce `type: 'cron'` with `cronExpression` +
1404
+ * `timezone` populated; previously they fell through to the default
1405
+ * `nextFireAt = now + 60_000` with `type: 'once'`, firing once and getting
1406
+ * garbage-collected. The cron branch mirrors the MCP-side `load_lineup` tool
1407
+ * at `src/tools/load-lineup.ts:280-300` so both load paths agree on the wire
1408
+ * shape submitted to the scheduler workflow.
1409
+ *
1410
+ * Exported for unit tests; consumed internally by `up` /
1411
+ * `ensembleCommand` after a lineup is parsed.
1412
+ */
1413
+ function lineupScheduleToEntry(sched,
1414
+ /** Injectable clock for deterministic tests. Defaults to `Date.now()`. */
1415
+ now = Date.now()) {
1416
+ let nextFireAt;
1417
+ let interval;
1418
+ let cronExpression;
1419
+ let timezone;
1420
+ if (sched.cron) {
1421
+ // #586 — cron branch matches MCP `load_lineup` (src/tools/load-lineup.ts:280).
1422
+ // `croner` is a runtime dependency declared in package.json; the scheduler
1423
+ // workflow uses it on the firing side too (src/activities/schedule-fire.ts).
1424
+ cronExpression = sched.cron;
1425
+ timezone = sched.timezone ?? 'UTC';
1426
+ const job = new croner_1.Cron(cronExpression, { timezone });
1427
+ const next = job.nextRun(new Date(now));
1428
+ if (!next) {
1429
+ throw new Error(`Cron expression "${sched.cron}" has no upcoming fire time (schedule "${sched.name}")`);
1430
+ }
1431
+ nextFireAt = next.toISOString();
1432
+ }
1433
+ else if (sched.every) {
1434
+ interval = parseDuration(sched.every);
1435
+ nextFireAt = sched.delay
1436
+ ? new Date(now + parseDuration(sched.delay)).toISOString()
1437
+ : new Date(now + interval).toISOString();
1438
+ }
1439
+ else if (sched.at) {
1440
+ nextFireAt = new Date(sched.at).toISOString();
1441
+ }
1442
+ else if (sched.delay) {
1443
+ nextFireAt = new Date(now + parseDuration(sched.delay)).toISOString();
1444
+ }
1445
+ else {
1446
+ nextFireAt = new Date(now + 60_000).toISOString(); // default: 1 minute
1447
+ }
1448
+ const type = cronExpression
1449
+ ? 'cron'
1450
+ : interval
1451
+ ? 'interval'
1452
+ : 'once';
1453
+ return {
1454
+ name: sched.name,
1455
+ message: sched.message,
1456
+ target: sched.target,
1457
+ createdBy: 'lineup',
1458
+ nextFireAt,
1459
+ interval,
1460
+ cronExpression,
1461
+ timezone,
1462
+ until: sched.until,
1463
+ remainingCount: sched.count,
1464
+ firedCount: 0,
1465
+ type,
1466
+ };
1467
+ }
1468
+ /** Parse a human duration string like "10m", "1h", "30s" to milliseconds. */
1469
+ function parseDuration(s) {
1470
+ const match = s.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/);
1471
+ if (!match)
1472
+ throw new Error(`Invalid duration: "${s}"`);
1473
+ const value = parseFloat(match[1]);
1474
+ switch (match[2]) {
1475
+ case 's': return value * 1_000;
1476
+ case 'm': return value * 60_000;
1477
+ case 'h': return value * 3_600_000;
1478
+ case 'd': return value * 86_400_000;
1479
+ default: throw new Error(`Unknown duration unit: "${match[2]}"`);
1480
+ }
1481
+ }
1482
+ /** Prompt the user for y/n confirmation. Exits with code 1 in non-TTY environments. */
1483
+ async function confirmPrompt(message) {
1484
+ if (!process.stdin.isTTY) {
1485
+ out.error('Non-interactive environment: use --yes / -y to confirm teardown.');
1486
+ process.exit(1);
1487
+ }
1488
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1489
+ return new Promise((resolve) => {
1490
+ rl.question(`${message} [y/N] `, (answer) => {
1491
+ rl.close();
1492
+ resolve(answer.trim().toLowerCase() === 'y');
1493
+ });
1494
+ });
1495
+ }
1496
+ /**
1497
+ * Require the user to type `expected` verbatim to confirm an irrecoverable
1498
+ * action. Exits with code 1 in non-TTY environments.
1499
+ */
1500
+ async function typedConfirmPrompt(message, expected) {
1501
+ if (!process.stdin.isTTY) {
1502
+ out.error('Non-interactive environment: use --yes / -y to skip this confirmation.');
1503
+ process.exit(1);
1504
+ }
1505
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1506
+ return new Promise((resolve) => {
1507
+ rl.question(`${message}\n Type ${out.bold(expected)} to confirm: `, (answer) => {
1508
+ rl.close();
1509
+ resolve(answer.trim() === expected);
1510
+ });
1511
+ });
1512
+ }
1513
+ /**
1514
+ * Stop the shared Temporal dev server, with the same cross-profile guard
1515
+ * that `stopDaemon`'s zombie reaper already applies (ADR 0014 §5.6).
1516
+ *
1517
+ * The Temporal dev server is a single OS-wide process — `pkill -f` on
1518
+ * POSIX and `taskkill /IM temporal.exe` on Windows kill it by name and
1519
+ * cannot distinguish dev-profile vs prod-profile ownership. So when
1520
+ * `agent-tempo --dev down` runs while the prod profile is also active,
1521
+ * the unconditional kill takes down the prod profile's Temporal as
1522
+ * collateral damage. This is exactly the bug `isOtherProfileLikelyRunning`
1523
+ * was introduced to prevent on the daemon side; the missing piece was
1524
+ * `down`'s own Temporal kill (#423).
1525
+ *
1526
+ * `--kill-shared-temporal` (passed as `killSharedTemporal: true`) is the
1527
+ * explicit opt-in for the hard-reset case where the user accepts cross-
1528
+ * profile collateral damage.
1529
+ */
1530
+ function stopTemporalServer(opts) {
1531
+ const otherLikelyRunning = opts.isOtherProfileLikelyRunning ?? daemon_1.isOtherProfileLikelyRunning;
1532
+ const exec = opts.exec ?? ((cmd, args) => { (0, child_process_1.execFileSync)(cmd, args, { stdio: 'ignore' }); });
1533
+ const platform = opts.platform ?? process.platform;
1534
+ if (!opts.killSharedTemporal && otherLikelyRunning()) {
1535
+ return { action: 'skipped-cross-profile' };
1536
+ }
1537
+ try {
1538
+ if (platform === 'win32') {
1539
+ exec('taskkill', ['/F', '/IM', 'temporal.exe']);
1540
+ }
1541
+ else {
1542
+ exec('pkill', ['-f', 'temporal server start-dev']);
1543
+ }
1544
+ return { action: 'killed' };
1545
+ }
1546
+ catch (err) {
1547
+ return { action: 'failed', error: err };
1548
+ }
1549
+ }
1550
+ async function down(opts) {
1551
+ const config = (0, config_1.getConfig)(opts);
1552
+ out.heading('agent-tempo teardown');
1553
+ out.log(opts.destroy
1554
+ ? ` ${out.bold('Destroying all workflows')}, then stopping daemon + Temporal.`
1555
+ : ` Stopping daemon + Temporal. Workflows stay parked for the next ${out.dim('agent-tempo up')}.`);
1556
+ // Step 1 (destroy mode only): enumerate + terminate workflows across every
1557
+ // ensemble, after a typed confirmation showing the user what's at stake.
1558
+ const temporalUp = await isTemporalReachable(config);
1559
+ if (opts.destroy && temporalUp) {
1560
+ try {
1561
+ const connection = await (0, connection_1.createTemporalConnection)(config);
1562
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
1563
+ try {
1564
+ // Enumerate every workflow type we own, not just sessions. Previously
1565
+ // we only listed `agentSessionWorkflow` and derived maestro/scheduler
1566
+ // workflow IDs from each session's `AgentTempoEnsemble` search
1567
+ // attribute. Two failure modes that left orphans behind:
1568
+ // 1. Sessions started without the search attribute set (e.g. from
1569
+ // an older or partially-rebranded build) were added to
1570
+ // `sessionIds` but their ensemble name never made it into the
1571
+ // `runningEnsembles` set — and the early-return on
1572
+ // `runningEnsembles.size === 0` then bailed out without
1573
+ // terminating ANY of the buffered session IDs.
1574
+ // 2. Maestro/scheduler workflows whose sessions had already exited
1575
+ // were invisible to a session-only query.
1576
+ // Listing each type directly catches both cases.
1577
+ const collect = async (query) => {
1578
+ const ids = [];
1579
+ for await (const wf of client.workflow.list({ query })) {
1580
+ ids.push(wf.workflowId);
1581
+ }
1582
+ return ids;
1583
+ };
1584
+ const baseFilter = 'ExecutionStatus = "Running"';
1585
+ const [sessionIds, maestroIds, schedulerIds, globalMaestroIds] = await Promise.all([
1586
+ collect(`WorkflowType = "agentSessionWorkflow" AND ${baseFilter}`),
1587
+ collect(`WorkflowType = "agentMaestroWorkflow" AND ${baseFilter}`),
1588
+ collect(`WorkflowType = "agentSchedulerWorkflow" AND ${baseFilter}`),
1589
+ collect(`WorkflowType = "agentGlobalMaestroWorkflow" AND ${baseFilter}`),
1590
+ ]);
1591
+ // Ensemble names are best-effort display only — derived from
1592
+ // workflow ID prefixes when present. We terminate by ID, not by
1593
+ // ensemble, so a missing name no longer blocks cleanup.
1594
+ const ensemblesFromIds = new Set();
1595
+ for (const id of sessionIds) {
1596
+ // `agent-session-<ensemble>-<playerId>` / legacy `claude-session-<ensemble>-<playerId>`
1597
+ const m = id.match(/^(?:agent|claude)-session-(.+?)-[^-]+$/);
1598
+ if (m)
1599
+ ensemblesFromIds.add(m[1]);
1600
+ }
1601
+ for (const id of maestroIds) {
1602
+ // `agent-maestro-<ensemble>` (and `agent-maestro-global` which we exclude as global)
1603
+ const m = id.match(/^(?:agent|claude)-maestro-(.+)$/);
1604
+ if (m && m[1] !== 'global')
1605
+ ensemblesFromIds.add(m[1]);
1606
+ }
1607
+ const totalTargets = sessionIds.length + maestroIds.length + schedulerIds.length + globalMaestroIds.length;
1608
+ if (totalTargets === 0) {
1609
+ out.log(' No active workflows to destroy.');
1610
+ }
1611
+ else {
1612
+ if (!opts.yes) {
1613
+ console.log();
1614
+ if (ensemblesFromIds.size > 0) {
1615
+ out.log(' The following ensembles will be destroyed:');
1616
+ for (const name of [...ensemblesFromIds].sort()) {
1617
+ out.log(` - ${name}`);
1618
+ }
1619
+ }
1620
+ out.log(` ${sessionIds.length} session${sessionIds.length !== 1 ? 's' : ''}, ` +
1621
+ `${maestroIds.length} maestro${maestroIds.length !== 1 ? 's' : ''}, ` +
1622
+ `${schedulerIds.length} scheduler${schedulerIds.length !== 1 ? 's' : ''}` +
1623
+ (globalMaestroIds.length > 0 ? `, ${globalMaestroIds.length} global maestro` : ''));
1624
+ console.log();
1625
+ const confirmed = await typedConfirmPrompt(` This terminates every workflow (${totalTargets}) and cannot be undone.`, 'destroy');
1626
+ if (!confirmed) {
1627
+ out.log('Aborted.');
1628
+ process.exit(0);
1629
+ }
1630
+ }
1631
+ // Fan out terminations in parallel. Individual failures are
1632
+ // swallowed — closed workflows are fine, and the overall operation
1633
+ // is best-effort scorched-earth.
1634
+ const terminate = async (id) => {
1635
+ try {
1636
+ await client.workflow.getHandle(id).terminate('agent-tempo down --destroy');
1637
+ return true;
1638
+ }
1639
+ catch {
1640
+ return false;
1641
+ }
1642
+ };
1643
+ const targets = [...sessionIds, ...maestroIds, ...schedulerIds, ...globalMaestroIds];
1644
+ const results = await Promise.all(targets.map(terminate));
1645
+ const terminated = results.filter(Boolean).length;
1646
+ const ensembleCount = ensemblesFromIds.size;
1647
+ out.success(`Terminated ${terminated}/${totalTargets} workflow${terminated !== 1 ? 's' : ''}` +
1648
+ (ensembleCount > 0 ? ` across ${ensembleCount} ensemble${ensembleCount !== 1 ? 's' : ''}` : ''));
1649
+ }
1650
+ }
1651
+ finally {
1652
+ await connection.close();
1653
+ }
1654
+ }
1655
+ catch (err) {
1656
+ out.warn(`Could not terminate active workflows: ${err instanceof Error ? err.message : String(err)}`);
1657
+ }
1658
+ }
1659
+ // Step 2: Kill bridge processes via PID files
1660
+ killBridgeProcesses();
1661
+ // Step 3: Stop worker daemon unless `--keep-daemon`.
1662
+ if (opts.keepDaemon) {
1663
+ if ((0, daemon_1.isDaemonRunning)()) {
1664
+ out.log(` ${out.dim('Worker daemon left running (--keep-daemon)')}`);
1665
+ }
1666
+ }
1667
+ else if ((0, daemon_1.stopDaemon)()) {
1668
+ out.success('Worker daemon stopped');
1669
+ }
1670
+ // Step 4: Stop Temporal dev server.
1671
+ //
1672
+ // Cross-profile coexistence (ADR 0014 §5.6, #423): the dev-server is one
1673
+ // OS-wide process and `pkill`/`taskkill` cannot distinguish profile
1674
+ // ownership. Without the guard, `--dev down` kills the prod profile's
1675
+ // Temporal as collateral damage (and vice versa). `stopTemporalServer`
1676
+ // skips the kill when the OPPOSITE profile is likely active;
1677
+ // `--kill-shared-temporal` is the explicit opt-in to override.
1678
+ if (temporalUp) {
1679
+ const result = stopTemporalServer({ killSharedTemporal: opts.killSharedTemporal });
1680
+ switch (result.action) {
1681
+ case 'killed':
1682
+ out.success('Temporal server stopped');
1683
+ break;
1684
+ case 'failed':
1685
+ out.warn('Could not stop Temporal server (may need to stop it manually)');
1686
+ break;
1687
+ case 'skipped-cross-profile': {
1688
+ const otherProfile = (0, config_1.isDevMode)() ? 'prod' : 'dev';
1689
+ out.warn(`Temporal server kept running — the ${otherProfile} profile appears active. ` +
1690
+ `Pass --kill-shared-temporal to override.`);
1691
+ break;
1692
+ }
1693
+ }
1694
+ }
1695
+ else {
1696
+ out.log(` ${out.dim('Temporal not running')}`);
1697
+ }
1698
+ // Step 4: Check for npx usage, then remove MCP config
1699
+ // npx check must happen BEFORE removal since step 4 deletes the entry
1700
+ let hasNpxWarning = false;
1701
+ const projectMcpPath = (0, path_1.join)(opts.dir, '.mcp.json');
1702
+ if ((0, fs_1.existsSync)(projectMcpPath)) {
1703
+ try {
1704
+ const mcpContent = JSON.parse((0, fs_1.readFileSync)(projectMcpPath, 'utf8'));
1705
+ // Backward-compat: check both the new (`agent-tempo`) and legacy (`agent-tempo`) keys.
1706
+ const tempoEntry = mcpContent?.mcpServers?.['agent-tempo'] ?? mcpContent?.mcpServers?.['agent-tempo'];
1707
+ if (tempoEntry) {
1708
+ const cmd = tempoEntry.command ?? '';
1709
+ const entryArgs = tempoEntry.args ?? [];
1710
+ if (cmd === 'npx' || entryArgs.some((a) => a === 'npx')) {
1711
+ hasNpxWarning = true;
1712
+ }
1713
+ }
1714
+ }
1715
+ catch {
1716
+ // Corrupt .mcp.json — ignore
1717
+ }
1718
+ }
1719
+ if (opts.removeMcp) {
1720
+ // Remove global registration
1721
+ if ((0, mcp_1.isGlobalMcpRegistered)()) {
1722
+ if ((0, mcp_1.removeGlobalMcp)()) {
1723
+ out.success('Removed agent-tempo from global MCP config');
1724
+ }
1725
+ else {
1726
+ out.warn('Could not remove global MCP entry');
1727
+ }
1728
+ }
1729
+ // Also remove project-level .mcp.json entry if present.
1730
+ // Backward-compat: clean up either the new (`agent-tempo`) or legacy (`agent-tempo`) key.
1731
+ if ((0, fs_1.existsSync)(projectMcpPath)) {
1732
+ try {
1733
+ const existing = JSON.parse((0, fs_1.readFileSync)(projectMcpPath, 'utf8'));
1734
+ const removedAny = (existing?.mcpServers?.['agent-tempo'] && (delete existing.mcpServers['agent-tempo'])) ||
1735
+ (existing?.mcpServers?.['agent-tempo'] && (delete existing.mcpServers['agent-tempo']));
1736
+ if (removedAny) {
1737
+ if (Object.keys(existing.mcpServers).length === 0) {
1738
+ (0, fs_1.unlinkSync)(projectMcpPath);
1739
+ out.success('Removed .mcp.json (no other servers configured)');
1740
+ }
1741
+ else {
1742
+ (0, fs_1.writeFileSync)(projectMcpPath, JSON.stringify(existing, null, 2) + '\n');
1743
+ out.success('Removed agent-tempo from .mcp.json');
1744
+ }
1745
+ }
1746
+ }
1747
+ catch {
1748
+ out.warn(`Could not update ${projectMcpPath}`);
1749
+ }
1750
+ }
1751
+ }
1752
+ if (hasNpxWarning) {
1753
+ console.log();
1754
+ out.warn('Your .mcp.json uses npx which may cache stale versions.');
1755
+ out.log(` ${out.dim('Consider removing it — user-level registration is preferred.')}`);
1756
+ out.log(` ${out.dim('Run: agent-tempo init')}`);
1757
+ }
1758
+ console.log();
1759
+ out.success('agent-tempo is shut down');
1760
+ out.log(` ${out.dim('Temporal data preserved in ~/.agent-tempo/ (delete manually to reset)')}`);
1761
+ console.log();
1762
+ }
1763
+ /**
1764
+ * Read PID info for a copilot bridge session from its PID file.
1765
+ * Returns a formatted string like " (pid 12345)" or "" if no PID file found.
1766
+ */
1767
+ function getBridgePidInfo(name) {
1768
+ const pidPath = (0, path_1.join)(process.cwd(), 'logs', `${name}.pid`);
1769
+ if (!(0, fs_1.existsSync)(pidPath))
1770
+ return '';
1771
+ try {
1772
+ const pid = parseInt((0, fs_1.readFileSync)(pidPath, 'utf8').trim(), 10);
1773
+ if (isNaN(pid))
1774
+ return '';
1775
+ // Check if process is still alive
1776
+ try {
1777
+ process.kill(pid, 0); // signal 0 = existence check, doesn't kill
1778
+ return out.dim(` (pid ${pid})`);
1779
+ }
1780
+ catch {
1781
+ return out.dim(` (pid ${pid}, dead)`);
1782
+ }
1783
+ }
1784
+ catch {
1785
+ return '';
1786
+ }
1787
+ }
1788
+ /**
1789
+ * Kill all bridge processes found in logs/*.pid and clean up PID files.
1790
+ */
1791
+ function killBridgeProcesses() {
1792
+ const logsDir = (0, path_1.join)(process.cwd(), 'logs');
1793
+ if (!(0, fs_1.existsSync)(logsDir))
1794
+ return;
1795
+ try {
1796
+ const pidFiles = (0, fs_1.readdirSync)(logsDir).filter(f => f.endsWith('.pid'));
1797
+ for (const pidFile of pidFiles) {
1798
+ const pidPath = (0, path_1.join)(logsDir, pidFile);
1799
+ try {
1800
+ const pid = parseInt((0, fs_1.readFileSync)(pidPath, 'utf8').trim(), 10);
1801
+ if (!isNaN(pid)) {
1802
+ try {
1803
+ process.kill(pid);
1804
+ out.log(` ${out.dim(`Killed bridge process ${pidFile.replace('.pid', '')} (pid ${pid})`)}`);
1805
+ }
1806
+ catch {
1807
+ // already dead
1808
+ }
1809
+ }
1810
+ (0, fs_1.unlinkSync)(pidPath);
1811
+ }
1812
+ catch {
1813
+ // unreadable — skip
1814
+ }
1815
+ }
1816
+ }
1817
+ catch {
1818
+ // logs dir unreadable
1819
+ }
1820
+ }
1821
+ async function agentTypesCommand(opts) {
1822
+ switch (opts.subcommand) {
1823
+ case 'list': {
1824
+ const types = (0, agent_types_1.listAgentTypes)();
1825
+ if (types.length === 0) {
1826
+ out.log('No agent types found.');
1827
+ out.log(` Run ${out.dim('agent-tempo agent-types init')} to install shipped examples.`);
1828
+ return;
1829
+ }
1830
+ out.heading('Available agent types');
1831
+ for (const t of types) {
1832
+ const src = t.source === 'shipped' ? out.dim('(shipped)') : t.source === 'user' ? out.dim('(user)') : out.dim('(project)');
1833
+ out.log(` ${out.bold(t.name)} ${src}`);
1834
+ if (t.description)
1835
+ out.log(` ${t.description}`);
1836
+ }
1837
+ console.log();
1838
+ break;
1839
+ }
1840
+ case 'show': {
1841
+ if (!opts.name) {
1842
+ out.error('Usage: agent-tempo agent-types show <name>');
1843
+ process.exit(1);
1844
+ }
1845
+ const info = (0, agent_types_1.resolveAgentType)(opts.name);
1846
+ if (!info) {
1847
+ out.error(`No agent type found named "${opts.name}"`);
1848
+ out.log(` Run ${out.dim('agent-tempo agent-types list')} to see available types.`);
1849
+ process.exit(1);
1850
+ }
1851
+ out.log(`${out.bold(info.name)} ${out.dim(`(${info.source}: ${info.path})`)}\n`);
1852
+ console.log((0, fs_1.readFileSync)(info.path, 'utf8'));
1853
+ break;
1854
+ }
1855
+ case 'init': {
1856
+ const shippedDir = (0, path_1.join)(PACKAGE_ROOT, 'examples', 'agents');
1857
+ const targetDir = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'agents');
1858
+ (0, fs_1.mkdirSync)(targetDir, { recursive: true });
1859
+ if (!(0, fs_1.existsSync)(shippedDir)) {
1860
+ out.error(`Shipped examples not found at ${shippedDir}`);
1861
+ process.exit(1);
1862
+ }
1863
+ const files = (0, fs_1.readdirSync)(shippedDir).filter(f => f.endsWith('.md'));
1864
+ let copied = 0;
1865
+ let skipped = 0;
1866
+ for (const file of files) {
1867
+ const target = (0, path_1.join)(targetDir, file);
1868
+ if ((0, fs_1.existsSync)(target)) {
1869
+ out.log(` ${out.dim('skip')} ${file} (already exists)`);
1870
+ skipped++;
1871
+ }
1872
+ else {
1873
+ (0, fs_1.copyFileSync)((0, path_1.join)(shippedDir, file), target);
1874
+ out.success(`${file} → ${target}`);
1875
+ copied++;
1876
+ }
1877
+ }
1878
+ console.log();
1879
+ out.log(`Copied ${copied} agent definitions to ${targetDir}${skipped ? ` (${skipped} skipped)` : ''}`);
1880
+ break;
1881
+ }
1882
+ default:
1883
+ out.error('Usage: agent-tempo agent-types <list|show|init> [name]');
1884
+ out.log(`\n ${out.dim('agent-tempo agent-types list')} List available agent types`);
1885
+ out.log(` ${out.dim('agent-tempo agent-types show <name>')} Display an agent definition`);
1886
+ out.log(` ${out.dim('agent-tempo agent-types init')} Copy shipped examples to ~/.claude/agents/`);
1887
+ process.exit(1);
1888
+ }
1889
+ }
1890
+ async function broadcast(opts) {
1891
+ const config = (0, config_1.getConfig)(opts);
1892
+ let connection;
1893
+ try {
1894
+ connection = await Promise.race([
1895
+ (0, connection_1.createTemporalConnection)(config),
1896
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
1897
+ ]);
1898
+ }
1899
+ catch {
1900
+ out.error(`Cannot connect to Temporal at ${config.temporalAddress}`);
1901
+ process.exit(1);
1902
+ return;
1903
+ }
1904
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
1905
+ const ensemble = opts.ensemble || config.ensemble;
1906
+ const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running"`;
1907
+ const targets = [];
1908
+ for await (const wf of client.workflow.list({ query })) {
1909
+ try {
1910
+ const handle = client.workflow.getHandle(wf.workflowId);
1911
+ const metadata = await handle.query('getMetadata');
1912
+ if (metadata.ensemble !== ensemble)
1913
+ continue;
1914
+ // Filter by attachment phase (post-#176). Phase lives on the
1915
+ // `AgentTempoAttachmentState` search attribute.
1916
+ const phase = (0, search_attributes_1.getAttachmentPhase)(wf);
1917
+ if (!(0, validation_1.shouldIncludeInBroadcast)(phase, !!opts.includeStale))
1918
+ continue;
1919
+ // Filter by player type if specified
1920
+ if (opts.type && metadata.playerType !== opts.type)
1921
+ continue;
1922
+ targets.push({ playerId: metadata.playerId, workflowId: wf.workflowId });
1923
+ }
1924
+ catch {
1925
+ // Workflow may have just completed — skip it
1926
+ }
1927
+ }
1928
+ if (targets.length === 0) {
1929
+ out.warn('No active players matched the broadcast filter.');
1930
+ await connection.close();
1931
+ return;
1932
+ }
1933
+ // Signal each target directly (CLI bypasses outbox)
1934
+ let sent = 0;
1935
+ for (const target of targets) {
1936
+ try {
1937
+ const handle = client.workflow.getHandle(target.workflowId);
1938
+ await handle.signal('receiveMessage', {
1939
+ from: 'cli',
1940
+ text: opts.message,
1941
+ responseRequested: false,
1942
+ });
1943
+ sent++;
1944
+ out.log(` ${out.green('✓')} ${target.playerId}`);
1945
+ }
1946
+ catch (err) {
1947
+ out.warn(` ${target.playerId}: ${err instanceof Error ? err.message : String(err)}`);
1948
+ }
1949
+ }
1950
+ out.success(`Broadcast sent to ${sent}/${targets.length} player${targets.length === 1 ? '' : 's'}`);
1951
+ await connection.close();
1952
+ }
1953
+ /**
1954
+ * Shared connection + client helper for verb commands. Exported so the
1955
+ * dev-mode verb dispatcher (`./dev-verbs.ts`) can use the same connection
1956
+ * idiom — single source of truth for the 3-second timeout + error-exit
1957
+ * behavior across all CLI verbs.
1958
+ */
1959
+ async function verbClient(opts) {
1960
+ const config = (0, config_1.getConfig)(opts);
1961
+ let connection;
1962
+ try {
1963
+ connection = await Promise.race([
1964
+ (0, connection_1.createTemporalConnection)(config),
1965
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
1966
+ ]);
1967
+ }
1968
+ catch {
1969
+ out.error(`Cannot connect to Temporal at ${config.temporalAddress}`);
1970
+ process.exit(1);
1971
+ }
1972
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
1973
+ return { config, connection, client };
1974
+ }
1975
+ /**
1976
+ * `agent-tempo destroy <ensemble> [-y]` — terminate every workflow in an
1977
+ * ensemble (#288). Prompts with the ensemble name and workflow count unless
1978
+ * `-y` is passed. The per-player destroy path lives in the TUI (`/destroy
1979
+ * --player`).
1980
+ */
1981
+ async function destroy(opts) {
1982
+ const { config, connection, client } = await verbClient(opts);
1983
+ try {
1984
+ const handles = [];
1985
+ const sessionQuery = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${opts.ensemble}"`;
1986
+ for await (const wf of client.workflow.list({ query: sessionQuery })) {
1987
+ handles.push({ id: wf.workflowId, label: 'session' });
1988
+ }
1989
+ const probe = async (id, label) => {
1990
+ try {
1991
+ const desc = await client.workflow.getHandle(id).describe();
1992
+ return desc.status.name === 'RUNNING' ? { id, label } : null;
1993
+ }
1994
+ catch {
1995
+ return null;
1996
+ }
1997
+ };
1998
+ const sidecars = await Promise.all([
1999
+ probe((0, config_1.schedulerWorkflowId)(opts.ensemble), 'scheduler'),
2000
+ probe((0, config_1.maestroWorkflowId)(opts.ensemble), 'maestro'),
2001
+ ]);
2002
+ for (const s of sidecars)
2003
+ if (s)
2004
+ handles.push(s);
2005
+ if (handles.length === 0) {
2006
+ out.log(`No active workflows in ensemble "${opts.ensemble}".`);
2007
+ return;
2008
+ }
2009
+ if (!opts.yes) {
2010
+ out.heading(`Destroy ensemble "${opts.ensemble}"`);
2011
+ for (const h of handles) {
2012
+ out.log(` ${out.dim('-')} ${h.label}: ${h.id}`);
2013
+ }
2014
+ console.log();
2015
+ const confirmed = await typedConfirmPrompt(` This terminates ${handles.length} workflow${handles.length !== 1 ? 's' : ''} and cannot be undone.`, 'destroy');
2016
+ if (!confirmed) {
2017
+ out.log('Aborted.');
2018
+ process.exit(0);
2019
+ }
2020
+ }
2021
+ const results = await Promise.all(handles.map(async (h) => {
2022
+ try {
2023
+ await client.workflow.getHandle(h.id).terminate(`agent-tempo destroy ${opts.ensemble}`);
2024
+ return true;
2025
+ }
2026
+ catch {
2027
+ return false;
2028
+ }
2029
+ }));
2030
+ const terminated = results.filter(Boolean).length;
2031
+ out.success(`Terminated ${terminated} workflow${terminated !== 1 ? 's' : ''} in "${opts.ensemble}".`);
2032
+ }
2033
+ catch (err) {
2034
+ out.error(err?.message || String(err));
2035
+ process.exit(1);
2036
+ }
2037
+ finally {
2038
+ await connection.close();
2039
+ }
2040
+ }
2041
+ async function attachmentInfo(opts) {
2042
+ const { config, connection, client } = await verbClient(opts);
2043
+ const ensemble = opts.ensemble || config.ensemble;
2044
+ try {
2045
+ const tempo = (0, client_2.createTempoClient)(client);
2046
+ const info = await tempo.attachmentInfo(ensemble, opts.name);
2047
+ // #264 carved the per-surface formatter out to src/utils/attachment-format.ts
2048
+ // so the TUI's /attachment-info renders identical output including heartbeat
2049
+ // age. The CLI is now a pure consumer; if you need to add a field, add it
2050
+ // to the shared formatter so every surface picks it up at once.
2051
+ for (const line of (0, attachment_format_1.formatAttachmentInfoForDisplay)(opts.name, info)) {
2052
+ out.log(line);
2053
+ }
2054
+ }
2055
+ catch (err) {
2056
+ out.error(err?.message || String(err));
2057
+ process.exit(1);
2058
+ }
2059
+ finally {
2060
+ await connection.close();
2061
+ }
2062
+ }
2063
+ async function hosts(opts) {
2064
+ const { config, connection, client } = await verbClient(opts);
2065
+ try {
2066
+ const { listHosts } = await Promise.resolve().then(() => __importStar(require('../utils/hosts')));
2067
+ const { formatHostList } = await Promise.resolve().then(() => __importStar(require('../utils/format-hosts')));
2068
+ const list = await listHosts(client, {
2069
+ force: true, // CLI always bypasses the cache — freshness expectation is "right now".
2070
+ namespace: config.temporalNamespace,
2071
+ taskQueue: config.taskQueue,
2072
+ });
2073
+ if (opts.json) {
2074
+ process.stdout.write(JSON.stringify(list, null, 2) + '\n');
2075
+ }
2076
+ else {
2077
+ out.log(formatHostList(list, { includeStale: opts.all }));
2078
+ }
2079
+ }
2080
+ catch (err) {
2081
+ out.error(err?.message || String(err));
2082
+ process.exit(1);
2083
+ }
2084
+ finally {
2085
+ await connection.close();
2086
+ }
2087
+ }
2088
+ /**
2089
+ * #274 AC5d (M12) — manual re-signal of this host's profile to the
2090
+ * global maestro. The daemon otherwise only re-signals on boot; this
2091
+ * subcommand re-computes the profile + signals fresh.
2092
+ *
2093
+ * Exit semantics (per my implementation-time call to the conductor,
2094
+ * approved): await ensureGlobalMaestro → signal → short poll on
2095
+ * `hostProfiles()` to confirm the new version is visible → exit 0.
2096
+ * Exits nonzero if the poll timeout elapses without confirmation.
2097
+ */
2098
+ async function refreshHostProfile(opts) {
2099
+ const { config, connection, client } = await verbClient(opts);
2100
+ try {
2101
+ const { computeHostProfile, scrubHostProfile, advertiseHostProfile } = await Promise.resolve().then(() => __importStar(require('../daemon')));
2102
+ const { GLOBAL_MAESTRO_WORKFLOW_ID } = await Promise.resolve().then(() => __importStar(require('../config')));
2103
+ const profile = scrubHostProfile(computeHostProfile(config));
2104
+ const result = await advertiseHostProfile(client, profile, { log: (...a) => out.log(a.map(String).join(' ')) });
2105
+ if (!result.ok) {
2106
+ out.error(`hostProfile signal failed after ${result.attempts} attempts. Global Maestro may be unreachable.`);
2107
+ process.exit(1);
2108
+ }
2109
+ // Short confirmation poll — give the workflow a moment to apply the
2110
+ // signal and respond to the query with the fresh version. If the
2111
+ // maestro is absent entirely, the query will throw and we exit 1.
2112
+ const deadline = Date.now() + (opts.confirmTimeoutMs ?? 10_000);
2113
+ const target = profile.version;
2114
+ const handle = client.workflow.getHandle(GLOBAL_MAESTRO_WORKFLOW_ID);
2115
+ while (Date.now() < deadline) {
2116
+ try {
2117
+ const profiles = (await handle.query('hostProfiles'));
2118
+ const live = profiles[profile.hostname];
2119
+ if (live && live.version === target) {
2120
+ out.success(`Host profile for "${profile.hostname}" refreshed (version ${target}).`);
2121
+ return;
2122
+ }
2123
+ }
2124
+ catch {
2125
+ // retry until deadline
2126
+ }
2127
+ await new Promise((r) => setTimeout(r, 500));
2128
+ }
2129
+ out.error(`Signal sent but not yet reflected in hostProfiles() query after ${opts.confirmTimeoutMs ?? 10_000}ms. May succeed shortly; re-run to confirm.`);
2130
+ process.exit(1);
2131
+ }
2132
+ catch (err) {
2133
+ out.error(err?.message || String(err));
2134
+ process.exit(1);
2135
+ }
2136
+ finally {
2137
+ await connection.close();
2138
+ }
2139
+ }
2140
+ async function recall(opts) {
2141
+ const { config, connection, client } = await verbClient(opts);
2142
+ const ensemble = opts.ensemble || config.ensemble;
2143
+ try {
2144
+ const tempo = (0, client_2.createTempoClient)(client);
2145
+ const { received, sent } = await tempo.recall(ensemble, opts.name);
2146
+ const timeline = (0, recall_format_1.buildTimeline)(received, sent, Boolean(opts.includeSent));
2147
+ const rendered = (0, recall_format_1.formatRecall)(timeline, {
2148
+ limit: opts.limit,
2149
+ offset: opts.offset,
2150
+ previewLength: opts.previewLength,
2151
+ since: opts.since,
2152
+ from: opts.from,
2153
+ });
2154
+ if (opts.json) {
2155
+ // Machine-readable. Includes the rendered text too so callers can
2156
+ // either re-render or pass through. Pagination state is explicit so
2157
+ // shell pipelines don't have to parse the header line.
2158
+ process.stdout.write(JSON.stringify({
2159
+ player: opts.name,
2160
+ ensemble,
2161
+ received,
2162
+ sent: opts.includeSent ? sent : [],
2163
+ total: rendered.total,
2164
+ shown: rendered.shown,
2165
+ hasMore: rendered.hasMore,
2166
+ text: rendered.text,
2167
+ }, null, 2) + '\n');
2168
+ }
2169
+ else {
2170
+ out.log(rendered.text);
2171
+ }
2172
+ }
2173
+ catch (err) {
2174
+ out.error(err?.message || String(err));
2175
+ process.exit(1);
2176
+ }
2177
+ finally {
2178
+ await connection.close();
2179
+ }
2180
+ }
2181
+ /**
2182
+ * `agent-tempo restore <ensemble>` — delegate to {@link TempoClient.restore},
2183
+ * which reattaches orphans AND unpauses maestro + scheduler (#298 — the
2184
+ * direct-to-`restoreOrphansOnce` path left the ensemble paused after a
2185
+ * `shutdown → restore` roundtrip). The TUI home view (#290) is the picker
2186
+ * surface; the CLI is the scriptable bulk operation, one ensemble at a time.
2187
+ *
2188
+ * `--all-hosts` (#151): switches to cluster-view readonly listing — the
2189
+ * positional `<ensemble>` becomes optional (when set, narrows the listing;
2190
+ * when omitted, lists across every ensemble). Never enqueues a restart.
2191
+ * See `formatCrossHostOrphans` for the output shape.
2192
+ */
2193
+ async function restore(opts) {
2194
+ if (opts.allHosts) {
2195
+ await restoreAllHosts(opts);
2196
+ return;
2197
+ }
2198
+ if (!opts.ensemble) {
2199
+ out.error('Usage: agent-tempo restore <ensemble> (or --all-hosts for cluster-view)');
2200
+ process.exit(1);
2201
+ }
2202
+ const { connection, client } = await verbClient(opts);
2203
+ try {
2204
+ const { formatRestoreOutcome } = await Promise.resolve().then(() => __importStar(require('../reconcile/orphans')));
2205
+ const tempo = (0, client_2.createTempoClient)(client);
2206
+ const summary = await tempo.restore(opts.ensemble);
2207
+ if (summary.details.length === 0) {
2208
+ out.log(`No orphans in ensemble "${opts.ensemble}" on this host.`);
2209
+ return;
2210
+ }
2211
+ out.heading(`Restored orphans in "${opts.ensemble}"`);
2212
+ for (const d of summary.details) {
2213
+ const text = `${d.playerId} — ${formatRestoreOutcome(d.outcome)}`;
2214
+ switch (d.outcome.kind) {
2215
+ case 'queued':
2216
+ out.success(text);
2217
+ break;
2218
+ case 'failed':
2219
+ out.warn(text);
2220
+ break;
2221
+ case 'skipped':
2222
+ out.log(` ${out.dim(text)}`);
2223
+ break;
2224
+ }
2225
+ }
2226
+ out.log(`\n${summary.reattached} reattached, ${summary.skipped} skipped, ${summary.failed} failed.`);
2227
+ }
2228
+ catch (err) {
2229
+ out.error(err?.message || String(err));
2230
+ process.exit(1);
2231
+ }
2232
+ finally {
2233
+ await connection.close();
2234
+ }
2235
+ }
2236
+ /**
2237
+ * #151 — `agent-tempo restore --all-hosts` implementation.
2238
+ *
2239
+ * Read-only cluster-view: enumerates every orphan in the namespace (not
2240
+ * just local) and groups by `preferredHost`. Each group is annotated with
2241
+ * a liveness label (`[live]` / `[stale]` / `[missing]`) derived from
2242
+ * `listHosts()`, and each orphan line includes the TUI `/migrate`
2243
+ * command the operator would run to deliberately steal the session to
2244
+ * the local host.
2245
+ *
2246
+ * Never enqueues a restart. The architect's spec (#151 refined Option D)
2247
+ * is explicit: recovery happens reflexively when the remote daemon comes
2248
+ * back (its own `reconcileOnBoot` picks up matching orphans), or by
2249
+ * deliberate operator action via `/migrate`. A timer-based reclaim is
2250
+ * disallowed (PR-F §3 Site 3) — a clock cannot distinguish "host
2251
+ * decommissioned" from "weekend offline."
2252
+ *
2253
+ * Output format mirrors the existing `hosts` formatter's section style
2254
+ * (per-host groupings, dimmed annotations). Liveness label semantics:
2255
+ *
2256
+ * [live] — host's daemon is polling now (`HOST_FRESHNESS_THRESHOLD_MS`,
2257
+ * 60s). Recovery is imminent on its next reconcile tick.
2258
+ * [stale] — host has a profile but no poller seen in the last minute.
2259
+ * Probably down; manual `/migrate` if recovery can't wait.
2260
+ * [missing] — host has no registered profile (never came back since boot,
2261
+ * or the maestro restarted and the profile expired). Almost
2262
+ * certainly safe to steal.
2263
+ */
2264
+ async function restoreAllHosts(opts) {
2265
+ const { connection, client } = await verbClient(opts);
2266
+ try {
2267
+ const localHost = (0, os_1.hostname)();
2268
+ const { restoreOrphansOnce } = await Promise.resolve().then(() => __importStar(require('../reconcile/orphans')));
2269
+ const { listHosts } = await Promise.resolve().then(() => __importStar(require('../utils/hosts')));
2270
+ const { formatCrossHostOrphans } = await Promise.resolve().then(() => __importStar(require('../utils/restore-format')));
2271
+ // Run the cluster-view query and the host enumeration concurrently —
2272
+ // the join is cheap and both calls take 50-200ms apiece against a
2273
+ // healthy Temporal.
2274
+ const [summary, hosts] = await Promise.all([
2275
+ restoreOrphansOnce(client, {
2276
+ hostname: localHost,
2277
+ invokerPlayerId: 'cli',
2278
+ policy: 'auto',
2279
+ mode: 'all-hosts-readonly',
2280
+ ...(opts.ensemble ? { ensemble: opts.ensemble } : {}),
2281
+ }),
2282
+ listHosts(client, { force: true }),
2283
+ ]);
2284
+ const output = formatCrossHostOrphans(summary.details, hosts, {
2285
+ localHost,
2286
+ ...(opts.ensemble ? { ensemble: opts.ensemble } : {}),
2287
+ });
2288
+ out.log(output);
2289
+ }
2290
+ catch (err) {
2291
+ out.error(err?.message || String(err));
2292
+ process.exit(1);
2293
+ }
2294
+ finally {
2295
+ await connection.close();
2296
+ }
2297
+ }
2298
+ async function ensembleCommand(opts) {
2299
+ switch (opts.subcommand) {
2300
+ case 'save': {
2301
+ const config = (0, config_1.getConfig)(opts);
2302
+ const connection = await (0, connection_1.createTemporalConnection)(config);
2303
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
2304
+ const ensemble = opts.name || config.ensemble;
2305
+ try {
2306
+ const path = await (0, saver_1.saveLineup)(client, ensemble);
2307
+ out.success(`Saved ensemble "${ensemble}" to ${path}`);
2308
+ }
2309
+ finally {
2310
+ await connection.close();
2311
+ }
2312
+ break;
2313
+ }
2314
+ case 'list': {
2315
+ const lineups = (0, saver_1.listLineups)();
2316
+ if (lineups.length === 0) {
2317
+ out.log('No saved ensembles. Use `agent-tempo ensemble save [name]` to save one.');
2318
+ return;
2319
+ }
2320
+ out.heading('Saved ensembles');
2321
+ for (const bp of lineups) {
2322
+ out.log(` ${out.bold(bp.name)} ${out.dim(bp.path)}`);
2323
+ }
2324
+ console.log();
2325
+ break;
2326
+ }
2327
+ case 'show': {
2328
+ if (!opts.name) {
2329
+ out.error('Usage: agent-tempo ensemble show <name>');
2330
+ process.exit(1);
2331
+ }
2332
+ const content = (0, saver_1.readSavedLineup)(opts.name);
2333
+ if (!content) {
2334
+ out.error(`No saved ensemble named "${opts.name}"`);
2335
+ out.log(` Run ${out.dim('agent-tempo ensemble list')} to see available ensembles.`);
2336
+ process.exit(1);
2337
+ }
2338
+ console.log(content);
2339
+ break;
2340
+ }
2341
+ default:
2342
+ out.error('Usage: agent-tempo ensemble <save|list|show> [name]');
2343
+ out.log(`\n ${out.dim('agent-tempo ensemble save [name]')} Save current ensemble state`);
2344
+ out.log(` ${out.dim('agent-tempo ensemble list')} List saved ensembles`);
2345
+ out.log(` ${out.dim('agent-tempo ensemble show <name>')} Display a saved lineup`);
2346
+ process.exit(1);
2347
+ }
2348
+ }
2349
+ /** Release all held sessions in an ensemble (unlock outbox, deliver initial messages). */
2350
+ async function release(opts) {
2351
+ const config = (0, config_1.getConfig)(opts);
2352
+ let connection;
2353
+ try {
2354
+ connection = await Promise.race([
2355
+ (0, connection_1.createTemporalConnection)(config),
2356
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
2357
+ ]);
2358
+ }
2359
+ catch {
2360
+ out.error(`Cannot connect to Temporal at ${config.temporalAddress}`);
2361
+ process.exit(1);
2362
+ return;
2363
+ }
2364
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
2365
+ const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${opts.ensemble.replace(/["\\\n\r]/g, '')}"`;
2366
+ let released = 0;
2367
+ for await (const wf of client.workflow.list({ query })) {
2368
+ try {
2369
+ const handle = client.workflow.getHandle(wf.workflowId);
2370
+ const locked = await handle.query(signals_1.outboxLockedQuery);
2371
+ if (locked) {
2372
+ await handle.signal(signals_1.releaseHeldSignal);
2373
+ released++;
2374
+ const sa = wf.searchAttributes || {};
2375
+ const playerId = Array.isArray(sa.AgentTempoPlayerId) ? String(sa.AgentTempoPlayerId[0]) : wf.workflowId;
2376
+ out.log(` ${out.dim('released')} ${playerId}`);
2377
+ }
2378
+ }
2379
+ catch {
2380
+ // Skip failed queries (terminated workflows, etc.)
2381
+ }
2382
+ }
2383
+ if (released > 0) {
2384
+ out.success(`Released ${released} player${released !== 1 ? 's' : ''}`);
2385
+ }
2386
+ else {
2387
+ out.log('No held players found.');
2388
+ }
2389
+ await connection.close();
2390
+ }
2391
+ /**
2392
+ * Fan out the paused/unpaused state to every component of an ensemble —
2393
+ * maestro hub, scheduler, and each session. Shared by the TUI `/pause` +
2394
+ * `/play` surface and by the internal initial-startup hold in
2395
+ * {@link applyLineupPlayersAndSchedules}.
2396
+ */
2397
+ async function setPausedState(client, ensemble, paused) {
2398
+ // 1. Signal maestro hub
2399
+ try {
2400
+ const mh = client.workflow.getHandle((0, config_1.maestroWorkflowId)(ensemble));
2401
+ await mh.signal(maestro_signals_1.maestroSetPausedSignal, paused);
2402
+ }
2403
+ catch {
2404
+ // Maestro may not be running — non-critical
2405
+ }
2406
+ // 2. Signal all active sessions
2407
+ const sanitized = ensemble.replace(/["\\\n\r]/g, '');
2408
+ const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitized}"`;
2409
+ let count = 0;
2410
+ for await (const wf of client.workflow.list({ query })) {
2411
+ try {
2412
+ const handle = client.workflow.getHandle(wf.workflowId);
2413
+ await handle.signal(signals_1.setPausedSignal, paused);
2414
+ count++;
2415
+ }
2416
+ catch {
2417
+ // Skip failed signals
2418
+ }
2419
+ }
2420
+ out.log(` ${out.dim(paused ? 'paused' : 'resumed')} ${count} session${count !== 1 ? 's' : ''}`);
2421
+ // 3. Signal scheduler
2422
+ try {
2423
+ const sh = client.workflow.getHandle((0, config_1.schedulerWorkflowId)(ensemble));
2424
+ await sh.signal(scheduler_signals_1.setSchedulerPausedSignal, paused);
2425
+ out.log(` ${out.dim(paused ? 'paused' : 'resumed')} scheduler`);
2426
+ }
2427
+ catch {
2428
+ // Scheduler may not be running — non-critical
2429
+ }
2430
+ }
2431
+ // `help()`, `version()`, and `upgrade()` moved out of commands.ts (#157 PR C)
2432
+ // to keep them crash-proof under a broken Temporal SDK install:
2433
+ // - help → src/cli/help-text.ts (dedicated minimal module)
2434
+ // - version → inlined in src/cli.ts (package.json read only)
2435
+ // - upgrade → src/cli/upgrade-command.ts (dynamic Temporal imports)
2436
+ // All three are routed directly from `src/cli.ts` before `./cli/commands`
2437
+ // is dynamic-imported, so they remain available as recovery levers.
2438
+ // See src/cli/upgrade-command.ts for the upgrade handler implementation.