@rhize/skill-forge 0.14.0 → 0.16.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
@@ -8,7 +8,7 @@ import { createRequire } from "module";
8
8
  import { join as join21 } from "path";
9
9
 
10
10
  // src/config.ts
11
- import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
11
+ import { chmodSync, existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
12
12
 
13
13
  // src/errors.ts
14
14
  var UserError = class extends Error {
@@ -218,10 +218,10 @@ function validatePreference(key, value) {
218
218
  if (/[\u0000-\u001F\u007F-\u009F]/.test(value)) {
219
219
  throw new UserError(`preference "${key}" contains control characters \u2014 refusing to store terminal escape sequences`);
220
220
  }
221
- for (const { pattern, label } of SECRET_VALUE_PATTERNS) {
221
+ for (const { pattern, label: label2 } of SECRET_VALUE_PATTERNS) {
222
222
  if (pattern.test(value)) {
223
223
  throw new UserError(
224
- `refusing to store "${key}": the value looks like ${label}, and config.json is plain text. Keep secrets in your OS keychain or an env var.`
224
+ `refusing to store "${key}": the value looks like ${label2}, and config.json is plain text. Keep secrets in your OS keychain or an env var.`
225
225
  );
226
226
  }
227
227
  }
@@ -418,14 +418,30 @@ function saveConfig(config) {
418
418
  if (!existsSync(home)) {
419
419
  mkdirSync(home, { recursive: true });
420
420
  }
421
- writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
421
+ const path = getConfigPath();
422
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
423
+ chmodSync(path, 384);
422
424
  }
423
425
 
424
426
  // src/install/resolve.ts
425
427
  import { existsSync as existsSync2 } from "fs";
426
428
 
427
429
  // src/util.ts
428
- import { accessSync, constants, mkdirSync as mkdirSync2, renameSync, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
430
+ import {
431
+ accessSync,
432
+ closeSync,
433
+ constants,
434
+ fchmodSync,
435
+ fstatSync,
436
+ lstatSync,
437
+ mkdirSync as mkdirSync2,
438
+ openSync,
439
+ renameSync,
440
+ rmSync,
441
+ statSync as statSync2,
442
+ writeSync,
443
+ writeFileSync as writeFileSync2
444
+ } from "fs";
429
445
  import { homedir as homedir2 } from "os";
430
446
  import { basename, delimiter, dirname, join as join3 } from "path";
431
447
  import { randomUUID } from "crypto";
@@ -484,6 +500,48 @@ function writeNewFile(path, contents, hint) {
484
500
  throw err;
485
501
  }
486
502
  }
503
+ function appendPrivateJsonLine(path, value) {
504
+ const line = Buffer.from(`${JSON.stringify(value)}
505
+ `, "utf-8");
506
+ let priorStat;
507
+ try {
508
+ priorStat = lstatSync(path);
509
+ if (priorStat.isSymbolicLink()) throw new UserError(`refusing to append through a symlink: ${path}`);
510
+ if (!priorStat.isFile()) throw new UserError(`append destination must be a regular file: ${path}`);
511
+ } catch (error) {
512
+ if (error.code !== "ENOENT") throw error;
513
+ }
514
+ const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
515
+ let descriptor;
516
+ try {
517
+ descriptor = openSync(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | noFollow, 384);
518
+ const descriptorStat = fstatSync(descriptor);
519
+ if (!descriptorStat.isFile()) throw new UserError(`append destination must be a regular file: ${path}`);
520
+ const openedPathStat = lstatSync(path);
521
+ if (openedPathStat.isSymbolicLink()) throw new UserError(`refusing to append through a symlink: ${path}`);
522
+ if (!openedPathStat.isFile()) throw new UserError(`append destination must be a regular file: ${path}`);
523
+ if (openedPathStat.dev !== descriptorStat.dev || openedPathStat.ino !== descriptorStat.ino) {
524
+ throw new UserError(`append destination changed while opening: ${path}`);
525
+ }
526
+ if (priorStat && (priorStat.dev !== descriptorStat.dev || priorStat.ino !== descriptorStat.ino)) {
527
+ throw new UserError(`append destination changed while opening: ${path}`);
528
+ }
529
+ fchmodSync(descriptor, 384);
530
+ let offset = 0;
531
+ while (offset < line.byteLength) {
532
+ const written = writeSync(descriptor, line, offset, line.byteLength - offset);
533
+ if (written === 0) throw new Error(`unable to append JSONL record to ${path}`);
534
+ offset += written;
535
+ }
536
+ } catch (error) {
537
+ if (error.code === "ELOOP") {
538
+ throw new UserError(`refusing to append through a symlink: ${path}`);
539
+ }
540
+ throw error;
541
+ } finally {
542
+ if (descriptor !== void 0) closeSync(descriptor);
543
+ }
544
+ }
487
545
 
488
546
  // src/install/resolve.ts
489
547
  var GIT_URL_PATTERN = /^(git@|https?:\/\/|ssh:\/\/)/;
@@ -659,7 +717,7 @@ import { basename as basename3, join as join6 } from "path";
659
717
 
660
718
  // src/install/fingerprint.ts
661
719
  import { createHash } from "crypto";
662
- import { lstatSync, readFileSync as readFileSync3, readdirSync as readdirSync3, readlinkSync, realpathSync } from "fs";
720
+ import { lstatSync as lstatSync2, readFileSync as readFileSync3, readdirSync as readdirSync3, readlinkSync, realpathSync } from "fs";
663
721
  import { isAbsolute, join as join5, relative, sep } from "path";
664
722
  var SKIP_DIRS3 = /* @__PURE__ */ new Set([".git", "node_modules"]);
665
723
  function isInside(child, root) {
@@ -730,7 +788,7 @@ function fingerprintDirs(dirs) {
730
788
  }
731
789
  function isRealDirectory(path) {
732
790
  try {
733
- return lstatSync(path).isDirectory();
791
+ return lstatSync2(path).isDirectory();
734
792
  } catch {
735
793
  return false;
736
794
  }
@@ -971,8 +1029,8 @@ function appendProvenance(entry, skillsRoot) {
971
1029
  appendFileSync(ledgerPath, block, "utf-8");
972
1030
  return ledgerPath;
973
1031
  }
974
- function extractField(block, label) {
975
- const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1032
+ function extractField(block, label2) {
1033
+ const escaped = label2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
976
1034
  const match = block.match(new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.*)`));
977
1035
  return match ? match[1].trim() : void 0;
978
1036
  }
@@ -1031,7 +1089,7 @@ import { basename as basename7, dirname as dirname5, join as join13 } from "path
1031
1089
  // src/audit.ts
1032
1090
  import {
1033
1091
  existsSync as existsSync9,
1034
- lstatSync as lstatSync2,
1092
+ lstatSync as lstatSync3,
1035
1093
  mkdtempSync,
1036
1094
  readFileSync as readFileSync9,
1037
1095
  readdirSync as readdirSync5,
@@ -1465,14 +1523,14 @@ skill skills user claude code based comprehensive enhanced practical including i
1465
1523
  `.split(/\s+/).filter(Boolean)
1466
1524
  );
1467
1525
  var DEFAULT_SET_THRESHOLD = 0.45;
1468
- function splitLimited(text, sep5, maxSplit) {
1526
+ function splitLimited(text, sep7, maxSplit) {
1469
1527
  const parts = [];
1470
1528
  let rest = text;
1471
1529
  for (let i = 0; i < maxSplit; i++) {
1472
- const idx = rest.indexOf(sep5);
1530
+ const idx = rest.indexOf(sep7);
1473
1531
  if (idx === -1) break;
1474
1532
  parts.push(rest.slice(0, idx));
1475
- rest = rest.slice(idx + sep5.length);
1533
+ rest = rest.slice(idx + sep7.length);
1476
1534
  }
1477
1535
  parts.push(rest);
1478
1536
  return parts;
@@ -2570,7 +2628,7 @@ async function scaffoldFoundationSkill(config, profile, targetRoot) {
2570
2628
  const destDir = join12(resolvedRoot, "business-foundation");
2571
2629
  let destLstat;
2572
2630
  try {
2573
- destLstat = lstatSync2(destDir);
2631
+ destLstat = lstatSync3(destDir);
2574
2632
  } catch {
2575
2633
  destLstat = void 0;
2576
2634
  }
@@ -2742,10 +2800,10 @@ import { join as join16 } from "path";
2742
2800
  // src/gate/profile.ts
2743
2801
  import { execFileSync } from "child_process";
2744
2802
  import {
2745
- closeSync,
2803
+ closeSync as closeSync2,
2746
2804
  existsSync as existsSync12,
2747
2805
  mkdtempSync as mkdtempSync2,
2748
- openSync,
2806
+ openSync as openSync2,
2749
2807
  readFileSync as readFileSync12,
2750
2808
  readdirSync as readdirSync6,
2751
2809
  readSync,
@@ -3073,10 +3131,10 @@ function findSkillMdOneLevelDown(dir) {
3073
3131
  }
3074
3132
  function isZipFile(p) {
3075
3133
  try {
3076
- const fd = openSync(p, "r");
3134
+ const fd = openSync2(p, "r");
3077
3135
  const buf = Buffer.alloc(4);
3078
3136
  const bytesRead = readSync(fd, buf, 0, 4, 0);
3079
- closeSync(fd);
3137
+ closeSync2(fd);
3080
3138
  if (bytesRead < 4) return false;
3081
3139
  return buf[0] === 80 && buf[1] === 75 && (buf[2] === 3 || buf[2] === 5 || buf[2] === 7);
3082
3140
  } catch {
@@ -3179,8 +3237,8 @@ function detectLicense(root, fm) {
3179
3237
  const p = join15(root, name);
3180
3238
  if (existsSync12(p) && statSync7(p).isFile()) {
3181
3239
  const head = readFileSync12(p, "utf-8").slice(0, 2e3).toLowerCase();
3182
- for (const [hint, label] of Object.entries(SPDX_HINTS)) {
3183
- if (head.includes(hint)) return label;
3240
+ for (const [hint, label2] of Object.entries(SPDX_HINTS)) {
3241
+ if (head.includes(hint)) return label2;
3184
3242
  }
3185
3243
  return "present but unrecognized";
3186
3244
  }
@@ -3458,8 +3516,8 @@ function tagsForNode(map, node) {
3458
3516
  if (edge.from !== node.id) continue;
3459
3517
  if (edge.type !== "topic-tag" && edge.type !== "stack-tag") continue;
3460
3518
  const tagNode = nodeById(map, edge.to);
3461
- const label = tagNode?.name ?? tagNode?.id;
3462
- if (label) tags.add(label);
3519
+ const label2 = tagNode?.name ?? tagNode?.id;
3520
+ if (label2) tags.add(label2);
3463
3521
  }
3464
3522
  return Array.from(tags);
3465
3523
  }
@@ -3574,9 +3632,9 @@ function renderListField(labelText, items, placeholder) {
3574
3632
  if (!items || items.length === 0) {
3575
3633
  return `${head}${placeholder}`;
3576
3634
  }
3577
- const bullets = items.map((item) => ` - ${item}`).join("\n");
3635
+ const bullets2 = items.map((item) => ` - ${item}`).join("\n");
3578
3636
  return `${head.trimEnd()}
3579
- ${bullets}`;
3637
+ ${bullets2}`;
3580
3638
  }
3581
3639
  function renderSafetyLine(safety) {
3582
3640
  const detail = [safety.severity, safety.score !== void 0 ? `score ${safety.score}` : void 0].filter((v) => Boolean(v)).join(", ");
@@ -3836,11 +3894,14 @@ function generateId(existingIds) {
3836
3894
  // src/handoff.ts
3837
3895
  import { spawnSync } from "child_process";
3838
3896
  import { existsSync as existsSync15 } from "fs";
3839
- import { dirname as dirname7, join as join18 } from "path";
3897
+ import { dirname as dirname7, isAbsolute as isAbsolute2, join as join18, win32 } from "path";
3840
3898
  import { fileURLToPath } from "url";
3841
3899
  var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
3842
3900
  var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
3843
3901
  function resolvePromptPath(assetName = "ingest-prompt.md") {
3902
+ if (assetName.length === 0 || assetName.includes("\0") || isAbsolute2(assetName) || win32.isAbsolute(assetName) || assetName.split(/[\\/]/).includes("..")) {
3903
+ throw new UserError(`Invalid handoff prompt asset path: ${assetName}`);
3904
+ }
3844
3905
  const here = dirname7(fileURLToPath(import.meta.url));
3845
3906
  const builtCandidate = join18(here, assetName);
3846
3907
  if (existsSync15(builtCandidate)) return builtCandidate;
@@ -3857,29 +3918,35 @@ function substitute(template, vars) {
3857
3918
  function buildHandoffCommand(config, installedPath, deps = {}) {
3858
3919
  const isOnPath = deps.isOnPath ?? defaultIsOnPath;
3859
3920
  const promptPath = resolvePromptPath(deps.promptAsset);
3860
- const vars = { path: installedPath, prompt: promptPath };
3921
+ const baseVars = { path: installedPath, prompt: promptPath };
3922
+ const instruction = substitute([deps.instruction ?? HANDOFF_INSTRUCTION], baseVars)[0];
3923
+ const vars = { ...baseVars, instruction };
3861
3924
  if (config.handoffCommand && config.handoffCommand.length > 0) {
3862
- return { argv: substitute(config.handoffCommand, vars), source: "template" };
3925
+ const template = config.handoffCommand.map(
3926
+ (token) => deps.instruction !== void 0 && token === HANDOFF_INSTRUCTION ? "{instruction}" : token
3927
+ );
3928
+ return { argv: substitute(template, vars), source: "template" };
3863
3929
  }
3864
3930
  for (const bin of KNOWN_AGENT_BINARIES) {
3865
3931
  if (isOnPath(bin)) {
3866
- return { argv: substitute([bin, "-p", HANDOFF_INSTRUCTION], vars), source: "detected" };
3932
+ const template = bin === "codex" ? [bin, "exec", "{instruction}"] : [bin, "-p", "{instruction}"];
3933
+ return { argv: substitute(template, vars), source: "detected" };
3867
3934
  }
3868
3935
  }
3869
3936
  return { argv: [], source: "none" };
3870
3937
  }
3871
- function printOnlyMessage(installedPath, promptAsset) {
3938
+ function printOnlyMessage(installedPath, promptAsset, targetLabel = "Skill path") {
3872
3939
  const promptPath = resolvePromptPath(promptAsset);
3873
3940
  return `[skill-forge] no agent handoff available \u2014 run one yourself:
3874
3941
  Prompt: ${promptPath}
3875
- Skill path: ${installedPath}`;
3942
+ ${targetLabel}: ${installedPath}`;
3876
3943
  }
3877
3944
  function runHandoff(config, installedPath, options = {}) {
3878
3945
  const command = buildHandoffCommand(config, installedPath, options);
3879
3946
  const spawn2 = options.spawn ?? spawnSync;
3880
3947
  const log = options.log ?? console.log;
3881
3948
  if (command.source === "none" || command.argv.length === 0) {
3882
- const printed = printOnlyMessage(installedPath, options.promptAsset);
3949
+ const printed = printOnlyMessage(installedPath, options.promptAsset, options.targetLabel);
3883
3950
  log(printed);
3884
3951
  return { ran: false, command, printed };
3885
3952
  }
@@ -3887,7 +3954,7 @@ function runHandoff(config, installedPath, options = {}) {
3887
3954
  const result = spawn2(bin, args, { stdio: options.stdio ?? "inherit" });
3888
3955
  const error = result.error;
3889
3956
  if (error?.code === "ENOENT") {
3890
- const printed = printOnlyMessage(installedPath, options.promptAsset);
3957
+ const printed = printOnlyMessage(installedPath, options.promptAsset, options.targetLabel);
3891
3958
  log(`[skill-forge] ${bin} not found on PATH \u2014 run this yourself:`);
3892
3959
  log(printed);
3893
3960
  return { ran: false, command, printed, error: error.message };
@@ -4150,6 +4217,136 @@ var AGENTS = [
4150
4217
  { id: "adal", displayName: "AdaL", projectRoot: ".adal/skills", globalRoot: "~/.adal/skills" }
4151
4218
  ].map(makeAgent);
4152
4219
 
4220
+ // src/gitPreflight.ts
4221
+ import { relative as relative2, sep as sep2 } from "path";
4222
+ function splitNulTerminated(output) {
4223
+ return output.split("\0").filter((s) => s.length > 0);
4224
+ }
4225
+ function toPosixPath(p) {
4226
+ return p.split(sep2).join("/");
4227
+ }
4228
+ var missingResult = (root) => ({
4229
+ root,
4230
+ state: "MISSING",
4231
+ baseline: "NO_HEAD",
4232
+ dirty: false,
4233
+ otherStaged: false,
4234
+ rollbackReady: false
4235
+ });
4236
+ var notInRepoResult = (root) => ({
4237
+ root,
4238
+ state: "NOT_IN_REPO",
4239
+ baseline: "NO_HEAD",
4240
+ dirty: false,
4241
+ otherStaged: false,
4242
+ rollbackReady: false
4243
+ });
4244
+ async function evaluateGitPreflight(root, deps = {}) {
4245
+ const exec = deps.exec ?? defaultExec;
4246
+ if (!isDir(root)) return missingResult(root);
4247
+ let toplevel;
4248
+ try {
4249
+ toplevel = await exec("git", ["-C", root, "rev-parse", "--show-toplevel"], { cwd: root });
4250
+ } catch {
4251
+ return notInRepoResult(root);
4252
+ }
4253
+ if (toplevel.code !== 0) return notInRepoResult(root);
4254
+ const repoRoot = toplevel.stdout.trim();
4255
+ if (!repoRoot) return notInRepoResult(root);
4256
+ const rel = toPosixPath(relative2(repoRoot, root));
4257
+ const pathspec = rel === "" ? "." : rel;
4258
+ const tracked = await exec("git", ["--literal-pathspecs", "-C", repoRoot, "ls-files", "-z", "--", pathspec], { cwd: repoRoot });
4259
+ const untracked = await exec(
4260
+ "git",
4261
+ ["--literal-pathspecs", "-C", repoRoot, "ls-files", "-z", "--others", "--exclude-standard", "--", pathspec],
4262
+ { cwd: repoRoot }
4263
+ );
4264
+ const ignored = await exec(
4265
+ "git",
4266
+ ["--literal-pathspecs", "-C", repoRoot, "ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--", pathspec],
4267
+ { cwd: repoRoot }
4268
+ );
4269
+ let rootIgnored = false;
4270
+ if (rel !== "") {
4271
+ const checkIgnore = await exec("git", ["-C", repoRoot, "check-ignore", "--no-index", "-q", "--", pathspec], { cwd: repoRoot });
4272
+ rootIgnored = checkIgnore.code === 0;
4273
+ }
4274
+ const hasTracked = splitNulTerminated(tracked.stdout).length > 0;
4275
+ const hasUntracked = splitNulTerminated(untracked.stdout).length > 0;
4276
+ const hasIgnored = splitNulTerminated(ignored.stdout).length > 0 || rootIgnored;
4277
+ let state;
4278
+ if (hasTracked && hasUntracked) state = "MIXED";
4279
+ else if (hasTracked) state = "TRACKED";
4280
+ else if (hasUntracked) state = "UNTRACKED";
4281
+ else if (hasIgnored) state = "IGNORED";
4282
+ else state = "UNTRACKED";
4283
+ const headCheck = await exec("git", ["-C", repoRoot, "rev-parse", "--verify", "HEAD"], { cwd: repoRoot });
4284
+ const hasHead = headCheck.code === 0;
4285
+ const diffCached = await exec("git", ["-C", repoRoot, "diff", "--cached", "--name-only", "-z"], { cwd: repoRoot });
4286
+ const stagedFiles = splitNulTerminated(diffCached.stdout);
4287
+ const otherStaged = rel !== "" && stagedFiles.some((f) => f !== pathspec && !f.startsWith(`${pathspec}/`));
4288
+ if (!hasHead) {
4289
+ return { root, repoRoot, state, baseline: "NO_HEAD", dirty: hasTracked || hasUntracked, otherStaged, rollbackReady: false };
4290
+ }
4291
+ const committed = await exec(
4292
+ "git",
4293
+ ["--literal-pathspecs", "-C", repoRoot, "ls-tree", "-rz", "--name-only", "HEAD", "--", pathspec],
4294
+ { cwd: repoRoot }
4295
+ );
4296
+ const hasCommitted = splitNulTerminated(committed.stdout).length > 0;
4297
+ const statusRes = await exec(
4298
+ "git",
4299
+ ["--literal-pathspecs", "-C", repoRoot, "status", "--porcelain", "-z", "--", pathspec],
4300
+ { cwd: repoRoot }
4301
+ );
4302
+ const dirty = splitNulTerminated(statusRes.stdout).length > 0;
4303
+ const baseline = !hasCommitted ? "UNCOMMITTED" : dirty ? "PARTIAL" : "COMMITTED";
4304
+ return { root, repoRoot, state, baseline, dirty, otherStaged, rollbackReady: hasCommitted };
4305
+ }
4306
+ function quoteForDisplay(arg) {
4307
+ return /\s/.test(arg) ? `"${arg}"` : arg;
4308
+ }
4309
+ function formatGitCommand(cmd) {
4310
+ return `git ${cmd.args.map(quoteForDisplay).join(" ")}`;
4311
+ }
4312
+ var BASELINE_COMMIT_MESSAGE = "skill-forge init: baseline";
4313
+ function notInRepoInitCommands(root) {
4314
+ return [
4315
+ { args: ["init", "--", root], cwd: root },
4316
+ { args: ["--literal-pathspecs", "-C", root, "add", "--", "."], cwd: root },
4317
+ { args: ["--literal-pathspecs", "-C", root, "commit", "-m", BASELINE_COMMIT_MESSAGE, "--", "."], cwd: root }
4318
+ ];
4319
+ }
4320
+ function untrackedAddCommitCommands(root, repoRoot) {
4321
+ return [
4322
+ { args: ["--literal-pathspecs", "-C", repoRoot, "add", "--", root], cwd: repoRoot },
4323
+ { args: ["--literal-pathspecs", "-C", repoRoot, "commit", "-m", BASELINE_COMMIT_MESSAGE, "--", root], cwd: repoRoot }
4324
+ ];
4325
+ }
4326
+ function ignoredUnignoreSuggestions(root, repoRoot) {
4327
+ const rel = toPosixPath(relative2(repoRoot, root));
4328
+ return [`!${rel}/`, `!${rel}/**`];
4329
+ }
4330
+ async function runGitCommands(commands, deps = {}) {
4331
+ const exec = deps.exec ?? defaultExec;
4332
+ const results = [];
4333
+ for (const cmd of commands) {
4334
+ const result = await exec("git", cmd.args, { cwd: cmd.cwd });
4335
+ results.push(result);
4336
+ if (result.code !== 0) break;
4337
+ }
4338
+ return results;
4339
+ }
4340
+ function describeGitPreflight(result) {
4341
+ if (result.state === "MISSING") return "root does not exist";
4342
+ if (result.state === "NOT_IN_REPO") return "not in a Git repository";
4343
+ const parts = [result.state.toLowerCase()];
4344
+ if (result.state !== "IGNORED") parts.push(result.baseline.toLowerCase());
4345
+ if (result.dirty) parts.push("dirty");
4346
+ if (result.otherStaged) parts.push("other staged changes present");
4347
+ return parts.join(", ");
4348
+ }
4349
+
4153
4350
  // src/commands/audit.ts
4154
4351
  import { existsSync as existsSync16 } from "fs";
4155
4352
  import { join as join20, resolve as resolvePath3 } from "path";
@@ -4393,6 +4590,15 @@ function presenceNote(presence) {
4393
4590
  if (presence === "absent") return " [CLI not on PATH]";
4394
4591
  return "";
4395
4592
  }
4593
+ async function printGitDetectionColumn(detected, deps = {}) {
4594
+ const groups = groupByRoot(detected);
4595
+ if (groups.length === 0) return;
4596
+ console.log("\n[skill-forge] init \u2014 version control");
4597
+ for (const group of groups) {
4598
+ const result = await evaluateGitPreflight(group.root, deps);
4599
+ console.log(` ${group.root} \u2014 ${describeGitPreflight(result)}`);
4600
+ }
4601
+ }
4396
4602
  function printMcpDetectionResults(detectedMcp) {
4397
4603
  if (detectedMcp.length === 0) return;
4398
4604
  console.log("\n[skill-forge] init \u2014 MCP config detection");
@@ -4422,13 +4628,17 @@ function buildConfig(selected, defaultTargetEntry, handoffAgent, selectedMcp = [
4422
4628
  mcpTargets: selectedMcp.length > 0 ? selectedMcp.map((m) => m.path) : existing.mcpTargets
4423
4629
  };
4424
4630
  }
4425
- function printSummary(config) {
4631
+ async function printSummary(config, deps = {}) {
4426
4632
  console.log(`
4427
4633
  [skill-forge] wrote config to ${getConfigPath()}`);
4428
4634
  console.log(` gate targets: ${config.skillsRoots.join(", ")}`);
4429
4635
  if (config.defaultTarget) console.log(` default target: ${config.defaultTarget}`);
4430
4636
  if (config.handoffCommand?.length) console.log(` handoff command: ${config.handoffCommand.join(" ")}`);
4431
4637
  if (config.mcpTargets?.length) console.log(` mcp targets: ${config.mcpTargets.join(", ")}`);
4638
+ for (const root of config.skillsRoots) {
4639
+ const result = await evaluateGitPreflight(root, deps);
4640
+ console.log(` version control: ${root} \u2014 ${describeGitPreflight(result)}`);
4641
+ }
4432
4642
  }
4433
4643
  function isPromptAbort(err) {
4434
4644
  return err instanceof Error && (err.name === "AbortError" || err.code === "ABORT_ERR");
@@ -4544,8 +4754,41 @@ async function promptHandoffAgent(rl, detected, narrow) {
4544
4754
  const idx = await promptSingleChoice(rl, `Choose [1-${ordered.length}] or Enter to skip: `, ordered.length, "skip");
4545
4755
  return idx === void 0 ? void 0 : ordered[idx].agent;
4546
4756
  }
4757
+ async function promptGitPreflight(rl, selected, deps = {}) {
4758
+ for (const group of selected) {
4759
+ const result = await evaluateGitPreflight(group.root, deps);
4760
+ if (result.state === "IGNORED") {
4761
+ console.log(`
4762
+ ${group.root} is excluded by a .gitignore pattern.`);
4763
+ console.log(" To track it, add these lines to the relevant .gitignore:");
4764
+ for (const line of ignoredUnignoreSuggestions(group.root, result.repoRoot ?? group.root)) {
4765
+ console.log(` ${line}`);
4766
+ }
4767
+ continue;
4768
+ }
4769
+ const commands = result.state === "NOT_IN_REPO" ? notInRepoInitCommands(group.root) : result.state === "UNTRACKED" && result.repoRoot ? untrackedAddCommitCommands(group.root, result.repoRoot) : void 0;
4770
+ if (!commands) continue;
4771
+ console.log(
4772
+ result.state === "NOT_IN_REPO" ? `
4773
+ ${group.root} is not inside a Git repository yet.` : `
4774
+ ${group.root} is inside a Git repository but isn't tracked yet.`
4775
+ );
4776
+ if (result.otherStaged) {
4777
+ console.log(" Note: this repository has OTHER staged changes outside this root \u2014 they are left untouched below.");
4778
+ }
4779
+ console.log(" Would run:");
4780
+ for (const cmd of commands) console.log(` ${formatGitCommand(cmd)}`);
4781
+ const answer = (await rl.question(" Commit a Git baseline for this root now? [y/N] ")).trim().toLowerCase();
4782
+ if (answer !== "y" && answer !== "yes") continue;
4783
+ const results = await runGitCommands(commands, deps);
4784
+ if (results.some((r) => r.code !== 0)) {
4785
+ console.log(" git baseline failed \u2014 see output above; the root is left as-is.");
4786
+ }
4787
+ }
4788
+ }
4547
4789
  async function init(options = {}, deps = {}) {
4548
4790
  const runAudit = deps.runAudit ?? auditCommand;
4791
+ const gitDeps = deps.gitExec ? { exec: deps.gitExec } : {};
4549
4792
  const cwd = options.cwd ?? process.cwd();
4550
4793
  const home = options.home ?? homedir4();
4551
4794
  const detected = detectAgents(cwd, home, options.pathEnv);
@@ -4553,6 +4796,7 @@ async function init(options = {}, deps = {}) {
4553
4796
  if (options.list) {
4554
4797
  printDetectionResults(detected);
4555
4798
  printMcpDetectionResults(detectedMcp);
4799
+ await printGitDetectionColumn(detected, gitDeps);
4556
4800
  return;
4557
4801
  }
4558
4802
  if (detected.length === 0 && detectedMcp.length === 0) {
@@ -4574,7 +4818,7 @@ async function init(options = {}, deps = {}) {
4574
4818
  const selectedMcpTargets = narrow ? mcpGroups.filter(hasInstalledMember) : mcpGroups;
4575
4819
  const config = buildConfig(selectedRoots, selectedRoots[0], void 0, selectedMcpTargets);
4576
4820
  saveConfig(config);
4577
- printSummary(config);
4821
+ await printSummary(config, gitDeps);
4578
4822
  await runAudit({ yes: true });
4579
4823
  return;
4580
4824
  }
@@ -4595,9 +4839,10 @@ async function init(options = {}, deps = {}) {
4595
4839
  }
4596
4840
  const target = selected.length > 0 ? await promptDefaultTarget(rl, selected) : void 0;
4597
4841
  const handoffAgent = detected.length > 0 ? await promptHandoffAgent(rl, detected, narrow) : void 0;
4842
+ await promptGitPreflight(rl, selected, gitDeps);
4598
4843
  const config = buildConfig(selected, target, handoffAgent, selectedMcp);
4599
4844
  saveConfig(config);
4600
- printSummary(config);
4845
+ await printSummary(config, gitDeps);
4601
4846
  if (options.offerAudit !== false) {
4602
4847
  const answer = (await rl.question("\nRun the skills & MCP audit now? [Y/n] ")).trim().toLowerCase();
4603
4848
  runAuditNow = answer === "" || answer === "y" || answer === "yes";
@@ -4896,7 +5141,7 @@ import { existsSync as existsSync19 } from "fs";
4896
5141
 
4897
5142
  // src/commands/held.ts
4898
5143
  import { existsSync as existsSync18, realpathSync as realpathSync3 } from "fs";
4899
- import { basename as basename9, join as join23, resolve as resolvePath4, sep as sep2 } from "path";
5144
+ import { basename as basename9, join as join23, resolve as resolvePath4, sep as sep3 } from "path";
4900
5145
  var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
4901
5146
  function resolveHeldPath(quarantineDir, id) {
4902
5147
  if (id === "" || id !== basename9(id) || id === "." || id === "..") {
@@ -4908,7 +5153,7 @@ function resolveHeldPath(quarantineDir, id) {
4908
5153
  }
4909
5154
  const realRoot = realpathSync3(resolvePath4(quarantineDir));
4910
5155
  const realCandidate = realpathSync3(candidate);
4911
- if (!realCandidate.startsWith(realRoot + sep2)) {
5156
+ if (!realCandidate.startsWith(realRoot + sep3)) {
4912
5157
  throw new UserError(`refusing to act on "${id}": it resolves outside the quarantine dir (${realRoot})`);
4913
5158
  }
4914
5159
  return realCandidate;
@@ -5436,6 +5681,10 @@ async function status() {
5436
5681
  if (config.mcpTargets?.length) console.log(` mcpTargets: ${config.mcpTargets.join(", ")}`);
5437
5682
  console.log(` quarantineDir: ${config.quarantineDir}`);
5438
5683
  console.log(` strictness: ${config.strictness}`);
5684
+ for (const root of config.skillsRoots) {
5685
+ const git = await evaluateGitPreflight(root);
5686
+ console.log(` version control: ${root} \u2014 ${describeGitPreflight(git)}`);
5687
+ }
5439
5688
  console.log(` last audit: ${formatAuditFreshness(readAuditState())}`);
5440
5689
  console.log(
5441
5690
  ` held in quarantine: ${held.readable ? held.count : "UNREADABLE (quarantine dir cannot be listed)"}` + (held.count > 0 ? " (`skill-forge promote <id>` / `reject <id>`)" : "")
@@ -5448,9 +5697,9 @@ async function status() {
5448
5697
  // src/evolve.ts
5449
5698
  import { createInterface as createInterface5 } from "readline/promises";
5450
5699
  import { createHash as createHash3 } from "crypto";
5451
- import { cpSync as cpSync3, existsSync as existsSync20, lstatSync as lstatSync3, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, rmSync as rmSync6 } from "fs";
5700
+ import { cpSync as cpSync3, existsSync as existsSync20, lstatSync as lstatSync4, mkdtempSync as mkdtempSync3, readFileSync as readFileSync16, rmSync as rmSync6 } from "fs";
5452
5701
  import { tmpdir as tmpdir2 } from "os";
5453
- import { basename as basename10, dirname as dirname9, join as join24, resolve as resolvePath5, sep as sep3 } from "path";
5702
+ import { basename as basename10, dirname as dirname9, join as join24, resolve as resolvePath5, sep as sep4 } from "path";
5454
5703
  var SKILLOPT_BIN = "skillopt-sleep";
5455
5704
  var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
5456
5705
  async function evolve(targetSkillDir, options = {}, deps = {}) {
@@ -5518,7 +5767,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
5518
5767
  }
5519
5768
  const stagingRoot = join24(project, ".skillopt-sleep", "staging");
5520
5769
  const stagingDir = resolvePath5(parsed.staging_dir);
5521
- if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep3)) {
5770
+ if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep4)) {
5522
5771
  throw new UserError(
5523
5772
  `skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
5524
5773
  );
@@ -5737,15 +5986,15 @@ function isAlreadyAdopted(stagingDir, manifest) {
5737
5986
  }
5738
5987
  return sawComparison;
5739
5988
  }
5740
- function assertNotSymlink(path, label) {
5989
+ function assertNotSymlink(path, label2) {
5741
5990
  let stat;
5742
5991
  try {
5743
- stat = lstatSync3(path);
5992
+ stat = lstatSync4(path);
5744
5993
  } catch {
5745
5994
  return;
5746
5995
  }
5747
5996
  if (stat.isSymbolicLink()) {
5748
- throw new UserError(`refusing to re-gate ${label}: ${path} is a symlink (a staged proposal must be a plain file)`);
5997
+ throw new UserError(`refusing to re-gate ${label2}: ${path} is a symlink (a staged proposal must be a plain file)`);
5749
5998
  }
5750
5999
  }
5751
6000
  function sha256File(path) {
@@ -6496,7 +6745,7 @@ import { join as join26, resolve as resolvePath8 } from "path";
6496
6745
  // src/gate/skillMapDrift.ts
6497
6746
  import { createHash as createHash4 } from "crypto";
6498
6747
  import { readFileSync as readFileSync18, existsSync as existsSync23 } from "fs";
6499
- import { dirname as dirname10, isAbsolute as isAbsolute2, resolve as resolve7 } from "path";
6748
+ import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve7 } from "path";
6500
6749
  function sha256(text) {
6501
6750
  return createHash4("sha256").update(text, "utf-8").digest("hex");
6502
6751
  }
@@ -6529,7 +6778,7 @@ function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
6529
6778
  }
6530
6779
  function resolveLocalPath(path, mapDir) {
6531
6780
  const expanded = expandHome(path);
6532
- if (isAbsolute2(expanded)) return resolve7(expanded);
6781
+ if (isAbsolute3(expanded)) return resolve7(expanded);
6533
6782
  const candidates = [resolve7(expanded)];
6534
6783
  if (mapDir) {
6535
6784
  candidates.push(resolve7(mapDir, expanded));
@@ -6942,9 +7191,9 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
6942
7191
  if (result.housekeeping.length > 0) {
6943
7192
  lines.push("", "## Housekeeping", "");
6944
7193
  for (const action of result.housekeeping) {
6945
- const label = action.outcome === "applied" ? "APPLIED" : action.outcome === "failed" ? "FAILED" : "WOULD APPLY";
7194
+ const label2 = action.outcome === "applied" ? "APPLIED" : action.outcome === "failed" ? "FAILED" : "WOULD APPLY";
6946
7195
  lines.push(
6947
- `- ${label} ${action.kind}: ${sanitizeForMarkdown(action.target)} \u2014 ${sanitizeForMarkdown(action.reason)}`
7196
+ `- ${label2} ${action.kind}: ${sanitizeForMarkdown(action.target)} \u2014 ${sanitizeForMarkdown(action.reason)}`
6948
7197
  );
6949
7198
  }
6950
7199
  }
@@ -7060,8 +7309,8 @@ async function routineCommand(options = {}) {
7060
7309
  if (result.organize) console.log(` registry: ${result.organize.skills} skill(s)`);
7061
7310
  console.log(` pending: ${result.pending.queue} queue, ${result.pending.quarantine} quarantine`);
7062
7311
  for (const action of housekeeping) {
7063
- const label = action.outcome === "applied" ? "housekeeping" : action.outcome === "failed" ? "FAILED" : "would do";
7064
- console.log(` ${label}: ${action.kind} ${action.target}${action.outcome === "failed" ? ` \u2014 ${action.reason}` : ""}`);
7312
+ const label2 = action.outcome === "applied" ? "housekeeping" : action.outcome === "failed" ? "FAILED" : "would do";
7313
+ console.log(` ${label2}: ${action.kind} ${action.target}${action.outcome === "failed" ? ` \u2014 ${action.reason}` : ""}`);
7065
7314
  }
7066
7315
  for (const item of result.degraded) console.log(` INCOMPLETE: ${item}`);
7067
7316
  console.log(` report -> ${result.reportPath}`);
@@ -7080,7 +7329,7 @@ async function routineCommand(options = {}) {
7080
7329
 
7081
7330
  // src/commands/ingest.ts
7082
7331
  import { realpathSync as realpathSync6 } from "fs";
7083
- import { dirname as dirname12, isAbsolute as isAbsolute3, relative as relative2, resolve as resolvePath9 } from "path";
7332
+ import { dirname as dirname12, isAbsolute as isAbsolute4, relative as relative3, resolve as resolvePath9 } from "path";
7084
7333
  var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
7085
7334
  var VALID_STATUSES = ["ingested", "dismissed"];
7086
7335
  function requireProFeature() {
@@ -7127,8 +7376,8 @@ function realpathOrNull(p) {
7127
7376
  }
7128
7377
  }
7129
7378
  function isUnderRoot(candidate, root) {
7130
- const rel = relative2(root, candidate);
7131
- return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
7379
+ const rel = relative3(root, candidate);
7380
+ return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
7132
7381
  }
7133
7382
  function validatePendingEntries(entries, config) {
7134
7383
  const quarantineRoot = realpathOrNull(resolvePath9(config.quarantineDir));
@@ -7204,195 +7453,2714 @@ async function queueClose(id, options = {}) {
7204
7453
  console.log(`[skill-forge] queue entry ${updated.id} closed -> status: ${updated.status}`);
7205
7454
  }
7206
7455
 
7207
- // src/commands/refine.ts
7208
- import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
7209
- import { chmodSync, existsSync as existsSync27, lstatSync as lstatSync4, mkdirSync as mkdirSync9, readFileSync as readFileSync20, realpathSync as realpathSync9, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
7210
- import { dirname as dirname14, isAbsolute as isAbsolute5, join as join30, relative as relative4, resolve as resolvePath12, sep as sep4 } from "path";
7456
+ // src/commands/insight.ts
7211
7457
  import { createInterface as createInterface6 } from "readline/promises";
7458
+ import {
7459
+ chmodSync as chmodSync3,
7460
+ lstatSync as lstatSync6,
7461
+ mkdirSync as mkdirSync9,
7462
+ readFileSync as readFileSync22,
7463
+ readSync as readSync3,
7464
+ writeFileSync as writeFileSync8
7465
+ } from "fs";
7466
+ import { dirname as dirname13, isAbsolute as isAbsolute6, join as join32, relative as relative5, resolve as resolvePath10 } from "path";
7212
7467
 
7213
- // src/refine/context.ts
7214
- import { existsSync as existsSync26, realpathSync as realpathSync8, statSync as statSync12 } from "fs";
7215
- import { isAbsolute as isAbsolute4, join as join29, relative as relative3, resolve as resolvePath11 } from "path";
7468
+ // src/insights/inventory.ts
7469
+ import { readFileSync as readFileSync19 } from "fs";
7470
+ import { join as join28 } from "path";
7216
7471
 
7217
- // src/refine/models.ts
7218
- import { randomBytes } from "crypto";
7219
- var REFINEMENT_CATEGORIES = ["trigger", "content", "hook", "tool", "pattern", "config", "new"];
7220
- var OVERRIDE_TYPES = ["patch", "extend", "config", "full", "hook", "script", "new"];
7221
- var PATCH_ACTIONS = ["append", "prepend", "replace-section", "insert-after", "insert-before", "delete-section"];
7222
- var PATTERN_STATUSES = ["tracking", "ready", "generalized", "dismissed"];
7223
- function isRefinementCategory(value) {
7224
- return REFINEMENT_CATEGORIES.includes(value);
7225
- }
7226
- function isOverrideType(value) {
7227
- return OVERRIDE_TYPES.includes(value);
7472
+ // src/insights/models.ts
7473
+ import { createHash as createHash5 } from "crypto";
7474
+ var INSIGHT_SCHEMA_VERSION = 1;
7475
+ var MAX_EVIDENCE_EXCERPT_CHARS = 500;
7476
+ var SOURCE_TYPES = ["url", "file", "directory", "connector", "stdin"];
7477
+ var SOURCE_ACCESS_CLASSES = [
7478
+ "public",
7479
+ "user-owned",
7480
+ "client-confidential",
7481
+ "restricted",
7482
+ "unknown"
7483
+ ];
7484
+ var SOURCE_LICENSE_CLASSES = [
7485
+ "public-domain",
7486
+ "permissive",
7487
+ "attribution-required",
7488
+ "copyleft",
7489
+ "proprietary",
7490
+ "unknown"
7491
+ ];
7492
+ var SOURCE_RETENTIONS = ["not-retained", "retained", "deleted"];
7493
+ var ANALYSIS_MODES = ["multi-agent", "separate-context", "single-context"];
7494
+ var INSIGHT_VALIDATIONS = [
7495
+ "corroborated",
7496
+ "contested",
7497
+ "single-source",
7498
+ "rejected",
7499
+ "unverifiable"
7500
+ ];
7501
+ var FORGE_VERBS2 = ["DEFER", "ABSORB", "FORK", "REJECT", "WATCH"];
7502
+ var INVENTORY_COVERAGE_VALUES = ["complete", "partial", "unavailable"];
7503
+ var STUDY_STATUSES = [
7504
+ "collecting",
7505
+ "analysis-submitted",
7506
+ "review-ready",
7507
+ "approved",
7508
+ "jira-manifested",
7509
+ "dismissed",
7510
+ "archived"
7511
+ ];
7512
+ function canonicalJson(value) {
7513
+ const normalize = (input) => {
7514
+ if (Array.isArray(input)) return input.map(normalize);
7515
+ if (input !== null && typeof input === "object") {
7516
+ return Object.fromEntries(
7517
+ Object.entries(input).filter(([, child]) => child !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, child]) => [key, normalize(child)])
7518
+ );
7519
+ }
7520
+ return input;
7521
+ };
7522
+ return JSON.stringify(normalize(value));
7523
+ }
7524
+ function stableHash(value) {
7525
+ return createHash5("sha256").update(canonicalJson(value)).digest("hex");
7526
+ }
7527
+ function stableId(prefix, value, length = 16) {
7528
+ if (!/^[A-Z][A-Z0-9-]*$/.test(prefix)) {
7529
+ throw new Error(`invalid stable id prefix: ${prefix}`);
7530
+ }
7531
+ if (!Number.isInteger(length) || length < 8 || length > 64) {
7532
+ throw new Error(`stable id length must be an integer from 8 to 64`);
7533
+ }
7534
+ return `${prefix}-${stableHash(value).slice(0, length)}`;
7535
+ }
7536
+ function sourceIdFor(fingerprint) {
7537
+ return stableId("SRC", fingerprint);
7538
+ }
7539
+ function sourceInventoryHashFor(sources) {
7540
+ return stableHash(
7541
+ sources.map((source) => ({
7542
+ sourceId: source.sourceId,
7543
+ type: source.type,
7544
+ title: source.title,
7545
+ privateLocator: source.privateLocator,
7546
+ publicLocator: source.publicLocator,
7547
+ author: source.author,
7548
+ publisher: source.publisher,
7549
+ publicationDate: source.publicationDate,
7550
+ revisionDate: source.revisionDate,
7551
+ revision: source.revision,
7552
+ etag: source.etag,
7553
+ contentSha256: source.contentSha256,
7554
+ hashUnavailableReason: source.hashUnavailableReason,
7555
+ fingerprint: source.fingerprint,
7556
+ accessClass: source.accessClass,
7557
+ licenseName: source.licenseName,
7558
+ licenseClass: source.licenseClass,
7559
+ byteLength: source.byteLength,
7560
+ mediaType: source.mediaType,
7561
+ includeHidden: source.includeHidden,
7562
+ skippedContent: source.skippedContent,
7563
+ suspiciousInstructionFlags: source.suspiciousInstructionFlags
7564
+ })).sort((a, b) => a.sourceId < b.sourceId ? -1 : a.sourceId > b.sourceId ? 1 : 0)
7565
+ );
7228
7566
  }
7229
- function isPatchAction(value) {
7230
- return PATCH_ACTIONS.includes(value);
7567
+ function insightIdFor(domain, claim, sourceIds) {
7568
+ return stableId("INS", { domain: domain.trim(), claim: claim.trim(), sourceIds: [...sourceIds].sort() });
7231
7569
  }
7232
- function isPatternStatus(value) {
7233
- return PATTERN_STATUSES.includes(value);
7570
+ function domainPlanIdFor(domain, insightIds) {
7571
+ return stableId("DOM", { domain: domain.trim(), insightIds: [...insightIds].sort() });
7234
7572
  }
7235
- var REFINEMENT_STATUSES = ["pending", "applied", "generalized"];
7236
- function isRefinementStatus(value) {
7237
- return REFINEMENT_STATUSES.includes(value);
7573
+
7574
+ // src/insights/inventory.ts
7575
+ function compareStrings(a, b) {
7576
+ return a < b ? -1 : a > b ? 1 : 0;
7238
7577
  }
7239
- function patternCount(pattern) {
7240
- return new Set(pattern.occurrences.map((o) => o.projectRealpath)).size;
7578
+ function declaredMapCoverage(path, claim) {
7579
+ if (!path) return "not-requested";
7580
+ return claim ?? "unknown";
7241
7581
  }
7242
- function defaultRandomHex() {
7243
- return randomBytes(2).toString("hex");
7582
+ function fingerprintInstalledSkill(path) {
7583
+ try {
7584
+ return { fingerprint: stableHash(readFileSync19(join28(path, "SKILL.md"), "utf-8")) };
7585
+ } catch (error) {
7586
+ return { reason: `could not fingerprint SKILL.md: ${error.message}` };
7587
+ }
7244
7588
  }
7245
- function formatYYYYMMDD(date) {
7246
- const yyyy = date.getUTCFullYear().toString().padStart(4, "0");
7247
- const mm = (date.getUTCMonth() + 1).toString().padStart(2, "0");
7248
- const dd = date.getUTCDate().toString().padStart(2, "0");
7249
- return `${yyyy}${mm}${dd}`;
7589
+ function installedEntries(config) {
7590
+ const report = buildOrganizeReport(config);
7591
+ const warnings = [...report.registry.warnings, ...report.notices];
7592
+ const entries = report.registry.skills.map((skill) => {
7593
+ const fingerprint = fingerprintInstalledSkill(skill.path);
7594
+ if (fingerprint.reason) warnings.push(`${skill.name}: ${fingerprint.reason}`);
7595
+ return {
7596
+ capabilityId: skill.id,
7597
+ name: skill.name,
7598
+ source: "installed",
7599
+ fingerprint: fingerprint.fingerprint,
7600
+ fingerprintUnavailableReason: fingerprint.reason,
7601
+ tier: skill.tier,
7602
+ domain: skill.domain,
7603
+ consumes: [...skill.consumes],
7604
+ provenance: skill.provenance,
7605
+ maturity: skill.maturity
7606
+ };
7607
+ });
7608
+ return { entries, warnings, rootCount: report.registry.skillsRoots.length };
7250
7609
  }
7251
- function generateRefinementId(existingIds = /* @__PURE__ */ new Set(), now = /* @__PURE__ */ new Date(), randomHex = defaultRandomHex) {
7252
- const datePart = formatYYYYMMDD(now);
7253
- let id;
7254
- do {
7255
- id = `REF-${datePart}-${randomHex()}`;
7256
- } while (existingIds.has(id));
7257
- return id;
7610
+ function skillMapEntries(map) {
7611
+ return map.nodes.filter((node) => node.type === "skill").map((node) => {
7612
+ const validFingerprint = typeof node.contentHash === "string" && /^[0-9a-f]{64}$/.test(node.contentHash);
7613
+ return {
7614
+ capabilityId: node.id,
7615
+ name: node.name ?? node.id,
7616
+ source: "skill-map",
7617
+ fingerprint: validFingerprint ? node.contentHash : void 0,
7618
+ fingerprintUnavailableReason: validFingerprint ? void 0 : "skill-map node has no valid contentHash",
7619
+ consumes: [],
7620
+ description: node.description
7621
+ };
7622
+ });
7258
7623
  }
7259
- function generatePatternId(existingIds = /* @__PURE__ */ new Set(), randomHex = defaultRandomHex) {
7260
- let id;
7261
- do {
7262
- id = `PAT-${randomHex()}`;
7263
- } while (existingIds.has(id));
7264
- return id;
7624
+ function candidateFor(match, proposal, byId) {
7625
+ const entry = byId.get(match.skillId);
7626
+ const extended = (proposal.extendsCapabilityIds ?? []).some((id) => id === match.skillId || id === match.name);
7627
+ return {
7628
+ capabilityId: match.skillId,
7629
+ label: match.name,
7630
+ fingerprint: entry?.fingerprint,
7631
+ lexicalScore: match.score,
7632
+ relationship: extended ? "extension" : "ambiguous",
7633
+ semanticReviewRequired: true
7634
+ };
7265
7635
  }
7266
- function fingerprintKey(skill, target, overrideType, action) {
7267
- const normalize = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
7268
- return [normalize(skill), normalize(target), overrideType, action ?? ""].join("::");
7636
+ function mapProposals(proposals, capabilities, map, coverage) {
7637
+ const byId = new Map(capabilities.map((entry) => [entry.capabilityId, entry]));
7638
+ const installed = capabilities.filter((entry) => entry.source === "installed");
7639
+ return proposals.map((proposal) => {
7640
+ const exact = installed.filter((entry) => entry.name === proposal.label);
7641
+ const overlap = map ? scanMapOverlap({ name: proposal.label, description: proposal.description }, map) : { matches: [], compared: 0, topMatch: void 0 };
7642
+ const candidates = exact.map((entry) => ({
7643
+ capabilityId: entry.capabilityId,
7644
+ label: entry.name,
7645
+ fingerprint: entry.fingerprint,
7646
+ relationship: (proposal.extendsCapabilityIds ?? []).includes(entry.capabilityId) ? "extension" : "existing",
7647
+ semanticReviewRequired: true
7648
+ }));
7649
+ const seen = new Set(candidates.map((candidate) => candidate.capabilityId));
7650
+ for (const match of overlap.matches) {
7651
+ if (!seen.has(match.skillId)) candidates.push(candidateFor(match, proposal, byId));
7652
+ }
7653
+ if (candidates.length === 0) {
7654
+ candidates.push({
7655
+ label: proposal.label,
7656
+ relationship: coverage === "complete" ? "gap" : "ambiguous",
7657
+ semanticReviewRequired: true
7658
+ });
7659
+ }
7660
+ return {
7661
+ proposalId: proposal.proposalId,
7662
+ candidates,
7663
+ installedExactMatches: exact.map((entry) => entry.capabilityId),
7664
+ mapCompared: overlap.compared,
7665
+ mapTopMatch: overlap.topMatch,
7666
+ duplicateCapabilityBlocked: candidates.some(
7667
+ (candidate) => candidate.relationship === "existing" || candidate.relationship === "ambiguous"
7668
+ ),
7669
+ semanticReviewRequired: true
7670
+ };
7671
+ });
7269
7672
  }
7270
- function contentSimilarity(a, b) {
7271
- const tokenize3 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
7272
- const tokensA = tokenize3(a);
7273
- const tokensB = tokenize3(b);
7274
- if (tokensA.size === 0 || tokensB.size === 0) return 0;
7275
- let intersectionSize = 0;
7276
- for (const token of tokensA) {
7277
- if (tokensB.has(token)) intersectionSize++;
7673
+ function buildInsightInventory(config, options = {}) {
7674
+ if (options.skillMapCoverage !== void 0 && options.skillMapPath === void 0) {
7675
+ throw new UserError(`skill-map coverage requires a skill-map path`);
7278
7676
  }
7279
- const unionSize3 = tokensA.size + tokensB.size - intersectionSize;
7280
- return unionSize3 === 0 ? 0 : intersectionSize / unionSize3;
7677
+ const installed = installedEntries(config);
7678
+ let map;
7679
+ let mapEntries = [];
7680
+ const warnings = [...installed.warnings];
7681
+ if (options.skillMapPath) {
7682
+ map = loadSkillMap(options.skillMapPath);
7683
+ mapEntries = skillMapEntries(map);
7684
+ const installedIds = new Set(installed.entries.map((entry) => entry.capabilityId));
7685
+ if (mapEntries.some((entry) => installedIds.has(entry.capabilityId))) {
7686
+ throw new UserError(`skill-map capability id collides with installed capability identity`);
7687
+ }
7688
+ for (const entry of mapEntries) {
7689
+ if (entry.fingerprintUnavailableReason) warnings.push(`${entry.name}: ${entry.fingerprintUnavailableReason}`);
7690
+ }
7691
+ }
7692
+ const capabilities = [...installed.entries, ...mapEntries].sort(
7693
+ (a, b) => compareStrings(`${a.source}:${a.capabilityId}`, `${b.source}:${b.capabilityId}`)
7694
+ );
7695
+ const missingFingerprints = capabilities.filter((entry) => !entry.fingerprint).length;
7696
+ const mapCoverage = declaredMapCoverage(options.skillMapPath, options.skillMapCoverage);
7697
+ const coverage = mapCoverage === "resolved" && missingFingerprints === 0 ? "complete" : "partial";
7698
+ const coverageReason = mapCoverage === "not-requested" ? "no resolved skill map was supplied; installed roots were inventoried only" : mapCoverage !== "resolved" ? `${mapCoverage} skill-map coverage cannot prove ecosystem-wide deduplication` : missingFingerprints > 0 ? `${missingFingerprints} capability fingerprint(s) were unavailable` : void 0;
7699
+ const mappings = mapProposals(options.proposals ?? [], capabilities, map, coverage);
7700
+ const capabilityInventoryHash = stableHash({
7701
+ coverage,
7702
+ coverageReason,
7703
+ installedRootCount: installed.rootCount,
7704
+ mapCoverage,
7705
+ capabilities: capabilities.map((entry) => ({
7706
+ capabilityId: entry.capabilityId,
7707
+ name: entry.name,
7708
+ source: entry.source,
7709
+ fingerprint: entry.fingerprint,
7710
+ tier: entry.tier,
7711
+ domain: entry.domain,
7712
+ consumes: entry.consumes,
7713
+ provenance: entry.provenance,
7714
+ maturity: entry.maturity,
7715
+ description: entry.description
7716
+ }))
7717
+ });
7718
+ return {
7719
+ generatedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
7720
+ coverage,
7721
+ coverageReason,
7722
+ installedRootCount: installed.rootCount,
7723
+ installedCapabilityCount: installed.entries.length,
7724
+ mapCoverage,
7725
+ mapCapabilityCount: mapEntries.length,
7726
+ capabilities,
7727
+ mappings,
7728
+ warnings,
7729
+ capabilityInventoryHash
7730
+ };
7281
7731
  }
7282
7732
 
7283
- // src/refine/store.ts
7284
- import { createHash as createHash5 } from "crypto";
7285
- import { existsSync as existsSync25, readFileSync as readFileSync19, statSync as statSync11, realpathSync as realpathSync7 } from "fs";
7286
- import { join as join28, resolve as resolvePath10 } from "path";
7287
- var SIMILARITY_JOIN_THRESHOLD = 0.7;
7288
- var READY_THRESHOLD = 2;
7289
- function getRefinementsDir() {
7290
- return join28(getHomeDir(), "refinements");
7291
- }
7292
- function getRefinementsPath() {
7293
- return join28(getRefinementsDir(), "refinements.json");
7733
+ // src/insights/jira.ts
7734
+ import { existsSync as existsSync26, readFileSync as readFileSync21 } from "fs";
7735
+ import { join as join31 } from "path";
7736
+
7737
+ // src/insights/store.ts
7738
+ import { chmodSync as chmodSync2, existsSync as existsSync25, mkdirSync as mkdirSync8, readFileSync as readFileSync20 } from "fs";
7739
+ import { join as join30 } from "path";
7740
+
7741
+ // src/insights/sources.ts
7742
+ import { createHash as createHash6 } from "crypto";
7743
+ import {
7744
+ closeSync as closeSync3,
7745
+ constants as constants2,
7746
+ fstatSync as fstatSync2,
7747
+ lstatSync as lstatSync5,
7748
+ openSync as openSync3,
7749
+ readSync as readSync2,
7750
+ readdirSync as readdirSync9,
7751
+ realpathSync as realpathSync7,
7752
+ statSync as statSync11
7753
+ } from "fs";
7754
+ import { basename as basename12, extname as extname6, isAbsolute as isAbsolute5, join as join29, relative as relative4, resolve as resolve8, sep as sep5 } from "path";
7755
+ var DEFAULT_MAX_SOURCE_FILE_BYTES = 5 * 1024 * 1024;
7756
+ var DEFAULT_MAX_SOURCE_TOTAL_BYTES = 25 * 1024 * 1024;
7757
+ var SKIP_DIRS4 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "__pycache__", ".venv", "venv"]);
7758
+ var CREDENTIAL_NAME = /(?:^|[._-])(\.env|credentials?|secrets?|tokens?|api[._-]?keys?|id_rsa|id_ed25519)(?:$|[._-])/i;
7759
+ var CREDENTIAL_QUERY_KEY = /(?:^|[._-])(?:auth(?:orization)?|token|secret|password|credential|api[._-]?key|sig(?:nature)?)(?:$|[._-])/i;
7760
+ var CLOUD_SIGNED_QUERY_KEY = /^(?:(?:x-amz|x-goog)-(?:algorithm|credential|date|expires|security-token|signature|signedheaders)|awsaccesskeyid|googleaccessid|key[-_]pair[-_]id|signed(?:[-_]url)?)$/i;
7761
+ var COMPACT_CREDENTIAL_QUERY_KEYS = /* @__PURE__ */ new Set([
7762
+ "apikey",
7763
+ "auth",
7764
+ "authorization",
7765
+ "authtoken",
7766
+ "bearertoken",
7767
+ "clientsecret",
7768
+ "credential",
7769
+ "credentials",
7770
+ "idtoken",
7771
+ "password",
7772
+ "refreshtoken",
7773
+ "secret",
7774
+ "securitytoken",
7775
+ "sig",
7776
+ "signature",
7777
+ "signedurl",
7778
+ "token",
7779
+ "accesstoken"
7780
+ ]);
7781
+ var SUPPORTED_MEDIA_BY_EXTENSION = {
7782
+ ".md": "text/markdown",
7783
+ ".markdown": "text/markdown",
7784
+ ".txt": "text/plain",
7785
+ ".html": "text/html",
7786
+ ".htm": "text/html",
7787
+ ".pdf": "application/pdf"
7788
+ };
7789
+ function ensurePositiveLimit(value, label2) {
7790
+ if (!Number.isSafeInteger(value) || value <= 0) {
7791
+ throw new UserError(`${label2} must be a positive integer number of bytes`);
7792
+ }
7793
+ return value;
7294
7794
  }
7295
- function getPatternsPath() {
7296
- return join28(getRefinementsDir(), "patterns.json");
7795
+ function validateObservedMetadata(sha2562, byteLength) {
7796
+ if (sha2562 !== void 0 && !/^[0-9a-f]{64}$/.test(sha2562)) {
7797
+ throw new UserError(`observedSha256 must be a lowercase SHA-256 digest`);
7798
+ }
7799
+ if (byteLength !== void 0 && (!Number.isSafeInteger(byteLength) || byteLength < 0)) {
7800
+ throw new UserError(`observedByteLength must be a non-negative integer number of bytes`);
7801
+ }
7297
7802
  }
7298
- function getBackupsDir() {
7299
- return join28(getRefinementsDir(), "backups");
7803
+ function compareStrings2(a, b) {
7804
+ return a < b ? -1 : a > b ? 1 : 0;
7300
7805
  }
7301
- function readJsonFile(path, label) {
7302
- let raw;
7806
+ function canonicalPublicUrl(raw, label2) {
7807
+ let url;
7303
7808
  try {
7304
- raw = readFileSync19(path, "utf-8");
7305
- } catch (err) {
7306
- throw new UserError(`unable to read ${label} at ${path}: ${err.message}`);
7809
+ url = new URL(raw);
7810
+ } catch {
7811
+ throw new UserError(`invalid ${label2}: ${raw}`);
7812
+ }
7813
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
7814
+ throw new UserError(`${label2} must use http or https: ${raw}`);
7815
+ }
7816
+ if (url.username || url.password || [...url.searchParams.keys()].some(
7817
+ (key) => CREDENTIAL_QUERY_KEY.test(key) || CLOUD_SIGNED_QUERY_KEY.test(key) || COMPACT_CREDENTIAL_QUERY_KEYS.has(key.toLowerCase())
7818
+ )) {
7819
+ throw new UserError(`${label2} must not contain credentials: ${raw}`);
7820
+ }
7821
+ url.hash = "";
7822
+ return url.toString();
7823
+ }
7824
+ function isInside2(child, root) {
7825
+ const rel = relative4(root, child);
7826
+ return rel === "" || !rel.startsWith(`..${sep5}`) && rel !== ".." && !isAbsolute5(rel);
7827
+ }
7828
+ function isHiddenName(name) {
7829
+ return name.startsWith(".") && name !== "." && name !== "..";
7830
+ }
7831
+ function hasCredentialShapedName(name) {
7832
+ return name === ".env" || name.startsWith(".env.") || CREDENTIAL_NAME.test(name);
7833
+ }
7834
+ function mediaTypeFor(path, declared) {
7835
+ return declared ?? SUPPORTED_MEDIA_BY_EXTENSION[extname6(path).toLowerCase()];
7836
+ }
7837
+ function suspiciousInstructionFlags(bytes, mediaType) {
7838
+ if (!mediaType.startsWith("text/")) return [];
7839
+ const sample = bytes.subarray(0, 64 * 1024).toString("utf-8");
7840
+ const flags = [];
7841
+ if (/ignore\s+(?:all\s+)?previous\s+instructions/i.test(sample)) flags.push("instruction-override-language");
7842
+ if (/(?:reveal|print|exfiltrate).{0,40}(?:system prompt|credential|secret|token)/i.test(sample)) flags.push("data-exfiltration-language");
7843
+ if (/(?:run|execute)\s+(?:this\s+)?(?:command|script)/i.test(sample)) flags.push("execution-request-language");
7844
+ return flags;
7845
+ }
7846
+ function observeFile(path, relativePath, declaredMediaType, maxFileBytes, remainingBytes) {
7847
+ const name = basename12(path);
7848
+ if (hasCredentialShapedName(name)) {
7849
+ return { path: relativePath, reason: "credential-shaped-name" };
7850
+ }
7851
+ const mediaType = mediaTypeFor(path, declaredMediaType);
7852
+ if (!mediaType) return { path: relativePath, reason: "unsupported-media-type" };
7853
+ const noFollow = typeof constants2.O_NOFOLLOW === "number" ? constants2.O_NOFOLLOW : 0;
7854
+ let descriptor;
7855
+ let bytes;
7856
+ let descriptorStat;
7857
+ try {
7858
+ descriptor = openSync3(path, constants2.O_RDONLY | noFollow);
7859
+ descriptorStat = fstatSync2(descriptor);
7860
+ if (!descriptorStat.isFile()) return { path: relativePath, reason: "unsupported-media-type" };
7861
+ if (descriptorStat.size > maxFileBytes) {
7862
+ return { path: relativePath, reason: "file-too-large", byteLength: descriptorStat.size };
7863
+ }
7864
+ if (descriptorStat.size > remainingBytes) {
7865
+ return { path: relativePath, reason: "total-byte-limit", byteLength: descriptorStat.size };
7866
+ }
7867
+ const limit = Math.min(maxFileBytes, remainingBytes);
7868
+ const chunks = [];
7869
+ let total = 0;
7870
+ while (true) {
7871
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, limit - total + 1));
7872
+ const count = readSync2(descriptor, chunk, 0, chunk.byteLength, null);
7873
+ if (count === 0) break;
7874
+ total += count;
7875
+ if (total > limit) {
7876
+ const reason = maxFileBytes <= remainingBytes ? "file-too-large" : "total-byte-limit";
7877
+ return { path: relativePath, reason, byteLength: total };
7878
+ }
7879
+ chunks.push(chunk.subarray(0, count));
7880
+ }
7881
+ bytes = Buffer.concat(chunks, total);
7882
+ } catch (error) {
7883
+ throw new UserError(`unable to read source file ${relativePath}: ${error.message}`);
7884
+ } finally {
7885
+ if (descriptor !== void 0) closeSync3(descriptor);
7307
7886
  }
7887
+ if (!bytes || !descriptorStat) throw new UserError(`source file could not be observed: ${relativePath}`);
7888
+ let realPath;
7308
7889
  try {
7309
- return JSON.parse(raw);
7890
+ realPath = realpathSync7(path);
7891
+ const pathStat = statSync11(realPath);
7892
+ if (pathStat.dev !== descriptorStat.dev || pathStat.ino !== descriptorStat.ino) {
7893
+ throw new Error("path identity changed");
7894
+ }
7310
7895
  } catch {
7311
- throw new UserError(`malformed ${label} at ${path}: not valid JSON`);
7312
- }
7313
- }
7314
- function readStore(path, label, validateRecord) {
7315
- if (!existsSync25(path)) {
7316
- return { schemaVersion: 1, records: [] };
7317
- }
7318
- const parsed = readJsonFile(path, label);
7319
- if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.records)) {
7320
- throw new UserError(`malformed ${label} at ${path}: expected an object with a "records" array`);
7321
- }
7322
- const records = parsed.records;
7323
- if (validateRecord) {
7324
- records.forEach((record, index) => validateRecord(record, index));
7325
- }
7326
- return { schemaVersion: 1, records };
7327
- }
7328
- function validateRefinementRecordShape(record, index, path) {
7329
- if (typeof record !== "object" || record === null) {
7330
- throw new UserError(`malformed refinements.json at ${path}: record[${index}] is not an object`);
7331
- }
7332
- const r = record;
7333
- if (typeof r.id !== "string" || r.id.length === 0) {
7334
- throw new UserError(`malformed refinements.json at ${path}: record[${index}] is missing a valid "id"`);
7335
- }
7336
- if (typeof r.createdAt !== "string" || r.createdAt.length === 0) {
7337
- throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "createdAt"`);
7338
- }
7339
- if (typeof r.status !== "string" || r.status.length === 0) {
7340
- throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "status"`);
7896
+ throw new UserError(`source file changed while it was being observed: ${relativePath}`);
7341
7897
  }
7342
- if (typeof r.skill !== "string" || r.skill.length === 0) {
7343
- throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "skill"`);
7344
- }
7345
- const patchActionsRequiringMarker = /* @__PURE__ */ new Set(["insert-after", "insert-before", "replace-section", "delete-section"]);
7346
- if (r.overrideType === "patch") {
7347
- if (typeof r.patchAction !== "string" || r.patchAction.length === 0) {
7348
- throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchAction"`);
7349
- }
7350
- if (patchActionsRequiringMarker.has(r.patchAction) && (typeof r.patchMarker !== "string" || r.patchMarker.length === 0)) {
7351
- throw new UserError(
7352
- `malformed refinements.json at ${path}: record "${r.id}" has patchAction "${r.patchAction}" but is missing "patchMarker"`
7353
- );
7354
- }
7355
- if (typeof r.patchContent !== "string" || r.patchContent.length === 0) {
7356
- throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchContent"`);
7357
- }
7358
- } else if (r.patchAction !== void 0) {
7359
- throw new UserError(
7360
- `malformed refinements.json at ${path}: record "${r.id}" has overrideType "${String(r.overrideType)}" but carries "patchAction" \u2014 only valid when overrideType is "patch"`
7361
- );
7898
+ if (mediaType !== "application/pdf" && bytes.subarray(0, 512).includes(0)) {
7899
+ return { path: relativePath, reason: "binary", byteLength: bytes.byteLength };
7362
7900
  }
7901
+ return {
7902
+ realPath,
7903
+ relativePath,
7904
+ byteLength: bytes.byteLength,
7905
+ sha256: createHash6("sha256").update(bytes).digest("hex"),
7906
+ mediaType,
7907
+ suspiciousInstructionFlags: suspiciousInstructionFlags(bytes, mediaType),
7908
+ bytes
7909
+ };
7363
7910
  }
7364
- function validatePatternRecordShape(record, index, path) {
7365
- if (typeof record !== "object" || record === null) {
7366
- throw new UserError(`malformed patterns.json at ${path}: record[${index}] is not an object`);
7367
- }
7368
- const p = record;
7369
- if (typeof p.id !== "string" || p.id.length === 0) {
7370
- throw new UserError(`malformed patterns.json at ${path}: record[${index}] is missing a valid "id"`);
7371
- }
7372
- if (typeof p.status !== "string" || p.status.length === 0) {
7373
- throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "status"`);
7374
- }
7375
- if (!Array.isArray(p.occurrences)) {
7376
- throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "occurrences" array`);
7377
- }
7378
- if (p.status === "dismissed" && (typeof p.dismissedReason !== "string" || p.dismissedReason.length === 0)) {
7379
- throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "dismissed" but is missing "dismissedReason"`);
7380
- }
7381
- if (p.status === "generalized") {
7382
- if (typeof p.generalizedAt !== "string" || p.generalizedAt.length === 0) {
7383
- throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedAt"`);
7384
- }
7385
- if (typeof p.generalizedBackupId !== "string" || p.generalizedBackupId.length === 0) {
7386
- throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedBackupId"`);
7387
- }
7388
- }
7911
+ function normalizedCommon(input, retrievedAt) {
7912
+ return {
7913
+ title: input.title?.trim() ?? "",
7914
+ accessClass: input.accessClass ?? "unknown",
7915
+ licenseName: input.licenseName,
7916
+ licenseClass: input.licenseClass ?? "unknown",
7917
+ author: input.author,
7918
+ publisher: input.publisher,
7919
+ publicationDate: input.publicationDate,
7920
+ revisionDate: input.revisionDate,
7921
+ retrievedAt,
7922
+ revision: input.revision,
7923
+ etag: input.etag
7924
+ };
7389
7925
  }
7390
- function readRefinementsStore() {
7391
- const path = getRefinementsPath();
7392
- return readStore(path, "refinements.json", (record, index) => validateRefinementRecordShape(record, index, path));
7926
+ function normalizeUrl(input, retrievedAt) {
7927
+ validateObservedMetadata(input.observedSha256, input.observedByteLength);
7928
+ const canonicalUrl = canonicalPublicUrl(input.url, "source URL");
7929
+ const fingerprint = stableHash({
7930
+ type: input.type,
7931
+ canonicalUrl,
7932
+ revision: input.revision,
7933
+ etag: input.etag,
7934
+ observedSha256: input.observedSha256
7935
+ });
7936
+ const common = normalizedCommon(input, retrievedAt);
7937
+ return {
7938
+ ...common,
7939
+ title: common.title || canonicalUrl,
7940
+ sourceId: sourceIdFor(fingerprint),
7941
+ type: "url",
7942
+ privateLocator: canonicalUrl,
7943
+ publicLocator: common.accessClass === "public" ? canonicalUrl : void 0,
7944
+ contentSha256: input.observedSha256,
7945
+ hashUnavailableReason: input.observedSha256 ? void 0 : "not fetched by skill-forge",
7946
+ fingerprint,
7947
+ retention: "not-retained",
7948
+ byteLength: input.observedByteLength,
7949
+ mediaType: input.mediaType,
7950
+ skippedContent: [],
7951
+ suspiciousInstructionFlags: []
7952
+ };
7393
7953
  }
7394
- function writeRefinementsStore(store) {
7395
- writeJsonAtomic(getRefinementsPath(), store);
7954
+ function normalizeConnector(input, retrievedAt) {
7955
+ validateObservedMetadata(input.observedSha256, input.observedByteLength);
7956
+ const provider = input.provider.trim();
7957
+ const opaqueId = input.opaqueId.trim();
7958
+ if (!provider || !opaqueId) throw new UserError(`connector sources require a provider and opaque id`);
7959
+ const fingerprint = stableHash({
7960
+ type: input.type,
7961
+ provider,
7962
+ opaqueId,
7963
+ revision: input.revision,
7964
+ observedSha256: input.observedSha256
7965
+ });
7966
+ const common = normalizedCommon(input, retrievedAt);
7967
+ const publicLocator = input.publicLocator ? canonicalPublicUrl(input.publicLocator, "connector public locator") : void 0;
7968
+ return {
7969
+ ...common,
7970
+ title: common.title || `${provider} source`,
7971
+ sourceId: sourceIdFor(fingerprint),
7972
+ type: "connector",
7973
+ privateLocator: `${provider}:${opaqueId}`,
7974
+ publicLocator: common.accessClass === "public" ? publicLocator : void 0,
7975
+ contentSha256: input.observedSha256,
7976
+ hashUnavailableReason: input.observedSha256 ? void 0 : "content is available only through the configured connector",
7977
+ fingerprint,
7978
+ retention: "not-retained",
7979
+ byteLength: input.observedByteLength,
7980
+ mediaType: input.mediaType,
7981
+ skippedContent: [],
7982
+ suspiciousInstructionFlags: []
7983
+ };
7984
+ }
7985
+ function normalizeStdin(input, retrievedAt, maxFileBytes, remainingBytes) {
7986
+ const bytes = Buffer.from(input.content);
7987
+ if (bytes.byteLength > maxFileBytes) throw new UserError(`stdin source exceeds the per-source byte limit (${maxFileBytes})`);
7988
+ if (bytes.byteLength > remainingBytes) throw new UserError(`stdin source exceeds the total source byte limit`);
7989
+ const contentSha256 = createHash6("sha256").update(bytes).digest("hex");
7990
+ const mediaType = input.mediaType ?? "text/plain";
7991
+ const fingerprint = stableHash({ type: input.type, contentSha256, mediaType });
7992
+ const common = normalizedCommon(input, retrievedAt);
7993
+ return {
7994
+ receipt: {
7995
+ ...common,
7996
+ title: common.title || "stdin source",
7997
+ sourceId: sourceIdFor(fingerprint),
7998
+ type: "stdin",
7999
+ privateLocator: "stdin",
8000
+ contentSha256,
8001
+ fingerprint,
8002
+ retention: "not-retained",
8003
+ byteLength: bytes.byteLength,
8004
+ mediaType,
8005
+ skippedContent: [],
8006
+ suspiciousInstructionFlags: suspiciousInstructionFlags(bytes, mediaType)
8007
+ },
8008
+ consumedBytes: bytes.byteLength
8009
+ };
8010
+ }
8011
+ function normalizeFile(input, retrievedAt, maxFileBytes, remainingBytes) {
8012
+ const resolved = resolve8(input.path);
8013
+ let lexicalStat;
8014
+ try {
8015
+ lexicalStat = lstatSync5(resolved);
8016
+ } catch (err) {
8017
+ throw new UserError(`unable to read source file ${resolved}: ${err.message}`);
8018
+ }
8019
+ if (lexicalStat.isSymbolicLink()) throw new UserError(`source file must not be a symlink: ${resolved}`);
8020
+ if (!lexicalStat.isFile()) throw new UserError(`source file is not a regular file: ${resolved}`);
8021
+ if (hasCredentialShapedName(basename12(resolved))) throw new UserError(`refusing credential-shaped source file: ${resolved}`);
8022
+ const observed = observeFile(resolved, basename12(resolved), input.mediaType, maxFileBytes, remainingBytes);
8023
+ if (!("realPath" in observed)) {
8024
+ throw new UserError(`source file ${resolved} was rejected: ${observed.reason}`);
8025
+ }
8026
+ const fingerprint = stableHash({
8027
+ type: input.type,
8028
+ path: observed.realPath,
8029
+ sha256: observed.sha256,
8030
+ mediaType: observed.mediaType
8031
+ });
8032
+ const sourceId = sourceIdFor(fingerprint);
8033
+ const common = normalizedCommon(input, retrievedAt);
8034
+ const receipt = {
8035
+ ...common,
8036
+ title: common.title || basename12(observed.realPath),
8037
+ sourceId,
8038
+ type: "file",
8039
+ privateLocator: observed.realPath,
8040
+ contentSha256: observed.sha256,
8041
+ fingerprint,
8042
+ retention: "not-retained",
8043
+ byteLength: observed.byteLength,
8044
+ mediaType: observed.mediaType,
8045
+ skippedContent: [],
8046
+ suspiciousInstructionFlags: observed.suspiciousInstructionFlags
8047
+ };
8048
+ return {
8049
+ receipt,
8050
+ observedFiles: [{
8051
+ sourceId,
8052
+ path: observed.realPath,
8053
+ relativePath: observed.relativePath,
8054
+ byteLength: observed.byteLength,
8055
+ sha256: observed.sha256,
8056
+ mediaType: observed.mediaType,
8057
+ bytes: observed.bytes
8058
+ }],
8059
+ consumedBytes: observed.byteLength
8060
+ };
8061
+ }
8062
+ function normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes) {
8063
+ const resolved = resolve8(input.path);
8064
+ let lexicalStat;
8065
+ try {
8066
+ lexicalStat = lstatSync5(resolved);
8067
+ } catch (err) {
8068
+ throw new UserError(`unable to read source directory ${resolved}: ${err.message}`);
8069
+ }
8070
+ if (lexicalStat.isSymbolicLink()) throw new UserError(`source directory must not be a symlink: ${resolved}`);
8071
+ if (!lexicalStat.isDirectory()) throw new UserError(`source directory is not a directory: ${resolved}`);
8072
+ const realRoot = realpathSync7(resolved);
8073
+ const observations = [];
8074
+ const skipped = [];
8075
+ const seenFiles = /* @__PURE__ */ new Set();
8076
+ const seenDirs = /* @__PURE__ */ new Set([realRoot]);
8077
+ let consumedBytes = 0;
8078
+ const walk2 = (dir, logicalDir) => {
8079
+ let entries;
8080
+ try {
8081
+ entries = readdirSync9(dir, { withFileTypes: true }).sort((a, b) => compareStrings2(a.name, b.name));
8082
+ } catch (err) {
8083
+ throw new UserError(`unable to enumerate source directory ${dir}: ${err.message}`);
8084
+ }
8085
+ for (const entry of entries) {
8086
+ const full = join29(dir, entry.name);
8087
+ const logicalPath = logicalDir ? `${logicalDir}/${entry.name}` : entry.name;
8088
+ if (entry.isDirectory() && SKIP_DIRS4.has(entry.name)) {
8089
+ skipped.push({ path: logicalPath, reason: "ignored-directory" });
8090
+ continue;
8091
+ }
8092
+ if (!input.includeHidden && isHiddenName(entry.name)) {
8093
+ skipped.push({ path: logicalPath, reason: "hidden" });
8094
+ continue;
8095
+ }
8096
+ if (hasCredentialShapedName(entry.name)) {
8097
+ skipped.push({ path: logicalPath, reason: "credential-shaped-name" });
8098
+ continue;
8099
+ }
8100
+ if (entry.isSymbolicLink()) {
8101
+ let realTarget;
8102
+ try {
8103
+ realTarget = realpathSync7(full);
8104
+ } catch {
8105
+ throw new UserError(`source directory contains an unreadable symlink: ${logicalPath}`);
8106
+ }
8107
+ if (!isInside2(realTarget, realRoot)) {
8108
+ throw new UserError(`source directory symlink escapes its root: ${logicalPath}`);
8109
+ }
8110
+ const targetStat = statSync11(realTarget);
8111
+ if (targetStat.isDirectory()) {
8112
+ if (seenDirs.has(realTarget)) continue;
8113
+ seenDirs.add(realTarget);
8114
+ walk2(realTarget, logicalPath);
8115
+ continue;
8116
+ }
8117
+ if (!targetStat.isFile()) {
8118
+ skipped.push({ path: logicalPath, reason: "unsupported-media-type" });
8119
+ continue;
8120
+ }
8121
+ } else if (entry.isDirectory()) {
8122
+ const realDir = realpathSync7(full);
8123
+ if (seenDirs.has(realDir)) continue;
8124
+ seenDirs.add(realDir);
8125
+ walk2(full, logicalPath);
8126
+ continue;
8127
+ } else if (!entry.isFile()) {
8128
+ skipped.push({ path: logicalPath, reason: "unsupported-media-type" });
8129
+ continue;
8130
+ }
8131
+ const realFile = realpathSync7(full);
8132
+ if (!isInside2(realFile, realRoot)) {
8133
+ throw new UserError(`source directory entry escaped its root while being observed: ${logicalPath}`);
8134
+ }
8135
+ if (seenFiles.has(realFile)) {
8136
+ skipped.push({ path: logicalPath, reason: "duplicate-realpath" });
8137
+ continue;
8138
+ }
8139
+ seenFiles.add(realFile);
8140
+ const observed = observeFile(realFile, logicalPath, void 0, maxFileBytes, remainingBytes - consumedBytes);
8141
+ if (!("realPath" in observed)) {
8142
+ skipped.push(observed);
8143
+ continue;
8144
+ }
8145
+ observations.push(observed);
8146
+ consumedBytes += observed.byteLength;
8147
+ }
8148
+ };
8149
+ walk2(realRoot, "");
8150
+ observations.sort((a, b) => compareStrings2(a.relativePath, b.relativePath));
8151
+ skipped.sort((a, b) => compareStrings2(a.path, b.path) || compareStrings2(a.reason, b.reason));
8152
+ const contentSha256 = createHash6("sha256");
8153
+ for (const file of observations) contentSha256.update(`${file.relativePath}\0${file.sha256}
8154
+ `);
8155
+ const aggregateHash = contentSha256.digest("hex");
8156
+ const fingerprint = stableHash({
8157
+ type: input.type,
8158
+ path: realRoot,
8159
+ files: observations.map(({ relativePath, sha256: sha2562, byteLength, mediaType }) => ({ relativePath, sha256: sha2562, byteLength, mediaType }))
8160
+ });
8161
+ const sourceId = sourceIdFor(fingerprint);
8162
+ const common = normalizedCommon(input, retrievedAt);
8163
+ const receipt = {
8164
+ ...common,
8165
+ title: common.title || basename12(realRoot),
8166
+ sourceId,
8167
+ type: "directory",
8168
+ privateLocator: realRoot,
8169
+ contentSha256: aggregateHash,
8170
+ fingerprint,
8171
+ retention: "not-retained",
8172
+ byteLength: consumedBytes,
8173
+ mediaType: input.mediaType ?? "application/x-directory",
8174
+ includeHidden: input.includeHidden === true,
8175
+ skippedContent: skipped,
8176
+ suspiciousInstructionFlags: [...new Set(observations.flatMap((file) => file.suspiciousInstructionFlags))].sort()
8177
+ };
8178
+ return {
8179
+ receipt,
8180
+ observedFiles: observations.map((file) => ({
8181
+ sourceId,
8182
+ path: file.realPath,
8183
+ relativePath: file.relativePath,
8184
+ byteLength: file.byteLength,
8185
+ sha256: file.sha256,
8186
+ mediaType: file.mediaType,
8187
+ bytes: file.bytes
8188
+ })),
8189
+ consumedBytes
8190
+ };
8191
+ }
8192
+ function normalizeSources(inputs, options = {}) {
8193
+ if (inputs.length === 0) throw new UserError(`at least one source is required`);
8194
+ const maxFileBytes = ensurePositiveLimit(options.maxFileBytes ?? DEFAULT_MAX_SOURCE_FILE_BYTES, "maxFileBytes");
8195
+ const maxTotalBytes = ensurePositiveLimit(options.maxTotalBytes ?? DEFAULT_MAX_SOURCE_TOTAL_BYTES, "maxTotalBytes");
8196
+ const retrievedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
8197
+ const sources = [];
8198
+ const observedFiles = [];
8199
+ let totalBytes = 0;
8200
+ for (const input of inputs) {
8201
+ const remainingBytes = maxTotalBytes - totalBytes;
8202
+ if (remainingBytes <= 0) throw new UserError(`total source byte limit exceeded (${maxTotalBytes})`);
8203
+ if (input.type === "url") {
8204
+ sources.push(normalizeUrl(input, retrievedAt));
8205
+ } else if (input.type === "connector") {
8206
+ sources.push(normalizeConnector(input, retrievedAt));
8207
+ } else if (input.type === "stdin") {
8208
+ const normalized = normalizeStdin(input, retrievedAt, maxFileBytes, remainingBytes);
8209
+ sources.push(normalized.receipt);
8210
+ totalBytes += normalized.consumedBytes;
8211
+ } else if (input.type === "file") {
8212
+ const normalized = normalizeFile(input, retrievedAt, maxFileBytes, remainingBytes);
8213
+ sources.push(normalized.receipt);
8214
+ observedFiles.push(...normalized.observedFiles);
8215
+ totalBytes += normalized.consumedBytes;
8216
+ } else {
8217
+ const normalized = normalizeDirectory(input, retrievedAt, maxFileBytes, remainingBytes);
8218
+ sources.push(normalized.receipt);
8219
+ observedFiles.push(...normalized.observedFiles);
8220
+ totalBytes += normalized.consumedBytes;
8221
+ }
8222
+ }
8223
+ const distinctSources = [...new Map(sources.map((source) => [source.sourceId, source])).values()];
8224
+ const distinctObservedFiles = [
8225
+ ...new Map(observedFiles.map((file) => [`${file.sourceId}\0${file.path}`, file])).values()
8226
+ ];
8227
+ return {
8228
+ sources: distinctSources,
8229
+ sourceInventoryHash: sourceInventoryHashFor(distinctSources),
8230
+ observedFiles: distinctObservedFiles,
8231
+ totalBytes
8232
+ };
8233
+ }
8234
+
8235
+ // src/insights/store.ts
8236
+ var STUDY_ID = /^STUDY-[0-9a-f]{16}$/;
8237
+ function getInsightsDir() {
8238
+ return join30(getHomeDir(), "insights");
8239
+ }
8240
+ function getStudyDir(studyId) {
8241
+ assertStudyId(studyId);
8242
+ return join30(getInsightsDir(), studyId);
8243
+ }
8244
+ function getStudyPath(studyId) {
8245
+ return join30(getStudyDir(studyId), "study.json");
8246
+ }
8247
+ function getStudyEventsPath(studyId) {
8248
+ return join30(getStudyDir(studyId), "events.jsonl");
8249
+ }
8250
+ function createStudyId(sourceInventoryHash, createdAt) {
8251
+ return stableId("STUDY", { sourceInventoryHash, createdAt });
8252
+ }
8253
+ function assertStudyId(studyId) {
8254
+ if (!STUDY_ID.test(studyId)) throw new UserError(`invalid insight study id: ${studyId}`);
8255
+ }
8256
+ function ensurePrivateDir(path) {
8257
+ mkdirSync8(path, { recursive: true, mode: 448 });
8258
+ chmodSync2(path, 448);
8259
+ }
8260
+ function isRecord2(value) {
8261
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8262
+ }
8263
+ function isNonEmptyString(value) {
8264
+ return typeof value === "string" && value.length > 0;
8265
+ }
8266
+ function isStringArray(value) {
8267
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
8268
+ }
8269
+ function assertIsoDate(value, label2, path) {
8270
+ if (!isNonEmptyString(value) || Number.isNaN(Date.parse(value))) {
8271
+ throw new UserError(`malformed insight study at ${path}: ${label2} must be an ISO-8601 timestamp`);
8272
+ }
8273
+ }
8274
+ function validateSourceReceipt(value, index, path) {
8275
+ if (!isRecord2(value)) throw new UserError(`malformed insight study at ${path}: sources[${index}] is not an object`);
8276
+ if (!isNonEmptyString(value.sourceId) || !value.sourceId.startsWith("SRC-")) {
8277
+ throw new UserError(`malformed insight study at ${path}: sources[${index}] has an invalid sourceId`);
8278
+ }
8279
+ if (!SOURCE_TYPES.includes(value.type)) {
8280
+ throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid type`);
8281
+ }
8282
+ if (!isNonEmptyString(value.title) || !isNonEmptyString(value.privateLocator) || !isNonEmptyString(value.fingerprint)) {
8283
+ throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} is missing required identity fields`);
8284
+ }
8285
+ if (value.publicLocator !== void 0) {
8286
+ if (!isNonEmptyString(value.publicLocator)) {
8287
+ throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid publicLocator`);
8288
+ }
8289
+ try {
8290
+ if (canonicalPublicUrl(value.publicLocator, "source public locator") !== value.publicLocator) {
8291
+ throw new Error("not canonical");
8292
+ }
8293
+ } catch {
8294
+ throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid publicLocator`);
8295
+ }
8296
+ }
8297
+ if (value.type === "directory") {
8298
+ if (typeof value.includeHidden !== "boolean") {
8299
+ throw new UserError(`malformed insight study at ${path}: directory source ${value.sourceId} is missing includeHidden`);
8300
+ }
8301
+ } else if (value.includeHidden !== void 0) {
8302
+ throw new UserError(`malformed insight study at ${path}: non-directory source ${value.sourceId} has includeHidden`);
8303
+ }
8304
+ assertIsoDate(value.retrievedAt, `source ${value.sourceId}.retrievedAt`, path);
8305
+ if (!SOURCE_ACCESS_CLASSES.includes(value.accessClass)) {
8306
+ throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid accessClass`);
8307
+ }
8308
+ if (!SOURCE_LICENSE_CLASSES.includes(value.licenseClass)) {
8309
+ throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid licenseClass`);
8310
+ }
8311
+ if (!SOURCE_RETENTIONS.includes(value.retention)) {
8312
+ throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has an invalid retention value`);
8313
+ }
8314
+ const hasHash = isNonEmptyString(value.contentSha256);
8315
+ const hasUnavailableReason = isNonEmptyString(value.hashUnavailableReason);
8316
+ if (hasHash === hasUnavailableReason) {
8317
+ throw new UserError(
8318
+ `malformed insight study at ${path}: source ${value.sourceId} must have exactly one of contentSha256 or hashUnavailableReason`
8319
+ );
8320
+ }
8321
+ if (!Array.isArray(value.skippedContent) || !isStringArray(value.suspiciousInstructionFlags)) {
8322
+ throw new UserError(`malformed insight study at ${path}: source ${value.sourceId} has invalid evidence arrays`);
8323
+ }
8324
+ }
8325
+ function validateStudyShape(value, path) {
8326
+ if (!isRecord2(value)) throw new UserError(`malformed insight study at ${path}: expected an object`);
8327
+ if (value.schemaVersion !== INSIGHT_SCHEMA_VERSION) {
8328
+ throw new UserError(`malformed insight study at ${path}: unsupported schemaVersion ${String(value.schemaVersion)}`);
8329
+ }
8330
+ if (!isNonEmptyString(value.studyId) || !STUDY_ID.test(value.studyId)) {
8331
+ throw new UserError(`malformed insight study at ${path}: invalid studyId`);
8332
+ }
8333
+ if (!STUDY_STATUSES.includes(value.status)) {
8334
+ throw new UserError(`malformed insight study at ${path}: invalid status ${String(value.status)}`);
8335
+ }
8336
+ assertIsoDate(value.createdAt, "createdAt", path);
8337
+ assertIsoDate(value.updatedAt, "updatedAt", path);
8338
+ if (value.requestedAnalysisMode !== "independent" && value.requestedAnalysisMode !== "single") {
8339
+ throw new UserError(`malformed insight study at ${path}: invalid requestedAnalysisMode`);
8340
+ }
8341
+ if (!Array.isArray(value.sources)) throw new UserError(`malformed insight study at ${path}: sources must be an array`);
8342
+ value.sources.forEach((source, index) => validateSourceReceipt(source, index, path));
8343
+ if (!isNonEmptyString(value.sourceInventoryHash)) {
8344
+ throw new UserError(`malformed insight study at ${path}: missing sourceInventoryHash`);
8345
+ }
8346
+ if (sourceInventoryHashFor(value.sources) !== value.sourceInventoryHash) {
8347
+ throw new UserError(`malformed insight study at ${path}: sourceInventoryHash does not match sources`);
8348
+ }
8349
+ if (!Array.isArray(value.insights) || !Array.isArray(value.domainPlans)) {
8350
+ throw new UserError(`malformed insight study at ${path}: insights and domainPlans must be arrays`);
8351
+ }
8352
+ if (value.status === "approved" || value.status === "jira-manifested") {
8353
+ if (!isRecord2(value.approval) || !isNonEmptyString(value.planHash)) {
8354
+ throw new UserError(`malformed insight study at ${path}: status ${value.status} requires planHash and approval`);
8355
+ }
8356
+ if (value.approval.planHash !== value.planHash) {
8357
+ throw new UserError(`malformed insight study at ${path}: approval planHash does not match study planHash`);
8358
+ }
8359
+ if (value.approval.sourceInventoryHash !== value.sourceInventoryHash) {
8360
+ throw new UserError(`malformed insight study at ${path}: approval sourceInventoryHash does not match study`);
8361
+ }
8362
+ if (value.approval.capabilityInventoryHash !== value.capabilityInventoryHash) {
8363
+ throw new UserError(`malformed insight study at ${path}: approval capabilityInventoryHash does not match study`);
8364
+ }
8365
+ assertIsoDate(value.approval.approvedAt, "approval.approvedAt", path);
8366
+ } else if (value.approval !== void 0 || value.jira !== void 0) {
8367
+ throw new UserError(`malformed insight study at ${path}: status ${value.status} must not carry approval or Jira state`);
8368
+ }
8369
+ if (value.status === "jira-manifested" && !isRecord2(value.jira)) {
8370
+ throw new UserError(`malformed insight study at ${path}: jira-manifested status requires Jira receipt metadata`);
8371
+ }
8372
+ if (value.status === "dismissed" || value.status === "archived") {
8373
+ assertIsoDate(value.closedAt, "closedAt", path);
8374
+ }
8375
+ }
8376
+ function parseStudy(path) {
8377
+ let raw;
8378
+ try {
8379
+ raw = readFileSync20(path, "utf-8");
8380
+ } catch (err) {
8381
+ throw new UserError(`unable to read insight study at ${path}: ${err.message}`);
8382
+ }
8383
+ let parsed;
8384
+ try {
8385
+ parsed = JSON.parse(raw);
8386
+ } catch {
8387
+ throw new UserError(`malformed insight study at ${path}: not valid JSON`);
8388
+ }
8389
+ validateStudyShape(parsed, path);
8390
+ return parsed;
8391
+ }
8392
+ function approvalBasisHash(study) {
8393
+ return stableHash({
8394
+ sourceInventoryHash: study.sourceInventoryHash,
8395
+ requestedAnalysisMode: study.requestedAnalysisMode,
8396
+ actualAnalysisMode: study.actualAnalysisMode,
8397
+ capabilityInventoryHash: study.capabilityInventoryHash,
8398
+ insights: study.insights,
8399
+ domainPlans: study.domainPlans,
8400
+ planHash: study.planHash,
8401
+ approvalSelection: study.approval ? {
8402
+ approvedDomainIds: study.approval.approvedDomainIds,
8403
+ acceptedExceptions: study.approval.acceptedExceptions
8404
+ } : void 0
8405
+ });
8406
+ }
8407
+ function createInsightStudy(input, options = {}) {
8408
+ const now = options.now ?? /* @__PURE__ */ new Date();
8409
+ const createdAt = now.toISOString();
8410
+ const studyId = options.studyId ?? createStudyId(input.sourceInventoryHash, createdAt);
8411
+ assertStudyId(studyId);
8412
+ const path = getStudyPath(studyId);
8413
+ if (existsSync25(path)) throw new UserError(`insight study already exists: ${studyId}`);
8414
+ const study = {
8415
+ schemaVersion: INSIGHT_SCHEMA_VERSION,
8416
+ studyId,
8417
+ status: "collecting",
8418
+ createdAt,
8419
+ updatedAt: createdAt,
8420
+ requestedAnalysisMode: input.requestedAnalysisMode,
8421
+ sources: input.sources,
8422
+ sourceInventoryHash: input.sourceInventoryHash,
8423
+ insights: [],
8424
+ domainPlans: []
8425
+ };
8426
+ validateStudyShape(study, path);
8427
+ ensurePrivateDir(getInsightsDir());
8428
+ ensurePrivateDir(getStudyDir(studyId));
8429
+ writeJsonAtomic(path, study);
8430
+ return study;
8431
+ }
8432
+ function readInsightStudy(studyId) {
8433
+ const path = getStudyPath(studyId);
8434
+ if (!existsSync25(path)) throw new UserError(`no insight study with id: ${studyId}`);
8435
+ const study = parseStudy(path);
8436
+ if (study.studyId !== studyId) {
8437
+ throw new UserError(`malformed insight study at ${path}: stored studyId does not match its directory`);
8438
+ }
8439
+ return study;
8440
+ }
8441
+ function writeInsightStudy(study, now = /* @__PURE__ */ new Date()) {
8442
+ assertStudyId(study.studyId);
8443
+ const path = getStudyPath(study.studyId);
8444
+ if (!existsSync25(path)) throw new UserError(`no insight study with id: ${study.studyId}`);
8445
+ const existing = parseStudy(path);
8446
+ let next = { ...study, updatedAt: now.toISOString() };
8447
+ if (next.status === "dismissed" || next.status === "archived") {
8448
+ next = { ...next, approval: void 0, jira: void 0 };
8449
+ } else if (existing.approval && approvalBasisHash(existing) !== approvalBasisHash(next)) {
8450
+ next = {
8451
+ ...next,
8452
+ status: "review-ready",
8453
+ approval: void 0,
8454
+ jira: void 0
8455
+ };
8456
+ }
8457
+ validateStudyShape(next, path);
8458
+ ensurePrivateDir(getInsightsDir());
8459
+ ensurePrivateDir(getStudyDir(next.studyId));
8460
+ writeJsonAtomic(path, next);
8461
+ return next;
8462
+ }
8463
+ function appendStudyEvent(studyId, input) {
8464
+ readInsightStudy(studyId);
8465
+ const occurredAt = input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString();
8466
+ if (!isNonEmptyString(input.type) || /[\u0000-\u001f\u007f-\u009f]/u.test(input.type)) {
8467
+ throw new UserError(`study event type must be non-empty and contain no control characters`);
8468
+ }
8469
+ assertIsoDate(occurredAt, "event occurredAt", getStudyEventsPath(studyId));
8470
+ const event = {
8471
+ eventId: stableId("EVT", { studyId, type: input.type, occurredAt, detail: input.detail }),
8472
+ studyId,
8473
+ type: input.type,
8474
+ occurredAt,
8475
+ detail: input.detail
8476
+ };
8477
+ const path = getStudyEventsPath(studyId);
8478
+ appendPrivateJsonLine(path, event);
8479
+ return event;
8480
+ }
8481
+
8482
+ // src/insights/validate.ts
8483
+ var CONTROL_OR_ANSI = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]|\u001b\[[0-?]*[ -/]*[@-~]/u;
8484
+ var PRIVATE_PATH = /(?:^|[\s("'])(?:~\/|\/(?:Users|home|private|tmp|var|etc|opt|Volumes|root)\/|[A-Za-z]:\\)/u;
8485
+ var SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat)-?[A-Za-z0-9_]{16,}|\bAKIA[0-9A-Z]{16}\b|\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b|\b(?:password|passwd|api[_-]?key|access[_-]?token)\s*[:=]\s*\S+)/iu;
8486
+ var STUDY_ID2 = /^STUDY-[0-9a-f]{16}$/;
8487
+ var INSIGHT_ID = /^INS-[0-9a-f]{16}$/;
8488
+ var DOMAIN_ID = /^DOM-[0-9a-f]{16}$/;
8489
+ var SAFE_WORK_ITEM_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
8490
+ var MAX_EXPORT_TEXT_CHARS = 1e4;
8491
+ function isRecord3(value) {
8492
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8493
+ }
8494
+ function nonEmpty(value) {
8495
+ return typeof value === "string" && value.trim().length > 0;
8496
+ }
8497
+ function stringArray(value) {
8498
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
8499
+ }
8500
+ function assertKeys(value, allowed, label2) {
8501
+ const allowedSet = new Set(allowed);
8502
+ const extra = Object.keys(value).filter((key) => value[key] !== void 0 && !allowedSet.has(key));
8503
+ if (extra.length > 0) throw new UserError(`${label2} contains unsupported field(s): ${extra.join(", ")}`);
8504
+ }
8505
+ function assertRequired(value, required, label2) {
8506
+ const missing = required.filter((key) => value[key] === void 0);
8507
+ if (missing.length > 0) throw new UserError(`${label2} is missing required field(s): ${missing.join(", ")}`);
8508
+ }
8509
+ function assertStringArray(value, label2, minItems = 0) {
8510
+ if (!stringArray(value) || value.length < minItems || value.some((entry) => entry.trim().length === 0)) {
8511
+ throw new UserError(`${label2} must be an array of ${minItems > 0 ? "non-empty " : ""}strings${minItems > 0 ? ` with at least ${minItems} item(s)` : ""}`);
8512
+ }
8513
+ }
8514
+ function assertExportSafeText(value, label2, privateLocators) {
8515
+ if (value.length > MAX_EXPORT_TEXT_CHARS) throw new UserError(`${label2} exceeds ${MAX_EXPORT_TEXT_CHARS} characters`);
8516
+ if (CONTROL_OR_ANSI.test(value)) throw new UserError(`${label2} contains control or terminal escape characters`);
8517
+ if (PRIVATE_PATH.test(value)) throw new UserError(`${label2} contains an absolute private path`);
8518
+ if (SECRET_VALUE.test(value)) throw new UserError(`${label2} contains credential-shaped content`);
8519
+ const disclosedLocator = privateLocators.find((locator) => locator.length >= 4 && value.includes(locator));
8520
+ if (disclosedLocator) throw new UserError(`${label2} contains a private source locator`);
8521
+ }
8522
+ function exportStrings(value, prefix = "$") {
8523
+ if (typeof value === "string") return [{ path: prefix, value }];
8524
+ if (Array.isArray(value)) return value.flatMap((child, index) => exportStrings(child, `${prefix}[${index}]`));
8525
+ if (!isRecord3(value)) return [];
8526
+ return Object.entries(value).flatMap(([key, child]) => exportStrings(child, `${prefix}.${key}`));
8527
+ }
8528
+ function privateLocatorsFor(study) {
8529
+ return study.sources.filter((source) => source.privateLocator !== source.publicLocator).map((source) => source.privateLocator).filter((locator) => locator !== "stdin");
8530
+ }
8531
+ function assertExportSafe(value, study, label2 = "export") {
8532
+ const privateLocators = privateLocatorsFor(study);
8533
+ for (const entry of exportStrings(value)) assertExportSafeText(entry.value, `${label2}${entry.path}`, privateLocators);
8534
+ }
8535
+ function parseAnchor(value, label2, sourceIds) {
8536
+ if (!isRecord3(value)) throw new UserError(`${label2} must be an object`);
8537
+ assertKeys(value, ["sourceId", "locator", "excerpt"], label2);
8538
+ assertRequired(value, ["sourceId", "locator"], label2);
8539
+ if (!nonEmpty(value.sourceId) || !sourceIds.has(value.sourceId)) throw new UserError(`${label2}.sourceId is unknown`);
8540
+ if (!nonEmpty(value.locator)) throw new UserError(`${label2}.locator must be a non-empty string`);
8541
+ if (value.excerpt !== void 0) {
8542
+ if (typeof value.excerpt !== "string" || value.excerpt.length > MAX_EVIDENCE_EXCERPT_CHARS) {
8543
+ throw new UserError(`${label2}.excerpt must not exceed ${MAX_EVIDENCE_EXCERPT_CHARS} characters`);
8544
+ }
8545
+ if (CONTROL_OR_ANSI.test(value.excerpt)) throw new UserError(`${label2}.excerpt contains control characters`);
8546
+ }
8547
+ return value;
8548
+ }
8549
+ function parseCapabilityCandidate(value, label2, inventoryById) {
8550
+ if (!isRecord3(value)) throw new UserError(`${label2} must be an object`);
8551
+ assertKeys(value, ["capabilityId", "label", "fingerprint", "lexicalScore", "relationship", "semanticReviewRequired"], label2);
8552
+ assertRequired(value, ["label", "relationship", "semanticReviewRequired"], label2);
8553
+ if (!nonEmpty(value.label)) throw new UserError(`${label2}.label must be a non-empty string`);
8554
+ if (!["existing", "extension", "ambiguous", "gap"].includes(String(value.relationship))) {
8555
+ throw new UserError(`${label2}.relationship is unsupported`);
8556
+ }
8557
+ if (value.semanticReviewRequired !== true) throw new UserError(`${label2}.semanticReviewRequired must be true`);
8558
+ if (value.lexicalScore !== void 0 && (typeof value.lexicalScore !== "number" || value.lexicalScore < 0 || value.lexicalScore > 1)) {
8559
+ throw new UserError(`${label2}.lexicalScore must be a number from 0 to 1`);
8560
+ }
8561
+ if (value.capabilityId !== void 0) {
8562
+ if (!nonEmpty(value.capabilityId) || !inventoryById.has(value.capabilityId)) {
8563
+ throw new UserError(`${label2}.capabilityId is not present in the current inventory`);
8564
+ }
8565
+ const current = inventoryById.get(value.capabilityId);
8566
+ if (value.fingerprint !== void 0 && value.fingerprint !== current.fingerprint) {
8567
+ throw new UserError(`${label2}.fingerprint does not match the current capability inventory`);
8568
+ }
8569
+ } else if (value.fingerprint !== void 0) {
8570
+ throw new UserError(`${label2}.fingerprint requires a capabilityId`);
8571
+ }
8572
+ if (value.relationship === "existing" || value.relationship === "extension") {
8573
+ const currentFingerprint = nonEmpty(value.capabilityId) ? inventoryById.get(value.capabilityId)?.fingerprint : void 0;
8574
+ if (!currentFingerprint || value.fingerprint !== currentFingerprint) {
8575
+ throw new UserError(`${label2}.${String(value.relationship)} requires a capabilityId and exact current fingerprint`);
8576
+ }
8577
+ }
8578
+ if (value.relationship === "gap" && (value.capabilityId !== void 0 || value.fingerprint !== void 0)) {
8579
+ throw new UserError(`${label2}.gap must not claim an existing capability identity`);
8580
+ }
8581
+ return value;
8582
+ }
8583
+ function parseInsight(value, index, study, inventory) {
8584
+ const label2 = `insights[${index}]`;
8585
+ if (!isRecord3(value)) throw new UserError(`${label2} must be an object`);
8586
+ const allowed = [
8587
+ "insightId",
8588
+ "claim",
8589
+ "domain",
8590
+ "sourceIds",
8591
+ "evidence",
8592
+ "extractionPassIds",
8593
+ "independenceMode",
8594
+ "validation",
8595
+ "contradictingEvidence",
8596
+ "unresolvedQuestions",
8597
+ "relevance",
8598
+ "relevanceBasis",
8599
+ "confidence",
8600
+ "confidenceBasis",
8601
+ "provenanceConstraints",
8602
+ "licenseConstraints",
8603
+ "privacyConstraints",
8604
+ "securityConstraints",
8605
+ "proposedVerb",
8606
+ "capabilityCandidates",
8607
+ "inventoryCoverage",
8608
+ "inventoryCoverageReason"
8609
+ ];
8610
+ assertKeys(value, allowed, label2);
8611
+ assertRequired(value, allowed.filter((key) => key !== "inventoryCoverageReason"), label2);
8612
+ if (!nonEmpty(value.insightId) || !INSIGHT_ID.test(value.insightId)) throw new UserError(`${label2}.insightId is invalid`);
8613
+ if (!nonEmpty(value.claim) || !nonEmpty(value.domain)) throw new UserError(`${label2} requires a claim and domain`);
8614
+ assertStringArray(value.sourceIds, `${label2}.sourceIds`, 1);
8615
+ const knownSources = new Set(study.sources.map((source) => source.sourceId));
8616
+ if (value.sourceIds.some((id) => !knownSources.has(id))) throw new UserError(`${label2}.sourceIds contains an unknown source`);
8617
+ if (new Set(value.sourceIds).size !== value.sourceIds.length) throw new UserError(`${label2}.sourceIds contains duplicates`);
8618
+ const expectedInsightId = insightIdFor(value.domain, value.claim, value.sourceIds);
8619
+ if (value.insightId !== expectedInsightId) throw new UserError(`${label2}.insightId must equal the stable id ${expectedInsightId}`);
8620
+ if (!Array.isArray(value.evidence) || value.evidence.length === 0) throw new UserError(`${label2}.evidence must not be empty`);
8621
+ const sourceIds = new Set(value.sourceIds);
8622
+ const evidence = value.evidence.map((anchor, anchorIndex) => parseAnchor(anchor, `${label2}.evidence[${anchorIndex}]`, sourceIds));
8623
+ assertStringArray(value.extractionPassIds, `${label2}.extractionPassIds`, 1);
8624
+ if (new Set(value.extractionPassIds).size !== value.extractionPassIds.length) {
8625
+ throw new UserError(`${label2}.extractionPassIds contains duplicates`);
8626
+ }
8627
+ if (!ANALYSIS_MODES.includes(value.independenceMode)) throw new UserError(`${label2}.independenceMode is unsupported`);
8628
+ if (!INSIGHT_VALIDATIONS.includes(value.validation)) throw new UserError(`${label2}.validation is unsupported`);
8629
+ if (!Array.isArray(value.contradictingEvidence)) throw new UserError(`${label2}.contradictingEvidence must be an array`);
8630
+ const contradictingEvidence = value.contradictingEvidence.map((item, contradictionIndex) => {
8631
+ const itemLabel = `${label2}.contradictingEvidence[${contradictionIndex}]`;
8632
+ if (!isRecord3(item)) throw new UserError(`${itemLabel} must be an object`);
8633
+ assertKeys(item, ["claim", "anchors"], itemLabel);
8634
+ assertRequired(item, ["claim", "anchors"], itemLabel);
8635
+ if (!nonEmpty(item.claim) || !Array.isArray(item.anchors) || item.anchors.length === 0) {
8636
+ throw new UserError(`${itemLabel} requires a claim and at least one anchor`);
8637
+ }
8638
+ return {
8639
+ claim: item.claim,
8640
+ anchors: item.anchors.map((anchor, anchorIndex) => parseAnchor(anchor, `${itemLabel}.anchors[${anchorIndex}]`, sourceIds))
8641
+ };
8642
+ });
8643
+ const anchoredSourceIds = /* @__PURE__ */ new Set([
8644
+ ...evidence.map((anchor) => anchor.sourceId),
8645
+ ...contradictingEvidence.flatMap((item) => item.anchors.map((anchor) => anchor.sourceId))
8646
+ ]);
8647
+ if (value.sourceIds.some((sourceId) => !anchoredSourceIds.has(sourceId))) {
8648
+ throw new UserError(`${label2}.evidence must anchor every declared sourceId`);
8649
+ }
8650
+ if (value.validation === "contested" && contradictingEvidence.length === 0) {
8651
+ throw new UserError(`${label2} is contested but has no contradicting evidence`);
8652
+ }
8653
+ for (const key of ["unresolvedQuestions", "provenanceConstraints", "licenseConstraints", "privacyConstraints", "securityConstraints"]) {
8654
+ assertStringArray(value[key], `${label2}.${key}`);
8655
+ }
8656
+ if (!["high", "medium", "low"].includes(String(value.relevance)) || !nonEmpty(value.relevanceBasis)) {
8657
+ throw new UserError(`${label2} requires a supported relevance and written basis`);
8658
+ }
8659
+ if (!["high", "medium", "low"].includes(String(value.confidence)) || !nonEmpty(value.confidenceBasis)) {
8660
+ throw new UserError(`${label2} requires a supported confidence and written basis`);
8661
+ }
8662
+ if (!FORGE_VERBS2.includes(value.proposedVerb)) throw new UserError(`${label2}.proposedVerb is unsupported`);
8663
+ if (!INVENTORY_COVERAGE_VALUES.includes(value.inventoryCoverage)) throw new UserError(`${label2}.inventoryCoverage is unsupported`);
8664
+ if (value.inventoryCoverage !== "complete" && !nonEmpty(value.inventoryCoverageReason)) {
8665
+ throw new UserError(`${label2}.inventoryCoverageReason is required when coverage is not complete`);
8666
+ }
8667
+ if (value.inventoryCoverage === "complete" && inventory.coverage !== "complete") {
8668
+ throw new UserError(`${label2} cannot claim complete inventory coverage when the current inventory is ${inventory.coverage}`);
8669
+ }
8670
+ if (!Array.isArray(value.capabilityCandidates)) throw new UserError(`${label2}.capabilityCandidates must be an array`);
8671
+ const inventoryById = new Map(inventory.capabilities.map((entry) => [entry.capabilityId, entry]));
8672
+ const capabilityCandidates = value.capabilityCandidates.map(
8673
+ (candidate, candidateIndex) => parseCapabilityCandidate(candidate, `${label2}.capabilityCandidates[${candidateIndex}]`, inventoryById)
8674
+ );
8675
+ if (value.proposedVerb === "ABSORB" && !capabilityCandidates.some((candidate) => candidate.relationship === "existing" || candidate.relationship === "extension")) {
8676
+ throw new UserError(`${label2}: ABSORB requires an exact existing/extension capability candidate`);
8677
+ }
8678
+ if (value.proposedVerb === "FORK") {
8679
+ if (value.inventoryCoverage !== "complete" || inventory.coverage !== "complete") {
8680
+ throw new UserError(`${label2}: FORK requires complete installed and skill-map inventory coverage`);
8681
+ }
8682
+ if (!capabilityCandidates.some((candidate) => candidate.relationship === "gap")) {
8683
+ throw new UserError(`${label2}: FORK requires an explicitly reviewed capability gap`);
8684
+ }
8685
+ if (capabilityCandidates.some((candidate) => candidate.relationship === "existing" || candidate.relationship === "ambiguous")) {
8686
+ throw new UserError(`${label2}: FORK is blocked while existing or ambiguous capability candidates remain`);
8687
+ }
8688
+ }
8689
+ if (value.validation === "corroborated" && (new Set(value.sourceIds).size < 2 || new Set(value.extractionPassIds).size < 2)) {
8690
+ throw new UserError(`${label2}: corroborated evidence requires at least two sources and two extraction passes`);
8691
+ }
8692
+ const sources = study.sources.filter((source) => sourceIds.has(source.sourceId));
8693
+ const restricted = sources.some(
8694
+ (source) => ["copyleft", "proprietary", "unknown"].includes(source.licenseClass) || ["client-confidential", "restricted"].includes(source.accessClass)
8695
+ );
8696
+ if (restricted && (value.proposedVerb === "ABSORB" || value.proposedVerb === "FORK")) {
8697
+ throw new UserError(`${label2}: restrictive/private evidence cannot authorize ABSORB or FORK without a separately modeled legal exception`);
8698
+ }
8699
+ if (sources.some((source) => source.licenseClass === "attribution-required") && value.licenseConstraints.length === 0) {
8700
+ throw new UserError(`${label2}: attribution-required evidence must carry a license obligation`);
8701
+ }
8702
+ if (sources.some((source) => ["client-confidential", "restricted"].includes(source.accessClass)) && value.privacyConstraints.length === 0) {
8703
+ throw new UserError(`${label2}: private/restricted evidence must carry a privacy obligation`);
8704
+ }
8705
+ if (sources.some((source) => source.suspiciousInstructionFlags.length > 0) && value.securityConstraints.length === 0) {
8706
+ throw new UserError(`${label2}: suspicious source instructions must remain visible as a security constraint`);
8707
+ }
8708
+ const parsed = {
8709
+ ...value,
8710
+ sourceIds: [...value.sourceIds],
8711
+ evidence,
8712
+ contradictingEvidence,
8713
+ capabilityCandidates
8714
+ };
8715
+ assertExportSafe(
8716
+ {
8717
+ claim: parsed.claim,
8718
+ domain: parsed.domain,
8719
+ unresolvedQuestions: parsed.unresolvedQuestions,
8720
+ relevanceBasis: parsed.relevanceBasis,
8721
+ confidenceBasis: parsed.confidenceBasis,
8722
+ provenanceConstraints: parsed.provenanceConstraints,
8723
+ licenseConstraints: parsed.licenseConstraints,
8724
+ privacyConstraints: parsed.privacyConstraints,
8725
+ securityConstraints: parsed.securityConstraints,
8726
+ candidateLabels: parsed.capabilityCandidates.map((candidate) => candidate.label)
8727
+ },
8728
+ study,
8729
+ label2
8730
+ );
8731
+ return parsed;
8732
+ }
8733
+ function parseDomainPlan(value, index, insightsById, inventory, study) {
8734
+ const label2 = `domainPlans[${index}]`;
8735
+ if (!isRecord3(value)) throw new UserError(`${label2} must be an object`);
8736
+ const required = [
8737
+ "domainId",
8738
+ "domain",
8739
+ "outcome",
8740
+ "acceptedInsightIds",
8741
+ "contestedInsightIds",
8742
+ "rejectedInsightIds",
8743
+ "targetCapabilityIds",
8744
+ "targetFingerprints",
8745
+ "deduplicationEvidence",
8746
+ "selectedVerb",
8747
+ "proposedChanges",
8748
+ "provenanceObligations",
8749
+ "licenseObligations",
8750
+ "privacyObligations",
8751
+ "securityObligations",
8752
+ "dependencies",
8753
+ "acceptanceTests",
8754
+ "measurement",
8755
+ "rollbackMechanism",
8756
+ "rollbackTrigger",
8757
+ "workItems",
8758
+ "nonGoals"
8759
+ ];
8760
+ assertKeys(value, required, label2);
8761
+ assertRequired(value, required, label2);
8762
+ if (!nonEmpty(value.domainId) || !DOMAIN_ID.test(value.domainId) || !nonEmpty(value.domain) || !nonEmpty(value.outcome)) {
8763
+ throw new UserError(`${label2} requires a valid domainId, domain, and outcome`);
8764
+ }
8765
+ const groups = ["acceptedInsightIds", "contestedInsightIds", "rejectedInsightIds"];
8766
+ for (const key of groups) assertStringArray(value[key], `${label2}.${key}`);
8767
+ const allInsightIds = groups.flatMap((key) => value[key]);
8768
+ if (new Set(allInsightIds).size !== allInsightIds.length) throw new UserError(`${label2} classifies an insight more than once`);
8769
+ if (allInsightIds.some((id) => !insightsById.has(id))) throw new UserError(`${label2} references an unknown insight`);
8770
+ const expectedId = domainPlanIdFor(value.domain, allInsightIds);
8771
+ if (value.domainId !== expectedId) throw new UserError(`${label2}.domainId must equal the stable id ${expectedId}`);
8772
+ for (const insightId of allInsightIds) {
8773
+ if (insightsById.get(insightId).domain !== value.domain) throw new UserError(`${label2} contains an insight from another domain`);
8774
+ }
8775
+ for (const insightId of value.acceptedInsightIds) {
8776
+ const insight2 = insightsById.get(insightId);
8777
+ if (["contested", "rejected", "unverifiable"].includes(insight2.validation)) {
8778
+ throw new UserError(`${label2} cannot accept ${insight2.validation} insight ${insightId}`);
8779
+ }
8780
+ }
8781
+ for (const insightId of value.contestedInsightIds) {
8782
+ if (insightsById.get(insightId).validation !== "contested") throw new UserError(`${label2} misclassifies non-contested insight ${insightId}`);
8783
+ }
8784
+ if (!FORGE_VERBS2.includes(value.selectedVerb)) throw new UserError(`${label2}.selectedVerb is unsupported`);
8785
+ const selectedVerb = value.selectedVerb;
8786
+ if (value.acceptedInsightIds.some((id) => insightsById.get(id).proposedVerb !== selectedVerb)) {
8787
+ throw new UserError(`${label2}.selectedVerb must match each accepted insight`);
8788
+ }
8789
+ assertStringArray(value.targetCapabilityIds, `${label2}.targetCapabilityIds`);
8790
+ if (!isRecord3(value.targetFingerprints)) throw new UserError(`${label2}.targetFingerprints must be an object`);
8791
+ const targetFingerprints = value.targetFingerprints;
8792
+ const inventoryById = new Map(inventory.capabilities.map((entry) => [entry.capabilityId, entry]));
8793
+ for (const capabilityId of value.targetCapabilityIds) {
8794
+ const current = inventoryById.get(capabilityId);
8795
+ if (!current) throw new UserError(`${label2} targets an unknown capability ${capabilityId}`);
8796
+ if (!current.fingerprint || targetFingerprints[capabilityId] !== current.fingerprint) {
8797
+ throw new UserError(`${label2} has a stale or unavailable fingerprint for ${capabilityId}`);
8798
+ }
8799
+ }
8800
+ if (Object.keys(targetFingerprints).some((id) => !value.targetCapabilityIds.includes(id))) {
8801
+ throw new UserError(`${label2}.targetFingerprints contains an untargeted capability`);
8802
+ }
8803
+ if (selectedVerb === "ABSORB") {
8804
+ const exactCandidateIds = new Set(
8805
+ value.acceptedInsightIds.flatMap((insightId) => insightsById.get(insightId).capabilityCandidates).flatMap(
8806
+ (candidate) => (candidate.relationship === "existing" || candidate.relationship === "extension") && candidate.capabilityId ? [candidate.capabilityId] : []
8807
+ )
8808
+ );
8809
+ const hasExactTarget = value.targetCapabilityIds.some(
8810
+ (capabilityId) => exactCandidateIds.has(capabilityId)
8811
+ );
8812
+ if (!hasExactTarget) {
8813
+ throw new UserError(`${label2}: ABSORB must bind an exact existing/extension capability candidate and current fingerprint`);
8814
+ }
8815
+ }
8816
+ for (const key of [
8817
+ "deduplicationEvidence",
8818
+ "proposedChanges",
8819
+ "provenanceObligations",
8820
+ "licenseObligations",
8821
+ "privacyObligations",
8822
+ "securityObligations",
8823
+ "dependencies",
8824
+ "acceptanceTests",
8825
+ "nonGoals"
8826
+ ]) assertStringArray(value[key], `${label2}.${key}`);
8827
+ if (value.acceptanceTests.length === 0) throw new UserError(`${label2}.acceptanceTests must not be empty`);
8828
+ if (!nonEmpty(value.rollbackMechanism) || !nonEmpty(value.rollbackTrigger)) throw new UserError(`${label2} requires rollback mechanism and trigger`);
8829
+ if (!isRecord3(value.measurement)) throw new UserError(`${label2}.measurement must be an object`);
8830
+ assertKeys(value.measurement, ["evidenceClass", "baseline", "method", "observationWindow", "stoppingRule", "guardrails"], `${label2}.measurement`);
8831
+ assertRequired(value.measurement, ["evidenceClass", "baseline", "method", "observationWindow", "stoppingRule", "guardrails"], `${label2}.measurement`);
8832
+ if (!["controlled", "natural", "observational"].includes(String(value.measurement.evidenceClass))) {
8833
+ throw new UserError(`${label2}.measurement.evidenceClass is unsupported`);
8834
+ }
8835
+ for (const key of ["baseline", "method", "observationWindow", "stoppingRule"]) {
8836
+ if (!nonEmpty(value.measurement[key])) throw new UserError(`${label2}.measurement.${key} must be non-empty`);
8837
+ }
8838
+ assertStringArray(value.measurement.guardrails, `${label2}.measurement.guardrails`);
8839
+ if (!Array.isArray(value.workItems) || value.workItems.length === 0) throw new UserError(`${label2}.workItems must not be empty`);
8840
+ const workItems = value.workItems.map((item, itemIndex) => {
8841
+ const itemLabel = `${label2}.workItems[${itemIndex}]`;
8842
+ if (!isRecord3(item)) throw new UserError(`${itemLabel} must be an object`);
8843
+ assertKeys(item, ["workItemId", "intent", "summary", "acceptanceCriteria", "dependsOn"], itemLabel);
8844
+ assertRequired(item, ["workItemId", "intent", "summary", "acceptanceCriteria", "dependsOn"], itemLabel);
8845
+ if (!nonEmpty(item.workItemId) || !SAFE_WORK_ITEM_ID.test(item.workItemId)) throw new UserError(`${itemLabel}.workItemId is invalid`);
8846
+ if (!["implementation", "measurement", "research", "prerequisite"].includes(String(item.intent)) || !nonEmpty(item.summary)) {
8847
+ throw new UserError(`${itemLabel} has an invalid intent or summary`);
8848
+ }
8849
+ assertStringArray(item.acceptanceCriteria, `${itemLabel}.acceptanceCriteria`, 1);
8850
+ assertStringArray(item.dependsOn, `${itemLabel}.dependsOn`);
8851
+ return item;
8852
+ });
8853
+ const workIds = new Set(workItems.map((item) => item.workItemId));
8854
+ if (workIds.size !== workItems.length) throw new UserError(`${label2}.workItems contains duplicate ids`);
8855
+ const primaryIntent = selectedVerb === "WATCH" ? "research" : "implementation";
8856
+ const primaryIds = workItems.filter((item) => item.intent === primaryIntent).map((item) => item.workItemId);
8857
+ const measurements = workItems.filter((item) => item.intent === "measurement");
8858
+ if (primaryIds.length === 0 || measurements.length === 0 || measurements.some((item) => !item.dependsOn.some((id) => primaryIds.includes(id)))) {
8859
+ throw new UserError(`${label2} requires ${primaryIntent} work plus dependent measurement work`);
8860
+ }
8861
+ const parsed = value;
8862
+ assertExportSafe({
8863
+ domain: parsed.domain,
8864
+ outcome: parsed.outcome,
8865
+ deduplicationEvidence: parsed.deduplicationEvidence,
8866
+ proposedChanges: parsed.proposedChanges,
8867
+ provenanceObligations: parsed.provenanceObligations,
8868
+ licenseObligations: parsed.licenseObligations,
8869
+ privacyObligations: parsed.privacyObligations,
8870
+ securityObligations: parsed.securityObligations,
8871
+ dependencies: parsed.dependencies,
8872
+ acceptanceTests: parsed.acceptanceTests,
8873
+ measurement: parsed.measurement,
8874
+ rollbackMechanism: parsed.rollbackMechanism,
8875
+ rollbackTrigger: parsed.rollbackTrigger,
8876
+ workItems: parsed.workItems,
8877
+ nonGoals: parsed.nonGoals
8878
+ }, study, label2);
8879
+ return parsed;
8880
+ }
8881
+ function parseSubmission(value, study, inventory) {
8882
+ if (!isRecord3(value)) throw new UserError(`insight submission must be an object`);
8883
+ const keys = ["schemaVersion", "studyId", "sourceInventoryHash", "capabilityInventoryHash", "actualAnalysisMode", "insights", "domainPlans"];
8884
+ assertKeys(value, keys, "insight submission");
8885
+ assertRequired(value, keys, "insight submission");
8886
+ if (value.schemaVersion !== INSIGHT_SCHEMA_VERSION) throw new UserError(`insight submission has an unsupported schemaVersion`);
8887
+ if (!nonEmpty(value.studyId) || !STUDY_ID2.test(value.studyId) || value.studyId !== study.studyId) {
8888
+ throw new UserError(`insight submission studyId does not match the target study`);
8889
+ }
8890
+ if (value.sourceInventoryHash !== study.sourceInventoryHash) throw new UserError(`source inventory changed; re-run analysis`);
8891
+ if (value.capabilityInventoryHash !== inventory.capabilityInventoryHash) throw new UserError(`capability inventory changed; re-run mapping`);
8892
+ if (!ANALYSIS_MODES.includes(value.actualAnalysisMode)) throw new UserError(`actualAnalysisMode is unsupported`);
8893
+ if (!Array.isArray(value.insights) || !Array.isArray(value.domainPlans)) throw new UserError(`insights and domainPlans must be arrays`);
8894
+ const insights = value.insights.map((insight2, index) => parseInsight(insight2, index, study, inventory));
8895
+ const insightIds = new Set(insights.map((insight2) => insight2.insightId));
8896
+ if (insightIds.size !== insights.length) throw new UserError(`insight submission contains duplicate insight ids`);
8897
+ if (insights.some((insight2) => insight2.independenceMode !== value.actualAnalysisMode)) {
8898
+ throw new UserError(`each insight independenceMode must match actualAnalysisMode`);
8899
+ }
8900
+ const domainPlans = value.domainPlans.map(
8901
+ (plan, index) => parseDomainPlan(plan, index, new Map(insights.map((insight2) => [insight2.insightId, insight2])), inventory, study)
8902
+ );
8903
+ if (new Set(domainPlans.map((plan) => plan.domainId)).size !== domainPlans.length) throw new UserError(`domainPlans contains duplicate ids`);
8904
+ const allWorkItems = domainPlans.flatMap((plan) => plan.workItems);
8905
+ const allWorkItemIds = new Set(allWorkItems.map((item) => item.workItemId));
8906
+ if (allWorkItemIds.size !== allWorkItems.length) throw new UserError(`domainPlans contains duplicate work-item ids`);
8907
+ if (allWorkItems.some((item) => item.dependsOn.some((id) => !allWorkItemIds.has(id)))) {
8908
+ throw new UserError(`domainPlans contains an unknown work-item dependency`);
8909
+ }
8910
+ const classified = domainPlans.flatMap((plan) => [
8911
+ ...plan.acceptedInsightIds,
8912
+ ...plan.contestedInsightIds,
8913
+ ...plan.rejectedInsightIds
8914
+ ]);
8915
+ if (new Set(classified).size !== classified.length || classified.length !== insights.length) {
8916
+ throw new UserError(`every insight must be classified exactly once across domain plans`);
8917
+ }
8918
+ return {
8919
+ schemaVersion: INSIGHT_SCHEMA_VERSION,
8920
+ studyId: study.studyId,
8921
+ sourceInventoryHash: study.sourceInventoryHash,
8922
+ capabilityInventoryHash: inventory.capabilityInventoryHash,
8923
+ actualAnalysisMode: value.actualAnalysisMode,
8924
+ insights,
8925
+ domainPlans
8926
+ };
8927
+ }
8928
+ function insightPlanHash(value) {
8929
+ return stableHash({
8930
+ sourceInventoryHash: value.sourceInventoryHash,
8931
+ capabilityInventoryHash: value.capabilityInventoryHash,
8932
+ actualAnalysisMode: value.actualAnalysisMode,
8933
+ insights: value.insights,
8934
+ domainPlans: value.domainPlans
8935
+ });
8936
+ }
8937
+ function validateCurrentSourceFingerprints(study) {
8938
+ const issues = [];
8939
+ for (const source of study.sources) {
8940
+ if (source.type !== "file" && source.type !== "directory") continue;
8941
+ try {
8942
+ const normalized = normalizeSources([
8943
+ source.type === "file" ? { type: "file", path: source.privateLocator, mediaType: source.mediaType } : { type: "directory", path: source.privateLocator, includeHidden: source.includeHidden }
8944
+ ]);
8945
+ if (normalized.sources[0]?.fingerprint !== source.fingerprint) {
8946
+ issues.push({
8947
+ severity: "blocker",
8948
+ code: "source-drift",
8949
+ message: `Local source ${source.sourceId} changed after analysis.`
8950
+ });
8951
+ }
8952
+ } catch (error) {
8953
+ issues.push({
8954
+ severity: "blocker",
8955
+ code: "source-unavailable",
8956
+ message: `Local source ${source.sourceId} cannot be revalidated: ${error.message}`
8957
+ });
8958
+ }
8959
+ }
8960
+ return issues;
8961
+ }
8962
+ function validateInsightSubmission(study, value, inventory) {
8963
+ if (["approved", "jira-manifested", "dismissed", "archived"].includes(study.status)) {
8964
+ throw new UserError(`study ${study.studyId} is ${study.status} and cannot accept an analysis submission`);
8965
+ }
8966
+ const submission = parseSubmission(value, study, inventory);
8967
+ const next = {
8968
+ ...study,
8969
+ status: "review-ready",
8970
+ actualAnalysisMode: submission.actualAnalysisMode,
8971
+ capabilityInventoryHash: submission.capabilityInventoryHash,
8972
+ insights: submission.insights,
8973
+ domainPlans: submission.domainPlans,
8974
+ approval: void 0,
8975
+ jira: void 0
8976
+ };
8977
+ next.planHash = insightPlanHash(next);
8978
+ return next;
8979
+ }
8980
+ function validateStudyForReview(study, inventory) {
8981
+ const blockers = [];
8982
+ const warnings = [];
8983
+ const planHash = study.capabilityInventoryHash && study.actualAnalysisMode ? insightPlanHash(study) : stableHash({ studyId: study.studyId, domainPlans: study.domainPlans });
8984
+ if (!study.actualAnalysisMode) blockers.push({ severity: "blocker", code: "analysis-mode-missing", message: "No actual analysis mode is recorded." });
8985
+ if (!study.capabilityInventoryHash) blockers.push({ severity: "blocker", code: "inventory-missing", message: "No capability inventory hash is recorded." });
8986
+ if (study.planHash !== planHash) blockers.push({ severity: "blocker", code: "plan-hash-stale", message: "The stored plan hash does not match current analysis and plans." });
8987
+ if (study.insights.length === 0) blockers.push({ severity: "blocker", code: "insights-empty", message: "No validated insights were submitted." });
8988
+ if (study.domainPlans.length === 0) blockers.push({ severity: "blocker", code: "plans-empty", message: "No domain plans were submitted." });
8989
+ blockers.push(...validateCurrentSourceFingerprints(study));
8990
+ if (inventory && study.capabilityInventoryHash !== inventory.capabilityInventoryHash) {
8991
+ blockers.push({ severity: "blocker", code: "inventory-drift", message: "The capability inventory changed after analysis." });
8992
+ }
8993
+ if (inventory && inventory.coverage !== "complete") {
8994
+ warnings.push({ severity: "warning", code: "inventory-incomplete", message: inventory.coverageReason ?? "Capability inventory coverage is incomplete." });
8995
+ }
8996
+ if (inventory && study.actualAnalysisMode && study.capabilityInventoryHash) {
8997
+ try {
8998
+ parseSubmission({
8999
+ schemaVersion: INSIGHT_SCHEMA_VERSION,
9000
+ studyId: study.studyId,
9001
+ sourceInventoryHash: study.sourceInventoryHash,
9002
+ capabilityInventoryHash: study.capabilityInventoryHash,
9003
+ actualAnalysisMode: study.actualAnalysisMode,
9004
+ insights: study.insights,
9005
+ domainPlans: study.domainPlans
9006
+ }, study, inventory);
9007
+ } catch (error) {
9008
+ blockers.push({
9009
+ severity: "blocker",
9010
+ code: "submission-invalid",
9011
+ message: error.message
9012
+ });
9013
+ }
9014
+ }
9015
+ if (study.requestedAnalysisMode === "independent" && study.actualAnalysisMode === "single-context") {
9016
+ warnings.push({ severity: "warning", code: "independence-degraded", message: "Independent analysis was requested but only a single context was recorded." });
9017
+ }
9018
+ for (const insight2 of study.insights) {
9019
+ if (insight2.validation === "contested") warnings.push({ severity: "warning", code: "contested", message: "Contradicting evidence remains visible.", insightId: insight2.insightId });
9020
+ if (insight2.validation === "single-source") warnings.push({ severity: "warning", code: "single-source", message: "This insight has only single-source support.", insightId: insight2.insightId });
9021
+ if (insight2.inventoryCoverage !== "complete") warnings.push({ severity: "warning", code: "insight-coverage-incomplete", message: insight2.inventoryCoverageReason ?? "Insight mapping coverage is incomplete.", insightId: insight2.insightId });
9022
+ }
9023
+ return { valid: blockers.length === 0, blockers, warnings, planHash };
9024
+ }
9025
+ function assertCurrentApproval(study) {
9026
+ if (study.status !== "approved" && study.status !== "jira-manifested" || !study.approval || !study.planHash) {
9027
+ throw new UserError(`study ${study.studyId} does not have a current human approval`);
9028
+ }
9029
+ const expectedPlanHash = insightPlanHash(study);
9030
+ if (study.planHash !== expectedPlanHash || study.approval.planHash !== expectedPlanHash) {
9031
+ throw new UserError(`study ${study.studyId} approval is stale because the plan hash changed`);
9032
+ }
9033
+ if (sourceInventoryHashFor(study.sources) !== study.sourceInventoryHash) {
9034
+ throw new UserError(`study ${study.studyId} approval is stale because the source policy changed`);
9035
+ }
9036
+ if (study.approval.sourceInventoryHash !== study.sourceInventoryHash) {
9037
+ throw new UserError(`study ${study.studyId} approval is stale because the source inventory changed`);
9038
+ }
9039
+ if (!study.capabilityInventoryHash || study.approval.capabilityInventoryHash !== study.capabilityInventoryHash) {
9040
+ throw new UserError(`study ${study.studyId} approval is stale because the capability inventory changed`);
9041
+ }
9042
+ const domainIds = new Set(study.domainPlans.map((plan) => plan.domainId));
9043
+ if (study.approval.approvedDomainIds.length === 0 || study.approval.approvedDomainIds.some((id) => !domainIds.has(id))) {
9044
+ throw new UserError(`study ${study.studyId} approval names an unknown domain plan`);
9045
+ }
9046
+ const sourceIssues = validateCurrentSourceFingerprints(study);
9047
+ if (sourceIssues.length > 0) throw new UserError(sourceIssues[0].message);
9048
+ return study.approval;
9049
+ }
9050
+
9051
+ // src/insights/jira.ts
9052
+ var PROJECT_KEY = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
9053
+ var RECEIPT_EVENT_ID = /^JIRA-EVT-[0-9a-f]{16}$/;
9054
+ var RECEIPT_KINDS = ["attempted", "found", "created", "failed"];
9055
+ var SAFE_MARKER_PART = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
9056
+ var ISSUE_KEY = /^[A-Za-z][A-Za-z0-9_-]*-[1-9][0-9]*$/;
9057
+ var CONTROL = /[\u0000-\u001f\u007f-\u009f]/u;
9058
+ function nonEmpty2(value) {
9059
+ return typeof value === "string" && value.trim().length > 0;
9060
+ }
9061
+ function isRecord4(value) {
9062
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9063
+ }
9064
+ function safeText(value, max = 2e3) {
9065
+ const normalized = value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu, "").replace(/\s+/gu, " ").trim();
9066
+ return normalized.length <= max ? normalized : `${normalized.slice(0, max)}\u2026`;
9067
+ }
9068
+ function label(value) {
9069
+ return value.toLowerCase().replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 64) || "unclassified";
9070
+ }
9071
+ function markerFor(studyId, workItemId) {
9072
+ if (!SAFE_MARKER_PART.test(workItemId)) throw new UserError(`invalid Jira work-item id for marker: ${workItemId}`);
9073
+ return `skill-forge-study:${studyId}:${workItemId}`;
9074
+ }
9075
+ function approvedPlans(study) {
9076
+ const approval = assertCurrentApproval(study);
9077
+ const approvedIds = new Set(approval.approvedDomainIds);
9078
+ return study.domainPlans.filter((plan) => approvedIds.has(plan.domainId));
9079
+ }
9080
+ function sourceIdsFor(plan, study) {
9081
+ const insightIds = /* @__PURE__ */ new Set([
9082
+ ...plan.acceptedInsightIds,
9083
+ ...plan.contestedInsightIds,
9084
+ ...plan.rejectedInsightIds
9085
+ ]);
9086
+ return [...new Set(
9087
+ study.insights.filter((insight2) => insightIds.has(insight2.insightId)).flatMap((insight2) => insight2.sourceIds)
9088
+ )].sort();
9089
+ }
9090
+ function approvedPublicUrl(value) {
9091
+ if (!value) return void 0;
9092
+ try {
9093
+ return canonicalPublicUrl(value, "source public locator");
9094
+ } catch {
9095
+ return void 0;
9096
+ }
9097
+ }
9098
+ function implementationRoute(plan) {
9099
+ switch (plan.selectedVerb) {
9100
+ case "ABSORB":
9101
+ return "Use the existing refinement or targeted plugin-change path; preview first and preserve rollback.";
9102
+ case "DEFER":
9103
+ return "Keep the maintained source/capability authoritative; add only a thin wrapper or reference when approved.";
9104
+ case "FORK":
9105
+ return "Gate and deduplicate the proposed custom capability before any promotion or repository implementation.";
9106
+ case "REJECT":
9107
+ return "No capability mutation; document why the existing behavior is sufficient.";
9108
+ case "WATCH":
9109
+ return "Research and measure only; do not implement a capability until evidence and fit are ready.";
9110
+ }
9111
+ }
9112
+ function measurementText(plan) {
9113
+ const measurement = plan.measurement;
9114
+ return safeText(
9115
+ `${measurement.evidenceClass}: baseline ${measurement.baseline}; method ${measurement.method}; window ${measurement.observationWindow}; stopping rule ${measurement.stoppingRule}`
9116
+ );
9117
+ }
9118
+ function workItemFor(plan, workItem, study) {
9119
+ const insightIds = [.../* @__PURE__ */ new Set([
9120
+ ...plan.acceptedInsightIds,
9121
+ ...plan.contestedInsightIds,
9122
+ ...plan.rejectedInsightIds
9123
+ ])].sort();
9124
+ return {
9125
+ workItemId: workItem.workItemId,
9126
+ marker: markerFor(study.studyId, workItem.workItemId),
9127
+ issueTypeIntent: workItem.intent,
9128
+ summary: safeText(workItem.summary, 255),
9129
+ context: safeText(`${plan.outcome} Domain plan: ${plan.domainId}. Evidence is referenced by stable source/insight ids only.`),
9130
+ requirements: plan.proposedChanges.map((entry) => safeText(entry)),
9131
+ acceptanceCriteria: workItem.acceptanceCriteria.map((entry) => safeText(entry)),
9132
+ dependencies: workItem.dependsOn.map((entry) => markerFor(study.studyId, entry)),
9133
+ implementationRoute: implementationRoute(plan),
9134
+ measurement: measurementText(plan),
9135
+ rollback: safeText(`${plan.rollbackMechanism} Trigger: ${plan.rollbackTrigger}`),
9136
+ labels: ["skill-forge", "source-insight", `domain-${label(plan.domain)}`, `intent-${label(workItem.intent)}`],
9137
+ domainIds: [plan.domainId],
9138
+ insightIds,
9139
+ sourceIds: sourceIdsFor(plan, study)
9140
+ };
9141
+ }
9142
+ function buildJiraManifest(study, projectKey, options = {}) {
9143
+ if (!PROJECT_KEY.test(projectKey)) throw new UserError(`invalid Jira project key: ${projectKey}`);
9144
+ const plans = approvedPlans(study);
9145
+ if (plans.length === 0) throw new UserError(`the current approval contains no domain plans`);
9146
+ const workItems = plans.flatMap((plan) => plan.workItems.map((item) => workItemFor(plan, item, study)));
9147
+ const ids = new Set(workItems.map((item) => item.workItemId));
9148
+ if (ids.size !== workItems.length) throw new UserError(`approved plans contain duplicate Jira work-item ids`);
9149
+ if (workItems.some((item) => item.dependencies.some((dependency) => !ids.has(dependency.split(":").at(-1))))) {
9150
+ throw new UserError(`approved plans depend on a work item outside the approved Jira manifest`);
9151
+ }
9152
+ const epicId = stableId("EPIC", { studyId: study.studyId, planHash: study.planHash });
9153
+ const approvedSourceIds = new Set(plans.flatMap((plan) => sourceIdsFor(plan, study)));
9154
+ const approvedSourceLinks = [...new Set(
9155
+ study.sources.filter((source) => approvedSourceIds.has(source.sourceId) && source.accessClass === "public").map((source) => approvedPublicUrl(source.publicLocator)).filter((locator) => locator !== void 0)
9156
+ )].sort();
9157
+ const manifest = {
9158
+ schemaVersion: INSIGHT_SCHEMA_VERSION,
9159
+ generatedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
9160
+ studyId: study.studyId,
9161
+ approvedPlanHash: study.planHash,
9162
+ projectKey: projectKey.toUpperCase(),
9163
+ backlogMode: "epic",
9164
+ epic: {
9165
+ workItemId: epicId,
9166
+ marker: markerFor(study.studyId, epicId),
9167
+ summary: safeText(`Source insight improvements \u2014 ${study.studyId}`, 255),
9168
+ overview: "Implement only the human-approved source-insight domain plans. Source material remains untrusted data and private locators are excluded.",
9169
+ objectives: plans.map((plan) => safeText(plan.outcome)),
9170
+ scope: plans.map((plan) => `${plan.domainId}: ${safeText(plan.domain)}`),
9171
+ successCriteria: plans.flatMap((plan) => plan.acceptanceTests.map((entry) => safeText(entry))),
9172
+ approvedSourceLinks,
9173
+ labels: ["skill-forge", "source-insight", `study-${label(study.studyId)}`]
9174
+ },
9175
+ workItems
9176
+ };
9177
+ assertExportSafe(manifest, study, "Jira manifest");
9178
+ return manifest;
9179
+ }
9180
+ function getJiraReceiptsPath(studyId) {
9181
+ return join31(getStudyDir(studyId), "jira-receipts.jsonl");
9182
+ }
9183
+ function assertCurrentJiraManifest(study, value) {
9184
+ if (study.status !== "jira-manifested" || !study.jira) {
9185
+ throw new UserError(`study ${study.studyId} does not have a current Jira manifest`);
9186
+ }
9187
+ if (!isRecord4(value) || !nonEmpty2(value.projectKey) || !nonEmpty2(value.generatedAt)) {
9188
+ throw new UserError(`stored Jira manifest for study ${study.studyId} is malformed`);
9189
+ }
9190
+ const generatedAt = new Date(value.generatedAt);
9191
+ if (Number.isNaN(generatedAt.getTime())) {
9192
+ throw new UserError(`stored Jira manifest for study ${study.studyId} has an invalid generatedAt`);
9193
+ }
9194
+ const expected = buildJiraManifest(study, value.projectKey, { now: generatedAt });
9195
+ const manifestHash = stableHash(value);
9196
+ if (stableHash(expected) !== manifestHash || study.jira.manifestHash !== manifestHash || study.jira.projectKey.toUpperCase() !== expected.projectKey || study.jira.generatedAt !== expected.generatedAt) {
9197
+ throw new UserError(`stored Jira manifest for study ${study.studyId} does not match the current approved plan`);
9198
+ }
9199
+ return expected;
9200
+ }
9201
+ function manifestMarkers(manifest) {
9202
+ return /* @__PURE__ */ new Set([manifest.epic.marker, ...manifest.workItems.map((item) => item.marker)]);
9203
+ }
9204
+ function assertManifestMarker(marker, manifest, label2) {
9205
+ if (!manifestMarkers(manifest).has(marker)) {
9206
+ throw new UserError(`${label2}: marker is not present in the current approved Jira manifest`);
9207
+ }
9208
+ }
9209
+ function assertResultProjectKey(event, manifest, label2) {
9210
+ if (event.kind !== "created" && event.kind !== "found") return;
9211
+ const expectedPrefix = `${manifest.projectKey}-`;
9212
+ const issueKey = event.issueKey;
9213
+ if (!nonEmpty2(issueKey) || !issueKey.startsWith(expectedPrefix) || !/^[1-9][0-9]*$/.test(issueKey.slice(expectedPrefix.length))) {
9214
+ throw new UserError(`${label2}: issueKey does not match Jira project ${manifest.projectKey}`);
9215
+ }
9216
+ }
9217
+ function parseJiraReceiptInput(value) {
9218
+ if (!isRecord4(value)) throw new UserError(`Jira receipt import must be an object`);
9219
+ const allowed = /* @__PURE__ */ new Set(["kind", "marker", "occurredAt", "issueKey", "issueUrl", "error"]);
9220
+ const extra = Object.keys(value).filter((key) => !allowed.has(key));
9221
+ if (extra.length > 0) throw new UserError(`Jira receipt import contains unsupported field(s): ${extra.join(", ")}`);
9222
+ if (!RECEIPT_KINDS.includes(value.kind) || !nonEmpty2(value.marker)) {
9223
+ throw new UserError(`Jira receipt import requires a supported kind and non-empty marker`);
9224
+ }
9225
+ if (value.occurredAt !== void 0 && (!nonEmpty2(value.occurredAt) || Number.isNaN(Date.parse(value.occurredAt)))) {
9226
+ throw new UserError(`Jira receipt import occurredAt must be an ISO-8601 timestamp`);
9227
+ }
9228
+ for (const key of ["issueKey", "issueUrl", "error"]) {
9229
+ if (value[key] !== void 0 && typeof value[key] !== "string") {
9230
+ throw new UserError(`Jira receipt import ${key} must be a string`);
9231
+ }
9232
+ }
9233
+ return value;
9234
+ }
9235
+ function assertReceiptShape(value, studyId, manifest, line) {
9236
+ const where = line ? ` line ${line}` : "";
9237
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new UserError(`malformed Jira receipts${where}: expected an object`);
9238
+ const event = value;
9239
+ const allowed = /* @__PURE__ */ new Set(["eventId", "studyId", "kind", "marker", "occurredAt", "issueKey", "issueUrl", "error"]);
9240
+ const extra = Object.keys(event).filter((key) => !allowed.has(key));
9241
+ if (extra.length > 0) throw new UserError(`malformed Jira receipts${where}: unsupported fields ${extra.join(", ")}`);
9242
+ if (!nonEmpty2(event.eventId) || !RECEIPT_EVENT_ID.test(event.eventId) || event.studyId !== studyId) {
9243
+ throw new UserError(`malformed Jira receipts${where}: invalid event identity`);
9244
+ }
9245
+ if (!RECEIPT_KINDS.includes(event.kind) || !nonEmpty2(event.marker)) {
9246
+ throw new UserError(`malformed Jira receipts${where}: invalid kind or marker`);
9247
+ }
9248
+ const markerPrefix = `skill-forge-study:${studyId}:`;
9249
+ if (!event.marker.startsWith(markerPrefix) || !SAFE_MARKER_PART.test(event.marker.slice(markerPrefix.length)) || CONTROL.test(event.marker)) {
9250
+ throw new UserError(`malformed Jira receipts${where}: marker does not belong to the study`);
9251
+ }
9252
+ assertManifestMarker(event.marker, manifest, `malformed Jira receipts${where}`);
9253
+ if (!nonEmpty2(event.occurredAt) || Number.isNaN(Date.parse(event.occurredAt))) {
9254
+ throw new UserError(`malformed Jira receipts${where}: occurredAt is invalid`);
9255
+ }
9256
+ if ((event.kind === "created" || event.kind === "found") && (!nonEmpty2(event.issueKey) || !nonEmpty2(event.issueUrl))) {
9257
+ throw new UserError(`malformed Jira receipts${where}: ${event.kind} requires issueKey and issueUrl`);
9258
+ }
9259
+ if (event.issueKey !== void 0 && !ISSUE_KEY.test(String(event.issueKey))) {
9260
+ throw new UserError(`malformed Jira receipts${where}: issueKey is invalid`);
9261
+ }
9262
+ assertResultProjectKey(event, manifest, `malformed Jira receipts${where}`);
9263
+ if (event.issueUrl !== void 0) {
9264
+ try {
9265
+ if (canonicalPublicUrl(String(event.issueUrl), "Jira issue URL") !== event.issueUrl) {
9266
+ throw new Error("non-canonical URL");
9267
+ }
9268
+ } catch {
9269
+ throw new UserError(`malformed Jira receipts${where}: issueUrl violates the public URL policy`);
9270
+ }
9271
+ }
9272
+ if (event.kind === "failed" && !nonEmpty2(event.error)) throw new UserError(`malformed Jira receipts${where}: failed requires error`);
9273
+ for (const key of ["issueKey", "issueUrl", "error"]) {
9274
+ if (event[key] !== void 0 && (typeof event[key] !== "string" || CONTROL.test(event[key]))) {
9275
+ throw new UserError(`malformed Jira receipts${where}: ${key} is invalid`);
9276
+ }
9277
+ }
9278
+ const expectedEventId = stableId("JIRA-EVT", {
9279
+ studyId,
9280
+ kind: event.kind,
9281
+ marker: event.marker,
9282
+ occurredAt: event.occurredAt,
9283
+ issueKey: event.issueKey,
9284
+ issueUrl: event.issueUrl,
9285
+ error: event.error
9286
+ });
9287
+ if (event.eventId !== expectedEventId) throw new UserError(`malformed Jira receipts${where}: eventId does not match event contents`);
9288
+ }
9289
+ function appendJiraReceipt(studyId, manifest, input) {
9290
+ const currentManifest = assertCurrentJiraManifest(readInsightStudy(studyId), manifest);
9291
+ const occurredAt = input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString();
9292
+ const identity = {
9293
+ studyId,
9294
+ kind: input.kind,
9295
+ marker: input.marker,
9296
+ occurredAt,
9297
+ issueKey: input.issueKey,
9298
+ issueUrl: input.issueUrl,
9299
+ error: input.error
9300
+ };
9301
+ const event = {
9302
+ eventId: stableId("JIRA-EVT", identity),
9303
+ ...identity
9304
+ };
9305
+ assertReceiptShape(event, studyId, currentManifest);
9306
+ const path = getJiraReceiptsPath(studyId);
9307
+ appendPrivateJsonLine(path, event);
9308
+ return event;
9309
+ }
9310
+ function readJiraReceipts(studyId, manifest) {
9311
+ const currentManifest = assertCurrentJiraManifest(readInsightStudy(studyId), manifest);
9312
+ const path = getJiraReceiptsPath(studyId);
9313
+ if (!existsSync26(path)) return [];
9314
+ let text;
9315
+ try {
9316
+ text = readFileSync21(path, "utf-8");
9317
+ } catch (error) {
9318
+ throw new UserError(`unable to read Jira receipts at ${path}: ${error.message}`);
9319
+ }
9320
+ return text.split("\n").filter(Boolean).map((line, index) => {
9321
+ let value;
9322
+ try {
9323
+ value = JSON.parse(line);
9324
+ } catch {
9325
+ throw new UserError(`malformed Jira receipts line ${index + 1}: not valid JSON`);
9326
+ }
9327
+ assertReceiptShape(value, studyId, currentManifest, index + 1);
9328
+ return value;
9329
+ });
9330
+ }
9331
+ function resumeItem(marker, workItemId, kind, receipts) {
9332
+ const events = receipts.filter((event) => event.marker === marker);
9333
+ const completed = [...events].reverse().find((event) => event.kind === "created" || event.kind === "found");
9334
+ if (completed) {
9335
+ return { marker, workItemId, kind, state: "created", issueKey: completed.issueKey, issueUrl: completed.issueUrl };
9336
+ }
9337
+ const last = events.at(-1);
9338
+ if (last) return { marker, workItemId, kind, state: "search-required", lastError: last.error };
9339
+ return { marker, workItemId, kind, state: "create" };
9340
+ }
9341
+ function buildJiraResumeState(manifest, receipts) {
9342
+ for (const receipt of receipts) {
9343
+ if (receipt.studyId !== manifest.studyId) {
9344
+ throw new UserError(`Jira receipt ${receipt.eventId} belongs to another study`);
9345
+ }
9346
+ assertManifestMarker(receipt.marker, manifest, `Jira receipt ${receipt.eventId}`);
9347
+ assertResultProjectKey(receipt, manifest, `Jira receipt ${receipt.eventId}`);
9348
+ }
9349
+ const epic = resumeItem(manifest.epic.marker, manifest.epic.workItemId, "epic", receipts);
9350
+ const workItems = manifest.workItems.map((item) => resumeItem(item.marker, item.workItemId, "work-item", receipts));
9351
+ const all = [epic, ...workItems];
9352
+ const complete = all.every((item) => item.state === "created");
9353
+ const nextAction = complete ? "complete" : all.some((item) => item.state === "search-required") ? "search-by-marker" : epic.state !== "created" ? "create-epic" : "create-work-item";
9354
+ return { epic, workItems, complete, nextAction };
9355
+ }
9356
+
9357
+ // src/insights/render.ts
9358
+ var ANSI_ESCAPE = /\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/gu;
9359
+ var UNSAFE_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
9360
+ var MAX_RENDERED_TEXT = 4e3;
9361
+ function sanitizeRenderedText(value) {
9362
+ const text = String(value ?? "").replace(ANSI_ESCAPE, "").replace(UNSAFE_CONTROL, "").replace(/\r\n?|\u2028|\u2029/gu, "\n");
9363
+ return text.length <= MAX_RENDERED_TEXT ? text : `${text.slice(0, MAX_RENDERED_TEXT)}\u2026`;
9364
+ }
9365
+ function oneLine(value) {
9366
+ return sanitizeRenderedText(value).replace(/\s+/gu, " ").trim();
9367
+ }
9368
+ function bullets(values, empty = "none") {
9369
+ return values.length > 0 ? values.map((value) => ` - ${oneLine(value)}`) : [` - ${empty}`];
9370
+ }
9371
+ function renderDomainPlan(plan) {
9372
+ const lines = [
9373
+ `${oneLine(plan.domain)} (${oneLine(plan.domainId)})`,
9374
+ ` Outcome: ${oneLine(plan.outcome)}`,
9375
+ ` Forge verb: ${oneLine(plan.selectedVerb)}`,
9376
+ ` Accepted insights: ${plan.acceptedInsightIds.map(oneLine).join(", ") || "none"}`,
9377
+ ` Contested insights: ${plan.contestedInsightIds.map(oneLine).join(", ") || "none"}`,
9378
+ ` Rejected insights: ${plan.rejectedInsightIds.map(oneLine).join(", ") || "none"}`,
9379
+ ` Targets: ${plan.targetCapabilityIds.length} private capability id(s)`,
9380
+ " Proposed changes:",
9381
+ ...bullets(plan.proposedChanges),
9382
+ " Acceptance tests:",
9383
+ ...bullets(plan.acceptanceTests),
9384
+ ` Measurement: ${oneLine(plan.measurement.evidenceClass)} \u2014 ${oneLine(plan.measurement.method)}`,
9385
+ ` Stopping rule: ${oneLine(plan.measurement.stoppingRule)}`,
9386
+ ` Rollback: ${oneLine(plan.rollbackMechanism)} (trigger: ${oneLine(plan.rollbackTrigger)})`,
9387
+ " Non-goals:",
9388
+ ...bullets(plan.nonGoals)
9389
+ ];
9390
+ return lines.join("\n");
9391
+ }
9392
+ function renderInsightReview(study, inventory, validation = validateStudyForReview(study, inventory)) {
9393
+ const lines = [
9394
+ `Insight study ${oneLine(study.studyId)}`,
9395
+ `Status: ${oneLine(study.status)}`,
9396
+ `Plan hash: ${oneLine(validation.planHash)}`,
9397
+ `Analysis: requested ${oneLine(study.requestedAnalysisMode)}; actual ${oneLine(study.actualAnalysisMode ?? "not submitted")}`,
9398
+ `Inventory: ${inventory ? `${oneLine(inventory.coverage)} (${inventory.installedCapabilityCount} installed, ${inventory.mapCapabilityCount} mapped)` : "not refreshed"}`,
9399
+ `Review gate: ${validation.valid ? "READY FOR HUMAN APPROVAL" : "BLOCKED"}`
9400
+ ];
9401
+ if (validation.blockers.length > 0) {
9402
+ lines.push("Blockers:", ...validation.blockers.map((issue) => ` - [${oneLine(issue.code)}] ${oneLine(issue.message)}`));
9403
+ }
9404
+ if (validation.warnings.length > 0) {
9405
+ lines.push("Warnings:", ...validation.warnings.map((issue) => ` - [${oneLine(issue.code)}] ${oneLine(issue.message)}`));
9406
+ }
9407
+ lines.push("Sources:");
9408
+ for (const source of study.sources) {
9409
+ const publicLink = source.accessClass === "public" && source.publicLocator ? ` \u2014 ${oneLine(source.publicLocator)}` : "";
9410
+ lines.push(` - ${oneLine(source.sourceId)}: ${oneLine(source.title)}${publicLink}`);
9411
+ if (source.suspiciousInstructionFlags.length > 0) {
9412
+ lines.push(` untrusted-instruction flags: ${source.suspiciousInstructionFlags.map(oneLine).join(", ")}`);
9413
+ }
9414
+ }
9415
+ lines.push("Insights:");
9416
+ for (const insight2 of study.insights) {
9417
+ lines.push(
9418
+ ` - ${oneLine(insight2.insightId)} [${oneLine(insight2.validation)}/${oneLine(insight2.confidence)}] ${oneLine(insight2.claim)}`
9419
+ );
9420
+ }
9421
+ lines.push("Domain plans:");
9422
+ for (const plan of study.domainPlans) lines.push(renderDomainPlan(plan));
9423
+ lines.push(
9424
+ "",
9425
+ "Approval is human-only. An agent must stop here and ask the user to run `skill-forge insight approve` in an interactive terminal."
9426
+ );
9427
+ return lines.join("\n");
9428
+ }
9429
+
9430
+ // src/commands/insight.ts
9431
+ var PRO_FEATURE_LABEL8 = "source insight planning";
9432
+ var WORKFLOW_ASSET = "workflows/source-insight-planning/SKILL.md";
9433
+ var CONTEXT_FILENAME = "command-context.json";
9434
+ var INVENTORY_FILENAME = "inventory.json";
9435
+ var JIRA_MANIFEST_FILENAME = "jira-manifest.json";
9436
+ function requireProFeature2() {
9437
+ const status2 = proFeatureStatus(PRO_FEATURE_LABEL8);
9438
+ if (!status2.enabled) {
9439
+ throw new UserError(
9440
+ status2.notice ?? "insight is a Pro feature \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock."
9441
+ );
9442
+ }
9443
+ if (status2.notice) console.error(`[skill-forge] ${status2.notice}`);
9444
+ }
9445
+ function isRecord5(value) {
9446
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9447
+ }
9448
+ function nonEmptyString(value, label2) {
9449
+ if (typeof value !== "string" || value.trim().length === 0) {
9450
+ throw new UserError(`${label2} must be a non-empty string`);
9451
+ }
9452
+ return value;
9453
+ }
9454
+ function parseJsonFile(path, label2) {
9455
+ const resolved = resolvePath10(path);
9456
+ let raw;
9457
+ try {
9458
+ raw = readFileSync22(resolved, "utf-8");
9459
+ } catch (error) {
9460
+ throw new UserError(`unable to read ${label2} at ${resolved}: ${error.message}`);
9461
+ }
9462
+ try {
9463
+ return JSON.parse(raw);
9464
+ } catch {
9465
+ throw new UserError(`${label2} at ${resolved} is not valid JSON`);
9466
+ }
9467
+ }
9468
+ var COMMON_DESCRIPTOR_FIELDS = /* @__PURE__ */ new Set([
9469
+ "type",
9470
+ "title",
9471
+ "accessClass",
9472
+ "licenseName",
9473
+ "licenseClass",
9474
+ "author",
9475
+ "publisher",
9476
+ "publicationDate",
9477
+ "revisionDate",
9478
+ "revision",
9479
+ "etag",
9480
+ "mediaType"
9481
+ ]);
9482
+ var COMMON_STRING_DESCRIPTOR_FIELDS = [
9483
+ "title",
9484
+ "licenseName",
9485
+ "author",
9486
+ "publisher",
9487
+ "publicationDate",
9488
+ "revisionDate",
9489
+ "revision",
9490
+ "etag",
9491
+ "mediaType"
9492
+ ];
9493
+ function assertDescriptorKeys(value, specific, label2) {
9494
+ const allowed = /* @__PURE__ */ new Set([...COMMON_DESCRIPTOR_FIELDS, ...specific]);
9495
+ const extra = Object.keys(value).filter((key) => !allowed.has(key));
9496
+ if (extra.length > 0) throw new UserError(`${label2} contains unsupported field(s): ${extra.join(", ")}`);
9497
+ }
9498
+ function assertCommonDescriptorFields(value, label2) {
9499
+ for (const field of COMMON_STRING_DESCRIPTOR_FIELDS) {
9500
+ if (value[field] !== void 0 && typeof value[field] !== "string") {
9501
+ throw new UserError(`${label2}.${field} must be a string`);
9502
+ }
9503
+ }
9504
+ if (value.accessClass !== void 0 && !SOURCE_ACCESS_CLASSES.includes(value.accessClass)) {
9505
+ throw new UserError(`${label2}.accessClass is unsupported`);
9506
+ }
9507
+ if (value.licenseClass !== void 0 && !SOURCE_LICENSE_CLASSES.includes(value.licenseClass)) {
9508
+ throw new UserError(`${label2}.licenseClass is unsupported`);
9509
+ }
9510
+ }
9511
+ function pathRelativeTo(baseDir, raw, label2) {
9512
+ const path = nonEmptyString(raw, label2);
9513
+ return isAbsolute6(path) ? path : resolvePath10(baseDir, path);
9514
+ }
9515
+ function parseManifestDescriptor(value, index, baseDir) {
9516
+ const label2 = `source manifest entry ${index + 1}`;
9517
+ if (!isRecord5(value)) throw new UserError(`${label2} must be an object`);
9518
+ const type = nonEmptyString(value.type, `${label2}.type`);
9519
+ assertCommonDescriptorFields(value, label2);
9520
+ if (type === "url") {
9521
+ assertDescriptorKeys(value, ["url", "observedSha256", "observedByteLength"], label2);
9522
+ return { ...value, type, url: nonEmptyString(value.url, `${label2}.url`) };
9523
+ }
9524
+ if (type === "file" || type === "directory") {
9525
+ assertDescriptorKeys(value, type === "directory" ? ["path", "includeHidden"] : ["path"], label2);
9526
+ if (value.includeHidden !== void 0 && typeof value.includeHidden !== "boolean") {
9527
+ throw new UserError(`${label2}.includeHidden must be a boolean`);
9528
+ }
9529
+ return { ...value, type, path: pathRelativeTo(baseDir, value.path, `${label2}.path`) };
9530
+ }
9531
+ if (type === "connector") {
9532
+ assertDescriptorKeys(value, ["provider", "opaqueId", "publicLocator", "observedSha256", "observedByteLength"], label2);
9533
+ if (value.publicLocator !== void 0 && typeof value.publicLocator !== "string") {
9534
+ throw new UserError(`${label2}.publicLocator must be a string`);
9535
+ }
9536
+ return {
9537
+ ...value,
9538
+ type,
9539
+ provider: nonEmptyString(value.provider, `${label2}.provider`),
9540
+ opaqueId: nonEmptyString(value.opaqueId, `${label2}.opaqueId`)
9541
+ };
9542
+ }
9543
+ if (type === "stdin") {
9544
+ assertDescriptorKeys(value, ["content"], label2);
9545
+ if (typeof value.content !== "string") throw new UserError(`${label2}.content must be a string`);
9546
+ return { ...value, type, content: value.content };
9547
+ }
9548
+ throw new UserError(`${label2}.type must be url|file|directory|connector|stdin`);
9549
+ }
9550
+ function readManifest2(path) {
9551
+ const resolved = resolvePath10(path);
9552
+ const parsed = parseJsonFile(resolved, "source manifest");
9553
+ let values;
9554
+ if (Array.isArray(parsed)) {
9555
+ values = parsed;
9556
+ } else if (isRecord5(parsed)) {
9557
+ const extra = Object.keys(parsed).filter((key) => key !== "sources");
9558
+ if (extra.length > 0) throw new UserError(`source manifest contains unsupported field(s): ${extra.join(", ")}`);
9559
+ values = parsed.sources;
9560
+ }
9561
+ if (!Array.isArray(values)) throw new UserError(`source manifest must be an array or an object with a sources array`);
9562
+ return values.map((value, index) => parseManifestDescriptor(value, index, dirname13(resolved)));
9563
+ }
9564
+ function readBoundedStdin(descriptor = 0, maxFileBytes = DEFAULT_MAX_SOURCE_FILE_BYTES) {
9565
+ const chunks = [];
9566
+ let total = 0;
9567
+ while (true) {
9568
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxFileBytes + 1 - total));
9569
+ const count = readSync3(descriptor, chunk, 0, chunk.byteLength, null);
9570
+ if (count === 0) return Buffer.concat(chunks, total);
9571
+ total += count;
9572
+ if (total > maxFileBytes) {
9573
+ throw new UserError(`stdin source exceeds the per-source byte limit (${maxFileBytes})`);
9574
+ }
9575
+ chunks.push(chunk.subarray(0, count));
9576
+ }
9577
+ }
9578
+ function positionalSource(value) {
9579
+ if (/^https?:\/\//iu.test(value)) return { type: "url", url: value };
9580
+ if (value === "-") {
9581
+ if (process.stdin.isTTY) throw new UserError(`stdin source "-" requires piped input`);
9582
+ return { type: "stdin", content: readBoundedStdin() };
9583
+ }
9584
+ const resolved = resolvePath10(value);
9585
+ let stat;
9586
+ try {
9587
+ stat = lstatSync6(resolved);
9588
+ } catch (error) {
9589
+ throw new UserError(`unable to read source ${resolved}: ${error.message}`);
9590
+ }
9591
+ if (stat.isSymbolicLink()) throw new UserError(`source path must not be a symlink: ${resolved}`);
9592
+ if (stat.isFile()) return { type: "file", path: resolved };
9593
+ if (stat.isDirectory()) return { type: "directory", path: resolved };
9594
+ throw new UserError(`source must be a regular file or directory: ${resolved}`);
9595
+ }
9596
+ function collectSourceInputs(sourceArgs, options) {
9597
+ const inputs = sourceArgs.map(positionalSource);
9598
+ if (options.manifest) inputs.push(...readManifest2(options.manifest));
9599
+ if (inputs.length === 0) throw new UserError(`insight analyze requires at least one source or --manifest`);
9600
+ return inputs;
9601
+ }
9602
+ function contextPath(studyId) {
9603
+ return join32(getStudyDir(studyId), CONTEXT_FILENAME);
9604
+ }
9605
+ function inventoryPath(studyId) {
9606
+ return join32(getStudyDir(studyId), INVENTORY_FILENAME);
9607
+ }
9608
+ function writeCommandContext(studyId, skillMapPath, skillMapCoverage, inventory) {
9609
+ const context = {
9610
+ schemaVersion: 1,
9611
+ skillMapPath: skillMapPath ? resolvePath10(skillMapPath) : void 0,
9612
+ skillMapCoverage
9613
+ };
9614
+ writeJsonAtomic(contextPath(studyId), context);
9615
+ writeJsonAtomic(inventoryPath(studyId), inventory);
9616
+ }
9617
+ function readCommandContext(studyId) {
9618
+ const path = contextPath(studyId);
9619
+ const parsed = parseJsonFile(path, "insight command context");
9620
+ if (!isRecord5(parsed) || parsed.schemaVersion !== 1 || parsed.skillMapPath !== void 0 && (typeof parsed.skillMapPath !== "string" || !isAbsolute6(parsed.skillMapPath)) || parsed.skillMapCoverage !== void 0 && parsed.skillMapCoverage !== "static" && parsed.skillMapCoverage !== "resolved" || parsed.skillMapCoverage !== void 0 && parsed.skillMapPath === void 0 || Object.keys(parsed).some((key) => !["schemaVersion", "skillMapPath", "skillMapCoverage"].includes(key))) {
9621
+ throw new UserError(`malformed insight command context at ${path}`);
9622
+ }
9623
+ return parsed;
9624
+ }
9625
+ function currentInventory(study, config) {
9626
+ const storedPath = inventoryPath(study.studyId);
9627
+ const stored = parseJsonFile(storedPath, "insight inventory snapshot");
9628
+ if (!isRecord5(stored) || typeof stored.capabilityInventoryHash !== "string" || stored.capabilityInventoryHash !== study.capabilityInventoryHash) {
9629
+ throw new UserError(`insight inventory snapshot at ${storedPath} does not match study ${study.studyId}`);
9630
+ }
9631
+ const context = readCommandContext(study.studyId);
9632
+ return buildInsightInventory(config, {
9633
+ skillMapPath: context.skillMapPath,
9634
+ skillMapCoverage: context.skillMapCoverage
9635
+ });
9636
+ }
9637
+ function readCurrentStudy(studyId) {
9638
+ const study = readInsightStudy(studyId);
9639
+ const config = loadConfig();
9640
+ return { study, inventory: currentInventory(study, config), config };
9641
+ }
9642
+ function retainedPath(root, relativePath) {
9643
+ const candidate = resolvePath10(root, relativePath);
9644
+ const rel = relative5(root, candidate);
9645
+ if (rel.startsWith("..") || isAbsolute6(rel)) throw new UserError(`refusing retained source path outside the study`);
9646
+ return candidate;
9647
+ }
9648
+ function retainObservedSources(study, observedFiles) {
9649
+ if (observedFiles.length === 0) return study;
9650
+ const root = join32(getStudyDir(study.studyId), "retained-sources");
9651
+ mkdirSync9(root, { recursive: true, mode: 448 });
9652
+ chmodSync3(root, 448);
9653
+ const retainedSourceIds = /* @__PURE__ */ new Set();
9654
+ for (const file of observedFiles) {
9655
+ const sourceRoot = join32(root, file.sourceId);
9656
+ mkdirSync9(sourceRoot, { recursive: true, mode: 448 });
9657
+ chmodSync3(sourceRoot, 448);
9658
+ const destination = retainedPath(sourceRoot, file.relativePath);
9659
+ mkdirSync9(dirname13(destination), { recursive: true, mode: 448 });
9660
+ writeFileSync8(destination, file.bytes, { flag: "wx", mode: 384 });
9661
+ chmodSync3(destination, 384);
9662
+ retainedSourceIds.add(file.sourceId);
9663
+ }
9664
+ const sources = study.sources.map((source) => {
9665
+ if (!retainedSourceIds.has(source.sourceId)) return source;
9666
+ return {
9667
+ ...source,
9668
+ retention: "retained"
9669
+ };
9670
+ });
9671
+ return writeInsightStudy({ ...study, sources });
9672
+ }
9673
+ function studySummary(study) {
9674
+ const validationCounts = Object.fromEntries(
9675
+ ["corroborated", "contested", "single-source", "rejected", "unverifiable"].map((value) => [
9676
+ value,
9677
+ study.insights.filter((insight2) => insight2.validation === value).length
9678
+ ])
9679
+ );
9680
+ return {
9681
+ studyId: study.studyId,
9682
+ status: study.status,
9683
+ requestedAnalysisMode: study.requestedAnalysisMode,
9684
+ actualAnalysisMode: study.actualAnalysisMode,
9685
+ sourceCount: study.sources.length,
9686
+ insightCount: study.insights.length,
9687
+ validationCounts,
9688
+ domainCount: study.domainPlans.length,
9689
+ planHash: study.planHash,
9690
+ approval: study.approval ? { approvedDomainIds: study.approval.approvedDomainIds, planHash: study.approval.planHash, approvedAt: study.approval.approvedAt } : void 0,
9691
+ jira: study.jira,
9692
+ closedAt: study.closedAt,
9693
+ closeReason: study.closeReason
9694
+ };
9695
+ }
9696
+ function emit(json, payload, text) {
9697
+ if (json) console.log(JSON.stringify(payload, null, 2));
9698
+ else text();
9699
+ }
9700
+ function reviewDomainPlan(plan) {
9701
+ return {
9702
+ domainId: plan.domainId,
9703
+ domain: plan.domain,
9704
+ outcome: plan.outcome,
9705
+ acceptedInsightIds: plan.acceptedInsightIds,
9706
+ contestedInsightIds: plan.contestedInsightIds,
9707
+ rejectedInsightIds: plan.rejectedInsightIds,
9708
+ deduplicationEvidence: plan.deduplicationEvidence,
9709
+ selectedVerb: plan.selectedVerb,
9710
+ proposedChanges: plan.proposedChanges,
9711
+ provenanceObligations: plan.provenanceObligations,
9712
+ licenseObligations: plan.licenseObligations,
9713
+ privacyObligations: plan.privacyObligations,
9714
+ securityObligations: plan.securityObligations,
9715
+ dependencies: plan.dependencies,
9716
+ acceptanceTests: plan.acceptanceTests,
9717
+ measurement: plan.measurement,
9718
+ rollbackMechanism: plan.rollbackMechanism,
9719
+ rollbackTrigger: plan.rollbackTrigger,
9720
+ workItems: plan.workItems,
9721
+ nonGoals: plan.nonGoals
9722
+ };
9723
+ }
9724
+ function runStudyHandoff(config, studyId, instruction, json) {
9725
+ runHandoff(config, getStudyDir(studyId), {
9726
+ promptAsset: WORKFLOW_ASSET,
9727
+ instruction,
9728
+ targetLabel: "Study path",
9729
+ log: json ? console.error : console.log
9730
+ });
9731
+ }
9732
+ async function insightAnalyze(sourceArgs = [], options = {}) {
9733
+ requireProFeature2();
9734
+ const config = loadConfig();
9735
+ const inputs = collectSourceInputs(sourceArgs, options);
9736
+ const skillMapPath = options.skillMap ? resolvePath10(options.skillMap) : void 0;
9737
+ if (options.skillMapCoverage !== void 0 && skillMapPath === void 0) {
9738
+ throw new UserError("--skill-map-coverage requires --skill-map");
9739
+ }
9740
+ const inventory = buildInsightInventory(config, {
9741
+ skillMapPath,
9742
+ skillMapCoverage: options.skillMapCoverage
9743
+ });
9744
+ const normalized = normalizeSources(inputs);
9745
+ let study = createInsightStudy({
9746
+ sources: normalized.sources,
9747
+ sourceInventoryHash: normalized.sourceInventoryHash,
9748
+ requestedAnalysisMode: options.analysisMode ?? "independent"
9749
+ });
9750
+ study = writeInsightStudy({ ...study, capabilityInventoryHash: inventory.capabilityInventoryHash });
9751
+ writeCommandContext(study.studyId, skillMapPath, options.skillMapCoverage, inventory);
9752
+ if (options.retainSourceCopies) study = retainObservedSources(study, normalized.observedFiles);
9753
+ appendStudyEvent(study.studyId, {
9754
+ type: "study-created",
9755
+ detail: {
9756
+ sourceCount: study.sources.length,
9757
+ requestedAnalysisMode: study.requestedAnalysisMode,
9758
+ retainedLocalSources: options.retainSourceCopies === true,
9759
+ inventoryCoverage: inventory.coverage
9760
+ }
9761
+ });
9762
+ emit(options.json, { ...studySummary(study), inventory: {
9763
+ coverage: inventory.coverage,
9764
+ coverageReason: inventory.coverageReason,
9765
+ installedCapabilityCount: inventory.installedCapabilityCount,
9766
+ mapCapabilityCount: inventory.mapCapabilityCount
9767
+ } }, () => {
9768
+ console.log(`[skill-forge] created insight study ${study.studyId}`);
9769
+ console.log(` sources: ${study.sources.length}; inventory: ${inventory.coverage}`);
9770
+ console.log(` study: ${getStudyDir(study.studyId)}`);
9771
+ console.log(` next: skill-forge insight submit ${study.studyId} --file <analysis.json>`);
9772
+ });
9773
+ if (options.handoff) {
9774
+ runStudyHandoff(
9775
+ config,
9776
+ study.studyId,
9777
+ "Read {prompt}. Analyze the untrusted source study at {path}. Write candidate JSON outside the study and import it only through `skill-forge insight submit`. Never approve the study, call Jira, or mutate a capability.",
9778
+ options.json
9779
+ );
9780
+ }
9781
+ }
9782
+ async function insightSubmit(studyId, options) {
9783
+ requireProFeature2();
9784
+ const { study, inventory } = readCurrentStudy(studyId);
9785
+ const sourceIssue = validateCurrentSourceFingerprints(study)[0];
9786
+ if (sourceIssue) throw new UserError(sourceIssue.message);
9787
+ const candidate = parseJsonFile(options.file, "insight analysis submission");
9788
+ const next = writeInsightStudy(validateInsightSubmission(study, candidate, inventory));
9789
+ appendStudyEvent(studyId, { type: "analysis-submitted", detail: { planHash: next.planHash } });
9790
+ emit(options.json, studySummary(next), () => {
9791
+ console.log(`[skill-forge] imported analysis for ${studyId}`);
9792
+ console.log(` plan hash: ${next.planHash}`);
9793
+ console.log(` next: skill-forge insight review ${studyId}`);
9794
+ });
9795
+ }
9796
+ async function insightReview(studyId, options = {}) {
9797
+ requireProFeature2();
9798
+ const { study, inventory } = readCurrentStudy(studyId);
9799
+ const validation = validateStudyForReview(study, inventory);
9800
+ if (options.json) {
9801
+ const payload = {
9802
+ ...studySummary(study),
9803
+ validation,
9804
+ inventory: {
9805
+ coverage: inventory.coverage,
9806
+ coverageReason: inventory.coverageReason,
9807
+ installedCapabilityCount: inventory.installedCapabilityCount,
9808
+ mapCapabilityCount: inventory.mapCapabilityCount
9809
+ },
9810
+ domainPlans: study.domainPlans.map(reviewDomainPlan)
9811
+ };
9812
+ assertExportSafe(payload, study, "insight review JSON");
9813
+ console.log(JSON.stringify(payload, null, 2));
9814
+ return;
9815
+ }
9816
+ console.log(renderInsightReview(study, inventory, validation));
9817
+ }
9818
+ async function insightApprove(studyId, options = {}) {
9819
+ requireProFeature2();
9820
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
9821
+ throw new UserError(
9822
+ "insight approve is human-only and requires an interactive terminal; there is deliberately no --yes option"
9823
+ );
9824
+ }
9825
+ const { study, inventory } = readCurrentStudy(studyId);
9826
+ if (study.status !== "review-ready") {
9827
+ throw new UserError(`study ${studyId} is ${study.status} and cannot be approved`);
9828
+ }
9829
+ const validation = validateStudyForReview(study, inventory);
9830
+ if (!validation.valid) {
9831
+ throw new UserError(`study ${studyId} is not ready for approval: ${validation.blockers.map((issue) => issue.message).join("; ")}`);
9832
+ }
9833
+ if (!study.capabilityInventoryHash) throw new UserError(`study ${studyId} has no capability inventory hash`);
9834
+ const knownDomains = new Set(study.domainPlans.map((plan) => plan.domainId));
9835
+ const requested = options.domain ?? [];
9836
+ const approvedDomainIds = requested.length > 0 ? [...new Set(requested)] : [...knownDomains];
9837
+ const unknown = approvedDomainIds.filter((id) => !knownDomains.has(id));
9838
+ if (unknown.length > 0) throw new UserError(`unknown domain id(s): ${unknown.join(", ")}`);
9839
+ if (approvedDomainIds.length === 0) throw new UserError(`study ${studyId} has no domain plans to approve`);
9840
+ console.log(renderInsightReview(study, inventory, validation));
9841
+ console.log("\nSelected domain plans:");
9842
+ for (const plan of study.domainPlans.filter((candidate) => approvedDomainIds.includes(candidate.domainId))) {
9843
+ console.log(renderDomainPlan(plan));
9844
+ }
9845
+ console.log(`
9846
+ Exact plan hash: ${validation.planHash}`);
9847
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
9848
+ let confirmed = false;
9849
+ try {
9850
+ const answer = (await rl.question("Approve this exact plan hash and selected domains? [y/N] ")).trim().toLowerCase();
9851
+ confirmed = answer === "y" || answer === "yes";
9852
+ } finally {
9853
+ rl.close();
9854
+ }
9855
+ if (!confirmed) {
9856
+ console.log(`[skill-forge] study ${studyId} was not approved.`);
9857
+ return;
9858
+ }
9859
+ const approvedAt = (/* @__PURE__ */ new Date()).toISOString();
9860
+ const approval = {
9861
+ approvedDomainIds,
9862
+ planHash: validation.planHash,
9863
+ approvedAt,
9864
+ sourceInventoryHash: study.sourceInventoryHash,
9865
+ capabilityInventoryHash: study.capabilityInventoryHash,
9866
+ acceptedExceptions: validation.warnings.map((issue) => `${issue.code}: ${issue.message}`)
9867
+ };
9868
+ const next = writeInsightStudy({ ...study, status: "approved", planHash: validation.planHash, approval, jira: void 0 });
9869
+ appendStudyEvent(studyId, {
9870
+ type: "plan-approved",
9871
+ occurredAt: approvedAt,
9872
+ detail: { planHash: validation.planHash, approvedDomainIds }
9873
+ });
9874
+ console.log(`[skill-forge] approved ${approvedDomainIds.length} domain plan(s) for ${next.studyId}`);
9875
+ console.log(` plan hash: ${validation.planHash}`);
9876
+ }
9877
+ async function insightJira(studyId, options) {
9878
+ requireProFeature2();
9879
+ const localMode = options.receipt !== void 0 || options.resume === true;
9880
+ if (options.receipt !== void 0 && options.resume === true) {
9881
+ throw new UserError("--receipt and --resume are mutually exclusive");
9882
+ }
9883
+ if (localMode && (options.project !== void 0 || options.out !== void 0 || options.handoff === true)) {
9884
+ throw new UserError("--receipt and --resume cannot be combined with --project, --out, or --handoff");
9885
+ }
9886
+ const projectKey = localMode ? void 0 : nonEmptyString(options.project, "--project").trim();
9887
+ const { study, inventory, config } = readCurrentStudy(studyId);
9888
+ const validation = validateStudyForReview(study, inventory);
9889
+ if (!validation.valid) {
9890
+ throw new UserError(`study ${studyId} cannot generate a Jira manifest: ${validation.blockers.map((issue) => issue.message).join("; ")}`);
9891
+ }
9892
+ if (localMode) {
9893
+ const privatePath2 = join32(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
9894
+ const manifest2 = assertCurrentJiraManifest(study, parseJsonFile(privatePath2, "stored Jira manifest"));
9895
+ const receipt = options.receipt === void 0 ? void 0 : appendJiraReceipt(studyId, manifest2, parseJiraReceiptInput(parseJsonFile(options.receipt, "Jira receipt import")));
9896
+ const resume = buildJiraResumeState(manifest2, readJiraReceipts(studyId, manifest2));
9897
+ emit(options.json, receipt ? { receipt, resume } : resume, () => {
9898
+ if (receipt) console.log(`[skill-forge] recorded Jira receipt ${receipt.eventId} for ${studyId}`);
9899
+ console.log(`[skill-forge] Jira resume state for ${studyId}: ${resume.nextAction}`);
9900
+ console.log(` complete: ${resume.complete ? "yes" : "no"}`);
9901
+ });
9902
+ return;
9903
+ }
9904
+ const manifest = buildJiraManifest(study, projectKey);
9905
+ const privatePath = join32(getStudyDir(studyId), JIRA_MANIFEST_FILENAME);
9906
+ writeJsonAtomic(privatePath, manifest);
9907
+ if (options.out) {
9908
+ writeNewFile(resolvePath10(options.out), `${JSON.stringify(manifest, null, 2)}
9909
+ `, "--out");
9910
+ }
9911
+ const generatedAt = manifest.generatedAt;
9912
+ const manifestHash = stableHash(manifest);
9913
+ const next = writeInsightStudy({
9914
+ ...study,
9915
+ status: "jira-manifested",
9916
+ jira: { projectKey: manifest.projectKey, manifestHash, generatedAt }
9917
+ });
9918
+ appendStudyEvent(studyId, {
9919
+ type: "jira-manifest-generated",
9920
+ occurredAt: generatedAt,
9921
+ detail: { projectKey: manifest.projectKey, manifestHash, workItemCount: manifest.workItems.length }
9922
+ });
9923
+ emit(options.json, manifest, () => {
9924
+ console.log(`[skill-forge] wrote Jira manifest for ${next.studyId}`);
9925
+ console.log(` manifest: ${privatePath}`);
9926
+ if (options.out) console.log(` export: ${resolvePath10(options.out)}`);
9927
+ console.log(" no Jira issues were created.");
9928
+ });
9929
+ if (options.handoff) {
9930
+ runStudyHandoff(
9931
+ config,
9932
+ studyId,
9933
+ "Read {prompt}. Present the approved Jira manifest in the private study at {path}, resolve live project metadata and every idempotency marker, then obtain literal human confirmation before any Jira write. Never approve the plan or mutate a capability.",
9934
+ options.json
9935
+ );
9936
+ }
9937
+ }
9938
+ async function insightStatus(studyId, options = {}) {
9939
+ requireProFeature2();
9940
+ const study = readInsightStudy(studyId);
9941
+ const summary = studySummary(study);
9942
+ emit(options.json, summary, () => {
9943
+ console.log(`[skill-forge] insight study ${sanitizeRenderedText(studyId)}`);
9944
+ console.log(` status: ${study.status}`);
9945
+ console.log(` sources: ${study.sources.length}`);
9946
+ console.log(` insights: ${study.insights.length}`);
9947
+ console.log(` domains: ${study.domainPlans.length}`);
9948
+ console.log(` plan: ${study.planHash ?? "not submitted"}`);
9949
+ console.log(` approval: ${study.approval ? `${study.approval.approvedAt} (${study.approval.approvedDomainIds.length} domain(s))` : "none"}`);
9950
+ console.log(` Jira: ${study.jira ? `manifest for ${sanitizeRenderedText(study.jira.projectKey)}; issues not asserted` : "no manifest"}`);
9951
+ });
9952
+ }
9953
+ async function insightClose(studyId, options) {
9954
+ requireProFeature2();
9955
+ if (options.status !== "dismissed" && options.status !== "archived") {
9956
+ throw new UserError(`--status must be dismissed or archived`);
9957
+ }
9958
+ const study = readInsightStudy(studyId);
9959
+ if (study.status === "dismissed" || study.status === "archived") {
9960
+ throw new UserError(`study ${studyId} is already ${study.status}`);
9961
+ }
9962
+ const closedAt = (/* @__PURE__ */ new Date()).toISOString();
9963
+ const next = writeInsightStudy({
9964
+ ...study,
9965
+ status: options.status,
9966
+ approval: void 0,
9967
+ jira: void 0,
9968
+ closedAt,
9969
+ closeReason: `closed by insight close --status ${options.status}`
9970
+ });
9971
+ appendStudyEvent(studyId, { type: "study-closed", occurredAt: closedAt, detail: { status: options.status } });
9972
+ console.log(`[skill-forge] ${next.studyId} closed as ${next.status}; append-only receipts were preserved.`);
9973
+ }
9974
+
9975
+ // src/commands/refine.ts
9976
+ import { createHash as createHash8, randomUUID as randomUUID5 } from "crypto";
9977
+ import { chmodSync as chmodSync4, existsSync as existsSync29, lstatSync as lstatSync7, mkdirSync as mkdirSync11, readFileSync as readFileSync24, realpathSync as realpathSync10, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
9978
+ import { dirname as dirname15, isAbsolute as isAbsolute8, join as join35, relative as relative7, resolve as resolvePath13, sep as sep6 } from "path";
9979
+ import { createInterface as createInterface7 } from "readline/promises";
9980
+
9981
+ // src/refine/context.ts
9982
+ import { existsSync as existsSync28, realpathSync as realpathSync9, statSync as statSync13 } from "fs";
9983
+ import { isAbsolute as isAbsolute7, join as join34, relative as relative6, resolve as resolvePath12 } from "path";
9984
+
9985
+ // src/refine/models.ts
9986
+ import { randomBytes } from "crypto";
9987
+ var REFINEMENT_CATEGORIES = ["trigger", "content", "hook", "tool", "pattern", "config", "new"];
9988
+ var OVERRIDE_TYPES = ["patch", "extend", "config", "full", "hook", "script", "new"];
9989
+ var PATCH_ACTIONS = ["append", "prepend", "replace-section", "insert-after", "insert-before", "delete-section"];
9990
+ var PATTERN_STATUSES = ["tracking", "ready", "generalized", "dismissed"];
9991
+ function isRefinementCategory(value) {
9992
+ return REFINEMENT_CATEGORIES.includes(value);
9993
+ }
9994
+ function isOverrideType(value) {
9995
+ return OVERRIDE_TYPES.includes(value);
9996
+ }
9997
+ function isPatchAction(value) {
9998
+ return PATCH_ACTIONS.includes(value);
9999
+ }
10000
+ function isPatternStatus(value) {
10001
+ return PATTERN_STATUSES.includes(value);
10002
+ }
10003
+ var REFINEMENT_STATUSES = ["pending", "applied", "generalized"];
10004
+ function isRefinementStatus(value) {
10005
+ return REFINEMENT_STATUSES.includes(value);
10006
+ }
10007
+ function patternCount(pattern) {
10008
+ return new Set(pattern.occurrences.map((o) => o.projectRealpath)).size;
10009
+ }
10010
+ function defaultRandomHex() {
10011
+ return randomBytes(2).toString("hex");
10012
+ }
10013
+ function formatYYYYMMDD(date) {
10014
+ const yyyy = date.getUTCFullYear().toString().padStart(4, "0");
10015
+ const mm = (date.getUTCMonth() + 1).toString().padStart(2, "0");
10016
+ const dd = date.getUTCDate().toString().padStart(2, "0");
10017
+ return `${yyyy}${mm}${dd}`;
10018
+ }
10019
+ function generateRefinementId(existingIds = /* @__PURE__ */ new Set(), now = /* @__PURE__ */ new Date(), randomHex = defaultRandomHex) {
10020
+ const datePart = formatYYYYMMDD(now);
10021
+ let id;
10022
+ do {
10023
+ id = `REF-${datePart}-${randomHex()}`;
10024
+ } while (existingIds.has(id));
10025
+ return id;
10026
+ }
10027
+ function generatePatternId(existingIds = /* @__PURE__ */ new Set(), randomHex = defaultRandomHex) {
10028
+ let id;
10029
+ do {
10030
+ id = `PAT-${randomHex()}`;
10031
+ } while (existingIds.has(id));
10032
+ return id;
10033
+ }
10034
+ function fingerprintKey(skill, target, overrideType, action) {
10035
+ const normalize = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
10036
+ return [normalize(skill), normalize(target), overrideType, action ?? ""].join("::");
10037
+ }
10038
+ function contentSimilarity(a, b) {
10039
+ const tokenize3 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
10040
+ const tokensA = tokenize3(a);
10041
+ const tokensB = tokenize3(b);
10042
+ if (tokensA.size === 0 || tokensB.size === 0) return 0;
10043
+ let intersectionSize = 0;
10044
+ for (const token of tokensA) {
10045
+ if (tokensB.has(token)) intersectionSize++;
10046
+ }
10047
+ const unionSize3 = tokensA.size + tokensB.size - intersectionSize;
10048
+ return unionSize3 === 0 ? 0 : intersectionSize / unionSize3;
10049
+ }
10050
+
10051
+ // src/refine/store.ts
10052
+ import { createHash as createHash7 } from "crypto";
10053
+ import { existsSync as existsSync27, readFileSync as readFileSync23, statSync as statSync12, realpathSync as realpathSync8 } from "fs";
10054
+ import { join as join33, resolve as resolvePath11 } from "path";
10055
+ var SIMILARITY_JOIN_THRESHOLD = 0.7;
10056
+ var READY_THRESHOLD = 2;
10057
+ function getRefinementsDir() {
10058
+ return join33(getHomeDir(), "refinements");
10059
+ }
10060
+ function getRefinementsPath() {
10061
+ return join33(getRefinementsDir(), "refinements.json");
10062
+ }
10063
+ function getPatternsPath() {
10064
+ return join33(getRefinementsDir(), "patterns.json");
10065
+ }
10066
+ function getBackupsDir() {
10067
+ return join33(getRefinementsDir(), "backups");
10068
+ }
10069
+ function readJsonFile(path, label2) {
10070
+ let raw;
10071
+ try {
10072
+ raw = readFileSync23(path, "utf-8");
10073
+ } catch (err) {
10074
+ throw new UserError(`unable to read ${label2} at ${path}: ${err.message}`);
10075
+ }
10076
+ try {
10077
+ return JSON.parse(raw);
10078
+ } catch {
10079
+ throw new UserError(`malformed ${label2} at ${path}: not valid JSON`);
10080
+ }
10081
+ }
10082
+ function readStore(path, label2, validateRecord) {
10083
+ if (!existsSync27(path)) {
10084
+ return { schemaVersion: 1, records: [] };
10085
+ }
10086
+ const parsed = readJsonFile(path, label2);
10087
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.records)) {
10088
+ throw new UserError(`malformed ${label2} at ${path}: expected an object with a "records" array`);
10089
+ }
10090
+ const records = parsed.records;
10091
+ if (validateRecord) {
10092
+ records.forEach((record, index) => validateRecord(record, index));
10093
+ }
10094
+ return { schemaVersion: 1, records };
10095
+ }
10096
+ function validateRefinementRecordShape(record, index, path) {
10097
+ if (typeof record !== "object" || record === null) {
10098
+ throw new UserError(`malformed refinements.json at ${path}: record[${index}] is not an object`);
10099
+ }
10100
+ const r = record;
10101
+ if (typeof r.id !== "string" || r.id.length === 0) {
10102
+ throw new UserError(`malformed refinements.json at ${path}: record[${index}] is missing a valid "id"`);
10103
+ }
10104
+ if (typeof r.createdAt !== "string" || r.createdAt.length === 0) {
10105
+ throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "createdAt"`);
10106
+ }
10107
+ if (typeof r.status !== "string" || r.status.length === 0) {
10108
+ throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "status"`);
10109
+ }
10110
+ if (typeof r.skill !== "string" || r.skill.length === 0) {
10111
+ throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "skill"`);
10112
+ }
10113
+ const patchActionsRequiringMarker = /* @__PURE__ */ new Set(["insert-after", "insert-before", "replace-section", "delete-section"]);
10114
+ if (r.overrideType === "patch") {
10115
+ if (typeof r.patchAction !== "string" || r.patchAction.length === 0) {
10116
+ throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchAction"`);
10117
+ }
10118
+ if (patchActionsRequiringMarker.has(r.patchAction) && (typeof r.patchMarker !== "string" || r.patchMarker.length === 0)) {
10119
+ throw new UserError(
10120
+ `malformed refinements.json at ${path}: record "${r.id}" has patchAction "${r.patchAction}" but is missing "patchMarker"`
10121
+ );
10122
+ }
10123
+ if (typeof r.patchContent !== "string" || r.patchContent.length === 0) {
10124
+ throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchContent"`);
10125
+ }
10126
+ } else if (r.patchAction !== void 0) {
10127
+ throw new UserError(
10128
+ `malformed refinements.json at ${path}: record "${r.id}" has overrideType "${String(r.overrideType)}" but carries "patchAction" \u2014 only valid when overrideType is "patch"`
10129
+ );
10130
+ }
10131
+ }
10132
+ function validatePatternRecordShape(record, index, path) {
10133
+ if (typeof record !== "object" || record === null) {
10134
+ throw new UserError(`malformed patterns.json at ${path}: record[${index}] is not an object`);
10135
+ }
10136
+ const p = record;
10137
+ if (typeof p.id !== "string" || p.id.length === 0) {
10138
+ throw new UserError(`malformed patterns.json at ${path}: record[${index}] is missing a valid "id"`);
10139
+ }
10140
+ if (typeof p.status !== "string" || p.status.length === 0) {
10141
+ throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "status"`);
10142
+ }
10143
+ if (!Array.isArray(p.occurrences)) {
10144
+ throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "occurrences" array`);
10145
+ }
10146
+ if (p.status === "dismissed" && (typeof p.dismissedReason !== "string" || p.dismissedReason.length === 0)) {
10147
+ throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "dismissed" but is missing "dismissedReason"`);
10148
+ }
10149
+ if (p.status === "generalized") {
10150
+ if (typeof p.generalizedAt !== "string" || p.generalizedAt.length === 0) {
10151
+ throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedAt"`);
10152
+ }
10153
+ if (typeof p.generalizedBackupId !== "string" || p.generalizedBackupId.length === 0) {
10154
+ throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedBackupId"`);
10155
+ }
10156
+ }
10157
+ }
10158
+ function readRefinementsStore() {
10159
+ const path = getRefinementsPath();
10160
+ return readStore(path, "refinements.json", (record, index) => validateRefinementRecordShape(record, index, path));
10161
+ }
10162
+ function writeRefinementsStore(store) {
10163
+ writeJsonAtomic(getRefinementsPath(), store);
7396
10164
  }
7397
10165
  function readPatternsStore() {
7398
10166
  const path = getPatternsPath();
@@ -7402,9 +10170,9 @@ function writePatternsStore(store) {
7402
10170
  writeJsonAtomic(getPatternsPath(), store);
7403
10171
  }
7404
10172
  function resolveProjectRealpath(projectRoot) {
7405
- const resolved = resolvePath10(projectRoot);
10173
+ const resolved = resolvePath11(projectRoot);
7406
10174
  try {
7407
- return realpathSync7(resolved);
10175
+ return realpathSync8(resolved);
7408
10176
  } catch {
7409
10177
  return resolved;
7410
10178
  }
@@ -7536,16 +10304,16 @@ function createBackup(filePaths, deps = {}) {
7536
10304
  const randomHex = deps.randomHex ?? defaultRandomHex;
7537
10305
  const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
7538
10306
  const files = filePaths.map((path) => {
7539
- if (!existsSync25(path)) {
10307
+ if (!existsSync27(path)) {
7540
10308
  return { path, existed: false, sha256: null, mode: null, contentBase64: null };
7541
10309
  }
7542
- const bytes = readFileSync19(path);
7543
- const mode = statSync11(path).mode & 511;
7544
- const sha2562 = createHash5("sha256").update(bytes).digest("hex");
10310
+ const bytes = readFileSync23(path);
10311
+ const mode = statSync12(path).mode & 511;
10312
+ const sha2562 = createHash7("sha256").update(bytes).digest("hex");
7545
10313
  return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
7546
10314
  });
7547
10315
  const manifest = { id, createdAt: now.toISOString(), files };
7548
- writeJsonAtomic(join28(getBackupsDir(), id, "manifest.json"), manifest);
10316
+ writeJsonAtomic(join33(getBackupsDir(), id, "manifest.json"), manifest);
7549
10317
  return manifest;
7550
10318
  }
7551
10319
  function validateBackupManifestShape(value, path) {
@@ -7594,8 +10362,8 @@ function readBackupManifest(id) {
7594
10362
  if (!isValidBackupId(id)) {
7595
10363
  throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
7596
10364
  }
7597
- const manifestPath = join28(getBackupsDir(), id, "manifest.json");
7598
- if (!existsSync25(manifestPath)) {
10365
+ const manifestPath = join33(getBackupsDir(), id, "manifest.json");
10366
+ if (!existsSync27(manifestPath)) {
7599
10367
  throw new UserError(`no backup with id: ${id}`);
7600
10368
  }
7601
10369
  const parsed = readJsonFile(manifestPath, "backup manifest");
@@ -7605,7 +10373,7 @@ function readBackupManifest(id) {
7605
10373
 
7606
10374
  // src/refine/context.ts
7607
10375
  function resolveConfiguredRoot2(root) {
7608
- return resolvePath11(expandHome(root));
10376
+ return resolvePath12(expandHome(root));
7609
10377
  }
7610
10378
  function assertPlainSkillName(name) {
7611
10379
  if (!name || name.trim().length === 0) {
@@ -7617,32 +10385,32 @@ function assertPlainSkillName(name) {
7617
10385
  }
7618
10386
  function realpathOrNull2(p) {
7619
10387
  try {
7620
- return realpathSync8(p);
10388
+ return realpathSync9(p);
7621
10389
  } catch {
7622
10390
  return null;
7623
10391
  }
7624
10392
  }
7625
10393
  function isUnderRoot2(candidate, root) {
7626
- const rel = relative3(root, candidate);
7627
- return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
10394
+ const rel = relative6(root, candidate);
10395
+ return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
7628
10396
  }
7629
10397
  function resolveScopes(config, cwd, skill, userRootFlag) {
7630
10398
  assertPlainSkillName(skill);
7631
- const resolvedCwd = resolvePath11(cwd);
7632
- const localDir = join29(resolvedCwd, ".claude", "skills", skill);
7633
- const sharedDir = join29(resolvedCwd, "skills", skill);
7634
- const local = { scope: "local", dir: localDir, exists: existsSync26(localDir) };
7635
- const shared = { scope: "shared", dir: sharedDir, exists: existsSync26(sharedDir) };
10399
+ const resolvedCwd = resolvePath12(cwd);
10400
+ const localDir = join34(resolvedCwd, ".claude", "skills", skill);
10401
+ const sharedDir = join34(resolvedCwd, "skills", skill);
10402
+ const local = { scope: "local", dir: localDir, exists: existsSync28(localDir) };
10403
+ const shared = { scope: "shared", dir: sharedDir, exists: existsSync28(sharedDir) };
7636
10404
  const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
7637
10405
  return { local, shared, user };
7638
10406
  }
7639
10407
  function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
7640
10408
  if (userRootFlag) {
7641
- const root = resolvePath11(userRootFlag);
7642
- return { resolved: true, dir: join29(root, skill), root: userRootFlag, reason: "flag" };
10409
+ const root = resolvePath12(userRootFlag);
10410
+ return { resolved: true, dir: join34(root, skill), root: userRootFlag, reason: "flag" };
7643
10411
  }
7644
10412
  const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
7645
- const candidates = config.skillsRoots.map((root) => ({ root, dir: join29(resolveConfiguredRoot2(root), skill) }));
10413
+ const candidates = config.skillsRoots.map((root) => ({ root, dir: join34(resolveConfiguredRoot2(root), skill) }));
7646
10414
  for (const candidate of candidates) {
7647
10415
  const skillReal = realpathOrNull2(candidate.dir);
7648
10416
  if (skillReal) {
@@ -7660,14 +10428,14 @@ function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
7660
10428
  }
7661
10429
  function isFile2(p) {
7662
10430
  try {
7663
- return statSync12(p).isFile();
10431
+ return statSync13(p).isFile();
7664
10432
  } catch {
7665
10433
  return false;
7666
10434
  }
7667
10435
  }
7668
10436
  function isDirectory(p) {
7669
10437
  try {
7670
- return statSync12(p).isDirectory();
10438
+ return statSync13(p).isDirectory();
7671
10439
  } catch {
7672
10440
  return false;
7673
10441
  }
@@ -7677,12 +10445,12 @@ function inventoryScopeDir(scope, dir) {
7677
10445
  scope,
7678
10446
  dir,
7679
10447
  dirExists: isDirectory(dir),
7680
- skillMd: isFile2(join29(dir, "SKILL.md")),
7681
- patchMd: isFile2(join29(dir, "SKILL.patch.md")),
7682
- extendMd: isFile2(join29(dir, "SKILL.extend.md")),
7683
- configJson: isFile2(join29(dir, "skill-config.json")),
7684
- hooksDir: isDirectory(join29(dir, "hooks")),
7685
- scriptsDir: isDirectory(join29(dir, "scripts"))
10448
+ skillMd: isFile2(join34(dir, "SKILL.md")),
10449
+ patchMd: isFile2(join34(dir, "SKILL.patch.md")),
10450
+ extendMd: isFile2(join34(dir, "SKILL.extend.md")),
10451
+ configJson: isFile2(join34(dir, "skill-config.json")),
10452
+ hooksDir: isDirectory(join34(dir, "hooks")),
10453
+ scriptsDir: isDirectory(join34(dir, "scripts"))
7686
10454
  };
7687
10455
  }
7688
10456
  function inventoryAllScopes(resolution) {
@@ -7915,10 +10683,10 @@ function previewDiff(before, after, path) {
7915
10683
  }
7916
10684
 
7917
10685
  // src/commands/refine.ts
7918
- var PRO_FEATURE_LABEL8 = "skill refinement";
10686
+ var PRO_FEATURE_LABEL9 = "skill refinement";
7919
10687
  var REFINE_PROMPT_ASSET = "refine-prompt.md";
7920
- function requireProFeature2() {
7921
- const status2 = proFeatureStatus(PRO_FEATURE_LABEL8);
10688
+ function requireProFeature3() {
10689
+ const status2 = proFeatureStatus(PRO_FEATURE_LABEL9);
7922
10690
  if (!status2.enabled) {
7923
10691
  throw new UserError(status2.notice ?? "refine is a Pro feature \u2014 set SKILL_FORGE_LICENSE or config.licenseKey to a valid license to unlock.");
7924
10692
  }
@@ -7927,7 +10695,7 @@ function requireProFeature2() {
7927
10695
  function isInteractive(json) {
7928
10696
  return !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
7929
10697
  }
7930
- function emit(json, payload, renderText) {
10698
+ function emit2(json, payload, renderText) {
7931
10699
  if (json) {
7932
10700
  console.log(JSON.stringify(payload, null, 2));
7933
10701
  } else {
@@ -7943,7 +10711,7 @@ async function confirmWrite(yes, json, prompt) {
7943
10711
  if (!isInteractive(json)) {
7944
10712
  throw new UserError("refusing to write without confirmation \u2014 pass --yes, or run interactively.");
7945
10713
  }
7946
- const rl = createInterface6({ input: process.stdin, output: process.stdout });
10714
+ const rl = createInterface7({ input: process.stdin, output: process.stdout });
7947
10715
  try {
7948
10716
  return await confirmYesNo(rl, prompt);
7949
10717
  } finally {
@@ -7951,18 +10719,18 @@ async function confirmWrite(yes, json, prompt) {
7951
10719
  }
7952
10720
  }
7953
10721
  function resolveConfiguredRoot3(root) {
7954
- return resolvePath12(expandHome(root));
10722
+ return resolvePath13(expandHome(root));
7955
10723
  }
7956
10724
  function isUnderRoot3(candidate, root) {
7957
- const rel = relative4(root, candidate);
7958
- return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
10725
+ const rel = relative7(root, candidate);
10726
+ return rel === "" || !rel.startsWith("..") && !isAbsolute8(rel);
7959
10727
  }
7960
10728
  function sanitizeDiffForConsole(diff) {
7961
10729
  return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
7962
10730
  }
7963
10731
  function resolveContentOption(options) {
7964
10732
  if (options.content !== void 0) return options.content;
7965
- if (options.contentFile) return readFileSync20(resolvePath12(options.contentFile), "utf-8");
10733
+ if (options.contentFile) return readFileSync24(resolvePath13(options.contentFile), "utf-8");
7966
10734
  return void 0;
7967
10735
  }
7968
10736
  function validateCaptureOptions(options) {
@@ -8088,19 +10856,19 @@ ${rendered}` : rendered;
8088
10856
  function ensureScopeDirSafe(scopeDir, resolvedCwd) {
8089
10857
  let lst;
8090
10858
  try {
8091
- lst = lstatSync4(scopeDir);
10859
+ lst = lstatSync7(scopeDir);
8092
10860
  } catch {
8093
10861
  lst = void 0;
8094
10862
  }
8095
10863
  if (lst?.isSymbolicLink()) {
8096
10864
  throw new UserError(`refusing to write under ${scopeDir}: scope directory is a symlink`);
8097
10865
  }
8098
- mkdirSync9(scopeDir, { recursive: true });
10866
+ mkdirSync11(scopeDir, { recursive: true });
8099
10867
  const real = resolveConfiguredRoot3(scopeDir);
8100
- const lexical = resolvePath12(scopeDir);
10868
+ const lexical = resolvePath13(scopeDir);
8101
10869
  let realResolved;
8102
10870
  try {
8103
- realResolved = realpathSync9(scopeDir);
10871
+ realResolved = realpathSync10(scopeDir);
8104
10872
  } catch {
8105
10873
  realResolved = real;
8106
10874
  }
@@ -8114,28 +10882,28 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
8114
10882
  }
8115
10883
  function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
8116
10884
  const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
8117
- const destPath = join30(scopeDir, relPath);
8118
- const destDir = dirname14(destPath);
8119
- mkdirSync9(destDir, { recursive: true });
8120
- const realDestDir = realpathSync9(destDir);
10885
+ const destPath = join35(scopeDir, relPath);
10886
+ const destDir = dirname15(destPath);
10887
+ mkdirSync11(destDir, { recursive: true });
10888
+ const realDestDir = realpathSync10(destDir);
8121
10889
  if (!isUnderRoot3(realDestDir, realScopeDir)) {
8122
10890
  throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
8123
10891
  }
8124
10892
  let destLst;
8125
10893
  try {
8126
- destLst = lstatSync4(destPath);
10894
+ destLst = lstatSync7(destPath);
8127
10895
  } catch {
8128
10896
  destLst = void 0;
8129
10897
  }
8130
10898
  if (destLst?.isSymbolicLink()) {
8131
10899
  throw new UserError(`refusing to write ${destPath}: destination is a symlink`);
8132
10900
  }
8133
- writeFileSync9(destPath, content, { mode: 384 });
8134
- chmodSync(destPath, 384);
10901
+ writeFileSync10(destPath, content, { mode: 384 });
10902
+ chmodSync4(destPath, 384);
8135
10903
  return destPath;
8136
10904
  }
8137
10905
  async function refineCaptureCommand(rawOptions = {}) {
8138
- requireProFeature2();
10906
+ requireProFeature3();
8139
10907
  const opts = validateCaptureOptions(rawOptions);
8140
10908
  const config = loadConfig();
8141
10909
  const cwd = process.cwd();
@@ -8147,7 +10915,7 @@ async function refineCaptureCommand(rawOptions = {}) {
8147
10915
  const refinementId = generateRefinementId(existingIds, now, defaultRandomHex);
8148
10916
  const preview = renderCaptureArtifactPreview(opts, content, refinementId, now.toISOString(), scopeDir);
8149
10917
  if (opts.dryRun) {
8150
- emit(opts.json, { dryRun: true, path: preview.destPath, diff: preview.diff }, () => {
10918
+ emit2(opts.json, { dryRun: true, path: preview.destPath, diff: preview.diff }, () => {
8151
10919
  console.log(`[skill-forge] refine capture \u2014 ${opts.overrideType} override for "${sanitizeForMarkdown(opts.skill)}" at ${opts.scope} scope`);
8152
10920
  console.log(sanitizeDiffForConsole(preview.diff));
8153
10921
  });
@@ -8160,7 +10928,7 @@ async function refineCaptureCommand(rawOptions = {}) {
8160
10928
  const proceed = await confirmWrite(opts.yes, opts.json, `
8161
10929
  Write ${preview.destPath}? [y/N] `);
8162
10930
  if (!proceed) {
8163
- emit(opts.json, { written: false, cancelled: true, path: preview.destPath }, () => {
10931
+ emit2(opts.json, { written: false, cancelled: true, path: preview.destPath }, () => {
8164
10932
  console.log("[skill-forge] capture cancelled \u2014 nothing written.");
8165
10933
  });
8166
10934
  return;
@@ -8193,7 +10961,7 @@ Write ${preview.destPath}? [y/N] `);
8193
10961
  } catch (err) {
8194
10962
  throw new UserError(`wrote the override artifact at ${writtenPath}, but recording the refinement in the store failed: ${err.message}`);
8195
10963
  }
8196
- emit(
10964
+ emit2(
8197
10965
  opts.json,
8198
10966
  { written: true, path: writtenPath, refinementId: result.refinement.id, patternId: result.pattern.id, patternBecameReady: result.patternBecameReady },
8199
10967
  () => {
@@ -8210,21 +10978,21 @@ Write ${preview.destPath}? [y/N] `);
8210
10978
  }
8211
10979
  function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
8212
10980
  const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
8213
- const probePath = probeRelPath ? join30(scopeDir, probeRelPath) : void 0;
8214
- const existingContent = probePath && existsSync27(probePath) ? readFileSync20(probePath, "utf-8") : "";
10981
+ const probePath = probeRelPath ? join35(scopeDir, probeRelPath) : void 0;
10982
+ const existingContent = probePath && existsSync29(probePath) ? readFileSync24(probePath, "utf-8") : "";
8215
10983
  const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
8216
10984
  const relPath = rendered.relPath;
8217
- const destPath = join30(scopeDir, relPath);
8218
- const before = existsSync27(destPath) ? readFileSync20(destPath, "utf-8") : "";
10985
+ const destPath = join35(scopeDir, relPath);
10986
+ const before = existsSync29(destPath) ? readFileSync24(destPath, "utf-8") : "";
8219
10987
  const diff = previewDiff(before, rendered.content, destPath);
8220
10988
  return { relPath, content: rendered.content, destPath, diff };
8221
10989
  }
8222
10990
  async function refineListCommand(options = {}) {
8223
- requireProFeature2();
10991
+ requireProFeature3();
8224
10992
  const { records } = readRefinementsStore();
8225
10993
  let rows = records;
8226
10994
  if (options.skill) rows = rows.filter((r) => r.skill === options.skill);
8227
- if (options.project) rows = rows.filter((r) => r.project === options.project || r.projectRoot === resolvePath12(options.project));
10995
+ if (options.project) rows = rows.filter((r) => r.project === options.project || r.projectRoot === resolvePath13(options.project));
8228
10996
  if (options.status) {
8229
10997
  if (!isRefinementStatus(options.status)) {
8230
10998
  throw new UserError("--status must be one of: pending, applied, generalized");
@@ -8232,7 +11000,7 @@ async function refineListCommand(options = {}) {
8232
11000
  rows = rows.filter((r) => r.status === options.status);
8233
11001
  }
8234
11002
  const filtered = Boolean(options.skill || options.project || options.status);
8235
- emit(options.json, { records: rows }, () => {
11003
+ emit2(options.json, { records: rows }, () => {
8236
11004
  if (rows.length === 0) {
8237
11005
  if (filtered && records.length > 0) {
8238
11006
  console.log(`[skill-forge] no refinements match the given filter(s) (${records.length} total recorded).`);
@@ -8250,7 +11018,7 @@ async function refineListCommand(options = {}) {
8250
11018
  });
8251
11019
  }
8252
11020
  async function refinePatternsCommand(options = {}) {
8253
- requireProFeature2();
11021
+ requireProFeature3();
8254
11022
  const { records } = readPatternsStore();
8255
11023
  let rows = records;
8256
11024
  if (options.status) {
@@ -8261,11 +11029,11 @@ async function refinePatternsCommand(options = {}) {
8261
11029
  }
8262
11030
  if (options.skill) rows = rows.filter((p) => p.affectedSkills.includes(options.skill));
8263
11031
  if (options.project) {
8264
- const resolvedProject = resolvePath12(options.project);
11032
+ const resolvedProject = resolvePath13(options.project);
8265
11033
  rows = rows.filter((p) => p.occurrences.some((o) => o.projectRealpath === resolvedProject));
8266
11034
  }
8267
11035
  const filtered = Boolean(options.skill || options.project || options.status);
8268
- emit(options.json, { records: rows.map((p) => ({ ...p, count: patternCount(p) })) }, () => {
11036
+ emit2(options.json, { records: rows.map((p) => ({ ...p, count: patternCount(p) })) }, () => {
8269
11037
  if (rows.length === 0) {
8270
11038
  if (filtered && records.length > 0) {
8271
11039
  console.log(`[skill-forge] no patterns match the given filter(s) (${records.length} total recorded).`);
@@ -8294,13 +11062,13 @@ function renderInventoryLine(row) {
8294
11062
  return ` [${row.scope}] ${row.dir}${row.dirExists ? "" : " (missing)"} \u2014 ${present.length > 0 ? present.join(", ") : "(no override files)"}`;
8295
11063
  }
8296
11064
  async function refineWhichCommand(skill, options = {}) {
8297
- requireProFeature2();
11065
+ requireProFeature3();
8298
11066
  if (!skill) throw new UserError("a skill name is required: skill-forge refine which <skill>");
8299
11067
  const config = loadConfig();
8300
11068
  const cwd = process.cwd();
8301
11069
  const scopes = resolveScopes(config, cwd, skill, options.userRoot);
8302
11070
  const inventory = inventoryAllScopes(scopes);
8303
- emit(options.json, { scopes, inventory }, () => {
11071
+ emit2(options.json, { scopes, inventory }, () => {
8304
11072
  console.log(`[skill-forge] refine which "${sanitizeForMarkdown(skill)}" \u2014 resolution order (highest precedence first):`);
8305
11073
  for (const row of inventory) console.log(renderInventoryLine(row));
8306
11074
  if (!scopes.user.resolved) {
@@ -8312,16 +11080,16 @@ async function refineWhichCommand(skill, options = {}) {
8312
11080
  }
8313
11081
  function writePostWriteSidecar(backupId, files) {
8314
11082
  const sidecar = { schemaVersion: 1, files };
8315
- const dir = join30(getBackupsDir(), backupId);
8316
- mkdirSync9(dir, { recursive: true });
8317
- writeFileSync9(join30(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
11083
+ const dir = join35(getBackupsDir(), backupId);
11084
+ mkdirSync11(dir, { recursive: true });
11085
+ writeFileSync10(join35(dir, "post.json"), JSON.stringify(sidecar, null, 2) + "\n", { mode: 384 });
8318
11086
  }
8319
11087
  function readPostWriteSidecar(backupId) {
8320
- const path = join30(getBackupsDir(), backupId, "post.json");
8321
- if (!existsSync27(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
11088
+ const path = join35(getBackupsDir(), backupId, "post.json");
11089
+ if (!existsSync29(path)) return { hashes: /* @__PURE__ */ new Map(), status: "missing" };
8322
11090
  let raw;
8323
11091
  try {
8324
- raw = readFileSync20(path, "utf-8");
11092
+ raw = readFileSync24(path, "utf-8");
8325
11093
  } catch {
8326
11094
  return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
8327
11095
  }
@@ -8340,7 +11108,7 @@ function readPostWriteSidecar(backupId) {
8340
11108
  return { hashes, status: "ok" };
8341
11109
  }
8342
11110
  async function refinePromoteCommand(patternId, options = {}) {
8343
- requireProFeature2();
11111
+ requireProFeature3();
8344
11112
  if (!patternId) throw new UserError("a pattern id is required: skill-forge refine promote <pattern-id>");
8345
11113
  const patternsStore = readPatternsStore();
8346
11114
  const pattern = patternsStore.records.find((p) => p.id === patternId);
@@ -8366,21 +11134,21 @@ async function refinePromoteCommand(patternId, options = {}) {
8366
11134
  const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
8367
11135
  let realUserRoot;
8368
11136
  try {
8369
- realUserRoot = realpathSync9(resolvedUserRoot);
11137
+ realUserRoot = realpathSync10(resolvedUserRoot);
8370
11138
  } catch {
8371
11139
  realUserRoot = resolvedUserRoot;
8372
11140
  }
8373
11141
  let realSkillDir;
8374
11142
  try {
8375
- realSkillDir = realpathSync9(skillDir);
11143
+ realSkillDir = realpathSync10(skillDir);
8376
11144
  } catch {
8377
- realSkillDir = resolvePath12(skillDir);
11145
+ realSkillDir = resolvePath13(skillDir);
8378
11146
  }
8379
11147
  if (!isUnderRoot3(realSkillDir, realUserRoot)) {
8380
11148
  throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
8381
11149
  }
8382
- const skillMdPath = join30(skillDir, "SKILL.md");
8383
- if (!existsSync27(skillMdPath)) {
11150
+ const skillMdPath = join35(skillDir, "SKILL.md");
11151
+ if (!existsSync29(skillMdPath)) {
8384
11152
  throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
8385
11153
  }
8386
11154
  const refinementsStore = readRefinementsStore();
@@ -8397,11 +11165,11 @@ async function refinePromoteCommand(patternId, options = {}) {
8397
11165
  `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.`
8398
11166
  );
8399
11167
  }
8400
- const before = readFileSync20(skillMdPath, "utf-8");
11168
+ const before = readFileSync24(skillMdPath, "utf-8");
8401
11169
  const after = applyPatchBlocks(before, [block]);
8402
11170
  const diff = previewDiff(before, after, skillMdPath);
8403
11171
  if (options.dryRun) {
8404
- emit(options.json, { dryRun: true, path: skillMdPath, diff }, () => {
11172
+ emit2(options.json, { dryRun: true, path: skillMdPath, diff }, () => {
8405
11173
  console.log(`[skill-forge] refine promote ${patternId} -> ${skillMdPath}`);
8406
11174
  console.log(sanitizeDiffForConsole(diff));
8407
11175
  });
@@ -8414,7 +11182,7 @@ async function refinePromoteCommand(patternId, options = {}) {
8414
11182
  const proceed = await confirmWrite(options.yes, options.json, `
8415
11183
  Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
8416
11184
  if (!proceed) {
8417
- emit(options.json, { promoted: false, cancelled: true }, () => {
11185
+ emit2(options.json, { promoted: false, cancelled: true }, () => {
8418
11186
  console.log("[skill-forge] promote cancelled \u2014 nothing written.");
8419
11187
  });
8420
11188
  return;
@@ -8422,14 +11190,14 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
8422
11190
  const backup = createBackup([skillMdPath]);
8423
11191
  const tempPath = `${skillMdPath}.tmp-${randomUUID5()}`;
8424
11192
  try {
8425
- writeFileSync9(tempPath, after, "utf-8");
11193
+ writeFileSync10(tempPath, after, "utf-8");
8426
11194
  renameSync6(tempPath, skillMdPath);
8427
11195
  } catch (err) {
8428
11196
  rmSync9(tempPath, { force: true });
8429
11197
  throw err;
8430
11198
  }
8431
11199
  try {
8432
- const postSha256 = createHash6("sha256").update(readFileSync20(skillMdPath)).digest("hex");
11200
+ const postSha256 = createHash8("sha256").update(readFileSync24(skillMdPath)).digest("hex");
8433
11201
  writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
8434
11202
  markGeneralized(patternId, backup.id);
8435
11203
  const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
@@ -8449,31 +11217,31 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
8449
11217
  `the user-scope skill was updated at ${skillMdPath} (backup ${backup.id}), but recording post-write verification and/or pattern/provenance state failed: ${err.message}`
8450
11218
  );
8451
11219
  }
8452
- emit(options.json, { promoted: true, path: skillMdPath, backupId: backup.id }, () => {
11220
+ emit2(options.json, { promoted: true, path: skillMdPath, backupId: backup.id }, () => {
8453
11221
  console.log(`[skill-forge] promoted pattern ${patternId} -> ${skillMdPath} (backup: ${backup.id})`);
8454
11222
  });
8455
11223
  }
8456
11224
  function assertBackupDirContained(backupId) {
8457
11225
  const backupsDir = getBackupsDir();
8458
- if (!existsSync27(backupsDir)) return;
8459
- const realBackupsDir = realpathSync9(backupsDir);
8460
- const backupDir = join30(backupsDir, backupId);
8461
- if (!existsSync27(backupDir)) return;
8462
- const realBackupDir = realpathSync9(backupDir);
11226
+ if (!existsSync29(backupsDir)) return;
11227
+ const realBackupsDir = realpathSync10(backupsDir);
11228
+ const backupDir = join35(backupsDir, backupId);
11229
+ if (!existsSync29(backupDir)) return;
11230
+ const realBackupDir = realpathSync10(backupDir);
8463
11231
  if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
8464
11232
  throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
8465
11233
  }
8466
11234
  }
8467
11235
  function resolveTargetRealPath(path) {
8468
- let lexicalAncestor = resolvePath12(path);
11236
+ let lexicalAncestor = resolvePath13(path);
8469
11237
  while (true) {
8470
11238
  try {
8471
- const real = realpathSync9(lexicalAncestor);
8472
- const remainder = relative4(lexicalAncestor, resolvePath12(path));
8473
- return remainder === "" ? real : join30(real, remainder);
11239
+ const real = realpathSync10(lexicalAncestor);
11240
+ const remainder = relative7(lexicalAncestor, resolvePath13(path));
11241
+ return remainder === "" ? real : join35(real, remainder);
8474
11242
  } catch {
8475
- const parent = dirname14(lexicalAncestor);
8476
- if (parent === lexicalAncestor) return resolvePath12(path);
11243
+ const parent = dirname15(lexicalAncestor);
11244
+ if (parent === lexicalAncestor) return resolvePath13(path);
8477
11245
  lexicalAncestor = parent;
8478
11246
  }
8479
11247
  }
@@ -8485,7 +11253,7 @@ function assertManifestPathsWithinRoots(manifest, config) {
8485
11253
  const roots = config.skillsRoots.map((root) => {
8486
11254
  const resolved = resolveConfiguredRoot3(root);
8487
11255
  try {
8488
- return realpathSync9(resolved);
11256
+ return realpathSync10(resolved);
8489
11257
  } catch {
8490
11258
  return resolved;
8491
11259
  }
@@ -8502,7 +11270,7 @@ function assertNoSymlinkLeaves(manifest) {
8502
11270
  for (const file of manifest.files) {
8503
11271
  let lst;
8504
11272
  try {
8505
- lst = lstatSync4(file.path);
11273
+ lst = lstatSync7(file.path);
8506
11274
  } catch {
8507
11275
  lst = void 0;
8508
11276
  }
@@ -8512,14 +11280,14 @@ function assertNoSymlinkLeaves(manifest) {
8512
11280
  }
8513
11281
  }
8514
11282
  function findLexicalRoot(filePath, config) {
8515
- const lexicalTarget = resolvePath12(filePath);
11283
+ const lexicalTarget = resolvePath13(filePath);
8516
11284
  for (const root of config.skillsRoots) {
8517
11285
  const rootLexical = resolveConfiguredRoot3(root);
8518
- const rel = relative4(rootLexical, lexicalTarget);
8519
- if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute5(rel))) continue;
11286
+ const rel = relative7(rootLexical, lexicalTarget);
11287
+ if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep6}`) || isAbsolute8(rel))) continue;
8520
11288
  let rootReal;
8521
11289
  try {
8522
- rootReal = realpathSync9(rootLexical);
11290
+ rootReal = realpathSync10(rootLexical);
8523
11291
  } catch {
8524
11292
  rootReal = rootLexical;
8525
11293
  }
@@ -8529,8 +11297,8 @@ function findLexicalRoot(filePath, config) {
8529
11297
  }
8530
11298
  function walkRealComponents(filePath, config, createMissing) {
8531
11299
  const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
8532
- const relPath = relative4(rootLexical, resolvePath12(filePath));
8533
- const components = relPath.split(sep4).filter((c) => c.length > 0);
11300
+ const relPath = relative7(rootLexical, resolvePath13(filePath));
11301
+ const components = relPath.split(sep6).filter((c) => c.length > 0);
8534
11302
  if (components.length === 0) {
8535
11303
  throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
8536
11304
  }
@@ -8539,10 +11307,10 @@ function walkRealComponents(filePath, config, createMissing) {
8539
11307
  let current = rootReal;
8540
11308
  for (const component of dirComponents) {
8541
11309
  if (current === void 0) break;
8542
- const next = join30(current, component);
11310
+ const next = join35(current, component);
8543
11311
  let lst;
8544
11312
  try {
8545
- lst = lstatSync4(next);
11313
+ lst = lstatSync7(next);
8546
11314
  } catch {
8547
11315
  lst = void 0;
8548
11316
  }
@@ -8551,7 +11319,7 @@ function walkRealComponents(filePath, config, createMissing) {
8551
11319
  current = void 0;
8552
11320
  continue;
8553
11321
  }
8554
- mkdirSync9(next);
11322
+ mkdirSync11(next);
8555
11323
  current = next;
8556
11324
  continue;
8557
11325
  }
@@ -8570,13 +11338,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
8570
11338
  if (parentReal === void 0) {
8571
11339
  throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
8572
11340
  }
8573
- const finalPath = join30(parentReal, leafComponent);
11341
+ const finalPath = join35(parentReal, leafComponent);
8574
11342
  if (!isUnderRoot3(finalPath, rootReal)) {
8575
11343
  throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
8576
11344
  }
8577
11345
  let leafLst;
8578
11346
  try {
8579
- leafLst = lstatSync4(finalPath);
11347
+ leafLst = lstatSync7(finalPath);
8580
11348
  } catch {
8581
11349
  leafLst = void 0;
8582
11350
  }
@@ -8585,8 +11353,8 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
8585
11353
  }
8586
11354
  const tempPath = `${finalPath}.tmp-${randomUUID5()}`;
8587
11355
  try {
8588
- writeFileSync9(tempPath, contentBuffer);
8589
- chmodSync(tempPath, mode);
11356
+ writeFileSync10(tempPath, contentBuffer);
11357
+ chmodSync4(tempPath, mode);
8590
11358
  renameSync6(tempPath, finalPath);
8591
11359
  } catch (err) {
8592
11360
  rmSync9(tempPath, { force: true });
@@ -8596,13 +11364,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
8596
11364
  function safeDeleteTombstone(filePath, config) {
8597
11365
  const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
8598
11366
  if (parentReal === void 0) return;
8599
- const finalPath = join30(parentReal, leafComponent);
11367
+ const finalPath = join35(parentReal, leafComponent);
8600
11368
  if (!isUnderRoot3(finalPath, rootReal)) {
8601
11369
  throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
8602
11370
  }
8603
11371
  let leafLst;
8604
11372
  try {
8605
- leafLst = lstatSync4(finalPath);
11373
+ leafLst = lstatSync7(finalPath);
8606
11374
  } catch {
8607
11375
  leafLst = void 0;
8608
11376
  }
@@ -8613,7 +11381,7 @@ function safeDeleteTombstone(filePath, config) {
8613
11381
  rmSync9(finalPath, { force: true });
8614
11382
  }
8615
11383
  async function refineRollbackCommand(backupId, options = {}) {
8616
- requireProFeature2();
11384
+ requireProFeature3();
8617
11385
  if (!backupId) throw new UserError("a backup id is required: skill-forge refine rollback <backup-id>");
8618
11386
  if (!isValidBackupId(backupId)) {
8619
11387
  throw new UserError(`invalid backup id "${backupId}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
@@ -8638,7 +11406,7 @@ async function refineRollbackCommand(backupId, options = {}) {
8638
11406
  for (const file of manifest.files) {
8639
11407
  const expected = sidecar.hashes.get(file.path);
8640
11408
  if (expected === void 0) continue;
8641
- const currentSha256 = existsSync27(file.path) ? createHash6("sha256").update(readFileSync20(file.path)).digest("hex") : null;
11409
+ const currentSha256 = existsSync29(file.path) ? createHash8("sha256").update(readFileSync24(file.path)).digest("hex") : null;
8642
11410
  if (currentSha256 !== expected) mismatches.push(file.path);
8643
11411
  }
8644
11412
  if (mismatches.length > 0 && !options.force) {
@@ -8657,7 +11425,7 @@ Roll back backup ${backupId}? Affected files:
8657
11425
  ${fileList}
8658
11426
  [y/N] `);
8659
11427
  if (!proceed) {
8660
- emit(options.json, { rolledBack: false, cancelled: true, backupId }, () => {
11428
+ emit2(options.json, { rolledBack: false, cancelled: true, backupId }, () => {
8661
11429
  console.log("[skill-forge] rollback cancelled \u2014 nothing restored.");
8662
11430
  });
8663
11431
  return;
@@ -8686,7 +11454,7 @@ ${fileList}
8686
11454
  pattern.generalizedBackupId = void 0;
8687
11455
  writePatternsStore(patternsStore);
8688
11456
  }
8689
- emit(options.json, { rolledBack: true, backupId, patternReset: pattern?.id, warnings }, () => {
11457
+ emit2(options.json, { rolledBack: true, backupId, patternReset: pattern?.id, warnings }, () => {
8690
11458
  console.log(`[skill-forge] rollback complete for ${backupId}${pattern ? ` (pattern ${pattern.id} reset to ready)` : ""}`);
8691
11459
  for (const w of warnings) console.log(`[skill-forge] warning: ${w}`);
8692
11460
  });
@@ -8729,6 +11497,27 @@ function parsePruneDays(value) {
8729
11497
  }
8730
11498
  return n;
8731
11499
  }
11500
+ function parseInsightAnalysisMode(value) {
11501
+ if (value !== "independent" && value !== "single") {
11502
+ throw new UserError(`--analysis-mode must be "independent" or "single" (got "${value}")`);
11503
+ }
11504
+ return value;
11505
+ }
11506
+ function parseSkillMapCoverage(value) {
11507
+ if (value !== "static" && value !== "resolved") {
11508
+ throw new UserError(`--skill-map-coverage must be "static" or "resolved" (got "${value}")`);
11509
+ }
11510
+ return value;
11511
+ }
11512
+ function parseInsightCloseStatus(value) {
11513
+ if (value !== "dismissed" && value !== "archived") {
11514
+ throw new UserError(`--status must be "dismissed" or "archived" (got "${value}")`);
11515
+ }
11516
+ return value;
11517
+ }
11518
+ function collectValue(value, previous) {
11519
+ return [...previous, value];
11520
+ }
8732
11521
  var program = new Command();
8733
11522
  program.name("skill-forge").description("The supply-chain gate for agent skills").version(pkg.version);
8734
11523
  program.command("add <source>").description("Quarantine-install a skill and run it through the safety gate").option("-y, --yes", "skip the interactive prompt; honor the gate verdict automatically").option("-t, --target <dir>", "skills root to promote into (defaults to config defaultTarget, then skillsRoots[0])").option("--json", "print the gate result as JSON instead of the terminal report").option("--ingest", "after a successful promote, hand off the bundled ingest prompt to a coding agent").option("--artifact <type>", 'gate as a "skill" or an "mcp" server (default: skill)', parseArtifact).option(
@@ -8772,6 +11561,14 @@ program.command("ingest").description(
8772
11561
  ).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
8773
11562
  var queue = program.command("queue").description("Manage the pending-ingestion queue (Pro)");
8774
11563
  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);
11564
+ var insight = program.command("insight").description("Turn untrusted sources into evidence-backed capability plans and Jira manifests (Pro)");
11565
+ insight.command("analyze [source...]").description("Register and fingerprint sources in a private study; never fetches URLs or mutates capabilities").option("--manifest <file>", "JSON source manifest (an array or { sources: [...] })").option("--skill-map <file>", "optional resolved/static skill map used for capability coverage").option("--skill-map-coverage <coverage>", "declare supplied map provenance: static|resolved", parseSkillMapCoverage).option("--analysis-mode <mode>", "requested evidence mode: independent|single (default: independent)", parseInsightAnalysisMode).option("--retain-source-copies", "copy observed local source files into the private study workspace").option("--handoff", "launch the configured agent with the bundled source-insight workflow").option("--json", "print the created study summary as one JSON document").action(insightAnalyze);
11566
+ insight.command("submit <study-id>").description("Validate and atomically import agent-produced analysis (cannot approve the study)").requiredOption("--file <analysis.json>", "candidate analysis JSON file").option("--json", "print the updated study summary as one JSON document").action(insightSubmit);
11567
+ insight.command("review <study-id>").description("Validate and render the current evidence and domain plans; read-only").option("--json", "print the validation/report payload as one JSON document").action(insightReview);
11568
+ insight.command("approve <study-id>").description("Human-only TTY approval of the exact plan hash; agents must never invoke this command").option("--domain <id>", "approve one domain ID (repeatable; default: every domain)", collectValue, []).action(insightApprove);
11569
+ insight.command("jira <study-id>").description("Generate a Jira manifest or manage its private local receipts; never calls Jira").option("--project <key>", "destination Jira project key recorded in a new manifest").option("--out <file>", "write an additional export-safe manifest here (refuses an existing file)").option("--handoff", "launch the configured agent to present and confirm the backlog before Jira writes").option("--receipt <file>", "append one validated local receipt for the current manifest").option("--resume", "show local resume state for the current manifest").option("--json", "print the selected mode result as one JSON document").action(insightJira);
11570
+ insight.command("status <study-id>").description("Show private study lifecycle and evidence counts; read-only").option("--json", "print the status payload as one JSON document").action(insightStatus);
11571
+ insight.command("close <study-id>").description("Close a study while preserving its append-only receipts").requiredOption("--status <status>", "dismissed|archived", parseInsightCloseStatus).action(insightClose);
8775
11572
  var refine = program.command("refine").description("Capture, apply, and generalize improvements to installed skills from usage feedback (Pro)");
8776
11573
  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);
8777
11574
  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);