claude-code-swarm 0.3.24 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/docs/loadout-consumer-design.md +469 -0
- package/e2e/tier7-loadout-live.test.mjs +221 -0
- package/package.json +2 -2
- package/scripts/scope-check.mjs +132 -0
- package/skills/swarm-mcp/SKILL.md +116 -0
- package/src/__tests__/cognitive-core-loadout-e2e.test.mjs +260 -0
- package/src/__tests__/e2e-loadout-demo.test.mjs +150 -0
- package/src/__tests__/fixtures/loadout-compile-team/loadouts/base-reviewer.yaml +16 -0
- package/src/__tests__/fixtures/loadout-compile-team/loadouts/extended-security.yaml +10 -0
- package/src/__tests__/fixtures/loadout-compile-team/roles/auditor.yaml +4 -0
- package/src/__tests__/fixtures/loadout-compile-team/roles/inline-extender.yaml +10 -0
- package/src/__tests__/fixtures/loadout-compile-team/roles/reviewer.yaml +4 -0
- package/src/__tests__/fixtures/loadout-compile-team/team.yaml +15 -0
- package/src/__tests__/loadout-materializer.test.mjs +578 -0
- package/src/__tests__/loadout-schema-bridge.test.mjs +177 -0
- package/src/__tests__/loadout-skilltree-compile-e2e.test.mjs +444 -0
- package/src/__tests__/loadout-template-shape.test.mjs +102 -0
- package/src/__tests__/mcp-health-checker.test.mjs +327 -0
- package/src/__tests__/scope-check.test.mjs +210 -0
- package/src/__tests__/skilltree-client.test.mjs +185 -1
- package/src/agent-generator.mjs +135 -8
- package/src/context-output.mjs +32 -0
- package/src/loadout-materializer.mjs +315 -0
- package/src/mcp-health-checker.mjs +237 -0
- package/src/skilltree-client.mjs +135 -24
- 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,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,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: []
|