opencode-swarm-plugin 0.36.1 → 0.37.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/.hive/issues.jsonl +12 -0
- package/.turbo/turbo-test.log +321 -321
- package/CHANGELOG.md +42 -0
- package/bin/swarm.test.ts +106 -0
- package/bin/swarm.ts +179 -0
- package/package.json +1 -1
- package/src/hive.integration.test.ts +148 -0
- package/src/hive.ts +89 -0
- package/src/swarm-decompose.test.ts +195 -0
- package/src/swarm-decompose.ts +52 -1
- package/src/swarm-review.integration.test.ts +24 -29
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# opencode-swarm-plugin
|
|
2
2
|
|
|
3
|
+
## 0.37.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`66b5795`](https://github.com/joelhooks/swarm-tools/commit/66b57951e2c114702c663b98829d5f7626607a16) Thanks [@joelhooks](https://github.com/joelhooks)! - ## 🐝 `swarm cells` - Query Your Hive Like a Pro
|
|
8
|
+
|
|
9
|
+
New CLI command AND plugin tool for querying cells directly from the database.
|
|
10
|
+
|
|
11
|
+
### CLI: `swarm cells`
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
swarm cells # List all cells (table format)
|
|
15
|
+
swarm cells --status open # Filter by status
|
|
16
|
+
swarm cells --type bug # Filter by type
|
|
17
|
+
swarm cells --ready # Next unblocked cell
|
|
18
|
+
swarm cells mjkmd # Partial ID lookup
|
|
19
|
+
swarm cells --json # Raw JSON for scripting
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Replaces:** The awkward `swarm tool hive_query --json '{"status":"open"}'` pattern.
|
|
23
|
+
|
|
24
|
+
### Plugin Tool: `hive_cells`
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// Agents can now query cells directly
|
|
28
|
+
hive_cells({ status: "open", type: "task" });
|
|
29
|
+
hive_cells({ id: "mjkmd" }); // Partial ID works!
|
|
30
|
+
hive_cells({ ready: true }); // Next unblocked
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Why this matters:**
|
|
34
|
+
|
|
35
|
+
- Reads from DATABASE (fast, indexed) not JSONL files
|
|
36
|
+
- Partial ID resolution built-in
|
|
37
|
+
- Consistent JSON array output
|
|
38
|
+
- Rich descriptions encourage agentic use
|
|
39
|
+
|
|
40
|
+
### Also Fixed
|
|
41
|
+
|
|
42
|
+
- `swarm_review_feedback` tests updated for coordinator-driven retry architecture
|
|
43
|
+
- 425 tests passing
|
|
44
|
+
|
|
3
45
|
## 0.36.1
|
|
4
46
|
|
|
5
47
|
### Patch Changes
|
package/bin/swarm.test.ts
CHANGED
|
@@ -197,6 +197,112 @@ READ-ONLY research agent. Never modifies code - only gathers intel and stores fi
|
|
|
197
197
|
// Log Command Tests (TDD)
|
|
198
198
|
// ============================================================================
|
|
199
199
|
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// Cells Command Tests (TDD)
|
|
202
|
+
// ============================================================================
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Format cells as table output
|
|
206
|
+
*/
|
|
207
|
+
function formatCellsTable(cells: Array<{
|
|
208
|
+
id: string;
|
|
209
|
+
title: string;
|
|
210
|
+
status: string;
|
|
211
|
+
priority: number;
|
|
212
|
+
}>): string {
|
|
213
|
+
if (cells.length === 0) {
|
|
214
|
+
return "No cells found";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const rows = cells.map(c => ({
|
|
218
|
+
id: c.id,
|
|
219
|
+
title: c.title.length > 50 ? c.title.slice(0, 47) + "..." : c.title,
|
|
220
|
+
status: c.status,
|
|
221
|
+
priority: String(c.priority),
|
|
222
|
+
}));
|
|
223
|
+
|
|
224
|
+
// Calculate column widths
|
|
225
|
+
const widths = {
|
|
226
|
+
id: Math.max(2, ...rows.map(r => r.id.length)),
|
|
227
|
+
title: Math.max(5, ...rows.map(r => r.title.length)),
|
|
228
|
+
status: Math.max(6, ...rows.map(r => r.status.length)),
|
|
229
|
+
priority: Math.max(8, ...rows.map(r => r.priority.length)),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Build header
|
|
233
|
+
const header = [
|
|
234
|
+
"ID".padEnd(widths.id),
|
|
235
|
+
"TITLE".padEnd(widths.title),
|
|
236
|
+
"STATUS".padEnd(widths.status),
|
|
237
|
+
"PRIORITY".padEnd(widths.priority),
|
|
238
|
+
].join(" ");
|
|
239
|
+
|
|
240
|
+
const separator = "-".repeat(header.length);
|
|
241
|
+
|
|
242
|
+
// Build rows
|
|
243
|
+
const bodyRows = rows.map(r =>
|
|
244
|
+
[
|
|
245
|
+
r.id.padEnd(widths.id),
|
|
246
|
+
r.title.padEnd(widths.title),
|
|
247
|
+
r.status.padEnd(widths.status),
|
|
248
|
+
r.priority.padEnd(widths.priority),
|
|
249
|
+
].join(" ")
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return [header, separator, ...bodyRows].join("\n");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
describe("Cells command", () => {
|
|
256
|
+
describe("formatCellsTable", () => {
|
|
257
|
+
test("formats cells as table with id, title, status, priority", () => {
|
|
258
|
+
const cells = [
|
|
259
|
+
{
|
|
260
|
+
id: "test-abc123-xyz",
|
|
261
|
+
title: "Fix bug",
|
|
262
|
+
status: "open",
|
|
263
|
+
priority: 0,
|
|
264
|
+
type: "bug",
|
|
265
|
+
created_at: 1234567890,
|
|
266
|
+
updated_at: 1234567890,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: "test-def456-abc",
|
|
270
|
+
title: "Add feature",
|
|
271
|
+
status: "in_progress",
|
|
272
|
+
priority: 2,
|
|
273
|
+
type: "feature",
|
|
274
|
+
created_at: 1234567890,
|
|
275
|
+
updated_at: 1234567890,
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const table = formatCellsTable(cells);
|
|
280
|
+
|
|
281
|
+
// Should contain headers
|
|
282
|
+
expect(table).toContain("ID");
|
|
283
|
+
expect(table).toContain("TITLE");
|
|
284
|
+
expect(table).toContain("STATUS");
|
|
285
|
+
expect(table).toContain("PRIORITY");
|
|
286
|
+
|
|
287
|
+
// Should contain cell data
|
|
288
|
+
expect(table).toContain("test-abc123-xyz");
|
|
289
|
+
expect(table).toContain("Fix bug");
|
|
290
|
+
expect(table).toContain("open");
|
|
291
|
+
expect(table).toContain("0");
|
|
292
|
+
|
|
293
|
+
expect(table).toContain("test-def456-abc");
|
|
294
|
+
expect(table).toContain("Add feature");
|
|
295
|
+
expect(table).toContain("in_progress");
|
|
296
|
+
expect(table).toContain("2");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("returns 'No cells found' for empty array", () => {
|
|
300
|
+
const table = formatCellsTable([]);
|
|
301
|
+
expect(table).toBe("No cells found");
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
200
306
|
describe("Log command helpers", () => {
|
|
201
307
|
let testDir: string;
|
|
202
308
|
|
package/bin/swarm.ts
CHANGED
|
@@ -2722,6 +2722,7 @@ ${cyan("Commands:")}
|
|
|
2722
2722
|
swarm config Show paths to generated config files
|
|
2723
2723
|
swarm agents Update AGENTS.md with skill awareness
|
|
2724
2724
|
swarm migrate Migrate PGlite database to libSQL
|
|
2725
|
+
swarm cells List or get cells from database (replaces 'swarm tool hive_query')
|
|
2725
2726
|
swarm log View swarm logs with filtering
|
|
2726
2727
|
swarm update Update to latest version
|
|
2727
2728
|
swarm version Show version and banner
|
|
@@ -2733,6 +2734,14 @@ ${cyan("Tool Execution:")}
|
|
|
2733
2734
|
swarm tool <name> Execute tool with no args
|
|
2734
2735
|
swarm tool <name> --json '<args>' Execute tool with JSON args
|
|
2735
2736
|
|
|
2737
|
+
${cyan("Cell Management:")}
|
|
2738
|
+
swarm cells List cells from database (default: 20 most recent)
|
|
2739
|
+
swarm cells <id> Get single cell by ID or partial hash
|
|
2740
|
+
swarm cells --status <status> Filter by status (open, in_progress, closed, blocked)
|
|
2741
|
+
swarm cells --type <type> Filter by type (task, bug, feature, epic, chore)
|
|
2742
|
+
swarm cells --ready Show next ready (unblocked) cell
|
|
2743
|
+
swarm cells --json Raw JSON output (array, no wrapper)
|
|
2744
|
+
|
|
2736
2745
|
${cyan("Log Viewing:")}
|
|
2737
2746
|
swarm log Tail recent logs (last 50 lines)
|
|
2738
2747
|
swarm log <module> Filter by module (e.g., compaction)
|
|
@@ -3245,6 +3254,173 @@ function readLogFiles(dir: string): LogEntry[] {
|
|
|
3245
3254
|
return entries;
|
|
3246
3255
|
}
|
|
3247
3256
|
|
|
3257
|
+
/**
|
|
3258
|
+
* Format cells as table output
|
|
3259
|
+
*/
|
|
3260
|
+
function formatCellsTable(cells: Array<{
|
|
3261
|
+
id: string;
|
|
3262
|
+
title: string;
|
|
3263
|
+
status: string;
|
|
3264
|
+
priority: number;
|
|
3265
|
+
}>): string {
|
|
3266
|
+
if (cells.length === 0) {
|
|
3267
|
+
return "No cells found";
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
const rows = cells.map(c => ({
|
|
3271
|
+
id: c.id,
|
|
3272
|
+
title: c.title.length > 50 ? c.title.slice(0, 47) + "..." : c.title,
|
|
3273
|
+
status: c.status,
|
|
3274
|
+
priority: String(c.priority),
|
|
3275
|
+
}));
|
|
3276
|
+
|
|
3277
|
+
// Calculate column widths
|
|
3278
|
+
const widths = {
|
|
3279
|
+
id: Math.max(2, ...rows.map(r => r.id.length)),
|
|
3280
|
+
title: Math.max(5, ...rows.map(r => r.title.length)),
|
|
3281
|
+
status: Math.max(6, ...rows.map(r => r.status.length)),
|
|
3282
|
+
priority: Math.max(8, ...rows.map(r => r.priority.length)),
|
|
3283
|
+
};
|
|
3284
|
+
|
|
3285
|
+
// Build header
|
|
3286
|
+
const header = [
|
|
3287
|
+
"ID".padEnd(widths.id),
|
|
3288
|
+
"TITLE".padEnd(widths.title),
|
|
3289
|
+
"STATUS".padEnd(widths.status),
|
|
3290
|
+
"PRIORITY".padEnd(widths.priority),
|
|
3291
|
+
].join(" ");
|
|
3292
|
+
|
|
3293
|
+
const separator = "-".repeat(header.length);
|
|
3294
|
+
|
|
3295
|
+
// Build rows
|
|
3296
|
+
const bodyRows = rows.map(r =>
|
|
3297
|
+
[
|
|
3298
|
+
r.id.padEnd(widths.id),
|
|
3299
|
+
r.title.padEnd(widths.title),
|
|
3300
|
+
r.status.padEnd(widths.status),
|
|
3301
|
+
r.priority.padEnd(widths.priority),
|
|
3302
|
+
].join(" ")
|
|
3303
|
+
);
|
|
3304
|
+
|
|
3305
|
+
return [header, separator, ...bodyRows].join("\n");
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
/**
|
|
3309
|
+
* List or get cells from database
|
|
3310
|
+
*/
|
|
3311
|
+
async function cells() {
|
|
3312
|
+
const args = process.argv.slice(3);
|
|
3313
|
+
|
|
3314
|
+
// Parse arguments
|
|
3315
|
+
let cellId: string | null = null;
|
|
3316
|
+
let statusFilter: string | null = null;
|
|
3317
|
+
let typeFilter: string | null = null;
|
|
3318
|
+
let readyOnly = false;
|
|
3319
|
+
let jsonOutput = false;
|
|
3320
|
+
|
|
3321
|
+
for (let i = 0; i < args.length; i++) {
|
|
3322
|
+
const arg = args[i];
|
|
3323
|
+
|
|
3324
|
+
if (arg === "--status" && i + 1 < args.length) {
|
|
3325
|
+
statusFilter = args[++i];
|
|
3326
|
+
if (!["open", "in_progress", "closed", "blocked"].includes(statusFilter)) {
|
|
3327
|
+
p.log.error(`Invalid status: ${statusFilter}`);
|
|
3328
|
+
p.log.message(dim(" Valid statuses: open, in_progress, closed, blocked"));
|
|
3329
|
+
process.exit(1);
|
|
3330
|
+
}
|
|
3331
|
+
} else if (arg === "--type" && i + 1 < args.length) {
|
|
3332
|
+
typeFilter = args[++i];
|
|
3333
|
+
if (!["task", "bug", "feature", "epic", "chore"].includes(typeFilter)) {
|
|
3334
|
+
p.log.error(`Invalid type: ${typeFilter}`);
|
|
3335
|
+
p.log.message(dim(" Valid types: task, bug, feature, epic, chore"));
|
|
3336
|
+
process.exit(1);
|
|
3337
|
+
}
|
|
3338
|
+
} else if (arg === "--ready") {
|
|
3339
|
+
readyOnly = true;
|
|
3340
|
+
} else if (arg === "--json") {
|
|
3341
|
+
jsonOutput = true;
|
|
3342
|
+
} else if (!arg.startsWith("--") && !arg.startsWith("-")) {
|
|
3343
|
+
// Positional arg = cell ID (full or partial)
|
|
3344
|
+
cellId = arg;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
// Get adapter using swarm-mail
|
|
3349
|
+
const projectPath = process.cwd();
|
|
3350
|
+
const { getSwarmMailLibSQL, createHiveAdapter, resolvePartialId } = await import("swarm-mail");
|
|
3351
|
+
|
|
3352
|
+
try {
|
|
3353
|
+
const swarmMail = await getSwarmMailLibSQL(projectPath);
|
|
3354
|
+
const db = await swarmMail.getDatabase();
|
|
3355
|
+
const adapter = createHiveAdapter(db, projectPath);
|
|
3356
|
+
|
|
3357
|
+
// Run migrations to ensure schema exists
|
|
3358
|
+
await adapter.runMigrations();
|
|
3359
|
+
|
|
3360
|
+
// If cell ID provided, get single cell
|
|
3361
|
+
if (cellId) {
|
|
3362
|
+
// Resolve partial ID to full ID
|
|
3363
|
+
const fullId = await resolvePartialId(adapter, projectPath, cellId) || cellId;
|
|
3364
|
+
const cell = await adapter.getCell(projectPath, fullId);
|
|
3365
|
+
|
|
3366
|
+
if (!cell) {
|
|
3367
|
+
p.log.error(`Cell not found: ${cellId}`);
|
|
3368
|
+
process.exit(1);
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
if (jsonOutput) {
|
|
3372
|
+
console.log(JSON.stringify([cell], null, 2));
|
|
3373
|
+
} else {
|
|
3374
|
+
const table = formatCellsTable([{
|
|
3375
|
+
id: cell.id,
|
|
3376
|
+
title: cell.title,
|
|
3377
|
+
status: cell.status,
|
|
3378
|
+
priority: cell.priority,
|
|
3379
|
+
}]);
|
|
3380
|
+
console.log(table);
|
|
3381
|
+
}
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// Otherwise query cells
|
|
3386
|
+
let cells: Array<{ id: string; title: string; status: string; priority: number }>;
|
|
3387
|
+
|
|
3388
|
+
if (readyOnly) {
|
|
3389
|
+
const readyCell = await adapter.getNextReadyCell(projectPath);
|
|
3390
|
+
cells = readyCell ? [{
|
|
3391
|
+
id: readyCell.id,
|
|
3392
|
+
title: readyCell.title,
|
|
3393
|
+
status: readyCell.status,
|
|
3394
|
+
priority: readyCell.priority,
|
|
3395
|
+
}] : [];
|
|
3396
|
+
} else {
|
|
3397
|
+
const queriedCells = await adapter.queryCells(projectPath, {
|
|
3398
|
+
status: statusFilter as any || undefined,
|
|
3399
|
+
type: typeFilter as any || undefined,
|
|
3400
|
+
limit: 20,
|
|
3401
|
+
});
|
|
3402
|
+
|
|
3403
|
+
cells = queriedCells.map(c => ({
|
|
3404
|
+
id: c.id,
|
|
3405
|
+
title: c.title,
|
|
3406
|
+
status: c.status,
|
|
3407
|
+
priority: c.priority,
|
|
3408
|
+
}));
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
if (jsonOutput) {
|
|
3412
|
+
console.log(JSON.stringify(cells, null, 2));
|
|
3413
|
+
} else {
|
|
3414
|
+
const table = formatCellsTable(cells);
|
|
3415
|
+
console.log(table);
|
|
3416
|
+
}
|
|
3417
|
+
} catch (error) {
|
|
3418
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3419
|
+
p.log.error(`Failed to query cells: ${message}`);
|
|
3420
|
+
process.exit(1);
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3248
3424
|
async function logs() {
|
|
3249
3425
|
const args = process.argv.slice(3);
|
|
3250
3426
|
|
|
@@ -3614,6 +3790,9 @@ switch (command) {
|
|
|
3614
3790
|
case "db":
|
|
3615
3791
|
await db();
|
|
3616
3792
|
break;
|
|
3793
|
+
case "cells":
|
|
3794
|
+
await cells();
|
|
3795
|
+
break;
|
|
3617
3796
|
case "log":
|
|
3618
3797
|
case "logs":
|
|
3619
3798
|
await logs();
|
package/package.json
CHANGED
|
@@ -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
|
};
|