claude-tempo 0.11.1 → 0.13.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.
Files changed (39) hide show
  1. package/CLAUDE.md +8 -3
  2. package/README.md +50 -3
  3. package/dist/activities/outbox.d.ts +4 -0
  4. package/dist/activities/outbox.js +12 -1
  5. package/dist/activities/schedule-fire.d.ts +5 -0
  6. package/dist/activities/schedule-fire.js +6 -0
  7. package/dist/ensemble/agent-types.js +7 -0
  8. package/dist/ensemble/loader.js +3 -2
  9. package/dist/ensemble/saver.js +6 -1
  10. package/dist/ensemble/schema.d.ts +3 -0
  11. package/dist/server.js +9 -0
  12. package/dist/tools/agent-types.js +4 -1
  13. package/dist/tools/evaluate-gate.d.ts +3 -0
  14. package/dist/tools/evaluate-gate.js +40 -0
  15. package/dist/tools/gates.d.ts +3 -0
  16. package/dist/tools/gates.js +51 -0
  17. package/dist/tools/load-lineup.js +28 -1
  18. package/dist/tools/quality-gate.d.ts +3 -0
  19. package/dist/tools/quality-gate.js +34 -0
  20. package/dist/tools/recruit.js +3 -0
  21. package/dist/tools/schedule.js +48 -8
  22. package/dist/tools/schedules.js +3 -1
  23. package/dist/types.d.ts +26 -1
  24. package/dist/utils/validation.d.ts +10 -0
  25. package/dist/utils/validation.js +11 -1
  26. package/dist/workflows/scheduler.js +13 -1
  27. package/dist/workflows/session.js +50 -1
  28. package/dist/workflows/signals.d.ts +17 -2
  29. package/dist/workflows/signals.js +5 -1
  30. package/examples/agents/tempo-composer.md +10 -0
  31. package/examples/agents/tempo-conductor.md +10 -0
  32. package/examples/agents/tempo-critic.md +28 -1
  33. package/examples/agents/tempo-improv.md +10 -0
  34. package/examples/agents/tempo-liner.md +10 -0
  35. package/examples/agents/tempo-roadie.md +10 -0
  36. package/examples/agents/tempo-soloist.md +10 -0
  37. package/examples/agents/tempo-tuner.md +28 -0
  38. package/package.json +2 -1
  39. package/workflow-bundle.js +69 -4
package/CLAUDE.md CHANGED
@@ -9,7 +9,8 @@ claude-tempo is an MCP server that enables multiple Claude Code sessions to coor
9
9
  - **Runtime**: Node.js 18+ with TypeScript
10
10
  - **MCP**: `@modelcontextprotocol/sdk` (stdio transport)
11
11
  - **Temporal**: `@temporalio/client`, `@temporalio/worker`, `@temporalio/workflow`, `@temporalio/activity`
12
- - **No other dependencies** — no database, no custom broker
12
+ - **croner** — cron expression parsing and next-fire computation (used by `schedule` tool)
13
+ - **yaml**, **zod** — lineup parsing and schema validation
13
14
 
14
15
  ## Project Structure
15
16
 
@@ -61,6 +62,9 @@ src/
61
62
  │ ├── schedule.ts # Create one-shot or recurring schedules
62
63
  │ ├── unschedule.ts # Cancel a named schedule
63
64
  │ ├── schedules.ts # List active schedules
65
+ │ ├── quality-gate.ts # Define quality gates for tasks (conductor only)
66
+ │ ├── evaluate-gate.ts # Mark gate criteria as passed/failed (conductor only)
67
+ │ ├── gates.ts # List quality gates and their status (conductor only)
64
68
  │ └── helpers.ts # Zod/MCP tool registration wrapper
65
69
  ├── utils/
66
70
  │ └── validation.ts # Shared validation constants (name/message/path limits, encore defaults) and helpers
@@ -111,10 +115,11 @@ npm test
111
115
  - **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.
112
116
  - **Recall**: Queries a session's own message history from the Temporal workflow. Shows received messages by default; pass `includeSent: true` to also see sent messages. Supports `limit`, `since`, and `from` filters.
