@hasna/skills 0.10.54 → 0.10.56

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
@@ -696,6 +696,10 @@ remain absent only while unchanged native settings and registrations select a
696
696
  different extant, witnessed user version. Individual missing files, current
697
697
  registered roots, aliases and registration changes still refuse; historical
698
698
  source hashes and typed settings witnesses remain intact.
699
+ An explicit Claude discovery review must cover every registered active cache
700
+ manifest, skill root and hook source. Installation refuses an incomplete review
701
+ that covers only an older cache, before retirement of that cache can break prompt
702
+ delivery. Hook behavior still requires the existing no-skill-injection review.
699
703
 
700
704
  For a Codex 0.160.0, 0.160.1 or 0.161.0 remote refresh omitted by an existing qualified-name disable,
701
705
  a fresh native catalog and discovery review can plan the new exact path disable
package/bin/index.js CHANGED
@@ -36888,7 +36888,7 @@ var package_default;
36888
36888
  var init_package = __esm(() => {
36889
36889
  package_default = {
36890
36890
  name: "@hasna/skills",
36891
- version: "0.10.54",
36891
+ version: "0.10.56",
36892
36892
  description: "Skills library for AI coding agents",
36893
36893
  type: "module",
36894
36894
  bin: {
@@ -59361,6 +59361,97 @@ function read2(path, changes) {
59361
59361
  throw new Error(`Unsupported or oversized native discovery input: ${path}`);
59362
59362
  return readFileSync21(path, "utf8");
59363
59363
  }
59364
+ function assertReviewedClaudeCacheClosures(home, config, review) {
59365
+ const enabledIds = Object.entries(config.enabledPlugins ?? {}).filter(([, enabled]) => enabled === true).map(([id]) => id);
59366
+ if (enabledIds.length === 0)
59367
+ return;
59368
+ const registryPath = join37(home, ".claude/plugins/installed_plugins.json");
59369
+ const cacheRoot = join37(home, ".claude/plugins/cache");
59370
+ const registryText = read2(registryPath);
59371
+ if (registryText === null)
59372
+ return;
59373
+ const registry = parseConfig(registryText, registryPath);
59374
+ if (registry.version !== 2 || !registry.plugins || typeof registry.plugins !== "object" || Array.isArray(registry.plugins))
59375
+ throw new Error("Reviewed Claude installed plugin registrations are invalid");
59376
+ const cachePath = (path) => typeof path === "string" && isAbsolute22(path) && resolve21(path) === path && (path === cacheRoot || path.startsWith(cacheRoot + sep8));
59377
+ const activeRoots = new Set;
59378
+ for (const id of enabledIds) {
59379
+ const rows = registry.plugins[id];
59380
+ if (rows === undefined)
59381
+ continue;
59382
+ if (!Array.isArray(rows))
59383
+ throw new Error("Reviewed Claude enabled plugin registrations must be an array");
59384
+ for (const row of rows) {
59385
+ if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.installPath !== "string") {
59386
+ throw new Error("Reviewed Claude enabled plugin registration requires an object with an installPath string");
59387
+ }
59388
+ if (typeof row.installPath === "string" && (row.installPath === cacheRoot || row.installPath.startsWith(cacheRoot + sep8))) {
59389
+ if (!cachePath(row.installPath) || row.installPath.length > AGENT_POLICY_LIMITS.pathCharacters || row.version !== undefined && (typeof row.version !== "string" || basename11(row.installPath) !== row.version)) {
59390
+ throw new Error("Reviewed Claude discovery has a malformed active cache registration");
59391
+ }
59392
+ if (row.scope === "user" && (row.projectPath === undefined || row.projectPath === null)) {} else if (row.scope === "project" && typeof row.projectPath === "string" && isAbsolute22(row.projectPath) && resolve21(row.projectPath) === row.projectPath && row.projectPath.length <= AGENT_POLICY_LIMITS.pathCharacters) {} else
59393
+ throw new Error("Reviewed Claude discovery has an unsupported active cache registration scope");
59394
+ activeRoots.add(row.installPath);
59395
+ }
59396
+ }
59397
+ }
59398
+ if (activeRoots.size === 0)
59399
+ return;
59400
+ const registrySource = review.sources.find((source) => source.path === registryPath && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes"));
59401
+ if (!registrySource)
59402
+ throw new Error("Reviewed Claude discovery must bind installed registrations for active cache plugins");
59403
+ const reviewedFile = (path, manifest = false) => review.sources.some((source) => source.path === path && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes" || manifest && source.hashMode === "claude-plugin-manifest-v1"));
59404
+ const requireSource = (path, description, manifest = false) => {
59405
+ if (!reviewedFile(path, manifest))
59406
+ throw new Error(`Reviewed active Claude cache plugin ${description} is not bound`);
59407
+ };
59408
+ for (const root2 of activeRoots) {
59409
+ safe4(root2);
59410
+ const rootStat = lstatSync25(root2, { throwIfNoEntry: false });
59411
+ if (!rootStat?.isDirectory() || realpathSync11(root2) !== root2)
59412
+ throw new Error("Reviewed Claude active cache plugin root is missing or unsafe");
59413
+ const manifestPath = join37(root2, ".claude-plugin/plugin.json");
59414
+ const manifestStat = lstatSync25(manifestPath, { throwIfNoEntry: false });
59415
+ let manifest;
59416
+ if (manifestStat) {
59417
+ if (!manifestStat.isFile())
59418
+ throw new Error("Reviewed active Claude cache plugin manifest is unsupported");
59419
+ requireSource(manifestPath, "manifest", true);
59420
+ const raw = read2(manifestPath);
59421
+ if (raw === null)
59422
+ throw new Error("Reviewed Claude active cache plugin manifest is missing");
59423
+ manifest = JSON.parse(raw);
59424
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
59425
+ throw new Error("Reviewed Claude active cache plugin manifest is invalid");
59426
+ } else if (!review.sources.some((source) => source.path === manifestPath && source.hashMode === "path-bytes" && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined)) {
59427
+ throw new Error("Reviewed Claude active cache plugin manifest absence requires a path-bytes witness");
59428
+ }
59429
+ const roots = new Set([join37(root2, "skills")]);
59430
+ if (lstatSync25(join37(root2, "SKILL.md"), { throwIfNoEntry: false })?.isFile())
59431
+ roots.add(root2);
59432
+ const declared = manifest?.skills === undefined ? ["./skills"] : typeof manifest.skills === "string" ? [manifest.skills] : manifest.skills;
59433
+ if (!Array.isArray(declared) || declared.some((path) => typeof path !== "string"))
59434
+ throw new Error("Reviewed Claude active cache plugin has unsupported skill paths");
59435
+ for (const value of declared) {
59436
+ const path = resolve21(root2, value);
59437
+ if (path !== root2 && !path.startsWith(root2 + sep8))
59438
+ throw new Error("Reviewed Claude active cache plugin skill path escapes its root");
59439
+ roots.add(path.endsWith(`${sep8}SKILL.md`) ? dirname20(path) : path);
59440
+ }
59441
+ for (const skillRoot of roots)
59442
+ if (!review.roots.includes(skillRoot))
59443
+ throw new Error("Reviewed Claude active cache plugin skill root is not covered");
59444
+ const hooksPath = join37(root2, "hooks/hooks.json");
59445
+ const hooksStat = lstatSync25(hooksPath, { throwIfNoEntry: false });
59446
+ if (hooksStat) {
59447
+ if (!hooksStat.isFile())
59448
+ throw new Error("Reviewed Claude plugin hook manifest is unsupported");
59449
+ requireSource(hooksPath, "hook manifest");
59450
+ }
59451
+ if (typeof manifest?.hooks === "string")
59452
+ requireSource(resolve21(root2, manifest.hooks), "manifest-declared hook source");
59453
+ }
59454
+ }
59364
59455
  function projectNativeDiscoveryFields(text3, format, fields2, path) {
59365
59456
  const object6 = format === "yaml" ? parseHermesConfig(text3) : parseConfig(text3, path, format === "toml");
59366
59457
  if (!object6 || typeof object6 !== "object" || Array.isArray(object6))
@@ -59937,6 +60028,8 @@ function resolveAgentDiscovery(options) {
59937
60028
  throw new Error("Reviewed Claude plugin manifest requires separately reviewed exact hook file sources");
59938
60029
  return { path: source.path, sha256: manifestSha256, hashMode: "claude-plugin-manifest-v1" };
59939
60030
  });
60031
+ if (agent === "claude")
60032
+ assertReviewedClaudeCacheClosures(home, config, review);
59940
60033
  const combined = [...reviewedSources, ...reviewedSourcesWithManifestProjection];
59941
60034
  const unique = combined.filter((source, index) => combined.findIndex((candidate2) => isDeepStrictEqual11(candidate2, source)) === index);
59942
60035
  return { ...supplied, roots: [...new Set([...roots, ...supplied.roots])].sort(), sources: unique, ...agent === "gemini" ? { builtinNames } : {} };
@@ -86070,8 +86163,12 @@ function assertPlanWindow(issuedAt, expiresAt, now3) {
86070
86163
  throw new SkillSelectionError("SESSION_PLAN_EXPIRED", "The reviewed session reconciliation plan is not currently valid; inspect the receipt and prepare a new plan.");
86071
86164
  }
86072
86165
  }
86166
+ function selectionMetadata(selection) {
86167
+ return { slug: selection.slug, version: selection.version, bundleDigest: selection.bundleDigest };
86168
+ }
86073
86169
  function inspectSkillSession(sessionId, options = {}) {
86074
86170
  const snapshot2 = readSkillSessionSnapshot(sessionId, options), { receipt } = snapshot2;
86171
+ const loaded = new Set(receipt.loaded);
86075
86172
  return {
86076
86173
  sessionId,
86077
86174
  path: snapshot2.path,
@@ -86083,7 +86180,8 @@ function inspectSkillSession(sessionId, options = {}) {
86083
86180
  profileId: receipt.profile.profileId,
86084
86181
  profileRevision: receipt.profile.profileRevision,
86085
86182
  selectionCount: receipt.profile.selections.length,
86086
- loadedCount: receipt.loaded.length
86183
+ loadedCount: receipt.loaded.length,
86184
+ selections: receipt.profile.selections.map((selection) => ({ ...selectionMetadata(selection), loaded: loaded.has(selectionKey(selection)) }))
86087
86185
  };
86088
86186
  }
86089
86187
  function renewalDeadlineSignal(error61) {
@@ -86305,6 +86403,21 @@ async function reconcileSkillSession(input2, options = {}) {
86305
86403
  }
86306
86404
  const targetKeys = new Set(target.selections.map(selectionKey));
86307
86405
  const loaded = old.loaded.filter((key) => targetKeys.has(key));
86406
+ const previouslyLoaded = new Set(old.loaded);
86407
+ const targetBySlug = new Map(target.selections.map((selection) => [selection.slug, selection]));
86408
+ const previousSlugs = new Set(old.profile.selections.map((selection) => selection.slug));
86409
+ const selectionDelta = old.profile.selections.map((previous) => {
86410
+ const current = targetBySlug.get(previous.slug);
86411
+ const wasLoaded = previouslyLoaded.has(selectionKey(previous));
86412
+ const reason2 = !current ? "selection-removed" : selectionKey(previous) === selectionKey(current) ? "same-bundle" : "bundle-changed";
86413
+ return {
86414
+ selection: selectionMetadata(previous),
86415
+ loaded: wasLoaded,
86416
+ outcome: !wasLoaded ? "unloaded" : reason2 === "same-bundle" ? "retained" : "retired",
86417
+ reason: reason2,
86418
+ ...reason2 === "bundle-changed" ? { replacement: selectionMetadata(current) } : {}
86419
+ };
86420
+ });
86308
86421
  const replacementGeneration = nextSkillSessionGeneration(snapshot2.generation);
86309
86422
  const plan = {
86310
86423
  schemaVersion: 1,
@@ -86317,6 +86430,8 @@ async function reconcileSkillSession(input2, options = {}) {
86317
86430
  replacementGeneration,
86318
86431
  retainedLoadedCount: loaded.length,
86319
86432
  retiredLoadedCount: old.loaded.length - loaded.length,
86433
+ selectionDelta,
86434
+ addedSelections: target.selections.filter((selection) => !previousSlugs.has(selection.slug)).map(selectionMetadata),
86320
86435
  scope: "Only this receipt; existing child sessions, project locks, shared profiles, hooks and running processes are unchanged."
86321
86436
  };
86322
86437
  const planDigest = digest3(plan);
@@ -86356,6 +86471,9 @@ __export(exports_context, {
86356
86471
  contextResolverOptions: () => contextResolverOptions
86357
86472
  });
86358
86473
  import { readSync as readSync12 } from "fs";
86474
+ function sessionSelectionLabel(selection) {
86475
+ return `${selection.slug}@${selection.version} ${selection.bundleDigest}`;
86476
+ }
86359
86477
  function selectedProfileId(explicit) {
86360
86478
  return explicit ?? process.env.HASNA_SKILLS_SELECTION_PROFILE ?? readManagedSkillPolicy()?.profileId ?? "default";
86361
86479
  }
@@ -86373,8 +86491,12 @@ function registerContextCommands(parent) {
86373
86491
  sessions.command("show <id>").option("--json", "Return metadata as JSON", false).description("Inspect one session receipt without displaying payloads or changing its pin").action(async (id, options) => {
86374
86492
  try {
86375
86493
  const result2 = inspectSkillSession(id);
86376
- await writeCliOutput(options.json ? JSON.stringify(result2) : `${JSON.stringify(id)}: ${result2.profileId} at ${result2.profileRevision}
86377
- Receipt SHA256: ${result2.receiptSha256}`);
86494
+ await writeCliOutput(options.json ? JSON.stringify(result2) : [
86495
+ `${JSON.stringify(id)}: ${result2.profileId} at ${result2.profileRevision}`,
86496
+ `Receipt SHA256: ${result2.receiptSha256}`,
86497
+ ...result2.selections.map((selection) => `${selection.loaded ? "Loaded" : "Unloaded"}: ${sessionSelectionLabel(selection)}`)
86498
+ ].join(`
86499
+ `));
86378
86500
  } catch (error61) {
86379
86501
  reportContextError(error61, options.json);
86380
86502
  }
@@ -86401,7 +86523,13 @@ Review digest: ${result2.reviewDigest}`);
86401
86523
  try {
86402
86524
  const result2 = await reconcileSkillSession({ ...options, sessionId: id });
86403
86525
  await writeCliOutput(options.json ? JSON.stringify(result2) : result2.applied ? `Reconciled ${JSON.stringify(id)} to ${result2.plan.target.profileId} at ${result2.plan.target.profileRevision}.
86404
- Preserved original receipt: ${result2.archivePath}` : `Planned one session reconciliation. Review --json output, then use --apply --plan-digest ${result2.planDigest} --plan-issued-at ${result2.plan.issuedAt} --plan-expires-at ${result2.plan.expiresAt}.`);
86526
+ Preserved original receipt: ${result2.archivePath}` : [
86527
+ `Planned one session reconciliation: ${result2.plan.retainedLoadedCount} loaded retained, ${result2.plan.retiredLoadedCount} loaded retired.`,
86528
+ ...result2.plan.selectionDelta.map((row) => `${row.outcome} (${row.reason}): ${sessionSelectionLabel(row.selection)}${row.replacement ? ` -> ${sessionSelectionLabel(row.replacement)}` : ""}`),
86529
+ ...result2.plan.addedSelections.map((selection) => `Added selection (unloaded): ${sessionSelectionLabel(selection)}`),
86530
+ `Review --json output, then use --apply --plan-digest ${result2.planDigest} --plan-issued-at ${result2.plan.issuedAt} --plan-expires-at ${result2.plan.expiresAt}.`
86531
+ ].join(`
86532
+ `));
86405
86533
  } catch (error61) {
86406
86534
  reportContextError(error61, options.json);
86407
86535
  }
package/bin/mcp.js CHANGED
@@ -20019,7 +20019,7 @@ var package_default;
20019
20019
  var init_package = __esm(() => {
20020
20020
  package_default = {
20021
20021
  name: "@hasna/skills",
20022
- version: "0.10.54",
20022
+ version: "0.10.56",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
@@ -50788,6 +50788,97 @@ function read(path, changes) {
50788
50788
  throw new Error(`Unsupported or oversized native discovery input: ${path}`);
50789
50789
  return readFileSync15(path, "utf8");
50790
50790
  }
50791
+ function assertReviewedClaudeCacheClosures(home, config2, review) {
50792
+ const enabledIds = Object.entries(config2.enabledPlugins ?? {}).filter(([, enabled]) => enabled === true).map(([id]) => id);
50793
+ if (enabledIds.length === 0)
50794
+ return;
50795
+ const registryPath = join26(home, ".claude/plugins/installed_plugins.json");
50796
+ const cacheRoot = join26(home, ".claude/plugins/cache");
50797
+ const registryText = read(registryPath);
50798
+ if (registryText === null)
50799
+ return;
50800
+ const registry2 = parseConfig(registryText, registryPath);
50801
+ if (registry2.version !== 2 || !registry2.plugins || typeof registry2.plugins !== "object" || Array.isArray(registry2.plugins))
50802
+ throw new Error("Reviewed Claude installed plugin registrations are invalid");
50803
+ const cachePath = (path) => typeof path === "string" && isAbsolute15(path) && resolve17(path) === path && (path === cacheRoot || path.startsWith(cacheRoot + sep6));
50804
+ const activeRoots = new Set;
50805
+ for (const id of enabledIds) {
50806
+ const rows = registry2.plugins[id];
50807
+ if (rows === undefined)
50808
+ continue;
50809
+ if (!Array.isArray(rows))
50810
+ throw new Error("Reviewed Claude enabled plugin registrations must be an array");
50811
+ for (const row of rows) {
50812
+ if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.installPath !== "string") {
50813
+ throw new Error("Reviewed Claude enabled plugin registration requires an object with an installPath string");
50814
+ }
50815
+ if (typeof row.installPath === "string" && (row.installPath === cacheRoot || row.installPath.startsWith(cacheRoot + sep6))) {
50816
+ if (!cachePath(row.installPath) || row.installPath.length > AGENT_POLICY_LIMITS.pathCharacters || row.version !== undefined && (typeof row.version !== "string" || basename9(row.installPath) !== row.version)) {
50817
+ throw new Error("Reviewed Claude discovery has a malformed active cache registration");
50818
+ }
50819
+ if (row.scope === "user" && (row.projectPath === undefined || row.projectPath === null)) {} else if (row.scope === "project" && typeof row.projectPath === "string" && isAbsolute15(row.projectPath) && resolve17(row.projectPath) === row.projectPath && row.projectPath.length <= AGENT_POLICY_LIMITS.pathCharacters) {} else
50820
+ throw new Error("Reviewed Claude discovery has an unsupported active cache registration scope");
50821
+ activeRoots.add(row.installPath);
50822
+ }
50823
+ }
50824
+ }
50825
+ if (activeRoots.size === 0)
50826
+ return;
50827
+ const registrySource = review.sources.find((source) => source.path === registryPath && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes"));
50828
+ if (!registrySource)
50829
+ throw new Error("Reviewed Claude discovery must bind installed registrations for active cache plugins");
50830
+ const reviewedFile = (path, manifest = false) => review.sources.some((source) => source.path === path && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes" || manifest && source.hashMode === "claude-plugin-manifest-v1"));
50831
+ const requireSource = (path, description, manifest = false) => {
50832
+ if (!reviewedFile(path, manifest))
50833
+ throw new Error(`Reviewed active Claude cache plugin ${description} is not bound`);
50834
+ };
50835
+ for (const root of activeRoots) {
50836
+ safe4(root);
50837
+ const rootStat = lstatSync18(root, { throwIfNoEntry: false });
50838
+ if (!rootStat?.isDirectory() || realpathSync5(root) !== root)
50839
+ throw new Error("Reviewed Claude active cache plugin root is missing or unsafe");
50840
+ const manifestPath = join26(root, ".claude-plugin/plugin.json");
50841
+ const manifestStat = lstatSync18(manifestPath, { throwIfNoEntry: false });
50842
+ let manifest;
50843
+ if (manifestStat) {
50844
+ if (!manifestStat.isFile())
50845
+ throw new Error("Reviewed active Claude cache plugin manifest is unsupported");
50846
+ requireSource(manifestPath, "manifest", true);
50847
+ const raw = read(manifestPath);
50848
+ if (raw === null)
50849
+ throw new Error("Reviewed Claude active cache plugin manifest is missing");
50850
+ manifest = JSON.parse(raw);
50851
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
50852
+ throw new Error("Reviewed Claude active cache plugin manifest is invalid");
50853
+ } else if (!review.sources.some((source) => source.path === manifestPath && source.hashMode === "path-bytes" && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined)) {
50854
+ throw new Error("Reviewed Claude active cache plugin manifest absence requires a path-bytes witness");
50855
+ }
50856
+ const roots = new Set([join26(root, "skills")]);
50857
+ if (lstatSync18(join26(root, "SKILL.md"), { throwIfNoEntry: false })?.isFile())
50858
+ roots.add(root);
50859
+ const declared = manifest?.skills === undefined ? ["./skills"] : typeof manifest.skills === "string" ? [manifest.skills] : manifest.skills;
50860
+ if (!Array.isArray(declared) || declared.some((path) => typeof path !== "string"))
50861
+ throw new Error("Reviewed Claude active cache plugin has unsupported skill paths");
50862
+ for (const value of declared) {
50863
+ const path = resolve17(root, value);
50864
+ if (path !== root && !path.startsWith(root + sep6))
50865
+ throw new Error("Reviewed Claude active cache plugin skill path escapes its root");
50866
+ roots.add(path.endsWith(`${sep6}SKILL.md`) ? dirname16(path) : path);
50867
+ }
50868
+ for (const skillRoot of roots)
50869
+ if (!review.roots.includes(skillRoot))
50870
+ throw new Error("Reviewed Claude active cache plugin skill root is not covered");
50871
+ const hooksPath = join26(root, "hooks/hooks.json");
50872
+ const hooksStat = lstatSync18(hooksPath, { throwIfNoEntry: false });
50873
+ if (hooksStat) {
50874
+ if (!hooksStat.isFile())
50875
+ throw new Error("Reviewed Claude plugin hook manifest is unsupported");
50876
+ requireSource(hooksPath, "hook manifest");
50877
+ }
50878
+ if (typeof manifest?.hooks === "string")
50879
+ requireSource(resolve17(root, manifest.hooks), "manifest-declared hook source");
50880
+ }
50881
+ }
50791
50882
  function projectNativeDiscoveryFields(text2, format, fields2, path) {
50792
50883
  const object7 = format === "yaml" ? parseHermesConfig(text2) : parseConfig(text2, path, format === "toml");
50793
50884
  if (!object7 || typeof object7 !== "object" || Array.isArray(object7))
@@ -51254,6 +51345,8 @@ function resolveAgentDiscovery(options) {
51254
51345
  throw new Error("Reviewed Claude plugin manifest requires separately reviewed exact hook file sources");
51255
51346
  return { path: source.path, sha256: manifestSha256, hashMode: "claude-plugin-manifest-v1" };
51256
51347
  });
51348
+ if (agent === "claude")
51349
+ assertReviewedClaudeCacheClosures(home, config2, review);
51257
51350
  const combined = [...reviewedSources, ...reviewedSourcesWithManifestProjection];
51258
51351
  const unique = combined.filter((source, index) => combined.findIndex((candidate) => isDeepStrictEqual4(candidate, source)) === index);
51259
51352
  return { ...supplied, roots: [...new Set([...roots, ...supplied.roots])].sort(), sources: unique, ...agent === "gemini" ? { builtinNames } : {} };
package/bin/migrate.js CHANGED
@@ -25,7 +25,7 @@ import { join as join5 } from "path";
25
25
  // package.json
26
26
  var package_default = {
27
27
  name: "@hasna/skills",
28
- version: "0.10.54",
28
+ version: "0.10.56",
29
29
  description: "Skills library for AI coding agents",
30
30
  type: "module",
31
31
  bin: {
package/bin/server.js CHANGED
@@ -24140,7 +24140,7 @@ restoreLaunchCwd();
24140
24140
  // package.json
24141
24141
  var package_default = {
24142
24142
  name: "@hasna/skills",
24143
- version: "0.10.54",
24143
+ version: "0.10.56",
24144
24144
  description: "Skills library for AI coding agents",
24145
24145
  type: "module",
24146
24146
  bin: {
package/bin/worker.js CHANGED
@@ -24143,7 +24143,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24143
24143
  // package.json
24144
24144
  var package_default = {
24145
24145
  name: "@hasna/skills",
24146
- version: "0.10.54",
24146
+ version: "0.10.56",
24147
24147
  description: "Skills library for AI coding agents",
24148
24148
  type: "module",
24149
24149
  bin: {
package/dist/index.js CHANGED
@@ -11833,7 +11833,7 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
11833
11833
  // package.json
11834
11834
  var package_default = {
11835
11835
  name: "@hasna/skills",
11836
- version: "0.10.54",
11836
+ version: "0.10.56",
11837
11837
  description: "Skills library for AI coding agents",
11838
11838
  type: "module",
11839
11839
  bin: {
@@ -20693,6 +20693,97 @@ function read2(path, changes) {
20693
20693
  throw new Error(`Unsupported or oversized native discovery input: ${path}`);
20694
20694
  return readFileSync20(path, "utf8");
20695
20695
  }
20696
+ function assertReviewedClaudeCacheClosures(home, config, review) {
20697
+ const enabledIds = Object.entries(config.enabledPlugins ?? {}).filter(([, enabled]) => enabled === true).map(([id]) => id);
20698
+ if (enabledIds.length === 0)
20699
+ return;
20700
+ const registryPath = join32(home, ".claude/plugins/installed_plugins.json");
20701
+ const cacheRoot = join32(home, ".claude/plugins/cache");
20702
+ const registryText = read2(registryPath);
20703
+ if (registryText === null)
20704
+ return;
20705
+ const registry = parseConfig(registryText, registryPath);
20706
+ if (registry.version !== 2 || !registry.plugins || typeof registry.plugins !== "object" || Array.isArray(registry.plugins))
20707
+ throw new Error("Reviewed Claude installed plugin registrations are invalid");
20708
+ const cachePath = (path) => typeof path === "string" && isAbsolute21(path) && resolve20(path) === path && (path === cacheRoot || path.startsWith(cacheRoot + sep8));
20709
+ const activeRoots = new Set;
20710
+ for (const id of enabledIds) {
20711
+ const rows = registry.plugins[id];
20712
+ if (rows === undefined)
20713
+ continue;
20714
+ if (!Array.isArray(rows))
20715
+ throw new Error("Reviewed Claude enabled plugin registrations must be an array");
20716
+ for (const row of rows) {
20717
+ if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.installPath !== "string") {
20718
+ throw new Error("Reviewed Claude enabled plugin registration requires an object with an installPath string");
20719
+ }
20720
+ if (typeof row.installPath === "string" && (row.installPath === cacheRoot || row.installPath.startsWith(cacheRoot + sep8))) {
20721
+ if (!cachePath(row.installPath) || row.installPath.length > AGENT_POLICY_LIMITS.pathCharacters || row.version !== undefined && (typeof row.version !== "string" || basename11(row.installPath) !== row.version)) {
20722
+ throw new Error("Reviewed Claude discovery has a malformed active cache registration");
20723
+ }
20724
+ if (row.scope === "user" && (row.projectPath === undefined || row.projectPath === null)) {} else if (row.scope === "project" && typeof row.projectPath === "string" && isAbsolute21(row.projectPath) && resolve20(row.projectPath) === row.projectPath && row.projectPath.length <= AGENT_POLICY_LIMITS.pathCharacters) {} else
20725
+ throw new Error("Reviewed Claude discovery has an unsupported active cache registration scope");
20726
+ activeRoots.add(row.installPath);
20727
+ }
20728
+ }
20729
+ }
20730
+ if (activeRoots.size === 0)
20731
+ return;
20732
+ const registrySource = review.sources.find((source) => source.path === registryPath && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes"));
20733
+ if (!registrySource)
20734
+ throw new Error("Reviewed Claude discovery must bind installed registrations for active cache plugins");
20735
+ const reviewedFile = (path, manifest = false) => review.sources.some((source) => source.path === path && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes" || manifest && source.hashMode === "claude-plugin-manifest-v1"));
20736
+ const requireSource = (path, description, manifest = false) => {
20737
+ if (!reviewedFile(path, manifest))
20738
+ throw new Error(`Reviewed active Claude cache plugin ${description} is not bound`);
20739
+ };
20740
+ for (const root2 of activeRoots) {
20741
+ safe4(root2);
20742
+ const rootStat = lstatSync25(root2, { throwIfNoEntry: false });
20743
+ if (!rootStat?.isDirectory() || realpathSync11(root2) !== root2)
20744
+ throw new Error("Reviewed Claude active cache plugin root is missing or unsafe");
20745
+ const manifestPath = join32(root2, ".claude-plugin/plugin.json");
20746
+ const manifestStat = lstatSync25(manifestPath, { throwIfNoEntry: false });
20747
+ let manifest;
20748
+ if (manifestStat) {
20749
+ if (!manifestStat.isFile())
20750
+ throw new Error("Reviewed active Claude cache plugin manifest is unsupported");
20751
+ requireSource(manifestPath, "manifest", true);
20752
+ const raw = read2(manifestPath);
20753
+ if (raw === null)
20754
+ throw new Error("Reviewed Claude active cache plugin manifest is missing");
20755
+ manifest = JSON.parse(raw);
20756
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
20757
+ throw new Error("Reviewed Claude active cache plugin manifest is invalid");
20758
+ } else if (!review.sources.some((source) => source.path === manifestPath && source.hashMode === "path-bytes" && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined)) {
20759
+ throw new Error("Reviewed Claude active cache plugin manifest absence requires a path-bytes witness");
20760
+ }
20761
+ const roots = new Set([join32(root2, "skills")]);
20762
+ if (lstatSync25(join32(root2, "SKILL.md"), { throwIfNoEntry: false })?.isFile())
20763
+ roots.add(root2);
20764
+ const declared = manifest?.skills === undefined ? ["./skills"] : typeof manifest.skills === "string" ? [manifest.skills] : manifest.skills;
20765
+ if (!Array.isArray(declared) || declared.some((path) => typeof path !== "string"))
20766
+ throw new Error("Reviewed Claude active cache plugin has unsupported skill paths");
20767
+ for (const value of declared) {
20768
+ const path = resolve20(root2, value);
20769
+ if (path !== root2 && !path.startsWith(root2 + sep8))
20770
+ throw new Error("Reviewed Claude active cache plugin skill path escapes its root");
20771
+ roots.add(path.endsWith(`${sep8}SKILL.md`) ? dirname20(path) : path);
20772
+ }
20773
+ for (const skillRoot of roots)
20774
+ if (!review.roots.includes(skillRoot))
20775
+ throw new Error("Reviewed Claude active cache plugin skill root is not covered");
20776
+ const hooksPath = join32(root2, "hooks/hooks.json");
20777
+ const hooksStat = lstatSync25(hooksPath, { throwIfNoEntry: false });
20778
+ if (hooksStat) {
20779
+ if (!hooksStat.isFile())
20780
+ throw new Error("Reviewed Claude plugin hook manifest is unsupported");
20781
+ requireSource(hooksPath, "hook manifest");
20782
+ }
20783
+ if (typeof manifest?.hooks === "string")
20784
+ requireSource(resolve20(root2, manifest.hooks), "manifest-declared hook source");
20785
+ }
20786
+ }
20696
20787
  function projectNativeDiscoveryFields(text2, format, fields2, path) {
20697
20788
  const object6 = format === "yaml" ? parseHermesConfig(text2) : parseConfig(text2, path, format === "toml");
20698
20789
  if (!object6 || typeof object6 !== "object" || Array.isArray(object6))
@@ -21281,6 +21372,8 @@ function resolveAgentDiscovery(options) {
21281
21372
  throw new Error("Reviewed Claude plugin manifest requires separately reviewed exact hook file sources");
21282
21373
  return { path: source.path, sha256: manifestSha256, hashMode: "claude-plugin-manifest-v1" };
21283
21374
  });
21375
+ if (agent === "claude")
21376
+ assertReviewedClaudeCacheClosures(home, config, review);
21284
21377
  const combined = [...reviewedSources, ...reviewedSourcesWithManifestProjection];
21285
21378
  const unique = combined.filter((source, index) => combined.findIndex((candidate2) => isDeepStrictEqual7(candidate2, source)) === index);
21286
21379
  return { ...supplied, roots: [...new Set([...roots, ...supplied.roots])].sort(), sources: unique, ...agent === "gemini" ? { builtinNames } : {} };
@@ -48150,8 +48243,12 @@ function assertPlanWindow(issuedAt, expiresAt, now) {
48150
48243
  throw new SkillSelectionError("SESSION_PLAN_EXPIRED", "The reviewed session reconciliation plan is not currently valid; inspect the receipt and prepare a new plan.");
48151
48244
  }
48152
48245
  }
48246
+ function selectionMetadata(selection) {
48247
+ return { slug: selection.slug, version: selection.version, bundleDigest: selection.bundleDigest };
48248
+ }
48153
48249
  function inspectSkillSession(sessionId, options = {}) {
48154
48250
  const snapshot2 = readSkillSessionSnapshot(sessionId, options), { receipt } = snapshot2;
48251
+ const loaded = new Set(receipt.loaded);
48155
48252
  return {
48156
48253
  sessionId,
48157
48254
  path: snapshot2.path,
@@ -48163,7 +48260,8 @@ function inspectSkillSession(sessionId, options = {}) {
48163
48260
  profileId: receipt.profile.profileId,
48164
48261
  profileRevision: receipt.profile.profileRevision,
48165
48262
  selectionCount: receipt.profile.selections.length,
48166
- loadedCount: receipt.loaded.length
48263
+ loadedCount: receipt.loaded.length,
48264
+ selections: receipt.profile.selections.map((selection) => ({ ...selectionMetadata(selection), loaded: loaded.has(selectionKey(selection)) }))
48167
48265
  };
48168
48266
  }
48169
48267
  var SESSION_RENEWAL_REFUSAL_TTL_MS = 5 * 60 * 1000;
@@ -48198,6 +48296,21 @@ async function reconcileSkillSession(input2, options = {}) {
48198
48296
  }
48199
48297
  const targetKeys = new Set(target.selections.map(selectionKey));
48200
48298
  const loaded = old.loaded.filter((key) => targetKeys.has(key));
48299
+ const previouslyLoaded = new Set(old.loaded);
48300
+ const targetBySlug = new Map(target.selections.map((selection) => [selection.slug, selection]));
48301
+ const previousSlugs = new Set(old.profile.selections.map((selection) => selection.slug));
48302
+ const selectionDelta = old.profile.selections.map((previous) => {
48303
+ const current = targetBySlug.get(previous.slug);
48304
+ const wasLoaded = previouslyLoaded.has(selectionKey(previous));
48305
+ const reason2 = !current ? "selection-removed" : selectionKey(previous) === selectionKey(current) ? "same-bundle" : "bundle-changed";
48306
+ return {
48307
+ selection: selectionMetadata(previous),
48308
+ loaded: wasLoaded,
48309
+ outcome: !wasLoaded ? "unloaded" : reason2 === "same-bundle" ? "retained" : "retired",
48310
+ reason: reason2,
48311
+ ...reason2 === "bundle-changed" ? { replacement: selectionMetadata(current) } : {}
48312
+ };
48313
+ });
48201
48314
  const replacementGeneration = nextSkillSessionGeneration(snapshot2.generation);
48202
48315
  const plan = {
48203
48316
  schemaVersion: 1,
@@ -48210,6 +48323,8 @@ async function reconcileSkillSession(input2, options = {}) {
48210
48323
  replacementGeneration,
48211
48324
  retainedLoadedCount: loaded.length,
48212
48325
  retiredLoadedCount: old.loaded.length - loaded.length,
48326
+ selectionDelta,
48327
+ addedSelections: target.selections.filter((selection) => !previousSlugs.has(selection.slug)).map(selectionMetadata),
48213
48328
  scope: "Only this receipt; existing child sessions, project locks, shared profiles, hooks and running processes are unchanged."
48214
48329
  };
48215
48330
  const planDigest = digest3(plan);
@@ -16,6 +16,19 @@ export interface SessionReconciliationOptions extends SelectionCacheOptions {
16
16
  client?: ProfileClient;
17
17
  }
18
18
  export declare const SESSION_RECONCILIATION_PLAN_TTL_MS: number;
19
+ /** Exact bundle identity only; never spread receipt, policy or payload fields. */
20
+ export interface SessionSelectionMetadata {
21
+ slug: string;
22
+ version: string;
23
+ bundleDigest: string;
24
+ }
25
+ export interface SessionSelectionDelta {
26
+ selection: SessionSelectionMetadata;
27
+ loaded: boolean;
28
+ outcome: "retained" | "retired" | "unloaded";
29
+ reason: "same-bundle" | "selection-removed" | "bundle-changed";
30
+ replacement?: SessionSelectionMetadata;
31
+ }
19
32
  /** Metadata only: inspection never displays skill payloads or resolves credentials. */
20
33
  export declare function inspectSkillSession(sessionId: string, options?: SelectionCacheOptions): {
21
34
  sessionId: string;
@@ -29,6 +42,12 @@ export declare function inspectSkillSession(sessionId: string, options?: Selecti
29
42
  profileRevision: string;
30
43
  selectionCount: number;
31
44
  loadedCount: number;
45
+ selections: {
46
+ loaded: boolean;
47
+ slug: string;
48
+ version: string;
49
+ bundleDigest: string;
50
+ }[];
32
51
  };
33
52
  /** Fits inside the managed context child's 6.5-second ceiling, including CLI work. */
34
53
  export declare const SESSION_RENEWAL_TIMEOUT_MS = 4000;
@@ -75,6 +94,8 @@ export declare function reconcileSkillSession(input: SessionReconciliationInput,
75
94
  replacementGeneration: number;
76
95
  retainedLoadedCount: number;
77
96
  retiredLoadedCount: number;
97
+ selectionDelta: SessionSelectionDelta[];
98
+ addedSelections: SessionSelectionMetadata[];
78
99
  scope: string;
79
100
  };
80
101
  planDigest: string;
@@ -112,6 +133,8 @@ export declare function reconcileSkillSession(input: SessionReconciliationInput,
112
133
  replacementGeneration: number;
113
134
  retainedLoadedCount: number;
114
135
  retiredLoadedCount: number;
136
+ selectionDelta: SessionSelectionDelta[];
137
+ addedSelections: SessionSelectionMetadata[];
115
138
  scope: string;
116
139
  };
117
140
  planDigest: string;
@@ -19,7 +19,7 @@ export { CloudExecutionClient, type CloudExecution, type CloudEligibility } from
19
19
  export { readExecutionGrantPolicy, saveExecutionGrantPolicy, resolveExecutionGrant } from "../lib/execution-grant-client.js";
20
20
  export type { ExecutionGrant, ExecutionGrantPolicy, ExecutionGrantRequest, ResolvedExecutionGrant } from "../lib/execution-grants.js";
21
21
  export { HttpProfileClient, createProfileClient, type ProfileClient, type AuthenticatedProfilePrincipal, type AuthenticatedProfileRole } from "../lib/profile-client.js";
22
- export { inspectSkillSession, reconcileSkillSession, type SessionReconciliationInput, type SessionReconciliationOptions } from "../lib/session-reconciliation.js";
22
+ export { inspectSkillSession, reconcileSkillSession, type SessionSelectionMetadata, type SessionSelectionDelta, type SessionReconciliationInput, type SessionReconciliationOptions } from "../lib/session-reconciliation.js";
23
23
  export { resolveSelectedRun, executeSelectedLocal, prepareSelectedSecretBindings, type ResolvedSelectedRun, type SelectedLocalRunOptions } from "../lib/selected-run.js";
24
24
  export { readSelectedSecretBindings, type SelectedSecretBindings, type SelectedSecretsClient } from "../lib/execution-secrets.js";
25
25
  export type { SkillSelection, SkillProfile, ResolvedSkillSelection, ResolvedSkillProfile, StationSkillStateInput, StationSkillState } from "../types/skill-selection.js";
package/dist/sdk/index.js CHANGED
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
32373
32373
  // package.json
32374
32374
  var package_default = {
32375
32375
  name: "@hasna/skills",
32376
- version: "0.10.54",
32376
+ version: "0.10.56",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -37490,8 +37490,12 @@ function assertPlanWindow(issuedAt, expiresAt, now) {
37490
37490
  throw new SkillSelectionError("SESSION_PLAN_EXPIRED", "The reviewed session reconciliation plan is not currently valid; inspect the receipt and prepare a new plan.");
37491
37491
  }
37492
37492
  }
37493
+ function selectionMetadata(selection2) {
37494
+ return { slug: selection2.slug, version: selection2.version, bundleDigest: selection2.bundleDigest };
37495
+ }
37493
37496
  function inspectSkillSession(sessionId, options = {}) {
37494
37497
  const snapshot = readSkillSessionSnapshot(sessionId, options), { receipt } = snapshot;
37498
+ const loaded = new Set(receipt.loaded);
37495
37499
  return {
37496
37500
  sessionId,
37497
37501
  path: snapshot.path,
@@ -37503,7 +37507,8 @@ function inspectSkillSession(sessionId, options = {}) {
37503
37507
  profileId: receipt.profile.profileId,
37504
37508
  profileRevision: receipt.profile.profileRevision,
37505
37509
  selectionCount: receipt.profile.selections.length,
37506
- loadedCount: receipt.loaded.length
37510
+ loadedCount: receipt.loaded.length,
37511
+ selections: receipt.profile.selections.map((selection2) => ({ ...selectionMetadata(selection2), loaded: loaded.has(selectionKey(selection2)) }))
37507
37512
  };
37508
37513
  }
37509
37514
  var SESSION_RENEWAL_REFUSAL_TTL_MS = 5 * 60 * 1000;
@@ -37538,6 +37543,21 @@ async function reconcileSkillSession(input, options = {}) {
37538
37543
  }
37539
37544
  const targetKeys = new Set(target.selections.map(selectionKey));
37540
37545
  const loaded = old.loaded.filter((key) => targetKeys.has(key));
37546
+ const previouslyLoaded = new Set(old.loaded);
37547
+ const targetBySlug = new Map(target.selections.map((selection2) => [selection2.slug, selection2]));
37548
+ const previousSlugs = new Set(old.profile.selections.map((selection2) => selection2.slug));
37549
+ const selectionDelta = old.profile.selections.map((previous) => {
37550
+ const current = targetBySlug.get(previous.slug);
37551
+ const wasLoaded = previouslyLoaded.has(selectionKey(previous));
37552
+ const reason = !current ? "selection-removed" : selectionKey(previous) === selectionKey(current) ? "same-bundle" : "bundle-changed";
37553
+ return {
37554
+ selection: selectionMetadata(previous),
37555
+ loaded: wasLoaded,
37556
+ outcome: !wasLoaded ? "unloaded" : reason === "same-bundle" ? "retained" : "retired",
37557
+ reason,
37558
+ ...reason === "bundle-changed" ? { replacement: selectionMetadata(current) } : {}
37559
+ };
37560
+ });
37541
37561
  const replacementGeneration = nextSkillSessionGeneration(snapshot.generation);
37542
37562
  const plan = {
37543
37563
  schemaVersion: 1,
@@ -37550,6 +37570,8 @@ async function reconcileSkillSession(input, options = {}) {
37550
37570
  replacementGeneration,
37551
37571
  retainedLoadedCount: loaded.length,
37552
37572
  retiredLoadedCount: old.loaded.length - loaded.length,
37573
+ selectionDelta,
37574
+ addedSelections: target.selections.filter((selection2) => !previousSlugs.has(selection2.slug)).map(selectionMetadata),
37553
37575
  scope: "Only this receipt; existing child sessions, project locks, shared profiles, hooks and running processes are unchanged."
37554
37576
  };
37555
37577
  const planDigest = digest(plan);
@@ -43511,6 +43533,97 @@ function read2(path, changes) {
43511
43533
  throw new Error(`Unsupported or oversized native discovery input: ${path}`);
43512
43534
  return readFileSync15(path, "utf8");
43513
43535
  }
43536
+ function assertReviewedClaudeCacheClosures(home, config, review) {
43537
+ const enabledIds = Object.entries(config.enabledPlugins ?? {}).filter(([, enabled]) => enabled === true).map(([id2]) => id2);
43538
+ if (enabledIds.length === 0)
43539
+ return;
43540
+ const registryPath = join26(home, ".claude/plugins/installed_plugins.json");
43541
+ const cacheRoot = join26(home, ".claude/plugins/cache");
43542
+ const registryText = read2(registryPath);
43543
+ if (registryText === null)
43544
+ return;
43545
+ const registry = parseConfig(registryText, registryPath);
43546
+ if (registry.version !== 2 || !registry.plugins || typeof registry.plugins !== "object" || Array.isArray(registry.plugins))
43547
+ throw new Error("Reviewed Claude installed plugin registrations are invalid");
43548
+ const cachePath = (path) => typeof path === "string" && isAbsolute20(path) && resolve19(path) === path && (path === cacheRoot || path.startsWith(cacheRoot + sep6));
43549
+ const activeRoots = new Set;
43550
+ for (const id2 of enabledIds) {
43551
+ const rows = registry.plugins[id2];
43552
+ if (rows === undefined)
43553
+ continue;
43554
+ if (!Array.isArray(rows))
43555
+ throw new Error("Reviewed Claude enabled plugin registrations must be an array");
43556
+ for (const row of rows) {
43557
+ if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.installPath !== "string") {
43558
+ throw new Error("Reviewed Claude enabled plugin registration requires an object with an installPath string");
43559
+ }
43560
+ if (typeof row.installPath === "string" && (row.installPath === cacheRoot || row.installPath.startsWith(cacheRoot + sep6))) {
43561
+ if (!cachePath(row.installPath) || row.installPath.length > AGENT_POLICY_LIMITS.pathCharacters || row.version !== undefined && (typeof row.version !== "string" || basename9(row.installPath) !== row.version)) {
43562
+ throw new Error("Reviewed Claude discovery has a malformed active cache registration");
43563
+ }
43564
+ if (row.scope === "user" && (row.projectPath === undefined || row.projectPath === null)) {} else if (row.scope === "project" && typeof row.projectPath === "string" && isAbsolute20(row.projectPath) && resolve19(row.projectPath) === row.projectPath && row.projectPath.length <= AGENT_POLICY_LIMITS.pathCharacters) {} else
43565
+ throw new Error("Reviewed Claude discovery has an unsupported active cache registration scope");
43566
+ activeRoots.add(row.installPath);
43567
+ }
43568
+ }
43569
+ }
43570
+ if (activeRoots.size === 0)
43571
+ return;
43572
+ const registrySource = review.sources.find((source) => source.path === registryPath && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes"));
43573
+ if (!registrySource)
43574
+ throw new Error("Reviewed Claude discovery must bind installed registrations for active cache plugins");
43575
+ const reviewedFile = (path, manifest = false) => review.sources.some((source) => source.path === path && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && (source.hashMode === undefined || source.hashMode === "bytes" || manifest && source.hashMode === "claude-plugin-manifest-v1"));
43576
+ const requireSource = (path, description, manifest = false) => {
43577
+ if (!reviewedFile(path, manifest))
43578
+ throw new Error(`Reviewed active Claude cache plugin ${description} is not bound`);
43579
+ };
43580
+ for (const root2 of activeRoots) {
43581
+ safe4(root2);
43582
+ const rootStat = lstatSync21(root2, { throwIfNoEntry: false });
43583
+ if (!rootStat?.isDirectory() || realpathSync9(root2) !== root2)
43584
+ throw new Error("Reviewed Claude active cache plugin root is missing or unsafe");
43585
+ const manifestPath = join26(root2, ".claude-plugin/plugin.json");
43586
+ const manifestStat = lstatSync21(manifestPath, { throwIfNoEntry: false });
43587
+ let manifest;
43588
+ if (manifestStat) {
43589
+ if (!manifestStat.isFile())
43590
+ throw new Error("Reviewed active Claude cache plugin manifest is unsupported");
43591
+ requireSource(manifestPath, "manifest", true);
43592
+ const raw = read2(manifestPath);
43593
+ if (raw === null)
43594
+ throw new Error("Reviewed Claude active cache plugin manifest is missing");
43595
+ manifest = JSON.parse(raw);
43596
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest))
43597
+ throw new Error("Reviewed Claude active cache plugin manifest is invalid");
43598
+ } else if (!review.sources.some((source) => source.path === manifestPath && source.hashMode === "path-bytes" && source.sha256 !== null && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined)) {
43599
+ throw new Error("Reviewed Claude active cache plugin manifest absence requires a path-bytes witness");
43600
+ }
43601
+ const roots = new Set([join26(root2, "skills")]);
43602
+ if (lstatSync21(join26(root2, "SKILL.md"), { throwIfNoEntry: false })?.isFile())
43603
+ roots.add(root2);
43604
+ const declared = manifest?.skills === undefined ? ["./skills"] : typeof manifest.skills === "string" ? [manifest.skills] : manifest.skills;
43605
+ if (!Array.isArray(declared) || declared.some((path) => typeof path !== "string"))
43606
+ throw new Error("Reviewed Claude active cache plugin has unsupported skill paths");
43607
+ for (const value of declared) {
43608
+ const path = resolve19(root2, value);
43609
+ if (path !== root2 && !path.startsWith(root2 + sep6))
43610
+ throw new Error("Reviewed Claude active cache plugin skill path escapes its root");
43611
+ roots.add(path.endsWith(`${sep6}SKILL.md`) ? dirname17(path) : path);
43612
+ }
43613
+ for (const skillRoot of roots)
43614
+ if (!review.roots.includes(skillRoot))
43615
+ throw new Error("Reviewed Claude active cache plugin skill root is not covered");
43616
+ const hooksPath = join26(root2, "hooks/hooks.json");
43617
+ const hooksStat = lstatSync21(hooksPath, { throwIfNoEntry: false });
43618
+ if (hooksStat) {
43619
+ if (!hooksStat.isFile())
43620
+ throw new Error("Reviewed Claude plugin hook manifest is unsupported");
43621
+ requireSource(hooksPath, "hook manifest");
43622
+ }
43623
+ if (typeof manifest?.hooks === "string")
43624
+ requireSource(resolve19(root2, manifest.hooks), "manifest-declared hook source");
43625
+ }
43626
+ }
43514
43627
  function projectNativeDiscoveryFields(text2, format, fields2, path) {
43515
43628
  const object6 = format === "yaml" ? parseHermesConfig(text2) : parseConfig(text2, path, format === "toml");
43516
43629
  if (!object6 || typeof object6 !== "object" || Array.isArray(object6))
@@ -44099,6 +44212,8 @@ function resolveAgentDiscovery(options) {
44099
44212
  throw new Error("Reviewed Claude plugin manifest requires separately reviewed exact hook file sources");
44100
44213
  return { path: source.path, sha256: manifestSha256, hashMode: "claude-plugin-manifest-v1" };
44101
44214
  });
44215
+ if (agent === "claude")
44216
+ assertReviewedClaudeCacheClosures(home, config, review);
44102
44217
  const combined = [...reviewedSources, ...reviewedSourcesWithManifestProjection];
44103
44218
  const unique = combined.filter((source, index) => combined.findIndex((candidate2) => isDeepStrictEqual6(candidate2, source)) === index);
44104
44219
  return { ...supplied, roots: [...new Set([...roots, ...supplied.roots])].sort(), sources: unique, ...agent === "gemini" ? { builtinNames } : {} };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.54",
3
+ "version": "0.10.56",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {