@hasna/skills 0.10.2 → 0.10.4

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
@@ -453,17 +453,22 @@ during installation. A successful update does not reload an existing session.
453
453
  At session start, the hook authenticates and refreshes the profile. Prompt hooks
454
454
  select complete skill instructions from that verified cache using explicit
455
455
  `$skill` references, profile keywords, paths and always-required selections.
456
- A session retains its selected versions; compaction restores loaded instructions,
456
+ A session retains its loaded selections; compaction restores loaded instructions,
457
457
  and subagents inherit the parent's selection. Instructions that exceed the
458
458
  context budget produce an explicit `skills load` command. A hook never executes
459
- a skill. Cached use is explicit and expires after 24 hours. When cached context
460
- expires or is missing, the hook makes one authenticated API attempt for the same
461
- pinned session. It preserves exact versions and does not extend an expired
462
- receipt's lifetime. Authentication, integrity and native discovery failures still
463
- block; they do not silently switch to a local catalog.
464
-
465
- An intentional profile change for an already running session uses a separate
466
- reviewed operation. `sync` and hook reinstallation do not migrate session pins:
459
+ a skill. Cached use is explicit and expires after 24 hours. When a session
460
+ receipt expires, the hook authenticates the current profile. If every loaded
461
+ selection still has identical metadata and bundle digest, it archives the old
462
+ receipt and advances that session to the current profile under an exact receipt
463
+ precondition. Unloaded selections may be added or removed. If a loaded selection
464
+ changed or disappeared, the hook blocks and requires the explicit reconciliation
465
+ below. A missing cache may be recovered through authenticated context.
466
+ Authentication, integrity and native discovery failures still block; they do
467
+ not silently switch to a local catalog.
468
+
469
+ An intentional change to a loaded selection or profile identity for an already
470
+ running session uses a separate reviewed operation. `sync` and hook
471
+ reinstallation do not migrate session pins:
467
472
 
