claude-tempo 0.13.0 → 0.14.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.
@@ -32,6 +32,8 @@ interface UpOpts extends CliOverrides {
32
32
  }
33
33
  export declare function up(opts: UpOpts): Promise<void>;
34
34
  interface DownOpts extends CliOverrides {
35
+ ensemble: string;
36
+ all: boolean;
35
37
  removeMcp: boolean;
36
38
  dir: string;
37
39
  }
@@ -661,7 +661,7 @@ async function up(opts) {
661
661
  }
662
662
  else {
663
663
  // Default conductor name so the Claude Code session name matches the ensemble role
664
- const sessionName = opts.name || 'conductor';
664
+ const sessionName = opts.name || lineup?.conductor?.name || 'conductor';
665
665
  // Resolve conductor agent type from lineup
666
666
  const conductorType = lineup?.conductor?.agent && lineup.conductor.agent !== 'default' && lineup.conductor.agent !== 'copilot'
667
667
  ? lineup.conductor.agent // custom agent path
@@ -893,16 +893,26 @@ function parseDuration(s) {
893
893
  }
894
894
  async function down(opts) {
895
895
  const config = (0, config_1.getConfig)(opts);
896
+ const ensembleName = opts.ensemble;
897
+ // Validate ensemble name before interpolating into query strings
898
+ const nameErr = (0, validation_1.validateEnsembleName)(ensembleName);
899
+ if (nameErr) {
900
+ out.error(nameErr);
901
+ process.exit(1);
902
+ }
896
903
  out.heading('claude-tempo teardown');
897
- // Step 1: Terminate all active workflows
904
+ out.log(` Ensemble: ${out.bold(ensembleName)}${opts.all ? ' (--all: will also stop Temporal server)' : ''}`);
905
+ // Step 1: Terminate workflows for the target ensemble
898
906
  const temporalUp = await isTemporalReachable(config);
907
+ let hasRemainingWorkflows = false;
899
908
  if (temporalUp) {
900
909
  try {
901
910
  const connection = await (0, connection_1.createTemporalConnection)(config);
902
911
  const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
903
- const query = 'WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"';
912
+ // Terminate session workflows scoped to this ensemble
913
+ const sessionQuery = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running" AND ClaudeTempoEnsemble = "${ensembleName}"`;
904
914
  let terminated = 0;
905
- for await (const wf of client.workflow.list({ query })) {
915
+ for await (const wf of client.workflow.list({ query: sessionQuery })) {
906
916
  try {
907
917
  const handle = client.workflow.getHandle(wf.workflowId);
908
918
  await handle.terminate('claude-tempo down');
@@ -910,12 +920,25 @@ async function down(opts) {
910
920
  }
911
921
  catch { /* already closed */ }
912
922
  }
923
+ // Also terminate the ensemble's scheduler workflow
924
+ try {
925
+ const schedulerHandle = client.workflow.getHandle((0, config_1.schedulerWorkflowId)(ensembleName));
926
+ await schedulerHandle.terminate('claude-tempo down');
927
+ terminated++;
928
+ }
929
+ catch { /* no scheduler or already closed */ }
930
+ // Check if other workflows still running (to decide whether to kill Temporal)
931
+ const allRunningQuery = 'ExecutionStatus = "Running"';
932
+ for await (const _ of client.workflow.list({ query: allRunningQuery })) {
933
+ hasRemainingWorkflows = true;
934
+ break;
935
+ }
913
936
  await connection.close();
914
937
  if (terminated > 0) {
915
- out.success(`Terminated ${terminated} active session${terminated !== 1 ? 's' : ''}`);
938
+ out.success(`Terminated ${terminated} workflow${terminated !== 1 ? 's' : ''} in ensemble "${ensembleName}"`);
916
939
  }
917
940
  else {
918
- out.log(` ${out.dim('No active sessions to terminate')}`);
941
+ out.warn(`No active workflows found for ensemble "${ensembleName}"`);
919
942
  }
920
943
  }
921
944
  catch {
@@ -924,8 +947,8 @@ async function down(opts) {
924
947
  }
925
948
  // Step 2: Kill bridge processes via PID files
926
949
  killBridgeProcesses();
927
- // Step 3: Stop Temporal server
928
- if (temporalUp) {
950
+ // Step 3: Stop Temporal server — only if --all flag or no other workflows remain
951
+ if (temporalUp && (opts.all || !hasRemainingWorkflows)) {
929
952
  // Find and kill the temporal dev server process
930
953
  try {
931
954
  if (process.platform === 'win32') {
@@ -941,6 +964,9 @@ async function down(opts) {
941
964
  out.warn('Could not stop Temporal server (may need to stop it manually)');
942
965
  }
943
966
  }
967
+ else if (temporalUp) {
968
+ out.log(` ${out.dim('Temporal server left running (other ensembles still active)')}`);
969
+ }
944
970
  else {
945
971
  out.log(` ${out.dim('Temporal not running')}`);
946
972
  }
package/dist/cli.js CHANGED
@@ -205,6 +205,8 @@ async function main() {
205
205
  break;
206
206
  case 'down':
207
207
  await (0, commands_1.down)({
208
+ ensemble,
209
+ all: args.all,
208
210
  removeMcp: !args.keepMcp,
209
211
  dir: args.dir,
210
212
  ...overrides,
@@ -28,6 +28,12 @@ function loadLineup(filePath) {
28
28
  if (!/^[a-zA-Z0-9_-]+$/.test(p.name)) {
29
29
  throw new Error(`Invalid lineup: players[${i}].name "${p.name}" contains invalid characters`);
30
30
  }
31
+ if (p.isolation != null && p.isolation !== 'worktree') {
32
+ throw new Error(`Invalid lineup: players[${i}].isolation must be "worktree" if specified`);
33
+ }
34
+ if (p.branch != null && (typeof p.branch !== 'string' || !p.branch)) {
35
+ throw new Error(`Invalid lineup: players[${i}].branch must be a non-empty string`);
36
+ }
31
37
  }
32
38
  // Validate schedules if present
33
39
  if (doc.schedules != null) {
@@ -50,6 +56,15 @@ function loadLineup(filePath) {
50
56
  }
51
57
  }
52
58
  }
59
+ // Validate conductor name if present
60
+ if (doc.conductor?.name != null) {
61
+ if (typeof doc.conductor.name !== 'string' || !doc.conductor.name) {
62
+ throw new Error(`Invalid lineup: conductor.name must be a non-empty string`);
63
+ }
64
+ if (!/^[a-zA-Z0-9_-]+$/.test(doc.conductor.name)) {
65
+ throw new Error(`Invalid lineup: conductor.name "${doc.conductor.name}" contains invalid characters`);
66
+ }
67
+ }
53
68
  return {
54
69
  name: doc.name,
55
70
  description: doc.description,
@@ -61,6 +76,8 @@ function loadLineup(filePath) {
61
76
  ...(p.agent != null && { agent: p.agent }),
62
77
  ...(p.instructions != null && { instructions: p.instructions }),
63
78
  ...(Array.isArray(p.allowedTools) && { allowedTools: p.allowedTools.map(String) }),
79
+ ...(p.isolation != null && { isolation: p.isolation }),
80
+ ...(p.branch != null && { branch: p.branch }),
64
81
  })),
65
82
  schedules: doc.schedules,
66
83
  };
@@ -36,7 +36,10 @@ async function saveLineup(client, ensemble, filePath, name) {
36
36
  const agentType = meta.agentType || 'claude';
37
37
  const workDir = meta.workDir || undefined;
38
38
  if (isConductor) {
39
+ const conductorName = meta.playerId || undefined;
39
40
  conductor = {
41
+ // Only save name if it's not the default 'conductor'
42
+ ...(conductorName && conductorName !== 'conductor' ? { name: conductorName } : {}),
40
43
  agent: agentType === 'copilot' ? 'copilot' : undefined,
41
44
  };
42
45
  }
@@ -2,6 +2,7 @@ export interface EnsembleLineup {
2
2
  name: string;
3
3
  description?: string;
4
4
  conductor?: {
5
+ name?: string;
5
6
  type?: string;
6
7
  agent?: string;
7
8
  instructions?: string;
@@ -13,6 +14,8 @@ export interface EnsembleLineup {
13
14
  agent?: string;
14
15
  instructions?: string;
15
16
  allowedTools?: string[];
17
+ isolation?: 'worktree';
18
+ branch?: string;
16
19
  /** Transient: resolved agent definition name (set by loadAndResolveLineup). */
17
20
  _agentDefinition?: string;
18
21
  /** Transient: resolved absolute path to .md file (set by loadAndResolveLineup). */
package/dist/server.js CHANGED
@@ -83,10 +83,11 @@ async function main() {
83
83
  const config = (0, config_1.getConfig)();
84
84
  const isConductor = process.env[config_1.ENV.CONDUCTOR] === 'true';
85
85
  const requestedName = process.env[config_1.ENV.PLAYER_NAME] || '';
86
- // Prevent non-conductor sessions from using "conductor" as a name,
86
+ // Conductors use their requested name or fall back to 'conductor'.
87
+ // Non-conductors are prevented from using "conductor" as a name,
87
88
  // which would collide with the conductor's deterministic workflow ID.
88
89
  let playerId = isConductor
89
- ? 'conductor'
90
+ ? (requestedName || 'conductor')
90
91
  : (requestedName && requestedName !== 'conductor' ? requestedName : '') || crypto.randomBytes(4).toString('hex');
91
92
  const getPlayerId = () => playerId;
92
93
  const setPlayerId = (id) => { playerId = id; };
@@ -239,7 +240,7 @@ async function main() {
239
240
  }
240
241
  }
241
242
  // Create MCP server
242
- const hasRequestedName = Boolean(requestedName && requestedName !== 'conductor');
243
+ const hasRequestedName = isConductor || Boolean(requestedName && requestedName !== 'conductor');
243
244
  const playerTypeLine = playerType
244
245
  ? `Your player type is "${playerType}"${playerTypeDescription ? ` (${playerTypeDescription})` : ''}. `
245
246
  : '';
@@ -15,6 +15,7 @@ const duration_1 = require("../utils/duration");
15
15
  const safe_path_1 = require("../utils/safe-path");
16
16
  const helpers_1 = require("./helpers");
17
17
  const validation_1 = require("../utils/validation");
18
+ const worktree_1 = require("../utils/worktree");
18
19
  const log = (...args) => console.error('[claude-tempo:load-lineup]', ...args);
19
20
  function sleep(ms) {
20
21
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -79,7 +80,8 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
79
80
  // Recruit players sequentially
80
81
  for (const player of lineup.players) {
81
82
  const playerName = player.name;
82
- const workDir = player.workDir || process.cwd();
83
+ let workDir = player.workDir || process.cwd();
84
+ let worktreePath;
83
85
  const agentType = player.agent === 'copilot' ? 'copilot' : 'claude';
84
86
  const isCustomAgent = player.agent && player.agent !== 'default' && player.agent !== 'copilot';
85
87
  const systemPrompt = player._agentDefinition ? undefined : (isCustomAgent ? player.agent : undefined);
@@ -104,6 +106,31 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
104
106
  }
105
107
  continue;
106
108
  }
109
+ // Create worktree if isolation is requested
110
+ if (player.isolation === 'worktree') {
111
+ try {
112
+ // Determine git root: use the player's workDir as the git root,
113
+ // or fall back to cwd (which should be a git repo).
114
+ const gitRoot = workDir;
115
+ const result = (0, worktree_1.createWorktree)({
116
+ gitRoot,
117
+ ensemble: config.ensemble,
118
+ playerName,
119
+ branch: player.branch,
120
+ });
121
+ worktreePath = result.path;
122
+ workDir = result.path;
123
+ log(`Worktree for "${playerName}": ${result.path} (branch: ${result.branch}, created: ${result.created})`);
124
+ // Install dependencies — blocking but failure is non-fatal
125
+ if (result.created) {
126
+ (0, worktree_1.installDependencies)(result.path);
127
+ }
128
+ }
129
+ catch (err) {
130
+ failed.push(`${playerName}: worktree creation failed — ${err}`);
131
+ continue;
132
+ }
133
+ }
107
134
  // Record existing workflows to detect the new one
108
135
  const existingIds = new Set();
109
136
  const listQuery = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"`;
@@ -194,6 +221,16 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
194
221
  failed.push(`${playerName}: spawned but did not register within 15s`);
195
222
  continue;
196
223
  }
224
+ // Record worktree path in session metadata
225
+ if (worktreePath && newWorkflowId) {
226
+ try {
227
+ const newHandle = client.workflow.getHandle(newWorkflowId);
228
+ await newHandle.signal('updateMetadata', { worktreePath });
229
+ }
230
+ catch (err) {
231
+ log(`Failed to set worktreePath metadata for "${playerName}":`, err);
232
+ }
233
+ }
197
234
  // Send initial instructions if provided
198
235
  if (player.instructions) {
199
236
  try {
package/dist/types.d.ts CHANGED
@@ -16,6 +16,8 @@ export interface SessionMetadata {
16
16
  playerTypeDescription?: string;
17
17
  /** Player ID of who recruited this player. */
18
18
  recruitedBy?: string;
19
+ /** Worktree path if this session was spawned in an isolated worktree. */
20
+ worktreePath?: string;
19
21
  }
20
22
  export interface AgentTypeInfo {
21
23
  name: string;
@@ -30,6 +30,8 @@ export declare const GATE_CRITERIA_MAX = 20;
30
30
  export declare const GATE_CRITERION_TEXT_MAX = 512;
31
31
  /** Maximum length for gate criterion notes. */
32
32
  export declare const GATE_NOTES_MAX = 1024;
33
+ /** Timeout for npm install in worktrees (60s). */
34
+ export declare const WORKTREE_INSTALL_TIMEOUT = 60000;
33
35
  /** Default number of recent messages to include as context in an encore. */
34
36
  export declare const ENCORE_DEFAULT_CONTEXT_MESSAGES = 10;
35
37
  /** Maximum length for message preview truncation. */
@@ -4,7 +4,7 @@
4
4
  * Used by MCP tool Zod schemas and config validation.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.PREVIEW_MAX_LENGTH = exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = exports.GATE_NOTES_MAX = exports.GATE_CRITERION_TEXT_MAX = exports.GATE_CRITERIA_MAX = exports.GATE_TASK_MAX = exports.CRON_EXPRESSION_MAX = exports.SCHEDULE_MESSAGE_MAX = exports.SCHEDULE_NAME_MAX = exports.PATH_MAX = exports.PART_MAX = exports.MESSAGE_MAX = exports.ENSEMBLE_NAME_REGEX = exports.PLAYER_NAME_MAX = exports.PLAYER_NAME_REGEX = void 0;
7
+ exports.PREVIEW_MAX_LENGTH = exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = exports.WORKTREE_INSTALL_TIMEOUT = exports.GATE_NOTES_MAX = exports.GATE_CRITERION_TEXT_MAX = exports.GATE_CRITERIA_MAX = exports.GATE_TASK_MAX = exports.CRON_EXPRESSION_MAX = exports.SCHEDULE_MESSAGE_MAX = exports.SCHEDULE_NAME_MAX = exports.PATH_MAX = exports.PART_MAX = exports.MESSAGE_MAX = exports.ENSEMBLE_NAME_REGEX = exports.PLAYER_NAME_MAX = exports.PLAYER_NAME_REGEX = void 0;
8
8
  exports.shouldIncludeInBroadcast = shouldIncludeInBroadcast;
9
9
  exports.validatePlayerName = validatePlayerName;
10
10
  exports.validateEnsembleName = validateEnsembleName;
@@ -36,6 +36,8 @@ exports.GATE_CRITERIA_MAX = 20;
36
36
  exports.GATE_CRITERION_TEXT_MAX = 512;
37
37
  /** Maximum length for gate criterion notes. */
38
38
  exports.GATE_NOTES_MAX = 1024;
39
+ /** Timeout for npm install in worktrees (60s). */
40
+ exports.WORKTREE_INSTALL_TIMEOUT = 60000;
39
41
  /** Default number of recent messages to include as context in an encore. */
40
42
  exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = 10;
41
43
  /** Maximum length for message preview truncation. */
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Compute the base directory for all worktrees in an ensemble.
3
+ * Convention: `{gitRoot}/../.ct-worktrees/{ensemble}/`
4
+ */
5
+ export declare function worktreeBasePath(gitRoot: string, ensemble: string): string;
6
+ export interface CreateWorktreeOpts {
7
+ gitRoot: string;
8
+ ensemble: string;
9
+ playerName: string;
10
+ branch?: string;
11
+ }
12
+ export interface CreateWorktreeResult {
13
+ /** Absolute path to the worktree directory. */
14
+ path: string;
15
+ /** Branch name used for the worktree. */
16
+ branch: string;
17
+ /** Whether the worktree was newly created (false if it already existed). */
18
+ created: boolean;
19
+ }
20
+ /**
21
+ * Create a git worktree for a player. If the worktree already exists
22
+ * at the expected path, returns it without re-creating.
23
+ *
24
+ * Branch defaults to `{ensemble}/{playerName}` if not specified.
25
+ */
26
+ export declare function createWorktree(opts: CreateWorktreeOpts): CreateWorktreeResult;
27
+ /**
28
+ * Install dependencies in a worktree directory.
29
+ *
30
+ * Detects the package manager (npm, yarn, pnpm) by lockfile presence.
31
+ * Failure or timeout is logged but does not throw — the recruit proceeds
32
+ * with whatever state the worktree is in.
33
+ */
34
+ export declare function installDependencies(worktreePath: string, timeoutMs?: number): void;
35
+ /**
36
+ * Remove a git worktree.
37
+ */
38
+ export declare function removeWorktree(worktreePath: string): void;
@@ -0,0 +1,168 @@
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.worktreeBasePath = worktreeBasePath;
37
+ exports.createWorktree = createWorktree;
38
+ exports.installDependencies = installDependencies;
39
+ exports.removeWorktree = removeWorktree;
40
+ /**
41
+ * Git worktree helpers for player isolation.
42
+ *
43
+ * Creates and manages git worktrees so each player can work on an
44
+ * isolated copy of the repository without conflicting with others.
45
+ */
46
+ const child_process_1 = require("child_process");
47
+ const fs_1 = require("fs");
48
+ const path = __importStar(require("path"));
49
+ const validation_1 = require("./validation");
50
+ const log = (...args) => console.error('[claude-tempo:worktree]', ...args);
51
+ /**
52
+ * Compute the base directory for all worktrees in an ensemble.
53
+ * Convention: `{gitRoot}/../.ct-worktrees/{ensemble}/`
54
+ */
55
+ function worktreeBasePath(gitRoot, ensemble) {
56
+ return path.join(path.dirname(gitRoot), '.ct-worktrees', ensemble);
57
+ }
58
+ /**
59
+ * Create a git worktree for a player. If the worktree already exists
60
+ * at the expected path, returns it without re-creating.
61
+ *
62
+ * Branch defaults to `{ensemble}/{playerName}` if not specified.
63
+ */
64
+ function createWorktree(opts) {
65
+ const { gitRoot, ensemble, playerName } = opts;
66
+ const branch = opts.branch || `${ensemble}/${playerName}`;
67
+ const basePath = worktreeBasePath(gitRoot, ensemble);
68
+ const wtPath = path.join(basePath, playerName);
69
+ // If worktree already exists, reuse it
70
+ if ((0, fs_1.existsSync)(path.join(wtPath, '.git'))) {
71
+ log(`Worktree already exists at "${wtPath}" — reusing`);
72
+ return { path: wtPath, branch, created: false };
73
+ }
74
+ // Ensure base directory exists
75
+ (0, fs_1.mkdirSync)(basePath, { recursive: true });
76
+ // Check if the branch already has a worktree (would cause git error)
77
+ try {
78
+ const existing = (0, child_process_1.execFileSync)('git', ['worktree', 'list', '--porcelain'], {
79
+ cwd: gitRoot,
80
+ encoding: 'utf8',
81
+ stdio: ['pipe', 'pipe', 'pipe'],
82
+ });
83
+ // Parse porcelain output: "branch refs/heads/{branch}" lines
84
+ const branchRef = `refs/heads/${branch}`;
85
+ if (existing.includes(`branch ${branchRef}`)) {
86
+ throw new Error(`Branch "${branch}" already has an active worktree. ` +
87
+ `Remove it first with \`git worktree remove\` or choose a different branch.`);
88
+ }
89
+ }
90
+ catch (err) {
91
+ // Re-throw our own error, swallow git failures (e.g., no worktrees yet)
92
+ if (err.message?.includes('already has an active worktree'))
93
+ throw err;
94
+ }
95
+ // Create the worktree. Use -B to create/reset the branch.
96
+ try {
97
+ log(`Creating worktree: git worktree add -B ${branch} ${wtPath}`);
98
+ (0, child_process_1.execFileSync)('git', ['worktree', 'add', '-B', branch, wtPath], {
99
+ cwd: gitRoot,
100
+ encoding: 'utf8',
101
+ stdio: ['pipe', 'pipe', 'pipe'],
102
+ });
103
+ }
104
+ catch (err) {
105
+ const msg = err.stderr || err.stdout || err.message || String(err);
106
+ throw new Error(`Failed to create worktree at "${wtPath}": ${msg.trim()}`);
107
+ }
108
+ return { path: wtPath, branch, created: true };
109
+ }
110
+ /**
111
+ * Install dependencies in a worktree directory.
112
+ *
113
+ * Detects the package manager (npm, yarn, pnpm) by lockfile presence.
114
+ * Failure or timeout is logged but does not throw — the recruit proceeds
115
+ * with whatever state the worktree is in.
116
+ */
117
+ function installDependencies(worktreePath, timeoutMs = validation_1.WORKTREE_INSTALL_TIMEOUT) {
118
+ // Detect package manager by lockfile
119
+ let cmd;
120
+ let args;
121
+ if ((0, fs_1.existsSync)(path.join(worktreePath, 'pnpm-lock.yaml'))) {
122
+ cmd = 'pnpm';
123
+ args = ['install', '--frozen-lockfile'];
124
+ }
125
+ else if ((0, fs_1.existsSync)(path.join(worktreePath, 'yarn.lock'))) {
126
+ cmd = 'yarn';
127
+ args = ['install', '--frozen-lockfile'];
128
+ }
129
+ else if ((0, fs_1.existsSync)(path.join(worktreePath, 'package-lock.json')) || (0, fs_1.existsSync)(path.join(worktreePath, 'package.json'))) {
130
+ cmd = 'npm';
131
+ args = ['install'];
132
+ }
133
+ else {
134
+ log(`No package.json found in "${worktreePath}" — skipping install`);
135
+ return;
136
+ }
137
+ try {
138
+ log(`Installing dependencies in "${worktreePath}": ${cmd} ${args.join(' ')}`);
139
+ (0, child_process_1.execFileSync)(cmd, args, {
140
+ cwd: worktreePath,
141
+ encoding: 'utf8',
142
+ timeout: timeoutMs,
143
+ stdio: ['pipe', 'pipe', 'pipe'],
144
+ });
145
+ log(`Dependencies installed successfully in "${worktreePath}"`);
146
+ }
147
+ catch (err) {
148
+ // Log warning but don't throw — recruit should still proceed
149
+ const msg = err.killed ? `Timed out after ${timeoutMs}ms` : (err.stderr || err.message || String(err));
150
+ log(`Warning: dependency install failed in "${worktreePath}": ${msg}`);
151
+ }
152
+ }
153
+ /**
154
+ * Remove a git worktree.
155
+ */
156
+ function removeWorktree(worktreePath) {
157
+ try {
158
+ (0, child_process_1.execFileSync)('git', ['worktree', 'remove', '--force', worktreePath], {
159
+ encoding: 'utf8',
160
+ stdio: ['pipe', 'pipe', 'pipe'],
161
+ });
162
+ log(`Removed worktree at "${worktreePath}"`);
163
+ }
164
+ catch (err) {
165
+ const msg = err.stderr || err.message || String(err);
166
+ log(`Warning: failed to remove worktree at "${worktreePath}": ${msg.trim()}`);
167
+ }
168
+ }
@@ -114,6 +114,8 @@ async function claudeSessionWorkflow(input) {
114
114
  input.metadata.playerType = update.playerType;
115
115
  if (update.playerTypeDescription != null)
116
116
  input.metadata.playerTypeDescription = update.playerTypeDescription;
117
+ if (update.worktreePath != null)
118
+ input.metadata.worktreePath = update.worktreePath;
117
119
  if (update.status != null) {
118
120
  input.metadata.status = update.status;
119
121
  // Re-enable stale detection only when explicitly requested (server.ts sets this)
@@ -21,6 +21,7 @@ export declare const updateMetadataSignal: import("@temporalio/workflow").Signal
21
21
  enableStaleDetection?: boolean;
22
22
  playerType?: string;
23
23
  playerTypeDescription?: string;
24
+ worktreePath?: string;
24
25
  }], string>;
25
26
  export declare const getPartQuery: import("@temporalio/workflow").QueryDefinition<string, [], string>;
26
27
  export declare const getMetadataQuery: import("@temporalio/workflow").QueryDefinition<SessionMetadata, [], string>;
@@ -51,6 +51,41 @@ You are a combination of Product Manager, Task Decomposition Expert, and Context
51
51
  - **Escalation**: If a player reports a blocker you can't resolve, report it upward or recruit a specialist.
52
52
  - **Wrap-up**: Collect final reports, synthesize results, stop idle players, report completion.
53
53
 
54
+ ## Worktree Coordination
55
+
56
+ Use git worktrees when two or more engineers need to work in the same repo on different branches simultaneously. Each worktree is an independent checkout — players can build, test, and commit without interfering with each other.
57
+
58
+ ### When to use
59
+
60
+ - Two players working on different feature branches in the same repo
61
+ - Running a long build/test in one branch while another player continues development
62
+ - Isolating risky changes from the main working tree
63
+
64
+ ### How to coordinate
65
+
66
+ 1. **Create the worktree** (you or a player with shell access):
67
+ ```
68
+ git worktree add ../ct-{task} {branch}
69
+ ```
70
+ 2. **Install dependencies** in the new worktree:
71
+ ```
72
+ cd ../ct-{task} && npm install
73
+ ```
74
+ 3. **Recruit with `workDir`** pointing to the worktree:
75
+ ```
76
+ recruit({ name: "eng-33", workDir: "../ct-{task}", ... })
77
+ ```
78
+ 4. **Clean up** after the task: stop the player first, then remove the worktree:
79
+ ```
80
+ git worktree remove ../ct-{task}
81
+ ```
82
+
83
+ `recruit` already accepts `workDir` — no new tools are needed for manual worktree coordination.
84
+
85
+ ### Platform notes
86
+
87
+ - **Windows**: Use short sibling paths (e.g. `../ct-feat33`) to avoid MAX_PATH limits. Always stop players before removing worktrees — NTFS file locks will block cleanup while a session is active.
88
+
54
89
  ## Handling Context Pressure
55
90
 
56
91
  When a player reports context pressure (growing context, lost instructions, repeated work), act immediately:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-tempo",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "MCP server for multi-session Claude Code coordination via Temporal",
5
5
  "keywords": [
6
6
  "mcp",