claude-tempo 0.14.0 → 0.16.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/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. |
@@ -477,6 +478,8 @@ claude-tempo <command> [options]
477
478
  | `stop [ensemble]` | Stop sessions (`-n <name>` for one, `--all` for everything) |
478
479
  | `init` | Register claude-tempo MCP server globally (`--project` for per-directory) |
479
480
  | `preflight` | Run environment checks |
481
+ | `broadcast <msg>` | Send a message to all active players. Use `--type` to filter by player type, `--include-stale` to include stale sessions. |
482
+ | `encore <name>` | Revive a stale player session by name. Use `--host` to target a remote machine. |
480
483
  | `ensemble <sub>` | Manage saved lineups (`save`, `list`, `show`) |
481
484
  | `agent-types <sub>` | Manage player types (`list`, `show <name>`, `init`) |
482
485
  | `version` | Print the installed version |
@@ -46,6 +46,8 @@ export interface SpawnProcessInput {
46
46
  nativeResolvable?: boolean;
47
47
  /** When true, use --resume instead of -n (reconnect to existing session). */
48
48
  resume?: boolean;
49
+ /** Claude Code session UUID for --session-id (new sessions) or --resume (encore). */
50
+ claudeSessionId?: string;
49
51
  /** Tool restrictions from the agent definition frontmatter. */
50
52
  allowedTools?: string[];
51
53
  }
@@ -64,6 +66,8 @@ export interface EncoreResult {
64
66
  agentDefinitionPath?: string;
65
67
  nativeResolvable?: boolean;
66
68
  allowedTools?: string[];
69
+ /** Claude Code session UUID for deterministic --resume. */
70
+ claudeSessionId?: string;
67
71
  temporalAddress: string;
68
72
  temporalNamespace: string;
69
73
  }
@@ -71,11 +75,15 @@ export interface OutboxActivityResult {
71
75
  success: boolean;
72
76
  error?: string;
73
77
  }
78
+ export interface RecruitResult extends OutboxActivityResult {
79
+ /** Claude Code session UUID assigned at recruit time. */
80
+ claudeSessionId?: string;
81
+ }
74
82
  export interface OutboxActivities {
75
83
  deliverCue(input: DeliverCueInput): Promise<OutboxActivityResult>;
76
84
  deliverReport(input: DeliverReportInput): Promise<OutboxActivityResult>;
77
85
  terminateSession(input: TerminateSessionInput): Promise<OutboxActivityResult>;
78
- startRecruitedSession(input: StartRecruitedSessionInput): Promise<OutboxActivityResult>;
86
+ startRecruitedSession(input: StartRecruitedSessionInput): Promise<RecruitResult>;
79
87
  spawnProcess(input: SpawnProcessInput): Promise<OutboxActivityResult>;
80
88
  performEncore(input: PerformEncoreInput): Promise<EncoreResult>;
81
89
  }
@@ -98,6 +98,8 @@ function createOutboxActivities(client, config) {
98
98
  ? (0, config_1.conductorWorkflowId)(ensemble)
99
99
  : (0, config_1.sessionWorkflowId)(ensemble, targetName);
100
100
  const { gitRoot, gitBranch } = (0, git_info_1.getGitInfo)(workDir);
101
+ // Generate a UUID for the Claude Code session — used for deterministic --resume on encore
102
+ const claudeSessionId = crypto.randomUUID();
101
103
  const sessionInput = {
102
104
  metadata: {
103
105
  playerId: targetName,
@@ -109,6 +111,7 @@ function createOutboxActivities(client, config) {
109
111
  isConductor,
110
112
  agentType: agent,
111
113
  status: 'pending',
114
+ claudeSessionId,
112
115
  ...(agentDefinition ? { playerType: agentDefinition } : {}),
113
116
  ...(agentDefinitionDescription ? { playerTypeDescription: agentDefinitionDescription } : {}),
114
117
  recruitedBy: fromPlayerId,
@@ -137,15 +140,15 @@ function createOutboxActivities(client, config) {
137
140
  ClaudeTempoPlayerId: [targetName],
138
141
  },
139
142
  });
140
- log(`Pre-created workflow ${workflowId} for recruit "${targetName}"`);
141
- return { success: true };
143
+ log(`Pre-created workflow ${workflowId} for recruit "${targetName}" (sessionId=${claudeSessionId})`);
144
+ return { success: true, claudeSessionId };
142
145
  }
143
146
  catch (err) {
144
147
  throw activity_1.ApplicationFailure.nonRetryable(`Failed to start recruited session "${targetName}": ${err instanceof Error ? err.message : String(err)}`);
145
148
  }
146
149
  },
