claude-tempo 0.22.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CLAUDE.md +12 -3
  2. package/README.md +20 -1
  3. package/dist/activities/outbox.d.ts +7 -0
  4. package/dist/activities/outbox.js +42 -6
  5. package/dist/cli/commands.d.ts +12 -0
  6. package/dist/cli/commands.js +125 -0
  7. package/dist/cli.js +18 -0
  8. package/dist/client/index.d.ts +10 -0
  9. package/dist/client/index.js +482 -0
  10. package/dist/client/interface.d.ts +66 -0
  11. package/dist/client/interface.js +2 -0
  12. package/dist/server.js +18 -0
  13. package/dist/tools/load-lineup.js +25 -1
  14. package/dist/tools/pause-ensemble.d.ts +4 -0
  15. package/dist/tools/pause-ensemble.js +58 -0
  16. package/dist/tools/release.d.ts +4 -0
  17. package/dist/tools/release.js +98 -0
  18. package/dist/tools/resume-ensemble.d.ts +4 -0
  19. package/dist/tools/resume-ensemble.js +58 -0
  20. package/dist/tui/App.d.ts +1 -1
  21. package/dist/tui/client.d.ts +4 -67
  22. package/dist/tui/client.js +5 -468
  23. package/dist/tui/commands.d.ts +1 -1
  24. package/dist/tui/commands.js +78 -0
  25. package/dist/tui/index.js +1 -1
  26. package/dist/tui/store.d.ts +1 -1
  27. package/dist/types.d.ts +15 -1
  28. package/dist/workflows/maestro-signals.d.ts +4 -0
  29. package/dist/workflows/maestro-signals.js +5 -1
  30. package/dist/workflows/maestro.js +6 -0
  31. package/dist/workflows/scheduler-signals.d.ts +2 -0
  32. package/dist/workflows/scheduler-signals.js +3 -1
  33. package/dist/workflows/scheduler.d.ts +2 -0
  34. package/dist/workflows/scheduler.js +24 -11
  35. package/dist/workflows/session.js +58 -3
  36. package/dist/workflows/signals.d.ts +9 -1
  37. package/dist/workflows/signals.js +10 -1
  38. package/package.json +1 -1
  39. package/workflow-bundle.js +107 -18
package/CLAUDE.md CHANGED
@@ -27,6 +27,9 @@ src/
27
27
  │ ├── output.ts # Shared CLI output formatting helpers
28
28
  │ └── preflight.ts # Environment preflight checks
29
29
  ├── copilot-bridge.ts # Copilot SDK bridge for Copilot CLI players
30
+ ├── client/
31
+ │ ├── interface.ts # TempoClient TypeScript interface and related types
32
+ │ └── index.ts # TempoClient factory implementation and barrel re-exports
30
33
  ├── worker.ts # Temporal worker setup (used by daemon only)
31
34
  ├── connection.ts # Temporal connection factory (shared by server + CLI)
32
35
  ├── spawn.ts # Cross-platform process spawning helpers
@@ -75,12 +78,15 @@ src/
75
78
  │ ├── stage.ts # Define a stage — fan-out/fan-in tracking for parallel tasks (conductor only)
76
79
  │ ├── stages.ts # List stages and their status (conductor only)
77
80
  │ ├── cancel-stage.ts # Cancel an active stage (conductor only)
81
+ │ ├── release.ts # Release held players (unlock outbox + deliver deferred messages)
82
+ │ ├── pause-ensemble.ts # Pause all sessions and the scheduler ensemble-wide
83
+ │ ├── resume-ensemble.ts # Resume a paused ensemble
78
84
  │ └── helpers.ts # Zod/MCP tool registration wrapper
79
85
  ├── tui/
80
86
  │ ├── index.ts # TUI entry point — connects to Temporal and renders the Ink app
81
87
  │ ├── App.tsx # Root TUI component — chat-focused shell with slash commands
82
88
  │ ├── store.ts # TUI state reducer (phase, players, messages, schedules, static history)
83
- │ ├── client.ts # TempoClient interface and implementation wraps Temporal queries via Maestro
89
+ │ ├── client.ts # Thin re-export shimre-exports createTempoClient from src/client/ for backward compatibility
84
90
  │ ├── commands.ts # Slash command parser and registry (/player, /broadcast, /status, etc.)