468
473
  ```bash
469
474
  skills sessions show <session-id> --json
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.2",
36871
+ version: "0.10.4",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -51164,7 +51164,7 @@ function parseNativeMigrationTargetManifest(input) {
51164
51164
  if (!target || typeof target !== "object" || Array.isArray(target))
51165
51165
  throw new Error("Invalid native migration target");
51166
51166
  const item = target;
51167
- if (Object.keys(item).some((key) => !["agent", "projectRoot", "path", "treeSha256"].includes(key)) || typeof item.agent !== "string" || typeof item.projectRoot !== "string" || typeof item.path !== "string" || typeof item.treeSha256 !== "string")
51167
+ if (Object.keys(item).some((key) => !["agent", "projectRoot", "path", "treeSha256", "vendor"].includes(key)) || typeof item.agent !== "string" || typeof item.projectRoot !== "string" || typeof item.path !== "string" || typeof item.treeSha256 !== "string" || item.vendor !== undefined && item.vendor !== true)
51168
51168
  throw new Error("Invalid native migration target fields");
51169
51169
  if (!ROOTS.some(([agent]) => agent === item.agent))
51170
51170
  throw new Error(`Unsupported native migration target agent: ${item.agent}`);
@@ -51183,7 +51183,7 @@ function parseNativeMigrationTargetManifest(input) {
51183
51183
  if (identities.has(identity4))
51184
51184
  throw new Error(`Duplicate native migration target: ${item.path}`);
51185
51185
  identities.add(identity4);
51186
- targets.push({ agent: item.agent, projectRoot, path: normalized, treeSha256: item.treeSha256 });
51186
+ targets.push({ agent: item.agent, projectRoot, path: normalized, treeSha256: item.treeSha256, ...item.vendor === true ? { vendor: true } : {} });
51187
51187
  }
51188
51188
  return { schema: "hasna.skills-native-migration-targets.v1", targets, digest };
51189
51189
  }
@@ -51211,8 +51211,8 @@ function selectNativeMigrationTargets(inventory, manifest) {
51211
51211
  if (matches2.length !== 1)
51212
51212
  throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
51213
51213
  const entry = matches2[0];
51214
- if (entry.bridge || entry.system || entry.vendor)
51215
- throw new Error(`Native migration target is protected or vendor-owned: ${entry.path}`);
51214
+ if (entry.bridge || entry.system || entry.vendor !== (target.vendor === true))
51215
+ throw new Error(`Native migration target is protected or vendor classification changed: ${entry.path}`);
51216
51216
  if (entry.hash !== target.treeSha256 || treeHash(entry.path) !== target.treeSha256)
51217
51217
  throw new Error(`Native migration target changed after review: ${entry.path}`);
51218
51218
  selected.push(entry);
@@ -51235,6 +51235,8 @@ function projectAncestorDirectories(projects) {
51235
51235
  return [...directories];
51236
51236
  }
51237
51237
  function inventoryNativeSkills(home = homedir3(), options = {}) {
51238
+ if (options.reviewedCacheAlias !== undefined && (!options.includeVendor || !isAbsolute9(options.reviewedCacheAlias) || resolve10(options.reviewedCacheAlias) !== options.reviewedCacheAlias))
51239
+ throw new Error("A reviewed cache alias requires vendor inventory and an exact absolute path");
51238
51240
  const aliases = rootAliases(home, options.allowRootAliases);
51239
51241
  const selectedAgents = options.agents ? new Set(options.agents) : undefined;
51240
51242
  const includesAgent = (agent) => !selectedAgents || selectedAgents.has(agent);
@@ -51255,14 +51257,22 @@ function inventoryNativeSkills(home = homedir3(), options = {}) {
51255
51257
  if (discoveryPathBytes > 4 * 1024 * 1024)
51256
51258
  throw new Error("Native skill discovery metadata limit exceeded");
51257
51259
  }
51258
- function emptySiblingCacheAlias(path, parent) {
51260
+ let reviewedCacheAliasFound = false;
51261
+ function verifiedSiblingCacheAlias(path, parent) {
51259
51262
  const link2 = readlinkSync3(path), target = resolve10(parent, link2);
51260
51263
  if (dirname10(target) !== parent || (isAbsolute9(link2) ? link2 !== target : dirname10(link2) !== "."))
51261
51264
  return false;
51262
51265
  if (!lstatSync14(target, { throwIfNoEntry: false })?.isDirectory())
51263
51266
  return false;
51264
51267
  const scan = cacheScans.get(target);
51265
- return Boolean(scan?.complete && !scan.hasSkills && scan.entries <= maxAliasProofEntries);
51268
+ if (!scan?.complete || scan.entries > maxAliasProofEntries)
51269
+ return false;
51270
+ if (scan.hasSkills) {
51271
+ if (path !== options.reviewedCacheAlias)
51272
+ return false;
51273
+ reviewedCacheAliasFound = true;
51274
+ }
51275
+ return true;
51266
51276
  }
51267
51277
  function visit2(agent, path, vendor = false, depth = 0, pluginCache = false, admitted = false) {
51268
51278
  if (!admitted)
@@ -51319,7 +51329,7 @@ function inventoryNativeSkills(home = homedir3(), options = {}) {
51319
51329
  const child = join20(path, name);
51320
51330
  if (isVendor && lstatSync14(child, { throwIfNoEntry: false })?.isSymbolicLink()) {
51321
51331
  const target = statSync7(child, { throwIfNoEntry: false });
51322
- if (target?.isFile() || pluginCache && target?.isDirectory() && emptySiblingCacheAlias(child, path)) {
51332
+ if (target?.isFile() || pluginCache && target?.isDirectory() && verifiedSiblingCacheAlias(child, path)) {
51323
51333
  scan.entries++;
51324
51334
  continue;
51325
51335
  }
@@ -51353,6 +51363,8 @@ function inventoryNativeSkills(home = homedir3(), options = {}) {
51353
51363
  for (const root of [...options.agentRoots ?? [], ...configured])
51354
51364
  if (includesAgent(root.agent))
51355
51365
  scanRoot(root.agent, canonicalAgentPath(root.path, aliases), true);
51366
+ if (options.reviewedCacheAlias !== undefined && !reviewedCacheAliasFound)
51367
+ throw new Error("Reviewed cache alias was not an inventoried skill-containing sibling alias");
51356
51368
  recheckRootAliases(aliases);
51357
51369
  return entries;
51358
51370
  }
@@ -51934,12 +51946,17 @@ function assertManagedAgentBridge(agent, options = {}) {
51934
51946
  const setting = (path) => (codexConfig.skills?.config ?? []).filter((entry) => typeof entry.path === "string" && [path, join20(path, "SKILL.md")].includes(canonicalAgentPath(entry.path, aliases)));
51935
51947
  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))
51936
51948
  throw new Error("NATIVE_SKILL_DRIFT: the Codex CLI bridge is disabled; run skills hook install");
51937
- const disabledBuiltin = (entry) => {
51938
- if (agent !== "codex" || !entry.vendor || !entry.path.startsWith(canonicalAgentPath(join20(home, ".codex", "skills", ".system"), aliases) + sep4))
51949
+ const disabledVendorSkill = (entry) => {
51950
+ if (agent !== "codex" || !entry.vendor)
51939
51951
  return false;
51940
- const matched = Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
51941
51952
  const settings = setting(entry.path);
51942
- return matched && settings.length === 1 && settings[0]?.enabled === false;
51953
+ if (settings.length !== 1 || settings[0]?.enabled !== false)
51954
+ return false;
51955
+ if (entry.path.startsWith(canonicalAgentPath(join20(home, ".codex", "plugins", "cache"), aliases) + sep4))
51956
+ return true;
51957
+ if (!entry.path.startsWith(canonicalAgentPath(join20(home, ".codex", "skills", ".system"), aliases) + sep4))
51958
+ return false;
51959
+ return Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
51943
51960
  };
51944
51961
  if (!discovery || discovery.agent !== agent)
51945
51962
  throw new Error("NATIVE_SKILL_DRIFT: native discovery coverage is missing; run skills hook install");
@@ -51956,7 +51973,7 @@ function assertManagedAgentBridge(agent, options = {}) {
51956
51973
  throw new Error(`NATIVE_SKILL_DRIFT: ${error.message}`);
51957
51974
  }
51958
51975
  const inventory = inventoryNativeSkills(home, { includeVendor: true, guardHermes: agent === "hermes", agents: [agent], projectDirs: [...roots], agentRoots: discovery.roots.map((path) => ({ agent, path })), allowRootAliases: aliases.length > 0 });
51959
- const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledBuiltin(entry));
51976
+ const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledVendorSkill(entry));
51960
51977
  if (unexpected.length) {
51961
51978
  const paths = unexpected.slice(0, 3).map((entry) => {
51962
51979
  const escaped = JSON.stringify(entry.path).replace(/[\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
@@ -81557,6 +81574,7 @@ var init_skill_context = __esm(() => {
81557
81574
 
81558
81575
  // src/lib/session-reconciliation.ts
81559
81576
  import { createHash as createHash22 } from "crypto";
81577
+ import { isDeepStrictEqual as isDeepStrictEqual8 } from "util";
81560
81578
  function exactTimestamp(value) {
81561
81579
  const parsed = value === undefined ? Number.NaN : Date.parse(value);
81562
81580
  if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
@@ -81589,6 +81607,52 @@ function inspectSkillSession(sessionId, options = {}) {
81589
81607
  loadedCount: receipt.loaded.length
81590
81608
  };
81591
81609
  }
81610
+ async function reconcileSkillSessionIfSafe(sessionId, profileId, options = {}) {
81611
+ const snapshot2 = readSkillSessionSnapshotIfExists(sessionId, options);
81612
+ if (!snapshot2)
81613
+ return false;
81614
+ const old = snapshot2.receipt;
81615
+ if (old.profile.profileId !== profileId)
81616
+ throw new SkillSelectionError("PROFILE_LOCK_MISMATCH", "The session is pinned to a different Skills profile.");
81617
+ const now3 = (options.now ?? Date.now)();
81618
+ const age = now3 - Date.parse(old.verifiedAt);
81619
+ if (age >= 0 && age <= MAX_CACHED_PROFILE_AGE_MS)
81620
+ return false;
81621
+ if (age < 0)
81622
+ throw new SkillSelectionError("SESSION_RECONCILIATION_REQUIRED", "The session receipt has a future timestamp; inspect it before changing its pin.");
81623
+ const client2 = options.client ?? await createProfileClient();
81624
+ const target = structuredClone(await client2.resolveProfile(profileId));
81625
+ validateResolvedProfile(target, client2.authority);
81626
+ if (target.profileId !== profileId || target.authority !== old.profile.authority || target.workspaceId !== old.profile.workspaceId) {
81627
+ throw new SkillSelectionError("PROFILE_IDENTITY_MISMATCH", "The authenticated Skills profile does not match this session's profile, authority and workspace.");
81628
+ }
81629
+ const oldByKey = new Map(old.profile.selections.map((selection) => [selectionKey(selection), selection]));
81630
+ const targetBySlug = new Map(target.selections.map((selection) => [selection.slug, selection]));
81631
+ for (const key of old.loaded) {
81632
+ const previous = oldByKey.get(key);
81633
+ const current = targetBySlug.get(previous.slug);
81634
+ if (!current || !isDeepStrictEqual8({ ...previous, profileRevision: target.profileRevision }, current)) {
81635
+ throw new SkillSelectionError("SESSION_RECONCILIATION_REQUIRED", "A loaded Skills selection was removed or changed; review skills sessions reconcile before changing this session's pin.");
81636
+ }
81637
+ }
81638
+ const replacement = {
81639
+ ...old,
81640
+ generation: nextSkillSessionGeneration(snapshot2.generation),
81641
+ verifiedAt: new Date((options.now ?? Date.now)()).toISOString(),
81642
+ profile: target,
81643
+ loaded: [...old.loaded]
81644
+ };
81645
+ replaceSkillSession(skillSessionSnapshotBinding(snapshot2), replacement, {
81646
+ schemaVersion: 1,
81647
+ kind: "managed-hook-safe-reconciliation",
81648
+ sessionId,
81649
+ fromProfileRevision: old.profile.profileRevision,
81650
+ toProfileRevision: target.profileRevision,
81651
+ retainedLoadedCount: old.loaded.length,
81652
+ retiredLoadedCount: 0
81653
+ }, options);
81654
+ return true;
81655
+ }
81592
81656
  async function reconcileSkillSession(input2, options = {}) {
81593
81657
  if (![input2.fromProfile, input2.fromRevision, input2.selectionProfile, input2.profileRevision].every((value) => typeof value === "string" && value.trim()) || !/^[a-f0-9]{64}$/.test(input2.receiptSha256)) {
81594
81658
  throw new SkillSelectionError("INVALID_SESSION_RECONCILIATION", "Name the exact old receipt SHA256, old profile/revision and intended target profile/revision.");
@@ -81707,7 +81771,7 @@ Review digest: ${result2.reviewDigest}`);
81707
81771
  reportContextError(error61, options.json);
81708
81772
  }
81709
81773
  });
