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,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 7: Skill-Tree Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the skill-tree loadout compilation pipeline without LLM calls:
|
|
5
|
+
* 1. parseSkillTreeExtension extracts skilltree namespace from manifest
|
|
6
|
+
* 2. inferProfileFromRole maps role names to profiles
|
|
7
|
+
* 3. compileAllRoleLoadouts processes full manifests
|
|
8
|
+
* 4. Loadouts are cached in skill-loadouts.json per template
|
|
9
|
+
* 5. AGENT.md generation embeds skill loadouts
|
|
10
|
+
* 6. Bootstrap context output includes skill-tree section
|
|
11
|
+
*
|
|
12
|
+
* No LLM calls — exercises pure computation and file I/O.
|
|
13
|
+
*
|
|
14
|
+
* Run:
|
|
15
|
+
* npx vitest run --config e2e/vitest.config.e2e.mjs e2e/tier7-skilltree.test.mjs
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import path from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { createWorkspace } from "./helpers/workspace.mjs";
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const SHORT_TMPDIR = "/tmp";
|
|
26
|
+
|
|
27
|
+
// Import source modules
|
|
28
|
+
const {
|
|
29
|
+
parseSkillTreeExtension,
|
|
30
|
+
inferProfileFromRole,
|
|
31
|
+
compileRoleLoadout,
|
|
32
|
+
compileAllRoleLoadouts,
|
|
33
|
+
} = await import("../src/skilltree-client.mjs");
|
|
34
|
+
|
|
35
|
+
const { buildCapabilitiesContext } = await import("../src/context-output.mjs");
|
|
36
|
+
const { generateAgentMd } = await import("../src/agent-generator.mjs");
|
|
37
|
+
|
|
38
|
+
// Check if skill-tree is available for compilation tests
|
|
39
|
+
let skillTreeAvailable = false;
|
|
40
|
+
try {
|
|
41
|
+
const st = await import("skill-tree");
|
|
42
|
+
skillTreeAvailable = !!st.createSkillBank;
|
|
43
|
+
} catch {
|
|
44
|
+
// Not installed or no createSkillBank export
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
// Group 1: parseSkillTreeExtension — YAML namespace parsing
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
describe(
|
|
52
|
+
"tier7: skilltree parseSkillTreeExtension",
|
|
53
|
+
{ timeout: 15_000 },
|
|
54
|
+
() => {
|
|
55
|
+
it("extracts defaults and roles from manifest", () => {
|
|
56
|
+
const manifest = {
|
|
57
|
+
skilltree: {
|
|
58
|
+
defaults: {
|
|
59
|
+
profile: "implementation",
|
|
60
|
+
maxSkills: 6,
|
|
61
|
+
},
|
|
62
|
+
roles: {
|
|
63
|
+
orchestrator: { profile: "code-review" },
|
|
64
|
+
executor: { profile: "implementation", tags: ["development"] },
|
|
65
|
+
verifier: { profile: "testing" },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const { defaults, roles } = parseSkillTreeExtension(manifest);
|
|
71
|
+
|
|
72
|
+
expect(defaults.profile).toBe("implementation");
|
|
73
|
+
expect(defaults.maxSkills).toBe(6);
|
|
74
|
+
expect(roles.orchestrator.profile).toBe("code-review");
|
|
75
|
+
expect(roles.executor.profile).toBe("implementation");
|
|
76
|
+
expect(roles.executor.tags).toEqual(["development"]);
|
|
77
|
+
expect(roles.verifier.profile).toBe("testing");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns empty defaults and roles when no skilltree namespace", () => {
|
|
81
|
+
const { defaults, roles } = parseSkillTreeExtension({});
|
|
82
|
+
expect(defaults).toEqual({});
|
|
83
|
+
expect(roles).toEqual({});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns empty defaults and roles for null manifest", () => {
|
|
87
|
+
const { defaults, roles } = parseSkillTreeExtension(null);
|
|
88
|
+
expect(defaults).toEqual({});
|
|
89
|
+
expect(roles).toEqual({});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("handles missing defaults or roles gracefully", () => {
|
|
93
|
+
const { defaults, roles } = parseSkillTreeExtension({
|
|
94
|
+
skilltree: { defaults: { profile: "testing" } },
|
|
95
|
+
});
|
|
96
|
+
expect(defaults.profile).toBe("testing");
|
|
97
|
+
expect(roles).toEqual({});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("handles missing defaults with roles present", () => {
|
|
101
|
+
const { defaults, roles } = parseSkillTreeExtension({
|
|
102
|
+
skilltree: {
|
|
103
|
+
roles: { executor: { profile: "implementation" } },
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
expect(defaults).toEqual({});
|
|
107
|
+
expect(roles.executor.profile).toBe("implementation");
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
+
// Group 2: inferProfileFromRole — role name to profile mapping
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
describe(
|
|
117
|
+
"tier7: skilltree inferProfileFromRole",
|
|
118
|
+
{ timeout: 15_000 },
|
|
119
|
+
() => {
|
|
120
|
+
it("maps known role names to profiles", () => {
|
|
121
|
+
expect(inferProfileFromRole("executor")).toBe("implementation");
|
|
122
|
+
expect(inferProfileFromRole("developer")).toBe("implementation");
|
|
123
|
+
expect(inferProfileFromRole("debugger")).toBe("debugging");
|
|
124
|
+
expect(inferProfileFromRole("verifier")).toBe("testing");
|
|
125
|
+
expect(inferProfileFromRole("qa")).toBe("testing");
|
|
126
|
+
expect(inferProfileFromRole("architect")).toBe("refactoring");
|
|
127
|
+
expect(inferProfileFromRole("tech-writer")).toBe("documentation");
|
|
128
|
+
expect(inferProfileFromRole("security-auditor")).toBe("security");
|
|
129
|
+
expect(inferProfileFromRole("plan-checker")).toBe("code-review");
|
|
130
|
+
expect(inferProfileFromRole("integration-checker")).toBe("code-review");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("handles partial matches (e.g. senior-developer)", () => {
|
|
134
|
+
expect(inferProfileFromRole("senior-developer")).toBe("implementation");
|
|
135
|
+
expect(inferProfileFromRole("lead-executor")).toBe("implementation");
|
|
136
|
+
expect(inferProfileFromRole("junior-debugger")).toBe("debugging");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("returns empty string for unknown roles", () => {
|
|
140
|
+
expect(inferProfileFromRole("orchestrator")).toBe("");
|
|
141
|
+
expect(inferProfileFromRole("coordinator")).toBe("");
|
|
142
|
+
expect(inferProfileFromRole("unknown-role")).toBe("");
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
148
|
+
// Group 3: compileRoleLoadout — single role compilation
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
describe(
|
|
152
|
+
"tier7: skilltree compileRoleLoadout",
|
|
153
|
+
{ timeout: 30_000 },
|
|
154
|
+
() => {
|
|
155
|
+
let workspace;
|
|
156
|
+
|
|
157
|
+
afterEach(() => {
|
|
158
|
+
if (workspace) {
|
|
159
|
+
workspace.cleanup();
|
|
160
|
+
workspace = null;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns empty string when basePath does not exist", async () => {
|
|
165
|
+
const result = await compileRoleLoadout("executor", { profile: "implementation" }, {
|
|
166
|
+
basePath: "/tmp/nonexistent-skill-tree-" + Date.now(),
|
|
167
|
+
});
|
|
168
|
+
expect(result).toBe("");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("returns empty string when no criteria specified", async () => {
|
|
172
|
+
const result = await compileRoleLoadout("executor", {}, {
|
|
173
|
+
basePath: "/tmp/nonexistent-skill-tree-" + Date.now(),
|
|
174
|
+
});
|
|
175
|
+
expect(result).toBe("");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it.skipIf(!skillTreeAvailable)(
|
|
179
|
+
"compiles loadout from profile when skill-tree basePath exists",
|
|
180
|
+
async () => {
|
|
181
|
+
workspace = createWorkspace({
|
|
182
|
+
tmpdir: SHORT_TMPDIR,
|
|
183
|
+
prefix: "t7-st-compile-",
|
|
184
|
+
config: { template: "gsd", skilltree: { enabled: true } },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const stDir = path.join(workspace.dir, ".swarm", "skill-tree");
|
|
188
|
+
fs.mkdirSync(stDir, { recursive: true });
|
|
189
|
+
|
|
190
|
+
// Initialize skill-tree in the workspace
|
|
191
|
+
try {
|
|
192
|
+
const { createSkillBank } = await import("skill-tree");
|
|
193
|
+
const bank = createSkillBank({ storage: { basePath: stDir } });
|
|
194
|
+
await bank.initialize();
|
|
195
|
+
await bank.shutdown();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.log("[tier7] skill-tree init skipped:", err.message);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const result = await compileRoleLoadout("executor", { profile: "implementation" }, {
|
|
202
|
+
basePath: stDir,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
console.log("[tier7] compiled loadout length:", result.length);
|
|
206
|
+
// May be empty if no skills are loaded in the test bank
|
|
207
|
+
// The key assertion is that it didn't throw
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
214
|
+
// Group 4: compileAllRoleLoadouts — full manifest compilation
|
|
215
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
describe(
|
|
218
|
+
"tier7: skilltree compileAllRoleLoadouts",
|
|
219
|
+
{ timeout: 30_000 },
|
|
220
|
+
() => {
|
|
221
|
+
it("processes all roles with overrides and defaults", async () => {
|
|
222
|
+
const manifest = {
|
|
223
|
+
roles: ["orchestrator", "executor", "verifier", "debugger"],
|
|
224
|
+
skilltree: {
|
|
225
|
+
defaults: { profile: "implementation" },
|
|
226
|
+
roles: {
|
|
227
|
+
orchestrator: { profile: "code-review" },
|
|
228
|
+
verifier: { profile: "testing" },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// With a nonexistent basePath, all compilations return empty
|
|
234
|
+
// but the function should process all roles without error
|
|
235
|
+
const result = await compileAllRoleLoadouts(manifest, {
|
|
236
|
+
basePath: "/tmp/nonexistent-st-" + Date.now(),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Result is an object mapping role names to { content, profile }
|
|
240
|
+
expect(typeof result).toBe("object");
|
|
241
|
+
console.log("[tier7] compiled roles:", Object.keys(result).join(", "));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("skips roles with no criteria and no inferred profile", async () => {
|
|
245
|
+
const manifest = {
|
|
246
|
+
roles: ["custom-role-xyz"],
|
|
247
|
+
// No skilltree namespace at all
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const result = await compileAllRoleLoadouts(manifest, {
|
|
251
|
+
basePath: "/tmp/nonexistent-st-" + Date.now(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// custom-role-xyz has no match in ROLE_PROFILE_MAP and no defaults
|
|
255
|
+
expect(Object.keys(result)).not.toContain("custom-role-xyz");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("uses defaultProfile from config as fallback", async () => {
|
|
259
|
+
const manifest = {
|
|
260
|
+
roles: ["custom-role-xyz"],
|
|
261
|
+
// No skilltree namespace
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const result = await compileAllRoleLoadouts(manifest, {
|
|
265
|
+
basePath: "/tmp/nonexistent-st-" + Date.now(),
|
|
266
|
+
defaultProfile: "implementation",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// With defaultProfile, it should attempt compilation
|
|
270
|
+
// (returns empty since basePath doesn't exist, but no error)
|
|
271
|
+
console.log("[tier7] with defaultProfile, compiled:", Object.keys(result).join(", "));
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("auto-infers profiles from role names", async () => {
|
|
275
|
+
const manifest = {
|
|
276
|
+
roles: ["executor", "debugger", "verifier"],
|
|
277
|
+
// No skilltree namespace — should use inferProfileFromRole
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const result = await compileAllRoleLoadouts(manifest, {
|
|
281
|
+
basePath: "/tmp/nonexistent-st-" + Date.now(),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// These roles have known mappings in ROLE_PROFILE_MAP
|
|
285
|
+
// Compilation returns empty (no basePath), but the function ran without error
|
|
286
|
+
console.log("[tier7] auto-inferred roles processed:", Object.keys(result).join(", "));
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
292
|
+
// Group 5: Context Output — buildCapabilitiesContext includes skill-tree
|
|
293
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
describe(
|
|
296
|
+
"tier7: skilltree context output",
|
|
297
|
+
{ timeout: 15_000 },
|
|
298
|
+
() => {
|
|
299
|
+
it("includes per-role skills section when enabled and ready", () => {
|
|
300
|
+
const context = buildCapabilitiesContext({
|
|
301
|
+
skilltreeEnabled: true,
|
|
302
|
+
skilltreeStatus: "ready",
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(context).toContain("### Per-Role Skills");
|
|
306
|
+
expect(context).toContain("skill loadout");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("shows status when installed but not ready", () => {
|
|
310
|
+
const context = buildCapabilitiesContext({
|
|
311
|
+
skilltreeEnabled: true,
|
|
312
|
+
skilltreeStatus: "installed",
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(context).toContain("### Per-Role Skills");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("omits skill-tree section when disabled", () => {
|
|
319
|
+
const context = buildCapabilitiesContext({
|
|
320
|
+
skilltreeEnabled: false,
|
|
321
|
+
skilltreeStatus: "disabled",
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(context).not.toContain("### Per-Role Skills");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("main agent context describes team-wide loadout config", () => {
|
|
328
|
+
const context = buildCapabilitiesContext({
|
|
329
|
+
role: null,
|
|
330
|
+
skilltreeEnabled: true,
|
|
331
|
+
skilltreeStatus: "ready",
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(context).toContain("skill loadout");
|
|
335
|
+
expect(context).toContain("profiles");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("spawned agent context references embedded skills section", () => {
|
|
339
|
+
const context = buildCapabilitiesContext({
|
|
340
|
+
role: "executor",
|
|
341
|
+
teamName: "gsd",
|
|
342
|
+
skilltreeEnabled: true,
|
|
343
|
+
skilltreeStatus: "ready",
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(context).toContain("## Skills");
|
|
347
|
+
expect(context).toContain("skill loadout");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("spawned agent context shows profile name when provided", () => {
|
|
351
|
+
const context = buildCapabilitiesContext({
|
|
352
|
+
role: "executor",
|
|
353
|
+
teamName: "gsd",
|
|
354
|
+
skilltreeEnabled: true,
|
|
355
|
+
skilltreeStatus: "ready",
|
|
356
|
+
skillProfile: "implementation",
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
expect(context).toContain("implementation");
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
365
|
+
// Group 6: Agent Generation — AGENT.md embeds skill loadout
|
|
366
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
describe(
|
|
369
|
+
"tier7: skilltree agent generation",
|
|
370
|
+
{ timeout: 15_000 },
|
|
371
|
+
() => {
|
|
372
|
+
it("generateAgentMd includes Skills section when loadout provided", () => {
|
|
373
|
+
const md = generateAgentMd({
|
|
374
|
+
roleName: "executor",
|
|
375
|
+
teamName: "gsd",
|
|
376
|
+
position: "spawned",
|
|
377
|
+
description: "Executor agent",
|
|
378
|
+
tools: ["Read", "Write", "Bash"],
|
|
379
|
+
skillContent: "# Role: executor\n\nTest content.",
|
|
380
|
+
manifest: {},
|
|
381
|
+
skilltreeEnabled: true,
|
|
382
|
+
skilltreeStatus: "ready",
|
|
383
|
+
skillLoadout: "## Skill: Clean Code\n\nWrite clean, readable code.",
|
|
384
|
+
skillProfile: "implementation",
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
expect(md).toContain("## Skills");
|
|
388
|
+
expect(md).toContain("Clean Code");
|
|
389
|
+
expect(md).toContain("Write clean, readable code");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("generateAgentMd omits Skills section when no loadout", () => {
|
|
393
|
+
const md = generateAgentMd({
|
|
394
|
+
roleName: "executor",
|
|
395
|
+
teamName: "gsd",
|
|
396
|
+
position: "spawned",
|
|
397
|
+
description: "Executor agent",
|
|
398
|
+
tools: ["Read", "Write", "Bash"],
|
|
399
|
+
skillContent: "# Role: executor\n\nTest content.",
|
|
400
|
+
manifest: {},
|
|
401
|
+
skilltreeEnabled: true,
|
|
402
|
+
skilltreeStatus: "ready",
|
|
403
|
+
// No skillLoadout
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Should not have the actual "## Skills" heading as a standalone section
|
|
407
|
+
// (it may appear in capabilities context as a reference like "## Skills section above")
|
|
408
|
+
expect(md).not.toMatch(/^## Skills$/m);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("generateAgentMd includes capabilities context with skill-tree info", () => {
|
|
412
|
+
const md = generateAgentMd({
|
|
413
|
+
roleName: "verifier",
|
|
414
|
+
teamName: "gsd",
|
|
415
|
+
position: "spawned",
|
|
416
|
+
description: "Verifier agent",
|
|
417
|
+
tools: ["Read", "Bash"],
|
|
418
|
+
skillContent: "# Role: verifier\n\nTest content.",
|
|
419
|
+
manifest: {},
|
|
420
|
+
skilltreeEnabled: true,
|
|
421
|
+
skilltreeStatus: "ready",
|
|
422
|
+
skillProfile: "testing",
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(md).toContain("### Per-Role Skills");
|
|
426
|
+
expect(md).toContain("testing");
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
432
|
+
// Group 7: Cached Loadouts — skill-loadouts.json read by agent generator
|
|
433
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
describe(
|
|
436
|
+
"tier7: skilltree cached loadouts",
|
|
437
|
+
{ timeout: 15_000 },
|
|
438
|
+
() => {
|
|
439
|
+
let workspace;
|
|
440
|
+
|
|
441
|
+
afterEach(() => {
|
|
442
|
+
if (workspace) {
|
|
443
|
+
workspace.cleanup();
|
|
444
|
+
workspace = null;
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("agent generator reads skill-loadouts.json for loadout content", async () => {
|
|
449
|
+
workspace = createWorkspace({
|
|
450
|
+
tmpdir: SHORT_TMPDIR,
|
|
451
|
+
prefix: "t7-st-cache-",
|
|
452
|
+
config: { template: "gsd", skilltree: { enabled: true } },
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const cacheDir = path.join(workspace.dir, ".swarm", "claude-swarm", "tmp", "teams", "gsd");
|
|
456
|
+
const agentsDir = path.join(cacheDir, "agents");
|
|
457
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
458
|
+
|
|
459
|
+
// Write a cached loadouts file
|
|
460
|
+
const loadouts = {
|
|
461
|
+
executor: {
|
|
462
|
+
content: "## Skill: Test-Driven Development\n\nAlways write tests first.",
|
|
463
|
+
profile: "implementation",
|
|
464
|
+
},
|
|
465
|
+
verifier: {
|
|
466
|
+
content: "## Skill: Code Review\n\nReview code thoroughly.",
|
|
467
|
+
profile: "testing",
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
fs.writeFileSync(
|
|
471
|
+
path.join(cacheDir, "skill-loadouts.json"),
|
|
472
|
+
JSON.stringify(loadouts)
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// Verify the file is readable and parseable
|
|
476
|
+
const read = JSON.parse(
|
|
477
|
+
fs.readFileSync(path.join(cacheDir, "skill-loadouts.json"), "utf-8")
|
|
478
|
+
);
|
|
479
|
+
expect(read.executor.content).toContain("Test-Driven Development");
|
|
480
|
+
expect(read.executor.profile).toBe("implementation");
|
|
481
|
+
expect(read.verifier.content).toContain("Code Review");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("handles legacy string format in skill-loadouts.json", () => {
|
|
485
|
+
// The agent generator handles both { content, profile } and plain string
|
|
486
|
+
const legacyLoadouts = {
|
|
487
|
+
executor: "## Skill: Legacy Format\n\nOld-style loadout.",
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// Simulate the getLoadout helper from agent-generator
|
|
491
|
+
function getLoadout(roleName) {
|
|
492
|
+
const entry = legacyLoadouts[roleName];
|
|
493
|
+
if (!entry) return { content: "", profile: "" };
|
|
494
|
+
if (typeof entry === "string") return { content: entry, profile: "" };
|
|
495
|
+
return { content: entry.content || "", profile: entry.profile || "" };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const { content, profile } = getLoadout("executor");
|
|
499
|
+
expect(content).toContain("Legacy Format");
|
|
500
|
+
expect(profile).toBe("");
|
|
501
|
+
|
|
502
|
+
const { content: missing } = getLoadout("nonexistent");
|
|
503
|
+
expect(missing).toBe("");
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
);
|
|
@@ -12,7 +12,7 @@ export default defineConfig({
|
|
|
12
12
|
maxForks: 1,
|
|
13
13
|
server: {
|
|
14
14
|
deps: {
|
|
15
|
-
external: ["openteams", "agent-inbox", "@multi-agent-protocol/sdk"],
|
|
15
|
+
external: ["openteams", "agent-inbox", "@multi-agent-protocol/sdk", "opentasks", "minimem", "skill-tree"],
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
18
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-swarm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Claude Code plugin for launching agent teams from openteams topologies with MAP observability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"test:e2e:tier3": "vitest run --config e2e/vitest.config.e2e.mjs -t tier3",
|
|
48
48
|
"test:e2e:tier4": "vitest run --config e2e/vitest.config.e2e.mjs -t tier4",
|
|
49
49
|
"test:e2e:tier5": "vitest run --config e2e/vitest.config.e2e.mjs -t tier5",
|
|
50
|
+
"test:e2e:tier7": "vitest run --config e2e/vitest.config.e2e.mjs -t tier7",
|
|
50
51
|
"version:patch": "npm version patch --no-git-tag-version && node scripts/sync-version.mjs",
|
|
51
52
|
"version:minor": "npm version minor --no-git-tag-version && node scripts/sync-version.mjs",
|
|
52
53
|
"version:major": "npm version major --no-git-tag-version && node scripts/sync-version.mjs"
|
|
@@ -56,7 +57,10 @@
|
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@multi-agent-protocol/sdk": "^0.1.4",
|
|
59
|
-
"agent-inbox": "^0.1.
|
|
60
|
+
"agent-inbox": "^0.1.9",
|
|
61
|
+
"minimem": "^0.1.0",
|
|
62
|
+
"opentasks": "^0.0.6",
|
|
63
|
+
"skill-tree": "^0.1.5",
|
|
60
64
|
"vitest": "^4.0.18",
|
|
61
65
|
"ws": "^8.0.0"
|
|
62
66
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-inbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "agent-inbox",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.9",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
@@ -26,6 +26,7 @@ export { TraceabilityLayer } from "./traceability/traceability.js";
|
|
|
26
26
|
export { IpcServer } from "./ipc/ipc-server.js";
|
|
27
27
|
export { MapClient } from "./map/map-client.js";
|
|
28
28
|
export { InboxMcpServer } from "./mcp/mcp-server.js";
|
|
29
|
+
export { InboxMcpProxy } from "./mcp/mcp-proxy.js";
|
|
29
30
|
export { MailJsonRpcServer } from "./jsonrpc/mail-server.js";
|
|
30
31
|
export { PushNotifier, formatInboxMarkdown, parseCommand } from "./push/notifier.js";
|
|
31
32
|
export type { InboxMessageEvent, NotifierConfig, InboxFileEntry } from "./push/notifier.js";
|
|
@@ -346,8 +347,21 @@ const isMainModule =
|
|
|
346
347
|
if (isMainModule) {
|
|
347
348
|
const mode = process.argv[2] ?? "ipc";
|
|
348
349
|
|
|
349
|
-
if (mode === "mcp") {
|
|
350
|
-
// MCP
|
|
350
|
+
if (mode === "mcp-proxy" || (mode === "mcp" && process.env.INBOX_SOCKET_PATH)) {
|
|
351
|
+
// MCP proxy mode — forward all tool calls to an existing inbox IPC socket.
|
|
352
|
+
// Auto-detected when INBOX_SOCKET_PATH is set in mcp mode.
|
|
353
|
+
const { InboxMcpProxy } = await import("./mcp/mcp-proxy.js");
|
|
354
|
+
const socketPath = process.env.INBOX_SOCKET_PATH ?? defaultSocketPath();
|
|
355
|
+
const agentId = process.env.INBOX_AGENT_ID ?? "anonymous";
|
|
356
|
+
const scope = process.env.INBOX_SCOPE ?? "default";
|
|
357
|
+
console.error(`[agent-inbox] MCP proxy mode → ${socketPath} (agent: ${agentId})`);
|
|
358
|
+
const proxy = new InboxMcpProxy(socketPath, agentId, scope);
|
|
359
|
+
proxy.start().catch((err) => {
|
|
360
|
+
console.error("MCP proxy failed:", err);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
});
|
|
363
|
+
} else if (mode === "mcp") {
|
|
364
|
+
// MCP standalone mode (stdio transport, own storage)
|
|
351
365
|
const config = loadConfig();
|
|
352
366
|
const sqlitePath = process.env.INBOX_SQLITE_PATH;
|
|
353
367
|
const storage: Storage = sqlitePath
|
|
@@ -117,6 +117,12 @@ export class IpcServer {
|
|
|
117
117
|
case "check_inbox":
|
|
118
118
|
return this.handleCheckInbox(command);
|
|
119
119
|
|
|
120
|
+
case "read_thread":
|
|
121
|
+
return this.handleReadThread(command);
|
|
122
|
+
|
|
123
|
+
case "list_agents":
|
|
124
|
+
return this.handleListAgents(command);
|
|
125
|
+
|
|
120
126
|
case "emit":
|
|
121
127
|
// For Phase 1, emit is acknowledged but not forwarded to MAP
|
|
122
128
|
return { ok: true };
|
|
@@ -170,6 +176,58 @@ export class IpcServer {
|
|
|
170
176
|
return { ok: true, messages };
|
|
171
177
|
}
|
|
172
178
|
|
|
179
|
+
private handleReadThread(
|
|
180
|
+
command: Extract<IpcCommand, { action: "read_thread" }>
|
|
181
|
+
): IpcResponse {
|
|
182
|
+
const messages = this.storage.getThread({
|
|
183
|
+
threadTag: command.threadTag,
|
|
184
|
+
scope: command.scope ?? "default",
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
threadTag: command.threadTag,
|
|
189
|
+
count: messages.length,
|
|
190
|
+
messages: messages.map((m) => ({
|
|
191
|
+
...m,
|
|
192
|
+
// Flatten for IPC consumers — keep full message but ensure key fields are accessible
|
|
193
|
+
})),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private handleListAgents(
|
|
198
|
+
command: Extract<IpcCommand, { action: "list_agents" }>
|
|
199
|
+
): IpcResponse {
|
|
200
|
+
const agents = this.storage.listAgents(command.scope);
|
|
201
|
+
const localList = agents.map((a) => ({
|
|
202
|
+
agentId: a.agent_id,
|
|
203
|
+
name: a.display_name,
|
|
204
|
+
scope: a.scope,
|
|
205
|
+
status: a.status,
|
|
206
|
+
program: a.program,
|
|
207
|
+
lastActive: a.last_active_at,
|
|
208
|
+
location: "local" as const,
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
let federatedList: IpcResponse["agents"] = [];
|
|
212
|
+
if (command.includeFederated && this.router.getFederation?.()) {
|
|
213
|
+
const federation = this.router.getFederation!()!;
|
|
214
|
+
const routingTable = federation.routing.getTable();
|
|
215
|
+
federatedList = routingTable.map((entry) => ({
|
|
216
|
+
agentId: entry.agentId,
|
|
217
|
+
peerId: entry.peerId,
|
|
218
|
+
scope: command.scope ?? "default",
|
|
219
|
+
status: entry.status,
|
|
220
|
+
location: "federated" as const,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
count: localList.length + (federatedList?.length ?? 0),
|
|
227
|
+
agents: [...localList, ...(federatedList ?? [])],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
173
231
|
private handleNotify(
|
|
174
232
|
command: Extract<IpcCommand, { action: "notify" }>
|
|
175
233
|
): IpcResponse {
|