85
91
  │ ├── ink-loader.ts # Dynamic ESM loader for Ink (avoids CJS/ESM conflicts)
86
92
  │ ├── ink-context.tsx # React context for injected Ink primitives
@@ -170,9 +176,12 @@ npm test
170
176
  - **Quality Gate**: A named checklist of criteria a conductor tracks to verify a task is complete. Created via `quality_gate` (conductor only), evaluated via `evaluate_gate`, and listed via `gates`. Each criterion has a `pending` → `passed` | `failed` status; the gate's aggregate status is derived automatically (all passed → `passed`, any failed → `failed`, else `open`). Gates are stored in the conductor workflow and survive `continueAsNew`.
171
177
  - **Worktree**: A git worktree provisioned by the conductor for a player, giving them an isolated checkout on a separate branch. Managed via the `worktree` tool (conductor only): `create` provisions the worktree and notifies the player, `remove` cleans up after the task, `list` shows all active worktrees. Worktree assignments are stored in the conductor workflow (`WorktreeEntry` records: player, path, branch, gitRoot, createdAt, createdBy).
172
178
  - **Stage**: A fan-out/fan-in tracking primitive for the conductor. Created via `stage` (conductor only), listing via `stages`, cancelled via `cancel_stage`. Each stage tracks a set of players; when a tracked player sends a `report`, their stage status updates automatically (`waiting` → `reported` or `blocked`). When all players have reported, the conductor is notified that the stage is complete. If `failurePolicy` is `'halt'` (default), a blocker from any player fails the entire stage. Stages are stored in the conductor workflow and survive `continueAsNew`.
179
+ - **Hold / Release**: Controlled ensemble startup mechanism. `load_lineup(hold: true)` spawns all players with locked outboxes and a standby message ("waiting for release") instead of their real task. When ready, the conductor calls `release` (MCP tool or `claude-tempo release` CLI) to unlock outboxes and deliver the actual task messages. Use case: pre-warm a full team before kicking off a long job.
180
+ - **Pause / Resume**: Ensemble-wide mid-session flow control. `pause_ensemble` locks all session outboxes and signals the scheduler to skip fires; `resume_ensemble` reverses both. `stop` outbox entries bypass the pause lock and are always dispatched. Pause state is owned by the per-ensemble Maestro (`maestroSetPaused` signal) and synced to sessions and the scheduler.
181
+ - **Outbox lock**: A workflow-level flag on each session that gates outbox dispatch independently of pause. Used by the hold mechanism (`outboxLocked` query, `releaseHeld` signal) and the pause mechanism (`setPaused` signal, `paused` query). The two flags are independent — a session can be held (locked) but not paused, or paused but not locked.
173
182
  - **Maestro**: Two Maestro workflow variants exist. The **per-ensemble** `claudeMaestroWorkflow` (ID: `claude-maestro-{ensemble}`) monitors a single ensemble — maintains a player snapshot, ring-buffer event log (max 200 entries), an aggregated ensemble chat cache (max 500 entries, refreshed every ~10s via `fetchEnsembleChat` activity), and queues commands for relay to the conductor via `maestroSendCommand`. The ensemble chat cache merges maestro + conductor traffic and is served via the `maestroEnsembleChat` query. The **global** `claudeGlobalMaestroWorkflow` (ID: `claude-maestro-global`) spans all ensembles — aggregates players by ensemble, maintains a cross-ensemble message ring buffer (max 500 entries), and exposes on-demand player/conductor history via `maestroFetchPlayerMessages` and `maestroFetchConductorHistory` updates. Both are implemented in `src/workflows/maestro.ts` with activities in `src/activities/maestro.ts`.
