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