@rhize/skill-forge 0.13.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
  }
@@ -3505,18 +3563,23 @@ function nodeText(map, node) {
3505
3563
  }
3506
3564
  function scanMapOverlap(candidate, map) {
3507
3565
  const candidateTokens = tokenize2(`${candidate.name ?? ""} ${candidate.description ?? ""}`);
3508
- if (candidateTokens.size === 0) return { matches: [] };
3566
+ if (candidateTokens.size === 0) return { matches: [], compared: 0 };
3509
3567
  const matches = [];
3568
+ let compared = 0;
3569
+ let topMatch;
3510
3570
  for (const node of map.nodes) {
3511
3571
  if (node.type !== "skill") continue;
3572
+ compared += 1;
3512
3573
  const otherTokens = tokenize2(nodeText(map, node));
3513
3574
  const s = score3(candidateTokens, otherTokens);
3575
+ const match = { skillId: node.id, name: node.name ?? node.id, score: s };
3576
+ if (!topMatch || s > topMatch.score) topMatch = match;
3514
3577
  if (s >= REPORT_THRESHOLD) {
3515
- matches.push({ skillId: node.id, name: node.name ?? node.id, score: s });
3578
+ matches.push(match);
3516
3579
  }
3517
3580
  }
3518
3581
  matches.sort((a, b) => b.score - a.score);
3519
- return { matches };
3582
+ return { matches, compared, topMatch };
3520
3583
  }
