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.
Files changed (42) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.claude-plugin/run-agent-inbox-mcp.sh +22 -3
  4. package/.gitattributes +3 -0
  5. package/.opentasks/config.json +9 -0
  6. package/.opentasks/graph.jsonl +0 -0
  7. package/e2e/helpers/opentasks-daemon.mjs +149 -0
  8. package/e2e/tier6-live-inbox-flow.test.mjs +938 -0
  9. package/e2e/tier7-hooks.test.mjs +992 -0
  10. package/e2e/tier7-minimem.test.mjs +461 -0
  11. package/e2e/tier7-opentasks.test.mjs +513 -0
  12. package/e2e/tier7-skilltree.test.mjs +506 -0
  13. package/e2e/vitest.config.e2e.mjs +1 -1
  14. package/package.json +6 -2
  15. package/references/agent-inbox/package-lock.json +2 -2
  16. package/references/agent-inbox/package.json +1 -1
  17. package/references/agent-inbox/src/index.ts +16 -2
  18. package/references/agent-inbox/src/ipc/ipc-server.ts +58 -0
  19. package/references/agent-inbox/src/mcp/mcp-proxy.ts +326 -0
  20. package/references/agent-inbox/src/types.ts +26 -0
  21. package/references/agent-inbox/test/ipc-new-commands.test.ts +200 -0
  22. package/references/agent-inbox/test/mcp-proxy.test.ts +191 -0
  23. package/references/minimem/package-lock.json +2 -2
  24. package/references/minimem/package.json +1 -1
  25. package/scripts/bootstrap.mjs +8 -1
  26. package/scripts/map-hook.mjs +6 -2
  27. package/scripts/map-sidecar.mjs +19 -0
  28. package/scripts/team-loader.mjs +15 -8
  29. package/skills/swarm/SKILL.md +16 -22
  30. package/src/__tests__/agent-generator.test.mjs +9 -10
  31. package/src/__tests__/context-output.test.mjs +13 -14
  32. package/src/__tests__/e2e-inbox-integration.test.mjs +732 -0
  33. package/src/__tests__/e2e-live-inbox.test.mjs +597 -0
  34. package/src/__tests__/inbox-integration.test.mjs +298 -0
  35. package/src/__tests__/integration.test.mjs +12 -11
  36. package/src/__tests__/skilltree-client.test.mjs +47 -1
  37. package/src/agent-generator.mjs +79 -88
  38. package/src/bootstrap.mjs +24 -3
  39. package/src/context-output.mjs +238 -64
  40. package/src/index.mjs +2 -0
  41. package/src/sidecar-server.mjs +30 -0
  42. package/src/skilltree-client.mjs +50 -5
package/src/bootstrap.mjs CHANGED
@@ -12,11 +12,11 @@
12
12
 
13
13
  import fs from "fs";
14
14
  import { execSync } from "child_process";
15
- import { readConfig, resolveScope } from "./config.mjs";
15
+ import { readConfig, resolveScope, resolveTeamName } from "./config.mjs";
16
16
  import { SOCKET_PATH, MAP_DIR, pluginDir, ensureSwarmDir, ensureOpentasksDir, ensureSessionDir, listSessionDirs } from "./paths.mjs";
17
17
  import { findSocketPath, isDaemonAlive, ensureDaemon } from "./opentasks-client.mjs";
18
18
  import { loadTeam } from "./template.mjs";
19
- import { killSidecar, startSidecar } from "./sidecar-client.mjs";
19
+ import { killSidecar, startSidecar, sendToInbox } from "./sidecar-client.mjs";
20
20
  import { checkSessionlogStatus, syncSessionlog } from "./sessionlog.mjs";
21
21
  import { resolveSwarmkit, configureNodePath } from "./swarmkit-resolver.mjs";
22
22
 
@@ -316,7 +316,28 @@ export async function bootstrap(pluginDirOverride, sessionId) {
316
316
  }
317
317
  }
318
318
 
319
- // 3b. Initial sessionlog sync (fire and forget)
319
+ // 3b. Register main agent in inbox for message routing
320
+ if (config.map.enabled && config.inbox?.enabled) {
321
+ const teamName = resolveTeamName(config);
322
+ const sPaths = sessionId
323
+ ? (await import("./paths.mjs")).sessionPaths(sessionId)
324
+ : { inboxSocketPath: (await import("./paths.mjs")).INBOX_SOCKET_PATH };
325
+ sendToInbox({
326
+ action: "notify",
327
+ event: {
328
+ type: "agent.spawn",
329
+ agent: {
330
+ agentId: `${teamName}-main`,
331
+ name: `${teamName}-main`,
332
+ role: "orchestrator",
333
+ scopes: [scope],
334
+ metadata: { isMain: true, sessionId },
335
+ },
336
+ },
337
+ }, sPaths.inboxSocketPath).catch(() => {});
338
+ }
339
+
340
+ // 3c. Initial sessionlog sync (fire and forget)
320
341
  if (config.map.enabled && config.sessionlog.sync !== "off") {
321
342
  syncSessionlog(config, sessionId).catch(() => {});
322
343
  }
@@ -2,72 +2,249 @@
2
2
  * context-output.mjs — Markdown formatting for hook stdout
3
3
  *
4
4
  * Generates the markdown context that gets injected into Claude's conversation
5
- * by SessionStart and team-loader hooks.
5
+ * by SessionStart and team-loader hooks. Also provides buildCapabilitiesContext()
6
+ * which is shared between SessionStart injection and AGENT.md generation.
6
7
  */
7
8
 
8
9
  import fs from "fs";
9
10
 
11
+ // ── Capabilities context (shared between main agent + spawned agents) ────────
12
+
10
13
  /**
11
- * Format the SessionStart bootstrap context.
14
+ * Build the unified capabilities context markdown.
15
+ *
16
+ * Assembles conditional sections based on which capabilities are enabled.
17
+ * Used by:
18
+ * - formatBootstrapContext() → injected into main agent via SessionStart stdout
19
+ * - generateAgentMd() → embedded in each spawned agent's AGENT.md
20
+ *
21
+ * @param {object} options
22
+ * @param {string|null} options.role - null for main agent, role name for spawned agents
23
+ * @param {boolean} options.opentasksEnabled
24
+ * @param {string} options.opentasksStatus - "connected", "enabled", "disabled", etc.
25
+ * @param {boolean} options.minimemEnabled
26
+ * @param {string} options.minimemStatus - "ready", "installed", "disabled"
27
+ * @param {boolean} options.skilltreeEnabled
28
+ * @param {string} options.skilltreeStatus - "ready", "installed", "disabled"
29
+ * @param {boolean} options.inboxEnabled
30
+ * @param {boolean} options.meshEnabled
31
+ * @param {boolean} options.mapEnabled
32
+ * @param {string} options.mapStatus - "connected (scope: ...)", "disabled", etc.
33
+ * @param {string} options.sessionlogSync - "off", "lifecycle", "metrics", "full"
34
+ * @param {string} options.teamName - team name for spawned agent context
35
+ * @returns {string} Markdown capabilities context
12
36
  */
