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,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnsembleNotFoundError = exports.SNAPSHOT_CHAT_LIMIT = void 0;
4
+ exports.toPlayerSummaryV1 = toPlayerSummaryV1;
5
+ exports.buildEnsembleSnapshot = buildEnsembleSnapshot;
6
+ const types_1 = require("../types");
7
+ const event_types_1 = require("./event-types");
8
+ /**
9
+ * Runtime type guard — `MaestroPlayerInfo.agentType` is intentionally typed
10
+ * as open `string` so a forked daemon advertising a never-shipped adapter
11
+ * (e.g. `'gemini'`) doesn't crash the projection at the type level. The
12
+ * guard narrows the open-string input to the closed wire union before it
13
+ * reaches `PlayerSummaryV1`. Anything not in `AGENT_TYPES` (the canonical
14
+ * source-of-truth list at `src/types.ts`) falls back to `'claude'` in the
15
+ * caller — same defensive default as pre-#535, just over a wider whitelist.
16
+ */
17
+ function isWireAgentType(s) {
18
+ return types_1.AGENT_TYPES.includes(s);
19
+ }
20
+ /**
21
+ * Maximum chat messages embedded in the snapshot. Larger paging is left
22
+ * to the explicit `getEnsembleChat` query / `recall` tool — the snapshot
23
+ * is a steady-state view, not a paginator.
24
+ */
25
+ exports.SNAPSHOT_CHAT_LIMIT = 50;
26
+ /** Thrown when the ensemble doesn't exist; route handler maps to 404. */
27
+ class EnsembleNotFoundError extends Error {
28
+ ensemble;
29
+ constructor(ensemble) {
30
+ super(`ensemble not found: ${ensemble}`);
31
+ this.ensemble = ensemble;
32
+ this.name = 'EnsembleNotFoundError';
33
+ }
34
+ }
35
+ exports.EnsembleNotFoundError = EnsembleNotFoundError;
36
+ /**
37
+ * Fan-out helper — run all client queries in parallel and tolerate
38
+ * per-query failures (an empty array / `false` is the right fallback for
39
+ * every soft-failure case the existing TempoClient already returns).
40
+ */
41
+ async function fanOut(queries) {
42
+ const keys = Object.keys(queries);
43
+ const results = await Promise.all(keys.map((key) => queries[key]().catch(() => undefined)));
44
+ const out = {};
45
+ keys.forEach((key, i) => {
46
+ out[key] = results[i];
47
+ });
48
+ return out;
49
+ }
50
+ /**
51
+ * Project a `MaestroPlayerInfo` into the wire-stable `PlayerSummaryV1`.
52
+ * Drops fields that aren't part of the v1 contract; passes `agentType`
53
+ * through verbatim — the wire union mirrors {@link AgentType} from
54
+ * `src/types.ts`, so every shipped adapter projects to its own label
55
+ * (#535). Pre-#535 the wire union was closed at `'claude' | 'copilot' |
56
+ * 'mock'` and headless adapters were coerced to `'claude'`, which made
57
+ * them indistinguishable from interactive Claude Code players in the
58
+ * dashboard. The union expansion is additive per the §6 stability rule
59
+ * in `event-types.ts` — no `/v1/` → `/v2/` bump.
60
+ *
61
+ * `wireMeta` is the session-level projection from
62
+ * `TempoClient.getPlayerWireMeta` (Issue #399 W2). Pass `null` (or omit)
63
+ * when the session workflow couldn't be queried — the projection then
64
+ * carries no `runId` / `messaging` / `lease` fields and the dashboard
65
+ * renders `—` placeholders.
66
+ *
67
+ * `activityCount` passes through from `MaestroPlayerInfo` (Q5.6).
68
+ * `MaestroPlayerInfo.lastActivityAt` maps to the wire field
69
+ * `PlayerSummaryV1.lastHeartbeatAt` — same data, rename happens at the
70
+ * wire boundary so consumers read one canonical name.
71
+ */
72
+ function toPlayerSummaryV1(p, wireMeta = null) {
73
+ // `MaestroPlayerInfo.agentType` is intentionally open (`string`) so a
74
+ // forked daemon advertising a never-shipped adapter doesn't crash the
75
+ // type system here. `isWireAgentType` narrows it against `AGENT_TYPES`
76
+ // (the canonical source-of-truth list); anything outside falls back to
77
+ // `'claude'` — same defensive default as pre-#535, just enforced over
78
+ // a wider whitelist. The drift detector in `test/snapshot.test.ts`
79
+ // asserts the wire union mirrors `AgentType`, so a future shipped
80
+ // adapter missing from one of the two surfaces fails CI.
81
+ const agentType = isWireAgentType(p.agentType) ? p.agentType : 'claude';
82
+ return {
83
+ playerId: p.playerId,
84
+ ensemble: p.ensemble,
85
+ hostname: p.hostname,
86
+ isConductor: p.isConductor,
87
+ agentType,
88
+ ...(p.playerType !== undefined ? { playerType: p.playerType } : {}),
89
+ ...(p.phase !== undefined ? { phase: p.phase } : {}),
90
+ part: p.part ?? '',
91
+ workDir: p.workDir ?? '',
92
+ ...(p.gitBranch !== undefined ? { gitBranch: p.gitBranch } : {}),
93
+ // Issue #399 Q5.6 — pass-through from MaestroPlayerInfo.
94
+ ...(p.activityCount !== undefined ? { activityCount: p.activityCount } : {}),
95
+ // Wire-name rename: source `lastActivityAt` → contract `lastHeartbeatAt`
96
+ // (#389 R3.P1.4). Aggregate's phase-change diff reads the wire name.
97
+ ...(p.lastActivityAt !== undefined ? { lastHeartbeatAt: p.lastActivityAt } : {}),
98
+ // Issue #399 W2 — session-query fan-out merged in when reachable.
99
+ ...(wireMeta?.runId !== undefined ? { runId: wireMeta.runId } : {}),
100
+ ...(wireMeta?.messaging !== undefined ? { messaging: wireMeta.messaging } : {}),
101
+ ...(wireMeta?.lease !== undefined ? { lease: wireMeta.lease } : {}),
102
+ };
103
+ }
104
+ /**
105
+ * Build the `/v1/state/:ensemble` payload. Throws
106
+ * {@link EnsembleNotFoundError} when the requested ensemble has no live
107
+ * workflows; route handler turns that into a 404.
108
+ */
109
+ async function buildEnsembleSnapshot(client, ensemble, opts = {}) {
110
+ // Existence gate — `listEnsembles` is fast (single workflow.list scan)
111
+ // and returns the canonical state classification (online/paused/offline).
112
+ const ensembles = await client.listEnsembles().catch(() => []);
113
+ const summary = ensembles.find((e) => e.name === ensemble);
114
+ if (!summary)
115
+ throw new EnsembleNotFoundError(ensemble);
116
+ // Fan-out the rest in parallel. Each soft-fails to a sane default — the
117
+ // PR-1 snapshot must NEVER 500 just because one downstream query glitched.
118
+ // Issue #399 DB1a adds `meta` (4 maestro queries) to the ensemble-level
119
+ // fan-out; per-player wire-meta is fanned out below once `players`
120
+ // resolves so we know which session ids to query.
121
+ const fanned = await fanOut({
122
+ players: () => client.getPlayers(ensemble),
123
+ chat: () => client.getEnsembleChat(ensemble, 0, exports.SNAPSHOT_CHAT_LIMIT),
124
+ schedules: () => client.getSchedules(ensemble),
125
+ paused: () => client.isMaestroPaused(ensemble),
126
+ held: () => client.isAnySessionHeld(ensemble),
127
+ hosts: () => client.listHosts(),
128
+ meta: () => client.getEnsembleMeta(ensemble),
129
+ });
130
+ // Issue #399 W2 — fan-out 3 session queries per player in parallel.
131
+ // For a 12-player ensemble that's 36 queries; running them concurrently
132
+ // keeps total snapshot latency bounded by the slowest single session
133
+ // query, not the sum. `getPlayerWireMeta` returns `null` when the
134
+ // session workflow can't be reached — `toPlayerSummaryV1` gracefully
135
+ // omits the wire-meta fields in that case.
136
+ const playerInfos = fanned.players ?? [];
137
+ const wireMetas = await Promise.all(playerInfos.map((p) => client.getPlayerWireMeta(ensemble, p.playerId).catch(() => null)));
138
+ const players = playerInfos.map((p, i) => toPlayerSummaryV1(p, wireMetas[i]));
139
+ const chat = fanned.chat ?? { messages: [], total: 0, hasMore: false, hasConductor: false };
140
+ const schedules = fanned.schedules ?? [];
141
+ const paused = fanned.paused === true;
142
+ const held = fanned.held === true;
143
+ const hostProfiles = {};
144
+ for (const h of fanned.hosts ?? []) {
145
+ if (h.profile)
146
+ hostProfiles[h.hostname] = h.profile;
147
+ }
148
+ // Issue #399 W1 — sentinel defaults when the maestro fan-out soft-failed
149
+ // entirely. `getEnsembleMeta` already soft-fails individual queries to
150
+ // their sentinels; this branch covers the outer `fanOut` swallow when
151
+ // the whole call rejected.
152
+ const meta = fanned.meta ?? {
153
+ description: '',
154
+ startedAt: '',
155
+ currentBpm: 0,
156
+ tempoSeries: [],
157
+ };
158
+ const capturedAt = (opts.now?.() ?? new Date()).toISOString();
159
+ return {
160
+ v: 1,
161
+ ensemble,
162
+ capturedAt,
163
+ lastEventId: event_types_1.PR1_SENTINEL_EVENT_ID,
164
+ state: summary.state ?? 'online',
165
+ hasConductor: summary.hasConductor,
166
+ flags: { paused, held },
167
+ players,
168
+ schedules,
169
+ chat: {
170
+ messages: chat.messages,
171
+ total: chat.total,
172
+ hasMore: chat.hasMore,
173
+ },
174
+ hostProfiles,
175
+ description: meta.description,
176
+ startedAt: meta.startedAt,
177
+ currentBpm: meta.currentBpm,
178
+ tempoSeries: meta.tempoSeries,
179
+ };
180
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * SSE handler — wires `EnsembleEventBus` to the HTTP socket.
3
+ *
4
+ * **Responsibilities**:
5
+ * - Frame `BusEvent` into SSE `id:` / `event:` / `data:` lines (§5).
6
+ * - Decide the §7.2 connection-flow branch:
7
+ * - no `Last-Event-ID` → emit `snapshot` (per-ensemble only), then live tail
8
+ * - epoch mismatch → emit `gap` with `reason: 'epoch-mismatch'`, live tail
9
+ * - epoch match, `seq < ringStart` → emit `gap` with `reason: 'overflow'`, live tail
10
+ * - epoch match, `seq >= ringStart` → replay `[seq+1 … newest]`, live tail
11
+ * - Per-connection 1 MiB write-buffer cap (§7.3) with destroy-on-overflow.
12
+ * - Process-wide connection cap (`AGENT_TEMPO_SSE_MAX_CONNECTIONS`,
13
+ * default 100); over-cap → `503 Service Unavailable`, `Retry-After: 5`.
14
+ * - Cancellation: socket close drops the subscription within one
15
+ * event-loop tick.
16
+ */
17
+ import type { IncomingMessage, ServerResponse } from 'http';
18
+ import type { TempoClient } from '../client/interface';
19
+ import { EnsembleEventBus, type BusEvent, type EventBus, type Subscription, type TopicCategory } from './event-bus';
20
+ /** Default per-process SSE connection cap. */
21
+ export declare const DEFAULT_MAX_CONNECTIONS = 100;
22
+ /** Per-connection write-buffer ceiling (§7.3). */
23
+ export declare const PER_CONNECTION_BUFFER_LIMIT: number;
24
+ /** Counts open SSE connections. Exported for `/v1/health.subscriberCount`. */
25
+ export declare class ConnectionCap {
26
+ readonly limit: number;
27
+ private current;
28
+ constructor(limit?: number);
29
+ size(): number;
30
+ /**
31
+ * Try to take a slot. Returns `true` when the connection is
32
+ * accepted; `false` when over the cap.
33
+ */
34
+ acquire(): boolean;
35
+ release(): void;
36
+ }
37
+ /**
38
+ * SSE frame format (§5). Returns the byte buffer that goes on the wire.
39
+ *
40
+ * Multi-line payloads are split on `\n` so each becomes its own
41
+ * `data:` line per the WHATWG spec — but in practice the daemon
42
+ * always emits single-line JSON, so this branch is defensive.
43
+ */
44
+ export declare function frameSseEvent(event: BusEvent): Buffer;
45
+ /**
46
+ * Same on-the-wire shape as {@link frameSseEvent} but takes the wire
47
+ * fields directly — for callers (e.g. fixture mode) that don't have a
48
+ * full `BusEvent` to project from.
49
+ */
50
+ export declare function frameSseEventData(eventId: string, type: string, payload: unknown): Buffer;
51
+ /**
52
+ * Frame a synthetic event-id-less prelude — `: <comment>\n\n`. Used for
53
+ * the initial keepalive nudge some intermediaries need before the
54
+ * first real event.
55
+ */
56
+ export declare function frameSseComment(comment: string): Buffer;
57
+ /**
58
+ * Write the §1 SSE response headers and the initial keepalive comment.
59
+ * Shared by {@link handleSseRequest} and the fixture-mode handler so
60
+ * the wire-level handshake stays in one place.
61
+ */
62
+ export declare function openSseResponse(res: ServerResponse, comment?: string): void;
63
+ /** Parse `?topics=phase,chat,...` query string into a Set, or `undefined`. */
64
+ export declare function parseTopicQuery(raw: string | string[] | undefined): Set<TopicCategory> | undefined;
65
+ export interface HandleSseOptions {
66
+ client: TempoClient;
67
+ bus: EventBus;
68
+ /**
69
+ * `true` for `/v1/events/:ensemble` — emits `snapshot` on fresh
70
+ * connect. `false` for `/v1/events` global stream which has no
71
+ * snapshot semantics (§6 table).
72
+ */
73
+ emitSnapshot: boolean;
74
+ /** Ensemble name when `emitSnapshot` is true; ignored otherwise. */
75
+ ensemble?: string;
76
+ /** Process-wide connection cap. */
77
+ cap: ConnectionCap;
78
+ /** Test seam for the per-connection write buffer ceiling. */
79
+ bufferLimit?: number;
80
+ }
81
+ /**
82
+ * Handle an SSE request. Long-lived — resolves only when the client
83
+ * disconnects, the bus closes, or the per-connection buffer overflows.
84
+ */
85
+ export declare function handleSseRequest(req: IncomingMessage, res: ServerResponse, opts: HandleSseOptions): Promise<void>;
86
+ /** Type guard re-export to satisfy TS without circular refs. */
87
+ export type { EnsembleEventBus, Subscription };
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConnectionCap = exports.PER_CONNECTION_BUFFER_LIMIT = exports.DEFAULT_MAX_CONNECTIONS = void 0;
4
+ exports.frameSseEvent = frameSseEvent;
5
+ exports.frameSseEventData = frameSseEventData;
6
+ exports.frameSseComment = frameSseComment;
7
+ exports.openSseResponse = openSseResponse;
8
+ exports.parseTopicQuery = parseTopicQuery;
9
+ exports.handleSseRequest = handleSseRequest;
10
+ const snapshot_1 = require("./snapshot");
11
+ const event_id_1 = require("./event-id");
12
+ /** Default per-process SSE connection cap. */
13
+ exports.DEFAULT_MAX_CONNECTIONS = 100;
14
+ /** Per-connection write-buffer ceiling (§7.3). */
15
+ exports.PER_CONNECTION_BUFFER_LIMIT = 1 * 1024 * 1024;
16
+ const log = (...args) => console.error('[agent-tempo:sse]', ...args);
17
+ /** Counts open SSE connections. Exported for `/v1/health.subscriberCount`. */
18
+ class ConnectionCap {
19
+ limit;
20
+ current = 0;
21
+ constructor(limit = exports.DEFAULT_MAX_CONNECTIONS) {
22
+ this.limit = limit;
23
+ }
24
+ size() { return this.current; }
25
+ /**
26
+ * Try to take a slot. Returns `true` when the connection is
27
+ * accepted; `false` when over the cap.
28
+ */
29
+ acquire() {
30
+ if (this.current >= this.limit)
31
+ return false;
32
+ this.current++;
33
+ return true;
34
+ }
35
+ release() {
36
+ if (this.current > 0)
37
+ this.current--;
38
+ }
39
+ }
40
+ exports.ConnectionCap = ConnectionCap;
41
+ /**
42
+ * SSE frame format (§5). Returns the byte buffer that goes on the wire.
43
+ *
44
+ * Multi-line payloads are split on `\n` so each becomes its own
45
+ * `data:` line per the WHATWG spec — but in practice the daemon
46
+ * always emits single-line JSON, so this branch is defensive.
47
+ */
48
+ function frameSseEvent(event) {
49
+ return frameSseEventData(event.eventId, event.type, event.payload);
50
+ }
51
+ /**
52
+ * Same on-the-wire shape as {@link frameSseEvent} but takes the wire
53
+ * fields directly — for callers (e.g. fixture mode) that don't have a
54
+ * full `BusEvent` to project from.
55
+ */
56
+ function frameSseEventData(eventId, type, payload) {
57
+ const lines = [
58
+ `id: ${eventId}`,
59
+ `event: ${type}`,
60
+ ];
61
+ const data = JSON.stringify({ v: 1, eventId, payload });
62
+ // Single-line JSON — but defend against line breaks in payloads.
63
+ for (const piece of data.split('\n'))
64
+ lines.push(`data: ${piece}`);
65
+ return Buffer.from(lines.join('\n') + '\n\n', 'utf8');
66
+ }
67
+ /**
68
+ * Frame a synthetic event-id-less prelude — `: <comment>\n\n`. Used for
69
+ * the initial keepalive nudge some intermediaries need before the
70
+ * first real event.
71
+ */
72
+ function frameSseComment(comment) {
73
+ return Buffer.from(`: ${comment}\n\n`, 'utf8');
74
+ }
75
+ /**
76
+ * Write the §1 SSE response headers and the initial keepalive comment.
77
+ * Shared by {@link handleSseRequest} and the fixture-mode handler so
78
+ * the wire-level handshake stays in one place.
79
+ */
80
+ function openSseResponse(res, comment = 'agent-tempo SSE') {
81
+ res.writeHead(200, {
82
+ 'Content-Type': 'text/event-stream; charset=utf-8',
83
+ 'Cache-Control': 'no-store',
84
+ Connection: 'keep-alive',
85
+ 'X-Accel-Buffering': 'no', // disable buffering on nginx + friends
86
+ });
87
+ res.flushHeaders?.();
88
+ res.write(frameSseComment(comment));
89
+ }
90
+ /** Parse `?topics=phase,chat,...` query string into a Set, or `undefined`. */
91
+ function parseTopicQuery(raw) {
92
+ if (!raw)
93
+ return undefined;
94
+ const value = Array.isArray(raw) ? raw[0] : raw;
95
+ const known = new Set(['phase', 'chat', 'flags', 'schedules', 'heartbeat']);
96
+ const out = new Set();
97
+ for (const tok of value.split(',').map((s) => s.trim()).filter(Boolean)) {
98
+ if (known.has(tok))
99
+ out.add(tok);
100
+ }
101
+ return out.size > 0 ? out : undefined;
102
+ }
103
+ /** Pull a single string from a possibly-array header. */
104
+ function headerString(v) {
105
+ if (Array.isArray(v))
106
+ return v[0];
107
+ return v;
108
+ }
109
+ /**
110
+ * Handle an SSE request. Long-lived — resolves only when the client
111
+ * disconnects, the bus closes, or the per-connection buffer overflows.
112
+ */
113
+ async function handleSseRequest(req, res, opts) {
114
+ // Connection cap.
115
+ if (!opts.cap.acquire()) {
116
+ res.writeHead(503, {
117
+ 'Content-Type': 'application/json; charset=utf-8',
118
+ 'Retry-After': '5',
119
+ });
120
+ res.end(JSON.stringify({ error: 'connection-cap-exceeded' }));
121
+ return;
122
+ }
123
+ // Always release the cap on exit.
124
+ let capReleased = false;
125
+ const releaseCap = () => {
126
+ if (!capReleased) {
127
+ capReleased = true;
128
+ opts.cap.release();
129
+ }
130
+ };
131
+ // Open SSE response (headers per §1 + initial keepalive comment).
132
+ openSseResponse(res);
133
+ // Determine §7.2 entry branch.
134
+ const lastEventIdRaw = headerString(req.headers['last-event-id']);
135
+ const requested = (0, event_id_1.parseEventId)(lastEventIdRaw);
136
+ const url = new URL(req.url ?? '/', 'http://localhost');
137
+ const topics = parseTopicQuery(url.searchParams.get('topics') ?? undefined);
138
+ // Subscribe — afterSeq is determined by branch decisions below. The
139
+ // bus's subscribe call replays `seq > afterSeq` events from the ring.
140
+ let afterSeq;
141
+ let preludeEvents = [];
142
+ if (requested) {
143
+ if (requested.epoch !== opts.bus.bootEpoch) {
144
+ // Epoch mismatch → gap.
145
+ preludeEvents.push(synthEvent(opts.bus, 'gap', {
146
+ from: lastEventIdRaw ?? '',
147
+ to: makeIdToken(opts.bus, opts.bus.nextSeq()),
148
+ reason: 'epoch-mismatch',
149
+ }));
150
+ // Don't replay — client will re-fetch /v1/state and reconnect.
151
+ afterSeq = undefined;
152
+ }
153
+ else {
154
+ const ringStart = opts.bus.oldestSeq();
155
+ if (ringStart === null || requested.seq + 1 < ringStart) {
156
+ // Overflow → gap. `ringStart === null` happens when the bus
157
+ // has emitted nothing yet — semantically "I have no events
158
+ // to replay you" rather than the strict overflow case
159
+ // (client predates the ring's oldest entry). The wire spec
160
+ // §6 only defines two `gap` reasons (`epoch-mismatch` and
161
+ // `overflow`), so we use `overflow` for both flavors. The
162
+ // recovery path is identical: re-fetch `/v1/state` and
163
+ // reconnect with the snapshot's `lastEventId`. PR #324
164
+ // review nit — flagged for a possible §6 spec extension
165
+ // (`reason: 'empty-ring'`) if a future PR-3 consumer can use
166
+ // the distinction; for now collapsing keeps the wire stable.
167
+ preludeEvents.push(synthEvent(opts.bus, 'gap', {
168
+ from: lastEventIdRaw ?? '',
169
+ to: makeIdToken(opts.bus, opts.bus.nextSeq()),
170
+ reason: 'overflow',
171
+ }));
172
+ afterSeq = undefined;
173
+ }
174
+ else {
175
+ // Replay range — bus.subscribe will pull from the ring.
176
+ afterSeq = requested.seq;
177
+ }
178
+ }
179
+ }
180
+ else if (opts.emitSnapshot) {
181
+ // No Last-Event-ID + per-ensemble stream → emit snapshot.
182
+ if (!opts.ensemble) {
183
+ res.end();
184
+ releaseCap();
185
+ throw new Error('handleSseRequest: ensemble required when emitSnapshot=true');
186
+ }
187
+ try {
188
+ const snap = await (0, snapshot_1.buildEnsembleSnapshot)(opts.client, opts.ensemble);
189
+ preludeEvents.push(synthEvent(opts.bus, 'snapshot', snap));
190
+ }
191
+ catch (err) {
192
+ if (err instanceof snapshot_1.EnsembleNotFoundError) {
193
+ // The route handler validates ensemble existence before calling
194
+ // this, but the ensemble could go away between resolution and
195
+ // snapshot fetch — return a 404-equivalent SSE close with a
196
+ // helpful body comment.
197
+ res.write(frameSseComment(`ensemble-not-found ${opts.ensemble}`));
198
+ res.end();
199
+ releaseCap();
200
+ return;
201
+ }
202
+ log('snapshot fetch failed:', err instanceof Error ? err.message : err);
203
+ // Non-fatal — let the bus do the live tail; consumer is no worse off.
204
+ }
205
+ }
206
+ const sub = opts.bus.subscribe({ ...(afterSeq !== undefined ? { afterSeq } : {}), ...(topics ? { topics } : {}) });
207
+ let closed = false;
208
+ const close = () => {
209
+ if (closed)
210
+ return;
211
+ closed = true;
212
+ try {
213
+ sub.close();
214
+ }
215
+ catch { /* ignore */ }
216
+ try {
217
+ res.end();
218
+ }
219
+ catch { /* ignore */ }
220
+ releaseCap();
221
+ };
222
+ // Bind socket lifecycle. `req.on('close')` covers TCP drop + abort.
223
+ req.on('close', close);
224
+ res.on('close', close);
225
+ // Stream loop.
226
+ try {
227
+ // Prelude (snapshot / gap) goes first.
228
+ for (const ev of preludeEvents) {
229
+ if (closed)
230
+ return;
231
+ if (!writeOrDrop(res, ev, opts.bufferLimit ?? exports.PER_CONNECTION_BUFFER_LIMIT, close))
232
+ return;
233
+ }
234
+ for await (const ev of sub) {
235
+ if (closed)
236
+ return;
237
+ if (!writeOrDrop(res, ev, opts.bufferLimit ?? exports.PER_CONNECTION_BUFFER_LIMIT, close))
238
+ return;
239
+ }
240
+ }
241
+ finally {
242
+ close();
243
+ }
244
+ }
245
+ /** Per-event write with §7.3 backpressure. Returns `false` when the connection was destroyed. */
246
+ function writeOrDrop(res, event, bufferLimit, close) {
247
+ const payload = frameSseEvent(event);
248
+ // Track per-socket buffered bytes via Node's `writableLength`.
249
+ const socket = res.socket;
250
+ if (socket && socket.writableLength + payload.length > bufferLimit) {
251
+ log('connection write buffer exceeded — dropping subscriber');
252
+ try {
253
+ socket.destroy();
254
+ }
255
+ catch { /* ignore */ }
256
+ close();
257
+ return false;
258
+ }
259
+ res.write(payload);
260
+ return true;
261
+ }
262
+ /**
263
+ * Build a synthetic `BusEvent` not allocated by the bus's `emit`
264
+ * pipeline — used only for the connection prelude (snapshot / gap)
265
+ * which is per-connection rather than per-bus. Allocates a sentinel
266
+ * `eventId` based on the bus's current `nextSeq` minus one so the
267
+ * client's local `Last-Event-ID` tracking stays internally
268
+ * consistent without polluting the ring.
269
+ *
270
+ * NOTE: the eventId on a synthetic event is for client-side ordering
271
+ * only — the daemon will never reuse this id from its allocator.
272
+ * Per §7.2 the snapshot/gap re-bridge means the client always
273
+ * follows up with a fresh `/v1/state/:ensemble` fetch on `gap` and
274
+ * normalizes its Last-Event-ID against the snapshot's `lastEventId`.
275
+ */
276
+ function synthEvent(bus, type, payload) {
277
+ // Pick an id strictly less than the bus's next allocation so it
278
+ // sorts before any real event. Synthetic prelude events are an
279
+ // implementation detail of the per-connection wire and are never
280
+ // expected to be addressed via Last-Event-ID resume.
281
+ const seq = Math.max(0, bus.nextSeq() - 1);
282
+ const tuple = { epoch: bus.bootEpoch, seq };
283
+ return {
284
+ eventId: `${tuple.epoch}:${tuple.seq}`,
285
+ tuple,
286
+ type,
287
+ payload,
288
+ emittedAt: Date.now(),
289
+ bufferable: false,
290
+ };
291
+ }
292
+ function makeIdToken(bus, seq) {
293
+ return `${bus.bootEpoch}:${seq}`;
294
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Daemon HTTP write surface — PR-7a of #340.
3
+ *
4
+ * Adds five POST routes under `/v1/ensembles/:ensemble/{cue, pause,
5
+ * play, release, recruit}`. Each handler is a thin shim over the
6
+ * existing {@link TempoClient} method the daemon already has in scope
7
+ * (the same `ctx.client` used for snapshots).
8
+ *
9
+ * **Why now**: PR-1 → PR-6 of the dashboard shipped a read-only
10
+ * surface; PR-7b wires the dashboard's disabled CTAs to real submit
11
+ * handlers. Those handlers need an HTTP write API — until now the
12
+ * daemon was GET-only by design. See the full background in
13
+ * `docs/SSE-PROTOCOL.md` § 11b.
14
+ *
15
+ * **Auth posture** matches the read side:
16
+ * - Loopback bind + no `Origin` header → no auth (TUI/CLI parity; the
17
+ * TUI already writes via Temporal directly anyway, so loopback-no-auth
18
+ * is equivalent risk).
19
+ * - Non-loopback bind OR cross-origin browser → bearer required.
20
+ *
21
+ * **Body validation** is strict — every field shape is checked before
22
+ * the handler reaches the Temporal layer. Bad bodies fast-fail with
23
+ * 400; the underlying Temporal calls aren't asked to validate things
24
+ * the HTTP layer can catch.
25
+ *
26
+ * **Error mapping**: `Error('No session found …')` → 404 not-found;
27
+ * any other thrown `Error` → 500 (logged at the dispatcher).
28
+ */
29
+ import type { IncomingMessage, ServerResponse } from 'http';
30
+ import type { TempoClient } from '../client/interface';
31
+ import { WRITE_BODY_MAX } from './body';
32
+ export { WRITE_BODY_MAX };
33
+ /**
34
+ * Names of the write actions exposed under `/v1/ensembles/:ensemble/<action>`.
35
+ *
36
+ * Two semantic groups, kept in this order so the table reads top-to-bottom
37
+ * by surface intent:
38
+ * - **Ensemble-scoped** (cue / pause / play / release / recruit) — the
39
+ * original PR-7a #340 surface; bodies don't carry `playerId`.
40
+ * - **Per-player destructive** (restart / destroy / detach / recall) —
41
+ * added so the dashboard's PlayerDetail action row can wire to live
42
+ * mutations. Bodies are uniform `{ playerId, reason? }` (plus per-action
43
+ * extras); the ensemble lives in the URL.
44
+ */
45
+ export declare const WRITE_ACTIONS: readonly ["cue", "pause", "play", "release", "recruit", "restart", "destroy", "detach", "recall"];
46
+ export type WriteAction = (typeof WRITE_ACTIONS)[number];
47
+ /** Type guard — narrows an arbitrary string to a known `WriteAction`. */
48
+ export declare function isWriteAction(s: string): s is WriteAction;
49
+ /**
50
+ * Top-level dispatch — called from `server.ts` when the URL matches
51
+ * `/v1/ensembles/:ensemble/<action>` with a known `<action>`. The
52
+ * caller has already bearer/CORS-gated the request; this function
53
+ * trusts the gates and focuses on body parsing + per-action routing.
54
+ */
55
+ export declare function handleWriteRoute(req: IncomingMessage, res: ServerResponse, client: TempoClient, ensemble: string, action: WriteAction): Promise<void>;