147
150
  async spawnProcess(input) {
148
- const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume, allowedTools } = input;
151
+ const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume, claudeSessionId, allowedTools } = input;
149
152
  // Read secrets from the worker's config closure — never from workflow state
150
153
  const { temporalApiKey, temporalTlsCertPath, temporalTlsKeyPath } = config;
151
154
  try {
@@ -178,8 +181,12 @@ function createOutboxActivities(client, config) {
178
181
  else if (systemPrompt) {
179
182
  agentFlags = ['--system-prompt', systemPrompt];
180
183
  }
181
- // Use --resume for encore (reconnect to existing session) or -n for new sessions
182
- const nameArgs = resume ? ['--resume', targetName] : ['-n', targetName];
184
+ // Use --resume for encore (reconnect to existing session) or -n for new sessions.
185
+ // For encore: use UUID for deterministic --resume (no interactive picker).
186
+ // For new sessions: use --session-id to track the UUID for future encores.
187
+ const nameArgs = resume
188
+ ? ['--resume', claudeSessionId || targetName]
189
+ : ['-n', targetName, ...(claudeSessionId ? ['--session-id', claudeSessionId] : [])];
183
190
  // Build --allowedTools flag from agent definition frontmatter
184
191
  const allowedToolsFlags = allowedTools && allowedTools.length > 0
185
192
  ? ['--allowedTools', ...allowedTools]
@@ -280,6 +287,7 @@ function createOutboxActivities(client, config) {
280
287
  agentDefinitionPath,
281
288
  nativeResolvable,
282
289
  allowedTools,
290
+ claudeSessionId: metadata.claudeSessionId || undefined,
283
291
  temporalAddress: config.temporalAddress,
284
292
  temporalNamespace: config.temporalNamespace,
285
293
  };
@@ -28,12 +28,6 @@ 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
- }
37
31
  }
38
32
  // Validate schedules if present
39
33
  if (doc.schedules != null) {
@@ -76,8 +70,6 @@ function loadLineup(filePath) {
76
70
  ...(p.agent != null && { agent: p.agent }),
77
71
  ...(p.instructions != null && { instructions: p.instructions }),
78
72
  ...(Array.isArray(p.allowedTools) && { allowedTools: p.allowedTools.map(String) }),
79
- ...(p.isolation != null && { isolation: p.isolation }),
80
- ...(p.branch != null && { branch: p.branch }),
81
73
  })),
82
74
  schedules: doc.schedules,
83
75
  };
@@ -14,8 +14,6 @@ export interface EnsembleLineup {
14
14
  agent?: string;
15
15
  instructions?: string;
16
16
  allowedTools?: string[];
17
- isolation?: 'worktree';
18
- branch?: string;
19
17
  /** Transient: resolved agent definition name (set by loadAndResolveLineup). */
20
18
  _agentDefinition?: string;
21
19
  /** Transient: resolved absolute path to .md file (set by loadAndResolveLineup). */
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.
@@ -15,7 +15,6 @@ 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");
19
18
  const log = (...args) => console.error('[claude-tempo:load-lineup]', ...args);
20
19
  function sleep(ms) {
21
20
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -80,8 +79,7 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
80
79
  // Recruit players sequentially
81
80
  for (const player of lineup.players) {
82
81
  const playerName = player.name;
83
- let workDir = player.workDir || process.cwd();
84
- let worktreePath;
82
+ const workDir = player.workDir || process.cwd();
85
83
  const agentType = player.agent === 'copilot' ? 'copilot' : 'claude';
86
84
  const isCustomAgent = player.agent && player.agent !== 'default' && player.agent !== 'copilot';
87
85
  const systemPrompt = player._agentDefinition ? undefined : (isCustomAgent ? player.agent : undefined);
@@ -106,31 +104,6 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
106
104
  }
107
105
  continue;
108
106
  }
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
- }
134
107
  // Record existing workflows to detect the new one
135
108
  const existingIds = new Set();
136
109
  const listQuery = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"`;
@@ -221,16 +194,6 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
221
194
  failed.push(`${playerName}: spawned but did not register within 15s`);
222
195
  continue;
223
196
  }
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
- }
234
197
  // Send initial instructions if provided
