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.
- 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 +3 -3
- package/scripts/map-sidecar.mjs +34 -0
- 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/opentasks-bridge.mjs +140 -0
- package/src/skilltree-client.mjs +135 -24
- package/src/template.mjs +158 -2
package/src/skilltree-client.mjs
CHANGED
|
@@ -149,40 +149,137 @@ export function inferProfileFromRole(roleName) {
|
|
|
149
149
|
return "";
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// ────────────────────────────────────────────────────────────────
|
|
153
|
+
// Openteams ↔ skill-tree bridge
|
|
154
|
+
//
|
|
155
|
+
// The bridge between openteams `loadout.skills` (SkillsConfig in the
|
|
156
|
+
// schema) and skill-tree's LoadoutCriteria. skill-tree is the
|
|
157
|
+
// *mechanism*; openteams is the *declaration layer* that dispatches
|
|
158
|
+
// into it. See openhive's docs/LOADOUT_INTEGRATION.md for the model.
|
|
159
|
+
//
|
|
160
|
+
// Bridged fields are locked in by src/__tests__/loadout-schema-bridge.test.mjs
|
|
161
|
+
// which cross-references this list against openteams' SkillsConfig schema.
|
|
162
|
+
// Adding a field on either side without updating the other fails the test.
|
|
163
|
+
// ────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/** Fields on openteams `loadout.skills` that the bridge maps into
|
|
166
|
+
* LoadoutCriteria. Order is the order they appear in openteams'
|
|
167
|
+
* `loadout.schema.json` $defs.SkillsConfig.properties. */
|
|
168
|
+
export const OPENTEAMS_BRIDGED_FIELDS = Object.freeze([
|
|
169
|
+
"profile",
|
|
170
|
+
"include",
|
|
171
|
+
"exclude",
|
|
172
|
+
"max_tokens",
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Overlay an openteams `loadout.skills` block onto a skill-tree
|
|
177
|
+
* `LoadoutCriteria`. Pure function — returns a new object, does not
|
|
178
|
+
* mutate the input.
|
|
179
|
+
*
|
|
180
|
+
* Mapping:
|
|
181
|
+
* profile → criteria.profile (replace if set)
|
|
182
|
+
* include → criteria.include (union, deduped)
|
|
183
|
+
* exclude → criteria.exclude (union, deduped)
|
|
184
|
+
* max_tokens → criteria.maxTokens (replace if set)
|
|
185
|
+
*
|
|
186
|
+
* Passing null/undefined for `loadoutSkills` returns a shallow copy of
|
|
187
|
+
* the input criteria unchanged.
|
|
188
|
+
*/
|
|
189
|
+
export function mergeOpenteamsSkillsIntoCriteria(criteria, loadoutSkills) {
|
|
190
|
+
const merged = { ...(criteria ?? {}) };
|
|
191
|
+
if (!loadoutSkills) return merged;
|
|
192
|
+
if (loadoutSkills.profile) merged.profile = loadoutSkills.profile;
|
|
193
|
+
if (loadoutSkills.include?.length) {
|
|
194
|
+
merged.include = mergeUnique(merged.include, loadoutSkills.include);
|
|
195
|
+
}
|
|
196
|
+
if (loadoutSkills.exclude?.length) {
|
|
197
|
+
merged.exclude = mergeUnique(merged.exclude, loadoutSkills.exclude);
|
|
198
|
+
}
|
|
199
|
+
if (typeof loadoutSkills.max_tokens === "number") {
|
|
200
|
+
merged.maxTokens = loadoutSkills.max_tokens;
|
|
201
|
+
}
|
|
202
|
+
return merged;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolve the LoadoutCriteria for a single role. Pure function — no I/O.
|
|
207
|
+
* Returns null when no criteria can be derived (signals to the caller
|
|
208
|
+
* that the role should be skipped).
|
|
209
|
+
*
|
|
210
|
+
* Priority order:
|
|
211
|
+
* 1. team.yaml `skilltree:` extension defaults
|
|
212
|
+
* 2. team.yaml `skilltree:` extension per-role override (replaces step 1)
|
|
213
|
+
* 3. openteams `role.loadout.skills` overlay via mergeOpenteamsSkillsIntoCriteria
|
|
214
|
+
* (this is the openteams ↔ skill-tree bridge — declarations dispatch
|
|
215
|
+
* into the mechanism)
|
|
216
|
+
* 4. If no skill-bearing criteria exists yet:
|
|
217
|
+
* a. config.defaultProfile, else
|
|
218
|
+
* b. ROLE_PROFILE_MAP auto-inference from role name, else
|
|
219
|
+
* c. return null (no criteria — caller skips this role)
|
|
220
|
+
*
|
|
221
|
+
* @param {string} roleName
|
|
222
|
+
* @param {object} manifest - Parsed team.yaml manifest
|
|
223
|
+
* @param {object} [config] - Plugin config (skilltree section)
|
|
224
|
+
* @param {object} [template] - Optional openteams ResolvedTemplate
|
|
225
|
+
* @returns {object|null} LoadoutCriteria or null when role has no criteria
|
|
226
|
+
*/
|
|
227
|
+
export function computeRoleCriteria(
|
|
228
|
+
roleName,
|
|
229
|
+
manifest,
|
|
230
|
+
config = {},
|
|
231
|
+
template = null,
|
|
232
|
+
) {
|
|
233
|
+
const { defaults, roles: roleOverrides } = parseSkillTreeExtension(manifest);
|
|
234
|
+
const templateRoles = normalizeRolesMap(template?.roles);
|
|
235
|
+
|
|
236
|
+
// Step 1+2: skilltree extension defaults + per-role override
|
|
237
|
+
let roleCriteria = roleOverrides[roleName]
|
|
238
|
+
? { ...defaults, ...roleOverrides[roleName] }
|
|
239
|
+
: { ...defaults };
|
|
240
|
+
|
|
241
|
+
// Step 3: openteams loadout.skills overlay (canonical bridge — openteams wins)
|
|
242
|
+
const loadoutSkills = templateRoles?.get(roleName)?.loadout?.skills;
|
|
243
|
+
roleCriteria = mergeOpenteamsSkillsIntoCriteria(roleCriteria, loadoutSkills);
|
|
244
|
+
|
|
245
|
+
// Step 4: fallback chain — only fires when nothing skill-bearing is set yet
|
|
246
|
+
if (
|
|
247
|
+
!roleCriteria.profile &&
|
|
248
|
+
!roleCriteria.tags &&
|
|
249
|
+
!roleCriteria.include &&
|
|
250
|
+
!roleCriteria.taskDescription
|
|
251
|
+
) {
|
|
252
|
+
if (config?.defaultProfile) {
|
|
253
|
+
roleCriteria.profile = config.defaultProfile;
|
|
254
|
+
} else {
|
|
255
|
+
const inferred = inferProfileFromRole(roleName);
|
|
256
|
+
if (inferred) {
|
|
257
|
+
roleCriteria.profile = inferred;
|
|
258
|
+
} else {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return roleCriteria;
|
|
265
|
+
}
|
|
266
|
+
|
|
152
267
|
/**
|
|
153
|
-
* Compile skill loadouts for all roles in a team manifest.
|
|
154
|
-
*
|
|
155
|
-
* Returns metadata alongside content for richer agent context.
|
|
268
|
+
* Compile skill loadouts for all roles in a team manifest. Thin
|
|
269
|
+
* orchestrator over `computeRoleCriteria` + `compileRoleLoadout`.
|
|
156
270
|
*
|
|
157
271
|
* @param {object} manifest - Parsed team.yaml manifest
|
|
158
272
|
* @param {object} config - Plugin config (skilltree section)
|
|
273
|
+
* @param {object} [template] - Optional openteams ResolvedTemplate
|
|
159
274
|
* @returns {Promise<object>} Map of roleName → { content, profile }
|
|
160
275
|
*/
|
|
161
|
-
export async function compileAllRoleLoadouts(manifest, config) {
|
|
162
|
-
const { defaults, roles: roleOverrides } = parseSkillTreeExtension(manifest);
|
|
276
|
+
export async function compileAllRoleLoadouts(manifest, config, template = null) {
|
|
163
277
|
const allRoles = manifest.roles || [];
|
|
164
278
|
const result = {};
|
|
165
279
|
|
|
166
280
|
for (const roleName of allRoles) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
? { ...defaults, ...roleOverrides[roleName] }
|
|
170
|
-
: { ...defaults };
|
|
171
|
-
|
|
172
|
-
// Fallback chain for profile selection
|
|
173
|
-
if (!roleCriteria.profile && !roleCriteria.tags && !roleCriteria.include && !roleCriteria.taskDescription) {
|
|
174
|
-
if (config?.defaultProfile) {
|
|
175
|
-
roleCriteria.profile = config.defaultProfile;
|
|
176
|
-
} else {
|
|
177
|
-
// Auto-infer from role name
|
|
178
|
-
const inferred = inferProfileFromRole(roleName);
|
|
179
|
-
if (inferred) {
|
|
180
|
-
roleCriteria.profile = inferred;
|
|
181
|
-
} else {
|
|
182
|
-
continue; // No criteria at all — skip
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
281
|
+
const roleCriteria = computeRoleCriteria(roleName, manifest, config, template);
|
|
282
|
+
if (!roleCriteria) continue;
|
|
186
283
|
|
|
187
284
|
const loadout = await compileRoleLoadout(roleName, roleCriteria, config);
|
|
188
285
|
if (loadout) {
|
|
@@ -195,3 +292,17 @@ export async function compileAllRoleLoadouts(manifest, config) {
|
|
|
195
292
|
|
|
196
293
|
return result;
|
|
197
294
|
}
|
|
295
|
+
|
|
296
|
+
function normalizeRolesMap(roles) {
|
|
297
|
+
if (!roles) return null;
|
|
298
|
+
if (roles instanceof Map) return roles;
|
|
299
|
+
if (typeof roles === "object") return new Map(Object.entries(roles));
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function mergeUnique(a, b) {
|
|
304
|
+
const out = new Set();
|
|
305
|
+
for (const x of a ?? []) out.add(x);
|
|
306
|
+
for (const x of b ?? []) out.add(x);
|
|
307
|
+
return [...out];
|
|
308
|
+
}
|
package/src/template.mjs
CHANGED
|
@@ -13,6 +13,12 @@ import { getGlobalNodeModules } from "./swarmkit-resolver.mjs";
|
|
|
13
13
|
import { teamDir } from "./paths.mjs";
|
|
14
14
|
import { writeRoles } from "./roles.mjs";
|
|
15
15
|
import { createLogger } from "./log.mjs";
|
|
16
|
+
import { materializeLoadout } from "./loadout-materializer.mjs";
|
|
17
|
+
import {
|
|
18
|
+
checkMcpHealth,
|
|
19
|
+
collectScopeReferences,
|
|
20
|
+
discoverActiveSet,
|
|
21
|
+
} from "./mcp-health-checker.mjs";
|
|
16
22
|
|
|
17
23
|
const log = createLogger("template");
|
|
18
24
|
import { readConfig } from "./config.mjs";
|
|
@@ -151,7 +157,10 @@ export async function loadTeam(templateName) {
|
|
|
151
157
|
// Use template name as fallback
|
|
152
158
|
}
|
|
153
159
|
|
|
154
|
-
// Compile skill-tree loadouts if enabled (cached alongside template artifacts)
|
|
160
|
+
// Compile skill-tree loadouts if enabled (cached alongside template artifacts).
|
|
161
|
+
// When openteams is available, we also pass the full ResolvedTemplate so
|
|
162
|
+
// compileAllRoleLoadouts can read `role.loadout.skills` (first-class) in
|
|
163
|
+
// addition to the legacy `skilltree:` extension.
|
|
155
164
|
const config = readConfig();
|
|
156
165
|
if (config.skilltree?.enabled) {
|
|
157
166
|
const loadoutsPath = path.join(outputDir, "skill-loadouts.json");
|
|
@@ -159,7 +168,20 @@ export async function loadTeam(templateName) {
|
|
|
159
168
|
try {
|
|
160
169
|
const { compileAllRoleLoadouts } = await import("./skilltree-client.mjs");
|
|
161
170
|
const manifest = readTeamManifest(templatePath);
|
|
162
|
-
|
|
171
|
+
let template = null;
|
|
172
|
+
const ot = loadOpenteams();
|
|
173
|
+
if (ot?.TemplateLoader) {
|
|
174
|
+
try {
|
|
175
|
+
template = ot.TemplateLoader.load(templatePath);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
log.warn("template load for skill-tree failed", { error: err.message });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const loadouts = await compileAllRoleLoadouts(
|
|
181
|
+
manifest,
|
|
182
|
+
config.skilltree,
|
|
183
|
+
template
|
|
184
|
+
);
|
|
163
185
|
if (Object.keys(loadouts).length > 0) {
|
|
164
186
|
fs.writeFileSync(loadoutsPath, JSON.stringify(loadouts, null, 2), "utf-8");
|
|
165
187
|
}
|
|
@@ -169,9 +191,143 @@ export async function loadTeam(templateName) {
|
|
|
169
191
|
}
|
|
170
192
|
}
|
|
171
193
|
|
|
194
|
+
// Cache openteams loadout artifacts (per-role scope + providers + health report).
|
|
195
|
+
// Always regenerates on loadTeam — fresh active-set discovery each session.
|
|
196
|
+
try {
|
|
197
|
+
cacheLoadoutArtifacts({ templatePath, outputDir, templateName, teamName });
|
|
198
|
+
} catch (err) {
|
|
199
|
+
log.warn("loadout artifact caching failed", { error: err.message });
|
|
200
|
+
}
|
|
201
|
+
|
|
172
202
|
return { success: true, templateName, templatePath, outputDir, teamName, cached };
|
|
173
203
|
}
|
|
174
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Materialize per-role loadout artifacts + team MCP state to the
|
|
207
|
+
* per-template cache directory. Side-effecting; only writes under
|
|
208
|
+
* `outputDir`. Best-effort — errors are logged but don't fail loadTeam.
|
|
209
|
+
*
|
|
210
|
+
* Written outputs (all under `outputDir`):
|
|
211
|
+
* loadouts/<role>.json — openteams LoadoutArtifacts (inspection + debug)
|
|
212
|
+
* scope/<role>.json — runtime scope file read by the scope-check hook
|
|
213
|
+
* mcp-providers.json — team.mcp_providers as a plain object (when non-empty)
|
|
214
|
+
* mcp-health.json — health report: ok / missing / refs / orphaned
|
|
215
|
+
*/
|
|
216
|
+
export function cacheLoadoutArtifacts({
|
|
217
|
+
templatePath,
|
|
218
|
+
outputDir,
|
|
219
|
+
templateName,
|
|
220
|
+
teamName,
|
|
221
|
+
} = {}) {
|
|
222
|
+
const ot = loadOpenteams();
|
|
223
|
+
if (!ot?.TemplateLoader) return;
|
|
224
|
+
|
|
225
|
+
let template;
|
|
226
|
+
try {
|
|
227
|
+
template = ot.TemplateLoader.load(templatePath);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
log.warn("template load failed during loadout caching", { error: err.message });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const loadouts = template?.roles ?? new Map();
|
|
234
|
+
const providers = template?.mcpProviders ?? new Map();
|
|
235
|
+
const name = teamName || template?.manifest?.name || templateName;
|
|
236
|
+
|
|
237
|
+
// Per-role artifacts
|
|
238
|
+
const loadoutsDir = path.join(outputDir, "loadouts");
|
|
239
|
+
const scopeDir = path.join(outputDir, "scope");
|
|
240
|
+
|
|
241
|
+
// Short-circuit if there are no loadouts at all — avoid creating empty dirs.
|
|
242
|
+
const rolesWithLoadouts = [];
|
|
243
|
+
for (const [roleName, role] of loadouts) {
|
|
244
|
+
if (role?.loadout) rolesWithLoadouts.push([roleName, role]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (rolesWithLoadouts.length > 0) {
|
|
248
|
+
fs.mkdirSync(loadoutsDir, { recursive: true });
|
|
249
|
+
fs.mkdirSync(scopeDir, { recursive: true });
|
|
250
|
+
|
|
251
|
+
for (const [roleName, role] of rolesWithLoadouts) {
|
|
252
|
+
// Dump openteams' artifact view for debugging/observability.
|
|
253
|
+
if (ot.generateLoadoutArtifacts) {
|
|
254
|
+
try {
|
|
255
|
+
const artifacts = ot.generateLoadoutArtifacts(role.loadout);
|
|
256
|
+
fs.writeFileSync(
|
|
257
|
+
path.join(loadoutsDir, `${roleName}.json`),
|
|
258
|
+
JSON.stringify(artifacts, null, 2),
|
|
259
|
+
"utf-8"
|
|
260
|
+
);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
log.warn("generateLoadoutArtifacts failed", {
|
|
263
|
+
role: roleName,
|
|
264
|
+
error: err.message,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Scope file — consumed at runtime by scripts/scope-check.mjs
|
|
270
|
+
const scopeFilePath = path.join(scopeDir, `${roleName}.json`);
|
|
271
|
+
try {
|
|
272
|
+
const { scopeFile, warnings } = materializeLoadout({
|
|
273
|
+
role: {
|
|
274
|
+
name: roleName,
|
|
275
|
+
description: role.description,
|
|
276
|
+
displayName: role.displayName,
|
|
277
|
+
},
|
|
278
|
+
loadout: role.loadout,
|
|
279
|
+
template,
|
|
280
|
+
options: { teamName: name, scopeFilePath },
|
|
281
|
+
});
|
|
282
|
+
fs.writeFileSync(
|
|
283
|
+
scopeFilePath,
|
|
284
|
+
JSON.stringify(scopeFile, null, 2),
|
|
285
|
+
"utf-8"
|
|
286
|
+
);
|
|
287
|
+
for (const w of warnings) log.warn(w);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
log.warn("scope-file materialization failed", {
|
|
290
|
+
role: roleName,
|
|
291
|
+
error: err.message,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Team-level providers map
|
|
298
|
+
const providersObj =
|
|
299
|
+
providers instanceof Map ? Object.fromEntries(providers) : providers || {};
|
|
300
|
+
if (Object.keys(providersObj).length > 0) {
|
|
301
|
+
fs.writeFileSync(
|
|
302
|
+
path.join(outputDir, "mcp-providers.json"),
|
|
303
|
+
JSON.stringify(providersObj, null, 2),
|
|
304
|
+
"utf-8"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Health report — regenerated each session against current active set
|
|
309
|
+
try {
|
|
310
|
+
const pluginPath = process.env.CLAUDE_PLUGIN_ROOT;
|
|
311
|
+
const activeSet = discoverActiveSet({
|
|
312
|
+
projectPath: process.cwd(),
|
|
313
|
+
pluginPath,
|
|
314
|
+
});
|
|
315
|
+
const scopeReferences = collectScopeReferences(template.loadouts ?? new Map());
|
|
316
|
+
const report = checkMcpHealth({
|
|
317
|
+
providers,
|
|
318
|
+
activeSet,
|
|
319
|
+
scopeReferences,
|
|
320
|
+
});
|
|
321
|
+
fs.writeFileSync(
|
|
322
|
+
path.join(outputDir, "mcp-health.json"),
|
|
323
|
+
JSON.stringify(report, null, 2),
|
|
324
|
+
"utf-8"
|
|
325
|
+
);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
log.warn("mcp health check failed", { error: err.message });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
175
331
|
/**
|
|
176
332
|
* Generate team artifacts (SKILL.md + agent prompts) using openteams.
|
|
177
333
|
* Prefers programmatic API (generateSkillMd + generateAgentPrompts),
|