opencode-swarm-plugin 0.28.2 → 0.29.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 +1 @@
1
- {"version":3,"file":"swarm-orchestrate.d.ts","sourceRoot":"","sources":["../src/swarm-orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAwiBxB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;CA8JrB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;CAoFvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;CA8GzB,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;CA6E1B,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAknBzB,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkJ/B,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;CA6CjC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;CAmClC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;CAmB9B,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;CAoJ9B,CAAC;AA4BH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqG3B,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;CAsGxB,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgMtB,CAAC;AAMH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAc5B,CAAC"}
1
+ {"version":3,"file":"swarm-orchestrate.d.ts","sourceRoot":"","sources":["../src/swarm-orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA8hBxB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;CA8JrB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;CAoFvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;CAkHzB,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;CA6E1B,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqnBzB,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkJ/B,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;CA6CjC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;CAmClC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;CAmB9B,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;CAoJ9B,CAAC;AA4BH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqG3B,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;CAsGxB,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgMtB,CAAC;AAMH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAc5B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"tool-availability.d.ts","sourceRoot":"","sources":["../src/tool-availability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAUH,MAAM,MAAM,QAAQ,GAChB,iBAAiB,GACjB,MAAM,GACN,KAAK,GACL,MAAM,GACN,OAAO,GACP,YAAY,GACZ,YAAY,CAAC;AAEjB,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AA+PD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAWnE;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAGtE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,gBAAgB,CAAC,CAO3B;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAC5C,GAAG,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAChC,CA0BA;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAQpD;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/D;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACxB,QAAQ,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAC7B,OAAO,CAAC,CAAC,CAAC,CASZ;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CASxB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAGrC;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,GAAG,CAAC,QAAQ,EAAE,gBAAgB,CAAC,GAC5C,MAAM,CAWR"}
1
+ {"version":3,"file":"tool-availability.d.ts","sourceRoot":"","sources":["../src/tool-availability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAUH,MAAM,MAAM,QAAQ,GAChB,iBAAiB,GACjB,MAAM,GACN,KAAK,GACL,MAAM,GACN,OAAO,GACP,YAAY,GACZ,YAAY,CAAC;AAEjB,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AA6OD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAWnE;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAGtE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,gBAAgB,CAAC,CAO3B;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAC5C,GAAG,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAChC,CA0BA;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAQpD;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/D;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACxB,QAAQ,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAC7B,OAAO,CAAC,CAAC,CAAC,CASZ;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CASxB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAGrC;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,GAAG,CAAC,QAAQ,EAAE,gBAAgB,CAAC,GAC5C,MAAM,CAWR"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.28.2",
3
+ "version": "0.29.0",
4
4
  "description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,7 +34,7 @@
34
34
  "@opencode-ai/plugin": "^1.0.134",
35
35
  "gray-matter": "^4.0.3",
36
36
  "ioredis": "^5.4.1",
37
- "swarm-mail": "0.3.4",
37
+ "swarm-mail": "0.4.0",
38
38
  "zod": "4.1.8"
39
39
  },
40
40
  "devDependencies": {
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Tests for Swarm-Aware Compaction Hook
3
+ */
4
+
5
+ import { describe, expect, it, mock } from "bun:test";
6
+ import {
7
+ SWARM_COMPACTION_CONTEXT,
8
+ SWARM_DETECTION_FALLBACK,
9
+ createCompactionHook,
10
+ } from "./compaction-hook";
11
+
12
+ // Mock the dependencies
13
+ mock.module("./hive", () => ({
14
+ getHiveWorkingDirectory: () => "/test/project",
15
+ getHiveAdapter: async () => ({
16
+ queryCells: async () => [],
17
+ }),
18
+ }));
19
+
20
+ mock.module("swarm-mail", () => ({
21
+ checkSwarmHealth: async () => ({
22
+ healthy: true,
23
+ database: "connected",
24
+ stats: {
25
+ events: 0,
26
+ agents: 0,
27
+ messages: 0,
28
+ reservations: 0,
29
+ },
30
+ }),
31
+ }));
32
+
33
+ describe("Compaction Hook", () => {
34
+ describe("SWARM_COMPACTION_CONTEXT", () => {
35
+ it("contains coordinator instructions", () => {
36
+ expect(SWARM_COMPACTION_CONTEXT).toContain("COORDINATOR");
37
+ expect(SWARM_COMPACTION_CONTEXT).toContain("Keep Cooking");
38
+ });
39
+
40
+ it("contains resume instructions", () => {
41
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_status");
42
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarmmail_inbox");
43
+ });
44
+
45
+ it("contains summary format", () => {
46
+ expect(SWARM_COMPACTION_CONTEXT).toContain("Swarm State");
47
+ expect(SWARM_COMPACTION_CONTEXT).toContain("Active:");
48
+ expect(SWARM_COMPACTION_CONTEXT).toContain("Blocked:");
49
+ expect(SWARM_COMPACTION_CONTEXT).toContain("Completed:");
50
+ });
51
+ });
52
+
53
+ describe("SWARM_DETECTION_FALLBACK", () => {
54
+ it("contains detection patterns", () => {
55
+ expect(SWARM_DETECTION_FALLBACK).toContain("swarm_decompose");
56
+ expect(SWARM_DETECTION_FALLBACK).toContain("swarmmail_init");
57
+ expect(SWARM_DETECTION_FALLBACK).toContain("hive_create_epic");
58
+ });
59
+
60
+ it("contains ID patterns", () => {
61
+ expect(SWARM_DETECTION_FALLBACK).toContain("bd-xxx");
62
+ expect(SWARM_DETECTION_FALLBACK).toContain("Agent names");
63
+ });
64
+
65
+ it("contains coordination language", () => {
66
+ expect(SWARM_DETECTION_FALLBACK).toContain("spawn");
67
+ expect(SWARM_DETECTION_FALLBACK).toContain("coordinator");
68
+ expect(SWARM_DETECTION_FALLBACK).toContain("reservation");
69
+ });
70
+ });
71
+
72
+ describe("createCompactionHook", () => {
73
+ it("returns a function", () => {
74
+ const hook = createCompactionHook();
75
+ expect(typeof hook).toBe("function");
76
+ });
77
+
78
+ it("accepts input and output parameters", async () => {
79
+ const hook = createCompactionHook();
80
+ const input = { sessionID: "test-session" };
81
+ const output = { context: [] as string[] };
82
+
83
+ // Should not throw
84
+ await hook(input, output);
85
+ });
86
+
87
+ it("does not inject context when no swarm detected", async () => {
88
+ const hook = createCompactionHook();
89
+ const output = { context: [] as string[] };
90
+
91
+ await hook({ sessionID: "test" }, output);
92
+
93
+ // With mocked empty data, should not inject
94
+ expect(output.context.length).toBe(0);
95
+ });
96
+ });
97
+
98
+ describe("Detection confidence levels", () => {
99
+ it("HIGH confidence triggers full context", async () => {
100
+ // This would need proper mocking of active reservations
101
+ // For now, just verify the context strings exist
102
+ expect(SWARM_COMPACTION_CONTEXT).toContain("SWARM ACTIVE");
103
+ });
104
+
105
+ it("LOW confidence triggers fallback prompt", async () => {
106
+ expect(SWARM_DETECTION_FALLBACK).toContain("Swarm Detection");
107
+ expect(SWARM_DETECTION_FALLBACK).toContain("Check Your Context");
108
+ });
109
+ });
110
+ });
@@ -5,6 +5,12 @@
5
5
  * When context is compacted, this hook injects instructions for the summarizer
6
6
  * to preserve swarm coordination state and enable seamless resumption.
7
7
  *
8
+ * ## Philosophy: Err on the Side of Continuation
9
+ *
10
+ * It's better to inject swarm context unnecessarily than to lose an active swarm.
11
+ * The cost of a false positive (extra context) is low.
12
+ * The cost of a false negative (lost swarm) is high - wasted work, confused agents.
13
+ *
8
14
  * Hook signature (from @opencode-ai/plugin):
9
15
  * ```typescript
10
16
  * "experimental.session.compacting"?: (
@@ -23,6 +29,9 @@
23
29
  * ```
24
30
  */
25
31
 
32
+ import { getHiveAdapter, getHiveWorkingDirectory } from "./hive";
33
+ import { checkSwarmHealth } from "swarm-mail";
34
+
26
35
  // ============================================================================
27
36
  // Compaction Context
28
37
  // ============================================================================
@@ -93,50 +102,187 @@ Extract from session context:
93
102
  **You are not waiting for instructions. You are the coordinator. Coordinate.**
94
103
  `;
95
104
 
105
+ /**
106
+ * Fallback detection prompt - tells the compactor what to look for
107
+ *
108
+ * Used when we can't definitively detect a swarm but want to be safe.
109
+ * The compactor can check the conversation context for these patterns.
110
+ */
111
+ export const SWARM_DETECTION_FALLBACK = `## 🐝 Swarm Detection - Check Your Context
112
+
113
+ **IMPORTANT:** Before summarizing, check if this session involves an active swarm.
114
+
115
+ Look for ANY of these patterns in the conversation:
116
+
117
+ ### Tool Calls (definite swarm sign)
118
+ - \`swarm_decompose\`, \`swarm_spawn_subtask\`, \`swarm_status\`, \`swarm_complete\`
119
+ - \`swarmmail_init\`, \`swarmmail_reserve\`, \`swarmmail_send\`
120
+ - \`hive_create_epic\`, \`hive_start\`, \`hive_close\`
121
+
122
+ ### IDs and Names
123
+ - Cell IDs: \`bd-xxx\`, \`bd-xxx.N\` (subtask format)
124
+ - Agent names: BlueLake, RedMountain, GreenValley, etc.
125
+ - Epic references: "epic", "subtask", "parent"
126
+
127
+ ### Coordination Language
128
+ - "spawn", "worker", "coordinator"
129
+ - "reserve", "reservation", "files"
130
+ - "blocked", "unblock", "dependency"
131
+ - "progress", "complete", "in_progress"
132
+
133
+ ### If You Find Swarm Evidence
134
+
135
+ Include this in your summary:
136
+ 1. Epic ID and title
137
+ 2. Project path
138
+ 3. Subtask status (running/blocked/done/pending)
139
+ 4. Any blockers or issues
140
+ 5. What should happen next
141
+
142
+ **Then tell the resumed session:**
143
+ "This is an active swarm. Check swarm_status and swarmmail_inbox immediately."
144
+ `;
145
+
96
146
  // ============================================================================
97
- // Hook Registration Helper
147
+ // Swarm Detection
98
148
  // ============================================================================
99
149
 
150
+ /**
151
+ * Detection result with confidence level
152
+ */
153
+ interface SwarmDetection {
154
+ detected: boolean;
155
+ confidence: "high" | "medium" | "low" | "none";
156
+ reasons: string[];
157
+ }
158
+
100
159
  /**
101
160
  * Check for swarm sign - evidence a swarm passed through
102
161
  *
103
- * Like deer scat on a trail, we look for traces:
104
- * - In-progress cells (active work)
105
- * - Open cells with parent_id (subtasks of an epic)
106
- * - Unclosed epics
162
+ * Uses multiple signals with different confidence levels:
163
+ * - HIGH: Active reservations, in_progress cells
164
+ * - MEDIUM: Open subtasks, unclosed epics, recent activity
165
+ * - LOW: Any cells exist, swarm-mail initialized
107
166
  *
108
- * Uses the adapter directly to query beads.
167
+ * Philosophy: Err on the side of continuation.
109
168
  */
110
- import { getHiveAdapter, getHiveWorkingDirectory } from "./hive";
169
+ async function detectSwarm(): Promise<SwarmDetection> {
170
+ const reasons: string[] = [];
171
+ let highConfidence = false;
172
+ let mediumConfidence = false;
173
+ let lowConfidence = false;
111
174
 
112
- async function hasSwarmSign(): Promise<boolean> {
113
175
  try {
114
176
  const projectKey = getHiveWorkingDirectory();
115
- const adapter = await getHiveAdapter(projectKey);
116
- const cells = await adapter.queryCells(projectKey, {});
117
-
118
- if (!Array.isArray(cells)) return false;
119
-
120
- // Look for swarm sign:
121
- // 1. Any in_progress cells
122
- // 2. Any open cells with a parent (subtasks)
123
- // 3. Any epics that aren't closed
124
- return cells.some(
125
- (c) =>
126
- c.status === "in_progress" ||
127
- (c.status === "open" && c.parent_id) ||
128
- (c.type === "epic" && c.status !== "closed"),
129
- );
177
+
178
+ // Check 1: Active reservations in swarm-mail (HIGH confidence)
179
+ try {
180
+ const health = await checkSwarmHealth(projectKey);
181
+ if (health.healthy && health.stats) {
182
+ if (health.stats.reservations > 0) {
183
+ highConfidence = true;
184
+ reasons.push(`${health.stats.reservations} active file reservations`);
185
+ }
186
+ if (health.stats.agents > 0) {
187
+ mediumConfidence = true;
188
+ reasons.push(`${health.stats.agents} registered agents`);
189
+ }
190
+ if (health.stats.messages > 0) {
191
+ lowConfidence = true;
192
+ reasons.push(`${health.stats.messages} swarm messages`);
193
+ }
194
+ }
195
+ } catch {
196
+ // Swarm-mail not available, continue with other checks
197
+ }
198
+
199
+ // Check 2: Hive cells (various confidence levels)
200
+ try {
201
+ const adapter = await getHiveAdapter(projectKey);
202
+ const cells = await adapter.queryCells(projectKey, {});
203
+
204
+ if (Array.isArray(cells) && cells.length > 0) {
205
+ // HIGH: Any in_progress cells
206
+ const inProgress = cells.filter((c) => c.status === "in_progress");
207
+ if (inProgress.length > 0) {
208
+ highConfidence = true;
209
+ reasons.push(`${inProgress.length} cells in_progress`);
210
+ }
211
+
212
+ // MEDIUM: Open subtasks (cells with parent_id)
213
+ const subtasks = cells.filter(
214
+ (c) => c.status === "open" && c.parent_id
215
+ );
216
+ if (subtasks.length > 0) {
217
+ mediumConfidence = true;
218
+ reasons.push(`${subtasks.length} open subtasks`);
219
+ }
220
+
221
+ // MEDIUM: Unclosed epics
222
+ const openEpics = cells.filter(
223
+ (c) => c.type === "epic" && c.status !== "closed"
224
+ );
225
+ if (openEpics.length > 0) {
226
+ mediumConfidence = true;
227
+ reasons.push(`${openEpics.length} unclosed epics`);
228
+ }
229
+
230
+ // MEDIUM: Recently updated cells (last hour)
231
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
232
+ const recentCells = cells.filter((c) => c.updated_at > oneHourAgo);
233
+ if (recentCells.length > 0) {
234
+ mediumConfidence = true;
235
+ reasons.push(`${recentCells.length} cells updated in last hour`);
236
+ }
237
+
238
+ // LOW: Any cells exist at all
239
+ if (cells.length > 0) {
240
+ lowConfidence = true;
241
+ reasons.push(`${cells.length} total cells in hive`);
242
+ }
243
+ }
244
+ } catch {
245
+ // Hive not available, continue
246
+ }
130
247
  } catch {
131
- return false;
248
+ // Project detection failed, use fallback
249
+ lowConfidence = true;
250
+ reasons.push("Could not detect project, using fallback");
251
+ }
252
+
253
+ // Determine overall confidence
254
+ let confidence: "high" | "medium" | "low" | "none";
255
+ if (highConfidence) {
256
+ confidence = "high";
257
+ } else if (mediumConfidence) {
258
+ confidence = "medium";
259
+ } else if (lowConfidence) {
260
+ confidence = "low";
261
+ } else {
262
+ confidence = "none";
132
263
  }
264
+
265
+ return {
266
+ detected: confidence !== "none",
267
+ confidence,
268
+ reasons,
269
+ };
133
270
  }
134
271
 
272
+ // ============================================================================
273
+ // Hook Registration
274
+ // ============================================================================
275
+
135
276
  /**
136
277
  * Create the compaction hook for use in plugin registration
137
278
  *
138
- * Only injects swarm context if there's an active swarm (in-progress beads).
139
- * This keeps the coordinator cooking after compaction.
279
+ * Injects swarm context based on detection confidence:
280
+ * - HIGH/MEDIUM: Full swarm context (definitely/probably a swarm)
281
+ * - LOW: Fallback detection prompt (let compactor check context)
282
+ * - NONE: No injection (probably not a swarm)
283
+ *
284
+ * Philosophy: Err on the side of continuation. A false positive costs
285
+ * a bit of context space. A false negative loses the swarm.
140
286
  *
141
287
  * @example
142
288
  * ```typescript
@@ -153,9 +299,17 @@ export function createCompactionHook() {
153
299
  _input: { sessionID: string },
154
300
  output: { context: string[] },
155
301
  ): Promise<void> => {
156
- const hasSign = await hasSwarmSign();
157
- if (hasSign) {
158
- output.context.push(SWARM_COMPACTION_CONTEXT);
302
+ const detection = await detectSwarm();
303
+
304
+ if (detection.confidence === "high" || detection.confidence === "medium") {
305
+ // Definite or probable swarm - inject full context
306
+ const header = `[Swarm detected: ${detection.reasons.join(", ")}]\n\n`;
307
+ output.context.push(header + SWARM_COMPACTION_CONTEXT);
308
+ } else if (detection.confidence === "low") {
309
+ // Possible swarm - inject fallback detection prompt
310
+ const header = `[Possible swarm: ${detection.reasons.join(", ")}]\n\n`;
311
+ output.context.push(header + SWARM_DETECTION_FALLBACK);
159
312
  }
313
+ // confidence === "none" - no injection, probably not a swarm
160
314
  };
161
315
  }
@@ -1572,7 +1572,13 @@ describe("3-Strike Detection", () => {
1572
1572
  "Failed 1",
1573
1573
  storage,
1574
1574
  );
1575
- await new Promise((resolve) => setTimeout(resolve, 100));
1575
+ // Capture the timestamps from first strike
1576
+ const firstStrikeAt = record1.first_strike_at;
1577
+ const firstLastStrikeAt = record1.last_strike_at;
1578
+
1579
+ // Wait to ensure different timestamp
1580
+ await new Promise((resolve) => setTimeout(resolve, 10));
1581
+
1576
1582
  const record2 = await addStrike(
1577
1583
  "test-bead-4",
1578
1584
  "Fix 2",
@@ -1580,8 +1586,10 @@ describe("3-Strike Detection", () => {
1580
1586
  storage,
1581
1587
  );
1582
1588
 
1583
- expect(record2.first_strike_at).toBe(record1.first_strike_at);
1584
- expect(record2.last_strike_at).not.toBe(record1.last_strike_at);
1589
+ // first_strike_at should be preserved from first call
1590
+ expect(record2.first_strike_at).toBe(firstStrikeAt);
1591
+ // last_strike_at should be updated (different from first call's last_strike_at)
1592
+ expect(record2.last_strike_at).not.toBe(firstLastStrikeAt);
1585
1593
  });
1586
1594
  });
1587
1595
 
@@ -67,6 +67,7 @@ import {
67
67
  isToolAvailable,
68
68
  warnMissingTool,
69
69
  } from "./tool-availability";
70
+ import { getHiveAdapter } from "./hive";
70
71
  import { listSkills } from "./skills";
71
72
  import {
72
73
  canUseWorktreeIsolation,
@@ -82,45 +83,34 @@ import {
82
83
  // ============================================================================
83
84
 
84
85
  /**
85
- * Query beads for subtasks of an epic
86
+ * Query beads for subtasks of an epic using HiveAdapter (not bd CLI)
86
87
  */
87
- async function queryEpicSubtasks(epicId: string): Promise<Bead[]> {
88
- // Check if beads is available
89
- const beadsAvailable = await isToolAvailable("beads");
90
- if (!beadsAvailable) {
91
- warnMissingTool("beads");
92
- return []; // Return empty - swarm can still function without status tracking
93
- }
94
-
95
- const result = await Bun.$`bd list --parent ${epicId} --json`
96
- .quiet()
97
- .nothrow();
98
-
99
- if (result.exitCode !== 0) {
100
- // Don't throw - just return empty and log error prominently
101
- console.error(
102
- `[swarm] ERROR: Failed to query subtasks for epic ${epicId}:`,
103
- result.stderr.toString(),
104
- );
105
- return [];
106
- }
107
-
88
+ async function queryEpicSubtasks(projectKey: string, epicId: string): Promise<Bead[]> {
108
89
  try {
109
- const parsed = JSON.parse(result.stdout.toString());
110
- return z.array(BeadSchema).parse(parsed);
90
+ const adapter = await getHiveAdapter(projectKey);
91
+ const cells = await adapter.queryCells(projectKey, { parent_id: epicId });
92
+ // Map Cell (from HiveAdapter) to Bead schema format
93
+ // Cell uses `type` and numeric timestamps, Bead uses `issue_type` and ISO strings
94
+ return cells
95
+ .filter(cell => cell.status !== "tombstone") // Exclude deleted cells
96
+ .map(cell => ({
97
+ id: cell.id,
98
+ title: cell.title,
99
+ description: cell.description || "",
100
+ status: cell.status as "open" | "in_progress" | "blocked" | "closed",
101
+ priority: cell.priority,
102
+ issue_type: cell.type as "bug" | "feature" | "task" | "epic" | "chore",
103
+ created_at: new Date(cell.created_at).toISOString(),
104
+ updated_at: cell.updated_at ? new Date(cell.updated_at).toISOString() : undefined,
105
+ dependencies: [], // Dependencies fetched separately if needed
106
+ metadata: {},
107
+ }));
111
108
  } catch (error) {
112
- if (error instanceof z.ZodError) {
113
- console.error(
114
- `[swarm] ERROR: Invalid bead data for epic ${epicId}:`,
115
- error.message,
116
- );
117
- return [];
118
- }
119
109
  console.error(
120
- `[swarm] ERROR: Failed to parse beads for epic ${epicId}:`,
121
- error,
110
+ `[swarm] ERROR: Failed to query subtasks for epic ${epicId}:`,
111
+ error instanceof Error ? error.message : String(error),
122
112
  );
123
- throw error;
113
+ return [];
124
114
  }
125
115
  }
126
116
 
@@ -758,7 +748,7 @@ export const swarm_status = tool({
758
748
  },
759
749
  async execute(args) {
760
750
  // Query subtasks from beads
761
- const subtasks = await queryEpicSubtasks(args.epic_id);
751
+ const subtasks = await queryEpicSubtasks(args.project_key, args.epic_id);
762
752
 
763
753
  // Count statuses
764
754
  const statusCounts = {
@@ -878,12 +868,16 @@ export const swarm_progress = tool({
878
868
  // Validate
879
869
  const validated = AgentProgressSchema.parse(progress);
880
870
 
881
- // Update cell status if needed
871
+ // Update cell status if needed (using HiveAdapter, not bd CLI)
882
872
  if (args.status === "blocked" || args.status === "in_progress") {
883
- const beadStatus = args.status === "blocked" ? "blocked" : "in_progress";
884
- await Bun.$`bd update ${args.bead_id} --status ${beadStatus} --json`
885
- .quiet()
886
- .nothrow();
873
+ try {
874
+ const adapter = await getHiveAdapter(args.project_key);
875
+ const newStatus = args.status === "blocked" ? "blocked" : "in_progress";
876
+ await adapter.changeCellStatus(args.project_key, args.bead_id, newStatus);
877
+ } catch (error) {
878
+ // Non-fatal - log but continue
879
+ console.error(`[swarm] Failed to update cell status: ${error instanceof Error ? error.message : String(error)}`);
880
+ }
887
881
  }
888
882
 
889
883
  // Extract epic ID from bead ID (e.g., bd-abc123.1 -> bd-abc123)
@@ -1152,11 +1146,35 @@ Or use skip_review=true to bypass (not recommended for production work).`,
1152
1146
  }
1153
1147
 
1154
1148
  try {
1155
- // Verify agent is registered in swarm-mail
1156
- // This catches agents who skipped swarmmail_init
1149
+ // Validate bead_id exists and is not already closed (EARLY validation)
1157
1150
  const projectKey = args.project_key
1158
1151
  .replace(/\//g, "-")
1159
1152
  .replace(/\\/g, "-");
1153
+
1154
+ // Use HiveAdapter for validation (not bd CLI)
1155
+ const adapter = await getHiveAdapter(args.project_key);
1156
+
1157
+ // 1. Check if bead exists
1158
+ const cell = await adapter.getCell(projectKey, args.bead_id);
1159
+ if (!cell) {
1160
+ return JSON.stringify({
1161
+ success: false,
1162
+ error: `Bead not found: ${args.bead_id}`,
1163
+ hint: "Check the bead ID is correct. Use hive_query to list open cells.",
1164
+ });
1165
+ }
1166
+
1167
+ // 2. Check if bead is already closed
1168
+ if (cell.status === "closed") {
1169
+ return JSON.stringify({
1170
+ success: false,
1171
+ error: `Bead already closed: ${args.bead_id}`,
1172
+ hint: "This bead was already completed. No action needed.",
1173
+ });
1174
+ }
1175
+
1176
+ // Verify agent is registered in swarm-mail
1177
+ // This catches agents who skipped swarmmail_init
1160
1178
  let agentRegistered = false;
1161
1179
  let registrationWarning = "";
1162
1180
 
@@ -1303,49 +1321,27 @@ Continuing with completion, but this should be fixed for future subtasks.`;
1303
1321
  }
1304
1322
  }
1305
1323
 
1306
- // Close the cell - use project_key as working directory to find correct .beads/
1307
- // This fixes the issue where cell ID prefix (e.g., "pdf-library-g84.2") doesn't match CWD
1308
- const closeResult =
1309
- await Bun.$`bd close ${args.bead_id} --reason ${args.summary} --json`
1310
- .cwd(args.project_key)
1311
- .quiet()
1312
- .nothrow();
1313
-
1314
- if (closeResult.exitCode !== 0) {
1315
- const stderrOutput = closeResult.stderr.toString().trim();
1316
- const stdoutOutput = closeResult.stdout.toString().trim();
1317
-
1318
- // Check for common error patterns and provide better guidance
1319
- const isNoDatabaseError = stderrOutput.includes("no beads database found");
1320
- const isNotFoundError = stderrOutput.includes("not found") || stderrOutput.includes("does not exist");
1321
-
1324
+ // Close the cell using HiveAdapter (not bd CLI)
1325
+ try {
1326
+ await adapter.closeCell(args.project_key, args.bead_id, args.summary);
1327
+ } catch (closeError) {
1328
+ const errorMessage = closeError instanceof Error ? closeError.message : String(closeError);
1322
1329
  return JSON.stringify(
1323
1330
  {
1324
1331
  success: false,
1325
1332
  error: "Failed to close cell",
1326
- failed_step: "bd close",
1327
- details: stderrOutput || stdoutOutput || "Unknown error from bd close command",
1333
+ failed_step: "closeCell",
1334
+ details: errorMessage,
1328
1335
  bead_id: args.bead_id,
1329
1336
  project_key: args.project_key,
1330
1337
  recovery: {
1331
- steps: isNoDatabaseError
1332
- ? [
1333
- `1. Verify project_key is correct: "${args.project_key}"`,
1334
- `2. Check .beads/ exists in that directory`,
1335
- `3. Cell ID prefix "${args.bead_id.split("-")[0]}" should match project`,
1336
- `4. Try: hive_close(id="${args.bead_id}", reason="...")`,
1337
- ]
1338
- : [
1339
- `1. Check cell exists: bd show ${args.bead_id}`,
1340
- `2. Check cell status (might already be closed): hive_query()`,
1341
- `3. If cell is blocked, unblock first: hive_update(id="${args.bead_id}", status="in_progress")`,
1342
- `4. Try closing directly: hive_close(id="${args.bead_id}", reason="...")`,
1343
- ],
1344
- hint: isNoDatabaseError
1345
- ? `The project_key "${args.project_key}" doesn't have a .beads/ directory. Make sure you're using the correct project path.`
1346
- : isNotFoundError
1347
- ? `Cell "${args.bead_id}" not found. It may have been closed already or the ID is incorrect.`
1348
- : "If cell is in 'blocked' status, you must change it to 'in_progress' or 'open' before closing.",
1338
+ steps: [
1339
+ `1. Check cell exists: hive_query()`,
1340
+ `2. Check cell status (might already be closed)`,
1341
+ `3. If cell is blocked, unblock first: hive_update(id="${args.bead_id}", status="in_progress")`,
1342
+ `4. Try closing directly: hive_close(id="${args.bead_id}", reason="...")`,
1343
+ ],
1344
+ hint: "Cell may already be closed, or the ID is incorrect.",
1349
1345
  },
1350
1346
  },
1351
1347
  null,
@@ -1645,12 +1641,13 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1645
1641
  return JSON.stringify(
1646
1642
  {
1647
1643
  success: false,
1648
- error: errorMessage,
1644
+ error: `swarm_complete failed: ${errorMessage}`,
1649
1645
  failed_step: failedStep,
1650
1646
  bead_id: args.bead_id,
1651
1647
  agent_name: args.agent_name,
1652
1648
  coordinator_notified: notificationSent,
1653
1649
  stack_trace: errorStack?.slice(0, 500),
1650
+ hint: "Check the error message above. Common issues: bead not found, session not initialized.",
1654
1651
  context: {
1655
1652
  summary: args.summary,
1656
1653
  files_touched: args.files_touched || [],