@rhize/skill-forge 0.11.3 → 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";
@@ -1165,7 +1165,8 @@ function scanStaticRules(targetPath) {
1165
1165
  severity: refined.severity,
1166
1166
  rule: rule.rule,
1167
1167
  file,
1168
- detail: `line ${i + 1}: ${refined.message}`
1168
+ detail: `line ${i + 1}: ${refined.message}`,
1169
+ context: lines[i].trim()
1169
1170
  });
1170
1171
  }
1171
1172
  }
@@ -1266,19 +1267,27 @@ function scanJsonConfig(file, parsed) {
1266
1267
  const command = typeof node.command === "string" ? node.command : void 0;
1267
1268
  const args = Array.isArray(node.args) ? node.args.filter((a) => typeof a === "string") : [];
1268
1269
  if (command && isNpxCommand(command)) {
1269
- const flagIdx = args.findIndex((a) => a === "-y" || a === "--yes");
1270
- if (flagIdx !== -1) {
1271
- const { packageFlagSpecs, positionalSpec } = collectNpxPackageSpecs(args);
1272
- const allSpecs = positionalSpec ? [...packageFlagSpecs, positionalSpec] : packageFlagSpecs;
1273
- for (const pkgSpec of allSpecs) {
1274
- if (!isPinnedPackageSpec(pkgSpec)) {
1275
- findings.push({
1276
- severity: "MEDIUM",
1277
- rule: "mcp-unpinned-npx",
1278
- file,
1279
- detail: `npx -y ${pkgSpec} is unpinned \u2014 pin an exact version (${packageSpecBase(pkgSpec)}@<version>) so the resolved server code can't change silently`
1280
- });
1281
- }
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
+ });
1282
1291
  }
1283
1292
  }
1284
1293
  }
@@ -1288,7 +1297,8 @@ function scanJsonConfig(file, parsed) {
1288
1297
  severity: "HIGH",
1289
1298
  rule: "mcp-dangerous-flag",
1290
1299
  file,
1291
- detail: `launch arg ${a} disables a safety guard`
1300
+ detail: `launch arg ${a} disables a safety guard`,
1301
+ context: a
1292
1302
  });
1293
1303
  }
1294
1304
  if (a === "/" || a === "~") {
@@ -1296,7 +1306,8 @@ function scanJsonConfig(file, parsed) {
1296
1306
  severity: "MEDIUM",
1297
1307
  rule: "mcp-filesystem-root-arg",
1298
1308
  file,
1299
- 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
1300
1311
  });
1301
1312
  }
1302
1313
  }
@@ -1309,14 +1320,16 @@ function scanJsonConfig(file, parsed) {
1309
1320
  severity: "HIGH",
1310
1321
  rule: "mcp-inline-credential-value",
1311
1322
  file,
1312
- 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}`
1313
1325
  });
1314
1326
  } else if (kind === "file-path") {
1315
1327
  findings.push({
1316
1328
  severity: "LOW",
1317
1329
  rule: "mcp-credential-file-path",
1318
1330
  file,
1319
- 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}`
1320
1333
  });
1321
1334
  }
1322
1335
  }
@@ -1324,7 +1337,7 @@ function scanJsonConfig(file, parsed) {
1324
1337
  }
1325
1338
  return findings;
1326
1339
  }
1327
- 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;
1328
1341
  var DANGEROUS_FLAG_RE = /--dangerously-[\w-]+|--no-sandbox\b/g;
1329
1342
  var CREDENTIAL_ASSIGNMENT_RE = /["']?([A-Za-z][A-Za-z0-9_]*)["']?\s*[:=]\s*["']([^"']{6,})["']/g;
1330
1343
  var CREDENTIAL_ASSIGNMENT_UNQUOTED_RE = /^\s*(?:export\s+|-\s+)?([A-Za-z][A-Za-z0-9_]*)\s*[:=]\s*(\S+)/;
@@ -1343,14 +1356,16 @@ function pushCredentialFinding(findings, file, lineNo, varName, value) {
1343
1356
  severity: "HIGH",
1344
1357
  rule: "mcp-inline-credential-value",
1345
1358
  file,
1346
- detail: `line ${lineNo}: inline credential value assigned to ${varName}`
1359
+ detail: `line ${lineNo}: inline credential value assigned to ${varName}`,
1360
+ context: varName
1347
1361
  });
1348
1362
  } else if (kind === "file-path") {
1349
1363
  findings.push({
1350
1364
  severity: "LOW",
1351
1365
  rule: "mcp-credential-file-path",
1352
1366
  file,
1353
- 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
1354
1369
  });
1355
1370
  }
1356
1371
  }
@@ -1363,15 +1378,28 @@ function scanTextFile(file, content, allowCredentialCheck) {
1363
1378
  NPX_SHELL_RE.lastIndex = 0;
1364
1379
  let m;
1365
1380
  while ((m = NPX_SHELL_RE.exec(line)) !== null) {
1366
- const pkgSpec = m[1];
1367
- const versionSuffix = m[2] ?? "";
1368
- if (!isPinnedPackageSpec(pkgSpec + versionSuffix)) {
1369
- findings.push({
1370
- severity: "MEDIUM",
1371
- rule: "mcp-unpinned-npx",
1372
- file,
1373
- detail: `line ${lineNo}: npx -y ${pkgSpec} is unpinned \u2014 pin an exact version (${pkgSpec}@<version>)`
1374
- });
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
+ }
1375
1403
  }
1376
1404
  }
1377
1405
  DANGEROUS_FLAG_RE.lastIndex = 0;
@@ -1624,6 +1652,89 @@ function scanOverlapPaths(skillMdPaths, threshold = DEFAULT_SET_THRESHOLD) {
1624
1652
  return pairs;
1625
1653
  }
1626
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
+
1627
1738
  // src/license.ts
1628
1739
  import { verify as cryptoVerify } from "crypto";
1629
1740
  var LICENSE_PREFIX = "skf1";
@@ -1713,7 +1824,13 @@ function proFeatureStatus(feature, betaFree = BETA_FREE) {
1713
1824
  // src/audit.ts
1714
1825
  var OVERSIZED_SKILL_EST_TOKENS = 5e3;
1715
1826
  function toAuditFindings(source) {
1716
- 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
+ );
1717
1834
  }
1718
1835
  function resolveConfiguredRoot(root) {
1719
1836
  return resolvePath2(expandHome(root));
@@ -1737,7 +1854,7 @@ function listSubdirsFollowingSymlinks(dir) {
1737
1854
  }
1738
1855
  const out = [];
1739
1856
  for (const entry of entries) {
1740
- const full = join11(dir, entry.name);
1857
+ const full = join12(dir, entry.name);
1741
1858
  if (entry.isDirectory() || entry.isSymbolicLink() && isDirFollowingSymlink(full)) {
1742
1859
  out.push(full);
1743
1860
  }
@@ -1751,20 +1868,20 @@ function listSubdirsNoSymlinks(dir) {
1751
1868
  } catch {
1752
1869
  return [];
1753
1870
  }
1754
- 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));
1755
1872
  }
1756
1873
  function findSkillDirsUnderRoot(root) {
1757
1874
  const found = /* @__PURE__ */ new Set();
1758
1875
  const rootSubdirs = listSubdirsFollowingSymlinks(root);
1759
1876
  for (const dir of rootSubdirs) {
1760
- if (existsSync8(join11(dir, "SKILL.md"))) found.add(dir);
1877
+ if (existsSync9(join12(dir, "SKILL.md"))) found.add(dir);
1761
1878
  }
1762
- for (const dir of listSubdirsFollowingSymlinks(join11(root, "skills"))) {
1763
- 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);
1764
1881
  }
1765
1882
  for (const topDir of rootSubdirs) {
1766
- for (const dir of listSubdirsNoSymlinks(join11(topDir, "skills"))) {
1767
- 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);
1768
1885
  }
1769
1886
  }
1770
1887
  return Array.from(found).filter((p) => !p.includes("/.git/"));
@@ -1860,7 +1977,7 @@ function estimateTokens(text) {
1860
1977
  return Math.ceil(Buffer.byteLength(text, "utf-8") / 4);
1861
1978
  }
