claude-tempo 0.16.3 → 0.17.1

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 CHANGED
@@ -32,10 +32,14 @@ src/
32
32
  │ ├── index.ts # Workflow exports (re-exports for worker bundle)
33
33
  │ ├── session.ts # claude-session workflow
34
34
  │ ├── scheduler.ts # durable scheduler workflow (one per ensemble)
35
+ │ ├── maestro.ts # Maestro ensemble hub workflow (one per ensemble)
36
+ │ ├── maestro-signals.ts # Maestro signal/query/update type definitions
35
37
  │ ├── scheduler-signals.ts # Scheduler signal/query type definitions
36
38
  │ └── signals.ts # Session signal/query type definitions
37
39
  ├── activities/
38
40
  │ ├── outbox.ts # Outbox delivery activities (cue, report, stop, recruit, encore)
41
+ │ ├── maestro.ts # Maestro activities (refreshEnsembleState, relayCommandToConductor, fetchConductorHistory)
42
+ │ ├── resolve.ts # Session resolver shared by outbox + schedule-fire activities
39
43
  │ └── schedule-fire.ts # Schedule fire activity
40
44
  ├── ensemble/
41
45
  │ ├── schema.ts # Lineup type definitions
@@ -125,6 +129,7 @@ npm test
125
129
  - **Lineup**: A YAML file defining an ensemble configuration — which players to recruit, their types, working directories, and optional startup messages. Load via `load_lineup` to bootstrap a full ensemble in one step; save via `save_lineup` to snapshot a running ensemble's state for later reuse.
126
130
  - **Quality Gate**: A named checklist of criteria a conductor tracks to verify a task is complete. Created via `quality_gate` (conductor only), evaluated via `evaluate_gate`, and listed via `gates`. Each criterion has a `pending` → `passed` | `failed` status; the gate's aggregate status is derived automatically (all passed → `passed`, any failed → `failed`, else `open`). Gates are stored in the conductor workflow and survive `continueAsNew`.
127
131
  - **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).
