claude-tempo 0.7.0 → 0.9.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.
package/CLAUDE.md CHANGED
@@ -21,19 +21,23 @@ src/
21
21
  ├── workflows/
22
22
  │ ├── session.ts # claude-session workflow
23
23
  │ └── signals.ts # Signal/query type definitions
24
+ ├── activities/
25
+ │ ├── outbox.ts # Outbox delivery activities (cue, report, stop, recruit)
26
+ │ └── schedule-fire.ts # Schedule fire activity
24
27
  ├── tools/
25
28
  │ ├── ensemble.ts # Discover active sessions
26
- │ ├── cue.ts # Send message to peer
29
+ │ ├── cue.ts # Send message to peer (via outbox)
27
30
  │ ├── set-name.ts # Set session name
28
31
  │ ├── set-part.ts # Update own summary
29
32
  │ ├── resolve.ts # Search-attribute session lookup
30
33
  │ ├── listen.ts # Manual message check
31
- │ ├── recruit.ts # Spawn new session
32
- │ ├── report.ts # Report to conductor
33
- │ ├── terminate.ts # Terminate a session
34
+ │ ├── recruit.ts # Spawn new session (via outbox)
35
+ │ ├── report.ts # Report to conductor (via outbox)
36
+ │ ├── stop.ts # Stop a session (via outbox)
34
37
  │ └── helpers.ts # Zod/MCP tool registration wrapper
35
38
  ├── types.ts # Shared type definitions
36
39
  ├── channel.ts # Claude channel notification helper
40
+ ├── git-info.ts # Git repository detection helper
37
41
  └── config.ts # Env var handling
