@klhapp/skillmux 1.10.0 → 1.11.1

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.
@@ -1,3 +1,5 @@
1
+ import { MANAGED_PINS_AGENT_IDS, SUPPORTED_AGENT_IDS } from "./init-agents";
2
+
1
3
  export type ShellType = "bash" | "zsh" | "fish";
2
4
 
3
5
  const TOP_LEVEL_COMMANDS: { name: string; description: string }[] = [
@@ -6,7 +8,7 @@ const TOP_LEVEL_COMMANDS: { name: string; description: string }[] = [
6
8
  { name: "serve", description: "Start MCP server" },
7
9
  { name: "index", description: "Rebuild local search index" },
8
10
  { name: "sync", description: "Synchronize vault skills" },
9
- { name: "init", description: "Configure this machine and its clients" },
11
+ { name: "init", description: "Configure this machine and its agents" },
10
12
  { name: "project", description: "Configure project-scoped skills" },
11
13
  { name: "target", description: "Manage advanced skill-delivery targets" },
12
14
  { name: "core", description: "Pin/unpin skills into [core]" },
@@ -62,18 +64,25 @@ _skillmux_completions() {
62
64
  local-vault)
63
65
  COMPREPLY=( $(compgen -W "init" -- "$cur") )
64
66
  ;;
65
- --client)
66
- COMPREPLY=( $(compgen -W "claude-code codex gemini-cli opencode github-copilot windsurf antigravity goose hermes skillmux-mcp" -- "$cur") )
67
+ eval)
68
+ COMPREPLY=( $(compgen -W "promote" -- "$cur") )
67
69
  ;;
68
- --target)
69
- COMPREPLY=( $(compgen -W "agent-skills claude-code codex custom" -- "$cur") )
70
+ --agent)
71
+ if [ "\${COMP_WORDS[1]}" = "project" ]; then
72
+ COMPREPLY=( $(compgen -W "${MANAGED_PINS_AGENT_IDS.join(" ")}" -- "$cur") )
73
+ else
74
+ COMPREPLY=( $(compgen -W "${SUPPORTED_AGENT_IDS.join(" ")}" -- "$cur") )
75
+ fi
70
76
  ;;
71
77
  esac
72
78
  if [ "\${COMP_WORDS[1]}" = "init" ] && [ "\${#COMPREPLY[@]}" -eq 0 ]; then
73
- COMPREPLY=( $(compgen -W "--client --target --dir --vault --core --migrate-full-vault --no-instructions --no-sync --interactive --yes --dry-run --json" -- "$cur") )
79
+ COMPREPLY=( $(compgen -W "--agent --vault --core --migrate-full-vault --show-mcp-setup --register-mcp --no-instructions --no-sync --interactive --yes --dry-run --json" -- "$cur") )
74
80
  fi
75
81
  if [ "\${COMP_WORDS[1]}" = "project" ] && [ "\${COMP_WORDS[2]}" = "init" ]; then
76
- COMPREPLY=( $(compgen -W "--name --skill --client --target --no-sync --interactive --yes --dry-run --json" -- "$cur") )
82
+ COMPREPLY=( $(compgen -W "--name --skill --agent --target --register-mcp --no-sync --interactive --yes --dry-run --json" -- "$cur") )
83
+ fi
84
+ if [ "\${COMP_WORDS[1]}" = "eval" ] && [ "\${COMP_WORDS[2]}" = "promote" ]; then
85
+ COMPREPLY=( $(compgen -W "--since --out --dry-run --yes --json" -- "$cur") )
77
86
  fi
78
87
  }
79
88
  complete -F _skillmux_completions skillmux
@@ -92,12 +101,12 @@ ${commands}
92
101
  _describe -t commands 'skillmux command' commands
93
102
  elif [[ "$words[2]" == "init" ]]; then
94
103
  _arguments \\
