@rhize/skill-forge 0.16.0 → 0.17.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 +17 -5
- package/dist/cli.js +1013 -346
- package/dist/cli.js.map +1 -1
- 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 join24 } from "path";
|
|
9
9
|
|
|
10
10
|
// src/config.ts
|
|
11
11
|
import { chmodSync, existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
@@ -166,6 +166,57 @@ function looksHighEntropy(value) {
|
|
|
166
166
|
const uniqueChars = new Set(value).size;
|
|
167
167
|
return varietyCount >= 2 && uniqueChars / value.length > 0.4;
|
|
168
168
|
}
|
|
169
|
+
var STACK_TOKENS = /* @__PURE__ */ new Set([
|
|
170
|
+
"django",
|
|
171
|
+
"laravel",
|
|
172
|
+
"springboot",
|
|
173
|
+
"spring",
|
|
174
|
+
"quarkus",
|
|
175
|
+
"csharp",
|
|
176
|
+
"fsharp",
|
|
177
|
+
"golang",
|
|
178
|
+
"go",
|
|
179
|
+
"kotlin",
|
|
180
|
+
"python",
|
|
181
|
+
"perl",
|
|
182
|
+
"dotnet",
|
|
183
|
+
"rust",
|
|
184
|
+
"swift",
|
|
185
|
+
"react",
|
|
186
|
+
"vue",
|
|
187
|
+
"angular",
|
|
188
|
+
"svelte",
|
|
189
|
+
"nextjs",
|
|
190
|
+
"nuxt",
|
|
191
|
+
"flutter",
|
|
192
|
+
"dart",
|
|
193
|
+
"java",
|
|
194
|
+
"typescript",
|
|
195
|
+
"php",
|
|
196
|
+
"ruby",
|
|
197
|
+
"rails",
|
|
198
|
+
"fastapi",
|
|
199
|
+
"nestjs",
|
|
200
|
+
"express",
|
|
201
|
+
"node",
|
|
202
|
+
"deno",
|
|
203
|
+
"bun",
|
|
204
|
+
"ios",
|
|
205
|
+
"android"
|
|
206
|
+
]);
|
|
207
|
+
function isStackSiblingPair(a, b) {
|
|
208
|
+
const tokensA = a.split("-");
|
|
209
|
+
const tokensB = b.split("-");
|
|
210
|
+
if (tokensA.length !== tokensB.length) return false;
|
|
211
|
+
let diffCount = 0;
|
|
212
|
+
for (let i = 0; i < tokensA.length; i++) {
|
|
213
|
+
if (tokensA[i] === tokensB[i]) continue;
|
|
214
|
+
diffCount++;
|
|
215
|
+
if (diffCount > 1) return false;
|
|
216
|
+
if (!STACK_TOKENS.has(tokensA[i]) || !STACK_TOKENS.has(tokensB[i])) return false;
|
|
217
|
+
}
|
|
218
|
+
return diffCount === 1;
|
|
219
|
+
}
|
|
169
220
|
|
|
170
221
|
// src/preferences.ts
|
|
171
222
|
function markAccepted(value) {
|
|
@@ -410,7 +461,9 @@ function loadConfig() {
|
|
|
410
461
|
// Both are repaired on read (invalid/secret-shaped entries dropped) — config.json is a plain
|
|
411
462
|
// file anything can edit, and these render straight into a terminal. See src/preferences.ts.
|
|
412
463
|
preferences: normalizePreferences(parsed.preferences) ?? defaults.preferences,
|
|
413
|
-
proposedPreferences: normalizeProposals(parsed.proposedPreferences) ?? defaults.proposedPreferences
|
|
464
|
+
proposedPreferences: normalizeProposals(parsed.proposedPreferences) ?? defaults.proposedPreferences,
|
|
465
|
+
claudePlugins: parsed.claudePlugins ?? defaults.claudePlugins,
|
|
466
|
+
usageSnapshotPath: parsed.usageSnapshotPath ?? defaults.usageSnapshotPath
|
|
414
467
|
};
|
|
415
468
|
}
|
|
416
469
|
function saveConfig(config) {
|
|
@@ -1099,16 +1152,16 @@ import {
|
|
|
1099
1152
|
statSync as statSync6,
|
|
1100
1153
|
writeFileSync as writeFileSync5
|
|
1101
1154
|
} from "fs";
|
|
1102
|
-
import { basename as basename6, delimiter as delimiter3, extname as
|
|
1155
|
+
import { basename as basename6, delimiter as delimiter3, extname as extname4, join as join12, resolve as resolvePath2, sep as sep2 } from "path";
|
|
1103
1156
|
|
|
1104
1157
|
// src/gate/safety.ts
|
|
1105
1158
|
import { statSync as statSync4 } from "fs";
|
|
1106
1159
|
import { execFile } from "child_process";
|
|
1107
|
-
import { delimiter as delimiter2, join as join9 } from "path";
|
|
1160
|
+
import { delimiter as delimiter2, extname as extname2, join as join9 } from "path";
|
|
1108
1161
|
import { promisify } from "util";
|
|
1109
1162
|
var execFileAsync = promisify(execFile);
|
|
1110
1163
|
var RM_RF_TARGET_RE = /\brm\s+(?:-[a-z]+\s+)+("[^"]*"|'[^']*'|[^\s;|&)"'`]+)/i;
|
|
1111
|
-
function refineRmRf(line) {
|
|
1164
|
+
function refineRmRf(line, _ctx) {
|
|
1112
1165
|
const match = line.match(RM_RF_TARGET_RE);
|
|
1113
1166
|
if (!match) {
|
|
1114
1167
|
return { severity: "HIGH", message: "recursive force-delete (rm -rf) with an unparseable target \u2014 verify the path is scoped" };
|
|
@@ -1138,12 +1191,41 @@ function refineRmRf(line) {
|
|
|
1138
1191
|
}
|
|
1139
1192
|
return null;
|
|
1140
1193
|
}
|
|
1194
|
+
var DOC_CUE_RE = /\b(never|not|don't|do not|must not|reject|refuse|attack|dangerous|unsafe|avoid|warning)\b/i;
|
|
1195
|
+
var DOC_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".markdown"]);
|
|
1196
|
+
var NO_FENCES = [];
|
|
1197
|
+
function refineCurlPipeShell(line, ctx) {
|
|
1198
|
+
const base = { severity: "CRITICAL", message: "downloads a remote script and pipes it directly into a shell" };
|
|
1199
|
+
if (ctx.inFence) return base;
|
|
1200
|
+
if (!DOC_EXTENSIONS.has(extname2(ctx.file).toLowerCase())) return base;
|
|
1201
|
+
if (!DOC_CUE_RE.test(line)) return base;
|
|
1202
|
+
return { ...base, classification: "documentation-example" };
|
|
1203
|
+
}
|
|
1204
|
+
function computeFenceFlags(lines) {
|
|
1205
|
+
const flags = new Array(lines.length).fill(false);
|
|
1206
|
+
let closeRe = null;
|
|
1207
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1208
|
+
const line = lines[i].replace(/\r$/, "");
|
|
1209
|
+
if (closeRe === null) {
|
|
1210
|
+
const open = line.match(/^\s{0,3}(`{3,}|~{3,})/);
|
|
1211
|
+
if (open) {
|
|
1212
|
+
closeRe = new RegExp(`^\\s{0,3}${open[1][0]}{${open[1].length},}\\s*$`);
|
|
1213
|
+
flags[i] = true;
|
|
1214
|
+
}
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
flags[i] = true;
|
|
1218
|
+
if (closeRe.test(line)) closeRe = null;
|
|
1219
|
+
}
|
|
1220
|
+
return flags;
|
|
1221
|
+
}
|
|
1141
1222
|
var DENY_RULES = [
|
|
1142
1223
|
{
|
|
1143
1224
|
rule: "curl-pipe-shell",
|
|
1144
1225
|
severity: "CRITICAL",
|
|
1145
1226
|
pattern: /\b(curl|wget)\b[^\n|]*\|\s*(sudo\s+)?(bash|sh|zsh)\b/i,
|
|
1146
|
-
message: "downloads a remote script and pipes it directly into a shell"
|
|
1227
|
+
message: "downloads a remote script and pipes it directly into a shell",
|
|
1228
|
+
refine: refineCurlPipeShell
|
|
1147
1229
|
},
|
|
1148
1230
|
{
|
|
1149
1231
|
rule: "base64-obfuscated-exec",
|
|
@@ -1214,17 +1296,19 @@ function scanStaticRules(targetPath) {
|
|
|
1214
1296
|
const content = readTextFile(file);
|
|
1215
1297
|
if (content === void 0) continue;
|
|
1216
1298
|
const lines = content.split("\n");
|
|
1299
|
+
const fenceFlags = DOC_EXTENSIONS.has(extname2(file).toLowerCase()) ? computeFenceFlags(lines) : NO_FENCES;
|
|
1217
1300
|
for (const rule of DENY_RULES) {
|
|
1218
1301
|
for (let i = 0; i < lines.length; i++) {
|
|
1219
1302
|
if (rule.pattern.test(lines[i])) {
|
|
1220
|
-
const refined = rule.refine ? rule.refine(lines[i]) : { severity: rule.severity, message: rule.message };
|
|
1303
|
+
const refined = rule.refine ? rule.refine(lines[i], { inFence: fenceFlags[i], file }) : { severity: rule.severity, message: rule.message };
|
|
1221
1304
|
if (!refined) continue;
|
|
1222
1305
|
findings.push({
|
|
1223
1306
|
severity: refined.severity,
|
|
1224
1307
|
rule: rule.rule,
|
|
1225
1308
|
file,
|
|
1226
1309
|
detail: `line ${i + 1}: ${refined.message}`,
|
|
1227
|
-
context: lines[i].trim()
|
|
1310
|
+
context: lines[i].trim(),
|
|
1311
|
+
...refined.classification ? { classification: refined.classification } : {}
|
|
1228
1312
|
});
|
|
1229
1313
|
}
|
|
1230
1314
|
}
|
|
@@ -1279,7 +1363,7 @@ async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
|
1279
1363
|
|
|
1280
1364
|
// src/gate/mcpSafety.ts
|
|
1281
1365
|
import { statSync as statSync5 } from "fs";
|
|
1282
|
-
import { basename as basename4, extname as
|
|
1366
|
+
import { basename as basename4, extname as extname3 } from "path";
|
|
1283
1367
|
function isConfigOrExampleFile(file) {
|
|
1284
1368
|
const base = basename4(file).toLowerCase();
|
|
1285
1369
|
if (base.startsWith(".env")) return true;
|
|
@@ -1485,7 +1569,7 @@ function scanTextFile(file, content, allowCredentialCheck) {
|
|
|
1485
1569
|
function scanFileForMcpFindings(file) {
|
|
1486
1570
|
const content = readTextFile(file);
|
|
1487
1571
|
if (content === void 0) return [];
|
|
1488
|
-
if (
|
|
1572
|
+
if (extname3(file).toLowerCase() === ".json") {
|
|
1489
1573
|
const parsed = tryParseJson(content);
|
|
1490
1574
|
if (parsed !== void 0) return scanJsonConfig(file, parsed);
|
|
1491
1575
|
}
|
|
@@ -1523,14 +1607,14 @@ skill skills user claude code based comprehensive enhanced practical including i
|
|
|
1523
1607
|
`.split(/\s+/).filter(Boolean)
|
|
1524
1608
|
);
|
|
1525
1609
|
var DEFAULT_SET_THRESHOLD = 0.45;
|
|
1526
|
-
function splitLimited(text,
|
|
1610
|
+
function splitLimited(text, sep9, maxSplit) {
|
|
1527
1611
|
const parts = [];
|
|
1528
1612
|
let rest = text;
|
|
1529
1613
|
for (let i = 0; i < maxSplit; i++) {
|
|
1530
|
-
const idx = rest.indexOf(
|
|
1614
|
+
const idx = rest.indexOf(sep9);
|
|
1531
1615
|
if (idx === -1) break;
|
|
1532
1616
|
parts.push(rest.slice(0, idx));
|
|
1533
|
-
rest = rest.slice(idx +
|
|
1617
|
+
rest = rest.slice(idx + sep9.length);
|
|
1534
1618
|
}
|
|
1535
1619
|
parts.push(rest);
|
|
1536
1620
|
return parts;
|
|
@@ -1882,7 +1966,14 @@ function proFeatureStatus(feature, betaFree = BETA_FREE) {
|
|
|
1882
1966
|
// src/audit.ts
|
|
1883
1967
|
var OVERSIZED_SKILL_EST_TOKENS = 5e3;
|
|
1884
1968
|
function toAuditFindings(source) {
|
|
1885
|
-
return source.map((f) => ({
|
|
1969
|
+
return source.map((f) => ({
|
|
1970
|
+
severity: f.severity,
|
|
1971
|
+
rule: f.rule,
|
|
1972
|
+
target: f.file,
|
|
1973
|
+
detail: f.detail,
|
|
1974
|
+
context: f.context,
|
|
1975
|
+
...f.classification ? { classification: f.classification } : {}
|
|
1976
|
+
}));
|
|
1886
1977
|
}
|
|
1887
1978
|
function activeVerdictFor(findings, strictness) {
|
|
1888
1979
|
return verdictFor(
|
|
@@ -2034,13 +2125,14 @@ function parseStrictFrontmatter(text) {
|
|
|
2034
2125
|
function estimateTokens(text) {
|
|
2035
2126
|
return Math.ceil(Buffer.byteLength(text, "utf-8") / 4);
|
|
2036
2127
|
}
|
|
2037
|
-
function profileAuditedSkill(group, findings, unreadableSkillMdPaths) {
|
|
2128
|
+
function profileAuditedSkill(group, findings, unreadableSkillMdPaths, pluginId) {
|
|
2038
2129
|
const skillMdPath = join12(group.canonicalPath, "SKILL.md");
|
|
2039
2130
|
const base = {
|
|
2040
2131
|
path: group.canonicalPath,
|
|
2041
2132
|
skillMdPath,
|
|
2042
2133
|
root: group.root,
|
|
2043
|
-
aliasPaths: group.aliasPaths
|
|
2134
|
+
aliasPaths: group.aliasPaths,
|
|
2135
|
+
pluginId
|
|
2044
2136
|
};
|
|
2045
2137
|
let text;
|
|
2046
2138
|
let sizeBytes;
|
|
@@ -2101,7 +2193,7 @@ function enumerateMcpServers(parsed) {
|
|
|
2101
2193
|
}
|
|
2102
2194
|
function auditMcpTarget(targetPath, notices) {
|
|
2103
2195
|
const findings = [];
|
|
2104
|
-
const isToml =
|
|
2196
|
+
const isToml = extname4(targetPath).toLowerCase() === ".toml";
|
|
2105
2197
|
const format = isToml ? "toml" : "json";
|
|
2106
2198
|
if (!existsSync9(targetPath)) {
|
|
2107
2199
|
findings.push({
|
|
@@ -2209,6 +2301,122 @@ function toClusterMembers(members) {
|
|
|
2209
2301
|
}
|
|
2210
2302
|
return sorted;
|
|
2211
2303
|
}
|
|
2304
|
+
function annotateSiblingClusters(clusters, nameByPath) {
|
|
2305
|
+
return clusters.map((cluster) => {
|
|
2306
|
+
const names = cluster.members.map((p) => nameByPath.get(p) ?? basename6(p));
|
|
2307
|
+
let allSiblings = true;
|
|
2308
|
+
outer: for (let i = 0; i < names.length && allSiblings; i++) {
|
|
2309
|
+
for (let j = i + 1; j < names.length; j++) {
|
|
2310
|
+
if (!isStackSiblingPair(names[i], names[j])) {
|
|
2311
|
+
allSiblings = false;
|
|
2312
|
+
break outer;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
return allSiblings ? { ...cluster, siblingVariants: true, suggestedVerb: "DEFER" } : cluster;
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
function profilePluginSkills(pluginRoots, findings, unreadableSkillMdPaths) {
|
|
2320
|
+
const skills = [];
|
|
2321
|
+
for (const pr of pluginRoots) {
|
|
2322
|
+
for (const dir of pr.skillDirs) {
|
|
2323
|
+
const group = { realPath: dir, canonicalPath: dir, root: pr.installPath, aliasPaths: [] };
|
|
2324
|
+
skills.push(profileAuditedSkill(group, findings, unreadableSkillMdPaths, pr.pluginId));
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
return skills;
|
|
2328
|
+
}
|
|
2329
|
+
function attachContentHashes(skills) {
|
|
2330
|
+
for (const skill of skills) {
|
|
2331
|
+
try {
|
|
2332
|
+
skill.contentHash = fingerprintTree(skill.path).digest;
|
|
2333
|
+
} catch {
|
|
2334
|
+
skill.contentHash = void 0;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
function collapseIdenticalContent(skills, configuredRootOrder) {
|
|
2339
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
2340
|
+
for (const skill of skills) {
|
|
2341
|
+
if (skill.pluginId !== void 0 || skill.contentHash === void 0) continue;
|
|
2342
|
+
const group = byHash.get(skill.contentHash) ?? [];
|
|
2343
|
+
group.push(skill);
|
|
2344
|
+
byHash.set(skill.contentHash, group);
|
|
2345
|
+
}
|
|
2346
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
2347
|
+
let collapsedCount = 0;
|
|
2348
|
+
for (const group of byHash.values()) {
|
|
2349
|
+
if (group.length < 2) continue;
|
|
2350
|
+
const sorted = [...group].sort((a, b) => {
|
|
2351
|
+
const ai = configuredRootOrder.indexOf(a.root);
|
|
2352
|
+
const bi = configuredRootOrder.indexOf(b.root);
|
|
2353
|
+
if (ai !== bi) return ai - bi;
|
|
2354
|
+
return a.path.localeCompare(b.path);
|
|
2355
|
+
});
|
|
2356
|
+
const canonical = sorted[0];
|
|
2357
|
+
for (const duplicate of sorted.slice(1)) {
|
|
2358
|
+
if (!canonical.aliasPaths.includes(duplicate.path)) canonical.aliasPaths.push(duplicate.path);
|
|
2359
|
+
toRemove.add(duplicate);
|
|
2360
|
+
collapsedCount++;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
return { skills: skills.filter((s) => !toRemove.has(s)), collapsedCount };
|
|
2364
|
+
}
|
|
2365
|
+
function matchUsage(skill, usage, notices) {
|
|
2366
|
+
if (usage.has(skill.name)) return usage.get(skill.name);
|
|
2367
|
+
if (skill.pluginId) {
|
|
2368
|
+
const composite = `${skill.pluginId.split("@")[0]}:${skill.name}`;
|
|
2369
|
+
if (usage.has(composite)) return usage.get(composite);
|
|
2370
|
+
}
|
|
2371
|
+
const bareMatches = [...usage.keys()].filter((k) => (k.includes(":") ? k.slice(k.lastIndexOf(":") + 1) : k) === skill.name);
|
|
2372
|
+
if (bareMatches.length === 1) return usage.get(bareMatches[0]);
|
|
2373
|
+
if (bareMatches.length > 1) {
|
|
2374
|
+
notices.push(`usage snapshot: ambiguous bare name "${skill.name}" matches ${bareMatches.length} entries \u2014 skipping attribution`);
|
|
2375
|
+
}
|
|
2376
|
+
return void 0;
|
|
2377
|
+
}
|
|
2378
|
+
function isUnderInstallPath(target, installPath) {
|
|
2379
|
+
return target === installPath || target.startsWith(installPath + sep2);
|
|
2380
|
+
}
|
|
2381
|
+
function computePluginSummaries(pluginRoots, activeFindings, skills, usage) {
|
|
2382
|
+
return pluginRoots.map((pr) => {
|
|
2383
|
+
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2384
|
+
for (const f of activeFindings) {
|
|
2385
|
+
if (isUnderInstallPath(f.target, pr.installPath)) findingCounts[f.severity]++;
|
|
2386
|
+
}
|
|
2387
|
+
const skillCount = pr.skillDirs.length;
|
|
2388
|
+
const hasUsage = usage !== void 0;
|
|
2389
|
+
const exhaustive = usage?.exhaustive ?? false;
|
|
2390
|
+
const windowDays = usage?.windowDays;
|
|
2391
|
+
const observedSkillCount = hasUsage ? skills.filter((s) => s.pluginId === pr.pluginId && (s.usage ?? 0) > 0).length : void 0;
|
|
2392
|
+
let recommendation;
|
|
2393
|
+
const reasons = [];
|
|
2394
|
+
if (findingCounts.HIGH > 0 || findingCounts.CRITICAL > 0) {
|
|
2395
|
+
recommendation = "review";
|
|
2396
|
+
} else if (exhaustive && windowDays !== void 0 && windowDays >= 28 && skillCount > 0 && observedSkillCount === 0) {
|
|
2397
|
+
recommendation = "unobserved";
|
|
2398
|
+
} else if (exhaustive && (observedSkillCount ?? 0) >= 1) {
|
|
2399
|
+
recommendation = "keep";
|
|
2400
|
+
} else {
|
|
2401
|
+
recommendation = "unknown";
|
|
2402
|
+
if (skillCount === 0) reasons.push("no skills to observe");
|
|
2403
|
+
if (!hasUsage) reasons.push("no usage telemetry");
|
|
2404
|
+
else if (!exhaustive) reasons.push("usage snapshot is not exhaustive (top-50 only)");
|
|
2405
|
+
else if (windowDays === void 0 || windowDays < 28) reasons.push(`window ${windowDays ?? "unknown"} < 28 days`);
|
|
2406
|
+
if (reasons.length === 0) reasons.push("no usage telemetry");
|
|
2407
|
+
}
|
|
2408
|
+
return {
|
|
2409
|
+
pluginId: pr.pluginId,
|
|
2410
|
+
version: pr.version,
|
|
2411
|
+
installPath: pr.installPath,
|
|
2412
|
+
skillCount,
|
|
2413
|
+
findingCounts,
|
|
2414
|
+
observedSkillCount,
|
|
2415
|
+
recommendation,
|
|
2416
|
+
reasons: [...new Set(reasons)].sort()
|
|
2417
|
+
};
|
|
2418
|
+
}).sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
2419
|
+
}
|
|
2212
2420
|
function defaultWhich(cmd) {
|
|
2213
2421
|
const pathEnv = process.env.PATH ?? "";
|
|
2214
2422
|
const dirs = pathEnv.split(delimiter3).filter(Boolean);
|
|
@@ -2253,10 +2461,16 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2253
2461
|
const oversizedThreshold = options.oversizedSkillTokens ?? OVERSIZED_SKILL_EST_TOKENS;
|
|
2254
2462
|
const findings = [];
|
|
2255
2463
|
const notices = [];
|
|
2464
|
+
const pluginRoots = options.pluginRoots ?? [];
|
|
2256
2465
|
const { candidates, rootMeta } = collectSkillCandidates(config.skillsRoots);
|
|
2257
2466
|
const groups = dedupeSkillCandidates(candidates, findings);
|
|
2258
2467
|
const unreadableSkillMdPaths = /* @__PURE__ */ new Set();
|
|
2259
|
-
const
|
|
2468
|
+
const configuredSkills = groups.map((g) => profileAuditedSkill(g, findings, unreadableSkillMdPaths));
|
|
2469
|
+
const pluginSkills = profilePluginSkills(pluginRoots, findings, unreadableSkillMdPaths);
|
|
2470
|
+
let skills = [...configuredSkills, ...pluginSkills];
|
|
2471
|
+
attachContentHashes(skills);
|
|
2472
|
+
const collapsed = collapseIdenticalContent(skills, config.skillsRoots);
|
|
2473
|
+
skills = collapsed.skills;
|
|
2260
2474
|
const skillRoots = config.skillsRoots.map((root) => {
|
|
2261
2475
|
const meta = rootMeta.get(root) ?? { exists: false };
|
|
2262
2476
|
if (!meta.exists) {
|
|
@@ -2273,9 +2487,20 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2273
2487
|
root,
|
|
2274
2488
|
realPath: meta.realPath ?? resolveConfiguredRoot(root),
|
|
2275
2489
|
exists: meta.exists,
|
|
2276
|
-
skillCount: skills.filter((s) => s.root === root).length
|
|
2490
|
+
skillCount: skills.filter((s) => s.root === root).length,
|
|
2491
|
+
origin: "configured"
|
|
2277
2492
|
};
|
|
2278
2493
|
});
|
|
2494
|
+
for (const pr of pluginRoots) {
|
|
2495
|
+
skillRoots.push({
|
|
2496
|
+
root: pr.installPath,
|
|
2497
|
+
realPath: pr.installPath,
|
|
2498
|
+
exists: isDir(pr.installPath),
|
|
2499
|
+
skillCount: pr.skillDirs.length,
|
|
2500
|
+
origin: "plugin",
|
|
2501
|
+
pluginId: pr.pluginId
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2279
2504
|
const byName = /* @__PURE__ */ new Map();
|
|
2280
2505
|
for (const skill of skills) {
|
|
2281
2506
|
const arr = byName.get(skill.name) ?? [];
|
|
@@ -2284,13 +2509,15 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2284
2509
|
}
|
|
2285
2510
|
for (const [name, group] of byName) {
|
|
2286
2511
|
if (group.length > 1) {
|
|
2512
|
+
const isIdenticalPluginPair = group.length === 2 && group.every((s) => s.pluginId !== void 0) && group[0].contentHash !== void 0 && group[0].contentHash === group[1].contentHash;
|
|
2513
|
+
const severity = isIdenticalPluginPair ? "LOW" : "MEDIUM";
|
|
2287
2514
|
for (const skill of group) {
|
|
2288
2515
|
const otherPaths = group.filter((g) => g.path !== skill.path).map((g) => g.path);
|
|
2289
2516
|
findings.push({
|
|
2290
|
-
severity
|
|
2517
|
+
severity,
|
|
2291
2518
|
rule: "duplicate-skill-name",
|
|
2292
2519
|
target: skill.path,
|
|
2293
|
-
detail: `name "${name}" is duplicated \u2014 also found at ${otherPaths.join(", ")}`
|
|
2520
|
+
detail: isIdenticalPluginPair ? `identical content shipped by two enabled plugins \u2014 also found at ${otherPaths.join(", ")}` : `name "${name}" is duplicated \u2014 also found at ${otherPaths.join(", ")}`
|
|
2294
2521
|
});
|
|
2295
2522
|
}
|
|
2296
2523
|
}
|
|
@@ -2340,9 +2567,10 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2340
2567
|
if (overlapStatus.enabled) {
|
|
2341
2568
|
if (overlapStatus.notice) notices.push(overlapStatus.notice);
|
|
2342
2569
|
try {
|
|
2343
|
-
const readableSkillMdPaths = skills.map((s) => s.skillMdPath).filter((p) => !unreadableSkillMdPaths.has(p));
|
|
2570
|
+
const readableSkillMdPaths = skills.filter((s) => s.pluginId === void 0).map((s) => s.skillMdPath).filter((p) => !unreadableSkillMdPaths.has(p));
|
|
2344
2571
|
const pairs = scanOverlapPaths(readableSkillMdPaths, options.overlapThreshold);
|
|
2345
|
-
|
|
2572
|
+
const nameByPath = new Map(skills.map((s) => [s.skillMdPath, s.name]));
|
|
2573
|
+
overlapClusters = annotateSiblingClusters(clusterOverlapPairs(pairs), nameByPath);
|
|
2346
2574
|
} catch (err) {
|
|
2347
2575
|
findings.push({
|
|
2348
2576
|
severity: "MEDIUM",
|
|
@@ -2381,7 +2609,7 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2381
2609
|
}
|
|
2382
2610
|
const activeFindingSet = new Set(activeFindings);
|
|
2383
2611
|
const evolveEligible = skills.filter((s) => {
|
|
2384
|
-
if (!s.validFrontmatter || skillScanFailed.has(s.path)) return false;
|
|
2612
|
+
if (!s.validFrontmatter || skillScanFailed.has(s.path) || s.pluginId !== void 0) return false;
|
|
2385
2613
|
const skillFindings = perSkillFindings.get(s.path) ?? [];
|
|
2386
2614
|
const activeSkillFindings = skillFindings.filter((f) => activeFindingSet.has(f));
|
|
2387
2615
|
return activeVerdictFor(activeSkillFindings, config.strictness) === "pass";
|
|
@@ -2394,6 +2622,18 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2394
2622
|
available: true,
|
|
2395
2623
|
message: "no business-foundation skill yet \u2014 `skill-forge audit --foundation` can scaffold one to ground future skills in your business profile"
|
|
2396
2624
|
};
|
|
2625
|
+
const usage = options.usage;
|
|
2626
|
+
if (usage) {
|
|
2627
|
+
for (const skill of skills) {
|
|
2628
|
+
const matched = matchUsage(skill, usage.usage, notices);
|
|
2629
|
+
if (matched !== void 0) skill.usage = matched;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
const usageJoined = usage ? skills.filter((s) => s.usage !== void 0).length : void 0;
|
|
2633
|
+
if (usage && usageJoined === 0) {
|
|
2634
|
+
notices.push(`usage snapshot (${usage.sourcePath}) joined 0 of ${skills.length} skill(s)`);
|
|
2635
|
+
}
|
|
2636
|
+
const unobservedInWindow = usage && usage.exhaustive ? skills.filter((s) => (s.usage ?? 0) === 0).map((s) => ({ name: s.name, path: s.path, pluginId: s.pluginId })) : [];
|
|
2397
2637
|
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2398
2638
|
for (const f of activeFindings) findingCounts[f.severity]++;
|
|
2399
2639
|
const summary = {
|
|
@@ -2403,8 +2643,12 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2403
2643
|
findingCounts,
|
|
2404
2644
|
overlapClusterCount: overlapClusters.length,
|
|
2405
2645
|
evolveEligibleCount: evolveEligible.length,
|
|
2406
|
-
acceptedCount: acceptedEntries.length
|
|
2646
|
+
acceptedCount: acceptedEntries.length,
|
|
2647
|
+
collapsedDuplicateCount: collapsed.collapsedCount,
|
|
2648
|
+
documentationExampleCount: activeFindings.filter((f) => f.classification === "documentation-example").length,
|
|
2649
|
+
...usage ? { usageJoined, usageExhaustive: usage.exhaustive, usageWindowDays: usage.windowDays } : {}
|
|
2407
2650
|
};
|
|
2651
|
+
const plugins = options.pluginRoots ? computePluginSummaries(pluginRoots, activeFindings, skills, usage) : void 0;
|
|
2408
2652
|
const tooling = checkTooling(options.which ?? defaultWhich, options.env ?? process.env);
|
|
2409
2653
|
return {
|
|
2410
2654
|
schemaVersion: 1,
|
|
@@ -2412,10 +2656,11 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2412
2656
|
inventory: { skillRoots, skills, mcpTargets },
|
|
2413
2657
|
hygiene: activeFindings,
|
|
2414
2658
|
accepted: acceptedEntries,
|
|
2415
|
-
opportunities: { overlapClusters, evolveEligible, foundation, overlapLocked: !overlapStatus.enabled },
|
|
2659
|
+
opportunities: { overlapClusters, evolveEligible, foundation, overlapLocked: !overlapStatus.enabled, unobservedInWindow },
|
|
2416
2660
|
tooling,
|
|
2417
2661
|
notices,
|
|
2418
|
-
summary
|
|
2662
|
+
summary,
|
|
2663
|
+
plugins
|
|
2419
2664
|
};
|
|
2420
2665
|
}
|
|
2421
2666
|
function bulletList(items, emptyPlaceholder) {
|
|
@@ -2432,7 +2677,9 @@ function renderRootsSection(roots) {
|
|
|
2432
2677
|
"### Skill roots",
|
|
2433
2678
|
"",
|
|
2434
2679
|
...bulletList(
|
|
2435
|
-
roots.map(
|
|
2680
|
+
roots.map(
|
|
2681
|
+
(r) => `${r.root}${r.exists ? "" : " (MISSING)"} \u2014 ${r.skillCount} skill${r.skillCount === 1 ? "" : "s"}` + (r.origin === "plugin" ? ` (plugin ${sanitizeForMarkdown(r.pluginId ?? "")})` : "")
|
|
2682
|
+
),
|
|
2436
2683
|
"(none configured)"
|
|
2437
2684
|
)
|
|
2438
2685
|
];
|
|
@@ -2476,17 +2723,17 @@ function renderMcpTargetsSection(targets) {
|
|
|
2476
2723
|
function displayFingerprint(f) {
|
|
2477
2724
|
return f.fingerprint ? f.fingerprint.slice(0, FINGERPRINT_DISPLAY_LENGTH) : "unknown";
|
|
2478
2725
|
}
|
|
2726
|
+
function renderFindingLine(f) {
|
|
2727
|
+
return `[${f.severity}] ${f.rule} \u2014 ${sanitizeForMarkdown(f.target)}: ${sanitizeForMarkdown(f.detail)} (fp ${displayFingerprint(f)})`;
|
|
2728
|
+
}
|
|
2479
2729
|
function renderFindingsSection(findings) {
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
"(none \u2014 clean)"
|
|
2488
|
-
)
|
|
2489
|
-
];
|
|
2730
|
+
const live = findings.filter((f) => f.classification !== "documentation-example");
|
|
2731
|
+
const docExamples = findings.filter((f) => f.classification === "documentation-example");
|
|
2732
|
+
const lines = ["## Hygiene findings", "", ...bulletList(live.map(renderFindingLine), "(none \u2014 clean)")];
|
|
2733
|
+
if (docExamples.length > 0) {
|
|
2734
|
+
lines.push("", "### Probable documentation examples (still CRITICAL \u2014 review)", "", ...bulletList(docExamples.map(renderFindingLine), ""));
|
|
2735
|
+
}
|
|
2736
|
+
return lines;
|
|
2490
2737
|
}
|
|
2491
2738
|
function renderAcceptedFindingsSection(entries) {
|
|
2492
2739
|
return [
|
|
@@ -2508,7 +2755,9 @@ function renderOpportunitiesSection(report) {
|
|
|
2508
2755
|
"### Overlap clusters",
|
|
2509
2756
|
"",
|
|
2510
2757
|
...bulletList(
|
|
2511
|
-
overlapClusters.map(
|
|
2758
|
+
overlapClusters.map(
|
|
2759
|
+
(c) => `top score ${c.topScore} \u2014 ${c.suggestedVerb} \u2014 ${c.members.join(", ")}` + (c.siblingVariants ? " (stack variants \u2014 deliberate siblings, not duplicates)" : "")
|
|
2760
|
+
),
|
|
2512
2761
|
"(none \u2014 either no cross-root overlap found, or the Pro feature is locked, see Notices)"
|
|
2513
2762
|
),
|
|
2514
2763
|
"",
|
|
@@ -2524,6 +2773,39 @@ function renderOpportunitiesSection(report) {
|
|
|
2524
2773
|
`- ${foundation.message}`
|
|
2525
2774
|
];
|
|
2526
2775
|
}
|
|
2776
|
+
function renderUsageSection(report) {
|
|
2777
|
+
const { summary } = report;
|
|
2778
|
+
const lines = ["## Usage", ""];
|
|
2779
|
+
lines.push(`- Matched: ${summary.usageJoined ?? 0} of ${summary.skillCount} skill(s)`);
|
|
2780
|
+
lines.push(`- Window: ${summary.usageWindowDays !== void 0 ? `${summary.usageWindowDays} day(s)` : "unknown"}`);
|
|
2781
|
+
if (summary.usageExhaustive) {
|
|
2782
|
+
lines.push("- Coverage: exhaustive (every skill counted this window)");
|
|
2783
|
+
} else {
|
|
2784
|
+
lines.push("- Coverage: top-50 only; absence is not zero usage");
|
|
2785
|
+
}
|
|
2786
|
+
if (report.opportunities.unobservedInWindow.length > 0) {
|
|
2787
|
+
lines.push("", "### Unobserved this window", "");
|
|
2788
|
+
for (const s of report.opportunities.unobservedInWindow) {
|
|
2789
|
+
lines.push(`- ${sanitizeForMarkdown(s.name)} \u2014 ${s.path}${s.pluginId ? ` (plugin ${sanitizeForMarkdown(s.pluginId)})` : ""}`);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
return lines;
|
|
2793
|
+
}
|
|
2794
|
+
function renderPluginsSection(plugins) {
|
|
2795
|
+
return [
|
|
2796
|
+
"## Plugins",
|
|
2797
|
+
"",
|
|
2798
|
+
...bulletList(
|
|
2799
|
+
plugins.map((p) => {
|
|
2800
|
+
const findingsPart = `${p.findingCounts.CRITICAL} CRITICAL, ${p.findingCounts.HIGH} HIGH, ${p.findingCounts.MEDIUM} MEDIUM, ${p.findingCounts.LOW} LOW`;
|
|
2801
|
+
const observedPart = p.observedSkillCount !== void 0 ? `, observed ${p.observedSkillCount}` : "";
|
|
2802
|
+
const reasonsPart = p.reasons.length > 0 ? ` (${p.reasons.join("; ")})` : "";
|
|
2803
|
+
return `${sanitizeForMarkdown(p.pluginId)} @ ${sanitizeForMarkdown(p.version)} \u2014 ${sanitizeForMarkdown(p.installPath)} \u2014 ${p.skillCount} skill(s)${observedPart} \u2014 findings: ${findingsPart} \u2014 **${p.recommendation}**${reasonsPart}`;
|
|
2804
|
+
}),
|
|
2805
|
+
"(no enabled plugins)"
|
|
2806
|
+
)
|
|
2807
|
+
];
|
|
2808
|
+
}
|
|
2527
2809
|
function renderToolingSection(tooling) {
|
|
2528
2810
|
const lines = ["## Tooling", ""];
|
|
2529
2811
|
if (!tooling) {
|
|
@@ -2560,6 +2842,7 @@ function renderAuditMarkdown(report) {
|
|
|
2560
2842
|
`- Accepted: ${summary.acceptedCount} (suppressed from the counts above; see the Accepted findings section)`,
|
|
2561
2843
|
`- Overlap clusters: ${summary.overlapClusterCount}`,
|
|
2562
2844
|
`- Evolve-eligible skills: ${summary.evolveEligibleCount}`,
|
|
2845
|
+
`- Collapsed duplicates: ${summary.collapsedDuplicateCount}`,
|
|
2563
2846
|
"",
|
|
2564
2847
|
"## Inventory",
|
|
2565
2848
|
"",
|
|
@@ -2575,12 +2858,11 @@ function renderAuditMarkdown(report) {
|
|
|
2575
2858
|
"",
|
|
2576
2859
|
...renderOpportunitiesSection(report),
|
|
2577
2860
|
"",
|
|
2578
|
-
...renderToolingSection(report.tooling)
|
|
2579
|
-
"",
|
|
2580
|
-
"## Notices",
|
|
2581
|
-
"",
|
|
2582
|
-
...bulletList(report.notices, "(none)")
|
|
2861
|
+
...renderToolingSection(report.tooling)
|
|
2583
2862
|
];
|
|
2863
|
+
if (summary.usageJoined !== void 0) lines.push("", ...renderUsageSection(report));
|
|
2864
|
+
if (report.plugins) lines.push("", ...renderPluginsSection(report.plugins));
|
|
2865
|
+
lines.push("", "## Notices", "", ...bulletList(report.notices, "(none)"));
|
|
2584
2866
|
return lines.join("\n") + "\n";
|
|
2585
2867
|
}
|
|
2586
2868
|
function renderFoundationSkillMd(profile) {
|
|
@@ -2811,11 +3093,11 @@ import {
|
|
|
2811
3093
|
statSync as statSync7
|
|
2812
3094
|
} from "fs";
|
|
2813
3095
|
import { tmpdir } from "os";
|
|
2814
|
-
import { basename as basename8, dirname as dirname6, extname as
|
|
3096
|
+
import { basename as basename8, dirname as dirname6, extname as extname6, join as join15, resolve as resolve3 } from "path";
|
|
2815
3097
|
|
|
2816
3098
|
// src/gate/mcpCapabilities.ts
|
|
2817
3099
|
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
2818
|
-
import { extname as
|
|
3100
|
+
import { extname as extname5, join as join14 } from "path";
|
|
2819
3101
|
var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
2820
3102
|
var MAX_ITEMS_PER_CATEGORY = 200;
|
|
2821
3103
|
function readJsonSafe(file) {
|
|
@@ -2940,7 +3222,7 @@ function extractMcpCapabilities(packageDir) {
|
|
|
2940
3222
|
let sawHttpTransport = false;
|
|
2941
3223
|
let sawMcpSdkImport = false;
|
|
2942
3224
|
for (const file of walk(packageDir)) {
|
|
2943
|
-
if (!SOURCE_EXT.has(
|
|
3225
|
+
if (!SOURCE_EXT.has(extname5(file))) continue;
|
|
2944
3226
|
const text = readTextFile(file);
|
|
2945
3227
|
if (text === void 0) continue;
|
|
2946
3228
|
captureRegistrations(text, buckets);
|
|
@@ -3078,7 +3360,7 @@ function resolveSkillMd(inputPath) {
|
|
|
3078
3360
|
let currentPath = inputPath;
|
|
3079
3361
|
let tmpDir;
|
|
3080
3362
|
const initialStat = statSync7(currentPath);
|
|
3081
|
-
if (
|
|
3363
|
+
if (extname6(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
|
|
3082
3364
|
tmpDir = mkdtempSync2(join15(tmpdir(), "forge_"));
|
|
3083
3365
|
extractZip(currentPath, tmpDir);
|
|
3084
3366
|
currentPath = tmpDir;
|
|
@@ -3641,14 +3923,14 @@ function renderSafetyLine(safety) {
|
|
|
3641
3923
|
const withDetail = detail ? `${safety.verdict} (${detail})` : safety.verdict;
|
|
3642
3924
|
return safety.recommendation ? `${withDetail} \u2014 ${safety.recommendation}` : withDetail;
|
|
3643
3925
|
}
|
|
3644
|
-
function
|
|
3926
|
+
function renderFindingLine2(finding2) {
|
|
3645
3927
|
const severity = finding2.severity ?? "UNKNOWN";
|
|
3646
3928
|
const rule = finding2.rule ?? "unknown-rule";
|
|
3647
3929
|
return ` - [${severity}] ${rule}${finding2.message ? `: ${finding2.message}` : ""}`;
|
|
3648
3930
|
}
|
|
3649
3931
|
function renderFindingsLines(findings) {
|
|
3650
3932
|
if (!findings?.length) return [];
|
|
3651
|
-
return [`${padLabel("Findings")}:`, ...findings.map(
|
|
3933
|
+
return [`${padLabel("Findings")}:`, ...findings.map(renderFindingLine2)];
|
|
3652
3934
|
}
|
|
3653
3935
|
function profileHeaderFields(profile) {
|
|
3654
3936
|
if (profile.kind === "mcp") {
|
|
@@ -3967,8 +4249,9 @@ function runHandoff(config, installedPath, options = {}) {
|
|
|
3967
4249
|
}
|
|
3968
4250
|
|
|
3969
4251
|
// src/commands/init.ts
|
|
3970
|
-
import { existsSync as
|
|
3971
|
-
import { homedir as
|
|
4252
|
+
import { existsSync as existsSync18 } from "fs";
|
|
4253
|
+
import { homedir as homedir5 } from "os";
|
|
4254
|
+
import { join as join23 } from "path";
|
|
3972
4255
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
3973
4256
|
|
|
3974
4257
|
// src/agents.ts
|
|
@@ -4218,12 +4501,12 @@ var AGENTS = [
|
|
|
4218
4501
|
].map(makeAgent);
|
|
4219
4502
|
|
|
4220
4503
|
// src/gitPreflight.ts
|
|
4221
|
-
import { relative as relative2, sep as
|
|
4504
|
+
import { relative as relative2, sep as sep3 } from "path";
|
|
4222
4505
|
function splitNulTerminated(output) {
|
|
4223
4506
|
return output.split("\0").filter((s) => s.length > 0);
|
|
4224
4507
|
}
|
|
4225
4508
|
function toPosixPath(p) {
|
|
4226
|
-
return p.split(
|
|
4509
|
+
return p.split(sep3).join("/");
|
|
4227
4510
|
}
|
|
4228
4511
|
var missingResult = (root) => ({
|
|
4229
4512
|
root,
|
|
@@ -4348,8 +4631,8 @@ function describeGitPreflight(result) {
|
|
|
4348
4631
|
}
|
|
4349
4632
|
|
|
4350
4633
|
// src/commands/audit.ts
|
|
4351
|
-
import { existsSync as
|
|
4352
|
-
import { join as
|
|
4634
|
+
import { existsSync as existsSync17 } from "fs";
|
|
4635
|
+
import { join as join22, resolve as resolvePath5 } from "path";
|
|
4353
4636
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
4354
4637
|
|
|
4355
4638
|
// src/decision.ts
|
|
@@ -4381,8 +4664,325 @@ ${prompt}`);
|
|
|
4381
4664
|
}
|
|
4382
4665
|
}
|
|
4383
4666
|
|
|
4667
|
+
// src/pluginRoots.ts
|
|
4668
|
+
import { existsSync as existsSync16, readFileSync as readFileSync16, readdirSync as readdirSync8, realpathSync as realpathSync3 } from "fs";
|
|
4669
|
+
import { homedir as homedir4 } from "os";
|
|
4670
|
+
import { isAbsolute as isAbsolute3, join as join20, relative as relative3, resolve as resolvePath3, sep as sep4 } from "path";
|
|
4671
|
+
function readJsonFileSafe(path) {
|
|
4672
|
+
let raw;
|
|
4673
|
+
try {
|
|
4674
|
+
raw = readFileSync16(path, "utf-8");
|
|
4675
|
+
} catch (err) {
|
|
4676
|
+
return { problem: err.code === "ENOENT" ? "missing" : "unreadable" };
|
|
4677
|
+
}
|
|
4678
|
+
try {
|
|
4679
|
+
return { data: JSON.parse(raw) };
|
|
4680
|
+
} catch {
|
|
4681
|
+
return { problem: "malformed" };
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
function describeProblem(problem, path) {
|
|
4685
|
+
const verb = problem === "missing" ? "not found" : problem === "unreadable" ? "unreadable" : "malformed (invalid JSON)";
|
|
4686
|
+
return `${path} ${verb}`;
|
|
4687
|
+
}
|
|
4688
|
+
function extractEnabledMap(data) {
|
|
4689
|
+
if (typeof data !== "object" || data === null) return {};
|
|
4690
|
+
const raw = data.enabledPlugins;
|
|
4691
|
+
if (typeof raw !== "object" || raw === null) return {};
|
|
4692
|
+
const out = {};
|
|
4693
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
4694
|
+
if (typeof value === "boolean") out[id] = value;
|
|
4695
|
+
}
|
|
4696
|
+
return out;
|
|
4697
|
+
}
|
|
4698
|
+
function extractRegistry(data) {
|
|
4699
|
+
if (typeof data !== "object" || data === null) return {};
|
|
4700
|
+
const pluginsRaw = data.plugins;
|
|
4701
|
+
if (typeof pluginsRaw !== "object" || pluginsRaw === null) return {};
|
|
4702
|
+
const out = {};
|
|
4703
|
+
for (const [id, entriesRaw] of Object.entries(pluginsRaw)) {
|
|
4704
|
+
if (!Array.isArray(entriesRaw)) continue;
|
|
4705
|
+
const entries = [];
|
|
4706
|
+
for (const raw of entriesRaw) {
|
|
4707
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
4708
|
+
const rec = raw;
|
|
4709
|
+
const scope = rec.scope;
|
|
4710
|
+
const installPath = rec.installPath;
|
|
4711
|
+
const version = rec.version;
|
|
4712
|
+
if ((scope === "user" || scope === "project" || scope === "local") && typeof installPath === "string" && typeof version === "string") {
|
|
4713
|
+
entries.push({
|
|
4714
|
+
scope,
|
|
4715
|
+
projectPath: typeof rec.projectPath === "string" ? rec.projectPath : void 0,
|
|
4716
|
+
installPath,
|
|
4717
|
+
version
|
|
4718
|
+
});
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
if (entries.length > 0) out[id] = entries;
|
|
4722
|
+
}
|
|
4723
|
+
return out;
|
|
4724
|
+
}
|
|
4725
|
+
function splitPluginId(id) {
|
|
4726
|
+
const idx = id.lastIndexOf("@");
|
|
4727
|
+
if (idx === -1) return { plugin: id, marketplace: "" };
|
|
4728
|
+
return { plugin: id.slice(0, idx), marketplace: id.slice(idx + 1) };
|
|
4729
|
+
}
|
|
4730
|
+
function safeRealpath(p) {
|
|
4731
|
+
try {
|
|
4732
|
+
return realpathSync3(p);
|
|
4733
|
+
} catch {
|
|
4734
|
+
return void 0;
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
function parseSemver(v) {
|
|
4738
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
4739
|
+
if (!m) return void 0;
|
|
4740
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
4741
|
+
}
|
|
4742
|
+
function compareVersionsDesc(a, b) {
|
|
4743
|
+
const pa = parseSemver(a);
|
|
4744
|
+
const pb = parseSemver(b);
|
|
4745
|
+
if (pa && pb) {
|
|
4746
|
+
for (let i = 0; i < 3; i++) {
|
|
4747
|
+
if (pa[i] !== pb[i]) return pb[i] - pa[i];
|
|
4748
|
+
}
|
|
4749
|
+
return b.localeCompare(a);
|
|
4750
|
+
}
|
|
4751
|
+
if (pa && !pb) return -1;
|
|
4752
|
+
if (!pa && pb) return 1;
|
|
4753
|
+
return b.localeCompare(a);
|
|
4754
|
+
}
|
|
4755
|
+
function selectInstallEntry(entries, cwd) {
|
|
4756
|
+
const userEntry = entries.find((e) => e.scope === "user");
|
|
4757
|
+
if (userEntry) return userEntry;
|
|
4758
|
+
const cwdReal = safeRealpath(cwd);
|
|
4759
|
+
if (cwdReal) {
|
|
4760
|
+
const projectEntry = entries.find((e) => e.scope === "project" && e.projectPath && safeRealpath(e.projectPath) === cwdReal);
|
|
4761
|
+
if (projectEntry) return projectEntry;
|
|
4762
|
+
}
|
|
4763
|
+
return [...entries].sort((a, b) => compareVersionsDesc(a.version, b.version))[0];
|
|
4764
|
+
}
|
|
4765
|
+
function isInside2(child, root) {
|
|
4766
|
+
if (child === root) return true;
|
|
4767
|
+
const rel = relative3(root, child);
|
|
4768
|
+
return rel !== "" && !rel.startsWith(".." + sep4) && rel !== ".." && !isAbsolute3(rel);
|
|
4769
|
+
}
|
|
4770
|
+
var DEFAULT_SKILL_LAYOUT = { paths: ["./skills/"], explicit: false };
|
|
4771
|
+
function normalizeDeclaredSkillsField(manifest, pluginId, notices) {
|
|
4772
|
+
if (typeof manifest !== "object" || manifest === null) return DEFAULT_SKILL_LAYOUT;
|
|
4773
|
+
const skillsField = manifest.skills;
|
|
4774
|
+
if (skillsField === void 0) return DEFAULT_SKILL_LAYOUT;
|
|
4775
|
+
if (typeof skillsField === "string") return { paths: [skillsField], explicit: true };
|
|
4776
|
+
if (Array.isArray(skillsField) && skillsField.every((s) => typeof s === "string")) return { paths: skillsField, explicit: true };
|
|
4777
|
+
notices.push(`plugin ${pluginId}: manifest "skills" field has an unrecognized shape \u2014 using default skill layout`);
|
|
4778
|
+
return DEFAULT_SKILL_LAYOUT;
|
|
4779
|
+
}
|
|
4780
|
+
function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, pluginId, notices) {
|
|
4781
|
+
const joined = resolvePath3(installPath, declaredPath);
|
|
4782
|
+
let real;
|
|
4783
|
+
try {
|
|
4784
|
+
real = realpathSync3(joined);
|
|
4785
|
+
} catch {
|
|
4786
|
+
if (explicit) notices.push(`plugin ${pluginId}: declared skill path does not exist, skipping: ${declaredPath}`);
|
|
4787
|
+
return [];
|
|
4788
|
+
}
|
|
4789
|
+
if (!realInstall || !isInside2(real, realInstall)) {
|
|
4790
|
+
notices.push(`plugin ${pluginId}: declared skill path escapes the install root, skipping: ${declaredPath}`);
|
|
4791
|
+
return [];
|
|
4792
|
+
}
|
|
4793
|
+
if (existsSync16(join20(real, "SKILL.md"))) return [real];
|
|
4794
|
+
let entries;
|
|
4795
|
+
try {
|
|
4796
|
+
entries = readdirSync8(real, { withFileTypes: true });
|
|
4797
|
+
} catch {
|
|
4798
|
+
return [];
|
|
4799
|
+
}
|
|
4800
|
+
const found = [];
|
|
4801
|
+
for (const entry of entries) {
|
|
4802
|
+
if (!entry.isDirectory()) continue;
|
|
4803
|
+
const childPath = join20(real, entry.name);
|
|
4804
|
+
if (existsSync16(join20(childPath, "SKILL.md"))) found.push(childPath);
|
|
4805
|
+
}
|
|
4806
|
+
return found;
|
|
4807
|
+
}
|
|
4808
|
+
function resolveSkillDirs(installPath, pluginId) {
|
|
4809
|
+
const notices = [];
|
|
4810
|
+
const manifestPath = join20(installPath, ".claude-plugin", "plugin.json");
|
|
4811
|
+
const manifestResult = readJsonFileSafe(manifestPath);
|
|
4812
|
+
if (manifestResult.problem) {
|
|
4813
|
+
notices.push(`plugin ${pluginId}: ${manifestResult.problem} manifest at ${manifestPath} \u2014 using default skill layout`);
|
|
4814
|
+
}
|
|
4815
|
+
const declared = manifestResult.problem ? DEFAULT_SKILL_LAYOUT : normalizeDeclaredSkillsField(manifestResult.data, pluginId, notices);
|
|
4816
|
+
const realInstall = safeRealpath(installPath);
|
|
4817
|
+
const skillDirs = /* @__PURE__ */ new Set();
|
|
4818
|
+
for (const declaredPath of declared.paths) {
|
|
4819
|
+
for (const dir of resolveDeclaredPath(installPath, realInstall, declaredPath, declared.explicit, pluginId, notices)) {
|
|
4820
|
+
skillDirs.add(dir);
|
|
4821
|
+
}
|
|
4822
|
+
}
|
|
4823
|
+
return { skillDirs: Array.from(skillDirs).sort(), notices };
|
|
4824
|
+
}
|
|
4825
|
+
function discoverClaudePluginRoots(options = {}) {
|
|
4826
|
+
const homeDir = options.homeDir ?? homedir4();
|
|
4827
|
+
const cwd = options.cwd ?? process.cwd();
|
|
4828
|
+
const settingsPath = options.settingsPath ?? join20(homeDir, ".claude", "settings.json");
|
|
4829
|
+
const localSettingsPath = options.localSettingsPath ?? join20(cwd, ".claude", "settings.local.json");
|
|
4830
|
+
const installedPluginsPath = options.installedPluginsPath ?? join20(homeDir, ".claude", "plugins", "installed_plugins.json");
|
|
4831
|
+
const notices = [];
|
|
4832
|
+
const settingsResult = readJsonFileSafe(settingsPath);
|
|
4833
|
+
if (settingsResult.problem) {
|
|
4834
|
+
notices.push(`Claude Code settings ${describeProblem(settingsResult.problem, settingsPath)} \u2014 skipping Claude plugin discovery`);
|
|
4835
|
+
return { roots: [], notices };
|
|
4836
|
+
}
|
|
4837
|
+
const registryResult = readJsonFileSafe(installedPluginsPath);
|
|
4838
|
+
if (registryResult.problem) {
|
|
4839
|
+
notices.push(`Claude Code plugin registry ${describeProblem(registryResult.problem, installedPluginsPath)} \u2014 skipping Claude plugin discovery`);
|
|
4840
|
+
return { roots: [], notices };
|
|
4841
|
+
}
|
|
4842
|
+
const globalEnabled = extractEnabledMap(settingsResult.data);
|
|
4843
|
+
let localEnabled = {};
|
|
4844
|
+
if (existsSync16(localSettingsPath)) {
|
|
4845
|
+
const localResult = readJsonFileSafe(localSettingsPath);
|
|
4846
|
+
if (localResult.problem) {
|
|
4847
|
+
notices.push(`local settings ${describeProblem(localResult.problem, localSettingsPath)} \u2014 ignoring local plugin overrides`);
|
|
4848
|
+
} else {
|
|
4849
|
+
localEnabled = extractEnabledMap(localResult.data);
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
const merged = { ...globalEnabled, ...localEnabled };
|
|
4853
|
+
const enabledIds = Object.keys(merged).filter((id) => merged[id] === true).sort();
|
|
4854
|
+
const registry = extractRegistry(registryResult.data);
|
|
4855
|
+
const roots = [];
|
|
4856
|
+
for (const id of enabledIds) {
|
|
4857
|
+
const entries = registry[id];
|
|
4858
|
+
if (!entries || entries.length === 0) {
|
|
4859
|
+
notices.push(`plugin ${id} is enabled but has no installed_plugins.json entry \u2014 skipping`);
|
|
4860
|
+
continue;
|
|
4861
|
+
}
|
|
4862
|
+
const entry = selectInstallEntry(entries, cwd);
|
|
4863
|
+
const { plugin, marketplace } = splitPluginId(id);
|
|
4864
|
+
const { skillDirs, notices: manifestNotices } = resolveSkillDirs(entry.installPath, id);
|
|
4865
|
+
notices.push(...manifestNotices);
|
|
4866
|
+
roots.push({ pluginId: id, marketplace, plugin, version: entry.version, installPath: entry.installPath, skillDirs });
|
|
4867
|
+
}
|
|
4868
|
+
roots.sort((a, b) => a.pluginId.localeCompare(b.pluginId));
|
|
4869
|
+
return { roots, notices };
|
|
4870
|
+
}
|
|
4871
|
+
|
|
4872
|
+
// src/usageSnapshot.ts
|
|
4873
|
+
import { readFileSync as readFileSync17, readdirSync as readdirSync9 } from "fs";
|
|
4874
|
+
import { extname as extname7, join as join21, resolve as resolvePath4 } from "path";
|
|
4875
|
+
function emptySnapshot(sourcePath, warning) {
|
|
4876
|
+
return { usage: /* @__PURE__ */ new Map(), exhaustive: false, sourcePath, warning };
|
|
4877
|
+
}
|
|
4878
|
+
function parseSnapshotFile(raw) {
|
|
4879
|
+
let parsed;
|
|
4880
|
+
try {
|
|
4881
|
+
parsed = JSON.parse(raw);
|
|
4882
|
+
} catch (err) {
|
|
4883
|
+
return { error: `could not parse usage snapshot (${err.message})` };
|
|
4884
|
+
}
|
|
4885
|
+
const outer = parsed && typeof parsed === "object" ? parsed : void 0;
|
|
4886
|
+
const report = outer && typeof outer.report === "object" && outer.report !== null ? outer.report : void 0;
|
|
4887
|
+
if (!report) return { error: 'usage snapshot has no "report" object' };
|
|
4888
|
+
const generatedAt = typeof report.generated_at === "string" ? report.generated_at : void 0;
|
|
4889
|
+
const windowDays = typeof report.window_days === "number" ? report.window_days : void 0;
|
|
4890
|
+
const uniqueSkillsUsed = typeof report.unique_skills_used === "number" ? report.unique_skills_used : void 0;
|
|
4891
|
+
const skillTotals = report.skill_totals && typeof report.skill_totals === "object" ? report.skill_totals : void 0;
|
|
4892
|
+
const topSkills = Array.isArray(report.top_skills) ? report.top_skills : void 0;
|
|
4893
|
+
const usage = /* @__PURE__ */ new Map();
|
|
4894
|
+
if (skillTotals) {
|
|
4895
|
+
for (const [name, count] of Object.entries(skillTotals)) {
|
|
4896
|
+
if (typeof count === "number") usage.set(name, count);
|
|
4897
|
+
}
|
|
4898
|
+
} else if (topSkills) {
|
|
4899
|
+
for (const row of topSkills) {
|
|
4900
|
+
if (Array.isArray(row) && row.length === 2 && typeof row[0] === "string" && typeof row[1] === "number") {
|
|
4901
|
+
usage.set(row[0], row[1]);
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
const exhaustive = Boolean(skillTotals) || topSkills !== void 0 && uniqueSkillsUsed !== void 0 && topSkills.length === uniqueSkillsUsed;
|
|
4906
|
+
return { usage, exhaustive, windowDays, generatedAt };
|
|
4907
|
+
}
|
|
4908
|
+
function pickLatest(candidates) {
|
|
4909
|
+
return candidates.reduce((best, cur) => {
|
|
4910
|
+
if (!best) return cur;
|
|
4911
|
+
if (cur.generatedAt === void 0 && best.generatedAt === void 0) return cur.path > best.path ? cur : best;
|
|
4912
|
+
if (cur.generatedAt === void 0) return best;
|
|
4913
|
+
if (best.generatedAt === void 0) return cur;
|
|
4914
|
+
if (cur.generatedAt === best.generatedAt) return cur.path > best.path ? cur : best;
|
|
4915
|
+
return cur.generatedAt > best.generatedAt ? cur : best;
|
|
4916
|
+
}, void 0);
|
|
4917
|
+
}
|
|
4918
|
+
function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
4919
|
+
const resolved = resolvePath4(expandHome(pathOrDir));
|
|
4920
|
+
if (isDir(resolved)) {
|
|
4921
|
+
let entries;
|
|
4922
|
+
try {
|
|
4923
|
+
entries = readdirSync9(resolved).filter((f) => extname7(f).toLowerCase() === ".json");
|
|
4924
|
+
} catch (err) {
|
|
4925
|
+
return emptySnapshot(resolved, `could not read usage snapshot directory (${err.message})`);
|
|
4926
|
+
}
|
|
4927
|
+
if (entries.length === 0) {
|
|
4928
|
+
return emptySnapshot(resolved, `no *.json snapshot files found in ${resolved}`);
|
|
4929
|
+
}
|
|
4930
|
+
const candidates = [];
|
|
4931
|
+
for (const name of entries) {
|
|
4932
|
+
const filePath = join21(resolved, name);
|
|
4933
|
+
let raw3;
|
|
4934
|
+
try {
|
|
4935
|
+
raw3 = readFileSync17(filePath, "utf-8");
|
|
4936
|
+
} catch {
|
|
4937
|
+
continue;
|
|
4938
|
+
}
|
|
4939
|
+
const parsed3 = parseSnapshotFile(raw3);
|
|
4940
|
+
if ("error" in parsed3) continue;
|
|
4941
|
+
if (opts.windowDays !== void 0 && parsed3.windowDays !== opts.windowDays) continue;
|
|
4942
|
+
candidates.push({ path: filePath, generatedAt: parsed3.generatedAt });
|
|
4943
|
+
}
|
|
4944
|
+
const picked = pickLatest(candidates);
|
|
4945
|
+
if (!picked) {
|
|
4946
|
+
return emptySnapshot(
|
|
4947
|
+
resolved,
|
|
4948
|
+
opts.windowDays !== void 0 ? `no usage snapshot in ${resolved} matches --usage-window ${opts.windowDays}` : `no readable usage snapshot found in ${resolved}`
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
const raw2 = readFileSync17(picked.path, "utf-8");
|
|
4952
|
+
const parsed2 = parseSnapshotFile(raw2);
|
|
4953
|
+
if ("error" in parsed2) return emptySnapshot(picked.path, parsed2.error);
|
|
4954
|
+
return { usage: parsed2.usage, exhaustive: parsed2.exhaustive, windowDays: parsed2.windowDays, generatedAt: parsed2.generatedAt, sourcePath: picked.path };
|
|
4955
|
+
}
|
|
4956
|
+
let raw;
|
|
4957
|
+
try {
|
|
4958
|
+
raw = readFileSync17(resolved, "utf-8");
|
|
4959
|
+
} catch (err) {
|
|
4960
|
+
return emptySnapshot(resolved, `could not read usage snapshot (${err.message})`);
|
|
4961
|
+
}
|
|
4962
|
+
const parsed = parseSnapshotFile(raw);
|
|
4963
|
+
if ("error" in parsed) return emptySnapshot(resolved, parsed.error);
|
|
4964
|
+
return { usage: parsed.usage, exhaustive: parsed.exhaustive, windowDays: parsed.windowDays, generatedAt: parsed.generatedAt, sourcePath: resolved };
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4384
4967
|
// src/commands/audit.ts
|
|
4385
4968
|
var CURATION_PROMPT_ASSET = "curation-prompt.md";
|
|
4969
|
+
function resolvePluginAndUsageContext(config, options, deps = {}) {
|
|
4970
|
+
if (options.usageWindow !== void 0 && (!Number.isInteger(options.usageWindow) || options.usageWindow < 1)) {
|
|
4971
|
+
throw new UserError(`--usage-window must be a positive integer (got "${options.usageWindow}")`);
|
|
4972
|
+
}
|
|
4973
|
+
const wantPlugins = options.claudePlugins ?? config.claudePlugins ?? false;
|
|
4974
|
+
let pluginRoots;
|
|
4975
|
+
const notices = [];
|
|
4976
|
+
if (wantPlugins) {
|
|
4977
|
+
const discovered = discoverClaudePluginRoots(deps);
|
|
4978
|
+
pluginRoots = discovered.roots;
|
|
4979
|
+
notices.push(...discovered.notices);
|
|
4980
|
+
}
|
|
4981
|
+
const usageSnapshotPath = options.usageSnapshot ?? config.usageSnapshotPath;
|
|
4982
|
+
const usage = usageSnapshotPath ? loadUsageSnapshot(usageSnapshotPath, { windowDays: options.usageWindow }) : void 0;
|
|
4983
|
+
if (usage?.warning) notices.push(usage.warning);
|
|
4984
|
+
return { pluginRoots, usage, notices };
|
|
4985
|
+
}
|
|
4386
4986
|
function splitCsv(s) {
|
|
4387
4987
|
return s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
4388
4988
|
}
|
|
@@ -4415,11 +5015,16 @@ async function captureBusinessProfile(rl, existing) {
|
|
|
4415
5015
|
return profile;
|
|
4416
5016
|
}
|
|
4417
5017
|
function resolveReportPath(reportOption) {
|
|
4418
|
-
if (reportOption) return
|
|
5018
|
+
if (reportOption) return resolvePath5(reportOption);
|
|
4419
5019
|
const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4420
|
-
return
|
|
5020
|
+
return join22(getHomeDir(), "reports", `audit-${iso}.md`);
|
|
4421
5021
|
}
|
|
4422
|
-
function
|
|
5022
|
+
function summarizePluginRecommendations(plugins) {
|
|
5023
|
+
const counts = { keep: 0, review: 0, unobserved: 0, unknown: 0 };
|
|
5024
|
+
for (const p of plugins) counts[p.recommendation]++;
|
|
5025
|
+
return `plugins: ${plugins.length} (review ${counts.review}, unobserved ${counts.unobserved}, unknown ${counts.unknown})`;
|
|
5026
|
+
}
|
|
5027
|
+
function printTerminalSummary(report, reportPath, usage) {
|
|
4423
5028
|
const { summary } = report;
|
|
4424
5029
|
console.log("[skill-forge] audit complete");
|
|
4425
5030
|
console.log(` skill roots: ${summary.rootCount} skills: ${summary.skillCount} MCP targets: ${summary.mcpTargetCount}`);
|
|
@@ -4428,6 +5033,12 @@ function printTerminalSummary(report, reportPath) {
|
|
|
4428
5033
|
);
|
|
4429
5034
|
console.log(` accepted: ${summary.acceptedCount} (suppressed from counts; see report)`);
|
|
4430
5035
|
console.log(` overlap clusters: ${summary.overlapClusterCount} evolve-eligible: ${summary.evolveEligibleCount}`);
|
|
5036
|
+
if (report.plugins) console.log(` ${summarizePluginRecommendations(report.plugins)}`);
|
|
5037
|
+
if (usage) {
|
|
5038
|
+
console.log(
|
|
5039
|
+
` usage: ${usage.sourcePath} (window ${usage.windowDays !== void 0 ? `${usage.windowDays} day(s)` : "unknown"}, ${usage.exhaustive ? "exhaustive" : "top-50 only"})`
|
|
5040
|
+
);
|
|
5041
|
+
}
|
|
4431
5042
|
console.log(` report -> ${reportPath}`);
|
|
4432
5043
|
for (const notice of report.notices) console.log(` [notice] ${notice}`);
|
|
4433
5044
|
console.log(
|
|
@@ -4465,9 +5076,9 @@ async function maybeHandoff(config, reportPath, options, rl) {
|
|
|
4465
5076
|
if (!wantHandoff) return;
|
|
4466
5077
|
runHandoff(config, reportPath, { promptAsset: CURATION_PROMPT_ASSET, log: options.json ? console.error : console.log });
|
|
4467
5078
|
}
|
|
4468
|
-
async function auditCommand(options = {}) {
|
|
5079
|
+
async function auditCommand(options = {}, deps = {}) {
|
|
4469
5080
|
await maybeOfferInit({ json: options.json, yes: options.yes, suppressAuditOffer: true });
|
|
4470
|
-
if (!
|
|
5081
|
+
if (!existsSync17(getConfigPath())) {
|
|
4471
5082
|
throw new UserError(
|
|
4472
5083
|
"no skill-forge config found \u2014 run `skill-forge init` first (audit requires a configured skill/MCP set; it never audits an invented default)."
|
|
4473
5084
|
);
|
|
@@ -4483,9 +5094,11 @@ async function auditCommand(options = {}) {
|
|
|
4483
5094
|
saveConfig(config);
|
|
4484
5095
|
}
|
|
4485
5096
|
}
|
|
5097
|
+
const { pluginRoots, usage, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
4486
5098
|
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
4487
|
-
const report = await auditConfiguredSet(config, { accepted });
|
|
4488
|
-
|
|
5099
|
+
const report = await auditConfiguredSet(config, { accepted, pluginRoots, usage });
|
|
5100
|
+
const extraNotices = [...storeNotices, ...pluginUsageNotices];
|
|
5101
|
+
if (extraNotices.length > 0) report.notices = [...report.notices, ...extraNotices];
|
|
4489
5102
|
const reportPath = resolveReportPath(options.report);
|
|
4490
5103
|
writeNewFile(reportPath, renderAuditMarkdown(report), "--report");
|
|
4491
5104
|
try {
|
|
@@ -4496,7 +5109,7 @@ async function auditCommand(options = {}) {
|
|
|
4496
5109
|
if (options.json) {
|
|
4497
5110
|
console.log(JSON.stringify(report, null, 2));
|
|
4498
5111
|
} else {
|
|
4499
|
-
printTerminalSummary(report, reportPath);
|
|
5112
|
+
printTerminalSummary(report, reportPath, usage);
|
|
4500
5113
|
}
|
|
4501
5114
|
config = await maybeScaffoldFoundation(config, report, options, rl);
|
|
4502
5115
|
await maybeHandoff(config, reportPath, options, rl);
|
|
@@ -4613,7 +5226,7 @@ function printMcpDetectionResults(detectedMcp) {
|
|
|
4613
5226
|
function handoffTemplateFor(agent) {
|
|
4614
5227
|
return agent.defaultHandoffTemplate ?? [agent.id, "-p", HANDOFF_INSTRUCTION];
|
|
4615
5228
|
}
|
|
4616
|
-
function buildConfig(selected, defaultTargetEntry, handoffAgent, selectedMcp = []) {
|
|
5229
|
+
function buildConfig(selected, defaultTargetEntry, handoffAgent, selectedMcp = [], claudePlugins) {
|
|
4617
5230
|
const existing = loadConfig();
|
|
4618
5231
|
return {
|
|
4619
5232
|
...existing,
|
|
@@ -4625,7 +5238,8 @@ function buildConfig(selected, defaultTargetEntry, handoffAgent, selectedMcp = [
|
|
|
4625
5238
|
agents: selected.length > 0 ? selected.flatMap((s) => s.members.map((m) => ({ id: m.agent.id, skillsRoot: s.root }))) : existing.agents,
|
|
4626
5239
|
defaultTarget: defaultTargetEntry?.root ?? existing.defaultTarget,
|
|
4627
5240
|
handoffCommand: handoffAgent ? handoffTemplateFor(handoffAgent) : existing.handoffCommand,
|
|
4628
|
-
mcpTargets: selectedMcp.length > 0 ? selectedMcp.map((m) => m.path) : existing.mcpTargets
|
|
5241
|
+
mcpTargets: selectedMcp.length > 0 ? selectedMcp.map((m) => m.path) : existing.mcpTargets,
|
|
5242
|
+
claudePlugins: claudePlugins !== void 0 ? claudePlugins : existing.claudePlugins
|
|
4629
5243
|
};
|
|
4630
5244
|
}
|
|
4631
5245
|
async function printSummary(config, deps = {}) {
|
|
@@ -4790,7 +5404,7 @@ async function init(options = {}, deps = {}) {
|
|
|
4790
5404
|
const runAudit = deps.runAudit ?? auditCommand;
|
|
4791
5405
|
const gitDeps = deps.gitExec ? { exec: deps.gitExec } : {};
|
|
4792
5406
|
const cwd = options.cwd ?? process.cwd();
|
|
4793
|
-
const home = options.home ??
|
|
5407
|
+
const home = options.home ?? homedir5();
|
|
4794
5408
|
const detected = detectAgents(cwd, home, options.pathEnv);
|
|
4795
5409
|
const detectedMcp = detectMcpTargets(cwd, home, options.pathEnv);
|
|
4796
5410
|
if (options.list) {
|
|
@@ -4813,10 +5427,18 @@ async function init(options = {}, deps = {}) {
|
|
|
4813
5427
|
"[skill-forge] No detected agent CLI was found on PATH \u2014 keeping every detected target selected."
|
|
4814
5428
|
);
|
|
4815
5429
|
}
|
|
5430
|
+
const installedPluginsPath = options.installedPluginsPath ?? join23(home, ".claude", "plugins", "installed_plugins.json");
|
|
5431
|
+
const claudePluginsRegistryExists = existsSync18(installedPluginsPath);
|
|
4816
5432
|
if (options.defaults) {
|
|
4817
5433
|
const selectedRoots = narrow ? rootGroups.filter(hasInstalledMember) : rootGroups;
|
|
4818
5434
|
const selectedMcpTargets = narrow ? mcpGroups.filter(hasInstalledMember) : mcpGroups;
|
|
4819
|
-
const config = buildConfig(
|
|
5435
|
+
const config = buildConfig(
|
|
5436
|
+
selectedRoots,
|
|
5437
|
+
selectedRoots[0],
|
|
5438
|
+
void 0,
|
|
5439
|
+
selectedMcpTargets,
|
|
5440
|
+
claudePluginsRegistryExists ? true : void 0
|
|
5441
|
+
);
|
|
4820
5442
|
saveConfig(config);
|
|
4821
5443
|
await printSummary(config, gitDeps);
|
|
4822
5444
|
await runAudit({ yes: true });
|
|
@@ -4840,7 +5462,12 @@ async function init(options = {}, deps = {}) {
|
|
|
4840
5462
|
const target = selected.length > 0 ? await promptDefaultTarget(rl, selected) : void 0;
|
|
4841
5463
|
const handoffAgent = detected.length > 0 ? await promptHandoffAgent(rl, detected, narrow) : void 0;
|
|
4842
5464
|
await promptGitPreflight(rl, selected, gitDeps);
|
|
4843
|
-
|
|
5465
|
+
let claudePluginsAnswer;
|
|
5466
|
+
if (claudePluginsRegistryExists) {
|
|
5467
|
+
const answer = (await rl.question("\nInclude enabled Claude Code plugins in audits? [Y/n] ")).trim().toLowerCase();
|
|
5468
|
+
claudePluginsAnswer = answer === "" || answer === "y" || answer === "yes";
|
|
5469
|
+
}
|
|
5470
|
+
const config = buildConfig(selected, target, handoffAgent, selectedMcp, claudePluginsAnswer);
|
|
4844
5471
|
saveConfig(config);
|
|
4845
5472
|
await printSummary(config, gitDeps);
|
|
4846
5473
|
if (options.offerAudit !== false) {
|
|
@@ -4860,7 +5487,7 @@ async function init(options = {}, deps = {}) {
|
|
|
4860
5487
|
}
|
|
4861
5488
|
async function maybeOfferInit(options = {}) {
|
|
4862
5489
|
if (options.json || options.yes || !process.stdin.isTTY) return;
|
|
4863
|
-
if (
|
|
5490
|
+
if (existsSync18(getConfigPath())) return;
|
|
4864
5491
|
console.log(
|
|
4865
5492
|
"[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)."
|
|
4866
5493
|
);
|
|
@@ -5011,7 +5638,7 @@ async function addMcp(source, config, options) {
|
|
|
5011
5638
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
5012
5639
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
5013
5640
|
};
|
|
5014
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
5641
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join24(process.cwd(), ".mcp.json");
|
|
5015
5642
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
5016
5643
|
reject(entry);
|
|
5017
5644
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -5107,7 +5734,7 @@ async function scan(source, options = {}) {
|
|
|
5107
5734
|
}
|
|
5108
5735
|
|
|
5109
5736
|
// src/commands/list.ts
|
|
5110
|
-
import { join as
|
|
5737
|
+
import { join as join25 } from "path";
|
|
5111
5738
|
async function list() {
|
|
5112
5739
|
await maybeOfferInit();
|
|
5113
5740
|
const config = loadConfig();
|
|
@@ -5120,14 +5747,14 @@ async function list() {
|
|
|
5120
5747
|
}
|
|
5121
5748
|
console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
|
|
5122
5749
|
for (const id of ids) {
|
|
5123
|
-
const skillDirs = findSkillDirs(
|
|
5750
|
+
const skillDirs = findSkillDirs(join25(quarantineDir, id));
|
|
5124
5751
|
if (skillDirs.length === 0) {
|
|
5125
5752
|
console.log(` ${id} (no SKILL.md found)`);
|
|
5126
5753
|
continue;
|
|
5127
5754
|
}
|
|
5128
5755
|
const names = skillDirs.map((dir) => {
|
|
5129
5756
|
try {
|
|
5130
|
-
return readFrontmatter(
|
|
5757
|
+
return readFrontmatter(join25(dir, "SKILL.md")).name;
|
|
5131
5758
|
} catch {
|
|
5132
5759
|
return dir;
|
|
5133
5760
|
}
|
|
@@ -5137,23 +5764,23 @@ async function list() {
|
|
|
5137
5764
|
}
|
|
5138
5765
|
|
|
5139
5766
|
// src/commands/guide.ts
|
|
5140
|
-
import { existsSync as
|
|
5767
|
+
import { existsSync as existsSync20 } from "fs";
|
|
5141
5768
|
|
|
5142
5769
|
// src/commands/held.ts
|
|
5143
|
-
import { existsSync as
|
|
5144
|
-
import { basename as basename9, join as
|
|
5770
|
+
import { existsSync as existsSync19, realpathSync as realpathSync4 } from "fs";
|
|
5771
|
+
import { basename as basename9, join as join26, resolve as resolvePath6, sep as sep5 } from "path";
|
|
5145
5772
|
var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
|
|
5146
5773
|
function resolveHeldPath(quarantineDir, id) {
|
|
5147
5774
|
if (id === "" || id !== basename9(id) || id === "." || id === "..") {
|
|
5148
5775
|
throw new UserError(`invalid quarantine id "${id}" \u2014 pass a single id exactly as printed by \`skill-forge list\``);
|
|
5149
5776
|
}
|
|
5150
|
-
const candidate =
|
|
5151
|
-
if (!
|
|
5777
|
+
const candidate = join26(quarantineDir, id);
|
|
5778
|
+
if (!existsSync19(candidate)) {
|
|
5152
5779
|
throw new UserError(`no held entry with id "${id}" \u2014 run \`skill-forge list\` to see what is in quarantine`);
|
|
5153
5780
|
}
|
|
5154
|
-
const realRoot =
|
|
5155
|
-
const realCandidate =
|
|
5156
|
-
if (!realCandidate.startsWith(realRoot +
|
|
5781
|
+
const realRoot = realpathSync4(resolvePath6(quarantineDir));
|
|
5782
|
+
const realCandidate = realpathSync4(candidate);
|
|
5783
|
+
if (!realCandidate.startsWith(realRoot + sep5)) {
|
|
5157
5784
|
throw new UserError(`refusing to act on "${id}": it resolves outside the quarantine dir (${realRoot})`);
|
|
5158
5785
|
}
|
|
5159
5786
|
return realCandidate;
|
|
@@ -5176,7 +5803,7 @@ function resolveTargetRoot(config, target) {
|
|
|
5176
5803
|
function heldSkillNames(skillDirs) {
|
|
5177
5804
|
return skillDirs.map((dir) => {
|
|
5178
5805
|
try {
|
|
5179
|
-
return readFrontmatter(
|
|
5806
|
+
return readFrontmatter(join26(dir, "SKILL.md")).name;
|
|
5180
5807
|
} catch {
|
|
5181
5808
|
return basename9(dir);
|
|
5182
5809
|
}
|
|
@@ -5184,7 +5811,7 @@ function heldSkillNames(skillDirs) {
|
|
|
5184
5811
|
}
|
|
5185
5812
|
function looksLikeMcpSandbox(quarantinePath) {
|
|
5186
5813
|
const candidate = resolveMcpCandidatePath({ quarantinePath });
|
|
5187
|
-
return [".mcp.json", "mcp.json", "package.json"].some((f) =>
|
|
5814
|
+
return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync19(join26(candidate, f)));
|
|
5188
5815
|
}
|
|
5189
5816
|
async function promoteHeldMcp(id, entry, config, options) {
|
|
5190
5817
|
const candidatePath = resolveMcpCandidatePath(entry);
|
|
@@ -5218,7 +5845,7 @@ async function promoteHeldMcp(id, entry, config, options) {
|
|
|
5218
5845
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
5219
5846
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
5220
5847
|
};
|
|
5221
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
5848
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join26(process.cwd(), ".mcp.json");
|
|
5222
5849
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
5223
5850
|
reject(entry);
|
|
5224
5851
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -5463,7 +6090,7 @@ var TOPICS = [
|
|
|
5463
6090
|
];
|
|
5464
6091
|
function readState() {
|
|
5465
6092
|
const configPath = getConfigPath();
|
|
5466
|
-
const configured =
|
|
6093
|
+
const configured = existsSync20(configPath);
|
|
5467
6094
|
const base = {
|
|
5468
6095
|
configured,
|
|
5469
6096
|
configPath,
|
|
@@ -5697,17 +6324,17 @@ async function status() {
|
|
|
5697
6324
|
// src/evolve.ts
|
|
5698
6325
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
5699
6326
|
import { createHash as createHash3 } from "crypto";
|
|
5700
|
-
import { cpSync as cpSync3, existsSync as
|
|
6327
|
+
import { cpSync as cpSync3, existsSync as existsSync21, lstatSync as lstatSync4, mkdtempSync as mkdtempSync3, readFileSync as readFileSync18, rmSync as rmSync6 } from "fs";
|
|
5701
6328
|
import { tmpdir as tmpdir2 } from "os";
|
|
5702
|
-
import { basename as basename10, dirname as dirname9, join as
|
|
6329
|
+
import { basename as basename10, dirname as dirname9, join as join27, resolve as resolvePath7, sep as sep6 } from "path";
|
|
5703
6330
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
5704
6331
|
var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
|
|
5705
6332
|
async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
5706
6333
|
const exec = deps.exec ?? defaultExec;
|
|
5707
6334
|
const json = options.json;
|
|
5708
|
-
const resolvedTargetDir =
|
|
5709
|
-
const skillMdPath =
|
|
5710
|
-
if (!
|
|
6335
|
+
const resolvedTargetDir = resolvePath7(targetSkillDir);
|
|
6336
|
+
const skillMdPath = join27(resolvedTargetDir, "SKILL.md");
|
|
6337
|
+
if (!existsSync21(skillMdPath)) {
|
|
5711
6338
|
throw new UserError(`no SKILL.md found at ${skillMdPath} \u2014 evolve requires an existing, installed skill directory`);
|
|
5712
6339
|
}
|
|
5713
6340
|
const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
|
|
@@ -5734,7 +6361,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
5734
6361
|
} else {
|
|
5735
6362
|
notices.push('using the offline "mock" backend \u2014 deterministic, no network calls, no session data leaves this machine.');
|
|
5736
6363
|
}
|
|
5737
|
-
const project =
|
|
6364
|
+
const project = resolvePath7(options.project ?? process.cwd());
|
|
5738
6365
|
const present = await detectSkillopt(exec, project);
|
|
5739
6366
|
if (!present) {
|
|
5740
6367
|
throw new UserError(
|
|
@@ -5765,33 +6392,33 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
5765
6392
|
}
|
|
5766
6393
|
return;
|
|
5767
6394
|
}
|
|
5768
|
-
const stagingRoot =
|
|
5769
|
-
const stagingDir =
|
|
5770
|
-
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot +
|
|
6395
|
+
const stagingRoot = join27(project, ".skillopt-sleep", "staging");
|
|
6396
|
+
const stagingDir = resolvePath7(parsed.staging_dir);
|
|
6397
|
+
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep6)) {
|
|
5771
6398
|
throw new UserError(
|
|
5772
6399
|
`skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
|
|
5773
6400
|
);
|
|
5774
6401
|
}
|
|
5775
|
-
const proposedSkillMd =
|
|
5776
|
-
const proposedClaudeMd =
|
|
5777
|
-
const reportMdPath =
|
|
5778
|
-
if (!
|
|
6402
|
+
const proposedSkillMd = join27(stagingDir, "proposed_SKILL.md");
|
|
6403
|
+
const proposedClaudeMd = join27(stagingDir, "proposed_CLAUDE.md");
|
|
6404
|
+
const reportMdPath = join27(stagingDir, "report.md");
|
|
6405
|
+
if (!existsSync21(proposedSkillMd)) {
|
|
5779
6406
|
throw new UserError(`skillopt-sleep reported an accepted proposal but ${proposedSkillMd} is missing`);
|
|
5780
6407
|
}
|
|
5781
|
-
if (
|
|
6408
|
+
if (existsSync21(reportMdPath)) {
|
|
5782
6409
|
notices.push(`SkillOpt-Sleep's full report: ${reportMdPath}`);
|
|
5783
6410
|
}
|
|
5784
6411
|
assertNotSymlink(proposedSkillMd, "proposed_SKILL.md");
|
|
5785
6412
|
assertNotSymlink(proposedClaudeMd, "proposed_CLAUDE.md");
|
|
5786
6413
|
const scannedSkillHash = sha256File(proposedSkillMd);
|
|
5787
6414
|
const scannedClaudeHash = sha256File(proposedClaudeMd);
|
|
5788
|
-
const tempDir = mkdtempSync3(
|
|
6415
|
+
const tempDir = mkdtempSync3(join27(tmpdir2(), "skill-forge-evolve-"));
|
|
5789
6416
|
let profile;
|
|
5790
6417
|
let safety;
|
|
5791
6418
|
try {
|
|
5792
|
-
cpSync3(proposedSkillMd,
|
|
5793
|
-
if (
|
|
5794
|
-
cpSync3(proposedClaudeMd,
|
|
6419
|
+
cpSync3(proposedSkillMd, join27(tempDir, "SKILL.md"));
|
|
6420
|
+
if (existsSync21(proposedClaudeMd)) {
|
|
6421
|
+
cpSync3(proposedClaudeMd, join27(tempDir, "CLAUDE.md"));
|
|
5795
6422
|
}
|
|
5796
6423
|
profile = asSkillProfile(profileSkill(tempDir), "for the evolved proposal");
|
|
5797
6424
|
const strictness = loadConfig().strictness;
|
|
@@ -5890,7 +6517,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
5890
6517
|
if (safetyVerdict === "block") process.exitCode = 1;
|
|
5891
6518
|
} else {
|
|
5892
6519
|
logOutcome(`[skill-forge] held \u2014 staging dir left in place -> ${stagingDir}`, json);
|
|
5893
|
-
if (
|
|
6520
|
+
if (existsSync21(reportMdPath)) {
|
|
5894
6521
|
logOutcome(`[skill-forge] see ${reportMdPath} for the full evidence report`, json);
|
|
5895
6522
|
}
|
|
5896
6523
|
}
|
|
@@ -5932,10 +6559,10 @@ function validateRunOutput(raw) {
|
|
|
5932
6559
|
return { accepted: Boolean(obj.accepted), staging_dir: stagingDirRaw ?? null };
|
|
5933
6560
|
}
|
|
5934
6561
|
function readManifest(stagingDir) {
|
|
5935
|
-
const manifestPath =
|
|
6562
|
+
const manifestPath = join27(stagingDir, "manifest.json");
|
|
5936
6563
|
let raw;
|
|
5937
6564
|
try {
|
|
5938
|
-
raw =
|
|
6565
|
+
raw = readFileSync18(manifestPath, "utf-8");
|
|
5939
6566
|
} catch (err) {
|
|
5940
6567
|
throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
|
|
5941
6568
|
}
|
|
@@ -5969,18 +6596,18 @@ function validateManifest(raw, manifestPath) {
|
|
|
5969
6596
|
};
|
|
5970
6597
|
}
|
|
5971
6598
|
function filesIdentical(a, b) {
|
|
5972
|
-
if (!
|
|
5973
|
-
return
|
|
6599
|
+
if (!existsSync21(a) || !existsSync21(b)) return false;
|
|
6600
|
+
return readFileSync18(a).equals(readFileSync18(b));
|
|
5974
6601
|
}
|
|
5975
6602
|
function isAlreadyAdopted(stagingDir, manifest) {
|
|
5976
|
-
const proposedSkillMd =
|
|
5977
|
-
const proposedClaudeMd =
|
|
6603
|
+
const proposedSkillMd = join27(stagingDir, "proposed_SKILL.md");
|
|
6604
|
+
const proposedClaudeMd = join27(stagingDir, "proposed_CLAUDE.md");
|
|
5978
6605
|
let sawComparison = false;
|
|
5979
|
-
if (
|
|
6606
|
+
if (existsSync21(proposedSkillMd)) {
|
|
5980
6607
|
sawComparison = true;
|
|
5981
6608
|
if (!filesIdentical(proposedSkillMd, manifest.live_skill_path)) return false;
|
|
5982
6609
|
}
|
|
5983
|
-
if (
|
|
6610
|
+
if (existsSync21(proposedClaudeMd) && manifest.live_memory_path) {
|
|
5984
6611
|
sawComparison = true;
|
|
5985
6612
|
if (!filesIdentical(proposedClaudeMd, manifest.live_memory_path)) return false;
|
|
5986
6613
|
}
|
|
@@ -5998,8 +6625,8 @@ function assertNotSymlink(path, label2) {
|
|
|
5998
6625
|
}
|
|
5999
6626
|
}
|
|
6000
6627
|
function sha256File(path) {
|
|
6001
|
-
if (!
|
|
6002
|
-
return createHash3("sha256").update(
|
|
6628
|
+
if (!existsSync21(path)) return null;
|
|
6629
|
+
return createHash3("sha256").update(readFileSync18(path)).digest("hex");
|
|
6003
6630
|
}
|
|
6004
6631
|
|
|
6005
6632
|
// src/commands/evolve.ts
|
|
@@ -6008,9 +6635,9 @@ async function evolveCommand(targetSkillDir, options = {}) {
|
|
|
6008
6635
|
}
|
|
6009
6636
|
|
|
6010
6637
|
// src/commands/finding.ts
|
|
6011
|
-
import { existsSync as
|
|
6638
|
+
import { existsSync as existsSync22 } from "fs";
|
|
6012
6639
|
function requireSavedConfig() {
|
|
6013
|
-
if (!
|
|
6640
|
+
if (!existsSync22(getConfigPath())) {
|
|
6014
6641
|
throw new UserError(
|
|
6015
6642
|
"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)."
|
|
6016
6643
|
);
|
|
@@ -6096,7 +6723,7 @@ async function findingRevokeCommand(prefix, options = {}) {
|
|
|
6096
6723
|
async function findingListCommand(options = {}) {
|
|
6097
6724
|
const { records, notices } = readAcceptedFindings();
|
|
6098
6725
|
for (const notice of notices) console.error(`[skill-forge] ${notice}`);
|
|
6099
|
-
const rows = options.stale ? records.filter((r) => !
|
|
6726
|
+
const rows = options.stale ? records.filter((r) => !existsSync22(r.target)) : records;
|
|
6100
6727
|
if (options.json) {
|
|
6101
6728
|
console.log(JSON.stringify(rows, null, 2));
|
|
6102
6729
|
return;
|
|
@@ -6114,24 +6741,24 @@ async function findingListCommand(options = {}) {
|
|
|
6114
6741
|
}
|
|
6115
6742
|
|
|
6116
6743
|
// src/commands/organize.ts
|
|
6117
|
-
import { resolve as
|
|
6744
|
+
import { resolve as resolvePath9 } from "path";
|
|
6118
6745
|
|
|
6119
6746
|
// src/organize.ts
|
|
6120
|
-
import { existsSync as
|
|
6121
|
-
import { basename as basename11, join as
|
|
6747
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19, readdirSync as readdirSync10, realpathSync as realpathSync5, statSync as statSync9 } from "fs";
|
|
6748
|
+
import { basename as basename11, join as join28, resolve as resolvePath8 } from "path";
|
|
6122
6749
|
var PRO_FEATURE_LABEL4 = "set-level organizer reports";
|
|
6123
6750
|
var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
|
|
6124
|
-
var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated"]);
|
|
6751
|
+
var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated", "experimental"]);
|
|
6125
6752
|
function listSubdirs2(dir) {
|
|
6126
6753
|
let entries;
|
|
6127
6754
|
try {
|
|
6128
|
-
entries =
|
|
6755
|
+
entries = readdirSync10(dir, { withFileTypes: true });
|
|
6129
6756
|
} catch {
|
|
6130
6757
|
return [];
|
|
6131
6758
|
}
|
|
6132
6759
|
const out = [];
|
|
6133
6760
|
for (const entry of entries) {
|
|
6134
|
-
const full =
|
|
6761
|
+
const full = join28(dir, entry.name);
|
|
6135
6762
|
if (entry.isDirectory()) {
|
|
6136
6763
|
out.push(full);
|
|
6137
6764
|
continue;
|
|
@@ -6149,14 +6776,14 @@ function findSkillDirsUnderRoot2(root) {
|
|
|
6149
6776
|
const found = /* @__PURE__ */ new Set();
|
|
6150
6777
|
const topDirs = listSubdirs2(root);
|
|
6151
6778
|
for (const dir of topDirs) {
|
|
6152
|
-
if (
|
|
6779
|
+
if (existsSync23(join28(dir, "SKILL.md"))) found.add(dir);
|
|
6153
6780
|
}
|
|
6154
|
-
for (const dir of listSubdirs2(
|
|
6155
|
-
if (
|
|
6781
|
+
for (const dir of listSubdirs2(join28(root, "skills"))) {
|
|
6782
|
+
if (existsSync23(join28(dir, "SKILL.md"))) found.add(dir);
|
|
6156
6783
|
}
|
|
6157
6784
|
for (const topDir of topDirs) {
|
|
6158
|
-
for (const dir of listSubdirs2(
|
|
6159
|
-
if (
|
|
6785
|
+
for (const dir of listSubdirs2(join28(topDir, "skills"))) {
|
|
6786
|
+
if (existsSync23(join28(dir, "SKILL.md"))) found.add(dir);
|
|
6160
6787
|
}
|
|
6161
6788
|
}
|
|
6162
6789
|
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
@@ -6164,7 +6791,7 @@ function findSkillDirsUnderRoot2(root) {
|
|
|
6164
6791
|
function collectCandidates(roots) {
|
|
6165
6792
|
const out = [];
|
|
6166
6793
|
for (const root of roots) {
|
|
6167
|
-
const resolvedRoot =
|
|
6794
|
+
const resolvedRoot = resolvePath8(expandHome(root));
|
|
6168
6795
|
if (!isDir(resolvedRoot)) continue;
|
|
6169
6796
|
for (const dirPath of findSkillDirsUnderRoot2(resolvedRoot)) {
|
|
6170
6797
|
out.push({ dirPath, root });
|
|
@@ -6201,57 +6828,85 @@ function extractFrontmatterBlock(text) {
|
|
|
6201
6828
|
if (closeIdx === -1) return null;
|
|
6202
6829
|
return afterOpen.slice(0, closeIdx);
|
|
6203
6830
|
}
|
|
6204
|
-
function
|
|
6205
|
-
const
|
|
6206
|
-
|
|
6207
|
-
|
|
6831
|
+
function indentOf(line) {
|
|
6832
|
+
const m = line.match(/^ */);
|
|
6833
|
+
return m ? m[0].length : 0;
|
|
6834
|
+
}
|
|
6835
|
+
function extractIndentedBlock(lines, key, parentIndent) {
|
|
6836
|
+
const keyRe = new RegExp(`^${" ".repeat(parentIndent)}${key}:\\s*$`);
|
|
6837
|
+
const startIdx = lines.findIndex((l) => keyRe.test(l));
|
|
6838
|
+
if (startIdx === -1) return null;
|
|
6839
|
+
const block = [];
|
|
6840
|
+
for (const line of lines.slice(startIdx + 1)) {
|
|
6841
|
+
if (line.trim() === "") {
|
|
6842
|
+
block.push(line);
|
|
6843
|
+
continue;
|
|
6844
|
+
}
|
|
6845
|
+
if (indentOf(line) <= parentIndent) break;
|
|
6846
|
+
block.push(line);
|
|
6208
6847
|
}
|
|
6848
|
+
return block;
|
|
6849
|
+
}
|
|
6850
|
+
function dedent(lines) {
|
|
6851
|
+
const nonBlank = lines.filter((l) => l.trim() !== "");
|
|
6852
|
+
if (nonBlank.length === 0) return "";
|
|
6853
|
+
const minIndent = Math.min(...nonBlank.map(indentOf));
|
|
6854
|
+
return lines.map((l) => l.trim() === "" ? "" : l.slice(minIndent)).join("\n");
|
|
6855
|
+
}
|
|
6856
|
+
function extractFlatFields(text, keys) {
|
|
6209
6857
|
const fields = {};
|
|
6210
|
-
for (const key of
|
|
6211
|
-
const m =
|
|
6858
|
+
for (const key of keys) {
|
|
6859
|
+
const m = text.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
6212
6860
|
if (m) fields[key] = stripQuotes(m[1].trim());
|
|
6213
6861
|
}
|
|
6862
|
+
return fields;
|
|
6863
|
+
}
|
|
6864
|
+
function parseNestedRhizeMetadata(block) {
|
|
6865
|
+
const lines = block.split(/\r\n|\r|\n/);
|
|
6866
|
+
const metaBlock = extractIndentedBlock(lines, "metadata", 0);
|
|
6867
|
+
if (metaBlock === null) return null;
|
|
6868
|
+
const rhizeLine = metaBlock.find((l) => /^\s*rhize:\s*$/.test(l));
|
|
6869
|
+
if (!rhizeLine) return null;
|
|
6870
|
+
const rhizeBlock = extractIndentedBlock(metaBlock, "rhize", indentOf(rhizeLine));
|
|
6871
|
+
if (rhizeBlock === null) return null;
|
|
6872
|
+
const dedented = dedent(rhizeBlock);
|
|
6873
|
+
const fields = extractFlatFields(dedented, ["tier", "domain", "provenance", "maturity"]);
|
|
6214
6874
|
return {
|
|
6215
|
-
name: fields.name ?? fallbackName,
|
|
6216
6875
|
tier: fields.tier,
|
|
6217
6876
|
domain: fields.domain,
|
|
6218
6877
|
provenance: fields.provenance,
|
|
6219
6878
|
maturity: fields.maturity,
|
|
6220
|
-
consumes: parseYamlList(
|
|
6221
|
-
validFrontmatter: true
|
|
6879
|
+
consumes: parseYamlList(dedented, "consumes")
|
|
6222
6880
|
};
|
|
6223
6881
|
}
|
|
6224
|
-
function
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
return {
|
|
6231
|
-
}
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
}
|
|
6253
|
-
}
|
|
6254
|
-
return { usage };
|
|
6882
|
+
function contributesAnyField(f) {
|
|
6883
|
+
return f.tier !== void 0 || f.domain !== void 0 || f.provenance !== void 0 || f.maturity !== void 0 || Boolean(f.consumes && f.consumes.length > 0);
|
|
6884
|
+
}
|
|
6885
|
+
function parseCapabilityFrontmatter(text, fallbackName) {
|
|
6886
|
+
const block = extractFrontmatterBlock(text);
|
|
6887
|
+
if (block === null) {
|
|
6888
|
+
return { name: fallbackName, consumes: [], validFrontmatter: false, tagSource: "none" };
|
|
6889
|
+
}
|
|
6890
|
+
const fields = extractFlatFields(block, ["name", "tier", "domain", "provenance", "maturity"]);
|
|
6891
|
+
const flatConsumes = parseYamlList(block, "consumes");
|
|
6892
|
+
const nested = parseNestedRhizeMetadata(block);
|
|
6893
|
+
const flatContributed = contributesAnyField({ ...fields, consumes: flatConsumes });
|
|
6894
|
+
const nestedContributed = nested !== null && contributesAnyField(nested);
|
|
6895
|
+
let tagSource;
|
|
6896
|
+
if (flatContributed && nestedContributed) tagSource = "mixed";
|
|
6897
|
+
else if (flatContributed) tagSource = "flat";
|
|
6898
|
+
else if (nestedContributed) tagSource = "metadata.rhize";
|
|
6899
|
+
else tagSource = "none";
|
|
6900
|
+
return {
|
|
6901
|
+
name: fields.name ?? fallbackName,
|
|
6902
|
+
tier: fields.tier ?? nested?.tier,
|
|
6903
|
+
domain: fields.domain ?? nested?.domain,
|
|
6904
|
+
provenance: fields.provenance ?? nested?.provenance,
|
|
6905
|
+
maturity: fields.maturity ?? nested?.maturity,
|
|
6906
|
+
consumes: flatConsumes.length > 0 ? flatConsumes : nested?.consumes ?? [],
|
|
6907
|
+
validFrontmatter: true,
|
|
6908
|
+
tagSource
|
|
6909
|
+
};
|
|
6255
6910
|
}
|
|
6256
6911
|
var WHITE = 0;
|
|
6257
6912
|
var GRAY = 1;
|
|
@@ -6324,17 +6979,17 @@ function buildRegistryEntries(candidates, warnings) {
|
|
|
6324
6979
|
for (const candidate of candidates) {
|
|
6325
6980
|
let realPath;
|
|
6326
6981
|
try {
|
|
6327
|
-
realPath =
|
|
6982
|
+
realPath = realpathSync5(candidate.dirPath);
|
|
6328
6983
|
} catch (err) {
|
|
6329
6984
|
warnings.push(`could not resolve skill directory ${candidate.dirPath}: ${err.message}`);
|
|
6330
6985
|
continue;
|
|
6331
6986
|
}
|
|
6332
6987
|
if (seenRealPaths.has(realPath)) continue;
|
|
6333
6988
|
seenRealPaths.add(realPath);
|
|
6334
|
-
const skillMdPath =
|
|
6989
|
+
const skillMdPath = join28(realPath, "SKILL.md");
|
|
6335
6990
|
let text;
|
|
6336
6991
|
try {
|
|
6337
|
-
text =
|
|
6992
|
+
text = readFileSync19(skillMdPath, "utf-8");
|
|
6338
6993
|
} catch (err) {
|
|
6339
6994
|
warnings.push(`could not read ${skillMdPath}: ${err.message}`);
|
|
6340
6995
|
continue;
|
|
@@ -6357,7 +7012,8 @@ function buildRegistryEntries(candidates, warnings) {
|
|
|
6357
7012
|
consumes: fm.consumes,
|
|
6358
7013
|
provenance: fm.provenance,
|
|
6359
7014
|
maturity: fm.maturity,
|
|
6360
|
-
validFrontmatter: fm.validFrontmatter
|
|
7015
|
+
validFrontmatter: fm.validFrontmatter,
|
|
7016
|
+
tagSource: fm.tagSource
|
|
6361
7017
|
});
|
|
6362
7018
|
}
|
|
6363
7019
|
return entries;
|
|
@@ -6446,7 +7102,7 @@ function buildOrganizeReport(config, options = {}) {
|
|
|
6446
7102
|
|
|
6447
7103
|
// src/commands/organize.ts
|
|
6448
7104
|
function writeReport(outPath, contents) {
|
|
6449
|
-
const resolved =
|
|
7105
|
+
const resolved = resolvePath9(outPath);
|
|
6450
7106
|
writeNewFile(resolved, contents, "--out");
|
|
6451
7107
|
return resolved;
|
|
6452
7108
|
}
|
|
@@ -6739,13 +7395,13 @@ async function find(query, options = {}, deps = {}) {
|
|
|
6739
7395
|
}
|
|
6740
7396
|
|
|
6741
7397
|
// src/commands/watch.ts
|
|
6742
|
-
import { realpathSync as
|
|
6743
|
-
import { join as
|
|
7398
|
+
import { realpathSync as realpathSync6 } from "fs";
|
|
7399
|
+
import { join as join29, resolve as resolvePath10 } from "path";
|
|
6744
7400
|
|
|
6745
7401
|
// src/gate/skillMapDrift.ts
|
|
6746
7402
|
import { createHash as createHash4 } from "crypto";
|
|
6747
|
-
import { readFileSync as
|
|
6748
|
-
import { dirname as dirname10, isAbsolute as
|
|
7403
|
+
import { readFileSync as readFileSync20, existsSync as existsSync24 } from "fs";
|
|
7404
|
+
import { dirname as dirname10, isAbsolute as isAbsolute4, resolve as resolve7 } from "path";
|
|
6749
7405
|
function sha256(text) {
|
|
6750
7406
|
return createHash4("sha256").update(text, "utf-8").digest("hex");
|
|
6751
7407
|
}
|
|
@@ -6778,22 +7434,22 @@ function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
|
|
|
6778
7434
|
}
|
|
6779
7435
|
function resolveLocalPath(path, mapDir) {
|
|
6780
7436
|
const expanded = expandHome(path);
|
|
6781
|
-
if (
|
|
7437
|
+
if (isAbsolute4(expanded)) return resolve7(expanded);
|
|
6782
7438
|
const candidates = [resolve7(expanded)];
|
|
6783
7439
|
if (mapDir) {
|
|
6784
7440
|
candidates.push(resolve7(mapDir, expanded));
|
|
6785
7441
|
candidates.push(resolve7(mapDir, "..", expanded));
|
|
6786
7442
|
}
|
|
6787
7443
|
for (const candidate of candidates) {
|
|
6788
|
-
if (
|
|
7444
|
+
if (existsSync24(candidate)) return candidate;
|
|
6789
7445
|
}
|
|
6790
7446
|
return candidates[0];
|
|
6791
7447
|
}
|
|
6792
7448
|
function defaultReadLocalFile(path, mapDir) {
|
|
6793
7449
|
const resolved = resolveLocalPath(path, mapDir);
|
|
6794
|
-
if (!
|
|
7450
|
+
if (!existsSync24(resolved)) return void 0;
|
|
6795
7451
|
try {
|
|
6796
|
-
return
|
|
7452
|
+
return readFileSync20(resolved, "utf-8");
|
|
6797
7453
|
} catch {
|
|
6798
7454
|
return void 0;
|
|
6799
7455
|
}
|
|
@@ -6834,7 +7490,7 @@ async function computeMapDrift(map, deps = {}) {
|
|
|
6834
7490
|
continue;
|
|
6835
7491
|
}
|
|
6836
7492
|
const localResolved = resolveLocalPath(localNode.path, mapDir);
|
|
6837
|
-
if (!
|
|
7493
|
+
if (!existsSync24(localResolved)) {
|
|
6838
7494
|
rows.push({
|
|
6839
7495
|
skill: skillLabel,
|
|
6840
7496
|
upstream,
|
|
@@ -6940,12 +7596,12 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
6940
7596
|
const warnings = [];
|
|
6941
7597
|
const seenRealPaths = /* @__PURE__ */ new Set();
|
|
6942
7598
|
for (const root of config.skillsRoots) {
|
|
6943
|
-
const resolvedRoot =
|
|
7599
|
+
const resolvedRoot = resolvePath10(expandHome(root));
|
|
6944
7600
|
const exists = isDir(resolvedRoot);
|
|
6945
7601
|
let realPath = resolvedRoot;
|
|
6946
7602
|
if (exists) {
|
|
6947
7603
|
try {
|
|
6948
|
-
realPath =
|
|
7604
|
+
realPath = realpathSync6(resolvedRoot);
|
|
6949
7605
|
} catch {
|
|
6950
7606
|
realPath = resolvedRoot;
|
|
6951
7607
|
}
|
|
@@ -6957,7 +7613,7 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
6957
7613
|
ledger = readProvenanceLedger(resolvedRoot);
|
|
6958
7614
|
} catch (err) {
|
|
6959
7615
|
const message = err instanceof Error ? err.message : String(err);
|
|
6960
|
-
const ledgerPath =
|
|
7616
|
+
const ledgerPath = join29(resolvedRoot, "SOURCES.md");
|
|
6961
7617
|
roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
|
|
6962
7618
|
warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
|
|
6963
7619
|
continue;
|
|
@@ -7067,8 +7723,8 @@ async function watchCommand(options = {}) {
|
|
|
7067
7723
|
}
|
|
7068
7724
|
|
|
7069
7725
|
// src/commands/routine.ts
|
|
7070
|
-
import { existsSync as
|
|
7071
|
-
import { join as
|
|
7726
|
+
import { existsSync as existsSync25, rmSync as rmSync7, statSync as statSync10 } from "fs";
|
|
7727
|
+
import { join as join30 } from "path";
|
|
7072
7728
|
var PRO_FEATURE_LABEL6 = "the scheduled maintenance routine";
|
|
7073
7729
|
var SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
7074
7730
|
function parseFailOnFloor(failOn) {
|
|
@@ -7089,7 +7745,7 @@ function exceedsFloor(audit2, failOn) {
|
|
|
7089
7745
|
}
|
|
7090
7746
|
function resolveReportPath2(override, generatedAt) {
|
|
7091
7747
|
if (override) return override;
|
|
7092
|
-
return
|
|
7748
|
+
return join30(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
|
|
7093
7749
|
}
|
|
7094
7750
|
function runHousekeeping(config, options) {
|
|
7095
7751
|
const actions = [];
|
|
@@ -7098,7 +7754,7 @@ function runHousekeeping(config, options) {
|
|
|
7098
7754
|
const pruneDays = options.pruneDays ?? 30;
|
|
7099
7755
|
const cutoff = Date.now() - pruneDays * 864e5;
|
|
7100
7756
|
for (const id of listQuarantineIds(config.quarantineDir)) {
|
|
7101
|
-
const path =
|
|
7757
|
+
const path = join30(config.quarantineDir, id);
|
|
7102
7758
|
let mtime;
|
|
7103
7759
|
try {
|
|
7104
7760
|
mtime = statSync10(path).mtimeMs;
|
|
@@ -7136,7 +7792,7 @@ function runHousekeeping(config, options) {
|
|
|
7136
7792
|
}
|
|
7137
7793
|
for (const entry of entries) {
|
|
7138
7794
|
if (entry.status !== "pending") continue;
|
|
7139
|
-
if (!entry.installedPath ||
|
|
7795
|
+
if (!entry.installedPath || existsSync25(entry.installedPath)) continue;
|
|
7140
7796
|
if (apply) {
|
|
7141
7797
|
try {
|
|
7142
7798
|
updateQueueEntry(entry.id, { status: "dismissed" });
|
|
@@ -7227,7 +7883,7 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
|
|
|
7227
7883
|
lines.push("");
|
|
7228
7884
|
return lines.join("\n");
|
|
7229
7885
|
}
|
|
7230
|
-
async function routineCommand(options = {}) {
|
|
7886
|
+
async function routineCommand(options = {}, deps = {}) {
|
|
7231
7887
|
const pro = proFeatureStatus(PRO_FEATURE_LABEL6);
|
|
7232
7888
|
const log = options.json ? console.error : console.log;
|
|
7233
7889
|
if (pro.notice) log(`[skill-forge] ${pro.notice}`);
|
|
@@ -7240,9 +7896,11 @@ async function routineCommand(options = {}) {
|
|
|
7240
7896
|
}
|
|
7241
7897
|
const config = loadConfig();
|
|
7242
7898
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7899
|
+
const { pluginRoots, usage, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
7243
7900
|
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
7244
|
-
|
|
7245
|
-
const
|
|
7901
|
+
const notices = [...storeNotices, ...pluginUsageNotices];
|
|
7902
|
+
for (const notice of notices) log(`[skill-forge] ${notice}`);
|
|
7903
|
+
const audit2 = await auditConfiguredSet(config, { accepted, pluginRoots, usage });
|
|
7246
7904
|
const auditState = summarizeAudit(audit2);
|
|
7247
7905
|
const degraded = [];
|
|
7248
7906
|
let watch;
|
|
@@ -7283,9 +7941,10 @@ async function routineCommand(options = {}) {
|
|
|
7283
7941
|
notices: watch.notices
|
|
7284
7942
|
} : void 0,
|
|
7285
7943
|
organize: organize ? { skills: organize.registry.count, roots: organize.registry.skillsRoots.length, notices: organize.notices } : void 0,
|
|
7944
|
+
plugins: audit2.plugins,
|
|
7286
7945
|
pending: { queue: pending.count, quarantine: quarantine2.count },
|
|
7287
7946
|
housekeeping,
|
|
7288
|
-
notices
|
|
7947
|
+
notices,
|
|
7289
7948
|
degraded
|
|
7290
7949
|
};
|
|
7291
7950
|
const failure = degraded.length > 0 ? `${degraded.length} step(s) did not complete: ${degraded.join("; ")}` : exceedsFloor(auditState, options.failOn ?? "none");
|
|
@@ -7307,6 +7966,7 @@ async function routineCommand(options = {}) {
|
|
|
7307
7966
|
console.log(` accepted: ${auditState.acceptedCount ?? 0} (suppressed from counts; see report)`);
|
|
7308
7967
|
if (result.watch) console.log(` drift: ${result.watch.moved} moved of ${result.watch.checked} tracked`);
|
|
7309
7968
|
if (result.organize) console.log(` registry: ${result.organize.skills} skill(s)`);
|
|
7969
|
+
if (result.plugins) console.log(` ${summarizePluginRecommendations(result.plugins)}`);
|
|
7310
7970
|
console.log(` pending: ${result.pending.queue} queue, ${result.pending.quarantine} quarantine`);
|
|
7311
7971
|
for (const action of housekeeping) {
|
|
7312
7972
|
const label2 = action.outcome === "applied" ? "housekeeping" : action.outcome === "failed" ? "FAILED" : "would do";
|
|
@@ -7328,8 +7988,8 @@ async function routineCommand(options = {}) {
|
|
|
7328
7988
|
}
|
|
7329
7989
|
|
|
7330
7990
|
// src/commands/ingest.ts
|
|
7331
|
-
import { realpathSync as
|
|
7332
|
-
import { dirname as dirname12, isAbsolute as
|
|
7991
|
+
import { realpathSync as realpathSync7 } from "fs";
|
|
7992
|
+
import { dirname as dirname12, isAbsolute as isAbsolute5, relative as relative4, resolve as resolvePath11 } from "path";
|
|
7333
7993
|
var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
|
|
7334
7994
|
var VALID_STATUSES = ["ingested", "dismissed"];
|
|
7335
7995
|
function requireProFeature() {
|
|
@@ -7370,19 +8030,19 @@ function printPendingList(pending, json) {
|
|
|
7370
8030
|
}
|
|
7371
8031
|
function realpathOrNull(p) {
|
|
7372
8032
|
try {
|
|
7373
|
-
return
|
|
8033
|
+
return realpathSync7(p);
|
|
7374
8034
|
} catch {
|
|
7375
8035
|
return null;
|
|
7376
8036
|
}
|
|
7377
8037
|
}
|
|
7378
8038
|
function isUnderRoot(candidate, root) {
|
|
7379
|
-
const rel =
|
|
7380
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
8039
|
+
const rel = relative4(root, candidate);
|
|
8040
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
|
|
7381
8041
|
}
|
|
7382
8042
|
function validatePendingEntries(entries, config) {
|
|
7383
|
-
const quarantineRoot = realpathOrNull(
|
|
7384
|
-
const skillsRoots = config.skillsRoots.map((r) => realpathOrNull(
|
|
7385
|
-
const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname12(
|
|
8043
|
+
const quarantineRoot = realpathOrNull(resolvePath11(config.quarantineDir));
|
|
8044
|
+
const skillsRoots = config.skillsRoots.map((r) => realpathOrNull(resolvePath11(r))).filter((r) => r !== null);
|
|
8045
|
+
const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname12(resolvePath11(f)))).filter((r) => r !== null);
|
|
7386
8046
|
const allowedRoots = [quarantineRoot, ...skillsRoots, ...mcpTargetDirs].filter((r) => r !== null);
|
|
7387
8047
|
const valid = [];
|
|
7388
8048
|
const excluded = [];
|
|
@@ -7392,7 +8052,7 @@ function validatePendingEntries(entries, config) {
|
|
|
7392
8052
|
excluded.push({ entry, reason: "no installedPath or quarantinePath recorded on the entry" });
|
|
7393
8053
|
continue;
|
|
7394
8054
|
}
|
|
7395
|
-
const real = realpathOrNull(
|
|
8055
|
+
const real = realpathOrNull(resolvePath11(effectivePath));
|
|
7396
8056
|
if (!real) {
|
|
7397
8057
|
excluded.push({ entry, reason: `path does not exist or could not be resolved: ${effectivePath}` });
|
|
7398
8058
|
continue;
|
|
@@ -7459,15 +8119,15 @@ import {
|
|
|
7459
8119
|
chmodSync as chmodSync3,
|
|
7460
8120
|
lstatSync as lstatSync6,
|
|
7461
8121
|
mkdirSync as mkdirSync9,
|
|
7462
|
-
readFileSync as
|
|
8122
|
+
readFileSync as readFileSync24,
|
|
7463
8123
|
readSync as readSync3,
|
|
7464
8124
|
writeFileSync as writeFileSync8
|
|
7465
8125
|
} from "fs";
|
|
7466
|
-
import { dirname as dirname13, isAbsolute as
|
|
8126
|
+
import { dirname as dirname13, isAbsolute as isAbsolute7, join as join35, relative as relative6, resolve as resolvePath12 } from "path";
|
|
7467
8127
|
|
|
7468
8128
|
// src/insights/inventory.ts
|
|
7469
|
-
import { readFileSync as
|
|
7470
|
-
import { join as
|
|
8129
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
8130
|
+
import { join as join31 } from "path";
|
|
7471
8131
|
|
|
7472
8132
|
// src/insights/models.ts
|
|
7473
8133
|
import { createHash as createHash5 } from "crypto";
|
|
@@ -7581,7 +8241,7 @@ function declaredMapCoverage(path, claim) {
|
|
|
7581
8241
|
}
|
|
7582
8242
|
function fingerprintInstalledSkill(path) {
|
|
7583
8243
|
try {
|
|
7584
|
-
return { fingerprint: stableHash(
|
|
8244
|
+
return { fingerprint: stableHash(readFileSync21(join31(path, "SKILL.md"), "utf-8")) };
|
|
7585
8245
|
} catch (error) {
|
|
7586
8246
|
return { reason: `could not fingerprint SKILL.md: ${error.message}` };
|
|
7587
8247
|
}
|
|
@@ -7731,12 +8391,12 @@ function buildInsightInventory(config, options = {}) {
|
|
|
7731
8391
|
}
|
|
7732
8392
|
|
|
7733
8393
|
// src/insights/jira.ts
|
|
7734
|
-
import { existsSync as
|
|
7735
|
-
import { join as
|
|
8394
|
+
import { existsSync as existsSync27, readFileSync as readFileSync23 } from "fs";
|
|
8395
|
+
import { join as join34 } from "path";
|
|
7736
8396
|
|
|
7737
8397
|
// src/insights/store.ts
|
|
7738
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
7739
|
-
import { join as
|
|
8398
|
+
import { chmodSync as chmodSync2, existsSync as existsSync26, mkdirSync as mkdirSync8, readFileSync as readFileSync22 } from "fs";
|
|
8399
|
+
import { join as join33 } from "path";
|
|
7740
8400
|
|
|
7741
8401
|
// src/insights/sources.ts
|
|
7742
8402
|
import { createHash as createHash6 } from "crypto";
|
|
@@ -7747,11 +8407,11 @@ import {
|
|
|
7747
8407
|
lstatSync as lstatSync5,
|
|
7748
8408
|
openSync as openSync3,
|
|
7749
8409
|
readSync as readSync2,
|
|
7750
|
-
readdirSync as
|
|
7751
|
-
realpathSync as
|
|
8410
|
+
readdirSync as readdirSync11,
|
|
8411
|
+
realpathSync as realpathSync8,
|
|
7752
8412
|
statSync as statSync11
|
|
7753
8413
|
} from "fs";
|
|
7754
|
-
import { basename as basename12, extname as
|
|
8414
|
+
import { basename as basename12, extname as extname8, isAbsolute as isAbsolute6, join as join32, relative as relative5, resolve as resolve8, sep as sep7 } from "path";
|
|
7755
8415
|
var DEFAULT_MAX_SOURCE_FILE_BYTES = 5 * 1024 * 1024;
|
|
7756
8416
|
var DEFAULT_MAX_SOURCE_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
7757
8417
|
var SKIP_DIRS4 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "__pycache__", ".venv", "venv"]);
|
|
@@ -7821,9 +8481,9 @@ function canonicalPublicUrl(raw, label2) {
|
|
|
7821
8481
|
url.hash = "";
|
|
7822
8482
|
return url.toString();
|
|
7823
8483
|
}
|
|
7824
|
-
function
|
|
7825
|
-
const rel =
|
|
7826
|
-
return rel === "" || !rel.startsWith(`..${
|
|
8484
|
+
function isInside3(child, root) {
|
|
8485
|
+
const rel = relative5(root, child);
|
|
8486
|
+
return rel === "" || !rel.startsWith(`..${sep7}`) && rel !== ".." && !isAbsolute6(rel);
|
|
7827
8487
|
}
|
|
7828
8488
|
function isHiddenName(name) {
|
|
7829
8489
|
return name.startsWith(".") && name !== "." && name !== "..";
|
|
@@ -7832,7 +8492,7 @@ function hasCredentialShapedName(name) {
|
|
|
7832
8492
|
return name === ".env" || name.startsWith(".env.") || CREDENTIAL_NAME.test(name);
|
|
7833
8493
|
}
|
|
7834
8494
|
function mediaTypeFor(path, declared) {
|
|
7835
|
-
return declared ?? SUPPORTED_MEDIA_BY_EXTENSION[
|
|
8495
|
+
return declared ?? SUPPORTED_MEDIA_BY_EXTENSION[extname8(path).toLowerCase()];
|
|
7836
8496
|
}
|
|
7837
8497
|
function suspiciousInstructionFlags(bytes, mediaType) {
|
|
7838
8498
|
if (!mediaType.startsWith("text/")) return [];
|
|
@@ -7887,7 +8547,7 @@ function observeFile(path, relativePath, declaredMediaType, maxFileBytes, remain
|
|
|
7887
8547
|
if (!bytes || !descriptorStat) throw new UserError(`source file could not be observed: ${relativePath}`);
|
|
7888
8548
|
let realPath;
|
|
7889
8549
|
try {
|
|
7890
|
-
realPath =
|
|
8550
|
+
realPath = realpathSync8(path);
|
|
7891
8551
|
const pathStat = statSync11(realPath);
|
|
7892
8552
|
if (pathStat.dev !== descriptorStat.dev || pathStat.ino !== descriptorStat.ino) {
|
|
7893
8553
|
throw new Error("path identity changed");
|
|
@@ -8069,7 +8729,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8069
8729
|
}
|
|
8070
8730
|
if (lexicalStat.isSymbolicLink()) throw new UserError(`source directory must not be a symlink: ${resolved}`);
|
|
8071
8731
|
if (!lexicalStat.isDirectory()) throw new UserError(`source directory is not a directory: ${resolved}`);
|
|
8072
|
-
const realRoot =
|
|
8732
|
+
const realRoot = realpathSync8(resolved);
|
|
8073
8733
|
const observations = [];
|
|
8074
8734
|
const skipped = [];
|
|
8075
8735
|
const seenFiles = /* @__PURE__ */ new Set();
|
|
@@ -8078,12 +8738,12 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8078
8738
|
const walk2 = (dir, logicalDir) => {
|
|
8079
8739
|
let entries;
|
|
8080
8740
|
try {
|
|
8081
|
-
entries =
|
|
8741
|
+
entries = readdirSync11(dir, { withFileTypes: true }).sort((a, b) => compareStrings2(a.name, b.name));
|
|
8082
8742
|
} catch (err) {
|
|
8083
8743
|
throw new UserError(`unable to enumerate source directory ${dir}: ${err.message}`);
|
|
8084
8744
|
}
|
|
8085
8745
|
for (const entry of entries) {
|
|
8086
|
-
const full =
|
|
8746
|
+
const full = join32(dir, entry.name);
|
|
8087
8747
|
const logicalPath = logicalDir ? `${logicalDir}/${entry.name}` : entry.name;
|
|
8088
8748
|
if (entry.isDirectory() && SKIP_DIRS4.has(entry.name)) {
|
|
8089
8749
|
skipped.push({ path: logicalPath, reason: "ignored-directory" });
|
|
@@ -8100,11 +8760,11 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8100
8760
|
if (entry.isSymbolicLink()) {
|
|
8101
8761
|
let realTarget;
|
|
8102
8762
|
try {
|
|
8103
|
-
realTarget =
|
|
8763
|
+
realTarget = realpathSync8(full);
|
|
8104
8764
|
} catch {
|
|
8105
8765
|
throw new UserError(`source directory contains an unreadable symlink: ${logicalPath}`);
|
|
8106
8766
|
}
|
|
8107
|
-
if (!
|
|
8767
|
+
if (!isInside3(realTarget, realRoot)) {
|
|
8108
8768
|
throw new UserError(`source directory symlink escapes its root: ${logicalPath}`);
|
|
8109
8769
|
}
|
|
8110
8770
|
const targetStat = statSync11(realTarget);
|
|
@@ -8119,7 +8779,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8119
8779
|
continue;
|
|
8120
8780
|
}
|
|
8121
8781
|
} else if (entry.isDirectory()) {
|
|
8122
|
-
const realDir =
|
|
8782
|
+
const realDir = realpathSync8(full);
|
|
8123
8783
|
if (seenDirs.has(realDir)) continue;
|
|
8124
8784
|
seenDirs.add(realDir);
|
|
8125
8785
|
walk2(full, logicalPath);
|
|
@@ -8128,8 +8788,8 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8128
8788
|
skipped.push({ path: logicalPath, reason: "unsupported-media-type" });
|
|
8129
8789
|
continue;
|
|
8130
8790
|
}
|
|
8131
|
-
const realFile =
|
|
8132
|
-
if (!
|
|
8791
|
+
const realFile = realpathSync8(full);
|
|
8792
|
+
if (!isInside3(realFile, realRoot)) {
|
|
8133
8793
|
throw new UserError(`source directory entry escaped its root while being observed: ${logicalPath}`);
|
|
8134
8794
|
}
|
|
8135
8795
|
if (seenFiles.has(realFile)) {
|
|
@@ -8235,17 +8895,17 @@ function normalizeSources(inputs, options = {}) {
|
|
|
8235
8895
|
// src/insights/store.ts
|
|
8236
8896
|
var STUDY_ID = /^STUDY-[0-9a-f]{16}$/;
|
|
8237
8897
|
function getInsightsDir() {
|
|
8238
|
-
return
|
|
8898
|
+
return join33(getHomeDir(), "insights");
|
|
8239
8899
|
}
|
|
8240
8900
|
function getStudyDir(studyId) {
|
|
8241
8901
|
assertStudyId(studyId);
|
|
8242
|
-
return
|
|
8902
|
+
return join33(getInsightsDir(), studyId);
|
|
8243
8903
|
}
|
|
8244
8904
|
function getStudyPath(studyId) {
|
|
8245
|
-
return
|
|
8905
|
+
return join33(getStudyDir(studyId), "study.json");
|
|
8246
8906
|
}
|
|
8247
8907
|
function getStudyEventsPath(studyId) {
|
|
8248
|
-
return
|
|
8908
|
+
return join33(getStudyDir(studyId), "events.jsonl");
|
|
8249
8909
|
}
|
|
8250
8910
|
function createStudyId(sourceInventoryHash, createdAt) {
|
|
8251
8911
|
return stableId("STUDY", { sourceInventoryHash, createdAt });
|
|
@@ -8376,7 +9036,7 @@ function validateStudyShape(value, path) {
|
|
|
8376
9036
|
function parseStudy(path) {
|
|
8377
9037
|
let raw;
|
|
8378
9038
|
try {
|
|
8379
|
-
raw =
|
|
9039
|
+
raw = readFileSync22(path, "utf-8");
|
|
8380
9040
|
} catch (err) {
|
|
8381
9041
|
throw new UserError(`unable to read insight study at ${path}: ${err.message}`);
|
|
8382
9042
|
}
|
|
@@ -8410,7 +9070,7 @@ function createInsightStudy(input, options = {}) {
|
|
|
8410
9070
|
const studyId = options.studyId ?? createStudyId(input.sourceInventoryHash, createdAt);
|
|
8411
9071
|
assertStudyId(studyId);
|
|
8412
9072
|
const path = getStudyPath(studyId);
|
|
8413
|
-
if (
|
|
9073
|
+
if (existsSync26(path)) throw new UserError(`insight study already exists: ${studyId}`);
|
|
8414
9074
|
const study = {
|
|
8415
9075
|
schemaVersion: INSIGHT_SCHEMA_VERSION,
|
|
8416
9076
|
studyId,
|
|
@@ -8431,7 +9091,7 @@ function createInsightStudy(input, options = {}) {
|
|
|
8431
9091
|
}
|
|
8432
9092
|
function readInsightStudy(studyId) {
|
|
8433
9093
|
const path = getStudyPath(studyId);
|
|
8434
|
-
if (!
|
|
9094
|
+
if (!existsSync26(path)) throw new UserError(`no insight study with id: ${studyId}`);
|
|
8435
9095
|
const study = parseStudy(path);
|
|
8436
9096
|
if (study.studyId !== studyId) {
|
|
8437
9097
|
throw new UserError(`malformed insight study at ${path}: stored studyId does not match its directory`);
|
|
@@ -8441,7 +9101,7 @@ function readInsightStudy(studyId) {
|
|
|
8441
9101
|
function writeInsightStudy(study, now = /* @__PURE__ */ new Date()) {
|
|
8442
9102
|
assertStudyId(study.studyId);
|
|
8443
9103
|
const path = getStudyPath(study.studyId);
|
|
8444
|
-
if (!
|
|
9104
|
+
if (!existsSync26(path)) throw new UserError(`no insight study with id: ${study.studyId}`);
|
|
8445
9105
|
const existing = parseStudy(path);
|
|
8446
9106
|
let next = { ...study, updatedAt: now.toISOString() };
|
|
8447
9107
|
if (next.status === "dismissed" || next.status === "archived") {
|
|
@@ -9178,7 +9838,7 @@ function buildJiraManifest(study, projectKey, options = {}) {
|
|
|
9178
9838
|
return manifest;
|
|
9179
9839
|
}
|
|
9180
9840
|
function getJiraReceiptsPath(studyId) {
|
|
9181
|
-
return
|
|
9841
|
+
return join34(getStudyDir(studyId), "jira-receipts.jsonl");
|
|
9182
9842
|
}
|
|
9183
9843
|
function assertCurrentJiraManifest(study, value) {
|
|
9184
9844
|
if (study.status !== "jira-manifested" || !study.jira) {
|
|
@@ -9310,10 +9970,10 @@ function appendJiraReceipt(studyId, manifest, input) {
|
|
|
9310
9970
|
function readJiraReceipts(studyId, manifest) {
|
|
9311
9971
|
const currentManifest = assertCurrentJiraManifest(readInsightStudy(studyId), manifest);
|
|
9312
9972
|
const path = getJiraReceiptsPath(studyId);
|
|
9313
|
-
if (!
|
|
9973
|
+
if (!existsSync27(path)) return [];
|
|
9314
9974
|
let text;
|
|
9315
9975
|
try {
|
|
9316
|
-
text =
|
|
9976
|
+
text = readFileSync23(path, "utf-8");
|
|
9317
9977
|
} catch (error) {
|
|
9318
9978
|
throw new UserError(`unable to read Jira receipts at ${path}: ${error.message}`);
|
|
9319
9979
|
}
|
|
@@ -9452,10 +10112,10 @@ function nonEmptyString(value, label2) {
|
|
|
9452
10112
|
return value;
|
|
9453
10113
|
}
|
|
9454
10114
|
function parseJsonFile(path, label2) {
|
|
9455
|
-
const resolved =
|
|
10115
|
+
const resolved = resolvePath12(path);
|
|
9456
10116
|
let raw;
|
|
9457
10117
|
try {
|
|
9458
|
-
raw =
|
|
10118
|
+
raw = readFileSync24(resolved, "utf-8");
|
|
9459
10119
|
} catch (error) {
|
|
9460
10120
|
throw new UserError(`unable to read ${label2} at ${resolved}: ${error.message}`);
|
|
9461
10121
|
}
|
|
@@ -9510,7 +10170,7 @@ function assertCommonDescriptorFields(value, label2) {
|
|
|
9510
10170
|
}
|
|
9511
10171
|
function pathRelativeTo(baseDir, raw, label2) {
|
|
9512
10172
|
const path = nonEmptyString(raw, label2);
|
|
9513
|
-
return
|
|
10173
|
+
return isAbsolute7(path) ? path : resolvePath12(baseDir, path);
|
|
9514
10174
|
}
|
|
9515
10175
|
function parseManifestDescriptor(value, index, baseDir) {
|
|
9516
10176
|
const label2 = `source manifest entry ${index + 1}`;
|
|
@@ -9548,7 +10208,7 @@ function parseManifestDescriptor(value, index, baseDir) {
|
|
|
9548
10208
|
throw new UserError(`${label2}.type must be url|file|directory|connector|stdin`);
|
|
9549
10209
|
}
|
|
9550
10210
|
function readManifest2(path) {
|
|
9551
|
-
const resolved =
|
|
10211
|
+
const resolved = resolvePath12(path);
|
|
9552
10212
|
const parsed = parseJsonFile(resolved, "source manifest");
|
|
9553
10213
|
let values;
|
|
9554
10214
|
if (Array.isArray(parsed)) {
|
|
@@ -9581,7 +10241,7 @@ function positionalSource(value) {
|
|
|
9581
10241
|
if (process.stdin.isTTY) throw new UserError(`stdin source "-" requires piped input`);
|
|
9582
10242
|
return { type: "stdin", content: readBoundedStdin() };
|
|
9583
10243
|
}
|
|
9584
|
-
const resolved =
|
|
10244
|
+
const resolved = resolvePath12(value);
|
|
9585
10245
|
let stat;
|
|
9586
10246
|
try {
|
|
9587
10247
|
stat = lstatSync6(resolved);
|
|
@@ -9600,15 +10260,15 @@ function collectSourceInputs(sourceArgs, options) {
|
|
|
9600
10260
|
return inputs;
|
|
9601
10261
|
}
|
|
9602
10262
|
function contextPath(studyId) {
|
|
9603
|
-
return
|
|
10263
|
+
return join35(getStudyDir(studyId), CONTEXT_FILENAME);
|
|
9604
10264
|
}
|
|
9605
10265
|
function inventoryPath(studyId) {
|
|
9606
|
-
return
|
|
10266
|
+
return join35(getStudyDir(studyId), INVENTORY_FILENAME);
|
|
9607
10267
|
}
|
|
9608
10268
|
function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory) {
|
|
9609
10269
|
const context = {
|
|
9610
10270
|
schemaVersion: 1,
|
|
9611
|
-
skillMapPath: skillMapPath ?
|
|
10271
|
+
skillMapPath: skillMapPath ? resolvePath12(skillMapPath) : void 0,
|
|
9612
10272
|
skillMapCoverage
|
|
9613
10273
|
};
|
|
9614
10274
|
writeJsonAtomic(contextPath(studyId), context);
|
|
@@ -9617,7 +10277,7 @@ function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory)
|
|
|
9617
10277
|
function readCommandContext(studyId) {
|
|
9618
10278
|
const path = contextPath(studyId);
|
|
9619
10279
|
const parsed = parseJsonFile(path, "insight command context");
|
|
9620
|
-
if (!isRecord5(parsed) || parsed.schemaVersion !== 1 || parsed.skillMapPath !== void 0 && (typeof parsed.skillMapPath !== "string" || !
|
|
10280
|
+
if (!isRecord5(parsed) || parsed.schemaVersion !== 1 || parsed.skillMapPath !== void 0 && (typeof parsed.skillMapPath !== "string" || !isAbsolute7(parsed.skillMapPath)) || parsed.skillMapCoverage !== void 0 && parsed.skillMapCoverage !== "static" && parsed.skillMapCoverage !== "resolved" || parsed.skillMapCoverage !== void 0 && parsed.skillMapPath === void 0 || Object.keys(parsed).some((key) => !["schemaVersion", "skillMapPath", "skillMapCoverage"].includes(key))) {
|
|
9621
10281
|
throw new UserError(`malformed insight command context at ${path}`);
|
|
9622
10282
|
}
|
|
9623
10283
|
return parsed;
|
|
@@ -9640,19 +10300,19 @@ function readCurrentStudy(studyId) {
|
|
|
9640
10300
|
return { study, inventory: currentInventory(study, config), config };
|
|
9641
10301
|
}
|
|
9642
10302
|
function retainedPath(root, relativePath) {
|
|
9643
|
-
const candidate =
|
|
9644
|
-
const rel =
|
|
9645
|
-
if (rel.startsWith("..") ||
|
|
10303
|
+
const candidate = resolvePath12(root, relativePath);
|
|
10304
|
+
const rel = relative6(root, candidate);
|
|
10305
|
+
if (rel.startsWith("..") || isAbsolute7(rel)) throw new UserError(`refusing retained source path outside the study`);
|
|
9646
10306
|
return candidate;
|
|
9647
10307
|
}
|
|
9648
10308
|
function retainObservedSources(study, observedFiles) {
|
|
9649
10309
|
if (observedFiles.length === 0) return study;
|
|
9650
|
-
const root =
|
|
10310
|
+
const root = join35(getStudyDir(study.studyId), "retained-sources");
|
|
9651
10311
|
mkdirSync9(root, { recursive: true, mode: 448 });
|
|
9652
10312
|
chmodSync3(root, 448);
|
|
9653
10313
|
const retainedSourceIds = /* @__PURE__ */ new Set();
|
|
9654
10314
|
for (const file of observedFiles) {
|
|
9655
|
-
const sourceRoot =
|
|
10315
|
+
const sourceRoot = join35(root, file.sourceId);
|
|
9656
10316
|
mkdirSync9(sourceRoot, { recursive: true, mode: 448 });
|
|
9657
10317
|
chmodSync3(sourceRoot, 448);
|
|
9658
10318
|
const destination = retainedPath(sourceRoot, file.relativePath);
|
|
@@ -9733,7 +10393,7 @@ async function insightAnalyze(sourceArgs = [], options = {}) {
|
|
|
9733
10393
|
requireProFeature2();
|
|
9734
10394
|
const config = loadConfig();
|
|
9735
10395
|
const inputs = collectSourceInputs(sourceArgs, options);
|
|
9736
|
-
const skillMapPath = options.skillMap ?
|
|
10396
|
+
const skillMapPath = options.skillMap ? resolvePath12(options.skillMap) : void 0;
|
|
9737
10397
|
if (options.skillMapCoverage !== void 0 && skillMapPath === void 0) {
|
|
9738
10398
|
throw new UserError("--skill-map-coverage requires --skill-map");
|
|
9739
10399
|
}
|
|
@@ -9890,7 +10550,7 @@ async function insightJira(studyId, options) {
|
|
|
9890
10550
|
throw new UserError(`study ${studyId} cannot generate a Jira manifest: ${validation.blockers.map((issue) => issue.message).join("; ")}`);
|
|
9891
10551
|
}
|
|
9892
10552
|
if (localMode) {
|
|
9893
|
-
const privatePath2 =
|
|
10553
|
+
const privatePath2 = join35(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
9894
10554
|
const manifest2 = assertCurrentJiraManifest(study, parseJsonFile(privatePath2, "stored Jira manifest"));
|
|
9895
10555
|
const receipt = options.receipt === void 0 ? void 0 : appendJiraReceipt(studyId, manifest2, parseJiraReceiptInput(parseJsonFile(options.receipt, "Jira receipt import")));
|
|
9896
10556
|
const resume = buildJiraResumeState(manifest2, readJiraReceipts(studyId, manifest2));
|
|
@@ -9902,10 +10562,10 @@ async function insightJira(studyId, options) {
|
|
|
9902
10562
|
return;
|
|
9903
10563
|
}
|
|
9904
10564
|
const manifest = buildJiraManifest(study, projectKey);
|
|
9905
|
-
const privatePath =
|
|
10565
|
+
const privatePath = join35(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
9906
10566
|
writeJsonAtomic(privatePath, manifest);
|
|
9907
10567
|
if (options.out) {
|
|
9908
|
-
writeNewFile(
|
|
10568
|
+
writeNewFile(resolvePath12(options.out), `${JSON.stringify(manifest, null, 2)}
|
|
9909
10569
|
`, "--out");
|
|
9910
10570
|
}
|
|
9911
10571
|
const generatedAt = manifest.generatedAt;
|
|
@@ -9923,7 +10583,7 @@ async function insightJira(studyId, options) {
|
|
|
9923
10583
|
emit(options.json, manifest, () => {
|
|
9924
10584
|
console.log(`[skill-forge] wrote Jira manifest for ${next.studyId}`);
|
|
9925
10585
|
console.log(` manifest: ${privatePath}`);
|
|
9926
|
-
if (options.out) console.log(` export: ${
|
|
10586
|
+
if (options.out) console.log(` export: ${resolvePath12(options.out)}`);
|
|
9927
10587
|
console.log(" no Jira issues were created.");
|
|
9928
10588
|
});
|
|
9929
10589
|
if (options.handoff) {
|
|
@@ -9974,13 +10634,13 @@ async function insightClose(studyId, options) {
|
|
|
9974
10634
|
|
|
9975
10635
|
// src/commands/refine.ts
|
|
9976
10636
|
import { createHash as createHash8, randomUUID as randomUUID5 } from "crypto";
|
|
9977
|
-
import { chmodSync as chmodSync4, existsSync as
|
|
9978
|
-
import { dirname as dirname15, isAbsolute as
|
|
10637
|
+
import { chmodSync as chmodSync4, existsSync as existsSync30, lstatSync as lstatSync7, mkdirSync as mkdirSync11, readFileSync as readFileSync26, realpathSync as realpathSync11, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
|
|
10638
|
+
import { dirname as dirname15, isAbsolute as isAbsolute9, join as join38, relative as relative8, resolve as resolvePath15, sep as sep8 } from "path";
|
|
9979
10639
|
import { createInterface as createInterface7 } from "readline/promises";
|
|
9980
10640
|
|
|
9981
10641
|
// src/refine/context.ts
|
|
9982
|
-
import { existsSync as
|
|
9983
|
-
import { isAbsolute as
|
|
10642
|
+
import { existsSync as existsSync29, realpathSync as realpathSync10, statSync as statSync13 } from "fs";
|
|
10643
|
+
import { isAbsolute as isAbsolute8, join as join37, relative as relative7, resolve as resolvePath14 } from "path";
|
|
9984
10644
|
|
|
9985
10645
|
// src/refine/models.ts
|
|
9986
10646
|
import { randomBytes } from "crypto";
|
|
@@ -10050,26 +10710,26 @@ function contentSimilarity(a, b) {
|
|
|
10050
10710
|
|
|
10051
10711
|
// src/refine/store.ts
|
|
10052
10712
|
import { createHash as createHash7 } from "crypto";
|
|
10053
|
-
import { existsSync as
|
|
10054
|
-
import { join as
|
|
10713
|
+
import { existsSync as existsSync28, readFileSync as readFileSync25, statSync as statSync12, realpathSync as realpathSync9 } from "fs";
|
|
10714
|
+
import { join as join36, resolve as resolvePath13 } from "path";
|
|
10055
10715
|
var SIMILARITY_JOIN_THRESHOLD = 0.7;
|
|
10056
10716
|
var READY_THRESHOLD = 2;
|
|
10057
10717
|
function getRefinementsDir() {
|
|
10058
|
-
return
|
|
10718
|
+
return join36(getHomeDir(), "refinements");
|
|
10059
10719
|
}
|
|
10060
10720
|
function getRefinementsPath() {
|
|
10061
|
-
return
|
|
10721
|
+
return join36(getRefinementsDir(), "refinements.json");
|
|
10062
10722
|
}
|
|
10063
10723
|
function getPatternsPath() {
|
|
10064
|
-
return
|
|
10724
|
+
return join36(getRefinementsDir(), "patterns.json");
|
|
10065
10725
|
}
|
|
10066
10726
|
function getBackupsDir() {
|
|
10067
|
-
return
|
|
10727
|
+
return join36(getRefinementsDir(), "backups");
|
|
10068
10728
|
}
|
|
10069
10729
|
function readJsonFile(path, label2) {
|
|
10070
10730
|
let raw;
|
|
10071
10731
|
try {
|
|
10072
|
-
raw =
|
|
10732
|
+
raw = readFileSync25(path, "utf-8");
|
|
10073
10733
|
} catch (err) {
|
|
10074
10734
|
throw new UserError(`unable to read ${label2} at ${path}: ${err.message}`);
|
|
10075
10735
|
}
|
|
@@ -10080,7 +10740,7 @@ function readJsonFile(path, label2) {
|
|
|
10080
10740
|
}
|
|
10081
10741
|
}
|
|
10082
10742
|
function readStore(path, label2, validateRecord) {
|
|
10083
|
-
if (!
|
|
10743
|
+
if (!existsSync28(path)) {
|
|
10084
10744
|
return { schemaVersion: 1, records: [] };
|
|
10085
10745
|
}
|
|
10086
10746
|
const parsed = readJsonFile(path, label2);
|
|
@@ -10170,9 +10830,9 @@ function writePatternsStore(store) {
|
|
|
10170
10830
|
writeJsonAtomic(getPatternsPath(), store);
|
|
10171
10831
|
}
|
|
10172
10832
|
function resolveProjectRealpath(projectRoot) {
|
|
10173
|
-
const resolved =
|
|
10833
|
+
const resolved = resolvePath13(projectRoot);
|
|
10174
10834
|
try {
|
|
10175
|
-
return
|
|
10835
|
+
return realpathSync9(resolved);
|
|
10176
10836
|
} catch {
|
|
10177
10837
|
return resolved;
|
|
10178
10838
|
}
|
|
@@ -10304,16 +10964,16 @@ function createBackup(filePaths, deps = {}) {
|
|
|
10304
10964
|
const randomHex = deps.randomHex ?? defaultRandomHex;
|
|
10305
10965
|
const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
|
|
10306
10966
|
const files = filePaths.map((path) => {
|
|
10307
|
-
if (!
|
|
10967
|
+
if (!existsSync28(path)) {
|
|
10308
10968
|
return { path, existed: false, sha256: null, mode: null, contentBase64: null };
|
|
10309
10969
|
}
|
|
10310
|
-
const bytes =
|
|
10970
|
+
const bytes = readFileSync25(path);
|
|
10311
10971
|
const mode = statSync12(path).mode & 511;
|
|
10312
10972
|
const sha2562 = createHash7("sha256").update(bytes).digest("hex");
|
|
10313
10973
|
return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
|
|
10314
10974
|
});
|
|
10315
10975
|
const manifest = { id, createdAt: now.toISOString(), files };
|
|
10316
|
-
writeJsonAtomic(
|
|
10976
|
+
writeJsonAtomic(join36(getBackupsDir(), id, "manifest.json"), manifest);
|
|
10317
10977
|
return manifest;
|
|
10318
10978
|
}
|
|
10319
10979
|
function validateBackupManifestShape(value, path) {
|
|
@@ -10362,8 +11022,8 @@ function readBackupManifest(id) {
|
|
|
10362
11022
|
if (!isValidBackupId(id)) {
|
|
10363
11023
|
throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
10364
11024
|
}
|
|
10365
|
-
const manifestPath =
|
|
10366
|
-
if (!
|
|
11025
|
+
const manifestPath = join36(getBackupsDir(), id, "manifest.json");
|
|
11026
|
+
if (!existsSync28(manifestPath)) {
|
|
10367
11027
|
throw new UserError(`no backup with id: ${id}`);
|
|
10368
11028
|
}
|
|
10369
11029
|
const parsed = readJsonFile(manifestPath, "backup manifest");
|
|
@@ -10373,7 +11033,7 @@ function readBackupManifest(id) {
|
|
|
10373
11033
|
|
|
10374
11034
|
// src/refine/context.ts
|
|
10375
11035
|
function resolveConfiguredRoot2(root) {
|
|
10376
|
-
return
|
|
11036
|
+
return resolvePath14(expandHome(root));
|
|
10377
11037
|
}
|
|
10378
11038
|
function assertPlainSkillName(name) {
|
|
10379
11039
|
if (!name || name.trim().length === 0) {
|
|
@@ -10385,32 +11045,32 @@ function assertPlainSkillName(name) {
|
|
|
10385
11045
|
}
|
|
10386
11046
|
function realpathOrNull2(p) {
|
|
10387
11047
|
try {
|
|
10388
|
-
return
|
|
11048
|
+
return realpathSync10(p);
|
|
10389
11049
|
} catch {
|
|
10390
11050
|
return null;
|
|
10391
11051
|
}
|
|
10392
11052
|
}
|
|
10393
11053
|
function isUnderRoot2(candidate, root) {
|
|
10394
|
-
const rel =
|
|
10395
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
11054
|
+
const rel = relative7(root, candidate);
|
|
11055
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute8(rel);
|
|
10396
11056
|
}
|
|
10397
11057
|
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
10398
11058
|
assertPlainSkillName(skill);
|
|
10399
|
-
const resolvedCwd =
|
|
10400
|
-
const localDir =
|
|
10401
|
-
const sharedDir =
|
|
10402
|
-
const local = { scope: "local", dir: localDir, exists:
|
|
10403
|
-
const shared = { scope: "shared", dir: sharedDir, exists:
|
|
11059
|
+
const resolvedCwd = resolvePath14(cwd);
|
|
11060
|
+
const localDir = join37(resolvedCwd, ".claude", "skills", skill);
|
|
11061
|
+
const sharedDir = join37(resolvedCwd, "skills", skill);
|
|
11062
|
+
const local = { scope: "local", dir: localDir, exists: existsSync29(localDir) };
|
|
11063
|
+
const shared = { scope: "shared", dir: sharedDir, exists: existsSync29(sharedDir) };
|
|
10404
11064
|
const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
|
|
10405
11065
|
return { local, shared, user };
|
|
10406
11066
|
}
|
|
10407
11067
|
function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
10408
11068
|
if (userRootFlag) {
|
|
10409
|
-
const root =
|
|
10410
|
-
return { resolved: true, dir:
|
|
11069
|
+
const root = resolvePath14(userRootFlag);
|
|
11070
|
+
return { resolved: true, dir: join37(root, skill), root: userRootFlag, reason: "flag" };
|
|
10411
11071
|
}
|
|
10412
11072
|
const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
|
|
10413
|
-
const candidates = config.skillsRoots.map((root) => ({ root, dir:
|
|
11073
|
+
const candidates = config.skillsRoots.map((root) => ({ root, dir: join37(resolveConfiguredRoot2(root), skill) }));
|
|
10414
11074
|
for (const candidate of candidates) {
|
|
10415
11075
|
const skillReal = realpathOrNull2(candidate.dir);
|
|
10416
11076
|
if (skillReal) {
|
|
@@ -10445,12 +11105,12 @@ function inventoryScopeDir(scope, dir) {
|
|
|
10445
11105
|
scope,
|
|
10446
11106
|
dir,
|
|
10447
11107
|
dirExists: isDirectory(dir),
|
|
10448
|
-
skillMd: isFile2(
|
|
10449
|
-
patchMd: isFile2(
|
|
10450
|
-
extendMd: isFile2(
|
|
10451
|
-
configJson: isFile2(
|
|
10452
|
-
hooksDir: isDirectory(
|
|
10453
|
-
scriptsDir: isDirectory(
|
|
11108
|
+
skillMd: isFile2(join37(dir, "SKILL.md")),
|
|
11109
|
+
patchMd: isFile2(join37(dir, "SKILL.patch.md")),
|
|
11110
|
+
extendMd: isFile2(join37(dir, "SKILL.extend.md")),
|
|
11111
|
+
configJson: isFile2(join37(dir, "skill-config.json")),
|
|
11112
|
+
hooksDir: isDirectory(join37(dir, "hooks")),
|
|
11113
|
+
scriptsDir: isDirectory(join37(dir, "scripts"))
|
|
10454
11114
|
};
|
|
10455
11115
|
}
|
|
10456
11116
|
function inventoryAllScopes(resolution) {
|
|
@@ -10719,18 +11379,18 @@ async function confirmWrite(yes, json, prompt) {
|
|
|
10719
11379
|
}
|
|
10720
11380
|
}
|
|
10721
11381
|
function resolveConfiguredRoot3(root) {
|
|
10722
|
-
return
|
|
11382
|
+
return resolvePath15(expandHome(root));
|
|
10723
11383
|
}
|
|
10724
11384
|
function isUnderRoot3(candidate, root) {
|
|
10725
|
-
const rel =
|
|
10726
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
11385
|
+
const rel = relative8(root, candidate);
|
|
11386
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute9(rel);
|
|
10727
11387
|
}
|
|
10728
11388
|
function sanitizeDiffForConsole(diff) {
|
|
10729
11389
|
return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
|
|
10730
11390
|
}
|
|
10731
11391
|
function resolveContentOption(options) {
|
|
10732
11392
|
if (options.content !== void 0) return options.content;
|
|
10733
|
-
if (options.contentFile) return
|
|
11393
|
+
if (options.contentFile) return readFileSync26(resolvePath15(options.contentFile), "utf-8");
|
|
10734
11394
|
return void 0;
|
|
10735
11395
|
}
|
|
10736
11396
|
function validateCaptureOptions(options) {
|
|
@@ -10865,10 +11525,10 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
10865
11525
|
}
|
|
10866
11526
|
mkdirSync11(scopeDir, { recursive: true });
|
|
10867
11527
|
const real = resolveConfiguredRoot3(scopeDir);
|
|
10868
|
-
const lexical =
|
|
11528
|
+
const lexical = resolvePath15(scopeDir);
|
|
10869
11529
|
let realResolved;
|
|
10870
11530
|
try {
|
|
10871
|
-
realResolved =
|
|
11531
|
+
realResolved = realpathSync11(scopeDir);
|
|
10872
11532
|
} catch {
|
|
10873
11533
|
realResolved = real;
|
|
10874
11534
|
}
|
|
@@ -10882,10 +11542,10 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
10882
11542
|
}
|
|
10883
11543
|
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
10884
11544
|
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
10885
|
-
const destPath =
|
|
11545
|
+
const destPath = join38(scopeDir, relPath);
|
|
10886
11546
|
const destDir = dirname15(destPath);
|
|
10887
11547
|
mkdirSync11(destDir, { recursive: true });
|
|
10888
|
-
const realDestDir =
|
|
11548
|
+
const realDestDir = realpathSync11(destDir);
|
|
10889
11549
|
if (!isUnderRoot3(realDestDir, realScopeDir)) {
|
|
10890
11550
|
throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
|
|
10891
11551
|
}
|
|
@@ -10978,12 +11638,12 @@ Write ${preview.destPath}? [y/N] `);
|
|
|
10978
11638
|
}
|
|
10979
11639
|
function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
|
|
10980
11640
|
const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
|
|
10981
|
-
const probePath = probeRelPath ?
|
|
10982
|
-
const existingContent = probePath &&
|
|
11641
|
+
const probePath = probeRelPath ? join38(scopeDir, probeRelPath) : void 0;
|
|
11642
|
+
const existingContent = probePath && existsSync30(probePath) ? readFileSync26(probePath, "utf-8") : "";
|
|
10983
11643
|
const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
|
|
10984
11644
|
const relPath = rendered.relPath;
|
|
10985
|
-
const destPath =
|
|
10986
|
-
const before =
|
|
11645
|
+
const destPath = join38(scopeDir, relPath);
|
|
11646
|
+
const before = existsSync30(destPath) ? readFileSync26(destPath, "utf-8") : "";
|
|
10987
11647
|
const diff = previewDiff(before, rendered.content, destPath);
|
|
10988
11648
|
return { relPath, content: rendered.content, destPath, diff };
|
|
10989
11649
|
}
|
|
@@ -10992,7 +11652,7 @@ async function refineListCommand(options = {}) {
|
|
|
10992
11652
|
const { records } = readRefinementsStore();
|
|
10993
11653
|
let rows = records;
|
|
10994
11654
|
if (options.skill) rows = rows.filter((r) => r.skill === options.skill);
|
|
10995
|
-
if (options.project) rows = rows.filter((r) => r.project === options.project || r.projectRoot ===
|
|
11655
|
+
if (options.project) rows = rows.filter((r) => r.project === options.project || r.projectRoot === resolvePath15(options.project));
|
|
10996
11656
|
if (options.status) {
|
|
10997
11657
|
if (!isRefinementStatus(options.status)) {
|
|
10998
11658
|
throw new UserError("--status must be one of: pending, applied, generalized");
|
|
@@ -11029,7 +11689,7 @@ async function refinePatternsCommand(options = {}) {
|
|
|
11029
11689
|
}
|
|
11030
11690
|
if (options.skill) rows = rows.filter((p) => p.affectedSkills.includes(options.skill));
|
|
11031
11691
|
if (options.project) {
|
|
11032
|
-
const resolvedProject =
|
|
11692
|
+
const resolvedProject = resolvePath15(options.project);
|
|
11033
11693
|
rows = rows.filter((p) => p.occurrences.some((o) => o.projectRealpath === resolvedProject));
|
|
11034
11694
|
}
|
|
11035
11695
|
const filtered = Boolean(options.skill || options.project || options.status);
|
|
@@ -11080,16 +11740,16 @@ async function refineWhichCommand(skill, options = {}) {
|
|
|
11080
11740
|
}
|
|
11081
11741
|
function writePostWriteSidecar(backupId, files) {
|
|
11082
11742
|
const sidecar = { schemaVersion: 1, files };
|
|
11083
|
-
const dir =
|
|
11743
|
+
const dir = join38(getBackupsDir(), backupId);
|
|
11084
11744
|
mkdirSync11(dir, { recursive: true });
|
|
11085
|
-
writeFileSync10(
|
|
11745
|
+
writeFileSync10(join38(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
|
|
11086
11746
|
}
|
|
11087
11747
|
function readPostWriteSidecar(backupId) {
|
|
11088
|
-
const path =
|
|
11089
|
-
if (!
|
|
11748
|
+
const path = join38(getBackupsDir(), backupId, "post.json");
|
|
11749
|
+
if (!existsSync30(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
|
|
11090
11750
|
let raw;
|
|
11091
11751
|
try {
|
|
11092
|
-
raw =
|
|
11752
|
+
raw = readFileSync26(path, "utf-8");
|
|
11093
11753
|
} catch {
|
|
11094
11754
|
return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
|
|
11095
11755
|
}
|
|
@@ -11134,21 +11794,21 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
11134
11794
|
const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
11135
11795
|
let realUserRoot;
|
|
11136
11796
|
try {
|
|
11137
|
-
realUserRoot =
|
|
11797
|
+
realUserRoot = realpathSync11(resolvedUserRoot);
|
|
11138
11798
|
} catch {
|
|
11139
11799
|
realUserRoot = resolvedUserRoot;
|
|
11140
11800
|
}
|
|
11141
11801
|
let realSkillDir;
|
|
11142
11802
|
try {
|
|
11143
|
-
realSkillDir =
|
|
11803
|
+
realSkillDir = realpathSync11(skillDir);
|
|
11144
11804
|
} catch {
|
|
11145
|
-
realSkillDir =
|
|
11805
|
+
realSkillDir = resolvePath15(skillDir);
|
|
11146
11806
|
}
|
|
11147
11807
|
if (!isUnderRoot3(realSkillDir, realUserRoot)) {
|
|
11148
11808
|
throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
|
|
11149
11809
|
}
|
|
11150
|
-
const skillMdPath =
|
|
11151
|
-
if (!
|
|
11810
|
+
const skillMdPath = join38(skillDir, "SKILL.md");
|
|
11811
|
+
if (!existsSync30(skillMdPath)) {
|
|
11152
11812
|
throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
|
|
11153
11813
|
}
|
|
11154
11814
|
const refinementsStore = readRefinementsStore();
|
|
@@ -11165,7 +11825,7 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
11165
11825
|
`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.`
|
|
11166
11826
|
);
|
|
11167
11827
|
}
|
|
11168
|
-
const before =
|
|
11828
|
+
const before = readFileSync26(skillMdPath, "utf-8");
|
|
11169
11829
|
const after = applyPatchBlocks(before, [block]);
|
|
11170
11830
|
const diff = previewDiff(before, after, skillMdPath);
|
|
11171
11831
|
if (options.dryRun) {
|
|
@@ -11197,7 +11857,7 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
11197
11857
|
throw err;
|
|
11198
11858
|
}
|
|
11199
11859
|
try {
|
|
11200
|
-
const postSha256 = createHash8("sha256").update(
|
|
11860
|
+
const postSha256 = createHash8("sha256").update(readFileSync26(skillMdPath)).digest("hex");
|
|
11201
11861
|
writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
|
|
11202
11862
|
markGeneralized(patternId, backup.id);
|
|
11203
11863
|
const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
@@ -11223,25 +11883,25 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
11223
11883
|
}
|
|
11224
11884
|
function assertBackupDirContained(backupId) {
|
|
11225
11885
|
const backupsDir = getBackupsDir();
|
|
11226
|
-
if (!
|
|
11227
|
-
const realBackupsDir =
|
|
11228
|
-
const backupDir =
|
|
11229
|
-
if (!
|
|
11230
|
-
const realBackupDir =
|
|
11886
|
+
if (!existsSync30(backupsDir)) return;
|
|
11887
|
+
const realBackupsDir = realpathSync11(backupsDir);
|
|
11888
|
+
const backupDir = join38(backupsDir, backupId);
|
|
11889
|
+
if (!existsSync30(backupDir)) return;
|
|
11890
|
+
const realBackupDir = realpathSync11(backupDir);
|
|
11231
11891
|
if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
|
|
11232
11892
|
throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
|
|
11233
11893
|
}
|
|
11234
11894
|
}
|
|
11235
11895
|
function resolveTargetRealPath(path) {
|
|
11236
|
-
let lexicalAncestor =
|
|
11896
|
+
let lexicalAncestor = resolvePath15(path);
|
|
11237
11897
|
while (true) {
|
|
11238
11898
|
try {
|
|
11239
|
-
const real =
|
|
11240
|
-
const remainder =
|
|
11241
|
-
return remainder === "" ? real :
|
|
11899
|
+
const real = realpathSync11(lexicalAncestor);
|
|
11900
|
+
const remainder = relative8(lexicalAncestor, resolvePath15(path));
|
|
11901
|
+
return remainder === "" ? real : join38(real, remainder);
|
|
11242
11902
|
} catch {
|
|
11243
11903
|
const parent = dirname15(lexicalAncestor);
|
|
11244
|
-
if (parent === lexicalAncestor) return
|
|
11904
|
+
if (parent === lexicalAncestor) return resolvePath15(path);
|
|
11245
11905
|
lexicalAncestor = parent;
|
|
11246
11906
|
}
|
|
11247
11907
|
}
|
|
@@ -11253,7 +11913,7 @@ function assertManifestPathsWithinRoots(manifest, config) {
|
|
|
11253
11913
|
const roots = config.skillsRoots.map((root) => {
|
|
11254
11914
|
const resolved = resolveConfiguredRoot3(root);
|
|
11255
11915
|
try {
|
|
11256
|
-
return
|
|
11916
|
+
return realpathSync11(resolved);
|
|
11257
11917
|
} catch {
|
|
11258
11918
|
return resolved;
|
|
11259
11919
|
}
|
|
@@ -11280,14 +11940,14 @@ function assertNoSymlinkLeaves(manifest) {
|
|
|
11280
11940
|
}
|
|
11281
11941
|
}
|
|
11282
11942
|
function findLexicalRoot(filePath, config) {
|
|
11283
|
-
const lexicalTarget =
|
|
11943
|
+
const lexicalTarget = resolvePath15(filePath);
|
|
11284
11944
|
for (const root of config.skillsRoots) {
|
|
11285
11945
|
const rootLexical = resolveConfiguredRoot3(root);
|
|
11286
|
-
const rel =
|
|
11287
|
-
if (rel !== "" && (rel === ".." || rel.startsWith(`..${
|
|
11946
|
+
const rel = relative8(rootLexical, lexicalTarget);
|
|
11947
|
+
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep8}`) || isAbsolute9(rel))) continue;
|
|
11288
11948
|
let rootReal;
|
|
11289
11949
|
try {
|
|
11290
|
-
rootReal =
|
|
11950
|
+
rootReal = realpathSync11(rootLexical);
|
|
11291
11951
|
} catch {
|
|
11292
11952
|
rootReal = rootLexical;
|
|
11293
11953
|
}
|
|
@@ -11297,8 +11957,8 @@ function findLexicalRoot(filePath, config) {
|
|
|
11297
11957
|
}
|
|
11298
11958
|
function walkRealComponents(filePath, config, createMissing) {
|
|
11299
11959
|
const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
|
|
11300
|
-
const relPath =
|
|
11301
|
-
const components = relPath.split(
|
|
11960
|
+
const relPath = relative8(rootLexical, resolvePath15(filePath));
|
|
11961
|
+
const components = relPath.split(sep8).filter((c) => c.length > 0);
|
|
11302
11962
|
if (components.length === 0) {
|
|
11303
11963
|
throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
|
|
11304
11964
|
}
|
|
@@ -11307,7 +11967,7 @@ function walkRealComponents(filePath, config, createMissing) {
|
|
|
11307
11967
|
let current = rootReal;
|
|
11308
11968
|
for (const component of dirComponents) {
|
|
11309
11969
|
if (current === void 0) break;
|
|
11310
|
-
const next =
|
|
11970
|
+
const next = join38(current, component);
|
|
11311
11971
|
let lst;
|
|
11312
11972
|
try {
|
|
11313
11973
|
lst = lstatSync7(next);
|
|
@@ -11338,7 +11998,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
11338
11998
|
if (parentReal === void 0) {
|
|
11339
11999
|
throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
|
|
11340
12000
|
}
|
|
11341
|
-
const finalPath =
|
|
12001
|
+
const finalPath = join38(parentReal, leafComponent);
|
|
11342
12002
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
11343
12003
|
throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
|
|
11344
12004
|
}
|
|
@@ -11364,7 +12024,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
11364
12024
|
function safeDeleteTombstone(filePath, config) {
|
|
11365
12025
|
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
|
|
11366
12026
|
if (parentReal === void 0) return;
|
|
11367
|
-
const finalPath =
|
|
12027
|
+
const finalPath = join38(parentReal, leafComponent);
|
|
11368
12028
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
11369
12029
|
throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
|
|
11370
12030
|
}
|
|
@@ -11406,7 +12066,7 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
11406
12066
|
for (const file of manifest.files) {
|
|
11407
12067
|
const expected = sidecar.hashes.get(file.path);
|
|
11408
12068
|
if (expected === void 0) continue;
|
|
11409
|
-
const currentSha256 =
|
|
12069
|
+
const currentSha256 = existsSync30(file.path) ? createHash8("sha256").update(readFileSync26(file.path)).digest("hex") : null;
|
|
11410
12070
|
if (currentSha256 !== expected) mismatches.push(file.path);
|
|
11411
12071
|
}
|
|
11412
12072
|
if (mismatches.length > 0 && !options.force) {
|
|
@@ -11497,6 +12157,13 @@ function parsePruneDays(value) {
|
|
|
11497
12157
|
}
|
|
11498
12158
|
return n;
|
|
11499
12159
|
}
|
|
12160
|
+
function parseUsageWindow(value) {
|
|
12161
|
+
const n = Number(value);
|
|
12162
|
+
if (value.trim() === "" || !Number.isInteger(n) || n < 1) {
|
|
12163
|
+
throw new UserError(`--usage-window must be a positive integer (got "${value}")`);
|
|
12164
|
+
}
|
|
12165
|
+
return n;
|
|
12166
|
+
}
|
|
11500
12167
|
function parseInsightAnalysisMode(value) {
|
|
11501
12168
|
if (value !== "independent" && value !== "single") {
|
|
11502
12169
|
throw new UserError(`--analysis-mode must be "independent" or "single" (got "${value}")`);
|
|
@@ -11544,7 +12211,7 @@ configCmd.command("propose <key> <value>").description("Queue a preference for r
|
|
|
11544
12211
|
configCmd.command("review").description("Review pending agent-proposed preferences one at a time").option("--accept-all", "accept every pending proposal without prompting").option("--reject-all", "reject every pending proposal without prompting").action(configReview);
|
|
11545
12212
|
program.command("guide [topic]").description("What this tool does, what state you are in, and the next command to run").action(guide);
|
|
11546
12213
|
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);
|
|
11547
|
-
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);
|
|
12214
|
+
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").option("--claude-plugins", "include enabled Claude Code plugins' declared skills in this audit (default: config.claudePlugins, else off)").option("--no-claude-plugins", "exclude enabled Claude Code plugins from this audit even when config.claudePlugins is on").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON file or a directory of them (default: config.usageSnapshotPath)").option("--usage-window <days>", "restrict directory-input usage-snapshot selection to a snapshot with this window_days (integer)", parseUsageWindow).action(auditCommand);
|
|
11548
12215
|
var finding = program.command("finding").description("Review and acknowledge audit findings \u2014 human-only, never a gate input (see docs/accepted-findings-schema.md)");
|
|
11549
12216
|
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);
|
|
11550
12217
|
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);
|
|
@@ -11555,7 +12222,7 @@ program.command("watch").description("Drift check across every SOURCES.md proven
|
|
|
11555
12222
|
"--skill-map <path>",
|
|
11556
12223
|
"also drift-check every fork-of edge in a generated rhize-plugins skill map (contentHash compare against upstream)"
|
|
11557
12224
|
).action(watchCommand);
|
|
11558
|
-
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);
|
|
12225
|
+
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)").option("--claude-plugins", "include enabled Claude Code plugins' declared skills in the audit step (default: config.claudePlugins, else off)").option("--no-claude-plugins", "exclude enabled Claude Code plugins from the audit step even when config.claudePlugins is on").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON file or a directory of them (default: config.usageSnapshotPath)").option("--usage-window <days>", "restrict directory-input usage-snapshot selection to a snapshot with this window_days (integer)", parseUsageWindow).action(routineCommand);
|
|
11559
12226
|
program.command("ingest").description(
|
|
11560
12227
|
"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."
|
|
11561
12228
|
).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
|