claude-code-swarm 0.3.21 → 0.3.23

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-swarm",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "Launch Claude Code with swarmkit capabilities, including team orchestration, MAP observability, and session tracking.",
5
5
  "owner": {
6
6
  "name": "alexngai"
@@ -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.21",
4
+ "version": "0.3.23",
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.21",
3
+ "version": "0.3.23",
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.0.8",
58
+ "opentasks": "^0.1.1",
59
59
  "skill-tree": "^0.1.5",
60
60
  "vitest": "^4.0.18",
61
61
  "ws": "^8.0.0"
@@ -5,9 +5,17 @@ import { registerOpenTasksHandler } from "../opentasks-connector.mjs";
5
5
 
6
6
  const MOCK_METHODS = {
7
7
  QUERY_REQUEST: "opentasks/query.request",
8
+ QUERY_RESPONSE: "opentasks/query.response",
8
9
  LINK_REQUEST: "opentasks/link.request",
10
+ LINK_RESPONSE: "opentasks/link.response",
9
11
  ANNOTATE_REQUEST: "opentasks/annotate.request",
12
+ ANNOTATE_RESPONSE: "opentasks/annotate.response",
10
13
  TASK_REQUEST: "opentasks/task.request",
14
+ TASK_RESPONSE: "opentasks/task.response",
15
+ GRAPH_CREATE_REQUEST: "opentasks/graph.create.request",
16
+ GRAPH_CREATE_RESPONSE: "opentasks/graph.create.response",
17
+ GRAPH_UPDATE_REQUEST: "opentasks/graph.update.request",
18
+ GRAPH_UPDATE_RESPONSE: "opentasks/graph.update.response",
11
19
  };
12
20
 
13
21
  function createMockConnection() {
@@ -89,16 +97,53 @@ describe("registerOpenTasksHandler", () => {
89
97
  expect(callArgs.agentId).toBe("swarm:test-sidecar");
90
98
  });
91
99
 
92
- it("registers onNotification for all 4 request methods", async () => {
100
+ it("registers onNotification for every *.request method exported by opentasks", async () => {
93
101
  await callRegister();
94
102
 
95
- expect(mockConn.onNotification).toHaveBeenCalledTimes(4);
103
+ const expectedRequestMethods = Object.values(MOCK_METHODS).filter((m) =>
104
+ m.endsWith(".request"),
105
+ );
106
+ expect(mockConn.onNotification).toHaveBeenCalledTimes(expectedRequestMethods.length);
96
107
 
97
108
  const registeredMethods = mockConn.onNotification.mock.calls.map((c) => c[0]);
98
109
  expect(registeredMethods).toContain(MOCK_METHODS.QUERY_REQUEST);
99
110
  expect(registeredMethods).toContain(MOCK_METHODS.LINK_REQUEST);
100
111
  expect(registeredMethods).toContain(MOCK_METHODS.ANNOTATE_REQUEST);
101
112
  expect(registeredMethods).toContain(MOCK_METHODS.TASK_REQUEST);
113
+ expect(registeredMethods).toContain(MOCK_METHODS.GRAPH_CREATE_REQUEST);
114
+ expect(registeredMethods).toContain(MOCK_METHODS.GRAPH_UPDATE_REQUEST);
115
+ });
116
+
117
+ it("does not register response methods (only .request is subscribed)", async () => {
118
+ await callRegister();
119
+
120
+ const registeredMethods = mockConn.onNotification.mock.calls.map((c) => c[0]);
121
+ const responseMethods = registeredMethods.filter((m) => m.endsWith(".response"));
122
+ expect(responseMethods).toEqual([]);
123
+ });
124
+
125
+ it("forwards graph.create.request to the connector", async () => {
126
+ await callRegister();
127
+
128
+ const params = { request_id: "req-c1", create: { type: "task", title: "New" } };
129
+ await mockConn._fireNotification(MOCK_METHODS.GRAPH_CREATE_REQUEST, params);
130
+
131
+ expect(mockOpentasks._connector.handleNotification).toHaveBeenCalledWith(
132
+ MOCK_METHODS.GRAPH_CREATE_REQUEST,
133
+ params,
134
+ );
135
+ });
136
+
137
+ it("forwards graph.update.request to the connector", async () => {
138
+ await callRegister();
139
+
140
+ const params = { request_id: "req-u1", update: { id: "n-1", title: "Renamed" } };
141
+ await mockConn._fireNotification(MOCK_METHODS.GRAPH_UPDATE_REQUEST, params);
142
+
143
+ expect(mockOpentasks._connector.handleNotification).toHaveBeenCalledWith(
144
+ MOCK_METHODS.GRAPH_UPDATE_REQUEST,
145
+ params,
146
+ );
102
147
  });
103
148
 
104
149
  it("forwards notifications to connector.handleNotification", async () => {
@@ -63,13 +63,13 @@ export async function registerOpenTasksHandler(conn, options = {}) {
63
63
  agentId: `${scope}-sidecar`,
64
64
  });
65
65
 
66
- // Register handlers for all 4 request methods
67
- const requestMethods = [
68
- MAP_CONNECTOR_METHODS.QUERY_REQUEST,
69
- MAP_CONNECTOR_METHODS.LINK_REQUEST,
70
- MAP_CONNECTOR_METHODS.ANNOTATE_REQUEST,
71
- MAP_CONNECTOR_METHODS.TASK_REQUEST,
72
- ];
66
+ // Subscribe to every `opentasks/*.request` method the opentasks package
67
+ // exports. Iterating MAP_CONNECTOR_METHODS (rather than hardcoding a list)
68
+ // means new request methods added upstream — e.g. graph.create.request,
69
+ // graph.update.request — are wired up automatically.
70
+ const requestMethods = Object.values(MAP_CONNECTOR_METHODS).filter(
71
+ (m) => typeof m === "string" && m.endsWith(".request"),
72
+ );
73
73
 
74
74
  for (const method of requestMethods) {
75
75
  conn.onNotification(method, async (params) => {
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