@rhize/skill-forge 0.16.0 → 0.17.0

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