claude-code-swarm 0.3.5 → 0.3.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.
Files changed (42) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.claude-plugin/run-agent-inbox-mcp.sh +22 -3
  4. package/.gitattributes +3 -0
  5. package/.opentasks/config.json +9 -0
  6. package/.opentasks/graph.jsonl +0 -0
  7. package/e2e/helpers/opentasks-daemon.mjs +149 -0
  8. package/e2e/tier6-live-inbox-flow.test.mjs +938 -0
  9. package/e2e/tier7-hooks.test.mjs +992 -0
  10. package/e2e/tier7-minimem.test.mjs +461 -0
  11. package/e2e/tier7-opentasks.test.mjs +513 -0
  12. package/e2e/tier7-skilltree.test.mjs +506 -0
  13. package/e2e/vitest.config.e2e.mjs +1 -1
  14. package/package.json +6 -2
  15. package/references/agent-inbox/package-lock.json +2 -2
  16. package/references/agent-inbox/package.json +1 -1
  17. package/references/agent-inbox/src/index.ts +16 -2
  18. package/references/agent-inbox/src/ipc/ipc-server.ts +58 -0
  19. package/references/agent-inbox/src/mcp/mcp-proxy.ts +326 -0
  20. package/references/agent-inbox/src/types.ts +26 -0
  21. package/references/agent-inbox/test/ipc-new-commands.test.ts +200 -0
  22. package/references/agent-inbox/test/mcp-proxy.test.ts +191 -0
  23. package/references/minimem/package-lock.json +2 -2
  24. package/references/minimem/package.json +1 -1
  25. package/scripts/bootstrap.mjs +8 -1
  26. package/scripts/map-hook.mjs +6 -2
  27. package/scripts/map-sidecar.mjs +19 -0
  28. package/scripts/team-loader.mjs +15 -8
  29. package/skills/swarm/SKILL.md +16 -22
  30. package/src/__tests__/agent-generator.test.mjs +9 -10
  31. package/src/__tests__/context-output.test.mjs +13 -14
  32. package/src/__tests__/e2e-inbox-integration.test.mjs +732 -0
  33. package/src/__tests__/e2e-live-inbox.test.mjs +597 -0
  34. package/src/__tests__/inbox-integration.test.mjs +298 -0
  35. package/src/__tests__/integration.test.mjs +12 -11
  36. package/src/__tests__/skilltree-client.test.mjs +47 -1
  37. package/src/agent-generator.mjs +79 -88
  38. package/src/bootstrap.mjs +24 -3
  39. package/src/context-output.mjs +238 -64
  40. package/src/index.mjs +2 -0
  41. package/src/sidecar-server.mjs +30 -0
  42. package/src/skilltree-client.mjs +50 -5
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Tier 7: Minimem Integration Tests
3
+ *
4
+ * Tests the minimem MCP server and integration without LLM calls:
5
+ * 1. MCP server starts and responds to tool calls
6
+ * 2. Memory search returns results for seeded files
7
+ * 3. AGENT.md generation includes minimem tool instructions
8
+ * 4. Bootstrap context output includes memory capability section
9
+ * 5. MCP wrapper script exits cleanly when disabled
10
+ *
11
+ * No LLM calls — exercises the MCP server, agent generation, and context output.
12
+ *
13
+ * Run:
14
+ * npx vitest run --config e2e/vitest.config.e2e.mjs e2e/tier7-minimem.test.mjs
15
+ */
16
+
17
+ import { describe, it, expect, afterEach } from "vitest";
18
+ import fs from "fs";
19
+ import path from "path";
20
+ import { spawn } from "child_process";
21
+ import { fileURLToPath } from "url";
22
+ import { createWorkspace } from "./helpers/workspace.mjs";
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+ const PLUGIN_DIR = path.resolve(__dirname, "..");
26
+ const SHORT_TMPDIR = "/tmp";
27
+
28
+ // Check if minimem is available
29
+ let minimemAvailable = false;
30
+ try {
31
+ await import("minimem");
32
+ minimemAvailable = true;
33
+ } catch {
34
+ // Not installed
35
+ }
36
+
37
+ // Import source modules for direct testing
38
+ const { buildCapabilitiesContext } = await import("../src/context-output.mjs");
39
+ const { generateAgentMd } = await import("../src/agent-generator.mjs");
40
+
41
+ /**
42
+ * Start a minimem MCP server process and return a handle for JSON-RPC communication.
43
+ */
44
+ function startMinimemMcp(memoryDir, options = {}) {
45
+ return new Promise((resolve, reject) => {
46
+ const args = ["mcp", "--dir", memoryDir, "--provider", options.provider || "none"];
47
+ if (options.global) args.push("--global");
48
+
49
+ const child = spawn("minimem", args, {
50
+ stdio: ["pipe", "pipe", "pipe"],
51
+ env: { ...process.env },
52
+ });
53
+
54
+ let stderr = "";
55
+ child.stderr.on("data", (d) => { stderr += d.toString(); });
56
+
57
+ // MCP uses stdin/stdout JSON-RPC — give it a moment to start
58
+ const timer = setTimeout(() => {
59
+ resolve({
60
+ child,
61
+ stderr: () => stderr,
62
+ send: (request) => sendMcpRequest(child, request),
63
+ cleanup: () => { child.kill(); },
64
+ });
65
+ }, 1000);
66
+
67
+ child.on("error", (err) => {
68
+ clearTimeout(timer);
69
+ reject(err);
70
+ });
71
+
72
+ child.on("exit", (code) => {
73
+ clearTimeout(timer);
74
+ if (code !== null && code !== 0) {
75
+ reject(new Error(`minimem mcp exited with code ${code}: ${stderr}`));
76
+ }
77
+ });
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Send a JSON-RPC request to the MCP server via stdin and read response from stdout.
83
+ */
84
+ function sendMcpRequest(child, request) {
85
+ return new Promise((resolve) => {
86
+ const id = request.id || Date.now();
87
+ const payload = JSON.stringify({
88
+ jsonrpc: "2.0",
89
+ id,
90
+ ...request,
91
+ });
92
+
93
+ let buffer = "";
94
+ const onData = (data) => {
95
+ buffer += data.toString();
96
+ // MCP uses Content-Length header framing or newline-delimited JSON
97
+ const lines = buffer.split("\n");
98
+ for (const line of lines) {
99
+ if (!line.trim()) continue;
100
+ try {
101
+ const response = JSON.parse(line);
102
+ if (response.id === id) {
103
+ child.stdout.removeListener("data", onData);
104
+ clearTimeout(timer);
105
+ resolve(response);
106
+ return;
107
+ }
108
+ } catch {
109
+ // Not complete JSON yet, or header line
110
+ }
111
+ }
112
+ };
113
+
114
+ child.stdout.on("data", onData);
115
+ child.stdin.write(payload + "\n");
116
+
117
+ const timer = setTimeout(() => {
118
+ child.stdout.removeListener("data", onData);
119
+ resolve(null); // Timeout
120
+ }, 5000);
121
+ });
122
+ }
123
+
124
+ // ─────────────────────────────────────────────────────────────────────────────
125
+ // Group 1: Context Output — buildCapabilitiesContext includes minimem
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+
128
+ describe(
129
+ "tier7: minimem context output",
130
+ { timeout: 30_000 },
131
+ () => {
132
+ it("buildCapabilitiesContext includes memory section when enabled + ready", () => {
133
+ const context = buildCapabilitiesContext({
134
+ minimemEnabled: true,
135
+ minimemStatus: "ready",
136
+ });
137
+
138
+ expect(context).toContain("### Memory");
139
+ expect(context).toContain("minimem__memory_search");
140
+ expect(context).toContain("minimem__memory_get_details");
141
+ expect(context).toContain("minimem__knowledge_search");
142
+ expect(context).toContain("minimem__knowledge_graph");
143
+ expect(context).toContain("minimem__knowledge_path");
144
+ });
145
+
146
+ it("buildCapabilitiesContext shows status when installed but not ready", () => {
147
+ const context = buildCapabilitiesContext({
148
+ minimemEnabled: true,
149
+ minimemStatus: "installed",
150
+ });
151
+
152
+ expect(context).toContain("### Memory");
153
+ expect(context).toContain("installed");
154
+ // Should not show full tool instructions
155
+ expect(context).not.toContain("minimem__memory_search");
156
+ });
157
+
158
+ it("buildCapabilitiesContext omits memory section when disabled", () => {
159
+ const context = buildCapabilitiesContext({
160
+ minimemEnabled: false,
161
+ minimemStatus: "disabled",
162
+ });
163
+
164
+ expect(context).not.toContain("### Memory");
165
+ expect(context).not.toContain("minimem");
166
+ });
167
+
168
+ it("main agent context includes storage instructions", () => {
169
+ const context = buildCapabilitiesContext({
170
+ role: null, // main agent
171
+ minimemEnabled: true,
172
+ minimemStatus: "ready",
173
+ });
174
+
175
+ expect(context).toContain("Storing memories");
176
+ expect(context).toContain("MEMORY.md");
177
+ expect(context).toContain("Team strategy");
178
+ });
179
+
180
+ it("spawned agent context omits storage section but has search guidance", () => {
181
+ const context = buildCapabilitiesContext({
182
+ role: "executor",
183
+ teamName: "gsd",
184
+ minimemEnabled: true,
185
+ minimemStatus: "ready",
186
+ });
187
+
188
+ expect(context).not.toContain("Storing memories");
189
+ expect(context).toContain("Before major work");
190
+ expect(context).toContain("After completing work");
191
+ });
192
+ }
193
+ );
194
+
195
+ // ─────────────────────────────────────────────────────────────────────────────
196
+ // Group 2: Agent Generation — AGENT.md includes minimem tools
197
+ // ─────────────────────────────────────────────────────────────────────────────
198
+
199
+ describe(
200
+ "tier7: minimem agent generation",
201
+ { timeout: 30_000 },
202
+ () => {
203
+ it("generateAgentMd includes minimem capabilities when enabled", () => {
204
+ const md = generateAgentMd({
205
+ roleName: "executor",
206
+ teamName: "gsd",
207
+ position: "spawned",
208
+ description: "Executor agent",
209
+ tools: ["Read", "Write", "Bash"],
210
+ skillContent: "# Role: executor\n\nTest content.",
211
+ manifest: {},
212
+ minimemEnabled: true,
213
+ minimemStatus: "ready",
214
+ });
215
+
216
+ expect(md).toContain("### Memory");
217
+ expect(md).toContain("minimem__memory_search");
218
+ });
219
+
220
+ it("generateAgentMd omits minimem when disabled", () => {
221
+ const md = generateAgentMd({
222
+ roleName: "executor",
223
+ teamName: "gsd",
224
+ position: "spawned",
225
+ description: "Executor agent",
226
+ tools: ["Read", "Write", "Bash"],
227
+ skillContent: "# Role: executor\n\nTest content.",
228
+ manifest: {},
229
+ minimemEnabled: false,
230
+ });
231
+
232
+ expect(md).not.toContain("### Memory");
233
+ expect(md).not.toContain("minimem");
234
+ });
235
+ }
236
+ );
237
+
238
+ // ─────────────────────────────────────────────────────────────────────────────
239
+ // Group 3: MCP Wrapper Script — exits cleanly when disabled
240
+ // ─────────────────────────────────────────────────────────────────────────────
241
+
242
+ describe(
243
+ "tier7: minimem MCP wrapper script",
244
+ { timeout: 15_000 },
245
+ () => {
246
+ let workspace;
247
+
248
+ afterEach(() => {
249
+ if (workspace) {
250
+ workspace.cleanup();
251
+ workspace = null;
252
+ }
253
+ });
254
+
255
+ it("run-minimem-mcp.sh exits 0 when minimem is disabled", async () => {
256
+ workspace = createWorkspace({
257
+ tmpdir: SHORT_TMPDIR,
258
+ prefix: "t7-mm-script-",
259
+ config: {
260
+ template: "gsd",
261
+ minimem: { enabled: false },
262
+ },
263
+ });
264
+
265
+ const result = await new Promise((resolve) => {
266
+ const child = spawn("bash", [
267
+ path.join(PLUGIN_DIR, ".claude-plugin", "run-minimem-mcp.sh"),
268
+ ], {
269
+ cwd: workspace.dir,
270
+ stdio: ["ignore", "pipe", "pipe"],
271
+ });
272
+
273
+ let stdout = "";
274
+ let stderr = "";
275
+ child.stdout.on("data", (d) => { stdout += d.toString(); });
276
+ child.stderr.on("data", (d) => { stderr += d.toString(); });
277
+
278
+ child.on("close", (code) => {
279
+ resolve({ code, stdout, stderr });
280
+ });
281
+
282
+ setTimeout(() => {
283
+ child.kill();
284
+ resolve({ code: -1, stdout, stderr });
285
+ }, 10000);
286
+ });
287
+
288
+ expect(result.code).toBe(0);
289
+ });
290
+
291
+ it("run-minimem-mcp.sh exits 0 when no config file exists", async () => {
292
+ workspace = createWorkspace({
293
+ tmpdir: SHORT_TMPDIR,
294
+ prefix: "t7-mm-noconf-",
295
+ // No config at all
296
+ });
297
+
298
+ const result = await new Promise((resolve) => {
299
+ const child = spawn("bash", [
300
+ path.join(PLUGIN_DIR, ".claude-plugin", "run-minimem-mcp.sh"),
301
+ ], {
302
+ cwd: workspace.dir,
303
+ stdio: ["ignore", "pipe", "pipe"],
304
+ });
305
+
306
+ let stdout = "";
307
+ let stderr = "";
308
+ child.stdout.on("data", (d) => { stdout += d.toString(); });
309
+ child.stderr.on("data", (d) => { stderr += d.toString(); });
310
+
311
+ child.on("close", (code) => {
312
+ resolve({ code, stdout, stderr });
313
+ });
314
+
315
+ setTimeout(() => {
316
+ child.kill();
317
+ resolve({ code: -1, stdout, stderr });
318
+ }, 10000);
319
+ });
320
+
321
+ expect(result.code).toBe(0);
322
+ });
323
+ }
324
+ );
325
+
326
+ // ─────────────────────────────────────────────────────────────────────────────
327
+ // Group 4: MCP Server — starts and responds to tool listing
328
+ // ─────────────────────────────────────────────────────────────────────────────
329
+
330
+ describe.skipIf(!minimemAvailable)(
331
+ "tier7: minimem MCP server",
332
+ { timeout: 30_000 },
333
+ () => {
334
+ let workspace;
335
+ let mcpHandle;
336
+
337
+ afterEach(async () => {
338
+ if (mcpHandle) {
339
+ mcpHandle.cleanup();
340
+ mcpHandle = null;
341
+ }
342
+ if (workspace) {
343
+ workspace.cleanup();
344
+ workspace = null;
345
+ }
346
+ });
347
+
348
+ it("minimem mcp process starts without error", async () => {
349
+ workspace = createWorkspace({
350
+ tmpdir: SHORT_TMPDIR,
351
+ prefix: "t7-mm-mcp-",
352
+ config: { template: "gsd", minimem: { enabled: true } },
353
+ });
354
+
355
+ // Create memory dir
356
+ const memDir = path.join(workspace.dir, ".swarm", "minimem");
357
+ fs.mkdirSync(memDir, { recursive: true });
358
+
359
+ mcpHandle = await startMinimemMcp(memDir);
360
+ expect(mcpHandle.child.pid).toBeGreaterThan(0);
361
+
362
+ // Process should still be alive
363
+ expect(mcpHandle.child.exitCode).toBeNull();
364
+ });
365
+
366
+ it("memory search on empty store returns empty results", async () => {
367
+ workspace = createWorkspace({
368
+ tmpdir: SHORT_TMPDIR,
369
+ prefix: "t7-mm-search-",
370
+ config: { template: "gsd", minimem: { enabled: true } },
371
+ });
372
+
373
+ const memDir = path.join(workspace.dir, ".swarm", "minimem");
374
+ fs.mkdirSync(memDir, { recursive: true });
375
+
376
+ mcpHandle = await startMinimemMcp(memDir);
377
+
378
+ // Send MCP initialize first
379
+ const initResp = await mcpHandle.send({
380
+ method: "initialize",
381
+ params: {
382
+ protocolVersion: "2024-11-05",
383
+ capabilities: {},
384
+ clientInfo: { name: "test", version: "1.0" },
385
+ },
386
+ });
387
+ console.log("[tier7] MCP init response:", JSON.stringify(initResp));
388
+
389
+ if (initResp) {
390
+ // List tools to verify memory_search is available
391
+ const toolsResp = await mcpHandle.send({
392
+ method: "tools/list",
393
+ params: {},
394
+ });
395
+ console.log("[tier7] MCP tools:", JSON.stringify(toolsResp?.result?.tools?.map(t => t.name)));
396
+
397
+ if (toolsResp?.result?.tools) {
398
+ const toolNames = toolsResp.result.tools.map((t) => t.name);
399
+ expect(toolNames).toContain("memory_search");
400
+ }
401
+ }
402
+ });
403
+
404
+ it("seeded memory file is findable via search", async () => {
405
+ workspace = createWorkspace({
406
+ tmpdir: SHORT_TMPDIR,
407
+ prefix: "t7-mm-seed-",
408
+ config: { template: "gsd", minimem: { enabled: true } },
409
+ });
410
+
411
+ const memDir = path.join(workspace.dir, ".swarm", "minimem");
412
+ fs.mkdirSync(memDir, { recursive: true });
413
+
414
+ // Seed a memory file
415
+ fs.writeFileSync(
416
+ path.join(memDir, "test-decision.md"),
417
+ [
418
+ "### 2026-03-18 10:00",
419
+ "<!-- type: decision -->",
420
+ "We decided to use PostgreSQL for the user database because of its",
421
+ "strong JSON support and concurrent write performance.",
422
+ ].join("\n")
423
+ );
424
+
425
+ mcpHandle = await startMinimemMcp(memDir);
426
+
427
+ // Initialize MCP
428
+ const initResp = await mcpHandle.send({
429
+ method: "initialize",
430
+ params: {
431
+ protocolVersion: "2024-11-05",
432
+ capabilities: {},
433
+ clientInfo: { name: "test", version: "1.0" },
434
+ },
435
+ });
436
+
437
+ if (!initResp) {
438
+ console.log("[tier7] MCP init failed — skipping search test");
439
+ return;
440
+ }
441
+
442
+ // Search for the seeded content
443
+ const searchResp = await mcpHandle.send({
444
+ method: "tools/call",
445
+ params: {
446
+ name: "memory_search",
447
+ arguments: { query: "PostgreSQL database decision" },
448
+ },
449
+ });
450
+
451
+ console.log("[tier7] search response:", JSON.stringify(searchResp?.result));
452
+
453
+ if (searchResp?.result) {
454
+ // Should find the seeded file
455
+ const content = JSON.stringify(searchResp.result);
456
+ const found = content.includes("PostgreSQL") || content.includes("test-decision");
457
+ console.log("[tier7] found seeded memory:", found);
458
+ }
459
+ });
460
+ }
461
+ );