clearctx 3.0.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/CHANGELOG.md +71 -0
- package/LICENSE +21 -0
- package/README.md +1006 -0
- package/STRATEGY.md +485 -0
- package/bin/cli.js +1756 -0
- package/bin/continuity-hook.js +118 -0
- package/bin/mcp.js +27 -0
- package/bin/setup.js +929 -0
- package/package.json +56 -0
- package/src/artifact-store.js +710 -0
- package/src/atomic-io.js +99 -0
- package/src/briefing-generator.js +451 -0
- package/src/continuity-hooks.js +253 -0
- package/src/contract-store.js +525 -0
- package/src/decision-journal.js +229 -0
- package/src/delegate.js +348 -0
- package/src/dependency-resolver.js +453 -0
- package/src/diff-engine.js +473 -0
- package/src/file-lock.js +161 -0
- package/src/index.js +61 -0
- package/src/lineage-graph.js +402 -0
- package/src/manager.js +510 -0
- package/src/mcp-server.js +3501 -0
- package/src/pattern-registry.js +221 -0
- package/src/pipeline-engine.js +618 -0
- package/src/prompts.js +1217 -0
- package/src/safety-net.js +170 -0
- package/src/session-snapshot.js +508 -0
- package/src/snapshot-engine.js +490 -0
- package/src/stale-detector.js +169 -0
- package/src/store.js +131 -0
- package/src/stream-session.js +463 -0
- package/src/team-hub.js +615 -0
|
@@ -0,0 +1,3501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server — Model Context Protocol server for clearctx.
|
|
3
|
+
*
|
|
4
|
+
* This turns the multi-session system into NATIVE Claude Code tools.
|
|
5
|
+
* Instead of Claude running CLI commands via Bash, it calls tools directly:
|
|
6
|
+
*
|
|
7
|
+
* mcp__multi-session__spawn_session
|
|
8
|
+
* mcp__multi-session__send_message
|
|
9
|
+
* mcp__multi-session__delegate_task
|
|
10
|
+
* etc.
|
|
11
|
+
*
|
|
12
|
+
* Protocol: JSON-RPC 2.0 over stdio (newline-delimited JSON).
|
|
13
|
+
* Zero dependencies — implements the MCP protocol manually.
|
|
14
|
+
*
|
|
15
|
+
* How MCP works:
|
|
16
|
+
* 1. Claude Code starts this server as a child process
|
|
17
|
+
* 2. Claude sends JSON-RPC requests on stdin
|
|
18
|
+
* 3. Server responds with JSON-RPC responses on stdout
|
|
19
|
+
* 4. All debug/log output goes to stderr (never stdout)
|
|
20
|
+
*
|
|
21
|
+
* JSON-RPC methods handled:
|
|
22
|
+
* - initialize → capability negotiation
|
|
23
|
+
* - notifications/initialized → client confirms init (no response)
|
|
24
|
+
* - tools/list → return available tools
|
|
25
|
+
* - tools/call → execute a tool and return result
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const os = require('os');
|
|
31
|
+
const readline = require('readline');
|
|
32
|
+
const SessionManager = require('./manager');
|
|
33
|
+
const Delegate = require('./delegate');
|
|
34
|
+
const { buildFullTeamPrompt, getOrchestratorGuideSection } = require('./prompts');
|
|
35
|
+
|
|
36
|
+
// Team Hub imports — all the new Layer 1, 2, 3 modules
|
|
37
|
+
const TeamHub = require('./team-hub');
|
|
38
|
+
const ArtifactStore = require('./artifact-store');
|
|
39
|
+
const ContractStore = require('./contract-store');
|
|
40
|
+
const DependencyResolver = require('./dependency-resolver');
|
|
41
|
+
const LineageGraph = require('./lineage-graph');
|
|
42
|
+
const PipelineEngine = require('./pipeline-engine');
|
|
43
|
+
const SnapshotEngine = require('./snapshot-engine');
|
|
44
|
+
|
|
45
|
+
// Layer 0: Session Continuity Engine
|
|
46
|
+
const SessionSnapshot = require('./session-snapshot');
|
|
47
|
+
const DiffEngine = require('./diff-engine');
|
|
48
|
+
const BriefingGenerator = require('./briefing-generator');
|
|
49
|
+
const DecisionJournal = require('./decision-journal');
|
|
50
|
+
const PatternRegistry = require('./pattern-registry');
|
|
51
|
+
const StaleDetector = require('./stale-detector');
|
|
52
|
+
|
|
53
|
+
// Capture version at load time — used to detect stale server processes
|
|
54
|
+
const LOADED_VERSION = require('../package.json').version;
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// Server State — persists across all tool calls
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
// Single SessionManager instance for the entire MCP server lifetime.
|
|
61
|
+
// This means spawned sessions stay alive between tool calls — much more
|
|
62
|
+
// efficient than the CLI where each command creates a new manager.
|
|
63
|
+
const manager = new SessionManager();
|
|
64
|
+
|
|
65
|
+
// Track delegates per session (for continue/finish/abort)
|
|
66
|
+
const delegates = new Map();
|
|
67
|
+
|
|
68
|
+
// Team Hub instances (created lazily on first team tool call)
|
|
69
|
+
// These are cached per team name to avoid recreating them on every call
|
|
70
|
+
let teamHub = null;
|
|
71
|
+
let artifactStore = null;
|
|
72
|
+
let contractStore = null;
|
|
73
|
+
let resolver = null;
|
|
74
|
+
let lineageGraph = null;
|
|
75
|
+
let pipelineEngine = null;
|
|
76
|
+
let snapshotEngine = null;
|
|
77
|
+
let currentTeamName = null;
|
|
78
|
+
|
|
79
|
+
// Phase gate enforcement state — deterministic open/close gate per team
|
|
80
|
+
// team -> { gateOpen: boolean, phase: string|null, spawnCount: number, firstBatchFree: boolean }
|
|
81
|
+
const _gateState = new Map();
|
|
82
|
+
|
|
83
|
+
function getGateState(team) {
|
|
84
|
+
if (!_gateState.has(team)) {
|
|
85
|
+
_gateState.set(team, { gateOpen: false, phase: null, spawnCount: 0, firstBatchFree: true, spawnedWorkers: new Set() });
|
|
86
|
+
}
|
|
87
|
+
return _gateState.get(team);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Inbox check enforcement state — tracks which workers have called team_check_inbox
|
|
91
|
+
// "teamName:workerName" -> boolean (true = checked inbox)
|
|
92
|
+
const _inboxCheckState = new Map();
|
|
93
|
+
|
|
94
|
+
// Worker-to-team mapping — used to auto-update roster status on stop/kill
|
|
95
|
+
// workerName -> teamName
|
|
96
|
+
const _workerTeamMap = new Map();
|
|
97
|
+
|
|
98
|
+
// File ownership tracking — maps filePath → { worker, team } for Rule 6 violation detection
|
|
99
|
+
const _fileOwnership = new Map();
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if a caller is a team worker who hasn't checked their inbox yet.
|
|
103
|
+
* Returns an error string if blocked, or null if allowed to proceed.
|
|
104
|
+
*/
|
|
105
|
+
function checkInboxEnforcement(teamName, callerName) {
|
|
106
|
+
if (!callerName) return null;
|
|
107
|
+
const key = teamName + ':' + callerName;
|
|
108
|
+
if (_inboxCheckState.has(key) && !_inboxCheckState.get(key)) {
|
|
109
|
+
return 'BLOCKED: You must call team_check_inbox before using other team tools. This is mandatory step 1 of your workflow.';
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Auto-set a team worker's roster status to idle when their session stops or is killed.
|
|
116
|
+
* Only acts if the worker is a known team worker (exists in _workerTeamMap).
|
|
117
|
+
*/
|
|
118
|
+
function autoSetWorkerIdle(workerName) {
|
|
119
|
+
const teamName = _workerTeamMap.get(workerName);
|
|
120
|
+
if (!teamName) return; // Not a team worker
|
|
121
|
+
try {
|
|
122
|
+
const { teamHub } = getTeamInstances(teamName);
|
|
123
|
+
teamHub.updateMember(workerName, { status: 'idle', task: 'Completed' });
|
|
124
|
+
|
|
125
|
+
// Auto-close gate when all spawned workers from current batch are idle
|
|
126
|
+
const gate = getGateState(teamName);
|
|
127
|
+
if (gate.spawnedWorkers.size > 0 && gate.spawnedWorkers.has(workerName)) {
|
|
128
|
+
const roster = teamHub.getRoster();
|
|
129
|
+
const safeRoster = Array.isArray(roster) ? roster : [];
|
|
130
|
+
const allIdle = [...gate.spawnedWorkers].every(name => {
|
|
131
|
+
const member = safeRoster.find(m => m.name === name);
|
|
132
|
+
return member && member.status === 'idle';
|
|
133
|
+
});
|
|
134
|
+
if (allIdle) {
|
|
135
|
+
gate.gateOpen = false;
|
|
136
|
+
gate.firstBatchFree = false;
|
|
137
|
+
gate.spawnedWorkers = new Set();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch (_) {
|
|
141
|
+
// Best-effort — don't fail the stop/kill if roster update fails
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Build a compact one-line roster summary for a team.
|
|
147
|
+
* Returns e.g. "[Team: worker-a=active, worker-b=idle]" or "" if empty/error.
|
|
148
|
+
*/
|
|
149
|
+
function getRosterSummary(teamName) {
|
|
150
|
+
try {
|
|
151
|
+
const { teamHub } = getTeamInstances(teamName);
|
|
152
|
+
const roster = teamHub.getRoster();
|
|
153
|
+
// Defensive: ensure roster is an array
|
|
154
|
+
if (!Array.isArray(roster) || roster.length === 0) return '';
|
|
155
|
+
const pairs = roster.map(m => m.name + '=' + (m.status || 'unknown'));
|
|
156
|
+
return '\n[Team: ' + pairs.join(', ') + ']';
|
|
157
|
+
} catch (_) {
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get or create Team Hub instances for a given team
|
|
164
|
+
* This function lazily creates the instances on first use
|
|
165
|
+
* and caches them for subsequent calls to the same team
|
|
166
|
+
*
|
|
167
|
+
* @param {string} teamName - Name of the team (default: 'default')
|
|
168
|
+
* @returns {Object} Object with all team instances
|
|
169
|
+
*/
|
|
170
|
+
function getTeamInstances(teamName = 'default') {
|
|
171
|
+
// Validate team name to prevent path traversal attacks
|
|
172
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(teamName)) {
|
|
173
|
+
throw new Error(`Invalid team name "${teamName}": must contain only alphanumeric characters, hyphens, and underscores`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// If team name changed or instances not yet created, recreate them
|
|
177
|
+
if (!teamHub || currentTeamName !== teamName) {
|
|
178
|
+
teamHub = new TeamHub(teamName);
|
|
179
|
+
artifactStore = new ArtifactStore(teamName);
|
|
180
|
+
contractStore = new ContractStore(teamName);
|
|
181
|
+
resolver = new DependencyResolver(teamName);
|
|
182
|
+
lineageGraph = new LineageGraph(teamName);
|
|
183
|
+
pipelineEngine = new PipelineEngine(teamName);
|
|
184
|
+
snapshotEngine = new SnapshotEngine(teamName);
|
|
185
|
+
|
|
186
|
+
// Set pipeline engine reference in resolver (avoid circular dependency)
|
|
187
|
+
resolver.setPipelineEngine(pipelineEngine);
|
|
188
|
+
|
|
189
|
+
// Update the current team name
|
|
190
|
+
currentTeamName = teamName;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { teamHub, artifactStore, contractStore, resolver, lineageGraph, pipelineEngine, snapshotEngine };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// Tool Definitions — these appear in Claude's tool list
|
|
198
|
+
// =============================================================================
|
|
199
|
+
|
|
200
|
+
const TOOLS = [
|
|
201
|
+
// ── Session Management ──────────────────────────────────────────────────
|
|
202
|
+
{
|
|
203
|
+
name: 'spawn_session',
|
|
204
|
+
description:
|
|
205
|
+
'Start a new Claude Code session. Spawns a long-lived streaming process. ' +
|
|
206
|
+
'Optionally sends an initial prompt and returns the response. ' +
|
|
207
|
+
'The session stays alive for follow-up messages via send_message.',
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: {
|
|
211
|
+
name: { type: 'string', description: 'Unique session name (e.g. "fix-auth", "build-login")' },
|
|
212
|
+
prompt: { type: 'string', description: 'Initial prompt to send (optional — if omitted, session starts idle)' },
|
|
213
|
+
model: { type: 'string', enum: ['sonnet', 'opus', 'haiku'], description: 'Model to use (default: sonnet)' },
|
|
214
|
+
work_dir: { type: 'string', description: 'Working directory for the session (default: current directory)' },
|
|
215
|
+
permission_mode:{ type: 'string', enum: ['default', 'acceptEdits', 'bypassPermissions', 'plan'], description: 'Permission mode (default: default)' },
|
|
216
|
+
allowed_tools: { type: 'array', items: { type: 'string' }, description: 'Restrict to specific tools (e.g. ["Read", "Glob", "Grep"])' },
|
|
217
|
+
system_prompt: { type: 'string', description: 'Text to append to system prompt' },
|
|
218
|
+
max_budget: { type: 'number', description: 'Max budget in USD for the session' },
|
|
219
|
+
agent: { type: 'string', description: 'Agent to use for the session' },
|
|
220
|
+
},
|
|
221
|
+
required: ['name'],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
{
|
|
226
|
+
name: 'send_message',
|
|
227
|
+
description:
|
|
228
|
+
'Send a follow-up message to an existing session. ' +
|
|
229
|
+
'The session keeps full conversation context — no restart needed. ' +
|
|
230
|
+
'If the session was stopped, it auto-resumes using the saved session ID.',
|
|
231
|
+
inputSchema: {
|
|
232
|
+
type: 'object',
|
|
233
|
+
properties: {
|
|
234
|
+
name: { type: 'string', description: 'Session name' },
|
|
235
|
+
message: { type: 'string', description: 'Message to send' },
|
|
236
|
+
},
|
|
237
|
+
required: ['name', 'message'],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
{
|
|
242
|
+
name: 'resume_session',
|
|
243
|
+
description:
|
|
244
|
+
'Resume a stopped/paused session. Uses --resume with the saved session ID ' +
|
|
245
|
+
'to restore the full conversation. Optionally sends a message immediately.',
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {
|
|
249
|
+
name: { type: 'string', description: 'Session name to resume' },
|
|
250
|
+
message: { type: 'string', description: 'Optional message to send after resuming' },
|
|
251
|
+
},
|
|
252
|
+
required: ['name'],
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
{
|
|
257
|
+
name: 'pause_session',
|
|
258
|
+
description: 'Pause a session. The process stays alive but no messages are accepted until resumed.',
|
|
259
|
+
inputSchema: {
|
|
260
|
+
type: 'object',
|
|
261
|
+
properties: {
|
|
262
|
+
name: { type: 'string', description: 'Session name to pause' },
|
|
263
|
+
},
|
|
264
|
+
required: ['name'],
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
{
|
|
269
|
+
name: 'fork_session',
|
|
270
|
+
description:
|
|
271
|
+
'Fork a session into a new one. The new session starts with all the parent\'s ' +
|
|
272
|
+
'conversation context. Useful for trying different approaches from the same starting point.',
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: {
|
|
276
|
+
name: { type: 'string', description: 'Source session name to fork from' },
|
|
277
|
+
new_name: { type: 'string', description: 'Name for the new forked session' },
|
|
278
|
+
message: { type: 'string', description: 'Initial message for the fork (default: "Continue from the forked conversation.")' },
|
|
279
|
+
model: { type: 'string', enum: ['sonnet', 'opus', 'haiku'], description: 'Model for the forked session' },
|
|
280
|
+
},
|
|
281
|
+
required: ['name', 'new_name'],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
{
|
|
286
|
+
name: 'stop_session',
|
|
287
|
+
description: 'Gracefully stop a session. Saves state so it can be resumed later with resume_session.',
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: 'object',
|
|
290
|
+
properties: {
|
|
291
|
+
name: { type: 'string', description: 'Session name to stop' },
|
|
292
|
+
},
|
|
293
|
+
required: ['name'],
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
{
|
|
298
|
+
name: 'kill_session',
|
|
299
|
+
description: 'Force kill a session immediately. Use when stop_session is not working.',
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: 'object',
|
|
302
|
+
properties: {
|
|
303
|
+
name: { type: 'string', description: 'Session name to kill' },
|
|
304
|
+
},
|
|
305
|
+
required: ['name'],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
// ── Information ─────────────────────────────────────────────────────────
|
|
310
|
+
{
|
|
311
|
+
name: 'get_session_status',
|
|
312
|
+
description: 'Get detailed status of a session — status, model, cost, turns, interactions.',
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
properties: {
|
|
316
|
+
name: { type: 'string', description: 'Session name' },
|
|
317
|
+
},
|
|
318
|
+
required: ['name'],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
{
|
|
323
|
+
name: 'get_last_output',
|
|
324
|
+
description: 'Get the last response text from a session.',
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: 'object',
|
|
327
|
+
properties: {
|
|
328
|
+
name: { type: 'string', description: 'Session name' },
|
|
329
|
+
},
|
|
330
|
+
required: ['name'],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
{
|
|
335
|
+
name: 'list_sessions',
|
|
336
|
+
description:
|
|
337
|
+
'List all sessions (both alive and stopped). Shows name, status, model, cost, turns. ' +
|
|
338
|
+
'Optionally filter by status.',
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: 'object',
|
|
341
|
+
properties: {
|
|
342
|
+
status: { type: 'string', enum: ['ready', 'paused', 'stopped', 'killed', 'busy'], description: 'Filter by status (optional)' },
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
{
|
|
348
|
+
name: 'get_history',
|
|
349
|
+
description: 'Get full interaction history (all prompts and responses) for a session.',
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: 'object',
|
|
352
|
+
properties: {
|
|
353
|
+
name: { type: 'string', description: 'Session name' },
|
|
354
|
+
},
|
|
355
|
+
required: ['name'],
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
{
|
|
360
|
+
name: 'delete_session',
|
|
361
|
+
description: 'Permanently delete a session and all its data. Cannot be undone.',
|
|
362
|
+
inputSchema: {
|
|
363
|
+
type: 'object',
|
|
364
|
+
properties: {
|
|
365
|
+
name: { type: 'string', description: 'Session name to delete' },
|
|
366
|
+
},
|
|
367
|
+
required: ['name'],
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
// ── Delegate (Control Loop + Safety) ────────────────────────────────────
|
|
372
|
+
{
|
|
373
|
+
name: 'delegate_task',
|
|
374
|
+
description:
|
|
375
|
+
'Smart task delegation with control loop and safety net. ' +
|
|
376
|
+
'Spawns a child session, sends the task, monitors for issues ' +
|
|
377
|
+
'(permission denials, cost overruns, turn limits), and returns structured output. ' +
|
|
378
|
+
'Use this for autonomous task execution with safety guardrails. ' +
|
|
379
|
+
'After delegation, use continue_task to send corrections if the result is not satisfactory. ' +
|
|
380
|
+
'Use for SINGLE isolated tasks. For multi-session projects with 3+ workers, use team_spawn instead.',
|
|
381
|
+
inputSchema: {
|
|
382
|
+
type: 'object',
|
|
383
|
+
properties: {
|
|
384
|
+
name: { type: 'string', description: 'Unique name for this delegated task' },
|
|
385
|
+
task: { type: 'string', description: 'Task description for the child session' },
|
|
386
|
+
model: { type: 'string', enum: ['sonnet', 'opus', 'haiku'], description: 'Model to use (default: sonnet)' },
|
|
387
|
+
preset: { type: 'string', enum: ['read-only', 'review', 'edit', 'full', 'plan'], description: 'Permission preset (default: edit)' },
|
|
388
|
+
max_cost: { type: 'number', description: 'Max cost in USD before auto-kill (default: 2.00)' },
|
|
389
|
+
max_turns: { type: 'number', description: 'Max agent turns before auto-kill (default: 50)' },
|
|
390
|
+
context: { type: 'string', description: 'Extra context to prepend to the task' },
|
|
391
|
+
work_dir: { type: 'string', description: 'Working directory for the child session' },
|
|
392
|
+
system_prompt: { type: 'string', description: 'Text to append to system prompt' },
|
|
393
|
+
agent: { type: 'string', description: 'Agent to use' },
|
|
394
|
+
safety: { type: 'boolean', description: 'Enable safety net (default: true). Set false to disable cost/turn/path limits.' },
|
|
395
|
+
},
|
|
396
|
+
required: ['name', 'task'],
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
{
|
|
401
|
+
name: 'continue_task',
|
|
402
|
+
description:
|
|
403
|
+
'Send a follow-up correction or instruction to a delegated task. ' +
|
|
404
|
+
'The child session retains its full conversation memory — it remembers ' +
|
|
405
|
+
'what it did, what files it touched, and what the original task was. ' +
|
|
406
|
+
'Use this to correct mistakes, request changes, or add requirements.',
|
|
407
|
+
inputSchema: {
|
|
408
|
+
type: 'object',
|
|
409
|
+
properties: {
|
|
410
|
+
name: { type: 'string', description: 'Name of the delegated task to continue' },
|
|
411
|
+
message: { type: 'string', description: 'Follow-up instruction or correction' },
|
|
412
|
+
},
|
|
413
|
+
required: ['name', 'message'],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
{
|
|
418
|
+
name: 'finish_task',
|
|
419
|
+
description: 'Finish a delegated task — stop the session cleanly. Call this when the task result is satisfactory.',
|
|
420
|
+
inputSchema: {
|
|
421
|
+
type: 'object',
|
|
422
|
+
properties: {
|
|
423
|
+
name: { type: 'string', description: 'Name of the delegated task to finish' },
|
|
424
|
+
},
|
|
425
|
+
required: ['name'],
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
{
|
|
430
|
+
name: 'abort_task',
|
|
431
|
+
description: 'Abort a delegated task immediately — force kill the session. Use when the task is going off track and corrections are not working.',
|
|
432
|
+
inputSchema: {
|
|
433
|
+
type: 'object',
|
|
434
|
+
properties: {
|
|
435
|
+
name: { type: 'string', description: 'Name of the delegated task to abort' },
|
|
436
|
+
},
|
|
437
|
+
required: ['name'],
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
// ── Batch ───────────────────────────────────────────────────────────────
|
|
442
|
+
{
|
|
443
|
+
name: 'batch_spawn',
|
|
444
|
+
description:
|
|
445
|
+
'Spawn multiple sessions in parallel. Each session gets its own name, prompt, and model. ' +
|
|
446
|
+
'Returns results for all sessions. Use for parallel task execution.',
|
|
447
|
+
inputSchema: {
|
|
448
|
+
type: 'object',
|
|
449
|
+
properties: {
|
|
450
|
+
sessions: {
|
|
451
|
+
type: 'array',
|
|
452
|
+
description: 'Array of session specs to spawn in parallel',
|
|
453
|
+
items: {
|
|
454
|
+
type: 'object',
|
|
455
|
+
properties: {
|
|
456
|
+
name: { type: 'string', description: 'Session name' },
|
|
457
|
+
prompt: { type: 'string', description: 'Initial prompt' },
|
|
458
|
+
model: { type: 'string', description: 'Model to use' },
|
|
459
|
+
},
|
|
460
|
+
required: ['name', 'prompt'],
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
required: ['sessions'],
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
// ── Orchestrator Guide ──────────────────────────────────────────────────
|
|
469
|
+
{
|
|
470
|
+
name: 'get_orchestrator_guide',
|
|
471
|
+
description:
|
|
472
|
+
'Get the multi-session orchestration guide. Call this when you need to orchestrate ' +
|
|
473
|
+
'a complex multi-session project and want instructions on how to spawn workers, ' +
|
|
474
|
+
'set up dependencies, and monitor progress.',
|
|
475
|
+
inputSchema: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
properties: {
|
|
478
|
+
section: {
|
|
479
|
+
type: 'string',
|
|
480
|
+
enum: ['full', 'quick-start', 'team-spawn', 'delegate', 'monitoring', 'when-to-use'],
|
|
481
|
+
description: 'Which section of the guide to return (default: full)',
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
// ── Server Version ─────────────────────────────────────────────────────
|
|
488
|
+
{
|
|
489
|
+
name: 'server_version',
|
|
490
|
+
description:
|
|
491
|
+
'Check the running MCP server version and detect staleness. ' +
|
|
492
|
+
'Call this if tools seem missing or behave unexpectedly.',
|
|
493
|
+
inputSchema: {
|
|
494
|
+
type: 'object',
|
|
495
|
+
properties: {},
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
|
|
499
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
500
|
+
// TEAM HUB v2 — 32 new tools for team collaboration
|
|
501
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
502
|
+
|
|
503
|
+
// ── Layer 1: Team Chat (8 tools) ────────────────────────────────────────
|
|
504
|
+
{
|
|
505
|
+
name: 'team_spawn',
|
|
506
|
+
description:
|
|
507
|
+
'Spawn a session with team system prompt injected. The session joins the team roster and ' +
|
|
508
|
+
'receives context about teammates, communication tools, and collaboration rules. ' +
|
|
509
|
+
'Spawn ALL independent workers in a SINGLE message. Workers coordinate directly via team_ask — do NOT relay messages.',
|
|
510
|
+
inputSchema: {
|
|
511
|
+
type: 'object',
|
|
512
|
+
properties: {
|
|
513
|
+
name: { type: 'string', description: 'Unique session name (e.g. "db-dev", "api-dev")' },
|
|
514
|
+
prompt: { type: 'string', description: 'Initial prompt to send' },
|
|
515
|
+
role: { type: 'string', description: 'Role in the team (e.g. "database", "backend", "QA")' },
|
|
516
|
+
task: { type: 'string', description: 'Current task description' },
|
|
517
|
+
model: { type: 'string', enum: ['sonnet', 'opus', 'haiku'], description: 'Model to use (default: sonnet)' },
|
|
518
|
+
permission_mode: { type: 'string', enum: ['default', 'acceptEdits', 'bypassPermissions', 'plan'], description: 'Permission mode. Use bypassPermissions to allow sessions to write files without approval (default: bypassPermissions)' },
|
|
519
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
520
|
+
work_dir: { type: 'string', description: 'Working directory for the session (default: current directory)' },
|
|
521
|
+
},
|
|
522
|
+
required: ['name', 'prompt'],
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
{
|
|
527
|
+
name: 'team_send_message',
|
|
528
|
+
description:
|
|
529
|
+
'Send a direct message to a specific teammate. The message appears in their inbox.',
|
|
530
|
+
inputSchema: {
|
|
531
|
+
type: 'object',
|
|
532
|
+
properties: {
|
|
533
|
+
from: { type: 'string', description: 'Sender session name' },
|
|
534
|
+
to: { type: 'string', description: 'Recipient session name' },
|
|
535
|
+
content: { type: 'string', description: 'Message content' },
|
|
536
|
+
priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'], description: 'Message priority (default: normal)' },
|
|
537
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
538
|
+
},
|
|
539
|
+
required: ['from', 'to', 'content'],
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
{
|
|
544
|
+
name: 'team_broadcast',
|
|
545
|
+
description:
|
|
546
|
+
'Send a message to all team members. Appears in everyone\'s inbox.',
|
|
547
|
+
inputSchema: {
|
|
548
|
+
type: 'object',
|
|
549
|
+
properties: {
|
|
550
|
+
from: { type: 'string', description: 'Sender session name' },
|
|
551
|
+
content: { type: 'string', description: 'Message content' },
|
|
552
|
+
priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'], description: 'Message priority (default: normal)' },
|
|
553
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
554
|
+
},
|
|
555
|
+
required: ['from', 'content'],
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
{
|
|
560
|
+
name: 'team_check_inbox',
|
|
561
|
+
description:
|
|
562
|
+
'Check inbox for unread messages. Sessions should check their inbox before starting work ' +
|
|
563
|
+
'and after major steps to stay coordinated with the team.',
|
|
564
|
+
inputSchema: {
|
|
565
|
+
type: 'object',
|
|
566
|
+
properties: {
|
|
567
|
+
name: { type: 'string', description: 'Session name whose inbox to check' },
|
|
568
|
+
mark_read: { type: 'boolean', description: 'Mark messages as read after retrieving (default: true)' },
|
|
569
|
+
limit: { type: 'number', description: 'Max messages to return (default: 20)' },
|
|
570
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
571
|
+
},
|
|
572
|
+
required: ['name'],
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
{
|
|
577
|
+
name: 'team_ask',
|
|
578
|
+
description:
|
|
579
|
+
'Ask a teammate a question and WAIT for their reply. This is a blocking operation that polls ' +
|
|
580
|
+
'until the recipient replies or the timeout expires.',
|
|
581
|
+
inputSchema: {
|
|
582
|
+
type: 'object',
|
|
583
|
+
properties: {
|
|
584
|
+
from: { type: 'string', description: 'Sender session name' },
|
|
585
|
+
to: { type: 'string', description: 'Recipient session name' },
|
|
586
|
+
question: { type: 'string', description: 'Question to ask' },
|
|
587
|
+
timeout: { type: 'number', description: 'Timeout in milliseconds (default: 60000 = 1 minute)' },
|
|
588
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
589
|
+
},
|
|
590
|
+
required: ['from', 'to', 'question'],
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
{
|
|
595
|
+
name: 'team_reply',
|
|
596
|
+
description:
|
|
597
|
+
'Reply to a question you received. Use the message_id from the ask message in your inbox.',
|
|
598
|
+
inputSchema: {
|
|
599
|
+
type: 'object',
|
|
600
|
+
properties: {
|
|
601
|
+
from: { type: 'string', description: 'Your session name' },
|
|
602
|
+
message_id: { type: 'string', description: 'ID of the ask message you\'re replying to' },
|
|
603
|
+
answer: { type: 'string', description: 'Your answer to the question' },
|
|
604
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
605
|
+
},
|
|
606
|
+
required: ['from', 'message_id', 'answer'],
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
{
|
|
611
|
+
name: 'team_roster',
|
|
612
|
+
description:
|
|
613
|
+
'View the current team roster with all members, their roles, status, and lastSeen times.',
|
|
614
|
+
inputSchema: {
|
|
615
|
+
type: 'object',
|
|
616
|
+
properties: {
|
|
617
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
{
|
|
623
|
+
name: 'team_update_status',
|
|
624
|
+
description:
|
|
625
|
+
'Update your own status in the team roster. Use this to keep teammates informed of what you\'re doing.',
|
|
626
|
+
inputSchema: {
|
|
627
|
+
type: 'object',
|
|
628
|
+
properties: {
|
|
629
|
+
name: { type: 'string', description: 'Your session name' },
|
|
630
|
+
status: { type: 'string', enum: ['active', 'busy', 'idle', 'offline'], description: 'Your current status' },
|
|
631
|
+
task: { type: 'string', description: 'What you\'re currently working on' },
|
|
632
|
+
role: { type: 'string', description: 'Your role (if it changed)' },
|
|
633
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
634
|
+
},
|
|
635
|
+
required: ['name'],
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
// ── Layer 2: Artifacts (4 tools) ─────────────────────────────────────────
|
|
640
|
+
{
|
|
641
|
+
name: 'artifact_publish',
|
|
642
|
+
description:
|
|
643
|
+
'Publish a structured artifact (versioned, immutable). This is how sessions exchange structured ' +
|
|
644
|
+
'data like API contracts, schemas, test results. Publishing triggers dependency resolution and pipelines.',
|
|
645
|
+
inputSchema: {
|
|
646
|
+
type: 'object',
|
|
647
|
+
properties: {
|
|
648
|
+
artifactId: { type: 'string', description: 'Unique artifact ID (e.g. "api-contract-user-auth")' },
|
|
649
|
+
type: { type: 'string', description: 'Artifact type (e.g. "api-contract", "schema-change", "test-results", "custom")' },
|
|
650
|
+
name: { type: 'string', description: 'Human-readable name' },
|
|
651
|
+
data: { type: 'object', description: 'The artifact data payload (validated against schema for well-known types)' },
|
|
652
|
+
summary: { type: 'string', description: 'Brief summary of this version' },
|
|
653
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags for filtering (e.g. ["auth", "v1"])' },
|
|
654
|
+
publisher: { type: 'string', description: 'Session name publishing this artifact' },
|
|
655
|
+
derivedFrom: { type: 'array', items: { type: 'string' }, description: 'Array of artifact IDs this was derived from (for lineage)' },
|
|
656
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
657
|
+
},
|
|
658
|
+
required: ['artifactId', 'type', 'name', 'data'],
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
|
|
662
|
+
{
|
|
663
|
+
name: 'artifact_get',
|
|
664
|
+
description:
|
|
665
|
+
'Read an artifact (latest version or specific version). ' +
|
|
666
|
+
'IMPORTANT: Pass your session name as the "reader" parameter to track artifact consumption. ' +
|
|
667
|
+
'The orchestrator uses this to verify workers actually read shared data.',
|
|
668
|
+
inputSchema: {
|
|
669
|
+
type: 'object',
|
|
670
|
+
properties: {
|
|
671
|
+
artifactId: { type: 'string', description: 'Artifact ID to retrieve' },
|
|
672
|
+
version: { type: 'number', description: 'Specific version number (omit for latest)' },
|
|
673
|
+
reader: { type: 'string', description: 'Session name reading this artifact (for tracking who consumed it)' },
|
|
674
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
675
|
+
},
|
|
676
|
+
required: ['artifactId'],
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
|
|
680
|
+
{
|
|
681
|
+
name: 'artifact_list',
|
|
682
|
+
description:
|
|
683
|
+
'List all artifacts with optional filters by type, publisher, or tag.',
|
|
684
|
+
inputSchema: {
|
|
685
|
+
type: 'object',
|
|
686
|
+
properties: {
|
|
687
|
+
type: { type: 'string', description: 'Filter by artifact type' },
|
|
688
|
+
publisher: { type: 'string', description: 'Filter by publisher session name' },
|
|
689
|
+
tag: { type: 'string', description: 'Filter by tag' },
|
|
690
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
|
|
695
|
+
{
|
|
696
|
+
name: 'artifact_readers',
|
|
697
|
+
description:
|
|
698
|
+
'Check who has read a specific artifact. Use this to verify that workers actually consumed shared artifacts like conventions or schemas.',
|
|
699
|
+
inputSchema: {
|
|
700
|
+
type: 'object',
|
|
701
|
+
properties: {
|
|
702
|
+
artifactId: { type: 'string', description: 'Artifact ID to check readers for' },
|
|
703
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
704
|
+
},
|
|
705
|
+
required: ['artifactId'],
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
{
|
|
710
|
+
name: 'artifact_history',
|
|
711
|
+
description:
|
|
712
|
+
'View the version history of an artifact.',
|
|
713
|
+
inputSchema: {
|
|
714
|
+
type: 'object',
|
|
715
|
+
properties: {
|
|
716
|
+
artifactId: { type: 'string', description: 'Artifact ID' },
|
|
717
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
718
|
+
},
|
|
719
|
+
required: ['artifactId'],
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
|
|
723
|
+
// ── Layer 2: Contracts (8 tools) ─────────────────────────────────────────
|
|
724
|
+
{
|
|
725
|
+
name: 'contract_create',
|
|
726
|
+
description:
|
|
727
|
+
'Create a task contract and assign it to ANY teammate (peer-to-peer). Contracts define work with ' +
|
|
728
|
+
'inputs, outputs, dependencies, and acceptance criteria. The dependency resolver automatically advances ' +
|
|
729
|
+
'contracts when their dependencies are satisfied.',
|
|
730
|
+
inputSchema: {
|
|
731
|
+
type: 'object',
|
|
732
|
+
properties: {
|
|
733
|
+
contractId: { type: 'string', description: 'Unique contract ID (e.g. "build-auth-api")' },
|
|
734
|
+
title: { type: 'string', description: 'Human-readable title' },
|
|
735
|
+
assignee: { type: 'string', description: 'Session name who will do the work' },
|
|
736
|
+
assigner: { type: 'string', description: 'Session name who created the contract' },
|
|
737
|
+
description: { type: 'string', description: 'Detailed description of the work' },
|
|
738
|
+
inputs: { type: 'object', description: 'Input artifacts and context for the assignee' },
|
|
739
|
+
expectedOutputs: { type: 'array', description: 'What the assignee should produce' },
|
|
740
|
+
dependencies: { type: 'array', description: 'Contract/artifact dependencies that must be satisfied first' },
|
|
741
|
+
acceptanceCriteria: { type: 'array', description: 'How we know the work is done' },
|
|
742
|
+
autoComplete: { type: 'boolean', description: 'Auto-complete when criteria met (default: true)' },
|
|
743
|
+
timeoutMs: { type: 'number', description: 'Timeout in milliseconds (null = no timeout)' },
|
|
744
|
+
maxRetries: { type: 'number', description: 'Maximum reopen attempts (default: 3)' },
|
|
745
|
+
priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'], description: 'Priority level (default: normal)' },
|
|
746
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
747
|
+
},
|
|
748
|
+
required: ['contractId', 'title', 'assignee', 'assigner'],
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
{
|
|
753
|
+
name: 'contract_start',
|
|
754
|
+
description:
|
|
755
|
+
'Start working on a contract. Transitions status from ready to in_progress and returns the contract ' +
|
|
756
|
+
'details with resolved input artifacts.',
|
|
757
|
+
inputSchema: {
|
|
758
|
+
type: 'object',
|
|
759
|
+
properties: {
|
|
760
|
+
contractId: { type: 'string', description: 'Contract ID to start' },
|
|
761
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
762
|
+
},
|
|
763
|
+
required: ['contractId'],
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
{
|
|
768
|
+
name: 'contract_complete',
|
|
769
|
+
description:
|
|
770
|
+
'Mark a contract as completed. This triggers dependency resolution for downstream contracts and ' +
|
|
771
|
+
'evaluates reactive pipelines.',
|
|
772
|
+
inputSchema: {
|
|
773
|
+
type: 'object',
|
|
774
|
+
properties: {
|
|
775
|
+
contractId: { type: 'string', description: 'Contract ID to complete' },
|
|
776
|
+
summary: { type: 'string', description: 'Summary of what was done' },
|
|
777
|
+
publishedArtifacts: { type: 'array', items: { type: 'string' }, description: 'Artifact IDs that were published' },
|
|
778
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
779
|
+
},
|
|
780
|
+
required: ['contractId'],
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
|
|
784
|
+
{
|
|
785
|
+
name: 'contract_fail',
|
|
786
|
+
description:
|
|
787
|
+
'Mark a contract as failed. Notifies the assigner and triggers dependency resolution.',
|
|
788
|
+
inputSchema: {
|
|
789
|
+
type: 'object',
|
|
790
|
+
properties: {
|
|
791
|
+
contractId: { type: 'string', description: 'Contract ID to fail' },
|
|
792
|
+
reason: { type: 'string', description: 'Why the contract failed' },
|
|
793
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
794
|
+
},
|
|
795
|
+
required: ['contractId', 'reason'],
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
|
|
799
|
+
{
|
|
800
|
+
name: 'contract_reopen',
|
|
801
|
+
description:
|
|
802
|
+
'Reopen a failed or completed contract for retry. Increments retryCount and transitions back to ready. ' +
|
|
803
|
+
'Rejects if retryCount >= maxRetries.',
|
|
804
|
+
inputSchema: {
|
|
805
|
+
type: 'object',
|
|
806
|
+
properties: {
|
|
807
|
+
contractId: { type: 'string', description: 'Contract ID to reopen' },
|
|
808
|
+
reason: { type: 'string', description: 'Why we are reopening' },
|
|
809
|
+
newInputs: { type: 'object', description: 'Optional updated inputs' },
|
|
810
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
811
|
+
},
|
|
812
|
+
required: ['contractId'],
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
|
|
816
|
+
{
|
|
817
|
+
name: 'contract_get',
|
|
818
|
+
description:
|
|
819
|
+
'Get full details of a contract including status, inputs, outputs, dependencies, and acceptance criteria.',
|
|
820
|
+
inputSchema: {
|
|
821
|
+
type: 'object',
|
|
822
|
+
properties: {
|
|
823
|
+
contractId: { type: 'string', description: 'Contract ID' },
|
|
824
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
825
|
+
},
|
|
826
|
+
required: ['contractId'],
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
|
|
830
|
+
{
|
|
831
|
+
name: 'contract_list',
|
|
832
|
+
description:
|
|
833
|
+
'List all contracts with optional filters by status, assignee, or assigner.',
|
|
834
|
+
inputSchema: {
|
|
835
|
+
type: 'object',
|
|
836
|
+
properties: {
|
|
837
|
+
status: { type: 'string', enum: ['pending', 'ready', 'in_progress', 'completed', 'failed'], description: 'Filter by status' },
|
|
838
|
+
assignee: { type: 'string', description: 'Filter by assignee session name' },
|
|
839
|
+
assigner: { type: 'string', description: 'Filter by assigner session name' },
|
|
840
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
{
|
|
846
|
+
name: 'contract_reassign',
|
|
847
|
+
description:
|
|
848
|
+
'Reassign a contract to a different session.',
|
|
849
|
+
inputSchema: {
|
|
850
|
+
type: 'object',
|
|
851
|
+
properties: {
|
|
852
|
+
contractId: { type: 'string', description: 'Contract ID' },
|
|
853
|
+
newAssignee: { type: 'string', description: 'New assignee session name' },
|
|
854
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
855
|
+
},
|
|
856
|
+
required: ['contractId', 'newAssignee'],
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
|
|
860
|
+
// ── Layer 3: Lineage (4 tools) ───────────────────────────────────────────
|
|
861
|
+
{
|
|
862
|
+
name: 'artifact_lineage',
|
|
863
|
+
description:
|
|
864
|
+
'Query the artifact lineage graph. Upstream shows what this was derived from. Downstream shows what ' +
|
|
865
|
+
'depends on this. Returns the full chain recursively.',
|
|
866
|
+
inputSchema: {
|
|
867
|
+
type: 'object',
|
|
868
|
+
properties: {
|
|
869
|
+
artifactId: { type: 'string', description: 'Artifact ID' },
|
|
870
|
+
direction: { type: 'string', enum: ['upstream', 'downstream'], description: 'Which direction to traverse' },
|
|
871
|
+
version: { type: 'number', description: 'Specific version (omit for latest)' },
|
|
872
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
873
|
+
},
|
|
874
|
+
required: ['artifactId', 'direction'],
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
|
|
878
|
+
{
|
|
879
|
+
name: 'artifact_impact',
|
|
880
|
+
description:
|
|
881
|
+
'Analyze the impact radius: "If I change this artifact, what downstream artifacts and contracts break?" ' +
|
|
882
|
+
'Returns all affected artifacts and contracts with impact depth.',
|
|
883
|
+
inputSchema: {
|
|
884
|
+
type: 'object',
|
|
885
|
+
properties: {
|
|
886
|
+
artifactId: { type: 'string', description: 'Artifact ID to analyze' },
|
|
887
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
888
|
+
},
|
|
889
|
+
required: ['artifactId'],
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
|
|
893
|
+
{
|
|
894
|
+
name: 'artifact_stale',
|
|
895
|
+
description:
|
|
896
|
+
'Find all stale artifacts — artifacts derived from sources that have been updated to newer versions. ' +
|
|
897
|
+
'This helps identify what needs to be regenerated after changes.',
|
|
898
|
+
inputSchema: {
|
|
899
|
+
type: 'object',
|
|
900
|
+
properties: {
|
|
901
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
902
|
+
},
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
|
|
906
|
+
{
|
|
907
|
+
name: 'team_audit',
|
|
908
|
+
description:
|
|
909
|
+
'Get the full audit trail for an artifact: who created it, from what inputs, under which contract, ' +
|
|
910
|
+
'at what time. Follows the provenance chain upstream to the root.',
|
|
911
|
+
inputSchema: {
|
|
912
|
+
type: 'object',
|
|
913
|
+
properties: {
|
|
914
|
+
artifactId: { type: 'string', description: 'Artifact ID' },
|
|
915
|
+
version: { type: 'number', description: 'Specific version (omit for latest)' },
|
|
916
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
917
|
+
},
|
|
918
|
+
required: ['artifactId'],
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
|
|
922
|
+
// ── Layer 3: Pipelines (4 tools) ─────────────────────────────────────────
|
|
923
|
+
{
|
|
924
|
+
name: 'pipeline_create',
|
|
925
|
+
description:
|
|
926
|
+
'Create a reactive pipeline: "When X happens, automatically do Y." Pipelines contain trigger/condition/action ' +
|
|
927
|
+
'rules that fire when artifacts are published or contracts change status. This enables self-healing CI loops.',
|
|
928
|
+
inputSchema: {
|
|
929
|
+
type: 'object',
|
|
930
|
+
properties: {
|
|
931
|
+
pipelineId: { type: 'string', description: 'Unique pipeline ID (e.g. "ci-loop")' },
|
|
932
|
+
rules: { type: 'array', description: 'Array of trigger/condition/action rules' },
|
|
933
|
+
owner: { type: 'string', description: 'Session name who owns this pipeline' },
|
|
934
|
+
enabled: { type: 'boolean', description: 'Whether pipeline is active (default: true)' },
|
|
935
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
936
|
+
},
|
|
937
|
+
required: ['pipelineId', 'rules'],
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
|
|
941
|
+
{
|
|
942
|
+
name: 'pipeline_list',
|
|
943
|
+
description:
|
|
944
|
+
'List all pipelines with optional filter by owner. Shows execution counts and last fired times.',
|
|
945
|
+
inputSchema: {
|
|
946
|
+
type: 'object',
|
|
947
|
+
properties: {
|
|
948
|
+
owner: { type: 'string', description: 'Filter by owner session name' },
|
|
949
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
|
|
954
|
+
{
|
|
955
|
+
name: 'pipeline_pause',
|
|
956
|
+
description:
|
|
957
|
+
'Pause a pipeline (disable without deleting). The pipeline stops evaluating rules.',
|
|
958
|
+
inputSchema: {
|
|
959
|
+
type: 'object',
|
|
960
|
+
properties: {
|
|
961
|
+
pipelineId: { type: 'string', description: 'Pipeline ID to pause' },
|
|
962
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
963
|
+
},
|
|
964
|
+
required: ['pipelineId'],
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
|
|
968
|
+
{
|
|
969
|
+
name: 'pipeline_resume',
|
|
970
|
+
description:
|
|
971
|
+
'Resume a paused pipeline. It will start evaluating rules again on new events.',
|
|
972
|
+
inputSchema: {
|
|
973
|
+
type: 'object',
|
|
974
|
+
properties: {
|
|
975
|
+
pipelineId: { type: 'string', description: 'Pipeline ID to resume' },
|
|
976
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
977
|
+
},
|
|
978
|
+
required: ['pipelineId'],
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
|
|
982
|
+
// ── Layer 3: Snapshots (4 tools) ─────────────────────────────────────────
|
|
983
|
+
{
|
|
984
|
+
name: 'team_snapshot',
|
|
985
|
+
description:
|
|
986
|
+
'Capture a snapshot of the entire team state (contracts, artifacts, pipelines). Use this before ' +
|
|
987
|
+
'risky operations or at major milestones.',
|
|
988
|
+
inputSchema: {
|
|
989
|
+
type: 'object',
|
|
990
|
+
properties: {
|
|
991
|
+
snapshotId: { type: 'string', description: 'Unique snapshot ID (e.g. "pre-work", "all-complete")' },
|
|
992
|
+
label: { type: 'string', description: 'Short label for this snapshot' },
|
|
993
|
+
description: { type: 'string', description: 'Longer description' },
|
|
994
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
995
|
+
},
|
|
996
|
+
required: ['snapshotId'],
|
|
997
|
+
},
|
|
998
|
+
},
|
|
999
|
+
|
|
1000
|
+
{
|
|
1001
|
+
name: 'team_snapshot_list',
|
|
1002
|
+
description:
|
|
1003
|
+
'List all snapshots with timestamp, label, and summary of what was captured.',
|
|
1004
|
+
inputSchema: {
|
|
1005
|
+
type: 'object',
|
|
1006
|
+
properties: {
|
|
1007
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
1008
|
+
},
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
|
|
1012
|
+
{
|
|
1013
|
+
name: 'team_rollback',
|
|
1014
|
+
description:
|
|
1015
|
+
'Rollback to a previous snapshot. Resets contract and pipeline state. Artifact version files are ' +
|
|
1016
|
+
'preserved on disk but can be marked as rolled-back in the index.',
|
|
1017
|
+
inputSchema: {
|
|
1018
|
+
type: 'object',
|
|
1019
|
+
properties: {
|
|
1020
|
+
snapshotId: { type: 'string', description: 'Snapshot ID to rollback to' },
|
|
1021
|
+
preserveArtifacts: { type: 'boolean', description: 'Keep artifact index entries (default: true)' },
|
|
1022
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
1023
|
+
},
|
|
1024
|
+
required: ['snapshotId'],
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
|
|
1028
|
+
{
|
|
1029
|
+
name: 'team_replay',
|
|
1030
|
+
description:
|
|
1031
|
+
'Replay from a snapshot with optional overrides to contract inputs. Rollbacks to the snapshot, ' +
|
|
1032
|
+
'applies modifications, then re-triggers dependency resolution. Use this to test "what if" scenarios.',
|
|
1033
|
+
inputSchema: {
|
|
1034
|
+
type: 'object',
|
|
1035
|
+
properties: {
|
|
1036
|
+
snapshotId: { type: 'string', description: 'Snapshot ID to replay from' },
|
|
1037
|
+
overrides: { type: 'object', description: 'Map of contractId -> { inputs: {...} } to override' },
|
|
1038
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
1039
|
+
},
|
|
1040
|
+
required: ['snapshotId'],
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
// ── Phase Gate & Team Reset ─────────────────────────────────────────
|
|
1045
|
+
{
|
|
1046
|
+
name: 'phase_gate',
|
|
1047
|
+
description:
|
|
1048
|
+
'Run all 4 phase gate checks in a single call. Verifies: (1) expected artifacts exist, ' +
|
|
1049
|
+
'(2) artifact content is valid, (3) all previous-phase workers are idle, (4) expected consumers read artifacts. ' +
|
|
1050
|
+
'Returns a structured pass/fail report. Use this BETWEEN every pair of phases.',
|
|
1051
|
+
inputSchema: {
|
|
1052
|
+
type: 'object',
|
|
1053
|
+
properties: {
|
|
1054
|
+
phase_completing: { type: 'string', description: 'Name of the phase that just completed (e.g. "Phase 0: Foundation")' },
|
|
1055
|
+
phase_starting: { type: 'string', description: 'Name of the phase about to start (e.g. "Phase 1: Routes")' },
|
|
1056
|
+
expected_artifacts: { type: 'array', items: { type: 'string' }, description: 'Artifact IDs that should exist before proceeding' },
|
|
1057
|
+
expected_idle: { type: 'array', items: { type: 'string' }, description: 'Worker names that should be idle (optional — if omitted, checks ALL roster members)' },
|
|
1058
|
+
expected_readers: { type: 'object', description: 'Map of artifactId -> array of expected reader names. E.g. {"shared-conventions": ["api-dev", "db-dev"]}' },
|
|
1059
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
1060
|
+
},
|
|
1061
|
+
required: ['phase_completing', 'phase_starting', 'expected_artifacts'],
|
|
1062
|
+
},
|
|
1063
|
+
},
|
|
1064
|
+
|
|
1065
|
+
{
|
|
1066
|
+
name: 'team_reset',
|
|
1067
|
+
description:
|
|
1068
|
+
'Reset all team state — clear artifacts, contracts, roster, messages. ' +
|
|
1069
|
+
'Use this between orchestration runs to start fresh. Optionally preserve specific artifacts.',
|
|
1070
|
+
inputSchema: {
|
|
1071
|
+
type: 'object',
|
|
1072
|
+
properties: {
|
|
1073
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
1074
|
+
preserve_artifacts: { type: 'array', items: { type: 'string' }, description: 'Artifact IDs to keep (optional)' },
|
|
1075
|
+
confirm: { type: 'boolean', description: 'Must be true to execute (safety check)' },
|
|
1076
|
+
},
|
|
1077
|
+
required: ['confirm'],
|
|
1078
|
+
},
|
|
1079
|
+
},
|
|
1080
|
+
|
|
1081
|
+
// ── File Ownership (Rule 6 enforcement) ────────────────────────────────
|
|
1082
|
+
{
|
|
1083
|
+
name: 'register_files',
|
|
1084
|
+
description:
|
|
1085
|
+
'Register file ownership for a worker. Call this after creating or modifying files ' +
|
|
1086
|
+
'so the system can track who owns which files and warn about Rule 6 violations.',
|
|
1087
|
+
inputSchema: {
|
|
1088
|
+
type: 'object',
|
|
1089
|
+
properties: {
|
|
1090
|
+
worker: { type: 'string', description: 'Worker session name that owns these files' },
|
|
1091
|
+
files: { type: 'array', items: { type: 'string' }, description: 'Array of file paths to register' },
|
|
1092
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
1093
|
+
},
|
|
1094
|
+
required: ['worker', 'files'],
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
|
|
1098
|
+
{
|
|
1099
|
+
name: 'check_file_owner',
|
|
1100
|
+
description:
|
|
1101
|
+
'Check who owns a file. Use this before directly editing a file to verify ' +
|
|
1102
|
+
'whether it belongs to a worker (Rule 6: don\'t fix worker code yourself).',
|
|
1103
|
+
inputSchema: {
|
|
1104
|
+
type: 'object',
|
|
1105
|
+
properties: {
|
|
1106
|
+
file: { type: 'string', description: 'File path to check ownership of' },
|
|
1107
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
1108
|
+
},
|
|
1109
|
+
required: ['file'],
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
|
|
1113
|
+
// ── Session Continuity (Layer 0) ──────────────────────────────────────
|
|
1114
|
+
{
|
|
1115
|
+
name: 'continuity_snapshot',
|
|
1116
|
+
description: 'Capture a lightweight snapshot of the current project state (git state, file tree, working context). Use on session end.',
|
|
1117
|
+
inputSchema: {
|
|
1118
|
+
type: 'object',
|
|
1119
|
+
properties: {
|
|
1120
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' },
|
|
1121
|
+
sessionName: { type: 'string', description: 'Name of the session capturing this snapshot' },
|
|
1122
|
+
taskSummary: { type: 'string', description: 'What you were working on' },
|
|
1123
|
+
activeFiles: { type: 'array', items: { type: 'string' }, description: 'Files read/edited this session' },
|
|
1124
|
+
openQuestions: { type: 'array', items: { type: 'string' }, description: 'Unresolved questions' }
|
|
1125
|
+
},
|
|
1126
|
+
required: ['projectPath', 'sessionName']
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
name: 'continuity_briefing',
|
|
1131
|
+
description: 'Generate a diff-aware session briefing. Shows what changed since last session, previous context, decisions, patterns. Use on session start.',
|
|
1132
|
+
inputSchema: {
|
|
1133
|
+
type: 'object',
|
|
1134
|
+
properties: {
|
|
1135
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' },
|
|
1136
|
+
maxTokens: { type: 'number', description: 'Max briefing length in characters (default: 4000)' },
|
|
1137
|
+
includeDecisions: { type: 'boolean', description: 'Include recent decisions (default: true)' },
|
|
1138
|
+
includePatterns: { type: 'boolean', description: 'Include pattern rules (default: true)' }
|
|
1139
|
+
},
|
|
1140
|
+
required: ['projectPath']
|
|
1141
|
+
}
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
name: 'continuity_status',
|
|
1145
|
+
description: 'Quick status check: what changed since the last snapshot. Lighter than a full briefing.',
|
|
1146
|
+
inputSchema: {
|
|
1147
|
+
type: 'object',
|
|
1148
|
+
properties: {
|
|
1149
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' }
|
|
1150
|
+
},
|
|
1151
|
+
required: ['projectPath']
|
|
1152
|
+
}
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
name: 'continuity_diff',
|
|
1156
|
+
description: 'Detailed diff between two snapshots or between latest snapshot and current state.',
|
|
1157
|
+
inputSchema: {
|
|
1158
|
+
type: 'object',
|
|
1159
|
+
properties: {
|
|
1160
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' },
|
|
1161
|
+
fromSnapshot: { type: 'string', description: 'Source snapshot ID (default: latest)' },
|
|
1162
|
+
toSnapshot: { type: 'string', description: 'Target snapshot ID (default: current state)' }
|
|
1163
|
+
},
|
|
1164
|
+
required: ['projectPath']
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
name: 'continuity_history',
|
|
1169
|
+
description: 'List all snapshots for a project, newest first.',
|
|
1170
|
+
inputSchema: {
|
|
1171
|
+
type: 'object',
|
|
1172
|
+
properties: {
|
|
1173
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' },
|
|
1174
|
+
limit: { type: 'number', description: 'Max snapshots to return (default: 20)' }
|
|
1175
|
+
},
|
|
1176
|
+
required: ['projectPath']
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
name: 'continuity_stale_check',
|
|
1181
|
+
description: 'Check if your previous session context is stale. Shows warnings about changed files, branch switches, etc.',
|
|
1182
|
+
inputSchema: {
|
|
1183
|
+
type: 'object',
|
|
1184
|
+
properties: {
|
|
1185
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' }
|
|
1186
|
+
},
|
|
1187
|
+
required: ['projectPath']
|
|
1188
|
+
}
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
name: 'decision_add',
|
|
1192
|
+
description: 'Record an architectural or design decision in the project decision journal.',
|
|
1193
|
+
inputSchema: {
|
|
1194
|
+
type: 'object',
|
|
1195
|
+
properties: {
|
|
1196
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' },
|
|
1197
|
+
decision: { type: 'string', description: 'The decision made (e.g., "Use bcrypt over argon2")' },
|
|
1198
|
+
reason: { type: 'string', description: 'Why this decision was made' },
|
|
1199
|
+
files: { type: 'array', items: { type: 'string' }, description: 'Related file paths' },
|
|
1200
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags for categorization' }
|
|
1201
|
+
},
|
|
1202
|
+
required: ['projectPath', 'decision']
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
name: 'decision_list',
|
|
1207
|
+
description: 'List recent decisions from the project decision journal.',
|
|
1208
|
+
inputSchema: {
|
|
1209
|
+
type: 'object',
|
|
1210
|
+
properties: {
|
|
1211
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' },
|
|
1212
|
+
limit: { type: 'number', description: 'Max decisions to return (default: 50)' },
|
|
1213
|
+
tag: { type: 'string', description: 'Filter by tag' }
|
|
1214
|
+
},
|
|
1215
|
+
required: ['projectPath']
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
name: 'decision_search',
|
|
1220
|
+
description: 'Search decisions by keyword across decision text, reasoning, and context.',
|
|
1221
|
+
inputSchema: {
|
|
1222
|
+
type: 'object',
|
|
1223
|
+
properties: {
|
|
1224
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' },
|
|
1225
|
+
keyword: { type: 'string', description: 'Search keyword (case-insensitive)' }
|
|
1226
|
+
},
|
|
1227
|
+
required: ['projectPath', 'keyword']
|
|
1228
|
+
}
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
name: 'pattern_add',
|
|
1232
|
+
description: 'Add a coding pattern rule. Patterns are corrections that persist across sessions (e.g., "Always use enum constants for status comparisons").',
|
|
1233
|
+
inputSchema: {
|
|
1234
|
+
type: 'object',
|
|
1235
|
+
properties: {
|
|
1236
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' },
|
|
1237
|
+
rule: { type: 'string', description: 'The pattern rule' },
|
|
1238
|
+
context: { type: 'string', description: 'Where this pattern applies (e.g., "auth module")' },
|
|
1239
|
+
example: { type: 'string', description: 'Code example or explanation' },
|
|
1240
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags for categorization' }
|
|
1241
|
+
},
|
|
1242
|
+
required: ['projectPath', 'rule']
|
|
1243
|
+
}
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
name: 'pattern_list',
|
|
1247
|
+
description: 'List all coding patterns for a project.',
|
|
1248
|
+
inputSchema: {
|
|
1249
|
+
type: 'object',
|
|
1250
|
+
properties: {
|
|
1251
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' },
|
|
1252
|
+
tag: { type: 'string', description: 'Filter by tag' }
|
|
1253
|
+
},
|
|
1254
|
+
required: ['projectPath']
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
name: 'pattern_remove',
|
|
1259
|
+
description: 'Remove a coding pattern by ID.',
|
|
1260
|
+
inputSchema: {
|
|
1261
|
+
type: 'object',
|
|
1262
|
+
properties: {
|
|
1263
|
+
projectPath: { type: 'string', description: 'Absolute path to the project directory' },
|
|
1264
|
+
patternId: { type: 'string', description: 'Pattern ID to remove' }
|
|
1265
|
+
},
|
|
1266
|
+
required: ['projectPath', 'patternId']
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
];
|
|
1270
|
+
|
|
1271
|
+
// =============================================================================
|
|
1272
|
+
// Staleness Warning — cached check for version drift
|
|
1273
|
+
// =============================================================================
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Check if the server is stale and return a warning string if so.
|
|
1277
|
+
* Called on every tool response to ensure stale servers are noticed.
|
|
1278
|
+
*/
|
|
1279
|
+
function getStalenessWarning() {
|
|
1280
|
+
try {
|
|
1281
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
1282
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1283
|
+
if (LOADED_VERSION !== pkg.version) {
|
|
1284
|
+
return `\n\n⚠️ STALE SERVER: Running v${LOADED_VERSION} but v${pkg.version} is installed. Restart Claude Code to load updated tools.`;
|
|
1285
|
+
}
|
|
1286
|
+
} catch (e) {
|
|
1287
|
+
// Ignore — can't check staleness
|
|
1288
|
+
}
|
|
1289
|
+
return '';
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Cache the staleness check result for 60 seconds to avoid reading package.json on every call
|
|
1293
|
+
let _stalenessCache = { warning: '', checkedAt: 0 };
|
|
1294
|
+
function getCachedStalenessWarning() {
|
|
1295
|
+
const now = Date.now();
|
|
1296
|
+
if (now - _stalenessCache.checkedAt > 60000) {
|
|
1297
|
+
_stalenessCache = { warning: getStalenessWarning(), checkedAt: now };
|
|
1298
|
+
}
|
|
1299
|
+
return _stalenessCache.warning;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// =============================================================================
|
|
1303
|
+
// Tool Handlers — execute each tool and return result
|
|
1304
|
+
// =============================================================================
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Execute a tool by name with given arguments.
|
|
1308
|
+
* Returns { content: [{ type: "text", text: "..." }], isError?: true }
|
|
1309
|
+
*/
|
|
1310
|
+
async function executeTool(toolName, args) {
|
|
1311
|
+
try {
|
|
1312
|
+
switch (toolName) {
|
|
1313
|
+
// ── Session Management ────────────────────────────────────────────
|
|
1314
|
+
case 'spawn_session':
|
|
1315
|
+
return await handleSpawn(args);
|
|
1316
|
+
case 'send_message':
|
|
1317
|
+
return await handleSend(args);
|
|
1318
|
+
case 'resume_session':
|
|
1319
|
+
return await handleResume(args);
|
|
1320
|
+
case 'pause_session':
|
|
1321
|
+
return handlePause(args);
|
|
1322
|
+
case 'fork_session':
|
|
1323
|
+
return await handleFork(args);
|
|
1324
|
+
case 'stop_session':
|
|
1325
|
+
return handleStop(args);
|
|
1326
|
+
case 'kill_session':
|
|
1327
|
+
return handleKill(args);
|
|
1328
|
+
|
|
1329
|
+
// ── Information ───────────────────────────────────────────────────
|
|
1330
|
+
case 'get_session_status':
|
|
1331
|
+
return handleStatus(args);
|
|
1332
|
+
case 'get_last_output':
|
|
1333
|
+
return handleLastOutput(args);
|
|
1334
|
+
case 'list_sessions':
|
|
1335
|
+
return handleList(args);
|
|
1336
|
+
case 'get_history':
|
|
1337
|
+
return handleHistory(args);
|
|
1338
|
+
case 'delete_session':
|
|
1339
|
+
return handleDelete(args);
|
|
1340
|
+
|
|
1341
|
+
// ── Delegate ──────────────────────────────────────────────────────
|
|
1342
|
+
case 'delegate_task':
|
|
1343
|
+
return await handleDelegate(args);
|
|
1344
|
+
case 'continue_task':
|
|
1345
|
+
return await handleContinue(args);
|
|
1346
|
+
case 'finish_task':
|
|
1347
|
+
return handleFinish(args);
|
|
1348
|
+
case 'abort_task':
|
|
1349
|
+
return handleAbort(args);
|
|
1350
|
+
|
|
1351
|
+
// ── Server Version ──────────────────────────────────────────────
|
|
1352
|
+
case 'server_version':
|
|
1353
|
+
return handleServerVersion(args);
|
|
1354
|
+
|
|
1355
|
+
// ── Orchestrator Guide ────────────────────────────────────────────
|
|
1356
|
+
case 'get_orchestrator_guide':
|
|
1357
|
+
return handleGetOrchestratorGuide(args);
|
|
1358
|
+
|
|
1359
|
+
// ── Batch ─────────────────────────────────────────────────────────
|
|
1360
|
+
case 'batch_spawn':
|
|
1361
|
+
return await handleBatch(args);
|
|
1362
|
+
|
|
1363
|
+
// ══════════════════════════════════════════════════════════════════
|
|
1364
|
+
// Team Hub Tools
|
|
1365
|
+
// ══════════════════════════════════════════════════════════════════
|
|
1366
|
+
|
|
1367
|
+
// ── Layer 1: Chat ─────────────────────────────────────────────────
|
|
1368
|
+
case 'team_spawn':
|
|
1369
|
+
return await handleTeamSpawn(args);
|
|
1370
|
+
case 'team_send_message':
|
|
1371
|
+
return handleTeamSendMessage(args);
|
|
1372
|
+
case 'team_broadcast':
|
|
1373
|
+
return handleTeamBroadcast(args);
|
|
1374
|
+
case 'team_check_inbox':
|
|
1375
|
+
return handleTeamCheckInbox(args);
|
|
1376
|
+
case 'team_ask':
|
|
1377
|
+
return await handleTeamAsk(args);
|
|
1378
|
+
case 'team_reply':
|
|
1379
|
+
return handleTeamReply(args);
|
|
1380
|
+
case 'team_roster':
|
|
1381
|
+
return handleTeamRoster(args);
|
|
1382
|
+
case 'team_update_status':
|
|
1383
|
+
return handleTeamUpdateStatus(args);
|
|
1384
|
+
|
|
1385
|
+
// ── Layer 2: Artifacts ────────────────────────────────────────────
|
|
1386
|
+
case 'artifact_publish':
|
|
1387
|
+
return await handleArtifactPublish(args);
|
|
1388
|
+
case 'artifact_get':
|
|
1389
|
+
return handleArtifactGet(args);
|
|
1390
|
+
case 'artifact_list':
|
|
1391
|
+
return handleArtifactList(args);
|
|
1392
|
+
case 'artifact_readers':
|
|
1393
|
+
return handleArtifactReaders(args);
|
|
1394
|
+
case 'artifact_history':
|
|
1395
|
+
return handleArtifactHistory(args);
|
|
1396
|
+
|
|
1397
|
+
// ── Layer 2: Contracts ────────────────────────────────────────────
|
|
1398
|
+
case 'contract_create':
|
|
1399
|
+
return await handleContractCreate(args);
|
|
1400
|
+
case 'contract_start':
|
|
1401
|
+
return handleContractStart(args);
|
|
1402
|
+
case 'contract_complete':
|
|
1403
|
+
return await handleContractComplete(args);
|
|
1404
|
+
case 'contract_fail':
|
|
1405
|
+
return await handleContractFail(args);
|
|
1406
|
+
case 'contract_reopen':
|
|
1407
|
+
return await handleContractReopen(args);
|
|
1408
|
+
case 'contract_get':
|
|
1409
|
+
return handleContractGet(args);
|
|
1410
|
+
case 'contract_list':
|
|
1411
|
+
return handleContractList(args);
|
|
1412
|
+
case 'contract_reassign':
|
|
1413
|
+
return handleContractReassign(args);
|
|
1414
|
+
|
|
1415
|
+
// ── Layer 3: Lineage ──────────────────────────────────────────────
|
|
1416
|
+
case 'artifact_lineage':
|
|
1417
|
+
return handleArtifactLineage(args);
|
|
1418
|
+
case 'artifact_impact':
|
|
1419
|
+
return handleArtifactImpact(args);
|
|
1420
|
+
case 'artifact_stale':
|
|
1421
|
+
return handleArtifactStale(args);
|
|
1422
|
+
case 'team_audit':
|
|
1423
|
+
return handleTeamAudit(args);
|
|
1424
|
+
|
|
1425
|
+
// ── Layer 3: Pipelines ────────────────────────────────────────────
|
|
1426
|
+
case 'pipeline_create':
|
|
1427
|
+
return handlePipelineCreate(args);
|
|
1428
|
+
case 'pipeline_list':
|
|
1429
|
+
return handlePipelineList(args);
|
|
1430
|
+
case 'pipeline_pause':
|
|
1431
|
+
return handlePipelinePause(args);
|
|
1432
|
+
case 'pipeline_resume':
|
|
1433
|
+
return handlePipelineResume(args);
|
|
1434
|
+
|
|
1435
|
+
// ── Layer 3: Snapshots ────────────────────────────────────────────
|
|
1436
|
+
case 'team_snapshot':
|
|
1437
|
+
return handleTeamSnapshot(args);
|
|
1438
|
+
case 'team_snapshot_list':
|
|
1439
|
+
return handleTeamSnapshotList(args);
|
|
1440
|
+
case 'team_rollback':
|
|
1441
|
+
return handleTeamRollback(args);
|
|
1442
|
+
case 'team_replay':
|
|
1443
|
+
return await handleTeamReplay(args);
|
|
1444
|
+
|
|
1445
|
+
// ── Phase Gate & Team Reset ──
|
|
1446
|
+
case 'phase_gate':
|
|
1447
|
+
return handlePhaseGate(args);
|
|
1448
|
+
case 'team_reset':
|
|
1449
|
+
return handleTeamReset(args);
|
|
1450
|
+
|
|
1451
|
+
// ── File Ownership handlers ──
|
|
1452
|
+
case 'register_files':
|
|
1453
|
+
return handleRegisterFiles(args);
|
|
1454
|
+
case 'check_file_owner':
|
|
1455
|
+
return handleCheckFileOwner(args);
|
|
1456
|
+
|
|
1457
|
+
// ── Session Continuity (Layer 0) handlers ──
|
|
1458
|
+
case 'continuity_snapshot': {
|
|
1459
|
+
const snap = new SessionSnapshot(args.projectPath);
|
|
1460
|
+
const result = snap.capture(args.sessionName, {
|
|
1461
|
+
activeFiles: args.activeFiles,
|
|
1462
|
+
taskSummary: args.taskSummary,
|
|
1463
|
+
openQuestions: args.openQuestions
|
|
1464
|
+
});
|
|
1465
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1466
|
+
}
|
|
1467
|
+
case 'continuity_briefing': {
|
|
1468
|
+
const gen = new BriefingGenerator(args.projectPath);
|
|
1469
|
+
const result = gen.generate({
|
|
1470
|
+
maxTokens: args.maxTokens,
|
|
1471
|
+
includeDecisions: args.includeDecisions,
|
|
1472
|
+
includePatterns: args.includePatterns
|
|
1473
|
+
});
|
|
1474
|
+
return textResult(result.markdown);
|
|
1475
|
+
}
|
|
1476
|
+
case 'continuity_status': {
|
|
1477
|
+
const diff = new DiffEngine(args.projectPath);
|
|
1478
|
+
const result = diff.diffFromLatest();
|
|
1479
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1480
|
+
}
|
|
1481
|
+
case 'continuity_diff': {
|
|
1482
|
+
const diff = new DiffEngine(args.projectPath);
|
|
1483
|
+
let result;
|
|
1484
|
+
if (args.fromSnapshot && args.toSnapshot) {
|
|
1485
|
+
result = diff.diffBetween(args.fromSnapshot, args.toSnapshot);
|
|
1486
|
+
} else {
|
|
1487
|
+
result = diff.diffFromLatest();
|
|
1488
|
+
}
|
|
1489
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1490
|
+
}
|
|
1491
|
+
case 'continuity_history': {
|
|
1492
|
+
const snap = new SessionSnapshot(args.projectPath);
|
|
1493
|
+
const result = snap.list(args.limit || 20);
|
|
1494
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1495
|
+
}
|
|
1496
|
+
case 'continuity_stale_check': {
|
|
1497
|
+
const detector = new StaleDetector(args.projectPath);
|
|
1498
|
+
const result = detector.check();
|
|
1499
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1500
|
+
}
|
|
1501
|
+
case 'decision_add': {
|
|
1502
|
+
const journal = new DecisionJournal(args.projectPath);
|
|
1503
|
+
const result = journal.add({
|
|
1504
|
+
decision: args.decision,
|
|
1505
|
+
reason: args.reason,
|
|
1506
|
+
files: args.files,
|
|
1507
|
+
tags: args.tags
|
|
1508
|
+
});
|
|
1509
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1510
|
+
}
|
|
1511
|
+
case 'decision_list': {
|
|
1512
|
+
const journal = new DecisionJournal(args.projectPath);
|
|
1513
|
+
const result = journal.list({ limit: args.limit, tag: args.tag });
|
|
1514
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1515
|
+
}
|
|
1516
|
+
case 'decision_search': {
|
|
1517
|
+
const journal = new DecisionJournal(args.projectPath);
|
|
1518
|
+
const result = journal.search(args.keyword);
|
|
1519
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1520
|
+
}
|
|
1521
|
+
case 'pattern_add': {
|
|
1522
|
+
const registry = new PatternRegistry(args.projectPath);
|
|
1523
|
+
const result = registry.add({
|
|
1524
|
+
rule: args.rule,
|
|
1525
|
+
context: args.context,
|
|
1526
|
+
example: args.example,
|
|
1527
|
+
tags: args.tags
|
|
1528
|
+
});
|
|
1529
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1530
|
+
}
|
|
1531
|
+
case 'pattern_list': {
|
|
1532
|
+
const registry = new PatternRegistry(args.projectPath);
|
|
1533
|
+
const result = registry.list({ tag: args.tag });
|
|
1534
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1535
|
+
}
|
|
1536
|
+
case 'pattern_remove': {
|
|
1537
|
+
const registry = new PatternRegistry(args.projectPath);
|
|
1538
|
+
const result = registry.remove(args.patternId);
|
|
1539
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
default:
|
|
1543
|
+
return errorResult(`Unknown tool: ${toolName}`);
|
|
1544
|
+
}
|
|
1545
|
+
} catch (err) {
|
|
1546
|
+
return errorResult(err.message);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// ── Session Handlers ──────────────────────────────────────────────────────
|
|
1551
|
+
|
|
1552
|
+
async function handleSpawn(args) {
|
|
1553
|
+
const { session, response } = await manager.spawn(args.name, {
|
|
1554
|
+
prompt: args.prompt,
|
|
1555
|
+
model: args.model,
|
|
1556
|
+
workDir: args.work_dir,
|
|
1557
|
+
permissionMode: args.permission_mode,
|
|
1558
|
+
allowedTools: args.allowed_tools,
|
|
1559
|
+
systemPrompt: args.system_prompt,
|
|
1560
|
+
maxBudget: args.max_budget,
|
|
1561
|
+
agent: args.agent,
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
const result = {
|
|
1565
|
+
session_name: session.name,
|
|
1566
|
+
status: session.status,
|
|
1567
|
+
model: session.model,
|
|
1568
|
+
session_id: session.id,
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
if (response) {
|
|
1572
|
+
result.response = response.text;
|
|
1573
|
+
result.cost = response.cost;
|
|
1574
|
+
result.turns = response.turns;
|
|
1575
|
+
result.duration_ms = response.duration;
|
|
1576
|
+
} else {
|
|
1577
|
+
result.message = 'Session started (idle). Send a message with send_message.';
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
async function handleSend(args) {
|
|
1584
|
+
try {
|
|
1585
|
+
// Check if target is a team worker for roster summary injection
|
|
1586
|
+
const workerTeam = _workerTeamMap.get(args.name);
|
|
1587
|
+
const rosterSuffix = workerTeam ? getRosterSummary(workerTeam) : '';
|
|
1588
|
+
|
|
1589
|
+
// Auto-resume if session is not alive
|
|
1590
|
+
if (!manager.sessions.has(args.name)) {
|
|
1591
|
+
log(`Session "${args.name}" not alive. Auto-resuming...`);
|
|
1592
|
+
const response = await manager.resume(args.name, args.message);
|
|
1593
|
+
return textResult(JSON.stringify({
|
|
1594
|
+
session_name: args.name,
|
|
1595
|
+
auto_resumed: true,
|
|
1596
|
+
response: response?.text || '',
|
|
1597
|
+
cost: response?.cost || 0,
|
|
1598
|
+
turns: response?.turns || 0,
|
|
1599
|
+
duration_ms: response?.duration || 0,
|
|
1600
|
+
}, null, 2) + rosterSuffix);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const response = await manager.send(args.name, args.message);
|
|
1604
|
+
return textResult(JSON.stringify({
|
|
1605
|
+
session_name: args.name,
|
|
1606
|
+
response: response.text,
|
|
1607
|
+
cost: response.cost,
|
|
1608
|
+
turns: response.turns,
|
|
1609
|
+
duration_ms: response.duration,
|
|
1610
|
+
}, null, 2) + rosterSuffix);
|
|
1611
|
+
} catch (err) {
|
|
1612
|
+
// Try to set worker status to error if this is a team worker
|
|
1613
|
+
try {
|
|
1614
|
+
const workerTeam = _workerTeamMap.get(args.name);
|
|
1615
|
+
if (workerTeam) {
|
|
1616
|
+
const { teamHub } = getTeamInstances(workerTeam);
|
|
1617
|
+
teamHub.updateMember(args.name, { status: 'error', task: `Failed: ${err.message}` });
|
|
1618
|
+
}
|
|
1619
|
+
} catch (e) {
|
|
1620
|
+
// Ignore cleanup errors
|
|
1621
|
+
}
|
|
1622
|
+
return errorResult(err.message);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
async function handleResume(args) {
|
|
1627
|
+
const response = await manager.resume(args.name, args.message);
|
|
1628
|
+
return textResult(JSON.stringify({
|
|
1629
|
+
session_name: args.name,
|
|
1630
|
+
status: 'resumed',
|
|
1631
|
+
response: response?.text || null,
|
|
1632
|
+
cost: response?.cost || 0,
|
|
1633
|
+
turns: response?.turns || 0,
|
|
1634
|
+
}, null, 2));
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function handlePause(args) {
|
|
1638
|
+
manager.pause(args.name);
|
|
1639
|
+
return textResult(JSON.stringify({
|
|
1640
|
+
session_name: args.name,
|
|
1641
|
+
status: 'paused',
|
|
1642
|
+
}, null, 2));
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
async function handleFork(args) {
|
|
1646
|
+
const { session, response } = await manager.fork(args.name, args.new_name, {
|
|
1647
|
+
message: args.message,
|
|
1648
|
+
model: args.model,
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
return textResult(JSON.stringify({
|
|
1652
|
+
source: args.name,
|
|
1653
|
+
forked_as: args.new_name,
|
|
1654
|
+
status: session.status,
|
|
1655
|
+
response: response?.text || '',
|
|
1656
|
+
cost: response?.cost || 0,
|
|
1657
|
+
turns: response?.turns || 0,
|
|
1658
|
+
}, null, 2));
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function handleStop(args) {
|
|
1662
|
+
manager.stop(args.name);
|
|
1663
|
+
|
|
1664
|
+
// Auto-set team worker status to idle when stopped
|
|
1665
|
+
autoSetWorkerIdle(args.name);
|
|
1666
|
+
|
|
1667
|
+
return textResult(JSON.stringify({
|
|
1668
|
+
session_name: args.name,
|
|
1669
|
+
status: 'stopped',
|
|
1670
|
+
message: 'Session stopped. Can be resumed later with resume_session.',
|
|
1671
|
+
}, null, 2));
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function handleKill(args) {
|
|
1675
|
+
manager.kill(args.name);
|
|
1676
|
+
|
|
1677
|
+
// Auto-set team worker status to idle when killed
|
|
1678
|
+
autoSetWorkerIdle(args.name);
|
|
1679
|
+
|
|
1680
|
+
return textResult(JSON.stringify({
|
|
1681
|
+
session_name: args.name,
|
|
1682
|
+
status: 'killed',
|
|
1683
|
+
}, null, 2));
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// ── Information Handlers ──────────────────────────────────────────────────
|
|
1687
|
+
|
|
1688
|
+
function handleStatus(args) {
|
|
1689
|
+
const info = manager.status(args.name);
|
|
1690
|
+
return textResult(JSON.stringify({
|
|
1691
|
+
name: info.name,
|
|
1692
|
+
status: info.status,
|
|
1693
|
+
model: info.model,
|
|
1694
|
+
session_id: info.claudeSessionId || info.id,
|
|
1695
|
+
work_dir: info.workDir,
|
|
1696
|
+
total_cost: info.totalCostUsd || 0,
|
|
1697
|
+
total_turns: info.totalTurns || 0,
|
|
1698
|
+
interactions: info.interactionCount || (info.interactions || []).length,
|
|
1699
|
+
pid: info.pid || null,
|
|
1700
|
+
}, null, 2));
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function handleLastOutput(args) {
|
|
1704
|
+
const last = manager.lastOutput(args.name);
|
|
1705
|
+
if (!last) {
|
|
1706
|
+
return textResult(JSON.stringify({ session_name: args.name, output: null, message: 'No output yet.' }, null, 2));
|
|
1707
|
+
}
|
|
1708
|
+
return textResult(JSON.stringify({
|
|
1709
|
+
session_name: args.name,
|
|
1710
|
+
prompt: last.prompt,
|
|
1711
|
+
response: last.response,
|
|
1712
|
+
cost: last.cost,
|
|
1713
|
+
timestamp: last.timestamp,
|
|
1714
|
+
}, null, 2));
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function handleList(args) {
|
|
1718
|
+
const sessions = manager.list(args.status);
|
|
1719
|
+
const summary = sessions.map(s => ({
|
|
1720
|
+
name: s.name,
|
|
1721
|
+
status: s.status,
|
|
1722
|
+
model: s.model,
|
|
1723
|
+
total_cost: s.totalCostUsd || 0,
|
|
1724
|
+
total_turns: s.totalTurns || 0,
|
|
1725
|
+
interactions: s.interactionCount || (s.interactions || []).length,
|
|
1726
|
+
}));
|
|
1727
|
+
return textResult(JSON.stringify({ total: summary.length, sessions: summary }, null, 2));
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function handleHistory(args) {
|
|
1731
|
+
const hist = manager.history(args.name);
|
|
1732
|
+
return textResult(JSON.stringify({
|
|
1733
|
+
session_name: args.name,
|
|
1734
|
+
interaction_count: hist.length,
|
|
1735
|
+
interactions: hist.map((h, i) => ({
|
|
1736
|
+
index: i,
|
|
1737
|
+
prompt: h.prompt,
|
|
1738
|
+
response: h.response,
|
|
1739
|
+
cost: h.cost,
|
|
1740
|
+
turns: h.turns,
|
|
1741
|
+
duration_ms: h.duration,
|
|
1742
|
+
timestamp: h.timestamp,
|
|
1743
|
+
})),
|
|
1744
|
+
}, null, 2));
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function handleDelete(args) {
|
|
1748
|
+
manager.delete(args.name);
|
|
1749
|
+
return textResult(JSON.stringify({
|
|
1750
|
+
session_name: args.name,
|
|
1751
|
+
status: 'deleted',
|
|
1752
|
+
message: 'Session permanently deleted.',
|
|
1753
|
+
}, null, 2));
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// ── Delegate Handlers ─────────────────────────────────────────────────────
|
|
1757
|
+
|
|
1758
|
+
async function handleDelegate(args) {
|
|
1759
|
+
// Create or reuse a delegate instance for this session
|
|
1760
|
+
let delegate = delegates.get(args.name);
|
|
1761
|
+
if (!delegate) {
|
|
1762
|
+
delegate = new Delegate(manager);
|
|
1763
|
+
delegates.set(args.name, delegate);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const result = await delegate.run(args.name, {
|
|
1767
|
+
task: args.task,
|
|
1768
|
+
model: args.model,
|
|
1769
|
+
preset: args.preset,
|
|
1770
|
+
workDir: args.work_dir,
|
|
1771
|
+
maxCost: args.max_cost,
|
|
1772
|
+
maxTurns: args.max_turns,
|
|
1773
|
+
context: args.context,
|
|
1774
|
+
systemPrompt: args.system_prompt,
|
|
1775
|
+
agent: args.agent,
|
|
1776
|
+
safety: args.safety,
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
async function handleContinue(args) {
|
|
1783
|
+
// Get or create delegate for this session
|
|
1784
|
+
let delegate = delegates.get(args.name);
|
|
1785
|
+
if (!delegate) {
|
|
1786
|
+
delegate = new Delegate(manager);
|
|
1787
|
+
delegates.set(args.name, delegate);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
const result = await delegate.continue(args.name, args.message);
|
|
1791
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function handleFinish(args) {
|
|
1795
|
+
const delegate = delegates.get(args.name);
|
|
1796
|
+
if (delegate) {
|
|
1797
|
+
delegate.finish(args.name);
|
|
1798
|
+
delegates.delete(args.name);
|
|
1799
|
+
} else {
|
|
1800
|
+
// No delegate — just stop the session
|
|
1801
|
+
try { manager.stop(args.name); } catch (e) {}
|
|
1802
|
+
}
|
|
1803
|
+
return textResult(JSON.stringify({
|
|
1804
|
+
session_name: args.name,
|
|
1805
|
+
status: 'finished',
|
|
1806
|
+
message: 'Task finished. Session stopped.',
|
|
1807
|
+
}, null, 2));
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function handleAbort(args) {
|
|
1811
|
+
const delegate = delegates.get(args.name);
|
|
1812
|
+
if (delegate) {
|
|
1813
|
+
delegate.abort(args.name);
|
|
1814
|
+
delegates.delete(args.name);
|
|
1815
|
+
} else {
|
|
1816
|
+
try { manager.kill(args.name); } catch (e) {}
|
|
1817
|
+
}
|
|
1818
|
+
return textResult(JSON.stringify({
|
|
1819
|
+
session_name: args.name,
|
|
1820
|
+
status: 'aborted',
|
|
1821
|
+
message: 'Task aborted. Session killed.',
|
|
1822
|
+
}, null, 2));
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// ── Batch Handler ─────────────────────────────────────────────────────────
|
|
1826
|
+
|
|
1827
|
+
async function handleBatch(args) {
|
|
1828
|
+
const results = await manager.batch(args.sessions);
|
|
1829
|
+
const summary = results.map(r => ({
|
|
1830
|
+
name: r.session?.name || 'unknown',
|
|
1831
|
+
status: r.error ? 'failed' : 'completed',
|
|
1832
|
+
response: r.response?.text || null,
|
|
1833
|
+
cost: r.response?.cost || 0,
|
|
1834
|
+
error: r.error || null,
|
|
1835
|
+
}));
|
|
1836
|
+
return textResult(JSON.stringify({ total: summary.length, results: summary }, null, 2));
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// ── Orchestrator Guide Handler ─────────────────────────────────────────────
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* Return the orchestrator guide (or a specific section of it).
|
|
1843
|
+
* Uses the section constants from src/prompts.js.
|
|
1844
|
+
*/
|
|
1845
|
+
function handleGetOrchestratorGuide(args) {
|
|
1846
|
+
const section = args.section || 'full';
|
|
1847
|
+
const guide = getOrchestratorGuideSection(section);
|
|
1848
|
+
return textResult(guide);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
/**
|
|
1852
|
+
* Check the running MCP server version and detect staleness.
|
|
1853
|
+
* Compares the version loaded into memory at startup against the
|
|
1854
|
+
* version currently installed on disk (package.json).
|
|
1855
|
+
*/
|
|
1856
|
+
function handleServerVersion() {
|
|
1857
|
+
let installedVersion = LOADED_VERSION;
|
|
1858
|
+
try {
|
|
1859
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
1860
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1861
|
+
installedVersion = pkg.version;
|
|
1862
|
+
} catch (e) {
|
|
1863
|
+
// If we can't read package.json at runtime, use loaded version
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
const stale = LOADED_VERSION !== installedVersion;
|
|
1867
|
+
return textResult(JSON.stringify({
|
|
1868
|
+
running: LOADED_VERSION,
|
|
1869
|
+
installed: installedVersion,
|
|
1870
|
+
stale,
|
|
1871
|
+
message: stale
|
|
1872
|
+
? `Server is stale: running v${LOADED_VERSION} but v${installedVersion} is installed. Restart Claude Code to load new tools.`
|
|
1873
|
+
: `Server is up to date (v${LOADED_VERSION}).`
|
|
1874
|
+
}, null, 2));
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// =============================================================================
|
|
1878
|
+
// Team Hub Handlers — Layer 1, 2, 3
|
|
1879
|
+
// =============================================================================
|
|
1880
|
+
|
|
1881
|
+
// ── Layer 1: Team Chat Handlers ───────────────────────────────────────────
|
|
1882
|
+
|
|
1883
|
+
/**
|
|
1884
|
+
* Build team system prompt from roster.
|
|
1885
|
+
* Delegates to the centralized prompt builder in src/prompts.js which applies
|
|
1886
|
+
* production-quality prompt engineering techniques (identity anchoring, examples,
|
|
1887
|
+
* anti-patterns, state machines, tool preference hierarchy, etc.)
|
|
1888
|
+
*/
|
|
1889
|
+
function buildTeamSystemPrompt(name, role, task, roster, teamName) {
|
|
1890
|
+
return buildFullTeamPrompt({ name, role, task, roster, teamName });
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
async function handleTeamSpawn(args) {
|
|
1894
|
+
try {
|
|
1895
|
+
const teamName = args.team || 'default';
|
|
1896
|
+
const { teamHub } = getTeamInstances(teamName);
|
|
1897
|
+
|
|
1898
|
+
// Deterministic gate enforcement: phase_gate opens gate, spawns go through, next phase_gate closes and reopens
|
|
1899
|
+
const gate = getGateState(teamName);
|
|
1900
|
+
|
|
1901
|
+
if (gate.firstBatchFree) {
|
|
1902
|
+
// First batch before any phase_gate call — always allowed
|
|
1903
|
+
gate.spawnCount++;
|
|
1904
|
+
gate.spawnedWorkers.add(args.name);
|
|
1905
|
+
} else if (gate.gateOpen) {
|
|
1906
|
+
// Gate was opened by a passing phase_gate — allowed
|
|
1907
|
+
gate.spawnCount++;
|
|
1908
|
+
gate.spawnedWorkers.add(args.name);
|
|
1909
|
+
} else {
|
|
1910
|
+
// Gate is closed — block the spawn
|
|
1911
|
+
return errorResult(
|
|
1912
|
+
`GATE CLOSED: Cannot spawn workers. ` +
|
|
1913
|
+
`Call phase_gate to verify phase "${gate.phase || 'previous'}" is complete before spawning the next batch.\n\n` +
|
|
1914
|
+
`This is structural enforcement of Rule 4 (Phase Gate Verification).\n\n` +
|
|
1915
|
+
`Example:\n` +
|
|
1916
|
+
`phase_gate({ phase_completing: "${gate.phase || 'Phase N'}", phase_starting: "Phase N+1", ` +
|
|
1917
|
+
`expected_artifacts: ["..."] })`
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Join the team roster
|
|
1922
|
+
teamHub.joinTeam(args.name, {
|
|
1923
|
+
role: args.role || 'team member',
|
|
1924
|
+
task: args.task || 'Starting up',
|
|
1925
|
+
model: args.model || 'sonnet',
|
|
1926
|
+
});
|
|
1927
|
+
|
|
1928
|
+
// Track inbox check enforcement — worker must call team_check_inbox before using artifact/contract tools
|
|
1929
|
+
_inboxCheckState.set(teamName + ':' + args.name, false);
|
|
1930
|
+
|
|
1931
|
+
// Map worker name to team for auto-status updates on stop/kill
|
|
1932
|
+
_workerTeamMap.set(args.name, teamName);
|
|
1933
|
+
|
|
1934
|
+
// Get roster to build system prompt
|
|
1935
|
+
const roster = teamHub.getRoster();
|
|
1936
|
+
|
|
1937
|
+
// Build team system prompt (now includes role-specific guidance, examples, anti-patterns)
|
|
1938
|
+
const teamSystemPrompt = buildTeamSystemPrompt(args.name, args.role, args.task, roster, teamName);
|
|
1939
|
+
|
|
1940
|
+
// Spawn the session with team system prompt appended
|
|
1941
|
+
// Default to bypassPermissions so team sessions can write files without manual approval
|
|
1942
|
+
const { session, response } = await manager.spawn(args.name, {
|
|
1943
|
+
prompt: args.prompt,
|
|
1944
|
+
model: args.model,
|
|
1945
|
+
systemPrompt: teamSystemPrompt,
|
|
1946
|
+
permissionMode: args.permission_mode || 'bypassPermissions',
|
|
1947
|
+
workDir: args.work_dir || process.cwd(),
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
// Auto-set worker status to active with their task description
|
|
1951
|
+
const taskDesc = args.task || args.prompt || 'Starting up';
|
|
1952
|
+
teamHub.updateMember(args.name, {
|
|
1953
|
+
status: 'active',
|
|
1954
|
+
task: taskDesc.length > 100 ? taskDesc.slice(0, 100) + '...' : taskDesc,
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
const result = {
|
|
1958
|
+
session_name: session.name,
|
|
1959
|
+
status: session.status,
|
|
1960
|
+
model: session.model,
|
|
1961
|
+
team: teamName,
|
|
1962
|
+
role: args.role,
|
|
1963
|
+
roster_size: roster.length,
|
|
1964
|
+
};
|
|
1965
|
+
|
|
1966
|
+
if (response) {
|
|
1967
|
+
result.response = response.text;
|
|
1968
|
+
result.cost = response.cost;
|
|
1969
|
+
result.turns = response.turns;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// Auto version check on first spawn
|
|
1973
|
+
const staleness = getCachedStalenessWarning();
|
|
1974
|
+
if (staleness) {
|
|
1975
|
+
result._staleness_warning = `Server is stale! ${staleness.trim()}`;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
return textResult(JSON.stringify(result, null, 2) + getRosterSummary(teamName));
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
// Try to set worker status to error so phase_gate doesn't hang
|
|
1981
|
+
try {
|
|
1982
|
+
const teamName = args.team || 'default';
|
|
1983
|
+
const { teamHub } = getTeamInstances(teamName);
|
|
1984
|
+
teamHub.updateMember(args.name, { status: 'error', task: `Failed: ${err.message}` });
|
|
1985
|
+
} catch (e) {
|
|
1986
|
+
// Ignore cleanup errors
|
|
1987
|
+
}
|
|
1988
|
+
return errorResult(err.message);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function handleTeamSendMessage(args) {
|
|
1993
|
+
try {
|
|
1994
|
+
const teamName = args.team || 'default';
|
|
1995
|
+
const { teamHub } = getTeamInstances(teamName);
|
|
1996
|
+
|
|
1997
|
+
teamHub.sendDirect(args.from, args.to, args.content, {
|
|
1998
|
+
priority: args.priority || 'normal',
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
return textResult(JSON.stringify({
|
|
2002
|
+
status: 'sent',
|
|
2003
|
+
from: args.from,
|
|
2004
|
+
to: args.to,
|
|
2005
|
+
team: teamName,
|
|
2006
|
+
}, null, 2));
|
|
2007
|
+
} catch (err) {
|
|
2008
|
+
return errorResult(err.message);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
function handleTeamBroadcast(args) {
|
|
2013
|
+
try {
|
|
2014
|
+
const teamName = args.team || 'default';
|
|
2015
|
+
const { teamHub } = getTeamInstances(teamName);
|
|
2016
|
+
|
|
2017
|
+
const count = teamHub.sendBroadcast(args.from, args.content, {
|
|
2018
|
+
priority: args.priority || 'normal',
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
return textResult(JSON.stringify({
|
|
2022
|
+
status: 'broadcast',
|
|
2023
|
+
from: args.from,
|
|
2024
|
+
recipients: count,
|
|
2025
|
+
team: teamName,
|
|
2026
|
+
}, null, 2));
|
|
2027
|
+
} catch (err) {
|
|
2028
|
+
return errorResult(err.message);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
function handleTeamCheckInbox(args) {
|
|
2033
|
+
try {
|
|
2034
|
+
const teamName = args.team || 'default';
|
|
2035
|
+
const { teamHub } = getTeamInstances(teamName);
|
|
2036
|
+
|
|
2037
|
+
// Mark this worker as having checked their inbox
|
|
2038
|
+
const inboxKey = teamName + ':' + args.name;
|
|
2039
|
+
if (_inboxCheckState.has(inboxKey)) {
|
|
2040
|
+
_inboxCheckState.set(inboxKey, true);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
const messages = teamHub.getInbox(args.name, {
|
|
2044
|
+
markRead: args.mark_read !== false,
|
|
2045
|
+
limit: args.limit || 20,
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
return textResult(JSON.stringify({
|
|
2049
|
+
session: args.name,
|
|
2050
|
+
team: teamName,
|
|
2051
|
+
count: messages.length,
|
|
2052
|
+
messages: messages.map(m => ({
|
|
2053
|
+
id: m.id,
|
|
2054
|
+
from: m.from,
|
|
2055
|
+
type: m.type,
|
|
2056
|
+
content: m.content,
|
|
2057
|
+
timestamp: m.timestamp,
|
|
2058
|
+
priority: m.priority,
|
|
2059
|
+
replyTo: m.replyTo,
|
|
2060
|
+
})),
|
|
2061
|
+
}, null, 2));
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
return errorResult(err.message);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
async function handleTeamAsk(args) {
|
|
2068
|
+
try {
|
|
2069
|
+
const teamName = args.team || 'default';
|
|
2070
|
+
const { teamHub } = getTeamInstances(teamName);
|
|
2071
|
+
|
|
2072
|
+
// Create the ask
|
|
2073
|
+
const askId = teamHub.createAsk(
|
|
2074
|
+
args.from,
|
|
2075
|
+
args.to,
|
|
2076
|
+
args.question,
|
|
2077
|
+
args.timeout || 60000
|
|
2078
|
+
);
|
|
2079
|
+
|
|
2080
|
+
// Poll for reply
|
|
2081
|
+
const reply = await teamHub.pollForReply(askId, args.timeout || 60000);
|
|
2082
|
+
|
|
2083
|
+
if (reply) {
|
|
2084
|
+
return textResult(JSON.stringify({
|
|
2085
|
+
status: 'answered',
|
|
2086
|
+
question: args.question,
|
|
2087
|
+
answer: reply.answer,
|
|
2088
|
+
from: args.from,
|
|
2089
|
+
to: args.to,
|
|
2090
|
+
team: teamName,
|
|
2091
|
+
}, null, 2));
|
|
2092
|
+
} else {
|
|
2093
|
+
return textResult(JSON.stringify({
|
|
2094
|
+
status: 'timeout',
|
|
2095
|
+
question: args.question,
|
|
2096
|
+
message: 'No reply received within timeout period',
|
|
2097
|
+
team: teamName,
|
|
2098
|
+
}, null, 2));
|
|
2099
|
+
}
|
|
2100
|
+
} catch (err) {
|
|
2101
|
+
return errorResult(err.message);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function handleTeamReply(args) {
|
|
2106
|
+
try {
|
|
2107
|
+
const teamName = args.team || 'default';
|
|
2108
|
+
const { teamHub } = getTeamInstances(teamName);
|
|
2109
|
+
|
|
2110
|
+
teamHub.submitReply(args.from, args.message_id, args.answer);
|
|
2111
|
+
|
|
2112
|
+
return textResult(JSON.stringify({
|
|
2113
|
+
status: 'replied',
|
|
2114
|
+
message_id: args.message_id,
|
|
2115
|
+
from: args.from,
|
|
2116
|
+
team: teamName,
|
|
2117
|
+
}, null, 2));
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
return errorResult(err.message);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function handleTeamRoster(args) {
|
|
2124
|
+
try {
|
|
2125
|
+
const teamName = args.team || 'default';
|
|
2126
|
+
const { teamHub } = getTeamInstances(teamName);
|
|
2127
|
+
|
|
2128
|
+
const roster = teamHub.getRoster();
|
|
2129
|
+
// Defensive: ensure roster is an array
|
|
2130
|
+
const safeRoster = Array.isArray(roster) ? roster : [];
|
|
2131
|
+
|
|
2132
|
+
return textResult(JSON.stringify({
|
|
2133
|
+
team: teamName,
|
|
2134
|
+
count: safeRoster.length,
|
|
2135
|
+
members: safeRoster.map(m => ({
|
|
2136
|
+
name: m.name,
|
|
2137
|
+
role: m.role,
|
|
2138
|
+
status: m.status,
|
|
2139
|
+
task: m.task,
|
|
2140
|
+
model: m.model,
|
|
2141
|
+
joinedAt: m.joinedAt,
|
|
2142
|
+
lastSeen: m.lastSeen,
|
|
2143
|
+
})),
|
|
2144
|
+
}, null, 2));
|
|
2145
|
+
} catch (err) {
|
|
2146
|
+
return errorResult(err.message);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
function handleTeamUpdateStatus(args) {
|
|
2151
|
+
try {
|
|
2152
|
+
const teamName = args.team || 'default';
|
|
2153
|
+
const { teamHub } = getTeamInstances(teamName);
|
|
2154
|
+
|
|
2155
|
+
const updates = {};
|
|
2156
|
+
if (args.status) updates.status = args.status;
|
|
2157
|
+
if (args.task) updates.task = args.task;
|
|
2158
|
+
if (args.role) updates.role = args.role;
|
|
2159
|
+
|
|
2160
|
+
teamHub.updateMember(args.name, updates);
|
|
2161
|
+
|
|
2162
|
+
return textResult(JSON.stringify({
|
|
2163
|
+
status: 'updated',
|
|
2164
|
+
name: args.name,
|
|
2165
|
+
updates,
|
|
2166
|
+
team: teamName,
|
|
2167
|
+
}, null, 2));
|
|
2168
|
+
} catch (err) {
|
|
2169
|
+
return errorResult(err.message);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// ── Layer 2: Artifact Handlers ────────────────────────────────────────────
|
|
2174
|
+
|
|
2175
|
+
async function handleArtifactPublish(args) {
|
|
2176
|
+
try {
|
|
2177
|
+
const teamName = args.team || 'default';
|
|
2178
|
+
|
|
2179
|
+
// Inbox check enforcement — workers must call team_check_inbox first
|
|
2180
|
+
const inboxBlock = checkInboxEnforcement(teamName, args.publisher);
|
|
2181
|
+
if (inboxBlock) return errorResult(inboxBlock);
|
|
2182
|
+
|
|
2183
|
+
const { artifactStore, resolver } = getTeamInstances(teamName);
|
|
2184
|
+
|
|
2185
|
+
// Publish the artifact
|
|
2186
|
+
const result = artifactStore.publish(args.artifactId, {
|
|
2187
|
+
type: args.type,
|
|
2188
|
+
name: args.name,
|
|
2189
|
+
data: args.data,
|
|
2190
|
+
summary: args.summary,
|
|
2191
|
+
tags: args.tags,
|
|
2192
|
+
publisher: args.publisher,
|
|
2193
|
+
derivedFrom: args.derivedFrom,
|
|
2194
|
+
});
|
|
2195
|
+
|
|
2196
|
+
// Trigger dependency resolution and pipeline evaluation
|
|
2197
|
+
await resolver.resolve({
|
|
2198
|
+
type: 'artifact_published',
|
|
2199
|
+
artifactId: args.artifactId,
|
|
2200
|
+
artifactType: args.type,
|
|
2201
|
+
version: result.version,
|
|
2202
|
+
data: args.data,
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
// Auto-register file ownership from artifact data
|
|
2206
|
+
let filesRegistered = 0;
|
|
2207
|
+
if (args.data && args.publisher) {
|
|
2208
|
+
const fileLists = [args.data.files, args.data.filesCreated, args.data.filesModified].filter(Array.isArray);
|
|
2209
|
+
for (const list of fileLists) {
|
|
2210
|
+
for (const filePath of list) {
|
|
2211
|
+
if (typeof filePath === 'string') {
|
|
2212
|
+
_fileOwnership.set(filePath, { worker: args.publisher, team: teamName });
|
|
2213
|
+
filesRegistered++;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
const publishResult = {
|
|
2220
|
+
status: 'published',
|
|
2221
|
+
artifactId: result.artifactId,
|
|
2222
|
+
version: result.version,
|
|
2223
|
+
type: result.type,
|
|
2224
|
+
team: teamName,
|
|
2225
|
+
};
|
|
2226
|
+
if (filesRegistered > 0) {
|
|
2227
|
+
publishResult.filesRegistered = filesRegistered;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
return textResult(JSON.stringify(publishResult, null, 2));
|
|
2231
|
+
} catch (err) {
|
|
2232
|
+
return errorResult(err.message);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
function handleArtifactGet(args) {
|
|
2237
|
+
try {
|
|
2238
|
+
const teamName = args.team || 'default';
|
|
2239
|
+
|
|
2240
|
+
// Inbox check enforcement — workers must call team_check_inbox first
|
|
2241
|
+
const inboxBlock = checkInboxEnforcement(teamName, args.reader);
|
|
2242
|
+
if (inboxBlock) return errorResult(inboxBlock);
|
|
2243
|
+
|
|
2244
|
+
const { artifactStore } = getTeamInstances(teamName);
|
|
2245
|
+
|
|
2246
|
+
const artifact = artifactStore.get(args.artifactId, args.version);
|
|
2247
|
+
|
|
2248
|
+
if (!artifact) {
|
|
2249
|
+
return errorResult(`Artifact ${args.artifactId} not found`);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// Track this read if a reader was specified
|
|
2253
|
+
if (args.reader) {
|
|
2254
|
+
artifactStore.trackRead(args.artifactId, args.reader, artifact.version);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// Build the response
|
|
2258
|
+
const response = {
|
|
2259
|
+
artifactId: artifact.artifactId,
|
|
2260
|
+
version: artifact.version,
|
|
2261
|
+
type: artifact.type,
|
|
2262
|
+
name: artifact.name,
|
|
2263
|
+
data: artifact.data,
|
|
2264
|
+
publisher: artifact.publisher,
|
|
2265
|
+
publishedAt: artifact.publishedAt,
|
|
2266
|
+
summary: artifact.summary,
|
|
2267
|
+
lineage: artifact.lineage,
|
|
2268
|
+
team: teamName,
|
|
2269
|
+
readBy: artifactStore.getReads(args.artifactId),
|
|
2270
|
+
};
|
|
2271
|
+
|
|
2272
|
+
// Add prominent warning if reader param was not provided
|
|
2273
|
+
if (!args.reader) {
|
|
2274
|
+
response._WARNING = '⚠️ UNTRACKED READ: You did not pass the "reader" parameter. This read will NOT be tracked. The orchestrator cannot verify you consumed this artifact. Fix: artifact_get({ artifactId: "' + args.artifactId + '", reader: "YOUR-SESSION-NAME" })';
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
return textResult(JSON.stringify(response, null, 2));
|
|
2278
|
+
} catch (err) {
|
|
2279
|
+
return errorResult(err.message);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
function handleArtifactList(args) {
|
|
2284
|
+
try {
|
|
2285
|
+
const teamName = args.team || 'default';
|
|
2286
|
+
const { artifactStore } = getTeamInstances(teamName);
|
|
2287
|
+
|
|
2288
|
+
const filters = {};
|
|
2289
|
+
if (args.type) filters.type = args.type;
|
|
2290
|
+
if (args.publisher) filters.publisher = args.publisher;
|
|
2291
|
+
if (args.tag) filters.tag = args.tag;
|
|
2292
|
+
|
|
2293
|
+
const artifacts = artifactStore.list(filters);
|
|
2294
|
+
|
|
2295
|
+
return textResult(JSON.stringify({
|
|
2296
|
+
team: teamName,
|
|
2297
|
+
count: artifacts.length,
|
|
2298
|
+
artifacts: artifacts.map(a => {
|
|
2299
|
+
const reads = artifactStore.getReads(a.artifactId);
|
|
2300
|
+
const readers = [...new Set(reads.map(r => r.reader))];
|
|
2301
|
+
return {
|
|
2302
|
+
artifactId: a.artifactId,
|
|
2303
|
+
type: a.type,
|
|
2304
|
+
name: a.name,
|
|
2305
|
+
publisher: a.publisher,
|
|
2306
|
+
latestVersion: a.latestVersion,
|
|
2307
|
+
createdAt: a.createdAt,
|
|
2308
|
+
updatedAt: a.updatedAt,
|
|
2309
|
+
tags: a.tags,
|
|
2310
|
+
readCount: reads.length,
|
|
2311
|
+
readers: readers,
|
|
2312
|
+
};
|
|
2313
|
+
}),
|
|
2314
|
+
}, null, 2));
|
|
2315
|
+
} catch (err) {
|
|
2316
|
+
return errorResult(err.message);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
function handleArtifactReaders(args) {
|
|
2321
|
+
try {
|
|
2322
|
+
const teamName = args.team || 'default';
|
|
2323
|
+
const { artifactStore } = getTeamInstances(teamName);
|
|
2324
|
+
|
|
2325
|
+
const reads = artifactStore.getReads(args.artifactId);
|
|
2326
|
+
const uniqueReaders = [...new Set(reads.map(r => r.reader))];
|
|
2327
|
+
|
|
2328
|
+
return textResult(JSON.stringify({
|
|
2329
|
+
artifactId: args.artifactId,
|
|
2330
|
+
totalReads: reads.length,
|
|
2331
|
+
uniqueReaders,
|
|
2332
|
+
reads,
|
|
2333
|
+
}, null, 2));
|
|
2334
|
+
} catch (err) {
|
|
2335
|
+
return errorResult(err.message);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
function handleArtifactHistory(args) {
|
|
2340
|
+
try {
|
|
2341
|
+
const teamName = args.team || 'default';
|
|
2342
|
+
const { artifactStore } = getTeamInstances(teamName);
|
|
2343
|
+
|
|
2344
|
+
const history = artifactStore.history(args.artifactId);
|
|
2345
|
+
|
|
2346
|
+
return textResult(JSON.stringify({
|
|
2347
|
+
artifactId: args.artifactId,
|
|
2348
|
+
team: teamName,
|
|
2349
|
+
versions: history.length,
|
|
2350
|
+
history: history.map(v => ({
|
|
2351
|
+
version: v.version,
|
|
2352
|
+
publishedAt: v.publishedAt,
|
|
2353
|
+
publisher: v.publisher,
|
|
2354
|
+
summary: v.summary,
|
|
2355
|
+
derivedFrom: v.lineage?.derivedFrom || [],
|
|
2356
|
+
})),
|
|
2357
|
+
}, null, 2));
|
|
2358
|
+
} catch (err) {
|
|
2359
|
+
return errorResult(err.message);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// ── Layer 2: Contract Handlers ────────────────────────────────────────────
|
|
2364
|
+
|
|
2365
|
+
async function handleContractCreate(args) {
|
|
2366
|
+
try {
|
|
2367
|
+
const teamName = args.team || 'default';
|
|
2368
|
+
|
|
2369
|
+
// Inbox check enforcement — workers must call team_check_inbox first
|
|
2370
|
+
const inboxBlock = checkInboxEnforcement(teamName, args.assigner);
|
|
2371
|
+
if (inboxBlock) return errorResult(inboxBlock);
|
|
2372
|
+
|
|
2373
|
+
const { contractStore, resolver, teamHub } = getTeamInstances(teamName);
|
|
2374
|
+
|
|
2375
|
+
// Create the contract
|
|
2376
|
+
const contract = contractStore.create(args.contractId, {
|
|
2377
|
+
title: args.title,
|
|
2378
|
+
description: args.description,
|
|
2379
|
+
assignee: args.assignee,
|
|
2380
|
+
assigner: args.assigner,
|
|
2381
|
+
inputs: args.inputs,
|
|
2382
|
+
expectedOutputs: args.expectedOutputs,
|
|
2383
|
+
dependencies: args.dependencies,
|
|
2384
|
+
acceptanceCriteria: args.acceptanceCriteria,
|
|
2385
|
+
autoComplete: args.autoComplete !== false,
|
|
2386
|
+
timeoutMs: args.timeoutMs,
|
|
2387
|
+
maxRetries: args.maxRetries,
|
|
2388
|
+
priority: args.priority,
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
// Trigger dependency resolution to check if contract is immediately ready
|
|
2392
|
+
await resolver.resolve({
|
|
2393
|
+
type: 'contract_created',
|
|
2394
|
+
contractId: args.contractId,
|
|
2395
|
+
});
|
|
2396
|
+
|
|
2397
|
+
// If contract became ready, send inbox notification to assignee
|
|
2398
|
+
const updatedContract = contractStore.get(args.contractId);
|
|
2399
|
+
if (updatedContract.status === 'ready') {
|
|
2400
|
+
teamHub.sendDirect(
|
|
2401
|
+
'system',
|
|
2402
|
+
args.assignee,
|
|
2403
|
+
`Contract ready: ${args.title} (${args.contractId})`,
|
|
2404
|
+
{ priority: args.priority || 'normal' }
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
return textResult(JSON.stringify({
|
|
2409
|
+
status: 'created',
|
|
2410
|
+
contractId: contract.contractId,
|
|
2411
|
+
initialStatus: updatedContract.status,
|
|
2412
|
+
assignee: contract.assignee,
|
|
2413
|
+
team: teamName,
|
|
2414
|
+
}, null, 2));
|
|
2415
|
+
} catch (err) {
|
|
2416
|
+
return errorResult(err.message);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
function handleContractStart(args) {
|
|
2421
|
+
try {
|
|
2422
|
+
const teamName = args.team || 'default';
|
|
2423
|
+
const { contractStore } = getTeamInstances(teamName);
|
|
2424
|
+
|
|
2425
|
+
// Inbox check enforcement — look up the contract's assignee to check
|
|
2426
|
+
const contractData = contractStore.get(args.contractId);
|
|
2427
|
+
if (contractData) {
|
|
2428
|
+
const inboxBlock = checkInboxEnforcement(teamName, contractData.assignee);
|
|
2429
|
+
if (inboxBlock) return errorResult(inboxBlock);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
const contract = contractStore.start(args.contractId);
|
|
2433
|
+
|
|
2434
|
+
return textResult(JSON.stringify({
|
|
2435
|
+
status: 'started',
|
|
2436
|
+
contractId: contract.contractId,
|
|
2437
|
+
title: contract.title,
|
|
2438
|
+
inputs: contract.inputs,
|
|
2439
|
+
expectedOutputs: contract.expectedOutputs,
|
|
2440
|
+
acceptanceCriteria: contract.acceptanceCriteria,
|
|
2441
|
+
startedAt: contract.startedAt,
|
|
2442
|
+
team: teamName,
|
|
2443
|
+
}, null, 2));
|
|
2444
|
+
} catch (err) {
|
|
2445
|
+
return errorResult(err.message);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
async function handleContractComplete(args) {
|
|
2450
|
+
try {
|
|
2451
|
+
const teamName = args.team || 'default';
|
|
2452
|
+
const { contractStore, resolver } = getTeamInstances(teamName);
|
|
2453
|
+
|
|
2454
|
+
// Complete the contract
|
|
2455
|
+
contractStore.complete(args.contractId, {
|
|
2456
|
+
summary: args.summary,
|
|
2457
|
+
publishedArtifacts: args.publishedArtifacts,
|
|
2458
|
+
});
|
|
2459
|
+
|
|
2460
|
+
// Trigger dependency resolution for cascade
|
|
2461
|
+
await resolver.resolve({
|
|
2462
|
+
type: 'contract_completed',
|
|
2463
|
+
contractId: args.contractId,
|
|
2464
|
+
});
|
|
2465
|
+
|
|
2466
|
+
return textResult(JSON.stringify({
|
|
2467
|
+
status: 'completed',
|
|
2468
|
+
contractId: args.contractId,
|
|
2469
|
+
team: teamName,
|
|
2470
|
+
}, null, 2));
|
|
2471
|
+
} catch (err) {
|
|
2472
|
+
return errorResult(err.message);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
async function handleContractFail(args) {
|
|
2477
|
+
try {
|
|
2478
|
+
const teamName = args.team || 'default';
|
|
2479
|
+
const { contractStore, resolver } = getTeamInstances(teamName);
|
|
2480
|
+
|
|
2481
|
+
// Fail the contract
|
|
2482
|
+
contractStore.fail(args.contractId, args.reason);
|
|
2483
|
+
|
|
2484
|
+
// Trigger dependency resolution
|
|
2485
|
+
await resolver.resolve({
|
|
2486
|
+
type: 'contract_failed',
|
|
2487
|
+
contractId: args.contractId,
|
|
2488
|
+
reason: args.reason,
|
|
2489
|
+
});
|
|
2490
|
+
|
|
2491
|
+
return textResult(JSON.stringify({
|
|
2492
|
+
status: 'failed',
|
|
2493
|
+
contractId: args.contractId,
|
|
2494
|
+
reason: args.reason,
|
|
2495
|
+
team: teamName,
|
|
2496
|
+
}, null, 2));
|
|
2497
|
+
} catch (err) {
|
|
2498
|
+
return errorResult(err.message);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
async function handleContractReopen(args) {
|
|
2503
|
+
try {
|
|
2504
|
+
const teamName = args.team || 'default';
|
|
2505
|
+
const { contractStore, resolver } = getTeamInstances(teamName);
|
|
2506
|
+
|
|
2507
|
+
// Reopen the contract
|
|
2508
|
+
const contract = contractStore.reopen(args.contractId, {
|
|
2509
|
+
reason: args.reason,
|
|
2510
|
+
newInputs: args.newInputs,
|
|
2511
|
+
});
|
|
2512
|
+
|
|
2513
|
+
// Trigger dependency resolution
|
|
2514
|
+
await resolver.resolve({
|
|
2515
|
+
type: 'contract_reopened',
|
|
2516
|
+
contractId: args.contractId,
|
|
2517
|
+
});
|
|
2518
|
+
|
|
2519
|
+
return textResult(JSON.stringify({
|
|
2520
|
+
status: 'reopened',
|
|
2521
|
+
contractId: contract.contractId,
|
|
2522
|
+
retryCount: contract.retryCount,
|
|
2523
|
+
currentStatus: contract.status,
|
|
2524
|
+
team: teamName,
|
|
2525
|
+
}, null, 2));
|
|
2526
|
+
} catch (err) {
|
|
2527
|
+
return errorResult(err.message);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function handleContractGet(args) {
|
|
2532
|
+
try {
|
|
2533
|
+
const teamName = args.team || 'default';
|
|
2534
|
+
const { contractStore } = getTeamInstances(teamName);
|
|
2535
|
+
|
|
2536
|
+
const contract = contractStore.get(args.contractId);
|
|
2537
|
+
|
|
2538
|
+
if (!contract) {
|
|
2539
|
+
return errorResult(`Contract ${args.contractId} not found`);
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
return textResult(JSON.stringify({
|
|
2543
|
+
contractId: contract.contractId,
|
|
2544
|
+
title: contract.title,
|
|
2545
|
+
status: contract.status,
|
|
2546
|
+
assignee: contract.assignee,
|
|
2547
|
+
assigner: contract.assigner,
|
|
2548
|
+
inputs: contract.inputs,
|
|
2549
|
+
expectedOutputs: contract.expectedOutputs,
|
|
2550
|
+
dependencies: contract.dependencies,
|
|
2551
|
+
acceptanceCriteria: contract.acceptanceCriteria,
|
|
2552
|
+
retryCount: contract.retryCount,
|
|
2553
|
+
maxRetries: contract.maxRetries,
|
|
2554
|
+
createdAt: contract.createdAt,
|
|
2555
|
+
startedAt: contract.startedAt,
|
|
2556
|
+
completedAt: contract.completedAt,
|
|
2557
|
+
team: teamName,
|
|
2558
|
+
}, null, 2));
|
|
2559
|
+
} catch (err) {
|
|
2560
|
+
return errorResult(err.message);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
function handleContractList(args) {
|
|
2565
|
+
try {
|
|
2566
|
+
const teamName = args.team || 'default';
|
|
2567
|
+
const { contractStore } = getTeamInstances(teamName);
|
|
2568
|
+
|
|
2569
|
+
const filters = {};
|
|
2570
|
+
if (args.status) filters.status = args.status;
|
|
2571
|
+
if (args.assignee) filters.assignee = args.assignee;
|
|
2572
|
+
if (args.assigner) filters.assigner = args.assigner;
|
|
2573
|
+
|
|
2574
|
+
const contracts = contractStore.list(filters);
|
|
2575
|
+
|
|
2576
|
+
return textResult(JSON.stringify({
|
|
2577
|
+
team: teamName,
|
|
2578
|
+
count: contracts.length,
|
|
2579
|
+
contracts: contracts.map(c => ({
|
|
2580
|
+
contractId: c.contractId,
|
|
2581
|
+
title: c.title,
|
|
2582
|
+
status: c.status,
|
|
2583
|
+
assignee: c.assignee,
|
|
2584
|
+
assigner: c.assigner,
|
|
2585
|
+
priority: c.priority,
|
|
2586
|
+
retryCount: c.retryCount,
|
|
2587
|
+
createdAt: c.createdAt,
|
|
2588
|
+
})),
|
|
2589
|
+
}, null, 2));
|
|
2590
|
+
} catch (err) {
|
|
2591
|
+
return errorResult(err.message);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
function handleContractReassign(args) {
|
|
2596
|
+
try {
|
|
2597
|
+
const teamName = args.team || 'default';
|
|
2598
|
+
const { contractStore } = getTeamInstances(teamName);
|
|
2599
|
+
|
|
2600
|
+
contractStore.reassign(args.contractId, args.newAssignee);
|
|
2601
|
+
|
|
2602
|
+
return textResult(JSON.stringify({
|
|
2603
|
+
status: 'reassigned',
|
|
2604
|
+
contractId: args.contractId,
|
|
2605
|
+
newAssignee: args.newAssignee,
|
|
2606
|
+
team: teamName,
|
|
2607
|
+
}, null, 2));
|
|
2608
|
+
} catch (err) {
|
|
2609
|
+
return errorResult(err.message);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// ── Layer 3: Lineage Handlers ─────────────────────────────────────────────
|
|
2614
|
+
|
|
2615
|
+
function handleArtifactLineage(args) {
|
|
2616
|
+
try {
|
|
2617
|
+
const teamName = args.team || 'default';
|
|
2618
|
+
const { lineageGraph } = getTeamInstances(teamName);
|
|
2619
|
+
|
|
2620
|
+
let result;
|
|
2621
|
+
if (args.direction === 'upstream') {
|
|
2622
|
+
result = lineageGraph.getUpstream(args.artifactId, args.version);
|
|
2623
|
+
} else {
|
|
2624
|
+
result = lineageGraph.getDownstream(args.artifactId, args.version);
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
return textResult(JSON.stringify({
|
|
2628
|
+
artifactId: args.artifactId,
|
|
2629
|
+
direction: args.direction,
|
|
2630
|
+
chain: result,
|
|
2631
|
+
team: teamName,
|
|
2632
|
+
}, null, 2));
|
|
2633
|
+
} catch (err) {
|
|
2634
|
+
return errorResult(err.message);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
function handleArtifactImpact(args) {
|
|
2639
|
+
try {
|
|
2640
|
+
const teamName = args.team || 'default';
|
|
2641
|
+
const { lineageGraph } = getTeamInstances(teamName);
|
|
2642
|
+
|
|
2643
|
+
const impact = lineageGraph.getImpact(args.artifactId);
|
|
2644
|
+
|
|
2645
|
+
return textResult(JSON.stringify({
|
|
2646
|
+
artifactId: args.artifactId,
|
|
2647
|
+
impactRadius: impact.impactRadius,
|
|
2648
|
+
affectedArtifacts: impact.affectedArtifacts,
|
|
2649
|
+
affectedContracts: impact.affectedContracts,
|
|
2650
|
+
team: teamName,
|
|
2651
|
+
}, null, 2));
|
|
2652
|
+
} catch (err) {
|
|
2653
|
+
return errorResult(err.message);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
function handleArtifactStale(args) {
|
|
2658
|
+
try {
|
|
2659
|
+
const teamName = args.team || 'default';
|
|
2660
|
+
const { lineageGraph } = getTeamInstances(teamName);
|
|
2661
|
+
|
|
2662
|
+
const staleArtifacts = lineageGraph.findStale();
|
|
2663
|
+
|
|
2664
|
+
return textResult(JSON.stringify({
|
|
2665
|
+
team: teamName,
|
|
2666
|
+
count: staleArtifacts.length,
|
|
2667
|
+
staleArtifacts: staleArtifacts,
|
|
2668
|
+
}, null, 2));
|
|
2669
|
+
} catch (err) {
|
|
2670
|
+
return errorResult(err.message);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
function handleTeamAudit(args) {
|
|
2675
|
+
try {
|
|
2676
|
+
const teamName = args.team || 'default';
|
|
2677
|
+
const { lineageGraph } = getTeamInstances(teamName);
|
|
2678
|
+
|
|
2679
|
+
const trail = lineageGraph.getAuditTrail(args.artifactId, args.version);
|
|
2680
|
+
|
|
2681
|
+
return textResult(JSON.stringify({
|
|
2682
|
+
artifactId: args.artifactId,
|
|
2683
|
+
auditTrail: trail,
|
|
2684
|
+
team: teamName,
|
|
2685
|
+
}, null, 2));
|
|
2686
|
+
} catch (err) {
|
|
2687
|
+
return errorResult(err.message);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// ── Layer 3: Pipeline Handlers ────────────────────────────────────────────
|
|
2692
|
+
|
|
2693
|
+
function handlePipelineCreate(args) {
|
|
2694
|
+
try {
|
|
2695
|
+
const teamName = args.team || 'default';
|
|
2696
|
+
const { pipelineEngine } = getTeamInstances(teamName);
|
|
2697
|
+
|
|
2698
|
+
const pipeline = pipelineEngine.create(args.pipelineId, {
|
|
2699
|
+
rules: args.rules,
|
|
2700
|
+
owner: args.owner,
|
|
2701
|
+
enabled: args.enabled !== false,
|
|
2702
|
+
});
|
|
2703
|
+
|
|
2704
|
+
return textResult(JSON.stringify({
|
|
2705
|
+
status: 'created',
|
|
2706
|
+
pipelineId: pipeline.pipelineId,
|
|
2707
|
+
rulesCount: pipeline.rules.length,
|
|
2708
|
+
enabled: pipeline.enabled,
|
|
2709
|
+
team: teamName,
|
|
2710
|
+
}, null, 2));
|
|
2711
|
+
} catch (err) {
|
|
2712
|
+
return errorResult(err.message);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
function handlePipelineList(args) {
|
|
2717
|
+
try {
|
|
2718
|
+
const teamName = args.team || 'default';
|
|
2719
|
+
const { pipelineEngine } = getTeamInstances(teamName);
|
|
2720
|
+
|
|
2721
|
+
const filters = {};
|
|
2722
|
+
if (args.owner) filters.owner = args.owner;
|
|
2723
|
+
|
|
2724
|
+
const pipelines = pipelineEngine.list(filters);
|
|
2725
|
+
|
|
2726
|
+
return textResult(JSON.stringify({
|
|
2727
|
+
team: teamName,
|
|
2728
|
+
count: pipelines.length,
|
|
2729
|
+
pipelines: pipelines.map(p => ({
|
|
2730
|
+
pipelineId: p.pipelineId,
|
|
2731
|
+
owner: p.owner,
|
|
2732
|
+
enabled: p.enabled,
|
|
2733
|
+
rulesCount: p.rules.length,
|
|
2734
|
+
executionCount: p.executionCount,
|
|
2735
|
+
lastExecutedAt: p.lastExecutedAt,
|
|
2736
|
+
})),
|
|
2737
|
+
}, null, 2));
|
|
2738
|
+
} catch (err) {
|
|
2739
|
+
return errorResult(err.message);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
function handlePipelinePause(args) {
|
|
2744
|
+
try {
|
|
2745
|
+
const teamName = args.team || 'default';
|
|
2746
|
+
const { pipelineEngine } = getTeamInstances(teamName);
|
|
2747
|
+
|
|
2748
|
+
pipelineEngine.pause(args.pipelineId);
|
|
2749
|
+
|
|
2750
|
+
return textResult(JSON.stringify({
|
|
2751
|
+
status: 'paused',
|
|
2752
|
+
pipelineId: args.pipelineId,
|
|
2753
|
+
team: teamName,
|
|
2754
|
+
}, null, 2));
|
|
2755
|
+
} catch (err) {
|
|
2756
|
+
return errorResult(err.message);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
function handlePipelineResume(args) {
|
|
2761
|
+
try {
|
|
2762
|
+
const teamName = args.team || 'default';
|
|
2763
|
+
const { pipelineEngine } = getTeamInstances(teamName);
|
|
2764
|
+
|
|
2765
|
+
pipelineEngine.resume(args.pipelineId);
|
|
2766
|
+
|
|
2767
|
+
return textResult(JSON.stringify({
|
|
2768
|
+
status: 'resumed',
|
|
2769
|
+
pipelineId: args.pipelineId,
|
|
2770
|
+
team: teamName,
|
|
2771
|
+
}, null, 2));
|
|
2772
|
+
} catch (err) {
|
|
2773
|
+
return errorResult(err.message);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
// ── Layer 3: Snapshot Handlers ────────────────────────────────────────────
|
|
2778
|
+
|
|
2779
|
+
function handleTeamSnapshot(args) {
|
|
2780
|
+
try {
|
|
2781
|
+
const teamName = args.team || 'default';
|
|
2782
|
+
const { snapshotEngine } = getTeamInstances(teamName);
|
|
2783
|
+
|
|
2784
|
+
const snapshot = snapshotEngine.createSnapshot(args.snapshotId, {
|
|
2785
|
+
label: args.label,
|
|
2786
|
+
description: args.description,
|
|
2787
|
+
});
|
|
2788
|
+
|
|
2789
|
+
return textResult(JSON.stringify({
|
|
2790
|
+
status: 'created',
|
|
2791
|
+
snapshotId: snapshot.snapshotId,
|
|
2792
|
+
label: snapshot.label,
|
|
2793
|
+
createdAt: snapshot.createdAt,
|
|
2794
|
+
contractsCount: snapshot.contractsCount,
|
|
2795
|
+
artifactsCount: snapshot.artifactsCount,
|
|
2796
|
+
pipelinesCount: snapshot.pipelinesCount,
|
|
2797
|
+
team: teamName,
|
|
2798
|
+
}, null, 2));
|
|
2799
|
+
} catch (err) {
|
|
2800
|
+
return errorResult(err.message);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
function handleTeamSnapshotList(args) {
|
|
2805
|
+
try {
|
|
2806
|
+
const teamName = args.team || 'default';
|
|
2807
|
+
const { snapshotEngine } = getTeamInstances(teamName);
|
|
2808
|
+
|
|
2809
|
+
const snapshots = snapshotEngine.listSnapshots();
|
|
2810
|
+
|
|
2811
|
+
return textResult(JSON.stringify({
|
|
2812
|
+
team: teamName,
|
|
2813
|
+
count: snapshots.length,
|
|
2814
|
+
snapshots: snapshots.map(s => ({
|
|
2815
|
+
snapshotId: s.snapshotId,
|
|
2816
|
+
label: s.label,
|
|
2817
|
+
description: s.description,
|
|
2818
|
+
createdAt: s.createdAt,
|
|
2819
|
+
contractsCount: s.contractsCount,
|
|
2820
|
+
artifactsCount: s.artifactsCount,
|
|
2821
|
+
})),
|
|
2822
|
+
}, null, 2));
|
|
2823
|
+
} catch (err) {
|
|
2824
|
+
return errorResult(err.message);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
function handleTeamRollback(args) {
|
|
2829
|
+
try {
|
|
2830
|
+
const teamName = args.team || 'default';
|
|
2831
|
+
const { snapshotEngine } = getTeamInstances(teamName);
|
|
2832
|
+
|
|
2833
|
+
const result = snapshotEngine.rollback(args.snapshotId, {
|
|
2834
|
+
preserveArtifacts: args.preserveArtifacts !== false,
|
|
2835
|
+
});
|
|
2836
|
+
|
|
2837
|
+
return textResult(JSON.stringify({
|
|
2838
|
+
status: 'rolled_back',
|
|
2839
|
+
snapshotId: args.snapshotId,
|
|
2840
|
+
restoredContracts: result.restoredContracts,
|
|
2841
|
+
restoredPipelines: result.restoredPipelines,
|
|
2842
|
+
team: teamName,
|
|
2843
|
+
}, null, 2));
|
|
2844
|
+
} catch (err) {
|
|
2845
|
+
return errorResult(err.message);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
async function handleTeamReplay(args) {
|
|
2850
|
+
try {
|
|
2851
|
+
const teamName = args.team || 'default';
|
|
2852
|
+
const { snapshotEngine, resolver } = getTeamInstances(teamName);
|
|
2853
|
+
|
|
2854
|
+
// Replay from snapshot with overrides
|
|
2855
|
+
const result = snapshotEngine.replay(args.snapshotId, {
|
|
2856
|
+
overrides: args.overrides,
|
|
2857
|
+
});
|
|
2858
|
+
|
|
2859
|
+
// Re-trigger dependency resolution to execute the workflow
|
|
2860
|
+
await resolver.resolve({
|
|
2861
|
+
type: 'replay_triggered',
|
|
2862
|
+
snapshotId: args.snapshotId,
|
|
2863
|
+
});
|
|
2864
|
+
|
|
2865
|
+
return textResult(JSON.stringify({
|
|
2866
|
+
status: 'replayed',
|
|
2867
|
+
snapshotId: args.snapshotId,
|
|
2868
|
+
appliedOverrides: Object.keys(args.overrides || {}).length,
|
|
2869
|
+
restoredContracts: result.restoredContracts,
|
|
2870
|
+
team: teamName,
|
|
2871
|
+
}, null, 2));
|
|
2872
|
+
} catch (err) {
|
|
2873
|
+
return errorResult(err.message);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
// =============================================================================
|
|
2878
|
+
// Phase Gate & Team Reset Handlers
|
|
2879
|
+
// =============================================================================
|
|
2880
|
+
|
|
2881
|
+
function handlePhaseGate(args) {
|
|
2882
|
+
try {
|
|
2883
|
+
const teamName = args.team || 'default';
|
|
2884
|
+
const { artifactStore, teamHub } = getTeamInstances(teamName);
|
|
2885
|
+
|
|
2886
|
+
const report = {
|
|
2887
|
+
gate: `${args.phase_completing} → ${args.phase_starting}`,
|
|
2888
|
+
timestamp: new Date().toISOString(),
|
|
2889
|
+
checks: [],
|
|
2890
|
+
passed: true,
|
|
2891
|
+
};
|
|
2892
|
+
|
|
2893
|
+
// Check 1: Expected artifacts exist
|
|
2894
|
+
const allArtifacts = artifactStore.list({});
|
|
2895
|
+
const existingIds = new Set(allArtifacts.map(a => a.artifactId));
|
|
2896
|
+
const artifactCheck = {
|
|
2897
|
+
check: 'artifacts_exist',
|
|
2898
|
+
expected: args.expected_artifacts,
|
|
2899
|
+
found: [],
|
|
2900
|
+
missing: [],
|
|
2901
|
+
passed: true,
|
|
2902
|
+
};
|
|
2903
|
+
|
|
2904
|
+
for (const id of args.expected_artifacts) {
|
|
2905
|
+
if (existingIds.has(id)) {
|
|
2906
|
+
artifactCheck.found.push(id);
|
|
2907
|
+
} else {
|
|
2908
|
+
artifactCheck.missing.push(id);
|
|
2909
|
+
artifactCheck.passed = false;
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
report.checks.push(artifactCheck);
|
|
2913
|
+
|
|
2914
|
+
// Check 2: Artifact content valid (get each artifact with reader="orchestrator")
|
|
2915
|
+
const contentCheck = {
|
|
2916
|
+
check: 'artifacts_valid',
|
|
2917
|
+
results: [],
|
|
2918
|
+
passed: true,
|
|
2919
|
+
};
|
|
2920
|
+
|
|
2921
|
+
for (const id of artifactCheck.found) {
|
|
2922
|
+
const artifact = artifactStore.get(id);
|
|
2923
|
+
// Track read as orchestrator
|
|
2924
|
+
artifactStore.trackRead(id, 'orchestrator', artifact?.version);
|
|
2925
|
+
|
|
2926
|
+
if (!artifact) {
|
|
2927
|
+
contentCheck.results.push({ artifactId: id, valid: false, reason: 'Could not read artifact' });
|
|
2928
|
+
contentCheck.passed = false;
|
|
2929
|
+
} else if (!artifact.data || (typeof artifact.data === 'object' && Object.keys(artifact.data).length === 0)) {
|
|
2930
|
+
contentCheck.results.push({ artifactId: id, valid: false, reason: 'Artifact data is empty' });
|
|
2931
|
+
contentCheck.passed = false;
|
|
2932
|
+
} else {
|
|
2933
|
+
contentCheck.results.push({
|
|
2934
|
+
artifactId: id,
|
|
2935
|
+
valid: true,
|
|
2936
|
+
version: artifact.version,
|
|
2937
|
+
publisher: artifact.publisher,
|
|
2938
|
+
summary: artifact.summary || '(no summary)',
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
report.checks.push(contentCheck);
|
|
2943
|
+
|
|
2944
|
+
// Check 3: Workers idle
|
|
2945
|
+
const roster = teamHub.getRoster();
|
|
2946
|
+
// Defensive: ensure roster is an array
|
|
2947
|
+
const safeRoster = Array.isArray(roster) ? roster : [];
|
|
2948
|
+
const idleCheck = {
|
|
2949
|
+
check: 'workers_idle',
|
|
2950
|
+
results: [],
|
|
2951
|
+
passed: true,
|
|
2952
|
+
};
|
|
2953
|
+
|
|
2954
|
+
const workersToCheck = args.expected_idle
|
|
2955
|
+
? safeRoster.filter(m => args.expected_idle.includes(m.name))
|
|
2956
|
+
: safeRoster;
|
|
2957
|
+
|
|
2958
|
+
for (const member of workersToCheck) {
|
|
2959
|
+
const isIdle = member.status === 'idle';
|
|
2960
|
+
idleCheck.results.push({
|
|
2961
|
+
name: member.name,
|
|
2962
|
+
status: member.status,
|
|
2963
|
+
task: member.task,
|
|
2964
|
+
idle: isIdle,
|
|
2965
|
+
});
|
|
2966
|
+
if (!isIdle) {
|
|
2967
|
+
idleCheck.passed = false;
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
report.checks.push(idleCheck);
|
|
2971
|
+
|
|
2972
|
+
// Check 4: Artifact readers verification
|
|
2973
|
+
const readerCheck = {
|
|
2974
|
+
check: 'artifact_readers',
|
|
2975
|
+
results: [],
|
|
2976
|
+
passed: true,
|
|
2977
|
+
};
|
|
2978
|
+
|
|
2979
|
+
if (args.expected_readers) {
|
|
2980
|
+
for (const [artifactId, expectedReaders] of Object.entries(args.expected_readers)) {
|
|
2981
|
+
const reads = artifactStore.getReads(artifactId);
|
|
2982
|
+
const actualReaders = [...new Set(reads.map(r => r.reader))];
|
|
2983
|
+
const missing = expectedReaders.filter(r => !actualReaders.includes(r));
|
|
2984
|
+
|
|
2985
|
+
readerCheck.results.push({
|
|
2986
|
+
artifactId,
|
|
2987
|
+
expectedReaders,
|
|
2988
|
+
actualReaders,
|
|
2989
|
+
missingReaders: missing,
|
|
2990
|
+
allRead: missing.length === 0,
|
|
2991
|
+
});
|
|
2992
|
+
|
|
2993
|
+
if (missing.length > 0) {
|
|
2994
|
+
readerCheck.passed = false;
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
} else {
|
|
2998
|
+
// If no expected readers specified, just show who read what
|
|
2999
|
+
for (const id of artifactCheck.found) {
|
|
3000
|
+
const reads = artifactStore.getReads(id);
|
|
3001
|
+
const readers = [...new Set(reads.map(r => r.reader))];
|
|
3002
|
+
readerCheck.results.push({
|
|
3003
|
+
artifactId: id,
|
|
3004
|
+
readers,
|
|
3005
|
+
readCount: reads.length,
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
report.checks.push(readerCheck);
|
|
3010
|
+
|
|
3011
|
+
// Check 5: Convention completeness (advisory — does NOT affect pass/fail)
|
|
3012
|
+
const requiredConventionFields = [
|
|
3013
|
+
'responseFormat', 'errorFormat', 'statusCodes', 'namingConventions',
|
|
3014
|
+
'filePaths', 'enumValues', 'booleanHandling', 'dateFormat',
|
|
3015
|
+
'auditActions', 'sharedColumnNames',
|
|
3016
|
+
];
|
|
3017
|
+
const conventionCheck = {
|
|
3018
|
+
check: 'convention_completeness',
|
|
3019
|
+
advisory: true,
|
|
3020
|
+
};
|
|
3021
|
+
|
|
3022
|
+
const conventionArtifact = artifactStore.get('shared-conventions');
|
|
3023
|
+
if (!conventionArtifact) {
|
|
3024
|
+
conventionCheck.status = 'WARNING';
|
|
3025
|
+
conventionCheck.message = "No 'shared-conventions' artifact found. Recommend publishing conventions before spawning workers.";
|
|
3026
|
+
} else if (!conventionArtifact.data) {
|
|
3027
|
+
conventionCheck.status = 'WARNING';
|
|
3028
|
+
conventionCheck.message = "Convention artifact exists but has no data. Recommend re-publishing with all 10 fields.";
|
|
3029
|
+
} else {
|
|
3030
|
+
// Check that all required fields exist and are non-empty
|
|
3031
|
+
const missing = requiredConventionFields.filter(f => {
|
|
3032
|
+
if (!(f in conventionArtifact.data)) return true;
|
|
3033
|
+
const value = conventionArtifact.data[f];
|
|
3034
|
+
if (typeof value === 'string' && value.trim() === '') return true;
|
|
3035
|
+
return false;
|
|
3036
|
+
});
|
|
3037
|
+
|
|
3038
|
+
if (missing.length > 0) {
|
|
3039
|
+
conventionCheck.status = 'WARNING';
|
|
3040
|
+
conventionCheck.missingConventions = missing;
|
|
3041
|
+
conventionCheck.message = `Convention artifact missing ${missing.length}/10 fields: ${missing.join(', ')}. Incomplete conventions cause format mismatches between workers.`;
|
|
3042
|
+
} else {
|
|
3043
|
+
conventionCheck.status = 'PASS';
|
|
3044
|
+
conventionCheck.message = 'All 10 conventions defined.';
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
report.checks.push(conventionCheck);
|
|
3048
|
+
|
|
3049
|
+
// Overall pass/fail (convention check is advisory, excluded from gate decision)
|
|
3050
|
+
report.passed = report.checks.filter(c => !c.advisory).every(c => c.passed);
|
|
3051
|
+
|
|
3052
|
+
// Deterministic gate: on pass, close current gate and open for next phase
|
|
3053
|
+
if (report.passed) {
|
|
3054
|
+
const gate = getGateState(teamName);
|
|
3055
|
+
gate.firstBatchFree = false;
|
|
3056
|
+
gate.gateOpen = true;
|
|
3057
|
+
gate.phase = args.phase_starting || null;
|
|
3058
|
+
gate.spawnCount = 0;
|
|
3059
|
+
gate.spawnedWorkers = new Set();
|
|
3060
|
+
_gateState.set(teamName, gate);
|
|
3061
|
+
report._gate_phase = gate.phase;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
// Action recommendation
|
|
3065
|
+
if (report.passed) {
|
|
3066
|
+
report.recommendation = `ALL CHECKS PASSED. Safe to proceed to ${args.phase_starting}.`;
|
|
3067
|
+
} else {
|
|
3068
|
+
const failures = report.checks.filter(c => !c.passed).map(c => c.check);
|
|
3069
|
+
report.recommendation = `BLOCKED: ${failures.join(', ')} failed. Fix these before proceeding to ${args.phase_starting}.`;
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
return textResult(JSON.stringify(report, null, 2) + getRosterSummary(teamName));
|
|
3073
|
+
} catch (err) {
|
|
3074
|
+
return errorResult(err.message);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
// ── File Ownership Handlers ────────────────────────────────────────────────
|
|
3079
|
+
|
|
3080
|
+
function handleRegisterFiles(args) {
|
|
3081
|
+
try {
|
|
3082
|
+
const teamName = args.team || 'default';
|
|
3083
|
+
let registered = 0;
|
|
3084
|
+
|
|
3085
|
+
for (const filePath of args.files) {
|
|
3086
|
+
if (typeof filePath === 'string') {
|
|
3087
|
+
_fileOwnership.set(filePath, { worker: args.worker, team: teamName });
|
|
3088
|
+
registered++;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
return textResult(JSON.stringify({
|
|
3093
|
+
status: 'registered',
|
|
3094
|
+
worker: args.worker,
|
|
3095
|
+
filesRegistered: registered,
|
|
3096
|
+
team: teamName,
|
|
3097
|
+
}, null, 2));
|
|
3098
|
+
} catch (err) {
|
|
3099
|
+
return errorResult(err.message);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
function handleCheckFileOwner(args) {
|
|
3104
|
+
try {
|
|
3105
|
+
const owner = _fileOwnership.get(args.file);
|
|
3106
|
+
|
|
3107
|
+
if (!owner) {
|
|
3108
|
+
return textResult(JSON.stringify({
|
|
3109
|
+
file: args.file,
|
|
3110
|
+
owned: false,
|
|
3111
|
+
message: 'No owner registered',
|
|
3112
|
+
}, null, 2));
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
return textResult(JSON.stringify({
|
|
3116
|
+
file: args.file,
|
|
3117
|
+
owned: true,
|
|
3118
|
+
worker: owner.worker,
|
|
3119
|
+
team: owner.team,
|
|
3120
|
+
warning: `This file belongs to worker "${owner.worker}". Rule 6: Do not edit worker files directly unless the fix is ≤3 lines and the worker is idle.`,
|
|
3121
|
+
}, null, 2));
|
|
3122
|
+
} catch (err) {
|
|
3123
|
+
return errorResult(err.message);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
function handleTeamReset(args) {
|
|
3128
|
+
try {
|
|
3129
|
+
if (!args.confirm) {
|
|
3130
|
+
return errorResult('Must pass confirm: true to reset team state. This is destructive and cannot be undone.');
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
const teamName = args.team || 'default';
|
|
3134
|
+
const baseDir = path.join(os.homedir(), '.clearctx', 'team', teamName);
|
|
3135
|
+
|
|
3136
|
+
const summary = {
|
|
3137
|
+
team: teamName,
|
|
3138
|
+
cleared: [],
|
|
3139
|
+
preserved: args.preserve_artifacts || [],
|
|
3140
|
+
};
|
|
3141
|
+
|
|
3142
|
+
// Clear artifacts (except preserved ones)
|
|
3143
|
+
const artifactsDir = path.join(baseDir, 'artifacts');
|
|
3144
|
+
if (fs.existsSync(artifactsDir)) {
|
|
3145
|
+
const indexPath = path.join(artifactsDir, 'index.json');
|
|
3146
|
+
if (fs.existsSync(indexPath)) {
|
|
3147
|
+
try {
|
|
3148
|
+
const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
3149
|
+
const preserveSet = new Set(args.preserve_artifacts || []);
|
|
3150
|
+
|
|
3151
|
+
if (preserveSet.size > 0) {
|
|
3152
|
+
// Filter out preserved artifacts
|
|
3153
|
+
const filtered = {};
|
|
3154
|
+
for (const [id, entry] of Object.entries(index)) {
|
|
3155
|
+
if (preserveSet.has(id)) {
|
|
3156
|
+
filtered[id] = entry;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
fs.writeFileSync(indexPath, JSON.stringify(filtered, null, 2));
|
|
3160
|
+
summary.cleared.push(`artifacts (kept ${preserveSet.size} preserved)`);
|
|
3161
|
+
} else {
|
|
3162
|
+
fs.writeFileSync(indexPath, '{}');
|
|
3163
|
+
summary.cleared.push('artifacts');
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// Clean data directory (version files and reads)
|
|
3167
|
+
const dataDir = path.join(artifactsDir, 'data');
|
|
3168
|
+
if (fs.existsSync(dataDir)) {
|
|
3169
|
+
const artifactDirs = fs.readdirSync(dataDir);
|
|
3170
|
+
for (const dir of artifactDirs) {
|
|
3171
|
+
if (!preserveSet.has(dir)) {
|
|
3172
|
+
const dirPath = path.join(dataDir, dir);
|
|
3173
|
+
// Remove all files in the directory
|
|
3174
|
+
const files = fs.readdirSync(dirPath);
|
|
3175
|
+
for (const file of files) {
|
|
3176
|
+
fs.unlinkSync(path.join(dirPath, file));
|
|
3177
|
+
}
|
|
3178
|
+
fs.rmdirSync(dirPath);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
} catch (e) {
|
|
3183
|
+
summary.cleared.push(`artifacts (error: ${e.message})`);
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// Clear contracts
|
|
3189
|
+
const contractsPath = path.join(baseDir, 'contracts.json');
|
|
3190
|
+
if (fs.existsSync(contractsPath)) {
|
|
3191
|
+
fs.writeFileSync(contractsPath, '{}');
|
|
3192
|
+
summary.cleared.push('contracts');
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
// Clear roster
|
|
3196
|
+
const rosterPath = path.join(baseDir, 'roster.json');
|
|
3197
|
+
if (fs.existsSync(rosterPath)) {
|
|
3198
|
+
fs.writeFileSync(rosterPath, '[]');
|
|
3199
|
+
summary.cleared.push('roster');
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
// Clear messages
|
|
3203
|
+
const messagesDir = path.join(baseDir, 'messages');
|
|
3204
|
+
if (fs.existsSync(messagesDir)) {
|
|
3205
|
+
const files = fs.readdirSync(messagesDir);
|
|
3206
|
+
for (const file of files) {
|
|
3207
|
+
fs.unlinkSync(path.join(messagesDir, file));
|
|
3208
|
+
}
|
|
3209
|
+
summary.cleared.push('messages');
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
// Clear pipelines
|
|
3213
|
+
const pipelinesPath = path.join(baseDir, 'pipelines.json');
|
|
3214
|
+
if (fs.existsSync(pipelinesPath)) {
|
|
3215
|
+
fs.writeFileSync(pipelinesPath, '{}');
|
|
3216
|
+
summary.cleared.push('pipelines');
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
// Clear locks
|
|
3220
|
+
const locksDir = path.join(baseDir, 'locks');
|
|
3221
|
+
if (fs.existsSync(locksDir)) {
|
|
3222
|
+
const lockFiles = fs.readdirSync(locksDir);
|
|
3223
|
+
for (const file of lockFiles) {
|
|
3224
|
+
fs.unlinkSync(path.join(locksDir, file));
|
|
3225
|
+
}
|
|
3226
|
+
summary.cleared.push('locks');
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// Reset in-memory team instances
|
|
3230
|
+
teamHub = null;
|
|
3231
|
+
artifactStore = null;
|
|
3232
|
+
contractStore = null;
|
|
3233
|
+
resolver = null;
|
|
3234
|
+
lineageGraph = null;
|
|
3235
|
+
pipelineEngine = null;
|
|
3236
|
+
snapshotEngine = null;
|
|
3237
|
+
currentTeamName = null;
|
|
3238
|
+
|
|
3239
|
+
// Clear gate enforcement state
|
|
3240
|
+
_gateState.delete(teamName);
|
|
3241
|
+
|
|
3242
|
+
// Clear inbox check enforcement state for this team
|
|
3243
|
+
for (const key of _inboxCheckState.keys()) {
|
|
3244
|
+
if (key.startsWith(teamName + ':')) {
|
|
3245
|
+
_inboxCheckState.delete(key);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
// Clear worker-to-team mapping for this team
|
|
3250
|
+
for (const [worker, team] of _workerTeamMap.entries()) {
|
|
3251
|
+
if (team === teamName) {
|
|
3252
|
+
_workerTeamMap.delete(worker);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
// Clear file ownership entries for this team
|
|
3257
|
+
for (const [filePath, owner] of _fileOwnership.entries()) {
|
|
3258
|
+
if (owner.team === teamName) {
|
|
3259
|
+
_fileOwnership.delete(filePath);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
summary.message = `Team "${teamName}" has been reset. ${summary.cleared.length} stores cleared.`;
|
|
3264
|
+
|
|
3265
|
+
return textResult(JSON.stringify(summary, null, 2));
|
|
3266
|
+
} catch (err) {
|
|
3267
|
+
return errorResult(err.message);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
// =============================================================================
|
|
3272
|
+
// Result Helpers
|
|
3273
|
+
// =============================================================================
|
|
3274
|
+
|
|
3275
|
+
function textResult(text) {
|
|
3276
|
+
return {
|
|
3277
|
+
content: [{ type: 'text', text: text + getCachedStalenessWarning() }],
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
function errorResult(message) {
|
|
3282
|
+
return {
|
|
3283
|
+
content: [{ type: 'text', text: `Error: ${message}` + getCachedStalenessWarning() }],
|
|
3284
|
+
isError: true,
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
// =============================================================================
|
|
3289
|
+
// MCP Protocol Handler — JSON-RPC 2.0 over stdio
|
|
3290
|
+
// =============================================================================
|
|
3291
|
+
|
|
3292
|
+
/**
|
|
3293
|
+
* Structured log to stderr (NEVER stdout — stdout is for MCP protocol only).
|
|
3294
|
+
* Includes ISO timestamp and server version for debugging.
|
|
3295
|
+
*/
|
|
3296
|
+
function log(msg, level = 'info') {
|
|
3297
|
+
const entry = JSON.stringify({
|
|
3298
|
+
ts: new Date().toISOString(),
|
|
3299
|
+
level,
|
|
3300
|
+
server: 'multi-session-mcp',
|
|
3301
|
+
version: LOADED_VERSION,
|
|
3302
|
+
msg,
|
|
3303
|
+
});
|
|
3304
|
+
process.stderr.write(entry + '\n');
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
/**
|
|
3308
|
+
* Send a JSON-RPC response on stdout.
|
|
3309
|
+
*/
|
|
3310
|
+
function sendResponse(id, result) {
|
|
3311
|
+
const msg = JSON.stringify({ jsonrpc: '2.0', id, result });
|
|
3312
|
+
process.stdout.write(msg + '\n');
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
/**
|
|
3316
|
+
* Send a JSON-RPC error on stdout.
|
|
3317
|
+
*/
|
|
3318
|
+
function sendError(id, code, message) {
|
|
3319
|
+
const msg = JSON.stringify({
|
|
3320
|
+
jsonrpc: '2.0',
|
|
3321
|
+
id,
|
|
3322
|
+
error: { code, message },
|
|
3323
|
+
});
|
|
3324
|
+
process.stdout.write(msg + '\n');
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
/**
|
|
3328
|
+
* Handle an incoming JSON-RPC message.
|
|
3329
|
+
*/
|
|
3330
|
+
async function handleMessage(message) {
|
|
3331
|
+
const { id, method, params } = message;
|
|
3332
|
+
|
|
3333
|
+
switch (method) {
|
|
3334
|
+
// ── Initialize ────────────────────────────────────────────────────
|
|
3335
|
+
case 'initialize':
|
|
3336
|
+
sendResponse(id, {
|
|
3337
|
+
protocolVersion: '2024-11-05',
|
|
3338
|
+
capabilities: {
|
|
3339
|
+
tools: {},
|
|
3340
|
+
},
|
|
3341
|
+
serverInfo: {
|
|
3342
|
+
name: 'clearctx',
|
|
3343
|
+
version: LOADED_VERSION,
|
|
3344
|
+
},
|
|
3345
|
+
});
|
|
3346
|
+
break;
|
|
3347
|
+
|
|
3348
|
+
// ── Initialized notification (no response needed) ─────────────────
|
|
3349
|
+
case 'notifications/initialized':
|
|
3350
|
+
log('Client initialized.');
|
|
3351
|
+
break;
|
|
3352
|
+
|
|
3353
|
+
// ── List tools ────────────────────────────────────────────────────
|
|
3354
|
+
case 'tools/list':
|
|
3355
|
+
sendResponse(id, { tools: TOOLS });
|
|
3356
|
+
break;
|
|
3357
|
+
|
|
3358
|
+
// ── Call a tool ───────────────────────────────────────────────────
|
|
3359
|
+
case 'tools/call':
|
|
3360
|
+
if (!params || !params.name) {
|
|
3361
|
+
sendError(id, -32602, 'Missing tool name');
|
|
3362
|
+
break;
|
|
3363
|
+
}
|
|
3364
|
+
try {
|
|
3365
|
+
const result = await executeTool(params.name, params.arguments || {});
|
|
3366
|
+
sendResponse(id, result);
|
|
3367
|
+
} catch (err) {
|
|
3368
|
+
sendResponse(id, errorResult(err.message));
|
|
3369
|
+
}
|
|
3370
|
+
break;
|
|
3371
|
+
|
|
3372
|
+
// ── Ping ──────────────────────────────────────────────────────────
|
|
3373
|
+
case 'ping':
|
|
3374
|
+
sendResponse(id, {});
|
|
3375
|
+
break;
|
|
3376
|
+
|
|
3377
|
+
// ── Unknown method ────────────────────────────────────────────────
|
|
3378
|
+
default:
|
|
3379
|
+
if (id !== undefined) {
|
|
3380
|
+
sendError(id, -32601, `Method not found: ${method}`);
|
|
3381
|
+
}
|
|
3382
|
+
// Notifications (no id) are silently ignored
|
|
3383
|
+
break;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
// =============================================================================
|
|
3388
|
+
// Start Server — read from stdin, write to stdout
|
|
3389
|
+
// =============================================================================
|
|
3390
|
+
|
|
3391
|
+
function startServer() {
|
|
3392
|
+
log('Starting MCP server...');
|
|
3393
|
+
|
|
3394
|
+
// Read newline-delimited JSON from stdin
|
|
3395
|
+
const rl = readline.createInterface({
|
|
3396
|
+
input: process.stdin,
|
|
3397
|
+
terminal: false,
|
|
3398
|
+
});
|
|
3399
|
+
|
|
3400
|
+
rl.on('line', async (line) => {
|
|
3401
|
+
if (!line.trim()) return;
|
|
3402
|
+
|
|
3403
|
+
let message;
|
|
3404
|
+
try {
|
|
3405
|
+
message = JSON.parse(line);
|
|
3406
|
+
} catch (e) {
|
|
3407
|
+
log(`Failed to parse message: ${e.message}`);
|
|
3408
|
+
// Send parse error if we can extract an id
|
|
3409
|
+
process.stdout.write(JSON.stringify({
|
|
3410
|
+
jsonrpc: '2.0',
|
|
3411
|
+
id: null,
|
|
3412
|
+
error: { code: -32700, message: 'Parse error' },
|
|
3413
|
+
}) + '\n');
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
// Validate JSON-RPC 2.0 format
|
|
3418
|
+
if (message.jsonrpc !== '2.0') {
|
|
3419
|
+
log(`Invalid JSON-RPC version: ${message.jsonrpc}`);
|
|
3420
|
+
if (message.id !== undefined) {
|
|
3421
|
+
sendError(message.id, -32600, 'Invalid Request: must use jsonrpc 2.0');
|
|
3422
|
+
}
|
|
3423
|
+
return;
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
await handleMessage(message);
|
|
3427
|
+
});
|
|
3428
|
+
|
|
3429
|
+
rl.on('close', () => {
|
|
3430
|
+
log('stdin closed. Shutting down...');
|
|
3431
|
+
// Clean up — stop all live sessions
|
|
3432
|
+
manager.stopAll();
|
|
3433
|
+
process.exit(0);
|
|
3434
|
+
});
|
|
3435
|
+
|
|
3436
|
+
// Graceful shutdown handler — works on Windows (SIGINT, SIGBREAK) and Unix (SIGTERM)
|
|
3437
|
+
let shuttingDown = false;
|
|
3438
|
+
function gracefulShutdown(signal) {
|
|
3439
|
+
if (shuttingDown) return; // Prevent double-shutdown
|
|
3440
|
+
shuttingDown = true;
|
|
3441
|
+
log(`${signal} received. Graceful shutdown starting...`);
|
|
3442
|
+
|
|
3443
|
+
// Stop accepting new work
|
|
3444
|
+
rl.close();
|
|
3445
|
+
|
|
3446
|
+
// Kill all spawned child sessions
|
|
3447
|
+
try {
|
|
3448
|
+
manager.stopAll();
|
|
3449
|
+
log('All sessions stopped.');
|
|
3450
|
+
} catch (err) {
|
|
3451
|
+
log(`Error stopping sessions: ${err.message}`, 'error');
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
// Force exit after 5 second timeout if cleanup hangs
|
|
3455
|
+
const forceTimer = setTimeout(() => {
|
|
3456
|
+
log('Shutdown timeout exceeded. Force exiting.', 'warn');
|
|
3457
|
+
process.exit(1);
|
|
3458
|
+
}, 5000);
|
|
3459
|
+
forceTimer.unref(); // Don't keep process alive just for this timer
|
|
3460
|
+
|
|
3461
|
+
process.exit(0);
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
3465
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
3466
|
+
|
|
3467
|
+
// Windows-specific: SIGBREAK is sent on Ctrl+Break
|
|
3468
|
+
if (process.platform === 'win32') {
|
|
3469
|
+
process.on('SIGBREAK', () => gracefulShutdown('SIGBREAK'));
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// Handle uncaught errors gracefully
|
|
3473
|
+
process.on('uncaughtException', (err) => {
|
|
3474
|
+
log(`Uncaught exception: ${err.message}\n${err.stack}`, 'error');
|
|
3475
|
+
gracefulShutdown('uncaughtException');
|
|
3476
|
+
});
|
|
3477
|
+
|
|
3478
|
+
process.on('unhandledRejection', (reason) => {
|
|
3479
|
+
log(`Unhandled rejection: ${reason}`, 'error');
|
|
3480
|
+
// Don't shutdown on unhandled rejections — log and continue
|
|
3481
|
+
});
|
|
3482
|
+
|
|
3483
|
+
log('MCP server ready. Waiting for messages...');
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
module.exports = { startServer, TOOLS, executeTool };
|
|
3487
|
+
|
|
3488
|
+
// Test-only exports — expose internal state for unit testing
|
|
3489
|
+
if (process.env.CMS_TEST_MODE) {
|
|
3490
|
+
Object.assign(module.exports, {
|
|
3491
|
+
_inboxCheckState,
|
|
3492
|
+
_gateState,
|
|
3493
|
+
_fileOwnership,
|
|
3494
|
+
_workerTeamMap,
|
|
3495
|
+
checkInboxEnforcement,
|
|
3496
|
+
autoSetWorkerIdle,
|
|
3497
|
+
getRosterSummary,
|
|
3498
|
+
getTeamInstances,
|
|
3499
|
+
getGateState,
|
|
3500
|
+
});
|
|
3501
|
+
}
|