@rhize/skill-forge 0.8.0 → 0.10.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
@@ -561,7 +561,7 @@ function resolveMcpCandidateDetail(profile) {
561
561
  }
562
562
 
563
563
  // src/provenance.ts
564
- import { appendFileSync, existsSync as existsSync6, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
564
+ import { appendFileSync, existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
565
565
  import { join as join7 } from "path";
566
566
  var FORGE_VERBS = ["DEFER", "ABSORB", "FORK", "REJECT", "WATCH"];
567
567
  var LEDGER_HEADER = "# Skill Forge \u2014 Provenance Ledger\n\nOne entry per external-skill ingestion decision.\n";
@@ -600,6 +600,55 @@ function appendProvenance(entry, skillsRoot) {
600
600
  appendFileSync(ledgerPath, block, "utf-8");
601
601
  return ledgerPath;
602
602
  }
603
+ function extractField(block, label) {
604
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
605
+ const match = block.match(new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.*)`));
606
+ return match ? match[1].trim() : void 0;
607
+ }
608
+ function parseProvenanceLedger(text) {
609
+ const entries = [];
610
+ const warnings = [];
611
+ const blocks = text.split(/\n## /);
612
+ for (const block of blocks.slice(1)) {
613
+ const headerLine = block.split("\n", 1)[0] ?? "";
614
+ const [namePart, ...dateParts] = headerLine.split(" \u2014 ");
615
+ const name = namePart?.trim();
616
+ if (!name) {
617
+ warnings.push(`skipped malformed ledger block: missing "## <name> \u2014 <date>" header (saw "${headerLine.slice(0, 80)}")`);
618
+ continue;
619
+ }
620
+ const date = dateParts.join(" \u2014 ").trim();
621
+ const verbRaw = extractField(block, "Verb");
622
+ if (!verbRaw || !FORGE_VERBS.includes(verbRaw)) {
623
+ warnings.push(`skipped malformed ledger block for "${name}": missing or invalid Verb (saw "${verbRaw ?? ""}")`);
624
+ continue;
625
+ }
626
+ const driftCheckMatch = block.match(/\*\*Drift check:\*\*\s*`([^`]*)`/);
627
+ entries.push({
628
+ name,
629
+ date,
630
+ source: extractField(block, "Source") ?? "",
631
+ upstreamRef: extractField(block, "Upstream ref") ?? "",
632
+ license: extractField(block, "License") ?? "",
633
+ verb: verbRaw,
634
+ target: extractField(block, "Target") ?? "",
635
+ took: extractField(block, "Took") ?? "",
636
+ verified: extractField(block, "Verified") ?? "",
637
+ driftCheck: driftCheckMatch ? driftCheckMatch[1] : "",
638
+ notes: extractField(block, "Notes") ?? ""
639
+ });
640
+ }
641
+ return { entries, warnings };
642
+ }
643
+ function readProvenanceLedger(skillsRoot) {
644
+ const ledgerPath = join7(skillsRoot, "SOURCES.md");
645
+ if (!existsSync6(ledgerPath)) {
646
+ return { ledgerPath, exists: false, entries: [], warnings: [] };
647
+ }
648
+ const text = readFileSync5(ledgerPath, "utf-8");
649
+ const parsed = parseProvenanceLedger(text);
650
+ return { ledgerPath, exists: true, ...parsed };
651
+ }
603
652
 
604
653
  // src/commands/gatePipeline.ts
605
654
  import { readdirSync as readdirSync5 } from "fs";
@@ -612,7 +661,7 @@ import {
612
661
  existsSync as existsSync8,
613
662
  mkdtempSync,
614
663
  openSync,
615
- readFileSync as readFileSync6,
664
+ readFileSync as readFileSync7,
616
665
  readdirSync as readdirSync3,
617
666
  readSync,
618
667
  rmSync as rmSync2,
@@ -622,13 +671,13 @@ import { tmpdir } from "os";
622
671
  import { basename as basename3, dirname as dirname3, extname as extname3, join as join9, resolve as resolve2 } from "path";
623
672
 
624
673
  // src/gate/mcpCapabilities.ts
625
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
674
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
626
675
  import { extname as extname2, join as join8 } from "path";
627
676
  var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
628
677
  var MAX_ITEMS_PER_CATEGORY = 200;
629
678
  function readJsonSafe(file) {
630
679
  try {
631
- return JSON.parse(readFileSync5(file, "utf-8"));
680
+ return JSON.parse(readFileSync6(file, "utf-8"));
632
681
  } catch {
633
682
  return void 0;
634
683
  }
@@ -856,7 +905,7 @@ function readPackageJsonSafe(root) {
856
905
  const pkgPath = join9(root, "package.json");
857
906
  if (!existsSync8(pkgPath)) return void 0;
858
907
  try {
859
- return JSON.parse(readFileSync6(pkgPath, "utf-8"));
908
+ return JSON.parse(readFileSync7(pkgPath, "utf-8"));
860
909
  } catch {
861
910
  return void 0;
862
911
  }
@@ -1016,7 +1065,7 @@ function detectLicense(root, fm) {
1016
1065
  for (const name of names) {
1017
1066
  const p = join9(root, name);
1018
1067
  if (existsSync8(p) && statSync4(p).isFile()) {
1019
- const head = readFileSync6(p, "utf-8").slice(0, 2e3).toLowerCase();
1068
+ const head = readFileSync7(p, "utf-8").slice(0, 2e3).toLowerCase();
1020
1069
  for (const [hint, label] of Object.entries(SPDX_HINTS)) {
1021
1070
  if (head.includes(hint)) return label;
1022
1071
  }
@@ -1055,7 +1104,7 @@ function scanMcpAndImports(text, root) {
1055
1104
  if (isDir(scriptsDir)) {
1056
1105
  for (const f of walkFiles(scriptsDir)) {
1057
1106
  if (!f.endsWith(".py")) continue;
1058
- const ft = readFileSync6(f, "utf-8");
1107
+ const ft = readFileSync7(f, "utf-8");
1059
1108
  for (const m2 of ft.match(mcpRefRe) ?? []) mcpRefs.add(m2);
1060
1109
  const importRe = /^\s*(?:import|from)\s+([a-zA-Z0-9_]+)/gm;
1061
1110
  let m;
@@ -1072,7 +1121,7 @@ function scanMcpAndImports(text, root) {
1072
1121
  function profileSkillDir(inputPath) {
1073
1122
  const { skillMd, root, tmpDir } = resolveSkillMd(inputPath);
1074
1123
  try {
1075
- const text = readFileSync6(skillMd, "utf-8");
1124
+ const text = readFileSync7(skillMd, "utf-8");
1076
1125
  const fm = parseFrontmatter(text);
1077
1126
  const bodyLines = (text.match(/\n/g)?.length ?? 0) + 1;
1078
1127
  const headers = Array.from(text.matchAll(/^#{1,3}\s+(.+)$/gm)).map((m) => m[1]);
@@ -1103,7 +1152,7 @@ function profilePlugin(p) {
1103
1152
  const manifestPath = join9(p, ".claude-plugin", "plugin.json");
1104
1153
  let manifest;
1105
1154
  try {
1106
- manifest = JSON.parse(readFileSync6(manifestPath, "utf-8"));
1155
+ manifest = JSON.parse(readFileSync7(manifestPath, "utf-8"));
1107
1156
  } catch (err) {
1108
1157
  throw new Error(`could not parse plugin.json under ${p}: ${err.message}`);
1109
1158
  }
@@ -1134,7 +1183,7 @@ function profileMcp(p) {
1134
1183
  if (explicitConfig) {
1135
1184
  let cfg;
1136
1185
  try {
1137
- cfg = JSON.parse(readFileSync6(explicitConfig, "utf-8"));
1186
+ cfg = JSON.parse(readFileSync7(explicitConfig, "utf-8"));
1138
1187
  } catch (err) {
1139
1188
  throw new Error(`could not parse MCP config ${explicitConfig}: ${err.message}`);
1140
1189
  }
@@ -1153,7 +1202,7 @@ function profileMcp(p) {
1153
1202
  const pkgPath = join9(p, "package.json");
1154
1203
  let pkg2;
1155
1204
  try {
1156
- pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
1205
+ pkg2 = JSON.parse(readFileSync7(pkgPath, "utf-8"));
1157
1206
  } catch (err) {
1158
1207
  throw new Error(`could not parse package.json ${pkgPath}: ${err.message}`);
1159
1208
  }
@@ -1311,7 +1360,7 @@ async function scanSafety(targetPath, strictness = DEFAULT_STRICTNESS) {
1311
1360
  }
1312
1361
 
1313
1362
  // src/gate/overlap.ts
1314
- import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
1363
+ import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "fs";
1315
1364
  import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve3 } from "path";
1316
1365
  var STOPWORDS = new Set(
1317
1366
  `
@@ -1321,20 +1370,20 @@ skill skills user claude code based comprehensive enhanced practical including i
1321
1370
  `.split(/\s+/).filter(Boolean)
1322
1371
  );
1323
1372
  var DEFAULT_SET_THRESHOLD = 0.45;
1324
- function splitLimited(text, sep2, maxSplit) {
1373
+ function splitLimited(text, sep3, maxSplit) {
1325
1374
  const parts = [];
1326
1375
  let rest = text;
1327
1376
  for (let i = 0; i < maxSplit; i++) {
1328
- const idx = rest.indexOf(sep2);
1377
+ const idx = rest.indexOf(sep3);
1329
1378
  if (idx === -1) break;
1330
1379
  parts.push(rest.slice(0, idx));
1331
- rest = rest.slice(idx + sep2.length);
1380
+ rest = rest.slice(idx + sep3.length);
1332
1381
  }
1333
1382
  parts.push(rest);
1334
1383
  return parts;
1335
1384
  }
1336
1385
  function readFrontmatter(skillMdPath) {
1337
- const text = readFileSync7(skillMdPath, "utf-8");
1386
+ const text = readFileSync8(skillMdPath, "utf-8");
1338
1387
  const meta = { name: basename4(dirname4(skillMdPath)), description: "" };
1339
1388
  if (text.startsWith("---\n")) {
1340
1389
  const block = splitLimited(text, "---", 2)[1] ?? "";
@@ -1705,14 +1754,14 @@ async function mcpScan(targetPath, strictness = DEFAULT_STRICTNESS) {
1705
1754
  }
1706
1755
 
1707
1756
  // src/gate/mcpOverlap.ts
1708
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
1757
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
1709
1758
  import { resolve as resolve4 } from "path";
1710
1759
  function readTargetServers(targetPath) {
1711
1760
  const path = resolve4(expandHome(targetPath));
1712
1761
  if (!existsSync10(path)) return [];
1713
1762
  let cfg;
1714
1763
  try {
1715
- cfg = JSON.parse(readFileSync8(path, "utf-8"));
1764
+ cfg = JSON.parse(readFileSync9(path, "utf-8"));
1716
1765
  } catch {
1717
1766
  return [];
1718
1767
  }
@@ -2020,7 +2069,7 @@ async function runGate(candidatePath, skillsRoot, {
2020
2069
 
2021
2070
  // src/queue.ts
2022
2071
  import { randomUUID as randomUUID2 } from "crypto";
2023
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
2072
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync10, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
2024
2073
  import { join as join13 } from "path";
2025
2074
  function getQueuePath() {
2026
2075
  return join13(getHomeDir(), "queue.json");
@@ -2030,26 +2079,46 @@ function readQueue() {
2030
2079
  if (!existsSync11(path)) {
2031
2080
  return { version: 1, entries: [] };
2032
2081
  }
2033
- const raw = readFileSync9(path, "utf-8");
2082
+ const raw = readFileSync10(path, "utf-8");
2034
2083
  const parsed = JSON.parse(raw);
2035
2084
  return { version: 1, entries: parsed.entries ?? [] };
2036
2085
  }
2037
- function writeQueue(queue) {
2038
- mkdirSync5(getHomeDir(), { recursive: true });
2039
- writeFileSync4(getQueuePath(), JSON.stringify(queue, null, 2) + "\n", "utf-8");
2086
+ function writeQueue(queue2) {
2087
+ const home = getHomeDir();
2088
+ mkdirSync5(home, { recursive: true });
2089
+ const finalPath = getQueuePath();
2090
+ const tempPath = join13(home, `.queue.json.tmp-${randomUUID2()}`);
2091
+ try {
2092
+ writeFileSync4(tempPath, JSON.stringify(queue2, null, 2) + "\n", "utf-8");
2093
+ renameSync2(tempPath, finalPath);
2094
+ } catch (err) {
2095
+ rmSync3(tempPath, { force: true });
2096
+ throw err;
2097
+ }
2040
2098
  }
2041
2099
  function appendQueueEntry(entry) {
2042
- const queue = readQueue();
2043
- const existingIds = new Set(queue.entries.map((e) => e.id));
2100
+ const queue2 = readQueue();
2101
+ const existingIds = new Set(queue2.entries.map((e) => e.id));
2044
2102
  const full = {
2045
2103
  ...entry,
2046
2104
  id: generateId(existingIds),
2047
2105
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2048
2106
  };
2049
- queue.entries.push(full);
2050
- writeQueue(queue);
2107
+ queue2.entries.push(full);
2108
+ writeQueue(queue2);
2051
2109
  return full;
2052
2110
  }
2111
+ function updateQueueEntry(id, patch) {
2112
+ const queue2 = readQueue();
2113
+ const index = queue2.entries.findIndex((e) => e.id === id);
2114
+ if (index === -1) {
2115
+ throw new Error(`no queue entry with id: ${id}`);
2116
+ }
2117
+ const updated = { ...queue2.entries[index], ...patch };
2118
+ queue2.entries[index] = updated;
2119
+ writeQueue(queue2);
2120
+ return updated;
2121
+ }
2053
2122
  function generateId(existingIds) {
2054
2123
  let id;
2055
2124
  do {
@@ -2376,15 +2445,15 @@ import {
2376
2445
  existsSync as existsSync13,
2377
2446
  lstatSync,
2378
2447
  mkdtempSync as mkdtempSync2,
2379
- readFileSync as readFileSync10,
2448
+ readFileSync as readFileSync11,
2380
2449
  readdirSync as readdirSync6,
2381
2450
  realpathSync,
2382
- renameSync as renameSync2,
2383
- rmSync as rmSync3,
2451
+ renameSync as renameSync3,
2452
+ rmSync as rmSync4,
2384
2453
  statSync as statSync8,
2385
2454
  writeFileSync as writeFileSync5
2386
2455
  } from "fs";
2387
- import { basename as basename6, extname as extname5, join as join16, resolve as resolvePath2 } from "path";
2456
+ import { basename as basename6, delimiter as delimiter2, extname as extname5, join as join16, resolve as resolvePath2 } from "path";
2388
2457
  var OVERSIZED_SKILL_EST_TOKENS = 5e3;
2389
2458
  function toAuditFindings(source) {
2390
2459
  return source.map((f) => ({ severity: f.severity, rule: f.rule, target: f.file, detail: f.detail }));
@@ -2545,7 +2614,7 @@ function profileAuditedSkill(group, findings, unreadableSkillMdPaths) {
2545
2614
  let sizeBytes;
2546
2615
  try {
2547
2616
  sizeBytes = statSync8(skillMdPath).size;
2548
- text = readFileSync10(skillMdPath, "utf-8");
2617
+ text = readFileSync11(skillMdPath, "utf-8");
2549
2618
  } catch (err) {
2550
2619
  findings.push({
2551
2620
  severity: "MEDIUM",
@@ -2623,7 +2692,7 @@ function auditMcpTarget(targetPath, notices) {
2623
2692
  }
2624
2693
  if (isToml) {
2625
2694
  try {
2626
- readFileSync10(targetPath, "utf-8");
2695
+ readFileSync11(targetPath, "utf-8");
2627
2696
  } catch (err) {
2628
2697
  findings.push({
2629
2698
  severity: "MEDIUM",
@@ -2638,7 +2707,7 @@ function auditMcpTarget(targetPath, notices) {
2638
2707
  }
2639
2708
  let raw;
2640
2709
  try {
2641
- raw = readFileSync10(targetPath, "utf-8");
2710
+ raw = readFileSync11(targetPath, "utf-8");
2642
2711
  } catch (err) {
2643
2712
  findings.push({
2644
2713
  severity: "MEDIUM",
@@ -2672,7 +2741,7 @@ function auditMcpTarget(targetPath, notices) {
2672
2741
  }
2673
2742
  function clusterOverlapPairs(pairs) {
2674
2743
  const parent = /* @__PURE__ */ new Map();
2675
- function find(x) {
2744
+ function find2(x) {
2676
2745
  if (!parent.has(x)) parent.set(x, x);
2677
2746
  let root = x;
2678
2747
  while (parent.get(root) !== root) root = parent.get(root);
@@ -2685,14 +2754,14 @@ function clusterOverlapPairs(pairs) {
2685
2754
  return root;
2686
2755
  }
2687
2756
  function union(a, b) {
2688
- const ra = find(a);
2689
- const rb = find(b);
2757
+ const ra = find2(a);
2758
+ const rb = find2(b);
2690
2759
  if (ra !== rb) parent.set(ra, rb);
2691
2760
  }
2692
2761
  for (const pair of pairs) union(pair.aPath, pair.bPath);
2693
2762
  const groups = /* @__PURE__ */ new Map();
2694
2763
  for (const pair of pairs) {
2695
- const root = find(pair.aPath);
2764
+ const root = find2(pair.aPath);
2696
2765
  const group = groups.get(root) ?? { members: /* @__PURE__ */ new Set(), topScore: 0 };
2697
2766
  group.members.add(pair.aPath);
2698
2767
  group.members.add(pair.bPath);
@@ -2708,6 +2777,45 @@ function toClusterMembers(members) {
2708
2777
  }
2709
2778
  return sorted;
2710
2779
  }
2780
+ function defaultWhich(cmd) {
2781
+ const pathEnv = process.env.PATH ?? "";
2782
+ const dirs = pathEnv.split(delimiter2).filter(Boolean);
2783
+ const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
2784
+ for (const dir of dirs) {
2785
+ for (const ext of exts) {
2786
+ const candidate = join16(dir, cmd + ext);
2787
+ try {
2788
+ if (statSync8(candidate).isFile()) return candidate;
2789
+ } catch {
2790
+ }
2791
+ }
2792
+ }
2793
+ return void 0;
2794
+ }
2795
+ var SKILLSPECTOR_PROVIDER_KEYMAP = {
2796
+ openai: "OPENAI_API_KEY",
2797
+ anthropic: "ANTHROPIC_API_KEY",
2798
+ nv_build: "NVIDIA_INFERENCE_KEY"
2799
+ };
2800
+ function checkTooling(which2, env) {
2801
+ const skillspectorPath = which2("skillspector");
2802
+ const skillspector = skillspectorPath ? { installed: true } : {
2803
+ installed: false,
2804
+ hint: "NOT installed \u2014 github.com/NVIDIA/skillspector (git clone + make install; Python 3.12+)"
2805
+ };
2806
+ const tokenPresent = Boolean(env.VERCEL_OIDC_TOKEN);
2807
+ const skillsShToken = tokenPresent ? { present: true } : {
2808
+ present: false,
2809
+ hint: "missing \u2014 enable OIDC Federation on your Vercel project, then `vercel link && vercel env pull`"
2810
+ };
2811
+ const provider = env.SKILLSPECTOR_PROVIDER;
2812
+ const providerKeyEnv = provider ? SKILLSPECTOR_PROVIDER_KEYMAP[provider] : void 0;
2813
+ const skillspectorLlm = {
2814
+ configured: Boolean(provider && providerKeyEnv && env[providerKeyEnv]),
2815
+ provider: provider || void 0
2816
+ };
2817
+ return { skillspector, skillsShToken, skillspectorLlm };
2818
+ }
2711
2819
  async function auditConfiguredSet(config, options = {}) {
2712
2820
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
2713
2821
  const oversizedThreshold = options.oversizedSkillTokens ?? OVERSIZED_SKILL_EST_TOKENS;
@@ -2828,12 +2936,14 @@ async function auditConfiguredSet(config, options = {}) {
2828
2936
  overlapClusterCount: overlapClusters.length,
2829
2937
  evolveEligibleCount: evolveEligible.length
2830
2938
  };
2939
+ const tooling = checkTooling(options.which ?? defaultWhich, options.env ?? process.env);
2831
2940
  return {
2832
2941
  schemaVersion: 1,
2833
2942
  generatedAt: now().toISOString(),
2834
2943
  inventory: { skillRoots, skills, mcpTargets },
2835
2944
  hygiene: findings,
2836
2945
  opportunities: { overlapClusters, evolveEligible, foundation, overlapLocked: !overlapStatus.enabled },
2946
+ tooling,
2837
2947
  notices,
2838
2948
  summary
2839
2949
  };
@@ -2927,6 +3037,28 @@ function renderOpportunitiesSection(report) {
2927
3037
  `- ${foundation.message}`
2928
3038
  ];
2929
3039
  }
3040
+ function renderToolingSection(tooling) {
3041
+ const lines = ["## Tooling", ""];
3042
+ if (!tooling) {
3043
+ lines.push("- (not available in this report)");
3044
+ return lines;
3045
+ }
3046
+ lines.push(`- SkillSpector CLI: ${tooling.skillspector.installed ? "installed" : "NOT installed"}`);
3047
+ if (tooling.skillspector.hint) lines.push(` - ${tooling.skillspector.hint}`);
3048
+ lines.push(`- skills.sh auth (VERCEL_OIDC_TOKEN): ${tooling.skillsShToken.present ? "present" : "missing"}`);
3049
+ if (tooling.skillsShToken.hint) lines.push(` - ${tooling.skillsShToken.hint}`);
3050
+ const llm = tooling.skillspectorLlm;
3051
+ if (llm.configured) {
3052
+ lines.push(`- SkillSpector LLM stage (optional): configured (provider: ${sanitizeForMarkdown(llm.provider ?? "")})`);
3053
+ } else if (llm.provider) {
3054
+ lines.push(`- SkillSpector LLM stage (optional): not configured \u2014 SKILLSPECTOR_PROVIDER is "${sanitizeForMarkdown(llm.provider)}" but its API key env var is not set`);
3055
+ } else {
3056
+ lines.push(
3057
+ "- SkillSpector LLM stage (optional): not set \u2014 static scan works without it (use --no-llm); set SKILLSPECTOR_PROVIDER + its API key for semantic analysis"
3058
+ );
3059
+ }
3060
+ return lines;
3061
+ }
2930
3062
  function renderAuditMarkdown(report) {
2931
3063
  const { summary } = report;
2932
3064
  const lines = [
@@ -2953,6 +3085,8 @@ function renderAuditMarkdown(report) {
2953
3085
  "",
2954
3086
  ...renderOpportunitiesSection(report),
2955
3087
  "",
3088
+ ...renderToolingSection(report.tooling),
3089
+ "",
2956
3090
  "## Notices",
2957
3091
  "",
2958
3092
  ...bulletList(report.notices, "(none)")
@@ -3024,9 +3158,9 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
3024
3158
  `refusing to scaffold a foundation skill: the generated SKILL.md was flagged by the safety scan (${safety.findings.map(formatSafetyFinding).join("; ")})`
3025
3159
  );
3026
3160
  }
3027
- renameSync2(tempDir, destDir);
3161
+ renameSync3(tempDir, destDir);
3028
3162
  } catch (err) {
3029
- rmSync3(tempDir, { recursive: true, force: true });
3163
+ rmSync4(tempDir, { recursive: true, force: true });
3030
3164
  throw err;
3031
3165
  }
3032
3166
  try {
@@ -3664,7 +3798,7 @@ async function status() {
3664
3798
  // src/evolve.ts
3665
3799
  import { createInterface as createInterface4 } from "readline/promises";
3666
3800
  import { createHash } from "crypto";
3667
- import { cpSync as cpSync3, existsSync as existsSync16, lstatSync as lstatSync2, mkdtempSync as mkdtempSync3, readFileSync as readFileSync11, rmSync as rmSync4 } from "fs";
3801
+ import { cpSync as cpSync3, existsSync as existsSync16, lstatSync as lstatSync2, mkdtempSync as mkdtempSync3, readFileSync as readFileSync12, rmSync as rmSync5 } from "fs";
3668
3802
  import { tmpdir as tmpdir2 } from "os";
3669
3803
  import { basename as basename7, dirname as dirname7, join as join20, resolve as resolvePath4, sep } from "path";
3670
3804
  var SKILLOPT_BIN = "skillopt-sleep";
@@ -3765,7 +3899,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
3765
3899
  safety = await scanSafety(tempDir, strictness);
3766
3900
  } finally {
3767
3901
  try {
3768
- rmSync4(tempDir, { recursive: true, force: true });
3902
+ rmSync5(tempDir, { recursive: true, force: true });
3769
3903
  } catch (err) {
3770
3904
  console.error(`[skill-forge] failed to clean up temp re-gate dir ${tempDir}: ${err.message}`);
3771
3905
  }
@@ -3847,7 +3981,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
3847
3981
  logOutcome("[skill-forge] queued for ingestion review", json);
3848
3982
  } else if (decision === "reject") {
3849
3983
  try {
3850
- rmSync4(stagingDir, { recursive: true, force: true });
3984
+ rmSync5(stagingDir, { recursive: true, force: true });
3851
3985
  } catch (err) {
3852
3986
  throw new UserError(
3853
3987
  `rejected the proposal, but removing the staging dir ${stagingDir} failed: ${err.message} \u2014 remove it manually.`
@@ -3902,7 +4036,7 @@ function readManifest(stagingDir) {
3902
4036
  const manifestPath = join20(stagingDir, "manifest.json");
3903
4037
  let raw;
3904
4038
  try {
3905
- raw = readFileSync11(manifestPath, "utf-8");
4039
+ raw = readFileSync12(manifestPath, "utf-8");
3906
4040
  } catch (err) {
3907
4041
  throw new UserError(`could not read staging manifest ${manifestPath}: ${err.message}`);
3908
4042
  }
@@ -3937,7 +4071,7 @@ function validateManifest(raw, manifestPath) {
3937
4071
  }
3938
4072
  function filesIdentical(a, b) {
3939
4073
  if (!existsSync16(a) || !existsSync16(b)) return false;
3940
- return readFileSync11(a).equals(readFileSync11(b));
4074
+ return readFileSync12(a).equals(readFileSync12(b));
3941
4075
  }
3942
4076
  function isAlreadyAdopted(stagingDir, manifest) {
3943
4077
  const proposedSkillMd = join20(stagingDir, "proposed_SKILL.md");
@@ -3966,7 +4100,7 @@ function assertNotSymlink(path, label) {
3966
4100
  }
3967
4101
  function sha256File(path) {
3968
4102
  if (!existsSync16(path)) return null;
3969
- return createHash("sha256").update(readFileSync11(path)).digest("hex");
4103
+ return createHash("sha256").update(readFileSync12(path)).digest("hex");
3970
4104
  }
3971
4105
 
3972
4106
  // src/commands/evolve.ts
@@ -3974,19 +4108,2440 @@ async function evolveCommand(targetSkillDir, options = {}) {
3974
4108
  await evolve(targetSkillDir, options);
3975
4109
  }
3976
4110
 
3977
- // src/cli.ts
3978
- var require2 = createRequire(import.meta.url);
3979
- var pkg = require2("../package.json");
3980
- function parseArtifact(value) {
3981
- if (value !== "skill" && value !== "mcp") {
3982
- throw new UserError(`--artifact must be "skill" or "mcp" (got "${value}")`);
4111
+ // src/commands/organize.ts
4112
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
4113
+ import { dirname as dirname8, resolve as resolvePath6 } from "path";
4114
+
4115
+ // src/organize.ts
4116
+ import { existsSync as existsSync17, readFileSync as readFileSync13, readdirSync as readdirSync7, realpathSync as realpathSync2, statSync as statSync9 } from "fs";
4117
+ import { basename as basename8, join as join21, resolve as resolvePath5 } from "path";
4118
+ var PRO_FEATURE_LABEL3 = "set-level organizer reports";
4119
+ var TIERS = /* @__PURE__ */ new Set(["resource", "custom"]);
4120
+ var MATURITIES = /* @__PURE__ */ new Set(["seedling", "stable", "deprecated"]);
4121
+ function listSubdirs2(dir) {
4122
+ let entries;
4123
+ try {
4124
+ entries = readdirSync7(dir, { withFileTypes: true });
4125
+ } catch {
4126
+ return [];
3983
4127
  }
3984
- return value;
4128
+ const out = [];
4129
+ for (const entry of entries) {
4130
+ const full = join21(dir, entry.name);
4131
+ if (entry.isDirectory()) {
4132
+ out.push(full);
4133
+ continue;
4134
+ }
4135
+ if (entry.isSymbolicLink()) {
4136
+ try {
4137
+ if (statSync9(full).isDirectory()) out.push(full);
4138
+ } catch {
4139
+ }
4140
+ }
4141
+ }
4142
+ return out;
3985
4143
  }
3986
- function parseLookbackHours(value) {
3987
- const n = Number(value);
3988
- if (value.trim() === "" || !Number.isFinite(n) || n < 0) {
3989
- throw new UserError(`--lookback-hours must be a non-negative number (got "${value}")`);
4144
+ function findSkillDirsUnderRoot2(root) {
4145
+ const found = /* @__PURE__ */ new Set();
4146
+ const topDirs = listSubdirs2(root);
4147
+ for (const dir of topDirs) {
4148
+ if (existsSync17(join21(dir, "SKILL.md"))) found.add(dir);
4149
+ }
4150
+ for (const dir of listSubdirs2(join21(root, "skills"))) {
4151
+ if (existsSync17(join21(dir, "SKILL.md"))) found.add(dir);
4152
+ }
4153
+ for (const topDir of topDirs) {
4154
+ for (const dir of listSubdirs2(join21(topDir, "skills"))) {
4155
+ if (existsSync17(join21(dir, "SKILL.md"))) found.add(dir);
4156
+ }
4157
+ }
4158
+ return Array.from(found).filter((p) => !p.includes("/.git/"));
4159
+ }
4160
+ function collectCandidates(roots) {
4161
+ const out = [];
4162
+ for (const root of roots) {
4163
+ const resolvedRoot = resolvePath5(expandHome(root));
4164
+ if (!isDir(resolvedRoot)) continue;
4165
+ for (const dirPath of findSkillDirsUnderRoot2(resolvedRoot)) {
4166
+ out.push({ dirPath, root });
4167
+ }
4168
+ }
4169
+ return out;
4170
+ }
4171
+ function parseYamlList(block, key) {
4172
+ const inlineMatch = block.match(new RegExp(`^${key}:\\s*\\[(.*?)\\]\\s*$`, "m"));
4173
+ if (inlineMatch) {
4174
+ return inlineMatch[1].split(",").map((x) => stripQuotes(x.trim())).filter(Boolean);
4175
+ }
4176
+ if (!new RegExp(`^${key}:\\s*$`, "m").test(block)) return [];
4177
+ const lines = block.split(/\r\n|\r|\n/);
4178
+ const startIdx = lines.findIndex((l) => new RegExp(`^${key}:\\s*$`).test(l));
4179
+ if (startIdx === -1) return [];
4180
+ const out = [];
4181
+ for (const line of lines.slice(startIdx + 1)) {
4182
+ const itemMatch = line.match(/^\s*-\s*(.+)$/);
4183
+ if (itemMatch) {
4184
+ out.push(stripQuotes(itemMatch[1].trim()));
4185
+ } else if (line.trim() === "") {
4186
+ continue;
4187
+ } else {
4188
+ break;
4189
+ }
4190
+ }
4191
+ return out;
4192
+ }
4193
+ function extractFrontmatterBlock(text) {
4194
+ if (!text.startsWith("---\n")) return null;
4195
+ const afterOpen = text.slice(4);
4196
+ const closeIdx = afterOpen.indexOf("\n---");
4197
+ if (closeIdx === -1) return null;
4198
+ return afterOpen.slice(0, closeIdx);
4199
+ }
4200
+ function parseCapabilityFrontmatter(text, fallbackName) {
4201
+ const block = extractFrontmatterBlock(text);
4202
+ if (block === null) {
4203
+ return { name: fallbackName, consumes: [], validFrontmatter: false };
4204
+ }
4205
+ const fields = {};
4206
+ for (const key of ["name", "tier", "domain", "provenance", "maturity"]) {
4207
+ const m = block.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
4208
+ if (m) fields[key] = stripQuotes(m[1].trim());
4209
+ }
4210
+ return {
4211
+ name: fields.name ?? fallbackName,
4212
+ tier: fields.tier,
4213
+ domain: fields.domain,
4214
+ provenance: fields.provenance,
4215
+ maturity: fields.maturity,
4216
+ consumes: parseYamlList(block, "consumes"),
4217
+ validFrontmatter: true
4218
+ };
4219
+ }
4220
+ function loadUsageSnapshot(snapshotPath) {
4221
+ const resolvedPath = resolvePath5(expandHome(snapshotPath));
4222
+ let raw;
4223
+ try {
4224
+ raw = readFileSync13(resolvedPath, "utf-8");
4225
+ } catch (err) {
4226
+ return { usage: /* @__PURE__ */ new Map(), warning: `could not read usage snapshot (${err.message}); proceeding without usage` };
4227
+ }
4228
+ let parsed;
4229
+ try {
4230
+ parsed = JSON.parse(raw);
4231
+ } catch (err) {
4232
+ return { usage: /* @__PURE__ */ new Map(), warning: `could not parse usage snapshot (${err.message}); proceeding without usage` };
4233
+ }
4234
+ const usage = /* @__PURE__ */ new Map();
4235
+ const outer = parsed;
4236
+ const report = outer && typeof outer === "object" ? outer.report : void 0;
4237
+ const topSkills = report && typeof report === "object" ? report.top_skills : void 0;
4238
+ if (Array.isArray(topSkills)) {
4239
+ for (const row of topSkills) {
4240
+ if (Array.isArray(row) && row.length === 2 && typeof row[0] === "string" && typeof row[1] === "number") {
4241
+ const [name, count] = row;
4242
+ usage.set(name, count);
4243
+ if (name.includes(":")) {
4244
+ const bare = name.split(":").pop();
4245
+ if (!usage.has(bare)) usage.set(bare, count);
4246
+ }
4247
+ }
4248
+ }
4249
+ }
4250
+ return { usage };
4251
+ }
4252
+ var WHITE = 0;
4253
+ var GRAY = 1;
4254
+ var BLACK = 2;
4255
+ function findCycles(adjacency) {
4256
+ const color = /* @__PURE__ */ new Map();
4257
+ for (const id of adjacency.keys()) color.set(id, WHITE);
4258
+ const stack = [];
4259
+ const cycles = [];
4260
+ function dfs(u) {
4261
+ color.set(u, GRAY);
4262
+ stack.push(u);
4263
+ for (const v of adjacency.get(u) ?? []) {
4264
+ if (!color.has(v)) continue;
4265
+ if (color.get(v) === GRAY && stack.includes(v)) {
4266
+ const idx = stack.indexOf(v);
4267
+ cycles.push([...stack.slice(idx), v]);
4268
+ } else if (color.get(v) === WHITE) {
4269
+ dfs(v);
4270
+ }
4271
+ }
4272
+ color.set(u, BLACK);
4273
+ stack.pop();
4274
+ }
4275
+ for (const id of adjacency.keys()) {
4276
+ if (color.get(id) === WHITE) dfs(id);
4277
+ }
4278
+ return cycles;
4279
+ }
4280
+ function buildGraph(entries, nameToIds) {
4281
+ const nodes = entries.map((e) => ({ id: e.id, name: e.name, tier: e.tier, domain: e.domain }));
4282
+ const edges = [];
4283
+ const dangling = [];
4284
+ const ambiguous = [];
4285
+ const incoming = new Map(entries.map((e) => [e.id, 0]));
4286
+ const adjacency = new Map(entries.map((e) => [e.id, []]));
4287
+ for (const e of entries) {
4288
+ for (const target of e.consumes) {
4289
+ const matches = nameToIds.get(target) ?? [];
4290
+ edges.push({ from: e.id, to: target, resolvedIds: matches });
4291
+ if (matches.length === 0) {
4292
+ dangling.push({ from: e.id, to: target });
4293
+ } else if (matches.length === 1) {
4294
+ incoming.set(matches[0], (incoming.get(matches[0]) ?? 0) + 1);
4295
+ adjacency.get(e.id).push(matches[0]);
4296
+ } else {
4297
+ ambiguous.push({ from: e.id, to: target, matches });
4298
+ }
4299
+ }
4300
+ }
4301
+ const orphans = entries.filter((e) => e.tier === "custom" && e.consumes.length === 0).map((e) => e.id);
4302
+ const byId = new Map(entries.map((e) => [e.id, e]));
4303
+ const hotResources = Array.from(incoming.entries()).filter(([, count]) => count > 0).map(([id, count]) => ({ id, name: byId.get(id).name, consumedBy: count })).sort((a, b) => b.consumedBy - a.consumedBy);
4304
+ const cycles = findCycles(adjacency);
4305
+ return {
4306
+ nodeCount: nodes.length,
4307
+ edgeCount: edges.length,
4308
+ nodes,
4309
+ edges,
4310
+ orphans,
4311
+ hotResources,
4312
+ danglingEdges: dangling,
4313
+ ambiguousConsumesTargets: ambiguous,
4314
+ cycles
4315
+ };
4316
+ }
4317
+ function buildRegistryEntries(candidates, warnings) {
4318
+ const entries = [];
4319
+ const seenRealPaths = /* @__PURE__ */ new Set();
4320
+ for (const candidate of candidates) {
4321
+ let realPath;
4322
+ try {
4323
+ realPath = realpathSync2(candidate.dirPath);
4324
+ } catch (err) {
4325
+ warnings.push(`could not resolve skill directory ${candidate.dirPath}: ${err.message}`);
4326
+ continue;
4327
+ }
4328
+ if (seenRealPaths.has(realPath)) continue;
4329
+ seenRealPaths.add(realPath);
4330
+ const skillMdPath = join21(realPath, "SKILL.md");
4331
+ let text;
4332
+ try {
4333
+ text = readFileSync13(skillMdPath, "utf-8");
4334
+ } catch (err) {
4335
+ warnings.push(`could not read ${skillMdPath}: ${err.message}`);
4336
+ continue;
4337
+ }
4338
+ const fm = parseCapabilityFrontmatter(text, basename8(realPath));
4339
+ const name = fm.name ?? basename8(realPath);
4340
+ if (fm.tier && !TIERS.has(fm.tier)) {
4341
+ warnings.push(`${name}: tier '${fm.tier}' not in ${JSON.stringify(Array.from(TIERS).sort())}`);
4342
+ }
4343
+ if (fm.maturity && !MATURITIES.has(fm.maturity)) {
4344
+ warnings.push(`${name}: maturity '${fm.maturity}' not in ${JSON.stringify(Array.from(MATURITIES).sort())}`);
4345
+ }
4346
+ entries.push({
4347
+ id: realPath,
4348
+ name,
4349
+ path: realPath,
4350
+ root: candidate.root,
4351
+ tier: fm.tier,
4352
+ domain: fm.domain,
4353
+ consumes: fm.consumes,
4354
+ provenance: fm.provenance,
4355
+ maturity: fm.maturity,
4356
+ validFrontmatter: fm.validFrontmatter
4357
+ });
4358
+ }
4359
+ return entries;
4360
+ }
4361
+ function joinUsageSnapshot(entries, snapshotPath, warnings) {
4362
+ const { usage, warning } = loadUsageSnapshot(snapshotPath);
4363
+ if (warning) warnings.push(warning);
4364
+ if (usage.size === 0) return false;
4365
+ for (const entry of entries) {
4366
+ const count = usage.get(entry.name);
4367
+ if (count !== void 0) entry.usage = count;
4368
+ }
4369
+ if (!entries.some((e) => e.usage !== void 0)) {
4370
+ warnings.push(
4371
+ "usage snapshot loaded but 0 skills matched by name \u2014 registry `name:` vs telemetry key mismatch; the usage column will be empty"
4372
+ );
4373
+ }
4374
+ return true;
4375
+ }
4376
+ function buildNameToIds(entries, warnings) {
4377
+ const nameToIds = /* @__PURE__ */ new Map();
4378
+ for (const e of entries) {
4379
+ const ids = nameToIds.get(e.name) ?? [];
4380
+ ids.push(e.id);
4381
+ nameToIds.set(e.name, ids);
4382
+ }
4383
+ for (const [name, ids] of nameToIds) {
4384
+ if (ids.length > 1) {
4385
+ warnings.push(`duplicate skill name "${name}" across ${ids.length} locations: ${ids.join(", ")}`);
4386
+ }
4387
+ }
4388
+ return nameToIds;
4389
+ }
4390
+ function findUntagged(entries) {
4391
+ return entries.filter((e) => !e.tier || !e.domain).map((e) => ({
4392
+ id: e.id,
4393
+ name: e.name,
4394
+ missing: [!e.tier ? "tier" : void 0, !e.domain ? "domain" : void 0].filter((v) => Boolean(v))
4395
+ }));
4396
+ }
4397
+ function countByTierAndDomain(entries) {
4398
+ const byTier = {};
4399
+ const byDomain = {};
4400
+ for (const e of entries) {
4401
+ byTier[e.tier ?? "untagged"] = (byTier[e.tier ?? "untagged"] ?? 0) + 1;
4402
+ byDomain[e.domain ?? "untagged"] = (byDomain[e.domain ?? "untagged"] ?? 0) + 1;
4403
+ }
4404
+ return { byTier, byDomain };
4405
+ }
4406
+ function buildOrganizeReport(config, options = {}) {
4407
+ const proStatus = proFeatureStatus(PRO_FEATURE_LABEL3);
4408
+ if (!proStatus.enabled) {
4409
+ throw new UserError(proStatus.notice ?? upgradeNotice(PRO_FEATURE_LABEL3));
4410
+ }
4411
+ const notices = proStatus.notice ? [proStatus.notice] : [];
4412
+ const warnings = [];
4413
+ const candidates = collectCandidates(config.skillsRoots);
4414
+ if (candidates.length === 0) {
4415
+ throw new UserError(
4416
+ `no SKILL.md found under any configured skills root (${config.skillsRoots.join(", ") || "(none configured)"})`
4417
+ );
4418
+ }
4419
+ const entries = buildRegistryEntries(candidates, warnings);
4420
+ const usageJoined = options.usageSnapshot ? joinUsageSnapshot(entries, options.usageSnapshot, warnings) : false;
4421
+ const nameToIds = buildNameToIds(entries, warnings);
4422
+ const untagged = findUntagged(entries);
4423
+ const { byTier, byDomain } = countByTierAndDomain(entries);
4424
+ const registry = {
4425
+ skillsRoots: [...config.skillsRoots],
4426
+ count: entries.length,
4427
+ byTier,
4428
+ byDomain,
4429
+ untagged,
4430
+ warnings,
4431
+ usageJoined,
4432
+ skills: entries
4433
+ };
4434
+ const graph = buildGraph(entries, nameToIds);
4435
+ return {
4436
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4437
+ registry,
4438
+ graph,
4439
+ notices
4440
+ };
4441
+ }
4442
+
4443
+ // src/commands/organize.ts
4444
+ function writeReport2(outPath, contents) {
4445
+ const resolved = resolvePath6(outPath);
4446
+ mkdirSync7(dirname8(resolved), { recursive: true });
4447
+ try {
4448
+ writeFileSync7(resolved, contents, { flag: "wx", mode: 384 });
4449
+ } catch (err) {
4450
+ if (err.code === "EEXIST") {
4451
+ throw new UserError(`output file already exists: ${resolved} \u2014 pass a different --out path`);
4452
+ }
4453
+ throw err;
4454
+ }
4455
+ return resolved;
4456
+ }
4457
+ function renderSummary(report) {
4458
+ const { registry, graph } = report;
4459
+ const lines = [];
4460
+ lines.push(`[skill-forge] organize: ${registry.count} skill(s) across ${registry.skillsRoots.length} root(s)`);
4461
+ lines.push(` by tier: ${JSON.stringify(registry.byTier)}`);
4462
+ lines.push(` by domain: ${JSON.stringify(registry.byDomain)}`);
4463
+ lines.push(` usage joined: ${registry.usageJoined}`);
4464
+ lines.push(` untagged (missing tier/domain): ${registry.untagged.length}`);
4465
+ for (const u of registry.untagged.slice(0, 30)) {
4466
+ lines.push(` - ${sanitizeForMarkdown(u.name)}: missing ${u.missing.join(", ")}`);
4467
+ }
4468
+ if (registry.warnings.length > 0) {
4469
+ lines.push(" warnings:");
4470
+ for (const w of registry.warnings) lines.push(` ! ${sanitizeForMarkdown(w)}`);
4471
+ }
4472
+ lines.push("");
4473
+ lines.push(` graph: ${graph.nodeCount} nodes, ${graph.edgeCount} edges`);
4474
+ if (graph.hotResources.length > 0) {
4475
+ lines.push(" hot resources (consumed by N customs):");
4476
+ for (const h of graph.hotResources) lines.push(` ${h.consumedBy}x ${sanitizeForMarkdown(h.name)}`);
4477
+ }
4478
+ if (graph.danglingEdges.length > 0) {
4479
+ lines.push(" dangling edges (consumes a target not in the set):");
4480
+ for (const d of graph.danglingEdges) lines.push(` ${sanitizeForMarkdown(d.from)} -> ${sanitizeForMarkdown(d.to)} (missing)`);
4481
+ }
4482
+ if (graph.ambiguousConsumesTargets.length > 0) {
4483
+ lines.push(" ambiguous consumes targets (name matched multiple skills):");
4484
+ for (const a of graph.ambiguousConsumesTargets) {
4485
+ lines.push(
4486
+ ` ${sanitizeForMarkdown(a.from)} -> ${sanitizeForMarkdown(a.to)} (matches: ${a.matches.map(sanitizeForMarkdown).join(", ")})`
4487
+ );
4488
+ }
4489
+ }
4490
+ if (graph.cycles.length > 0) {
4491
+ lines.push(" cycles (consumes should be acyclic):");
4492
+ for (const c of graph.cycles) lines.push(` ${c.map(sanitizeForMarkdown).join(" -> ")}`);
4493
+ }
4494
+ if (graph.orphans.length > 0) {
4495
+ lines.push(` custom skills with no declared consumes (${graph.orphans.length}):`);
4496
+ for (const o of graph.orphans.slice(0, 30)) lines.push(` - ${sanitizeForMarkdown(o)}`);
4497
+ }
4498
+ for (const n of report.notices) lines.push(` [notice] ${sanitizeForMarkdown(n)}`);
4499
+ return lines;
4500
+ }
4501
+ async function organizeCommand(options = {}) {
4502
+ const config = loadConfig();
4503
+ const report = buildOrganizeReport(config, { usageSnapshot: options.usageSnapshot });
4504
+ let outPath;
4505
+ if (options.out) {
4506
+ outPath = writeReport2(options.out, JSON.stringify(report, null, 2) + "\n");
4507
+ }
4508
+ if (options.json) {
4509
+ for (const n of report.notices) console.error(`[skill-forge] ${n}`);
4510
+ console.log(JSON.stringify(report, null, 2));
4511
+ return;
4512
+ }
4513
+ for (const line of renderSummary(report)) console.log(line);
4514
+ if (outPath) console.log(` report -> ${outPath}`);
4515
+ }
4516
+
4517
+ // src/gate/skillsShApi.ts
4518
+ var SKILLS_SH_ORIGIN = "https://skills.sh";
4519
+ var SKILLS_SH_API_BASE = "/api/v1";
4520
+ var MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
4521
+ var REQUEST_TIMEOUT_MS = 15e3;
4522
+ var ID_PATTERN = /^[A-Za-z0-9._/-]+$/;
4523
+ var TOKEN_HINT = "No VERCEL_OIDC_TOKEN found. skills.sh authenticates with a short-lived Vercel OIDC token:\n 1. Vercel dashboard -> your project -> Settings -> OIDC Federation -> enable.\n 2. npm i -g vercel && vercel link && vercel env pull (writes VERCEL_OIDC_TOKEN to .env.local)\n 3. export VERCEL_OIDC_TOKEN from .env.local into your shell before running this command\n (or set it directly: export VERCEL_OIDC_TOKEN=...) - docs: https://skills.sh/docs/api";
4524
+ var SkillsShError = class extends Error {
4525
+ exitCode;
4526
+ constructor(message, exitCode) {
4527
+ super(message);
4528
+ this.name = "SkillsShError";
4529
+ this.exitCode = exitCode;
4530
+ }
4531
+ };
4532
+ var RISK_ORDER = { NONE: 0, LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 };
4533
+ function getToken(env = process.env) {
4534
+ const tok = env.VERCEL_OIDC_TOKEN;
4535
+ return tok ? tok : void 0;
4536
+ }
4537
+ function assertValidId(id) {
4538
+ if (id.length === 0 || !ID_PATTERN.test(id)) {
4539
+ throw new SkillsShError(`invalid skills.sh id: ${JSON.stringify(id)}`, 1);
4540
+ }
4541
+ }
4542
+ function validatedIdSegments(id) {
4543
+ assertValidId(id);
4544
+ const segments = id.split("/");
4545
+ if (segments.some((s) => s === "" || s === "." || s === "..")) {
4546
+ throw new SkillsShError(`invalid skills.sh id: ${JSON.stringify(id)}`, 1);
4547
+ }
4548
+ return segments;
4549
+ }
4550
+ function buildUrl(pathSegments, params) {
4551
+ const url = new URL(SKILLS_SH_API_BASE + "/" + pathSegments.map((s) => encodeURIComponent(s)).join("/"), SKILLS_SH_ORIGIN);
4552
+ if (params) {
4553
+ const search2 = new URLSearchParams();
4554
+ for (const [k, v] of Object.entries(params)) search2.set(k, String(v));
4555
+ url.search = search2.toString();
4556
+ }
4557
+ return url;
4558
+ }
4559
+ async function readCappedText(res) {
4560
+ const text = await res.text();
4561
+ if (new TextEncoder().encode(text).length > MAX_RESPONSE_BYTES) {
4562
+ throw new SkillsShError("skills.sh response exceeded the 2 MB size cap", 1);
4563
+ }
4564
+ return text;
4565
+ }
4566
+ async function call(pathSegments, params, options = {}) {
4567
+ const env = options.env ?? process.env;
4568
+ const token = getToken(env);
4569
+ if (!token) {
4570
+ throw new SkillsShError(TOKEN_HINT, 3);
4571
+ }
4572
+ const url = buildUrl(pathSegments, params);
4573
+ const fetchImpl = options.fetchImpl ?? fetch;
4574
+ let res;
4575
+ try {
4576
+ res = await fetchImpl(url, {
4577
+ headers: { Authorization: `Bearer ${token}` },
4578
+ redirect: "error",
4579
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
4580
+ });
4581
+ } catch (err) {
4582
+ const message = err instanceof Error ? err.message : String(err);
4583
+ throw new SkillsShError(`skills.sh request failed: ${message}`, 1);
4584
+ }
4585
+ if (res.status === 404) {
4586
+ return void 0;
4587
+ }
4588
+ if (!res.ok) {
4589
+ if (res.status === 401) {
4590
+ throw new SkillsShError("skills.sh HTTP 401: unauthorized \u2014 the VERCEL_OIDC_TOKEN was rejected (expired or invalid)", 3);
4591
+ }
4592
+ let snippet = "";
4593
+ try {
4594
+ snippet = (await readCappedText(res)).slice(0, 300);
4595
+ } catch {
4596
+ }
4597
+ throw new SkillsShError(`skills.sh HTTP ${res.status}${snippet ? `: ${snippet}` : ""}`, 1);
4598
+ }
4599
+ const text = await readCappedText(res);
4600
+ let parsed;
4601
+ try {
4602
+ parsed = JSON.parse(text);
4603
+ } catch {
4604
+ throw new SkillsShError("skills.sh returned malformed JSON", 1);
4605
+ }
4606
+ return parsed;
4607
+ }
4608
+ function isRecord(v) {
4609
+ return typeof v === "object" && v !== null && !Array.isArray(v);
4610
+ }
4611
+ async function search(query, limit, options = {}) {
4612
+ const data = await call(["skills", "search"], { q: query, limit }, options);
4613
+ if (!isRecord(data) || !Array.isArray(data.data)) {
4614
+ throw new SkillsShError("skills.sh search response had an unexpected shape", 1);
4615
+ }
4616
+ return data;
4617
+ }
4618
+ async function audit(id, options = {}) {
4619
+ const data = await call(["skills", "audit", ...validatedIdSegments(id)], void 0, options);
4620
+ if (data === void 0) return void 0;
4621
+ if (!isRecord(data) || !Array.isArray(data.audits)) {
4622
+ throw new SkillsShError("skills.sh audit response had an unexpected shape", 1);
4623
+ }
4624
+ return data;
4625
+ }
4626
+ async function get(id, options = {}) {
4627
+ return call(["skills", ...validatedIdSegments(id)], void 0, options);
4628
+ }
4629
+ async function curated(options = {}) {
4630
+ return call(["skills", "curated"], void 0, options);
4631
+ }
4632
+ function auditVerdict(data) {
4633
+ const audits = data.audits ?? [];
4634
+ const statuses = new Set(audits.map((a) => a.status));
4635
+ let worst = "NONE";
4636
+ let worstRank = 0;
4637
+ for (const a of audits) {
4638
+ const rl = a.riskLevel ?? "NONE";
4639
+ const rank = RISK_ORDER[rl] ?? 0;
4640
+ if (rank > worstRank) {
4641
+ worstRank = rank;
4642
+ worst = rl;
4643
+ }
4644
+ }
4645
+ const verdict = statuses.has("fail") ? "fail" : statuses.has("warn") ? "warn" : audits.length > 0 ? "pass" : "none";
4646
+ return { verdict, risk: worst, audits };
4647
+ }
4648
+
4649
+ // src/commands/find.ts
4650
+ var INSTALL_HINT_NOTE = "Results are unvetted \u2014 `skill-forge add <id>` runs the full quarantine + safety gate; `skill-forge find --audit <id>` shows partner audit verdicts.";
4651
+ function isRecordWithDataArray(v) {
4652
+ return typeof v === "object" && v !== null && Array.isArray(v.data);
4653
+ }
4654
+ function printJson(data) {
4655
+ console.log(JSON.stringify(data, null, 2));
4656
+ }
4657
+ function printSearchResults(data, query) {
4658
+ const items = data.data;
4659
+ const count = data.count ?? items.length;
4660
+ const searchType = data.searchType ? ` (${sanitizeForMarkdown(data.searchType)})` : "";
4661
+ console.log(`skills.sh \u2014 ${count} matches for ${JSON.stringify(query)}${searchType}:`);
4662
+ for (const item of items) {
4663
+ const name = sanitizeForMarkdown(String(item.name ?? item.id));
4664
+ const id = sanitizeForMarkdown(String(item.id));
4665
+ const installs = item.installs ?? "?";
4666
+ console.log(` - ${name} [${id}] ${installs} installs [unvetted]`);
4667
+ console.log(` install: skill-forge add ${id}`);
4668
+ }
4669
+ console.log("");
4670
+ console.log(INSTALL_HINT_NOTE);
4671
+ }
4672
+ async function find(query, options = {}, deps = {}) {
4673
+ const modes = [query !== void 0, options.audit !== void 0, options.get !== void 0, Boolean(options.curated)];
4674
+ const activeCount = modes.filter(Boolean).length;
4675
+ if (activeCount === 0) {
4676
+ throw new UserError("skill-forge find: provide a search query, or exactly one of --audit <id>, --get <id>, --curated");
4677
+ }
4678
+ if (activeCount > 1) {
4679
+ throw new UserError("skill-forge find: exactly one mode per invocation \u2014 a search query, or exactly one of --audit <id>, --get <id>, --curated");
4680
+ }
4681
+ const callOptions = { fetchImpl: deps.fetchImpl, env: deps.env };
4682
+ try {
4683
+ if (query !== void 0) {
4684
+ if (query.trim().length < 2) {
4685
+ throw new UserError("skill-forge find: query must be at least 2 characters");
4686
+ }
4687
+ const limit = options.limit ?? 10;
4688
+ const data2 = await search(query, limit, callOptions);
4689
+ if (options.json) {
4690
+ printJson(data2);
4691
+ return;
4692
+ }
4693
+ printSearchResults(data2, query);
4694
+ return;
4695
+ }
4696
+ if (options.audit !== void 0) {
4697
+ const id = options.audit;
4698
+ const data2 = await audit(id, callOptions);
4699
+ if (!data2) {
4700
+ console.log(`no partner audits yet for ${sanitizeForMarkdown(id)} (generated after first install).`);
4701
+ return;
4702
+ }
4703
+ if (options.json) {
4704
+ printJson(data2);
4705
+ return;
4706
+ }
4707
+ const { verdict, risk, audits } = auditVerdict(data2);
4708
+ console.log(
4709
+ `skills.sh audit for ${sanitizeForMarkdown(id)}: ${verdict.toUpperCase()} (max risk ${sanitizeForMarkdown(risk)}) \u2014 ${audits.length} partner(s)`
4710
+ );
4711
+ for (const entry of audits) {
4712
+ const provider = sanitizeForMarkdown(String(entry.provider ?? "unknown"));
4713
+ const status2 = sanitizeForMarkdown(String(entry.status ?? "unknown"));
4714
+ const riskLevel = entry.riskLevel ? ` \xB7 ${sanitizeForMarkdown(String(entry.riskLevel))}` : "";
4715
+ const summary = entry.summary ? ` \u2014 ${sanitizeForMarkdown(String(entry.summary))}` : "";
4716
+ console.log(` - ${provider}: ${status2}${riskLevel}${summary}`);
4717
+ }
4718
+ return;
4719
+ }
4720
+ if (options.get !== void 0) {
4721
+ const data2 = await get(options.get, callOptions);
4722
+ printJson(data2);
4723
+ return;
4724
+ }
4725
+ const data = await curated(callOptions);
4726
+ if (options.json) {
4727
+ printJson(data);
4728
+ return;
4729
+ }
4730
+ if (isRecordWithDataArray(data)) {
4731
+ printSearchResults(data, "(curated)");
4732
+ return;
4733
+ }
4734
+ printJson(data);
4735
+ } catch (err) {
4736
+ if (err instanceof SkillsShError) {
4737
+ console.error(err.message);
4738
+ process.exitCode = err.exitCode;
4739
+ return;
4740
+ }
4741
+ throw err;
4742
+ }
4743
+ }
4744
+
4745
+ // src/commands/watch.ts
4746
+ import { realpathSync as realpathSync3 } from "fs";
4747
+ import { join as join22, resolve as resolvePath7 } from "path";
4748
+ var PRO_FEATURE_LABEL4 = "provenance drift watch (watch)";
4749
+ function isGitUrl(source) {
4750
+ const s = source.trim();
4751
+ return /^https?:\/\/\S+$/i.test(s) || /^git@[\w.-]+:\S+$/i.test(s);
4752
+ }
4753
+ function looksLikeGitRef(ref) {
4754
+ const r = ref.trim();
4755
+ return /^[0-9a-fA-F]{7,40}$/.test(r);
4756
+ }
4757
+ function firstNonEmptyLine(text) {
4758
+ return text.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "(no output)";
4759
+ }
4760
+ async function checkGitDrift(exec, url, ref) {
4761
+ let result;
4762
+ try {
4763
+ result = await exec("git", ["ls-remote", "--", url, ref], { cwd: process.cwd() });
4764
+ } catch (err) {
4765
+ return { status: "unreachable", detail: `git ls-remote failed to run: ${err.message}` };
4766
+ }
4767
+ if (result.code !== 0) {
4768
+ return { status: "unreachable", detail: `git ls-remote exited ${result.code}: ${firstNonEmptyLine(result.stderr || result.stdout)}` };
4769
+ }
4770
+ const shas = result.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).map((l) => l.split(/\s+/)[0] ?? "").filter((sha) => sha.length > 0);
4771
+ if (shas.length === 0) {
4772
+ return { status: "unreachable", detail: `no ref matching "${ref}" found on remote` };
4773
+ }
4774
+ const recorded = ref.trim().toLowerCase();
4775
+ if (shas.some((sha) => sha.toLowerCase() === recorded)) {
4776
+ return { status: "unchanged", detail: `current: ${recorded}` };
4777
+ }
4778
+ return { status: "moved", detail: `recorded ${ref}, remote now resolves to ${shas[0]}` };
4779
+ }
4780
+ async function runWatch(config, options = {}, deps = {}) {
4781
+ const proStatus = proFeatureStatus(PRO_FEATURE_LABEL4);
4782
+ if (!proStatus.enabled) {
4783
+ return { roots: [], rows: [], warnings: [], notices: [proStatus.notice ?? "Pro feature (watch) locked."] };
4784
+ }
4785
+ const notices = proStatus.notice ? [proStatus.notice] : [];
4786
+ const exec = deps.exec ?? defaultExec;
4787
+ const roots = [];
4788
+ const rows = [];
4789
+ const warnings = [];
4790
+ const seenRealPaths = /* @__PURE__ */ new Set();
4791
+ for (const root of config.skillsRoots) {
4792
+ const resolvedRoot = resolvePath7(expandHome(root));
4793
+ const exists = isDir(resolvedRoot);
4794
+ let realPath = resolvedRoot;
4795
+ if (exists) {
4796
+ try {
4797
+ realPath = realpathSync3(resolvedRoot);
4798
+ } catch {
4799
+ realPath = resolvedRoot;
4800
+ }
4801
+ }
4802
+ if (seenRealPaths.has(realPath)) continue;
4803
+ seenRealPaths.add(realPath);
4804
+ let ledger;
4805
+ try {
4806
+ ledger = readProvenanceLedger(resolvedRoot);
4807
+ } catch (err) {
4808
+ const message = err instanceof Error ? err.message : String(err);
4809
+ const ledgerPath = join22(resolvedRoot, "SOURCES.md");
4810
+ roots.push({ root, realPath, exists, ledgerPath, ledgerExists: false });
4811
+ warnings.push(`${ledgerPath}: could not read ledger: ${message}`);
4812
+ continue;
4813
+ }
4814
+ roots.push({ root, realPath, exists, ledgerPath: ledger.ledgerPath, ledgerExists: ledger.exists });
4815
+ warnings.push(...ledger.warnings.map((w) => `${ledger.ledgerPath}: ${w}`));
4816
+ for (const entry of ledger.entries) {
4817
+ if (entry.verb === "REJECT") continue;
4818
+ const rowBase = {
4819
+ root,
4820
+ ledgerPath: ledger.ledgerPath,
4821
+ name: entry.name,
4822
+ verb: entry.verb,
4823
+ source: entry.source,
4824
+ upstreamRef: entry.upstreamRef,
4825
+ driftCheckCommand: entry.driftCheck
4826
+ };
4827
+ if (!(isGitUrl(entry.source) && looksLikeGitRef(entry.upstreamRef))) {
4828
+ rows.push({
4829
+ ...rowBase,
4830
+ checkMethod: "manual",
4831
+ driftStatus: "not-checked",
4832
+ detail: "source is not a recognized git URL + ref pin \u2014 check manually"
4833
+ });
4834
+ continue;
4835
+ }
4836
+ if (options.offline) {
4837
+ rows.push({
4838
+ ...rowBase,
4839
+ checkMethod: "git-ls-remote",
4840
+ driftStatus: "not-checked",
4841
+ detail: "--offline: network check skipped"
4842
+ });
4843
+ continue;
4844
+ }
4845
+ const { status: status2, detail } = await checkGitDrift(exec, entry.source, entry.upstreamRef);
4846
+ rows.push({ ...rowBase, checkMethod: "git-ls-remote", driftStatus: status2, detail });
4847
+ }
4848
+ }
4849
+ return { roots, rows, warnings, notices };
4850
+ }
4851
+ function printWatchTerminal(report) {
4852
+ console.log("[skill-forge] watch \u2014 provenance drift check");
4853
+ for (const notice of report.notices) console.log(`[notice] ${notice}`);
4854
+ if (report.roots.length === 0) {
4855
+ console.log("\nNo configured skills roots.");
4856
+ }
4857
+ for (const root of report.roots) {
4858
+ console.log(`
4859
+ ${sanitizeForMarkdown(root.root)} (${sanitizeForMarkdown(root.realPath)})`);
4860
+ if (!root.exists) {
4861
+ console.log(" root does not exist on disk");
4862
+ continue;
4863
+ }
4864
+ if (!root.ledgerExists) {
4865
+ console.log(` no ledger at ${sanitizeForMarkdown(root.ledgerPath)}`);
4866
+ continue;
4867
+ }
4868
+ console.log(` ledger: ${sanitizeForMarkdown(root.ledgerPath)}`);
4869
+ const rowsForRoot = report.rows.filter((r) => r.root === root.root);
4870
+ if (rowsForRoot.length === 0) {
4871
+ console.log(" (no tracked, non-REJECT sources)");
4872
+ continue;
4873
+ }
4874
+ for (const row of rowsForRoot) {
4875
+ console.log(` \u2022 ${sanitizeForMarkdown(row.name)} [${sanitizeForMarkdown(row.verb)}] ref=${sanitizeForMarkdown(row.upstreamRef)}`);
4876
+ if (row.checkMethod === "git-ls-remote") {
4877
+ console.log(` drift: ${row.driftStatus} \u2014 ${sanitizeForMarkdown(row.detail)}`);
4878
+ } else {
4879
+ console.log(` manual check \u2014 stored drift-check command (data, NOT executed): ${sanitizeForMarkdown(row.driftCheckCommand)}`);
4880
+ }
4881
+ }
4882
+ }
4883
+ if (report.warnings.length > 0) {
4884
+ console.log("\nWarnings:");
4885
+ for (const w of report.warnings) console.log(` - ${sanitizeForMarkdown(w)}`);
4886
+ }
4887
+ console.log("\nCompare each stored ref to current upstream; re-run ingestion on any that moved.");
4888
+ }
4889
+ async function watchCommand(options = {}) {
4890
+ const report = await runWatch(loadConfig(), options);
4891
+ if (options.json) {
4892
+ console.log(JSON.stringify(report, null, 2));
4893
+ for (const notice of report.notices) console.error(`[skill-forge] ${notice}`);
4894
+ return;
4895
+ }
4896
+ printWatchTerminal(report);
4897
+ }
4898
+
4899
+ // src/commands/ingest.ts
4900
+ import { realpathSync as realpathSync4 } from "fs";
4901
+ import { dirname as dirname9, isAbsolute, relative, resolve as resolvePath8 } from "path";
4902
+ var PRO_FEATURE_LABEL5 = "the pending-ingestion queue";
4903
+ var VALID_STATUSES = ["ingested", "dismissed"];
4904
+ function requireProFeature() {
4905
+ const status2 = proFeatureStatus(PRO_FEATURE_LABEL5);
4906
+ if (!status2.enabled) {
4907
+ throw new UserError(
4908
+ status2.notice ?? "ingest is a Pro feature \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock."
4909
+ );
4910
+ }
4911
+ if (status2.notice) console.error(`[skill-forge] ${status2.notice}`);
4912
+ }
4913
+ function pluralizeEntries(count) {
4914
+ return `entr${count === 1 ? "y" : "ies"}`;
4915
+ }
4916
+ function toListRow(entry) {
4917
+ return {
4918
+ id: entry.id,
4919
+ source: entry.source,
4920
+ artifactType: entry.artifactType ?? "skill",
4921
+ safetyVerdict: entry.gate.safetyVerdict,
4922
+ createdAt: entry.createdAt
4923
+ };
4924
+ }
4925
+ function printPendingList(pending, json) {
4926
+ if (json) {
4927
+ console.log(JSON.stringify({ entries: pending.map(toListRow) }, null, 2));
4928
+ return;
4929
+ }
4930
+ if (pending.length === 0) {
4931
+ console.log("[skill-forge] no pending queue entries.");
4932
+ return;
4933
+ }
4934
+ console.log(`[skill-forge] ${pending.length} pending ${pluralizeEntries(pending.length)}:`);
4935
+ for (const entry of pending) {
4936
+ const row = toListRow(entry);
4937
+ console.log(` ${row.id} ${row.source} (${row.artifactType}) verdict=${row.safetyVerdict} created=${row.createdAt}`);
4938
+ }
4939
+ }
4940
+ function realpathOrNull(p) {
4941
+ try {
4942
+ return realpathSync4(p);
4943
+ } catch {
4944
+ return null;
4945
+ }
4946
+ }
4947
+ function isUnderRoot(candidate, root) {
4948
+ const rel = relative(root, candidate);
4949
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
4950
+ }
4951
+ function validatePendingEntries(entries, config) {
4952
+ const quarantineRoot = realpathOrNull(resolvePath8(config.quarantineDir));
4953
+ const skillsRoots = config.skillsRoots.map((r) => realpathOrNull(resolvePath8(r))).filter((r) => r !== null);
4954
+ const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname9(resolvePath8(f)))).filter((r) => r !== null);
4955
+ const allowedRoots = [quarantineRoot, ...skillsRoots, ...mcpTargetDirs].filter((r) => r !== null);
4956
+ const valid = [];
4957
+ const excluded = [];
4958
+ for (const entry of entries) {
4959
+ const effectivePath = entry.installedPath ?? entry.quarantinePath;
4960
+ if (!effectivePath) {
4961
+ excluded.push({ entry, reason: "no installedPath or quarantinePath recorded on the entry" });
4962
+ continue;
4963
+ }
4964
+ const real = realpathOrNull(resolvePath8(effectivePath));
4965
+ if (!real) {
4966
+ excluded.push({ entry, reason: `path does not exist or could not be resolved: ${effectivePath}` });
4967
+ continue;
4968
+ }
4969
+ if (allowedRoots.length === 0 || !allowedRoots.some((root) => isUnderRoot(real, root))) {
4970
+ excluded.push({ entry, reason: `path escapes every configured root (quarantineDir/skillsRoots/mcpTargets): ${effectivePath}` });
4971
+ continue;
4972
+ }
4973
+ valid.push(entry);
4974
+ }
4975
+ return { valid, excluded };
4976
+ }
4977
+ async function ingest(options = {}) {
4978
+ requireProFeature();
4979
+ const queue2 = readQueue();
4980
+ const pending = queue2.entries.filter((e) => e.status === "pending");
4981
+ if (options.list) {
4982
+ printPendingList(pending, options.json);
4983
+ return;
4984
+ }
4985
+ if (pending.length === 0) {
4986
+ console.log("[skill-forge] no pending queue entries to ingest.");
4987
+ return;
4988
+ }
4989
+ const config = loadConfig();
4990
+ const { valid, excluded } = validatePendingEntries(pending, config);
4991
+ if (excluded.length > 0) {
4992
+ console.error(`[skill-forge] excluded ${excluded.length} pending ${pluralizeEntries(excluded.length)} that failed path validation:`);
4993
+ for (const { entry, reason } of excluded) {
4994
+ console.error(` - ${entry.id} (${entry.source}): ${reason}`);
4995
+ }
4996
+ console.error(
4997
+ "[skill-forge] excluded entries stay in the queue \u2014 review each, then dismiss with: skill-forge queue close <id> --status dismissed"
4998
+ );
4999
+ }
5000
+ if (valid.length === 0) {
5001
+ console.log("[skill-forge] no pending entries passed path validation \u2014 nothing to hand off.");
5002
+ return;
5003
+ }
5004
+ console.log(`[skill-forge] ${valid.length} pending ${pluralizeEntries(valid.length)} ready for ingest:`);
5005
+ for (const entry of valid) {
5006
+ console.log(` - ${entry.id} ${entry.source} (${entry.artifactType ?? "skill"})`);
5007
+ }
5008
+ runHandoff(config, getQueuePath(), { promptAsset: "ingest-prompt.md" });
5009
+ }
5010
+ async function queueClose(id, options = {}) {
5011
+ requireProFeature();
5012
+ const status2 = options.status;
5013
+ if (!status2 || !VALID_STATUSES.includes(status2)) {
5014
+ throw new UserError(`--status must be one of: ${VALID_STATUSES.join(", ")} (got ${status2 ? JSON.stringify(status2) : "nothing"})`);
5015
+ }
5016
+ let updated;
5017
+ try {
5018
+ updated = updateQueueEntry(id, { status: status2 });
5019
+ } catch (err) {
5020
+ throw new UserError(err instanceof Error ? err.message : String(err));
5021
+ }
5022
+ console.log(`[skill-forge] queue entry ${updated.id} closed -> status: ${updated.status}`);
5023
+ }
5024
+
5025
+ // src/commands/refine.ts
5026
+ import { createHash as createHash3, randomUUID as randomUUID4 } from "crypto";
5027
+ import { chmodSync, existsSync as existsSync20, lstatSync as lstatSync3, mkdirSync as mkdirSync9, readFileSync as readFileSync15, realpathSync as realpathSync7, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
5028
+ import { dirname as dirname11, isAbsolute as isAbsolute3, join as join25, relative as relative3, resolve as resolvePath11, sep as sep2 } from "path";
5029
+ import { createInterface as createInterface5 } from "readline/promises";
5030
+
5031
+ // src/refine/context.ts
5032
+ import { existsSync as existsSync19, realpathSync as realpathSync6, statSync as statSync11 } from "fs";
5033
+ import { isAbsolute as isAbsolute2, join as join24, relative as relative2, resolve as resolvePath10 } from "path";
5034
+
5035
+ // src/refine/models.ts
5036
+ import { randomBytes } from "crypto";
5037
+ var REFINEMENT_CATEGORIES = ["trigger", "content", "hook", "tool", "pattern", "config", "new"];
5038
+ var OVERRIDE_TYPES = ["patch", "extend", "config", "full", "hook", "script", "new"];
5039
+ var PATCH_ACTIONS = ["append", "prepend", "replace-section", "insert-after", "insert-before", "delete-section"];
5040
+ var PATTERN_STATUSES = ["tracking", "ready", "generalized", "dismissed"];
5041
+ function isRefinementCategory(value) {
5042
+ return REFINEMENT_CATEGORIES.includes(value);
5043
+ }
5044
+ function isOverrideType(value) {
5045
+ return OVERRIDE_TYPES.includes(value);
5046
+ }
5047
+ function isPatchAction(value) {
5048
+ return PATCH_ACTIONS.includes(value);
5049
+ }
5050
+ function isPatternStatus(value) {
5051
+ return PATTERN_STATUSES.includes(value);
5052
+ }
5053
+ var REFINEMENT_STATUSES = ["pending", "applied", "generalized"];
5054
+ function isRefinementStatus(value) {
5055
+ return REFINEMENT_STATUSES.includes(value);
5056
+ }
5057
+ function patternCount(pattern) {
5058
+ return new Set(pattern.occurrences.map((o) => o.projectRealpath)).size;
5059
+ }
5060
+ function defaultRandomHex() {
5061
+ return randomBytes(2).toString("hex");
5062
+ }
5063
+ function formatYYYYMMDD(date) {
5064
+ const yyyy = date.getUTCFullYear().toString().padStart(4, "0");
5065
+ const mm = (date.getUTCMonth() + 1).toString().padStart(2, "0");
5066
+ const dd = date.getUTCDate().toString().padStart(2, "0");
5067
+ return `${yyyy}${mm}${dd}`;
5068
+ }
5069
+ function generateRefinementId(existingIds = /* @__PURE__ */ new Set(), now = /* @__PURE__ */ new Date(), randomHex = defaultRandomHex) {
5070
+ const datePart = formatYYYYMMDD(now);
5071
+ let id;
5072
+ do {
5073
+ id = `REF-${datePart}-${randomHex()}`;
5074
+ } while (existingIds.has(id));
5075
+ return id;
5076
+ }
5077
+ function generatePatternId(existingIds = /* @__PURE__ */ new Set(), randomHex = defaultRandomHex) {
5078
+ let id;
5079
+ do {
5080
+ id = `PAT-${randomHex()}`;
5081
+ } while (existingIds.has(id));
5082
+ return id;
5083
+ }
5084
+ function fingerprintKey(skill, target, overrideType, action) {
5085
+ const normalize = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
5086
+ return [normalize(skill), normalize(target), overrideType, action ?? ""].join("::");
5087
+ }
5088
+ function contentSimilarity(a, b) {
5089
+ const tokenize2 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
5090
+ const tokensA = tokenize2(a);
5091
+ const tokensB = tokenize2(b);
5092
+ if (tokensA.size === 0 || tokensB.size === 0) return 0;
5093
+ let intersectionSize = 0;
5094
+ for (const token of tokensA) {
5095
+ if (tokensB.has(token)) intersectionSize++;
5096
+ }
5097
+ const unionSize2 = tokensA.size + tokensB.size - intersectionSize;
5098
+ return unionSize2 === 0 ? 0 : intersectionSize / unionSize2;
5099
+ }
5100
+
5101
+ // src/refine/store.ts
5102
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
5103
+ import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync14, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync10, writeFileSync as writeFileSync8, realpathSync as realpathSync5 } from "fs";
5104
+ import { basename as basename9, dirname as dirname10, join as join23, resolve as resolvePath9 } from "path";
5105
+ var SIMILARITY_JOIN_THRESHOLD = 0.7;
5106
+ var READY_THRESHOLD = 2;
5107
+ function getRefinementsDir() {
5108
+ return join23(getHomeDir(), "refinements");
5109
+ }
5110
+ function getRefinementsPath() {
5111
+ return join23(getRefinementsDir(), "refinements.json");
5112
+ }
5113
+ function getPatternsPath() {
5114
+ return join23(getRefinementsDir(), "patterns.json");
5115
+ }
5116
+ function getBackupsDir() {
5117
+ return join23(getRefinementsDir(), "backups");
5118
+ }
5119
+ function writeJsonAtomic(path, data) {
5120
+ const dir = dirname10(path);
5121
+ mkdirSync8(dir, { recursive: true });
5122
+ const tempPath = join23(dir, `.${basename9(path)}.tmp-${randomUUID3()}`);
5123
+ try {
5124
+ writeFileSync8(tempPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
5125
+ renameSync4(tempPath, path);
5126
+ } catch (err) {
5127
+ rmSync6(tempPath, { force: true });
5128
+ throw err;
5129
+ }
5130
+ }
5131
+ function readJsonFile(path, label) {
5132
+ let raw;
5133
+ try {
5134
+ raw = readFileSync14(path, "utf-8");
5135
+ } catch (err) {
5136
+ throw new UserError(`unable to read ${label} at ${path}: ${err.message}`);
5137
+ }
5138
+ try {
5139
+ return JSON.parse(raw);
5140
+ } catch {
5141
+ throw new UserError(`malformed ${label} at ${path}: not valid JSON`);
5142
+ }
5143
+ }
5144
+ function readStore(path, label, validateRecord) {
5145
+ if (!existsSync18(path)) {
5146
+ return { schemaVersion: 1, records: [] };
5147
+ }
5148
+ const parsed = readJsonFile(path, label);
5149
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.records)) {
5150
+ throw new UserError(`malformed ${label} at ${path}: expected an object with a "records" array`);
5151
+ }
5152
+ const records = parsed.records;
5153
+ if (validateRecord) {
5154
+ records.forEach((record, index) => validateRecord(record, index));
5155
+ }
5156
+ return { schemaVersion: 1, records };
5157
+ }
5158
+ function validateRefinementRecordShape(record, index, path) {
5159
+ if (typeof record !== "object" || record === null) {
5160
+ throw new UserError(`malformed refinements.json at ${path}: record[${index}] is not an object`);
5161
+ }
5162
+ const r = record;
5163
+ if (typeof r.id !== "string" || r.id.length === 0) {
5164
+ throw new UserError(`malformed refinements.json at ${path}: record[${index}] is missing a valid "id"`);
5165
+ }
5166
+ if (typeof r.createdAt !== "string" || r.createdAt.length === 0) {
5167
+ throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "createdAt"`);
5168
+ }
5169
+ if (typeof r.status !== "string" || r.status.length === 0) {
5170
+ throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "status"`);
5171
+ }
5172
+ if (typeof r.skill !== "string" || r.skill.length === 0) {
5173
+ throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "skill"`);
5174
+ }
5175
+ const patchActionsRequiringMarker = /* @__PURE__ */ new Set(["insert-after", "insert-before", "replace-section", "delete-section"]);
5176
+ if (r.overrideType === "patch") {
5177
+ if (typeof r.patchAction !== "string" || r.patchAction.length === 0) {
5178
+ throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchAction"`);
5179
+ }
5180
+ if (patchActionsRequiringMarker.has(r.patchAction) && (typeof r.patchMarker !== "string" || r.patchMarker.length === 0)) {
5181
+ throw new UserError(
5182
+ `malformed refinements.json at ${path}: record "${r.id}" has patchAction "${r.patchAction}" but is missing "patchMarker"`
5183
+ );
5184
+ }
5185
+ if (typeof r.patchContent !== "string" || r.patchContent.length === 0) {
5186
+ throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchContent"`);
5187
+ }
5188
+ } else if (r.patchAction !== void 0) {
5189
+ throw new UserError(
5190
+ `malformed refinements.json at ${path}: record "${r.id}" has overrideType "${String(r.overrideType)}" but carries "patchAction" \u2014 only valid when overrideType is "patch"`
5191
+ );
5192
+ }
5193
+ }
5194
+ function validatePatternRecordShape(record, index, path) {
5195
+ if (typeof record !== "object" || record === null) {
5196
+ throw new UserError(`malformed patterns.json at ${path}: record[${index}] is not an object`);
5197
+ }
5198
+ const p = record;
5199
+ if (typeof p.id !== "string" || p.id.length === 0) {
5200
+ throw new UserError(`malformed patterns.json at ${path}: record[${index}] is missing a valid "id"`);
5201
+ }
5202
+ if (typeof p.status !== "string" || p.status.length === 0) {
5203
+ throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "status"`);
5204
+ }
5205
+ if (!Array.isArray(p.occurrences)) {
5206
+ throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "occurrences" array`);
5207
+ }
5208
+ if (p.status === "dismissed" && (typeof p.dismissedReason !== "string" || p.dismissedReason.length === 0)) {
5209
+ throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "dismissed" but is missing "dismissedReason"`);
5210
+ }
5211
+ if (p.status === "generalized") {
5212
+ if (typeof p.generalizedAt !== "string" || p.generalizedAt.length === 0) {
5213
+ throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedAt"`);
5214
+ }
5215
+ if (typeof p.generalizedBackupId !== "string" || p.generalizedBackupId.length === 0) {
5216
+ throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedBackupId"`);
5217
+ }
5218
+ }
5219
+ }
5220
+ function readRefinementsStore() {
5221
+ const path = getRefinementsPath();
5222
+ return readStore(path, "refinements.json", (record, index) => validateRefinementRecordShape(record, index, path));
5223
+ }
5224
+ function writeRefinementsStore(store) {
5225
+ writeJsonAtomic(getRefinementsPath(), store);
5226
+ }
5227
+ function readPatternsStore() {
5228
+ const path = getPatternsPath();
5229
+ return readStore(path, "patterns.json", (record, index) => validatePatternRecordShape(record, index, path));
5230
+ }
5231
+ function writePatternsStore(store) {
5232
+ writeJsonAtomic(getPatternsPath(), store);
5233
+ }
5234
+ function resolveProjectRealpath(projectRoot) {
5235
+ const resolved = resolvePath9(projectRoot);
5236
+ try {
5237
+ return realpathSync5(resolved);
5238
+ } catch {
5239
+ return resolved;
5240
+ }
5241
+ }
5242
+ function generatePatternName(refinement) {
5243
+ const parts = refinement.targetSection.replace(/\//g, " ").replace(/-/g, " ").split(/\s+/).filter(Boolean);
5244
+ const lastThree = parts.slice(-3);
5245
+ if (lastThree.length === 0) return refinement.targetSection || `pattern-${refinement.skill}`;
5246
+ return lastThree.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
5247
+ }
5248
+ function generatePatternDescription(refinement) {
5249
+ const category = refinement.category.charAt(0).toUpperCase() + refinement.category.slice(1);
5250
+ const cause = refinement.rootCause || refinement.expectedBehavior;
5251
+ return `${category} modification for ${refinement.skill}: ${cause}`;
5252
+ }
5253
+ function upsertPattern(records, refinement, projectRealpath, nowIso, randomHex, explicitPatternId) {
5254
+ let pattern;
5255
+ if (explicitPatternId) {
5256
+ pattern = records.find((p) => p.id === explicitPatternId);
5257
+ if (!pattern) {
5258
+ throw new UserError(`no pattern with id: ${explicitPatternId}`);
5259
+ }
5260
+ if (pattern.status === "dismissed" || pattern.status === "generalized") {
5261
+ throw new UserError(`cannot join pattern ${explicitPatternId}: it is ${pattern.status} and no longer accepts new occurrences`);
5262
+ }
5263
+ } else {
5264
+ const joinable = records.filter((p) => p.status !== "generalized" && p.status !== "dismissed");
5265
+ const fingerprint = fingerprintKey(refinement.skill, refinement.targetSection, refinement.overrideType, refinement.patchAction);
5266
+ pattern = joinable.find((p) => p.fingerprint === fingerprint);
5267
+ if (!pattern && refinement.patchContent) {
5268
+ pattern = joinable.find((p) => {
5269
+ const sample = p.proposedChanges.content;
5270
+ return sample ? contentSimilarity(refinement.patchContent, sample) > SIMILARITY_JOIN_THRESHOLD : false;
5271
+ });
5272
+ }
5273
+ }
5274
+ const wasTracking = pattern?.status === "tracking";
5275
+ if (!pattern) {
5276
+ const created = {
5277
+ id: generatePatternId(new Set(records.map((p) => p.id)), randomHex),
5278
+ name: generatePatternName(refinement),
5279
+ description: generatePatternDescription(refinement),
5280
+ affectedSkills: [refinement.skill],
5281
+ refinementIds: [refinement.id],
5282
+ occurrences: [{ refinementId: refinement.id, projectRealpath, timestamp: nowIso }],
5283
+ status: "tracking",
5284
+ proposedChanges: refinement.patchContent ? { content: refinement.patchContent } : {},
5285
+ fingerprint: fingerprintKey(refinement.skill, refinement.targetSection, refinement.overrideType, refinement.patchAction),
5286
+ firstSeen: nowIso,
5287
+ lastSeen: nowIso
5288
+ };
5289
+ records.push(created);
5290
+ return { pattern: created, becameReady: false };
5291
+ }
5292
+ if (!pattern.affectedSkills.includes(refinement.skill)) pattern.affectedSkills.push(refinement.skill);
5293
+ if (!pattern.refinementIds.includes(refinement.id)) pattern.refinementIds.push(refinement.id);
5294
+ pattern.occurrences.push({ refinementId: refinement.id, projectRealpath, timestamp: nowIso });
5295
+ pattern.lastSeen = nowIso;
5296
+ const becameReady = wasTracking && patternCount(pattern) >= READY_THRESHOLD;
5297
+ if (becameReady) pattern.status = "ready";
5298
+ return { pattern, becameReady };
5299
+ }
5300
+ function recordRefinement(input, deps = {}) {
5301
+ const now = deps.now ?? /* @__PURE__ */ new Date();
5302
+ const nowIso = now.toISOString();
5303
+ const refinementsStore = readRefinementsStore();
5304
+ const patternsStore = readPatternsStore();
5305
+ const existingRefIds = new Set(refinementsStore.records.map((r) => r.id));
5306
+ const refinementId = deps.id ?? generateRefinementId(existingRefIds, now, deps.randomHex);
5307
+ const projectRealpath = resolveProjectRealpath(input.projectRoot);
5308
+ const refinement = {
5309
+ id: refinementId,
5310
+ skill: input.skill,
5311
+ category: input.category,
5312
+ overrideType: input.overrideType,
5313
+ targetSection: input.targetSection,
5314
+ expectedBehavior: input.expectedBehavior,
5315
+ actualBehavior: input.actualBehavior,
5316
+ example: input.example,
5317
+ desiredOutcome: input.desiredOutcome,
5318
+ rootCause: input.rootCause,
5319
+ confidence: input.confidence,
5320
+ project: input.project,
5321
+ projectRoot: projectRealpath,
5322
+ patchAction: input.patchAction,
5323
+ patchMarker: input.patchMarker,
5324
+ patchContent: input.patchContent,
5325
+ generalizationPotential: input.generalizationPotential,
5326
+ scope: input.scope,
5327
+ status: input.status ?? "applied",
5328
+ createdAt: nowIso
5329
+ };
5330
+ const { pattern, becameReady } = upsertPattern(patternsStore.records, refinement, projectRealpath, nowIso, deps.randomHex, input.patternId);
5331
+ refinement.patternId = pattern.id;
5332
+ refinementsStore.records.push(refinement);
5333
+ writeRefinementsStore(refinementsStore);
5334
+ try {
5335
+ writePatternsStore(patternsStore);
5336
+ } catch (err) {
5337
+ throw new UserError(
5338
+ `refinements.json was updated with refinement ${refinement.id}, but patterns.json failed to update: ${err.message}`
5339
+ );
5340
+ }
5341
+ return { refinement, pattern, patternBecameReady: becameReady };
5342
+ }
5343
+ function markGeneralized(id, backupId) {
5344
+ const store = readPatternsStore();
5345
+ const pattern = store.records.find((p) => p.id === id);
5346
+ if (!pattern) {
5347
+ throw new UserError(`no pattern with id: ${id}`);
5348
+ }
5349
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5350
+ pattern.status = "generalized";
5351
+ pattern.generalizedAt = nowIso;
5352
+ pattern.generalizedBackupId = backupId;
5353
+ pattern.lastSeen = nowIso;
5354
+ writePatternsStore(store);
5355
+ return pattern;
5356
+ }
5357
+ function formatBackupTimestamp(date) {
5358
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
5359
+ }
5360
+ var BACKUP_ID_PATTERN = /^\d{8}T\d{6}Z-[0-9a-f]{4}$/;
5361
+ function isValidBackupId(id) {
5362
+ return BACKUP_ID_PATTERN.test(id);
5363
+ }
5364
+ function createBackup(filePaths, deps = {}) {
5365
+ const now = deps.now ?? /* @__PURE__ */ new Date();
5366
+ const randomHex = deps.randomHex ?? defaultRandomHex;
5367
+ const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
5368
+ const files = filePaths.map((path) => {
5369
+ if (!existsSync18(path)) {
5370
+ return { path, existed: false, sha256: null, mode: null, contentBase64: null };
5371
+ }
5372
+ const bytes = readFileSync14(path);
5373
+ const mode = statSync10(path).mode & 511;
5374
+ const sha256 = createHash2("sha256").update(bytes).digest("hex");
5375
+ return { path, existed: true, sha256, mode, contentBase64: bytes.toString("base64") };
5376
+ });
5377
+ const manifest = { id, createdAt: now.toISOString(), files };
5378
+ writeJsonAtomic(join23(getBackupsDir(), id, "manifest.json"), manifest);
5379
+ return manifest;
5380
+ }
5381
+ function validateBackupManifestShape(value, path) {
5382
+ if (typeof value !== "object" || value === null) {
5383
+ throw new UserError(`malformed backup manifest at ${path}: not an object`);
5384
+ }
5385
+ const m = value;
5386
+ if (typeof m.id !== "string" || m.id.length === 0) {
5387
+ throw new UserError(`malformed backup manifest at ${path}: missing a valid "id"`);
5388
+ }
5389
+ if (typeof m.createdAt !== "string" || m.createdAt.length === 0) {
5390
+ throw new UserError(`malformed backup manifest at ${path}: missing a valid "createdAt"`);
5391
+ }
5392
+ if (!Array.isArray(m.files)) {
5393
+ throw new UserError(`malformed backup manifest at ${path}: missing a valid "files" array`);
5394
+ }
5395
+ m.files.forEach((entry, index) => {
5396
+ if (typeof entry !== "object" || entry === null) {
5397
+ throw new UserError(`malformed backup manifest at ${path}: files[${index}] is not an object`);
5398
+ }
5399
+ const f = entry;
5400
+ if (typeof f.path !== "string" || f.path.length === 0) {
5401
+ throw new UserError(`malformed backup manifest at ${path}: files[${index}] is missing a valid "path"`);
5402
+ }
5403
+ if (typeof f.existed !== "boolean") {
5404
+ throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") is missing a valid "existed" (must be boolean)`);
5405
+ }
5406
+ if (f.existed) {
5407
+ if (typeof f.sha256 !== "string" || f.sha256.length === 0) {
5408
+ throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:true but is missing "sha256"`);
5409
+ }
5410
+ if (typeof f.mode !== "number") {
5411
+ throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:true but is missing a numeric "mode"`);
5412
+ }
5413
+ if (typeof f.contentBase64 !== "string") {
5414
+ throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:true but is missing "contentBase64"`);
5415
+ }
5416
+ } else {
5417
+ if (f.sha256 !== null || f.mode !== null || f.contentBase64 !== null) {
5418
+ throw new UserError(`malformed backup manifest at ${path}: files[${index}] ("${f.path}") has existed:false (a tombstone) but sha256/mode/contentBase64 are not all null`);
5419
+ }
5420
+ }
5421
+ });
5422
+ }
5423
+ function readBackupManifest(id) {
5424
+ if (!isValidBackupId(id)) {
5425
+ throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
5426
+ }
5427
+ const manifestPath = join23(getBackupsDir(), id, "manifest.json");
5428
+ if (!existsSync18(manifestPath)) {
5429
+ throw new UserError(`no backup with id: ${id}`);
5430
+ }
5431
+ const parsed = readJsonFile(manifestPath, "backup manifest");
5432
+ validateBackupManifestShape(parsed, manifestPath);
5433
+ return parsed;
5434
+ }
5435
+
5436
+ // src/refine/context.ts
5437
+ function resolveConfiguredRoot2(root) {
5438
+ return resolvePath10(expandHome(root));
5439
+ }
5440
+ function assertPlainSkillName(name) {
5441
+ if (!name || name.trim().length === 0) {
5442
+ throw new UserError("skill name must not be empty");
5443
+ }
5444
+ if (name.includes("/") || name.includes("\\") || name === "." || name === "..") {
5445
+ throw new UserError(`invalid skill name "${name}" \u2014 must be a plain directory name, not a path`);
5446
+ }
5447
+ }
5448
+ function realpathOrNull2(p) {
5449
+ try {
5450
+ return realpathSync6(p);
5451
+ } catch {
5452
+ return null;
5453
+ }
5454
+ }
5455
+ function isUnderRoot2(candidate, root) {
5456
+ const rel = relative2(root, candidate);
5457
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
5458
+ }
5459
+ function resolveScopes(config, cwd, skill, userRootFlag) {
5460
+ assertPlainSkillName(skill);
5461
+ const resolvedCwd = resolvePath10(cwd);
5462
+ const localDir = join24(resolvedCwd, ".claude", "skills", skill);
5463
+ const sharedDir = join24(resolvedCwd, "skills", skill);
5464
+ const local = { scope: "local", dir: localDir, exists: existsSync19(localDir) };
5465
+ const shared = { scope: "shared", dir: sharedDir, exists: existsSync19(sharedDir) };
5466
+ const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
5467
+ return { local, shared, user };
5468
+ }
5469
+ function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
5470
+ if (userRootFlag) {
5471
+ const root = resolvePath10(userRootFlag);
5472
+ return { resolved: true, dir: join24(root, skill), root: userRootFlag, reason: "flag" };
5473
+ }
5474
+ const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
5475
+ const candidates = config.skillsRoots.map((root) => ({ root, dir: join24(resolveConfiguredRoot2(root), skill) }));
5476
+ for (const candidate of candidates) {
5477
+ const skillReal = realpathOrNull2(candidate.dir);
5478
+ if (skillReal) {
5479
+ return { resolved: true, dir: candidate.dir, root: candidate.root, reason: "contains-skill" };
5480
+ }
5481
+ }
5482
+ const externalCandidates = candidates.filter((candidate) => {
5483
+ const rootReal = realpathOrNull2(resolveConfiguredRoot2(candidate.root)) ?? resolveConfiguredRoot2(candidate.root);
5484
+ return !isUnderRoot2(rootReal, cwdReal) && !isUnderRoot2(cwdReal, rootReal);
5485
+ });
5486
+ if (externalCandidates.length === 1) {
5487
+ return { resolved: true, dir: externalCandidates[0].dir, root: externalCandidates[0].root, reason: "sole-external-root" };
5488
+ }
5489
+ return { resolved: false, candidates: externalCandidates.length > 0 ? externalCandidates : candidates };
5490
+ }
5491
+ function isFile2(p) {
5492
+ try {
5493
+ return statSync11(p).isFile();
5494
+ } catch {
5495
+ return false;
5496
+ }
5497
+ }
5498
+ function isDirectory(p) {
5499
+ try {
5500
+ return statSync11(p).isDirectory();
5501
+ } catch {
5502
+ return false;
5503
+ }
5504
+ }
5505
+ function inventoryScopeDir(scope, dir) {
5506
+ return {
5507
+ scope,
5508
+ dir,
5509
+ dirExists: isDirectory(dir),
5510
+ skillMd: isFile2(join24(dir, "SKILL.md")),
5511
+ patchMd: isFile2(join24(dir, "SKILL.patch.md")),
5512
+ extendMd: isFile2(join24(dir, "SKILL.extend.md")),
5513
+ configJson: isFile2(join24(dir, "skill-config.json")),
5514
+ hooksDir: isDirectory(join24(dir, "hooks")),
5515
+ scriptsDir: isDirectory(join24(dir, "scripts"))
5516
+ };
5517
+ }
5518
+ function inventoryAllScopes(resolution) {
5519
+ const rows = [inventoryScopeDir("local", resolution.local.dir), inventoryScopeDir("shared", resolution.shared.dir)];
5520
+ if (resolution.user.resolved) {
5521
+ rows.push(inventoryScopeDir("user", resolution.user.dir));
5522
+ }
5523
+ return rows;
5524
+ }
5525
+
5526
+ // src/refine/patch.ts
5527
+ var FENCE_RE = /^\s*(```|~~~)/;
5528
+ var HEADING_RE = /^(#{1,6})\s+(.*)$/;
5529
+ function frontmatterOffset(lines) {
5530
+ if (lines[0] !== "---") return 0;
5531
+ for (let i = 1; i < lines.length; i++) {
5532
+ if (lines[i] === "---") return i + 1;
5533
+ }
5534
+ return 0;
5535
+ }
5536
+ function buildHeadingTree(lines, bodyOffset) {
5537
+ const flat = [];
5538
+ let inFence = false;
5539
+ for (let i = bodyOffset; i < lines.length; i++) {
5540
+ const line = lines[i];
5541
+ if (FENCE_RE.test(line)) {
5542
+ inFence = !inFence;
5543
+ continue;
5544
+ }
5545
+ if (inFence) continue;
5546
+ const m = HEADING_RE.exec(line);
5547
+ if (m) flat.push({ level: m[1].length, text: m[2].trim(), line: i });
5548
+ }
5549
+ const root = { level: 0, heading: "", headingLine: bodyOffset - 1, bodyStart: bodyOffset, bodyEnd: lines.length, children: [] };
5550
+ const stack = [root];
5551
+ for (const h of flat) {
5552
+ while (stack.length > 1 && stack[stack.length - 1].level >= h.level) {
5553
+ const done = stack.pop();
5554
+ if (done) done.bodyEnd = h.line;
5555
+ }
5556
+ const node = { level: h.level, heading: h.text, headingLine: h.line, bodyStart: h.line + 1, bodyEnd: lines.length, children: [] };
5557
+ stack[stack.length - 1].children.push(node);
5558
+ stack.push(node);
5559
+ }
5560
+ return root;
5561
+ }
5562
+ function normalizeText(s) {
5563
+ return s.trim().replace(/\s+/g, " ");
5564
+ }
5565
+ function splitHeadingPath(target) {
5566
+ return target.split(">").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
5567
+ }
5568
+ function collectDescendants(node, acc) {
5569
+ acc.push(node);
5570
+ for (const child of node.children) collectDescendants(child, acc);
5571
+ }
5572
+ function resolveHeadingPath(root, segments) {
5573
+ if (segments.length === 0) return root;
5574
+ const allNodes = [];
5575
+ collectDescendants(root, allNodes);
5576
+ const headingNodes = allNodes.filter((n) => n.level > 0);
5577
+ const firstNorm = normalizeText(segments[0]);
5578
+ let candidates = headingNodes.filter((n) => normalizeText(n.heading) === firstNorm);
5579
+ if (candidates.length === 0) {
5580
+ const available = headingNodes.map((n) => n.heading);
5581
+ throw new UserError(`No section matching "${segments[0]}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
5582
+ }
5583
+ for (let i = 1; i < segments.length; i++) {
5584
+ const segment = segments[i];
5585
+ const norm = normalizeText(segment);
5586
+ const next = [];
5587
+ for (const candidate of candidates) {
5588
+ for (const child of candidate.children) {
5589
+ if (normalizeText(child.heading) === norm) next.push(child);
5590
+ }
5591
+ }
5592
+ if (next.length === 0) {
5593
+ const available = [...new Set(candidates.flatMap((c) => c.children.map((child) => child.heading)))];
5594
+ throw new UserError(`No section matching "${segment}" found (available headings: ${available.length > 0 ? available.join(", ") : "none"})`);
5595
+ }
5596
+ candidates = next;
5597
+ }
5598
+ if (candidates.length > 1) {
5599
+ throw new UserError(`Target selector "${segments.join(" > ")}" matched ${candidates.length} sections; be more specific`);
5600
+ }
5601
+ return candidates[0];
5602
+ }
5603
+ function findMarkerLine(lines, start, end, marker) {
5604
+ const norm = normalizeText(marker);
5605
+ const matches = [];
5606
+ for (let i = start; i < end; i++) {
5607
+ if (normalizeText(lines[i]).includes(norm)) matches.push(i);
5608
+ }
5609
+ if (matches.length === 0) {
5610
+ throw new UserError(`No line matching marker "${marker}" found in target section`);
5611
+ }
5612
+ if (matches.length > 1) {
5613
+ const preview = matches.map((i) => `line ${i + 1}: "${lines[i].trim()}"`).join("; ");
5614
+ throw new UserError(`Marker "${marker}" matched ${matches.length} lines, expected exactly 1 (${preview})`);
5615
+ }
5616
+ return matches[0];
5617
+ }
5618
+ function findChildByMarker(target, marker) {
5619
+ const norm = normalizeText(marker);
5620
+ const matches = target.children.filter((c) => normalizeText(c.heading).includes(norm));
5621
+ if (matches.length === 0) {
5622
+ const available = target.children.map((c) => c.heading).join(", ") || "none";
5623
+ throw new UserError(`No subsection matching "${marker}" found under target section (available: ${available})`);
5624
+ }
5625
+ if (matches.length > 1) {
5626
+ throw new UserError(`Marker "${marker}" matched ${matches.length} subsections: ${matches.map((c) => c.heading).join(", ")}`);
5627
+ }
5628
+ return matches[0];
5629
+ }
5630
+ function requireMarker(block) {
5631
+ if (!block.marker || block.marker.trim().length === 0) {
5632
+ throw new UserError(`Action "${block.action}" requires a marker`);
5633
+ }
5634
+ return block.marker;
5635
+ }
5636
+ function applyOneAction(lines, target, block) {
5637
+ const contentLines = block.content.length === 0 ? [""] : block.content.split("\n");
5638
+ switch (block.action) {
5639
+ case "append": {
5640
+ lines.splice(target.bodyEnd, 0, ...contentLines);
5641
+ return;
5642
+ }
5643
+ case "prepend": {
5644
+ lines.splice(target.bodyStart, 0, ...contentLines);
5645
+ return;
5646
+ }
5647
+ case "insert-after":
5648
+ case "insert-before": {
5649
+ const idx = findMarkerLine(lines, target.bodyStart, target.bodyEnd, requireMarker(block));
5650
+ lines.splice(block.action === "insert-after" ? idx + 1 : idx, 0, ...contentLines);
5651
+ return;
5652
+ }
5653
+ case "replace-section":
5654
+ case "delete-section": {
5655
+ const child = findChildByMarker(target, requireMarker(block));
5656
+ const replacement = block.action === "replace-section" ? contentLines : [];
5657
+ lines.splice(child.headingLine, child.bodyEnd - child.headingLine, ...replacement);
5658
+ return;
5659
+ }
5660
+ default: {
5661
+ const exhaustive = block.action;
5662
+ throw new UserError(`Unknown patch action "${exhaustive}"`);
5663
+ }
5664
+ }
5665
+ }
5666
+ function applyPatchBlocks(skillMd, blocks) {
5667
+ const lines = skillMd.split("\n");
5668
+ for (const block of blocks) {
5669
+ const bodyOffset = frontmatterOffset(lines);
5670
+ const root = buildHeadingTree(lines, bodyOffset);
5671
+ const segments = block.target ? splitHeadingPath(block.target) : [];
5672
+ const target = resolveHeadingPath(root, segments);
5673
+ applyOneAction(lines, target, block);
5674
+ }
5675
+ return lines.join("\n");
5676
+ }
5677
+ function renderExtendFile(meta) {
5678
+ return [
5679
+ "# SKILL.extend.md",
5680
+ `# Extensions for: ${meta.skillName}`,
5681
+ `# Generated: ${meta.generatedAt}`,
5682
+ `# Refinement: ${meta.refinementId}`,
5683
+ "",
5684
+ `## EXTEND: ${meta.targetSection}`,
5685
+ "<!-- ACTION: append -->",
5686
+ meta.content,
5687
+ ""
5688
+ ].join("\n");
5689
+ }
5690
+ function renderConfigOverride(meta) {
5691
+ const payload = {
5692
+ skill: meta.skillName,
5693
+ version: "1.0.0",
5694
+ generated: meta.generatedAt,
5695
+ config: meta.config
5696
+ };
5697
+ return `${JSON.stringify(payload, null, 2)}
5698
+ `;
5699
+ }
5700
+ function computeLcsDiff(a, b) {
5701
+ const n = a.length;
5702
+ const m = b.length;
5703
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
5704
+ for (let i2 = n - 1; i2 >= 0; i2--) {
5705
+ for (let j2 = m - 1; j2 >= 0; j2--) {
5706
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
5707
+ }
5708
+ }
5709
+ const ops = [];
5710
+ let i = 0;
5711
+ let j = 0;
5712
+ while (i < n && j < m) {
5713
+ if (a[i] === b[j]) {
5714
+ ops.push({ type: "context", line: a[i] });
5715
+ i++;
5716
+ j++;
5717
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
5718
+ ops.push({ type: "remove", line: a[i] });
5719
+ i++;
5720
+ } else {
5721
+ ops.push({ type: "add", line: b[j] });
5722
+ j++;
5723
+ }
5724
+ }
5725
+ while (i < n) {
5726
+ ops.push({ type: "remove", line: a[i] });
5727
+ i++;
5728
+ }
5729
+ while (j < m) {
5730
+ ops.push({ type: "add", line: b[j] });
5731
+ j++;
5732
+ }
5733
+ return ops;
5734
+ }
5735
+ function previewDiff(before, after, path) {
5736
+ const a = before.split("\n");
5737
+ const b = after.split("\n");
5738
+ const ops = computeLcsDiff(a, b);
5739
+ const lines = [`--- ${path}`, `+++ ${path}`];
5740
+ for (const op of ops) {
5741
+ const prefix = op.type === "add" ? "+" : op.type === "remove" ? "-" : " ";
5742
+ lines.push(`${prefix}${op.line}`);
5743
+ }
5744
+ return lines.join("\n");
5745
+ }
5746
+
5747
+ // src/commands/refine.ts
5748
+ var PRO_FEATURE_LABEL6 = "skill refinement";
5749
+ var REFINE_PROMPT_ASSET = "refine-prompt.md";
5750
+ function requireProFeature2() {
5751
+ const status2 = proFeatureStatus(PRO_FEATURE_LABEL6);
5752
+ if (!status2.enabled) {
5753
+ throw new UserError(status2.notice ?? "refine is a Pro feature \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock.");
5754
+ }
5755
+ if (status2.notice) console.error(`[skill-forge] ${status2.notice}`);
5756
+ }
5757
+ function isInteractive(json) {
5758
+ return !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
5759
+ }
5760
+ function emit(json, payload, renderText) {
5761
+ if (json) {
5762
+ console.log(JSON.stringify(payload, null, 2));
5763
+ } else {
5764
+ renderText();
5765
+ }
5766
+ }
5767
+ async function confirmYesNo(rl, question) {
5768
+ const answer = (await rl.question(question)).trim().toLowerCase();
5769
+ return answer === "y" || answer === "yes";
5770
+ }
5771
+ async function confirmWrite(yes, json, prompt) {
5772
+ if (yes) return true;
5773
+ if (!isInteractive(json)) {
5774
+ throw new UserError("refusing to write without confirmation \u2014 pass --yes, or run interactively.");
5775
+ }
5776
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
5777
+ try {
5778
+ return await confirmYesNo(rl, prompt);
5779
+ } finally {
5780
+ rl.close();
5781
+ }
5782
+ }
5783
+ function resolveConfiguredRoot3(root) {
5784
+ return resolvePath11(expandHome(root));
5785
+ }
5786
+ function isUnderRoot3(candidate, root) {
5787
+ const rel = relative3(root, candidate);
5788
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
5789
+ }
5790
+ function sanitizeDiffForConsole(diff) {
5791
+ return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
5792
+ }
5793
+ function resolveContentOption(options) {
5794
+ if (options.content !== void 0) return options.content;
5795
+ if (options.contentFile) return readFileSync15(resolvePath11(options.contentFile), "utf-8");
5796
+ return void 0;
5797
+ }
5798
+ function validateCaptureOptions(options) {
5799
+ if (!options.skill) throw new UserError("--skill is required");
5800
+ assertPlainSkillName(options.skill);
5801
+ if (!options.category || !isRefinementCategory(options.category)) {
5802
+ throw new UserError(`--category is required and must be one of: trigger, content, hook, tool, pattern, config, new`);
5803
+ }
5804
+ if (!options.target) throw new UserError("--target is required (heading-path selector or a short description for whole-file override types)");
5805
+ if (!options.overrideType || !isOverrideType(options.overrideType)) {
5806
+ throw new UserError("--override-type is required and must be one of: patch, extend, config, full, hook, script, new");
5807
+ }
5808
+ if (!options.expected) throw new UserError("--expected is required");
5809
+ if (!options.actual) throw new UserError("--actual is required");
5810
+ const scopeRaw = options.scope ?? "local";
5811
+ if (scopeRaw !== "local" && scopeRaw !== "shared") {
5812
+ throw new UserError('--scope must be "local" or "shared"');
5813
+ }
5814
+ let action;
5815
+ if (options.overrideType === "patch") {
5816
+ if (!options.action || !isPatchAction(options.action)) {
5817
+ throw new UserError(
5818
+ "--action is required for --override-type patch (one of: append, prepend, replace-section, insert-after, insert-before, delete-section)"
5819
+ );
5820
+ }
5821
+ action = options.action;
5822
+ }
5823
+ return {
5824
+ skill: options.skill,
5825
+ category: options.category,
5826
+ target: options.target,
5827
+ overrideType: options.overrideType,
5828
+ action,
5829
+ marker: options.marker,
5830
+ expected: options.expected,
5831
+ actual: options.actual,
5832
+ example: options.example,
5833
+ outcome: options.outcome,
5834
+ rootCause: options.rootCause,
5835
+ patternId: options.patternId,
5836
+ scope: scopeRaw,
5837
+ dryRun: options.dryRun ?? false,
5838
+ json: options.json ?? false,
5839
+ yes: options.yes ?? false,
5840
+ handoff: options.handoff ?? false
5841
+ };
5842
+ }
5843
+ function renderPatchActionBlock(params) {
5844
+ const lines = [];
5845
+ lines.push(params.marker ? `<!-- ACTION: ${params.action} "${params.marker}" -->` : `<!-- ACTION: ${params.action} -->`);
5846
+ lines.push(`<!-- TARGET: ${params.target} -->`);
5847
+ lines.push(params.content);
5848
+ return lines.join("\n");
5849
+ }
5850
+ function defaultOverrideFilename(overrideType) {
5851
+ switch (overrideType) {
5852
+ case "full":
5853
+ return "SKILL.md";
5854
+ case "hook":
5855
+ return "hooks/refinement.sh";
5856
+ case "script":
5857
+ return "scripts/refinement.sh";
5858
+ default: {
5859
+ const exhaustive = overrideType;
5860
+ throw new Error(`internal error: defaultOverrideFilename called for non-whole-file override type "${exhaustive}"`);
5861
+ }
5862
+ }
5863
+ }
5864
+ function renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent) {
5865
+ switch (opts.overrideType) {
5866
+ case "patch": {
5867
+ if (!content) throw new UserError("--content or --content-file is required for --override-type patch");
5868
+ const block = renderPatchActionBlock({ action: opts.action, marker: opts.marker, target: opts.target, content });
5869
+ const preamble = `# SKILL.patch.md
5870
+ # Patches for: ${opts.skill}
5871
+ `;
5872
+ const base = existingContent.trim().length > 0 ? existingContent.trimEnd() : preamble.trimEnd();
5873
+ return { relPath: "SKILL.patch.md", content: `${base}
5874
+
5875
+ ${block}
5876
+ ` };
5877
+ }
5878
+ case "extend":
5879
+ case "new": {
5880
+ if (!content) throw new UserError(`--content or --content-file is required for --override-type ${opts.overrideType}`);
5881
+ const rendered = renderExtendFile({ skillName: opts.skill, refinementId, generatedAt, targetSection: opts.target, content });
5882
+ const combined = existingContent.trim().length > 0 ? `${existingContent.trimEnd()}
5883
+
5884
+ ${rendered}` : rendered;
5885
+ return { relPath: "SKILL.extend.md", content: combined };
5886
+ }
5887
+ case "config": {
5888
+ if (!content) throw new UserError("--content or --content-file (JSON object) is required for --override-type config");
5889
+ let parsed;
5890
+ try {
5891
+ parsed = JSON.parse(content);
5892
+ } catch (err) {
5893
+ throw new UserError(`--content for --override-type config must be valid JSON: ${err.message}`);
5894
+ }
5895
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5896
+ throw new UserError("--content for --override-type config must be a JSON object");
5897
+ }
5898
+ const rendered = renderConfigOverride({
5899
+ skillName: opts.skill,
5900
+ refinementId,
5901
+ generatedAt,
5902
+ config: parsed
5903
+ });
5904
+ return { relPath: "skill-config.json", content: rendered };
5905
+ }
5906
+ case "full":
5907
+ case "hook":
5908
+ case "script": {
5909
+ if (!content) throw new UserError(`--content or --content-file is required for --override-type ${opts.overrideType}`);
5910
+ return { relPath: opts.marker && opts.marker.trim().length > 0 ? opts.marker.trim() : defaultOverrideFilename(opts.overrideType), content };
5911
+ }
5912
+ default: {
5913
+ const exhaustive = opts.overrideType;
5914
+ throw new UserError(`unknown override type "${exhaustive}"`);
5915
+ }
5916
+ }
5917
+ }
5918
+ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
5919
+ let lst;
5920
+ try {
5921
+ lst = lstatSync3(scopeDir);
5922
+ } catch {
5923
+ lst = void 0;
5924
+ }
5925
+ if (lst?.isSymbolicLink()) {
5926
+ throw new UserError(`refusing to write under ${scopeDir}: scope directory is a symlink`);
5927
+ }
5928
+ mkdirSync9(scopeDir, { recursive: true });
5929
+ const real = resolveConfiguredRoot3(scopeDir);
5930
+ const lexical = resolvePath11(scopeDir);
5931
+ let realResolved;
5932
+ try {
5933
+ realResolved = realpathSync7(scopeDir);
5934
+ } catch {
5935
+ realResolved = real;
5936
+ }
5937
+ if (realResolved !== lexical) {
5938
+ throw new UserError(`refusing to write under ${scopeDir}: path contains a symlink`);
5939
+ }
5940
+ if (!isUnderRoot3(realResolved, resolveConfiguredRoot3(resolvedCwd))) {
5941
+ throw new UserError(`refusing to write under ${scopeDir}: resolves outside the project root ${resolvedCwd}`);
5942
+ }
5943
+ return realResolved;
5944
+ }
5945
+ function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
5946
+ const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
5947
+ const destPath = join25(scopeDir, relPath);
5948
+ const destDir = dirname11(destPath);
5949
+ mkdirSync9(destDir, { recursive: true });
5950
+ const realDestDir = realpathSync7(destDir);
5951
+ if (!isUnderRoot3(realDestDir, realScopeDir)) {
5952
+ throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
5953
+ }
5954
+ let destLst;
5955
+ try {
5956
+ destLst = lstatSync3(destPath);
5957
+ } catch {
5958
+ destLst = void 0;
5959
+ }
5960
+ if (destLst?.isSymbolicLink()) {
5961
+ throw new UserError(`refusing to write ${destPath}: destination is a symlink`);
5962
+ }
5963
+ writeFileSync9(destPath, content, { mode: 384 });
5964
+ chmodSync(destPath, 384);
5965
+ return destPath;
5966
+ }
5967
+ async function refineCaptureCommand(rawOptions = {}) {
5968
+ requireProFeature2();
5969
+ const opts = validateCaptureOptions(rawOptions);
5970
+ const config = loadConfig();
5971
+ const cwd = process.cwd();
5972
+ const scopes = resolveScopes(config, cwd, opts.skill);
5973
+ const scopeDir = opts.scope === "local" ? scopes.local.dir : scopes.shared.dir;
5974
+ const content = resolveContentOption(rawOptions);
5975
+ const now = /* @__PURE__ */ new Date();
5976
+ const existingIds = new Set(readRefinementsStore().records.map((r) => r.id));
5977
+ const refinementId = generateRefinementId(existingIds, now, defaultRandomHex);
5978
+ const preview = renderCaptureArtifactPreview(opts, content, refinementId, now.toISOString(), scopeDir);
5979
+ if (opts.dryRun) {
5980
+ emit(opts.json, { dryRun: true, path: preview.destPath, diff: preview.diff }, () => {
5981
+ console.log(`[skill-forge] refine capture \u2014 ${opts.overrideType} override for "${sanitizeForMarkdown(opts.skill)}" at ${opts.scope} scope`);
5982
+ console.log(sanitizeDiffForConsole(preview.diff));
5983
+ });
5984
+ return;
5985
+ }
5986
+ if (!opts.json) {
5987
+ console.log(`[skill-forge] refine capture \u2014 ${opts.overrideType} override for "${sanitizeForMarkdown(opts.skill)}" at ${opts.scope} scope`);
5988
+ console.log(sanitizeDiffForConsole(preview.diff));
5989
+ }
5990
+ const proceed = await confirmWrite(opts.yes, opts.json, `
5991
+ Write ${preview.destPath}? [y/N] `);
5992
+ if (!proceed) {
5993
+ emit(opts.json, { written: false, cancelled: true, path: preview.destPath }, () => {
5994
+ console.log("[skill-forge] capture cancelled \u2014 nothing written.");
5995
+ });
5996
+ return;
5997
+ }
5998
+ const writtenPath = writeOverrideFile(scopeDir, preview.relPath, preview.content, cwd);
5999
+ let result;
6000
+ try {
6001
+ result = recordRefinement(
6002
+ {
6003
+ skill: opts.skill,
6004
+ category: opts.category,
6005
+ overrideType: opts.overrideType,
6006
+ targetSection: opts.target,
6007
+ expectedBehavior: opts.expected,
6008
+ actualBehavior: opts.actual,
6009
+ example: opts.example,
6010
+ desiredOutcome: opts.outcome,
6011
+ rootCause: opts.rootCause,
6012
+ project: cwd,
6013
+ projectRoot: cwd,
6014
+ patchAction: opts.action,
6015
+ patchMarker: opts.marker,
6016
+ patchContent: content,
6017
+ status: "applied",
6018
+ scope: opts.scope,
6019
+ patternId: opts.patternId
6020
+ },
6021
+ { now, id: refinementId }
6022
+ );
6023
+ } catch (err) {
6024
+ throw new UserError(`wrote the override artifact at ${writtenPath}, but recording the refinement in the store failed: ${err.message}`);
6025
+ }
6026
+ emit(
6027
+ opts.json,
6028
+ { written: true, path: writtenPath, refinementId: result.refinement.id, patternId: result.pattern.id, patternBecameReady: result.patternBecameReady },
6029
+ () => {
6030
+ console.log(`[skill-forge] wrote ${writtenPath}`);
6031
+ console.log(`[skill-forge] recorded refinement ${result.refinement.id} -> pattern ${result.pattern.id} (status: ${result.pattern.status})`);
6032
+ if (result.patternBecameReady) {
6033
+ console.log(`[skill-forge] pattern ${result.pattern.id} is now READY (seen in a second project) \u2014 consider \`skill-forge refine promote ${result.pattern.id}\``);
6034
+ }
6035
+ }
6036
+ );
6037
+ if (opts.handoff) {
6038
+ runHandoff(config, writtenPath, { promptAsset: REFINE_PROMPT_ASSET, log: opts.json ? console.error : console.log });
6039
+ }
6040
+ }
6041
+ function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
6042
+ const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
6043
+ const probePath = probeRelPath ? join25(scopeDir, probeRelPath) : void 0;
6044
+ const existingContent = probePath && existsSync20(probePath) ? readFileSync15(probePath, "utf-8") : "";
6045
+ const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
6046
+ const relPath = rendered.relPath;
6047
+ const destPath = join25(scopeDir, relPath);
6048
+ const before = existsSync20(destPath) ? readFileSync15(destPath, "utf-8") : "";
6049
+ const diff = previewDiff(before, rendered.content, destPath);
6050
+ return { relPath, content: rendered.content, destPath, diff };
6051
+ }
6052
+ async function refineListCommand(options = {}) {
6053
+ requireProFeature2();
6054
+ const { records } = readRefinementsStore();
6055
+ let rows = records;
6056
+ if (options.skill) rows = rows.filter((r) => r.skill === options.skill);
6057
+ if (options.project) rows = rows.filter((r) => r.project === options.project || r.projectRoot === resolvePath11(options.project));
6058
+ if (options.status) {
6059
+ if (!isRefinementStatus(options.status)) {
6060
+ throw new UserError("--status must be one of: pending, applied, generalized");
6061
+ }
6062
+ rows = rows.filter((r) => r.status === options.status);
6063
+ }
6064
+ const filtered = Boolean(options.skill || options.project || options.status);
6065
+ emit(options.json, { records: rows }, () => {
6066
+ if (rows.length === 0) {
6067
+ if (filtered && records.length > 0) {
6068
+ console.log(`[skill-forge] no refinements match the given filter(s) (${records.length} total recorded).`);
6069
+ } else {
6070
+ console.log("[skill-forge] no refinements recorded.");
6071
+ }
6072
+ return;
6073
+ }
6074
+ console.log(`[skill-forge] ${rows.length} refinement(s):`);
6075
+ for (const r of rows) {
6076
+ console.log(
6077
+ ` ${r.id} ${sanitizeForMarkdown(r.skill)} [${r.category}/${r.overrideType}] status=${r.status} pattern=${r.patternId ?? "(none)"} ${r.createdAt}`
6078
+ );
6079
+ }
6080
+ });
6081
+ }
6082
+ async function refinePatternsCommand(options = {}) {
6083
+ requireProFeature2();
6084
+ const { records } = readPatternsStore();
6085
+ let rows = records;
6086
+ if (options.status) {
6087
+ if (!isPatternStatus(options.status)) {
6088
+ throw new UserError("--status must be one of: tracking, ready, generalized, dismissed");
6089
+ }
6090
+ rows = rows.filter((p) => p.status === options.status);
6091
+ }
6092
+ if (options.skill) rows = rows.filter((p) => p.affectedSkills.includes(options.skill));
6093
+ if (options.project) {
6094
+ const resolvedProject = resolvePath11(options.project);
6095
+ rows = rows.filter((p) => p.occurrences.some((o) => o.projectRealpath === resolvedProject));
6096
+ }
6097
+ const filtered = Boolean(options.skill || options.project || options.status);
6098
+ emit(options.json, { records: rows.map((p) => ({ ...p, count: patternCount(p) })) }, () => {
6099
+ if (rows.length === 0) {
6100
+ if (filtered && records.length > 0) {
6101
+ console.log(`[skill-forge] no patterns match the given filter(s) (${records.length} total recorded).`);
6102
+ } else {
6103
+ console.log("[skill-forge] no patterns recorded.");
6104
+ }
6105
+ return;
6106
+ }
6107
+ console.log(`[skill-forge] ${rows.length} pattern(s):`);
6108
+ for (const p of rows) {
6109
+ console.log(
6110
+ ` ${p.id} ${sanitizeForMarkdown(p.name)} status=${p.status} count=${patternCount(p)} skills=${p.affectedSkills.map(sanitizeForMarkdown).join(", ")}`
6111
+ );
6112
+ }
6113
+ });
6114
+ }
6115
+ function renderInventoryLine(row) {
6116
+ const present = [
6117
+ row.skillMd ? "SKILL.md" : void 0,
6118
+ row.patchMd ? "SKILL.patch.md" : void 0,
6119
+ row.extendMd ? "SKILL.extend.md" : void 0,
6120
+ row.configJson ? "skill-config.json" : void 0,
6121
+ row.hooksDir ? "hooks/" : void 0,
6122
+ row.scriptsDir ? "scripts/" : void 0
6123
+ ].filter((v) => Boolean(v));
6124
+ return ` [${row.scope}] ${row.dir}${row.dirExists ? "" : " (missing)"} \u2014 ${present.length > 0 ? present.join(", ") : "(no override files)"}`;
6125
+ }
6126
+ async function refineWhichCommand(skill, options = {}) {
6127
+ requireProFeature2();
6128
+ if (!skill) throw new UserError("a skill name is required: skill-forge refine which <skill>");
6129
+ const config = loadConfig();
6130
+ const cwd = process.cwd();
6131
+ const scopes = resolveScopes(config, cwd, skill, options.userRoot);
6132
+ const inventory = inventoryAllScopes(scopes);
6133
+ emit(options.json, { scopes, inventory }, () => {
6134
+ console.log(`[skill-forge] refine which "${sanitizeForMarkdown(skill)}" \u2014 resolution order (highest precedence first):`);
6135
+ for (const row of inventory) console.log(renderInventoryLine(row));
6136
+ if (!scopes.user.resolved) {
6137
+ console.log(
6138
+ ` [user] ambiguous \u2014 candidates: ${scopes.user.candidates.map((c) => c.root).join(", ") || "(none configured)"} (pass --user-root to disambiguate)`
6139
+ );
6140
+ }
6141
+ });
6142
+ }
6143
+ function writePostWriteSidecar(backupId, files) {
6144
+ const sidecar = { schemaVersion: 1, files };
6145
+ const dir = join25(getBackupsDir(), backupId);
6146
+ mkdirSync9(dir, { recursive: true });
6147
+ writeFileSync9(join25(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
6148
+ }
6149
+ function readPostWriteSidecar(backupId) {
6150
+ const path = join25(getBackupsDir(), backupId, "post.json");
6151
+ if (!existsSync20(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
6152
+ let raw;
6153
+ try {
6154
+ raw = readFileSync15(path, "utf-8");
6155
+ } catch {
6156
+ return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
6157
+ }
6158
+ const hashes = /* @__PURE__ */ new Map();
6159
+ try {
6160
+ const parsed = JSON.parse(raw);
6161
+ for (const f of parsed.files ?? []) {
6162
+ if (typeof f !== "object" || f === null || typeof f.path !== "string" || typeof f.sha256 !== "string") {
6163
+ throw new Error("malformed post.json files entry");
6164
+ }
6165
+ hashes.set(f.path, f.sha256);
6166
+ }
6167
+ } catch {
6168
+ return { hashes: /* @__PURE__ */ new Map(), status: "corrupt" };
6169
+ }
6170
+ return { hashes, status: "ok" };
6171
+ }
6172
+ async function refinePromoteCommand(patternId, options = {}) {
6173
+ requireProFeature2();
6174
+ if (!patternId) throw new UserError("a pattern id is required: skill-forge refine promote <pattern-id>");
6175
+ const patternsStore = readPatternsStore();
6176
+ const pattern = patternsStore.records.find((p) => p.id === patternId);
6177
+ if (!pattern) throw new UserError(`no pattern with id: ${patternId}`);
6178
+ if (pattern.status === "generalized" && !options.force) {
6179
+ throw new UserError(`pattern ${patternId} is already generalized (backup ${pattern.generalizedBackupId ?? "(unknown)"}) \u2014 pass --force to re-promote.`);
6180
+ }
6181
+ if (pattern.status !== "ready" && pattern.status !== "generalized" && !options.force) {
6182
+ throw new UserError(`pattern ${patternId} is not ready (status: ${pattern.status}) \u2014 pass --force to promote anyway.`);
6183
+ }
6184
+ const skill = pattern.affectedSkills[0];
6185
+ if (!skill) throw new UserError(`pattern ${patternId} has no affected skills recorded`);
6186
+ assertPlainSkillName(skill);
6187
+ const config = loadConfig();
6188
+ const cwd = process.cwd();
6189
+ const scopes = resolveScopes(config, cwd, skill, options.userRoot);
6190
+ if (!scopes.user.resolved) {
6191
+ throw new UserError(
6192
+ `cannot resolve a single user scope for skill "${skill}" \u2014 candidates: ${scopes.user.candidates.map((c) => c.root).join(", ") || "(none configured)"}. Pass --user-root to disambiguate.`
6193
+ );
6194
+ }
6195
+ const skillDir = scopes.user.dir;
6196
+ const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
6197
+ let realUserRoot;
6198
+ try {
6199
+ realUserRoot = realpathSync7(resolvedUserRoot);
6200
+ } catch {
6201
+ realUserRoot = resolvedUserRoot;
6202
+ }
6203
+ let realSkillDir;
6204
+ try {
6205
+ realSkillDir = realpathSync7(skillDir);
6206
+ } catch {
6207
+ realSkillDir = resolvePath11(skillDir);
6208
+ }
6209
+ if (!isUnderRoot3(realSkillDir, realUserRoot)) {
6210
+ throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
6211
+ }
6212
+ const skillMdPath = join25(skillDir, "SKILL.md");
6213
+ if (!existsSync20(skillMdPath)) {
6214
+ throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
6215
+ }
6216
+ const refinementsStore = readRefinementsStore();
6217
+ const patternRefinements = refinementsStore.records.filter((r) => pattern.refinementIds.includes(r.id));
6218
+ const representative = patternRefinements[patternRefinements.length - 1];
6219
+ if (!representative) throw new UserError(`pattern ${patternId} has no recorded refinements to promote`);
6220
+ let block;
6221
+ if (representative.overrideType === "patch" && representative.patchAction && representative.patchContent) {
6222
+ block = { action: representative.patchAction, marker: representative.patchMarker, target: representative.targetSection, content: representative.patchContent };
6223
+ } else if ((representative.overrideType === "extend" || representative.overrideType === "new") && representative.patchContent) {
6224
+ block = { action: "append", target: representative.targetSection, content: representative.patchContent };
6225
+ } else {
6226
+ throw new UserError(
6227
+ `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.`
6228
+ );
6229
+ }
6230
+ const before = readFileSync15(skillMdPath, "utf-8");
6231
+ const after = applyPatchBlocks(before, [block]);
6232
+ const diff = previewDiff(before, after, skillMdPath);
6233
+ if (options.dryRun) {
6234
+ emit(options.json, { dryRun: true, path: skillMdPath, diff }, () => {
6235
+ console.log(`[skill-forge] refine promote ${patternId} -> ${skillMdPath}`);
6236
+ console.log(sanitizeDiffForConsole(diff));
6237
+ });
6238
+ return;
6239
+ }
6240
+ if (!options.json) {
6241
+ console.log(`[skill-forge] refine promote ${patternId} -> ${skillMdPath}`);
6242
+ console.log(sanitizeDiffForConsole(diff));
6243
+ }
6244
+ const proceed = await confirmWrite(options.yes, options.json, `
6245
+ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
6246
+ if (!proceed) {
6247
+ emit(options.json, { promoted: false, cancelled: true }, () => {
6248
+ console.log("[skill-forge] promote cancelled \u2014 nothing written.");
6249
+ });
6250
+ return;
6251
+ }
6252
+ const backup = createBackup([skillMdPath]);
6253
+ const tempPath = `${skillMdPath}.tmp-${randomUUID4()}`;
6254
+ try {
6255
+ writeFileSync9(tempPath, after, "utf-8");
6256
+ renameSync5(tempPath, skillMdPath);
6257
+ } catch (err) {
6258
+ rmSync7(tempPath, { force: true });
6259
+ throw err;
6260
+ }
6261
+ try {
6262
+ const postSha256 = createHash3("sha256").update(readFileSync15(skillMdPath)).digest("hex");
6263
+ writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
6264
+ markGeneralized(patternId, backup.id);
6265
+ const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
6266
+ appendProvenance(
6267
+ {
6268
+ name: skill,
6269
+ source: `skill-forge refine (${patternId})`,
6270
+ verb: "DEFER",
6271
+ target: skillMdPath,
6272
+ took: `applied a ${block.action} patch materializing pattern ${patternId}`,
6273
+ notes: `generalized from ${patternId} via skill-forge refine`
6274
+ },
6275
+ skillsRoot
6276
+ );
6277
+ } catch (err) {
6278
+ throw new UserError(
6279
+ `the user-scope skill was updated at ${skillMdPath} (backup ${backup.id}), but recording post-write verification and/or pattern/provenance state failed: ${err.message}`
6280
+ );
6281
+ }
6282
+ emit(options.json, { promoted: true, path: skillMdPath, backupId: backup.id }, () => {
6283
+ console.log(`[skill-forge] promoted pattern ${patternId} -> ${skillMdPath} (backup: ${backup.id})`);
6284
+ });
6285
+ }
6286
+ function assertBackupDirContained(backupId) {
6287
+ const backupsDir = getBackupsDir();
6288
+ if (!existsSync20(backupsDir)) return;
6289
+ const realBackupsDir = realpathSync7(backupsDir);
6290
+ const backupDir = join25(backupsDir, backupId);
6291
+ if (!existsSync20(backupDir)) return;
6292
+ const realBackupDir = realpathSync7(backupDir);
6293
+ if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
6294
+ throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
6295
+ }
6296
+ }
6297
+ function resolveTargetRealPath(path) {
6298
+ let lexicalAncestor = resolvePath11(path);
6299
+ while (true) {
6300
+ try {
6301
+ const real = realpathSync7(lexicalAncestor);
6302
+ const remainder = relative3(lexicalAncestor, resolvePath11(path));
6303
+ return remainder === "" ? real : join25(real, remainder);
6304
+ } catch {
6305
+ const parent = dirname11(lexicalAncestor);
6306
+ if (parent === lexicalAncestor) return resolvePath11(path);
6307
+ lexicalAncestor = parent;
6308
+ }
6309
+ }
6310
+ }
6311
+ function assertManifestPathsWithinRoots(manifest, config) {
6312
+ if (config.skillsRoots.length === 0) {
6313
+ throw new UserError(`refusing rollback ${manifest.id}: no skillsRoots are configured \u2014 cannot verify the backup's file paths are within a trusted skills root.`);
6314
+ }
6315
+ const roots = config.skillsRoots.map((root) => {
6316
+ const resolved = resolveConfiguredRoot3(root);
6317
+ try {
6318
+ return realpathSync7(resolved);
6319
+ } catch {
6320
+ return resolved;
6321
+ }
6322
+ });
6323
+ for (const file of manifest.files) {
6324
+ const resolvedTarget = resolveTargetRealPath(file.path);
6325
+ const withinAnyRoot = roots.some((root) => isUnderRoot3(resolvedTarget, root));
6326
+ if (!withinAnyRoot) {
6327
+ throw new UserError(`refusing rollback ${manifest.id}: backup manifest references a path outside configured skills roots \u2014 ${file.path}`);
6328
+ }
6329
+ }
6330
+ }
6331
+ function assertNoSymlinkLeaves(manifest) {
6332
+ for (const file of manifest.files) {
6333
+ let lst;
6334
+ try {
6335
+ lst = lstatSync3(file.path);
6336
+ } catch {
6337
+ lst = void 0;
6338
+ }
6339
+ if (lst?.isSymbolicLink()) {
6340
+ throw new UserError(`refusing rollback ${manifest.id}: backup manifest path is currently a symlink \u2014 refusing to follow or overwrite it: ${file.path}`);
6341
+ }
6342
+ }
6343
+ }
6344
+ function findLexicalRoot(filePath, config) {
6345
+ const lexicalTarget = resolvePath11(filePath);
6346
+ for (const root of config.skillsRoots) {
6347
+ const rootLexical = resolveConfiguredRoot3(root);
6348
+ const rel = relative3(rootLexical, lexicalTarget);
6349
+ if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute3(rel))) continue;
6350
+ let rootReal;
6351
+ try {
6352
+ rootReal = realpathSync7(rootLexical);
6353
+ } catch {
6354
+ rootReal = rootLexical;
6355
+ }
6356
+ return { rootLexical, rootReal };
6357
+ }
6358
+ throw new UserError(`refusing to operate on ${filePath}: does not resolve lexically under any configured skills root`);
6359
+ }
6360
+ function walkRealComponents(filePath, config, createMissing) {
6361
+ const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
6362
+ const relPath = relative3(rootLexical, resolvePath11(filePath));
6363
+ const components = relPath.split(sep2).filter((c) => c.length > 0);
6364
+ if (components.length === 0) {
6365
+ throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
6366
+ }
6367
+ const leafComponent = components[components.length - 1];
6368
+ const dirComponents = components.slice(0, -1);
6369
+ let current = rootReal;
6370
+ for (const component of dirComponents) {
6371
+ if (current === void 0) break;
6372
+ const next = join25(current, component);
6373
+ let lst;
6374
+ try {
6375
+ lst = lstatSync3(next);
6376
+ } catch {
6377
+ lst = void 0;
6378
+ }
6379
+ if (lst === void 0) {
6380
+ if (!createMissing) {
6381
+ current = void 0;
6382
+ continue;
6383
+ }
6384
+ mkdirSync9(next);
6385
+ current = next;
6386
+ continue;
6387
+ }
6388
+ if (lst.isSymbolicLink()) {
6389
+ throw new UserError(`refusing to operate on ${filePath}: intermediate path component is a symlink \u2014 ${next}`);
6390
+ }
6391
+ if (!lst.isDirectory()) {
6392
+ throw new UserError(`refusing to operate on ${filePath}: intermediate path component is not a directory \u2014 ${next}`);
6393
+ }
6394
+ current = next;
6395
+ }
6396
+ return { parentReal: current, leafComponent, rootReal };
6397
+ }
6398
+ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
6399
+ const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, true);
6400
+ if (parentReal === void 0) {
6401
+ throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
6402
+ }
6403
+ const finalPath = join25(parentReal, leafComponent);
6404
+ if (!isUnderRoot3(finalPath, rootReal)) {
6405
+ throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
6406
+ }
6407
+ let leafLst;
6408
+ try {
6409
+ leafLst = lstatSync3(finalPath);
6410
+ } catch {
6411
+ leafLst = void 0;
6412
+ }
6413
+ if (leafLst?.isSymbolicLink()) {
6414
+ throw new UserError(`refusing to restore ${filePath}: it is now a symlink \u2014 refusing to follow or overwrite it.`);
6415
+ }
6416
+ const tempPath = `${finalPath}.tmp-${randomUUID4()}`;
6417
+ try {
6418
+ writeFileSync9(tempPath, contentBuffer);
6419
+ chmodSync(tempPath, mode);
6420
+ renameSync5(tempPath, finalPath);
6421
+ } catch (err) {
6422
+ rmSync7(tempPath, { force: true });
6423
+ throw err;
6424
+ }
6425
+ }
6426
+ function safeDeleteTombstone(filePath, config) {
6427
+ const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
6428
+ if (parentReal === void 0) return;
6429
+ const finalPath = join25(parentReal, leafComponent);
6430
+ if (!isUnderRoot3(finalPath, rootReal)) {
6431
+ throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
6432
+ }
6433
+ let leafLst;
6434
+ try {
6435
+ leafLst = lstatSync3(finalPath);
6436
+ } catch {
6437
+ leafLst = void 0;
6438
+ }
6439
+ if (leafLst === void 0) return;
6440
+ if (leafLst.isSymbolicLink()) {
6441
+ throw new UserError(`refusing to delete ${filePath}: it is now a symlink \u2014 refusing to follow or overwrite it.`);
6442
+ }
6443
+ rmSync7(finalPath, { force: true });
6444
+ }
6445
+ async function refineRollbackCommand(backupId, options = {}) {
6446
+ requireProFeature2();
6447
+ if (!backupId) throw new UserError("a backup id is required: skill-forge refine rollback <backup-id>");
6448
+ if (!isValidBackupId(backupId)) {
6449
+ throw new UserError(`invalid backup id "${backupId}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
6450
+ }
6451
+ assertBackupDirContained(backupId);
6452
+ const manifest = readBackupManifest(backupId);
6453
+ const config = loadConfig();
6454
+ assertManifestPathsWithinRoots(manifest, config);
6455
+ assertNoSymlinkLeaves(manifest);
6456
+ const warnings = [];
6457
+ const sidecar = readPostWriteSidecar(backupId);
6458
+ if (sidecar.status !== "ok") {
6459
+ const reason = sidecar.status;
6460
+ if (!options.force) {
6461
+ throw new UserError(
6462
+ `refusing rollback ${backupId}: post-write verification data (post.json) is ${reason} \u2014 cannot confirm the current file state matches what was promoted. Pass --force to override.`
6463
+ );
6464
+ }
6465
+ warnings.push(`post-write verification data for backup ${backupId} is ${reason} \u2014 restoring without hash verification (--force).`);
6466
+ }
6467
+ const mismatches = [];
6468
+ for (const file of manifest.files) {
6469
+ const expected = sidecar.hashes.get(file.path);
6470
+ if (expected === void 0) continue;
6471
+ const currentSha256 = existsSync20(file.path) ? createHash3("sha256").update(readFileSync15(file.path)).digest("hex") : null;
6472
+ if (currentSha256 !== expected) mismatches.push(file.path);
6473
+ }
6474
+ if (mismatches.length > 0 && !options.force) {
6475
+ throw new UserError(
6476
+ `refusing rollback ${backupId}: current file(s) differ from the state recorded right after promote \u2014 ${mismatches.join(", ")} (pass --force to override)`
6477
+ );
6478
+ }
6479
+ const fileList = manifest.files.map((file) => ` ${file.existed ? "restore" : "delete"} ${file.path}`).join("\n");
6480
+ if (!options.json) {
6481
+ console.log(`[skill-forge] refine rollback ${backupId} \u2014 restoring ${manifest.files.length} file(s):`);
6482
+ console.log(fileList);
6483
+ for (const w of warnings) console.log(`[skill-forge] warning: ${w}`);
6484
+ }
6485
+ const proceed = await confirmWrite(options.yes, options.json, `
6486
+ Roll back backup ${backupId}? Affected files:
6487
+ ${fileList}
6488
+ [y/N] `);
6489
+ if (!proceed) {
6490
+ emit(options.json, { rolledBack: false, cancelled: true, backupId }, () => {
6491
+ console.log("[skill-forge] rollback cancelled \u2014 nothing restored.");
6492
+ });
6493
+ return;
6494
+ }
6495
+ const restored = [];
6496
+ for (const file of manifest.files) {
6497
+ try {
6498
+ if (!file.existed) {
6499
+ safeDeleteTombstone(file.path, config);
6500
+ } else {
6501
+ safeRestoreWrite(file.path, Buffer.from(file.contentBase64 ?? "", "base64"), file.mode ?? 420, config);
6502
+ }
6503
+ restored.push(file.path);
6504
+ } catch (err) {
6505
+ const remaining = manifest.files.slice(restored.length + 1).map((f) => f.path);
6506
+ throw new UserError(
6507
+ `rollback ${backupId} failed partway through: restored ${restored.length} of ${manifest.files.length} file(s) (${restored.join(", ") || "(none)"}), then failed on ${file.path}: ${err.message}. NOT restored: ${[file.path, ...remaining].join(", ")}.`
6508
+ );
6509
+ }
6510
+ }
6511
+ const patternsStore = readPatternsStore();
6512
+ const pattern = patternsStore.records.find((p) => p.generalizedBackupId === backupId);
6513
+ if (pattern) {
6514
+ pattern.status = "ready";
6515
+ pattern.generalizedAt = void 0;
6516
+ pattern.generalizedBackupId = void 0;
6517
+ writePatternsStore(patternsStore);
6518
+ }
6519
+ emit(options.json, { rolledBack: true, backupId, patternReset: pattern?.id, warnings }, () => {
6520
+ console.log(`[skill-forge] rollback complete for ${backupId}${pattern ? ` (pattern ${pattern.id} reset to ready)` : ""}`);
6521
+ for (const w of warnings) console.log(`[skill-forge] warning: ${w}`);
6522
+ });
6523
+ }
6524
+
6525
+ // src/cli.ts
6526
+ var require2 = createRequire(import.meta.url);
6527
+ var pkg = require2("../package.json");
6528
+ function parseArtifact(value) {
6529
+ if (value !== "skill" && value !== "mcp") {
6530
+ throw new UserError(`--artifact must be "skill" or "mcp" (got "${value}")`);
6531
+ }
6532
+ return value;
6533
+ }
6534
+ function parseLookbackHours(value) {
6535
+ const n = Number(value);
6536
+ if (value.trim() === "" || !Number.isFinite(n) || n < 0) {
6537
+ throw new UserError(`--lookback-hours must be a non-negative number (got "${value}")`);
6538
+ }
6539
+ return n;
6540
+ }
6541
+ function parseLimit(value) {
6542
+ const n = Number(value);
6543
+ if (value.trim() === "" || !Number.isInteger(n) || n < 1 || n > 50) {
6544
+ throw new UserError(`--limit must be an integer between 1 and 50 (got "${value}")`);
3990
6545
  }
3991
6546
  return n;
3992
6547
  }
@@ -4001,6 +6556,21 @@ program.command("list").description("List skills currently in quarantine").actio
4001
6556
  program.command("status").description("Show skill-forge configuration and quarantine status").action(status);
4002
6557
  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);
4003
6558
  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);
6559
+ program.command("organize").description("Build the set-level capability registry + dependency graph across configured skills roots (Pro)").option("--json", "print the registry+graph report as JSON instead of the terminal summary").option("--out <file>", "write the full report JSON to this path (refuses an existing path)").option("--usage-snapshot <file>", "join per-skill usage counts from a skill-monitor snapshot JSON").action(organizeCommand);
6560
+ program.command("find [query]").description("Discover skills via skills.sh and check partner security audits (requires VERCEL_OIDC_TOKEN)").option("--limit <n>", "max search results (default 10)", parseLimit).option("--audit <id>", "show partner audit verdicts (Socket/Snyk/\u2026) for a skills.sh id").option("--get <id>", "show detail + file tree for a skills.sh id").option("--curated", "list skills.sh curated skills").option("--json", "print the raw API payload as JSON").action(find);
6561
+ program.command("watch").description("Drift check across every SOURCES.md provenance ledger: which ingested upstreams have moved (Pro)").option("--offline", "skip all network checks; list entries for manual comparison only").option("--json", "print the drift report as JSON instead of the terminal summary").action(watchCommand);
6562
+ program.command("ingest").description(
6563
+ "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."
6564
+ ).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
6565
+ var queue = program.command("queue").description("Manage the pending-ingestion queue (Pro)");
6566
+ queue.command("close <id>").description("Close a queue entry after the decide pass \u2014 agents use this instead of hand-editing queue.json").requiredOption("--status <status>", 'final status: "ingested" or "dismissed"').action(queueClose);
6567
+ var refine = program.command("refine").description("Capture, apply, and generalize improvements to installed skills from usage feedback (Pro)");
6568
+ refine.command("capture", { isDefault: true }).description("Record a refinement and write a project-scope override artifact (never touches the base skill)").option("--skill <name>", "target skill name").option("--category <cat>", "trigger|content|hook|tool|pattern|config|new").option("--target <section>", 'heading path or file the refinement targets, e.g. "Hooks > duplicate-check"').option("--override-type <type>", "patch|extend|config|full|hook|script|new").option("--action <action>", "patch action: append|prepend|replace-section|insert-after|insert-before|delete-section").option("--marker <text>", "marker text for insert-* / replace-section / delete-section (filename for full|hook|script)").option("--content <text>", "override content inline").option("--content-file <file>", "read override content from a file").option("--expected <text>", "expected behavior").option("--actual <text>", "actual observed behavior").option("--example <text>", "concrete example of the gap").option("--outcome <text>", "desired outcome").option("--root-cause <text>", "root cause per the gap analysis").option("--pattern-id <id>", "join an existing pattern (PAT-xxxx) instead of fingerprint matching").option("--scope <scope>", 'where to write the artifact: "local" (.claude/skills, default) or "shared" (./skills)').option("--dry-run", "preview the artifact diff; write nothing").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm; never implies --handoff").option("--handoff", "authorizes launching the configured agent with the bundled refine prompt").action(refineCaptureCommand);
6569
+ refine.command("list").description("Show refinement history from the user-scope store").option("--status <status>", "filter by refinement status").option("--skill <name>", "filter by skill").option("--project <name>", "filter by project").option("--json", "print records as JSON").action(refineListCommand);
6570
+ refine.command("patterns").description("Review tracked patterns and the generalization queue").option("--status <status>", "filter: tracking|ready|generalized|dismissed").option("--skill <name>", "filter by affected skill").option("--project <name>", "filter by project").option("--json", "print patterns as JSON").action(refinePatternsCommand);
6571
+ refine.command("which <skill>").description("Print override-scope resolution order and which override files exist at each scope").option("--user-root <dir>", "explicit user-scope skills root").option("--json", "print the resolution as JSON").action(refineWhichCommand);
6572
+ refine.command("promote <pattern-id>").description("Materialize a READY pattern into the user-scope skill default (backup + manifest first)").option("--force", "promote a pattern that is not READY").option("--dry-run", "preview the user-scope diff; write nothing").option("--user-root <dir>", "explicit user-scope skills root").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm").action(refinePromoteCommand);
6573
+ refine.command("rollback <backup-id>").description("Restore a promotion backup (refuses if files changed since promotion unless --force)").option("--force", "restore even if current files differ from the post-promotion state").option("--json", "print the result as JSON").option("-y, --yes", "skip the interactive confirm").action(refineRollbackCommand);
4004
6574
  program.command("init").description("Detect installed agents and set gate targets, default promotion target, and handoff agent").option("--defaults", "non-interactive: use all detected agents as gate targets, the first as the default target").option("--list", "print detected agent skill roots and exit \u2014 no writes").action(init);
4005
6575
  try {
4006
6576
  await program.parseAsync();