claude-tempo 0.13.1 → 0.15.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
@@ -65,9 +65,13 @@ src/
65
65
  │ ├── quality-gate.ts # Define quality gates for tasks (conductor only)
66
66
  │ ├── evaluate-gate.ts # Mark gate criteria as passed/failed (conductor only)
67
67
  │ ├── gates.ts # List quality gates and their status (conductor only)
68
+ │ ├── worktree.ts # Manage git worktrees for player isolation (conductor only)
68
69
  │ └── helpers.ts # Zod/MCP tool registration wrapper
69
70
  ├── utils/
70
- └── validation.ts # Shared validation constants (name/message/path limits, encore defaults) and helpers
71
+ ├── validation.ts # Shared validation constants (name/message/path limits, encore defaults) and helpers
72
+ │ ├── worktree.ts # Git worktree create/remove helpers (cross-platform)
73
+ │ ├── safe-path.ts # Path safety utilities
74
+ │ └── duration.ts # Duration parsing helpers
71
75
  ├── types.ts # Shared type definitions
72
76
  ├── channel.ts # Claude channel notification helper
73
77
  ├── git-info.ts # Git repository detection helper
@@ -120,6 +124,7 @@ npm test
120
124
  - **Schedule**: A one-shot or recurring message delivery configured via the `schedule` tool. Backed by a durable `claudeSchedulerWorkflow` — survives restarts. Supports delay (`delay`), fixed time (`at`), recurring interval (`every`), and cron expressions (`cron`) with optional IANA timezone (`timezone`). Cron schedules use `croner` for expression parsing and next-fire computation. Managed via `schedule`, `unschedule`, and `schedules` tools.
121
125
  - **Lineup**: A YAML file defining an ensemble configuration — which players to recruit, their types, working directories, and optional startup messages. Load via `load_lineup` to bootstrap a full ensemble in one step; save via `save_lineup` to snapshot a running ensemble's state for later reuse.
