@hasna/skills 0.10.4 → 0.10.5

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
@@ -561,6 +561,18 @@ skills pull --all --selection-profile default --json
561
561
  skills pull pdf-generate@0.5.2 --selection-profile default --json
562
562
  ```
563
563
 
564
+ To inspect a published version that is not selected, export its immutable bundle
565
+ to an existing owner-only directory outside Git and agent-native discovery:
566
+
567
+ ```bash
568
+ skills review-export example-skill@1.0.0 --output /absolute/private/review/example-skill-1.0.0.tar.gz --json
569
+ ```
570
+
571
+ The command verifies the version metadata, response headers, byte count, and
572
+ SHA-256 before creating a mode-0600 file. It prints a metadata-only receipt and
573
+ does not install or select the skill. Review and scan the private archive before
574
+ reading its content.
575
+
564
576
  Use `sync --selection-profile default --station station-example` when you also
565
577
  need a station receipt. Native-folder migration options belong to the older
566
578
  sync mode and cannot be mixed with profile sync.
@@ -945,6 +957,7 @@ of app folders, and `XDG_CONFIG_HOME` is not consulted at all.
945
957
  | `skills hook agents --json` | | Report maintained adapters and explicit coverage limits |
946
958
  | `skills migrate native` | | Inventory native copies; `--apply` archives managed copies, with explicit `--include-unmanaged`, `--include-vendor`, or reviewed `--target-manifest` selection |
947
959
  | `skills pull --all --selection-profile <id>` | | With CLI loading active, refresh the selected profile into the verified cache |
960
+ | `skills review-export <slug@version> --output <path>` | | Export one immutable hosted bundle to a private archive for review without selecting it |
948
961
  | `skills sync --selection-profile <id> [--check] [--station <id>]` | | Sync or check the selected profile/cache; optionally record station state |
949
962
  | `skills station-state <id>` | | Read a station's sync receipt in the authenticated workspace |
950
963
  | `skills run <name> [args]` | | Execute a skill directly |
package/bin/index.js CHANGED
@@ -36868,7 +36868,7 @@ var package_default;
36868
36868
  var init_package = __esm(() => {
36869
36869
  package_default = {
36870
36870
  name: "@hasna/skills",
36871
- version: "0.10.4",
36871
+ version: "0.10.5",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -51954,6 +51954,9 @@ function assertManagedAgentBridge(agent, options = {}) {
51954
51954
  return false;
51955
51955
  if (entry.path.startsWith(canonicalAgentPath(join20(home, ".codex", "plugins", "cache"), aliases) + sep4))
51956
51956
  return true;
51957
+ const runtimeCache = canonicalAgentPath(join20(home, ".cache", "codex-runtimes"), aliases);
51958
+ if (entry.path.startsWith(runtimeCache + sep4) && discovery?.method === "automatic" && discovery.roots.some((root) => entry.path.startsWith(root + sep4)))
51959
+ return true;
51957
51960
  if (!entry.path.startsWith(canonicalAgentPath(join20(home, ".codex", "skills", ".system"), aliases) + sep4))
51958
51961
  return false;
51959
51962
  return Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
@@ -111809,10 +111812,100 @@ var init_registry_sync = __esm(() => {
111809
111812
  init_skill_validation();
111810
111813
  });
111811
111814
 
111815
+ // src/lib/review-export.ts
111816
+ import { constants as constants22, closeSync as closeSync25, existsSync as existsSync35, fstatSync as fstatSync25, fsyncSync as fsyncSync7, lstatSync as lstatSync31, openSync as openSync25, realpathSync as realpathSync15, statSync as statSync23, writeFileSync as writeFileSync30 } from "fs";
111817
+ import { basename as basename14, dirname as dirname36, isAbsolute as isAbsolute29, join as join61, parse as parse12, resolve as resolve26, sep as sep13 } from "path";
111818
+ function exactVersion(spec) {
111819
+ const at2 = spec.indexOf("@");
111820
+ const slug2 = spec.slice(0, at2);
111821
+ const version2 = spec.slice(at2 + 1);
111822
+ if (at2 < 1 || slug2.length > 128 || !SLUG2.test(slug2) || !isValidSkillVersion(version2)) {
111823
+ throw new Error("Review export requires an exact valid slug@version.");
111824
+ }
111825
+ return { slug: slug2, version: version2 };
111826
+ }
111827
+ function assertPrivateDestination(output2) {
111828
+ if (!isAbsolute29(output2) || resolve26(output2) !== output2 || !basename14(output2).endsWith(".tar.gz")) {
111829
+ throw new Error("Review output must be an absolute, normalized .tar.gz path.");
111830
+ }
111831
+ const parent = dirname36(output2);
111832
+ if (realpathSync15(parent) !== parent || lstatSync31(parent).isSymbolicLink()) {
111833
+ throw new Error("Review output parent must be a real directory, not a symlink.");
111834
+ }
111835
+ const parentStat = statSync23(parent);
111836
+ if (!parentStat.isDirectory() || (parentStat.mode & 63) !== 0 || process.getuid && parentStat.uid !== process.getuid()) {
111837
+ throw new Error("Review output parent must be an owner-only directory (mode 0700).");
111838
+ }
111839
+ const segments = parent.toLowerCase().split(sep13);
111840
+ if (segments.some((segment) => NATIVE_CONTAINERS.has(segment)) || segments.some((segment, index) => segment === ".config" && segments[index + 1] === "opencode") || segments.some((segment, index) => segment === ".hasna" && segments[index + 1] === "skills")) {
111841
+ throw new Error("Review output cannot be placed in agent-native discovery or Skills installation paths.");
111842
+ }
111843
+ for (let current = parent;; current = dirname36(current)) {
111844
+ if (existsSync35(join61(current, ".git"))) {
111845
+ throw new Error("Review output cannot be placed inside a Git worktree.");
111846
+ }
111847
+ if (current === parse12(current).root)
111848
+ break;
111849
+ }
111850
+ return output2;
111851
+ }
111852
+ async function exportSkillVersionForReview(spec, output2, client2) {
111853
+ const { slug: slug2, version: version2 } = exactVersion(spec);
111854
+ const target = assertPrivateDestination(output2);
111855
+ if (existsSync35(target))
111856
+ throw new Error("Review output already exists; choose a new filename.");
111857
+ const metadata = await client2.getSkillVersion(slug2, version2);
111858
+ if (!metadata)
111859
+ throw new Error(`Published skill version ${slug2}@${version2} was not found.`);
111860
+ if (!/^[a-f0-9]{64}$/i.test(metadata.bundleSha256) || !Number.isSafeInteger(metadata.bundleByteSize) || metadata.bundleByteSize < 1 || metadata.bundleByteSize > MAX_REVIEW_BUNDLE_BYTES) {
111861
+ throw new Error("Published bundle metadata is invalid or exceeds the review size limit.");
111862
+ }
111863
+ const response = await client2.getBundle(slug2, version2);
111864
+ if (!response)
111865
+ throw new Error(`Published bundle for ${slug2}@${version2} is unavailable.`);
111866
+ if (!response.ok) {
111867
+ response.body?.cancel().catch(() => {});
111868
+ throw new Error(`Published bundle request failed: HTTP ${response.status}.`);
111869
+ }
111870
+ const headerSha = response.headers.get("X-Skill-Bundle-Sha256");
111871
+ const headerVersion = response.headers.get("X-Skill-Version");
111872
+ if (!headerSha || headerSha.toLowerCase() !== metadata.bundleSha256.toLowerCase() || headerVersion !== version2) {
111873
+ response.body?.cancel().catch(() => {});
111874
+ throw new Error("Published bundle headers do not match the exact version metadata.");
111875
+ }
111876
+ const bytes = await readBoundedResponse(response, Math.min(metadata.bundleByteSize, MAX_REVIEW_BUNDLE_BYTES));
111877
+ if (bytes.byteLength !== metadata.bundleByteSize || sha256Hex(bytes) !== metadata.bundleSha256.toLowerCase()) {
111878
+ throw new Error("Published bundle bytes do not match the exact version metadata.");
111879
+ }
111880
+ const fd = openSync25(target, constants22.O_WRONLY | constants22.O_CREAT | constants22.O_EXCL | constants22.O_NOFOLLOW, 384);
111881
+ try {
111882
+ writeFileSync30(fd, bytes);
111883
+ fsyncSync7(fd);
111884
+ if (fstatSync25(fd).size !== bytes.byteLength)
111885
+ throw new Error("Incomplete review output write.");
111886
+ } catch (error61) {
111887
+ throw new Error(`Review output write failed. A private partial file may remain at ${target}; use Trash to remove it before reusing this path.`, { cause: error61 });
111888
+ } finally {
111889
+ closeSync25(fd);
111890
+ }
111891
+ return { slug: slug2, version: version2, bundleSha256: metadata.bundleSha256.toLowerCase(), bundleByteSize: bytes.byteLength, output: target };
111892
+ }
111893
+ var MAX_REVIEW_BUNDLE_BYTES, SLUG2, NATIVE_CONTAINERS;
111894
+ var init_review_export = __esm(() => {
111895
+ init_remote_files();
111896
+ init_skill_bundle();
111897
+ init_skill_version();
111898
+ init_native_discovery_roots();
111899
+ MAX_REVIEW_BUNDLE_BYTES = 32 * 1024 * 1024;
111900
+ SLUG2 = /^[a-z0-9][a-z0-9._-]*$/;
111901
+ NATIVE_CONTAINERS = new Set(NATIVE_SKILL_ROOTS.map(([, root2]) => root2.split("/")[0]).filter((root2) => root2 !== ".config"));
111902
+ });
111903
+
111812
111904
  // src/cli/commands/registry.ts
111813
111905
  var exports_registry2 = {};
111814
111906
  __export(exports_registry2, {
111815
111907
  registerVersions: () => registerVersions,
111908
+ registerReviewExport: () => registerReviewExport,
111816
111909
  registerRegistry: () => registerRegistry,
111817
111910
  registerPull: () => registerPull,
111818
111911
  registerLifecycle: () => registerLifecycle
@@ -111824,12 +111917,12 @@ function registerRegistry(parent) {
111824
111917
  async function writeJson2(value, space) {
111825
111918
  const text4 = `${JSON.stringify(value, null, space)}
