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.
- package/.changeset/swarm-insights-data-layer.md +63 -0
- package/.hive/issues.jsonl +19 -1
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +54 -0
- package/README.md +147 -0
- package/bin/swarm.ts +4 -4
- package/dist/hive.d.ts +12 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +86 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +236 -42
- package/dist/plugin.js +236 -42
- package/dist/schemas/cell.d.ts +2 -0
- package/dist/schemas/cell.d.ts.map +1 -1
- package/dist/swarm-insights.d.ts +155 -0
- package/dist/swarm-insights.d.ts.map +1 -0
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/examples/plugin-wrapper-template.ts +30 -0
- package/package.json +2 -2
- package/src/hive.integration.test.ts +105 -0
- package/src/hive.ts +8 -0
- package/src/index.ts +1 -0
- package/src/schemas/cell.ts +1 -0
- package/src/swarm-insights.test.ts +214 -0
- package/src/swarm-insights.ts +459 -0
- package/src/swarm-prompts.test.ts +165 -0
- package/src/swarm-prompts.ts +74 -56
|
@@ -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
package/src/schemas/cell.ts
CHANGED
|
@@ -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
|
+
});
|