@csark0812/skeleton 1.5.1 → 1.5.3

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.
package/dist/cli.js CHANGED
@@ -15651,6 +15651,9 @@ var require_extend = __commonJS((exports, module) => {
15651
15651
  };
15652
15652
  });
15653
15653
 
15654
+ // src/cli.ts
15655
+ import { readFileSync as readFileSync22 } from "node:fs";
15656
+
15654
15657
  // src/audit/config/load.ts
15655
15658
  var import_ajv = __toESM(require_ajv(), 1);
15656
15659
  import { existsSync, readFileSync } from "node:fs";
@@ -16984,12 +16987,99 @@ async function loadPlugins(root, config) {
16984
16987
  }
16985
16988
 
16986
16989
  // src/audit/core/collect.ts
16987
- import { existsSync as existsSync6, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
16988
- import { join as join5, relative as relative4 } from "node:path";
16990
+ import { existsSync as existsSync7, readFileSync as readFileSync5, realpathSync as realpathSync4, statSync as statSync2 } from "node:fs";
16991
+ import { join as join6, relative as relative4 } from "node:path";
16992
+
16993
+ // src/audit/core/skill-roots.ts
16994
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
16995
+ import { join as join5, relative as relative3 } from "node:path";
16996
+
16997
+ // src/audit/core/skill-provenance.ts
16998
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
16999
+ import { join as join4 } from "node:path";
17000
+ var DEFAULT_SKILLS_LOCKFILE = "skills-lock.json";
17001
+ function isForeignLockSourceType(sourceType) {
17002
+ return sourceType !== "local";
17003
+ }
17004
+ function loadSkillsLock(root, lockfileRel = DEFAULT_SKILLS_LOCKFILE) {
17005
+ const warnings = [];
17006
+ const abs = join4(root, lockfileRel);
17007
+ if (!existsSync5(abs)) {
17008
+ return { lockfile: null, entries: {}, warnings };
17009
+ }
17010
+ let raw;
17011
+ try {
17012
+ raw = JSON.parse(readFileSync4(abs, "utf8"));
17013
+ } catch (error) {
17014
+ warnings.push(`malformed ${lockfileRel}: ${error instanceof Error ? error.message : String(error)}`);
17015
+ return { lockfile: lockfileRel, entries: {}, warnings };
17016
+ }
17017
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
17018
+ warnings.push(`${lockfileRel}: expected a JSON object`);
17019
+ return { lockfile: lockfileRel, entries: {}, warnings };
17020
+ }
17021
+ const obj = raw;
17022
+ if (obj.version !== 1) {
17023
+ warnings.push(`${lockfileRel}: unsupported version ${String(obj.version)} (expected 1); ignoring entries`);
17024
+ return { lockfile: lockfileRel, entries: {}, warnings };
17025
+ }
17026
+ const skillsRaw = obj.skills;
17027
+ if (!skillsRaw || typeof skillsRaw !== "object" || Array.isArray(skillsRaw)) {
17028
+ warnings.push(`${lockfileRel}: missing or invalid "skills" object`);
17029
+ return { lockfile: lockfileRel, entries: {}, warnings };
17030
+ }
17031
+ const entries = {};
17032
+ for (const [slug, value] of Object.entries(skillsRaw)) {
17033
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
17034
+ warnings.push(`${lockfileRel}: skill "${slug}" has invalid entry`);
17035
+ continue;
17036
+ }
17037
+ const entry = value;
17038
+ const source = entry.source;
17039
+ const sourceType = entry.sourceType;
17040
+ if (typeof source !== "string" || source.length === 0) {
17041
+ warnings.push(`${lockfileRel}: skill "${slug}" missing source`);
17042
+ continue;
17043
+ }
17044
+ if (typeof sourceType !== "string" || sourceType.length === 0) {
17045
+ warnings.push(`${lockfileRel}: skill "${slug}" missing sourceType`);
17046
+ continue;
17047
+ }
17048
+ const parsed = { source, sourceType };
17049
+ if (typeof entry.skillPath === "string")
17050
+ parsed.skillPath = entry.skillPath;
17051
+ if (typeof entry.computedHash === "string")
17052
+ parsed.computedHash = entry.computedHash;
17053
+ entries[slug] = parsed;
17054
+ }
17055
+ return { lockfile: lockfileRel, entries, warnings };
17056
+ }
17057
+ function classifySkillOwnership(slug, provenance, ownership) {
17058
+ const ownedOverrides = new Set(ownership?.ownedSlugs ?? []);
17059
+ const foreignOverrides = new Set(ownership?.foreignSlugs ?? []);
17060
+ if (ownedOverrides.has(slug))
17061
+ return "owned";
17062
+ if (foreignOverrides.has(slug))
17063
+ return "foreign";
17064
+ const entry = provenance.entries[slug];
17065
+ if (entry && isForeignLockSourceType(entry.sourceType))
17066
+ return "foreign";
17067
+ return "owned";
17068
+ }
17069
+ function resolveOwnershipForSlugs(slugs, provenance, ownership) {
17070
+ const ownedSlugs = [];
17071
+ const foreignSlugs = [];
17072
+ for (const slug of slugs) {
17073
+ if (classifySkillOwnership(slug, provenance, ownership) === "foreign") {
17074
+ foreignSlugs.push(slug);
17075
+ } else {
17076
+ ownedSlugs.push(slug);
17077
+ }
17078
+ }
17079
+ return { ownedSlugs, foreignSlugs };
17080
+ }
16989
17081
 
16990
17082
  // src/audit/core/skill-roots.ts
16991
- import { existsSync as existsSync5, readdirSync as readdirSync2, readlinkSync, realpathSync as realpathSync3 } from "node:fs";
16992
- import { join as join4, relative as relative3 } from "node:path";
16993
17083
  var NESTED_SKILL_ROOTS = [".claude/skills", ".agents/skills"];
