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,1260 @@
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.createTempoClientCore = createTempoClientCore;
37
+ /**
38
+ * `TempoClientCore` — pure-RPC factory implementation.
39
+ *
40
+ * Every method here goes through the Temporal `Client`; none shell out to
41
+ * a local terminal. Safe to instantiate from the daemon, MCP server,
42
+ * future SSE event source, and any external SDK consumer that wants a
43
+ * headless surface (no `child_process` dependency).
44
+ *
45
+ * The two spawn methods (`createEnsemble`, `spawnConductor`) and the
46
+ * `runTempoCli` helper live in `./with-spawn.ts`, which composes this
47
+ * factory and adds the TTY-bound surface.
48
+ *
49
+ * See `docs/adr/0007-tempoclient-core-withspawn-split.md` and
50
+ * `docs/design/tempoclient-core-spawn-split.md`.
51
+ */
52
+ const os_1 = require("os");
53
+ const client_1 = require("@temporalio/client");
54
+ const config_1 = require("../config");
55
+ const signals_1 = require("../workflows/signals");
56
+ const maestro_signals_1 = require("../workflows/maestro-signals");
57
+ const resolve_1 = require("../activities/resolve");
58
+ const orphans_1 = require("../reconcile/orphans");
59
+ const query_timeout_1 = require("../utils/query-timeout");
60
+ const visibility_deadline_1 = require("../utils/visibility-deadline");
61
+ const ensemble_ops_1 = require("../utils/ensemble-ops");
62
+ const search_attributes_1 = require("../utils/search-attributes");
63
+ const subscribe_1 = require("./subscribe");
64
+ // ── Helpers (module-private; shared with `with-spawn.ts` if needed via re-export) ──
65
+ /** Escape a value for use in Temporal visibility query strings.
66
+ * Strips characters that could break or inject into the query. */
67
+ function sanitizeQueryValue(value) {
68
+ return value.replace(/["\\\n\r]/g, '');
69
+ }
70
+ /** Shared unknown-error → string helper for summary `error` fields. */
71
+ function errMsg(err) {
72
+ return err instanceof Error ? err.message : String(err);
73
+ }
74
+ // ── Factory ──
75
+ /**
76
+ * Build a `TempoClientCore` over a configured Temporal `Client`. Headless
77
+ * callers (daemon, MCP tools, SSE event source) use this directly; TTY
78
+ * callers go through {@link createTempoClientWithSpawn} from
79
+ * `./with-spawn.ts`.
80
+ *
81
+ * `opts.subscribeDeps` is forwarded to the SSE subscribe wrapper so
82
+ * tests/non-default environments can override `baseUrl`, `token`,
83
+ * `fetchImpl`, or `sleep` without monkey-patching globals.
84
+ */
85
+ function createTempoClientCore(client, opts = {}) {
86
+ const globalMaestroId = config_1.GLOBAL_MAESTRO_WORKFLOW_ID;
87
+ const subscribe = (0, subscribe_1.createSubscribe)(opts.subscribeDeps);
88
+ // Closed over by `listHosts` below — see #437. Daemon HTTP/MCP/aggregate
89
+ // construction sites must pass `taskQueue: config.taskQueue` for dev-mode
90
+ // host discovery to find the right pollers.
91
+ const taskQueue = opts.taskQueue;
92
+ /** Helper: get a workflow handle by ID. */
93
+ function handle(workflowId) {
94
+ return client.workflow.getHandle(workflowId);
95
+ }
96
+ /**
97
+ * Shared between `listEnsembles()` and `listEnsemblesBounded()` (#336/#529).
98
+ * Given the per-ensemble aggregation map produced by the visibility-list
99
+ * loop, fans out the per-ensemble maestro `maestroPaused` query and
100
+ * builds the final `EnsembleSummary[]`.
101
+ *
102
+ * Kept as a closure so it inherits `handle` from the factory scope (and
103
+ * the `queryHandleWithTimeout` + `maestroPausedQuery` bindings from
104
+ * module scope) without threading them through as args.
105
+ */
106
+ async function finishEnsembleSummaries(byEnsemble) {
107
+ // Per-ensemble paused lookup: `/pause` and `/shutdown` both flip
108
+ // `maestroSetPausedSignal` on the maestro hub workflow. The hub's
109
+ // `maestroPaused` query is the authoritative "ensemble is paused"
110
+ // signal — fall back to the phase heuristic when the hub doesn't
111
+ // exist (bare ensemble before any conductor / TUI was attached).
112
+ const pausedByEnsemble = new Map();
113
+ await Promise.all([...byEnsemble.keys()].map(async (name) => {
114
+ try {
115
+ // Issue #433 — bound per-ensemble maestro query so a wedged
116
+ // maestro can't hang `listEnsembles` (the snapshot existence
117
+ // gate at snapshot.ts:144). Existing catch maps any failure
118
+ // to "leave paused undefined" and the downstream phase
119
+ // heuristic classifies the ensemble.
120
+ const paused = await (0, query_timeout_1.queryHandleWithTimeout)(handle((0, config_1.maestroWorkflowId)(name)), maestro_signals_1.maestroPausedQuery);
121
+ pausedByEnsemble.set(name, !!paused);
122
+ }
123
+ catch {
124
+ // Hub workflow not running, or worker wedged (#433) — leave
125
+ // undefined so the phase heuristic below decides
126
+ // classification.
127
+ }
128
+ }));
129
+ const out = [];
130
+ for (const [name, info] of byEnsemble) {
131
+ // Skip ensembles with no non-gone sessions — they're either
132
+ // terminating or fully destroyed.
133
+ if (info.liveAdapterCount === 0 && !info.hasDetached)
134
+ continue;
135
+ const paused = pausedByEnsemble.get(name);
136
+ // Three-state classification:
137
+ // online — hub unpaused (or no hub + at least one live adapter).
138
+ // paused — hub paused AND at least one live adapter remains
139
+ // (`/pause` semantics: resume in place via `/play`).
140
+ // offline — hub paused AND zero live adapters
141
+ // (`/shutdown` semantics: requires `/restore`).
142
+ // When the hub didn't answer (no maestro yet), fall back to the
143
+ // phase heuristic — a live adapter implies online.
144
+ let state;
145
+ if (paused === true) {
146
+ state = info.liveAdapterCount > 0 ? 'paused' : 'offline';
147
+ }
148
+ else if (paused === false) {
149
+ state = 'online';
150
+ }
151
+ else {
152
+ state = info.liveAdapterCount > 0 ? 'online' : 'offline';
153
+ }
154
+ out.push({
155
+ name,
156
+ playerCount: info.count,
157
+ hasConductor: info.hasConductor,
158
+ conductorStatus: info.conductorStatus,
159
+ state,
160
+ });
161
+ }
162
+ return out;
163
+ }
164
+ return {
165
+ subscribe,
166
+ async discoverEnsembles() {
167
+ // Strategy 1: Global Maestro playersByEnsemble query
168
+ try {
169
+ const h = handle(globalMaestroId);
170
+ // #433: unbounded — justified because `discoverEnsembles` is not
171
+ // reachable from `buildEnsembleSnapshot` (it's a CLI / TUI
172
+ // discovery surface, separate from the snapshot existence gate
173
+ // which uses `listEnsembles`). A hung global maestro here only
174
+ // affects the CLI lister, which has its own user-facing
175
+ // cancellability via Ctrl-C.
176
+ const byEnsemble = await h.query('maestroPlayersByEnsemble');
177
+ const results = Object.entries(byEnsemble).map(([name, players]) => {
178
+ const conductor = players.find(p => p.isConductor);
179
+ return {
180
+ name,
181
+ playerCount: players.length,
182
+ hasConductor: !!conductor,
183
+ // `conductorStatus` is a public TempoClient API field (EnsembleInfo);
184
+ // its value now carries the attachment-phase string (post-#176 drift).
185
+ conductorStatus: conductor?.phase,
186
+ };
187
+ });
188
+ // Only trust Maestro if it has discovered ensembles; fall through to
189
+ // Strategy 2 when empty — the Maestro may not have refreshed yet.
190
+ if (results.length > 0)
191
+ return results;
192
+ }
193
+ catch {
194
+ // Global Maestro not available — fall through
195
+ }
196
+ // Strategy 2: Direct workflow list scan
197
+ try {
198
+ const query = 'WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running"';
199
+ const ensembleMap = new Map();
200
+ for await (const wf of client.workflow.list({ query })) {
201
+ const name = (0, search_attributes_1.getEnsembleName)(wf);
202
+ if (!name)
203
+ continue;
204
+ const entry = ensembleMap.get(name) || { count: 0, hasConductor: false };
205
+ entry.count++;
206
+ // Preferred: AgentTempoIsConductor search attribute (canonical, queryable).
207
+ // Fallback: workflow ID convention — covers the brief window after a
208
+ // conductor spawn before the search attribute is indexed.
209
+ const isConductorFromSA = (0, search_attributes_1.getIsConductor)(wf) === true;
210
+ const isConductorFromId = wf.workflowId?.endsWith('-conductor') ?? false;
211
+ if (isConductorFromSA || isConductorFromId) {
212
+ entry.hasConductor = true;
213
+ // Post-#175 the workflow writes `AgentTempoAttachmentState` (phase) in
214
+ // place of the removed `AgentTempoStatus` search attribute.
215
+ entry.conductorStatus = (0, search_attributes_1.getAttachmentPhase)(wf);
216
+ }
217
+ ensembleMap.set(name, entry);
218
+ }
219
+ return [...ensembleMap.entries()].map(([name, info]) => ({
220
+ name,
221
+ playerCount: info.count,
222
+ hasConductor: info.hasConductor,
223
+ conductorStatus: info.conductorStatus,
224
+ }));
225
+ }
226
+ catch {
227
+ return [];
228
+ }
229
+ },
230
+ async listEnsembles() {
231
+ // Direct workflow-list scan — the Global Maestro index only tracks
232
+ // live ensembles, so classifying paused/offline ensembles requires
233
+ // reading the attachment-state search attribute per workflow.
234
+ //
235
+ // `liveAdapterCount` distinguishes `paused` (≥1 live adapter, can
236
+ // resume in place via `/play`) from `offline` (zero live adapters,
237
+ // requires `/restore`). The maestro session is excluded from this
238
+ // count — it's the TUI's own dashboard attachment, never a peer
239
+ // agent that user-facing `/play` should target.
240
+ const LIVE_PHASES = new Set([
241
+ 'attached', 'processing', 'awaiting', 'booting', 'draining',
242
+ ]);
243
+ const byEnsemble = new Map();
244
+ try {
245
+ const query = 'WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running"';
246
+ for await (const wf of client.workflow.list({ query })) {
247
+ const name = (0, search_attributes_1.getEnsembleName)(wf);
248
+ if (!name)
249
+ continue;
250
+ // Exclude the maestro session from the headline player count.
251
+ // The maestro is the TUI's own dashboard attachment, not a peer
252
+ // agent — counting it produced confusing "(2 players)" rows on
253
+ // a fresh ensemble with one real player. Mirrors the
254
+ // `filterRealPlayers` rule used in StatusBar (cf6becd). Detect
255
+ // via the canonical `AgentTempoPlayerType` search attribute,
256
+ // with a workflow-id-suffix fallback for the brief post-start
257
+ // window before search attributes propagate.
258
+ const playerType = (0, search_attributes_1.getSearchAttrString)(wf, 'AgentTempoPlayerType');
259
+ const isMaestroSession = playerType === 'maestro'
260
+ || (wf.workflowId?.endsWith('-maestro') ?? false);
261
+ const phase = (0, search_attributes_1.getAttachmentPhase)(wf);
262
+ const entry = byEnsemble.get(name) ?? {
263
+ count: 0, hasConductor: false, liveAdapterCount: 0, hasDetached: false,
264
+ };
265
+ if (!isMaestroSession)
266
+ entry.count++;
267
+ if (phase === 'detached')
268
+ entry.hasDetached = true;
269
+ else if (phase && LIVE_PHASES.has(phase) && !isMaestroSession) {
270
+ entry.liveAdapterCount++;
271
+ }
272
+ const isConductorFromSA = (0, search_attributes_1.getIsConductor)(wf) === true;
273
+ const isConductorFromId = wf.workflowId?.endsWith('-conductor') ?? false;
274
+ if (isConductorFromSA || isConductorFromId) {
275
+ entry.hasConductor = true;
276
+ if (phase)
277
+ entry.conductorStatus = phase;
278
+ }
279
+ byEnsemble.set(name, entry);
280
+ }
281
+ }
282
+ catch {
283
+ return [];
284
+ }
285
+ return await finishEnsembleSummaries(byEnsemble);
286
+ },
287
+ async listEnsemblesBounded(deadlineMs) {
288
+ // #336/#529 site 6 — bounded variant for `AggregateRunner.collect()`'s
289
+ // 750ms poll. On deadline, propagates `timedOut: true` so the
290
+ // collect tick can skip the entire diff round (preserving
291
+ // `knownEnsembles` and avoiding phantom `ensemble.destroyed`
292
+ // SSE events). Architect-approved invariant for this PR.
293
+ const LIVE_PHASES = new Set([
294
+ 'attached', 'processing', 'awaiting', 'booting', 'draining',
295
+ ]);
296
+ const byEnsemble = new Map();
297
+ let scanned = 0;
298
+ let timedOut = false;
299
+ try {
300
+ const query = 'WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running"';
301
+ for await (const wf of (0, visibility_deadline_1.iterateWithDeadline)(client.workflow.list({ query }), deadlineMs, 'listEnsemblesBounded')) {
302
+ scanned++;
303
+ const name = (0, search_attributes_1.getEnsembleName)(wf);
304
+ if (!name)
305
+ continue;
306
+ const playerType = (0, search_attributes_1.getSearchAttrString)(wf, 'AgentTempoPlayerType');
307
+ const isMaestroSession = playerType === 'maestro'
308
+ || (wf.workflowId?.endsWith('-maestro') ?? false);
309
+ const phase = (0, search_attributes_1.getAttachmentPhase)(wf);
310
+ const entry = byEnsemble.get(name) ?? {
311
+ count: 0, hasConductor: false, liveAdapterCount: 0, hasDetached: false,
312
+ };
313
+ if (!isMaestroSession)
314
+ entry.count++;
315
+ if (phase === 'detached')
316
+ entry.hasDetached = true;
317
+ else if (phase && LIVE_PHASES.has(phase) && !isMaestroSession) {
318
+ entry.liveAdapterCount++;
319
+ }
320
+ const isConductorFromSA = (0, search_attributes_1.getIsConductor)(wf) === true;
321
+ const isConductorFromId = wf.workflowId?.endsWith('-conductor') ?? false;
322
+ if (isConductorFromSA || isConductorFromId) {
323
+ entry.hasConductor = true;
324
+ if (phase)
325
+ entry.conductorStatus = phase;
326
+ }
327
+ byEnsemble.set(name, entry);
328
+ }
329
+ }
330
+ catch (err) {
331
+ if ((0, visibility_deadline_1.isVisibilityTimeout)(err)) {
332
+ timedOut = true;
333
+ // Fall through: the caller (`AggregateRunner`) checks
334
+ // `timedOut` and bails before applying any diff; we still
335
+ // return whatever we managed to enumerate so test/diag
336
+ // surfaces can inspect partial state if useful.
337
+ }
338
+ else {
339
+ throw err; // catastrophic — propagate (no unbounded swallow).
340
+ }
341
+ }
342
+ const items = await finishEnsembleSummaries(byEnsemble);
343
+ return { items, timedOut, scanned };
344
+ },
345
+ async getPlayers(ensemble) {
346
+ // Strategy 1: Global Maestro — filter by ensemble
347
+ try {
348
+ const h = handle(globalMaestroId);
349
+ // Issue #433 — bound the global-maestro query so a wedged
350
+ // global maestro can't hang `getPlayers` (called from the
351
+ // snapshot fan-out and many other paths). Existing catch falls
352
+ // through to Strategy 2 → Strategy 3 on any failure.
353
+ const byEnsemble = await (0, query_timeout_1.queryHandleWithTimeout)(h, 'maestroPlayersByEnsemble');
354
+ if (byEnsemble[ensemble])
355
+ return byEnsemble[ensemble];
356
+ }
357
+ catch {
358
+ // Fall through
359
+ }
360
+ // Strategy 2: Per-ensemble Maestro
361
+ try {
362
+ const h = handle((0, config_1.maestroWorkflowId)(ensemble));
363
+ // Issue #433 — same reasoning, applied to the per-ensemble maestro.
364
+ return await (0, query_timeout_1.queryHandleWithTimeout)(h, 'maestroPlayers');
365
+ }
366
+ catch {
367
+ // Fall through
368
+ }
369
+ // Strategy 3: Direct workflow list
370
+ try {
371
+ const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}"`;
372
+ const players = [];
373
+ for await (const wf of client.workflow.list({ query })) {
374
+ const sa = wf.searchAttributes || {};
375
+ const playerId = Array.isArray(sa.AgentTempoPlayerId) ? String(sa.AgentTempoPlayerId[0]) : wf.workflowId;
376
+ // Preferred: AgentTempoIsConductor search attribute (canonical, queryable).
377
+ // Fallback: workflow ID convention — covers the brief window after a
378
+ // conductor spawn before the search attribute is indexed.
379
+ const isConductorFromSA = Array.isArray(sa.AgentTempoIsConductor) && sa.AgentTempoIsConductor[0] === true;
380
+ const isConductorFromId = wf.workflowId?.endsWith('-conductor') ?? false;
381
+ players.push({
382
+ playerId,
383
+ ensemble,
384
+ part: '',
385
+ hostname: Array.isArray(sa.AgentTempoHostname) ? String(sa.AgentTempoHostname[0]) : '',
386
+ workDir: '',
387
+ isConductor: isConductorFromSA || isConductorFromId,
388
+ agentType: 'claude',
389
+ // Attachment phase from `AgentTempoAttachmentState` search attr.
390
+ phase: Array.isArray(sa.AgentTempoAttachmentState)
391
+ ? String(sa.AgentTempoAttachmentState[0])
392
+ : undefined,
393
+ });
394
+ }
395
+ return players;
396
+ }
397
+ catch {
398
+ return [];
399
+ }
400
+ },
401
+ async getEnsembleMeta(ensemble) {
402
+ // Issue #399 W1 — fan-out four queries against the per-ensemble
403
+ // maestro hub. Each query soft-fails to its sentinel default so
404
+ // a single transient failure can't block the snapshot endpoint.
405
+ const h = handle((0, config_1.maestroWorkflowId)(ensemble));
406
+ // Issue #433 — bound each per-maestro query so a wedged maestro
407
+ // worker can't hang `getEnsembleMeta` (called from snapshot fan-out
408
+ // on every `/v1/state/:ensemble` request and every aggregate tick).
409
+ // Each query already soft-fails to its sentinel; `QueryTimeoutError`
410
+ // falls into the same `.catch(() => sentinel)` path.
411
+ const [description, startedAt, currentBpm, tempoSeries] = await Promise.all([
412
+ (0, query_timeout_1.queryHandleWithTimeout)(h, maestro_signals_1.getEnsembleDescriptionQuery).catch(() => ''),
413
+ (0, query_timeout_1.queryHandleWithTimeout)(h, maestro_signals_1.getEnsembleStartTimeQuery).catch(() => ''),
414
+ (0, query_timeout_1.queryHandleWithTimeout)(h, maestro_signals_1.getCurrentBpmQuery).catch(() => 0),
415
+ (0, query_timeout_1.queryHandleWithTimeout)(h, maestro_signals_1.getTempoSeriesQuery).catch(() => []),
416
+ ]);
417
+ return { description, startedAt, currentBpm, tempoSeries };
418
+ },
419
+ async getPlayerWireMeta(ensemble, playerId) {
420
+ // Issue #399 W2 — fan-out three queries against the session
421
+ // workflow. The handle is opened by workflow ID directly; if the
422
+ // workflow can't be resolved (just-recruited, just-destroyed,
423
+ // transient lookup failure) every query rejects together and
424
+ // we return `null` so the caller's projection drops the whole
425
+ // wire-meta block rather than emitting half-populated fields.
426
+ const h = handle((0, config_1.sessionWorkflowId)(ensemble, playerId));
427
+ // Issue #433 — bound each per-session query. Without a timeout,
428
+ // `Promise.allSettled` waits for the slowest query to settle (or
429
+ // never, if the session worker is wedged), so a single hung session
430
+ // would block the entire snapshot fan-out for `/v1/state/:ensemble`
431
+ // and the AggregateRunner's 750ms poll loop. With timeouts, hung
432
+ // queries reject as `QueryTimeoutError`, `Promise.allSettled` sees
433
+ // three rejections and the existing all-rejected branch returns
434
+ // `null` — caller treats this player's wireMeta as missing.
435
+ const [runIdR, messagingR, leaseR] = await Promise.allSettled([
436
+ (0, query_timeout_1.queryHandleWithTimeout)(h, signals_1.getRunIdQuery),
437
+ (0, query_timeout_1.queryHandleWithTimeout)(h, signals_1.getMessagingStateQuery),
438
+ (0, query_timeout_1.queryHandleWithTimeout)(h, signals_1.getLeaseStateQuery),
439
+ ]);
440
+ // If every query rejected, treat this as "session unreachable" —
441
+ // the caller renders no wire-meta rather than partial sentinels.
442
+ if (runIdR.status === 'rejected' &&
443
+ messagingR.status === 'rejected' &&
444
+ leaseR.status === 'rejected') {
445
+ return null;
446
+ }
447
+ const out = {};
448
+ if (runIdR.status === 'fulfilled')
449
+ out.runId = runIdR.value;
450
+ if (messagingR.status === 'fulfilled')
451
+ out.messaging = messagingR.value;
452
+ if (leaseR.status === 'fulfilled')
453
+ out.lease = leaseR.value;
454
+ return out;
455
+ },
456
+ async getMessages(ensemble, limit) {
457
+ try {
458
+ const h = handle(globalMaestroId);
459
+ // #433: unbounded — justified, `getMessages` is not reachable from
460
+ // `buildEnsembleSnapshot` (snapshot uses `getEnsembleChat`).
461
+ // Called by the recall MCP tool / TUI on user demand; user can
462
+ // Ctrl-C if it hangs.
463
+ const all = await h.query('maestroRecentMessages');
464
+ const filtered = all.filter(m => m.ensemble === ensemble);
465
+ return limit ? filtered.slice(-limit) : filtered;
466
+ }
467
+ catch {
468
+ return [];
469
+ }
470
+ },
471
+ async getConductorHistory(ensemble) {
472
+ try {
473
+ const h = handle(globalMaestroId);
474
+ const result = await h.executeUpdate('maestroFetchConductorHistory', {
475
+ args: [{ ensemble }],
476
+ });
477
+ if (result.success)
478
+ return result.history;
479
+ return [];
480
+ }
481
+ catch {
482
+ return [];
483
+ }
484
+ },
485
+ async getPlayerMessages(ensemble, playerId) {
486
+ try {
487
+ const h = handle(globalMaestroId);
488
+ return await h.executeUpdate('maestroFetchPlayerMessages', {
489
+ args: [{ ensemble, playerId }],
490
+ });
491
+ }
492
+ catch {
493
+ return [];
494
+ }
495
+ },
496
+ async getPlayerMetadata(ensemble, playerId) {
497
+ try {
498
+ // Query the player's workflow directly for metadata
499
+ const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}" AND AgentTempoPlayerId = "${sanitizeQueryValue(playerId)}"`;
500
+ for await (const wf of client.workflow.list({ query })) {
501
+ const h = handle(wf.workflowId);
502
+ // #433: unbounded — justified, `getPlayerMetadata` is not
503
+ // reachable from `buildEnsembleSnapshot` (snapshot reads
504
+ // metadata via `getPlayers` → maestro fan-out, not per-player
505
+ // direct query). Used by ad-hoc tools / debug surfaces on
506
+ // user demand.
507
+ return await h.query('getMetadata');
508
+ }
509
+ return null;
510
+ }
511
+ catch {
512
+ return null;
513
+ }
514
+ },
515
+ async sendCommand(ensemble, text, source) {
516
+ // Route commands through Maestro hub → conductor's commandSignal
517
+ let result;
518
+ try {
519
+ const h = handle(globalMaestroId);
520
+ result = await h.executeUpdate('maestroGlobalSendCommand', {
521
+ args: [{ ensemble, text, source }],
522
+ });
523
+ }
524
+ catch {
525
+ const h = handle((0, config_1.maestroWorkflowId)(ensemble));
526
+ result = await h.executeUpdate('maestroSendCommand', {
527
+ args: [{ text, source }],
528
+ });
529
+ }
530
+ // Record on maestro workflow for history persistence
531
+ try {
532
+ const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
533
+ const mh = handle(maestroId);
534
+ await mh.signal('recordSentMessage', { to: 'conductor', text });
535
+ }
536
+ catch { /* best effort */ }
537
+ return result;
538
+ },
539
+ async sendMessage(ensemble, to, text, source) {
540
+ // Direct signal with isMaestro flag — matches web Maestro pattern
541
+ const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}" AND AgentTempoPlayerId = "${sanitizeQueryValue(to)}"`;
542
+ let sent = false;
543
+ for await (const wf of client.workflow.list({ query })) {
544
+ const h = handle(wf.workflowId);
545
+ await h.signal('receiveMessage', {
546
+ from: source,
547
+ text,
548
+ isMaestro: true,
549
+ });
550
+ sent = true;
551
+ break;
552
+ }
553
+ if (!sent) {
554
+ // Fallback: try via Maestro hub if direct resolution fails
555
+ try {
556
+ const h = handle(globalMaestroId);
557
+ await h.executeUpdate('maestroSendMessage', {
558
+ args: [{ ensemble, to, text, source }],
559
+ });
560
+ }
561
+ catch {
562
+ throw new Error(`Player "${to}" not found in ensemble "${ensemble}"`);
563
+ }
564
+ }
565
+ // Record on maestro workflow for history persistence
566
+ try {
567
+ const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
568
+ const mh = handle(maestroId);
569
+ await mh.signal('recordSentMessage', { to, text });
570
+ }
571
+ catch { /* best effort */ }
572
+ return `maestro-msg-${Date.now()}`;
573
+ },
574
+ async terminatePlayer(ensemble, playerId) {
575
+ const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}" AND AgentTempoPlayerId = "${sanitizeQueryValue(playerId)}"`;
576
+ for await (const wf of client.workflow.list({ query })) {
577
+ const h = handle(wf.workflowId);
578
+ await h.terminate('terminated via TUI');
579
+ return;
580
+ }
581
+ throw new Error(`Player "${playerId}" not found in ensemble "${ensemble}"`);
582
+ },
583
+ // ── PR-D verbs — enqueue on the TUI-owned maestro session's outbox.
584
+ // The dispatch loop runs `deliverDetach` / `deliverDestroy` /
585
+ // `deliverRestart` activities against the target (QA B1/B2/B3).
586
+ async recruit(ensemble, opts) {
587
+ // #306: Lazy-import the agent-type resolver so the TUI/CLI bundle
588
+ // doesn't pull in the subagent YAML crawler at module-load time.
589
+ // The `held` flow on the TUI side doesn't currently exercise this;
590
+ // `playerType` is only resolved when supplied.
591
+ let agentDefinition;
592
+ let agentDefinitionPath;
593
+ let agentDefinitionDescription;
594
+ let nativeResolvable;
595
+ let allowedTools;
596
+ if (opts.playerType) {
597
+ const { resolveAgentType } = await Promise.resolve().then(() => __importStar(require('../ensemble/agent-types')));
598
+ const info = resolveAgentType(opts.playerType);
599
+ if (!info) {
600
+ throw new Error(`Unknown agent type "${opts.playerType}"`);
601
+ }
602
+ agentDefinition = info.name;
603
+ agentDefinitionPath = info.path;
604
+ agentDefinitionDescription = info.description;
605
+ nativeResolvable = info.nativeResolvable;
606
+ allowedTools = info.allowedTools;
607
+ }
608
+ const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
609
+ const h = handle(maestroId);
610
+ // #306 fix: always set `targetHostname` on the entry. The TUI-owned
611
+ // maestro session stores `hostname: 'dashboard'` in its metadata
612
+ // (a placeholder, not a real host), so the session workflow's
613
+ // fallback path — `entry.targetHostname || input.metadata.hostname`
614
+ // — routes `spawnProcess` to task queue `agent-tempo-dashboard`,
615
+ // which has no worker. The MCP `recruit` tool worked because the
616
+ // conductor session that ran it had a real OS hostname in metadata.
617
+ // Mirror that behavior here by defaulting to `osHostname()` when
618
+ // the caller didn't pin a specific host.
619
+ const targetHostname = opts.host ?? (0, os_1.hostname)();
620
+ const entry = {
621
+ type: 'recruit',
622
+ targetName: opts.name,
623
+ workDir: opts.workDir,
624
+ isConductor: opts.isConductor === true,
625
+ agent: opts.agent ?? 'claude',
626
+ ...(opts.initialMessage !== undefined ? { initialMessage: opts.initialMessage } : {}),
627
+ // If a player-type is provided, let the outbox activity supply the
628
+ // agent definition bundle; otherwise fall back to an explicit
629
+ // systemPrompt path (mirrors the recruit MCP tool's branching).
630
+ ...(agentDefinition
631
+ ? { agentDefinition, agentDefinitionPath, agentDefinitionDescription, nativeResolvable, allowedTools }
632
+ : opts.systemPrompt !== undefined ? { systemPrompt: opts.systemPrompt } : {}),
633
+ targetHostname,
634
+ ...(opts.held === true ? { held: true } : {}),
635
+ };
636
+ const entryId = await h.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
637
+ return { playerId: opts.name, entryId };
638
+ },
639
+ async release(ensemble, playerId) {
640
+ const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
641
+ const mh = handle(maestroId);
642
+ const submitRelease = async (target) => {
643
+ const entry = { type: 'release', targetPlayerId: target };
644
+ await mh.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
645
+ };
646
+ if (playerId) {
647
+ // Single-player release — match the MCP tool: only submit when the
648
+ // session's outbox is actually locked, so the caller sees a clean
649
+ // error instead of a no-op success for already-running sessions.
650
+ const target = await (0, resolve_1.resolveSession)(client, ensemble, playerId);
651
+ if (!target) {
652
+ throw new Error(`No session found with name "${playerId}" in ensemble "${ensemble}".`);
653
+ }
654
+ let isLocked = false;
655
+ try {
656
+ // #433: unbounded — justified, the single-player `release()`
657
+ // path is an explicit MCP tool action triggered by the user
658
+ // ("release X"), not the snapshot fan-out. `isAnySessionHeld`
659
+ // (also called `outboxLockedQuery` per-session, but in the
660
+ // snapshot path) IS wrapped — see line ~1020.
661
+ isLocked = await target.query(signals_1.outboxLockedQuery);
662
+ }
663
+ catch {
664
+ // Query may fail for old workflows — treat as "not held" to avoid
665
+ // false-positive release requests on pre-outboxLocked builds.
666
+ }
667
+ if (!isLocked) {
668
+ throw new Error(`Session "${playerId}" is not held (outbox not locked).`);
669
+ }
670
+ await submitRelease(playerId);
671
+ return { released: [playerId], errors: [] };
672
+ }
673
+ // Bulk release — scan + query + enqueue each held session. The scan
674
+ // skips the TUI's own maestro session so we don't try to release
675
+ // ourselves. Errors are returned as soft failures so the caller can
676
+ // render a partial-success summary.
677
+ const sessions = await (0, resolve_1.scanEnsembleSessions)(client, ensemble);
678
+ const held = [];
679
+ for (const s of sessions) {
680
+ if (s.playerId === 'maestro')
681
+ continue;
682
+ try {
683
+ const sh = handle(s.workflowId);
684
+ // Issue #433 — bound the per-session query so a single wedged
685
+ // worker doesn't block the rest of the bulk-release scan. The
686
+ // existing catch already maps failures to "not held".
687
+ const locked = await (0, query_timeout_1.queryHandleWithTimeout)(sh, signals_1.outboxLockedQuery);
688
+ if (locked)
689
+ held.push(s);
690
+ }
691
+ catch {
692
+ // Skip sessions where the query fails (old workflows, terminated,
693
+ // or wedged-worker timeout per #433).
694
+ }
695
+ }
696
+ const released = [];
697
+ const errors = [];
698
+ for (const s of held) {
699
+ try {
700
+ await submitRelease(s.playerId);
701
+ released.push(s.playerId);
702
+ }
703
+ catch (err) {
704
+ errors.push({ playerId: s.playerId, error: errMsg(err) });
705
+ }
706
+ }
707
+ return { released, errors };
708
+ },
709
+ async restart(ensemble, playerId, opts = {}) {
710
+ const invokerPlayerId = opts.invokerPlayerId ?? 'cli';
711
+ const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
712
+ const h = handle(maestroId);
713
+ // #580 — `confirmStealFromHost` is a caller-side intent flag (§16.5
714
+ // Option B). The outbox entry has no slot for it because the workflow
715
+ // trusts the caller; the gate is enforced pre-submit by the TUI
716
+ // handler and the shared MCP-tool guard. Accepting the field on
717
+ // `RestartClientOpts` gives external SDK consumers and the TUI a
718
+ // typed pipeline to forward the confirmed value.
719
+ const entry = {
720
+ type: 'restart',
721
+ targetPlayerId: playerId,
722
+ invokerPlayerId,
723
+ ...(opts.host !== undefined ? { host: opts.host } : {}),
724
+ ...(opts.fresh !== undefined ? { fresh: opts.fresh } : {}),
725
+ ...(opts.force !== undefined ? { force: opts.force } : {}),
726
+ ...(opts.contextMessages !== undefined ? { contextMessages: opts.contextMessages } : {}),
727
+ ...(opts.loadFromState !== undefined ? { loadFromState: opts.loadFromState } : {}),
728
+ ...(opts.transcript !== undefined ? { transcript: opts.transcript } : {}),
729
+ };
730
+ const entryId = await h.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
731
+ return {
732
+ playerId,
733
+ ...(opts.host !== undefined ? { host: opts.host } : {}),
734
+ entryId,
735
+ };
736
+ },
737
+ async detach(ensemble, playerId, deadlineMs = 5_000) {
738
+ const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
739
+ const h = handle(maestroId);
740
+ const entry = {
741
+ type: 'detach',
742
+ targetPlayerId: playerId,
743
+ reason: 'user-stop',
744
+ deadlineMs,
745
+ };
746
+ await h.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
747
+ },
748
+ async destroy(ensemble, playerId, reason) {
749
+ // #287: ensemble-scope when `playerId` is omitted. Peer sessions in
750
+ // parallel → scheduler + maestro terminate in parallel → conductor
751
+ // last so it sees every peer teardown. Matches the destroy tool.
752
+ if (playerId === undefined) {
753
+ const destroyReason = reason ?? 'ensemble destroy via TempoClient';
754
+ const conductorWfId = (0, config_1.conductorWorkflowId)(ensemble);
755
+ const sessions = await (0, resolve_1.scanEnsembleSessions)(client, ensemble);
756
+ const peers = [];
757
+ let conductorPresent = false;
758
+ for (const s of sessions) {
759
+ if (s.workflowId === conductorWfId)
760
+ conductorPresent = true;
761
+ else
762
+ peers.push(s);
763
+ }
764
+ const summary = {
765
+ destroyed: 0,
766
+ terminated: 0,
767
+ failed: 0,
768
+ details: [],
769
+ };
770
+ const destroyArgs = { reason: destroyReason, terminatedBy: 'tempo-client' };
771
+ // Peers in parallel.
772
+ const peerResults = await Promise.allSettled(peers.map(async (s) => {
773
+ try {
774
+ await handle(s.workflowId).executeUpdate(signals_1.destroyUpdate, { args: [destroyArgs] });
775
+ return { session: s, outcome: 'destroyed' };
776
+ }
777
+ catch (err) {
778
+ return { session: s, outcome: 'failed', error: errMsg(err) };
779
+ }
780
+ }));
781
+ for (const r of peerResults) {
782
+ if (r.status !== 'fulfilled')
783
+ continue;
784
+ const v = r.value;
785
+ if (v.outcome === 'destroyed') {
786
+ summary.details.push({ target: v.session.playerId, outcome: 'destroyed' });
787
+ summary.destroyed++;
788
+ }
789
+ else {
790
+ summary.details.push({ target: v.session.playerId, outcome: 'failed', error: v.error });
791
+ summary.failed++;
792
+ }
793
+ }
794
+ // Scheduler + maestro terminate in parallel. `terminate` rejects on
795
+ // missing workflows; treat as "not present" (don't count as failure).
796
+ const [schedRes, maestroRes] = await Promise.allSettled([
797
+ handle((0, config_1.schedulerWorkflowId)(ensemble)).terminate(destroyReason),
798
+ handle((0, config_1.maestroWorkflowId)(ensemble)).terminate(destroyReason),
799
+ ]);
800
+ if (schedRes.status === 'fulfilled') {
801
+ summary.details.push({ target: 'scheduler', outcome: 'terminated' });
802
+ summary.terminated++;
803
+ }
804
+ if (maestroRes.status === 'fulfilled') {
805
+ summary.details.push({ target: 'maestro', outcome: 'terminated' });
806
+ summary.terminated++;
807
+ }
808
+ // Conductor last.
809
+ if (conductorPresent) {
810
+ try {
811
+ await handle(conductorWfId).executeUpdate(signals_1.destroyUpdate, { args: [destroyArgs] });
812
+ summary.details.push({ target: 'conductor', outcome: 'destroyed' });
813
+ summary.destroyed++;
814
+ }
815
+ catch (err) {
816
+ summary.details.push({ target: 'conductor', outcome: 'failed', error: errMsg(err) });
817
+ summary.failed++;
818
+ }
819
+ }
820
+ return summary;
821
+ }
822
+ const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
823
+ const h = handle(maestroId);
824
+ const entry = {
825
+ type: 'destroy',
826
+ targetPlayerId: playerId,
827
+ ...(reason !== undefined ? { reason } : {}),
828
+ notifyConductor: true,
829
+ };
830
+ await h.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
831
+ },
832
+ async pause(ensemble) {
833
+ await Promise.all([
834
+ (0, ensemble_ops_1.pauseMaestroAndScheduler)(client, ensemble),
835
+ (0, ensemble_ops_1.signalAllSessions)(client, ensemble, signals_1.setPausedSignal.name, true),
836
+ ]);
837
+ },
838
+ async play(ensemble, opts = {}) {
839
+ const [, unpaused] = await Promise.all([
840
+ (0, ensemble_ops_1.unpauseMaestroAndScheduler)(client, ensemble),
841
+ (0, ensemble_ops_1.signalAllSessions)(client, ensemble, signals_1.setPausedSignal.name, false),
842
+ ]);
843
+ if (opts.release === true && unpaused.sent > 0) {
844
+ // Fan out releaseHeld AFTER everyone is unpaused so no session
845
+ // receives `releaseHeld` while still paused.
846
+ await (0, ensemble_ops_1.signalAllSessions)(client, ensemble, signals_1.releaseHeldSignal.name, undefined);
847
+ }
848
+ },
849
+ async shutdown(ensemble, opts = {}) {
850
+ const deadlineMs = opts.deadlineMs ?? 5_000;
851
+ const [toggle, fanout] = await Promise.all([
852
+ (0, ensemble_ops_1.pauseMaestroAndScheduler)(client, ensemble),
853
+ (0, ensemble_ops_1.signalAllSessions)(client, ensemble, signals_1.requestDetachSignal.name, { reason: 'user-stop', deadlineMs }),
854
+ ]);
855
+ return {
856
+ detached: fanout.sent,
857
+ skipped: fanout.skipped,
858
+ failed: fanout.failed,
859
+ maestroPaused: toggle.maestro,
860
+ schedulerPaused: toggle.scheduler,
861
+ // #299 sibling: TempoClient does not pass a `skip` predicate to
862
+ // `signalAllSessions`, so `fanout.perSession[*].outcome` will never
863
+ // be `'skipped'` here. The narrowed `EnsembleShutdownDetail.outcome`
864
+ // reflects the actual public surface; the `'skipped'` branch is an
865
+ // explicit no-op that emits nothing.
866
+ details: fanout.perSession.flatMap((p) => {
867
+ if (p.outcome === 'sent') {
868
+ return [{ playerId: p.playerId, outcome: 'detaching' }];
869
+ }
870
+ if (p.outcome === 'failed') {
871
+ return [{ playerId: p.playerId, outcome: 'failed', error: p.error }];
872
+ }
873
+ return []; // 'skipped' is unreachable in the TempoClient path
874
+ }),
875
+ };
876
+ },
877
+ async restore(ensemble) {
878
+ // Scope the orphan scan to the requested ensemble (#298 — matches the
879
+ // `ensemble?` filter the CLI/TUI pass through) and unpause maestro +
880
+ // scheduler for the same ensemble in parallel.
881
+ //
882
+ // #306: narrow to `phases: ['detached']`. User-invoked `/restore`
883
+ // revives a parked ensemble — a live attached/processing session is
884
+ // NOT a restorable orphan and must not be flagged as one. The broad
885
+ // live-phase default is reserved for daemon reconcile-on-boot + CLI
886
+ // `up --resume`, which have no PID memory after a crash and must
887
+ // treat every live phase as a presumed orphan. Without this narrowing
888
+ // a healthy conductor gets deliverRestart → requestDetach and is
889
+ // hard-terminated by `drainingDeadline`.
890
+ //
891
+ // Bug A: also fan out `setPaused=false` to every session. Without
892
+ // this, sessions whose `paused` flag was flipped (via `/pause` or
893
+ // any prior pause path) stay frozen — the conductor receives
894
+ // messages but its outbox dispatcher is gated by `!paused`, so
895
+ // typed messages get no reply. Mirrors the pattern in `play()`:
896
+ // the maestro/scheduler hub toggle is not enough on its own.
897
+ const [summary] = await Promise.all([
898
+ (0, orphans_1.restoreOrphansOnce)(client, {
899
+ hostname: (0, os_1.hostname)(),
900
+ invokerPlayerId: 'tempo-client',
901
+ policy: 'auto',
902
+ ensemble,
903
+ phases: ['detached'],
904
+ }),
905
+ (0, ensemble_ops_1.unpauseMaestroAndScheduler)(client, ensemble),
906
+ (0, ensemble_ops_1.signalAllSessions)(client, ensemble, signals_1.setPausedSignal.name, false),
907
+ ]);
908
+ return summary;
909
+ },
910
+ async migrate(ensemble, playerId, host, opts = {}) {
911
+ if (!host || !host.trim()) {
912
+ throw new Error('`host` is required for migrate. Use `restart` to revive on the current host.');
913
+ }
914
+ return this.restart(ensemble, playerId, { ...opts, host });
915
+ },
916
+ async attachmentInfo(ensemble, playerId) {
917
+ // Read-only query — resolve + query directly (no outbox needed).
918
+ const target = await (0, resolve_1.resolveSession)(client, ensemble, playerId);
919
+ if (!target)
920
+ throw new Error(`No session found with name "${playerId}" in ensemble "${ensemble}".`);
921
+ // #433: unbounded — justified, `attachmentInfo()` is the
922
+ // user-facing MCP tool that returns one player's lease/phase to
923
+ // the operator. Not reachable from `buildEnsembleSnapshot`
924
+ // (snapshot reads attachment info via the `phase` search
925
+ // attribute and `getPlayerWireMeta`'s lease query, both bounded).
926
+ return target.query(signals_1.attachmentInfoQuery);
927
+ },
928
+ async listHosts(opts = {}) {
929
+ // Lazy import so this doesn't drag utils/hosts into every
930
+ // consumer of TempoClient at module-load time.
931
+ const { listHosts } = await Promise.resolve().then(() => __importStar(require('../utils/hosts')));
932
+ // #437 — both `namespace` and `taskQueue` must match the daemon's
933
+ // config or poller discovery silently returns `[]` (dev mode hits
934
+ // `'agent-tempo-dev'`, prod hits `'agent-tempo'`). Passing
935
+ // `taskQueue: undefined` is harmless — `listHosts` defaults via
936
+ // `?? 'agent-tempo'` and unconditional pass-through avoids
937
+ // per-call object allocation on this hot path.
938
+ return listHosts(client, {
939
+ force: Boolean(opts.force),
940
+ namespace: client.options.namespace,
941
+ taskQueue,
942
+ });
943
+ },
944
+ async recall(ensemble, playerId) {
945
+ // #128: direct session queries, no maestro round-trip. Throws rather
946
+ // than returning empties so the CLI / TUI wrappers can surface a
947
+ // clean "session not found" error instead of rendering a silently
948
+ // empty timeline that looks indistinguishable from "no messages yet."
949
+ const target = await (0, resolve_1.resolveSession)(client, ensemble, playerId);
950
+ if (!target)
951
+ throw new Error(`No session found with name "${playerId}" in ensemble "${ensemble}".`);
952
+ // #433: unbounded — justified, `recall()` is an explicit MCP tool
953
+ // action invoked on user demand ("recall messages for X"). Not
954
+ // reachable from `buildEnsembleSnapshot` (snapshot's per-player
955
+ // wire-meta uses bounded `getMessagingStateQuery` for counters
956
+ // only, never the full message list).
957
+ const [received, sent] = await Promise.all([
958
+ target.query('allMessages'),
959
+ target.query('allSentMessages'),
960
+ ]);
961
+ return { received, sent };
962
+ },
963
+ async disbandEnsemble(ensemble) {
964
+ let terminated = 0;
965
+ // Terminate all session workflows in the ensemble
966
+ const sessionQuery = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}"`;
967
+ for await (const wf of client.workflow.list({ query: sessionQuery })) {
968
+ try {
969
+ const h = handle(wf.workflowId);
970
+ await h.terminate('disbanded via TUI');
971
+ terminated++;
972
+ }
973
+ catch { /* already closed */ }
974
+ }
975
+ // Terminate scheduler workflow
976
+ try {
977
+ const h = handle((0, config_1.schedulerWorkflowId)(ensemble));
978
+ await h.terminate('disbanded via TUI');
979
+ terminated++;
980
+ }
981
+ catch { /* no scheduler or already closed */ }
982
+ // Terminate per-ensemble maestro workflow
983
+ try {
984
+ const h = handle((0, config_1.maestroWorkflowId)(ensemble));
985
+ await h.terminate('disbanded via TUI');
986
+ terminated++;
987
+ }
988
+ catch { /* no maestro or already closed */ }
989
+ return { terminated };
990
+ },
991
+ async isConnected() {
992
+ try {
993
+ // Lightweight check: list with limit 1
994
+ const query = 'ExecutionStatus = "Running"';
995
+ for await (const _ of client.workflow.list({ query })) {
996
+ return true;
997
+ }
998
+ return true; // Connected but no workflows
999
+ }
1000
+ catch {
1001
+ return false;
1002
+ }
1003
+ },
1004
+ async getSchedules(ensemble) {
1005
+ try {
1006
+ const h = handle((0, config_1.schedulerWorkflowId)(ensemble));
1007
+ // Issue #433 — bound the scheduler query so a wedged scheduler
1008
+ // worker can't hang `getSchedules` (called from snapshot fan-out
1009
+ // and aggregate poll). Existing catch maps any failure to `[]`.
1010
+ return await (0, query_timeout_1.queryHandleWithTimeout)(h, 'getSchedules');
1011
+ }
1012
+ catch {
1013
+ return [];
1014
+ }
1015
+ },
1016
+ async cancelSchedule(ensemble, name) {
1017
+ const h = handle((0, config_1.schedulerWorkflowId)(ensemble));
1018
+ await h.signal('removeSchedule', name);
1019
+ },
1020
+ async getEnsembleChat(ensemble, offset, limit) {
1021
+ try {
1022
+ const h = handle((0, config_1.maestroWorkflowId)(ensemble));
1023
+ // Issue #433 — bound the maestro chat query so a wedged maestro
1024
+ // worker can't hang `getEnsembleChat` (called from snapshot
1025
+ // fan-out and aggregate poll). Existing catch maps any failure
1026
+ // to an empty chat result. Note: dedup keys on workflowId+name
1027
+ // only, so concurrent snapshot+aggregate calls with different
1028
+ // (offset, limit) pairs share a result — the wider window is a
1029
+ // superset of the narrower so this is safe (see helper JSDoc).
1030
+ return await (0, query_timeout_1.queryHandleWithTimeout)(h, 'maestroEnsembleChat', { args: [{ offset, limit }] });
1031
+ }
1032
+ catch {
1033
+ return { messages: [], total: 0, hasMore: false, hasConductor: false };
1034
+ }
1035
+ },
1036
+ async isMaestroPaused(ensemble) {
1037
+ // Reads the same `maestroPaused` query that `listEnsembles` uses for
1038
+ // the home-view classification. Treat hub-not-running as "not paused"
1039
+ // — bare ensembles without a maestro hub aren't displaying any
1040
+ // pause-related state in the chat view either.
1041
+ try {
1042
+ // Issue #433 — bound the maestro query so a wedged maestro worker
1043
+ // can't hang `isMaestroPaused` (called from `buildEnsembleSnapshot`
1044
+ // on every `/v1/state/:ensemble` request and aggregate tick).
1045
+ // Existing `catch` maps any failure to `false` (not paused).
1046
+ const paused = await (0, query_timeout_1.queryHandleWithTimeout)(handle((0, config_1.maestroWorkflowId)(ensemble)), maestro_signals_1.maestroPausedQuery);
1047
+ return !!paused;
1048
+ }
1049
+ catch {
1050
+ return false;
1051
+ }
1052
+ },
1053
+ async isAnySessionHeld(ensemble) {
1054
+ // Scan the ensemble's sessions and check the per-session
1055
+ // `outboxLocked` query. The maestro session is skipped — it's the
1056
+ // TUI's own dashboard attachment, not a peer agent that the user-
1057
+ // facing `/go` should target. Per-session query failures are
1058
+ // treated as "not held" so a single flaky workflow doesn't make
1059
+ // the whole ensemble appear held forever.
1060
+ try {
1061
+ const sessions = await (0, resolve_1.scanEnsembleSessions)(client, ensemble);
1062
+ for (const s of sessions) {
1063
+ if (s.playerId === 'maestro')
1064
+ continue;
1065
+ try {
1066
+ const sh = handle(s.workflowId);
1067
+ // Issue #433 — bound the per-session query so a wedged worker
1068
+ // can't hang `isAnySessionHeld` (called from
1069
+ // `buildEnsembleSnapshot` on every snapshot fan-out). Without
1070
+ // this, the first hung session blocks every subsequent
1071
+ // session and the entire `held` field of the snapshot.
1072
+ const locked = await (0, query_timeout_1.queryHandleWithTimeout)(sh, signals_1.outboxLockedQuery);
1073
+ if (locked)
1074
+ return true;
1075
+ }
1076
+ catch {
1077
+ // Old workflow without `outboxLocked` query, terminated
1078
+ // mid-scan, or wedged-worker timeout (#433) — skip this
1079
+ // session, keep checking the rest.
1080
+ }
1081
+ }
1082
+ return false;
1083
+ }
1084
+ catch {
1085
+ return false;
1086
+ }
1087
+ },
1088
+ async getGates(ensemble) {
1089
+ // Gates are stored on the conductor's workflow
1090
+ try {
1091
+ const h = handle((0, config_1.conductorWorkflowId)(ensemble));
1092
+ // #433: unbounded — justified, `getGates` is not reachable from
1093
+ // `buildEnsembleSnapshot` (snapshot doesn't surface gates).
1094
+ // Called by `gates` MCP tool / dashboard quality-gate panel on
1095
+ // explicit fetch.
1096
+ return await h.query('qualityGates');
1097
+ }
1098
+ catch {
1099
+ return [];
1100
+ }
1101
+ },
1102
+ async getStages(ensemble) {
1103
+ try {
1104
+ const h = handle((0, config_1.conductorWorkflowId)(ensemble));
1105
+ // #433: unbounded — justified, `getStages` is not reachable from
1106
+ // `buildEnsembleSnapshot` (snapshot doesn't surface stages).
1107
+ // Called by `stages` MCP tool on explicit fetch.
1108
+ return await h.query('stages');
1109
+ }
1110
+ catch {
1111
+ return [];
1112
+ }
1113
+ },
1114
+ async getWorktrees(ensemble) {
1115
+ try {
1116
+ const h = handle((0, config_1.conductorWorkflowId)(ensemble));
1117
+ // #433: unbounded — justified, `getWorktrees` is not reachable
1118
+ // from `buildEnsembleSnapshot` (snapshot doesn't surface
1119
+ // worktrees). Called by `worktree` MCP tool on explicit fetch.
1120
+ return await h.query('worktrees');
1121
+ }
1122
+ catch {
1123
+ return [];
1124
+ }
1125
+ },
1126
+ async hasGlobalMaestro() {
1127
+ try {
1128
+ const h = handle(globalMaestroId);
1129
+ const desc = await h.describe();
1130
+ return desc.status.name === 'RUNNING';
1131
+ }
1132
+ catch {
1133
+ return false;
1134
+ }
1135
+ },
1136
+ // ── Maestro session (TUI-owned workflow for two-way messaging) ──
1137
+ async ensureMaestroSession(ensemble) {
1138
+ const workflowId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
1139
+ const sessionInput = {
1140
+ metadata: {
1141
+ playerId: 'maestro',
1142
+ ensemble,
1143
+ hostname: 'dashboard',
1144
+ workDir: process.cwd(),
1145
+ isConductor: false,
1146
+ agentType: 'claude',
1147
+ playerType: 'maestro',
1148
+ playerTypeDescription: 'TUI dashboard — human operator interface',
1149
+ },
1150
+ part: 'Dashboard interface (human operator)',
1151
+ disableStaleDetection: true,
1152
+ };
1153
+ try {
1154
+ const wfHandle = await client.workflow.start('agentSessionWorkflow', {
1155
+ workflowId,
1156
+ taskQueue: 'agent-tempo',
1157
+ args: [sessionInput],
1158
+ workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
1159
+ workflowExecutionTimeout: '24 hours',
1160
+ searchAttributes: {
1161
+ AgentTempoHostname: ['dashboard'],
1162
+ AgentTempoEnsemble: [ensemble],
1163
+ AgentTempoPlayerId: ['maestro'],
1164
+ AgentTempoPlayerType: ['maestro'],
1165
+ },
1166
+ });
1167
+ console.error(`[tui:client] Maestro session started: ${wfHandle.workflowId}`);
1168
+ // Also ensure the per-ensemble Maestro hub workflow exists.
1169
+ // Without this, getEnsembleChat returns empty when the hub wasn't
1170
+ // previously created by a CLI command.
1171
+ const maestroHubId = (0, config_1.maestroWorkflowId)(ensemble);
1172
+ try {
1173
+ await client.workflow.start('agentMaestroWorkflow', {
1174
+ workflowId: maestroHubId,
1175
+ taskQueue: 'agent-tempo',
1176
+ args: [{ ensemble }],
1177
+ workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
1178
+ searchAttributes: {
1179
+ AgentTempoEnsemble: [ensemble],
1180
+ },
1181
+ });
1182
+ console.error(`[tui:client] Maestro hub ensured: ${maestroHubId}`);
1183
+ }
1184
+ catch {
1185
+ // Maestro hub is non-critical — log but don't fail
1186
+ console.error(`[tui:client] Maestro hub start skipped (may already exist): ${maestroHubId}`);
1187
+ }
1188
+ return wfHandle.workflowId;
1189
+ }
1190
+ catch (err) {
1191
+ console.error('[tui:client] Failed to start maestro session:', err);
1192
+ throw err;
1193
+ }
1194
+ },
1195
+ async sendAsMaestro(ensemble, targetPlayer, text) {
1196
+ // Resolve target player workflow via search attributes
1197
+ const query = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running" AND AgentTempoEnsemble = "${sanitizeQueryValue(ensemble)}" AND AgentTempoPlayerId = "${sanitizeQueryValue(targetPlayer)}"`;
1198
+ let targetHandle;
1199
+ for await (const wf of client.workflow.list({ query })) {
1200
+ targetHandle = handle(wf.workflowId);
1201
+ break;
1202
+ }
1203
+ if (!targetHandle) {
1204
+ throw new Error(`Player "${targetPlayer}" not found in ensemble "${ensemble}"`);
1205
+ }
1206
+ // Signal the target with the message
1207
+ await targetHandle.signal('receiveMessage', { from: 'maestro', text, isMaestro: true });
1208
+ // Record outbound on maestro's own workflow
1209
+ const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
1210
+ try {
1211
+ const maestroHandle = handle(maestroId);
1212
+ await maestroHandle.signal('recordSentMessage', { to: targetPlayer, text });
1213
+ }
1214
+ catch {
1215
+ // Best-effort — maestro workflow may not exist yet
1216
+ }
1217
+ },
1218
+ async getMaestroMessages(ensemble) {
1219
+ const maestroId = (0, config_1.sessionWorkflowId)(ensemble, 'maestro');
1220
+ try {
1221
+ const h = handle(maestroId);
1222
+ // #433: unbounded (3× below) — justified, `getMaestroMessages`
1223
+ // is not reachable from `buildEnsembleSnapshot` (snapshot
1224
+ // surfaces ensemble-level chat via the bounded `getEnsembleChat`,
1225
+ // not the maestro session's per-message log). Called on explicit
1226
+ // operator fetch from the MCP `recall`/CLI inspect surfaces.
1227
+ // Query received messages (allMessages preferred, pendingMessages fallback)
1228
+ let received;
1229
+ try {
1230
+ received = await h.query('allMessages');
1231
+ }
1232
+ catch {
1233
+ received = await h.query('pendingMessages');
1234
+ }
1235
+ // Auto-mark undelivered messages as delivered (maestro has no listener)
1236
+ const undeliveredIds = received.filter(m => !m.delivered).map(m => m.id);
1237
+ if (undeliveredIds.length > 0) {
1238
+ try {
1239
+ await h.signal('markDelivered', undeliveredIds);
1240
+ }
1241
+ catch {
1242
+ // Best-effort
1243
+ }
1244
+ }
1245
+ // Query sent messages
1246
+ let sent;
1247
+ try {
1248
+ sent = await h.query('allSentMessages');
1249
+ }
1250
+ catch {
1251
+ sent = [];
1252
+ }
1253
+ return { received, sent };
1254
+ }
1255
+ catch {
1256
+ return { received: [], sent: [] };
1257
+ }
1258
+ },
1259
+ };
1260
+ }