1862
1979
  function profileAuditedSkill(group, findings, unreadableSkillMdPaths) {
1863
- const skillMdPath = join11(group.canonicalPath, "SKILL.md");
1980
+ const skillMdPath = join12(group.canonicalPath, "SKILL.md");
1864
1981
  const base = {
1865
1982
  path: group.canonicalPath,
1866
1983
  skillMdPath,
@@ -1871,7 +1988,7 @@ function profileAuditedSkill(group, findings, unreadableSkillMdPaths) {
1871
1988
  let sizeBytes;
1872
1989
  try {
1873
1990
  sizeBytes = statSync6(skillMdPath).size;
1874
- text = readFileSync8(skillMdPath, "utf-8");
1991
+ text = readFileSync9(skillMdPath, "utf-8");
1875
1992
  } catch (err) {
1876
1993
  findings.push({
1877
1994
  severity: "MEDIUM",
@@ -1928,7 +2045,7 @@ function auditMcpTarget(targetPath, notices) {
1928
2045
  const findings = [];
1929
2046
  const isToml = extname3(targetPath).toLowerCase() === ".toml";
1930
2047
  const format = isToml ? "toml" : "json";
1931
- if (!existsSync8(targetPath)) {
2048
+ if (!existsSync9(targetPath)) {
1932
2049
  findings.push({
1933
2050
  severity: "LOW",
1934
2051
  rule: "missing-mcp-target",
@@ -1949,7 +2066,7 @@ function auditMcpTarget(targetPath, notices) {
1949
2066
  }
1950
2067
  if (isToml) {
1951
2068
  try {
1952
- readFileSync8(targetPath, "utf-8");
2069
+ readFileSync9(targetPath, "utf-8");
1953
2070
  } catch (err) {
1954
2071
  findings.push({
1955
2072
  severity: "MEDIUM",
@@ -1964,7 +2081,7 @@ function auditMcpTarget(targetPath, notices) {
1964
2081
  }
1965
2082
  let raw;
1966
2083
  try {
1967
- raw = readFileSync8(targetPath, "utf-8");
2084
+ raw = readFileSync9(targetPath, "utf-8");
1968
2085
  } catch (err) {
1969
2086
  findings.push({
1970
2087
  severity: "MEDIUM",
@@ -2040,7 +2157,7 @@ function defaultWhich(cmd) {
2040
2157
  const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
2041
2158
  for (const dir of dirs) {
2042
2159
  for (const ext of exts) {
2043
- const candidate = join11(dir, cmd + ext);
2160
+ const candidate = join12(dir, cmd + ext);
2044
2161
  try {
2045
2162
  if (statSync6(candidate).isFile()) return candidate;
2046
2163
  } catch {
@@ -2126,16 +2243,23 @@ async function auditConfiguredSet(config, options = {}) {
2126
2243
  severity: "LOW",
2127
2244
  rule: "oversized-skill",
2128
2245
  target: skill.skillMdPath,
2129
- 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: ""
2130
2252
  });
2131
2253
  }
2132
2254
  }
2133
- const safetyVerdictBySkillPath = /* @__PURE__ */ new Map();
2255
+ const perSkillFindings = /* @__PURE__ */ new Map();
2256
+ const skillScanFailed = /* @__PURE__ */ new Set();
2134
2257
  for (const skill of skills) {
2135
2258
  try {
2136
2259
  const result = await scanSafety(skill.path, config.strictness);
2137
- findings.push(...toAuditFindings(result.findings));
2138
- safetyVerdictBySkillPath.set(skill.path, result.verdict);
2260
+ const skillFindings = toAuditFindings(result.findings);
2261
+ findings.push(...skillFindings);
2262
+ perSkillFindings.set(skill.path, skillFindings);
2139
2263
  } catch (err) {
2140
2264
  findings.push({
2141
2265
  severity: "MEDIUM",
@@ -2143,7 +2267,7 @@ async function auditConfiguredSet(config, options = {}) {
2143
2267
  target: skill.path,
2144
2268
  detail: `safety scan failed: ${err.message}`
2145
2269
  });
2146
- safetyVerdictBySkillPath.set(skill.path, "block");
2270
+ skillScanFailed.add(skill.path);
2147
2271
  }
2148
2272
  }
2149
2273
  const mcpTargetPaths = config.mcpTargets ?? [];
@@ -2174,7 +2298,36 @@ async function auditConfiguredSet(config, options = {}) {
2174
2298
  } else {
2175
2299
  notices.push(overlapStatus.notice ?? "Pro feature (installed-set overlap analysis) locked.");
2176
2300
  }
2177
- 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 }));
2178
2331
  const foundation = config.foundationSkillPath ? {
2179
2332
  available: false,
2180
2333
  existingPath: config.foundationSkillPath,
@@ -2184,21 +2337,23 @@ async function auditConfiguredSet(config, options = {}) {
2184
2337
  message: "no business-foundation skill yet \u2014 `skill-forge audit --foundation` can scaffold one to ground future skills in your business profile"
2185
2338
  };
2186
2339
  const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
2187
- for (const f of findings) findingCounts[f.severity]++;
2340
+ for (const f of activeFindings) findingCounts[f.severity]++;
2188
2341
  const summary = {
2189
2342
  rootCount: skillRoots.length,
2190
2343
  skillCount: skills.length,
2191
2344
  mcpTargetCount: mcpTargets.length,
2192
2345
  findingCounts,
2193
2346
  overlapClusterCount: overlapClusters.length,
2194
- evolveEligibleCount: evolveEligible.length
2347
+ evolveEligibleCount: evolveEligible.length,
2348
+ acceptedCount: acceptedEntries.length
2195
2349
  };
2196
2350
  const tooling = checkTooling(options.which ?? defaultWhich, options.env ?? process.env);
2197
2351
  return {
2198
2352
  schemaVersion: 1,
2199
2353
  generatedAt: now().toISOString(),
2200
2354
  inventory: { skillRoots, skills, mcpTargets },
2201
- hygiene: findings,
2355
+ hygiene: activeFindings,
2356
+ accepted: acceptedEntries,
2202
2357
  opportunities: { overlapClusters, evolveEligible, foundation, overlapLocked: !overlapStatus.enabled },
2203
2358
  tooling,
2204
2359
  notices,
@@ -2260,16 +2415,33 @@ function renderMcpTargetsSection(targets) {
2260
2415
  }
2261
2416
  return lines;
2262
2417
  }
2418
+ function displayFingerprint(f) {
2419
+ return f.fingerprint ? f.fingerprint.slice(0, FINGERPRINT_DISPLAY_LENGTH) : "unknown";
2420
+ }
2263
2421
  function renderFindingsSection(findings) {
2264
2422
  return [
2265
2423
  "## Hygiene findings",
2266
2424
  "",
2267
2425
  ...bulletList(
2268
- 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
+ ),
2269
2429
  "(none \u2014 clean)"
2270
2430
  )
2271
2431
  ];
2272
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
+ }
2273
2445
  function renderOpportunitiesSection(report) {
2274
2446
  const { overlapClusters, evolveEligible, foundation } = report.opportunities;
2275
2447
  return [
@@ -2327,6 +2499,7 @@ function renderAuditMarkdown(report) {
2327
2499
  `- Skills: ${summary.skillCount}`,
2328
2500
  `- MCP targets: ${summary.mcpTargetCount}`,
2329
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)`,
2330
2503
  `- Overlap clusters: ${summary.overlapClusterCount}`,
2331
2504
  `- Evolve-eligible skills: ${summary.evolveEligibleCount}`,
2332
2505
  "",
@@ -2340,6 +2513,8 @@ function renderAuditMarkdown(report) {
2340
2513
  "",
2341
2514
  ...renderFindingsSection(report.hygiene),
2342
2515
  "",
2516
+ ...renderAcceptedFindingsSection(report.accepted),
2517
+ "",
2343
2518
  ...renderOpportunitiesSection(report),
2344
2519
  "",
2345
2520
  ...renderToolingSection(report.tooling),
@@ -2392,7 +2567,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
2392
2567
  if (!isDir(resolvedRoot)) {
2393
2568
  throw new UserError(`skills root does not exist: ${resolvedRoot}`);
2394
2569
  }
2395
- const destDir = join11(resolvedRoot, "business-foundation");
2570
+ const destDir = join12(resolvedRoot, "business-foundation");
2396
2571
  let destLstat;
2397
2572
  try {
2398
2573
  destLstat = lstatSync2(destDir);
@@ -2406,9 +2581,9 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
2406
2581
  throw new UserError(`refusing to scaffold a foundation skill at ${destDir}: already exists`);
2407
2582
  }
2408
2583
  const content = renderFoundationSkillMd(profile);
2409
- const tempDir = mkdtempSync(join11(resolvedRoot, ".business-foundation-"));
2584
+ const tempDir = mkdtempSync(join12(resolvedRoot, ".business-foundation-"));
2410
2585
  try {
2411
- writeFileSync5(join11(tempDir, "SKILL.md"), content, { flag: "wx", mode: 384 });
2586
+ writeFileSync5(join12(tempDir, "SKILL.md"), content, { flag: "wx", mode: 384 });
2412
2587
  const safety = await scanSafety(tempDir, config.strictness);
2413
2588
  if (safety.verdict === "block") {
2414
2589
  throw new UserError(
@@ -2442,7 +2617,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
2442
2617
 
2443
2618
  // src/auditState.ts
2444
2619
  function getAuditStatePath() {
2445
- return join12(getHomeDir(), "audit-state.json");
2620
+ return join13(getHomeDir(), "audit-state.json");
2446
2621
  }
2447
2622
  function emptyCounts() {
2448
2623
  return { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
@@ -2459,15 +2634,15 @@ function summarizeAudit(report, reportPath) {
2459
2634
  rootOfSkillPath.set(skill.path, skill.root);
2460
2635
  }
2461
2636
  const byRoot = {};
2462
- for (const finding of report.hygiene) {
2637
+ for (const finding2 of report.hygiene) {
2463
2638
  let root;
2464
- for (let dir = finding.target; ; dir = dirname5(dir)) {
2639
+ for (let dir = finding2.target; ; dir = dirname5(dir)) {
2465
2640
  root = rootOfSkillPath.get(dir);
2466
2641
  if (root || dirname5(dir) === dir) break;
2467
2642
  }
2468
2643
  if (!root) continue;
2469
2644
  byRoot[root] ??= emptyCounts();
2470
- bump(byRoot[root], finding.severity);
2645
+ bump(byRoot[root], finding2.severity);
2471
2646
  }
2472
2647
  return {
2473
2648
  schemaVersion: 1,
@@ -2489,7 +2664,8 @@ function summarizeAudit(report, reportPath) {
2489
2664
  suggestedVerb: c.suggestedVerb
2490
2665
  })),
2491
2666
  overlapLocked: report.opportunities.overlapLocked === true,
2492
- evolveEligibleCount: report.summary.evolveEligibleCount
2667
+ evolveEligibleCount: report.summary.evolveEligibleCount,
2668
+ acceptedCount: report.summary.acceptedCount
2493
2669
  };
2494
2670
  }
2495
2671
  function writeAuditState(state) {
@@ -2497,9 +2673,9 @@ function writeAuditState(state) {
2497
2673
  }
2498
2674
  function readAuditState() {
2499
2675
  const path = getAuditStatePath();
2500
- if (!existsSync9(path)) return void 0;
2676
+ if (!existsSync10(path)) return void 0;
2501
2677
  try {
2502
- const parsed = JSON.parse(readFileSync9(path, "utf-8"));
2678
+ const parsed = JSON.parse(readFileSync10(path, "utf-8"));
2503
2679
  if (parsed.schemaVersion !== 1 || typeof parsed.lastRunAt !== "string") return void 0;
2504
2680
  return {
2505
2681
  schemaVersion: 1,
@@ -2511,7 +2687,8 @@ function readAuditState() {
2511
2687
  byRoot: parsed.byRoot ?? {},
2512
2688
  overlapClusters: parsed.overlapClusters ?? [],
2513
2689
  overlapLocked: parsed.overlapLocked === true,
2514
- evolveEligibleCount: parsed.evolveEligibleCount ?? 0
2690
+ evolveEligibleCount: parsed.evolveEligibleCount ?? 0,
2691
+ acceptedCount: parsed.acceptedCount ?? 0
2515
2692
  };
2516
2693
  } catch {
2517
2694
  return void 0;
@@ -2560,32 +2737,32 @@ function formatAuditAdvisory(state, options = {}) {
2560
2737
  }
2561
2738
 
2562
2739
  // src/commands/gatePipeline.ts
2563
- import { join as join15 } from "path";
2740
+ import { join as join16 } from "path";
2564
2741
 
2565
2742
  // src/gate/profile.ts
2566
2743
  import { execFileSync } from "child_process";
2567
2744
  import {
2568
2745
  closeSync,
2569
- existsSync as existsSync11,
2746
+ existsSync as existsSync12,
2570
2747
  mkdtempSync as mkdtempSync2,
2571
2748
  openSync,
2572
- readFileSync as readFileSync11,
2749
+ readFileSync as readFileSync12,
2573
2750
  readdirSync as readdirSync6,
2574
2751
  readSync,
2575
2752
  rmSync as rmSync4,
2576
2753
  statSync as statSync7
2577
2754
  } from "fs";
2578
2755
  import { tmpdir } from "os";
2579
- 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";
2580
2757
 
2581
2758
  // src/gate/mcpCapabilities.ts
2582
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
2583
- 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";
2584
2761
  var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
2585
2762
  var MAX_ITEMS_PER_CATEGORY = 200;
2586
2763
  function readJsonSafe(file) {
2587
2764
  try {
2588
- return JSON.parse(readFileSync10(file, "utf-8"));
2765
+ return JSON.parse(readFileSync11(file, "utf-8"));
2589
2766
  } catch {
2590
2767
  return void 0;
2591
2768
  }
@@ -2686,16 +2863,16 @@ var MCP_SDK_IMPORT_RE = /@modelcontextprotocol\/sdk/;
2686
2863
  function extractMcpCapabilities(packageDir) {
2687
2864
  const buckets = { tools: /* @__PURE__ */ new Set(), resources: /* @__PURE__ */ new Set(), prompts: /* @__PURE__ */ new Set() };
2688
2865
  let hasPackageJsonMarker = false;
2689
- const pkgPath = join13(packageDir, "package.json");
2690
- if (existsSync10(pkgPath)) {
2866
+ const pkgPath = join14(packageDir, "package.json");
2867
+ if (existsSync11(pkgPath)) {
2691
2868
  const pkg2 = readJsonSafe(pkgPath);
2692
2869
  if (pkg2 && typeof pkg2 === "object" && !Array.isArray(pkg2)) {
2693
2870
  hasPackageJsonMarker = hasPackageJsonMcpMarkers(pkg2);
2694
2871
  }
2695
2872
  }
2696
2873
  for (const manifestName of [".mcp.json", "mcp.json"]) {
2697
- const manifestPath = join13(packageDir, manifestName);
2698
- if (!existsSync10(manifestPath)) continue;
2874
+ const manifestPath = join14(packageDir, manifestName);
2875
+ if (!existsSync11(manifestPath)) continue;
2699
2876
  const parsed = readJsonSafe(manifestPath);
2700
2877
  if (parsed !== void 0) {
2701
2878
  collectDeclaredFromManifest(parsed, buckets);
@@ -2772,7 +2949,7 @@ var PYTHON_STDLIB = /* @__PURE__ */ new Set([
2772
2949
  ]);
2773
2950
  function profileSkill(inputPath) {
2774
2951
  const p = resolve3(expandHome(inputPath));
2775
- if (!existsSync11(p)) {
2952
+ if (!existsSync12(p)) {
2776
2953
  throw new Error(`path does not exist: ${p}`);
2777
2954
  }
2778
2955
  const stat = statSync7(p);
@@ -2802,7 +2979,7 @@ function asProfile(candidatePath, artifact) {
2802
2979
  return asSkillProfile(profile, `at ${candidatePath}`);
2803
2980
  }
2804
2981
  function classifyInput(p) {
2805
- if (existsSync11(join14(p, ".claude-plugin", "plugin.json"))) return "plugin";
2982
+ if (existsSync12(join15(p, ".claude-plugin", "plugin.json"))) return "plugin";
2806
2983
  if (detectArtifactType(p) === "mcp") return "mcp";
2807
2984
  return "skill";
2808
2985
  }
@@ -2810,10 +2987,10 @@ var MCP_SDK_DEP2 = "@modelcontextprotocol/sdk";
2810
2987
  var MCP_KEYWORD2 = "mcp-server";
2811
2988
  var MCP_BIN_PREFIX2 = /^mcp-/i;
2812
2989
  function readPackageJsonSafe(root) {
2813
- const pkgPath = join14(root, "package.json");
2814
- if (!existsSync11(pkgPath)) return void 0;
2990
+ const pkgPath = join15(root, "package.json");
2991
+ if (!existsSync12(pkgPath)) return void 0;
2815
2992
  try {
2816
- return JSON.parse(readFileSync11(pkgPath, "utf-8"));
2993
+ return JSON.parse(readFileSync12(pkgPath, "utf-8"));
2817
2994
  } catch {
2818
2995
  return void 0;
2819
2996
  }
@@ -2834,7 +3011,7 @@ function hasMcpPackageMarkers(pkg2) {
2834
3011
  return false;
2835
3012
  }
2836
3013
  function detectArtifactType(root) {
2837
- 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";
2838
3015
  const pkg2 = readPackageJsonSafe(root);
2839
3016
  if (pkg2 && hasMcpPackageMarkers(pkg2)) return "mcp";
2840
3017
  return "skill";
@@ -2844,7 +3021,7 @@ function resolveSkillMd(inputPath) {
2844
3021
  let tmpDir;
2845
3022
  const initialStat = statSync7(currentPath);
2846
3023
  if (extname5(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
2847
- tmpDir = mkdtempSync2(join14(tmpdir(), "forge_"));
3024
+ tmpDir = mkdtempSync2(join15(tmpdir(), "forge_"));
2848
3025
  extractZip(currentPath, tmpDir);
2849
3026
  currentPath = tmpDir;
2850
3027
  }
@@ -2853,8 +3030,8 @@ function resolveSkillMd(inputPath) {
2853
3030
  return { skillMd: currentPath, root: dirname6(currentPath), tmpDir };
2854
3031
  }
2855
3032
  if (stat.isDirectory()) {
2856
- const direct = join14(currentPath, "SKILL.md");
2857
- if (existsSync11(direct)) {
3033
+ const direct = join15(currentPath, "SKILL.md");
3034
+ if (existsSync12(direct)) {
2858
3035
  return { skillMd: direct, root: currentPath, tmpDir };
2859
3036
  }
2860
3037
  const matches = findSkillMdOneLevelDown(currentPath);
@@ -2879,16 +3056,16 @@ function findSkillMdOneLevelDown(dir) {
2879
3056
  const matches = /* @__PURE__ */ new Set();
2880
3057
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
2881
3058
  if (entry.isDirectory()) {
2882
- const candidate = join14(dir, entry.name, "SKILL.md");
2883
- if (existsSync11(candidate)) matches.add(candidate);
3059
+ const candidate = join15(dir, entry.name, "SKILL.md");
3060
+ if (existsSync12(candidate)) matches.add(candidate);
2884
3061
  }
2885
3062
  }
2886
- const skillsDir = join14(dir, "skills");
3063
+ const skillsDir = join15(dir, "skills");
2887
3064
  if (isDir(skillsDir)) {
2888
3065
  for (const entry of readdirSync6(skillsDir, { withFileTypes: true })) {
2889
3066
  if (entry.isDirectory()) {
2890
- const candidate = join14(skillsDir, entry.name, "SKILL.md");
2891
- if (existsSync11(candidate)) matches.add(candidate);
3067
+ const candidate = join15(skillsDir, entry.name, "SKILL.md");
3068
+ if (existsSync12(candidate)) matches.add(candidate);
2892
3069
  }
2893
3070
  }
2894
3071
  }
@@ -2971,9 +3148,9 @@ function detectLicense(root, fm) {
2971
3148
  if (fm.license) return fm.license;
2972
3149
  const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
2973
3150
  for (const name of names) {
2974
- const p = join14(root, name);
2975
- if (existsSync11(p) && statSync7(p).isFile()) {
2976
- 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();
2977
3154
  for (const [hint, label] of Object.entries(SPDX_HINTS)) {
2978
3155
  if (head.includes(hint)) return label;
2979
3156
  }
@@ -2985,7 +3162,7 @@ function detectLicense(root, fm) {
2985
3162
  function walkFiles(dir) {
2986
3163
  const out = [];
2987
3164
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
2988
- const full = join14(dir, entry.name);
3165
+ const full = join15(dir, entry.name);
2989
3166
  if (entry.isDirectory()) {
2990
3167
  out.push(...walkFiles(full));
2991
3168
  } else if (entry.isFile()) {
@@ -2997,7 +3174,7 @@ function walkFiles(dir) {
2997
3174
  function countResources(root) {
2998
3175
  const resources = {};
2999
3176
  for (const d of RESOURCE_DIRS) {
3000
- const dp = join14(root, d);
3177
+ const dp = join15(root, d);
3001
3178
  if (isDir(dp)) {
3002
3179
  resources[d] = walkFiles(dp).length;
3003
3180
  }
@@ -3008,11 +3185,11 @@ function scanMcpAndImports(text, root) {
3008
3185
  const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
3009
3186
  const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
3010
3187
  const imports = /* @__PURE__ */ new Set();
3011
- const scriptsDir = join14(root, "scripts");
3188
+ const scriptsDir = join15(root, "scripts");
3012
3189
  if (isDir(scriptsDir)) {
3013
3190
  for (const f of walkFiles(scriptsDir)) {
3014
3191
  if (!f.endsWith(".py")) continue;
3015
- const ft = readFileSync11(f, "utf-8");
3192
+ const ft = readFileSync12(f, "utf-8");
3016
3193
  for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
3017
3194
  const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
3018
3195
  let m;
@@ -3029,7 +3206,7 @@ function scanMcpAndImports(text, root) {
3029
3206
  function profileSkillDir(inputPath) {
3030
3207
  const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
3031
3208
  try {
3032
- const text = readFileSync11(skillMd, "utf-8");
3209
+ const text = readFileSync12(skillMd, "utf-8");
3033
3210
  const fm = parseFrontmatter(text);
3034
3211
  const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
3035
3212
  const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
@@ -3057,19 +3234,19 @@ function profileSkillDir(inputPath) {
3057
3234
  }
3058
3235
  }
3059
3236
  function profilePlugin(p) {
3060
- const manifestPath = join14(p, ".claude-plugin", "plugin.json");
3237
+ const manifestPath = join15(p, ".claude-plugin", "plugin.json");
3061
3238
  let manifest;
3062
3239
  try {
3063
- manifest = JSON.parse(readFileSync11(manifestPath, "utf-8"));
3240
+ manifest = JSON.parse(readFileSync12(manifestPath, "utf-8"));
3064
3241
  } catch (err) {
3065
3242
  throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
3066
3243
  }
3067
- const skillsDir = join14(p, "skills");
3244
+ const skillsDir = join15(p, "skills");
3068
3245
  let skills = [];
3069
3246
  if (isDir(skillsDir)) {
3070
- 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();
3071
3248
  }
3072
- const commandsDir = join14(p, "commands");
3249
+ const commandsDir = join15(p, "commands");
3073
3250
  const commands = isDir(commandsDir) ? readdirSync6(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
3074
3251
  const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
3075
3252
  const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
@@ -3087,11 +3264,11 @@ function profilePlugin(p) {
3087
3264
  };
3088
3265
  }
3089
3266
  function profileMcp(p) {
3090
- 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;
3091
3268
  if (explicitConfig) {
3092
3269
  let cfg;
3093
3270
  try {
3094
- cfg = JSON.parse(readFileSync11(explicitConfig, "utf-8"));
3271
+ cfg = JSON.parse(readFileSync12(explicitConfig, "utf-8"));
3095
3272
  } catch (err) {
3096
3273
  throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
3097
3274
  }
@@ -3107,10 +3284,10 @@ function profileMcp(p) {
3107
3284
  capabilities: extractMcpCapabilities(p)
3108
3285
  };
3109
3286
  }
3110
- const pkgPath = join14(p, "package.json");
3287
+ const pkgPath = join15(p, "package.json");
3111
3288
  let pkg2;
3112
3289
  try {
3113
- pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
3290
+ pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3114
3291
  } catch (err) {
3115
3292
  throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
3116
3293
  }
@@ -3127,14 +3304,14 @@ function profileMcp(p) {
3127
3304
  }
3128
3305
 
3129
3306
  // src/gate/mcpOverlap.ts
3130
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
3307
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
3131
3308
  import { resolve as resolve4 } from "path";
3132
3309
  function readTargetServers(targetPath) {
3133
3310
  const path = resolve4(expandHome(targetPath));
3134
- if (!existsSync12(path)) return [];
3311
+ if (!existsSync13(path)) return [];
3135
3312
  let cfg;
3136
3313
  try {
3137
- cfg = JSON.parse(readFileSync12(path, "utf-8"));
3314
+ cfg = JSON.parse(readFileSync13(path, "utf-8"));
3138
3315
  } catch {
3139
3316
  return [];
3140
3317
  }
@@ -3206,6 +3383,121 @@ function scanMcpOverlap(candidate, mcpTargetPaths) {
3206
3383
  };
3207
3384
  }
3208
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
+
3209
3501
  // src/report/render.ts
3210
3502
  var SAFETY_VERDICT_MAP = {
3211
3503
  pass: "ALLOW",
@@ -3234,10 +3526,10 @@ function renderSafetyLine(safety) {
3234
3526
  const withDetail = detail ? `${safety.verdict} (${detail})` : safety.verdict;
3235
3527
  return safety.recommendation ? `${withDetail} \u2014 ${safety.recommendation}` : withDetail;
3236
3528
  }
3237
- function renderFindingLine(finding) {
3238
- const severity = finding.severity ?? "UNKNOWN";
3239
- const rule = finding.rule ?? "unknown-rule";
3240
- 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}` : ""}`;
3241
3533
  }
3242
3534
  function renderFindingsLines(findings) {
3243
3535
  if (!findings?.length) return [];
@@ -3331,6 +3623,16 @@ function tryMcpOverlap(profile, mcpTargets) {
3331
3623
  return void 0;
3332
3624
  }
3333
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
+ }
3334
3636
  function resolveMcpCandidatePath(entry) {
3335
3637
  if (entry.packageDir) return entry.packageDir;
3336
3638
  let children = [];
@@ -3338,18 +3640,24 @@ function resolveMcpCandidatePath(entry) {
3338
3640
  children = readdirSync7(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
3339
3641
  } catch {
3340
3642
  }
3341
- return children.length === 1 ? join15(entry.quarantinePath, children[0]) : entry.quarantinePath;
3643
+ return children.length === 1 ? join16(entry.quarantinePath, children[0]) : entry.quarantinePath;
3342
3644
  }
3343
3645
  async function runGate(candidatePath, skillsRoot, {
3344
3646
  proFeatureLabel = "overlap analysis",
3345
3647
  strictness = DEFAULT_STRICTNESS,
3346
3648
  artifact = "skill",
3347
- mcpTargets = []
3649
+ mcpTargets = [],
3650
+ skillMapPath
3348
3651
  } = {}) {
3349
3652
  const profile = asProfile(candidatePath, artifact);
3350
3653
  const proStatus = proFeatureStatus(proFeatureLabel);
3351
3654
  const safety = artifact === "mcp" ? await mcpScan(candidatePath, strictness) : await scanSafety(candidatePath, strictness);
3352
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;
3353
3661
  const advisory = formatAuditAdvisory(readAuditState(), {
3354
3662
  targetRoot: skillsRoot || void 0,
3355
3663
  candidateName: "name" in profile ? profile.name : void 0
@@ -3360,27 +3668,27 @@ async function runGate(candidatePath, skillsRoot, {
3360
3668
  overlapLocked: !proStatus.enabled,
3361
3669
  notices: proStatus.notice ? [proStatus.notice] : void 0,
3362
3670
  safety: {
3363
- verdict: SAFETY_VERDICT_MAP[safety.verdict],
3364
- findings: mapSafetyFindings(safety.findings)
3671
+ verdict: SAFETY_VERDICT_MAP[combinedSafety.verdict],
3672
+ findings: mapSafetyFindings(combinedSafety.findings)
3365
3673
  },
3366
3674
  advisory
3367
3675
  };
3368
- return { safety, gateResult, proEnabled: proStatus.enabled };
3676
+ return { safety: combinedSafety, gateResult, proEnabled: proStatus.enabled };
3369
3677
  }
3370
3678
 
3371
3679
  // src/queue.ts
3372
3680
  import { randomUUID as randomUUID3 } from "crypto";
3373
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync13, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
3374
- 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";
3375
3683
  function getQueuePath() {
3376
- return join16(getHomeDir(), "queue.json");
3684
+ return join17(getHomeDir(), "queue.json");
3377
3685
  }
3378
3686
  function readQueue() {
3379
3687
  const path = getQueuePath();
3380
- if (!existsSync13(path)) {
3688
+ if (!existsSync14(path)) {
3381
3689
  return { version: 1, entries: [] };
3382
3690
  }
3383
- const raw = readFileSync13(path, "utf-8");
3691
+ const raw = readFileSync15(path, "utf-8");
3384
3692
  const parsed = JSON.parse(raw);
3385
3693
  return { version: 1, entries: parsed.entries ?? [] };
3386
3694
  }
@@ -3388,7 +3696,7 @@ function writeQueue(queue2) {
3388
3696
  const home = getHomeDir();
3389
3697
  mkdirSync6(home, { recursive: true });
3390
3698
  const finalPath = getQueuePath();
3391
- const tempPath = join16(home, `.queue.json.tmp-${randomUUID3()}`);
3699
+ const tempPath = join17(home, `.queue.json.tmp-${randomUUID3()}`);
3392
3700
  try {
3393
3701
  writeFileSync6(tempPath, JSON.stringify(queue2, null, 2) + "\n", "utf-8");
3394
3702
  renameSync4(tempPath, finalPath);
@@ -3437,16 +3745,16 @@ function generateId(existingIds) {
3437
3745
 
3438
3746
  // src/handoff.ts
3439
3747
  import { spawnSync } from "child_process";
3440
- import { existsSync as existsSync14 } from "fs";
3441
- 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";
3442
3750
  import { fileURLToPath } from "url";
3443
3751
  var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
3444
3752
  var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
3445
3753
  function resolvePromptPath(assetName = "ingest-prompt.md") {
3446
3754
  const here = dirname7(fileURLToPath(import.meta.url));
3447
- const builtCandidate = join17(here, assetName);
3448
- if (existsSync14(builtCandidate)) return builtCandidate;
3449
- return join17(here, "..", "assets", assetName);
3755
+ const builtCandidate = join18(here, assetName);
3756
+ if (existsSync15(builtCandidate)) return builtCandidate;
3757
+ return join18(here, "..", "assets", assetName);
3450
3758
  }
3451
3759
  function defaultIsOnPath(bin) {
3452
3760
  const finder = process.platform === "win32" ? "where" : "which";
@@ -3502,14 +3810,14 @@ function runHandoff(config, installedPath, options = {}) {
3502
3810
  }
3503
3811
 
3504
3812
  // src/commands/init.ts
3505
- import { existsSync as existsSync16 } from "fs";
3813
+ import { existsSync as existsSync17 } from "fs";
3506
3814
  import { homedir as homedir4 } from "os";
3507
3815
  import { createInterface as createInterface3 } from "readline/promises";
3508
3816
 
3509
3817
  // src/agents.ts
3510
3818
  import { statSync as statSync8 } from "fs";
3511
3819
  import { homedir as homedir3 } from "os";
3512
- import { join as join18, resolve as resolve5 } from "path";
3820
+ import { join as join19, resolve as resolve6 } from "path";
3513
3821
  function isFile(p) {
3514
3822
  try {
3515
3823
  return statSync8(p).isFile();
@@ -3518,12 +3826,12 @@ function isFile(p) {
3518
3826
  }
3519
3827
  }
3520
3828
  function projectPath(root, cwd) {
3521
- return root ? resolve5(cwd, root) : void 0;
3829
+ return root ? resolve6(cwd, root) : void 0;
3522
3830
  }
3523
3831
  function globalPath(root, home) {
3524
3832
  if (!root) return void 0;
3525
3833
  if (root === "~") return home;
3526
- if (root.startsWith("~/")) return join18(home, root.slice(2));
3834
+ if (root.startsWith("~/")) return join19(home, root.slice(2));
3527
3835
  return root;
3528
3836
  }
3529
3837
  function makeAgent(spec) {
@@ -3753,8 +4061,8 @@ var AGENTS = [
3753
4061
  ].map(makeAgent);
3754
4062
 
3755
4063
  // src/commands/audit.ts
3756
- import { existsSync as existsSync15 } from "fs";
3757
- 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";
3758
4066
  import { createInterface as createInterface2 } from "readline/promises";
3759
4067
 
3760
4068
  // src/decision.ts
@@ -3822,7 +4130,7 @@ async function captureBusinessProfile(rl, existing) {
3822
4130
  function resolveReportPath(reportOption) {
3823
4131
  if (reportOption) return resolvePath3(reportOption);
3824
4132
  const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3825
- return join19(getHomeDir(), "reports", `audit-${iso}.md`);
4133
+ return join20(getHomeDir(), "reports", `audit-${iso}.md`);
3826
4134
  }
3827
4135
  function printTerminalSummary(report, reportPath) {
3828
4136
  const { summary } = report;
@@ -3831,6 +4139,7 @@ function printTerminalSummary(report, reportPath) {
3831
4139
  console.log(
3832
4140
  ` findings: ${summary.findingCounts.CRITICAL} CRITICAL, ${summary.findingCounts.HIGH} HIGH, ${summary.findingCounts.MEDIUM} MEDIUM, ${summary.findingCounts.LOW} LOW`
3833
4141
  );
4142
+ console.log(` accepted: ${summary.acceptedCount} (suppressed from counts; see report)`);
3834
4143
  console.log(` overlap clusters: ${summary.overlapClusterCount} evolve-eligible: ${summary.evolveEligibleCount}`);
3835
4144
  console.log(` report -> ${reportPath}`);
3836
4145
  for (const notice of report.notices) console.log(` [notice] ${notice}`);
@@ -3871,7 +4180,7 @@ async function maybeHandoff(config, reportPath, options, rl) {
3871
4180
  }
3872
4181
  async function auditCommand(options = {}) {
3873
4182
  await maybeOfferInit({ json: options.json, yes: options.yes, suppressAuditOffer: true });
3874
- if (!existsSync15(getConfigPath())) {
4183
+ if (!existsSync16(getConfigPath())) {
3875
4184
  throw new UserError(
3876
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)."
3877
4186
  );
@@ -3887,7 +4196,9 @@ async function auditCommand(options = {}) {
3887
4196
  saveConfig(config);
3888
4197
  }
3889
4198
  }
3890
- 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];
3891
4202
  const reportPath = resolveReportPath(options.report);
3892
4203
  writeNewFile(reportPath, renderAuditMarkdown(report), "--report");
3893
4204
  try {
@@ -4214,7 +4525,7 @@ async function init(options = {}, deps = {}) {
4214
4525
  }
4215
4526
  async function maybeOfferInit(options = {}) {
4216
4527
  if (options.json || options.yes || !process.stdin.isTTY) return;
4217
- if (existsSync16(getConfigPath())) return;
4528
+ if (existsSync17(getConfigPath())) return;
4218
4529
  console.log(
4219
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)."
4220
4531
  );
@@ -4263,7 +4574,8 @@ async function add(source, options = {}) {
4263
4574
  const { safety, gateResult, proEnabled } = await runGate(entry.skillDirs[0], targetRoot, {
4264
4575
  proFeatureLabel: PRO_FEATURE_LABEL,
4265
4576
  strictness: config.strictness,
4266
- artifact
4577
+ artifact,
4578
+ skillMapPath: options.skillMap
4267
4579
  });
4268
4580
  const { profile, overlap } = gateResult;
4269
4581
  if (profile.kind !== "skill") {
@@ -4364,7 +4676,7 @@ async function addMcp(source, config, options) {
4364
4676
  envVars: detail.env ? Object.keys(detail.env) : void 0,
4365
4677
  candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
4366
4678
  };
4367
- 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");
4368
4680
  const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
4369
4681
  reject(entry);
4370
4682
  logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
@@ -4442,7 +4754,8 @@ async function scan(source, options = {}) {
4442
4754
  const { safety, gateResult } = await runGate(candidatePath, skillsRoot ?? "", {
4443
4755
  strictness: config.strictness,
4444
4756
  artifact,
4445
- mcpTargets: config.mcpTargets ?? []
4757
+ mcpTargets: config.mcpTargets ?? [],
4758
+ skillMapPath: options.skillMap
4446
4759
  });
4447
4760
  if (artifact === "mcp" && gateResult.profile.kind === "mcp" && gateResult.profile.serverCount > 1) {
4448
4761
  console.error(
@@ -4459,7 +4772,7 @@ async function scan(source, options = {}) {
4459
4772
  }
4460
4773
 
4461
4774
  // src/commands/list.ts
4462
- import { join as join21 } from "path";
4775
+ import { join as join22 } from "path";
4463
4776
  async function list() {
4464
4777
  await maybeOfferInit();
4465
4778
  const config = loadConfig();
@@ -4472,14 +4785,14 @@ async function list() {
4472
4785
  }
4473
4786
  console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
4474
4787
  for (const id of ids) {
4475
- const skillDirs = findSkillDirs(join21(quarantineDir, id));
4788
+ const skillDirs = findSkillDirs(join22(quarantineDir, id));
4476
4789
  if (skillDirs.length === 0) {
4477
4790
  console.log(` ${id} (no SKILL.md found)`);
4478
4791
  continue;
4479
4792
  }
4480
4793
  const names = skillDirs.map((dir) => {
4481
4794
  try {
4482
- return readFrontmatter(join21(dir, "SKILL.md")).name;
4795
+ return readFrontmatter(join22(dir, "SKILL.md")).name;
4483
4796
  } catch {
4484
4797
  return dir;
4485
4798
  }
@@ -4489,18 +4802,18 @@ async function list() {
4489
4802
  }
4490
4803
 
4491
4804
  // src/commands/guide.ts
4492
- import { existsSync as existsSync18 } from "fs";
4805
+ import { existsSync as existsSync19 } from "fs";
4493
4806
 
4494
4807
  // src/commands/held.ts
4495
- import { existsSync as existsSync17, realpathSync as realpathSync3 } from "fs";
4496
- 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";
4497
4810
  var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
4498
4811
  function resolveHeldPath(quarantineDir, id) {
4499
4812
  if (id === "" || id !== basename9(id) || id === "." || id === "..") {
4500
4813
  throw new UserError(`invalid quarantine id "${id}" \u2014 pass a single id exactly as printed by \`skill-forge list\``);
4501
4814
  }
4502
- const candidate = join22(quarantineDir, id);
4503
- if (!existsSync17(candidate)) {
4815
+ const candidate = join23(quarantineDir, id);
4816
+ if (!existsSync18(candidate)) {
4504
4817
  throw new UserError(`no held entry with id "${id}" \u2014 run \`skill-forge list\` to see what is in quarantine`);
4505
4818
  }
4506
4819
  const realRoot = realpathSync3(resolvePath4(quarantineDir));
@@ -4528,7 +4841,7 @@ function resolveTargetRoot(config, target) {
4528
4841
  function heldSkillNames(skillDirs) {
4529
4842
  return skillDirs.map((dir) => {
4530
4843
  try {
4531
- return readFrontmatter(join22(dir, "SKILL.md")).name;
4844
+ return readFrontmatter(join23(dir, "SKILL.md")).name;
4532
4845
  } catch {
4533
4846
  return basename9(dir);
4534
4847
  }
@@ -4536,7 +4849,7 @@ function heldSkillNames(skillDirs) {
4536
4849
  }
4537
4850
  function looksLikeMcpSandbox(quarantinePath) {
4538
4851
  const candidate = resolveMcpCandidatePath({ quarantinePath });
4539
- 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)));
4540
4853
  }
4541
4854
  async function promoteHeldMcp(id, entry, config, options) {
4542
4855
  const candidatePath = resolveMcpCandidatePath(entry);
@@ -4570,7 +4883,7 @@ async function promoteHeldMcp(id, entry, config, options) {
4570
4883
  envVars: detail.env ? Object.keys(detail.env) : void 0,
4571
4884
  candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
4572
4885
  };
4573
- 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");
4574
4887
  const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
4575
4888
  reject(entry);
4576
4889
  logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
@@ -4815,7 +5128,7 @@ var TOPICS = [
4815
5128
  ];
4816
5129
  function readState() {
4817
5130
  const configPath = getConfigPath();
4818
- const configured = existsSync18(configPath);
5131
+ const configured = existsSync19(configPath);
4819
5132
  const base = {
4820
5133
  configured,
4821
5134
  configPath,
@@ -5044,18 +5357,18 @@ async function status() {
5044
5357
 
5045
5358
  // src/evolve.ts
5046
5359
  import { createInterface as createInterface5 } from "readline/promises";
5047
- import { createHash as createHash2 } from "crypto";
5048
- 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";
5049
5362
  import { tmpdir as tmpdir2 } from "os";
5050
- 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";
5051
5364
  var SKILLOPT_BIN = "skillopt-sleep";
5052
5365
  var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
5053
5366
  async function evolve(targetSkillDir, options = {}, deps = {}) {
5054
5367
  const exec = deps.exec ?? defaultExec;
5055
5368
  const json = options.json;
5056
5369
  const resolvedTargetDir = resolvePath5(targetSkillDir);
5057
- const skillMdPath = join23(resolvedTargetDir, "SKILL.md");
5058
- if (!existsSync19(skillMdPath)) {
5370
+ const skillMdPath = join24(resolvedTargetDir, "SKILL.md");
5371
+ if (!existsSync20(skillMdPath)) {
5059
5372
  throw new UserError(`no SKILL.md found at ${skillMdPath} \u2014 evolve requires an existing, installed skill directory`);
5060
5373
  }
5061
5374
  const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
@@ -5113,33 +5426,33 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
5113
5426
  }
5114
5427
  return;
5115
5428
  }
5116
- const stagingRoot = join23(project, ".skillopt-sleep", "staging");
5429
+ const stagingRoot = join24(project, ".skillopt-sleep", "staging");
5117
5430
  const stagingDir = resolvePath5(parsed.staging_dir);
5118
5431
  if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep3)) {
5119
5432
  throw new UserError(
5120
5433
  `skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
5121
5434
  );
5122
5435
  }
5123
- const proposedSkillMd = join23(stagingDir, "proposed_SKILL.md");
5124
- const proposedClaudeMd = join23(stagingDir, "proposed_CLAUDE.md");
5125
- const reportMdPath = join23(stagingDir, "report.md");
5126
- 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)) {
5127
5440
  throw new UserError(`skillopt-sleep reported an accepted proposal but ${proposedSkillMd} is missing`);
5128
5441
  }
5129
- if (existsSync19(reportMdPath)) {
5442
+ if (existsSync20(reportMdPath)) {
5130
5443
  notices.push(`SkillOpt-Sleep's full report: ${reportMdPath}`);
5131
5444
  }
5132
5445
  assertNotSymlink(proposedSkillMd, "proposed_SKILL.md");
5133
5446
  assertNotSymlink(proposedClaudeMd, "proposed_CLAUDE.md");
5134
5447
  const scannedSkillHash = sha256File(proposedSkillMd);
5135
5448
  const scannedClaudeHash = sha256File(proposedClaudeMd);
5136
- const tempDir = mkdtempSync3(join23(tmpdir2(), "skill-forge-evolve-"));
5449
+ const tempDir = mkdtempSync3(join24(tmpdir2(), "skill-forge-evolve-"));
5137
5450
  let profile;
5138
5451
  let safety;
5139
5452
  try {
5140
- cpSync3(proposedSkillMd, join23(tempDir, "SKILL.md"));
5141
- if (existsSync19(proposedClaudeMd)) {
5142
- cpSync3(proposedClaudeMd, join23(tempDir, "CLAUDE.md"));
5453
+ cpSync3(proposedSkillMd, join24(tempDir, "SKILL.md"));
5454
+ if (existsSync20(proposedClaudeMd)) {
5455
+ cpSync3(proposedClaudeMd, join24(tempDir, "CLAUDE.md"));
5143
5456
  }
5144
5457
  profile = asSkillProfile(profileSkill(tempDir), "for the evolved proposal");
5145
5458
  const strictness = loadConfig().strictness;
@@ -5238,7 +5551,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
5238
5551
  if (safetyVerdict === "block") process.exitCode = 1;
5239
5552
  } else {
5240
5553
  logOutcome(`[skill-forge] held \u2014 staging dir left in place -> ${stagingDir}`, json);
5241
- if (existsSync19(reportMdPath)) {
5554
+ if (existsSync20(reportMdPath)) {
5242
5555
  logOutcome(`[skill-forge] see ${reportMdPath} for the full evidence report`, json);
5243
5556
  }
5244
5557
  }
@@ -5280,10 +5593,10 @@ function validateRunOutput(raw) {
5280
5593
  return { accepted: Boolean(obj.accepted), staging_dir: stagingDirRaw ?? null };
5281
5594
  }
5282
5595
  function readManifest(stagingDir) {
5283
- const manifestPath = join23(stagingDir, "manifest.json");
5596
+ const manifestPath = join24(stagingDir, "manifest.json");
5284
5597
  let raw;
5285
5598
  try {
5286
- raw = readFileSync14(manifestPath, "utf-8");
5599
+ raw = readFileSync16(manifestPath, "utf-8");
5287
5600
  } catch (err) {
5288
5601
  throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
5289
5602
  }
@@ -5317,18 +5630,18 @@ function validateManifest(raw, manifestPath) {
5317
5630
  };
5318
5631
  }
5319
5632
  function filesIdentical(a, b) {
5320
- if (!existsSync19(a) || !existsSync19(b)) return false;
5321
- return readFileSync14(a).equals(readFileSync14(b));
5633
+ if (!existsSync20(a) || !existsSync20(b)) return false;
5634
+ return readFileSync16(a).equals(readFileSync16(b));
5322
5635
  }
5323
5636
  function isAlreadyAdopted(stagingDir, manifest) {
5324
- const proposedSkillMd = join23(stagingDir, "proposed_SKILL.md");
5325
- const proposedClaudeMd = join23(stagingDir, "proposed_CLAUDE.md");
5637
+ const proposedSkillMd = join24(stagingDir, "proposed_SKILL.md");
5638
+ const proposedClaudeMd = join24(stagingDir, "proposed_CLAUDE.md");
5326
5639
  let sawComparison = false;
5327
- if (existsSync19(proposedSkillMd)) {
5640
+ if (existsSync20(proposedSkillMd)) {
5328
5641
  sawComparison = true;
5329
5642
  if (!filesIdentical(proposedSkillMd, manifest.live_skill_path)) return false;
5330
5643
  }
5331
- if (existsSync19(proposedClaudeMd) && manifest.live_memory_path) {
5644
+ if (existsSync20(proposedClaudeMd) && manifest.live_memory_path) {
5332
5645
  sawComparison = true;
5333
5646
  if (!filesIdentical(proposedClaudeMd, manifest.live_memory_path)) return false;
5334
5647
  }
@@ -5346,8 +5659,8 @@ function assertNotSymlink(path, label) {
5346
5659
  }
5347
5660
  }
5348
5661
  function sha256File(path) {
5349
- if (!existsSync19(path)) return null;
5350
- return createHash2("sha256").update(readFileSync14(path)).digest("hex");
5662
+ if (!existsSync20(path)) return null;
5663
+ return createHash3("sha256").update(readFileSync16(path)).digest("hex");
5351
5664
  }
5352
5665
 
5353
5666
  // src/commands/evolve.ts
@@ -5355,12 +5668,118 @@ async function evolveCommand(targetSkillDir, options = {}) {
5355
5668
  await evolve(targetSkillDir, options);
5356
5669
  }
5357
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
+
5358
5777
  // src/commands/organize.ts
5359
5778
  import { resolve as resolvePath7 } from "path";
5360
5779
 
5361
5780
  // src/organize.ts
5362
- import { existsSync as existsSync20, readFileSync as readFileSync15, readdirSync as readdirSync8, realpathSync as realpathSync4, statSync as statSync9 } from "fs";
5363
- 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";
5364
5783
  var PRO_FEATURE_LABEL4 = "set-level organizer reports";
5365
5784
  var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
5366
5785
  var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated"]);
@@ -5373,7 +5792,7 @@ function listSubdirs2(dir) {
5373
5792
  }
5374
5793
  const out = [];
5375
5794
  for (const entry of entries) {
5376
- const full = join24(dir, entry.name);
5795
+ const full = join25(dir, entry.name);
5377
5796
  if (entry.isDirectory()) {
5378
5797
  out.push(full);
5379
5798
  continue;
@@ -5391,14 +5810,14 @@ function findSkillDirsUnderRoot2(root) {
5391
5810
  const found = /* @__PURE__ */ new Set();
5392
5811
  const topDirs = listSubdirs2(root);
5393
5812
  for (const dir of topDirs) {
5394
- if (existsSync20(join24(dir, "SKILL.md"))) found.add(dir);
5813
+ if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
5395
5814
  }
5396
- for (const dir of listSubdirs2(join24(root, "skills"))) {
5397
- 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);
5398
5817
  }
5399
5818
  for (const topDir of topDirs) {
5400
- for (const dir of listSubdirs2(join24(topDir, "skills"))) {
5401
- 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);
5402
5821
  }
5403
5822
  }
5404
5823
  return Array.from(found).filter((p) => !p.includes("/.git/"));
@@ -5467,7 +5886,7 @@ function loadUsageSnapshot(snapshotPath) {
5467
5886
  const resolvedPath = resolvePath6(expandHome(snapshotPath));
5468
5887
  let raw;
5469
5888
  try {
5470
- raw = readFileSync15(resolvedPath, "utf-8");
5889
+ raw = readFileSync17(resolvedPath, "utf-8");
5471
5890
  } catch (err) {
5472
5891
  return { usage: /* @__PURE__ */ new Map(), warning: `could not read usage snapshot (${err.message}); proceeding without usage` };
5473
5892
  }
@@ -5573,10 +5992,10 @@ function buildRegistryEntries(candidates, warnings) {
5573
5992
  }
5574
5993
  if (seenRealPaths.has(realPath)) continue;
5575
5994
  seenRealPaths.add(realPath);
5576
- const skillMdPath = join24(realPath, "SKILL.md");
5995
+ const skillMdPath = join25(realPath, "SKILL.md");
5577
5996
  let text;
5578
5997
  try {
5579
- text = readFileSync15(skillMdPath, "utf-8");
5998
+ text = readFileSync17(skillMdPath, "utf-8");
5580
5999
  } catch (err) {
5581
6000
  warnings.push(`could not read ${skillMdPath}: ${err.message}`);
5582
6001
  continue;
@@ -5982,7 +6401,93 @@ async function find(query, options = {}, deps = {}) {
5982
6401
 
5983
6402
  // src/commands/watch.ts
5984
6403
  import { realpathSync as realpathSync5 } from "fs";
5985
- 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
5986
6491
  var PRO_FEATURE_LABEL5 = "provenance drift watch (watch)";
5987
6492
  function isGitUrl(source) {
5988
6493
  const s = source.trim();
@@ -6018,7 +6523,7 @@ async function checkGitDrift(exec, url, ref) {
6018
6523
  async function runWatch(config, options = {}, deps = {}) {
6019
6524
  const proStatus = proFeatureStatus(PRO_FEATURE_LABEL5);
6020
6525
  if (!proStatus.enabled) {
6021
- return { roots: [], rows: [], warnings: [], notices: [proStatus.notice ?? "Pro feature (watch) locked."] };
6526
+ return { roots: [], rows: [], warnings: [], notices: [proStatus.notice ?? "Pro feature (watch) locked."], mapDrift: [] };
6022
6527
  }
6023
6528
  const notices = proStatus.notice ? [proStatus.notice] : [];
6024
6529
  const exec = deps.exec ?? defaultExec;
@@ -6044,7 +6549,7 @@ async function runWatch(config, options = {}, deps = {}) {
6044
6549
  ledger = readProvenanceLedger(resolvedRoot);
6045
6550
  } catch (err) {
6046
6551
  const message = err instanceof Error ? err.message : String(err);
6047
- const ledgerPath = join25(resolvedRoot, "SOURCES.md");
6552
+ const ledgerPath = join26(resolvedRoot, "SOURCES.md");
6048
6553
  roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
6049
6554
  warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
6050
6555
  continue;
@@ -6084,7 +6589,19 @@ async function runWatch(config, options = {}, deps = {}) {
6084
6589
  rows.push({ ...rowBase, checkMethod: "git-ls-remote", driftStatus: status2, detail });
6085
6590
  }
6086
6591
  }
6087
- 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 };
6088
6605
  }
6089
6606
  function printWatchTerminal(report) {
6090
6607
  console.log("[skill-forge] watch \u2014 provenance drift check");
@@ -6122,6 +6639,13 @@ ${sanitizeForMarkdown(root.root)} (${sanitizeForMarkdown(root.realPath)})`);
6122
6639
  console.log("\nWarnings:");
6123
6640
  for (const w of report.warnings) console.log(` - ${sanitizeForMarkdown(w)}`);
6124
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
+ }
6125
6649
  console.log("\nCompare each stored ref to current upstream; re-run ingestion on any that moved.");
6126
6650
  }
6127
6651
  async function watchCommand(options = {}) {
@@ -6135,8 +6659,8 @@ async function watchCommand(options = {}) {
6135
6659
  }
6136
6660
 
6137
6661
  // src/commands/routine.ts
6138
- import { existsSync as existsSync21, rmSync as rmSync7, statSync as statSync10 } from "fs";
6139
- 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";
6140
6664
  var PRO_FEATURE_LABEL6 = "the scheduled maintenance routine";
6141
6665
  var SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
6142
6666
  function parseFailOnFloor(failOn) {
@@ -6157,7 +6681,7 @@ function exceedsFloor(audit2, failOn) {
6157
6681
  }
6158
6682
  function resolveReportPath2(override, generatedAt) {
6159
6683
  if (override) return override;
6160
- return join26(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
6684
+ return join27(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
6161
6685
  }
6162
6686
  function runHousekeeping(config, options) {
6163
6687
  const actions = [];
@@ -6166,7 +6690,7 @@ function runHousekeeping(config, options) {
6166
6690
  const pruneDays = options.pruneDays ?? 30;
6167
6691
  const cutoff = Date.now() - pruneDays * 864e5;
6168
6692
  for (const id of listQuarantineIds(config.quarantineDir)) {
6169
- const path = join26(config.quarantineDir, id);
6693
+ const path = join27(config.quarantineDir, id);
6170
6694
  let mtime;
6171
6695
  try {
6172
6696
  mtime = statSync10(path).mtimeMs;
@@ -6204,7 +6728,7 @@ function runHousekeeping(config, options) {
6204
6728
  }
6205
6729
  for (const entry of entries) {
6206
6730
  if (entry.status !== "pending") continue;
6207
- if (!entry.installedPath || existsSync21(entry.installedPath)) continue;
6731
+ if (!entry.installedPath || existsSync24(entry.installedPath)) continue;
6208
6732
  if (apply) {
6209
6733
  try {
6210
6734
  updateQueueEntry(entry.id, { status: "dismissed" });
@@ -6237,6 +6761,7 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
6237
6761
  "",
6238
6762
  `- Skills: ${result.audit.skillCount} across ${audit2.summary.rootCount} configured root(s)`,
6239
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)`,
6240
6765
  `- Overlap clusters: ${result.audit.overlapClusters.length}${result.audit.overlapLocked ? ' (overlap analysis was locked \u2014 not "none found")' : ""}`,
6241
6766
  `- Pending queue entries: ${result.pending.queue}`,
6242
6767
  `- Held in quarantine: ${result.pending.quarantine}`
@@ -6247,6 +6772,10 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
6247
6772
  if (organize) {
6248
6773
  lines.push(`- Capability registry: ${result.organize?.skills ?? 0} skill(s) across ${result.organize?.roots ?? 0} root(s)`);
6249
6774
  }
6775
+ if (result.notices.length > 0) {
6776
+ lines.push("", "## Notices", "");
6777
+ for (const item of result.notices) lines.push(`- ${sanitizeForMarkdown(item)}`);
6778
+ }
6250
6779
  if (result.degraded.length > 0) {
6251
6780
  lines.push("", "## Incomplete steps", "");
6252
6781
  for (const item of result.degraded) lines.push(`- ${sanitizeForMarkdown(item)}`);
@@ -6262,9 +6791,18 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
6262
6791
  }
6263
6792
  if (audit2.hygiene.length > 0) {
6264
6793
  lines.push("", "## Findings", "");
6265
- 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) {
6266
6804
  lines.push(
6267
- `- **${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))})`
6268
6806
  );
6269
6807
  }
6270
6808
  }
@@ -6294,7 +6832,9 @@ async function routineCommand(options = {}) {
6294
6832
  }
6295
6833
  const config = loadConfig();
6296
6834
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
6297
- 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 });
6298
6838
  const auditState = summarizeAudit(audit2);
6299
6839
  const degraded = [];
6300
6840
  let watch;
@@ -6337,6 +6877,7 @@ async function routineCommand(options = {}) {
6337
6877
  organize: organize ? { skills: organize.registry.count, roots: organize.registry.skillsRoots.length, notices: organize.notices } : void 0,
6338
6878
  pending: { queue: pending.count, quarantine: quarantine2.count },
6339
6879
  housekeeping,
6880
+ notices: storeNotices,
6340
6881
  degraded
6341
6882
  };
6342
6883
  const failure = degraded.length > 0 ? `${degraded.length} step(s) did not complete: ${degraded.join("; ")}` : exceedsFloor(auditState, options.failOn ?? "none");
@@ -6355,6 +6896,7 @@ async function routineCommand(options = {}) {
6355
6896
  console.log(
6356
6897
  ` findings: ${auditState.findings.CRITICAL ?? 0} critical / ${auditState.findings.HIGH ?? 0} high / ${auditState.findings.MEDIUM ?? 0} medium / ${auditState.findings.LOW ?? 0} low`
6357
6898
  );
6899
+ console.log(` accepted: ${auditState.acceptedCount ?? 0} (suppressed from counts; see report)`);
6358
6900
  if (result.watch) console.log(` drift: ${result.watch.moved} moved of ${result.watch.checked} tracked`);
6359
6901
  if (result.organize) console.log(` registry: ${result.organize.skills} skill(s)`);
6360
6902
  console.log(` pending: ${result.pending.queue} queue, ${result.pending.quarantine} quarantine`);
@@ -6504,14 +7046,14 @@ async function queueClose(id, options = {}) {
6504
7046
  }
6505
7047
 
6506
7048
  // src/commands/refine.ts
6507
- import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
6508
- 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";
6509
- 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";
6510
7052
  import { createInterface as createInterface6 } from "readline/promises";
6511
7053
 
6512
7054
  // src/refine/context.ts
6513
- import { existsSync as existsSync23, realpathSync as realpathSync8, statSync as statSync12 } from "fs";
6514
- 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";
6515
7057
 
6516
7058
  // src/refine/models.ts
6517
7059
  import { randomBytes } from "crypto";
@@ -6567,40 +7109,40 @@ function fingerprintKey(skill, target, overrideType, action) {
6567
7109
  return [normalize(skill), normalize(target), overrideType, action ?? ""].join("::");
6568
7110
  }
6569
7111
  function contentSimilarity(a, b) {
6570
- const tokenize2 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
6571
- const tokensA = tokenize2(a);
6572
- 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);
6573
7115
  if (tokensA.size === 0 || tokensB.size === 0) return 0;
6574
7116
  let intersectionSize = 0;
6575
7117
  for (const token of tokensA) {
6576
7118
  if (tokensB.has(token)) intersectionSize++;
6577
7119
  }
6578
- const unionSize2 = tokensA.size + tokensB.size - intersectionSize;
6579
- return unionSize2 === 0 ? 0 : intersectionSize / unionSize2;
7120
+ const unionSize3 = tokensA.size + tokensB.size - intersectionSize;
7121
+ return unionSize3 === 0 ? 0 : intersectionSize / unionSize3;
6580
7122
  }
6581
7123
 
6582
7124
  // src/refine/store.ts
6583
- import { createHash as createHash3 } from "crypto";
6584
- import { existsSync as existsSync22, readFileSync as readFileSync16, statSync as statSync11, realpathSync as realpathSync7 } from "fs";
6585
- 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";
6586
7128
  var SIMILARITY_JOIN_THRESHOLD = 0.7;
6587
7129
  var READY_THRESHOLD = 2;
6588
7130
  function getRefinementsDir() {
6589
- return join27(getHomeDir(), "refinements");
7131
+ return join28(getHomeDir(), "refinements");
6590
7132
  }
6591
7133
  function getRefinementsPath() {
6592
- return join27(getRefinementsDir(), "refinements.json");
7134
+ return join28(getRefinementsDir(), "refinements.json");
6593
7135
  }
6594
7136
  function getPatternsPath() {
6595
- return join27(getRefinementsDir(), "patterns.json");
7137
+ return join28(getRefinementsDir(), "patterns.json");
6596
7138
  }
6597
7139
  function getBackupsDir() {
6598
- return join27(getRefinementsDir(), "backups");
7140
+ return join28(getRefinementsDir(), "backups");
6599
7141
  }
6600
7142
  function readJsonFile(path, label) {
6601
7143
  let raw;
6602
7144
  try {
6603
- raw = readFileSync16(path, "utf-8");
7145
+ raw = readFileSync19(path, "utf-8");
6604
7146
  } catch (err) {
6605
7147
  throw new UserError(`unable to read ${label} at ${path}: ${err.message}`);
6606
7148
  }
@@ -6611,7 +7153,7 @@ function readJsonFile(path, label) {
6611
7153
  }
6612
7154
  }
6613
7155
  function readStore(path, label, validateRecord) {
6614
- if (!existsSync22(path)) {
7156
+ if (!existsSync25(path)) {
6615
7157
  return { schemaVersion: 1, records: [] };
6616
7158
  }
6617
7159
  const parsed = readJsonFile(path, label);
@@ -6835,16 +7377,16 @@ function createBackup(filePaths, deps = {}) {
6835
7377
  const randomHex = deps.randomHex ?? defaultRandomHex;
6836
7378
  const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
6837
7379
  const files = filePaths.map((path) => {
6838
- if (!existsSync22(path)) {
7380
+ if (!existsSync25(path)) {
6839
7381
  return { path, existed: false, sha256: null, mode: null, contentBase64: null };
6840
7382
  }
6841
- const bytes = readFileSync16(path);
7383
+ const bytes = readFileSync19(path);
6842
7384
  const mode = statSync11(path).mode & 511;
6843
- const sha256 = createHash3("sha256").update(bytes).digest("hex");
6844
- 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") };
6845
7387
  });
6846
7388
  const manifest = { id, createdAt: now.toISOString(), files };
6847
- writeJsonAtomic(join27(getBackupsDir(), id, "manifest.json"), manifest);
7389
+ writeJsonAtomic(join28(getBackupsDir(), id, "manifest.json"), manifest);
6848
7390
  return manifest;
6849
7391
  }
6850
7392
  function validateBackupManifestShape(value, path) {
@@ -6893,8 +7435,8 @@ function readBackupManifest(id) {
6893
7435
  if (!isValidBackupId(id)) {
6894
7436
  throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
6895
7437
  }
6896
- const manifestPath = join27(getBackupsDir(), id, "manifest.json");
6897
- if (!existsSync22(manifestPath)) {
7438
+ const manifestPath = join28(getBackupsDir(), id, "manifest.json");
7439
+ if (!existsSync25(manifestPath)) {
6898
7440
  throw new UserError(`no backup with id: ${id}`);
6899
7441
  }
6900
7442
  const parsed = readJsonFile(manifestPath, "backup manifest");
@@ -6928,20 +7470,20 @@ function isUnderRoot2(candidate, root) {
6928
7470
  function resolveScopes(config, cwd, skill, userRootFlag) {
6929
7471
  assertPlainSkillName(skill);
6930
7472
  const resolvedCwd = resolvePath11(cwd);
6931
- const localDir = join28(resolvedCwd, ".claude", "skills", skill);
6932
- const sharedDir = join28(resolvedCwd, "skills", skill);
6933
- const local = { scope: "local", dir: localDir, exists: existsSync23(localDir) };
6934
- 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) };
6935
7477
  const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
6936
7478
  return { local, shared, user };
6937
7479
  }
6938
7480
  function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
6939
7481
  if (userRootFlag) {
6940
7482
  const root = resolvePath11(userRootFlag);
6941
- return { resolved: true, dir: join28(root, skill), root: userRootFlag, reason: "flag" };
7483
+ return { resolved: true, dir: join29(root, skill), root: userRootFlag, reason: "flag" };
6942
7484
  }
6943
7485
  const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
6944
- 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) }));
6945
7487
  for (const candidate of candidates) {
6946
7488
  const skillReal = realpathOrNull2(candidate.dir);
6947
7489
  if (skillReal) {
@@ -6976,12 +7518,12 @@ function inventoryScopeDir(scope, dir) {
6976
7518
  scope,
6977
7519
  dir,
6978
7520
  dirExists: isDirectory(dir),
6979
- skillMd: isFile2(join28(dir, "SKILL.md")),
6980
- patchMd: isFile2(join28(dir, "SKILL.patch.md")),
6981
- extendMd: isFile2(join28(dir, "SKILL.extend.md")),
6982
- configJson: isFile2(join28(dir, "skill-config.json")),
6983
- hooksDir: isDirectory(join28(dir, "hooks")),
6984
- 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"))
6985
7527
  };
6986
7528
  }
6987
7529
  function inventoryAllScopes(resolution) {
@@ -7261,7 +7803,7 @@ function sanitizeDiffForConsole(diff) {
7261
7803
  }
7262
7804
  function resolveContentOption(options) {
7263
7805
  if (options.content !== void 0) return options.content;
7264
- if (options.contentFile) return readFileSync17(resolvePath12(options.contentFile), "utf-8");
7806
+ if (options.contentFile) return readFileSync20(resolvePath12(options.contentFile), "utf-8");
7265
7807
  return void 0;
7266
7808
  }
7267
7809
  function validateCaptureOptions(options) {
@@ -7413,7 +7955,7 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
7413
7955
  }
7414
7956
  function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
7415
7957
  const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
7416
- const destPath = join29(scopeDir, relPath);
7958
+ const destPath = join30(scopeDir, relPath);
7417
7959
  const destDir = dirname13(destPath);
7418
7960
  mkdirSync9(destDir, { recursive: true });
7419
7961
  const realDestDir = realpathSync9(destDir);
@@ -7509,12 +8051,12 @@ Write ${preview.destPath}? [y/N] `);
7509
8051
  }
7510
8052
  function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
7511
8053
  const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
7512
- const probePath = probeRelPath ? join29(scopeDir, probeRelPath) : void 0;
7513
- 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") : "";
7514
8056
  const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
7515
8057
  const relPath = rendered.relPath;
7516
- const destPath = join29(scopeDir, relPath);
7517
- const before = existsSync24(destPath) ? readFileSync17(destPath, "utf-8") : "";
8058
+ const destPath = join30(scopeDir, relPath);
8059
+ const before = existsSync27(destPath) ? readFileSync20(destPath, "utf-8") : "";
7518
8060
  const diff = previewDiff(before, rendered.content, destPath);
7519
8061
  return { relPath, content: rendered.content, destPath, diff };
7520
8062
  }
@@ -7611,16 +8153,16 @@ async function refineWhichCommand(skill, options = {}) {
7611
8153
  }
7612
8154
  function writePostWriteSidecar(backupId, files) {
7613
8155
  const sidecar = { schemaVersion: 1, files };
7614
- const dir = join29(getBackupsDir(), backupId);
8156
+ const dir = join30(getBackupsDir(), backupId);
7615
8157
  mkdirSync9(dir, { recursive: true });
7616
- 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 });
7617
8159
  }
7618
8160
  function readPostWriteSidecar(backupId) {
7619
- const path = join29(getBackupsDir(), backupId, "post.json");
7620
- 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" };
7621
8163
  let raw;
7622
8164
  try {
7623
- raw = readFileSync17(path, "utf-8");
8165
+ raw = readFileSync20(path, "utf-8");
7624
8166
  } catch {
7625
8167
  return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
7626
8168
  }
@@ -7678,8 +8220,8 @@ async function refinePromoteCommand(patternId, options = {}) {
7678
8220
  if (!isUnderRoot3(realSkillDir, realUserRoot)) {
7679
8221
  throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
7680
8222
  }
7681
- const skillMdPath = join29(skillDir, "SKILL.md");
7682
- if (!existsSync24(skillMdPath)) {
8223
+ const skillMdPath = join30(skillDir, "SKILL.md");
8224
+ if (!existsSync27(skillMdPath)) {
7683
8225
  throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
7684
8226
  }
7685
8227
  const refinementsStore = readRefinementsStore();
@@ -7696,7 +8238,7 @@ async function refinePromoteCommand(patternId, options = {}) {
7696
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.`
7697
8239
  );
7698
8240
  }
7699
- const before = readFileSync17(skillMdPath, "utf-8");
8241
+ const before = readFileSync20(skillMdPath, "utf-8");
7700
8242
  const after = applyPatchBlocks(before, [block]);
7701
8243
  const diff = previewDiff(before, after, skillMdPath);
7702
8244
  if (options.dryRun) {
@@ -7728,7 +8270,7 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
7728
8270
  throw err;
7729
8271
  }
7730
8272
  try {
7731
- const postSha256 = createHash4("sha256").update(readFileSync17(skillMdPath)).digest("hex");
8273
+ const postSha256 = createHash6("sha256").update(readFileSync20(skillMdPath)).digest("hex");
7732
8274
  writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
7733
8275
  markGeneralized(patternId, backup.id);
7734
8276
  const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
@@ -7754,10 +8296,10 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
7754
8296
  }
7755
8297
  function assertBackupDirContained(backupId) {
7756
8298
  const backupsDir = getBackupsDir();
7757
- if (!existsSync24(backupsDir)) return;
8299
+ if (!existsSync27(backupsDir)) return;
7758
8300
  const realBackupsDir = realpathSync9(backupsDir);
7759
- const backupDir = join29(backupsDir, backupId);
7760
- if (!existsSync24(backupDir)) return;
8301
+ const backupDir = join30(backupsDir, backupId);
8302
+ if (!existsSync27(backupDir)) return;
7761
8303
  const realBackupDir = realpathSync9(backupDir);
7762
8304
  if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
7763
8305
  throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
@@ -7769,7 +8311,7 @@ function resolveTargetRealPath(path) {
7769
8311
  try {
7770
8312
  const real = realpathSync9(lexicalAncestor);
7771
8313
  const remainder = relative4(lexicalAncestor, resolvePath12(path));
7772
- return remainder === "" ? real : join29(real, remainder);
8314
+ return remainder === "" ? real : join30(real, remainder);
7773
8315
  } catch {
7774
8316
  const parent = dirname13(lexicalAncestor);
7775
8317
  if (parent === lexicalAncestor) return resolvePath12(path);
@@ -7838,7 +8380,7 @@ function walkRealComponents(filePath, config, createMissing) {
7838
8380
  let current = rootReal;
7839
8381
  for (const component of dirComponents) {
7840
8382
  if (current === void 0) break;
7841
- const next = join29(current, component);
8383
+ const next = join30(current, component);
7842
8384
  let lst;
7843
8385
  try {
7844
8386
  lst = lstatSync4(next);
@@ -7869,7 +8411,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
7869
8411
  if (parentReal === void 0) {
7870
8412
  throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
7871
8413
  }
7872
- const finalPath = join29(parentReal, leafComponent);
8414
+ const finalPath = join30(parentReal, leafComponent);
7873
8415
  if (!isUnderRoot3(finalPath, rootReal)) {
7874
8416
  throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
7875
8417
  }
@@ -7895,7 +8437,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
7895
8437
  function safeDeleteTombstone(filePath, config) {
7896
8438
  const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
7897
8439
  if (parentReal === void 0) return;
7898
- const finalPath = join29(parentReal, leafComponent);
8440
+ const finalPath = join30(parentReal, leafComponent);
7899
8441
  if (!isUnderRoot3(finalPath, rootReal)) {
7900
8442
  throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
7901
8443
  }
@@ -7937,7 +8479,7 @@ async function refineRollbackCommand(backupId, options = {}) {
7937
8479
  for (const file of manifest.files) {
7938
8480
  const expected = sidecar.hashes.get(file.path);
7939
8481
  if (expected === void 0) continue;
7940
- 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;
7941
8483
  if (currentSha256 !== expected) mismatches.push(file.path);
7942
8484
  }
7943
8485
  if (mismatches.length > 0 && !options.force) {
@@ -8033,8 +8575,14 @@ program.name("skill-forge").description("The supply-chain gate for agent skills"
8033
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(
8034
8576
  "--mcp-target <file>",
8035
8577
  "MCP config file to promote into (mcp artifacts only; defaults to config.mcpTargets[0], then <cwd>/.mcp.json)"
8036
- ).option("--force", "overwrite an existing same-name MCP server entry when promoting (mcp artifacts only)").action(add);
8037
- 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);
8038
8586
  program.command("list").description("List skills currently in quarantine").action(list);
8039
8587
  program.command("status").description("Show skill-forge configuration and quarantine status").action(status);
8040
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);
@@ -8049,9 +8597,16 @@ configCmd.command("review").description("Review pending agent-proposed preferenc
8049
8597
  program.command("guide [topic]").description("What this tool does, what state you are in, and the next command to run").action(guide);
8050
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);
8051
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);
8052
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);
8053
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);
8054
- 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);
8055
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);
8056
8611
  program.command("ingest").description(
8057
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."