claude-tempo 0.2.0 → 0.2.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/README.md CHANGED
@@ -156,7 +156,6 @@ The `claude-tempo` CLI handles setup, session management, and diagnostics.
156
156
  ```
157
157
  --temporal-address <addr> Temporal server address (default: localhost:7233)
158
158
  -n, --name <name> Set the player name for the session (start/conduct/up)
159
- --agent <claude|copilot> Agent type to spawn (default: claude; start/conduct)
160
159
  --skip-preflight Skip preflight checks (start/conduct)
161
160
  --background, -d Run Temporal in background (server only)
162
161
  --dir <path> Target directory for init (default: cwd)
@@ -187,7 +186,6 @@ ok You're all set!
187
186
 
188
187
  What next?
189
188
  claude-tempo start myband Add a player session
190
- claude-tempo start myband --agent copilot -n copilot-1 Add a Copilot player
191
189
  claude-tempo status myband See who's active
192
190
  Or ask the conductor to recruit players for you
193
191
  ```
@@ -395,8 +393,14 @@ When a Claude Code session crashes or is closed without graceful shutdown, its T
395
393
 
396
394
  This means you don't need to manually clean up crashed sessions — just `cue` the dead player and the system handles the rest.
397
395
 
396
+ ## Known limitations
397
+
398
+ - **`recruit` requires manual acknowledgment**: Recruited Claude Code sessions use `--dangerously-load-development-channels` to enable channel-based message delivery. Claude Code shows an interactive confirmation prompt that must be manually acknowledged (press Enter) in the spawned terminal window. This will be resolved once claude-tempo is published as an approved channel plugin. Copilot bridge sessions do not have this limitation.
399
+
398
400
  ## Copilot CLI integration (experimental)
399
401
 
