opencode-swarm-plugin 0.42.5 → 0.42.7

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.
@@ -17,6 +17,7 @@ import {
17
17
  hive_close,
18
18
  hive_start,
19
19
  hive_ready,
20
+ hive_cells,
20
21
  hive_link_thread,
21
22
  hive_sync,
22
23
  HiveError,
@@ -2133,4 +2134,108 @@ describe("beads integration", () => {
2133
2134
  }
2134
2135
  });
2135
2136
  });
2137
+
2138
+ describe("parent_id filter", () => {
2139
+ it("hive_query filters by parent_id to get epic children", async () => {
2140
+ const { rmSync } = await import("node:fs");
2141
+ const tempProject = join(tmpdir(), `hive-parent-filter-${Date.now()}`);
2142
+ const originalDir = getHiveWorkingDirectory();
2143
+ setHiveWorkingDirectory(tempProject);
2144
+
2145
+ try {
2146
+ // Create an epic
2147
+ const epicResponse = await hive_create.execute(
2148
+ { title: "Epic Task", type: "epic", priority: 1 },
2149
+ mockContext
2150
+ );
2151
+ const epic = parseResponse<Cell>(epicResponse);
2152
+
2153
+ // Create children of the epic
2154
+ const child1Response = await hive_create.execute(
2155
+ { title: "Subtask 1", type: "task", parent_id: epic.id },
2156
+ mockContext
2157
+ );
2158
+ const child1 = parseResponse<Cell>(child1Response);
2159
+
2160
+ const child2Response = await hive_create.execute(
2161
+ { title: "Subtask 2", type: "task", parent_id: epic.id },
2162
+ mockContext
2163
+ );
2164
+ const child2 = parseResponse<Cell>(child2Response);
2165
+
2166
+ // Create unrelated cell
2167
+ await hive_create.execute(
2168
+ { title: "Unrelated Task", type: "task" },
2169
+ mockContext
2170
+ );
2171
+
2172
+ // Query by parent_id
2173
+ const queryResponse = await hive_query.execute(
2174
+ { parent_id: epic.id },
2175
+ mockContext
2176
+ );
2177
+ const children = parseResponse<Cell[]>(queryResponse);
2178
+
2179
+ // Should only return the 2 children
2180
+ expect(children).toHaveLength(2);
2181
+ expect(children.map(c => c.id)).toContain(child1.id);
2182
+ expect(children.map(c => c.id)).toContain(child2.id);
2183
+ expect(children.every(c => c.parent_id === epic.id)).toBe(true);
2184
+ } finally {
2185
+ setHiveWorkingDirectory(originalDir);
2186
+ rmSync(tempProject, { recursive: true, force: true });
2187
+ }
2188
+ });
2189
+
2190
+ it("hive_cells filters by parent_id to get epic children", async () => {
2191
+ const { rmSync } = await import("node:fs");
2192
+ const tempProject = join(tmpdir(), `hive-cells-parent-filter-${Date.now()}`);
2193
+ const originalDir = getHiveWorkingDirectory();
2194
+ setHiveWorkingDirectory(tempProject);
2195
+
2196
+ try {
2197
+ // Create an epic
2198
+ const epicResponse = await hive_create.execute(
2199
+ { title: "Epic with Children", type: "epic", priority: 1 },
2200
+ mockContext
2201
+ );
2202
+ const epic = parseResponse<Cell>(epicResponse);
2203
+
2204
+ // Create children
2205
+ const child1Response = await hive_create.execute(
2206
+ { title: "Child A", type: "task", parent_id: epic.id },
2207
+ mockContext
2208
+ );
2209
+ const child1 = parseResponse<Cell>(child1Response);
2210
+
2211
+ const child2Response = await hive_create.execute(
2212
+ { title: "Child B", type: "bug", parent_id: epic.id },
2213
+ mockContext
2214
+ );
2215
+ const child2 = parseResponse<Cell>(child2Response);
2216
+
2217
+ // Create unrelated cells
2218
+ await hive_create.execute(
2219
+ { title: "Orphan Task", type: "task" },
2220
+ mockContext
2221
+ );
2222
+
2223
+ // Query using hive_cells with parent_id
2224
+ const cellsResponse = await hive_cells.execute(
2225
+ { parent_id: epic.id },
2226
+ mockContext
2227
+ );
2228
+ const cells = parseResponse<Cell[]>(cellsResponse);
2229
+
2230
+ // Should only return the 2 children
2231
+ expect(cells).toHaveLength(2);
2232
+ expect(cells.map(c => c.id)).toContain(child1.id);
2233
+ expect(cells.map(c => c.id)).toContain(child2.id);
2234
+ expect(cells.every(c => c.parent_id === epic.id)).toBe(true);
2235
+ } finally {
2236
+ setHiveWorkingDirectory(originalDir);
2237
+ rmSync(tempProject, { recursive: true, force: true });
2238
+ }
2239
+ });
2240
+ });
2136
2241
  });