235
198
  if (player.instructions) {
236
199
  try {
@@ -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
@@ -1,5 +1,5 @@
1
1
  export type AgentType = 'claude' | 'copilot';
2
- export type SessionStatus = 'active' | 'stale' | 'pending' | 'terminated';
2
+ export type SessionStatus = 'active' | 'stale' | 'pending' | 'terminated' | 'blocked';
3
3
  export interface SessionMetadata {
4
4
  playerId: string;
5
5
  ensemble: string;
@@ -18,6 +18,8 @@ export interface SessionMetadata {
18
18
  recruitedBy?: string;
19
19
  /** Worktree path if this session was spawned in an isolated worktree. */
20
20
  worktreePath?: string;
21
+ /** Claude Code session UUID — used for deterministic --resume on encore. */
22
+ claudeSessionId?: string;
21
23
  }
22
24
  export interface AgentTypeInfo {
23
25
  name: string;
@@ -46,6 +48,8 @@ export interface SessionInput {
46
48
  disableStaleDetection?: boolean;
47
49
  /** Restored from continue-as-new (conductor only) */
48
50
  qualityGates?: QualityGate[];
51
+ /** Restored from continue-as-new (conductor only) */
52
+ worktrees?: WorktreeEntry[];
49
53
  /** Temporal config passed through for outbox activities (non-secret fields only). */
50
54
  temporalConfig?: {
51
55
  temporalAddress: string;
@@ -154,6 +158,20 @@ export interface QualityGate {
154
158
  /** Derived: all passed → passed, any failed → failed, else open. */
155
159
  status: 'open' | 'passed' | 'failed';
156
160
  }
161
+ export interface WorktreeEntry {
162
+ /** Player name assigned to this worktree. */
163
+ player: string;
164
+ /** Absolute path to worktree directory. */
165
+ path: string;
166
+ /** Git branch for this worktree. */
167
+ branch: string;
168
+ /** Original git root (for git worktree remove). */
169
+ gitRoot: string;
170
+ /** ISO timestamp of creation. */
171
+ createdAt: string;
172
+ /** Player ID of creator. */
173
+ createdBy: string;
174
+ }
157
175
  export interface ScheduleEntry {
158
176
  /** Unique name for this schedule (used as key for add/replace/remove). */
159
177
  name: string;
@@ -32,13 +32,15 @@ export declare const GATE_CRITERION_TEXT_MAX = 512;
32
32
  export declare const GATE_NOTES_MAX = 1024;
33
33
  /** Timeout for npm install in worktrees (60s). */
34
34
  export declare const WORKTREE_INSTALL_TIMEOUT = 60000;
35
+ /** Window for blocked session detection (5 minutes). */
36
+ export declare const BLOCKED_WINDOW_MS: number;
35
37
  /** Default number of recent messages to include as context in an encore. */
36
38
  export declare const ENCORE_DEFAULT_CONTEXT_MESSAGES = 10;
37
39
  /** Maximum length for message preview truncation. */
38
40
  export declare const PREVIEW_MAX_LENGTH = 200;
39
41
  /**
40
42
  * Whether a session should be included in a broadcast based on its status.
41
- * Always excludes pending and terminated. Excludes stale unless includeStale is true.
43
+ * Always excludes pending, terminated, and blocked. Excludes stale unless includeStale is true.
42
44
  */
43
45
  export declare function shouldIncludeInBroadcast(status: string | undefined, includeStale: boolean): boolean;
44
46
  /** Validate a player name string. Returns an error message or null if valid. */
@@ -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.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;
7
+ exports.PREVIEW_MAX_LENGTH = exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = exports.BLOCKED_WINDOW_MS = 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;
@@ -38,17 +38,19 @@ exports.GATE_CRITERION_TEXT_MAX = 512;
38
38
  exports.GATE_NOTES_MAX = 1024;
39
39
  /** Timeout for npm install in worktrees (60s). */
40
40
  exports.WORKTREE_INSTALL_TIMEOUT = 60000;
41
+ /** Window for blocked session detection (5 minutes). */
42
+ exports.BLOCKED_WINDOW_MS = 5 * 60 * 1000;
41
43
  /** Default number of recent messages to include as context in an encore. */
42
44
  exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = 10;
43
45
  /** Maximum length for message preview truncation. */
44
46
  exports.PREVIEW_MAX_LENGTH = 200;
45
47
  /**
46
48
  * Whether a session should be included in a broadcast based on its status.
47
- * Always excludes pending and terminated. Excludes stale unless includeStale is true.
49
+ * Always excludes pending, terminated, and blocked. Excludes stale unless includeStale is true.
48
50
  */
49
51
  function shouldIncludeInBroadcast(status, includeStale) {
50
52
  const s = status || 'active';
51
- if (s === 'pending' || s === 'terminated')
53
+ if (s === 'pending' || s === 'terminated' || s === 'blocked')
52
54
  return false;
53
55
  if (s === 'stale' && !includeStale)
54
56
  return false;