@csark0812/skeleton 4.0.0 → 4.0.2

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
@@ -15968,8 +15968,8 @@ var require_extend = __commonJS((exports, module) => {
15968
15968
  });
15969
15969
 
15970
15970
  // src/cli.ts
15971
- import { readFileSync as readFileSync22 } from "node:fs";
15972
- import process7 from "node:process";
15971
+ import { readFileSync as readFileSync24 } from "node:fs";
15972
+ import process6 from "node:process";
15973
15973
 
15974
15974
  // src/audit/config/load.ts
15975
15975
  var import_ajv = __toESM(require_ajv(), 1);
@@ -17083,9 +17083,6 @@ function nonPublicSkills(config) {
17083
17083
  return config.scan.nonPublicSkills ?? [];
17084
17084
  }
17085
17085
 
17086
- // src/audit/run.ts
17087
- import process4 from "node:process";
17088
-
17089
17086
  // src/catalog.ts
17090
17087
  import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
17091
17088
  import { dirname as dirname3, join as join5 } from "node:path";
@@ -18571,19 +18568,11 @@ function writeCatalog(root) {
18571
18568
  writeFileSync(abs, content, "utf8");
18572
18569
  return { path: normalizeRelPath(CATALOG_REL_PATH), entries };
18573
18570
  }
