@hasna/skills 0.10.73 → 0.10.75

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.
@@ -49,7 +49,7 @@ var __require = import.meta.require;
49
49
  // src/runtime-prerequisites.ts
50
50
  import { isAbsolute as isAbsolute7 } from "path";
51
51
  // package.json
52
- var version = "0.10.73";
52
+ var version = "0.10.75";
53
53
  var package_default = {
54
54
  name: "@hasna/skills",
55
55
  version,
package/dist/sdk/index.js CHANGED
@@ -32371,7 +32371,7 @@ var init_event_sink = __esm(() => {
32371
32371
  init_dist();
32372
32372
  });
32373
32373
  // package.json
32374
- var version = "0.10.73";
32374
+ var version = "0.10.75";
32375
32375
  var package_default = {
32376
32376
  name: "@hasna/skills",
32377
32377
  version,
@@ -46491,6 +46491,12 @@ function verifyAgentDiscovery(binding, codexRecovery, reviewedRootAliases, witne
46491
46491
  }
46492
46492
  var CLAUDE_CONTROL_WITNESS_MODES = new Set(["claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-settings-v4", "claude-marketplace-registry", "claude-marketplace-registry-v2", "claude-marketplace-entry-v1", "claude-plugin-registry"]);
46493
46493
  var CLAUDE_CONTROL_FILES = new Set(["settings.json", "settings.local.json", "installed_plugins.json", "known_marketplaces.json"]);
46494
+ function claudeControlSource(source) {
46495
+ return CLAUDE_CONTROL_WITNESS_MODES.has(source.hashMode ?? "") || CLAUDE_CONTROL_FILES.has(basename12(source.path)) && !source.path.includes(`${sep13}.claude${sep13}plugins${sep13}cache${sep13}`);
46496
+ }
46497
+ function retainedAbsentClaudeRoot(absentRoots, source) {
46498
+ return claudeControlSource(source) ? undefined : retainedAbsentRoot(absentRoots, source);
46499
+ }
46494
46500
  function codexInstallationRoots(binding) {
46495
46501
  const proof = binding.codexInstallationInputs;
46496
46502
  if (proof === undefined)
@@ -46900,7 +46906,10 @@ function resolveAgentDiscovery(options) {
46900
46906
  if (!review.sources.some((source) => source.path === canonical9(configPath)))
46901
46907
  throw new Error("Discovery review must include the agent configuration source");
46902
46908
  const reviewedSources = supersedesSettingsProjection(agent, review.sources, canonical9(configPath)) ? sources.filter((source) => !isSupersededSettingsProjection(agent, canonical9(configPath), source)) : sources;
46909
+ const absentRoots = absentRetiredClaudeRoots(supplied);
46903
46910
  const reviewedSourcesWithManifestProjection = review.sources.map((source) => {
46911
+ if (retainedAbsentClaudeRoot(absentRoots, source) !== undefined)
46912
+ return source;
46904
46913
  if (agent === "claude" && source.format === undefined && source.fields === undefined && source.managedPlugins === undefined && source.reviewArtifact === undefined && [undefined, "bytes", "path-bytes"].includes(source.hashMode) && isClaudePluginCacheOrphanMarker(source.path)) {
46905
46914
  return { path: source.path, hashMode: CLAUDE_ORPHAN_MARKER_MODE, sha256: hashClaudePluginCacheOrphanMarker(source.path) };
46906
46915
  }
@@ -46933,6 +46942,11 @@ function resolveAgentDiscovery(options) {
46933
46942
  });
46934
46943
  if (agent === "claude")
46935
46944
  assertReviewedClaudeCacheClosures(home, config, review, canonical9);
46945
+ for (const root2 of absentRoots) {
46946
+ safe6(root2);
46947
+ if (lstatSync28(root2, { throwIfNoEntry: false }))
46948
+ throw new Error("Retired native materialization changed during discovery review");
46949
+ }
46936
46950
  const combined = [...reviewedSources, ...reviewedSourcesWithManifestProjection];
46937
46951
  const unique = combined.filter((source, index) => combined.findIndex((candidate2) => isDeepStrictEqual8(candidate2, source)) === index);
46938
46952
  return { ...supplied, roots: [...new Set([...roots, ...supplied.roots])].sort(), sources: unique, ...agent === "gemini" ? { builtinNames } : {} };
@@ -48259,11 +48273,19 @@ function planAgentIntegration(options) {
48259
48273
  const executable2 = discoveryExecutable(agent) ?? (prior ? recordedDiscoveryExecutable(agent, prior) : null);
48260
48274
  executables.set(agent, executable2);
48261
48275
  const retainedReview = options.discoveryInputs === undefined && policy.bridge?.discovery?.[agent]?.method === "reviewed" ? { ...policy.bridge.discovery[agent], ...agent === "codex" ? { codexDisabledPluginSkills: policy.bridge.codexPluginSkills ?? [] } : {} } : undefined;
48262
- if (retainedReview) {
48263
- assertManagedAgentBridge(agent, { home, dataDir: dataDir2, projectDir: options.projectDir ?? home });
48264
- retainedAgents.push(agent);
48276
+ try {
48277
+ if (retainedReview) {
48278
+ assertRetainedReviewState(agent, { home, dataDir: dataDir2, projectDir: options.projectDir ?? home });
48279
+ retainedAgents.push(agent);
48280
+ }
48281
+ return resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, retainedReview, canonical: (path) => canonicalAgentPath(path, aliases), executable: executable2 });
48282
+ } catch (error) {
48283
+ if (isDiscoveryRootUnresolved(error) || error instanceof SumiPathResolverError || error instanceof NativeSkillCopiesDriftError)
48284
+ throw error;
48285
+ if (!retainedReview)
48286
+ throw error;
48287
+ throw new Error(`NATIVE_SKILL_DRIFT: ${error.message.replace(/^NATIVE_SKILL_DRIFT: /, "")}. This agent reuses a saved discovery review and that review no longer verifies, so a fresh explicit review is required: skills hook install --agent ${agent} --discovery-inputs <review> --apply.`);
48265
48288
  }
48266
- return resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, retainedReview, canonical: (path) => canonicalAgentPath(path, aliases), executable: executable2 });
48267
48289
  });
48268
48290
  const codexConfigBefore = options.agents.includes("codex") ? readOptional(canonicalAgentPath(join33(home, ".codex", "config.toml"), aliases)) : null;
48269
48291
  const codexConfig = Bun.TOML.parse(codexConfigBefore ?? "");
@@ -49017,7 +49039,7 @@ function applyAgentIntegrationUnlocked(plan, options = {}, assertCurrent) {
49017
49039
  assertCurrent();
49018
49040
  recheckRootAliases(aliases);
49019
49041
  for (const agent of plan.retainedReviewChecks?.agents ?? [])
49020
- assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
49042
+ assertRetainedReviewState(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
49021
49043
  if (plan.reviewArtifactMigration) {
49022
49044
  const verified = planReviewedArtifactMigration(plan.reviewArtifactMigration);
49023
49045
  if (JSON.stringify(verified.changes) !== JSON.stringify(plan.changes) || JSON.stringify(verified.discoveryAfter) !== JSON.stringify(plan.discoveryAfter))
@@ -49060,6 +49082,8 @@ function applyAgentIntegrationUnlocked(plan, options = {}, assertCurrent) {
49060
49082
  if (!plan.changes.length) {
49061
49083
  assertCurrent();
49062
49084
  recheckRootAliases(aliases);
49085
+ for (const agent of plan.retainedReviewChecks?.agents ?? [])
49086
+ assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
49063
49087
  return { changed: [], backups, ...aliases.length ? { rootAliases: aliases } : {} };
49064
49088
  }
49065
49089
  assertSafePath(backupRoot);
@@ -49109,6 +49133,8 @@ function applyAgentIntegrationUnlocked(plan, options = {}, assertCurrent) {
49109
49133
  }
49110
49134
  for (const agent of plan.managedAgentChecks?.agents ?? [])
49111
49135
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.projectDir ?? plan.managedAgentChecks.home });
49136
+ for (const agent of plan.retainedReviewChecks?.agents ?? [])
49137
+ assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
49112
49138
  atomicWrite2(join33(backupRoot, "receipt.json"), JSON.stringify({ version: 1, changes: written.map((change) => ({ path: change.path, beforeHash: change.before === null ? null : sha5(change.before), afterHash: sha5(change.after) })), backups, ...plan.cacheWitnessUpgrade ? { cacheWitnessUpgrade: plan.cacheWitnessUpgrade } : {}, ...plan.retainedReviewArtifacts?.length ? { retainedReviewArtifacts: plan.retainedReviewArtifacts.map((item) => ({ path: item.path, ...item.artifact })) } : {}, ...aliases.length ? { rootAliases: aliases } : {} }) + `
49113
49139
  `);
49114
49140
  } catch (error) {
@@ -49440,7 +49466,7 @@ enabled = false
49440
49466
  function assertManagedAgentBridge(agent, options = {}) {
49441
49467
  return assertManagedAgentBridgeWithDiscovery(agent, options);
49442
49468
  }
49443
- function assertManagedAgentBridgeWithDiscovery(agent, options = {}, provenDiscovery) {
49469
+ function assertManagedBridgeCustody(agent, options = {}, provenDiscovery) {
49444
49470
  const home = resolve27(options.home ?? homedir3()), dataDir2 = options.dataDir ?? getDataDirReadOnly();
49445
49471
  assertSafePath(join33(dataDir2, "agent-policy.json"));
49446
49472
  const snapshot2 = readManagedSkillPolicySnapshot(dataDir2);
@@ -49457,7 +49483,6 @@ function assertManagedAgentBridgeWithDiscovery(agent, options = {}, provenDiscov
49457
49483
  if (!isOwnedCliBridge(expected, [expected]))
49458
49484
  throw new Error("NATIVE_SKILL_DRIFT: the native Skills bridge is missing or modified; repair it before continuing");
49459
49485
  const roots = projectAncestorDirectories([options.projectDir ?? process.cwd(), ...options.projectDirs ?? [], ...agent === "hermes" && process.env.TERMINAL_CWD ? [process.cwd()] : []]);
49460
- const visible = (entry) => entry.agent === agent || ["codex", "gemini", "opencode", "hermes", "sumi"].includes(agent) && entry.path.includes(`${sep14}.agents${sep14}skills${sep14}`) || ["opencode", "sumi"].includes(agent) && entry.agent === "claude";
49461
49486
  const discovery = provenDiscovery ?? binding.discovery?.[agent];
49462
49487
  assertProjectDiscovery(agent, [...roots], home, (path) => canonicalAgentPath(path, aliases), discovery);
49463
49488
  for (const source of discovery?.sources ?? [])
@@ -49472,11 +49497,30 @@ function assertManagedAgentBridgeWithDiscovery(agent, options = {}, provenDiscov
49472
49497
  throw new Error("NATIVE_SKILL_DRIFT: the native hook command/profile binding is missing");
49473
49498
  if (options.profileId !== undefined && options.profileId !== profile)
49474
49499
  throw new Error("NATIVE_SKILL_DRIFT: the hook selection profile differs from its managed binding; restart the native client after reviewing skills hook install");
49500
+ return { home, dataDir: dataDir2, binding, aliases, expected, roots, discovery, configPath, config, command, profile };
49501
+ }
49502
+ function assertUnrepairableBridgeProtection(custody, agent) {
49503
+ if (agent !== "hermes")
49504
+ return;
49505
+ const { home, dataDir: dataDir2, binding, config, command, profile } = custody;
49506
+ const supervisor = binding.supervisors?.hermes;
49507
+ if (supervisor?.path !== join33(dataDir2, "agent-hooks", "hermes.js") || supervisor?.sha256 !== sha5(renderHermesSupervisor(command, profile)))
49508
+ throw new Error("NATIVE_SKILL_DRIFT: Hermes supervisor binding changed; run skills hook install");
49509
+ assertHermesProtection(home, config, command, profile, supervisor);
49510
+ }
49511
+ function assertRetainedReviewState(agent, options) {
49512
+ const custody = assertManagedBridgeCustody(agent, options);
49513
+ assertUnrepairableBridgeProtection(custody, agent);
49514
+ assertManagedNativeDiscovery(custody, agent, options);
49515
+ }
49516
+
49517
+ class NativeSkillCopiesDriftError extends Error {
49518
+ }
49519
+ function assertManagedAgentBridgeWithDiscovery(agent, options = {}, provenDiscovery) {
49520
+ const custody = assertManagedBridgeCustody(agent, options, provenDiscovery);
49521
+ const { home, aliases, expected, discovery, config, command, profile } = custody;
49475
49522
  if (agent === "hermes") {
49476
- const supervisor = binding.supervisors?.hermes;
49477
- if (supervisor?.path !== join33(dataDir2, "agent-hooks", "hermes.js") || supervisor?.sha256 !== sha5(renderHermesSupervisor(command, profile)))
49478
- throw new Error("NATIVE_SKILL_DRIFT: Hermes supervisor binding changed; run skills hook install");
49479
- assertHermesProtection(home, config, command, profile, supervisor);
49523
+ assertUnrepairableBridgeProtection(custody, agent);
49480
49524
  } else if (agent === "sumi") {
49481
49525
  if (JSON.stringify(config.permissions?.filter((rule) => rule.action === "skill")) !== JSON.stringify(SUMI_SKILL_PERMISSIONS) || !isManagedSumiPlugin(readOptional(join33(sumiConfigDirectory(home, process.env, options.projectDir ?? process.cwd()), "plugins", "skills-cli.js")), command, profile))
49482
49526
  throw new Error("NATIVE_SKILL_DRIFT: Sumi bridge protection changed; run skills hook install");
@@ -49498,6 +49542,21 @@ function assertManagedAgentBridgeWithDiscovery(agent, options = {}, provenDiscov
49498
49542
  if (agent === "gemini" && (config.hooksConfig?.enabled === false || config.skills?.enabled !== true || !["antigravity-support", "skill-creator"].every((name) => config.skills?.disabled?.includes(name)) || config.skills?.disabled?.includes(CLI_BRIDGE_NAME)))
49499
49543
  throw new Error("NATIVE_SKILL_DRIFT: Gemini bridge or bundled-skill protection changed; run skills hook install");
49500
49544
  }
49545
+ if (agent === "codex") {
49546
+ const codexConfig = Bun.TOML.parse(readOptional(canonicalAgentPath(join33(home, ".codex", "config.toml"), aliases)) ?? "");
49547
+ if (codexConfig.skills?.bundled?.enabled !== false)
49548
+ throw new Error("NATIVE_SKILL_DRIFT: Codex bundled skill reseeding is not disabled (skills.bundled.enabled); run skills hook install");
49549
+ const setting = (path) => (codexConfig.skills?.config ?? []).filter((entry) => typeof entry.path === "string" && [path, join33(path, "SKILL.md")].includes(canonicalAgentPath(entry.path, aliases)));
49550
+ if ([...setting(expected), ...(codexConfig.skills?.config ?? []).filter((entry) => typeof entry.name === "string" && entry.name.trim() === CLI_BRIDGE_NAME)].some((entry) => entry.enabled === false))
49551
+ throw new Error("NATIVE_SKILL_DRIFT: the Codex CLI bridge is disabled; run skills hook install");
49552
+ }
49553
+ if (agent === "gemini" && discovery && !discovery.builtinNames?.every((name) => config.skills.disabled.includes(name)))
49554
+ throw new Error("NATIVE_SKILL_DRIFT: an installed Gemini builtin is not disabled");
49555
+ return assertManagedNativeDiscovery(custody, agent, options);
49556
+ }
49557
+ function assertManagedNativeDiscovery(custody, agent, options) {
49558
+ const { home, dataDir: dataDir2, binding, aliases, expected, roots, discovery } = custody;
49559
+ const visible = (entry) => entry.agent === agent || ["codex", "gemini", "opencode", "hermes", "sumi"].includes(agent) && entry.path.includes(`${sep14}.agents${sep14}skills${sep14}`) || ["opencode", "sumi"].includes(agent) && entry.agent === "claude";
49501
49560
  const codexPath = canonicalAgentPath(join33(home, ".codex", "config.toml"), aliases);
49502
49561
  const vendorCache = canonicalAgentPath(join33(home, ".codex", "plugins", "cache"), aliases);
49503
49562
  const witnessTolerance = agent === "codex" && options.codexPromptHook === true ? {
@@ -49508,11 +49567,6 @@ function assertManagedAgentBridgeWithDiscovery(agent, options = {}, provenDiscov
49508
49567
  if (options.codexDiscoveryRecovery && (agent !== "codex" || options.codexDiscoveryRecovery.configPath !== codexPath))
49509
49568
  throw new Error("NATIVE_SKILL_DRIFT: Codex trust recovery names a different configuration root");
49510
49569
  const codexConfig = agent === "codex" ? Bun.TOML.parse(readOptional(codexPath) ?? "") : {};
49511
- if (agent === "codex" && codexConfig.skills?.bundled?.enabled !== false)
49512
- throw new Error("NATIVE_SKILL_DRIFT: Codex bundled skill reseeding is not disabled (skills.bundled.enabled); run skills hook install");
49513
- const setting = (path) => (codexConfig.skills?.config ?? []).filter((entry) => typeof entry.path === "string" && [path, join33(path, "SKILL.md")].includes(canonicalAgentPath(entry.path, aliases)));
49514
- if (agent === "codex" && [...setting(expected), ...(codexConfig.skills?.config ?? []).filter((entry) => typeof entry.name === "string" && entry.name.trim() === CLI_BRIDGE_NAME)].some((entry) => entry.enabled === false))
49515
- throw new Error("NATIVE_SKILL_DRIFT: the Codex CLI bridge is disabled; run skills hook install");
49516
49570
  if (agent === "codex" && !reviewedCodexPluginCapabilitiesUnchanged(canonicalAgentPath(join33(home, ".codex/plugins/cache"), aliases), binding.codexPluginSkills ?? [], (path) => new TextDecoder("utf-8", { fatal: true }).decode(readNativeBytes(path, 1024 * 1024)), codexConfig.skills?.config ?? []))
49517
49571
  throw new Error("NATIVE_SKILL_DRIFT: reviewed native plugin capability controls changed; run skills hook install with a fresh discovery review");
49518
49572
  const disabledPlugin = (entry) => {
@@ -49549,8 +49603,6 @@ function assertManagedAgentBridgeWithDiscovery(agent, options = {}, provenDiscov
49549
49603
  };
49550
49604
  if (!discovery || discovery.agent !== agent)
49551
49605
  throw new Error("NATIVE_SKILL_DRIFT: native discovery coverage is missing; run skills hook install");
49552
- if (agent === "gemini" && !discovery.builtinNames?.every((name) => config.skills.disabled.includes(name)))
49553
- throw new Error("NATIVE_SKILL_DRIFT: an installed Gemini builtin is not disabled");
49554
49606
  try {
49555
49607
  verifyAgentDiscovery(agent === "codex" ? { ...discovery, codexDisabledPluginSkills: binding.codexPluginSkills ?? [] } : discovery, options.codexDiscoveryRecovery, aliases, witnessTolerance);
49556
49608
  if (agent === "sumi" && discovery.method === "reviewed")
@@ -49605,7 +49657,7 @@ function assertManagedAgentBridgeWithDiscovery(agent, options = {}, provenDiscov
49605
49657
  return false;
49606
49658
  }
49607
49659
  }, read: (path) => new TextDecoder("utf-8", { fatal: true }).decode(readNativeBytes(path, 1024 * 1024)) });
49608
- throw new Error(`NATIVE_SKILL_DRIFT: ${blocking.length} unexpected native skill copies were found: ${nativeSkillPathList(blocking.map((entry) => entry.path))}. ${remediation}${adapterRefusal ? ` Native policy adapter refused: ${adapterRefusal}` : ""}`);
49660
+ throw new NativeSkillCopiesDriftError(`NATIVE_SKILL_DRIFT: ${blocking.length} unexpected native skill copies were found: ${nativeSkillPathList(blocking.map((entry) => entry.path))}. ${remediation}${adapterRefusal ? ` Native policy adapter refused: ${adapterRefusal}` : ""}`);
49609
49661
  }
49610
49662
  recheckRootAliases(aliases);
49611
49663
  const discoveryChanges = (witnessTolerance?.changes ?? []).filter((change, index, all) => all.findIndex((other) => other.kind === change.kind && other.path === change.path) === index);
@@ -50460,8 +50512,8 @@ function ensureInstructionSkillFiles(skillPath, manifest) {
50460
50512
  displayName: manifest.displayName ?? displayName(manifest.name),
50461
50513
  category: manifest.category ?? "Development Tools",
50462
50514
  tags: manifest.tags?.length ? manifest.tags : ["custom", manifest.name],
50463
- inputs: [],
50464
- commands: []
50515
+ inputs: Array.isArray(manifest.inputs) ? manifest.inputs : [],
50516
+ commands: Array.isArray(manifest.commands) ? manifest.commands : []
50465
50517
  };
50466
50518
  if (!existsSync9(join35(skillPath, "SKILL.md"))) {
50467
50519
  writeFileSync6(join35(skillPath, "SKILL.md"), renderSkillMd(next));
@@ -51861,10 +51913,33 @@ function searchSkills(query, registry = loadRegistry()) {
51861
51913
  scored.sort((a, b) => b.score - a.score);
51862
51914
  return scored.map((s) => s.skill);
51863
51915
  }
51916
+ function findCloseSkillNames(query, names, maxResults = 3) {
51917
+ if (maxResults <= 0)
51918
+ return [];
51919
+ const q = query.trim().toLowerCase();
51920
+ if (q.length === 0)
51921
+ return [];
51922
+ const tokens = q.split("-").filter(Boolean);
51923
+ const ranked = [];
51924
+ if (q.length >= 3) {
51925
+ for (const name of names) {
51926
+ const candidate2 = name.toLowerCase();
51927
+ const rank = candidate2 === q ? 0 : candidate2.endsWith(`-${q}`) || candidate2.startsWith(`${q}-`) ? 1 : tokens.length > 1 && tokens.every((token) => candidate2.split("-").includes(token)) ? 2 : undefined;
51928
+ if (rank !== undefined)
51929
+ ranked.push([name, rank]);
51930
+ }
51931
+ }
51932
+ ranked.sort((a, b) => a[1] - b[1] || a[0].length - b[0].length || a[0].localeCompare(b[0]));
51933
+ const found = ranked.slice(0, maxResults).map(([name]) => name);
51934
+ if (found.length === maxResults)
51935
+ return found;
51936
+ const taken = new Set(found);
51937
+ const threshold = Math.max(3, Math.floor(q.length / 2));
51938
+ const byDistance = names.filter((name) => !taken.has(name)).map((name) => ({ name, distance: editDistance(q, name.toLowerCase()) })).filter((entry) => entry.distance <= threshold).sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name));
51939
+ return [...found, ...byDistance.slice(0, maxResults - found.length).map((entry) => entry.name)];
51940
+ }
51864
51941
  function findSimilarSkills(query, maxResults = 3, registry = loadRegistry()) {
51865
- const q = query.toLowerCase();
51866
- const scored = registry.map((s) => ({ name: s.name, dist: editDistance(q, s.name.toLowerCase()) })).filter((s) => s.dist <= Math.max(3, Math.floor(q.length / 2))).sort((a, b) => a.dist - b.dist);
51867
- return scored.slice(0, maxResults).map((s) => s.name);
51942
+ return findCloseSkillNames(query, registry.map((skill) => skill.name), maxResults);
51868
51943
  }
51869
51944
  // src/lib/registry.ts
51870
51945
  function isBasicSkillName(name) {
@@ -52885,11 +52960,31 @@ function exactProfileSelection(spec, profile) {
52885
52960
  const at = spec.lastIndexOf("@");
52886
52961
  const slug = at > 0 ? spec.slice(0, at) : spec;
52887
52962
  const version2 = at > 0 ? spec.slice(at + 1) : undefined;
52888
- const selection2 = profile.selections.find((entry) => (entry.slug === slug || entry.aliases?.includes(slug)) && (version2 === undefined || version2 === entry.version));
52889
- if (!selection2 || version2 === "")
52890
- throw new SkillSelectionError("SKILL_NOT_SELECTED", "The requested exact skill version is not selected by this profile or project lock.");
52963
+ const named = profile.selections.filter((entry) => entry.slug === slug || entry.aliases?.includes(slug));
52964
+ if (version2 === "")
52965
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", emptyVersionRefusal(spec));
52966
+ if (named.length === 0)
52967
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", unknownNameRefusal(slug, profile));
52968
+ const selection2 = version2 === undefined ? named[0] : named.find((entry) => entry.version === version2);
52969
+ if (!selection2)
52970
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", wrongVersionRefusal(slug, version2, named));
52891
52971
  return selection2;
52892
52972
  }
52973
+ var ACCEPTED_FORM = "Accepted form: '<slug>' or '<slug>@<version>'.";
52974
+ function emptyVersionRefusal(spec) {
52975
+ return `The requested name '${spec}' has an empty version. ${ACCEPTED_FORM}`;
52976
+ }
52977
+ function wrongVersionRefusal(slug, version2, named) {
52978
+ const versions = [...new Set(named.map((entry) => entry.version))].sort();
52979
+ const canonical9 = named[0].slug;
52980
+ return `Skill '${slug}' is selected at version '${versions.join("', '")}', but '${version2}' was requested. ` + `Accepted form: '${canonical9}@${versions[0]}'.`;
52981
+ }
52982
+ function unknownNameRefusal(slug, profile) {
52983
+ const names = profile.selections.flatMap((entry) => [entry.slug, ...entry.aliases ?? []]);
52984
+ const close = findCloseSkillNames(slug, names);
52985
+ const hint = close.length > 0 ? ` Did you mean: ${close.join(", ")}?` : "";
52986
+ return `Skill '${slug}' is not selected by this Skills profile or project lock.${hint} ` + `Read the accepted names with 'skills search <term>' or 'skills list --all --json'; accept one with 'skills pin <name>'. ${ACCEPTED_FORM}`;
52987
+ }
52893
52988
 
52894
52989
  // src/lib/selected-run.ts
52895
52990
  async function resolveSelectedRun(spec, profileId, options = {}) {
@@ -52898,12 +52993,51 @@ async function resolveSelectedRun(spec, profileId, options = {}) {
52898
52993
  const entries = await readSelectedEntries(selection2, context, options);
52899
52994
  return { selection: selection2, profileId: context.receipt.profile.profileId, entries, ...describeEntries(entries), cacheDir: selectionCacheRoot(options) };
52900
52995
  }
52996
+ function declaredOperation(manifest) {
52997
+ const entry = instructionCommandEntry(manifest);
52998
+ if (!entry)
52999
+ return null;
53000
+ const runtime = manifest.runtime;
53001
+ if (!runtime || typeof runtime !== "object" || Array.isArray(runtime))
53002
+ return null;
53003
+ if (runtime.runtime !== "bun" && runtime.runtime !== "node" && runtime.runtime !== "python3")
53004
+ return null;
53005
+ if (instructionEntrypointMismatch(manifest))
53006
+ return null;
53007
+ return { entry };
53008
+ }
53009
+ function instructionCommandEntry(manifest) {
53010
+ const commands = Array.isArray(manifest.commands) ? manifest.commands : [];
53011
+ const command = commands[0];
53012
+ if (!command || typeof command !== "object" || typeof command.entry !== "string" || command.entry.length === 0)
53013
+ return null;
53014
+ return command.entry;
53015
+ }
53016
+ function instructionEntrypointMismatch(manifest) {
53017
+ const entry = instructionCommandEntry(manifest);
53018
+ const declared = manifest.runtime;
53019
+ if (!entry || !declared || typeof declared !== "object" || Array.isArray(declared))
53020
+ return null;
53021
+ const runtimeEntrypoint = declared.entrypoint;
53022
+ return typeof runtimeEntrypoint === "string" && runtimeEntrypoint !== entry ? runtimeEntrypoint : null;
53023
+ }
53024
+ function instructionRunRefusal(manifest, isBundledFile) {
53025
+ const declaredEntry = instructionCommandEntry(manifest);
53026
+ if (declaredEntry && !isBundledFile(declaredEntry))
53027
+ return { code: "LOCAL_ENTRYPOINT_MISSING", message: `This selected instruction skill declares its operation entry as ${declaredEntry}, which is not a regular file in the selected bundle; a different runtime.entrypoint never substitutes for it.` };
53028
+ const mismatch = instructionEntrypointMismatch(manifest);
53029
+ if (mismatch)
53030
+ return { code: "LOCAL_ENTRYPOINT_MISMATCH", message: `This selected instruction skill declares its operation entry as ${declaredEntry}, but its runtime.entrypoint is ${mismatch}; the two must agree.` };
53031
+ if (!declaredOperation(manifest))
53032
+ return { code: "INSTRUCTION_SKILL", message: "This selected skill contains instructions. Use skills load instead of skills run." };
53033
+ return null;
53034
+ }
52901
53035
  async function prepareSelectedSecretBindings(selected, cwd = process.cwd()) {
52902
53036
  const entries = await readCachedSelection(selected.selection, { cacheDir: selected.cacheDir });
52903
53037
  if (!entries)
52904
53038
  throw new SkillSelectionError("CACHED_BUNDLE_MISSING", "The selected execution bundle is not cached.");
52905
53039
  const { kind, manifest } = describeEntries(entries);
52906
- if (kind !== "executable")
53040
+ if (kind !== "executable" && !declaredOperation(manifest))
52907
53041
  throw new SkillSelectionError("INSTRUCTION_SKILL", "Instruction skills do not accept execution secret bindings.");
52908
53042
  const names = declaredSecretNames(manifest.runtime?.env);
52909
53043
  if (!names.length)
@@ -52916,8 +53050,12 @@ async function executeSelectedLocal(selected, options = {}) {
52916
53050
  if (!entries)
52917
53051
  throw new SkillSelectionError("CACHED_BUNDLE_MISSING", "The selected local execution bundle is not cached.");
52918
53052
  const { kind, manifest, packageJson } = describeEntries(entries);
52919
- if (kind === "instruction")
52920
- throw new SkillSelectionError("INSTRUCTION_SKILL", "This selected skill contains instructions. Use skills load instead of skills run.");
53053
+ const operation = declaredOperation(manifest);
53054
+ if (kind === "instruction") {
53055
+ const refusal2 = instructionRunRefusal(manifest, (path) => entries.some((entry) => entry.path === path));
53056
+ if (refusal2)
53057
+ throw new SkillSelectionError(refusal2.code, refusal2.message);
53058
+ }
52921
53059
  const runtime = manifest.runtime ?? {};
52922
53060
  if (!runtime || typeof runtime !== "object" || Array.isArray(runtime))
52923
53061
  throw new SkillSelectionError("INVALID_SKILL_MANIFEST", "The selected runtime declaration is invalid.");
@@ -52936,9 +53074,9 @@ async function executeSelectedLocal(selected, options = {}) {
52936
53074
  const executable2 = runtimeName === "bun" ? process.execPath : Bun.which(runtimeName);
52937
53075
  if (!executable2)
52938
53076
  throw new SkillSelectionError("LOCAL_RUNTIME_UNAVAILABLE", "The selected runtime is not installed on this station.");
52939
- const declaredEntry = runtime.entrypoint ?? (typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin && typeof packageJson.bin === "object" ? Object.values(packageJson.bin)[0] : undefined);
53077
+ const declaredEntry = (kind === "instruction" ? operation?.entry : undefined) ?? runtime.entrypoint ?? (typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin && typeof packageJson.bin === "object" ? Object.values(packageJson.bin)[0] : undefined);
52940
53078
  if (typeof declaredEntry !== "string" || !entries.some((entry) => entry.path === declaredEntry))
52941
- throw new SkillSelectionError("LOCAL_ENTRYPOINT_MISSING", "The selected bundle must declare a regular-file entrypoint in its runtime manifest or package bin.");
53079
+ throw new SkillSelectionError("LOCAL_ENTRYPOINT_MISSING", kind === "instruction" ? "The selected instruction skill must declare the operation entrypoint it ships in its manifest, and that declared entry must be a regular file in the selected bundle; a different runtime.entrypoint never substitutes for it." : "The selected bundle must declare a regular-file entrypoint in its runtime manifest or package bin.");
52942
53080
  const args = options.args ?? [];
52943
53081
  if (args.length > 128 || args.some((arg) => typeof arg !== "string" || arg.length > 16384 || arg.includes("\x00")))
52944
53082
  throw new SkillSelectionError("INVALID_RUN_INPUT", "Local skill arguments exceed their limits.");
@@ -53013,6 +53151,7 @@ async function executeSelectedLocal(selected, options = {}) {
53013
53151
  selection: selection2,
53014
53152
  target: "local",
53015
53153
  runtime: runtimeName,
53154
+ entrypoint: declaredEntry,
53016
53155
  inputDigest: `sha256:${createHash32("sha256").update(input).digest("hex")}`,
53017
53156
  runDirectory,
53018
53157
  ...executionGrant ? { executionGrant } : {},
@@ -354,7 +354,8 @@ What the software currently enforces is narrower:
354
354
  non-null hash. A non-string `hooks` value and a `hooks` target outside the
355
355
  plugin root are refused. A manifest listed as `claude-plugin-manifest-v1`
356
356
  (the form Skills writes into the stored policy) or as `path-bytes` triggers
357
- no hook-file requirement. Nothing else is required.
357
+ no hook-file requirement, and neither does a manifest the absent retired
358
+ version rule below retains. Nothing else is required.
358
359
  - A `hooks/hooks.json` that is absent at review time and appears later is
359
360
  detected if the review pins it with `sha256: null` or covers it with a
360
361
  reviewed `directories` membership witness on the plugin root or its `hooks/`
@@ -383,7 +384,7 @@ What the software currently enforces is narrower:
383
384
  `captureDiscoveryPathSources` (including the installer `path-bytes`
384
385
  operation) produces a `claude-plugin-orphaned-at-v1` witness, and a fresh
385
386
  explicit review converts a plain byte or `path-bytes` witness of it to that
386
- form. The witness binds the marker's presence or absence, not its bytes or
387
+ form, unless the absent retired version rule below retains it. The witness binds the marker's presence or absence, not its bytes or
387
388
  inode. Every capture and check still requires a regular, non-symlink file
388
389
  owned by the current user, at most 32 bytes, matching `^[1-9][0-9]{12}$`.
389
390
  The name stays in `directories` membership, so other added or removed names
@@ -392,17 +393,20 @@ What the software currently enforces is narrower:
392
393
  that binds the marker's bytes keeps that meaning and needs one fresh review.
393
394
  - An entirely absent retired Claude version root exempts only its plain byte
394
395
  sources (`hashMode` omitted or `"bytes"`, with no `format`, `fields` or
395
- `managedPlugins`) and a present `claude-plugin-orphaned-at-v1` marker. Such a
396
- source with a non-null `sha256` is also the only
396
+ `managedPlugins`), its `.claude-plugin/plugin.json` as a
397
+ `claude-plugin-manifest-v1` witness and a present
398
+ `claude-plugin-orphaned-at-v1` marker. Such a source with a non-null
399
+ `sha256` is also the only
397
400
  thing that identifies the root, and the root qualifies only while it stays
398
401
  entirely absent and the reviewed settings and registry select a different,
399
402
  verified version. Every other source under that root is re-hashed as usual,
400
403
  and one whose file existed at review no longer matches its hash, so the
401
- check refuses. That includes the plugin manifest in the
402
- `claude-plugin-manifest-v1` form Skills stores, so a stored review that lists
403
- the retired version's manifest refuses until a fresh review replaces it. A
404
- `directories` witness that covers the retired root is not exempt either. If
405
- the root reappears, its sources are checked again.
404
+ check refuses. A `directories` witness that covers the retired root is not
405
+ exempt either. An explicit review (`--discovery-inputs`) keeps each exempt
406
+ source exactly as listed: it does not project a byte-witnessed manifest,
407
+ require its hook files or convert a byte-witnessed marker, so a historical
408
+ byte witness keeps its meaning. If the root reappears, its sources are
409
+ checked again.
406
410
  - It is detection-only. Later checks re-hash the stored sources and re-walk any
407
411
  reviewed `directories` witnesses. Drift makes the
408
412
  Skills hooks refuse with `NATIVE_SKILL_DRIFT`; it does not stop, unload or
@@ -91,10 +91,24 @@ existing Codewith conventions):
91
91
  `package.json`, a non-empty `bin`, and `src/index.ts`. `skills run` executes it.
92
92
  - `kind: instruction` — a `SKILL.md`-primary prose skill for coding agents.
93
93
  `package.json`, `bin`, and `src/` are all optional. Instruction skills may still
94
- bundle optional helper scripts (a `bin`/`src` is permitted, not forbidden), but
95
- they are consumed by agents via `SKILL.md`, not executed. `skills run` on an
96
- instruction skill returns a clear "not runnable — instruction skill" error
97
- instead of executing a stub.
94
+ bundle optional helper scripts (a `bin`/`src` is permitted, not forbidden) and
95
+ are consumed by agents via `SKILL.md`. `skills run` refuses a prose-only
96
+ instruction skill with a clear "not runnable — instruction skill" error instead
97
+ of executing a stub.
98
+ - An instruction skill may instead declare the one operation it ships: the first
99
+ `commands` entry, naming its helper, together with an explicit `runtime`
100
+ contract. `skills run` then executes exactly that declared entrypoint from the
101
+ selected bundle — the same immutable object, digest receipt, argument,
102
+ timeout, output and no-ambient-credential contract an executable skill gets —
103
+ and still refuses the skill when it declares no operation or no runtime. The
104
+ declared command entry is the authority: it must be a regular file in the
105
+ selected bundle, and a `runtime.entrypoint` that disagrees with it is an
106
+ inconsistent declaration that refuses (`LOCAL_ENTRYPOINT_MISMATCH`) rather than
107
+ running the other file. A declared entry missing from the bundle refuses
108
+ (`LOCAL_ENTRYPOINT_MISSING`) and is never substituted, and the mutable
109
+ authoring corpus is never executed. Executable skills keep their own
110
+ precedence (`runtime.entrypoint`, then `package.json` bin) unchanged, and cloud
111
+ execution keeps requiring an executable skill version.
98
112
 
99
113
  New scaffolds always record their selected kind. Legacy local readers retain
100
114
  their executable fallback, while the legacy publication API retains its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.73",
3
+ "version": "0.10.75",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {