@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
@@ -1,192 +0,0 @@
1
- import {
2
- lstatSync,
3
- mkdirSync,
4
- readFileSync,
5
- renameSync,
6
- rmSync,
7
- statSync,
8
- writeFileSync,
9
- } from "node:fs";
10
- import { dirname, join } from "node:path";
11
- import { DISCOVERY_PARAGRAPH } from "./init";
12
- import { getAgentDefinition, type AgentId } from "./init-agents";
13
-
14
- export const INSTRUCTION_BLOCK_START = "<!-- skillmux:discovery:start -->";
15
- export const INSTRUCTION_BLOCK_END = "<!-- skillmux:discovery:end -->";
16
-
17
- const MANAGED_BLOCK = [
18
- INSTRUCTION_BLOCK_START,
19
- DISCOVERY_PARAGRAPH,
20
- INSTRUCTION_BLOCK_END,
21
- ].join("\n");
22
-
23
- export interface InstructionChange {
24
- path: string;
25
- agents: AgentId[];
26
- status: "create" | "update" | "unchanged";
27
- before: string | null;
28
- after: string;
29
- }
30
-
31
- export interface InstructionPlan {
32
- changes: InstructionChange[];
33
- manual: Array<{ agent: AgentId; reason: string }>;
34
- }
35
-
36
- interface InstructionPlanOptions {
37
- home?: string;
38
- codexHome?: string;
39
- claudeConfigDir?: string;
40
- readFile?: (path: string) => string | null;
41
- }
42
-
43
- function instructionPath(
44
- agent: AgentId,
45
- options: Required<Pick<InstructionPlanOptions, "home" | "codexHome" | "claudeConfigDir">>,
46
- ): string | undefined {
47
- return getAgentDefinition(agent).instructions?.global?.(options);
48
- }
49
-
50
- function readInstructionFile(path: string): string | null {
51
- try {
52
- const stat = lstatSync(path);
53
- if (stat.isSymbolicLink()) {
54
- throw new Error(`instruction file is a symbolic link and will not be modified: ${path}`);
55
- }
56
- if (!stat.isFile()) throw new Error(`instruction path is not a file: ${path}`);
57
- return readFileSync(path, "utf8");
58
- } catch (error) {
59
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
60
- throw error;
61
- }
62
- }
63
-
64
- function withManagedBlock(existing: string | null, path: string): string {
65
- if (existing === null || existing.length === 0) return `${MANAGED_BLOCK}\n`;
66
-
67
- const starts = existing.split(INSTRUCTION_BLOCK_START).length - 1;
68
- const ends = existing.split(INSTRUCTION_BLOCK_END).length - 1;
69
- if (starts === 0 && ends === 0) return `${existing.trimEnd()}\n\n${MANAGED_BLOCK}\n`;
70
- if (starts !== 1 || ends !== 1) {
71
- throw new Error(`instruction file has a malformed skillmux managed block: ${path}`);
72
- }
73
-
74
- const start = existing.indexOf(INSTRUCTION_BLOCK_START);
75
- const end = existing.indexOf(INSTRUCTION_BLOCK_END, start);
76
- if (end < start) throw new Error(`instruction file has a malformed skillmux managed block: ${path}`);
77
- return `${existing.slice(0, start)}${MANAGED_BLOCK}${existing.slice(end + INSTRUCTION_BLOCK_END.length)}`;
78
- }
79
-
80
- function planInstructionSetupWithResolver(
81
- requestedAgents: readonly AgentId[],
82
- resolvePath: (agent: AgentId) => string | undefined,
83
- readFile: (path: string) => string | null,
84
- ): InstructionPlan {
85
- const changesByPath = new Map<string, InstructionChange>();
86
- const manual: InstructionPlan["manual"] = [];
87
-
88
- for (const agent of [...new Set(requestedAgents)]) {
89
- const path = resolvePath(agent);
90
- if (!path) {
91
- manual.push({ agent, reason: "no safe durable user instruction file is known" });
92
- continue;
93
- }
94
- const existingChange = changesByPath.get(path);
95
- if (existingChange) {
96
- existingChange.agents.push(agent);
97
- continue;
98
- }
99
-
100
- const before = readFile(path);
101
- const after = withManagedBlock(before, path);
102
- changesByPath.set(path, {
103
- path,
104
- agents: [agent],
105
- status: before === null ? "create" : before === after ? "unchanged" : "update",
106
- before,
107
- after,
108
- });
109
- }
110
-
111
- return { changes: [...changesByPath.values()], manual };
112
- }
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
-
147
- function atomicWrite(path: string, content: string, mode: number): void {
148
- mkdirSync(dirname(path), { recursive: true });
149
- const temporaryPath = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
150
- try {
151
- writeFileSync(temporaryPath, content, { encoding: "utf8", mode });
152
- renameSync(temporaryPath, path);
153
- } finally {
154
- rmSync(temporaryPath, { force: true });
155
- }
156
- }
157
-
158
- export function applyInstructionPlan(plan: InstructionPlan): void {
159
- for (const change of plan.changes) {
160
- if (change.status === "unchanged") continue;
161
- if (readInstructionFile(change.path) !== change.before) {
162
- throw new Error(`instruction file changed after planning: ${change.path}`);
163
- }
164
- }
165
-
166
- const applied: InstructionChange[] = [];
167
- try {
168
- for (const change of plan.changes) {
169
- if (change.status === "unchanged") continue;
170
- const mode = change.before === null ? 0o600 : statSync(change.path).mode;
171
- atomicWrite(change.path, change.after, mode);
172
- applied.push(change);
173
- }
174
- } catch (error) {
175
- rollbackInstructionChanges(applied);
176
- throw error;
177
- }
178
- }
179
-
180
- function rollbackInstructionChanges(changes: InstructionChange[]): void {
181
- for (const change of [...changes].reverse()) {
182
- if (change.before === null) {
183
- rmSync(change.path, { force: true });
184
- } else {
185
- atomicWrite(change.path, change.before, statSync(change.path).mode);
186
- }
187
- }
188
- }
189
-
190
- export function rollbackInstructionPlan(plan: InstructionPlan): void {
191
- rollbackInstructionChanges(plan.changes.filter((change) => change.status !== "unchanged"));
192
- }
package/src/init.ts DELETED
@@ -1,390 +0,0 @@
1
- import {
2
- existsSync,
3
- lstatSync,
4
- mkdirSync,
5
- readFileSync,
6
- readlinkSync,
7
- readdirSync,
8
- realpathSync,
9
- renameSync,
10
- rmdirSync,
11
- symlinkSync,
12
- unlinkSync,
13
- writeFileSync,
14
- } from "node:fs";
15
- import { hostname } from "node:os";
16
- import { basename, dirname, join } from "node:path";
17
- import {
18
- parseManifest,
19
- resolveManifestPath,
20
- serializeManifest,
21
- type Manifest,
22
- CORE_SKILL_LIMIT,
23
- MANIFEST_FILENAME,
24
- } from "./manifest";
25
- import { BUILT_IN_TARGET_NAMES } from "./init-agents";
26
- import {
27
- adoptTarget,
28
- preflightAdoptTarget,
29
- readSkillmuxMarker,
30
- SKILLMUX_MARKER_FILENAME,
31
- } from "./sync";
32
- import { SKILL_ID_PATTERN } from "./vault";
33
-
34
- export const DEFAULT_SURFACE_CANDIDATES = ["~/.claude/skills", "~/.agents/skills"];
35
-
36
- /**
37
- * Test/ops escape hatch: comma-separated absolute paths overriding
38
- * DEFAULT_SURFACE_CANDIDATES. Not part of config.toml — "others as
39
- * configured" (spec.md) is deliberately left as an implementation-time
40
- * choice, same as the proposal heuristic. Exists primarily so tests never
41
- * probe the real $HOME's ~/.claude/skills or ~/.agents/skills.
42
- */
43
- export function surfaceCandidates(): string[] {
44
- const override = process.env.SKILLMUX_INIT_SURFACES;
45
- return override ? override.split(",").filter((p) => p.length > 0) : DEFAULT_SURFACE_CANDIDATES;
46
- }
47
-
48
- export interface SurfaceCandidate {
49
- path: string;
50
- canonicalPath?: string;
51
- exists: boolean;
52
- isSymlink: boolean;
53
- skillCount: number;
54
- alreadyMarked: boolean;
55
- state: "missing" | "directory" | "broken-symlink" | "external-symlink" | "full-vault" | "unsupported";
56
- deliveryMode: "managed-pins" | "full-vault" | "external";
57
- }
58
-
59
- function countSkillDirs(dir: string): number {
60
- let count = 0;
61
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
62
- if (entry.isDirectory() && SKILL_ID_PATTERN.test(entry.name) && existsSync(join(dir, entry.name, "SKILL.md"))) {
63
- count++;
64
- }
65
- }
66
- return count;
67
- }
68
-
69
- export function detectSurfaces(candidatePaths: string[], vaultPath?: string): SurfaceCandidate[] {
70
- const canonicalVaultPath = vaultPath ? realpathSync(vaultPath) : undefined;
71
-
72
- return candidatePaths.map((path): SurfaceCandidate => {
73
- let stat;
74
- try {
75
- stat = lstatSync(path);
76
- } catch (error) {
77
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
78
- return {
79
- path,
80
- exists: false,
81
- isSymlink: false,
82
- skillCount: 0,
83
- alreadyMarked: false,
84
- state: "missing",
85
- deliveryMode: "managed-pins",
86
- };
87
- }
88
-
89
- if (stat.isSymbolicLink()) {
90
- let canonicalPath: string;
91
- try {
92
- canonicalPath = realpathSync(path);
93
- } catch (error) {
94
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
95
- return {
96
- path,
97
- exists: false,
98
- isSymlink: true,
99
- skillCount: 0,
100
- alreadyMarked: false,
101
- state: "broken-symlink",
102
- deliveryMode: "external",
103
- };
104
- }
105
-
106
- const isFullVault = canonicalVaultPath !== undefined && canonicalPath === canonicalVaultPath;
107
- return {
108
- path,
109
- canonicalPath,
110
- exists: true,
111
- isSymlink: true,
112
- skillCount: 0,
113
- alreadyMarked: false,
114
- state: isFullVault ? "full-vault" : "external-symlink",
115
- deliveryMode: isFullVault ? "full-vault" : "external",
116
- };
117
- }
118
-
119
- const canonicalPath = realpathSync(path);
120
- const isFullVault = canonicalVaultPath !== undefined && canonicalPath === canonicalVaultPath;
121
- if (isFullVault) {
122
- return {
123
- path,
124
- canonicalPath,
125
- exists: true,
126
- isSymlink: false,
127
- skillCount: 0,
128
- alreadyMarked: false,
129
- state: "full-vault",
130
- deliveryMode: "full-vault",
131
- };
132
- }
133
-
134
- if (!stat.isDirectory()) {
135
- return {
136
- path,
137
- canonicalPath,
138
- exists: true,
139
- isSymlink: false,
140
- skillCount: 0,
141
- alreadyMarked: false,
142
- state: "unsupported",
143
- deliveryMode: "external",
144
- };
145
- }
146
-
147
- return {
148
- path,
149
- canonicalPath,
150
- exists: true,
151
- isSymlink: false,
152
- skillCount: countSkillDirs(path),
153
- alreadyMarked: readSkillmuxMarker(path) !== null,
154
- state: "directory",
155
- deliveryMode: "managed-pins",
156
- };
157
- });
158
- }
159
-
160
- /**
161
- * Conservative default: no slash-command/workflow-router detection heuristic
162
- * at TDD time (spec.md, "skr init" AC) — evidence-only, nothing proposed
163
- * until a concrete heuristic is agreed.
164
- */
165
- export function proposeManifest(_candidates: SurfaceCandidate[]): Pick<Manifest, "core" | "project"> {
166
- return { core: { skills: [] }, project: {} };
167
- }
168
-
169
- /** e.g. ~/.claude/skills -> "claude"; ~/.agents/skills -> "agents". */
170
- export function deriveTargetName(path: string): string {
171
- return basename(dirname(path)).replace(/^\./, "").toLowerCase();
172
- }
173
-
174
- export interface ConfirmedTarget {
175
- name: string;
176
- dir: string;
177
- migrateFullVault?: boolean;
178
- }
179
-
180
- export interface InitTransactionParticipant {
181
- apply: () => void;
182
- rollback: () => void;
183
- }
184
-
185
- function preflightManagedTargets(vaultPath: string, targets: ConfirmedTarget[]): void {
186
- const canonicalVaultPath = realpathSync(vaultPath);
187
-
188
- for (const target of targets) {
189
- let stat;
190
- try {
191
- stat = lstatSync(target.dir);
192
- } catch (error) {
193
- if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
194
- throw error;
195
- }
196
-
197
- if (stat.isSymbolicLink()) {
198
- const canonicalTargetPath = realpathSync(target.dir);
199
- if (target.migrateFullVault && canonicalTargetPath === canonicalVaultPath) {
200
- continue;
201
- }
202
- throw new Error(
203
- `target "${target.name}" (${target.dir}) is a symbolic link; classify or migrate it before managed-pins adoption`,
204
- );
205
- }
206
- if (!stat.isDirectory()) {
207
- throw new Error(`target "${target.name}" (${target.dir}) is not a directory`);
208
- }
209
- if (realpathSync(target.dir) === canonicalVaultPath) {
210
- throw new Error(
211
- `target "${target.name}" (${target.dir}) is the full-vault surface; it cannot be adopted as managed-pins`,
212
- );
213
- }
214
- preflightAdoptTarget(target.dir, target.name, vaultPath);
215
- }
216
- }
217
-
218
- /**
219
- * Writes skillmux.toml with the conservative-default core/project and the
220
- * confirmed targets, then adopts each confirmed dir in place (creating it
221
- * first if it doesn't exist yet). Unconfirmed candidates are simply never
222
- * passed in — this function never discovers paths on its own.
223
- */
224
- export function planInitManifest(
225
- vaultPath: string,
226
- confirmedTargets: ConfirmedTarget[],
227
- coreSkillIds: string[] = [],
228
- ): Manifest {
229
- preflightManagedTargets(vaultPath, confirmedTargets);
230
-
231
- const existingManifestPath = resolveManifestPath(vaultPath);
232
- const existingManifest = existingManifestPath
233
- ? parseManifest(readFileSync(existingManifestPath, "utf-8"))
234
- : { ...proposeManifest([]), targets: {} };
235
- const manifest: Manifest = {
236
- ...existingManifest,
237
- core: {
238
- ...existingManifest.core,
239
- skills: [...new Set([...existingManifest.core.skills, ...coreSkillIds])],
240
- },
241
- targets: {
242
- ...existingManifest.targets,
243
- ...Object.fromEntries(
244
- confirmedTargets.map((target) => {
245
- const existingTarget = existingManifest.targets[target.name];
246
- return [
247
- target.name,
248
- BUILT_IN_TARGET_NAMES.has(target.name)
249
- ? existingTarget
250
- ? { ...existingTarget, dir: undefined }
251
- : { host: hostname(), project_groups: [] }
252
- : existingTarget
253
- ? { ...existingTarget, dir: target.dir }
254
- : { dir: target.dir, host: hostname(), project_groups: [] },
255
- ];
256
- }),
257
- ),
258
- },
259
- };
260
- const effectiveLimit = manifest.core.limit ?? CORE_SKILL_LIMIT;
261
- if (manifest.core.skills.length > effectiveLimit) {
262
- throw new Error(
263
- `[core] has ${manifest.core.skills.length} skills, exceeding the limit of ${effectiveLimit}`,
264
- );
265
- }
266
- for (const skillId of coreSkillIds) {
267
- if (!SKILL_ID_PATTERN.test(skillId) || !existsSync(join(vaultPath, skillId, "SKILL.md"))) {
268
- throw new Error(`[core] skill "${skillId}" does not exist in the vault`);
269
- }
270
- for (const [groupName, group] of Object.entries(existingManifest.project ?? {})) {
271
- if (group.skills.includes(skillId)) {
272
- throw new Error(`skill "${skillId}" appears in both [core] and [project.${groupName}]`);
273
- }
274
- }
275
- }
276
- return manifest;
277
- }
278
-
279
- export function applyInit(
280
- vaultPath: string,
281
- confirmedTargets: ConfirmedTarget[],
282
- participant?: InitTransactionParticipant,
283
- coreSkillIds: string[] = [],
284
- ): Manifest {
285
- const manifest = planInitManifest(vaultPath, confirmedTargets, coreSkillIds);
286
-
287
- const manifestPath = join(vaultPath, MANIFEST_FILENAME);
288
- const serializedManifest = serializeManifest(manifest);
289
- const shouldWriteManifest =
290
- !existsSync(manifestPath) || readFileSync(manifestPath, "utf-8") !== serializedManifest;
291
- const createdDirs: string[] = [];
292
- const adoptedDirs: string[] = [];
293
- const migratedFullVaultDirs: Array<{ dir: string; linkTarget: string }> = [];
294
- let participantApplied = false;
295
-
296
- try {
297
- for (const target of confirmedTargets) {
298
- let targetStat;
299
- try {
300
- targetStat = lstatSync(target.dir);
301
- } catch (error) {
302
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
303
- }
304
- if (targetStat?.isSymbolicLink()) {
305
- if (!target.migrateFullVault || realpathSync(target.dir) !== realpathSync(vaultPath)) {
306
- throw new Error(
307
- `target "${target.name}" (${target.dir}) changed to an unsafe symbolic link after preflight`,
308
- );
309
- }
310
- const linkTarget = readlinkSync(target.dir);
311
- unlinkSync(target.dir);
312
- mkdirSync(target.dir, { recursive: true });
313
- migratedFullVaultDirs.push({ dir: target.dir, linkTarget });
314
- } else if (targetStat && !targetStat.isDirectory()) {
315
- throw new Error(`target "${target.name}" (${target.dir}) changed to a non-directory after preflight`);
316
- }
317
- if (!existsSync(target.dir)) {
318
- mkdirSync(target.dir, { recursive: true });
319
- createdDirs.push(target.dir);
320
- }
321
- if (adoptTarget(target.dir, target.name, vaultPath).adopted) {
322
- adoptedDirs.push(target.dir);
323
- }
324
- }
325
-
326
- if (participant) {
327
- participant.apply();
328
- participantApplied = true;
329
- }
330
-
331
- if (shouldWriteManifest) {
332
- const temporaryManifestPath = join(
333
- vaultPath,
334
- `.${MANIFEST_FILENAME}.${process.pid}-${Date.now()}.tmp`,
335
- );
336
- try {
337
- writeFileSync(temporaryManifestPath, serializedManifest);
338
- renameSync(temporaryManifestPath, manifestPath);
339
- } catch (error) {
340
- if (existsSync(temporaryManifestPath)) unlinkSync(temporaryManifestPath);
341
- throw error;
342
- }
343
- }
344
- } catch (error) {
345
- try {
346
- if (participantApplied) participant?.rollback();
347
- } finally {
348
- for (const dir of adoptedDirs.reverse()) {
349
- const markerPath = join(dir, SKILLMUX_MARKER_FILENAME);
350
- if (existsSync(markerPath)) unlinkSync(markerPath);
351
- }
352
- for (const migration of migratedFullVaultDirs.reverse()) {
353
- if (existsSync(migration.dir)) rmdirSync(migration.dir);
354
- symlinkSync(migration.linkTarget, migration.dir);
355
- }
356
- for (const dir of createdDirs.reverse()) {
357
- if (existsSync(dir)) rmdirSync(dir);
358
- }
359
- }
360
- throw error;
361
- }
362
-
363
- return manifest;
364
- }
365
-
366
- export const DISCOVERY_PARAGRAPH =
367
- "Skills: only a curated core is loaded statically. Before improvising a " +
368
- "multi-step workflow, or when a task smells like a domain you have no loaded " +
369
- "skill for (career/resume, trading, SEO, i18n, design, one-off tooling), " +
370
- "call `resolve_skill` with a one-line task description. `resolve_skill` " +
371
- "returns a ranked shortlist; inspect candidates, fetch one or more " +
372
- "relevant skills with `fetch_skill`, or ignore all and proceed normally " +
373
- "when none are useful.";
374
-
375
- export const MCP_REGISTRATION_SNIPPET = JSON.stringify(
376
- { mcpServers: { "skillmux": { command: "skillmux", args: ["serve"] } } },
377
- null,
378
- 2,
379
- );
380
-
381
- /** §3.4 step 4: "print the last mile" — MCP registration command + discovery paragraph. */
382
- export function printLastMile(): string {
383
- return [
384
- "Register with your MCP client:",
385
- MCP_REGISTRATION_SNIPPET,
386
- "",
387
- "Add this paragraph to your shared agent instructions (~80 tokens, the entire T3 discovery mechanism):",
388
- DISCOVERY_PARAGRAPH,
389
- ].join("\n");
390
- }