claude-code-swarm 0.3.23 → 0.3.25

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 (30) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/docs/loadout-consumer-design.md +469 -0
  4. package/e2e/tier7-loadout-live.test.mjs +221 -0
  5. package/package.json +3 -3
  6. package/scripts/map-sidecar.mjs +34 -0
  7. package/scripts/scope-check.mjs +132 -0
  8. package/skills/swarm-mcp/SKILL.md +116 -0
  9. package/src/__tests__/cognitive-core-loadout-e2e.test.mjs +260 -0
  10. package/src/__tests__/e2e-loadout-demo.test.mjs +150 -0
  11. package/src/__tests__/fixtures/loadout-compile-team/loadouts/base-reviewer.yaml +16 -0
  12. package/src/__tests__/fixtures/loadout-compile-team/loadouts/extended-security.yaml +10 -0
  13. package/src/__tests__/fixtures/loadout-compile-team/roles/auditor.yaml +4 -0
  14. package/src/__tests__/fixtures/loadout-compile-team/roles/inline-extender.yaml +10 -0
  15. package/src/__tests__/fixtures/loadout-compile-team/roles/reviewer.yaml +4 -0
  16. package/src/__tests__/fixtures/loadout-compile-team/team.yaml +15 -0
  17. package/src/__tests__/loadout-materializer.test.mjs +578 -0
  18. package/src/__tests__/loadout-schema-bridge.test.mjs +177 -0
  19. package/src/__tests__/loadout-skilltree-compile-e2e.test.mjs +444 -0
  20. package/src/__tests__/loadout-template-shape.test.mjs +102 -0
  21. package/src/__tests__/mcp-health-checker.test.mjs +327 -0
  22. package/src/__tests__/scope-check.test.mjs +210 -0
  23. package/src/__tests__/skilltree-client.test.mjs +185 -1
  24. package/src/agent-generator.mjs +135 -8
  25. package/src/context-output.mjs +32 -0
  26. package/src/loadout-materializer.mjs +315 -0
  27. package/src/mcp-health-checker.mjs +237 -0
  28. package/src/opentasks-bridge.mjs +140 -0
  29. package/src/skilltree-client.mjs +135 -24
  30. package/src/template.mjs +158 -2
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Cross-repo e2e — cognitive-core publishes skills → skill-tree storage →
3
+ * cc-swarm's compileAllRoleLoadouts materializes them via openteams template.
4
+ *
5
+ * Real components throughout, no mocks:
6
+ * - cognitive-core's `convertPlaybookToSkill` + `SkillPublisher` (real
7
+ * playbook → skill conversion, real write to FS storage)
8
+ * - skill-tree's `createSkillBank` (real file-backed storage)
9
+ * - cc-swarm's `compileAllRoleLoadouts` driving skill-tree's compile
10
+ * - hand-built openteams template-shape that includes the published IDs
11
+ *
12
+ * What this proves:
13
+ * - The metric-free Skill shape (post skill-tree 0.2 + cognitive-core
14
+ * Phase 3) round-trips through both publish (write) and compile
15
+ * (read) without losing any required fields.
16
+ * - cc-swarm's bridge (`mergeOpenteamsSkillsIntoCriteria`) correctly
17
+ * surfaces playbook-derived skills via `include: [...]`.
18
+ * - The full chain works end-to-end across three packages.
19
+ */
20
+
21
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
22
+ import fs from 'node:fs';
23
+ import os from 'node:os';
24
+ import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+ import { compileAllRoleLoadouts } from '../skilltree-client.mjs';
27
+
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+
30
+ function depsAvailable() {
31
+ try {
32
+ const cwd = path.resolve(__dirname, '../..');
33
+ require.resolve('skill-tree', { paths: [cwd] });
34
+ require.resolve('cognitive-core', { paths: [cwd] });
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ const SKIP = !depsAvailable();
42
+
43
+ function makePlaybook(id, overrides = {}) {
44
+ // Minimal Playbook shape that cognitive-core's convertPlaybookToSkill
45
+ // can consume. Keeps imports light — we don't import createPlaybook
46
+ // here because cognitive-core's deep import paths aren't directly
47
+ // accessible from cc-swarm's runtime.
48
+ const now = new Date();
49
+ return {
50
+ id,
51
+ name: overrides.name ?? id,
52
+ applicability: {
53
+ situations: [overrides.situation ?? `Situation for ${id}`],
54
+ triggers: [],
55
+ antiPatterns: [],
56
+ domains: overrides.domains ?? ['testing'],
57
+ },
58
+ guidance: {
59
+ strategy: overrides.strategy ?? `Strategy: how to handle ${id}`,
60
+ tactics: overrides.tactics ?? [],
61
+ ...(overrides.codeExample && { codeExample: overrides.codeExample }),
62
+ },
63
+ verification: {
64
+ successIndicators: ['operation completes'],
65
+ failureIndicators: [],
66
+ },
67
+ evolution: {
68
+ version: overrides.version ?? '1.0.0',
69
+ createdFrom: ['session-x'],
70
+ failures: [],
71
+ refinements: [],
72
+ successCount: overrides.successCount ?? 5,
73
+ failureCount: overrides.failureCount ?? 1,
74
+ lastUsed: now,
75
+ },
76
+ provenance: { origin: 'extracted', recordedAt: now },
77
+ confidence: overrides.confidence ?? 0.85,
78
+ complexity: 'moderate',
79
+ estimatedEffort: 3,
80
+ createdAt: now,
81
+ updatedAt: now,
82
+ };
83
+ }
84
+
85
+ describe.skipIf(SKIP)(
86
+ 'cross-repo e2e — cognitive-core skills → cc-swarm compile',
87
+ () => {
88
+ /** @type {string} */
89
+ let basePath;
90
+
91
+ beforeEach(async () => {
92
+ basePath = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-cc-e2e-'));
93
+
94
+ // Use cognitive-core's publisher to write skills into a real
95
+ // skill-tree FilesystemStorageAdapter. This is the actual
96
+ // cross-repo handshake.
97
+ const ccCore = await import('cognitive-core');
98
+ const st = await import('skill-tree');
99
+
100
+ const bank = st.createSkillBank({ storage: { basePath } });
101
+ await bank.initialize();
102
+
103
+ const publisher = new ccCore.SkillPublisher(bank.getStorage());
104
+
105
+ await publisher.publishPlaybook(
106
+ makePlaybook('typescript-import-fix', {
107
+ situation: 'TypeScript ESM build emits "Cannot find module"',
108
+ strategy: 'Add .js extensions to all relative imports',
109
+ tactics: ['Use codemod', 'Verify with tsc --noEmit'],
110
+ domains: ['typescript', 'esm'],
111
+ }),
112
+ );
113
+ await publisher.publishPlaybook(
114
+ makePlaybook('react-rerender-bug', {
115
+ situation: 'Component re-renders on every parent update',
116
+ strategy: 'Wrap in React.memo and audit prop identity',
117
+ tactics: ['useCallback for callbacks', 'useMemo for objects'],
118
+ domains: ['react', 'performance'],
119
+ }),
120
+ );
121
+ await publisher.publishPlaybook(
122
+ makePlaybook('flaky-test-fix', {
123
+ situation: 'Test passes locally, fails in CI',
124
+ strategy: 'Eliminate timing dependencies with explicit waits',
125
+ domains: ['testing'],
126
+ }),
127
+ );
128
+
129
+ await bank.shutdown();
130
+ });
131
+
132
+ afterEach(() => {
133
+ fs.rmSync(basePath, { recursive: true, force: true });
134
+ });
135
+
136
+ it('cc-swarm compiles a loadout including a cognitive-core-published skill', async () => {
137
+ const manifest = { name: 'test-team', roles: ['developer'] };
138
+ const template = {
139
+ roles: new Map([
140
+ [
141
+ 'developer',
142
+ {
143
+ loadout: {
144
+ skills: { include: ['typescript-import-fix'] },
145
+ },
146
+ },
147
+ ],
148
+ ]),
149
+ };
150
+
151
+ const result = await compileAllRoleLoadouts(
152
+ manifest,
153
+ { basePath },
154
+ template,
155
+ );
156
+
157
+ expect(result.developer).toBeDefined();
158
+ expect(result.developer.content.length).toBeGreaterThan(0);
159
+ // The skill content (from cognitive-core's convertPlaybookToSkill)
160
+ // should appear in the rendered system prompt
161
+ const lower = result.developer.content.toLowerCase();
162
+ expect(lower).toContain('typescript-import-fix');
163
+ });
164
+
165
+ it('all three published skills are retrievable by id via skill-tree', async () => {
166
+ const st = await import('skill-tree');
167
+ const bank = st.createSkillBank({ storage: { basePath } });
168
+ await bank.initialize();
169
+
170
+ const ts = await bank.getSkill('typescript-import-fix');
171
+ const react = await bank.getSkill('react-rerender-bug');
172
+ const test = await bank.getSkill('flaky-test-fix');
173
+
174
+ expect(ts).not.toBeNull();
175
+ expect(react).not.toBeNull();
176
+ expect(test).not.toBeNull();
177
+
178
+ // All three have NO metrics field — proves the metric-free shape
179
+ // survived publish and read across packages
180
+ for (const skill of [ts, react, test]) {
181
+ expect(skill.metrics).toBeUndefined();
182
+ expect(skill.author).toBe('cognitive-core');
183
+ }
184
+ await bank.shutdown();
185
+ });
186
+
187
+ it('cc-swarm openteams overlay (max_tokens) flows through into skill-tree compile', async () => {
188
+ // The bridge maps openteams `loadout.skills.max_tokens` → skill-tree
189
+ // `criteria.maxTokens` (rename). Use a low budget to verify the
190
+ // value reaches skill-tree's bundle limiter.
191
+ const manifest = { name: 'test-team', roles: ['developer'] };
192
+ const include = [
193
+ 'typescript-import-fix',
194
+ 'react-rerender-bug',
195
+ 'flaky-test-fix',
196
+ ];
197
+ const buildTemplate = (max_tokens) => ({
198
+ roles: new Map([
199
+ ['developer', { loadout: { skills: { include, max_tokens } } }],
200
+ ]),
201
+ });
202
+
203
+ const low = await compileAllRoleLoadouts(
204
+ manifest,
205
+ { basePath },
206
+ buildTemplate(1),
207
+ );
208
+ const high = await compileAllRoleLoadouts(
209
+ manifest,
210
+ { basePath },
211
+ buildTemplate(100000),
212
+ );
213
+
214
+ const lowLen = low.developer?.content?.length ?? 0;
215
+ const highLen = high.developer?.content?.length ?? 0;
216
+ // High budget produces more content than effectively-zero budget —
217
+ // proves max_tokens flows through the bridge into the compile
218
+ expect(highLen).toBeGreaterThan(lowLen);
219
+ });
220
+
221
+ it('multiple roles bind to disjoint published skills', async () => {
222
+ const manifest = {
223
+ name: 'test-team',
224
+ roles: ['ts-dev', 'react-dev', 'qa'],
225
+ };
226
+ const template = {
227
+ roles: new Map([
228
+ [
229
+ 'ts-dev',
230
+ { loadout: { skills: { include: ['typescript-import-fix'] } } },
231
+ ],
232
+ [
233
+ 'react-dev',
234
+ { loadout: { skills: { include: ['react-rerender-bug'] } } },
235
+ ],
236
+ [
237
+ 'qa',
238
+ { loadout: { skills: { include: ['flaky-test-fix'] } } },
239
+ ],
240
+ ]),
241
+ };
242
+
243
+ const result = await compileAllRoleLoadouts(
244
+ manifest,
245
+ { basePath },
246
+ template,
247
+ );
248
+
249
+ expect(result['ts-dev']?.content?.toLowerCase()).toContain(
250
+ 'typescript-import-fix',
251
+ );
252
+ expect(result['react-dev']?.content?.toLowerCase()).toContain(
253
+ 'react-rerender-bug',
254
+ );
255
+ expect(result['qa']?.content?.toLowerCase()).toContain(
256
+ 'flaky-test-fix',
257
+ );
258
+ });
259
+ },
260
+ );
@@ -0,0 +1,150 @@
1
+ /**
2
+ * End-to-end integration test against the openteams loadout-demo template.
3
+ *
4
+ * Exercises the full loadout consumer path:
5
+ * cacheLoadoutArtifacts — writes loadouts/, scope/, mcp-providers.json, mcp-health.json
6
+ * generateAllAgents — writes AGENT.md with enriched frontmatter
7
+ *
8
+ * Requires openteams >= 0.3 installed (or symlinked) under node_modules.
9
+ * The test skips gracefully if openteams is unavailable at runtime.
10
+ */
11
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
12
+ import { spawnSync } from "child_process";
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import os from "os";
16
+ import yaml from "js-yaml";
17
+
18
+ // Test file is at <root>/references/claude-code-swarm/src/__tests__/*.test.mjs
19
+ // openteams demo is at <root>/references/openteams/examples/loadout-demo
20
+ const LOADOUT_DEMO = path.resolve(
21
+ new URL("..", import.meta.url).pathname, // -> src/
22
+ "..", // -> claude-code-swarm/
23
+ "..", // -> references/
24
+ "openteams",
25
+ "examples",
26
+ "loadout-demo"
27
+ );
28
+
29
+ function openteamsAvailable() {
30
+ try {
31
+ const resolved = require.resolve
32
+ ? require.resolve("openteams", {
33
+ paths: [path.resolve(new URL("..", import.meta.url).pathname, "..")],
34
+ })
35
+ : null;
36
+ return !!resolved;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ const SKIP = !fs.existsSync(LOADOUT_DEMO);
43
+
44
+ describe.skipIf(SKIP)("E2E — loadout-demo", () => {
45
+ let tmpDir;
46
+
47
+ beforeEach(() => {
48
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "swarm-e2e-"));
49
+ });
50
+
51
+ afterEach(() => {
52
+ fs.rmSync(tmpDir, { recursive: true, force: true });
53
+ });
54
+
55
+ it("cacheLoadoutArtifacts writes per-role scope + team providers + health", async () => {
56
+ const { cacheLoadoutArtifacts } = await import("../template.mjs");
57
+ const outputDir = path.join(tmpDir, "artifacts");
58
+ fs.mkdirSync(outputDir, { recursive: true });
59
+
60
+ cacheLoadoutArtifacts({
61
+ templatePath: LOADOUT_DEMO,
62
+ outputDir,
63
+ templateName: "loadout-demo",
64
+ teamName: "loadout-demo",
65
+ });
66
+
67
+ // Per-role artifacts written for roles that have loadouts
68
+ expect(fs.existsSync(path.join(outputDir, "loadouts", "implementer.json"))).toBe(true);
69
+ expect(fs.existsSync(path.join(outputDir, "loadouts", "reviewer.json"))).toBe(true);
70
+ // Planner in loadout-demo has no loadout — skipped
71
+ expect(fs.existsSync(path.join(outputDir, "loadouts", "planner.json"))).toBe(false);
72
+
73
+ // Scope files written for loadout-bearing roles
74
+ expect(fs.existsSync(path.join(outputDir, "scope", "implementer.json"))).toBe(true);
75
+ const reviewerScope = JSON.parse(
76
+ fs.readFileSync(path.join(outputDir, "scope", "reviewer.json"), "utf-8")
77
+ );
78
+ expect(reviewerScope.role).toBe("reviewer");
79
+ expect(reviewerScope.team).toBe("loadout-demo");
80
+ // Reviewer inline-extends security-auditor → should have chrome-devtools in scope
81
+ expect(reviewerScope.scope.some((s) => s.server === "chrome-devtools")).toBe(true);
82
+ // Deny list should accumulate through the inheritance chain
83
+ expect(reviewerScope.permissions.deny).toContain("Bash(git push:*)");
84
+
85
+ // Team providers cached
86
+ const providers = JSON.parse(
87
+ fs.readFileSync(path.join(outputDir, "mcp-providers.json"), "utf-8")
88
+ );
89
+ expect(Object.keys(providers)).toContain("ast-grep");
90
+ expect(Object.keys(providers)).toContain("chrome-devtools");
91
+ expect(providers["secrets-scanner"]?.ref).toBe("@openhive/secrets-scanner");
92
+
93
+ // Health report present
94
+ expect(fs.existsSync(path.join(outputDir, "mcp-health.json"))).toBe(true);
95
+ const health = JSON.parse(
96
+ fs.readFileSync(path.join(outputDir, "mcp-health.json"), "utf-8")
97
+ );
98
+ expect(Array.isArray(health.missing)).toBe(true);
99
+ expect(Array.isArray(health.ok)).toBe(true);
100
+ // secrets-scanner is a ref — should land in refs[]
101
+ expect(health.refs.some((r) => r.name === "secrets-scanner")).toBe(true);
102
+ });
103
+
104
+ it("generateAllAgents writes AGENT.md files with loadout-enriched frontmatter", async () => {
105
+ const { generateAllAgents } = await import("../agent-generator.mjs");
106
+ const outputDir = path.join(tmpDir, "agents");
107
+
108
+ const result = await generateAllAgents(LOADOUT_DEMO, outputDir, {
109
+ projectPath: tmpDir,
110
+ });
111
+ expect(result.success).toBe(true);
112
+ expect(result.roles.sort()).toEqual(
113
+ ["implementer", "planner", "reviewer"].sort()
114
+ );
115
+
116
+ // Reviewer — inline-extends security-auditor → rich frontmatter
117
+ const reviewerMd = fs.readFileSync(
118
+ path.join(outputDir, "reviewer", "AGENT.md"),
119
+ "utf-8"
120
+ );
121
+ const frontmatter = extractFrontmatter(reviewerMd);
122
+ expect(frontmatter.name).toBe("loadout-demo-reviewer");
123
+ expect(frontmatter.generated_by).toBe("claude-code-swarm");
124
+ expect(frontmatter.team_name).toBe("loadout-demo");
125
+ expect(frontmatter.role).toBe("reviewer");
126
+ expect(Array.isArray(frontmatter.mcpServers)).toBe(true);
127
+ expect(frontmatter.mcpServers).toContain("ast-grep");
128
+ expect(frontmatter.mcpServers).toContain("chrome-devtools");
129
+ // Hooks block should exist (chrome-devtools has a tools allowlist)
130
+ expect(frontmatter.hooks?.PreToolUse).toBeDefined();
131
+ expect(frontmatter.hooks.PreToolUse[0].matcher).toBe("mcp__.*");
132
+ // Capabilities flow through
133
+ expect(frontmatter.capabilities).toContain("task.update");
134
+
135
+ // Planner — no loadout → legacy minimal frontmatter
136
+ const plannerMd = fs.readFileSync(
137
+ path.join(outputDir, "planner", "AGENT.md"),
138
+ "utf-8"
139
+ );
140
+ expect(plannerMd).toContain("name: loadout-demo-planner");
141
+ // No mcpServers section since planner has no loadout
142
+ expect(plannerMd).not.toContain("mcpServers:");
143
+ });
144
+ });
145
+
146
+ function extractFrontmatter(agentMd) {
147
+ const match = agentMd.match(/^---\n([\s\S]*?)\n---/);
148
+ if (!match) return {};
149
+ return yaml.load(match[1]);
150
+ }
@@ -0,0 +1,16 @@
1
+ name: base-reviewer
2
+ description: "Baseline reviewer loadout. Uses skill-tree's built-in code-review profile so the e2e test exercises the profile-resolution path."
3
+ skills:
4
+ profile: code-review
5
+ max_tokens: 30000
6
+ capabilities:
7
+ - file.read
8
+ - codebase.search
9
+ permissions:
10
+ allow:
11
+ - "Read(**)"
12
+ deny:
13
+ - "Bash(git push:*)"
14
+ prompt_addendum: |
15
+ ## Review Mindset
16
+ Cite line numbers; suggest, don't command.
@@ -0,0 +1,10 @@
1
+ name: extended-security
2
+ description: "Security-focused extension of base-reviewer. Demonstrates extends: chain — child replaces skills.profile, inherits max_tokens + capabilities."
3
+ extends: base-reviewer
4
+ skills:
5
+ profile: security
6
+ capabilities_add:
7
+ - exec.test
8
+ permissions:
9
+ allow:
10
+ - "Bash(grep:*)"
@@ -0,0 +1,4 @@
1
+ name: auditor
2
+ display_name: Auditor
3
+ description: "Security auditor role bound to the extended-security loadout (extends base-reviewer)."
4
+ loadout: extended-security
@@ -0,0 +1,10 @@
1
+ name: inline-extender
2
+ display_name: Inline Extender
3
+ description: "Role with an inline loadout that extends the named base-reviewer. Tests inline-extends resolution through compile."
4
+ loadout:
5
+ extends: base-reviewer
6
+ capabilities_add:
7
+ - exec.run
8
+ prompt_addendum: |
9
+ ## Inline Mindset
10
+ Inherit base-reviewer; add execution capability.
@@ -0,0 +1,4 @@
1
+ name: reviewer
2
+ display_name: Reviewer
3
+ description: "Code reviewer role bound to the base-reviewer loadout."
4
+ loadout: base-reviewer
@@ -0,0 +1,15 @@
1
+ name: loadout-compile-test
2
+ description: "Fixture team for loadout-compile e2e tests. Roles bind to named loadouts that use built-in skill-tree profiles."
3
+ version: 1
4
+ roles:
5
+ - reviewer
6
+ - auditor
7
+ - inline-extender
8
+
9
+ topology:
10
+ root:
11
+ role: reviewer
12
+ spawn_rules:
13
+ reviewer: [auditor, inline-extender]
14
+ auditor: []
15
+ inline-extender: []