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