claude-multi-session 2.7.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 +22 -8
- package/package.json +11 -2
- package/src/artifact-store.js +25 -4
- package/src/mcp-server.js +396 -56
- package/src/prompts.js +65 -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,6 +121,9 @@ 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.
|
|
@@ -134,17 +137,17 @@ Do NOT verify worker output by reading files directly — check artifacts instea
|
|
|
134
137
|
|
|
135
138
|
### Rule 5: Always tell workers to publish artifacts
|
|
136
139
|
Every worker prompt should include instructions to:
|
|
137
|
-
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
|
|
138
141
|
2. Use \`team_ask\` to communicate with teammates (NOT you)
|
|
139
|
-
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
|
|
140
143
|
4. Broadcast completion (\`team_broadcast\`)
|
|
141
|
-
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".
|
|
142
145
|
6. Follow shared conventions defined in Rule 0 (include them in the prompt or reference the conventions artifact)
|
|
143
146
|
|
|
144
147
|
### Rule 6: Don't fix worker code yourself (pragmatic exception for trivial fixes)
|
|
145
148
|
|
|
146
149
|
=== FIX PROTOCOL (when you must fix worker code directly) ===
|
|
147
|
-
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:
|
|
148
151
|
|
|
149
152
|
1. Is this fix ≤ 3 lines?
|
|
150
153
|
NO → \`send_message\` to worker or spawn fix-worker. Do NOT fix yourself.
|
|
@@ -175,19 +178,30 @@ This shows which workers actually read the conventions. If a worker is missing,
|
|
|
175
178
|
|
|
176
179
|
NEVER trust a worker's self-reported completion — verify the artifact exists yourself.
|
|
177
180
|
|
|
178
|
-
##
|
|
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)
|
|
179
191
|
|
|
180
192
|
| You want to... | Use this tool |
|
|
181
193
|
|----------------|---------------|
|
|
182
194
|
| Verify tools before starting | \`server_version\` |
|
|
183
195
|
| Build a multi-person project | \`team_spawn\` (multiple in parallel) |
|
|
184
196
|
| Run a single isolated task | \`delegate_task\` |
|
|
185
|
-
| 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) |
|
|
186
198
|
| See published outputs | \`artifact_list\` |
|
|
187
199
|
| See task completion status | \`contract_list\` |
|
|
200
|
+
| Verify phase completion | \`phase_gate\` |
|
|
188
201
|
| Send a correction to a worker | \`send_message\` to that session |
|
|
189
202
|
| Check who read an artifact | \`artifact_readers\` |
|
|
190
|
-
|
|
|
203
|
+
| Check file ownership (Rule 6) | \`check_file_owner\` |
|
|
204
|
+
| Register file ownership | \`register_files\` |
|
|
191
205
|
| Clean up between runs | \`team_reset\` |
|
|
192
206
|
|
|
193
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,10 +76,89 @@ let pipelineEngine = null;
|
|
|
76
76
|
let snapshotEngine = null;
|
|
77
77
|
let currentTeamName = null;
|
|
78
78
|
|
|
79
|
-
// Phase gate enforcement state —
|
|
80
|
-
// team -> {
|
|
79
|
+
// Phase gate enforcement state — deterministic open/close gate per team
|
|
80
|
+
// team -> { gateOpen: boolean, phase: string|null, spawnCount: number, firstBatchFree: boolean }
|
|
81
81
|
const _gateState = new Map();
|
|
82
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
|
+
|
|
83
162
|
/**
|
|
84
163
|
* Get or create Team Hub instances for a given team
|
|
85
164
|
* This function lazily creates the instances on first use
|
|
@@ -999,6 +1078,38 @@ const TOOLS = [
|
|
|
999
1078
|
},
|
|
1000
1079
|
},
|
|
1001
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
|
+
|
|
1002
1113
|
// ── Session Continuity (Layer 0) ──────────────────────────────────────
|
|
1003
1114
|
{
|
|
1004
1115
|
name: 'continuity_snapshot',
|
|
@@ -1337,6 +1448,12 @@ async function executeTool(toolName, args) {
|
|
|
1337
1448
|
case 'team_reset':
|
|
1338
1449
|
return handleTeamReset(args);
|
|
1339
1450
|
|
|
1451
|
+
// ── File Ownership handlers ──
|
|
1452
|
+
case 'register_files':
|
|
1453
|
+
return handleRegisterFiles(args);
|
|
1454
|
+
case 'check_file_owner':
|
|
1455
|
+
return handleCheckFileOwner(args);
|
|
1456
|
+
|
|
1340
1457
|
// ── Session Continuity (Layer 0) handlers ──
|
|
1341
1458
|
case 'continuity_snapshot': {
|
|
1342
1459
|
const snap = new SessionSnapshot(args.projectPath);
|
|
@@ -1464,28 +1581,46 @@ async function handleSpawn(args) {
|
|
|
1464
1581
|
}
|
|
1465
1582
|
|
|
1466
1583
|
async function handleSend(args) {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
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);
|
|
1471
1604
|
return textResult(JSON.stringify({
|
|
1472
1605
|
session_name: args.name,
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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);
|
|
1479
1623
|
}
|
|
1480
|
-
|
|
1481
|
-
const response = await manager.send(args.name, args.message);
|
|
1482
|
-
return textResult(JSON.stringify({
|
|
1483
|
-
session_name: args.name,
|
|
1484
|
-
response: response.text,
|
|
1485
|
-
cost: response.cost,
|
|
1486
|
-
turns: response.turns,
|
|
1487
|
-
duration_ms: response.duration,
|
|
1488
|
-
}, null, 2));
|
|
1489
1624
|
}
|
|
1490
1625
|
|
|
1491
1626
|
async function handleResume(args) {
|
|
@@ -1525,6 +1660,10 @@ async function handleFork(args) {
|
|
|
1525
1660
|
|
|
1526
1661
|
function handleStop(args) {
|
|
1527
1662
|
manager.stop(args.name);
|
|
1663
|
+
|
|
1664
|
+
// Auto-set team worker status to idle when stopped
|
|
1665
|
+
autoSetWorkerIdle(args.name);
|
|
1666
|
+
|
|
1528
1667
|
return textResult(JSON.stringify({
|
|
1529
1668
|
session_name: args.name,
|
|
1530
1669
|
status: 'stopped',
|
|
@@ -1534,6 +1673,10 @@ function handleStop(args) {
|
|
|
1534
1673
|
|
|
1535
1674
|
function handleKill(args) {
|
|
1536
1675
|
manager.kill(args.name);
|
|
1676
|
+
|
|
1677
|
+
// Auto-set team worker status to idle when killed
|
|
1678
|
+
autoSetWorkerIdle(args.name);
|
|
1679
|
+
|
|
1537
1680
|
return textResult(JSON.stringify({
|
|
1538
1681
|
session_name: args.name,
|
|
1539
1682
|
status: 'killed',
|
|
@@ -1752,27 +1895,25 @@ async function handleTeamSpawn(args) {
|
|
|
1752
1895
|
const teamName = args.team || 'default';
|
|
1753
1896
|
const { teamHub } = getTeamInstances(teamName);
|
|
1754
1897
|
|
|
1755
|
-
//
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
// (first batch is free, every subsequent batch needs a gate)
|
|
1769
|
-
if (state.spawnBatchCount > state.gateCount + 1) {
|
|
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
|
|
1770
1911
|
return errorResult(
|
|
1771
|
-
`
|
|
1772
|
-
`
|
|
1773
|
-
`
|
|
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` +
|
|
1774
1915
|
`Example:\n` +
|
|
1775
|
-
`phase_gate({ phase_completing: "Phase N", phase_starting: "Phase N+1", ` +
|
|
1916
|
+
`phase_gate({ phase_completing: "${gate.phase || 'Phase N'}", phase_starting: "Phase N+1", ` +
|
|
1776
1917
|
`expected_artifacts: ["..."] })`
|
|
1777
1918
|
);
|
|
1778
1919
|
}
|
|
@@ -1784,6 +1925,12 @@ async function handleTeamSpawn(args) {
|
|
|
1784
1925
|
model: args.model || 'sonnet',
|
|
1785
1926
|
});
|
|
1786
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
|
+
|
|
1787
1934
|
// Get roster to build system prompt
|
|
1788
1935
|
const roster = teamHub.getRoster();
|
|
1789
1936
|
|
|
@@ -1800,6 +1947,13 @@ async function handleTeamSpawn(args) {
|
|
|
1800
1947
|
workDir: args.work_dir || process.cwd(),
|
|
1801
1948
|
});
|
|
1802
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
|
+
|
|
1803
1957
|
const result = {
|
|
1804
1958
|
session_name: session.name,
|
|
1805
1959
|
status: session.status,
|
|
@@ -1821,8 +1975,16 @@ async function handleTeamSpawn(args) {
|
|
|
1821
1975
|
result._staleness_warning = `Server is stale! ${staleness.trim()}`;
|
|
1822
1976
|
}
|
|
1823
1977
|
|
|
1824
|
-
return textResult(JSON.stringify(result, null, 2));
|
|
1978
|
+
return textResult(JSON.stringify(result, null, 2) + getRosterSummary(teamName));
|
|
1825
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
|
+
}
|
|
1826
1988
|
return errorResult(err.message);
|
|
1827
1989
|
}
|
|
1828
1990
|
}
|
|
@@ -1872,6 +2034,12 @@ function handleTeamCheckInbox(args) {
|
|
|
1872
2034
|
const teamName = args.team || 'default';
|
|
1873
2035
|
const { teamHub } = getTeamInstances(teamName);
|
|
1874
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
|
+
|
|
1875
2043
|
const messages = teamHub.getInbox(args.name, {
|
|
1876
2044
|
markRead: args.mark_read !== false,
|
|
1877
2045
|
limit: args.limit || 20,
|
|
@@ -1958,11 +2126,13 @@ function handleTeamRoster(args) {
|
|
|
1958
2126
|
const { teamHub } = getTeamInstances(teamName);
|
|
1959
2127
|
|
|
1960
2128
|
const roster = teamHub.getRoster();
|
|
2129
|
+
// Defensive: ensure roster is an array
|
|
2130
|
+
const safeRoster = Array.isArray(roster) ? roster : [];
|
|
1961
2131
|
|
|
1962
2132
|
return textResult(JSON.stringify({
|
|
1963
2133
|
team: teamName,
|
|
1964
|
-
count:
|
|
1965
|
-
members:
|
|
2134
|
+
count: safeRoster.length,
|
|
2135
|
+
members: safeRoster.map(m => ({
|
|
1966
2136
|
name: m.name,
|
|
1967
2137
|
role: m.role,
|
|
1968
2138
|
status: m.status,
|
|
@@ -2005,6 +2175,11 @@ function handleTeamUpdateStatus(args) {
|
|
|
2005
2175
|
async function handleArtifactPublish(args) {
|
|
2006
2176
|
try {
|
|
2007
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
|
+
|
|
2008
2183
|
const { artifactStore, resolver } = getTeamInstances(teamName);
|
|
2009
2184
|
|
|
2010
2185
|
// Publish the artifact
|
|
@@ -2027,13 +2202,32 @@ async function handleArtifactPublish(args) {
|
|
|
2027
2202
|
data: args.data,
|
|
2028
2203
|
});
|
|
2029
2204
|
|
|
2030
|
-
|
|
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 = {
|
|
2031
2220
|
status: 'published',
|
|
2032
2221
|
artifactId: result.artifactId,
|
|
2033
2222
|
version: result.version,
|
|
2034
2223
|
type: result.type,
|
|
2035
2224
|
team: teamName,
|
|
2036
|
-
}
|
|
2225
|
+
};
|
|
2226
|
+
if (filesRegistered > 0) {
|
|
2227
|
+
publishResult.filesRegistered = filesRegistered;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
return textResult(JSON.stringify(publishResult, null, 2));
|
|
2037
2231
|
} catch (err) {
|
|
2038
2232
|
return errorResult(err.message);
|
|
2039
2233
|
}
|
|
@@ -2042,6 +2236,11 @@ async function handleArtifactPublish(args) {
|
|
|
2042
2236
|
function handleArtifactGet(args) {
|
|
2043
2237
|
try {
|
|
2044
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
|
+
|
|
2045
2244
|
const { artifactStore } = getTeamInstances(teamName);
|
|
2046
2245
|
|
|
2047
2246
|
const artifact = artifactStore.get(args.artifactId, args.version);
|
|
@@ -2166,6 +2365,11 @@ function handleArtifactHistory(args) {
|
|
|
2166
2365
|
async function handleContractCreate(args) {
|
|
2167
2366
|
try {
|
|
2168
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
|
+
|
|
2169
2373
|
const { contractStore, resolver, teamHub } = getTeamInstances(teamName);
|
|
2170
2374
|
|
|
2171
2375
|
// Create the contract
|
|
@@ -2218,6 +2422,13 @@ function handleContractStart(args) {
|
|
|
2218
2422
|
const teamName = args.team || 'default';
|
|
2219
2423
|
const { contractStore } = getTeamInstances(teamName);
|
|
2220
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
|
+
|
|
2221
2432
|
const contract = contractStore.start(args.contractId);
|
|
2222
2433
|
|
|
2223
2434
|
return textResult(JSON.stringify({
|
|
@@ -2732,6 +2943,8 @@ function handlePhaseGate(args) {
|
|
|
2732
2943
|
|
|
2733
2944
|
// Check 3: Workers idle
|
|
2734
2945
|
const roster = teamHub.getRoster();
|
|
2946
|
+
// Defensive: ensure roster is an array
|
|
2947
|
+
const safeRoster = Array.isArray(roster) ? roster : [];
|
|
2735
2948
|
const idleCheck = {
|
|
2736
2949
|
check: 'workers_idle',
|
|
2737
2950
|
results: [],
|
|
@@ -2739,8 +2952,8 @@ function handlePhaseGate(args) {
|
|
|
2739
2952
|
};
|
|
2740
2953
|
|
|
2741
2954
|
const workersToCheck = args.expected_idle
|
|
2742
|
-
?
|
|
2743
|
-
:
|
|
2955
|
+
? safeRoster.filter(m => args.expected_idle.includes(m.name))
|
|
2956
|
+
: safeRoster;
|
|
2744
2957
|
|
|
2745
2958
|
for (const member of workersToCheck) {
|
|
2746
2959
|
const isIdle = member.status === 'idle';
|
|
@@ -2795,15 +3008,57 @@ function handlePhaseGate(args) {
|
|
|
2795
3008
|
}
|
|
2796
3009
|
report.checks.push(readerCheck);
|
|
2797
3010
|
|
|
2798
|
-
//
|
|
2799
|
-
|
|
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);
|
|
2800
3048
|
|
|
2801
|
-
//
|
|
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
|
|
2802
3053
|
if (report.passed) {
|
|
2803
|
-
const
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
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;
|
|
2807
3062
|
}
|
|
2808
3063
|
|
|
2809
3064
|
// Action recommendation
|
|
@@ -2814,7 +3069,56 @@ function handlePhaseGate(args) {
|
|
|
2814
3069
|
report.recommendation = `BLOCKED: ${failures.join(', ')} failed. Fix these before proceeding to ${args.phase_starting}.`;
|
|
2815
3070
|
}
|
|
2816
3071
|
|
|
2817
|
-
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));
|
|
2818
3122
|
} catch (err) {
|
|
2819
3123
|
return errorResult(err.message);
|
|
2820
3124
|
}
|
|
@@ -2891,7 +3195,7 @@ function handleTeamReset(args) {
|
|
|
2891
3195
|
// Clear roster
|
|
2892
3196
|
const rosterPath = path.join(baseDir, 'roster.json');
|
|
2893
3197
|
if (fs.existsSync(rosterPath)) {
|
|
2894
|
-
fs.writeFileSync(rosterPath, '
|
|
3198
|
+
fs.writeFileSync(rosterPath, '[]');
|
|
2895
3199
|
summary.cleared.push('roster');
|
|
2896
3200
|
}
|
|
2897
3201
|
|
|
@@ -2935,6 +3239,27 @@ function handleTeamReset(args) {
|
|
|
2935
3239
|
// Clear gate enforcement state
|
|
2936
3240
|
_gateState.delete(teamName);
|
|
2937
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
|
+
|
|
2938
3263
|
summary.message = `Team "${teamName}" has been reset. ${summary.cleared.length} stores cleared.`;
|
|
2939
3264
|
|
|
2940
3265
|
return textResult(JSON.stringify(summary, null, 2));
|
|
@@ -3159,3 +3484,18 @@ function startServer() {
|
|
|
3159
3484
|
}
|
|
3160
3485
|
|
|
3161
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
|
|
|
@@ -790,6 +795,58 @@ When done:
|
|
|
790
795
|
// =============================================================================
|
|
791
796
|
|
|
792
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
|
+
|
|
793
850
|
'backend': `
|
|
794
851
|
### Role-Specific: Backend Developer
|
|
795
852
|
- Publish API contracts as artifacts with type "api-contract" including: endpoints, methods, request/response schemas
|
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
|
/**
|