174
- - **TempoClient**: The TUI's API layer (`src/tui/client.ts`) a TypeScript interface and implementation that wraps Temporal queries to the Maestro and conductor workflows. Provides `discoverEnsembles`, `getPlayers`, `getMessages`, `getConductorHistory`, `sendMessage`, `sendCommand`, `getEnsembleChat`, `getGates`, `getStages`, `getWorktrees`, and `terminatePlayer`. Uses Global Maestro as the primary source with graceful fallback to per-ensemble Maestro and direct workflow list queries.
175
- - **Wire protocol**: All Temporal signal, query, update, and workflow names are documented in [`docs/WIRE-PROTOCOL.md`](docs/WIRE-PROTOCOL.md). These names are stable as of v0.10 — renaming or removing any is a breaking change requiring a major version bump.
183
+ - **TempoClient**: The API layer for querying ensemble state (`src/client/`). The interface and types live in `interface.ts`; the factory implementation lives in `index.ts`. Provides `discoverEnsembles`, `getPlayers`, `getMessages`, `getConductorHistory`, `sendMessage`, `sendCommand`, `getEnsembleChat`, `getGates`, `getStages`, `getWorktrees`, and `terminatePlayer`. Uses Global Maestro as the primary source with graceful fallback to per-ensemble Maestro and direct workflow list queries. `src/tui/client.ts` is a thin re-export shim for backward compatibility — new consumers should import from `src/client/` directly.
184
+ - **Wire protocol**: All Temporal signal, query, update, and workflow names are documented in [`docs/WIRE-PROTOCOL.md`](docs/WIRE-PROTOCOL.md). These names are stable as of v0.10 — renaming or removing any is a breaking change requiring a major version bump. **Process**: when adding signals, queries, or updates to any workflow file (`signals.ts`, `scheduler-signals.ts`, `maestro-signals.ts`), update `docs/WIRE-PROTOCOL.md` in the same commit.
176
185
  - **Daemon**: A standalone background process (`src/daemon.ts`) that runs all Temporal workers. Auto-started by any claude-tempo command if not already running. PID stored at `~/.claude-tempo/daemon.pid`; logs at `~/.claude-tempo/daemon.log`. Sessions are now pure MCP clients — they no longer run in-process workers. Managed via `claude-tempo daemon start|stop|status|logs`.
177
186
 
178
187
  ## TUI Key Behaviors
package/README.md CHANGED
@@ -36,6 +36,7 @@ Each session registers as a **player** in Temporal. Players discover each other
36
36
  | 🎭 **Player Types** | Reusable agent definitions with 8 shipped types and three-tier lookup |
37
37
  | 🖥️ **Terminal UI** | Chat-focused TUI with slash commands, overlays, and interactive wizards |
38
38
  | 🌐 **Cross-machine** | Any session that can reach your Temporal server can join the ensemble |
39
+ | ⏸️ **Hold / Pause / Resume** | Pre-warm a full team before delivering tasks; pause and resume mid-session |
39
40
  | 🤖 **Copilot bridge** | Mix Claude Code and GitHub Copilot CLI sessions in the same ensemble |
40
41
 
41
42
  ## Installation
@@ -86,6 +87,21 @@ Stops the daemon, installs the latest version, and restarts automatically. To up
86
87
  claude-tempo upgrade 0.22.0
