@klhapp/skillmux 1.12.0 → 1.13.0

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 (103) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +18 -9
  3. package/bin/skillmux.js +89 -0
  4. package/docs/README.md +4 -4
  5. package/docs/cli.md +14 -4
  6. package/docs/concepts.md +2 -2
  7. package/docs/configuration.md +18 -3
  8. package/docs/deployment.md +3 -3
  9. package/docs/getting-started.md +32 -7
  10. package/docs/releasing.md +17 -2
  11. package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
  12. package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
  13. package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
  14. package/docs/sdd/cli-remote-target-parity/think.md +183 -0
  15. package/docs/sdd/cli-surface-consistency/think.md +149 -0
  16. package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
  17. package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
  18. package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
  19. package/docs/sdd/feat-packaging-deployment/review.md +95 -0
  20. package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
  21. package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
  22. package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
  23. package/docs/sdd/npm-native-binaries/review.md +103 -0
  24. package/docs/sdd/npm-native-binaries/spec.md +148 -0
  25. package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
  26. package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
  27. package/docs/sdd/runtime-resource-hardening/review.md +86 -0
  28. package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
  29. package/docs/sdd/security-hardening-phase/think.md +129 -0
  30. package/docs/sdd/security-observability/review.md +96 -0
  31. package/docs/sdd/security-observability/spec.md +55 -0
  32. package/docs/sdd/skill-provenance-update/review.md +152 -0
  33. package/docs/sdd/skill-provenance-update/schema.json +193 -0
  34. package/docs/sdd/skill-provenance-update/spec.md +134 -0
  35. package/docs/sdd/supply-chain-hardening/review.md +50 -0
  36. package/docs/sdd/supply-chain-hardening/spec.md +42 -0
  37. package/docs/sdd/target-marker-rehome/spec.md +33 -0
  38. package/docs/troubleshooting.md +18 -2
  39. package/package.json +15 -8
  40. package/src/adapters.ts +0 -438
  41. package/src/audit.ts +0 -21
  42. package/src/cli.ts +0 -928
  43. package/src/clients.ts +0 -368
  44. package/src/commands/audit.ts +0 -85
  45. package/src/commands/config.ts +0 -224
  46. package/src/commands/context.ts +0 -104
  47. package/src/commands/core.ts +0 -56
  48. package/src/commands/doctor.ts +0 -97
  49. package/src/commands/eval.ts +0 -88
  50. package/src/commands/init.ts +0 -672
  51. package/src/commands/install.ts +0 -155
  52. package/src/commands/local-vault.ts +0 -60
  53. package/src/commands/models.ts +0 -10
  54. package/src/commands/outdated.ts +0 -115
  55. package/src/commands/project.ts +0 -580
  56. package/src/commands/report.ts +0 -66
  57. package/src/commands/scan.ts +0 -71
  58. package/src/commands/shared.ts +0 -38
  59. package/src/commands/skill.ts +0 -33
  60. package/src/commands/sync.ts +0 -233
  61. package/src/commands/target.ts +0 -225
  62. package/src/commands/update.ts +0 -277
  63. package/src/completions.ts +0 -196
  64. package/src/concurrency-limiter.ts +0 -61
  65. package/src/config-service.ts +0 -398
  66. package/src/config-watcher.ts +0 -253
  67. package/src/config.ts +0 -583
  68. package/src/context.ts +0 -188
  69. package/src/db-audit.ts +0 -286
  70. package/src/db-index.ts +0 -238
  71. package/src/db.ts +0 -3
  72. package/src/deployment.ts +0 -39
  73. package/src/doctor.ts +0 -205
  74. package/src/eval.ts +0 -302
  75. package/src/global-flags.ts +0 -46
  76. package/src/init-agents.ts +0 -329
  77. package/src/init-instructions.ts +0 -192
  78. package/src/init.ts +0 -390
  79. package/src/install.ts +0 -232
  80. package/src/lifecycle.ts +0 -51
  81. package/src/logger.ts +0 -26
  82. package/src/manifest.ts +0 -359
  83. package/src/mcp-registration.ts +0 -89
  84. package/src/metrics.ts +0 -121
  85. package/src/models.ts +0 -20
  86. package/src/output.ts +0 -214
  87. package/src/project-setup.ts +0 -36
  88. package/src/prompts.ts +0 -124
  89. package/src/provenance.ts +0 -99
  90. package/src/rate-limiter.ts +0 -137
  91. package/src/readiness.ts +0 -30
  92. package/src/redact.ts +0 -52
  93. package/src/router-core.ts +0 -666
  94. package/src/rrf.ts +0 -31
  95. package/src/scan.ts +0 -300
  96. package/src/server.ts +0 -930
  97. package/src/setup.ts +0 -145
  98. package/src/snapshot.ts +0 -135
  99. package/src/stats.ts +0 -340
  100. package/src/sync.ts +0 -482
  101. package/src/toml-writer.ts +0 -51
  102. package/src/types.ts +0 -212
  103. package/src/vault.ts +0 -207
