@rhize/skill-forge 0.16.0 → 0.17.1
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 +1089 -350
- 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,49 @@ 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 INSTALL_CUE_RE = /\b(install|run|paste|set ?up)\b/gi;
|
|
1197
|
+
var NEGATION_BEFORE_CUE_RE = /\b(never|not|don't|avoid|shouldn't|mustn't)(\s+\w+)?\s*$/i;
|
|
1198
|
+
function hasUnnegatedInstallCue(line) {
|
|
1199
|
+
for (const m of line.matchAll(INSTALL_CUE_RE)) {
|
|
1200
|
+
if (!NEGATION_BEFORE_CUE_RE.test(line.slice(0, m.index))) return true;
|
|
1201
|
+
}
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
function refineCurlPipeShell(line, ctx) {
|
|
1205
|
+
const base = { severity: "CRITICAL", message: "downloads a remote script and pipes it directly into a shell" };
|
|
1206
|
+
if (ctx.inFence) return base;
|
|
1207
|
+
if (!DOC_EXTENSIONS.has(extname2(ctx.file).toLowerCase())) return base;
|
|
1208
|
+
if (!DOC_CUE_RE.test(line)) return base;
|
|
1209
|
+
if (hasUnnegatedInstallCue(line)) return base;
|
|
1210
|
+
return { ...base, classification: "documentation-example" };
|
|
1211
|
+
}
|
|
1212
|
+
function computeFenceFlags(lines) {
|
|
1213
|
+
const flags = new Array(lines.length).fill(false);
|
|
1214
|
+
let closeRe = null;
|
|
1215
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1216
|
+
const line = lines[i].replace(/\r$/, "");
|
|
1217
|
+
if (closeRe === null) {
|
|
1218
|
+
const open = line.match(/^\s{0,3}(`{3,}|~{3,})/);
|
|
1219
|
+
if (open) {
|
|
1220
|
+
closeRe = new RegExp(`^\\s{0,3}${open[1][0]}{${open[1].length},}\\s*$`);
|
|
1221
|
+
flags[i] = true;
|
|
1222
|
+
}
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
flags[i] = true;
|
|
1226
|
+
if (closeRe.test(line)) closeRe = null;
|
|
1227
|
+
}
|
|
1228
|
+
return flags;
|
|
1229
|
+
}
|
|
1141
1230
|
var DENY_RULES = [
|
|
1142
1231
|
{
|
|
1143
1232
|
rule: "curl-pipe-shell",
|
|
1144
1233
|
severity: "CRITICAL",
|
|
1145
1234
|
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"
|
|
1235
|
+
message: "downloads a remote script and pipes it directly into a shell",
|
|
1236
|
+
refine: refineCurlPipeShell
|
|
1147
1237
|
},
|
|
1148
1238
|
{
|
|
1149
1239
|
rule: "base64-obfuscated-exec",
|
|
@@ -1214,17 +1304,19 @@ function scanStaticRules(targetPath) {
|
|
|
1214
1304
|
const content = readTextFile(file);
|
|
1215
1305
|
if (content === void 0) continue;
|
|
1216
1306
|
const lines = content.split("\n");
|
|
1307
|
+
const fenceFlags = DOC_EXTENSIONS.has(extname2(file).toLowerCase()) ? computeFenceFlags(lines) : void 0;
|
|
1217
1308
|
for (const rule of DENY_RULES) {
|
|
1218
1309
|
for (let i = 0; i < lines.length; i++) {
|
|
1219
1310
|
if (rule.pattern.test(lines[i])) {
|
|
1220
|
-
const refined = rule.refine ? rule.refine(lines[i]) : { severity: rule.severity, message: rule.message };
|
|
1311
|
+
const refined = rule.refine ? rule.refine(lines[i], { inFence: fenceFlags?.[i] ?? false, file }) : { severity: rule.severity, message: rule.message };
|
|
1221
1312
|
if (!refined) continue;
|
|
1222
1313
|
findings.push({
|
|
1223
1314
|
severity: refined.severity,
|
|
1224
1315
|
rule: rule.rule,
|
|
1225
1316
|
file,
|
|
1226
1317
|
detail: `line ${i + 1}: ${refined.message}`,
|
|
1227
|
-
context: lines[i].trim()
|
|
1318
|
+
context: lines[i].trim(),
|
|
1319
|
+
...refined.classification ? { classification: refined.classification } : {}
|
|
1228
1320
|
});
|
|
1229
1321
|
}
|
|
1230
1322
|
}
|
|
@@ -1279,7 +1371,7 @@ async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
|
|
|
1279
1371
|
|
|
1280
1372
|
// src/gate/mcpSafety.ts
|
|
1281
1373
|
import { statSync as statSync5 } from "fs";
|
|
1282
|
-
import { basename as basename4, extname as
|
|
1374
|
+
import { basename as basename4, extname as extname3 } from "path";
|
|
1283
1375
|
function isConfigOrExampleFile(file) {
|
|
1284
1376
|
const base = basename4(file).toLowerCase();
|
|
1285
1377
|
if (base.startsWith(".env")) return true;
|
|
@@ -1485,7 +1577,7 @@ function scanTextFile(file, content, allowCredentialCheck) {
|
|
|
1485
1577
|
function scanFileForMcpFindings(file) {
|
|
1486
1578
|
const content = readTextFile(file);
|
|
1487
1579
|
if (content === void 0) return [];
|
|
1488
|
-
if (
|
|
1580
|
+
if (extname3(file).toLowerCase() === ".json") {
|
|
1489
1581
|
const parsed = tryParseJson(content);
|
|
1490
1582
|
if (parsed !== void 0) return scanJsonConfig(file, parsed);
|
|
1491
1583
|
}
|
|
@@ -1523,14 +1615,14 @@ skill skills user claude code based comprehensive enhanced practical including i
|
|
|
1523
1615
|
`.split(/\s+/).filter(Boolean)
|
|
1524
1616
|
);
|
|
1525
1617
|
var DEFAULT_SET_THRESHOLD = 0.45;
|
|
1526
|
-
function splitLimited(text,
|
|
1618
|
+
function splitLimited(text, sep9, maxSplit) {
|
|
1527
1619
|
const parts = [];
|
|
1528
1620
|
let rest = text;
|
|
1529
1621
|
for (let i = 0; i < maxSplit; i++) {
|
|
1530
|
-
const idx = rest.indexOf(
|
|
1622
|
+
const idx = rest.indexOf(sep9);
|
|
1531
1623
|
if (idx === -1) break;
|
|
1532
1624
|
parts.push(rest.slice(0, idx));
|
|
1533
|
-
rest = rest.slice(idx +
|
|
1625
|
+
rest = rest.slice(idx + sep9.length);
|
|
1534
1626
|
}
|
|
1535
1627
|
parts.push(rest);
|
|
1536
1628
|
return parts;
|
|
@@ -1882,7 +1974,14 @@ function proFeatureStatus(feature, betaFree = BETA_FREE) {
|
|
|
1882
1974
|
// src/audit.ts
|
|
1883
1975
|
var OVERSIZED_SKILL_EST_TOKENS = 5e3;
|
|
1884
1976
|
function toAuditFindings(source) {
|
|
1885
|
-
return source.map((f) => ({
|
|
1977
|
+
return source.map((f) => ({
|
|
1978
|
+
severity: f.severity,
|
|
1979
|
+
rule: f.rule,
|
|
1980
|
+
target: f.file,
|
|
1981
|
+
detail: f.detail,
|
|
1982
|
+
context: f.context,
|
|
1983
|
+
...f.classification ? { classification: f.classification } : {}
|
|
1984
|
+
}));
|
|
1886
1985
|
}
|
|
1887
1986
|
function activeVerdictFor(findings, strictness) {
|
|
1888
1987
|
return verdictFor(
|
|
@@ -2034,13 +2133,14 @@ function parseStrictFrontmatter(text) {
|
|
|
2034
2133
|
function estimateTokens(text) {
|
|
2035
2134
|
return Math.ceil(Buffer.byteLength(text, "utf-8") / 4);
|
|
2036
2135
|
}
|
|
2037
|
-
function profileAuditedSkill(group, findings, unreadableSkillMdPaths) {
|
|
2136
|
+
function profileAuditedSkill(group, findings, unreadableSkillMdPaths, pluginId) {
|
|
2038
2137
|
const skillMdPath = join12(group.canonicalPath, "SKILL.md");
|
|
2039
2138
|
const base = {
|
|
2040
2139
|
path: group.canonicalPath,
|
|
2041
2140
|
skillMdPath,
|
|
2042
2141
|
root: group.root,
|
|
2043
|
-
aliasPaths: group.aliasPaths
|
|
2142
|
+
aliasPaths: group.aliasPaths,
|
|
2143
|
+
pluginId
|
|
2044
2144
|
};
|
|
2045
2145
|
let text;
|
|
2046
2146
|
let sizeBytes;
|
|
@@ -2101,7 +2201,7 @@ function enumerateMcpServers(parsed) {
|
|
|
2101
2201
|
}
|
|
2102
2202
|
function auditMcpTarget(targetPath, notices) {
|
|
2103
2203
|
const findings = [];
|
|
2104
|
-
const isToml =
|
|
2204
|
+
const isToml = extname4(targetPath).toLowerCase() === ".toml";
|
|
2105
2205
|
const format = isToml ? "toml" : "json";
|
|
2106
2206
|
if (!existsSync9(targetPath)) {
|
|
2107
2207
|
findings.push({
|
|
@@ -2209,6 +2309,134 @@ function toClusterMembers(members) {
|
|
|
2209
2309
|
}
|
|
2210
2310
|
return sorted;
|
|
2211
2311
|
}
|
|
2312
|
+
function annotateSiblingClusters(clusters, nameByPath) {
|
|
2313
|
+
return clusters.map((cluster) => {
|
|
2314
|
+
const names = cluster.members.map((p) => nameByPath.get(p) ?? basename6(p));
|
|
2315
|
+
const allSiblings = names.every((a, i) => names.slice(i + 1).every((b) => isStackSiblingPair(a, b)));
|
|
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 comparePaths(a.path, 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 comparePaths(a, b) {
|
|
2366
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
2367
|
+
}
|
|
2368
|
+
function buildUsageJoinIndex(usage, skills) {
|
|
2369
|
+
const bareKeys = /* @__PURE__ */ new Map();
|
|
2370
|
+
for (const key of usage.keys()) {
|
|
2371
|
+
const bare = key.includes(":") ? key.slice(key.lastIndexOf(":") + 1) : key;
|
|
2372
|
+
bareKeys.set(bare, [...bareKeys.get(bare) ?? [], key]);
|
|
2373
|
+
}
|
|
2374
|
+
const inventoryNameCounts = /* @__PURE__ */ new Map();
|
|
2375
|
+
for (const skill of skills) inventoryNameCounts.set(skill.name, (inventoryNameCounts.get(skill.name) ?? 0) + 1);
|
|
2376
|
+
return { bareKeys, inventoryNameCounts, noticed: /* @__PURE__ */ new Set() };
|
|
2377
|
+
}
|
|
2378
|
+
function matchUsage(skill, usage, index, notices) {
|
|
2379
|
+
if (usage.has(skill.name)) return usage.get(skill.name);
|
|
2380
|
+
if (skill.pluginId) {
|
|
2381
|
+
const composite = `${skill.pluginId.split("@")[0]}:${skill.name}`;
|
|
2382
|
+
if (usage.has(composite)) return usage.get(composite);
|
|
2383
|
+
}
|
|
2384
|
+
const bareMatches = index.bareKeys.get(skill.name) ?? [];
|
|
2385
|
+
if (bareMatches.length === 0) return void 0;
|
|
2386
|
+
const installedWithName = index.inventoryNameCounts.get(skill.name) ?? 0;
|
|
2387
|
+
if (bareMatches.length === 1 && installedWithName === 1) return usage.get(bareMatches[0]);
|
|
2388
|
+
if (!index.noticed.has(skill.name)) {
|
|
2389
|
+
index.noticed.add(skill.name);
|
|
2390
|
+
notices.push(
|
|
2391
|
+
bareMatches.length > 1 ? `usage snapshot: ambiguous bare name "${skill.name}" matches ${bareMatches.length} entries \u2014 skipping attribution` : `usage snapshot: bare name "${skill.name}" matches ${installedWithName} installed skills \u2014 skipping attribution`
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
return void 0;
|
|
2395
|
+
}
|
|
2396
|
+
function isUnderInstallPath(target, installPath) {
|
|
2397
|
+
return target === installPath || target.startsWith(installPath + sep2);
|
|
2398
|
+
}
|
|
2399
|
+
function computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined) {
|
|
2400
|
+
return pluginRoots.map((pr) => {
|
|
2401
|
+
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2402
|
+
for (const f of activeFindings) {
|
|
2403
|
+
if (isUnderInstallPath(f.target, pr.installPath)) findingCounts[f.severity]++;
|
|
2404
|
+
}
|
|
2405
|
+
const skillCount = pr.skillDirs.length;
|
|
2406
|
+
const hasUsage = usage !== void 0;
|
|
2407
|
+
const exhaustive = usage?.exhaustive ?? false;
|
|
2408
|
+
const windowDays = usage?.windowDays;
|
|
2409
|
+
const observedSkillCount = hasUsage ? skills.filter((s) => s.pluginId === pr.pluginId && (s.usage ?? 0) > 0).length : void 0;
|
|
2410
|
+
let recommendation;
|
|
2411
|
+
const reasons = [];
|
|
2412
|
+
const longWindow = windowDays !== void 0 && windowDays >= 28;
|
|
2413
|
+
if (findingCounts.HIGH > 0 || findingCounts.CRITICAL > 0) {
|
|
2414
|
+
recommendation = "review";
|
|
2415
|
+
} else if (exhaustive && longWindow && skillCount > 0 && observedSkillCount === 0 && (usageJoined ?? 0) > 0) {
|
|
2416
|
+
recommendation = "unobserved";
|
|
2417
|
+
} else if (exhaustive && (observedSkillCount ?? 0) >= 1) {
|
|
2418
|
+
recommendation = "keep";
|
|
2419
|
+
} else {
|
|
2420
|
+
recommendation = "unknown";
|
|
2421
|
+
if (skillCount === 0) reasons.push("no skills to observe");
|
|
2422
|
+
if (!hasUsage) reasons.push("no usage telemetry");
|
|
2423
|
+
else if (usage.usage.size === 0) reasons.push("usage snapshot is empty or unreadable (see notices)");
|
|
2424
|
+
else if (!exhaustive) reasons.push("usage snapshot is not exhaustive (top-50 only)");
|
|
2425
|
+
else if (!longWindow) reasons.push(`window ${windowDays ?? "unknown"} < 28 days`);
|
|
2426
|
+
else if ((usageJoined ?? 0) === 0) reasons.push("usage snapshot joined 0 installed skills");
|
|
2427
|
+
}
|
|
2428
|
+
return {
|
|
2429
|
+
pluginId: pr.pluginId,
|
|
2430
|
+
version: pr.version,
|
|
2431
|
+
installPath: pr.installPath,
|
|
2432
|
+
skillCount,
|
|
2433
|
+
findingCounts,
|
|
2434
|
+
observedSkillCount,
|
|
2435
|
+
recommendation,
|
|
2436
|
+
reasons: reasons.sort()
|
|
2437
|
+
};
|
|
2438
|
+
}).sort((a, b) => comparePaths(a.pluginId, b.pluginId));
|
|
2439
|
+
}
|
|
2212
2440
|
function defaultWhich(cmd) {
|
|
2213
2441
|
const pathEnv = process.env.PATH ?? "";
|
|
2214
2442
|
const dirs = pathEnv.split(delimiter3).filter(Boolean);
|
|
@@ -2253,10 +2481,16 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2253
2481
|
const oversizedThreshold = options.oversizedSkillTokens ?? OVERSIZED_SKILL_EST_TOKENS;
|
|
2254
2482
|
const findings = [];
|
|
2255
2483
|
const notices = [];
|
|
2484
|
+
const pluginRoots = options.pluginRoots ?? [];
|
|
2256
2485
|
const { candidates, rootMeta } = collectSkillCandidates(config.skillsRoots);
|
|
2257
2486
|
const groups = dedupeSkillCandidates(candidates, findings);
|
|
2258
2487
|
const unreadableSkillMdPaths = /* @__PURE__ */ new Set();
|
|
2259
|
-
const
|
|
2488
|
+
const configuredSkills = groups.map((g) => profileAuditedSkill(g, findings, unreadableSkillMdPaths));
|
|
2489
|
+
const pluginSkills = profilePluginSkills(pluginRoots, findings, unreadableSkillMdPaths);
|
|
2490
|
+
let skills = [...configuredSkills, ...pluginSkills];
|
|
2491
|
+
attachContentHashes(skills);
|
|
2492
|
+
const collapsed = collapseIdenticalContent(skills, config.skillsRoots);
|
|
2493
|
+
skills = collapsed.skills;
|
|
2260
2494
|
const skillRoots = config.skillsRoots.map((root) => {
|
|
2261
2495
|
const meta = rootMeta.get(root) ?? { exists: false };
|
|
2262
2496
|
if (!meta.exists) {
|
|
@@ -2273,9 +2507,20 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2273
2507
|
root,
|
|
2274
2508
|
realPath: meta.realPath ?? resolveConfiguredRoot(root),
|
|
2275
2509
|
exists: meta.exists,
|
|
2276
|
-
skillCount: skills.filter((s) => s.root === root).length
|
|
2510
|
+
skillCount: skills.filter((s) => s.root === root).length,
|
|
2511
|
+
origin: "configured"
|
|
2277
2512
|
};
|
|
2278
2513
|
});
|
|
2514
|
+
for (const pr of pluginRoots) {
|
|
2515
|
+
skillRoots.push({
|
|
2516
|
+
root: pr.installPath,
|
|
2517
|
+
realPath: pr.installPath,
|
|
2518
|
+
exists: isDir(pr.installPath),
|
|
2519
|
+
skillCount: pr.skillDirs.length,
|
|
2520
|
+
origin: "plugin",
|
|
2521
|
+
pluginId: pr.pluginId
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2279
2524
|
const byName = /* @__PURE__ */ new Map();
|
|
2280
2525
|
for (const skill of skills) {
|
|
2281
2526
|
const arr = byName.get(skill.name) ?? [];
|
|
@@ -2284,13 +2529,15 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2284
2529
|
}
|
|
2285
2530
|
for (const [name, group] of byName) {
|
|
2286
2531
|
if (group.length > 1) {
|
|
2532
|
+
const isIdenticalPluginPair = group.length === 2 && group.every((s) => s.pluginId !== void 0) && group[0].contentHash !== void 0 && group[0].contentHash === group[1].contentHash;
|
|
2533
|
+
const severity = isIdenticalPluginPair ? "LOW" : "MEDIUM";
|
|
2287
2534
|
for (const skill of group) {
|
|
2288
2535
|
const otherPaths = group.filter((g) => g.path !== skill.path).map((g) => g.path);
|
|
2289
2536
|
findings.push({
|
|
2290
|
-
severity
|
|
2537
|
+
severity,
|
|
2291
2538
|
rule: "duplicate-skill-name",
|
|
2292
2539
|
target: skill.path,
|
|
2293
|
-
detail: `name "${name}" is duplicated \u2014 also found at ${otherPaths.join(", ")}`
|
|
2540
|
+
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
2541
|
});
|
|
2295
2542
|
}
|
|
2296
2543
|
}
|
|
@@ -2340,9 +2587,10 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2340
2587
|
if (overlapStatus.enabled) {
|
|
2341
2588
|
if (overlapStatus.notice) notices.push(overlapStatus.notice);
|
|
2342
2589
|
try {
|
|
2343
|
-
const readableSkillMdPaths = skills.map((s) => s.skillMdPath).filter((p) => !unreadableSkillMdPaths.has(p));
|
|
2590
|
+
const readableSkillMdPaths = skills.filter((s) => s.pluginId === void 0).map((s) => s.skillMdPath).filter((p) => !unreadableSkillMdPaths.has(p));
|
|
2344
2591
|
const pairs = scanOverlapPaths(readableSkillMdPaths, options.overlapThreshold);
|
|
2345
|
-
|
|
2592
|
+
const nameByPath = new Map(skills.map((s) => [s.skillMdPath, s.name]));
|
|
2593
|
+
overlapClusters = annotateSiblingClusters(clusterOverlapPairs(pairs), nameByPath);
|
|
2346
2594
|
} catch (err) {
|
|
2347
2595
|
findings.push({
|
|
2348
2596
|
severity: "MEDIUM",
|
|
@@ -2381,7 +2629,7 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2381
2629
|
}
|
|
2382
2630
|
const activeFindingSet = new Set(activeFindings);
|
|
2383
2631
|
const evolveEligible = skills.filter((s) => {
|
|
2384
|
-
if (!s.validFrontmatter || skillScanFailed.has(s.path)) return false;
|
|
2632
|
+
if (!s.validFrontmatter || skillScanFailed.has(s.path) || s.pluginId !== void 0) return false;
|
|
2385
2633
|
const skillFindings = perSkillFindings.get(s.path) ?? [];
|
|
2386
2634
|
const activeSkillFindings = skillFindings.filter((f) => activeFindingSet.has(f));
|
|
2387
2635
|
return activeVerdictFor(activeSkillFindings, config.strictness) === "pass";
|
|
@@ -2394,6 +2642,19 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2394
2642
|
available: true,
|
|
2395
2643
|
message: "no business-foundation skill yet \u2014 `skill-forge audit --foundation` can scaffold one to ground future skills in your business profile"
|
|
2396
2644
|
};
|
|
2645
|
+
const usage = options.usage;
|
|
2646
|
+
if (usage) {
|
|
2647
|
+
const index = buildUsageJoinIndex(usage.usage, skills);
|
|
2648
|
+
for (const skill of skills) {
|
|
2649
|
+
const matched = matchUsage(skill, usage.usage, index, notices);
|
|
2650
|
+
if (matched !== void 0) skill.usage = matched;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
const usageJoined = usage ? skills.filter((s) => s.usage !== void 0).length : void 0;
|
|
2654
|
+
if (usage && usageJoined === 0) {
|
|
2655
|
+
notices.push(`usage snapshot (${usage.sourcePath}) joined 0 of ${skills.length} skill(s)`);
|
|
2656
|
+
}
|
|
2657
|
+
const unobservedInWindow = usage && usage.exhaustive && usage.windowDays !== void 0 && usage.windowDays >= 28 && (usageJoined ?? 0) > 0 ? skills.filter((s) => (s.usage ?? 0) === 0).map((s) => ({ name: s.name, path: s.path, pluginId: s.pluginId })) : [];
|
|
2397
2658
|
const findingCounts = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 };
|
|
2398
2659
|
for (const f of activeFindings) findingCounts[f.severity]++;
|
|
2399
2660
|
const summary = {
|
|
@@ -2403,8 +2664,12 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2403
2664
|
findingCounts,
|
|
2404
2665
|
overlapClusterCount: overlapClusters.length,
|
|
2405
2666
|
evolveEligibleCount: evolveEligible.length,
|
|
2406
|
-
acceptedCount: acceptedEntries.length
|
|
2667
|
+
acceptedCount: acceptedEntries.length,
|
|
2668
|
+
collapsedDuplicateCount: collapsed.collapsedCount,
|
|
2669
|
+
documentationExampleCount: activeFindings.filter((f) => f.classification === "documentation-example").length,
|
|
2670
|
+
...usage ? { usageJoined, usageExhaustive: usage.exhaustive, usageWindowDays: usage.windowDays } : {}
|
|
2407
2671
|
};
|
|
2672
|
+
const plugins = options.pluginRoots ? computePluginSummaries(pluginRoots, activeFindings, skills, usage, usageJoined) : void 0;
|
|
2408
2673
|
const tooling = checkTooling(options.which ?? defaultWhich, options.env ?? process.env);
|
|
2409
2674
|
return {
|
|
2410
2675
|
schemaVersion: 1,
|
|
@@ -2412,10 +2677,11 @@ async function auditConfiguredSet(config, options = {}) {
|
|
|
2412
2677
|
inventory: { skillRoots, skills, mcpTargets },
|
|
2413
2678
|
hygiene: activeFindings,
|
|
2414
2679
|
accepted: acceptedEntries,
|
|
2415
|
-
opportunities: { overlapClusters, evolveEligible, foundation, overlapLocked: !overlapStatus.enabled },
|
|
2680
|
+
opportunities: { overlapClusters, evolveEligible, foundation, overlapLocked: !overlapStatus.enabled, unobservedInWindow },
|
|
2416
2681
|
tooling,
|
|
2417
2682
|
notices,
|
|
2418
|
-
summary
|
|
2683
|
+
summary,
|
|
2684
|
+
plugins
|
|
2419
2685
|
};
|
|
2420
2686
|
}
|
|
2421
2687
|
function bulletList(items, emptyPlaceholder) {
|
|
@@ -2432,7 +2698,9 @@ function renderRootsSection(roots) {
|
|
|
2432
2698
|
"### Skill roots",
|
|
2433
2699
|
"",
|
|
2434
2700
|
...bulletList(
|
|
2435
|
-
roots.map(
|
|
2701
|
+
roots.map(
|
|
2702
|
+
(r) => `${r.root}${r.exists ? "" : " (MISSING)"} \u2014 ${r.skillCount} skill${r.skillCount === 1 ? "" : "s"}` + (r.origin === "plugin" ? ` (plugin ${sanitizeForMarkdown(r.pluginId ?? "")})` : "")
|
|
2703
|
+
),
|
|
2436
2704
|
"(none configured)"
|
|
2437
2705
|
)
|
|
2438
2706
|
];
|
|
@@ -2476,17 +2744,17 @@ function renderMcpTargetsSection(targets) {
|
|
|
2476
2744
|
function displayFingerprint(f) {
|
|
2477
2745
|
return f.fingerprint ? f.fingerprint.slice(0, FINGERPRINT_DISPLAY_LENGTH) : "unknown";
|
|
2478
2746
|
}
|
|
2747
|
+
function renderFindingLine(f) {
|
|
2748
|
+
return `[${f.severity}] ${f.rule} \u2014 ${sanitizeForMarkdown(f.target)}: ${sanitizeForMarkdown(f.detail)} (fp ${displayFingerprint(f)})`;
|
|
2749
|
+
}
|
|
2479
2750
|
function renderFindingsSection(findings) {
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
"(none \u2014 clean)"
|
|
2488
|
-
)
|
|
2489
|
-
];
|
|
2751
|
+
const live = findings.filter((f) => f.classification !== "documentation-example");
|
|
2752
|
+
const docExamples = findings.filter((f) => f.classification === "documentation-example");
|
|
2753
|
+
const lines = ["## Hygiene findings", "", ...bulletList(live.map(renderFindingLine), "(none \u2014 clean)")];
|
|
2754
|
+
if (docExamples.length > 0) {
|
|
2755
|
+
lines.push("", "### Heuristically classified as documentation examples (still CRITICAL \u2014 review each; the heuristic can be gamed)", "", ...bulletList(docExamples.map(renderFindingLine), ""));
|
|
2756
|
+
}
|
|
2757
|
+
return lines;
|
|
2490
2758
|
}
|
|
2491
2759
|
function renderAcceptedFindingsSection(entries) {
|
|
2492
2760
|
return [
|
|
@@ -2508,7 +2776,9 @@ function renderOpportunitiesSection(report) {
|
|
|
2508
2776
|
"### Overlap clusters",
|
|
2509
2777
|
"",
|
|
2510
2778
|
...bulletList(
|
|
2511
|
-
overlapClusters.map(
|
|
2779
|
+
overlapClusters.map(
|
|
2780
|
+
(c) => `top score ${c.topScore} \u2014 ${c.suggestedVerb} \u2014 ${c.members.join(", ")}` + (c.siblingVariants ? " (stack variants \u2014 deliberate siblings, not duplicates)" : "")
|
|
2781
|
+
),
|
|
2512
2782
|
"(none \u2014 either no cross-root overlap found, or the Pro feature is locked, see Notices)"
|
|
2513
2783
|
),
|
|
2514
2784
|
"",
|
|
@@ -2524,6 +2794,43 @@ function renderOpportunitiesSection(report) {
|
|
|
2524
2794
|
`- ${foundation.message}`
|
|
2525
2795
|
];
|
|
2526
2796
|
}
|
|
2797
|
+
function renderUsageSection(report) {
|
|
2798
|
+
const { summary } = report;
|
|
2799
|
+
const lines = ["## Usage", ""];
|
|
2800
|
+
lines.push(`- Matched: ${summary.usageJoined ?? 0} of ${summary.skillCount} skill(s)`);
|
|
2801
|
+
lines.push(`- Window: ${summary.usageWindowDays !== void 0 ? `${summary.usageWindowDays} day(s)` : "unknown"}`);
|
|
2802
|
+
if ((summary.usageJoined ?? 0) === 0) {
|
|
2803
|
+
lines.push("- Coverage: nothing joined (see Notices)");
|
|
2804
|
+
} else if (summary.usageExhaustive) {
|
|
2805
|
+
lines.push("- Coverage: exhaustive (every skill counted this window)");
|
|
2806
|
+
} else {
|
|
2807
|
+
lines.push("- Coverage: top-50 only; absence is not zero usage");
|
|
2808
|
+
}
|
|
2809
|
+
if (report.opportunities.unobservedInWindow.length > 0) {
|
|
2810
|
+
lines.push("", "### Unobserved this window", "");
|
|
2811
|
+
for (const s of report.opportunities.unobservedInWindow) {
|
|
2812
|
+
lines.push(`- ${sanitizeForMarkdown(s.name)} \u2014 ${s.path}${s.pluginId ? ` (plugin ${sanitizeForMarkdown(s.pluginId)})` : ""}`);
|
|
2813
|
+
}
|
|
2814
|
+
} else if (summary.usageExhaustive && (summary.usageJoined ?? 0) > 0 && (summary.usageWindowDays ?? 0) < 28) {
|
|
2815
|
+
lines.push("- Unobserved list: suppressed \u2014 window shorter than 28 days proves nothing about monthly-use skills");
|
|
2816
|
+
}
|
|
2817
|
+
return lines;
|
|
2818
|
+
}
|
|
2819
|
+
function renderPluginsSection(plugins) {
|
|
2820
|
+
return [
|
|
2821
|
+
"## Plugins",
|
|
2822
|
+
"",
|
|
2823
|
+
...bulletList(
|
|
2824
|
+
plugins.map((p) => {
|
|
2825
|
+
const findingsPart = `${p.findingCounts.CRITICAL} CRITICAL, ${p.findingCounts.HIGH} HIGH, ${p.findingCounts.MEDIUM} MEDIUM, ${p.findingCounts.LOW} LOW`;
|
|
2826
|
+
const observedPart = p.observedSkillCount !== void 0 ? `, observed ${p.observedSkillCount}` : "";
|
|
2827
|
+
const reasonsPart = p.reasons.length > 0 ? ` (${p.reasons.join("; ")})` : "";
|
|
2828
|
+
return `${sanitizeForMarkdown(p.pluginId)} @ ${sanitizeForMarkdown(p.version)} \u2014 ${sanitizeForMarkdown(p.installPath)} \u2014 ${p.skillCount} skill(s)${observedPart} \u2014 findings: ${findingsPart} \u2014 **${p.recommendation}**${reasonsPart}`;
|
|
2829
|
+
}),
|
|
2830
|
+
"(no enabled plugins)"
|
|
2831
|
+
)
|
|
2832
|
+
];
|
|
2833
|
+
}
|
|
2527
2834
|
function renderToolingSection(tooling) {
|
|
2528
2835
|
const lines = ["## Tooling", ""];
|
|
2529
2836
|
if (!tooling) {
|
|
@@ -2560,6 +2867,8 @@ function renderAuditMarkdown(report) {
|
|
|
2560
2867
|
`- Accepted: ${summary.acceptedCount} (suppressed from the counts above; see the Accepted findings section)`,
|
|
2561
2868
|
`- Overlap clusters: ${summary.overlapClusterCount}`,
|
|
2562
2869
|
`- Evolve-eligible skills: ${summary.evolveEligibleCount}`,
|
|
2870
|
+
`- Collapsed duplicates: ${summary.collapsedDuplicateCount}`,
|
|
2871
|
+
`- Documentation-example classified (heuristic; counted above as CRITICAL): ${summary.documentationExampleCount}`,
|
|
2563
2872
|
"",
|
|
2564
2873
|
"## Inventory",
|
|
2565
2874
|
"",
|
|
@@ -2575,12 +2884,11 @@ function renderAuditMarkdown(report) {
|
|
|
2575
2884
|
"",
|
|
2576
2885
|
...renderOpportunitiesSection(report),
|
|
2577
2886
|
"",
|
|
2578
|
-
...renderToolingSection(report.tooling)
|
|
2579
|
-
"",
|
|
2580
|
-
"## Notices",
|
|
2581
|
-
"",
|
|
2582
|
-
...bulletList(report.notices, "(none)")
|
|
2887
|
+
...renderToolingSection(report.tooling)
|
|
2583
2888
|
];
|
|
2889
|
+
if (summary.usageJoined !== void 0) lines.push("", ...renderUsageSection(report));
|
|
2890
|
+
if (report.plugins) lines.push("", ...renderPluginsSection(report.plugins));
|
|
2891
|
+
lines.push("", "## Notices", "", ...bulletList(report.notices, "(none)"));
|
|
2584
2892
|
return lines.join("\n") + "\n";
|
|
2585
2893
|
}
|
|
2586
2894
|
function renderFoundationSkillMd(profile) {
|
|
@@ -2691,7 +2999,9 @@ function summarizeAudit(report, reportPath) {
|
|
|
2691
2999
|
for (const skill of report.inventory.skills) {
|
|
2692
3000
|
rootOfSkillPath.set(skill.path, skill.root);
|
|
2693
3001
|
}
|
|
3002
|
+
const pluginRootPaths = new Set(report.inventory.skillRoots.filter((r) => r.origin === "plugin").map((r) => r.root));
|
|
2694
3003
|
const byRoot = {};
|
|
3004
|
+
const pluginCounts = emptyCounts();
|
|
2695
3005
|
for (const finding2 of report.hygiene) {
|
|
2696
3006
|
let root;
|
|
2697
3007
|
for (let dir = finding2.target; ; dir = dirname5(dir)) {
|
|
@@ -2699,19 +3009,24 @@ function summarizeAudit(report, reportPath) {
|
|
|
2699
3009
|
if (root || dirname5(dir) === dir) break;
|
|
2700
3010
|
}
|
|
2701
3011
|
if (!root) continue;
|
|
3012
|
+
if (pluginRootPaths.has(root)) {
|
|
3013
|
+
bump(pluginCounts, finding2.severity);
|
|
3014
|
+
continue;
|
|
3015
|
+
}
|
|
2702
3016
|
byRoot[root] ??= emptyCounts();
|
|
2703
3017
|
bump(byRoot[root], finding2.severity);
|
|
2704
3018
|
}
|
|
3019
|
+
const findings = emptyCounts();
|
|
3020
|
+
for (const severity of Object.keys(findings)) {
|
|
3021
|
+
findings[severity] = report.summary.findingCounts[severity] - pluginCounts[severity];
|
|
3022
|
+
}
|
|
2705
3023
|
return {
|
|
2706
3024
|
schemaVersion: 1,
|
|
2707
3025
|
lastRunAt: report.generatedAt,
|
|
2708
3026
|
reportPath,
|
|
2709
3027
|
skillCount: report.summary.skillCount,
|
|
2710
3028
|
mcpTargetCount: report.summary.mcpTargetCount,
|
|
2711
|
-
|
|
2712
|
-
// array, and a second tally is how the terminal summary and the markdown report end up
|
|
2713
|
-
// disagreeing about the same run.
|
|
2714
|
-
findings: { ...report.summary.findingCounts },
|
|
3029
|
+
findings,
|
|
2715
3030
|
byRoot,
|
|
2716
3031
|
overlapClusters: report.opportunities.overlapClusters.map((c) => ({
|
|
2717
3032
|
// Cluster members are SKILL.md paths; the skill name is the parent directory name. Stripped
|
|
@@ -2811,11 +3126,11 @@ import {
|
|
|
2811
3126
|
statSync as statSync7
|
|
2812
3127
|
} from "fs";
|
|
2813
3128
|
import { tmpdir } from "os";
|
|
2814
|
-
import { basename as basename8, dirname as dirname6, extname as
|
|
3129
|
+
import { basename as basename8, dirname as dirname6, extname as extname6, join as join15, resolve as resolve3 } from "path";
|
|
2815
3130
|
|
|
2816
3131
|
// src/gate/mcpCapabilities.ts
|
|
2817
3132
|
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
2818
|
-
import { extname as
|
|
3133
|
+
import { extname as extname5, join as join14 } from "path";
|
|
2819
3134
|
var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
2820
3135
|
var MAX_ITEMS_PER_CATEGORY = 200;
|
|
2821
3136
|
function readJsonSafe(file) {
|
|
@@ -2940,7 +3255,7 @@ function extractMcpCapabilities(packageDir) {
|
|
|
2940
3255
|
let sawHttpTransport = false;
|
|
2941
3256
|
let sawMcpSdkImport = false;
|
|
2942
3257
|
for (const file of walk(packageDir)) {
|
|
2943
|
-
if (!SOURCE_EXT.has(
|
|
3258
|
+
if (!SOURCE_EXT.has(extname5(file))) continue;
|
|
2944
3259
|
const text = readTextFile(file);
|
|
2945
3260
|
if (text === void 0) continue;
|
|
2946
3261
|
captureRegistrations(text, buckets);
|
|
@@ -3078,7 +3393,7 @@ function resolveSkillMd(inputPath) {
|
|
|
3078
3393
|
let currentPath = inputPath;
|
|
3079
3394
|
let tmpDir;
|
|
3080
3395
|
const initialStat = statSync7(currentPath);
|
|
3081
|
-
if (
|
|
3396
|
+
if (extname6(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
|
|
3082
3397
|
tmpDir = mkdtempSync2(join15(tmpdir(), "forge_"));
|
|
3083
3398
|
extractZip(currentPath, tmpDir);
|
|
3084
3399
|
currentPath = tmpDir;
|
|
@@ -3641,14 +3956,14 @@ function renderSafetyLine(safety) {
|
|
|
3641
3956
|
const withDetail = detail ? `${safety.verdict} (${detail})` : safety.verdict;
|
|
3642
3957
|
return safety.recommendation ? `${withDetail} \u2014 ${safety.recommendation}` : withDetail;
|
|
3643
3958
|
}
|
|
3644
|
-
function
|
|
3959
|
+
function renderFindingLine2(finding2) {
|
|
3645
3960
|
const severity = finding2.severity ?? "UNKNOWN";
|
|
3646
3961
|
const rule = finding2.rule ?? "unknown-rule";
|
|
3647
3962
|
return ` - [${severity}] ${rule}${finding2.message ? `: ${finding2.message}` : ""}`;
|
|
3648
3963
|
}
|
|
3649
3964
|
function renderFindingsLines(findings) {
|
|
3650
3965
|
if (!findings?.length) return [];
|
|
3651
|
-
return [`${padLabel("Findings")}:`, ...findings.map(
|
|
3966
|
+
return [`${padLabel("Findings")}:`, ...findings.map(renderFindingLine2)];
|
|
3652
3967
|
}
|
|
3653
3968
|
function profileHeaderFields(profile) {
|
|
3654
3969
|
if (profile.kind === "mcp") {
|
|
@@ -3967,8 +4282,9 @@ function runHandoff(config, installedPath, options = {}) {
|
|
|
3967
4282
|
}
|
|
3968
4283
|
|
|
3969
4284
|
// src/commands/init.ts
|
|
3970
|
-
import { existsSync as
|
|
3971
|
-
import { homedir as
|
|
4285
|
+
import { existsSync as existsSync18 } from "fs";
|
|
4286
|
+
import { homedir as homedir5 } from "os";
|
|
4287
|
+
import { join as join23 } from "path";
|
|
3972
4288
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
3973
4289
|
|
|
3974
4290
|
// src/agents.ts
|
|
@@ -4218,12 +4534,12 @@ var AGENTS = [
|
|
|
4218
4534
|
].map(makeAgent);
|
|
4219
4535
|
|
|
4220
4536
|
// src/gitPreflight.ts
|
|
4221
|
-
import { relative as relative2, sep as
|
|
4537
|
+
import { relative as relative2, sep as sep3 } from "path";
|
|
4222
4538
|
function splitNulTerminated(output) {
|
|
4223
4539
|
return output.split("\0").filter((s) => s.length > 0);
|
|
4224
4540
|
}
|
|
4225
4541
|
function toPosixPath(p) {
|
|
4226
|
-
return p.split(
|
|
4542
|
+
return p.split(sep3).join("/");
|
|
4227
4543
|
}
|
|
4228
4544
|
var missingResult = (root) => ({
|
|
4229
4545
|
root,
|
|
@@ -4348,8 +4664,8 @@ function describeGitPreflight(result) {
|
|
|
4348
4664
|
}
|
|
4349
4665
|
|
|
4350
4666
|
// src/commands/audit.ts
|
|
4351
|
-
import { existsSync as
|
|
4352
|
-
import { join as
|
|
4667
|
+
import { existsSync as existsSync17 } from "fs";
|
|
4668
|
+
import { join as join22, resolve as resolvePath5 } from "path";
|
|
4353
4669
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
4354
4670
|
|
|
4355
4671
|
// src/decision.ts
|
|
@@ -4381,8 +4697,355 @@ ${prompt}`);
|
|
|
4381
4697
|
}
|
|
4382
4698
|
}
|
|
4383
4699
|
|
|
4700
|
+
// src/pluginRoots.ts
|
|
4701
|
+
import { existsSync as existsSync16, readFileSync as readFileSync16, readdirSync as readdirSync8, realpathSync as realpathSync3 } from "fs";
|
|
4702
|
+
import { homedir as homedir4 } from "os";
|
|
4703
|
+
import { isAbsolute as isAbsolute3, join as join20, relative as relative3, resolve as resolvePath3, sep as sep4 } from "path";
|
|
4704
|
+
function readJsonFileSafe(path) {
|
|
4705
|
+
let raw;
|
|
4706
|
+
try {
|
|
4707
|
+
raw = readFileSync16(path, "utf-8");
|
|
4708
|
+
} catch (err) {
|
|
4709
|
+
return { problem: err.code === "ENOENT" ? "missing" : "unreadable" };
|
|
4710
|
+
}
|
|
4711
|
+
try {
|
|
4712
|
+
return { data: JSON.parse(raw) };
|
|
4713
|
+
} catch {
|
|
4714
|
+
return { problem: "malformed" };
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
function describeProblem(problem, path) {
|
|
4718
|
+
const verb = problem === "missing" ? "not found" : problem === "unreadable" ? "unreadable" : "malformed (invalid JSON)";
|
|
4719
|
+
return `${path} ${verb}`;
|
|
4720
|
+
}
|
|
4721
|
+
function extractEnabledMap(data) {
|
|
4722
|
+
if (typeof data !== "object" || data === null) return {};
|
|
4723
|
+
const raw = data.enabledPlugins;
|
|
4724
|
+
if (typeof raw !== "object" || raw === null) return {};
|
|
4725
|
+
const out = {};
|
|
4726
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
4727
|
+
if (typeof value === "boolean") out[id] = value;
|
|
4728
|
+
}
|
|
4729
|
+
return out;
|
|
4730
|
+
}
|
|
4731
|
+
function extractRegistry(data) {
|
|
4732
|
+
const empty = { entries: {}, malformed: {} };
|
|
4733
|
+
if (typeof data !== "object" || data === null) return empty;
|
|
4734
|
+
const pluginsRaw = data.plugins;
|
|
4735
|
+
if (typeof pluginsRaw !== "object" || pluginsRaw === null) return empty;
|
|
4736
|
+
const out = {};
|
|
4737
|
+
const malformed = {};
|
|
4738
|
+
for (const [id, entriesRaw] of Object.entries(pluginsRaw)) {
|
|
4739
|
+
if (!Array.isArray(entriesRaw)) continue;
|
|
4740
|
+
const entries = [];
|
|
4741
|
+
for (const raw of entriesRaw) {
|
|
4742
|
+
const rec = typeof raw === "object" && raw !== null ? raw : {};
|
|
4743
|
+
const scope = rec.scope;
|
|
4744
|
+
const installPath = rec.installPath;
|
|
4745
|
+
const version = rec.version;
|
|
4746
|
+
if ((scope === "user" || scope === "project" || scope === "local") && typeof installPath === "string" && typeof version === "string") {
|
|
4747
|
+
entries.push({
|
|
4748
|
+
scope,
|
|
4749
|
+
projectPath: typeof rec.projectPath === "string" ? rec.projectPath : void 0,
|
|
4750
|
+
installPath,
|
|
4751
|
+
version
|
|
4752
|
+
});
|
|
4753
|
+
} else {
|
|
4754
|
+
malformed[id] = (malformed[id] ?? 0) + 1;
|
|
4755
|
+
}
|
|
4756
|
+
}
|
|
4757
|
+
if (entries.length > 0) out[id] = entries;
|
|
4758
|
+
}
|
|
4759
|
+
return { entries: out, malformed };
|
|
4760
|
+
}
|
|
4761
|
+
function splitPluginId(id) {
|
|
4762
|
+
const idx = id.lastIndexOf("@");
|
|
4763
|
+
if (idx === -1) return { plugin: id, marketplace: "" };
|
|
4764
|
+
return { plugin: id.slice(0, idx), marketplace: id.slice(idx + 1) };
|
|
4765
|
+
}
|
|
4766
|
+
function safeRealpath(p) {
|
|
4767
|
+
try {
|
|
4768
|
+
return realpathSync3(p);
|
|
4769
|
+
} catch {
|
|
4770
|
+
return void 0;
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
function parseSemver(v) {
|
|
4774
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
4775
|
+
if (!m) return void 0;
|
|
4776
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
4777
|
+
}
|
|
4778
|
+
function compareVersionsDesc(a, b) {
|
|
4779
|
+
const pa = parseSemver(a);
|
|
4780
|
+
const pb = parseSemver(b);
|
|
4781
|
+
if (pa && pb) {
|
|
4782
|
+
for (let i = 0; i < 3; i++) {
|
|
4783
|
+
if (pa[i] !== pb[i]) return pb[i] - pa[i];
|
|
4784
|
+
}
|
|
4785
|
+
return b.localeCompare(a);
|
|
4786
|
+
}
|
|
4787
|
+
if (pa && !pb) return -1;
|
|
4788
|
+
if (!pa && pb) return 1;
|
|
4789
|
+
return b.localeCompare(a);
|
|
4790
|
+
}
|
|
4791
|
+
function selectInstallEntry(entries, cwd) {
|
|
4792
|
+
const userEntry = entries.find((e) => e.scope === "user");
|
|
4793
|
+
if (userEntry) return userEntry;
|
|
4794
|
+
const cwdReal = safeRealpath(cwd);
|
|
4795
|
+
if (cwdReal) {
|
|
4796
|
+
const projectEntry = entries.find((e) => e.scope === "project" && e.projectPath && safeRealpath(e.projectPath) === cwdReal);
|
|
4797
|
+
if (projectEntry) return projectEntry;
|
|
4798
|
+
}
|
|
4799
|
+
return [...entries].sort((a, b) => compareVersionsDesc(a.version, b.version))[0];
|
|
4800
|
+
}
|
|
4801
|
+
function isInside2(child, root) {
|
|
4802
|
+
if (child === root) return true;
|
|
4803
|
+
const rel = relative3(root, child);
|
|
4804
|
+
return rel !== "" && !rel.startsWith(".." + sep4) && rel !== ".." && !isAbsolute3(rel);
|
|
4805
|
+
}
|
|
4806
|
+
var DEFAULT_SKILL_LAYOUT = { paths: ["./skills/"], explicit: false };
|
|
4807
|
+
function normalizeDeclaredSkillsField(manifest, pluginId, notices) {
|
|
4808
|
+
if (typeof manifest !== "object" || manifest === null) return DEFAULT_SKILL_LAYOUT;
|
|
4809
|
+
const skillsField = manifest.skills;
|
|
4810
|
+
if (skillsField === void 0) return DEFAULT_SKILL_LAYOUT;
|
|
4811
|
+
if (typeof skillsField === "string") return { paths: [skillsField], explicit: true };
|
|
4812
|
+
if (Array.isArray(skillsField) && skillsField.every((s) => typeof s === "string")) return { paths: skillsField, explicit: true };
|
|
4813
|
+
notices.push(`plugin ${pluginId}: manifest "skills" field has an unrecognized shape \u2014 using default skill layout`);
|
|
4814
|
+
return DEFAULT_SKILL_LAYOUT;
|
|
4815
|
+
}
|
|
4816
|
+
function resolveDeclaredPath(installPath, realInstall, declaredPath, explicit, pluginId, notices) {
|
|
4817
|
+
const joined = resolvePath3(installPath, declaredPath);
|
|
4818
|
+
let real;
|
|
4819
|
+
try {
|
|
4820
|
+
real = realpathSync3(joined);
|
|
4821
|
+
} catch {
|
|
4822
|
+
if (explicit) notices.push(`plugin ${pluginId}: declared skill path does not exist, skipping: ${declaredPath}`);
|
|
4823
|
+
return [];
|
|
4824
|
+
}
|
|
4825
|
+
if (!realInstall || !isInside2(real, realInstall)) {
|
|
4826
|
+
notices.push(`plugin ${pluginId}: declared skill path escapes the install root, skipping: ${declaredPath}`);
|
|
4827
|
+
return [];
|
|
4828
|
+
}
|
|
4829
|
+
if (existsSync16(join20(real, "SKILL.md"))) return [real];
|
|
4830
|
+
let entries;
|
|
4831
|
+
try {
|
|
4832
|
+
entries = readdirSync8(real, { withFileTypes: true });
|
|
4833
|
+
} catch {
|
|
4834
|
+
return [];
|
|
4835
|
+
}
|
|
4836
|
+
const found = [];
|
|
4837
|
+
for (const entry of entries) {
|
|
4838
|
+
let childPath = join20(real, entry.name);
|
|
4839
|
+
if (entry.isSymbolicLink()) {
|
|
4840
|
+
const target = safeRealpath(childPath);
|
|
4841
|
+
if (!target || !isInside2(target, realInstall)) {
|
|
4842
|
+
notices.push(`plugin ${pluginId}: skill dir symlink escapes the install root, skipping: ${join20(declaredPath, entry.name)}`);
|
|
4843
|
+
continue;
|
|
4844
|
+
}
|
|
4845
|
+
childPath = target;
|
|
4846
|
+
} else if (!entry.isDirectory()) {
|
|
4847
|
+
continue;
|
|
4848
|
+
}
|
|
4849
|
+
if (existsSync16(join20(childPath, "SKILL.md"))) found.push(childPath);
|
|
4850
|
+
}
|
|
4851
|
+
return found;
|
|
4852
|
+
}
|
|
4853
|
+
function resolveSkillDirs(installPath, pluginId) {
|
|
4854
|
+
const notices = [];
|
|
4855
|
+
const manifestPath = join20(installPath, ".claude-plugin", "plugin.json");
|
|
4856
|
+
const manifestResult = readJsonFileSafe(manifestPath);
|
|
4857
|
+
if (manifestResult.problem) {
|
|
4858
|
+
notices.push(`plugin ${pluginId}: ${manifestResult.problem} manifest at ${manifestPath} \u2014 using default skill layout`);
|
|
4859
|
+
}
|
|
4860
|
+
const declared = manifestResult.problem ? DEFAULT_SKILL_LAYOUT : normalizeDeclaredSkillsField(manifestResult.data, pluginId, notices);
|
|
4861
|
+
const realInstall = safeRealpath(installPath);
|
|
4862
|
+
const skillDirs = /* @__PURE__ */ new Set();
|
|
4863
|
+
for (const declaredPath of declared.paths) {
|
|
4864
|
+
for (const dir of resolveDeclaredPath(installPath, realInstall, declaredPath, declared.explicit, pluginId, notices)) {
|
|
4865
|
+
skillDirs.add(dir);
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
return { skillDirs: Array.from(skillDirs).sort(), notices, realInstall };
|
|
4869
|
+
}
|
|
4870
|
+
function discoverClaudePluginRoots(options = {}) {
|
|
4871
|
+
const homeDir = options.homeDir ?? homedir4();
|
|
4872
|
+
const cwd = options.cwd ?? process.cwd();
|
|
4873
|
+
const settingsPath = options.settingsPath ?? join20(homeDir, ".claude", "settings.json");
|
|
4874
|
+
const localSettingsPath = options.localSettingsPath ?? join20(cwd, ".claude", "settings.local.json");
|
|
4875
|
+
const installedPluginsPath = options.installedPluginsPath ?? join20(homeDir, ".claude", "plugins", "installed_plugins.json");
|
|
4876
|
+
const notices = [];
|
|
4877
|
+
const settingsResult = readJsonFileSafe(settingsPath);
|
|
4878
|
+
if (settingsResult.problem) {
|
|
4879
|
+
notices.push(`Claude Code settings ${describeProblem(settingsResult.problem, settingsPath)} \u2014 skipping Claude plugin discovery`);
|
|
4880
|
+
return { roots: [], notices };
|
|
4881
|
+
}
|
|
4882
|
+
const registryResult = readJsonFileSafe(installedPluginsPath);
|
|
4883
|
+
if (registryResult.problem) {
|
|
4884
|
+
notices.push(`Claude Code plugin registry ${describeProblem(registryResult.problem, installedPluginsPath)} \u2014 skipping Claude plugin discovery`);
|
|
4885
|
+
return { roots: [], notices };
|
|
4886
|
+
}
|
|
4887
|
+
const globalEnabled = extractEnabledMap(settingsResult.data);
|
|
4888
|
+
let localEnabled = {};
|
|
4889
|
+
if (existsSync16(localSettingsPath)) {
|
|
4890
|
+
const localResult = readJsonFileSafe(localSettingsPath);
|
|
4891
|
+
if (localResult.problem) {
|
|
4892
|
+
notices.push(`local settings ${describeProblem(localResult.problem, localSettingsPath)} \u2014 ignoring local plugin overrides`);
|
|
4893
|
+
} else {
|
|
4894
|
+
localEnabled = extractEnabledMap(localResult.data);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
const merged = { ...globalEnabled, ...localEnabled };
|
|
4898
|
+
const enabledIds = Object.keys(merged).filter((id) => merged[id] === true).sort();
|
|
4899
|
+
const { entries: registry, malformed } = extractRegistry(registryResult.data);
|
|
4900
|
+
const roots = [];
|
|
4901
|
+
for (const id of enabledIds) {
|
|
4902
|
+
const entries = registry[id];
|
|
4903
|
+
const dropped = malformed[id] ?? 0;
|
|
4904
|
+
if (dropped > 0) {
|
|
4905
|
+
notices.push(`plugin ${id}: ignored ${dropped} malformed installed_plugins.json entr${dropped === 1 ? "y" : "ies"} (missing installPath/version or unknown scope)`);
|
|
4906
|
+
}
|
|
4907
|
+
if (!entries || entries.length === 0) {
|
|
4908
|
+
notices.push(`plugin ${id} is enabled but has no ${dropped > 0 ? "usable " : ""}installed_plugins.json entry \u2014 skipping`);
|
|
4909
|
+
continue;
|
|
4910
|
+
}
|
|
4911
|
+
const entry = selectInstallEntry(entries, cwd);
|
|
4912
|
+
const { plugin, marketplace } = splitPluginId(id);
|
|
4913
|
+
const { skillDirs, notices: manifestNotices, realInstall } = resolveSkillDirs(entry.installPath, id);
|
|
4914
|
+
notices.push(...manifestNotices);
|
|
4915
|
+
roots.push({ pluginId: id, marketplace, plugin, version: entry.version, installPath: realInstall ?? entry.installPath, skillDirs });
|
|
4916
|
+
}
|
|
4917
|
+
roots.sort((a, b) => a.pluginId < b.pluginId ? -1 : a.pluginId > b.pluginId ? 1 : 0);
|
|
4918
|
+
return { roots, notices };
|
|
4919
|
+
}
|
|
4920
|
+
|
|
4921
|
+
// src/usageSnapshot.ts
|
|
4922
|
+
import { readFileSync as readFileSync17, readdirSync as readdirSync9 } from "fs";
|
|
4923
|
+
import { extname as extname7, join as join21, resolve as resolvePath4 } from "path";
|
|
4924
|
+
function isValidCount(value) {
|
|
4925
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
4926
|
+
}
|
|
4927
|
+
function toSnapshot(parsed, sourcePath) {
|
|
4928
|
+
return {
|
|
4929
|
+
usage: parsed.usage,
|
|
4930
|
+
exhaustive: parsed.exhaustive,
|
|
4931
|
+
windowDays: parsed.windowDays,
|
|
4932
|
+
generatedAt: parsed.generatedAt,
|
|
4933
|
+
sourcePath,
|
|
4934
|
+
...parsed.warning ? { warning: parsed.warning } : {}
|
|
4935
|
+
};
|
|
4936
|
+
}
|
|
4937
|
+
function emptySnapshot(sourcePath, warning) {
|
|
4938
|
+
return { usage: /* @__PURE__ */ new Map(), exhaustive: false, sourcePath, warning };
|
|
4939
|
+
}
|
|
4940
|
+
function parseSnapshotFile(raw) {
|
|
4941
|
+
let parsed;
|
|
4942
|
+
try {
|
|
4943
|
+
parsed = JSON.parse(raw);
|
|
4944
|
+
} catch (err) {
|
|
4945
|
+
return { error: `could not parse usage snapshot (${err.message})` };
|
|
4946
|
+
}
|
|
4947
|
+
const outer = parsed && typeof parsed === "object" ? parsed : void 0;
|
|
4948
|
+
const report = outer && typeof outer.report === "object" && outer.report !== null ? outer.report : void 0;
|
|
4949
|
+
if (!report) return { error: 'usage snapshot has no "report" object' };
|
|
4950
|
+
const generatedAt = typeof report.generated_at === "string" ? report.generated_at : void 0;
|
|
4951
|
+
const windowDays = typeof report.window_days === "number" ? report.window_days : void 0;
|
|
4952
|
+
const uniqueSkillsUsed = typeof report.unique_skills_used === "number" ? report.unique_skills_used : void 0;
|
|
4953
|
+
const skillTotalsRaw = report.skill_totals;
|
|
4954
|
+
const skillTotals = skillTotalsRaw && typeof skillTotalsRaw === "object" && !Array.isArray(skillTotalsRaw) ? skillTotalsRaw : void 0;
|
|
4955
|
+
const warning = skillTotalsRaw !== void 0 && skillTotals === void 0 ? 'usage snapshot: "skill_totals" is not an object \u2014 ignoring it' : void 0;
|
|
4956
|
+
const topSkills = Array.isArray(report.top_skills) ? report.top_skills : void 0;
|
|
4957
|
+
const usage = /* @__PURE__ */ new Map();
|
|
4958
|
+
if (skillTotals) {
|
|
4959
|
+
for (const [name, count] of Object.entries(skillTotals)) {
|
|
4960
|
+
if (isValidCount(count)) usage.set(name, count);
|
|
4961
|
+
}
|
|
4962
|
+
} else if (topSkills) {
|
|
4963
|
+
for (const row of topSkills) {
|
|
4964
|
+
if (Array.isArray(row) && row.length === 2 && typeof row[0] === "string" && isValidCount(row[1])) {
|
|
4965
|
+
usage.set(row[0], row[1]);
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
4969
|
+
const exhaustive = Boolean(skillTotals) || topSkills !== void 0 && uniqueSkillsUsed !== void 0 && topSkills.length === uniqueSkillsUsed;
|
|
4970
|
+
return { usage, exhaustive, windowDays, generatedAt, warning };
|
|
4971
|
+
}
|
|
4972
|
+
function pickLatest(candidates) {
|
|
4973
|
+
return candidates.reduce((best, cur) => {
|
|
4974
|
+
if (!best) return cur;
|
|
4975
|
+
const curAt = cur.parsed.generatedAt;
|
|
4976
|
+
const bestAt = best.parsed.generatedAt;
|
|
4977
|
+
if (curAt === void 0 && bestAt === void 0) return cur.path > best.path ? cur : best;
|
|
4978
|
+
if (curAt === void 0) return best;
|
|
4979
|
+
if (bestAt === void 0) return cur;
|
|
4980
|
+
if (curAt === bestAt) return cur.path > best.path ? cur : best;
|
|
4981
|
+
return curAt > bestAt ? cur : best;
|
|
4982
|
+
}, void 0);
|
|
4983
|
+
}
|
|
4984
|
+
function loadUsageSnapshot(pathOrDir, opts = {}) {
|
|
4985
|
+
const resolved = resolvePath4(expandHome(pathOrDir));
|
|
4986
|
+
if (isDir(resolved)) {
|
|
4987
|
+
let entries;
|
|
4988
|
+
try {
|
|
4989
|
+
entries = readdirSync9(resolved).filter((f) => extname7(f).toLowerCase() === ".json");
|
|
4990
|
+
} catch (err) {
|
|
4991
|
+
return emptySnapshot(resolved, `could not read usage snapshot directory (${err.message})`);
|
|
4992
|
+
}
|
|
4993
|
+
if (entries.length === 0) {
|
|
4994
|
+
return emptySnapshot(resolved, `no *.json snapshot files found in ${resolved}`);
|
|
4995
|
+
}
|
|
4996
|
+
const candidates = [];
|
|
4997
|
+
for (const name of entries) {
|
|
4998
|
+
const filePath = join21(resolved, name);
|
|
4999
|
+
let raw2;
|
|
5000
|
+
try {
|
|
5001
|
+
raw2 = readFileSync17(filePath, "utf-8");
|
|
5002
|
+
} catch {
|
|
5003
|
+
continue;
|
|
5004
|
+
}
|
|
5005
|
+
const parsed2 = parseSnapshotFile(raw2);
|
|
5006
|
+
if ("error" in parsed2) continue;
|
|
5007
|
+
if (opts.windowDays !== void 0 && parsed2.windowDays !== opts.windowDays) continue;
|
|
5008
|
+
candidates.push({ path: filePath, parsed: parsed2 });
|
|
5009
|
+
}
|
|
5010
|
+
const picked = pickLatest(candidates);
|
|
5011
|
+
if (!picked) {
|
|
5012
|
+
return emptySnapshot(
|
|
5013
|
+
resolved,
|
|
5014
|
+
opts.windowDays !== void 0 ? `no usage snapshot in ${resolved} matches --usage-window ${opts.windowDays}` : `no readable usage snapshot found in ${resolved}`
|
|
5015
|
+
);
|
|
5016
|
+
}
|
|
5017
|
+
return toSnapshot(picked.parsed, picked.path);
|
|
5018
|
+
}
|
|
5019
|
+
let raw;
|
|
5020
|
+
try {
|
|
5021
|
+
raw = readFileSync17(resolved, "utf-8");
|
|
5022
|
+
} catch (err) {
|
|
5023
|
+
return emptySnapshot(resolved, `could not read usage snapshot (${err.message})`);
|
|
5024
|
+
}
|
|
5025
|
+
const parsed = parseSnapshotFile(raw);
|
|
5026
|
+
if ("error" in parsed) return emptySnapshot(resolved, parsed.error);
|
|
5027
|
+
return toSnapshot(parsed, resolved);
|
|
5028
|
+
}
|
|
5029
|
+
|
|
4384
5030
|
// src/commands/audit.ts
|
|
4385
5031
|
var CURATION_PROMPT_ASSET = "curation-prompt.md";
|
|
5032
|
+
function resolvePluginAndUsageContext(config, options, deps = {}) {
|
|
5033
|
+
if (options.usageWindow !== void 0 && (!Number.isInteger(options.usageWindow) || options.usageWindow < 1)) {
|
|
5034
|
+
throw new UserError(`--usage-window must be a positive integer (got "${options.usageWindow}")`);
|
|
5035
|
+
}
|
|
5036
|
+
const wantPlugins = options.claudePlugins ?? config.claudePlugins ?? false;
|
|
5037
|
+
let pluginRoots;
|
|
5038
|
+
const notices = [];
|
|
5039
|
+
if (wantPlugins) {
|
|
5040
|
+
const discovered = discoverClaudePluginRoots(deps);
|
|
5041
|
+
pluginRoots = discovered.roots;
|
|
5042
|
+
notices.push(...discovered.notices);
|
|
5043
|
+
}
|
|
5044
|
+
const usageSnapshotPath = options.usageSnapshot ?? config.usageSnapshotPath;
|
|
5045
|
+
const usage = usageSnapshotPath ? loadUsageSnapshot(usageSnapshotPath, { windowDays: options.usageWindow }) : void 0;
|
|
5046
|
+
if (usage?.warning) notices.push(usage.warning);
|
|
5047
|
+
return { pluginRoots, usage, notices };
|
|
5048
|
+
}
|
|
4386
5049
|
function splitCsv(s) {
|
|
4387
5050
|
return s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
4388
5051
|
}
|
|
@@ -4415,11 +5078,16 @@ async function captureBusinessProfile(rl, existing) {
|
|
|
4415
5078
|
return profile;
|
|
4416
5079
|
}
|
|
4417
5080
|
function resolveReportPath(reportOption) {
|
|
4418
|
-
if (reportOption) return
|
|
5081
|
+
if (reportOption) return resolvePath5(reportOption);
|
|
4419
5082
|
const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4420
|
-
return
|
|
5083
|
+
return join22(getHomeDir(), "reports", `audit-${iso}.md`);
|
|
5084
|
+
}
|
|
5085
|
+
function summarizePluginRecommendations(plugins) {
|
|
5086
|
+
const counts = { keep: 0, review: 0, unobserved: 0, unknown: 0 };
|
|
5087
|
+
for (const p of plugins) counts[p.recommendation]++;
|
|
5088
|
+
return `plugins: ${plugins.length} (review ${counts.review}, unobserved ${counts.unobserved}, unknown ${counts.unknown})`;
|
|
4421
5089
|
}
|
|
4422
|
-
function printTerminalSummary(report, reportPath) {
|
|
5090
|
+
function printTerminalSummary(report, reportPath, usage) {
|
|
4423
5091
|
const { summary } = report;
|
|
4424
5092
|
console.log("[skill-forge] audit complete");
|
|
4425
5093
|
console.log(` skill roots: ${summary.rootCount} skills: ${summary.skillCount} MCP targets: ${summary.mcpTargetCount}`);
|
|
@@ -4428,6 +5096,12 @@ function printTerminalSummary(report, reportPath) {
|
|
|
4428
5096
|
);
|
|
4429
5097
|
console.log(` accepted: ${summary.acceptedCount} (suppressed from counts; see report)`);
|
|
4430
5098
|
console.log(` overlap clusters: ${summary.overlapClusterCount} evolve-eligible: ${summary.evolveEligibleCount}`);
|
|
5099
|
+
if (report.plugins) console.log(` ${summarizePluginRecommendations(report.plugins)}`);
|
|
5100
|
+
if (usage) {
|
|
5101
|
+
console.log(
|
|
5102
|
+
` usage: ${usage.sourcePath} (window ${usage.windowDays !== void 0 ? `${usage.windowDays} day(s)` : "unknown"}, ${usage.exhaustive ? "exhaustive" : "top-50 only"})`
|
|
5103
|
+
);
|
|
5104
|
+
}
|
|
4431
5105
|
console.log(` report -> ${reportPath}`);
|
|
4432
5106
|
for (const notice of report.notices) console.log(` [notice] ${notice}`);
|
|
4433
5107
|
console.log(
|
|
@@ -4465,9 +5139,9 @@ async function maybeHandoff(config, reportPath, options, rl) {
|
|
|
4465
5139
|
if (!wantHandoff) return;
|
|
4466
5140
|
runHandoff(config, reportPath, { promptAsset: CURATION_PROMPT_ASSET, log: options.json ? console.error : console.log });
|
|
4467
5141
|
}
|
|
4468
|
-
async function auditCommand(options = {}) {
|
|
5142
|
+
async function auditCommand(options = {}, deps = {}) {
|
|
4469
5143
|
await maybeOfferInit({ json: options.json, yes: options.yes, suppressAuditOffer: true });
|
|
4470
|
-
if (!
|
|
5144
|
+
if (!existsSync17(getConfigPath())) {
|
|
4471
5145
|
throw new UserError(
|
|
4472
5146
|
"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
5147
|
);
|
|
@@ -4483,9 +5157,11 @@ async function auditCommand(options = {}) {
|
|
|
4483
5157
|
saveConfig(config);
|
|
4484
5158
|
}
|
|
4485
5159
|
}
|
|
5160
|
+
const { pluginRoots, usage, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
4486
5161
|
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
4487
|
-
const report = await auditConfiguredSet(config, { accepted });
|
|
4488
|
-
|
|
5162
|
+
const report = await auditConfiguredSet(config, { accepted, pluginRoots, usage });
|
|
5163
|
+
const extraNotices = [...storeNotices, ...pluginUsageNotices];
|
|
5164
|
+
if (extraNotices.length > 0) report.notices = [...report.notices, ...extraNotices];
|
|
4489
5165
|
const reportPath = resolveReportPath(options.report);
|
|
4490
5166
|
writeNewFile(reportPath, renderAuditMarkdown(report), "--report");
|
|
4491
5167
|
try {
|
|
@@ -4496,7 +5172,7 @@ async function auditCommand(options = {}) {
|
|
|
4496
5172
|
if (options.json) {
|
|
4497
5173
|
console.log(JSON.stringify(report, null, 2));
|
|
4498
5174
|
} else {
|
|
4499
|
-
printTerminalSummary(report, reportPath);
|
|
5175
|
+
printTerminalSummary(report, reportPath, usage);
|
|
4500
5176
|
}
|
|
4501
5177
|
config = await maybeScaffoldFoundation(config, report, options, rl);
|
|
4502
5178
|
await maybeHandoff(config, reportPath, options, rl);
|
|
@@ -4613,7 +5289,7 @@ function printMcpDetectionResults(detectedMcp) {
|
|
|
4613
5289
|
function handoffTemplateFor(agent) {
|
|
4614
5290
|
return agent.defaultHandoffTemplate ?? [agent.id, "-p", HANDOFF_INSTRUCTION];
|
|
4615
5291
|
}
|
|
4616
|
-
function buildConfig(selected, defaultTargetEntry, handoffAgent, selectedMcp = []) {
|
|
5292
|
+
function buildConfig(selected, defaultTargetEntry, handoffAgent, selectedMcp = [], claudePlugins) {
|
|
4617
5293
|
const existing = loadConfig();
|
|
4618
5294
|
return {
|
|
4619
5295
|
...existing,
|
|
@@ -4625,7 +5301,8 @@ function buildConfig(selected, defaultTargetEntry, handoffAgent, selectedMcp = [
|
|
|
4625
5301
|
agents: selected.length > 0 ? selected.flatMap((s) => s.members.map((m) => ({ id: m.agent.id, skillsRoot: s.root }))) : existing.agents,
|
|
4626
5302
|
defaultTarget: defaultTargetEntry?.root ?? existing.defaultTarget,
|
|
4627
5303
|
handoffCommand: handoffAgent ? handoffTemplateFor(handoffAgent) : existing.handoffCommand,
|
|
4628
|
-
mcpTargets: selectedMcp.length > 0 ? selectedMcp.map((m) => m.path) : existing.mcpTargets
|
|
5304
|
+
mcpTargets: selectedMcp.length > 0 ? selectedMcp.map((m) => m.path) : existing.mcpTargets,
|
|
5305
|
+
claudePlugins: claudePlugins !== void 0 ? claudePlugins : existing.claudePlugins
|
|
4629
5306
|
};
|
|
4630
5307
|
}
|
|
4631
5308
|
async function printSummary(config, deps = {}) {
|
|
@@ -4790,7 +5467,7 @@ async function init(options = {}, deps = {}) {
|
|
|
4790
5467
|
const runAudit = deps.runAudit ?? auditCommand;
|
|
4791
5468
|
const gitDeps = deps.gitExec ? { exec: deps.gitExec } : {};
|
|
4792
5469
|
const cwd = options.cwd ?? process.cwd();
|
|
4793
|
-
const home = options.home ??
|
|
5470
|
+
const home = options.home ?? homedir5();
|
|
4794
5471
|
const detected = detectAgents(cwd, home, options.pathEnv);
|
|
4795
5472
|
const detectedMcp = detectMcpTargets(cwd, home, options.pathEnv);
|
|
4796
5473
|
if (options.list) {
|
|
@@ -4813,10 +5490,18 @@ async function init(options = {}, deps = {}) {
|
|
|
4813
5490
|
"[skill-forge] No detected agent CLI was found on PATH \u2014 keeping every detected target selected."
|
|
4814
5491
|
);
|
|
4815
5492
|
}
|
|
5493
|
+
const installedPluginsPath = options.installedPluginsPath ?? join23(home, ".claude", "plugins", "installed_plugins.json");
|
|
5494
|
+
const claudePluginsRegistryExists = existsSync18(installedPluginsPath);
|
|
4816
5495
|
if (options.defaults) {
|
|
4817
5496
|
const selectedRoots = narrow ? rootGroups.filter(hasInstalledMember) : rootGroups;
|
|
4818
5497
|
const selectedMcpTargets = narrow ? mcpGroups.filter(hasInstalledMember) : mcpGroups;
|
|
4819
|
-
const config = buildConfig(
|
|
5498
|
+
const config = buildConfig(
|
|
5499
|
+
selectedRoots,
|
|
5500
|
+
selectedRoots[0],
|
|
5501
|
+
void 0,
|
|
5502
|
+
selectedMcpTargets,
|
|
5503
|
+
claudePluginsRegistryExists ? true : void 0
|
|
5504
|
+
);
|
|
4820
5505
|
saveConfig(config);
|
|
4821
5506
|
await printSummary(config, gitDeps);
|
|
4822
5507
|
await runAudit({ yes: true });
|
|
@@ -4840,7 +5525,12 @@ async function init(options = {}, deps = {}) {
|
|
|
4840
5525
|
const target = selected.length > 0 ? await promptDefaultTarget(rl, selected) : void 0;
|
|
4841
5526
|
const handoffAgent = detected.length > 0 ? await promptHandoffAgent(rl, detected, narrow) : void 0;
|
|
4842
5527
|
await promptGitPreflight(rl, selected, gitDeps);
|
|
4843
|
-
|
|
5528
|
+
let claudePluginsAnswer;
|
|
5529
|
+
if (claudePluginsRegistryExists) {
|
|
5530
|
+
const answer = (await rl.question("\nInclude enabled Claude Code plugins in audits? [Y/n] ")).trim().toLowerCase();
|
|
5531
|
+
claudePluginsAnswer = answer === "" || answer === "y" || answer === "yes";
|
|
5532
|
+
}
|
|
5533
|
+
const config = buildConfig(selected, target, handoffAgent, selectedMcp, claudePluginsAnswer);
|
|
4844
5534
|
saveConfig(config);
|
|
4845
5535
|
await printSummary(config, gitDeps);
|
|
4846
5536
|
if (options.offerAudit !== false) {
|
|
@@ -4860,7 +5550,7 @@ async function init(options = {}, deps = {}) {
|
|
|
4860
5550
|
}
|
|
4861
5551
|
async function maybeOfferInit(options = {}) {
|
|
4862
5552
|
if (options.json || options.yes || !process.stdin.isTTY) return;
|
|
4863
|
-
if (
|
|
5553
|
+
if (existsSync18(getConfigPath())) return;
|
|
4864
5554
|
console.log(
|
|
4865
5555
|
"[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
5556
|
);
|
|
@@ -5011,7 +5701,7 @@ async function addMcp(source, config, options) {
|
|
|
5011
5701
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
5012
5702
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
5013
5703
|
};
|
|
5014
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
5704
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join24(process.cwd(), ".mcp.json");
|
|
5015
5705
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
5016
5706
|
reject(entry);
|
|
5017
5707
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -5107,7 +5797,7 @@ async function scan(source, options = {}) {
|
|
|
5107
5797
|
}
|
|
5108
5798
|
|
|
5109
5799
|
// src/commands/list.ts
|
|
5110
|
-
import { join as
|
|
5800
|
+
import { join as join25 } from "path";
|
|
5111
5801
|
async function list() {
|
|
5112
5802
|
await maybeOfferInit();
|
|
5113
5803
|
const config = loadConfig();
|
|
@@ -5120,14 +5810,14 @@ async function list() {
|
|
|
5120
5810
|
}
|
|
5121
5811
|
console.log(`[skill-forge] ${ids.length} held in quarantine (${quarantineDir}):`);
|
|
5122
5812
|
for (const id of ids) {
|
|
5123
|
-
const skillDirs = findSkillDirs(
|
|
5813
|
+
const skillDirs = findSkillDirs(join25(quarantineDir, id));
|
|
5124
5814
|
if (skillDirs.length === 0) {
|
|
5125
5815
|
console.log(` ${id} (no SKILL.md found)`);
|
|
5126
5816
|
continue;
|
|
5127
5817
|
}
|
|
5128
5818
|
const names = skillDirs.map((dir) => {
|
|
5129
5819
|
try {
|
|
5130
|
-
return readFrontmatter(
|
|
5820
|
+
return readFrontmatter(join25(dir, "SKILL.md")).name;
|
|
5131
5821
|
} catch {
|
|
5132
5822
|
return dir;
|
|
5133
5823
|
}
|
|
@@ -5137,23 +5827,23 @@ async function list() {
|
|
|
5137
5827
|
}
|
|
5138
5828
|
|
|
5139
5829
|
// src/commands/guide.ts
|
|
5140
|
-
import { existsSync as
|
|
5830
|
+
import { existsSync as existsSync20 } from "fs";
|
|
5141
5831
|
|
|
5142
5832
|
// src/commands/held.ts
|
|
5143
|
-
import { existsSync as
|
|
5144
|
-
import { basename as basename9, join as
|
|
5833
|
+
import { existsSync as existsSync19, realpathSync as realpathSync4 } from "fs";
|
|
5834
|
+
import { basename as basename9, join as join26, resolve as resolvePath6, sep as sep5 } from "path";
|
|
5145
5835
|
var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
|
|
5146
5836
|
function resolveHeldPath(quarantineDir, id) {
|
|
5147
5837
|
if (id === "" || id !== basename9(id) || id === "." || id === "..") {
|
|
5148
5838
|
throw new UserError(`invalid quarantine id "${id}" \u2014 pass a single id exactly as printed by \`skill-forge list\``);
|
|
5149
5839
|
}
|
|
5150
|
-
const candidate =
|
|
5151
|
-
if (!
|
|
5840
|
+
const candidate = join26(quarantineDir, id);
|
|
5841
|
+
if (!existsSync19(candidate)) {
|
|
5152
5842
|
throw new UserError(`no held entry with id "${id}" \u2014 run \`skill-forge list\` to see what is in quarantine`);
|
|
5153
5843
|
}
|
|
5154
|
-
const realRoot =
|
|
5155
|
-
const realCandidate =
|
|
5156
|
-
if (!realCandidate.startsWith(realRoot +
|
|
5844
|
+
const realRoot = realpathSync4(resolvePath6(quarantineDir));
|
|
5845
|
+
const realCandidate = realpathSync4(candidate);
|
|
5846
|
+
if (!realCandidate.startsWith(realRoot + sep5)) {
|
|
5157
5847
|
throw new UserError(`refusing to act on "${id}": it resolves outside the quarantine dir (${realRoot})`);
|
|
5158
5848
|
}
|
|
5159
5849
|
return realCandidate;
|
|
@@ -5176,7 +5866,7 @@ function resolveTargetRoot(config, target) {
|
|
|
5176
5866
|
function heldSkillNames(skillDirs) {
|
|
5177
5867
|
return skillDirs.map((dir) => {
|
|
5178
5868
|
try {
|
|
5179
|
-
return readFrontmatter(
|
|
5869
|
+
return readFrontmatter(join26(dir, "SKILL.md")).name;
|
|
5180
5870
|
} catch {
|
|
5181
5871
|
return basename9(dir);
|
|
5182
5872
|
}
|
|
@@ -5184,7 +5874,7 @@ function heldSkillNames(skillDirs) {
|
|
|
5184
5874
|
}
|
|
5185
5875
|
function looksLikeMcpSandbox(quarantinePath) {
|
|
5186
5876
|
const candidate = resolveMcpCandidatePath({ quarantinePath });
|
|
5187
|
-
return [".mcp.json", "mcp.json", "package.json"].some((f) =>
|
|
5877
|
+
return [".mcp.json", "mcp.json", "package.json"].some((f) => existsSync19(join26(candidate, f)));
|
|
5188
5878
|
}
|
|
5189
5879
|
async function promoteHeldMcp(id, entry, config, options) {
|
|
5190
5880
|
const candidatePath = resolveMcpCandidatePath(entry);
|
|
@@ -5218,7 +5908,7 @@ async function promoteHeldMcp(id, entry, config, options) {
|
|
|
5218
5908
|
envVars: detail.env ? Object.keys(detail.env) : void 0,
|
|
5219
5909
|
candidateEntry: detail.command ? { command: detail.command, args: detail.args, env: detail.env } : void 0
|
|
5220
5910
|
};
|
|
5221
|
-
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ??
|
|
5911
|
+
const mcpTargetPath = options.mcpTarget ?? config.mcpTargets?.[0] ?? join26(process.cwd(), ".mcp.json");
|
|
5222
5912
|
const result = promoteMcp(entry, serverProfile, mcpTargetPath, { force: options.force });
|
|
5223
5913
|
reject(entry);
|
|
5224
5914
|
logOutcome(`[skill-forge] promoted -> ${result.target} (server "${result.serverName}")`, options.json);
|
|
@@ -5463,7 +6153,7 @@ var TOPICS = [
|
|
|
5463
6153
|
];
|
|
5464
6154
|
function readState() {
|
|
5465
6155
|
const configPath = getConfigPath();
|
|
5466
|
-
const configured =
|
|
6156
|
+
const configured = existsSync20(configPath);
|
|
5467
6157
|
const base = {
|
|
5468
6158
|
configured,
|
|
5469
6159
|
configPath,
|
|
@@ -5697,17 +6387,17 @@ async function status() {
|
|
|
5697
6387
|
// src/evolve.ts
|
|
5698
6388
|
import { createInterface as createInterface5 } from "readline/promises";
|
|
5699
6389
|
import { createHash as createHash3 } from "crypto";
|
|
5700
|
-
import { cpSync as cpSync3, existsSync as
|
|
6390
|
+
import { cpSync as cpSync3, existsSync as existsSync21, lstatSync as lstatSync4, mkdtempSync as mkdtempSync3, readFileSync as readFileSync18, rmSync as rmSync6 } from "fs";
|
|
5701
6391
|
import { tmpdir as tmpdir2 } from "os";
|
|
5702
|
-
import { basename as basename10, dirname as dirname9, join as
|
|
6392
|
+
import { basename as basename10, dirname as dirname9, join as join27, resolve as resolvePath7, sep as sep6 } from "path";
|
|
5703
6393
|
var SKILLOPT_BIN = "skillopt-sleep";
|
|
5704
6394
|
var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
|
|
5705
6395
|
async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
5706
6396
|
const exec = deps.exec ?? defaultExec;
|
|
5707
6397
|
const json = options.json;
|
|
5708
|
-
const resolvedTargetDir =
|
|
5709
|
-
const skillMdPath =
|
|
5710
|
-
if (!
|
|
6398
|
+
const resolvedTargetDir = resolvePath7(targetSkillDir);
|
|
6399
|
+
const skillMdPath = join27(resolvedTargetDir, "SKILL.md");
|
|
6400
|
+
if (!existsSync21(skillMdPath)) {
|
|
5711
6401
|
throw new UserError(`no SKILL.md found at ${skillMdPath} \u2014 evolve requires an existing, installed skill directory`);
|
|
5712
6402
|
}
|
|
5713
6403
|
const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
|
|
@@ -5734,7 +6424,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
5734
6424
|
} else {
|
|
5735
6425
|
notices.push('using the offline "mock" backend \u2014 deterministic, no network calls, no session data leaves this machine.');
|
|
5736
6426
|
}
|
|
5737
|
-
const project =
|
|
6427
|
+
const project = resolvePath7(options.project ?? process.cwd());
|
|
5738
6428
|
const present = await detectSkillopt(exec, project);
|
|
5739
6429
|
if (!present) {
|
|
5740
6430
|
throw new UserError(
|
|
@@ -5765,33 +6455,33 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
5765
6455
|
}
|
|
5766
6456
|
return;
|
|
5767
6457
|
}
|
|
5768
|
-
const stagingRoot =
|
|
5769
|
-
const stagingDir =
|
|
5770
|
-
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot +
|
|
6458
|
+
const stagingRoot = join27(project, ".skillopt-sleep", "staging");
|
|
6459
|
+
const stagingDir = resolvePath7(parsed.staging_dir);
|
|
6460
|
+
if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep6)) {
|
|
5771
6461
|
throw new UserError(
|
|
5772
6462
|
`skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
|
|
5773
6463
|
);
|
|
5774
6464
|
}
|
|
5775
|
-
const proposedSkillMd =
|
|
5776
|
-
const proposedClaudeMd =
|
|
5777
|
-
const reportMdPath =
|
|
5778
|
-
if (!
|
|
6465
|
+
const proposedSkillMd = join27(stagingDir, "proposed_SKILL.md");
|
|
6466
|
+
const proposedClaudeMd = join27(stagingDir, "proposed_CLAUDE.md");
|
|
6467
|
+
const reportMdPath = join27(stagingDir, "report.md");
|
|
6468
|
+
if (!existsSync21(proposedSkillMd)) {
|
|
5779
6469
|
throw new UserError(`skillopt-sleep reported an accepted proposal but ${proposedSkillMd} is missing`);
|
|
5780
6470
|
}
|
|
5781
|
-
if (
|
|
6471
|
+
if (existsSync21(reportMdPath)) {
|
|
5782
6472
|
notices.push(`SkillOpt-Sleep's full report: ${reportMdPath}`);
|
|
5783
6473
|
}
|
|
5784
6474
|
assertNotSymlink(proposedSkillMd, "proposed_SKILL.md");
|
|
5785
6475
|
assertNotSymlink(proposedClaudeMd, "proposed_CLAUDE.md");
|
|
5786
6476
|
const scannedSkillHash = sha256File(proposedSkillMd);
|
|
5787
6477
|
const scannedClaudeHash = sha256File(proposedClaudeMd);
|
|
5788
|
-
const tempDir = mkdtempSync3(
|
|
6478
|
+
const tempDir = mkdtempSync3(join27(tmpdir2(), "skill-forge-evolve-"));
|
|
5789
6479
|
let profile;
|
|
5790
6480
|
let safety;
|
|
5791
6481
|
try {
|
|
5792
|
-
cpSync3(proposedSkillMd,
|
|
5793
|
-
if (
|
|
5794
|
-
cpSync3(proposedClaudeMd,
|
|
6482
|
+
cpSync3(proposedSkillMd, join27(tempDir, "SKILL.md"));
|
|
6483
|
+
if (existsSync21(proposedClaudeMd)) {
|
|
6484
|
+
cpSync3(proposedClaudeMd, join27(tempDir, "CLAUDE.md"));
|
|
5795
6485
|
}
|
|
5796
6486
|
profile = asSkillProfile(profileSkill(tempDir), "for the evolved proposal");
|
|
5797
6487
|
const strictness = loadConfig().strictness;
|
|
@@ -5890,7 +6580,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
|
|
|
5890
6580
|
if (safetyVerdict === "block") process.exitCode = 1;
|
|
5891
6581
|
} else {
|
|
5892
6582
|
logOutcome(`[skill-forge] held \u2014 staging dir left in place -> ${stagingDir}`, json);
|
|
5893
|
-
if (
|
|
6583
|
+
if (existsSync21(reportMdPath)) {
|
|
5894
6584
|
logOutcome(`[skill-forge] see ${reportMdPath} for the full evidence report`, json);
|
|
5895
6585
|
}
|
|
5896
6586
|
}
|
|
@@ -5932,10 +6622,10 @@ function validateRunOutput(raw) {
|
|
|
5932
6622
|
return { accepted: Boolean(obj.accepted), staging_dir: stagingDirRaw ?? null };
|
|
5933
6623
|
}
|
|
5934
6624
|
function readManifest(stagingDir) {
|
|
5935
|
-
const manifestPath =
|
|
6625
|
+
const manifestPath = join27(stagingDir, "manifest.json");
|
|
5936
6626
|
let raw;
|
|
5937
6627
|
try {
|
|
5938
|
-
raw =
|
|
6628
|
+
raw = readFileSync18(manifestPath, "utf-8");
|
|
5939
6629
|
} catch (err) {
|
|
5940
6630
|
throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
|
|
5941
6631
|
}
|
|
@@ -5969,18 +6659,18 @@ function validateManifest(raw, manifestPath) {
|
|
|
5969
6659
|
};
|
|
5970
6660
|
}
|
|
5971
6661
|
function filesIdentical(a, b) {
|
|
5972
|
-
if (!
|
|
5973
|
-
return
|
|
6662
|
+
if (!existsSync21(a) || !existsSync21(b)) return false;
|
|
6663
|
+
return readFileSync18(a).equals(readFileSync18(b));
|
|
5974
6664
|
}
|
|
5975
6665
|
function isAlreadyAdopted(stagingDir, manifest) {
|
|
5976
|
-
const proposedSkillMd =
|
|
5977
|
-
const proposedClaudeMd =
|
|
6666
|
+
const proposedSkillMd = join27(stagingDir, "proposed_SKILL.md");
|
|
6667
|
+
const proposedClaudeMd = join27(stagingDir, "proposed_CLAUDE.md");
|
|
5978
6668
|
let sawComparison = false;
|
|
5979
|
-
if (
|
|
6669
|
+
if (existsSync21(proposedSkillMd)) {
|
|
5980
6670
|
sawComparison = true;
|
|
5981
6671
|
if (!filesIdentical(proposedSkillMd, manifest.live_skill_path)) return false;
|
|
5982
6672
|
}
|
|
5983
|
-
if (
|
|
6673
|
+
if (existsSync21(proposedClaudeMd) && manifest.live_memory_path) {
|
|
5984
6674
|
sawComparison = true;
|
|
5985
6675
|
if (!filesIdentical(proposedClaudeMd, manifest.live_memory_path)) return false;
|
|
5986
6676
|
}
|
|
@@ -5998,8 +6688,8 @@ function assertNotSymlink(path, label2) {
|
|
|
5998
6688
|
}
|
|
5999
6689
|
}
|
|
6000
6690
|
function sha256File(path) {
|
|
6001
|
-
if (!
|
|
6002
|
-
return createHash3("sha256").update(
|
|
6691
|
+
if (!existsSync21(path)) return null;
|
|
6692
|
+
return createHash3("sha256").update(readFileSync18(path)).digest("hex");
|
|
6003
6693
|
}
|
|
6004
6694
|
|
|
6005
6695
|
// src/commands/evolve.ts
|
|
@@ -6008,9 +6698,9 @@ async function evolveCommand(targetSkillDir, options = {}) {
|
|
|
6008
6698
|
}
|
|
6009
6699
|
|
|
6010
6700
|
// src/commands/finding.ts
|
|
6011
|
-
import { existsSync as
|
|
6701
|
+
import { existsSync as existsSync22 } from "fs";
|
|
6012
6702
|
function requireSavedConfig() {
|
|
6013
|
-
if (!
|
|
6703
|
+
if (!existsSync22(getConfigPath())) {
|
|
6014
6704
|
throw new UserError(
|
|
6015
6705
|
"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
6706
|
);
|
|
@@ -6096,7 +6786,7 @@ async function findingRevokeCommand(prefix, options = {}) {
|
|
|
6096
6786
|
async function findingListCommand(options = {}) {
|
|
6097
6787
|
const { records, notices } = readAcceptedFindings();
|
|
6098
6788
|
for (const notice of notices) console.error(`[skill-forge] ${notice}`);
|
|
6099
|
-
const rows = options.stale ? records.filter((r) => !
|
|
6789
|
+
const rows = options.stale ? records.filter((r) => !existsSync22(r.target)) : records;
|
|
6100
6790
|
if (options.json) {
|
|
6101
6791
|
console.log(JSON.stringify(rows, null, 2));
|
|
6102
6792
|
return;
|
|
@@ -6114,24 +6804,24 @@ async function findingListCommand(options = {}) {
|
|
|
6114
6804
|
}
|
|
6115
6805
|
|
|
6116
6806
|
// src/commands/organize.ts
|
|
6117
|
-
import { resolve as
|
|
6807
|
+
import { resolve as resolvePath9 } from "path";
|
|
6118
6808
|
|
|
6119
6809
|
// src/organize.ts
|
|
6120
|
-
import { existsSync as
|
|
6121
|
-
import { basename as basename11, join as
|
|
6810
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19, readdirSync as readdirSync10, realpathSync as realpathSync5, statSync as statSync9 } from "fs";
|
|
6811
|
+
import { basename as basename11, join as join28, resolve as resolvePath8 } from "path";
|
|
6122
6812
|
var PRO_FEATURE_LABEL4 = "set-level organizer reports";
|
|
6123
6813
|
var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
|
|
6124
|
-
var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated"]);
|
|
6814
|
+
var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated", "experimental"]);
|
|
6125
6815
|
function listSubdirs2(dir) {
|
|
6126
6816
|
let entries;
|
|
6127
6817
|
try {
|
|
6128
|
-
entries =
|
|
6818
|
+
entries = readdirSync10(dir, { withFileTypes: true });
|
|
6129
6819
|
} catch {
|
|
6130
6820
|
return [];
|
|
6131
6821
|
}
|
|
6132
6822
|
const out = [];
|
|
6133
6823
|
for (const entry of entries) {
|
|
6134
|
-
const full =
|
|
6824
|
+
const full = join28(dir, entry.name);
|
|
6135
6825
|
if (entry.isDirectory()) {
|
|
6136
6826
|
out.push(full);
|
|
6137
6827
|
continue;
|
|
@@ -6149,14 +6839,14 @@ function findSkillDirsUnderRoot2(root) {
|
|
|
6149
6839
|
const found = /* @__PURE__ */ new Set();
|
|
6150
6840
|
const topDirs = listSubdirs2(root);
|
|
6151
6841
|
for (const dir of topDirs) {
|
|
6152
|
-
if (
|
|
6842
|
+
if (existsSync23(join28(dir, "SKILL.md"))) found.add(dir);
|
|
6153
6843
|
}
|
|
6154
|
-
for (const dir of listSubdirs2(
|
|
6155
|
-
if (
|
|
6844
|
+
for (const dir of listSubdirs2(join28(root, "skills"))) {
|
|
6845
|
+
if (existsSync23(join28(dir, "SKILL.md"))) found.add(dir);
|
|
6156
6846
|
}
|
|
6157
6847
|
for (const topDir of topDirs) {
|
|
6158
|
-
for (const dir of listSubdirs2(
|
|
6159
|
-
if (
|
|
6848
|
+
for (const dir of listSubdirs2(join28(topDir, "skills"))) {
|
|
6849
|
+
if (existsSync23(join28(dir, "SKILL.md"))) found.add(dir);
|
|
6160
6850
|
}
|
|
6161
6851
|
}
|
|
6162
6852
|
return Array.from(found).filter((p) => !p.includes("/.git/"));
|
|
@@ -6164,7 +6854,7 @@ function findSkillDirsUnderRoot2(root) {
|
|
|
6164
6854
|
function collectCandidates(roots) {
|
|
6165
6855
|
const out = [];
|
|
6166
6856
|
for (const root of roots) {
|
|
6167
|
-
const resolvedRoot =
|
|
6857
|
+
const resolvedRoot = resolvePath8(expandHome(root));
|
|
6168
6858
|
if (!isDir(resolvedRoot)) continue;
|
|
6169
6859
|
for (const dirPath of findSkillDirsUnderRoot2(resolvedRoot)) {
|
|
6170
6860
|
out.push({ dirPath, root });
|
|
@@ -6201,57 +6891,85 @@ function extractFrontmatterBlock(text) {
|
|
|
6201
6891
|
if (closeIdx === -1) return null;
|
|
6202
6892
|
return afterOpen.slice(0, closeIdx);
|
|
6203
6893
|
}
|
|
6204
|
-
function
|
|
6205
|
-
const
|
|
6206
|
-
|
|
6207
|
-
|
|
6894
|
+
function indentOf(line) {
|
|
6895
|
+
const m = line.match(/^ */);
|
|
6896
|
+
return m ? m[0].length : 0;
|
|
6897
|
+
}
|
|
6898
|
+
function extractIndentedBlock(lines, key, parentIndent) {
|
|
6899
|
+
const keyRe = new RegExp(`^${" ".repeat(parentIndent)}${key}:\\s*$`);
|
|
6900
|
+
const startIdx = lines.findIndex((l) => keyRe.test(l));
|
|
6901
|
+
if (startIdx === -1) return null;
|
|
6902
|
+
const block = [];
|
|
6903
|
+
for (const line of lines.slice(startIdx + 1)) {
|
|
6904
|
+
if (line.trim() === "") {
|
|
6905
|
+
block.push(line);
|
|
6906
|
+
continue;
|
|
6907
|
+
}
|
|
6908
|
+
if (indentOf(line) <= parentIndent) break;
|
|
6909
|
+
block.push(line);
|
|
6208
6910
|
}
|
|
6911
|
+
return block;
|
|
6912
|
+
}
|
|
6913
|
+
function dedent(lines) {
|
|
6914
|
+
const nonBlank = lines.filter((l) => l.trim() !== "");
|
|
6915
|
+
if (nonBlank.length === 0) return "";
|
|
6916
|
+
const minIndent = Math.min(...nonBlank.map(indentOf));
|
|
6917
|
+
return lines.map((l) => l.trim() === "" ? "" : l.slice(minIndent)).join("\n");
|
|
6918
|
+
}
|
|
6919
|
+
function extractFlatFields(text, keys) {
|
|
6209
6920
|
const fields = {};
|
|
6210
|
-
for (const key of
|
|
6211
|
-
const m =
|
|
6921
|
+
for (const key of keys) {
|
|
6922
|
+
const m = text.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
6212
6923
|
if (m) fields[key] = stripQuotes(m[1].trim());
|
|
6213
6924
|
}
|
|
6925
|
+
return fields;
|
|
6926
|
+
}
|
|
6927
|
+
function parseNestedRhizeMetadata(block) {
|
|
6928
|
+
const lines = block.split(/\r\n|\r|\n/);
|
|
6929
|
+
const metaBlock = extractIndentedBlock(lines, "metadata", 0);
|
|
6930
|
+
if (metaBlock === null) return null;
|
|
6931
|
+
const rhizeLine = metaBlock.find((l) => /^\s*rhize:\s*$/.test(l));
|
|
6932
|
+
if (!rhizeLine) return null;
|
|
6933
|
+
const rhizeBlock = extractIndentedBlock(metaBlock, "rhize", indentOf(rhizeLine));
|
|
6934
|
+
if (rhizeBlock === null) return null;
|
|
6935
|
+
const dedented = dedent(rhizeBlock);
|
|
6936
|
+
const fields = extractFlatFields(dedented, ["tier", "domain", "provenance", "maturity"]);
|
|
6214
6937
|
return {
|
|
6215
|
-
name: fields.name ?? fallbackName,
|
|
6216
6938
|
tier: fields.tier,
|
|
6217
6939
|
domain: fields.domain,
|
|
6218
6940
|
provenance: fields.provenance,
|
|
6219
6941
|
maturity: fields.maturity,
|
|
6220
|
-
consumes: parseYamlList(
|
|
6221
|
-
validFrontmatter: true
|
|
6942
|
+
consumes: parseYamlList(dedented, "consumes")
|
|
6222
6943
|
};
|
|
6223
6944
|
}
|
|
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 };
|
|
6945
|
+
function contributesAnyField(f) {
|
|
6946
|
+
return f.tier !== void 0 || f.domain !== void 0 || f.provenance !== void 0 || f.maturity !== void 0 || Boolean(f.consumes && f.consumes.length > 0);
|
|
6947
|
+
}
|
|
6948
|
+
function parseCapabilityFrontmatter(text, fallbackName) {
|
|
6949
|
+
const block = extractFrontmatterBlock(text);
|
|
6950
|
+
if (block === null) {
|
|
6951
|
+
return { name: fallbackName, consumes: [], validFrontmatter: false, tagSource: "none" };
|
|
6952
|
+
}
|
|
6953
|
+
const fields = extractFlatFields(block, ["name", "tier", "domain", "provenance", "maturity"]);
|
|
6954
|
+
const flatConsumes = parseYamlList(block, "consumes");
|
|
6955
|
+
const nested = parseNestedRhizeMetadata(block);
|
|
6956
|
+
const flatContributed = contributesAnyField({ ...fields, consumes: flatConsumes });
|
|
6957
|
+
const nestedContributed = nested !== null && contributesAnyField(nested);
|
|
6958
|
+
let tagSource;
|
|
6959
|
+
if (flatContributed && nestedContributed) tagSource = "mixed";
|
|
6960
|
+
else if (flatContributed) tagSource = "flat";
|
|
6961
|
+
else if (nestedContributed) tagSource = "metadata.rhize";
|
|
6962
|
+
else tagSource = "none";
|
|
6963
|
+
return {
|
|
6964
|
+
name: fields.name ?? fallbackName,
|
|
6965
|
+
tier: fields.tier ?? nested?.tier,
|
|
6966
|
+
domain: fields.domain ?? nested?.domain,
|
|
6967
|
+
provenance: fields.provenance ?? nested?.provenance,
|
|
6968
|
+
maturity: fields.maturity ?? nested?.maturity,
|
|
6969
|
+
consumes: flatConsumes.length > 0 ? flatConsumes : nested?.consumes ?? [],
|
|
6970
|
+
validFrontmatter: true,
|
|
6971
|
+
tagSource
|
|
6972
|
+
};
|
|
6255
6973
|
}
|
|
6256
6974
|
var WHITE = 0;
|
|
6257
6975
|
var GRAY = 1;
|
|
@@ -6324,17 +7042,17 @@ function buildRegistryEntries(candidates, warnings) {
|
|
|
6324
7042
|
for (const candidate of candidates) {
|
|
6325
7043
|
let realPath;
|
|
6326
7044
|
try {
|
|
6327
|
-
realPath =
|
|
7045
|
+
realPath = realpathSync5(candidate.dirPath);
|
|
6328
7046
|
} catch (err) {
|
|
6329
7047
|
warnings.push(`could not resolve skill directory ${candidate.dirPath}: ${err.message}`);
|
|
6330
7048
|
continue;
|
|
6331
7049
|
}
|
|
6332
7050
|
if (seenRealPaths.has(realPath)) continue;
|
|
6333
7051
|
seenRealPaths.add(realPath);
|
|
6334
|
-
const skillMdPath =
|
|
7052
|
+
const skillMdPath = join28(realPath, "SKILL.md");
|
|
6335
7053
|
let text;
|
|
6336
7054
|
try {
|
|
6337
|
-
text =
|
|
7055
|
+
text = readFileSync19(skillMdPath, "utf-8");
|
|
6338
7056
|
} catch (err) {
|
|
6339
7057
|
warnings.push(`could not read ${skillMdPath}: ${err.message}`);
|
|
6340
7058
|
continue;
|
|
@@ -6357,7 +7075,8 @@ function buildRegistryEntries(candidates, warnings) {
|
|
|
6357
7075
|
consumes: fm.consumes,
|
|
6358
7076
|
provenance: fm.provenance,
|
|
6359
7077
|
maturity: fm.maturity,
|
|
6360
|
-
validFrontmatter: fm.validFrontmatter
|
|
7078
|
+
validFrontmatter: fm.validFrontmatter,
|
|
7079
|
+
tagSource: fm.tagSource
|
|
6361
7080
|
});
|
|
6362
7081
|
}
|
|
6363
7082
|
return entries;
|
|
@@ -6446,7 +7165,7 @@ function buildOrganizeReport(config, options = {}) {
|
|
|
6446
7165
|
|
|
6447
7166
|
// src/commands/organize.ts
|
|
6448
7167
|
function writeReport(outPath, contents) {
|
|
6449
|
-
const resolved =
|
|
7168
|
+
const resolved = resolvePath9(outPath);
|
|
6450
7169
|
writeNewFile(resolved, contents, "--out");
|
|
6451
7170
|
return resolved;
|
|
6452
7171
|
}
|
|
@@ -6739,13 +7458,13 @@ async function find(query, options = {}, deps = {}) {
|
|
|
6739
7458
|
}
|
|
6740
7459
|
|
|
6741
7460
|
// src/commands/watch.ts
|
|
6742
|
-
import { realpathSync as
|
|
6743
|
-
import { join as
|
|
7461
|
+
import { realpathSync as realpathSync6 } from "fs";
|
|
7462
|
+
import { join as join29, resolve as resolvePath10 } from "path";
|
|
6744
7463
|
|
|
6745
7464
|
// src/gate/skillMapDrift.ts
|
|
6746
7465
|
import { createHash as createHash4 } from "crypto";
|
|
6747
|
-
import { readFileSync as
|
|
6748
|
-
import { dirname as dirname10, isAbsolute as
|
|
7466
|
+
import { readFileSync as readFileSync20, existsSync as existsSync24 } from "fs";
|
|
7467
|
+
import { dirname as dirname10, isAbsolute as isAbsolute4, resolve as resolve7 } from "path";
|
|
6749
7468
|
function sha256(text) {
|
|
6750
7469
|
return createHash4("sha256").update(text, "utf-8").digest("hex");
|
|
6751
7470
|
}
|
|
@@ -6778,22 +7497,22 @@ function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
|
|
|
6778
7497
|
}
|
|
6779
7498
|
function resolveLocalPath(path, mapDir) {
|
|
6780
7499
|
const expanded = expandHome(path);
|
|
6781
|
-
if (
|
|
7500
|
+
if (isAbsolute4(expanded)) return resolve7(expanded);
|
|
6782
7501
|
const candidates = [resolve7(expanded)];
|
|
6783
7502
|
if (mapDir) {
|
|
6784
7503
|
candidates.push(resolve7(mapDir, expanded));
|
|
6785
7504
|
candidates.push(resolve7(mapDir, "..", expanded));
|
|
6786
7505
|
}
|
|
6787
7506
|
for (const candidate of candidates) {
|
|
6788
|
-
if (
|
|
7507
|
+
if (existsSync24(candidate)) return candidate;
|
|
6789
7508
|
}
|
|
6790
7509
|
return candidates[0];
|
|
6791
7510
|
}
|
|
6792
7511
|
function defaultReadLocalFile(path, mapDir) {
|
|
6793
7512
|
const resolved = resolveLocalPath(path, mapDir);
|
|
6794
|
-
if (!
|
|
7513
|
+
if (!existsSync24(resolved)) return void 0;
|
|
6795
7514
|
try {
|
|
6796
|
-
return
|
|
7515
|
+
return readFileSync20(resolved, "utf-8");
|
|
6797
7516
|
} catch {
|
|
6798
7517
|
return void 0;
|
|
6799
7518
|
}
|
|
@@ -6834,7 +7553,7 @@ async function computeMapDrift(map, deps = {}) {
|
|
|
6834
7553
|
continue;
|
|
6835
7554
|
}
|
|
6836
7555
|
const localResolved = resolveLocalPath(localNode.path, mapDir);
|
|
6837
|
-
if (!
|
|
7556
|
+
if (!existsSync24(localResolved)) {
|
|
6838
7557
|
rows.push({
|
|
6839
7558
|
skill: skillLabel,
|
|
6840
7559
|
upstream,
|
|
@@ -6940,12 +7659,12 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
6940
7659
|
const warnings = [];
|
|
6941
7660
|
const seenRealPaths = /* @__PURE__ */ new Set();
|
|
6942
7661
|
for (const root of config.skillsRoots) {
|
|
6943
|
-
const resolvedRoot =
|
|
7662
|
+
const resolvedRoot = resolvePath10(expandHome(root));
|
|
6944
7663
|
const exists = isDir(resolvedRoot);
|
|
6945
7664
|
let realPath = resolvedRoot;
|
|
6946
7665
|
if (exists) {
|
|
6947
7666
|
try {
|
|
6948
|
-
realPath =
|
|
7667
|
+
realPath = realpathSync6(resolvedRoot);
|
|
6949
7668
|
} catch {
|
|
6950
7669
|
realPath = resolvedRoot;
|
|
6951
7670
|
}
|
|
@@ -6957,7 +7676,7 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
6957
7676
|
ledger = readProvenanceLedger(resolvedRoot);
|
|
6958
7677
|
} catch (err) {
|
|
6959
7678
|
const message = err instanceof Error ? err.message : String(err);
|
|
6960
|
-
const ledgerPath =
|
|
7679
|
+
const ledgerPath = join29(resolvedRoot, "SOURCES.md");
|
|
6961
7680
|
roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
|
|
6962
7681
|
warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
|
|
6963
7682
|
continue;
|
|
@@ -7067,8 +7786,8 @@ async function watchCommand(options = {}) {
|
|
|
7067
7786
|
}
|
|
7068
7787
|
|
|
7069
7788
|
// src/commands/routine.ts
|
|
7070
|
-
import { existsSync as
|
|
7071
|
-
import { join as
|
|
7789
|
+
import { existsSync as existsSync25, rmSync as rmSync7, statSync as statSync10 } from "fs";
|
|
7790
|
+
import { join as join30 } from "path";
|
|
7072
7791
|
var PRO_FEATURE_LABEL6 = "the scheduled maintenance routine";
|
|
7073
7792
|
var SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
7074
7793
|
function parseFailOnFloor(failOn) {
|
|
@@ -7089,7 +7808,7 @@ function exceedsFloor(audit2, failOn) {
|
|
|
7089
7808
|
}
|
|
7090
7809
|
function resolveReportPath2(override, generatedAt) {
|
|
7091
7810
|
if (override) return override;
|
|
7092
|
-
return
|
|
7811
|
+
return join30(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
|
|
7093
7812
|
}
|
|
7094
7813
|
function runHousekeeping(config, options) {
|
|
7095
7814
|
const actions = [];
|
|
@@ -7098,7 +7817,7 @@ function runHousekeeping(config, options) {
|
|
|
7098
7817
|
const pruneDays = options.pruneDays ?? 30;
|
|
7099
7818
|
const cutoff = Date.now() - pruneDays * 864e5;
|
|
7100
7819
|
for (const id of listQuarantineIds(config.quarantineDir)) {
|
|
7101
|
-
const path =
|
|
7820
|
+
const path = join30(config.quarantineDir, id);
|
|
7102
7821
|
let mtime;
|
|
7103
7822
|
try {
|
|
7104
7823
|
mtime = statSync10(path).mtimeMs;
|
|
@@ -7136,7 +7855,7 @@ function runHousekeeping(config, options) {
|
|
|
7136
7855
|
}
|
|
7137
7856
|
for (const entry of entries) {
|
|
7138
7857
|
if (entry.status !== "pending") continue;
|
|
7139
|
-
if (!entry.installedPath ||
|
|
7858
|
+
if (!entry.installedPath || existsSync25(entry.installedPath)) continue;
|
|
7140
7859
|
if (apply) {
|
|
7141
7860
|
try {
|
|
7142
7861
|
updateQueueEntry(entry.id, { status: "dismissed" });
|
|
@@ -7227,7 +7946,7 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
|
|
|
7227
7946
|
lines.push("");
|
|
7228
7947
|
return lines.join("\n");
|
|
7229
7948
|
}
|
|
7230
|
-
async function routineCommand(options = {}) {
|
|
7949
|
+
async function routineCommand(options = {}, deps = {}) {
|
|
7231
7950
|
const pro = proFeatureStatus(PRO_FEATURE_LABEL6);
|
|
7232
7951
|
const log = options.json ? console.error : console.log;
|
|
7233
7952
|
if (pro.notice) log(`[skill-forge] ${pro.notice}`);
|
|
@@ -7240,10 +7959,21 @@ async function routineCommand(options = {}) {
|
|
|
7240
7959
|
}
|
|
7241
7960
|
const config = loadConfig();
|
|
7242
7961
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7962
|
+
const { pluginRoots, usage, notices: pluginUsageNotices } = resolvePluginAndUsageContext(config, options, deps);
|
|
7243
7963
|
const { records: accepted, notices: storeNotices } = readAcceptedFindings();
|
|
7244
|
-
|
|
7245
|
-
const
|
|
7964
|
+
const notices = [...storeNotices, ...pluginUsageNotices];
|
|
7965
|
+
for (const notice of notices) log(`[skill-forge] ${notice}`);
|
|
7966
|
+
const audit2 = await auditConfiguredSet(config, { accepted, pluginRoots, usage });
|
|
7246
7967
|
const auditState = summarizeAudit(audit2);
|
|
7968
|
+
if (audit2.plugins) {
|
|
7969
|
+
const critical = audit2.plugins.reduce((n, p) => n + p.findingCounts.CRITICAL, 0);
|
|
7970
|
+
const high = audit2.plugins.reduce((n, p) => n + p.findingCounts.HIGH, 0);
|
|
7971
|
+
if (critical + high > 0) {
|
|
7972
|
+
const notice = `plugin-origin findings (${critical} CRITICAL, ${high} HIGH) are excluded from --fail-on and the add/scan advisory \u2014 see the Plugins section for per-plugin review recommendations`;
|
|
7973
|
+
notices.push(notice);
|
|
7974
|
+
log(`[skill-forge] ${notice}`);
|
|
7975
|
+
}
|
|
7976
|
+
}
|
|
7247
7977
|
const degraded = [];
|
|
7248
7978
|
let watch;
|
|
7249
7979
|
if (!options.skipWatch) {
|
|
@@ -7283,9 +8013,10 @@ async function routineCommand(options = {}) {
|
|
|
7283
8013
|
notices: watch.notices
|
|
7284
8014
|
} : void 0,
|
|
7285
8015
|
organize: organize ? { skills: organize.registry.count, roots: organize.registry.skillsRoots.length, notices: organize.notices } : void 0,
|
|
8016
|
+
plugins: audit2.plugins,
|
|
7286
8017
|
pending: { queue: pending.count, quarantine: quarantine2.count },
|
|
7287
8018
|
housekeeping,
|
|
7288
|
-
notices
|
|
8019
|
+
notices,
|
|
7289
8020
|
degraded
|
|
7290
8021
|
};
|
|
7291
8022
|
const failure = degraded.length > 0 ? `${degraded.length} step(s) did not complete: ${degraded.join("; ")}` : exceedsFloor(auditState, options.failOn ?? "none");
|
|
@@ -7307,6 +8038,7 @@ async function routineCommand(options = {}) {
|
|
|
7307
8038
|
console.log(` accepted: ${auditState.acceptedCount ?? 0} (suppressed from counts; see report)`);
|
|
7308
8039
|
if (result.watch) console.log(` drift: ${result.watch.moved} moved of ${result.watch.checked} tracked`);
|
|
7309
8040
|
if (result.organize) console.log(` registry: ${result.organize.skills} skill(s)`);
|
|
8041
|
+
if (result.plugins) console.log(` ${summarizePluginRecommendations(result.plugins)}`);
|
|
7310
8042
|
console.log(` pending: ${result.pending.queue} queue, ${result.pending.quarantine} quarantine`);
|
|
7311
8043
|
for (const action of housekeeping) {
|
|
7312
8044
|
const label2 = action.outcome === "applied" ? "housekeeping" : action.outcome === "failed" ? "FAILED" : "would do";
|
|
@@ -7328,8 +8060,8 @@ async function routineCommand(options = {}) {
|
|
|
7328
8060
|
}
|
|
7329
8061
|
|
|
7330
8062
|
// src/commands/ingest.ts
|
|
7331
|
-
import { realpathSync as
|
|
7332
|
-
import { dirname as dirname12, isAbsolute as
|
|
8063
|
+
import { realpathSync as realpathSync7 } from "fs";
|
|
8064
|
+
import { dirname as dirname12, isAbsolute as isAbsolute5, relative as relative4, resolve as resolvePath11 } from "path";
|
|
7333
8065
|
var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
|
|
7334
8066
|
var VALID_STATUSES = ["ingested", "dismissed"];
|
|
7335
8067
|
function requireProFeature() {
|
|
@@ -7370,19 +8102,19 @@ function printPendingList(pending, json) {
|
|
|
7370
8102
|
}
|
|
7371
8103
|
function realpathOrNull(p) {
|
|
7372
8104
|
try {
|
|
7373
|
-
return
|
|
8105
|
+
return realpathSync7(p);
|
|
7374
8106
|
} catch {
|
|
7375
8107
|
return null;
|
|
7376
8108
|
}
|
|
7377
8109
|
}
|
|
7378
8110
|
function isUnderRoot(candidate, root) {
|
|
7379
|
-
const rel =
|
|
7380
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
8111
|
+
const rel = relative4(root, candidate);
|
|
8112
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
|
|
7381
8113
|
}
|
|
7382
8114
|
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(
|
|
8115
|
+
const quarantineRoot = realpathOrNull(resolvePath11(config.quarantineDir));
|
|
8116
|
+
const skillsRoots = config.skillsRoots.map((r) => realpathOrNull(resolvePath11(r))).filter((r) => r !== null);
|
|
8117
|
+
const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname12(resolvePath11(f)))).filter((r) => r !== null);
|
|
7386
8118
|
const allowedRoots = [quarantineRoot, ...skillsRoots, ...mcpTargetDirs].filter((r) => r !== null);
|
|
7387
8119
|
const valid = [];
|
|
7388
8120
|
const excluded = [];
|
|
@@ -7392,7 +8124,7 @@ function validatePendingEntries(entries, config) {
|
|
|
7392
8124
|
excluded.push({ entry, reason: "no installedPath or quarantinePath recorded on the entry" });
|
|
7393
8125
|
continue;
|
|
7394
8126
|
}
|
|
7395
|
-
const real = realpathOrNull(
|
|
8127
|
+
const real = realpathOrNull(resolvePath11(effectivePath));
|
|
7396
8128
|
if (!real) {
|
|
7397
8129
|
excluded.push({ entry, reason: `path does not exist or could not be resolved: ${effectivePath}` });
|
|
7398
8130
|
continue;
|
|
@@ -7459,15 +8191,15 @@ import {
|
|
|
7459
8191
|
chmodSync as chmodSync3,
|
|
7460
8192
|
lstatSync as lstatSync6,
|
|
7461
8193
|
mkdirSync as mkdirSync9,
|
|
7462
|
-
readFileSync as
|
|
8194
|
+
readFileSync as readFileSync24,
|
|
7463
8195
|
readSync as readSync3,
|
|
7464
8196
|
writeFileSync as writeFileSync8
|
|
7465
8197
|
} from "fs";
|
|
7466
|
-
import { dirname as dirname13, isAbsolute as
|
|
8198
|
+
import { dirname as dirname13, isAbsolute as isAbsolute7, join as join35, relative as relative6, resolve as resolvePath12 } from "path";
|
|
7467
8199
|
|
|
7468
8200
|
// src/insights/inventory.ts
|
|
7469
|
-
import { readFileSync as
|
|
7470
|
-
import { join as
|
|
8201
|
+
import { readFileSync as readFileSync21 } from "fs";
|
|
8202
|
+
import { join as join31 } from "path";
|
|
7471
8203
|
|
|
7472
8204
|
// src/insights/models.ts
|
|
7473
8205
|
import { createHash as createHash5 } from "crypto";
|
|
@@ -7581,7 +8313,7 @@ function declaredMapCoverage(path, claim) {
|
|
|
7581
8313
|
}
|
|
7582
8314
|
function fingerprintInstalledSkill(path) {
|
|
7583
8315
|
try {
|
|
7584
|
-
return { fingerprint: stableHash(
|
|
8316
|
+
return { fingerprint: stableHash(readFileSync21(join31(path, "SKILL.md"), "utf-8")) };
|
|
7585
8317
|
} catch (error) {
|
|
7586
8318
|
return { reason: `could not fingerprint SKILL.md: ${error.message}` };
|
|
7587
8319
|
}
|
|
@@ -7731,12 +8463,12 @@ function buildInsightInventory(config, options = {}) {
|
|
|
7731
8463
|
}
|
|
7732
8464
|
|
|
7733
8465
|
// src/insights/jira.ts
|
|
7734
|
-
import { existsSync as
|
|
7735
|
-
import { join as
|
|
8466
|
+
import { existsSync as existsSync27, readFileSync as readFileSync23 } from "fs";
|
|
8467
|
+
import { join as join34 } from "path";
|
|
7736
8468
|
|
|
7737
8469
|
// src/insights/store.ts
|
|
7738
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
7739
|
-
import { join as
|
|
8470
|
+
import { chmodSync as chmodSync2, existsSync as existsSync26, mkdirSync as mkdirSync8, readFileSync as readFileSync22 } from "fs";
|
|
8471
|
+
import { join as join33 } from "path";
|
|
7740
8472
|
|
|
7741
8473
|
// src/insights/sources.ts
|
|
7742
8474
|
import { createHash as createHash6 } from "crypto";
|
|
@@ -7747,11 +8479,11 @@ import {
|
|
|
7747
8479
|
lstatSync as lstatSync5,
|
|
7748
8480
|
openSync as openSync3,
|
|
7749
8481
|
readSync as readSync2,
|
|
7750
|
-
readdirSync as
|
|
7751
|
-
realpathSync as
|
|
8482
|
+
readdirSync as readdirSync11,
|
|
8483
|
+
realpathSync as realpathSync8,
|
|
7752
8484
|
statSync as statSync11
|
|
7753
8485
|
} from "fs";
|
|
7754
|
-
import { basename as basename12, extname as
|
|
8486
|
+
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
8487
|
var DEFAULT_MAX_SOURCE_FILE_BYTES = 5 * 1024 * 1024;
|
|
7756
8488
|
var DEFAULT_MAX_SOURCE_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
7757
8489
|
var SKIP_DIRS4 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "__pycache__", ".venv", "venv"]);
|
|
@@ -7821,9 +8553,9 @@ function canonicalPublicUrl(raw, label2) {
|
|
|
7821
8553
|
url.hash = "";
|
|
7822
8554
|
return url.toString();
|
|
7823
8555
|
}
|
|
7824
|
-
function
|
|
7825
|
-
const rel =
|
|
7826
|
-
return rel === "" || !rel.startsWith(`..${
|
|
8556
|
+
function isInside3(child, root) {
|
|
8557
|
+
const rel = relative5(root, child);
|
|
8558
|
+
return rel === "" || !rel.startsWith(`..${sep7}`) && rel !== ".." && !isAbsolute6(rel);
|
|
7827
8559
|
}
|
|
7828
8560
|
function isHiddenName(name) {
|
|
7829
8561
|
return name.startsWith(".") && name !== "." && name !== "..";
|
|
@@ -7832,7 +8564,7 @@ function hasCredentialShapedName(name) {
|
|
|
7832
8564
|
return name === ".env" || name.startsWith(".env.") || CREDENTIAL_NAME.test(name);
|
|
7833
8565
|
}
|
|
7834
8566
|
function mediaTypeFor(path, declared) {
|
|
7835
|
-
return declared ?? SUPPORTED_MEDIA_BY_EXTENSION[
|
|
8567
|
+
return declared ?? SUPPORTED_MEDIA_BY_EXTENSION[extname8(path).toLowerCase()];
|
|
7836
8568
|
}
|
|
7837
8569
|
function suspiciousInstructionFlags(bytes, mediaType) {
|
|
7838
8570
|
if (!mediaType.startsWith("text/")) return [];
|
|
@@ -7887,7 +8619,7 @@ function observeFile(path, relativePath, declaredMediaType, maxFileBytes, remain
|
|
|
7887
8619
|
if (!bytes || !descriptorStat) throw new UserError(`source file could not be observed: ${relativePath}`);
|
|
7888
8620
|
let realPath;
|
|
7889
8621
|
try {
|
|
7890
|
-
realPath =
|
|
8622
|
+
realPath = realpathSync8(path);
|
|
7891
8623
|
const pathStat = statSync11(realPath);
|
|
7892
8624
|
if (pathStat.dev !== descriptorStat.dev || pathStat.ino !== descriptorStat.ino) {
|
|
7893
8625
|
throw new Error("path identity changed");
|
|
@@ -8069,7 +8801,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8069
8801
|
}
|
|
8070
8802
|
if (lexicalStat.isSymbolicLink()) throw new UserError(`source directory must not be a symlink: ${resolved}`);
|
|
8071
8803
|
if (!lexicalStat.isDirectory()) throw new UserError(`source directory is not a directory: ${resolved}`);
|
|
8072
|
-
const realRoot =
|
|
8804
|
+
const realRoot = realpathSync8(resolved);
|
|
8073
8805
|
const observations = [];
|
|
8074
8806
|
const skipped = [];
|
|
8075
8807
|
const seenFiles = /* @__PURE__ */ new Set();
|
|
@@ -8078,12 +8810,12 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8078
8810
|
const walk2 = (dir, logicalDir) => {
|
|
8079
8811
|
let entries;
|
|
8080
8812
|
try {
|
|
8081
|
-
entries =
|
|
8813
|
+
entries = readdirSync11(dir, { withFileTypes: true }).sort((a, b) => compareStrings2(a.name, b.name));
|
|
8082
8814
|
} catch (err) {
|
|
8083
8815
|
throw new UserError(`unable to enumerate source directory ${dir}: ${err.message}`);
|
|
8084
8816
|
}
|
|
8085
8817
|
for (const entry of entries) {
|
|
8086
|
-
const full =
|
|
8818
|
+
const full = join32(dir, entry.name);
|
|
8087
8819
|
const logicalPath = logicalDir ? `${logicalDir}/${entry.name}` : entry.name;
|
|
8088
8820
|
if (entry.isDirectory() && SKIP_DIRS4.has(entry.name)) {
|
|
8089
8821
|
skipped.push({ path: logicalPath, reason: "ignored-directory" });
|
|
@@ -8100,11 +8832,11 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8100
8832
|
if (entry.isSymbolicLink()) {
|
|
8101
8833
|
let realTarget;
|
|
8102
8834
|
try {
|
|
8103
|
-
realTarget =
|
|
8835
|
+
realTarget = realpathSync8(full);
|
|
8104
8836
|
} catch {
|
|
8105
8837
|
throw new UserError(`source directory contains an unreadable symlink: ${logicalPath}`);
|
|
8106
8838
|
}
|
|
8107
|
-
if (!
|
|
8839
|
+
if (!isInside3(realTarget, realRoot)) {
|
|
8108
8840
|
throw new UserError(`source directory symlink escapes its root: ${logicalPath}`);
|
|
8109
8841
|
}
|
|
8110
8842
|
const targetStat = statSync11(realTarget);
|
|
@@ -8119,7 +8851,7 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8119
8851
|
continue;
|
|
8120
8852
|
}
|
|
8121
8853
|
} else if (entry.isDirectory()) {
|
|
8122
|
-
const realDir =
|
|
8854
|
+
const realDir = realpathSync8(full);
|
|
8123
8855
|
if (seenDirs.has(realDir)) continue;
|
|
8124
8856
|
seenDirs.add(realDir);
|
|
8125
8857
|
walk2(full, logicalPath);
|
|
@@ -8128,8 +8860,8 @@ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
|
|
|
8128
8860
|
skipped.push({ path: logicalPath, reason: "unsupported-media-type" });
|
|
8129
8861
|
continue;
|
|
8130
8862
|
}
|
|
8131
|
-
const realFile =
|
|
8132
|
-
if (!
|
|
8863
|
+
const realFile = realpathSync8(full);
|
|
8864
|
+
if (!isInside3(realFile, realRoot)) {
|
|
8133
8865
|
throw new UserError(`source directory entry escaped its root while being observed: ${logicalPath}`);
|
|
8134
8866
|
}
|
|
8135
8867
|
if (seenFiles.has(realFile)) {
|
|
@@ -8235,17 +8967,17 @@ function normalizeSources(inputs, options = {}) {
|
|
|
8235
8967
|
// src/insights/store.ts
|
|
8236
8968
|
var STUDY_ID = /^STUDY-[0-9a-f]{16}$/;
|
|
8237
8969
|
function getInsightsDir() {
|
|
8238
|
-
return
|
|
8970
|
+
return join33(getHomeDir(), "insights");
|
|
8239
8971
|
}
|
|
8240
8972
|
function getStudyDir(studyId) {
|
|
8241
8973
|
assertStudyId(studyId);
|
|
8242
|
-
return
|
|
8974
|
+
return join33(getInsightsDir(), studyId);
|
|
8243
8975
|
}
|
|
8244
8976
|
function getStudyPath(studyId) {
|
|
8245
|
-
return
|
|
8977
|
+
return join33(getStudyDir(studyId), "study.json");
|
|
8246
8978
|
}
|
|
8247
8979
|
function getStudyEventsPath(studyId) {
|
|
8248
|
-
return
|
|
8980
|
+
return join33(getStudyDir(studyId), "events.jsonl");
|
|
8249
8981
|
}
|
|
8250
8982
|
function createStudyId(sourceInventoryHash, createdAt) {
|
|
8251
8983
|
return stableId("STUDY", { sourceInventoryHash, createdAt });
|
|
@@ -8376,7 +9108,7 @@ function validateStudyShape(value, path) {
|
|
|
8376
9108
|
function parseStudy(path) {
|
|
8377
9109
|
let raw;
|
|
8378
9110
|
try {
|
|
8379
|
-
raw =
|
|
9111
|
+
raw = readFileSync22(path, "utf-8");
|
|
8380
9112
|
} catch (err) {
|
|
8381
9113
|
throw new UserError(`unable to read insight study at ${path}: ${err.message}`);
|
|
8382
9114
|
}
|
|
@@ -8410,7 +9142,7 @@ function createInsightStudy(input, options = {}) {
|
|
|
8410
9142
|
const studyId = options.studyId ?? createStudyId(input.sourceInventoryHash, createdAt);
|
|
8411
9143
|
assertStudyId(studyId);
|
|
8412
9144
|
const path = getStudyPath(studyId);
|
|
8413
|
-
if (
|
|
9145
|
+
if (existsSync26(path)) throw new UserError(`insight study already exists: ${studyId}`);
|
|
8414
9146
|
const study = {
|
|
8415
9147
|
schemaVersion: INSIGHT_SCHEMA_VERSION,
|
|
8416
9148
|
studyId,
|
|
@@ -8431,7 +9163,7 @@ function createInsightStudy(input, options = {}) {
|
|
|
8431
9163
|
}
|
|
8432
9164
|
function readInsightStudy(studyId) {
|
|
8433
9165
|
const path = getStudyPath(studyId);
|
|
8434
|
-
if (!
|
|
9166
|
+
if (!existsSync26(path)) throw new UserError(`no insight study with id: ${studyId}`);
|
|
8435
9167
|
const study = parseStudy(path);
|
|
8436
9168
|
if (study.studyId !== studyId) {
|
|
8437
9169
|
throw new UserError(`malformed insight study at ${path}: stored studyId does not match its directory`);
|
|
@@ -8441,7 +9173,7 @@ function readInsightStudy(studyId) {
|
|
|
8441
9173
|
function writeInsightStudy(study, now = /* @__PURE__ */ new Date()) {
|
|
8442
9174
|
assertStudyId(study.studyId);
|
|
8443
9175
|
const path = getStudyPath(study.studyId);
|
|
8444
|
-
if (!
|
|
9176
|
+
if (!existsSync26(path)) throw new UserError(`no insight study with id: ${study.studyId}`);
|
|
8445
9177
|
const existing = parseStudy(path);
|
|
8446
9178
|
let next = { ...study, updatedAt: now.toISOString() };
|
|
8447
9179
|
if (next.status === "dismissed" || next.status === "archived") {
|
|
@@ -9178,7 +9910,7 @@ function buildJiraManifest(study, projectKey, options = {}) {
|
|
|
9178
9910
|
return manifest;
|
|
9179
9911
|
}
|
|
9180
9912
|
function getJiraReceiptsPath(studyId) {
|
|
9181
|
-
return
|
|
9913
|
+
return join34(getStudyDir(studyId), "jira-receipts.jsonl");
|
|
9182
9914
|
}
|
|
9183
9915
|
function assertCurrentJiraManifest(study, value) {
|
|
9184
9916
|
if (study.status !== "jira-manifested" || !study.jira) {
|
|
@@ -9310,10 +10042,10 @@ function appendJiraReceipt(studyId, manifest, input) {
|
|
|
9310
10042
|
function readJiraReceipts(studyId, manifest) {
|
|
9311
10043
|
const currentManifest = assertCurrentJiraManifest(readInsightStudy(studyId), manifest);
|
|
9312
10044
|
const path = getJiraReceiptsPath(studyId);
|
|
9313
|
-
if (!
|
|
10045
|
+
if (!existsSync27(path)) return [];
|
|
9314
10046
|
let text;
|
|
9315
10047
|
try {
|
|
9316
|
-
text =
|
|
10048
|
+
text = readFileSync23(path, "utf-8");
|
|
9317
10049
|
} catch (error) {
|
|
9318
10050
|
throw new UserError(`unable to read Jira receipts at ${path}: ${error.message}`);
|
|
9319
10051
|
}
|
|
@@ -9452,10 +10184,10 @@ function nonEmptyString(value, label2) {
|
|
|
9452
10184
|
return value;
|
|
9453
10185
|
}
|
|
9454
10186
|
function parseJsonFile(path, label2) {
|
|
9455
|
-
const resolved =
|
|
10187
|
+
const resolved = resolvePath12(path);
|
|
9456
10188
|
let raw;
|
|
9457
10189
|
try {
|
|
9458
|
-
raw =
|
|
10190
|
+
raw = readFileSync24(resolved, "utf-8");
|
|
9459
10191
|
} catch (error) {
|
|
9460
10192
|
throw new UserError(`unable to read ${label2} at ${resolved}: ${error.message}`);
|
|
9461
10193
|
}
|
|
@@ -9510,7 +10242,7 @@ function assertCommonDescriptorFields(value, label2) {
|
|
|
9510
10242
|
}
|
|
9511
10243
|
function pathRelativeTo(baseDir, raw, label2) {
|
|
9512
10244
|
const path = nonEmptyString(raw, label2);
|
|
9513
|
-
return
|
|
10245
|
+
return isAbsolute7(path) ? path : resolvePath12(baseDir, path);
|
|
9514
10246
|
}
|
|
9515
10247
|
function parseManifestDescriptor(value, index, baseDir) {
|
|
9516
10248
|
const label2 = `source manifest entry ${index + 1}`;
|
|
@@ -9548,7 +10280,7 @@ function parseManifestDescriptor(value, index, baseDir) {
|
|
|
9548
10280
|
throw new UserError(`${label2}.type must be url|file|directory|connector|stdin`);
|
|
9549
10281
|
}
|
|
9550
10282
|
function readManifest2(path) {
|
|
9551
|
-
const resolved =
|
|
10283
|
+
const resolved = resolvePath12(path);
|
|
9552
10284
|
const parsed = parseJsonFile(resolved, "source manifest");
|
|
9553
10285
|
let values;
|
|
9554
10286
|
if (Array.isArray(parsed)) {
|
|
@@ -9581,7 +10313,7 @@ function positionalSource(value) {
|
|
|
9581
10313
|
if (process.stdin.isTTY) throw new UserError(`stdin source "-" requires piped input`);
|
|
9582
10314
|
return { type: "stdin", content: readBoundedStdin() };
|
|
9583
10315
|
}
|
|
9584
|
-
const resolved =
|
|
10316
|
+
const resolved = resolvePath12(value);
|
|
9585
10317
|
let stat;
|
|
9586
10318
|
try {
|
|
9587
10319
|
stat = lstatSync6(resolved);
|
|
@@ -9600,15 +10332,15 @@ function collectSourceInputs(sourceArgs, options) {
|
|
|
9600
10332
|
return inputs;
|
|
9601
10333
|
}
|
|
9602
10334
|
function contextPath(studyId) {
|
|
9603
|
-
return
|
|
10335
|
+
return join35(getStudyDir(studyId), CONTEXT_FILENAME);
|
|
9604
10336
|
}
|
|
9605
10337
|
function inventoryPath(studyId) {
|
|
9606
|
-
return
|
|
10338
|
+
return join35(getStudyDir(studyId), INVENTORY_FILENAME);
|
|
9607
10339
|
}
|
|
9608
10340
|
function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory) {
|
|
9609
10341
|
const context = {
|
|
9610
10342
|
schemaVersion: 1,
|
|
9611
|
-
skillMapPath: skillMapPath ?
|
|
10343
|
+
skillMapPath: skillMapPath ? resolvePath12(skillMapPath) : void 0,
|
|
9612
10344
|
skillMapCoverage
|
|
9613
10345
|
};
|
|
9614
10346
|
writeJsonAtomic(contextPath(studyId), context);
|
|
@@ -9617,7 +10349,7 @@ function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory)
|
|
|
9617
10349
|
function readCommandContext(studyId) {
|
|
9618
10350
|
const path = contextPath(studyId);
|
|
9619
10351
|
const parsed = parseJsonFile(path, "insight command context");
|
|
9620
|
-
if (!isRecord5(parsed) || parsed.schemaVersion !== 1 || parsed.skillMapPath !== void 0 && (typeof parsed.skillMapPath !== "string" || !
|
|
10352
|
+
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
10353
|
throw new UserError(`malformed insight command context at ${path}`);
|
|
9622
10354
|
}
|
|
9623
10355
|
return parsed;
|
|
@@ -9640,19 +10372,19 @@ function readCurrentStudy(studyId) {
|
|
|
9640
10372
|
return { study, inventory: currentInventory(study, config), config };
|
|
9641
10373
|
}
|
|
9642
10374
|
function retainedPath(root, relativePath) {
|
|
9643
|
-
const candidate =
|
|
9644
|
-
const rel =
|
|
9645
|
-
if (rel.startsWith("..") ||
|
|
10375
|
+
const candidate = resolvePath12(root, relativePath);
|
|
10376
|
+
const rel = relative6(root, candidate);
|
|
10377
|
+
if (rel.startsWith("..") || isAbsolute7(rel)) throw new UserError(`refusing retained source path outside the study`);
|
|
9646
10378
|
return candidate;
|
|
9647
10379
|
}
|
|
9648
10380
|
function retainObservedSources(study, observedFiles) {
|
|
9649
10381
|
if (observedFiles.length === 0) return study;
|
|
9650
|
-
const root =
|
|
10382
|
+
const root = join35(getStudyDir(study.studyId), "retained-sources");
|
|
9651
10383
|
mkdirSync9(root, { recursive: true, mode: 448 });
|
|
9652
10384
|
chmodSync3(root, 448);
|
|
9653
10385
|
const retainedSourceIds = /* @__PURE__ */ new Set();
|
|
9654
10386
|
for (const file of observedFiles) {
|
|
9655
|
-
const sourceRoot =
|
|
10387
|
+
const sourceRoot = join35(root, file.sourceId);
|
|
9656
10388
|
mkdirSync9(sourceRoot, { recursive: true, mode: 448 });
|
|
9657
10389
|
chmodSync3(sourceRoot, 448);
|
|
9658
10390
|
const destination = retainedPath(sourceRoot, file.relativePath);
|
|
@@ -9733,7 +10465,7 @@ async function insightAnalyze(sourceArgs = [], options = {}) {
|
|
|
9733
10465
|
requireProFeature2();
|
|
9734
10466
|
const config = loadConfig();
|
|
9735
10467
|
const inputs = collectSourceInputs(sourceArgs, options);
|
|
9736
|
-
const skillMapPath = options.skillMap ?
|
|
10468
|
+
const skillMapPath = options.skillMap ? resolvePath12(options.skillMap) : void 0;
|
|
9737
10469
|
if (options.skillMapCoverage !== void 0 && skillMapPath === void 0) {
|
|
9738
10470
|
throw new UserError("--skill-map-coverage requires --skill-map");
|
|
9739
10471
|
}
|
|
@@ -9890,7 +10622,7 @@ async function insightJira(studyId, options) {
|
|
|
9890
10622
|
throw new UserError(`study ${studyId} cannot generate a Jira manifest: ${validation.blockers.map((issue) => issue.message).join("; ")}`);
|
|
9891
10623
|
}
|
|
9892
10624
|
if (localMode) {
|
|
9893
|
-
const privatePath2 =
|
|
10625
|
+
const privatePath2 = join35(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
9894
10626
|
const manifest2 = assertCurrentJiraManifest(study, parseJsonFile(privatePath2, "stored Jira manifest"));
|
|
9895
10627
|
const receipt = options.receipt === void 0 ? void 0 : appendJiraReceipt(studyId, manifest2, parseJiraReceiptInput(parseJsonFile(options.receipt, "Jira receipt import")));
|
|
9896
10628
|
const resume = buildJiraResumeState(manifest2, readJiraReceipts(studyId, manifest2));
|
|
@@ -9902,10 +10634,10 @@ async function insightJira(studyId, options) {
|
|
|
9902
10634
|
return;
|
|
9903
10635
|
}
|
|
9904
10636
|
const manifest = buildJiraManifest(study, projectKey);
|
|
9905
|
-
const privatePath =
|
|
10637
|
+
const privatePath = join35(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
|
|
9906
10638
|
writeJsonAtomic(privatePath, manifest);
|
|
9907
10639
|
if (options.out) {
|
|
9908
|
-
writeNewFile(
|
|
10640
|
+
writeNewFile(resolvePath12(options.out), `${JSON.stringify(manifest, null, 2)}
|
|
9909
10641
|
`, "--out");
|
|
9910
10642
|
}
|
|
9911
10643
|
const generatedAt = manifest.generatedAt;
|
|
@@ -9923,7 +10655,7 @@ async function insightJira(studyId, options) {
|
|
|
9923
10655
|
emit(options.json, manifest, () => {
|
|
9924
10656
|
console.log(`[skill-forge] wrote Jira manifest for ${next.studyId}`);
|
|
9925
10657
|
console.log(` manifest: ${privatePath}`);
|
|
9926
|
-
if (options.out) console.log(` export: ${
|
|
10658
|
+
if (options.out) console.log(` export: ${resolvePath12(options.out)}`);
|
|
9927
10659
|
console.log(" no Jira issues were created.");
|
|
9928
10660
|
});
|
|
9929
10661
|
if (options.handoff) {
|
|
@@ -9974,13 +10706,13 @@ async function insightClose(studyId, options) {
|
|
|
9974
10706
|
|
|
9975
10707
|
// src/commands/refine.ts
|
|
9976
10708
|
import { createHash as createHash8, randomUUID as randomUUID5 } from "crypto";
|
|
9977
|
-
import { chmodSync as chmodSync4, existsSync as
|
|
9978
|
-
import { dirname as dirname15, isAbsolute as
|
|
10709
|
+
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";
|
|
10710
|
+
import { dirname as dirname15, isAbsolute as isAbsolute9, join as join38, relative as relative8, resolve as resolvePath15, sep as sep8 } from "path";
|
|
9979
10711
|
import { createInterface as createInterface7 } from "readline/promises";
|
|
9980
10712
|
|
|
9981
10713
|
// src/refine/context.ts
|
|
9982
|
-
import { existsSync as
|
|
9983
|
-
import { isAbsolute as
|
|
10714
|
+
import { existsSync as existsSync29, realpathSync as realpathSync10, statSync as statSync13 } from "fs";
|
|
10715
|
+
import { isAbsolute as isAbsolute8, join as join37, relative as relative7, resolve as resolvePath14 } from "path";
|
|
9984
10716
|
|
|
9985
10717
|
// src/refine/models.ts
|
|
9986
10718
|
import { randomBytes } from "crypto";
|
|
@@ -10050,26 +10782,26 @@ function contentSimilarity(a, b) {
|
|
|
10050
10782
|
|
|
10051
10783
|
// src/refine/store.ts
|
|
10052
10784
|
import { createHash as createHash7 } from "crypto";
|
|
10053
|
-
import { existsSync as
|
|
10054
|
-
import { join as
|
|
10785
|
+
import { existsSync as existsSync28, readFileSync as readFileSync25, statSync as statSync12, realpathSync as realpathSync9 } from "fs";
|
|
10786
|
+
import { join as join36, resolve as resolvePath13 } from "path";
|
|
10055
10787
|
var SIMILARITY_JOIN_THRESHOLD = 0.7;
|
|
10056
10788
|
var READY_THRESHOLD = 2;
|
|
10057
10789
|
function getRefinementsDir() {
|
|
10058
|
-
return
|
|
10790
|
+
return join36(getHomeDir(), "refinements");
|
|
10059
10791
|
}
|
|
10060
10792
|
function getRefinementsPath() {
|
|
10061
|
-
return
|
|
10793
|
+
return join36(getRefinementsDir(), "refinements.json");
|
|
10062
10794
|
}
|
|
10063
10795
|
function getPatternsPath() {
|
|
10064
|
-
return
|
|
10796
|
+
return join36(getRefinementsDir(), "patterns.json");
|
|
10065
10797
|
}
|
|
10066
10798
|
function getBackupsDir() {
|
|
10067
|
-
return
|
|
10799
|
+
return join36(getRefinementsDir(), "backups");
|
|
10068
10800
|
}
|
|
10069
10801
|
function readJsonFile(path, label2) {
|
|
10070
10802
|
let raw;
|
|
10071
10803
|
try {
|
|
10072
|
-
raw =
|
|
10804
|
+
raw = readFileSync25(path, "utf-8");
|
|
10073
10805
|
} catch (err) {
|
|
10074
10806
|
throw new UserError(`unable to read ${label2} at ${path}: ${err.message}`);
|
|
10075
10807
|
}
|
|
@@ -10080,7 +10812,7 @@ function readJsonFile(path, label2) {
|
|
|
10080
10812
|
}
|
|
10081
10813
|
}
|
|
10082
10814
|
function readStore(path, label2, validateRecord) {
|
|
10083
|
-
if (!
|
|
10815
|
+
if (!existsSync28(path)) {
|
|
10084
10816
|
return { schemaVersion: 1, records: [] };
|
|
10085
10817
|
}
|
|
10086
10818
|
const parsed = readJsonFile(path, label2);
|
|
@@ -10170,9 +10902,9 @@ function writePatternsStore(store) {
|
|
|
10170
10902
|
writeJsonAtomic(getPatternsPath(), store);
|
|
10171
10903
|
}
|
|
10172
10904
|
function resolveProjectRealpath(projectRoot) {
|
|
10173
|
-
const resolved =
|
|
10905
|
+
const resolved = resolvePath13(projectRoot);
|
|
10174
10906
|
try {
|
|
10175
|
-
return
|
|
10907
|
+
return realpathSync9(resolved);
|
|
10176
10908
|
} catch {
|
|
10177
10909
|
return resolved;
|
|
10178
10910
|
}
|
|
@@ -10304,16 +11036,16 @@ function createBackup(filePaths, deps = {}) {
|
|
|
10304
11036
|
const randomHex = deps.randomHex ?? defaultRandomHex;
|
|
10305
11037
|
const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
|
|
10306
11038
|
const files = filePaths.map((path) => {
|
|
10307
|
-
if (!
|
|
11039
|
+
if (!existsSync28(path)) {
|
|
10308
11040
|
return { path, existed: false, sha256: null, mode: null, contentBase64: null };
|
|
10309
11041
|
}
|
|
10310
|
-
const bytes =
|
|
11042
|
+
const bytes = readFileSync25(path);
|
|
10311
11043
|
const mode = statSync12(path).mode & 511;
|
|
10312
11044
|
const sha2562 = createHash7("sha256").update(bytes).digest("hex");
|
|
10313
11045
|
return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
|
|
10314
11046
|
});
|
|
10315
11047
|
const manifest = { id, createdAt: now.toISOString(), files };
|
|
10316
|
-
writeJsonAtomic(
|
|
11048
|
+
writeJsonAtomic(join36(getBackupsDir(), id, "manifest.json"), manifest);
|
|
10317
11049
|
return manifest;
|
|
10318
11050
|
}
|
|
10319
11051
|
function validateBackupManifestShape(value, path) {
|
|
@@ -10362,8 +11094,8 @@ function readBackupManifest(id) {
|
|
|
10362
11094
|
if (!isValidBackupId(id)) {
|
|
10363
11095
|
throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
|
|
10364
11096
|
}
|
|
10365
|
-
const manifestPath =
|
|
10366
|
-
if (!
|
|
11097
|
+
const manifestPath = join36(getBackupsDir(), id, "manifest.json");
|
|
11098
|
+
if (!existsSync28(manifestPath)) {
|
|
10367
11099
|
throw new UserError(`no backup with id: ${id}`);
|
|
10368
11100
|
}
|
|
10369
11101
|
const parsed = readJsonFile(manifestPath, "backup manifest");
|
|
@@ -10373,7 +11105,7 @@ function readBackupManifest(id) {
|
|
|
10373
11105
|
|
|
10374
11106
|
// src/refine/context.ts
|
|
10375
11107
|
function resolveConfiguredRoot2(root) {
|
|
10376
|
-
return
|
|
11108
|
+
return resolvePath14(expandHome(root));
|
|
10377
11109
|
}
|
|
10378
11110
|
function assertPlainSkillName(name) {
|
|
10379
11111
|
if (!name || name.trim().length === 0) {
|
|
@@ -10385,32 +11117,32 @@ function assertPlainSkillName(name) {
|
|
|
10385
11117
|
}
|
|
10386
11118
|
function realpathOrNull2(p) {
|
|
10387
11119
|
try {
|
|
10388
|
-
return
|
|
11120
|
+
return realpathSync10(p);
|
|
10389
11121
|
} catch {
|
|
10390
11122
|
return null;
|
|
10391
11123
|
}
|
|
10392
11124
|
}
|
|
10393
11125
|
function isUnderRoot2(candidate, root) {
|
|
10394
|
-
const rel =
|
|
10395
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
11126
|
+
const rel = relative7(root, candidate);
|
|
11127
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute8(rel);
|
|
10396
11128
|
}
|
|
10397
11129
|
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
10398
11130
|
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:
|
|
11131
|
+
const resolvedCwd = resolvePath14(cwd);
|
|
11132
|
+
const localDir = join37(resolvedCwd, ".claude", "skills", skill);
|
|
11133
|
+
const sharedDir = join37(resolvedCwd, "skills", skill);
|
|
11134
|
+
const local = { scope: "local", dir: localDir, exists: existsSync29(localDir) };
|
|
11135
|
+
const shared = { scope: "shared", dir: sharedDir, exists: existsSync29(sharedDir) };
|
|
10404
11136
|
const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
|
|
10405
11137
|
return { local, shared, user };
|
|
10406
11138
|
}
|
|
10407
11139
|
function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
|
|
10408
11140
|
if (userRootFlag) {
|
|
10409
|
-
const root =
|
|
10410
|
-
return { resolved: true, dir:
|
|
11141
|
+
const root = resolvePath14(userRootFlag);
|
|
11142
|
+
return { resolved: true, dir: join37(root, skill), root: userRootFlag, reason: "flag" };
|
|
10411
11143
|
}
|
|
10412
11144
|
const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
|
|
10413
|
-
const candidates = config.skillsRoots.map((root) => ({ root, dir:
|
|
11145
|
+
const candidates = config.skillsRoots.map((root) => ({ root, dir: join37(resolveConfiguredRoot2(root), skill) }));
|
|
10414
11146
|
for (const candidate of candidates) {
|
|
10415
11147
|
const skillReal = realpathOrNull2(candidate.dir);
|
|
10416
11148
|
if (skillReal) {
|
|
@@ -10445,12 +11177,12 @@ function inventoryScopeDir(scope, dir) {
|
|
|
10445
11177
|
scope,
|
|
10446
11178
|
dir,
|
|
10447
11179
|
dirExists: isDirectory(dir),
|
|
10448
|
-
skillMd: isFile2(
|
|
10449
|
-
patchMd: isFile2(
|
|
10450
|
-
extendMd: isFile2(
|
|
10451
|
-
configJson: isFile2(
|
|
10452
|
-
hooksDir: isDirectory(
|
|
10453
|
-
scriptsDir: isDirectory(
|
|
11180
|
+
skillMd: isFile2(join37(dir, "SKILL.md")),
|
|
11181
|
+
patchMd: isFile2(join37(dir, "SKILL.patch.md")),
|
|
11182
|
+
extendMd: isFile2(join37(dir, "SKILL.extend.md")),
|
|
11183
|
+
configJson: isFile2(join37(dir, "skill-config.json")),
|
|
11184
|
+
hooksDir: isDirectory(join37(dir, "hooks")),
|
|
11185
|
+
scriptsDir: isDirectory(join37(dir, "scripts"))
|
|
10454
11186
|
};
|
|
10455
11187
|
}
|
|
10456
11188
|
function inventoryAllScopes(resolution) {
|
|
@@ -10719,18 +11451,18 @@ async function confirmWrite(yes, json, prompt) {
|
|
|
10719
11451
|
}
|
|
10720
11452
|
}
|
|
10721
11453
|
function resolveConfiguredRoot3(root) {
|
|
10722
|
-
return
|
|
11454
|
+
return resolvePath15(expandHome(root));
|
|
10723
11455
|
}
|
|
10724
11456
|
function isUnderRoot3(candidate, root) {
|
|
10725
|
-
const rel =
|
|
10726
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
11457
|
+
const rel = relative8(root, candidate);
|
|
11458
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute9(rel);
|
|
10727
11459
|
}
|
|
10728
11460
|
function sanitizeDiffForConsole(diff) {
|
|
10729
11461
|
return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
|
|
10730
11462
|
}
|
|
10731
11463
|
function resolveContentOption(options) {
|
|
10732
11464
|
if (options.content !== void 0) return options.content;
|
|
10733
|
-
if (options.contentFile) return
|
|
11465
|
+
if (options.contentFile) return readFileSync26(resolvePath15(options.contentFile), "utf-8");
|
|
10734
11466
|
return void 0;
|
|
10735
11467
|
}
|
|
10736
11468
|
function validateCaptureOptions(options) {
|
|
@@ -10865,10 +11597,10 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
10865
11597
|
}
|
|
10866
11598
|
mkdirSync11(scopeDir, { recursive: true });
|
|
10867
11599
|
const real = resolveConfiguredRoot3(scopeDir);
|
|
10868
|
-
const lexical =
|
|
11600
|
+
const lexical = resolvePath15(scopeDir);
|
|
10869
11601
|
let realResolved;
|
|
10870
11602
|
try {
|
|
10871
|
-
realResolved =
|
|
11603
|
+
realResolved = realpathSync11(scopeDir);
|
|
10872
11604
|
} catch {
|
|
10873
11605
|
realResolved = real;
|
|
10874
11606
|
}
|
|
@@ -10882,10 +11614,10 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
10882
11614
|
}
|
|
10883
11615
|
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
10884
11616
|
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
10885
|
-
const destPath =
|
|
11617
|
+
const destPath = join38(scopeDir, relPath);
|
|
10886
11618
|
const destDir = dirname15(destPath);
|
|
10887
11619
|
mkdirSync11(destDir, { recursive: true });
|
|
10888
|
-
const realDestDir =
|
|
11620
|
+
const realDestDir = realpathSync11(destDir);
|
|
10889
11621
|
if (!isUnderRoot3(realDestDir, realScopeDir)) {
|
|
10890
11622
|
throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
|
|
10891
11623
|
}
|
|
@@ -10978,12 +11710,12 @@ Write ${preview.destPath}? [y/N] `);
|
|
|
10978
11710
|
}
|
|
10979
11711
|
function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
|
|
10980
11712
|
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 &&
|
|
11713
|
+
const probePath = probeRelPath ? join38(scopeDir, probeRelPath) : void 0;
|
|
11714
|
+
const existingContent = probePath && existsSync30(probePath) ? readFileSync26(probePath, "utf-8") : "";
|
|
10983
11715
|
const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
|
|
10984
11716
|
const relPath = rendered.relPath;
|
|
10985
|
-
const destPath =
|
|
10986
|
-
const before =
|
|
11717
|
+
const destPath = join38(scopeDir, relPath);
|
|
11718
|
+
const before = existsSync30(destPath) ? readFileSync26(destPath, "utf-8") : "";
|
|
10987
11719
|
const diff = previewDiff(before, rendered.content, destPath);
|
|
10988
11720
|
return { relPath, content: rendered.content, destPath, diff };
|
|
10989
11721
|
}
|
|
@@ -10992,7 +11724,7 @@ async function refineListCommand(options = {}) {
|
|
|
10992
11724
|
const { records } = readRefinementsStore();
|
|
10993
11725
|
let rows = records;
|
|
10994
11726
|
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 ===
|
|
11727
|
+
if (options.project) rows = rows.filter((r) => r.project === options.project || r.projectRoot === resolvePath15(options.project));
|
|
10996
11728
|
if (options.status) {
|
|
10997
11729
|
if (!isRefinementStatus(options.status)) {
|
|
10998
11730
|
throw new UserError("--status must be one of: pending, applied, generalized");
|
|
@@ -11029,7 +11761,7 @@ async function refinePatternsCommand(options = {}) {
|
|
|
11029
11761
|
}
|
|
11030
11762
|
if (options.skill) rows = rows.filter((p) => p.affectedSkills.includes(options.skill));
|
|
11031
11763
|
if (options.project) {
|
|
11032
|
-
const resolvedProject =
|
|
11764
|
+
const resolvedProject = resolvePath15(options.project);
|
|
11033
11765
|
rows = rows.filter((p) => p.occurrences.some((o) => o.projectRealpath === resolvedProject));
|
|
11034
11766
|
}
|
|
11035
11767
|
const filtered = Boolean(options.skill || options.project || options.status);
|
|
@@ -11080,16 +11812,16 @@ async function refineWhichCommand(skill, options = {}) {
|
|
|
11080
11812
|
}
|
|
11081
11813
|
function writePostWriteSidecar(backupId, files) {
|
|
11082
11814
|
const sidecar = { schemaVersion: 1, files };
|
|
11083
|
-
const dir =
|
|
11815
|
+
const dir = join38(getBackupsDir(), backupId);
|
|
11084
11816
|
mkdirSync11(dir, { recursive: true });
|
|
11085
|
-
writeFileSync10(
|
|
11817
|
+
writeFileSync10(join38(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
|
|
11086
11818
|
}
|
|
11087
11819
|
function readPostWriteSidecar(backupId) {
|
|
11088
|
-
const path =
|
|
11089
|
-
if (!
|
|
11820
|
+
const path = join38(getBackupsDir(), backupId, "post.json");
|
|
11821
|
+
if (!existsSync30(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
|
|
11090
11822
|
let raw;
|
|
11091
11823
|
try {
|
|
11092
|
-
raw =
|
|
11824
|
+
raw = readFileSync26(path, "utf-8");
|
|
11093
11825
|
} catch {
|
|
11094
11826
|
return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
|
|
11095
11827
|
}
|
|
@@ -11134,21 +11866,21 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
11134
11866
|
const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
11135
11867
|
let realUserRoot;
|
|
11136
11868
|
try {
|
|
11137
|
-
realUserRoot =
|
|
11869
|
+
realUserRoot = realpathSync11(resolvedUserRoot);
|
|
11138
11870
|
} catch {
|
|
11139
11871
|
realUserRoot = resolvedUserRoot;
|
|
11140
11872
|
}
|
|
11141
11873
|
let realSkillDir;
|
|
11142
11874
|
try {
|
|
11143
|
-
realSkillDir =
|
|
11875
|
+
realSkillDir = realpathSync11(skillDir);
|
|
11144
11876
|
} catch {
|
|
11145
|
-
realSkillDir =
|
|
11877
|
+
realSkillDir = resolvePath15(skillDir);
|
|
11146
11878
|
}
|
|
11147
11879
|
if (!isUnderRoot3(realSkillDir, realUserRoot)) {
|
|
11148
11880
|
throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
|
|
11149
11881
|
}
|
|
11150
|
-
const skillMdPath =
|
|
11151
|
-
if (!
|
|
11882
|
+
const skillMdPath = join38(skillDir, "SKILL.md");
|
|
11883
|
+
if (!existsSync30(skillMdPath)) {
|
|
11152
11884
|
throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
|
|
11153
11885
|
}
|
|
11154
11886
|
const refinementsStore = readRefinementsStore();
|
|
@@ -11165,7 +11897,7 @@ async function refinePromoteCommand(patternId, options = {}) {
|
|
|
11165
11897
|
`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
11898
|
);
|
|
11167
11899
|
}
|
|
11168
|
-
const before =
|
|
11900
|
+
const before = readFileSync26(skillMdPath, "utf-8");
|
|
11169
11901
|
const after = applyPatchBlocks(before, [block]);
|
|
11170
11902
|
const diff = previewDiff(before, after, skillMdPath);
|
|
11171
11903
|
if (options.dryRun) {
|
|
@@ -11197,7 +11929,7 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
11197
11929
|
throw err;
|
|
11198
11930
|
}
|
|
11199
11931
|
try {
|
|
11200
|
-
const postSha256 = createHash8("sha256").update(
|
|
11932
|
+
const postSha256 = createHash8("sha256").update(readFileSync26(skillMdPath)).digest("hex");
|
|
11201
11933
|
writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
|
|
11202
11934
|
markGeneralized(patternId, backup.id);
|
|
11203
11935
|
const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
|
|
@@ -11223,25 +11955,25 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
|
|
|
11223
11955
|
}
|
|
11224
11956
|
function assertBackupDirContained(backupId) {
|
|
11225
11957
|
const backupsDir = getBackupsDir();
|
|
11226
|
-
if (!
|
|
11227
|
-
const realBackupsDir =
|
|
11228
|
-
const backupDir =
|
|
11229
|
-
if (!
|
|
11230
|
-
const realBackupDir =
|
|
11958
|
+
if (!existsSync30(backupsDir)) return;
|
|
11959
|
+
const realBackupsDir = realpathSync11(backupsDir);
|
|
11960
|
+
const backupDir = join38(backupsDir, backupId);
|
|
11961
|
+
if (!existsSync30(backupDir)) return;
|
|
11962
|
+
const realBackupDir = realpathSync11(backupDir);
|
|
11231
11963
|
if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
|
|
11232
11964
|
throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
|
|
11233
11965
|
}
|
|
11234
11966
|
}
|
|
11235
11967
|
function resolveTargetRealPath(path) {
|
|
11236
|
-
let lexicalAncestor =
|
|
11968
|
+
let lexicalAncestor = resolvePath15(path);
|
|
11237
11969
|
while (true) {
|
|
11238
11970
|
try {
|
|
11239
|
-
const real =
|
|
11240
|
-
const remainder =
|
|
11241
|
-
return remainder === "" ? real :
|
|
11971
|
+
const real = realpathSync11(lexicalAncestor);
|
|
11972
|
+
const remainder = relative8(lexicalAncestor, resolvePath15(path));
|
|
11973
|
+
return remainder === "" ? real : join38(real, remainder);
|
|
11242
11974
|
} catch {
|
|
11243
11975
|
const parent = dirname15(lexicalAncestor);
|
|
11244
|
-
if (parent === lexicalAncestor) return
|
|
11976
|
+
if (parent === lexicalAncestor) return resolvePath15(path);
|
|
11245
11977
|
lexicalAncestor = parent;
|
|
11246
11978
|
}
|
|
11247
11979
|
}
|
|
@@ -11253,7 +11985,7 @@ function assertManifestPathsWithinRoots(manifest, config) {
|
|
|
11253
11985
|
const roots = config.skillsRoots.map((root) => {
|
|
11254
11986
|
const resolved = resolveConfiguredRoot3(root);
|
|
11255
11987
|
try {
|
|
11256
|
-
return
|
|
11988
|
+
return realpathSync11(resolved);
|
|
11257
11989
|
} catch {
|
|
11258
11990
|
return resolved;
|
|
11259
11991
|
}
|
|
@@ -11280,14 +12012,14 @@ function assertNoSymlinkLeaves(manifest) {
|
|
|
11280
12012
|
}
|
|
11281
12013
|
}
|
|
11282
12014
|
function findLexicalRoot(filePath, config) {
|
|
11283
|
-
const lexicalTarget =
|
|
12015
|
+
const lexicalTarget = resolvePath15(filePath);
|
|
11284
12016
|
for (const root of config.skillsRoots) {
|
|
11285
12017
|
const rootLexical = resolveConfiguredRoot3(root);
|
|
11286
|
-
const rel =
|
|
11287
|
-
if (rel !== "" && (rel === ".." || rel.startsWith(`..${
|
|
12018
|
+
const rel = relative8(rootLexical, lexicalTarget);
|
|
12019
|
+
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep8}`) || isAbsolute9(rel))) continue;
|
|
11288
12020
|
let rootReal;
|
|
11289
12021
|
try {
|
|
11290
|
-
rootReal =
|
|
12022
|
+
rootReal = realpathSync11(rootLexical);
|
|
11291
12023
|
} catch {
|
|
11292
12024
|
rootReal = rootLexical;
|
|
11293
12025
|
}
|
|
@@ -11297,8 +12029,8 @@ function findLexicalRoot(filePath, config) {
|
|
|
11297
12029
|
}
|
|
11298
12030
|
function walkRealComponents(filePath, config, createMissing) {
|
|
11299
12031
|
const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
|
|
11300
|
-
const relPath =
|
|
11301
|
-
const components = relPath.split(
|
|
12032
|
+
const relPath = relative8(rootLexical, resolvePath15(filePath));
|
|
12033
|
+
const components = relPath.split(sep8).filter((c) => c.length > 0);
|
|
11302
12034
|
if (components.length === 0) {
|
|
11303
12035
|
throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
|
|
11304
12036
|
}
|
|
@@ -11307,7 +12039,7 @@ function walkRealComponents(filePath, config, createMissing) {
|
|
|
11307
12039
|
let current = rootReal;
|
|
11308
12040
|
for (const component of dirComponents) {
|
|
11309
12041
|
if (current === void 0) break;
|
|
11310
|
-
const next =
|
|
12042
|
+
const next = join38(current, component);
|
|
11311
12043
|
let lst;
|
|
11312
12044
|
try {
|
|
11313
12045
|
lst = lstatSync7(next);
|
|
@@ -11338,7 +12070,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
11338
12070
|
if (parentReal === void 0) {
|
|
11339
12071
|
throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
|
|
11340
12072
|
}
|
|
11341
|
-
const finalPath =
|
|
12073
|
+
const finalPath = join38(parentReal, leafComponent);
|
|
11342
12074
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
11343
12075
|
throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
|
|
11344
12076
|
}
|
|
@@ -11364,7 +12096,7 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
|
|
|
11364
12096
|
function safeDeleteTombstone(filePath, config) {
|
|
11365
12097
|
const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
|
|
11366
12098
|
if (parentReal === void 0) return;
|
|
11367
|
-
const finalPath =
|
|
12099
|
+
const finalPath = join38(parentReal, leafComponent);
|
|
11368
12100
|
if (!isUnderRoot3(finalPath, rootReal)) {
|
|
11369
12101
|
throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
|
|
11370
12102
|
}
|
|
@@ -11406,7 +12138,7 @@ async function refineRollbackCommand(backupId, options = {}) {
|
|
|
11406
12138
|
for (const file of manifest.files) {
|
|
11407
12139
|
const expected = sidecar.hashes.get(file.path);
|
|
11408
12140
|
if (expected === void 0) continue;
|
|
11409
|
-
const currentSha256 =
|
|
12141
|
+
const currentSha256 = existsSync30(file.path) ? createHash8("sha256").update(readFileSync26(file.path)).digest("hex") : null;
|
|
11410
12142
|
if (currentSha256 !== expected) mismatches.push(file.path);
|
|
11411
12143
|
}
|
|
11412
12144
|
if (mismatches.length > 0 && !options.force) {
|
|
@@ -11497,6 +12229,13 @@ function parsePruneDays(value) {
|
|
|
11497
12229
|
}
|
|
11498
12230
|
return n;
|
|
11499
12231
|
}
|
|
12232
|
+
function parseUsageWindow(value) {
|
|
12233
|
+
const n = Number(value);
|
|
12234
|
+
if (value.trim() === "" || !Number.isInteger(n) || n < 1) {
|
|
12235
|
+
throw new UserError(`--usage-window must be a positive integer (got "${value}")`);
|
|
12236
|
+
}
|
|
12237
|
+
return n;
|
|
12238
|
+
}
|
|
11500
12239
|
function parseInsightAnalysisMode(value) {
|
|
11501
12240
|
if (value !== "independent" && value !== "single") {
|
|
11502
12241
|
throw new UserError(`--analysis-mode must be "independent" or "single" (got "${value}")`);
|
|
@@ -11544,7 +12283,7 @@ configCmd.command("propose <key> <value>").description("Queue a preference for r
|
|
|
11544
12283
|
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
12284
|
program.command("guide [topic]").description("What this tool does, what state you are in, and the next command to run").action(guide);
|
|
11546
12285
|
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);
|
|
12286
|
+
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
12287
|
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
12288
|
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
12289
|
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 +12294,7 @@ program.command("watch").description("Drift check across every SOURCES.md proven
|
|
|
11555
12294
|
"--skill-map <path>",
|
|
11556
12295
|
"also drift-check every fork-of edge in a generated rhize-plugins skill map (contentHash compare against upstream)"
|
|
11557
12296
|
).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);
|
|
12297
|
+
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
12298
|
program.command("ingest").description(
|
|
11560
12299
|
"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
12300
|
).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
|