38
42
  ```
39
43
 
@@ -59,6 +63,10 @@ npm test
59
63
  > **Important**: Always run `npm run build` after changing workflow code (`src/workflows/`).
60
64
  > The build pre-bundles workflows into `workflow-bundle.js` so all workers use identical code.
61
65
 
66
+ > **Dual workers**: Each session runs two Temporal workers — a shared `claude-tempo` queue
67
+ > (workflows + delivery activities) and a per-host `claude-tempo-{hostname}` queue (spawn activities only).
68
+ > Both are created via `createWorkers()` in `src/worker.ts`.
69
+
62
70
  ## Key Concepts
63
71
 
64
72
  - **Player**: A Claude Code session registered as a Temporal workflow
@@ -66,8 +74,11 @@ npm test
66
74
  - **Ensemble**: The set of all active players, namespaced by `CLAUDE_TEMPO_ENSEMBLE`
67
75
  - **Cue**: A message sent to a player by name via Temporal signal
68
76
  - **Part**: A player's description of what it's working on
69
- - **Recruit**: Spawning a new Claude Code session as a player
77
+ - **Recruit**: Spawning a new Claude Code session as a player. The workflow is pre-created with the initial message before the process spawns, ensuring reliable delivery.
70
78
  - **set_name**: Players start with a random hex ID; `set_name` updates the `ClaudeTempoPlayerId` search attribute to a human-readable name
79
+ - **Session status**: Each session has a status (`pending` → `active` → `stale`) tracked via `ClaudeTempoStatus` search attribute. Pre-created workflows start as `pending`, transition to `active` when the process connects, and become `stale` if messages go undelivered for 3+ minutes.
80
+ - **Outbox**: Outbound requests (cue, report, stop, recruit) go through the session's own workflow outbox instead of directly signaling other workflows. The workflow's dispatch loop processes entries via activities, decoupling tools from cross-workflow signaling.
81
+ - **Per-host task queues**: Each host runs a `claude-tempo-{hostname}` activity worker for local-only operations (e.g., `spawnProcess`). This enables cross-machine recruiting — the `recruit` tool accepts an optional `host` parameter to route the spawn to a remote machine's task queue.
71
82
 
72
83
  ## Dashboard
73
84
 
package/README.md CHANGED
@@ -1,6 +1,13 @@
1
- # claude-tempo
2
-
3
- Multi-session [Claude Code](https://claude.ai/code) coordination via [Temporal](https://temporal.io).
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
4
+ <source media="(prefers-color-scheme: light)" srcset="assets/logo-light.svg">
5
+ <img alt="claude-tempo" src="assets/logo-light.svg" height="140">
6
+ </picture>
7
+ </p>
8
+ <p align="center">
9
+ Multi-session <a href="https://claude.ai/code">Claude Code</a> coordination via <a href="https://temporal.io">Temporal</a>.
10
+ </p>
4
11
 
5
12
  Multiple Claude Code sessions discover each other, exchange messages in real time, and coordinate work — across machines, not just localhost.
6
13
 
@@ -174,7 +181,7 @@ Ensemble: myband
174
181
  Building the REST endpoints
175
182
  /Users/me/projects/app feat/api my-machine.local
176
183
 
177
- bob
184
+ bob (pending)
178
185
  Working on the dashboard
179
186
  /Users/me/projects/app feat/ui my-machine.local
180
187
 
@@ -210,7 +217,7 @@ These tools are available inside Claude Code sessions connected to claude-tempo:
210
217
  | `listen` | Manually check for pending messages. |
211
218
  | `recruit` | Spawn a new Claude Code session in a directory. Can recruit a conductor with `conductor: true`. |
212
219
  | `report` | Send updates to the conductor. No-op if no conductor exists. |
213
- | `terminate` | Terminate a player session by name. |
220
+ | `stop` | Stop a player session by name. |
214
221
  | `schedule` | Create a one-shot or recurring schedule to cue a player. |
215
222
  | `unschedule` | Cancel a named schedule. |
216
223
  | `schedules` | List all active schedules. |
@@ -398,6 +405,23 @@ Sessions start with a random 8-character hex ID. Set a name at launch with `-n`
398
405
  - Names must contain only letters, numbers, hyphens, and underscores
399
406
  - The name "conductor" is reserved for conductor sessions
400
407
 
408
+ ### Session status lifecycle
409
+
410
+ Each session has a status that tracks its connection state:
411
+
412
+ | Status | Meaning |
413
+ |--------|---------|
414
+ | `pending` | Workflow created by `recruit`, but the Claude Code process hasn't connected yet |
415
+ | `active` | Session is running and responsive |
416
+ | `stale` | Messages have gone undelivered for 3+ minutes — the session is likely disconnected |
417
+
418
+ Status transitions:
419
+ - **`pending` → `active`** — when the spawned session connects and sends its `updateMetadata` signal
420
+ - **`active` → `stale`** — when undelivered messages exceed the stale threshold (3 minutes)
421
+ - Any status → **terminated** — on graceful shutdown or `stop`
422
+
423
+ `claude-tempo status` shows `(pending)` and `(stale)` indicators next to player names. The `ClaudeTempoStatus` search attribute is also set, so you can filter sessions by status in the Temporal UI (e.g., `ClaudeTempoStatus = "stale"`).
424
+
401
425
  ### Terminal support
402
426
 
403
427
  `recruit` and the CLI detect your terminal automatically:
@@ -409,10 +433,13 @@ Sessions start with a random 8-character hex ID. Set a name at launch with `-n`
409
433
  | Terminal.app | ✓ | — | — |
410
434
  | gnome-terminal | — | ✓ | — |
411
435
  | konsole / xterm | — | ✓ | — |
436
+ | Windows Terminal | — | — | ✓ (tabs) |
412
437
  | cmd.exe / PowerShell | — | — | ✓ |
413
438
 
414
439
  macOS terminals preserve the full shell environment (fish, zsh, bash) including node version managers (fnm, nvm).
415
440
 
441
+ Windows Terminal is detected automatically via the `WT_SESSION` environment variable. When running inside Windows Terminal, recruited sessions open as new tabs (with the player name as the tab title) instead of separate cmd.exe windows.
442
+
416
443
  ## Configuration
417
444
 
418
445
  Run `claude-tempo config` to save Temporal connection settings so you don't need flags or env vars every time:
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <!-- Ultra-minimal metronome icon: triangle + pendulum (dark mode) -->
3
+ <!-- Metronome body — single-stroke triangle -->
4
+ <path d="M32 8 L14 54 L50 54 Z" stroke="#FAF3EE" stroke-width="3" fill="none" stroke-linejoin="round"/>
5
+ <!-- Pendulum arm (angled right) -->
6
+ <line x1="32" y1="46" x2="44" y2="14" stroke="#E07A5F" stroke-width="3" stroke-linecap="round"/>
7
+ <!-- Pivot dot -->
8
+ <circle cx="32" cy="46" r="3" fill="#E07A5F"/>
9
+ </svg>
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
+ <!-- Ultra-minimal metronome icon: triangle + pendulum -->
3
+ <!-- Metronome body — single-stroke triangle -->
4
+ <path d="M32 8 L14 54 L50 54 Z" stroke="#1B2838" stroke-width="3" fill="none" stroke-linejoin="round"/>
5
+ <!-- Pendulum arm (angled right) -->
6
+ <line x1="32" y1="46" x2="44" y2="14" stroke="#E07A5F" stroke-width="3" stroke-linecap="round"/>
7
+ <!-- Pivot dot -->
8
+ <circle cx="32" cy="46" r="3" fill="#E07A5F"/>
9
+ </svg>
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 140" fill="none">
2
+ <!-- Ultra-minimal metronome: triangle outline + pendulum line (dark mode) -->
3
+ <!-- Metronome body — single-stroke triangle -->
4
+ <path d="M160 18 L122 100 L198 100 Z" stroke="#FAF3EE" stroke-width="3" fill="none" stroke-linejoin="round"/>
5
+ <!-- Pendulum arm (angled right ~18deg) -->
6
+ <line x1="160" y1="88" x2="182" y2="24" stroke="#E07A5F" stroke-width="3" stroke-linecap="round"/>
7
+ <!-- Pivot dot -->
8
+ <circle cx="160" cy="88" r="3.5" fill="#E07A5F"/>
9
+ <!-- Text -->
10
+ <text x="160" y="132" text-anchor="middle" font-family="'JetBrains Mono','SF Mono','Consolas',monospace" font-size="18" font-weight="600" fill="#FAF3EE" letter-spacing="-0.5">claude-tempo</text>
11
+ </svg>
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 140" fill="none">
2
+ <!-- Ultra-minimal metronome: triangle outline + pendulum line -->
3
+ <!-- Metronome body — single-stroke triangle -->
4
+ <path d="M160 18 L122 100 L198 100 Z" stroke="#1B2838" stroke-width="3" fill="none" stroke-linejoin="round"/>
5
+ <!-- Pendulum arm (angled right ~18deg) -->
6
+ <line x1="160" y1="88" x2="182" y2="24" stroke="#E07A5F" stroke-width="3" stroke-linecap="round"/>
7
+ <!-- Pivot dot -->
8
+ <circle cx="160" cy="88" r="3.5" fill="#E07A5F"/>
9
+ <!-- Text -->
10
+ <text x="160" y="132" text-anchor="middle" font-family="'JetBrains Mono','SF Mono','Consolas',monospace" font-size="18" font-weight="600" fill="#1B2838" letter-spacing="-0.5">claude-tempo</text>
11
+ </svg>
@@ -0,0 +1,60 @@
1
+ import { Client } from '@temporalio/client';
2
+ import { Config } from '../config';
3
+ import { AgentType } from '../types';
4
+ export interface DeliverCueInput {
5
+ ensemble: string;
6
+ fromPlayerId: string;
7
+ targetPlayerId: string;
8
+ message: string;
9
+ }
10
+ export interface DeliverReportInput {
11
+ ensemble: string;
12
+ fromPlayerId: string;
13
+ text: string;
14
+ reportType: 'result' | 'blocker' | 'question';
15
+ }
16
+ export interface TerminateSessionInput {
17
+ ensemble: string;
18
+ targetPlayerId: string;
19
+ terminatedBy: string;
20
+ }
21
+ export interface StartRecruitedSessionInput {
22
+ ensemble: string;
23
+ targetName: string;
24
+ workDir: string;
25
+ isConductor: boolean;
26
+ initialMessage?: string;
27
+ fromPlayerId: string;
28
+ agent: AgentType;
29
+ systemPrompt?: string;
30
+ taskQueue: string;
31
+ }
32
+ export interface SpawnProcessInput {
33
+ targetName: string;
34
+ workDir: string;
35
+ isConductor: boolean;
36
+ agent: AgentType;
37
+ systemPrompt?: string;
38
+ ensemble: string;
39
+ temporalAddress: string;
40
+ temporalNamespace: string;
41
+ temporalApiKey?: string;
42
+ temporalTlsCertPath?: string;
43
+ temporalTlsKeyPath?: string;
44
+ }
45
+ export interface OutboxActivityResult {
46
+ success: boolean;
47
+ error?: string;
48
+ }
49
+ export interface OutboxActivities {
50
+ deliverCue(input: DeliverCueInput): Promise<OutboxActivityResult>;
51
+ deliverReport(input: DeliverReportInput): Promise<OutboxActivityResult>;
52
+ terminateSession(input: TerminateSessionInput): Promise<OutboxActivityResult>;
53
+ startRecruitedSession(input: StartRecruitedSessionInput): Promise<OutboxActivityResult>;
54
+ spawnProcess(input: SpawnProcessInput): Promise<OutboxActivityResult>;
55
+ }
56
+ /**
57
+ * Create outbox delivery activities bound to a Temporal client and config.
58
+ * The returned object is registered with the worker as activities.
59
+ */
60
+ export declare function createOutboxActivities(client: Client, config: Config): OutboxActivities;
@@ -0,0 +1,203 @@
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.createOutboxActivities = createOutboxActivities;
37
+ const client_1 = require("@temporalio/client");
38
+ const activity_1 = require("@temporalio/activity");
39
+ const os = __importStar(require("os"));
40
+ const config_1 = require("../config");
41
+ const git_info_1 = require("../git-info");
42
+ const spawn_1 = require("../spawn");
43
+ const config_2 = require("../config");
44
+ const log = (...args) => console.error('[claude-tempo:outbox]', ...args);
45
+ // ── Helper: resolve session by player name ──
46
+ async function resolveSession(client, ensemble, playerName) {
47
+ const query = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"`;
48
+ for await (const wf of client.workflow.list({ query })) {
49
+ try {
50
+ const handle = client.workflow.getHandle(wf.workflowId);
51
+ const metadata = await handle.query('getMetadata');
52
+ if (metadata.ensemble === ensemble && metadata.playerId === playerName) {
53
+ return handle;
54
+ }
55
+ }
56
+ catch {
57
+ // Workflow may have just completed — skip
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+ /**
63
+ * Create outbox delivery activities bound to a Temporal client and config.
64
+ * The returned object is registered with the worker as activities.
65
+ */
66
+ function createOutboxActivities(client, config) {
67
+ return {
68
+ async deliverCue(input) {
69
+ const { ensemble, fromPlayerId, targetPlayerId, message } = input;
70
+ const handle = await resolveSession(client, ensemble, targetPlayerId);
71
+ if (!handle) {
72
+ throw activity_1.ApplicationFailure.nonRetryable(`No active session found for "${targetPlayerId}"`);
73
+ }
74
+ await handle.signal('receiveMessage', { from: fromPlayerId, text: message });
75
+ return { success: true };
76
+ },
77
+ async deliverReport(input) {
78
+ const { ensemble, fromPlayerId, text, reportType } = input;
79
+ const conductorId = (0, config_1.conductorWorkflowId)(ensemble);
80
+ const handle = client.workflow.getHandle(conductorId);
81
+ await handle.signal('playerReport', { playerId: fromPlayerId, text, type: reportType });
82
+ return { success: true };
83
+ },
84
+ async terminateSession(input) {
85
+ const { ensemble, targetPlayerId, terminatedBy } = input;
86
+ const handle = await resolveSession(client, ensemble, targetPlayerId);
87
+ if (!handle) {
88
+ throw activity_1.ApplicationFailure.nonRetryable(`No active session found for "${targetPlayerId}"`);
89
+ }
90
+ // Signal target to mark as terminated
91
+ await handle.signal('updateMetadata', { status: 'terminated', terminatedBy });
92
+ // Notify conductor about the termination (best effort)
93
+ try {
94
+ const conductorId = (0, config_1.conductorWorkflowId)(ensemble);
95
+ const conductorHandle = client.workflow.getHandle(conductorId);
96
+ await conductorHandle.signal('receiveMessage', {
97
+ from: 'system',
98
+ text: `Session "${targetPlayerId}" was terminated by ${terminatedBy}.`,
99
+ });
100
+ }
101
+ catch {
102
+ // Conductor may not exist — that's fine
103
+ }
104
+ return { success: true };
105
+ },
106
+ async startRecruitedSession(input) {
107
+ const { ensemble, targetName, workDir, isConductor, initialMessage, fromPlayerId, agent, systemPrompt, taskQueue } = input;
108
+ try {
109
+ const workflowId = isConductor
110
+ ? (0, config_1.conductorWorkflowId)(ensemble)
111
+ : (0, config_1.sessionWorkflowId)(ensemble, targetName);
112
+ const { gitRoot, gitBranch } = (0, git_info_1.getGitInfo)(workDir);
113
+ const sessionInput = {
114
+ metadata: {
115
+ playerId: targetName,
116
+ ensemble,
117
+ hostname: os.hostname(),
118
+ workDir,
119
+ gitRoot,
120
+ gitBranch,
121
+ isConductor,
122
+ agentType: agent,
123
+ status: 'pending',
124
+ },
125
+ autoSummary: `Session in ${require('path').basename(workDir)}`,
126
+ disableStaleDetection: true,
127
+ ...(initialMessage ? {
128
+ messages: [{
129
+ id: require('crypto').randomUUID(),
130
+ from: fromPlayerId,
131
+ text: initialMessage,
132
+ timestamp: new Date().toISOString(),
133
+ delivered: false,
134
+ }],
135
+ } : {}),
136
+ };
137
+ await client.workflow.start('claudeSessionWorkflow', {
138
+ workflowId,
139
+ taskQueue,
140
+ args: [sessionInput],
141
+ workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
142
+ searchAttributes: {
143
+ ...(gitRoot ? { ClaudeTempoGitRoot: [gitRoot] } : {}),
144
+ ClaudeTempoHostname: [os.hostname()],
145
+ ClaudeTempoEnsemble: [ensemble],
146
+ ClaudeTempoPlayerId: [targetName],
147
+ },
148
+ });
149
+ log(`Pre-created workflow ${workflowId} for recruit "${targetName}"`);
150
+ return { success: true };
151
+ }
152
+ catch (err) {
153
+ throw activity_1.ApplicationFailure.nonRetryable(`Failed to start recruited session "${targetName}": ${err instanceof Error ? err.message : String(err)}`);
154
+ }
155
+ },
156
+ async spawnProcess(input) {
157
+ const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, temporalApiKey, temporalTlsCertPath, temporalTlsKeyPath } = input;
158
+ try {
159
+ if (agent === 'copilot') {
160
+ const { pid } = (0, spawn_1.spawnCopilotBridge)({
161
+ name: targetName,
162
+ ensemble,
163
+ temporalAddress,
164
+ temporalNamespace,
165
+ temporalApiKey,
166
+ temporalTlsCertPath,
167
+ temporalTlsKeyPath,
168
+ isConductor,
169
+ workDir,
170
+ });
171
+ log(`Spawned copilot-bridge (pid ${pid}) in ${workDir} as "${targetName}"`);
172
+ }
173
+ else {
174
+ const spawnArgs = [
175
+ '--dangerously-skip-permissions',
176
+ '--dangerously-load-development-channels', 'server:claude-tempo',
177
+ '-n', targetName,
178
+ ...(systemPrompt ? ['--system-prompt', systemPrompt] : []),
179
+ ];
180
+ const envVars = {
181
+ [config_2.ENV.ENSEMBLE]: ensemble,
182
+ [config_2.ENV.CONDUCTOR]: isConductor ? 'true' : '',
183
+ [config_2.ENV.PLAYER_NAME]: targetName,
184
+ [config_2.ENV.TEMPORAL_ADDRESS]: temporalAddress,
185
+ [config_2.ENV.TEMPORAL_NAMESPACE]: temporalNamespace,
186
+ };
187
+ if (temporalApiKey)
188
+ envVars[config_2.ENV.TEMPORAL_API_KEY] = temporalApiKey;
189
+ if (temporalTlsCertPath)
190
+ envVars[config_2.ENV.TEMPORAL_TLS_CERT_PATH] = temporalTlsCertPath;
191
+ if (temporalTlsKeyPath)
192
+ envVars[config_2.ENV.TEMPORAL_TLS_KEY_PATH] = temporalTlsKeyPath;
193
+ const { pid } = (0, spawn_1.spawnInTerminal)(spawnArgs, workDir, envVars);
194
+ log(`Spawned claude process (pid ${pid}) in ${workDir} as "${targetName}"`);
195
+ }
196
+ return { success: true };
197
+ }
198
+ catch (err) {
199
+ throw activity_1.ApplicationFailure.nonRetryable(`Failed to spawn process for "${targetName}": ${err instanceof Error ? err.message : String(err)}`);
200
+ }
201
+ },
202
+ };
203
+ }
@@ -97,7 +97,7 @@ async function start(opts) {
97
97
  if (opts.replace) {
98
98
  out.log(`Stopping existing conductor for ensemble "${opts.ensemble}"...`);
99
99
  try {
100
- await handle.signal(signals_1.shutdownSignal);
100
+ await handle.signal(signals_1.updateMetadataSignal, { status: 'terminated' });
101
101
  // Wait briefly for graceful shutdown
102
102
  for (let i = 0; i < 10; i++) {
103
103
  await new Promise(r => setTimeout(r, 500));
@@ -228,6 +228,7 @@ async function status(opts) {
228
228
  host: meta.hostname || '',
229
229
  conductor: meta.isConductor || false,
230
230
  agentType: meta.agentType || 'claude',
231
+ status: meta.status || 'active',
231
232
  });
232
233
  }
233
234
  catch {
@@ -280,8 +281,11 @@ async function status(opts) {
280
281
  for (const s of members) {
281
282
  const role = s.conductor ? out.yellow(' (conductor)') : '';
282
283
  const agent = s.agentType === 'copilot' ? out.dim(' [copilot]') : '';
284
+ const statusLabel = s.status === 'stale' ? out.yellow(' (stale)')
285
+ : s.status === 'pending' ? out.dim(' (pending)')
286
+ : '';
283
287
  const name = out.bold(s.name);
284
- out.log(` ${name}${role}${agent}`);
288
+ out.log(` ${name}${role}${statusLabel}${agent}`);
285
289
  if (s.part)
286
290
  out.log(` ${out.dim(s.part)}`);
287
291
  const details = [s.workDir, s.branch, s.host].filter(Boolean).join(' ');
@@ -382,6 +386,7 @@ const SEARCH_ATTRIBUTES = [
382
386
  { name: 'ClaudeTempoGitRoot', type: 'Keyword' },
383
387
  { name: 'ClaudeTempoEnsemble', type: 'Keyword' },
384
388
  { name: 'ClaudeTempoPlayerId', type: 'Keyword' },
389
+ { name: 'ClaudeTempoStatus', type: 'Keyword' },
385
390
  ];
386
391
  function isTemporalReachable(config) {
387
392
  return (0, connection_1.createTemporalConnection)(config)
@@ -926,7 +931,7 @@ async function stop(opts) {
926
931
  continue;
927
932
  }
928
933
  }
929
- await handle.signal(signals_1.shutdownSignal);
934
+ await handle.signal(signals_1.updateMetadataSignal, { status: 'terminated' });
930
935
  stopped++;
931
936
  out.log(` ${out.dim('stopped')} ${wf.workflowId}`);
932
937
  }
@@ -986,9 +991,9 @@ async function stopByName(client, name, config, ensemble) {
986
991
  // No conductor or conductor not running — fine
987
992
  }
988
993
  }
989
- // Send shutdown signal (graceful)
994
+ // Send termination status update (graceful)
990
995
  try {
991
- await handle.signal(signals_1.shutdownSignal);
996
+ await handle.signal(signals_1.updateMetadataSignal, { status: 'terminated' });
992
997
  out.success(`Stopped "${name}"`);
993
998
  }
994
999
  catch {
package/dist/config.d.ts CHANGED
@@ -85,6 +85,8 @@ export interface ConfigWithSources {
85
85
  * Used by `claude-tempo config show` to help users debug.
86
86
  */
87
87
  export declare function getConfigWithSources(overrides?: CliOverrides): ConfigWithSources;
88
+ /** Build a per-host task queue name for cross-machine activities: {taskQueue}-{hostname} */
89
+ export declare function hostTaskQueue(taskQueue: string, hostname: string): string;
88
90
  /** Build a workflow ID for a player session: claude-session-{ensemble}-{playerId} */
89
91
  export declare function sessionWorkflowId(ensemble: string, playerId: string): string;
90
92
  /** Build a workflow ID for a conductor: claude-session-{ensemble}-conductor */
package/dist/config.js CHANGED
@@ -7,6 +7,7 @@ exports.loadTemporalCliConfig = loadTemporalCliConfig;
7
7
  exports.parseTemporalYaml = parseTemporalYaml;
8
8
  exports.getConfig = getConfig;
9
9
  exports.getConfigWithSources = getConfigWithSources;
10
+ exports.hostTaskQueue = hostTaskQueue;
10
11
  exports.sessionWorkflowId = sessionWorkflowId;
11
12
  exports.conductorWorkflowId = conductorWorkflowId;
12
13
  exports.schedulerWorkflowId = schedulerWorkflowId;
@@ -239,6 +240,10 @@ function getConfigWithSources(overrides = {}) {
239
240
  },
240
241
  };
241
242
  }
243
+ /** Build a per-host task queue name for cross-machine activities: {taskQueue}-{hostname} */
244
+ function hostTaskQueue(taskQueue, hostname) {
245
+ return `${taskQueue}-${hostname}`;
246
+ }
242
247
  /** Build a workflow ID for a player session: claude-session-{ensemble}-{playerId} */
243
248
  function sessionWorkflowId(ensemble, playerId) {
244
249
  return `claude-session-${ensemble}-${playerId}`;
@@ -179,7 +179,7 @@ async function main() {
179
179
  `- listen: Check for pending messages\n` +
180
180
  `- recruit: Spawn a new player session\n` +
181
181
  `- report: Report to the conductor\n` +
182
- `- terminate: Terminate a session\n\n` +
182
+ `- stop: Stop a session\n\n` +
183
183
  `When you receive a message from another session, treat it like a coworker asking for help — respond promptly using your MCP tools.`,
184
184
  },
185
185
  excludedTools: ['write_powershell', 'read_powershell', 'list_powershell'],
@@ -391,7 +391,7 @@ async function main() {
391
391
  polling = false;
392
392
  clearInterval(interval);
393
393
  try {
394
- await handle.signal('shutdown');
394
+ await handle.signal('updateMetadata', { status: 'terminated', terminatedBy: 'system' });
395
395
  }
396
396
  catch {
397
397
  // workflow may already be gone
@@ -0,0 +1,4 @@
1
+ export declare function getGitInfo(workDir: string): {
2
+ gitRoot?: string;
3
+ gitBranch?: string;
4
+ };
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getGitInfo = getGitInfo;
4
+ const child_process_1 = require("child_process");
5
+ function getGitInfo(workDir) {
6
+ try {
7
+ const gitRoot = (0, child_process_1.execSync)('git rev-parse --show-toplevel', {
8
+ cwd: workDir,
9
+ encoding: 'utf-8',
10
+ stdio: ['pipe', 'pipe', 'pipe'],
11
+ }).trim();
12
+ let gitBranch;
13
+ try {
14
+ gitBranch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
15
+ cwd: workDir,
16
+ encoding: 'utf-8',
17
+ stdio: ['pipe', 'pipe', 'pipe'],
18
+ }).trim();
19
+ }
20
+ catch {
21
+ // not on a branch
22
+ }
23
+ return { gitRoot, gitBranch };
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }