claude-multi-session 2.5.0 → 2.6.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/bin/setup.js +158 -18
- package/package.json +1 -1
- package/src/delegate.js +16 -0
- package/src/mcp-server.js +435 -51
- package/src/prompts.js +38 -21
package/bin/setup.js
CHANGED
|
@@ -43,6 +43,10 @@ const CLAUDE_MD_END_MARKER = '<!-- claude-multi-session:end -->';
|
|
|
43
43
|
|
|
44
44
|
// This teaches the main Claude session how to orchestrate team workers.
|
|
45
45
|
// Wrapped in markers so we can find and remove it later.
|
|
46
|
+
//
|
|
47
|
+
// IMPORTANT: This content MUST stay in sync with docs/ORCHESTRATOR-CLAUDE.md.
|
|
48
|
+
// When updating rules or adding features, update BOTH files.
|
|
49
|
+
// Failure to sync caused all test runs 1-6 to use stale orchestrator rules.
|
|
46
50
|
const STRATEGY_CONTENT = `
|
|
47
51
|
${CLAUDE_MD_START_MARKER}
|
|
48
52
|
|
|
@@ -52,11 +56,39 @@ You have access to a Multi-Session MCP server (\`mcp__multi-session__*\` tools)
|
|
|
52
56
|
|
|
53
57
|
IMPORTANT: When the user asks you to build something complex (more than 2 related tasks), use the multi-session system to parallelize the work instead of doing everything yourself.
|
|
54
58
|
|
|
59
|
+
## Step 0: Verify Your Tools
|
|
60
|
+
|
|
61
|
+
Before starting ANY orchestration work, call \`server_version()\` to verify you're running the latest MCP tools. If the response shows a version mismatch, tell the user to restart Claude Code before proceeding — stale tools cause phantom failures.
|
|
62
|
+
|
|
63
|
+
## How to Orchestrate
|
|
64
|
+
|
|
65
|
+
### Rule 0: Define shared conventions BEFORE spawning workers
|
|
66
|
+
Before spawning workers, fill in the CONVENTION CHECKLIST. Either publish as an artifact (\`shared-conventions\`) or embed in every worker's prompt.
|
|
67
|
+
|
|
68
|
+
=== CONVENTION CHECKLIST (define every item before spawning) ===
|
|
69
|
+
- [ ] Response format: e.g., \`{ data: <result> }\`
|
|
70
|
+
- [ ] Error format: e.g., \`{ error: <message> }\`
|
|
71
|
+
- [ ] Status codes: create=201, read=200, update=200, delete=200, notFound=404, badRequest=400, conflict=409
|
|
72
|
+
- [ ] Naming: e.g., snake_case for DB columns, camelCase for JS variables
|
|
73
|
+
- [ ] File paths: relative only, never absolute
|
|
74
|
+
- [ ] Enum/status values: list EXACT strings (e.g., "pending", "in_progress", "completed" — NOT "Pending" or "InProgress")
|
|
75
|
+
- [ ] Boolean handling: true/false vs 1/0 — pick one, specify it
|
|
76
|
+
- [ ] Date format: ISO 8601 strings, Unix timestamps, or other — specify which
|
|
77
|
+
- [ ] Audit/log action names: exact strings (e.g., "created" vs "create" vs "CREATE")
|
|
78
|
+
- [ ] Shared column names: list exact DB column names for tables multiple workers reference
|
|
79
|
+
|
|
80
|
+
Missing even ONE item causes convention mismatches that the orchestrator then has to fix manually — which violates Rule 6.
|
|
81
|
+
|
|
82
|
+
NEVER assume workers will independently agree on conventions — they won't.
|
|
83
|
+
|
|
55
84
|
### Rule 1: You are the ORCHESTRATOR — not the implementer
|
|
56
85
|
- Plan the work, spawn workers, monitor progress
|
|
57
86
|
- Do NOT implement code yourself when you can delegate
|
|
87
|
+
- Do NOT create project foundation files (package.json, db.js, app.js, server.js) yourself — spawn a setup worker for Phase 0
|
|
58
88
|
- Do NOT read full outputs from workers — check artifacts and contract status instead
|
|
59
89
|
|
|
90
|
+
**Phase 0: Foundation Setup** — If the project needs shared infrastructure (database, app skeleton, package.json), spawn a \`setup\` worker FIRST. Wait for its \`project-foundation\` artifact before spawning other workers. Do NOT create these files yourself.
|
|
91
|
+
|
|
60
92
|
### Rule 2: Use team_spawn for multi-session work
|
|
61
93
|
IMPORTANT: Spawn ALL independent workers in a SINGLE message with multiple tool calls. This makes them run in parallel.
|
|
62
94
|
|
|
@@ -64,10 +96,39 @@ IMPORTANT: Spawn ALL independent workers in a SINGLE message with multiple tool
|
|
|
64
96
|
- Workers have team_ask, team_send_message, team_broadcast tools
|
|
65
97
|
- They can publish and read artifacts directly
|
|
66
98
|
- You should NOT relay messages between them
|
|
99
|
+
- If workers need each other's output, tell them to use team_ask
|
|
100
|
+
- Note: team_ask is a **fallback** for unexpected ambiguity. In well-orchestrated projects where you provide all context upfront, team_ask may never be called — this is the ideal case.
|
|
101
|
+
|
|
102
|
+
### Rule 4: Post-Phase Verification (MANDATORY)
|
|
103
|
+
After ALL workers in a phase complete, BEFORE spawning the next phase, STOP and fill in this checklist:
|
|
67
104
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
105
|
+
=== PHASE GATE CHECKPOINT (use phase_gate tool before EVERY team_spawn after Phase 0) ===
|
|
106
|
+
|
|
107
|
+
Instead of manually running 4 separate tool calls, use the \`phase_gate\` tool which does ALL checks in one call:
|
|
108
|
+
|
|
109
|
+
\`\`\`
|
|
110
|
+
mcp__multi-session__phase_gate({
|
|
111
|
+
phase_completing: "Phase 0: Foundation",
|
|
112
|
+
phase_starting: "Phase 1: Routes",
|
|
113
|
+
expected_artifacts: ["project-foundation", "shared-conventions"],
|
|
114
|
+
expected_idle: ["setup"],
|
|
115
|
+
expected_readers: { "shared-conventions": ["api-dev", "db-dev"] }
|
|
116
|
+
})
|
|
117
|
+
\`\`\`
|
|
118
|
+
|
|
119
|
+
The tool automatically:
|
|
120
|
+
1. Checks all expected artifacts exist
|
|
121
|
+
2. Validates artifact content and tracks the read as "orchestrator"
|
|
122
|
+
3. Verifies all previous-phase workers are idle
|
|
123
|
+
4. Confirms expected consumers actually read the artifacts
|
|
124
|
+
|
|
125
|
+
Returns a structured pass/fail report with recommendation.
|
|
126
|
+
PROCEED ONLY IF the report says ALL CHECKS PASSED.
|
|
127
|
+
|
|
128
|
+
Count your phases upfront. If you have N phases, fill in this checkpoint exactly N-1 times (between every adjacent pair of phases). Skipping verification for later phases is the #1 cause of test failures.
|
|
129
|
+
|
|
130
|
+
Only intervene in workers when a session is BLOCKED or FAILED.
|
|
131
|
+
Do NOT verify worker output by reading files directly — check artifacts instead.
|
|
71
132
|
|
|
72
133
|
### Rule 5: Always tell workers to publish artifacts
|
|
73
134
|
Every worker prompt should include instructions to:
|
|
@@ -76,22 +137,65 @@ Every worker prompt should include instructions to:
|
|
|
76
137
|
3. Publish output as artifacts (\`artifact_publish\`)
|
|
77
138
|
4. Broadcast completion (\`team_broadcast\`)
|
|
78
139
|
5. Update status to idle when done (\`team_update_status\`)
|
|
140
|
+
6. Follow shared conventions defined in Rule 0 (include them in the prompt or reference the conventions artifact)
|
|
141
|
+
|
|
142
|
+
### Rule 6: Don't fix worker code yourself (pragmatic exception for trivial fixes)
|
|
143
|
+
|
|
144
|
+
=== FIX PROTOCOL (when you must fix worker code directly) ===
|
|
145
|
+
STOP. Before editing any file a worker created, answer these questions:
|
|
146
|
+
|
|
147
|
+
1. Is this fix ≤ 3 lines?
|
|
148
|
+
NO → \`send_message\` to worker or spawn fix-worker. Do NOT fix yourself.
|
|
149
|
+
YES → continue to step 2.
|
|
150
|
+
|
|
151
|
+
2. Is the worker done (idle status in \`team_roster\`)?
|
|
152
|
+
NO → \`send_message\` to worker. Do NOT fix yourself.
|
|
153
|
+
YES → continue to step 3.
|
|
154
|
+
|
|
155
|
+
3. Make the fix.
|
|
156
|
+
|
|
157
|
+
4. Broadcast: \`team_broadcast({ from: "orchestrator", content: "Fixed [file]:[lines] — [description of change]" })\`
|
|
158
|
+
|
|
159
|
+
5. Re-publish: If the fix changes data in a published artifact, call \`artifact_publish\` to update it.
|
|
160
|
+
|
|
161
|
+
NEVER skip steps 4-5. Unannounced fixes cause downstream workers to use stale assumptions.
|
|
162
|
+
|
|
163
|
+
If the failure is due to convention mismatch (wrong response format, etc.), that's YOUR fault — update the conventions and notify the affected workers.
|
|
164
|
+
|
|
165
|
+
### Rule 7: Verify artifacts between phases (Phase Gates)
|
|
166
|
+
Use the PHASE GATE CHECKPOINT from Rule 4 between every pair of phases. This is the same checklist — Rule 7 reinforces that it applies to EVERY phase transition, not just the first one.
|
|
167
|
+
|
|
168
|
+
After all workers finish, verify they consumed shared artifacts:
|
|
169
|
+
\`\`\`
|
|
170
|
+
mcp__multi-session__artifact_readers({ artifactId: "shared-conventions" })
|
|
171
|
+
\`\`\`
|
|
172
|
+
This shows which workers actually read the conventions. If a worker is missing, they may have ignored the shared contract.
|
|
173
|
+
|
|
174
|
+
NEVER trust a worker's self-reported completion — verify the artifact exists yourself.
|
|
175
|
+
|
|
176
|
+
## Quick Reference
|
|
79
177
|
|
|
80
|
-
### Quick Reference
|
|
81
178
|
| You want to... | Use this tool |
|
|
82
179
|
|----------------|---------------|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
180
|
+
| Verify tools before starting | \`server_version\` |
|
|
181
|
+
| Build a multi-person project | \`team_spawn\` (multiple in parallel) |
|
|
182
|
+
| Run a single isolated task | \`delegate_task\` |
|
|
183
|
+
| Check who's working on what | \`team_roster\` |
|
|
86
184
|
| See published outputs | \`artifact_list\` |
|
|
87
|
-
|
|
|
88
|
-
| Send correction to worker | \`send_message\` to that session |
|
|
185
|
+
| See task completion status | \`contract_list\` |
|
|
186
|
+
| Send a correction to a worker | \`send_message\` to that session |
|
|
187
|
+
| Check who read an artifact | \`artifact_readers\` |
|
|
188
|
+
| Verify phase completion | \`phase_gate\` |
|
|
189
|
+
| Clean up between runs | \`team_reset\` |
|
|
89
190
|
|
|
90
191
|
### When NOT to Delegate
|
|
91
192
|
- Simple tasks (< 5 min, < 3 files) — do it yourself
|
|
92
193
|
- Just reading/exploring — use Read, Grep, Glob directly
|
|
93
194
|
- Tightly coupled changes — must happen atomically
|
|
94
195
|
|
|
196
|
+
### Resetting Between Runs
|
|
197
|
+
Call \`team_reset({ confirm: true })\` to clean up all team state between orchestration runs. This clears artifacts, contracts, roster, and messages.
|
|
198
|
+
|
|
95
199
|
${CLAUDE_MD_END_MARKER}
|
|
96
200
|
`;
|
|
97
201
|
|
|
@@ -305,8 +409,27 @@ function addGuide(scope) {
|
|
|
305
409
|
existing = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
306
410
|
}
|
|
307
411
|
|
|
308
|
-
// Check if our section already exists
|
|
412
|
+
// Check if our section already exists
|
|
309
413
|
if (existing.includes(CLAUDE_MD_START_MARKER)) {
|
|
414
|
+
// Extract current content between markers and compare with latest
|
|
415
|
+
const startIdx = existing.indexOf(CLAUDE_MD_START_MARKER);
|
|
416
|
+
const endIdx = existing.indexOf(CLAUDE_MD_END_MARKER);
|
|
417
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
418
|
+
const currentContent = existing.slice(startIdx, endIdx + CLAUDE_MD_END_MARKER.length).trim();
|
|
419
|
+
const newContent = STRATEGY_CONTENT.trim();
|
|
420
|
+
if (currentContent === newContent) {
|
|
421
|
+
result.skipped = true;
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
// Content is stale — replace our section, preserve user's other content
|
|
425
|
+
const before = existing.slice(0, startIdx).trimEnd();
|
|
426
|
+
const after = existing.slice(endIdx + CLAUDE_MD_END_MARKER.length).trimStart();
|
|
427
|
+
const updated = (before ? before + '\n\n' : '') + newContent + (after ? '\n\n' + after : '\n');
|
|
428
|
+
fs.writeFileSync(claudeMdPath, updated, 'utf-8');
|
|
429
|
+
result.updated = true;
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
// Malformed markers — skip to be safe
|
|
310
433
|
result.skipped = true;
|
|
311
434
|
return result;
|
|
312
435
|
}
|
|
@@ -410,10 +533,15 @@ function runPostinstallHint() {
|
|
|
410
533
|
write(' Run "cms-setup" to configure orchestrator guide.');
|
|
411
534
|
write('');
|
|
412
535
|
} else {
|
|
413
|
-
// Already registered —
|
|
536
|
+
// Already registered — check if CLAUDE.md guide needs updating
|
|
414
537
|
write('');
|
|
415
|
-
|
|
416
|
-
|
|
538
|
+
const guideResult = addGuide('global');
|
|
539
|
+
if (guideResult.updated) {
|
|
540
|
+
write(' claude-multi-session: Orchestrator guide updated in ~/.claude/CLAUDE.md');
|
|
541
|
+
write(' Restart Claude Code to use the latest rules.');
|
|
542
|
+
} else {
|
|
543
|
+
write(' claude-multi-session: Already configured and up to date.');
|
|
544
|
+
}
|
|
417
545
|
write('');
|
|
418
546
|
}
|
|
419
547
|
} catch {
|
|
@@ -563,9 +691,19 @@ async function runInteractiveSetup(flags) {
|
|
|
563
691
|
if (wantGuide === null) {
|
|
564
692
|
// Tell the user what will happen before they confirm
|
|
565
693
|
if (guideAlreadyInjected) {
|
|
566
|
-
//
|
|
567
|
-
|
|
568
|
-
|
|
694
|
+
// Check if content is stale
|
|
695
|
+
const currentFile = fs.readFileSync(guideTargetPath, 'utf-8');
|
|
696
|
+
const startIdx = currentFile.indexOf(CLAUDE_MD_START_MARKER);
|
|
697
|
+
const endIdx = currentFile.indexOf(CLAUDE_MD_END_MARKER);
|
|
698
|
+
const currentContent = currentFile.slice(startIdx, endIdx + CLAUDE_MD_END_MARKER.length).trim();
|
|
699
|
+
const isStale = currentContent !== STRATEGY_CONTENT.trim();
|
|
700
|
+
if (isStale) {
|
|
701
|
+
clack.log.warn(`Orchestrator guide in ${guideDisplayPath} is outdated.`);
|
|
702
|
+
wantGuide = true; // Auto-update — addGuide handles the replacement
|
|
703
|
+
} else {
|
|
704
|
+
clack.log.info(`Orchestrator guide in ${guideDisplayPath} is up to date.`);
|
|
705
|
+
wantGuide = false;
|
|
706
|
+
}
|
|
569
707
|
} else if (guideFileExists) {
|
|
570
708
|
// File exists — reassure user their content is safe
|
|
571
709
|
clack.log.info(`Found existing ${guideDisplayPath} — your content will be preserved.`);
|
|
@@ -599,8 +737,10 @@ async function runInteractiveSetup(flags) {
|
|
|
599
737
|
if (wantGuide) {
|
|
600
738
|
const guideResult = addGuide(scope);
|
|
601
739
|
|
|
602
|
-
if (guideResult.
|
|
603
|
-
clack.log.
|
|
740
|
+
if (guideResult.updated) {
|
|
741
|
+
clack.log.success(`Updated orchestrator guide in ${guideDisplayPath} (new rules synced)`);
|
|
742
|
+
} else if (guideResult.skipped) {
|
|
743
|
+
clack.log.info('Orchestrator guide already up to date — skipped.');
|
|
604
744
|
} else if (guideResult.created) {
|
|
605
745
|
clack.log.success(`Created ${guideDisplayPath} with orchestrator guide`);
|
|
606
746
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-multi-session",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Multi-session orchestrator for Claude Code CLI — spawn, control, pause, resume, and send multiple inputs to Claude Code sessions programmatically",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/delegate.js
CHANGED
|
@@ -278,13 +278,29 @@ class Delegate {
|
|
|
278
278
|
|
|
279
279
|
/**
|
|
280
280
|
* Handle a permission denial by sending approval.
|
|
281
|
+
* Max 2 retries to prevent infinite permission loops.
|
|
281
282
|
*/
|
|
282
283
|
async _handlePermissionRetry(name, deniedText) {
|
|
284
|
+
// Track retry count per session
|
|
285
|
+
if (!this._permRetries) this._permRetries = new Map();
|
|
286
|
+
const retries = (this._permRetries.get(name) || 0) + 1;
|
|
287
|
+
this._permRetries.set(name, retries);
|
|
288
|
+
|
|
289
|
+
if (retries > 2) {
|
|
290
|
+
return null; // Give up after 2 retries
|
|
291
|
+
}
|
|
292
|
+
|
|
283
293
|
try {
|
|
284
294
|
const response = await this.manager.send(
|
|
285
295
|
name,
|
|
286
296
|
'Yes, you have permission. Go ahead and proceed with all file operations. Do not ask for permission again — you are fully authorized.'
|
|
287
297
|
);
|
|
298
|
+
|
|
299
|
+
// Check if response still indicates permission denial
|
|
300
|
+
if (response && this._isPermissionDenied(response.text)) {
|
|
301
|
+
return null; // Still denied, don't retry further
|
|
302
|
+
}
|
|
303
|
+
|
|
288
304
|
return response;
|
|
289
305
|
} catch (err) {
|
|
290
306
|
return null;
|
package/src/mcp-server.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
const fs = require('fs');
|
|
29
29
|
const path = require('path');
|
|
30
|
+
const os = require('os');
|
|
30
31
|
const readline = require('readline');
|
|
31
32
|
const SessionManager = require('./manager');
|
|
32
33
|
const Delegate = require('./delegate');
|
|
@@ -84,6 +85,11 @@ let currentTeamName = null;
|
|
|
84
85
|
* @returns {Object} Object with all team instances
|
|
85
86
|
*/
|
|
86
87
|
function getTeamInstances(teamName = 'default') {
|
|
88
|
+
// Validate team name to prevent path traversal attacks
|
|
89
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(teamName)) {
|
|
90
|
+
throw new Error(`Invalid team name "${teamName}": must contain only alphanumeric characters, hyphens, and underscores`);
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
// If team name changed or instances not yet created, recreate them
|
|
88
94
|
if (!teamHub || currentTeamName !== teamName) {
|
|
89
95
|
teamHub = new TeamHub(teamName);
|
|
@@ -428,6 +434,7 @@ const TOOLS = [
|
|
|
428
434
|
model: { type: 'string', enum: ['sonnet', 'opus', 'haiku'], description: 'Model to use (default: sonnet)' },
|
|
429
435
|
permission_mode: { type: 'string', enum: ['default', 'acceptEdits', 'bypassPermissions', 'plan'], description: 'Permission mode. Use bypassPermissions to allow sessions to write files without approval (default: bypassPermissions)' },
|
|
430
436
|
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
437
|
+
work_dir: { type: 'string', description: 'Working directory for the session (default: current directory)' },
|
|
431
438
|
},
|
|
432
439
|
required: ['name', 'prompt'],
|
|
433
440
|
},
|
|
@@ -951,6 +958,43 @@ const TOOLS = [
|
|
|
951
958
|
},
|
|
952
959
|
},
|
|
953
960
|
|
|
961
|
+
// ── Phase Gate & Team Reset ─────────────────────────────────────────
|
|
962
|
+
{
|
|
963
|
+
name: 'phase_gate',
|
|
964
|
+
description:
|
|
965
|
+
'Run all 4 phase gate checks in a single call. Verifies: (1) expected artifacts exist, ' +
|
|
966
|
+
'(2) artifact content is valid, (3) all previous-phase workers are idle, (4) expected consumers read artifacts. ' +
|
|
967
|
+
'Returns a structured pass/fail report. Use this BETWEEN every pair of phases.',
|
|
968
|
+
inputSchema: {
|
|
969
|
+
type: 'object',
|
|
970
|
+
properties: {
|
|
971
|
+
phase_completing: { type: 'string', description: 'Name of the phase that just completed (e.g. "Phase 0: Foundation")' },
|
|
972
|
+
phase_starting: { type: 'string', description: 'Name of the phase about to start (e.g. "Phase 1: Routes")' },
|
|
973
|
+
expected_artifacts: { type: 'array', items: { type: 'string' }, description: 'Artifact IDs that should exist before proceeding' },
|
|
974
|
+
expected_idle: { type: 'array', items: { type: 'string' }, description: 'Worker names that should be idle (optional — if omitted, checks ALL roster members)' },
|
|
975
|
+
expected_readers: { type: 'object', description: 'Map of artifactId -> array of expected reader names. E.g. {"shared-conventions": ["api-dev", "db-dev"]}' },
|
|
976
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
977
|
+
},
|
|
978
|
+
required: ['phase_completing', 'phase_starting', 'expected_artifacts'],
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
|
|
982
|
+
{
|
|
983
|
+
name: 'team_reset',
|
|
984
|
+
description:
|
|
985
|
+
'Reset all team state — clear artifacts, contracts, roster, messages. ' +
|
|
986
|
+
'Use this between orchestration runs to start fresh. Optionally preserve specific artifacts.',
|
|
987
|
+
inputSchema: {
|
|
988
|
+
type: 'object',
|
|
989
|
+
properties: {
|
|
990
|
+
team: { type: 'string', description: 'Team name (default: "default")' },
|
|
991
|
+
preserve_artifacts: { type: 'array', items: { type: 'string' }, description: 'Artifact IDs to keep (optional)' },
|
|
992
|
+
confirm: { type: 'boolean', description: 'Must be true to execute (safety check)' },
|
|
993
|
+
},
|
|
994
|
+
required: ['confirm'],
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
|
|
954
998
|
// ── Session Continuity (Layer 0) ──────────────────────────────────────
|
|
955
999
|
{
|
|
956
1000
|
name: 'continuity_snapshot',
|
|
@@ -1109,6 +1153,37 @@ const TOOLS = [
|
|
|
1109
1153
|
}
|
|
1110
1154
|
];
|
|
1111
1155
|
|
|
1156
|
+
// =============================================================================
|
|
1157
|
+
// Staleness Warning — cached check for version drift
|
|
1158
|
+
// =============================================================================
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Check if the server is stale and return a warning string if so.
|
|
1162
|
+
* Called on every tool response to ensure stale servers are noticed.
|
|
1163
|
+
*/
|
|
1164
|
+
function getStalenessWarning() {
|
|
1165
|
+
try {
|
|
1166
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
1167
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1168
|
+
if (LOADED_VERSION !== pkg.version) {
|
|
1169
|
+
return `\n\n⚠️ STALE SERVER: Running v${LOADED_VERSION} but v${pkg.version} is installed. Restart Claude Code to load updated tools.`;
|
|
1170
|
+
}
|
|
1171
|
+
} catch (e) {
|
|
1172
|
+
// Ignore — can't check staleness
|
|
1173
|
+
}
|
|
1174
|
+
return '';
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Cache the staleness check result for 60 seconds to avoid reading package.json on every call
|
|
1178
|
+
let _stalenessCache = { warning: '', checkedAt: 0 };
|
|
1179
|
+
function getCachedStalenessWarning() {
|
|
1180
|
+
const now = Date.now();
|
|
1181
|
+
if (now - _stalenessCache.checkedAt > 60000) {
|
|
1182
|
+
_stalenessCache = { warning: getStalenessWarning(), checkedAt: now };
|
|
1183
|
+
}
|
|
1184
|
+
return _stalenessCache.warning;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1112
1187
|
// =============================================================================
|
|
1113
1188
|
// Tool Handlers — execute each tool and return result
|
|
1114
1189
|
// =============================================================================
|
|
@@ -1252,6 +1327,12 @@ async function executeTool(toolName, args) {
|
|
|
1252
1327
|
case 'team_replay':
|
|
1253
1328
|
return await handleTeamReplay(args);
|
|
1254
1329
|
|
|
1330
|
+
// ── Phase Gate & Team Reset ──
|
|
1331
|
+
case 'phase_gate':
|
|
1332
|
+
return handlePhaseGate(args);
|
|
1333
|
+
case 'team_reset':
|
|
1334
|
+
return handleTeamReset(args);
|
|
1335
|
+
|
|
1255
1336
|
// ── Session Continuity (Layer 0) handlers ──
|
|
1256
1337
|
case 'continuity_snapshot': {
|
|
1257
1338
|
const snap = new SessionSnapshot(args.projectPath);
|
|
@@ -1687,6 +1768,7 @@ async function handleTeamSpawn(args) {
|
|
|
1687
1768
|
model: args.model,
|
|
1688
1769
|
systemPrompt: teamSystemPrompt,
|
|
1689
1770
|
permissionMode: args.permission_mode || 'bypassPermissions',
|
|
1771
|
+
workDir: args.work_dir || process.cwd(),
|
|
1690
1772
|
});
|
|
1691
1773
|
|
|
1692
1774
|
const result = {
|
|
@@ -1704,6 +1786,12 @@ async function handleTeamSpawn(args) {
|
|
|
1704
1786
|
result.turns = response.turns;
|
|
1705
1787
|
}
|
|
1706
1788
|
|
|
1789
|
+
// Auto version check on first spawn
|
|
1790
|
+
const staleness = getCachedStalenessWarning();
|
|
1791
|
+
if (staleness) {
|
|
1792
|
+
result._staleness_warning = `Server is stale! ${staleness.trim()}`;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1707
1795
|
return textResult(JSON.stringify(result, null, 2));
|
|
1708
1796
|
} catch (err) {
|
|
1709
1797
|
return errorResult(err.message);
|
|
@@ -1953,9 +2041,9 @@ function handleArtifactGet(args) {
|
|
|
1953
2041
|
readBy: artifactStore.getReads(args.artifactId),
|
|
1954
2042
|
};
|
|
1955
2043
|
|
|
1956
|
-
// Add
|
|
2044
|
+
// Add prominent warning if reader param was not provided
|
|
1957
2045
|
if (!args.reader) {
|
|
1958
|
-
response.
|
|
2046
|
+
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" })';
|
|
1959
2047
|
}
|
|
1960
2048
|
|
|
1961
2049
|
return textResult(JSON.stringify(response, null, 2));
|
|
@@ -1979,18 +2067,22 @@ function handleArtifactList(args) {
|
|
|
1979
2067
|
return textResult(JSON.stringify({
|
|
1980
2068
|
team: teamName,
|
|
1981
2069
|
count: artifacts.length,
|
|
1982
|
-
artifacts: artifacts.map(a =>
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2070
|
+
artifacts: artifacts.map(a => {
|
|
2071
|
+
const reads = artifactStore.getReads(a.artifactId);
|
|
2072
|
+
const readers = [...new Set(reads.map(r => r.reader))];
|
|
2073
|
+
return {
|
|
2074
|
+
artifactId: a.artifactId,
|
|
2075
|
+
type: a.type,
|
|
2076
|
+
name: a.name,
|
|
2077
|
+
publisher: a.publisher,
|
|
2078
|
+
latestVersion: a.latestVersion,
|
|
2079
|
+
createdAt: a.createdAt,
|
|
2080
|
+
updatedAt: a.updatedAt,
|
|
2081
|
+
tags: a.tags,
|
|
2082
|
+
readCount: reads.length,
|
|
2083
|
+
readers: readers,
|
|
2084
|
+
};
|
|
2085
|
+
}),
|
|
1994
2086
|
}, null, 2));
|
|
1995
2087
|
} catch (err) {
|
|
1996
2088
|
return errorResult(err.message);
|
|
@@ -2543,37 +2635,289 @@ async function handleTeamReplay(args) {
|
|
|
2543
2635
|
}
|
|
2544
2636
|
|
|
2545
2637
|
// =============================================================================
|
|
2546
|
-
//
|
|
2638
|
+
// Phase Gate & Team Reset Handlers
|
|
2547
2639
|
// =============================================================================
|
|
2548
2640
|
|
|
2549
|
-
function
|
|
2550
|
-
|
|
2551
|
-
|
|
2641
|
+
function handlePhaseGate(args) {
|
|
2642
|
+
try {
|
|
2643
|
+
const teamName = args.team || 'default';
|
|
2644
|
+
const { artifactStore, teamHub } = getTeamInstances(teamName);
|
|
2552
2645
|
|
|
2553
|
-
|
|
2554
|
-
|
|
2646
|
+
const report = {
|
|
2647
|
+
gate: `${args.phase_completing} → ${args.phase_starting}`,
|
|
2648
|
+
timestamp: new Date().toISOString(),
|
|
2649
|
+
checks: [],
|
|
2650
|
+
passed: true,
|
|
2651
|
+
};
|
|
2652
|
+
|
|
2653
|
+
// Check 1: Expected artifacts exist
|
|
2654
|
+
const allArtifacts = artifactStore.list({});
|
|
2655
|
+
const existingIds = new Set(allArtifacts.map(a => a.artifactId));
|
|
2656
|
+
const artifactCheck = {
|
|
2657
|
+
check: 'artifacts_exist',
|
|
2658
|
+
expected: args.expected_artifacts,
|
|
2659
|
+
found: [],
|
|
2660
|
+
missing: [],
|
|
2661
|
+
passed: true,
|
|
2662
|
+
};
|
|
2663
|
+
|
|
2664
|
+
for (const id of args.expected_artifacts) {
|
|
2665
|
+
if (existingIds.has(id)) {
|
|
2666
|
+
artifactCheck.found.push(id);
|
|
2667
|
+
} else {
|
|
2668
|
+
artifactCheck.missing.push(id);
|
|
2669
|
+
artifactCheck.passed = false;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
report.checks.push(artifactCheck);
|
|
2673
|
+
|
|
2674
|
+
// Check 2: Artifact content valid (get each artifact with reader="orchestrator")
|
|
2675
|
+
const contentCheck = {
|
|
2676
|
+
check: 'artifacts_valid',
|
|
2677
|
+
results: [],
|
|
2678
|
+
passed: true,
|
|
2679
|
+
};
|
|
2680
|
+
|
|
2681
|
+
for (const id of artifactCheck.found) {
|
|
2682
|
+
const artifact = artifactStore.get(id);
|
|
2683
|
+
// Track read as orchestrator
|
|
2684
|
+
artifactStore.trackRead(id, 'orchestrator', artifact?.version);
|
|
2685
|
+
|
|
2686
|
+
if (!artifact) {
|
|
2687
|
+
contentCheck.results.push({ artifactId: id, valid: false, reason: 'Could not read artifact' });
|
|
2688
|
+
contentCheck.passed = false;
|
|
2689
|
+
} else if (!artifact.data || (typeof artifact.data === 'object' && Object.keys(artifact.data).length === 0)) {
|
|
2690
|
+
contentCheck.results.push({ artifactId: id, valid: false, reason: 'Artifact data is empty' });
|
|
2691
|
+
contentCheck.passed = false;
|
|
2692
|
+
} else {
|
|
2693
|
+
contentCheck.results.push({
|
|
2694
|
+
artifactId: id,
|
|
2695
|
+
valid: true,
|
|
2696
|
+
version: artifact.version,
|
|
2697
|
+
publisher: artifact.publisher,
|
|
2698
|
+
summary: artifact.summary || '(no summary)',
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
report.checks.push(contentCheck);
|
|
2703
|
+
|
|
2704
|
+
// Check 3: Workers idle
|
|
2705
|
+
const roster = teamHub.getRoster();
|
|
2706
|
+
const idleCheck = {
|
|
2707
|
+
check: 'workers_idle',
|
|
2708
|
+
results: [],
|
|
2709
|
+
passed: true,
|
|
2710
|
+
};
|
|
2711
|
+
|
|
2712
|
+
const workersToCheck = args.expected_idle
|
|
2713
|
+
? roster.filter(m => args.expected_idle.includes(m.name))
|
|
2714
|
+
: roster;
|
|
2715
|
+
|
|
2716
|
+
for (const member of workersToCheck) {
|
|
2717
|
+
const isIdle = member.status === 'idle';
|
|
2718
|
+
idleCheck.results.push({
|
|
2719
|
+
name: member.name,
|
|
2720
|
+
status: member.status,
|
|
2721
|
+
task: member.task,
|
|
2722
|
+
idle: isIdle,
|
|
2723
|
+
});
|
|
2724
|
+
if (!isIdle) {
|
|
2725
|
+
idleCheck.passed = false;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
report.checks.push(idleCheck);
|
|
2729
|
+
|
|
2730
|
+
// Check 4: Artifact readers verification
|
|
2731
|
+
const readerCheck = {
|
|
2732
|
+
check: 'artifact_readers',
|
|
2733
|
+
results: [],
|
|
2734
|
+
passed: true,
|
|
2735
|
+
};
|
|
2736
|
+
|
|
2737
|
+
if (args.expected_readers) {
|
|
2738
|
+
for (const [artifactId, expectedReaders] of Object.entries(args.expected_readers)) {
|
|
2739
|
+
const reads = artifactStore.getReads(artifactId);
|
|
2740
|
+
const actualReaders = [...new Set(reads.map(r => r.reader))];
|
|
2741
|
+
const missing = expectedReaders.filter(r => !actualReaders.includes(r));
|
|
2742
|
+
|
|
2743
|
+
readerCheck.results.push({
|
|
2744
|
+
artifactId,
|
|
2745
|
+
expectedReaders,
|
|
2746
|
+
actualReaders,
|
|
2747
|
+
missingReaders: missing,
|
|
2748
|
+
allRead: missing.length === 0,
|
|
2749
|
+
});
|
|
2750
|
+
|
|
2751
|
+
if (missing.length > 0) {
|
|
2752
|
+
readerCheck.passed = false;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
} else {
|
|
2756
|
+
// If no expected readers specified, just show who read what
|
|
2757
|
+
for (const id of artifactCheck.found) {
|
|
2758
|
+
const reads = artifactStore.getReads(id);
|
|
2759
|
+
const readers = [...new Set(reads.map(r => r.reader))];
|
|
2760
|
+
readerCheck.results.push({
|
|
2761
|
+
artifactId: id,
|
|
2762
|
+
readers,
|
|
2763
|
+
readCount: reads.length,
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
report.checks.push(readerCheck);
|
|
2768
|
+
|
|
2769
|
+
// Overall pass/fail
|
|
2770
|
+
report.passed = report.checks.every(c => c.passed);
|
|
2771
|
+
|
|
2772
|
+
// Action recommendation
|
|
2773
|
+
if (report.passed) {
|
|
2774
|
+
report.recommendation = `ALL CHECKS PASSED. Safe to proceed to ${args.phase_starting}.`;
|
|
2775
|
+
} else {
|
|
2776
|
+
const failures = report.checks.filter(c => !c.passed).map(c => c.check);
|
|
2777
|
+
report.recommendation = `BLOCKED: ${failures.join(', ')} failed. Fix these before proceeding to ${args.phase_starting}.`;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
return textResult(JSON.stringify(report, null, 2));
|
|
2781
|
+
} catch (err) {
|
|
2782
|
+
return errorResult(err.message);
|
|
2783
|
+
}
|
|
2555
2784
|
}
|
|
2556
2785
|
|
|
2557
|
-
|
|
2558
|
-
* Append a staleness warning to tool results if the server version is outdated.
|
|
2559
|
-
* Reads package.json from disk on each call to detect post-install version drift.
|
|
2560
|
-
* @param {Object} result - The tool result object
|
|
2561
|
-
* @returns {Object} The result, possibly with a staleness warning appended
|
|
2562
|
-
*/
|
|
2563
|
-
function appendStalenessWarning(result) {
|
|
2786
|
+
function handleTeamReset(args) {
|
|
2564
2787
|
try {
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2788
|
+
if (!args.confirm) {
|
|
2789
|
+
return errorResult('Must pass confirm: true to reset team state. This is destructive and cannot be undone.');
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
const teamName = args.team || 'default';
|
|
2793
|
+
const baseDir = path.join(os.homedir(), '.claude-multi-session', 'team', teamName);
|
|
2794
|
+
|
|
2795
|
+
const summary = {
|
|
2796
|
+
team: teamName,
|
|
2797
|
+
cleared: [],
|
|
2798
|
+
preserved: args.preserve_artifacts || [],
|
|
2799
|
+
};
|
|
2800
|
+
|
|
2801
|
+
// Clear artifacts (except preserved ones)
|
|
2802
|
+
const artifactsDir = path.join(baseDir, 'artifacts');
|
|
2803
|
+
if (fs.existsSync(artifactsDir)) {
|
|
2804
|
+
const indexPath = path.join(artifactsDir, 'index.json');
|
|
2805
|
+
if (fs.existsSync(indexPath)) {
|
|
2806
|
+
try {
|
|
2807
|
+
const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
2808
|
+
const preserveSet = new Set(args.preserve_artifacts || []);
|
|
2809
|
+
|
|
2810
|
+
if (preserveSet.size > 0) {
|
|
2811
|
+
// Filter out preserved artifacts
|
|
2812
|
+
const filtered = {};
|
|
2813
|
+
for (const [id, entry] of Object.entries(index)) {
|
|
2814
|
+
if (preserveSet.has(id)) {
|
|
2815
|
+
filtered[id] = entry;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
fs.writeFileSync(indexPath, JSON.stringify(filtered, null, 2));
|
|
2819
|
+
summary.cleared.push(`artifacts (kept ${preserveSet.size} preserved)`);
|
|
2820
|
+
} else {
|
|
2821
|
+
fs.writeFileSync(indexPath, '{}');
|
|
2822
|
+
summary.cleared.push('artifacts');
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// Clean data directory (version files and reads)
|
|
2826
|
+
const dataDir = path.join(artifactsDir, 'data');
|
|
2827
|
+
if (fs.existsSync(dataDir)) {
|
|
2828
|
+
const artifactDirs = fs.readdirSync(dataDir);
|
|
2829
|
+
for (const dir of artifactDirs) {
|
|
2830
|
+
if (!preserveSet.has(dir)) {
|
|
2831
|
+
const dirPath = path.join(dataDir, dir);
|
|
2832
|
+
// Remove all files in the directory
|
|
2833
|
+
const files = fs.readdirSync(dirPath);
|
|
2834
|
+
for (const file of files) {
|
|
2835
|
+
fs.unlinkSync(path.join(dirPath, file));
|
|
2836
|
+
}
|
|
2837
|
+
fs.rmdirSync(dirPath);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
} catch (e) {
|
|
2842
|
+
summary.cleared.push(`artifacts (error: ${e.message})`);
|
|
2843
|
+
}
|
|
2571
2844
|
}
|
|
2572
2845
|
}
|
|
2573
|
-
|
|
2574
|
-
//
|
|
2846
|
+
|
|
2847
|
+
// Clear contracts
|
|
2848
|
+
const contractsPath = path.join(baseDir, 'contracts.json');
|
|
2849
|
+
if (fs.existsSync(contractsPath)) {
|
|
2850
|
+
fs.writeFileSync(contractsPath, '{}');
|
|
2851
|
+
summary.cleared.push('contracts');
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
// Clear roster
|
|
2855
|
+
const rosterPath = path.join(baseDir, 'roster.json');
|
|
2856
|
+
if (fs.existsSync(rosterPath)) {
|
|
2857
|
+
fs.writeFileSync(rosterPath, '{}');
|
|
2858
|
+
summary.cleared.push('roster');
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
// Clear messages
|
|
2862
|
+
const messagesDir = path.join(baseDir, 'messages');
|
|
2863
|
+
if (fs.existsSync(messagesDir)) {
|
|
2864
|
+
const files = fs.readdirSync(messagesDir);
|
|
2865
|
+
for (const file of files) {
|
|
2866
|
+
fs.unlinkSync(path.join(messagesDir, file));
|
|
2867
|
+
}
|
|
2868
|
+
summary.cleared.push('messages');
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// Clear pipelines
|
|
2872
|
+
const pipelinesPath = path.join(baseDir, 'pipelines.json');
|
|
2873
|
+
if (fs.existsSync(pipelinesPath)) {
|
|
2874
|
+
fs.writeFileSync(pipelinesPath, '{}');
|
|
2875
|
+
summary.cleared.push('pipelines');
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// Clear locks
|
|
2879
|
+
const locksDir = path.join(baseDir, 'locks');
|
|
2880
|
+
if (fs.existsSync(locksDir)) {
|
|
2881
|
+
const lockFiles = fs.readdirSync(locksDir);
|
|
2882
|
+
for (const file of lockFiles) {
|
|
2883
|
+
fs.unlinkSync(path.join(locksDir, file));
|
|
2884
|
+
}
|
|
2885
|
+
summary.cleared.push('locks');
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// Reset in-memory team instances
|
|
2889
|
+
teamHub = null;
|
|
2890
|
+
artifactStore = null;
|
|
2891
|
+
contractStore = null;
|
|
2892
|
+
resolver = null;
|
|
2893
|
+
lineageGraph = null;
|
|
2894
|
+
pipelineEngine = null;
|
|
2895
|
+
snapshotEngine = null;
|
|
2896
|
+
currentTeamName = null;
|
|
2897
|
+
|
|
2898
|
+
summary.message = `Team "${teamName}" has been reset. ${summary.cleared.length} stores cleared.`;
|
|
2899
|
+
|
|
2900
|
+
return textResult(JSON.stringify(summary, null, 2));
|
|
2901
|
+
} catch (err) {
|
|
2902
|
+
return errorResult(err.message);
|
|
2575
2903
|
}
|
|
2576
|
-
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
// =============================================================================
|
|
2907
|
+
// Result Helpers
|
|
2908
|
+
// =============================================================================
|
|
2909
|
+
|
|
2910
|
+
function textResult(text) {
|
|
2911
|
+
return {
|
|
2912
|
+
content: [{ type: 'text', text: text + getCachedStalenessWarning() }],
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
function errorResult(message) {
|
|
2917
|
+
return {
|
|
2918
|
+
content: [{ type: 'text', text: `Error: ${message}` + getCachedStalenessWarning() }],
|
|
2919
|
+
isError: true,
|
|
2920
|
+
};
|
|
2577
2921
|
}
|
|
2578
2922
|
|
|
2579
2923
|
// =============================================================================
|
|
@@ -2581,10 +2925,18 @@ function appendStalenessWarning(result) {
|
|
|
2581
2925
|
// =============================================================================
|
|
2582
2926
|
|
|
2583
2927
|
/**
|
|
2584
|
-
*
|
|
2928
|
+
* Structured log to stderr (NEVER stdout — stdout is for MCP protocol only).
|
|
2929
|
+
* Includes ISO timestamp and server version for debugging.
|
|
2585
2930
|
*/
|
|
2586
|
-
function log(msg) {
|
|
2587
|
-
|
|
2931
|
+
function log(msg, level = 'info') {
|
|
2932
|
+
const entry = JSON.stringify({
|
|
2933
|
+
ts: new Date().toISOString(),
|
|
2934
|
+
level,
|
|
2935
|
+
server: 'multi-session-mcp',
|
|
2936
|
+
version: LOADED_VERSION,
|
|
2937
|
+
msg,
|
|
2938
|
+
});
|
|
2939
|
+
process.stderr.write(entry + '\n');
|
|
2588
2940
|
}
|
|
2589
2941
|
|
|
2590
2942
|
/**
|
|
@@ -2623,7 +2975,7 @@ async function handleMessage(message) {
|
|
|
2623
2975
|
},
|
|
2624
2976
|
serverInfo: {
|
|
2625
2977
|
name: 'claude-multi-session',
|
|
2626
|
-
version:
|
|
2978
|
+
version: LOADED_VERSION,
|
|
2627
2979
|
},
|
|
2628
2980
|
});
|
|
2629
2981
|
break;
|
|
@@ -2645,9 +2997,7 @@ async function handleMessage(message) {
|
|
|
2645
2997
|
break;
|
|
2646
2998
|
}
|
|
2647
2999
|
try {
|
|
2648
|
-
|
|
2649
|
-
// Append staleness warning if server version is outdated
|
|
2650
|
-
result = appendStalenessWarning(result);
|
|
3000
|
+
const result = await executeTool(params.name, params.arguments || {});
|
|
2651
3001
|
sendResponse(id, result);
|
|
2652
3002
|
} catch (err) {
|
|
2653
3003
|
sendResponse(id, errorResult(err.message));
|
|
@@ -2718,17 +3068,51 @@ function startServer() {
|
|
|
2718
3068
|
process.exit(0);
|
|
2719
3069
|
});
|
|
2720
3070
|
|
|
2721
|
-
//
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
3071
|
+
// Graceful shutdown handler — works on Windows (SIGINT, SIGBREAK) and Unix (SIGTERM)
|
|
3072
|
+
let shuttingDown = false;
|
|
3073
|
+
function gracefulShutdown(signal) {
|
|
3074
|
+
if (shuttingDown) return; // Prevent double-shutdown
|
|
3075
|
+
shuttingDown = true;
|
|
3076
|
+
log(`${signal} received. Graceful shutdown starting...`);
|
|
3077
|
+
|
|
3078
|
+
// Stop accepting new work
|
|
3079
|
+
rl.close();
|
|
3080
|
+
|
|
3081
|
+
// Kill all spawned child sessions
|
|
3082
|
+
try {
|
|
3083
|
+
manager.stopAll();
|
|
3084
|
+
log('All sessions stopped.');
|
|
3085
|
+
} catch (err) {
|
|
3086
|
+
log(`Error stopping sessions: ${err.message}`, 'error');
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
// Force exit after 5 second timeout if cleanup hangs
|
|
3090
|
+
const forceTimer = setTimeout(() => {
|
|
3091
|
+
log('Shutdown timeout exceeded. Force exiting.', 'warn');
|
|
3092
|
+
process.exit(1);
|
|
3093
|
+
}, 5000);
|
|
3094
|
+
forceTimer.unref(); // Don't keep process alive just for this timer
|
|
3095
|
+
|
|
2725
3096
|
process.exit(0);
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
3100
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
3101
|
+
|
|
3102
|
+
// Windows-specific: SIGBREAK is sent on Ctrl+Break
|
|
3103
|
+
if (process.platform === 'win32') {
|
|
3104
|
+
process.on('SIGBREAK', () => gracefulShutdown('SIGBREAK'));
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// Handle uncaught errors gracefully
|
|
3108
|
+
process.on('uncaughtException', (err) => {
|
|
3109
|
+
log(`Uncaught exception: ${err.message}\n${err.stack}`, 'error');
|
|
3110
|
+
gracefulShutdown('uncaughtException');
|
|
2726
3111
|
});
|
|
2727
3112
|
|
|
2728
|
-
process.on('
|
|
2729
|
-
log(
|
|
2730
|
-
|
|
2731
|
-
process.exit(0);
|
|
3113
|
+
process.on('unhandledRejection', (reason) => {
|
|
3114
|
+
log(`Unhandled rejection: ${reason}`, 'error');
|
|
3115
|
+
// Don't shutdown on unhandled rejections — log and continue
|
|
2732
3116
|
});
|
|
2733
3117
|
|
|
2734
3118
|
log('MCP server ready. Waiting for messages...');
|
package/src/prompts.js
CHANGED
|
@@ -295,7 +295,11 @@ function buildDelegatePrompt(task, context, name) {
|
|
|
295
295
|
|
|
296
296
|
You are "${name || 'worker'}" — an autonomous delegated worker session. You were spawned to complete a specific task independently, with no team communication tools. Your only job is to finish this task thoroughly and report back.
|
|
297
297
|
|
|
298
|
-
IMPORTANT: You are operating under safety limits
|
|
298
|
+
IMPORTANT: You are operating under STRICT safety limits. Your session will be AUTO-KILLED without warning if you exceed:
|
|
299
|
+
- **Cost limit:** ~$2.00 USD (default)
|
|
300
|
+
- **Turn limit:** ~50 agent turns (default)
|
|
301
|
+
- **Time limit:** ~5 minutes (default)
|
|
302
|
+
Work efficiently — do not waste turns on unnecessary exploration or over-engineering.
|
|
299
303
|
|
|
300
304
|
=== CRITICAL: MANDATORY WORKFLOW ===
|
|
301
305
|
|
|
@@ -581,29 +585,28 @@ NEVER assume workers will independently agree on conventions. Define them explic
|
|
|
581
585
|
|
|
582
586
|
### Phase Gate: VERIFY Before Spawning
|
|
583
587
|
|
|
584
|
-
=== PHASE GATE CHECKPOINT (
|
|
588
|
+
=== PHASE GATE CHECKPOINT (use phase_gate tool before EVERY team_spawn after Phase 0) ===
|
|
585
589
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
Phase completing: ___ → Phase starting: ___
|
|
589
|
-
|
|
590
|
-
1. artifact_list()
|
|
591
|
-
Expected artifacts: [___]
|
|
592
|
-
All present? YES / NO
|
|
593
|
-
|
|
594
|
-
2. artifact_get({ artifactId: "___", reader: "orchestrator" })
|
|
595
|
-
Content valid and complete? YES / NO
|
|
596
|
-
|
|
597
|
-
3. team_roster()
|
|
598
|
-
All previous-phase workers idle? YES / NO
|
|
590
|
+
Instead of manually running 4 separate tool calls, use the \`phase_gate\` tool which does ALL checks in one call:
|
|
599
591
|
|
|
600
|
-
|
|
601
|
-
|
|
592
|
+
\`\`\`
|
|
593
|
+
mcp__multi-session__phase_gate({
|
|
594
|
+
phase_completing: "Phase 0: Foundation",
|
|
595
|
+
phase_starting: "Phase 1: Routes",
|
|
596
|
+
expected_artifacts: ["project-foundation", "shared-conventions"],
|
|
597
|
+
expected_idle: ["setup"],
|
|
598
|
+
expected_readers: { "shared-conventions": ["api-dev", "db-dev"] }
|
|
599
|
+
})
|
|
600
|
+
\`\`\`
|
|
602
601
|
|
|
603
|
-
|
|
604
|
-
|
|
602
|
+
The tool automatically:
|
|
603
|
+
1. Checks all expected artifacts exist
|
|
604
|
+
2. Validates artifact content and tracks the read as "orchestrator"
|
|
605
|
+
3. Verifies all previous-phase workers are idle
|
|
606
|
+
4. Confirms expected consumers actually read the artifacts
|
|
605
607
|
|
|
606
|
-
|
|
608
|
+
Returns a structured pass/fail report with recommendation.
|
|
609
|
+
PROCEED ONLY IF the report says ALL CHECKS PASSED.
|
|
607
610
|
|
|
608
611
|
=== PHASE COUNTING RULE ===
|
|
609
612
|
At the start of planning, count and list your phases explicitly:
|
|
@@ -676,6 +679,8 @@ When all workers are done:
|
|
|
676
679
|
| Workers need to communicate | \`team_spawn\` (has team tools) | \`delegate_task\` (isolated) |
|
|
677
680
|
| Quick one-off task | \`delegate_task\` | \`team_spawn\` |
|
|
678
681
|
| Need safety limits (cost/turns) | \`delegate_task\` | \`team_spawn\` |
|
|
682
|
+
| Verify phase completion | \`phase_gate\` |
|
|
683
|
+
| Clean up between runs | \`team_reset\` |
|
|
679
684
|
|
|
680
685
|
## WHAT GOES WRONG (And How to Avoid It)
|
|
681
686
|
|
|
@@ -912,7 +917,7 @@ IMPORTANT: You are the ORCHESTRATOR. Your job is to PLAN, SPAWN, and MONITOR —
|
|
|
912
917
|
|
|
913
918
|
4.5. **Phase Gate** — Before spawning workers that depend on previous workers' output, VERIFY the dependency artifact exists by calling \`artifact_list()\` and \`artifact_get()\`. Never trust self-reported completion — verify the artifact.
|
|
914
919
|
|
|
915
|
-
5. **Post-Phase Verification** — After each phase completes,
|
|
920
|
+
5. **Post-Phase Verification** — After each phase completes, call \`phase_gate()\` which runs ALL verification checks in one call: confirms artifacts exist, validates content, checks workers are idle, and verifies artifact readers. Only proceed when it reports ALL CHECKS PASSED. If you have N phases, verify N-1 times.
|
|
916
921
|
|
|
917
922
|
6. **Collect** — When all workers are idle, check \`artifact_list\` for published outputs and summarize results for the user.
|
|
918
923
|
|
|
@@ -973,6 +978,8 @@ Use \`delegate_task\` for SINGLE, isolated tasks that don't need team communicat
|
|
|
973
978
|
| Single isolated task | \`delegate_task\` |
|
|
974
979
|
| Quick one-off task | \`delegate_task\` |
|
|
975
980
|
| Need safety limits (cost/turns) | \`delegate_task\` |
|
|
981
|
+
| Verify phase completion | \`phase_gate\` |
|
|
982
|
+
| Clean up between runs | \`team_reset\` |
|
|
976
983
|
|
|
977
984
|
### Lifecycle: delegate_task → continue_task → finish_task
|
|
978
985
|
|
|
@@ -1053,6 +1060,16 @@ NEVER do these:
|
|
|
1053
1060
|
- Do NOT fix bugs found by one worker — tell that worker to fix them
|
|
1054
1061
|
- Do NOT act as a message router — workers can talk directly via team_ask
|
|
1055
1062
|
- Do NOT keep sending corrections endlessly — if 3 corrections don't work, abort and re-spawn
|
|
1063
|
+
|
|
1064
|
+
### Resetting Between Runs
|
|
1065
|
+
Use \`team_reset\` to clean up all team state between orchestration runs:
|
|
1066
|
+
\`\`\`
|
|
1067
|
+
mcp__multi-session__team_reset({ confirm: true })
|
|
1068
|
+
\`\`\`
|
|
1069
|
+
This clears artifacts, contracts, roster, messages, and pipelines. Optionally preserve specific artifacts:
|
|
1070
|
+
\`\`\`
|
|
1071
|
+
mcp__multi-session__team_reset({ confirm: true, preserve_artifacts: ["shared-conventions"] })
|
|
1072
|
+
\`\`\`
|
|
1056
1073
|
`;
|
|
1057
1074
|
|
|
1058
1075
|
const ORCHESTRATOR_WHEN_TO_USE = `
|