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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.claude-plugin/run-agent-inbox-mcp.sh +22 -3
- package/.gitattributes +3 -0
- package/.opentasks/config.json +9 -0
- package/.opentasks/graph.jsonl +0 -0
- package/e2e/helpers/opentasks-daemon.mjs +149 -0
- package/e2e/tier6-live-inbox-flow.test.mjs +938 -0
- package/e2e/tier7-hooks.test.mjs +992 -0
- package/e2e/tier7-minimem.test.mjs +461 -0
- package/e2e/tier7-opentasks.test.mjs +513 -0
- package/e2e/tier7-skilltree.test.mjs +506 -0
- package/e2e/vitest.config.e2e.mjs +1 -1
- package/package.json +6 -2
- package/references/agent-inbox/package-lock.json +2 -2
- package/references/agent-inbox/package.json +1 -1
- package/references/agent-inbox/src/index.ts +16 -2
- package/references/agent-inbox/src/ipc/ipc-server.ts +58 -0
- package/references/agent-inbox/src/mcp/mcp-proxy.ts +326 -0
- package/references/agent-inbox/src/types.ts +26 -0
- package/references/agent-inbox/test/ipc-new-commands.test.ts +200 -0
- package/references/agent-inbox/test/mcp-proxy.test.ts +191 -0
- package/references/minimem/package-lock.json +2 -2
- package/references/minimem/package.json +1 -1
- package/scripts/bootstrap.mjs +8 -1
- package/scripts/map-hook.mjs +6 -2
- package/scripts/map-sidecar.mjs +19 -0
- package/scripts/team-loader.mjs +15 -8
- package/skills/swarm/SKILL.md +16 -22
- package/src/__tests__/agent-generator.test.mjs +9 -10
- package/src/__tests__/context-output.test.mjs +13 -14
- package/src/__tests__/e2e-inbox-integration.test.mjs +732 -0
- package/src/__tests__/e2e-live-inbox.test.mjs +597 -0
- package/src/__tests__/inbox-integration.test.mjs +298 -0
- package/src/__tests__/integration.test.mjs +12 -11
- package/src/__tests__/skilltree-client.test.mjs +47 -1
- package/src/agent-generator.mjs +79 -88
- package/src/bootstrap.mjs +24 -3
- package/src/context-output.mjs +238 -64
- package/src/index.mjs +2 -0
- package/src/sidecar-server.mjs +30 -0
- package/src/skilltree-client.mjs +50 -5
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { generateAgentMd } from "../agent-generator.mjs";
|
|
3
|
+
import { buildCapabilitiesContext } from "../context-output.mjs";
|
|
4
|
+
import { createCommandHandler, respond } from "../sidecar-server.mjs";
|
|
5
|
+
|
|
6
|
+
// Minimal manifest for agent generation
|
|
7
|
+
const minManifest = {
|
|
8
|
+
name: "gsd",
|
|
9
|
+
topology: { root: { role: "coordinator" } },
|
|
10
|
+
communication: {},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("inbox integration — agent identity in AGENT.md", () => {
|
|
14
|
+
it("embeds inbox identity section when inboxEnabled is true", () => {
|
|
15
|
+
const md = generateAgentMd({
|
|
16
|
+
roleName: "executor",
|
|
17
|
+
teamName: "gsd",
|
|
18
|
+
position: "spawned",
|
|
19
|
+
description: "Executes tasks",
|
|
20
|
+
tools: ["Read", "Bash"],
|
|
21
|
+
skillContent: "# Role: executor",
|
|
22
|
+
manifest: minManifest,
|
|
23
|
+
inboxEnabled: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(md).toContain("## Your Agent Inbox Identity");
|
|
27
|
+
expect(md).toContain("**gsd-executor**");
|
|
28
|
+
expect(md).toContain('agent-inbox__check_inbox(agentId: "gsd-executor")');
|
|
29
|
+
expect(md).toContain('agent-inbox__send_message(from: "gsd-executor"');
|
|
30
|
+
expect(md).toContain("agent-inbox__read_thread");
|
|
31
|
+
expect(md).toContain("agent-inbox__list_agents");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does not embed inbox identity when inboxEnabled is false", () => {
|
|
35
|
+
const md = generateAgentMd({
|
|
36
|
+
roleName: "executor",
|
|
37
|
+
teamName: "gsd",
|
|
38
|
+
position: "spawned",
|
|
39
|
+
description: "Executes tasks",
|
|
40
|
+
tools: ["Read", "Bash"],
|
|
41
|
+
skillContent: "# Role: executor",
|
|
42
|
+
manifest: minManifest,
|
|
43
|
+
inboxEnabled: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(md).not.toContain("Your Agent Inbox Identity");
|
|
47
|
+
expect(md).not.toContain("agent-inbox__check_inbox");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("uses correct ID format: teamName-roleName", () => {
|
|
51
|
+
const md = generateAgentMd({
|
|
52
|
+
roleName: "verifier",
|
|
53
|
+
teamName: "my-team",
|
|
54
|
+
position: "spawned",
|
|
55
|
+
description: "Verifies",
|
|
56
|
+
tools: ["Read"],
|
|
57
|
+
skillContent: "# Role: verifier",
|
|
58
|
+
manifest: minManifest,
|
|
59
|
+
inboxEnabled: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(md).toContain("**my-team-verifier**");
|
|
63
|
+
expect(md).toContain('agentId: "my-team-verifier"');
|
|
64
|
+
expect(md).toContain('from: "my-team-verifier"');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("mentions federated addressing", () => {
|
|
68
|
+
const md = generateAgentMd({
|
|
69
|
+
roleName: "executor",
|
|
70
|
+
teamName: "gsd",
|
|
71
|
+
position: "spawned",
|
|
72
|
+
description: "Executes",
|
|
73
|
+
tools: ["Read"],
|
|
74
|
+
skillContent: "# Role: executor",
|
|
75
|
+
manifest: minManifest,
|
|
76
|
+
inboxEnabled: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(md).toContain("agent@system");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("AGENT.md frontmatter name matches inbox ID format", () => {
|
|
83
|
+
const md = generateAgentMd({
|
|
84
|
+
roleName: "executor",
|
|
85
|
+
teamName: "gsd",
|
|
86
|
+
position: "spawned",
|
|
87
|
+
description: "Executes",
|
|
88
|
+
tools: ["Read"],
|
|
89
|
+
skillContent: "# Role: executor",
|
|
90
|
+
manifest: minManifest,
|
|
91
|
+
inboxEnabled: true,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Frontmatter should have same format as inbox ID
|
|
95
|
+
expect(md).toContain("name: gsd-executor");
|
|
96
|
+
expect(md).toContain("**gsd-executor**");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("inbox integration — capabilities context", () => {
|
|
101
|
+
it("includes detailed inbox MCP tool instructions when enabled", () => {
|
|
102
|
+
const ctx = buildCapabilitiesContext({
|
|
103
|
+
inboxEnabled: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(ctx).toContain("agent-inbox__check_inbox");
|
|
107
|
+
expect(ctx).toContain("agent-inbox__send_message");
|
|
108
|
+
expect(ctx).toContain("agent-inbox__read_thread");
|
|
109
|
+
expect(ctx).toContain("agent-inbox__list_agents");
|
|
110
|
+
expect(ctx).toContain("cross-system messages");
|
|
111
|
+
expect(ctx).toContain("threaded conversations");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("omits inbox tool instructions when disabled", () => {
|
|
115
|
+
const ctx = buildCapabilitiesContext({
|
|
116
|
+
inboxEnabled: false,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(ctx).not.toContain("agent-inbox__check_inbox");
|
|
120
|
+
expect(ctx).not.toContain("Structured messaging");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("distinguishes between SendMessage and inbox use cases", () => {
|
|
124
|
+
const ctx = buildCapabilitiesContext({
|
|
125
|
+
inboxEnabled: true,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(ctx).toContain("Use inbox for:");
|
|
129
|
+
expect(ctx).toContain("Use `SendMessage` for:");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("mentions federated addressing in tool description", () => {
|
|
133
|
+
const ctx = buildCapabilitiesContext({
|
|
134
|
+
inboxEnabled: true,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(ctx).toContain("agent@system");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("inbox integration — sidecar websocket mode agent registration", () => {
|
|
142
|
+
it("registers agent in inbox storage during websocket spawn", async () => {
|
|
143
|
+
// Mock connection that returns agent data
|
|
144
|
+
const mockConn = {
|
|
145
|
+
spawn: vi.fn().mockResolvedValue({ agentId: "gsd-exec", name: "exec" }),
|
|
146
|
+
callExtension: vi.fn().mockResolvedValue({}),
|
|
147
|
+
updateState: vi.fn().mockResolvedValue(undefined),
|
|
148
|
+
updateMetadata: vi.fn().mockResolvedValue(undefined),
|
|
149
|
+
send: vi.fn().mockResolvedValue(undefined),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const mockInboxStorage = {
|
|
153
|
+
putAgent: vi.fn(),
|
|
154
|
+
};
|
|
155
|
+
const mockInboxInstance = { storage: mockInboxStorage };
|
|
156
|
+
|
|
157
|
+
const registeredAgents = new Map();
|
|
158
|
+
const handler = createCommandHandler(mockConn, "swarm:gsd", registeredAgents, {
|
|
159
|
+
inboxInstance: mockInboxInstance,
|
|
160
|
+
transportMode: "websocket",
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const client = { write: vi.fn() };
|
|
164
|
+
await handler({
|
|
165
|
+
action: "spawn",
|
|
166
|
+
agent: {
|
|
167
|
+
agentId: "gsd-executor",
|
|
168
|
+
name: "executor",
|
|
169
|
+
role: "executor",
|
|
170
|
+
scopes: ["swarm:gsd"],
|
|
171
|
+
metadata: { template: "gsd" },
|
|
172
|
+
},
|
|
173
|
+
}, client);
|
|
174
|
+
|
|
175
|
+
// Should have called conn.spawn (MAP SDK)
|
|
176
|
+
expect(mockConn.spawn).toHaveBeenCalledOnce();
|
|
177
|
+
|
|
178
|
+
// Should also register in inbox storage
|
|
179
|
+
expect(mockInboxStorage.putAgent).toHaveBeenCalledOnce();
|
|
180
|
+
const agentArg = mockInboxStorage.putAgent.mock.calls[0][0];
|
|
181
|
+
expect(agentArg.agent_id).toBe("gsd-executor");
|
|
182
|
+
expect(agentArg.status).toBe("active");
|
|
183
|
+
expect(agentArg.scope).toBe("swarm:gsd");
|
|
184
|
+
|
|
185
|
+
// Should be in registered agents map
|
|
186
|
+
expect(registeredAgents.has("gsd-executor")).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("marks agent disconnected in inbox storage during websocket done", async () => {
|
|
190
|
+
const mockConn = {
|
|
191
|
+
spawn: vi.fn().mockResolvedValue({}),
|
|
192
|
+
callExtension: vi.fn().mockResolvedValue({}),
|
|
193
|
+
updateState: vi.fn(),
|
|
194
|
+
updateMetadata: vi.fn(),
|
|
195
|
+
send: vi.fn(),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const mockInboxStorage = {
|
|
199
|
+
putAgent: vi.fn(),
|
|
200
|
+
};
|
|
201
|
+
const mockInboxInstance = { storage: mockInboxStorage };
|
|
202
|
+
|
|
203
|
+
const registeredAgents = new Map();
|
|
204
|
+
registeredAgents.set("gsd-executor", { name: "executor", role: "executor" });
|
|
205
|
+
|
|
206
|
+
const handler = createCommandHandler(mockConn, "swarm:gsd", registeredAgents, {
|
|
207
|
+
inboxInstance: mockInboxInstance,
|
|
208
|
+
transportMode: "websocket",
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const client = { write: vi.fn() };
|
|
212
|
+
await handler({
|
|
213
|
+
action: "done",
|
|
214
|
+
agentId: "gsd-executor",
|
|
215
|
+
reason: "completed",
|
|
216
|
+
}, client);
|
|
217
|
+
|
|
218
|
+
// Should have called MAP unregister
|
|
219
|
+
expect(mockConn.callExtension).toHaveBeenCalledOnce();
|
|
220
|
+
|
|
221
|
+
// Should mark disconnected in inbox storage
|
|
222
|
+
expect(mockInboxStorage.putAgent).toHaveBeenCalledOnce();
|
|
223
|
+
const agentArg = mockInboxStorage.putAgent.mock.calls[0][0];
|
|
224
|
+
expect(agentArg.agent_id).toBe("gsd-executor");
|
|
225
|
+
expect(agentArg.status).toBe("disconnected");
|
|
226
|
+
|
|
227
|
+
// Should be removed from registered agents
|
|
228
|
+
expect(registeredAgents.has("gsd-executor")).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("does not fail when inbox storage is not available in websocket spawn", async () => {
|
|
232
|
+
const mockConn = {
|
|
233
|
+
spawn: vi.fn().mockResolvedValue({}),
|
|
234
|
+
callExtension: vi.fn(),
|
|
235
|
+
updateState: vi.fn(),
|
|
236
|
+
updateMetadata: vi.fn(),
|
|
237
|
+
send: vi.fn(),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const registeredAgents = new Map();
|
|
241
|
+
const handler = createCommandHandler(mockConn, "swarm:gsd", registeredAgents, {
|
|
242
|
+
// No inboxInstance provided
|
|
243
|
+
transportMode: "websocket",
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const client = { write: vi.fn() };
|
|
247
|
+
|
|
248
|
+
// Should not throw
|
|
249
|
+
await handler({
|
|
250
|
+
action: "spawn",
|
|
251
|
+
agent: {
|
|
252
|
+
agentId: "gsd-worker",
|
|
253
|
+
name: "worker",
|
|
254
|
+
role: "worker",
|
|
255
|
+
scopes: ["swarm:gsd"],
|
|
256
|
+
metadata: {},
|
|
257
|
+
},
|
|
258
|
+
}, client);
|
|
259
|
+
|
|
260
|
+
expect(mockConn.spawn).toHaveBeenCalledOnce();
|
|
261
|
+
expect(registeredAgents.has("gsd-worker")).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("inbox integration — agent ID consistency", () => {
|
|
266
|
+
it("AGENT.md name matches the ID format used by sidecar and hooks", () => {
|
|
267
|
+
// The AGENT.md frontmatter uses teamName-roleName
|
|
268
|
+
const teamName = "gsd";
|
|
269
|
+
const roleName = "executor";
|
|
270
|
+
const expectedId = `${teamName}-${roleName}`;
|
|
271
|
+
|
|
272
|
+
// Generated AGENT.md
|
|
273
|
+
const md = generateAgentMd({
|
|
274
|
+
roleName,
|
|
275
|
+
teamName,
|
|
276
|
+
position: "spawned",
|
|
277
|
+
description: "test",
|
|
278
|
+
tools: [],
|
|
279
|
+
skillContent: "",
|
|
280
|
+
manifest: minManifest,
|
|
281
|
+
inboxEnabled: true,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Frontmatter name
|
|
285
|
+
expect(md).toContain(`name: ${expectedId}`);
|
|
286
|
+
// Inbox identity
|
|
287
|
+
expect(md).toContain(`**${expectedId}**`);
|
|
288
|
+
// check_inbox instruction
|
|
289
|
+
expect(md).toContain(`agentId: "${expectedId}"`);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("main agent ID follows teamName-main convention", () => {
|
|
293
|
+
// This is what bootstrap.mjs and map-hook.mjs use
|
|
294
|
+
const teamName = "gsd";
|
|
295
|
+
const mainAgentId = `${teamName}-main`;
|
|
296
|
+
expect(mainAgentId).toBe("gsd-main");
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -95,8 +95,8 @@ describe("integration: agent generation with opentasks disabled", () => {
|
|
|
95
95
|
// In fallback mode all roles get TaskCreate in the tools list
|
|
96
96
|
expect(workerFrontmatter).toContain("TaskCreate");
|
|
97
97
|
|
|
98
|
-
// Body should mention native task tools
|
|
99
|
-
expect(workerMd).toContain("Claude Code
|
|
98
|
+
// Body should mention native task tools in capabilities
|
|
99
|
+
expect(workerMd).toContain("Claude Code native task tools");
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it("root and companion roles include TaskCreate in determineTools", async () => {
|
|
@@ -237,20 +237,20 @@ describe("integration: native task hook → MAP bridge events", () => {
|
|
|
237
237
|
// ── 4. Context output with opentasks ─────────────────────────────────────────────
|
|
238
238
|
|
|
239
239
|
describe("integration: context output with opentasks", () => {
|
|
240
|
-
it("formatBootstrapContext mentions opentasks MCP tools when connected", async () => {
|
|
240
|
+
it("formatBootstrapContext mentions opentasks MCP tools when enabled and connected", async () => {
|
|
241
241
|
const { formatBootstrapContext } = await import("../context-output.mjs");
|
|
242
242
|
|
|
243
243
|
const output = formatBootstrapContext({
|
|
244
244
|
template: "gsd",
|
|
245
|
+
opentasksEnabled: true,
|
|
245
246
|
opentasksStatus: "connected",
|
|
246
247
|
});
|
|
247
248
|
|
|
248
|
-
expect(output).toContain("Opentasks: connected");
|
|
249
249
|
expect(output).toContain("opentasks MCP tools");
|
|
250
250
|
expect(output).toContain("opentasks__create_task");
|
|
251
251
|
});
|
|
252
252
|
|
|
253
|
-
it("formatBootstrapContext does not mention opentasks tools when not
|
|
253
|
+
it("formatBootstrapContext does not mention opentasks tools when not enabled", async () => {
|
|
254
254
|
const { formatBootstrapContext } = await import("../context-output.mjs");
|
|
255
255
|
|
|
256
256
|
const output = formatBootstrapContext({
|
|
@@ -261,21 +261,21 @@ describe("integration: context output with opentasks", () => {
|
|
|
261
261
|
expect(output).not.toContain("opentasks__create_task");
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
-
it("formatTeamLoadedContext mentions opentasks when connected", async () => {
|
|
264
|
+
it("formatTeamLoadedContext mentions opentasks when enabled and connected", async () => {
|
|
265
265
|
const { formatTeamLoadedContext } = await import("../context-output.mjs");
|
|
266
266
|
|
|
267
267
|
const output = formatTeamLoadedContext(
|
|
268
268
|
"/tmp/agents",
|
|
269
269
|
"/tmp/template",
|
|
270
270
|
"gsd",
|
|
271
|
-
{ opentasksStatus: "connected" },
|
|
271
|
+
{ opentasksEnabled: true, opentasksStatus: "connected" },
|
|
272
272
|
);
|
|
273
273
|
|
|
274
274
|
expect(output).toContain("opentasks MCP tools");
|
|
275
275
|
expect(output).toContain("opentasks__create_task");
|
|
276
276
|
});
|
|
277
277
|
|
|
278
|
-
it("formatTeamLoadedContext mentions native task tools when opentasks not
|
|
278
|
+
it("formatTeamLoadedContext mentions native task tools when opentasks not enabled", async () => {
|
|
279
279
|
const { formatTeamLoadedContext } = await import("../context-output.mjs");
|
|
280
280
|
|
|
281
281
|
const output = formatTeamLoadedContext(
|
|
@@ -284,16 +284,17 @@ describe("integration: context output with opentasks", () => {
|
|
|
284
284
|
"gsd",
|
|
285
285
|
);
|
|
286
286
|
|
|
287
|
-
expect(output).toContain("Claude Code
|
|
288
|
-
expect(output).toContain("TaskCreate
|
|
287
|
+
expect(output).toContain("Claude Code native task tools");
|
|
288
|
+
expect(output).toContain("TaskCreate");
|
|
289
289
|
expect(output).not.toContain("opentasks MCP tools");
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
-
it("formatBootstrapContext with
|
|
292
|
+
it("formatBootstrapContext with opentasksEnabled and status 'enabled' shows opentasks tools", async () => {
|
|
293
293
|
const { formatBootstrapContext } = await import("../context-output.mjs");
|
|
294
294
|
|
|
295
295
|
const output = formatBootstrapContext({
|
|
296
296
|
template: "gsd",
|
|
297
|
+
opentasksEnabled: true,
|
|
297
298
|
opentasksStatus: "enabled",
|
|
298
299
|
});
|
|
299
300
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parseSkillTreeExtension } from "../skilltree-client.mjs";
|
|
2
|
+
import { parseSkillTreeExtension, inferProfileFromRole } from "../skilltree-client.mjs";
|
|
3
3
|
|
|
4
4
|
describe("skilltree-client", () => {
|
|
5
5
|
describe("parseSkillTreeExtension", () => {
|
|
@@ -77,4 +77,50 @@ describe("skilltree-client", () => {
|
|
|
77
77
|
expect(result.roles).toEqual({});
|
|
78
78
|
});
|
|
79
79
|
});
|
|
80
|
+
|
|
81
|
+
describe("inferProfileFromRole", () => {
|
|
82
|
+
it("maps executor to implementation", () => {
|
|
83
|
+
expect(inferProfileFromRole("executor")).toBe("implementation");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("maps developer to implementation", () => {
|
|
87
|
+
expect(inferProfileFromRole("developer")).toBe("implementation");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("maps debugger to debugging", () => {
|
|
91
|
+
expect(inferProfileFromRole("debugger")).toBe("debugging");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("maps verifier to testing", () => {
|
|
95
|
+
expect(inferProfileFromRole("verifier")).toBe("testing");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("maps qa to testing", () => {
|
|
99
|
+
expect(inferProfileFromRole("qa")).toBe("testing");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("maps plan-checker to code-review", () => {
|
|
103
|
+
expect(inferProfileFromRole("plan-checker")).toBe("code-review");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("maps tech-writer to documentation", () => {
|
|
107
|
+
expect(inferProfileFromRole("tech-writer")).toBe("documentation");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("returns empty string for unknown role", () => {
|
|
111
|
+
expect(inferProfileFromRole("orchestrator")).toBe("");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("returns empty string for roadmapper", () => {
|
|
115
|
+
expect(inferProfileFromRole("roadmapper")).toBe("");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("matches partial role names", () => {
|
|
119
|
+
expect(inferProfileFromRole("senior-developer")).toBe("implementation");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("matches quick-flow-dev to implementation", () => {
|
|
123
|
+
expect(inferProfileFromRole("quick-flow-dev")).toBe("implementation");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
80
126
|
});
|
package/src/agent-generator.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import path from "path";
|
|
10
|
+
import { buildCapabilitiesContext } from "./context-output.mjs";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Parse team.yaml without js-yaml dependency (basic YAML subset).
|
|
@@ -91,6 +92,17 @@ export function generateAgentMd({
|
|
|
91
92
|
opentasksEnabled,
|
|
92
93
|
minimemEnabled,
|
|
93
94
|
skillLoadout,
|
|
95
|
+
skillProfile,
|
|
96
|
+
// Capabilities context options (passed through to buildCapabilitiesContext)
|
|
97
|
+
opentasksStatus,
|
|
98
|
+
minimemStatus,
|
|
99
|
+
skilltreeEnabled,
|
|
100
|
+
skilltreeStatus,
|
|
101
|
+
inboxEnabled,
|
|
102
|
+
meshEnabled,
|
|
103
|
+
mapEnabled,
|
|
104
|
+
mapStatus,
|
|
105
|
+
sessionlogSync,
|
|
94
106
|
}) {
|
|
95
107
|
const lines = [];
|
|
96
108
|
|
|
@@ -111,25 +123,7 @@ export function generateAgentMd({
|
|
|
111
123
|
lines.push(skillContent);
|
|
112
124
|
lines.push("");
|
|
113
125
|
|
|
114
|
-
//
|
|
115
|
-
lines.push("## Team Coordination");
|
|
116
|
-
lines.push("");
|
|
117
|
-
lines.push(`You are part of the **${teamName}** team.`);
|
|
118
|
-
lines.push("");
|
|
119
|
-
|
|
120
|
-
// Communication via SendMessage
|
|
121
|
-
lines.push("### Communication");
|
|
122
|
-
lines.push("");
|
|
123
|
-
lines.push("Use **SendMessage** to communicate with teammates:");
|
|
124
|
-
lines.push(
|
|
125
|
-
'- `SendMessage(type="message", recipient="<agent-name>", content="...", summary="...")`'
|
|
126
|
-
);
|
|
127
|
-
lines.push(
|
|
128
|
-
"- Only use broadcast when absolutely necessary (it messages every teammate)."
|
|
129
|
-
);
|
|
130
|
-
lines.push("");
|
|
131
|
-
|
|
132
|
-
// Add role-specific communication patterns from topology routing
|
|
126
|
+
// Role-specific communication patterns from topology (unique per role, not in capabilities)
|
|
133
127
|
if (manifest.communication?.routing?.peers) {
|
|
134
128
|
const outbound = manifest.communication.routing.peers.filter(
|
|
135
129
|
(r) => r.from === roleName
|
|
@@ -138,7 +132,7 @@ export function generateAgentMd({
|
|
|
138
132
|
(r) => r.to === roleName
|
|
139
133
|
);
|
|
140
134
|
if (outbound.length > 0 || inbound.length > 0) {
|
|
141
|
-
lines.push("
|
|
135
|
+
lines.push("## Your Communication Patterns");
|
|
142
136
|
lines.push("");
|
|
143
137
|
for (const route of outbound) {
|
|
144
138
|
const signals = route.signals?.join(", ") || "updates";
|
|
@@ -152,10 +146,10 @@ export function generateAgentMd({
|
|
|
152
146
|
}
|
|
153
147
|
}
|
|
154
148
|
|
|
155
|
-
// Signals this role emits
|
|
149
|
+
// Signals this role emits
|
|
156
150
|
if (manifest.communication?.emissions?.[roleName]) {
|
|
157
151
|
const emissions = manifest.communication.emissions[roleName];
|
|
158
|
-
lines.push("
|
|
152
|
+
lines.push("## Signals You Emit");
|
|
159
153
|
lines.push("");
|
|
160
154
|
lines.push(
|
|
161
155
|
"When these events occur, notify the relevant agents via SendMessage:"
|
|
@@ -169,7 +163,7 @@ export function generateAgentMd({
|
|
|
169
163
|
// Signals this role subscribes to
|
|
170
164
|
if (manifest.communication?.subscriptions?.[roleName]) {
|
|
171
165
|
const subs = manifest.communication.subscriptions[roleName];
|
|
172
|
-
lines.push("
|
|
166
|
+
lines.push("## Signals You Receive");
|
|
173
167
|
lines.push("");
|
|
174
168
|
lines.push("Watch for messages from teammates about:");
|
|
175
169
|
for (const sub of subs) {
|
|
@@ -179,79 +173,48 @@ export function generateAgentMd({
|
|
|
179
173
|
lines.push("");
|
|
180
174
|
}
|
|
181
175
|
|
|
182
|
-
//
|
|
183
|
-
if (
|
|
184
|
-
lines.push("
|
|
185
|
-
lines.push("");
|
|
186
|
-
lines.push("Use **opentasks MCP tools** for all task management:");
|
|
187
|
-
lines.push(
|
|
188
|
-
"- **opentasks__list_tasks** — check available tasks and their status"
|
|
189
|
-
);
|
|
190
|
-
lines.push(
|
|
191
|
-
"- **opentasks__update_task** — claim tasks (set assignee to your name), update status"
|
|
192
|
-
);
|
|
193
|
-
if (position === "root" || position === "companion") {
|
|
194
|
-
lines.push(
|
|
195
|
-
"- **opentasks__create_task** — create new tasks for the team when you identify additional work"
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
lines.push("");
|
|
199
|
-
lines.push(
|
|
200
|
-
"After completing a task, mark it completed via opentasks__update_task, then check opentasks__list_tasks for your next assignment."
|
|
201
|
-
);
|
|
202
|
-
lines.push("");
|
|
203
|
-
} else {
|
|
204
|
-
lines.push("### Task Management");
|
|
205
|
-
lines.push("");
|
|
206
|
-
lines.push("Use Claude Code's native task tools:");
|
|
207
|
-
lines.push(
|
|
208
|
-
"- **TaskList** — check available tasks and their status"
|
|
209
|
-
);
|
|
210
|
-
lines.push(
|
|
211
|
-
"- **TaskUpdate** — claim tasks (set owner to your name), update status to in_progress or completed"
|
|
212
|
-
);
|
|
213
|
-
if (position === "root" || position === "companion") {
|
|
214
|
-
lines.push(
|
|
215
|
-
"- **TaskCreate** — create new tasks for the team when you identify additional work"
|
|
216
|
-
);
|
|
217
|
-
}
|
|
176
|
+
// Skills section (skill-tree loadout — role-specific content, kept separate)
|
|
177
|
+
if (skillLoadout) {
|
|
178
|
+
lines.push("## Skills");
|
|
218
179
|
lines.push("");
|
|
219
|
-
lines.push(
|
|
220
|
-
"After completing a task, mark it completed with TaskUpdate, then check TaskList for your next assignment."
|
|
221
|
-
);
|
|
180
|
+
lines.push(skillLoadout);
|
|
222
181
|
lines.push("");
|
|
223
182
|
}
|
|
224
183
|
|
|
225
|
-
//
|
|
226
|
-
if (
|
|
227
|
-
|
|
184
|
+
// Agent inbox identity (embedded per-agent so each agent knows its inbox ID)
|
|
185
|
+
if (inboxEnabled) {
|
|
186
|
+
const agentInboxId = `${teamName}-${roleName}`;
|
|
187
|
+
lines.push("## Your Agent Inbox Identity");
|
|
228
188
|
lines.push("");
|
|
229
|
-
lines.push(
|
|
230
|
-
lines.push("- **minimem__memory_search** — search past decisions, context, patterns");
|
|
231
|
-
lines.push("- **minimem__memory_get_details** — get full text for a search result");
|
|
232
|
-
lines.push("- **minimem__knowledge_search** — search with domain/entity filters");
|
|
189
|
+
lines.push(`Your inbox ID is **${agentInboxId}**. Use this with agent-inbox MCP tools:`);
|
|
233
190
|
lines.push("");
|
|
234
|
-
lines.push(
|
|
191
|
+
lines.push(`- Check your inbox: \`agent-inbox__check_inbox(agentId: "${agentInboxId}")\``);
|
|
192
|
+
lines.push(`- Send messages: \`agent-inbox__send_message(from: "${agentInboxId}", to: "<recipient>", body: "...")\``);
|
|
193
|
+
lines.push(`- Read threads: \`agent-inbox__read_thread(threadTag: "...")\``);
|
|
194
|
+
lines.push("- List agents: `agent-inbox__list_agents()`");
|
|
235
195
|
lines.push("");
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// Skills section (skill-tree loadout)
|
|
239
|
-
if (skillLoadout) {
|
|
240
|
-
lines.push("## Skills");
|
|
241
|
-
lines.push("");
|
|
242
|
-
lines.push(skillLoadout);
|
|
196
|
+
lines.push("Check your inbox periodically for messages from teammates and external systems.");
|
|
197
|
+
lines.push("Use `agent@system` addressing for cross-system (federated) messages.");
|
|
243
198
|
lines.push("");
|
|
244
199
|
}
|
|
245
200
|
|
|
246
|
-
//
|
|
247
|
-
lines.push(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
201
|
+
// Unified capabilities context (shared with main agent)
|
|
202
|
+
lines.push(buildCapabilitiesContext({
|
|
203
|
+
role: roleName,
|
|
204
|
+
teamName,
|
|
205
|
+
opentasksEnabled,
|
|
206
|
+
opentasksStatus: opentasksStatus || (opentasksEnabled ? "enabled" : "disabled"),
|
|
207
|
+
minimemEnabled,
|
|
208
|
+
minimemStatus: minimemStatus || (minimemEnabled ? "ready" : "disabled"),
|
|
209
|
+
skilltreeEnabled,
|
|
210
|
+
skilltreeStatus: skilltreeStatus || (skilltreeEnabled ? "ready" : "disabled"),
|
|
211
|
+
skillProfile: skillProfile || "",
|
|
212
|
+
inboxEnabled,
|
|
213
|
+
meshEnabled,
|
|
214
|
+
mapEnabled,
|
|
215
|
+
mapStatus: mapStatus || "disabled",
|
|
216
|
+
sessionlogSync: sessionlogSync || "off",
|
|
217
|
+
}));
|
|
255
218
|
|
|
256
219
|
return lines.join("\n") + "\n";
|
|
257
220
|
}
|
|
@@ -282,6 +245,7 @@ export async function generateAllAgents(templateDir, outputDir, options = {}) {
|
|
|
282
245
|
}
|
|
283
246
|
|
|
284
247
|
// Load skill loadouts if available (compiled by skilltree-client during team loading)
|
|
248
|
+
// Format: { roleName: { content, profile } } (or legacy string format)
|
|
285
249
|
let skillLoadouts = {};
|
|
286
250
|
const loadoutsPath = path.join(absOutputDir, "..", "skill-loadouts.json");
|
|
287
251
|
if (fs.existsSync(loadoutsPath)) {
|
|
@@ -289,6 +253,13 @@ export async function generateAllAgents(templateDir, outputDir, options = {}) {
|
|
|
289
253
|
skillLoadouts = JSON.parse(fs.readFileSync(loadoutsPath, "utf-8"));
|
|
290
254
|
} catch { /* ignore */ }
|
|
291
255
|
}
|
|
256
|
+
// Helper to extract content and profile from loadout entry (handles both formats)
|
|
257
|
+
function getLoadout(roleName) {
|
|
258
|
+
const entry = skillLoadouts[roleName];
|
|
259
|
+
if (!entry) return { content: "", profile: "" };
|
|
260
|
+
if (typeof entry === "string") return { content: entry, profile: "" };
|
|
261
|
+
return { content: entry.content || "", profile: entry.profile || "" };
|
|
262
|
+
}
|
|
292
263
|
|
|
293
264
|
const generatedRoles = [];
|
|
294
265
|
|
|
@@ -331,8 +302,18 @@ export async function generateAllAgents(templateDir, outputDir, options = {}) {
|
|
|
331
302
|
skillContent: roleSkill.content,
|
|
332
303
|
manifest,
|
|
333
304
|
opentasksEnabled: options.opentasksEnabled,
|
|
305
|
+
opentasksStatus: options.opentasksStatus,
|
|
334
306
|
minimemEnabled: options.minimemEnabled,
|
|
335
|
-
|
|
307
|
+
minimemStatus: options.minimemStatus,
|
|
308
|
+
skilltreeEnabled: options.skilltreeEnabled,
|
|
309
|
+
skilltreeStatus: options.skilltreeStatus,
|
|
310
|
+
inboxEnabled: options.inboxEnabled,
|
|
311
|
+
meshEnabled: options.meshEnabled,
|
|
312
|
+
mapEnabled: options.mapEnabled,
|
|
313
|
+
mapStatus: options.mapStatus,
|
|
314
|
+
sessionlogSync: options.sessionlogSync,
|
|
315
|
+
skillLoadout: getLoadout(roleName).content,
|
|
316
|
+
skillProfile: getLoadout(roleName).profile,
|
|
336
317
|
});
|
|
337
318
|
|
|
338
319
|
const agentDir = path.join(absOutputDir, roleName);
|
|
@@ -377,8 +358,18 @@ export async function generateAllAgents(templateDir, outputDir, options = {}) {
|
|
|
377
358
|
description: `${roleName} agent in the ${manifest.name} team`,
|
|
378
359
|
tools: fallbackTools,
|
|
379
360
|
opentasksEnabled: options.opentasksEnabled,
|
|
361
|
+
opentasksStatus: options.opentasksStatus,
|
|
380
362
|
minimemEnabled: options.minimemEnabled,
|
|
381
|
-
|
|
363
|
+
minimemStatus: options.minimemStatus,
|
|
364
|
+
skilltreeEnabled: options.skilltreeEnabled,
|
|
365
|
+
skilltreeStatus: options.skilltreeStatus,
|
|
366
|
+
inboxEnabled: options.inboxEnabled,
|
|
367
|
+
meshEnabled: options.meshEnabled,
|
|
368
|
+
mapEnabled: options.mapEnabled,
|
|
369
|
+
mapStatus: options.mapStatus,
|
|
370
|
+
sessionlogSync: options.sessionlogSync,
|
|
371
|
+
skillLoadout: getLoadout(roleName).content,
|
|
372
|
+
skillProfile: getLoadout(roleName).profile,
|
|
382
373
|
skillContent: prompt
|
|
383
374
|
? `# Role: ${roleName}\n\nMember of the **${manifest.name}** team.\n\n## Instructions\n\n${prompt}`
|
|
384
375
|
: `# Role: ${roleName}\n\nMember of the **${manifest.name}** team.`,
|