claude-code-swarm 0.3.26 → 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.
@@ -0,0 +1,285 @@
1
+ /**
2
+ * cascade-events.mjs — x-cascade/* event builders + emit for claude-code-swarm
3
+ *
4
+ * cc-swarm emits `x-cascade/*` notifications itself over the MAP connection
5
+ * (rather than letting git-cascade's `emit` callback drive them). This module
6
+ * builds the snake_case wire payloads and forwards them as MAP extension
7
+ * notifications.
8
+ *
9
+ * Wire shapes match git-cascade's `events/index.d.ts` (StreamOpenedParams etc.)
10
+ * so an OpenHive hub can consume them with no translation.
11
+ */
12
+
13
+ import { createLogger } from "./log.mjs";
14
+
15
+ const log = createLogger("cascade-events");
16
+
17
+ /**
18
+ * Build the `x-cascade/stream.opened` param shape (snake_case wire format).
19
+ *
20
+ * Mirrors git-cascade's `StreamOpenedParams`. Always sets `is_local_mode: true`
21
+ * — cc-swarm only ever registers existing branches as local-mode streams.
22
+ *
23
+ * `parent_stream` carries the fork edge: when the watcher detects a branch
24
+ * forked from a tracked branch it passes the parent's stream id, and the hub's
25
+ * `cascade-handler` writes it as `parent_stream_id` so the PR-stack walker can
26
+ * traverse the stack. Omitted (left undefined) when there is no parent.
27
+ *
28
+ * @param {object} opts
29
+ * @param {string} opts.streamId git-cascade stream id
30
+ * @param {string} opts.name Human-readable stream name
31
+ * @param {string} opts.agentId Owning agent id
32
+ * @param {string} opts.baseCommit Commit the stream was based from
33
+ * @param {string} [opts.branchName] Branch the stream maps to
34
+ * @param {string} [opts.parentStream] Parent stream id, when forked
35
+ * @param {object} [opts.metadata] Free-form caller metadata
36
+ * @returns {object} StreamOpenedParams-shaped object
37
+ */
38
+ export function buildStreamOpenedParams({ streamId, name, agentId, baseCommit, branchName, parentStream, metadata } = {}) {
39
+ return {
40
+ stream_id: streamId,
41
+ name,
42
+ agent_id: agentId,
43
+ base_commit: baseCommit,
44
+ branch_name: branchName,
45
+ is_local_mode: true,
46
+ ...(parentStream ? { parent_stream: parentStream } : {}),
47
+ metadata: metadata || {},
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Emit an `x-cascade/stream.opened` notification over a MAP connection.
53
+ *
54
+ * Fire-and-forget: any failure is caught and logged — this never throws, so a
55
+ * missing/dead connection can't crash the sidecar.
56
+ *
57
+ * @param {object} connection A MAP AgentConnection (must expose callExtension)
58
+ * @param {object} params Payload from buildStreamOpenedParams()
59
+ */
60
+ export function emitStreamOpened(connection, params) {
61
+ emitCascadeEvent(connection, "x-cascade/stream.opened", params);
62
+ }
63
+
64
+ /**
65
+ * Build the `x-cascade/stream.committed` param shape (snake_case wire format).
66
+ *
67
+ * Mirrors git-cascade's `StreamCommittedParams`. The watcher pulls real git
68
+ * data (summary, files, parent) and a git-cascade change id per commit.
69
+ *
70
+ * @param {object} opts
71
+ * @param {string} opts.streamId Stream that received the commit
72
+ * @param {string} opts.commitHash Commit SHA
73
+ * @param {string} opts.changeId git-cascade Change-Id for this change
74
+ * @param {string} opts.agentId Authoring agent id ("" when unattributed)
75
+ * @param {string} opts.messageSummary First line of the commit message
76
+ * @param {string[]} opts.filesTouched Files modified by the commit
77
+ * @param {string} opts.parentCommit Parent commit SHA
78
+ * @param {object} [opts.metadata] Free-form caller metadata
79
+ * @returns {object} StreamCommittedParams-shaped object
80
+ */
81
+ export function buildStreamCommittedParams({ streamId, commitHash, changeId, agentId, messageSummary, filesTouched, parentCommit, metadata } = {}) {
82
+ return {
83
+ stream_id: streamId,
84
+ commit_hash: commitHash,
85
+ change_id: changeId || "",
86
+ agent_id: agentId || "",
87
+ message_summary: messageSummary || "",
88
+ files_touched: Array.isArray(filesTouched) ? filesTouched : [],
89
+ parent_commit: parentCommit || "",
90
+ metadata: metadata || {},
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Emit an `x-cascade/stream.committed` notification over a MAP connection.
96
+ * Fire-and-forget — never throws.
97
+ *
98
+ * @param {object} connection A MAP AgentConnection (must expose callExtension)
99
+ * @param {object} params Payload from buildStreamCommittedParams()
100
+ */
101
+ export function emitStreamCommitted(connection, params) {
102
+ emitCascadeEvent(connection, "x-cascade/stream.committed", params);
103
+ }
104
+
105
+ /**
106
+ * Build the `x-cascade/stream.merged` param shape (snake_case wire format).
107
+ *
108
+ * Mirrors git-cascade's `StreamMergedParams`. `source_stream_id` is best-effort
109
+ * — the watcher resolves the source branch by the 2nd parent commit and it may
110
+ * be empty when the source branch was already deleted.
111
+ *
112
+ * @param {object} opts
113
+ * @param {string} opts.sourceStreamId Stream merged FROM ("" when unresolved)
114
+ * @param {string} opts.targetStreamId Stream merged INTO
115
+ * @param {string} opts.mergeCommit Resulting merge commit SHA
116
+ * @param {string} opts.agentId Agent that performed the merge
117
+ * @param {string} [opts.sourceCommit] Head of the source at merge time
118
+ * @param {string} [opts.strategy] Merge strategy label
119
+ * @param {object} [opts.metadata] Free-form caller metadata
120
+ * @returns {object} StreamMergedParams-shaped object
121
+ */
122
+ export function buildStreamMergedParams({ sourceStreamId, targetStreamId, mergeCommit, agentId, sourceCommit, strategy, metadata } = {}) {
123
+ return {
124
+ source_stream_id: sourceStreamId || "",
125
+ target_stream_id: targetStreamId,
126
+ merge_commit: mergeCommit,
127
+ agent_id: agentId || "",
128
+ source_commit: sourceCommit || "",
129
+ strategy: strategy || "merge-commit",
130
+ metadata: metadata || {},
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Emit an `x-cascade/stream.merged` notification over a MAP connection.
136
+ * Fire-and-forget — never throws.
137
+ *
138
+ * @param {object} connection A MAP AgentConnection (must expose callExtension)
139
+ * @param {object} params Payload from buildStreamMergedParams()
140
+ */
141
+ export function emitStreamMerged(connection, params) {
142
+ emitCascadeEvent(connection, "x-cascade/stream.merged", params);
143
+ }
144
+
145
+ /**
146
+ * Build the `x-cascade/stream.pushed` param shape (snake_case wire format).
147
+ *
148
+ * Mirrors git-cascade's `StreamPushedParams`.
149
+ *
150
+ * @param {object} opts
151
+ * @param {string} opts.streamId Stream whose head was pushed
152
+ * @param {string} opts.agentId Agent that did the push
153
+ * @param {string} opts.pushedCommit Commit SHA at the head when pushed
154
+ * @param {string} opts.remote Remote name (e.g. 'origin')
155
+ * @param {string} opts.remoteRef Remote ref pushed to
156
+ * @param {object} [opts.metadata] Free-form caller metadata
157
+ * @returns {object} StreamPushedParams-shaped object
158
+ */
159
+ export function buildStreamPushedParams({ streamId, agentId, pushedCommit, remote, remoteRef, metadata } = {}) {
160
+ return {
161
+ stream_id: streamId,
162
+ agent_id: agentId || "",
163
+ pushed_commit: pushedCommit,
164
+ remote: remote || "origin",
165
+ remote_ref: remoteRef || "",
166
+ metadata: metadata || {},
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Emit an `x-cascade/stream.pushed` notification over a MAP connection.
172
+ * Fire-and-forget — never throws.
173
+ *
174
+ * @param {object} connection A MAP AgentConnection (must expose callExtension)
175
+ * @param {object} params Payload from buildStreamPushedParams()
176
+ */
177
+ export function emitStreamPushed(connection, params) {
178
+ emitCascadeEvent(connection, "x-cascade/stream.pushed", params);
179
+ }
180
+
181
+ /**
182
+ * Build the `x-cascade/stream.conflicted` param shape (snake_case wire format).
183
+ *
184
+ * Mirrors git-cascade's `StreamConflictedParams`. cc-swarm emits this on the
185
+ * transition where a tracked stream enters an in-progress merge state (i.e.
186
+ * the watcher observes `.git/MERGE_HEAD` appear). Rebase conflicts are out of
187
+ * scope for v1.
188
+ *
189
+ * @param {object} opts
190
+ * @param {string} opts.streamId Stream that became conflicted
191
+ * @param {string} [opts.conflictId] Persisted conflict record id (cf-xxx)
192
+ * @param {string[]} opts.conflictedFiles Files reported as conflicted
193
+ * @param {string} [opts.agentId] Agent that triggered the conflicting op
194
+ * @param {string} [opts.conflictingCommit] Commit being applied (e.g. MERGE_HEAD)
195
+ * @param {string} [opts.targetCommit] Commit being applied onto (HEAD)
196
+ * @param {string} [opts.source] Operation flavor: "merge" | "rebase" | ...
197
+ * @param {object} [opts.metadata] Free-form caller metadata
198
+ * @returns {object} StreamConflictedParams-shaped object
199
+ */
200
+ export function buildStreamConflictedParams({ streamId, conflictId, conflictedFiles, agentId, conflictingCommit, targetCommit, source, metadata } = {}) {
201
+ return {
202
+ stream_id: streamId,
203
+ ...(conflictId ? { conflict_id: conflictId } : {}),
204
+ conflicted_files: Array.isArray(conflictedFiles) ? conflictedFiles : [],
205
+ agent_id: agentId || "",
206
+ conflicting_commit: conflictingCommit || "",
207
+ target_commit: targetCommit || "",
208
+ source: source || "merge",
209
+ metadata: metadata || {},
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Emit an `x-cascade/stream.conflicted` notification over a MAP connection.
215
+ * Fire-and-forget — never throws.
216
+ *
217
+ * @param {object} connection A MAP AgentConnection (must expose callExtension)
218
+ * @param {object} params Payload from buildStreamConflictedParams()
219
+ */
220
+ export function emitStreamConflicted(connection, params) {
221
+ emitCascadeEvent(connection, "x-cascade/stream.conflicted", params);
222
+ }
223
+
224
+ /**
225
+ * Build the `x-cascade/stream.conflict_resolved` param shape (snake_case wire).
226
+ *
227
+ * Mirrors git-cascade's `StreamConflictResolvedParams`. cc-swarm emits this on
228
+ * the transition where the in-progress merge state goes away: either HEAD
229
+ * advanced to a merge commit (`manual` / `agent` resolution) or HEAD is
230
+ * unchanged (the merge was aborted — `abandoned`).
231
+ *
232
+ * @param {object} opts
233
+ * @param {string} opts.streamId Stream whose conflict was resolved
234
+ * @param {string} opts.conflictId Conflict record id that was resolved
235
+ * @param {string} opts.resolutionMethod "manual" | "agent" | "abandoned" | ...
236
+ * @param {string} [opts.resolvedBy] Agent or human that resolved it
237
+ * @param {string} [opts.resolutionSummary] Optional human-readable summary
238
+ * @param {object} [opts.metadata] Free-form caller metadata
239
+ * @returns {object} StreamConflictResolvedParams-shaped object
240
+ */
241
+ export function buildStreamConflictResolvedParams({ streamId, conflictId, resolutionMethod, resolvedBy, resolutionSummary, metadata } = {}) {
242
+ return {
243
+ stream_id: streamId,
244
+ conflict_id: conflictId || "",
245
+ resolution_method: resolutionMethod || "manual",
246
+ ...(resolvedBy ? { resolved_by: resolvedBy } : {}),
247
+ ...(resolutionSummary ? { resolution_summary: resolutionSummary } : {}),
248
+ metadata: metadata || {},
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Emit an `x-cascade/stream.conflict_resolved` notification over a MAP connection.
254
+ * Fire-and-forget — never throws.
255
+ *
256
+ * @param {object} connection A MAP AgentConnection (must expose callExtension)
257
+ * @param {object} params Payload from buildStreamConflictResolvedParams()
258
+ */
259
+ export function emitStreamConflictResolved(connection, params) {
260
+ emitCascadeEvent(connection, "x-cascade/stream.conflict_resolved", params);
261
+ }
262
+
263
+ /**
264
+ * Shared fire-and-forget emit for all `x-cascade/*` notifications.
265
+ *
266
+ * Any failure (no connection, callExtension throws, promise rejects) is caught
267
+ * and logged. This never throws — a missing/dead connection or a misbehaving
268
+ * hub can't crash the sidecar.
269
+ *
270
+ * @param {object} connection A MAP AgentConnection (must expose callExtension)
271
+ * @param {string} method Full `x-cascade/*` method name
272
+ * @param {object} params Snake_case wire payload
273
+ */
274
+ function emitCascadeEvent(connection, method, params) {
275
+ if (!connection || typeof connection.callExtension !== "function") {
276
+ log.debug("skipping cascade emit: no MAP connection", { method });
277
+ return;
278
+ }
279
+ try {
280
+ Promise.resolve(connection.callExtension(method, params))
281
+ .catch((err) => log.warn("cascade emit failed", { method, error: err.message }));
282
+ } catch (err) {
283
+ log.warn("cascade emit threw", { method, error: err.message });
284
+ }
285
+ }