claude-tempo 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands.d.ts +2 -0
- package/dist/cli/commands.js +33 -7
- package/dist/cli.js +2 -0
- package/dist/ensemble/loader.js +8 -0
- package/dist/ensemble/schema.d.ts +2 -0
- package/dist/tools/load-lineup.js +38 -1
- package/dist/types.d.ts +2 -0
- package/dist/utils/validation.d.ts +2 -0
- package/dist/utils/validation.js +3 -1
- package/dist/utils/worktree.d.ts +38 -0
- package/dist/utils/worktree.js +168 -0
- package/dist/workflows/session.js +2 -0
- package/dist/workflows/signals.d.ts +1 -0
- package/examples/agents/tempo-conductor.md +35 -0
- package/package.json +1 -1
- package/workflow-bundle.js +3 -1
package/dist/cli/commands.d.ts
CHANGED
package/dist/cli/commands.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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}
|
|
938
|
+
out.success(`Terminated ${terminated} workflow${terminated !== 1 ? 's' : ''} in ensemble "${ensembleName}"`);
|
|
916
939
|
}
|
|
917
940
|
else {
|
|
918
|
-
out.
|
|
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
package/dist/ensemble/loader.js
CHANGED
|
@@ -28,6 +28,12 @@ function loadLineup(filePath) {
|
|
|
28
28
|
if (!/^[a-zA-Z0-9_-]+$/.test(p.name)) {
|
|
29
29
|
throw new Error(`Invalid lineup: players[${i}].name "${p.name}" contains invalid characters`);
|
|
30
30
|
}
|
|
31
|
+
if (p.isolation != null && p.isolation !== 'worktree') {
|
|
32
|
+
throw new Error(`Invalid lineup: players[${i}].isolation must be "worktree" if specified`);
|
|
33
|
+
}
|
|
34
|
+
if (p.branch != null && (typeof p.branch !== 'string' || !p.branch)) {
|
|
35
|
+
throw new Error(`Invalid lineup: players[${i}].branch must be a non-empty string`);
|
|
36
|
+
}
|
|
31
37
|
}
|
|
32
38
|
// Validate schedules if present
|
|
33
39
|
if (doc.schedules != null) {
|
|
@@ -70,6 +76,8 @@ function loadLineup(filePath) {
|
|
|
70
76
|
...(p.agent != null && { agent: p.agent }),
|
|
71
77
|
...(p.instructions != null && { instructions: p.instructions }),
|
|
72
78
|
...(Array.isArray(p.allowedTools) && { allowedTools: p.allowedTools.map(String) }),
|
|
79
|
+
...(p.isolation != null && { isolation: p.isolation }),
|
|
80
|
+
...(p.branch != null && { branch: p.branch }),
|
|
73
81
|
})),
|
|
74
82
|
schedules: doc.schedules,
|
|
75
83
|
};
|
|
@@ -14,6 +14,8 @@ export interface EnsembleLineup {
|
|
|
14
14
|
agent?: string;
|
|
15
15
|
instructions?: string;
|
|
16
16
|
allowedTools?: string[];
|
|
17
|
+
isolation?: 'worktree';
|
|
18
|
+
branch?: string;
|
|
17
19
|
/** Transient: resolved agent definition name (set by loadAndResolveLineup). */
|
|
18
20
|
_agentDefinition?: string;
|
|
19
21
|
/** Transient: resolved absolute path to .md file (set by loadAndResolveLineup). */
|
|
@@ -15,6 +15,7 @@ const duration_1 = require("../utils/duration");
|
|
|
15
15
|
const safe_path_1 = require("../utils/safe-path");
|
|
16
16
|
const helpers_1 = require("./helpers");
|
|
17
17
|
const validation_1 = require("../utils/validation");
|
|
18
|
+
const worktree_1 = require("../utils/worktree");
|
|
18
19
|
const log = (...args) => console.error('[claude-tempo:load-lineup]', ...args);
|
|
19
20
|
function sleep(ms) {
|
|
20
21
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -79,7 +80,8 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
|
|
|
79
80
|
// Recruit players sequentially
|
|
80
81
|
for (const player of lineup.players) {
|
|
81
82
|
const playerName = player.name;
|
|
82
|
-
|
|
83
|
+
let workDir = player.workDir || process.cwd();
|
|
84
|
+
let worktreePath;
|
|
83
85
|
const agentType = player.agent === 'copilot' ? 'copilot' : 'claude';
|
|
84
86
|
const isCustomAgent = player.agent && player.agent !== 'default' && player.agent !== 'copilot';
|
|
85
87
|
const systemPrompt = player._agentDefinition ? undefined : (isCustomAgent ? player.agent : undefined);
|
|
@@ -104,6 +106,31 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
|
|
|
104
106
|
}
|
|
105
107
|
continue;
|
|
106
108
|
}
|
|
109
|
+
// Create worktree if isolation is requested
|
|
110
|
+
if (player.isolation === 'worktree') {
|
|
111
|
+
try {
|
|
112
|
+
// Determine git root: use the player's workDir as the git root,
|
|
113
|
+
// or fall back to cwd (which should be a git repo).
|
|
114
|
+
const gitRoot = workDir;
|
|
115
|
+
const result = (0, worktree_1.createWorktree)({
|
|
116
|
+
gitRoot,
|
|
117
|
+
ensemble: config.ensemble,
|
|
118
|
+
playerName,
|
|
119
|
+
branch: player.branch,
|
|
120
|
+
});
|
|
121
|
+
worktreePath = result.path;
|
|
122
|
+
workDir = result.path;
|
|
123
|
+
log(`Worktree for "${playerName}": ${result.path} (branch: ${result.branch}, created: ${result.created})`);
|
|
124
|
+
// Install dependencies — blocking but failure is non-fatal
|
|
125
|
+
if (result.created) {
|
|
126
|
+
(0, worktree_1.installDependencies)(result.path);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
failed.push(`${playerName}: worktree creation failed — ${err}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
107
134
|
// Record existing workflows to detect the new one
|
|
108
135
|
const existingIds = new Set();
|
|
109
136
|
const listQuery = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"`;
|
|
@@ -194,6 +221,16 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
|
|
|
194
221
|
failed.push(`${playerName}: spawned but did not register within 15s`);
|
|
195
222
|
continue;
|
|
196
223
|
}
|
|
224
|
+
// Record worktree path in session metadata
|
|
225
|
+
if (worktreePath && newWorkflowId) {
|
|
226
|
+
try {
|
|
227
|
+
const newHandle = client.workflow.getHandle(newWorkflowId);
|
|
228
|
+
await newHandle.signal('updateMetadata', { worktreePath });
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
log(`Failed to set worktreePath metadata for "${playerName}":`, err);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
197
234
|
// Send initial instructions if provided
|
|
198
235
|
if (player.instructions) {
|
|
199
236
|
try {
|
package/dist/types.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface SessionMetadata {
|
|
|
16
16
|
playerTypeDescription?: string;
|
|
17
17
|
/** Player ID of who recruited this player. */
|
|
18
18
|
recruitedBy?: string;
|
|
19
|
+
/** Worktree path if this session was spawned in an isolated worktree. */
|
|
20
|
+
worktreePath?: string;
|
|
19
21
|
}
|
|
20
22
|
export interface AgentTypeInfo {
|
|
21
23
|
name: string;
|
|
@@ -30,6 +30,8 @@ export declare const GATE_CRITERIA_MAX = 20;
|
|
|
30
30
|
export declare const GATE_CRITERION_TEXT_MAX = 512;
|
|
31
31
|
/** Maximum length for gate criterion notes. */
|
|
32
32
|
export declare const GATE_NOTES_MAX = 1024;
|
|
33
|
+
/** Timeout for npm install in worktrees (60s). */
|
|
34
|
+
export declare const WORKTREE_INSTALL_TIMEOUT = 60000;
|
|
33
35
|
/** Default number of recent messages to include as context in an encore. */
|
|
34
36
|
export declare const ENCORE_DEFAULT_CONTEXT_MESSAGES = 10;
|
|
35
37
|
/** Maximum length for message preview truncation. */
|
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.GATE_NOTES_MAX = exports.GATE_CRITERION_TEXT_MAX = exports.GATE_CRITERIA_MAX = exports.GATE_TASK_MAX = exports.CRON_EXPRESSION_MAX = exports.SCHEDULE_MESSAGE_MAX = exports.SCHEDULE_NAME_MAX = exports.PATH_MAX = exports.PART_MAX = exports.MESSAGE_MAX = exports.ENSEMBLE_NAME_REGEX = exports.PLAYER_NAME_MAX = exports.PLAYER_NAME_REGEX = void 0;
|
|
7
|
+
exports.PREVIEW_MAX_LENGTH = exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = exports.WORKTREE_INSTALL_TIMEOUT = exports.GATE_NOTES_MAX = exports.GATE_CRITERION_TEXT_MAX = exports.GATE_CRITERIA_MAX = exports.GATE_TASK_MAX = exports.CRON_EXPRESSION_MAX = exports.SCHEDULE_MESSAGE_MAX = exports.SCHEDULE_NAME_MAX = exports.PATH_MAX = exports.PART_MAX = exports.MESSAGE_MAX = exports.ENSEMBLE_NAME_REGEX = exports.PLAYER_NAME_MAX = exports.PLAYER_NAME_REGEX = void 0;
|
|
8
8
|
exports.shouldIncludeInBroadcast = shouldIncludeInBroadcast;
|
|
9
9
|
exports.validatePlayerName = validatePlayerName;
|
|
10
10
|
exports.validateEnsembleName = validateEnsembleName;
|
|
@@ -36,6 +36,8 @@ exports.GATE_CRITERIA_MAX = 20;
|
|
|
36
36
|
exports.GATE_CRITERION_TEXT_MAX = 512;
|
|
37
37
|
/** Maximum length for gate criterion notes. */
|
|
38
38
|
exports.GATE_NOTES_MAX = 1024;
|
|
39
|
+
/** Timeout for npm install in worktrees (60s). */
|
|
40
|
+
exports.WORKTREE_INSTALL_TIMEOUT = 60000;
|
|
39
41
|
/** Default number of recent messages to include as context in an encore. */
|
|
40
42
|
exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = 10;
|
|
41
43
|
/** Maximum length for message preview truncation. */
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute the base directory for all worktrees in an ensemble.
|
|
3
|
+
* Convention: `{gitRoot}/../.ct-worktrees/{ensemble}/`
|
|
4
|
+
*/
|
|
5
|
+
export declare function worktreeBasePath(gitRoot: string, ensemble: string): string;
|
|
6
|
+
export interface CreateWorktreeOpts {
|
|
7
|
+
gitRoot: string;
|
|
8
|
+
ensemble: string;
|
|
9
|
+
playerName: string;
|
|
10
|
+
branch?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface CreateWorktreeResult {
|
|
13
|
+
/** Absolute path to the worktree directory. */
|
|
14
|
+
path: string;
|
|
15
|
+
/** Branch name used for the worktree. */
|
|
16
|
+
branch: string;
|
|
17
|
+
/** Whether the worktree was newly created (false if it already existed). */
|
|
18
|
+
created: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a git worktree for a player. If the worktree already exists
|
|
22
|
+
* at the expected path, returns it without re-creating.
|
|
23
|
+
*
|
|
24
|
+
* Branch defaults to `{ensemble}/{playerName}` if not specified.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createWorktree(opts: CreateWorktreeOpts): CreateWorktreeResult;
|
|
27
|
+
/**
|
|
28
|
+
* Install dependencies in a worktree directory.
|
|
29
|
+
*
|
|
30
|
+
* Detects the package manager (npm, yarn, pnpm) by lockfile presence.
|
|
31
|
+
* Failure or timeout is logged but does not throw — the recruit proceeds
|
|
32
|
+
* with whatever state the worktree is in.
|
|
33
|
+
*/
|
|
34
|
+
export declare function installDependencies(worktreePath: string, timeoutMs?: number): void;
|
|
35
|
+
/**
|
|
36
|
+
* Remove a git worktree.
|
|
37
|
+
*/
|
|
38
|
+
export declare function removeWorktree(worktreePath: string): void;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.worktreeBasePath = worktreeBasePath;
|
|
37
|
+
exports.createWorktree = createWorktree;
|
|
38
|
+
exports.installDependencies = installDependencies;
|
|
39
|
+
exports.removeWorktree = removeWorktree;
|
|
40
|
+
/**
|
|
41
|
+
* Git worktree helpers for player isolation.
|
|
42
|
+
*
|
|
43
|
+
* Creates and manages git worktrees so each player can work on an
|
|
44
|
+
* isolated copy of the repository without conflicting with others.
|
|
45
|
+
*/
|
|
46
|
+
const child_process_1 = require("child_process");
|
|
47
|
+
const fs_1 = require("fs");
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const validation_1 = require("./validation");
|
|
50
|
+
const log = (...args) => console.error('[claude-tempo:worktree]', ...args);
|
|
51
|
+
/**
|
|
52
|
+
* Compute the base directory for all worktrees in an ensemble.
|
|
53
|
+
* Convention: `{gitRoot}/../.ct-worktrees/{ensemble}/`
|
|
54
|
+
*/
|
|
55
|
+
function worktreeBasePath(gitRoot, ensemble) {
|
|
56
|
+
return path.join(path.dirname(gitRoot), '.ct-worktrees', ensemble);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create a git worktree for a player. If the worktree already exists
|
|
60
|
+
* at the expected path, returns it without re-creating.
|
|
61
|
+
*
|
|
62
|
+
* Branch defaults to `{ensemble}/{playerName}` if not specified.
|
|
63
|
+
*/
|
|
64
|
+
function createWorktree(opts) {
|
|
65
|
+
const { gitRoot, ensemble, playerName } = opts;
|
|
66
|
+
const branch = opts.branch || `${ensemble}/${playerName}`;
|
|
67
|
+
const basePath = worktreeBasePath(gitRoot, ensemble);
|
|
68
|
+
const wtPath = path.join(basePath, playerName);
|
|
69
|
+
// If worktree already exists, reuse it
|
|
70
|
+
if ((0, fs_1.existsSync)(path.join(wtPath, '.git'))) {
|
|
71
|
+
log(`Worktree already exists at "${wtPath}" — reusing`);
|
|
72
|
+
return { path: wtPath, branch, created: false };
|
|
73
|
+
}
|
|
74
|
+
// Ensure base directory exists
|
|
75
|
+
(0, fs_1.mkdirSync)(basePath, { recursive: true });
|
|
76
|
+
// Check if the branch already has a worktree (would cause git error)
|
|
77
|
+
try {
|
|
78
|
+
const existing = (0, child_process_1.execFileSync)('git', ['worktree', 'list', '--porcelain'], {
|
|
79
|
+
cwd: gitRoot,
|
|
80
|
+
encoding: 'utf8',
|
|
81
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
82
|
+
});
|
|
83
|
+
// Parse porcelain output: "branch refs/heads/{branch}" lines
|
|
84
|
+
const branchRef = `refs/heads/${branch}`;
|
|
85
|
+
if (existing.includes(`branch ${branchRef}`)) {
|
|
86
|
+
throw new Error(`Branch "${branch}" already has an active worktree. ` +
|
|
87
|
+
`Remove it first with \`git worktree remove\` or choose a different branch.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
// Re-throw our own error, swallow git failures (e.g., no worktrees yet)
|
|
92
|
+
if (err.message?.includes('already has an active worktree'))
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
// Create the worktree. Use -B to create/reset the branch.
|
|
96
|
+
try {
|
|
97
|
+
log(`Creating worktree: git worktree add -B ${branch} ${wtPath}`);
|
|
98
|
+
(0, child_process_1.execFileSync)('git', ['worktree', 'add', '-B', branch, wtPath], {
|
|
99
|
+
cwd: gitRoot,
|
|
100
|
+
encoding: 'utf8',
|
|
101
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const msg = err.stderr || err.stdout || err.message || String(err);
|
|
106
|
+
throw new Error(`Failed to create worktree at "${wtPath}": ${msg.trim()}`);
|
|
107
|
+
}
|
|
108
|
+
return { path: wtPath, branch, created: true };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Install dependencies in a worktree directory.
|
|
112
|
+
*
|
|
113
|
+
* Detects the package manager (npm, yarn, pnpm) by lockfile presence.
|
|
114
|
+
* Failure or timeout is logged but does not throw — the recruit proceeds
|
|
115
|
+
* with whatever state the worktree is in.
|
|
116
|
+
*/
|
|
117
|
+
function installDependencies(worktreePath, timeoutMs = validation_1.WORKTREE_INSTALL_TIMEOUT) {
|
|
118
|
+
// Detect package manager by lockfile
|
|
119
|
+
let cmd;
|
|
120
|
+
let args;
|
|
121
|
+
if ((0, fs_1.existsSync)(path.join(worktreePath, 'pnpm-lock.yaml'))) {
|
|
122
|
+
cmd = 'pnpm';
|
|
123
|
+
args = ['install', '--frozen-lockfile'];
|
|
124
|
+
}
|
|
125
|
+
else if ((0, fs_1.existsSync)(path.join(worktreePath, 'yarn.lock'))) {
|
|
126
|
+
cmd = 'yarn';
|
|
127
|
+
args = ['install', '--frozen-lockfile'];
|
|
128
|
+
}
|
|
129
|
+
else if ((0, fs_1.existsSync)(path.join(worktreePath, 'package-lock.json')) || (0, fs_1.existsSync)(path.join(worktreePath, 'package.json'))) {
|
|
130
|
+
cmd = 'npm';
|
|
131
|
+
args = ['install'];
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
log(`No package.json found in "${worktreePath}" — skipping install`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
log(`Installing dependencies in "${worktreePath}": ${cmd} ${args.join(' ')}`);
|
|
139
|
+
(0, child_process_1.execFileSync)(cmd, args, {
|
|
140
|
+
cwd: worktreePath,
|
|
141
|
+
encoding: 'utf8',
|
|
142
|
+
timeout: timeoutMs,
|
|
143
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
144
|
+
});
|
|
145
|
+
log(`Dependencies installed successfully in "${worktreePath}"`);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
// Log warning but don't throw — recruit should still proceed
|
|
149
|
+
const msg = err.killed ? `Timed out after ${timeoutMs}ms` : (err.stderr || err.message || String(err));
|
|
150
|
+
log(`Warning: dependency install failed in "${worktreePath}": ${msg}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Remove a git worktree.
|
|
155
|
+
*/
|
|
156
|
+
function removeWorktree(worktreePath) {
|
|
157
|
+
try {
|
|
158
|
+
(0, child_process_1.execFileSync)('git', ['worktree', 'remove', '--force', worktreePath], {
|
|
159
|
+
encoding: 'utf8',
|
|
160
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
161
|
+
});
|
|
162
|
+
log(`Removed worktree at "${worktreePath}"`);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const msg = err.stderr || err.message || String(err);
|
|
166
|
+
log(`Warning: failed to remove worktree at "${worktreePath}": ${msg.trim()}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -114,6 +114,8 @@ async function claudeSessionWorkflow(input) {
|
|
|
114
114
|
input.metadata.playerType = update.playerType;
|
|
115
115
|
if (update.playerTypeDescription != null)
|
|
116
116
|
input.metadata.playerTypeDescription = update.playerTypeDescription;
|
|
117
|
+
if (update.worktreePath != null)
|
|
118
|
+
input.metadata.worktreePath = update.worktreePath;
|
|
117
119
|
if (update.status != null) {
|
|
118
120
|
input.metadata.status = update.status;
|
|
119
121
|
// Re-enable stale detection only when explicitly requested (server.ts sets this)
|
|
@@ -21,6 +21,7 @@ export declare const updateMetadataSignal: import("@temporalio/workflow").Signal
|
|
|
21
21
|
enableStaleDetection?: boolean;
|
|
22
22
|
playerType?: string;
|
|
23
23
|
playerTypeDescription?: string;
|
|
24
|
+
worktreePath?: string;
|
|
24
25
|
}], string>;
|
|
25
26
|
export declare const getPartQuery: import("@temporalio/workflow").QueryDefinition<string, [], string>;
|
|
26
27
|
export declare const getMetadataQuery: import("@temporalio/workflow").QueryDefinition<SessionMetadata, [], string>;
|
|
@@ -51,6 +51,41 @@ You are a combination of Product Manager, Task Decomposition Expert, and Context
|
|
|
51
51
|
- **Escalation**: If a player reports a blocker you can't resolve, report it upward or recruit a specialist.
|
|
52
52
|
- **Wrap-up**: Collect final reports, synthesize results, stop idle players, report completion.
|
|
53
53
|
|
|
54
|
+
## Worktree Coordination
|
|
55
|
+
|
|
56
|
+
Use git worktrees when two or more engineers need to work in the same repo on different branches simultaneously. Each worktree is an independent checkout — players can build, test, and commit without interfering with each other.
|
|
57
|
+
|
|
58
|
+
### When to use
|
|
59
|
+
|
|
60
|
+
- Two players working on different feature branches in the same repo
|
|
61
|
+
- Running a long build/test in one branch while another player continues development
|
|
62
|
+
- Isolating risky changes from the main working tree
|
|
63
|
+
|
|
64
|
+
### How to coordinate
|
|
65
|
+
|
|
66
|
+
1. **Create the worktree** (you or a player with shell access):
|
|
67
|
+
```
|
|
68
|
+
git worktree add ../ct-{task} {branch}
|
|
69
|
+
```
|
|
70
|
+
2. **Install dependencies** in the new worktree:
|
|
71
|
+
```
|
|
72
|
+
cd ../ct-{task} && npm install
|
|
73
|
+
```
|
|
74
|
+
3. **Recruit with `workDir`** pointing to the worktree:
|
|
75
|
+
```
|
|
76
|
+
recruit({ name: "eng-33", workDir: "../ct-{task}", ... })
|
|
77
|
+
```
|
|
78
|
+
4. **Clean up** after the task: stop the player first, then remove the worktree:
|
|
79
|
+
```
|
|
80
|
+
git worktree remove ../ct-{task}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`recruit` already accepts `workDir` — no new tools are needed for manual worktree coordination.
|
|
84
|
+
|
|
85
|
+
### Platform notes
|
|
86
|
+
|
|
87
|
+
- **Windows**: Use short sibling paths (e.g. `../ct-feat33`) to avoid MAX_PATH limits. Always stop players before removing worktrees — NTFS file locks will block cleanup while a session is active.
|
|
88
|
+
|
|
54
89
|
## Handling Context Pressure
|
|
55
90
|
|
|
56
91
|
When a player reports context pressure (growing context, lost instructions, repeated work), act immediately:
|