81710
- sessions.command("reconcile <id>").requiredOption("--from-profile <id>", "Expected current session profile").requiredOption("--from-revision <revision>", "Expected current session profile revision").requiredOption("--receipt-sha256 <sha256>", "Expected exact current receipt bytes").requiredOption("--selection-profile <id>", "Intended target selection profile").requiredOption("--profile-revision <revision>", "Expected current API target revision").option("--apply", "Archive the old receipt and atomically apply the reviewed plan", false).option("--plan-digest <sha256>", "Exact reviewed plan digest; required with --apply").option("--plan-issued-at <timestamp>", "Exact reviewed plan issuedAt; required with --apply").option("--plan-expires-at <timestamp>", "Exact reviewed plan expiresAt; required with --apply").option("--json", "Return the plan or application receipt as JSON", false).description("Plan an intentional migration of one session pin; ordinary hooks never migrate pins").action(async (id, options) => {
81774
+ sessions.command("reconcile <id>").requiredOption("--from-profile <id>", "Expected current session profile").requiredOption("--from-revision <revision>", "Expected current session profile revision").requiredOption("--receipt-sha256 <sha256>", "Expected exact current receipt bytes").requiredOption("--selection-profile <id>", "Intended target selection profile").requiredOption("--profile-revision <revision>", "Expected current API target revision").option("--apply", "Archive the old receipt and atomically apply the reviewed plan", false).option("--plan-digest <sha256>", "Exact reviewed plan digest; required with --apply").option("--plan-issued-at <timestamp>", "Exact reviewed plan issuedAt; required with --apply").option("--plan-expires-at <timestamp>", "Exact reviewed plan expiresAt; required with --apply").option("--json", "Return the plan or application receipt as JSON", false).description("Plan an intentional migration of one session pin when loaded selections changed").action(async (id, options) => {
81711
81775
  try {
81712
81776
  const result2 = await reconcileSkillSession({ ...options, sessionId: id });
81713
81777
  await writeCliOutput(options.json ? JSON.stringify(result2) : result2.applied ? `Reconciled ${JSON.stringify(id)} to ${result2.plan.target.profileId} at ${result2.plan.target.profileRevision}.
@@ -81729,18 +81793,27 @@ Preserved original receipt: ${result2.archivePath}` : `Planned one session recon
81729
81793
  reportContextError(error61, options.json);
81730
81794
  }
81731
81795
  });
81732
- parent.command("context [prompt]").description("Select bounded, versioned skill context for this prompt without running skills").option("--stdin", "Read native hook JSON or a prompt from stdin", false).option("--selection-profile <id>", "Selection profile (separate from the credential --profile)").option("--cached", "Use only an explicitly selected verified cache, at most 24 hours old", false).option("--session <id>", "Keep the same skill versions across this session").option("--restore", "Re-emit loaded session context after compaction or resume", false).option("--max-chars <count>", "Maximum injected characters", "8000").option("--max-skills <count>", "Maximum loaded skills", "3").option("--json", "Return context and selection receipts as JSON", false).action(async (prompt, options) => {
81796
+ parent.command("context [prompt]").description("Select bounded, versioned skill context for this prompt without running skills").option("--stdin", "Read native hook JSON or a prompt from stdin", false).option("--selection-profile <id>", "Selection profile (separate from the credential --profile)").option("--cached", "Use only an explicitly selected verified cache, at most 24 hours old", false).option("--auto-reconcile-safe", "For managed hooks, archive and renew an expired session only when all loaded selections are unchanged", false).option("--session <id>", "Keep the same skill versions across this session").option("--restore", "Re-emit loaded session context after compaction or resume", false).option("--max-chars <count>", "Maximum injected characters", "8000").option("--max-skills <count>", "Maximum loaded skills", "3").option("--json", "Return context and selection receipts as JSON", false).action(async (prompt, options) => {
81733
81797
  try {
81798
+ if (options.autoReconcileSafe && !options.cached)
81799
+ throw new SkillSelectionError("INVALID_CONTEXT_INPUT", "--auto-reconcile-safe requires --cached.");
81734
81800
  const input2 = options.stdin ? parseSkillContextInput(await readContextStdin()) : { prompt: prompt ?? "" };
81735
81801
  input2.profileId = selectedProfileId(options.selectionProfile ?? input2.profileId);
81736
81802
  input2.cwd ??= process.cwd();
81737
81803
  input2.sessionId = options.session ?? input2.sessionId;
81738
81804
  input2.restore ||= options.restore;
81739
- const result2 = await buildSkillContext(input2, {
81740
- ...contextResolverOptions(options),
81741
- maxChars: Number(options.maxChars),
81742
- maxSkills: Number(options.maxSkills)
81743
- });
81805
+ const resolverOptions = { ...contextResolverOptions(options), maxChars: Number(options.maxChars), maxSkills: Number(options.maxSkills) };
81806
+ let result2;
81807
+ try {
81808
+ result2 = await buildSkillContext(input2, resolverOptions);
81809
+ } catch (error61) {
81810
+ if (!options.cached || !options.autoReconcileSafe || !(error61 instanceof SkillSelectionError) || error61.code !== "CACHED_PROFILE_EXPIRED" || !input2.sessionId)
81811
+ throw error61;
81812
+ const sessionId = input2.agentId ? `${input2.sessionId}:${input2.agentId}` : input2.sessionId;
81813
+ if (!await reconcileSkillSessionIfSafe(sessionId, input2.profileId, resolverOptions))
81814
+ throw error61;
81815
+ result2 = await buildSkillContext(input2, resolverOptions);
81816
+ }
81744
81817
  await writeCliOutput(options.json ? JSON.stringify(result2) : result2.context);
81745
81818
  } catch (error61) {
81746
81819
  reportContextError(error61, options.json);
@@ -86158,7 +86231,7 @@ var init_execution_grants = __esm(() => {
86158
86231
  });
86159
86232
 
86160
86233
  // src/lib/execution-grant-client.ts
86161
- import { isDeepStrictEqual as isDeepStrictEqual8 } from "util";
86234
+ import { isDeepStrictEqual as isDeepStrictEqual9 } from "util";
86162
86235
  function id(value) {
86163
86236
  if (!grantIdentifier(value))
86164
86237
  throw new Error("Invalid execution grant profile or revision identifier.");
@@ -86210,7 +86283,7 @@ async function saveExecutionGrantPolicy(profileId, grants, revision) {
86210
86283
  headers: revision === undefined ? { "If-None-Match": "*" } : { "If-Match": `"${revision}"` },
86211
86284
  body: JSON.stringify({ grants: snapshot2 })
86212
86285
  }));
86213
- if (policy.profileId !== profileId || policy.previousRevision !== (revision ?? null) || !isDeepStrictEqual8(policy.grants, snapshot2)) {
86286
+ if (policy.profileId !== profileId || policy.previousRevision !== (revision ?? null) || !isDeepStrictEqual9(policy.grants, snapshot2)) {
86214
86287
  throw new Error("The API did not preserve the reviewed execution grant policy; read it before retrying.");
86215
86288
  }
86216
86289
  return policy;
@@ -86221,7 +86294,7 @@ async function resolveExecutionGrant(input2) {
86221
86294
  if (!grantRecord(value) || Object.keys(value).length !== 3 || !grantIdentifier(value.policyRevision) || !grantIdentifier(value.grantId) || !grantRecord(value.bindings)) {
86222
86295
  throw new SkillSelectionError("INVALID_EXECUTION_GRANT", "The Skills API returned an invalid execution grant.");
86223
86296
  }
86224
- if (!isDeepStrictEqual8(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual8(value.bindings.consumer, snapshot2.consumer)) {
86297
+ if (!isDeepStrictEqual9(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual9(value.bindings.consumer, snapshot2.consumer)) {
86225
86298
  throw new SkillSelectionError("EXECUTION_GRANT_SCOPE_MISMATCH", "The Skills API returned a grant for a different selection or consumer.");
86226
86299
  }
86227
86300
  return value;
@@ -109440,7 +109513,7 @@ var init_create_sync_config = __esm(() => {
109440
109513
  });
109441
109514
 
109442
109515
  // src/lib/codex-native-config.ts
109443
- import { isDeepStrictEqual as isDeepStrictEqual9 } from "util";
109516
+ import { isDeepStrictEqual as isDeepStrictEqual10 } from "util";
109444
109517
  function isRecord7(value) {
109445
109518
  if (!value || typeof value !== "object")
109446
109519
  return false;
@@ -109462,7 +109535,7 @@ function decimalIdentity(text4) {
109462
109535
  return `${match[1]}${coefficient}e${exponent}`;
109463
109536
  }
109464
109537
  function codexNativeConfigEqual(native, expected) {
109465
- if (isDeepStrictEqual9(native, expected))
109538
+ if (isDeepStrictEqual10(native, expected))
109466
109539
  return true;
109467
109540
  if (typeof expected === "number") {
109468
109541
  if (!Number.isFinite(expected) || !isRecord7(native))
@@ -109628,7 +109701,7 @@ var init_codex_hook_rpc = __esm(() => {
109628
109701
  // src/lib/codex-hook-trust-identity.ts
109629
109702
  import { realpathSync as realpathSync13 } from "fs";
109630
109703
  import { dirname as dirname31, isAbsolute as isAbsolute25, join as join57 } from "path";
109631
- import { isDeepStrictEqual as isDeepStrictEqual10 } from "util";
109704
+ import { isDeepStrictEqual as isDeepStrictEqual11 } from "util";
109632
109705
  function inspectRecordedSkillsCli(command, expected) {
109633
109706
  need4(isAbsolute25(command) && !command.includes("\x00"), "RECONCILE_RECORDED_COMMAND_REQUIRED");
109634
109707
  const resolved = realpathSync13(command);
@@ -109638,7 +109711,7 @@ function inspectRecordedSkillsCli(command, expected) {
109638
109711
  need4(pkg.name === "@hasna/skills" && /^\d+\.\d+\.\d+$/.test(pkg.version) && pkg.bin?.skills === "bin/index.js" && resolved === join57(dirname31(manifest.file), "bin/index.js"), "SKILLS_PACKAGE_MISMATCH");
109639
109712
  need4((cli.stat.mode & 0o100n) !== 0n, "SKILLS_COMMAND_NOT_EXECUTABLE");
109640
109713
  const receipt = { path: resolved, version: pkg.version, sha256: cli.sha256, manifestSha256: manifest.sha256 };
109641
- need4(isDeepStrictEqual10(receipt, expected), "RECONCILE_SKILLS_BINDING_CHANGED");
109714
+ need4(isDeepStrictEqual11(receipt, expected), "RECONCILE_SKILLS_BINDING_CHANGED");
109642
109715
  return { receipt, recheck() {
109643
109716
  need4(realpathSync13(command) === resolved, "SKILLS_COMMAND_CHANGED");
109644
109717
  unchanged2(cli);
@@ -109674,19 +109747,19 @@ var init_codex_hook_trust_identity = __esm(() => {
109674
109747
  });
109675
109748
 
109676
109749
  // src/lib/codex-hook-policy-recovery.ts
109677
- import { isDeepStrictEqual as isDeepStrictEqual11 } from "util";
109750
+ import { isDeepStrictEqual as isDeepStrictEqual12 } from "util";
109678
109751
  function reviewedClaudeDiscoveryChange(beforeText, currentText, options) {
109679
109752
  need4(typeof options.reason === "string" && options.reason.trim().length > 0 && options.reason.length <= 512 && !/[\x00-\x1f\x7f]/.test(options.reason), "RECONCILE_RECOVERY_REASON_REQUIRED");
109680
109753
  need4(options.reviewedPlanDigest === undefined || /^[a-f0-9]{64}$/.test(options.reviewedPlanDigest), "RECONCILE_RECOVERY_PLAN_CHANGED");
109681
109754
  const before2 = parseManagedSkillPolicy(beforeText), current = parseManagedSkillPolicy(currentText);
109682
109755
  const oldDiscovery = before2.bridge?.discovery?.claude, newDiscovery = current.bridge?.discovery?.claude;
109683
109756
  need4(oldDiscovery?.agent === "claude" && newDiscovery?.agent === "claude" && oldDiscovery.method === "reviewed" && newDiscovery.method === "reviewed", "RECONCILE_POLICY_CHANGED");
109684
- const changes = ["roots", "sources"].filter((key) => !isDeepStrictEqual11(oldDiscovery[key], newDiscovery[key])).map((key) => ({ path: `bridge.discovery.claude.${key}`, before: oldDiscovery[key], after: newDiscovery[key] }));
109757
+ const changes = ["roots", "sources"].filter((key) => !isDeepStrictEqual12(oldDiscovery[key], newDiscovery[key])).map((key) => ({ path: `bridge.discovery.claude.${key}`, before: oldDiscovery[key], after: newDiscovery[key] }));
109685
109758
  need4(changes.length > 0, "RECONCILE_POLICY_CHANGED");
109686
109759
  const compared = structuredClone(current);
109687
109760
  compared.bridge.discovery.claude.roots = oldDiscovery.roots;
109688
109761
  compared.bridge.discovery.claude.sources = oldDiscovery.sources;
109689
- need4(isDeepStrictEqual11(before2, compared), "RECONCILE_POLICY_CHANGED");
109762
+ need4(isDeepStrictEqual12(before2, compared), "RECONCILE_POLICY_CHANGED");
109690
109763
  verifyAgentDiscovery(newDiscovery);
109691
109764
  return { reason: options.reason, changes };
109692
109765
  }
@@ -109702,7 +109775,7 @@ import { createHash as createHash28, randomUUID as randomUUID10 } from "crypto";
109702
109775
  import { execFileSync } from "child_process";
109703
109776
  import { dirname as dirname32, isAbsolute as isAbsolute26, join as join58, resolve as resolve23 } from "path";
109704
109777
  import { homedir as homedir12 } from "os";
109705
- import { isDeepStrictEqual as isDeepStrictEqual12 } from "util";
109778
+ import { isDeepStrictEqual as isDeepStrictEqual13 } from "util";
109706
109779
  function verifyReleaseArtifact(proof, skillsCli) {
109707
109780
  need4(isAbsolute26(proof.packageTarPath) && !proof.packageTarPath.includes("\x00"), "RECONCILE_RELEASE_PROVENANCE_PATH");
109708
109781
  const tarPath = realpathSync14(proof.packageTarPath);
@@ -109744,7 +109817,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109744
109817
  const configSha256 = intent.configSha256 ?? configBefore.sha256, configVersion = intent.configVersion ?? intent.expectedVersion;
109745
109818
  const digestInput = { version: 1, home: intent.home ?? home, configPath: intent.configPath, ...intent.nativeConfigPath !== undefined ? { nativeConfigPath: intent.nativeConfigPath } : {}, configSha256, configVersion, policySha256: intent.policySha256, hooksSha256: intent.hooksSha256, skillsCli: intent.skillsCli, nativeVersion: intent.nativeVersion, declarations, admitted };
109746
109819
  need4(makePlanDigest(digestInput) === intent.planDigest, "RECONCILE_PLAN_CHANGED");
109747
- need4(isDeepStrictEqual12(intent.hooks, admitted.filter((hook) => !hook.enabled || hook.trustStatus !== "trusted")), "RECONCILE_INTENT_HOOKS_CHANGED");
109820
+ need4(isDeepStrictEqual13(intent.hooks, admitted.filter((hook) => !hook.enabled || hook.trustStatus !== "trusted")), "RECONCILE_INTENT_HOOKS_CHANGED");
109748
109821
  need4(configBefore.sha256 === (intent.beforeSha256 ?? intent.configSha256) && hooksBefore.sha256 === intent.hooksSha256 && policyBefore.sha256 === intent.policySha256, "RECONCILE_JOURNAL_CHANGED");
109749
109822
  const policyCurrent = snapshot(join58(dataDir3, "agent-policy.json"), true);
109750
109823
  const { claudeDiscoveryRecovery: claudeRecovery, nativeHookAdditionsRecovery: additionsRecovery } = options;
@@ -109757,7 +109830,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109757
109830
  if (policyCurrent.sha256 !== policyBefore.sha256 && !claudeRecovery) {
109758
109831
  need4(options.supersedeBinding === true, "RECONCILE_POLICY_CHANGED");
109759
109832
  const oldPolicy = JSON.parse(policyBefore.text), currentPolicy = JSON.parse(policyCurrent.text);
109760
- need4(isDeepStrictEqual12(withoutCodexCommand(oldPolicy), withoutCodexCommand(currentPolicy)), "RECONCILE_POLICY_CHANGED");
109833
+ need4(isDeepStrictEqual13(withoutCodexCommand(oldPolicy), withoutCodexCommand(currentPolicy)), "RECONCILE_POLICY_CHANGED");
109761
109834
  supersededBinding = true;
109762
109835
  }
109763
109836
  if (supersededBinding || recovery) {
@@ -109793,7 +109866,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109793
109866
  need4(state?.enabled === true && typeof state.trusted_hash === "string" && /^sha256:[a-f0-9]{64}$/.test(state.trusted_hash), "RECONCILE_CONFIG_DRIFT");
109794
109867
  }
109795
109868
  } else
109796
- need4(isDeepStrictEqual12(expectedConfig, current), "RECONCILE_CONFIG_DRIFT");
109869
+ need4(isDeepStrictEqual13(expectedConfig, current), "RECONCILE_CONFIG_DRIFT");
109797
109870
  const configDiscoveryRecovery = recovery && binding.discovery?.codex ? createCodexHookDiscoveryRecovery(binding.discovery.codex, configBefore, currentConfig, intent.hooks, additions?.trustProof) : undefined;
109798
109871
  assertManagedAgentBridge("codex", { home, dataDir: dataDir3, projectDir: home, codexDiscoveryRecovery: configDiscoveryRecovery });
109799
109872
  const recoveryCli = recovery ? bindSkillsCli("skills", options.reviewedSkillsCli) : undefined;
@@ -109802,14 +109875,14 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109802
109875
  const skillsCli = recovery ? inspectRecordedSkillsCli(binding.commands.codex, intent.skillsCli) : bindSkillsCli(binding.commands.codex, options.reviewedSkillsCli);
109803
109876
  if (supersededBinding)
109804
109877
  verifyReleaseArtifact(options.releaseProof, skillsCli);
109805
- const skillsBindingChanged = !isDeepStrictEqual12(skillsCli.receipt, intent.skillsCli);
109878
+ const skillsBindingChanged = !isDeepStrictEqual13(skillsCli.receipt, intent.skillsCli);
109806
109879
  need4(!skillsBindingChanged || supersededBinding, "RECONCILE_SKILLS_BINDING_CHANGED");
109807
109880
  if (!additions && hooksCurrent.sha256 !== intent.hooksSha256 && hooksCurrent.sha256 !== hooksBefore.sha256) {
109808
109881
  need4(supersededBinding, "RECONCILE_HOOKS_CHANGED");
109809
109882
  const currentDeclarations = JSON.parse(hooksCurrent.text).hooks;
109810
109883
  for (const [event, [nativeEvent]] of Object.entries(eventNames)) {
109811
109884
  const command = renderAgentHookCommand(binding.commands.codex, "codex", binding.profiles.codex, event);
109812
- need4(isDeepStrictEqual12(currentDeclarations?.[event], [{ hooks: [{ type: "command", command, timeout: 15 }] }]), "RECONCILE_BINDING_HOOK_CHANGED");
109885
+ need4(isDeepStrictEqual13(currentDeclarations?.[event], [{ hooks: [{ type: "command", command, timeout: 15 }] }]), "RECONCILE_BINDING_HOOK_CHANGED");
109813
109886
  }
109814
109887
  }
109815
109888
  rpc = await connect({ command: options.codexCommand ?? "codex", home, codexHome });
@@ -109832,9 +109905,9 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109832
109905
  delete identity4.command;
109833
109906
  return identity4;
109834
109907
  };
109835
- need4(isDeepStrictEqual12(stable(actual), stable(expected)), "RECONCILE_NATIVE_IDENTITY_CHANGED");
109908
+ need4(isDeepStrictEqual13(stable(actual), stable(expected)), "RECONCILE_NATIVE_IDENTITY_CHANGED");
109836
109909
  if (!plannedKeys.has(expected.key))
109837
- need4(isDeepStrictEqual12(actual, expected), "RECONCILE_UNRELATED_HOOK_CHANGED");
109910
+ need4(isDeepStrictEqual13(actual, expected), "RECONCILE_UNRELATED_HOOK_CHANGED");
109838
109911
  }
109839
109912
  for (const wanted of intent.hooks) {
109840
109913
  const matches2 = nativeHooks.filter((h2) => h2.key === wanted.key || h2.key === wanted.key.replace(join58(home, ".codex/hooks.json"), hooksPath));
@@ -109853,7 +109926,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109853
109926
  const layers = config2?.layers?.filter((layer) => layer.name?.type === "user" && layer.name.file === nativeConfigPath && !layer.name.profile);
109854
109927
  need4(layers?.length === 1 && !layers[0].disabledReason && codexNativeConfigEqual(layers[0].config, current), "RECONCILE_NATIVE_CONFIG_MISMATCH");
109855
109928
  const finalDiscovery = await rpc.request("hooks/list", { cwds: [home] });
109856
- need4(Array.isArray(finalDiscovery?.data) && finalDiscovery.data.length === 1 && finalDiscovery.data[0].cwd === home && !finalDiscovery.data[0].errors?.length && !finalDiscovery.data[0].warnings?.length && (!recordedNativeHooks || isDeepStrictEqual12(finalDiscovery.data[0].hooks.map(safeNativeHook), nativeHooks.map(safeNativeHook))), "RECONCILE_NATIVE_STATE_CHANGED");
109929
+ need4(Array.isArray(finalDiscovery?.data) && finalDiscovery.data.length === 1 && finalDiscovery.data[0].cwd === home && !finalDiscovery.data[0].errors?.length && !finalDiscovery.data[0].warnings?.length && (!recordedNativeHooks || isDeepStrictEqual13(finalDiscovery.data[0].hooks.map(safeNativeHook), nativeHooks.map(safeNativeHook))), "RECONCILE_NATIVE_STATE_CHANGED");
109857
109930
  const finalConfig = await rpc.request("config/read", { includeLayers: true, cwd: home });
109858
109931
  const finalLayers = finalConfig?.layers?.filter((layer) => layer.name?.type === "user" && layer.name.file === nativeConfigPath && !layer.name.profile);
109859
109932
  need4(finalLayers?.length === 1 && !finalLayers[0].disabledReason && codexNativeConfigEqual(finalLayers[0].config, current), "RECONCILE_NATIVE_CONFIG_CHANGED");
@@ -109914,7 +109987,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
109914
109987
  const groups = declarations[event];
109915
109988
  need4(Array.isArray(groups), "DECLARATION_CHANGED");
109916
109989
  const matches2 = groups.flatMap((group, gi) => (group.hooks ?? []).flatMap((entry, hi) => entry.command === command ? [{ group, entry, gi, hi }] : []));
109917
- need4(matches2.length === 1 && isDeepStrictEqual12(matches2[0].group, { hooks: [{ type: "command", command, timeout: 15 }] }), "DECLARATION_CHANGED");
109990
+ need4(matches2.length === 1 && isDeepStrictEqual13(matches2[0].group, { hooks: [{ type: "command", command, timeout: 15 }] }), "DECLARATION_CHANGED");
109918
109991
  expected.push({ event: nativeEvent, key: `${join58(home, ".codex/hooks.json")}:${label}:${matches2[0].gi}:${matches2[0].hi}`, command });
109919
109992
  }
109920
109993
  codexTrustTextWitness(configFile.text, expected.map((h) => h.key));
@@ -109980,7 +110053,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
109980
110053
  save(join58(journal, "hooks.before.json"), hooksFile.bytes);
109981
110054
  save(join58(journal, "policy.before.json"), policyFile.bytes);
109982
110055
  save(join58(journal, "intent.json"), json2({ ...digestInput, planDigest, hooks: planned, nativeHooks: discovered.map(safeNativeHook) }));
109983
- need4(isDeepStrictEqual12(admit(await list2()), admitted), "NATIVE_DISCOVERY_CHANGED");
110056
+ need4(isDeepStrictEqual13(admit(await list2()), admitted), "NATIVE_DISCOVERY_CHANGED");
109984
110057
  checkInputs();
109985
110058
  let result2;
109986
110059
  try {
@@ -109994,7 +110067,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
109994
110067
  expectedConfig.hooks.state ??= {};
109995
110068
  for (const h of planned)
109996
110069
  expectedConfig.hooks.state[h.key] = { ...expectedConfig.hooks.state[h.key], enabled: true, trusted_hash: h.currentHash };
109997
- need4(isDeepStrictEqual12(structure3(after2.text), expectedConfig) && codexTrustTextWitness(after2.text, keys3) === preservationText, "PRESERVATION_FAILED");
110070
+ need4(isDeepStrictEqual13(structure3(after2.text), expectedConfig) && codexTrustTextWitness(after2.text, keys3) === preservationText, "PRESERVATION_FAILED");
109998
110071
  const savedConfig = snapshot(join58(journal, "config.before.toml"), true);
109999
110072
  need4(savedConfig.sha256 === configFile.sha256, "JOURNAL_CHANGED");
110000
110073
  const discoveryTransition = binding.discovery?.codex ? createCodexHookDiscoveryRecovery(binding.discovery.codex, savedConfig, after2, planned) : undefined;
@@ -110002,8 +110075,8 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
110002
110075
  unchanged2(hooksFile);
110003
110076
  assertManagedAgentBridge("codex", { home, dataDir: dataDir3, projectDir: home, codexDiscoveryRecovery: discoveryTransition });
110004
110077
  const current = await list2(), confirmed = admit(current);
110005
- need4(confirmed.every((h) => h.enabled && h.trustStatus === "trusted") && isDeepStrictEqual12(confirmed.map((h) => [h.key, h.currentHash]), admitted.map((h) => [h.key, h.currentHash])), "NATIVE_NOT_ELIGIBLE");
110006
- need4(isDeepStrictEqual12(current.filter((h) => !keys3.includes(h.key)), discovered.filter((h) => !keys3.includes(h.key))), "UNRELATED_HOOK_CHANGED");
110078
+ need4(confirmed.every((h) => h.enabled && h.trustStatus === "trusted") && isDeepStrictEqual13(confirmed.map((h) => [h.key, h.currentHash]), admitted.map((h) => [h.key, h.currentHash])), "NATIVE_NOT_ELIGIBLE");
110079
+ need4(isDeepStrictEqual13(current.filter((h) => !keys3.includes(h.key)), discovered.filter((h) => !keys3.includes(h.key))), "UNRELATED_HOOK_CHANGED");
110007
110080
  unchanged2(after2);
110008
110081
  unchanged2(savedConfig);
110009
110082
  unchanged2(hooksFile);
@@ -110070,6 +110143,9 @@ function hookFailureReason(error61, profileId) {
110070
110143
  if (code2 === "PROFILE_LOCK_MISMATCH") {
110071
110144
  return `${prefix} This session or project is pinned to a different profile. Inspect the session with skills sessions show <session-id> --json; review skills sessions reconcile for an intentional session change, or explicit project sync for a project change. Sync alone does not change that pin.`;
110072
110145
  }
110146
+ if (code2 === "SESSION_RECONCILIATION_REQUIRED") {
110147
+ return `${prefix} A loaded skill changed or was removed. Inspect this session with skills sessions show <session-id> --json, then review skills sessions reconcile for an intentional change. Sync alone does not change this pin.`;
110148
+ }
110073
110149
  if (code2.startsWith("SESSION_")) {
110074
110150
  return `${prefix} Inspect the session with skills sessions show <session-id> --json and review its parent or concurrent writer before retrying. Do not remove or rewrite its receipt to bypass this refusal.`;
110075
110151
  }
@@ -110112,6 +110188,7 @@ var init_hook_diagnostics = __esm(() => {
110112
110188
  "SESSION_GENERATION_EXHAUSTED",
110113
110189
  "SESSION_PARENT_CHANGED",
110114
110190
  "SESSION_RECEIPT_CHANGED",
110191
+ "SESSION_RECONCILIATION_REQUIRED",
110115
110192
  "SESSION_RECONCILIATION_INCOMPLETE",
110116
110193
  "SESSION_WRITE_LOCKED",
110117
110194
  "SESSION_WRITE_LOCK_CHANGED",
@@ -110156,7 +110233,7 @@ async function contextForHook(input3, profileId, cached2, deadline) {
110156
110233
  throw new HookDiagnosticError("SKILLS_HOOK_TIMEOUT", "context");
110157
110234
  const args2 = [process.execPath, process.argv[1], "context", "--stdin", "--json", "--selection-profile", profileId];
110158
110235
  if (cached2)
110159
- args2.push("--cached");
110236
+ args2.push("--cached", "--auto-reconcile-safe");
110160
110237
  const child = Bun.spawn(args2, { stdin: new Blob([JSON.stringify(input3)]), stdout: "pipe", stderr: "pipe", env: { ...process.env, NO_COLOR: "1" } });
110161
110238
  let timedOut = false;
110162
110239
  const timer = setTimeout(() => {
@@ -110363,13 +110440,18 @@ function registerAgentIntegration(parent) {
110363
110440
  }
110364
110441
  });
110365
110442
  const migrate = parent.command("migrate").description("Preserve and retire native agent skill copies");
110366
- migrate.command("native").option("--project <directory>", "Also inventory a project and its ancestors (the current directory and its ancestors are always included)").option("--include-unmanaged", "Archive user-authored skills as well as Skills-managed copies", false).option("--include-vendor", "Retire vendor SKILL.md discovery files while preserving plugin scripts and assets", false).option("--target-manifest <file>", "Reviewed exact native migration target manifest").option("--discovery-inputs <file>", "Advanced reviewed active plugin roots and source hashes").option("--allow-root-aliases", "Allow home .claude/.codex aliases to existing directories within this home", false).option("--apply", "Move selected skills to private archives outside agent discovery roots", false).option("--json", "Output inventory and archive receipt as JSON", false).description("Inventory native skill copies; preserve complete directories before retiring them").action(async (options) => {
110443
+ migrate.command("native").option("--agent <agent>", "Limit inventory and migration to one maintained agent").option("--project <directory>", "Also inventory a project and its ancestors (the current directory and its ancestors are always included)").option("--include-unmanaged", "Archive user-authored skills as well as Skills-managed copies", false).option("--include-vendor", "Retire vendor SKILL.md discovery files while preserving plugin scripts and assets", false).option("--reviewed-cache-alias <path>", "Exact skill-containing vendor cache alias reviewed for migration; runtime hooks still refuse it").option("--target-manifest <file>", "Reviewed exact native migration target manifest").option("--discovery-inputs <file>", "Advanced reviewed active plugin roots and source hashes").option("--allow-root-aliases", "Allow home .claude/.codex aliases to existing directories within this home", false).option("--apply", "Move selected skills to private archives outside agent discovery roots", false).option("--json", "Output inventory and archive receipt as JSON", false).description("Inventory native skill copies; preserve complete directories before retiring them").action(async (options) => {
110367
110444
  try {
110445
+ if (options.agent && !INTEGRATION_AGENTS.includes(options.agent))
110446
+ throw new Error(`Unsupported migration agent: ${options.agent}`);
110368
110447
  const discoveryInputs = options.discoveryInputs ? JSON.parse(readFileSync40(options.discoveryInputs, "utf8")) : undefined;
110369
110448
  if (options.targetManifest && (options.includeUnmanaged || options.includeVendor))
110370
110449
  throw new Error("--target-manifest cannot be combined with broad native migration selectors");
110371
110450
  const targetManifest = options.targetManifest ? readNativeMigrationTargetManifest(options.targetManifest) : undefined;
110372
- const inventory = inventoryNativeSkills(undefined, { projectDirs: [process.cwd(), ...options.project ? [options.project] : []], includeVendor: options.includeVendor, configured: options.includeVendor, discoveryInputs, allowRootAliases: options.allowRootAliases });
110451
+ if (options.apply && options.reviewedCacheAlias && !targetManifest)
110452
+ throw new Error("Applying a reviewed cache alias requires an exact --target-manifest");
110453
+ const includeVendor = options.includeVendor || Boolean(targetManifest?.targets.some((target) => target.vendor));
110454
+ const inventory = inventoryNativeSkills(undefined, { projectDirs: [process.cwd(), ...options.project ? [options.project] : []], agents: options.agent ? [options.agent] : undefined, includeVendor, configured: includeVendor, discoveryInputs, allowRootAliases: options.allowRootAliases, reviewedCacheAlias: options.reviewedCacheAlias });
110373
110455
  if (targetManifest)
110374
110456
  selectNativeMigrationTargets(inventory, targetManifest);
110375
110457
  const result2 = options.apply ? archiveNativeSkills(inventory, { includeUnmanaged: options.includeUnmanaged, includeVendor: options.includeVendor, allowRootAliases: options.allowRootAliases, targetManifest }) : { entries: [], ...targetManifest ? { targetManifest: { schema: targetManifest.schema, digest: targetManifest.digest, targetCount: targetManifest.targets.length } } : {} };
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.2",
20022
+ version: "0.10.4",
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.2",
10
+ version: "0.10.4",
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.2",
24125
+ version: "0.10.4",
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.2",
24128
+ version: "0.10.4",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
@@ -5,6 +5,7 @@ export declare function selectedProfileId(explicit?: string): string;
5
5
  export interface ContextCommandOptions {
6
6
  selectionProfile?: string;
7
7
  cached?: boolean;
8
+ autoReconcileSafe?: boolean;
8
9
  json?: boolean;
9
10
  stdin?: boolean;
10
11
  session?: string;
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.2",
9614
+ version: "0.10.4",
9615
9615
  description: "Skills library for AI coding agents",
9616
9616
  type: "module",
9617
9617
  bin: {
@@ -13309,8 +13309,8 @@ function selectNativeMigrationTargets(inventory, manifest) {
13309
13309
  if (matches.length !== 1)
13310
13310
  throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
13311
13311
  const entry = matches[0];
13312
- if (entry.bridge || entry.system || entry.vendor)
13313
- throw new Error(`Native migration target is protected or vendor-owned: ${entry.path}`);
13312
+ if (entry.bridge || entry.system || entry.vendor !== (target.vendor === true))
13313
+ throw new Error(`Native migration target is protected or vendor classification changed: ${entry.path}`);
13314
13314
  if (entry.hash !== target.treeSha256 || treeHash(entry.path) !== target.treeSha256)
13315
13315
  throw new Error(`Native migration target changed after review: ${entry.path}`);
13316
13316
  selected.push(entry);
@@ -13333,6 +13333,8 @@ function projectAncestorDirectories(projects) {
13333
13333
  return [...directories];
13334
13334
  }
13335
13335
  function inventoryNativeSkills(home = homedir2(), options = {}) {
13336
+ if (options.reviewedCacheAlias !== undefined && (!options.includeVendor || !isAbsolute9(options.reviewedCacheAlias) || resolve9(options.reviewedCacheAlias) !== options.reviewedCacheAlias))
13337
+ throw new Error("A reviewed cache alias requires vendor inventory and an exact absolute path");
13336
13338
  const aliases = rootAliases(home, options.allowRootAliases);
13337
13339
  const selectedAgents = options.agents ? new Set(options.agents) : undefined;
13338
13340
  const includesAgent = (agent) => !selectedAgents || selectedAgents.has(agent);
@@ -13353,14 +13355,22 @@ function inventoryNativeSkills(home = homedir2(), options = {}) {
13353
13355
  if (discoveryPathBytes > 4 * 1024 * 1024)
13354
13356
  throw new Error("Native skill discovery metadata limit exceeded");
13355
13357
  }
13356
- function emptySiblingCacheAlias(path, parent) {
13358
+ let reviewedCacheAliasFound = false;
13359
+ function verifiedSiblingCacheAlias(path, parent) {
13357
13360
  const link = readlinkSync3(path), target = resolve9(parent, link);
13358
13361
  if (dirname10(target) !== parent || (isAbsolute9(link) ? link !== target : dirname10(link) !== "."))
13359
13362
  return false;
13360
13363
  if (!lstatSync14(target, { throwIfNoEntry: false })?.isDirectory())
13361
13364
  return false;
13362
13365
  const scan = cacheScans.get(target);
13363
- return Boolean(scan?.complete && !scan.hasSkills && scan.entries <= maxAliasProofEntries);
13366
+ if (!scan?.complete || scan.entries > maxAliasProofEntries)
13367
+ return false;
13368
+ if (scan.hasSkills) {
13369
+ if (path !== options.reviewedCacheAlias)
13370
+ return false;
13371
+ reviewedCacheAliasFound = true;
13372
+ }
13373
+ return true;
13364
13374
  }
13365
13375
  function visit2(agent, path, vendor = false, depth = 0, pluginCache = false, admitted = false) {
13366
13376
  if (!admitted)
@@ -13417,7 +13427,7 @@ function inventoryNativeSkills(home = homedir2(), options = {}) {
13417
13427
  const child = join16(path, name);
13418
13428
  if (isVendor && lstatSync14(child, { throwIfNoEntry: false })?.isSymbolicLink()) {
13419
13429
  const target = statSync7(child, { throwIfNoEntry: false });
13420
- if (target?.isFile() || pluginCache && target?.isDirectory() && emptySiblingCacheAlias(child, path)) {
13430
+ if (target?.isFile() || pluginCache && target?.isDirectory() && verifiedSiblingCacheAlias(child, path)) {
13421
13431
  scan.entries++;
13422
13432
  continue;
13423
13433
  }
@@ -13451,6 +13461,8 @@ function inventoryNativeSkills(home = homedir2(), options = {}) {
13451
13461
  for (const root of [...options.agentRoots ?? [], ...configured])
13452
13462
  if (includesAgent(root.agent))
13453
13463
  scanRoot(root.agent, canonicalAgentPath(root.path, aliases), true);
13464
+ if (options.reviewedCacheAlias !== undefined && !reviewedCacheAliasFound)
13465
+ throw new Error("Reviewed cache alias was not an inventoried skill-containing sibling alias");
13454
13466
  recheckRootAliases(aliases);
13455
13467
  return entries;
13456
13468
  }
@@ -14113,12 +14125,17 @@ function assertManagedAgentBridge(agent, options = {}) {
14113
14125
  const setting = (path) => (codexConfig.skills?.config ?? []).filter((entry) => typeof entry.path === "string" && [path, join16(path, "SKILL.md")].includes(canonicalAgentPath(entry.path, aliases)));
14114
14126
  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))
14115
14127
  throw new Error("NATIVE_SKILL_DRIFT: the Codex CLI bridge is disabled; run skills hook install");
14116
- const disabledBuiltin = (entry) => {
14117
- if (agent !== "codex" || !entry.vendor || !entry.path.startsWith(canonicalAgentPath(join16(home, ".codex", "skills", ".system"), aliases) + sep4))
14128
+ const disabledVendorSkill = (entry) => {
14129
+ if (agent !== "codex" || !entry.vendor)
14118
14130
  return false;
14119
- const matched = Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
14120
14131
  const settings = setting(entry.path);
14121
- return matched && settings.length === 1 && settings[0]?.enabled === false;
14132
+ if (settings.length !== 1 || settings[0]?.enabled !== false)
14133
+ return false;
14134
+ if (entry.path.startsWith(canonicalAgentPath(join16(home, ".codex", "plugins", "cache"), aliases) + sep4))
14135
+ return true;
14136
+ if (!entry.path.startsWith(canonicalAgentPath(join16(home, ".codex", "skills", ".system"), aliases) + sep4))
14137
+ return false;
14138
+ return Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
14122
14139
  };
14123
14140
  if (!discovery || discovery.agent !== agent)
14124
14141
  throw new Error("NATIVE_SKILL_DRIFT: native discovery coverage is missing; run skills hook install");
@@ -14135,7 +14152,7 @@ function assertManagedAgentBridge(agent, options = {}) {
14135
14152
  throw new Error(`NATIVE_SKILL_DRIFT: ${error.message}`);
14136
14153
  }
14137
14154
  const inventory = inventoryNativeSkills(home, { includeVendor: true, guardHermes: agent === "hermes", agents: [agent], projectDirs: [...roots], agentRoots: discovery.roots.map((path) => ({ agent, path })), allowRootAliases: aliases.length > 0 });
14138
- const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledBuiltin(entry));
14155
+ const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledVendorSkill(entry));
14139
14156
  if (unexpected.length) {
14140
14157
  const paths = unexpected.slice(0, 3).map((entry) => {
14141
14158
  const escaped = JSON.stringify(entry.path).replace(/[\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
@@ -29,6 +29,7 @@ export interface NativeMigrationTarget {
29
29
  projectRoot: string;
30
30
  path: string;
31
31
  treeSha256: string;
32
+ vendor?: true;
32
33
  }
33
34
  export interface NativeMigrationTargetManifest {
34
35
  schema: "hasna.skills-native-migration-targets.v1";
@@ -84,6 +85,7 @@ export declare function inventoryNativeSkills(home?: string, options?: {
84
85
  configured?: boolean;
85
86
  discoveryInputs?: ReviewedDiscoveryInputs;
86
87
  allowRootAliases?: boolean;
88
+ reviewedCacheAlias?: string;
87
89
  }): NativeSkillEntry[];
88
90
  /** Planning is read-only; credentials and unrelated settings never appear in CLI output. */
89
91
  export declare function planAgentIntegration(options: {
@@ -30,7 +30,13 @@ export declare function inspectSkillSession(sessionId: string, options?: Selecti
30
30
  selectionCount: number;
31
31
  loadedCount: number;
32
32
  };
33
- /** Explicit opt-in only. Ordinary loads and hooks retain their immutable session pins. */
33
+ /**
34
+ * Managed hooks may move an expired pin only when every skill already loaded
35
+ * by this session is still selected with exactly the same behavior and bytes.
36
+ * The old receipt is archived and replaced under its exact snapshot lock.
37
+ */
38
+ export declare function reconcileSkillSessionIfSafe(sessionId: string, profileId: string, options?: SessionReconciliationOptions): Promise<boolean>;
39
+ /** Explicit review for changed loaded selections or intentional profile migration. */
34
40
  export declare function reconcileSkillSession(input: SessionReconciliationInput, options?: SessionReconciliationOptions): Promise<{
35
41
  applied: false;
36
42
  plan: {
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.2",
32376
+ version: "0.10.4",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -41180,8 +41180,8 @@ function selectNativeMigrationTargets(inventory, manifest) {
41180
41180
  if (matches.length !== 1)
41181
41181
  throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
41182
41182
  const entry = matches[0];
41183
- if (entry.bridge || entry.system || entry.vendor)
41184
- throw new Error(`Native migration target is protected or vendor-owned: ${entry.path}`);
41183
+ if (entry.bridge || entry.system || entry.vendor !== (target.vendor === true))
41184
+ throw new Error(`Native migration target is protected or vendor classification changed: ${entry.path}`);
41185
41185
  if (entry.hash !== target.treeSha256 || treeHash(entry.path) !== target.treeSha256)
41186
41186
  throw new Error(`Native migration target changed after review: ${entry.path}`);
41187
41187
  selected.push(entry);
@@ -41204,6 +41204,8 @@ function projectAncestorDirectories(projects) {
41204
41204
  return [...directories];
41205
41205
  }
41206
41206
  function inventoryNativeSkills(home = homedir3(), options = {}) {
41207
+ if (options.reviewedCacheAlias !== undefined && (!options.includeVendor || !isAbsolute15(options.reviewedCacheAlias) || resolve14(options.reviewedCacheAlias) !== options.reviewedCacheAlias))
41208
+ throw new Error("A reviewed cache alias requires vendor inventory and an exact absolute path");
41207
41209
  const aliases = rootAliases(home, options.allowRootAliases);
41208
41210
  const selectedAgents = options.agents ? new Set(options.agents) : undefined;
41209
41211
  const includesAgent = (agent) => !selectedAgents || selectedAgents.has(agent);
@@ -41224,14 +41226,22 @@ function inventoryNativeSkills(home = homedir3(), options = {}) {
41224
41226
  if (discoveryPathBytes > 4 * 1024 * 1024)
41225
41227
  throw new Error("Native skill discovery metadata limit exceeded");
41226
41228
  }
41227
- function emptySiblingCacheAlias(path, parent) {
41229
+ let reviewedCacheAliasFound = false;
41230
+ function verifiedSiblingCacheAlias(path, parent) {
41228
41231
  const link = readlinkSync3(path), target = resolve14(parent, link);
41229
41232
  if (dirname14(target) !== parent || (isAbsolute15(link) ? link !== target : dirname14(link) !== "."))
41230
41233
  return false;
41231
41234
  if (!lstatSync18(target, { throwIfNoEntry: false })?.isDirectory())
41232
41235
  return false;
41233
41236
  const scan = cacheScans.get(target);
41234
- return Boolean(scan?.complete && !scan.hasSkills && scan.entries <= maxAliasProofEntries);
41237
+ if (!scan?.complete || scan.entries > maxAliasProofEntries)
41238
+ return false;
41239
+ if (scan.hasSkills) {
41240
+ if (path !== options.reviewedCacheAlias)
41241
+ return false;
41242
+ reviewedCacheAliasFound = true;
41243
+ }
41244
+ return true;
41235
41245
  }
41236
41246
  function visit2(agent, path, vendor = false, depth = 0, pluginCache = false, admitted = false) {
41237
41247
  if (!admitted)
@@ -41288,7 +41298,7 @@ function inventoryNativeSkills(home = homedir3(), options = {}) {
41288
41298
  const child = join21(path, name);
41289
41299
  if (isVendor && lstatSync18(child, { throwIfNoEntry: false })?.isSymbolicLink()) {
41290
41300
  const target = statSync5(child, { throwIfNoEntry: false });
41291
- if (target?.isFile() || pluginCache && target?.isDirectory() && emptySiblingCacheAlias(child, path)) {
41301
+ if (target?.isFile() || pluginCache && target?.isDirectory() && verifiedSiblingCacheAlias(child, path)) {
41292
41302
  scan.entries++;
41293
41303
  continue;
41294
41304
  }
@@ -41322,6 +41332,8 @@ function inventoryNativeSkills(home = homedir3(), options = {}) {
41322
41332
  for (const root2 of [...options.agentRoots ?? [], ...configured])
41323
41333
  if (includesAgent(root2.agent))
41324
41334
  scanRoot(root2.agent, canonicalAgentPath(root2.path, aliases), true);
41335
+ if (options.reviewedCacheAlias !== undefined && !reviewedCacheAliasFound)
41336
+ throw new Error("Reviewed cache alias was not an inventoried skill-containing sibling alias");
41325
41337
  recheckRootAliases(aliases);
41326
41338
  return entries;
41327
41339
  }
@@ -41984,12 +41996,17 @@ function assertManagedAgentBridge(agent, options = {}) {
41984
41996
  const setting = (path) => (codexConfig.skills?.config ?? []).filter((entry) => typeof entry.path === "string" && [path, join21(path, "SKILL.md")].includes(canonicalAgentPath(entry.path, aliases)));
41985
41997
  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))
41986
41998
  throw new Error("NATIVE_SKILL_DRIFT: the Codex CLI bridge is disabled; run skills hook install");
41987
- const disabledBuiltin = (entry) => {
41988
- if (agent !== "codex" || !entry.vendor || !entry.path.startsWith(canonicalAgentPath(join21(home, ".codex", "skills", ".system"), aliases) + sep4))
41999
+ const disabledVendorSkill = (entry) => {
42000
+ if (agent !== "codex" || !entry.vendor)
41989
42001
  return false;
41990
- const matched = Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
41991
42002
  const settings = setting(entry.path);
41992
- return matched && settings.length === 1 && settings[0]?.enabled === false;
42003
+ if (settings.length !== 1 || settings[0]?.enabled !== false)
42004
+ return false;
42005
+ if (entry.path.startsWith(canonicalAgentPath(join21(home, ".codex", "plugins", "cache"), aliases) + sep4))
42006
+ return true;
42007
+ if (!entry.path.startsWith(canonicalAgentPath(join21(home, ".codex", "skills", ".system"), aliases) + sep4))
42008
+ return false;
42009
+ return Array.isArray(binding.disabledBuiltins) && binding.disabledBuiltins.some((item) => item.path === entry.path && item.hash === entry.hash);
41993
42010
  };
41994
42011
  if (!discovery || discovery.agent !== agent)
41995
42012
  throw new Error("NATIVE_SKILL_DRIFT: native discovery coverage is missing; run skills hook install");
@@ -42006,7 +42023,7 @@ function assertManagedAgentBridge(agent, options = {}) {
42006
42023
  throw new Error(`NATIVE_SKILL_DRIFT: ${error.message}`);
42007
42024
  }
42008
42025
  const inventory = inventoryNativeSkills(home, { includeVendor: true, guardHermes: agent === "hermes", agents: [agent], projectDirs: [...roots], agentRoots: discovery.roots.map((path) => ({ agent, path })), allowRootAliases: aliases.length > 0 });
42009
- const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledBuiltin(entry));
42026
+ const unexpected = inventory.filter((entry) => visible(entry) && !entry.bridge && !disabledVendorSkill(entry));
42010
42027
  if (unexpected.length) {
42011
42028
  const paths = unexpected.slice(0, 3).map((entry) => {
42012
42029
  const escaped = JSON.stringify(entry.path).replace(/[\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {