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,191 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as net from "node:net";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { EventEmitter } from "node:events";
6
+ import { InMemoryStorage } from "../src/storage/memory.js";
7
+ import { MessageRouter } from "../src/router/message-router.js";
8
+ import { IpcServer } from "../src/ipc/ipc-server.js";
9
+ import { InboxMcpProxy } from "../src/mcp/mcp-proxy.js";
10
+
11
+ function tmpSocketPath(): string {
12
+ return path.join(os.tmpdir(), `inbox-proxy-test-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`);
13
+ }
14
+
15
+ /**
16
+ * Helper: directly send an IPC command (bypass proxy, used for setup/verification).
17
+ */
18
+ function sendIpc(socketPath: string, command: object): Promise<Record<string, unknown>> {
19
+ return new Promise((resolve, reject) => {
20
+ const client = net.createConnection(socketPath, () => {
21
+ client.write(JSON.stringify(command) + "\n");
22
+ });
23
+ let buffer = "";
24
+ client.on("data", (data) => {
25
+ buffer += data.toString();
26
+ const idx = buffer.indexOf("\n");
27
+ if (idx !== -1) {
28
+ const line = buffer.slice(0, idx);
29
+ client.end();
30
+ resolve(JSON.parse(line));
31
+ }
32
+ });
33
+ client.on("error", reject);
34
+ });
35
+ }
36
+
37
+ describe("InboxMcpProxy", () => {
38
+ let storage: InMemoryStorage;
39
+ let events: EventEmitter;
40
+ let router: MessageRouter;
41
+ let ipcServer: IpcServer;
42
+ let socketPath: string;
43
+ let proxy: InboxMcpProxy;
44
+
45
+ beforeEach(async () => {
46
+ storage = new InMemoryStorage();
47
+ events = new EventEmitter();
48
+ router = new MessageRouter(storage, events, "test-scope");
49
+ socketPath = tmpSocketPath();
50
+ ipcServer = new IpcServer(socketPath, router, storage);
51
+ await ipcServer.start();
52
+ proxy = new InboxMcpProxy(socketPath, "test-agent", "test-scope");
53
+ });
54
+
55
+ afterEach(async () => {
56
+ await ipcServer.stop();
57
+ });
58
+
59
+ it("should be constructable with socket path, agent ID, and scope", () => {
60
+ expect(proxy).toBeDefined();
61
+ expect(proxy.server).toBeDefined();
62
+ });
63
+
64
+ it("should proxy send via IPC and message lands in storage", async () => {
65
+ // Use the proxy's internal sendIpc (test via IPC directly since MCP stdio is hard to test)
66
+ // Instead, verify the proxy's IPC client works by sending directly and checking storage
67
+ const resp = await sendIpc(socketPath, {
68
+ action: "send",
69
+ from: "test-agent",
70
+ to: "recipient",
71
+ payload: "hello from proxy",
72
+ });
73
+ expect(resp.ok).toBe(true);
74
+ expect(resp.messageId).toBeTruthy();
75
+
76
+ // Verify message is in shared storage
77
+ const msg = storage.getMessage(resp.messageId as string);
78
+ expect(msg).toBeDefined();
79
+ expect(msg!.sender_id).toBe("test-agent");
80
+ });
81
+
82
+ it("should see messages sent to the IPC server via check_inbox", async () => {
83
+ // Send a message via IPC
84
+ await sendIpc(socketPath, {
85
+ action: "send",
86
+ from: "external",
87
+ to: "my-agent",
88
+ payload: "you have a task",
89
+ });
90
+
91
+ // Check inbox via IPC (same path proxy would use)
92
+ const resp = await sendIpc(socketPath, {
93
+ action: "check_inbox",
94
+ agentId: "my-agent",
95
+ unreadOnly: true,
96
+ });
97
+
98
+ expect(resp.ok).toBe(true);
99
+ const messages = resp.messages as Array<{ sender_id: string }>;
100
+ expect(messages).toHaveLength(1);
101
+ expect(messages[0].sender_id).toBe("external");
102
+ });
103
+
104
+ it("should read threads via IPC", async () => {
105
+ await sendIpc(socketPath, {
106
+ action: "send",
107
+ from: "alice",
108
+ to: "bob",
109
+ payload: "msg 1",
110
+ threadTag: "thread-1",
111
+ });
112
+ await sendIpc(socketPath, {
113
+ action: "send",
114
+ from: "bob",
115
+ to: "alice",
116
+ payload: "msg 2",
117
+ threadTag: "thread-1",
118
+ });
119
+
120
+ const resp = await sendIpc(socketPath, {
121
+ action: "read_thread",
122
+ threadTag: "thread-1",
123
+ scope: "test-scope",
124
+ });
125
+
126
+ expect(resp.ok).toBe(true);
127
+ expect(resp.count).toBe(2);
128
+ });
129
+
130
+ it("should list agents via IPC", async () => {
131
+ await sendIpc(socketPath, {
132
+ action: "notify",
133
+ event: {
134
+ type: "agent.spawn",
135
+ agent: { agentId: "agent-a", name: "Agent A", scopes: ["test-scope"] },
136
+ },
137
+ });
138
+
139
+ const resp = await sendIpc(socketPath, {
140
+ action: "list_agents",
141
+ });
142
+
143
+ expect(resp.ok).toBe(true);
144
+ expect(resp.count).toBe(1);
145
+ const agents = resp.agents as Array<{ agentId: string }>;
146
+ expect(agents[0].agentId).toBe("agent-a");
147
+ });
148
+
149
+ it("should handle unavailable socket gracefully", async () => {
150
+ const badProxy = new InboxMcpProxy("/tmp/nonexistent-socket.sock", "agent", "default");
151
+ // Access internal sendIpc method indirectly — the proxy shouldn't crash
152
+ // We test this by verifying the class instantiates without error
153
+ expect(badProxy).toBeDefined();
154
+ });
155
+ });
156
+
157
+ describe("InboxMcpProxy default agent ID", () => {
158
+ let storage: InMemoryStorage;
159
+ let events: EventEmitter;
160
+ let router: MessageRouter;
161
+ let ipcServer: IpcServer;
162
+ let socketPath: string;
163
+
164
+ beforeEach(async () => {
165
+ storage = new InMemoryStorage();
166
+ events = new EventEmitter();
167
+ router = new MessageRouter(storage, events, "default");
168
+ socketPath = tmpSocketPath();
169
+ ipcServer = new IpcServer(socketPath, router, storage);
170
+ await ipcServer.start();
171
+ });
172
+
173
+ afterEach(async () => {
174
+ await ipcServer.stop();
175
+ });
176
+
177
+ it("should use default agent ID as sender when from is not specified", async () => {
178
+ // The proxy's defaultAgentId should be used when send_message doesn't specify from.
179
+ // We test this by sending via IPC with the expected default and checking storage.
180
+ const resp = await sendIpc(socketPath, {
181
+ action: "send",
182
+ from: "gsd-executor", // proxy would inject this as default
183
+ to: "observer",
184
+ payload: "status update",
185
+ });
186
+
187
+ expect(resp.ok).toBe(true);
188
+ const msg = storage.getMessage(resp.messageId as string);
189
+ expect(msg!.sender_id).toBe("gsd-executor");
190
+ });
191
+ });
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "minimem",
3
- "version": "0.0.7",
3
+ "version": "0.1.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "minimem",
9
- "version": "0.0.7",
9
+ "version": "0.1.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "chokidar": "^4.0.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimem",
3
- "version": "0.0.7",
3
+ "version": "0.1.0",
4
4
  "description": "A lightweight file-based memory system with vector search for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,11 +37,18 @@ try {
