@hasna/skills 0.10.2 → 0.10.3

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.3",
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
  }
@@ -81557,6 +81569,7 @@ var init_skill_context = __esm(() => {
81557
81569
 
81558
81570
  // src/lib/session-reconciliation.ts
81559
81571
  import { createHash as createHash22 } from "crypto";
81572
+ import { isDeepStrictEqual as isDeepStrictEqual8 } from "util";
81560
81573
  function exactTimestamp(value) {
81561
81574
  const parsed = value === undefined ? Number.NaN : Date.parse(value);
81562
81575
  if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== value) {
@@ -81589,6 +81602,52 @@ function inspectSkillSession(sessionId, options = {}) {
81589
81602
  loadedCount: receipt.loaded.length
81590
81603
  };
81591
81604
  }
81605
+ async function reconcileSkillSessionIfSafe(sessionId, profileId, options = {}) {
81606
+ const snapshot2 = readSkillSessionSnapshotIfExists(sessionId, options);
81607
+ if (!snapshot2)
81608
+ return false;
81609
+ const old = snapshot2.receipt;
81610
+ if (old.profile.profileId !== profileId)
81611
+ throw new SkillSelectionError("PROFILE_LOCK_MISMATCH", "The session is pinned to a different Skills profile.");
81612
+ const now3 = (options.now ?? Date.now)();
81613
+ const age = now3 - Date.parse(old.verifiedAt);
81614
+ if (age >= 0 && age <= MAX_CACHED_PROFILE_AGE_MS)
81615
+ return false;
81616
+ if (age < 0)
81617
+ throw new SkillSelectionError("SESSION_RECONCILIATION_REQUIRED", "The session receipt has a future timestamp; inspect it before changing its pin.");
81618
+ const client2 = options.client ?? await createProfileClient();
81619
+ const target = structuredClone(await client2.resolveProfile(profileId));
81620
+ validateResolvedProfile(target, client2.authority);
81621
+ if (target.profileId !== profileId || target.authority !== old.profile.authority || target.workspaceId !== old.profile.workspaceId) {
81622
+ throw new SkillSelectionError("PROFILE_IDENTITY_MISMATCH", "The authenticated Skills profile does not match this session's profile, authority and workspace.");
81623
+ }
81624
+ const oldByKey = new Map(old.profile.selections.map((selection) => [selectionKey(selection), selection]));
81625
+ const targetBySlug = new Map(target.selections.map((selection) => [selection.slug, selection]));
81626
+ for (const key of old.loaded) {
81627
+ const previous = oldByKey.get(key);
81628
+ const current = targetBySlug.get(previous.slug);
81629
+ if (!current || !isDeepStrictEqual8({ ...previous, profileRevision: target.profileRevision }, current)) {
81630
+ throw new SkillSelectionError("SESSION_RECONCILIATION_REQUIRED", "A loaded Skills selection was removed or changed; review skills sessions reconcile before changing this session's pin.");
81631
+ }
81632
+ }
81633
+ const replacement = {
81634
+ ...old,
81635
+ generation: nextSkillSessionGeneration(snapshot2.generation),
81636
+ verifiedAt: new Date((options.now ?? Date.now)()).toISOString(),
81637
+ profile: target,
81638
+ loaded: [...old.loaded]
81639
+ };
81640
+ replaceSkillSession(skillSessionSnapshotBinding(snapshot2), replacement, {
81641
+ schemaVersion: 1,
81642
+ kind: "managed-hook-safe-reconciliation",
81643
+ sessionId,
81644
+ fromProfileRevision: old.profile.profileRevision,
81645
+ toProfileRevision: target.profileRevision,
81646
+ retainedLoadedCount: old.loaded.length,
81647
+ retiredLoadedCount: 0
81648
+ }, options);
81649
+ return true;
81650
+ }
81592
81651
  async function reconcileSkillSession(input2, options = {}) {
81593
81652
  if (![input2.fromProfile, input2.fromRevision, input2.selectionProfile, input2.profileRevision].every((value) => typeof value === "string" && value.trim()) || !/^[a-f0-9]{64}$/.test(input2.receiptSha256)) {
81594
81653
  throw new SkillSelectionError("INVALID_SESSION_RECONCILIATION", "Name the exact old receipt SHA256, old profile/revision and intended target profile/revision.");
@@ -81707,7 +81766,7 @@ Review digest: ${result2.reviewDigest}`);
81707
81766
  reportContextError(error61, options.json);
81708
81767
  }
81709
81768
  });
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) => {
81769
+ 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
81770
  try {
81712
81771
  const result2 = await reconcileSkillSession({ ...options, sessionId: id });
81713
81772
  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 +81788,27 @@ Preserved original receipt: ${result2.archivePath}` : `Planned one session recon
81729
81788
  reportContextError(error61, options.json);
81730
81789
  }
81731
81790
  });
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) => {
81791
+ 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
81792
  try {
81793
+ if (options.autoReconcileSafe && !options.cached)
81794
+ throw new SkillSelectionError("INVALID_CONTEXT_INPUT", "--auto-reconcile-safe requires --cached.");
81734
81795
  const input2 = options.stdin ? parseSkillContextInput(await readContextStdin()) : { prompt: prompt ?? "" };
81735
81796
  input2.profileId = selectedProfileId(options.selectionProfile ?? input2.profileId);
81736
81797
  input2.cwd ??= process.cwd();
81737
81798
  input2.sessionId = options.session ?? input2.sessionId;
81738
81799
  input2.restore ||= options.restore;
81739
- const result2 = await buildSkillContext(input2, {
81740
- ...contextResolverOptions(options),
81741
- maxChars: Number(options.maxChars),
81742
- maxSkills: Number(options.maxSkills)
81743
- });
81800
+ const resolverOptions = { ...contextResolverOptions(options), maxChars: Number(options.maxChars), maxSkills: Number(options.maxSkills) };
81801
+ let result2;
81802
+ try {
81803
+ result2 = await buildSkillContext(input2, resolverOptions);
81804
+ } catch (error61) {
81805
+ if (!options.cached || !options.autoReconcileSafe || !(error61 instanceof SkillSelectionError) || error61.code !== "CACHED_PROFILE_EXPIRED" || !input2.sessionId)
81806
+ throw error61;
81807
+ const sessionId = input2.agentId ? `${input2.sessionId}:${input2.agentId}` : input2.sessionId;
81808
+ if (!await reconcileSkillSessionIfSafe(sessionId, input2.profileId, resolverOptions))
81809
+ throw error61;
81810
+ result2 = await buildSkillContext(input2, resolverOptions);
81811
+ }
81744
81812
  await writeCliOutput(options.json ? JSON.stringify(result2) : result2.context);
81745
81813
  } catch (error61) {
81746
81814
  reportContextError(error61, options.json);
@@ -86158,7 +86226,7 @@ var init_execution_grants = __esm(() => {
86158
86226
  });
86159
86227
 
86160
86228
  // src/lib/execution-grant-client.ts
86161
- import { isDeepStrictEqual as isDeepStrictEqual8 } from "util";
86229
+ import { isDeepStrictEqual as isDeepStrictEqual9 } from "util";
86162
86230
  function id(value) {
86163
86231
  if (!grantIdentifier(value))
86164
86232
  throw new Error("Invalid execution grant profile or revision identifier.");
@@ -86210,7 +86278,7 @@ async function saveExecutionGrantPolicy(profileId, grants, revision) {
86210
86278
  headers: revision === undefined ? { "If-None-Match": "*" } : { "If-Match": `"${revision}"` },
86211
86279
  body: JSON.stringify({ grants: snapshot2 })
86212
86280
  }));
86213
- if (policy.profileId !== profileId || policy.previousRevision !== (revision ?? null) || !isDeepStrictEqual8(policy.grants, snapshot2)) {
86281
+ if (policy.profileId !== profileId || policy.previousRevision !== (revision ?? null) || !isDeepStrictEqual9(policy.grants, snapshot2)) {
86214
86282
  throw new Error("The API did not preserve the reviewed execution grant policy; read it before retrying.");
86215
86283
  }
86216
86284
  return policy;
@@ -86221,7 +86289,7 @@ async function resolveExecutionGrant(input2) {
86221
86289
  if (!grantRecord(value) || Object.keys(value).length !== 3 || !grantIdentifier(value.policyRevision) || !grantIdentifier(value.grantId) || !grantRecord(value.bindings)) {
86222
86290
  throw new SkillSelectionError("INVALID_EXECUTION_GRANT", "The Skills API returned an invalid execution grant.");
86223
86291
  }
86224
- if (!isDeepStrictEqual8(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual8(value.bindings.consumer, snapshot2.consumer)) {
86292
+ if (!isDeepStrictEqual9(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual9(value.bindings.consumer, snapshot2.consumer)) {
86225
86293
  throw new SkillSelectionError("EXECUTION_GRANT_SCOPE_MISMATCH", "The Skills API returned a grant for a different selection or consumer.");
86226
86294
  }
86227
86295
  return value;
@@ -109440,7 +109508,7 @@ var init_create_sync_config = __esm(() => {
109440
109508
  });
109441
109509
 
109442
109510
  // src/lib/codex-native-config.ts
109443
- import { isDeepStrictEqual as isDeepStrictEqual9 } from "util";
109511
+ import { isDeepStrictEqual as isDeepStrictEqual10 } from "util";
109444
109512
  function isRecord7(value) {
109445
109513
  if (!value || typeof value !== "object")
109446
109514
  return false;
@@ -109462,7 +109530,7 @@ function decimalIdentity(text4) {
109462
109530
  return `${match[1]}${coefficient}e${exponent}`;
109463
109531
  }
109464
109532
  function codexNativeConfigEqual(native, expected) {
109465
- if (isDeepStrictEqual9(native, expected))
109533
+ if (isDeepStrictEqual10(native, expected))
109466
109534
  return true;
109467
109535
  if (typeof expected === "number") {
109468
109536
  if (!Number.isFinite(expected) || !isRecord7(native))
@@ -109628,7 +109696,7 @@ var init_codex_hook_rpc = __esm(() => {
109628
109696
  // src/lib/codex-hook-trust-identity.ts
109629
109697
  import { realpathSync as realpathSync13 } from "fs";
109630
109698
  import { dirname as dirname31, isAbsolute as isAbsolute25, join as join57 } from "path";
109631
- import { isDeepStrictEqual as isDeepStrictEqual10 } from "util";
109699
+ import { isDeepStrictEqual as isDeepStrictEqual11 } from "util";
109632
109700
  function inspectRecordedSkillsCli(command, expected) {
109633
109701
  need4(isAbsolute25(command) && !command.includes("\x00"), "RECONCILE_RECORDED_COMMAND_REQUIRED");
109634
109702
  const resolved = realpathSync13(command);
@@ -109638,7 +109706,7 @@ function inspectRecordedSkillsCli(command, expected) {
109638
109706
  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
109707
  need4((cli.stat.mode & 0o100n) !== 0n, "SKILLS_COMMAND_NOT_EXECUTABLE");
109640
109708
  const receipt = { path: resolved, version: pkg.version, sha256: cli.sha256, manifestSha256: manifest.sha256 };
109641
- need4(isDeepStrictEqual10(receipt, expected), "RECONCILE_SKILLS_BINDING_CHANGED");
109709
+ need4(isDeepStrictEqual11(receipt, expected), "RECONCILE_SKILLS_BINDING_CHANGED");
109642
109710
  return { receipt, recheck() {
109643
109711
  need4(realpathSync13(command) === resolved, "SKILLS_COMMAND_CHANGED");
109644
109712
  unchanged2(cli);
@@ -109674,19 +109742,19 @@ var init_codex_hook_trust_identity = __esm(() => {
109674
109742
  });
109675
109743
 
109676
109744
  // src/lib/codex-hook-policy-recovery.ts
109677
- import { isDeepStrictEqual as isDeepStrictEqual11 } from "util";
109745
+ import { isDeepStrictEqual as isDeepStrictEqual12 } from "util";
109678
109746
  function reviewedClaudeDiscoveryChange(beforeText, currentText, options) {
109679
109747
  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
109748
  need4(options.reviewedPlanDigest === undefined || /^[a-f0-9]{64}$/.test(options.reviewedPlanDigest), "RECONCILE_RECOVERY_PLAN_CHANGED");
109681
109749
  const before2 = parseManagedSkillPolicy(beforeText), current = parseManagedSkillPolicy(currentText);
109682
109750
  const oldDiscovery = before2.bridge?.discovery?.claude, newDiscovery = current.bridge?.discovery?.claude;
109683
109751
  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] }));
109752
+ 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
109753
  need4(changes.length > 0, "RECONCILE_POLICY_CHANGED");
109686
109754
  const compared = structuredClone(current);
109687
109755
  compared.bridge.discovery.claude.roots = oldDiscovery.roots;
109688
109756
  compared.bridge.discovery.claude.sources = oldDiscovery.sources;
109689
- need4(isDeepStrictEqual11(before2, compared), "RECONCILE_POLICY_CHANGED");
109757
+ need4(isDeepStrictEqual12(before2, compared), "RECONCILE_POLICY_CHANGED");
109690
109758
  verifyAgentDiscovery(newDiscovery);
109691
109759
  return { reason: options.reason, changes };
109692
109760
  }
@@ -109702,7 +109770,7 @@ import { createHash as createHash28, randomUUID as randomUUID10 } from "crypto";
109702
109770
  import { execFileSync } from "child_process";
109703
109771
  import { dirname as dirname32, isAbsolute as isAbsolute26, join as join58, resolve as resolve23 } from "path";
109704
109772
  import { homedir as homedir12 } from "os";
109705
- import { isDeepStrictEqual as isDeepStrictEqual12 } from "util";
109773
+ import { isDeepStrictEqual as isDeepStrictEqual13 } from "util";
109706
109774
  function verifyReleaseArtifact(proof, skillsCli) {
109707
109775
  need4(isAbsolute26(proof.packageTarPath) && !proof.packageTarPath.includes("\x00"), "RECONCILE_RELEASE_PROVENANCE_PATH");
109708
109776
  const tarPath = realpathSync14(proof.packageTarPath);
@@ -109744,7 +109812,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109744
109812
  const configSha256 = intent.configSha256 ?? configBefore.sha256, configVersion = intent.configVersion ?? intent.expectedVersion;
109745
109813
  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
109814
  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");
109815
+ need4(isDeepStrictEqual13(intent.hooks, admitted.filter((hook) => !hook.enabled || hook.trustStatus !== "trusted")), "RECONCILE_INTENT_HOOKS_CHANGED");
109748
109816
  need4(configBefore.sha256 === (intent.beforeSha256 ?? intent.configSha256) && hooksBefore.sha256 === intent.hooksSha256 && policyBefore.sha256 === intent.policySha256, "RECONCILE_JOURNAL_CHANGED");
109749
109817
  const policyCurrent = snapshot(join58(dataDir3, "agent-policy.json"), true);
109750
109818
  const { claudeDiscoveryRecovery: claudeRecovery, nativeHookAdditionsRecovery: additionsRecovery } = options;
@@ -109757,7 +109825,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109757
109825
  if (policyCurrent.sha256 !== policyBefore.sha256 && !claudeRecovery) {
109758
109826
  need4(options.supersedeBinding === true, "RECONCILE_POLICY_CHANGED");
109759
109827
  const oldPolicy = JSON.parse(policyBefore.text), currentPolicy = JSON.parse(policyCurrent.text);
109760
- need4(isDeepStrictEqual12(withoutCodexCommand(oldPolicy), withoutCodexCommand(currentPolicy)), "RECONCILE_POLICY_CHANGED");
109828
+ need4(isDeepStrictEqual13(withoutCodexCommand(oldPolicy), withoutCodexCommand(currentPolicy)), "RECONCILE_POLICY_CHANGED");
109761
109829
  supersededBinding = true;
109762
109830
  }
109763
109831
  if (supersededBinding || recovery) {
@@ -109793,7 +109861,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109793
109861
  need4(state?.enabled === true && typeof state.trusted_hash === "string" && /^sha256:[a-f0-9]{64}$/.test(state.trusted_hash), "RECONCILE_CONFIG_DRIFT");
109794
109862
  }
109795
109863
  } else
109796
- need4(isDeepStrictEqual12(expectedConfig, current), "RECONCILE_CONFIG_DRIFT");
109864
+ need4(isDeepStrictEqual13(expectedConfig, current), "RECONCILE_CONFIG_DRIFT");
109797
109865
  const configDiscoveryRecovery = recovery && binding.discovery?.codex ? createCodexHookDiscoveryRecovery(binding.discovery.codex, configBefore, currentConfig, intent.hooks, additions?.trustProof) : undefined;
109798
109866
  assertManagedAgentBridge("codex", { home, dataDir: dataDir3, projectDir: home, codexDiscoveryRecovery: configDiscoveryRecovery });
109799
109867
  const recoveryCli = recovery ? bindSkillsCli("skills", options.reviewedSkillsCli) : undefined;
@@ -109802,14 +109870,14 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109802
109870
  const skillsCli = recovery ? inspectRecordedSkillsCli(binding.commands.codex, intent.skillsCli) : bindSkillsCli(binding.commands.codex, options.reviewedSkillsCli);
109803
109871
  if (supersededBinding)
109804
109872
  verifyReleaseArtifact(options.releaseProof, skillsCli);
109805
- const skillsBindingChanged = !isDeepStrictEqual12(skillsCli.receipt, intent.skillsCli);
109873
+ const skillsBindingChanged = !isDeepStrictEqual13(skillsCli.receipt, intent.skillsCli);
109806
109874
  need4(!skillsBindingChanged || supersededBinding, "RECONCILE_SKILLS_BINDING_CHANGED");
109807
109875
  if (!additions && hooksCurrent.sha256 !== intent.hooksSha256 && hooksCurrent.sha256 !== hooksBefore.sha256) {
109808
109876
  need4(supersededBinding, "RECONCILE_HOOKS_CHANGED");
109809
109877
  const currentDeclarations = JSON.parse(hooksCurrent.text).hooks;
109810
109878
  for (const [event, [nativeEvent]] of Object.entries(eventNames)) {
109811
109879
  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");
109880
+ need4(isDeepStrictEqual13(currentDeclarations?.[event], [{ hooks: [{ type: "command", command, timeout: 15 }] }]), "RECONCILE_BINDING_HOOK_CHANGED");
109813
109881
  }
109814
109882
  }
109815
109883
  rpc = await connect({ command: options.codexCommand ?? "codex", home, codexHome });
@@ -109832,9 +109900,9 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109832
109900
  delete identity4.command;
109833
109901
  return identity4;
109834
109902
  };
109835
- need4(isDeepStrictEqual12(stable(actual), stable(expected)), "RECONCILE_NATIVE_IDENTITY_CHANGED");
109903
+ need4(isDeepStrictEqual13(stable(actual), stable(expected)), "RECONCILE_NATIVE_IDENTITY_CHANGED");
109836
109904
  if (!plannedKeys.has(expected.key))
109837
- need4(isDeepStrictEqual12(actual, expected), "RECONCILE_UNRELATED_HOOK_CHANGED");
109905
+ need4(isDeepStrictEqual13(actual, expected), "RECONCILE_UNRELATED_HOOK_CHANGED");
109838
109906
  }
109839
109907
  for (const wanted of intent.hooks) {
109840
109908
  const matches2 = nativeHooks.filter((h2) => h2.key === wanted.key || h2.key === wanted.key.replace(join58(home, ".codex/hooks.json"), hooksPath));
@@ -109853,7 +109921,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
109853
109921
  const layers = config2?.layers?.filter((layer) => layer.name?.type === "user" && layer.name.file === nativeConfigPath && !layer.name.profile);
109854
109922
  need4(layers?.length === 1 && !layers[0].disabledReason && codexNativeConfigEqual(layers[0].config, current), "RECONCILE_NATIVE_CONFIG_MISMATCH");
109855
109923
  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");
109924
+ 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
109925
  const finalConfig = await rpc.request("config/read", { includeLayers: true, cwd: home });
109858
109926
  const finalLayers = finalConfig?.layers?.filter((layer) => layer.name?.type === "user" && layer.name.file === nativeConfigPath && !layer.name.profile);
109859
109927
  need4(finalLayers?.length === 1 && !finalLayers[0].disabledReason && codexNativeConfigEqual(finalLayers[0].config, current), "RECONCILE_NATIVE_CONFIG_CHANGED");
@@ -109914,7 +109982,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
109914
109982
  const groups = declarations[event];
109915
109983
  need4(Array.isArray(groups), "DECLARATION_CHANGED");
109916
109984
  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");
109985
+ need4(matches2.length === 1 && isDeepStrictEqual13(matches2[0].group, { hooks: [{ type: "command", command, timeout: 15 }] }), "DECLARATION_CHANGED");
109918
109986
  expected.push({ event: nativeEvent, key: `${join58(home, ".codex/hooks.json")}:${label}:${matches2[0].gi}:${matches2[0].hi}`, command });
109919
109987
  }
109920
109988
  codexTrustTextWitness(configFile.text, expected.map((h) => h.key));
@@ -109980,7 +110048,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
109980
110048
  save(join58(journal, "hooks.before.json"), hooksFile.bytes);
109981
110049
  save(join58(journal, "policy.before.json"), policyFile.bytes);
109982
110050
  save(join58(journal, "intent.json"), json2({ ...digestInput, planDigest, hooks: planned, nativeHooks: discovered.map(safeNativeHook) }));
109983
- need4(isDeepStrictEqual12(admit(await list2()), admitted), "NATIVE_DISCOVERY_CHANGED");
110051
+ need4(isDeepStrictEqual13(admit(await list2()), admitted), "NATIVE_DISCOVERY_CHANGED");
109984
110052
  checkInputs();
109985
110053
  let result2;
109986
110054
  try {
@@ -109994,7 +110062,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
109994
110062
  expectedConfig.hooks.state ??= {};
109995
110063
  for (const h of planned)
109996
110064
  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");
110065
+ need4(isDeepStrictEqual13(structure3(after2.text), expectedConfig) && codexTrustTextWitness(after2.text, keys3) === preservationText, "PRESERVATION_FAILED");
109998
110066
  const savedConfig = snapshot(join58(journal, "config.before.toml"), true);
109999
110067
  need4(savedConfig.sha256 === configFile.sha256, "JOURNAL_CHANGED");
110000
110068
  const discoveryTransition = binding.discovery?.codex ? createCodexHookDiscoveryRecovery(binding.discovery.codex, savedConfig, after2, planned) : undefined;
@@ -110002,8 +110070,8 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
110002
110070
  unchanged2(hooksFile);
110003
110071
  assertManagedAgentBridge("codex", { home, dataDir: dataDir3, projectDir: home, codexDiscoveryRecovery: discoveryTransition });
110004
110072
  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");
110073
+ 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");
110074
+ need4(isDeepStrictEqual13(current.filter((h) => !keys3.includes(h.key)), discovered.filter((h) => !keys3.includes(h.key))), "UNRELATED_HOOK_CHANGED");
110007
110075
  unchanged2(after2);
110008
110076
  unchanged2(savedConfig);
110009
110077
  unchanged2(hooksFile);
@@ -110070,6 +110138,9 @@ function hookFailureReason(error61, profileId) {
110070
110138
  if (code2 === "PROFILE_LOCK_MISMATCH") {
110071
110139
  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
110140
  }
110141
+ if (code2 === "SESSION_RECONCILIATION_REQUIRED") {
110142
+ 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.`;
110143
+ }
110073
110144
  if (code2.startsWith("SESSION_")) {
110074
110145
  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
110146
  }
@@ -110112,6 +110183,7 @@ var init_hook_diagnostics = __esm(() => {
110112
110183
  "SESSION_GENERATION_EXHAUSTED",
110113
110184
  "SESSION_PARENT_CHANGED",
110114
110185
  "SESSION_RECEIPT_CHANGED",
110186
+ "SESSION_RECONCILIATION_REQUIRED",
110115
110187
  "SESSION_RECONCILIATION_INCOMPLETE",
110116
110188
  "SESSION_WRITE_LOCKED",
110117
110189
  "SESSION_WRITE_LOCK_CHANGED",
@@ -110156,7 +110228,7 @@ async function contextForHook(input3, profileId, cached2, deadline) {
110156
110228
  throw new HookDiagnosticError("SKILLS_HOOK_TIMEOUT", "context");
110157
110229
  const args2 = [process.execPath, process.argv[1], "context", "--stdin", "--json", "--selection-profile", profileId];
110158
110230
  if (cached2)
110159
- args2.push("--cached");
110231
+ args2.push("--cached", "--auto-reconcile-safe");
110160
110232
  const child = Bun.spawn(args2, { stdin: new Blob([JSON.stringify(input3)]), stdout: "pipe", stderr: "pipe", env: { ...process.env, NO_COLOR: "1" } });
110161
110233
  let timedOut = false;
110162
110234
  const timer = setTimeout(() => {
@@ -110363,13 +110435,18 @@ function registerAgentIntegration(parent) {
110363
110435
  }
110364
110436
  });
110365
110437
  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) => {
110438
+ 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
110439
  try {
110440
+ if (options.agent && !INTEGRATION_AGENTS.includes(options.agent))
110441
+ throw new Error(`Unsupported migration agent: ${options.agent}`);
110368
110442
  const discoveryInputs = options.discoveryInputs ? JSON.parse(readFileSync40(options.discoveryInputs, "utf8")) : undefined;
110369
110443
  if (options.targetManifest && (options.includeUnmanaged || options.includeVendor))
110370
110444
  throw new Error("--target-manifest cannot be combined with broad native migration selectors");
110371
110445
  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 });
110446
+ if (options.apply && options.reviewedCacheAlias && !targetManifest)
110447
+ throw new Error("Applying a reviewed cache alias requires an exact --target-manifest");
110448
+ const includeVendor = options.includeVendor || Boolean(targetManifest?.targets.some((target) => target.vendor));
110449
+ 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
110450
  if (targetManifest)
110374
110451
  selectNativeMigrationTargets(inventory, targetManifest);
110375
110452
  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.3",
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.3",
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.3",
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.3",
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.3",
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
  }
@@ -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.3",
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {