@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/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 join21 } from "path";
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 extname3, join as join12, resolve as resolvePath2 } from "path";
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 extname2 } from "path";
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 (extname2(file).toLowerCase() === ".json") {
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, sep7, maxSplit) {
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(sep7);
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 + sep7.length);
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) => ({ severity: f.severity, rule: f.rule, target: f.file, detail: f.detail, context: f.context }));
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 = extname3(targetPath).toLowerCase() === ".toml";
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 skills = groups.map((g) => profileAuditedSkill(g, findings, unreadableSkillMdPaths));
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: "MEDIUM",
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
- overlapClusters = clusterOverlapPairs(pairs);
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((r) => `${r.root}${r.exists ? "" : " (MISSING)"} \u2014 ${r.skillCount} skill${r.skillCount === 1 ? "" : "s"}`),
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
- return [
2481
- "## Hygiene findings",
2482
- "",
2483
- ...bulletList(
2484
- findings.map(
2485
- (f) => `[${f.severity}] ${f.rule} \u2014 ${sanitizeForMarkdown(f.target)}: ${sanitizeForMarkdown(f.detail)} (fp ${displayFingerprint(f)})`
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((c) => `top score ${c.topScore} \u2014 ${c.suggestedVerb} \u2014 ${c.members.join(", ")}`),
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
- // Taken from the report rather than re-accumulated: `audit.ts` already counted exactly this
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 extname5, join as join15, resolve as resolve3 } from "path";
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 extname4, join as join14 } from "path";
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(extname4(file))) continue;
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 (extname5(currentPath) === ".skill" || initialStat.isFile() && isZipFile(currentPath)) {
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 renderFindingLine(finding2) {
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(renderFindingLine)];
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 existsSync17 } from "fs";
3971
- import { homedir as homedir4 } from "os";
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 sep2 } from "path";
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(sep2).join("/");
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 existsSync16 } from "fs";
4352
- import { join as join20, resolve as resolvePath3 } from "path";
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 resolvePath3(reportOption);
5081
+ if (reportOption) return resolvePath5(reportOption);
4419
5082
  const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4420
- return join20(getHomeDir(), "reports", `audit-${iso}.md`);
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 (!existsSync16(getConfigPath())) {
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
- if (storeNotices.length > 0) report.notices = [...report.notices, ...storeNotices];
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 ?? homedir4();
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(selectedRoots, selectedRoots[0], void 0, selectedMcpTargets);
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
- const config = buildConfig(selected, target, handoffAgent, selectedMcp);
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 (existsSync17(getConfigPath())) return;
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] ?? join21(process.cwd(), ".mcp.json");
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 join22 } from "path";
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(join22(quarantineDir, id));
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(join22(dir, "SKILL.md")).name;
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 existsSync19 } from "fs";
5830
+ import { existsSync as existsSync20 } from "fs";
5141
5831
 
5142
5832
  // src/commands/held.ts
5143
- import { existsSync as existsSync18, realpathSync as realpathSync3 } from "fs";
5144
- import { basename as basename9, join as join23, resolve as resolvePath4, sep as sep3 } from "path";
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 = join23(quarantineDir, id);
5151
- if (!existsSync18(candidate)) {
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 = realpathSync3(resolvePath4(quarantineDir));
5155
- const realCandidate = realpathSync3(candidate);
5156
- if (!realCandidate.startsWith(realRoot + sep3)) {
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(join23(dir, "SKILL.md")).name;
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) => existsSync18(join23(candidate, 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] ?? join23(process.cwd(), ".mcp.json");
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 = existsSync19(configPath);
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 existsSync20, lstatSync as lstatSync4, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, rmSync as rmSync6 } from "fs";
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 join24, resolve as resolvePath5, sep as sep4 } from "path";
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 = resolvePath5(targetSkillDir);
5709
- const skillMdPath = join24(resolvedTargetDir, "SKILL.md");
5710
- if (!existsSync20(skillMdPath)) {
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 = resolvePath5(options.project ?? process.cwd());
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 = join24(project, ".skillopt-sleep", "staging");
5769
- const stagingDir = resolvePath5(parsed.staging_dir);
5770
- if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep4)) {
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 = join24(stagingDir, "proposed_SKILL.md");
5776
- const proposedClaudeMd = join24(stagingDir, "proposed_CLAUDE.md");
5777
- const reportMdPath = join24(stagingDir, "report.md");
5778
- if (!existsSync20(proposedSkillMd)) {
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 (existsSync20(reportMdPath)) {
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(join24(tmpdir2(), "skill-forge-evolve-"));
6478
+ const tempDir = mkdtempSync3(join27(tmpdir2(), "skill-forge-evolve-"));
5789
6479
  let profile;
5790
6480
  let safety;
5791
6481
  try {
5792
- cpSync3(proposedSkillMd, join24(tempDir, "SKILL.md"));
5793
- if (existsSync20(proposedClaudeMd)) {
5794
- cpSync3(proposedClaudeMd, join24(tempDir, "CLAUDE.md"));
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 (existsSync20(reportMdPath)) {
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 = join24(stagingDir, "manifest.json");
6625
+ const manifestPath = join27(stagingDir, "manifest.json");
5936
6626
  let raw;
5937
6627
  try {
5938
- raw = readFileSync16(manifestPath, "utf-8");
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 (!existsSync20(a) || !existsSync20(b)) return false;
5973
- return readFileSync16(a).equals(readFileSync16(b));
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 = join24(stagingDir, "proposed_SKILL.md");
5977
- const proposedClaudeMd = join24(stagingDir, "proposed_CLAUDE.md");
6666
+ const proposedSkillMd = join27(stagingDir, "proposed_SKILL.md");
6667
+ const proposedClaudeMd = join27(stagingDir, "proposed_CLAUDE.md");
5978
6668
  let sawComparison = false;
5979
- if (existsSync20(proposedSkillMd)) {
6669
+ if (existsSync21(proposedSkillMd)) {
5980
6670
  sawComparison = true;
5981
6671
  if (!filesIdentical(proposedSkillMd, manifest.live_skill_path)) return false;
5982
6672
  }
5983
- if (existsSync20(proposedClaudeMd) && manifest.live_memory_path) {
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 (!existsSync20(path)) return null;
6002
- return createHash3("sha256").update(readFileSync16(path)).digest("hex");
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 existsSync21 } from "fs";
6701
+ import { existsSync as existsSync22 } from "fs";
6012
6702
  function requireSavedConfig() {
6013
- if (!existsSync21(getConfigPath())) {
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) => !existsSync21(r.target)) : records;
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 resolvePath7 } from "path";
6807
+ import { resolve as resolvePath9 } from "path";
6118
6808
 
6119
6809
  // src/organize.ts
6120
- import { existsSync as existsSync22, readFileSync as readFileSync17, readdirSync as readdirSync8, realpathSync as realpathSync4, statSync as statSync9 } from "fs";
6121
- import { basename as basename11, join as join25, resolve as resolvePath6 } from "path";
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 = readdirSync8(dir, { withFileTypes: true });
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 = join25(dir, entry.name);
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 (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
6842
+ if (existsSync23(join28(dir, "SKILL.md"))) found.add(dir);
6153
6843
  }
6154
- for (const dir of listSubdirs2(join25(root, "skills"))) {
6155
- if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
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(join25(topDir, "skills"))) {
6159
- if (existsSync22(join25(dir, "SKILL.md"))) found.add(dir);
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 = resolvePath6(expandHome(root));
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 parseCapabilityFrontmatter(text, fallbackName) {
6205
- const block = extractFrontmatterBlock(text);
6206
- if (block === null) {
6207
- return { name: fallbackName, consumes: [], validFrontmatter: false };
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 ["name", "tier", "domain", "provenance", "maturity"]) {
6211
- const m = block.match(new RegExp(`^${key}:\\s*(.+)$`, "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(block, "consumes"),
6221
- validFrontmatter: true
6942
+ consumes: parseYamlList(dedented, "consumes")
6222
6943
  };
6223
6944
  }
6224
- function loadUsageSnapshot(snapshotPath) {
6225
- const resolvedPath = resolvePath6(expandHome(snapshotPath));
6226
- let raw;
6227
- try {
6228
- raw = readFileSync17(resolvedPath, "utf-8");
6229
- } catch (err) {
6230
- return { usage: /* @__PURE__ */ new Map(), warning: `could not read usage snapshot (${err.message}); proceeding without usage` };
6231
- }
6232
- let parsed;
6233
- try {
6234
- parsed = JSON.parse(raw);
6235
- } catch (err) {
6236
- return { usage: /* @__PURE__ */ new Map(), warning: `could not parse usage snapshot (${err.message}); proceeding without usage` };
6237
- }
6238
- const usage = /* @__PURE__ */ new Map();
6239
- const outer = parsed;
6240
- const report = outer && typeof outer === "object" ? outer.report : void 0;
6241
- const topSkills = report && typeof report === "object" ? report.top_skills : void 0;
6242
- if (Array.isArray(topSkills)) {
6243
- for (const row of topSkills) {
6244
- if (Array.isArray(row) && row.length === 2 && typeof row[0] === "string" && typeof row[1] === "number") {
6245
- const [name, count] = row;
6246
- usage.set(name, count);
6247
- if (name.includes(":")) {
6248
- const bare = name.split(":").pop();
6249
- if (!usage.has(bare)) usage.set(bare, count);
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 = realpathSync4(candidate.dirPath);
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 = join25(realPath, "SKILL.md");
7052
+ const skillMdPath = join28(realPath, "SKILL.md");
6335
7053
  let text;
6336
7054
  try {
6337
- text = readFileSync17(skillMdPath, "utf-8");
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 = resolvePath7(outPath);
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 realpathSync5 } from "fs";
6743
- import { join as join26, resolve as resolvePath8 } from "path";
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 readFileSync18, existsSync as existsSync23 } from "fs";
6748
- import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve7 } from "path";
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 (isAbsolute3(expanded)) return resolve7(expanded);
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 (existsSync23(candidate)) return candidate;
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 (!existsSync23(resolved)) return void 0;
7513
+ if (!existsSync24(resolved)) return void 0;
6795
7514
  try {
6796
- return readFileSync18(resolved, "utf-8");
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 (!existsSync23(localResolved)) {
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 = resolvePath8(expandHome(root));
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 = realpathSync5(resolvedRoot);
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 = join26(resolvedRoot, "SOURCES.md");
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 existsSync24, rmSync as rmSync7, statSync as statSync10 } from "fs";
7071
- import { join as join27 } from "path";
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 join27(getHomeDir(), "reports", `routine-${generatedAt.replace(/[:.]/g, "-")}.md`);
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 = join27(config.quarantineDir, id);
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 || existsSync24(entry.installedPath)) continue;
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
- for (const notice of storeNotices) log(`[skill-forge] ${notice}`);
7245
- const audit2 = await auditConfiguredSet(config, { accepted });
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: storeNotices,
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 realpathSync6 } from "fs";
7332
- import { dirname as dirname12, isAbsolute as isAbsolute4, relative as relative3, resolve as resolvePath9 } from "path";
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 realpathSync6(p);
8105
+ return realpathSync7(p);
7374
8106
  } catch {
7375
8107
  return null;
7376
8108
  }
7377
8109
  }
7378
8110
  function isUnderRoot(candidate, root) {
7379
- const rel = relative3(root, candidate);
7380
- return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
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(resolvePath9(config.quarantineDir));
7384
- const skillsRoots = config.skillsRoots.map((r) => realpathOrNull(resolvePath9(r))).filter((r) => r !== null);
7385
- const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname12(resolvePath9(f)))).filter((r) => r !== null);
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(resolvePath9(effectivePath));
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 readFileSync22,
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 isAbsolute6, join as join32, relative as relative5, resolve as resolvePath10 } from "path";
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 readFileSync19 } from "fs";
7470
- import { join as join28 } from "path";
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(readFileSync19(join28(path, "SKILL.md"), "utf-8")) };
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 existsSync26, readFileSync as readFileSync21 } from "fs";
7735
- import { join as join31 } from "path";
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 existsSync25, mkdirSync as mkdirSync8, readFileSync as readFileSync20 } from "fs";
7739
- import { join as join30 } from "path";
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 readdirSync9,
7751
- realpathSync as realpathSync7,
8482
+ readdirSync as readdirSync11,
8483
+ realpathSync as realpathSync8,
7752
8484
  statSync as statSync11
7753
8485
  } from "fs";
7754
- import { basename as basename12, extname as extname6, isAbsolute as isAbsolute5, join as join29, relative as relative4, resolve as resolve8, sep as sep5 } from "path";
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 isInside2(child, root) {
7825
- const rel = relative4(root, child);
7826
- return rel === "" || !rel.startsWith(`..${sep5}`) && rel !== ".." && !isAbsolute5(rel);
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[extname6(path).toLowerCase()];
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 = realpathSync7(path);
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 = realpathSync7(resolved);
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 = readdirSync9(dir, { withFileTypes: true }).sort((a, b) => compareStrings2(a.name, b.name));
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 = join29(dir, entry.name);
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 = realpathSync7(full);
8835
+ realTarget = realpathSync8(full);
8104
8836
  } catch {
8105
8837
  throw new UserError(`source directory contains an unreadable symlink: ${logicalPath}`);
8106
8838
  }
8107
- if (!isInside2(realTarget, realRoot)) {
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 = realpathSync7(full);
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 = realpathSync7(full);
8132
- if (!isInside2(realFile, realRoot)) {
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 join30(getHomeDir(), "insights");
8970
+ return join33(getHomeDir(), "insights");
8239
8971
  }
8240
8972
  function getStudyDir(studyId) {
8241
8973
  assertStudyId(studyId);
8242
- return join30(getInsightsDir(), studyId);
8974
+ return join33(getInsightsDir(), studyId);
8243
8975
  }
8244
8976
  function getStudyPath(studyId) {
8245
- return join30(getStudyDir(studyId), "study.json");
8977
+ return join33(getStudyDir(studyId), "study.json");
8246
8978
  }
8247
8979
  function getStudyEventsPath(studyId) {
8248
- return join30(getStudyDir(studyId), "events.jsonl");
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 = readFileSync20(path, "utf-8");
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 (existsSync25(path)) throw new UserError(`insight study already exists: ${studyId}`);
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 (!existsSync25(path)) throw new UserError(`no insight study with id: ${studyId}`);
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 (!existsSync25(path)) throw new UserError(`no insight study with id: ${study.studyId}`);
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 join31(getStudyDir(studyId), "jira-receipts.jsonl");
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 (!existsSync26(path)) return [];
10045
+ if (!existsSync27(path)) return [];
9314
10046
  let text;
9315
10047
  try {
9316
- text = readFileSync21(path, "utf-8");
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 = resolvePath10(path);
10187
+ const resolved = resolvePath12(path);
9456
10188
  let raw;
9457
10189
  try {
9458
- raw = readFileSync22(resolved, "utf-8");
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 isAbsolute6(path) ? path : resolvePath10(baseDir, path);
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 = resolvePath10(path);
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 = resolvePath10(value);
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 join32(getStudyDir(studyId), CONTEXT_FILENAME);
10335
+ return join35(getStudyDir(studyId), CONTEXT_FILENAME);
9604
10336
  }
9605
10337
  function inventoryPath(studyId) {
9606
- return join32(getStudyDir(studyId), INVENTORY_FILENAME);
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 ? resolvePath10(skillMapPath) : void 0,
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" || !isAbsolute6(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))) {
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 = resolvePath10(root, relativePath);
9644
- const rel = relative5(root, candidate);
9645
- if (rel.startsWith("..") || isAbsolute6(rel)) throw new UserError(`refusing retained source path outside the study`);
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 = join32(getStudyDir(study.studyId), "retained-sources");
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 = join32(root, file.sourceId);
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 ? resolvePath10(options.skillMap) : void 0;
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 = join32(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
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 = join32(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
10637
+ const privatePath = join35(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
9906
10638
  writeJsonAtomic(privatePath, manifest);
9907
10639
  if (options.out) {
9908
- writeNewFile(resolvePath10(options.out), `${JSON.stringify(manifest, null, 2)}
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: ${resolvePath10(options.out)}`);
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 existsSync29, lstatSync as lstatSync7, mkdirSync as mkdirSync11, readFileSync as readFileSync24, realpathSync as realpathSync10, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
9978
- import { dirname as dirname15, isAbsolute as isAbsolute8, join as join35, relative as relative7, resolve as resolvePath13, sep as sep6 } from "path";
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 existsSync28, realpathSync as realpathSync9, statSync as statSync13 } from "fs";
9983
- import { isAbsolute as isAbsolute7, join as join34, relative as relative6, resolve as resolvePath12 } from "path";
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 existsSync27, readFileSync as readFileSync23, statSync as statSync12, realpathSync as realpathSync8 } from "fs";
10054
- import { join as join33, resolve as resolvePath11 } from "path";
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 join33(getHomeDir(), "refinements");
10790
+ return join36(getHomeDir(), "refinements");
10059
10791
  }
10060
10792
  function getRefinementsPath() {
10061
- return join33(getRefinementsDir(), "refinements.json");
10793
+ return join36(getRefinementsDir(), "refinements.json");
10062
10794
  }
10063
10795
  function getPatternsPath() {
10064
- return join33(getRefinementsDir(), "patterns.json");
10796
+ return join36(getRefinementsDir(), "patterns.json");
10065
10797
  }
10066
10798
  function getBackupsDir() {
10067
- return join33(getRefinementsDir(), "backups");
10799
+ return join36(getRefinementsDir(), "backups");
10068
10800
  }
10069
10801
  function readJsonFile(path, label2) {
10070
10802
  let raw;
10071
10803
  try {
10072
- raw = readFileSync23(path, "utf-8");
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 (!existsSync27(path)) {
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 = resolvePath11(projectRoot);
10905
+ const resolved = resolvePath13(projectRoot);
10174
10906
  try {
10175
- return realpathSync8(resolved);
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 (!existsSync27(path)) {
11039
+ if (!existsSync28(path)) {
10308
11040
  return { path, existed: false, sha256: null, mode: null, contentBase64: null };
10309
11041
  }
10310
- const bytes = readFileSync23(path);
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(join33(getBackupsDir(), id, "manifest.json"), manifest);
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 = join33(getBackupsDir(), id, "manifest.json");
10366
- if (!existsSync27(manifestPath)) {
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 resolvePath12(expandHome(root));
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 realpathSync9(p);
11120
+ return realpathSync10(p);
10389
11121
  } catch {
10390
11122
  return null;
10391
11123
  }
10392
11124
  }
10393
11125
  function isUnderRoot2(candidate, root) {
10394
- const rel = relative6(root, candidate);
10395
- return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
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 = resolvePath12(cwd);
10400
- const localDir = join34(resolvedCwd, ".claude", "skills", skill);
10401
- const sharedDir = join34(resolvedCwd, "skills", skill);
10402
- const local = { scope: "local", dir: localDir, exists: existsSync28(localDir) };
10403
- const shared = { scope: "shared", dir: sharedDir, exists: existsSync28(sharedDir) };
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 = resolvePath12(userRootFlag);
10410
- return { resolved: true, dir: join34(root, skill), root: userRootFlag, reason: "flag" };
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: join34(resolveConfiguredRoot2(root), skill) }));
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(join34(dir, "SKILL.md")),
10449
- patchMd: isFile2(join34(dir, "SKILL.patch.md")),
10450
- extendMd: isFile2(join34(dir, "SKILL.extend.md")),
10451
- configJson: isFile2(join34(dir, "skill-config.json")),
10452
- hooksDir: isDirectory(join34(dir, "hooks")),
10453
- scriptsDir: isDirectory(join34(dir, "scripts"))
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 resolvePath13(expandHome(root));
11454
+ return resolvePath15(expandHome(root));
10723
11455
  }
10724
11456
  function isUnderRoot3(candidate, root) {
10725
- const rel = relative7(root, candidate);
10726
- return rel === "" || !rel.startsWith("..") && !isAbsolute8(rel);
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 readFileSync24(resolvePath13(options.contentFile), "utf-8");
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 = resolvePath13(scopeDir);
11600
+ const lexical = resolvePath15(scopeDir);
10869
11601
  let realResolved;
10870
11602
  try {
10871
- realResolved = realpathSync10(scopeDir);
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 = join35(scopeDir, relPath);
11617
+ const destPath = join38(scopeDir, relPath);
10886
11618
  const destDir = dirname15(destPath);
10887
11619
  mkdirSync11(destDir, { recursive: true });
10888
- const realDestDir = realpathSync10(destDir);
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 ? join35(scopeDir, probeRelPath) : void 0;
10982
- const existingContent = probePath && existsSync29(probePath) ? readFileSync24(probePath, "utf-8") : "";
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 = join35(scopeDir, relPath);
10986
- const before = existsSync29(destPath) ? readFileSync24(destPath, "utf-8") : "";
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 === resolvePath13(options.project));
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 = resolvePath13(options.project);
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 = join35(getBackupsDir(), backupId);
11815
+ const dir = join38(getBackupsDir(), backupId);
11084
11816
  mkdirSync11(dir, { recursive: true });
11085
- writeFileSync10(join35(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
11817
+ writeFileSync10(join38(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
11086
11818
  }
11087
11819
  function readPostWriteSidecar(backupId) {
11088
- const path = join35(getBackupsDir(), backupId, "post.json");
11089
- if (!existsSync29(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
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 = readFileSync24(path, "utf-8");
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 = realpathSync10(resolvedUserRoot);
11869
+ realUserRoot = realpathSync11(resolvedUserRoot);
11138
11870
  } catch {
11139
11871
  realUserRoot = resolvedUserRoot;
11140
11872
  }
11141
11873
  let realSkillDir;
11142
11874
  try {
11143
- realSkillDir = realpathSync10(skillDir);
11875
+ realSkillDir = realpathSync11(skillDir);
11144
11876
  } catch {
11145
- realSkillDir = resolvePath13(skillDir);
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 = join35(skillDir, "SKILL.md");
11151
- if (!existsSync29(skillMdPath)) {
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 = readFileSync24(skillMdPath, "utf-8");
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(readFileSync24(skillMdPath)).digest("hex");
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 (!existsSync29(backupsDir)) return;
11227
- const realBackupsDir = realpathSync10(backupsDir);
11228
- const backupDir = join35(backupsDir, backupId);
11229
- if (!existsSync29(backupDir)) return;
11230
- const realBackupDir = realpathSync10(backupDir);
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 = resolvePath13(path);
11968
+ let lexicalAncestor = resolvePath15(path);
11237
11969
  while (true) {
11238
11970
  try {
11239
- const real = realpathSync10(lexicalAncestor);
11240
- const remainder = relative7(lexicalAncestor, resolvePath13(path));
11241
- return remainder === "" ? real : join35(real, remainder);
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 resolvePath13(path);
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 realpathSync10(resolved);
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 = resolvePath13(filePath);
12015
+ const lexicalTarget = resolvePath15(filePath);
11284
12016
  for (const root of config.skillsRoots) {
11285
12017
  const rootLexical = resolveConfiguredRoot3(root);
11286
- const rel = relative7(rootLexical, lexicalTarget);
11287
- if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep6}`) || isAbsolute8(rel))) continue;
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 = realpathSync10(rootLexical);
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 = relative7(rootLexical, resolvePath13(filePath));
11301
- const components = relPath.split(sep6).filter((c) => c.length > 0);
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 = join35(current, component);
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 = join35(parentReal, leafComponent);
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 = join35(parentReal, leafComponent);
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 = existsSync29(file.path) ? createHash8("sha256").update(readFileSync24(file.path)).digest("hex") : null;
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);