95
- '*--client[select a client]:client:(claude-code codex gemini-cli opencode github-copilot windsurf antigravity goose hermes skillmux-mcp)' \\
96
- '*--target[select a delivery target]:target:(agent-skills claude-code codex custom)' \\
97
- '--dir[custom target directory]:directory:_directories' \\
104
+ '*--agent[select an agent]:agent:(${SUPPORTED_AGENT_IDS.join(" ")})' \\
98
105
  '--vault[vault directory]:directory:_directories' \\
99
106
  '*--core[seed a core skill]:skill id:' \\
100
107
  '--migrate-full-vault[convert a full-vault symlink to managed pins]' \\
108
+ '--show-mcp-setup[also print the MCP registration snippet]' \\
109
+ '--register-mcp[register skillmux via the agent own CLI]' \\
101
110
  '--no-instructions[skip managed instruction files]' \\
102
111
  '--no-sync[save setup without synchronizing targets]' \\
103
112
  '--interactive[force guided setup]' \\
@@ -109,8 +118,9 @@ ${commands}
109
118
  '1:project directory:_directories' \\
110
119
  '--name[project group name]:group:' \\
111
120
  '*--skill[project skill]:skill id:' \\
112
- '*--client[select a client]:client:(claude-code codex gemini-cli opencode github-copilot windsurf antigravity)' \\
121
+ '*--agent[select an agent]:agent:(${MANAGED_PINS_AGENT_IDS.join(" ")})' \\
113
122
  '*--target[select an advanced target]:target:' \\
123
+ '--register-mcp[register a project-scoped MCP server for claude-code]' \\
114
124
  '--no-sync[save setup without synchronizing targets]' \\
115
125
  '--interactive[force guided setup]' \\
116
126
  '--yes[apply without prompts]' \\
@@ -118,6 +128,15 @@ ${commands}
118
128
  '--json[emit a JSON envelope]'
119
129
  elif [[ "$words[2]" == "project" && CURRENT == 3 ]]; then
120
130
  _values 'project command' init list show add-path remove-path pin unpin attach detach
131
+ elif [[ "$words[2]" == "eval" && "$words[3]" == "promote" ]]; then
132
+ _arguments \
133
+ '--since[time window]:window:' \
134
+ '--out[output file]:file:_files' \
135
+ '--dry-run[print the plan without writing]' \
136
+ '--yes[apply without prompts]' \
137
+ '--json[emit a JSON envelope]'
138
+ elif [[ "$words[2]" == "eval" && CURRENT == 3 ]]; then
139
+ _values 'eval command' promote
121
140
  elif [[ "$words[2]" == "target" && CURRENT == 3 ]]; then
122
141
  _values 'target command' list show add remove
123
142
  elif [[ "$words[2]" == "skill" && CURRENT == 3 ]]; then
