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.
- package/CLAUDE.md +8 -3
- package/README.md +50 -3
- package/dist/activities/outbox.d.ts +4 -0
- package/dist/activities/outbox.js +12 -1
- package/dist/activities/schedule-fire.d.ts +5 -0
- package/dist/activities/schedule-fire.js +6 -0
- package/dist/ensemble/agent-types.js +7 -0
- package/dist/ensemble/loader.js +3 -2
- package/dist/ensemble/saver.js +6 -1
- package/dist/ensemble/schema.d.ts +3 -0
- package/dist/server.js +9 -0
- package/dist/tools/agent-types.js +4 -1
- package/dist/tools/evaluate-gate.d.ts +3 -0
- package/dist/tools/evaluate-gate.js +40 -0
- package/dist/tools/gates.d.ts +3 -0
- package/dist/tools/gates.js +51 -0
- package/dist/tools/load-lineup.js +28 -1
- package/dist/tools/quality-gate.d.ts +3 -0
- package/dist/tools/quality-gate.js +34 -0
- package/dist/tools/recruit.js +3 -0
- package/dist/tools/schedule.js +48 -8
- package/dist/tools/schedules.js +3 -1
- package/dist/types.d.ts +26 -1
- package/dist/utils/validation.d.ts +10 -0
- package/dist/utils/validation.js +11 -1
- package/dist/workflows/scheduler.js +13 -1
- package/dist/workflows/session.js +50 -1
- package/dist/workflows/signals.d.ts +17 -2
- package/dist/workflows/signals.js +5 -1
- package/examples/agents/tempo-composer.md +10 -0
- package/examples/agents/tempo-conductor.md +10 -0
- package/examples/agents/tempo-critic.md +28 -1
- package/examples/agents/tempo-improv.md +10 -0
- package/examples/agents/tempo-liner.md +10 -0
- package/examples/agents/tempo-roadie.md +10 -0
- package/examples/agents/tempo-soloist.md +10 -0
- package/examples/agents/tempo-tuner.md +28 -0
- package/package.json +2 -1
- 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
|
-
- **
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/ensemble/loader.js
CHANGED
|
@@ -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
|
};
|
package/dist/ensemble/saver.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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,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,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
|
|
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,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
|
+
}
|
package/dist/tools/recruit.js
CHANGED
|
@@ -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 {
|