opencode-swarm-plugin 0.59.1 → 0.60.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.
Files changed (64) hide show
  1. package/README.md +14 -1
  2. package/bin/commands/doctor.test.ts +622 -0
  3. package/bin/commands/doctor.ts +658 -0
  4. package/bin/commands/status.test.ts +506 -0
  5. package/bin/commands/status.ts +520 -0
  6. package/bin/commands/tree.ts +39 -3
  7. package/bin/swarm.ts +19 -3
  8. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  9. package/claude-plugin/commands/swarm.md +125 -2
  10. package/claude-plugin/dist/index.js +669 -308
  11. package/claude-plugin/dist/schemas/cell.d.ts +2 -0
  12. package/claude-plugin/dist/schemas/cell.d.ts.map +1 -1
  13. package/claude-plugin/dist/utils/adapter-cache.d.ts +36 -0
  14. package/claude-plugin/dist/utils/adapter-cache.d.ts.map +1 -0
  15. package/claude-plugin/dist/utils/event-utils.d.ts +31 -0
  16. package/claude-plugin/dist/utils/event-utils.d.ts.map +1 -0
  17. package/claude-plugin/dist/utils/git-commit-info.d.ts +10 -0
  18. package/claude-plugin/dist/utils/git-commit-info.d.ts.map +1 -0
  19. package/claude-plugin/dist/utils/tree-renderer.d.ts +69 -13
  20. package/claude-plugin/dist/utils/tree-renderer.d.ts.map +1 -1
  21. package/dist/bin/swarm.js +2664 -980
  22. package/dist/cass-tools.d.ts.map +1 -1
  23. package/dist/dashboard.d.ts.map +1 -1
  24. package/dist/hive.d.ts +8 -0
  25. package/dist/hive.d.ts.map +1 -1
  26. package/dist/hive.js +81 -101
  27. package/dist/hivemind-tools.d.ts.map +1 -1
  28. package/dist/index.d.ts +22 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +458 -311
  31. package/dist/marketplace/index.js +669 -308
  32. package/dist/memory-tools.d.ts.map +1 -1
  33. package/dist/memory.d.ts.map +1 -1
  34. package/dist/plugin.js +456 -308
  35. package/dist/replay-tools.d.ts +5 -1
  36. package/dist/replay-tools.d.ts.map +1 -1
  37. package/dist/schemas/cell.d.ts +2 -0
  38. package/dist/schemas/cell.d.ts.map +1 -1
  39. package/dist/skills.d.ts +4 -0
  40. package/dist/skills.d.ts.map +1 -1
  41. package/dist/storage.d.ts +7 -0
  42. package/dist/storage.d.ts.map +1 -1
  43. package/dist/swarm-mail.d.ts +2 -2
  44. package/dist/swarm-mail.d.ts.map +1 -1
  45. package/dist/swarm-orchestrate.d.ts +12 -0
  46. package/dist/swarm-orchestrate.d.ts.map +1 -1
  47. package/dist/swarm-prompts.d.ts +1 -1
  48. package/dist/swarm-prompts.d.ts.map +1 -1
  49. package/dist/swarm-prompts.js +408 -274
  50. package/dist/swarm-verify.d.ts +100 -0
  51. package/dist/swarm-verify.d.ts.map +1 -0
  52. package/dist/swarm.d.ts +19 -1
  53. package/dist/swarm.d.ts.map +1 -1
  54. package/dist/test-utils/msw-server.d.ts +21 -0
  55. package/dist/test-utils/msw-server.d.ts.map +1 -0
  56. package/dist/utils/adapter-cache.d.ts +36 -0
  57. package/dist/utils/adapter-cache.d.ts.map +1 -0
  58. package/dist/utils/event-utils.d.ts +31 -0
  59. package/dist/utils/event-utils.d.ts.map +1 -0
  60. package/dist/utils/git-commit-info.d.ts +10 -0
  61. package/dist/utils/git-commit-info.d.ts.map +1 -0
  62. package/dist/utils/tree-renderer.d.ts +69 -13
  63. package/dist/utils/tree-renderer.d.ts.map +1 -1
  64. package/package.json +3 -2
