@rhize/skill-forge 0.11.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -4
- package/dist/cli.js +829 -274
- package/dist/cli.js.map +1 -1
- package/dist/curation-prompt.md +21 -0
- package/dist/ingest-prompt.md +10 -0
- package/package.json +1 -1
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
|
|
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
|
|
1029
|
-
import { basename as basename7, dirname as dirname5, join as
|
|
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
|
|
1033
|
+
existsSync as existsSync9,
|
|
1034
1034
|
lstatSync as lstatSync2,
|
|
1035
1035
|
mkdtempSync,
|
|
1036
|
-
readFileSync as
|
|
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
|
|
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
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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+(
|
|
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
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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 =
|
|
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) =>
|
|
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 (
|
|
1877
|
+
if (existsSync9(join12(dir, "SKILL.md"))) found.add(dir);
|
|
1761
1878
|
}
|
|
1762
|
-
for (const dir of listSubdirsFollowingSymlinks(
|
|
1763
|
-
if (
|
|
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(
|
|
1767
|
-
if (
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
2138
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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 =
|
|
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(
|
|
2584
|
+
const tempDir = mkdtempSync(join12(resolvedRoot, ".business-foundation-"));
|
|
2410
2585
|
try {
|
|
2411
|
-
writeFileSync5(
|
|
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
|
|
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
|
|
2637
|
+
for (const finding2 of report.hygiene) {
|
|
2463
2638
|
let root;
|
|
2464
|
-
for (let 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],
|
|
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 (!
|
|
2676
|
+
if (!existsSync10(path)) return void 0;
|
|
2501
2677
|
try {
|
|
2502
|
-
const parsed = JSON.parse(
|
|
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
|
|
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
|
|
2746
|
+
existsSync as existsSync12,
|
|
2570
2747
|
mkdtempSync as mkdtempSync2,
|
|
2571
2748
|
openSync,
|
|
2572
|
-
readFileSync as
|
|
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
|
|
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
|
|
2583
|
-
import { extname as extname4, join as
|
|
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(
|
|
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 =
|
|
2690
|
-
if (
|
|
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 =
|
|
2698
|
-
if (!
|
|
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 (!
|
|
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 (
|
|
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 =
|
|
2814
|
-
if (!
|
|
2990
|
+
const pkgPath = join15(root, "package.json");
|
|
2991
|
+
if (!existsSync12(pkgPath)) return void 0;
|
|
2815
2992
|
try {
|
|
2816
|
-
return JSON.parse(
|
|
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 (
|
|
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(
|
|
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 =
|
|
2857
|
-
if (
|
|
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 =
|
|
2883
|
-
if (
|
|
3059
|
+
const candidate = join15(dir, entry.name, "SKILL.md");
|
|
3060
|
+
if (existsSync12(candidate)) matches.add(candidate);
|
|
2884
3061
|
}
|
|
2885
3062
|
}
|
|
2886
|
-
const skillsDir =
|
|
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 =
|
|
2891
|
-
if (
|
|
3067
|
+
const candidate = join15(skillsDir, entry.name, "SKILL.md");
|
|
3068
|
+
if (existsSync12(candidate)) matches.add(candidate);
|
|
2892
3069
|
}
|
|
2893
3070
|
}
|
|
2894
3071
|
}
|
|
@@ -2971,9 +3148,9 @@ function detectLicense(root, fm) {
|
|
|
2971
3148
|
if (fm.license) return fm.license;
|
|
2972
3149
|
const names = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "license"];
|
|
2973
3150
|
for (const name of names) {
|
|
2974
|
-
const p =
|
|
2975
|
-
if (
|
|
2976
|
-
const head =
|
|
3151
|
+
const p = join15(root, name);
|
|
3152
|
+
if (existsSync12(p) && statSync7(p).isFile()) {
|
|
3153
|
+
const head = readFileSync12(p, "utf-8").slice(0, 2e3).toLowerCase();
|
|
2977
3154
|
for (const [hint, label] of Object.entries(SPDX_HINTS)) {
|
|
2978
3155
|
if (head.includes(hint)) return label;
|
|
2979
3156
|
}
|
|
@@ -2985,7 +3162,7 @@ function detectLicense(root, fm) {
|
|
|
2985
3162
|
function walkFiles(dir) {
|
|
2986
3163
|
const out = [];
|
|
2987
3164
|
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
2988
|
-
const full =
|
|
3165
|
+
const full = join15(dir, entry.name);
|
|
2989
3166
|
if (entry.isDirectory()) {
|
|
2990
3167
|
out.push(...walkFiles(full));
|
|
2991
3168
|
} else if (entry.isFile()) {
|
|
@@ -2997,7 +3174,7 @@ function walkFiles(dir) {
|
|
|
2997
3174
|
function countResources(root) {
|
|
2998
3175
|
const resources = {};
|
|
2999
3176
|
for (const d of RESOURCE_DIRS) {
|
|
3000
|
-
const dp =
|
|
3177
|
+
const dp = join15(root, d);
|
|
3001
3178
|
if (isDir(dp)) {
|
|
3002
3179
|
resources[d] = walkFiles(dp).length;
|
|
3003
3180
|
}
|
|
@@ -3008,11 +3185,11 @@ function scanMcpAndImports(text, root) {
|
|
|
3008
3185
|
const mcpRefRe = /mcp__[a-zA-Z0-9_]+/g;
|
|
3009
3186
|
const mcpRefs = new Set(text.match(mcpRefRe) ?? []);
|
|
3010
3187
|
const imports = /* @__PURE__ */ new Set();
|
|
3011
|
-
const scriptsDir =
|
|
3188
|
+
const scriptsDir = join15(root, "scripts");
|
|
3012
3189
|
if (isDir(scriptsDir)) {
|
|
3013
3190
|
for (const f of walkFiles(scriptsDir)) {
|
|
3014
3191
|
if (!f.endsWith(".py")) continue;
|
|
3015
|
-
const ft =
|
|
3192
|
+
const ft = readFileSync12(f, "utf-8");
|
|
3016
3193
|
for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
|
|
3017
3194
|
const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
|
|
3018
3195
|
let m;
|
|
@@ -3029,7 +3206,7 @@ function scanMcpAndImports(text, root) {
|
|
|
3029
3206
|
function profileSkillDir(inputPath) {
|
|
3030
3207
|
const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
|
|
3031
3208
|
try {
|
|
3032
|
-
const text =
|
|
3209
|
+
const text = readFileSync12(skillMd, "utf-8");
|
|
3033
3210
|
const fm = parseFrontmatter(text);
|
|
3034
3211
|
const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
|
|
3035
3212
|
const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
|
|
@@ -3057,19 +3234,19 @@ function profileSkillDir(inputPath) {
|
|
|
3057
3234
|
}
|
|
3058
3235
|
}
|
|
3059
3236
|
function profilePlugin(p) {
|
|
3060
|
-
const manifestPath =
|
|
3237
|
+
const manifestPath = join15(p, ".claude-plugin", "plugin.json");
|
|
3061
3238
|
let manifest;
|
|
3062
3239
|
try {
|
|
3063
|
-
manifest = JSON.parse(
|
|
3240
|
+
manifest = JSON.parse(readFileSync12(manifestPath, "utf-8"));
|
|
3064
3241
|
} catch (err) {
|
|
3065
3242
|
throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
|
|
3066
3243
|
}
|
|
3067
|
-
const skillsDir =
|
|
3244
|
+
const skillsDir = join15(p, "skills");
|
|
3068
3245
|
let skills = [];
|
|
3069
3246
|
if (isDir(skillsDir)) {
|
|
3070
|
-
skills = readdirSync6(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() &&
|
|
3247
|
+
skills = readdirSync6(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync12(join15(skillsDir, d.name, "SKILL.md"))).map((d) => d.name).sort();
|
|
3071
3248
|
}
|
|
3072
|
-
const commandsDir =
|
|
3249
|
+
const commandsDir = join15(p, "commands");
|
|
3073
3250
|
const commands = isDir(commandsDir) ? readdirSync6(commandsDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
3074
3251
|
const mc = manifest.mcpServers ?? manifest.mcp_servers ?? {};
|
|
3075
3252
|
const mcpServers = typeof mc === "object" && mc !== null ? Object.keys(mc).sort() : [];
|
|
@@ -3087,11 +3264,11 @@ function profilePlugin(p) {
|
|
|
3087
3264
|
};
|
|
3088
3265
|
}
|
|
3089
3266
|
function profileMcp(p) {
|
|
3090
|
-
const explicitConfig =
|
|
3267
|
+
const explicitConfig = existsSync12(join15(p, ".mcp.json")) ? join15(p, ".mcp.json") : existsSync12(join15(p, "mcp.json")) ? join15(p, "mcp.json") : void 0;
|
|
3091
3268
|
if (explicitConfig) {
|
|
3092
3269
|
let cfg;
|
|
3093
3270
|
try {
|
|
3094
|
-
cfg = JSON.parse(
|
|
3271
|
+
cfg = JSON.parse(readFileSync12(explicitConfig, "utf-8"));
|
|
3095
3272
|
} catch (err) {
|
|
3096
3273
|
throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
|
|
3097
3274
|
}
|
|
@@ -3107,10 +3284,10 @@ function profileMcp(p) {
|
|
|
3107
3284
|
capabilities: extractMcpCapabilities(p)
|
|
3108
3285
|
};
|
|
3109
3286
|
}
|
|
3110
|
-
const pkgPath =
|
|
3287
|
+
const pkgPath = join15(p, "package.json");
|
|
3111
3288
|
let pkg2;
|
|
3112
3289
|
try {
|
|
3113
|
-
pkg2 = JSON.parse(
|
|
3290
|
+
pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
3114
3291
|
} catch (err) {
|
|
3115
3292
|
throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
|
|
3116
3293
|
}
|
|
@@ -3127,14 +3304,14 @@ function profileMcp(p) {
|
|
|
3127
3304
|
}
|
|
3128
3305
|
|
|
3129
3306
|
// src/gate/mcpOverlap.ts
|
|
3130
|
-
import { existsSync as
|
|
3307
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
|
|
3131
3308
|
import { resolve as resolve4 } from "path";
|
|
3132
3309
|
function readTargetServers(targetPath) {
|
|
3133
3310
|
const path = resolve4(expandHome(targetPath));
|
|
3134
|
-
if (!
|
|
3311
|
+
if (!existsSync13(path)) return [];
|
|
3135
3312
|
let cfg;
|
|
3136
3313
|
try {
|
|
3137
|
-
cfg = JSON.parse(
|
|
3314
|
+
cfg = JSON.parse(readFileSync13(path, "utf-8"));
|
|
3138
3315
|
} catch {
|
|
3139
3316
|
return [];
|
|
3140
3317
|
}
|
|
@@ -3206,6 +3383,121 @@ function scanMcpOverlap(candidate, mcpTargetPaths) {
|
|
|
3206
3383
|
};
|
|
3207
3384
|
}
|
|
3208
3385
|
|
|
3386
|
+
// src/gate/skillMap.ts
|
|
3387
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
3388
|
+
import { resolve as resolve5 } from "path";
|
|
3389
|
+
function loadSkillMap(path) {
|
|
3390
|
+
const resolved = resolve5(expandHome(path));
|
|
3391
|
+
let text;
|
|
3392
|
+
try {
|
|
3393
|
+
text = readFileSync14(resolved, "utf-8");
|
|
3394
|
+
} catch (err) {
|
|
3395
|
+
throw new Error(`could not read skill map at ${resolved}: ${err.message}`);
|
|
3396
|
+
}
|
|
3397
|
+
let parsed;
|
|
3398
|
+
try {
|
|
3399
|
+
parsed = JSON.parse(text);
|
|
3400
|
+
} catch (err) {
|
|
3401
|
+
throw new Error(`skill map at ${resolved} is not valid JSON: ${err.message}`);
|
|
3402
|
+
}
|
|
3403
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
3404
|
+
throw new Error(`skill map at ${resolved}: expected a JSON object`);
|
|
3405
|
+
}
|
|
3406
|
+
const obj = parsed;
|
|
3407
|
+
if (!Array.isArray(obj.nodes) || !Array.isArray(obj.edges)) {
|
|
3408
|
+
throw new Error(`skill map at ${resolved}: expected "nodes" and "edges" arrays`);
|
|
3409
|
+
}
|
|
3410
|
+
return {
|
|
3411
|
+
schemaVersion: typeof obj.schemaVersion === "string" ? obj.schemaVersion : void 0,
|
|
3412
|
+
nodes: obj.nodes,
|
|
3413
|
+
edges: obj.edges
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
function nodeById(map, id) {
|
|
3417
|
+
return map.nodes.find((n) => n.id === id);
|
|
3418
|
+
}
|
|
3419
|
+
function edgesOfType(map, type) {
|
|
3420
|
+
return map.edges.filter((e) => e.type === type);
|
|
3421
|
+
}
|
|
3422
|
+
function tagsForNode(map, node) {
|
|
3423
|
+
const tags = new Set(node.tags ?? []);
|
|
3424
|
+
for (const edge of map.edges) {
|
|
3425
|
+
if (edge.from !== node.id) continue;
|
|
3426
|
+
if (edge.type !== "topic-tag" && edge.type !== "stack-tag") continue;
|
|
3427
|
+
const tagNode = nodeById(map, edge.to);
|
|
3428
|
+
const label = tagNode?.name ?? tagNode?.id;
|
|
3429
|
+
if (label) tags.add(label);
|
|
3430
|
+
}
|
|
3431
|
+
return Array.from(tags);
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
// src/gate/mapOverlap.ts
|
|
3435
|
+
var STOPWORDS2 = new Set(
|
|
3436
|
+
`
|
|
3437
|
+
a an the and or of to for in on with use used using when whether this that these those is are be
|
|
3438
|
+
your you it its as at by from into via per also any all not no than then so if up out about
|
|
3439
|
+
skill skills user claude code based comprehensive enhanced practical including include includes
|
|
3440
|
+
`.split(/\s+/).filter(Boolean)
|
|
3441
|
+
);
|
|
3442
|
+
var STRONG_THRESHOLD = 0.45;
|
|
3443
|
+
var REPORT_THRESHOLD = 0.2;
|
|
3444
|
+
function tokenize2(text) {
|
|
3445
|
+
const matches = text.toLowerCase().match(/[a-z][a-z0-9+.-]{2,}/g) ?? [];
|
|
3446
|
+
return new Set(matches.filter((t) => !STOPWORDS2.has(t)));
|
|
3447
|
+
}
|
|
3448
|
+
function intersectSize2(a, b) {
|
|
3449
|
+
let n = 0;
|
|
3450
|
+
for (const x of a) if (b.has(x)) n += 1;
|
|
3451
|
+
return n;
|
|
3452
|
+
}
|
|
3453
|
+
function unionSize2(a, b) {
|
|
3454
|
+
const u = new Set(a);
|
|
3455
|
+
for (const x of b) u.add(x);
|
|
3456
|
+
return u.size;
|
|
3457
|
+
}
|
|
3458
|
+
function round33(n) {
|
|
3459
|
+
return Math.round(n * 1e3) / 1e3;
|
|
3460
|
+
}
|
|
3461
|
+
function score3(candidateTokens, otherTokens) {
|
|
3462
|
+
if (candidateTokens.size === 0) return 0;
|
|
3463
|
+
const inter = intersectSize2(candidateTokens, otherTokens);
|
|
3464
|
+
const union = unionSize2(candidateTokens, otherTokens);
|
|
3465
|
+
const jaccard = union ? inter / union : 0;
|
|
3466
|
+
const containment = inter / candidateTokens.size;
|
|
3467
|
+
return round33(0.3 * jaccard + 0.7 * containment);
|
|
3468
|
+
}
|
|
3469
|
+
function nodeText(map, node) {
|
|
3470
|
+
const tags = tagsForNode(map, node);
|
|
3471
|
+
return [node.name ?? "", node.description ?? "", ...tags].join(" ");
|
|
3472
|
+
}
|
|
3473
|
+
function scanMapOverlap(candidate, map) {
|
|
3474
|
+
const candidateTokens = tokenize2(`${candidate.name ?? ""} ${candidate.description ?? ""}`);
|
|
3475
|
+
if (candidateTokens.size === 0) return { matches: [] };
|
|
3476
|
+
const matches = [];
|
|
3477
|
+
for (const node of map.nodes) {
|
|
3478
|
+
if (node.type !== "skill") continue;
|
|
3479
|
+
const otherTokens = tokenize2(nodeText(map, node));
|
|
3480
|
+
const s = score3(candidateTokens, otherTokens);
|
|
3481
|
+
if (s >= REPORT_THRESHOLD) {
|
|
3482
|
+
matches.push({ skillId: node.id, name: node.name ?? node.id, score: s });
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
matches.sort((a, b) => b.score - a.score);
|
|
3486
|
+
return { matches };
|
|
3487
|
+
}
|
|
3488
|
+
function mapOverlapFinding(result) {
|
|
3489
|
+
const top = result.matches[0];
|
|
3490
|
+
if (!top) return void 0;
|
|
3491
|
+
const severity = top.score >= STRONG_THRESHOLD ? "HIGH" : "MEDIUM";
|
|
3492
|
+
return {
|
|
3493
|
+
severity,
|
|
3494
|
+
rule: "skill-map-overlap",
|
|
3495
|
+
file: "skill-map",
|
|
3496
|
+
detail: `candidate overlaps existing marketplace skill "${top.name}" (score ${top.score}) \u2014 see CLAUDE.md's curation rule: close the gap, don't duplicate`,
|
|
3497
|
+
context: top.skillId
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3209
3501
|
// src/report/render.ts
|
|
3210
3502
|
var SAFETY_VERDICT_MAP = {
|
|
3211
3503
|
pass: "ALLOW",
|
|
@@ -3234,10 +3526,10 @@ function renderSafetyLine(safety) {
|
|
|
3234
3526
|
const withDetail = detail ? `${safety.verdict} (${detail})` : safety.verdict;
|
|
3235
3527
|
return safety.recommendation ? `${withDetail} \u2014 ${safety.recommendation}` : withDetail;
|
|
3236
3528
|
}
|
|
3237
|
-
function renderFindingLine(
|
|
3238
|
-
const severity =
|
|
3239
|
-
const rule =
|
|
3240
|
-
return ` - [${severity}] ${rule}${
|
|
3529
|
+
function renderFindingLine(finding2) {
|
|
3530
|
+
const severity = finding2.severity ?? "UNKNOWN";
|
|
3531
|
+
const rule = finding2.rule ?? "unknown-rule";
|
|
3532
|
+
return ` - [${severity}] ${rule}${finding2.message ? `: ${finding2.message}` : ""}`;
|
|
3241
3533
|
}
|
|
3242
3534
|
function renderFindingsLines(findings) {
|
|
3243
3535
|
if (!findings?.length) return [];
|
|
@@ -3331,6 +3623,16 @@ function tryMcpOverlap(profile, mcpTargets) {
|
|
|
3331
3623
|
return void 0;
|
|
3332
3624
|
}
|
|
3333
3625
|
}
|
|
3626
|
+
function tryMapOverlapFinding(skillMapPath, candidate) {
|
|
3627
|
+
if (!skillMapPath) return void 0;
|
|
3628
|
+
try {
|
|
3629
|
+
const map = loadSkillMap(skillMapPath);
|
|
3630
|
+
return mapOverlapFinding(scanMapOverlap(candidate, map));
|
|
3631
|
+
} catch (err) {
|
|
3632
|
+
console.error(`[skill-forge] skill-map overlap check skipped: ${err.message}`);
|
|
3633
|
+
return void 0;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3334
3636
|
function resolveMcpCandidatePath(entry) {
|
|
3335
3637
|
if (entry.packageDir) return entry.packageDir;
|
|
3336
3638
|
let children = [];
|
|
@@ -3338,18 +3640,24 @@ function resolveMcpCandidatePath(entry) {
|
|
|
3338
3640
|
children = readdirSync7(entry.quarantinePath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3339
3641
|
} catch {
|
|
3340
3642
|
}
|
|
3341
|
-
return children.length === 1 ?
|
|
3643
|
+
return children.length === 1 ? join16(entry.quarantinePath, children[0]) : entry.quarantinePath;
|
|
3342
3644
|
}
|
|
3343
3645
|
async function runGate(candidatePath, skillsRoot, {
|
|
3344
3646
|
proFeatureLabel = "overlap analysis",
|
|
3345
3647
|
strictness = DEFAULT_STRICTNESS,
|
|
3346
3648
|
artifact = "skill",
|
|
3347
|
-
mcpTargets = []
|
|
3649
|
+
mcpTargets = [],
|
|
3650
|
+
skillMapPath
|
|
3348
3651
|
} = {}) {
|
|
3349
3652
|
const profile = asProfile(candidatePath, artifact);
|
|
3350
3653
|
const proStatus = proFeatureStatus(proFeatureLabel);
|
|
3351
3654
|
const safety = artifact === "mcp" ? await mcpScan(candidatePath, strictness) : await scanSafety(candidatePath, strictness);
|
|
3352
3655
|
const overlap = !proStatus.enabled ? void 0 : artifact === "mcp" ? tryMcpOverlap(profile, mcpTargets) : tryOverlap(candidatePath, skillsRoot);
|
|
3656
|
+
const mapFinding = artifact === "mcp" ? void 0 : tryMapOverlapFinding(skillMapPath, {
|
|
3657
|
+
name: "name" in profile ? profile.name : void 0,
|
|
3658
|
+
description: "description" in profile ? profile.description : void 0
|
|
3659
|
+
});
|
|
3660
|
+
const combinedSafety = mapFinding ? { verdict: verdictFor([...safety.findings, mapFinding], strictness), findings: [...safety.findings, mapFinding] } : safety;
|
|
3353
3661
|
const advisory = formatAuditAdvisory(readAuditState(), {
|
|
3354
3662
|
targetRoot: skillsRoot || void 0,
|
|
3355
3663
|
candidateName: "name" in profile ? profile.name : void 0
|
|
@@ -3360,27 +3668,27 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
3360
3668
|
overlapLocked: !proStatus.enabled,
|
|
3361
3669
|
notices: proStatus.notice ? [proStatus.notice] : void 0,
|
|
3362
3670
|
safety: {
|
|
3363
|
-
verdict: SAFETY_VERDICT_MAP[
|
|
3364
|
-
findings: mapSafetyFindings(
|
|
3671
|
+
verdict: SAFETY_VERDICT_MAP[combinedSafety.verdict],
|
|
3672
|
+
findings: mapSafetyFindings(combinedSafety.findings)
|
|
3365
3673
|
},
|
|
3366
3674
|
advisory
|
|
3367
3675
|
};
|
|
3368
|
-
return { safety, gateResult, proEnabled: proStatus.enabled };
|
|
3676
|
+
return { safety: combinedSafety, gateResult, proEnabled: proStatus.enabled };
|
|
3369
3677
|
}
|
|
3370
3678
|
|
|
3371
3679
|
// src/queue.ts
|
|
3372
3680
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
3373
|
-
import { existsSync as
|
|
3374
|
-
import { join as
|
|
3681
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync15, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3682
|
+
import { join as join17 } from "path";
|
|
3375
3683
|
function getQueuePath() {
|
|
3376
|
-
return
|
|
3684
|
+
return join17(getHomeDir(), "queue.json");
|
|
3377
3685
|
}
|
|
3378
3686
|
function readQueue() {
|
|
3379
3687
|
const path = getQueuePath();
|
|
3380
|
-
if (!
|
|
3688
|
+
if (!existsSync14(path)) {
|
|
3381
3689
|
return { version: 1, entries: [] };
|
|
3382
3690
|
}
|
|
3383
|
-
const raw =
|
|
3691
|
+
const raw = readFileSync15(path, "utf-8");
|
|
3384
3692
|
const parsed = JSON.parse(raw);
|
|
3385
3693
|
return { version: 1, entries: parsed.entries ?? [] };
|
|
3386
3694
|
}
|
|
@@ -3388,7 +3696,7 @@ function writeQueue(queue2) {
|
|
|
3388
3696
|
const home = getHomeDir();
|
|
3389
3697
|
mkdirSync6(home, { recursive: true });
|
|
3390
3698
|
const finalPath = getQueuePath();
|
|
3391
|
-
const tempPath =
|
|
3699
|
+
const tempPath = join17(home, `.queue.json.tmp-${randomUUID3()}`);
|
|
3392
3700
|
try {
|
|
3393
3701
|
writeFileSync6(tempPath, JSON.stringify(queue2, null, 2) + "\n", "utf-8");
|
|
3394
3702
|
renameSync4(tempPath, finalPath);
|
|
@@ -3437,16 +3745,16 @@ function generateId(existingIds) {
|
|
|
3437
3745
|
|
|
3438
3746
|
// src/handoff.ts
|
|
3439
3747
|
import { spawnSync } from "child_process";
|
|
3440
|
-
import { existsSync as
|
|
3441
|
-
import { dirname as dirname7, join as
|
|
3748
|
+
import { existsSync as existsSync15 } from "fs";
|
|
3749
|
+
import { dirname as dirname7, join as join18 } from "path";
|
|
3442
3750
|
import { fileURLToPath } from "url";
|
|
3443
3751
|
var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
|
|
3444
3752
|
var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
|
|
3445
3753
|
function resolvePromptPath(assetName = "ingest-prompt.md") {
|
|
3446
3754
|
const here = dirname7(fileURLToPath(import.meta.url));
|
|
3447
|
-
const builtCandidate =
|
|
3448
|
-
if (
|
|
3449
|
-
return
|
|
3755
|
+
const builtCandidate = join18(here, assetName);
|
|
3756
|
+
if (existsSync15(builtCandidate)) return builtCandidate;
|
|
3757
|
+
return join18(here, "..", "assets", assetName);
|
|
3450
3758
|
}
|
|
3451
3759
|
function defaultIsOnPath(bin) {
|
|
3452
3760
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
@@ -3502,14 +3810,14 @@ function runHandoff(config, installedPath, options = {}) {
|
|
|
3502
3810
|
}
|
|
3503
3811
|
|
|
3504
3812
|
// src/commands/init.ts
|
|
3505
|
-
import { existsSync as
|
|
3813
|
+
import { existsSync as existsSync17 } from "fs";
|
|
3506
3814
|
import { homedir as homedir4 } from "os";
|
|
3507
3815
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
3508
3816
|
|
|
3509
3817
|
// src/agents.ts
|
|
3510
3818
|
import { statSync as statSync8 } from "fs";
|
|
3511
3819
|
import { homedir as homedir3 } from "os";
|
|
3512
|
-
import { join as
|
|
3820
|
+
import { join as join19, resolve as resolve6 } from "path";
|
|
3513
3821
|
function isFile(p) {
|
|
3514
3822
|
try {
|
|
3515
3823
|
return statSync8(p).isFile();
|
|
@@ -3518,12 +3826,12 @@ function isFile(p) {
|
|
|
3518
3826
|
}
|
|
3519
3827
|
}
|
|
3520
3828
|
function projectPath(root, cwd) {
|
|
3521
|
-
return root ?
|
|
3829
|
+
return root ? resolve6(cwd, root) : void 0;
|
|
3522
3830
|
}
|
|
3523
3831
|
function globalPath(root, home) {
|
|
3524
3832
|
if (!root) return void 0;
|
|
3525
3833
|
if (root === "~") return home;
|
|
3526
|
-
if (root.startsWith("~/")) return
|
|
3834
|
+
if (root.startsWith("~/")) return join19(home, root.slice(2));
|
|
3527
3835
|
return root;
|
|
3528
3836
|
}
|
|
3529
3837
|
function makeAgent(spec) {
|
|
@@ -3753,8 +4061,8 @@ var AGENTS = [
|
|
|
3753
4061
|
].map(makeAgent);
|
|
3754
4062
|
|
|
3755
4063
|
// src/commands/audit.ts
|
|
3756
|
-
import { existsSync as
|
|
3757
|
-
import { join as
|
|
4064
|
+
import { existsSync as existsSync16 } from "fs";
|
|
4065
|
+
import { join as join20, resolve as resolvePath3 } from "path";
|
|
3758
4066
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
3759
4067
|
|
|
3760
4068
|
// src/decision.ts
|
|
@@ -3822,7 +4130,7 @@ async function captureBusinessProfile(rl, existing) {
|
|
|
3822
4130
|
function resolveReportPath(reportOption) {
|
|
3823
4131
|
if (reportOption) return resolvePath3(reportOption);
|
|
3824
4132
|
const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3825
|
-
return
|
|
4133
|
+
return join20(getHomeDir(), "reports", `audit-${iso}.md`);
|
|
3826
4134
|
}
|
|
3827
4135
|
function printTerminalSummary(report, reportPath) {
|
|
3828
4136
|
const { summary } = report;
|
|
@@ -3831,6 +4139,7 @@ function printTerminalSummary(report, reportPath) {
|
|
|
3831
4139
|
console.log(
|
|
3832
4140
|
` findings: ${summary.findingCounts.CRITICAL} CRITICAL, ${summary.findingCounts.HIGH} HIGH, ${summary.findingCounts.MEDIUM} MEDIUM, ${summary.findingCounts.LOW} LOW`
|
|
3833
4141
|
);
|
|
4142
|
+
console.log(` accepted: ${summary.acceptedCount} (suppressed from counts; see report)`);
|
|
3834
4143
|
console.log(` overlap clusters: ${summary.overlapClusterCount} evolve-eligible: ${summary.evolveEligibleCount}`);
|
|
3835
4144
|
console.log(` report -> ${reportPath}`);
|
|
3836
4145
|
for (const notice of report.notices) console.log(` [notice] ${notice}`);
|
|
@@ -3871,7 +4180,7 @@ async function maybeHandoff(config, reportPath, options, rl) {
|
|
|
3871
4180
|
}
|
|
3872
4181
|
async function auditCommand(options = {}) {
|
|
3873
4182
|
await maybeOfferInit({ json: options.json, yes: options.yes, suppressAuditOffer: true });
|
|
3874
|
-
if (!
|
|
4183
|
+
if (!existsSync16(getConfigPath())) {
|
|
3875
4184
|
throw new UserError(
|
|
3876
4185
|
"no skill-forge config found \u2014 run `skill-forge init` first (audit requires a configured skill/MCP set; it never audits an invented default)."
|
|
3877
4186
|
);
|
|
@@ -3887,7 +4196,9 @@ async function auditCommand(options = {}) {
|
|
|
3887
4196
|
saveConfig(config);
|
|
3888
4197
|
}
|
|
3889
4198
|
}
|
|
3890
|
-
const
|
|
4199
|
+
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
4200
|
+
const report = await auditConfiguredSet(config, { accepted });
|
|
4201
|
+
if (storeNotices.length > 0) report.notices = [...report.notices, ...storeNotices];
|
|
3891
4202
|
const reportPath = resolveReportPath(options.report);
|
|
3892
4203
|
writeNewFile(reportPath, renderAuditMarkdown(report), "--report");
|
|
3893
4204
|
try {
|
|
@@ -4214,7 +4525,7 @@ async function init(options = {}, deps = {}) {
|
|
|
4214
4525
|
}
|
|
4215
4526
|
async function maybeOfferInit(options = {}) {
|
|
4216
4527
|
if (options.json || options.yes || !process.stdin.isTTY) return;
|
|
4217
|
-
if (
|
|
4528
|
+
if (existsSync17(getConfigPath())) return;
|
|
4218
4529
|
console.log(
|
|
4219
4530
|
"[skill-forge] No config found \u2014 run `skill-forge init` to detect your agents and set gate targets (init finishes with a `skill-forge audit` health check over what it just configured)."
|
|
4220
4531
|
);
|
|
@@ -4263,7 +4574,8 @@ async function add(source, options = {}) {
|
|
|
4263
4574
|
const { safety, gateResult, proEnabled } = await runGate(entry.skillDirs[0], targetRoot, {
|
|
4264
4575
|
proFeatureLabel: PRO_FEATURE_LABEL,
|
|
4265
4576
|
strictness: config.strictness,
|
|
4266
|
-
artifact
|
|
4577
|
+
artifact,
|
|
4578
|
+
skillMapPath: options.skillMap
|
|
4267
4579
|
});
|
|
4268
4580
|
const { profile, overlap } = gateResult;
|
|
4269
4581
|
if (profile.kind !== "skill") {
|
|
@@ -4364,7 +4676,7 @@ async function addMcp(source, config, options) {
|
|
|
4364
4676
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
4365
4677
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
4366
4678
|
};
|
|
4367
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
4679
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join21(process.cwd(), ".mcp.json");
|
|
4368
4680
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
4369
4681
|
reject(entry);
|
|
4370
4682
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -4442,7 +4754,8 @@ async function scan(source, options = {}) {
|
|
|
4442
4754
|
const { safety, gateResult } = await runGate(candidatePath, skillsRoot ?? "", {
|
|
4443
4755
|
strictness: config.strictness,
|
|
4444
4756
|
artifact,
|
|
4445
|
-
mcpTargets: config.mcpTargets ?? []
|
|
4757
|
+
mcpTargets: config.mcpTargets ?? [],
|
|
4758
|
+
skillMapPath: options.skillMap
|
|
4446
4759
|
});
|
|
4447
4760
|
if (artifact === "mcp" && gateResult.profile.kind === "mcp" && gateResult.profile.serverCount > 1) {
|
|
4448
4761
|
console.error(
|
|
@@ -4459,7 +4772,7 @@ async function scan(source, options = {}) {
|
|
|
4459
4772
|
}
|
|
4460
4773
|
|
|
4461
4774
|
// src/commands/list.ts
|
|
4462
|
-
import { join as
|
|
4775
|
+
import { join as join22 } from "path";
|
|
4463
4776
|
async function list() {
|
|
4464
4777
|
await maybeOfferInit();
|
|
4465
4778
|
const config = loadConfig();
|
|
@@ -4472,14 +4785,14 @@ async function list() {
|
|
|
4472
4785
|
}
|
|
4473
4786
|
console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
|
|
4474
4787
|
for (const id of ids) {
|
|
4475
|
-
const skillDirs = findSkillDirs(
|
|
4788
|
+
const skillDirs = findSkillDirs(join22(quarantineDir, id));
|
|
4476
4789
|
if (skillDirs.length === 0) {
|
|
4477
4790
|
console.log(` ${id} (no SKILL.md found)`);
|
|
4478
4791
|
continue;
|
|
4479
4792
|
}
|
|
4480
4793
|
const names = skillDirs.map((dir) => {
|
|
4481
4794
|
try {
|
|
4482
|
-
return readFrontmatter(
|
|
4795
|
+
return readFrontmatter(join22(dir, "SKILL.md")).name;
|
|
4483
4796
|
} catch {
|
|
4484
4797
|
return dir;
|
|
4485
4798
|
}
|
|
@@ -4489,18 +4802,18 @@ async function list() {
|
|
|
4489
4802
|
}
|
|
4490
4803
|
|
|
4491
4804
|
// src/commands/guide.ts
|
|
4492
|
-
import { existsSync as
|
|
4805
|
+
import { existsSync as existsSync19 } from "fs";
|
|
4493
4806
|
|
|
4494
4807
|
// src/commands/held.ts
|
|
4495
|
-
import { existsSync as
|
|
4496
|
-
import { basename as basename9, join as
|
|
4808
|
+
import { existsSync as existsSync18, realpathSync as realpathSync3 } from "fs";
|
|
4809
|
+
import { basename as basename9, join as join23, resolve as resolvePath4, sep as sep2 } from "path";
|
|
4497
4810
|
var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
|
|
4498
4811
|
function resolveHeldPath(quarantineDir, id) {
|
|
4499
4812
|
if (id === "" || id !== basename9(id) || id === "." || id === "..") {
|
|
4500
4813
|
throw new UserError(`invalid quarantine id "${id}" \u2014 pass a single id exactly as printed by \`skill-forge list\``);
|
|
4501
4814
|
}
|
|
4502
|
-
const candidate =
|
|
4503
|
-
if (!
|
|
4815
|
+
const candidate = join23(quarantineDir, id);
|
|
4816
|
+
if (!existsSync18(candidate)) {
|
|
4504
4817
|
throw new UserError(`no held entry with id "${id}" \u2014 run \`skill-forge list\` to see what is in quarantine`);
|
|
4505
4818
|
}
|
|
4506
4819
|
const realRoot = realpathSync3(resolvePath4(quarantineDir));
|
|
@@ -4528,7 +4841,7 @@ function resolveTargetRoot(config, target) {
|
|
|
4528
4841
|
function heldSkillNames(skillDirs) {
|
|
4529
4842
|
return skillDirs.map((dir) => {
|
|
4530
4843
|
try {
|
|
4531
|
-
return readFrontmatter(
|
|
4844
|
+
return readFrontmatter(join23(dir, "SKILL.md")).name;
|
|
4532
4845
|
} catch {
|
|
4533
4846
|
return basename9(dir);
|
|
4534
4847
|
}
|
|
@@ -4536,7 +4849,7 @@ function heldSkillNames(skillDirs) {
|
|
|
4536
4849
|
}
|
|
4537
4850
|
function looksLikeMcpSandbox(quarantinePath) {
|
|
4538
4851
|
const candidate = resolveMcpCandidatePath({ quarantinePath });
|
|
4539
|
-
return [".mcp.json", "mcp.json", "package.json"].some((f) =>
|
|
4852
|
+
return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync18(join23(candidate, f)));
|
|
4540
4853
|
}
|
|
4541
4854
|
async function promoteHeldMcp(id, entry, config, options) {
|
|
4542
4855
|
const candidatePath = resolveMcpCandidatePath(entry);
|
|
@@ -4570,7 +4883,7 @@ async function promoteHeldMcp(id, entry, config, options) {
|
|
|
4570
4883
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
4571
4884
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
4572
4885
|
};
|
|
4573
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
4886
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join23(process.cwd(), ".mcp.json");
|
|
4574
4887
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
4575
4888
|
reject(entry);
|
|
4576
4889
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -4815,7 +5128,7 @@ var TOPICS = [
|
|
|
4815
5128
|
];
|
|
4816
5129
|
function readState() {
|
|
4817
5130
|
const configPath = getConfigPath();
|
|
4818
|
-
const configured =
|
|
5131
|
+
const configured = existsSync19(configPath);
|
|
4819
5132
|
const base = {
|
|
4820
5133
|
configured,
|
|
4821
5134
|
configPath,
|
|
@@ -5044,18 +5357,18 @@ async function status() {
|
|
|
5044
5357
|
|
|
5045
5358
|
// src/evolve.ts
|
|
5046
5359
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
5047
|
-
import { createHash as
|
|
5048
|
-
import { cpSync as cpSync3, existsSync as
|
|
5360
|
+
import { createHash as createHash3 } from "crypto";
|
|
5361
|
+
import { cpSync as cpSync3, existsSync as existsSync20, lstatSync as lstatSync3, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, rmSync as rmSync6 } from "fs";
|
|
5049
5362
|
import { tmpdir as tmpdir2 } from "os";
|
|
5050
|
-
import { basename as basename10, dirname as dirname9, join as
|
|
5363
|
+
import { basename as basename10, dirname as dirname9, join as join24, resolve as resolvePath5, sep as sep3 } from "path";
|
|
5051
5364
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
5052
5365
|
var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
|
|
5053
5366
|
async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
5054
5367
|
const exec = deps.exec ?? defaultExec;
|
|
5055
5368
|
const json = options.json;
|
|
5056
5369
|
const resolvedTargetDir = resolvePath5(targetSkillDir);
|
|
5057
|
-
const skillMdPath =
|
|
5058
|
-
if (!
|
|
5370
|
+
const skillMdPath = join24(resolvedTargetDir, "SKILL.md");
|
|
5371
|
+
if (!existsSync20(skillMdPath)) {
|
|
5059
5372
|
throw new UserError(`no SKILL.md found at ${skillMdPath} \u2014 evolve requires an existing, installed skill directory`);
|
|
5060
5373
|
}
|
|
5061
5374
|
const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
|
|
@@ -5113,33 +5426,33 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
5113
5426
|
}
|
|
5114
5427
|
return;
|
|
5115
5428
|
}
|
|
5116
|
-
const stagingRoot =
|
|
5429
|
+
const stagingRoot = join24(project, ".skillopt-sleep", "staging");
|
|
5117
5430
|
const stagingDir = resolvePath5(parsed.staging_dir);
|
|
5118
5431
|
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep3)) {
|
|
5119
5432
|
throw new UserError(
|
|
5120
5433
|
`skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
|
|
5121
5434
|
);
|
|
5122
5435
|
}
|
|
5123
|
-
const proposedSkillMd =
|
|
5124
|
-
const proposedClaudeMd =
|
|
5125
|
-
const reportMdPath =
|
|
5126
|
-
if (!
|
|
5436
|
+
const proposedSkillMd = join24(stagingDir, "proposed_SKILL.md");
|
|
5437
|
+
const proposedClaudeMd = join24(stagingDir, "proposed_CLAUDE.md");
|
|
5438
|
+
const reportMdPath = join24(stagingDir, "report.md");
|
|
5439
|
+
if (!existsSync20(proposedSkillMd)) {
|
|
5127
5440
|
throw new UserError(`skillopt-sleep reported an accepted proposal but ${proposedSkillMd} is missing`);
|
|
5128
5441
|
}
|
|
5129
|
-
if (
|
|
5442
|
+
if (existsSync20(reportMdPath)) {
|
|
5130
5443
|
notices.push(`SkillOpt-Sleep's full report: ${reportMdPath}`);
|
|
5131
5444
|
}
|
|
5132
5445
|
assertNotSymlink(proposedSkillMd, "proposed_SKILL.md");
|
|
5133
5446
|
assertNotSymlink(proposedClaudeMd, "proposed_CLAUDE.md");
|
|
5134
5447
|
const scannedSkillHash = sha256File(proposedSkillMd);
|
|
5135
5448
|
const scannedClaudeHash = sha256File(proposedClaudeMd);
|
|
5136
|
-
const tempDir = mkdtempSync3(
|
|
5449
|
+
const tempDir = mkdtempSync3(join24(tmpdir2(), "skill-forge-evolve-"));
|
|
5137
5450
|
let profile;
|
|
5138
5451
|
let safety;
|
|
5139
5452
|
try {
|
|
5140
|
-
cpSync3(proposedSkillMd,
|
|
5141
|
-
if (
|
|
5142
|
-
cpSync3(proposedClaudeMd,
|
|
5453
|
+
cpSync3(proposedSkillMd, join24(tempDir, "SKILL.md"));
|
|
5454
|
+
if (existsSync20(proposedClaudeMd)) {
|
|
5455
|
+
cpSync3(proposedClaudeMd, join24(tempDir, "CLAUDE.md"));
|
|
5143
5456
|
}
|
|
5144
5457
|
profile = asSkillProfile(profileSkill(tempDir), "for the evolved proposal");
|
|
5145
5458
|
const strictness = loadConfig().strictness;
|
|
@@ -5238,7 +5551,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
5238
5551
|
if (safetyVerdict === "block") process.exitCode = 1;
|
|
5239
5552
|
} else {
|
|
5240
5553
|
logOutcome(`[skill-forge] held \u2014 staging dir left in place -> ${stagingDir}`, json);
|
|
5241
|
-
if (
|
|
5554
|
+
if (existsSync20(reportMdPath)) {
|
|
5242
5555
|
logOutcome(`[skill-forge] see ${reportMdPath} for the full evidence report`, json);
|
|
5243
5556
|
}
|
|
5244
5557
|
}
|
|
@@ -5280,10 +5593,10 @@ function validateRunOutput(raw) {
|
|
|
5280
5593
|
return { accepted: Boolean(obj.accepted), staging_dir: stagingDirRaw ?? null };
|
|
5281
5594
|
}
|
|
5282
5595
|
function readManifest(stagingDir) {
|
|
5283
|
-
const manifestPath =
|
|
5596
|
+
const manifestPath = join24(stagingDir, "manifest.json");
|
|
5284
5597
|
let raw;
|
|
5285
5598
|
try {
|
|
5286
|
-
raw =
|
|
5599
|
+
raw = readFileSync16(manifestPath, "utf-8");
|
|
5287
5600
|
} catch (err) {
|
|
5288
5601
|
throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
|
|
5289
5602
|
}
|
|
@@ -5317,18 +5630,18 @@ function validateManifest(raw, manifestPath) {
|
|
|
5317
5630
|
};
|
|
5318
5631
|
}
|
|
5319
5632
|
function filesIdentical(a, b) {
|
|
5320
|
-
if (!
|
|
5321
|
-
return
|
|
5633
|
+
if (!existsSync20(a) || !existsSync20(b)) return false;
|
|
5634
|
+
return readFileSync16(a).equals(readFileSync16(b));
|
|
5322
5635
|
}
|
|
5323
5636
|
function isAlreadyAdopted(stagingDir, manifest) {
|
|
5324
|
-
const proposedSkillMd =
|
|
5325
|
-
const proposedClaudeMd =
|
|
5637
|
+
const proposedSkillMd = join24(stagingDir, "proposed_SKILL.md");
|
|
5638
|
+
const proposedClaudeMd = join24(stagingDir, "proposed_CLAUDE.md");
|
|
5326
5639
|
let sawComparison = false;
|
|
5327
|
-
if (
|
|
5640
|
+
if (existsSync20(proposedSkillMd)) {
|
|
5328
5641
|
sawComparison = true;
|
|
5329
5642
|
if (!filesIdentical(proposedSkillMd, manifest.live_skill_path)) return false;
|
|
5330
5643
|
}
|
|
5331
|
-
if (
|
|
5644
|
+
if (existsSync20(proposedClaudeMd) && manifest.live_memory_path) {
|
|
5332
5645
|
sawComparison = true;
|
|
5333
5646
|
if (!filesIdentical(proposedClaudeMd, manifest.live_memory_path)) return false;
|
|
5334
5647
|
}
|
|
@@ -5346,8 +5659,8 @@ function assertNotSymlink(path, label) {
|
|
|
5346
5659
|
}
|
|
5347
5660
|
}
|
|
5348
5661
|
function sha256File(path) {
|
|
5349
|
-
if (!
|
|
5350
|
-
return
|
|
5662
|
+
if (!existsSync20(path)) return null;
|
|
5663
|
+
return createHash3("sha256").update(readFileSync16(path)).digest("hex");
|
|
5351
5664
|
}
|
|
5352
5665
|
|
|
5353
5666
|
// src/commands/evolve.ts
|
|
@@ -5355,12 +5668,118 @@ async function evolveCommand(targetSkillDir, options = {}) {
|
|
|
5355
5668
|
await evolve(targetSkillDir, options);
|
|
5356
5669
|
}
|
|
5357
5670
|
|
|
5671
|
+
// src/commands/finding.ts
|
|
5672
|
+
import { existsSync as existsSync21 } from "fs";
|
|
5673
|
+
function requireSavedConfig() {
|
|
5674
|
+
if (!existsSync21(getConfigPath())) {
|
|
5675
|
+
throw new UserError(
|
|
5676
|
+
"no skill-forge config found \u2014 run `skill-forge init` first (`finding accept` re-runs the audit engine to resolve a live finding, and audit never runs against an invented default)."
|
|
5677
|
+
);
|
|
5678
|
+
}
|
|
5679
|
+
}
|
|
5680
|
+
function shortDate(iso) {
|
|
5681
|
+
return iso.slice(0, 10) || iso;
|
|
5682
|
+
}
|
|
5683
|
+
async function findingAcceptCommand(prefix, options = {}) {
|
|
5684
|
+
requireSavedConfig();
|
|
5685
|
+
if (prefix.length < MIN_FINGERPRINT_PREFIX_LENGTH) {
|
|
5686
|
+
throw new UserError(`prefix too short \u2014 use at least ${MIN_FINGERPRINT_PREFIX_LENGTH} hex chars (got "${prefix}")`);
|
|
5687
|
+
}
|
|
5688
|
+
const reason = options.reason;
|
|
5689
|
+
if (reason === void 0) {
|
|
5690
|
+
throw new UserError("--reason is required \u2014 a short note on why this finding is accepted");
|
|
5691
|
+
}
|
|
5692
|
+
validateAcceptReason(reason);
|
|
5693
|
+
const config = loadConfig();
|
|
5694
|
+
const { records: accepted } = readAcceptedFindings();
|
|
5695
|
+
const report = await auditConfiguredSet(config, { accepted });
|
|
5696
|
+
const activeMatches = resolveAmongFindings(prefix, report.hygiene);
|
|
5697
|
+
if (activeMatches.length > 1) {
|
|
5698
|
+
const list2 = activeMatches.map((f) => `${f.rule} @ ${f.target} (${f.fingerprint})`).join("; ");
|
|
5699
|
+
throw new UserError(`prefix "${prefix}" matches ${activeMatches.length} current findings \u2014 use a longer prefix: ${list2}`);
|
|
5700
|
+
}
|
|
5701
|
+
if (activeMatches.length === 0) {
|
|
5702
|
+
const alreadyMatches = resolveAmongRecords(prefix, accepted);
|
|
5703
|
+
if (alreadyMatches.length > 0) {
|
|
5704
|
+
const already = alreadyMatches[0];
|
|
5705
|
+
throw new UserError(`already accepted on ${shortDate(already.acceptedAt)}: ${already.reason}`);
|
|
5706
|
+
}
|
|
5707
|
+
throw new UserError(`no current finding matches "${prefix}" \u2014 run \`skill-forge audit\` and copy a fingerprint from the report`);
|
|
5708
|
+
}
|
|
5709
|
+
const finding2 = activeMatches[0];
|
|
5710
|
+
if (finding2.severity === "HIGH" || finding2.severity === "CRITICAL") {
|
|
5711
|
+
throw new UserError(
|
|
5712
|
+
`refusing to accept a ${finding2.severity} finding (${finding2.rule}) \u2014 acceptance is capped at LOW/MEDIUM, and there is no override flag. A HIGH/CRITICAL false positive is a rule bug to report/fix, not something to baseline.`
|
|
5713
|
+
);
|
|
5714
|
+
}
|
|
5715
|
+
const record = {
|
|
5716
|
+
fingerprint: finding2.fingerprint,
|
|
5717
|
+
severity: finding2.severity,
|
|
5718
|
+
rule: finding2.rule,
|
|
5719
|
+
target: finding2.target,
|
|
5720
|
+
context: finding2.context ?? finding2.detail,
|
|
5721
|
+
detail: finding2.detail,
|
|
5722
|
+
reason,
|
|
5723
|
+
acceptedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5724
|
+
source: "user"
|
|
5725
|
+
};
|
|
5726
|
+
writeAcceptedFindings([...accepted, record]);
|
|
5727
|
+
if (options.json) {
|
|
5728
|
+
console.log(JSON.stringify(record, null, 2));
|
|
5729
|
+
return;
|
|
5730
|
+
}
|
|
5731
|
+
console.log(`[skill-forge] accepted [${record.severity}] ${record.rule} \u2014 ${record.target}`);
|
|
5732
|
+
console.log(` fingerprint: ${record.fingerprint}`);
|
|
5733
|
+
console.log(` reason: ${record.reason}`);
|
|
5734
|
+
console.log(' this finding is excluded from active counts, but stays permanently visible under "Accepted findings" in every future report.');
|
|
5735
|
+
}
|
|
5736
|
+
async function findingRevokeCommand(prefix, options = {}) {
|
|
5737
|
+
if (prefix.length < MIN_FINGERPRINT_PREFIX_LENGTH) {
|
|
5738
|
+
throw new UserError(`prefix too short \u2014 use at least ${MIN_FINGERPRINT_PREFIX_LENGTH} hex chars (got "${prefix}")`);
|
|
5739
|
+
}
|
|
5740
|
+
const { records } = readAcceptedFindings();
|
|
5741
|
+
const matches = resolveAmongRecords(prefix, records);
|
|
5742
|
+
if (matches.length > 1) {
|
|
5743
|
+
const list2 = matches.map((r) => `${r.rule} @ ${r.target} (${r.fingerprint})`).join("; ");
|
|
5744
|
+
throw new UserError(`prefix "${prefix}" matches ${matches.length} accepted findings \u2014 use a longer prefix: ${list2}`);
|
|
5745
|
+
}
|
|
5746
|
+
if (matches.length === 0) {
|
|
5747
|
+
throw new UserError(`no accepted finding matches "${prefix}" \u2014 run \`skill-forge finding list\` to see what is accepted`);
|
|
5748
|
+
}
|
|
5749
|
+
const record = matches[0];
|
|
5750
|
+
writeAcceptedFindings(records.filter((r) => r.fingerprint !== record.fingerprint));
|
|
5751
|
+
if (options.json) {
|
|
5752
|
+
console.log(JSON.stringify(record, null, 2));
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
console.log(`[skill-forge] revoked [${record.severity}] ${record.rule} \u2014 ${record.target}`);
|
|
5756
|
+
}
|
|
5757
|
+
async function findingListCommand(options = {}) {
|
|
5758
|
+
const { records, notices } = readAcceptedFindings();
|
|
5759
|
+
for (const notice of notices) console.error(`[skill-forge] ${notice}`);
|
|
5760
|
+
const rows = options.stale ? records.filter((r) => !existsSync21(r.target)) : records;
|
|
5761
|
+
if (options.json) {
|
|
5762
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
5763
|
+
return;
|
|
5764
|
+
}
|
|
5765
|
+
if (rows.length === 0) {
|
|
5766
|
+
console.log(options.stale ? "[skill-forge] no stale accepted findings." : "[skill-forge] no accepted findings.");
|
|
5767
|
+
return;
|
|
5768
|
+
}
|
|
5769
|
+
console.log(`[skill-forge] accepted findings${options.stale ? " (stale \u2014 target no longer exists)" : ""}:`);
|
|
5770
|
+
for (const r of rows) {
|
|
5771
|
+
console.log(
|
|
5772
|
+
` [${r.severity}] ${sanitizeForMarkdown(r.rule)} \u2014 ${sanitizeForMarkdown(r.target)}: ${sanitizeForMarkdown(r.reason)} (fp ${r.fingerprint.slice(0, FINGERPRINT_DISPLAY_LENGTH)}, accepted ${shortDate(r.acceptedAt)})`
|
|
5773
|
+
);
|
|
5774
|
+
}
|
|
5775
|
+
}
|
|
5776
|
+
|
|
5358
5777
|
// src/commands/organize.ts
|
|
5359
5778
|
import { resolve as resolvePath7 } from "path";
|
|
5360
5779
|
|
|
5361
5780
|
// src/organize.ts
|
|
5362
|
-
import { existsSync as
|
|
5363
|
-
import { basename as basename11, join as
|
|
5781
|
+
import { existsSync as existsSync22, readFileSync as readFileSync17, readdirSync as readdirSync8, realpathSync as realpathSync4, statSync as statSync9 } from "fs";
|
|
5782
|
+
import { basename as basename11, join as join25, resolve as resolvePath6 } from "path";
|
|
5364
5783
|
var PRO_FEATURE_LABEL4 = "set-level organizer reports";
|
|
5365
5784
|
var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
|
|
5366
5785
|
var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated"]);
|
|
@@ -5373,7 +5792,7 @@ function listSubdirs2(dir) {
|
|
|
5373
5792
|
}
|
|
5374
5793
|
const out = [];
|
|
5375
5794
|
for (const entry of entries) {
|
|
5376
|
-
const full =
|
|
5795
|
+
const full = join25(dir, entry.name);
|
|
5377
5796
|
if (entry.isDirectory()) {
|
|
5378
5797
|
out.push(full);
|
|
5379
5798
|
continue;
|
|
@@ -5391,14 +5810,14 @@ function findSkillDirsUnderRoot2(root) {
|
|
|
5391
5810
|
const found = /* @__PURE__ */ new Set();
|
|
5392
5811
|
const topDirs = listSubdirs2(root);
|
|
5393
5812
|
for (const dir of topDirs) {
|
|
5394
|
-
if (
|
|
5813
|
+
if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
|
|
5395
5814
|
}
|
|
5396
|
-
for (const dir of listSubdirs2(
|
|
5397
|
-
if (
|
|
5815
|
+
for (const dir of listSubdirs2(join25(root, "skills"))) {
|
|
5816
|
+
if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
|
|
5398
5817
|
}
|
|
5399
5818
|
for (const topDir of topDirs) {
|
|
5400
|
-
for (const dir of listSubdirs2(
|
|
5401
|
-
if (
|
|
5819
|
+
for (const dir of listSubdirs2(join25(topDir, "skills"))) {
|
|
5820
|
+
if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
|
|
5402
5821
|
}
|
|
5403
5822
|
}
|
|
5404
5823
|
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
@@ -5467,7 +5886,7 @@ function loadUsageSnapshot(snapshotPath) {
|
|
|
5467
5886
|
const resolvedPath = resolvePath6(expandHome(snapshotPath));
|
|
5468
5887
|
let raw;
|
|
5469
5888
|
try {
|
|
5470
|
-
raw =
|
|
5889
|
+
raw = readFileSync17(resolvedPath, "utf-8");
|
|
5471
5890
|
} catch (err) {
|
|
5472
5891
|
return { usage: /* @__PURE__ */ new Map(), warning: `could not read usage snapshot (${err.message}); proceeding without usage` };
|
|
5473
5892
|
}
|
|
@@ -5573,10 +5992,10 @@ function buildRegistryEntries(candidates, warnings) {
|
|
|
5573
5992
|
}
|
|
5574
5993
|
if (seenRealPaths.has(realPath)) continue;
|
|
5575
5994
|
seenRealPaths.add(realPath);
|
|
5576
|
-
const skillMdPath =
|
|
5995
|
+
const skillMdPath = join25(realPath, "SKILL.md");
|
|
5577
5996
|
let text;
|
|
5578
5997
|
try {
|
|
5579
|
-
text =
|
|
5998
|
+
text = readFileSync17(skillMdPath, "utf-8");
|
|
5580
5999
|
} catch (err) {
|
|
5581
6000
|
warnings.push(`could not read ${skillMdPath}: ${err.message}`);
|
|
5582
6001
|
continue;
|
|
@@ -5982,7 +6401,93 @@ async function find(query, options = {}, deps = {}) {
|
|
|
5982
6401
|
|
|
5983
6402
|
// src/commands/watch.ts
|
|
5984
6403
|
import { realpathSync as realpathSync5 } from "fs";
|
|
5985
|
-
import { join as
|
|
6404
|
+
import { join as join26, resolve as resolvePath8 } from "path";
|
|
6405
|
+
|
|
6406
|
+
// src/gate/skillMapDrift.ts
|
|
6407
|
+
import { createHash as createHash4 } from "crypto";
|
|
6408
|
+
import { readFileSync as readFileSync18, existsSync as existsSync23 } from "fs";
|
|
6409
|
+
import { resolve as resolve7 } from "path";
|
|
6410
|
+
function sha256(text) {
|
|
6411
|
+
return createHash4("sha256").update(text, "utf-8").digest("hex");
|
|
6412
|
+
}
|
|
6413
|
+
function defaultReadLocalFile(path) {
|
|
6414
|
+
const resolved = resolve7(expandHome(path));
|
|
6415
|
+
if (!existsSync23(resolved)) return void 0;
|
|
6416
|
+
try {
|
|
6417
|
+
return readFileSync18(resolved, "utf-8");
|
|
6418
|
+
} catch {
|
|
6419
|
+
return void 0;
|
|
6420
|
+
}
|
|
6421
|
+
}
|
|
6422
|
+
async function defaultFetchUpstream(url) {
|
|
6423
|
+
try {
|
|
6424
|
+
const res = await fetch(url);
|
|
6425
|
+
if (!res.ok) return void 0;
|
|
6426
|
+
return await res.text();
|
|
6427
|
+
} catch {
|
|
6428
|
+
return void 0;
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
function isHttpUrl(value) {
|
|
6432
|
+
return /^https?:\/\//i.test(value.trim());
|
|
6433
|
+
}
|
|
6434
|
+
function upstreamLabel(node, edge) {
|
|
6435
|
+
return node?.url ?? node?.path ?? node?.name ?? node?.id ?? edge.to;
|
|
6436
|
+
}
|
|
6437
|
+
async function computeMapDrift(map, deps = {}) {
|
|
6438
|
+
const readLocalFile = deps.readLocalFile ?? defaultReadLocalFile;
|
|
6439
|
+
const fetchUpstream = deps.fetchUpstream ?? defaultFetchUpstream;
|
|
6440
|
+
const rows = [];
|
|
6441
|
+
for (const edge of edgesOfType(map, "fork-of")) {
|
|
6442
|
+
const localNode = nodeById(map, edge.from);
|
|
6443
|
+
const upstreamNode = nodeById(map, edge.to);
|
|
6444
|
+
const skillLabel = localNode?.name ?? localNode?.id ?? edge.from;
|
|
6445
|
+
const upstream = upstreamLabel(upstreamNode, edge);
|
|
6446
|
+
if (!localNode || !localNode.path || !localNode.contentHash) {
|
|
6447
|
+
rows.push({
|
|
6448
|
+
skill: skillLabel,
|
|
6449
|
+
upstream,
|
|
6450
|
+
status: "local-missing",
|
|
6451
|
+
detail: !localNode ? `fork-of edge "from" node not found in map: ${edge.from}` : 'local skill node missing "path" or "contentHash" \u2014 cannot compare'
|
|
6452
|
+
});
|
|
6453
|
+
continue;
|
|
6454
|
+
}
|
|
6455
|
+
const localResolved = resolve7(expandHome(localNode.path));
|
|
6456
|
+
if (!existsSync23(localResolved)) {
|
|
6457
|
+
rows.push({ skill: skillLabel, upstream, status: "local-missing", detail: `local path does not exist: ${localResolved}` });
|
|
6458
|
+
continue;
|
|
6459
|
+
}
|
|
6460
|
+
const upstreamLocation = upstreamNode?.url ?? upstreamNode?.path;
|
|
6461
|
+
if (!upstreamLocation) {
|
|
6462
|
+
rows.push({
|
|
6463
|
+
skill: skillLabel,
|
|
6464
|
+
upstream,
|
|
6465
|
+
status: "upstream-unreachable",
|
|
6466
|
+
detail: 'upstream node has no "url" or "path" to fetch/read'
|
|
6467
|
+
});
|
|
6468
|
+
continue;
|
|
6469
|
+
}
|
|
6470
|
+
const upstreamText = isHttpUrl(upstreamLocation) ? await fetchUpstream(upstreamLocation) : readLocalFile(upstreamLocation);
|
|
6471
|
+
if (upstreamText === void 0) {
|
|
6472
|
+
rows.push({ skill: skillLabel, upstream, status: "upstream-unreachable", detail: `could not fetch/read upstream: ${upstreamLocation}` });
|
|
6473
|
+
continue;
|
|
6474
|
+
}
|
|
6475
|
+
const upstreamHash = sha256(upstreamText);
|
|
6476
|
+
if (upstreamHash === localNode.contentHash) {
|
|
6477
|
+
rows.push({ skill: skillLabel, upstream, status: "in-sync", detail: `contentHash matches (${upstreamHash.slice(0, 12)}\u2026)` });
|
|
6478
|
+
} else {
|
|
6479
|
+
rows.push({
|
|
6480
|
+
skill: skillLabel,
|
|
6481
|
+
upstream,
|
|
6482
|
+
status: "drifted",
|
|
6483
|
+
detail: `recorded ${localNode.contentHash.slice(0, 12)}\u2026, upstream now hashes to ${upstreamHash.slice(0, 12)}\u2026`
|
|
6484
|
+
});
|
|
6485
|
+
}
|
|
6486
|
+
}
|
|
6487
|
+
return rows;
|
|
6488
|
+
}
|
|
6489
|
+
|
|
6490
|
+
// src/commands/watch.ts
|
|
5986
6491
|
var PRO_FEATURE_LABEL5 = "provenance drift watch (watch)";
|
|
5987
6492
|
function isGitUrl(source) {
|
|
5988
6493
|
const s = source.trim();
|
|
@@ -6018,7 +6523,7 @@ async function checkGitDrift(exec, url, ref) {
|
|
|
6018
6523
|
async function runWatch(config, options = {}, deps = {}) {
|
|
6019
6524
|
const proStatus = proFeatureStatus(PRO_FEATURE_LABEL5);
|
|
6020
6525
|
if (!proStatus.enabled) {
|
|
6021
|
-
return { roots: [], rows: [], warnings: [], notices: [proStatus.notice ?? "Pro feature (watch) locked."] };
|
|
6526
|
+
return { roots: [], rows: [], warnings: [], notices: [proStatus.notice ?? "Pro feature (watch) locked."], mapDrift: [] };
|
|
6022
6527
|
}
|
|
6023
6528
|
const notices = proStatus.notice ? [proStatus.notice] : [];
|
|
6024
6529
|
const exec = deps.exec ?? defaultExec;
|
|
@@ -6044,7 +6549,7 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
6044
6549
|
ledger = readProvenanceLedger(resolvedRoot);
|
|
6045
6550
|
} catch (err) {
|
|
6046
6551
|
const message = err instanceof Error ? err.message : String(err);
|
|
6047
|
-
const ledgerPath =
|
|
6552
|
+
const ledgerPath = join26(resolvedRoot, "SOURCES.md");
|
|
6048
6553
|
roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
|
|
6049
6554
|
warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
|
|
6050
6555
|
continue;
|
|
@@ -6084,7 +6589,19 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
6084
6589
|
rows.push({ ...rowBase, checkMethod: "git-ls-remote", driftStatus: status2, detail });
|
|
6085
6590
|
}
|
|
6086
6591
|
}
|
|
6087
|
-
|
|
6592
|
+
let mapDrift = [];
|
|
6593
|
+
if (options.skillMap) {
|
|
6594
|
+
try {
|
|
6595
|
+
const map = loadSkillMap(options.skillMap);
|
|
6596
|
+
mapDrift = await computeMapDrift(
|
|
6597
|
+
map,
|
|
6598
|
+
options.offline ? { fetchUpstream: async () => void 0 } : void 0
|
|
6599
|
+
);
|
|
6600
|
+
} catch (err) {
|
|
6601
|
+
warnings.push(`skill map at ${options.skillMap}: ${err.message}`);
|
|
6602
|
+
}
|
|
6603
|
+
}
|
|
6604
|
+
return { roots, rows, warnings, notices, mapDrift };
|
|
6088
6605
|
}
|
|
6089
6606
|
function printWatchTerminal(report) {
|
|
6090
6607
|
console.log("[skill-forge] watch \u2014 provenance drift check");
|
|
@@ -6122,6 +6639,13 @@ ${sanitizeForMarkdown(root.root)} (${sanitizeForMarkdown(root.realPath)})`);
|
|
|
6122
6639
|
console.log("\nWarnings:");
|
|
6123
6640
|
for (const w of report.warnings) console.log(` - ${sanitizeForMarkdown(w)}`);
|
|
6124
6641
|
}
|
|
6642
|
+
if (report.mapDrift.length > 0) {
|
|
6643
|
+
console.log("\nSkill-map fork-of drift:");
|
|
6644
|
+
for (const row of report.mapDrift) {
|
|
6645
|
+
console.log(` \u2022 ${sanitizeForMarkdown(row.skill)} <- ${sanitizeForMarkdown(row.upstream)}`);
|
|
6646
|
+
console.log(` ${row.status} \u2014 ${sanitizeForMarkdown(row.detail)}`);
|
|
6647
|
+
}
|
|
6648
|
+
}
|
|
6125
6649
|
console.log("\nCompare each stored ref to current upstream; re-run ingestion on any that moved.");
|
|
6126
6650
|
}
|
|
6127
6651
|
async function watchCommand(options = {}) {
|
|
@@ -6135,8 +6659,8 @@ async function watchCommand(options = {}) {
|
|
|
6135
6659
|
}
|
|
6136
6660
|
|
|
6137
6661
|
// src/commands/routine.ts
|
|
6138
|
-
import { existsSync as
|
|
6139
|
-
import { join as
|
|
6662
|
+
import { existsSync as existsSync24, rmSync as rmSync7, statSync as statSync10 } from "fs";
|
|
6663
|
+
import { join as join27 } from "path";
|
|
6140
6664
|
var PRO_FEATURE_LABEL6 = "the scheduled maintenance routine";
|
|
6141
6665
|
var SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
6142
6666
|
function parseFailOnFloor(failOn) {
|
|
@@ -6157,7 +6681,7 @@ function exceedsFloor(audit2, failOn) {
|
|
|
6157
6681
|
}
|
|
6158
6682
|
function resolveReportPath2(override, generatedAt) {
|
|
6159
6683
|
if (override) return override;
|
|
6160
|
-
return
|
|
6684
|
+
return join27(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
|
|
6161
6685
|
}
|
|
6162
6686
|
function runHousekeeping(config, options) {
|
|
6163
6687
|
const actions = [];
|
|
@@ -6166,7 +6690,7 @@ function runHousekeeping(config, options) {
|
|
|
6166
6690
|
const pruneDays = options.pruneDays ?? 30;
|
|
6167
6691
|
const cutoff = Date.now() - pruneDays * 864e5;
|
|
6168
6692
|
for (const id of listQuarantineIds(config.quarantineDir)) {
|
|
6169
|
-
const path =
|
|
6693
|
+
const path = join27(config.quarantineDir, id);
|
|
6170
6694
|
let mtime;
|
|
6171
6695
|
try {
|
|
6172
6696
|
mtime = statSync10(path).mtimeMs;
|
|
@@ -6204,7 +6728,7 @@ function runHousekeeping(config, options) {
|
|
|
6204
6728
|
}
|
|
6205
6729
|
for (const entry of entries) {
|
|
6206
6730
|
if (entry.status !== "pending") continue;
|
|
6207
|
-
if (!entry.installedPath ||
|
|
6731
|
+
if (!entry.installedPath || existsSync24(entry.installedPath)) continue;
|
|
6208
6732
|
if (apply) {
|
|
6209
6733
|
try {
|
|
6210
6734
|
updateQueueEntry(entry.id, { status: "dismissed" });
|
|
@@ -6237,6 +6761,7 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
|
|
|
6237
6761
|
"",
|
|
6238
6762
|
`- Skills: ${result.audit.skillCount} across ${audit2.summary.rootCount} configured root(s)`,
|
|
6239
6763
|
`- Findings: ${audit2.summary.findingCounts.CRITICAL ?? 0} critical, ${audit2.summary.findingCounts.HIGH ?? 0} high, ${audit2.summary.findingCounts.MEDIUM ?? 0} medium, ${audit2.summary.findingCounts.LOW ?? 0} low`,
|
|
6764
|
+
`- Accepted: ${audit2.summary.acceptedCount} (suppressed from the counts above; see Accepted findings)`,
|
|
6240
6765
|
`- Overlap clusters: ${result.audit.overlapClusters.length}${result.audit.overlapLocked ? ' (overlap analysis was locked \u2014 not "none found")' : ""}`,
|
|
6241
6766
|
`- Pending queue entries: ${result.pending.queue}`,
|
|
6242
6767
|
`- Held in quarantine: ${result.pending.quarantine}`
|
|
@@ -6247,6 +6772,10 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
|
|
|
6247
6772
|
if (organize) {
|
|
6248
6773
|
lines.push(`- Capability registry: ${result.organize?.skills ?? 0} skill(s) across ${result.organize?.roots ?? 0} root(s)`);
|
|
6249
6774
|
}
|
|
6775
|
+
if (result.notices.length > 0) {
|
|
6776
|
+
lines.push("", "## Notices", "");
|
|
6777
|
+
for (const item of result.notices) lines.push(`- ${sanitizeForMarkdown(item)}`);
|
|
6778
|
+
}
|
|
6250
6779
|
if (result.degraded.length > 0) {
|
|
6251
6780
|
lines.push("", "## Incomplete steps", "");
|
|
6252
6781
|
for (const item of result.degraded) lines.push(`- ${sanitizeForMarkdown(item)}`);
|
|
@@ -6262,9 +6791,18 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
|
|
|
6262
6791
|
}
|
|
6263
6792
|
if (audit2.hygiene.length > 0) {
|
|
6264
6793
|
lines.push("", "## Findings", "");
|
|
6265
|
-
for (const
|
|
6794
|
+
for (const finding2 of audit2.hygiene) {
|
|
6795
|
+
const fp = finding2.fingerprint ? finding2.fingerprint.slice(0, 12) : "unknown";
|
|
6796
|
+
lines.push(
|
|
6797
|
+
`- **${finding2.severity}** \`${sanitizeForMarkdown(finding2.rule)}\` \u2014 ${sanitizeForMarkdown(finding2.target)}: ${sanitizeForMarkdown(finding2.detail)} (fp ${fp})`
|
|
6798
|
+
);
|
|
6799
|
+
}
|
|
6800
|
+
}
|
|
6801
|
+
if (audit2.accepted.length > 0) {
|
|
6802
|
+
lines.push("", "## Accepted findings", "");
|
|
6803
|
+
for (const entry of audit2.accepted) {
|
|
6266
6804
|
lines.push(
|
|
6267
|
-
`- **${finding.severity}** \`${sanitizeForMarkdown(finding.rule)}\` \u2014 ${sanitizeForMarkdown(finding.target)}: ${sanitizeForMarkdown(
|
|
6805
|
+
`- **${entry.finding.severity}** \`${sanitizeForMarkdown(entry.finding.rule)}\` \u2014 ${sanitizeForMarkdown(entry.finding.target)}: ${sanitizeForMarkdown(entry.reason)} (accepted ${sanitizeForMarkdown(entry.acceptedAt.slice(0, 10))})`
|
|
6268
6806
|
);
|
|
6269
6807
|
}
|
|
6270
6808
|
}
|
|
@@ -6294,7 +6832,9 @@ async function routineCommand(options = {}) {
|
|
|
6294
6832
|
}
|
|
6295
6833
|
const config = loadConfig();
|
|
6296
6834
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6297
|
-
const
|
|
6835
|
+
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
6836
|
+
for (const notice of storeNotices) log(`[skill-forge] ${notice}`);
|
|
6837
|
+
const audit2 = await auditConfiguredSet(config, { accepted });
|
|
6298
6838
|
const auditState = summarizeAudit(audit2);
|
|
6299
6839
|
const degraded = [];
|
|
6300
6840
|
let watch;
|
|
@@ -6337,6 +6877,7 @@ async function routineCommand(options = {}) {
|
|
|
6337
6877
|
organize: organize ? { skills: organize.registry.count, roots: organize.registry.skillsRoots.length, notices: organize.notices } : void 0,
|
|
6338
6878
|
pending: { queue: pending.count, quarantine: quarantine2.count },
|
|
6339
6879
|
housekeeping,
|
|
6880
|
+
notices: storeNotices,
|
|
6340
6881
|
degraded
|
|
6341
6882
|
};
|
|
6342
6883
|
const failure = degraded.length > 0 ? `${degraded.length} step(s) did not complete: ${degraded.join("; ")}` : exceedsFloor(auditState, options.failOn ?? "none");
|
|
@@ -6355,6 +6896,7 @@ async function routineCommand(options = {}) {
|
|
|
6355
6896
|
console.log(
|
|
6356
6897
|
` findings: ${auditState.findings.CRITICAL ?? 0} critical / ${auditState.findings.HIGH ?? 0} high / ${auditState.findings.MEDIUM ?? 0} medium / ${auditState.findings.LOW ?? 0} low`
|
|
6357
6898
|
);
|
|
6899
|
+
console.log(` accepted: ${auditState.acceptedCount ?? 0} (suppressed from counts; see report)`);
|
|
6358
6900
|
if (result.watch) console.log(` drift: ${result.watch.moved} moved of ${result.watch.checked} tracked`);
|
|
6359
6901
|
if (result.organize) console.log(` registry: ${result.organize.skills} skill(s)`);
|
|
6360
6902
|
console.log(` pending: ${result.pending.queue} queue, ${result.pending.quarantine} quarantine`);
|
|
@@ -6504,14 +7046,14 @@ async function queueClose(id, options = {}) {
|
|
|
6504
7046
|
}
|
|
6505
7047
|
|
|
6506
7048
|
// src/commands/refine.ts
|
|
6507
|
-
import { createHash as
|
|
6508
|
-
import { chmodSync, existsSync as
|
|
6509
|
-
import { dirname as dirname13, isAbsolute as isAbsolute4, join as
|
|
7049
|
+
import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
|
|
7050
|
+
import { chmodSync, existsSync as existsSync27, lstatSync as lstatSync4, mkdirSync as mkdirSync9, readFileSync as readFileSync20, realpathSync as realpathSync9, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
7051
|
+
import { dirname as dirname13, isAbsolute as isAbsolute4, join as join30, relative as relative4, resolve as resolvePath12, sep as sep4 } from "path";
|
|
6510
7052
|
import { createInterface as createInterface6 } from "readline/promises";
|
|
6511
7053
|
|
|
6512
7054
|
// src/refine/context.ts
|
|
6513
|
-
import { existsSync as
|
|
6514
|
-
import { isAbsolute as isAbsolute3, join as
|
|
7055
|
+
import { existsSync as existsSync26, realpathSync as realpathSync8, statSync as statSync12 } from "fs";
|
|
7056
|
+
import { isAbsolute as isAbsolute3, join as join29, relative as relative3, resolve as resolvePath11 } from "path";
|
|
6515
7057
|
|
|
6516
7058
|
// src/refine/models.ts
|
|
6517
7059
|
import { randomBytes } from "crypto";
|
|
@@ -6567,40 +7109,40 @@ function fingerprintKey(skill, target, overrideType, action) {
|
|
|
6567
7109
|
return [normalize(skill), normalize(target), overrideType, action ?? ""].join("::");
|
|
6568
7110
|
}
|
|
6569
7111
|
function contentSimilarity(a, b) {
|
|
6570
|
-
const
|
|
6571
|
-
const tokensA =
|
|
6572
|
-
const tokensB =
|
|
7112
|
+
const tokenize3 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
|
|
7113
|
+
const tokensA = tokenize3(a);
|
|
7114
|
+
const tokensB = tokenize3(b);
|
|
6573
7115
|
if (tokensA.size === 0 || tokensB.size === 0) return 0;
|
|
6574
7116
|
let intersectionSize = 0;
|
|
6575
7117
|
for (const token of tokensA) {
|
|
6576
7118
|
if (tokensB.has(token)) intersectionSize++;
|
|
6577
7119
|
}
|
|
6578
|
-
const
|
|
6579
|
-
return
|
|
7120
|
+
const unionSize3 = tokensA.size + tokensB.size - intersectionSize;
|
|
7121
|
+
return unionSize3 === 0 ? 0 : intersectionSize / unionSize3;
|
|
6580
7122
|
}
|
|
6581
7123
|
|
|
6582
7124
|
// src/refine/store.ts
|
|
6583
|
-
import { createHash as
|
|
6584
|
-
import { existsSync as
|
|
6585
|
-
import { join as
|
|
7125
|
+
import { createHash as createHash5 } from "crypto";
|
|
7126
|
+
import { existsSync as existsSync25, readFileSync as readFileSync19, statSync as statSync11, realpathSync as realpathSync7 } from "fs";
|
|
7127
|
+
import { join as join28, resolve as resolvePath10 } from "path";
|
|
6586
7128
|
var SIMILARITY_JOIN_THRESHOLD = 0.7;
|
|
6587
7129
|
var READY_THRESHOLD = 2;
|
|
6588
7130
|
function getRefinementsDir() {
|
|
6589
|
-
return
|
|
7131
|
+
return join28(getHomeDir(), "refinements");
|
|
6590
7132
|
}
|
|
6591
7133
|
function getRefinementsPath() {
|
|
6592
|
-
return
|
|
7134
|
+
return join28(getRefinementsDir(), "refinements.json");
|
|
6593
7135
|
}
|
|
6594
7136
|
function getPatternsPath() {
|
|
6595
|
-
return
|
|
7137
|
+
return join28(getRefinementsDir(), "patterns.json");
|
|
6596
7138
|
}
|
|
6597
7139
|
function getBackupsDir() {
|
|
6598
|
-
return
|
|
7140
|
+
return join28(getRefinementsDir(), "backups");
|
|
6599
7141
|
}
|
|
6600
7142
|
function readJsonFile(path, label) {
|
|
6601
7143
|
let raw;
|
|
6602
7144
|
try {
|
|
6603
|
-
raw =
|
|
7145
|
+
raw = readFileSync19(path, "utf-8");
|
|
6604
7146
|
} catch (err) {
|
|
6605
7147
|
throw new UserError(`unable to read ${label} at ${path}: ${err.message}`);
|
|
6606
7148
|
}
|
|
@@ -6611,7 +7153,7 @@ function readJsonFile(path, label) {
|
|
|
6611
7153
|
}
|
|
6612
7154
|
}
|
|
6613
7155
|
function readStore(path, label, validateRecord) {
|
|
6614
|
-
if (!
|
|
7156
|
+
if (!existsSync25(path)) {
|
|
6615
7157
|
return { schemaVersion: 1, records: [] };
|
|
6616
7158
|
}
|
|
6617
7159
|
const parsed = readJsonFile(path, label);
|
|
@@ -6835,16 +7377,16 @@ function createBackup(filePaths, deps = {}) {
|
|
|
6835
7377
|
const randomHex = deps.randomHex ?? defaultRandomHex;
|
|
6836
7378
|
const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
|
|
6837
7379
|
const files = filePaths.map((path) => {
|
|
6838
|
-
if (!
|
|
7380
|
+
if (!existsSync25(path)) {
|
|
6839
7381
|
return { path, existed: false, sha256: null, mode: null, contentBase64: null };
|
|
6840
7382
|
}
|
|
6841
|
-
const bytes =
|
|
7383
|
+
const bytes = readFileSync19(path);
|
|
6842
7384
|
const mode = statSync11(path).mode & 511;
|
|
6843
|
-
const
|
|
6844
|
-
return { path, existed: true, sha256, mode, contentBase64: bytes.toString("base64") };
|
|
7385
|
+
const sha2562 = createHash5("sha256").update(bytes).digest("hex");
|
|
7386
|
+
return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
|
|
6845
7387
|
});
|
|
6846
7388
|
const manifest = { id, createdAt: now.toISOString(), files };
|
|
6847
|
-
writeJsonAtomic(
|
|
7389
|
+
writeJsonAtomic(join28(getBackupsDir(), id, "manifest.json"), manifest);
|
|
6848
7390
|
return manifest;
|
|
6849
7391
|
}
|
|
6850
7392
|
function validateBackupManifestShape(value, path) {
|
|
@@ -6893,8 +7435,8 @@ function readBackupManifest(id) {
|
|
|
6893
7435
|
if (!isValidBackupId(id)) {
|
|
6894
7436
|
throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
6895
7437
|
}
|
|
6896
|
-
const manifestPath =
|
|
6897
|
-
if (!
|
|
7438
|
+
const manifestPath = join28(getBackupsDir(), id, "manifest.json");
|
|
7439
|
+
if (!existsSync25(manifestPath)) {
|
|
6898
7440
|
throw new UserError(`no backup with id: ${id}`);
|
|
6899
7441
|
}
|
|
6900
7442
|
const parsed = readJsonFile(manifestPath, "backup manifest");
|
|
@@ -6928,20 +7470,20 @@ function isUnderRoot2(candidate, root) {
|
|
|
6928
7470
|
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
6929
7471
|
assertPlainSkillName(skill);
|
|
6930
7472
|
const resolvedCwd = resolvePath11(cwd);
|
|
6931
|
-
const localDir =
|
|
6932
|
-
const sharedDir =
|
|
6933
|
-
const local = { scope: "local", dir: localDir, exists:
|
|
6934
|
-
const shared = { scope: "shared", dir: sharedDir, exists:
|
|
7473
|
+
const localDir = join29(resolvedCwd, ".claude", "skills", skill);
|
|
7474
|
+
const sharedDir = join29(resolvedCwd, "skills", skill);
|
|
7475
|
+
const local = { scope: "local", dir: localDir, exists: existsSync26(localDir) };
|
|
7476
|
+
const shared = { scope: "shared", dir: sharedDir, exists: existsSync26(sharedDir) };
|
|
6935
7477
|
const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
|
|
6936
7478
|
return { local, shared, user };
|
|
6937
7479
|
}
|
|
6938
7480
|
function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
6939
7481
|
if (userRootFlag) {
|
|
6940
7482
|
const root = resolvePath11(userRootFlag);
|
|
6941
|
-
return { resolved: true, dir:
|
|
7483
|
+
return { resolved: true, dir: join29(root, skill), root: userRootFlag, reason: "flag" };
|
|
6942
7484
|
}
|
|
6943
7485
|
const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
|
|
6944
|
-
const candidates = config.skillsRoots.map((root) => ({ root, dir:
|
|
7486
|
+
const candidates = config.skillsRoots.map((root) => ({ root, dir: join29(resolveConfiguredRoot2(root), skill) }));
|
|
6945
7487
|
for (const candidate of candidates) {
|
|
6946
7488
|
const skillReal = realpathOrNull2(candidate.dir);
|
|
6947
7489
|
if (skillReal) {
|
|
@@ -6976,12 +7518,12 @@ function inventoryScopeDir(scope, dir) {
|
|
|
6976
7518
|
scope,
|
|
6977
7519
|
dir,
|
|
6978
7520
|
dirExists: isDirectory(dir),
|
|
6979
|
-
skillMd: isFile2(
|
|
6980
|
-
patchMd: isFile2(
|
|
6981
|
-
extendMd: isFile2(
|
|
6982
|
-
configJson: isFile2(
|
|
6983
|
-
hooksDir: isDirectory(
|
|
6984
|
-
scriptsDir: isDirectory(
|
|
7521
|
+
skillMd: isFile2(join29(dir, "SKILL.md")),
|
|
7522
|
+
patchMd: isFile2(join29(dir, "SKILL.patch.md")),
|
|
7523
|
+
extendMd: isFile2(join29(dir, "SKILL.extend.md")),
|
|
7524
|
+
configJson: isFile2(join29(dir, "skill-config.json")),
|
|
7525
|
+
hooksDir: isDirectory(join29(dir, "hooks")),
|
|
7526
|
+
scriptsDir: isDirectory(join29(dir, "scripts"))
|
|
6985
7527
|
};
|
|
6986
7528
|
}
|
|
6987
7529
|
function inventoryAllScopes(resolution) {
|
|
@@ -7261,7 +7803,7 @@ function sanitizeDiffForConsole(diff) {
|
|
|
7261
7803
|
}
|
|
7262
7804
|
function resolveContentOption(options) {
|
|
7263
7805
|
if (options.content !== void 0) return options.content;
|
|
7264
|
-
if (options.contentFile) return
|
|
7806
|
+
if (options.contentFile) return readFileSync20(resolvePath12(options.contentFile), "utf-8");
|
|
7265
7807
|
return void 0;
|
|
7266
7808
|
}
|
|
7267
7809
|
function validateCaptureOptions(options) {
|
|
@@ -7413,7 +7955,7 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
7413
7955
|
}
|
|
7414
7956
|
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
7415
7957
|
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
7416
|
-
const destPath =
|
|
7958
|
+
const destPath = join30(scopeDir, relPath);
|
|
7417
7959
|
const destDir = dirname13(destPath);
|
|
7418
7960
|
mkdirSync9(destDir, { recursive: true });
|
|
7419
7961
|
const realDestDir = realpathSync9(destDir);
|
|
@@ -7509,12 +8051,12 @@ Write ${preview.destPath}? [y/N] `);
|
|
|
7509
8051
|
}
|
|
7510
8052
|
function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
|
|
7511
8053
|
const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
|
|
7512
|
-
const probePath = probeRelPath ?
|
|
7513
|
-
const existingContent = probePath &&
|
|
8054
|
+
const probePath = probeRelPath ? join30(scopeDir, probeRelPath) : void 0;
|
|
8055
|
+
const existingContent = probePath && existsSync27(probePath) ? readFileSync20(probePath, "utf-8") : "";
|
|
7514
8056
|
const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
|
|
7515
8057
|
const relPath = rendered.relPath;
|
|
7516
|
-
const destPath =
|
|
7517
|
-
const before =
|
|
8058
|
+
const destPath = join30(scopeDir, relPath);
|
|
8059
|
+
const before = existsSync27(destPath) ? readFileSync20(destPath, "utf-8") : "";
|
|
7518
8060
|
const diff = previewDiff(before, rendered.content, destPath);
|
|
7519
8061
|
return { relPath, content: rendered.content, destPath, diff };
|
|
7520
8062
|
}
|
|
@@ -7611,16 +8153,16 @@ async function refineWhichCommand(skill, options = {}) {
|
|
|
7611
8153
|
}
|
|
7612
8154
|
function writePostWriteSidecar(backupId, files) {
|
|
7613
8155
|
const sidecar = { schemaVersion: 1, files };
|
|
7614
|
-
const dir =
|
|
8156
|
+
const dir = join30(getBackupsDir(), backupId);
|
|
7615
8157
|
mkdirSync9(dir, { recursive: true });
|
|
7616
|
-
writeFileSync9(
|
|
8158
|
+
writeFileSync9(join30(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
|
|
7617
8159
|
}
|
|
7618
8160
|
function readPostWriteSidecar(backupId) {
|
|
7619
|
-
const path =
|
|
7620
|
-
if (!
|
|
8161
|
+
const path = join30(getBackupsDir(), backupId, "post.json");
|
|
8162
|
+
if (!existsSync27(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
|
|
7621
8163
|
let raw;
|
|
7622
8164
|
try {
|
|
7623
|
-
raw =
|
|
8165
|
+
raw = readFileSync20(path, "utf-8");
|
|
7624
8166
|
} catch {
|
|
7625
8167
|
return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
|
|
7626
8168
|
}
|
|
@@ -7678,8 +8220,8 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
7678
8220
|
if (!isUnderRoot3(realSkillDir, realUserRoot)) {
|
|
7679
8221
|
throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
|
|
7680
8222
|
}
|
|
7681
|
-
const skillMdPath =
|
|
7682
|
-
if (!
|
|
8223
|
+
const skillMdPath = join30(skillDir, "SKILL.md");
|
|
8224
|
+
if (!existsSync27(skillMdPath)) {
|
|
7683
8225
|
throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
|
|
7684
8226
|
}
|
|
7685
8227
|
const refinementsStore = readRefinementsStore();
|
|
@@ -7696,7 +8238,7 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
7696
8238
|
`pattern ${patternId}'s representative refinement is a '${representative.overrideType}' override \u2014 promote currently only materializes patch/extend/new-type patterns against the user-scope SKILL.md; other override types must be merged manually.`
|
|
7697
8239
|
);
|
|
7698
8240
|
}
|
|
7699
|
-
const before =
|
|
8241
|
+
const before = readFileSync20(skillMdPath, "utf-8");
|
|
7700
8242
|
const after = applyPatchBlocks(before, [block]);
|
|
7701
8243
|
const diff = previewDiff(before, after, skillMdPath);
|
|
7702
8244
|
if (options.dryRun) {
|
|
@@ -7728,7 +8270,7 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
7728
8270
|
throw err;
|
|
7729
8271
|
}
|
|
7730
8272
|
try {
|
|
7731
|
-
const postSha256 =
|
|
8273
|
+
const postSha256 = createHash6("sha256").update(readFileSync20(skillMdPath)).digest("hex");
|
|
7732
8274
|
writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
|
|
7733
8275
|
markGeneralized(patternId, backup.id);
|
|
7734
8276
|
const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
@@ -7754,10 +8296,10 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
7754
8296
|
}
|
|
7755
8297
|
function assertBackupDirContained(backupId) {
|
|
7756
8298
|
const backupsDir = getBackupsDir();
|
|
7757
|
-
if (!
|
|
8299
|
+
if (!existsSync27(backupsDir)) return;
|
|
7758
8300
|
const realBackupsDir = realpathSync9(backupsDir);
|
|
7759
|
-
const backupDir =
|
|
7760
|
-
if (!
|
|
8301
|
+
const backupDir = join30(backupsDir, backupId);
|
|
8302
|
+
if (!existsSync27(backupDir)) return;
|
|
7761
8303
|
const realBackupDir = realpathSync9(backupDir);
|
|
7762
8304
|
if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
|
|
7763
8305
|
throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
|
|
@@ -7769,7 +8311,7 @@ function resolveTargetRealPath(path) {
|
|
|
7769
8311
|
try {
|
|
7770
8312
|
const real = realpathSync9(lexicalAncestor);
|
|
7771
8313
|
const remainder = relative4(lexicalAncestor, resolvePath12(path));
|
|
7772
|
-
return remainder === "" ? real :
|
|
8314
|
+
return remainder === "" ? real : join30(real, remainder);
|
|
7773
8315
|
} catch {
|
|
7774
8316
|
const parent = dirname13(lexicalAncestor);
|
|
7775
8317
|
if (parent === lexicalAncestor) return resolvePath12(path);
|
|
@@ -7838,7 +8380,7 @@ function walkRealComponents(filePath, config, createMissing) {
|
|
|
7838
8380
|
let current = rootReal;
|
|
7839
8381
|
for (const component of dirComponents) {
|
|
7840
8382
|
if (current === void 0) break;
|
|
7841
|
-
const next =
|
|
8383
|
+
const next = join30(current, component);
|
|
7842
8384
|
let lst;
|
|
7843
8385
|
try {
|
|
7844
8386
|
lst = lstatSync4(next);
|
|
@@ -7869,7 +8411,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
7869
8411
|
if (parentReal === void 0) {
|
|
7870
8412
|
throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
|
|
7871
8413
|
}
|
|
7872
|
-
const finalPath =
|
|
8414
|
+
const finalPath = join30(parentReal, leafComponent);
|
|
7873
8415
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
7874
8416
|
throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
|
|
7875
8417
|
}
|
|
@@ -7895,7 +8437,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
7895
8437
|
function safeDeleteTombstone(filePath, config) {
|
|
7896
8438
|
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
|
|
7897
8439
|
if (parentReal === void 0) return;
|
|
7898
|
-
const finalPath =
|
|
8440
|
+
const finalPath = join30(parentReal, leafComponent);
|
|
7899
8441
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
7900
8442
|
throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
|
|
7901
8443
|
}
|
|
@@ -7937,7 +8479,7 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
7937
8479
|
for (const file of manifest.files) {
|
|
7938
8480
|
const expected = sidecar.hashes.get(file.path);
|
|
7939
8481
|
if (expected === void 0) continue;
|
|
7940
|
-
const currentSha256 =
|
|
8482
|
+
const currentSha256 = existsSync27(file.path) ? createHash6("sha256").update(readFileSync20(file.path)).digest("hex") : null;
|
|
7941
8483
|
if (currentSha256 !== expected) mismatches.push(file.path);
|
|
7942
8484
|
}
|
|
7943
8485
|
if (mismatches.length > 0 && !options.force) {
|
|
@@ -8033,8 +8575,14 @@ program.name("skill-forge").description("The supply-chain gate for agent skills"
|
|
|
8033
8575
|
program.command("add <source>").description("Quarantine-install a skill and run it through the safety gate").option("-y, --yes", "skip the interactive prompt; honor the gate verdict automatically").option("-t, --target <dir>", "skills root to promote into (defaults to config defaultTarget, then skillsRoots[0])").option("--json", "print the gate result as JSON instead of the terminal report").option("--ingest", "after a successful promote, hand off the bundled ingest prompt to a coding agent").option("--artifact <type>", 'gate as a "skill" or an "mcp" server (default: skill)', parseArtifact).option(
|
|
8034
8576
|
"--mcp-target <file>",
|
|
8035
8577
|
"MCP config file to promote into (mcp artifacts only; defaults to config.mcpTargets[0], then <cwd>/.mcp.json)"
|
|
8036
|
-
).option("--force", "overwrite an existing same-name MCP server entry when promoting (mcp artifacts only)").
|
|
8037
|
-
|
|
8578
|
+
).option("--force", "overwrite an existing same-name MCP server entry when promoting (mcp artifacts only)").option(
|
|
8579
|
+
"--skill-map <path>",
|
|
8580
|
+
"path to a generated rhize-plugins skill map; reports existing-capability overlap against every skill node in it before promotion (curation-rule check)"
|
|
8581
|
+
).action(add);
|
|
8582
|
+
program.command("scan <source>").description("Run the safety gate against a skill without installing it").option("--json", "print the gate result as JSON instead of the terminal report").option("--artifact <type>", 'gate as a "skill" or an "mcp" server (default: skill)', parseArtifact).option(
|
|
8583
|
+
"--skill-map <path>",
|
|
8584
|
+
"path to a generated rhize-plugins skill map; reports existing-capability overlap against every skill node in it (curation-rule check)"
|
|
8585
|
+
).action(scan);
|
|
8038
8586
|
program.command("list").description("List skills currently in quarantine").action(list);
|
|
8039
8587
|
program.command("status").description("Show skill-forge configuration and quarantine status").action(status);
|
|
8040
8588
|
program.command("promote <id>").description("Re-gate a held quarantine entry and promote it (finishes a decision left on hold)").option("-t, --target <dir>", "skills root to promote into (defaults to config defaultTarget, then skillsRoots[0])").option("--force", "overwrite an existing same-name skill dir / MCP server entry in the target").option("--mcp-target <file>", "MCP config file to promote a held MCP candidate into (defaults to config.mcpTargets[0], then <cwd>/.mcp.json)").option("-y, --yes", "skip the prompt; honor the re-gate verdict automatically").option("--json", "print the re-gate result as JSON instead of the terminal report").action(promoteHeld);
|
|
@@ -8049,9 +8597,16 @@ configCmd.command("review").description("Review pending agent-proposed preferenc
|
|
|
8049
8597
|
program.command("guide [topic]").description("What this tool does, what state you are in, and the next command to run").action(guide);
|
|
8050
8598
|
program.command("evolve <target-skill-dir>").description("Self-evolve a skill via SkillOpt-Sleep, re-gate the proposal, then promote/hold/reject (Pro)").option("--project <dir>", "project dir passed to SkillOpt-Sleep as --project (defaults to cwd)").option("--dry-run", "SkillOpt-Sleep dry-run: report only, nothing staged").option("--backend <name>", "SkillOpt-Sleep backend (default: mock, offline \u2014 a real backend requires --yes or interactive confirmation)").option("--lookback-hours <n>", "hours of session history for SkillOpt-Sleep to harvest", parseLookbackHours).option("-y, --yes", "skip interactive prompts; honor the re-gate verdict automatically").option("--json", "print the re-gate result as JSON instead of the terminal report").option("--force", "allow re-adopting a staging dir whose proposed content already matches the live skill").action(evolveCommand);
|
|
8051
8599
|
program.command("audit").alias("doctor").description("Doctor-style health check over the configured skill/MCP set: inventory, hygiene findings, and consolidation/refinement opportunities").option("--json", "print the audit report as JSON instead of the terminal summary (non-interactive: skips the business-profile prompt)").option("-y, --yes", "skip interactive prompts (business profile, foundation, handoff); never implies --foundation or --handoff").option("--report <file>", "write the report to this path instead of the default ~/.skill-forge/reports/audit-<timestamp>.md (refuses an existing path)").option("--foundation", "authorizes exactly one skills-root write: scaffold a business-foundation skill from the captured business profile").option("--handoff", "authorizes launching the configured agent with the bundled curation prompt against the written report").action(auditCommand);
|
|
8600
|
+
var finding = program.command("finding").description("Review and acknowledge audit findings \u2014 human-only, never a gate input (see docs/accepted-findings-schema.md)");
|
|
8601
|
+
finding.command("accept <fingerprint-prefix>").description("Accept a currently-active LOW/MEDIUM finding (no override for HIGH/CRITICAL); re-runs the audit engine to resolve it").requiredOption("--reason <text>", "why this finding is accepted (mandatory, 1-200 chars, no control characters)").option("--json", "print the accepted record as JSON").action(findingAcceptCommand);
|
|
8602
|
+
finding.command("revoke <fingerprint-prefix>").description("Remove a stored acceptance so the finding reports again").option("--json", "print the removed record as JSON").action(findingRevokeCommand);
|
|
8603
|
+
finding.command("list", { isDefault: true }).description("List every accepted finding").option("--stale", "only records whose target no longer exists on disk").option("--json", "print records as JSON").action(findingListCommand);
|
|
8052
8604
|
program.command("organize").description("Build the set-level capability registry + dependency graph across configured skills roots (Pro)").option("--json", "print the registry+graph report as JSON instead of the terminal summary").option("--out <file>", "write the full report JSON to this path (refuses an existing path)").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON").action(organizeCommand);
|
|
8053
8605
|
program.command("find [query]").description("Discover skills via skills.sh and check partner security audits (requires VERCEL_OIDC_TOKEN)").option("--limit <n>", "max search results (default 10)", parseLimit).option("--audit <id>", "show partner audit verdicts (Socket/Snyk/\u2026) for a skills.sh id").option("--get <id>", "show detail + file tree for a skills.sh id").option("--curated", "list skills.sh curated skills").option("--json", "print the raw API payload as JSON").action(find);
|
|
8054
|
-
program.command("watch").description("Drift check across every SOURCES.md provenance ledger: which ingested upstreams have moved (Pro)").option("--offline", "skip all network checks; list entries for manual comparison only").option("--json", "print the drift report as JSON instead of the terminal summary").
|
|
8606
|
+
program.command("watch").description("Drift check across every SOURCES.md provenance ledger: which ingested upstreams have moved (Pro)").option("--offline", "skip all network checks; list entries for manual comparison only").option("--json", "print the drift report as JSON instead of the terminal summary").option(
|
|
8607
|
+
"--skill-map <path>",
|
|
8608
|
+
"also drift-check every fork-of edge in a generated rhize-plugins skill map (contentHash compare against upstream)"
|
|
8609
|
+
).action(watchCommand);
|
|
8055
8610
|
program.command("routine").description("One scheduled maintenance pass: audit + drift + registry, one report, cron-friendly exit codes (Pro)").option("--json", "print the routine result as JSON instead of the terminal summary").option("--offline", "skip every network check (drift entries are reported unchecked)").option("--skip-watch", "skip the drift check").option("--skip-organize", "skip the capability registry").option("--fail-on <level>", "exit 1 when a finding at or above this severity exists: none|low|medium|high|critical|any (default none)", parseFailOn).option("--housekeeping", "opt in to bounded writes: prune expired quarantine sandboxes, close orphaned queue entries").option("--prune-days <n>", "quarantine age threshold in days for --housekeeping (default 30)", parsePruneDays).option("--dry-run", "with --housekeeping: report what would change, change nothing").option("--handoff", "hand the written report to the configured agent (curation prompt)").option("--report <file>", "write the report here instead of ~/.skill-forge/reports/routine-<timestamp>.md (refuses an existing path)").action(routineCommand);
|
|
8056
8611
|
program.command("ingest").description(
|
|
8057
8612
|
"Hand the pending-ingestion queue to the configured coding agent for the decide/absorb pass (Pro). For a single new source, use `skill-forge add <source> --ingest` instead."
|