claude-code-swarm 0.3.23 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-swarm",
3
- "version": "0.3.23",
3
+ "version": "0.3.24",
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.23",
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.23",
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": {
@@ -55,7 +55,7 @@
55
55
  "devDependencies": {
56
56
  "agent-inbox": "^0.1.9",
57
57
  "minimem": "^0.1.1",
58
- "opentasks": "^0.1.1",
58
+ "opentasks": "^0.1.2",
59
59
  "skill-tree": "^0.1.5",
60
60
  "vitest": "^4.0.18",
61
61
  "ws": "^8.0.0"
@@ -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
+ }