13
- export function formatBootstrapContext({
14
- template,
15
- team,
16
- mapStatus,
17
- sessionlogStatus,
18
- sessionlogSync,
19
- opentasksStatus,
20
- inboxEnabled,
21
- minimemStatus,
22
- skilltreeStatus,
23
- }) {
24
- const lines = ["## Claude Code Swarm", ""];
37
+ export function buildCapabilitiesContext({
38
+ role = null,
39
+ opentasksEnabled = false,
40
+ opentasksStatus = "disabled",
41
+ minimemEnabled = false,
42
+ minimemStatus = "disabled",
43
+ skilltreeEnabled = false,
44
+ skilltreeStatus = "disabled",
45
+ skillProfile = "",
46
+ inboxEnabled = false,
47
+ meshEnabled = false,
48
+ mapEnabled = false,
49
+ mapStatus = "disabled",
50
+ sessionlogSync = "off",
51
+ teamName = "",
52
+ } = {}) {
53
+ const isAgent = role !== null;
54
+ const lines = ["## Swarm Capabilities", ""];
25
55
 
26
- if (template) {
27
- lines.push(`Team template configured: **${template}**`);
56
+ // ── Team Orchestration ───────────────────────────────────────────────
57
+ lines.push("### Team Orchestration");
58
+ lines.push("");
59
+ if (isAgent) {
60
+ lines.push(`You are part of the **${teamName}** team. The orchestrator spawns and coordinates all teammates — spawned agents cannot spawn other agents.`);
28
61
  } else {
29
- lines.push("No team template configured.");
62
+ lines.push("Use `/swarm` to launch a team from the configured topology, or `/swarm <template>` to pick a different one.");
63
+ lines.push("Available templates: **gsd**, **bmad-method**, **bug-fix-pipeline**, **docs-sync**, **security-audit**, and more.");
64
+ lines.push("Only the orchestrator spawns teammates — spawned agents cannot spawn other agents.");
30
65
  }
66
+ lines.push("");
31
67
 
32
- if (mapStatus) {
33
- lines.push(`MAP: ${mapStatus}`);
68
+ // ── Task Management ──────────────────────────────────────────────────
69
+ lines.push("### Task Management");
70
+ lines.push("");
71
+ const otActive = opentasksEnabled && (opentasksStatus === "connected" || opentasksStatus === "enabled");
72
+ if (otActive) {
73
+ lines.push("Use **opentasks MCP tools** for task management:");
74
+ lines.push("- `opentasks__create_task` — create tasks with metadata and links");
75
+ lines.push("- `opentasks__update_task` — claim, update status, annotate");
76
+ lines.push("- `opentasks__list_tasks` / `opentasks__query` — check progress, filter by status/assignee");
77
+ lines.push("Cross-system task graph supports linking (`opentasks__link`) and annotations.");
78
+ lines.push("Native Claude tasks are auto-federated into the graph via the claude-tasks provider.");
79
+ } else {
80
+ lines.push("Use Claude Code native task tools:");
81
+ lines.push("- `TaskCreate` — create tasks for the team");
82
+ lines.push("- `TaskUpdate` — claim (set owner), update status");
83
+ lines.push("- `TaskList` — check progress");
84
+ lines.push("Tasks are shared team-wide when agents use the same `team_name`.");
34
85
  }
86
+ lines.push("");
35
87
 
36
- if (sessionlogStatus) {
37
- if (sessionlogStatus === "active") {
38
- let syncLabel = "";
39
- if (sessionlogSync && sessionlogSync !== "off") {
40
- syncLabel = ` (MAP sync: ${sessionlogSync})`;
88
+ // ── Communication ────────────────────────────────────────────────────
89
+ lines.push("### Communication");
90
+ lines.push("");
91
+ lines.push("Use `SendMessage` for quick same-team agent-to-agent messaging:");
92
+ lines.push("- Direct: `SendMessage(recipient=\"<agent-name>\", content=\"...\")`");
93
+ lines.push("- Broadcast only when truly necessary (messages every teammate).");
94
+ lines.push("");
95
+ if (inboxEnabled) {
96
+ lines.push("**Structured messaging** via agent-inbox MCP tools (persistent, threaded, cross-system):");
97
+ lines.push("- `agent-inbox__check_inbox(agentId)` — check for new messages (auto-marks as read)");
98
+ lines.push("- `agent-inbox__send_message(to, body, from)` — send to an agent, or `agent@system` for federated");
99
+ lines.push("- `agent-inbox__read_thread(threadTag)` — read full conversation thread");
100
+ lines.push("- `agent-inbox__list_agents()` — see who is registered (local + federated)");
101
+ lines.push("");
102
+ lines.push("Use inbox for: cross-system messages, threaded conversations, delivery tracking, messaging external observers.");
103
+ lines.push("Use `SendMessage` for: quick same-team coordination that doesn't need persistence.");
104
+ }
105
+ if (meshEnabled) {
106
+ lines.push("Encrypted P2P transport via MeshPeer with agent discovery.");
107
+ }
108
+ lines.push("");
109
+
110
+ // ── Memory (minimem) ────────────────────────────────────────────────
111
+ if (minimemEnabled && minimemStatus !== "disabled") {
112
+ lines.push("### Memory");
113
+ lines.push("");
114
+ if (minimemStatus === "ready") {
115
+ lines.push("Use **minimem MCP tools** for persistent, searchable team memory. Memory is shared team-wide.");
116
+ lines.push("");
117
+ lines.push("**Searching (two-phase workflow):**");
118
+ lines.push("1. `minimem__memory_search(query)` — returns compact index (path, score, preview)");
119
+ lines.push("2. `minimem__memory_get_details(results)` — fetch full text for relevant results");
120
+ lines.push("Use `detail: \"full\"` for quick lookups. Filter by type: `type: \"decision\"` (decision, bugfix, feature, discovery, context, note).");
121
+ lines.push("");
122
+ lines.push("**Knowledge search** (structured metadata):");
123
+ lines.push("- `minimem__knowledge_search(query, { domain, entities, minConfidence })` — filter by domain/entity/confidence");
124
+ lines.push("- `minimem__knowledge_graph(nodeId, depth)` — traverse knowledge relationships");
125
+ lines.push("- `minimem__knowledge_path(fromId, toId)` — find path between knowledge nodes");
126
+ lines.push("");
127
+ if (!isAgent) {
128
+ lines.push("**Storing memories** (use filesystem tools to write Markdown):");
129
+ lines.push("- `MEMORY.md` — important decisions and architecture notes");
130
+ lines.push("- `memory/YYYY-MM-DD.md` — daily logs and session notes");
131
+ lines.push("- `memory/<topic>.md` — topic-specific files");
132
+ lines.push("");
133
+ lines.push("Format entries with a type comment for filtered search:");
134
+ lines.push("```");
135
+ lines.push("### YYYY-MM-DD HH:MM");
136
+ lines.push("<!-- type: decision -->");
137
+ lines.push("<content>");
138
+ lines.push("```");
139
+ lines.push("Types: `decision`, `bugfix`, `feature`, `discovery`, `context`, `note`.");
140
+ lines.push("Wrap secrets in `<private>` tags to exclude from indexing.");
141
+ lines.push("");
142
+ lines.push("**Team strategy**: Search memory before spawning agents for prior context on the user's goal. After team completion, store key decisions and outcomes.");
143
+ lines.push("");
144
+ } else {
145
+ lines.push("**Before major work**: Search memory for relevant prior decisions and context.");
146
+ lines.push("**After completing work**: Store key decisions and findings using filesystem tools — write to `memory/` files with `<!-- type: decision -->` tags.");
147
+ lines.push("");
41
148
  }
42
- lines.push(`Sessionlog: active${syncLabel}`);
43
- } else if (sessionlogStatus !== "not installed") {
44
- lines.push(`Sessionlog: WARNING — configured but ${sessionlogStatus}`);
149
+ } else {
150
+ lines.push(`Memory: ${minimemStatus} (minimem installed but not fully ready).`);
151
+ lines.push("");
45
152
  }
46
153
  }
47
154
 
48
- if (opentasksStatus) {
49
- lines.push(`Opentasks: ${opentasksStatus}`);
50
- if (opentasksStatus === "connected" || opentasksStatus === "enabled") {
51
- lines.push("Use **opentasks MCP tools** (opentasks__create_task, opentasks__update_task, opentasks__list_tasks) for task management instead of native TaskCreate/TaskUpdate/TaskList.");
155
+ // ── Per-Role Skills (skill-tree) ─────────────────────────────────────
156
+ if (skilltreeEnabled && skilltreeStatus !== "disabled") {
157
+ lines.push("### Per-Role Skills");
158
+ lines.push("");
159
+ if (isAgent) {
160
+ lines.push("Your **skill loadout** is embedded in the `## Skills` section above. These are versioned, reusable patterns selected for your role.");
161
+ if (skillProfile) {
162
+ lines.push(`Your loadout was compiled from the **${skillProfile}** profile.`);
163
+ }
164
+ lines.push("Read and apply these skills to guide your approach — they represent proven patterns for your type of work.");
165
+ } else {
166
+ lines.push("Each spawned agent receives a **skill loadout** compiled per-role and embedded in their AGENT.md.");
167
+ lines.push("Skills are versioned, reusable patterns from the skill-tree library, selected based on role profiles.");
168
+ lines.push("");
169
+ lines.push("Loadouts are configured via the `skilltree:` block in team.yaml, or auto-inferred from role names.");
170
+ lines.push("Built-in profiles: **code-review**, **implementation**, **debugging**, **security**, **testing**, **refactoring**, **documentation**, **devops**.");
171
+ lines.push("Skills are cached per template — delete the template cache to refresh.");
52
172
  }
173
+ lines.push("");
53
174
  }
54
175
 
55
- if (inboxEnabled) {
56
- lines.push("Inbox: enabled (agent-inbox messaging)");
176
+ // ── External Observability (MAP) ─────────────────────────────────────
177
+ lines.push("### External Observability");
178
+ lines.push("");
179
+ if (mapEnabled) {
180
+ if (isAgent) {
181
+ lines.push("MAP is active — lifecycle and task events are emitted automatically by hooks. No direct interaction needed.");
182
+ } else {
183
+ lines.push(`MAP: ${mapStatus}`);
184
+ lines.push("Agent lifecycle and task events are emitted automatically by hooks. No direct MAP interaction needed.");
185
+ }
186
+ if (sessionlogSync && sessionlogSync !== "off") {
187
+ lines.push(`Session trajectory checkpoints synced to MAP (level: ${sessionlogSync}).`);
188
+ }
189
+ } else {
190
+ lines.push("No external observability configured.");
57
191
  }
192
+ lines.push("");
58
193
 
59
- if (minimemStatus && minimemStatus !== "disabled") {
60
- lines.push(`Memory: ${minimemStatus}`);
61
- if (minimemStatus === "ready") {
62
- lines.push("Use **minimem MCP tools** (minimem__memory_search, minimem__knowledge_search) to recall past decisions and context.");
63
- }
194
+ return lines.join("\n");
195
+ }
196
+
197
+ // ── SessionStart context ─────────────────────────────────────────────────────
198
+
199
+ /**
200
+ * Format the SessionStart bootstrap context.
201
+ */
202
+ export function formatBootstrapContext({
203
+ template,
204
+ team,
205
+ mapEnabled = false,
206
+ mapStatus,
207
+ sessionlogStatus,
208
+ sessionlogSync,
209
+ opentasksEnabled = false,
210
+ opentasksStatus = "disabled",
211
+ inboxEnabled = false,
212
+ meshEnabled = false,
213
+ minimemEnabled = false,
214
+ minimemStatus = "disabled",
215
+ skilltreeEnabled = false,
216
+ skilltreeStatus = "disabled",
217
+ }) {
218
+ const lines = ["## Claude Code Swarm", ""];
219
+
220
+ if (template) {
221
+ lines.push(`Team template configured: **${template}**`);
222
+ } else {
223
+ lines.push("No team template configured.");
64
224
  }
225
+ lines.push("");
65
226
 
66
- if (skilltreeStatus && skilltreeStatus !== "disabled") {
67
- lines.push(`Skills: ${skilltreeStatus} (per-role loadouts from team.yaml)`);
227
+ // Sessionlog warning (non-capability surface config issues)
228
+ if (sessionlogStatus && sessionlogStatus !== "active" && sessionlogStatus !== "not installed") {
229
+ lines.push(`Sessionlog: WARNING — configured but ${sessionlogStatus}`);
230
+ lines.push("");
68
231
  }
69
232
 
70
- lines.push("");
233
+ // Capabilities context
234
+ lines.push(buildCapabilitiesContext({
235
+ role: null,
236
+ opentasksEnabled,
237
+ opentasksStatus,
238
+ minimemEnabled,
239
+ minimemStatus,
240
+ skilltreeEnabled,
241
+ skilltreeStatus,
242
+ inboxEnabled,
243
+ meshEnabled,
244
+ mapEnabled,
245
+ mapStatus: mapStatus || "disabled",
246
+ sessionlogSync: sessionlogSync || "off",
247
+ }));
71
248
 
72
249
  if (team) {
73
250
  // Embed the SKILL.md content directly so the agent has the topology immediately
@@ -87,7 +264,6 @@ export function formatBootstrapContext({
87
264
  lines.push(
88
265
  "Use `/swarm` to launch the team (creates a native Claude Code team)."
89
266
  );
90
- lines.push("Templates available via openteams: **gsd**, **bmad-method**, **bug-fix-pipeline**, **docs-sync** (and more)");
91
267
  lines.push("");
92
268
 
93
269
  return lines.join("\n");
@@ -95,8 +271,9 @@ export function formatBootstrapContext({
95
271
 
96
272
  /**
97
273
  * Format the team-loaded context output.
274
+ * Uses buildCapabilitiesContext for coordination/tool instructions.
98
275
  */
99
- export function formatTeamLoadedContext(generatedDir, templatePath, teamName, { opentasksStatus } = {}) {
276
+ export function formatTeamLoadedContext(generatedDir, templatePath, teamName, options = {}) {
100
277
  const lines = ["## Claude Code Swarm — Team Loaded", ""];
101
278
 
102
279
  // Include the catalog if available
@@ -121,13 +298,13 @@ export function formatTeamLoadedContext(generatedDir, templatePath, teamName, {
121
298
  "1. Call **TeamCreate** to set up a native Claude Code team"
122
299
  );
123
300
  lines.push(
124
- "2. Create tasks via **TaskCreate** based on the user's goal"
301
+ "2. Create tasks based on the user's goal"
125
302
  );
126
303
  lines.push(
127
304
  "3. Spawn agents directly as teammates (only you can spawn — teammates cannot spawn other teammates)"
128
305
  );
129
306
  lines.push(
130
- "4. Coordinate the team via **SendMessage** and track progress via **TaskUpdate**"
307
+ "4. Coordinate the team via **SendMessage** and track progress"
131
308
  );
132
309
  lines.push("");
133
310
  lines.push(
@@ -137,25 +314,22 @@ export function formatTeamLoadedContext(generatedDir, templatePath, teamName, {
137
314
  "Read a role's agent prompt before spawning an agent for that role."
138
315
  );
139
316
  lines.push("");
140
- lines.push("### Coordination");
141
- lines.push("");
142
- if (opentasksStatus === "connected" || opentasksStatus === "enabled") {
143
- lines.push("This team uses **opentasks MCP tools** for task management:");
144
- lines.push("- **opentasks__create_task / opentasks__update_task / opentasks__list_tasks** for task lifecycle");
145
- lines.push("- **SendMessage** for agent-to-agent communication");
146
- lines.push("- **TeamCreate** for team setup");
147
- } else {
148
- lines.push("This team uses Claude Code's native team features:");
149
- lines.push("- **TaskCreate/TaskUpdate** for task lifecycle");
150
- lines.push("- **SendMessage** for agent-to-agent communication");
151
- lines.push("- **TeamCreate** for team setup");
152
- }
153
- lines.push("");
154
- lines.push(
155
- "**MAP (if enabled):** Lifecycle events are emitted for external observability."
156
- );
157
- lines.push("Agents do not interact with MAP directly.");
158
- lines.push("");
317
+
318
+ // Capabilities context for coordination details
319
+ lines.push(buildCapabilitiesContext({
320
+ role: null,
321
+ opentasksEnabled: options.opentasksEnabled,
322
+ opentasksStatus: options.opentasksStatus || "disabled",
323
+ minimemEnabled: options.minimemEnabled,
324
+ minimemStatus: options.minimemStatus || "disabled",
325
+ skilltreeEnabled: options.skilltreeEnabled,
326
+ skilltreeStatus: options.skilltreeStatus || "disabled",
327
+ inboxEnabled: options.inboxEnabled,
328
+ meshEnabled: options.meshEnabled,
329
+ mapEnabled: options.mapEnabled,
330
+ mapStatus: options.mapStatus || "disabled",
331
+ sessionlogSync: options.sessionlogSync || "off",
332
+ }));
159
333
 
160
334
  return lines.join("\n");
161
335
  }
package/src/index.mjs CHANGED
@@ -41,6 +41,7 @@ export { formatInboxAsMarkdown, formatAge } from "./inbox.mjs";
41
41
 
42
42
  // Context output
43
43
  export {
44
+ buildCapabilitiesContext,
44
45
  formatBootstrapContext,
45
46
  formatTeamLoadedContext,
46
47
  formatNoTemplateMessage,
@@ -123,6 +124,7 @@ export {
123
124
  parseSkillTreeExtension,
124
125
  compileRoleLoadout,
125
126
  compileAllRoleLoadouts,
127
+ inferProfileFromRole,
126
128
  } from "./skilltree-client.mjs";
127
129
 
128
130
  // Bootstrap
@@ -176,6 +176,21 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
176
176
  metadata,
177
177
  });
178
178
  registeredAgents.set(agentId, { name, role, metadata });
179
+
180
+ // Also register in inbox storage for message routing
181
+ if (inboxInstance?.storage) {
182
+ try {
183
+ inboxInstance.storage.putAgent({
184
+ agent_id: agentId,
185
+ scope,
186
+ status: "active",
187
+ metadata: { name, role, ...metadata },
188
+ registered_at: new Date().toISOString(),
189
+ last_active_at: new Date().toISOString(),
190
+ });
191
+ } catch { /* best-effort */ }
192
+ }
193
+
179
194
  respond(client, { ok: true, agent: result });
180
195
  } catch (err) {
181
196
  process.stderr.write(
@@ -232,6 +247,21 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
232
247
  } catch {
233
248
  // Agent may already be gone
234
249
  }
250
+
251
+ // Also update inbox storage
252
+ if (inboxInstance?.storage) {
253
+ try {
254
+ inboxInstance.storage.putAgent({
255
+ agent_id: agentId,
256
+ scope,
257
+ status: "disconnected",
258
+ metadata: {},
259
+ registered_at: new Date().toISOString(),
260
+ last_active_at: new Date().toISOString(),
261
+ });
262
+ } catch { /* best-effort */ }
263
+ }
264
+
235
265
  registeredAgents.delete(agentId);
236
266
  }
237
267
  respond(client, { ok: true });
@@ -135,13 +135,49 @@ export async function compileRoleLoadout(roleName, criteria, config) {
135
135
  }
136
136
  }
137
137
 
138
+ /**
139
+ * Role name → built-in profile auto-mapping.
140
+ * Used as fallback when no explicit criteria or default profile is configured.
141
+ */
142
+ const ROLE_PROFILE_MAP = {
143
+ executor: "implementation",
144
+ developer: "implementation",
145
+ "quick-flow-dev": "implementation",
146
+ debugger: "debugging",
147
+ verifier: "testing",
148
+ qa: "testing",
149
+ "plan-checker": "code-review",
150
+ "integration-checker": "code-review",
151
+ "tech-writer": "documentation",
152
+ architect: "refactoring",
153
+ "ux-designer": "documentation",
154
+ "security-auditor": "security",
155
+ };
156
+
157
+ /**
158
+ * Infer a skill-tree profile from a role name.
159
+ * Returns empty string if no match found.
160
+ */
161
+ export function inferProfileFromRole(roleName) {
162
+ // Direct match
163
+ if (ROLE_PROFILE_MAP[roleName]) return ROLE_PROFILE_MAP[roleName];
164
+
165
+ // Partial match (e.g., "senior-developer" matches "developer")
166
+ for (const [pattern, profile] of Object.entries(ROLE_PROFILE_MAP)) {
167
+ if (roleName.includes(pattern)) return profile;
168
+ }
169
+
170
+ return "";
171
+ }
172
+
138
173
  /**
139
174
  * Compile skill loadouts for all roles in a team manifest.
140
175
  * Reads the skilltree extension from the manifest, compiles loadouts per role.
176
+ * Returns metadata alongside content for richer agent context.
141
177
  *
142
178
  * @param {object} manifest - Parsed team.yaml manifest
143
179
  * @param {object} config - Plugin config (skilltree section)
144
- * @returns {Promise<object>} Map of roleName → loadout markdown
180
+ * @returns {Promise<object>} Map of roleName → { content, profile }
145
181
  */
146
182
  export async function compileAllRoleLoadouts(manifest, config) {
147
183
  const { defaults, roles: roleOverrides } = parseSkillTreeExtension(manifest);
@@ -152,20 +188,29 @@ export async function compileAllRoleLoadouts(manifest, config) {
152
188
  // Merge defaults with role-specific overrides
153
189
  const roleCriteria = roleOverrides[roleName]
154
190
  ? { ...defaults, ...roleOverrides[roleName] }
155
- : defaults;
191
+ : { ...defaults };
156
192
 
157
- // Apply config-level default profile as final fallback
193
+ // Fallback chain for profile selection
158
194
  if (!roleCriteria.profile && !roleCriteria.tags && !roleCriteria.include && !roleCriteria.taskDescription) {
159
195
  if (config?.defaultProfile) {
160
196
  roleCriteria.profile = config.defaultProfile;
161
197
  } else {
162
- continue; // No criteria at all — skip
198
+ // Auto-infer from role name
199
+ const inferred = inferProfileFromRole(roleName);
200
+ if (inferred) {
201
+ roleCriteria.profile = inferred;
202
+ } else {
203
+ continue; // No criteria at all — skip
204
+ }
163
205
  }
164
206
  }
165
207
 
166
208
  const loadout = await compileRoleLoadout(roleName, roleCriteria, config);
167
209
  if (loadout) {
168
- result[roleName] = loadout;
210
+ result[roleName] = {
211
+ content: loadout,
212
+ profile: roleCriteria.profile || "",
213
+ };
169
214
  }
170
215
  }
171
216