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 +6 -1
- package/README.md +3 -0
- package/dist/activities/outbox.d.ts +9 -1
- package/dist/activities/outbox.js +13 -5
- package/dist/ensemble/loader.js +0 -8
- package/dist/ensemble/schema.d.ts +0 -2
- package/dist/server.js +2 -0
- package/dist/tools/load-lineup.js +1 -38
- package/dist/tools/worktree.d.ts +4 -0
- package/dist/tools/worktree.js +184 -0
- package/dist/types.d.ts +19 -1
- package/dist/utils/validation.d.ts +3 -1
- package/dist/utils/validation.js +5 -3
- package/dist/workflows/session.js +47 -2
- package/dist/workflows/signals.d.ts +6 -2
- package/dist/workflows/signals.js +5 -1
- package/examples/agents/tempo-conductor.md +8 -20
- package/package.json +1 -1
- package/workflow-bundle.js +53 -4
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
|
-
│
|
|
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<
|
|
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
|
-
|
|
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
|
};
|
package/dist/ensemble/loader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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. */
|
package/dist/utils/validation.js
CHANGED
|
@@ -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
|
|
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;
|