claude-code-swarm 0.3.24 → 0.3.26

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 (34) 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-hook.mjs +30 -5
  7. package/scripts/map-sidecar.mjs +32 -0
  8. package/scripts/scope-check.mjs +132 -0
  9. package/skills/swarm-mcp/SKILL.md +116 -0
  10. package/src/__tests__/cognitive-core-loadout-e2e.test.mjs +260 -0
  11. package/src/__tests__/e2e-loadout-demo.test.mjs +150 -0
  12. package/src/__tests__/fixtures/loadout-compile-team/loadouts/base-reviewer.yaml +16 -0
  13. package/src/__tests__/fixtures/loadout-compile-team/loadouts/extended-security.yaml +10 -0
  14. package/src/__tests__/fixtures/loadout-compile-team/roles/auditor.yaml +4 -0
  15. package/src/__tests__/fixtures/loadout-compile-team/roles/inline-extender.yaml +10 -0
  16. package/src/__tests__/fixtures/loadout-compile-team/roles/reviewer.yaml +4 -0
  17. package/src/__tests__/fixtures/loadout-compile-team/team.yaml +15 -0
  18. package/src/__tests__/loadout-materializer.test.mjs +578 -0
  19. package/src/__tests__/loadout-schema-bridge.test.mjs +176 -0
  20. package/src/__tests__/loadout-skilltree-compile-e2e.test.mjs +444 -0
  21. package/src/__tests__/loadout-template-shape.test.mjs +102 -0
  22. package/src/__tests__/mcp-health-checker.test.mjs +327 -0
  23. package/src/__tests__/scope-check.test.mjs +210 -0
  24. package/src/__tests__/sidecar-nudge.test.mjs +137 -0
  25. package/src/__tests__/skilltree-client.test.mjs +185 -1
  26. package/src/agent-generator.mjs +135 -8
  27. package/src/bootstrap.mjs +17 -9
  28. package/src/context-output.mjs +32 -0
  29. package/src/loadout-materializer.mjs +315 -0
  30. package/src/map-events.mjs +8 -1
  31. package/src/mcp-health-checker.mjs +237 -0
  32. package/src/sidecar-server.mjs +36 -0
  33. package/src/skilltree-client.mjs +135 -24
  34. package/src/template.mjs +158 -2
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Tier 7: Live Loadout E2E Tests
3
+ *
4
+ * Full end-to-end tests with a LIVE Claude Code instance to verify the
5
+ * loadout-consumer integration designed in docs/loadout-consumer-design.md.
6
+ *
7
+ * These tests cover the Claude-Code-side unknowns that pure-function +
8
+ * subprocess tests cannot prove:
9
+ * 1. Claude Code discovers file-based sub-agents at `.claude/agents/`
10
+ * with the rich frontmatter we generate (mcpServers, hooks, etc.)
11
+ * 3. PreToolUse hooks declared in sub-agent frontmatter fire, and the
12
+ * `env:` field propagates to the hook subprocess. This is the
13
+ * critical Open Verification #3 from the design doc.
14
+ *
15
+ * Gated behind LIVE_AGENT_TEST=1 — makes real LLM calls (~$1-2 per run).
16
+ *
17
+ * Run:
18
+ * LIVE_AGENT_TEST=1 npx vitest run --config e2e/vitest.config.e2e.mjs \
19
+ * e2e/tier7-loadout-live.test.mjs
20
+ */
21
+
22
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
23
+ import fs from "fs";
24
+ import path from "path";
25
+ import { execFileSync } from "child_process";
26
+ import { fileURLToPath } from "url";
27
+ import { runClaude, CLI_AVAILABLE, PLUGIN_DIR } from "./helpers/cli.mjs";
28
+ import { createWorkspace } from "./helpers/workspace.mjs";
29
+ import { cleanupWorkspace } from "./helpers/cleanup.mjs";
30
+
31
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
+ const LIVE = !!process.env.LIVE_AGENT_TEST;
33
+
34
+ const LOADOUT_DEMO = path.resolve(
35
+ __dirname,
36
+ "..",
37
+ "..",
38
+ "openteams",
39
+ "examples",
40
+ "loadout-demo"
41
+ );
42
+
43
+ const DEMO_AVAILABLE = fs.existsSync(LOADOUT_DEMO);
44
+
45
+ // ────────────────────────────────────────────────────────────────
46
+ // Test 1 — Claude Code discovers generated AGENT.md sub-agents
47
+ // ────────────────────────────────────────────────────────────────
48
+
49
+ describe.skipIf(!LIVE || !CLI_AVAILABLE || !DEMO_AVAILABLE)(
50
+ "tier7: loadout live — file-based sub-agent discovery",
51
+ { timeout: 180_000 },
52
+ () => {
53
+ let workspace;
54
+
55
+ beforeAll(async () => {
56
+ workspace = createWorkspace({
57
+ prefix: "swarm-loadout-live-disco-",
58
+ config: { template: LOADOUT_DEMO },
59
+ });
60
+
61
+ // Materialize the loadout-demo team into .claude/agents/
62
+ const agentsDir = path.join(workspace.dir, ".claude", "agents");
63
+ execFileSync(
64
+ "node",
65
+ [
66
+ path.join(PLUGIN_DIR, "scripts", "generate-agents.mjs"),
67
+ LOADOUT_DEMO,
68
+ agentsDir,
69
+ ],
70
+ { stdio: "ignore", timeout: 30_000 }
71
+ );
72
+ });
73
+
74
+ afterAll(() => {
75
+ if (workspace) cleanupWorkspace(workspace.dir);
76
+ });
77
+
78
+ it("writes AGENT.md files with expected names", () => {
79
+ const agentsDir = path.join(workspace.dir, ".claude", "agents");
80
+ expect(
81
+ fs.existsSync(path.join(agentsDir, "reviewer", "AGENT.md"))
82
+ ).toBe(true);
83
+ expect(
84
+ fs.existsSync(path.join(agentsDir, "implementer", "AGENT.md"))
85
+ ).toBe(true);
86
+ });
87
+
88
+ it("Claude Code accepts the frontmatter and lists our sub-agents", async () => {
89
+ const { messages, result, logFile } = await runClaude(
90
+ "List the names of all sub-agents available to you in this project " +
91
+ "(look at .claude/agents/). Respond with a comma-separated list " +
92
+ "of just the sub-agent names, nothing else.",
93
+ {
94
+ cwd: workspace.dir,
95
+ maxTurns: 5,
96
+ maxBudgetUsd: "0.30",
97
+ label: "tier7-disco",
98
+ }
99
+ );
100
+
101
+ expect(result, `see log: ${logFile}`).toBeTruthy();
102
+ const resultText = JSON.stringify(result || {}) + JSON.stringify(messages);
103
+
104
+ // The generated sub-agents should appear in the response.
105
+ // Claude Code auto-resolves .claude/agents/<dir>/AGENT.md as <dir>.
106
+ expect(
107
+ resultText,
108
+ `Expected Claude to list sub-agents; see log: ${logFile}`
109
+ ).toMatch(/reviewer/i);
110
+ expect(resultText).toMatch(/implementer/i);
111
+ });
112
+ }
113
+ );
114
+
115
+ // ────────────────────────────────────────────────────────────────
116
+ // Test 3 — PreToolUse hook with `env:` propagates to subprocess
117
+ //
118
+ // This is the critical Open Verification from the design doc.
119
+ // If this passes, the whole non-invasive scope-check enforcement
120
+ // strategy works. If it fails, we need a fallback (CLI args instead
121
+ // of env vars in the hook command).
122
+ // ────────────────────────────────────────────────────────────────
123
+
124
+ describe.skipIf(!LIVE || !CLI_AVAILABLE)(
125
+ "tier7: loadout live — PreToolUse hook env: in sub-agent frontmatter",
126
+ { timeout: 180_000 },
127
+ () => {
128
+ let workspace;
129
+ let probePath;
130
+ let hookScriptPath;
131
+
132
+ beforeAll(async () => {
133
+ workspace = createWorkspace({
134
+ prefix: "swarm-loadout-live-hook-",
135
+ config: { template: "" },
136
+ });
137
+
138
+ probePath = path.join(workspace.dir, "hook-probe.log");
139
+ hookScriptPath = path.join(workspace.dir, "log-env.sh");
140
+
141
+ // Hook script: write the env vars we care about to a probe file.
142
+ // Exit 0 (allow) so the tool call proceeds normally.
143
+ fs.writeFileSync(
144
+ hookScriptPath,
145
+ [
146
+ "#!/usr/bin/env bash",
147
+ `echo "PROBE_KEY=\${PROBE_KEY:-MISSING}" >> "${probePath}"`,
148
+ `echo "ROLE_NAME=\${ROLE_NAME:-MISSING}" >> "${probePath}"`,
149
+ `echo "--tool=\${CLAUDE_TOOL_NAME:-?}" >> "${probePath}"`,
150
+ "exit 0",
151
+ ].join("\n"),
152
+ { mode: 0o755 }
153
+ );
154
+
155
+ // Write a minimal sub-agent with a Bash PreToolUse hook that
156
+ // uses env: to inject PROBE_KEY and ROLE_NAME.
157
+ const agentDir = path.join(workspace.dir, ".claude", "agents");
158
+ fs.mkdirSync(agentDir, { recursive: true });
159
+ const agentMd = [
160
+ "---",
161
+ "name: probe-agent",
162
+ 'description: "Probe agent for hook env: verification"',
163
+ "tools:",
164
+ " - Bash",
165
+ "hooks:",
166
+ " PreToolUse:",
167
+ ' - matcher: "Bash"',
168
+ " hooks:",
169
+ " - type: command",
170
+ ` command: ${hookScriptPath}`,
171
+ " env:",
172
+ " PROBE_KEY: probe-value-xyz-42",
173
+ " ROLE_NAME: probe-role",
174
+ "---",
175
+ "",
176
+ "# Probe Agent",
177
+ "",
178
+ "When asked to run a shell command, run it via Bash. No commentary.",
179
+ "",
180
+ ].join("\n");
181
+ fs.writeFileSync(path.join(agentDir, "probe-agent.md"), agentMd);
182
+ });
183
+
184
+ afterAll(() => {
185
+ if (workspace) cleanupWorkspace(workspace.dir);
186
+ });
187
+
188
+ it("invokes a Bash-using sub-agent and captures hook env vars", async () => {
189
+ const { messages, result, logFile } = await runClaude(
190
+ "Use the Task tool to delegate to sub-agent 'probe-agent' with the task: " +
191
+ "\"Run the shell command: echo hello-from-probe\"",
192
+ {
193
+ cwd: workspace.dir,
194
+ maxTurns: 6,
195
+ maxBudgetUsd: "0.30",
196
+ label: "tier7-hook-env",
197
+ }
198
+ );
199
+
200
+ expect(result, `see log: ${logFile}`).toBeTruthy();
201
+
202
+ // The hook should have fired at least once — probe log must exist and
203
+ // contain the specific env value we set in the sub-agent frontmatter.
204
+ expect(
205
+ fs.existsSync(probePath),
206
+ `Hook probe log missing at ${probePath}. See runClaude log: ${logFile}. ` +
207
+ `This is the key assertion — if the file is absent, Claude Code did not ` +
208
+ `invoke the PreToolUse hook from the sub-agent's frontmatter.`
209
+ ).toBe(true);
210
+
211
+ const probe = fs.readFileSync(probePath, "utf-8");
212
+ expect(
213
+ probe,
214
+ `Env var PROBE_KEY did not propagate. Probe contents: ${probe}. ` +
215
+ `Log: ${logFile}. This means Claude Code does not honor \`env:\` in ` +
216
+ `sub-agent hook frontmatter, and we need the CLI-arg fallback.`
217
+ ).toContain("PROBE_KEY=probe-value-xyz-42");
218
+ expect(probe).toContain("ROLE_NAME=probe-role");
219
+ });
220
+ }
221
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-swarm",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
4
4
  "description": "Claude Code plugin for launching agent teams from openteams topologies with MAP observability",
5
5
  "type": "module",
6
6
  "exports": {
@@ -53,10 +53,10 @@
53
53
  "node": ">=18.0.0"
54
54
  },
55
55
  "devDependencies": {
56
- "agent-inbox": "^0.1.9",
56
+ "agent-inbox": "^0.2.3",
57
57
  "minimem": "^0.1.1",
58
58
  "opentasks": "^0.1.2",
59
- "skill-tree": "^0.1.5",
59
+ "skill-tree": "^0.2.0",
60
60
  "vitest": "^4.0.18",
61
61
  "ws": "^8.0.0"
62
62
  }
@@ -77,7 +77,29 @@ async function handleInject(hookData, sessionId) {
77
77
  const sPaths = sessionPaths(sessionId);
78
78
  const config = readConfig();
79
79
 
80
- if (!config.inbox?.enabled) return;
80
+ // Check for dispatch thread nudges (advisory push from hub).
81
+ // Nudges arrive via x-dispatch/nudge MAP notifications; the sidecar
82
+ // stores them until this hook drains them. We check nudges even when
83
+ // inbox is disabled — the nudge path is independent.
84
+ // Uses sendToInbox (which waits for a response) on the sidecar socket.
85
+ let nudgeOutput = "";
86
+ try {
87
+ const nudgeResp = await sendToInbox(
88
+ { action: "check-nudge" },
89
+ sPaths.socketPath,
90
+ );
91
+ if (nudgeResp && nudgeResp.ok && nudgeResp.nudges?.length > 0) {
92
+ const ids = nudgeResp.nudges.map((n) => n.dispatch_id).join(", ");
93
+ nudgeOutput = `\n<dispatch-thread-nudge>\nYou have pending messages in dispatch coordination thread(s): ${ids}. Check your inbox for new turns.\n</dispatch-thread-nudge>\n`;
94
+ }
95
+ } catch {
96
+ // Best effort — nudge is advisory
97
+ }
98
+
99
+ if (!config.inbox?.enabled) {
100
+ if (nudgeOutput) process.stdout.write(nudgeOutput);
101
+ return;
102
+ }
81
103
 
82
104
  // Only check messages addressed to the main agent (not all scope messages).
83
105
  // Per-agent messages stay in storage for agents to pull via MCP tools.
@@ -89,10 +111,9 @@ async function handleInject(hookData, sessionId) {
89
111
  { action: "check_inbox", agentId: mainAgentId, scope, unreadOnly: true, clear: true },
90
112
  sPaths.inboxSocketPath
91
113
  );
92
- if (!resp || !resp.ok || !resp.messages?.length) return;
93
114
 
94
115
  // Forward task.* events to opentasks graph if enabled
95
- if (config.opentasks?.enabled) {
116
+ if (resp?.ok && resp.messages?.length && config.opentasks?.enabled) {
96
117
  const otSocketPath = findSocketPath();
97
118
  const taskEvents = resp.messages.filter(
98
119
  (m) => m.content?.type === "event" && m.content?.event?.startsWith("task.")
@@ -102,8 +123,12 @@ async function handleInject(hookData, sessionId) {
102
123
  }
103
124
  }
104
125
 
105
- const output = formatInboxAsMarkdown(resp.messages);
106
- if (output) process.stdout.write(output);
126
+ const inboxOutput = resp?.ok && resp.messages?.length
127
+ ? formatInboxAsMarkdown(resp.messages)
128
+ : "";
129
+
130
+ const output = (nudgeOutput + (inboxOutput || "")).trim();
131
+ if (output) process.stdout.write(output + "\n");
107
132
  }
108
133
 
109
134
  async function handleTurnCompleted(hookData, sessionId) {
@@ -425,6 +425,34 @@ function registerContentHandler(conn) {
425
425
  });
426
426
  }
427
427
 
428
+ /**
429
+ * Register the x-dispatch/nudge notification handler on a connection.
430
+ * When the hub sends a nudge (a dispatch thread received a new turn),
431
+ * the sidecar stores the nudge so the UserPromptSubmit hook can inject
432
+ * a hint about pending messages.
433
+ */
434
+ function registerNudgeHandler(conn) {
435
+ if (!conn || typeof conn.onNotification !== "function") return;
436
+
437
+ conn.onNotification("x-dispatch/nudge", (params) => {
438
+ const dispatchId = params?.dispatch_id;
439
+ const conversationId = params?.conversation_id;
440
+ if (!dispatchId) return;
441
+
442
+ log.info("dispatch nudge received", { dispatchId, conversationId });
443
+ resetInactivityTimer();
444
+
445
+ // Store the nudge via the command handler's nudge command.
446
+ // Use a fake client since we don't need the response.
447
+ if (commandHandler) {
448
+ commandHandler(
449
+ { action: "nudge", dispatch_id: dispatchId, conversation_id: conversationId },
450
+ { write: () => {}, writable: true },
451
+ );
452
+ }
453
+ });
454
+ }
455
+
428
456
  async function startWebSocketTransport() {
429
457
  connection = await connectToMAP({
430
458
  server: MAP_SERVER,
@@ -557,6 +585,10 @@ async function main() {
557
585
  return commandHandler(command, client);
558
586
  });
559
587
 
588
+ // Register dispatch nudge handler — must come after commandHandler is created
589
+ // so the notification can store nudge state via the command handler.
590
+ registerNudgeHandler(connection);
591
+
560
592
  // Start memory file watcher if minimem is enabled
561
593
  const sidecarConfig = readConfig();
562
594
  if (sidecarConfig.minimem?.enabled) {
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scope-check.mjs — Generic PreToolUse hook for enforcing per-role MCP
4
+ * scope declared in a swarm-generated scope file.
5
+ *
6
+ * Invoked per sub-agent via frontmatter:
7
+ *
8
+ * hooks:
9
+ * PreToolUse:
10
+ * - matcher: "mcp__.*"
11
+ * hooks:
12
+ * - type: command
13
+ * command: ${CLAUDE_PLUGIN_ROOT}/scripts/scope-check.mjs
14
+ * env:
15
+ * SCOPE_FILE: .swarm/.../scope/<role>.json
16
+ * ROLE_NAME: <role>
17
+ *
18
+ * Input: tool-call JSON via stdin (Claude Code hook protocol).
19
+ * Output:
20
+ * exit 0 → allow
21
+ * exit 2 → block, with human-readable reason printed to stderr
22
+ *
23
+ * See docs/loadout-consumer-design.md for the larger design.
24
+ */
25
+
26
+ import fs from "fs";
27
+
28
+ const MCP_PREFIX = "mcp__";
29
+
30
+ async function main() {
31
+ const input = await readStdin();
32
+ let parsed;
33
+ try {
34
+ parsed = input ? JSON.parse(input) : {};
35
+ } catch {
36
+ // Can't parse input — allow, not our place to block on hook-protocol glitches.
37
+ process.exit(0);
38
+ }
39
+
40
+ const toolName = parsed?.tool_name ?? "";
41
+ if (!toolName.startsWith(MCP_PREFIX)) {
42
+ // Shouldn't happen under the mcp__.* matcher, but be defensive.
43
+ process.exit(0);
44
+ }
45
+
46
+ const parts = toolName.slice(MCP_PREFIX.length).split("__");
47
+ if (parts.length < 2) {
48
+ // Malformed MCP tool name — allow, nothing to check.
49
+ process.exit(0);
50
+ }
51
+ const server = parts[0];
52
+ const tool = parts.slice(1).join("__");
53
+
54
+ const scopeFilePath = process.env.SCOPE_FILE;
55
+ if (!scopeFilePath) {
56
+ // No scope file configured — nothing to enforce, allow.
57
+ process.exit(0);
58
+ }
59
+
60
+ let scopeDoc;
61
+ try {
62
+ scopeDoc = JSON.parse(fs.readFileSync(scopeFilePath, "utf-8"));
63
+ } catch (err) {
64
+ writeError(
65
+ `scope-check: could not read scope file at ${scopeFilePath} ` +
66
+ `(${err.message}). Allowing tool call — install claude-code-swarm ` +
67
+ `correctly or remove the hook to silence this.`
68
+ );
69
+ process.exit(0);
70
+ }
71
+
72
+ const scopeList = Array.isArray(scopeDoc?.scope) ? scopeDoc.scope : [];
73
+ const entry = scopeList.find((s) => s?.server === server);
74
+
75
+ // If the server is referenced in scope but restricted → enforce.
76
+ // If the server is not in scope at all, Claude Code's own `mcpServers:`
77
+ // allowlist should have blocked this already; we don't second-guess it.
78
+ if (!entry) {
79
+ process.exit(0);
80
+ }
81
+
82
+ const role = process.env.ROLE_NAME || scopeDoc?.role || "(unknown)";
83
+
84
+ if (Array.isArray(entry.exclude) && entry.exclude.includes(tool)) {
85
+ writeError(
86
+ `Tool ${toolName} is denied for role "${role}" (listed in scope exclude).`
87
+ );
88
+ process.exit(2);
89
+ }
90
+
91
+ if (Array.isArray(entry.tools) && entry.tools.length > 0) {
92
+ if (!entry.tools.includes(tool)) {
93
+ writeError(
94
+ `Tool ${toolName} is not in the scope allowlist for role "${role}" ` +
95
+ `(allowed: ${entry.tools.join(", ")}).`
96
+ );
97
+ process.exit(2);
98
+ }
99
+ }
100
+
101
+ // No restriction hit — allow.
102
+ process.exit(0);
103
+ }
104
+
105
+ function writeError(msg) {
106
+ process.stderr.write(msg + "\n");
107
+ }
108
+
109
+ function readStdin() {
110
+ return new Promise((resolve, reject) => {
111
+ // If stdin is a TTY we're running interactively — no input to consume.
112
+ if (process.stdin.isTTY) {
113
+ resolve("");
114
+ return;
115
+ }
116
+ let buf = "";
117
+ process.stdin.setEncoding("utf-8");
118
+ process.stdin.on("data", (chunk) => {
119
+ buf += chunk;
120
+ });
121
+ process.stdin.on("end", () => resolve(buf));
122
+ process.stdin.on("error", reject);
123
+ });
124
+ }
125
+
126
+ main().catch((err) => {
127
+ // On any unexpected error, fail open (allow) with a message.
128
+ // Hook errors should not break the agent — blocking a valid tool
129
+ // call because our hook crashed is worse than allowing an out-of-scope one.
130
+ writeError(`scope-check: unexpected error — ${err?.message ?? err}`);
131
+ process.exit(0);
132
+ });
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: swarm-mcp
3
+ description: Inspect and optionally install MCP providers declared by the current swarm team. Non-invasive by default — reports status without modifying user MCP configuration unless explicitly requested.
4
+ user_invocable: true
5
+ argument: optional
6
+ ---
7
+
8
+ # /swarm-mcp — Inspect and Manage Team MCP Providers
9
+
10
+ You manage the **MCP (Model Context Protocol) surface** for the configured swarm team. Your role is to inspect declared providers, report health, and optionally install or sync with explicit user consent.
11
+
12
+ ## Design principles
13
+
14
+ - **Non-invasive by default.** Do NOT modify `.mcp.json`, `.claude/settings.json`, `.claude/settings.local.json`, or user-global config without explicit user opt-in.
15
+ - **Check-and-notify over auto-install.** Surface discrepancies. Let the user decide.
16
+ - **Confirmation before any write.** Always show a diff preview and wait for explicit approval.
17
+
18
+ ## Artifact sources
19
+
20
+ The swarm team loader (`scripts/team-loader.mjs`) has already populated the following cache files (one pass, at session start):
21
+
22
+ - `.swarm/claude-swarm/tmp/teams/<template>/mcp-providers.json` — team-declared provider install specs
23
+ - `.swarm/claude-swarm/tmp/teams/<template>/mcp-health.json` — pre-computed health report (regenerated each session)
24
+ - `.swarm/claude-swarm/tmp/teams/<template>/scope/<role>.json` — per-role scope files consumed by the PreToolUse hook
25
+ - `.swarm/claude-swarm/tmp/teams/<template>/loadouts/<role>.json` — debug view of resolved per-role loadout
26
+
27
+ Do NOT regenerate these files. They're owned by the team-loader.
28
+
29
+ ## Subcommands
30
+
31
+ Parse `$ARGUMENTS` as the first token to determine which subcommand to run.
32
+
33
+ ### `check` — Dry-run health report
34
+
35
+ Purpose: show declared providers vs active set, flag missing / refs / disabled / orphaned scope references.
36
+
37
+ Steps:
38
+ 1. Read `.swarm/claude-swarm/tmp/teams/<template>/mcp-health.json`. If missing, ask the user to reload the team (`/swarm <template>`) — the loader populates this file.
39
+ 2. Render the report using the classification fields:
40
+ - `ok[]` — servers declared AND active (checkmark + source)
41
+ - `missing[]` — declared, not active (warning + suggest `install`)
42
+ - `refs[]` — symbolic refs deferred to consumer (inform)
43
+ - `disabled[]` — declared with `disabled: true`
44
+ - `activeOnly[]` — active but not declared (informational)
45
+ - `orphanedReferences[]` — loadout scope references not backed by anything
46
+ 3. Print a compact table. Do NOT write any files.
47
+
48
+ ### `install [name]` — Explicit MCP install
49
+
50
+ Purpose: append a team-declared provider (from `mcp-providers.json`) to the project `.mcp.json` after explicit user confirmation. If `[name]` is omitted, list installable entries and ask the user to pick.
51
+
52
+ Steps:
53
+ 1. Read `.swarm/claude-swarm/tmp/teams/<template>/mcp-providers.json`.
54
+ 2. Read (or initialize) project-root `.mcp.json`.
55
+ 3. For each requested `name`:
56
+ a. Strip openteams-specific fields (`ref`, `description`, `disabled`) from the provider spec.
57
+ b. If `.mcp.json` already contains the same name, diff the specs and show both to the user. Ask whether to overwrite.
58
+ c. Show the full diff that would land in `.mcp.json`.
59
+ d. Wait for explicit user approval (`yes` / `install` / `y`) before writing.
60
+ 4. Write `.mcp.json` atomically. Inform the user they will need to restart Claude Code for the new server to connect.
61
+
62
+ Refuse to proceed if:
63
+ - The provider has `disabled: true` (skip with a message)
64
+ - The provider has `ref:` but no bundled registry to resolve it (skip with a suggestion to run `/swarm-mcp resolve-ref`)
65
+ - The user hasn't confirmed
66
+
67
+ ### `permissions-sync` — Explicit permissions sync
68
+
69
+ Purpose: merge loadout-driven permissions into `.claude/settings.local.json` under a clearly-marked swarm block. `settings.local.json` is user-owned and typically gitignored, so we never touch `.claude/settings.json`.
70
+
71
+ Steps:
72
+ 1. Collect the union of all roles' permissions:
73
+ - `.swarm/claude-swarm/tmp/teams/<template>/scope/<role>.json` → `permissions.{allow,deny,ask}`
74
+ - Scope entries → translated to `mcp__<server>__*` (for bare server access) and `mcp__<server>__<tool>` (for exclude entries)
75
+ 2. Read `.claude/settings.local.json` (or initialize an empty object if absent).
76
+ 3. Locate or create a marked block:
77
+ ```json
78
+ "permissions": {
79
+ "allow": [
80
+ "/* swarm:<template>:start */",
81
+ "...",
82
+ "/* swarm:<template>:end */"
83
+ ]
84
+ }
85
+ ```
86
+ (If JSON doesn't tolerate comments, use a sentinel entry like `"// swarm:<template>:start"`.)
87
+ 4. Replace the marked block's contents; preserve everything outside the sentinel fences.
88
+ 5. Show the diff, wait for confirmation, write atomically.
89
+
90
+ ### `clean` — Remove swarm-generated agents
91
+
92
+ Purpose: remove `.claude/agents/<team>-*` files written by swarm, respecting the `generated_by: claude-code-swarm` marker. Never touches hand-authored agents.
93
+
94
+ Steps:
95
+ 1. Scan `.claude/agents/*.md` (or `~/.claude/agents/<project-slug>-<team>-*.md` if the current team uses user scope).
96
+ 2. Filter to files whose frontmatter contains:
97
+ - `generated_by: claude-code-swarm` AND
98
+ - `team_name: <current-template>` (if `--team` given) or any swarm team (default)
99
+ 3. Show the list and confirm with the user before deleting.
100
+ 4. Optionally also delete `.swarm/claude-swarm/tmp/teams/<template>/` cache files when `--with-artifacts` is given.
101
+
102
+ ### `resolve-ref <ref>` — (Deferred)
103
+
104
+ For future use. Currently prints a stub message indicating ref resolution requires an OpenHive hive connection or a bundled registry (not yet shipped).
105
+
106
+ ## Default behavior when no subcommand given
107
+
108
+ If `$ARGUMENTS` is empty, run the `check` subcommand — the safest and most informative default.
109
+
110
+ ## What you must NOT do
111
+
112
+ - Do not modify `.mcp.json` without explicit user confirmation
113
+ - Do not modify `.claude/settings.json` (committed project settings) — only `.claude/settings.local.json`
114
+ - Do not touch user-global config in `~/.claude/` without explicit opt-in
115
+ - Do not invoke `install` or `permissions-sync` without a clear user yes
116
+ - Do not re-run the team-loader from inside this skill (`/swarm` or `loadTeam` does that)