claude-multi-session 2.5.1 → 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 CHANGED
@@ -102,25 +102,28 @@ IMPORTANT: Spawn ALL independent workers in a SINGLE message with multiple tool
102
102
  ### Rule 4: Post-Phase Verification (MANDATORY)
103
103
  After ALL workers in a phase complete, BEFORE spawning the next phase, STOP and fill in this checklist:
104
104
 
105
- === PHASE GATE CHECKPOINT (fill in before EVERY team_spawn after Phase 0) ===
105
+ === PHASE GATE CHECKPOINT (use phase_gate tool before EVERY team_spawn after Phase 0) ===
106
106
 
107
- Phase completing: ___ → Phase starting: ___
107
+ Instead of manually running 4 separate tool calls, use the \`phase_gate\` tool which does ALL checks in one call:
108
108
 
109
- 1. \`artifact_list()\`
110
- Expected artifacts: [___]
111
- All present? YES / NO
112
-
113
- 2. \`artifact_get({ artifactId: "___", reader: "orchestrator" })\`
114
- Content valid and complete? YES / NO
115
-
116
- 3. \`team_roster()\`
117
- All previous-phase workers idle? YES / NO
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
118
 
119
- 4. \`artifact_readers({ artifactId: "___" })\`
120
- All expected consumers listed? YES / NO
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
121
124
 
122
- PROCEED ONLY IF all answers are YES.
123
- If any is NO diagnose and fix before continuing.
125
+ Returns a structured pass/fail report with recommendation.
126
+ PROCEED ONLY IF the report says ALL CHECKS PASSED.
124
127
 
125
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.
126
129
 
@@ -182,12 +185,17 @@ NEVER trust a worker's self-reported completion — verify the artifact exists y
182
185
  | See task completion status | \`contract_list\` |
183
186
  | Send a correction to a worker | \`send_message\` to that session |
184
187
  | Check who read an artifact | \`artifact_readers\` |
188
+ | Verify phase completion | \`phase_gate\` |
189
+ | Clean up between runs | \`team_reset\` |
185
190
 
186
191
  ### When NOT to Delegate
187
192
  - Simple tasks (< 5 min, < 3 files) — do it yourself
188
193
  - Just reading/exploring — use Read, Grep, Glob directly
189
194
  - Tightly coupled changes — must happen atomically
190
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
+
191
199
  ${CLAUDE_MD_END_MARKER}
192
200
  `;
193
201
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multi-session",
3
- "version": "2.5.1",
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 nudge if reader param was not provided
2044
+ // Add prominent warning if reader param was not provided
1957
2045
  if (!args.reader) {
1958
- response._hint = 'Tip: Pass your session name as the "reader" parameter to track artifact consumption. Example: artifact_get({ artifactId: "...", reader: "your-session-name" })';
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
- artifactId: a.artifactId,
1984
- type: a.type,
1985
- name: a.name,
1986
- publisher: a.publisher,
1987
- latestVersion: a.latestVersion,
1988
- createdAt: a.createdAt,
1989
- updatedAt: a.updatedAt,
1990
- tags: a.tags,
1991
- readCount: a.readCount,
1992
- uniqueReaders: a.uniqueReaders,
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
- // Result Helpers
2638
+ // Phase Gate & Team Reset Handlers
2547
2639
  // =============================================================================
2548
2640
 
2549
- function textResult(text) {
2550
- return { content: [{ type: 'text', text }] };
2551
- }
2641
+ function handlePhaseGate(args) {
2642
+ try {
2643
+ const teamName = args.team || 'default';
2644
+ const { artifactStore, teamHub } = getTeamInstances(teamName);
2552
2645
 
2553
- function errorResult(message) {
2554
- return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
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
- const pkgPath = path.join(__dirname, '..', 'package.json');
2566
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
2567
- if (pkg.version !== LOADED_VERSION) {
2568
- const warning = `\n\n⚠️ STALE SERVER: Running v${LOADED_VERSION} but v${pkg.version} is installed. Restart Claude Code to load updated tools.`;
2569
- if (result && result.content && result.content[0] && result.content[0].text) {
2570
- result.content[0].text += warning;
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
- } catch (e) {
2574
- // Silently ignore — staleness check is best-effort
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
- return result;
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
- * Log to stderr (NEVER stdout — stdout is for MCP protocol only).
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
- process.stderr.write(`[multi-session-mcp] ${msg}\n`);
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: '1.0.0',
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
- let result = await executeTool(params.name, params.arguments || {});
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
- // Handle process signals gracefully
2722
- process.on('SIGTERM', () => {
2723
- log('SIGTERM received. Shutting down...');
2724
- manager.stopAll();
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('SIGINT', () => {
2729
- log('SIGINT received. Shutting down...');
2730
- manager.stopAll();
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 (cost and turn caps). Work efficiently do not waste turns on unnecessary exploration or over-engineering.
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 (fill in and run before EVERY team_spawn after Phase 0) ===
588
+ === PHASE GATE CHECKPOINT (use phase_gate tool before EVERY team_spawn after Phase 0) ===
585
589
 
586
- Before spawning the next phase, STOP and fill in this checklist:
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
- 4. artifact_readers({ artifactId: "___" })
601
- All expected consumers listed? YES / NO (skip if Phase 0→1)
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
- PROCEED ONLY IF all answers are YES.
604
- If any is NO diagnose and fix before continuing.
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
- NEVER skip verification. NEVER rely on a worker's self-reported completion verify the artifact exists yourself.
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, run the verification checklist: \`artifact_list()\` to confirm artifacts exist, \`artifact_get()\` to verify content, \`team_roster()\` to confirm workers are idle. Only proceed when all checks pass. If you have N phases, verify N-1 times.
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 = `