package/src/eval.ts DELETED
@@ -1,302 +0,0 @@
1
- import type { Database } from "bun:sqlite";
2
- import { readFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import {
5
- backfillEmbeddings,
6
- getRuntime,
7
- retrieveAndRerank,
8
- } from "./router-core";
9
-
10
- export interface EvalCase {
11
- query: string;
12
- split?: string;
13
- relevant_skill_ids: string[];
14
- }
15
-
16
- export function parseEvalCases(raw: unknown): EvalCase[] {
17
- if (!Array.isArray(raw)) throw new Error("Eval cases file must contain a JSON array");
18
- const result: EvalCase[] = [];
19
- for (let i = 0; i < raw.length; i++) {
20
- const item = raw[i];
21
- if (typeof item !== "object" || item === null) {
22
- throw new Error(`Eval case at index ${i} must be an object`);
23
- }
24
- if ("expected" in item) {
25
- throw new Error(
26
- `Legacy 'expected' field at case ${i} is no longer supported in eval datasets; use 'relevant_skill_ids' instead.`,
27
- );
28
- }
29
- if ("expected_outcome" in item) {
30
- throw new Error(
31
- `Legacy 'expected_outcome' field at case ${i} is no longer supported in eval datasets; remove it and provide 'relevant_skill_ids' instead.`,
32
- );
33
- }
34
- if (typeof (item as any).query !== "string" || (item as any).query.trim().length === 0) {
35
- throw new Error(`Eval case at index ${i} has invalid 'query': must be a non-empty string`);
36
- }
37
- if (
38
- !Array.isArray((item as any).relevant_skill_ids) ||
39
- (item as any).relevant_skill_ids.some((id: unknown) => typeof id !== "string")
40
- ) {
41
- throw new Error(`Eval case at index ${i} has invalid 'relevant_skill_ids': must be an array of strings`);
42
- }
43
- const relevantSkillIds = (item as any).relevant_skill_ids as string[];
44
- if (relevantSkillIds.some((id) => id.trim().length === 0)) {
45
- throw new Error(`Eval case at index ${i} has invalid 'relevant_skill_ids': IDs must be non-empty strings`);
46
- }
47
- if (new Set(relevantSkillIds).size !== relevantSkillIds.length) {
48
- throw new Error(`Eval case at index ${i} has invalid 'relevant_skill_ids': duplicate IDs are not allowed`);
49
- }
50
- if ("split" in item && (item as any).split !== undefined && typeof (item as any).split !== "string") {
51
- throw new Error(`Eval case at index ${i} has invalid 'split': must be a string if present`);
52
- }
53
- result.push({
54
- query: (item as any).query,
55
- ...((item as any).split !== undefined ? { split: (item as any).split } : {}),
56
- relevant_skill_ids: relevantSkillIds,
57
- });
58
- }
59
- return result;
60
- }
61
-
62
- /** AC17: dedup key for promoted cases. Collapses whitespace and case differences that are not meaningfully distinct queries. */
63
- export function normalizeQuery(query: string): string {
64
- return query.trim().replace(/\s+/g, " ").toLowerCase();
65
- }
66
-
67
- export interface ObservedFetch {
68
- query: string;
69
- skill_id: string;
70
- }
71
-
72
- /** AC17: groups fetched skill ids by normalized query into observed-split eval cases. */
73
- export function buildPromotedCases(fetches: ObservedFetch[]): EvalCase[] {
74
- const byQuery = new Map<string, { query: string; skillIds: string[]; seen: Set<string> }>();
75
- for (const fetch of fetches) {
76
- const key = normalizeQuery(fetch.query);
77
- if (!key) continue;
78
- let entry = byQuery.get(key);
79
- if (!entry) {
80
- entry = { query: fetch.query, skillIds: [], seen: new Set() };
81
- byQuery.set(key, entry);
82
- }
83
- if (!entry.seen.has(fetch.skill_id)) {
84
- entry.seen.add(fetch.skill_id);
85
- entry.skillIds.push(fetch.skill_id);
86
- }
87
- }
88
- return [...byQuery.values()].map((entry) => ({
89
- query: entry.query,
90
- split: "observed",
91
- relevant_skill_ids: entry.skillIds,
92
- }));
93
- }
94
-
95
- /** AC18: never rewrites a case whose normalized query already exists in the target file. */
96
- export function excludeExistingCases(
97
- cases: EvalCase[],
98
- existing: EvalCase[],
99
- ): { cases: EvalCase[]; skipped: number } {
100
- const existingKeys = new Set(existing.map((c) => normalizeQuery(c.query)));
101
- const kept: EvalCase[] = [];
102
- let skipped = 0;
103
- for (const c of cases) {
104
- if (existingKeys.has(normalizeQuery(c.query))) {
105
- skipped++;
106
- } else {
107
- kept.push(c);
108
- }
109
- }
110
- return { cases: kept, skipped };
111
- }
112
-
113
- /**
114
- * AC17: joins fetches to the resolve that produced them for promotion. Only
115
- * correlated fetches (a known resolve_audit_id) carry a query to promote;
116
- * uncorrelated fetches have no resolve to join against and are excluded.
117
- */
118
- export function queryPromotableFetches(db: Database, sinceIso: string): ObservedFetch[] {
119
- return db
120
- .query(
121
- `SELECT audit.query AS query, fetch.skill_id AS skill_id
122
- FROM fetch
123
- JOIN audit ON fetch.resolve_audit_id = audit.id
124
- WHERE fetch.ts >= ?
125
- ORDER BY fetch.ts ASC`,
126
- )
127
- .all(sinceIso) as ObservedFetch[];
128
- }
129
-
130
- export interface EvalMetrics {
131
- recall_at_5: number;
132
- recall_at_10: number;
133
- mrr: number;
134
- ndcg_at_10: number;
135
- }
136
-
137
- export interface CandidateEvalDetail {
138
- skill_id: string;
139
- lexical_rank: number | null;
140
- fused_rank: number | null;
141
- reranked_rank?: number | null;
142
- }
143
-
144
- export interface EvalCaseResult {
145
- query: string;
146
- relevant_skill_ids: string[];
147
- retrieval: string;
148
- degraded_from?: string | null;
149
- degradation_reason?: string | null;
150
- latency_ms: number;
151
- recall_settings: {
152
- k_lexical: number;
153
- k_vector: number;
154
- k_rerank: number;
155
- };
156
- candidates: CandidateEvalDetail[];
157
- }
158
-
159
- export interface EvalReport {
160
- queries: number;
161
- judged_queries: number;
162
- unjudged_queries: number;
163
- lexical: EvalMetrics;
164
- hybrid: EvalMetrics;
165
- cases?: EvalCaseResult[];
166
- }
167
-
168
- export function computeRankingMetrics(rankings: string[][], cases: EvalCase[]): EvalMetrics {
169
- const judgedIndices: number[] = [];
170
- for (let i = 0; i < cases.length; i++) {
171
- if (cases[i]!.relevant_skill_ids && cases[i]!.relevant_skill_ids.length > 0) {
172
- judgedIndices.push(i);
173
- }
174
- }
175
-
176
- if (judgedIndices.length === 0) {
177
- return {
178
- recall_at_5: 0,
179
- recall_at_10: 0,
180
- mrr: 0,
181
- ndcg_at_10: 0,
182
- };
183
- }
184
-
185
- let totalRecall5 = 0;
186
- let totalRecall10 = 0;
187
- let totalMRR = 0;
188
- let totalNDCG10 = 0;
189
-
190
- for (const idx of judgedIndices) {
191
- const c = cases[idx]!;
192
- const ranking = rankings[idx] ?? [];
193
- const relevantSet = new Set(c.relevant_skill_ids);
194
- const numRelevant = c.relevant_skill_ids.length;
195
-
196
- // Recall@5: fraction of relevant skill IDs present in the first 5 results
197
- const top5 = ranking.slice(0, 5);
198
- const count5 = top5.filter((id) => relevantSet.has(id)).length;
199
- totalRecall5 += count5 / numRelevant;
200
-
201
- // Recall@10: fraction of relevant skill IDs present in the first 10 results
202
- const top10 = ranking.slice(0, 10);
203
- const count10 = top10.filter((id) => relevantSet.has(id)).length;
204
- totalRecall10 += count10 / numRelevant;
205
-
206
- // MRR: reciprocal rank of the first relevant result (1-based), 0 if missing
207
- const firstRelevantRank = ranking.findIndex((id) => relevantSet.has(id));
208
- if (firstRelevantRank >= 0) {
209
- totalMRR += 1 / (firstRelevantRank + 1);
210
- }
211
-
212
- // nDCG@10: binary relevance, DCG gain 1/log2(rank+1), IDCG over min(numRelevant, 10)
213
- let dcg10 = 0;
214
- for (let r = 0; r < Math.min(10, ranking.length); r++) {
215
- if (relevantSet.has(ranking[r]!)) {
216
- dcg10 += 1 / Math.log2(r + 2);
217
- }
218
- }
219
- let idcg10 = 0;
220
- const idealCount = Math.min(numRelevant, 10);
221
- for (let r = 0; r < idealCount; r++) {
222
- idcg10 += 1 / Math.log2(r + 2);
223
- }
224
- if (idcg10 > 0) {
225
- totalNDCG10 += dcg10 / idcg10;
226
- }
227
- }
228
-
229
- const judgedCount = judgedIndices.length;
230
- return {
231
- recall_at_5: totalRecall5 / judgedCount,
232
- recall_at_10: totalRecall10 / judgedCount,
233
- mrr: totalMRR / judgedCount,
234
- ndcg_at_10: totalNDCG10 / judgedCount,
235
- };
236
- }
237
-
238
- export function loadEvalCases(path = join(import.meta.dir, "..", "eval", "queries.json")): EvalCase[] {
239
- const raw = JSON.parse(readFileSync(path, "utf8"));
240
- return parseEvalCases(raw);
241
- }
242
-
243
- export async function evalVault(cases = loadEvalCases()): Promise<EvalReport> {
244
- const { config } = await getRuntime();
245
- await backfillEmbeddings();
246
-
247
- const lexicalRankings: string[][] = [];
248
- const hybridRankings: string[][] = [];
249
- const caseResults: EvalCaseResult[] = [];
250
- const kRerank = config.recall.k_rerank ?? Math.min(10, config.recall.k_lexical + config.recall.k_vector);
251
-
252
- for (const evalCase of cases) {
253
- const start = performance.now();
254
- const retrievalResult = await retrieveAndRerank({ query: evalCase.query });
255
-
256
- // lexical metrics use lexical rank
257
- lexicalRankings.push(
258
- retrievalResult.trace
259
- .filter((candidate) => candidate.lexical_rank !== null)
260
- .sort((a, b) => a.lexical_rank! - b.lexical_rank!)
261
- .map((candidate) => candidate.skill_id),
262
- );
263
-
264
- // retrieveAndRerank returns candidates in the exact order delivered to the
265
- // caller: reranked when available, otherwise fused or lexical fallback.
266
- hybridRankings.push(
267
- retrievalResult.candidates.map((candidate) => candidate.skill_id),
268
- );
269
-
270
- const latency_ms = Math.round(performance.now() - start);
271
- const candidateDetails: CandidateEvalDetail[] = retrievalResult.trace;
272
-
273
- caseResults.push({
274
- query: evalCase.query,
275
- relevant_skill_ids: evalCase.relevant_skill_ids,
276
- retrieval: retrievalResult.retrieval,
277
- degraded_from: retrievalResult.degraded_from ?? null,
278
- degradation_reason: retrievalResult.degradation_reason ?? null,
279
- latency_ms,
280
- recall_settings: {
281
- k_lexical: config.recall.k_lexical,
282
- k_vector: config.recall.k_vector,
283
- k_rerank: kRerank,
284
- },
285
- candidates: candidateDetails,
286
- });
287
- }
288
-
289
- const judged_queries = cases.filter(
290
- (c) => c.relevant_skill_ids && c.relevant_skill_ids.length > 0,
291
- ).length;
292
- const unjudged_queries = cases.length - judged_queries;
293
-
294
- return {
295
- queries: cases.length,
296
- judged_queries,
297
- unjudged_queries,
298
- lexical: computeRankingMetrics(lexicalRankings, cases),
299
- hybrid: computeRankingMetrics(hybridRankings, cases),
300
- cases: caseResults,
301
- };
302
- }
@@ -1,46 +0,0 @@
1
- export type GlobalFlag =
2
- | "--json"
3
- | "--allow-insecure"
4
- | "--verbose"
5
- | "--dry-run";
6
-
7
- export type GlobalFlagWithValue = "--context" | "--server";
8
-
9
- /**
10
- * Checks if an argument is a global flag that takes a value (e.g. `--context`, `--server`).
11
- * If an `allowed` list is provided, only flags in that subset are matched.
12
- */
13
- export function isGlobalFlagWithValue(
14
- option: string | undefined,
15
- allowed?: readonly GlobalFlagWithValue[] | GlobalFlagWithValue,
16
- ...rest: GlobalFlagWithValue[]
17
- ): boolean {
18
- if (!option) return false;
19
- if (allowed !== undefined) {
20
- const list = Array.isArray(allowed) ? allowed : [allowed, ...rest];
21
- return list.includes(option as GlobalFlagWithValue);
22
- }
23
- return option === "--context" || option === "--server";
24
- }
25
-
26
- /**
27
- * Checks if an argument is a global flag that takes no value (e.g. `--json`, `--allow-insecure`, `--verbose`, `--dry-run`).
28
- * If an `allowed` list is provided, only flags in that subset are matched.
29
- */
30
- export function isGlobalFlag(
31
- option: string | undefined,
32
- allowed?: readonly GlobalFlag[] | GlobalFlag,
33
- ...rest: GlobalFlag[]
34
- ): boolean {
35
- if (!option) return false;
36
- if (allowed !== undefined) {
37
- const list = Array.isArray(allowed) ? allowed : [allowed, ...rest];
38
- return list.includes(option as GlobalFlag);
39
- }
40
- return (
41
- option === "--json" ||
42
- option === "--allow-insecure" ||
43
- option === "--verbose" ||
44
- option === "--dry-run"
45
- );
46
- }
@@ -1,329 +0,0 @@
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
- }