@klhapp/skillmux 1.11.2 → 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 (104) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +20 -10
  3. package/bin/skillmux.js +89 -0
  4. package/docs/README.md +4 -4
  5. package/docs/cli.md +15 -5
  6. package/docs/concepts.md +4 -3
  7. package/docs/configuration.md +22 -5
  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/skill-management.md +1 -1
  39. package/docs/troubleshooting.md +18 -2
  40. package/package.json +15 -8
  41. package/src/adapters.ts +0 -438
  42. package/src/audit.ts +0 -21
  43. package/src/cli.ts +0 -928
  44. package/src/clients.ts +0 -368
  45. package/src/commands/audit.ts +0 -85
  46. package/src/commands/config.ts +0 -224
  47. package/src/commands/context.ts +0 -104
  48. package/src/commands/core.ts +0 -56
  49. package/src/commands/doctor.ts +0 -97
  50. package/src/commands/eval.ts +0 -88
  51. package/src/commands/init.ts +0 -672
  52. package/src/commands/install.ts +0 -155
  53. package/src/commands/local-vault.ts +0 -60
  54. package/src/commands/models.ts +0 -10
  55. package/src/commands/outdated.ts +0 -115
  56. package/src/commands/project.ts +0 -580
  57. package/src/commands/report.ts +0 -66
  58. package/src/commands/scan.ts +0 -71
  59. package/src/commands/shared.ts +0 -38
  60. package/src/commands/skill.ts +0 -33
  61. package/src/commands/sync.ts +0 -233
  62. package/src/commands/target.ts +0 -225
  63. package/src/commands/update.ts +0 -277
  64. package/src/completions.ts +0 -196
  65. package/src/concurrency-limiter.ts +0 -61
  66. package/src/config-service.ts +0 -398
  67. package/src/config-watcher.ts +0 -253
  68. package/src/config.ts +0 -583
  69. package/src/context.ts +0 -188
  70. package/src/db-audit.ts +0 -286
  71. package/src/db-index.ts +0 -238
  72. package/src/db.ts +0 -3
  73. package/src/deployment.ts +0 -39
  74. package/src/doctor.ts +0 -205
  75. package/src/eval.ts +0 -302
  76. package/src/global-flags.ts +0 -46
  77. package/src/init-agents.ts +0 -329
  78. package/src/init-instructions.ts +0 -192
  79. package/src/init.ts +0 -389
  80. package/src/install.ts +0 -232
  81. package/src/lifecycle.ts +0 -51
  82. package/src/logger.ts +0 -26
  83. package/src/manifest.ts +0 -352
  84. package/src/mcp-registration.ts +0 -89
  85. package/src/metrics.ts +0 -121
  86. package/src/models.ts +0 -20
  87. package/src/output.ts +0 -214
  88. package/src/project-setup.ts +0 -36
  89. package/src/prompts.ts +0 -124
  90. package/src/provenance.ts +0 -99
  91. package/src/rate-limiter.ts +0 -137
  92. package/src/readiness.ts +0 -30
  93. package/src/redact.ts +0 -52
  94. package/src/router-core.ts +0 -666
  95. package/src/rrf.ts +0 -31
  96. package/src/scan.ts +0 -300
  97. package/src/server.ts +0 -930
  98. package/src/setup.ts +0 -145
  99. package/src/snapshot.ts +0 -135
  100. package/src/stats.ts +0 -340
  101. package/src/sync.ts +0 -482
  102. package/src/toml-writer.ts +0 -51
  103. package/src/types.ts +0 -212
  104. 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,389 +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
