@rhize/skill-forge 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { Command } from "commander";
5
5
  import { createRequire } from "module";
6
6
 
7
7
  // src/commands/add.ts
8
- import { join as join20 } from "path";
8
+ import { join as join21 } from "path";
9
9
 
10
10
  // src/config.ts
11
11
  import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
@@ -1025,15 +1025,15 @@ function readProvenanceLedger(skillsRoot) {
1025
1025
  import { readdirSync as readdirSync7 } from "fs";
1026
1026
 
1027
1027
  // src/auditState.ts
1028
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1029
- import { basename as basename7, dirname as dirname5, join as join12 } from "path";
1028
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
1029
+ import { basename as basename7, dirname as dirname5, join as join13 } from "path";
1030
1030
 
1031
1031
  // src/audit.ts
1032
1032
  import {
1033
- existsSync as existsSync8,
1033
+ existsSync as existsSync9,
1034
1034
  lstatSync as lstatSync2,
1035
1035
  mkdtempSync,
1036
- readFileSync as readFileSync8,
1036
+ readFileSync as readFileSync9,
1037
1037
  readdirSync as readdirSync5,
1038
1038
  realpathSync as realpathSync2,
1039
1039
  renameSync as renameSync3,
@@ -1041,7 +1041,7 @@ import {
1041
1041
  statSync as statSync6,
1042
1042
  writeFileSync as writeFileSync5
1043
1043
  } from "fs";
1044
- import { basename as basename6, delimiter as delimiter3, extname as extname3, join as join11, resolve as resolvePath2 } from "path";
1044
+ import { basename as basename6, delimiter as delimiter3, extname as extname3, join as join12, resolve as resolvePath2 } from "path";
1045
1045
 
1046
1046
  // src/gate/safety.ts
1047
1047
  import { statSync as statSync4 } from "fs";
@@ -1049,6 +1049,37 @@ import { execFile } from "child_process";
1049
1049
  import { delimiter as delimiter2, join as join9 } from "path";
1050
1050
  import { promisify } from "util";
1051
1051
  var execFileAsync = promisify(execFile);
1052
+ var RM_RF_TARGET_RE = /\brm\s+(?:-[a-z]+\s+)+("[^"]*"|'[^']*'|[^\s;|&)"'`]+)/i;
1053
+ function refineRmRf(line) {
1054
+ const match = line.match(RM_RF_TARGET_RE);
1055
+ if (!match) {
1056
+ return { severity: "HIGH", message: "recursive force-delete (rm -rf) with an unparseable target \u2014 verify the path is scoped" };
1057
+ }
1058
+ const target = match[1].replace(/^["']|["']$/g, "").trim();
1059
+ if (target.includes("$") || target.includes("`")) {
1060
+ return {
1061
+ severity: "HIGH",
1062
+ message: `recursive force-delete of "${target}" \u2014 the target is a shell expansion; if it is empty or unset this can delete far more than intended`
1063
+ };
1064
+ }
1065
+ if (target === "/" || target === "/*" || target === "*" || target === "." || target === ".." || target === "~" || target === "~/") {
1066
+ return { severity: "HIGH", message: `recursive force-delete of "${target}" \u2014 this targets the filesystem root, the home directory, or everything in the working directory` };
1067
+ }
1068
+ if (target.startsWith("~")) {
1069
+ return { severity: "HIGH", message: `recursive force-delete of "${target}" \u2014 targets a path inside the home directory, outside any project` };
1070
+ }
1071
+ if (target.split("/").includes("..")) {
1072
+ return { severity: "HIGH", message: `recursive force-delete of "${target}" \u2014 the target escapes the current directory via ".."` };
1073
+ }
1074
+ if (target.startsWith("/")) {
1075
+ const segments = target.split("/").filter(Boolean);
1076
+ if (segments.length < 3) {
1077
+ return { severity: "HIGH", message: `recursive force-delete of "${target}" \u2014 a shallow absolute path like this is a system directory` };
1078
+ }
1079
+ return { severity: "MEDIUM", message: `recursive force-delete of "${target}" \u2014 an absolute path outside any project directory` };
1080
+ }
1081
+ return null;
1082
+ }
1052
1083
  var DENY_RULES = [
1053
1084
  {
1054
1085
  rule: "curl-pipe-shell",
@@ -1072,7 +1103,8 @@ var DENY_RULES = [
1072
1103
  rule: "destructive-rm-rf",
1073
1104
  severity: "HIGH",
1074
1105
  pattern: /\brm\s+-[a-z]*r[a-z]*f[a-z]*\b|\brm\s+-[a-z]*f[a-z]*r[a-z]*\b/i,
1075
- message: "recursive force-delete (rm -rf) \u2014 verify the target path is scoped"
1106
+ message: "recursive force-delete (rm -rf) \u2014 verify the target path is scoped",
1107
+ refine: refineRmRf
1076
1108
  },
1077
1109
  {
1078
1110
  rule: "credential-file-access",
@@ -1127,11 +1159,14 @@ function scanStaticRules(targetPath) {
1127
1159
  for (const rule of DENY_RULES) {
1128
1160
  for (let i = 0; i < lines.length; i++) {
1129
1161
  if (rule.pattern.test(lines[i])) {
1162
+ const refined = rule.refine ? rule.refine(lines[i]) : { severity: rule.severity, message: rule.message };
1163
+ if (!refined) continue;
1130
1164
  findings.push({
1131
- severity: rule.severity,
1165
+ severity: refined.severity,
1132
1166
  rule: rule.rule,
1133
1167
  file,
1134
- detail: `line ${i + 1}: ${rule.message}`
1168
+ detail: `line ${i + 1}: ${refined.message}`,
1169
+ context: lines[i].trim()
1135
1170
  });
1136
1171
  }
1137
1172
  }
@@ -1232,19 +1267,27 @@ function scanJsonConfig(file, parsed) {
1232
1267
  const command = typeof node.command === "string" ? node.command : void 0;
1233
1268
  const args = Array.isArray(node.args) ? node.args.filter((a) => typeof a === "string") : [];
1234
1269
  if (command && isNpxCommand(command)) {
1235
- const flagIdx = args.findIndex((a) => a === "-y" || a === "--yes");
1236
- if (flagIdx !== -1) {
1237
- const { packageFlagSpecs, positionalSpec } = collectNpxPackageSpecs(args);
1238
- const allSpecs = positionalSpec ? [...packageFlagSpecs, positionalSpec] : packageFlagSpecs;
1239
- for (const pkgSpec of allSpecs) {
1240
- if (!isPinnedPackageSpec(pkgSpec)) {
1241
- findings.push({
1242
- severity: "MEDIUM",
1243
- rule: "mcp-unpinned-npx",
1244
- file,
1245
- detail: `npx -y ${pkgSpec} is unpinned \u2014 pin an exact version (${packageSpecBase(pkgSpec)}@<version>) so the resolved server code can't change silently`
1246
- });
1247
- }
1270
+ const hasYes = args.includes("-y") || args.includes("--yes");
1271
+ const { packageFlagSpecs, positionalSpec } = collectNpxPackageSpecs(args);
1272
+ const allSpecs = positionalSpec ? [...packageFlagSpecs, positionalSpec] : packageFlagSpecs;
1273
+ for (const pkgSpec of allSpecs) {
1274
+ if (isPinnedPackageSpec(pkgSpec)) continue;
1275
+ if (hasYes) {
1276
+ findings.push({
1277
+ severity: "MEDIUM",
1278
+ rule: "mcp-unpinned-npx",
1279
+ file,
1280
+ detail: `npx -y ${pkgSpec} is unpinned \u2014 pin an exact version (${packageSpecBase(pkgSpec)}@<version>) so the resolved server code can't change silently`,
1281
+ context: pkgSpec
1282
+ });
1283
+ } else {
1284
+ findings.push({
1285
+ severity: "LOW",
1286
+ rule: "mcp-unpinned-npx",
1287
+ file,
1288
+ detail: `npx ${pkgSpec} is unpinned \u2014 without -y npx prompts before first install, but once cached the tag still floats to whatever the registry points at; pin ${packageSpecBase(pkgSpec)}@<version>`,
1289
+ context: pkgSpec
1290
+ });
1248
1291
  }
1249
1292
  }
1250
1293
  }
@@ -1254,7 +1297,8 @@ function scanJsonConfig(file, parsed) {
1254
1297
  severity: "HIGH",
1255
1298
  rule: "mcp-dangerous-flag",
1256
1299
  file,
1257
- detail: `launch arg ${a} disables a safety guard`
1300
+ detail: `launch arg ${a} disables a safety guard`,
1301
+ context: a
1258
1302
  });
1259
1303
  }
1260
1304
  if (a === "/" || a === "~") {
@@ -1262,7 +1306,8 @@ function scanJsonConfig(file, parsed) {
1262
1306
  severity: "MEDIUM",
1263
1307
  rule: "mcp-filesystem-root-arg",
1264
1308
  file,
1265
- detail: `launch arg "${a}" grants filesystem access at the root/home directory \u2014 scope this to a specific directory`
1309
+ detail: `launch arg "${a}" grants filesystem access at the root/home directory \u2014 scope this to a specific directory`,
1310
+ context: a
1266
1311
  });
1267
1312
  }
1268
1313
  }
@@ -1275,14 +1320,16 @@ function scanJsonConfig(file, parsed) {
1275
1320
  severity: "HIGH",
1276
1321
  rule: "mcp-inline-credential-value",
1277
1322
  file,
1278
- detail: `env.${key} has an inline credential value \u2014 emit an empty string and document that the user must fill it in`
1323
+ detail: `env.${key} has an inline credential value \u2014 emit an empty string and document that the user must fill it in`,
1324
+ context: `env.${key}`
1279
1325
  });
1280
1326
  } else if (kind === "file-path") {
1281
1327
  findings.push({
1282
1328
  severity: "LOW",
1283
1329
  rule: "mcp-credential-file-path",
1284
1330
  file,
1285
- detail: `env.${key} points at a credential file rather than an inline secret \u2014 the value leaks nothing, but keep that file out of version control`
1331
+ detail: `env.${key} points at a credential file rather than an inline secret \u2014 the value leaks nothing, but keep that file out of version control`,
1332
+ context: `env.${key}`
1286
1333
  });
1287
1334
  }
1288
1335
  }
@@ -1290,7 +1337,7 @@ function scanJsonConfig(file, parsed) {
1290
1337
  }
1291
1338
  return findings;
1292
1339
  }
1293
- var NPX_SHELL_RE = /\bnpx(?:\.cmd|\.exe)?\s+(?:-y|--yes)\s+((?:@[\w.-]+\/)?[\w.-]+)(@[\w.-]+)?/gi;
1340
+ var NPX_SHELL_RE = /\bnpx(?:\.cmd|\.exe)?\s+(?:(-y|--yes)\s+)?((?:@[\w.-]+\/)?[A-Za-z0-9][\w.-]*)(@[\w.-]+)?/gi;
1294
1341
  var DANGEROUS_FLAG_RE = /--dangerously-[\w-]+|--no-sandbox\b/g;
1295
1342
  var CREDENTIAL_ASSIGNMENT_RE = /["']?([A-Za-z][A-Za-z0-9_]*)["']?\s*[:=]\s*["']([^"']{6,})["']/g;
1296
1343
  var CREDENTIAL_ASSIGNMENT_UNQUOTED_RE = /^\s*(?:export\s+|-\s+)?([A-Za-z][A-Za-z0-9_]*)\s*[:=]\s*(\S+)/;
@@ -1309,14 +1356,16 @@ function pushCredentialFinding(findings, file, lineNo, varName, value) {
1309
1356
  severity: "HIGH",
1310
1357
  rule: "mcp-inline-credential-value",
1311
1358
  file,
1312
- detail: `line ${lineNo}: inline credential value assigned to ${varName}`
1359
+ detail: `line ${lineNo}: inline credential value assigned to ${varName}`,
1360
+ context: varName
1313
1361
  });
1314
1362
  } else if (kind === "file-path") {
1315
1363
  findings.push({
1316
1364
  severity: "LOW",
1317
1365
  rule: "mcp-credential-file-path",
1318
1366
  file,
1319
- detail: `line ${lineNo}: ${varName} points at a credential file rather than an inline secret \u2014 the value leaks nothing, but keep that file out of version control`
1367
+ detail: `line ${lineNo}: ${varName} points at a credential file rather than an inline secret \u2014 the value leaks nothing, but keep that file out of version control`,
1368
+ context: varName
1320
1369
  });
1321
1370
  }
1322
1371
  }
@@ -1329,15 +1378,28 @@ function scanTextFile(file, content, allowCredentialCheck) {
1329
1378
  NPX_SHELL_RE.lastIndex = 0;
1330
1379
  let m;
1331
1380
  while ((m = NPX_SHELL_RE.exec(line)) !== null) {
1332
- const pkgSpec = m[1];
1333
- const versionSuffix = m[2] ?? "";
1334
- if (!isPinnedPackageSpec(pkgSpec + versionSuffix)) {
1335
- findings.push({
1336
- severity: "MEDIUM",
1337
- rule: "mcp-unpinned-npx",
1338
- file,
1339
- detail: `line ${lineNo}: npx -y ${pkgSpec} is unpinned \u2014 pin an exact version (${pkgSpec}@<version>)`
1340
- });
1381
+ const hasYes = Boolean(m[1]);
1382
+ const pkgSpec = m[2];
1383
+ const versionSuffix = m[3] ?? "";
1384
+ const fullSpec = pkgSpec + versionSuffix;
1385
+ if (!isPinnedPackageSpec(fullSpec)) {
1386
+ if (hasYes) {
1387
+ findings.push({
1388
+ severity: "MEDIUM",
1389
+ rule: "mcp-unpinned-npx",
1390
+ file,
1391
+ detail: `line ${lineNo}: npx -y ${pkgSpec} is unpinned \u2014 pin an exact version (${pkgSpec}@<version>)`,
1392
+ context: fullSpec
1393
+ });
1394
+ } else {
1395
+ findings.push({
1396
+ severity: "LOW",
1397
+ rule: "mcp-unpinned-npx",
1398
+ file,
1399
+ detail: `line ${lineNo}: npx ${pkgSpec} is unpinned \u2014 without -y npx prompts before first install, but once cached the tag still floats to whatever the registry points at; pin ${pkgSpec}@<version>`,
1400
+ context: fullSpec
1401
+ });
1402
+ }
1341
1403
  }
1342
1404
  }
1343
1405
  DANGEROUS_FLAG_RE.lastIndex = 0;
@@ -1590,6 +1652,89 @@ function scanOverlapPaths(skillMdPaths, threshold = DEFAULT_SET_THRESHOLD) {
1590
1652
  return pairs;
1591
1653
  }
1592
1654
 
1655
+ // src/findings.ts
1656
+ import { createHash as createHash2 } from "crypto";
1657
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
1658
+ import { join as join11 } from "path";
1659
+ var FINGERPRINT_DISPLAY_LENGTH = 12;
1660
+ var MIN_FINGERPRINT_PREFIX_LENGTH = 8;
1661
+ var FINGERPRINT_HEX_RE = /^[0-9a-f]{64}$/;
1662
+ var VALID_SEVERITIES = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
1663
+ var CONTROL_CHAR_RE = /[\u0000-\u001F\u007F-\u009F]/;
1664
+ function isValidSeverity(value) {
1665
+ return typeof value === "string" && VALID_SEVERITIES.includes(value);
1666
+ }
1667
+ function fingerprintFinding(f) {
1668
+ return createHash2("sha256").update(`${f.severity}\0${f.rule}\0${f.target}\0${f.context}`).digest("hex");
1669
+ }
1670
+ function getAcceptedFindingsPath() {
1671
+ return join11(getHomeDir(), "accepted-findings.json");
1672
+ }
1673
+ function invalidRecordReason(record) {
1674
+ if (typeof record !== "object" || record === null) return "not an object";
1675
+ const r = record;
1676
+ if (typeof r.fingerprint !== "string" || !FINGERPRINT_HEX_RE.test(r.fingerprint)) return "invalid fingerprint";
1677
+ if (!isValidSeverity(r.severity)) return "invalid severity";
1678
+ if (typeof r.rule !== "string" || r.rule.length === 0 || CONTROL_CHAR_RE.test(r.rule)) return "invalid rule";
1679
+ if (typeof r.target !== "string" || r.target.length === 0 || CONTROL_CHAR_RE.test(r.target)) return "invalid target";
1680
+ if (typeof r.context !== "string" || CONTROL_CHAR_RE.test(r.context)) return "invalid context";
1681
+ if (typeof r.detail !== "string" || CONTROL_CHAR_RE.test(r.detail)) return "invalid detail";
1682
+ if (typeof r.reason !== "string" || r.reason.length === 0 || r.reason.length > MAX_PREFERENCE_NOTE_LENGTH || CONTROL_CHAR_RE.test(r.reason)) {
1683
+ return "invalid reason";
1684
+ }
1685
+ if (typeof r.acceptedAt !== "string" || r.acceptedAt.length === 0) return "invalid acceptedAt";
1686
+ if (r.source !== "user") return "invalid source";
1687
+ return void 0;
1688
+ }
1689
+ function readAcceptedFindings() {
1690
+ const path = getAcceptedFindingsPath();
1691
+ if (!existsSync8(path)) return { records: [], notices: [] };
1692
+ let parsed;
1693
+ try {
1694
+ parsed = JSON.parse(readFileSync8(path, "utf-8"));
1695
+ } catch {
1696
+ return { records: [], notices: ["accepted-findings store unreadable (corrupt JSON) \u2014 nothing suppressed this run"] };
1697
+ }
1698
+ if (typeof parsed !== "object" || parsed === null || parsed.schemaVersion !== 1 || !Array.isArray(parsed.records)) {
1699
+ return { records: [], notices: ["accepted-findings store unreadable (unrecognized shape or schema version) \u2014 nothing suppressed this run"] };
1700
+ }
1701
+ const rawRecords = parsed.records;
1702
+ const records = [];
1703
+ let dropped = 0;
1704
+ for (const raw of rawRecords) {
1705
+ if (invalidRecordReason(raw) === void 0) {
1706
+ records.push(raw);
1707
+ } else {
1708
+ dropped++;
1709
+ }
1710
+ }
1711
+ const notices = [];
1712
+ if (dropped > 0) {
1713
+ notices.push(
1714
+ `${dropped} accepted-finding record(s) in the store failed validation and were dropped \u2014 whatever they described is reported as active again`
1715
+ );
1716
+ }
1717
+ return { records, notices };
1718
+ }
1719
+ function writeAcceptedFindings(records) {
1720
+ const store = { schemaVersion: 1, records };
1721
+ writeJsonAtomic(getAcceptedFindingsPath(), store);
1722
+ }
1723
+ function validateAcceptReason(reason) {
1724
+ if (reason.length === 0) {
1725
+ throw new UserError('--reason is required \u2014 a short note on why this finding is accepted (e.g. "funnel genuinely needs root")');
1726
+ }
1727
+ validateProvenanceText("reason", reason);
1728
+ }
1729
+ function resolveAmongFindings(prefix, findings) {
1730
+ const lower = prefix.toLowerCase();
1731
+ return findings.filter((f) => f.fingerprint !== void 0 && f.fingerprint.startsWith(lower));
1732
+ }
1733
+ function resolveAmongRecords(prefix, records) {
1734
+ const lower = prefix.toLowerCase();
1735
+ return records.filter((r) => r.fingerprint.startsWith(lower));
1736
+ }
1737
+
1593
1738
  // src/license.ts
1594
1739
  import { verify as cryptoVerify } from "crypto";
1595
1740
  var LICENSE_PREFIX = "skf1";
@@ -1679,7 +1824,13 @@ function proFeatureStatus(feature, betaFree = BETA_FREE) {
1679
1824
  // src/audit.ts
1680
1825
  var OVERSIZED_SKILL_EST_TOKENS = 5e3;
1681
1826
  function toAuditFindings(source) {
1682
- return source.map((f) => ({ severity: f.severity, rule: f.rule, target: f.file, detail: f.detail }));
1827
+ return source.map((f) => ({ severity: f.severity, rule: f.rule, target: f.file, detail: f.detail, context: f.context }));
1828
+ }
1829
+ function activeVerdictFor(findings, strictness) {
1830
+ return verdictFor(
1831
+ findings.map((f) => ({ severity: f.severity, rule: f.rule, file: f.target, detail: f.detail })),
1832
+ strictness
1833
+ );
1683
1834
  }
1684
1835
  function resolveConfiguredRoot(root) {
1685
1836
  return resolvePath2(expandHome(root));
@@ -1703,7 +1854,7 @@ function listSubdirsFollowingSymlinks(dir) {
1703
1854
  }
1704
1855
  const out = [];
1705
1856
  for (const entry of entries) {
1706
- const full = join11(dir, entry.name);
1857
+ const full = join12(dir, entry.name);
1707
1858
  if (entry.isDirectory() || entry.isSymbolicLink() && isDirFollowingSymlink(full)) {
1708
1859
  out.push(full);
1709
1860
  }
@@ -1717,20 +1868,20 @@ function listSubdirsNoSymlinks(dir) {
1717
1868
  } catch {
1718
1869
  return [];
1719
1870
  }
1720
- return entries.filter((e) => e.isDirectory()).map((e) => join11(dir, e.name));
1871
+ return entries.filter((e) => e.isDirectory()).map((e) => join12(dir, e.name));
1721
1872
  }
1722
1873
  function findSkillDirsUnderRoot(root) {
1723
1874
  const found = /* @__PURE__ */ new Set();
1724
1875
  const rootSubdirs = listSubdirsFollowingSymlinks(root);
1725
1876
  for (const dir of rootSubdirs) {
1726
- if (existsSync8(join11(dir, "SKILL.md"))) found.add(dir);
1877
+ if (existsSync9(join12(dir, "SKILL.md"))) found.add(dir);
1727
1878
  }
1728
- for (const dir of listSubdirsFollowingSymlinks(join11(root, "skills"))) {
1729
- if (existsSync8(join11(dir, "SKILL.md"))) found.add(dir);
1879
+ for (const dir of listSubdirsFollowingSymlinks(join12(root, "skills"))) {
1880
+ if (existsSync9(join12(dir, "SKILL.md"))) found.add(dir);
1730
1881
  }
1731
1882
  for (const topDir of rootSubdirs) {
1732
- for (const dir of listSubdirsNoSymlinks(join11(topDir, "skills"))) {
1733
- if (existsSync8(join11(dir, "SKILL.md"))) found.add(dir);
1883
+ for (const dir of listSubdirsNoSymlinks(join12(topDir, "skills"))) {
1884
+ if (existsSync9(join12(dir, "SKILL.md"))) found.add(dir);
1734
1885
  }
1735
1886
  }
1736
1887
  return Array.from(found).filter((p) => !p.includes("/.git/"));
@@ -1826,7 +1977,7 @@ function estimateTokens(text) {
1826
1977
  return Math.ceil(Buffer.byteLength(text, "utf-8") / 4);
1827
1978
  }
1828
1979
  function profileAuditedSkill(group, findings, unreadableSkillMdPaths) {
1829
- const skillMdPath = join11(group.canonicalPath, "SKILL.md");
1980
+ const skillMdPath = join12(group.canonicalPath, "SKILL.md");
1830
1981
  const base = {
1831
1982
  path: group.canonicalPath,
1832
1983
  skillMdPath,
@@ -1837,7 +1988,7 @@ function profileAuditedSkill(group, findings, unreadableSkillMdPaths) {
1837
1988
  let sizeBytes;
1838
1989
  try {
1839
1990
  sizeBytes = statSync6(skillMdPath).size;
1840
- text = readFileSync8(skillMdPath, "utf-8");
1991
+ text = readFileSync9(skillMdPath, "utf-8");
1841
1992
  } catch (err) {
1842
1993
  findings.push({
1843
1994
  severity: "MEDIUM",
@@ -1894,7 +2045,7 @@ function auditMcpTarget(targetPath, notices) {
1894
2045
  const findings = [];
1895
2046
  const isToml = extname3(targetPath).toLowerCase() === ".toml";
1896
2047
  const format = isToml ? "toml" : "json";
1897
- if (!existsSync8(targetPath)) {
2048
+ if (!existsSync9(targetPath)) {
1898
2049
  findings.push({
1899
2050
  severity: "LOW",
1900
2051
  rule: "missing-mcp-target",
@@ -1915,7 +2066,7 @@ function auditMcpTarget(targetPath, notices) {
1915
2066
  }
1916
2067
  if (isToml) {
1917
2068
  try {
1918
- readFileSync8(targetPath, "utf-8");
2069
+ readFileSync9(targetPath, "utf-8");
1919
2070
  } catch (err) {
1920
2071
  findings.push({
1921
2072
  severity: "MEDIUM",
@@ -1930,7 +2081,7 @@ function auditMcpTarget(targetPath, notices) {
1930
2081
  }
1931
2082
  let raw;
1932
2083
  try {
1933
- raw = readFileSync8(targetPath, "utf-8");
2084
+ raw = readFileSync9(targetPath, "utf-8");
1934
2085
  } catch (err) {
1935
2086
  findings.push({
1936
2087
  severity: "MEDIUM",
@@ -2006,7 +2157,7 @@ function defaultWhich(cmd) {
2006
2157
  const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
2007
2158
  for (const dir of dirs) {
2008
2159
  for (const ext of exts) {
2009
- const candidate = join11(dir, cmd + ext);
2160
+ const candidate = join12(dir, cmd + ext);
2010
2161
  try {
2011
2162
  if (statSync6(candidate).isFile()) return candidate;
2012
2163
  } catch {
@@ -2092,16 +2243,23 @@ async function auditConfiguredSet(config, options = {}) {
2092
2243
  severity: "LOW",
2093
2244
  rule: "oversized-skill",
2094
2245
  target: skill.skillMdPath,
2095
- detail: `est. ${skill.estTokens} tokens exceeds the ${oversizedThreshold}-token threshold \u2014 consider trimming or splitting`
2246
+ detail: `est. ${skill.estTokens} tokens exceeds the ${oversizedThreshold}-token threshold \u2014 consider trimming or splitting`,
2247
+ // Deliberate '' (v0.12, see docs/accepted-findings-schema.md's context table): the detail
2248
+ // embeds a token count that changes on every edit, so "this skill may exceed the
2249
+ // threshold" is genuinely a rule+path identity here. An accepted oversized-skill finding
2250
+ // therefore stays suppressed as the skill keeps growing — a documented tradeoff, not a bug.
2251
+ context: ""
2096
2252
  });
2097
2253
  }
2098
2254
  }
2099
- const safetyVerdictBySkillPath = /* @__PURE__ */ new Map();
2255
+ const perSkillFindings = /* @__PURE__ */ new Map();
2256
+ const skillScanFailed = /* @__PURE__ */ new Set();
2100
2257
  for (const skill of skills) {
2101
2258
  try {
2102
2259
  const result = await scanSafety(skill.path, config.strictness);
2103
- findings.push(...toAuditFindings(result.findings));
2104
- safetyVerdictBySkillPath.set(skill.path, result.verdict);
2260
+ const skillFindings = toAuditFindings(result.findings);
2261
+ findings.push(...skillFindings);
2262
+ perSkillFindings.set(skill.path, skillFindings);
2105
2263
  } catch (err) {
2106
2264
  findings.push({
2107
2265
  severity: "MEDIUM",
@@ -2109,7 +2267,7 @@ async function auditConfiguredSet(config, options = {}) {
2109
2267
  target: skill.path,
2110
2268
  detail: `safety scan failed: ${err.message}`
2111
2269
  });
2112
- safetyVerdictBySkillPath.set(skill.path, "block");
2270
+ skillScanFailed.add(skill.path);
2113
2271
  }
2114
2272
  }
2115
2273
  const mcpTargetPaths = config.mcpTargets ?? [];
@@ -2140,7 +2298,36 @@ async function auditConfiguredSet(config, options = {}) {
2140
2298
  } else {
2141
2299
  notices.push(overlapStatus.notice ?? "Pro feature (installed-set overlap analysis) locked.");
2142
2300
  }
2143
- const evolveEligible = skills.filter((s) => s.validFrontmatter && safetyVerdictBySkillPath.get(s.path) === "pass").map((s) => ({ name: s.name, path: s.path }));
2301
+ const acceptedRecords = options.accepted ?? [];
2302
+ const acceptedByFingerprint = new Map(acceptedRecords.map((r) => [r.fingerprint, r]));
2303
+ const matchedFingerprints = /* @__PURE__ */ new Set();
2304
+ const activeFindings = [];
2305
+ const acceptedEntries = [];
2306
+ for (const f of findings) {
2307
+ const context = f.context ?? f.detail;
2308
+ const fingerprint = fingerprintFinding({ severity: f.severity, rule: f.rule, target: f.target, context });
2309
+ f.fingerprint = fingerprint;
2310
+ const record = acceptedByFingerprint.get(fingerprint);
2311
+ if (record) {
2312
+ matchedFingerprints.add(fingerprint);
2313
+ acceptedEntries.push({ finding: f, reason: record.reason, acceptedAt: record.acceptedAt });
2314
+ } else {
2315
+ activeFindings.push(f);
2316
+ }
2317
+ }
2318
+ const unmatchedAcceptedCount = acceptedRecords.filter((r) => !matchedFingerprints.has(r.fingerprint)).length;
2319
+ if (unmatchedAcceptedCount > 0) {
2320
+ notices.push(
2321
+ `${unmatchedAcceptedCount} accepted finding(s) did not match anything this run (resolved, or content changed) \u2014 see \`skill-forge finding list --stale\``
2322
+ );
2323
+ }
2324
+ const activeFindingSet = new Set(activeFindings);
2325
+ const evolveEligible = skills.filter((s) => {
2326
+ if (!s.validFrontmatter || skillScanFailed.has(s.path)) return false;
2327
+ const skillFindings = perSkillFindings.get(s.path) ?? [];
2328
+ const activeSkillFindings = skillFindings.filter((f) => activeFindingSet.has(f));
2329
+ return activeVerdictFor(activeSkillFindings, config.strictness) === "pass";
2330
+ }).map((s) => ({ name: s.name, path: s.path }));
2144
2331
  const foundation = config.foundationSkillPath ? {
2145
2332
  available: false,
2146
2333
  existingPath: config.foundationSkillPath,
@@ -2150,21 +2337,23 @@ async function auditConfiguredSet(config, options = {}) {
2150
2337
  message: "no business-foundation skill yet \u2014 `skill-forge audit --foundation` can scaffold one to ground future skills in your business profile"
2151
2338
  };
2152
2339
  const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
2153
- for (const f of findings) findingCounts[f.severity]++;
2340
+ for (const f of activeFindings) findingCounts[f.severity]++;
2154
2341
  const summary = {
2155
2342
  rootCount: skillRoots.length,
2156
2343
  skillCount: skills.length,
2157
2344
  mcpTargetCount: mcpTargets.length,
2158
2345
  findingCounts,
2159
2346
  overlapClusterCount: overlapClusters.length,
2160
- evolveEligibleCount: evolveEligible.length
2347
+ evolveEligibleCount: evolveEligible.length,
2348
+ acceptedCount: acceptedEntries.length
2161
2349
  };
2162
2350
  const tooling = checkTooling(options.which ?? defaultWhich, options.env ?? process.env);
2163
2351
  return {
2164
2352
  schemaVersion: 1,
2165
2353
  generatedAt: now().toISOString(),
2166
2354
  inventory: { skillRoots, skills, mcpTargets },
2167
- hygiene: findings,
2355
+ hygiene: activeFindings,
2356
+ accepted: acceptedEntries,
2168
2357
  opportunities: { overlapClusters, evolveEligible, foundation, overlapLocked: !overlapStatus.enabled },
2169
2358
  tooling,
2170
2359
  notices,
@@ -2226,16 +2415,33 @@ function renderMcpTargetsSection(targets) {
2226
2415
  }
2227
2416
  return lines;
2228
2417
  }
2418
+ function displayFingerprint(f) {
2419
+ return f.fingerprint ? f.fingerprint.slice(0, FINGERPRINT_DISPLAY_LENGTH) : "unknown";
2420
+ }
2229
2421
  function renderFindingsSection(findings) {
2230
2422
  return [
2231
2423
  "## Hygiene findings",
2232
2424
  "",
2233
2425
  ...bulletList(
2234
- findings.map((f) => `[${f.severity}] ${f.rule} \u2014 ${sanitizeForMarkdown(f.target)}: ${sanitizeForMarkdown(f.detail)}`),
2426
+ findings.map(
2427
+ (f) => `[${f.severity}] ${f.rule} \u2014 ${sanitizeForMarkdown(f.target)}: ${sanitizeForMarkdown(f.detail)} (fp ${displayFingerprint(f)})`
2428
+ ),
2235
2429
  "(none \u2014 clean)"
2236
2430
  )
2237
2431
  ];
2238
2432
  }
2433
+ function renderAcceptedFindingsSection(entries) {
2434
+ return [
2435
+ "## Accepted findings",
2436
+ "",
2437
+ ...bulletList(
2438
+ entries.map(
2439
+ (e) => `[${e.finding.severity}] ${e.finding.rule} \u2014 ${sanitizeForMarkdown(e.finding.target)}: ${sanitizeForMarkdown(e.reason)} (accepted ${sanitizeForMarkdown(e.acceptedAt.slice(0, 10))})`
2440
+ ),
2441
+ "(none)"
2442
+ )
2443
+ ];
2444
+ }
2239
2445
  function renderOpportunitiesSection(report) {
2240
2446
  const { overlapClusters, evolveEligible, foundation } = report.opportunities;
2241
2447
  return [
@@ -2293,6 +2499,7 @@ function renderAuditMarkdown(report) {
2293
2499
  `- Skills: ${summary.skillCount}`,
2294
2500
  `- MCP targets: ${summary.mcpTargetCount}`,
2295
2501
  `- Findings: ${summary.findingCounts.CRITICAL} CRITICAL, ${summary.findingCounts.HIGH} HIGH, ${summary.findingCounts.MEDIUM} MEDIUM, ${summary.findingCounts.LOW} LOW`,
2502
+ `- Accepted: ${summary.acceptedCount} (suppressed from the counts above; see the Accepted findings section)`,
2296
2503
  `- Overlap clusters: ${summary.overlapClusterCount}`,
2297
2504
  `- Evolve-eligible skills: ${summary.evolveEligibleCount}`,
2298
2505
  "",
@@ -2306,6 +2513,8 @@ function renderAuditMarkdown(report) {
2306
2513
  "",
2307
2514
  ...renderFindingsSection(report.hygiene),
2308
2515
  "",
2516
+ ...renderAcceptedFindingsSection(report.accepted),
2517
+ "",
2309
2518
  ...renderOpportunitiesSection(report),
2310
2519
  "",
2311
2520
  ...renderToolingSection(report.tooling),
@@ -2358,7 +2567,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
2358
2567
  if (!isDir(resolvedRoot)) {
2359
2568
  throw new UserError(`skills root does not exist: ${resolvedRoot}`);
2360
2569
  }
2361
- const destDir = join11(resolvedRoot, "business-foundation");
2570
+ const destDir = join12(resolvedRoot, "business-foundation");
2362
2571
  let destLstat;
2363
2572
  try {
2364
2573
  destLstat = lstatSync2(destDir);
@@ -2372,9 +2581,9 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
2372
2581
  throw new UserError(`refusing to scaffold a foundation skill at ${destDir}: already exists`);
2373
2582
  }
2374
2583
  const content = renderFoundationSkillMd(profile);
2375
- const tempDir = mkdtempSync(join11(resolvedRoot, ".business-foundation-"));
2584
+ const tempDir = mkdtempSync(join12(resolvedRoot, ".business-foundation-"));
2376
2585
  try {
2377
- writeFileSync5(join11(tempDir, "SKILL.md"), content, { flag: "wx", mode: 384 });
2586
+ writeFileSync5(join12(tempDir, "SKILL.md"), content, { flag: "wx", mode: 384 });
2378
2587
  const safety = await scanSafety(tempDir, config.strictness);
2379
2588
  if (safety.verdict === "block") {
2380
2589
  throw new UserError(
@@ -2408,7 +2617,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
2408
2617
 
2409
2618
  // src/auditState.ts
2410
2619
  function getAuditStatePath() {
2411
- return join12(getHomeDir(), "audit-state.json");
2620
+ return join13(getHomeDir(), "audit-state.json");
2412
2621
  }
2413
2622
  function emptyCounts() {
2414
2623
  return { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
@@ -2425,15 +2634,15 @@ function summarizeAudit(report, reportPath) {
2425
2634
  rootOfSkillPath.set(skill.path, skill.root);
2426
2635
  }
2427
2636
  const byRoot = {};
2428
- for (const finding of report.hygiene) {
2637
+ for (const finding2 of report.hygiene) {
2429
2638
  let root;
2430
- for (let dir = finding.target; ; dir = dirname5(dir)) {
2639
+ for (let dir = finding2.target; ; dir = dirname5(dir)) {
2431
2640
  root = rootOfSkillPath.get(dir);
2432
2641
  if (root || dirname5(dir) === dir) break;
2433
2642
  }
2434
2643
  if (!root) continue;
2435
2644
  byRoot[root] ??= emptyCounts();
2436
- bump(byRoot[root], finding.severity);
2645
+ bump(byRoot[root], finding2.severity);
2437
2646
  }
2438
2647
  return {
2439
2648
  schemaVersion: 1,
@@ -2455,7 +2664,8 @@ function summarizeAudit(report, reportPath) {
2455
2664
  suggestedVerb: c.suggestedVerb
2456
2665
  })),
2457
2666
  overlapLocked: report.opportunities.overlapLocked === true,
2458
- evolveEligibleCount: report.summary.evolveEligibleCount
2667
+ evolveEligibleCount: report.summary.evolveEligibleCount,
2668
+ acceptedCount: report.summary.acceptedCount
2459
2669
  };
2460
2670
  }
2461
2671
  function writeAuditState(state) {
@@ -2463,9 +2673,9 @@ function writeAuditState(state) {
2463
2673
  }
2464
2674
  function readAuditState() {
2465
2675
  const path = getAuditStatePath();
2466
- if (!existsSync9(path)) return void 0;
2676
+ if (!existsSync10(path)) return void 0;
2467
2677
  try {
2468
- const parsed = JSON.parse(readFileSync9(path, "utf-8"));
2678
+ const parsed = JSON.parse(readFileSync10(path, "utf-8"));
2469
2679
  if (parsed.schemaVersion !== 1 || typeof parsed.lastRunAt !== "string") return void 0;
2470
2680
  return {
2471
2681
  schemaVersion: 1,
@@ -2477,7 +2687,8 @@ function readAuditState() {
2477
2687
  byRoot: parsed.byRoot ?? {},
2478
2688
  overlapClusters: parsed.overlapClusters ?? [],
2479
2689
  overlapLocked: parsed.overlapLocked === true,
2480
- evolveEligibleCount: parsed.evolveEligibleCount ?? 0
2690
+ evolveEligibleCount: parsed.evolveEligibleCount ?? 0,
2691
+ acceptedCount: parsed.acceptedCount ?? 0
2481
2692
  };
2482
2693
  } catch {
2483
2694
  return void 0;
@@ -2526,32 +2737,32 @@ function formatAuditAdvisory(state, options = {}) {
2526
2737
  }
2527
2738
 
2528
2739
  // src/commands/gatePipeline.ts
2529
- import { join as join15 } from "path";
2740
+ import { join as join16 } from "path";
2530
2741
 
2531
2742
  // src/gate/profile.ts
2532
2743
  import { execFileSync } from "child_process";
2533
2744
  import {
2534
2745
  closeSync,
2535
- existsSync as existsSync11,
2746
+ existsSync as existsSync12,
2536
2747
  mkdtempSync as mkdtempSync2,
2537
2748
  openSync,
2538
- readFileSync as readFileSync11,
2749
+ readFileSync as readFileSync12,
2539
2750
  readdirSync as readdirSync6,
2540
2751
  readSync,
2541
2752
  rmSync as rmSync4,
2542
2753
  statSync as statSync7
2543
2754
  } from "fs";
2544
2755
  import { tmpdir } from "os";
2545
- import { basename as basename8, dirname as dirname6, extname as extname5, join as join14, resolve as resolve3 } from "path";
2756
+ import { basename as basename8, dirname as dirname6, extname as extname5, join as join15, resolve as resolve3 } from "path";
2546
2757
 
2547
2758
  // src/gate/mcpCapabilities.ts
2548
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
2549
- import { extname as extname4, join as join13 } from "path";
2759
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
2760
+ import { extname as extname4, join as join14 } from "path";
2550
2761
  var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
2551
2762
  var MAX_ITEMS_PER_CATEGORY = 200;
2552
2763
  function readJsonSafe(file) {
2553
2764
  try {
2554
- return JSON.parse(readFileSync10(file, "utf-8"));
2765
+ return JSON.parse(readFileSync11(file, "utf-8"));
2555
2766
  } catch {
2556
2767
  return void 0;
2557
2768
  }
@@ -2652,16 +2863,16 @@ var MCP_SDK_IMPORT_RE = /@modelcontextprotocol\/sdk/;
2652
2863
  function extractMcpCapabilities(packageDir) {
2653
2864
  const buckets = { tools: /* @__PURE__ */ new Set(), resources: /* @__PURE__ */ new Set(), prompts: /* @__PURE__ */ new Set() };
2654
2865
  let hasPackageJsonMarker = false;
2655
- const pkgPath = join13(packageDir, "package.json");
2656
- if (existsSync10(pkgPath)) {
2866
+ const pkgPath = join14(packageDir, "package.json");
2867
+ if (existsSync11(pkgPath)) {
2657
2868
  const pkg2 = readJsonSafe(pkgPath);
2658
2869
  if (pkg2 && typeof pkg2 === "object" && !Array.isArray(pkg2)) {
2659
2870
  hasPackageJsonMarker = hasPackageJsonMcpMarkers(pkg2);
2660
2871
  }
2661
2872
  }
2662
2873
  for (const manifestName of [".mcp.json", "mcp.json"]) {
2663
- const manifestPath = join13(packageDir, manifestName);
2664
- if (!existsSync10(manifestPath)) continue;
2874
+ const manifestPath = join14(packageDir, manifestName);
2875
+ if (!existsSync11(manifestPath)) continue;
2665
2876
  const parsed = readJsonSafe(manifestPath);
2666
2877
  if (parsed !== void 0) {
2667
2878
  collectDeclaredFromManifest(parsed, buckets);
@@ -2738,7 +2949,7 @@ var PYTHON_STDLIB = /* @__PURE__ */ new Set([
2738
2949
  ]);
2739
2950
  function profileSkill(inputPath) {
2740
2951
  const p = resolve3(expandHome(inputPath));
2741
- if (!existsSync11(p)) {
2952
+ if (!existsSync12(p)) {
2742
2953
  throw new Error(`path does not exist: ${p}`);
2743
2954
  }
2744
2955
  const stat = statSync7(p);
@@ -2768,7 +2979,7 @@ function asProfile(candidatePath, artifact) {
2768
2979
  return asSkillProfile(profile, `at ${candidatePath}`);
2769
2980
  }
2770
2981
  function classifyInput(p) {
2771
- if (existsSync11(join14(p, ".claude-plugin", "plugin.json"))) return "plugin";
2982
+ if (existsSync12(join15(p, ".claude-plugin", "plugin.json"))) return "plugin";
2772
2983
  if (detectArtifactType(p) === "mcp") return "mcp";
2773
2984
  return "skill";
2774
2985
  }
@@ -2776,10 +2987,10 @@ var MCP_SDK_DEP2 = "@modelcontextprotocol/sdk";
2776
2987
  var MCP_KEYWORD2 = "mcp-server";
2777
2988
  var MCP_BIN_PREFIX2 = /^mcp-/i;
2778
2989
  function readPackageJsonSafe(root) {
2779
- const pkgPath = join14(root, "package.json");
2780
- if (!existsSync11(pkgPath)) return void 0;
2990
+ const pkgPath = join15(root, "package.json");
2991
+ if (!existsSync12(pkgPath)) return void 0;
2781
2992
  try {
2782
- return JSON.parse(readFileSync11(pkgPath, "utf-8"));
2993
+ return JSON.parse(readFileSync12(pkgPath, "utf-8"));
2783
2994
  } catch {
2784
2995
  return void 0;
2785
2996
  }
@@ -2800,7 +3011,7 @@ function hasMcpPackageMarkers(pkg2) {
2800
3011
  return false;
2801
3012
  }
2802
3013
  function detectArtifactType(root) {
2803
- if (existsSync11(join14(root, ".mcp.json")) || existsSync11(join14(root, "mcp.json"))) return "mcp";
3014
+ if (existsSync12(join15(root, ".mcp.json")) || existsSync12(join15(root, "mcp.json"))) return "mcp";
2804
3015
  const pkg2 = readPackageJsonSafe(root);
2805
3016
  if (pkg2 && hasMcpPackageMarkers(pkg2)) return "mcp";
2806
3017
  return "skill";
@@ -2810,7 +3021,7 @@ function resolveSkillMd(inputPath) {
2810
3021
  let tmpDir;
2811
3022
  const initialStat = statSync7(currentPath);
2812
3023
  if (extname5(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
2813
- tmpDir = mkdtempSync2(join14(tmpdir(), "forge_"));
3024
+ tmpDir = mkdtempSync2(join15(tmpdir(), "forge_"));
2814
3025
  extractZip(currentPath, tmpDir);
2815
3026
  currentPath = tmpDir;
2816
3027
  }
@@ -2819,8 +3030,8 @@ function resolveSkillMd(inputPath) {
2819
3030
  return { skillMd: currentPath, root: dirname6(currentPath), tmpDir };
2820
3031
  }
2821
3032
  if (stat.isDirectory()) {
2822
- const direct = join14(currentPath, "SKILL.md");
2823
- if (existsSync11(direct)) {
3033
+ const direct = join15(currentPath, "SKILL.md");
3034
+ if (existsSync12(direct)) {
2824
3035
  return { skillMd: direct, root: currentPath, tmpDir };
2825
3036
  }
2826
3037
  const matches = findSkillMdOneLevelDown(currentPath);
@@ -2845,16 +3056,16 @@ function findSkillMdOneLevelDown(dir) {
2845
3056
  const matches = /* @__PURE__ */ new Set();
2846
3057
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
2847
3058
  if (entry.isDirectory()) {
2848
- const candidate = join14(dir, entry.name, "SKILL.md");
2849
- if (existsSync11(candidate)) matches.add(candidate);
3059
+ const candidate = join15(dir, entry.name, "SKILL.md");
3060
+ if (existsSync12(candidate)) matches.add(candidate);
2850
3061
  }
2851
3062
  }
2852
- const skillsDir = join14(dir, "skills");
3063
+ const skillsDir = join15(dir, "skills");
2853
3064
  if (isDir(skillsDir)) {
2854
3065
  for (const entry of readdirSync6(skillsDir, { withFileTypes: true })) {
2855
3066
  if (entry.isDirectory()) {
2856
- const candidate = join14(skillsDir, entry.name, "SKILL.md");
2857
- if (existsSync11(candidate)) matches.add(candidate);
3067
+ const candidate = join15(skillsDir, entry.name, "SKILL.md");
3068
+ if (existsSync12(candidate)) matches.add(candidate);
2858
3069
  }
2859
3070
  }
2860
3071
  }
@@ -2937,9 +3148,9 @@ function detectLicense(root, fm) {
2937
3148
  if (fm.license) return fm.license;
2938
3149
  const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
2939
3150
  for (const name of names) {
2940
- const p = join14(root, name);
2941
- if (existsSync11(p) && statSync7(p).isFile()) {
2942
- const head = readFileSync11(p, "utf-8").slice(0, 2e3).toLowerCase();
3151
+ const p = join15(root, name);
3152
+ if (existsSync12(p) && statSync7(p).isFile()) {
3153
+ const head = readFileSync12(p, "utf-8").slice(0, 2e3).toLowerCase();
2943
3154
  for (const [hint, label] of Object.entries(SPDX_HINTS)) {
2944
3155
  if (head.includes(hint)) return label;
2945
3156
  }
@@ -2951,7 +3162,7 @@ function detectLicense(root, fm) {
2951
3162
  function walkFiles(dir) {
2952
3163
  const out = [];
2953
3164
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
2954
- const full = join14(dir, entry.name);
3165
+ const full = join15(dir, entry.name);
2955
3166
  if (entry.isDirectory()) {
2956
3167
  out.push(...walkFiles(full));
2957
3168
  } else if (entry.isFile()) {
@@ -2963,7 +3174,7 @@ function walkFiles(dir) {
2963
3174
  function countResources(root) {
2964
3175
  const resources = {};
2965
3176
  for (const d of RESOURCE_DIRS) {
2966
- const dp = join14(root, d);
3177
+ const dp = join15(root, d);
2967
3178
  if (isDir(dp)) {
2968
3179
  resources[d] = walkFiles(dp).length;
2969
3180
  }
@@ -2974,11 +3185,11 @@ function scanMcpAndImports(text, root) {
2974
3185
  const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
2975
3186
  const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
2976
3187
  const imports = /* @__PURE__ */ new Set();
2977
- const scriptsDir = join14(root, "scripts");
3188
+ const scriptsDir = join15(root, "scripts");
2978
3189
  if (isDir(scriptsDir)) {
2979
3190
  for (const f of walkFiles(scriptsDir)) {
2980
3191
  if (!f.endsWith(".py")) continue;
2981
- const ft = readFileSync11(f, "utf-8");
3192
+ const ft = readFileSync12(f, "utf-8");
2982
3193
  for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
2983
3194
  const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
2984
3195
  let m;
@@ -2995,7 +3206,7 @@ function scanMcpAndImports(text, root) {
2995
3206
  function profileSkillDir(inputPath) {
2996
3207
  const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
2997
3208
  try {
2998
- const text = readFileSync11(skillMd, "utf-8");
3209
+ const text = readFileSync12(skillMd, "utf-8");
2999
3210
  const fm = parseFrontmatter(text);
3000
3211
  const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
3001
3212
  const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
@@ -3023,19 +3234,19 @@ function profileSkillDir(inputPath) {
3023
3234
  }
3024
3235
  }
3025
3236
  function profilePlugin(p) {
3026
- const manifestPath = join14(p, ".claude-plugin", "plugin.json");
3237
+ const manifestPath = join15(p, ".claude-plugin", "plugin.json");
3027
3238
  let manifest;
3028
3239
  try {
3029
- manifest = JSON.parse(readFileSync11(manifestPath, "utf-8"));
3240
+ manifest = JSON.parse(readFileSync12(manifestPath, "utf-8"));
3030
3241
  } catch (err) {
3031
3242
  throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
3032
3243
  }
3033
- const skillsDir = join14(p, "skills");
3244
+ const skillsDir = join15(p, "skills");
3034
3245
  let skills = [];
3035
3246
  if (isDir(skillsDir)) {
3036
- skills = readdirSync6(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync11(join14(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
3247
+ skills = readdirSync6(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync12(join15(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
3037
3248
  }
3038
- const commandsDir = join14(p, "commands");
3249
+ const commandsDir = join15(p, "commands");
3039
3250
  const commands = isDir(commandsDir) ? readdirSync6(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
3040
3251
  const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
3041
3252
  const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
@@ -3053,11 +3264,11 @@ function profilePlugin(p) {
3053
3264
  };
3054
3265
  }
3055
3266
  function profileMcp(p) {
3056
- const explicitConfig = existsSync11(join14(p, ".mcp.json")) ? join14(p, ".mcp.json") : existsSync11(join14(p, "mcp.json")) ? join14(p, "mcp.json") : void 0;
3267
+ const explicitConfig = existsSync12(join15(p, ".mcp.json")) ? join15(p, ".mcp.json") : existsSync12(join15(p, "mcp.json")) ? join15(p, "mcp.json") : void 0;
3057
3268
  if (explicitConfig) {
3058
3269
  let cfg;
3059
3270
  try {
3060
- cfg = JSON.parse(readFileSync11(explicitConfig, "utf-8"));
3271
+ cfg = JSON.parse(readFileSync12(explicitConfig, "utf-8"));
3061
3272
  } catch (err) {
3062
3273
  throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
3063
3274
  }
@@ -3073,10 +3284,10 @@ function profileMcp(p) {
3073
3284
  capabilities: extractMcpCapabilities(p)
3074
3285
  };
3075
3286
  }
3076
- const pkgPath = join14(p, "package.json");
3287
+ const pkgPath = join15(p, "package.json");
3077
3288
  let pkg2;
3078
3289
  try {
3079
- pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
3290
+ pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3080
3291
  } catch (err) {
3081
3292
  throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
3082
3293
  }
@@ -3093,14 +3304,14 @@ function profileMcp(p) {
3093
3304
  }
3094
3305
 
3095
3306
  // src/gate/mcpOverlap.ts
3096
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
3307
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
3097
3308
  import { resolve as resolve4 } from "path";
3098
3309
  function readTargetServers(targetPath) {
3099
3310
  const path = resolve4(expandHome(targetPath));
3100
- if (!existsSync12(path)) return [];
3311
+ if (!existsSync13(path)) return [];
3101
3312
  let cfg;
3102
3313
  try {
3103
- cfg = JSON.parse(readFileSync12(path, "utf-8"));
3314
+ cfg = JSON.parse(readFileSync13(path, "utf-8"));
3104
3315
  } catch {
3105
3316
  return [];
3106
3317
  }
@@ -3172,6 +3383,121 @@ function scanMcpOverlap(candidate, mcpTargetPaths) {
3172
3383
  };
3173
3384
  }
3174
3385
 
3386
+ // src/gate/skillMap.ts
3387
+ import { readFileSync as readFileSync14 } from "fs";
3388
+ import { resolve as resolve5 } from "path";
3389
+ function loadSkillMap(path) {
3390
+ const resolved = resolve5(expandHome(path));
3391
+ let text;
3392
+ try {
3393
+ text = readFileSync14(resolved, "utf-8");
3394
+ } catch (err) {
3395
+ throw new Error(`could not read skill map at ${resolved}: ${err.message}`);
3396
+ }
3397
+ let parsed;
3398
+ try {
3399
+ parsed = JSON.parse(text);
3400
+ } catch (err) {
3401
+ throw new Error(`skill map at ${resolved} is not valid JSON: ${err.message}`);
3402
+ }
3403
+ if (typeof parsed !== "object" || parsed === null) {
3404
+ throw new Error(`skill map at ${resolved}: expected a JSON object`);
3405
+ }
3406
+ const obj = parsed;
3407
+ if (!Array.isArray(obj.nodes) || !Array.isArray(obj.edges)) {
3408
+ throw new Error(`skill map at ${resolved}: expected "nodes" and "edges" arrays`);
3409
+ }
3410
+ return {
3411
+ schemaVersion: typeof obj.schemaVersion === "string" ? obj.schemaVersion : void 0,
3412
+ nodes: obj.nodes,
3413
+ edges: obj.edges
3414
+ };
3415
+ }
3416
+ function nodeById(map, id) {
3417
+ return map.nodes.find((n) => n.id === id);
3418
+ }
3419
+ function edgesOfType(map, type) {
3420
+ return map.edges.filter((e) => e.type === type);
3421
+ }
3422
+ function tagsForNode(map, node) {
3423
+ const tags = new Set(node.tags ?? []);
3424
+ for (const edge of map.edges) {
3425
+ if (edge.from !== node.id) continue;
3426
+ if (edge.type !== "topic-tag" && edge.type !== "stack-tag") continue;
3427
+ const tagNode = nodeById(map, edge.to);
3428
+ const label = tagNode?.name ?? tagNode?.id;
3429
+ if (label) tags.add(label);
3430
+ }
3431
+ return Array.from(tags);
3432
+ }
3433
+
3434
+ // src/gate/mapOverlap.ts
3435
+ var STOPWORDS2 = new Set(
3436
+ `
3437
+ a an the and or of to for in on with use used using when whether this that these those is are be
3438
+ your you it its as at by from into via per also any all not no than then so if up out about
3439
+ skill skills user claude code based comprehensive enhanced practical including include includes
3440
+ `.split(/\s+/).filter(Boolean)
3441
+ );
3442
+ var STRONG_THRESHOLD = 0.45;
3443
+ var REPORT_THRESHOLD = 0.2;
3444
+ function tokenize2(text) {
3445
+ const matches = text.toLowerCase().match(/[a-z][a-z0-9+.-]{2,}/g) ?? [];
3446
+ return new Set(matches.filter((t) => !STOPWORDS2.has(t)));
3447
+ }
3448
+ function intersectSize2(a, b) {
3449
+ let n = 0;
3450
+ for (const x of a) if (b.has(x)) n += 1;
3451
+ return n;
3452
+ }
3453
+ function unionSize2(a, b) {
3454
+ const u = new Set(a);
3455
+ for (const x of b) u.add(x);
3456
+ return u.size;
3457
+ }
3458
+ function round33(n) {
3459
+ return Math.round(n * 1e3) / 1e3;
3460
+ }
3461
+ function score3(candidateTokens, otherTokens) {
3462
+ if (candidateTokens.size === 0) return 0;
3463
+ const inter = intersectSize2(candidateTokens, otherTokens);
3464
+ const union = unionSize2(candidateTokens, otherTokens);
3465
+ const jaccard = union ? inter / union : 0;
3466
+ const containment = inter / candidateTokens.size;
3467
+ return round33(0.3 * jaccard + 0.7 * containment);
3468
+ }
3469
+ function nodeText(map, node) {
3470
+ const tags = tagsForNode(map, node);
3471
+ return [node.name ?? "", node.description ?? "", ...tags].join(" ");
3472
+ }
3473
+ function scanMapOverlap(candidate, map) {
3474
+ const candidateTokens = tokenize2(`${candidate.name ?? ""} ${candidate.description ?? ""}`);
3475
+ if (candidateTokens.size === 0) return { matches: [] };
3476
+ const matches = [];
3477
+ for (const node of map.nodes) {
3478
+ if (node.type !== "skill") continue;
3479
+ const otherTokens = tokenize2(nodeText(map, node));
3480
+ const s = score3(candidateTokens, otherTokens);
3481
+ if (s >= REPORT_THRESHOLD) {
3482
+ matches.push({ skillId: node.id, name: node.name ?? node.id, score: s });
3483
+ }
3484
+ }
3485
+ matches.sort((a, b) => b.score - a.score);
3486
+ return { matches };
3487
+ }
3488
+ function mapOverlapFinding(result) {
3489
+ const top = result.matches[0];
3490
+ if (!top) return void 0;
3491
+ const severity = top.score >= STRONG_THRESHOLD ? "HIGH" : "MEDIUM";
3492
+ return {
3493
+ severity,
3494
+ rule: "skill-map-overlap",
3495
+ file: "skill-map",
3496
+ detail: `candidate overlaps existing marketplace skill "${top.name}" (score ${top.score}) \u2014 see CLAUDE.md's curation rule: close the gap, don't duplicate`,
3497
+ context: top.skillId
3498
+ };
3499
+ }
3500
+
3175
3501
  // src/report/render.ts
3176
3502
  var SAFETY_VERDICT_MAP = {
3177
3503
  pass: "ALLOW",
@@ -3200,10 +3526,10 @@ function renderSafetyLine(safety) {
3200
3526
  const withDetail = detail ? `${safety.verdict} (${detail})` : safety.verdict;
3201
3527
  return safety.recommendation ? `${withDetail} \u2014 ${safety.recommendation}` : withDetail;
3202
3528
  }
3203
- function renderFindingLine(finding) {
3204
- const severity = finding.severity ?? "UNKNOWN";
3205
- const rule = finding.rule ?? "unknown-rule";
3206
- return ` - [${severity}] ${rule}${finding.message ? `: ${finding.message}` : ""}`;
3529
+ function renderFindingLine(finding2) {
3530
+ const severity = finding2.severity ?? "UNKNOWN";
3531
+ const rule = finding2.rule ?? "unknown-rule";
3532
+ return ` - [${severity}] ${rule}${finding2.message ? `: ${finding2.message}` : ""}`;
3207
3533
  }
3208
3534
  function renderFindingsLines(findings) {
3209
3535
  if (!findings?.length) return [];
@@ -3297,6 +3623,16 @@ function tryMcpOverlap(profile, mcpTargets) {
3297
3623
  return void 0;
3298
3624
  }
3299
3625
  }
3626
+ function tryMapOverlapFinding(skillMapPath, candidate) {
3627
+ if (!skillMapPath) return void 0;
3628
+ try {
3629
+ const map = loadSkillMap(skillMapPath);
3630
+ return mapOverlapFinding(scanMapOverlap(candidate, map));
3631
+ } catch (err) {
3632
+ console.error(`[skill-forge] skill-map overlap check skipped: ${err.message}`);
3633
+ return void 0;
3634
+ }
3635
+ }
3300
3636
  function resolveMcpCandidatePath(entry) {
3301
3637
  if (entry.packageDir) return entry.packageDir;
3302
3638
  let children = [];
@@ -3304,18 +3640,24 @@ function resolveMcpCandidatePath(entry) {
3304
3640
  children = readdirSync7(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
3305
3641
  } catch {
3306
3642
  }
3307
- return children.length === 1 ? join15(entry.quarantinePath, children[0]) : entry.quarantinePath;
3643
+ return children.length === 1 ? join16(entry.quarantinePath, children[0]) : entry.quarantinePath;
3308
3644
  }
3309
3645
  async function runGate(candidatePath, skillsRoot, {
3310
3646
  proFeatureLabel = "overlap analysis",
3311
3647
  strictness = DEFAULT_STRICTNESS,
3312
3648
  artifact = "skill",
3313
- mcpTargets = []
3649
+ mcpTargets = [],
3650
+ skillMapPath
3314
3651
  } = {}) {
3315
3652
  const profile = asProfile(candidatePath, artifact);
3316
3653
  const proStatus = proFeatureStatus(proFeatureLabel);
3317
3654
  const safety = artifact === "mcp" ? await mcpScan(candidatePath, strictness) : await scanSafety(candidatePath, strictness);
3318
3655
  const overlap = !proStatus.enabled ? void 0 : artifact === "mcp" ? tryMcpOverlap(profile, mcpTargets) : tryOverlap(candidatePath, skillsRoot);
3656
+ const mapFinding = artifact === "mcp" ? void 0 : tryMapOverlapFinding(skillMapPath, {
3657
+ name: "name" in profile ? profile.name : void 0,
3658
+ description: "description" in profile ? profile.description : void 0
3659
+ });
3660
+ const combinedSafety = mapFinding ? { verdict: verdictFor([...safety.findings, mapFinding], strictness), findings: [...safety.findings, mapFinding] } : safety;
3319
3661
  const advisory = formatAuditAdvisory(readAuditState(), {
3320
3662
  targetRoot: skillsRoot || void 0,
3321
3663
  candidateName: "name" in profile ? profile.name : void 0
@@ -3326,27 +3668,27 @@ async function runGate(candidatePath, skillsRoot, {
3326
3668
  overlapLocked: !proStatus.enabled,
3327
3669
  notices: proStatus.notice ? [proStatus.notice] : void 0,
3328
3670
  safety: {
3329
- verdict: SAFETY_VERDICT_MAP[safety.verdict],
3330
- findings: mapSafetyFindings(safety.findings)
3671
+ verdict: SAFETY_VERDICT_MAP[combinedSafety.verdict],
3672
+ findings: mapSafetyFindings(combinedSafety.findings)
3331
3673
  },
3332
3674
  advisory
3333
3675
  };
3334
- return { safety, gateResult, proEnabled: proStatus.enabled };
3676
+ return { safety: combinedSafety, gateResult, proEnabled: proStatus.enabled };
3335
3677
  }
3336
3678
 
3337
3679
  // src/queue.ts
3338
3680
  import { randomUUID as randomUUID3 } from "crypto";
3339
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync13, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
3340
- import { join as join16 } from "path";
3681
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync15, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
3682
+ import { join as join17 } from "path";
3341
3683
  function getQueuePath() {
3342
- return join16(getHomeDir(), "queue.json");
3684
+ return join17(getHomeDir(), "queue.json");
3343
3685
  }
3344
3686
  function readQueue() {
3345
3687
  const path = getQueuePath();
3346
- if (!existsSync13(path)) {
3688
+ if (!existsSync14(path)) {
3347
3689
  return { version: 1, entries: [] };
3348
3690
  }
3349
- const raw = readFileSync13(path, "utf-8");
3691
+ const raw = readFileSync15(path, "utf-8");
3350
3692
  const parsed = JSON.parse(raw);
3351
3693
  return { version: 1, entries: parsed.entries ?? [] };
3352
3694
  }
@@ -3354,7 +3696,7 @@ function writeQueue(queue2) {
3354
3696
  const home = getHomeDir();
3355
3697
  mkdirSync6(home, { recursive: true });
3356
3698
  const finalPath = getQueuePath();
3357
- const tempPath = join16(home, `.queue.json.tmp-${randomUUID3()}`);
3699
+ const tempPath = join17(home, `.queue.json.tmp-${randomUUID3()}`);
3358
3700
  try {
3359
3701
  writeFileSync6(tempPath, JSON.stringify(queue2, null, 2) + "\n", "utf-8");
3360
3702
  renameSync4(tempPath, finalPath);
@@ -3403,16 +3745,16 @@ function generateId(existingIds) {
3403
3745
 
3404
3746
  // src/handoff.ts
3405
3747
  import { spawnSync } from "child_process";
3406
- import { existsSync as existsSync14 } from "fs";
3407
- import { dirname as dirname7, join as join17 } from "path";
3748
+ import { existsSync as existsSync15 } from "fs";
3749
+ import { dirname as dirname7, join as join18 } from "path";
3408
3750
  import { fileURLToPath } from "url";
3409
3751
  var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
3410
3752
  var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
3411
3753
  function resolvePromptPath(assetName = "ingest-prompt.md") {
3412
3754
  const here = dirname7(fileURLToPath(import.meta.url));
3413
- const builtCandidate = join17(here, assetName);
3414
- if (existsSync14(builtCandidate)) return builtCandidate;
3415
- return join17(here, "..", "assets", assetName);
3755
+ const builtCandidate = join18(here, assetName);
3756
+ if (existsSync15(builtCandidate)) return builtCandidate;
3757
+ return join18(here, "..", "assets", assetName);
3416
3758
  }
3417
3759
  function defaultIsOnPath(bin) {
3418
3760
  const finder = process.platform === "win32" ? "where" : "which";
@@ -3468,14 +3810,14 @@ function runHandoff(config, installedPath, options = {}) {
3468
3810
  }
3469
3811
 
3470
3812
  // src/commands/init.ts
3471
- import { existsSync as existsSync16 } from "fs";
3813
+ import { existsSync as existsSync17 } from "fs";
3472
3814
  import { homedir as homedir4 } from "os";
3473
3815
  import { createInterface as createInterface3 } from "readline/promises";
3474
3816
 
3475
3817
  // src/agents.ts
3476
3818
  import { statSync as statSync8 } from "fs";
3477
3819
  import { homedir as homedir3 } from "os";
3478
- import { join as join18, resolve as resolve5 } from "path";
3820
+ import { join as join19, resolve as resolve6 } from "path";
3479
3821
  function isFile(p) {
3480
3822
  try {
3481
3823
  return statSync8(p).isFile();
@@ -3484,12 +3826,12 @@ function isFile(p) {
3484
3826
  }
3485
3827
  }
3486
3828
  function projectPath(root, cwd) {
3487
- return root ? resolve5(cwd, root) : void 0;
3829
+ return root ? resolve6(cwd, root) : void 0;
3488
3830
  }
3489
3831
  function globalPath(root, home) {
3490
3832
  if (!root) return void 0;
3491
3833
  if (root === "~") return home;
3492
- if (root.startsWith("~/")) return join18(home, root.slice(2));
3834
+ if (root.startsWith("~/")) return join19(home, root.slice(2));
3493
3835
  return root;
3494
3836
  }
3495
3837
  function makeAgent(spec) {
@@ -3719,8 +4061,8 @@ var AGENTS = [
3719
4061
  ].map(makeAgent);
3720
4062
 
3721
4063
  // src/commands/audit.ts
3722
- import { existsSync as existsSync15 } from "fs";
3723
- import { join as join19, resolve as resolvePath3 } from "path";
4064
+ import { existsSync as existsSync16 } from "fs";
4065
+ import { join as join20, resolve as resolvePath3 } from "path";
3724
4066
  import { createInterface as createInterface2 } from "readline/promises";
3725
4067
 
3726
4068
  // src/decision.ts
@@ -3788,7 +4130,7 @@ async function captureBusinessProfile(rl, existing) {
3788
4130
  function resolveReportPath(reportOption) {
3789
4131
  if (reportOption) return resolvePath3(reportOption);
3790
4132
  const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3791
- return join19(getHomeDir(), "reports", `audit-${iso}.md`);
4133
+ return join20(getHomeDir(), "reports", `audit-${iso}.md`);
3792
4134
  }
3793
4135
  function printTerminalSummary(report, reportPath) {
3794
4136
  const { summary } = report;
@@ -3797,6 +4139,7 @@ function printTerminalSummary(report, reportPath) {
3797
4139
  console.log(
3798
4140
  ` findings: ${summary.findingCounts.CRITICAL} CRITICAL, ${summary.findingCounts.HIGH} HIGH, ${summary.findingCounts.MEDIUM} MEDIUM, ${summary.findingCounts.LOW} LOW`
3799
4141
  );
4142
+ console.log(` accepted: ${summary.acceptedCount} (suppressed from counts; see report)`);
3800
4143
  console.log(` overlap clusters: ${summary.overlapClusterCount} evolve-eligible: ${summary.evolveEligibleCount}`);
3801
4144
  console.log(` report -> ${reportPath}`);
3802
4145
  for (const notice of report.notices) console.log(` [notice] ${notice}`);
@@ -3837,7 +4180,7 @@ async function maybeHandoff(config, reportPath, options, rl) {
3837
4180
  }
3838
4181
  async function auditCommand(options = {}) {
3839
4182
  await maybeOfferInit({ json: options.json, yes: options.yes, suppressAuditOffer: true });
3840
- if (!existsSync15(getConfigPath())) {
4183
+ if (!existsSync16(getConfigPath())) {
3841
4184
  throw new UserError(
3842
4185
  "no skill-forge config found \u2014 run `skill-forge init` first (audit requires a configured skill/MCP set; it never audits an invented default)."
3843
4186
  );
@@ -3853,7 +4196,9 @@ async function auditCommand(options = {}) {
3853
4196
  saveConfig(config);
3854
4197
  }
3855
4198
  }
3856
- const report = await auditConfiguredSet(config);
4199
+ const { records: accepted, notices: storeNotices } = readAcceptedFindings();
4200
+ const report = await auditConfiguredSet(config, { accepted });
4201
+ if (storeNotices.length > 0) report.notices = [...report.notices, ...storeNotices];
3857
4202
  const reportPath = resolveReportPath(options.report);
3858
4203
  writeNewFile(reportPath, renderAuditMarkdown(report), "--report");
3859
4204
  try {
@@ -4180,7 +4525,7 @@ async function init(options = {}, deps = {}) {
4180
4525
  }
4181
4526
  async function maybeOfferInit(options = {}) {
4182
4527
  if (options.json || options.yes || !process.stdin.isTTY) return;
4183
- if (existsSync16(getConfigPath())) return;
4528
+ if (existsSync17(getConfigPath())) return;
4184
4529
  console.log(
4185
4530
  "[skill-forge] No config found \u2014 run `skill-forge init` to detect your agents and set gate targets (init finishes with a `skill-forge audit` health check over what it just configured)."
4186
4531
  );
@@ -4229,7 +4574,8 @@ async function add(source, options = {}) {
4229
4574
  const { safety, gateResult, proEnabled } = await runGate(entry.skillDirs[0], targetRoot, {
4230
4575
  proFeatureLabel: PRO_FEATURE_LABEL,
4231
4576
  strictness: config.strictness,
4232
- artifact
4577
+ artifact,
4578
+ skillMapPath: options.skillMap
4233
4579
  });
4234
4580
  const { profile, overlap } = gateResult;
4235
4581
  if (profile.kind !== "skill") {
@@ -4330,7 +4676,7 @@ async function addMcp(source, config, options) {
4330
4676
  envVars: detail.env ? Object.keys(detail.env) : void 0,
4331
4677
  candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
4332
4678
  };
4333
- const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join20(process.cwd(), ".mcp.json");
4679
+ const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join21(process.cwd(), ".mcp.json");
4334
4680
  const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
4335
4681
  reject(entry);
4336
4682
  logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
@@ -4408,7 +4754,8 @@ async function scan(source, options = {}) {
4408
4754
  const { safety, gateResult } = await runGate(candidatePath, skillsRoot ?? "", {
4409
4755
  strictness: config.strictness,
4410
4756
  artifact,
4411
- mcpTargets: config.mcpTargets ?? []
4757
+ mcpTargets: config.mcpTargets ?? [],
4758
+ skillMapPath: options.skillMap
4412
4759
  });
4413
4760
  if (artifact === "mcp" && gateResult.profile.kind === "mcp" && gateResult.profile.serverCount > 1) {
4414
4761
  console.error(
@@ -4425,7 +4772,7 @@ async function scan(source, options = {}) {
4425
4772
  }
4426
4773
 
4427
4774
  // src/commands/list.ts
4428
- import { join as join21 } from "path";
4775
+ import { join as join22 } from "path";
4429
4776
  async function list() {
4430
4777
  await maybeOfferInit();
4431
4778
  const config = loadConfig();
@@ -4438,14 +4785,14 @@ async function list() {
4438
4785
  }
4439
4786
  console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
4440
4787
  for (const id of ids) {
4441
- const skillDirs = findSkillDirs(join21(quarantineDir, id));
4788
+ const skillDirs = findSkillDirs(join22(quarantineDir, id));
4442
4789
  if (skillDirs.length === 0) {
4443
4790
  console.log(` ${id} (no SKILL.md found)`);
4444
4791
  continue;
4445
4792
  }
4446
4793
  const names = skillDirs.map((dir) => {
4447
4794
  try {
4448
- return readFrontmatter(join21(dir, "SKILL.md")).name;
4795
+ return readFrontmatter(join22(dir, "SKILL.md")).name;
4449
4796
  } catch {
4450
4797
  return dir;
4451
4798
  }
@@ -4455,18 +4802,18 @@ async function list() {
4455
4802
  }
4456
4803
 
4457
4804
  // src/commands/guide.ts
4458
- import { existsSync as existsSync18 } from "fs";
4805
+ import { existsSync as existsSync19 } from "fs";
4459
4806
 
4460
4807
  // src/commands/held.ts
4461
- import { existsSync as existsSync17, realpathSync as realpathSync3 } from "fs";
4462
- import { basename as basename9, join as join22, resolve as resolvePath4, sep as sep2 } from "path";
4808
+ import { existsSync as existsSync18, realpathSync as realpathSync3 } from "fs";
4809
+ import { basename as basename9, join as join23, resolve as resolvePath4, sep as sep2 } from "path";
4463
4810
  var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
4464
4811
  function resolveHeldPath(quarantineDir, id) {
4465
4812
  if (id === "" || id !== basename9(id) || id === "." || id === "..") {
4466
4813
  throw new UserError(`invalid quarantine id "${id}" \u2014 pass a single id exactly as printed by \`skill-forge list\``);
4467
4814
  }
4468
- const candidate = join22(quarantineDir, id);
4469
- if (!existsSync17(candidate)) {
4815
+ const candidate = join23(quarantineDir, id);
4816
+ if (!existsSync18(candidate)) {
4470
4817
  throw new UserError(`no held entry with id "${id}" \u2014 run \`skill-forge list\` to see what is in quarantine`);
4471
4818
  }
4472
4819
  const realRoot = realpathSync3(resolvePath4(quarantineDir));
@@ -4494,7 +4841,7 @@ function resolveTargetRoot(config, target) {
4494
4841
  function heldSkillNames(skillDirs) {
4495
4842
  return skillDirs.map((dir) => {
4496
4843
  try {
4497
- return readFrontmatter(join22(dir, "SKILL.md")).name;
4844
+ return readFrontmatter(join23(dir, "SKILL.md")).name;
4498
4845
  } catch {
4499
4846
  return basename9(dir);
4500
4847
  }
@@ -4502,7 +4849,7 @@ function heldSkillNames(skillDirs) {
4502
4849
  }
4503
4850
  function looksLikeMcpSandbox(quarantinePath) {
4504
4851
  const candidate = resolveMcpCandidatePath({ quarantinePath });
4505
- return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync17(join22(candidate, f)));
4852
+ return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync18(join23(candidate, f)));
4506
4853
  }
4507
4854
  async function promoteHeldMcp(id, entry, config, options) {
4508
4855
  const candidatePath = resolveMcpCandidatePath(entry);
@@ -4536,7 +4883,7 @@ async function promoteHeldMcp(id, entry, config, options) {
4536
4883
  envVars: detail.env ? Object.keys(detail.env) : void 0,
4537
4884
  candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
4538
4885
  };
4539
- const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join22(process.cwd(), ".mcp.json");
4886
+ const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join23(process.cwd(), ".mcp.json");
4540
4887
  const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
4541
4888
  reject(entry);
4542
4889
  logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
@@ -4781,7 +5128,7 @@ var TOPICS = [
4781
5128
  ];
4782
5129
  function readState() {
4783
5130
  const configPath = getConfigPath();
4784
- const configured = existsSync18(configPath);
5131
+ const configured = existsSync19(configPath);
4785
5132
  const base = {
4786
5133
  configured,
4787
5134
  configPath,
@@ -5010,18 +5357,18 @@ async function status() {
5010
5357
 
5011
5358
  // src/evolve.ts
5012
5359
  import { createInterface as createInterface5 } from "readline/promises";
5013
- import { createHash as createHash2 } from "crypto";
5014
- import { cpSync as cpSync3, existsSync as existsSync19, lstatSync as lstatSync3, mkdtempSync as mkdtempSync3, readFileSync as readFileSync14, rmSync as rmSync6 } from "fs";
5360
+ import { createHash as createHash3 } from "crypto";
5361
+ import { cpSync as cpSync3, existsSync as existsSync20, lstatSync as lstatSync3, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, rmSync as rmSync6 } from "fs";
5015
5362
  import { tmpdir as tmpdir2 } from "os";
5016
- import { basename as basename10, dirname as dirname9, join as join23, resolve as resolvePath5, sep as sep3 } from "path";
5363
+ import { basename as basename10, dirname as dirname9, join as join24, resolve as resolvePath5, sep as sep3 } from "path";
5017
5364
  var SKILLOPT_BIN = "skillopt-sleep";
5018
5365
  var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
5019
5366
  async function evolve(targetSkillDir, options = {}, deps = {}) {
5020
5367
  const exec = deps.exec ?? defaultExec;
5021
5368
  const json = options.json;
5022
5369
  const resolvedTargetDir = resolvePath5(targetSkillDir);
5023
- const skillMdPath = join23(resolvedTargetDir, "SKILL.md");
5024
- if (!existsSync19(skillMdPath)) {
5370
+ const skillMdPath = join24(resolvedTargetDir, "SKILL.md");
5371
+ if (!existsSync20(skillMdPath)) {
5025
5372
  throw new UserError(`no SKILL.md found at ${skillMdPath} \u2014 evolve requires an existing, installed skill directory`);
5026
5373
  }
5027
5374
  const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
@@ -5079,33 +5426,33 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
5079
5426
  }
5080
5427
  return;
5081
5428
  }
5082
- const stagingRoot = join23(project, ".skillopt-sleep", "staging");
5429
+ const stagingRoot = join24(project, ".skillopt-sleep", "staging");
5083
5430
  const stagingDir = resolvePath5(parsed.staging_dir);
5084
5431
  if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep3)) {
5085
5432
  throw new UserError(
5086
5433
  `skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
5087
5434
  );
5088
5435
  }
5089
- const proposedSkillMd = join23(stagingDir, "proposed_SKILL.md");
5090
- const proposedClaudeMd = join23(stagingDir, "proposed_CLAUDE.md");
5091
- const reportMdPath = join23(stagingDir, "report.md");
5092
- if (!existsSync19(proposedSkillMd)) {
5436
+ const proposedSkillMd = join24(stagingDir, "proposed_SKILL.md");
5437
+ const proposedClaudeMd = join24(stagingDir, "proposed_CLAUDE.md");
5438
+ const reportMdPath = join24(stagingDir, "report.md");
5439
+ if (!existsSync20(proposedSkillMd)) {
5093
5440
  throw new UserError(`skillopt-sleep reported an accepted proposal but ${proposedSkillMd} is missing`);
5094
5441
  }
5095
- if (existsSync19(reportMdPath)) {
5442
+ if (existsSync20(reportMdPath)) {
5096
5443
  notices.push(`SkillOpt-Sleep's full report: ${reportMdPath}`);
5097
5444
  }
5098
5445
  assertNotSymlink(proposedSkillMd, "proposed_SKILL.md");
5099
5446
  assertNotSymlink(proposedClaudeMd, "proposed_CLAUDE.md");
5100
5447
  const scannedSkillHash = sha256File(proposedSkillMd);
5101
5448
  const scannedClaudeHash = sha256File(proposedClaudeMd);
5102
- const tempDir = mkdtempSync3(join23(tmpdir2(), "skill-forge-evolve-"));
5449
+ const tempDir = mkdtempSync3(join24(tmpdir2(), "skill-forge-evolve-"));
5103
5450
  let profile;
5104
5451
  let safety;
5105
5452
  try {
5106
- cpSync3(proposedSkillMd, join23(tempDir, "SKILL.md"));
5107
- if (existsSync19(proposedClaudeMd)) {
5108
- cpSync3(proposedClaudeMd, join23(tempDir, "CLAUDE.md"));
5453
+ cpSync3(proposedSkillMd, join24(tempDir, "SKILL.md"));
5454
+ if (existsSync20(proposedClaudeMd)) {
5455
+ cpSync3(proposedClaudeMd, join24(tempDir, "CLAUDE.md"));
5109
5456
  }
5110
5457
  profile = asSkillProfile(profileSkill(tempDir), "for the evolved proposal");
5111
5458
  const strictness = loadConfig().strictness;
@@ -5204,7 +5551,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
5204
5551
  if (safetyVerdict === "block") process.exitCode = 1;
5205
5552
  } else {
5206
5553
  logOutcome(`[skill-forge] held \u2014 staging dir left in place -> ${stagingDir}`, json);
5207
- if (existsSync19(reportMdPath)) {
5554
+ if (existsSync20(reportMdPath)) {
5208
5555
  logOutcome(`[skill-forge] see ${reportMdPath} for the full evidence report`, json);
5209
5556
  }
5210
5557
  }
@@ -5246,10 +5593,10 @@ function validateRunOutput(raw) {
5246
5593
  return { accepted: Boolean(obj.accepted), staging_dir: stagingDirRaw ?? null };
5247
5594
  }
5248
5595
  function readManifest(stagingDir) {
5249
- const manifestPath = join23(stagingDir, "manifest.json");
5596
+ const manifestPath = join24(stagingDir, "manifest.json");
5250
5597
  let raw;
5251
5598
  try {
5252
- raw = readFileSync14(manifestPath, "utf-8");
5599
+ raw = readFileSync16(manifestPath, "utf-8");
5253
5600
  } catch (err) {
5254
5601
  throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
5255
5602
  }
@@ -5283,18 +5630,18 @@ function validateManifest(raw, manifestPath) {
5283
5630
  };
5284
5631
  }
5285
5632
  function filesIdentical(a, b) {
5286
- if (!existsSync19(a) || !existsSync19(b)) return false;
5287
- return readFileSync14(a).equals(readFileSync14(b));
5633
+ if (!existsSync20(a) || !existsSync20(b)) return false;
5634
+ return readFileSync16(a).equals(readFileSync16(b));
5288
5635
  }
5289
5636
  function isAlreadyAdopted(stagingDir, manifest) {
5290
- const proposedSkillMd = join23(stagingDir, "proposed_SKILL.md");
5291
- const proposedClaudeMd = join23(stagingDir, "proposed_CLAUDE.md");
5637
+ const proposedSkillMd = join24(stagingDir, "proposed_SKILL.md");
5638
+ const proposedClaudeMd = join24(stagingDir, "proposed_CLAUDE.md");
5292
5639
  let sawComparison = false;
5293
- if (existsSync19(proposedSkillMd)) {
5640
+ if (existsSync20(proposedSkillMd)) {
5294
5641
  sawComparison = true;
5295
5642
  if (!filesIdentical(proposedSkillMd, manifest.live_skill_path)) return false;
5296
5643
  }
5297
- if (existsSync19(proposedClaudeMd) && manifest.live_memory_path) {
5644
+ if (existsSync20(proposedClaudeMd) && manifest.live_memory_path) {
5298
5645
  sawComparison = true;
5299
5646
  if (!filesIdentical(proposedClaudeMd, manifest.live_memory_path)) return false;
5300
5647
  }
@@ -5312,8 +5659,8 @@ function assertNotSymlink(path, label) {
5312
5659
  }
5313
5660
  }
5314
5661
  function sha256File(path) {
5315
- if (!existsSync19(path)) return null;
5316
- return createHash2("sha256").update(readFileSync14(path)).digest("hex");
5662
+ if (!existsSync20(path)) return null;
5663
+ return createHash3("sha256").update(readFileSync16(path)).digest("hex");
5317
5664
  }
5318
5665
 
5319
5666
  // src/commands/evolve.ts
@@ -5321,12 +5668,118 @@ async function evolveCommand(targetSkillDir, options = {}) {
5321
5668
  await evolve(targetSkillDir, options);
5322
5669
  }
5323
5670
 
5671
+ // src/commands/finding.ts
5672
+ import { existsSync as existsSync21 } from "fs";
5673
+ function requireSavedConfig() {
5674
+ if (!existsSync21(getConfigPath())) {
5675
+ throw new UserError(
5676
+ "no skill-forge config found \u2014 run `skill-forge init` first (`finding accept` re-runs the audit engine to resolve a live finding, and audit never runs against an invented default)."
5677
+ );
5678
+ }
5679
+ }
5680
+ function shortDate(iso) {
5681
+ return iso.slice(0, 10) || iso;
5682
+ }
5683
+ async function findingAcceptCommand(prefix, options = {}) {
5684
+ requireSavedConfig();
5685
+ if (prefix.length < MIN_FINGERPRINT_PREFIX_LENGTH) {
5686
+ throw new UserError(`prefix too short \u2014 use at least ${MIN_FINGERPRINT_PREFIX_LENGTH} hex chars (got "${prefix}")`);
5687
+ }
5688
+ const reason = options.reason;
5689
+ if (reason === void 0) {
5690
+ throw new UserError("--reason is required \u2014 a short note on why this finding is accepted");
5691
+ }
5692
+ validateAcceptReason(reason);
5693
+ const config = loadConfig();
5694
+ const { records: accepted } = readAcceptedFindings();
5695
+ const report = await auditConfiguredSet(config, { accepted });
5696
+ const activeMatches = resolveAmongFindings(prefix, report.hygiene);
5697
+ if (activeMatches.length > 1) {
5698
+ const list2 = activeMatches.map((f) => `${f.rule} @ ${f.target} (${f.fingerprint})`).join("; ");
5699
+ throw new UserError(`prefix "${prefix}" matches ${activeMatches.length} current findings \u2014 use a longer prefix: ${list2}`);
5700
+ }
5701
+ if (activeMatches.length === 0) {
5702
+ const alreadyMatches = resolveAmongRecords(prefix, accepted);
5703
+ if (alreadyMatches.length > 0) {
5704
+ const already = alreadyMatches[0];
5705
+ throw new UserError(`already accepted on ${shortDate(already.acceptedAt)}: ${already.reason}`);
5706
+ }
5707
+ throw new UserError(`no current finding matches "${prefix}" \u2014 run \`skill-forge audit\` and copy a fingerprint from the report`);
5708
+ }
5709
+ const finding2 = activeMatches[0];
5710
+ if (finding2.severity === "HIGH" || finding2.severity === "CRITICAL") {
5711
+ throw new UserError(
5712
+ `refusing to accept a ${finding2.severity} finding (${finding2.rule}) \u2014 acceptance is capped at LOW/MEDIUM, and there is no override flag. A HIGH/CRITICAL false positive is a rule bug to report/fix, not something to baseline.`
5713
+ );
5714
+ }
5715
+ const record = {
5716
+ fingerprint: finding2.fingerprint,
5717
+ severity: finding2.severity,
5718
+ rule: finding2.rule,
5719
+ target: finding2.target,
5720
+ context: finding2.context ?? finding2.detail,
5721
+ detail: finding2.detail,
5722
+ reason,
5723
+ acceptedAt: (/* @__PURE__ */ new Date()).toISOString(),
5724
+ source: "user"
5725
+ };
5726
+ writeAcceptedFindings([...accepted, record]);
5727
+ if (options.json) {
5728
+ console.log(JSON.stringify(record, null, 2));
5729
+ return;
5730
+ }
5731
+ console.log(`[skill-forge] accepted [${record.severity}] ${record.rule} \u2014 ${record.target}`);
5732
+ console.log(` fingerprint: ${record.fingerprint}`);
5733
+ console.log(` reason: ${record.reason}`);
5734
+ console.log(' this finding is excluded from active counts, but stays permanently visible under "Accepted findings" in every future report.');
5735
+ }
5736
+ async function findingRevokeCommand(prefix, options = {}) {
5737
+ if (prefix.length < MIN_FINGERPRINT_PREFIX_LENGTH) {
5738
+ throw new UserError(`prefix too short \u2014 use at least ${MIN_FINGERPRINT_PREFIX_LENGTH} hex chars (got "${prefix}")`);
5739
+ }
5740
+ const { records } = readAcceptedFindings();
5741
+ const matches = resolveAmongRecords(prefix, records);
5742
+ if (matches.length > 1) {
5743
+ const list2 = matches.map((r) => `${r.rule} @ ${r.target} (${r.fingerprint})`).join("; ");
5744
+ throw new UserError(`prefix "${prefix}" matches ${matches.length} accepted findings \u2014 use a longer prefix: ${list2}`);
5745
+ }
5746
+ if (matches.length === 0) {
5747
+ throw new UserError(`no accepted finding matches "${prefix}" \u2014 run \`skill-forge finding list\` to see what is accepted`);
5748
+ }
5749
+ const record = matches[0];
5750
+ writeAcceptedFindings(records.filter((r) => r.fingerprint !== record.fingerprint));
5751
+ if (options.json) {
5752
+ console.log(JSON.stringify(record, null, 2));
5753
+ return;
5754
+ }
5755
+ console.log(`[skill-forge] revoked [${record.severity}] ${record.rule} \u2014 ${record.target}`);
5756
+ }
5757
+ async function findingListCommand(options = {}) {
5758
+ const { records, notices } = readAcceptedFindings();
5759
+ for (const notice of notices) console.error(`[skill-forge] ${notice}`);
5760
+ const rows = options.stale ? records.filter((r) => !existsSync21(r.target)) : records;
5761
+ if (options.json) {
5762
+ console.log(JSON.stringify(rows, null, 2));
5763
+ return;
5764
+ }
5765
+ if (rows.length === 0) {
5766
+ console.log(options.stale ? "[skill-forge] no stale accepted findings." : "[skill-forge] no accepted findings.");
5767
+ return;
5768
+ }
5769
+ console.log(`[skill-forge] accepted findings${options.stale ? " (stale \u2014 target no longer exists)" : ""}:`);
5770
+ for (const r of rows) {
5771
+ console.log(
5772
+ ` [${r.severity}] ${sanitizeForMarkdown(r.rule)} \u2014 ${sanitizeForMarkdown(r.target)}: ${sanitizeForMarkdown(r.reason)} (fp ${r.fingerprint.slice(0, FINGERPRINT_DISPLAY_LENGTH)}, accepted ${shortDate(r.acceptedAt)})`
5773
+ );
5774
+ }
5775
+ }
5776
+
5324
5777
  // src/commands/organize.ts
5325
5778
  import { resolve as resolvePath7 } from "path";
5326
5779
 
5327
5780
  // src/organize.ts
5328
- import { existsSync as existsSync20, readFileSync as readFileSync15, readdirSync as readdirSync8, realpathSync as realpathSync4, statSync as statSync9 } from "fs";
5329
- import { basename as basename11, join as join24, resolve as resolvePath6 } from "path";
5781
+ import { existsSync as existsSync22, readFileSync as readFileSync17, readdirSync as readdirSync8, realpathSync as realpathSync4, statSync as statSync9 } from "fs";
5782
+ import { basename as basename11, join as join25, resolve as resolvePath6 } from "path";
5330
5783
  var PRO_FEATURE_LABEL4 = "set-level organizer reports";
5331
5784
  var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
5332
5785
  var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated"]);
@@ -5339,7 +5792,7 @@ function listSubdirs2(dir) {
5339
5792
  }
5340
5793
  const out = [];
5341
5794
  for (const entry of entries) {
5342
- const full = join24(dir, entry.name);
5795
+ const full = join25(dir, entry.name);
5343
5796
  if (entry.isDirectory()) {
5344
5797
  out.push(full);
5345
5798
  continue;
@@ -5357,14 +5810,14 @@ function findSkillDirsUnderRoot2(root) {
5357
5810
  const found = /* @__PURE__ */ new Set();
5358
5811
  const topDirs = listSubdirs2(root);
5359
5812
  for (const dir of topDirs) {
5360
- if (existsSync20(join24(dir, "SKILL.md"))) found.add(dir);
5813
+ if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
5361
5814
  }
5362
- for (const dir of listSubdirs2(join24(root, "skills"))) {
5363
- if (existsSync20(join24(dir, "SKILL.md"))) found.add(dir);
5815
+ for (const dir of listSubdirs2(join25(root, "skills"))) {
5816
+ if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
5364
5817
  }
5365
5818
  for (const topDir of topDirs) {
5366
- for (const dir of listSubdirs2(join24(topDir, "skills"))) {
5367
- if (existsSync20(join24(dir, "SKILL.md"))) found.add(dir);
5819
+ for (const dir of listSubdirs2(join25(topDir, "skills"))) {
5820
+ if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
5368
5821
  }
5369
5822
  }
5370
5823
  return Array.from(found).filter((p) => !p.includes("/.git/"));
@@ -5433,7 +5886,7 @@ function loadUsageSnapshot(snapshotPath) {
5433
5886
  const resolvedPath = resolvePath6(expandHome(snapshotPath));
5434
5887
  let raw;
5435
5888
  try {
5436
- raw = readFileSync15(resolvedPath, "utf-8");
5889
+ raw = readFileSync17(resolvedPath, "utf-8");
5437
5890
  } catch (err) {
5438
5891
  return { usage: /* @__PURE__ */ new Map(), warning: `could not read usage snapshot (${err.message}); proceeding without usage` };
5439
5892
  }
@@ -5539,10 +5992,10 @@ function buildRegistryEntries(candidates, warnings) {
5539
5992
  }
5540
5993
  if (seenRealPaths.has(realPath)) continue;
5541
5994
  seenRealPaths.add(realPath);
5542
- const skillMdPath = join24(realPath, "SKILL.md");
5995
+ const skillMdPath = join25(realPath, "SKILL.md");
5543
5996
  let text;
5544
5997
  try {
5545
- text = readFileSync15(skillMdPath, "utf-8");
5998
+ text = readFileSync17(skillMdPath, "utf-8");
5546
5999
  } catch (err) {
5547
6000
  warnings.push(`could not read ${skillMdPath}: ${err.message}`);
5548
6001
  continue;
@@ -5948,7 +6401,93 @@ async function find(query, options = {}, deps = {}) {
5948
6401
 
5949
6402
  // src/commands/watch.ts
5950
6403
  import { realpathSync as realpathSync5 } from "fs";
5951
- import { join as join25, resolve as resolvePath8 } from "path";
6404
+ import { join as join26, resolve as resolvePath8 } from "path";
6405
+
6406
+ // src/gate/skillMapDrift.ts
6407
+ import { createHash as createHash4 } from "crypto";
6408
+ import { readFileSync as readFileSync18, existsSync as existsSync23 } from "fs";
6409
+ import { resolve as resolve7 } from "path";
6410
+ function sha256(text) {
6411
+ return createHash4("sha256").update(text, "utf-8").digest("hex");
6412
+ }
6413
+ function defaultReadLocalFile(path) {
6414
+ const resolved = resolve7(expandHome(path));
6415
+ if (!existsSync23(resolved)) return void 0;
6416
+ try {
6417
+ return readFileSync18(resolved, "utf-8");
6418
+ } catch {
6419
+ return void 0;
6420
+ }
6421
+ }
6422
+ async function defaultFetchUpstream(url) {
6423
+ try {
6424
+ const res = await fetch(url);
6425
+ if (!res.ok) return void 0;
6426
+ return await res.text();
6427
+ } catch {
6428
+ return void 0;
6429
+ }
6430
+ }
6431
+ function isHttpUrl(value) {
6432
+ return /^https?:\/\//i.test(value.trim());
6433
+ }
6434
+ function upstreamLabel(node, edge) {
6435
+ return node?.url ?? node?.path ?? node?.name ?? node?.id ?? edge.to;
6436
+ }
6437
+ async function computeMapDrift(map, deps = {}) {
6438
+ const readLocalFile = deps.readLocalFile ?? defaultReadLocalFile;
6439
+ const fetchUpstream = deps.fetchUpstream ?? defaultFetchUpstream;
6440
+ const rows = [];
6441
+ for (const edge of edgesOfType(map, "fork-of")) {
6442
+ const localNode = nodeById(map, edge.from);
6443
+ const upstreamNode = nodeById(map, edge.to);
6444
+ const skillLabel = localNode?.name ?? localNode?.id ?? edge.from;
6445
+ const upstream = upstreamLabel(upstreamNode, edge);
6446
+ if (!localNode || !localNode.path || !localNode.contentHash) {
6447
+ rows.push({
6448
+ skill: skillLabel,
6449
+ upstream,
6450
+ status: "local-missing",
6451
+ detail: !localNode ? `fork-of edge "from" node not found in map: ${edge.from}` : 'local skill node missing "path" or "contentHash" \u2014 cannot compare'
6452
+ });
6453
+ continue;
6454
+ }
6455
+ const localResolved = resolve7(expandHome(localNode.path));
6456
+ if (!existsSync23(localResolved)) {
6457
+ rows.push({ skill: skillLabel, upstream, status: "local-missing", detail: `local path does not exist: ${localResolved}` });
6458
+ continue;
6459
+ }
6460
+ const upstreamLocation = upstreamNode?.url ?? upstreamNode?.path;
6461
+ if (!upstreamLocation) {
6462
+ rows.push({
6463
+ skill: skillLabel,
6464
+ upstream,
6465
+ status: "upstream-unreachable",
6466
+ detail: 'upstream node has no "url" or "path" to fetch/read'
6467
+ });
6468
+ continue;
6469
+ }
6470
+ const upstreamText = isHttpUrl(upstreamLocation) ? await fetchUpstream(upstreamLocation) : readLocalFile(upstreamLocation);
6471
+ if (upstreamText === void 0) {
6472
+ rows.push({ skill: skillLabel, upstream, status: "upstream-unreachable", detail: `could not fetch/read upstream: ${upstreamLocation}` });
6473
+ continue;
6474
+ }
6475
+ const upstreamHash = sha256(upstreamText);
6476
+ if (upstreamHash === localNode.contentHash) {
6477
+ rows.push({ skill: skillLabel, upstream, status: "in-sync", detail: `contentHash matches (${upstreamHash.slice(0, 12)}\u2026)` });
6478
+ } else {
6479
+ rows.push({
6480
+ skill: skillLabel,
6481
+ upstream,
6482
+ status: "drifted",
6483
+ detail: `recorded ${localNode.contentHash.slice(0, 12)}\u2026, upstream now hashes to ${upstreamHash.slice(0, 12)}\u2026`
6484
+ });
6485
+ }
6486
+ }
6487
+ return rows;
6488
+ }
6489
+
6490
+ // src/commands/watch.ts
5952
6491
  var PRO_FEATURE_LABEL5 = "provenance drift watch (watch)";
5953
6492
  function isGitUrl(source) {
5954
6493
  const s = source.trim();
@@ -5984,7 +6523,7 @@ async function checkGitDrift(exec, url, ref) {
5984
6523
  async function runWatch(config, options = {}, deps = {}) {
5985
6524
  const proStatus = proFeatureStatus(PRO_FEATURE_LABEL5);
5986
6525
  if (!proStatus.enabled) {
5987
- return { roots: [], rows: [], warnings: [], notices: [proStatus.notice ?? "Pro feature (watch) locked."] };
6526
+ return { roots: [], rows: [], warnings: [], notices: [proStatus.notice ?? "Pro feature (watch) locked."], mapDrift: [] };
5988
6527
  }
5989
6528
  const notices = proStatus.notice ? [proStatus.notice] : [];
5990
6529
  const exec = deps.exec ?? defaultExec;
@@ -6010,7 +6549,7 @@ async function runWatch(config, options = {}, deps = {}) {
6010
6549
  ledger = readProvenanceLedger(resolvedRoot);
6011
6550
  } catch (err) {
6012
6551
  const message = err instanceof Error ? err.message : String(err);
6013
- const ledgerPath = join25(resolvedRoot, "SOURCES.md");
6552
+ const ledgerPath = join26(resolvedRoot, "SOURCES.md");
6014
6553
  roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
6015
6554
  warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
6016
6555
  continue;
@@ -6050,7 +6589,19 @@ async function runWatch(config, options = {}, deps = {}) {
6050
6589
  rows.push({ ...rowBase, checkMethod: "git-ls-remote", driftStatus: status2, detail });
6051
6590
  }
6052
6591
  }
6053
- return { roots, rows, warnings, notices };
6592
+ let mapDrift = [];
6593
+ if (options.skillMap) {
6594
+ try {
6595
+ const map = loadSkillMap(options.skillMap);
6596
+ mapDrift = await computeMapDrift(
6597
+ map,
6598
+ options.offline ? { fetchUpstream: async () => void 0 } : void 0
6599
+ );
6600
+ } catch (err) {
6601
+ warnings.push(`skill map at ${options.skillMap}: ${err.message}`);
6602
+ }
6603
+ }
6604
+ return { roots, rows, warnings, notices, mapDrift };
6054
6605
  }
6055
6606
  function printWatchTerminal(report) {
6056
6607
  console.log("[skill-forge] watch \u2014 provenance drift check");
@@ -6088,6 +6639,13 @@ ${sanitizeForMarkdown(root.root)} (${sanitizeForMarkdown(root.realPath)})`);
6088
6639
  console.log("\nWarnings:");
6089
6640
  for (const w of report.warnings) console.log(` - ${sanitizeForMarkdown(w)}`);
6090
6641
  }
6642
+ if (report.mapDrift.length > 0) {
6643
+ console.log("\nSkill-map fork-of drift:");
6644
+ for (const row of report.mapDrift) {
6645
+ console.log(` \u2022 ${sanitizeForMarkdown(row.skill)} <- ${sanitizeForMarkdown(row.upstream)}`);
6646
+ console.log(` ${row.status} \u2014 ${sanitizeForMarkdown(row.detail)}`);
6647
+ }
6648
+ }
6091
6649
  console.log("\nCompare each stored ref to current upstream; re-run ingestion on any that moved.");
6092
6650
  }
6093
6651
  async function watchCommand(options = {}) {
@@ -6101,8 +6659,8 @@ async function watchCommand(options = {}) {
6101
6659
  }
6102
6660
 
6103
6661
  // src/commands/routine.ts
6104
- import { existsSync as existsSync21, rmSync as rmSync7, statSync as statSync10 } from "fs";
6105
- import { join as join26 } from "path";
6662
+ import { existsSync as existsSync24, rmSync as rmSync7, statSync as statSync10 } from "fs";
6663
+ import { join as join27 } from "path";
6106
6664
  var PRO_FEATURE_LABEL6 = "the scheduled maintenance routine";
6107
6665
  var SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
6108
6666
  function parseFailOnFloor(failOn) {
@@ -6123,7 +6681,7 @@ function exceedsFloor(audit2, failOn) {
6123
6681
  }
6124
6682
  function resolveReportPath2(override, generatedAt) {
6125
6683
  if (override) return override;
6126
- return join26(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
6684
+ return join27(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
6127
6685
  }
6128
6686
  function runHousekeeping(config, options) {
6129
6687
  const actions = [];
@@ -6132,7 +6690,7 @@ function runHousekeeping(config, options) {
6132
6690
  const pruneDays = options.pruneDays ?? 30;
6133
6691
  const cutoff = Date.now() - pruneDays * 864e5;
6134
6692
  for (const id of listQuarantineIds(config.quarantineDir)) {
6135
- const path = join26(config.quarantineDir, id);
6693
+ const path = join27(config.quarantineDir, id);
6136
6694
  let mtime;
6137
6695
  try {
6138
6696
  mtime = statSync10(path).mtimeMs;
@@ -6170,7 +6728,7 @@ function runHousekeeping(config, options) {
6170
6728
  }
6171
6729
  for (const entry of entries) {
6172
6730
  if (entry.status !== "pending") continue;
6173
- if (!entry.installedPath || existsSync21(entry.installedPath)) continue;
6731
+ if (!entry.installedPath || existsSync24(entry.installedPath)) continue;
6174
6732
  if (apply) {
6175
6733
  try {
6176
6734
  updateQueueEntry(entry.id, { status: "dismissed" });
@@ -6203,6 +6761,7 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
6203
6761
  "",
6204
6762
  `- Skills: ${result.audit.skillCount} across ${audit2.summary.rootCount} configured root(s)`,
6205
6763
  `- Findings: ${audit2.summary.findingCounts.CRITICAL ?? 0} critical, ${audit2.summary.findingCounts.HIGH ?? 0} high, ${audit2.summary.findingCounts.MEDIUM ?? 0} medium, ${audit2.summary.findingCounts.LOW ?? 0} low`,
6764
+ `- Accepted: ${audit2.summary.acceptedCount} (suppressed from the counts above; see Accepted findings)`,
6206
6765
  `- Overlap clusters: ${result.audit.overlapClusters.length}${result.audit.overlapLocked ? ' (overlap analysis was locked \u2014 not "none found")' : ""}`,
6207
6766
  `- Pending queue entries: ${result.pending.queue}`,
6208
6767
  `- Held in quarantine: ${result.pending.quarantine}`
@@ -6213,6 +6772,10 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
6213
6772
  if (organize) {
6214
6773
  lines.push(`- Capability registry: ${result.organize?.skills ?? 0} skill(s) across ${result.organize?.roots ?? 0} root(s)`);
6215
6774
  }
6775
+ if (result.notices.length > 0) {
6776
+ lines.push("", "## Notices", "");
6777
+ for (const item of result.notices) lines.push(`- ${sanitizeForMarkdown(item)}`);
6778
+ }
6216
6779
  if (result.degraded.length > 0) {
6217
6780
  lines.push("", "## Incomplete steps", "");
6218
6781
  for (const item of result.degraded) lines.push(`- ${sanitizeForMarkdown(item)}`);
@@ -6228,9 +6791,18 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
6228
6791
  }
6229
6792
  if (audit2.hygiene.length > 0) {
6230
6793
  lines.push("", "## Findings", "");
6231
- for (const finding of audit2.hygiene) {
6794
+ for (const finding2 of audit2.hygiene) {
6795
+ const fp = finding2.fingerprint ? finding2.fingerprint.slice(0, 12) : "unknown";
6796
+ lines.push(
6797
+ `- **${finding2.severity}** \`${sanitizeForMarkdown(finding2.rule)}\` \u2014 ${sanitizeForMarkdown(finding2.target)}: ${sanitizeForMarkdown(finding2.detail)} (fp ${fp})`
6798
+ );
6799
+ }
6800
+ }
6801
+ if (audit2.accepted.length > 0) {
6802
+ lines.push("", "## Accepted findings", "");
6803
+ for (const entry of audit2.accepted) {
6232
6804
  lines.push(
6233
- `- **${finding.severity}** \`${sanitizeForMarkdown(finding.rule)}\` \u2014 ${sanitizeForMarkdown(finding.target)}: ${sanitizeForMarkdown(finding.detail)}`
6805
+ `- **${entry.finding.severity}** \`${sanitizeForMarkdown(entry.finding.rule)}\` \u2014 ${sanitizeForMarkdown(entry.finding.target)}: ${sanitizeForMarkdown(entry.reason)} (accepted ${sanitizeForMarkdown(entry.acceptedAt.slice(0, 10))})`
6234
6806
  );
6235
6807
  }
6236
6808
  }
@@ -6260,7 +6832,9 @@ async function routineCommand(options = {}) {
6260
6832
  }
6261
6833
  const config = loadConfig();
6262
6834
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
6263
- const audit2 = await auditConfiguredSet(config);
6835
+ const { records: accepted, notices: storeNotices } = readAcceptedFindings();
6836
+ for (const notice of storeNotices) log(`[skill-forge] ${notice}`);
6837
+ const audit2 = await auditConfiguredSet(config, { accepted });
6264
6838
  const auditState = summarizeAudit(audit2);
6265
6839
  const degraded = [];
6266
6840
  let watch;
@@ -6303,6 +6877,7 @@ async function routineCommand(options = {}) {
6303
6877
  organize: organize ? { skills: organize.registry.count, roots: organize.registry.skillsRoots.length, notices: organize.notices } : void 0,
6304
6878
  pending: { queue: pending.count, quarantine: quarantine2.count },
6305
6879
  housekeeping,
6880
+ notices: storeNotices,
6306
6881
  degraded
6307
6882
  };
6308
6883
  const failure = degraded.length > 0 ? `${degraded.length} step(s) did not complete: ${degraded.join("; ")}` : exceedsFloor(auditState, options.failOn ?? "none");
@@ -6321,6 +6896,7 @@ async function routineCommand(options = {}) {
6321
6896
  console.log(
6322
6897
  ` findings: ${auditState.findings.CRITICAL ?? 0} critical / ${auditState.findings.HIGH ?? 0} high / ${auditState.findings.MEDIUM ?? 0} medium / ${auditState.findings.LOW ?? 0} low`
6323
6898
  );
6899
+ console.log(` accepted: ${auditState.acceptedCount ?? 0} (suppressed from counts; see report)`);
6324
6900
  if (result.watch) console.log(` drift: ${result.watch.moved} moved of ${result.watch.checked} tracked`);
6325
6901
  if (result.organize) console.log(` registry: ${result.organize.skills} skill(s)`);
6326
6902
  console.log(` pending: ${result.pending.queue} queue, ${result.pending.quarantine} quarantine`);
@@ -6470,14 +7046,14 @@ async function queueClose(id, options = {}) {
6470
7046
  }
6471
7047
 
6472
7048
  // src/commands/refine.ts
6473
- import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
6474
- import { chmodSync, existsSync as existsSync24, lstatSync as lstatSync4, mkdirSync as mkdirSync9, readFileSync as readFileSync17, realpathSync as realpathSync9, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
6475
- import { dirname as dirname13, isAbsolute as isAbsolute4, join as join29, relative as relative4, resolve as resolvePath12, sep as sep4 } from "path";
7049
+ import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
7050
+ import { chmodSync, existsSync as existsSync27, lstatSync as lstatSync4, mkdirSync as mkdirSync9, readFileSync as readFileSync20, realpathSync as realpathSync9, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
7051
+ import { dirname as dirname13, isAbsolute as isAbsolute4, join as join30, relative as relative4, resolve as resolvePath12, sep as sep4 } from "path";
6476
7052
  import { createInterface as createInterface6 } from "readline/promises";
6477
7053
 
6478
7054
  // src/refine/context.ts
6479
- import { existsSync as existsSync23, realpathSync as realpathSync8, statSync as statSync12 } from "fs";
6480
- import { isAbsolute as isAbsolute3, join as join28, relative as relative3, resolve as resolvePath11 } from "path";
7055
+ import { existsSync as existsSync26, realpathSync as realpathSync8, statSync as statSync12 } from "fs";
7056
+ import { isAbsolute as isAbsolute3, join as join29, relative as relative3, resolve as resolvePath11 } from "path";
6481
7057
 
6482
7058
  // src/refine/models.ts
6483
7059
  import { randomBytes } from "crypto";
@@ -6533,40 +7109,40 @@ function fingerprintKey(skill, target, overrideType, action) {
6533
7109
  return [normalize(skill), normalize(target), overrideType, action ?? ""].join("::");
6534
7110
  }
6535
7111
  function contentSimilarity(a, b) {
6536
- const tokenize2 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
6537
- const tokensA = tokenize2(a);
6538
- const tokensB = tokenize2(b);
7112
+ const tokenize3 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
7113
+ const tokensA = tokenize3(a);
7114
+ const tokensB = tokenize3(b);
6539
7115
  if (tokensA.size === 0 || tokensB.size === 0) return 0;
6540
7116
  let intersectionSize = 0;
6541
7117
  for (const token of tokensA) {
6542
7118
  if (tokensB.has(token)) intersectionSize++;
6543
7119
  }
6544
- const unionSize2 = tokensA.size + tokensB.size - intersectionSize;
6545
- return unionSize2 === 0 ? 0 : intersectionSize / unionSize2;
7120
+ const unionSize3 = tokensA.size + tokensB.size - intersectionSize;
7121
+ return unionSize3 === 0 ? 0 : intersectionSize / unionSize3;
6546
7122
  }
6547
7123
 
6548
7124
  // src/refine/store.ts
6549
- import { createHash as createHash3 } from "crypto";
6550
- import { existsSync as existsSync22, readFileSync as readFileSync16, statSync as statSync11, realpathSync as realpathSync7 } from "fs";
6551
- import { join as join27, resolve as resolvePath10 } from "path";
7125
+ import { createHash as createHash5 } from "crypto";
7126
+ import { existsSync as existsSync25, readFileSync as readFileSync19, statSync as statSync11, realpathSync as realpathSync7 } from "fs";
7127
+ import { join as join28, resolve as resolvePath10 } from "path";
6552
7128
  var SIMILARITY_JOIN_THRESHOLD = 0.7;
6553
7129
  var READY_THRESHOLD = 2;
6554
7130
  function getRefinementsDir() {
6555
- return join27(getHomeDir(), "refinements");
7131
+ return join28(getHomeDir(), "refinements");
6556
7132
  }
6557
7133
  function getRefinementsPath() {
6558
- return join27(getRefinementsDir(), "refinements.json");
7134
+ return join28(getRefinementsDir(), "refinements.json");
6559
7135
  }
6560
7136
  function getPatternsPath() {
6561
- return join27(getRefinementsDir(), "patterns.json");
7137
+ return join28(getRefinementsDir(), "patterns.json");
6562
7138
  }
6563
7139
  function getBackupsDir() {
6564
- return join27(getRefinementsDir(), "backups");
7140
+ return join28(getRefinementsDir(), "backups");
6565
7141
  }
6566
7142
  function readJsonFile(path, label) {
6567
7143
  let raw;
6568
7144
  try {
6569
- raw = readFileSync16(path, "utf-8");
7145
+ raw = readFileSync19(path, "utf-8");
6570
7146
  } catch (err) {
6571
7147
  throw new UserError(`unable to read ${label} at ${path}: ${err.message}`);
6572
7148
  }
@@ -6577,7 +7153,7 @@ function readJsonFile(path, label) {
6577
7153
  }
6578
7154
  }
6579
7155
  function readStore(path, label, validateRecord) {
6580
- if (!existsSync22(path)) {
7156
+ if (!existsSync25(path)) {
6581
7157
  return { schemaVersion: 1, records: [] };
6582
7158
  }
6583
7159
  const parsed = readJsonFile(path, label);
@@ -6801,16 +7377,16 @@ function createBackup(filePaths, deps = {}) {
6801
7377
  const randomHex = deps.randomHex ?? defaultRandomHex;
6802
7378
  const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
6803
7379
  const files = filePaths.map((path) => {
6804
- if (!existsSync22(path)) {
7380
+ if (!existsSync25(path)) {
6805
7381
  return { path, existed: false, sha256: null, mode: null, contentBase64: null };
6806
7382
  }
6807
- const bytes = readFileSync16(path);
7383
+ const bytes = readFileSync19(path);
6808
7384
  const mode = statSync11(path).mode & 511;
6809
- const sha256 = createHash3("sha256").update(bytes).digest("hex");
6810
- return { path, existed: true, sha256, mode, contentBase64: bytes.toString("base64") };
7385
+ const sha2562 = createHash5("sha256").update(bytes).digest("hex");
7386
+ return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
6811
7387
  });
6812
7388
  const manifest = { id, createdAt: now.toISOString(), files };
6813
- writeJsonAtomic(join27(getBackupsDir(), id, "manifest.json"), manifest);
7389
+ writeJsonAtomic(join28(getBackupsDir(), id, "manifest.json"), manifest);
6814
7390
  return manifest;
6815
7391
  }
6816
7392
  function validateBackupManifestShape(value, path) {
@@ -6859,8 +7435,8 @@ function readBackupManifest(id) {
6859
7435
  if (!isValidBackupId(id)) {
6860
7436
  throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
6861
7437
  }
6862
- const manifestPath = join27(getBackupsDir(), id, "manifest.json");
6863
- if (!existsSync22(manifestPath)) {
7438
+ const manifestPath = join28(getBackupsDir(), id, "manifest.json");
7439
+ if (!existsSync25(manifestPath)) {
6864
7440
  throw new UserError(`no backup with id: ${id}`);
6865
7441
  }
6866
7442
  const parsed = readJsonFile(manifestPath, "backup manifest");
@@ -6894,20 +7470,20 @@ function isUnderRoot2(candidate, root) {
6894
7470
  function resolveScopes(config, cwd, skill, userRootFlag) {
6895
7471
  assertPlainSkillName(skill);
6896
7472
  const resolvedCwd = resolvePath11(cwd);
6897
- const localDir = join28(resolvedCwd, ".claude", "skills", skill);
6898
- const sharedDir = join28(resolvedCwd, "skills", skill);
6899
- const local = { scope: "local", dir: localDir, exists: existsSync23(localDir) };
6900
- const shared = { scope: "shared", dir: sharedDir, exists: existsSync23(sharedDir) };
7473
+ const localDir = join29(resolvedCwd, ".claude", "skills", skill);
7474
+ const sharedDir = join29(resolvedCwd, "skills", skill);
7475
+ const local = { scope: "local", dir: localDir, exists: existsSync26(localDir) };
7476
+ const shared = { scope: "shared", dir: sharedDir, exists: existsSync26(sharedDir) };
6901
7477
  const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
6902
7478
  return { local, shared, user };
6903
7479
  }
6904
7480
  function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
6905
7481
  if (userRootFlag) {
6906
7482
  const root = resolvePath11(userRootFlag);
6907
- return { resolved: true, dir: join28(root, skill), root: userRootFlag, reason: "flag" };
7483
+ return { resolved: true, dir: join29(root, skill), root: userRootFlag, reason: "flag" };
6908
7484
  }
6909
7485
  const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
6910
- const candidates = config.skillsRoots.map((root) => ({ root, dir: join28(resolveConfiguredRoot2(root), skill) }));
7486
+ const candidates = config.skillsRoots.map((root) => ({ root, dir: join29(resolveConfiguredRoot2(root), skill) }));
6911
7487
  for (const candidate of candidates) {
6912
7488
  const skillReal = realpathOrNull2(candidate.dir);
6913
7489
  if (skillReal) {
@@ -6942,12 +7518,12 @@ function inventoryScopeDir(scope, dir) {
6942
7518
  scope,
6943
7519
  dir,
6944
7520
  dirExists: isDirectory(dir),
6945
- skillMd: isFile2(join28(dir, "SKILL.md")),
6946
- patchMd: isFile2(join28(dir, "SKILL.patch.md")),
6947
- extendMd: isFile2(join28(dir, "SKILL.extend.md")),
6948
- configJson: isFile2(join28(dir, "skill-config.json")),
6949
- hooksDir: isDirectory(join28(dir, "hooks")),
6950
- scriptsDir: isDirectory(join28(dir, "scripts"))
7521
+ skillMd: isFile2(join29(dir, "SKILL.md")),
7522
+ patchMd: isFile2(join29(dir, "SKILL.patch.md")),
7523
+ extendMd: isFile2(join29(dir, "SKILL.extend.md")),
7524
+ configJson: isFile2(join29(dir, "skill-config.json")),
7525
+ hooksDir: isDirectory(join29(dir, "hooks")),
7526
+ scriptsDir: isDirectory(join29(dir, "scripts"))
6951
7527
  };
6952
7528
  }
6953
7529
  function inventoryAllScopes(resolution) {
@@ -7227,7 +7803,7 @@ function sanitizeDiffForConsole(diff) {
7227
7803
  }
7228
7804
  function resolveContentOption(options) {
7229
7805
  if (options.content !== void 0) return options.content;
7230
- if (options.contentFile) return readFileSync17(resolvePath12(options.contentFile), "utf-8");
7806
+ if (options.contentFile) return readFileSync20(resolvePath12(options.contentFile), "utf-8");
7231
7807
  return void 0;
7232
7808
  }
7233
7809
  function validateCaptureOptions(options) {
@@ -7379,7 +7955,7 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
7379
7955
  }
7380
7956
  function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
7381
7957
  const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
7382
- const destPath = join29(scopeDir, relPath);
7958
+ const destPath = join30(scopeDir, relPath);
7383
7959
  const destDir = dirname13(destPath);
7384
7960
  mkdirSync9(destDir, { recursive: true });
7385
7961
  const realDestDir = realpathSync9(destDir);
@@ -7475,12 +8051,12 @@ Write ${preview.destPath}? [y/N] `);
7475
8051
  }
7476
8052
  function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
7477
8053
  const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
7478
- const probePath = probeRelPath ? join29(scopeDir, probeRelPath) : void 0;
7479
- const existingContent = probePath && existsSync24(probePath) ? readFileSync17(probePath, "utf-8") : "";
8054
+ const probePath = probeRelPath ? join30(scopeDir, probeRelPath) : void 0;
8055
+ const existingContent = probePath && existsSync27(probePath) ? readFileSync20(probePath, "utf-8") : "";
7480
8056
  const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
7481
8057
  const relPath = rendered.relPath;
7482
- const destPath = join29(scopeDir, relPath);
7483
- const before = existsSync24(destPath) ? readFileSync17(destPath, "utf-8") : "";
8058
+ const destPath = join30(scopeDir, relPath);
8059
+ const before = existsSync27(destPath) ? readFileSync20(destPath, "utf-8") : "";
7484
8060
  const diff = previewDiff(before, rendered.content, destPath);
7485
8061
  return { relPath, content: rendered.content, destPath, diff };
7486
8062
  }
@@ -7577,16 +8153,16 @@ async function refineWhichCommand(skill, options = {}) {
7577
8153
  }
7578
8154
  function writePostWriteSidecar(backupId, files) {
7579
8155
  const sidecar = { schemaVersion: 1, files };
7580
- const dir = join29(getBackupsDir(), backupId);
8156
+ const dir = join30(getBackupsDir(), backupId);
7581
8157
  mkdirSync9(dir, { recursive: true });
7582
- writeFileSync9(join29(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
8158
+ writeFileSync9(join30(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
7583
8159
  }
7584
8160
  function readPostWriteSidecar(backupId) {
7585
- const path = join29(getBackupsDir(), backupId, "post.json");
7586
- if (!existsSync24(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
8161
+ const path = join30(getBackupsDir(), backupId, "post.json");
8162
+ if (!existsSync27(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
7587
8163
  let raw;
7588
8164
  try {
7589
- raw = readFileSync17(path, "utf-8");
8165
+ raw = readFileSync20(path, "utf-8");
7590
8166
  } catch {
7591
8167
  return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
7592
8168
  }
@@ -7644,8 +8220,8 @@ async function refinePromoteCommand(patternId, options = {}) {
7644
8220
  if (!isUnderRoot3(realSkillDir, realUserRoot)) {
7645
8221
  throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
7646
8222
  }
7647
- const skillMdPath = join29(skillDir, "SKILL.md");
7648
- if (!existsSync24(skillMdPath)) {
8223
+ const skillMdPath = join30(skillDir, "SKILL.md");
8224
+ if (!existsSync27(skillMdPath)) {
7649
8225
  throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
7650
8226
  }
7651
8227
  const refinementsStore = readRefinementsStore();
@@ -7662,7 +8238,7 @@ async function refinePromoteCommand(patternId, options = {}) {
7662
8238
  `pattern ${patternId}'s representative refinement is a '${representative.overrideType}' override \u2014 promote currently only materializes patch/extend/new-type patterns against the user-scope SKILL.md; other override types must be merged manually.`
7663
8239
  );
7664
8240
  }
7665
- const before = readFileSync17(skillMdPath, "utf-8");
8241
+ const before = readFileSync20(skillMdPath, "utf-8");
7666
8242
  const after = applyPatchBlocks(before, [block]);
7667
8243
  const diff = previewDiff(before, after, skillMdPath);
7668
8244
  if (options.dryRun) {
@@ -7694,7 +8270,7 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
7694
8270
  throw err;
7695
8271
  }
7696
8272
  try {
7697
- const postSha256 = createHash4("sha256").update(readFileSync17(skillMdPath)).digest("hex");
8273
+ const postSha256 = createHash6("sha256").update(readFileSync20(skillMdPath)).digest("hex");
7698
8274
  writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
7699
8275
  markGeneralized(patternId, backup.id);
7700
8276
  const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
@@ -7720,10 +8296,10 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
7720
8296
  }
7721
8297
  function assertBackupDirContained(backupId) {
7722
8298
  const backupsDir = getBackupsDir();
7723
- if (!existsSync24(backupsDir)) return;
8299
+ if (!existsSync27(backupsDir)) return;
7724
8300
  const realBackupsDir = realpathSync9(backupsDir);
7725
- const backupDir = join29(backupsDir, backupId);
7726
- if (!existsSync24(backupDir)) return;
8301
+ const backupDir = join30(backupsDir, backupId);
8302
+ if (!existsSync27(backupDir)) return;
7727
8303
  const realBackupDir = realpathSync9(backupDir);
7728
8304
  if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
7729
8305
  throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
@@ -7735,7 +8311,7 @@ function resolveTargetRealPath(path) {
7735
8311
  try {
7736
8312
  const real = realpathSync9(lexicalAncestor);
7737
8313
  const remainder = relative4(lexicalAncestor, resolvePath12(path));
7738
- return remainder === "" ? real : join29(real, remainder);
8314
+ return remainder === "" ? real : join30(real, remainder);
7739
8315
  } catch {
7740
8316
  const parent = dirname13(lexicalAncestor);
7741
8317
  if (parent === lexicalAncestor) return resolvePath12(path);
@@ -7804,7 +8380,7 @@ function walkRealComponents(filePath, config, createMissing) {
7804
8380
  let current = rootReal;
7805
8381
  for (const component of dirComponents) {
7806
8382
  if (current === void 0) break;
7807
- const next = join29(current, component);
8383
+ const next = join30(current, component);
7808
8384
  let lst;
7809
8385
  try {
7810
8386
  lst = lstatSync4(next);
@@ -7835,7 +8411,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
7835
8411
  if (parentReal === void 0) {
7836
8412
  throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
7837
8413
  }
7838
- const finalPath = join29(parentReal, leafComponent);
8414
+ const finalPath = join30(parentReal, leafComponent);
7839
8415
  if (!isUnderRoot3(finalPath, rootReal)) {
7840
8416
  throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
7841
8417
  }
@@ -7861,7 +8437,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
7861
8437
  function safeDeleteTombstone(filePath, config) {
7862
8438
  const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
7863
8439
  if (parentReal === void 0) return;
7864
- const finalPath = join29(parentReal, leafComponent);
8440
+ const finalPath = join30(parentReal, leafComponent);
7865
8441
  if (!isUnderRoot3(finalPath, rootReal)) {
7866
8442
  throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
7867
8443
  }
@@ -7903,7 +8479,7 @@ async function refineRollbackCommand(backupId, options = {}) {
7903
8479
  for (const file of manifest.files) {
7904
8480
  const expected = sidecar.hashes.get(file.path);
7905
8481
  if (expected === void 0) continue;
7906
- const currentSha256 = existsSync24(file.path) ? createHash4("sha256").update(readFileSync17(file.path)).digest("hex") : null;
8482
+ const currentSha256 = existsSync27(file.path) ? createHash6("sha256").update(readFileSync20(file.path)).digest("hex") : null;
7907
8483
  if (currentSha256 !== expected) mismatches.push(file.path);
7908
8484
  }
7909
8485
  if (mismatches.length > 0 && !options.force) {
@@ -7999,8 +8575,14 @@ program.name("skill-forge").description("The supply-chain gate for agent skills"
7999
8575
  program.command("add <source>").description("Quarantine-install a skill and run it through the safety gate").option("-y, --yes", "skip the interactive prompt; honor the gate verdict automatically").option("-t, --target <dir>", "skills root to promote into (defaults to config defaultTarget, then skillsRoots[0])").option("--json", "print the gate result as JSON instead of the terminal report").option("--ingest", "after a successful promote, hand off the bundled ingest prompt to a coding agent").option("--artifact <type>", 'gate as a "skill" or an "mcp" server (default: skill)', parseArtifact).option(
8000
8576
  "--mcp-target <file>",
8001
8577
  "MCP config file to promote into (mcp artifacts only; defaults to config.mcpTargets[0], then <cwd>/.mcp.json)"
8002
- ).option("--force", "overwrite an existing same-name MCP server entry when promoting (mcp artifacts only)").action(add);
8003
- program.command("scan <source>").description("Run the safety gate against a skill without installing it").option("--json", "print the gate result as JSON instead of the terminal report").option("--artifact <type>", 'gate as a "skill" or an "mcp" server (default: skill)', parseArtifact).action(scan);
8578
+ ).option("--force", "overwrite an existing same-name MCP server entry when promoting (mcp artifacts only)").option(
8579
+ "--skill-map <path>",
8580
+ "path to a generated rhize-plugins skill map; reports existing-capability overlap against every skill node in it before promotion (curation-rule check)"
8581
+ ).action(add);
8582
+ program.command("scan <source>").description("Run the safety gate against a skill without installing it").option("--json", "print the gate result as JSON instead of the terminal report").option("--artifact <type>", 'gate as a "skill" or an "mcp" server (default: skill)', parseArtifact).option(
8583
+ "--skill-map <path>",
8584
+ "path to a generated rhize-plugins skill map; reports existing-capability overlap against every skill node in it (curation-rule check)"
8585
+ ).action(scan);
8004
8586
  program.command("list").description("List skills currently in quarantine").action(list);
8005
8587
  program.command("status").description("Show skill-forge configuration and quarantine status").action(status);
8006
8588
  program.command("promote <id>").description("Re-gate a held quarantine entry and promote it (finishes a decision left on hold)").option("-t, --target <dir>", "skills root to promote into (defaults to config defaultTarget, then skillsRoots[0])").option("--force", "overwrite an existing same-name skill dir / MCP server entry in the target").option("--mcp-target <file>", "MCP config file to promote a held MCP candidate into (defaults to config.mcpTargets[0], then <cwd>/.mcp.json)").option("-y, --yes", "skip the prompt; honor the re-gate verdict automatically").option("--json", "print the re-gate result as JSON instead of the terminal report").action(promoteHeld);
@@ -8015,9 +8597,16 @@ configCmd.command("review").description("Review pending agent-proposed preferenc
8015
8597
  program.command("guide [topic]").description("What this tool does, what state you are in, and the next command to run").action(guide);
8016
8598
  program.command("evolve <target-skill-dir>").description("Self-evolve a skill via SkillOpt-Sleep, re-gate the proposal, then promote/hold/reject (Pro)").option("--project <dir>", "project dir passed to SkillOpt-Sleep as --project (defaults to cwd)").option("--dry-run", "SkillOpt-Sleep dry-run: report only, nothing staged").option("--backend <name>", "SkillOpt-Sleep backend (default: mock, offline \u2014 a real backend requires --yes or interactive confirmation)").option("--lookback-hours <n>", "hours of session history for SkillOpt-Sleep to harvest", parseLookbackHours).option("-y, --yes", "skip interactive prompts; honor the re-gate verdict automatically").option("--json", "print the re-gate result as JSON instead of the terminal report").option("--force", "allow re-adopting a staging dir whose proposed content already matches the live skill").action(evolveCommand);
8017
8599
  program.command("audit").alias("doctor").description("Doctor-style health check over the configured skill/MCP set: inventory, hygiene findings, and consolidation/refinement opportunities").option("--json", "print the audit report as JSON instead of the terminal summary (non-interactive: skips the business-profile prompt)").option("-y, --yes", "skip interactive prompts (business profile, foundation, handoff); never implies --foundation or --handoff").option("--report <file>", "write the report to this path instead of the default ~/.skill-forge/reports/audit-<timestamp>.md (refuses an existing path)").option("--foundation", "authorizes exactly one skills-root write: scaffold a business-foundation skill from the captured business profile").option("--handoff", "authorizes launching the configured agent with the bundled curation prompt against the written report").action(auditCommand);
8600
+ var finding = program.command("finding").description("Review and acknowledge audit findings \u2014 human-only, never a gate input (see docs/accepted-findings-schema.md)");
8601
+ finding.command("accept <fingerprint-prefix>").description("Accept a currently-active LOW/MEDIUM finding (no override for HIGH/CRITICAL); re-runs the audit engine to resolve it").requiredOption("--reason <text>", "why this finding is accepted (mandatory, 1-200 chars, no control characters)").option("--json", "print the accepted record as JSON").action(findingAcceptCommand);
8602
+ finding.command("revoke <fingerprint-prefix>").description("Remove a stored acceptance so the finding reports again").option("--json", "print the removed record as JSON").action(findingRevokeCommand);
8603
+ finding.command("list", { isDefault: true }).description("List every accepted finding").option("--stale", "only records whose target no longer exists on disk").option("--json", "print records as JSON").action(findingListCommand);
8018
8604
  program.command("organize").description("Build the set-level capability registry + dependency graph across configured skills roots (Pro)").option("--json", "print the registry+graph report as JSON instead of the terminal summary").option("--out <file>", "write the full report JSON to this path (refuses an existing path)").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON").action(organizeCommand);
8019
8605
  program.command("find [query]").description("Discover skills via skills.sh and check partner security audits (requires VERCEL_OIDC_TOKEN)").option("--limit <n>", "max search results (default 10)", parseLimit).option("--audit <id>", "show partner audit verdicts (Socket/Snyk/\u2026) for a skills.sh id").option("--get <id>", "show detail + file tree for a skills.sh id").option("--curated", "list skills.sh curated skills").option("--json", "print the raw API payload as JSON").action(find);
8020
- program.command("watch").description("Drift check across every SOURCES.md provenance ledger: which ingested upstreams have moved (Pro)").option("--offline", "skip all network checks; list entries for manual comparison only").option("--json", "print the drift report as JSON instead of the terminal summary").action(watchCommand);
8606
+ program.command("watch").description("Drift check across every SOURCES.md provenance ledger: which ingested upstreams have moved (Pro)").option("--offline", "skip all network checks; list entries for manual comparison only").option("--json", "print the drift report as JSON instead of the terminal summary").option(
8607
+ "--skill-map <path>",
8608
+ "also drift-check every fork-of edge in a generated rhize-plugins skill map (contentHash compare against upstream)"
8609
+ ).action(watchCommand);
8021
8610
  program.command("routine").description("One scheduled maintenance pass: audit + drift + registry, one report, cron-friendly exit codes (Pro)").option("--json", "print the routine result as JSON instead of the terminal summary").option("--offline", "skip every network check (drift entries are reported unchecked)").option("--skip-watch", "skip the drift check").option("--skip-organize", "skip the capability registry").option("--fail-on <level>", "exit 1 when a finding at or above this severity exists: none|low|medium|high|critical|any (default none)", parseFailOn).option("--housekeeping", "opt in to bounded writes: prune expired quarantine sandboxes, close orphaned queue entries").option("--prune-days <n>", "quarantine age threshold in days for --housekeeping (default 30)", parsePruneDays).option("--dry-run", "with --housekeeping: report what would change, change nothing").option("--handoff", "hand the written report to the configured agent (curation prompt)").option("--report <file>", "write the report here instead of ~/.skill-forge/reports/routine-<timestamp>.md (refuses an existing path)").action(routineCommand);
8022
8611
  program.command("ingest").description(
8023
8612
  "Hand the pending-ingestion queue to the configured coding agent for the decide/absorb pass (Pro). For a single new source, use `skill-forge add <source> --ingest` instead."