113
117
  - **Per-host task queues**: Each host runs a `claude-tempo-{hostname}` activity worker for local-only operations (e.g., `spawnProcess`). This enables cross-machine recruiting — the `recruit` tool accepts an optional `host` parameter to route the spawn to a remote machine's task queue.
114
- - **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.
118
+ - **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`.
115
119
  - **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).
116
- - **Schedule**: A one-shot or recurring message delivery configured via the `schedule` tool. Backed by a durable `claudeSchedulerWorkflow` — survives restarts. Supports delay, cron-style intervals, and time-bounded delivery. Managed via `schedule`, `unschedule`, and `schedules` tools.
120
+ - **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.
117
121
  - **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.
122
+ - **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`.
118
123
  - **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.
119
124
 
120
125
  ## Dashboard
package/README.md CHANGED
@@ -133,6 +133,9 @@ These tools are available inside Claude Code sessions connected to claude-tempo:
133
133
  | `broadcast` | Send a message to all active players. Optional `type` filter limits to a specific player type. |
134
134
  | `encore` | Revive a stale player session — restarts the process and reconnects to the existing workflow with context restored. |
135
135
  | `recall` | Read your own message history. Shows received messages by default; pass `includeSent: true` for the full timeline. |
136
+ | `quality_gate` | Define or replace a quality gate for a task — a named checklist of criteria that must pass. Conductor only. |
137
+ | `evaluate_gate` | Mark one or more criteria on a quality gate as passed or failed. Conductor only. |
138
+ | `gates` | List quality gates and their status. Filter by task name or status (`open`, `passed`, `failed`). Conductor only. |
136
139
 
137
140
  ## Scheduling
138
141
 
@@ -150,11 +153,20 @@ Tell your session things like:
150
153
  - *"Schedule a check every hour called 'deploy-watch' — cue ops to check deployment status"*
151
154
  - *"Remind me in 30 minutes to review PR #42"*
152
155
  - *"Every 5 minutes for the next hour, ping frontend to check their progress"*
153
- - *"Set up a daily standup reminder at 9am UTC for the conductor"*
156
+ - *"Set up a daily standup at 9am New York time, weekdays only"*
154
157
  - *"Cancel the deploy-watch schedule"*
155
158
  - *"Show me all active schedules"*
156
159
 
157
- Schedules support one-shot delays, fixed times, and recurring intervals with optional bounds (max count or end time).
160
+ Schedules support four timing modes all accept optional bounds (`count` max fires, `until` end time):
161
+
162
+ | Mode | Parameter | Example |
163
+ |------|-----------|---------|
164
+ | One-shot delay | `delay` | `"10m"`, `"2h"`, `"1d"` |
165
+ | Fixed time | `at` | `"2026-04-03T20:00:00Z"` |
166
+ | Recurring interval | `every` | `"5m"`, `"1h"` |
167
+ | Cron expression | `cron` + optional `timezone` | `"0 9 * * 1-5"` (weekdays 9am) |
168
+
169
+ The `timezone` parameter accepts any IANA timezone (e.g. `"America/New_York"`, `"Europe/London"`). Defaults to UTC when omitted.
158
170
 
159
171
  ### How it works
160
172
 
@@ -165,6 +177,24 @@ Schedules support one-shot delays, fixed times, and recurring intervals with opt
165
177
  - `claude-tempo status` shows active schedules alongside sessions
166
178
  - A single durable scheduler workflow per ensemble manages all schedules using Temporal timers
167
179
 
180
+ ## Quality Gates
181
+
182
+ Conductors can define named checklists of criteria to verify task completion. Three conductor-only tools are available: `quality_gate` (create or replace a gate), `evaluate_gate` (mark criteria as passed or failed), and `gates` (list all gates with optional filters).
183
+
184
+ ### Examples
185
+
186
+ Tell your conductor things like:
187
+
188
+ - *"Set a quality gate 'pr-ready' with criteria: tests pass, no lint errors, code reviewed"*
189
+ - *"Mark criteria 0 and 1 on 'pr-ready' as passed"*
190
+ - *"Show me all open quality gates"*
191
+ - *"Check whether 'deploy-staging' has passed"*
192
+
193
+ ### How it works
194
+
195
+ - Gate status is derived from criteria: all passed → `passed`; any failed → `failed`; otherwise `open`
196
+ - Gates survive `continueAsNew` for the conductor workflow's lifetime
197
+
168
198
  ## Ensemble Lineups
