claude-code-swarm 0.3.25 → 0.4.0

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.25",
3
+ "version": "0.4.0",
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.25",
4
+ "version": "0.4.0",
5
5
  "author": {
6
6
  "name": "alexngai"
7
7
  },
package/CLAUDE.md CHANGED
@@ -226,6 +226,98 @@ Skill-tree options:
226
226
  - `basePath` — Path to skill-tree storage directory (default: `.swarm/skill-tree/`)
227
227
  - `defaultProfile` — Default profile when no role-specific criteria exist (default: `""`)
228
228
 
229
+ ### Cascade integration (Phases 1–2)
230
+ ```json
231
+ {
232
+ "template": "gsd",
233
+ "map": { "server": "ws://localhost:8080" },
234
+ "cascade": {
235
+ "enabled": true
236
+ }
237
+ }
238
+ ```
239
+
240
+ When enabled, the MAP sidecar opens a persistent `git-cascade` tracker
241
+ (`MultiAgentRepoTracker`) in **local mode** as a local state store under
242
+ `.swarm/claude-swarm/tmp/cascade/tracker.db`. On boot it registers the
243
+ repository's current working branch as a local-mode git-cascade stream and
244
+ emits a single `x-cascade/stream.opened` notification over the MAP connection
245
+ so an OpenHive hub's cascade subsystem can observe the swarm.
246
+
247
+ **Phase 2 — observed git.** The sidecar also runs a poll-based ref watcher
248
+ (`src/cascade-watcher.mjs`). Every ~3s it diffs `git for-each-ref` snapshots
249
+ and, when it detects git activity, emits `x-cascade/*` events with real git
250
+ data: `stream.committed` (per new commit, with message summary / files touched
251
+ / parent / git-cascade change id), `stream.merged` (for merge commits, with
252
+ best-effort source-stream resolution via the 2nd parent), `stream.pushed`
253
+ (when a remote-tracking ref catches up to a local branch), and `stream.opened`
254
+ (for newly-appeared branches + a connection re-assert on MAP reconnect). The
255
+ watcher is the **detector**; it works fully standalone, emitting unattributed
256
+ events when no attribution is present.
257
+
258
+ After the ref-diff pass each tick the watcher also probes in-progress
259
+ **merge-conflict** state: a cheap `existsSync` on the worktree-local
260
+ `.git/MERGE_HEAD`. On the off→on transition it records a conflict row
261
+ (`recordObservedConflict` → git-cascade `conflicts.createConflict`) and emits
262
+ `x-cascade/stream.conflicted` with the conflicted files (`git diff
263
+ --name-only --diff-filter=U`), the conflicting commit (`MERGE_HEAD`), the
264
+ target commit (`HEAD`), and `source: "merge"`. On the on→off transition it
265
+ discriminates by HEAD: if HEAD advanced to a commit with ≥2 parents the merge
266
+ was resolved + committed (`resolution_method: "manual"`, or `"agent"` when a
267
+ fresh attribution hint is present) — otherwise the merge was aborted (HEAD
268
+ unchanged → `"abandoned"`, via `conflicts.abandonConflict`). Conflict events
269
+ fire only on transitions, never on every tick while a conflict is open. The
270
+ sidecar advertises `cascade.emitsConflicts: true` honestly off the back of
271
+ this. Rebase-conflict observation (`.git/rebase-merge/`,
272
+ `.git/rebase-apply/`) is a known future expansion — v1 covers `git merge`
273
+ conflicts only.
274
+
275
+ A `PostToolUse(Bash)` hook supplies **attribution only** — it does not detect
276
+ git. It builds an `{ agentId, taskRef, ts }` hint and pushes it to the sidecar
277
+ via the `cascade-attribution` socket command. The watcher stamps `agent_id` /
278
+ `metadata.task_ref` on emitted events when a fresh hint (within ~30s) exists.
279
+ `taskRef` is correlated against the in-progress opentasks task only when
280
+ `opentasks.enabled`; without opentasks, attribution carries `agent_id` only.
281
+
282
+ When the watcher detects a branch forked from a tracked branch it links the
283
+ fork: the `x-cascade/stream.opened` event carries `parent_stream` (the parent
284
+ branch's stream id) and the git-cascade tracker DB records the parent edge, so
285
+ an OpenHive hub's PR-stack walker can traverse parent → child.
286
+
287
+ **Phase 3 — diff serving.** The sidecar wires a diff server
288
+ (`src/cascade-diff-server.mjs`) that registers an inbound `cascade/diff.request`
289
+ handler on the MAP connection, so an OpenHive hub's cascade changelog/diff
290
+ endpoints work for cc-swarm-tracked streams. On a request the server resolves
291
+ the diff with plain git in the repo cwd — cc-swarm runs git-cascade in local
292
+ mode with no worktrees, so the request's `head` / `base` (commit hashes,
293
+ self-identifying) drive `git show <head>` (single commit) or
294
+ `git diff <base>..<head>` (range), with `--name-only` when `files_only` is set
295
+ and a `file_paths` path restriction when present. `files_touched` is always
296
+ computed via `--name-only`. A 50 MB stdout cap and 30 s timeout bound the git
297
+ spawn. The server replies inline (`cascade/diff.response`, `streaming: false`)
298
+ when the raw diff is ≤ 512 KB; larger diffs send a streaming announcement
299
+ (`streaming: true`) followed by 1 MB base64 `cascade/diff.chunk` notifications,
300
+ seq-ordered, with `final: true` + a sha256 over the full payload on the last
301
+ chunk. Any error (bad request, git failure) folds into the typed
302
+ `cascade/diff.response` error variant — the server never throws.
303
+
304
+ The sidecar declares the `cascade: { canServeDiff: true }` MAP capability at
305
+ registration, **conditional on `cascade.enabled`** — the same gate the diff
306
+ server is wired on. Declaring `canServeDiff` without the server would invite
307
+ `cascade/diff.request` notifications from the hub that time out.
308
+
309
+ cc-swarm emits `x-cascade/*` events itself over MAP — the tracker is not given
310
+ git-cascade's `emit` callback. Cascade is only meaningful when `map` is enabled
311
+ (there is otherwise no connection to emit on); when `map` is disabled it is an
312
+ inert no-op rather than a hard failure. All cascade work is fully gated on
313
+ `cascade.enabled` and wrapped so a cascade failure can never crash the sidecar.
314
+
315
+ Cascade options:
316
+ - `enabled` — Enable git-cascade integration (default: `false`)
317
+
318
+ Requires `git-cascade` (`>= ^0.0.7`), installed on demand via swarmkit during
319
+ bootstrap when `cascade.enabled`.
320
+
229
321
  ### Logging
