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