169
199
 
170
200
  Define reusable ensemble configurations as YAML files. A lineup specifies which players to recruit, what instructions to give them, what schedules to create, and optionally which custom agent files to use.
@@ -241,7 +271,7 @@ players:
241
271
 
242
272
  ## Player Types
243
273
 
244
- Player types are reusable agent definitions in Claude Code's standard subagent format — `.md` files with YAML frontmatter specifying name, description, and optional model. They let you define specialized roles once and reuse them across lineups.
274
+ Player types are reusable agent definitions in Claude Code's standard subagent format — `.md` files with YAML frontmatter specifying name, description, optional model, and optional tool restrictions. They let you define specialized roles once and reuse them across lineups.
245
275
 
246
276
  ### How player types work
247
277
 
@@ -257,6 +287,23 @@ players:
257
287
 
258
288
  When a player is recruited with a type, the agent definition is resolved and passed to the session. Players know their type via the `who_am_i` tool.
259
289
 
290
+ ### Tool restrictions (`allowedTools`)
291
+
292
+ Agent type frontmatter may include an `allowedTools` array to restrict which tools the spawned session can use. When present, it is passed to the Claude Code session via `--allowedTools` and overrides any lineup-level setting.
293
+
294
+ ```yaml
295
+ ---
296
+ name: tempo-reviewer
297
+ description: Read-only code reviewer
298
+ allowedTools:
299
+ - Read
300
+ - Glob
301
+ - Grep
302
+ ---
303
+ ```
304
+
305
+ This is useful for security-sensitive roles (read-only reviewers, auditors) or to prevent specific players from making changes outside their scope. Sessions launched without a type, or with a type that omits `allowedTools`, receive no tool restrictions.
306
+
260
307
  ### Three-tier lookup
261
308
 
262
309
  Player types are resolved in order (first match wins):
@@ -30,6 +30,7 @@ export interface StartRecruitedSessionInput {
30
30
  taskQueue: string;
31
31
  agentDefinition?: string;
32
32
  agentDefinitionDescription?: string;
33
+ allowedTools?: string[];
33
34
  }
34
35
  export interface SpawnProcessInput {
35
36
  targetName: string;
@@ -45,6 +46,8 @@ export interface SpawnProcessInput {
45
46
  nativeResolvable?: boolean;
46
47
  /** When true, use --resume instead of -n (reconnect to existing session). */
47
48
  resume?: boolean;
49
+ /** Tool restrictions from the agent definition frontmatter. */
50
+ allowedTools?: string[];
48
51
  }
49
52
  export interface PerformEncoreInput {
50
53
  ensemble: string;
@@ -60,6 +63,7 @@ export interface EncoreResult {
60
63
  agentDefinition?: string;
61
64
  agentDefinitionPath?: string;
62
65
  nativeResolvable?: boolean;
66
+ allowedTools?: string[];
63
67
  temporalAddress: string;
64
68
  temporalNamespace: string;
65
69
  }
@@ -145,11 +145,14 @@ function createOutboxActivities(client, config) {
145
145
  }
146
146
  },
147
147
  async spawnProcess(input) {
148
- const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume } = input;
148
+ const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume, allowedTools } = input;
149
149
  // Read secrets from the worker's config closure — never from workflow state
150
150
  const { temporalApiKey, temporalTlsCertPath, temporalTlsKeyPath } = config;
