claude-tempo 0.19.0 → 0.20.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 +3 -3
- package/README.md +3 -1
- package/dist/activities/outbox.js +2 -1
- package/dist/activities/schedule-fire.js +4 -0
- package/dist/cli/commands.d.ts +6 -0
- package/dist/cli/commands.js +236 -49
- package/dist/cli.js +16 -0
- package/dist/ensemble/loader.d.ts +9 -0
- package/dist/ensemble/loader.js +50 -0
- package/dist/server.js +1 -0
- package/dist/tools/load-lineup.js +13 -20
- package/dist/tools/recruit.js +1 -0
- package/dist/tools/stop.js +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/workflows/session.js +15 -6
- package/dist/workflows/signals.d.ts +1 -0
- package/package.json +1 -1
- package/workflow-bundle.js +16 -7
package/CLAUDE.md
CHANGED
|
@@ -20,7 +20,7 @@ src/
|
|
|
20
20
|
├── cli.ts # CLI entry point (claude-tempo command)
|
|
21
21
|
├── daemon.ts # Daemon entry point — runs Temporal workers as a detached background process
|
|
22
22
|
├── cli/
|
|
23
|
-
│ ├── commands.ts # CLI command implementations (up, start, conduct, status, stop, …)
|
|
23
|
+
│ ├── commands.ts # CLI command implementations (up, start, conduct, status, stop, upgrade, …)
|
|
24
24
|
│ ├── config-command.ts # config subcommand (interactive + set/show)
|
|
25
25
|
│ ├── daemon.ts # Daemon management utilities (start, stop, status, logs, isDaemonRunning)
|
|
26
26
|
│ ├── mcp.ts # MCP server registration helpers (init, global vs project)
|
|
@@ -137,7 +137,7 @@ npm test
|
|
|
137
137
|
- **Part**: A player's description of what it's working on
|
|
138
138
|
- **Recruit**: Spawning a new Claude Code session as a player. The workflow is pre-created with the initial message before the process spawns, ensuring reliable delivery.
|
|
139
139
|
- **set_name**: Players start with a random hex ID; `set_name` updates the `ClaudeTempoPlayerId` search attribute to a human-readable name
|
|
140
|
-
- **Session status**: Each session has a status (`pending` → `active` → `stale`) tracked via `ClaudeTempoStatus` search attribute. Pre-created workflows start as `pending`, transition to `active` when the process connects, and become `stale` if messages go undelivered for 3+ minutes.
|
|
140
|
+
- **Session status**: Each session has a status (`pending` → `active` → `stale` | `blocked`) tracked via `ClaudeTempoStatus` search attribute. Pre-created workflows start as `pending`, transition to `active` when the process connects, and become `stale` if messages go undelivered for 3+ minutes. Sessions become `blocked` when they are alive (delivering messages) but have produced no response to a `responseRequested: true` message for 5+ minutes — they may be stuck or spinning. Informational messages (broadcasts, schedule-fires, heartbeats, system notifications) set `responseRequested: false` and do not trigger blocked detection. Blocked status auto-recovers to `active` on next outbound.
|
|
141
141
|
- **Outbox**: Outbound requests (cue, report, stop, recruit, encore) go through the session's own workflow outbox instead of directly signaling other workflows. The workflow's dispatch loop processes entries via activities, decoupling tools from cross-workflow signaling.
|
|
142
142
|
- **Encore**: Revives a `stale` player session by restarting the Claude process and reconnecting to the existing Temporal workflow, with recent message context restored. Cannot encore `active`, `pending`, or `terminated` sessions — use `cue`, wait, or `recruit` respectively.
|
|
143
143
|
- **Broadcast**: Fan-out variant of `cue` — sends a message to all active players in the ensemble in a single call. Optionally filtered by player type. Skips the sender, pending sessions, and (by default) stale sessions.
|
|
@@ -146,7 +146,7 @@ npm test
|
|
|
146
146
|
- **Player types**: Reusable agent definitions in Claude Code's standard subagent format (`.md` files with YAML frontmatter). Ensemble lineups can reference types by name via a `type` field on players. Three-tier lookup: project `.claude/agents/` → user `~/.claude/agents/` → shipped `examples/agents/`. Players know their type via workflow metadata and the `who_am_i` tool. Agent type frontmatter may include an `allowedTools` array to restrict which MCP/CLI tools the spawned session can use (e.g., `allowedTools: [Read, Glob, Grep]`). When present, the type's `allowedTools` overrides any lineup-level setting and is passed to the Claude Code session via `--allowedTools`.
|
|
147
147
|
- **Agent type discovery**: The `agent_types` MCP tool and `claude-tempo agent-types` CLI command let conductors discover available player types. Shipped examples (tempo-conductor, tempo-composer, tempo-soloist, tempo-tuner, tempo-critic, tempo-roadie, tempo-improv, tempo-liner) work out of the box. Ensemble lineups: tempo-big-band (full lifecycle), tempo-dev-team (feature work), tempo-review-squad (parallel review), tempo-jam-session (exploration).
|
|
148
148
|
- **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.
|
|
149
|
-
- **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;
|
|
149
|
+
- **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; `load_lineup` resolves the lineup by name using a three-tier lookup: saved lineups → shipped examples → file path. Save via `save_lineup` to snapshot a running ensemble's state for later reuse.
|
|
150
150
|
- **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`.
|
|
151
151
|
- **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).
|
|
152
152
|
- **Stage**: A fan-out/fan-in tracking primitive for the conductor. Created via `stage` (conductor only), listing via `stages`, cancelled via `cancel_stage`. Each stage tracks a set of players; when a tracked player sends a `report`, their stage status updates automatically (`waiting` → `reported` or `blocked`). When all players have reported, the conductor is notified that the stage is complete. If `failurePolicy` is `'halt'` (default), a blocker from any player fails the entire stage. Stages are stored in the conductor workflow and survive `continueAsNew`.
|
package/README.md
CHANGED
|
@@ -145,7 +145,8 @@ Inside the TUI, type `/help` for slash commands. Inside Claude Code, use the `en
|
|
|
145
145
|
| `claude-tempo start [ensemble]` | Open a player session |
|
|
146
146
|
| `claude-tempo conduct [ensemble]` | Start a conductor session |
|
|
147
147
|
| `claude-tempo status` | Show active sessions |
|
|
148
|
-
| `claude-tempo
|
|
148
|
+
| `claude-tempo stop [ensemble]` | Stop sessions (recoverable via `encore`) |
|
|
149
|
+
| `claude-tempo down` | Full teardown — sessions, daemon, and Temporal |
|
|
149
150
|
|
|
150
151
|
**Lineups**
|
|
151
152
|
| Command | Description |
|
|
@@ -165,6 +166,7 @@ Inside the TUI, type `/help` for slash commands. Inside Claude Code, use the `en
|
|
|
165
166
|
| Command | Description |
|
|
166
167
|
|---------|-------------|
|
|
167
168
|
| `claude-tempo daemon start\|stop\|status\|logs` | Manage the worker daemon |
|
|
169
|
+
| `claude-tempo upgrade [version]` | Graceful self-update (stops daemon, installs, restarts) |
|
|
168
170
|
| `claude-tempo config` | Configure env vars interactively |
|
|
169
171
|
| `claude-tempo preflight` | Verify environment |
|
|
170
172
|
|
|
@@ -92,6 +92,7 @@ function createOutboxActivities(client, config) {
|
|
|
92
92
|
await conductorHandle.signal('receiveMessage', {
|
|
93
93
|
from: 'system',
|
|
94
94
|
text: `Session "${targetPlayerId}" was terminated by ${terminatedBy}.`,
|
|
95
|
+
responseRequested: false,
|
|
95
96
|
});
|
|
96
97
|
}
|
|
97
98
|
catch {
|
|
@@ -265,7 +266,7 @@ function createOutboxActivities(client, config) {
|
|
|
265
266
|
'Resume where you left off. Use `ensemble` to see who is active.',
|
|
266
267
|
].filter(Boolean).join('\n');
|
|
267
268
|
// Inject context message (status already set to pending atomically above)
|
|
268
|
-
await handle.signal('receiveMessage', { from: fromPlayerId, text: contextMessage });
|
|
269
|
+
await handle.signal('receiveMessage', { from: fromPlayerId, text: contextMessage, responseRequested: false });
|
|
269
270
|
log(`Encore prepared for "${targetPlayerId}" — status reset to pending, context injected`);
|
|
270
271
|
// Return spawn parameters from the target's metadata
|
|
271
272
|
const agentType = metadata.agentType || 'claude';
|
|
@@ -37,6 +37,7 @@ function createScheduleActivities(client) {
|
|
|
37
37
|
text,
|
|
38
38
|
isScheduled: true,
|
|
39
39
|
scheduleName,
|
|
40
|
+
responseRequested: false,
|
|
40
41
|
});
|
|
41
42
|
delivered++;
|
|
42
43
|
}
|
|
@@ -67,6 +68,7 @@ function createScheduleActivities(client) {
|
|
|
67
68
|
text,
|
|
68
69
|
isScheduled: true,
|
|
69
70
|
scheduleName,
|
|
71
|
+
responseRequested: false,
|
|
70
72
|
});
|
|
71
73
|
return { success: true };
|
|
72
74
|
}
|
|
@@ -92,6 +94,7 @@ async function notifyFailure(client, ensemble, createdBy, scheduleName, target,
|
|
|
92
94
|
text: failureText,
|
|
93
95
|
isScheduled: true,
|
|
94
96
|
scheduleName,
|
|
97
|
+
responseRequested: false,
|
|
95
98
|
});
|
|
96
99
|
return;
|
|
97
100
|
}
|
|
@@ -107,6 +110,7 @@ async function notifyFailure(client, ensemble, createdBy, scheduleName, target,
|
|
|
107
110
|
from: 'scheduler',
|
|
108
111
|
text: failureText,
|
|
109
112
|
isScheduled: true,
|
|
113
|
+
responseRequested: false,
|
|
110
114
|
scheduleName,
|
|
111
115
|
});
|
|
112
116
|
}
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ interface DownOpts extends CliOverrides {
|
|
|
36
36
|
ensemble?: string;
|
|
37
37
|
all: boolean;
|
|
38
38
|
removeMcp: boolean;
|
|
39
|
+
keepDaemon: boolean;
|
|
40
|
+
yes: boolean;
|
|
39
41
|
dir: string;
|
|
40
42
|
}
|
|
41
43
|
export declare function down(opts: DownOpts): Promise<void>;
|
|
@@ -77,4 +79,8 @@ interface DaemonOpts extends CliOverrides {
|
|
|
77
79
|
export declare function daemon(opts: DaemonOpts): Promise<void>;
|
|
78
80
|
export declare function help(): void;
|
|
79
81
|
export declare function version(): void;
|
|
82
|
+
interface UpgradeOpts extends CliOverrides {
|
|
83
|
+
version?: string;
|
|
84
|
+
}
|
|
85
|
+
export declare function upgrade(opts: UpgradeOpts): Promise<void>;
|
|
80
86
|
export {};
|
package/dist/cli/commands.js
CHANGED
|
@@ -47,6 +47,8 @@ exports.ensembleCommand = ensembleCommand;
|
|
|
47
47
|
exports.daemon = daemon;
|
|
48
48
|
exports.help = help;
|
|
49
49
|
exports.version = version;
|
|
50
|
+
exports.upgrade = upgrade;
|
|
51
|
+
const readline = __importStar(require("readline"));
|
|
50
52
|
const fs_1 = require("fs");
|
|
51
53
|
const path_1 = require("path");
|
|
52
54
|
const child_process_1 = require("child_process");
|
|
@@ -652,46 +654,14 @@ async function up(opts) {
|
|
|
652
654
|
let lineup;
|
|
653
655
|
const lineupArg = opts.lineup;
|
|
654
656
|
if (lineupArg) {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
// Direct file path
|
|
659
|
-
lineupPath = (0, path_1.resolve)(lineupArg);
|
|
657
|
+
try {
|
|
658
|
+
const resolution = (0, loader_1.resolveLineupPath)(lineupArg);
|
|
659
|
+
lineup = (0, loader_1.loadLineup)(resolution.path);
|
|
660
660
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const savedYaml = (0, path_1.join)(ensemblesDir, `${lineupArg}.yaml`);
|
|
665
|
-
const savedYml = (0, path_1.join)(ensemblesDir, `${lineupArg}.yml`);
|
|
666
|
-
if ((0, fs_1.existsSync)(savedYaml)) {
|
|
667
|
-
lineupPath = savedYaml;
|
|
668
|
-
}
|
|
669
|
-
else if ((0, fs_1.existsSync)(savedYml)) {
|
|
670
|
-
lineupPath = savedYml;
|
|
671
|
-
}
|
|
672
|
-
else {
|
|
673
|
-
// Try shipped examples
|
|
674
|
-
const shippedPath = (0, path_1.join)(PACKAGE_ROOT, 'examples', 'ensembles', `${lineupArg}.yaml`);
|
|
675
|
-
const shippedYml = (0, path_1.join)(PACKAGE_ROOT, 'examples', 'ensembles', `${lineupArg}.yml`);
|
|
676
|
-
if ((0, fs_1.existsSync)(shippedPath)) {
|
|
677
|
-
lineupPath = shippedPath;
|
|
678
|
-
}
|
|
679
|
-
else if ((0, fs_1.existsSync)(shippedYml)) {
|
|
680
|
-
lineupPath = shippedYml;
|
|
681
|
-
}
|
|
682
|
-
else {
|
|
683
|
-
out.error(`Lineup "${lineupArg}" not found as file, saved lineup, or shipped example`);
|
|
684
|
-
const saved = (0, saver_1.listLineups)();
|
|
685
|
-
if (saved.length)
|
|
686
|
-
out.log(` Saved: ${saved.map(b => b.name).join(', ')}`);
|
|
687
|
-
const shipped = (0, fs_1.readdirSync)((0, path_1.join)(PACKAGE_ROOT, 'examples', 'ensembles')).filter(f => f.endsWith('.yaml') || f.endsWith('.yml')).map(f => f.replace(/\.ya?ml$/, ''));
|
|
688
|
-
if (shipped.length)
|
|
689
|
-
out.log(` Shipped: ${shipped.join(', ')}`);
|
|
690
|
-
process.exit(1);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
out.error(err.message);
|
|
663
|
+
process.exit(1);
|
|
693
664
|
}
|
|
694
|
-
lineup = (0, loader_1.loadLineup)(lineupPath);
|
|
695
665
|
}
|
|
696
666
|
if (lineup) {
|
|
697
667
|
out.check('Lineup loaded', true, lineup.name);
|
|
@@ -989,6 +959,20 @@ function parseDuration(s) {
|
|
|
989
959
|
default: throw new Error(`Unknown duration unit: "${match[2]}"`);
|
|
990
960
|
}
|
|
991
961
|
}
|
|
962
|
+
/** Prompt the user for y/n confirmation. Exits with code 1 in non-TTY environments. */
|
|
963
|
+
async function confirmPrompt(message) {
|
|
964
|
+
if (!process.stdin.isTTY) {
|
|
965
|
+
out.error('Non-interactive environment: use --yes / -y to confirm teardown.');
|
|
966
|
+
process.exit(1);
|
|
967
|
+
}
|
|
968
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
969
|
+
return new Promise((resolve) => {
|
|
970
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
971
|
+
rl.close();
|
|
972
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
}
|
|
992
976
|
async function down(opts) {
|
|
993
977
|
const config = (0, config_1.getConfig)(opts);
|
|
994
978
|
let ensembleName = opts.ensemble;
|
|
@@ -1017,14 +1001,42 @@ async function down(opts) {
|
|
|
1017
1001
|
}
|
|
1018
1002
|
else if (runningEnsembles.size === 1) {
|
|
1019
1003
|
ensembleName = [...runningEnsembles][0];
|
|
1004
|
+
// Auto-detected ensemble requires confirmation unless --yes
|
|
1005
|
+
if (!opts.yes) {
|
|
1006
|
+
out.heading('claude-tempo teardown');
|
|
1007
|
+
out.log(` This will destroy ensemble ${out.bold(ensembleName)}: all sessions, daemon, and Temporal server.`);
|
|
1008
|
+
const confirmed = await confirmPrompt('Proceed?');
|
|
1009
|
+
if (!confirmed) {
|
|
1010
|
+
out.log('Aborted.');
|
|
1011
|
+
process.exit(0);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1020
1014
|
}
|
|
1021
1015
|
else {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
out.
|
|
1016
|
+
// Multiple ensembles: require confirmation or --yes for each
|
|
1017
|
+
if (!opts.yes) {
|
|
1018
|
+
out.heading('claude-tempo teardown');
|
|
1019
|
+
out.log(` Multiple ensembles running:`);
|
|
1020
|
+
for (const name of [...runningEnsembles].sort()) {
|
|
1021
|
+
out.log(` - ${name}`);
|
|
1022
|
+
}
|
|
1023
|
+
out.log('');
|
|
1024
|
+
out.log(` This will destroy ${out.bold('all')} of them: sessions, daemon, and Temporal server.`);
|
|
1025
|
+
const confirmed = await confirmPrompt('Proceed?');
|
|
1026
|
+
if (!confirmed) {
|
|
1027
|
+
out.log('Aborted. To tear down a specific ensemble:');
|
|
1028
|
+
for (const name of [...runningEnsembles].sort()) {
|
|
1029
|
+
out.log(` - claude-tempo down ${name}`);
|
|
1030
|
+
}
|
|
1031
|
+
process.exit(0);
|
|
1032
|
+
}
|
|
1033
|
+
// Treat as --all since user confirmed tearing down everything
|
|
1034
|
+
opts.all = true;
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
// --yes with multiple ensembles: treat as --all
|
|
1038
|
+
opts.all = true;
|
|
1025
1039
|
}
|
|
1026
|
-
out.log(` - claude-tempo down --all`);
|
|
1027
|
-
process.exit(1);
|
|
1028
1040
|
}
|
|
1029
1041
|
}
|
|
1030
1042
|
catch (err) {
|
|
@@ -1132,9 +1144,13 @@ async function down(opts) {
|
|
|
1132
1144
|
}
|
|
1133
1145
|
// Step 2: Kill bridge processes via PID files
|
|
1134
1146
|
killBridgeProcesses();
|
|
1135
|
-
// Step 2.5: Stop worker daemon —
|
|
1136
|
-
|
|
1137
|
-
|
|
1147
|
+
// Step 2.5: Stop worker daemon — unless --keep-daemon or other ensembles still active
|
|
1148
|
+
if (opts.keepDaemon) {
|
|
1149
|
+
if ((0, daemon_1.isDaemonRunning)()) {
|
|
1150
|
+
out.log(` ${out.dim('Worker daemon left running (--keep-daemon)')}`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
else if (opts.all || !hasRemainingWorkflows) {
|
|
1138
1154
|
if ((0, daemon_1.stopDaemon)()) {
|
|
1139
1155
|
out.success('Worker daemon stopped');
|
|
1140
1156
|
}
|
|
@@ -1549,6 +1565,7 @@ async function broadcast(opts) {
|
|
|
1549
1565
|
await handle.signal('receiveMessage', {
|
|
1550
1566
|
from: 'cli',
|
|
1551
1567
|
text: opts.message,
|
|
1568
|
+
responseRequested: false,
|
|
1552
1569
|
});
|
|
1553
1570
|
sent++;
|
|
1554
1571
|
out.log(` ${out.green('✓')} ${target.playerId}`);
|
|
@@ -1629,7 +1646,7 @@ async function encore(opts) {
|
|
|
1629
1646
|
].filter(Boolean).join('\n');
|
|
1630
1647
|
// Reset status and inject context message
|
|
1631
1648
|
await targetHandle.signal('updateMetadata', { status: 'pending' });
|
|
1632
|
-
await targetHandle.signal('receiveMessage', { from: 'system', text: contextMessage });
|
|
1649
|
+
await targetHandle.signal('receiveMessage', { from: 'system', text: contextMessage, responseRequested: false });
|
|
1633
1650
|
out.log(`Reviving "${opts.name}" in ${targetMeta.workDir}...`);
|
|
1634
1651
|
// Resolve agent flags
|
|
1635
1652
|
let agentFlags = [];
|
|
@@ -1823,7 +1840,7 @@ ${out.bold('Usage:')}
|
|
|
1823
1840
|
|
|
1824
1841
|
${out.bold('Commands:')}
|
|
1825
1842
|
${out.cyan('up')} [ensemble] First-time setup: start Temporal, configure MCP, launch conductor
|
|
1826
|
-
${out.cyan('down')}
|
|
1843
|
+
${out.cyan('down')} [ensemble] Tear down everything: sessions, daemon, Temporal server, MCP config
|
|
1827
1844
|
${out.cyan('server')} Start the Temporal dev server and register search attributes
|
|
1828
1845
|
${out.cyan('conduct')} [ensemble] Start a conductor session (resumes existing, --replace to restart)
|
|
1829
1846
|
${out.cyan('start')} [ensemble] Start a player session
|
|
@@ -1834,6 +1851,7 @@ ${out.bold('Commands:')}
|
|
|
1834
1851
|
${out.cyan('encore')} <name> Revive a stale player session (reconnect with context)
|
|
1835
1852
|
${out.cyan('agent-types')} <sub> Manage player type definitions (list/show/init)
|
|
1836
1853
|
${out.cyan('daemon')} <sub> Manage the worker daemon (start/stop/status/logs)
|
|
1854
|
+
${out.cyan('upgrade')} [version] Upgrade claude-tempo to latest (or specific version)
|
|
1837
1855
|
${out.cyan('config')} Configure Temporal connection settings
|
|
1838
1856
|
${out.cyan('init')} Register MCP server globally (or --project for .mcp.json)
|
|
1839
1857
|
${out.cyan('preflight')} Run preflight checks only
|
|
@@ -1853,9 +1871,11 @@ ${out.bold('Other options:')}
|
|
|
1853
1871
|
--background Run Temporal in background (server only)
|
|
1854
1872
|
--project Use per-project .mcp.json instead of global (init only)
|
|
1855
1873
|
--keep-mcp Don't remove MCP config (down only)
|
|
1874
|
+
--keep-daemon Don't stop the worker daemon (down only)
|
|
1875
|
+
-y, --yes Skip confirmation prompt (down only)
|
|
1856
1876
|
--all Stop all sessions (stop only)
|
|
1857
1877
|
--lineup <name|file> Load ensemble lineup by name or file path (up only)
|
|
1858
|
-
--ensemble <name> Target a specific ensemble (stop
|
|
1878
|
+
--ensemble <name> Target a specific ensemble (stop/down)
|
|
1859
1879
|
-d, --dir <path> Target directory (default: cwd)
|
|
1860
1880
|
|
|
1861
1881
|
${out.bold('Config command:')}
|
|
@@ -1898,3 +1918,170 @@ function version() {
|
|
|
1898
1918
|
out.log('claude-tempo (unknown version)');
|
|
1899
1919
|
}
|
|
1900
1920
|
}
|
|
1921
|
+
async function upgrade(opts) {
|
|
1922
|
+
const config = (0, config_1.getConfig)(opts);
|
|
1923
|
+
const targetVersion = opts.version || 'latest';
|
|
1924
|
+
const installSpec = targetVersion === 'latest' ? 'claude-tempo' : `claude-tempo@${targetVersion}`;
|
|
1925
|
+
// Read current version
|
|
1926
|
+
let currentVersion = 'unknown';
|
|
1927
|
+
try {
|
|
1928
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
1929
|
+
currentVersion = pkg.version || 'unknown';
|
|
1930
|
+
}
|
|
1931
|
+
catch { /* ignore */ }
|
|
1932
|
+
out.heading('claude-tempo upgrade');
|
|
1933
|
+
out.log(` Current: v${currentVersion}`);
|
|
1934
|
+
out.log(` Target: ${targetVersion}`);
|
|
1935
|
+
console.log();
|
|
1936
|
+
// Check for active sessions — warn the user
|
|
1937
|
+
let activeSessions = 0;
|
|
1938
|
+
try {
|
|
1939
|
+
const connection = await Promise.race([
|
|
1940
|
+
(0, connection_1.createTemporalConnection)(config),
|
|
1941
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
|
|
1942
|
+
]);
|
|
1943
|
+
const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
|
|
1944
|
+
const query = 'WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"';
|
|
1945
|
+
for await (const _wf of client.workflow.list({ query })) {
|
|
1946
|
+
activeSessions++;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
catch {
|
|
1950
|
+
// Can't connect to Temporal — that's fine, proceed with upgrade
|
|
1951
|
+
}
|
|
1952
|
+
if (activeSessions > 0) {
|
|
1953
|
+
out.warn(`${activeSessions} active session(s) detected. They will lose daemon connectivity during upgrade.`);
|
|
1954
|
+
out.log(` ${out.dim('Consider running: claude-tempo stop --all')}`);
|
|
1955
|
+
console.log();
|
|
1956
|
+
}
|
|
1957
|
+
// Stop the daemon gracefully (releases .node file locks)
|
|
1958
|
+
const daemonWasRunning = (0, daemon_1.isDaemonRunning)();
|
|
1959
|
+
if (daemonWasRunning) {
|
|
1960
|
+
out.log('Stopping daemon...');
|
|
1961
|
+
(0, daemon_1.stopDaemon)();
|
|
1962
|
+
// Wait for process to exit — important on Windows for file lock release
|
|
1963
|
+
const deadline = Date.now() + 5000;
|
|
1964
|
+
while (Date.now() < deadline && (0, daemon_1.isDaemonRunning)()) {
|
|
1965
|
+
await new Promise(r => setTimeout(r, 200));
|
|
1966
|
+
}
|
|
1967
|
+
if ((0, daemon_1.isDaemonRunning)()) {
|
|
1968
|
+
out.error('Daemon did not stop in time. Try: claude-tempo daemon stop');
|
|
1969
|
+
process.exit(1);
|
|
1970
|
+
}
|
|
1971
|
+
out.success('Daemon stopped');
|
|
1972
|
+
}
|
|
1973
|
+
// Build the detached updater script.
|
|
1974
|
+
// This is a self-contained Node.js script (no native module deps) that:
|
|
1975
|
+
// 1. Waits for the CLI process to exit (by PID)
|
|
1976
|
+
// 2. Runs npm install -g
|
|
1977
|
+
// 3. Verifies the install
|
|
1978
|
+
// 4. Restarts the daemon
|
|
1979
|
+
const cliPid = process.pid;
|
|
1980
|
+
const isWin = process.platform === 'win32';
|
|
1981
|
+
const updaterScript = `
|
|
1982
|
+
const { execFileSync } = require('child_process');
|
|
1983
|
+
const fs = require('fs');
|
|
1984
|
+
|
|
1985
|
+
const PID = ${cliPid};
|
|
1986
|
+
const INSTALL_SPEC = ${JSON.stringify(installSpec)};
|
|
1987
|
+
const TARGET = ${JSON.stringify(targetVersion)};
|
|
1988
|
+
const IS_WIN = ${isWin};
|
|
1989
|
+
const LOG_PATH = ${JSON.stringify((0, path_1.join)(config_1.CLAUDE_TEMPO_HOME, 'upgrade.log'))};
|
|
1990
|
+
|
|
1991
|
+
function log(msg) {
|
|
1992
|
+
const line = new Date().toISOString() + ' ' + msg;
|
|
1993
|
+
try { fs.appendFileSync(LOG_PATH, line + '\\n'); } catch {}
|
|
1994
|
+
console.log(msg);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function isPidAlive(pid) {
|
|
1998
|
+
try { process.kill(pid, 0); return true; }
|
|
1999
|
+
catch { return false; }
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
async function main() {
|
|
2003
|
+
// Wait for CLI process to exit (up to 10s)
|
|
2004
|
+
log('Waiting for CLI process (pid ' + PID + ') to exit...');
|
|
2005
|
+
const deadline = Date.now() + 10000;
|
|
2006
|
+
while (Date.now() < deadline && isPidAlive(PID)) {
|
|
2007
|
+
await new Promise(r => setTimeout(r, 300));
|
|
2008
|
+
}
|
|
2009
|
+
if (isPidAlive(PID)) {
|
|
2010
|
+
log('WARNING: CLI process still alive after 10s, proceeding anyway');
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Run npm install -g
|
|
2014
|
+
log('Installing ' + INSTALL_SPEC + '...');
|
|
2015
|
+
try {
|
|
2016
|
+
const npmCmd = IS_WIN ? 'npm.cmd' : 'npm';
|
|
2017
|
+
execFileSync(npmCmd, ['install', '-g', INSTALL_SPEC], {
|
|
2018
|
+
stdio: 'inherit',
|
|
2019
|
+
timeout: 120000,
|
|
2020
|
+
});
|
|
2021
|
+
log('Install completed');
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
log('Install FAILED: ' + err.message);
|
|
2024
|
+
log('Recovery: npm install -g ' + INSTALL_SPEC);
|
|
2025
|
+
process.exit(1);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// Verify installation
|
|
2029
|
+
try {
|
|
2030
|
+
const tempoCmd = IS_WIN ? 'claude-tempo.cmd' : 'claude-tempo';
|
|
2031
|
+
const ver = execFileSync(tempoCmd, ['--version'], {
|
|
2032
|
+
encoding: 'utf8',
|
|
2033
|
+
timeout: 10000,
|
|
2034
|
+
}).trim();
|
|
2035
|
+
log('Verified: ' + ver);
|
|
2036
|
+
} catch (err) {
|
|
2037
|
+
log('WARNING: Could not verify installation: ' + err.message);
|
|
2038
|
+
log('Recovery: npm install -g claude-tempo');
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Restart the daemon
|
|
2042
|
+
log('Restarting daemon...');
|
|
2043
|
+
try {
|
|
2044
|
+
const tempoCmd = IS_WIN ? 'claude-tempo.cmd' : 'claude-tempo';
|
|
2045
|
+
execFileSync(tempoCmd, ['daemon', 'start'], {
|
|
2046
|
+
stdio: 'inherit',
|
|
2047
|
+
timeout: 30000,
|
|
2048
|
+
});
|
|
2049
|
+
log('Daemon restarted');
|
|
2050
|
+
} catch (err) {
|
|
2051
|
+
log('WARNING: Daemon restart failed: ' + err.message);
|
|
2052
|
+
log('Run manually: claude-tempo daemon start');
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
log('Upgrade complete!');
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
main().catch(err => {
|
|
2059
|
+
log('Upgrade failed: ' + err.message);
|
|
2060
|
+
process.exit(1);
|
|
2061
|
+
});
|
|
2062
|
+
`.trim();
|
|
2063
|
+
// Clear previous upgrade log before spawning
|
|
2064
|
+
const logPath = (0, path_1.join)(config_1.CLAUDE_TEMPO_HOME, 'upgrade.log');
|
|
2065
|
+
try {
|
|
2066
|
+
(0, fs_1.writeFileSync)(logPath, '');
|
|
2067
|
+
}
|
|
2068
|
+
catch { /* ignore */ }
|
|
2069
|
+
// Spawn the updater as a detached child process
|
|
2070
|
+
out.log(`Spawning upgrade process for ${out.cyan(installSpec)}...`);
|
|
2071
|
+
const child = (0, child_process_1.spawn)(process.execPath, ['-e', updaterScript], {
|
|
2072
|
+
detached: true,
|
|
2073
|
+
stdio: 'ignore',
|
|
2074
|
+
env: { ...process.env },
|
|
2075
|
+
});
|
|
2076
|
+
child.unref();
|
|
2077
|
+
console.log();
|
|
2078
|
+
out.success('Upgrade started in background');
|
|
2079
|
+
out.log(` ${out.dim('Monitor progress: ')}`);
|
|
2080
|
+
if (isWin) {
|
|
2081
|
+
out.log(` ${out.dim('powershell Get-Content -Wait "' + logPath + '"')}`);
|
|
2082
|
+
}
|
|
2083
|
+
else {
|
|
2084
|
+
out.log(` ${out.dim('tail -f ' + logPath)}`);
|
|
2085
|
+
}
|
|
2086
|
+
out.log(` ${out.dim('If it fails, run manually: npm install -g ' + installSpec)}`);
|
|
2087
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -47,6 +47,8 @@ function parseArgs(argv) {
|
|
|
47
47
|
skipPreflight: false,
|
|
48
48
|
background: false,
|
|
49
49
|
keepMcp: false,
|
|
50
|
+
keepDaemon: false,
|
|
51
|
+
yes: false,
|
|
50
52
|
all: false,
|
|
51
53
|
project: false,
|
|
52
54
|
replace: false,
|
|
@@ -89,6 +91,12 @@ function parseArgs(argv) {
|
|
|
89
91
|
else if (arg === '--keep-mcp') {
|
|
90
92
|
result.keepMcp = true;
|
|
91
93
|
}
|
|
94
|
+
else if (arg === '--keep-daemon') {
|
|
95
|
+
result.keepDaemon = true;
|
|
96
|
+
}
|
|
97
|
+
else if (arg === '--yes' || arg === '-y') {
|
|
98
|
+
result.yes = true;
|
|
99
|
+
}
|
|
92
100
|
else if (arg === '--all') {
|
|
93
101
|
result.all = true;
|
|
94
102
|
}
|
|
@@ -208,6 +216,8 @@ async function main() {
|
|
|
208
216
|
ensemble: args.ensemble || args.positional[1] || process.env[config_1.ENV.ENSEMBLE],
|
|
209
217
|
all: args.all,
|
|
210
218
|
removeMcp: !args.keepMcp,
|
|
219
|
+
keepDaemon: args.keepDaemon,
|
|
220
|
+
yes: args.yes,
|
|
211
221
|
dir: args.dir,
|
|
212
222
|
...overrides,
|
|
213
223
|
});
|
|
@@ -297,6 +307,12 @@ async function main() {
|
|
|
297
307
|
await runTui({ config, ensemble });
|
|
298
308
|
break;
|
|
299
309
|
}
|
|
310
|
+
case 'upgrade':
|
|
311
|
+
await (0, commands_1.upgrade)({
|
|
312
|
+
version: args.positional[1], // e.g. "0.20.0" or "latest" or undefined
|
|
313
|
+
...overrides,
|
|
314
|
+
});
|
|
315
|
+
break;
|
|
300
316
|
case 'version':
|
|
301
317
|
(0, commands_1.version)();
|
|
302
318
|
break;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { EnsembleLineup } from './schema';
|
|
2
|
+
export interface LineupResolution {
|
|
3
|
+
path: string;
|
|
4
|
+
source: 'saved' | 'shipped' | 'file';
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a lineup name or file path to an absolute file path.
|
|
8
|
+
* Resolution order: saved lineups → shipped examples → direct file path → error.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveLineupPath(nameOrPath: string): LineupResolution;
|
|
2
11
|
/**
|
|
3
12
|
* Load and validate an ensemble lineup from a YAML file.
|
|
4
13
|
*/
|
package/dist/ensemble/loader.js
CHANGED
|
@@ -1,8 +1,58 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveLineupPath = resolveLineupPath;
|
|
3
4
|
exports.loadLineup = loadLineup;
|
|
4
5
|
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
5
7
|
const yaml_1 = require("yaml");
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
/** Walk up from a directory to find the nearest package.json. */
|
|
10
|
+
function findPackageRoot(dir) {
|
|
11
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'package.json')))
|
|
12
|
+
return dir;
|
|
13
|
+
const parent = (0, path_1.resolve)(dir, '..');
|
|
14
|
+
return parent === dir ? dir : findPackageRoot(parent);
|
|
15
|
+
}
|
|
16
|
+
/** Package root — works from both dist/ (production) and dist-test/ (tests). */
|
|
17
|
+
const PACKAGE_ROOT = findPackageRoot((0, path_1.resolve)(__dirname));
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a lineup name or file path to an absolute file path.
|
|
20
|
+
* Resolution order: saved lineups → shipped examples → direct file path → error.
|
|
21
|
+
*/
|
|
22
|
+
function resolveLineupPath(nameOrPath) {
|
|
23
|
+
// 1. Saved lineups (~/.claude-tempo/ensembles/)
|
|
24
|
+
const ensemblesDir = (0, path_1.join)(config_1.CLAUDE_TEMPO_HOME, 'ensembles');
|
|
25
|
+
const savedYaml = (0, path_1.join)(ensemblesDir, `${nameOrPath}.yaml`);
|
|
26
|
+
const savedYml = (0, path_1.join)(ensemblesDir, `${nameOrPath}.yml`);
|
|
27
|
+
if ((0, fs_1.existsSync)(savedYaml))
|
|
28
|
+
return { path: savedYaml, source: 'saved' };
|
|
29
|
+
if ((0, fs_1.existsSync)(savedYml))
|
|
30
|
+
return { path: savedYml, source: 'saved' };
|
|
31
|
+
// 2. Shipped examples (<package-root>/examples/ensembles/)
|
|
32
|
+
const shippedYaml = (0, path_1.join)(PACKAGE_ROOT, 'examples', 'ensembles', `${nameOrPath}.yaml`);
|
|
33
|
+
const shippedYml = (0, path_1.join)(PACKAGE_ROOT, 'examples', 'ensembles', `${nameOrPath}.yml`);
|
|
34
|
+
if ((0, fs_1.existsSync)(shippedYaml))
|
|
35
|
+
return { path: shippedYaml, source: 'shipped' };
|
|
36
|
+
if ((0, fs_1.existsSync)(shippedYml))
|
|
37
|
+
return { path: shippedYml, source: 'shipped' };
|
|
38
|
+
// 3. Direct file path
|
|
39
|
+
const resolved = (0, path_1.resolve)(nameOrPath);
|
|
40
|
+
if ((0, fs_1.existsSync)(resolved))
|
|
41
|
+
return { path: resolved, source: 'file' };
|
|
42
|
+
// 4. Error with suggestions
|
|
43
|
+
const suggestions = [];
|
|
44
|
+
const saved = (0, fs_1.existsSync)(ensemblesDir) ? (0, fs_1.readdirSync)(ensemblesDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml')).map(f => f.replace(/\.ya?ml$/, '')) : [];
|
|
45
|
+
if (saved.length)
|
|
46
|
+
suggestions.push(`Saved: ${saved.join(', ')}`);
|
|
47
|
+
const shippedDir = (0, path_1.join)(PACKAGE_ROOT, 'examples', 'ensembles');
|
|
48
|
+
if ((0, fs_1.existsSync)(shippedDir)) {
|
|
49
|
+
const shipped = (0, fs_1.readdirSync)(shippedDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml')).map(f => f.replace(/\.ya?ml$/, ''));
|
|
50
|
+
if (shipped.length)
|
|
51
|
+
suggestions.push(`Shipped: ${shipped.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
const msg = `Lineup "${nameOrPath}" not found as saved lineup, shipped example, or file path.`;
|
|
54
|
+
throw new Error(suggestions.length ? `${msg}\n ${suggestions.join('\n ')}` : msg);
|
|
55
|
+
}
|
|
6
56
|
/**
|
|
7
57
|
* Load and validate an ensemble lineup from a YAML file.
|
|
8
58
|
*/
|