16994
17084
  var FLAT_SKILL_DENYLIST = new Set([
16995
17085
  ".git",
@@ -17018,10 +17108,10 @@ function safeRealpath(path) {
17018
17108
  }
17019
17109
  }
17020
17110
  function listNestedSlugs(root, relRoot) {
17021
- const absRoot = join4(root, relRoot);
17022
- if (!existsSync5(absRoot))
17111
+ const absRoot = join5(root, relRoot);
17112
+ if (!existsSync6(absRoot))
17023
17113
  return [];
17024
- return readdirSync2(absRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NESTED_EXCLUDED_DIRS.has(entry.name)).filter((entry) => existsSync5(join4(absRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
17114
+ return readdirSync2(absRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !NESTED_EXCLUDED_DIRS.has(entry.name)).filter((entry) => existsSync6(join5(absRoot, entry.name, "SKILL.md"))).map((entry) => entry.name).sort();
17025
17115
  }
17026
17116
  function listFlatSlugs(root) {
17027
17117
  const slugs = [];
@@ -17030,7 +17120,7 @@ function listFlatSlugs(root) {
17030
17120
  continue;
17031
17121
  if (FLAT_SKILL_DENYLIST.has(entry.name))
17032
17122
  continue;
17033
- if (existsSync5(join4(root, entry.name, "SKILL.md"))) {
17123
+ if (existsSync6(join5(root, entry.name, "SKILL.md"))) {
17034
17124
  slugs.push(entry.name);
17035
17125
  }
17036
17126
  }
@@ -17040,8 +17130,8 @@ function detectSkillRoots(root) {
17040
17130
  const roots = [];
17041
17131
  let claudeReal = null;
17042
17132
  for (const relRoot of NESTED_SKILL_ROOTS) {
17043
- const abs = join4(root, relRoot);
17044
- if (!existsSync5(abs))
17133
+ const abs = join5(root, relRoot);
17134
+ if (!existsSync6(abs))
17045
17135
  continue;
17046
17136
  if (relRoot === ".claude/skills") {
17047
17137
  claudeReal = safeRealpath(abs);
@@ -17055,11 +17145,11 @@ function detectSkillRoots(root) {
17055
17145
  continue;
17056
17146
  try {
17057
17147
  const link = readlinkSync(abs);
17058
- if (link && claudeReal && safeRealpath(join4(root, link)) === claudeReal)
17148
+ if (link && claudeReal && safeRealpath(join5(root, link)) === claudeReal)
17059
17149
  continue;
17060
17150
  } catch {}
17061
17151
  }
17062
- if (listNestedSlugs(root, relRoot).length > 0 || existsSync5(abs)) {
17152
+ if (listNestedSlugs(root, relRoot).length > 0 || existsSync6(abs)) {
17063
17153
  roots.push({ kind: "nested", relPath: relRoot });
17064
17154
  }
17065
17155
  }
@@ -17069,12 +17159,13 @@ function detectSkillRoots(root) {
17069
17159
  }
17070
17160
  return roots;
17071
17161
  }
17072
- function buildSkillIndex(root) {
17162
+ function buildSkillIndex(root, ownership) {
17073
17163
  const roots = detectSkillRoots(root);
17074
17164
  const slugSet = new Set;
17075
17165
  const slugs = [];
17166
+ const flatSlugs = listFlatSlugs(root);
17076
17167
  for (const skillRoot of roots) {
17077
- const rootSlugs = skillRoot.kind === "nested" ? listNestedSlugs(root, skillRoot.relPath) : listFlatSlugs(root);
17168
+ const rootSlugs = skillRoot.kind === "nested" ? listNestedSlugs(root, skillRoot.relPath) : flatSlugs;
17078
17169
  for (const slug of rootSlugs) {
17079
17170
  if (!slugSet.has(slug)) {
17080
17171
  slugSet.add(slug);
@@ -17082,40 +17173,94 @@ function buildSkillIndex(root) {
17082
17173
  }
17083
17174
  }
17084
17175
  }
17085
- return { roots, slugs };
17176
+ const lockfileRel = ownership?.lockfile ?? DEFAULT_SKILLS_LOCKFILE;
17177
+ const provenance = loadSkillsLock(root, lockfileRel);
17178
+ const { ownedSlugs, foreignSlugs } = resolveOwnershipForSlugs(slugs, provenance, ownership);
17179
+ return { roots, slugs, flatSlugs, ownedSlugs, foreignSlugs, provenance };
17180
+ }
17181
+ function isForeignSkillSlug(index, slug) {
17182
+ return index.foreignSlugs.includes(slug);
17086
17183
  }
17087
17184
  function resolveSkillPath(index, root, slug) {
17088
17185
  for (const skillRoot of index.roots) {
17089
- const candidate = skillRoot.kind === "nested" ? join4(root, skillRoot.relPath, slug, "SKILL.md") : join4(root, slug, "SKILL.md");
17090
- if (existsSync5(candidate)) {
17186
+ const candidate = skillRoot.kind === "nested" ? join5(root, skillRoot.relPath, slug, "SKILL.md") : join5(root, slug, "SKILL.md");
17187
+ if (existsSync6(candidate)) {
17091
17188
  return normalizeRelPath(relative3(root, candidate));
17092
17189
  }
17093
17190
  }
17094
17191
  return null;
17095
17192
  }
17096
17193
  function isSkillPath(relPath, index) {
17194
+ return skillSlugForPath(relPath, index) !== null;
17195
+ }
17196
+ function skillSlugForPath(relPath, index) {
17097
17197
  const normalized = normalizeRelPath(relPath);
17098
- if (normalized.endsWith("/SKILL.md"))
17099
- return true;
17198
+ const flat = new Set(index.flatSlugs);
17100
17199
  for (const skillRoot of index.roots) {
17101
- const prefix = skillRoot.kind === "nested" ? `${skillRoot.relPath}/` : "";
17102
- if (prefix && normalized.startsWith(prefix))
17103
- return true;
17104
- if (skillRoot.kind === "flat") {
17105
- const first = normalized.split("/")[0];
17106
- if (first && index.slugs.includes(first))
17107
- return true;
17200
+ if (skillRoot.kind === "nested") {
17201
+ const prefix = `${skillRoot.relPath}/`;
17202
+ if (!normalized.startsWith(prefix))
17203
+ continue;
17204
+ const slug = normalized.slice(prefix.length).split("/")[0];
17205
+ if (slug && index.slugs.includes(slug))
17206
+ return slug;
17207
+ continue;
17108
17208
  }
17209
+ const first = normalized.split("/")[0];
17210
+ if (first && flat.has(first))
17211
+ return first;
17109
17212
  }
17110
- return false;
17213
+ return null;
17214
+ }
17215
+ function isForeignSkillPath(relPath, index) {
17216
+ const slug = skillSlugForPath(relPath, index);
17217
+ if (!slug)
17218
+ return false;
17219
+ return isForeignSkillSlug(index, slug);
17220
+ }
17221
+ var NESTED_SKILL_SLUG_RE = /(?:^|\/)\.(?:claude|agents)\/skills\/([a-z0-9-]+)\//;
17222
+ var FLAT_SKILL_REFERENCES_RE = /(?:^|\/)([a-z0-9-]+)\/references(?:\/|$)/;
17223
+ function slugFromSkillPath(relPath) {
17224
+ const normalized = normalizeRelPath(relPath);
17225
+ const nested = normalized.match(NESTED_SKILL_SLUG_RE);
17226
+ if (nested?.[1])
17227
+ return nested[1];
17228
+ if (normalized.endsWith("/SKILL.md")) {
17229
+ const parts = normalized.split("/");
17230
+ return parts.at(-2) ?? null;
17231
+ }
17232
+ return null;
17233
+ }
17234
+ function slugFromPath(filePath, workspaceRoot) {
17235
+ const normalized = normalizeRelPath(filePath);
17236
+ const nested = normalized.match(NESTED_SKILL_SLUG_RE);
17237
+ if (nested?.[1])
17238
+ return nested[1];
17239
+ if (normalized.endsWith("/SKILL.md")) {
17240
+ return slugFromSkillPath(normalized);
17241
+ }
17242
+ const flatRef = normalized.match(FLAT_SKILL_REFERENCES_RE);
17243
+ const flatSlug = flatRef?.[1];
17244
+ if (!flatSlug || FLAT_SKILL_DENYLIST.has(flatSlug))
17245
+ return null;
17246
+ if (workspaceRoot) {
17247
+ if (!existsSync6(join5(workspaceRoot, flatSlug, "SKILL.md")))
17248
+ return null;
17249
+ }
17250
+ return flatSlug;
17111
17251
  }
17112
17252
  function skillCollectAugments(index) {
17253
+ const owned = new Set(index.ownedSlugs);
17113
17254
  const patterns = [];
17114
17255
  for (const skillRoot of index.roots) {
17115
17256
  if (skillRoot.kind === "nested") {
17116
- patterns.push(`${skillRoot.relPath}/**`);
17257
+ for (const slug of index.ownedSlugs) {
17258
+ patterns.push(`${skillRoot.relPath}/${slug}/**`);
17259
+ }
17117
17260
  } else {
17118
- for (const slug of index.slugs) {
17261
+ for (const slug of index.flatSlugs) {
17262
+ if (!owned.has(slug))
17263
+ continue;
17119
17264
  patterns.push(`${slug}/**`);
17120
17265
  }
17121
17266
  }
@@ -17123,12 +17268,15 @@ function skillCollectAugments(index) {
17123
17268
  return patterns;
17124
17269
  }
17125
17270
  function listSkillMarkdownPaths(root, index) {
17271
+ const owned = new Set(index.ownedSlugs);
17126
17272
  const paths = new Set;
17127
17273
  for (const skillRoot of index.roots) {
17128
17274
  const slugs = skillRoot.kind === "nested" ? listNestedSlugs(root, skillRoot.relPath) : listFlatSlugs(root);
17129
17275
  for (const slug of slugs) {
17130
- const absDir = skillRoot.kind === "nested" ? join4(root, skillRoot.relPath, slug) : join4(root, slug);
17131
- if (!existsSync5(absDir))
17276
+ if (!owned.has(slug))
17277
+ continue;
17278
+ const absDir = skillRoot.kind === "nested" ? join5(root, skillRoot.relPath, slug) : join5(root, slug);
17279
+ if (!existsSync6(absDir))
17132
17280
  continue;
17133
17281
  for (const abs of globSync("**/*.{md,mdc}", {
17134
17282
  cwd: absDir,
@@ -17145,6 +17293,9 @@ function listSkillMarkdownPaths(root, index) {
17145
17293
  function listSkillSlugs(index) {
17146
17294
  return index.slugs;
17147
17295
  }
17296
+ function listOwnedSkillSlugs(index) {
17297
+ return index.ownedSlugs;
17298
+ }
17148
17299
 
17149
17300
  // src/audit/core/collect.ts
17150
17301
  var MARKDOWN_GLOBS = ["**/*.md", "**/*.mdc"];
@@ -17156,7 +17307,7 @@ function shouldExclude(relPath, exclude) {
17156
17307
  return exclude.some((pattern) => matchesGlobScope(relPath, pattern));
17157
17308
  }
17158
17309
  function expandPatterns(root, patterns, exclude) {
17159
- const files = new Set;
17310
+ const byReal = new Map;
17160
17311
  for (const pattern of patterns) {
17161
17312
  for (const abs of globSync(pattern, {
17162
17313
  cwd: root,
@@ -17169,10 +17320,19 @@ function expandPatterns(root, patterns, exclude) {
17169
17320
  const rel = normalizeRelPath(relative4(root, abs));
17170
17321
  if (shouldExclude(rel, exclude))
17171
17322
  continue;
17172
- files.add(abs);
17323
+ let real;
17324
+ try {
17325
+ real = realpathSync4(abs);
17326
+ } catch {
17327
+ real = abs;
17328
+ }
17329
+ const existing = byReal.get(real);
17330
+ if (existing === undefined || abs === real && existing !== real) {
17331
+ byReal.set(real, abs);
17332
+ }
17173
17333
  }
17174
17334
  }
17175
- return [...files];
17335
+ return [...byReal.values()];
17176
17336
  }
17177
17337
  function collectScanFiles(config, root, skillIndex) {
17178
17338
  const exclude = mergedExcludes(config);
@@ -17180,7 +17340,13 @@ function collectScanFiles(config, root, skillIndex) {
17180
17340
  if (skillIndex) {
17181
17341
  includePatterns.push(...skillCollectAugments(skillIndex));
17182
17342
  }
17183
- return expandPatterns(root, includePatterns, exclude);
17343
+ const files = expandPatterns(root, includePatterns, exclude);
17344
+ if (!skillIndex)
17345
+ return files;
17346
+ return files.filter((abs) => {
17347
+ const rel = normalizeRelPath(relative4(root, abs));
17348
+ return !isForeignSkillPath(rel, skillIndex);
17349
+ });
17184
17350
  }
17185
17351
  function collectBannedFiles(config, root) {
17186
17352
  if (config.scan.banned.length === 0)
@@ -17226,19 +17392,19 @@ function collectDocMetaPaths(config, root, registryPaths, skillIndex) {
17226
17392
  }
17227
17393
  const extras = ["docs/README.md", ".skeleton/registry.md"];
17228
17394
  for (const file of extras) {
17229
- const abs = join5(root, file);
17230
- if (existsSync6(abs))
17395
+ const abs = join6(root, file);
17396
+ if (existsSync7(abs))
17231
17397
  paths.push(normalizeRelPath(file));
17232
17398
  }
17233
17399
  for (const rel of registryPaths) {
17234
17400
  if (!rel.endsWith(".md") && !rel.endsWith(".mdc"))
17235
17401
  continue;
17236
- const abs = join5(root, rel);
17237
- if (existsSync6(abs))
17402
+ const abs = join6(root, rel);
17403
+ if (existsSync7(abs))
17238
17404
  paths.push(normalizeRelPath(rel));
17239
17405
  }
17240
17406
  for (const abs of collectScanFiles(config, root, skillIndex)) {
17241
- const content = readFileSync4(abs, "utf8");
17407
+ const content = readFileSync5(abs, "utf8");
17242
17408
  if (/<!--\s*doc-meta:/.test(content)) {
17243
17409
  paths.push(normalizeRelPath(relative4(root, abs)));
17244
17410
  }
@@ -17248,7 +17414,7 @@ function collectDocMetaPaths(config, root, registryPaths, skillIndex) {
17248
17414
  function validateScanRoots(config, root) {
17249
17415
  const missing = [];
17250
17416
  for (const tree of extractScanRootsFromInclude(config.scan.include)) {
17251
- if (!existsSync6(join5(root, tree)))
17417
+ if (!existsSync7(join6(root, tree)))
17252
17418
  missing.push(tree);
17253
17419
  }
17254
17420
  return missing;
@@ -17270,8 +17436,8 @@ function includeExplicitMarkdownPaths(files, paths, root) {
17270
17436
  const out = new Set(files);
17271
17437
  for (const raw of paths) {
17272
17438
  const rel = normalizeRelPath(raw);
17273
- const abs = join5(root, rel);
17274
- if (!existsSync6(abs))
17439
+ const abs = join6(root, rel);
17440
+ if (!existsSync7(abs))
17275
17441
  continue;
17276
17442
  if (isMarkdownFile(rel)) {
17277
17443
  out.add(abs);
@@ -17295,23 +17461,23 @@ function includeExplicitMarkdownPaths(files, paths, root) {
17295
17461
  return [...out];
17296
17462
  }
17297
17463
  function readFileContent(absPath) {
17298
- return readFileSync4(absPath, "utf8");
17464
+ return readFileSync5(absPath, "utf8");
17299
17465
  }
17300
17466
  function relPath(absPath, root) {
17301
17467
  return normalizeRelPath(relative4(root, absPath));
17302
17468
  }
17303
17469
 
17304
17470
  // src/audit/core/registry.ts
17305
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
17306
- import { join as join6, relative as relative5, resolve as resolve4 } from "node:path";
17471
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
17472
+ import { join as join7, relative as relative5, resolve as resolve4 } from "node:path";
17307
17473
  var REGISTRY_TABLE_ROW_RE = /^\|\s*[^|]+\|\s*\[[^\]]*\]\(([^)]+)\)\s*\|/;
17308
17474
  var REGISTRY_TABLE_HEADER_RE = /\|\s*Topic\s*\|\s*Canonical file\s*\|/i;
17309
17475
  function parseRegistry(root) {
17310
- const abs = join6(root, REGISTRY_REL_PATH);
17311
- if (!existsSync7(abs)) {
17476
+ const abs = join7(root, REGISTRY_REL_PATH);
17477
+ if (!existsSync8(abs)) {
17312
17478
  return { paths: [], hasTableHeader: false };
17313
17479
  }
17314
- const content = readFileSync5(abs, "utf8");
17480
+ const content = readFileSync6(abs, "utf8");
17315
17481
  const hasTableHeader = REGISTRY_TABLE_HEADER_RE.test(content);
17316
17482
  const paths = [];
17317
17483
  for (const line of content.split(`
@@ -17322,7 +17488,7 @@ function parseRegistry(root) {
17322
17488
  const linkTarget = match[1].trim();
17323
17489
  if (linkTarget.startsWith("#"))
17324
17490
  continue;
17325
- const resolved = resolve4(join6(root, REGISTRY_DIR_REL), linkTarget);
17491
+ const resolved = resolve4(join7(root, REGISTRY_DIR_REL), linkTarget);
17326
17492
  paths.push(normalizeRelPath(relative5(root, resolved)));
17327
17493
  }
17328
17494
  return { paths: [...new Set(paths)], hasTableHeader };
@@ -17335,7 +17501,7 @@ function parseRegistryPaths(root) {
17335
17501
  function createContext(options = {}) {
17336
17502
  const root = options.root ?? findRepoRoot();
17337
17503
  const config = loadConfig(root);
17338
- const skillIndex = buildSkillIndex(root);
17504
+ const skillIndex = buildSkillIndex(root, config.skillOwnership);
17339
17505
  let files = collectScanFiles(config, root, skillIndex);
17340
17506
  if (options.includeExcludedSkillTrees) {
17341
17507
  files = includeExplicitMarkdownPaths(files, listSkillMarkdownPaths(root, skillIndex), root);
@@ -17355,16 +17521,17 @@ function createContext(options = {}) {
17355
17521
  registryHasTableHeader: registry.hasTableHeader,
17356
17522
  retiredSkills: new Set(retiredSkills(config)),
17357
17523
  skillIndex,
17524
+ lockedSkillSlugs: new Set(skillIndex.foreignSlugs),
17358
17525
  policies: options.policies ?? []
17359
17526
  };
17360
17527
  }
17361
17528
 
17362
17529
  // src/audit/core/fix.ts
17363
- import { existsSync as existsSync10, realpathSync as realpathSync4, writeFileSync } from "node:fs";
17530
+ import { existsSync as existsSync11, realpathSync as realpathSync5, writeFileSync } from "node:fs";
17364
17531
  import { dirname as dirname6, resolve as resolve6, sep as sep3 } from "node:path";
17365
17532
 
17366
17533
  // src/audit/fix/anchors.ts
17367
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
17534
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
17368
17535
  import { dirname as dirname5, resolve as resolve5 } from "node:path";
17369
17536
 
17370
17537
  // node_modules/github-slugger/regex.js
@@ -25545,7 +25712,7 @@ var handle = {
25545
25712
  };
25546
25713
 
25547
25714
  // node_modules/mdast-util-to-markdown/lib/join.js
25548
- var join7 = [joinDefaults];
25715
+ var join8 = [joinDefaults];
25549
25716
  function joinDefaults(left, right, parent, state) {
25550
25717
  if (right.type === "code" && formatCodeAsIndented(right, state) && (left.type === "list" || left.type === right.type && formatCodeAsIndented(left, state))) {
25551
25718
  return false;
@@ -25928,7 +26095,7 @@ function toMarkdown(tree, options) {
25928
26095
  handle: undefined,
25929
26096
  indentLines,
25930
26097
  indexStack: [],
25931
- join: [...join7],
26098
+ join: [...join8],
25932
26099
  options: {},
25933
26100
  safe: safeBound,
25934
26101
  stack: [],
@@ -28918,9 +29085,9 @@ function collectAnchorFixes(ctx) {
28918
29085
  if (!anchor)
28919
29086
  continue;
28920
29087
  const resolved = resolveLink(filePath, target);
28921
- if (!existsSync8(resolved))
29088
+ if (!existsSync9(resolved))
28922
29089
  continue;
28923
- const targetContent = readFileSync6(resolved, "utf8");
29090
+ const targetContent = readFileSync7(resolved, "utf8");
28924
29091
  const slugs = extractHeadingSlugs(targetContent, resolved);
28925
29092
  const anchorSlug = slugifyAnchor(anchor);
28926
29093
  if (slugs.has(anchorSlug))
@@ -28969,8 +29136,8 @@ function collectAnchorFixes(ctx) {
28969
29136
  }
28970
29137
 
28971
29138
  // src/audit/fix/doc-meta.ts
28972
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
28973
- import { join as join8 } from "node:path";
29139
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
29140
+ import { join as join9 } from "node:path";
28974
29141
 
28975
29142
  // src/audit/core/git-meta.ts
28976
29143
  import { spawnSync } from "node:child_process";
@@ -29001,10 +29168,10 @@ function bumpDocMetaLastReviewed(content3, gitDate) {
29001
29168
  function collectDocMetaFixes(ctx) {
29002
29169
  const edits = [];
29003
29170
  for (const relPath2 of ctx.docMetaPaths) {
29004
- const abs = join8(ctx.root, relPath2);
29005
- if (!existsSync9(abs))
29171
+ const abs = join9(ctx.root, relPath2);
29172
+ if (!existsSync10(abs))
29006
29173
  continue;
29007
- const content3 = readFileSync7(abs, "utf8");
29174
+ const content3 = readFileSync8(abs, "utf8");
29008
29175
  if (!DOC_META_RE.test(content3))
29009
29176
  continue;
29010
29177
  const reviewedStr = docMetaLastReviewed(content3);
@@ -29071,11 +29238,11 @@ function resolveWritePath(root2, relFile) {
29071
29238
  if (!underRoot(rootResolved, abs)) {
29072
29239
  throw new Error(`Refusing autofix outside repo root: ${relFile}`);
29073
29240
  }
29074
- const rootReal = existsSync10(rootResolved) ? realpathSync4(rootResolved) : rootResolved;
29241
+ const rootReal = existsSync11(rootResolved) ? realpathSync5(rootResolved) : rootResolved;
29075
29242
  let cursor = abs;
29076
29243
  while (true) {
29077
- if (existsSync10(cursor)) {
29078
- const real = realpathSync4(cursor);
29244
+ if (existsSync11(cursor)) {
29245
+ const real = realpathSync5(cursor);
29079
29246
  if (!underRoot(rootReal, real)) {
29080
29247
  throw new Error(`Refusing autofix outside repo root: ${relFile}`);
29081
29248
  }
@@ -29184,8 +29351,8 @@ function printReport(issues, options) {
29184
29351
  }
29185
29352
 
29186
29353
  // src/references/check.ts
29187
- import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
29188
- import { join as join10, relative as relative7 } from "node:path";
29354
+ import { existsSync as existsSync13, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
29355
+ import { join as join11, relative as relative7 } from "node:path";
29189
29356
 
29190
29357
  // src/references/constants.ts
29191
29358
  var CANONICAL_REFS_DIR = ".skeleton/references";
@@ -29208,16 +29375,16 @@ function isGeneratedReference(content3) {
29208
29375
  }
29209
29376
 
29210
29377
  // src/references/discover.ts
29211
- import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "node:fs";
29212
- import { join as join9, relative as relative6 } from "node:path";
29378
+ import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
29379
+ import { join as join10, relative as relative6 } from "node:path";
29213
29380
  function walkMarkdownFiles(dir, root2) {
29214
29381
  const files = [];
29215
- if (!existsSync11(dir))
29382
+ if (!existsSync12(dir))
29216
29383
  return files;
29217
29384
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
29218
29385
  if (entry.name.startsWith("."))
29219
29386
  continue;
29220
- const fullPath = join9(dir, entry.name);
29387
+ const fullPath = join10(dir, entry.name);
29221
29388
  if (entry.isDirectory()) {
29222
29389
  files.push(...walkMarkdownFiles(fullPath, root2));
29223
29390
  continue;
@@ -29229,7 +29396,7 @@ function walkMarkdownFiles(dir, root2) {
29229
29396
  return files;
29230
29397
  }
29231
29398
  function canonicalExists(root2, refPath) {
29232
- return existsSync11(join9(root2, CANONICAL_REFS_DIR, refPath));
29399
+ return existsSync12(join10(root2, CANONICAL_REFS_DIR, refPath));
29233
29400
  }
29234
29401
  function findSharedRefLinks(content3, sourceFile) {
29235
29402
  const links = [];
@@ -29260,7 +29427,7 @@ function findLocalCanonicalLinks(root2, content3, sourceFile) {
29260
29427
  const raw = normalizeRelPath(match[1] ?? "");
29261
29428
  if (!raw)
29262
29429
  continue;
29263
- const refPath = withinDir ? normalizeRelPath(join9(withinDir, raw)) : raw;
29430
+ const refPath = withinDir ? normalizeRelPath(join10(withinDir, raw)) : raw;
29264
29431
  if (!canonicalExists(root2, refPath))
29265
29432
  continue;
29266
29433
  links.push({ refPath, sourceFile });
@@ -29268,17 +29435,17 @@ function findLocalCanonicalLinks(root2, content3, sourceFile) {
29268
29435
  }
29269
29436
  return links;
29270
29437
  }
29271
- function discoverSkillReferencePlans(root2) {
29272
- const index2 = buildSkillIndex(root2);
29438
+ function discoverSkillReferencePlans(root2, ownership) {
29439
+ const index2 = buildSkillIndex(root2, ownership);
29273
29440
  const plans = [];
29274
- for (const slug2 of index2.slugs) {
29275
- const skillDir = join9(root2, slug2);
29276
- if (!existsSync11(join9(skillDir, "SKILL.md")))
29441
+ for (const slug2 of index2.ownedSlugs) {
29442
+ const skillDir = join10(root2, slug2);
29443
+ if (!existsSync12(join10(skillDir, "SKILL.md")))
29277
29444
  continue;
29278
29445
  const refPaths = new Set;
29279
29446
  const links = [];
29280
29447
  for (const relFile of walkMarkdownFiles(skillDir, root2)) {
29281
- const content3 = readFileSync8(join9(root2, relFile), "utf8");
29448
+ const content3 = readFileSync9(join10(root2, relFile), "utf8");
29282
29449
  if (isGeneratedReference(content3))
29283
29450
  continue;
29284
29451
  for (const link2 of findSharedRefLinks(content3, relFile)) {
@@ -29295,7 +29462,7 @@ function discoverSkillReferencePlans(root2) {
29295
29462
  const refPath = queue.pop();
29296
29463
  if (!refPath || !canonicalExists(root2, refPath))
29297
29464
  continue;
29298
- const canonicalContent = readFileSync8(join9(root2, CANONICAL_REFS_DIR, refPath), "utf8");
29465
+ const canonicalContent = readFileSync9(join10(root2, CANONICAL_REFS_DIR, refPath), "utf8");
29299
29466
  const syntheticSource = generatedRefPath(slug2, refPath);
29300
29467
  for (const link2 of findLocalCanonicalLinks(root2, canonicalContent, syntheticSource)) {
29301
29468
  if (refPaths.has(link2.refPath))
@@ -29312,7 +29479,7 @@ function discoverSkillReferencePlans(root2) {
29312
29479
  return plans.sort((a, b) => a.skill.localeCompare(b.skill));
29313
29480
  }
29314
29481
  function generatedRefPath(skill, refPath) {
29315
- return normalizeRelPath(join9(skill, "references", refPath));
29482
+ return normalizeRelPath(join10(skill, "references", refPath));
29316
29483
  }
29317
29484
  function rewriteSharedRefTarget(sourceFile, skill, refPath) {
29318
29485
  const sourceDir = sourceFile.slice(0, sourceFile.lastIndexOf("/"));
@@ -29341,19 +29508,19 @@ function rewriteSharedRefLinks(content3, sourceFile, skill) {
29341
29508
  function listAllGeneratedFiles(root2) {
29342
29509
  const files = [];
29343
29510
  const walk = (dir) => {
29344
- if (!existsSync12(dir))
29511
+ if (!existsSync13(dir))
29345
29512
  return;
29346
29513
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29347
29514
  if (entry.name.startsWith("."))
29348
29515
  continue;
29349
- const fullPath = join10(dir, entry.name);
29516
+ const fullPath = join11(dir, entry.name);
29350
29517
  if (entry.isDirectory()) {
29351
29518
  walk(fullPath);
29352
29519
  continue;
29353
29520
  }
29354
29521
  if (!entry.name.endsWith(".md"))
29355
29522
  continue;
29356
- const content3 = readFileSync9(fullPath, "utf8");
29523
+ const content3 = readFileSync10(fullPath, "utf8");
29357
29524
  if (isGeneratedReference(content3)) {
29358
29525
  files.push(normalizeRelPath(relative7(root2, fullPath)));
29359
29526
  }
@@ -29362,12 +29529,13 @@ function listAllGeneratedFiles(root2) {
29362
29529
  walk(root2);
29363
29530
  return files;
29364
29531
  }
29365
- function runGeneratedReferencesCheck(root2) {
29532
+ function runGeneratedReferencesCheck(root2, ownership) {
29366
29533
  const issues = [];
29367
- const canonicalDir = join10(root2, CANONICAL_REFS_DIR);
29368
- if (!existsSync12(canonicalDir))
29534
+ const canonicalDir = join11(root2, CANONICAL_REFS_DIR);
29535
+ if (!existsSync13(canonicalDir))
29369
29536
  return issues;
29370
- const plans = discoverSkillReferencePlans(root2);
29537
+ const skillIndex = buildSkillIndex(root2, ownership);
29538
+ const plans = discoverSkillReferencePlans(root2, ownership);
29371
29539
  const needed = new Set;
29372
29540
  for (const plan of plans) {
29373
29541
  for (const refPath of plan.refPaths) {
@@ -29375,42 +29543,44 @@ function runGeneratedReferencesCheck(root2) {
29375
29543
  }
29376
29544
  }
29377
29545
  for (const targetRel of needed) {
29378
- const targetPath = join10(root2, targetRel);
29379
- if (!existsSync12(targetPath)) {
29546
+ const targetPath = join11(root2, targetRel);
29547
+ if (!existsSync13(targetPath)) {
29380
29548
  issues.push(issue("generated-references", targetRel, "missing generated copy — run skeleton references sync"));
29381
29549
  continue;
29382
29550
  }
29383
- const generated = readFileSync9(targetPath, "utf8");
29551
+ const generated = readFileSync10(targetPath, "utf8");
29384
29552
  if (!isGeneratedReference(generated)) {
29385
29553
  issues.push(issue("generated-references", targetRel, "expected generated-reference provenance header"));
29386
29554
  continue;
29387
29555
  }
29388
29556
  const body = stripGeneratedHeader(generated);
29389
- const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join10(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
29390
- const canonicalPath = join10(root2, sourceRel);
29391
- if (!existsSync12(canonicalPath)) {
29557
+ const sourceRel = normalizeRelPath(generated.match(/source: ([^\n]+)/)?.[1] ?? join11(CANONICAL_REFS_DIR, targetRel.split("/references/")[1] ?? ""));
29558
+ const canonicalPath = join11(root2, sourceRel);
29559
+ if (!existsSync13(canonicalPath)) {
29392
29560
  issues.push(issue("generated-references", targetRel, `canonical source missing: ${sourceRel}`));
29393
29561
  continue;
29394
29562
  }
29395
- const canonical = readFileSync9(canonicalPath, "utf8");
29563
+ const canonical = readFileSync10(canonicalPath, "utf8");
29396
29564
  if (body !== canonical) {
29397
29565
  issues.push(issue("generated-references", targetRel, "stale generated copy — run skeleton references sync"));
29398
29566
  }
29399
29567
  }
29400
29568
  for (const generatedRel of listAllGeneratedFiles(root2)) {
29569
+ if (isForeignSkillPath(generatedRel, skillIndex))
29570
+ continue;
29401
29571
  if (!needed.has(generatedRel)) {
29402
29572
  issues.push(issue("generated-references", generatedRel, "orphaned generated copy — run skeleton references sync"));
29403
29573
  }
29404
29574
  }
29405
29575
  for (const plan of plans) {
29406
- const skillDir = join10(root2, plan.skill);
29407
- if (!existsSync12(skillDir))
29576
+ const skillDir = join11(root2, plan.skill);
29577
+ if (!existsSync13(skillDir))
29408
29578
  continue;
29409
29579
  const walk = (dir) => {
29410
29580
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
29411
29581
  if (entry.name.startsWith("."))
29412
29582
  continue;
29413
- const fullPath = join10(dir, entry.name);
29583
+ const fullPath = join11(dir, entry.name);
29414
29584
  if (entry.isDirectory()) {
29415
29585
  walk(fullPath);
29416
29586
  continue;
@@ -29418,7 +29588,7 @@ function runGeneratedReferencesCheck(root2) {
29418
29588
  if (!entry.name.endsWith(".md"))
29419
29589
  continue;
29420
29590
  const relFile = normalizeRelPath(relative7(root2, fullPath));
29421
- const content3 = readFileSync9(fullPath, "utf8");
29591
+ const content3 = readFileSync10(fullPath, "utf8");
29422
29592
  if (content3.match(SHARED_REF_LINK_RE)) {
29423
29593
  issues.push(issue("generated-references", relFile, "still links to shared root references/ — run skeleton references sync"));
29424
29594
  }
@@ -29429,7 +29599,7 @@ function runGeneratedReferencesCheck(root2) {
29429
29599
  return issues;
29430
29600
  }
29431
29601
  function runGeneratedReferencesRule(ctx) {
29432
- return runGeneratedReferencesCheck(ctx.root);
29602
+ return runGeneratedReferencesCheck(ctx.root, ctx.config.skillOwnership);
29433
29603
  }
29434
29604
  var generatedReferencesRule = {
29435
29605
  id: "generated-references",
@@ -29449,16 +29619,16 @@ function runBannedRule(ctx) {
29449
29619
  var bannedRule = { id: "banned", run: runBannedRule };
29450
29620
 
29451
29621
  // src/audit/rules/doc-meta.ts
29452
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
29453
- import { join as join11 } from "node:path";
29622
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
29623
+ import { join as join12 } from "node:path";
29454
29624
  function runDocMetaRule(ctx) {
29455
29625
  const issues = [];
29456
29626
  const today = new Date;
29457
29627
  for (const relPath2 of ctx.docMetaPaths) {
29458
- const abs = join11(ctx.root, relPath2);
29459
- if (!existsSync13(abs))
29628
+ const abs = join12(ctx.root, relPath2);
29629
+ if (!existsSync14(abs))
29460
29630
  continue;
29461
- const content3 = readFileSync10(abs, "utf8");
29631
+ const content3 = readFileSync11(abs, "utf8");
29462
29632
  if (!DOC_META_RE.test(content3)) {
29463
29633
  issues.push(issue("doc-meta", relPath2, "missing doc-meta comment (owner + last-reviewed)"));
29464
29634
  continue;
@@ -29473,6 +29643,9 @@ function runDocMetaRule(ctx) {
29473
29643
  if (ageDays > ctx.config.daysUntilStale) {
29474
29644
  issues.push(issue("doc-meta", relPath2, `doc-meta last-reviewed ${reviewedStr} is stale (>${ctx.config.daysUntilStale} days)`, { severity: "warning" }));
29475
29645
  }
29646
+ const slug2 = slugFromPath(relPath2, ctx.root);
29647
+ if (slug2 !== null && ctx.lockedSkillSlugs.has(slug2))
29648
+ continue;
29476
29649
  const gitDate = lastGitCommitDate(relPath2, ctx.root);
29477
29650
  if (!gitDate)
29478
29651
  continue;
@@ -29488,7 +29661,7 @@ function runDocMetaRule(ctx) {
29488
29661
  var docMetaRule = { id: "doc-meta", run: runDocMetaRule };
29489
29662
 
29490
29663
  // src/audit/rules/links.ts
29491
- import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
29664
+ import { existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
29492
29665
  import { dirname as dirname7, resolve as resolve7 } from "node:path";
29493
29666
  function resolveLink2(sourceFile, target) {
29494
29667
  const withoutAnchor = target.split("#")[0]?.split("?")[0] ?? "";
@@ -29525,19 +29698,19 @@ function validateTarget(ctx, sourceFile, target, linkLabel) {
29525
29698
  }
29526
29699
  if ((target.includes(".claude/agents/") || target.includes(".cursor/agents/")) && target.endsWith(".md")) {
29527
29700
  const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
29528
- if (!existsSync14(agentPath)) {
29701
+ if (!existsSync15(agentPath)) {
29529
29702
  issues.push(issue("links", relSource, "missing agent file", { link: linkLabel }));
29530
29703
  }
29531
29704
  return issues;
29532
29705
  }
29533
- if (pathPart && !existsSync14(resolved)) {
29706
+ if (pathPart && !existsSync15(resolved)) {
29534
29707
  issues.push(issue("links", relSource, `broken link → ${relTarget}`, {
29535
29708
  link: linkLabel
29536
29709
  }));
29537
29710
  return issues;
29538
29711
  }
29539
- if (anchor && existsSync14(resolved)) {
29540
- const targetContent = readFileSync11(resolved, "utf8");
29712
+ if (anchor && existsSync15(resolved)) {
29713
+ const targetContent = readFileSync12(resolved, "utf8");
29541
29714
  const slugs = extractHeadingSlugs(targetContent, resolved);
29542
29715
  const anchorSlug = slugifyAnchor(anchor);
29543
29716
  if (!slugs.has(anchorSlug)) {
@@ -29612,8 +29785,8 @@ function runProsePolicyRule(ctx) {
29612
29785
  var prosePolicyRule = { id: "prose-policy", run: runProsePolicyRule };
29613
29786
 
29614
29787
  // src/audit/rules/registry.ts
29615
- import { existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
29616
- import { join as join12 } from "node:path";
29788
+ import { existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
29789
+ import { join as join13 } from "node:path";
29617
29790
  function runRegistryRule(ctx) {
29618
29791
  const issues = [];
29619
29792
  const registry = new Set(ctx.registryPaths);
@@ -29621,12 +29794,12 @@ function runRegistryRule(ctx) {
29621
29794
  issues.push(issue("registry", REGISTRY_REL_PATH, "registry table header found but 0 rows parsed — check | Topic | Canonical file | format and link syntax"));
29622
29795
  }
29623
29796
  for (const rel of ctx.registryPaths) {
29624
- const abs = join12(ctx.root, rel);
29625
- if (!existsSync15(abs)) {
29797
+ const abs = join13(ctx.root, rel);
29798
+ if (!existsSync16(abs)) {
29626
29799
  issues.push(issue("registry", rel, "registry entry file missing"));
29627
29800
  continue;
29628
29801
  }
29629
- const content3 = readFileSync12(abs, "utf8");
29802
+ const content3 = readFileSync13(abs, "utf8");
29630
29803
  if (!SOURCE_OF_TRUTH_BANNER_RE.test(content3)) {
29631
29804
  issues.push(issue("registry", rel, "missing **Source of truth for** banner (required for registry entry)"));
29632
29805
  }
@@ -29637,7 +29810,7 @@ function runRegistryRule(ctx) {
29637
29810
  continue;
29638
29811
  if (!rel.endsWith(".md") && !rel.endsWith(".mdc"))
29639
29812
  continue;
29640
- const content3 = readFileSync12(filePath, "utf8");
29813
+ const content3 = readFileSync13(filePath, "utf8");
29641
29814
  if (!SOURCE_OF_TRUTH_BANNER_LINE_RE.test(content3))
29642
29815
  continue;
29643
29816
  if (!registry.has(rel)) {
@@ -29674,16 +29847,16 @@ function runScanRootsRule(ctx) {
29674
29847
  var scanRootsRule = { id: "scan-roots", run: runScanRootsRule };
29675
29848
 
29676
29849
  // src/audit/rules/skill-index.ts
29677
- import { existsSync as existsSync16, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
29678
- import { join as join13, relative as relative8 } from "node:path";
29850
+ import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync14 } from "node:fs";
29851
+ import { join as join14, relative as relative8 } from "node:path";
29679
29852
  function walkSkillMarkdown(dir) {
29680
29853
  const files = [];
29681
- if (!existsSync16(dir))
29854
+ if (!existsSync17(dir))
29682
29855
  return files;
29683
29856
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
29684
29857
  if (entry.name.startsWith("."))
29685
29858
  continue;
29686
- const fullPath = join13(dir, entry.name);
29859
+ const fullPath = join14(dir, entry.name);
29687
29860
  if (entry.isDirectory()) {
29688
29861
  files.push(...walkSkillMarkdown(fullPath));
29689
29862
  continue;
@@ -29710,7 +29883,7 @@ function parseReadmeTaxonomySlugs(content3) {
29710
29883
  function scanFileForSkillLinks(ctx, filePath, index2) {
29711
29884
  const issues = [];
29712
29885
  const rel = relative8(ctx.root, filePath).replace(/\\/g, "/");
29713
- const content3 = readFileSync13(filePath, "utf8");
29886
+ const content3 = readFileSync14(filePath, "utf8");
29714
29887
  if (isGeneratedReference(content3))
29715
29888
  return issues;
29716
29889
  for (const match of content3.matchAll(SKILL_LINK_RE)) {
@@ -29733,15 +29906,16 @@ function validateReadmeTaxonomy(ctx, index2, diskSlugs) {
29733
29906
  for (const skillRoot of index2.roots) {
29734
29907
  if (skillRoot.kind !== "nested")
29735
29908
  continue;
29736
- const readmePath = join13(ctx.root, skillRoot.relPath, "README.md");
29737
- if (!existsSync16(readmePath))
29909
+ const readmePath = join14(ctx.root, skillRoot.relPath, "README.md");
29910
+ if (!existsSync17(readmePath))
29738
29911
  continue;
29739
- const readme = readFileSync13(readmePath, "utf8");
29912
+ const readme = readFileSync14(readmePath, "utf8");
29740
29913
  if (!readme.includes("## Taxonomy"))
29741
29914
  continue;
29742
29915
  const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
29743
- const nestedSlugs = diskSlugs.filter((slug2) => existsSync16(join13(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
29744
- const publicSlugs = nestedSlugs.filter((slug2) => !nonPublic.has(slug2));
29916
+ const nestedSlugs = diskSlugs.filter((slug2) => existsSync17(join14(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
29917
+ const foreign = new Set(index2.foreignSlugs);
29918
+ const publicSlugs = nestedSlugs.filter((slug2) => !nonPublic.has(slug2) && !foreign.has(slug2));
29745
29919
  const relReadme = `${skillRoot.relPath}/README.md`;
29746
29920
  for (const slug2 of publicSlugs) {
29747
29921
  if (!taxonomySlugs.includes(slug2)) {
@@ -29760,30 +29934,45 @@ function runSkillIndexRule(ctx) {
29760
29934
  const issues = [];
29761
29935
  const index2 = ctx.skillIndex;
29762
29936
  const diskSlugs = listSkillSlugs(index2);
29937
+ for (const warning of index2.provenance.warnings) {
29938
+ issues.push(issue("skill-index", index2.provenance.lockfile ?? "skills-lock.json", `skill provenance: ${warning}`, {
29939
+ severity: "warning"
29940
+ }));
29941
+ }
29763
29942
  issues.push(...validateReadmeTaxonomy(ctx, index2, diskSlugs));
29943
+ const owned = new Set(index2.ownedSlugs);
29764
29944
  for (const skillRoot of index2.roots) {
29765
- const scanRoot = skillRoot.kind === "nested" ? join13(ctx.root, skillRoot.relPath) : join13(ctx.root);
29766
- if (skillRoot.kind === "nested" && existsSync16(scanRoot)) {
29767
- for (const skillMd of walkSkillMarkdown(scanRoot)) {
29768
- issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
29945
+ if (skillRoot.kind === "nested") {
29946
+ for (const slug2 of index2.ownedSlugs) {
29947
+ const skillDir = join14(ctx.root, skillRoot.relPath, slug2);
29948
+ if (!existsSync17(skillDir))
29949
+ continue;
29950
+ for (const skillMd of walkSkillMarkdown(skillDir)) {
29951
+ issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
29952
+ }
29769
29953
  }
29954
+ continue;
29770
29955
  }
29771
- if (skillRoot.kind === "flat") {
29772
- for (const slug2 of index2.slugs) {
29773
- const skillDir = join13(ctx.root, slug2);
29774
- if (existsSync16(skillDir)) {
29775
- for (const skillMd of walkSkillMarkdown(skillDir)) {
29776
- issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
29777
- }
29778
- }
29956
+ for (const slug2 of index2.flatSlugs) {
29957
+ if (!owned.has(slug2))
29958
+ continue;
29959
+ const skillDir = join14(ctx.root, slug2);
29960
+ if (!existsSync17(skillDir))
29961
+ continue;
29962
+ for (const skillMd of walkSkillMarkdown(skillDir)) {
29963
+ issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
29779
29964
  }
29780
29965
  }
29781
29966
  }
29782
29967
  return issues;
29783
29968
  }
29784
29969
  var skillIndexRule = { id: "skill-index", run: runSkillIndexRule };
29785
- function skillCountOnDisk(ctx) {
29786
- return listSkillSlugs(ctx.skillIndex).length;
29970
+ function skillAuditSuffix(ctx) {
29971
+ const owned = listOwnedSkillSlugs(ctx.skillIndex).length;
29972
+ const foreign = ctx.skillIndex.foreignSlugs.length;
29973
+ if (foreign === 0)
29974
+ return ` (${owned} skills on disk)`;
29975
+ return ` (${owned} owned skills audited, ${foreign} foreign ignored)`;
29787
29976
  }
29788
29977
 
29789
29978
  // src/audit/rules/index.ts
@@ -29952,24 +30141,24 @@ async function runAudit(options) {
29952
30141
  json: options.json,
29953
30142
  label,
29954
30143
  fileCount: options.suite === "docs" || options.suite === "self" ? ctx.files.length : undefined,
29955
- successSuffix: options.suite === "skills" ? ` (${skillCountOnDisk(ctx)} skills on disk)` : undefined
30144
+ successSuffix: options.suite === "skills" ? skillAuditSuffix(ctx) : undefined
29956
30145
  });
29957
30146
  }
29958
30147
 
29959
30148
  // src/customize/resolve.ts
29960
- import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
29961
- import { basename as basename3, join as join14, relative as relative9 } from "node:path";
30149
+ import { existsSync as existsSync18, readFileSync as readFileSync15 } from "node:fs";
30150
+ import { basename as basename3, join as join15, relative as relative9 } from "node:path";
29962
30151
  var CUSTOMIZE_PREFIX = "Customize: ";
29963
30152
  function customizeDir(root2) {
29964
- return join14(root2, REGISTRY_DIR_REL, "customize");
30153
+ return join15(root2, REGISTRY_DIR_REL, "customize");
29965
30154
  }
29966
30155
  function customizePathForSlug(root2, slug2) {
29967
- return join14(customizeDir(root2), `${slug2}.md`);
30156
+ return join15(customizeDir(root2), `${slug2}.md`);
29968
30157
  }
29969
30158
  function findCustomizeViaRegistry(root2, slug2) {
29970
30159
  for (const rel of parseRegistryPaths(root2)) {
29971
30160
  const expected = `${REGISTRY_DIR_REL}/customize/${slug2}.md`;
29972
- if (normalizeRelPath(rel) === expected && existsSync17(join14(root2, rel))) {
30161
+ if (normalizeRelPath(rel) === expected && existsSync18(join15(root2, rel))) {
29973
30162
  return rel;
29974
30163
  }
29975
30164
  }
@@ -29977,17 +30166,17 @@ function findCustomizeViaRegistry(root2, slug2) {
29977
30166
  }
29978
30167
  function resolveSlugFile(root2, slug2) {
29979
30168
  const direct = customizePathForSlug(root2, slug2);
29980
- if (existsSync17(direct)) {
30169
+ if (existsSync18(direct)) {
29981
30170
  return {
29982
- content: readFileSync14(direct, "utf8"),
30171
+ content: readFileSync15(direct, "utf8"),
29983
30172
  path: normalizeRelPath(relative9(root2, direct))
29984
30173
  };
29985
30174
  }
29986
30175
  const registryPath = findCustomizeViaRegistry(root2, slug2);
29987
30176
  if (registryPath) {
29988
- const abs = join14(root2, registryPath);
30177
+ const abs = join15(root2, registryPath);
29989
30178
  return {
29990
- content: readFileSync14(abs, "utf8"),
30179
+ content: readFileSync15(abs, "utf8"),
29991
30180
  path: registryPath
29992
30181
  };
29993
30182
  }
@@ -30009,10 +30198,10 @@ function readAlwaysInclude(root2, basenames, skipBasename) {
30009
30198
  const file = basename3(name);
30010
30199
  if (skipBasename && file === skipBasename)
30011
30200
  continue;
30012
- const abs = join14(dir, file);
30013
- if (!existsSync17(abs))
30201
+ const abs = join15(dir, file);
30202
+ if (!existsSync18(abs))
30014
30203
  continue;
30015
- parts.push(readFileSync14(abs, "utf8").trimEnd());
30204
+ parts.push(readFileSync15(abs, "utf8").trimEnd());
30016
30205
  paths.push(normalizeRelPath(relative9(root2, abs)));
30017
30206
  }
30018
30207
  return { parts, paths };
@@ -30051,61 +30240,151 @@ function resolveCustomizeFromRoot(slug2, startDir) {
30051
30240
  return resolveCustomize(root2, slug2);
30052
30241
  }
30053
30242
 
30243
+ // src/hooks/run.ts
30244
+ function parsePayload(raw) {
30245
+ if (!raw.trim())
30246
+ return {};
30247
+ return JSON.parse(raw);
30248
+ }
30249
+ function extractPath(payload) {
30250
+ const input = payload.tool_input ?? payload.toolInput ?? {};
30251
+ const candidates = [
30252
+ input.path,
30253
+ input.file_path,
30254
+ input.filePath,
30255
+ input.target_file,
30256
+ input.targetFile
30257
+ ];
30258
+ for (const value of candidates) {
30259
+ if (typeof value === "string" && value.trim())
30260
+ return normalizeRelPath(value.trim());
30261
+ }
30262
+ return null;
30263
+ }
30264
+ function extractSkillSlug(payload) {
30265
+ const tool = payload.tool_name ?? payload.toolName ?? "";
30266
+ if (tool === "Skill" || tool === "skill") {
30267
+ const input = payload.tool_input ?? payload.toolInput ?? {};
30268
+ const slug2 = input.skill ?? input.slug ?? input.name;
30269
+ if (typeof slug2 === "string" && slug2.trim())
30270
+ return slug2.trim();
30271
+ }
30272
+ const path2 = extractPath(payload);
30273
+ if (path2)
30274
+ return slugFromPath(path2, process.cwd());
30275
+ return null;
30276
+ }
30277
+ function cursorResponse(content3) {
30278
+ return JSON.stringify({ additional_context: content3 });
30279
+ }
30280
+ function claudeResponse(content3) {
30281
+ return JSON.stringify({
30282
+ hookSpecificOutput: {
30283
+ additionalContext: content3
30284
+ }
30285
+ });
30286
+ }
30287
+ function codexResponse(content3) {
30288
+ return JSON.stringify({ additionalContext: content3 });
30289
+ }
30290
+ function formatResponse(payload, content3) {
30291
+ const tool = payload.tool_name ?? payload.toolName ?? "";
30292
+ if (tool === "read_file")
30293
+ return codexResponse(content3);
30294
+ if (payload.hook_event_name?.toLowerCase().includes("claude"))
30295
+ return claudeResponse(content3);
30296
+ if (tool === "Read" && payload.hook_event_name === "postToolUse")
30297
+ return cursorResponse(content3);
30298
+ if (tool === "Read" || tool === "Skill")
30299
+ return claudeResponse(content3);
30300
+ return cursorResponse(content3);
30301
+ }
30302
+ function runCustomizeHook(raw) {
30303
+ try {
30304
+ const payload = parsePayload(raw);
30305
+ const slug2 = extractSkillSlug(payload);
30306
+ if (!slug2)
30307
+ return "{}";
30308
+ const resolved = resolveCustomizeFromRoot(slug2);
30309
+ if (!resolved.content)
30310
+ return "{}";
30311
+ const from = resolved.included.length > 0 ? resolved.included.join(", ") : resolved.path ?? ".skeleton/customize";
30312
+ const prefix = `
30313
+
30314
+ ---
30315
+ Customize override for /${slug2} (from ${from}):
30316
+
30317
+ `;
30318
+ return formatResponse(payload, prefix + resolved.content);
30319
+ } catch (error) {
30320
+ console.error(`customize hook error: ${error}`);
30321
+ return "{}";
30322
+ }
30323
+ }
30324
+
30054
30325
  // src/init/init.ts
30055
30326
  import { spawnSync as spawnSync2 } from "node:child_process";
30056
- import { copyFileSync, existsSync as existsSync21, mkdirSync as mkdirSync2, readFileSync as readFileSync16 } from "node:fs";
30057
- import { join as join18 } from "node:path";
30327
+ import { copyFileSync, existsSync as existsSync22, mkdirSync as mkdirSync2, readFileSync as readFileSync17 } from "node:fs";
30328
+ import { join as join19 } from "node:path";
30058
30329
 
30059
30330
  // src/init/merge-hooks.ts
30060
- import { existsSync as existsSync20, mkdirSync, readFileSync as readFileSync15, writeFileSync as writeFileSync2 } from "node:fs";
30061
- import { dirname as dirname10, join as join17 } from "node:path";
30331
+ import { existsSync as existsSync21, mkdirSync, readFileSync as readFileSync16, writeFileSync as writeFileSync2 } from "node:fs";
30332
+ import { dirname as dirname10, join as join18 } from "node:path";
30062
30333
 
30063
30334
  // src/init/package-paths.ts
30064
- import { existsSync as existsSync18 } from "node:fs";
30065
- import { dirname as dirname8, join as join15 } from "node:path";
30335
+ import { existsSync as existsSync19 } from "node:fs";
30336
+ import { dirname as dirname8, join as join16 } from "node:path";
30066
30337
  import { fileURLToPath as fileURLToPath5 } from "node:url";
30067
30338
  var MODULE_DIR = dirname8(fileURLToPath5(import.meta.url));
30068
- var PACKAGE_ROOT_CANDIDATES = [join15(MODULE_DIR, "../.."), join15(MODULE_DIR, "..")];
30339
+ var PACKAGE_ROOT_CANDIDATES = [join16(MODULE_DIR, "../.."), join16(MODULE_DIR, "..")];
30069
30340
  function resolvePackageRoot() {
30070
30341
  for (const candidate of PACKAGE_ROOT_CANDIDATES) {
30071
- if (existsSync18(join15(candidate, "package.json")))
30342
+ if (existsSync19(join16(candidate, "package.json")))
30072
30343
  return candidate;
30073
30344
  }
30074
30345
  throw new Error("Could not resolve @csark0812/skeleton package root");
30075
30346
  }
30076
30347
  function resolveTemplatesDir() {
30077
- const dir = join15(resolvePackageRoot(), "templates/skeleton-init");
30078
- if (!existsSync18(dir)) {
30348
+ const dir = join16(resolvePackageRoot(), "templates/skeleton-init");
30349
+ if (!existsSync19(dir)) {
30079
30350
  throw new Error("Missing templates/skeleton-init in package");
30080
30351
  }
30081
30352
  return dir;
30082
30353
  }
30083
30354
 
30084
30355
  // src/init/resolve-hook-command.ts
30085
- import { existsSync as existsSync19 } from "node:fs";
30356
+ import { existsSync as existsSync20, realpathSync as realpathSync6 } from "node:fs";
30086
30357
  import { createRequire as createRequire3 } from "node:module";
30087
- import { dirname as dirname9, join as join16, relative as relative10, resolve as resolve8 } from "node:path";
30358
+ import { dirname as dirname9, join as join17, relative as relative10, resolve as resolve8 } from "node:path";
30088
30359
  var PACKAGE_NAME = "@csark0812/skeleton";
30089
- var HOOK_DIST = "dist/hooks/customize-on-skill-read.js";
30090
- var HOOK_SRC = "src/hooks/customize-on-skill-read.ts";
30360
+ var CLI_DIST = "dist/cli.js";
30091
30361
  var PACKAGE_ROOT = resolvePackageRoot();
30362
+ var DEV_HOOK_COMMAND = "bun src/cli.ts hook customize";
30363
+ var FALLBACK_HOOK_COMMAND = `node node_modules/${PACKAGE_NAME}/${CLI_DIST} hook customize`;
30364
+ function safeRealpath2(path2) {
30365
+ try {
30366
+ return realpathSync6(path2);
30367
+ } catch {
30368
+ return path2;
30369
+ }
30370
+ }
30092
30371
  function toRepoRelative(cwd, absPath) {
30093
- const rel = relative10(cwd, absPath).replace(/\\/g, "/");
30372
+ const rel = relative10(safeRealpath2(cwd), safeRealpath2(absPath)).replace(/\\/g, "/");
30094
30373
  return rel.startsWith("..") ? absPath.replace(/\\/g, "/") : rel;
30095
30374
  }
30096
- function tryResolvePublished(cwd) {
30375
+ function tryResolvePublishedCli(cwd) {
30097
30376
  try {
30098
- const req = createRequire3(join16(cwd, "package.json"));
30099
- return req.resolve(`${PACKAGE_NAME}/${HOOK_DIST}`);
30377
+ const req = createRequire3(join17(cwd, "package.json"));
30378
+ return req.resolve(`${PACKAGE_NAME}/${CLI_DIST}`);
30100
30379
  } catch {
30101
30380
  return null;
30102
30381
  }
30103
30382
  }
30104
- function walkNodeModules(cwd) {
30383
+ function walkNodeModulesCli(cwd) {
30105
30384
  let dir = cwd;
30106
30385
  while (true) {
30107
- const candidate = join16(dir, "node_modules", PACKAGE_NAME, HOOK_DIST);
30108
- if (existsSync19(candidate))
30386
+ const candidate = join17(dir, "node_modules", PACKAGE_NAME, CLI_DIST);
30387
+ if (existsSync20(candidate))
30109
30388
  return candidate;
30110
30389
  const parent = dirname9(dir);
30111
30390
  if (parent === dir)
@@ -30118,28 +30397,25 @@ function isInsidePackageRoot(cwd) {
30118
30397
  const rel = relative10(PACKAGE_ROOT, resolve8(cwd)).replace(/\\/g, "/");
30119
30398
  return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
30120
30399
  }
30400
+ function nodeCliHookCommand(cliPath) {
30401
+ return `node ${cliPath} hook customize`;
30402
+ }
30121
30403
  function resolveHookCommand(cwd) {
30122
- const published = tryResolvePublished(cwd);
30404
+ if (isInsidePackageRoot(cwd))
30405
+ return DEV_HOOK_COMMAND;
30406
+ const published = tryResolvePublishedCli(cwd);
30123
30407
  if (published)
30124
- return toRepoRelative(cwd, published);
30125
- const hoisted = walkNodeModules(cwd);
30408
+ return nodeCliHookCommand(toRepoRelative(cwd, published));
30409
+ const hoisted = walkNodeModulesCli(cwd);
30126
30410
  if (hoisted)
30127
- return toRepoRelative(cwd, hoisted);
30128
- if (isInsidePackageRoot(cwd)) {
30129
- const distHook = join16(PACKAGE_ROOT, HOOK_DIST);
30130
- if (existsSync19(distHook))
30131
- return toRepoRelative(cwd, distHook);
30132
- const srcHook = join16(PACKAGE_ROOT, HOOK_SRC);
30133
- if (existsSync19(srcHook)) {
30134
- const rel = toRepoRelative(cwd, srcHook);
30135
- return rel.includes("/") ? `bun ${rel}` : `bun ./${rel}`;
30136
- }
30137
- }
30138
- return `node node_modules/${PACKAGE_NAME}/${HOOK_DIST}`;
30411
+ return nodeCliHookCommand(toRepoRelative(cwd, hoisted));
30412
+ return FALLBACK_HOOK_COMMAND;
30139
30413
  }
30140
30414
  function isSkeletonHookCommand(command) {
30141
30415
  if (!command)
30142
30416
  return false;
30417
+ if (/\bhook\s+customize\b/.test(command))
30418
+ return true;
30143
30419
  return /customize-on-skill-read\.(js|ts)\b/.test(command);
30144
30420
  }
30145
30421
 
@@ -30149,14 +30425,14 @@ function identityKey(platform, event, matcher) {
30149
30425
  return `skeleton:customize:${platform}:${event}:${matcher}`;
30150
30426
  }
30151
30427
  function loadFragment(name, hookCommand) {
30152
- const raw = readFileSync15(join17(TEMPLATES_DIR, name), "utf8");
30428
+ const raw = readFileSync16(join18(TEMPLATES_DIR, name), "utf8");
30153
30429
  return JSON.parse(raw.replaceAll("{{HOOK_COMMAND}}", hookCommand));
30154
30430
  }
30155
30431
  function readJson(path2) {
30156
- if (!existsSync20(path2))
30432
+ if (!existsSync21(path2))
30157
30433
  return null;
30158
30434
  try {
30159
- return JSON.parse(readFileSync15(path2, "utf8"));
30435
+ return JSON.parse(readFileSync16(path2, "utf8"));
30160
30436
  } catch (error) {
30161
30437
  throw new Error(`Invalid JSON in ${path2}: ${error}`);
30162
30438
  }
@@ -30262,14 +30538,14 @@ function mergeNestedHooks(platform, targetPath, fragment, eventName, opts) {
30262
30538
  }
30263
30539
  function mergeHookConfigs(opts) {
30264
30540
  const results = [];
30265
- const cursorPath = join17(opts.cwd, ".cursor/hooks.json");
30541
+ const cursorPath = join18(opts.cwd, ".cursor/hooks.json");
30266
30542
  const cursorFragment = loadFragment("cursor-hooks.fragment.json", opts.hookCommand);
30267
30543
  results.push(mergeCursorHooks(cursorPath, cursorFragment, opts));
30268
- const claudePath = join17(opts.cwd, ".claude/settings.json");
30544
+ const claudePath = join18(opts.cwd, ".claude/settings.json");
30269
30545
  const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand);
30270
30546
  results.push(mergeNestedHooks("claude", claudePath, claudeFragment, "PostToolUse", opts));
30271
- const codexPath = join17(opts.cwd, ".codex/hooks.json");
30272
- if (existsSync20(join17(opts.cwd, ".codex"))) {
30547
+ const codexPath = join18(opts.cwd, ".codex/hooks.json");
30548
+ if (existsSync21(join18(opts.cwd, ".codex"))) {
30273
30549
  const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand);
30274
30550
  results.push(mergeNestedHooks("codex", codexPath, codexFragment, "PostToolUse", opts));
30275
30551
  } else {
@@ -30278,11 +30554,11 @@ function mergeHookConfigs(opts) {
30278
30554
  return results;
30279
30555
  }
30280
30556
  function mergePackageJsonScripts(cwd) {
30281
- const pkgPath = join17(cwd, "package.json");
30282
- if (!existsSync20(pkgPath))
30557
+ const pkgPath = join18(cwd, "package.json");
30558
+ if (!existsSync21(pkgPath))
30283
30559
  return "skipped";
30284
- const fragment = JSON.parse(readFileSync15(join17(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"));
30285
- const pkg = JSON.parse(readFileSync15(pkgPath, "utf8"));
30560
+ const fragment = JSON.parse(readFileSync16(join18(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"));
30561
+ const pkg = JSON.parse(readFileSync16(pkgPath, "utf8"));
30286
30562
  pkg.scripts ??= {};
30287
30563
  let changed = false;
30288
30564
  for (const [key, value] of Object.entries(fragment)) {
@@ -30338,27 +30614,27 @@ function skillsAddArgs(options = {}) {
30338
30614
  // src/init/init.ts
30339
30615
  var TEMPLATES_DIR2 = resolveTemplatesDir();
30340
30616
  function writeScaffold(cwd) {
30341
- const skeletonDir2 = join18(cwd, ".skeleton");
30617
+ const skeletonDir2 = join19(cwd, ".skeleton");
30342
30618
  mkdirSync2(skeletonDir2, { recursive: true });
30343
30619
  let created = false;
30344
- const configPath = join18(skeletonDir2, "config.yaml");
30345
- if (!existsSync21(configPath)) {
30346
- copyFileSync(join18(TEMPLATES_DIR2, "config.yaml"), configPath);
30620
+ const configPath = join19(skeletonDir2, "config.yaml");
30621
+ if (!existsSync22(configPath)) {
30622
+ copyFileSync(join19(TEMPLATES_DIR2, "config.yaml"), configPath);
30347
30623
  created = true;
30348
30624
  }
30349
- const registryPath = join18(skeletonDir2, "registry.md");
30350
- if (!existsSync21(registryPath)) {
30351
- copyFileSync(join18(TEMPLATES_DIR2, "registry.md"), registryPath);
30625
+ const registryPath = join19(skeletonDir2, "registry.md");
30626
+ if (!existsSync22(registryPath)) {
30627
+ copyFileSync(join19(TEMPLATES_DIR2, "registry.md"), registryPath);
30352
30628
  created = true;
30353
30629
  }
30354
- mkdirSync2(join18(skeletonDir2, "customize"), { recursive: true });
30630
+ mkdirSync2(join19(skeletonDir2, "customize"), { recursive: true });
30355
30631
  return created ? "created" : "skipped";
30356
30632
  }
30357
30633
  function assertPackageResolvable(cwd) {
30358
- const pkgPath = join18(cwd, "package.json");
30359
- if (!existsSync21(pkgPath))
30634
+ const pkgPath = join19(cwd, "package.json");
30635
+ if (!existsSync22(pkgPath))
30360
30636
  return;
30361
- const pkg = JSON.parse(readFileSync16(pkgPath, "utf8"));
30637
+ const pkg = JSON.parse(readFileSync17(pkgPath, "utf8"));
30362
30638
  const hasDep = pkg.devDependencies?.["@csark0812/skeleton"] || pkg.dependencies?.["@csark0812/skeleton"];
30363
30639
  if (!hasDep) {
30364
30640
  try {
@@ -30442,7 +30718,7 @@ function parseInitArgs(argv) {
30442
30718
  // src/plugins/build.ts
30443
30719
  import { spawnSync as spawnSync3 } from "node:child_process";
30444
30720
  import { createHash } from "node:crypto";
30445
- import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync3 } from "node:fs";
30721
+ import { existsSync as existsSync23, readFileSync as readFileSync18, writeFileSync as writeFileSync3 } from "node:fs";
30446
30722
  import { basename as basename4, dirname as dirname11, resolve as resolve9 } from "node:path";
30447
30723
  function parseBuildPluginArgs(argv) {
30448
30724
  let check = false;
@@ -30483,7 +30759,7 @@ function localImportPaths(tsAbs, content3) {
30483
30759
  candidates.push(resolve9(dir, spec), resolve9(dir, `${spec}.ts`), resolve9(dir, `${spec}.js`), resolve9(dir, spec, "index.ts"));
30484
30760
  }
30485
30761
  for (const candidate of candidates) {
30486
- if (existsSync22(candidate) && candidate.endsWith(".ts")) {
30762
+ if (existsSync23(candidate) && candidate.endsWith(".ts")) {
30487
30763
  deps.push(candidate);
30488
30764
  break;
30489
30765
  }
@@ -30500,7 +30776,7 @@ function sourceFingerprint(tsAbs, seen = new Set) {
30500
30776
  if (seen.has(abs))
30501
30777
  return;
30502
30778
  seen.add(abs);
30503
- const content3 = readFileSync17(abs, "utf8");
30779
+ const content3 = readFileSync18(abs, "utf8");
30504
30780
  hash.update(basename4(abs));
30505
30781
  hash.update("\x00");
30506
30782
  hash.update(content3);
@@ -30518,7 +30794,7 @@ function writeStamp(tsAbs, mjsAbs) {
30518
30794
  }
30519
30795
  async function buildOne(tsAbs) {
30520
30796
  const mjsAbs = mjsPathForTs(tsAbs);
30521
- if (!existsSync22(tsAbs)) {
30797
+ if (!existsSync23(tsAbs)) {
30522
30798
  throw new Error(`Plugin source not found: ${tsAbs}`);
30523
30799
  }
30524
30800
  const proc = spawnSync3("bun", ["build", tsAbs, "--target=node", "--format=esm", `--outfile=${mjsAbs}`, "--packages=external"], { encoding: "utf8" });
@@ -30538,17 +30814,17 @@ ${proc.stderr || proc.stdout || `exit ${proc.status}`}`);
30538
30814
  }
30539
30815
  function checkOne(tsAbs) {
30540
30816
  const mjsAbs = mjsPathForTs(tsAbs);
30541
- if (!existsSync22(mjsAbs)) {
30817
+ if (!existsSync23(mjsAbs)) {
30542
30818
  throw new Error(`Plugin not built: ${tsAbs} (missing ${mjsAbs}). Run: skeleton build-plugin`);
30543
30819
  }
30544
- if (!existsSync22(tsAbs)) {
30820
+ if (!existsSync23(tsAbs)) {
30545
30821
  throw new Error(`Plugin source not found: ${tsAbs}`);
30546
30822
  }
30547
30823
  const stampAbs = stampPathForMjs(mjsAbs);
30548
- if (!existsSync22(stampAbs)) {
30824
+ if (!existsSync23(stampAbs)) {
30549
30825
  throw new Error(`Plugin stale: ${mjsAbs} has no fingerprint stamp. Run: skeleton build-plugin`);
30550
30826
  }
30551
- const expected = readFileSync17(stampAbs, "utf8").trim();
30827
+ const expected = readFileSync18(stampAbs, "utf8").trim();
30552
30828
  const actual = sourceFingerprint(tsAbs);
30553
30829
  if (expected !== actual) {
30554
30830
  throw new Error(`Plugin stale: ${mjsAbs} does not match ${tsAbs} (or local imports). Run: skeleton build-plugin`);
@@ -30578,22 +30854,31 @@ async function runBuildPlugin(options = {}) {
30578
30854
 
30579
30855
  // src/references/sync.ts
30580
30856
  import {
30581
- existsSync as existsSync23,
30857
+ existsSync as existsSync24,
30582
30858
  mkdirSync as mkdirSync3,
30583
30859
  readdirSync as readdirSync6,
30584
- readFileSync as readFileSync18,
30860
+ readFileSync as readFileSync19,
30585
30861
  unlinkSync,
30586
30862
  writeFileSync as writeFileSync4
30587
30863
  } from "node:fs";
30588
- import { dirname as dirname12, join as join19, relative as relative11 } from "node:path";
30864
+ import { dirname as dirname12, join as join20, relative as relative11 } from "node:path";
30865
+ function resolveOwnership(root2, override) {
30866
+ if (override !== undefined)
30867
+ return override;
30868
+ try {
30869
+ return loadConfig(root2).skillOwnership;
30870
+ } catch {
30871
+ return;
30872
+ }
30873
+ }
30589
30874
  function walkMarkdownFiles2(dir, root2) {
30590
30875
  const files = [];
30591
- if (!existsSync23(dir))
30876
+ if (!existsSync24(dir))
30592
30877
  return files;
30593
30878
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
30594
30879
  if (entry.name.startsWith("."))
30595
30880
  continue;
30596
- const fullPath = join19(dir, entry.name);
30881
+ const fullPath = join20(dir, entry.name);
30597
30882
  if (entry.isDirectory()) {
30598
30883
  files.push(...walkMarkdownFiles2(fullPath, root2));
30599
30884
  continue;
@@ -30605,20 +30890,20 @@ function walkMarkdownFiles2(dir, root2) {
30605
30890
  return files;
30606
30891
  }
30607
30892
  function listGeneratedReferenceFiles(skillDir, skill) {
30608
- const refsDir = join19(skillDir, "references");
30609
- if (!existsSync23(refsDir))
30893
+ const refsDir = join20(skillDir, "references");
30894
+ if (!existsSync24(refsDir))
30610
30895
  return [];
30611
30896
  const files = [];
30612
30897
  const walk = (dir) => {
30613
30898
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
30614
- const fullPath = join19(dir, entry.name);
30899
+ const fullPath = join20(dir, entry.name);
30615
30900
  if (entry.isDirectory()) {
30616
30901
  walk(fullPath);
30617
30902
  continue;
30618
30903
  }
30619
30904
  if (!entry.name.endsWith(".md"))
30620
30905
  continue;
30621
- const content3 = readFileSync18(fullPath, "utf8");
30906
+ const content3 = readFileSync19(fullPath, "utf8");
30622
30907
  if (isGeneratedReference(content3)) {
30623
30908
  const refPath = normalizeRelPath(relative11(refsDir, fullPath));
30624
30909
  files.push(generatedRefPath(skill, refPath));
@@ -30630,8 +30915,8 @@ function listGeneratedReferenceFiles(skillDir, skill) {
30630
30915
  }
30631
30916
  function syncReferences(options = {}) {
30632
30917
  const root2 = options.root ?? process.cwd();
30633
- const canonicalDir = join19(root2, CANONICAL_REFS_DIR);
30634
- if (!existsSync23(canonicalDir)) {
30918
+ const canonicalDir = join20(root2, CANONICAL_REFS_DIR);
30919
+ if (!existsSync24(canonicalDir)) {
30635
30920
  throw new Error(`canonical references dir not found: ${CANONICAL_REFS_DIR}`);
30636
30921
  }
30637
30922
  const result = {
@@ -30640,23 +30925,23 @@ function syncReferences(options = {}) {
30640
30925
  removed: [],
30641
30926
  skipped: []
30642
30927
  };
30643
- const plans = discoverSkillReferencePlans(root2);
30928
+ const plans = discoverSkillReferencePlans(root2, resolveOwnership(root2, options.ownership));
30644
30929
  for (const plan of plans) {
30645
- const skillDir = join19(root2, plan.skill);
30930
+ const skillDir = join20(root2, plan.skill);
30646
30931
  for (const refPath of plan.refPaths) {
30647
- const sourceRel = normalizeRelPath(join19(CANONICAL_REFS_DIR, refPath));
30648
- const canonicalPath = join19(root2, sourceRel);
30649
- if (!existsSync23(canonicalPath)) {
30932
+ const sourceRel = normalizeRelPath(join20(CANONICAL_REFS_DIR, refPath));
30933
+ const canonicalPath = join20(root2, sourceRel);
30934
+ if (!existsSync24(canonicalPath)) {
30650
30935
  throw new Error(`canonical reference missing: ${sourceRel}`);
30651
30936
  }
30652
30937
  const targetRel = generatedRefPath(plan.skill, refPath);
30653
- const targetPath = join19(root2, targetRel);
30654
- const canonicalContent = readFileSync18(canonicalPath, "utf8");
30938
+ const targetPath = join20(root2, targetRel);
30939
+ const canonicalContent = readFileSync19(canonicalPath, "utf8");
30655
30940
  const nextContent = formatGeneratedHeader(sourceRel) + canonicalContent;
30656
30941
  if (!options.dryRun) {
30657
30942
  mkdirSync3(dirname12(targetPath), { recursive: true });
30658
30943
  }
30659
- const existing = existsSync23(targetPath) ? readFileSync18(targetPath, "utf8") : null;
30944
+ const existing = existsSync24(targetPath) ? readFileSync19(targetPath, "utf8") : null;
30660
30945
  if (existing !== nextContent) {
30661
30946
  if (!options.dryRun)
30662
30947
  writeFileSync4(targetPath, nextContent, "utf8");
@@ -30667,8 +30952,8 @@ function syncReferences(options = {}) {
30667
30952
  }
30668
30953
  if (options.rewriteLinks !== false) {
30669
30954
  for (const relFile of walkMarkdownFiles2(skillDir, root2)) {
30670
- const filePath = join19(root2, relFile);
30671
- const content3 = readFileSync18(filePath, "utf8");
30955
+ const filePath = join20(root2, relFile);
30956
+ const content3 = readFileSync19(filePath, "utf8");
30672
30957
  const next = rewriteSharedRefLinks(content3, relFile, plan.skill);
30673
30958
  if (next !== content3) {
30674
30959
  if (!options.dryRun)
@@ -30680,7 +30965,7 @@ function syncReferences(options = {}) {
30680
30965
  for (const generatedRel of listGeneratedReferenceFiles(skillDir, plan.skill)) {
30681
30966
  const refPath = generatedRel.slice(`${plan.skill}/references/`.length);
30682
30967
  if (!plan.refPaths.has(refPath)) {
30683
- const fullPath = join19(root2, generatedRel);
30968
+ const fullPath = join20(root2, generatedRel);
30684
30969
  if (!options.dryRun)
30685
30970
  unlinkSync(fullPath);
30686
30971
  result.removed.push(generatedRel);
@@ -30696,7 +30981,13 @@ function runReferencesSync(options = {}) {
30696
30981
  }
30697
30982
  function runReferencesCheck(options = {}) {
30698
30983
  const root2 = options.root ?? process.cwd();
30699
- const issues = runGeneratedReferencesCheck(root2);
30984
+ let ownership;
30985
+ try {
30986
+ ownership = loadConfig(root2).skillOwnership;
30987
+ } catch {
30988
+ ownership = undefined;
30989
+ }
30990
+ const issues = runGeneratedReferencesCheck(root2, ownership);
30700
30991
  return printReport(issues, {
30701
30992
  strict: options.strict,
30702
30993
  json: options.json,
@@ -30725,8 +31016,8 @@ function printSyncResult(result) {
30725
31016
  }
30726
31017
 
30727
31018
  // src/register.ts
30728
- import { existsSync as existsSync24, readFileSync as readFileSync19, writeFileSync as writeFileSync5 } from "node:fs";
30729
- import { dirname as dirname13, join as join20, relative as relative12 } from "node:path";
31019
+ import { existsSync as existsSync25, readFileSync as readFileSync20, writeFileSync as writeFileSync5 } from "node:fs";
31020
+ import { dirname as dirname13, join as join21, relative as relative12 } from "node:path";
30730
31021
  var REGISTRY_TABLE_ROW_RE2 = /^\|\s*([^|]+)\|\s*\[[^\]]*\]\(([^)]+)\)\s*\|/;
30731
31022
  var REGISTRY_TABLE_HEADER = "| Topic | Canonical file |";
30732
31023
  function extractTopic(content3) {
@@ -30734,7 +31025,7 @@ function extractTopic(content3) {
30734
31025
  return match?.[1]?.trim().replace(/\s+$/, "") ?? null;
30735
31026
  }
30736
31027
  function toRegistryLink(root2, absPath) {
30737
- const fromRegistry = join20(root2, REGISTRY_DIR_REL);
31028
+ const fromRegistry = join21(root2, REGISTRY_DIR_REL);
30738
31029
  return normalizeRelPath(relative12(fromRegistry, absPath));
30739
31030
  }
30740
31031
  function inferSection(registryLink) {
@@ -30759,11 +31050,11 @@ function parseRegistryRows(content3) {
30759
31050
  return rows;
30760
31051
  }
30761
31052
  function pathFromRegistryLink(root2, link2) {
30762
- return normalizeRelPath(relative12(root2, join20(root2, REGISTRY_DIR_REL, link2)));
31053
+ return normalizeRelPath(relative12(root2, join21(root2, REGISTRY_DIR_REL, link2)));
30763
31054
  }
30764
31055
  function isOutsideScan(root2, relPath2) {
30765
31056
  const config = loadConfig(root2);
30766
- const skillIndex = buildSkillIndex(root2);
31057
+ const skillIndex = buildSkillIndex(root2, config.skillOwnership);
30767
31058
  const scanned = collectScanFiles(config, root2, skillIndex).map((abs) => normalizeRelPath(relative12(root2, abs)));
30768
31059
  if (scanned.includes(relPath2))
30769
31060
  return false;
@@ -30826,11 +31117,11 @@ ${newLine}
30826
31117
  function registerPath(options) {
30827
31118
  const root2 = options.root ?? findRepoRoot();
30828
31119
  const relPath2 = normalizeRelPath(options.path);
30829
- const absPath = join20(root2, relPath2);
30830
- if (!existsSync24(absPath)) {
31120
+ const absPath = join21(root2, relPath2);
31121
+ if (!existsSync25(absPath)) {
30831
31122
  throw new Error(`File not found: ${relPath2}`);
30832
31123
  }
30833
- const content3 = readFileSync19(absPath, "utf8");
31124
+ const content3 = readFileSync20(absPath, "utf8");
30834
31125
  let topic = options.topic ?? extractTopic(content3);
30835
31126
  if (!topic) {
30836
31127
  throw new Error(`No **Source of truth for** banner in ${relPath2} — add banner or pass --topic`);
@@ -30838,9 +31129,9 @@ function registerPath(options) {
30838
31129
  const registryLink = toRegistryLink(root2, absPath);
30839
31130
  topic = ensureCustomizeTopic(topic, registryLink);
30840
31131
  const section = inferSection(registryLink);
30841
- const registryAbs = join20(root2, REGISTRY_REL_PATH);
30842
- let registryContent = existsSync24(registryAbs) ? readFileSync19(registryAbs, "utf8") : defaultRegistryContent();
30843
- if (!existsSync24(registryAbs) && !existsSync24(join20(root2, ".skeleton/config.yaml"))) {
31132
+ const registryAbs = join21(root2, REGISTRY_REL_PATH);
31133
+ let registryContent = existsSync25(registryAbs) ? readFileSync20(registryAbs, "utf8") : defaultRegistryContent();
31134
+ if (!existsSync25(registryAbs) && !existsSync25(join21(root2, ".skeleton/config.yaml"))) {
30844
31135
  throw new Error("Missing .skeleton/config.yaml — run skeleton init first");
30845
31136
  }
30846
31137
  const { content: updated, action } = upsertRow(registryContent, topic, registryLink, section, root2);
@@ -30854,7 +31145,7 @@ function registerPath(options) {
30854
31145
  };
30855
31146
  if (!options.dryRun && action !== "noop") {
30856
31147
  const dir = dirname13(registryAbs);
30857
- if (!existsSync24(dir)) {
31148
+ if (!existsSync25(dir)) {
30858
31149
  throw new Error(`Missing ${REGISTRY_DIR_REL}/ directory`);
30859
31150
  }
30860
31151
  writeFileSync5(registryAbs, registryContent, "utf8");
@@ -30876,8 +31167,8 @@ function registerPath(options) {
30876
31167
 
30877
31168
  // src/validate/changed.ts
30878
31169
  import { spawnSync as spawnSync5 } from "node:child_process";
30879
- import { existsSync as existsSync25, readFileSync as readFileSync20 } from "node:fs";
30880
- import { basename as basename5, extname as extname2, join as join21 } from "node:path";
31170
+ import { existsSync as existsSync26, readFileSync as readFileSync21 } from "node:fs";
31171
+ import { basename as basename5, extname as extname2, join as join22 } from "node:path";
30881
31172
 
30882
31173
  // src/validate/git-diff.ts
30883
31174
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -30916,7 +31207,7 @@ function isSkeletonYamlCandidate(normalized, ext) {
30916
31207
  return false;
30917
31208
  return true;
30918
31209
  }
30919
- function bucketFor(relPath2, root2, wiredPolicies) {
31210
+ function bucketFor(relPath2, root2, wiredPolicies, skillIndex) {
30920
31211
  const normalized = normalizeRelPath(relPath2);
30921
31212
  const ext = extname2(normalized).toLowerCase();
30922
31213
  const name = basename5(normalized);
@@ -30929,9 +31220,11 @@ function bucketFor(relPath2, root2, wiredPolicies) {
30929
31220
  return "policy";
30930
31221
  return "skip";
30931
31222
  }
30932
- const skillIndex = buildSkillIndex(root2);
30933
- if (isSkillPath(normalized, skillIndex))
31223
+ if (isSkillPath(normalized, skillIndex)) {
31224
+ if (isForeignSkillPath(normalized, skillIndex))
31225
+ return "foreign-skill";
30934
31226
  return "skills";
31227
+ }
30935
31228
  if (DOC_EXTENSIONS.has(ext)) {
30936
31229
  const config = loadConfig(root2);
30937
31230
  if (isInScanPerimeter(normalized, config, root2, skillIndex))
@@ -30960,9 +31253,9 @@ function parseJsonContent(content3) {
30960
31253
  }
30961
31254
  }
30962
31255
  function validateJson(relPath2, root2) {
30963
- const abs = join21(root2, relPath2);
31256
+ const abs = join22(root2, relPath2);
30964
31257
  try {
30965
- parseJsonContent(readFileSync20(abs, "utf8"));
31258
+ parseJsonContent(readFileSync21(abs, "utf8"));
30966
31259
  return 0;
30967
31260
  } catch (error) {
30968
31261
  console.error(`validate changed: invalid JSON in ${relPath2}: ${error}`);
@@ -30970,9 +31263,9 @@ function validateJson(relPath2, root2) {
30970
31263
  }
30971
31264
  }
30972
31265
  function validatePolicy(relPath2, root2) {
30973
- const abs = join21(root2, relPath2);
31266
+ const abs = join22(root2, relPath2);
30974
31267
  try {
30975
- loadPolicyFile(abs, readFileSync20(abs, "utf8"));
31268
+ loadPolicyFile(abs, readFileSync21(abs, "utf8"));
30976
31269
  return 0;
30977
31270
  } catch (error) {
30978
31271
  console.error(`validate changed: invalid policy ${relPath2}: ${error}`);
@@ -30980,7 +31273,7 @@ function validatePolicy(relPath2, root2) {
30980
31273
  }
30981
31274
  }
30982
31275
  function validateShell(relPath2, root2) {
30983
- const abs = join21(root2, relPath2);
31276
+ const abs = join22(root2, relPath2);
30984
31277
  const shellcheck = spawnSync5("shellcheck", [abs], { encoding: "utf8" });
30985
31278
  if (shellcheck.status === 0)
30986
31279
  return 0;
@@ -31002,23 +31295,23 @@ function resolvePaths(options) {
31002
31295
  }
31003
31296
  function codeValidationHint(root2) {
31004
31297
  let pm2 = null;
31005
- const pkgPath = join21(root2, "package.json");
31006
- if (existsSync25(pkgPath)) {
31298
+ const pkgPath = join22(root2, "package.json");
31299
+ if (existsSync26(pkgPath)) {
31007
31300
  try {
31008
- const pkg = JSON.parse(readFileSync20(pkgPath, "utf8"));
31301
+ const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
31009
31302
  const raw = pkg.packageManager?.split("@")[0];
31010
31303
  if (raw === "bun" || raw === "npm" || raw === "pnpm" || raw === "yarn")
31011
31304
  pm2 = raw;
31012
31305
  } catch {}
31013
31306
  }
31014
31307
  if (!pm2) {
31015
- if (existsSync25(join21(root2, "bun.lock")) || existsSync25(join21(root2, "bun.lockb")))
31308
+ if (existsSync26(join22(root2, "bun.lock")) || existsSync26(join22(root2, "bun.lockb")))
31016
31309
  pm2 = "bun";
31017
- else if (existsSync25(join21(root2, "pnpm-lock.yaml")))
31310
+ else if (existsSync26(join22(root2, "pnpm-lock.yaml")))
31018
31311
  pm2 = "pnpm";
31019
- else if (existsSync25(join21(root2, "yarn.lock")))
31312
+ else if (existsSync26(join22(root2, "yarn.lock")))
31020
31313
  pm2 = "yarn";
31021
- else if (existsSync25(join21(root2, "package-lock.json")))
31314
+ else if (existsSync26(join22(root2, "package-lock.json")))
31022
31315
  pm2 = "npm";
31023
31316
  }
31024
31317
  switch (pm2) {
@@ -31042,6 +31335,7 @@ async function runValidateChanged(options = {}) {
31042
31335
  return 0;
31043
31336
  }
31044
31337
  const config = loadConfig(root2);
31338
+ const skillIndex = buildSkillIndex(root2, config.skillOwnership);
31045
31339
  let wiredPolicies;
31046
31340
  try {
31047
31341
  wiredPolicies = await collectWiredPolicyRelPaths(root2, config);
@@ -31058,11 +31352,12 @@ async function runValidateChanged(options = {}) {
31058
31352
  };
31059
31353
  let missing = 0;
31060
31354
  let skipped = 0;
31355
+ let foreignSkipped = 0;
31061
31356
  const orphans = [];
31062
31357
  for (const relPath2 of relPaths) {
31063
31358
  const normalized = normalizeRelPath(relPath2);
31064
- const abs = join21(root2, normalized);
31065
- if (!existsSync25(abs)) {
31359
+ const abs = join22(root2, normalized);
31360
+ if (!existsSync26(abs)) {
31066
31361
  missing++;
31067
31362
  console.error(`validate changed: path not found: ${relPath2}`);
31068
31363
  continue;
@@ -31072,11 +31367,16 @@ async function runValidateChanged(options = {}) {
31072
31367
  orphans.push(normalized);
31073
31368
  continue;
31074
31369
  }
31075
- const bucket = bucketFor(normalized, root2, wiredPolicies);
31370
+ const bucket = bucketFor(normalized, root2, wiredPolicies, skillIndex);
31076
31371
  if (bucket === "skip") {
31077
31372
  skipped++;
31078
31373
  continue;
31079
31374
  }
31375
+ if (bucket === "foreign-skill") {
31376
+ foreignSkipped++;
31377
+ console.log(`validate changed: skipping foreign skill ${normalized} (owned upstream; see skills-lock.json / skillOwnership)`);
31378
+ continue;
31379
+ }
31080
31380
  buckets[bucket].push(normalized);
31081
31381
  }
31082
31382
  if (orphans.length > 0) {
@@ -31124,24 +31424,24 @@ async function runValidateChanged(options = {}) {
31124
31424
  exitCode = 1;
31125
31425
  }
31126
31426
  if (buckets.skills.length > 0) {
31127
- const skillsOnly = buckets.docs.length === 0 && buckets.shell.length === 0 && buckets.json.length === 0 && buckets.policy.length === 0;
31128
- if (skillsOnly && !options.base) {
31427
+ if (!options.base) {
31129
31428
  console.error(`validate changed: skill paths need the full skills suite (path-scoped skill rules are empty).
31130
31429
  ` + ` Run: skeleton audit skills
31131
31430
  ` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
31132
- return 1;
31133
- }
31134
- const skillExit = await runAudit({
31135
- suite: "skills",
31136
- strict: false,
31137
- json: false,
31138
- paths: buckets.skills,
31139
- only: null,
31140
- root: root2,
31141
- pathScopedOnly: true
31142
- });
31143
- if (skillExit !== 0)
31144
31431
  exitCode = 1;
31432
+ } else {
31433
+ const skillExit = await runAudit({
31434
+ suite: "skills",
31435
+ strict: false,
31436
+ json: false,
31437
+ paths: buckets.skills,
31438
+ only: null,
31439
+ root: root2,
31440
+ pathScopedOnly: true
31441
+ });
31442
+ if (skillExit !== 0)
31443
+ exitCode = 1;
31444
+ }
31145
31445
  }
31146
31446
  for (const relPath2 of buckets.shell) {
31147
31447
  if (validateShell(relPath2, root2) !== 0)
@@ -31167,7 +31467,7 @@ async function runValidateChanged(options = {}) {
31167
31467
  });
31168
31468
  if (proseExit !== 0)
31169
31469
  exitCode = 1;
31170
- const skillPaths = listSkillMarkdownPaths(root2, buildSkillIndex(root2));
31470
+ const skillPaths = listSkillMarkdownPaths(root2, skillIndex);
31171
31471
  if (skillPaths.length > 0) {
31172
31472
  const skillProseExit = await runAudit({
31173
31473
  suite: "skills",
@@ -31182,17 +31482,20 @@ async function runValidateChanged(options = {}) {
31182
31482
  exitCode = 1;
31183
31483
  }
31184
31484
  } else {
31185
- if (exitCode === 0) {
31186
- console.error(`validate changed: policy YAML changes need a full prose-policy pass (path-scoped docs are not enough).
31485
+ console.error(`validate changed: policy YAML changes need a full prose-policy pass (path-scoped docs are not enough).
31187
31486
  ` + ` Run: skeleton audit docs
31188
31487
  ` + ` And: skeleton audit skills
31189
31488
  ` + " (audit self covers docs + .skeleton; excluded skill trees still need audit skills)");
31190
- }
31191
31489
  return 1;
31192
31490
  }
31193
31491
  }
31194
31492
  if (exitCode === 0) {
31195
- const note = skipped > 0 ? ` (${skipped} path(s) skipped)` : "";
31493
+ const parts = [];
31494
+ if (skipped > 0)
31495
+ parts.push(`${skipped} path(s) skipped`);
31496
+ if (foreignSkipped > 0)
31497
+ parts.push(`${foreignSkipped} foreign skill(s) ignored`);
31498
+ const note = parts.length > 0 ? ` (${parts.join(", ")})` : "";
31196
31499
  console.log(`validate changed passed${note}.`);
31197
31500
  }
31198
31501
  return exitCode;
@@ -31210,6 +31513,7 @@ Commands:
31210
31513
  validate changed [paths…] [--staged] [--base <ref>]
31211
31514
  register <path> [--topic=…] [--dry-run] [--json]
31212
31515
  customize resolve <slug> [--json]
31516
+ hook customize (reads a host hook payload on stdin)
31213
31517
  references sync [--dry-run] [--no-rewrite-links]
31214
31518
  references check [--json] [--strict]`);
31215
31519
  }
@@ -31306,6 +31610,14 @@ async function main() {
31306
31610
  }
31307
31611
  process.exit(0);
31308
31612
  }
31613
+ if (command === "hook") {
31614
+ if (argv[1] !== "customize") {
31615
+ usage();
31616
+ process.exit(1);
31617
+ }
31618
+ process.stdout.write(runCustomizeHook(readFileSync22(0, "utf8")));
31619
+ process.exit(0);
31620
+ }
31309
31621
  if (command === "init") {
31310
31622
  const parsed = parseInitArgs(argv.slice(1));
31311
31623
  runInit(parsed);