@rhize/skill-forge 0.12.0 → 0.13.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/README.md CHANGED
@@ -172,7 +172,18 @@ target skills root until you decide.
172
172
  | `-t, --target <dir>` | Skills root to promote into. Defaults to the config's `defaultTarget`, then `skillsRoots[0]`. |
173
173
  | `--json` | Prints the gate result (profile, safety findings, overlap) as JSON instead of the terminal report box. Implies non-interactive: the decision is made the same way `--yes` makes it (verdict decides promote/hold/reject), never an interactive prompt. |
174
174
  | `--ingest` | Pro (free during the 0.x beta). After a successful promote, hands off to a coding agent — see [Ingestion handoff](#ingestion-handoff---ingest). |
175
- | `--skill-map <path>` | Path to a generated `rhize-plugins` skill map (Phase 4 of that repo's skill-map-graph-substrate plan). When given, ranks the candidate's name/description against every `skill` node in the map — a near-duplicate of an already-shipped marketplace skill is folded into the safety findings as a `HIGH`-severity finding (escalates the verdict to `block`, so it's held rather than silently promoted); a moderate overlap is `MEDIUM` (`warn`). Free (not Pro-gated) — this enforces the marketplace's own curation rule ("close the gap, don't duplicate"), not a premium ranking feature. Missing/unreadable map: printed to stderr, never fatal. |
175
+ | `--skill-map <path>` | Path to a generated `rhize-plugins` skill map (Phase 4 of that repo's skill-map-graph-substrate plan). When given, ranks the candidate's name/description against every `skill` node in the map — a near-duplicate of an already-shipped marketplace skill is folded into the safety findings as a `HIGH`-severity finding (escalates the verdict to `block`, so it's held rather than silently promoted); a moderate overlap is `MEDIUM` (`warn`). Free (not Pro-gated) — this enforces the marketplace's own curation rule ("close the gap, don't duplicate"), not a premium ranking feature. Missing/unreadable map: printed to stderr, never fatal. **Two map variants exist, with different coverage**: the **static** map (`rhize-plugins/generated/skill-map.static.json`) is first-party-only — it only knows about that marketplace's own skills, so it cannot catch a candidate that duplicates an *installed third-party plugin's* skill. The **resolved** map (`~/.claude/context-manager/skill-map.resolved.json`, produced by `rhize-context-manager`) additionally carries the full third-party ecosystem inventory. Point `--skill-map` at the resolved map when you want ecosystem-wide duplicate detection; the static map is enough only when you're curating a single first-party marketplace against itself. |
176
+
177
+ **Bugfix (v0.13.0):** `--skill-map` previously matched nothing against either real map variant — the loader read a `type` field that the real generated artifact never sets (it uses `kind`), so every node was silently invisible to the overlap check. `loadSkillMap()` now normalizes `kind` → `type` at load time; regression coverage lives in `test/mapOverlap.realmap.test.ts` against vendored real-map snapshots. If you were relying on `--skill-map` before v0.13.0, it was a no-op — re-run `add`/`scan` on anything you're unsure about.
178
+
179
+ **Extends-declared overlap exemption**: a candidate can declare `metadata.rhize.extends:
180
+ ["<skill-name>" | "<plugin>/<skill-name>"]` in its SKILL.md frontmatter to mark itself a
181
+ deliberate specialization/layering of an existing map skill. When the top `--skill-map` match is
182
+ a skill the candidate declares it extends, that finding is downgraded from a HOLD-level safety
183
+ finding to an informational notice printed above the report — it never escalates the verdict.
184
+ Overlap with any *other* skill (a different match, or a second candidate/skill pair) still holds
185
+ at full severity: the exemption applies per declared pair, not globally, so declaring an
186
+ extension of skill X never waives overlap with skill Y.
176
187
 
177
188
  A promoted skill gets a provenance entry appended to `<target>/SOURCES.md`, and every promote or
178
189
  hold decision is recorded to `~/.skill-forge/queue.json` (or `$SKILL_FORGE_HOME/queue.json`) — see
@@ -478,11 +489,41 @@ It is only ever printed, sanitized, as a suggestion for you (or an agent) to run
478
489
  **`--skill-map <path>` (Phase 4 of `rhize-plugins`' skill-map-graph-substrate plan)** additionally
479
490
  drift-checks every `fork-of` edge in a generated skill map: for each edge it resolves the local
480
491
  `skill` node's `path`/`contentHash` and the upstream (`external`) node's `url`/`path`, fetches or
481
- reads the upstream content, and compares content hashes. Each edge reports one of `in-sync`,
482
- `drifted`, `upstream-unreachable`, or `local-missing`. Same never-execute posture as the ledger
492
+ reads the upstream content, and compares content hashes. Same never-execute posture as the ledger
483
493
  check above: nothing derived from the map — including a fork-of edge's own `driftCheck`
484
494
  metadata — is ever executed; only fetch/read/hash/compare. A missing/unreadable map is a warning,
485
- never fatal.
495
+ never fatal. A node's repo-relative `path` (e.g. `rhize-context-manager/skills/x/SKILL.md`) is
496
+ resolved against cwd, the map's own directory, and that directory's parent (in that order) — not
497
+ cwd alone — so `watch --skill-map <path>` gives correct verdicts regardless of the directory you
498
+ run it from.
499
+
500
+ **Verdict**, per fork-of edge — three-way comparison, four verdict states when both hashes below
501
+ are present on the map, else the older two-way fallback (`in-sync`/`drifted`, `contentHash` vs
502
+ freshly-fetched upstream):
503
+
504
+ | local-normalized vs baseline | upstream-now vs baseline | status | actionable |
505
+ |---|---|---|---|
506
+ | == | == | `in-sync` | no |
507
+ | != | == | `local-only` | no (deliberate fork divergence, e.g. Rhize's added frontmatter) |
508
+ | == | != | `upstream-moved` | yes |
509
+ | != | != | `diverged` | yes |
510
+
511
+ The three-way matrix fires only when the local `skill` node carries `contentHashNormalized` (a
512
+ hash of the file with Rhize-injected frontmatter stripped, computed once by the rhize-plugins
513
+ compiler) **and** the upstream `external` node carries `baselineHash` (the upstream content hash
514
+ as of the last human review, recorded in `rhize-plugins`' SOURCES.md and copied onto the node by
515
+ that compiler — skill-forge never computes or fetches it itself). Either field missing on a given
516
+ edge falls back to the two-way compare for that edge, so older maps keep working. All four
517
+ three-way verdicts — `in-sync`, `local-only`, `upstream-moved`, and `diverged` — carry
518
+ `baselineHash`/`upstreamHash` in the JSON output; the two-way-fallback rows and
519
+ `upstream-unreachable`/`local-missing` never do (no verdict without a successful fetch/read, so
520
+ there is nothing to compare against a baseline). Every row also carries an `actionable` boolean
521
+ (from `isActionable`), so callers don't have to re-derive "needs attention" from `status`/`detail`
522
+ prose — it's `true` for `drifted`, `upstream-moved`, `diverged`, `upstream-unreachable`, and
523
+ `local-missing`; `false` for `in-sync` and `local-only`. **Re-baselining**: after reviewing and
524
+ adopting an `upstream-moved`/`diverged` change, re-run `rhize-plugins`' `scripts/baseline_upstreams.py`
525
+ and commit the updated SOURCES.md — that's the "I reviewed upstream, accept its state" action that
526
+ clears the row back to `in-sync`/`local-only`.
486
527
 
487
528
  ### `ingest` + `queue close` (v0.9, Pro)
488
529
 
package/dist/cli.js CHANGED
@@ -3131,6 +3131,33 @@ function extractDescription(block) {
3131
3131
  }
3132
3132
  return stripQuotes(first);
3133
3133
  }
3134
+ function extractExtends(block) {
3135
+ const lines = block.split("\n");
3136
+ const idx = lines.findIndex((l) => /^\s*extends:\s*(.*)$/.test(l));
3137
+ if (idx === -1) return void 0;
3138
+ const inline = lines[idx].match(/^\s*extends:\s*(.*)$/)[1].trim();
3139
+ if (inline.startsWith("[")) {
3140
+ const inner = inline.replace(/^\[/, "").replace(/\]\s*$/, "");
3141
+ const items = inner.split(",").map((s) => stripQuotes(s.trim())).filter(Boolean);
3142
+ return items.length ? items : void 0;
3143
+ }
3144
+ if (inline === "") {
3145
+ const items = [];
3146
+ for (let i = idx + 1; i < lines.length; i++) {
3147
+ const line = lines[i];
3148
+ const m = line.match(/^\s*-\s*(.+)$/);
3149
+ if (m) {
3150
+ items.push(stripQuotes(m[1].trim()));
3151
+ } else if (line.trim() === "") {
3152
+ continue;
3153
+ } else {
3154
+ break;
3155
+ }
3156
+ }
3157
+ return items.length ? items : void 0;
3158
+ }
3159
+ return [stripQuotes(inline)];
3160
+ }
3134
3161
  function parseFrontmatter(text) {
3135
3162
  if (!text.startsWith("---\n")) {
3136
3163
  return { validFrontmatter: false };
@@ -3141,7 +3168,8 @@ function parseFrontmatter(text) {
3141
3168
  name: extractSimpleField(block, "name"),
3142
3169
  version: extractSimpleField(block, "version"),
3143
3170
  license: extractSimpleField(block, "license"),
3144
- description: extractDescription(block)
3171
+ description: extractDescription(block),
3172
+ extends: extractExtends(block)
3145
3173
  };
3146
3174
  }
3147
3175
  function detectLicense(root, fm) {
@@ -3227,7 +3255,8 @@ function profileSkillDir(inputPath) {
3227
3255
  resources,
3228
3256
  mcpDependencies: mcpDeps,
3229
3257
  externalPythonDeps: externalDeps,
3230
- artifactType: detectArtifactType(root)
3258
+ artifactType: detectArtifactType(root),
3259
+ extends: fm.extends ?? []
3231
3260
  };
3232
3261
  } finally {
3233
3262
  cleanupTmp(tmpDir);
@@ -3407,9 +3436,13 @@ function loadSkillMap(path) {
3407
3436
  if (!Array.isArray(obj.nodes) || !Array.isArray(obj.edges)) {
3408
3437
  throw new Error(`skill map at ${resolved}: expected "nodes" and "edges" arrays`);
3409
3438
  }
3439
+ const nodes = obj.nodes.map((n) => ({
3440
+ ...n,
3441
+ type: typeof n.type === "string" ? n.type : n.kind
3442
+ }));
3410
3443
  return {
3411
3444
  schemaVersion: typeof obj.schemaVersion === "string" ? obj.schemaVersion : void 0,
3412
- nodes: obj.nodes,
3445
+ nodes,
3413
3446
  edges: obj.edges
3414
3447
  };
3415
3448
  }
@@ -3497,6 +3530,25 @@ function mapOverlapFinding(result) {
3497
3530
  context: top.skillId
3498
3531
  };
3499
3532
  }
3533
+ function isDeclaredExtension(match, extendsDeclarations) {
3534
+ const qualifiedId = match.skillId.replace(/^[a-z]+:/, "");
3535
+ const bareId = qualifiedId.split("/").pop() ?? qualifiedId;
3536
+ return extendsDeclarations.some((raw) => {
3537
+ const declared = raw.trim();
3538
+ if (!declared) return false;
3539
+ return declared === match.name || declared === bareId || declared === qualifiedId;
3540
+ });
3541
+ }
3542
+ function mapOverlapDecision(result, extendsDeclarations = []) {
3543
+ const top = result.matches[0];
3544
+ if (!top) return {};
3545
+ if (extendsDeclarations.length > 0 && isDeclaredExtension(top, extendsDeclarations)) {
3546
+ return {
3547
+ informational: `declared extension of "${top.name}" (score ${top.score}) \u2014 overlap expected; verify the layering claim in review`
3548
+ };
3549
+ }
3550
+ return { finding: mapOverlapFinding(result) };
3551
+ }
3500
3552
 
3501
3553
  // src/report/render.ts
3502
3554
  var SAFETY_VERDICT_MAP = {
@@ -3623,11 +3675,11 @@ function tryMcpOverlap(profile, mcpTargets) {
3623
3675
  return void 0;
3624
3676
  }
3625
3677
  }
3626
- function tryMapOverlapFinding(skillMapPath, candidate) {
3678
+ function tryMapOverlapDecision(skillMapPath, candidate, extendsDeclarations) {
3627
3679
  if (!skillMapPath) return void 0;
3628
3680
  try {
3629
3681
  const map = loadSkillMap(skillMapPath);
3630
- return mapOverlapFinding(scanMapOverlap(candidate, map));
3682
+ return mapOverlapDecision(scanMapOverlap(candidate, map), extendsDeclarations);
3631
3683
  } catch (err) {
3632
3684
  console.error(`[skill-forge] skill-map overlap check skipped: ${err.message}`);
3633
3685
  return void 0;
@@ -3653,20 +3705,29 @@ async function runGate(candidatePath, skillsRoot, {
3653
3705
  const proStatus = proFeatureStatus(proFeatureLabel);
3654
3706
  const safety = artifact === "mcp" ? await mcpScan(candidatePath, strictness) : await scanSafety(candidatePath, strictness);
3655
3707
  const overlap = !proStatus.enabled ? void 0 : artifact === "mcp" ? tryMcpOverlap(profile, mcpTargets) : tryOverlap(candidatePath, skillsRoot);
3656
- const mapFinding = artifact === "mcp" ? void 0 : tryMapOverlapFinding(skillMapPath, {
3657
- name: "name" in profile ? profile.name : void 0,
3658
- description: "description" in profile ? profile.description : void 0
3659
- });
3708
+ const mapDecision = artifact === "mcp" ? void 0 : tryMapOverlapDecision(
3709
+ skillMapPath,
3710
+ {
3711
+ name: "name" in profile ? profile.name : void 0,
3712
+ description: "description" in profile ? profile.description : void 0
3713
+ },
3714
+ profile.kind === "skill" ? profile.extends : []
3715
+ );
3716
+ const mapFinding = mapDecision?.finding;
3660
3717
  const combinedSafety = mapFinding ? { verdict: verdictFor([...safety.findings, mapFinding], strictness), findings: [...safety.findings, mapFinding] } : safety;
3661
3718
  const advisory = formatAuditAdvisory(readAuditState(), {
3662
3719
  targetRoot: skillsRoot || void 0,
3663
3720
  candidateName: "name" in profile ? profile.name : void 0
3664
3721
  });
3722
+ const notices = [
3723
+ ...proStatus.notice ? [proStatus.notice] : [],
3724
+ ...mapDecision?.informational ? [mapDecision.informational] : []
3725
+ ];
3665
3726
  const gateResult = {
3666
3727
  profile,
3667
3728
  overlap,
3668
3729
  overlapLocked: !proStatus.enabled,
3669
- notices: proStatus.notice ? [proStatus.notice] : void 0,
3730
+ notices: notices.length > 0 ? notices : void 0,
3670
3731
  safety: {
3671
3732
  verdict: SAFETY_VERDICT_MAP[combinedSafety.verdict],
3672
3733
  findings: mapSafetyFindings(combinedSafety.findings)
@@ -6406,12 +6467,52 @@ import { join as join26, resolve as resolvePath8 } from "path";
6406
6467
  // src/gate/skillMapDrift.ts
6407
6468
  import { createHash as createHash4 } from "crypto";
6408
6469
  import { readFileSync as readFileSync18, existsSync as existsSync23 } from "fs";
6409
- import { resolve as resolve7 } from "path";
6470
+ import { dirname as dirname10, isAbsolute as isAbsolute2, resolve as resolve7 } from "path";
6410
6471
  function sha256(text) {
6411
6472
  return createHash4("sha256").update(text, "utf-8").digest("hex");
6412
6473
  }
6413
- function defaultReadLocalFile(path) {
6414
- const resolved = resolve7(expandHome(path));
6474
+ function shortHash(hash) {
6475
+ return hash.slice(0, 12);
6476
+ }
6477
+ var ACTIONABLE_STATUSES = /* @__PURE__ */ new Set([
6478
+ "upstream-moved",
6479
+ "diverged",
6480
+ "drifted",
6481
+ "upstream-unreachable",
6482
+ "local-missing"
6483
+ ]);
6484
+ function isActionable(status2) {
6485
+ return ACTIONABLE_STATUSES.has(status2);
6486
+ }
6487
+ function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
6488
+ const localMatchesBaseline = localNormalized === baselineHash;
6489
+ const upstreamMatchesBaseline = upstreamHash === baselineHash;
6490
+ const hashSuffix = `baseline=${shortHash(baselineHash)}\u2026, local-normalized=${shortHash(localNormalized)}\u2026, upstream=${shortHash(upstreamHash)}\u2026`;
6491
+ const TABLE = {
6492
+ TT: { status: "in-sync", detail: `local and upstream both match the recorded baseline (${hashSuffix})` },
6493
+ TF: { status: "upstream-moved", detail: `only upstream changed since baseline \u2014 review for adoption (${hashSuffix})` },
6494
+ FT: { status: "local-only", detail: `only local content changed since baseline \u2014 deliberate fork divergence (${hashSuffix})` },
6495
+ FF: { status: "diverged", detail: `both local and upstream changed since baseline \u2014 review for adoption (${hashSuffix})` }
6496
+ };
6497
+ const key = `${localMatchesBaseline ? "T" : "F"}${upstreamMatchesBaseline ? "T" : "F"}`;
6498
+ const { status: status2, detail } = TABLE[key];
6499
+ return { status: status2, detail, actionable: isActionable(status2) };
6500
+ }
6501
+ function resolveLocalPath(path, mapDir) {
6502
+ const expanded = expandHome(path);
6503
+ if (isAbsolute2(expanded)) return resolve7(expanded);
6504
+ const candidates = [resolve7(expanded)];
6505
+ if (mapDir) {
6506
+ candidates.push(resolve7(mapDir, expanded));
6507
+ candidates.push(resolve7(mapDir, "..", expanded));
6508
+ }
6509
+ for (const candidate of candidates) {
6510
+ if (existsSync23(candidate)) return candidate;
6511
+ }
6512
+ return candidates[0];
6513
+ }
6514
+ function defaultReadLocalFile(path, mapDir) {
6515
+ const resolved = resolveLocalPath(path, mapDir);
6415
6516
  if (!existsSync23(resolved)) return void 0;
6416
6517
  try {
6417
6518
  return readFileSync18(resolved, "utf-8");
@@ -6435,7 +6536,8 @@ function upstreamLabel(node, edge) {
6435
6536
  return node?.url ?? node?.path ?? node?.name ?? node?.id ?? edge.to;
6436
6537
  }
6437
6538
  async function computeMapDrift(map, deps = {}) {
6438
- const readLocalFile = deps.readLocalFile ?? defaultReadLocalFile;
6539
+ const mapDir = deps.mapPath ? dirname10(resolve7(expandHome(deps.mapPath))) : void 0;
6540
+ const readLocalFile = deps.readLocalFile ?? ((path) => defaultReadLocalFile(path, mapDir));
6439
6541
  const fetchUpstream = deps.fetchUpstream ?? defaultFetchUpstream;
6440
6542
  const rows = [];
6441
6543
  for (const edge of edgesOfType(map, "fork-of")) {
@@ -6448,13 +6550,20 @@ async function computeMapDrift(map, deps = {}) {
6448
6550
  skill: skillLabel,
6449
6551
  upstream,
6450
6552
  status: "local-missing",
6451
- detail: !localNode ? `fork-of edge "from" node not found in map: ${edge.from}` : 'local skill node missing "path" or "contentHash" \u2014 cannot compare'
6553
+ detail: !localNode ? `fork-of edge "from" node not found in map: ${edge.from}` : 'local skill node missing "path" or "contentHash" \u2014 cannot compare',
6554
+ actionable: isActionable("local-missing")
6452
6555
  });
6453
6556
  continue;
6454
6557
  }
6455
- const localResolved = resolve7(expandHome(localNode.path));
6558
+ const localResolved = resolveLocalPath(localNode.path, mapDir);
6456
6559
  if (!existsSync23(localResolved)) {
6457
- rows.push({ skill: skillLabel, upstream, status: "local-missing", detail: `local path does not exist: ${localResolved}` });
6560
+ rows.push({
6561
+ skill: skillLabel,
6562
+ upstream,
6563
+ status: "local-missing",
6564
+ detail: `local path does not exist: ${localResolved}`,
6565
+ actionable: isActionable("local-missing")
6566
+ });
6458
6567
  continue;
6459
6568
  }
6460
6569
  const upstreamLocation = upstreamNode?.url ?? upstreamNode?.path;
@@ -6463,24 +6572,45 @@ async function computeMapDrift(map, deps = {}) {
6463
6572
  skill: skillLabel,
6464
6573
  upstream,
6465
6574
  status: "upstream-unreachable",
6466
- detail: 'upstream node has no "url" or "path" to fetch/read'
6575
+ detail: 'upstream node has no "url" or "path" to fetch/read',
6576
+ actionable: isActionable("upstream-unreachable")
6467
6577
  });
6468
6578
  continue;
6469
6579
  }
6470
6580
  const upstreamText = isHttpUrl(upstreamLocation) ? await fetchUpstream(upstreamLocation) : readLocalFile(upstreamLocation);
6471
6581
  if (upstreamText === void 0) {
6472
- rows.push({ skill: skillLabel, upstream, status: "upstream-unreachable", detail: `could not fetch/read upstream: ${upstreamLocation}` });
6582
+ rows.push({
6583
+ skill: skillLabel,
6584
+ upstream,
6585
+ status: "upstream-unreachable",
6586
+ detail: `could not fetch/read upstream: ${upstreamLocation}`,
6587
+ actionable: isActionable("upstream-unreachable")
6588
+ });
6473
6589
  continue;
6474
6590
  }
6475
6591
  const upstreamHash = sha256(upstreamText);
6592
+ const baselineHash = upstreamNode?.baselineHash;
6593
+ const localNormalized = localNode.contentHashNormalized;
6594
+ if (baselineHash && localNormalized) {
6595
+ const { status: status2, detail, actionable } = threeWayVerdict(localNormalized, upstreamHash, baselineHash);
6596
+ rows.push({ skill: skillLabel, upstream, status: status2, detail, actionable, baselineHash, upstreamHash });
6597
+ continue;
6598
+ }
6476
6599
  if (upstreamHash === localNode.contentHash) {
6477
- rows.push({ skill: skillLabel, upstream, status: "in-sync", detail: `contentHash matches (${upstreamHash.slice(0, 12)}\u2026)` });
6600
+ rows.push({
6601
+ skill: skillLabel,
6602
+ upstream,
6603
+ status: "in-sync",
6604
+ detail: `contentHash matches (${shortHash(upstreamHash)}\u2026)`,
6605
+ actionable: isActionable("in-sync")
6606
+ });
6478
6607
  } else {
6479
6608
  rows.push({
6480
6609
  skill: skillLabel,
6481
6610
  upstream,
6482
6611
  status: "drifted",
6483
- detail: `recorded ${localNode.contentHash.slice(0, 12)}\u2026, upstream now hashes to ${upstreamHash.slice(0, 12)}\u2026`
6612
+ detail: `recorded ${shortHash(localNode.contentHash)}\u2026, upstream now hashes to ${shortHash(upstreamHash)}\u2026`,
6613
+ actionable: isActionable("drifted")
6484
6614
  });
6485
6615
  }
6486
6616
  }
@@ -6593,10 +6723,10 @@ async function runWatch(config, options = {}, deps = {}) {
6593
6723
  if (options.skillMap) {
6594
6724
  try {
6595
6725
  const map = loadSkillMap(options.skillMap);
6596
- mapDrift = await computeMapDrift(
6597
- map,
6598
- options.offline ? { fetchUpstream: async () => void 0 } : void 0
6599
- );
6726
+ mapDrift = await computeMapDrift(map, {
6727
+ mapPath: options.skillMap,
6728
+ ...options.offline ? { fetchUpstream: async () => void 0 } : {}
6729
+ });
6600
6730
  } catch (err) {
6601
6731
  warnings.push(`skill map at ${options.skillMap}: ${err.message}`);
6602
6732
  }
@@ -6921,7 +7051,7 @@ async function routineCommand(options = {}) {
6921
7051
 
6922
7052
  // src/commands/ingest.ts
6923
7053
  import { realpathSync as realpathSync6 } from "fs";
6924
- import { dirname as dirname11, isAbsolute as isAbsolute2, relative as relative2, resolve as resolvePath9 } from "path";
7054
+ import { dirname as dirname12, isAbsolute as isAbsolute3, relative as relative2, resolve as resolvePath9 } from "path";
6925
7055
  var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
6926
7056
  var VALID_STATUSES = ["ingested", "dismissed"];
6927
7057
  function requireProFeature() {
@@ -6969,12 +7099,12 @@ function realpathOrNull(p) {
6969
7099
  }
6970
7100
  function isUnderRoot(candidate, root) {
6971
7101
  const rel = relative2(root, candidate);
6972
- return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
7102
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
6973
7103
  }
6974
7104
  function validatePendingEntries(entries, config) {
6975
7105
  const quarantineRoot = realpathOrNull(resolvePath9(config.quarantineDir));
6976
7106
  const skillsRoots = config.skillsRoots.map((r) => realpathOrNull(resolvePath9(r))).filter((r) => r !== null);
6977
- const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname11(resolvePath9(f)))).filter((r) => r !== null);
7107
+ const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname12(resolvePath9(f)))).filter((r) => r !== null);
6978
7108
  const allowedRoots = [quarantineRoot, ...skillsRoots, ...mcpTargetDirs].filter((r) => r !== null);
6979
7109
  const valid = [];
6980
7110
  const excluded = [];
@@ -7048,12 +7178,12 @@ async function queueClose(id, options = {}) {
7048
7178
  // src/commands/refine.ts
7049
7179
  import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
7050
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";
7051
- import { dirname as dirname13, isAbsolute as isAbsolute4, join as join30, relative as relative4, resolve as resolvePath12, sep as sep4 } from "path";
7181
+ import { dirname as dirname14, isAbsolute as isAbsolute5, join as join30, relative as relative4, resolve as resolvePath12, sep as sep4 } from "path";
7052
7182
  import { createInterface as createInterface6 } from "readline/promises";
7053
7183
 
7054
7184
  // src/refine/context.ts
7055
7185
  import { existsSync as existsSync26, realpathSync as realpathSync8, statSync as statSync12 } from "fs";
7056
- import { isAbsolute as isAbsolute3, join as join29, relative as relative3, resolve as resolvePath11 } from "path";
7186
+ import { isAbsolute as isAbsolute4, join as join29, relative as relative3, resolve as resolvePath11 } from "path";
7057
7187
 
7058
7188
  // src/refine/models.ts
7059
7189
  import { randomBytes } from "crypto";
@@ -7465,7 +7595,7 @@ function realpathOrNull2(p) {
7465
7595
  }
7466
7596
  function isUnderRoot2(candidate, root) {
7467
7597
  const rel = relative3(root, candidate);
7468
- return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
7598
+ return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
7469
7599
  }
7470
7600
  function resolveScopes(config, cwd, skill, userRootFlag) {
7471
7601
  assertPlainSkillName(skill);
@@ -7796,7 +7926,7 @@ function resolveConfiguredRoot3(root) {
7796
7926
  }
7797
7927
  function isUnderRoot3(candidate, root) {
7798
7928
  const rel = relative4(root, candidate);
7799
- return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
7929
+ return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
7800
7930
  }
7801
7931
  function sanitizeDiffForConsole(diff) {
7802
7932
  return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
@@ -7956,7 +8086,7 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
7956
8086
  function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
7957
8087
  const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
7958
8088
  const destPath = join30(scopeDir, relPath);
7959
- const destDir = dirname13(destPath);
8089
+ const destDir = dirname14(destPath);
7960
8090
  mkdirSync9(destDir, { recursive: true });
7961
8091
  const realDestDir = realpathSync9(destDir);
7962
8092
  if (!isUnderRoot3(realDestDir, realScopeDir)) {
@@ -8313,7 +8443,7 @@ function resolveTargetRealPath(path) {
8313
8443
  const remainder = relative4(lexicalAncestor, resolvePath12(path));
8314
8444
  return remainder === "" ? real : join30(real, remainder);
8315
8445
  } catch {
8316
- const parent = dirname13(lexicalAncestor);
8446
+ const parent = dirname14(lexicalAncestor);
8317
8447
  if (parent === lexicalAncestor) return resolvePath12(path);
8318
8448
  lexicalAncestor = parent;
8319
8449
  }
@@ -8357,7 +8487,7 @@ function findLexicalRoot(filePath, config) {
8357
8487
  for (const root of config.skillsRoots) {
8358
8488
  const rootLexical = resolveConfiguredRoot3(root);
8359
8489
  const rel = relative4(rootLexical, lexicalTarget);
8360
- if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute4(rel))) continue;
8490
+ if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute5(rel))) continue;
8361
8491
  let rootReal;
8362
8492
  try {
8363
8493
  rootReal = realpathSync9(rootLexical);