opencode-swarm-plugin 0.36.1 → 0.38.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.
package/dist/swarm.d.ts CHANGED
@@ -488,9 +488,24 @@ export declare const swarmTools: {
488
488
  description: string;
489
489
  args: {
490
490
  response: import("zod").ZodString;
491
+ project_path: import("zod").ZodOptional<import("zod").ZodString>;
492
+ task: import("zod").ZodOptional<import("zod").ZodString>;
493
+ context: import("zod").ZodOptional<import("zod").ZodString>;
494
+ strategy: import("zod").ZodOptional<import("zod").ZodEnum<{
495
+ "file-based": "file-based";
496
+ "feature-based": "feature-based";
497
+ "risk-based": "risk-based";
498
+ auto: "auto";
499
+ }>>;
500
+ epic_id: import("zod").ZodOptional<import("zod").ZodString>;
491
501
  };
492
502
  execute(args: {
493
503
  response: string;
504
+ project_path?: string | undefined;
505
+ task?: string | undefined;
506
+ context?: string | undefined;
507
+ strategy?: "file-based" | "feature-based" | "risk-based" | "auto" | undefined;
508
+ epic_id?: string | undefined;
494
509
  }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
495
510
  };
496
511
  swarm_delegate_planning: {
@@ -1 +1 @@
1
- {"version":3,"file":"swarm.d.ts","sourceRoot":"","sources":["../src/swarm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AASjC;;;GAGG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAMtB,CAAC"}
1
+ {"version":3,"file":"swarm.d.ts","sourceRoot":"","sources":["../src/swarm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AASjC;;;GAGG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAMtB,CAAC"}
package/evals/README.md CHANGED
@@ -5,14 +5,12 @@ TypeScript-native evaluation framework for testing swarm task decomposition qual
5
5
  ## Quick Start
6
6
 
7
7
  ```bash
8
- # Watch mode for development
9
- pnpm eval:dev
10
-
11
8
  # Run all evals once
12
- pnpm eval:run
9
+ bun run eval:run
13
10
 
14
- # CI mode with 80% threshold
15
- pnpm eval:ci
11
+ # Run specific eval suite
12
+ bun run eval:decomposition
13
+ bun run eval:coordinator
16
14
  ```
17
15
 
18
16
  ## Structure
@@ -134,13 +132,32 @@ Scores coordinator discipline during swarm sessions.
134
132
  bunx evalite run evals/coordinator-session.eval.ts
135
133
  ```
136
134
 
137
- ## Data Loaders
135
+ ## Data Capture
136
+
137
+ ### What Gets Captured
138
+
139
+ **Decomposition Eval Data:**
140
+ - Task input (user's original request)
141
+ - Generated CellTree JSON (epic + subtasks)
142
+ - Timestamp and context
143
+ - Stored in: `.opencode/eval-data.jsonl`
144
+
145
+ **Coordinator Session Data:**
146
+ - Real swarm sessions captured during `/swarm` runs
147
+ - Includes: decomposition, spawn events, reviews, violations
148
+ - Stored in: `~/.config/swarm-tools/sessions/*.jsonl`
149
+
150
+ **Subtask Outcome Data:**
151
+ - Duration, success/failure, error count, retry count
152
+ - Files touched, strategy used
153
+ - Used for learning and pattern maturity
154
+ - Stored in: swarm-mail database (libSQL)
138
155
 
139
- ### lib/data-loader.ts
156
+ ### Data Loaders
140
157
 
141
- Loads eval data from multiple sources:
158
+ **lib/data-loader.ts** provides utilities to load eval data:
142
159
 
143
- - `loadEvalCases()` - PGlite eval_records table
160
+ - `loadEvalCases()` - Load eval records from swarm-mail database
144
161
  - `loadCapturedSessions()` - Real coordinator sessions from `~/.config/swarm-tools/sessions/`
145
162
  - `hasRealEvalData()` - Check if enough real data exists
146
163
  - `getEvalDataSummary()` - Stats about available eval data
@@ -305,6 +305,34 @@ const hive_sync = tool({
305
305
  execute: (args, ctx) => execTool("hive_sync", args, ctx),
306
306
  });
307
307
 
308
+ const hive_cells = tool({
309
+ description: `Query cells from the hive database with flexible filtering.
310
+
311
+ USE THIS TOOL TO:
312
+ - List all open cells: hive_cells()
313
+ - Find cells by status: hive_cells({ status: "in_progress" })
314
+ - Find cells by type: hive_cells({ type: "bug" })
315
+ - Get a specific cell by partial ID: hive_cells({ id: "mjkmd" })
316
+ - Get the next ready (unblocked) cell: hive_cells({ ready: true })
317
+ - Combine filters: hive_cells({ status: "open", type: "task" })
318
+
319
+ RETURNS: Array of cells with id, title, status, priority, type, parent_id, created_at, updated_at
320
+
321
+ PREFER THIS OVER hive_query when you need to:
322
+ - See what work is available
323
+ - Check status of multiple cells
324
+ - Find cells matching criteria
325
+ - Look up a cell by partial ID`,
326
+ args: {
327
+ id: tool.schema.string().optional().describe("Partial or full cell ID to look up"),
328
+ status: tool.schema.enum(["open", "in_progress", "blocked", "closed"]).optional().describe("Filter by status"),
329
+ type: tool.schema.enum(["task", "bug", "feature", "epic", "chore"]).optional().describe("Filter by type"),
330
+ ready: tool.schema.boolean().optional().describe("If true, return only the next unblocked cell"),
331
+ limit: tool.schema.number().optional().describe("Max cells to return (default 20)"),
332
+ },
333
+ execute: (args, ctx) => execTool("hive_cells", args, ctx),
334
+ });
335
+
308
336
  const beads_link_thread = tool({
309
337
  description: "Add metadata linking bead to Agent Mail thread",
310
338
  args: {
@@ -1202,9 +1230,18 @@ ${JSON.stringify(snapshot, null, 2)}
1202
1230
 
1203
1231
  Generate a prompt following this structure:
1204
1232
 
1233
+ ┌─────────────────────────────────────────────────────────────┐
1234
+ │ │
1235
+ │ 🐝 YOU ARE THE COORDINATOR 🐝 │
1236
+ │ │
1237
+ │ NOT A WORKER. NOT AN IMPLEMENTER. │
1238
+ │ YOU ORCHESTRATE. │
1239
+ │ │
1240
+ └─────────────────────────────────────────────────────────────┘
1241
+
1205
1242
  # 🐝 Swarm Continuation - [Epic Title or "Unknown"]
1206
1243
 
1207
- You are resuming coordination of an active swarm that was interrupted by context compaction.
1244
+ **NON-NEGOTIABLE: YOU ARE THE COORDINATOR.** You resumed after context compaction.
1208
1245
 
1209
1246
  ## Epic State
1210
1247
 
@@ -1231,15 +1268,29 @@ You are resuming coordination of an active swarm that was interrupted by context
1231
1268
 
1232
1269
  [List 3-5 concrete actions with actual commands, using real IDs from the state]
1233
1270
 
1234
- ## Coordinator Reminders
1271
+ ## 🎯 COORDINATOR MANDATES (NON-NEGOTIABLE)
1272
+
1273
+ **YOU ARE THE COORDINATOR. NOT A WORKER.**
1274
+
1275
+ ### ⛔ FORBIDDEN - NEVER do these:
1276
+ - ❌ NEVER use \`edit\`, \`write\`, or \`bash\` for implementation - SPAWN A WORKER
1277
+ - ❌ NEVER fetch directly with \`repo-crawl_*\`, \`repo-autopsy_*\`, \`webfetch\`, \`fetch_fetch\` - SPAWN A RESEARCHER
1278
+ - ❌ NEVER use \`context7_*\` or \`pdf-brain_*\` directly - SPAWN A RESEARCHER
1279
+ - ❌ NEVER reserve files - Workers reserve files
1280
+
1281
+ ### ✅ ALWAYS do these:
1282
+ - ✅ ALWAYS check \`swarm_status\` and \`swarmmail_inbox\` first
1283
+ - ✅ ALWAYS use \`swarm_spawn_subtask\` for implementation work
1284
+ - ✅ ALWAYS use \`swarm_spawn_researcher\` for external data fetching
1285
+ - ✅ ALWAYS review worker output with \`swarm_review\` → \`swarm_review_feedback\`
1286
+ - ✅ ALWAYS monitor actively - Check messages every ~10 minutes
1287
+ - ✅ ALWAYS unblock aggressively - Resolve dependencies immediately
1288
+
1289
+ **If you need external data:** Use \`swarm_spawn_researcher\` with a clear research task. The researcher will fetch, summarize, and return findings.
1235
1290
 
1236
- - **You are the coordinator** - Don't wait for instructions, orchestrate
1237
- - **Monitor actively** - Check messages every ~10 minutes
1238
- - **Unblock aggressively** - Resolve dependencies immediately
1239
- - **Review thoroughly** - 3-strike rule enforced
1240
- - **Ship it** - When all subtasks done, close the epic
1291
+ **3-strike rule enforced:** Workers get 3 review attempts. After 3 rejections, escalate to human.
1241
1292
 
1242
- Keep the prompt concise but actionable. Use actual data from the snapshot, not placeholders.`;
1293
+ Keep the prompt concise but actionable. Use actual data from the snapshot, not placeholders. Include the ASCII header and ALL coordinator mandates.`;
1243
1294
 
1244
1295
  logCompaction("debug", "generate_compaction_prompt_calling_llm", {
1245
1296
  session_id: snapshot.sessionID,
@@ -1896,6 +1947,7 @@ const SwarmPlugin: Plugin = async (
1896
1947
  hive_close,
1897
1948
  hive_start,
1898
1949
  hive_ready,
1950
+ hive_cells,
1899
1951
  hive_sync,
1900
1952
  beads_link_thread,
1901
1953
  // Swarm Mail (Embedded)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.36.1",
3
+ "version": "0.38.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",
@@ -30,6 +30,9 @@
30
30
  "test:all": "bun test --timeout 60000 src/",
31
31
  "test:watch": "bun test --watch src/",
32
32
  "typecheck": "tsc --noEmit",
33
+ "eval:run": "bunx evalite run evals/",
34
+ "eval:decomposition": "bunx evalite run evals/swarm-decomposition.eval.ts",
35
+ "eval:coordinator": "bunx evalite run evals/coordinator-session.eval.ts",
33
36
  "postinstall": "node -e \"console.log('\\n\\x1b[33m Run \\x1b[36mswarm setup\\x1b[33m to configure OpenCode integration\\x1b[0m\\n')\""
34
37
  },
35
38
  "dependencies": {
@@ -64,7 +64,7 @@ describe("Compaction Hook", () => {
64
64
  describe("SWARM_COMPACTION_CONTEXT", () => {
65
65
  it("contains coordinator instructions", () => {
66
66
  expect(SWARM_COMPACTION_CONTEXT).toContain("COORDINATOR");
67
- expect(SWARM_COMPACTION_CONTEXT).toContain("You Are The COORDINATOR");
67
+ expect(SWARM_COMPACTION_CONTEXT).toContain("YOU ARE THE COORDINATOR");
68
68
  });
69
69
 
70
70
  it("contains prohibition-first anti-patterns", () => {
@@ -136,7 +136,7 @@ describe("Compaction Hook", () => {
136
136
  it("HIGH confidence triggers full context", async () => {
137
137
  // This would need proper mocking of active reservations
138
138
  // For now, just verify the context strings exist
139
- expect(SWARM_COMPACTION_CONTEXT).toContain("SWARM ACTIVE");
139
+ expect(SWARM_COMPACTION_CONTEXT).toContain("YOU ARE THE COORDINATOR");
140
140
  });
141
141
 
142
142
  it("LOW confidence triggers fallback prompt", async () => {
@@ -145,6 +145,101 @@ describe("Compaction Hook", () => {
145
145
  });
146
146
  });
147
147
 
148
+ describe("Forbidden tools anti-pattern (TDD red phase)", () => {
149
+ it("SWARM_COMPACTION_CONTEXT includes 'NEVER fetch directly' rule", () => {
150
+ // Should warn against direct fetching
151
+ expect(SWARM_COMPACTION_CONTEXT).toContain("NEVER");
152
+ expect(SWARM_COMPACTION_CONTEXT).toContain("repo-crawl");
153
+ expect(SWARM_COMPACTION_CONTEXT).toContain("webfetch");
154
+ expect(SWARM_COMPACTION_CONTEXT).toContain("fetch_fetch");
155
+ expect(SWARM_COMPACTION_CONTEXT).toContain("context7");
156
+ expect(SWARM_COMPACTION_CONTEXT).toContain("pdf-brain");
157
+ });
158
+
159
+ it("SWARM_COMPACTION_CONTEXT instructs to spawn researcher instead", () => {
160
+ expect(SWARM_COMPACTION_CONTEXT).toContain("SPAWN A RESEARCHER");
161
+ expect(SWARM_COMPACTION_CONTEXT).toContain("swarm_spawn_researcher");
162
+ });
163
+
164
+ it("lists all forbidden repo-crawl tools", () => {
165
+ const forbiddenTools = [
166
+ "repo-crawl_file",
167
+ "repo-crawl_readme",
168
+ "repo-crawl_search",
169
+ "repo-crawl_structure",
170
+ "repo-crawl_tree"
171
+ ];
172
+
173
+ for (const tool of forbiddenTools) {
174
+ expect(SWARM_COMPACTION_CONTEXT).toContain(tool);
175
+ }
176
+ });
177
+
178
+ it("lists all forbidden repo-autopsy tools", () => {
179
+ expect(SWARM_COMPACTION_CONTEXT).toContain("repo-autopsy");
180
+ });
181
+
182
+ it("lists all forbidden context7 tools", () => {
183
+ const forbiddenTools = [
184
+ "context7_resolve-library-id",
185
+ "context7_get-library-docs"
186
+ ];
187
+
188
+ for (const tool of forbiddenTools) {
189
+ expect(SWARM_COMPACTION_CONTEXT).toContain(tool);
190
+ }
191
+ });
192
+
193
+ it("lists all forbidden pdf-brain tools", () => {
194
+ const forbiddenTools = [
195
+ "pdf-brain_search",
196
+ "pdf-brain_read"
197
+ ];
198
+
199
+ for (const tool of forbiddenTools) {
200
+ expect(SWARM_COMPACTION_CONTEXT).toContain(tool);
201
+ }
202
+ });
203
+ });
204
+
205
+ describe("Coordinator identity reinforcement (TDD red phase)", () => {
206
+ it("includes ASCII header for coordinator identity", () => {
207
+ // Should have prominent visual indicator
208
+ expect(SWARM_COMPACTION_CONTEXT).toMatch(/[╔═╗║╚╝]|[┌─┐│└┘]|[█▀▄]/);
209
+ });
210
+
211
+ it("repeats 'YOU ARE THE COORDINATOR' multiple times", () => {
212
+ const matches = SWARM_COMPACTION_CONTEXT.match(/YOU ARE THE COORDINATOR/gi);
213
+ expect(matches).toBeDefined();
214
+ expect(matches!.length).toBeGreaterThanOrEqual(2);
215
+ });
216
+
217
+ it("uses strong imperative language NEVER/ALWAYS/NON-NEGOTIABLE", () => {
218
+ expect(SWARM_COMPACTION_CONTEXT).toContain("NEVER");
219
+ expect(SWARM_COMPACTION_CONTEXT).toContain("ALWAYS");
220
+ expect(SWARM_COMPACTION_CONTEXT).toContain("NON-NEGOTIABLE");
221
+ });
222
+
223
+ it("makes role unmistakable with multiple strong statements", () => {
224
+ // Check for strong coordinator identity statements
225
+ const identityPatterns = [
226
+ /YOU ARE THE COORDINATOR/i,
227
+ /NOT A WORKER/i,
228
+ /ORCHESTRATE/i,
229
+ /DO NOT IMPLEMENT/i
230
+ ];
231
+
232
+ let matchCount = 0;
233
+ for (const pattern of identityPatterns) {
234
+ if (pattern.test(SWARM_COMPACTION_CONTEXT)) {
235
+ matchCount++;
236
+ }
237
+ }
238
+
239
+ expect(matchCount).toBeGreaterThanOrEqual(3);
240
+ });
241
+ });
242
+
148
243
  describe("Specific swarm state injection (TDD red phase)", () => {
149
244
  it("includes specific epic ID when in_progress epic exists", async () => {
150
245
  // Mock hive with an in_progress epic
@@ -68,9 +68,21 @@ function getLog() {
68
68
  * This is NOT about preserving state for a human - it's about the swarm continuing
69
69
  * autonomously after context compression.
70
70
  */
71
- export const SWARM_COMPACTION_CONTEXT = `## 🐝 SWARM ACTIVE - You Are The COORDINATOR
71
+ export const SWARM_COMPACTION_CONTEXT = `
72
+ ┌─────────────────────────────────────────────────────────────┐
73
+ │ │
74
+ │ 🐝 YOU ARE THE COORDINATOR 🐝 │
75
+ │ │
76
+ │ NOT A WORKER. NOT AN IMPLEMENTER. │
77
+ │ YOU ORCHESTRATE. │
78
+ │ │
79
+ └─────────────────────────────────────────────────────────────┘
72
80
 
73
- Context was compacted but the swarm is still running. You are the **COORDINATOR**.
81
+ ## 🎯 NON-NEGOTIABLE: YOU ARE THE COORDINATOR
82
+
83
+ Context was compacted but the swarm is still running. **YOU ARE THE COORDINATOR.**
84
+
85
+ Your role is ORCHESTRATION, not implementation. When you catch yourself about to do work directly, STOP.
74
86
 
75
87
  ### ⛔ NEVER DO THESE (Coordinator Anti-Patterns)
76
88
 
@@ -81,9 +93,27 @@ Context was compacted but the swarm is still running. You are the **COORDINATOR*
81
93
  - ❌ **NEVER** implement features yourself - SPAWN A WORKER
82
94
  - ❌ **NEVER** "just do it myself to save time" - NO. SPAWN A WORKER.
83
95
  - ❌ **NEVER** reserve files with \`swarmmail_reserve\` - Workers reserve files
96
+ - ❌ **NEVER** fetch files/docs directly - SPAWN A RESEARCHER
84
97
 
85
98
  **If you catch yourself about to edit a file, STOP. Use \`swarm_spawn_subtask\` instead.**
86
99
 
100
+ ### 🚫 FORBIDDEN TOOLS (Coordinators MUST delegate these)
101
+
102
+ **NEVER use these tools directly. ALWAYS spawn a researcher worker via \`swarm_spawn_researcher\`:**
103
+
104
+ **Repository fetching:**
105
+ - \`repo-crawl_file\`, \`repo-crawl_readme\`, \`repo-crawl_search\`, \`repo-crawl_structure\`, \`repo-crawl_tree\`
106
+ - \`repo-autopsy_*\` (all repo-autopsy tools)
107
+
108
+ **Web/documentation fetching:**
109
+ - \`webfetch\`, \`fetch_fetch\`
110
+ - \`context7_resolve-library-id\`, \`context7_get-library-docs\`
111
+
112
+ **Knowledge base:**
113
+ - \`pdf-brain_search\`, \`pdf-brain_read\`
114
+
115
+ **If you need external data:** Use \`swarm_spawn_researcher\` with a clear research task. The researcher will fetch, summarize, and return findings.
116
+
87
117
  ### ✅ ALWAYS DO THESE (Coordinator Checklist)
88
118
 
89
119
  On resume, execute this checklist IN ORDER:
@@ -1895,6 +1895,154 @@ describe("beads integration", () => {
1895
1895
  });
1896
1896
  });
1897
1897
 
1898
+ describe("hive_cells", () => {
1899
+ let testCellId: string;
1900
+
1901
+ beforeEach(async () => {
1902
+ // Create a test cell for hive_cells tests
1903
+ const result = await hive_create.execute(
1904
+ { title: "Cells tool test", type: "task" },
1905
+ mockContext,
1906
+ );
1907
+ const cell = parseResponse<Cell>(result);
1908
+ testCellId = cell.id;
1909
+ createdBeadIds.push(testCellId);
1910
+ });
1911
+
1912
+ it("lists all cells with no filters", async () => {
1913
+ const { hive_cells } = await import("./hive");
1914
+
1915
+ const result = await hive_cells.execute({}, mockContext);
1916
+ const cells = parseResponse<Cell[]>(result);
1917
+
1918
+ expect(Array.isArray(cells)).toBe(true);
1919
+ expect(cells.length).toBeGreaterThan(0);
1920
+ });
1921
+
1922
+ it("filters by status", async () => {
1923
+ const { hive_cells } = await import("./hive");
1924
+
1925
+ const result = await hive_cells.execute({ status: "open" }, mockContext);
1926
+ const cells = parseResponse<Cell[]>(result);
1927
+
1928
+ expect(Array.isArray(cells)).toBe(true);
1929
+ expect(cells.every((c) => c.status === "open")).toBe(true);
1930
+ });
1931
+
1932
+ it("filters by type", async () => {
1933
+ const { hive_cells } = await import("./hive");
1934
+
1935
+ // Create a bug cell
1936
+ const bugResult = await hive_create.execute(
1937
+ { title: "Bug for cells test", type: "bug" },
1938
+ mockContext,
1939
+ );
1940
+ const bug = parseResponse<Cell>(bugResult);
1941
+ createdBeadIds.push(bug.id);
1942
+
1943
+ const result = await hive_cells.execute({ type: "bug" }, mockContext);
1944
+ const cells = parseResponse<Cell[]>(result);
1945
+
1946
+ expect(Array.isArray(cells)).toBe(true);
1947
+ expect(cells.every((c) => c.issue_type === "bug")).toBe(true);
1948
+ });
1949
+
1950
+ it("returns next ready cell when ready=true", async () => {
1951
+ const { hive_cells } = await import("./hive");
1952
+
1953
+ const result = await hive_cells.execute({ ready: true }, mockContext);
1954
+ const cells = parseResponse<Cell[]>(result);
1955
+
1956
+ expect(Array.isArray(cells)).toBe(true);
1957
+ // Should return 0 or 1 cells (the next ready one)
1958
+ expect(cells.length).toBeLessThanOrEqual(1);
1959
+ if (cells.length === 1) {
1960
+ expect(["open", "in_progress"]).toContain(cells[0].status);
1961
+ }
1962
+ });
1963
+
1964
+ it("looks up cell by partial ID", async () => {
1965
+ const { hive_cells } = await import("./hive");
1966
+
1967
+ // Extract hash from full ID (6-char segment before the last hyphen)
1968
+ const lastHyphenIndex = testCellId.lastIndexOf("-");
1969
+ const beforeLast = testCellId.substring(0, lastHyphenIndex);
1970
+ const secondLastHyphenIndex = beforeLast.lastIndexOf("-");
1971
+ const hash = testCellId.substring(secondLastHyphenIndex + 1, lastHyphenIndex);
1972
+
1973
+ // Use last 6 chars of hash (or full hash if short)
1974
+ const shortHash = hash.substring(Math.max(0, hash.length - 6));
1975
+
1976
+ try {
1977
+ const result = await hive_cells.execute({ id: shortHash }, mockContext);
1978
+ const cells = parseResponse<Cell[]>(result);
1979
+
1980
+ // Should return exactly one cell matching the ID
1981
+ expect(cells).toHaveLength(1);
1982
+ expect(cells[0].id).toBe(testCellId);
1983
+ } catch (error) {
1984
+ // If ambiguous, verify error message is helpful
1985
+ if (error instanceof Error && error.message.includes("Ambiguous")) {
1986
+ expect(error.message).toMatch(/ambiguous.*multiple/i);
1987
+ expect(error.message).toContain(shortHash);
1988
+ } else {
1989
+ throw error;
1990
+ }
1991
+ }
1992
+ });
1993
+
1994
+ it("looks up cell by full ID", async () => {
1995
+ const { hive_cells } = await import("./hive");
1996
+
1997
+ const result = await hive_cells.execute({ id: testCellId }, mockContext);
1998
+ const cells = parseResponse<Cell[]>(result);
1999
+
2000
+ expect(cells).toHaveLength(1);
2001
+ expect(cells[0].id).toBe(testCellId);
2002
+ expect(cells[0].title).toBe("Cells tool test");
2003
+ });
2004
+
2005
+ it("throws error for non-existent ID", async () => {
2006
+ const { hive_cells } = await import("./hive");
2007
+
2008
+ await expect(
2009
+ hive_cells.execute({ id: "nonexistent999" }, mockContext),
2010
+ ).rejects.toThrow(/not found|no cell|nonexistent999/i);
2011
+ });
2012
+
2013
+ it("respects limit parameter", async () => {
2014
+ const { hive_cells } = await import("./hive");
2015
+
2016
+ const result = await hive_cells.execute({ limit: 2 }, mockContext);
2017
+ const cells = parseResponse<Cell[]>(result);
2018
+
2019
+ expect(cells.length).toBeLessThanOrEqual(2);
2020
+ });
2021
+
2022
+ it("combines filters (status + type + limit)", async () => {
2023
+ const { hive_cells } = await import("./hive");
2024
+
2025
+ // Create some task cells
2026
+ for (let i = 0; i < 3; i++) {
2027
+ const r = await hive_create.execute(
2028
+ { title: `Task ${i}`, type: "task" },
2029
+ mockContext,
2030
+ );
2031
+ const c = parseResponse<Cell>(r);
2032
+ createdBeadIds.push(c.id);
2033
+ }
2034
+
2035
+ const result = await hive_cells.execute(
2036
+ { status: "open", type: "task", limit: 2 },
2037
+ mockContext,
2038
+ );
2039
+ const cells = parseResponse<Cell[]>(result);
2040
+
2041
+ expect(cells.length).toBeLessThanOrEqual(2);
2042
+ expect(cells.every((c) => c.status === "open" && c.issue_type === "task")).toBe(true);
2043
+ });
2044
+ });
2045
+
1898
2046
  describe("bigint to Date conversion", () => {
1899
2047
  it("should handle PGLite bigint timestamps correctly in hive_query", async () => {
1900
2048
  const { mkdirSync, rmSync } = await import("node:fs");
package/src/hive.ts CHANGED
@@ -1092,6 +1092,94 @@ export const hive_ready = tool({
1092
1092
  },
1093
1093
  });
1094
1094
 
1095
+ /**
1096
+ * Query cells from the hive database with flexible filtering
1097
+ */
1098
+ export const hive_cells = tool({
1099
+ description: `Query cells from the hive database with flexible filtering.
1100
+
1101
+ USE THIS TOOL TO:
1102
+ - List all open cells: hive_cells()
1103
+ - Find cells by status: hive_cells({ status: "in_progress" })
1104
+ - Find cells by type: hive_cells({ type: "bug" })
1105
+ - Get a specific cell by partial ID: hive_cells({ id: "mjkmd" })
1106
+ - Get the next ready (unblocked) cell: hive_cells({ ready: true })
1107
+ - Combine filters: hive_cells({ status: "open", type: "task" })
1108
+
1109
+ RETURNS: Array of cells with id, title, status, priority, type, parent_id, created_at, updated_at
1110
+
1111
+ PREFER THIS OVER hive_query when you need to:
1112
+ - See what work is available
1113
+ - Check status of multiple cells
1114
+ - Find cells matching criteria
1115
+ - Look up a cell by partial ID`,
1116
+ args: {
1117
+ id: tool.schema.string().optional().describe("Partial or full cell ID to look up"),
1118
+ status: tool.schema.enum(["open", "in_progress", "blocked", "closed"]).optional().describe("Filter by status"),
1119
+ type: tool.schema.enum(["task", "bug", "feature", "epic", "chore"]).optional().describe("Filter by type"),
1120
+ ready: tool.schema.boolean().optional().describe("If true, return only the next unblocked cell"),
1121
+ limit: tool.schema.number().optional().describe("Max cells to return (default 20)"),
1122
+ },
1123
+ async execute(args, ctx) {
1124
+ const projectKey = getHiveWorkingDirectory();
1125
+ const adapter = await getHiveAdapter(projectKey);
1126
+
1127
+ try {
1128
+ // If specific ID requested, resolve and return single cell
1129
+ if (args.id) {
1130
+ const fullId = await resolvePartialId(adapter, projectKey, args.id) || args.id;
1131
+ const cell = await adapter.getCell(projectKey, fullId);
1132
+ if (!cell) {
1133
+ throw new HiveError(`No cell found matching ID '${args.id}'`, "hive_cells");
1134
+ }
1135
+ const formatted = formatCellForOutput(cell);
1136
+ return JSON.stringify([formatted], null, 2);
1137
+ }
1138
+
1139
+ // If ready flag, return next unblocked cell
1140
+ if (args.ready) {
1141
+ const ready = await adapter.getNextReadyCell(projectKey);
1142
+ if (!ready) {
1143
+ return JSON.stringify([], null, 2);
1144
+ }
1145
+ const formatted = formatCellForOutput(ready);
1146
+ return JSON.stringify([formatted], null, 2);
1147
+ }
1148
+
1149
+ // Query with filters
1150
+ const cells = await adapter.queryCells(projectKey, {
1151
+ status: args.status,
1152
+ type: args.type,
1153
+ limit: args.limit || 20,
1154
+ });
1155
+
1156
+ const formatted = cells.map(c => formatCellForOutput(c));
1157
+ return JSON.stringify(formatted, null, 2);
1158
+ } catch (error) {
1159
+ const message = error instanceof Error ? error.message : String(error);
1160
+
1161
+ // Provide helpful error messages
1162
+ if (message.includes("Ambiguous hash")) {
1163
+ throw new HiveError(
1164
+ `Ambiguous ID '${args.id}': multiple cells match. Please provide more characters.`,
1165
+ "hive_cells",
1166
+ );
1167
+ }
1168
+ if (message.includes("Bead not found") || message.includes("Cell not found")) {
1169
+ throw new HiveError(
1170
+ `No cell found matching ID '${args.id || "unknown"}'`,
1171
+ "hive_cells",
1172
+ );
1173
+ }
1174
+
1175
+ throw new HiveError(
1176
+ `Failed to query cells: ${message}`,
1177
+ "hive_cells",
1178
+ );
1179
+ }
1180
+ },
1181
+ });
1182
+
1095
1183
  /**
1096
1184
  * Sync hive to git and push
1097
1185
  */
@@ -1345,6 +1433,7 @@ export const hiveTools = {
1345
1433
  hive_close,
1346
1434
  hive_start,
1347
1435
  hive_ready,
1436
+ hive_cells,
1348
1437
  hive_sync,
1349
1438
  hive_link_thread,
1350
1439
  };