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 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. Opens a new terminal window. |
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 as Temporal search attributes (`ClaudeTempoPlayerId`)
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
+ }
@@ -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 {};