87
88
  ```
88
89
 
90
+ ## Stopping & Tear Down
91
+
92
+ ```bash
93
+ # Stop a specific player session
94
+ claude-tempo stop my-ensemble player-name
95
+
96
+ # Tear down everything (all sessions, schedulers, and Maestro workflows)
97
+ claude-tempo down --all
98
+
99
+ # Stop the background daemon
100
+ claude-tempo daemon stop
101
+ ```
102
+
103
+ 📖 [Full CLI reference → docs/cli.md](docs/cli.md)
104
+
89
105
  ---
90
106
 
91
107
  ## Core Concepts
@@ -127,6 +143,9 @@ claude-tempo up [ensemble] # first-time setup
127
143
  claude-tempo conduct [ensemble] # start a conductor
128
144
  claude-tempo start [ensemble] # start a player
129
145
  claude-tempo status [ensemble] # list active sessions
146
+ claude-tempo release [ensemble] # release held players (unlock + deliver tasks)
147
+ claude-tempo pause [ensemble] # pause all sessions and the scheduler
148
+ claude-tempo resume [ensemble] # resume a paused ensemble
130
149
  claude-tempo tui # open the terminal UI
131
150
  claude-tempo daemon <sub> # manage the worker daemon
132
151
  claude-tempo upgrade # update to latest
@@ -218,7 +237,7 @@ claude-tempo tui --ensemble my-ensemble # direct ensemble mode
218
237
  The TUI provides a chat-focused shell for managing your ensemble:
219
238
 
220
239
  - **Ensemble chat feed** — live aggregated view of conductor + player traffic; type bare text to message the conductor, `@player message` to message directly
221
- - **Slash commands** — `/recruit`, `/status`, `/schedule`, `/gates`, `/stages`, `/worktree`, and more; type `/help` for the full list
240
+ - **Slash commands** — `/recruit`, `/status`, `/schedule`, `/gates`, `/stages`, `/worktree`, `/go` (release held), `/pause`, `/resume`, and more; type `/help` for the full list
222
241
  - **Interactive overlays and wizards** — step-by-step flows for recruiting players, creating schedules, and managing ensembles
223
242
 
224
243
  📖 [TUI reference → docs/dashboard.md](docs/dashboard.md)
@@ -33,6 +33,12 @@ export interface StartRecruitedSessionInput {
33
33
  allowedTools?: string[];
34
34
  /** Custom claude binary path (from config.claudeBin). */
35
35
  claudeBin?: string;
36
+ /** When true, spawn process but lock outbox and defer initial message until release (warm hold). */
37
+ held?: boolean;
38
+ }
39
+ export interface ReleasePlayerInput {
40
+ ensemble: string;
41
+ targetPlayerId: string;
36
42
  }
37
43
  export interface SpawnProcessInput {
38
44
  targetName: string;
@@ -92,6 +98,7 @@ export interface OutboxActivities {
92
98
  startRecruitedSession(input: StartRecruitedSessionInput): Promise<RecruitResult>;
93
99
  spawnProcess(input: SpawnProcessInput): Promise<OutboxActivityResult>;
94
100
  performEncore(input: PerformEncoreInput): Promise<EncoreResult>;
101
+ releasePlayer(input: ReleasePlayerInput): Promise<OutboxActivityResult>;
95
102
  }
96
103
  /**
97
104
  * Create outbox delivery activities bound to a Temporal client and config.
@@ -101,7 +101,7 @@ function createOutboxActivities(client, config) {
101
101
  return { success: true };
102
102
  },
103
103
  async startRecruitedSession(input) {
104
- const { ensemble, targetName, workDir, isConductor, initialMessage, fromPlayerId, agent, systemPrompt, taskQueue, agentDefinition, agentDefinitionDescription } = input;
104
+ const { ensemble, targetName, workDir, isConductor, initialMessage, fromPlayerId, agent, systemPrompt, taskQueue, agentDefinition, agentDefinitionDescription, held } = input;
105
105
  try {
106
106
  const workflowId = isConductor
107
107
  ? (0, config_1.conductorWorkflowId)(ensemble)
@@ -109,6 +109,11 @@ function createOutboxActivities(client, config) {
109
109
  const { gitRoot, gitBranch } = (0, git_info_1.getGitInfo)(workDir);
110
110
  // Generate a UUID for the session — used for deterministic --resume on encore
111
111
  const sessionId = crypto.randomUUID();
112
+ // Warm hold: process will spawn and go active, but outbox is locked and
113
+ // the initial message is deferred. A standby message is sent instead.
114
+ const standbyMessage = held
115
+ ? 'You are on standby. Your ensemble is loading — other players are still connecting. Wait for your task assignment. Do not start work or send messages yet.'
116
+ : undefined;
112
117
  const sessionInput = {
113
118
  metadata: {
114
119
  playerId: targetName,
@@ -127,15 +132,23 @@ function createOutboxActivities(client, config) {
127
132
  },
128
133
  autoSummary: `Session in ${path.basename(workDir)}`,
129
134
  disableStaleDetection: true,
130
- ...(initialMessage ? {
131
- messages: [{
135
+ // When held: store the initial message for delivery on release, inject standby message instead
136
+ ...(held ? { outboxLocked: true, heldMessage: initialMessage } : {}),
137
+ messages: held
138
+ ? [{
139
+ id: crypto.randomUUID(),
140
+ from: 'system',
141
+ text: standbyMessage,
142
+ timestamp: new Date().toISOString(),
143
+ delivered: false,
144
+ }]
145
+ : (initialMessage ? [{
132
146
  id: crypto.randomUUID(),
133
147
  from: fromPlayerId,
134
148
  text: initialMessage,
135
149
  timestamp: new Date().toISOString(),
136
150
  delivered: false,
137
- }],
138
- } : {}),
151
+ }] : undefined),
139
152
  };
140
153
  await client.workflow.start('claudeSessionWorkflow', {
141
154
  workflowId,
@@ -149,7 +162,7 @@ function createOutboxActivities(client, config) {
149
162
  ClaudeTempoPlayerId: [targetName],
150
163
  },
151
164
  });
152
- log(`Pre-created workflow ${workflowId} for recruit "${targetName}" (sessionId=${sessionId})`);
165
+ log(`Pre-created workflow ${workflowId} for recruit "${targetName}" (sessionId=${sessionId}, held=${!!held})`);
153
166
  return { success: true, sessionId };
154
167
  }
155
168
  catch (err) {
@@ -309,5 +322,28 @@ function createOutboxActivities(client, config) {
309
322
  throw activity_1.ApplicationFailure.nonRetryable(`Encore failed for "${targetPlayerId}": ${err instanceof Error ? err.message : String(err)}`);
310
323
  }
311
324
  },
325
+ async releasePlayer(input) {
326
+ const { ensemble, targetPlayerId } = input;
327
+ try {
328
+ const handle = await (0, resolve_1.resolveSession)(client, ensemble, targetPlayerId);
329
+ if (!handle) {
330
+ throw activity_1.ApplicationFailure.nonRetryable(`No session found for "${targetPlayerId}"`);
331
+ }
332
+ // Check if the session is actually held (outbox locked)
333
+ const isLocked = await handle.query('outboxLocked');
334
+ if (!isLocked) {
335
+ throw activity_1.ApplicationFailure.nonRetryable(`Cannot release "${targetPlayerId}" — session is not held (outbox not locked).`);
336
+ }
337
+ // Signal the session to release — unlocks outbox and delivers held message
338
+ await handle.signal('releaseHeld');
339
+ log(`Released held session "${targetPlayerId}"`);
340
+ return { success: true };
341
+ }
342
+ catch (err) {
343
+ if (err instanceof activity_1.ApplicationFailure)
344
+ throw err;
345
+ throw activity_1.ApplicationFailure.nonRetryable(`Release failed for "${targetPlayerId}": ${err instanceof Error ? err.message : String(err)}`);
346
+ }
347
+ },
312
348
  };
313
349
  }
@@ -77,6 +77,18 @@ interface DaemonOpts extends CliOverrides {
77
77
  subcommand?: string;
78
78
  }
79
79
  export declare function daemon(opts: DaemonOpts): Promise<void>;
80
+ interface ReleaseOpts extends CliOverrides {
81
+ ensemble: string;
82
+ }
83
+ /** Release all held sessions in an ensemble (unlock outbox, deliver initial messages). */
84
+ export declare function release(opts: ReleaseOpts): Promise<void>;
85
+ interface PauseResumeOpts extends CliOverrides {
86
+ ensemble: string;
87
+ }
88
+ /** Pause an entire ensemble — sessions, scheduler, and maestro. */
89
+ export declare function pause(opts: PauseResumeOpts): Promise<void>;
90
+ /** Resume an entire ensemble — sessions, scheduler, and maestro. */
91
+ export declare function resume(opts: PauseResumeOpts): Promise<void>;
80
92
  export declare function help(): void;
81
93
  export declare function version(): void;
82
94
  interface UpgradeOpts extends CliOverrides {
@@ -45,6 +45,9 @@ exports.broadcast = broadcast;
45
45
  exports.encore = encore;
46
46
  exports.ensembleCommand = ensembleCommand;
47
47
  exports.daemon = daemon;
48
+ exports.release = release;
49
+ exports.pause = pause;
50
+ exports.resume = resume;
48
51
  exports.help = help;
49
52
  exports.version = version;
50
53
  exports.upgrade = upgrade;
@@ -61,6 +64,7 @@ const git_info_1 = require("../git-info");
61
64
  const connection_1 = require("../connection");
62
65
  const signals_1 = require("../workflows/signals");
63
66
  const scheduler_signals_1 = require("../workflows/scheduler-signals");
67
+ const maestro_signals_1 = require("../workflows/maestro-signals");
64
68
  const preflight_1 = require("./preflight");
65
69
  const mcp_1 = require("./mcp");
66
70
  const loader_1 = require("../ensemble/loader");
@@ -437,6 +441,7 @@ const SEARCH_ATTRIBUTES = [
437
441
  { name: 'ClaudeTempoPlayerId', type: 'Keyword' },
438
442
  { name: 'ClaudeTempoStatus', type: 'Keyword' },
439
443
  { name: 'ClaudeTempoPlayerType', type: 'Keyword' },
444
+ { name: 'ClaudeTempoIsConductor', type: 'Bool' },
440
445
  ];
441
446
  async function isTemporalReachable(config) {
442
447
  try {
@@ -1942,6 +1947,123 @@ async function daemon(opts) {
1942
1947
  process.exit(1);
1943
1948
  }
1944
1949
  }
1950
+ /** Release all held sessions in an ensemble (unlock outbox, deliver initial messages). */
1951
+ async function release(opts) {
1952
+ const config = (0, config_1.getConfig)(opts);
1953
+ let connection;
1954
+ try {
1955
+ connection = await Promise.race([
1956
+ (0, connection_1.createTemporalConnection)(config),
1957
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
1958
+ ]);
1959
+ }
1960
+ catch {
1961
+ out.error(`Cannot connect to Temporal at ${config.temporalAddress}`);
1962
+ process.exit(1);
1963
+ return;
1964
+ }
1965
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
1966
+ const query = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running" AND ClaudeTempoEnsemble = "${opts.ensemble.replace(/["\\\n\r]/g, '')}"`;
1967
+ let released = 0;
1968
+ for await (const wf of client.workflow.list({ query })) {
1969
+ try {
1970
+ const handle = client.workflow.getHandle(wf.workflowId);
1971
+ const locked = await handle.query(signals_1.outboxLockedQuery);
1972
+ if (locked) {
1973
+ await handle.signal(signals_1.releaseHeldSignal);
1974
+ released++;
1975
+ const sa = wf.searchAttributes || {};
1976
+ const playerId = Array.isArray(sa.ClaudeTempoPlayerId) ? String(sa.ClaudeTempoPlayerId[0]) : wf.workflowId;
1977
+ out.log(` ${out.dim('released')} ${playerId}`);
1978
+ }
1979
+ }
1980
+ catch {
1981
+ // Skip failed queries (terminated workflows, etc.)
1982
+ }
1983
+ }
1984
+ if (released > 0) {
1985
+ out.success(`Released ${released} player${released !== 1 ? 's' : ''}`);
1986
+ }
1987
+ else {
1988
+ out.log('No held players found.');
1989
+ }
1990
+ await connection.close();
1991
+ }
1992
+ /** Pause an entire ensemble — sessions, scheduler, and maestro. */
1993
+ async function pause(opts) {
1994
+ const config = (0, config_1.getConfig)(opts);
1995
+ let connection;
1996
+ try {
1997
+ connection = await Promise.race([
1998
+ (0, connection_1.createTemporalConnection)(config),
1999
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
2000
+ ]);
2001
+ }
2002
+ catch {
2003
+ out.error(`Cannot connect to Temporal at ${config.temporalAddress}`);
2004
+ process.exit(1);
2005
+ return;
2006
+ }
2007
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
2008
+ await setPausedState(client, opts.ensemble, true);
2009
+ out.success(`Ensemble "${opts.ensemble}" paused`);
2010
+ await connection.close();
2011
+ }
2012
+ /** Resume an entire ensemble — sessions, scheduler, and maestro. */
2013
+ async function resume(opts) {
2014
+ const config = (0, config_1.getConfig)(opts);
2015
+ let connection;
2016
+ try {
2017
+ connection = await Promise.race([
2018
+ (0, connection_1.createTemporalConnection)(config),
2019
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
2020
+ ]);
2021
+ }
2022
+ catch {
2023
+ out.error(`Cannot connect to Temporal at ${config.temporalAddress}`);
2024
+ process.exit(1);
2025
+ return;
2026
+ }
2027
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
2028
+ await setPausedState(client, opts.ensemble, false);
2029
+ out.success(`Ensemble "${opts.ensemble}" resumed`);
2030
+ await connection.close();
2031
+ }
2032
+ /** Shared logic: set paused state across all ensemble components. */
2033
+ async function setPausedState(client, ensemble, paused) {
2034
+ // 1. Signal maestro hub
2035
+ try {
2036
+ const mh = client.workflow.getHandle((0, config_1.maestroWorkflowId)(ensemble));
2037
+ await mh.signal(maestro_signals_1.maestroSetPausedSignal, paused);
2038
+ }
2039
+ catch {
2040
+ // Maestro may not be running — non-critical
2041
+ }
2042
+ // 2. Signal all active sessions
2043
+ const sanitized = ensemble.replace(/["\\\n\r]/g, '');
2044
+ const query = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running" AND ClaudeTempoEnsemble = "${sanitized}"`;
2045
+ let count = 0;
2046
+ for await (const wf of client.workflow.list({ query })) {
2047
+ try {
2048
+ const handle = client.workflow.getHandle(wf.workflowId);
2049
+ await handle.signal(signals_1.setPausedSignal, paused);
2050
+ count++;
2051
+ }
2052
+ catch {
2053
+ // Skip failed signals
2054
+ }
2055
+ }
2056
+ out.log(` ${out.dim(paused ? 'paused' : 'resumed')} ${count} session${count !== 1 ? 's' : ''}`);
2057
+ // 3. Signal scheduler
2058
+ try {
2059
+ const sh = client.workflow.getHandle((0, config_1.schedulerWorkflowId)(ensemble));
2060
+ await sh.signal(scheduler_signals_1.setSchedulerPausedSignal, paused);
2061
+ out.log(` ${out.dim(paused ? 'paused' : 'resumed')} scheduler`);
2062
+ }
2063
+ catch {
2064
+ // Scheduler may not be running — non-critical
2065
+ }
2066
+ }
1945
2067
  function help() {
1946
2068
  console.log(`
1947
2069
  ${out.bold('claude-tempo')} — Multi-session Claude Code coordination via Temporal
@@ -1963,6 +2085,9 @@ ${out.bold('Commands:')}
1963
2085
  ${out.cyan('ensemble')} <sub> Manage saved ensemble lineups (save/list/show)
1964
2086
  ${out.cyan('broadcast')} <message> Send a message to all active players
1965
2087
  ${out.cyan('encore')} <name> Revive a stale player session (reconnect with context)
2088
+ ${out.cyan('release')} [ensemble] Release all held players (unlock outbox, deliver messages)
2089
+ ${out.cyan('pause')} [ensemble] Pause an ensemble (sessions, scheduler, maestro)
2090
+ ${out.cyan('resume')} [ensemble] Resume a paused ensemble
1966
2091
  ${out.cyan('agent-types')} <sub> Manage player type definitions (list/show/init)
1967
2092
  ${out.cyan('daemon')} <sub> Manage the worker daemon (start/stop/status/logs)
1968
2093
  ${out.cyan('upgrade')} [version] Upgrade claude-tempo to latest (or specific version)
package/dist/cli.js CHANGED
@@ -260,6 +260,24 @@ async function main() {
260
260
  });
261
261
  break;
262
262
  }
263
+ case 'release':
264
+ await (0, commands_1.release)({
265
+ ensemble: args.ensemble || ensemble,
266
+ ...overrides,
267
+ });
268
+ break;
269
+ case 'pause':
270
+ await (0, commands_1.pause)({
271
+ ensemble: args.ensemble || ensemble,
272
+ ...overrides,
273
+ });
274
+ break;
275
+ case 'resume':
276
+ await (0, commands_1.resume)({
277
+ ensemble: args.ensemble || ensemble,
278
+ ...overrides,
279
+ });
280
+ break;
263
281
  case 'ensemble':
264
282
  await (0, commands_1.ensembleCommand)({
265
283
  subcommand: args.positional[1],
@@ -0,0 +1,10 @@
1
+ /**
2
+ * TempoClient — factory implementation and barrel re-exports.
3
+ *
4
+ * Extracted from `src/tui/client.ts` so that non-TUI consumers (CLI, tests,
5
+ * external integrations) can create a TempoClient without depending on Ink/React.
6
+ */
7
+ import { Client } from '@temporalio/client';
8
+ import type { TempoClient } from './interface';
9
+ export type { TempoClient, EnsembleSummary } from './interface';
10
+ export declare function createTempoClient(client: Client): TempoClient;