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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CLAUDE.md +95 -0
- package/LICENSE +21 -0
- package/hooks/hooks.json +9 -0
- package/package.json +15 -2
- package/renovate.json5 +6 -0
- package/scripts/map-hook.mjs +88 -7
- package/scripts/map-sidecar.mjs +210 -1
- package/src/__tests__/cascade-client.test.mjs +217 -0
- package/src/__tests__/cascade-diff-server.test.mjs +375 -0
- package/src/__tests__/cascade-watcher.test.mjs +475 -0
- package/src/__tests__/config.test.mjs +23 -0
- package/src/__tests__/loadout-schema-bridge.test.mjs +1 -2
- package/src/__tests__/sidecar-nudge.test.mjs +137 -0
- package/src/bootstrap.mjs +20 -9
- package/src/cascade-client.mjs +334 -0
- package/src/cascade-diff-server.mjs +326 -0
- package/src/cascade-events.mjs +285 -0
- package/src/cascade-watcher.mjs +694 -0
- package/src/config.mjs +7 -0
- package/src/map-connection.mjs +18 -1
- package/src/map-events.mjs +8 -1
- package/src/paths.mjs +12 -0
- package/src/sidecar-server.mjs +62 -0
- package/src/skilltree-client.mjs +1 -1
|
@@ -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.
|
|
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
|
+
"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.
|
|
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
package/scripts/map-hook.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
106
|
-
|
|
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 });
|