132
+ - **Maestro**: A durable `claudeMaestroWorkflow` (one per ensemble, ID: `claude-maestro-{ensemble}`) that acts as an ensemble state aggregator for external integrations. It periodically polls all session metadata to maintain a player snapshot and ring-buffer event log, and accepts commands via the `maestroSendCommand` update for relay to the conductor. The Maestro dashboard ([vinceblank/maestro](https://github.com/vinceblank/maestro)) connects to this workflow to display live ensemble state. Implemented in `src/workflows/maestro.ts` with activities in `src/activities/maestro.ts`.
128
133
  - **Wire protocol**: All Temporal signal, query, update, and workflow names are documented in [`docs/WIRE-PROTOCOL.md`](docs/WIRE-PROTOCOL.md). These names are stable as of v0.10 — renaming or removing any is a breaking change requiring a major version bump.
129
134
 
130
135
  ## Dashboard
@@ -0,0 +1,31 @@
1
+ import { Client } from '@temporalio/client';
2
+ import { HistoryEntry, MaestroPlayerInfo } from '../types';
3
+ export interface RelayCommandInput {
4
+ ensemble: string;
5
+ text: string;
6
+ source: string;
7
+ replyTo?: string;
8
+ }
9
+ export interface RelayCommandResult {
10
+ success: boolean;
11
+ error?: string;
12
+ }
13
+ export interface FetchConductorHistoryInput {
14
+ ensemble: string;
15
+ }
16
+ export interface FetchConductorHistoryResult {
17
+ success: boolean;
18
+ history: HistoryEntry[];
19
+ error?: string;
20
+ }
21
+ /** Activity interface — used by proxyActivities in the Maestro workflow. */
22
+ export interface MaestroActivities {
23
+ refreshEnsembleState(ensemble: string): Promise<MaestroPlayerInfo[]>;
24
+ fetchConductorHistory(input: FetchConductorHistoryInput): Promise<FetchConductorHistoryResult>;
25
+ relayCommandToConductor(input: RelayCommandInput): Promise<RelayCommandResult>;
26
+ }
27
+ /**
28
+ * Create the Maestro activity implementations bound to a Temporal client.
29
+ * Registered with the shared worker.
30
+ */
31
+ export declare function createMaestroActivities(client: Client): MaestroActivities;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMaestroActivities = createMaestroActivities;
4
+ const activity_1 = require("@temporalio/activity");
5
+ const config_1 = require("../config");
6
+ const resolve_1 = require("./resolve");
7
+ const log = (...args) => console.error('[claude-tempo:maestro]', ...args);
8
+ /**
9
+ * Create the Maestro activity implementations bound to a Temporal client.
10
+ * Registered with the shared worker.
11
+ */
12
+ function createMaestroActivities(client) {
13
+ return {
14
+ async refreshEnsembleState(ensemble) {
15
+ try {
16
+ const sessions = await (0, resolve_1.scanEnsembleSessions)(client, ensemble);
17
+ return sessions.map((s) => ({
18
+ playerId: s.playerId,
19
+ part: s.part,
20
+ hostname: s.hostname,
21
+ workDir: s.workDir,
22
+ gitRoot: s.gitRoot,
23
+ gitBranch: s.gitBranch,
24
+ isConductor: s.isConductor,
25
+ agentType: s.agentType,
26
+ playerType: s.playerType,
27
+ status: s.status,
28
+ }));
29
+ }
30
+ catch (err) {
31
+ log('refreshEnsembleState failed:', err);
32
+ throw activity_1.ApplicationFailure.nonRetryable(`Failed to scan ensemble: ${err instanceof Error ? err.message : String(err)}`);
33
+ }
34
+ },
35
+ async fetchConductorHistory(input) {
36
+ try {
37
+ const wfId = (0, config_1.conductorWorkflowId)(input.ensemble);
38
+ const handle = client.workflow.getHandle(wfId);
39
+ const history = await handle.query('history');
40
+ return { success: true, history };
41
+ }
42
+ catch (err) {
43
+ // ContinueAsNew transient errors and missing conductor are soft failures
44
+ const msg = err instanceof Error ? err.message : String(err);
45
+ log('fetchConductorHistory failed (soft):', msg);
46
+ return { success: false, history: [], error: msg };
47
+ }
48
+ },
49
+ async relayCommandToConductor(input) {
50
+ try {
51
+ const wfId = (0, config_1.conductorWorkflowId)(input.ensemble);
52
+ const handle = client.workflow.getHandle(wfId);
53
+ await handle.signal('command', {
54
+ text: input.text,
55
+ source: input.source,
56
+ ...(input.replyTo ? { replyTo: input.replyTo } : {}),
57
+ });
58
+ return { success: true };
59
+ }
60
+ catch (err) {
61
+ const msg = err instanceof Error ? err.message : String(err);
62
+ log('relayCommandToConductor failed:', msg);
63
+ return { success: false, error: msg };
64
+ }
65
+ },
66
+ };
67
+ }
@@ -8,3 +8,23 @@ import { Client, WorkflowHandle } from '@temporalio/client';
8
8
  * Shared by activity files (outbox, schedule-fire) and the tools layer.
9
9
  */
10
10
  export declare function resolveSession(client: Client, ensemble: string, playerName: string): Promise<WorkflowHandle | null>;
11
+ /** Info returned for each session by scanEnsembleSessions. */
12
+ export interface EnsembleSessionInfo {
13
+ workflowId: string;
14
+ playerId: string;
15
+ part: string;
16
+ hostname: string;
17
+ workDir: string;
18
+ gitRoot?: string;
19
+ gitBranch?: string;
20
+ isConductor: boolean;
21
+ agentType: string;
22
+ playerType?: string;
23
+ status?: string;
24
+ }
25
+ /**
26
+ * Scan all running session workflows in an ensemble.
27
+ * Returns metadata + part for each session. Shared by the ensemble MCP tool
28
+ * and the Maestro refresh activity.
29
+ */
30
+ export declare function scanEnsembleSessions(client: Client, ensemble: string): Promise<EnsembleSessionInfo[]>;
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveSession = resolveSession;
4
+ exports.scanEnsembleSessions = scanEnsembleSessions;
5
+ /** Shared query for listing running session workflows. */
6
+ const SESSION_LIST_QUERY = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"`;
4
7
  /**
5
8
  * Resolve a session by player name.
6
9
  * Lists all running session workflows and queries each for metadata.
@@ -10,8 +13,7 @@ exports.resolveSession = resolveSession;
10
13
  * Shared by activity files (outbox, schedule-fire) and the tools layer.
11
14
  */
12
15
  async function resolveSession(client, ensemble, playerName) {
13
- const query = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"`;
14
- for await (const wf of client.workflow.list({ query })) {
16
+ for await (const wf of client.workflow.list({ query: SESSION_LIST_QUERY })) {
15
17
  try {
16
18
  const handle = client.workflow.getHandle(wf.workflowId);
17
19
  const metadata = await handle.query('getMetadata');
@@ -25,3 +27,37 @@ async function resolveSession(client, ensemble, playerName) {
25
27
  }
26
28
  return null;
27
29
  }
30
+ /**
31
+ * Scan all running session workflows in an ensemble.
32
+ * Returns metadata + part for each session. Shared by the ensemble MCP tool
33
+ * and the Maestro refresh activity.
34
+ */
35
+ async function scanEnsembleSessions(client, ensemble) {
36
+ const sessions = [];
37
+ for await (const workflow of client.workflow.list({ query: SESSION_LIST_QUERY })) {
38
+ try {
39
+ const handle = client.workflow.getHandle(workflow.workflowId);
40
+ const metadata = await handle.query('getMetadata');
41
+ if (metadata.ensemble !== ensemble)
42
+ continue;
43
+ const part = await handle.query('getPart');
44
+ sessions.push({
45
+ workflowId: workflow.workflowId,
46
+ playerId: metadata.playerId,
47
+ part,
48
+ hostname: metadata.hostname,
49
+ workDir: metadata.workDir,
50
+ gitRoot: metadata.gitRoot,
51
+ gitBranch: metadata.gitBranch,
52
+ isConductor: metadata.isConductor,
53
+ agentType: metadata.agentType || 'claude',
54
+ playerType: metadata.playerType,
55
+ status: metadata.status,
56
+ });
57
+ }
58
+ catch {
59
+ // Workflow may have just completed — skip it
60
+ }
61
+ }
62
+ return sessions;
63
+ }
@@ -74,6 +74,27 @@ function formatDurationMs(ms) {
74
74
  return `${ms / 60_000}m`;
75
75
  return `${ms / 1000}s`;
76
76
  }
77
+ /**
78
+ * Ensure the Maestro workflow is running for the given ensemble.
79
+ * Idempotent — uses USE_EXISTING conflict policy.
80
+ */
81
+ async function ensureMaestroWorkflow(client, config, ensemble) {
82
+ const wfId = (0, config_1.maestroWorkflowId)(ensemble);
83
+ try {
84
+ await client.workflow.start('claudeMaestroWorkflow', {
85
+ workflowId: wfId,
86
+ taskQueue: config.taskQueue,
87
+ args: [{ ensemble }],
88
+ workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
89
+ searchAttributes: {
90
+ ClaudeTempoEnsemble: [ensemble],
91
+ },
92
+ });
93
+ }
94
+ catch {
95
+ // Maestro is non-critical — log but don't fail
96
+ }
97
+ }
77
98
  async function start(opts) {
78
99
  const config = (0, config_1.getConfig)(opts);
79
100
  const workDir = opts.dir || process.cwd();
@@ -190,6 +211,18 @@ async function start(opts) {
190
211
  }
191
212
  out.log(` Ensemble: ${opts.ensemble}`);
192
213
  out.log(` Directory: ${workDir}`);
214
+ // Start Maestro workflow when launching a conductor
215
+ if (opts.conductor) {
216
+ try {
217
+ const connection = await (0, connection_1.createTemporalConnection)(config);
218
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
219
+ await ensureMaestroWorkflow(client, config, opts.ensemble);
220
+ await connection.close();
221
+ }
222
+ catch {
223
+ // Maestro is non-critical
224
+ }
225
+ }
193
226
  out.log(`\nCheck status: ${out.dim('claude-tempo status ' + opts.ensemble)}`);
194
227
  }
195
228
  async function status(opts) {
@@ -719,6 +752,8 @@ async function up(opts) {
719
752
  }
720
753
  else {
721
754
  out.check('Conductor registered', true);
755
+ // Ensure Maestro workflow is running
756
+ await ensureMaestroWorkflow(client, config, opts.ensemble);
722
757
  // Send conductor instructions if provided
723
758
  if (lineup.conductor?.instructions) {
724
759
  try {
package/dist/config.d.ts CHANGED
@@ -94,3 +94,5 @@ export declare function sessionWorkflowId(ensemble: string, playerId: string): s
94
94
  export declare function conductorWorkflowId(ensemble: string): string;
95
95
  /** Build a workflow ID for the scheduler: claude-scheduler-{ensemble} */
96
96
  export declare function schedulerWorkflowId(ensemble: string): string;
97
+ /** Build a workflow ID for the Maestro: claude-maestro-{ensemble} */
98
+ export declare function maestroWorkflowId(ensemble: string): string;
package/dist/config.js CHANGED
@@ -11,6 +11,7 @@ exports.hostTaskQueue = hostTaskQueue;
11
11
  exports.sessionWorkflowId = sessionWorkflowId;
12
12
  exports.conductorWorkflowId = conductorWorkflowId;
13
13
  exports.schedulerWorkflowId = schedulerWorkflowId;
14
+ exports.maestroWorkflowId = maestroWorkflowId;
14
15
  const fs_1 = require("fs");
15
16
  const path_1 = require("path");
16
17
  const os_1 = require("os");
@@ -263,3 +264,7 @@ function conductorWorkflowId(ensemble) {
263
264
  function schedulerWorkflowId(ensemble) {
264
265
  return `claude-scheduler-${ensemble}`;
265
266
  }
267
+ /** Build a workflow ID for the Maestro: claude-maestro-{ensemble} */
268
+ function maestroWorkflowId(ensemble) {
269
+ return `claude-maestro-${ensemble}`;
270
+ }
package/dist/server.js CHANGED
@@ -255,7 +255,13 @@ async function main() {
255
255
  `Use \`ensemble\` to see who else is active. ` +
256
256
  `Use \`cue\` to reply directly to the player who messaged you, or to ask others for help. ` +
257
257
  `Use \`recruit\` if you need a session in a directory where none exists. ` +
258
- `Use \`report\` to notify the conductor of task completion, blockers, or questions — always report when you finish a recruited task.`;
258
+ `Use \`report\` to notify the conductor of task completion, blockers, or questions — always report when you finish a recruited task.` +
259
+ (isConductor
260
+ ? `\n\nOperational rules:\n` +
261
+ `- Before assigning parallel work on different branches, provision git worktrees via the \`worktree\` tool so each player has an isolated checkout.\n` +
262
+ `- No player should switch branches without your approval — if a player needs a different branch, provision a worktree for them.\n` +
263
+ `- Before shipping, verify the branch diff scope matches the assigned task (no unrelated changes).`
264
+ : `\n\nDo not switch git branches without the conductor's approval. If no conductor exists, broadcast your intent to the ensemble first. Prefer using the \`worktree\` tool for branch isolation.`);
259
265
  const mcpServer = new mcp_js_1.McpServer({
260
266
  name: 'claude-tempo',
261
267
  version: PKG_VERSION,
@@ -278,7 +284,7 @@ async function main() {
278
284
  (0, unschedule_1.registerUnscheduleTool)(mcpServer, client, config);
279
285
  (0, schedules_1.registerSchedulesTool)(mcpServer, client, config);
280
286
  (0, save_lineup_1.registerSaveLineupTool)(mcpServer, client, config, getPlayerId, isConductor);
281
- (0, load_lineup_1.registerLoadLineupTool)(mcpServer, client, config, getPlayerId, isBridgeMode ? 'copilot' : 'claude');
287
+ (0, load_lineup_1.registerLoadLineupTool)(mcpServer, client, config, getPlayerId, isBridgeMode ? 'copilot' : 'claude', handle, setPlayerId, isConductor);
282
288
  (0, agent_types_1.registerAgentTypesTool)(mcpServer);
283
289
  (0, who_am_i_1.registerWhoAmITool)(mcpServer, handle, getPlayerId);
284
290
  (0, broadcast_1.registerBroadcastTool)(mcpServer, client, config, getPlayerId, handle);
@@ -36,53 +36,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.registerEnsembleTool = registerEnsembleTool;
37
37
  const zod_1 = require("zod");
38
38
  const os = __importStar(require("os"));
39
+ const resolve_1 = require("../activities/resolve");
39
40
  const helpers_1 = require("./helpers");
40
41
  function registerEnsembleTool(server, client, config, getPlayerId, ownWorkflowId) {
41
42
  (0, helpers_1.defineTool)(server, 'ensemble', `Discover active Claude Code sessions in the "${config.ensemble}" ensemble. Returns player IDs, descriptions, and metadata.`, {
42
43
  scope: zod_1.z.string().optional().describe('Filter scope: "machine" (same hostname), "repo" (same git root), "all" (default). All scopes are within the current ensemble.'),
43
44
  }, async (args) => {
44
45
  const scope = (args.scope ?? 'all');
45
- // List all running session workflows, then filter by ensemble using
46
- // in-memory metadata queries. This avoids depending on custom search
47
- // attributes which are eventually consistent and may be missing/stale.
48
- const query = `WorkflowType = "claudeSessionWorkflow" AND ExecutionStatus = "Running"`;
49
- const players = [];
46
+ let sessions;
50
47
  try {
51
- for await (const workflow of client.workflow.list({ query })) {
52
- try {
53
- const handle = client.workflow.getHandle(workflow.workflowId);
54
- const metadata = await handle.query('getMetadata');
55
- // Filter by ensemble
56
- if (metadata.ensemble !== config.ensemble)
57
- continue;
58
- // Filter by scope
59
- if (scope === 'machine' && metadata.hostname !== os.hostname())
60
- continue;
61
- if (scope === 'repo') {
62
- const ownHandle = client.workflow.getHandle(ownWorkflowId);
63
- const ownMeta = await ownHandle.query('getMetadata');
64
- if (metadata.gitRoot !== ownMeta.gitRoot)
65
- continue;
66
- }
67
- const part = await handle.query('getPart');
68
- players.push({
69
- playerId: metadata.playerId,
70
- part,
71
- hostname: metadata.hostname,
72
- workDir: metadata.workDir,
73
- gitRoot: metadata.gitRoot,
74
- gitBranch: metadata.gitBranch,
75
- isConductor: metadata.isConductor,
76
- agentType: metadata.agentType || 'claude',
77
- playerType: metadata.playerType,
78
- status: metadata.status,
79
- isYou: metadata.playerId === getPlayerId(),
80
- });
81
- }
82
- catch {
83
- // Workflow may have just completed — skip it
84
- }
85
- }
48
+ sessions = await (0, resolve_1.scanEnsembleSessions)(client, config.ensemble);
86
49
  }
87
50
  catch (err) {
88
51
  return {
@@ -90,6 +53,30 @@ function registerEnsembleTool(server, client, config, getPlayerId, ownWorkflowId
90
53
  isError: true,
91
54
  };
92
55
  }
56
+ // Apply scope filters
57
+ let ownGitRoot;
58
+ if (scope === 'repo') {
59
+ try {
60
+ const ownHandle = client.workflow.getHandle(ownWorkflowId);
61
+ const ownMeta = await ownHandle.query('getMetadata');
62
+ ownGitRoot = ownMeta.gitRoot;
63
+ }
64
+ catch {
65
+ // Can't determine own git root — skip repo filtering
66
+ }
67
+ }
68
+ const players = sessions
69
+ .filter((s) => {
70
+ if (scope === 'machine' && s.hostname !== os.hostname())
71
+ return false;
72
+ if (scope === 'repo' && ownGitRoot && s.gitRoot !== ownGitRoot)
73
+ return false;
74
+ return true;
75
+ })
76
+ .map((s) => ({
77
+ ...s,
78
+ isYou: s.playerId === getPlayerId(),
79
+ }));
93
80
  if (players.length === 0) {
94
81
  return {
95
82
  content: [{ type: 'text', text: 'No active sessions found.' }],
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { Client } from '@temporalio/client';
2
+ import { Client, WorkflowHandle } from '@temporalio/client';
3
3
  import { Config } from '../config';
4
4
  import { AgentType } from '../types';
5
- export declare function registerLoadLineupTool(server: McpServer, client: Client, config: Config, getPlayerId: () => string, ownAgentType?: AgentType): void;
5
+ export declare function registerLoadLineupTool(server: McpServer, client: Client, config: Config, getPlayerId: () => string, ownAgentType?: AgentType, handle?: WorkflowHandle, setPlayerId?: (id: string) => void, isConductor?: boolean): void;
@@ -19,7 +19,7 @@ const log = (...args) => console.error('[claude-tempo:load-lineup]', ...args);
19
19
  function sleep(ms) {
20
20
  return new Promise((resolve) => setTimeout(resolve, ms));
21
21
  }
22
- function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentType = 'claude') {
22
+ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentType = 'claude', handle, setPlayerId, isConductor) {
23
23
  (0, helpers_1.defineTool)(server, 'load_lineup', 'Load an ensemble lineup — recruits players and creates schedules.', {
24
24
  name: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).optional().describe('Name of a saved lineup (from ~/.claude-tempo/ensembles/)'),
25
25
  path: zod_1.z.string().max(validation_1.PATH_MAX).optional().describe('Explicit file path to a lineup YAML file'),
@@ -76,6 +76,64 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
76
76
  const lineup = (0, agent_types_1.loadAndResolveLineup)(filePath);
77
77
  const recruited = [];
78
78
  const failed = [];
79
+ const conductorActions = [];
80
+ // Apply conductor section if present and this session is the conductor
81
+ if (lineup.conductor && isConductor && handle) {
82
+ // Apply conductor name
83
+ if (lineup.conductor.name && lineup.conductor.name !== getPlayerId()) {
84
+ try {
85
+ // Check if the name is already taken
86
+ const existing = await (0, resolve_1.resolveSession)(client, config.ensemble, lineup.conductor.name);
87
+ if (existing && existing.workflowId !== handle.workflowId) {
88
+ failed.push(`conductor name "${lineup.conductor.name}": already taken by another session`);
89
+ }
90
+ else {
91
+ await handle.signal('setName', lineup.conductor.name);
92
+ if (setPlayerId)
93
+ setPlayerId(lineup.conductor.name);
94
+ conductorActions.push(`name → ${lineup.conductor.name}`);
95
+ log(`Conductor name set to "${lineup.conductor.name}"`);
96
+ }
97
+ }
98
+ catch (err) {
99
+ failed.push(`conductor name: ${err}`);
100
+ }
101
+ }
102
+ // Apply conductor type (update metadata)
103
+ if (lineup.conductor.type) {
104
+ try {
105
+ const typeInfo = (0, agent_types_1.resolveAgentType)(lineup.conductor.type);
106
+ if (typeInfo) {
107
+ await handle.signal('updateMetadata', {
108
+ playerType: typeInfo.name,
109
+ playerTypeDescription: typeInfo.description || '',
110
+ });
111
+ conductorActions.push(`type → ${typeInfo.name}`);
112
+ log(`Conductor type set to "${typeInfo.name}"`);
113
+ }
114
+ else {
115
+ failed.push(`conductor type "${lineup.conductor.type}": agent type not found`);
116
+ }
117
+ }
118
+ catch (err) {
119
+ failed.push(`conductor type: ${err}`);
120
+ }
121
+ }
122
+ // Send conductor instructions
123
+ if (lineup.conductor.instructions) {
124
+ try {
125
+ await handle.signal('receiveMessage', {
126
+ from: 'lineup',
127
+ text: lineup.conductor.instructions,
128
+ });
129
+ conductorActions.push('instructions delivered');
130
+ log('Conductor instructions delivered');
131
+ }
132
+ catch (err) {
133
+ failed.push(`conductor instructions: ${err}`);
134
+ }
135
+ }
136
+ }
79
137
  // Recruit players sequentially
80
138
  for (const player of lineup.players) {
81
139
  const playerName = player.name;
@@ -296,6 +354,9 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
296
354
  }
297
355
  // Build summary
298
356
  const lines = [`Loaded lineup **${lineup.name}**.`];
357
+ if (conductorActions.length > 0) {
358
+ lines.push(`Conductor: ${conductorActions.join(', ')}`);
359
+ }
299
360
  if (recruited.length > 0) {
300
361
  lines.push(`Recruited: ${recruited.join(', ')}`);
301
362
  }
package/dist/types.d.ts CHANGED
@@ -198,4 +198,47 @@ export interface ScheduleEntry {
198
198
  /** IANA timezone for cron evaluation (e.g., "America/New_York"). Defaults to UTC. */
199
199
  timezone?: string;
200
200
  }
201
+ /** Snapshot of a player as seen by the Maestro workflow. */
202
+ export interface MaestroPlayerInfo {
203
+ playerId: string;
204
+ part: string;
205
+ hostname: string;
206
+ workDir: string;
207
+ gitRoot?: string;
208
+ gitBranch?: string;
209
+ isConductor: boolean;
210
+ agentType: string;
211
+ playerType?: string;
212
+ status?: string;
213
+ }
214
+ /** An event generated by diffing consecutive Maestro snapshots. */
215
+ export interface MaestroEvent {
216
+ type: 'player_joined' | 'player_left' | 'status_changed' | 'part_changed';
217
+ playerId: string;
218
+ timestamp: string;
219
+ oldValue?: string;
220
+ newValue?: string;
221
+ }
222
+ /** A command queued via the maestroSendCommand update, awaiting relay. */
223
+ export interface MaestroPendingCommand {
224
+ id: string;
225
+ text: string;
226
+ source: string;
227
+ replyTo?: string;
228
+ createdAt: string;
229
+ status: 'pending' | 'delivered' | 'failed';
230
+ error?: string;
231
+ }
232
+ /** Input for the Maestro workflow. */
233
+ export interface MaestroInput {
234
+ ensemble: string;
235
+ /** Restored from continue-as-new. */
236
+ players?: MaestroPlayerInfo[];
237
+ /** Restored from continue-as-new (ring buffer, max 200). */
238
+ events?: MaestroEvent[];
239
+ /** Restored from continue-as-new. */
240
+ pendingCommands?: MaestroPendingCommand[];
241
+ /** Refresh interval in milliseconds (default 10000). Lowered in tests. */
242
+ pollIntervalMs?: number;
243
+ }
201
244
  export {};
package/dist/worker.js CHANGED
@@ -45,6 +45,7 @@ const connection_1 = require("./connection");
45
45
  const connection_2 = require("./connection");
46
46
  const schedule_fire_1 = require("./activities/schedule-fire");
47
47
  const outbox_1 = require("./activities/outbox");
48
+ const maestro_1 = require("./activities/maestro");
48
49
  const log = (...args) => console.error('[claude-tempo:worker]', ...args);
49
50
  const BUNDLE_PATH = path.resolve(__dirname, '..', 'workflow-bundle.js');
50
51
  async function getWorkflowBundle() {
@@ -73,6 +74,7 @@ async function createWorkers(config) {
73
74
  const client = new client_1.Client({ connection: clientConnection, namespace: config.temporalNamespace });
74
75
  const scheduleActivities = (0, schedule_fire_1.createScheduleActivities)(client);
75
76
  const outboxActivities = (0, outbox_1.createOutboxActivities)(client, config);
77
+ const maestroActivities = (0, maestro_1.createMaestroActivities)(client);
76
78
  const workflowBundle = await getWorkflowBundle();
77
79
  const SHUTDOWN_GRACE_TIME = '10s';
78
80
  const SHUTDOWN_FORCE_TIME = '15s';
@@ -85,6 +87,7 @@ async function createWorkers(config) {
85
87
  shutdownForceTime: SHUTDOWN_FORCE_TIME,
86
88
  activities: {
87
89
  ...scheduleActivities,
90
+ ...maestroActivities,
88
91
  // Shared-queue delivery activities (everything except spawnProcess)
89
92
  deliverCue: outboxActivities.deliverCue,
90
93
  deliverReport: outboxActivities.deliverReport,
@@ -1,2 +1,3 @@
1
1
  export { claudeSessionWorkflow } from './session';
2
2
  export { claudeSchedulerWorkflow } from './scheduler';
3
+ export { claudeMaestroWorkflow } from './maestro';
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.claudeSchedulerWorkflow = exports.claudeSessionWorkflow = void 0;
3
+ exports.claudeMaestroWorkflow = exports.claudeSchedulerWorkflow = exports.claudeSessionWorkflow = void 0;
4
4
  // Workflow entry point — re-exports all workflows for bundling.
5
5
  var session_1 = require("./session");
6
6
  Object.defineProperty(exports, "claudeSessionWorkflow", { enumerable: true, get: function () { return session_1.claudeSessionWorkflow; } });
7
7
  var scheduler_1 = require("./scheduler");
8
8
  Object.defineProperty(exports, "claudeSchedulerWorkflow", { enumerable: true, get: function () { return scheduler_1.claudeSchedulerWorkflow; } });
9
+ var maestro_1 = require("./maestro");
10
+ Object.defineProperty(exports, "claudeMaestroWorkflow", { enumerable: true, get: function () { return maestro_1.claudeMaestroWorkflow; } });
@@ -0,0 +1,16 @@
1
+ import type { MaestroPlayerInfo, MaestroEvent, MaestroPendingCommand } from '../types';
2
+ export type { MaestroPlayerInfo, MaestroEvent, MaestroPendingCommand, MaestroInput, } from '../types';
3
+ /** Gracefully shut down the Maestro workflow. */
4
+ export declare const maestroShutdownSignal: import("@temporalio/workflow").SignalDefinition<[], "maestroShutdown">;
5
+ /** Get the current snapshot of all players in the ensemble. */
6
+ export declare const maestroPlayersQuery: import("@temporalio/workflow").QueryDefinition<MaestroPlayerInfo[], [], string>;
7
+ /** Get the event log (ring buffer, max 200 entries). */
8
+ export declare const maestroEventsQuery: import("@temporalio/workflow").QueryDefinition<MaestroEvent[], [], string>;
9
+ /** Get pending commands (queued but not yet relayed to conductor). */
10
+ export declare const maestroPendingCommandsQuery: import("@temporalio/workflow").QueryDefinition<MaestroPendingCommand[], [], string>;
11
+ /** Queue a command to be relayed to the conductor. Returns the command ID. */
12
+ export declare const maestroSendCommandUpdate: import("@temporalio/common").UpdateDefinition<string, [{
13
+ text: string;
14
+ source: string;
15
+ replyTo?: string;
16
+ }], string>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.maestroSendCommandUpdate = exports.maestroPendingCommandsQuery = exports.maestroEventsQuery = exports.maestroPlayersQuery = exports.maestroShutdownSignal = void 0;
4
+ const workflow_1 = require("@temporalio/workflow");
5
+ // ── Maestro Signals ──
6
+ /** Gracefully shut down the Maestro workflow. */
7
+ exports.maestroShutdownSignal = (0, workflow_1.defineSignal)('maestroShutdown');
8
+ // ── Maestro Queries ──
9
+ /** Get the current snapshot of all players in the ensemble. */
10
+ exports.maestroPlayersQuery = (0, workflow_1.defineQuery)('maestroPlayers');
11
+ /** Get the event log (ring buffer, max 200 entries). */
12
+ exports.maestroEventsQuery = (0, workflow_1.defineQuery)('maestroEvents');
13
+ /** Get pending commands (queued but not yet relayed to conductor). */
14
+ exports.maestroPendingCommandsQuery = (0, workflow_1.defineQuery)('maestroPendingCommands');
15
+ // ── Maestro Updates ──
16
+ /** Queue a command to be relayed to the conductor. Returns the command ID. */
17
+ exports.maestroSendCommandUpdate = (0, workflow_1.defineUpdate)('maestroSendCommand');
@@ -0,0 +1,2 @@
1
+ import { MaestroInput } from './maestro-signals';
2
+ export declare function claudeMaestroWorkflow(input: MaestroInput): Promise<void>;