111826
111919
  `;
111827
- await new Promise((resolve26, reject2) => {
111920
+ await new Promise((resolve27, reject2) => {
111828
111921
  process.stdout.write(text4, (error61) => {
111829
111922
  if (error61)
111830
111923
  reject2(error61);
111831
111924
  else
111832
- resolve26();
111925
+ resolve27();
111833
111926
  });
111834
111927
  });
111835
111928
  }
@@ -111971,6 +112064,26 @@ Versions of ${name}
111971
112064
  }
111972
112065
  });
111973
112066
  }
112067
+ function registerReviewExport(parent) {
112068
+ parent.command("review-export").argument("<slug@version>", "Exact published skill version to review").requiredOption("--output <path>", "New .tar.gz path inside an existing owner-only directory outside Git and agent discovery").option("--json", "Print a metadata-only receipt", false).description("Export an exact hosted bundle for private review without changing skill selection").action(async (spec, options) => {
112069
+ try {
112070
+ const client2 = await createRemoteSkillsClient();
112071
+ if (!client2)
112072
+ throw new Error("No Skills instance credential is configured. Run skills login.");
112073
+ const receipt = await exportSkillVersionForReview(spec, options.output, client2);
112074
+ if (options.json)
112075
+ console.log(JSON.stringify(receipt));
112076
+ else
112077
+ console.log(`Exported ${receipt.slug}@${receipt.version} to ${receipt.output} (${receipt.bundleSha256}).`);
112078
+ } catch (error61) {
112079
+ if (options.json)
112080
+ console.log(JSON.stringify({ error: error61.message }));
112081
+ else
112082
+ console.error(source_default.red(error61.message));
112083
+ process.exitCode = 1;
112084
+ }
112085
+ });
112086
+ }
111974
112087
  function registerLifecycle(parent) {
111975
112088
  parent.command("lifecycle").argument("<name>", "Skill name on the configured instance").requiredOption("--state <state>", "Lifecycle state: active or archived").requiredOption("--revision <revision>", "Exact revision id from skills info/get").option("--reason <reason>", "Bounded archive reason").option("--replacement <slug>", "Optional successor slug").option("--json", "Output as JSON", false).description("Change a hosted skill's active/archive lifecycle with an exact revision guard").action(async (name, options) => {
111976
112089
  if (options.state !== "active" && options.state !== "archived")
@@ -111999,6 +112112,7 @@ var init_registry2 = __esm(() => {
111999
112112
  init_selection_resolver();
112000
112113
  init_context();
112001
112114
  init_selection_cache();
112115
+ init_review_export();
112002
112116
  });
112003
112117
 
112004
112118
  // src/cli/commands/publish.ts
@@ -112012,9 +112126,9 @@ __export(exports_publish, {
112012
112126
  PushSkillError: () => PushSkillError
112013
112127
  });
112014
112128
  import { execFileSync as execFileSync2 } from "child_process";
112015
- import { existsSync as existsSync35, readFileSync as readFileSync43 } from "fs";
112129
+ import { existsSync as existsSync36, readFileSync as readFileSync43 } from "fs";
112016
112130
  import { hostname as hostname5 } from "os";
112017
- import { join as join61 } from "path";
112131
+ import { join as join62 } from "path";
112018
112132
  function registerPublish(parent) {
112019
112133
  parent.command("push").argument("<name>", "Name of a skill in the local corpus (~/.hasna/skills/installed or the migrated ~/.hasna/skills/skills)").option("--version <version>", "Override the version recorded on the instance").option("--force-new-version", "If name@version already exists with different content, publish as the next patch version", false).option("--dry-run", "Pack and validate without uploading", false).option("--json", "Output result as JSON", false).description("Publish a local skill to the configured skills instance").action(async (name, options) => {
112020
112134
  try {
@@ -112066,8 +112180,8 @@ async function pushSkill(name, options = {}) {
112066
112180
  const packed = packSkillBundle(skill.path, { maxUnpackedBytes: MAX_UNPACKED_BYTES });
112067
112181
  await verifyPackedCanonicalHash(skill.name, packed);
112068
112182
  const versionManifest = buildVersionManifest(skill.path, packed);
112069
- const skillMdPath = join61(skill.path, "SKILL.md");
112070
- const skillMd = existsSync35(skillMdPath) ? readFileSync43(skillMdPath, "utf-8") : undefined;
112183
+ const skillMdPath = join62(skill.path, "SKILL.md");
112184
+ const skillMd = existsSync36(skillMdPath) ? readFileSync43(skillMdPath, "utf-8") : undefined;
112071
112185
  const base3 = {
112072
112186
  slug: skill.name,
112073
112187
  path: skill.path,
@@ -112305,7 +112419,7 @@ var exports_private_publications = {};
112305
112419
  __export(exports_private_publications, {
112306
112420
  registerPrivatePublications: () => registerPrivatePublications
112307
112421
  });
112308
- import { resolve as resolve26 } from "path";
112422
+ import { resolve as resolve27 } from "path";
112309
112423
  function verifyOptions(options, action) {
112310
112424
  if (options.userId === undefined !== (options.membershipId === undefined) || options.userId !== undefined && (!publicationUuid(options.userId) || !publicationUuid(options.membershipId)))
112311
112425
  throw new PrivatePublicationError("PUBLICATION_CONTEXT_REQUIRED", "Provide both observed --user-id and --membership-id UUIDs.");
@@ -112350,9 +112464,9 @@ function registerPrivatePublications(parent) {
112350
112464
  if (code2 === null)
112351
112465
  return;
112352
112466
  const context2 = options.userId ? { userId: options.userId, membershipId: options.membershipId } : undefined;
112353
- const client2 = await privatePublicationSession(options.email, code2, context2), directory2 = resolve26(options.recoveryDir);
112467
+ const client2 = await privatePublicationSession(options.email, code2, context2), directory2 = resolve27(options.recoveryDir);
112354
112468
  if (action === "publish")
112355
- await preparePrivatePublication(client2, resolve26(args2[0]), directory2, { skillId: options.skillId, expectedCurrentVersionId: options.expectedCurrentVersion ?? null, idempotencyKey: options.idempotencyKey });
112469
+ await preparePrivatePublication(client2, resolve27(args2[0]), directory2, { skillId: options.skillId, expectedCurrentVersionId: options.expectedCurrentVersion ?? null, idempotencyKey: options.idempotencyKey });
112356
112470
  const result2 = action === "publish" || action === "resume" ? await continuePrivatePublication(client2, directory2, { confirm: true, waitMs: Number(options.waitSeconds) * 1000 }) : await inspectPrivatePublication(client2, directory2, action === "cancel");
112357
112471
  print2(result2, options.json);
112358
112472
  } catch (error61) {
@@ -112454,7 +112568,7 @@ __export(exports_auth, {
112454
112568
  import { createInterface as createInterface3 } from "readline";
112455
112569
  function prompt(question) {
112456
112570
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
112457
- return new Promise((resolve27) => {
112571
+ return new Promise((resolve28) => {
112458
112572
  let settled = false;
112459
112573
  const finish = (answer) => {
112460
112574
  if (settled)
@@ -112463,7 +112577,7 @@ function prompt(question) {
112463
112577
  rl.close();
112464
112578
  if (answer === null)
112465
112579
  process.exitCode = 130;
112466
- resolve27(answer);
112580
+ resolve28(answer);
112467
112581
  };
112468
112582
  rl.once("SIGINT", () => finish(null));
112469
112583
  rl.once("close", () => finish(null));
@@ -112641,7 +112755,7 @@ Skipped. Run it any time: skills setup agents`));
112641
112755
  function promptHidden(question) {
112642
112756
  const stdin = process.stdin;
112643
112757
  process.stdout.write(question);
112644
- return new Promise((resolve27) => {
112758
+ return new Promise((resolve28) => {
112645
112759
  let value = "";
112646
112760
  const done = (answer) => {
112647
112761
  stdin.off("data", onData);
@@ -112651,7 +112765,7 @@ function promptHidden(question) {
112651
112765
  `);
112652
112766
  if (answer === null)
112653
112767
  process.exitCode = 130;
112654
- resolve27(answer);
112768
+ resolve28(answer);
112655
112769
  };
112656
112770
  const onData = (chunk2) => {
112657
112771
  for (const character of chunk2.toString()) {
@@ -113197,7 +113311,7 @@ function promptInvitationProof(kind) {
113197
113311
  const label = kind === "token" ? "invitation token" : "recovery code";
113198
113312
  const character = kind === "token" ? /^[A-Za-z0-9_-]$/ : /^\d$/;
113199
113313
  const { stdin, stderr: output2 } = process, wasRaw = stdin.isRaw, wasFlowing = stdin.readableFlowing;
113200
- return new Promise((resolve27) => {
113314
+ return new Promise((resolve28) => {
113201
113315
  let value = "", settled = false, overflow = false;
113202
113316
  const finish = (answer) => {
113203
113317
  if (settled)
@@ -113214,7 +113328,7 @@ function promptInvitationProof(kind) {
113214
113328
  `);
113215
113329
  if (answer === null)
113216
113330
  process.exitCode = 130;
113217
- resolve27(answer);
113331
+ resolve28(answer);
113218
113332
  };
113219
113333
  const cancel = () => finish(null);
113220
113334
  const keypress = (text4, key) => {
@@ -113786,11 +113900,11 @@ var init_storage2 = __esm(() => {
113786
113900
  });
113787
113901
 
113788
113902
  // src/lib/registry-reconcile.ts
113789
- import { existsSync as existsSync36, readFileSync as readFileSync44, statSync as statSync23, writeFileSync as writeFileSync30 } from "fs";
113790
- import { join as join62 } from "path";
113903
+ import { existsSync as existsSync37, readFileSync as readFileSync44, statSync as statSync24, writeFileSync as writeFileSync31 } from "fs";
113904
+ import { join as join63 } from "path";
113791
113905
  function isDirectory2(path) {
113792
113906
  try {
113793
- return statSync23(path).isDirectory();
113907
+ return statSync24(path).isDirectory();
113794
113908
  } catch {
113795
113909
  return false;
113796
113910
  }
@@ -113798,12 +113912,12 @@ function isDirectory2(path) {
113798
113912
  function migrationNeeded(options) {
113799
113913
  if (options.rootDir)
113800
113914
  return false;
113801
- const appDir = options.homeDir ? join62(options.homeDir, ".hasna", "skills") : getDataDir();
113802
- return !(isOwnerLayoutMigrated(appDir) && isDirectory2(join62(appDir, SKILLS_CACHE_DIRNAME)));
113915
+ const appDir = options.homeDir ? join63(options.homeDir, ".hasna", "skills") : getDataDir();
113916
+ return !(isOwnerLayoutMigrated(appDir) && isDirectory2(join63(appDir, SKILLS_CACHE_DIRNAME)));
113803
113917
  }
113804
113918
  function readBaseline(skillDir) {
113805
- const markerPath = join62(skillDir, PULL_MARKER_FILE);
113806
- if (!existsSync36(markerPath))
113919
+ const markerPath = join63(skillDir, PULL_MARKER_FILE);
113920
+ if (!existsSync37(markerPath))
113807
113921
  return;
113808
113922
  try {
113809
113923
  const marker = JSON.parse(readFileSync44(markerPath, "utf-8"));
@@ -113818,8 +113932,8 @@ function readBaseline(skillDir) {
113818
113932
  }
113819
113933
  }
113820
113934
  function readCursor(root2) {
113821
- const path = join62(root2, SYNC_CURSOR_FILE);
113822
- if (!existsSync36(path))
113935
+ const path = join63(root2, SYNC_CURSOR_FILE);
113936
+ if (!existsSync37(path))
113823
113937
  return { runCount: 0 };
113824
113938
  try {
113825
113939
  const cursor2 = JSON.parse(readFileSync44(path, "utf-8"));
@@ -113831,12 +113945,12 @@ function readCursor(root2) {
113831
113945
  function resolveCorpusRootReadOnly(options) {
113832
113946
  if (options.rootDir)
113833
113947
  return { root: options.rootDir, migrationPending: false };
113834
- const appDir = options.homeDir ? join62(options.homeDir, ".hasna", "skills") : getDataDirReadOnly();
113835
- const cache3 = join62(appDir, SKILLS_CACHE_DIRNAME);
113948
+ const appDir = options.homeDir ? join63(options.homeDir, ".hasna", "skills") : getDataDirReadOnly();
113949
+ const cache3 = join63(appDir, SKILLS_CACHE_DIRNAME);
113836
113950
  if (isOwnerLayoutMigrated(appDir) && isDirectory2(cache3)) {
113837
113951
  return { root: cache3, migrationPending: false };
113838
113952
  }
113839
- return { root: join62(appDir, INSTALLED_SKILLS_DIRNAME), migrationPending: true };
113953
+ return { root: join63(appDir, INSTALLED_SKILLS_DIRNAME), migrationPending: true };
113840
113954
  }
113841
113955
  function remoteRowToSkill(record11) {
113842
113956
  const slug2 = typeof record11.slug === "string" ? record11.slug : typeof record11.name === "string" ? record11.name : undefined;
@@ -113850,7 +113964,7 @@ function remoteRowToSkill(record11) {
113850
113964
  }
113851
113965
  function recheckLocalSide(plannedLocal, localDir, ops = {
113852
113966
  pack: (dir) => packSkillBundle(dir).sha256,
113853
- exists: existsSync36
113967
+ exists: existsSync37
113854
113968
  }) {
113855
113969
  let localNow;
113856
113970
  try {
@@ -113970,7 +114084,7 @@ async function reconcileRegistry(options = {}) {
113970
114084
  for (const slug2 of allSlugs) {
113971
114085
  const local = locals.get(slug2);
113972
114086
  const remote = remotes.get(slug2);
113973
- const baseline = local ? readBaseline(join62(root2, slug2)) : undefined;
114087
+ const baseline = local ? readBaseline(join63(root2, slug2)) : undefined;
113974
114088
  const { state, reason } = classifySkill(local, remote, baseline);
113975
114089
  let { action, reason: actionReason } = resolveAction(state, direction, conflict);
113976
114090
  if (state === "remote-only" && isDigestless(remote)) {
@@ -114029,7 +114143,7 @@ async function reconcileRegistry(options = {}) {
114029
114143
  try {
114030
114144
  await pushSkill(slug2, { rootDir: root2, client: client2 });
114031
114145
  const pushed = locals.get(slug2);
114032
- writePullMarker(join62(root2, slug2), {
114146
+ writePullMarker(join63(root2, slug2), {
114033
114147
  skill: slug2,
114034
114148
  ...pushed?.version ? { version: pushed.version } : {},
114035
114149
  ...pushed?.sha256 ? { contentHash: pushed.sha256 } : {},
@@ -114101,7 +114215,7 @@ async function reconcileRegistry(options = {}) {
114101
114215
  runCount: readCursor(root2).runCount + 1,
114102
114216
  summary
114103
114217
  };
114104
- writeFileSync30(join62(root2, SYNC_CURSOR_FILE), `${JSON.stringify(cursor2, null, 2)}
114218
+ writeFileSync31(join63(root2, SYNC_CURSOR_FILE), `${JSON.stringify(cursor2, null, 2)}
114105
114219
  `);
114106
114220
  return {
114107
114221
  corpusRoot: root2,
@@ -122407,11 +122521,12 @@ var { registerPortableSkillCommands: registerPortableSkillCommands2 } = await Pr
122407
122521
  registerPortableSkillCommands2(program2);
122408
122522
  var { registerSchedule: registerSchedule2 } = await Promise.resolve().then(() => (init_schedule(), exports_schedule));
122409
122523
  registerSchedule2(program2);
122410
- var { registerRegistry: registerRegistry2, registerPull: registerPull2, registerVersions: registerVersions2, registerLifecycle: registerLifecycle2 } = await Promise.resolve().then(() => (init_registry2(), exports_registry2));
122524
+ var { registerRegistry: registerRegistry2, registerPull: registerPull2, registerVersions: registerVersions2, registerLifecycle: registerLifecycle2, registerReviewExport: registerReviewExport2 } = await Promise.resolve().then(() => (init_registry2(), exports_registry2));
122411
122525
  registerRegistry2(program2);
122412
122526
  registerPull2(program2);
122413
122527
  registerVersions2(program2);
122414
122528
  registerLifecycle2(program2);
122529
+ registerReviewExport2(program2);
122415
122530
  var { registerPublish: registerPublish2 } = await Promise.resolve().then(() => (init_publish(), exports_publish));
122416
122531
  registerPublish2(program2);
122417
122532
  var { registerPrivatePublications: registerPrivatePublications2 } = await Promise.resolve().then(() => (init_private_publications(), exports_private_publications));
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.4",
20022
+ version: "0.10.5",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
package/bin/migrate.js CHANGED
@@ -7,7 +7,7 @@ import { join as join5 } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@hasna/skills",
10
- version: "0.10.4",
10
+ version: "0.10.5",
11
11
  description: "Skills library for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
package/bin/server.js CHANGED
@@ -24122,7 +24122,7 @@ var init_dist_es9 = __esm(() => {
24122
24122
  // package.json
24123
24123
  var package_default = {
24124
24124
  name: "@hasna/skills",
24125
- version: "0.10.4",
24125
+ version: "0.10.5",
24126
24126
  description: "Skills library for AI coding agents",
24127
24127
  type: "module",
24128
24128
  bin: {
package/bin/worker.js CHANGED
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24125
24125
  // package.json
24126
24126
  var package_default = {
24127
24127
  name: "@hasna/skills",
24128
- version: "0.10.4",
24128
+ version: "0.10.5",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
@@ -13,4 +13,6 @@ export declare function registerPull(parent: Command): void;
13
13
  * instance, newest first, with the one the instance currently serves marked (hasna/apps#1630).
14
14
  */
15
15
  export declare function registerVersions(parent: Command): void;
16
+ /** Read one immutable bundle into a private review artifact without selecting or installing it. */
17
+ export declare function registerReviewExport(parent: Command): void;
16
18
  export declare function registerLifecycle(parent: Command): void;
package/dist/index.js CHANGED
@@ -9611,7 +9611,7 @@ import { isAbsolute as isAbsolute11, join as join23, resolve as resolve11 } from
9611
9611
  // package.json
9612
9612
  var package_default = {
9613
9613
  name: "@hasna/skills",
9614
- version: "0.10.4",
9614
+ version: "0.10.5",
9615
9615
  description: "Skills library for AI coding agents",
9616
9616
  type: "module",
9617
9617
  bin: {
@@ -14133,6 +14133,9 @@ function assertManagedAgentBridge(agent, options = {}) {
14133
14133
  return false;
14134
14134
  if (entry.path.startsWith(canonicalAgentPath(join16(home, ".codex", "plugins", "cache"), aliases) + sep4))
14135
14135
  return true;
14136
+ const runtimeCache = canonicalAgentPath(join16(home, ".cache", "codex-runtimes"), aliases);
14137
+ if (entry.path.startsWith(runtimeCache + sep4) && discovery?.method === "automatic" && discovery.roots.some((root) => entry.path.startsWith(root + sep4)))
14138
+ return true;
14136
14139
  if (!entry.path.startsWith(canonicalAgentPath(join16(home, ".codex", "skills", ".system"), aliases) + sep4))
14137
14140
  return false;
14138
14141
  return Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
@@ -0,0 +1,16 @@
1
+ export interface ReviewExportClient {
2
+ getSkillVersion(slug: string, version: string): Promise<{
3
+ bundleSha256: string;
4
+ bundleByteSize: number;
5
+ } | null>;
6
+ getBundle(slug: string, version: string): Promise<Response | null>;
7
+ }
8
+ export interface ReviewExportReceipt {
9
+ slug: string;
10
+ version: string;
11
+ bundleSha256: string;
12
+ bundleByteSize: number;
13
+ output: string;
14
+ }
15
+ /** Export for human review only. This never changes corpus, cache, profiles, or native discovery. */
16
+ export declare function exportSkillVersionForReview(spec: string, output: string, client: ReviewExportClient): Promise<ReviewExportReceipt>;
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.4",
32376
+ version: "0.10.5",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -42004,6 +42004,9 @@ function assertManagedAgentBridge(agent, options = {}) {
42004
42004
  return false;
42005
42005
  if (entry.path.startsWith(canonicalAgentPath(join21(home, ".codex", "plugins", "cache"), aliases) + sep4))
42006
42006
  return true;
42007
+ const runtimeCache = canonicalAgentPath(join21(home, ".cache", "codex-runtimes"), aliases);
42008
+ if (entry.path.startsWith(runtimeCache + sep4) && discovery?.method === "automatic" && discovery.roots.some((root2) => entry.path.startsWith(root2 + sep4)))
42009
+ return true;
42007
42010
  if (!entry.path.startsWith(canonicalAgentPath(join21(home, ".codex", "skills", ".system"), aliases) + sep4))
42008
42011
  return false;
42009
42012
  return Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {