opencode-swarm-plugin 0.28.2 → 0.30.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.
@@ -73,7 +73,11 @@ async function execTool(
73
73
  );
74
74
  } else if (!result.success && result.error) {
75
75
  // Tool returned an error in JSON format
76
- reject(new Error(result.error.message || "Tool execution failed"));
76
+ // Handle both string errors and object errors with .message
77
+ const errorMsg = typeof result.error === "string"
78
+ ? result.error
79
+ : (result.error.message || "Tool execution failed");
80
+ reject(new Error(errorMsg));
77
81
  } else {
78
82
  resolve(stdout);
79
83
  }
@@ -89,11 +93,11 @@ async function execTool(
89
93
  try {
90
94
  const result = JSON.parse(stdout);
91
95
  if (!result.success && result.error) {
92
- reject(
93
- new Error(
94
- result.error.message || `Tool failed with code ${code}`,
95
- ),
96
- );
96
+ // Handle both string errors and object errors with .message
97
+ const errorMsg = typeof result.error === "string"
98
+ ? result.error
99
+ : (result.error.message || `Tool failed with code ${code}`);
100
+ reject(new Error(errorMsg));
97
101
  } else {
98
102
  reject(
99
103
  new Error(stderr || stdout || `Tool failed with code ${code}`),
@@ -883,15 +887,33 @@ const skills_execute = tool({
883
887
  // Compaction Hook - Swarm Recovery Context
884
888
  // =============================================================================
885
889
 
890
+ /**
891
+ * Detection result with confidence level
892
+ */
893
+ interface SwarmDetection {
894
+ detected: boolean;
895
+ confidence: "high" | "medium" | "low" | "none";
896
+ reasons: string[];
897
+ }
898
+
886
899
  /**
887
900
  * Check for swarm sign - evidence a swarm passed through
888
901
  *
889
- * Like deer scat on a trail, we look for traces:
890
- * - In-progress beads (active work)
891
- * - Open beads with parent_id (subtasks of an epic)
892
- * - Unclosed epics
902
+ * Uses multiple signals with different confidence levels:
903
+ * - HIGH: in_progress cells (active work)
904
+ * - MEDIUM: Open subtasks, unclosed epics, recently updated cells
905
+ * - LOW: Any cells exist
906
+ *
907
+ * Philosophy: Err on the side of continuation.
908
+ * False positive = extra context (low cost)
909
+ * False negative = lost swarm (high cost)
893
910
  */
894
- async function hasSwarmSign(): Promise<boolean> {
911
+ async function detectSwarm(): Promise<SwarmDetection> {
912
+ const reasons: string[] = [];
913
+ let highConfidence = false;
914
+ let mediumConfidence = false;
915
+ let lowConfidence = false;
916
+
895
917
  try {
896
918
  const result = await new Promise<{ exitCode: number; stdout: string }>(
897
919
  (resolve) => {
@@ -909,24 +931,82 @@ async function hasSwarmSign(): Promise<boolean> {
909
931
  },
910
932
  );
911
933
 
912
- if (result.exitCode !== 0) return false;
934
+ if (result.exitCode !== 0) {
935
+ return { detected: false, confidence: "none", reasons: ["hive_query failed"] };
936
+ }
913
937
 
914
- const beads = JSON.parse(result.stdout);
915
- if (!Array.isArray(beads)) return false;
938
+ const cells = JSON.parse(result.stdout);
939
+ if (!Array.isArray(cells) || cells.length === 0) {
940
+ return { detected: false, confidence: "none", reasons: ["no cells found"] };
941
+ }
916
942
 
917
- // Look for swarm sign:
918
- // 1. Any in_progress beads
919
- // 2. Any open beads with a parent (subtasks)
920
- // 3. Any epics that aren't closed
921
- return beads.some(
922
- (b: { status: string; parent_id?: string; type?: string }) =>
923
- b.status === "in_progress" ||
924
- (b.status === "open" && b.parent_id) ||
925
- (b.type === "epic" && b.status !== "closed"),
943
+ // HIGH: Any in_progress cells
944
+ const inProgress = cells.filter(
945
+ (c: { status: string }) => c.status === "in_progress"
946
+ );
947
+ if (inProgress.length > 0) {
948
+ highConfidence = true;
949
+ reasons.push(`${inProgress.length} cells in_progress`);
950
+ }
951
+
952
+ // MEDIUM: Open subtasks (cells with parent_id)
953
+ const subtasks = cells.filter(
954
+ (c: { status: string; parent_id?: string }) =>
955
+ c.status === "open" && c.parent_id
956
+ );
957
+ if (subtasks.length > 0) {
958
+ mediumConfidence = true;
959
+ reasons.push(`${subtasks.length} open subtasks`);
960
+ }
961
+
962
+ // MEDIUM: Unclosed epics
963
+ const openEpics = cells.filter(
964
+ (c: { status: string; type?: string }) =>
965
+ c.type === "epic" && c.status !== "closed"
966
+ );
967
+ if (openEpics.length > 0) {
968
+ mediumConfidence = true;
969
+ reasons.push(`${openEpics.length} unclosed epics`);
970
+ }
971
+
972
+ // MEDIUM: Recently updated cells (last hour)
973
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
974
+ const recentCells = cells.filter(
975
+ (c: { updated_at?: number }) => c.updated_at && c.updated_at > oneHourAgo
926
976
  );
977
+ if (recentCells.length > 0) {
978
+ mediumConfidence = true;
979
+ reasons.push(`${recentCells.length} cells updated in last hour`);
980
+ }
981
+
982
+ // LOW: Any cells exist at all
983
+ if (cells.length > 0) {
984
+ lowConfidence = true;
985
+ reasons.push(`${cells.length} total cells in hive`);
986
+ }
927
987
  } catch {
928
- return false;
988
+ // Detection failed, use fallback
989
+ lowConfidence = true;
990
+ reasons.push("Detection error, using fallback");
929
991
  }
992
+
993
+ // Determine overall confidence
994
+ let confidence: "high" | "medium" | "low" | "none";
995
+ if (highConfidence) {
996
+ confidence = "high";
997
+ } else if (mediumConfidence) {
998
+ confidence = "medium";
999
+ } else if (lowConfidence) {
1000
+ confidence = "low";
1001
+ } else {
1002
+ confidence = "none";
1003
+ }
1004
+
1005
+ return {
1006
+ detected: confidence !== "none",
1007
+ confidence,
1008
+ reasons,
1009
+ };
930
1010
  }
931
1011
 
932
1012
  /**
@@ -992,6 +1072,47 @@ Extract from session context:
992
1072
  **You are not waiting for instructions. You are the coordinator. Coordinate.**
993
1073
  `;
994
1074
 
1075
+ /**
1076
+ * Fallback detection prompt - tells the compactor what to look for
1077
+ *
1078
+ * Used when we can't definitively detect a swarm but want to be safe.
1079
+ * The compactor can check the conversation context for these patterns.
1080
+ */
1081
+ const SWARM_DETECTION_FALLBACK = `## 🐝 Swarm Detection - Check Your Context
1082
+
1083
+ **IMPORTANT:** Before summarizing, check if this session involves an active swarm.
1084
+
1085
+ Look for ANY of these patterns in the conversation:
1086
+
1087
+ ### Tool Calls (definite swarm sign)
1088
+ - \`swarm_decompose\`, \`swarm_spawn_subtask\`, \`swarm_status\`, \`swarm_complete\`
1089
+ - \`swarmmail_init\`, \`swarmmail_reserve\`, \`swarmmail_send\`
1090
+ - \`hive_create_epic\`, \`hive_start\`, \`hive_close\`
1091
+
1092
+ ### IDs and Names
1093
+ - Cell IDs: \`bd-xxx\`, \`bd-xxx.N\` (subtask format)
1094
+ - Agent names: BlueLake, RedMountain, GreenValley, etc.
1095
+ - Epic references: "epic", "subtask", "parent"
1096
+
1097
+ ### Coordination Language
1098
+ - "spawn", "worker", "coordinator"
1099
+ - "reserve", "reservation", "files"
1100
+ - "blocked", "unblock", "dependency"
1101
+ - "progress", "complete", "in_progress"
1102
+
1103
+ ### If You Find Swarm Evidence
1104
+
1105
+ Include this in your summary:
1106
+ 1. Epic ID and title
1107
+ 2. Project path
1108
+ 3. Subtask status (running/blocked/done/pending)
1109
+ 4. Any blockers or issues
1110
+ 5. What should happen next
1111
+
1112
+ **Then tell the resumed session:**
1113
+ "This is an active swarm. Check swarm_status and swarmmail_inbox immediately."
1114
+ `;
1115
+
995
1116
  // Extended hooks type to include experimental compaction hook
996
1117
  type ExtendedHooks = Hooks & {
997
1118
  "experimental.session.compacting"?: (
@@ -1065,15 +1186,23 @@ export const SwarmPlugin: Plugin = async (
1065
1186
  skills_execute,
1066
1187
  },
1067
1188
 
1068
- // Swarm-aware compaction hook - only fires if there's an active swarm
1189
+ // Swarm-aware compaction hook - injects context based on detection confidence
1069
1190
  "experimental.session.compacting": async (
1070
1191
  _input: { sessionID: string },
1071
1192
  output: { context: string[] },
1072
1193
  ) => {
1073
- const hasSign = await hasSwarmSign();
1074
- if (hasSign) {
1075
- output.context.push(SWARM_COMPACTION_CONTEXT);
1194
+ const detection = await detectSwarm();
1195
+
1196
+ if (detection.confidence === "high" || detection.confidence === "medium") {
1197
+ // Definite or probable swarm - inject full context
1198
+ const header = `[Swarm detected: ${detection.reasons.join(", ")}]\n\n`;
1199
+ output.context.push(header + SWARM_COMPACTION_CONTEXT);
1200
+ } else if (detection.confidence === "low") {
1201
+ // Possible swarm - inject fallback detection prompt
1202
+ const header = `[Possible swarm: ${detection.reasons.join(", ")}]\n\n`;
1203
+ output.context.push(header + SWARM_DETECTION_FALLBACK);
1076
1204
  }
1205
+ // confidence === "none" - no injection, probably not a swarm
1077
1206
  },
1078
1207
  };
1079
1208
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.28.2",
3
+ "version": "0.30.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
  }
package/src/index.ts CHANGED
@@ -249,11 +249,9 @@ export const SwarmPlugin: Plugin = async (
249
249
  await releaseReservations();
250
250
  }
251
251
 
252
- // Auto-sync hive after closing (supports both hive_close and legacy hive_close)
253
- if (toolName === "hive_close" || toolName === "hive_close") {
254
- // Trigger async sync without blocking - fire and forget
255
- void $`bd sync`.quiet().nothrow();
256
- }
252
+ // Note: hive_sync should be called explicitly at session end
253
+ // Auto-sync was removed because bd CLI is deprecated
254
+ // The hive_sync tool handles flushing to JSONL and git commit/push
257
255
  },
258
256
  };
259
257
  };
@@ -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