37
37
  const output = formatBootstrapContext({
38
38
  template: result.template,
39
39
  team: result.team,
40
+ mapEnabled: result.mapEnabled,
40
41
  mapStatus: result.mapEnabled ? result.mapStatus : null,
41
42
  sessionlogStatus: result.sessionlogEnabled ? result.sessionlogStatus : null,
42
43
  sessionlogSync: result.sessionlogSync,
43
- opentasksStatus: result.opentasksEnabled ? result.opentasksStatus : null,
44
+ opentasksEnabled: result.opentasksEnabled,
45
+ opentasksStatus: result.opentasksStatus,
44
46
  inboxEnabled: result.inboxEnabled,
47
+ meshEnabled: result.meshEnabled,
48
+ minimemEnabled: result.minimemEnabled,
49
+ minimemStatus: result.minimemStatus,
50
+ skilltreeEnabled: result.skilltreeEnabled,
51
+ skilltreeStatus: result.skilltreeStatus,
45
52
  });
46
53
  process.stdout.write(output);
47
54
  } catch (err) {
@@ -72,10 +72,14 @@ async function handleInject() {
72
72
 
73
73
  if (!config.inbox?.enabled) return;
74
74
 
75
- // Read from agent-inbox IPC
75
+ // Only check messages addressed to the main agent (not all scope messages).
76
+ // Per-agent messages stay in storage for agents to pull via MCP tools.
77
+ const teamName = resolveTeamName(config);
78
+ const mainAgentId = `${teamName}-main`;
76
79
  const scope = config.map?.scope || "default";
80
+
77
81
  const resp = await sendToInbox(
78
- { action: "check_inbox", scope, clear: true },
82
+ { action: "check_inbox", agentId: mainAgentId, scope, unreadOnly: true, clear: true },
79
83
  sPaths.inboxSocketPath
80
84
  );
81
85
  if (!resp || !resp.ok || !resp.messages?.length) return;
@@ -244,6 +244,25 @@ async function main() {
244
244
  await startWebSocketTransport();
245
245
  }
246
246
 
247
+ // Subscribe to inbox message.created events for outbound MAP observability
248
+ if (inboxInstance?.events && connection) {
249
+ inboxInstance.events.on("message.created", (message) => {
250
+ // Emit message event to MAP for external observability (Flows B, E)
251
+ connection.send({ scope: MAP_SCOPE }, {
252
+ type: "inbox.message",
253
+ messageId: message.id,
254
+ from: message.sender_id,
255
+ to: (message.recipients || []).map((r) => r.agent_id),
256
+ contentType: message.content?.type || "text",
257
+ threadTag: message.thread_tag,
258
+ importance: message.importance,
259
+ }, { relationship: "broadcast" }).catch(() => {
260
+ // Best-effort — don't block on MAP delivery
261
+ });
262
+ });
263
+ process.stderr.write("[sidecar] Subscribed to inbox message.created events for MAP bridge\n");
264
+ }
265
+
247
266
  // Start lifecycle UNIX socket server
248
267
  const onCommand = createCommandHandler(connection, MAP_SCOPE, registeredAgents, {
249
268
  inboxInstance,
@@ -20,16 +20,11 @@ import {
20
20
  } from "../src/context-output.mjs";
21
21
 
22
22
  const argTemplate = process.argv[2] || "";
23
+ const config = readConfig();
23
24
 
24
25
  // ── Determine template name ─────────────────────────────────────────────────
25
26
 
26
- let templateName = argTemplate;
27
-
28
- // Fall back to config file
29
- if (!templateName) {
30
- const config = readConfig();
31
- templateName = config.template;
32
- }
27
+ let templateName = argTemplate || config.template;
33
28
 
34
29
  // If no template, show available templates
35
30
  if (!templateName) {
@@ -55,4 +50,16 @@ if (!result.success) {
55
50
 
56
51
  // ── Output context ──────────────────────────────────────────────────────────
57
52
 
58
- process.stdout.write(formatTeamLoadedContext(result.outputDir, result.templatePath, result.teamName));
53
+ process.stdout.write(formatTeamLoadedContext(result.outputDir, result.templatePath, result.teamName, {
54
+ opentasksEnabled: config.opentasks?.enabled,
55
+ opentasksStatus: config.opentasks?.enabled ? "enabled" : "disabled",
56
+ minimemEnabled: config.minimem?.enabled,
57
+ minimemStatus: config.minimem?.enabled ? "ready" : "disabled",
58
+ skilltreeEnabled: config.skilltree?.enabled,
59
+ skilltreeStatus: config.skilltree?.enabled ? "ready" : "disabled",
60
+ inboxEnabled: config.inbox?.enabled,
61
+ meshEnabled: config.mesh?.enabled,
62
+ mapEnabled: config.map?.enabled,
63
+ mapStatus: config.map?.enabled ? "enabled" : "disabled",
64
+ sessionlogSync: config.sessionlog?.sync || "off",
65
+ }));
@@ -102,34 +102,28 @@ When all work is complete:
102
102
 
103
103
  - **You are the only agent that can spawn teammates** — do not instruct agents to spawn other agents
104
104
  - **openteams is config-only** — used only for artifact generation, NOT for runtime coordination
105
- - **Use Claude Code native teams** for all runtime: `TeamCreate`, `TaskCreate`, `TaskUpdate`, `SendMessage`
106
105
  - All agents must be spawned with `team_name` so they share the team's task list
107
- - If MAP is enabled in `.swarm/claude-swarm/config.json`, lifecycle events are handled automatically by hooks
108
106
  - Start with the most critical roles first — you don't need to spawn all roles from the topology at once
109
107
  - Keep team size manageable (3-5 agents) — spawn more only when genuinely needed
110
108
 
111
- ## When minimem is enabled
109
+ ## Capabilities
112
110
 
113
- If minimem is configured (check init context for "Memory: ready"):
114
- - All agents have access to **minimem MCP tools** for searching and storing memories
115
- - Use `minimem__memory_search` to find relevant past decisions, patterns, and context
116
- - Use `minimem__knowledge_search` to search with domain or entity filters
117
- - Memories are shared team-wide — all agents search the same memory store
118
- - Instruct agents to search memory before starting major work for relevant prior context
111
+ Refer to the **Swarm Capabilities** section in the session init context for which tools and integrations are active (task management, memory, communication, observability). The capabilities context is also embedded in each spawned agent's prompt — all agents share the same understanding of available tools.
119
112
 
120
- ## When skill-tree is enabled
113
+ When creating tasks and coordinating agents, use the task tools described in Swarm Capabilities (opentasks MCP tools if opentasks is enabled, native TaskCreate/TaskUpdate otherwise).
121
114
 
122
- If skill-tree is configured (check init context for "Skills: ready"):
123
- - Per-role skill loadouts are compiled from the team.yaml `skilltree:` extension and embedded in agent prompts
124
- - Agents receive their role-appropriate skills automatically in their AGENT.md — no runtime action needed
125
- - Skills are cached per template alongside other artifacts
126
- - To update loadouts, delete the template cache directory and reload
115
+ ### When minimem is enabled
127
116
 
128
- ## When opentasks is enabled
117
+ If memory is active (check init context for "Memory: ready"):
118
+ - **Before spawning agents**: Search memory for prior context on the user's goal (`minimem__memory_search`). Include relevant findings in agent prompts when spawning.
119
+ - **After team completion**: Store key decisions and outcomes in memory files (`MEMORY.md` for decisions, `memory/<topic>.md` for topic context).
120
+ - Tag stored memories with observation types (`<!-- type: decision -->`) and use domain tags relevant to the template (e.g., "gsd", "backend").
121
+ - Memory is shared team-wide — all agents can search the same store during execution.
129
122
 
130
- If opentasks is configured (check init context for "Opentasks: connected"):
131
- - Use **opentasks MCP tools** instead of native `TaskCreate`/`TaskUpdate`/`TaskList`
132
- - `opentasks__create_task` to create tasks
133
- - `opentasks__update_task` to assign and update status
134
- - `opentasks__list_tasks` to check progress
135
- - Native task tools may be unavailableopentasks provides richer task management (links, annotations, cross-provider queries)
123
+ ### When skill-tree is enabled
124
+
125
+ If skills are active (check init context for "Per-Role Skills"):
126
+ - Each spawned agent automatically receives a **skill loadout** compiled for their role, embedded in their AGENT.md.
127
+ - Loadouts are configured via the `skilltree:` block in team.yaml, or **auto-inferred** from role names (e.g., "executor" → implementation profile, "debugger" → debugging profile).
128
+ - You don't need to manage skillsthey're baked into agent prompts at generation time.
129
+ - Available built-in profiles: code-review, implementation, debugging, security, testing, refactoring, documentation, devops.
@@ -145,34 +145,33 @@ describe("agent-generator", () => {
145
145
  expect(md).toContain("You execute things.");
146
146
  });
147
147
 
148
- it("includes team coordination section", () => {
148
+ it("includes capabilities section with team context", () => {
149
149
  const md = generateAgentMd(baseOpts);
150
- expect(md).toContain("## Team Coordination");
150
+ expect(md).toContain("## Swarm Capabilities");
151
151
  expect(md).toContain("**gsd** team");
152
152
  });
153
153
 
154
- it("includes communication section", () => {
154
+ it("includes communication in capabilities", () => {
155
155
  const md = generateAgentMd(baseOpts);
156
156
  expect(md).toContain("### Communication");
157
157
  expect(md).toContain("SendMessage");
158
158
  });
159
159
 
160
- it("includes task management section", () => {
160
+ it("includes task management in capabilities", () => {
161
161
  const md = generateAgentMd(baseOpts);
162
162
  expect(md).toContain("### Task Management");
163
163
  expect(md).toContain("TaskList");
164
164
  });
165
165
 
166
- it("includes TaskCreate for root position", () => {
167
- const md = generateAgentMd({ ...baseOpts, position: "root" });
166
+ it("includes TaskCreate in capabilities for all agents (general guidance)", () => {
167
+ const md = generateAgentMd(baseOpts);
168
168
  expect(md).toContain("TaskCreate");
169
169
  });
170
170
 
171
- it("omits TaskCreate for spawned position", () => {
171
+ it("omits TaskCreate from frontmatter tools for spawned position", () => {
172
172
  const md = generateAgentMd(baseOpts);
173
- // The task management section mentions TaskCreate for root/companion only
174
- const taskSection = md.split("### Task Management")[1].split("###")[0];
175
- expect(taskSection).not.toContain("TaskCreate");
173
+ const frontmatter = md.split("---")[1];
174
+ expect(frontmatter).not.toContain("TaskCreate");
176
175
  });
177
176
 
178
177
  it("includes MAP observability note", () => {
@@ -20,29 +20,28 @@ describe("context-output", () => {
20
20
  expect(out).toContain("No team template configured");
21
21
  });
22
22
 
23
- it("includes MAP status when mapStatus is provided", () => {
24
- const out = formatBootstrapContext({ template: "t", mapStatus: "connected" });
23
+ it("includes MAP status when mapEnabled and mapStatus provided", () => {
24
+ const out = formatBootstrapContext({ template: "t", mapEnabled: true, mapStatus: "connected (scope: swarm:t)" });
25
25
  expect(out).toContain("MAP: connected");
26
26
  });
27
27
 
28
- it("omits MAP line when mapStatus is null", () => {
29
- const out = formatBootstrapContext({ template: "t", mapStatus: null });
30
- expect(out).not.toContain("MAP:");
28
+ it("shows no observability when MAP is disabled", () => {
29
+ const out = formatBootstrapContext({ template: "t", mapEnabled: false });
30
+ expect(out).toContain("No external observability configured");
31
31
  });
32
32
 
33
- it("shows sessionlog active with sync label", () => {
33
+ it("shows sessionlog sync level when MAP enabled and sync is not off", () => {
34
34
  const out = formatBootstrapContext({
35
- template: "t", sessionlogStatus: "active", sessionlogSync: "full",
35
+ template: "t", mapEnabled: true, mapStatus: "connected", sessionlogStatus: "active", sessionlogSync: "full",
36
36
  });
37
- expect(out).toContain("Sessionlog: active (MAP sync: full)");
37
+ expect(out).toContain("trajectory checkpoints synced to MAP (level: full)");
38
38
  });
39
39
 
40
- it("shows sessionlog active without sync label when sync is off", () => {
40
+ it("omits sessionlog sync when sync is off", () => {
41
41
  const out = formatBootstrapContext({
42
- template: "t", sessionlogStatus: "active", sessionlogSync: "off",
42
+ template: "t", mapEnabled: true, mapStatus: "connected", sessionlogStatus: "active", sessionlogSync: "off",
43
43
  });
44
- expect(out).toContain("Sessionlog: active");
45
- expect(out).not.toContain("MAP sync:");
44
+ expect(out).not.toContain("trajectory checkpoints");
46
45
  });
47
46
 
48
47
  it("shows sessionlog WARNING when status is not active", () => {
@@ -92,9 +91,9 @@ describe("context-output", () => {
92
91
  expect(out).toContain("/my/template");
93
92
  });
94
93
 
95
- it("includes coordination section", () => {
94
+ it("includes capabilities section with task and communication tools", () => {
96
95
  const out = formatTeamLoadedContext(tmpDir, "/t", "test");
97
- expect(out).toContain("Coordination");
96
+ expect(out).toContain("Swarm Capabilities");
98
97
  expect(out).toContain("TaskCreate");
99
98
  expect(out).toContain("SendMessage");
100
99
  });