claude-multi-session 2.6.0 → 2.9.1
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 +64 -0
- package/README.md +8 -0
- package/bin/cli.js +1 -1
- package/bin/setup.js +24 -8
- package/package.json +11 -2
- package/src/artifact-store.js +25 -4
- package/src/mcp-server.js +410 -30
- package/src/prompts.js +69 -8
- package/src/store.js +2 -1
- package/src/team-hub.js +75 -44
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to claude-multi-session will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [2.9.1] - 2026-02-14
|
|
6
|
+
### Fixed
|
|
7
|
+
- Gate enforcement bug: gate stayed open forever after phase_gate passed, allowing spawns without verification
|
|
8
|
+
- Gate now auto-closes when ALL workers from the current batch go idle
|
|
9
|
+
- Added spawnedWorkers tracking to gate state
|
|
10
|
+
- Test count: 48 → 49 (+1 gate auto-close test)
|
|
11
|
+
|
|
12
|
+
## [2.9.0] - 2026-02-14
|
|
13
|
+
### Changed
|
|
14
|
+
- Replaced 120-second batch window timing hack with deterministic gate logic
|
|
15
|
+
- phase_gate now explicitly opens/closes the spawn gate (no timing involved)
|
|
16
|
+
- Gate error message changed from "PHASE GATE REQUIRED" to "GATE CLOSED"
|
|
17
|
+
- phase_gate response now includes `_gate_phase` field instead of `_gate_generation`
|
|
18
|
+
- Test count increased from 46 to 48 (2 new deterministic gate tests)
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
- 120-second batch window constant
|
|
22
|
+
- Time-based spawn batch detection (lastSpawnTime, spawnBatchCount fields)
|
|
23
|
+
|
|
24
|
+
## [2.8.0] - 2026-02-13
|
|
25
|
+
### Added
|
|
26
|
+
- Inbox enforcement — workers MUST call team_check_inbox before artifact/contract tools
|
|
27
|
+
- Auto-status — server auto-sets workers to "active" on spawn, "idle" on stop/kill
|
|
28
|
+
- Convention completeness check in phase_gate (advisory, check 5 of 5)
|
|
29
|
+
- File ownership tracking — 2 new tools: register_files, check_file_owner
|
|
30
|
+
- Roster injection in team_spawn, phase_gate, send_message responses
|
|
31
|
+
- 46 automated enforcement tests in test-enforcement.js
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- roster.filter crash when roster.json contains object instead of array
|
|
35
|
+
- Defensive Array.isArray() guards on all roster operations
|
|
36
|
+
|
|
37
|
+
## [2.7.0] - 2026-02
|
|
38
|
+
### Added
|
|
39
|
+
- Gate-aware team_spawn — structural enforcement of phase_gate between spawn batches
|
|
40
|
+
- phase_gate tool with 5 verification checks
|
|
41
|
+
- team_reset tool for clearing state between runs
|
|
42
|
+
- server_version tool for staleness detection
|
|
43
|
+
|
|
44
|
+
## [2.6.0] - 2026-02
|
|
45
|
+
### Added
|
|
46
|
+
- Staleness warnings appended to all tool responses when version mismatch detected
|
|
47
|
+
|
|
48
|
+
## [2.5.0] - 2026-01
|
|
49
|
+
### Added
|
|
50
|
+
- server_version tool
|
|
51
|
+
- Improved orchestrator prompts
|
|
52
|
+
|
|
53
|
+
## [2.4.0] - 2026-01
|
|
54
|
+
### Added
|
|
55
|
+
- Version detection feature
|
|
56
|
+
- artifact_readers tool
|
|
57
|
+
|
|
58
|
+
## [2.3.0] - 2026-01
|
|
59
|
+
### Added
|
|
60
|
+
- Initial release with convention system
|
|
61
|
+
- Team coordination tools (team_spawn, team_ask, team_broadcast, etc.)
|
|
62
|
+
- Artifact store with versioning and read tracking
|
|
63
|
+
- Contract store with dependency resolution
|
|
64
|
+
- Session management (spawn, pause, resume, fork, kill)
|
package/README.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# claude-multi-session
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/claude-multi-session)
|
|
4
|
+
[](https://github.com/)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
|
|
7
|
+
> Multi-session orchestration system for Claude Code CLI. Spawn parallel workers that coordinate via artifacts and phase gates. 68 MCP tools. v2.9.0.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
3
11
|
**Multi-session orchestrator for Claude Code CLI** — spawn, control, pause, resume, and send multiple inputs to Claude Code sessions programmatically.
|
|
4
12
|
|
|
5
13
|
Built on Claude CLI's `stream-json` protocol for efficient multi-turn conversations using a single long-lived process per session.
|
package/bin/cli.js
CHANGED
package/bin/setup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* ╔══════════════════════════════════════════════════════════════╗
|
|
@@ -121,10 +121,15 @@ The tool automatically:
|
|
|
121
121
|
2. Validates artifact content and tracks the read as "orchestrator"
|
|
122
122
|
3. Verifies all previous-phase workers are idle
|
|
123
123
|
4. Confirms expected consumers actually read the artifacts
|
|
124
|
+
5. Convention completeness advisory — warns if \`shared-conventions\` artifact is missing or incomplete (checks all 10 checklist keys: responseFormat, errorFormat, statusCodes, namingConventions, filePaths, enumValues, booleanHandling, dateFormat, auditActions, sharedColumnNames)
|
|
125
|
+
|
|
126
|
+
Check 5 is advisory (WARNING, not a gate failure) — the gate still passes/fails based on checks 1-4. But convention warnings are highly visible and should be addressed.
|
|
124
127
|
|
|
125
128
|
Returns a structured pass/fail report with recommendation.
|
|
126
129
|
PROCEED ONLY IF the report says ALL CHECKS PASSED.
|
|
127
130
|
|
|
131
|
+
**ENFORCED:** \`team_spawn\` will return an error if \`phase_gate\` was not called between spawn batches. The first batch (Phase 0) is free; every subsequent batch requires a passing \`phase_gate\` call first.
|
|
132
|
+
|
|
128
133
|
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
134
|
|
|
130
135
|
Only intervene in workers when a session is BLOCKED or FAILED.
|
|
@@ -132,17 +137,17 @@ Do NOT verify worker output by reading files directly — check artifacts instea
|
|
|
132
137
|
|
|
133
138
|
### Rule 5: Always tell workers to publish artifacts
|
|
134
139
|
Every worker prompt should include instructions to:
|
|
135
|
-
1. Check inbox before starting (\`team_check_inbox\`)
|
|
140
|
+
1. Check inbox before starting (\`team_check_inbox\`) — **ENFORCED**: artifact and contract tools are blocked until workers call this
|
|
136
141
|
2. Use \`team_ask\` to communicate with teammates (NOT you)
|
|
137
|
-
3. Publish output as artifacts (\`artifact_publish\`)
|
|
142
|
+
3. Publish output as artifacts (\`artifact_publish\`) — auto-registers file ownership from \`files\`/\`filesCreated\`/\`filesModified\` in data
|
|
138
143
|
4. Broadcast completion (\`team_broadcast\`)
|
|
139
|
-
5. Update status to idle when done
|
|
144
|
+
5. ~~Update status to idle when done~~ — **AUTO-MANAGED**: status is automatically set to "active" on spawn and "idle" on stop/kill. Workers only need \`team_update_status\` for custom statuses like "blocked".
|
|
140
145
|
6. Follow shared conventions defined in Rule 0 (include them in the prompt or reference the conventions artifact)
|
|
141
146
|
|
|
142
147
|
### Rule 6: Don't fix worker code yourself (pragmatic exception for trivial fixes)
|
|
143
148
|
|
|
144
149
|
=== FIX PROTOCOL (when you must fix worker code directly) ===
|
|
145
|
-
STOP. Before editing any file a worker created, answer these questions:
|
|
150
|
+
STOP. Before editing any file a worker created, call \`check_file_owner({ file: "path/to/file" })\` to verify ownership, then answer these questions:
|
|
146
151
|
|
|
147
152
|
1. Is this fix ≤ 3 lines?
|
|
148
153
|
NO → \`send_message\` to worker or spawn fix-worker. Do NOT fix yourself.
|
|
@@ -173,19 +178,30 @@ This shows which workers actually read the conventions. If a worker is missing,
|
|
|
173
178
|
|
|
174
179
|
NEVER trust a worker's self-reported completion — verify the artifact exists yourself.
|
|
175
180
|
|
|
176
|
-
##
|
|
181
|
+
## Auto-Behaviors (v2.7.0)
|
|
182
|
+
|
|
183
|
+
These happen automatically — no action needed from you or the workers:
|
|
184
|
+
- **Roster summary injection**: \`team_spawn\`, \`phase_gate\`, and \`send_message\` (to team workers) responses include a compact roster line: \`[Team: worker-a=active, worker-b=idle]\`
|
|
185
|
+
- **Auto-status management**: Workers are set to "active" on spawn and "idle" when their session stops/is killed
|
|
186
|
+
- **Inbox enforcement**: Workers are blocked from using artifact/contract tools until they call \`team_check_inbox\`
|
|
187
|
+
- **File ownership tracking**: \`artifact_publish\` auto-registers file ownership from \`files\`/\`filesCreated\`/\`filesModified\` arrays in artifact data
|
|
188
|
+
- **Convention completeness check**: \`phase_gate\` warns if \`shared-conventions\` artifact is missing or has incomplete fields
|
|
189
|
+
|
|
190
|
+
## Quick Reference (68 tools)
|
|
177
191
|
|
|
178
192
|
| You want to... | Use this tool |
|
|
179
193
|
|----------------|---------------|
|
|
180
194
|
| Verify tools before starting | \`server_version\` |
|
|
181
195
|
| Build a multi-person project | \`team_spawn\` (multiple in parallel) |
|
|
182
196
|
| Run a single isolated task | \`delegate_task\` |
|
|
183
|
-
| Check who's working on what | \`team_roster\` |
|
|
197
|
+
| Check who's working on what | \`team_roster\` (also auto-injected in spawn/gate/message responses) |
|
|
184
198
|
| See published outputs | \`artifact_list\` |
|
|
185
199
|
| See task completion status | \`contract_list\` |
|
|
200
|
+
| Verify phase completion | \`phase_gate\` |
|
|
186
201
|
| Send a correction to a worker | \`send_message\` to that session |
|
|
187
202
|
| Check who read an artifact | \`artifact_readers\` |
|
|
188
|
-
|
|
|
203
|
+
| Check file ownership (Rule 6) | \`check_file_owner\` |
|
|
204
|
+
| Register file ownership | \`register_files\` |
|
|
189
205
|
| Clean up between runs | \`team_reset\` |
|
|
190
206
|
|
|
191
207
|
### When NOT to Delegate
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-multi-session",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
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": {
|
|
@@ -17,12 +17,20 @@
|
|
|
17
17
|
"keywords": [
|
|
18
18
|
"claude",
|
|
19
19
|
"claude-code",
|
|
20
|
+
"anthropic",
|
|
20
21
|
"session-manager",
|
|
21
22
|
"multi-session",
|
|
22
23
|
"orchestrator",
|
|
24
|
+
"ai-agents",
|
|
25
|
+
"multi-agent",
|
|
26
|
+
"agent-coordination",
|
|
27
|
+
"mcp",
|
|
28
|
+
"model-context-protocol",
|
|
23
29
|
"ai",
|
|
24
30
|
"automation",
|
|
25
|
-
"cli"
|
|
31
|
+
"cli",
|
|
32
|
+
"parallel-tasks",
|
|
33
|
+
"workflow-automation"
|
|
26
34
|
],
|
|
27
35
|
"author": "",
|
|
28
36
|
"license": "MIT",
|
|
@@ -33,6 +41,7 @@
|
|
|
33
41
|
"src/",
|
|
34
42
|
"bin/",
|
|
35
43
|
"README.md",
|
|
44
|
+
"CHANGELOG.md",
|
|
36
45
|
"STRATEGY.md",
|
|
37
46
|
"LICENSE"
|
|
38
47
|
],
|
package/src/artifact-store.js
CHANGED
|
@@ -111,7 +111,24 @@ class ArtifactStore {
|
|
|
111
111
|
if (schema.required && Array.isArray(schema.required)) {
|
|
112
112
|
for (const field of schema.required) {
|
|
113
113
|
if (!(field in data)) {
|
|
114
|
-
|
|
114
|
+
// Build helpful error message with type-specific suggestions
|
|
115
|
+
let errorMsg = `field '${field}' is required`;
|
|
116
|
+
|
|
117
|
+
// Add helpful suggestions for well-known types
|
|
118
|
+
const typeHints = {
|
|
119
|
+
'api-contract': `type 'api-contract' requires an 'endpoints' field. If this artifact isn't an API contract, use type 'custom' instead.`,
|
|
120
|
+
'schema-change': `type 'schema-change' requires a 'models' field. If this artifact isn't a database schema change, use type 'custom' instead.`,
|
|
121
|
+
'test-results': `type 'test-results' requires 'total', 'passed', and 'failed' fields. If this artifact isn't test results, use type 'custom' instead.`,
|
|
122
|
+
'component-spec': `type 'component-spec' requires a 'componentName' field. If this artifact isn't a component specification, use type 'custom' instead.`,
|
|
123
|
+
'file-manifest': `type 'file-manifest' requires a 'files' field. If this artifact isn't a file manifest, use type 'custom' instead.`,
|
|
124
|
+
'config-change': `type 'config-change' requires a 'changes' field. If this artifact isn't a configuration change, use type 'custom' instead.`
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (typeHints[type]) {
|
|
128
|
+
errorMsg = `Schema validation failed: ${typeHints[type]}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
errors.push(errorMsg);
|
|
115
132
|
}
|
|
116
133
|
}
|
|
117
134
|
}
|
|
@@ -399,10 +416,14 @@ class ArtifactStore {
|
|
|
399
416
|
} catch (err) {
|
|
400
417
|
// Handle race condition - file already exists
|
|
401
418
|
if (err.code === 'EEXIST') {
|
|
402
|
-
//
|
|
403
|
-
|
|
419
|
+
// Re-read index to get actual latest version
|
|
420
|
+
const freshIndex = readJsonSafe(this.indexPath, {});
|
|
421
|
+
const freshLatest = freshIndex[artifactId]?.latestVersion || version;
|
|
422
|
+
version = freshLatest + 1;
|
|
423
|
+
const updatedData = { ...versionData, version, publishedAt: new Date().toISOString() };
|
|
404
424
|
const retryPath = path.join(artifactDir, `v${version}.json`);
|
|
405
|
-
writeImmutable(retryPath,
|
|
425
|
+
writeImmutable(retryPath, updatedData);
|
|
426
|
+
versionData = updatedData; // Update for index write below
|
|
406
427
|
} else {
|
|
407
428
|
throw err;
|
|
408
429
|
}
|
package/src/mcp-server.js
CHANGED
|
@@ -76,6 +76,89 @@ let pipelineEngine = null;
|
|
|
76
76
|
let snapshotEngine = null;
|
|
77
77
|
let currentTeamName = null;
|
|
78
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
|
+
|
|
79
162
|
/**
|
|
80
163
|
* Get or create Team Hub instances for a given team
|
|
81
164
|
* This function lazily creates the instances on first use
|
|
@@ -995,6 +1078,38 @@ const TOOLS = [
|
|
|
995
1078
|
},
|
|
996
1079
|
},
|
|
997
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
|
+
|
|
998
1113
|
// ── Session Continuity (Layer 0) ──────────────────────────────────────
|
|
999
1114
|
{
|
|
1000
1115
|
name: 'continuity_snapshot',
|
|
@@ -1333,6 +1448,12 @@ async function executeTool(toolName, args) {
|
|
|
1333
1448
|
case 'team_reset':
|
|
1334
1449
|
return handleTeamReset(args);
|
|
1335
1450
|
|
|
1451
|
+
// ── File Ownership handlers ──
|
|
1452
|
+
case 'register_files':
|
|
1453
|
+
return handleRegisterFiles(args);
|
|
1454
|
+
case 'check_file_owner':
|
|
1455
|
+
return handleCheckFileOwner(args);
|
|
1456
|
+
|
|
1336
1457
|
// ── Session Continuity (Layer 0) handlers ──
|
|
1337
1458
|
case 'continuity_snapshot': {
|
|
1338
1459
|
const snap = new SessionSnapshot(args.projectPath);
|
|
@@ -1460,28 +1581,46 @@ async function handleSpawn(args) {
|
|
|
1460
1581
|
}
|
|
1461
1582
|
|
|
1462
1583
|
async function handleSend(args) {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
const
|
|
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);
|
|
1467
1604
|
return textResult(JSON.stringify({
|
|
1468
1605
|
session_name: args.name,
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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);
|
|
1475
1623
|
}
|
|
1476
|
-
|
|
1477
|
-
const response = await manager.send(args.name, args.message);
|
|
1478
|
-
return textResult(JSON.stringify({
|
|
1479
|
-
session_name: args.name,
|
|
1480
|
-
response: response.text,
|
|
1481
|
-
cost: response.cost,
|
|
1482
|
-
turns: response.turns,
|
|
1483
|
-
duration_ms: response.duration,
|
|
1484
|
-
}, null, 2));
|
|
1485
1624
|
}
|
|
1486
1625
|
|
|
1487
1626
|
async function handleResume(args) {
|
|
@@ -1521,6 +1660,10 @@ async function handleFork(args) {
|
|
|
1521
1660
|
|
|
1522
1661
|
function handleStop(args) {
|
|
1523
1662
|
manager.stop(args.name);
|
|
1663
|
+
|
|
1664
|
+
// Auto-set team worker status to idle when stopped
|
|
1665
|
+
autoSetWorkerIdle(args.name);
|
|
1666
|
+
|
|
1524
1667
|
return textResult(JSON.stringify({
|
|
1525
1668
|
session_name: args.name,
|
|
1526
1669
|
status: 'stopped',
|
|
@@ -1530,6 +1673,10 @@ function handleStop(args) {
|
|
|
1530
1673
|
|
|
1531
1674
|
function handleKill(args) {
|
|
1532
1675
|
manager.kill(args.name);
|
|
1676
|
+
|
|
1677
|
+
// Auto-set team worker status to idle when killed
|
|
1678
|
+
autoSetWorkerIdle(args.name);
|
|
1679
|
+
|
|
1533
1680
|
return textResult(JSON.stringify({
|
|
1534
1681
|
session_name: args.name,
|
|
1535
1682
|
status: 'killed',
|
|
@@ -1748,6 +1895,29 @@ async function handleTeamSpawn(args) {
|
|
|
1748
1895
|
const teamName = args.team || 'default';
|
|
1749
1896
|
const { teamHub } = getTeamInstances(teamName);
|
|
1750
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
|
+
|
|
1751
1921
|
// Join the team roster
|
|
1752
1922
|
teamHub.joinTeam(args.name, {
|
|
1753
1923
|
role: args.role || 'team member',
|
|
@@ -1755,6 +1925,12 @@ async function handleTeamSpawn(args) {
|
|
|
1755
1925
|
model: args.model || 'sonnet',
|
|
1756
1926
|
});
|
|
1757
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
|
+
|
|
1758
1934
|
// Get roster to build system prompt
|
|
1759
1935
|
const roster = teamHub.getRoster();
|
|
1760
1936
|
|
|
@@ -1771,6 +1947,13 @@ async function handleTeamSpawn(args) {
|
|
|
1771
1947
|
workDir: args.work_dir || process.cwd(),
|
|
1772
1948
|
});
|
|
1773
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
|
+
|
|
1774
1957
|
const result = {
|
|
1775
1958
|
session_name: session.name,
|
|
1776
1959
|
status: session.status,
|
|
@@ -1792,8 +1975,16 @@ async function handleTeamSpawn(args) {
|
|
|
1792
1975
|
result._staleness_warning = `Server is stale! ${staleness.trim()}`;
|
|
1793
1976
|
}
|
|
1794
1977
|
|
|
1795
|
-
return textResult(JSON.stringify(result, null, 2));
|
|
1978
|
+
return textResult(JSON.stringify(result, null, 2) + getRosterSummary(teamName));
|
|
1796
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
|
+
}
|
|
1797
1988
|
return errorResult(err.message);
|
|
1798
1989
|
}
|
|
1799
1990
|
}
|
|
@@ -1843,6 +2034,12 @@ function handleTeamCheckInbox(args) {
|
|
|
1843
2034
|
const teamName = args.team || 'default';
|
|
1844
2035
|
const { teamHub } = getTeamInstances(teamName);
|
|
1845
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
|
+
|
|
1846
2043
|
const messages = teamHub.getInbox(args.name, {
|
|
1847
2044
|
markRead: args.mark_read !== false,
|
|
1848
2045
|
limit: args.limit || 20,
|
|
@@ -1929,11 +2126,13 @@ function handleTeamRoster(args) {
|
|
|
1929
2126
|
const { teamHub } = getTeamInstances(teamName);
|
|
1930
2127
|
|
|
1931
2128
|
const roster = teamHub.getRoster();
|
|
2129
|
+
// Defensive: ensure roster is an array
|
|
2130
|
+
const safeRoster = Array.isArray(roster) ? roster : [];
|
|
1932
2131
|
|
|
1933
2132
|
return textResult(JSON.stringify({
|
|
1934
2133
|
team: teamName,
|
|
1935
|
-
count:
|
|
1936
|
-
members:
|
|
2134
|
+
count: safeRoster.length,
|
|
2135
|
+
members: safeRoster.map(m => ({
|
|
1937
2136
|
name: m.name,
|
|
1938
2137
|
role: m.role,
|
|
1939
2138
|
status: m.status,
|
|
@@ -1976,6 +2175,11 @@ function handleTeamUpdateStatus(args) {
|
|
|
1976
2175
|
async function handleArtifactPublish(args) {
|
|
1977
2176
|
try {
|
|
1978
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
|
+
|
|
1979
2183
|
const { artifactStore, resolver } = getTeamInstances(teamName);
|
|
1980
2184
|
|
|
1981
2185
|
// Publish the artifact
|
|
@@ -1998,13 +2202,32 @@ async function handleArtifactPublish(args) {
|
|
|
1998
2202
|
data: args.data,
|
|
1999
2203
|
});
|
|
2000
2204
|
|
|
2001
|
-
|
|
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 = {
|
|
2002
2220
|
status: 'published',
|
|
2003
2221
|
artifactId: result.artifactId,
|
|
2004
2222
|
version: result.version,
|
|
2005
2223
|
type: result.type,
|
|
2006
2224
|
team: teamName,
|
|
2007
|
-
}
|
|
2225
|
+
};
|
|
2226
|
+
if (filesRegistered > 0) {
|
|
2227
|
+
publishResult.filesRegistered = filesRegistered;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
return textResult(JSON.stringify(publishResult, null, 2));
|
|
2008
2231
|
} catch (err) {
|
|
2009
2232
|
return errorResult(err.message);
|
|
2010
2233
|
}
|
|
@@ -2013,6 +2236,11 @@ async function handleArtifactPublish(args) {
|
|
|
2013
2236
|
function handleArtifactGet(args) {
|
|
2014
2237
|
try {
|
|
2015
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
|
+
|
|
2016
2244
|
const { artifactStore } = getTeamInstances(teamName);
|
|
2017
2245
|
|
|
2018
2246
|
const artifact = artifactStore.get(args.artifactId, args.version);
|
|
@@ -2137,6 +2365,11 @@ function handleArtifactHistory(args) {
|
|
|
2137
2365
|
async function handleContractCreate(args) {
|
|
2138
2366
|
try {
|
|
2139
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
|
+
|
|
2140
2373
|
const { contractStore, resolver, teamHub } = getTeamInstances(teamName);
|
|
2141
2374
|
|
|
2142
2375
|
// Create the contract
|
|
@@ -2189,6 +2422,13 @@ function handleContractStart(args) {
|
|
|
2189
2422
|
const teamName = args.team || 'default';
|
|
2190
2423
|
const { contractStore } = getTeamInstances(teamName);
|
|
2191
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
|
+
|
|
2192
2432
|
const contract = contractStore.start(args.contractId);
|
|
2193
2433
|
|
|
2194
2434
|
return textResult(JSON.stringify({
|
|
@@ -2703,6 +2943,8 @@ function handlePhaseGate(args) {
|
|
|
2703
2943
|
|
|
2704
2944
|
// Check 3: Workers idle
|
|
2705
2945
|
const roster = teamHub.getRoster();
|
|
2946
|
+
// Defensive: ensure roster is an array
|
|
2947
|
+
const safeRoster = Array.isArray(roster) ? roster : [];
|
|
2706
2948
|
const idleCheck = {
|
|
2707
2949
|
check: 'workers_idle',
|
|
2708
2950
|
results: [],
|
|
@@ -2710,8 +2952,8 @@ function handlePhaseGate(args) {
|
|
|
2710
2952
|
};
|
|
2711
2953
|
|
|
2712
2954
|
const workersToCheck = args.expected_idle
|
|
2713
|
-
?
|
|
2714
|
-
:
|
|
2955
|
+
? safeRoster.filter(m => args.expected_idle.includes(m.name))
|
|
2956
|
+
: safeRoster;
|
|
2715
2957
|
|
|
2716
2958
|
for (const member of workersToCheck) {
|
|
2717
2959
|
const isIdle = member.status === 'idle';
|
|
@@ -2766,8 +3008,58 @@ function handlePhaseGate(args) {
|
|
|
2766
3008
|
}
|
|
2767
3009
|
report.checks.push(readerCheck);
|
|
2768
3010
|
|
|
2769
|
-
//
|
|
2770
|
-
|
|
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
|
+
}
|
|
2771
3063
|
|
|
2772
3064
|
// Action recommendation
|
|
2773
3065
|
if (report.passed) {
|
|
@@ -2777,7 +3069,56 @@ function handlePhaseGate(args) {
|
|
|
2777
3069
|
report.recommendation = `BLOCKED: ${failures.join(', ')} failed. Fix these before proceeding to ${args.phase_starting}.`;
|
|
2778
3070
|
}
|
|
2779
3071
|
|
|
2780
|
-
return textResult(JSON.stringify(report, null, 2));
|
|
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));
|
|
2781
3122
|
} catch (err) {
|
|
2782
3123
|
return errorResult(err.message);
|
|
2783
3124
|
}
|
|
@@ -2854,7 +3195,7 @@ function handleTeamReset(args) {
|
|
|
2854
3195
|
// Clear roster
|
|
2855
3196
|
const rosterPath = path.join(baseDir, 'roster.json');
|
|
2856
3197
|
if (fs.existsSync(rosterPath)) {
|
|
2857
|
-
fs.writeFileSync(rosterPath, '
|
|
3198
|
+
fs.writeFileSync(rosterPath, '[]');
|
|
2858
3199
|
summary.cleared.push('roster');
|
|
2859
3200
|
}
|
|
2860
3201
|
|
|
@@ -2895,6 +3236,30 @@ function handleTeamReset(args) {
|
|
|
2895
3236
|
snapshotEngine = null;
|
|
2896
3237
|
currentTeamName = null;
|
|
2897
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
|
+
|
|
2898
3263
|
summary.message = `Team "${teamName}" has been reset. ${summary.cleared.length} stores cleared.`;
|
|
2899
3264
|
|
|
2900
3265
|
return textResult(JSON.stringify(summary, null, 2));
|
|
@@ -3119,3 +3484,18 @@ function startServer() {
|
|
|
3119
3484
|
}
|
|
3120
3485
|
|
|
3121
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
|
+
}
|
package/src/prompts.js
CHANGED
|
@@ -58,8 +58,9 @@ You have access to MCP tools that start with \`mcp__multi-session__team_*\` and
|
|
|
58
58
|
|
|
59
59
|
=== CRITICAL: MANDATORY WORKFLOW ===
|
|
60
60
|
|
|
61
|
-
### Step 1: CHECK INBOX (Before Starting ANY Work)
|
|
62
|
-
|
|
61
|
+
### Step 1: CHECK INBOX (Before Starting ANY Work) — ENFORCED
|
|
62
|
+
You MUST call \`mcp__multi-session__team_check_inbox\` FIRST — artifact and contract tools are blocked until you do.
|
|
63
|
+
Calling \`artifact_publish\`, \`artifact_get\`, \`contract_create\`, or \`contract_start\` without checking inbox first will return a BLOCKED error.
|
|
63
64
|
Your inbox may contain:
|
|
64
65
|
- Task assignments or contracts from teammates
|
|
65
66
|
- Questions that need IMMEDIATE replies
|
|
@@ -85,8 +86,8 @@ Follow the conventions STRICTLY. If NO convention artifact exists AND your work
|
|
|
85
86
|
|
|
86
87
|
Note: team_ask is a **fallback mechanism** for when information wasn't available upfront. If your orchestrator provided thorough prompts with all needed context and conventions, you may never need team_ask — this is the ideal case.
|
|
87
88
|
|
|
88
|
-
### Step 2:
|
|
89
|
-
|
|
89
|
+
### Step 2: STATUS (Auto-Managed)
|
|
90
|
+
Your status is automatically set to "active" when spawned and "idle" when your session stops. You do NOT need to call \`team_update_status\` for these transitions. Only call it if you need a custom status like "blocked" or to update your task description mid-work.
|
|
90
91
|
|
|
91
92
|
### Step 3: DO YOUR WORK
|
|
92
93
|
Complete your assigned task using the standard coding tools (Read, Write, Edit, Bash, etc.).
|
|
@@ -99,18 +100,22 @@ mcp__multi-session__artifact_publish({
|
|
|
99
100
|
type: "api-contract" | "schema-change" | "test-results" | "implementation" | "custom",
|
|
100
101
|
name: "Human readable name",
|
|
101
102
|
publisher: "${name}",
|
|
102
|
-
data: {
|
|
103
|
+
data: {
|
|
104
|
+
/* your structured output */
|
|
105
|
+
filesCreated: ["src/routes/users.js", "src/models/user.js"], // auto-registers file ownership
|
|
106
|
+
filesModified: ["src/app.js"] // auto-registers file ownership
|
|
107
|
+
},
|
|
103
108
|
summary: "Brief description of what this contains"
|
|
104
109
|
})
|
|
105
110
|
\`\`\`
|
|
111
|
+
**File ownership tracking:** If your \`data\` object includes \`files\`, \`filesCreated\`, or \`filesModified\` arrays, the system automatically registers you as the owner of those files. This helps the orchestrator respect Rule 6 (don't fix worker code directly). You can also call \`register_files\` explicitly to register ownership of additional files.
|
|
106
112
|
|
|
107
113
|
### Step 5: CHECK INBOX AGAIN (After Major Steps)
|
|
108
114
|
After completing significant work or before starting a new sub-task, check inbox again for messages from teammates.
|
|
109
115
|
|
|
110
116
|
### Step 6: SIGNAL COMPLETION
|
|
111
|
-
When fully done,
|
|
117
|
+
When fully done, broadcast a completion message. Your status will be automatically set to "idle" when your session ends, but you should still broadcast so teammates know:
|
|
112
118
|
\`\`\`
|
|
113
|
-
mcp__multi-session__team_update_status({ name: "${name}", status: "idle", task: "Completed: <summary>" })
|
|
114
119
|
mcp__multi-session__team_broadcast({ from: "${name}", content: "Completed: <what you did>. Published artifact: <artifact-id>" })
|
|
115
120
|
\`\`\`
|
|
116
121
|
|
|
@@ -140,7 +145,7 @@ mcp__multi-session__team_broadcast({ from: "${name}", content: "Completed: <what
|
|
|
140
145
|
|
|
141
146
|
IMPORTANT: Follow these rules strictly. Violating them causes coordination failures.
|
|
142
147
|
|
|
143
|
-
1. **ALWAYS check inbox before starting work.** Your teammates may have sent you critical information.
|
|
148
|
+
1. **ALWAYS check inbox before starting work (ENFORCED).** Artifact and contract tools are blocked until you call \`team_check_inbox\`. Your teammates may have sent you critical information.
|
|
144
149
|
|
|
145
150
|
2. **NEVER ask the orchestrator to relay messages.** Talk to teammates DIRECTLY using \`team_send_message\` or \`team_ask\`. The orchestrator should NOT be a message router.
|
|
146
151
|
|
|
@@ -608,6 +613,8 @@ The tool automatically:
|
|
|
608
613
|
Returns a structured pass/fail report with recommendation.
|
|
609
614
|
PROCEED ONLY IF the report says ALL CHECKS PASSED.
|
|
610
615
|
|
|
616
|
+
**ENFORCED:** \`team_spawn\` will return an error if \`phase_gate\` was not called between spawn batches. The first batch (Phase 0) is free; every subsequent batch requires a passing \`phase_gate\` call first.
|
|
617
|
+
|
|
611
618
|
=== PHASE COUNTING RULE ===
|
|
612
619
|
At the start of planning, count and list your phases explicitly:
|
|
613
620
|
"Phase 0: [foundation], Phase 1: [routes], Phase 2: [tests], ..."
|
|
@@ -788,6 +795,58 @@ When done:
|
|
|
788
795
|
// =============================================================================
|
|
789
796
|
|
|
790
797
|
const ROLE_PROMPTS = {
|
|
798
|
+
'setup': `
|
|
799
|
+
### Role-Specific: Setup / Foundation Worker
|
|
800
|
+
|
|
801
|
+
Your job is to create the COMPLETE project foundation. Do not leave placeholders or TODOs.
|
|
802
|
+
|
|
803
|
+
You MUST create ALL of these:
|
|
804
|
+
1. **package.json** with all dependencies AND scripts (start, test, dev)
|
|
805
|
+
2. **Database setup file** (schema, connection, initialization)
|
|
806
|
+
3. **Main app file** (Express app with middleware — CORS, JSON parsing, error handling)
|
|
807
|
+
4. **Server entry point** (server.js that imports app and calls app.listen on a port)
|
|
808
|
+
5. **.gitignore**
|
|
809
|
+
6. **Route mounting** in the main app file — use placeholder requires that Phase 1 workers will create:
|
|
810
|
+
\`\`\`javascript
|
|
811
|
+
const booksRoutes = require('./routes/books');
|
|
812
|
+
app.use('/books', booksRoutes);
|
|
813
|
+
\`\`\`
|
|
814
|
+
Create the routes/ directory and empty placeholder files so requires don't crash.
|
|
815
|
+
|
|
816
|
+
CRITICAL: The project must be runnable with "npm start" after your phase completes, even if routes return empty responses. Do NOT leave comments like "// mount routes here" — actually mount them with placeholder requires.
|
|
817
|
+
|
|
818
|
+
When done:
|
|
819
|
+
- Publish a "project-foundation" artifact (type "custom") with file paths and connection details
|
|
820
|
+
- Include: server port, database file path, all created files
|
|
821
|
+
- Broadcast completion so Phase 1 workers can start
|
|
822
|
+
`,
|
|
823
|
+
|
|
824
|
+
'foundation': `
|
|
825
|
+
### Role-Specific: Setup / Foundation Worker
|
|
826
|
+
|
|
827
|
+
Your job is to create the COMPLETE project foundation. Do not leave placeholders or TODOs.
|
|
828
|
+
|
|
829
|
+
You MUST create ALL of these:
|
|
830
|
+
1. **package.json** with all dependencies AND scripts (start, test, dev)
|
|
831
|
+
2. **Database setup file** (schema, connection, initialization)
|
|
832
|
+
3. **Main app file** (Express app with middleware — CORS, JSON parsing, error handling)
|
|
833
|
+
4. **Server entry point** (server.js that imports app and calls app.listen on a port)
|
|
834
|
+
5. **.gitignore**
|
|
835
|
+
6. **Route mounting** in the main app file — use placeholder requires that Phase 1 workers will create:
|
|
836
|
+
\`\`\`javascript
|
|
837
|
+
const booksRoutes = require('./routes/books');
|
|
838
|
+
app.use('/books', booksRoutes);
|
|
839
|
+
\`\`\`
|
|
840
|
+
Create the routes/ directory and empty placeholder files so requires don't crash.
|
|
841
|
+
|
|
842
|
+
CRITICAL: The project must be runnable with "npm start" after your phase completes, even if routes return empty responses. Do NOT leave comments like "// mount routes here" — actually mount them with placeholder requires.
|
|
843
|
+
|
|
844
|
+
When done:
|
|
845
|
+
- Publish a "project-foundation" artifact (type "custom") with file paths and connection details
|
|
846
|
+
- Include: server port, database file path, all created files
|
|
847
|
+
- Broadcast completion so Phase 1 workers can start
|
|
848
|
+
`,
|
|
849
|
+
|
|
791
850
|
'backend': `
|
|
792
851
|
### Role-Specific: Backend Developer
|
|
793
852
|
- Publish API contracts as artifacts with type "api-contract" including: endpoints, methods, request/response schemas
|
|
@@ -919,6 +978,8 @@ IMPORTANT: You are the ORCHESTRATOR. Your job is to PLAN, SPAWN, and MONITOR —
|
|
|
919
978
|
|
|
920
979
|
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.
|
|
921
980
|
|
|
981
|
+
**Note:** \`team_spawn\` will return an error if \`phase_gate\` was not called between spawn batches. The first batch (Phase 0) is free; every subsequent batch requires a passing \`phase_gate\` call first.
|
|
982
|
+
|
|
922
983
|
6. **Collect** — When all workers are idle, check \`artifact_list\` for published outputs and summarize results for the user.
|
|
923
984
|
|
|
924
985
|
### Critical Rule: Never Assign the Same File to Two Workers
|
package/src/store.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const os = require('os');
|
|
19
|
+
const { atomicWriteJson } = require('./atomic-io');
|
|
19
20
|
|
|
20
21
|
// Default data directory in user's home
|
|
21
22
|
const DEFAULT_DATA_DIR = path.join(os.homedir(), '.claude-multi-session');
|
|
@@ -54,7 +55,7 @@ class Store {
|
|
|
54
55
|
* @param {Object<string, object>} sessions
|
|
55
56
|
*/
|
|
56
57
|
saveAll(sessions) {
|
|
57
|
-
|
|
58
|
+
atomicWriteJson(this.sessionsFile, sessions);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/**
|
package/src/team-hub.js
CHANGED
|
@@ -19,6 +19,7 @@ const crypto = require('crypto');
|
|
|
19
19
|
|
|
20
20
|
// Import our atomic file operations
|
|
21
21
|
const { atomicWriteJson, readJsonSafe, appendJsonl } = require('./atomic-io');
|
|
22
|
+
const { acquireLock, releaseLock } = require('./file-lock');
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* TeamHub class - manages team communication and roster
|
|
@@ -43,9 +44,13 @@ class TeamHub {
|
|
|
43
44
|
const inboxDir = path.join(this.teamDir, 'inbox');
|
|
44
45
|
const asksDir = path.join(this.teamDir, 'asks');
|
|
45
46
|
|
|
47
|
+
// Create locks directory for roster locking
|
|
48
|
+
this.locksDir = path.join(baseDir, 'team', teamName, 'locks');
|
|
49
|
+
|
|
46
50
|
// Make sure all directories exist
|
|
47
51
|
fs.mkdirSync(inboxDir, { recursive: true });
|
|
48
52
|
fs.mkdirSync(asksDir, { recursive: true });
|
|
53
|
+
fs.mkdirSync(this.locksDir, { recursive: true });
|
|
49
54
|
|
|
50
55
|
// Rate limiting: track message counts per sender
|
|
51
56
|
// Map structure: { senderName: { count: number, windowStart: timestamp } }
|
|
@@ -66,7 +71,9 @@ class TeamHub {
|
|
|
66
71
|
getRoster() {
|
|
67
72
|
const rosterPath = path.join(this.teamDir, 'roster.json');
|
|
68
73
|
// readJsonSafe returns empty array if file doesn't exist
|
|
69
|
-
|
|
74
|
+
const data = readJsonSafe(rosterPath, []);
|
|
75
|
+
// Defensive: ensure we always return an array, even if file contains {}
|
|
76
|
+
return Array.isArray(data) ? data : [];
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
/**
|
|
@@ -79,29 +86,37 @@ class TeamHub {
|
|
|
79
86
|
* @param {string} options.model - AI model being used (e.g., 'sonnet', 'opus')
|
|
80
87
|
*/
|
|
81
88
|
joinTeam(name, { role, task, model }) {
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
// Acquire lock for roster modification
|
|
90
|
+
acquireLock(this.locksDir, 'roster');
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// Get current roster
|
|
94
|
+
const roster = this.getRoster();
|
|
95
|
+
|
|
96
|
+
// Create new member entry with all required fields
|
|
97
|
+
const newMember = {
|
|
98
|
+
name,
|
|
99
|
+
role,
|
|
100
|
+
task,
|
|
101
|
+
model,
|
|
102
|
+
status: 'active',
|
|
103
|
+
joinedAt: new Date().toISOString(),
|
|
104
|
+
lastSeen: new Date().toISOString()
|
|
105
|
+
};
|
|
95
106
|
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
// Remove existing entry if member is rejoining
|
|
108
|
+
const filtered = roster.filter(m => m.name !== name);
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
// Add the new member
|
|
111
|
+
filtered.push(newMember);
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
// Save the updated roster
|
|
114
|
+
const rosterPath = path.join(this.teamDir, 'roster.json');
|
|
115
|
+
atomicWriteJson(rosterPath, filtered);
|
|
116
|
+
} finally {
|
|
117
|
+
// Always release the lock
|
|
118
|
+
releaseLock(this.locksDir, 'roster');
|
|
119
|
+
}
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
/**
|
|
@@ -110,15 +125,23 @@ class TeamHub {
|
|
|
110
125
|
* @param {string} name - Name of member to remove
|
|
111
126
|
*/
|
|
112
127
|
leaveTeam(name) {
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
// Acquire lock for roster modification
|
|
129
|
+
acquireLock(this.locksDir, 'roster');
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Get current roster
|
|
133
|
+
const roster = this.getRoster();
|
|
134
|
+
|
|
135
|
+
// Filter out the member
|
|
136
|
+
const filtered = roster.filter(m => m.name !== name);
|
|
137
|
+
|
|
138
|
+
// Save the updated roster
|
|
139
|
+
const rosterPath = path.join(this.teamDir, 'roster.json');
|
|
140
|
+
atomicWriteJson(rosterPath, filtered);
|
|
141
|
+
} finally {
|
|
142
|
+
// Always release the lock
|
|
143
|
+
releaseLock(this.locksDir, 'roster');
|
|
144
|
+
}
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
/**
|
|
@@ -128,21 +151,29 @@ class TeamHub {
|
|
|
128
151
|
* @param {Object} updates - Fields to update (status, task, role, etc.)
|
|
129
152
|
*/
|
|
130
153
|
updateMember(name, updates) {
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
// Acquire lock for roster modification
|
|
155
|
+
acquireLock(this.locksDir, 'roster');
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Get current roster
|
|
159
|
+
const roster = this.getRoster();
|
|
160
|
+
|
|
161
|
+
// Find and update the member
|
|
162
|
+
const updated = roster.map(member => {
|
|
163
|
+
if (member.name === name) {
|
|
164
|
+
// Merge updates into existing member object
|
|
165
|
+
return { ...member, ...updates };
|
|
166
|
+
}
|
|
167
|
+
return member;
|
|
168
|
+
});
|
|
142
169
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
170
|
+
// Save the updated roster
|
|
171
|
+
const rosterPath = path.join(this.teamDir, 'roster.json');
|
|
172
|
+
atomicWriteJson(rosterPath, updated);
|
|
173
|
+
} finally {
|
|
174
|
+
// Always release the lock
|
|
175
|
+
releaseLock(this.locksDir, 'roster');
|
|
176
|
+
}
|
|
146
177
|
}
|
|
147
178
|
|
|
148
179
|
/**
|