151
151
  try {
152
152
  if (agent === 'copilot') {
153
+ if (allowedTools && allowedTools.length > 0) {
154
+ log(`Warning: allowedTools [${allowedTools.join(', ')}] specified for copilot agent "${targetName}" — copilot bridge does not support --allowedTools, skipping`);
155
+ }
153
156
  const { pid } = (0, spawn_1.spawnCopilotBridge)({
154
157
  name: targetName,
155
158
  ensemble,
@@ -177,11 +180,16 @@ function createOutboxActivities(client, config) {
177
180
  }
178
181
  // Use --resume for encore (reconnect to existing session) or -n for new sessions
179
182
  const nameArgs = resume ? ['--resume', targetName] : ['-n', targetName];
183
+ // Build --allowedTools flag from agent definition frontmatter
184
+ const allowedToolsFlags = allowedTools && allowedTools.length > 0
185
+ ? ['--allowedTools', ...allowedTools]
186
+ : [];
180
187
  const spawnArgs = [
181
188
  '--dangerously-skip-permissions',
182
189
  '--dangerously-load-development-channels', 'server:claude-tempo',
183
190
  ...nameArgs,
184
191
  ...agentFlags,
192
+ ...allowedToolsFlags,
185
193
  ];
186
194
  const envVars = {
187
195
  [config_2.ENV.ENSEMBLE]: ensemble,
@@ -249,12 +257,14 @@ function createOutboxActivities(client, config) {
249
257
  const playerType = metadata.playerType;
250
258
  let agentDefinitionPath;
251
259
  let nativeResolvable;
260
+ let allowedTools;
252
261
  if (playerType) {
253
262
  try {
254
263
  const info = (0, agent_types_1.resolveAgentType)(playerType);
255
264
  if (info) {
256
265
  agentDefinitionPath = info.path;
257
266
  nativeResolvable = info.nativeResolvable;
267
+ allowedTools = info.allowedTools;
258
268
  }
259
269
  }
260
270
  catch {
@@ -269,6 +279,7 @@ function createOutboxActivities(client, config) {
269
279
  agentDefinition: playerType,
270
280
  agentDefinitionPath,
271
281
  nativeResolvable,
282
+ allowedTools,
272
283
  temporalAddress: config.temporalAddress,
273
284
  temporalNamespace: config.temporalNamespace,
274
285
  };
@@ -10,9 +10,14 @@ export interface FireScheduleResult {
10
10
  success: boolean;
11
11
  error?: string;
12
12
  }
13
+ export interface ComputeNextCronInput {
14
+ cronExpression: string;
15
+ timezone?: string;
16
+ }
13
17
  /** Activity interface — used by proxyActivities in the scheduler workflow. */
14
18
  export interface ScheduleActivities {
15
19
  fireSchedule(input: FireScheduleInput): Promise<FireScheduleResult>;
20
+ computeNextCronFire(input: ComputeNextCronInput): Promise<string | null>;
16
21
  }
17
22
  /**
18
23
  * Create the schedule-fire activity bound to a Temporal client.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createScheduleActivities = createScheduleActivities;
4
+ const croner_1 = require("croner");
4
5
  const config_1 = require("../config");
5
6
  const resolve_1 = require("./resolve");
6
7
  /**
@@ -9,6 +10,11 @@ const resolve_1 = require("./resolve");
9
10
  */
10
11
  function createScheduleActivities(client) {
11
12
  return {
13
+ async computeNextCronFire(input) {
14
+ const job = new croner_1.Cron(input.cronExpression, { timezone: input.timezone || 'UTC' });
15
+ const next = job.nextRun();
16
+ return next ? next.toISOString() : null;
17
+ },
12
18
  async fireSchedule(input) {
13
19
  const { ensemble, scheduleName, message, target, createdBy } = input;
14
20
  try {
@@ -77,6 +77,7 @@ function listAgentTypes(cwd) {
77
77
  source,
78
78
  path: filePath,
79
79
  nativeResolvable,
80
+ ...(Array.isArray(fm.allowedTools) ? { allowedTools: fm.allowedTools.map(String) } : {}),
80
81
  });
81
82
  }
82
83
  }
@@ -98,6 +99,7 @@ function resolveAgentType(name, cwd) {
98
99
  source,
99
100
  path: filePath,
100
101
  nativeResolvable,
102
+ ...(Array.isArray(fm.allowedTools) ? { allowedTools: fm.allowedTools.map(String) } : {}),
101
103
  };
102
104
  }
103
105
  return null;
@@ -120,6 +122,11 @@ function loadAndResolveLineup(filePath, cwd) {
120
122
  }
121
123
  player._agentDefinition = info.name;
122
124
  player._agentDefinitionPath = info.path;
125
+ // Type's allowedTools is the security authority — overrides lineup-level setting
126
+ // Empty array means "not specified" (no restriction), so don't override
127
+ if (info.allowedTools && info.allowedTools.length > 0) {
128
+ player.allowedTools = info.allowedTools;
129
+ }
123
130
  }
124
131
  return lineup;
125
132
  }
@@ -45,8 +45,8 @@ function loadLineup(filePath) {
45
45
  if (typeof s.target !== 'string' || !s.target) {
46
46
  throw new Error(`Invalid lineup: schedules[${i}].target is required`);
47
47
  }
48
- if (!s.at && !s.delay && !s.every) {
49
- throw new Error(`Invalid lineup: schedules[${i}] must have at least one of: at, delay, every`);
48
+ if (!s.at && !s.delay && !s.every && !s.cron) {
49
+ throw new Error(`Invalid lineup: schedules[${i}] must have at least one of: at, delay, every, cron`);
50
50
  }
51
51
  }
52
52
  }
@@ -60,6 +60,7 @@ function loadLineup(filePath) {
60
60
  ...(p.workDir != null && { workDir: p.workDir }),
61
61
  ...(p.agent != null && { agent: p.agent }),
62
62
  ...(p.instructions != null && { instructions: p.instructions }),
63
+ ...(Array.isArray(p.allowedTools) && { allowedTools: p.allowedTools.map(String) }),
63
64
  })),
64
65
  schedules: doc.schedules,
65
66
  };
@@ -67,7 +67,12 @@ async function saveLineup(client, ensemble, filePath, name) {
67
67
  message: entry.message,
68
68
  target: entry.target,
69
69
  };
70
- if (entry.interval) {
70
+ if (entry.cronExpression) {
71
+ sched.cron = entry.cronExpression;
72
+ if (entry.timezone)
73
+ sched.timezone = entry.timezone;
74
+ }
75
+ else if (entry.interval) {
71
76
  sched.every = formatDurationMs(entry.interval);
72
77
  }
73
78
  if (entry.until) {
@@ -12,6 +12,7 @@ export interface EnsembleLineup {
12
12
  workDir?: string;
13
13
  agent?: string;
14
14
  instructions?: string;
15
+ allowedTools?: string[];
15
16
  /** Transient: resolved agent definition name (set by loadAndResolveLineup). */
16
17
  _agentDefinition?: string;
17
18
  /** Transient: resolved absolute path to .md file (set by loadAndResolveLineup). */
@@ -24,6 +25,8 @@ export interface EnsembleLineup {
24
25
  at?: string;
25
26
  delay?: string;
26
27
  every?: string;
28
+ cron?: string;
29
+ timezone?: string;
27
30
  until?: string;
28
31
  count?: number;
29
32
  }>;
package/dist/server.js CHANGED
@@ -64,6 +64,9 @@ const who_am_i_1 = require("./tools/who-am-i");
64
64
  const broadcast_1 = require("./tools/broadcast");
65
65
  const recall_1 = require("./tools/recall");
66
66
  const encore_1 = require("./tools/encore");
67
+ const quality_gate_1 = require("./tools/quality-gate");
68
+ const evaluate_gate_1 = require("./tools/evaluate-gate");
69
+ const gates_1 = require("./tools/gates");
67
70
  const channel_1 = require("./channel");
68
71
  const agent_types_2 = require("./ensemble/agent-types");
69
72
  const log = (...args) => console.error('[claude-tempo]', ...args);
@@ -279,6 +282,12 @@ async function main() {
279
282
  (0, broadcast_1.registerBroadcastTool)(mcpServer, client, config, getPlayerId, handle);
280
283
  (0, recall_1.registerRecallTool)(mcpServer, handle, getPlayerId);
281
284
  (0, encore_1.registerEncoreTool)(mcpServer, client, config, getPlayerId, handle);
285
+ // Conductor-only tools
286
+ if (isConductor) {
287
+ (0, quality_gate_1.registerQualityGateTool)(mcpServer, handle, getPlayerId);
288
+ (0, evaluate_gate_1.registerEvaluateGateTool)(mcpServer, handle, getPlayerId);
289
+ (0, gates_1.registerGatesTool)(mcpServer, handle);
290
+ }
282
291
  const MAESTRO_ACK = '\n\n[IMPORTANT: This message is from a human (Maestro). Immediately cue the sender back with a brief acknowledgment and your planned next step before doing the work.]';
283
292
  // Start message poller — push messages into Claude Code via channel notifications.
284
293
  // Skip when running under the Copilot bridge: the bridge has its own poller that
@@ -11,7 +11,10 @@ function registerAgentTypesTool(server) {
11
11
  }
12
12
  const lines = types.map(t => {
13
13
  const src = t.source === 'shipped' ? '(shipped)' : t.source === 'user' ? '(user)' : '(project)';
14
- return `**${t.name}** ${src}\n ${t.description || 'No description'}`;
14
+ const tools = t.allowedTools && t.allowedTools.length > 0
15
+ ? `\n Allowed tools: ${t.allowedTools.join(', ')}`
16
+ : '';
17
+ return `**${t.name}** ${src}\n ${t.description || 'No description'}${tools}`;
15
18
  });
16
19
  return { content: [{ type: 'text', text: lines.join('\n\n') }] };
17
20
  });
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { WorkflowHandle } from '@temporalio/client';
3
+ export declare function registerEvaluateGateTool(server: McpServer, handle: WorkflowHandle, getPlayerId: () => string): void;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerEvaluateGateTool = registerEvaluateGateTool;
4
+ const zod_1 = require("zod");
5
+ const helpers_1 = require("./helpers");
6
+ const validation_1 = require("../utils/validation");
7
+ function registerEvaluateGateTool(server, handle, getPlayerId) {
8
+ (0, helpers_1.defineTool)(server, 'evaluate_gate', 'Mark one or more criteria on a quality gate as passed or failed. Conductor only.', {
9
+ task: zod_1.z.string().max(validation_1.GATE_TASK_MAX).describe('The task name of the gate to evaluate'),
10
+ evaluations: zod_1.z.array(zod_1.z.object({
11
+ index: zod_1.z.number().int().min(0).describe('Zero-based index of the criterion'),
12
+ status: zod_1.z.enum(['passed', 'failed']).describe('Whether this criterion passed or failed'),
13
+ notes: zod_1.z.string().max(validation_1.GATE_NOTES_MAX).optional().describe('Optional notes explaining the evaluation'),
14
+ })).min(1).describe('List of criterion evaluations'),
15
+ }, async (args) => {
16
+ const { task, evaluations } = args;
17
+ try {
18
+ await handle.signal('evaluateGateCriteria', {
19
+ task,
20
+ evaluations,
21
+ evaluatedBy: getPlayerId(),
22
+ });
23
+ const summary = evaluations
24
+ .map((ev) => ` ${ev.index}: ${ev.status === 'passed' ? '\u2705' : '\u274c'} ${ev.status}${ev.notes ? ` — ${ev.notes}` : ''}`)
25
+ .join('\n');
26
+ return {
27
+ content: [{
28
+ type: 'text',
29
+ text: `Evaluated ${evaluations.length} criteria on gate **${task}**:\n${summary}`,
30
+ }],
31
+ };
32
+ }
33
+ catch (err) {
34
+ return {
35
+ content: [{ type: 'text', text: `Failed to evaluate gate: ${err}` }],
36
+ isError: true,
37
+ };
38
+ }
39
+ });
40
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { WorkflowHandle } from '@temporalio/client';
3
+ export declare function registerGatesTool(server: McpServer, handle: WorkflowHandle): void;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerGatesTool = registerGatesTool;
4
+ const zod_1 = require("zod");
5
+ const helpers_1 = require("./helpers");
6
+ const validation_1 = require("../utils/validation");
7
+ function registerGatesTool(server, handle) {
8
+ (0, helpers_1.defineTool)(server, 'gates', 'List quality gates and their status. Optionally filter by task name or status. Conductor only.', {
9
+ task: zod_1.z.string().max(validation_1.GATE_TASK_MAX).optional().describe('Filter by specific task name'),
10
+ status: zod_1.z.enum(['open', 'passed', 'failed']).optional().describe('Filter by gate status'),
11
+ }, async (args) => {
12
+ const { task, status } = args;
13
+ try {
14
+ const gates = await handle.query('qualityGates');
15
+ let filtered = gates;
16
+ if (task) {
17
+ filtered = filtered.filter((g) => g.task === task);
18
+ }
19
+ if (status) {
20
+ filtered = filtered.filter((g) => g.status === status);
21
+ }
22
+ if (filtered.length === 0) {
23
+ return {
24
+ content: [{ type: 'text', text: 'No quality gates found matching the filter.' }],
25
+ };
26
+ }
27
+ const lines = filtered.map((g) => {
28
+ const icon = g.status === 'passed' ? '\u2705' : g.status === 'failed' ? '\u274c' : '\u23f3';
29
+ const criteriaLines = g.criteria.map((c, i) => {
30
+ const cIcon = c.status === 'passed' ? '\u2705' : c.status === 'failed' ? '\u274c' : '\u2b1c';
31
+ const evaluator = c.evaluatedBy ? ` (by ${c.evaluatedBy})` : '';
32
+ const notes = c.notes ? ` — ${c.notes}` : '';
33
+ return ` ${i}. ${cIcon} ${c.text}${evaluator}${notes}`;
34
+ });
35
+ return `${icon} **${g.task}** [${g.status}] (by ${g.createdBy}, ${g.createdAt})\n${criteriaLines.join('\n')}`;
36
+ });
37
+ return {
38
+ content: [{
39
+ type: 'text',
40
+ text: `${filtered.length} quality gate${filtered.length === 1 ? '' : 's'}:\n\n${lines.join('\n\n')}`,
41
+ }],
42
+ };
43
+ }
44
+ catch (err) {
45
+ return {
46
+ content: [{ type: 'text', text: `Failed to query gates: ${err}` }],
47
+ isError: true,
48
+ };
49
+ }
50
+ });
51
+ }
@@ -4,6 +4,7 @@ exports.registerLoadLineupTool = registerLoadLineupTool;
4
4
  const zod_1 = require("zod");
5
5
  const fs_1 = require("fs");
6
6
  const path_1 = require("path");
7
+ const croner_1 = require("croner");
7
8
  const client_1 = require("@temporalio/client");
8
9
  const config_1 = require("../config");
9
10
  const agent_types_1 = require("../ensemble/agent-types");
@@ -143,11 +144,16 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
143
144
  else if (systemPrompt) {
144
145
  agentFlags = ['--system-prompt', systemPrompt];
145
146
  }
147
+ // Build --allowedTools flag from agent definition or lineup
148
+ const allowedToolsFlags = player.allowedTools && player.allowedTools.length > 0
149
+ ? ['--allowedTools', ...player.allowedTools]
150
+ : [];
146
151
  const spawnArgs = [
147
152
  '--dangerously-skip-permissions',
148
153
  '--dangerously-load-development-channels', 'server:claude-tempo',
149
154
  '-n', playerName,
150
155
  ...agentFlags,
156
+ ...allowedToolsFlags,
151
157
  ];
152
158
  const envVars = {
153
159
  [config_1.ENV.ENSEMBLE]: config.ensemble,
@@ -212,8 +218,17 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
212
218
  const now = Date.now();
213
219
  let nextFireAt;
214
220
  let interval;
221
+ let cronExpression;
222
+ let timezone;
215
223
  if (sched.at) {
216
224
  nextFireAt = Date.parse(sched.at);
225
+ // Support at + every: use `at` as the initial fire time, `every` as the interval
226
+ if (sched.every) {
227
+ const ms = (0, duration_1.parseDuration)(sched.every);
228
+ if (!ms)
229
+ throw new Error(`Invalid interval: ${sched.every}`);
230
+ interval = ms;
231
+ }
217
232
  }
218
233
  else if (sched.delay) {
219
234
  const ms = (0, duration_1.parseDuration)(sched.delay);
@@ -228,16 +243,28 @@ function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentTyp
228
243
  nextFireAt = now + ms;
229
244
  interval = ms;
230
245
  }
246
+ else if (sched.cron) {
247
+ cronExpression = sched.cron;
248
+ timezone = sched.timezone || 'UTC';
249
+ const job = new croner_1.Cron(cronExpression, { timezone });
250
+ const next = job.nextRun();
251
+ if (!next)
252
+ throw new Error(`Cron expression "${sched.cron}" has no upcoming fire time`);
253
+ nextFireAt = next.getTime();
254
+ }
231
255
  else {
232
256
  throw new Error('No timing specified');
233
257
  }
258
+ const type = sched.cron ? 'cron' : (sched.every || interval) ? 'interval' : 'once';
234
259
  const scheduleEntry = {
235
260
  name: sched.name,
236
261
  message: sched.message,
237
262
  target: sched.target,
238
- type: sched.every ? 'interval' : 'once',
263
+ type,
239
264
  nextFireAt: new Date(nextFireAt).toISOString(),
240
265
  interval,
266
+ cronExpression,
267
+ timezone,
241
268
  until: sched.until,
242
269
  remainingCount: sched.count,
243
270
  firedCount: 0,
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { WorkflowHandle } from '@temporalio/client';
3
+ export declare function registerQualityGateTool(server: McpServer, handle: WorkflowHandle, getPlayerId: () => string): void;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerQualityGateTool = registerQualityGateTool;
4
+ const zod_1 = require("zod");
5
+ const helpers_1 = require("./helpers");
6
+ const validation_1 = require("../utils/validation");
7
+ function registerQualityGateTool(server, handle, getPlayerId) {
8
+ (0, helpers_1.defineTool)(server, 'quality_gate', 'Define or replace a quality gate for a task. Each gate has a list of criteria that must pass before the task is considered complete. Conductor only.', {
9
+ task: zod_1.z.string().max(validation_1.GATE_TASK_MAX).describe('Unique task name for this gate (e.g. "pr-review", "deploy-staging")'),
10
+ criteria: zod_1.z.array(zod_1.z.string().max(validation_1.GATE_CRITERION_TEXT_MAX)).min(1).max(validation_1.GATE_CRITERIA_MAX).describe('List of criteria that must be evaluated (e.g. ["Tests pass", "No lint errors", "Code reviewed"])'),
11
+ }, async (args) => {
12
+ const { task, criteria } = args;
13
+ try {
14
+ await handle.signal('setQualityGate', {
15
+ task,
16
+ criteria,
17
+ createdBy: getPlayerId(),
18
+ });
19
+ const lines = criteria.map((c, i) => ` ${i}. [ ] ${c}`);
20
+ return {
21
+ content: [{
22
+ type: 'text',
23
+ text: `Quality gate **${task}** set with ${criteria.length} criteria:\n${lines.join('\n')}`,
24
+ }],
25
+ };
26
+ }
27
+ catch (err) {
28
+ return {
29
+ content: [{ type: 'text', text: `Failed to set quality gate: ${err}` }],
30
+ isError: true,
31
+ };
32
+ }
33
+ });
34
+ }
@@ -36,6 +36,7 @@ function registerRecruitTool(server, client, config, getPlayerId, handle, ownAge
36
36
  let agentDefinitionPath;
37
37
  let agentDefinitionDescription;
38
38
  let nativeResolvable;
39
+ let allowedTools;
39
40
  if (agentTypeName) {
40
41
  const info = (0, agent_types_1.resolveAgentType)(agentTypeName);
41
42
  if (!info) {
@@ -52,6 +53,7 @@ function registerRecruitTool(server, client, config, getPlayerId, handle, ownAge
52
53
  agentDefinitionPath = info.path;
53
54
  agentDefinitionDescription = info.description;
54
55
  nativeResolvable = info.nativeResolvable;
56
+ allowedTools = info.allowedTools;
55
57
  }
56
58
  // Validate name
57
59
  const nameError = (0, validation_1.validatePlayerName)(name);
@@ -118,6 +120,7 @@ function registerRecruitTool(server, client, config, getPlayerId, handle, ownAge
118
120
  agentDefinitionPath,
119
121
  agentDefinitionDescription,
120
122
  nativeResolvable,
123
+ allowedTools,
121
124
  };
122
125
  const entryId = await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
123
126
  return {