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,1375 @@
|
|
|
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.COMMANDS = exports.SUBCOMMAND_MAP = exports.PLAYER_PARAM_COMMANDS = exports.filterPlayerNames = exports.filterPaletteCommands = exports.classifyPaletteInput = exports.parseCommand = exports.tokenize = void 0;
|
|
37
|
+
exports.commitNotification = commitNotification;
|
|
38
|
+
exports.parseRecallFlags = parseRecallFlags;
|
|
39
|
+
exports.formatTimestamp = formatTimestamp;
|
|
40
|
+
exports.getCommandNames = getCommandNames;
|
|
41
|
+
exports.isValidCommand = isValidCommand;
|
|
42
|
+
exports.formatHelpSummary = formatHelpSummary;
|
|
43
|
+
/**
|
|
44
|
+
* Slash command parser and registry for the TUI shell.
|
|
45
|
+
* Parses user input into structured commands and provides handler
|
|
46
|
+
* implementations for each command.
|
|
47
|
+
*
|
|
48
|
+
* **#471/#472**: The pure helpers (`parseCommand`, `tokenize`,
|
|
49
|
+
* `classifyPaletteInput`, `filterPaletteCommands`, `filterPlayerNames`,
|
|
50
|
+
* `PLAYER_PARAM_COMMANDS`, `SUBCOMMAND_MAP`) live in `src/palette/` and
|
|
51
|
+
* are re-exported below so the TUI's surface stays unchanged. The
|
|
52
|
+
* dashboard imports the same helpers via `agent-tempo/palette`.
|
|
53
|
+
*/
|
|
54
|
+
const os_1 = require("os");
|
|
55
|
+
const store_1 = require("./store");
|
|
56
|
+
const platform_1 = require("./utils/platform");
|
|
57
|
+
const format_1 = require("./utils/format");
|
|
58
|
+
const saver_1 = require("../ensemble/saver");
|
|
59
|
+
const attachment_format_1 = require("../utils/attachment-format");
|
|
60
|
+
const format_hosts_1 = require("../utils/format-hosts");
|
|
61
|
+
const recall_format_1 = require("../utils/recall-format");
|
|
62
|
+
// ── Re-exports (#471/#472 — palette helpers moved to src/palette) ──
|
|
63
|
+
//
|
|
64
|
+
// These re-exports keep the TUI's pre-extraction surface intact. Existing
|
|
65
|
+
// callers (`PromptArea.tsx`, `tests/tui/palette.test.ts`,
|
|
66
|
+
// `tests/tui/commands.test.ts`) keep importing from `../commands` /
|
|
67
|
+
// `../../src/tui/commands` without change.
|
|
68
|
+
var palette_1 = require("../palette");
|
|
69
|
+
Object.defineProperty(exports, "tokenize", { enumerable: true, get: function () { return palette_1.tokenize; } });
|
|
70
|
+
Object.defineProperty(exports, "parseCommand", { enumerable: true, get: function () { return palette_1.parseCommand; } });
|
|
71
|
+
Object.defineProperty(exports, "classifyPaletteInput", { enumerable: true, get: function () { return palette_1.classifyPaletteInput; } });
|
|
72
|
+
Object.defineProperty(exports, "filterPaletteCommands", { enumerable: true, get: function () { return palette_1.filterPaletteCommands; } });
|
|
73
|
+
Object.defineProperty(exports, "filterPlayerNames", { enumerable: true, get: function () { return palette_1.filterPlayerNames; } });
|
|
74
|
+
Object.defineProperty(exports, "PLAYER_PARAM_COMMANDS", { enumerable: true, get: function () { return palette_1.PLAYER_PARAM_COMMANDS; } });
|
|
75
|
+
Object.defineProperty(exports, "SUBCOMMAND_MAP", { enumerable: true, get: function () { return palette_1.SUBCOMMAND_MAP; } });
|
|
76
|
+
// ── Parser ──
|
|
77
|
+
//
|
|
78
|
+
// `tokenize` and `parseCommand` moved to `src/palette` in #471/#472 and
|
|
79
|
+
// are re-exported above. They're imported into the TUI handler closures
|
|
80
|
+
// below via the same import surface (`from '../commands'`) so callers
|
|
81
|
+
// see no change.
|
|
82
|
+
// ── Static item helper ──
|
|
83
|
+
let _staticIdCounter = 0;
|
|
84
|
+
function nextId() {
|
|
85
|
+
return `cmd-${++_staticIdCounter}`;
|
|
86
|
+
}
|
|
87
|
+
function commitStatic(dispatch, type, content) {
|
|
88
|
+
dispatch({
|
|
89
|
+
type: 'COMMIT_STATIC',
|
|
90
|
+
item: { id: nextId(), type, content, timestamp: Date.now() },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// ── Bottom-pinned notifications (#306) ──
|
|
94
|
+
let _notificationIdCounter = 0;
|
|
95
|
+
function nextNotificationId() {
|
|
96
|
+
return ++_notificationIdCounter;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* #306: Dispatch a bottom-pinned notification — auto-expires after TTL
|
|
100
|
+
* (8s for errors, 5s for warn/info). Use for errors and warnings that
|
|
101
|
+
* must stay visible while other output streams by; regular activity
|
|
102
|
+
* logs still go through {@link commitStatic}.
|
|
103
|
+
*/
|
|
104
|
+
function commitNotification(dispatch, kind, content) {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
dispatch({
|
|
107
|
+
type: 'ADD_NOTIFICATION',
|
|
108
|
+
notification: {
|
|
109
|
+
id: nextNotificationId(),
|
|
110
|
+
kind,
|
|
111
|
+
content,
|
|
112
|
+
timestamp: now,
|
|
113
|
+
expiresAt: now + store_1.NOTIFICATION_TTL_MS[kind],
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// ── Handlers ──
|
|
118
|
+
/** /players — show interactive player picker. */
|
|
119
|
+
async function handlePlayers(_args, dispatch, _api) {
|
|
120
|
+
// Show picker overlay — data comes from store (already polled)
|
|
121
|
+
dispatch({ type: 'SHOW_PICKER', pickerType: 'players' });
|
|
122
|
+
}
|
|
123
|
+
/** /player <name> — show detailed player info. */
|
|
124
|
+
async function handlePlayer(args, dispatch, api, ctx) {
|
|
125
|
+
if (args.length === 0) {
|
|
126
|
+
// No args — show picker with navigate intent (App.tsx dispatches NAVIGATE_PLAYER on selection)
|
|
127
|
+
dispatch({ type: 'SHOW_PICKER', pickerType: 'players', intent: 'navigate' });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const target = args[0];
|
|
131
|
+
try {
|
|
132
|
+
// Verify the player exists
|
|
133
|
+
const ensembles = ctx.activeEnsemble
|
|
134
|
+
? [{ name: ctx.activeEnsemble }]
|
|
135
|
+
: await api.discoverEnsembles();
|
|
136
|
+
for (const ens of ensembles) {
|
|
137
|
+
const players = await api.getPlayers(ens.name);
|
|
138
|
+
const player = players.find(p => p.playerId === target);
|
|
139
|
+
if (!player)
|
|
140
|
+
continue;
|
|
141
|
+
// Navigate to the player detail view
|
|
142
|
+
dispatch({ type: 'NAVIGATE_PLAYER', playerId: target });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
commitNotification(dispatch, 'error', `Player "${target}" not found in any ensemble.`);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
commitNotification(dispatch, 'error', `Failed to get player info: ${err}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* /destroy <player> [reason] — request terminal-destroy confirmation.
|
|
153
|
+
*
|
|
154
|
+
* PR-H (#132): consolidates the legacy `/stop` slash command (which routed
|
|
155
|
+
* through `terminatePlayer` — raw Temporal terminate) and the no-confirm
|
|
156
|
+
* `/destroy` shipped in PR-D into one flow. The slash command always
|
|
157
|
+
* prompts y/N first; the yes-confirm handler in App.tsx routes through
|
|
158
|
+
* `TempoClient.destroy()` (V2 `destroyUpdate` via the outbox path) so the
|
|
159
|
+
* adapter's `isDestroyed` query sees the terminal state cleanly.
|
|
160
|
+
*
|
|
161
|
+
* The optional `reason` is concatenated from args[1..] and stashed on the
|
|
162
|
+
* action for the confirmation handler to forward to `destroy(reason)`.
|
|
163
|
+
*/
|
|
164
|
+
async function handleDestroy(args, dispatch, _api, ctx) {
|
|
165
|
+
if (args.length === 0) {
|
|
166
|
+
commitNotification(dispatch, 'error', 'Usage: /destroy <player|ensemble> [reason]');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const target = args[0];
|
|
170
|
+
// #306: Conductor is an ensemble invariant (#294) — it can't be destroyed
|
|
171
|
+
// standalone without leaving the ensemble in an invalid state (no conductor
|
|
172
|
+
// to accept cues, no coordination). Redirect the user to the correct verb
|
|
173
|
+
// instead of silently dispatching a confirmation the UI never renders.
|
|
174
|
+
if (target === 'conductor') {
|
|
175
|
+
commitNotification(dispatch, 'error', `✗ Cannot destroy the conductor — ensembles require one (see #294). ` +
|
|
176
|
+
`Use /shutdown to park this ensemble, /restart conductor to revive it, or ` +
|
|
177
|
+
`/destroy ${ctx.activeEnsemble ?? '<ensemble>'} to terminate the whole ensemble.`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Ensemble scope: target matches the active ensemble → typed-name
|
|
181
|
+
// confirmation. Player scope: any other target → y/N. The active-ensemble
|
|
182
|
+
// match is unambiguous inside a single-ensemble TUI session.
|
|
183
|
+
if (target === ctx.activeEnsemble) {
|
|
184
|
+
dispatch({ type: 'CONFIRM_ENSEMBLE_DESTROY', ensemble: target });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// #306: CONFIRM_STOP locks the input line awaiting y/N. The discoverability
|
|
188
|
+
// prompt was previously emitted to scrollback via commitStatic, but that
|
|
189
|
+
// line scrolled off-screen as new messages arrived — leaving the user
|
|
190
|
+
// staring at a frozen input. Replaced with a pinned render in App.tsx
|
|
191
|
+
// (`renderPinnedConfirmations`) that persists exactly as long as
|
|
192
|
+
// `state.confirmingStop` does (no TTL). The reason carried here surfaces
|
|
193
|
+
// in the pinned line too.
|
|
194
|
+
const reason = args.slice(1).join(' ') || undefined;
|
|
195
|
+
dispatch({ type: 'CONFIRM_STOP', player: target, ...(reason !== undefined ? { reason } : {}) });
|
|
196
|
+
}
|
|
197
|
+
/** /broadcast <message> — send a message to all active players in the current ensemble. */
|
|
198
|
+
async function handleBroadcast(args, dispatch, api, ctx) {
|
|
199
|
+
if (args.length === 0) {
|
|
200
|
+
commitNotification(dispatch, 'error', 'Usage: /broadcast <message>');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!ctx.activeEnsemble) {
|
|
204
|
+
commitNotification(dispatch, 'error', 'No active ensemble. Select one with /ensemble first.');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const message = args.join(' ');
|
|
208
|
+
try {
|
|
209
|
+
const players = await api.getPlayers(ctx.activeEnsemble);
|
|
210
|
+
let sent = 0;
|
|
211
|
+
for (const p of players) {
|
|
212
|
+
// Broadcast to talkable phases only — `active` (attached/processing) and
|
|
213
|
+
// `idle` (awaiting). Mirrors `shouldIncludeInBroadcast` in utils/validation.
|
|
214
|
+
const label = (0, format_1.phaseToLabel)(p.phase);
|
|
215
|
+
if (label === 'active' || label === 'idle') {
|
|
216
|
+
try {
|
|
217
|
+
await api.sendMessage(ctx.activeEnsemble, p.playerId, message, 'maestro');
|
|
218
|
+
sent++;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Skip individual failures
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
commitStatic(dispatch, 'message', `\u2714 Broadcast delivered to ${sent} player${sent !== 1 ? 's' : ''}: ${message}`);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
commitNotification(dispatch, 'error', `\u2717 Broadcast failed: ${err}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* /hosts [--all] — list daemons polling this Temporal namespace.
|
|
233
|
+
*
|
|
234
|
+
* #274. Surfaces the same data as `agent-tempo hosts` CLI and the
|
|
235
|
+
* `hosts` MCP tool via the shared `formatHostList` helper.
|
|
236
|
+
* `--all` opts into stale hosts; default hides them.
|
|
237
|
+
*/
|
|
238
|
+
async function handleHosts(args, dispatch, api) {
|
|
239
|
+
const includeStale = args.includes('--all');
|
|
240
|
+
try {
|
|
241
|
+
const list = await api.listHosts({ force: true });
|
|
242
|
+
dispatch({
|
|
243
|
+
type: 'SHOW_COMMAND_OVERLAY',
|
|
244
|
+
title: 'Hosts',
|
|
245
|
+
content: (0, format_hosts_1.formatHostList)(list, { includeStale }),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
commitNotification(dispatch, 'error', `/hosts failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* /recall [player] [--limit N] [--offset N] [--preview N] [--from X]
|
|
254
|
+
* [--since ISO] [--include-sent | --received-only]
|
|
255
|
+
*
|
|
256
|
+
* #128: unified per-session recall. Without a player positional, targets
|
|
257
|
+
* the ensemble's maestro session (the TUI's natural context); with a
|
|
258
|
+
* player positional, queries that player's session — same data shape the
|
|
259
|
+
* MCP `recall` tool and `agent-tempo recall <name>` CLI surface.
|
|
260
|
+
*
|
|
261
|
+
* The legacy pre-#128 behavior (aggregated maestro relay-log filtered
|
|
262
|
+
* by player) is retired; that path was inconsistent with the other two
|
|
263
|
+
* surfaces and the design doc on #128 explicitly chose parity.
|
|
264
|
+
*
|
|
265
|
+
* #361: The TUI default flipped to `includeSent: true` because the
|
|
266
|
+
* single-message view that `/recall` originally targeted was confusing
|
|
267
|
+
* when the user had just typed a message and wanted to see the dialog
|
|
268
|
+
* context. The MCP tool's default is unchanged (still received-only) so
|
|
269
|
+
* external callers see no contract drift. `--include-sent` is still
|
|
270
|
+
* accepted for backward compatibility (no-op when default is true);
|
|
271
|
+
* pass `--received-only` to opt out.
|
|
272
|
+
*/
|
|
273
|
+
async function handleRecall(args, dispatch, api, ctx) {
|
|
274
|
+
const ensemble = requireEnsemble(dispatch, ctx);
|
|
275
|
+
if (!ensemble)
|
|
276
|
+
return;
|
|
277
|
+
const parsed = parseRecallFlags(args);
|
|
278
|
+
if (parsed.error) {
|
|
279
|
+
commitNotification(dispatch, 'error', parsed.error);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// Player defaults to the ensemble's maestro — see docstring.
|
|
283
|
+
const targetPlayer = parsed.player ?? 'maestro';
|
|
284
|
+
try {
|
|
285
|
+
const { received, sent } = await api.recall(ensemble, targetPlayer);
|
|
286
|
+
// #361: TUI default → include sent. `--received-only` opts out;
|
|
287
|
+
// `--include-sent` remains accepted (no-op now) for backward compat.
|
|
288
|
+
const includeSent = parsed.receivedOnly === true ? false : (parsed.includeSent ?? true);
|
|
289
|
+
const timeline = (0, recall_format_1.buildTimeline)(received, sent, includeSent);
|
|
290
|
+
const rendered = (0, recall_format_1.formatRecall)(timeline, {
|
|
291
|
+
limit: parsed.limit,
|
|
292
|
+
offset: parsed.offset,
|
|
293
|
+
previewLength: parsed.previewLength,
|
|
294
|
+
since: parsed.since,
|
|
295
|
+
from: parsed.from,
|
|
296
|
+
});
|
|
297
|
+
const title = `Recall \u00B7 ${targetPlayer}`;
|
|
298
|
+
dispatch({ type: 'SHOW_COMMAND_OVERLAY', title, content: rendered.text });
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
commitNotification(dispatch, 'error', `Failed to recall messages: ${err instanceof Error ? err.message : String(err)}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Parse the `/recall` arg vector into structured flags. Exported for unit
|
|
306
|
+
* tests; the TUI handler consumes the structured result directly.
|
|
307
|
+
*
|
|
308
|
+
* Accepted shapes:
|
|
309
|
+
* `/recall` — maestro session
|
|
310
|
+
* `/recall alice` — alice's session
|
|
311
|
+
* `/recall --limit 5 --preview 80` — flags only
|
|
312
|
+
* `/recall alice --limit 5 --include-sent` — player + flags
|
|
313
|
+
*
|
|
314
|
+
* Unknown flags or non-numeric values for numeric flags produce a usage
|
|
315
|
+
* error rather than silent drop, so the user isn't surprised by a default.
|
|
316
|
+
*/
|
|
317
|
+
function parseRecallFlags(args) {
|
|
318
|
+
const out = {};
|
|
319
|
+
let i = 0;
|
|
320
|
+
// Leading non-flag positional is the player override.
|
|
321
|
+
if (args.length > 0 && !args[0].startsWith('--')) {
|
|
322
|
+
out.player = args[0];
|
|
323
|
+
i = 1;
|
|
324
|
+
}
|
|
325
|
+
while (i < args.length) {
|
|
326
|
+
const flag = args[i];
|
|
327
|
+
const consumeNumber = (name, key) => {
|
|
328
|
+
const raw = args[i + 1];
|
|
329
|
+
if (raw === undefined)
|
|
330
|
+
return `Missing value for ${name}`;
|
|
331
|
+
const n = Number(raw);
|
|
332
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < (key === 'offset' ? 0 : 1)) {
|
|
333
|
+
return `Invalid ${name}: ${raw}`;
|
|
334
|
+
}
|
|
335
|
+
// #270: cap --limit at 100 to match the MCP Zod schema. Recall queries
|
|
336
|
+
// load the full inbox/sent history from the workflow; the cap bounds
|
|
337
|
+
// the worst-case payload across every surface. Error message matches
|
|
338
|
+
// the CLI parser at `src/cli.ts` verbatim so operators see the same
|
|
339
|
+
// suggestion regardless of entry point.
|
|
340
|
+
if (key === 'limit' && n > 100) {
|
|
341
|
+
return `--limit exceeds max (100). Use --offset N to page through more results.`;
|
|
342
|
+
}
|
|
343
|
+
out[key] = n;
|
|
344
|
+
i += 2;
|
|
345
|
+
return null;
|
|
346
|
+
};
|
|
347
|
+
const consumeString = (name, key) => {
|
|
348
|
+
const raw = args[i + 1];
|
|
349
|
+
if (raw === undefined)
|
|
350
|
+
return `Missing value for ${name}`;
|
|
351
|
+
out[key] = raw;
|
|
352
|
+
i += 2;
|
|
353
|
+
return null;
|
|
354
|
+
};
|
|
355
|
+
let err = null;
|
|
356
|
+
switch (flag) {
|
|
357
|
+
case '--limit':
|
|
358
|
+
err = consumeNumber('--limit', 'limit');
|
|
359
|
+
break;
|
|
360
|
+
case '--offset':
|
|
361
|
+
err = consumeNumber('--offset', 'offset');
|
|
362
|
+
break;
|
|
363
|
+
case '--preview':
|
|
364
|
+
err = consumeNumber('--preview', 'previewLength');
|
|
365
|
+
break;
|
|
366
|
+
case '--since':
|
|
367
|
+
err = consumeString('--since', 'since');
|
|
368
|
+
break;
|
|
369
|
+
case '--from':
|
|
370
|
+
err = consumeString('--from', 'from');
|
|
371
|
+
break;
|
|
372
|
+
case '--include-sent':
|
|
373
|
+
out.includeSent = true;
|
|
374
|
+
i += 1;
|
|
375
|
+
break;
|
|
376
|
+
// #361: opt out of the post-flip default (received + sent).
|
|
377
|
+
case '--received-only':
|
|
378
|
+
out.receivedOnly = true;
|
|
379
|
+
i += 1;
|
|
380
|
+
break;
|
|
381
|
+
default: err = `Unknown flag: ${flag}`;
|
|
382
|
+
}
|
|
383
|
+
if (err)
|
|
384
|
+
return { error: err };
|
|
385
|
+
}
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
// ── PR-D verbs — thin handlers calling TempoClient methods ──
|
|
389
|
+
function requireEnsemble(dispatch, ctx) {
|
|
390
|
+
if (!ctx.activeEnsemble) {
|
|
391
|
+
commitNotification(dispatch, 'error', 'No active ensemble. Use /ensemble <name> first.');
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
return ctx.activeEnsemble;
|
|
395
|
+
}
|
|
396
|
+
/** /restart <player> [--fresh] [--no-force] [--load-from-state[=key]] [--stack-transcript] — revive a player session per §8.2. */
|
|
397
|
+
async function handleRestart(args, dispatch, api, ctx) {
|
|
398
|
+
if (args.length === 0) {
|
|
399
|
+
commitNotification(dispatch, 'error', 'Usage: /restart <player> [--fresh] [--no-force] [--load-from-state[=key]] [--stack-transcript]');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const ensemble = requireEnsemble(dispatch, ctx);
|
|
403
|
+
if (!ensemble)
|
|
404
|
+
return;
|
|
405
|
+
const target = args[0];
|
|
406
|
+
const fresh = args.includes('--fresh');
|
|
407
|
+
// Default: steal the lease. /restart is nearly always invoked against a
|
|
408
|
+
// live-but-unresponsive session, so forcing is the common case. Pass
|
|
409
|
+
// --no-force to refuse if a live attachment is present.
|
|
410
|
+
const force = !args.includes('--no-force');
|
|
411
|
+
// #334 PR-2 — saved-state seed. `--load-from-state` (no value) → default
|
|
412
|
+
// key 'main'; `--load-from-state=<key>` → named slot; `--load-from-state=`
|
|
413
|
+
// (empty after `=`) collapses to default rather than failing the Zod
|
|
414
|
+
// regex with a confusing slot-key validation error. `--stack-transcript`
|
|
415
|
+
// opts into stacking the transcript replay on top of the saved state.
|
|
416
|
+
const loadStateArg = args.find((a) => a === '--load-from-state' || a.startsWith('--load-from-state='));
|
|
417
|
+
let loadFromState;
|
|
418
|
+
if (loadStateArg !== undefined) {
|
|
419
|
+
const eqIdx = loadStateArg.indexOf('=');
|
|
420
|
+
const keyPart = eqIdx >= 0 ? loadStateArg.slice(eqIdx + 1) : '';
|
|
421
|
+
loadFromState = keyPart.length > 0 ? keyPart : true;
|
|
422
|
+
}
|
|
423
|
+
const transcript = args.includes('--stack-transcript') ? 'replay' : undefined;
|
|
424
|
+
try {
|
|
425
|
+
const result = await api.restart(ensemble, target, {
|
|
426
|
+
fresh,
|
|
427
|
+
force,
|
|
428
|
+
invokerPlayerId: 'tui',
|
|
429
|
+
...(loadFromState !== undefined ? { loadFromState } : {}),
|
|
430
|
+
...(transcript !== undefined ? { transcript } : {}),
|
|
431
|
+
});
|
|
432
|
+
// #306: Command-result summary as a bottom-pinned notification so it
|
|
433
|
+
// stays visible while subsequent activity (heartbeats, joins, etc.)
|
|
434
|
+
// streams into scrollback. Pre-#306 this rode commitStatic('info') and
|
|
435
|
+
// the user often missed the entryId / host on a busy ensemble.
|
|
436
|
+
commitNotification(dispatch, 'info', `\u21BB Restart queued for ${result.playerId}${result.host ? ` on ${result.host}` : ''} (outbox ${result.entryId}).`);
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
commitNotification(dispatch, 'error', `Restart failed for ${target}: ${err instanceof Error ? err.message : String(err)}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* /migrate <player> <host> [--fresh] [--force] [--yes-steal=<hostname>] — restart on a different host.
|
|
444
|
+
*
|
|
445
|
+
* #580 — when `--force` is set AND the target's current attachment is on a
|
|
446
|
+
* host other than the operator's, the deliberate-action gate from design
|
|
447
|
+
* §16.5 Option B requires `--yes-steal=<currentHost>` to match the holder's
|
|
448
|
+
* hostname exactly. This mirrors the MCP-tool guard (`enforceYesStealGuard`
|
|
449
|
+
* in `src/tools/restart.ts`) — making the operator type the host being stolen
|
|
450
|
+
* from prevents accidental cross-host takeover by the same finger habit that
|
|
451
|
+
* mashes `y` at any prompt. The MCP path was always covered; the TUI was the
|
|
452
|
+
* only operator surface that bypassed it.
|
|
453
|
+
*/
|
|
454
|
+
async function handleMigrate(args, dispatch, api, ctx) {
|
|
455
|
+
if (args.length < 2) {
|
|
456
|
+
commitNotification(dispatch, 'error', 'Usage: /migrate <player> <host> [--fresh] [--force] [--yes-steal=<hostname>]');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const ensemble = requireEnsemble(dispatch, ctx);
|
|
460
|
+
if (!ensemble)
|
|
461
|
+
return;
|
|
462
|
+
const target = args[0];
|
|
463
|
+
const host = args[1];
|
|
464
|
+
const fresh = args.includes('--fresh');
|
|
465
|
+
const force = args.includes('--force');
|
|
466
|
+
// #580 — `--yes-steal=<hostname>` parsing. Same shape as
|
|
467
|
+
// `--load-from-state=<key>` in handleRestart: accept `--yes-steal=value`
|
|
468
|
+
// (canonical), reject the bare flag with a friendly hint because the
|
|
469
|
+
// gate's whole UX value is naming the host explicitly.
|
|
470
|
+
const yesStealArg = args.find((a) => a === '--yes-steal' || a.startsWith('--yes-steal='));
|
|
471
|
+
let confirmStealFromHost;
|
|
472
|
+
if (yesStealArg !== undefined) {
|
|
473
|
+
const eqIdx = yesStealArg.indexOf('=');
|
|
474
|
+
const valuePart = eqIdx >= 0 ? yesStealArg.slice(eqIdx + 1).trim() : '';
|
|
475
|
+
if (valuePart.length === 0) {
|
|
476
|
+
commitNotification(dispatch, 'error', "--yes-steal requires a hostname: --yes-steal=<currentHost>. The flag's safety property is that you type the host name being stolen from.");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
confirmStealFromHost = valuePart;
|
|
480
|
+
}
|
|
481
|
+
// #580 — cross-host force-migrate gate. Only fires when --force is set
|
|
482
|
+
// AND the target's current attachment is on a different host. Mirrors
|
|
483
|
+
// the lookup `enforceYesStealGuard` does on the MCP side.
|
|
484
|
+
if (force) {
|
|
485
|
+
const localHostname = ctx.localHostname ?? (0, os_1.hostname)();
|
|
486
|
+
let currentHost;
|
|
487
|
+
try {
|
|
488
|
+
const info = await api.attachmentInfo(ensemble, target);
|
|
489
|
+
currentHost = info.currentAttachment?.hostname;
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// If we can't read the phase, fall through and let downstream
|
|
493
|
+
// handle the state. Not our job to synthesize a phantom guard
|
|
494
|
+
// failure from a transient query error.
|
|
495
|
+
}
|
|
496
|
+
if (currentHost && currentHost !== localHostname) {
|
|
497
|
+
if (!confirmStealFromHost) {
|
|
498
|
+
commitNotification(dispatch, 'error', `session "${target}" is attached to host "${currentHost}".\n` +
|
|
499
|
+
`To confirm moving it, re-run with --yes-steal:\n\n` +
|
|
500
|
+
` /migrate ${target} ${host} --force --yes-steal=${currentHost}\n\n` +
|
|
501
|
+
`This safety flag prevents accidental cross-host session takeover.`);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (confirmStealFromHost !== currentHost) {
|
|
505
|
+
commitNotification(dispatch, 'error', `--yes-steal mismatch: session "${target}" is on "${currentHost}", ` +
|
|
506
|
+
`not "${confirmStealFromHost}". Re-run with --yes-steal=${currentHost}.`);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
const result = await api.migrate(ensemble, target, host, {
|
|
513
|
+
fresh,
|
|
514
|
+
force,
|
|
515
|
+
invokerPlayerId: 'tui',
|
|
516
|
+
...(confirmStealFromHost !== undefined ? { confirmStealFromHost } : {}),
|
|
517
|
+
});
|
|
518
|
+
commitStatic(dispatch, 'info', `\u27A4 Migrate queued for ${result.playerId} \u2192 ${result.host ?? host} (outbox ${result.entryId}).`);
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
commitNotification(dispatch, 'error', `Migrate failed for ${target}: ${err instanceof Error ? err.message : String(err)}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/** /attachment-info <player> — inspect the V2 attachment phase + current holder. */
|
|
525
|
+
async function handleAttachmentInfo(args, dispatch, api, ctx) {
|
|
526
|
+
if (args.length === 0) {
|
|
527
|
+
commitNotification(dispatch, 'error', 'Usage: /attachment-info <player>');
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const ensemble = requireEnsemble(dispatch, ctx);
|
|
531
|
+
if (!ensemble)
|
|
532
|
+
return;
|
|
533
|
+
const target = args[0];
|
|
534
|
+
try {
|
|
535
|
+
const info = await api.attachmentInfo(ensemble, target);
|
|
536
|
+
// #264: share the display formatter with the CLI (`src/utils/attachment-format.ts`)
|
|
537
|
+
// so both surfaces render heartbeat age + match each other's field order /
|
|
538
|
+
// edge-case handling. Pre-#264 this block inlined a near-identical render
|
|
539
|
+
// but omitted heartbeat age, creating a latent parity gap.
|
|
540
|
+
const lines = (0, attachment_format_1.formatAttachmentInfoForDisplay)(target, info);
|
|
541
|
+
dispatch({ type: 'SHOW_COMMAND_OVERLAY', title: `Attachment \u00B7 ${target}`, content: lines.join('\n') });
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
commitNotification(dispatch, 'error', `attachment_info failed for ${target}: ${err instanceof Error ? err.message : String(err)}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/** /recruit [name] — launch the recruit wizard. Pre-fills name if given. */
|
|
548
|
+
async function handleRecruit(args, dispatch, _api, ctx) {
|
|
549
|
+
// Parse optional inline args: /recruit name --type foo --dir /path
|
|
550
|
+
const answers = {};
|
|
551
|
+
if (args.length > 0 && !args[0].startsWith('--')) {
|
|
552
|
+
answers.name = args[0];
|
|
553
|
+
}
|
|
554
|
+
for (let i = 0; i < args.length; i++) {
|
|
555
|
+
if (args[i] === '--type' && args[i + 1])
|
|
556
|
+
answers.playerType = args[++i];
|
|
557
|
+
if (args[i] === '--dir' && args[i + 1])
|
|
558
|
+
answers.workDir = args[++i];
|
|
559
|
+
if (args[i] === '--agent' && args[i + 1])
|
|
560
|
+
answers.agent = args[++i];
|
|
561
|
+
if (args[i] === '--host' && args[i + 1])
|
|
562
|
+
answers.host = args[++i];
|
|
563
|
+
}
|
|
564
|
+
dispatch({ type: 'ENTER_RECRUIT', answers, defaultAgent: ctx.defaultAgent });
|
|
565
|
+
}
|
|
566
|
+
/** Delete a schedule by name — shared logic for /schedule delete and /unschedule. */
|
|
567
|
+
async function deleteSchedule(name, dispatch, api, ctx) {
|
|
568
|
+
if (!ctx.activeEnsemble) {
|
|
569
|
+
commitNotification(dispatch, 'error', 'No active ensemble. Select one with /ensemble first.');
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
await api.cancelSchedule(ctx.activeEnsemble, name);
|
|
574
|
+
commitStatic(dispatch, 'message', `\u2714 Schedule "${name}" deleted.`);
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
commitNotification(dispatch, 'error', `\u2717 Failed to delete schedule "${name}": ${err}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/** /schedule [create|delete <name>] — manage schedules. */
|
|
581
|
+
async function handleSchedule(args, dispatch, api, ctx) {
|
|
582
|
+
if (args.length > 0) {
|
|
583
|
+
const sub = args[0].toLowerCase();
|
|
584
|
+
// /schedule create → enter wizard
|
|
585
|
+
if (sub === 'create') {
|
|
586
|
+
dispatch({ type: 'ENTER_SCHEDULE_WIZARD' });
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
// /schedule delete <name>
|
|
590
|
+
if (sub === 'delete') {
|
|
591
|
+
if (args.length < 2) {
|
|
592
|
+
commitNotification(dispatch, 'error', 'Usage: /schedule delete <name>');
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
await deleteSchedule(args[1], dispatch, api, ctx);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
commitNotification(dispatch, 'error', `Unknown subcommand: ${sub}. Usage: /schedule [create | delete <name>]`);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
// /schedule (no args) → show interactive overlay
|
|
602
|
+
if (!ctx.activeEnsemble) {
|
|
603
|
+
commitNotification(dispatch, 'error', 'No active ensemble. Select one with /ensemble first.');
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
const schedules = await api.getSchedules(ctx.activeEnsemble);
|
|
608
|
+
if (schedules.length === 0) {
|
|
609
|
+
dispatch({
|
|
610
|
+
type: 'SHOW_OVERLAY',
|
|
611
|
+
overlay: {
|
|
612
|
+
type: 'schedules',
|
|
613
|
+
title: 'Schedules',
|
|
614
|
+
items: [{ id: '_empty', label: 'No active schedules' }],
|
|
615
|
+
hint: 'n=new esc=close',
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const items = schedules.map(s => {
|
|
621
|
+
const timingParts = [];
|
|
622
|
+
if (s.type === 'interval' && s.interval) {
|
|
623
|
+
timingParts.push(`every ${formatMs(s.interval)}`);
|
|
624
|
+
}
|
|
625
|
+
if (s.cronExpression) {
|
|
626
|
+
timingParts.push(`cron: ${s.cronExpression}`);
|
|
627
|
+
}
|
|
628
|
+
if (s.nextFireAt) {
|
|
629
|
+
timingParts.push(`next: ${formatTimestamp(s.nextFireAt)}`);
|
|
630
|
+
}
|
|
631
|
+
if (s.firedCount > 0) {
|
|
632
|
+
timingParts.push(`fired ${s.firedCount}x`);
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
id: s.name,
|
|
636
|
+
label: `${s.name} \u2192 ${s.target}`,
|
|
637
|
+
sublabel: timingParts.join(' \u00B7 ') || s.type,
|
|
638
|
+
};
|
|
639
|
+
});
|
|
640
|
+
dispatch({
|
|
641
|
+
type: 'SHOW_OVERLAY',
|
|
642
|
+
overlay: {
|
|
643
|
+
type: 'schedules',
|
|
644
|
+
title: 'Schedules',
|
|
645
|
+
items,
|
|
646
|
+
hint: 'n=new d=delete esc=close',
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
commitNotification(dispatch, 'error', `Failed to fetch schedules: ${err}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/** /unschedule <name> — alias for /schedule delete. */
|
|
655
|
+
async function handleUnschedule(args, dispatch, api, ctx) {
|
|
656
|
+
if (args.length === 0) {
|
|
657
|
+
commitNotification(dispatch, 'error', 'Usage: /unschedule <name> (hint: use /schedule delete <name>)');
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
await deleteSchedule(args[0], dispatch, api, ctx);
|
|
661
|
+
}
|
|
662
|
+
/** /status — show ensemble status overlay. */
|
|
663
|
+
async function handleStatus(_args, dispatch) {
|
|
664
|
+
dispatch({ type: 'SHOW_STATUS' });
|
|
665
|
+
}
|
|
666
|
+
/** /gates — list quality gates. */
|
|
667
|
+
async function handleGates(_args, dispatch, api, ctx) {
|
|
668
|
+
try {
|
|
669
|
+
let ensembleNames;
|
|
670
|
+
if (ctx.activeEnsemble) {
|
|
671
|
+
ensembleNames = [ctx.activeEnsemble];
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
const ensembles = await api.discoverEnsembles();
|
|
675
|
+
if (ensembles.length === 0) {
|
|
676
|
+
commitNotification(dispatch, 'info', 'No ensembles running.');
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
ensembleNames = ensembles.map(e => e.name);
|
|
680
|
+
}
|
|
681
|
+
const icons = (0, platform_1.statusIcons)((0, platform_1.supportsUnicode)());
|
|
682
|
+
const allItems = [];
|
|
683
|
+
for (const ensName of ensembleNames) {
|
|
684
|
+
const gates = await api.getGates(ensName);
|
|
685
|
+
for (const g of gates) {
|
|
686
|
+
const icon = g.status === 'passed' ? icons.check
|
|
687
|
+
: g.status === 'failed' ? icons.cross
|
|
688
|
+
: '\u25CB';
|
|
689
|
+
const criteriaSum = g.criteria.map(c => {
|
|
690
|
+
const cIcon = c.status === 'passed' ? icons.check : c.status === 'failed' ? icons.cross : icons.pending;
|
|
691
|
+
return `${cIcon} ${c.text}`;
|
|
692
|
+
}).join(' ');
|
|
693
|
+
allItems.push({
|
|
694
|
+
id: `${ensName}:${g.task}`,
|
|
695
|
+
label: `${icon} ${g.task} [${g.status}]`,
|
|
696
|
+
sublabel: criteriaSum || '(no criteria)',
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (allItems.length === 0) {
|
|
701
|
+
commitNotification(dispatch, 'info', 'No quality gates defined.');
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
dispatch({
|
|
705
|
+
type: 'SHOW_OVERLAY',
|
|
706
|
+
overlay: {
|
|
707
|
+
type: 'gates',
|
|
708
|
+
title: 'Quality Gates',
|
|
709
|
+
items: allItems,
|
|
710
|
+
hint: '\u21B5=detail esc=close',
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
commitNotification(dispatch, 'error', `Failed to fetch gates: ${err}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/** /stages — list stages. */
|
|
720
|
+
async function handleStages(_args, dispatch, api, ctx) {
|
|
721
|
+
try {
|
|
722
|
+
let ensembleNames;
|
|
723
|
+
if (ctx.activeEnsemble) {
|
|
724
|
+
ensembleNames = [ctx.activeEnsemble];
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
const ensembles = await api.discoverEnsembles();
|
|
728
|
+
if (ensembles.length === 0) {
|
|
729
|
+
commitNotification(dispatch, 'info', 'No ensembles running.');
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
ensembleNames = ensembles.map(e => e.name);
|
|
733
|
+
}
|
|
734
|
+
const icons = (0, platform_1.statusIcons)((0, platform_1.supportsUnicode)());
|
|
735
|
+
const allItems = [];
|
|
736
|
+
for (const ensName of ensembleNames) {
|
|
737
|
+
const stages = await api.getStages(ensName);
|
|
738
|
+
for (const s of stages) {
|
|
739
|
+
const icon = s.status === 'complete' ? icons.check
|
|
740
|
+
: s.status === 'failed' ? icons.cross
|
|
741
|
+
: s.status === 'cancelled' ? icons.terminated
|
|
742
|
+
: icons.active;
|
|
743
|
+
const playerSum = s.players.map(p => {
|
|
744
|
+
const pIcon = p.status === 'reported' ? icons.check
|
|
745
|
+
: p.status === 'blocked' ? icons.cross
|
|
746
|
+
: icons.pending;
|
|
747
|
+
return `${pIcon} ${p.playerId}`;
|
|
748
|
+
}).join(' ');
|
|
749
|
+
allItems.push({
|
|
750
|
+
id: `${ensName}:${s.name}`,
|
|
751
|
+
label: `${icon} ${s.name} [${s.status}] (${s.failurePolicy})`,
|
|
752
|
+
sublabel: playerSum || '(no players)',
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (allItems.length === 0) {
|
|
757
|
+
commitNotification(dispatch, 'info', 'No stages defined.');
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
dispatch({
|
|
761
|
+
type: 'SHOW_OVERLAY',
|
|
762
|
+
overlay: {
|
|
763
|
+
type: 'stages',
|
|
764
|
+
title: 'Stages',
|
|
765
|
+
items: allItems,
|
|
766
|
+
hint: '\u21B5=detail esc=close',
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch (err) {
|
|
772
|
+
commitNotification(dispatch, 'error', `Failed to fetch stages: ${err}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/** /worktree [list] — list active worktrees. */
|
|
776
|
+
async function handleWorktree(args, dispatch, api, ctx) {
|
|
777
|
+
const subcommand = args[0] || 'list';
|
|
778
|
+
// /worktree create <player> [--branch <name>] — delegate to conductor
|
|
779
|
+
if (subcommand === 'create') {
|
|
780
|
+
if (args.length < 2) {
|
|
781
|
+
commitNotification(dispatch, 'error', 'Usage: /worktree create <player> [--branch <name>]');
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const ensemble = ctx.activeEnsemble;
|
|
785
|
+
if (!ensemble) {
|
|
786
|
+
commitNotification(dispatch, 'error', 'No active ensemble. Use /ensemble to select one.');
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const ensembles = await api.discoverEnsembles();
|
|
790
|
+
const ens = ensembles.find(e => e.name === ensemble);
|
|
791
|
+
if (!ens?.hasConductor) {
|
|
792
|
+
commitNotification(dispatch, 'error', 'No conductor in this ensemble. Worktree create requires a conductor.');
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const cmdParts = args.slice(0); // ['create', '<player>', ...flags]
|
|
796
|
+
await api.sendCommand(ensemble, `/worktree ${cmdParts.join(' ')}`, 'maestro');
|
|
797
|
+
commitStatic(dispatch, 'info', `\u2192 Worktree create request sent to conductor for ${args[1]}.`);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
// /worktree remove <player> — delegate to conductor
|
|
801
|
+
if (subcommand === 'remove') {
|
|
802
|
+
if (args.length < 2) {
|
|
803
|
+
commitNotification(dispatch, 'error', 'Usage: /worktree remove <player>');
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const ensemble = ctx.activeEnsemble;
|
|
807
|
+
if (!ensemble) {
|
|
808
|
+
commitNotification(dispatch, 'error', 'No active ensemble. Use /ensemble to select one.');
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const ensembles = await api.discoverEnsembles();
|
|
812
|
+
const ens = ensembles.find(e => e.name === ensemble);
|
|
813
|
+
if (!ens?.hasConductor) {
|
|
814
|
+
commitNotification(dispatch, 'error', 'No conductor in this ensemble. Worktree remove requires a conductor.');
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
await api.sendCommand(ensemble, `/worktree remove ${args[1]}`, 'maestro');
|
|
818
|
+
commitStatic(dispatch, 'info', `\u2192 Worktree remove request sent to conductor for ${args[1]}.`);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (subcommand !== 'list') {
|
|
822
|
+
commitNotification(dispatch, 'error', `Unknown subcommand: ${subcommand}. Usage: /worktree [list | create <player> | remove <player>]`);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
try {
|
|
826
|
+
const ensembles = await api.discoverEnsembles();
|
|
827
|
+
if (ensembles.length === 0) {
|
|
828
|
+
commitNotification(dispatch, 'info', 'No ensembles running.');
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const allItems = [];
|
|
832
|
+
for (const ens of ensembles) {
|
|
833
|
+
const worktrees = await api.getWorktrees(ens.name);
|
|
834
|
+
for (const w of worktrees) {
|
|
835
|
+
allItems.push({
|
|
836
|
+
id: `${ens.name}:${w.player}`,
|
|
837
|
+
label: `${w.player} \u2192 ${w.branch}`,
|
|
838
|
+
sublabel: `${w.path} (${w.createdBy})`,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (allItems.length === 0) {
|
|
843
|
+
commitNotification(dispatch, 'info', 'No active worktrees.');
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
dispatch({
|
|
847
|
+
type: 'SHOW_OVERLAY',
|
|
848
|
+
overlay: {
|
|
849
|
+
type: 'worktrees',
|
|
850
|
+
title: 'Worktrees',
|
|
851
|
+
items: allItems,
|
|
852
|
+
hint: 'esc=close',
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
catch (err) {
|
|
858
|
+
commitNotification(dispatch, 'error', `Failed to fetch worktrees: ${err}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/** /search <term> — search message history. */
|
|
862
|
+
async function handleSearch(args, dispatch, api, ctx) {
|
|
863
|
+
if (args.length === 0) {
|
|
864
|
+
commitNotification(dispatch, 'error', 'Usage: /search <term>');
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const term = args.join(' ');
|
|
868
|
+
const termLower = term.toLowerCase();
|
|
869
|
+
try {
|
|
870
|
+
// Fetch messages from ensemble(s)
|
|
871
|
+
const ensembles = ctx.activeEnsemble
|
|
872
|
+
? [{ name: ctx.activeEnsemble }]
|
|
873
|
+
: await api.discoverEnsembles();
|
|
874
|
+
const allResults = [];
|
|
875
|
+
const seen = new Set();
|
|
876
|
+
for (const ens of ensembles) {
|
|
877
|
+
// Fetch from both Maestro event log (100) and ensemble chat cache (500)
|
|
878
|
+
const [messages, chatResult] = await Promise.all([
|
|
879
|
+
api.getMessages(ens.name, 100),
|
|
880
|
+
api.getEnsembleChat(ens.name, 0, 500).catch(() => ({ messages: [] })),
|
|
881
|
+
]);
|
|
882
|
+
// Merge both sources, dedup by from+to+timestamp prefix
|
|
883
|
+
const combined = [
|
|
884
|
+
...messages.map(m => ({ from: m.from, to: m.to, text: m.text, timestamp: m.timestamp })),
|
|
885
|
+
...chatResult.messages.map((m) => ({ from: m.from, to: m.to, text: m.text, timestamp: m.timestamp })),
|
|
886
|
+
];
|
|
887
|
+
for (const m of combined) {
|
|
888
|
+
const dedupKey = `${m.from}:${m.to}:${m.text.slice(0, 60)}:${m.timestamp.slice(0, 19)}`;
|
|
889
|
+
if (seen.has(dedupKey))
|
|
890
|
+
continue;
|
|
891
|
+
seen.add(dedupKey);
|
|
892
|
+
const haystack = `${m.from} ${m.to} ${m.text}`.toLowerCase();
|
|
893
|
+
if (haystack.includes(termLower)) {
|
|
894
|
+
allResults.push({ ensemble: ens.name, ...m });
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (allResults.length === 0) {
|
|
899
|
+
commitNotification(dispatch, 'info', `No messages matching "${term}".`);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const lines = [`\n ${allResults.length} result${allResults.length !== 1 ? 's' : ''} for "${term}":\n`];
|
|
903
|
+
for (const r of allResults.slice(-50)) {
|
|
904
|
+
const time = formatTimestamp(r.timestamp);
|
|
905
|
+
const text = r.text.replace(/\n/g, ' ');
|
|
906
|
+
const truncated = text.length > 70 ? text.slice(0, 67) + '...' : text;
|
|
907
|
+
lines.push(` ${time} ${r.from} \u2192 ${r.to}: ${truncated}`);
|
|
908
|
+
}
|
|
909
|
+
if (allResults.length > 50) {
|
|
910
|
+
lines.push(`\n ... and ${allResults.length - 50} more. Narrow your search for fewer results.`);
|
|
911
|
+
}
|
|
912
|
+
dispatch({ type: 'SHOW_COMMAND_OVERLAY', title: `Search \u00B7 "${term}"`, content: lines.join('\n') });
|
|
913
|
+
}
|
|
914
|
+
catch (err) {
|
|
915
|
+
commitNotification(dispatch, 'error', `Search failed: ${err}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/** /recruit-conductor — one-shot recruit a conductor for the current ensemble. */
|
|
919
|
+
async function handleRecruitConductor(_args, dispatch, api, ctx) {
|
|
920
|
+
const ensemble = ctx.activeEnsemble;
|
|
921
|
+
if (!ensemble) {
|
|
922
|
+
commitNotification(dispatch, 'error', 'No active ensemble. Use /ensemble to select or create one.');
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
commitStatic(dispatch, 'info', '\u2026 Recruiting conductor (tempo-conductor)\u2026');
|
|
926
|
+
// `agent-tempo conduct` was removed in #288. Delegate to the shared helper
|
|
927
|
+
// which query-first checks for a live conductor phase, then shells out via
|
|
928
|
+
// `client.spawnConductor` (which runs `agent-tempo up <ensemble>`). This is
|
|
929
|
+
// the same two-op `/restore` uses, so the recruit-vs-restore code paths stay
|
|
930
|
+
// aligned.
|
|
931
|
+
const { ensureConductorSpawned } = await Promise.resolve().then(() => __importStar(require('../client/ensure-conductor-spawned')));
|
|
932
|
+
const outcome = await ensureConductorSpawned(ensemble, api);
|
|
933
|
+
if (outcome.spawned) {
|
|
934
|
+
commitStatic(dispatch, 'info', '\u2714 Conductor terminal launched.');
|
|
935
|
+
}
|
|
936
|
+
else if (outcome.reason === 'alreadyLive') {
|
|
937
|
+
commitStatic(dispatch, 'info', '\u2714 Conductor is already attached to this ensemble.');
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
commitNotification(dispatch, 'error', `\u2717 Conductor spawn failed: ${outcome.error}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
/** /lineup load|save — manage ensemble lineups. */
|
|
944
|
+
async function handleLineup(args, dispatch, api, ctx) {
|
|
945
|
+
if (args.length === 0) {
|
|
946
|
+
commitNotification(dispatch, 'error', 'Usage: /lineup load <file> | /lineup save [file]');
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const subcommand = args[0].toLowerCase();
|
|
950
|
+
if (subcommand === 'load') {
|
|
951
|
+
if (args.length < 2) {
|
|
952
|
+
// No file arg — show available lineups
|
|
953
|
+
try {
|
|
954
|
+
const lineups = (0, saver_1.listAllLineups)();
|
|
955
|
+
if (lineups.length === 0) {
|
|
956
|
+
commitNotification(dispatch, 'info', 'No lineups available. Create one with /lineup save.');
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const items = lineups.map(l => ({
|
|
960
|
+
id: l.name,
|
|
961
|
+
label: l.name,
|
|
962
|
+
sublabel: l.source === 'saved' ? 'saved' : 'shipped example',
|
|
963
|
+
}));
|
|
964
|
+
dispatch({
|
|
965
|
+
type: 'SHOW_OVERLAY',
|
|
966
|
+
overlay: {
|
|
967
|
+
type: 'lineups',
|
|
968
|
+
title: 'Available Lineups',
|
|
969
|
+
items,
|
|
970
|
+
hint: 'Usage: /lineup load <name> \u00B7 esc=close',
|
|
971
|
+
},
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
commitNotification(dispatch, 'error', 'Usage: /lineup load <name>');
|
|
976
|
+
}
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const filePath = args[1];
|
|
980
|
+
// Enter lineup confirmation mode — App.tsx handles y/n
|
|
981
|
+
dispatch({
|
|
982
|
+
type: 'CONFIRM_LINEUP',
|
|
983
|
+
action: 'load',
|
|
984
|
+
path: filePath,
|
|
985
|
+
summary: `Load lineup from: ${filePath}`,
|
|
986
|
+
});
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (subcommand === 'save') {
|
|
990
|
+
const ensemble = ctx.activeEnsemble;
|
|
991
|
+
if (!ensemble) {
|
|
992
|
+
commitNotification(dispatch, 'error', 'No active ensemble. Use /ensemble <name> first.');
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const filePath = args[1] || `ensemble-${ensemble}.yml`;
|
|
996
|
+
try {
|
|
997
|
+
await api.sendCommand(ensemble, `/save_lineup ${filePath}`, 'maestro');
|
|
998
|
+
commitStatic(dispatch, 'info', `\u2714 Lineup save requested: ${filePath}`);
|
|
999
|
+
}
|
|
1000
|
+
catch (err) {
|
|
1001
|
+
commitNotification(dispatch, 'error', `\u2717 Failed to save lineup: ${err}`);
|
|
1002
|
+
}
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
commitNotification(dispatch, 'error', `Unknown lineup subcommand: ${subcommand}. Use: load, save`);
|
|
1006
|
+
}
|
|
1007
|
+
/** /ensembles — show interactive ensemble picker. */
|
|
1008
|
+
/** /ensemble <name> — switch active ensemble context, or navigate home if no args. */
|
|
1009
|
+
async function handleEnsemble(args, dispatch, api) {
|
|
1010
|
+
if (args.length === 0) {
|
|
1011
|
+
// #306: No args — navigate to the full home view (Online/Paused/Offline
|
|
1012
|
+
// picker, badges, cwd-pinning, loading state) instead of the bare picker
|
|
1013
|
+
// overlay. The home view is the canonical ensemble switcher; /ensemble
|
|
1014
|
+
// exists for direct-by-name shortcut.
|
|
1015
|
+
dispatch({ type: 'NAVIGATE_HOME' });
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const name = args[0];
|
|
1019
|
+
try {
|
|
1020
|
+
const ensembles = await api.discoverEnsembles();
|
|
1021
|
+
const match = ensembles.find(e => e.name === name);
|
|
1022
|
+
if (!match) {
|
|
1023
|
+
const available = ensembles.map(e => e.name).join(', ') || 'none';
|
|
1024
|
+
commitNotification(dispatch, 'error', `Ensemble "${name}" not found. Available: ${available}`);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
// Switch ensemble — clears old data, polling will refresh
|
|
1028
|
+
dispatch({ type: 'NAVIGATE_ENSEMBLE', ensemble: name });
|
|
1029
|
+
commitStatic(dispatch, 'info', `\u2714 Switched to ensemble: ${name} (${match.playerCount} player${match.playerCount !== 1 ? 's' : ''})`);
|
|
1030
|
+
}
|
|
1031
|
+
catch (err) {
|
|
1032
|
+
commitNotification(dispatch, 'error', `Failed to switch ensemble: ${err}`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* /go — release all held players in the current ensemble.
|
|
1037
|
+
*
|
|
1038
|
+
* #306: Direct TempoClient path — the legacy `sendCommand('/release',
|
|
1039
|
+
* 'maestro')` routed through the maestro hub → conductor's Claude Code
|
|
1040
|
+
* session → `release` MCP tool, which required a live conductor + added
|
|
1041
|
+
* 2-5s of LLM-interpretation latency. `api.release(ensemble)` submits
|
|
1042
|
+
* release outbox entries from the TUI's own maestro session; no conductor
|
|
1043
|
+
* needed, no LLM hop, clean partial-success diagnostics.
|
|
1044
|
+
*/
|
|
1045
|
+
async function handleGo(_args, dispatch, api, ctx) {
|
|
1046
|
+
const ensemble = ctx.activeEnsemble;
|
|
1047
|
+
if (!ensemble) {
|
|
1048
|
+
commitNotification(dispatch, 'error', 'No active ensemble. Use /ensemble to select one.');
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
try {
|
|
1052
|
+
const summary = await api.release(ensemble);
|
|
1053
|
+
if (summary.released.length === 0 && summary.errors.length === 0) {
|
|
1054
|
+
// #306 follow-up: even with nothing to release, defensively clear
|
|
1055
|
+
// the held indicator. The poll will reconcile in 2s; this just
|
|
1056
|
+
// prevents a stale indicator from flashing yellow until then.
|
|
1057
|
+
dispatch({ type: 'SET_ENSEMBLE_HELD', held: false });
|
|
1058
|
+
commitNotification(dispatch, 'info', 'No held players to release.');
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (summary.released.length > 0) {
|
|
1062
|
+
commitStatic(dispatch, 'info', `\u2714 Released ${summary.released.length} player${summary.released.length !== 1 ? 's' : ''}: ${summary.released.join(', ')}`);
|
|
1063
|
+
}
|
|
1064
|
+
// #306 follow-up: optimistically clear the held indicator so the
|
|
1065
|
+
// StatusBar `held` segment + the pinned `Tip: /go` line disappear
|
|
1066
|
+
// immediately. Mirrors the pause/play optimistic toggle.
|
|
1067
|
+
dispatch({ type: 'SET_ENSEMBLE_HELD', held: false });
|
|
1068
|
+
for (const e of summary.errors) {
|
|
1069
|
+
commitNotification(dispatch, 'error', `\u2717 Release failed for ${e.playerId}: ${e.error}`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
catch (err) {
|
|
1073
|
+
commitNotification(dispatch, 'error', `\u2717 Release failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Resolve the ensemble target from args/context, emitting a friendly error
|
|
1078
|
+
* when neither is available. Shared by every ensemble-scoped verb below.
|
|
1079
|
+
*/
|
|
1080
|
+
function resolveEnsembleArg(args, ctx, dispatch, verb) {
|
|
1081
|
+
const target = args[0] ?? ctx.activeEnsemble;
|
|
1082
|
+
if (!target) {
|
|
1083
|
+
commitNotification(dispatch, 'error', `No active ensemble. Use /ensemble to select one, or /${verb} <ensemble>.`);
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
return target;
|
|
1087
|
+
}
|
|
1088
|
+
/** /pause [ensemble] — pause every session + scheduler + maestro. */
|
|
1089
|
+
async function handlePause(args, dispatch, api, ctx) {
|
|
1090
|
+
const ensemble = resolveEnsembleArg(args, ctx, dispatch, 'pause');
|
|
1091
|
+
if (!ensemble)
|
|
1092
|
+
return;
|
|
1093
|
+
try {
|
|
1094
|
+
await api.pause(ensemble);
|
|
1095
|
+
// Bug B: optimistically flip the StatusBar indicator so users don't
|
|
1096
|
+
// wait for the next 2s poll tick to see "paused". The poll loop will
|
|
1097
|
+
// sync the truth back if the call somehow lied.
|
|
1098
|
+
dispatch({ type: 'SET_ENSEMBLE_PAUSED', paused: true });
|
|
1099
|
+
commitStatic(dispatch, 'info', `\u23F8 Paused ensemble "${ensemble}".`);
|
|
1100
|
+
}
|
|
1101
|
+
catch (err) {
|
|
1102
|
+
commitNotification(dispatch, 'error', `\u2717 Pause failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/** /play [ensemble] — resume a paused ensemble (renamed from /resume). */
|
|
1106
|
+
async function handlePlay(args, dispatch, api, ctx) {
|
|
1107
|
+
const ensemble = resolveEnsembleArg(args, ctx, dispatch, 'play');
|
|
1108
|
+
if (!ensemble)
|
|
1109
|
+
return;
|
|
1110
|
+
try {
|
|
1111
|
+
await api.play(ensemble);
|
|
1112
|
+
// Bug B: optimistically clear the StatusBar `paused` segment so users
|
|
1113
|
+
// get instant feedback that the resume took effect.
|
|
1114
|
+
dispatch({ type: 'SET_ENSEMBLE_PAUSED', paused: false });
|
|
1115
|
+
// #306 follow-up: defensive clear of the held flag. `/play` does NOT
|
|
1116
|
+
// release held players (held + paused are orthogonal — `/load_lineup`
|
|
1117
|
+
// flips both, `/play` clears only paused), but if the held indicator
|
|
1118
|
+
// was stale this prevents it from lingering until the next poll. The
|
|
1119
|
+
// 2s poll will resync if any player is still actually held.
|
|
1120
|
+
dispatch({ type: 'SET_ENSEMBLE_HELD', held: false });
|
|
1121
|
+
commitStatic(dispatch, 'info', `▶ Resumed ensemble "${ensemble}".`);
|
|
1122
|
+
}
|
|
1123
|
+
catch (err) {
|
|
1124
|
+
commitNotification(dispatch, 'error', `✗ Play failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
/** /shutdown [ensemble] — graceful teardown (detach adapters + pause maestro/scheduler). */
|
|
1128
|
+
async function handleShutdown(args, dispatch, api, ctx) {
|
|
1129
|
+
const ensemble = resolveEnsembleArg(args, ctx, dispatch, 'shutdown');
|
|
1130
|
+
if (!ensemble)
|
|
1131
|
+
return;
|
|
1132
|
+
commitStatic(dispatch, 'info', `\u2026 Shutting down "${ensemble}" \u2026`);
|
|
1133
|
+
try {
|
|
1134
|
+
const summary = await api.shutdown(ensemble);
|
|
1135
|
+
// #306: per-player detail lines stay in scrollback as a record; the
|
|
1136
|
+
// aggregate summary below is pinned so it can't scroll above the fold
|
|
1137
|
+
// on a busy chat. After success we also land the user on home, since
|
|
1138
|
+
// a parked ensemble has no live players to talk to.
|
|
1139
|
+
for (const d of summary.details) {
|
|
1140
|
+
const line = ` ${d.playerId} \u2014 ${d.outcome}${d.error ? `: ${d.error}` : ''}`;
|
|
1141
|
+
commitStatic(dispatch, d.outcome === 'failed' ? 'error' : 'info', line);
|
|
1142
|
+
}
|
|
1143
|
+
commitNotification(dispatch, summary.failed === 0 ? 'info' : 'error', `\u2714 Shutdown "${ensemble}" \u2014 ${summary.detached} detached, ${summary.skipped} skipped, ${summary.failed} failed` +
|
|
1144
|
+
`${summary.maestroPaused ? ', maestro paused' : ''}${summary.schedulerPaused ? ', scheduler paused' : ''}.`);
|
|
1145
|
+
dispatch({ type: 'NAVIGATE_HOME' });
|
|
1146
|
+
}
|
|
1147
|
+
catch (err) {
|
|
1148
|
+
commitNotification(dispatch, 'error', `\u2717 Shutdown failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
/** /restore [ensemble] — bring a parked ensemble back (reattach orphans + spawn conductor if absent). */
|
|
1152
|
+
async function handleRestore(args, dispatch, api, ctx) {
|
|
1153
|
+
const ensemble = resolveEnsembleArg(args, ctx, dispatch, 'restore');
|
|
1154
|
+
if (!ensemble)
|
|
1155
|
+
return;
|
|
1156
|
+
commitStatic(dispatch, 'info', `\u2026 Restoring "${ensemble}" \u2026`);
|
|
1157
|
+
try {
|
|
1158
|
+
const summary = await api.restore(ensemble);
|
|
1159
|
+
// Bug B: optimistically clear the `paused` indicator. /restore both
|
|
1160
|
+
// unpauses maestro+scheduler AND fans setPaused=false to every session,
|
|
1161
|
+
// so by the time the call resolves the ensemble is no longer paused.
|
|
1162
|
+
dispatch({ type: 'SET_ENSEMBLE_PAUSED', paused: false });
|
|
1163
|
+
const { formatRestoreOutcome } = await Promise.resolve().then(() => __importStar(require('../reconcile/orphans')));
|
|
1164
|
+
// #306: per-player detail lines stay in scrollback as a record; the
|
|
1165
|
+
// aggregate summary below is pinned so it can't scroll above the fold.
|
|
1166
|
+
for (const d of summary.details) {
|
|
1167
|
+
const text = ` ${d.playerId} \u2014 ${formatRestoreOutcome(d.outcome)}`;
|
|
1168
|
+
commitStatic(dispatch, d.outcome.kind === 'failed' ? 'error' : 'info', text);
|
|
1169
|
+
}
|
|
1170
|
+
commitNotification(dispatch, summary.failed === 0 ? 'info' : 'error', `\u2714 Restore "${ensemble}" \u2014 ${summary.reattached} reattached, ${summary.skipped} skipped, ${summary.failed} failed.`);
|
|
1171
|
+
// Step 2: ensure a conductor terminal is present (spawn if absent).
|
|
1172
|
+
const { ensureConductorSpawned } = await Promise.resolve().then(() => __importStar(require('../client/ensure-conductor-spawned')));
|
|
1173
|
+
const conductorOutcome = await ensureConductorSpawned(ensemble, api);
|
|
1174
|
+
if (conductorOutcome.spawned) {
|
|
1175
|
+
commitStatic(dispatch, 'info', ' Conductor terminal launched.');
|
|
1176
|
+
}
|
|
1177
|
+
else if (conductorOutcome.reason === 'spawnFailed') {
|
|
1178
|
+
commitNotification(dispatch, 'error', ` Conductor spawn failed: ${conductorOutcome.error}`);
|
|
1179
|
+
}
|
|
1180
|
+
// `alreadyLive` is silent — no action needed.
|
|
1181
|
+
}
|
|
1182
|
+
catch (err) {
|
|
1183
|
+
commitNotification(dispatch, 'error', `\u2717 Restore failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
/** /home — navigate back to the home view without touching workflow state. */
|
|
1187
|
+
async function handleHome(_args, dispatch) {
|
|
1188
|
+
dispatch({ type: 'NAVIGATE_HOME' });
|
|
1189
|
+
}
|
|
1190
|
+
// ── Utility ──
|
|
1191
|
+
function formatTimestamp(ts) {
|
|
1192
|
+
try {
|
|
1193
|
+
const d = new Date(ts);
|
|
1194
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
1195
|
+
}
|
|
1196
|
+
catch {
|
|
1197
|
+
return '??:??';
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
/** Format milliseconds as a human-readable duration (e.g. "30s", "5m", "1.5h"). */
|
|
1201
|
+
function formatMs(ms) {
|
|
1202
|
+
if (ms < 60000)
|
|
1203
|
+
return `${Math.round(ms / 1000)}s`;
|
|
1204
|
+
if (ms < 3600000)
|
|
1205
|
+
return `${Math.round(ms / 60000)}m`;
|
|
1206
|
+
return `${(ms / 3600000).toFixed(1)}h`;
|
|
1207
|
+
}
|
|
1208
|
+
// ── Registry ──
|
|
1209
|
+
/** All supported slash commands. */
|
|
1210
|
+
exports.COMMANDS = {
|
|
1211
|
+
recruit: {
|
|
1212
|
+
description: 'Spawn a new player session',
|
|
1213
|
+
usage: '/recruit <name> [--type <type>] [--dir <path>]',
|
|
1214
|
+
handler: handleRecruit,
|
|
1215
|
+
},
|
|
1216
|
+
broadcast: {
|
|
1217
|
+
description: 'Send a message to all active players',
|
|
1218
|
+
usage: '/broadcast <message>',
|
|
1219
|
+
handler: handleBroadcast,
|
|
1220
|
+
},
|
|
1221
|
+
restart: {
|
|
1222
|
+
description: 'Restart a session (reap + claim + context replay + spawn). Steals a live lease by default — pass --no-force to refuse if held. --load-from-state seeds from a saved-state slot (#334).',
|
|
1223
|
+
usage: '/restart <player> [--fresh] [--no-force] [--load-from-state[=key]] [--stack-transcript]',
|
|
1224
|
+
handler: handleRestart,
|
|
1225
|
+
},
|
|
1226
|
+
destroy: {
|
|
1227
|
+
description: 'Terminally destroy a player (y/N) or an ensemble (typed-name)',
|
|
1228
|
+
usage: '/destroy <player|ensemble> [reason]',
|
|
1229
|
+
handler: handleDestroy,
|
|
1230
|
+
},
|
|
1231
|
+
migrate: {
|
|
1232
|
+
description: "Restart a session on a different host. --force on a cross-host target requires --yes-steal=<currentHost> (§16.5 Option B).",
|
|
1233
|
+
usage: '/migrate <player> <host> [--fresh] [--force] [--yes-steal=<hostname>]',
|
|
1234
|
+
handler: handleMigrate,
|
|
1235
|
+
},
|
|
1236
|
+
'attachment-info': {
|
|
1237
|
+
description: 'Inspect the V2 attachment state of a session',
|
|
1238
|
+
usage: '/attachment-info <player>',
|
|
1239
|
+
handler: handleAttachmentInfo,
|
|
1240
|
+
},
|
|
1241
|
+
hosts: {
|
|
1242
|
+
description: 'List daemons polling this Temporal namespace with advertised capabilities (#274)',
|
|
1243
|
+
usage: '/hosts [--all]',
|
|
1244
|
+
handler: handleHosts,
|
|
1245
|
+
},
|
|
1246
|
+
recall: {
|
|
1247
|
+
description: "Read a player's message history (received + sent by default; --received-only to opt out)",
|
|
1248
|
+
usage: '/recall [player] [--limit N] [--offset N] [--preview N] [--from X] [--since ISO] [--received-only]',
|
|
1249
|
+
handler: handleRecall,
|
|
1250
|
+
},
|
|
1251
|
+
schedule: {
|
|
1252
|
+
description: 'Manage schedules — list, create, or delete',
|
|
1253
|
+
usage: '/schedule [create | delete <name>]',
|
|
1254
|
+
handler: handleSchedule,
|
|
1255
|
+
},
|
|
1256
|
+
players: {
|
|
1257
|
+
description: 'List active players or show player detail',
|
|
1258
|
+
usage: '/players [name]',
|
|
1259
|
+
handler: handlePlayer,
|
|
1260
|
+
},
|
|
1261
|
+
gates: {
|
|
1262
|
+
description: 'List quality gates and their status',
|
|
1263
|
+
usage: '/gates',
|
|
1264
|
+
handler: handleGates,
|
|
1265
|
+
},
|
|
1266
|
+
stages: {
|
|
1267
|
+
description: 'List stages and their status',
|
|
1268
|
+
usage: '/stages',
|
|
1269
|
+
handler: handleStages,
|
|
1270
|
+
},
|
|
1271
|
+
worktree: {
|
|
1272
|
+
description: 'Manage git worktrees for player isolation',
|
|
1273
|
+
usage: '/worktree [list | create <player> | remove <player>]',
|
|
1274
|
+
handler: handleWorktree,
|
|
1275
|
+
},
|
|
1276
|
+
lineup: {
|
|
1277
|
+
description: 'Load or save an ensemble lineup',
|
|
1278
|
+
usage: '/lineup load <file> | save [file]',
|
|
1279
|
+
handler: handleLineup,
|
|
1280
|
+
},
|
|
1281
|
+
ensemble: {
|
|
1282
|
+
description: 'Switch active ensemble by name (or open home view with no arg)',
|
|
1283
|
+
usage: '/ensemble [name]',
|
|
1284
|
+
handler: handleEnsemble,
|
|
1285
|
+
},
|
|
1286
|
+
search: {
|
|
1287
|
+
description: 'Search message history',
|
|
1288
|
+
usage: '/search <term>',
|
|
1289
|
+
handler: handleSearch,
|
|
1290
|
+
},
|
|
1291
|
+
help: {
|
|
1292
|
+
description: 'Show available commands',
|
|
1293
|
+
usage: '/help [command]',
|
|
1294
|
+
handler: null, // Handled directly in App.tsx
|
|
1295
|
+
},
|
|
1296
|
+
'recruit-conductor': {
|
|
1297
|
+
description: 'Recruit a conductor for the current ensemble',
|
|
1298
|
+
usage: '/recruit-conductor',
|
|
1299
|
+
handler: handleRecruitConductor,
|
|
1300
|
+
},
|
|
1301
|
+
status: {
|
|
1302
|
+
description: 'Show ensemble players and status',
|
|
1303
|
+
usage: '/status',
|
|
1304
|
+
handler: handleStatus,
|
|
1305
|
+
},
|
|
1306
|
+
go: {
|
|
1307
|
+
description: 'Release all held players (unlock outbox)',
|
|
1308
|
+
usage: '/go',
|
|
1309
|
+
handler: handleGo,
|
|
1310
|
+
},
|
|
1311
|
+
pause: {
|
|
1312
|
+
description: 'Pause every session + scheduler + maestro in the ensemble',
|
|
1313
|
+
usage: '/pause [ensemble]',
|
|
1314
|
+
handler: handlePause,
|
|
1315
|
+
},
|
|
1316
|
+
play: {
|
|
1317
|
+
description: 'Resume a paused ensemble (renamed from /resume — avoids collision with `claude --resume`)',
|
|
1318
|
+
usage: '/play [ensemble]',
|
|
1319
|
+
handler: handlePlay,
|
|
1320
|
+
},
|
|
1321
|
+
shutdown: {
|
|
1322
|
+
description: 'Graceful ensemble teardown — detach adapters, pause maestro + scheduler',
|
|
1323
|
+
usage: '/shutdown [ensemble]',
|
|
1324
|
+
handler: handleShutdown,
|
|
1325
|
+
},
|
|
1326
|
+
restore: {
|
|
1327
|
+
description: 'Restore a parked ensemble — reattach orphans, unpause maestro + scheduler',
|
|
1328
|
+
usage: '/restore [ensemble]',
|
|
1329
|
+
handler: handleRestore,
|
|
1330
|
+
},
|
|
1331
|
+
home: {
|
|
1332
|
+
description: 'Return to the home view (does not touch workflows)',
|
|
1333
|
+
usage: '/home',
|
|
1334
|
+
handler: handleHome,
|
|
1335
|
+
},
|
|
1336
|
+
back: {
|
|
1337
|
+
description: 'Return to maestro view',
|
|
1338
|
+
usage: '/back',
|
|
1339
|
+
handler: null, // Handled directly in App.tsx
|
|
1340
|
+
},
|
|
1341
|
+
quit: {
|
|
1342
|
+
description: 'Exit the TUI',
|
|
1343
|
+
usage: '/quit (or /exit)',
|
|
1344
|
+
handler: null, // Handled directly in App.tsx
|
|
1345
|
+
},
|
|
1346
|
+
// #306: /exit is an alias for /quit. Claude Code uses /exit, so users
|
|
1347
|
+
// arriving from there expect it to work. Both go to the same App.tsx
|
|
1348
|
+
// handler that calls Ink's exit().
|
|
1349
|
+
exit: {
|
|
1350
|
+
description: 'Exit the TUI (alias for /quit)',
|
|
1351
|
+
usage: '/exit',
|
|
1352
|
+
handler: null, // Handled directly in App.tsx
|
|
1353
|
+
},
|
|
1354
|
+
};
|
|
1355
|
+
/** Get sorted list of command names for display. */
|
|
1356
|
+
function getCommandNames() {
|
|
1357
|
+
return Object.keys(exports.COMMANDS).sort();
|
|
1358
|
+
}
|
|
1359
|
+
/** Check if a command name is registered. */
|
|
1360
|
+
function isValidCommand(name) {
|
|
1361
|
+
return name in exports.COMMANDS;
|
|
1362
|
+
}
|
|
1363
|
+
/** Format a help summary of all commands. */
|
|
1364
|
+
function formatHelpSummary() {
|
|
1365
|
+
const lines = getCommandNames().map(name => {
|
|
1366
|
+
const cmd = exports.COMMANDS[name];
|
|
1367
|
+
return ` ${cmd.usage.padEnd(48)} ${cmd.description}`;
|
|
1368
|
+
});
|
|
1369
|
+
return ['Available commands:', '', ...lines, '', 'Type /help <command> for details.'].join('\n');
|
|
1370
|
+
}
|
|
1371
|
+
// `filterPaletteCommands`, `PLAYER_PARAM_COMMANDS`, `SUBCOMMAND_MAP`,
|
|
1372
|
+
// `PaletteMode`, `PaletteContext`, `classifyPaletteInput`, and
|
|
1373
|
+
// `filterPlayerNames` moved to `src/palette/index.ts` in #471/#472. The
|
|
1374
|
+
// re-exports at the top of this file preserve the pre-extraction surface
|
|
1375
|
+
// so PromptArea / App.tsx / tests/tui/* keep working unchanged.
|