claude-code-swarm 0.3.10 → 0.3.12
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/mcp-launcher.mjs +176 -0
- package/.claude-plugin/plugin.json +18 -13
- package/CLAUDE.md +10 -0
- package/hooks/hooks.json +87 -0
- package/package.json +1 -4
- package/scripts/map-hook.mjs +12 -1
- package/scripts/map-sidecar.mjs +4 -2
- package/src/__tests__/bootstrap.test.mjs +80 -5
- package/src/__tests__/e2e-main-agent-registration.test.mjs +229 -0
- package/src/__tests__/e2e-reconnection.test.mjs +5 -5
- package/src/__tests__/helpers.mjs +1 -0
- package/src/__tests__/sessionlog-e2e.test.mjs +270 -0
- package/src/__tests__/sessionlog.test.mjs +217 -14
- package/src/__tests__/sidecar-server.test.mjs +4 -4
- package/src/__tests__/swarmkit-resolver.test.mjs +168 -0
- package/src/bootstrap.mjs +31 -4
- package/src/config.mjs +6 -2
- package/src/index.mjs +3 -0
- package/src/map-connection.mjs +10 -3
- package/src/mesh-connection.mjs +10 -3
- package/src/sessionlog.mjs +148 -13
- package/src/sidecar-server.mjs +63 -25
- package/src/skilltree-client.mjs +7 -31
- package/src/swarmkit-resolver.mjs +48 -0
- package/.claude-plugin/run-agent-inbox-mcp.sh +0 -95
- package/.claude-plugin/run-minimem-mcp.sh +0 -98
- package/.claude-plugin/run-opentasks-mcp.sh +0 -65
- package/scripts/dev-link.mjs +0 -179
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Unified MCP server launcher for claude-code-swarm plugin.
|
|
4
|
+
*
|
|
5
|
+
* Usage: node mcp-launcher.mjs <server>
|
|
6
|
+
* server: opentasks | minimem | agent-inbox
|
|
7
|
+
*
|
|
8
|
+
* Reads .swarm/claude-swarm/config.json once, checks enablement,
|
|
9
|
+
* resolves CLI args, and exec's the real server or falls back to noop.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
|
|
13
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { createInterface } from 'node:readline';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const server = process.argv[2];
|
|
20
|
+
|
|
21
|
+
if (!server) {
|
|
22
|
+
process.stderr.write('Usage: mcp-launcher.mjs <opentasks|minimem|agent-inbox>\n');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Config ---
|
|
27
|
+
|
|
28
|
+
function readConfig() {
|
|
29
|
+
const configPath = '.swarm/claude-swarm/config.json';
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function envBool(key) {
|
|
38
|
+
const v = (process.env[key] || '').toLowerCase();
|
|
39
|
+
return ['true', '1', 'yes'].includes(v);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function env(key) {
|
|
43
|
+
return process.env[key] || '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const config = readConfig();
|
|
47
|
+
|
|
48
|
+
// --- Noop ---
|
|
49
|
+
|
|
50
|
+
function runNoop() {
|
|
51
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
52
|
+
rl.on('line', (line) => {
|
|
53
|
+
line = line.trim();
|
|
54
|
+
if (!line) return;
|
|
55
|
+
let msg;
|
|
56
|
+
try { msg = JSON.parse(line); } catch { return; }
|
|
57
|
+
const respond = (result) => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: msg.id, result }) + '\n');
|
|
58
|
+
if (msg.method === 'initialize') {
|
|
59
|
+
respond({ protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'noop-mcp', version: '1.0.0' } });
|
|
60
|
+
} else if (msg.method === 'tools/list') {
|
|
61
|
+
respond({ tools: [] });
|
|
62
|
+
} else if (msg.method === 'resources/list') {
|
|
63
|
+
respond({ resources: [] });
|
|
64
|
+
} else if (msg.method === 'prompts/list') {
|
|
65
|
+
respond({ prompts: [] });
|
|
66
|
+
} else if (msg.id !== undefined && !msg.method?.startsWith('notifications/')) {
|
|
67
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'Method not found' } }) + '\n');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
rl.on('close', () => process.exit(0));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- CLI resolution ---
|
|
74
|
+
|
|
75
|
+
function which(cmd) {
|
|
76
|
+
try {
|
|
77
|
+
return execFileSync('which', [cmd], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function findSocket(paths) {
|
|
84
|
+
for (const p of paths) {
|
|
85
|
+
try {
|
|
86
|
+
if (existsSync(p) && statSync(p).isSocket?.()) return p;
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Server definitions ---
|
|
93
|
+
|
|
94
|
+
const servers = {
|
|
95
|
+
opentasks: {
|
|
96
|
+
enabled: () => envBool('SWARM_OPENTASKS_ENABLED') || config.opentasks?.enabled === true,
|
|
97
|
+
launch: () => {
|
|
98
|
+
const scope = env('SWARM_OPENTASKS_SCOPE') || config.opentasks?.scope || 'tasks';
|
|
99
|
+
const socket = findSocket([
|
|
100
|
+
'.swarm/opentasks/daemon.sock',
|
|
101
|
+
'.opentasks/daemon.sock',
|
|
102
|
+
'.git/opentasks/daemon.sock',
|
|
103
|
+
]);
|
|
104
|
+
const args = ['mcp', '--scope', scope];
|
|
105
|
+
if (socket) args.push('--socket', socket);
|
|
106
|
+
return { cmd: 'opentasks', args };
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
minimem: {
|
|
111
|
+
enabled: () => envBool('SWARM_MINIMEM_ENABLED') || config.minimem?.enabled === true,
|
|
112
|
+
launch: () => {
|
|
113
|
+
const provider = env('SWARM_MINIMEM_PROVIDER') || config.minimem?.provider || 'auto';
|
|
114
|
+
let dir = env('SWARM_MINIMEM_DIR') || config.minimem?.dir || '';
|
|
115
|
+
if (!dir) {
|
|
116
|
+
dir = existsSync('.swarm/minimem') ? '.swarm/minimem' : '.';
|
|
117
|
+
}
|
|
118
|
+
const useGlobal = envBool('SWARM_MINIMEM_GLOBAL') || config.minimem?.global === true;
|
|
119
|
+
const args = ['mcp', '--dir', dir, '--provider', provider];
|
|
120
|
+
if (useGlobal) args.push('--global');
|
|
121
|
+
return { cmd: 'minimem', args };
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
'agent-inbox': {
|
|
126
|
+
enabled: () => envBool('SWARM_INBOX_ENABLED') || config.inbox?.enabled === true,
|
|
127
|
+
launch: () => {
|
|
128
|
+
const scope = env('SWARM_INBOX_SCOPE') || config.inbox?.scope || config.map?.scope || 'default';
|
|
129
|
+
process.env.INBOX_SCOPE = scope;
|
|
130
|
+
|
|
131
|
+
// Discover sidecar inbox socket for proxy mode
|
|
132
|
+
let inboxSock = findSocket(['.swarm/claude-swarm/tmp/map/inbox.sock']);
|
|
133
|
+
if (!inboxSock) {
|
|
134
|
+
const sessDir = '.swarm/claude-swarm/tmp/map/sessions';
|
|
135
|
+
try {
|
|
136
|
+
if (existsSync(sessDir)) {
|
|
137
|
+
for (const d of readdirSync(sessDir)) {
|
|
138
|
+
const sock = join(sessDir, d, 'inbox.sock');
|
|
139
|
+
if (existsSync(sock)) { inboxSock = sock; break; }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
if (inboxSock) {
|
|
145
|
+
process.env.INBOX_SOCKET_PATH = inboxSock;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { cmd: 'agent-inbox', args: ['mcp'] };
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// --- Main ---
|
|
154
|
+
|
|
155
|
+
const def = servers[server];
|
|
156
|
+
if (!def) {
|
|
157
|
+
process.stderr.write(`Unknown server: ${server}\n`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!def.enabled()) {
|
|
162
|
+
runNoop();
|
|
163
|
+
} else {
|
|
164
|
+
const { cmd, args } = def.launch();
|
|
165
|
+
if (which(cmd)) {
|
|
166
|
+
// Replace this process with the real server
|
|
167
|
+
const { execFileSync: _ , ...rest } = await import('node:child_process');
|
|
168
|
+
const child = (await import('node:child_process')).spawn(cmd, args, {
|
|
169
|
+
stdio: 'inherit',
|
|
170
|
+
});
|
|
171
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
172
|
+
} else {
|
|
173
|
+
process.stderr.write(`[${server}-mcp] ${cmd} CLI not found\n`);
|
|
174
|
+
runNoop();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-swarm",
|
|
3
3
|
"description": "Spin up Claude Code agent teams from openteams YAML topologies with optional MAP (Multi-Agent Protocol) observability and coordination. Provides hooks for session lifecycle, agent spawn/complete tracking, and a /swarm skill to launch team configurations.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.12",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "alexngai"
|
|
7
7
|
},
|
|
8
8
|
"mcpServers": {
|
|
9
9
|
"opentasks": {
|
|
10
|
-
"command": "
|
|
11
|
-
"args": [
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
10
|
+
"command": "node",
|
|
11
|
+
"args": [
|
|
12
|
+
"${CLAUDE_PLUGIN_ROOT}/.claude-plugin/mcp-launcher.mjs",
|
|
13
|
+
"opentasks"
|
|
14
|
+
],
|
|
15
|
+
"env": {}
|
|
15
16
|
},
|
|
16
17
|
"agent-inbox": {
|
|
17
|
-
"command": "
|
|
18
|
-
"args": [
|
|
18
|
+
"command": "node",
|
|
19
|
+
"args": [
|
|
20
|
+
"${CLAUDE_PLUGIN_ROOT}/.claude-plugin/mcp-launcher.mjs",
|
|
21
|
+
"agent-inbox"
|
|
22
|
+
],
|
|
19
23
|
"env": {}
|
|
20
24
|
},
|
|
21
25
|
"minimem": {
|
|
22
|
-
"command": "
|
|
23
|
-
"args": [
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
26
|
+
"command": "node",
|
|
27
|
+
"args": [
|
|
28
|
+
"${CLAUDE_PLUGIN_ROOT}/.claude-plugin/mcp-launcher.mjs",
|
|
29
|
+
"minimem"
|
|
30
|
+
],
|
|
31
|
+
"env": {}
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
}
|
package/CLAUDE.md
CHANGED
|
@@ -461,6 +461,16 @@ Runtime:
|
|
|
461
461
|
- **Claude Code agent teams** — enabled via `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` in settings.json
|
|
462
462
|
|
|
463
463
|
## Development notes
|
|
464
|
+
|
|
465
|
+
### Debugging with logs
|
|
466
|
+
When logging is enabled, per-session log files (JSON Lines format) are written to:
|
|
467
|
+
- **Default location**: `~/.claude-swarm/tmp/logs/<timestamp>_<sessionId>.log`
|
|
468
|
+
- **Custom location**: set `log.dir` in config or `SWARM_LOG_DIR` env var
|
|
469
|
+
- **Explicit file**: set `log.file` in config or `SWARM_LOG_FILE` env var
|
|
470
|
+
|
|
471
|
+
To enable verbose logging: `SWARM_LOG_LEVEL=debug claude` (or `info` for lifecycle events only). The default level is `warn`. Each session may produce multiple log files (e.g., one from bootstrap, one from the sidecar) sharing the same session ID in the filename.
|
|
472
|
+
|
|
473
|
+
### General notes
|
|
464
474
|
- Templates are provided by the openteams package (installed via swarmkit), not bundled with the plugin
|
|
465
475
|
- `.swarm/` directory is managed by swarmkit for ecosystem packages (openteams, sessionlog) via `initProjectPackage()`
|
|
466
476
|
- Plugin-specific state lives under `.swarm/claude-swarm/` (config, `.gitignore` ignoring `tmp/`). Runtime artifacts go in `.swarm/claude-swarm/tmp/` (per-template caches, MAP files)
|
package/hooks/hooks.json
CHANGED
|
@@ -7,6 +7,21 @@
|
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
9
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap.mjs\""
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "command",
|
|
13
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch session-start; fi"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"SessionEnd": [
|
|
19
|
+
{
|
|
20
|
+
"matcher": "",
|
|
21
|
+
"hooks": [
|
|
22
|
+
{
|
|
23
|
+
"type": "command",
|
|
24
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch session-end; fi"
|
|
10
25
|
}
|
|
11
26
|
]
|
|
12
27
|
}
|
|
@@ -18,6 +33,21 @@
|
|
|
18
33
|
{
|
|
19
34
|
"type": "command",
|
|
20
35
|
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8')).map||{};process.exit(c.enabled||c.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" inject; fi"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"type": "command",
|
|
39
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch user-prompt-submit; fi"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"PreToolUse": [
|
|
45
|
+
{
|
|
46
|
+
"matcher": "Task",
|
|
47
|
+
"hooks": [
|
|
48
|
+
{
|
|
49
|
+
"type": "command",
|
|
50
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch pre-task; fi"
|
|
21
51
|
}
|
|
22
52
|
]
|
|
23
53
|
}
|
|
@@ -32,12 +62,25 @@
|
|
|
32
62
|
}
|
|
33
63
|
]
|
|
34
64
|
},
|
|
65
|
+
{
|
|
66
|
+
"matcher": "Task",
|
|
67
|
+
"hooks": [
|
|
68
|
+
{
|
|
69
|
+
"type": "command",
|
|
70
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task; fi"
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
},
|
|
35
74
|
{
|
|
36
75
|
"matcher": "TaskCreate",
|
|
37
76
|
"hooks": [
|
|
38
77
|
{
|
|
39
78
|
"type": "command",
|
|
40
79
|
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8')).map||{};process.exit(c.enabled||c.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" native-task-created; fi"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"type": "command",
|
|
83
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task-create; fi"
|
|
41
84
|
}
|
|
42
85
|
]
|
|
43
86
|
},
|
|
@@ -47,6 +90,46 @@
|
|
|
47
90
|
{
|
|
48
91
|
"type": "command",
|
|
49
92
|
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8')).map||{};process.exit(c.enabled||c.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" native-task-updated; fi"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"type": "command",
|
|
96
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-task-update; fi"
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"matcher": "TodoWrite",
|
|
102
|
+
"hooks": [
|
|
103
|
+
{
|
|
104
|
+
"type": "command",
|
|
105
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-todo; fi"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"matcher": "EnterPlanMode",
|
|
111
|
+
"hooks": [
|
|
112
|
+
{
|
|
113
|
+
"type": "command",
|
|
114
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-plan-enter; fi"
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"matcher": "ExitPlanMode",
|
|
120
|
+
"hooks": [
|
|
121
|
+
{
|
|
122
|
+
"type": "command",
|
|
123
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-plan-exit; fi"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"matcher": "Skill",
|
|
129
|
+
"hooks": [
|
|
130
|
+
{
|
|
131
|
+
"type": "command",
|
|
132
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch post-skill; fi"
|
|
50
133
|
}
|
|
51
134
|
]
|
|
52
135
|
}
|
|
@@ -62,6 +145,10 @@
|
|
|
62
145
|
{
|
|
63
146
|
"type": "command",
|
|
64
147
|
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));const m=c.map||{};process.exit((m.enabled||m.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED)&&c.sessionlog?.sync&&c.sessionlog.sync!=='off'?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-sync; fi"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"type": "command",
|
|
151
|
+
"command": "if [ -f .swarm/claude-swarm/config.json ] && node -e \"const c=JSON.parse(require('fs').readFileSync('.swarm/claude-swarm/config.json','utf-8'));process.exit(c.sessionlog?.enabled?0:1)\" 2>/dev/null; then node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" sessionlog-dispatch stop; fi"
|
|
65
152
|
}
|
|
66
153
|
]
|
|
67
154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-swarm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Claude Code plugin for launching agent teams from openteams topologies with MAP observability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -45,9 +45,6 @@
|
|
|
45
45
|
"test:e2e:tier4": "vitest run --config e2e/vitest.config.e2e.mjs -t tier4",
|
|
46
46
|
"test:e2e:tier5": "vitest run --config e2e/vitest.config.e2e.mjs -t tier5",
|
|
47
47
|
"test:e2e:tier7": "vitest run --config e2e/vitest.config.e2e.mjs -t tier7",
|
|
48
|
-
"dev:link": "node scripts/dev-link.mjs link",
|
|
49
|
-
"dev:unlink": "node scripts/dev-link.mjs unlink",
|
|
50
|
-
"dev:status": "node scripts/dev-link.mjs status",
|
|
51
48
|
"version:patch": "npm version patch --no-git-tag-version && node scripts/sync-version.mjs",
|
|
52
49
|
"version:minor": "npm version minor --no-git-tag-version && node scripts/sync-version.mjs",
|
|
53
50
|
"version:major": "npm version major --no-git-tag-version && node scripts/sync-version.mjs"
|
package/scripts/map-hook.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* teammate-idle — Update teammate state to idle
|
|
20
20
|
* task-completed — Complete task in opentasks + emit bridge event
|
|
21
21
|
* opentasks-mcp-used — Bridge opentasks MCP tool use → MAP task sync payload
|
|
22
|
+
* sessionlog-dispatch — Dispatch a sessionlog lifecycle hook via programmatic API
|
|
22
23
|
*
|
|
23
24
|
* Usage: node map-hook.mjs <action>
|
|
24
25
|
* Hook event data is read from stdin (JSON).
|
|
@@ -48,7 +49,7 @@ import {
|
|
|
48
49
|
handleNativeTaskCreatedEvent,
|
|
49
50
|
handleNativeTaskUpdatedEvent,
|
|
50
51
|
} from "../src/map-events.mjs";
|
|
51
|
-
import { syncSessionlog } from "../src/sessionlog.mjs";
|
|
52
|
+
import { syncSessionlog, dispatchSessionlogHook } from "../src/sessionlog.mjs";
|
|
52
53
|
import { findSocketPath, pushSyncEvent } from "../src/opentasks-client.mjs";
|
|
53
54
|
|
|
54
55
|
const action = process.argv[2];
|
|
@@ -184,6 +185,15 @@ async function handleNativeTaskUpdated(hookData, sessionId) {
|
|
|
184
185
|
await handleNativeTaskUpdatedEvent(config, hookData, sessionId);
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
async function handleSessionlogDispatch(hookData) {
|
|
189
|
+
const sessionlogHookName = process.argv[3];
|
|
190
|
+
if (!sessionlogHookName) {
|
|
191
|
+
log.warn("sessionlog-dispatch: missing hook name argument");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
await dispatchSessionlogHook(sessionlogHookName, hookData);
|
|
195
|
+
}
|
|
196
|
+
|
|
187
197
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
188
198
|
|
|
189
199
|
async function main() {
|
|
@@ -204,6 +214,7 @@ async function main() {
|
|
|
204
214
|
case "opentasks-mcp-used": await handleOpentasksMcpUsed(hookData, sessionId); break;
|
|
205
215
|
case "native-task-created": await handleNativeTaskCreated(hookData, sessionId); break;
|
|
206
216
|
case "native-task-updated": await handleNativeTaskUpdated(hookData, sessionId); break;
|
|
217
|
+
case "sessionlog-dispatch": await handleSessionlogDispatch(hookData); break;
|
|
207
218
|
default:
|
|
208
219
|
log.warn("unknown action", { action });
|
|
209
220
|
}
|
package/scripts/map-sidecar.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { createMeshPeer, createMeshInbox } from "../src/mesh-connection.mjs";
|
|
|
26
26
|
import { createSocketServer, createCommandHandler } from "../src/sidecar-server.mjs";
|
|
27
27
|
import { readConfig } from "../src/config.mjs";
|
|
28
28
|
import { createLogger, init as initLog } from "../src/log.mjs";
|
|
29
|
-
import { configureNodePath } from "../src/swarmkit-resolver.mjs";
|
|
29
|
+
import { configureNodePath, resolvePackage } from "../src/swarmkit-resolver.mjs";
|
|
30
30
|
|
|
31
31
|
const log = createLogger("sidecar");
|
|
32
32
|
|
|
@@ -329,7 +329,9 @@ async function startLegacyAgentInbox(mapConnection) {
|
|
|
329
329
|
if (!INBOX_CONFIG) return null;
|
|
330
330
|
|
|
331
331
|
try {
|
|
332
|
-
const
|
|
332
|
+
const agentInboxMod = await resolvePackage("agent-inbox");
|
|
333
|
+
if (!agentInboxMod) throw new Error("agent-inbox not available");
|
|
334
|
+
const { createAgentInbox } = agentInboxMod;
|
|
333
335
|
|
|
334
336
|
const peers = INBOX_CONFIG.federation?.peers || [];
|
|
335
337
|
const federationConfig = peers.length > 0
|
|
@@ -16,8 +16,14 @@ vi.mock("../sidecar-client.mjs", () => ({
|
|
|
16
16
|
sendToInbox: vi.fn().mockResolvedValue(undefined),
|
|
17
17
|
}));
|
|
18
18
|
|
|
19
|
+
vi.mock("../map-events.mjs", () => ({
|
|
20
|
+
sendCommand: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
}));
|
|
22
|
+
|
|
19
23
|
vi.mock("../sessionlog.mjs", () => ({
|
|
20
24
|
checkSessionlogStatus: vi.fn(() => "not installed"),
|
|
25
|
+
ensureSessionlogEnabled: vi.fn().mockResolvedValue(false),
|
|
26
|
+
hasStandaloneHooks: vi.fn().mockReturnValue(false),
|
|
21
27
|
syncSessionlog: vi.fn().mockResolvedValue(undefined),
|
|
22
28
|
annotateSwarmSession: vi.fn().mockResolvedValue(undefined),
|
|
23
29
|
}));
|
|
@@ -81,7 +87,8 @@ vi.mock("../swarmkit-resolver.mjs", () => ({
|
|
|
81
87
|
const { bootstrap, backgroundInit } = await import("../bootstrap.mjs");
|
|
82
88
|
const { readConfig } = await import("../config.mjs");
|
|
83
89
|
const { killSidecar, startSidecar } = await import("../sidecar-client.mjs");
|
|
84
|
-
const {
|
|
90
|
+
const { sendCommand } = await import("../map-events.mjs");
|
|
91
|
+
const { checkSessionlogStatus, ensureSessionlogEnabled, hasStandaloneHooks, syncSessionlog, annotateSwarmSession } = await import("../sessionlog.mjs");
|
|
85
92
|
const { pluginDir, ensureOpentasksDir, ensureSessionDir, listSessionDirs } = await import("../paths.mjs");
|
|
86
93
|
const { findSocketPath, isDaemonAlive, ensureDaemon } = await import("../opentasks-client.mjs");
|
|
87
94
|
const { resolveSwarmkit, configureNodePath } = await import("../swarmkit-resolver.mjs");
|
|
@@ -156,16 +163,43 @@ describe("bootstrap", () => {
|
|
|
156
163
|
});
|
|
157
164
|
|
|
158
165
|
describe("sessionlog", () => {
|
|
159
|
-
it("
|
|
166
|
+
it("defers to standalone when standalone hooks are present", async () => {
|
|
167
|
+
hasStandaloneHooks.mockReturnValue(true);
|
|
160
168
|
readConfig.mockReturnValue(makeConfig({ sessionlogEnabled: true }));
|
|
161
169
|
const result = await bootstrap();
|
|
162
|
-
expect(result.sessionlogStatus).toBe("
|
|
170
|
+
expect(result.sessionlogStatus).toBe("active (standalone)");
|
|
171
|
+
expect(ensureSessionlogEnabled).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("enables sessionlog when no standalone hooks", async () => {
|
|
175
|
+
hasStandaloneHooks.mockReturnValue(false);
|
|
176
|
+
ensureSessionlogEnabled.mockResolvedValue(true);
|
|
177
|
+
readConfig.mockReturnValue(makeConfig({ sessionlogEnabled: true }));
|
|
178
|
+
const result = await bootstrap();
|
|
179
|
+
expect(result.sessionlogStatus).toBe("active");
|
|
180
|
+
expect(ensureSessionlogEnabled).toHaveBeenCalled();
|
|
163
181
|
});
|
|
164
182
|
|
|
165
|
-
it("
|
|
183
|
+
it("reports status when enable fails", async () => {
|
|
184
|
+
hasStandaloneHooks.mockReturnValue(false);
|
|
185
|
+
ensureSessionlogEnabled.mockResolvedValue(false);
|
|
166
186
|
readConfig.mockReturnValue(makeConfig({ sessionlogEnabled: true }));
|
|
187
|
+
const result = await bootstrap();
|
|
188
|
+
expect(result.sessionlogStatus).toBe("installed but not enabled");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns 'checking' when hasStandaloneHooks throws", async () => {
|
|
192
|
+
hasStandaloneHooks.mockImplementation(() => { throw new Error("unexpected"); });
|
|
193
|
+
readConfig.mockReturnValue(makeConfig({ sessionlogEnabled: true }));
|
|
194
|
+
const result = await bootstrap();
|
|
195
|
+
expect(result.sessionlogStatus).toBe("checking");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("does not check standalone when disabled", async () => {
|
|
199
|
+
readConfig.mockReturnValue(makeConfig({ sessionlogEnabled: false }));
|
|
167
200
|
await bootstrap();
|
|
168
|
-
expect(
|
|
201
|
+
expect(hasStandaloneHooks).not.toHaveBeenCalled();
|
|
202
|
+
expect(ensureSessionlogEnabled).not.toHaveBeenCalled();
|
|
169
203
|
});
|
|
170
204
|
});
|
|
171
205
|
|
|
@@ -398,6 +432,47 @@ describe("bootstrap", () => {
|
|
|
398
432
|
"session-xyz"
|
|
399
433
|
);
|
|
400
434
|
});
|
|
435
|
+
|
|
436
|
+
it("registers main agent via spawn command after sidecar starts", async () => {
|
|
437
|
+
startSidecar.mockResolvedValue(true);
|
|
438
|
+
const config = makeConfig({ mapEnabled: true, sidecar: "session" });
|
|
439
|
+
await backgroundInit(config, "swarm:test", pluginDir(), "session-main");
|
|
440
|
+
// Allow fire-and-forget promise to settle
|
|
441
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
442
|
+
expect(sendCommand).toHaveBeenCalledWith(
|
|
443
|
+
expect.anything(),
|
|
444
|
+
expect.objectContaining({
|
|
445
|
+
action: "spawn",
|
|
446
|
+
agent: expect.objectContaining({
|
|
447
|
+
agentId: "session-main",
|
|
448
|
+
name: "test-team-main",
|
|
449
|
+
role: "orchestrator",
|
|
450
|
+
metadata: expect.objectContaining({ isMain: true, sessionId: "session-main" }),
|
|
451
|
+
}),
|
|
452
|
+
}),
|
|
453
|
+
"session-main"
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("uses sessionId as agentId for main agent", async () => {
|
|
458
|
+
startSidecar.mockResolvedValue(true);
|
|
459
|
+
const config = makeConfig({ mapEnabled: true, sidecar: "session" });
|
|
460
|
+
await backgroundInit(config, "swarm:test", pluginDir(), "abc-123-def");
|
|
461
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
462
|
+
const spawnCall = sendCommand.mock.calls.find(
|
|
463
|
+
(c) => c[1]?.action === "spawn"
|
|
464
|
+
);
|
|
465
|
+
expect(spawnCall).toBeDefined();
|
|
466
|
+
expect(spawnCall[1].agent.agentId).toBe("abc-123-def");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("does not register main agent when sidecar fails to start", async () => {
|
|
470
|
+
startSidecar.mockResolvedValue(false);
|
|
471
|
+
const config = makeConfig({ mapEnabled: true, sidecar: "session" });
|
|
472
|
+
await backgroundInit(config, "swarm:test", pluginDir(), "session-fail");
|
|
473
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
474
|
+
expect(sendCommand).not.toHaveBeenCalled();
|
|
475
|
+
});
|
|
401
476
|
});
|
|
402
477
|
|
|
403
478
|
describe("sessionlog sync", () => {
|