@rhize/skill-forge 0.11.3 → 0.13.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
  }
@@ -2954,6 +3131,33 @@ function extractDescription(block) {
2954
3131
  }
2955
3132
  return stripQuotes(first);
2956
3133
  }
3134
+ function extractExtends(block) {
3135
+ const lines = block.split("\n");
3136
+ const idx = lines.findIndex((l) => /^\s*extends:\s*(.*)$/.test(l));
3137
+ if (idx === -1) return void 0;
3138
+ const inline = lines[idx].match(/^\s*extends:\s*(.*)$/)[1].trim();
3139
+ if (inline.startsWith("[")) {
3140
+ const inner = inline.replace(/^\[/, "").replace(/\]\s*$/, "");
3141
+ const items = inner.split(",").map((s) => stripQuotes(s.trim())).filter(Boolean);
3142
+ return items.length ? items : void 0;
3143
+ }
3144
+ if (inline === "") {
3145
+ const items = [];
3146
+ for (let i = idx + 1; i < lines.length; i++) {
3147
+ const line = lines[i];
3148
+ const m = line.match(/^\s*-\s*(.+)$/);
3149
+ if (m) {
3150
+ items.push(stripQuotes(m[1].trim()));
3151
+ } else if (line.trim() === "") {
3152
+ continue;
3153
+ } else {
3154
+ break;
3155
+ }
3156
+ }
3157
+ return items.length ? items : void 0;
3158
+ }
3159
+ return [stripQuotes(inline)];
3160
+ }
2957
3161
  function parseFrontmatter(text) {
2958
3162
  if (!text.startsWith("---\n")) {
2959
3163
  return { validFrontmatter: false };
@@ -2964,16 +3168,17 @@ function parseFrontmatter(text) {
2964
3168
  name: extractSimpleField(block, "name"),
2965
3169
  version: extractSimpleField(block, "version"),
2966
3170
  license: extractSimpleField(block, "license"),
2967
- description: extractDescription(block)
3171
+ description: extractDescription(block),
3172
+ extends: extractExtends(block)
2968
3173
  };
2969
3174
  }
2970
3175
  function detectLicense(root, fm) {
2971
3176
  if (fm.license) return fm.license;
2972
3177
  const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
2973
3178
  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();
3179
+ const p = join15(root, name);
3180
+ if (existsSync12(p) && statSync7(p).isFile()) {
3181
+ const head = readFileSync12(p, "utf-8").slice(0, 2e3).toLowerCase();
2977
3182
  for (const [hint, label] of Object.entries(SPDX_HINTS)) {
2978
3183
  if (head.includes(hint)) return label;
2979
3184
  }
@@ -2985,7 +3190,7 @@ function detectLicense(root, fm) {
2985
3190
  function walkFiles(dir) {
2986
3191
  const out = [];
2987
3192
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
2988
- const full = join14(dir, entry.name);
3193
+ const full = join15(dir, entry.name);
2989
3194
  if (entry.isDirectory()) {
2990
3195
  out.push(...walkFiles(full));
2991
3196
  } else if (entry.isFile()) {
@@ -2997,7 +3202,7 @@ function walkFiles(dir) {
2997
3202
  function countResources(root) {
2998
3203
  const resources = {};
2999
3204
  for (const d of RESOURCE_DIRS) {
3000
- const dp = join14(root, d);
3205
+ const dp = join15(root, d);
3001
3206
  if (isDir(dp)) {
3002
3207
  resources[d] = walkFiles(dp).length;
3003
3208
  }
@@ -3008,11 +3213,11 @@ function scanMcpAndImports(text, root) {
3008
3213
  const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
3009
3214
  const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
3010
3215
  const imports = /* @__PURE__ */ new Set();
3011
- const scriptsDir = join14(root, "scripts");
3216
+ const scriptsDir = join15(root, "scripts");
3012
3217
  if (isDir(scriptsDir)) {
3013
3218
  for (const f of walkFiles(scriptsDir)) {
3014
3219
  if (!f.endsWith(".py")) continue;
3015
- const ft = readFileSync11(f, "utf-8");
3220
+ const ft = readFileSync12(f, "utf-8");
3016
3221
  for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
3017
3222
  const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
3018
3223
  let m;
@@ -3029,7 +3234,7 @@ function scanMcpAndImports(text, root) {
3029
3234
  function profileSkillDir(inputPath) {
3030
3235
  const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
3031
3236
  try {
3032
- const text = readFileSync11(skillMd, "utf-8");
3237
+ const text = readFileSync12(skillMd, "utf-8");
3033
3238
  const fm = parseFrontmatter(text);
3034
3239
  const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
3035
3240
  const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
@@ -3050,26 +3255,27 @@ function profileSkillDir(inputPath) {
3050
3255
  resources,
3051
3256
  mcpDependencies: mcpDeps,
3052
3257
  externalPythonDeps: externalDeps,
3053
- artifactType: detectArtifactType(root)
3258
+ artifactType: detectArtifactType(root),
3259
+ extends: fm.extends ?? []
3054
3260
  };
3055
3261
  } finally {
3056
3262
  cleanupTmp(tmpDir);
3057
3263
  }
3058
3264
  }
3059
3265
  function profilePlugin(p) {
3060
- const manifestPath = join14(p, ".claude-plugin", "plugin.json");
3266
+ const manifestPath = join15(p, ".claude-plugin", "plugin.json");
3061
3267
  let manifest;
3062
3268
  try {
3063
- manifest = JSON.parse(readFileSync11(manifestPath, "utf-8"));
3269
+ manifest = JSON.parse(readFileSync12(manifestPath, "utf-8"));
3064
3270
  } catch (err) {
3065
3271
  throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
3066
3272
  }
3067
- const skillsDir = join14(p, "skills");
3273
+ const skillsDir = join15(p, "skills");
3068
3274
  let skills = [];
3069
3275
  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();
3276
+ skills = readdirSync6(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync12(join15(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
3071
3277
  }
3072
- const commandsDir = join14(p, "commands");
3278
+ const commandsDir = join15(p, "commands");
3073
3279
  const commands = isDir(commandsDir) ? readdirSync6(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
3074
3280
  const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
3075
3281
  const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
@@ -3087,11 +3293,11 @@ function profilePlugin(p) {
3087
3293
  };
3088
3294
  }
3089
3295
  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;
3296
+ const explicitConfig = existsSync12(join15(p, ".mcp.json")) ? join15(p, ".mcp.json") : existsSync12(join15(p, "mcp.json")) ? join15(p, "mcp.json") : void 0;
3091
3297
  if (explicitConfig) {
3092
3298
  let cfg;
3093
3299
  try {
3094
- cfg = JSON.parse(readFileSync11(explicitConfig, "utf-8"));
3300
+ cfg = JSON.parse(readFileSync12(explicitConfig, "utf-8"));
3095
3301
  } catch (err) {
3096
3302
  throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
3097
3303
  }
@@ -3107,10 +3313,10 @@ function profileMcp(p) {
3107
3313
  capabilities: extractMcpCapabilities(p)
3108
3314
  };
3109
3315
  }
3110
- const pkgPath = join14(p, "package.json");
3316
+ const pkgPath = join15(p, "package.json");
3111
3317
  let pkg2;
3112
3318
  try {
3113
- pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
3319
+ pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3114
3320
  } catch (err) {
3115
3321
  throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
3116
3322
  }
@@ -3127,14 +3333,14 @@ function profileMcp(p) {
3127
3333
  }
3128
3334
 
3129
3335
  // src/gate/mcpOverlap.ts
3130
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
3336
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
3131
3337
  import { resolve as resolve4 } from "path";
3132
3338
  function readTargetServers(targetPath) {
3133
3339
  const path = resolve4(expandHome(targetPath));
3134
- if (!existsSync12(path)) return [];
3340
+ if (!existsSync13(path)) return [];
3135
3341
  let cfg;
3136
3342
  try {
3137
- cfg = JSON.parse(readFileSync12(path, "utf-8"));
3343
+ cfg = JSON.parse(readFileSync13(path, "utf-8"));
3138
3344
  } catch {
3139
3345
  return [];
3140
3346
  }
@@ -3206,6 +3412,144 @@ function scanMcpOverlap(candidate, mcpTargetPaths) {
3206
3412
  };
3207
3413
  }
3208
3414
 
3415
+ // src/gate/skillMap.ts
3416
+ import { readFileSync as readFileSync14 } from "fs";
3417
+ import { resolve as resolve5 } from "path";
3418
+ function loadSkillMap(path) {
3419
+ const resolved = resolve5(expandHome(path));
3420
+ let text;
3421
+ try {
3422
+ text = readFileSync14(resolved, "utf-8");
3423
+ } catch (err) {
3424
+ throw new Error(`could not read skill map at ${resolved}: ${err.message}`);
3425
+ }
3426
+ let parsed;
3427
+ try {
3428
+ parsed = JSON.parse(text);
3429
+ } catch (err) {
3430
+ throw new Error(`skill map at ${resolved} is not valid JSON: ${err.message}`);
3431
+ }
3432
+ if (typeof parsed !== "object" || parsed === null) {
3433
+ throw new Error(`skill map at ${resolved}: expected a JSON object`);
3434
+ }
3435
+ const obj = parsed;
3436
+ if (!Array.isArray(obj.nodes) || !Array.isArray(obj.edges)) {
3437
+ throw new Error(`skill map at ${resolved}: expected "nodes" and "edges" arrays`);
3438
+ }
3439
+ const nodes = obj.nodes.map((n) => ({
3440
+ ...n,
3441
+ type: typeof n.type === "string" ? n.type : n.kind
3442
+ }));
3443
+ return {
3444
+ schemaVersion: typeof obj.schemaVersion === "string" ? obj.schemaVersion : void 0,
3445
+ nodes,
3446
+ edges: obj.edges
3447
+ };
3448
+ }
3449
+ function nodeById(map, id) {
3450
+ return map.nodes.find((n) => n.id === id);
3451
+ }
3452
+ function edgesOfType(map, type) {
3453
+ return map.edges.filter((e) => e.type === type);
3454
+ }
3455
+ function tagsForNode(map, node) {
3456
+ const tags = new Set(node.tags ?? []);
3457
+ for (const edge of map.edges) {
3458
+ if (edge.from !== node.id) continue;
3459
+ if (edge.type !== "topic-tag" && edge.type !== "stack-tag") continue;
3460
+ const tagNode = nodeById(map, edge.to);
3461
+ const label = tagNode?.name ?? tagNode?.id;
3462
+ if (label) tags.add(label);
3463
+ }
3464
+ return Array.from(tags);
3465
+ }
3466
+
3467
+ // src/gate/mapOverlap.ts
3468
+ var STOPWORDS2 = new Set(
3469
+ `
3470
+ a an the and or of to for in on with use used using when whether this that these those is are be
3471
+ your you it its as at by from into via per also any all not no than then so if up out about
3472
+ skill skills user claude code based comprehensive enhanced practical including include includes
3473
+ `.split(/\s+/).filter(Boolean)
3474
+ );
3475
+ var STRONG_THRESHOLD = 0.45;
3476
+ var REPORT_THRESHOLD = 0.2;
3477
+ function tokenize2(text) {
3478
+ const matches = text.toLowerCase().match(/[a-z][a-z0-9+.-]{2,}/g) ?? [];
3479
+ return new Set(matches.filter((t) => !STOPWORDS2.has(t)));
3480
+ }
3481
+ function intersectSize2(a, b) {
3482
+ let n = 0;
3483
+ for (const x of a) if (b.has(x)) n += 1;
3484
+ return n;
3485
+ }
3486
+ function unionSize2(a, b) {
3487
+ const u = new Set(a);
3488
+ for (const x of b) u.add(x);
3489
+ return u.size;
3490
+ }
3491
+ function round33(n) {
3492
+ return Math.round(n * 1e3) / 1e3;
3493
+ }
3494
+ function score3(candidateTokens, otherTokens) {
3495
+ if (candidateTokens.size === 0) return 0;
3496
+ const inter = intersectSize2(candidateTokens, otherTokens);
3497
+ const union = unionSize2(candidateTokens, otherTokens);
3498
+ const jaccard = union ? inter / union : 0;
3499
+ const containment = inter / candidateTokens.size;
3500
+ return round33(0.3 * jaccard + 0.7 * containment);
3501
+ }
3502
+ function nodeText(map, node) {
3503
+ const tags = tagsForNode(map, node);
3504
+ return [node.name ?? "", node.description ?? "", ...tags].join(" ");
3505
+ }
3506
+ function scanMapOverlap(candidate, map) {
3507
+ const candidateTokens = tokenize2(`${candidate.name ?? ""} ${candidate.description ?? ""}`);
3508
+ if (candidateTokens.size === 0) return { matches: [] };
3509
+ const matches = [];
3510
+ for (const node of map.nodes) {
3511
+ if (node.type !== "skill") continue;
3512
+ const otherTokens = tokenize2(nodeText(map, node));
3513
+ const s = score3(candidateTokens, otherTokens);
3514
+ if (s >= REPORT_THRESHOLD) {
3515
+ matches.push({ skillId: node.id, name: node.name ?? node.id, score: s });
3516
+ }
3517
+ }
3518
+ matches.sort((a, b) => b.score - a.score);
3519
+ return { matches };
3520
+ }
3521
+ function mapOverlapFinding(result) {
3522
+ const top = result.matches[0];
3523
+ if (!top) return void 0;
3524
+ const severity = top.score >= STRONG_THRESHOLD ? "HIGH" : "MEDIUM";
3525
+ return {
3526
+ severity,
3527
+ rule: "skill-map-overlap",
3528
+ file: "skill-map",
3529
+ detail: `candidate overlaps existing marketplace skill "${top.name}" (score ${top.score}) \u2014 see CLAUDE.md's curation rule: close the gap, don't duplicate`,
3530
+ context: top.skillId
3531
+ };
3532
+ }
3533
+ function isDeclaredExtension(match, extendsDeclarations) {
3534
+ const qualifiedId = match.skillId.replace(/^[a-z]+:/, "");
3535
+ const bareId = qualifiedId.split("/").pop() ?? qualifiedId;
3536
+ return extendsDeclarations.some((raw) => {
3537
+ const declared = raw.trim();
3538
+ if (!declared) return false;
3539
+ return declared === match.name || declared === bareId || declared === qualifiedId;
3540
+ });
3541
+ }
3542
+ function mapOverlapDecision(result, extendsDeclarations = []) {
3543
+ const top = result.matches[0];
3544
+ if (!top) return {};
3545
+ if (extendsDeclarations.length > 0 && isDeclaredExtension(top, extendsDeclarations)) {
3546
+ return {
3547
+ informational: `declared extension of "${top.name}" (score ${top.score}) \u2014 overlap expected; verify the layering claim in review`
3548
+ };
3549
+ }
3550
+ return { finding: mapOverlapFinding(result) };
3551
+ }
3552
+
3209
3553
  // src/report/render.ts
3210
3554
  var SAFETY_VERDICT_MAP = {
3211
3555
  pass: "ALLOW",
@@ -3234,10 +3578,10 @@ function renderSafetyLine(safety) {
3234
3578
  const withDetail = detail ? `${safety.verdict} (${detail})` : safety.verdict;
3235
3579
  return safety.recommendation ? `${withDetail} \u2014 ${safety.recommendation}` : withDetail;
3236
3580
  }
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}` : ""}`;
3581
+ function renderFindingLine(finding2) {
3582
+ const severity = finding2.severity ?? "UNKNOWN";
3583
+ const rule = finding2.rule ?? "unknown-rule";
3584
+ return ` - [${severity}] ${rule}${finding2.message ? `: ${finding2.message}` : ""}`;
3241
3585
  }
3242
3586
  function renderFindingsLines(findings) {
3243
3587
  if (!findings?.length) return [];
@@ -3331,6 +3675,16 @@ function tryMcpOverlap(profile, mcpTargets) {
3331
3675
  return void 0;
3332
3676
  }
3333
3677
  }
3678
+ function tryMapOverlapDecision(skillMapPath, candidate, extendsDeclarations) {
3679
+ if (!skillMapPath) return void 0;
3680
+ try {
3681
+ const map = loadSkillMap(skillMapPath);
3682
+ return mapOverlapDecision(scanMapOverlap(candidate, map), extendsDeclarations);
3683
+ } catch (err) {
3684
+ console.error(`[skill-forge] skill-map overlap check skipped: ${err.message}`);
3685
+ return void 0;
3686
+ }
3687
+ }
3334
3688
  function resolveMcpCandidatePath(entry) {
3335
3689
  if (entry.packageDir) return entry.packageDir;
3336
3690
  let children = [];
@@ -3338,49 +3692,64 @@ function resolveMcpCandidatePath(entry) {
3338
3692
  children = readdirSync7(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
3339
3693
  } catch {
3340
3694
  }
3341
- return children.length === 1 ? join15(entry.quarantinePath, children[0]) : entry.quarantinePath;
3695
+ return children.length === 1 ? join16(entry.quarantinePath, children[0]) : entry.quarantinePath;
3342
3696
  }
3343
3697
  async function runGate(candidatePath, skillsRoot, {
3344
3698
  proFeatureLabel = "overlap analysis",
3345
3699
  strictness = DEFAULT_STRICTNESS,
3346
3700
  artifact = "skill",
3347
- mcpTargets = []
3701
+ mcpTargets = [],
3702
+ skillMapPath
3348
3703
  } = {}) {
3349
3704
  const profile = asProfile(candidatePath, artifact);
3350
3705
  const proStatus = proFeatureStatus(proFeatureLabel);
3351
3706
  const safety = artifact === "mcp" ? await mcpScan(candidatePath, strictness) : await scanSafety(candidatePath, strictness);
3352
3707
  const overlap = !proStatus.enabled ? void 0 : artifact === "mcp" ? tryMcpOverlap(profile, mcpTargets) : tryOverlap(candidatePath, skillsRoot);
3708
+ const mapDecision = artifact === "mcp" ? void 0 : tryMapOverlapDecision(
3709
+ skillMapPath,
3710
+ {
3711
+ name: "name" in profile ? profile.name : void 0,
3712
+ description: "description" in profile ? profile.description : void 0
3713
+ },
3714
+ profile.kind === "skill" ? profile.extends : []
3715
+ );
3716
+ const mapFinding = mapDecision?.finding;
3717
+ const combinedSafety = mapFinding ? { verdict: verdictFor([...safety.findings, mapFinding], strictness), findings: [...safety.findings, mapFinding] } : safety;
3353
3718
  const advisory = formatAuditAdvisory(readAuditState(), {
3354
3719
  targetRoot: skillsRoot || void 0,
3355
3720
  candidateName: "name" in profile ? profile.name : void 0
3356
3721
  });
3722
+ const notices = [
3723
+ ...proStatus.notice ? [proStatus.notice] : [],
3724
+ ...mapDecision?.informational ? [mapDecision.informational] : []
3725
+ ];
3357
3726
  const gateResult = {
3358
3727
  profile,
3359
3728
  overlap,
3360
3729
  overlapLocked: !proStatus.enabled,
3361
- notices: proStatus.notice ? [proStatus.notice] : void 0,
3730
+ notices: notices.length > 0 ? notices : void 0,
3362
3731
  safety: {
3363
- verdict: SAFETY_VERDICT_MAP[safety.verdict],
3364
- findings: mapSafetyFindings(safety.findings)
3732
+ verdict: SAFETY_VERDICT_MAP[combinedSafety.verdict],
3733
+ findings: mapSafetyFindings(combinedSafety.findings)
3365
3734
  },
3366
3735
  advisory
3367
3736
  };
3368
- return { safety, gateResult, proEnabled: proStatus.enabled };
3737
+ return { safety: combinedSafety, gateResult, proEnabled: proStatus.enabled };
3369
3738
  }
3370
3739
 
3371
3740
  // src/queue.ts
3372
3741
  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";
3742
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync15, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
3743
+ import { join as join17 } from "path";
3375
3744
  function getQueuePath() {
3376
- return join16(getHomeDir(), "queue.json");
3745
+ return join17(getHomeDir(), "queue.json");
3377
3746
  }
3378
3747
  function readQueue() {
3379
3748
  const path = getQueuePath();
3380
- if (!existsSync13(path)) {
3749
+ if (!existsSync14(path)) {
3381
3750
  return { version: 1, entries: [] };
3382
3751
  }
3383
- const raw = readFileSync13(path, "utf-8");
3752
+ const raw = readFileSync15(path, "utf-8");
3384
3753
  const parsed = JSON.parse(raw);
3385
3754
  return { version: 1, entries: parsed.entries ?? [] };
3386
3755
  }
@@ -3388,7 +3757,7 @@ function writeQueue(queue2) {
3388
3757
  const home = getHomeDir();
3389
3758
  mkdirSync6(home, { recursive: true });
3390
3759
  const finalPath = getQueuePath();
3391
- const tempPath = join16(home, `.queue.json.tmp-${randomUUID3()}`);
3760
+ const tempPath = join17(home, `.queue.json.tmp-${randomUUID3()}`);
3392
3761
  try {
3393
3762
  writeFileSync6(tempPath, JSON.stringify(queue2, null, 2) + "\n", "utf-8");
3394
3763
  renameSync4(tempPath, finalPath);
@@ -3437,16 +3806,16 @@ function generateId(existingIds) {
3437
3806
 
3438
3807
  // src/handoff.ts
3439
3808
  import { spawnSync } from "child_process";
3440
- import { existsSync as existsSync14 } from "fs";
3441
- import { dirname as dirname7, join as join17 } from "path";
3809
+ import { existsSync as existsSync15 } from "fs";
3810
+ import { dirname as dirname7, join as join18 } from "path";
3442
3811
  import { fileURLToPath } from "url";
3443
3812
  var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
3444
3813
  var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
3445
3814
  function resolvePromptPath(assetName = "ingest-prompt.md") {
3446
3815
  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);
3816
+ const builtCandidate = join18(here, assetName);
3817
+ if (existsSync15(builtCandidate)) return builtCandidate;
3818
+ return join18(here, "..", "assets", assetName);
3450
3819
  }
3451
3820
  function defaultIsOnPath(bin) {
3452
3821
  const finder = process.platform === "win32" ? "where" : "which";
@@ -3502,14 +3871,14 @@ function runHandoff(config, installedPath, options = {}) {
3502
3871
  }
3503
3872
 
3504
3873
  // src/commands/init.ts
3505
- import { existsSync as existsSync16 } from "fs";
3874
+ import { existsSync as existsSync17 } from "fs";
3506
3875
  import { homedir as homedir4 } from "os";
3507
3876
  import { createInterface as createInterface3 } from "readline/promises";
3508
3877
 
3509
3878
  // src/agents.ts
3510
3879
  import { statSync as statSync8 } from "fs";
3511
3880
  import { homedir as homedir3 } from "os";
3512
- import { join as join18, resolve as resolve5 } from "path";
3881
+ import { join as join19, resolve as resolve6 } from "path";
3513
3882
  function isFile(p) {
3514
3883
  try {
3515
3884
  return statSync8(p).isFile();
@@ -3518,12 +3887,12 @@ function isFile(p) {
3518
3887
  }
3519
3888
  }
3520
3889
  function projectPath(root, cwd) {
3521
- return root ? resolve5(cwd, root) : void 0;
3890
+ return root ? resolve6(cwd, root) : void 0;
3522
3891
  }
3523
3892
  function globalPath(root, home) {
3524
3893
  if (!root) return void 0;
3525
3894
  if (root === "~") return home;
3526
- if (root.startsWith("~/")) return join18(home, root.slice(2));
3895
+ if (root.startsWith("~/")) return join19(home, root.slice(2));
3527
3896
  return root;
3528
3897
  }
3529
3898
  function makeAgent(spec) {
@@ -3753,8 +4122,8 @@ var AGENTS = [
3753
4122
  ].map(makeAgent);
3754
4123
 
3755
4124
  // src/commands/audit.ts
3756
- import { existsSync as existsSync15 } from "fs";
3757
- import { join as join19, resolve as resolvePath3 } from "path";
4125
+ import { existsSync as existsSync16 } from "fs";
4126
+ import { join as join20, resolve as resolvePath3 } from "path";
3758
4127
  import { createInterface as createInterface2 } from "readline/promises";
3759
4128
 
3760
4129
  // src/decision.ts
@@ -3822,7 +4191,7 @@ async function captureBusinessProfile(rl, existing) {
3822
4191
  function resolveReportPath(reportOption) {
3823
4192
  if (reportOption) return resolvePath3(reportOption);
3824
4193
  const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3825
- return join19(getHomeDir(), "reports", `audit-${iso}.md`);
4194
+ return join20(getHomeDir(), "reports", `audit-${iso}.md`);
3826
4195
  }
3827
4196
  function printTerminalSummary(report, reportPath) {
3828
4197
  const { summary } = report;
@@ -3831,6 +4200,7 @@ function printTerminalSummary(report, reportPath) {
3831
4200
  console.log(
3832
4201
  ` findings: ${summary.findingCounts.CRITICAL} CRITICAL, ${summary.findingCounts.HIGH} HIGH, ${summary.findingCounts.MEDIUM} MEDIUM, ${summary.findingCounts.LOW} LOW`
3833
4202
  );
4203
+ console.log(` accepted: ${summary.acceptedCount} (suppressed from counts; see report)`);
3834
4204
  console.log(` overlap clusters: ${summary.overlapClusterCount} evolve-eligible: ${summary.evolveEligibleCount}`);
3835
4205
  console.log(` report -> ${reportPath}`);
3836
4206
  for (const notice of report.notices) console.log(` [notice] ${notice}`);
@@ -3871,7 +4241,7 @@ async function maybeHandoff(config, reportPath, options, rl) {
3871
4241
  }
3872
4242
  async function auditCommand(options = {}) {
3873
4243
  await maybeOfferInit({ json: options.json, yes: options.yes, suppressAuditOffer: true });
3874
- if (!existsSync15(getConfigPath())) {
4244
+ if (!existsSync16(getConfigPath())) {
3875
4245
  throw new UserError(
3876
4246
  "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
4247
  );
@@ -3887,7 +4257,9 @@ async function auditCommand(options = {}) {
3887
4257
  saveConfig(config);
3888
4258
  }
3889
4259
  }
3890
- const report = await auditConfiguredSet(config);
4260
+ const { records: accepted, notices: storeNotices } = readAcceptedFindings();
4261
+ const report = await auditConfiguredSet(config, { accepted });
4262
+ if (storeNotices.length > 0) report.notices = [...report.notices, ...storeNotices];
3891
4263
  const reportPath = resolveReportPath(options.report);
3892
4264
  writeNewFile(reportPath, renderAuditMarkdown(report), "--report");
3893
4265
  try {
@@ -4214,7 +4586,7 @@ async function init(options = {}, deps = {}) {
4214
4586
  }
4215
4587
  async function maybeOfferInit(options = {}) {
4216
4588
  if (options.json || options.yes || !process.stdin.isTTY) return;
4217
- if (existsSync16(getConfigPath())) return;
4589
+ if (existsSync17(getConfigPath())) return;
4218
4590
  console.log(
4219
4591
  "[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
4592
  );
@@ -4263,7 +4635,8 @@ async function add(source, options = {}) {
4263
4635
  const { safety, gateResult, proEnabled } = await runGate(entry.skillDirs[0], targetRoot, {
4264
4636
  proFeatureLabel: PRO_FEATURE_LABEL,
4265
4637
  strictness: config.strictness,
4266
- artifact
4638
+ artifact,
4639
+ skillMapPath: options.skillMap
4267
4640
  });
4268
4641
  const { profile, overlap } = gateResult;
4269
4642
  if (profile.kind !== "skill") {
@@ -4364,7 +4737,7 @@ async function addMcp(source, config, options) {
4364
4737
  envVars: detail.env ? Object.keys(detail.env) : void 0,
4365
4738
  candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
4366
4739
  };
4367
- const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join20(process.cwd(), ".mcp.json");
4740
+ const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join21(process.cwd(), ".mcp.json");
4368
4741
  const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
4369
4742
  reject(entry);
4370
4743
  logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
@@ -4442,7 +4815,8 @@ async function scan(source, options = {}) {
4442
4815
  const { safety, gateResult } = await runGate(candidatePath, skillsRoot ?? "", {
4443
4816
  strictness: config.strictness,
4444
4817
  artifact,
4445
- mcpTargets: config.mcpTargets ?? []
4818
+ mcpTargets: config.mcpTargets ?? [],
4819
+ skillMapPath: options.skillMap
4446
4820
  });
4447
4821
  if (artifact === "mcp" && gateResult.profile.kind === "mcp" && gateResult.profile.serverCount > 1) {
4448
4822
  console.error(
@@ -4459,7 +4833,7 @@ async function scan(source, options = {}) {
4459
4833
  }
4460
4834
 
4461
4835
  // src/commands/list.ts
4462
- import { join as join21 } from "path";
4836
+ import { join as join22 } from "path";
4463
4837
  async function list() {
4464
4838
  await maybeOfferInit();
4465
4839
  const config = loadConfig();
@@ -4472,14 +4846,14 @@ async function list() {
4472
4846
  }
4473
4847
  console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
4474
4848
  for (const id of ids) {
4475
- const skillDirs = findSkillDirs(join21(quarantineDir, id));
4849
+ const skillDirs = findSkillDirs(join22(quarantineDir, id));
4476
4850
  if (skillDirs.length === 0) {
4477
4851
  console.log(` ${id} (no SKILL.md found)`);
4478
4852
  continue;
4479
4853
  }
4480
4854
  const names = skillDirs.map((dir) => {
4481
4855
  try {
4482
- return readFrontmatter(join21(dir, "SKILL.md")).name;
4856
+ return readFrontmatter(join22(dir, "SKILL.md")).name;
4483
4857
  } catch {
4484
4858
  return dir;
4485
4859
  }
@@ -4489,18 +4863,18 @@ async function list() {
4489
4863
  }
4490
4864
 
4491
4865
  // src/commands/guide.ts
4492
- import { existsSync as existsSync18 } from "fs";
4866
+ import { existsSync as existsSync19 } from "fs";
4493
4867
 
4494
4868
  // 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";
4869
+ import { existsSync as existsSync18, realpathSync as realpathSync3 } from "fs";
4870
+ import { basename as basename9, join as join23, resolve as resolvePath4, sep as sep2 } from "path";
4497
4871
  var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
4498
4872
  function resolveHeldPath(quarantineDir, id) {
4499
4873
  if (id === "" || id !== basename9(id) || id === "." || id === "..") {
4500
4874
  throw new UserError(`invalid quarantine id "${id}" \u2014 pass a single id exactly as printed by \`skill-forge list\``);
4501
4875
  }
4502
- const candidate = join22(quarantineDir, id);
4503
- if (!existsSync17(candidate)) {
4876
+ const candidate = join23(quarantineDir, id);
4877
+ if (!existsSync18(candidate)) {
4504
4878
  throw new UserError(`no held entry with id "${id}" \u2014 run \`skill-forge list\` to see what is in quarantine`);
4505
4879
  }
4506
4880
  const realRoot = realpathSync3(resolvePath4(quarantineDir));
@@ -4528,7 +4902,7 @@ function resolveTargetRoot(config, target) {
4528
4902
  function heldSkillNames(skillDirs) {
4529
4903
  return skillDirs.map((dir) => {
4530
4904
  try {
4531
- return readFrontmatter(join22(dir, "SKILL.md")).name;
4905
+ return readFrontmatter(join23(dir, "SKILL.md")).name;
4532
4906
  } catch {
4533
4907
  return basename9(dir);
4534
4908
  }
@@ -4536,7 +4910,7 @@ function heldSkillNames(skillDirs) {
4536
4910
  }
4537
4911
  function looksLikeMcpSandbox(quarantinePath) {
4538
4912
  const candidate = resolveMcpCandidatePath({ quarantinePath });
4539
- return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync17(join22(candidate, f)));
4913
+ return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync18(join23(candidate, f)));
4540
4914
  }
4541
4915
  async function promoteHeldMcp(id, entry, config, options) {
4542
4916
  const candidatePath = resolveMcpCandidatePath(entry);
@@ -4570,7 +4944,7 @@ async function promoteHeldMcp(id, entry, config, options) {
4570
4944
  envVars: detail.env ? Object.keys(detail.env) : void 0,
4571
4945
  candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
4572
4946
  };
4573
- const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join22(process.cwd(), ".mcp.json");
4947
+ const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join23(process.cwd(), ".mcp.json");
4574
4948
  const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
4575
4949
  reject(entry);
4576
4950
  logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
@@ -4815,7 +5189,7 @@ var TOPICS = [
4815
5189
  ];
4816
5190
  function readState() {
4817
5191
  const configPath = getConfigPath();
4818
- const configured = existsSync18(configPath);
5192
+ const configured = existsSync19(configPath);
4819
5193
  const base = {
4820
5194
  configured,
4821
5195
  configPath,
@@ -5044,18 +5418,18 @@ async function status() {
5044
5418
 
5045
5419
  // src/evolve.ts
5046
5420
  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";
5421
+ import { createHash as createHash3 } from "crypto";
5422
+ import { cpSync as cpSync3, existsSync as existsSync20, lstatSync as lstatSync3, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, rmSync as rmSync6 } from "fs";
5049
5423
  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";
5424
+ import { basename as basename10, dirname as dirname9, join as join24, resolve as resolvePath5, sep as sep3 } from "path";
5051
5425
  var SKILLOPT_BIN = "skillopt-sleep";
5052
5426
  var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
5053
5427
  async function evolve(targetSkillDir, options = {}, deps = {}) {
5054
5428
  const exec = deps.exec ?? defaultExec;
5055
5429
  const json = options.json;
5056
5430
  const resolvedTargetDir = resolvePath5(targetSkillDir);
5057
- const skillMdPath = join23(resolvedTargetDir, "SKILL.md");
5058
- if (!existsSync19(skillMdPath)) {
5431
+ const skillMdPath = join24(resolvedTargetDir, "SKILL.md");
5432
+ if (!existsSync20(skillMdPath)) {
5059
5433
  throw new UserError(`no SKILL.md found at ${skillMdPath} \u2014 evolve requires an existing, installed skill directory`);
5060
5434
  }
5061
5435
  const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
@@ -5113,33 +5487,33 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
5113
5487
  }
5114
5488
  return;
5115
5489
  }
5116
- const stagingRoot = join23(project, ".skillopt-sleep", "staging");
5490
+ const stagingRoot = join24(project, ".skillopt-sleep", "staging");
5117
5491
  const stagingDir = resolvePath5(parsed.staging_dir);
5118
5492
  if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep3)) {
5119
5493
  throw new UserError(
5120
5494
  `skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
5121
5495
  );
5122
5496
  }
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)) {
5497
+ const proposedSkillMd = join24(stagingDir, "proposed_SKILL.md");
5498
+ const proposedClaudeMd = join24(stagingDir, "proposed_CLAUDE.md");
5499
+ const reportMdPath = join24(stagingDir, "report.md");
5500
+ if (!existsSync20(proposedSkillMd)) {
5127
5501
  throw new UserError(`skillopt-sleep reported an accepted proposal but ${proposedSkillMd} is missing`);
5128
5502
  }
5129
- if (existsSync19(reportMdPath)) {
5503
+ if (existsSync20(reportMdPath)) {
5130
5504
  notices.push(`SkillOpt-Sleep's full report: ${reportMdPath}`);
5131
5505
  }
5132
5506
  assertNotSymlink(proposedSkillMd, "proposed_SKILL.md");
5133
5507
  assertNotSymlink(proposedClaudeMd, "proposed_CLAUDE.md");
5134
5508
  const scannedSkillHash = sha256File(proposedSkillMd);
5135
5509
  const scannedClaudeHash = sha256File(proposedClaudeMd);
5136
- const tempDir = mkdtempSync3(join23(tmpdir2(), "skill-forge-evolve-"));
5510
+ const tempDir = mkdtempSync3(join24(tmpdir2(), "skill-forge-evolve-"));
5137
5511
  let profile;
5138
5512
  let safety;
5139
5513
  try {
5140
- cpSync3(proposedSkillMd, join23(tempDir, "SKILL.md"));
5141
- if (existsSync19(proposedClaudeMd)) {
5142
- cpSync3(proposedClaudeMd, join23(tempDir, "CLAUDE.md"));
5514
+ cpSync3(proposedSkillMd, join24(tempDir, "SKILL.md"));
5515
+ if (existsSync20(proposedClaudeMd)) {
5516
+ cpSync3(proposedClaudeMd, join24(tempDir, "CLAUDE.md"));
5143
5517
  }
5144
5518
  profile = asSkillProfile(profileSkill(tempDir), "for the evolved proposal");
5145
5519
  const strictness = loadConfig().strictness;
@@ -5238,7 +5612,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
5238
5612
  if (safetyVerdict === "block") process.exitCode = 1;
5239
5613
  } else {
5240
5614
  logOutcome(`[skill-forge] held \u2014 staging dir left in place -> ${stagingDir}`, json);
5241
- if (existsSync19(reportMdPath)) {
5615
+ if (existsSync20(reportMdPath)) {
5242
5616
  logOutcome(`[skill-forge] see ${reportMdPath} for the full evidence report`, json);
5243
5617
  }
5244
5618
  }
@@ -5280,10 +5654,10 @@ function validateRunOutput(raw) {
5280
5654
  return { accepted: Boolean(obj.accepted), staging_dir: stagingDirRaw ?? null };
5281
5655
  }
5282
5656
  function readManifest(stagingDir) {
5283
- const manifestPath = join23(stagingDir, "manifest.json");
5657
+ const manifestPath = join24(stagingDir, "manifest.json");
5284
5658
  let raw;
5285
5659
  try {
5286
- raw = readFileSync14(manifestPath, "utf-8");
5660
+ raw = readFileSync16(manifestPath, "utf-8");
5287
5661
  } catch (err) {
5288
5662
  throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
5289
5663
  }
@@ -5317,18 +5691,18 @@ function validateManifest(raw, manifestPath) {
5317
5691
  };
5318
5692
  }
5319
5693
  function filesIdentical(a, b) {
5320
- if (!existsSync19(a) || !existsSync19(b)) return false;
5321
- return readFileSync14(a).equals(readFileSync14(b));
5694
+ if (!existsSync20(a) || !existsSync20(b)) return false;
5695
+ return readFileSync16(a).equals(readFileSync16(b));
5322
5696
  }
5323
5697
  function isAlreadyAdopted(stagingDir, manifest) {
5324
- const proposedSkillMd = join23(stagingDir, "proposed_SKILL.md");
5325
- const proposedClaudeMd = join23(stagingDir, "proposed_CLAUDE.md");
5698
+ const proposedSkillMd = join24(stagingDir, "proposed_SKILL.md");
5699
+ const proposedClaudeMd = join24(stagingDir, "proposed_CLAUDE.md");
5326
5700
  let sawComparison = false;
5327
- if (existsSync19(proposedSkillMd)) {
5701
+ if (existsSync20(proposedSkillMd)) {
5328
5702
  sawComparison = true;
5329
5703
  if (!filesIdentical(proposedSkillMd, manifest.live_skill_path)) return false;
5330
5704
  }
5331
- if (existsSync19(proposedClaudeMd) && manifest.live_memory_path) {
5705
+ if (existsSync20(proposedClaudeMd) && manifest.live_memory_path) {
5332
5706
  sawComparison = true;
5333
5707
  if (!filesIdentical(proposedClaudeMd, manifest.live_memory_path)) return false;
5334
5708
  }
@@ -5346,8 +5720,8 @@ function assertNotSymlink(path, label) {
5346
5720
  }
5347
5721
  }
5348
5722
  function sha256File(path) {
5349
- if (!existsSync19(path)) return null;
5350
- return createHash2("sha256").update(readFileSync14(path)).digest("hex");
5723
+ if (!existsSync20(path)) return null;
5724
+ return createHash3("sha256").update(readFileSync16(path)).digest("hex");
5351
5725
  }
5352
5726
 
5353
5727
  // src/commands/evolve.ts
@@ -5355,12 +5729,118 @@ async function evolveCommand(targetSkillDir, options = {}) {
5355
5729
  await evolve(targetSkillDir, options);
5356
5730
  }
5357
5731
 
5732
+ // src/commands/finding.ts
5733
+ import { existsSync as existsSync21 } from "fs";
5734
+ function requireSavedConfig() {
5735
+ if (!existsSync21(getConfigPath())) {
5736
+ throw new UserError(
5737
+ "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)."
5738
+ );
5739
+ }
5740
+ }
5741
+ function shortDate(iso) {
5742
+ return iso.slice(0, 10) || iso;
5743
+ }
5744
+ async function findingAcceptCommand(prefix, options = {}) {
5745
+ requireSavedConfig();
5746
+ if (prefix.length < MIN_FINGERPRINT_PREFIX_LENGTH) {
5747
+ throw new UserError(`prefix too short \u2014 use at least ${MIN_FINGERPRINT_PREFIX_LENGTH} hex chars (got "${prefix}")`);
5748
+ }
5749
+ const reason = options.reason;
5750
+ if (reason === void 0) {
5751
+ throw new UserError("--reason is required \u2014 a short note on why this finding is accepted");
5752
+ }
5753
+ validateAcceptReason(reason);
5754
+ const config = loadConfig();
5755
+ const { records: accepted } = readAcceptedFindings();
5756
+ const report = await auditConfiguredSet(config, { accepted });
5757
+ const activeMatches = resolveAmongFindings(prefix, report.hygiene);
5758
+ if (activeMatches.length > 1) {
5759
+ const list2 = activeMatches.map((f) => `${f.rule} @ ${f.target} (${f.fingerprint})`).join("; ");
5760
+ throw new UserError(`prefix "${prefix}" matches ${activeMatches.length} current findings \u2014 use a longer prefix: ${list2}`);
5761
+ }
5762
+ if (activeMatches.length === 0) {
5763
+ const alreadyMatches = resolveAmongRecords(prefix, accepted);
5764
+ if (alreadyMatches.length > 0) {
5765
+ const already = alreadyMatches[0];
5766
+ throw new UserError(`already accepted on ${shortDate(already.acceptedAt)}: ${already.reason}`);
5767
+ }
5768
+ throw new UserError(`no current finding matches "${prefix}" \u2014 run \`skill-forge audit\` and copy a fingerprint from the report`);
5769
+ }
5770
+ const finding2 = activeMatches[0];
5771
+ if (finding2.severity === "HIGH" || finding2.severity === "CRITICAL") {
5772
+ throw new UserError(
5773
+ `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.`
5774
+ );
5775
+ }
5776
+ const record = {
5777
+ fingerprint: finding2.fingerprint,
5778
+ severity: finding2.severity,
5779
+ rule: finding2.rule,
5780
+ target: finding2.target,
5781
+ context: finding2.context ?? finding2.detail,
5782
+ detail: finding2.detail,
5783
+ reason,
5784
+ acceptedAt: (/* @__PURE__ */ new Date()).toISOString(),
5785
+ source: "user"
5786
+ };
5787
+ writeAcceptedFindings([...accepted, record]);
5788
+ if (options.json) {
5789
+ console.log(JSON.stringify(record, null, 2));
5790
+ return;
5791
+ }
5792
+ console.log(`[skill-forge] accepted [${record.severity}] ${record.rule} \u2014 ${record.target}`);
5793
+ console.log(` fingerprint: ${record.fingerprint}`);
5794
+ console.log(` reason: ${record.reason}`);
5795
+ console.log(' this finding is excluded from active counts, but stays permanently visible under "Accepted findings" in every future report.');
5796
+ }
5797
+ async function findingRevokeCommand(prefix, options = {}) {
5798
+ if (prefix.length < MIN_FINGERPRINT_PREFIX_LENGTH) {
5799
+ throw new UserError(`prefix too short \u2014 use at least ${MIN_FINGERPRINT_PREFIX_LENGTH} hex chars (got "${prefix}")`);
5800
+ }
5801
+ const { records } = readAcceptedFindings();
5802
+ const matches = resolveAmongRecords(prefix, records);
5803
+ if (matches.length > 1) {
5804
+ const list2 = matches.map((r) => `${r.rule} @ ${r.target} (${r.fingerprint})`).join("; ");
5805
+ throw new UserError(`prefix "${prefix}" matches ${matches.length} accepted findings \u2014 use a longer prefix: ${list2}`);
5806
+ }
5807
+ if (matches.length === 0) {
5808
+ throw new UserError(`no accepted finding matches "${prefix}" \u2014 run \`skill-forge finding list\` to see what is accepted`);
5809
+ }
5810
+ const record = matches[0];
5811
+ writeAcceptedFindings(records.filter((r) => r.fingerprint !== record.fingerprint));
5812
+ if (options.json) {
5813
+ console.log(JSON.stringify(record, null, 2));
5814
+ return;
5815
+ }
5816
+ console.log(`[skill-forge] revoked [${record.severity}] ${record.rule} \u2014 ${record.target}`);
5817
+ }
5818
+ async function findingListCommand(options = {}) {
5819
+ const { records, notices } = readAcceptedFindings();
5820
+ for (const notice of notices) console.error(`[skill-forge] ${notice}`);
5821
+ const rows = options.stale ? records.filter((r) => !existsSync21(r.target)) : records;
5822
+ if (options.json) {
5823
+ console.log(JSON.stringify(rows, null, 2));
5824
+ return;
5825
+ }
5826
+ if (rows.length === 0) {
5827
+ console.log(options.stale ? "[skill-forge] no stale accepted findings." : "[skill-forge] no accepted findings.");
5828
+ return;
5829
+ }
5830
+ console.log(`[skill-forge] accepted findings${options.stale ? " (stale \u2014 target no longer exists)" : ""}:`);
5831
+ for (const r of rows) {
5832
+ console.log(
5833
+ ` [${r.severity}] ${sanitizeForMarkdown(r.rule)} \u2014 ${sanitizeForMarkdown(r.target)}: ${sanitizeForMarkdown(r.reason)} (fp ${r.fingerprint.slice(0, FINGERPRINT_DISPLAY_LENGTH)}, accepted ${shortDate(r.acceptedAt)})`
5834
+ );
5835
+ }
5836
+ }
5837
+
5358
5838
  // src/commands/organize.ts
5359
5839
  import { resolve as resolvePath7 } from "path";
5360
5840
 
5361
5841
  // 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";
5842
+ import { existsSync as existsSync22, readFileSync as readFileSync17, readdirSync as readdirSync8, realpathSync as realpathSync4, statSync as statSync9 } from "fs";
5843
+ import { basename as basename11, join as join25, resolve as resolvePath6 } from "path";
5364
5844
  var PRO_FEATURE_LABEL4 = "set-level organizer reports";
5365
5845
  var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
5366
5846
  var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated"]);
@@ -5373,7 +5853,7 @@ function listSubdirs2(dir) {
5373
5853
  }
5374
5854
  const out = [];
5375
5855
  for (const entry of entries) {
5376
- const full = join24(dir, entry.name);
5856
+ const full = join25(dir, entry.name);
5377
5857
  if (entry.isDirectory()) {
5378
5858
  out.push(full);
5379
5859
  continue;
@@ -5391,14 +5871,14 @@ function findSkillDirsUnderRoot2(root) {
5391
5871
  const found = /* @__PURE__ */ new Set();
5392
5872
  const topDirs = listSubdirs2(root);
5393
5873
  for (const dir of topDirs) {
5394
- if (existsSync20(join24(dir, "SKILL.md"))) found.add(dir);
5874
+ if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
5395
5875
  }
5396
- for (const dir of listSubdirs2(join24(root, "skills"))) {
5397
- if (existsSync20(join24(dir, "SKILL.md"))) found.add(dir);
5876
+ for (const dir of listSubdirs2(join25(root, "skills"))) {
5877
+ if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
5398
5878
  }
5399
5879
  for (const topDir of topDirs) {
5400
- for (const dir of listSubdirs2(join24(topDir, "skills"))) {
5401
- if (existsSync20(join24(dir, "SKILL.md"))) found.add(dir);
5880
+ for (const dir of listSubdirs2(join25(topDir, "skills"))) {
5881
+ if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
5402
5882
  }
5403
5883
  }
5404
5884
  return Array.from(found).filter((p) => !p.includes("/.git/"));
@@ -5467,7 +5947,7 @@ function loadUsageSnapshot(snapshotPath) {
5467
5947
  const resolvedPath = resolvePath6(expandHome(snapshotPath));
5468
5948
  let raw;
5469
5949
  try {
5470
- raw = readFileSync15(resolvedPath, "utf-8");
5950
+ raw = readFileSync17(resolvedPath, "utf-8");
5471
5951
  } catch (err) {
5472
5952
  return { usage: /* @__PURE__ */ new Map(), warning: `could not read usage snapshot (${err.message}); proceeding without usage` };
5473
5953
  }
@@ -5573,10 +6053,10 @@ function buildRegistryEntries(candidates, warnings) {
5573
6053
  }
5574
6054
  if (seenRealPaths.has(realPath)) continue;
5575
6055
  seenRealPaths.add(realPath);
5576
- const skillMdPath = join24(realPath, "SKILL.md");
6056
+ const skillMdPath = join25(realPath, "SKILL.md");
5577
6057
  let text;
5578
6058
  try {
5579
- text = readFileSync15(skillMdPath, "utf-8");
6059
+ text = readFileSync17(skillMdPath, "utf-8");
5580
6060
  } catch (err) {
5581
6061
  warnings.push(`could not read ${skillMdPath}: ${err.message}`);
5582
6062
  continue;
@@ -5982,7 +6462,162 @@ async function find(query, options = {}, deps = {}) {
5982
6462
 
5983
6463
  // src/commands/watch.ts
5984
6464
  import { realpathSync as realpathSync5 } from "fs";
5985
- import { join as join25, resolve as resolvePath8 } from "path";
6465
+ import { join as join26, resolve as resolvePath8 } from "path";
6466
+
6467
+ // src/gate/skillMapDrift.ts
6468
+ import { createHash as createHash4 } from "crypto";
6469
+ import { readFileSync as readFileSync18, existsSync as existsSync23 } from "fs";
6470
+ import { dirname as dirname10, isAbsolute as isAbsolute2, resolve as resolve7 } from "path";
6471
+ function sha256(text) {
6472
+ return createHash4("sha256").update(text, "utf-8").digest("hex");
6473
+ }
6474
+ function shortHash(hash) {
6475
+ return hash.slice(0, 12);
6476
+ }
6477
+ var ACTIONABLE_STATUSES = /* @__PURE__ */ new Set([
6478
+ "upstream-moved",
6479
+ "diverged",
6480
+ "drifted",
6481
+ "upstream-unreachable",
6482
+ "local-missing"
6483
+ ]);
6484
+ function isActionable(status2) {
6485
+ return ACTIONABLE_STATUSES.has(status2);
6486
+ }
6487
+ function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
6488
+ const localMatchesBaseline = localNormalized === baselineHash;
6489
+ const upstreamMatchesBaseline = upstreamHash === baselineHash;
6490
+ const hashSuffix = `baseline=${shortHash(baselineHash)}\u2026, local-normalized=${shortHash(localNormalized)}\u2026, upstream=${shortHash(upstreamHash)}\u2026`;
6491
+ const TABLE = {
6492
+ TT: { status: "in-sync", detail: `local and upstream both match the recorded baseline (${hashSuffix})` },
6493
+ TF: { status: "upstream-moved", detail: `only upstream changed since baseline \u2014 review for adoption (${hashSuffix})` },
6494
+ FT: { status: "local-only", detail: `only local content changed since baseline \u2014 deliberate fork divergence (${hashSuffix})` },
6495
+ FF: { status: "diverged", detail: `both local and upstream changed since baseline \u2014 review for adoption (${hashSuffix})` }
6496
+ };
6497
+ const key = `${localMatchesBaseline ? "T" : "F"}${upstreamMatchesBaseline ? "T" : "F"}`;
6498
+ const { status: status2, detail } = TABLE[key];
6499
+ return { status: status2, detail, actionable: isActionable(status2) };
6500
+ }
6501
+ function resolveLocalPath(path, mapDir) {
6502
+ const expanded = expandHome(path);
6503
+ if (isAbsolute2(expanded)) return resolve7(expanded);
6504
+ const candidates = [resolve7(expanded)];
6505
+ if (mapDir) {
6506
+ candidates.push(resolve7(mapDir, expanded));
6507
+ candidates.push(resolve7(mapDir, "..", expanded));
6508
+ }
6509
+ for (const candidate of candidates) {
6510
+ if (existsSync23(candidate)) return candidate;
6511
+ }
6512
+ return candidates[0];
6513
+ }
6514
+ function defaultReadLocalFile(path, mapDir) {
6515
+ const resolved = resolveLocalPath(path, mapDir);
6516
+ if (!existsSync23(resolved)) return void 0;
6517
+ try {
6518
+ return readFileSync18(resolved, "utf-8");
6519
+ } catch {
6520
+ return void 0;
6521
+ }
6522
+ }
6523
+ async function defaultFetchUpstream(url) {
6524
+ try {
6525
+ const res = await fetch(url);
6526
+ if (!res.ok) return void 0;
6527
+ return await res.text();
6528
+ } catch {
6529
+ return void 0;
6530
+ }
6531
+ }
6532
+ function isHttpUrl(value) {
6533
+ return /^https?:\/\//i.test(value.trim());
6534
+ }
6535
+ function upstreamLabel(node, edge) {
6536
+ return node?.url ?? node?.path ?? node?.name ?? node?.id ?? edge.to;
6537
+ }
6538
+ async function computeMapDrift(map, deps = {}) {
6539
+ const mapDir = deps.mapPath ? dirname10(resolve7(expandHome(deps.mapPath))) : void 0;
6540
+ const readLocalFile = deps.readLocalFile ?? ((path) => defaultReadLocalFile(path, mapDir));
6541
+ const fetchUpstream = deps.fetchUpstream ?? defaultFetchUpstream;
6542
+ const rows = [];
6543
+ for (const edge of edgesOfType(map, "fork-of")) {
6544
+ const localNode = nodeById(map, edge.from);
6545
+ const upstreamNode = nodeById(map, edge.to);
6546
+ const skillLabel = localNode?.name ?? localNode?.id ?? edge.from;
6547
+ const upstream = upstreamLabel(upstreamNode, edge);
6548
+ if (!localNode || !localNode.path || !localNode.contentHash) {
6549
+ rows.push({
6550
+ skill: skillLabel,
6551
+ upstream,
6552
+ status: "local-missing",
6553
+ detail: !localNode ? `fork-of edge "from" node not found in map: ${edge.from}` : 'local skill node missing "path" or "contentHash" \u2014 cannot compare',
6554
+ actionable: isActionable("local-missing")
6555
+ });
6556
+ continue;
6557
+ }
6558
+ const localResolved = resolveLocalPath(localNode.path, mapDir);
6559
+ if (!existsSync23(localResolved)) {
6560
+ rows.push({
6561
+ skill: skillLabel,
6562
+ upstream,
6563
+ status: "local-missing",
6564
+ detail: `local path does not exist: ${localResolved}`,
6565
+ actionable: isActionable("local-missing")
6566
+ });
6567
+ continue;
6568
+ }
6569
+ const upstreamLocation = upstreamNode?.url ?? upstreamNode?.path;
6570
+ if (!upstreamLocation) {
6571
+ rows.push({
6572
+ skill: skillLabel,
6573
+ upstream,
6574
+ status: "upstream-unreachable",
6575
+ detail: 'upstream node has no "url" or "path" to fetch/read',
6576
+ actionable: isActionable("upstream-unreachable")
6577
+ });
6578
+ continue;
6579
+ }
6580
+ const upstreamText = isHttpUrl(upstreamLocation) ? await fetchUpstream(upstreamLocation) : readLocalFile(upstreamLocation);
6581
+ if (upstreamText === void 0) {
6582
+ rows.push({
6583
+ skill: skillLabel,
6584
+ upstream,
6585
+ status: "upstream-unreachable",
6586
+ detail: `could not fetch/read upstream: ${upstreamLocation}`,
6587
+ actionable: isActionable("upstream-unreachable")
6588
+ });
6589
+ continue;
6590
+ }
6591
+ const upstreamHash = sha256(upstreamText);
6592
+ const baselineHash = upstreamNode?.baselineHash;
6593
+ const localNormalized = localNode.contentHashNormalized;
6594
+ if (baselineHash && localNormalized) {
6595
+ const { status: status2, detail, actionable } = threeWayVerdict(localNormalized, upstreamHash, baselineHash);
6596
+ rows.push({ skill: skillLabel, upstream, status: status2, detail, actionable, baselineHash, upstreamHash });
6597
+ continue;
6598
+ }
6599
+ if (upstreamHash === localNode.contentHash) {
6600
+ rows.push({
6601
+ skill: skillLabel,
6602
+ upstream,
6603
+ status: "in-sync",
6604
+ detail: `contentHash matches (${shortHash(upstreamHash)}\u2026)`,
6605
+ actionable: isActionable("in-sync")
6606
+ });
6607
+ } else {
6608
+ rows.push({
6609
+ skill: skillLabel,
6610
+ upstream,
6611
+ status: "drifted",
6612
+ detail: `recorded ${shortHash(localNode.contentHash)}\u2026, upstream now hashes to ${shortHash(upstreamHash)}\u2026`,
6613
+ actionable: isActionable("drifted")
6614
+ });
6615
+ }
6616
+ }
6617
+ return rows;
6618
+ }
6619
+
6620
+ // src/commands/watch.ts
5986
6621
  var PRO_FEATURE_LABEL5 = "provenance drift watch (watch)";
5987
6622
  function isGitUrl(source) {
5988
6623
  const s = source.trim();
@@ -6018,7 +6653,7 @@ async function checkGitDrift(exec, url, ref) {
6018
6653
  async function runWatch(config, options = {}, deps = {}) {
6019
6654
  const proStatus = proFeatureStatus(PRO_FEATURE_LABEL5);
6020
6655
  if (!proStatus.enabled) {
6021
- return { roots: [], rows: [], warnings: [], notices: [proStatus.notice ?? "Pro feature (watch) locked."] };
6656
+ return { roots: [], rows: [], warnings: [], notices: [proStatus.notice ?? "Pro feature (watch) locked."], mapDrift: [] };
6022
6657
  }
6023
6658
  const notices = proStatus.notice ? [proStatus.notice] : [];
6024
6659
  const exec = deps.exec ?? defaultExec;
@@ -6044,7 +6679,7 @@ async function runWatch(config, options = {}, deps = {}) {
6044
6679
  ledger = readProvenanceLedger(resolvedRoot);
6045
6680
  } catch (err) {
6046
6681
  const message = err instanceof Error ? err.message : String(err);
6047
- const ledgerPath = join25(resolvedRoot, "SOURCES.md");
6682
+ const ledgerPath = join26(resolvedRoot, "SOURCES.md");
6048
6683
  roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
6049
6684
  warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
6050
6685
  continue;
@@ -6084,7 +6719,19 @@ async function runWatch(config, options = {}, deps = {}) {
6084
6719
  rows.push({ ...rowBase, checkMethod: "git-ls-remote", driftStatus: status2, detail });
6085
6720
  }
6086
6721
  }
6087
- return { roots, rows, warnings, notices };
6722
+ let mapDrift = [];
6723
+ if (options.skillMap) {
6724
+ try {
6725
+ const map = loadSkillMap(options.skillMap);
6726
+ mapDrift = await computeMapDrift(map, {
6727
+ mapPath: options.skillMap,
6728
+ ...options.offline ? { fetchUpstream: async () => void 0 } : {}
6729
+ });
6730
+ } catch (err) {
6731
+ warnings.push(`skill map at ${options.skillMap}: ${err.message}`);
6732
+ }
6733
+ }
6734
+ return { roots, rows, warnings, notices, mapDrift };
6088
6735
  }
6089
6736
  function printWatchTerminal(report) {
6090
6737
  console.log("[skill-forge] watch \u2014 provenance drift check");
@@ -6122,6 +6769,13 @@ ${sanitizeForMarkdown(root.root)} (${sanitizeForMarkdown(root.realPath)})`);
6122
6769
  console.log("\nWarnings:");
6123
6770
  for (const w of report.warnings) console.log(` - ${sanitizeForMarkdown(w)}`);
6124
6771
  }
6772
+ if (report.mapDrift.length > 0) {
6773
+ console.log("\nSkill-map fork-of drift:");
6774
+ for (const row of report.mapDrift) {
6775
+ console.log(` \u2022 ${sanitizeForMarkdown(row.skill)} <- ${sanitizeForMarkdown(row.upstream)}`);
6776
+ console.log(` ${row.status} \u2014 ${sanitizeForMarkdown(row.detail)}`);
6777
+ }
6778
+ }
6125
6779
  console.log("\nCompare each stored ref to current upstream; re-run ingestion on any that moved.");
6126
6780
  }
6127
6781
  async function watchCommand(options = {}) {
@@ -6135,8 +6789,8 @@ async function watchCommand(options = {}) {
6135
6789
  }
6136
6790
 
6137
6791
  // src/commands/routine.ts
6138
- import { existsSync as existsSync21, rmSync as rmSync7, statSync as statSync10 } from "fs";
6139
- import { join as join26 } from "path";
6792
+ import { existsSync as existsSync24, rmSync as rmSync7, statSync as statSync10 } from "fs";
6793
+ import { join as join27 } from "path";
6140
6794
  var PRO_FEATURE_LABEL6 = "the scheduled maintenance routine";
6141
6795
  var SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
6142
6796
  function parseFailOnFloor(failOn) {
@@ -6157,7 +6811,7 @@ function exceedsFloor(audit2, failOn) {
6157
6811
  }
6158
6812
  function resolveReportPath2(override, generatedAt) {
6159
6813
  if (override) return override;
6160
- return join26(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
6814
+ return join27(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
6161
6815
  }
6162
6816
  function runHousekeeping(config, options) {
6163
6817
  const actions = [];
@@ -6166,7 +6820,7 @@ function runHousekeeping(config, options) {
6166
6820
  const pruneDays = options.pruneDays ?? 30;
6167
6821
  const cutoff = Date.now() - pruneDays * 864e5;
6168
6822
  for (const id of listQuarantineIds(config.quarantineDir)) {
6169
- const path = join26(config.quarantineDir, id);
6823
+ const path = join27(config.quarantineDir, id);
6170
6824
  let mtime;
6171
6825
  try {
6172
6826
  mtime = statSync10(path).mtimeMs;
@@ -6204,7 +6858,7 @@ function runHousekeeping(config, options) {
6204
6858
  }
6205
6859
  for (const entry of entries) {
6206
6860
  if (entry.status !== "pending") continue;
6207
- if (!entry.installedPath || existsSync21(entry.installedPath)) continue;
6861
+ if (!entry.installedPath || existsSync24(entry.installedPath)) continue;
6208
6862
  if (apply) {
6209
6863
  try {
6210
6864
  updateQueueEntry(entry.id, { status: "dismissed" });
@@ -6237,6 +6891,7 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
6237
6891
  "",
6238
6892
  `- Skills: ${result.audit.skillCount} across ${audit2.summary.rootCount} configured root(s)`,
6239
6893
  `- 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`,
6894
+ `- Accepted: ${audit2.summary.acceptedCount} (suppressed from the counts above; see Accepted findings)`,
6240
6895
  `- Overlap clusters: ${result.audit.overlapClusters.length}${result.audit.overlapLocked ? ' (overlap analysis was locked \u2014 not "none found")' : ""}`,
6241
6896
  `- Pending queue entries: ${result.pending.queue}`,
6242
6897
  `- Held in quarantine: ${result.pending.quarantine}`
@@ -6247,6 +6902,10 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
6247
6902
  if (organize) {
6248
6903
  lines.push(`- Capability registry: ${result.organize?.skills ?? 0} skill(s) across ${result.organize?.roots ?? 0} root(s)`);
6249
6904
  }
6905
+ if (result.notices.length > 0) {
6906
+ lines.push("", "## Notices", "");
6907
+ for (const item of result.notices) lines.push(`- ${sanitizeForMarkdown(item)}`);
6908
+ }
6250
6909
  if (result.degraded.length > 0) {
6251
6910
  lines.push("", "## Incomplete steps", "");
6252
6911
  for (const item of result.degraded) lines.push(`- ${sanitizeForMarkdown(item)}`);
@@ -6262,9 +6921,18 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
6262
6921
  }
6263
6922
  if (audit2.hygiene.length > 0) {
6264
6923
  lines.push("", "## Findings", "");
6265
- for (const finding of audit2.hygiene) {
6924
+ for (const finding2 of audit2.hygiene) {
6925
+ const fp = finding2.fingerprint ? finding2.fingerprint.slice(0, 12) : "unknown";
6266
6926
  lines.push(
6267
- `- **${finding.severity}** \`${sanitizeForMarkdown(finding.rule)}\` \u2014 ${sanitizeForMarkdown(finding.target)}: ${sanitizeForMarkdown(finding.detail)}`
6927
+ `- **${finding2.severity}** \`${sanitizeForMarkdown(finding2.rule)}\` \u2014 ${sanitizeForMarkdown(finding2.target)}: ${sanitizeForMarkdown(finding2.detail)} (fp ${fp})`
6928
+ );
6929
+ }
6930
+ }
6931
+ if (audit2.accepted.length > 0) {
6932
+ lines.push("", "## Accepted findings", "");
6933
+ for (const entry of audit2.accepted) {
6934
+ lines.push(
6935
+ `- **${entry.finding.severity}** \`${sanitizeForMarkdown(entry.finding.rule)}\` \u2014 ${sanitizeForMarkdown(entry.finding.target)}: ${sanitizeForMarkdown(entry.reason)} (accepted ${sanitizeForMarkdown(entry.acceptedAt.slice(0, 10))})`
6268
6936
  );
6269
6937
  }
6270
6938
  }
@@ -6294,7 +6962,9 @@ async function routineCommand(options = {}) {
6294
6962
  }
6295
6963
  const config = loadConfig();
6296
6964
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
6297
- const audit2 = await auditConfiguredSet(config);
6965
+ const { records: accepted, notices: storeNotices } = readAcceptedFindings();
6966
+ for (const notice of storeNotices) log(`[skill-forge] ${notice}`);
6967
+ const audit2 = await auditConfiguredSet(config, { accepted });
6298
6968
  const auditState = summarizeAudit(audit2);
6299
6969
  const degraded = [];
6300
6970
  let watch;
@@ -6337,6 +7007,7 @@ async function routineCommand(options = {}) {
6337
7007
  organize: organize ? { skills: organize.registry.count, roots: organize.registry.skillsRoots.length, notices: organize.notices } : void 0,
6338
7008
  pending: { queue: pending.count, quarantine: quarantine2.count },
6339
7009
  housekeeping,
7010
+ notices: storeNotices,
6340
7011
  degraded
6341
7012
  };
6342
7013
  const failure = degraded.length > 0 ? `${degraded.length} step(s) did not complete: ${degraded.join("; ")}` : exceedsFloor(auditState, options.failOn ?? "none");
@@ -6355,6 +7026,7 @@ async function routineCommand(options = {}) {
6355
7026
  console.log(
6356
7027
  ` findings: ${auditState.findings.CRITICAL ?? 0} critical / ${auditState.findings.HIGH ?? 0} high / ${auditState.findings.MEDIUM ?? 0} medium / ${auditState.findings.LOW ?? 0} low`
6357
7028
  );
7029
+ console.log(` accepted: ${auditState.acceptedCount ?? 0} (suppressed from counts; see report)`);
6358
7030
  if (result.watch) console.log(` drift: ${result.watch.moved} moved of ${result.watch.checked} tracked`);
6359
7031
  if (result.organize) console.log(` registry: ${result.organize.skills} skill(s)`);
6360
7032
  console.log(` pending: ${result.pending.queue} queue, ${result.pending.quarantine} quarantine`);
@@ -6379,7 +7051,7 @@ async function routineCommand(options = {}) {
6379
7051
 
6380
7052
  // src/commands/ingest.ts
6381
7053
  import { realpathSync as realpathSync6 } from "fs";
6382
- import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative2, resolve as resolvePath9 } from "path";
7054
+ import { dirname as dirname12, isAbsolute as isAbsolute3, relative as relative2, resolve as resolvePath9 } from "path";
6383
7055
  var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
6384
7056
  var VALID_STATUSES = ["ingested", "dismissed"];
6385
7057
  function requireProFeature() {
@@ -6427,12 +7099,12 @@ function realpathOrNull(p) {
6427
7099
  }
6428
7100
  function isUnderRoot(candidate, root) {
6429
7101
  const rel = relative2(root, candidate);
6430
- return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
7102
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
6431
7103
  }
6432
7104
  function validatePendingEntries(entries, config) {
6433
7105
  const quarantineRoot = realpathOrNull(resolvePath9(config.quarantineDir));
6434
7106
  const skillsRoots = config.skillsRoots.map((r) => realpathOrNull(resolvePath9(r))).filter((r) => r !== null);
6435
- const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname11(resolvePath9(f)))).filter((r) => r !== null);
7107
+ const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname12(resolvePath9(f)))).filter((r) => r !== null);
6436
7108
  const allowedRoots = [quarantineRoot, ...skillsRoots, ...mcpTargetDirs].filter((r) => r !== null);
6437
7109
  const valid = [];
6438
7110
  const excluded = [];
@@ -6504,14 +7176,14 @@ async function queueClose(id, options = {}) {
6504
7176
  }
6505
7177
 
6506
7178
  // 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";
7179
+ import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
7180
+ 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";
7181
+ import { dirname as dirname14, isAbsolute as isAbsolute5, join as join30, relative as relative4, resolve as resolvePath12, sep as sep4 } from "path";
6510
7182
  import { createInterface as createInterface6 } from "readline/promises";
6511
7183
 
6512
7184
  // 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";
7185
+ import { existsSync as existsSync26, realpathSync as realpathSync8, statSync as statSync12 } from "fs";
7186
+ import { isAbsolute as isAbsolute4, join as join29, relative as relative3, resolve as resolvePath11 } from "path";
6515
7187
 
6516
7188
  // src/refine/models.ts
6517
7189
  import { randomBytes } from "crypto";
@@ -6567,40 +7239,40 @@ function fingerprintKey(skill, target, overrideType, action) {
6567
7239
  return [normalize(skill), normalize(target), overrideType, action ?? ""].join("::");
6568
7240
  }
6569
7241
  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);
7242
+ const tokenize3 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
7243
+ const tokensA = tokenize3(a);
7244
+ const tokensB = tokenize3(b);
6573
7245
  if (tokensA.size === 0 || tokensB.size === 0) return 0;
6574
7246
  let intersectionSize = 0;
6575
7247
  for (const token of tokensA) {
6576
7248
  if (tokensB.has(token)) intersectionSize++;
6577
7249
  }
6578
- const unionSize2 = tokensA.size + tokensB.size - intersectionSize;
6579
- return unionSize2 === 0 ? 0 : intersectionSize / unionSize2;
7250
+ const unionSize3 = tokensA.size + tokensB.size - intersectionSize;
7251
+ return unionSize3 === 0 ? 0 : intersectionSize / unionSize3;
6580
7252
  }
6581
7253
 
6582
7254
  // 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";
7255
+ import { createHash as createHash5 } from "crypto";
7256
+ import { existsSync as existsSync25, readFileSync as readFileSync19, statSync as statSync11, realpathSync as realpathSync7 } from "fs";
7257
+ import { join as join28, resolve as resolvePath10 } from "path";
6586
7258
  var SIMILARITY_JOIN_THRESHOLD = 0.7;
6587
7259
  var READY_THRESHOLD = 2;
6588
7260
  function getRefinementsDir() {
6589
- return join27(getHomeDir(), "refinements");
7261
+ return join28(getHomeDir(), "refinements");
6590
7262
  }
6591
7263
  function getRefinementsPath() {
6592
- return join27(getRefinementsDir(), "refinements.json");
7264
+ return join28(getRefinementsDir(), "refinements.json");
6593
7265
  }
6594
7266
  function getPatternsPath() {
6595
- return join27(getRefinementsDir(), "patterns.json");
7267
+ return join28(getRefinementsDir(), "patterns.json");
6596
7268
  }
6597
7269
  function getBackupsDir() {
6598
- return join27(getRefinementsDir(), "backups");
7270
+ return join28(getRefinementsDir(), "backups");
6599
7271
  }
6600
7272
  function readJsonFile(path, label) {
6601
7273
  let raw;
6602
7274
  try {
6603
- raw = readFileSync16(path, "utf-8");
7275
+ raw = readFileSync19(path, "utf-8");
6604
7276
  } catch (err) {
6605
7277
  throw new UserError(`unable to read ${label} at ${path}: ${err.message}`);
6606
7278
  }
@@ -6611,7 +7283,7 @@ function readJsonFile(path, label) {
6611
7283
  }
6612
7284
  }
6613
7285
  function readStore(path, label, validateRecord) {
6614
- if (!existsSync22(path)) {
7286
+ if (!existsSync25(path)) {
6615
7287
  return { schemaVersion: 1, records: [] };
6616
7288
  }
6617
7289
  const parsed = readJsonFile(path, label);
@@ -6835,16 +7507,16 @@ function createBackup(filePaths, deps = {}) {
6835
7507
  const randomHex = deps.randomHex ?? defaultRandomHex;
6836
7508
  const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
6837
7509
  const files = filePaths.map((path) => {
6838
- if (!existsSync22(path)) {
7510
+ if (!existsSync25(path)) {
6839
7511
  return { path, existed: false, sha256: null, mode: null, contentBase64: null };
6840
7512
  }
6841
- const bytes = readFileSync16(path);
7513
+ const bytes = readFileSync19(path);
6842
7514
  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") };
7515
+ const sha2562 = createHash5("sha256").update(bytes).digest("hex");
7516
+ return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
6845
7517
  });
6846
7518
  const manifest = { id, createdAt: now.toISOString(), files };
6847
- writeJsonAtomic(join27(getBackupsDir(), id, "manifest.json"), manifest);
7519
+ writeJsonAtomic(join28(getBackupsDir(), id, "manifest.json"), manifest);
6848
7520
  return manifest;
6849
7521
  }
6850
7522
  function validateBackupManifestShape(value, path) {
@@ -6893,8 +7565,8 @@ function readBackupManifest(id) {
6893
7565
  if (!isValidBackupId(id)) {
6894
7566
  throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
6895
7567
  }
6896
- const manifestPath = join27(getBackupsDir(), id, "manifest.json");
6897
- if (!existsSync22(manifestPath)) {
7568
+ const manifestPath = join28(getBackupsDir(), id, "manifest.json");
7569
+ if (!existsSync25(manifestPath)) {
6898
7570
  throw new UserError(`no backup with id: ${id}`);
6899
7571
  }
6900
7572
  const parsed = readJsonFile(manifestPath, "backup manifest");
@@ -6923,25 +7595,25 @@ function realpathOrNull2(p) {
6923
7595
  }
6924
7596
  function isUnderRoot2(candidate, root) {
6925
7597
  const rel = relative3(root, candidate);
6926
- return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
7598
+ return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
6927
7599
  }
6928
7600
  function resolveScopes(config, cwd, skill, userRootFlag) {
6929
7601
  assertPlainSkillName(skill);
6930
7602
  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) };
7603
+ const localDir = join29(resolvedCwd, ".claude", "skills", skill);
7604
+ const sharedDir = join29(resolvedCwd, "skills", skill);
7605
+ const local = { scope: "local", dir: localDir, exists: existsSync26(localDir) };
7606
+ const shared = { scope: "shared", dir: sharedDir, exists: existsSync26(sharedDir) };
6935
7607
  const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
6936
7608
  return { local, shared, user };
6937
7609
  }
6938
7610
  function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
6939
7611
  if (userRootFlag) {
6940
7612
  const root = resolvePath11(userRootFlag);
6941
- return { resolved: true, dir: join28(root, skill), root: userRootFlag, reason: "flag" };
7613
+ return { resolved: true, dir: join29(root, skill), root: userRootFlag, reason: "flag" };
6942
7614
  }
6943
7615
  const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
6944
- const candidates = config.skillsRoots.map((root) => ({ root, dir: join28(resolveConfiguredRoot2(root), skill) }));
7616
+ const candidates = config.skillsRoots.map((root) => ({ root, dir: join29(resolveConfiguredRoot2(root), skill) }));
6945
7617
  for (const candidate of candidates) {
6946
7618
  const skillReal = realpathOrNull2(candidate.dir);
6947
7619
  if (skillReal) {
@@ -6976,12 +7648,12 @@ function inventoryScopeDir(scope, dir) {
6976
7648
  scope,
6977
7649
  dir,
6978
7650
  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"))
7651
+ skillMd: isFile2(join29(dir, "SKILL.md")),
7652
+ patchMd: isFile2(join29(dir, "SKILL.patch.md")),
7653
+ extendMd: isFile2(join29(dir, "SKILL.extend.md")),
7654
+ configJson: isFile2(join29(dir, "skill-config.json")),
7655
+ hooksDir: isDirectory(join29(dir, "hooks")),
7656
+ scriptsDir: isDirectory(join29(dir, "scripts"))
6985
7657
  };
6986
7658
  }
6987
7659
  function inventoryAllScopes(resolution) {
@@ -7254,14 +7926,14 @@ function resolveConfiguredRoot3(root) {
7254
7926
  }
7255
7927
  function isUnderRoot3(candidate, root) {
7256
7928
  const rel = relative4(root, candidate);
7257
- return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
7929
+ return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
7258
7930
  }
7259
7931
  function sanitizeDiffForConsole(diff) {
7260
7932
  return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
7261
7933
  }
7262
7934
  function resolveContentOption(options) {
7263
7935
  if (options.content !== void 0) return options.content;
7264
- if (options.contentFile) return readFileSync17(resolvePath12(options.contentFile), "utf-8");
7936
+ if (options.contentFile) return readFileSync20(resolvePath12(options.contentFile), "utf-8");
7265
7937
  return void 0;
7266
7938
  }
7267
7939
  function validateCaptureOptions(options) {
@@ -7413,8 +8085,8 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
7413
8085
  }
7414
8086
  function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
7415
8087
  const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
7416
- const destPath = join29(scopeDir, relPath);
7417
- const destDir = dirname13(destPath);
8088
+ const destPath = join30(scopeDir, relPath);
8089
+ const destDir = dirname14(destPath);
7418
8090
  mkdirSync9(destDir, { recursive: true });
7419
8091
  const realDestDir = realpathSync9(destDir);
7420
8092
  if (!isUnderRoot3(realDestDir, realScopeDir)) {
@@ -7509,12 +8181,12 @@ Write ${preview.destPath}? [y/N] `);
7509
8181
  }
7510
8182
  function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
7511
8183
  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") : "";
8184
+ const probePath = probeRelPath ? join30(scopeDir, probeRelPath) : void 0;
8185
+ const existingContent = probePath && existsSync27(probePath) ? readFileSync20(probePath, "utf-8") : "";
7514
8186
  const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
7515
8187
  const relPath = rendered.relPath;
7516
- const destPath = join29(scopeDir, relPath);
7517
- const before = existsSync24(destPath) ? readFileSync17(destPath, "utf-8") : "";
8188
+ const destPath = join30(scopeDir, relPath);
8189
+ const before = existsSync27(destPath) ? readFileSync20(destPath, "utf-8") : "";
7518
8190
  const diff = previewDiff(before, rendered.content, destPath);
7519
8191
  return { relPath, content: rendered.content, destPath, diff };
7520
8192
  }
@@ -7611,16 +8283,16 @@ async function refineWhichCommand(skill, options = {}) {
7611
8283
  }
7612
8284
  function writePostWriteSidecar(backupId, files) {
7613
8285
  const sidecar = { schemaVersion: 1, files };
7614
- const dir = join29(getBackupsDir(), backupId);
8286
+ const dir = join30(getBackupsDir(), backupId);
7615
8287
  mkdirSync9(dir, { recursive: true });
7616
- writeFileSync9(join29(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
8288
+ writeFileSync9(join30(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
7617
8289
  }
7618
8290
  function readPostWriteSidecar(backupId) {
7619
- const path = join29(getBackupsDir(), backupId, "post.json");
7620
- if (!existsSync24(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
8291
+ const path = join30(getBackupsDir(), backupId, "post.json");
8292
+ if (!existsSync27(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
7621
8293
  let raw;
7622
8294
  try {
7623
- raw = readFileSync17(path, "utf-8");
8295
+ raw = readFileSync20(path, "utf-8");
7624
8296
  } catch {
7625
8297
  return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
7626
8298
  }
@@ -7678,8 +8350,8 @@ async function refinePromoteCommand(patternId, options = {}) {
7678
8350
  if (!isUnderRoot3(realSkillDir, realUserRoot)) {
7679
8351
  throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
7680
8352
  }
7681
- const skillMdPath = join29(skillDir, "SKILL.md");
7682
- if (!existsSync24(skillMdPath)) {
8353
+ const skillMdPath = join30(skillDir, "SKILL.md");
8354
+ if (!existsSync27(skillMdPath)) {
7683
8355
  throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
7684
8356
  }
7685
8357
  const refinementsStore = readRefinementsStore();
@@ -7696,7 +8368,7 @@ async function refinePromoteCommand(patternId, options = {}) {
7696
8368
  `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
8369
  );
7698
8370
  }
7699
- const before = readFileSync17(skillMdPath, "utf-8");
8371
+ const before = readFileSync20(skillMdPath, "utf-8");
7700
8372
  const after = applyPatchBlocks(before, [block]);
7701
8373
  const diff = previewDiff(before, after, skillMdPath);
7702
8374
  if (options.dryRun) {
@@ -7728,7 +8400,7 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
7728
8400
  throw err;
7729
8401
  }
7730
8402
  try {
7731
- const postSha256 = createHash4("sha256").update(readFileSync17(skillMdPath)).digest("hex");
8403
+ const postSha256 = createHash6("sha256").update(readFileSync20(skillMdPath)).digest("hex");
7732
8404
  writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
7733
8405
  markGeneralized(patternId, backup.id);
7734
8406
  const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
@@ -7754,10 +8426,10 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
7754
8426
  }
7755
8427
  function assertBackupDirContained(backupId) {
7756
8428
  const backupsDir = getBackupsDir();
7757
- if (!existsSync24(backupsDir)) return;
8429
+ if (!existsSync27(backupsDir)) return;
7758
8430
  const realBackupsDir = realpathSync9(backupsDir);
7759
- const backupDir = join29(backupsDir, backupId);
7760
- if (!existsSync24(backupDir)) return;
8431
+ const backupDir = join30(backupsDir, backupId);
8432
+ if (!existsSync27(backupDir)) return;
7761
8433
  const realBackupDir = realpathSync9(backupDir);
7762
8434
  if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
7763
8435
  throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
@@ -7769,9 +8441,9 @@ function resolveTargetRealPath(path) {
7769
8441
  try {
7770
8442
  const real = realpathSync9(lexicalAncestor);
7771
8443
  const remainder = relative4(lexicalAncestor, resolvePath12(path));
7772
- return remainder === "" ? real : join29(real, remainder);
8444
+ return remainder === "" ? real : join30(real, remainder);
7773
8445
  } catch {
7774
- const parent = dirname13(lexicalAncestor);
8446
+ const parent = dirname14(lexicalAncestor);
7775
8447
  if (parent === lexicalAncestor) return resolvePath12(path);
7776
8448
  lexicalAncestor = parent;
7777
8449
  }
@@ -7815,7 +8487,7 @@ function findLexicalRoot(filePath, config) {
7815
8487
  for (const root of config.skillsRoots) {
7816
8488
  const rootLexical = resolveConfiguredRoot3(root);
7817
8489
  const rel = relative4(rootLexical, lexicalTarget);
7818
- if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute4(rel))) continue;
8490
+ if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute5(rel))) continue;
7819
8491
  let rootReal;
7820
8492
  try {
7821
8493
  rootReal = realpathSync9(rootLexical);
@@ -7838,7 +8510,7 @@ function walkRealComponents(filePath, config, createMissing) {
7838
8510
  let current = rootReal;
7839
8511
  for (const component of dirComponents) {
7840
8512
  if (current === void 0) break;
7841
- const next = join29(current, component);
8513
+ const next = join30(current, component);
7842
8514
  let lst;
7843
8515
  try {
7844
8516
  lst = lstatSync4(next);
@@ -7869,7 +8541,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
7869
8541
  if (parentReal === void 0) {
7870
8542
  throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
7871
8543
  }
7872
- const finalPath = join29(parentReal, leafComponent);
8544
+ const finalPath = join30(parentReal, leafComponent);
7873
8545
  if (!isUnderRoot3(finalPath, rootReal)) {
7874
8546
  throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
7875
8547
  }
@@ -7895,7 +8567,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
7895
8567
  function safeDeleteTombstone(filePath, config) {
7896
8568
  const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
7897
8569
  if (parentReal === void 0) return;
7898
- const finalPath = join29(parentReal, leafComponent);
8570
+ const finalPath = join30(parentReal, leafComponent);
7899
8571
  if (!isUnderRoot3(finalPath, rootReal)) {
7900
8572
  throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
7901
8573
  }
@@ -7937,7 +8609,7 @@ async function refineRollbackCommand(backupId, options = {}) {
7937
8609
  for (const file of manifest.files) {
7938
8610
  const expected = sidecar.hashes.get(file.path);
7939
8611
  if (expected === void 0) continue;
7940
- const currentSha256 = existsSync24(file.path) ? createHash4("sha256").update(readFileSync17(file.path)).digest("hex") : null;
8612
+ const currentSha256 = existsSync27(file.path) ? createHash6("sha256").update(readFileSync20(file.path)).digest("hex") : null;
7941
8613
  if (currentSha256 !== expected) mismatches.push(file.path);
7942
8614
  }
7943
8615
  if (mismatches.length > 0 && !options.force) {
@@ -8033,8 +8705,14 @@ program.name("skill-forge").description("The supply-chain gate for agent skills"
8033
8705
  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
8706
  "--mcp-target <file>",
8035
8707
  "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);
8708
+ ).option("--force", "overwrite an existing same-name MCP server entry when promoting (mcp artifacts only)").option(
8709
+ "--skill-map <path>",
8710
+ "path to a generated rhize-plugins skill map; reports existing-capability overlap against every skill node in it before promotion (curation-rule check)"
8711
+ ).action(add);
8712
+ 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(
8713
+ "--skill-map <path>",
8714
+ "path to a generated rhize-plugins skill map; reports existing-capability overlap against every skill node in it (curation-rule check)"
8715
+ ).action(scan);
8038
8716
  program.command("list").description("List skills currently in quarantine").action(list);
8039
8717
  program.command("status").description("Show skill-forge configuration and quarantine status").action(status);
8040
8718
  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 +8727,16 @@ configCmd.command("review").description("Review pending agent-proposed preferenc
8049
8727
  program.command("guide [topic]").description("What this tool does, what state you are in, and the next command to run").action(guide);
8050
8728
  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
8729
  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);
8730
+ var finding = program.command("finding").description("Review and acknowledge audit findings \u2014 human-only, never a gate input (see docs/accepted-findings-schema.md)");
8731
+ 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);
8732
+ 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);
8733
+ 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
8734
  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
8735
  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);
8736
+ 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(
8737
+ "--skill-map <path>",
8738
+ "also drift-check every fork-of edge in a generated rhize-plugins skill map (contentHash compare against upstream)"
8739
+ ).action(watchCommand);
8055
8740
  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
8741
  program.command("ingest").description(
8057
8742
  "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."