402
+ > **Warning:** Copilot bridge support is **experimental** and subject to breaking changes. Copilot bridge sessions are headless — they have no interactive terminal. A Claude conductor (or custom Temporal client) is required to send them work via `cue`. Do not rely on this feature for production workflows.
403
+
400
404
  GitHub Copilot CLI sessions can join an ensemble via the **Copilot bridge**. The bridge uses the [Copilot SDK](https://github.com/github/copilot-sdk) to spawn a Copilot session with claude-tempo as an MCP server, and injects incoming messages as prompts.
401
405
 
402
406
  ### Setup
@@ -413,54 +417,26 @@ You also need:
413
417
 
414
418
  ### Starting a Copilot player
415
419
 
416
- ```bash
417
- # Via CLI (recommended):
418
- claude-tempo start --agent copilot -n copilot-dev
419
-
420
- # Or directly with env vars (Linux/macOS):
421
- CLAUDE_TEMPO_ENSEMBLE=default COPILOT_BRIDGE_NAME=copilot-dev npx ts-node src/copilot-bridge.ts
422
-
423
- # Or directly with env vars (Windows PowerShell):
424
- $env:TEMPORAL_ADDRESS="localhost:7233"; $env:CLAUDE_TEMPO_ENSEMBLE="default"; $env:COPILOT_BRIDGE_NAME="copilot-dev"; npx ts-node src/copilot-bridge.ts
425
-
426
- # Or from any session in the ensemble, recruit one:
427
- # "Recruit a copilot session named 'copilot-dev' with agent copilot"
428
- ```
420
+ The easiest way to add a Copilot player is via the `recruit` tool from any active session in the ensemble. The `agent` parameter accepts `"claude"` (default) or `"copilot"`:
429
421
 
430
- The CLI `--agent` flag and the `recruit` tool's `agent` parameter both accept `"claude"` (default) or `"copilot"`.
422
+ > "Recruit a copilot session named 'copilot-dev' in /repos/my-project with agent copilot"
431
423
 
432
- ### Shell shortcuts
424
+ This spawns a headless bridge process, registers it as a Temporal workflow, and sets the player name automatically.
433
425
 
434
- Add these functions to your shell profile to simplify launching Copilot bridge sessions:
426
+ <details>
427
+ <summary>Advanced: running the bridge directly</summary>
435
428
 
436
- **Linux/macOS** add to `~/.bashrc` or `~/.zshrc`:
429
+ You can also start the bridge manually with environment variables:
437
430
 
438
431
  ```bash
439
- copilot-tempo() {
440
- CLAUDE_TEMPO_ENSEMBLE="${1:-default}" COPILOT_BRIDGE_NAME="${2}" \
441
- npx ts-node /path/to/claude-tempo/src/copilot-bridge.ts
442
- }
443
- ```
444
-
445
- **Windows** — add to your PowerShell `$PROFILE`:
432
+ # Linux/macOS
433
+ CLAUDE_TEMPO_ENSEMBLE=default COPILOT_BRIDGE_NAME=copilot-dev npx ts-node src/copilot-bridge.ts
446
434
 
447
- ```powershell
448
- function copilot-tempo($ensemble = "default", $name = "") {
449
- $env:TEMPORAL_ADDRESS = "localhost:7233"
450
- $env:CLAUDE_TEMPO_ENSEMBLE = $ensemble
451
- $env:COPILOT_BRIDGE_NAME = $name
452
- npx ts-node C:\path\to\claude-tempo\src\copilot-bridge.ts
453
- $env:CLAUDE_TEMPO_ENSEMBLE = ""
454
- $env:COPILOT_BRIDGE_NAME = ""
455
- }
435
+ # Windows PowerShell
436
+ $env:TEMPORAL_ADDRESS="localhost:7233"; $env:CLAUDE_TEMPO_ENSEMBLE="default"; $env:COPILOT_BRIDGE_NAME="copilot-dev"; npx ts-node src/copilot-bridge.ts
456
437
  ```
457
438
 
458
- Usage:
459
-
460
- ```bash
461
- copilot-tempo # join "default" ensemble, auto-generated name
462
- copilot-tempo my-project copilot-1 # join "my-project" ensemble as "copilot-1"
463
- ```
439
+ </details>
464
440
 
465
441
  ### How it works
466
442
 
@@ -481,10 +457,8 @@ copilot-tempo my-project copilot-1 # join "my-project" ensemble as "copilot-1"
481
457
 
482
458
  ### Limitations
483
459
 
484
- - **`recruit` requires manual acknowledgment (Claude backend)**: Recruited Claude Code sessions use `--dangerously-load-development-channels` to enable channel-based message delivery. Claude Code shows an interactive confirmation prompt that must be manually acknowledged (press Enter) in the spawned terminal window. This will be resolved once claude-tempo is published as an approved channel plugin. The Copilot backend does not have this limitation.
485
460
  - **No interactive access** — Copilot bridge sessions run in the background. Unlike Claude Code sessions where you can chat directly, bridge sessions only respond to cues from other players. To send messages to a bridge session, use `cue` from another player or signal the workflow directly via the Temporal CLI.
486
- - **Conductor polling latency** — Copilot conductors poll for messages every 2 seconds, unlike Claude Code conductors which receive instant channel notifications. This adds slight latency to orchestration.
487
- - **No push-based message delivery** — the bridge polls for messages (2s interval), unlike Claude Code sessions which receive instant channel notifications.
461
+ - **Polling latency** — the bridge polls for messages every 2 seconds, unlike Claude Code sessions which receive instant channel notifications. This adds slight latency to message delivery and orchestration.
488
462
  - **Copilot sessions must be spawned via the bridge** to participate (not standalone Copilot CLI).
489
463
  - **The `@github/copilot-sdk` adds ~243MB** to node_modules when installed.
490
464
  - **Node 20+ required for Copilot features** — the `@github/copilot-sdk` requires Node.js 20 or later. The rest of claude-tempo works on Node 18+.
@@ -1,10 +1,11 @@
1
+ import { AgentType } from '../types';
1
2
  interface StartOpts {
2
3
  ensemble: string;
3
4
  conductor: boolean;
4
5
  temporalAddress: string;
5
6
  name?: string;
6
7
  skipPreflight?: boolean;
7
- agent: 'claude' | 'copilot';
8
+ agent: AgentType;
8
9
  }
9
10
  export declare function start(opts: StartOpts): Promise<void>;
10
11
  interface StatusOpts {
@@ -87,38 +87,14 @@ async function start(opts) {
87
87
  }
88
88
  out.log(`Starting ${out.bold(role)} in ensemble ${out.cyan(opts.ensemble)}${opts.agent === 'copilot' ? out.dim(' (copilot)') : ''}`);
89
89
  if (opts.agent === 'copilot') {
90
- // Spawn copilot-bridge as a detached headless subprocess
91
- const isDev = __filename.endsWith('.ts');
92
- const cmd = isDev ? 'npx' : 'node';
93
- const cmdArgs = isDev
94
- ? ['ts-node', (0, path_1.resolve)(__dirname, '..', '..', 'src', 'copilot-bridge.ts')]
95
- : [(0, path_1.resolve)(__dirname, '..', 'copilot-bridge.js')];
96
- // Log bridge output for debugging
97
- const fs = require('fs');
98
- const logName = opts.name || `copilot-${Date.now()}`;
99
- const logPath = (0, path_1.join)(workDir, 'logs', `${logName}.log`);
100
- fs.mkdirSync((0, path_1.join)(workDir, 'logs'), { recursive: true });
101
- const logFd = fs.openSync(logPath, 'a');
102
- let child;
103
- try {
104
- child = (0, child_process_1.spawn)(cmd, cmdArgs, {
105
- cwd: workDir,
106
- detached: true,
107
- stdio: ['ignore', logFd, logFd],
108
- env: {
109
- ...process.env,
110
- CLAUDE_TEMPO_ENSEMBLE: opts.ensemble,
111
- COPILOT_BRIDGE_NAME: opts.name || '',
112
- TEMPORAL_ADDRESS: opts.temporalAddress,
113
- ...(opts.conductor ? { CLAUDE_TEMPO_CONDUCTOR: 'true' } : {}),
114
- },
115
- });
116
- child.unref();
117
- }
118
- finally {
119
- fs.closeSync(logFd);
120
- }
121
- out.success(`Launched copilot bridge${opts.name ? ` "${opts.name}"` : ''} (pid ${child.pid ?? 'unknown'})`);
90
+ const { pid } = (0, spawn_1.spawnCopilotBridge)({
91
+ name: opts.name || `copilot-${Date.now()}`,
92
+ ensemble: opts.ensemble,
93
+ temporalAddress: opts.temporalAddress,
94
+ isConductor: opts.conductor,
95
+ workDir,
96
+ });
97
+ out.success(`Launched copilot bridge${opts.name ? ` "${opts.name}"` : ''} (pid ${pid ?? 'unknown'})`);
122
98
  }
123
99
  else {
124
100
  const claudeArgs = [
@@ -129,13 +105,13 @@ async function start(opts) {
129
105
  claudeArgs.push('-n', opts.name);
130
106
  }
131
107
  const envVars = {
132
- CLAUDE_TEMPO_ENSEMBLE: opts.ensemble,
108
+ [config_1.ENV.ENSEMBLE]: opts.ensemble,
133
109
  };
134
110
  if (opts.conductor) {
135
- envVars.CLAUDE_TEMPO_CONDUCTOR = 'true';
111
+ envVars[config_1.ENV.CONDUCTOR] = 'true';
136
112
  }
137
113
  if (opts.name) {
138
- envVars.CLAUDE_TEMPO_PLAYER_NAME = opts.name;
114
+ envVars[config_1.ENV.PLAYER_NAME] = opts.name;
139
115
  }
140
116
  const { pid } = (0, spawn_1.spawnInTerminal)(claudeArgs, workDir, envVars);
141
117
  out.success(`Launched ${role} session${opts.name ? ` "${opts.name}"` : ''} (pid ${pid ?? 'unknown'})`);
@@ -454,8 +430,8 @@ async function up(opts) {
454
430
  if (opts.name)
455
431
  claudeArgs.push('-n', opts.name);
456
432
  const { pid } = (0, spawn_1.spawnInTerminal)(claudeArgs, process.cwd(), {
457
- CLAUDE_TEMPO_ENSEMBLE: opts.ensemble,
458
- CLAUDE_TEMPO_CONDUCTOR: 'true',
433
+ [config_1.ENV.ENSEMBLE]: opts.ensemble,
434
+ [config_1.ENV.CONDUCTOR]: 'true',
459
435
  });
460
436
  console.log();
461
437
  out.success('You\'re all set!');
package/dist/cli.js CHANGED
@@ -37,11 +37,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const commands_1 = require("./cli/commands");
38
38
  const preflight_1 = require("./cli/preflight");
39
39
  const out = __importStar(require("./cli/output"));
40
+ const config_1 = require("./config");
40
41
  function parseArgs(argv) {
41
42
  const result = {
42
43
  command: 'help',
43
44
  positional: [],
44
- temporalAddress: process.env.TEMPORAL_ADDRESS || 'localhost:7233',
45
+ temporalAddress: process.env[config_1.ENV.TEMPORAL_ADDRESS] || 'localhost:7233',
45
46
  dir: process.cwd(),
46
47
  skipPreflight: false,
47
48
  background: false,
@@ -99,7 +100,7 @@ function parseArgs(argv) {
99
100
  }
100
101
  async function main() {
101
102
  const args = parseArgs(process.argv.slice(2));
102
- const ensemble = args.positional[1] || process.env.CLAUDE_TEMPO_ENSEMBLE || 'default';
103
+ const ensemble = args.positional[1] || process.env[config_1.ENV.ENSEMBLE] || 'default';
103
104
  switch (args.command) {
104
105
  case 'conduct':
105
106
  await (0, commands_1.start)({
package/dist/config.d.ts CHANGED
@@ -1,3 +1,15 @@
1
+ /** Environment variable name constants — use these instead of string literals. */
2
+ export declare const ENV: {
3
+ readonly ENSEMBLE: "CLAUDE_TEMPO_ENSEMBLE";
4
+ readonly CONDUCTOR: "CLAUDE_TEMPO_CONDUCTOR";
5
+ readonly PLAYER_NAME: "CLAUDE_TEMPO_PLAYER_NAME";
6
+ readonly TASK_QUEUE: "CLAUDE_TEMPO_TASK_QUEUE";
7
+ readonly BRIDGE_NAME: "COPILOT_BRIDGE_NAME";
8
+ readonly BRIDGE_MODE: "CLAUDE_TEMPO_BRIDGE_MODE";
9
+ readonly BRIDGE_MODEL: "COPILOT_BRIDGE_MODEL";
10
+ readonly TEMPORAL_ADDRESS: "TEMPORAL_ADDRESS";
11
+ readonly TEMPORAL_NAMESPACE: "TEMPORAL_NAMESPACE";
12
+ };
1
13
  export interface Config {
2
14
  temporalAddress: string;
3
15
  temporalNamespace: string;
package/dist/config.js CHANGED
@@ -1,14 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ENV = void 0;
3
4
  exports.getConfig = getConfig;
4
5
  exports.sessionWorkflowId = sessionWorkflowId;
5
6
  exports.conductorWorkflowId = conductorWorkflowId;
7
+ /** Environment variable name constants — use these instead of string literals. */
8
+ exports.ENV = {
9
+ ENSEMBLE: 'CLAUDE_TEMPO_ENSEMBLE',
10
+ CONDUCTOR: 'CLAUDE_TEMPO_CONDUCTOR',
11
+ PLAYER_NAME: 'CLAUDE_TEMPO_PLAYER_NAME',
12
+ TASK_QUEUE: 'CLAUDE_TEMPO_TASK_QUEUE',
13
+ BRIDGE_NAME: 'COPILOT_BRIDGE_NAME',
14
+ BRIDGE_MODE: 'CLAUDE_TEMPO_BRIDGE_MODE',
15
+ BRIDGE_MODEL: 'COPILOT_BRIDGE_MODEL',
16
+ TEMPORAL_ADDRESS: 'TEMPORAL_ADDRESS',
17
+ TEMPORAL_NAMESPACE: 'TEMPORAL_NAMESPACE',
18
+ };
6
19
  function getConfig() {
7
20
  return {
8
- temporalAddress: process.env.TEMPORAL_ADDRESS ?? 'localhost:7233',
9
- temporalNamespace: process.env.TEMPORAL_NAMESPACE ?? 'default',
10
- taskQueue: process.env.CLAUDE_TEMPO_TASK_QUEUE ?? 'claude-tempo',
11
- ensemble: process.env.CLAUDE_TEMPO_ENSEMBLE ?? 'default',
21
+ temporalAddress: process.env[exports.ENV.TEMPORAL_ADDRESS] ?? 'localhost:7233',
22
+ temporalNamespace: process.env[exports.ENV.TEMPORAL_NAMESPACE] ?? 'default',
23
+ taskQueue: process.env[exports.ENV.TASK_QUEUE] ?? 'claude-tempo',
24
+ ensemble: process.env[exports.ENV.ENSEMBLE] ?? 'default',
12
25
  };
13
26
  }
14
27
  /** Build a workflow ID for a player session: claude-session-{ensemble}-{playerId} */
@@ -101,8 +101,8 @@ async function createSessionWithTimeout(copilotClient, sessionConfig, timeoutMs
101
101
  }
102
102
  async function main() {
103
103
  const config = (0, config_1.getConfig)();
104
- const playerName = process.env.COPILOT_BRIDGE_NAME;
105
- const model = process.env.COPILOT_BRIDGE_MODEL;
104
+ const playerName = process.env[config_1.ENV.BRIDGE_NAME];
105
+ const model = process.env[config_1.ENV.BRIDGE_MODEL];
106
106
  const workDir = process.cwd();
107
107
  log(`Starting Copilot bridge in ${workDir} (ensemble: ${config.ensemble})`);
108
108
  // Connect Temporal client (for polling only — the MCP server child process runs its own worker)
@@ -117,10 +117,10 @@ async function main() {
117
117
  // `claude-session-{ensemble}-{playerId}`, where playerId comes from
118
118
  // CLAUDE_TEMPO_PLAYER_NAME or a random hex. We pass CLAUDE_TEMPO_PLAYER_NAME
119
119
  // to the MCP server env so both sides agree on the ID.
120
- const isConductor = !!process.env.CLAUDE_TEMPO_CONDUCTOR;
120
+ const isConductor = !!process.env[config_1.ENV.CONDUCTOR];
121
121
  const playerIdForWorkflow = isConductor
122
122
  ? 'conductor'
123
- : (process.env.CLAUDE_TEMPO_PLAYER_NAME || playerName || `copilot-${Date.now()}`);
123
+ : (process.env[config_1.ENV.PLAYER_NAME] || playerName || `copilot-${Date.now()}`);
124
124
  const expectedWorkflowId = `claude-session-${config.ensemble}-${playerIdForWorkflow}`;
125
125
  // Build the MCP server command — always use the compiled dist/server.js
126
126
  // Run `npm run build` (or `pnpm build`) before using the bridge.
@@ -134,13 +134,13 @@ async function main() {
134
134
  const serverArgs = [serverJsPath];
135
135
  const mcpEnv = {
136
136
  ...cleanEnv(),
137
- CLAUDE_TEMPO_ENSEMBLE: config.ensemble,
138
- TEMPORAL_ADDRESS: config.temporalAddress,
139
- TEMPORAL_NAMESPACE: config.temporalNamespace,
140
- CLAUDE_TEMPO_TASK_QUEUE: config.taskQueue,
141
- CLAUDE_TEMPO_CONDUCTOR: process.env.CLAUDE_TEMPO_CONDUCTOR || '',
142
- CLAUDE_TEMPO_BRIDGE_MODE: '1', // disable MCP server's message poller — bridge handles delivery
143
- CLAUDE_TEMPO_PLAYER_NAME: playerIdForWorkflow, // ensures MCP server uses same workflow ID
137
+ [config_1.ENV.ENSEMBLE]: config.ensemble,
138
+ [config_1.ENV.TEMPORAL_ADDRESS]: config.temporalAddress,
139
+ [config_1.ENV.TEMPORAL_NAMESPACE]: config.temporalNamespace,
140
+ [config_1.ENV.TASK_QUEUE]: config.taskQueue,
141
+ [config_1.ENV.CONDUCTOR]: process.env[config_1.ENV.CONDUCTOR] || '',
142
+ [config_1.ENV.BRIDGE_MODE]: '1', // disable MCP server's message poller — bridge handles delivery
143
+ [config_1.ENV.PLAYER_NAME]: playerIdForWorkflow, // ensures MCP server uses same workflow ID
144
144
  };
145
145
  // Spawn Copilot SDK client and session
146
146
  const copilotClient = new CopilotClient({
package/dist/server.js CHANGED
@@ -79,8 +79,8 @@ function getGitInfo(workDir) {
79
79
  }
80
80
  async function main() {
81
81
  // Only activate when explicitly opted in via CLAUDE_TEMPO_ENSEMBLE
82
- if (!process.env.CLAUDE_TEMPO_ENSEMBLE) {
83
- log('CLAUDE_TEMPO_ENSEMBLE not set — MCP server idle (no workflow started)');
82
+ if (!process.env[config_1.ENV.ENSEMBLE]) {
83
+ log(`${config_1.ENV.ENSEMBLE} not set — MCP server idle (no workflow started)`);
84
84
  // Keep the process alive so Claude Code doesn't see a crash, but do nothing
85
85
  const transport = new stdio_js_1.StdioServerTransport();
86
86
  const idleServer = new mcp_js_1.McpServer({ name: 'claude-tempo', version: '0.1.0' });
@@ -88,8 +88,8 @@ async function main() {
88
88
  return;
89
89
  }
90
90
  const config = (0, config_1.getConfig)();
91
- const isConductor = process.env.CLAUDE_TEMPO_CONDUCTOR === 'true';
92
- let playerId = isConductor ? 'conductor' : (process.env.CLAUDE_TEMPO_PLAYER_NAME || crypto.randomBytes(4).toString('hex'));
91
+ const isConductor = process.env[config_1.ENV.CONDUCTOR] === 'true';
92
+ let playerId = isConductor ? 'conductor' : (process.env[config_1.ENV.PLAYER_NAME] || crypto.randomBytes(4).toString('hex'));
93
93
  const getPlayerId = () => playerId;
94
94
  const setPlayerId = (id) => { playerId = id; };
95
95
  const workDir = process.cwd();
@@ -185,7 +185,7 @@ async function main() {
185
185
  // Skip when running under the Copilot bridge: the bridge has its own poller that
186
186
  // injects messages via sendAndWait. If both pollers run, this one wins the race and
187
187
  // sends messages via notifications/claude/channel — which Copilot doesn't understand.
188
- const isBridgeMode = process.env.CLAUDE_TEMPO_BRIDGE_MODE === '1';
188
+ const isBridgeMode = process.env[config_1.ENV.BRIDGE_MODE] === '1';
189
189
  const stopPoller = isBridgeMode
190
190
  ? () => { } // no-op — bridge handles message delivery
191
191
  : (0, channel_1.startMessagePoller)(handle, async (messages) => {
package/dist/spawn.d.ts CHANGED
@@ -31,3 +31,22 @@ export declare function buildClaudeCommand(claudeBin: string, claudeArgs: string
31
31
  export declare function spawnInTerminal(claudeArgs: string[], workDir: string, envVars: Record<string, string>): {
32
32
  pid: number | undefined;
33
33
  };
34
+ export interface CopilotBridgeOpts {
35
+ name: string;
36
+ ensemble: string;
37
+ temporalAddress: string;
38
+ isConductor?: boolean;
39
+ workDir: string;
40
+ /** Directory for log and PID files. Defaults to `logs/` inside workDir. */
41
+ logDir?: string;
42
+ }
43
+ export interface CopilotBridgeResult {
44
+ pid: number | undefined;
45
+ logPath: string;
46
+ pidPath: string;
47
+ }
48
+ /**
49
+ * Spawn a copilot bridge as a detached headless subprocess.
50
+ * Sets up log file, PID file, and all required env vars.
51
+ */
52
+ export declare function spawnCopilotBridge(opts: CopilotBridgeOpts): CopilotBridgeResult;
package/dist/spawn.js CHANGED
@@ -6,10 +6,12 @@ exports.detectMacTerminal = detectMacTerminal;
6
6
  exports.findLinuxTerminal = findLinuxTerminal;
7
7
  exports.buildClaudeCommand = buildClaudeCommand;
8
8
  exports.spawnInTerminal = spawnInTerminal;
9
+ exports.spawnCopilotBridge = spawnCopilotBridge;
9
10
  const child_process_1 = require("child_process");
10
11
  const fs_1 = require("fs");
11
12
  const path_1 = require("path");
12
13
  const os_1 = require("os");
14
+ const config_1 = require("./config");
13
15
  const log = (...args) => console.error('[claude-tempo:spawn]', ...args);
14
16
  /** POSIX shell-safe single-quoting (works in bash, zsh, and fish) */
15
17
  function shellQuote(s) {
@@ -207,3 +209,51 @@ function spawnInTerminal(claudeArgs, workDir, envVars) {
207
209
  child.unref();
208
210
  return { pid: child.pid };
209
211
  }
212
+ /**
213
+ * Resolve the path to the compiled copilot-bridge.js.
214
+ * In dev (ts-node), returns a ts-node command; in production, returns the dist path.
215
+ */
216
+ function resolveBridgePath() {
217
+ const isDev = __filename.endsWith('.ts');
218
+ if (isDev) {
219
+ return { cmd: 'npx', args: ['ts-node', (0, path_1.resolve)(__dirname, 'copilot-bridge.ts')] };
220
+ }
221
+ return { cmd: 'node', args: [(0, path_1.resolve)(__dirname, 'copilot-bridge.js')] };
222
+ }
223
+ /**
224
+ * Spawn a copilot bridge as a detached headless subprocess.
225
+ * Sets up log file, PID file, and all required env vars.
226
+ */
227
+ function spawnCopilotBridge(opts) {
228
+ const { cmd, args } = resolveBridgePath();
229
+ const logDirPath = opts.logDir || (0, path_1.join)(opts.workDir, 'logs');
230
+ const logName = opts.name || `copilot-${Date.now()}`;
231
+ const logPath = (0, path_1.join)(logDirPath, `${logName}.log`);
232
+ const pidPath = (0, path_1.join)(logDirPath, `${logName}.pid`);
233
+ (0, fs_1.mkdirSync)(logDirPath, { recursive: true });
234
+ const logFd = (0, fs_1.openSync)(logPath, 'a');
235
+ let child;
236
+ try {
237
+ child = (0, child_process_1.spawn)(cmd, args, {
238
+ cwd: opts.workDir,
239
+ detached: true,
240
+ stdio: ['ignore', logFd, logFd],
241
+ env: {
242
+ ...process.env,
243
+ [config_1.ENV.ENSEMBLE]: opts.ensemble,
244
+ [config_1.ENV.BRIDGE_NAME]: opts.name,
245
+ [config_1.ENV.TEMPORAL_ADDRESS]: opts.temporalAddress,
246
+ ...(opts.isConductor ? { [config_1.ENV.CONDUCTOR]: 'true' } : {}),
247
+ },
248
+ });
249
+ child.unref();
250
+ }
251
+ finally {
252
+ (0, fs_1.closeSync)(logFd);
253
+ }
254
+ if (child.pid != null) {
255
+ (0, fs_1.writeFileSync)(pidPath, String(child.pid));
256
+ }
257
+ log(`Spawned copilot-bridge (pid ${child.pid}) in ${opts.workDir} as "${opts.name}"`);
258
+ return { pid: child.pid, logPath, pidPath };
259
+ }
@@ -1,42 +1,8 @@
1
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
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.registerRecruitTool = registerRecruitTool;
37
- const path = __importStar(require("path"));
38
- const child_process_1 = require("child_process");
39
4
  const zod_1 = require("zod");
5
+ const config_1 = require("../config");
40
6
  const spawn_1 = require("../spawn");
41
7
  const resolve_1 = require("./resolve");
42
8
  const helpers_1 = require("./helpers");
@@ -84,25 +50,13 @@ function registerRecruitTool(server, client, config, getPlayerId) {
84
50
  }
85
51
  // Spawn the session using the selected backend
86
52
  if (agent === 'copilot') {
87
- // Use ts-node in dev, compiled JS in production
88
- const isDev = __filename.endsWith('.ts');
89
- const cmd = isDev ? 'npx' : 'node';
90
- const cmdArgs = isDev
91
- ? ['ts-node', path.resolve(__dirname, '..', 'src', 'copilot-bridge.ts')]
92
- : [path.resolve(__dirname, '..', 'copilot-bridge.js')];
93
- const child = (0, child_process_1.spawn)(cmd, cmdArgs, {
94
- cwd: workDir,
95
- detached: true,
96
- stdio: 'ignore',
97
- env: {
98
- ...process.env,
99
- CLAUDE_TEMPO_ENSEMBLE: config.ensemble,
100
- COPILOT_BRIDGE_NAME: name,
101
- TEMPORAL_ADDRESS: config.temporalAddress,
102
- },
53
+ const { pid } = (0, spawn_1.spawnCopilotBridge)({
54
+ name,
55
+ ensemble: config.ensemble,
56
+ temporalAddress: config.temporalAddress,
57
+ workDir,
103
58
  });
104
- child.unref();
105
- log(`Spawned copilot-bridge (pid ${child.pid}) in ${workDir} as "${name}"`);
59
+ log(`Spawned copilot-bridge (pid ${pid}) in ${workDir} as "${name}"`);
106
60
  }
107
61
  else {
108
62
  const spawnArgs = [
@@ -111,8 +65,8 @@ function registerRecruitTool(server, client, config, getPlayerId) {
111
65
  '-n', name,
112
66
  ];
113
67
  const { pid } = (0, spawn_1.spawnInTerminal)(spawnArgs, workDir, {
114
- CLAUDE_TEMPO_ENSEMBLE: config.ensemble,
115
- CLAUDE_TEMPO_CONDUCTOR: '',
68
+ [config_1.ENV.ENSEMBLE]: config.ensemble,
69
+ [config_1.ENV.CONDUCTOR]: '',
116
70
  });
117
71
  log(`Spawned claude process (pid ${pid}) in ${workDir} as "${name}"`);
118
72
  }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export type AgentType = 'claude' | 'copilot';
1
2
  export interface SessionMetadata {
2
3
  playerId: string;
3
4
  ensemble: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-tempo",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "MCP server for multi-session Claude Code coordination via Temporal",
5
5
  "repository": {
6
6
  "type": "git",