230
322
  ```json
231
323
  {
@@ -303,6 +395,7 @@ All config values can be overridden via `SWARM_*` environment variables. Priorit
303
395
  | `skilltree.enabled` | `SWARM_SKILLTREE_ENABLED` | boolean (`true`/`1`/`yes`) | `false` |
304
396
  | `skilltree.basePath` | `SWARM_SKILLTREE_BASE_PATH` | string | `""` |
305
397
  | `skilltree.defaultProfile` | `SWARM_SKILLTREE_DEFAULT_PROFILE` | string | `""` |
398
+ | `cascade.enabled` | `SWARM_CASCADE_ENABLED` | boolean (`true`/`1`/`yes`) | `false` |
306
399
  | `mesh.enabled` | `SWARM_MESH_ENABLED` | boolean (`true`/`1`/`yes`) | `false` |
307
400
  | `mesh.peerId` | `SWARM_MESH_PEER_ID` | string | `""` |
308
401
  | `mesh.mapServer` | `SWARM_MESH_MAP_SERVER` | string | `""` |
@@ -404,6 +497,7 @@ Both modes:
404
497
  - `trajectory: { canReport: true, canServeContent: true }` — reports checkpoints, serves transcript content on demand
405
498
  - `tasks: { canCreate, canAssign, canUpdate, canList }` — task management
406
499
  - `opentasks: { canQuery, canLink, canAnnotate, canTask }` — conditional, when task_graph configured
500
+ - `cascade: { canServeDiff: true }` — conditional, when `cascade.enabled` — sidecar serves unified diffs on demand via `cascade/diff.request` (see Cascade integration above)
407
501
 
408
502
  Message delivery is **pull-based**: the `UserPromptSubmit` hook reads the inbox on each turn and injects messages into Claude Code's prompt context. No real-time push delivery.
409
503
 
@@ -465,6 +559,7 @@ Global (managed by swarmkit, installed on demand during bootstrap):
465
559
  - **sessionlog** — git-integrated session capture (installed when `sessionlog.enabled: true`)
466
560
  - **minimem** — file-based memory with vector search (installed when `minimem.enabled: true`)
467
561
  - **skill-tree** — versioned skill library with serving layer (installed when `skilltree.enabled: true`)
562
+ - **git-cascade** — multi-agent git tracking; local-mode tracker for cascade observability (installed when `cascade.enabled: true`)
468
563
 
469
564
  Runtime:
470
565
  - **Claude Code agent teams** — enabled via `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` in settings.json
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Ngai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/hooks/hooks.json CHANGED
@@ -80,6 +80,15 @@
80
80
  }
81
81
  ]
82
82
  },
83
+ {
84
+ "matcher": "Bash",
85
+ "hooks": [
86
+ {
87
+ "type": "command",
88
+ "command": "node -e \"const f=require('fs'),p=require('path'),h=require('os').homedir();let c={};try{c=JSON.parse(f.readFileSync('.swarm/claude-swarm/config.json','utf-8'))}catch{try{c=JSON.parse(f.readFileSync(p.join(h,'.claude-swarm','config.json'),'utf-8'))}catch{}};process.exit((c.cascade?.enabled||process.env.SWARM_CASCADE_ENABLED)&&(c.map?.enabled||c.map?.server||process.env.SWARM_MAP_SERVER||process.env.SWARM_MAP_ENABLED)?0:1)\" 2>/dev/null && node \"${CLAUDE_PLUGIN_ROOT}/scripts/map-hook.mjs\" cascade-bash-attribution"
89
+ }
90
+ ]
91
+ },
83
92
  {
84
93
  "matcher": "TaskCreate",
85
94
  "hooks": [
package/package.json CHANGED
@@ -1,7 +1,13 @@
1
1
  {
2
2
  "name": "claude-code-swarm",
3
- "version": "0.3.25",
3
+ "version": "0.4.0",
4
4
  "description": "Claude Code plugin for launching agent teams from openteams topologies with MAP observability",
5
+ "author": "Alex Ngai",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/alexngai/claude-code-swarm.git"
10
+ },
5
11
  "type": "module",
6
12
  "exports": {
7
13
  ".": "./src/index.mjs",
@@ -24,6 +30,7 @@
24
30
  },
25
31
  "peerDependencies": {
26
32
  "agent-inbox": "*",
33
+ "git-cascade": ">=0.0.9",
27
34
  "opentasks": ">=0.1.1",
28
35
  "swarmkit": "*"
29
36
  },
@@ -31,6 +38,9 @@
31
38
  "agent-inbox": {
32
39
  "optional": true
33
40
  },
41
+ "git-cascade": {
42
+ "optional": true
43
+ },
34
44
  "opentasks": {
35
45
  "optional": true
36
46
  }
@@ -45,6 +55,7 @@
45
55
  "test:e2e:tier4": "vitest run --config e2e/vitest.config.e2e.mjs -t tier4",
46
56
  "test:e2e:tier5": "vitest run --config e2e/vitest.config.e2e.mjs -t tier5",
47
57
  "test:e2e:tier7": "vitest run --config e2e/vitest.config.e2e.mjs -t tier7",
58
+ "prepublishOnly": "publint",
48
59
  "version:patch": "npm version patch --no-git-tag-version && node scripts/sync-version.mjs",
49
60
  "version:minor": "npm version minor --no-git-tag-version && node scripts/sync-version.mjs",
50
61
  "version:major": "npm version major --no-git-tag-version && node scripts/sync-version.mjs"
@@ -53,9 +64,11 @@
53
64
  "node": ">=18.0.0"
54
65
  },
55
66
  "devDependencies": {
56
- "agent-inbox": "^0.1.9",
67
+ "agent-inbox": "^0.2.3",
68
+ "git-cascade": "^0.0.9",
57
69
  "minimem": "^0.1.1",
58
70
  "opentasks": "^0.1.2",
71
+ "publint": "^0.3.21",
59
72
  "skill-tree": "^0.2.0",
60
73
  "vitest": "^4.0.18",
61
74
  "ws": "^8.0.0"
package/renovate.json5 ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ // Dependency-update policy is shared across the swarm/openhive ecosystem.
3
+ // See github.com/alexngai/swarm-renovate-config.
4
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
5
+ "extends": ["github>alexngai/swarm-renovate-config"]
6
+ }
@@ -20,6 +20,7 @@
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
22
  * sessionlog-dispatch — Dispatch a sessionlog lifecycle hook via programmatic API
23
+ * cascade-bash-attribution — Push a cascade attribution hint to the sidecar
23
24
  *
24
25
  * Usage: node map-hook.mjs <action>
25
26
  * Hook event data is read from stdin (JSON).
@@ -36,7 +37,7 @@ configureNodePath();
36
37
  const log = createLogger("map-hook");
37
38
  import { readRoles, matchRole } from "../src/roles.mjs";
38
39
  import { formatInboxAsMarkdown } from "../src/inbox.mjs";
39
- import { sendToInbox } from "../src/sidecar-client.mjs";
40
+ import { sendToInbox, sendToSidecar } from "../src/sidecar-client.mjs";
40
41
  import { sessionPaths } from "../src/paths.mjs";
41
42
  import {
42
43
  sendCommand,
@@ -51,7 +52,7 @@ import {
51
52
  buildMinimemBridgeCommand,
52
53
  } from "../src/map-events.mjs";
53
54
  import { syncSessionlog, dispatchSessionlogHook } from "../src/sessionlog.mjs";
54
- import { findSocketPath, pushSyncEvent } from "../src/opentasks-client.mjs";
55
+ import { findSocketPath, pushSyncEvent, rpcRequest } from "../src/opentasks-client.mjs";
55
56
 
56
57
  const action = process.argv[2];
57
58
 
@@ -77,7 +78,29 @@ async function handleInject(hookData, sessionId) {
77
78
  const sPaths = sessionPaths(sessionId);
78
79
  const config = readConfig();
79
80
 
80
- if (!config.inbox?.enabled) return;
81
+ // Check for dispatch thread nudges (advisory push from hub).
82
+ // Nudges arrive via x-dispatch/nudge MAP notifications; the sidecar
83
+ // stores them until this hook drains them. We check nudges even when
84
+ // inbox is disabled — the nudge path is independent.
85
+ // Uses sendToInbox (which waits for a response) on the sidecar socket.
86
+ let nudgeOutput = "";
87
+ try {
88
+ const nudgeResp = await sendToInbox(
89
+ { action: "check-nudge" },
90
+ sPaths.socketPath,
91
+ );
92
+ if (nudgeResp && nudgeResp.ok && nudgeResp.nudges?.length > 0) {
93
+ const ids = nudgeResp.nudges.map((n) => n.dispatch_id).join(", ");
94
+ nudgeOutput = `\n<dispatch-thread-nudge>\nYou have pending messages in dispatch coordination thread(s): ${ids}. Check your inbox for new turns.\n</dispatch-thread-nudge>\n`;
95
+ }
96
+ } catch {
97
+ // Best effort — nudge is advisory
98
+ }
99
+
100
+ if (!config.inbox?.enabled) {
101
+ if (nudgeOutput) process.stdout.write(nudgeOutput);
102
+ return;
103
+ }
81
104
 
82
105
  // Only check messages addressed to the main agent (not all scope messages).
83
106
  // Per-agent messages stay in storage for agents to pull via MCP tools.
@@ -89,10 +112,9 @@ async function handleInject(hookData, sessionId) {
89
112
  { action: "check_inbox", agentId: mainAgentId, scope, unreadOnly: true, clear: true },
90
113
  sPaths.inboxSocketPath
91
114
  );
92
- if (!resp || !resp.ok || !resp.messages?.length) return;
93
115
 
94
116
  // Forward task.* events to opentasks graph if enabled
95
- if (config.opentasks?.enabled) {
117
+ if (resp?.ok && resp.messages?.length && config.opentasks?.enabled) {
96
118
  const otSocketPath = findSocketPath();
97
119
  const taskEvents = resp.messages.filter(
98
120
  (m) => m.content?.type === "event" && m.content?.event?.startsWith("task.")
@@ -102,8 +124,12 @@ async function handleInject(hookData, sessionId) {
102
124
  }
103
125
  }
104
126
 
105
- const output = formatInboxAsMarkdown(resp.messages);
106
- if (output) process.stdout.write(output);
127
+ const inboxOutput = resp?.ok && resp.messages?.length
128
+ ? formatInboxAsMarkdown(resp.messages)
129
+ : "";
130
+
131
+ const output = (nudgeOutput + (inboxOutput || "")).trim();
132
+ if (output) process.stdout.write(output + "\n");
107
133
  }
108
134
 
109
135
  async function handleTurnCompleted(hookData, sessionId) {
@@ -207,6 +233,60 @@ async function handleMinimemMcpUsed(hookData, sessionId) {
207
233
  }
208
234
  }
209
235
 
236
+ // ── cascade attribution (PostToolUse Bash) ──────────────────────────────────
237
+ //
238
+ // Attribution-only: this does NOT parse the git command or detect git. The
239
+ // cascade-watcher in the sidecar is the detector. This hook just builds an
240
+ // attribution hint { agentId, taskRef, ts } and pushes it to the sidecar as a
241
+ // `cascade-attribution` command — the watcher reads the freshest hint to
242
+ // stamp agent_id / task_ref on observed-git events.
243
+
244
+ async function handleCascadeBashAttribution(hookData, sessionId) {
245
+ const config = readConfig();
246
+ if (!config.cascade?.enabled || !config.map?.enabled) return;
247
+
248
+ // Acting agent id — same resolution other hooks use: hook data first,
249
+ // then env. Falls back to the session id when nothing else identifies it.
250
+ const agentId =
251
+ hookData.agent_id ||
252
+ process.env.CLAUDE_AGENT_ID ||
253
+ process.env.MACRO_AGENT_ID ||
254
+ sessionId ||
255
+ "";
256
+
257
+ // taskRef — only correlated when opentasks is enabled. When opentasks is
258
+ // disabled we skip the query entirely and leave taskRef null; no task
259
+ // correlation is expected without opentasks (intended).
260
+ let taskRef = null;
261
+ if (config.opentasks?.enabled && agentId) {
262
+ try {
263
+ const otSocketPath = findSocketPath();
264
+ const nodes = await rpcRequest(
265
+ "graph.query",
266
+ { type: "task", filter: { status: "in_progress", assignee: agentId } },
267
+ otSocketPath,
268
+ );
269
+ const task = Array.isArray(nodes) ? nodes[0] : null;
270
+ if (task) {
271
+ // resource_id intentionally omitted — the hub resolves it from the
272
+ // swarm's registered task graph. Emitting a guessed value causes false
273
+ // bindings.
274
+ taskRef = {
275
+ node_id: task.node_id || task.id || "",
276
+ };
277
+ }
278
+ } catch {
279
+ // Best-effort — leave taskRef null on any failure.
280
+ }
281
+ }
282
+
283
+ const sPaths = sessionPaths(sessionId);
284
+ await sendToSidecar(
285
+ { action: "cascade-attribution", agentId, taskRef, ts: Date.now() },
286
+ sPaths.socketPath,
287
+ );
288
+ }
289
+
210
290
  // ── Main ──────────────────────────────────────────────────────────────────────
211
291
 
212
292
  async function main() {
@@ -228,6 +308,7 @@ async function main() {
228
308
  case "native-task-created": await handleNativeTaskCreated(hookData, sessionId); break;
229
309
  case "native-task-updated": await handleNativeTaskUpdated(hookData, sessionId); break;
230
310
  case "minimem-mcp-used": await handleMinimemMcpUsed(hookData, sessionId); break;
311
+ case "cascade-bash-attribution": await handleCascadeBashAttribution(hookData, sessionId); break;
231
312
  case "sessionlog-dispatch": await handleSessionlogDispatch(hookData); break;
232
313
  default:
233
314
  log.warn("unknown action", { action });