@@ -139,12 +158,12 @@ _skillmux "$@"
139
158
  return `# fish completion for skillmux
140
159
  complete -c skillmux -f
141
160
  ${topLevel}
142
- complete -c skillmux -n "__fish_seen_subcommand_from init" -l client -x -a "claude-code codex gemini-cli opencode github-copilot windsurf antigravity goose hermes skillmux-mcp" -d "Select a client"
143
- complete -c skillmux -n "__fish_seen_subcommand_from init" -l target -x -a "agent-skills claude-code codex custom" -d "Select a delivery target"
144
- complete -c skillmux -n "__fish_seen_subcommand_from init" -l dir -r -d "Custom target directory"
161
+ complete -c skillmux -n "__fish_seen_subcommand_from init" -l agent -x -a "${SUPPORTED_AGENT_IDS.join(" ")}" -d "Select an agent"
145
162
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l vault -r -d "Vault directory"
146
163
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l core -x -d "Seed a core skill"
147
164
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l migrate-full-vault -d "Convert a full-vault symlink"
165
+ complete -c skillmux -n "__fish_seen_subcommand_from init" -l show-mcp-setup -d "Also print the MCP registration snippet"
166
+ complete -c skillmux -n "__fish_seen_subcommand_from init" -l register-mcp -d "Register skillmux via the agent's own CLI"
148
167
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l no-instructions -d "Skip managed instruction files"
149
168
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l no-sync -d "Save without synchronizing"
150
169
  complete -c skillmux -n "__fish_seen_subcommand_from init" -l interactive -d "Force guided setup"
@@ -154,11 +173,18 @@ complete -c skillmux -n "__fish_seen_subcommand_from init" -l json -d "Emit a JS
154
173
  complete -c skillmux -n "__fish_seen_subcommand_from project" -a "init list show add-path remove-path pin unpin attach detach" -d "Manage projects"
155
174
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l name -x -d "Project group name"
156
175
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l skill -x -d "Project skill"
157
- complete -c skillmux -n "__fish_seen_subcommand_from project" -l client -x -a "claude-code codex gemini-cli opencode github-copilot windsurf antigravity" -d "Select a client"
176
+ complete -c skillmux -n "__fish_seen_subcommand_from project" -l agent -x -a "${MANAGED_PINS_AGENT_IDS.join(" ")}" -d "Select an agent"
158
177
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l target -x -d "Select an advanced delivery target"
178
+ complete -c skillmux -n "__fish_seen_subcommand_from project" -l register-mcp -d "Register a project-scoped MCP server for claude-code"
159
179
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l no-sync -d "Save without synchronizing"
160
180
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l interactive -d "Force guided setup"
161
181
  complete -c skillmux -n "__fish_seen_subcommand_from project" -l yes -d "Apply without prompts"
182
+ complete -c skillmux -n "__fish_seen_subcommand_from eval" -a "promote" -d "Promote correlated fetches into eval cases"
183
+ complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l since -x -d "Time window"
184
+ complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l out -r -d "Output file"
185
+ complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l dry-run -d "Print the plan without writing"
186
+ complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l yes -d "Apply without prompts"
187
+ complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l json -d "Emit a JSON envelope"
162
188
  complete -c skillmux -n "__fish_seen_subcommand_from target" -a "list show add remove" -d "Manage targets"
163
189
  complete -c skillmux -n "__fish_seen_subcommand_from core" -a "pin unpin" -d "Manage [core] pins"
164
190
  complete -c skillmux -n "__fish_seen_subcommand_from skill" -a "which" -d "Show which root resolves a skill_id"
@@ -309,7 +309,7 @@ export async function getDottedKey(key: string, configPath?: string): Promise<un
309
309
  export async function setDottedKey(
310
310
  key: string,
311
311
  rawValStr: string,
312
- opts?: { configPath?: string; dryRun?: boolean; targetName?: string }
312
+ opts?: { configPath?: string; dryRun?: boolean; contextName?: string }
313
313
  ): Promise<SetConfigResult> {
314
314
  validateDottedKey(key);
315
315
  if (isEnvMasked(key)) {
@@ -317,7 +317,7 @@ export async function setDottedKey(
317
317
  }
318
318
 
319
319
  const path = opts?.configPath ?? process.env.SKILLMUX_CONFIG ?? DEFAULT_CONFIG_PATH;
320
- const targetName = opts?.targetName ?? "local";
320
+ const contextName = opts?.contextName ?? "local";
321
321
 
322
322
  const { effective: priorEffective, rawToml } = await getEffectiveConfig(path);
323
323
  const priorVal = getNestedValue(priorEffective as Record<string, any>, key);
@@ -364,7 +364,7 @@ export async function setDottedKey(
364
364
  key,
365
365
  prior_val: priorVal,
366
366
  resulting_val: parsedVal,
367
- target: targetName,
367
+ target: contextName,
368
368
  prior_revision: priorRevision,
369
369
  resulting_revision: resultingRevision,
370
370
  persistence,
@@ -0,0 +1,329 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ export const SUPPORTED_AGENT_IDS = [
6
+ "claude-code",
7
+ "codex",
8
+ "opencode",
9
+ "github-copilot",
10
+ "windsurf",
11
+ "antigravity",
12
+ "goose",
13
+ "hermes",
14
+ ] as const;
15
+
16
+ export type AgentId = (typeof SUPPORTED_AGENT_IDS)[number];
17
+ export type DeliveryMode = "managed-pins" | "full-vault";
18
+
19
+ export interface DetectedAgent {
20
+ agent: AgentId;
21
+ evidence: string;
22
+ }
23
+
24
+ export type McpRegistrationScope = "user" | "project";
25
+
26
+ export interface McpRegistrationCommand {
27
+ command: string;
28
+ /** Returns undefined when this agent's CLI has no way to register at the requested scope. */
29
+ buildArgs: (scope: McpRegistrationScope) => string[] | undefined;
30
+ }
31
+
32
+ interface AgentPathOptions {
33
+ home: string;
34
+ codexHome: string;
35
+ }
36
+
37
+ interface AgentInstructionOptions extends AgentPathOptions {
38
+ claudeConfigDir: string;
39
+ }
40
+
41
+ /**
42
+ * The single source of truth for what each agent supports. Every other
43
+ * module (detection, skill-surface planning, readiness reporting, MCP
44
+ * registration, instruction files, shell completions, CLI help text) reads
45
+ * from this record instead of keeping its own copy — see git history for
46
+ * what it looked like before consolidation (agent support was duplicated
47
+ * across 6+ files and could silently drift, e.g. shell completions offering
48
+ * goose/hermes for `project init` when they can't actually be attached).
49
+ */
50
+ interface AgentDefinition {
51
+ id: AgentId;
52
+ surfaceId?: "agent-skills" | "claude-code" | "codex" | "antigravity";
53
+ deliveryMode: DeliveryMode;
54
+ /** Filesystem evidence used by guided-mode agent detection. */
55
+ detectionPath?: (options: AgentPathOptions) => string;
56
+ /** Bespoke readiness message for full-vault agents with no managed-pins surface. */
57
+ manualSkillSurfaceMessage?: string;
58
+ instructions?: {
59
+ global?: (options: AgentInstructionOptions) => string;
60
+ project?: (projectRoot: string) => string;
61
+ };
62
+ /**
63
+ * Only set for agents whose own CLI's exact registration command was
64
+ * verified against its own --help, not guessed from docs. Every other
65
+ * agent falls back to printing the registration snippet (see
66
+ * printLastMile in init.ts) rather than a guessed command.
67
+ */
68
+ mcpRegistration?: McpRegistrationCommand;
69
+ }
70
+
71
+ export interface PlannedAgentSurface {
72
+ id: string;
73
+ targetName: string;
74
+ path: string;
75
+ deliveryMode: "managed-pins";
76
+ agents: AgentId[];
77
+ }
78
+
79
+ export interface AgentSurfacePlan {
80
+ agents: AgentDefinition[];
81
+ surfaces: PlannedAgentSurface[];
82
+ }
83
+
84
+ type ReadinessStatus = "ready" | "planned" | "manual" | "not-applicable";
85
+
86
+ export interface ReadinessAxis {
87
+ status: ReadinessStatus;
88
+ detail: string;
89
+ }
90
+
91
+ export interface AgentReadiness {
92
+ agent: AgentId;
93
+ skillSurface: ReadinessAxis;
94
+ mcpRegistration: ReadinessAxis;
95
+ instructionSetup: ReadinessAxis;
96
+ }
97
+
98
+ export interface ResolvedBuiltInTarget {
99
+ targetName: string;
100
+ path: string;
101
+ }
102
+
103
+ /** Target names whose install directory is deterministic — --dir is optional for these. */
104
+ export const BUILT_IN_TARGET_NAMES = new Set(["agent-skills", "claude-code", "codex"]);
105
+
106
+ const AGENTS: Record<AgentId, AgentDefinition> = {
107
+ "claude-code": {
108
+ id: "claude-code",
109
+ surfaceId: "claude-code",
110
+ deliveryMode: "managed-pins",
111
+ detectionPath: ({ home }) => join(home, ".claude"),
112
+ instructions: {
113
+ global: ({ claudeConfigDir }) => join(claudeConfigDir, "CLAUDE.md"),
114
+ project: (projectRoot) => join(projectRoot, "CLAUDE.md"),
115
+ },
116
+ mcpRegistration: {
117
+ command: "claude",
118
+ // claude mcp add --scope: local (default, unshared), project (writes a
119
+ // committed .mcp.json), or user (global). "project" is the only shared
120
+ // option, so that's what a project-scoped registration means here.
121
+ buildArgs: (scope) => [
122
+ "mcp",
123
+ "add",
124
+ "-s",
125
+ scope === "project" ? "project" : "user",
126
+ "skillmux",
127
+ "--",
128
+ "skillmux",
129
+ "serve",
130
+ ],
131
+ },
132
+ },
133
+ codex: {
134
+ id: "codex",
135
+ surfaceId: "codex",
136
+ deliveryMode: "managed-pins",
137
+ detectionPath: ({ codexHome }) => codexHome,
138
+ instructions: {
139
+ global: ({ codexHome }) => join(codexHome, "AGENTS.md"),
140
+ },
141
+ mcpRegistration: {
142
+ command: "codex",
143
+ // codex mcp add has no --scope flag at all — it always writes to the
144
+ // global ~/.codex/config.toml, so project scope is not representable.
145
+ buildArgs: (scope) =>
146
+ scope === "project"
147
+ ? undefined
148
+ : ["mcp", "add", "skillmux", "--", "skillmux", "serve"],
149
+ },
150
+ },
151
+ opencode: {
152
+ id: "opencode",
153
+ surfaceId: "agent-skills",
154
+ deliveryMode: "managed-pins",
155
+ detectionPath: ({ home }) => join(home, ".config", "opencode"),
156
+ instructions: {
157
+ global: ({ home }) => join(home, ".config", "opencode", "AGENTS.md"),
158
+ },
159
+ },
160
+ "github-copilot": {
161
+ id: "github-copilot",
162
+ surfaceId: "agent-skills",
163
+ deliveryMode: "managed-pins",
164
+ detectionPath: ({ home }) => join(home, ".config", "github-copilot"),
165
+ },
166
+ windsurf: {
167
+ id: "windsurf",
168
+ surfaceId: "agent-skills",
169
+ deliveryMode: "managed-pins",
170
+ detectionPath: ({ home }) => join(home, ".codeium", "windsurf"),
171
+ },
172
+ antigravity: {
173
+ id: "antigravity",
174
+ surfaceId: "antigravity",
175
+ deliveryMode: "managed-pins",
176
+ instructions: {
177
+ global: ({ home }) => join(home, ".gemini", "GEMINI.md"),
178
+ },
179
+ },
180
+ goose: {
181
+ id: "goose",
182
+ deliveryMode: "full-vault",
183
+ detectionPath: ({ home }) => join(home, ".config", "goose"),
184
+ manualSkillSurfaceMessage: "configure the full vault in Goose",
185
+ instructions: {
186
+ global: ({ home }) => join(home, ".config", "goose", ".goosehints"),
187
+ },
188
+ },
189
+ hermes: {
190
+ id: "hermes",
191
+ deliveryMode: "full-vault",
192
+ detectionPath: ({ home }) => join(home, ".hermes"),
193
+ manualSkillSurfaceMessage: "configure the full vault in Hermes external_dirs",
194
+ instructions: {
195
+ global: ({ home }) => join(home, ".hermes.md"),
196
+ },
197
+ },
198
+ };
199
+
200
+ export function getAgentDefinition(agent: AgentId): AgentDefinition {
201
+ return AGENTS[agent];
202
+ }
203
+
204
+ /** Agents that can be attached to a project group (managed-pins delivery, not full-vault). */
205
+ export const MANAGED_PINS_AGENT_IDS = SUPPORTED_AGENT_IDS.filter(
206
+ (id) => AGENTS[id].deliveryMode === "managed-pins",
207
+ );
208
+
209
+ export function detectInstalledAgents(
210
+ options: {
211
+ home?: string;
212
+ codexHome?: string;
213
+ exists?: (path: string) => boolean;
214
+ } = {},
215
+ ): DetectedAgent[] {
216
+ const home = options.home ?? homedir();
217
+ const codexHome = options.codexHome ?? join(home, ".codex");
218
+ const exists = options.exists ?? existsSync;
219
+ return SUPPORTED_AGENT_IDS.flatMap((agent) => {
220
+ const evidence = AGENTS[agent].detectionPath?.({ home, codexHome });
221
+ return evidence && exists(evidence) ? [{ agent, evidence }] : [];
222
+ });
223
+ }
224
+
225
+ function surfacePath(
226
+ surfaceId: NonNullable<AgentDefinition["surfaceId"]>,
227
+ options: { home: string; codexHome?: string },
228
+ ): string {
229
+ if (surfaceId === "agent-skills") return join(options.home, ".agents", "skills");
230
+ if (surfaceId === "claude-code") return join(options.home, ".claude", "skills");
231
+ if (surfaceId === "codex") return join(options.codexHome ?? join(options.home, ".codex"), "skills");
232
+ return join(options.home, ".gemini", "config", "skills");
233
+ }
234
+
235
+ export function resolveBuiltInTarget(
236
+ name: string,
237
+ options: { home?: string; codexHome?: string; customPath?: string } = {},
238
+ ): ResolvedBuiltInTarget {
239
+ const home = options.home ?? homedir();
240
+ if (name === "custom") {
241
+ if (!options.customPath) throw new Error("--target custom requires --path <dir>");
242
+ return { targetName: name, path: options.customPath };
243
+ }
244
+ if (name === "agent-skills") {
245
+ return { targetName: name, path: surfacePath("agent-skills", { home }) };
246
+ }
247
+ if (name === "claude-code") {
248
+ return { targetName: name, path: surfacePath("claude-code", { home }) };
249
+ }
250
+ if (name === "codex") {
251
+ return {
252
+ targetName: name,
253
+ path: surfacePath("codex", { home, codexHome: options.codexHome }),
254
+ };
255
+ }
256
+ throw new Error(
257
+ `unknown --target "${name}"; supported targets: agent-skills, claude-code, codex, custom`,
258
+ );
259
+ }
260
+
261
+ export function planAgentSurfaces(
262
+ requestedAgents: readonly string[],
263
+ options: { home?: string; codexHome?: string } = {},
264
+ ): AgentSurfacePlan {
265
+ const agents = [...new Set(requestedAgents)].map((id) => {
266
+ if (!SUPPORTED_AGENT_IDS.includes(id as AgentId)) {
267
+ throw new Error(
268
+ `unsupported agent "${id}"; supported agents: ${SUPPORTED_AGENT_IDS.join(", ")}`,
269
+ );
270
+ }
271
+ return AGENTS[id as AgentId];
272
+ });
273
+ const home = options.home ?? homedir();
274
+ const surfaces = new Map<string, PlannedAgentSurface>();
275
+
276
+ for (const agent of agents) {
277
+ if (!agent.surfaceId) continue;
278
+ const path = surfacePath(agent.surfaceId, { home, codexHome: options.codexHome });
279
+ const existing = surfaces.get(path);
280
+ if (existing) {
281
+ if (!existing.agents.includes(agent.id)) existing.agents.push(agent.id);
282
+ continue;
283
+ }
284
+ surfaces.set(path, {
285
+ id: agent.surfaceId,
286
+ targetName: agent.surfaceId,
287
+ path,
288
+ deliveryMode: "managed-pins",
289
+ agents: [agent.id],
290
+ });
291
+ }
292
+
293
+ return { agents, surfaces: [...surfaces.values()] };
294
+ }
295
+
296
+ export function assessAgentReadiness(
297
+ plan: AgentSurfacePlan,
298
+ instructionReadiness: Partial<Record<AgentId, ReadinessAxis>> = {},
299
+ ): AgentReadiness[] {
300
+ return plan.agents.map((agent) => {
301
+ const surface = plan.surfaces.find((candidate) => candidate.agents.includes(agent.id));
302
+ let skillSurface: ReadinessAxis;
303
+ if (surface) {
304
+ skillSurface = { status: "planned", detail: surface.path };
305
+ } else if (agent.manualSkillSurfaceMessage) {
306
+ skillSurface = { status: "manual", detail: agent.manualSkillSurfaceMessage };
307
+ } else {
308
+ skillSurface = {
309
+ status: "not-applicable",
310
+ detail: "skills resolve through Skillmux MCP",
311
+ };
312
+ }
313
+
314
+ const mcpRegistration: ReadinessAxis = {
315
+ status: "not-applicable",
316
+ detail: "native skill loading",
317
+ };
318
+
319
+ return {
320
+ agent: agent.id,
321
+ skillSurface,
322
+ mcpRegistration,
323
+ instructionSetup: instructionReadiness[agent.id] ?? {
324
+ status: "manual",
325
+ detail: "instruction adapter not applied",
326
+ },
327
+ };
328
+ });
329
+ }
@@ -9,7 +9,7 @@ import {
9
9
  } from "node:fs";
10
10
  import { dirname, join } from "node:path";
11
11
  import { DISCOVERY_PARAGRAPH } from "./init";
12
- import type { ClientId } from "./init-clients";
12
+ import { getAgentDefinition, type AgentId } from "./init-agents";
13
13
 
14
14
  export const INSTRUCTION_BLOCK_START = "<!-- skillmux:discovery:start -->";
15
15
  export const INSTRUCTION_BLOCK_END = "<!-- skillmux:discovery:end -->";
@@ -22,7 +22,7 @@ const MANAGED_BLOCK = [
22
22
 
23
23
  export interface InstructionChange {
24
24
  path: string;
25
- clients: ClientId[];
25
+ agents: AgentId[];
26
26
  status: "create" | "update" | "unchanged";
27
27
  before: string | null;
28
28
  after: string;
@@ -30,7 +30,7 @@ export interface InstructionChange {
30
30
 
31
31
  export interface InstructionPlan {
32
32
  changes: InstructionChange[];
33
- manual: Array<{ client: ClientId; reason: string }>;
33
+ manual: Array<{ agent: AgentId; reason: string }>;
34
34
  }
35
35
 
36
36
  interface InstructionPlanOptions {
@@ -41,18 +41,10 @@ interface InstructionPlanOptions {
41
41
  }
42
42
 
43
43
  function instructionPath(
44
- client: ClientId,
44
+ agent: AgentId,
45
45
  options: Required<Pick<InstructionPlanOptions, "home" | "codexHome" | "claudeConfigDir">>,
46
46
  ): string | undefined {
47
- if (client === "claude-code") return join(options.claudeConfigDir, "CLAUDE.md");
48
- if (client === "codex") return join(options.codexHome, "AGENTS.md");
49
- if (client === "gemini-cli" || client === "antigravity") {
50
- return join(options.home, ".gemini", "GEMINI.md");
51
- }
52
- if (client === "opencode") return join(options.home, ".config", "opencode", "AGENTS.md");
53
- if (client === "goose") return join(options.home, ".config", "goose", ".goosehints");
54
- if (client === "hermes") return join(options.home, ".hermes.md");
55
- return undefined;
47
+ return getAgentDefinition(agent).instructions?.global?.(options);
56
48
  }
57
49
 
58
50
  function readInstructionFile(path: string): string | null {
@@ -85,29 +77,23 @@ function withManagedBlock(existing: string | null, path: string): string {
85
77
  return `${existing.slice(0, start)}${MANAGED_BLOCK}${existing.slice(end + INSTRUCTION_BLOCK_END.length)}`;
86
78
  }
87
79
 
88
- export function planInstructionSetup(
89
- requestedClients: readonly ClientId[],
90
- options: InstructionPlanOptions = {},
80
+ function planInstructionSetupWithResolver(
81
+ requestedAgents: readonly AgentId[],
82
+ resolvePath: (agent: AgentId) => string | undefined,
83
+ readFile: (path: string) => string | null,
91
84
  ): InstructionPlan {
92
- const home = options.home ?? process.env.HOME ?? "";
93
- const resolvedOptions = {
94
- home,
95
- codexHome: options.codexHome ?? process.env.CODEX_HOME ?? join(home, ".codex"),
96
- claudeConfigDir: options.claudeConfigDir ?? process.env.CLAUDE_CONFIG_DIR ?? join(home, ".claude"),
97
- };
98
- const readFile = options.readFile ?? readInstructionFile;
99
85
  const changesByPath = new Map<string, InstructionChange>();
100
86
  const manual: InstructionPlan["manual"] = [];
101
87
 
102
- for (const client of [...new Set(requestedClients)]) {
103
- const path = instructionPath(client, resolvedOptions);
88
+ for (const agent of [...new Set(requestedAgents)]) {
89
+ const path = resolvePath(agent);
104
90
  if (!path) {
105
- manual.push({ client, reason: "no safe durable user instruction file is known" });
91
+ manual.push({ agent, reason: "no safe durable user instruction file is known" });
106
92
  continue;
107
93
  }
108
94
  const existingChange = changesByPath.get(path);
109
95
  if (existingChange) {
110
- existingChange.clients.push(client);
96
+ existingChange.agents.push(agent);
111
97
  continue;
112
98
  }
113
99
 
@@ -115,7 +101,7 @@ export function planInstructionSetup(
115
101
  const after = withManagedBlock(before, path);
116
102
  changesByPath.set(path, {
117
103
  path,
118
- clients: [client],
104
+ agents: [agent],
119
105
  status: before === null ? "create" : before === after ? "unchanged" : "update",
120
106
  before,
121
107
  after,
@@ -125,6 +111,39 @@ export function planInstructionSetup(
125
111
  return { changes: [...changesByPath.values()], manual };
126
112
  }
127
113
 
114
+ export function planInstructionSetup(
115
+ requestedAgents: readonly AgentId[],
116
+ options: InstructionPlanOptions = {},
117
+ ): InstructionPlan {
118
+ const home = options.home ?? process.env.HOME ?? "";
119
+ const resolvedOptions = {
120
+ home,
121
+ codexHome: options.codexHome ?? process.env.CODEX_HOME ?? join(home, ".codex"),
122
+ claudeConfigDir: options.claudeConfigDir ?? process.env.CLAUDE_CONFIG_DIR ?? join(home, ".claude"),
123
+ };
124
+ return planInstructionSetupWithResolver(
125
+ requestedAgents,
126
+ (agent) => instructionPath(agent, resolvedOptions),
127
+ options.readFile ?? readInstructionFile,
128
+ );
129
+ }
130
+
131
+ function projectInstructionPath(agent: AgentId, projectRoot: string): string | undefined {
132
+ return getAgentDefinition(agent).instructions?.project?.(projectRoot);
133
+ }
134
+
135
+ export function planProjectInstructionSetup(
136
+ requestedAgents: readonly AgentId[],
137
+ projectRoot: string,
138
+ options: Pick<InstructionPlanOptions, "readFile"> = {},
139
+ ): InstructionPlan {
140
+ return planInstructionSetupWithResolver(
141
+ requestedAgents,
142
+ (agent) => projectInstructionPath(agent, projectRoot),
143
+ options.readFile ?? readInstructionFile,
144
+ );
145
+ }
146
+
128
147
  function atomicWrite(path: string, content: string, mode: number): void {
129
148
  mkdirSync(dirname(path), { recursive: true });
130
149
  const temporaryPath = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;