claude-tempo 0.14.0 → 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 +6 -1
- package/README.md +1 -0
- 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 +16 -0
- package/dist/workflows/session.js +20 -1
- package/dist/workflows/signals.d.ts +5 -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 +26 -3
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. |
|
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
|
@@ -46,6 +46,8 @@ export interface SessionInput {
|
|
|
46
46
|
disableStaleDetection?: boolean;
|
|
47
47
|
/** Restored from continue-as-new (conductor only) */
|
|
48
48
|
qualityGates?: QualityGate[];
|
|
49
|
+
/** Restored from continue-as-new (conductor only) */
|
|
50
|
+
worktrees?: WorktreeEntry[];
|
|
49
51
|
/** Temporal config passed through for outbox activities (non-secret fields only). */
|
|
50
52
|
temporalConfig?: {
|
|
51
53
|
temporalAddress: string;
|
|
@@ -154,6 +156,20 @@ export interface QualityGate {
|
|
|
154
156
|
/** Derived: all passed → passed, any failed → failed, else open. */
|
|
155
157
|
status: 'open' | 'passed' | 'failed';
|
|
156
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
|
+
}
|
|
157
173
|
export interface ScheduleEntry {
|
|
158
174
|
/** Unique name for this schedule (used as key for add/replace/remove). */
|
|
159
175
|
name: string;
|
|
@@ -25,6 +25,7 @@ async function claudeSessionWorkflow(input) {
|
|
|
25
25
|
(0, workflow_1.patched)('v0.10-initial');
|
|
26
26
|
(0, workflow_1.patched)('v0.11-check-and-set-status');
|
|
27
27
|
(0, workflow_1.patched)('v0.13-quality-gates');
|
|
28
|
+
(0, workflow_1.patched)('v0.14-worktrees');
|
|
28
29
|
// Ensure search attributes are always current — critical when reconnecting
|
|
29
30
|
// via WorkflowIdConflictPolicy.USE_EXISTING, which skips the attributes
|
|
30
31
|
// passed to client.workflow.start().
|
|
@@ -169,6 +170,7 @@ async function claudeSessionWorkflow(input) {
|
|
|
169
170
|
const commandHistory = input.commandHistory ?? [];
|
|
170
171
|
const reportHistory = input.reportHistory ?? [];
|
|
171
172
|
const qualityGates = input.qualityGates ?? [];
|
|
173
|
+
const worktrees = input.worktrees ?? [];
|
|
172
174
|
// ── Conductor-specific Handlers ──
|
|
173
175
|
if (input.metadata.isConductor) {
|
|
174
176
|
(0, workflow_1.setHandler)(signals_1.commandSignal, (cmd) => {
|
|
@@ -258,6 +260,23 @@ async function claudeSessionWorkflow(input) {
|
|
|
258
260
|
gate.status = deriveGateStatus(gate);
|
|
259
261
|
});
|
|
260
262
|
(0, workflow_1.setHandler)(signals_1.qualityGatesQuery, () => qualityGates);
|
|
263
|
+
// ── Worktree Handlers ──
|
|
264
|
+
(0, workflow_1.setHandler)(signals_1.setWorktreeSignal, (entry) => {
|
|
265
|
+
const existing = worktrees.findIndex((w) => w.player === entry.player);
|
|
266
|
+
if (existing >= 0) {
|
|
267
|
+
worktrees[existing] = entry;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
worktrees.push(entry);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
(0, workflow_1.setHandler)(signals_1.removeWorktreeSignal, (playerName) => {
|
|
274
|
+
const idx = worktrees.findIndex((w) => w.player === playerName);
|
|
275
|
+
if (idx >= 0) {
|
|
276
|
+
worktrees.splice(idx, 1);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
(0, workflow_1.setHandler)(signals_1.worktreesQuery, () => worktrees);
|
|
261
280
|
}
|
|
262
281
|
// ── Main Loop ──
|
|
263
282
|
const hasPendingOutbox = () => outbox.some((e) => e.status === 'pending');
|
|
@@ -416,7 +435,7 @@ async function claudeSessionWorkflow(input) {
|
|
|
416
435
|
messages: messages.filter((m) => !m.delivered),
|
|
417
436
|
sentMessages: sentMessages.slice(-50),
|
|
418
437
|
outbox: outbox.filter((e) => e.status === 'pending' || e.status === 'processing'),
|
|
419
|
-
...(input.metadata.isConductor ? { commandHistory, reportHistory, qualityGates } : {}),
|
|
438
|
+
...(input.metadata.isConductor ? { commandHistory, reportHistory, qualityGates, worktrees } : {}),
|
|
420
439
|
});
|
|
421
440
|
}
|
|
422
441
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SessionMetadata, Message, SentMessage, HistoryEntry, OutboxEntry, OutboxEntryInput, QualityGate } from '../types';
|
|
2
|
-
export type { SessionMetadata, SessionInput, SessionStatus, Message, Command, PlayerReport, SentMessage, HistoryEntry, OutboxEntry, OutboxEntryInput, OutboxEntryStatus, CueOutboxEntry, RecruitOutboxEntry, ReportOutboxEntry, StopOutboxEntry, EncoreOutboxEntry, QualityGate, QualityGateCriterion, } from '../types';
|
|
1
|
+
import type { SessionMetadata, Message, SentMessage, HistoryEntry, OutboxEntry, OutboxEntryInput, QualityGate, WorktreeEntry } from '../types';
|
|
2
|
+
export type { SessionMetadata, SessionInput, SessionStatus, Message, Command, PlayerReport, SentMessage, HistoryEntry, OutboxEntry, OutboxEntryInput, OutboxEntryStatus, CueOutboxEntry, RecruitOutboxEntry, ReportOutboxEntry, StopOutboxEntry, EncoreOutboxEntry, QualityGate, QualityGateCriterion, WorktreeEntry, } from '../types';
|
|
3
3
|
export declare const receiveMessageSignal: import("@temporalio/workflow").SignalDefinition<[{
|
|
4
4
|
from: string;
|
|
5
5
|
text: string;
|
|
@@ -61,3 +61,6 @@ export declare const evaluateGateCriteriaSignal: import("@temporalio/workflow").
|
|
|
61
61
|
evaluatedBy: string;
|
|
62
62
|
}], string>;
|
|
63
63
|
export declare const qualityGatesQuery: import("@temporalio/workflow").QueryDefinition<QualityGate[], [], string>;
|
|
64
|
+
export declare const setWorktreeSignal: import("@temporalio/workflow").SignalDefinition<[WorktreeEntry], string>;
|
|
65
|
+
export declare const removeWorktreeSignal: import("@temporalio/workflow").SignalDefinition<[string], string>;
|
|
66
|
+
export declare const worktreesQuery: import("@temporalio/workflow").QueryDefinition<WorktreeEntry[], [], string>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.qualityGatesQuery = exports.evaluateGateCriteriaSignal = exports.setQualityGateSignal = exports.outboxQuery = exports.submitOutboxUpdate = exports.checkAndSetStatusUpdate = exports.historyQuery = exports.playerReportSignal = exports.commandSignal = exports.allSentMessagesQuery = exports.allMessagesQuery = exports.pendingMessagesQuery = exports.getMetadataQuery = exports.getPartQuery = exports.updateMetadataSignal = exports.setNameSignal = exports.markDeliveredSignal = exports.setPartSignal = exports.recordSentMessageSignal = exports.receiveMessageSignal = void 0;
|
|
3
|
+
exports.worktreesQuery = exports.removeWorktreeSignal = exports.setWorktreeSignal = exports.qualityGatesQuery = exports.evaluateGateCriteriaSignal = exports.setQualityGateSignal = exports.outboxQuery = exports.submitOutboxUpdate = exports.checkAndSetStatusUpdate = exports.historyQuery = exports.playerReportSignal = exports.commandSignal = exports.allSentMessagesQuery = exports.allMessagesQuery = exports.pendingMessagesQuery = exports.getMetadataQuery = exports.getPartQuery = exports.updateMetadataSignal = exports.setNameSignal = exports.markDeliveredSignal = exports.setPartSignal = exports.recordSentMessageSignal = exports.receiveMessageSignal = void 0;
|
|
4
4
|
const workflow_1 = require("@temporalio/workflow");
|
|
5
5
|
// ── Player Signals ──
|
|
6
6
|
exports.receiveMessageSignal = (0, workflow_1.defineSignal)('receiveMessage');
|
|
@@ -30,3 +30,7 @@ exports.outboxQuery = (0, workflow_1.defineQuery)('outbox');
|
|
|
30
30
|
exports.setQualityGateSignal = (0, workflow_1.defineSignal)('setQualityGate');
|
|
31
31
|
exports.evaluateGateCriteriaSignal = (0, workflow_1.defineSignal)('evaluateGateCriteria');
|
|
32
32
|
exports.qualityGatesQuery = (0, workflow_1.defineQuery)('qualityGates');
|
|
33
|
+
// ── Worktree Signals + Query (conductor-only) ──
|
|
34
|
+
exports.setWorktreeSignal = (0, workflow_1.defineSignal)('setWorktree');
|
|
35
|
+
exports.removeWorktreeSignal = (0, workflow_1.defineSignal)('removeWorktree');
|
|
36
|
+
exports.worktreesQuery = (0, workflow_1.defineQuery)('worktrees');
|
|
@@ -53,7 +53,7 @@ You are a combination of Product Manager, Task Decomposition Expert, and Context
|
|
|
53
53
|
|
|
54
54
|
## Worktree Coordination
|
|
55
55
|
|
|
56
|
-
Use git
|
|
56
|
+
Use the `worktree` tool to give players isolated git checkouts 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
57
|
|
|
58
58
|
### When to use
|
|
59
59
|
|
|
@@ -63,28 +63,16 @@ Use git worktrees when two or more engineers need to work in the same repo on di
|
|
|
63
63
|
|
|
64
64
|
### How to coordinate
|
|
65
65
|
|
|
66
|
-
1. **Create the worktree
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
66
|
+
1. **Create**: `worktree({ action: "create", player: "eng-33" })` — provisions the worktree, installs dependencies, and notifies the player with the path and branch.
|
|
67
|
+
2. **Work**: the player receives a cue with their worktree path and branch. They commit and push as normal.
|
|
68
|
+
3. **Remove**: `worktree({ action: "remove", player: "eng-33" })` — cleans up the worktree and notifies the player. Stop the player session first on Windows (NTFS locks).
|
|
69
|
+
4. **List**: `worktree({ action: "list" })` — shows all active worktree assignments.
|
|
70
|
+
|
|
71
|
+
By default, `create` names the branch `{ensemble}/{player-name}`. Pass `branch` to override.
|
|
84
72
|
|
|
85
73
|
### Platform notes
|
|
86
74
|
|
|
87
|
-
- **Windows**:
|
|
75
|
+
- **Windows**: Worktrees are placed in short sibling directories (e.g. `../ct-feat33`) to avoid MAX_PATH limits. Stop the player session before calling `remove` — NTFS file locks will block cleanup while a session is active.
|
|
88
76
|
|
|
89
77
|
## Handling Context Pressure
|
|
90
78
|
|