claude-code-swarm 0.3.22 → 0.3.24
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/plugin.json +1 -1
- package/package.json +3 -3
- package/scripts/map-sidecar.mjs +34 -0
- package/src/opentasks-bridge.mjs +140 -0
- package/plan.md +0 -214
|
@@ -1,7 +1,7 @@
|
|
|
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.24",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "alexngai"
|
|
7
7
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-swarm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
4
4
|
"description": "Claude Code plugin for launching agent teams from openteams topologies with MAP observability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"agent-inbox": "*",
|
|
27
|
-
"opentasks": "
|
|
27
|
+
"opentasks": ">=0.1.1",
|
|
28
28
|
"swarmkit": "*"
|
|
29
29
|
},
|
|
30
30
|
"peerDependenciesMeta": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"agent-inbox": "^0.1.9",
|
|
57
57
|
"minimem": "^0.1.1",
|
|
58
|
-
"opentasks": "^0.
|
|
58
|
+
"opentasks": "^0.1.2",
|
|
59
59
|
"skill-tree": "^0.1.5",
|
|
60
60
|
"vitest": "^4.0.18",
|
|
61
61
|
"ws": "^8.0.0"
|
package/scripts/map-sidecar.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { SOCKET_PATH, PID_PATH, INBOX_SOCKET_PATH, sessionPaths, pluginDir } fro
|
|
|
24
24
|
import { connectToMAP } from "../src/map-connection.mjs";
|
|
25
25
|
import { createMeshPeer, createMeshInbox } from "../src/mesh-connection.mjs";
|
|
26
26
|
import { createSocketServer, createCommandHandler } from "../src/sidecar-server.mjs";
|
|
27
|
+
import { startOpenTasksEventBridge } from "../src/opentasks-bridge.mjs";
|
|
27
28
|
import { createContentProvider } from "../src/content-provider.mjs";
|
|
28
29
|
import { startMemoryWatcher } from "../src/memory-watcher.mjs";
|
|
29
30
|
import { readConfig } from "../src/config.mjs";
|
|
@@ -123,6 +124,7 @@ let inboxInstance = null;
|
|
|
123
124
|
let inactivityTimer = null;
|
|
124
125
|
let reconnectInterval = null;
|
|
125
126
|
let transportMode = "websocket"; // "mesh" or "websocket"
|
|
127
|
+
let opentasksBridge = null; // Daemon watch → MAP event bridge (Option A)
|
|
126
128
|
const registeredAgents = new Map();
|
|
127
129
|
|
|
128
130
|
// ── Inactivity Timer ────────────────────────────────────────────────────────
|
|
@@ -143,6 +145,13 @@ async function shutdown() {
|
|
|
143
145
|
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
144
146
|
if (reconnectInterval) clearInterval(reconnectInterval);
|
|
145
147
|
|
|
148
|
+
// Stop opentasks event bridge before the MAP connection drops — the
|
|
149
|
+
// bridge needs a live connection to send its unsubscribe over.
|
|
150
|
+
if (opentasksBridge) {
|
|
151
|
+
try { await opentasksBridge.stop(); } catch { /* ignore */ }
|
|
152
|
+
opentasksBridge = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
146
155
|
// Stop agent-inbox first (it borrows the connection/peer, doesn't own it)
|
|
147
156
|
if (inboxInstance) {
|
|
148
157
|
try { await inboxInstance.stop(); } catch { /* ignore */ }
|
|
@@ -242,6 +251,19 @@ function startSlowReconnectLoop() {
|
|
|
242
251
|
// Re-subscribe inbox events to the new connection
|
|
243
252
|
subscribeInboxEvents(newConn);
|
|
244
253
|
|
|
254
|
+
// Re-attach opentasks event bridge to the fresh connection —
|
|
255
|
+
// the previous bridge was bound to the dead one.
|
|
256
|
+
if (opentasksBridge) {
|
|
257
|
+
try { await opentasksBridge.stop(); } catch { /* ignore */ }
|
|
258
|
+
opentasksBridge = null;
|
|
259
|
+
}
|
|
260
|
+
if (PROJECT_CONTEXT.task_graph) {
|
|
261
|
+
opentasksBridge = await startOpenTasksEventBridge(newConn, {
|
|
262
|
+
scope: MAP_SCOPE,
|
|
263
|
+
onActivity: resetInactivityTimer,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
245
267
|
log.info("reconnected to MAP server");
|
|
246
268
|
}
|
|
247
269
|
} catch (err) {
|
|
@@ -428,6 +450,18 @@ async function startWebSocketTransport() {
|
|
|
428
450
|
await registerOpenTasksHandler(connection);
|
|
429
451
|
}
|
|
430
452
|
|
|
453
|
+
// Start the opentasks → MAP event bridge — surfaces every graph
|
|
454
|
+
// change (context/spec nodes in particular) as a MAP event over the
|
|
455
|
+
// shared connection. Task-event emission is suppressed inside the
|
|
456
|
+
// bridge to avoid double-sending alongside the existing PostToolUse
|
|
457
|
+
// `bridge-task-*` command chain.
|
|
458
|
+
if (connection && PROJECT_CONTEXT.task_graph) {
|
|
459
|
+
opentasksBridge = await startOpenTasksEventBridge(connection, {
|
|
460
|
+
scope: MAP_SCOPE,
|
|
461
|
+
onActivity: resetInactivityTimer,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
431
465
|
// Start agent-inbox with MAP connection (legacy mode)
|
|
432
466
|
if (INBOX_CONFIG && connection) {
|
|
433
467
|
inboxInstance = await startLegacyAgentInbox(connection);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opentasks-bridge.mjs — Opentasks MAP event bridge for the sidecar
|
|
3
|
+
*
|
|
4
|
+
* Attaches opentasks' `createMAPEventBridge` to the local daemon's watch
|
|
5
|
+
* stream so every graph change surfaces as a `task.*` / `context.*` MAP
|
|
6
|
+
* event over the shared MAP connection. The bridge is kind-agnostic for
|
|
7
|
+
* contexts — downstream consumers (e.g. OpenHive's hub) route by
|
|
8
|
+
* `metadata.kind` to classify specs vs plain contexts.
|
|
9
|
+
*
|
|
10
|
+
* This is the "Option A" daemon-wired path — no explicit `bridge-*`
|
|
11
|
+
* sidecar command is needed for contexts. For tasks, the existing
|
|
12
|
+
* PostToolUse(TaskCreate) → `bridge-task-*` command chain remains the
|
|
13
|
+
* active path (matches the filters in sidecar-server.mjs and avoids
|
|
14
|
+
* double-emission when both hooks and the watcher fire for the same
|
|
15
|
+
* change).
|
|
16
|
+
*
|
|
17
|
+
* Extracted from map-sidecar.mjs for testability.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createLogger } from "./log.mjs";
|
|
21
|
+
|
|
22
|
+
const log = createLogger("opentasks-bridge");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start the opentasks MAP event bridge.
|
|
26
|
+
*
|
|
27
|
+
* Connects to the local opentasks daemon, subscribes to graph changes,
|
|
28
|
+
* and forwards every event through the MAP event bridge so connected
|
|
29
|
+
* observers (OpenHive hub, peer swarms) see them as standard MAP events.
|
|
30
|
+
*
|
|
31
|
+
* Safe to call when the daemon isn't running or when MAP connection is
|
|
32
|
+
* absent — returns `null` and logs at debug level.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} conn - MAP connection (AgentConnection or MeshPeer connection)
|
|
35
|
+
* @param {object} options
|
|
36
|
+
* @param {string} options.scope - MAP scope (e.g. "swarm:gsd")
|
|
37
|
+
* @param {() => void} [options.onActivity] - Called on each bridged event
|
|
38
|
+
* @param {() => Promise<object>} [options.importOpentasks] - Override for `await import("opentasks")`
|
|
39
|
+
* @param {() => Promise<object>} [options.importOpentasksClient] - Override for `./opentasks-client.mjs` import
|
|
40
|
+
* @returns {Promise<{ stop: () => Promise<void> } | null>}
|
|
41
|
+
*/
|
|
42
|
+
export async function startOpenTasksEventBridge(conn, options = {}) {
|
|
43
|
+
if (!conn) return null;
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
scope = "swarm:default",
|
|
47
|
+
onActivity,
|
|
48
|
+
importOpentasks,
|
|
49
|
+
importOpentasksClient,
|
|
50
|
+
} = options;
|
|
51
|
+
|
|
52
|
+
let opentasks;
|
|
53
|
+
try {
|
|
54
|
+
opentasks = importOpentasks
|
|
55
|
+
? await importOpentasks()
|
|
56
|
+
: await import("opentasks");
|
|
57
|
+
} catch (err) {
|
|
58
|
+
log.debug("opentasks package not available", { error: err.message });
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { createMAPEventBridge, createIPCClient } = opentasks || {};
|
|
63
|
+
if (!createMAPEventBridge || !createIPCClient) {
|
|
64
|
+
log.debug("opentasks event-bridge exports missing");
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let socketPath;
|
|
69
|
+
try {
|
|
70
|
+
const opentasksClient = importOpentasksClient
|
|
71
|
+
? await importOpentasksClient()
|
|
72
|
+
: await import("./opentasks-client.mjs");
|
|
73
|
+
socketPath = opentasksClient.findSocketPath();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
log.debug("could not resolve opentasks socket path", { error: err.message });
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const client = createIPCClient(socketPath);
|
|
80
|
+
try {
|
|
81
|
+
await client.connect();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
log.debug("opentasks daemon not reachable, bridge disabled", {
|
|
84
|
+
socketPath,
|
|
85
|
+
error: err.message,
|
|
86
|
+
});
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const bridge = createMAPEventBridge({
|
|
91
|
+
connection: conn,
|
|
92
|
+
scope,
|
|
93
|
+
agentId: `${scope}-sidecar`,
|
|
94
|
+
// Suppress bridge task.* events — the sidecar's existing
|
|
95
|
+
// `bridge-task-*` command chain (driven by PostToolUse hooks) is the
|
|
96
|
+
// canonical path for tasks. Emitting here too would duplicate every
|
|
97
|
+
// task event. Contexts have no hook counterpart, so they only flow
|
|
98
|
+
// via this watcher.
|
|
99
|
+
filter: (type) => !type.startsWith("task."),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const offNotif = client.onNotification((method, params) => {
|
|
103
|
+
if (method !== "watch.event") return;
|
|
104
|
+
if (onActivity) onActivity();
|
|
105
|
+
log.debug("watch.event received", {
|
|
106
|
+
kind: params?.type,
|
|
107
|
+
nodeId: params?.nodeId,
|
|
108
|
+
nodeType: params?.node?.type,
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
bridge.handleProviderChange("native", { kind: "node", event: params });
|
|
112
|
+
} catch (err) {
|
|
113
|
+
log.debug("bridge.handleProviderChange threw", { error: err.message });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await client.request("watch.subscribe", {});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
log.debug("watch.subscribe failed, bridge disabled", { error: err.message });
|
|
121
|
+
offNotif();
|
|
122
|
+
try { client.disconnect(); } catch { /* ignore */ }
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
log.info("opentasks event bridge active", { scope, socketPath });
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
async stop() {
|
|
130
|
+
try {
|
|
131
|
+
await client.request("watch.unsubscribe", {});
|
|
132
|
+
} catch {
|
|
133
|
+
// ignore — we're shutting down anyway
|
|
134
|
+
}
|
|
135
|
+
offNotif();
|
|
136
|
+
bridge.stop();
|
|
137
|
+
try { client.disconnect(); } catch { /* ignore */ }
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
package/plan.md
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
# Refactor: Replace custom `swarm.*` events with MAP SDK primitives
|
|
2
|
-
|
|
3
|
-
## Goal
|
|
4
|
-
|
|
5
|
-
Eliminate all custom `swarm.*` event types by using the MAP SDK's built-in `AgentConnection` methods (`spawn()`, `done()`, `updateState()`, `updateMetadata()`, `send()`) and the server's automatic event emission (`agent_registered`, `agent_state_changed`, etc.). This means MAP clients only need to subscribe to standard MAP event types — zero swarm-specific integration.
|
|
6
|
-
|
|
7
|
-
## Guiding principle
|
|
8
|
-
|
|
9
|
-
**No new message types.** All swarm-specific context goes into `metadata` on agents and `payload` on messages — never into custom event type strings.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Current state → Target state mapping
|
|
14
|
-
|
|
15
|
-
| Current custom event | Target SDK primitive | Auto server event |
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| `swarm.agent.registered` (sidecar broadcasts) | `conn.spawn({ name, role, parent, scopes, metadata })` | `agent_registered` |
|
|
18
|
-
| `swarm.agent.unregistered` (sidecar broadcasts) | `spawnedAgent.done({ exitReason })` or sidecar tracks + calls `conn.callExtension(...)` to unregister | `agent_unregistered` |
|
|
19
|
-
| `swarm.agent.spawned` (emitEvent broadcast) | Replaced by `spawn()` above — no separate event needed | `agent_registered` |
|
|
20
|
-
| `swarm.agent.completed` (emitEvent broadcast) | Replaced by agent `done()` above | `agent_state_changed` → `stopped` |
|
|
21
|
-
| `swarm.task.dispatched` (emitEvent broadcast) | `conn.send({ scope }, { type: "task.dispatched", ... })` — typed payload in regular message | `message_sent` |
|
|
22
|
-
| `swarm.task.completed` (emitEvent broadcast) | `conn.send({ scope }, { type: "task.completed", ... })` | `message_sent` |
|
|
23
|
-
| `swarm.task.status_completed` (emitEvent broadcast) | `conn.send({ scope }, { type: "task.completed", ... })` with richer payload | `message_sent` |
|
|
24
|
-
| `swarm.turn.completed` (emitEvent broadcast) | `conn.idle()` + `conn.updateMetadata({ lastStopReason })` | `agent_state_changed` → `idle` |
|
|
25
|
-
| `swarm.subagent.started` (emitEvent broadcast) | `conn.spawn({ role: "subagent", metadata: { agentType, sessionId } })` | `agent_registered` |
|
|
26
|
-
| `swarm.subagent.stopped` (emitEvent broadcast) | `spawned.done({ exitReason })` | `agent_state_changed` → `stopped` |
|
|
27
|
-
| `swarm.teammate.idle` (emitEvent broadcast) | `agent.updateState("idle")` (via sidecar) | `agent_state_changed` → `idle` |
|
|
28
|
-
| `swarm.sessionlog.sync` (broadcast fallback) | `conn.callExtension("trajectory/checkpoint", ...)` — keep existing; fallback sends as regular message payload instead of custom event type | `trajectory.checkpoint` |
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## Implementation steps
|
|
33
|
-
|
|
34
|
-
### Step 1: Refactor `sidecar-server.mjs` — replace `register`/`unregister` with `spawn`/`done`
|
|
35
|
-
|
|
36
|
-
**File:** `src/sidecar-server.mjs`
|
|
37
|
-
|
|
38
|
-
Replace the `register` command handler:
|
|
39
|
-
- **Before:** `conn.send({ scope }, { type: "swarm.agent.registered", ... })` — broadcasts a custom message
|
|
40
|
-
- **After:** `conn.spawn({ agentId, name, role, scopes, metadata })` — uses SDK primitive; server auto-emits `agent_registered`
|
|
41
|
-
- Store spawned agent references in a `Map<agentId, AgentSpawnResult>` instead of just a `Set<agentId>`
|
|
42
|
-
|
|
43
|
-
Replace the `unregister` command handler:
|
|
44
|
-
- **Before:** `conn.send({ scope }, { type: "swarm.agent.unregistered", ... })` — broadcasts custom message
|
|
45
|
-
- **After:** Use `conn.callExtension("agents/unregister", { agentId, reason })` or track spawned agent IDs and send a deregistration request. Since the sidecar is the parent, it can manage child agent lifecycle.
|
|
46
|
-
- Actually, the simplest approach: call `conn.send()` with method `"map/agents/unregister"` as a request, or use the lower-level approach of sending a state update. Since the sidecar owns the spawned agents, we'll call `conn.callExtension("map/agents/unregister", { agentId, reason })`.
|
|
47
|
-
- Remove from tracked agents map
|
|
48
|
-
|
|
49
|
-
Replace the `trajectory-checkpoint` fallback:
|
|
50
|
-
- **Before:** Falls back to `conn.send({ scope }, { type: "swarm.sessionlog.sync", ... })`
|
|
51
|
-
- **After:** Falls back to `conn.send({ scope }, { type: "trajectory.checkpoint.fallback", checkpoint })` — still a regular message but with a standardized payload shape, not a custom event type. Or even simpler: just use `{ type: "trajectory.checkpoint", ... }` as the message payload since this is informational.
|
|
52
|
-
|
|
53
|
-
Keep `emit` command handler for now (backward compat) but mark as deprecated — it's the generic "send arbitrary payload" path.
|
|
54
|
-
|
|
55
|
-
Keep `state` and `ping` handlers unchanged.
|
|
56
|
-
|
|
57
|
-
### Step 2: Refactor `map-events.mjs` — replace builders with SDK-aligned builders
|
|
58
|
-
|
|
59
|
-
**File:** `src/map-events.mjs`
|
|
60
|
-
|
|
61
|
-
This is where the core change happens. Replace the custom event builders with functions that produce either:
|
|
62
|
-
- **Sidecar commands** (for `spawn`/`done`/state updates) — structured for the sidecar socket protocol
|
|
63
|
-
- **Message payloads** (for task lifecycle) — sent via `conn.send()` as regular MAP messages
|
|
64
|
-
|
|
65
|
-
New functions:
|
|
66
|
-
|
|
67
|
-
```javascript
|
|
68
|
-
// Agent lifecycle — produce sidecar commands
|
|
69
|
-
export function buildSpawnCommand(agentName, matchedRole, teamName, hookData) → { action: "spawn", agent: { ... } }
|
|
70
|
-
export function buildDoneCommand(agentName, matchedRole, teamName) → { action: "done", agentId, reason }
|
|
71
|
-
export function buildSubagentSpawnCommand(hookData, teamName) → { action: "spawn", agent: { role: "subagent", ... } }
|
|
72
|
-
export function buildSubagentDoneCommand(hookData, teamName) → { action: "done", agentId, reason }
|
|
73
|
-
|
|
74
|
-
// State updates — produce sidecar commands
|
|
75
|
-
export function buildStateCommand(agentId, state, metadata?) → { action: "state", agentId, state, metadata? }
|
|
76
|
-
|
|
77
|
-
// Task lifecycle — produce message payloads (sent via "emit" or "send")
|
|
78
|
-
export function buildTaskDispatchedPayload(hookData, teamName, matchedRole, agentName) → { type: "task.dispatched", ... }
|
|
79
|
-
export function buildTaskCompletedPayload(hookData, teamName, matchedRole, agentName) → { type: "task.completed", ... }
|
|
80
|
-
export function buildTaskStatusPayload(hookData, teamName, matchedRole) → { type: "task.completed", ... }
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Remove `emitEvent()` wrapper for agent lifecycle events — those go directly as sidecar commands.
|
|
84
|
-
Keep `emitEvent()` only for task-related message payloads (these are still sent as MAP messages to scope, which is fine).
|
|
85
|
-
|
|
86
|
-
### Step 3: Refactor `map-hook.mjs` — update action handlers
|
|
87
|
-
|
|
88
|
-
**File:** `scripts/map-hook.mjs`
|
|
89
|
-
|
|
90
|
-
`handleAgentSpawning()`:
|
|
91
|
-
- **Before:** `sendToSidecar({ action: "register", agent: {...} })` + `emitEvent(buildSpawnEvent)` + `emitEvent(buildTaskDispatchedEvent)`
|
|
92
|
-
- **After:** `sendToSidecar(buildSpawnCommand(...))` + `emitEvent(buildTaskDispatchedPayload(...))`
|
|
93
|
-
- The spawn command replaces both the register and the spawn event — one operation, server auto-emits the event
|
|
94
|
-
|
|
95
|
-
`handleAgentCompleted()`:
|
|
96
|
-
- **Before:** `sendToSidecar({ action: "unregister", ... })` + `emitEvent(buildCompletedEvent)` + `emitEvent(buildTaskCompletedEvent)`
|
|
97
|
-
- **After:** `sendToSidecar(buildDoneCommand(...))` + `emitEvent(buildTaskCompletedPayload(...))`
|
|
98
|
-
|
|
99
|
-
`handleTurnCompleted()`:
|
|
100
|
-
- **Before:** `sendToSidecar({ action: "state", state: "idle" })` + `emitEvent(buildTurnCompletedEvent)`
|
|
101
|
-
- **After:** `sendToSidecar(buildStateCommand(sidecarId, "idle", { lastStopReason }))` — just a state+metadata update, server auto-emits `agent_state_changed`
|
|
102
|
-
|
|
103
|
-
`handleSubagentStart()`:
|
|
104
|
-
- **Before:** `emitEvent(buildSubagentStartEvent)`
|
|
105
|
-
- **After:** `sendToSidecar(buildSubagentSpawnCommand(...))` — spawn in MAP for observability
|
|
106
|
-
|
|
107
|
-
`handleSubagentStop()`:
|
|
108
|
-
- **Before:** `emitEvent(buildSubagentStopEvent)`
|
|
109
|
-
- **After:** `sendToSidecar(buildSubagentDoneCommand(...))`
|
|
110
|
-
|
|
111
|
-
`handleTeammateIdle()`:
|
|
112
|
-
- **Before:** `sendToSidecar({ action: "state" })` + `emitEvent(buildTeammateIdleEvent)`
|
|
113
|
-
- **After:** `sendToSidecar(buildStateCommand(agentId, "idle"))` — state change only, server auto-emits
|
|
114
|
-
|
|
115
|
-
`handleTaskCompleted()`:
|
|
116
|
-
- **Before:** `emitEvent(buildTaskStatusCompletedEvent)`
|
|
117
|
-
- **After:** `emitEvent(buildTaskStatusPayload(...))`
|
|
118
|
-
|
|
119
|
-
### Step 4: Refactor `map-connection.mjs` — update `fireAndForget` and trajectory fallback
|
|
120
|
-
|
|
121
|
-
**File:** `src/map-connection.mjs`
|
|
122
|
-
|
|
123
|
-
`fireAndForget()`:
|
|
124
|
-
- Keep as-is but now it sends typed message payloads (for task events) rather than custom event types
|
|
125
|
-
|
|
126
|
-
`fireAndForgetTrajectory()`:
|
|
127
|
-
- **Before:** Fallback broadcasts `{ type: "swarm.sessionlog.sync", ... }`
|
|
128
|
-
- **After:** Fallback sends `{ type: "trajectory.checkpoint", checkpoint: {...} }` as a regular message payload — clients looking for trajectory data can filter on this
|
|
129
|
-
|
|
130
|
-
### Step 5: Update `sidecar-server.mjs` command handler — add `spawn`/`done` commands
|
|
131
|
-
|
|
132
|
-
**File:** `src/sidecar-server.mjs`
|
|
133
|
-
|
|
134
|
-
Add new command cases:
|
|
135
|
-
|
|
136
|
-
```javascript
|
|
137
|
-
case "spawn": {
|
|
138
|
-
if (conn) {
|
|
139
|
-
const result = await conn.spawn({
|
|
140
|
-
agentId: command.agent.agentId,
|
|
141
|
-
name: command.agent.name,
|
|
142
|
-
role: command.agent.role,
|
|
143
|
-
scopes: command.agent.scopes,
|
|
144
|
-
metadata: command.agent.metadata,
|
|
145
|
-
});
|
|
146
|
-
registeredAgents.set(command.agent.agentId, result);
|
|
147
|
-
}
|
|
148
|
-
respond(client, { ok: true });
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
case "done": {
|
|
153
|
-
if (conn) {
|
|
154
|
-
// Unregister the spawned child agent
|
|
155
|
-
try {
|
|
156
|
-
await conn.callExtension("map/agents/unregister", {
|
|
157
|
-
agentId: command.agentId,
|
|
158
|
-
reason: command.reason || "completed",
|
|
159
|
-
});
|
|
160
|
-
} catch {
|
|
161
|
-
// Agent may already be gone
|
|
162
|
-
}
|
|
163
|
-
registeredAgents.delete(command.agentId);
|
|
164
|
-
}
|
|
165
|
-
respond(client, { ok: true });
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Change `registeredAgents` from `Set` to `Map` in `scripts/map-sidecar.mjs`.
|
|
171
|
-
|
|
172
|
-
Keep old `register`/`unregister` commands working (deprecated) for backward compat during rollout.
|
|
173
|
-
|
|
174
|
-
### Step 6: Update tests
|
|
175
|
-
|
|
176
|
-
**Files:**
|
|
177
|
-
- `src/__tests__/map-events.test.mjs` — Update for new function signatures and return shapes
|
|
178
|
-
- `src/__tests__/sidecar-server.test.mjs` — Add tests for `spawn`/`done` commands, update `register`/`unregister` tests
|
|
179
|
-
|
|
180
|
-
### Step 7: Update `CLAUDE.md` documentation
|
|
181
|
-
|
|
182
|
-
Update the "MAP hooks" section to reflect:
|
|
183
|
-
- No custom `swarm.*` event types
|
|
184
|
-
- Agent lifecycle uses `spawn()`/`done()` SDK primitives
|
|
185
|
-
- Task lifecycle uses typed message payloads
|
|
186
|
-
- Turn lifecycle uses `updateState()` + `updateMetadata()`
|
|
187
|
-
- Clients subscribe to standard MAP events only
|
|
188
|
-
|
|
189
|
-
---
|
|
190
|
-
|
|
191
|
-
## What stays the same
|
|
192
|
-
|
|
193
|
-
- **Sidecar architecture** — persistent process with socket IPC, unchanged
|
|
194
|
-
- **Hook wiring** — same hooks, same scripts, same conditions in `hooks.json`
|
|
195
|
-
- **Trajectory checkpoints** — same `callExtension("trajectory/checkpoint")` with broadcast fallback
|
|
196
|
-
- **Inbox system** — unchanged (read/clear/format/write)
|
|
197
|
-
- **Role matching** — unchanged (`roles.mjs`)
|
|
198
|
-
- **Config system** — unchanged (`config.mjs`)
|
|
199
|
-
- **Bootstrap flow** — unchanged (`bootstrap.mjs`)
|
|
200
|
-
- **Fire-and-forget recovery** — same sidecar → recovery → direct pattern
|
|
201
|
-
|
|
202
|
-
## What changes
|
|
203
|
-
|
|
204
|
-
- **No custom event types** — everything uses MAP SDK primitives or typed message payloads
|
|
205
|
-
- **Sidecar registers agents properly** — `conn.spawn()` instead of broadcasting fake events
|
|
206
|
-
- **Server emits lifecycle events automatically** — clients see `agent_registered`, `agent_state_changed`, etc.
|
|
207
|
-
- **Task events are messages, not events** — sent via `conn.send()` with typed payloads
|
|
208
|
-
- **Cleaner client integration** — subscribe to standard MAP events, no `swarm.*` parsing needed
|
|
209
|
-
|
|
210
|
-
## Risk / rollback
|
|
211
|
-
|
|
212
|
-
- The refactor is fully backward-compatible at the config level (`.claude-swarm.json` unchanged)
|
|
213
|
-
- Old `register`/`unregister` sidecar commands still work during transition
|
|
214
|
-
- If `conn.spawn()` fails (e.g., server doesn't support it), we can fall back to the old broadcast approach
|