@@ -0,0 +1,506 @@
1
+ /**
2
+ * @fileoverview Tests for status dashboard command
3
+ *
4
+ * Tests the pure rendering and computation functions.
5
+ * These don't need a database - they operate on plain data structures.
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import type { Cell } from "swarm-mail";
10
+ import type { WorkerStatus, FileLock, EpicInfo, RecentMessage } from "../../src/dashboard.js";
11
+ import {
12
+ computeSummary,
13
+ groupCellsByStatus,
14
+ shortId,
15
+ formatPriority,
16
+ renderSummaryBar,
17
+ renderSectionHeader,
18
+ renderReadySection,
19
+ renderBlockedSection,
20
+ renderActiveSection,
21
+ renderCompletedSection,
22
+ renderWorkersSection,
23
+ renderLocksSection,
24
+ renderEmptyState,
25
+ renderDashboard,
26
+ buildJsonOutput,
27
+ parseStatusArgs,
28
+ SWARM_BANNER,
29
+ type DashboardData,
30
+ } from "./status.js";
31
+
32
+ // ============================================================================
33
+ // Test helpers
34
+ // ============================================================================
35
+
36
+ function makeCell(overrides: Partial<Cell> & { id: string; title: string }): Cell {
37
+ return {
38
+ project_key: "/test/project",
39
+ type: "task",
40
+ status: "open",
41
+ description: null,
42
+ priority: 2,
43
+ parent_id: null,
44
+ assignee: null,
45
+ created_at: Date.now(),
46
+ updated_at: Date.now(),
47
+ closed_at: null,
48
+ closed_reason: null,
49
+ deleted_at: null,
50
+ deleted_by: null,
51
+ delete_reason: null,
52
+ created_by: null,
53
+ ...overrides,
54
+ };
55
+ }
56
+
57
+ function makeDashboardData(overrides?: Partial<DashboardData>): DashboardData {
58
+ return {
59
+ cells: [],
60
+ epics: [],
61
+ workers: [],
62
+ fileLocks: [],
63
+ recentMessages: [],
64
+ ...overrides,
65
+ };
66
+ }
67
+
68
+ // Strip ANSI escape codes for assertion checks
69
+ function stripAnsi(s: string): string {
70
+ // eslint-disable-next-line no-control-regex
71
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
72
+ }
73
+
74
+ // ============================================================================
75
+ // computeSummary
76
+ // ============================================================================
77
+
78
+ describe("computeSummary", () => {
79
+ test("returns zeros for empty array", () => {
80
+ const result = computeSummary([]);
81
+ expect(result).toEqual({
82
+ total: 0,
83
+ completed: 0,
84
+ ready: 0,
85
+ blocked: 0,
86
+ active: 0,
87
+ percentComplete: 0,
88
+ });
89
+ });
90
+
91
+ test("excludes epics from counts", () => {
92
+ const cells: Cell[] = [
93
+ makeCell({ id: "epic-1", title: "Epic", type: "epic", status: "in_progress" }),
94
+ makeCell({ id: "task-1", title: "Task 1", status: "open" }),
95
+ makeCell({ id: "task-2", title: "Task 2", status: "closed" }),
96
+ ];
97
+ const result = computeSummary(cells);
98
+ expect(result.total).toBe(2); // not 3
99
+ expect(result.completed).toBe(1);
100
+ expect(result.percentComplete).toBe(50);
101
+ });
102
+
103
+ test("counts all statuses correctly", () => {
104
+ const cells: Cell[] = [
105
+ makeCell({ id: "t-1", title: "Open 1", status: "open" }),
106
+ makeCell({ id: "t-2", title: "Open 2", status: "open" }),
107
+ makeCell({ id: "t-3", title: "Blocked", status: "blocked" }),
108
+ makeCell({ id: "t-4", title: "Active", status: "in_progress" }),
109
+ makeCell({ id: "t-5", title: "Done 1", status: "closed" }),
110
+ makeCell({ id: "t-6", title: "Done 2", status: "closed" }),
111
+ makeCell({ id: "t-7", title: "Done 3", status: "closed" }),
112
+ ];
113
+ const result = computeSummary(cells);
114
+ expect(result.total).toBe(7);
115
+ expect(result.completed).toBe(3);
116
+ expect(result.ready).toBe(2);
117
+ expect(result.blocked).toBe(1);
118
+ expect(result.active).toBe(1);
119
+ expect(result.percentComplete).toBe(43); // 3/7 = 42.8 => 43
120
+ });
121
+ });
122
+
123
+ // ============================================================================
124
+ // groupCellsByStatus
125
+ // ============================================================================
126
+
127
+ describe("groupCellsByStatus", () => {
128
+ test("groups cells into correct buckets", () => {
129
+ const cells: Cell[] = [
130
+ makeCell({ id: "epic-1", title: "Epic", type: "epic", status: "in_progress" }),
131
+ makeCell({ id: "t-1", title: "Ready", status: "open", priority: 1 }),
132
+ makeCell({ id: "t-2", title: "Blocked", status: "blocked" }),
133
+ makeCell({ id: "t-3", title: "Active", status: "in_progress" }),
134
+ makeCell({ id: "t-4", title: "Done", status: "closed", closed_at: 1000 }),
135
+ ];
136
+
137
+ const groups = groupCellsByStatus(cells);
138
+ expect(groups.ready.length).toBe(1);
139
+ expect(groups.blocked.length).toBe(1);
140
+ expect(groups.active.length).toBe(1);
141
+ expect(groups.recentlyCompleted.length).toBe(1);
142
+ });
143
+
144
+ test("excludes epics from all groups", () => {
145
+ const cells: Cell[] = [
146
+ makeCell({ id: "epic-1", title: "Epic", type: "epic", status: "open" }),
147
+ ];
148
+ const groups = groupCellsByStatus(cells);
149
+ expect(groups.ready.length).toBe(0);
150
+ expect(groups.blocked.length).toBe(0);
151
+ expect(groups.active.length).toBe(0);
152
+ expect(groups.recentlyCompleted.length).toBe(0);
153
+ });
154
+
155
+ test("sorts ready by priority (ascending)", () => {
156
+ const cells: Cell[] = [
157
+ makeCell({ id: "t-1", title: "Low prio", status: "open", priority: 3 }),
158
+ makeCell({ id: "t-2", title: "High prio", status: "open", priority: 0 }),
159
+ makeCell({ id: "t-3", title: "Med prio", status: "open", priority: 1 }),
160
+ ];
161
+ const groups = groupCellsByStatus(cells);
162
+ expect(groups.ready[0].title).toBe("High prio");
163
+ expect(groups.ready[1].title).toBe("Med prio");
164
+ expect(groups.ready[2].title).toBe("Low prio");
165
+ });
166
+
167
+ test("limits recently completed to 5", () => {
168
+ const cells: Cell[] = Array.from({ length: 10 }, (_, i) =>
169
+ makeCell({
170
+ id: `t-${i}`,
171
+ title: `Done ${i}`,
172
+ status: "closed",
173
+ closed_at: i * 100,
174
+ }),
175
+ );
176
+ const groups = groupCellsByStatus(cells);
177
+ expect(groups.recentlyCompleted.length).toBe(5);
178
+ });
179
+ });
180
+
181
+ // ============================================================================
182
+ // shortId
183
+ // ============================================================================
184
+
185
+ describe("shortId", () => {
186
+ test("extracts last segment of cell ID", () => {
187
+ expect(shortId("cell--al4e8-mkuapgxru3p")).toBe("mkuapgx");
188
+ });
189
+
190
+ test("handles short IDs", () => {
191
+ expect(shortId("abc")).toBe("abc");
192
+ });
193
+
194
+ test("handles IDs with many segments", () => {
195
+ expect(shortId("a-b-c-defghijkl")).toBe("defghij");
196
+ });
197
+ });
198
+
199
+ // ============================================================================
200
+ // formatPriority
201
+ // ============================================================================
202
+
203
+ describe("formatPriority", () => {
204
+ test("formats P0-P3 with different styles", () => {
205
+ expect(stripAnsi(formatPriority(0))).toBe("P0");
206
+ expect(stripAnsi(formatPriority(1))).toBe("P1");
207
+ expect(stripAnsi(formatPriority(2))).toBe("P2");
208
+ expect(stripAnsi(formatPriority(3))).toBe("P3");
209
+ expect(stripAnsi(formatPriority(5))).toBe("P5");
210
+ });
211
+ });
212
+
213
+ // ============================================================================
214
+ // Render functions
215
+ // ============================================================================
216
+
217
+ describe("renderSummaryBar", () => {
218
+ test("renders stats with labels", () => {
219
+ const summary = {
220
+ total: 10,
221
+ completed: 5,
222
+ ready: 3,
223
+ blocked: 1,
224
+ active: 1,
225
+ percentComplete: 50,
226
+ };
227
+ const output = stripAnsi(renderSummaryBar(summary));
228
+ expect(output).toContain("50%");
229
+ expect(output).toContain("3");
230
+ expect(output).toContain("1");
231
+ expect(output).toContain("complete");
232
+ expect(output).toContain("ready");
233
+ expect(output).toContain("blocked");
234
+ expect(output).toContain("active");
235
+ });
236
+ });
237
+
238
+ describe("renderSectionHeader", () => {
239
+ test("renders title with count", () => {
240
+ const output = stripAnsi(renderSectionHeader("Ready to Work", 5));
241
+ expect(output).toContain("Ready to Work (5)");
242
+ expect(output).toContain("─");
243
+ });
244
+
245
+ test("renders title without count", () => {
246
+ const output = stripAnsi(renderSectionHeader("Workers"));
247
+ expect(output).toContain("Workers");
248
+ expect(output).not.toContain("(");
249
+ });
250
+ });
251
+
252
+ describe("renderReadySection", () => {
253
+ test("shows empty message when no cells", () => {
254
+ const output = stripAnsi(renderReadySection([]));
255
+ expect(output).toContain("Ready to Work (0)");
256
+ expect(output).toContain("No tasks ready");
257
+ });
258
+
259
+ test("shows cells with priority", () => {
260
+ const cells: Cell[] = [
261
+ makeCell({ id: "cell--a-bbbbbbb", title: "Do the thing", priority: 1 }),
262
+ ];
263
+ const output = stripAnsi(renderReadySection(cells));
264
+ expect(output).toContain("[ ]");
265
+ expect(output).toContain("Do the thing");
266
+ expect(output).toContain("P1");
267
+ });
268
+
269
+ test("truncates at 8 items", () => {
270
+ const cells: Cell[] = Array.from({ length: 12 }, (_, i) =>
271
+ makeCell({ id: `t-${i}`, title: `Task ${i}`, priority: 2 }),
272
+ );
273
+ const output = stripAnsi(renderReadySection(cells));
274
+ expect(output).toContain("... and 4 more");
275
+ });
276
+ });
277
+
278
+ describe("renderBlockedSection", () => {
279
+ test("returns empty string when no blocked cells", () => {
280
+ expect(renderBlockedSection([])).toBe("");
281
+ });
282
+
283
+ test("shows blocked cells with blocker reference", () => {
284
+ const cells: Cell[] = [
285
+ makeCell({
286
+ id: "cell--a-blockedone",
287
+ title: "Deploy to prod",
288
+ status: "blocked",
289
+ parent_id: "cell--a-blocker1",
290
+ }),
291
+ ];
292
+ const output = stripAnsi(renderBlockedSection(cells));
293
+ expect(output).toContain("[!]");
294
+ expect(output).toContain("Deploy to prod");
295
+ expect(output).toContain("[B:");
296
+ });
297
+ });
298
+
299
+ describe("renderActiveSection", () => {
300
+ test("returns empty string when no active cells", () => {
301
+ expect(renderActiveSection([])).toBe("");
302
+ });
303
+
304
+ test("shows active cells with assignee", () => {
305
+ const cells: Cell[] = [
306
+ makeCell({
307
+ id: "cell--a-active01",
308
+ title: "Implement auth",
309
+ status: "in_progress",
310
+ assignee: "worker-1",
311
+ }),
312
+ ];
313
+ const output = stripAnsi(renderActiveSection(cells));
314
+ expect(output).toContain("▶");
315
+ expect(output).toContain("Implement auth");
316
+ expect(output).toContain("[worker-1]");
317
+ });
318
+ });
319
+
320
+ describe("renderCompletedSection", () => {
321
+ test("returns empty string when no completed cells", () => {
322
+ expect(renderCompletedSection([])).toBe("");
323
+ });
324
+
325
+ test("shows completed cells with checkmark", () => {
326
+ const cells: Cell[] = [
327
+ makeCell({ id: "cell--a-done001", title: "Setup schema", status: "closed" }),
328
+ ];
329
+ const output = stripAnsi(renderCompletedSection(cells));
330
+ expect(output).toContain("[✓]");
331
+ expect(output).toContain("Setup schema");
332
+ });
333
+ });
334
+
335
+ describe("renderWorkersSection", () => {
336
+ test("returns empty string when no workers", () => {
337
+ expect(renderWorkersSection([])).toBe("");
338
+ });
339
+
340
+ test("shows workers with status icons", () => {
341
+ const workers: WorkerStatus[] = [
342
+ { agent_name: "worker-1", status: "working", current_task: "cell--a-task001", last_activity: new Date().toISOString() },
343
+ { agent_name: "worker-2", status: "idle", last_activity: new Date().toISOString() },
344
+ { agent_name: "worker-3", status: "blocked", current_task: "cell--a-task002", last_activity: new Date().toISOString() },
345
+ ];
346
+ const output = stripAnsi(renderWorkersSection(workers));
347
+ expect(output).toContain("worker-1");
348
+ expect(output).toContain("(working)");
349
+ expect(output).toContain("worker-2");
350
+ expect(output).toContain("(idle)");
351
+ expect(output).toContain("worker-3");
352
+ expect(output).toContain("(blocked)");
353
+ });
354
+ });
355
+
356
+ describe("renderLocksSection", () => {
357
+ test("returns empty string when no locks", () => {
358
+ expect(renderLocksSection([])).toBe("");
359
+ });
360
+
361
+ test("shows file locks", () => {
362
+ const locks: FileLock[] = [
363
+ { path: "src/auth.ts", agent_name: "worker-1", reason: "editing", acquired_at: new Date().toISOString(), ttl_seconds: 300 },
364
+ ];
365
+ const output = stripAnsi(renderLocksSection(locks));
366
+ expect(output).toContain("src/auth.ts");
367
+ expect(output).toContain("worker-1");
368
+ });
369
+ });
370
+
371
+ // ============================================================================
372
+ // renderEmptyState
373
+ // ============================================================================
374
+
375
+ describe("renderEmptyState", () => {
376
+ test("shows banner and getting-started info", () => {
377
+ const output = stripAnsi(renderEmptyState());
378
+ // ASCII art banner contains "/ __" (the S in SWARM)
379
+ expect(output).toContain("/ __");
380
+ expect(output).toContain("No swarm activity");
381
+ expect(output).toContain("swarm setup");
382
+ expect(output).toContain("swarm init");
383
+ });
384
+ });
385
+
386
+ // ============================================================================
387
+ // renderDashboard (integration of all sections)
388
+ // ============================================================================
389
+
390
+ describe("renderDashboard", () => {
391
+ test("shows empty state for no cells", () => {
392
+ const data = makeDashboardData();
393
+ const output = stripAnsi(renderDashboard(data));
394
+ expect(output).toContain("No swarm activity");
395
+ });
396
+
397
+ test("renders full dashboard with mixed cells", () => {
398
+ const data = makeDashboardData({
399
+ cells: [
400
+ makeCell({ id: "t-1", title: "Open task", status: "open", priority: 1 }),
401
+ makeCell({ id: "t-2", title: "Blocked task", status: "blocked", priority: 2 }),
402
+ makeCell({ id: "t-3", title: "Active task", status: "in_progress", priority: 2 }),
403
+ makeCell({ id: "t-4", title: "Done task", status: "closed", closed_at: 1000 }),
404
+ ],
405
+ workers: [
406
+ { agent_name: "w-1", status: "working", current_task: "t-3", last_activity: new Date().toISOString() },
407
+ ],
408
+ });
409
+
410
+ const output = stripAnsi(renderDashboard(data));
411
+
412
+ // Banner (ASCII art)
413
+ expect(output).toContain("/ __");
414
+
415
+ // Summary stats
416
+ expect(output).toContain("25%"); // 1 of 4 complete
417
+ expect(output).toContain("complete");
418
+
419
+ // Sections
420
+ expect(output).toContain("In Progress");
421
+ expect(output).toContain("Active task");
422
+ expect(output).toContain("Ready to Work");
423
+ expect(output).toContain("Open task");
424
+ expect(output).toContain("Blocked");
425
+ expect(output).toContain("Blocked task");
426
+ expect(output).toContain("Workers");
427
+ expect(output).toContain("Recently Completed");
428
+ expect(output).toContain("Done task");
429
+ });
430
+
431
+ test("skips empty sections", () => {
432
+ const data = makeDashboardData({
433
+ cells: [
434
+ makeCell({ id: "t-1", title: "Open task", status: "open" }),
435
+ ],
436
+ });
437
+ const output = stripAnsi(renderDashboard(data));
438
+ // These sections should not appear (no cells in those states)
439
+ expect(output).not.toContain("In Progress");
440
+ expect(output).not.toContain("Blocked");
441
+ expect(output).not.toContain("Workers");
442
+ expect(output).not.toContain("Recently Completed");
443
+ });
444
+ });
445
+
446
+ // ============================================================================
447
+ // buildJsonOutput
448
+ // ============================================================================
449
+
450
+ describe("buildJsonOutput", () => {
451
+ test("returns structured JSON with summary", () => {
452
+ const data = makeDashboardData({
453
+ cells: [
454
+ makeCell({ id: "t-1", title: "Open", status: "open", priority: 1 }),
455
+ makeCell({ id: "t-2", title: "Done", status: "closed" }),
456
+ ],
457
+ });
458
+
459
+ const json = buildJsonOutput(data) as any;
460
+
461
+ expect(json.summary).toBeDefined();
462
+ expect(json.summary.total).toBe(2);
463
+ expect(json.summary.completed).toBe(1);
464
+ expect(json.summary.percentComplete).toBe(50);
465
+
466
+ expect(json.ready).toHaveLength(1);
467
+ expect(json.ready[0].id).toBe("t-1");
468
+ expect(json.ready[0].title).toBe("Open");
469
+
470
+ expect(json.recentlyCompleted).toHaveLength(1);
471
+ expect(json.recentlyCompleted[0].id).toBe("t-2");
472
+ });
473
+
474
+ test("includes workers and file locks", () => {
475
+ const data = makeDashboardData({
476
+ workers: [
477
+ { agent_name: "w-1", status: "working", current_task: "t-1", last_activity: "2024-01-01T00:00:00Z" },
478
+ ],
479
+ fileLocks: [
480
+ { path: "src/auth.ts", agent_name: "w-1", reason: "editing", acquired_at: "2024-01-01T00:00:00Z", ttl_seconds: 300 },
481
+ ],
482
+ });
483
+
484
+ const json = buildJsonOutput(data) as any;
485
+ expect(json.workers).toHaveLength(1);
486
+ expect(json.fileLocks).toHaveLength(1);
487
+ });
488
+ });
489
+
490
+ // ============================================================================
491
+ // parseStatusArgs
492
+ // ============================================================================
493
+
494
+ describe("parseStatusArgs", () => {
495
+ test("parses --json flag", () => {
496
+ expect(parseStatusArgs(["--json"])).toEqual({ json: true });
497
+ });
498
+
499
+ test("returns defaults for no args", () => {
500
+ expect(parseStatusArgs([])).toEqual({});
501
+ });
502
+
503
+ test("ignores unknown flags", () => {
504
+ expect(parseStatusArgs(["--foo", "bar"])).toEqual({});
505
+ });
506
+ });