122
126
  - **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`.
127
+ - **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).
123
128
  - **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.
124
129
 
125
130
  ## Dashboard
package/README.md CHANGED
@@ -133,6 +133,7 @@ These tools are available inside Claude Code sessions connected to claude-tempo:
133
133
  | `broadcast` | Send a message to all active players. Optional `type` filter limits to a specific player type. |
134
134
  | `encore` | Revive a stale player session — restarts the process and reconnects to the existing workflow with context restored. |
135
135
  | `recall` | Read your own message history. Shows received messages by default; pass `includeSent: true` for the full timeline. |
136
+ | `worktree` | Manage git worktrees for player isolation. Actions: `create`, `remove`, `list`. Conductor only. |
136
137
  | `quality_gate` | Define or replace a quality gate for a task — a named checklist of criteria that must pass. Conductor only. |
137
138
  | `evaluate_gate` | Mark one or more criteria on a quality gate as passed or failed. Conductor only. |
138
139
  | `gates` | List quality gates and their status. Filter by task name or status (`open`, `passed`, `failed`). Conductor only. |
@@ -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
  }
@@ -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,
package/dist/server.js CHANGED
@@ -67,6 +67,7 @@ const encore_1 = require("./tools/encore");
67
67
  const quality_gate_1 = require("./tools/quality-gate");
68
68
  const evaluate_gate_1 = require("./tools/evaluate-gate");
69
69
  const gates_1 = require("./tools/gates");
70
+ const worktree_1 = require("./tools/worktree");
70
71
  const channel_1 = require("./channel");
71
72
  const agent_types_2 = require("./ensemble/agent-types");
72
73
  const log = (...args) => console.error('[claude-tempo]', ...args);
@@ -288,6 +289,7 @@ async function main() {
288
289
  (0, quality_gate_1.registerQualityGateTool)(mcpServer, handle, getPlayerId);
289
290
  (0, evaluate_gate_1.registerEvaluateGateTool)(mcpServer, handle, getPlayerId);
290
291
  (0, gates_1.registerGatesTool)(mcpServer, handle);
292
+ (0, worktree_1.registerWorktreeTool)(mcpServer, client, config, handle, getPlayerId);
291
293
  }
292
294
  const MAESTRO_ACK = '\n\n[IMPORTANT: This message is from a human (Maestro). Immediately cue the sender back with a brief acknowledgment and your planned next step before doing the work.]';
293
295
  // Start message poller — push messages into Claude Code via channel notifications.
@@ -0,0 +1,4 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { Client, WorkflowHandle } from '@temporalio/client';
3
+ import { Config } from '../config';
4
+ export declare function registerWorktreeTool(server: McpServer, client: Client, config: Config, handle: WorkflowHandle, getPlayerId: () => string): void;
@@ -0,0 +1,184 @@
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.registerWorktreeTool = registerWorktreeTool;
37
+ const zod_1 = require("zod");
38
+ const resolve_1 = require("./resolve");
39
+ const signals_1 = require("../workflows/signals");
40
+ const helpers_1 = require("./helpers");
41
+ const worktree_1 = require("../utils/worktree");
42
+ const validation_1 = require("../utils/validation");
43
+ function registerWorktreeTool(server, client, config, handle, getPlayerId) {
44
+ (0, helpers_1.defineTool)(server, 'worktree', 'Manage git worktrees for player isolation. Conductor only. Actions: create (provision worktree for a player), remove (clean up), list (show active worktrees).', {
45
+ action: zod_1.z.enum(['create', 'remove', 'list']).describe('Action to perform'),
46
+ player: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).optional().describe('Player name (required for create/remove)'),
47
+ branch: zod_1.z.string().optional().describe('Git branch for the worktree (defaults to {ensemble}/{player-name})'),
48
+ }, async (args) => {
49
+ const { action, player, branch } = args;
50
+ try {
51
+ switch (action) {
52
+ case 'create': {
53
+ if (!player) {
54
+ return {
55
+ content: [{ type: 'text', text: '`player` is required for create action.' }],
56
+ isError: true,
57
+ };
58
+ }
59
+ // Verify player exists
60
+ const targetHandle = await (0, resolve_1.resolveSession)(client, config.ensemble, player);
61
+ if (!targetHandle) {
62
+ return {
63
+ content: [{ type: 'text', text: `No active session found for "${player}".` }],
64
+ isError: true,
65
+ };
66
+ }
67
+ // Check target is on same host (cross-machine worktrees not supported)
68
+ const targetMeta = await targetHandle.query('getMetadata');
69
+ const { hostname } = await Promise.resolve().then(() => __importStar(require('os'))).then((os) => ({ hostname: os.hostname() }));
70
+ if (targetMeta.hostname && targetMeta.hostname !== hostname) {
71
+ return {
72
+ content: [{ type: 'text', text: `Cannot create worktree for "${player}" — they are on host "${targetMeta.hostname}" but worktrees must be created locally.` }],
73
+ isError: true,
74
+ };
75
+ }
76
+ const gitRoot = process.cwd();
77
+ const result = (0, worktree_1.createWorktree)({
78
+ gitRoot,
79
+ ensemble: config.ensemble,
80
+ playerName: player,
81
+ branch,
82
+ });
83
+ if (result.created) {
84
+ (0, worktree_1.installDependencies)(result.path);
85
+ }
86
+ // Record in conductor's worktree state
87
+ const entry = {
88
+ player,
89
+ path: result.path,
90
+ branch: result.branch,
91
+ gitRoot,
92
+ createdAt: new Date().toISOString(),
93
+ createdBy: getPlayerId(),
94
+ };
95
+ await handle.signal('setWorktree', entry);
96
+ // Auto-cue the player with worktree info
97
+ const cueMessage = [
98
+ `\u{1f33f} **Worktree ready** for your task:`,
99
+ `- **Path**: \`${result.path}\``,
100
+ `- **Branch**: \`${result.branch}\``,
101
+ '',
102
+ `Run \`cd ${result.path}\` to switch to your isolated workspace.`,
103
+ `All your changes will be on branch \`${result.branch}\`.`,
104
+ `When done, commit and push \u2014 the conductor will handle cleanup.`,
105
+ ].join('\n');
106
+ await handle.executeUpdate(signals_1.submitOutboxUpdate, {
107
+ args: [{
108
+ type: 'cue',
109
+ targetPlayerId: player,
110
+ message: cueMessage,
111
+ }],
112
+ });
113
+ return {
114
+ content: [{
115
+ type: 'text',
116
+ text: `Worktree created for **${player}**:\n- Path: \`${result.path}\`\n- Branch: \`${result.branch}\`\n- Created: ${result.created ? 'new' : 'reused existing'}\n\nPlayer has been notified.`,
117
+ }],
118
+ };
119
+ }
120
+ case 'remove': {
121
+ if (!player) {
122
+ return {
123
+ content: [{ type: 'text', text: '`player` is required for remove action.' }],
124
+ isError: true,
125
+ };
126
+ }
127
+ // Look up worktree entry from conductor state
128
+ const entries = await handle.query('worktrees');
129
+ const entry = entries.find((w) => w.player === player);
130
+ if (!entry) {
131
+ return {
132
+ content: [{ type: 'text', text: `No worktree found for player "${player}".` }],
133
+ isError: true,
134
+ };
135
+ }
136
+ // Remove from disk
137
+ (0, worktree_1.removeWorktree)(entry.path);
138
+ // Remove from conductor state
139
+ await handle.signal('removeWorktree', player);
140
+ // Auto-cue the player
141
+ try {
142
+ await handle.executeUpdate(signals_1.submitOutboxUpdate, {
143
+ args: [{
144
+ type: 'cue',
145
+ targetPlayerId: player,
146
+ message: `Worktree removed. You're back in the shared repository.`,
147
+ }],
148
+ });
149
+ }
150
+ catch {
151
+ // Player may no longer be active — non-fatal
152
+ }
153
+ return {
154
+ content: [{
155
+ type: 'text',
156
+ text: `Worktree for **${player}** removed (branch: \`${entry.branch}\`).`,
157
+ }],
158
+ };
159
+ }
160
+ case 'list': {
161
+ const entries = await handle.query('worktrees');
162
+ if (entries.length === 0) {
163
+ return {
164
+ content: [{ type: 'text', text: 'No active worktrees.' }],
165
+ };
166
+ }
167
+ const lines = entries.map((w) => `- **${w.player}**: \`${w.path}\` (branch: \`${w.branch}\`, created: ${w.createdAt} by ${w.createdBy})`);
168
+ return {
169
+ content: [{
170
+ type: 'text',
171
+ text: `${entries.length} active worktree${entries.length === 1 ? '' : 's'}:\n${lines.join('\n')}`,
172
+ }],
173
+ };
174
+ }
175
+ }
176
+ }
177
+ catch (err) {
178
+ return {
179
+ content: [{ type: 'text', text: `Worktree operation failed: ${err}` }],
180
+ isError: true,
181
+ };
182
+ }
183
+ });
184
+ }
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;
@@ -44,6 +46,8 @@ export interface SessionInput {
44
46
  disableStaleDetection?: boolean;
45
47
  /** Restored from continue-as-new (conductor only) */
46
48
  qualityGates?: QualityGate[];
49
+ /** Restored from continue-as-new (conductor only) */
50
+ worktrees?: WorktreeEntry[];
47
51
  /** Temporal config passed through for outbox activities (non-secret fields only). */
48
52
  temporalConfig?: {
49
53
  temporalAddress: string;
@@ -152,6 +156,20 @@ export interface QualityGate {
152
156
  /** Derived: all passed → passed, any failed → failed, else open. */
153
157
  status: 'open' | 'passed' | 'failed';
154
158
  }
159
+ export interface WorktreeEntry {
160
+ /** Player name assigned to this worktree. */
161
+ player: string;
162
+ /** Absolute path to worktree directory. */
163
+ path: string;
164
+ /** Git branch for this worktree. */
165
+ branch: string;
166
+ /** Original git root (for git worktree remove). */
167
+ gitRoot: string;
168
+ /** ISO timestamp of creation. */
169
+ createdAt: string;
170
+ /** Player ID of creator. */
171
+ createdBy: string;
172
+ }
155
173
  export interface ScheduleEntry {
156
174
  /** Unique name for this schedule (used as key for add/replace/remove). */
157
175
  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
+ }