3521
3584
  function mapOverlapFinding(result) {
3522
3585
  const top = result.matches[0];
@@ -3569,9 +3632,9 @@ function renderListField(labelText, items, placeholder) {
3569
3632
  if (!items || items.length === 0) {
3570
3633
  return `${head}${placeholder}`;
3571
3634
  }
3572
- const bullets = items.map((item) => ` - ${item}`).join("\n");
3635
+ const bullets2 = items.map((item) => ` - ${item}`).join("\n");
3573
3636
  return `${head.trimEnd()}
3574
- ${bullets}`;
3637
+ ${bullets2}`;
3575
3638
  }
3576
3639
  function renderSafetyLine(safety) {
3577
3640
  const detail = [safety.severity, safety.score !== void 0 ? `score ${safety.score}` : void 0].filter((v) => Boolean(v)).join(", ");
@@ -3608,6 +3671,17 @@ function renderCapabilitiesLine(profile) {
3608
3671
  );
3609
3672
  return `${padLabel("Capabilities")}: ${counts} (declaredConfidence: ${declaredConfidence})`;
3610
3673
  }
3674
+ function renderMapOverlapLines(report) {
3675
+ if (!report || report.status === "not-requested") return [];
3676
+ if (report.status === "skipped") {
3677
+ return [`${padLabel("Marketplace overlap")}: skipped \u2014 ${report.reason ?? "map unavailable"}`];
3678
+ }
3679
+ const top = report.topMatch;
3680
+ const flagged = report.matches?.length ?? 0;
3681
+ const closest = top ? `closest "${top.name}" (${top.score})` : "no comparable skills";
3682
+ const verdict = flagged > 0 ? `${flagged} at/over threshold` : "none over threshold";
3683
+ return [`${padLabel("Marketplace overlap")}: checked ${report.compared ?? 0} skills \u2014 ${closest}, ${verdict}`];
3684
+ }
3611
3685
  function renderReport(result) {
3612
3686
  const { profile, overlap, overlapLocked, safety, worthTaking, leaveBehind } = result;
3613
3687
  const { name, version, license } = profileHeaderFields(profile);
@@ -3622,6 +3696,7 @@ function renderReport(result) {
3622
3696
  `${padLabel("Artifact type")}: ${profile.artifactType}`,
3623
3697
  ...profile.kind === "mcp" ? [renderCapabilitiesLine(profile)] : [],
3624
3698
  ...overlapLines,
3699
+ ...renderMapOverlapLines(result.mapOverlap),
3625
3700
  renderListField("What's worth taking", worthTaking, "(pending \u2014 not yet determined)"),
3626
3701
  renderListField("What to leave behind", leaveBehind, "(pending \u2014 not yet determined)"),
3627
3702
  `${padLabel("Safety verdict")}: ${renderSafetyLine(safety)}`,
@@ -3676,13 +3751,23 @@ function tryMcpOverlap(profile, mcpTargets) {
3676
3751
  }
3677
3752
  }
3678
3753
  function tryMapOverlapDecision(skillMapPath, candidate, extendsDeclarations) {
3679
- if (!skillMapPath) return void 0;
3754
+ if (!skillMapPath) return { report: { status: "not-requested" } };
3680
3755
  try {
3681
3756
  const map = loadSkillMap(skillMapPath);
3682
- return mapOverlapDecision(scanMapOverlap(candidate, map), extendsDeclarations);
3757
+ const result = scanMapOverlap(candidate, map);
3758
+ return {
3759
+ decision: mapOverlapDecision(result, extendsDeclarations),
3760
+ report: {
3761
+ status: "checked",
3762
+ compared: result.compared,
3763
+ topMatch: result.topMatch,
3764
+ matches: result.matches
3765
+ }
3766
+ };
3683
3767
  } catch (err) {
3684
- console.error(`[skill-forge] skill-map overlap check skipped: ${err.message}`);
3685
- return void 0;
3768
+ const reason = err.message;
3769
+ console.error(`[skill-forge] skill-map overlap check skipped: ${reason}`);
3770
+ return { report: { status: "skipped", reason } };
3686
3771
  }
3687
3772
  }
3688
3773
  function resolveMcpCandidatePath(entry) {
@@ -3705,7 +3790,7 @@ async function runGate(candidatePath, skillsRoot, {
3705
3790
  const proStatus = proFeatureStatus(proFeatureLabel);
3706
3791
  const safety = artifact === "mcp" ? await mcpScan(candidatePath, strictness) : await scanSafety(candidatePath, strictness);
3707
3792
  const overlap = !proStatus.enabled ? void 0 : artifact === "mcp" ? tryMcpOverlap(profile, mcpTargets) : tryOverlap(candidatePath, skillsRoot);
3708
- const mapDecision = artifact === "mcp" ? void 0 : tryMapOverlapDecision(
3793
+ const mapOutcome = artifact === "mcp" ? { decision: void 0, report: { status: "not-requested" } } : tryMapOverlapDecision(
3709
3794
  skillMapPath,
3710
3795
  {
3711
3796
  name: "name" in profile ? profile.name : void 0,
@@ -3713,6 +3798,7 @@ async function runGate(candidatePath, skillsRoot, {
3713
3798
  },
3714
3799
  profile.kind === "skill" ? profile.extends : []
3715
3800
  );
3801
+ const mapDecision = mapOutcome.decision;
3716
3802
  const mapFinding = mapDecision?.finding;
3717
3803
  const combinedSafety = mapFinding ? { verdict: verdictFor([...safety.findings, mapFinding], strictness), findings: [...safety.findings, mapFinding] } : safety;
3718
3804
  const advisory = formatAuditAdvisory(readAuditState(), {
@@ -3728,6 +3814,7 @@ async function runGate(candidatePath, skillsRoot, {
3728
3814
  overlap,
3729
3815
  overlapLocked: !proStatus.enabled,
3730
3816
  notices: notices.length > 0 ? notices : void 0,
3817
+ mapOverlap: mapOutcome.report,
3731
3818
  safety: {
3732
3819
  verdict: SAFETY_VERDICT_MAP[combinedSafety.verdict],
3733
3820
  findings: mapSafetyFindings(combinedSafety.findings)
@@ -3807,11 +3894,14 @@ function generateId(existingIds) {
3807
3894
  // src/handoff.ts
3808
3895
  import { spawnSync } from "child_process";
3809
3896
  import { existsSync as existsSync15 } from "fs";
3810
- import { dirname as dirname7, join as join18 } from "path";
3897
+ import { dirname as dirname7, isAbsolute as isAbsolute2, join as join18, win32 } from "path";
3811
3898
  import { fileURLToPath } from "url";
3812
3899
  var KNOWN_AGENT_BINARIES = ["claude", "codex", "cursor-agent", "windsurf", "opencode", "gemini"];
3813
3900
  var HANDOFF_INSTRUCTION = "Read {prompt} and follow its instructions for the skill installed at {path}";
3814
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
+ }
3815
3905
  const here = dirname7(fileURLToPath(import.meta.url));
3816
3906
  const builtCandidate = join18(here, assetName);
3817
3907
  if (existsSync15(builtCandidate)) return builtCandidate;
@@ -3828,29 +3918,35 @@ function substitute(template, vars) {
3828
3918
  function buildHandoffCommand(config, installedPath, deps = {}) {
3829
3919
  const isOnPath = deps.isOnPath ?? defaultIsOnPath;
3830
3920
  const promptPath = resolvePromptPath(deps.promptAsset);
3831
- 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 };
3832
3924
  if (config.handoffCommand && config.handoffCommand.length > 0) {
3833
- 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" };
3834
3929
  }
3835
3930
  for (const bin of KNOWN_AGENT_BINARIES) {
3836
3931
  if (isOnPath(bin)) {
3837
- 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" };
3838
3934
  }
3839
3935
  }
3840
3936
  return { argv: [], source: "none" };
3841
3937
  }
3842
- function printOnlyMessage(installedPath, promptAsset) {
3938
+ function printOnlyMessage(installedPath, promptAsset, targetLabel = "Skill path") {
3843
3939
  const promptPath = resolvePromptPath(promptAsset);
3844
3940
  return `[skill-forge] no agent handoff available \u2014 run one yourself:
3845
3941
  Prompt: ${promptPath}
3846
- Skill path: ${installedPath}`;
3942
+ ${targetLabel}: ${installedPath}`;
3847
3943
  }
3848
3944
  function runHandoff(config, installedPath, options = {}) {
3849
3945
  const command = buildHandoffCommand(config, installedPath, options);
3850
3946
  const spawn2 = options.spawn ?? spawnSync;
3851
3947
  const log = options.log ?? console.log;
3852
3948
  if (command.source === "none" || command.argv.length === 0) {
3853
- const printed = printOnlyMessage(installedPath, options.promptAsset);
3949
+ const printed = printOnlyMessage(installedPath, options.promptAsset, options.targetLabel);
3854
3950
  log(printed);
3855
3951
  return { ran: false, command, printed };
3856
3952
  }
@@ -3858,7 +3954,7 @@ function runHandoff(config, installedPath, options = {}) {
3858
3954
  const result = spawn2(bin, args, { stdio: options.stdio ?? "inherit" });
3859
3955
  const error = result.error;
3860
3956
  if (error?.code === "ENOENT") {
3861
- const printed = printOnlyMessage(installedPath, options.promptAsset);
3957
+ const printed = printOnlyMessage(installedPath, options.promptAsset, options.targetLabel);
3862
3958
  log(`[skill-forge] ${bin} not found on PATH \u2014 run this yourself:`);
3863
3959
  log(printed);
3864
3960
  return { ran: false, command, printed, error: error.message };
@@ -4121,6 +4217,136 @@ var AGENTS = [
4121
4217
  { id: "adal", displayName: "AdaL", projectRoot: ".adal/skills", globalRoot: "~/.adal/skills" }
4122
4218
  ].map(makeAgent);
4123
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
+
4124
4350
  // src/commands/audit.ts
4125
4351
  import { existsSync as existsSync16 } from "fs";
4126
4352
  import { join as join20, resolve as resolvePath3 } from "path";
@@ -4364,6 +4590,15 @@ function presenceNote(presence) {
4364
4590
  if (presence === "absent") return " [CLI not on PATH]";
4365
4591
  return "";
4366
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
+ }
4367
4602
  function printMcpDetectionResults(detectedMcp) {
4368
4603
  if (detectedMcp.length === 0) return;
4369
4604
  console.log("\n[skill-forge] init \u2014 MCP config detection");
@@ -4393,13 +4628,17 @@ function buildConfig(selected, defaultTargetEntry, handoffAgent, selectedMcp = [
4393
4628
  mcpTargets: selectedMcp.length > 0 ? selectedMcp.map((m) => m.path) : existing.mcpTargets
4394
4629
  };
4395
4630
  }
4396
- function printSummary(config) {
4631
+ async function printSummary(config, deps = {}) {
4397
4632
  console.log(`
4398
4633
  [skill-forge] wrote config to ${getConfigPath()}`);
4399
4634
  console.log(` gate targets: ${config.skillsRoots.join(", ")}`);
4400
4635
  if (config.defaultTarget) console.log(` default target: ${config.defaultTarget}`);
4401
4636
  if (config.handoffCommand?.length) console.log(` handoff command: ${config.handoffCommand.join(" ")}`);
4402
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
+ }
4403
4642
  }
4404
4643
  function isPromptAbort(err) {
4405
4644
  return err instanceof Error && (err.name === "AbortError" || err.code === "ABORT_ERR");
@@ -4515,8 +4754,41 @@ async function promptHandoffAgent(rl, detected, narrow) {
4515
4754
  const idx = await promptSingleChoice(rl, `Choose [1-${ordered.length}] or Enter to skip: `, ordered.length, "skip");
4516
4755
  return idx === void 0 ? void 0 : ordered[idx].agent;
4517
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
+ }
4518
4789
  async function init(options = {}, deps = {}) {
4519
4790
  const runAudit = deps.runAudit ?? auditCommand;
4791
+ const gitDeps = deps.gitExec ? { exec: deps.gitExec } : {};
4520
4792
  const cwd = options.cwd ?? process.cwd();
4521
4793
  const home = options.home ?? homedir4();
4522
4794
  const detected = detectAgents(cwd, home, options.pathEnv);
@@ -4524,6 +4796,7 @@ async function init(options = {}, deps = {}) {
4524
4796
  if (options.list) {
4525
4797
  printDetectionResults(detected);
4526
4798
  printMcpDetectionResults(detectedMcp);
4799
+ await printGitDetectionColumn(detected, gitDeps);
4527
4800
  return;
4528
4801
  }
4529
4802
  if (detected.length === 0 && detectedMcp.length === 0) {
@@ -4545,7 +4818,7 @@ async function init(options = {}, deps = {}) {
4545
4818
  const selectedMcpTargets = narrow ? mcpGroups.filter(hasInstalledMember) : mcpGroups;
4546
4819
  const config = buildConfig(selectedRoots, selectedRoots[0], void 0, selectedMcpTargets);
4547
4820
  saveConfig(config);
4548
- printSummary(config);
4821
+ await printSummary(config, gitDeps);
4549
4822
  await runAudit({ yes: true });
4550
4823
  return;
4551
4824
  }
@@ -4566,9 +4839,10 @@ async function init(options = {}, deps = {}) {
4566
4839
  }
4567
4840
  const target = selected.length > 0 ? await promptDefaultTarget(rl, selected) : void 0;
4568
4841
  const handoffAgent = detected.length > 0 ? await promptHandoffAgent(rl, detected, narrow) : void 0;
4842
+ await promptGitPreflight(rl, selected, gitDeps);
4569
4843
  const config = buildConfig(selected, target, handoffAgent, selectedMcp);
4570
4844
  saveConfig(config);
4571
- printSummary(config);
4845
+ await printSummary(config, gitDeps);
4572
4846
  if (options.offerAudit !== false) {
4573
4847
  const answer = (await rl.question("\nRun the skills & MCP audit now? [Y/n] ")).trim().toLowerCase();
4574
4848
  runAuditNow = answer === "" || answer === "y" || answer === "yes";
@@ -4867,7 +5141,7 @@ import { existsSync as existsSync19 } from "fs";
4867
5141
 
4868
5142
  // src/commands/held.ts
4869
5143
  import { existsSync as existsSync18, realpathSync as realpathSync3 } from "fs";
4870
- 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";
4871
5145
  var PRO_FEATURE_LABEL2 = "overlap analysis, the provenance ledger, and the pending-ingestion queue";
4872
5146
  function resolveHeldPath(quarantineDir, id) {
4873
5147
  if (id === "" || id !== basename9(id) || id === "." || id === "..") {
@@ -4879,7 +5153,7 @@ function resolveHeldPath(quarantineDir, id) {
4879
5153
  }
4880
5154
  const realRoot = realpathSync3(resolvePath4(quarantineDir));
4881
5155
  const realCandidate = realpathSync3(candidate);
4882
- if (!realCandidate.startsWith(realRoot + sep2)) {
5156
+ if (!realCandidate.startsWith(realRoot + sep3)) {
4883
5157
  throw new UserError(`refusing to act on "${id}": it resolves outside the quarantine dir (${realRoot})`);
4884
5158
  }
4885
5159
  return realCandidate;
@@ -5407,6 +5681,10 @@ async function status() {
5407
5681
  if (config.mcpTargets?.length) console.log(` mcpTargets: ${config.mcpTargets.join(", ")}`);
5408
5682
  console.log(` quarantineDir: ${config.quarantineDir}`);
5409
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
+ }
5410
5688
  console.log(` last audit: ${formatAuditFreshness(readAuditState())}`);
5411
5689
  console.log(
5412
5690
  ` held in quarantine: ${held.readable ? held.count : "UNREADABLE (quarantine dir cannot be listed)"}` + (held.count > 0 ? " (`skill-forge promote <id>` / `reject <id>`)" : "")
@@ -5419,9 +5697,9 @@ async function status() {
5419
5697
  // src/evolve.ts
5420
5698
  import { createInterface as createInterface5 } from "readline/promises";
5421
5699
  import { createHash as createHash3 } from "crypto";
5422
- 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";
5423
5701
  import { tmpdir as tmpdir2 } from "os";
5424
- 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";
5425
5703
  var SKILLOPT_BIN = "skillopt-sleep";
5426
5704
  var PRO_FEATURE_LABEL3 = "self-evolution via SkillOpt-Sleep (evolve) \u2014 re-gating, provenance, and the pending-ingestion queue";
5427
5705
  async function evolve(targetSkillDir, options = {}, deps = {}) {
@@ -5489,7 +5767,7 @@ async function evolve(targetSkillDir, options = {}, deps = {}) {
5489
5767
  }
5490
5768
  const stagingRoot = join24(project, ".skillopt-sleep", "staging");
5491
5769
  const stagingDir = resolvePath5(parsed.staging_dir);
5492
- if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep3)) {
5770
+ if (stagingDir !== stagingRoot && !stagingDir.startsWith(stagingRoot + sep4)) {
5493
5771
  throw new UserError(
5494
5772
  `skillopt-sleep reported a staging_dir outside the expected sandbox (${stagingRoot}): ${stagingDir} \u2014 refusing to use it`
5495
5773
  );
@@ -5708,15 +5986,15 @@ function isAlreadyAdopted(stagingDir, manifest) {
5708
5986
  }
5709
5987
  return sawComparison;
5710
5988
  }
5711
- function assertNotSymlink(path, label) {
5989
+ function assertNotSymlink(path, label2) {
5712
5990
  let stat;
5713
5991
  try {
5714
- stat = lstatSync3(path);
5992
+ stat = lstatSync4(path);
5715
5993
  } catch {
5716
5994
  return;
5717
5995
  }
5718
5996
  if (stat.isSymbolicLink()) {
5719
- 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)`);
5720
5998
  }
5721
5999
  }
5722
6000
  function sha256File(path) {
@@ -6467,7 +6745,7 @@ import { join as join26, resolve as resolvePath8 } from "path";
6467
6745
  // src/gate/skillMapDrift.ts
6468
6746
  import { createHash as createHash4 } from "crypto";
6469
6747
  import { readFileSync as readFileSync18, existsSync as existsSync23 } from "fs";
6470
- 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";
6471
6749
  function sha256(text) {
6472
6750
  return createHash4("sha256").update(text, "utf-8").digest("hex");
6473
6751
  }
@@ -6500,7 +6778,7 @@ function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
6500
6778
  }
6501
6779
  function resolveLocalPath(path, mapDir) {
6502
6780
  const expanded = expandHome(path);
6503
- if (isAbsolute2(expanded)) return resolve7(expanded);
6781
+ if (isAbsolute3(expanded)) return resolve7(expanded);
6504
6782
  const candidates = [resolve7(expanded)];
6505
6783
  if (mapDir) {
6506
6784
  candidates.push(resolve7(mapDir, expanded));
@@ -6913,9 +7191,9 @@ function renderRoutineMarkdown(result, audit2, watch, organize) {
6913
7191
  if (result.housekeeping.length > 0) {
6914
7192
  lines.push("", "## Housekeeping", "");
6915
7193
  for (const action of result.housekeeping) {
6916
- 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";
6917
7195
  lines.push(
6918
- `- ${label} ${action.kind}: ${sanitizeForMarkdown(action.target)} \u2014 ${sanitizeForMarkdown(action.reason)}`
7196
+ `- ${label2} ${action.kind}: ${sanitizeForMarkdown(action.target)} \u2014 ${sanitizeForMarkdown(action.reason)}`
6919
7197
  );
6920
7198
  }
6921
7199
  }
@@ -7031,8 +7309,8 @@ async function routineCommand(options = {}) {
7031
7309
  if (result.organize) console.log(` registry: ${result.organize.skills} skill(s)`);
7032
7310
  console.log(` pending: ${result.pending.queue} queue, ${result.pending.quarantine} quarantine`);
7033
7311
  for (const action of housekeeping) {
7034
- const label = action.outcome === "applied" ? "housekeeping" : action.outcome === "failed" ? "FAILED" : "would do";
7035
- 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}` : ""}`);
7036
7314
  }
7037
7315
  for (const item of result.degraded) console.log(` INCOMPLETE: ${item}`);
7038
7316
  console.log(` report -> ${result.reportPath}`);
@@ -7051,7 +7329,7 @@ async function routineCommand(options = {}) {
7051
7329
 
7052
7330
  // src/commands/ingest.ts
7053
7331
  import { realpathSync as realpathSync6 } from "fs";
7054
- 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";
7055
7333
  var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
7056
7334
  var VALID_STATUSES = ["ingested", "dismissed"];
7057
7335
  function requireProFeature() {
@@ -7098,8 +7376,8 @@ function realpathOrNull(p) {
7098
7376
  }
7099
7377
  }
7100
7378
  function isUnderRoot(candidate, root) {
7101
- const rel = relative2(root, candidate);
7102
- return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
7379
+ const rel = relative3(root, candidate);
7380
+ return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
7103
7381
  }
7104
7382
  function validatePendingEntries(entries, config) {
7105
7383
  const quarantineRoot = realpathOrNull(resolvePath9(config.quarantineDir));
@@ -7175,192 +7453,2711 @@ async function queueClose(id, options = {}) {
7175
7453
  console.log(`[skill-forge] queue entry ${updated.id} closed -> status: ${updated.status}`);
7176
7454
  }
7177
7455
 
7178
- // src/commands/refine.ts
7179
- import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
7180
- 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";
7181
- 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
7182
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";
7183
7467
 
7184
- // src/refine/context.ts
7185
- import { existsSync as existsSync26, realpathSync as realpathSync8, statSync as statSync12 } from "fs";
7186
- 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";
7187
7471
 
7188
- // src/refine/models.ts
7189
- import { randomBytes } from "crypto";
7190
- var REFINEMENT_CATEGORIES = ["trigger", "content", "hook", "tool", "pattern", "config", "new"];
7191
- var OVERRIDE_TYPES = ["patch", "extend", "config", "full", "hook", "script", "new"];
7192
- var PATCH_ACTIONS = ["append", "prepend", "replace-section", "insert-after", "insert-before", "delete-section"];
7193
- var PATTERN_STATUSES = ["tracking", "ready", "generalized", "dismissed"];
7194
- function isRefinementCategory(value) {
7195
- return REFINEMENT_CATEGORIES.includes(value);
7196
- }
7197
- function isOverrideType(value) {
7198
- 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
+ );
7199
7566
  }
7200
- function isPatchAction(value) {
7201
- return PATCH_ACTIONS.includes(value);
7567
+ function insightIdFor(domain, claim, sourceIds) {
7568
+ return stableId("INS", { domain: domain.trim(), claim: claim.trim(), sourceIds: [...sourceIds].sort() });
7202
7569
  }
7203
- function isPatternStatus(value) {
7204
- return PATTERN_STATUSES.includes(value);
7570
+ function domainPlanIdFor(domain, insightIds) {
7571
+ return stableId("DOM", { domain: domain.trim(), insightIds: [...insightIds].sort() });
7205
7572
  }
7206
- var REFINEMENT_STATUSES = ["pending", "applied", "generalized"];
7207
- function isRefinementStatus(value) {
7208
- 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;
7209
7577
  }
7210
- function patternCount(pattern) {
7211
- 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";
7212
7581
  }
7213
- function defaultRandomHex() {
7214
- 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
+ }
7215
7588
  }
7216
- function formatYYYYMMDD(date) {
7217
- const yyyy = date.getUTCFullYear().toString().padStart(4, "0");
7218
- const mm = (date.getUTCMonth() + 1).toString().padStart(2, "0");
7219
- const dd = date.getUTCDate().toString().padStart(2, "0");
7220
- 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 };
7221
7609
  }
7222
- function generateRefinementId(existingIds = /* @__PURE__ */ new Set(), now = /* @__PURE__ */ new Date(), randomHex = defaultRandomHex) {
7223
- const datePart = formatYYYYMMDD(now);
7224
- let id;
7225
- do {
7226
- id = `REF-${datePart}-${randomHex()}`;
7227
- } while (existingIds.has(id));
7228
- 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
+ });
7229
7623
  }
7230
- function generatePatternId(existingIds = /* @__PURE__ */ new Set(), randomHex = defaultRandomHex) {
7231
- let id;
7232
- do {
7233
- id = `PAT-${randomHex()}`;
7234
- } while (existingIds.has(id));
7235
- 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
+ };
7236
7635
  }
7237
- function fingerprintKey(skill, target, overrideType, action) {
7238
- const normalize = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
7239
- 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
+ });
7240
7672
  }
7241
- function contentSimilarity(a, b) {
7242
- const tokenize3 = (s) => new Set(s.toLowerCase().split(/\s+/).filter(Boolean));
7243
- const tokensA = tokenize3(a);
7244
- const tokensB = tokenize3(b);
7245
- if (tokensA.size === 0 || tokensB.size === 0) return 0;
7246
- let intersectionSize = 0;
7247
- for (const token of tokensA) {
7248
- 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`);
7249
7676
  }
7250
- const unionSize3 = tokensA.size + tokensB.size - intersectionSize;
7251
- 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
+ };
7252
7731
  }
7253
7732
 
7254
- // src/refine/store.ts
7255
- import { createHash as createHash5 } from "crypto";
7256
- import { existsSync as existsSync25, readFileSync as readFileSync19, statSync as statSync11, realpathSync as realpathSync7 } from "fs";
7257
- import { join as join28, resolve as resolvePath10 } from "path";
7258
- var SIMILARITY_JOIN_THRESHOLD = 0.7;
7259
- var READY_THRESHOLD = 2;
7260
- function getRefinementsDir() {
7261
- return join28(getHomeDir(), "refinements");
7262
- }
7263
- function getRefinementsPath() {
7264
- 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;
7265
7794
  }
7266
- function getPatternsPath() {
7267
- 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
+ }
7268
7802
  }
7269
- function getBackupsDir() {
7270
- return join28(getRefinementsDir(), "backups");
7803
+ function compareStrings2(a, b) {
7804
+ return a < b ? -1 : a > b ? 1 : 0;
7271
7805
  }
7272
- function readJsonFile(path, label) {
7273
- let raw;
7806
+ function canonicalPublicUrl(raw, label2) {
7807
+ let url;
7274
7808
  try {
7275
- raw = readFileSync19(path, "utf-8");
7276
- } catch (err) {
7277
- 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);
7278
7886
  }
7887
+ if (!bytes || !descriptorStat) throw new UserError(`source file could not be observed: ${relativePath}`);
7888
+ let realPath;
7279
7889
  try {
7280
- 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
+ }
7281
7895
  } catch {
7282
- throw new UserError(`malformed ${label} at ${path}: not valid JSON`);
7283
- }
7284
- }
7285
- function readStore(path, label, validateRecord) {
7286
- if (!existsSync25(path)) {
7287
- return { schemaVersion: 1, records: [] };
7896
+ throw new UserError(`source file changed while it was being observed: ${relativePath}`);
7288
7897
  }
7289
- const parsed = readJsonFile(path, label);
7290
- if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.records)) {
7291
- throw new UserError(`malformed ${label} at ${path}: expected an object with a "records" array`);
7292
- }
7293
- const records = parsed.records;
7294
- if (validateRecord) {
7295
- records.forEach((record, index) => validateRecord(record, index));
7898
+ if (mediaType !== "application/pdf" && bytes.subarray(0, 512).includes(0)) {
7899
+ return { path: relativePath, reason: "binary", byteLength: bytes.byteLength };
7296
7900
  }
7297
- return { schemaVersion: 1, records };
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
+ };
7298
7910
  }
7299
- function validateRefinementRecordShape(record, index, path) {
7300
- if (typeof record !== "object" || record === null) {
7301
- throw new UserError(`malformed refinements.json at ${path}: record[${index}] is not an object`);
7302
- }
7303
- const r = record;
7304
- if (typeof r.id !== "string" || r.id.length === 0) {
7305
- throw new UserError(`malformed refinements.json at ${path}: record[${index}] is missing a valid "id"`);
7306
- }
7307
- if (typeof r.createdAt !== "string" || r.createdAt.length === 0) {
7308
- throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "createdAt"`);
7309
- }
7310
- if (typeof r.status !== "string" || r.status.length === 0) {
7311
- throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "status"`);
7312
- }
7313
- if (typeof r.skill !== "string" || r.skill.length === 0) {
7314
- throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" is missing a valid "skill"`);
7315
- }
7316
- const patchActionsRequiringMarker = /* @__PURE__ */ new Set(["insert-after", "insert-before", "replace-section", "delete-section"]);
7317
- if (r.overrideType === "patch") {
7318
- if (typeof r.patchAction !== "string" || r.patchAction.length === 0) {
7319
- throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchAction"`);
7320
- }
7321
- if (patchActionsRequiringMarker.has(r.patchAction) && (typeof r.patchMarker !== "string" || r.patchMarker.length === 0)) {
7322
- throw new UserError(
7323
- `malformed refinements.json at ${path}: record "${r.id}" has patchAction "${r.patchAction}" but is missing "patchMarker"`
7324
- );
7325
- }
7326
- if (typeof r.patchContent !== "string" || r.patchContent.length === 0) {
7327
- throw new UserError(`malformed refinements.json at ${path}: record "${r.id}" has overrideType "patch" but is missing "patchContent"`);
7328
- }
7329
- } else if (r.patchAction !== void 0) {
7330
- throw new UserError(
7331
- `malformed refinements.json at ${path}: record "${r.id}" has overrideType "${String(r.overrideType)}" but carries "patchAction" \u2014 only valid when overrideType is "patch"`
7332
- );
7333
- }
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
+ };
7334
7925
  }
7335
- function validatePatternRecordShape(record, index, path) {
7336
- if (typeof record !== "object" || record === null) {
7337
- throw new UserError(`malformed patterns.json at ${path}: record[${index}] is not an object`);
7338
- }
7339
- const p = record;
7340
- if (typeof p.id !== "string" || p.id.length === 0) {
7341
- throw new UserError(`malformed patterns.json at ${path}: record[${index}] is missing a valid "id"`);
7342
- }
7343
- if (typeof p.status !== "string" || p.status.length === 0) {
7344
- throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "status"`);
7345
- }
7346
- if (!Array.isArray(p.occurrences)) {
7347
- throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" is missing a valid "occurrences" array`);
7348
- }
7349
- if (p.status === "dismissed" && (typeof p.dismissedReason !== "string" || p.dismissedReason.length === 0)) {
7350
- throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "dismissed" but is missing "dismissedReason"`);
7351
- }
7352
- if (p.status === "generalized") {
7353
- if (typeof p.generalizedAt !== "string" || p.generalizedAt.length === 0) {
7354
- throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedAt"`);
7355
- }
7356
- if (typeof p.generalizedBackupId !== "string" || p.generalizedBackupId.length === 0) {
7357
- throw new UserError(`malformed patterns.json at ${path}: record "${p.id}" has status "generalized" but is missing "generalizedBackupId"`);
7358
- }
7359
- }
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
+ };
7360
7953
  }
7361
- function readRefinementsStore() {
7362
- const path = getRefinementsPath();
7363
- return readStore(path, "refinements.json", (record, index) => validateRefinementRecordShape(record, index, path));
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));
7364
10161
  }
7365
10162
  function writeRefinementsStore(store) {
7366
10163
  writeJsonAtomic(getRefinementsPath(), store);
@@ -7373,9 +10170,9 @@ function writePatternsStore(store) {
7373
10170
  writeJsonAtomic(getPatternsPath(), store);
7374
10171
  }
7375
10172
  function resolveProjectRealpath(projectRoot) {
7376
- const resolved = resolvePath10(projectRoot);
10173
+ const resolved = resolvePath11(projectRoot);
7377
10174
  try {
7378
- return realpathSync7(resolved);
10175
+ return realpathSync8(resolved);
7379
10176
  } catch {
7380
10177
  return resolved;
7381
10178
  }
@@ -7507,16 +10304,16 @@ function createBackup(filePaths, deps = {}) {
7507
10304
  const randomHex = deps.randomHex ?? defaultRandomHex;
7508
10305
  const id = `${formatBackupTimestamp(now)}-${randomHex()}`;
7509
10306
  const files = filePaths.map((path) => {
7510
- if (!existsSync25(path)) {
10307
+ if (!existsSync27(path)) {
7511
10308
  return { path, existed: false, sha256: null, mode: null, contentBase64: null };
7512
10309
  }
7513
- const bytes = readFileSync19(path);
7514
- const mode = statSync11(path).mode & 511;
7515
- 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");
7516
10313
  return { path, existed: true, sha256: sha2562, mode, contentBase64: bytes.toString("base64") };
7517
10314
  });
7518
10315
  const manifest = { id, createdAt: now.toISOString(), files };
7519
- writeJsonAtomic(join28(getBackupsDir(), id, "manifest.json"), manifest);
10316
+ writeJsonAtomic(join33(getBackupsDir(), id, "manifest.json"), manifest);
7520
10317
  return manifest;
7521
10318
  }
7522
10319
  function validateBackupManifestShape(value, path) {
@@ -7565,8 +10362,8 @@ function readBackupManifest(id) {
7565
10362
  if (!isValidBackupId(id)) {
7566
10363
  throw new UserError(`invalid backup id "${id}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
7567
10364
  }
7568
- const manifestPath = join28(getBackupsDir(), id, "manifest.json");
7569
- if (!existsSync25(manifestPath)) {
10365
+ const manifestPath = join33(getBackupsDir(), id, "manifest.json");
10366
+ if (!existsSync27(manifestPath)) {
7570
10367
  throw new UserError(`no backup with id: ${id}`);
7571
10368
  }
7572
10369
  const parsed = readJsonFile(manifestPath, "backup manifest");
@@ -7576,7 +10373,7 @@ function readBackupManifest(id) {
7576
10373
 
7577
10374
  // src/refine/context.ts
7578
10375
  function resolveConfiguredRoot2(root) {
7579
- return resolvePath11(expandHome(root));
10376
+ return resolvePath12(expandHome(root));
7580
10377
  }
7581
10378
  function assertPlainSkillName(name) {
7582
10379
  if (!name || name.trim().length === 0) {
@@ -7588,32 +10385,32 @@ function assertPlainSkillName(name) {
7588
10385
  }
7589
10386
  function realpathOrNull2(p) {
7590
10387
  try {
7591
- return realpathSync8(p);
10388
+ return realpathSync9(p);
7592
10389
  } catch {
7593
10390
  return null;
7594
10391
  }
7595
10392
  }
7596
10393
  function isUnderRoot2(candidate, root) {
7597
- const rel = relative3(root, candidate);
7598
- return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
10394
+ const rel = relative6(root, candidate);
10395
+ return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
7599
10396
  }
7600
10397
  function resolveScopes(config, cwd, skill, userRootFlag) {
7601
10398
  assertPlainSkillName(skill);
7602
- const resolvedCwd = resolvePath11(cwd);
7603
- const localDir = join29(resolvedCwd, ".claude", "skills", skill);
7604
- const sharedDir = join29(resolvedCwd, "skills", skill);
7605
- const local = { scope: "local", dir: localDir, exists: existsSync26(localDir) };
7606
- 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) };
7607
10404
  const user = resolveUserScope(config, resolvedCwd, skill, userRootFlag);
7608
10405
  return { local, shared, user };
7609
10406
  }
7610
10407
  function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
7611
10408
  if (userRootFlag) {
7612
- const root = resolvePath11(userRootFlag);
7613
- 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" };
7614
10411
  }
7615
10412
  const cwdReal = realpathOrNull2(resolvedCwd) ?? resolvedCwd;
7616
- 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) }));
7617
10414
  for (const candidate of candidates) {
7618
10415
  const skillReal = realpathOrNull2(candidate.dir);
7619
10416
  if (skillReal) {
@@ -7631,14 +10428,14 @@ function resolveUserScope(config, resolvedCwd, skill, userRootFlag) {
7631
10428
  }
7632
10429
  function isFile2(p) {
7633
10430
  try {
7634
- return statSync12(p).isFile();
10431
+ return statSync13(p).isFile();
7635
10432
  } catch {
7636
10433
  return false;
7637
10434
  }
7638
10435
  }
7639
10436
  function isDirectory(p) {
7640
10437
  try {
7641
- return statSync12(p).isDirectory();
10438
+ return statSync13(p).isDirectory();
7642
10439
  } catch {
7643
10440
  return false;
7644
10441
  }
@@ -7648,12 +10445,12 @@ function inventoryScopeDir(scope, dir) {
7648
10445
  scope,
7649
10446
  dir,
7650
10447
  dirExists: isDirectory(dir),
7651
- skillMd: isFile2(join29(dir, "SKILL.md")),
7652
- patchMd: isFile2(join29(dir, "SKILL.patch.md")),
7653
- extendMd: isFile2(join29(dir, "SKILL.extend.md")),
7654
- configJson: isFile2(join29(dir, "skill-config.json")),
7655
- hooksDir: isDirectory(join29(dir, "hooks")),
7656
- 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"))
7657
10454
  };
7658
10455
  }
7659
10456
  function inventoryAllScopes(resolution) {
@@ -7886,10 +10683,10 @@ function previewDiff(before, after, path) {
7886
10683
  }
7887
10684
 
7888
10685
  // src/commands/refine.ts
7889
- var PRO_FEATURE_LABEL8 = "skill refinement";
10686
+ var PRO_FEATURE_LABEL9 = "skill refinement";
7890
10687
  var REFINE_PROMPT_ASSET = "refine-prompt.md";
7891
- function requireProFeature2() {
7892
- const status2 = proFeatureStatus(PRO_FEATURE_LABEL8);
10688
+ function requireProFeature3() {
10689
+ const status2 = proFeatureStatus(PRO_FEATURE_LABEL9);
7893
10690
  if (!status2.enabled) {
7894
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.");
7895
10692
  }
@@ -7898,7 +10695,7 @@ function requireProFeature2() {
7898
10695
  function isInteractive(json) {
7899
10696
  return !json && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
7900
10697
  }
7901
- function emit(json, payload, renderText) {
10698
+ function emit2(json, payload, renderText) {
7902
10699
  if (json) {
7903
10700
  console.log(JSON.stringify(payload, null, 2));
7904
10701
  } else {
@@ -7914,7 +10711,7 @@ async function confirmWrite(yes, json, prompt) {
7914
10711
  if (!isInteractive(json)) {
7915
10712
  throw new UserError("refusing to write without confirmation \u2014 pass --yes, or run interactively.");
7916
10713
  }
7917
- const rl = createInterface6({ input: process.stdin, output: process.stdout });
10714
+ const rl = createInterface7({ input: process.stdin, output: process.stdout });
7918
10715
  try {
7919
10716
  return await confirmYesNo(rl, prompt);
7920
10717
  } finally {
@@ -7922,18 +10719,18 @@ async function confirmWrite(yes, json, prompt) {
7922
10719
  }
7923
10720
  }
7924
10721
  function resolveConfiguredRoot3(root) {
7925
- return resolvePath12(expandHome(root));
10722
+ return resolvePath13(expandHome(root));
7926
10723
  }
7927
10724
  function isUnderRoot3(candidate, root) {
7928
- const rel = relative4(root, candidate);
7929
- return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
10725
+ const rel = relative7(root, candidate);
10726
+ return rel === "" || !rel.startsWith("..") && !isAbsolute8(rel);
7930
10727
  }
7931
10728
  function sanitizeDiffForConsole(diff) {
7932
10729
  return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
7933
10730
  }
7934
10731
  function resolveContentOption(options) {
7935
10732
  if (options.content !== void 0) return options.content;
7936
- if (options.contentFile) return readFileSync20(resolvePath12(options.contentFile), "utf-8");
10733
+ if (options.contentFile) return readFileSync24(resolvePath13(options.contentFile), "utf-8");
7937
10734
  return void 0;
7938
10735
  }
7939
10736
  function validateCaptureOptions(options) {
@@ -8059,19 +10856,19 @@ ${rendered}` : rendered;
8059
10856
  function ensureScopeDirSafe(scopeDir, resolvedCwd) {
8060
10857
  let lst;
8061
10858
  try {
8062
- lst = lstatSync4(scopeDir);
10859
+ lst = lstatSync7(scopeDir);
8063
10860
  } catch {
8064
10861
  lst = void 0;
8065
10862
  }
8066
10863
  if (lst?.isSymbolicLink()) {
8067
10864
  throw new UserError(`refusing to write under ${scopeDir}: scope directory is a symlink`);
8068
10865
  }
8069
- mkdirSync9(scopeDir, { recursive: true });
10866
+ mkdirSync11(scopeDir, { recursive: true });
8070
10867
  const real = resolveConfiguredRoot3(scopeDir);
8071
- const lexical = resolvePath12(scopeDir);
10868
+ const lexical = resolvePath13(scopeDir);
8072
10869
  let realResolved;
8073
10870
  try {
8074
- realResolved = realpathSync9(scopeDir);
10871
+ realResolved = realpathSync10(scopeDir);
8075
10872
  } catch {
8076
10873
  realResolved = real;
8077
10874
  }
@@ -8085,28 +10882,28 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
8085
10882
  }
8086
10883
  function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
8087
10884
  const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
8088
- const destPath = join30(scopeDir, relPath);
8089
- const destDir = dirname14(destPath);
8090
- mkdirSync9(destDir, { recursive: true });
8091
- 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);
8092
10889
  if (!isUnderRoot3(realDestDir, realScopeDir)) {
8093
10890
  throw new UserError(`refusing to write ${destPath}: resolves outside scope directory ${scopeDir}`);
8094
10891
  }
8095
10892
  let destLst;
8096
10893
  try {
8097
- destLst = lstatSync4(destPath);
10894
+ destLst = lstatSync7(destPath);
8098
10895
  } catch {
8099
10896
  destLst = void 0;
8100
10897
  }
8101
10898
  if (destLst?.isSymbolicLink()) {
8102
10899
  throw new UserError(`refusing to write ${destPath}: destination is a symlink`);
8103
10900
  }
8104
- writeFileSync9(destPath, content, { mode: 384 });
8105
- chmodSync(destPath, 384);
10901
+ writeFileSync10(destPath, content, { mode: 384 });
10902
+ chmodSync4(destPath, 384);
8106
10903
  return destPath;
8107
10904
  }
8108
10905
  async function refineCaptureCommand(rawOptions = {}) {
8109
- requireProFeature2();
10906
+ requireProFeature3();
8110
10907
  const opts = validateCaptureOptions(rawOptions);
8111
10908
  const config = loadConfig();
8112
10909
  const cwd = process.cwd();
@@ -8118,7 +10915,7 @@ async function refineCaptureCommand(rawOptions = {}) {
8118
10915
  const refinementId = generateRefinementId(existingIds, now, defaultRandomHex);
8119
10916
  const preview = renderCaptureArtifactPreview(opts, content, refinementId, now.toISOString(), scopeDir);
8120
10917
  if (opts.dryRun) {
8121
- emit(opts.json, { dryRun: true, path: preview.destPath, diff: preview.diff }, () => {
10918
+ emit2(opts.json, { dryRun: true, path: preview.destPath, diff: preview.diff }, () => {
8122
10919
  console.log(`[skill-forge] refine capture \u2014 ${opts.overrideType} override for "${sanitizeForMarkdown(opts.skill)}" at ${opts.scope} scope`);
8123
10920
  console.log(sanitizeDiffForConsole(preview.diff));
8124
10921
  });
@@ -8131,7 +10928,7 @@ async function refineCaptureCommand(rawOptions = {}) {
8131
10928
  const proceed = await confirmWrite(opts.yes, opts.json, `
8132
10929
  Write ${preview.destPath}? [y/N] `);
8133
10930
  if (!proceed) {
8134
- emit(opts.json, { written: false, cancelled: true, path: preview.destPath }, () => {
10931
+ emit2(opts.json, { written: false, cancelled: true, path: preview.destPath }, () => {
8135
10932
  console.log("[skill-forge] capture cancelled \u2014 nothing written.");
8136
10933
  });
8137
10934
  return;
@@ -8164,7 +10961,7 @@ Write ${preview.destPath}? [y/N] `);
8164
10961
  } catch (err) {
8165
10962
  throw new UserError(`wrote the override artifact at ${writtenPath}, but recording the refinement in the store failed: ${err.message}`);
8166
10963
  }
8167
- emit(
10964
+ emit2(
8168
10965
  opts.json,
8169
10966
  { written: true, path: writtenPath, refinementId: result.refinement.id, patternId: result.pattern.id, patternBecameReady: result.patternBecameReady },
8170
10967
  () => {
@@ -8181,21 +10978,21 @@ Write ${preview.destPath}? [y/N] `);
8181
10978
  }
8182
10979
  function renderCaptureArtifactPreview(opts, content, refinementId, generatedAt, scopeDir) {
8183
10980
  const probeRelPath = opts.overrideType === "patch" ? "SKILL.patch.md" : opts.overrideType === "extend" || opts.overrideType === "new" ? "SKILL.extend.md" : void 0;
8184
- const probePath = probeRelPath ? join30(scopeDir, probeRelPath) : void 0;
8185
- 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") : "";
8186
10983
  const rendered = renderCaptureArtifact(opts, content, refinementId, generatedAt, existingContent);
8187
10984
  const relPath = rendered.relPath;
8188
- const destPath = join30(scopeDir, relPath);
8189
- const before = existsSync27(destPath) ? readFileSync20(destPath, "utf-8") : "";
10985
+ const destPath = join35(scopeDir, relPath);
10986
+ const before = existsSync29(destPath) ? readFileSync24(destPath, "utf-8") : "";
8190
10987
  const diff = previewDiff(before, rendered.content, destPath);
8191
10988
  return { relPath, content: rendered.content, destPath, diff };
8192
10989
  }
8193
10990
  async function refineListCommand(options = {}) {
8194
- requireProFeature2();
10991
+ requireProFeature3();
8195
10992
  const { records } = readRefinementsStore();
8196
10993
  let rows = records;
8197
10994
  if (options.skill) rows = rows.filter((r) => r.skill === options.skill);
8198
- 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));
8199
10996
  if (options.status) {
8200
10997
  if (!isRefinementStatus(options.status)) {
8201
10998
  throw new UserError("--status must be one of: pending, applied, generalized");
@@ -8203,7 +11000,7 @@ async function refineListCommand(options = {}) {
8203
11000
  rows = rows.filter((r) => r.status === options.status);
8204
11001
  }
8205
11002
  const filtered = Boolean(options.skill || options.project || options.status);
8206
- emit(options.json, { records: rows }, () => {
11003
+ emit2(options.json, { records: rows }, () => {
8207
11004
  if (rows.length === 0) {
8208
11005
  if (filtered && records.length > 0) {
8209
11006
  console.log(`[skill-forge] no refinements match the given filter(s) (${records.length} total recorded).`);
@@ -8221,7 +11018,7 @@ async function refineListCommand(options = {}) {
8221
11018
  });
8222
11019
  }
8223
11020
  async function refinePatternsCommand(options = {}) {
8224
- requireProFeature2();
11021
+ requireProFeature3();
8225
11022
  const { records } = readPatternsStore();
8226
11023
  let rows = records;
8227
11024
  if (options.status) {
@@ -8232,11 +11029,11 @@ async function refinePatternsCommand(options = {}) {
8232
11029
  }
8233
11030
  if (options.skill) rows = rows.filter((p) => p.affectedSkills.includes(options.skill));
8234
11031
  if (options.project) {
8235
- const resolvedProject = resolvePath12(options.project);
11032
+ const resolvedProject = resolvePath13(options.project);
8236
11033
  rows = rows.filter((p) => p.occurrences.some((o) => o.projectRealpath === resolvedProject));
8237
11034
  }
8238
11035
  const filtered = Boolean(options.skill || options.project || options.status);
8239
- emit(options.json, { records: rows.map((p) => ({ ...p, count: patternCount(p) })) }, () => {
11036
+ emit2(options.json, { records: rows.map((p) => ({ ...p, count: patternCount(p) })) }, () => {
8240
11037
  if (rows.length === 0) {
8241
11038
  if (filtered && records.length > 0) {
8242
11039
  console.log(`[skill-forge] no patterns match the given filter(s) (${records.length} total recorded).`);
@@ -8265,13 +11062,13 @@ function renderInventoryLine(row) {
8265
11062
  return ` [${row.scope}] ${row.dir}${row.dirExists ? "" : " (missing)"} \u2014 ${present.length > 0 ? present.join(", ") : "(no override files)"}`;
8266
11063
  }
8267
11064
  async function refineWhichCommand(skill, options = {}) {
8268
- requireProFeature2();
11065
+ requireProFeature3();
8269
11066
  if (!skill) throw new UserError("a skill name is required: skill-forge refine which <skill>");
8270
11067
  const config = loadConfig();
8271
11068
  const cwd = process.cwd();
8272
11069
  const scopes = resolveScopes(config, cwd, skill, options.userRoot);
8273
11070
  const inventory = inventoryAllScopes(scopes);
8274
- emit(options.json, { scopes, inventory }, () => {
11071
+ emit2(options.json, { scopes, inventory }, () => {
8275
11072
  console.log(`[skill-forge] refine which "${sanitizeForMarkdown(skill)}" \u2014 resolution order (highest precedence first):`);
8276
11073
  for (const row of inventory) console.log(renderInventoryLine(row));
8277
11074
  if (!scopes.user.resolved) {
@@ -8283,16 +11080,16 @@ async function refineWhichCommand(skill, options = {}) {
8283
11080
  }
8284
11081
  function writePostWriteSidecar(backupId, files) {
8285
11082
  const sidecar = { schemaVersion: 1, files };
8286
- const dir = join30(getBackupsDir(), backupId);
8287
- mkdirSync9(dir, { recursive: true });
8288
- 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 });
8289
11086
  }
8290
11087
  function readPostWriteSidecar(backupId) {
8291
- const path = join30(getBackupsDir(), backupId, "post.json");
8292
- 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" };
8293
11090
  let raw;
8294
11091
  try {
8295
- raw = readFileSync20(path, "utf-8");
11092
+ raw = readFileSync24(path, "utf-8");
8296
11093
  } catch {
8297
11094
  return { hashes: /* @__PURE__ */ new Map(), status: "unreadable" };
8298
11095
  }
@@ -8311,7 +11108,7 @@ function readPostWriteSidecar(backupId) {
8311
11108
  return { hashes, status: "ok" };
8312
11109
  }
8313
11110
  async function refinePromoteCommand(patternId, options = {}) {
8314
- requireProFeature2();
11111
+ requireProFeature3();
8315
11112
  if (!patternId) throw new UserError("a pattern id is required: skill-forge refine promote <pattern-id>");
8316
11113
  const patternsStore = readPatternsStore();
8317
11114
  const pattern = patternsStore.records.find((p) => p.id === patternId);
@@ -8337,21 +11134,21 @@ async function refinePromoteCommand(patternId, options = {}) {
8337
11134
  const resolvedUserRoot = resolveConfiguredRoot3(scopes.user.root);
8338
11135
  let realUserRoot;
8339
11136
  try {
8340
- realUserRoot = realpathSync9(resolvedUserRoot);
11137
+ realUserRoot = realpathSync10(resolvedUserRoot);
8341
11138
  } catch {
8342
11139
  realUserRoot = resolvedUserRoot;
8343
11140
  }
8344
11141
  let realSkillDir;
8345
11142
  try {
8346
- realSkillDir = realpathSync9(skillDir);
11143
+ realSkillDir = realpathSync10(skillDir);
8347
11144
  } catch {
8348
- realSkillDir = resolvePath12(skillDir);
11145
+ realSkillDir = resolvePath13(skillDir);
8349
11146
  }
8350
11147
  if (!isUnderRoot3(realSkillDir, realUserRoot)) {
8351
11148
  throw new UserError(`refusing to promote: resolved skill directory ${skillDir} is outside the configured user scope root ${scopes.user.root}`);
8352
11149
  }
8353
- const skillMdPath = join30(skillDir, "SKILL.md");
8354
- if (!existsSync27(skillMdPath)) {
11150
+ const skillMdPath = join35(skillDir, "SKILL.md");
11151
+ if (!existsSync29(skillMdPath)) {
8355
11152
  throw new UserError(`user-scope SKILL.md not found at ${skillMdPath} \u2014 refusing to promote against a missing base skill.`);
8356
11153
  }
8357
11154
  const refinementsStore = readRefinementsStore();
@@ -8368,11 +11165,11 @@ async function refinePromoteCommand(patternId, options = {}) {
8368
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.`
8369
11166
  );
8370
11167
  }
8371
- const before = readFileSync20(skillMdPath, "utf-8");
11168
+ const before = readFileSync24(skillMdPath, "utf-8");
8372
11169
  const after = applyPatchBlocks(before, [block]);
8373
11170
  const diff = previewDiff(before, after, skillMdPath);
8374
11171
  if (options.dryRun) {
8375
- emit(options.json, { dryRun: true, path: skillMdPath, diff }, () => {
11172
+ emit2(options.json, { dryRun: true, path: skillMdPath, diff }, () => {
8376
11173
  console.log(`[skill-forge] refine promote ${patternId} -> ${skillMdPath}`);
8377
11174
  console.log(sanitizeDiffForConsole(diff));
8378
11175
  });
@@ -8385,7 +11182,7 @@ async function refinePromoteCommand(patternId, options = {}) {
8385
11182
  const proceed = await confirmWrite(options.yes, options.json, `
8386
11183
  Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
8387
11184
  if (!proceed) {
8388
- emit(options.json, { promoted: false, cancelled: true }, () => {
11185
+ emit2(options.json, { promoted: false, cancelled: true }, () => {
8389
11186
  console.log("[skill-forge] promote cancelled \u2014 nothing written.");
8390
11187
  });
8391
11188
  return;
@@ -8393,14 +11190,14 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
8393
11190
  const backup = createBackup([skillMdPath]);
8394
11191
  const tempPath = `${skillMdPath}.tmp-${randomUUID5()}`;
8395
11192
  try {
8396
- writeFileSync9(tempPath, after, "utf-8");
11193
+ writeFileSync10(tempPath, after, "utf-8");
8397
11194
  renameSync6(tempPath, skillMdPath);
8398
11195
  } catch (err) {
8399
11196
  rmSync9(tempPath, { force: true });
8400
11197
  throw err;
8401
11198
  }
8402
11199
  try {
8403
- const postSha256 = createHash6("sha256").update(readFileSync20(skillMdPath)).digest("hex");
11200
+ const postSha256 = createHash8("sha256").update(readFileSync24(skillMdPath)).digest("hex");
8404
11201
  writePostWriteSidecar(backup.id, [{ path: skillMdPath, sha256: postSha256 }]);
8405
11202
  markGeneralized(patternId, backup.id);
8406
11203
  const skillsRoot = resolveConfiguredRoot3(scopes.user.root);
@@ -8420,31 +11217,31 @@ Promote pattern ${patternId} into ${skillMdPath}? [y/N] `);
8420
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}`
8421
11218
  );
8422
11219
  }
8423
- emit(options.json, { promoted: true, path: skillMdPath, backupId: backup.id }, () => {
11220
+ emit2(options.json, { promoted: true, path: skillMdPath, backupId: backup.id }, () => {
8424
11221
  console.log(`[skill-forge] promoted pattern ${patternId} -> ${skillMdPath} (backup: ${backup.id})`);
8425
11222
  });
8426
11223
  }
8427
11224
  function assertBackupDirContained(backupId) {
8428
11225
  const backupsDir = getBackupsDir();
8429
- if (!existsSync27(backupsDir)) return;
8430
- const realBackupsDir = realpathSync9(backupsDir);
8431
- const backupDir = join30(backupsDir, backupId);
8432
- if (!existsSync27(backupDir)) return;
8433
- 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);
8434
11231
  if (!isUnderRoot3(realBackupDir, realBackupsDir)) {
8435
11232
  throw new UserError(`refusing rollback: backup id "${backupId}" resolves outside the backups directory`);
8436
11233
  }
8437
11234
  }
8438
11235
  function resolveTargetRealPath(path) {
8439
- let lexicalAncestor = resolvePath12(path);
11236
+ let lexicalAncestor = resolvePath13(path);
8440
11237
  while (true) {
8441
11238
  try {
8442
- const real = realpathSync9(lexicalAncestor);
8443
- const remainder = relative4(lexicalAncestor, resolvePath12(path));
8444
- 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);
8445
11242
  } catch {
8446
- const parent = dirname14(lexicalAncestor);
8447
- if (parent === lexicalAncestor) return resolvePath12(path);
11243
+ const parent = dirname15(lexicalAncestor);
11244
+ if (parent === lexicalAncestor) return resolvePath13(path);
8448
11245
  lexicalAncestor = parent;
8449
11246
  }
8450
11247
  }
@@ -8456,7 +11253,7 @@ function assertManifestPathsWithinRoots(manifest, config) {
8456
11253
  const roots = config.skillsRoots.map((root) => {
8457
11254
  const resolved = resolveConfiguredRoot3(root);
8458
11255
  try {
8459
- return realpathSync9(resolved);
11256
+ return realpathSync10(resolved);
8460
11257
  } catch {
8461
11258
  return resolved;
8462
11259
  }
@@ -8473,7 +11270,7 @@ function assertNoSymlinkLeaves(manifest) {
8473
11270
  for (const file of manifest.files) {
8474
11271
  let lst;
8475
11272
  try {
8476
- lst = lstatSync4(file.path);
11273
+ lst = lstatSync7(file.path);
8477
11274
  } catch {
8478
11275
  lst = void 0;
8479
11276
  }
@@ -8483,14 +11280,14 @@ function assertNoSymlinkLeaves(manifest) {
8483
11280
  }
8484
11281
  }
8485
11282
  function findLexicalRoot(filePath, config) {
8486
- const lexicalTarget = resolvePath12(filePath);
11283
+ const lexicalTarget = resolvePath13(filePath);
8487
11284
  for (const root of config.skillsRoots) {
8488
11285
  const rootLexical = resolveConfiguredRoot3(root);
8489
- const rel = relative4(rootLexical, lexicalTarget);
8490
- 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;
8491
11288
  let rootReal;
8492
11289
  try {
8493
- rootReal = realpathSync9(rootLexical);
11290
+ rootReal = realpathSync10(rootLexical);
8494
11291
  } catch {
8495
11292
  rootReal = rootLexical;
8496
11293
  }
@@ -8500,8 +11297,8 @@ function findLexicalRoot(filePath, config) {
8500
11297
  }
8501
11298
  function walkRealComponents(filePath, config, createMissing) {
8502
11299
  const { rootLexical, rootReal } = findLexicalRoot(filePath, config);
8503
- const relPath = relative4(rootLexical, resolvePath12(filePath));
8504
- 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);
8505
11302
  if (components.length === 0) {
8506
11303
  throw new UserError(`refusing to operate on ${filePath}: resolves to the skills root itself`);
8507
11304
  }
@@ -8510,10 +11307,10 @@ function walkRealComponents(filePath, config, createMissing) {
8510
11307
  let current = rootReal;
8511
11308
  for (const component of dirComponents) {
8512
11309
  if (current === void 0) break;
8513
- const next = join30(current, component);
11310
+ const next = join35(current, component);
8514
11311
  let lst;
8515
11312
  try {
8516
- lst = lstatSync4(next);
11313
+ lst = lstatSync7(next);
8517
11314
  } catch {
8518
11315
  lst = void 0;
8519
11316
  }
@@ -8522,7 +11319,7 @@ function walkRealComponents(filePath, config, createMissing) {
8522
11319
  current = void 0;
8523
11320
  continue;
8524
11321
  }
8525
- mkdirSync9(next);
11322
+ mkdirSync11(next);
8526
11323
  current = next;
8527
11324
  continue;
8528
11325
  }
@@ -8541,13 +11338,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
8541
11338
  if (parentReal === void 0) {
8542
11339
  throw new UserError(`refusing to restore ${filePath}: could not establish a real parent directory`);
8543
11340
  }
8544
- const finalPath = join30(parentReal, leafComponent);
11341
+ const finalPath = join35(parentReal, leafComponent);
8545
11342
  if (!isUnderRoot3(finalPath, rootReal)) {
8546
11343
  throw new UserError(`refusing to restore ${filePath}: resolves outside root ${rootReal}`);
8547
11344
  }
8548
11345
  let leafLst;
8549
11346
  try {
8550
- leafLst = lstatSync4(finalPath);
11347
+ leafLst = lstatSync7(finalPath);
8551
11348
  } catch {
8552
11349
  leafLst = void 0;
8553
11350
  }
@@ -8556,8 +11353,8 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
8556
11353
  }
8557
11354
  const tempPath = `${finalPath}.tmp-${randomUUID5()}`;
8558
11355
  try {
8559
- writeFileSync9(tempPath, contentBuffer);
8560
- chmodSync(tempPath, mode);
11356
+ writeFileSync10(tempPath, contentBuffer);
11357
+ chmodSync4(tempPath, mode);
8561
11358
  renameSync6(tempPath, finalPath);
8562
11359
  } catch (err) {
8563
11360
  rmSync9(tempPath, { force: true });
@@ -8567,13 +11364,13 @@ function safeRestoreWrite(filePath, contentBuffer, mode, config) {
8567
11364
  function safeDeleteTombstone(filePath, config) {
8568
11365
  const { parentReal, leafComponent, rootReal } = walkRealComponents(filePath, config, false);
8569
11366
  if (parentReal === void 0) return;
8570
- const finalPath = join30(parentReal, leafComponent);
11367
+ const finalPath = join35(parentReal, leafComponent);
8571
11368
  if (!isUnderRoot3(finalPath, rootReal)) {
8572
11369
  throw new UserError(`refusing to delete ${filePath}: resolves outside root ${rootReal}`);
8573
11370
  }
8574
11371
  let leafLst;
8575
11372
  try {
8576
- leafLst = lstatSync4(finalPath);
11373
+ leafLst = lstatSync7(finalPath);
8577
11374
  } catch {
8578
11375
  leafLst = void 0;
8579
11376
  }
@@ -8584,7 +11381,7 @@ function safeDeleteTombstone(filePath, config) {
8584
11381
  rmSync9(finalPath, { force: true });
8585
11382
  }
8586
11383
  async function refineRollbackCommand(backupId, options = {}) {
8587
- requireProFeature2();
11384
+ requireProFeature3();
8588
11385
  if (!backupId) throw new UserError("a backup id is required: skill-forge refine rollback <backup-id>");
8589
11386
  if (!isValidBackupId(backupId)) {
8590
11387
  throw new UserError(`invalid backup id "${backupId}" \u2014 expected the format produced by \`skill-forge refine promote\` (e.g. 20260720T142305Z-a1b2)`);
@@ -8609,7 +11406,7 @@ async function refineRollbackCommand(backupId, options = {}) {
8609
11406
  for (const file of manifest.files) {
8610
11407
  const expected = sidecar.hashes.get(file.path);
8611
11408
  if (expected === void 0) continue;
8612
- 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;
8613
11410
  if (currentSha256 !== expected) mismatches.push(file.path);
8614
11411
  }
8615
11412
  if (mismatches.length > 0 && !options.force) {
@@ -8628,7 +11425,7 @@ Roll back backup ${backupId}? Affected files:
8628
11425
  ${fileList}
8629
11426
  [y/N] `);
8630
11427
  if (!proceed) {
8631
- emit(options.json, { rolledBack: false, cancelled: true, backupId }, () => {
11428
+ emit2(options.json, { rolledBack: false, cancelled: true, backupId }, () => {
8632
11429
  console.log("[skill-forge] rollback cancelled \u2014 nothing restored.");
8633
11430
  });
8634
11431
  return;
@@ -8657,7 +11454,7 @@ ${fileList}
8657
11454
  pattern.generalizedBackupId = void 0;
8658
11455
  writePatternsStore(patternsStore);
8659
11456
  }
8660
- emit(options.json, { rolledBack: true, backupId, patternReset: pattern?.id, warnings }, () => {
11457
+ emit2(options.json, { rolledBack: true, backupId, patternReset: pattern?.id, warnings }, () => {
8661
11458
  console.log(`[skill-forge] rollback complete for ${backupId}${pattern ? ` (pattern ${pattern.id} reset to ready)` : ""}`);
8662
11459
  for (const w of warnings) console.log(`[skill-forge] warning: ${w}`);
8663
11460
  });
@@ -8700,6 +11497,27 @@ function parsePruneDays(value) {
8700
11497
  }
8701
11498
  return n;
8702
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
+ }
8703
11521
  var program = new Command();
8704
11522
  program.name("skill-forge").description("The supply-chain gate for agent skills").version(pkg.version);
8705
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(
@@ -8743,6 +11561,14 @@ program.command("ingest").description(
8743
11561
  ).option("--list", "print pending queue entries and exit \u2014 no handoff").option("--json", "with --list: print pending entries as JSON").action(ingest);
8744
11562
  var queue = program.command("queue").description("Manage the pending-ingestion queue (Pro)");
8745
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);
8746
11572
  var refine = program.command("refine").description("Capture, apply, and generalize improvements to installed skills from usage feedback (Pro)");
8747
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);
8748
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);