package/src/hive.ts CHANGED
@@ -873,6 +873,10 @@ export const hive_query = tool({
873
873
  .boolean()
874
874
  .optional()
875
875
  .describe("Only show unblocked cells"),
876
+ parent_id: tool.schema
877
+ .string()
878
+ .optional()
879
+ .describe("Filter by parent epic ID (returns children of an epic)"),
876
880
  limit: tool.schema
877
881
  .number()
878
882
  .optional()
@@ -893,6 +897,7 @@ export const hive_query = tool({
893
897
  cells = await adapter.queryCells(projectKey, {
894
898
  status: validated.status,
895
899
  type: validated.type,
900
+ parent_id: validated.parent_id,
896
901
  limit: validated.limit || 20,
897
902
  });
898
903
  }
@@ -1139,6 +1144,7 @@ USE THIS TOOL TO:
1139
1144
  - Find cells by type: hive_cells({ type: "bug" })
1140
1145
  - Get a specific cell by partial ID: hive_cells({ id: "mjkmd" })
1141
1146
  - Get the next ready (unblocked) cell: hive_cells({ ready: true })
1147
+ - Get children of an epic: hive_cells({ parent_id: "epic-id" })
1142
1148
  - Combine filters: hive_cells({ status: "open", type: "task" })
1143
1149
 
1144
1150
  RETURNS: Array of cells with id, title, status, priority, type, parent_id, created_at, updated_at
@@ -1152,6 +1158,7 @@ PREFER THIS OVER hive_query when you need to:
1152
1158
  id: tool.schema.string().optional().describe("Partial or full cell ID to look up"),
1153
1159
  status: tool.schema.enum(["open", "in_progress", "blocked", "closed"]).optional().describe("Filter by status"),
1154
1160
  type: tool.schema.enum(["task", "bug", "feature", "epic", "chore"]).optional().describe("Filter by type"),
1161
+ parent_id: tool.schema.string().optional().describe("Filter by parent epic ID (returns children of an epic)"),
1155
1162
  ready: tool.schema.boolean().optional().describe("If true, return only the next unblocked cell"),
1156
1163
  limit: tool.schema.number().optional().describe("Max cells to return (default 20)"),
1157
1164
  },
@@ -1185,6 +1192,7 @@ PREFER THIS OVER hive_query when you need to:
1185
1192
  const cells = await adapter.queryCells(projectKey, {
1186
1193
  status: args.status,
1187
1194
  type: args.type,
1195
+ parent_id: args.parent_id,
1188
1196
  limit: args.limit || 20,
1189
1197
  });
1190
1198
 
package/src/index.ts CHANGED
@@ -515,6 +515,7 @@ export const allTools = {
515
515
  ...skillsTools,
516
516
  ...mandateTools,
517
517
  ...memoryTools,
518
+ ...observabilityTools,
518
519
  } as const;
519
520
 
520
521
  /**
@@ -122,6 +122,7 @@ export const CellQueryArgsSchema = z.object({
122
122
  status: CellStatusSchema.optional(),
123
123
  type: CellTypeSchema.optional(),
124
124
  ready: z.boolean().optional(),
125
+ parent_id: z.string().optional(),
125
126
  limit: z.number().int().positive().default(20),
126
127
  });
127
128
  export type CellQueryArgs = z.infer<typeof CellQueryArgsSchema>;
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Swarm Insights Data Layer Tests
3
+ *
4
+ * TDD: Red → Green → Refactor
5
+ */
6
+
7
+ import { describe, expect, test, beforeAll, afterAll } from "bun:test";
8
+ import {
9
+ getStrategyInsights,
10
+ getFileInsights,
11
+ getPatternInsights,
12
+ formatInsightsForPrompt,
13
+ type StrategyInsight,
14
+ type FileInsight,
15
+ type PatternInsight,
16
+ } from "./swarm-insights";
17
+ import { createInMemorySwarmMail, type SwarmMailAdapter } from "swarm-mail";
18
+
19
+ describe("swarm-insights data layer", () => {
20
+ let swarmMail: SwarmMailAdapter;
21
+
22
+ beforeAll(async () => {
23
+ swarmMail = await createInMemorySwarmMail("test-insights");
24
+ });
25
+
26
+ afterAll(async () => {
27
+ await swarmMail.close();
28
+ });
29
+
30
+ describe("getStrategyInsights", () => {
31
+ test("returns empty array when no data", async () => {
32
+ const insights = await getStrategyInsights(swarmMail, "test-task");
33
+ expect(insights).toEqual([]);
34
+ });
35
+
36
+ test("returns strategy success rates from outcomes", async () => {
37
+ // Seed some outcome events (id is auto-increment, timestamp is integer)
38
+ const db = await swarmMail.getDatabase();
39
+ const now = Date.now();
40
+ await db.query(
41
+ `INSERT INTO events (type, project_key, timestamp, data) VALUES
42
+ ('subtask_outcome', 'test', ?, ?),
43
+ ('subtask_outcome', 'test', ?, ?),
44
+ ('subtask_outcome', 'test', ?, ?)`,
45
+ [
46
+ now,
47
+ JSON.stringify({ strategy: "file-based", success: "true" }),
48
+ now,
49
+ JSON.stringify({ strategy: "file-based", success: "true" }),
50
+ now,
51
+ JSON.stringify({ strategy: "file-based", success: "false" }),
52
+ ],
53
+ );
54
+
55
+ const insights = await getStrategyInsights(swarmMail, "test-task");
56
+
57
+ expect(insights.length).toBeGreaterThan(0);
58
+ const fileBased = insights.find((i) => i.strategy === "file-based");
59
+ expect(fileBased).toBeDefined();
60
+ expect(fileBased?.successRate).toBeCloseTo(66.67, 0);
61
+ expect(fileBased?.totalAttempts).toBe(3);
62
+ });
63
+
64
+ test("includes recommendation based on success rate", async () => {
65
+ const insights = await getStrategyInsights(swarmMail, "test-task");
66
+ const fileBased = insights.find((i) => i.strategy === "file-based");
67
+
68
+ expect(fileBased?.recommendation).toBeDefined();
69
+ expect(typeof fileBased?.recommendation).toBe("string");
70
+ });
71
+ });
72
+
73
+ describe("getFileInsights", () => {
74
+ test("returns empty array for unknown files", async () => {
75
+ const insights = await getFileInsights(swarmMail, [
76
+ "src/unknown-file.ts",
77
+ ]);
78
+ expect(insights).toEqual([]);
79
+ });
80
+
81
+ test("returns past issues for known files", async () => {
82
+ // Seed some file-related events (id is auto-increment, timestamp is integer)
83
+ const db = await swarmMail.getDatabase();
84
+ const now = Date.now();
85
+ await db.query(
86
+ `INSERT INTO events (type, project_key, timestamp, data) VALUES
87
+ ('subtask_outcome', 'test', ?, ?)`,
88
+ [
89
+ now,
90
+ JSON.stringify({
91
+ files_touched: ["src/auth.ts"],
92
+ success: "false",
93
+ error_count: 2,
94
+ }),
95
+ ],
96
+ );
97
+
98
+ const insights = await getFileInsights(swarmMail, ["src/auth.ts"]);
99
+
100
+ expect(insights.length).toBeGreaterThan(0);
101
+ const authInsight = insights.find((i) => i.file === "src/auth.ts");
102
+ expect(authInsight).toBeDefined();
103
+ expect(authInsight?.failureCount).toBeGreaterThan(0);
104
+ });
105
+
106
+ test("includes gotchas from semantic memory", async () => {
107
+ // This would query semantic memory for file-specific learnings
108
+ const insights = await getFileInsights(swarmMail, ["src/auth.ts"]);
109
+
110
+ // Even if no gotchas, the structure should be correct
111
+ const authInsight = insights.find((i) => i.file === "src/auth.ts");
112
+ expect(authInsight?.gotchas).toBeDefined();
113
+ expect(Array.isArray(authInsight?.gotchas)).toBe(true);
114
+ });
115
+ });
116
+
117
+ describe("getPatternInsights", () => {
118
+ test("returns common failure patterns", async () => {
119
+ const insights = await getPatternInsights(swarmMail);
120
+
121
+ expect(Array.isArray(insights)).toBe(true);
122
+ // Structure check
123
+ if (insights.length > 0) {
124
+ expect(insights[0]).toHaveProperty("pattern");
125
+ expect(insights[0]).toHaveProperty("frequency");
126
+ expect(insights[0]).toHaveProperty("recommendation");
127
+ }
128
+ });
129
+
130
+ test("includes anti-patterns from learning system", async () => {
131
+ const insights = await getPatternInsights(swarmMail);
132
+
133
+ // Should include anti-patterns if any exist
134
+ expect(Array.isArray(insights)).toBe(true);
135
+ });
136
+ });
137
+
138
+ describe("formatInsightsForPrompt", () => {
139
+ test("formats strategy insights concisely", () => {
140
+ const strategies: StrategyInsight[] = [
141
+ {
142
+ strategy: "file-based",
143
+ successRate: 85,
144
+ totalAttempts: 20,
145
+ recommendation: "Preferred for this project",
146
+ },
147
+ {
148
+ strategy: "feature-based",
149
+ successRate: 60,
150
+ totalAttempts: 10,
151
+ recommendation: "Use with caution",
152
+ },
153
+ ];
154
+
155
+ const formatted = formatInsightsForPrompt({ strategies });
156
+
157
+ expect(formatted).toContain("file-based");
158
+ expect(formatted).toContain("85%");
159
+ expect(formatted.length).toBeLessThan(500); // Context-efficient
160
+ });
161
+
162
+ test("formats file insights concisely", () => {
163
+ const files: FileInsight[] = [
164
+ {
165
+ file: "src/auth.ts",
166
+ failureCount: 3,
167
+ lastFailure: "2025-12-25",
168
+ gotchas: ["Watch for race conditions in token refresh"],
169
+ },
170
+ ];
171
+
172
+ const formatted = formatInsightsForPrompt({ files });
173
+
174
+ expect(formatted).toContain("src/auth.ts");
175
+ expect(formatted).toContain("race conditions");
176
+ expect(formatted.length).toBeLessThan(300); // Per-file budget
177
+ });
178
+
179
+ test("formats pattern insights concisely", () => {
180
+ const patterns: PatternInsight[] = [
181
+ {
182
+ pattern: "Missing error handling",
183
+ frequency: 5,
184
+ recommendation: "Add try/catch around async operations",
185
+ },
186
+ ];
187
+
188
+ const formatted = formatInsightsForPrompt({ patterns });
189
+
190
+ expect(formatted).toContain("Missing error handling");
191
+ expect(formatted).toContain("try/catch");
192
+ });
193
+
194
+ test("respects token budget", () => {
195
+ // Create many insights
196
+ const strategies: StrategyInsight[] = Array.from({ length: 10 }, (_, i) => ({
197
+ strategy: `strategy-${i}`,
198
+ successRate: 50 + i * 5,
199
+ totalAttempts: 10,
200
+ recommendation: `Recommendation for strategy ${i}`,
201
+ }));
202
+
203
+ const formatted = formatInsightsForPrompt({ strategies }, { maxTokens: 200 });
204
+
205
+ // Should truncate to fit budget
206
+ expect(formatted.length).toBeLessThan(1000); // ~200 tokens ≈ 800 chars
207
+ });
208
+
209
+ test("returns empty string when no insights", () => {
210
+ const formatted = formatInsightsForPrompt({});
211
+ expect(formatted).toBe("");
212
+ });
213
+ });
214
+ });