skiller 0.8.2 → 0.9.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.
@@ -34,6 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.isReferenceBody = isReferenceBody;
37
+ exports.extractLocalRulesFromCanonicalSkills = extractLocalRulesFromCanonicalSkills;
38
+ exports.compileRulesToSkills = compileRulesToSkills;
39
+ exports.normalizeCanonicalSkills = normalizeCanonicalSkills;
37
40
  exports.syncMdcToSkillMd = syncMdcToSkillMd;
38
41
  exports.discoverSkills = discoverSkills;
39
42
  exports.copySkillsToAgent = copySkillsToAgent;
@@ -49,8 +52,74 @@ const yaml = __importStar(require("js-yaml"));
49
52
  const constants_1 = require("../constants");
50
53
  const SkillsUtils_1 = require("./SkillsUtils");
51
54
  const FrontmatterParser_1 = require("./FrontmatterParser");
55
+ const SkillOwnership_1 = require("./SkillOwnership");
52
56
  const LEGACY_CODEX_SKILLS_PATH = path.join('.codex', 'skills');
53
57
  const UNIVERSAL_AGENTS_SKILLS_PATH = path.join('.agents', 'skills');
58
+ async function pathExists(targetPath) {
59
+ try {
60
+ await fs.access(targetPath);
61
+ return true;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ async function resolveProjectSkillsDir(projectRoot, skillerDir) {
68
+ if (skillerDir)
69
+ return path.join(skillerDir, 'skills');
70
+ const canonicalSkillsDir = path.join(projectRoot, '.agents', 'skills');
71
+ if (await pathExists(canonicalSkillsDir)) {
72
+ return canonicalSkillsDir;
73
+ }
74
+ const legacySkillsDir = path.join(projectRoot, '.claude', 'skills');
75
+ if (await pathExists(legacySkillsDir)) {
76
+ return legacySkillsDir;
77
+ }
78
+ return canonicalSkillsDir;
79
+ }
80
+ async function skillFolderContainsMdc(dir, depth = 0) {
81
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
82
+ return false;
83
+ const entries = await fs.readdir(dir, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ const fullPath = path.join(dir, entry.name);
86
+ if (entry.isFile() && entry.name.endsWith('.mdc')) {
87
+ return true;
88
+ }
89
+ if (entry.isDirectory()) {
90
+ const nestedHasMdc = await skillFolderContainsMdc(fullPath, depth + 1);
91
+ if (nestedHasMdc)
92
+ return true;
93
+ }
94
+ }
95
+ return false;
96
+ }
97
+ async function skillCanBeSymlinked(skillPath) {
98
+ const skillMdPath = path.join(skillPath, constants_1.SKILL_MD_FILENAME);
99
+ let skillMdContent;
100
+ try {
101
+ skillMdContent = await fs.readFile(skillMdPath, 'utf8');
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ if (isReferenceBody((0, FrontmatterParser_1.parseFrontmatter)(skillMdContent).body).isReference) {
107
+ return false;
108
+ }
109
+ return !(await skillFolderContainsMdc(skillPath));
110
+ }
111
+ async function createRelativeDirectorySymlink(sourceDir, targetDir) {
112
+ try {
113
+ await fs.rm(targetDir, { recursive: true, force: true });
114
+ await fs.mkdir(path.dirname(targetDir), { recursive: true });
115
+ const relativeTarget = path.relative(path.dirname(targetDir), sourceDir);
116
+ await fs.symlink(relativeTarget, targetDir, 'junction');
117
+ return true;
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ }
54
123
  /**
55
124
  * For non-Claude agents, compile a wrapper SKILL.md (body is a single @reference)
56
125
  * into a standalone SKILL.md with the referenced file's body inlined.
@@ -59,30 +128,14 @@ const UNIVERSAL_AGENTS_SKILLS_PATH = path.join('.agents', 'skills');
59
128
  * an @reference line, to avoid accidentally treating email addresses or
60
129
  * "@mentions" inside real content as file references.
61
130
  */
62
- async function compileSkillMdForNonClaudeAgents(skillMdContent, projectRoot, skillFolderPath) {
131
+ async function compileSkillMdForNonClaudeAgents(skillMdContent, projectRoot, skillFolderPath, options = {}) {
63
132
  const { frontmatter, rawFrontmatter, body } = (0, FrontmatterParser_1.parseFrontmatter)(skillMdContent);
64
- const refCheck = isReferenceBody(body);
65
- if (!refCheck.isReference || !refCheck.referencePath) {
66
- return skillMdContent;
67
- }
68
- const referencePath = refCheck.referencePath;
69
- const absoluteRefPath = referencePath.startsWith('./') || referencePath.startsWith('../')
70
- ? path.resolve(skillFolderPath, referencePath)
71
- : path.resolve(projectRoot, referencePath);
72
- // Security: only inline references within the project root.
73
- const normalizedProjectRoot = path.resolve(projectRoot);
74
- const normalizedAbsoluteRefPath = path.resolve(absoluteRefPath);
75
- if (!normalizedAbsoluteRefPath.startsWith(normalizedProjectRoot + path.sep)) {
76
- return skillMdContent;
77
- }
78
- let referencedContent;
79
- try {
80
- referencedContent = await fs.readFile(normalizedAbsoluteRefPath, 'utf8');
81
- }
82
- catch {
133
+ const compiledBodyResult = await inlineReferenceDirectives(body, projectRoot, options.baseFilePath ?? path.join(skillFolderPath, constants_1.SKILL_MD_FILENAME), {
134
+ fallbackReferenceDir: options.fallbackReferenceDir,
135
+ });
136
+ if (!compiledBodyResult.changed) {
83
137
  return skillMdContent;
84
138
  }
85
- const { body: referencedBody } = (0, FrontmatterParser_1.parseFrontmatter)(referencedContent);
86
139
  const fmData = rawFrontmatter && Object.keys(rawFrontmatter).length > 0
87
140
  ? rawFrontmatter
88
141
  : frontmatter && Object.keys(frontmatter).length > 0
@@ -93,10 +146,10 @@ async function compileSkillMdForNonClaudeAgents(skillMdContent, projectRoot, ski
93
146
  ${yaml.dump(fmData, { lineWidth: -1, noRefs: true }).trim()}
94
147
  ---
95
148
 
96
- ${referencedBody}
149
+ ${compiledBodyResult.body}
97
150
  `;
98
151
  }
99
- return `${referencedBody}\n`;
152
+ return `${compiledBodyResult.body}\n`;
100
153
  }
101
154
  /**
102
155
  * Copies a single skill directory to an agent skill directory:
@@ -151,301 +204,493 @@ function isReferenceBody(body) {
151
204
  }
152
205
  return { isReference: false };
153
206
  }
154
- /**
155
- * Bidirectional sync between .mdc files and SKILL.md in the skills directory.
156
- *
157
- * Sync logic (using @reference pattern instead of synced: true):
158
- * 1. If sibling .mdc exists (name/name.mdc) but no SKILL.md → generate SKILL.md with @./name.mdc
159
- * 2. If SKILL.md body is @reference → referenced file is source of truth
160
- * 3. If SKILL.md has full content generate sibling .mdc, update SKILL.md to @./name.mdc
161
- *
162
- * File structure:
163
- * - .claude/skills/foo/foo.mdc → .claude/skills/foo/SKILL.md (with @./foo.mdc body)
164
- *
165
- * Backward compatibility:
166
- * - .claude/skills/foo.mdc at root → migrates to sibling pattern
167
- * - @.claude/rules/name.mdc → recognized as reference (pre-0.7 pattern)
168
- */
169
- async function syncMdcToSkillMd(skillsDir, verbose, dryRun) {
170
- const synced = [];
207
+ function parseReferenceDirectiveLine(line) {
208
+ const trimmed = line.trim();
209
+ if (!trimmed.startsWith('@'))
210
+ return null;
211
+ const raw = trimmed.slice(1);
212
+ const hashIndex = raw.indexOf('#');
213
+ const pathPart = (hashIndex === -1 ? raw : raw.slice(0, hashIndex)).trim();
214
+ const fragment = hashIndex === -1 ? undefined : raw.slice(hashIndex + 1).trim() || undefined;
215
+ const looksLikePath = pathPart.startsWith('./') ||
216
+ pathPart.startsWith('../') ||
217
+ pathPart.startsWith('.agents/') ||
218
+ pathPart.startsWith('.claude/') ||
219
+ pathPart.includes('/') ||
220
+ /\.[A-Za-z0-9_-]+$/.test(pathPart);
221
+ if (!looksLikePath)
222
+ return null;
223
+ return { pathPart, fragment };
224
+ }
225
+ function slugifyHeading(value) {
226
+ return value
227
+ .trim()
228
+ .toLowerCase()
229
+ .replace(/[`*_~]/g, '')
230
+ .replace(/[^\p{L}\p{N}\s-]/gu, '')
231
+ .replace(/\s+/g, '-')
232
+ .replace(/-+/g, '-');
233
+ }
234
+ function extractMarkdownFragment(body, fragment) {
235
+ if (!fragment)
236
+ return body;
237
+ const lines = body.split('\n');
238
+ const target = fragment.trim();
239
+ const targetSlug = slugifyHeading(target);
240
+ let startIndex = -1;
241
+ let headingLevel = 0;
242
+ for (let index = 0; index < lines.length; index += 1) {
243
+ const match = /^(#{1,6})\s+(.*)$/.exec(lines[index].trim());
244
+ if (!match)
245
+ continue;
246
+ const headingText = match[2].trim();
247
+ if (headingText === target || slugifyHeading(headingText) === targetSlug) {
248
+ startIndex = index + 1;
249
+ headingLevel = match[1].length;
250
+ break;
251
+ }
252
+ }
253
+ if (startIndex === -1)
254
+ return body;
255
+ let endIndex = lines.length;
256
+ for (let index = startIndex; index < lines.length; index += 1) {
257
+ const match = /^(#{1,6})\s+/.exec(lines[index].trim());
258
+ if (match && match[1].length <= headingLevel) {
259
+ endIndex = index;
260
+ break;
261
+ }
262
+ }
263
+ return lines.slice(startIndex, endIndex).join('\n').trim();
264
+ }
265
+ async function readReferenceDirectiveContent(projectRoot, baseFilePath, directive, options) {
266
+ const candidates = [];
267
+ const { pathPart, fragment } = directive;
268
+ if (pathPart.startsWith('./') || pathPart.startsWith('../')) {
269
+ candidates.push(path.resolve(path.dirname(baseFilePath), pathPart));
270
+ if (options.fallbackReferenceDir) {
271
+ candidates.push(path.resolve(options.fallbackReferenceDir, pathPart));
272
+ }
273
+ }
274
+ else {
275
+ candidates.push(path.resolve(projectRoot, pathPart));
276
+ }
277
+ let resolvedPath = null;
278
+ for (const candidate of candidates) {
279
+ try {
280
+ await fs.access(candidate);
281
+ resolvedPath = candidate;
282
+ break;
283
+ }
284
+ catch {
285
+ // Try next candidate.
286
+ }
287
+ }
288
+ if (!resolvedPath)
289
+ return null;
290
+ const visitKey = `${resolvedPath}#${fragment ?? ''}`;
291
+ if (options.visited.has(visitKey)) {
292
+ return null;
293
+ }
294
+ options.visited.add(visitKey);
295
+ try {
296
+ const rawContent = await fs.readFile(resolvedPath, 'utf8');
297
+ const extension = path.extname(resolvedPath).toLowerCase();
298
+ if (extension === '.md' || extension === '.mdc') {
299
+ const parsed = (0, FrontmatterParser_1.parseFrontmatter)(rawContent);
300
+ const nested = await inlineReferenceDirectives(parsed.body, projectRoot, resolvedPath, {
301
+ fallbackReferenceDir: path.dirname(resolvedPath),
302
+ visited: options.visited,
303
+ depth: options.depth + 1,
304
+ });
305
+ return extractMarkdownFragment(nested.body, fragment);
306
+ }
307
+ return rawContent.trim();
308
+ }
309
+ finally {
310
+ options.visited.delete(visitKey);
311
+ }
312
+ }
313
+ async function inlineReferenceDirectives(body, projectRoot, baseFilePath, options = {}) {
314
+ const depth = options.depth ?? 0;
315
+ if (depth >= constants_1.MAX_RECURSION_DEPTH) {
316
+ return { body, changed: false };
317
+ }
318
+ const visited = options.visited ?? new Set();
319
+ const lines = body.split('\n');
320
+ const output = [];
321
+ let changed = false;
322
+ for (const line of lines) {
323
+ const directive = parseReferenceDirectiveLine(line);
324
+ if (!directive) {
325
+ output.push(line);
326
+ continue;
327
+ }
328
+ const referencedContent = await readReferenceDirectiveContent(projectRoot, baseFilePath, directive, {
329
+ fallbackReferenceDir: options.fallbackReferenceDir,
330
+ visited,
331
+ depth,
332
+ });
333
+ if (referencedContent === null) {
334
+ output.push(line);
335
+ continue;
336
+ }
337
+ changed = true;
338
+ output.push(referencedContent.trimEnd());
339
+ }
340
+ return {
341
+ body: output.join('\n'),
342
+ changed,
343
+ };
344
+ }
345
+ function toProjectRelative(projectRoot, targetPath) {
346
+ return path.relative(projectRoot, targetPath).replace(/\\/g, '/');
347
+ }
348
+ function cloneRecord(value) {
349
+ return value && typeof value === 'object' && !Array.isArray(value)
350
+ ? { ...value }
351
+ : {};
352
+ }
353
+ function buildCanonicalSkillFrontmatter(skillName, rawFrontmatter, options = {}) {
354
+ const next = rawFrontmatter ? { ...rawFrontmatter } : {};
355
+ delete next.globs;
356
+ delete next.alwaysApply;
357
+ next.name = skillName;
358
+ if (typeof next.description !== 'string' ||
359
+ next.description.trim().length === 0) {
360
+ next.description = `Skill: ${skillName}`;
361
+ }
362
+ const metadata = cloneRecord(next.metadata);
363
+ const skillerMeta = cloneRecord(metadata.skiller);
364
+ if (options.sourceRelPath) {
365
+ skillerMeta.source = options.sourceRelPath;
366
+ }
367
+ if (options.alwaysApply === true) {
368
+ skillerMeta.alwaysApply = true;
369
+ }
370
+ else {
371
+ delete skillerMeta.alwaysApply;
372
+ }
373
+ if (Object.keys(skillerMeta).length > 0) {
374
+ metadata.skiller = skillerMeta;
375
+ }
376
+ else {
377
+ delete metadata.skiller;
378
+ }
379
+ if (Object.keys(metadata).length > 0) {
380
+ next.metadata = metadata;
381
+ }
382
+ else {
383
+ delete next.metadata;
384
+ }
385
+ return next;
386
+ }
387
+ function buildCanonicalSkillContent(skillName, rawFrontmatter, body, options = {}) {
388
+ const frontmatter = buildCanonicalSkillFrontmatter(skillName, rawFrontmatter, options);
389
+ return `---
390
+ ${yaml.dump(frontmatter, { lineWidth: -1, noRefs: true }).trim()}
391
+ ---
392
+
393
+ ${body.trim()}
394
+ `;
395
+ }
396
+ function buildRuleSourceContent(rawFrontmatter, body) {
397
+ const next = rawFrontmatter ? { ...rawFrontmatter } : {};
398
+ delete next.name;
399
+ delete next.globs;
400
+ const metadata = cloneRecord(next.metadata);
401
+ const skillerMeta = cloneRecord(metadata.skiller);
402
+ const alwaysApply = skillerMeta.alwaysApply === true;
403
+ delete skillerMeta.source;
404
+ delete skillerMeta.alwaysApply;
405
+ if (Object.keys(skillerMeta).length > 0) {
406
+ metadata.skiller = skillerMeta;
407
+ }
408
+ else {
409
+ delete metadata.skiller;
410
+ }
411
+ if (Object.keys(metadata).length > 0) {
412
+ next.metadata = metadata;
413
+ }
414
+ else {
415
+ delete next.metadata;
416
+ }
417
+ if (alwaysApply) {
418
+ next.alwaysApply = true;
419
+ }
420
+ if (Object.keys(next).length === 0) {
421
+ return `${body.trim()}\n`;
422
+ }
423
+ return `---
424
+ ${yaml.dump(next, { lineWidth: -1, noRefs: true }).trim()}
425
+ ---
426
+
427
+ ${body.trim()}
428
+ `;
429
+ }
430
+ function flattenNestedFrontmatter(rawFrontmatter, body) {
431
+ const nested = (0, FrontmatterParser_1.parseFrontmatter)(body);
432
+ if (!nested.rawFrontmatter) {
433
+ return { rawFrontmatter, body };
434
+ }
435
+ return {
436
+ rawFrontmatter: {
437
+ ...(rawFrontmatter ?? {}),
438
+ ...nested.rawFrontmatter,
439
+ },
440
+ body: nested.body,
441
+ };
442
+ }
443
+ function normalizeRuleSourceContent(content) {
444
+ const parsed = (0, FrontmatterParser_1.parseFrontmatter)(content);
445
+ const flattened = flattenNestedFrontmatter(parsed.rawFrontmatter, parsed.body);
446
+ return buildRuleSourceContent(flattened.rawFrontmatter, flattened.body);
447
+ }
448
+ function resolveSkillReferencePath(projectRoot, skillFolderPath, referencePath) {
449
+ return referencePath.startsWith('./') || referencePath.startsWith('../')
450
+ ? path.resolve(skillFolderPath, referencePath)
451
+ : path.resolve(projectRoot, referencePath);
452
+ }
453
+ async function readLegacyLocalRuleSource(projectRoot, skillFolderPath, skillMdContent) {
454
+ const { rawFrontmatter, body } = (0, FrontmatterParser_1.parseFrontmatter)(skillMdContent);
455
+ const refCheck = isReferenceBody(body);
456
+ if (refCheck.isReference && refCheck.referencePath) {
457
+ const referencedPath = resolveSkillReferencePath(projectRoot, skillFolderPath, refCheck.referencePath);
458
+ return fs.readFile(referencedPath, 'utf8');
459
+ }
460
+ const flattened = flattenNestedFrontmatter(rawFrontmatter, body);
461
+ return buildRuleSourceContent(flattened.rawFrontmatter, flattened.body);
462
+ }
463
+ async function writeFileIfChanged(targetPath, content, dryRun) {
464
+ try {
465
+ const existing = await fs.readFile(targetPath, 'utf8');
466
+ if (existing === content) {
467
+ return false;
468
+ }
469
+ }
470
+ catch {
471
+ // Write below.
472
+ }
473
+ if (!dryRun) {
474
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
475
+ await fs.writeFile(targetPath, content, 'utf8');
476
+ }
477
+ return true;
478
+ }
479
+ async function extractLocalRulesFromCanonicalSkills(projectRoot, verbose, dryRun) {
171
480
  const warnings = [];
481
+ const extracted = [];
482
+ const skillsDir = path.join(projectRoot, '.agents', 'skills');
483
+ const rulesDir = path.join(projectRoot, '.agents', 'rules');
484
+ const ownership = await (0, SkillOwnership_1.resolveSkillOwnership)(projectRoot);
172
485
  try {
173
486
  await fs.access(skillsDir);
174
487
  }
175
488
  catch {
176
- // Skills directory doesn't exist
177
- return { synced, warnings };
489
+ return { extracted, warnings };
178
490
  }
179
491
  const entries = await fs.readdir(skillsDir, { withFileTypes: true });
180
- // Find .mdc files at skills root (for backward compatibility/migration)
181
- const rootMdcFiles = entries.filter((e) => e.isFile() && e.name.endsWith('.mdc'));
182
- // First, migrate any root .mdc files to sibling pattern
183
- for (const mdcFile of rootMdcFiles) {
184
- const skillName = path.basename(mdcFile.name, '.mdc');
185
- const rootMdcPath = path.join(skillsDir, mdcFile.name);
492
+ const adoptedNames = [];
493
+ for (const entry of entries) {
494
+ if (!entry.isDirectory())
495
+ continue;
496
+ const skillName = entry.name;
497
+ if (ownership.upstreamOwned.has(skillName)) {
498
+ continue;
499
+ }
500
+ const rulePath = path.join(rulesDir, `${skillName}.mdc`);
501
+ try {
502
+ await fs.access(rulePath);
503
+ continue;
504
+ }
505
+ catch {
506
+ // No explicit local rule yet; safe to extract below.
507
+ }
186
508
  const skillFolderPath = path.join(skillsDir, skillName);
187
- const siblingMdcPath = path.join(skillFolderPath, mdcFile.name);
509
+ const skillMdPath = path.join(skillFolderPath, constants_1.SKILL_MD_FILENAME);
510
+ let skillMdContent;
188
511
  try {
189
- // Create skill folder if needed
190
- if (!dryRun) {
191
- await fs.mkdir(skillFolderPath, { recursive: true });
192
- }
193
- // Move .mdc to sibling location
194
- if (dryRun) {
195
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would migrate ${mdcFile.name} to ${skillName}/${mdcFile.name}`, verbose, dryRun);
196
- }
197
- else {
198
- const mdcContent = await fs.readFile(rootMdcPath, 'utf8');
199
- await fs.writeFile(siblingMdcPath, mdcContent, 'utf8');
200
- await fs.unlink(rootMdcPath);
201
- (0, constants_1.logVerboseInfo)(`Migrated ${mdcFile.name} to ${skillName}/${mdcFile.name}`, verbose, dryRun);
512
+ skillMdContent = await fs.readFile(skillMdPath, 'utf8');
513
+ }
514
+ catch {
515
+ continue;
516
+ }
517
+ try {
518
+ const ruleContent = await readLegacyLocalRuleSource(projectRoot, skillFolderPath, skillMdContent);
519
+ const changed = await writeFileIfChanged(rulePath, ruleContent, dryRun);
520
+ if (changed) {
521
+ (0, constants_1.logVerboseInfo)(dryRun
522
+ ? `DRY RUN: Would extract local skill source ${skillName} to ${toProjectRelative(projectRoot, rulePath)}`
523
+ : `Extracted local skill source ${skillName} to ${toProjectRelative(projectRoot, rulePath)}`, verbose, dryRun);
202
524
  }
525
+ extracted.push(skillName);
526
+ adoptedNames.push(skillName);
203
527
  }
204
528
  catch (err) {
205
- warnings.push(`Failed to migrate ${skillName}.mdc: ${err.message}`);
529
+ warnings.push(`Failed to extract local rule source for ${skillName}: ${err.message}`);
206
530
  }
207
531
  }
208
- // Re-read entries after migration
209
- const updatedEntries = await fs.readdir(skillsDir, { withFileTypes: true });
210
- const updatedSkillFolders = updatedEntries.filter((e) => e.isDirectory());
211
- // Process skill folders
212
- for (const folder of updatedSkillFolders) {
213
- const skillName = folder.name;
532
+ if (adoptedNames.length > 0) {
533
+ await (0, SkillOwnership_1.adoptSkillerOwnedSkillNames)(projectRoot, adoptedNames, dryRun);
534
+ }
535
+ return { extracted, warnings };
536
+ }
537
+ async function compileRulesToSkills(skillerDir, projectRoot, verbose, dryRun) {
538
+ const warnings = [];
539
+ const compiled = [];
540
+ const rulesDir = path.join(skillerDir, 'rules');
541
+ const skillsDir = path.join(skillerDir, 'skills');
542
+ const ownership = await (0, SkillOwnership_1.resolveSkillOwnership)(projectRoot);
543
+ try {
544
+ await fs.access(rulesDir);
545
+ }
546
+ catch {
547
+ return { compiled, warnings };
548
+ }
549
+ const entries = await fs.readdir(rulesDir, { withFileTypes: true });
550
+ const ruleFiles = entries.filter((entry) => {
551
+ return entry.isFile() && entry.name.endsWith('.mdc');
552
+ });
553
+ const adoptedNames = [];
554
+ for (const ruleFile of ruleFiles) {
555
+ const skillName = path.basename(ruleFile.name, '.mdc');
556
+ if (ownership.upstreamOwned.has(skillName)) {
557
+ throw new Error(`Local rule '${skillName}' conflicts with upstream-managed skill '${skillName}' in skills-lock.json`);
558
+ }
559
+ const sourcePath = path.join(rulesDir, ruleFile.name);
560
+ const sourceContent = await fs.readFile(sourcePath, 'utf8');
561
+ const normalizedSourceContent = normalizeRuleSourceContent(sourceContent);
562
+ if (normalizedSourceContent !== sourceContent) {
563
+ await writeFileIfChanged(sourcePath, normalizedSourceContent, dryRun);
564
+ }
565
+ const parsed = (0, FrontmatterParser_1.parseFrontmatter)(normalizedSourceContent);
214
566
  const skillFolderPath = path.join(skillsDir, skillName);
567
+ const compiledBodyResult = await inlineReferenceDirectives(parsed.body, projectRoot, sourcePath, {
568
+ fallbackReferenceDir: skillFolderPath,
569
+ });
570
+ const compiledContent = buildCanonicalSkillContent(skillName, parsed.rawFrontmatter, compiledBodyResult.body, {
571
+ sourceRelPath: toProjectRelative(projectRoot, sourcePath),
572
+ alwaysApply: parsed.frontmatter?.alwaysApply === true,
573
+ });
215
574
  const skillMdPath = path.join(skillFolderPath, constants_1.SKILL_MD_FILENAME);
216
- const siblingMdcPath = path.join(skillFolderPath, `${skillName}.mdc`);
217
- // Check if sibling .mdc exists
218
- let siblingMdcContent = null;
219
- try {
220
- siblingMdcContent = await fs.readFile(siblingMdcPath, 'utf8');
575
+ const changed = await writeFileIfChanged(skillMdPath, compiledContent, dryRun);
576
+ if (!dryRun) {
577
+ await fs.mkdir(skillFolderPath, { recursive: true });
578
+ const skillEntries = await fs.readdir(skillFolderPath, {
579
+ withFileTypes: true,
580
+ });
581
+ for (const entry of skillEntries) {
582
+ if (entry.isFile() && entry.name.endsWith('.mdc')) {
583
+ await fs.rm(path.join(skillFolderPath, entry.name), {
584
+ force: true,
585
+ });
586
+ }
587
+ }
221
588
  }
222
- catch {
223
- // No sibling .mdc
589
+ if (changed) {
590
+ (0, constants_1.logVerboseInfo)(dryRun
591
+ ? `DRY RUN: Would compile ${toProjectRelative(projectRoot, sourcePath)} to ${toProjectRelative(projectRoot, skillMdPath)}`
592
+ : `Compiled ${toProjectRelative(projectRoot, sourcePath)} to ${toProjectRelative(projectRoot, skillMdPath)}`, verbose, dryRun);
224
593
  }
225
- // Check if SKILL.md exists
594
+ compiled.push(skillName);
595
+ adoptedNames.push(skillName);
596
+ }
597
+ if (adoptedNames.length > 0) {
598
+ await (0, SkillOwnership_1.adoptSkillerOwnedSkillNames)(projectRoot, adoptedNames, dryRun);
599
+ }
600
+ return { compiled, warnings };
601
+ }
602
+ async function normalizeCanonicalSkills(projectRoot, skillsDir, verbose, dryRun) {
603
+ const normalized = [];
604
+ const warnings = [];
605
+ try {
606
+ await fs.access(skillsDir);
607
+ }
608
+ catch {
609
+ return { normalized, warnings };
610
+ }
611
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
612
+ for (const entry of entries) {
613
+ if (!entry.isDirectory())
614
+ continue;
615
+ const skillName = entry.name;
616
+ const skillFolderPath = path.join(skillsDir, skillName);
617
+ const skillMdPath = path.join(skillFolderPath, constants_1.SKILL_MD_FILENAME);
226
618
  let skillMdContent = null;
227
619
  try {
228
620
  skillMdContent = await fs.readFile(skillMdPath, 'utf8');
229
621
  }
230
622
  catch {
231
- // No SKILL.md
623
+ skillMdContent = null;
232
624
  }
233
- try {
234
- if (siblingMdcContent !== null && skillMdContent === null) {
235
- // Case 1: Sibling .mdc exists but no SKILL.md
236
- const { frontmatter: mdcFrontmatter } = (0, FrontmatterParser_1.parseFrontmatter)(siblingMdcContent);
237
- // Skip SKILL.md generation for .mdc files with alwaysApply: true
238
- // These are Cursor-style rules, not Claude Code skills
239
- if (mdcFrontmatter?.alwaysApply === true) {
240
- (0, constants_1.logVerboseInfo)(`Skipping SKILL.md generation for ${skillName} (alwaysApply rule)`, verbose, dryRun);
241
- continue;
242
- }
243
- // Generate SKILL.md with @reference (absolute path)
244
- // Keep all frontmatter from .mdc except globs and alwaysApply
245
- const skillFrontmatter = {
246
- name: skillName,
247
- ...Object.fromEntries(Object.entries(mdcFrontmatter || {}).filter(([key]) => key !== 'globs' && key !== 'alwaysApply')),
248
- };
249
- // Ensure description has a default
250
- if (!skillFrontmatter.description) {
251
- skillFrontmatter.description = `Skill: ${skillName}`;
252
- }
253
- const newSkillMd = `---
254
- ${yaml.dump(skillFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
255
- ---
256
-
257
- @.claude/skills/${skillName}/${skillName}.mdc
258
- `;
259
- if (dryRun) {
260
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would generate ${skillName}/SKILL.md with @reference`, verbose, dryRun);
261
- }
262
- else {
263
- await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
264
- (0, constants_1.logVerboseInfo)(`Generated ${skillName}/SKILL.md with @.claude/skills/${skillName}/${skillName}.mdc reference`, verbose, dryRun);
625
+ let changed = false;
626
+ if (skillMdContent) {
627
+ const compiled = await compileSkillMdForNonClaudeAgents(skillMdContent, projectRoot, skillFolderPath, {
628
+ baseFilePath: skillMdPath,
629
+ fallbackReferenceDir: skillFolderPath,
630
+ });
631
+ if (compiled !== skillMdContent) {
632
+ if (!dryRun) {
633
+ await fs.writeFile(skillMdPath, compiled, 'utf8');
265
634
  }
266
- synced.push(skillName);
635
+ changed = true;
267
636
  }
268
- else if (skillMdContent !== null) {
269
- // Check if sibling .mdc has alwaysApply: true - if so, delete SKILL.md
270
- if (siblingMdcContent !== null) {
271
- const { frontmatter: mdcFrontmatter } = (0, FrontmatterParser_1.parseFrontmatter)(siblingMdcContent);
272
- if (mdcFrontmatter?.alwaysApply === true) {
273
- // .mdc is now an alwaysApply rule - remove the SKILL.md
274
- if (dryRun) {
275
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would delete ${skillName}/SKILL.md (now alwaysApply rule)`, verbose, dryRun);
276
- }
277
- else {
278
- await fs.unlink(skillMdPath);
279
- (0, constants_1.logVerboseInfo)(`Deleted ${skillName}/SKILL.md (now alwaysApply rule)`, verbose, dryRun);
280
- }
281
- synced.push(skillName);
282
- continue;
283
- }
284
- }
285
- // SKILL.md exists - check if it's a reference
286
- const { frontmatter: skillFrontmatter, rawFrontmatter: skillRawFrontmatter, body: skillBody, } = (0, FrontmatterParser_1.parseFrontmatter)(skillMdContent);
287
- const refCheck = isReferenceBody(skillBody);
288
- if (refCheck.isReference) {
289
- // Case 2: SKILL.md is @reference → source file is truth
290
- // Check for both relative and absolute sibling reference patterns
291
- const isRelativeSiblingRef = refCheck.referencePath === `./${skillName}.mdc`;
292
- const isAbsoluteSiblingRef = refCheck.referencePath ===
293
- `.claude/skills/${skillName}/${skillName}.mdc`;
294
- if (isRelativeSiblingRef || isAbsoluteSiblingRef) {
295
- // Sibling reference pattern - only migrate path if needed (don't touch frontmatter)
296
- if (isRelativeSiblingRef) {
297
- // Migrate old relative refs to absolute path, preserving existing frontmatter
298
- const newSkillMd = `---
299
- ${yaml.dump(skillFrontmatter || { name: skillName }, { lineWidth: -1, noRefs: true }).trim()}
300
- ---
301
-
302
- @.claude/skills/${skillName}/${skillName}.mdc
303
- `;
304
- if (dryRun) {
305
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would migrate ${skillName}/SKILL.md to absolute path`, verbose, dryRun);
306
- }
307
- else {
308
- await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
309
- (0, constants_1.logVerboseInfo)(`Migrated ${skillName}/SKILL.md to absolute path`, verbose, dryRun);
310
- }
311
- synced.push(skillName);
312
- }
313
- // If already absolute path, nothing to do - SKILL.md is source of truth for frontmatter
314
- }
315
- else if (refCheck.referencePath) {
316
- // Pre-0.7 pattern or other external reference - migrate to sibling pattern
317
- // Determine base path for resolution:
318
- // - Paths starting with .claude/ are relative to project root
319
- // - Other paths are relative to the skill folder
320
- let referencedPath;
321
- if (refCheck.referencePath.startsWith('.claude/')) {
322
- // Project root is parent of .claude directory (skillsDir is .claude/skills)
323
- const projectRoot = path.dirname(path.dirname(skillsDir));
324
- referencedPath = path.join(projectRoot, refCheck.referencePath);
325
- }
326
- else {
327
- referencedPath = path.resolve(skillFolderPath, refCheck.referencePath);
328
- }
329
- // One-time migration: for old @.claude/rules/ references, prefer the
330
- // original rules path if it exists, then fall back to migrated skills.
331
- const candidatePaths = [referencedPath];
332
- if (refCheck.referencePath?.includes('/rules/')) {
333
- const refFileName = path.basename(refCheck.referencePath);
334
- const refBaseName = path.basename(refFileName, '.mdc');
335
- candidatePaths.push(path.join(skillsDir, refBaseName, refFileName));
336
- }
337
- let referencedContent = null;
338
- let actualPath = referencedPath;
339
- for (const candidatePath of candidatePaths) {
340
- try {
341
- referencedContent = await fs.readFile(candidatePath, 'utf8');
342
- actualPath = candidatePath;
343
- break;
344
- }
345
- catch {
346
- // Try next candidate
347
- }
348
- }
349
- if (referencedContent === null) {
350
- warnings.push(`Cannot migrate ${skillName}: referenced file not found at ${actualPath}`);
351
- }
352
- else {
353
- // Parse the referenced file for frontmatter
354
- const { frontmatter: refFrontmatter, body: refBody } = (0, FrontmatterParser_1.parseFrontmatter)(referencedContent);
355
- // Create sibling .mdc - only keep frontmatter for alwaysApply rules
356
- let mdcContent;
357
- if (refFrontmatter?.alwaysApply === true) {
358
- // alwaysApply rules keep frontmatter (description since no SKILL.md)
359
- const mdcFrontmatterData = {
360
- alwaysApply: true,
361
- };
362
- if (refFrontmatter.description) {
363
- mdcFrontmatterData.description = refFrontmatter.description;
364
- }
365
- mdcContent = `---
366
- ${yaml.dump(mdcFrontmatterData, { lineWidth: -1, noRefs: true }).trim()}
367
- ---
368
-
369
- ${refBody}
370
- `;
371
- }
372
- else {
373
- // Regular skills: body only (description goes in SKILL.md)
374
- mdcContent = refBody;
375
- }
376
- // Update SKILL.md to point to sibling .mdc (absolute path)
377
- const newFrontmatter = {
378
- name: skillFrontmatter?.name || skillName,
379
- description: refFrontmatter?.description ||
380
- skillFrontmatter?.description ||
381
- `Skill: ${skillName}`,
382
- };
383
- const newSkillMd = `---
384
- ${yaml.dump(newFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
385
- ---
386
-
387
- @.claude/skills/${skillName}/${skillName}.mdc
388
- `;
389
- if (dryRun) {
390
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would migrate ${skillName} from ${refCheck.referencePath} to sibling pattern`, verbose, dryRun);
391
- }
392
- else {
393
- await fs.writeFile(siblingMdcPath, mdcContent, 'utf8');
394
- await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
395
- (0, constants_1.logVerboseInfo)(`Migrated ${skillName} from ${actualPath} to sibling pattern`, verbose, dryRun);
396
- }
397
- synced.push(skillName);
398
- }
399
- }
400
- }
401
- else {
402
- // Case 3: SKILL.md has full content → generate sibling .mdc, update to @reference
403
- // Generate .mdc from SKILL.md body (no frontmatter needed - description is in SKILL.md)
404
- const mdcContent = skillBody;
405
- // Update SKILL.md to @reference (absolute path)
406
- // Preserve ALL existing frontmatter (use rawFrontmatter to keep custom fields like user-invocable)
407
- // Only add defaults for missing name/description
408
- const newSkillFrontmatter = skillRawFrontmatter ? { ...skillRawFrontmatter } : {};
409
- if (!newSkillFrontmatter.name) {
410
- newSkillFrontmatter.name = skillName;
411
- }
412
- if (!newSkillFrontmatter.description) {
413
- newSkillFrontmatter.description = `Skill: ${skillName}`;
414
- }
415
- const newSkillMd = `---
416
- ${yaml.dump(newSkillFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
417
- ---
418
-
419
- @.claude/skills/${skillName}/${skillName}.mdc
420
- `;
421
- if (dryRun) {
422
- (0, constants_1.logVerboseInfo)(`DRY RUN: Would generate ${skillName}/${skillName}.mdc and update SKILL.md`, verbose, dryRun);
423
- }
424
- else {
425
- await fs.writeFile(siblingMdcPath, mdcContent, 'utf8');
426
- await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
427
- (0, constants_1.logVerboseInfo)(`Generated ${skillName}/${skillName}.mdc and updated SKILL.md to @reference`, verbose, dryRun);
428
- }
429
- synced.push(skillName);
637
+ }
638
+ const folderEntries = await fs.readdir(skillFolderPath, {
639
+ withFileTypes: true,
640
+ });
641
+ const mdcEntries = folderEntries.filter((folderEntry) => folderEntry.isFile() && folderEntry.name.endsWith('.mdc'));
642
+ const legacySingleMdcSourcePath = skillMdContent === null && mdcEntries.length === 1
643
+ ? path.join(skillFolderPath, mdcEntries[0].name)
644
+ : null;
645
+ if (legacySingleMdcSourcePath) {
646
+ const sourceContent = await fs.readFile(legacySingleMdcSourcePath, 'utf8');
647
+ const parsed = (0, FrontmatterParser_1.parseFrontmatter)(sourceContent);
648
+ const compiledBodyResult = await inlineReferenceDirectives(parsed.body, projectRoot, legacySingleMdcSourcePath, {
649
+ fallbackReferenceDir: skillFolderPath,
650
+ });
651
+ const compiledContent = buildCanonicalSkillContent(skillName, parsed.rawFrontmatter, compiledBodyResult.body, {
652
+ sourceRelPath: toProjectRelative(projectRoot, legacySingleMdcSourcePath),
653
+ alwaysApply: parsed.frontmatter?.alwaysApply === true,
654
+ });
655
+ if (!dryRun) {
656
+ await fs.writeFile(skillMdPath, compiledContent, 'utf8');
657
+ }
658
+ changed = true;
659
+ }
660
+ else if (skillMdContent === null && mdcEntries.length > 1) {
661
+ warnings.push(`Canonical skill '${skillName}' has multiple legacy .mdc files and no SKILL.md`);
662
+ }
663
+ if (mdcEntries.length > 0) {
664
+ if (!dryRun) {
665
+ for (const mdcEntry of mdcEntries) {
666
+ await fs.rm(path.join(skillFolderPath, mdcEntry.name), {
667
+ force: true,
668
+ });
430
669
  }
431
670
  }
432
- // If neither exists, skip - not a valid skill folder
671
+ changed = true;
433
672
  }
434
- catch (err) {
435
- warnings.push(`Failed to sync ${skillName}: ${err.message}`);
673
+ if (changed) {
674
+ normalized.push(skillName);
675
+ (0, constants_1.logVerboseInfo)(dryRun
676
+ ? `DRY RUN: Would normalize canonical skill '${skillName}'`
677
+ : `Normalized canonical skill '${skillName}'`, verbose, dryRun);
436
678
  }
437
679
  }
438
- return { synced, warnings };
680
+ return { normalized, warnings };
681
+ }
682
+ // Deprecated compatibility shim. Canonical skills are now plain SKILL.md only.
683
+ async function syncMdcToSkillMd(skillsDir, verbose, dryRun) {
684
+ const projectRoot = path.resolve(skillsDir, '..', '..');
685
+ const { normalized, warnings } = await normalizeCanonicalSkills(projectRoot, skillsDir, verbose, dryRun);
686
+ return { synced: normalized, warnings };
439
687
  }
440
688
  /**
441
- * Discovers skills in the project's skills directory (.claude/skills).
689
+ * Discovers skills in the project's canonical skills directory.
442
690
  * Returns discovered skills, validation warnings, and deleted empty folders.
443
691
  */
444
692
  async function discoverSkills(projectRoot, skillerDir) {
445
- // Use .claude/skills
446
- const skillsPath = skillerDir
447
- ? path.join(skillerDir, 'skills')
448
- : path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
693
+ const skillsPath = await resolveProjectSkillsDir(projectRoot, skillerDir);
449
694
  // Check if skills directory exists
450
695
  try {
451
696
  await fs.access(skillsPath);
@@ -530,10 +775,15 @@ async function copySkillsToAgent(sourceSkillsDir, targetSkillsDir, projectRoot,
530
775
  }
531
776
  taken.add(destName);
532
777
  const targetSkillPath = path.join(targetSkillsDir, destName);
778
+ const sourceLeafName = path.basename(skillPath);
533
779
  if (!dryRun) {
534
- await copySkillDirectoryForNonClaudeAgents(skillPath, targetSkillPath, projectRoot, skillPath);
535
- const sourceLeafName = path.basename(skillPath);
536
- if (destName !== sourceLeafName) {
780
+ const symlinkSafe = destName === sourceLeafName && (await skillCanBeSymlinked(skillPath));
781
+ const symlinkCreated = symlinkSafe &&
782
+ (await createRelativeDirectorySymlink(skillPath, targetSkillPath));
783
+ if (!symlinkCreated) {
784
+ await copySkillDirectoryForNonClaudeAgents(skillPath, targetSkillPath, projectRoot, skillPath);
785
+ }
786
+ if (!symlinkCreated && destName !== sourceLeafName) {
537
787
  await rewriteSkillMdName(path.join(targetSkillPath, constants_1.SKILL_MD_FILENAME), destName);
538
788
  }
539
789
  }
@@ -546,11 +796,11 @@ async function copySkillsToAgent(sourceSkillsDir, targetSkillsDir, projectRoot,
546
796
  }
547
797
  /**
548
798
  * Gets the paths that skills will generate, for gitignore purposes.
549
- * Collects paths from all agents with native skills support, excluding the source (.claude/skills).
799
+ * Collects paths from all agents with native skills support, excluding the canonical source.
550
800
  */
551
801
  function getSkillsGitignorePaths(projectRoot, agents) {
552
802
  const paths = [];
553
- const sourceSkillsPath = path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
803
+ const sourceSkillsPath = path.join(projectRoot, constants_1.CANONICAL_SKILLS_PATH);
554
804
  for (const agent of agents) {
555
805
  if (agent.supportsNativeSkills?.() && agent.getSkillsPath) {
556
806
  const skillsPath = agent.getSkillsPath(projectRoot);
@@ -568,15 +818,16 @@ function getSkillsGitignorePaths(projectRoot, agents) {
568
818
  }
569
819
  /**
570
820
  * Propagates skills for agents that need them.
571
- * In the new architecture, skills are committed to .claude/skills and discovered by agents natively.
572
- * This function now only discovers and validates skills.
821
+ * Canonical skills live in .agents/skills, and local .mdc authoring lives in .agents/rules.
573
822
  */
574
823
  async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryRun, skillerDir) {
575
- async function migrateLegacyCodexSkillsDir(destinationPaths) {
824
+ async function migrateLegacyCodexSkillsDir(currentSourceSkillsDir, destinationPaths) {
576
825
  const universalSkillsDir = path.join(projectRoot, UNIVERSAL_AGENTS_SKILLS_PATH);
577
826
  const legacyCodexSkillsDir = path.join(projectRoot, LEGACY_CODEX_SKILLS_PATH);
578
- if (!destinationPaths.has(universalSkillsDir))
827
+ if (currentSourceSkillsDir !== universalSkillsDir &&
828
+ !destinationPaths.has(universalSkillsDir)) {
579
829
  return;
830
+ }
580
831
  try {
581
832
  await fs.access(legacyCodexSkillsDir);
582
833
  }
@@ -615,11 +866,19 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
615
866
  (0, constants_1.logVerboseInfo)('Skills support disabled', verbose, dryRun);
616
867
  return;
617
868
  }
618
- // Determine skills directory - always use .claude/skills
619
- const skillsDir = skillerDir
620
- ? path.join(skillerDir, 'skills')
621
- : path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
622
- // Compute destinations up-front so plugin sync can run even if .claude/skills is missing.
869
+ if (skillerDir) {
870
+ const extractedResult = await extractLocalRulesFromCanonicalSkills(projectRoot, verbose, dryRun);
871
+ for (const warning of extractedResult.warnings) {
872
+ (0, constants_1.logWarn)(warning, dryRun);
873
+ }
874
+ const compileResult = await compileRulesToSkills(skillerDir, projectRoot, verbose, dryRun);
875
+ for (const warning of compileResult.warnings) {
876
+ (0, constants_1.logWarn)(warning, dryRun);
877
+ }
878
+ }
879
+ // Determine canonical skills directory, with legacy fallback for migration.
880
+ const skillsDir = await resolveProjectSkillsDir(projectRoot, skillerDir);
881
+ // Compute destinations up-front so legacy codex migration can de-duplicate targets.
623
882
  const destinationPaths = new Set();
624
883
  for (const agent of agents) {
625
884
  if (agent.supportsNativeSkills?.() && agent.getSkillsPath) {
@@ -630,7 +889,7 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
630
889
  }
631
890
  }
632
891
  }
633
- await migrateLegacyCodexSkillsDir(destinationPaths);
892
+ await migrateLegacyCodexSkillsDir(skillsDir, destinationPaths);
634
893
  // Check if skills directory exists
635
894
  let skillsDirExists = true;
636
895
  try {
@@ -638,15 +897,15 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
638
897
  }
639
898
  catch {
640
899
  skillsDirExists = false;
641
- (0, constants_1.logVerboseInfo)(`No .claude/skills directory found`, verbose, dryRun);
900
+ (0, constants_1.logVerboseInfo)(`No skills directory found`, verbose, dryRun);
901
+ }
902
+ const ownership = await (0, SkillOwnership_1.resolveSkillOwnership)(projectRoot);
903
+ for (const warning of ownership.warnings) {
904
+ (0, constants_1.logWarn)(warning, dryRun);
642
905
  }
643
906
  if (skillsDirExists) {
644
- // Sync standalone .mdc files to SKILL.md folders before discovery
645
- const syncResult = await syncMdcToSkillMd(skillsDir, verbose, dryRun);
646
- if (syncResult.synced.length > 0) {
647
- (0, constants_1.logVerboseInfo)(`Synced ${syncResult.synced.length} .mdc file(s) to SKILL.md`, verbose, dryRun);
648
- }
649
- for (const warning of syncResult.warnings) {
907
+ const normalizeResult = await normalizeCanonicalSkills(projectRoot, skillsDir, verbose, dryRun);
908
+ for (const warning of normalizeResult.warnings) {
650
909
  (0, constants_1.logWarn)(warning, dryRun);
651
910
  }
652
911
  // Discover and validate skills
@@ -660,7 +919,7 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
660
919
  }
661
920
  }
662
921
  if (skills.length === 0) {
663
- (0, constants_1.logVerboseInfo)('No valid skills found in .claude/skills', verbose, dryRun);
922
+ (0, constants_1.logVerboseInfo)('No valid skills found in project skills directory', verbose, dryRun);
664
923
  }
665
924
  else {
666
925
  (0, constants_1.logVerboseInfo)(`Discovered ${skills.length} skill(s)`, verbose, dryRun);
@@ -677,7 +936,7 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
677
936
  }
678
937
  }
679
938
  // Sync project Claude commands + agents as skills into agent skills dirs.
680
- // This intentionally does NOT write into the committed .claude/skills source-of-truth.
939
+ // This intentionally does NOT write into the canonical .agents/skills source-of-truth.
681
940
  if (destinationPaths.size > 0) {
682
941
  const { syncClaudeProjectCommandsAndAgentsToSkillsDirs } = await Promise.resolve().then(() => __importStar(require('./ClaudeProjectSync')));
683
942
  await syncClaudeProjectCommandsAndAgentsToSkillsDirs({
@@ -687,17 +946,6 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
687
946
  dryRun,
688
947
  });
689
948
  }
690
- // Sync Claude plugins (skills + commands converted to skills) into agent skills dirs.
691
- // This intentionally does NOT write into the committed .claude/skills source-of-truth.
692
- if (destinationPaths.size > 0) {
693
- const { syncClaudePluginsToSkillsDirs } = await Promise.resolve().then(() => __importStar(require('./ClaudePluginSync')));
694
- await syncClaudePluginsToSkillsDirs({
695
- projectRoot,
696
- targetSkillsDirs: [...destinationPaths],
697
- verbose,
698
- dryRun,
699
- });
700
- }
701
949
  }
702
950
  /**
703
951
  * Recursively finds all folders containing SKILL.md in a directory.