18574
- function catalogAuditWarnings(root) {
18571
+ function refreshLocalCatalog(root) {
18575
18572
  if (process3.env.CI === "true")
18576
- return [];
18577
- const result = checkCatalog(root);
18578
- if (result.missing) {
18579
- return [
18580
- `${CATALOG_REL_PATH} missing — run \`skeleton catalog\` so agents can skim SSOT summaries`
18581
- ];
18582
- }
18583
- if (result.stale) {
18584
- return [`${CATALOG_REL_PATH} outdated — run \`skeleton catalog\` to refresh`];
18585
- }
18586
- return [];
18573
+ return "skipped-ci";
18574
+ writeCatalog(root);
18575
+ return "current";
18587
18576
  }
18588
18577
  function runCatalogCli(options = {}) {
18589
18578
  const root = options.root ?? findRepoRoot();
@@ -18945,7 +18934,8 @@ function createContext(options = {}) {
18945
18934
  registryHasTableHeader: false,
18946
18935
  skillIndex,
18947
18936
  lockedSkillSlugs: new Set(skillIndex.foreignSlugs),
18948
- policies: options.policies ?? []
18937
+ policies: options.policies ?? [],
18938
+ fileSource: options.fileSource
18949
18939
  };
18950
18940
  }
18951
18941
 
@@ -30900,9 +30890,8 @@ function printWarnings(label, warnings) {
30900
30890
  return;
30901
30891
  console.log(`${label} warnings:
30902
30892
  `);
30903
- for (const i of warnings) {
30904
- const linkPart = i.link ? ` (${i.link})` : "";
30905
- console.log(`- ${i.file}${linkPart}: ${i.message}`);
30893
+ for (const item of warnings) {
30894
+ console.log(`${item.file}: warning: ${item.message}`);
30906
30895
  }
30907
30896
  console.log("");
30908
30897
  }
@@ -30912,12 +30901,65 @@ function printSuccess(label, options, warnings) {
30912
30901
  console.log(`${label} passed${countNote}.`);
30913
30902
  return 0;
30914
30903
  }
30904
+ var REREAD_CODES = new Set([
30905
+ "review-dependency-changed",
30906
+ "impacted-document-review-required",
30907
+ "review-document-changed",
30908
+ "review-dependency-set-changed"
30909
+ ]);
30910
+ function isRereadIssue(item) {
30911
+ return Boolean(item.code && REREAD_CODES.has(item.code));
30912
+ }
30913
+ function rereadTrigger(item) {
30914
+ if (item.code === "review-document-changed")
30915
+ return item.file;
30916
+ if (item.code === "review-dependency-changed" || item.code === "impacted-document-review-required") {
30917
+ return item.link ?? null;
30918
+ }
30919
+ return null;
30920
+ }
30921
+ function orderTriggers(file, triggers) {
30922
+ const rest = [...triggers].filter((path3) => path3 !== file).sort();
30923
+ return triggers.has(file) ? [file, ...rest] : rest;
30924
+ }
30925
+ function collectRereadTriggers(errors2) {
30926
+ const triggers = new Map;
30927
+ const rest = [];
30928
+ for (const item of errors2) {
30929
+ if (!isRereadIssue(item)) {
30930
+ rest.push(item);
30931
+ continue;
30932
+ }
30933
+ const current = triggers.get(item.file) ?? new Set;
30934
+ const trigger = rereadTrigger(item);
30935
+ if (trigger)
30936
+ current.add(trigger);
30937
+ triggers.set(item.file, current);
30938
+ }
30939
+ return { triggers, rest };
30940
+ }
30941
+ function printRereadDiagnostic(file, triggers) {
30942
+ console.log(`${file}: error: review required`);
30943
+ const changed = orderTriggers(file, triggers);
30944
+ if (changed.length > 0) {
30945
+ console.log(` changed: ${changed.join(", ")}`);
30946
+ return;
30947
+ }
30948
+ console.log(" changed: review dependency set");
30949
+ }
30950
+ function printRereadLists(errors2) {
30951
+ const { triggers, rest } = collectRereadTriggers(errors2);
30952
+ for (const file of [...triggers.keys()].sort()) {
30953
+ printRereadDiagnostic(file, triggers.get(file) ?? new Set);
30954
+ }
30955
+ return rest;
30956
+ }
30915
30957
  function printErrors(label, errors2) {
30916
30958
  console.log(`${label} failed:
30917
30959
  `);
30918
- for (const i of errors2) {
30919
- const linkPart = i.link ? ` (${i.link})` : "";
30920
- console.log(`- ${i.file}${linkPart}: ${i.message}`);
30960
+ const rest = printRereadLists(errors2);
30961
+ for (const item of rest) {
30962
+ console.log(`${item.file}: error: ${item.message}`);
30921
30963
  }
30922
30964
  return 1;
30923
30965
  }
@@ -30931,11 +30973,43 @@ function printTextReport(ctx) {
30931
30973
 
30932
30974
  // src/audit/core/review-proof.ts
30933
30975
  import { createHash } from "node:crypto";
30934
- import { existsSync as existsSync13, mkdirSync as mkdirSync2, readFileSync as readFileSync11, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
30935
- import { dirname as dirname8, relative as relative6 } from "node:path";
30976
+ import { existsSync as existsSync14, mkdirSync as mkdirSync2, readFileSync as readFileSync12, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
30977
+ import { dirname as dirname8 } from "node:path";
30978
+
30979
+ // src/audit/core/repo-files.ts
30980
+ import { spawnSync as spawnSync2 } from "node:child_process";
30981
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
30982
+ import { join as join11 } from "node:path";
30983
+ function gitShow(root2, spec) {
30984
+ const proc = spawnSync2("git", ["show", spec], {
30985
+ cwd: root2,
30986
+ encoding: "utf8",
30987
+ maxBuffer: 20000000
30988
+ });
30989
+ if (proc.status !== 0)
30990
+ return null;
30991
+ return proc.stdout;
30992
+ }
30993
+ function readRepoText(root2, relPath2, source = "worktree") {
30994
+ const normalized = normalizeRelPath(relPath2);
30995
+ if (source === "index")
30996
+ return gitShow(root2, `:${normalized}`);
30997
+ const abs = join11(root2, normalized);
30998
+ if (!existsSync12(abs))
30999
+ return null;
31000
+ return readFileSync11(abs, "utf8");
31001
+ }
31002
+ function pathDiffersFromHead(root2, relPath2) {
31003
+ const normalized = normalizeRelPath(relPath2);
31004
+ const head = gitShow(root2, `HEAD:${normalized}`);
31005
+ const worktree = readRepoText(root2, normalized, "worktree");
31006
+ if (worktree === null)
31007
+ return head !== null;
31008
+ return worktree !== (head ?? "");
31009
+ }
30936
31010
 
30937
31011
  // src/audit/core/review-deps.ts
30938
- import { existsSync as existsSync12 } from "node:fs";
31012
+ import { existsSync as existsSync13 } from "node:fs";
30939
31013
  var REVIEW_DEPS_RE = /<!--\s*review-deps:\s*([^>]*?)-->/gi;
30940
31014
  var GLOB_MAGIC_RE = /[*?{[]/;
30941
31015
  function parseReviewDepsMarkers(content3) {
@@ -30967,7 +31041,7 @@ function resolveReviewDependencies(root2, patterns) {
30967
31041
  throw new Error(`Invalid review dependency path: ${pattern}`);
30968
31042
  }
30969
31043
  if (!isReviewDependencyGlob(pattern)) {
30970
- if (!existsSync12(`${root2}/${pattern}`)) {
31044
+ if (!existsSync13(`${root2}/${pattern}`)) {
30971
31045
  throw new Error(`Review dependency path is missing: ${pattern}`);
30972
31046
  }
30973
31047
  targets.add(pattern);
@@ -31059,19 +31133,19 @@ function parseLock(content3) {
31059
31133
  return null;
31060
31134
  }
31061
31135
  }
31062
- function loadLock(root2, relPath2) {
31063
- const abs = resolveWritePath(root2, relPath2);
31064
- if (!existsSync13(abs))
31136
+ function loadLock(root2, relPath2, source = "worktree") {
31137
+ const content3 = readRepoText(root2, relPath2, source);
31138
+ if (content3 === null)
31065
31139
  return null;
31066
- return parseLock(readFileSync11(abs, "utf8"));
31140
+ return parseLock(content3);
31067
31141
  }
31068
31142
  function hashDependencies(root2, targets) {
31069
31143
  const out = {};
31070
31144
  for (const target of targets) {
31071
31145
  const abs = resolveWritePath(root2, target);
31072
- if (!existsSync13(abs))
31146
+ if (!existsSync14(abs))
31073
31147
  throw new Error(`Cannot attest missing review dependency: ${target}`);
31074
- out[target] = hash(readFileSync11(abs, "utf8"));
31148
+ out[target] = hash(readFileSync12(abs, "utf8"));
31075
31149
  }
31076
31150
  return out;
31077
31151
  }
@@ -31100,7 +31174,7 @@ function commitWrites(root2, writes) {
31100
31174
  try {
31101
31175
  for (const [relPath2, content3] of writes) {
31102
31176
  const abs = resolveWritePath(root2, relPath2);
31103
- originals.set(relPath2, existsSync13(abs) ? readFileSync11(abs, "utf8") : null);
31177
+ originals.set(relPath2, existsSync14(abs) ? readFileSync12(abs, "utf8") : null);
31104
31178
  mkdirSync2(dirname8(abs), { recursive: true });
31105
31179
  writeFileSync3(abs, content3, "utf8");
31106
31180
  written.push(relPath2);
@@ -31131,7 +31205,7 @@ function attestDocuments(options) {
31131
31205
  if (proofPath) {
31132
31206
  const absProof = resolveWritePath(ctx.root, proofPath);
31133
31207
  lock = loadLock(ctx.root, proofPath);
31134
- if (existsSync13(absProof) && !lock) {
31208
+ if (existsSync14(absProof) && !lock) {
31135
31209
  throw new Error(`Review proof lockfile is malformed: ${proofPath}`);
31136
31210
  }
31137
31211
  lock ??= emptyLock();
@@ -31139,9 +31213,9 @@ function attestDocuments(options) {
31139
31213
  const writes = new Map;
31140
31214
  for (const relPath2 of selected) {
31141
31215
  const abs = resolveWritePath(ctx.root, relPath2);
31142
- if (!existsSync13(abs))
31216
+ if (!existsSync14(abs))
31143
31217
  throw new Error(`Cannot attest missing document: ${relPath2}`);
31144
- const original = readFileSync11(abs, "utf8");
31218
+ const original = readFileSync12(abs, "utf8");
31145
31219
  if (!DOC_META_RE.test(original)) {
31146
31220
  throw new Error(`Cannot attest ${relPath2}: missing doc-meta comment`);
31147
31221
  }
@@ -31191,7 +31265,13 @@ function validateEntry(input) {
31191
31265
  }));
31192
31266
  return issues;
31193
31267
  }
31194
- issues.push(...dependencyHashIssues({ root: ctx.root, relPath: relPath2, targets: currentTargets, entry }));
31268
+ issues.push(...dependencyHashIssues({
31269
+ root: ctx.root,
31270
+ relPath: relPath2,
31271
+ targets: currentTargets,
31272
+ entry,
31273
+ fileSource: ctx.fileSource
31274
+ }));
31195
31275
  return issues;
31196
31276
  }
31197
31277
  function documentProofIssues(relPath2, content3, entry) {
@@ -31225,16 +31305,17 @@ function currentDependencies(input) {
31225
31305
  }
31226
31306
  function dependencyHashIssues(input) {
31227
31307
  const { root: root2, relPath: relPath2, targets, entry } = input;
31308
+ const source = input.fileSource ?? "worktree";
31228
31309
  const issues = [];
31229
31310
  for (const target of targets) {
31230
- const abs = resolveWritePath(root2, target);
31231
- if (!existsSync13(abs)) {
31311
+ const content3 = readRepoText(root2, target, source);
31312
+ if (content3 === null) {
31232
31313
  issues.push(issue("review-proof", relPath2, {
31233
31314
  code: "review-dependency-missing",
31234
31315
  message: `recorded review dependency is missing: ${target}`,
31235
31316
  link: target
31236
31317
  }));
31237
- } else if (entry.reviewDependencies[target] !== hash(readFileSync11(abs, "utf8"))) {
31318
+ } else if (entry.reviewDependencies[target] !== hash(content3)) {
31238
31319
  issues.push(changedDependencyIssue(relPath2, target));
31239
31320
  }
31240
31321
  }
@@ -31244,8 +31325,9 @@ function runReviewProofRule(ctx) {
31244
31325
  if (!ctx.config.reviewProof)
31245
31326
  return [];
31246
31327
  const relLock = lockPath(ctx);
31247
- const absLock = resolveWritePath(ctx.root, relLock);
31248
- if (!existsSync13(absLock)) {
31328
+ const source = ctx.fileSource ?? "worktree";
31329
+ const lockContent = readRepoText(ctx.root, relLock, source);
31330
+ if (lockContent === null) {
31249
31331
  return [
31250
31332
  issue("review-proof", relLock, {
31251
31333
  code: "review-proof-lock-missing",
@@ -31253,7 +31335,7 @@ function runReviewProofRule(ctx) {
31253
31335
  })
31254
31336
  ];
31255
31337
  }
31256
- const lock = parseLock(readFileSync11(absLock, "utf8"));
31338
+ const lock = parseLock(lockContent);
31257
31339
  if (!lock) {
31258
31340
  return [
31259
31341
  issue("review-proof", relLock, {
@@ -31264,11 +31346,10 @@ function runReviewProofRule(ctx) {
31264
31346
  }
31265
31347
  const issues = [];
31266
31348
  for (const relPath2 of ctx.docMetaPaths) {
31267
- const abs = resolveWritePath(ctx.root, relPath2);
31268
- if (!existsSync13(abs))
31349
+ const content3 = readRepoText(ctx.root, relPath2, source);
31350
+ if (content3 === null)
31269
31351
  continue;
31270
- const content3 = readFileSync11(abs, "utf8");
31271
- const entry = lock.documents[normalizeRelPath(relative6(ctx.root, abs))];
31352
+ const entry = lock.documents[normalizeRelPath(relPath2)];
31272
31353
  if (!entry) {
31273
31354
  issues.push(issue("review-proof", relPath2, {
31274
31355
  code: "review-proof-missing",
@@ -31295,8 +31376,8 @@ function runBannedRule(ctx) {
31295
31376
  var bannedRule = { id: "banned", run: runBannedRule };
31296
31377
 
31297
31378
  // src/audit/rules/doc-meta.ts
31298
- import { existsSync as existsSync14, readFileSync as readFileSync12 } from "node:fs";
31299
- import { join as join11 } from "node:path";
31379
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
31380
+ import { join as join12 } from "node:path";
31300
31381
  function checkDocMetaBanner(relPath2, content3) {
31301
31382
  if (DOC_META_RE.test(content3))
31302
31383
  return null;
@@ -31351,10 +31432,10 @@ function runDocMetaRule(ctx) {
31351
31432
  const issues = [];
31352
31433
  const today = new Date;
31353
31434
  for (const relPath2 of ctx.docMetaPaths) {
31354
- const abs = join11(ctx.root, relPath2);
31355
- if (!existsSync14(abs))
31435
+ const abs = join12(ctx.root, relPath2);
31436
+ if (!existsSync15(abs))
31356
31437
  continue;
31357
- const content3 = readFileSync12(abs, "utf8");
31438
+ const content3 = readFileSync13(abs, "utf8");
31358
31439
  const banner = checkDocMetaBanner(relPath2, content3);
31359
31440
  if (banner) {
31360
31441
  issues.push(banner);
@@ -31382,7 +31463,7 @@ function runDocMetaRule(ctx) {
31382
31463
  var docMetaRule = { id: "doc-meta", run: runDocMetaRule };
31383
31464
 
31384
31465
  // src/audit/rules/links.ts
31385
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
31466
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "node:fs";
31386
31467
  import { dirname as dirname9, resolve as resolve5 } from "node:path";
31387
31468
  function resolveLink2(sourceFile, target) {
31388
31469
  const withoutAnchor = target.split("#")[0]?.split("?")[0] ?? "";
@@ -31406,13 +31487,13 @@ function checkAgentFile(input, resolved, relSource) {
31406
31487
  return null;
31407
31488
  }
31408
31489
  const agentPath = resolved.endsWith(".md") ? resolved : `${resolved}.md`;
31409
- if (existsSync15(agentPath))
31490
+ if (existsSync16(agentPath))
31410
31491
  return null;
31411
31492
  return issue("links", relSource, { message: "missing agent file", link: input.linkLabel });
31412
31493
  }
31413
31494
  function checkBrokenPath(ctx) {
31414
31495
  const { input, pathPart, resolved, relSource, relTarget } = ctx;
31415
- if (!(pathPart && !existsSync15(resolved)))
31496
+ if (!(pathPart && !existsSync16(resolved)))
31416
31497
  return null;
31417
31498
  return issue("links", relSource, {
31418
31499
  message: `broken link → ${relTarget}`,
@@ -31421,9 +31502,9 @@ function checkBrokenPath(ctx) {
31421
31502
  }
31422
31503
  function checkBrokenAnchor(ctx) {
31423
31504
  const { input, anchor, resolved, relSource, relTarget } = ctx;
31424
- if (!(anchor && existsSync15(resolved)))
31505
+ if (!(anchor && existsSync16(resolved)))
31425
31506
  return null;
31426
- const targetContent = readFileSync13(resolved, "utf8");
31507
+ const targetContent = readFileSync14(resolved, "utf8");
31427
31508
  const slugs = extractHeadingSlugs(targetContent, resolved);
31428
31509
  const anchorSlug = slugifyAnchor(anchor);
31429
31510
  if (slugs.has(anchorSlug))
@@ -31475,8 +31556,8 @@ function runLinksRule(ctx) {
31475
31556
  var linksRule = { id: "links", run: runLinksRule };
31476
31557
 
31477
31558
  // src/audit/rules/near-duplicate.ts
31478
- import { readFileSync as readFileSync14 } from "node:fs";
31479
- import { join as join12 } from "node:path";
31559
+ import { readFileSync as readFileSync15 } from "node:fs";
31560
+ import { join as join13 } from "node:path";
31480
31561
 
31481
31562
  // src/audit/core/ssot-fit.ts
31482
31563
  var DEFAULT_SSOT_OVERLAP_MIN = 0.35;
@@ -31764,7 +31845,7 @@ function runNearDuplicateRule(ctx) {
31764
31845
  const ignored = ignoredPairSet(ctx);
31765
31846
  const entries = eligibleEntries(ctx);
31766
31847
  const fingerprints = entries.map((e) => {
31767
- const content3 = readFileSync14(join12(ctx.root, e.path), "utf8");
31848
+ const content3 = readFileSync15(join13(ctx.root, e.path), "utf8");
31768
31849
  const tokens = tokenize2(bodyWithoutSsotNoise(content3));
31769
31850
  return {
31770
31851
  path: e.path,
@@ -31859,12 +31940,104 @@ function runProsePolicyRule(ctx) {
31859
31940
  }
31860
31941
  var prosePolicyRule = { id: "prose-policy", run: runProsePolicyRule };
31861
31942
 
31943
+ // src/audit/core/review-coverage.ts
31944
+ var DEFAULT_REVIEW_COVERAGE_INCLUDE = [
31945
+ "**/*.{ts,tsx,js,jsx,mjs,cjs,py}",
31946
+ "package.json",
31947
+ "project.json"
31948
+ ];
31949
+ var DEFAULT_REVIEW_COVERAGE_EXCLUDE = [
31950
+ "**/__tests__/**",
31951
+ "**/*.test.*",
31952
+ "**/*.spec.*",
31953
+ "**/fixtures/**",
31954
+ "templates/**",
31955
+ "dist/**",
31956
+ "node_modules/**",
31957
+ ".git/**",
31958
+ ".skeleton/plugins/**"
31959
+ ];
31960
+ function reviewCoveragePatterns(config) {
31961
+ const configured = config.reviewCoverage;
31962
+ if (configured?.include && configured.include.length === 0) {
31963
+ return { include: [], exclude: [] };
31964
+ }
31965
+ return {
31966
+ include: configured?.include && configured.include.length > 0 ? configured.include : DEFAULT_REVIEW_COVERAGE_INCLUDE,
31967
+ exclude: [...DEFAULT_REVIEW_COVERAGE_EXCLUDE, ...configured?.exclude ?? []]
31968
+ };
31969
+ }
31970
+ function pathRequiresReviewCoverage(relPath2, config) {
31971
+ const { include, exclude } = reviewCoveragePatterns(config);
31972
+ if (include.length === 0)
31973
+ return false;
31974
+ const path3 = normalizeRelPath(relPath2);
31975
+ if (exclude.some((pattern) => matchesGlobScope(path3, pattern)))
31976
+ return false;
31977
+ return include.some((pattern) => matchesGlobScope(path3, pattern));
31978
+ }
31979
+ function collectReviewDependencyPatterns(input) {
31980
+ const patterns = new Set;
31981
+ const source = input.fileSource ?? "worktree";
31982
+ for (const abs of collectScanFiles(input.config, input.root, input.skillIndex)) {
31983
+ const rel = relPath(abs, input.root);
31984
+ const content3 = readRepoText(input.root, rel, source);
31985
+ if (content3 === null)
31986
+ continue;
31987
+ for (const pattern of reviewDependencyPatterns(content3))
31988
+ patterns.add(pattern);
31989
+ }
31990
+ return [...patterns].sort();
31991
+ }
31992
+ function pathHasReviewOwner(relPath2, patterns) {
31993
+ return patterns.some((pattern) => reviewDependencyMatchesPath(pattern, relPath2));
31994
+ }
31995
+ function collectReviewCoverageFiles(root2, config) {
31996
+ const { include, exclude } = reviewCoveragePatterns(config);
31997
+ if (include.length === 0)
31998
+ return [];
31999
+ const files = new Set;
32000
+ for (const pattern of include) {
32001
+ for (const match of globSync(pattern, {
32002
+ cwd: root2,
32003
+ onlyFiles: true,
32004
+ dot: true,
32005
+ ignore: exclude
32006
+ })) {
32007
+ const rel = normalizeRelPath(match);
32008
+ if (exclude.some((item) => matchesGlobScope(rel, item)))
32009
+ continue;
32010
+ files.add(rel);
32011
+ }
32012
+ }
32013
+ return [...files].sort();
32014
+ }
32015
+
32016
+ // src/audit/rules/review-coverage.ts
32017
+ function runReviewCoverageRule(ctx) {
32018
+ const patterns = collectReviewDependencyPatterns({
32019
+ root: ctx.root,
32020
+ config: ctx.config,
32021
+ skillIndex: ctx.skillIndex,
32022
+ fileSource: ctx.fileSource
32023
+ });
32024
+ return collectReviewCoverageFiles(ctx.root, ctx.config).filter((path3) => !pathHasReviewOwner(path3, patterns)).map((path3) => issue("review-coverage", path3, {
32025
+ code: "review-coverage-gap",
32026
+ message: "file has no owning document; add a review-deps path or glob on the paper that describes it"
32027
+ }));
32028
+ }
32029
+ var reviewCoverageRule = {
32030
+ id: "review-coverage",
32031
+ global: true,
32032
+ run: runReviewCoverageRule
32033
+ };
32034
+
31862
32035
  // src/audit/rules/review-deps.ts
31863
- import { relative as relative7 } from "node:path";
32036
+ import { relative as relative6 } from "node:path";
31864
32037
  function runReviewDepsRule(ctx) {
31865
32038
  const issues = [];
31866
32039
  for (const abs of collectScanFiles(ctx.config, ctx.root, ctx.skillIndex)) {
31867
- const path3 = normalizeRelPath(relative7(ctx.root, abs));
32040
+ const path3 = normalizeRelPath(relative6(ctx.root, abs));
31868
32041
  for (const marker of parseReviewDepsMarkers(readFileContent(abs))) {
31869
32042
  issues.push(...validateMarker(ctx.root, path3, marker.paths));
31870
32043
  }
@@ -31925,16 +32098,16 @@ function runScanRootsRule(ctx) {
31925
32098
  var scanRootsRule = { id: "scan-roots", run: runScanRootsRule };
31926
32099
 
31927
32100
  // src/audit/rules/skill-index.ts
31928
- import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync15 } from "node:fs";
31929
- import { join as join13, relative as relative8 } from "node:path";
32101
+ import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync16 } from "node:fs";
32102
+ import { join as join14, relative as relative7 } from "node:path";
31930
32103
  function walkSkillMarkdown(dir) {
31931
32104
  const files = [];
31932
- if (!existsSync16(dir))
32105
+ if (!existsSync17(dir))
31933
32106
  return files;
31934
32107
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
31935
32108
  if (entry.name.startsWith("."))
31936
32109
  continue;
31937
- const fullPath = join13(dir, entry.name);
32110
+ const fullPath = join14(dir, entry.name);
31938
32111
  if (entry.isDirectory()) {
31939
32112
  files.push(...walkSkillMarkdown(fullPath));
31940
32113
  continue;
@@ -31960,8 +32133,8 @@ function parseReadmeTaxonomySlugs(content3) {
31960
32133
  }
31961
32134
  function scanFileForSkillLinks(ctx, filePath, index2) {
31962
32135
  const issues = [];
31963
- const rel = relative8(ctx.root, filePath).replace(/\\/g, "/");
31964
- const content3 = readFileSync15(filePath, "utf8");
32136
+ const rel = relative7(ctx.root, filePath).replace(/\\/g, "/");
32137
+ const content3 = readFileSync16(filePath, "utf8");
31965
32138
  for (const match of content3.matchAll(SKILL_LINK_RE)) {
31966
32139
  const slug2 = match[1];
31967
32140
  if (!slug2)
@@ -31974,14 +32147,14 @@ function scanFileForSkillLinks(ctx, filePath, index2) {
31974
32147
  }
31975
32148
  function taxonomyIssuesForReadme(input) {
31976
32149
  const { ctx, index: index2, skillRoot, diskSlugs, nonPublic } = input;
31977
- const readmePath = join13(ctx.root, skillRoot.relPath, "README.md");
31978
- if (!existsSync16(readmePath))
32150
+ const readmePath = join14(ctx.root, skillRoot.relPath, "README.md");
32151
+ if (!existsSync17(readmePath))
31979
32152
  return [];
31980
- const readme = readFileSync15(readmePath, "utf8");
32153
+ const readme = readFileSync16(readmePath, "utf8");
31981
32154
  if (!readme.includes("## Taxonomy"))
31982
32155
  return [];
31983
32156
  const taxonomySlugs = parseReadmeTaxonomySlugs(readme);
31984
- const nestedSlugs = diskSlugs.filter((slug2) => existsSync16(join13(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
32157
+ const nestedSlugs = diskSlugs.filter((slug2) => existsSync17(join14(ctx.root, skillRoot.relPath, slug2, "SKILL.md")));
31985
32158
  const foreign = new Set(index2.foreignSlugs);
31986
32159
  const publicSlugs = nestedSlugs.filter((slug2) => !(nonPublic.has(slug2) || foreign.has(slug2)));
31987
32160
  const relReadme = `${skillRoot.relPath}/README.md`;
@@ -32014,10 +32187,10 @@ function slugsForRoot(skillRoot, index2, owned) {
32014
32187
  function auditSkillRoot(input) {
32015
32188
  const { ctx, index: index2, skillRoot, owned } = input;
32016
32189
  const issues = [];
32017
- const base = skillRoot.kind === "nested" ? join13(ctx.root, skillRoot.relPath) : ctx.root;
32190
+ const base = skillRoot.kind === "nested" ? join14(ctx.root, skillRoot.relPath) : ctx.root;
32018
32191
  for (const slug2 of slugsForRoot(skillRoot, index2, owned)) {
32019
- const skillDir = join13(base, slug2);
32020
- if (!existsSync16(skillDir))
32192
+ const skillDir = join14(base, slug2);
32193
+ if (!existsSync17(skillDir))
32021
32194
  continue;
32022
32195
  for (const skillMd of walkSkillMarkdown(skillDir)) {
32023
32196
  issues.push(...scanFileForSkillLinks(ctx, skillMd, index2));
@@ -32067,8 +32240,8 @@ function runSsotRule(ctx) {
32067
32240
  var ssotRule = { id: "ssot", run: runSsotRule };
32068
32241
 
32069
32242
  // src/audit/rules/ssot-summary.ts
32070
- import { readFileSync as readFileSync16 } from "node:fs";
32071
- import { join as join14 } from "node:path";
32243
+ import { readFileSync as readFileSync17 } from "node:fs";
32244
+ import { join as join15 } from "node:path";
32072
32245
  function runSsotSummaryRule(ctx) {
32073
32246
  const overlapMin = ctx.config.docsLint?.ssotOverlapMin ?? DEFAULT_SSOT_OVERLAP_MIN;
32074
32247
  const margin = ctx.config.docsLint?.ssotBetterMatchMargin ?? DEFAULT_BETTER_MATCH_MARGIN;
@@ -32076,7 +32249,7 @@ function runSsotSummaryRule(ctx) {
32076
32249
  const files = ctx.ssotEntries.map((entry) => ({
32077
32250
  path: entry.path,
32078
32251
  summary: entry.summary,
32079
- content: readFileSync16(join14(ctx.root, entry.path), "utf8")
32252
+ content: readFileSync17(join15(ctx.root, entry.path), "utf8")
32080
32253
  }));
32081
32254
  return evaluateSsotFit(files, {
32082
32255
  overlapMin,
@@ -32097,6 +32270,7 @@ var docsRules = [
32097
32270
  { ...nearDuplicateRule, global: true },
32098
32271
  { ...ssotSummaryRule, global: true },
32099
32272
  { ...coverageGapsRule, global: true },
32273
+ { ...reviewCoverageRule, global: true },
32100
32274
  linksRule,
32101
32275
  docMetaRule,
32102
32276
  reviewProofRule,
@@ -32263,14 +32437,7 @@ function labelForSuite(suite) {
32263
32437
  function catalogStatusFor(root2, suite) {
32264
32438
  if (suite !== "docs" && suite !== "self")
32265
32439
  return "not-applicable";
32266
- if (process4.env.CI === "true")
32267
- return "skipped-ci";
32268
- const result = checkCatalog(root2);
32269
- if (result.missing)
32270
- return "missing";
32271
- if (result.stale)
32272
- return "stale";
32273
- return "current";
32440
+ return refreshLocalCatalog(root2);
32274
32441
  }
32275
32442
  function buildAuditResult(input) {
32276
32443
  const diagnostics = finalizeIssues(input.issues, input.options.strict);
@@ -32366,7 +32533,8 @@ async function evaluateAudit(options) {
32366
32533
  const base = createContext({
32367
32534
  root: options.root,
32368
32535
  paths: pathScoped ? options.paths : undefined,
32369
- includeExcludedSkillTrees: options.suite === "skills" && !pathScoped
32536
+ includeExcludedSkillTrees: options.suite === "skills" && !pathScoped,
32537
+ fileSource: options.fileSource
32370
32538
  });
32371
32539
  const loaded = await loadPlugins(base.root, base.config);
32372
32540
  const ctx = { ...base, policies: loaded.policies };
@@ -32408,11 +32576,6 @@ async function evaluateAudit(options) {
32408
32576
  for (const rule of executableRules) {
32409
32577
  issues.push(...rule.run(ctx));
32410
32578
  }
32411
- if (options.suite === "docs" || options.suite === "self") {
32412
- for (const message of catalogAuditWarnings(ctx.root)) {
32413
- issues.push(issue("catalog", CATALOG_REL_PATH, { message, severity: "warning" }));
32414
- }
32415
- }
32416
32579
  const executed = executableRules.map((rule) => rule.id);
32417
32580
  return buildAuditResult({
32418
32581
  options,
@@ -32442,20 +32605,20 @@ async function runAudit(options) {
32442
32605
  }
32443
32606
 
32444
32607
  // src/customize/resolve.ts
32445
- import { existsSync as existsSync17, readFileSync as readFileSync17 } from "node:fs";
32446
- import { basename as basename3, join as join15, relative as relative9 } from "node:path";
32608
+ import { existsSync as existsSync18, readFileSync as readFileSync18 } from "node:fs";
32609
+ import { basename as basename3, join as join16, relative as relative8 } from "node:path";
32447
32610
  function customizeDir(root2) {
32448
- return join15(root2, REGISTRY_DIR_REL, "customize");
32611
+ return join16(root2, REGISTRY_DIR_REL, "customize");
32449
32612
  }
32450
32613
  function customizePathForSlug(root2, slug2) {
32451
- return join15(customizeDir(root2), `${slug2}.md`);
32614
+ return join16(customizeDir(root2), `${slug2}.md`);
32452
32615
  }
32453
32616
  function resolveSlugFile(root2, slug2) {
32454
32617
  const direct = customizePathForSlug(root2, slug2);
32455
- if (existsSync17(direct)) {
32618
+ if (existsSync18(direct)) {
32456
32619
  return {
32457
- content: readFileSync17(direct, "utf8"),
32458
- path: normalizeRelPath(relative9(root2, direct))
32620
+ content: readFileSync18(direct, "utf8"),
32621
+ path: normalizeRelPath(relative8(root2, direct))
32459
32622
  };
32460
32623
  }
32461
32624
  return { content: null, path: null };
@@ -32476,11 +32639,11 @@ function readAlwaysInclude(root2, basenames, skipBasename) {
32476
32639
  const file = basename3(name);
32477
32640
  if (skipBasename && file === skipBasename)
32478
32641
  continue;
32479
- const abs = join15(dir, file);
32480
- if (!existsSync17(abs))
32642
+ const abs = join16(dir, file);
32643
+ if (!existsSync18(abs))
32481
32644
  continue;
32482
- parts.push(readFileSync17(abs, "utf8").trimEnd());
32483
- paths.push(normalizeRelPath(relative9(root2, abs)));
32645
+ parts.push(readFileSync18(abs, "utf8").trimEnd());
32646
+ paths.push(normalizeRelPath(relative8(root2, abs)));
32484
32647
  }
32485
32648
  return { parts, paths };
32486
32649
  }
@@ -32519,7 +32682,7 @@ function resolveCustomizeFromRoot(slug2, startDir) {
32519
32682
  }
32520
32683
 
32521
32684
  // src/hooks/run.ts
32522
- import process5 from "node:process";
32685
+ import process4 from "node:process";
32523
32686
  function parsePayload(raw) {
32524
32687
  if (!raw.trim())
32525
32688
  return {};
@@ -32550,7 +32713,7 @@ function extractSkillSlug(payload) {
32550
32713
  }
32551
32714
  const path3 = extractPath(payload);
32552
32715
  if (path3)
32553
- return slugFromPath(path3, process5.cwd());
32716
+ return slugFromPath(path3, process4.cwd());
32554
32717
  return null;
32555
32718
  }
32556
32719
  function cursorResponse(content3) {
@@ -32602,40 +32765,40 @@ Customize override for /${slug2} (from ${from}):
32602
32765
  }
32603
32766
 
32604
32767
  // src/init/init.ts
32605
- import { spawnSync as spawnSync2 } from "node:child_process";
32606
- import { copyFileSync, existsSync as existsSync21, mkdirSync as mkdirSync4, readFileSync as readFileSync19 } from "node:fs";
32607
- import { join as join19 } from "node:path";
32608
- import process6 from "node:process";
32768
+ import { spawnSync as spawnSync3 } from "node:child_process";
32769
+ import { copyFileSync, existsSync as existsSync23, mkdirSync as mkdirSync4, readFileSync as readFileSync21 } from "node:fs";
32770
+ import { join as join21 } from "node:path";
32771
+ import process5 from "node:process";
32609
32772
 
32610
32773
  // src/init/merge-hooks.ts
32611
- import { existsSync as existsSync20, mkdirSync as mkdirSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync4 } from "node:fs";
32612
- import { dirname as dirname12, join as join18 } from "node:path";
32774
+ import { existsSync as existsSync21, mkdirSync as mkdirSync3, readFileSync as readFileSync19, writeFileSync as writeFileSync4 } from "node:fs";
32775
+ import { dirname as dirname12, join as join19 } from "node:path";
32613
32776
 
32614
32777
  // src/init/package-paths.ts
32615
- import { existsSync as existsSync18 } from "node:fs";
32616
- import { dirname as dirname10, join as join16 } from "node:path";
32778
+ import { existsSync as existsSync19 } from "node:fs";
32779
+ import { dirname as dirname10, join as join17 } from "node:path";
32617
32780
  import { fileURLToPath as fileURLToPath4 } from "node:url";
32618
32781
  var MODULE_DIR = dirname10(fileURLToPath4(import.meta.url));
32619
- var PACKAGE_ROOT_CANDIDATES = [join16(MODULE_DIR, "../.."), join16(MODULE_DIR, "..")];
32782
+ var PACKAGE_ROOT_CANDIDATES = [join17(MODULE_DIR, "../.."), join17(MODULE_DIR, "..")];
32620
32783
  function resolvePackageRoot() {
32621
32784
  for (const candidate of PACKAGE_ROOT_CANDIDATES) {
32622
- if (existsSync18(join16(candidate, "package.json")))
32785
+ if (existsSync19(join17(candidate, "package.json")))
32623
32786
  return candidate;
32624
32787
  }
32625
32788
  throw new Error("Could not resolve @csark0812/skeleton package root");
32626
32789
  }
32627
32790
  function resolveTemplatesDir() {
32628
- const dir = join16(resolvePackageRoot(), "templates/skeleton-init");
32629
- if (!existsSync18(dir)) {
32791
+ const dir = join17(resolvePackageRoot(), "templates/skeleton-init");
32792
+ if (!existsSync19(dir)) {
32630
32793
  throw new Error("Missing templates/skeleton-init in package");
32631
32794
  }
32632
32795
  return dir;
32633
32796
  }
32634
32797
 
32635
32798
  // src/init/resolve-hook-command.ts
32636
- import { existsSync as existsSync19, realpathSync as realpathSync5 } from "node:fs";
32799
+ import { existsSync as existsSync20, realpathSync as realpathSync5 } from "node:fs";
32637
32800
  import { createRequire as createRequire3 } from "node:module";
32638
- import { dirname as dirname11, join as join17, relative as relative10, resolve as resolve6 } from "node:path";
32801
+ import { dirname as dirname11, join as join18, relative as relative9, resolve as resolve6 } from "node:path";
32639
32802
  var PACKAGE_NAME = "@csark0812/skeleton";
32640
32803
  var CLI_DIST = "dist/cli.js";
32641
32804
  var PACKAGE_ROOT = resolvePackageRoot();
@@ -32649,12 +32812,12 @@ function safeRealpath2(path3) {
32649
32812
  }
32650
32813
  }
32651
32814
  function toRepoRelative(cwd, absPath) {
32652
- const rel = relative10(safeRealpath2(cwd), safeRealpath2(absPath)).replace(/\\/g, "/");
32815
+ const rel = relative9(safeRealpath2(cwd), safeRealpath2(absPath)).replace(/\\/g, "/");
32653
32816
  return rel.startsWith("..") ? absPath.replace(/\\/g, "/") : rel;
32654
32817
  }
32655
32818
  function tryResolvePublishedCli(cwd) {
32656
32819
  try {
32657
- const req = createRequire3(join17(cwd, "package.json"));
32820
+ const req = createRequire3(join18(cwd, "package.json"));
32658
32821
  return req.resolve(`${PACKAGE_NAME}/${CLI_DIST}`);
32659
32822
  } catch {
32660
32823
  return null;
@@ -32663,8 +32826,8 @@ function tryResolvePublishedCli(cwd) {
32663
32826
  function walkNodeModulesCli(cwd) {
32664
32827
  let dir = cwd;
32665
32828
  while (true) {
32666
- const candidate = join17(dir, "node_modules", PACKAGE_NAME, CLI_DIST);
32667
- if (existsSync19(candidate))
32829
+ const candidate = join18(dir, "node_modules", PACKAGE_NAME, CLI_DIST);
32830
+ if (existsSync20(candidate))
32668
32831
  return candidate;
32669
32832
  const parent = dirname11(dir);
32670
32833
  if (parent === dir)
@@ -32674,7 +32837,7 @@ function walkNodeModulesCli(cwd) {
32674
32837
  return null;
32675
32838
  }
32676
32839
  function isInsidePackageRoot(cwd) {
32677
- const rel = relative10(PACKAGE_ROOT, resolve6(cwd)).replace(/\\/g, "/");
32840
+ const rel = relative9(PACKAGE_ROOT, resolve6(cwd)).replace(/\\/g, "/");
32678
32841
  return rel === "" || !(rel.startsWith("..") || rel.startsWith("/"));
32679
32842
  }
32680
32843
  function nodeCliHookCommand(cliPath) {
@@ -32705,14 +32868,14 @@ function identityKey(platform, event, matcher) {
32705
32868
  return `skeleton:customize:${platform}:${event}:${matcher}`;
32706
32869
  }
32707
32870
  function loadFragment(name, hookCommand) {
32708
- const raw = readFileSync18(join18(TEMPLATES_DIR, name), "utf8");
32871
+ const raw = readFileSync19(join19(TEMPLATES_DIR, name), "utf8");
32709
32872
  return JSON.parse(raw.replaceAll("{{HOOK_COMMAND}}", hookCommand));
32710
32873
  }
32711
32874
  function readJson(path3) {
32712
- if (!existsSync20(path3))
32875
+ if (!existsSync21(path3))
32713
32876
  return null;
32714
32877
  try {
32715
- return JSON.parse(readFileSync18(path3, "utf8"));
32878
+ return JSON.parse(readFileSync19(path3, "utf8"));
32716
32879
  } catch (error) {
32717
32880
  throw new Error(`Invalid JSON in ${path3}: ${error}`);
32718
32881
  }
@@ -32872,10 +33035,10 @@ function mergeNestedHooks(args) {
32872
33035
  }
32873
33036
  function mergeHookConfigs(opts) {
32874
33037
  const results = [];
32875
- const cursorPath = join18(opts.cwd, ".cursor/hooks.json");
33038
+ const cursorPath = join19(opts.cwd, ".cursor/hooks.json");
32876
33039
  const cursorFragment = loadFragment("cursor-hooks.fragment.json", opts.hookCommand);
32877
33040
  results.push(mergeCursorHooks(cursorPath, cursorFragment, opts));
32878
- const claudePath = join18(opts.cwd, ".claude/settings.json");
33041
+ const claudePath = join19(opts.cwd, ".claude/settings.json");
32879
33042
  const claudeFragment = loadFragment("claude-settings.fragment.json", opts.hookCommand);
32880
33043
  results.push(mergeNestedHooks({
32881
33044
  platform: "claude",
@@ -32884,8 +33047,8 @@ function mergeHookConfigs(opts) {
32884
33047
  eventName: "PostToolUse",
32885
33048
  opts
32886
33049
  }));
32887
- const codexPath = join18(opts.cwd, ".codex/hooks.json");
32888
- if (existsSync20(join18(opts.cwd, ".codex"))) {
33050
+ const codexPath = join19(opts.cwd, ".codex/hooks.json");
33051
+ if (existsSync21(join19(opts.cwd, ".codex"))) {
32889
33052
  const codexFragment = loadFragment("codex-hooks.fragment.json", opts.hookCommand);
32890
33053
  results.push(mergeNestedHooks({
32891
33054
  platform: "codex",
@@ -32900,11 +33063,11 @@ function mergeHookConfigs(opts) {
32900
33063
  return results;
32901
33064
  }
32902
33065
  function mergePackageJsonScripts(cwd) {
32903
- const pkgPath = join18(cwd, "package.json");
32904
- if (!existsSync20(pkgPath))
33066
+ const pkgPath = join19(cwd, "package.json");
33067
+ if (!existsSync21(pkgPath))
32905
33068
  return "skipped";
32906
- const fragment = JSON.parse(readFileSync18(join18(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"));
32907
- const pkg = JSON.parse(readFileSync18(pkgPath, "utf8"));
33069
+ const fragment = JSON.parse(readFileSync19(join19(TEMPLATES_DIR, "package.json.scripts.fragment.json"), "utf8"));
33070
+ const pkg = JSON.parse(readFileSync19(pkgPath, "utf8"));
32908
33071
  pkg.scripts ??= {};
32909
33072
  let changed = false;
32910
33073
  for (const [key, value] of Object.entries(fragment)) {
@@ -32920,6 +33083,37 @@ function mergePackageJsonScripts(cwd) {
32920
33083
  return "updated";
32921
33084
  }
32922
33085
 
33086
+ // src/init/merge-precommit.ts
33087
+ import { existsSync as existsSync22, readFileSync as readFileSync20, writeFileSync as writeFileSync5 } from "node:fs";
33088
+ import { join as join20 } from "node:path";
33089
+ var TEMPLATES_DIR2 = resolveTemplatesDir();
33090
+ var PRECOMMIT_NAME = ".pre-commit-config.yaml";
33091
+ var HOOK_ID = "id: skeleton-validate-staged";
33092
+ var LOCAL_HOOK_BLOCK = ` - repo: local
33093
+ hooks:
33094
+ - id: skeleton-validate-staged
33095
+ name: skeleton validate changed (staged)
33096
+ entry: node node_modules/@csark0812/skeleton/dist/cli.js validate changed --staged
33097
+ language: system
33098
+ pass_filenames: false
33099
+ `;
33100
+ function mergePrecommitConfig(cwd) {
33101
+ const target = join20(cwd, PRECOMMIT_NAME);
33102
+ const template = readFileSync20(join20(TEMPLATES_DIR2, "pre-commit-config.yaml"), "utf8");
33103
+ if (!existsSync22(target)) {
33104
+ writeFileSync5(target, template, "utf8");
33105
+ return "added";
33106
+ }
33107
+ const existing = readFileSync20(target, "utf8");
33108
+ if (existing.includes(HOOK_ID))
33109
+ return "skipped";
33110
+ const suffix = existing.includes("repos:") ? `
33111
+ ${LOCAL_HOOK_BLOCK}` : `
33112
+ ${template}`;
33113
+ writeFileSync5(target, `${existing.trimEnd()}${suffix}`, "utf8");
33114
+ return "updated";
33115
+ }
33116
+
32923
33117
  // src/init/skills-args.ts
32924
33118
  var SKILLS_SOURCE = "csark0812/skeleton";
32925
33119
  var DEFAULT_SKILL = "skeleton";
@@ -32958,25 +33152,25 @@ function skillsAddArgs(options = {}) {
32958
33152
  }
32959
33153
 
32960
33154
  // src/init/init.ts
32961
- var TEMPLATES_DIR2 = resolveTemplatesDir();
33155
+ var TEMPLATES_DIR3 = resolveTemplatesDir();
32962
33156
  function writeScaffold(cwd) {
32963
- const skeletonDir2 = join19(cwd, ".skeleton");
33157
+ const skeletonDir2 = join21(cwd, ".skeleton");
32964
33158
  mkdirSync4(skeletonDir2, { recursive: true });
32965
33159
  let created = false;
32966
- const tomlPath = join19(cwd, "skeleton.toml");
32967
- const legacyYaml = join19(skeletonDir2, "config.yaml");
32968
- if (!(existsSync21(tomlPath) || existsSync21(legacyYaml))) {
32969
- copyFileSync(join19(TEMPLATES_DIR2, "skeleton.toml"), tomlPath);
33160
+ const tomlPath = join21(cwd, "skeleton.toml");
33161
+ const legacyYaml = join21(skeletonDir2, "config.yaml");
33162
+ if (!(existsSync23(tomlPath) || existsSync23(legacyYaml))) {
33163
+ copyFileSync(join21(TEMPLATES_DIR3, "skeleton.toml"), tomlPath);
32970
33164
  created = true;
32971
33165
  }
32972
- mkdirSync4(join19(skeletonDir2, "customize"), { recursive: true });
33166
+ mkdirSync4(join21(skeletonDir2, "customize"), { recursive: true });
32973
33167
  return created ? "created" : "skipped";
32974
33168
  }
32975
33169
  function assertPackageResolvable(cwd) {
32976
- const pkgPath = join19(cwd, "package.json");
32977
- if (!existsSync21(pkgPath))
33170
+ const pkgPath = join21(cwd, "package.json");
33171
+ if (!existsSync23(pkgPath))
32978
33172
  return;
32979
- const pkg = JSON.parse(readFileSync19(pkgPath, "utf8"));
33173
+ const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
32980
33174
  const hasDep = pkg.devDependencies?.["@csark0812/skeleton"] || pkg.dependencies?.["@csark0812/skeleton"];
32981
33175
  if (!hasDep) {
32982
33176
  try {
@@ -32987,7 +33181,7 @@ function assertPackageResolvable(cwd) {
32987
33181
  }
32988
33182
  }
32989
33183
  function runSkillsAdd(args, cwd) {
32990
- const result = spawnSync2("npx", args, {
33184
+ const result = spawnSync3("npx", args, {
32991
33185
  cwd,
32992
33186
  stdio: "inherit",
32993
33187
  shell: false
@@ -33016,24 +33210,30 @@ function installSkillsIfRequested(options, cwd) {
33016
33210
  return "installed";
33017
33211
  }
33018
33212
  function runInit(options = {}) {
33019
- const cwd = options.cwd ?? process6.cwd();
33213
+ const cwd = options.cwd ?? process5.cwd();
33020
33214
  assertPackageResolvable(cwd);
33021
33215
  const scaffold = writeScaffold(cwd);
33022
33216
  const hookCommand = resolveHookCommand(cwd);
33023
33217
  const hooks = mergeHookConfigs({ cwd, hookCommand, forceHooks: options.forceHooks });
33024
33218
  const scripts = mergePackageJsonScripts(cwd);
33219
+ const precommit = mergePrecommitConfig(cwd);
33025
33220
  for (const result of hooks)
33026
33221
  logHookMergeResult(result);
33027
33222
  if (scaffold === "created") {
33028
- console.log("init: wrote skeleton.toml (hooks optional — see docs)");
33223
+ console.log("init: wrote skeleton.toml (IDE customize hooks optional)");
33029
33224
  } else {
33030
33225
  console.log("init: skeleton.toml or .skeleton/ already present — skipped scaffold write");
33031
33226
  }
33032
33227
  if (scripts === "updated") {
33033
33228
  console.log("init: merged validate/audit scripts into package.json");
33034
33229
  }
33230
+ if (precommit === "added") {
33231
+ console.log("init: wrote .pre-commit-config.yaml (run pre-commit install once per machine)");
33232
+ } else if (precommit === "updated") {
33233
+ console.log("init: added skeleton validate hook to .pre-commit-config.yaml");
33234
+ }
33035
33235
  const skills = installSkillsIfRequested(options, cwd);
33036
- return { scaffold, hooks, scripts, skills };
33236
+ return { scaffold, hooks, scripts, skills, precommit };
33037
33237
  }
33038
33238
 
33039
33239
  // src/init/parse-args.ts
@@ -33058,9 +33258,9 @@ function parseInitArgs(argv) {
33058
33258
  }
33059
33259
 
33060
33260
  // src/plugins/build.ts
33061
- import { spawnSync as spawnSync3 } from "node:child_process";
33261
+ import { spawnSync as spawnSync4 } from "node:child_process";
33062
33262
  import { createHash as createHash2 } from "node:crypto";
33063
- import { existsSync as existsSync22, readFileSync as readFileSync20, writeFileSync as writeFileSync5 } from "node:fs";
33263
+ import { existsSync as existsSync24, readFileSync as readFileSync22, writeFileSync as writeFileSync6 } from "node:fs";
33064
33264
  import { basename as basename4, dirname as dirname13, resolve as resolve7 } from "node:path";
33065
33265
  function parseBuildPluginArgs(argv) {
33066
33266
  let check = false;
@@ -33101,7 +33301,7 @@ function localImportPaths(tsAbs, content3) {
33101
33301
  candidates.push(resolve7(dir, spec), resolve7(dir, `${spec}.ts`), resolve7(dir, `${spec}.js`), resolve7(dir, spec, "index.ts"));
33102
33302
  }
33103
33303
  for (const candidate of candidates) {
33104
- if (existsSync22(candidate) && candidate.endsWith(".ts")) {
33304
+ if (existsSync24(candidate) && candidate.endsWith(".ts")) {
33105
33305
  deps.push(candidate);
33106
33306
  break;
33107
33307
  }
@@ -33118,7 +33318,7 @@ function sourceFingerprint(tsAbs, seen = new Set) {
33118
33318
  if (seen.has(abs))
33119
33319
  return;
33120
33320
  seen.add(abs);
33121
- const content3 = readFileSync20(abs, "utf8");
33321
+ const content3 = readFileSync22(abs, "utf8");
33122
33322
  hash2.update(basename4(abs));
33123
33323
  hash2.update("\x00");
33124
33324
  hash2.update(content3);
@@ -33131,15 +33331,15 @@ function sourceFingerprint(tsAbs, seen = new Set) {
33131
33331
  return hash2.digest("hex");
33132
33332
  }
33133
33333
  function writeStamp(tsAbs, mjsAbs) {
33134
- writeFileSync5(stampPathForMjs(mjsAbs), `${sourceFingerprint(tsAbs)}
33334
+ writeFileSync6(stampPathForMjs(mjsAbs), `${sourceFingerprint(tsAbs)}
33135
33335
  `, "utf8");
33136
33336
  }
33137
33337
  async function buildOne(tsAbs) {
33138
33338
  const mjsAbs = mjsPathForTs(tsAbs);
33139
- if (!existsSync22(tsAbs)) {
33339
+ if (!existsSync24(tsAbs)) {
33140
33340
  throw new Error(`Plugin source not found: ${tsAbs}`);
33141
33341
  }
33142
- const proc = spawnSync3("bun", ["build", tsAbs, "--target=node", "--format=esm", `--outfile=${mjsAbs}`, "--packages=external"], { encoding: "utf8" });
33342
+ const proc = spawnSync4("bun", ["build", tsAbs, "--target=node", "--format=esm", `--outfile=${mjsAbs}`, "--packages=external"], { encoding: "utf8" });
33143
33343
  if (proc.error) {
33144
33344
  const code3 = proc.error.code;
33145
33345
  if (code3 === "ENOENT") {
@@ -33156,17 +33356,17 @@ ${proc.stderr || proc.stdout || `exit ${proc.status}`}`);
33156
33356
  }
33157
33357
  function checkOne(tsAbs) {
33158
33358
  const mjsAbs = mjsPathForTs(tsAbs);
33159
- if (!existsSync22(mjsAbs)) {
33359
+ if (!existsSync24(mjsAbs)) {
33160
33360
  throw new Error(`Plugin not built: ${tsAbs} (missing ${mjsAbs}). Run: skeleton build-plugin`);
33161
33361
  }
33162
- if (!existsSync22(tsAbs)) {
33362
+ if (!existsSync24(tsAbs)) {
33163
33363
  throw new Error(`Plugin source not found: ${tsAbs}`);
33164
33364
  }
33165
33365
  const stampAbs = stampPathForMjs(mjsAbs);
33166
- if (!existsSync22(stampAbs)) {
33366
+ if (!existsSync24(stampAbs)) {
33167
33367
  throw new Error(`Plugin stale: ${mjsAbs} has no fingerprint stamp. Run: skeleton build-plugin`);
33168
33368
  }
33169
- const expected = readFileSync20(stampAbs, "utf8").trim();
33369
+ const expected = readFileSync22(stampAbs, "utf8").trim();
33170
33370
  const actual = sourceFingerprint(tsAbs);
33171
33371
  if (expected !== actual) {
33172
33372
  throw new Error(`Plugin stale: ${mjsAbs} does not match ${tsAbs} (or local imports). Run: skeleton build-plugin`);
@@ -33195,15 +33395,15 @@ async function runBuildPlugin(options = {}) {
33195
33395
  }
33196
33396
 
33197
33397
  // src/validate/changed.ts
33198
- import { spawnSync as spawnSync5 } from "node:child_process";
33199
- import { existsSync as existsSync23, readFileSync as readFileSync21 } from "node:fs";
33200
- import { basename as basename5, extname as extname2, join as join20 } from "node:path";
33398
+ import { spawnSync as spawnSync6 } from "node:child_process";
33399
+ import { existsSync as existsSync25, readFileSync as readFileSync23 } from "node:fs";
33400
+ import { basename as basename5, extname as extname2, join as join22 } from "node:path";
33201
33401
 
33202
33402
  // src/validate/git-diff.ts
33203
- import { spawnSync as spawnSync4 } from "node:child_process";
33403
+ import { spawnSync as spawnSync5 } from "node:child_process";
33204
33404
  function gitDiffChangedFiles(options = {}) {
33205
33405
  const root2 = options.root ?? findRepoRoot();
33206
- const proc = spawnSync4("git", gitDiffArgs(options), { cwd: root2, encoding: "utf8" });
33406
+ const proc = spawnSync5("git", gitDiffArgs(options), { cwd: root2, encoding: "utf8" });
33207
33407
  if (proc.status !== 0) {
33208
33408
  throw new Error(proc.stderr?.trim() || "git diff failed");
33209
33409
  }
@@ -33239,6 +33439,32 @@ function addChangedPath(input) {
33239
33439
  });
33240
33440
  }
33241
33441
 
33442
+ // src/validate/staged.ts
33443
+ function stageRequiredIssue(file) {
33444
+ return issue("validate-changed", file, {
33445
+ code: "stage-required",
33446
+ message: "worktree differs from HEAD and is not staged; stage the attested document and review-lock.json when hash mode is on"
33447
+ });
33448
+ }
33449
+ function stageRequiredDiagnostics(input) {
33450
+ if (!input.staged)
33451
+ return [];
33452
+ const staged = new Set(input.stagedPaths.map(normalizeRelPath));
33453
+ const required = [...input.impactedDocuments];
33454
+ if (input.config.reviewProof) {
33455
+ required.push(input.config.reviewProof.lockfile ?? DEFAULT_REVIEW_LOCKFILE);
33456
+ }
33457
+ const issues = [];
33458
+ for (const path3 of [...new Set(required)].sort()) {
33459
+ if (staged.has(path3))
33460
+ continue;
33461
+ if (!pathDiffersFromHead(input.root, path3))
33462
+ continue;
33463
+ issues.push(stageRequiredIssue(path3));
33464
+ }
33465
+ return issues;
33466
+ }
33467
+
33242
33468
  // src/validate/changed.ts
33243
33469
  var DOC_EXTENSIONS = new Set([".md", ".mdc", ".yaml", ".yml"]);
33244
33470
  var POLICY_EXTENSIONS = new Set([".yaml", ".yml"]);
@@ -33314,30 +33540,51 @@ function parseJsonContent(content3) {
33314
33540
  function validationIssue(code3, file, message) {
33315
33541
  return issue("validate-changed", file, { code: code3, message, severity: "error" });
33316
33542
  }
33317
- function validateJson(relPath2, root2) {
33318
- const abs = join20(root2, relPath2);
33543
+ function rereadValidationIssue(file, target) {
33544
+ return issue("validate-changed", file, {
33545
+ code: "impacted-document-review-required",
33546
+ message: "a linked review dependency changed; re-read the entire document, then attest it with --fix=doc-meta --confirm-reviewed and include the document in validation",
33547
+ severity: "error",
33548
+ link: target
33549
+ });
33550
+ }
33551
+ function validateJson(relPath2, root2, fileSource) {
33552
+ const content3 = readRepoText(root2, relPath2, fileSource);
33553
+ if (content3 === null)
33554
+ return validationIssue("invalid-json", relPath2, "path not found");
33319
33555
  try {
33320
- parseJsonContent(readFileSync21(abs, "utf8"));
33556
+ parseJsonContent(content3);
33321
33557
  return null;
33322
33558
  } catch (error) {
33323
33559
  return validationIssue("invalid-json", relPath2, `invalid JSON: ${error}`);
33324
33560
  }
33325
33561
  }
33326
- function validatePolicy(relPath2, root2) {
33327
- const abs = join20(root2, relPath2);
33562
+ function validatePolicy(relPath2, root2, fileSource) {
33563
+ const content3 = readRepoText(root2, relPath2, fileSource);
33564
+ if (content3 === null)
33565
+ return validationIssue("invalid-policy", relPath2, "path not found");
33328
33566
  try {
33329
- loadPolicyFile(abs, readFileSync21(abs, "utf8"));
33567
+ loadPolicyFile(join22(root2, relPath2), content3);
33330
33568
  return null;
33331
33569
  } catch (error) {
33332
33570
  return validationIssue("invalid-policy", relPath2, `invalid policy: ${error}`);
33333
33571
  }
33334
33572
  }
33335
- function validateShell(relPath2, root2) {
33336
- const abs = join20(root2, relPath2);
33337
- const shellcheck = spawnSync5("shellcheck", [abs], { encoding: "utf8" });
33573
+ function validateShell(relPath2, root2, fileSource) {
33574
+ if (fileSource === "index") {
33575
+ const content3 = readRepoText(root2, relPath2, fileSource);
33576
+ if (content3 === null)
33577
+ return validationIssue("invalid-shell", relPath2, "path not found");
33578
+ const bash2 = spawnSync6("bash", ["-n"], { input: content3, encoding: "utf8" });
33579
+ if (bash2.status === 0)
33580
+ return null;
33581
+ return validationIssue("invalid-shell", relPath2, `shell syntax check failed: ${bash2.stderr}`);
33582
+ }
33583
+ const abs = join22(root2, relPath2);
33584
+ const shellcheck = spawnSync6("shellcheck", [abs], { encoding: "utf8" });
33338
33585
  if (shellcheck.status === 0)
33339
33586
  return null;
33340
- const bash = spawnSync5("bash", ["-n", abs], { encoding: "utf8" });
33587
+ const bash = spawnSync6("bash", ["-n", abs], { encoding: "utf8" });
33341
33588
  if (bash.status === 0)
33342
33589
  return null;
33343
33590
  return validationIssue("invalid-shell", relPath2, `shell syntax check failed: ${bash.stderr || shellcheck.stderr}`);
@@ -33357,11 +33604,11 @@ function resolvePaths(options) {
33357
33604
  };
33358
33605
  }
33359
33606
  function packageManagerFromPackageJson(root2) {
33360
- const pkgPath = join20(root2, "package.json");
33361
- if (!existsSync23(pkgPath))
33607
+ const pkgPath = join22(root2, "package.json");
33608
+ if (!existsSync25(pkgPath))
33362
33609
  return null;
33363
33610
  try {
33364
- const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
33611
+ const pkg = JSON.parse(readFileSync23(pkgPath, "utf8"));
33365
33612
  const raw = pkg.packageManager?.split("@")[0];
33366
33613
  if (raw === "bun" || raw === "npm" || raw === "pnpm" || raw === "yarn")
33367
33614
  return raw;
@@ -33369,13 +33616,13 @@ function packageManagerFromPackageJson(root2) {
33369
33616
  return null;
33370
33617
  }
33371
33618
  function packageManagerFromLockfiles(root2) {
33372
- if (existsSync23(join20(root2, "bun.lock")) || existsSync23(join20(root2, "bun.lockb")))
33619
+ if (existsSync25(join22(root2, "bun.lock")) || existsSync25(join22(root2, "bun.lockb")))
33373
33620
  return "bun";
33374
- if (existsSync23(join20(root2, "pnpm-lock.yaml")))
33621
+ if (existsSync25(join22(root2, "pnpm-lock.yaml")))
33375
33622
  return "pnpm";
33376
- if (existsSync23(join20(root2, "yarn.lock")))
33623
+ if (existsSync25(join22(root2, "yarn.lock")))
33377
33624
  return "yarn";
33378
- if (existsSync23(join20(root2, "package-lock.json")))
33625
+ if (existsSync25(join22(root2, "package-lock.json")))
33379
33626
  return "npm";
33380
33627
  return null;
33381
33628
  }
@@ -33401,8 +33648,8 @@ function emptyBuckets() {
33401
33648
  function classifySinglePath(input) {
33402
33649
  const { relPath: relPath2, ctx, state, bucketCtx } = input;
33403
33650
  const normalized = normalizeRelPath(relPath2);
33404
- const abs = join20(ctx.root, normalized);
33405
- if (!existsSync23(abs)) {
33651
+ const abs = join22(ctx.root, normalized);
33652
+ if (!existsSync25(abs)) {
33406
33653
  state.missing.push(normalized);
33407
33654
  return;
33408
33655
  }
@@ -33440,20 +33687,20 @@ function classifyPaths(ctx) {
33440
33687
  }
33441
33688
  return state;
33442
33689
  }
33443
- function validateLocalBuckets(buckets, root2) {
33690
+ function validateLocalBuckets(buckets, root2, fileSource) {
33444
33691
  const diagnostics = [];
33445
33692
  for (const relPath2 of buckets.shell) {
33446
- const found = validateShell(relPath2, root2);
33693
+ const found = validateShell(relPath2, root2, fileSource);
33447
33694
  if (found)
33448
33695
  diagnostics.push(found);
33449
33696
  }
33450
33697
  for (const relPath2 of buckets.json) {
33451
- const found = validateJson(relPath2, root2);
33698
+ const found = validateJson(relPath2, root2, fileSource);
33452
33699
  if (found)
33453
33700
  diagnostics.push(found);
33454
33701
  }
33455
33702
  for (const relPath2 of buckets.policy) {
33456
- const found = validatePolicy(relPath2, root2);
33703
+ const found = validatePolicy(relPath2, root2, fileSource);
33457
33704
  if (found)
33458
33705
  diagnostics.push(found);
33459
33706
  }
@@ -33466,16 +33713,24 @@ function discoverImpactedDocuments(input) {
33466
33713
  const changed = new Set(input.relPaths.map(normalizeRelPath));
33467
33714
  const impacted = [];
33468
33715
  for (const abs of collectScanFiles(input.config, input.root, input.skillIndex)) {
33469
- const document4 = impactedDocumentForPath(abs, input.root, changed);
33716
+ const document4 = impactedDocumentForPath({
33717
+ abs,
33718
+ root: input.root,
33719
+ changed,
33720
+ fileSource: input.fileSource
33721
+ });
33470
33722
  if (document4)
33471
33723
  impacted.push(document4);
33472
33724
  }
33473
33725
  return impacted.sort((a, b) => a.path.localeCompare(b.path));
33474
33726
  }
33475
- function impactedDocumentForPath(abs, root2, changed) {
33476
- const path3 = relPath(abs, root2);
33477
- const reviewDependencies = reviewDependencyPatterns(readFileSync21(abs, "utf8"));
33478
- const reasons = impactReasons(path3, reviewDependencies, changed);
33727
+ function impactedDocumentForPath(input) {
33728
+ const path3 = relPath(input.abs, input.root);
33729
+ const content3 = readRepoText(input.root, path3, input.fileSource);
33730
+ if (content3 === null)
33731
+ return null;
33732
+ const reviewDependencies = reviewDependencyPatterns(content3);
33733
+ const reasons = impactReasons(path3, reviewDependencies, input.changed);
33479
33734
  return reasons.length > 0 ? { path: path3, reviewDependencies, reasons } : null;
33480
33735
  }
33481
33736
  function impactReasons(path3, reviewDependencies, changed) {
@@ -33494,18 +33749,35 @@ function dateModeImpactDiagnostics(input) {
33494
33749
  return [];
33495
33750
  const changed = new Set(input.relPaths.map(normalizeRelPath));
33496
33751
  const today = formatLocalReviewDate(new Date);
33497
- const diagnostics = [];
33498
- for (const impacted of input.impactedDocuments) {
33499
- if (!impacted.reasons.some((reason) => reason.kind === "changed-review-dependency"))
33500
- continue;
33501
- const content3 = readFileSync21(join20(input.root, impacted.path), "utf8");
33502
- if (changed.has(impacted.path) && docMetaLastReviewed(content3) === today)
33752
+ return input.impactedDocuments.flatMap((impacted) => dateModeIssuesForDocument({
33753
+ impacted,
33754
+ changed,
33755
+ today,
33756
+ root: input.root,
33757
+ fileSource: input.fileSource
33758
+ }));
33759
+ }
33760
+ function dateModeIssuesForDocument(input) {
33761
+ const content3 = readRepoText(input.root, input.impacted.path, input.fileSource);
33762
+ if (!content3) {
33763
+ return input.impacted.reasons.filter((reason) => reason.kind === "changed-review-dependency" && reason.target).map((reason) => rereadValidationIssue(input.impacted.path, reason.target ?? ""));
33764
+ }
33765
+ if (input.changed.has(input.impacted.path) && docMetaLastReviewed(content3) === input.today) {
33766
+ return [];
33767
+ }
33768
+ const issues = [];
33769
+ for (const reason of input.impacted.reasons) {
33770
+ if (reason.kind !== "changed-review-dependency" || !reason.target)
33503
33771
  continue;
33504
- diagnostics.push(validationIssue("impacted-document-review-required", impacted.path, "a linked review dependency changed; re-read the entire document, then attest it with --fix=doc-meta --confirm-reviewed and include the document in validation"));
33772
+ issues.push(rereadValidationIssue(input.impacted.path, reason.target));
33505
33773
  }
33506
- return diagnostics;
33774
+ return issues;
33775
+ }
33776
+ function uncoveredChangedPathDiagnostics(relPaths, config, patterns) {
33777
+ return relPaths.map(normalizeRelPath).filter((path3) => pathRequiresReviewCoverage(path3, config)).filter((path3) => !pathHasReviewOwner(path3, patterns)).map((path3) => validationIssue("uncovered-changed-path", path3, "no scanned document claims this path with review-deps. Add a review-deps marker on the owning paper."));
33507
33778
  }
33508
- function classificationDiagnostics(classification, root2, base) {
33779
+ function classificationDiagnostics(input) {
33780
+ const { classification, root: root2, base, coverageCandidateCount } = input;
33509
33781
  const diagnostics = [];
33510
33782
  for (const orphan of classification.orphans) {
33511
33783
  diagnostics.push(validationIssue("orphan-policy", orphan, "file is under .skeleton/ but is not referenced by any plugin policies glob; export it from a plugin policies array or move it"));
@@ -33514,7 +33786,7 @@ function classificationDiagnostics(classification, root2, base) {
33514
33786
  diagnostics.push(validationIssue("missing-path", missing, "path not found"));
33515
33787
  }
33516
33788
  const audited = auditedPathCount(classification.buckets);
33517
- if ((classification.skipped.length > 0 || classification.buckets.code.length > 0) && audited === 0 && !base) {
33789
+ if ((classification.skipped.length > 0 || classification.buckets.code.length > 0) && audited === 0 && coverageCandidateCount === 0 && !base) {
33518
33790
  diagnostics.push(validationIssue("all-paths-skipped", ".", `all paths were skipped (code/config). This does not verify application code.
33519
33791
  ${codeValidationHint(root2)}`));
33520
33792
  }
@@ -33545,35 +33817,54 @@ function auditOptions(suite, root2, paths, extra = {}) {
33545
33817
  ...extra
33546
33818
  };
33547
33819
  }
33820
+ async function auditSkillChanges(input) {
33821
+ const sourceOpt = { fileSource: input.fileSource };
33822
+ if (input.skills.length === 0)
33823
+ return [];
33824
+ if (!input.base) {
33825
+ return [await evaluateAudit(auditOptions("skills", input.root, [], sourceOpt))];
33826
+ }
33827
+ return [
33828
+ await evaluateAudit(auditOptions("skills", input.root, input.skills, {
33829
+ pathScopedOnly: true,
33830
+ ...sourceOpt
33831
+ }))
33832
+ ];
33833
+ }
33834
+ async function auditPolicyChanges(input) {
33835
+ const sourceOpt = { fileSource: input.fileSource };
33836
+ const audits = [await evaluateAudit(auditOptions("docs", input.root, [], sourceOpt))];
33837
+ const skillPaths = listSkillMarkdownPaths(input.root, input.skillIndex);
33838
+ if (skillPaths.length === 0)
33839
+ return audits;
33840
+ audits.push(await evaluateAudit(auditOptions("skills", input.root, skillPaths, {
33841
+ pathScopedOnly: true,
33842
+ ...sourceOpt
33843
+ })));
33844
+ return audits;
33845
+ }
33548
33846
  async function evaluateBucketAudits(input) {
33549
- const { classification, root: root2, skillIndex, base } = input;
33847
+ const { classification, root: root2, skillIndex, base, fileSource } = input;
33550
33848
  const audits = [];
33551
- const diagnostics = validateLocalBuckets(classification.buckets, root2);
33849
+ const diagnostics = validateLocalBuckets(classification.buckets, root2, fileSource);
33850
+ const sourceOpt = { fileSource };
33552
33851
  if (base) {
33553
- audits.push(await evaluateAudit(auditOptions("self", root2, [], { globalOnly: true })));
33852
+ audits.push(await evaluateAudit(auditOptions("self", root2, [], { globalOnly: true, ...sourceOpt })));
33554
33853
  }
33555
33854
  if (classification.buckets.docs.length > 0) {
33556
- audits.push(await evaluateAudit(auditOptions("docs", root2, classification.buckets.docs, { pathScopedOnly: true })));
33557
- }
33558
- if (classification.buckets.skills.length > 0) {
33559
- if (!base) {
33560
- diagnostics.push(validationIssue("full-skills-audit-required", classification.buckets.skills[0] ?? ".", "skill paths need the full skills suite; run skeleton audit skills (audit self covers docs and .skeleton; excluded skill trees still need audit skills)"));
33561
- } else {
33562
- audits.push(await evaluateAudit(auditOptions("skills", root2, classification.buckets.skills, {
33563
- pathScopedOnly: true
33564
- })));
33565
- }
33855
+ audits.push(await evaluateAudit(auditOptions("docs", root2, classification.buckets.docs, {
33856
+ pathScopedOnly: true,
33857
+ ...sourceOpt
33858
+ })));
33566
33859
  }
33567
- if (classification.buckets.policy.length > 0) {
33568
- if (!base) {
33569
- diagnostics.push(validationIssue("full-policy-audit-required", classification.buckets.policy[0] ?? ".", "policy YAML changes need full prose passes; run skeleton audit docs and skeleton audit skills"));
33570
- } else {
33571
- audits.push(await evaluateAudit(auditOptions("docs", root2, [])));
33572
- const skillPaths = listSkillMarkdownPaths(root2, skillIndex);
33573
- if (skillPaths.length > 0) {
33574
- audits.push(await evaluateAudit(auditOptions("skills", root2, skillPaths, { pathScopedOnly: true })));
33575
- }
33576
- }
33860
+ audits.push(...await auditSkillChanges({
33861
+ skills: classification.buckets.skills,
33862
+ root: root2,
33863
+ base,
33864
+ fileSource
33865
+ }));
33866
+ if (classification.buckets.policy.length > 0 && !diagnostics.some((item) => item.code === "invalid-policy")) {
33867
+ audits.push(...await auditPolicyChanges({ root: root2, skillIndex, fileSource }));
33577
33868
  }
33578
33869
  return { audits, diagnostics };
33579
33870
  }
@@ -33611,6 +33902,7 @@ function emptyClassification() {
33611
33902
  }
33612
33903
  async function evaluateValidateChanged(options = {}) {
33613
33904
  const root2 = options.root ?? findRepoRoot();
33905
+ refreshLocalCatalog(root2);
33614
33906
  const resolvedPaths = resolvePaths(options);
33615
33907
  const relPaths = resolvedPaths.paths;
33616
33908
  if (relPaths.length === 0) {
@@ -33618,6 +33910,7 @@ async function evaluateValidateChanged(options = {}) {
33618
33910
  }
33619
33911
  const config = loadConfig(root2);
33620
33912
  const skillIndex = buildSkillIndex(root2, config.skillOwnership);
33913
+ const fileSource = options.staged ? "index" : "worktree";
33621
33914
  let wiredPolicies;
33622
33915
  try {
33623
33916
  wiredPolicies = await collectWiredPolicyRelPaths(root2, config);
@@ -33638,37 +33931,57 @@ async function evaluateValidateChanged(options = {}) {
33638
33931
  skillIndex
33639
33932
  });
33640
33933
  classification.missing = classification.missing.filter((path3) => !resolvedPaths.deleted.has(path3));
33641
- const impactedDocuments = discoverImpactedDocuments({ relPaths, root: root2, config, skillIndex });
33934
+ const impactedDocuments = discoverImpactedDocuments({
33935
+ relPaths,
33936
+ root: root2,
33937
+ config,
33938
+ skillIndex,
33939
+ fileSource
33940
+ });
33642
33941
  for (const impacted of impactedDocuments) {
33643
33942
  if (!classification.buckets.docs.includes(impacted.path)) {
33644
33943
  classification.buckets.docs.push(impacted.path);
33645
33944
  }
33646
33945
  }
33647
33946
  classification.buckets.docs.sort();
33648
- const diagnostics = classificationDiagnostics(classification, root2, options.base);
33649
- if (diagnostics.length > 0) {
33650
- return resultFor({
33651
- options,
33652
- relPaths,
33653
- classification: publicClassification(classification),
33654
- impactedDocuments,
33655
- diagnostics
33656
- });
33657
- }
33947
+ const ownerPatterns = collectReviewDependencyPatterns({
33948
+ root: root2,
33949
+ config,
33950
+ skillIndex,
33951
+ fileSource
33952
+ });
33953
+ const coverageCandidateCount = relPaths.filter((path3) => pathRequiresReviewCoverage(path3, config)).length;
33954
+ const diagnostics = [
33955
+ ...classificationDiagnostics({
33956
+ classification,
33957
+ root: root2,
33958
+ base: options.base,
33959
+ coverageCandidateCount
33960
+ }),
33961
+ ...uncoveredChangedPathDiagnostics(relPaths, config, ownerPatterns),
33962
+ ...stageRequiredDiagnostics({
33963
+ staged: options.staged ?? false,
33964
+ stagedPaths: relPaths,
33965
+ impactedDocuments: impactedDocuments.map((item) => item.path),
33966
+ config,
33967
+ root: root2
33968
+ })
33969
+ ];
33658
33970
  const evaluated = await evaluateBucketAudits({
33659
33971
  classification,
33660
33972
  root: root2,
33661
33973
  skillIndex,
33662
- base: options.base
33974
+ base: options.base,
33975
+ fileSource
33663
33976
  });
33664
- evaluated.diagnostics.push(...dateModeImpactDiagnostics({ config, impactedDocuments, relPaths, root: root2 }));
33977
+ evaluated.diagnostics.push(...dateModeImpactDiagnostics({ config, impactedDocuments, relPaths, root: root2, fileSource }));
33665
33978
  return resultFor({
33666
33979
  options,
33667
33980
  relPaths,
33668
33981
  classification: publicClassification(classification),
33669
33982
  impactedDocuments,
33670
33983
  audits: evaluated.audits,
33671
- diagnostics: evaluated.diagnostics
33984
+ diagnostics: [...diagnostics, ...evaluated.diagnostics]
33672
33985
  });
33673
33986
  }
33674
33987
  function printValidateChangedResult(result) {
@@ -33679,16 +33992,20 @@ function printValidateChangedResult(result) {
33679
33992
  for (const path3 of result.classification.foreignSkills) {
33680
33993
  console.log(`validate changed: skipping foreign skill ${path3} (owned upstream; see skills-lock.json / skillOwnership)`);
33681
33994
  }
33682
- for (const audit of result.audits)
33683
- printAuditResult(audit, false);
33684
- for (const impacted of result.impactedDocuments) {
33685
- for (const reason of impacted.reasons) {
33686
- if (reason.kind !== "changed-review-dependency")
33687
- continue;
33688
- console.log(`validate changed: ${impacted.path} requires review (dependency ${reason.dependency} matched ${reason.target})`);
33995
+ if (result.ok) {
33996
+ for (const audit of result.audits)
33997
+ printAuditResult(audit, false);
33998
+ } else {
33999
+ for (const audit of result.audits.filter((item) => !item.ok)) {
34000
+ printAuditResult(audit, false);
33689
34001
  }
33690
34002
  }
33691
- for (const diagnostic of result.diagnostics) {
34003
+ const rereadDiagnostics = result.diagnostics.filter(isRereadIssue);
34004
+ const otherDiagnostics = result.diagnostics.filter((item) => !isRereadIssue(item));
34005
+ if (rereadDiagnostics.length > 0) {
34006
+ printReport(rereadDiagnostics, { label: "validate changed" });
34007
+ }
34008
+ for (const diagnostic of otherDiagnostics) {
33692
34009
  const path3 = diagnostic.file === "." ? "" : `${diagnostic.file}: `;
33693
34010
  console.error(`validate changed: ${path3}${diagnostic.message}`);
33694
34011
  }
@@ -33799,7 +34116,7 @@ function handleCustomizeResolve(argv) {
33799
34116
  if (json) {
33800
34117
  console.log(JSON.stringify(result, null, 2));
33801
34118
  } else if (result.content) {
33802
- process7.stdout.write(result.content);
34119
+ process6.stdout.write(result.content);
33803
34120
  }
33804
34121
  return 0;
33805
34122
  }
@@ -33808,7 +34125,7 @@ function handleHook(argv) {
33808
34125
  usage();
33809
34126
  return 1;
33810
34127
  }
33811
- process7.stdout.write(runCustomizeHook(readFileSync22(0, "utf8")));
34128
+ process6.stdout.write(runCustomizeHook(readFileSync24(0, "utf8")));
33812
34129
  return 0;
33813
34130
  }
33814
34131
  function handleInit(argv) {
@@ -33840,22 +34157,22 @@ async function dispatchCommand(argv) {
33840
34157
  }
33841
34158
  }
33842
34159
  async function main() {
33843
- const argv = process7.argv.slice(2);
34160
+ const argv = process6.argv.slice(2);
33844
34161
  const command = argv[0];
33845
34162
  if (!command || command === "--help" || command === "-h") {
33846
34163
  usage();
33847
- process7.exit(command ? 0 : 1);
34164
+ process6.exit(command ? 0 : 1);
33848
34165
  }
33849
34166
  try {
33850
34167
  const exitCode = await dispatchCommand(argv);
33851
34168
  if (exitCode === null) {
33852
34169
  usage();
33853
- process7.exit(1);
34170
+ process6.exit(1);
33854
34171
  }
33855
- process7.exit(exitCode);
34172
+ process6.exit(exitCode);
33856
34173
  } catch (error) {
33857
34174
  console.error(String(error));
33858
- process7.exit(1);
34175
+ process6.exit(1);
33859
34176
  }
33860
34177
  }
33861
34178
  main();