- if (manifest.core.skills.length > CORE_SKILL_LIMIT) {
261
- throw new Error(
262
- `[core] has ${manifest.core.skills.length} skills, exceeding the limit of ${CORE_SKILL_LIMIT}`,
263
- );
264
- }
265
- for (const skillId of coreSkillIds) {
266
- if (!SKILL_ID_PATTERN.test(skillId) || !existsSync(join(vaultPath, skillId, "SKILL.md"))) {
267
- throw new Error(`[core] skill "${skillId}" does not exist in the vault`);
268
- }
269
- for (const [groupName, group] of Object.entries(existingManifest.project ?? {})) {
270
- if (group.skills.includes(skillId)) {
271
- throw new Error(`skill "${skillId}" appears in both [core] and [project.${groupName}]`);
272
- }
273
- }
274
- }
275
- return manifest;
276
- }
277
-
278
- export function applyInit(
279
- vaultPath: string,
280
- confirmedTargets: ConfirmedTarget[],
281
- participant?: InitTransactionParticipant,
282
- coreSkillIds: string[] = [],
283
- ): Manifest {
284
- const manifest = planInitManifest(vaultPath, confirmedTargets, coreSkillIds);
285
-
286
- const manifestPath = join(vaultPath, MANIFEST_FILENAME);
287
- const serializedManifest = serializeManifest(manifest);
288
- const shouldWriteManifest =
289
- !existsSync(manifestPath) || readFileSync(manifestPath, "utf-8") !== serializedManifest;
290
- const createdDirs: string[] = [];
291
- const adoptedDirs: string[] = [];
292
- const migratedFullVaultDirs: Array<{ dir: string; linkTarget: string }> = [];
293
- let participantApplied = false;
294
-
295
- try {
296
- for (const target of confirmedTargets) {
297
- let targetStat;
298
- try {
299
- targetStat = lstatSync(target.dir);
300
- } catch (error) {
301
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
302
- }
303
- if (targetStat?.isSymbolicLink()) {
304
- if (!target.migrateFullVault || realpathSync(target.dir) !== realpathSync(vaultPath)) {
305
- throw new Error(
306
- `target "${target.name}" (${target.dir}) changed to an unsafe symbolic link after preflight`,
307
- );
308
- }
309
- const linkTarget = readlinkSync(target.dir);
310
- unlinkSync(target.dir);
311
- mkdirSync(target.dir, { recursive: true });
312
- migratedFullVaultDirs.push({ dir: target.dir, linkTarget });
313
- } else if (targetStat && !targetStat.isDirectory()) {
314
- throw new Error(`target "${target.name}" (${target.dir}) changed to a non-directory after preflight`);
315
- }
316
- if (!existsSync(target.dir)) {
317
- mkdirSync(target.dir, { recursive: true });
318
- createdDirs.push(target.dir);
319
- }
320
- if (adoptTarget(target.dir, target.name, vaultPath).adopted) {
321
- adoptedDirs.push(target.dir);
322
- }
323
- }
324
-
325
- if (participant) {
326
- participant.apply();
327
- participantApplied = true;
328
- }
329
-
330
- if (shouldWriteManifest) {
331
- const temporaryManifestPath = join(
332
- vaultPath,
333
- `.${MANIFEST_FILENAME}.${process.pid}-${Date.now()}.tmp`,
334
- );
335
- try {
336
- writeFileSync(temporaryManifestPath, serializedManifest);
337
- renameSync(temporaryManifestPath, manifestPath);
338
- } catch (error) {
339
- if (existsSync(temporaryManifestPath)) unlinkSync(temporaryManifestPath);
340
- throw error;
341
- }
342
- }
343
- } catch (error) {
344
- try {
345
- if (participantApplied) participant?.rollback();
346
- } finally {
347
- for (const dir of adoptedDirs.reverse()) {
348
- const markerPath = join(dir, SKILLMUX_MARKER_FILENAME);
349
- if (existsSync(markerPath)) unlinkSync(markerPath);
350
- }
351
- for (const migration of migratedFullVaultDirs.reverse()) {
352
- if (existsSync(migration.dir)) rmdirSync(migration.dir);
353
- symlinkSync(migration.linkTarget, migration.dir);
354
- }
355
- for (const dir of createdDirs.reverse()) {
356
- if (existsSync(dir)) rmdirSync(dir);
357
- }
358
- }
359
- throw error;
360
- }
361
-
362
- return manifest;
363
- }
364
-
365
- export const DISCOVERY_PARAGRAPH =
366
- "Skills: only a curated core is loaded statically. Before improvising a " +
367
- "multi-step workflow, or when a task smells like a domain you have no loaded " +
368
- "skill for (career/resume, trading, SEO, i18n, design, one-off tooling), " +
369
- "call `resolve_skill` with a one-line task description. `resolve_skill` " +
370
- "returns a ranked shortlist; inspect candidates, fetch one or more " +
371
- "relevant skills with `fetch_skill`, or ignore all and proceed normally " +
372
- "when none are useful.";
373
-
374
- export const MCP_REGISTRATION_SNIPPET = JSON.stringify(
375
- { mcpServers: { "skillmux": { command: "skillmux", args: ["serve"] } } },
376
- null,
377
- 2,
378
- );
379
-
380
- /** §3.4 step 4: "print the last mile" — MCP registration command + discovery paragraph. */
381
- export function printLastMile(): string {
382
- return [
383
- "Register with your MCP client:",
384
- MCP_REGISTRATION_SNIPPET,
385
- "",
386
- "Add this paragraph to your shared agent instructions (~80 tokens, the entire T3 discovery mechanism):",
387
- DISCOVERY_PARAGRAPH,
388
- ].join("\n");
389
- }