claude-tempo 0.5.0 → 0.7.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/README.md +122 -4
- package/dist/activities/schedule-fire.d.ts +21 -0
- package/dist/activities/schedule-fire.js +128 -0
- package/dist/cli/commands.d.ts +6 -0
- package/dist/cli/commands.js +300 -8
- package/dist/cli.js +11 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +5 -0
- package/dist/ensemble/loader.d.ts +5 -0
- package/dist/ensemble/loader.js +60 -0
- package/dist/ensemble/saver.d.ts +17 -0
- package/dist/ensemble/saver.js +130 -0
- package/dist/ensemble/schema.d.ts +24 -0
- package/dist/ensemble/schema.js +3 -0
- package/dist/server.js +15 -3
- package/dist/tools/load-ensemble.d.ts +5 -0
- package/dist/tools/load-ensemble.js +282 -0
- package/dist/tools/recruit.js +9 -14
- package/dist/tools/save-ensemble.d.ts +4 -0
- package/dist/tools/save-ensemble.js +44 -0
- package/dist/tools/schedule.d.ts +4 -0
- package/dist/tools/schedule.js +148 -0
- package/dist/tools/schedules.d.ts +4 -0
- package/dist/tools/schedules.js +66 -0
- package/dist/tools/unschedule.d.ts +4 -0
- package/dist/tools/unschedule.js +30 -0
- package/dist/types.d.ts +22 -0
- package/dist/worker.js +9 -1
- package/dist/workflows/index.d.ts +2 -0
- package/dist/workflows/index.js +8 -0
- package/dist/workflows/scheduler-signals.d.ts +6 -0
- package/dist/workflows/scheduler-signals.js +10 -0
- package/dist/workflows/scheduler.d.ts +7 -0
- package/dist/workflows/scheduler.js +99 -0
- package/package.json +3 -2
- package/workflow-bundle.js +155 -5
package/README.md
CHANGED
|
@@ -91,16 +91,17 @@ claude-tempo <command> [options]
|
|
|
91
91
|
|
|
92
92
|
| Command | Description |
|
|
93
93
|
|---------|-------------|
|
|
94
|
-
| `up [ensemble]` | First-time setup: start Temporal, configure MCP, launch conductor |
|
|
94
|
+
| `up [ensemble]` | First-time setup: start Temporal, configure MCP, launch conductor. Use `--from` to load a blueprint. |
|
|
95
95
|
| `down` | Stop Temporal, terminate sessions, remove MCP config |
|
|
96
96
|
| `server` | Start the Temporal dev server and register search attributes |
|
|
97
|
-
| `conduct [ensemble]` | Start a conductor session (one per ensemble) |
|
|
97
|
+
| `conduct [ensemble]` | Start a conductor session (one per ensemble). Use `--resume` or `--replace` if one exists. |
|
|
98
98
|
| `start [ensemble]` | Start a player session |
|
|
99
99
|
| `status [ensemble]` | Show active sessions and Temporal health |
|
|
100
100
|
| `config` | Configure Temporal connection settings (interactive or `set`/`show`) |
|
|
101
101
|
| `stop [ensemble]` | Stop sessions (`-n <name>` for one, `--all` for everything) |
|
|
102
102
|
| `init` | Register claude-tempo MCP server globally (`--project` for per-directory) |
|
|
103
103
|
| `preflight` | Run environment checks |
|
|
104
|
+
| `ensemble <sub>` | Manage saved blueprints (`save`, `list`, `show`) |
|
|
104
105
|
| `help` | Show usage info |
|
|
105
106
|
|
|
106
107
|
### Global options
|
|
@@ -115,6 +116,9 @@ claude-tempo <command> [options]
|
|
|
115
116
|
--skip-preflight Skip preflight checks (start/conduct)
|
|
116
117
|
-d, --dir <path> Target directory (default: cwd)
|
|
117
118
|
--background Run Temporal in background (server only)
|
|
119
|
+
--from <file> Load an ensemble blueprint on startup (up only)
|
|
120
|
+
--resume Resume an existing conductor session (conduct only)
|
|
121
|
+
--replace Stop existing conductor and start fresh (conduct only)
|
|
118
122
|
```
|
|
119
123
|
|
|
120
124
|
### `claude-tempo up`
|
|
@@ -173,6 +177,9 @@ Ensemble: myband
|
|
|
173
177
|
bob
|
|
174
178
|
Working on the dashboard
|
|
175
179
|
/Users/me/projects/app feat/ui my-machine.local
|
|
180
|
+
|
|
181
|
+
1 active schedule
|
|
182
|
+
deploy-watch → ops | every 1h | next: 3:00:00 PM
|
|
176
183
|
```
|
|
177
184
|
|
|
178
185
|
### `claude-tempo preflight`
|
|
@@ -201,9 +208,119 @@ These tools are available inside Claude Code sessions connected to claude-tempo:
|
|
|
201
208
|
| `set_name` | Set a human-readable name for this session. |
|
|
202
209
|
| `set_part` | Describe what you're working on. Visible to others via `ensemble`. |
|
|
203
210
|
| `listen` | Manually check for pending messages. |
|
|
204
|
-
| `recruit` | Spawn a new Claude Code session in a directory.
|
|
211
|
+
| `recruit` | Spawn a new Claude Code session in a directory. Can recruit a conductor with `conductor: true`. |
|
|
205
212
|
| `report` | Send updates to the conductor. No-op if no conductor exists. |
|
|
206
213
|
| `terminate` | Terminate a player session by name. |
|
|
214
|
+
| `schedule` | Create a one-shot or recurring schedule to cue a player. |
|
|
215
|
+
| `unschedule` | Cancel a named schedule. |
|
|
216
|
+
| `schedules` | List all active schedules. |
|
|
217
|
+
| `save_ensemble` | Save the current ensemble as a YAML blueprint (conductor only). |
|
|
218
|
+
| `load_ensemble` | Load a blueprint to recruit players and create schedules. |
|
|
219
|
+
|
|
220
|
+
## Scheduling
|
|
221
|
+
|
|
222
|
+
Players can set up schedules to send messages on timers — useful for periodic checks, reminders, and recurring coordination.
|
|
223
|
+
|
|
224
|
+
Three tools are available:
|
|
225
|
+
- **`schedule`** — Create a named schedule (one-shot or recurring)
|
|
226
|
+
- **`unschedule`** — Remove a schedule by name
|
|
227
|
+
- **`schedules`** — List all active schedules
|
|
228
|
+
|
|
229
|
+
### Examples
|
|
230
|
+
|
|
231
|
+
Tell your session things like:
|
|
232
|
+
|
|
233
|
+
- *"Schedule a check every hour called 'deploy-watch' — cue ops to check deployment status"*
|
|
234
|
+
- *"Remind me in 30 minutes to review PR #42"*
|
|
235
|
+
- *"Every 5 minutes for the next hour, ping frontend to check their progress"*
|
|
236
|
+
- *"Set up a daily standup reminder at 9am UTC for the conductor"*
|
|
237
|
+
- *"Cancel the deploy-watch schedule"*
|
|
238
|
+
- *"Show me all active schedules"*
|
|
239
|
+
|
|
240
|
+
Schedules support one-shot delays, fixed times, and recurring intervals with optional bounds (max count or end time).
|
|
241
|
+
|
|
242
|
+
### How it works
|
|
243
|
+
|
|
244
|
+
- Scheduled messages arrive with a `[scheduled: name]` prefix so recipients can distinguish them from direct cues
|
|
245
|
+
- The `from` field is set to the schedule creator, so replies go to the right person
|
|
246
|
+
- If the target player is gone when a schedule fires, the creator is notified so they can re-recruit if needed. Falls back to notifying the conductor if the creator is also unavailable
|
|
247
|
+
- Messages include `isScheduled` metadata for dashboard integrations
|
|
248
|
+
- `claude-tempo status` shows active schedules alongside sessions
|
|
249
|
+
- A single durable scheduler workflow per ensemble manages all schedules using Temporal timers
|
|
250
|
+
|
|
251
|
+
## Ensemble Blueprints
|
|
252
|
+
|
|
253
|
+
Define reusable ensemble configurations as YAML files. A blueprint specifies which players to recruit, what instructions to give them, what schedules to create, and optionally which custom agent files to use.
|
|
254
|
+
|
|
255
|
+
### Example blueprint
|
|
256
|
+
|
|
257
|
+
```yaml
|
|
258
|
+
name: my-project
|
|
259
|
+
conductor:
|
|
260
|
+
instructions: "Coordinate the frontend and backend teams"
|
|
261
|
+
players:
|
|
262
|
+
- name: frontend
|
|
263
|
+
workDir: /repos/my-app
|
|
264
|
+
instructions: "Build the React dashboard in src/components"
|
|
265
|
+
- name: backend
|
|
266
|
+
workDir: /repos/my-api
|
|
267
|
+
instructions: "Implement the REST endpoints in src/routes"
|
|
268
|
+
- name: ops
|
|
269
|
+
workDir: /repos/infra
|
|
270
|
+
agent: agents/ops-agent.md
|
|
271
|
+
instructions: "Monitor deployments and run health checks"
|
|
272
|
+
schedules:
|
|
273
|
+
- name: status-check
|
|
274
|
+
message: "Report your current progress and any blockers"
|
|
275
|
+
target: all
|
|
276
|
+
every: 30m
|
|
277
|
+
- name: deploy-reminder
|
|
278
|
+
message: "Check if the staging deploy succeeded"
|
|
279
|
+
target: ops
|
|
280
|
+
delay: 10m
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Three ways to use blueprints
|
|
284
|
+
|
|
285
|
+
1. **From the CLI** — load a blueprint when starting an ensemble:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
claude-tempo up --from my-blueprint.yaml
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
2. **From inside a session** — use the `load_ensemble` tool:
|
|
292
|
+
|
|
293
|
+
*"Load the blueprint from ~/.claude-tempo/ensembles/my-project.yaml"*
|
|
294
|
+
|
|
295
|
+
3. **Save the current state** — snapshot a running ensemble as a blueprint (conductor only):
|
|
296
|
+
|
|
297
|
+
*"Save this ensemble as a blueprint called my-project"*
|
|
298
|
+
|
|
299
|
+
### Natural language examples
|
|
300
|
+
|
|
301
|
+
Tell your session things like:
|
|
302
|
+
|
|
303
|
+
- *"Load the my-project blueprint"*
|
|
304
|
+
- *"Save this ensemble as a blueprint"*
|
|
305
|
+
- *"Load the blueprint from /repos/configs/team.yaml"*
|
|
306
|
+
|
|
307
|
+
### Fan-out schedules
|
|
308
|
+
|
|
309
|
+
Use `target: "all"` in a schedule to deliver a message to every active player (excluding the conductor). This is useful for periodic status checks or broadcast announcements:
|
|
310
|
+
|
|
311
|
+
- *"Schedule a message every 30 minutes to all players asking for a progress update"*
|
|
312
|
+
|
|
313
|
+
### Custom agents
|
|
314
|
+
|
|
315
|
+
The `agent` field on a player can be a path to a `.md` file that will be used as the session's system prompt via `--system-prompt`. This lets you create specialized agents with domain-specific instructions:
|
|
316
|
+
|
|
317
|
+
```yaml
|
|
318
|
+
players:
|
|
319
|
+
- name: security-reviewer
|
|
320
|
+
workDir: /repos/my-app
|
|
321
|
+
agent: agents/security-review.md
|
|
322
|
+
instructions: "Review the latest PR for security issues"
|
|
323
|
+
```
|
|
207
324
|
|
|
208
325
|
## Conductors
|
|
209
326
|
|
|
@@ -274,11 +391,12 @@ Inside a session, try:
|
|
|
274
391
|
|
|
275
392
|
Sessions start with a random 8-character hex ID. Set a name at launch with `-n` or use `set_name` inside a session.
|
|
276
393
|
|
|
277
|
-
- Names are stored
|
|
394
|
+
- Names are stored in workflow metadata and discoverable via metadata queries. Search attributes are also set for Temporal UI visibility.
|
|
278
395
|
- Other players use names to send messages via `cue`
|
|
279
396
|
- `recruit` automatically tells new sessions to set their name
|
|
280
397
|
- Names must be unique within an ensemble
|
|
281
398
|
- Names must contain only letters, numbers, hyphens, and underscores
|
|
399
|
+
- The name "conductor" is reserved for conductor sessions
|
|
282
400
|
|
|
283
401
|
### Terminal support
|
|
284
402
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Client } from '@temporalio/client';
|
|
2
|
+
export interface FireScheduleInput {
|
|
3
|
+
ensemble: string;
|
|
4
|
+
scheduleName: string;
|
|
5
|
+
message: string;
|
|
6
|
+
target: string;
|
|
7
|
+
createdBy: string;
|
|
8
|
+
}
|
|
9
|
+
export interface FireScheduleResult {
|
|
10
|
+
success: boolean;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Activity interface — used by proxyActivities in the scheduler workflow. */
|
|
14
|
+
export interface ScheduleActivities {
|
|
15
|
+
fireSchedule(input: FireScheduleInput): Promise<FireScheduleResult>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create the schedule-fire activity bound to a Temporal client.
|
|
19
|
+
* The returned object is registered with the worker as activities.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createScheduleActivities(client: Client): ScheduleActivities;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createScheduleActivities = createScheduleActivities;
|
|
4
|
+
/**
|
|
5
|
+
* Create the schedule-fire activity bound to a Temporal client.
|
|
6
|
+
* The returned object is registered with the worker as activities.
|
|
7
|
+
*/
|
|
8
|
+
function createScheduleActivities(client) {
|
|
9
|
+
return {
|
|
10
|
+
async fireSchedule(input) {
|
|
11
|
+
const { ensemble, scheduleName, message, target, createdBy } = input;
|
|
12
|
+
try {
|
|
13
|
+
const text = `[scheduled: ${scheduleName}] ${message}`;
|
|
14
|
+
// Handle target "all" — deliver to every active player except the conductor
|
|
15
|
+
if (target === 'all') {
|
|
16
|
+
const query = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"`;
|
|
17
|
+
const failures = [];
|
|
18
|
+
let delivered = 0;
|
|
19
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
20
|
+
try {
|
|
21
|
+
const handle = client.workflow.getHandle(wf.workflowId);
|
|
22
|
+
const metadata = await handle.query('getMetadata');
|
|
23
|
+
if (metadata.ensemble !== ensemble)
|
|
24
|
+
continue;
|
|
25
|
+
if (metadata.isConductor)
|
|
26
|
+
continue; // skip conductor
|
|
27
|
+
await handle.signal('receiveMessage', {
|
|
28
|
+
from: createdBy,
|
|
29
|
+
text,
|
|
30
|
+
isScheduled: true,
|
|
31
|
+
scheduleName,
|
|
32
|
+
});
|
|
33
|
+
delivered++;
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37
|
+
failures.push(`${wf.workflowId}: ${msg}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (delivered === 0 && failures.length === 0) {
|
|
41
|
+
await notifyFailure(client, ensemble, createdBy, scheduleName, target, 'No active players found in the ensemble.');
|
|
42
|
+
return { success: false, error: 'No active players found' };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
success: failures.length === 0,
|
|
46
|
+
error: failures.length > 0 ? `Delivered to ${delivered} players, ${failures.length} failed: ${failures.join('; ')}` : undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Resolve single target player by querying running session workflows
|
|
50
|
+
const handle = await resolveSession(client, ensemble, target);
|
|
51
|
+
if (!handle) {
|
|
52
|
+
// Notify the creator (or conductor as fallback) about the failure
|
|
53
|
+
await notifyFailure(client, ensemble, createdBy, scheduleName, target, `Player "${target}" not found — session may have been terminated.`);
|
|
54
|
+
return { success: false, error: `No active session found for "${target}"` };
|
|
55
|
+
}
|
|
56
|
+
// Send cue signal with from set to the original creator's name
|
|
57
|
+
await handle.signal('receiveMessage', {
|
|
58
|
+
from: createdBy,
|
|
59
|
+
text,
|
|
60
|
+
isScheduled: true,
|
|
61
|
+
scheduleName,
|
|
62
|
+
});
|
|
63
|
+
return { success: true };
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
67
|
+
return { success: false, error: errorMsg };
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Notify the schedule creator (or conductor as fallback) that delivery failed.
|
|
74
|
+
* This lets the creator's AI session decide whether to re-recruit the target.
|
|
75
|
+
*/
|
|
76
|
+
async function notifyFailure(client, ensemble, createdBy, scheduleName, target, reason) {
|
|
77
|
+
const failureText = `[scheduled: ${scheduleName}] Delivery to "${target}" failed — ${reason}`;
|
|
78
|
+
// Try the creator first
|
|
79
|
+
const creatorHandle = await resolveSession(client, ensemble, createdBy);
|
|
80
|
+
if (creatorHandle) {
|
|
81
|
+
try {
|
|
82
|
+
await creatorHandle.signal('receiveMessage', {
|
|
83
|
+
from: 'scheduler',
|
|
84
|
+
text: failureText,
|
|
85
|
+
isScheduled: true,
|
|
86
|
+
scheduleName,
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// creator signal failed, fall through to conductor
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Fallback: notify the conductor
|
|
95
|
+
try {
|
|
96
|
+
const conductorId = `claude-session-${ensemble}-conductor`;
|
|
97
|
+
const conductorHandle = client.workflow.getHandle(conductorId);
|
|
98
|
+
await conductorHandle.signal('receiveMessage', {
|
|
99
|
+
from: 'scheduler',
|
|
100
|
+
text: failureText,
|
|
101
|
+
isScheduled: true,
|
|
102
|
+
scheduleName,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Nobody available to notify — logged by the workflow
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a session by player name — mirrors src/tools/resolve.ts logic.
|
|
111
|
+
* We duplicate here because activities run in Node.js and need their own copy.
|
|
112
|
+
*/
|
|
113
|
+
async function resolveSession(client, ensemble, playerName) {
|
|
114
|
+
const query = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"`;
|
|
115
|
+
for await (const wf of client.workflow.list({ query })) {
|
|
116
|
+
try {
|
|
117
|
+
const handle = client.workflow.getHandle(wf.workflowId);
|
|
118
|
+
const metadata = await handle.query('getMetadata');
|
|
119
|
+
if (metadata.ensemble === ensemble && metadata.playerId === playerName) {
|
|
120
|
+
return handle;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Workflow may have just completed — skip
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare function server(opts: ServerOpts): Promise<void>;
|
|
|
27
27
|
interface UpOpts extends CliOverrides {
|
|
28
28
|
ensemble: string;
|
|
29
29
|
name?: string;
|
|
30
|
+
from?: string;
|
|
30
31
|
agent: AgentType;
|
|
31
32
|
}
|
|
32
33
|
export declare function up(opts: UpOpts): Promise<void>;
|
|
@@ -44,6 +45,11 @@ interface StopOpts extends CliOverrides {
|
|
|
44
45
|
all?: boolean;
|
|
45
46
|
}
|
|
46
47
|
export declare function stop(opts: StopOpts): Promise<void>;
|
|
48
|
+
interface EnsembleCommandOpts extends CliOverrides {
|
|
49
|
+
subcommand?: string;
|
|
50
|
+
name?: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function ensembleCommand(opts: EnsembleCommandOpts): Promise<void>;
|
|
47
53
|
export declare function help(): void;
|
|
48
54
|
export declare function version(): void;
|
|
49
55
|
export {};
|