@hasna/skills 0.9.15 → 0.9.16

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
@@ -373,6 +373,13 @@ system configuration, process-specific overrides, and alternate agent home
373
373
  directories are outside automatic coverage and require their own integration
374
374
  review before declaring a station migrated.
375
375
 
376
+ When `hook install` omits `--discovery-inputs`, it reuses an existing reviewed
377
+ binding only after rechecking its sources, directory membership, configuration
378
+ coverage, managed bridge and native trust. Unchanged reviews need no new input
379
+ file; drift still requires a fresh explicit review. Explicit discovery inputs
380
+ take precedence, including when their `agents` array omits a previously reviewed
381
+ agent. The default `--agent all` still includes every supported adapter.
382
+
376
383
  For reviewed Claude plugins, `skills integration plugin plan`, `admit` and
377
384
  `resolve` provide a command-source admission boundary before native discovery.
378
385
  Original packages and migrated skill/command prompts remain in private versioned
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.9.15",
36871
+ version: "0.9.16",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -51146,7 +51146,15 @@ function planAgentIntegration(options) {
51146
51146
  validateProfile(profileId2);
51147
51147
  return [agent, { command, profileId: profileId2 }];
51148
51148
  }));
51149
- const discoveries = [...new Set(options.agents)].map((agent) => resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, canonical: (path) => canonicalAgentPath(path, aliases) }));
51149
+ const retainedAgents = [];
51150
+ const discoveries = [...new Set(options.agents)].map((agent) => {
51151
+ const retainedReview = options.discoveryInputs === undefined && policy.bridge?.discovery?.[agent]?.method === "reviewed" ? policy.bridge.discovery[agent] : undefined;
51152
+ if (retainedReview) {
51153
+ assertManagedAgentBridge(agent, { home, dataDir: dataDir3, projectDir: options.projectDir ?? home });
51154
+ retainedAgents.push(agent);
51155
+ }
51156
+ return resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, retainedReview, canonical: (path) => canonicalAgentPath(path, aliases) });
51157
+ });
51150
51158
  const nativeSkills = inventoryNativeSkills(home, { includeVendor: true, guardHermes: options.agents.includes("hermes"), agents: options.agents, projectDir: options.projectDir, agentRoots: discoveries.flatMap((binding) => binding.roots.map((path) => ({ agent: binding.agent, path }))), allowRootAliases: options.allowRootAliases });
51151
51159
  const changes = [];
51152
51160
  for (const agent of [...new Set(options.agents)]) {
@@ -51231,7 +51239,7 @@ function planAgentIntegration(options) {
51231
51239
  if (JSON.stringify(policy) !== JSON.stringify(nextPolicy))
51232
51240
  changes.push({ path: policyPath, before: previousPolicy, after: serializedPolicy });
51233
51241
  recheckRootAliases(aliases);
51234
- return { dataDir: dataDir3, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {} };
51242
+ return { dataDir: dataDir3, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {}, ...retainedAgents.length ? { retainedReviewChecks: { home, projectDir: options.projectDir ?? home, agents: retainedAgents } } : {} };
51235
51243
  }
51236
51244
  function verifyCoordinatedDiscovery(binding, home, aliases) {
51237
51245
  try {
@@ -51269,6 +51277,8 @@ function applyAgentIntegration(plan) {
51269
51277
  parseManagedSkillPolicy(change.after);
51270
51278
  const aliases = plan.rootAliases ?? [];
51271
51279
  recheckRootAliases(aliases);
51280
+ for (const agent of plan.retainedReviewChecks?.agents ?? [])
51281
+ assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
51272
51282
  for (const agent of plan.managedAgentChecks?.agents ?? [])
51273
51283
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
51274
51284
  for (const binding of plan.discoveryBefore ?? []) {
@@ -54741,7 +54751,7 @@ function reviewNativeHookAdditions(options) {
54741
54751
  need4(Array.isArray(groups), "RECONCILE_ADDITIONS_DECLARATIONS");
54742
54752
  const key = `${options.nativeHooksPath}:${matchedEvents[event][1]}:${groups.length}:0`;
54743
54753
  need4(hook.key === key && !seen.has(key) && !options.recordedNativeHooks.some((originalHook) => originalHook.key === key) && !Object.hasOwn(expectedConfig.hooks.state, key), "RECONCILE_ADDITIONS_IDENTITY");
54744
- need4(hook.eventName === matchedEvents[event][0] && hook.handlerType === "command" && text2(hook.command, 16384) && !commands.has(hook.command) && Number.isInteger(hook.timeoutSec) && hook.timeoutSec > 0 && hook.timeoutSec <= 60 && (event === "PreToolUse" ? text2(hook.matcher) : hook.matcher === null) && hook.async === false && hook.statusMessage === null && hook.additionalContextLimit === null && hook.sourcePath === options.nativeHooksPath && hook.source === "user" && hook.pluginId === null && hook.isManaged === false && /^sha256:[a-f0-9]{64}$/.test(hook.currentHash) && hook.enabled === true && hook.trustStatus === "trusted", "RECONCILE_ADDITIONS_IDENTITY");
54754
+ need4(hook.eventName === matchedEvents[event][0] && hook.handlerType === "command" && (review.version === 2 ? v2Command(hook.command) : text2(hook.command, 16384)) && !commands.has(hook.command) && Number.isInteger(hook.timeoutSec) && hook.timeoutSec > 0 && hook.timeoutSec <= 60 && (event === "PreToolUse" ? text2(hook.matcher) : hook.matcher === null) && hook.async === false && hook.statusMessage === null && hook.additionalContextLimit === null && hook.sourcePath === options.nativeHooksPath && hook.source === "user" && hook.pluginId === null && hook.isManaged === false && /^sha256:[a-f0-9]{64}$/.test(hook.currentHash) && hook.enabled === true && hook.trustStatus === "trusted", "RECONCILE_ADDITIONS_IDENTITY");
54745
54755
  let trustState = { enabled: true, trusted_hash: hook.currentHash };
54746
54756
  if (review.version === 2) {
54747
54757
  const state = addition.trustState;
@@ -54769,7 +54779,7 @@ function reviewNativeHookAdditions(options) {
54769
54779
  proofs.set(trustProof, { before: options.configBefore, current: options.configCurrent, journalHooks: options.journalHooks.map(({ key, currentHash }) => ({ key, currentHash })), states: Object.freeze(states), recheck });
54770
54780
  return { hooks: added, recheck, trustProof, receipt: { ...review.version === 2 ? { version: 2 } : {}, reviewPath: reviewFile.file, reviewSha256: reviewFile.sha256, reason: review.reason, producerEvidence: review.producerEvidence, producerBefore: { configPath: producerConfig.file, configSha256: producerConfig.sha256, hooksPath: producerHooks.file, hooksSha256: producerHooks.sha256 }, additions: review.version === 1 ? added : review.additions } };
54771
54781
  }
54772
- var events, matchedEvents, hash2 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value), text2 = (value, max2 = 2048) => typeof value === "string" && value.trim().length > 0 && value.length <= max2 && !/[\0\r\n]/.test(value), structure = (value) => JSON.parse(JSON.stringify(Bun.TOML.parse(value))), nativeKeys, proofs;
54782
+ var events, matchedEvents, hash2 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value), text2 = (value, max2 = 2048) => typeof value === "string" && value.trim().length > 0 && value.length <= max2 && !/[\0\r\n]/.test(value), v2Command = (value) => typeof value === "string" && value.trim().length > 0 && value.length <= 16384 && !/[\0\r]/.test(value), structure = (value) => JSON.parse(JSON.stringify(Bun.TOML.parse(value))), nativeKeys, proofs;
54773
54783
  var init_codex_hook_additions_review = __esm(() => {
54774
54784
  init_codex_hook_trust_files();
54775
54785
  init_codex_hook_trust_layout();
@@ -54843,6 +54853,7 @@ var init_codex_hook_discovery_recovery = __esm(() => {
54843
54853
  import { createHash as createHash16 } from "crypto";
54844
54854
  import { lstatSync as lstatSync20, readFileSync as readFileSync17, readdirSync as readdirSync10, realpathSync as realpathSync4 } from "fs";
54845
54855
  import { basename as basename8, dirname as dirname16, isAbsolute as isAbsolute15, join as join31, resolve as resolve15, sep as sep5 } from "path";
54856
+ import { isDeepStrictEqual as isDeepStrictEqual7 } from "util";
54846
54857
  function parseConfig(text3, path, toml = false) {
54847
54858
  try {
54848
54859
  const value = toml ? Bun.TOML.parse(text3) : JSON.parse(text3);
@@ -54952,7 +54963,10 @@ function rebindAgentDiscovery(binding, changes) {
54952
54963
  const budget = discoveryByteBudget();
54953
54964
  return { ...binding, sources: binding.sources.map((source) => {
54954
54965
  assertMarketplaceBinding(binding, source);
54955
- return { ...source, sha256: projected(source, changes, budget) };
54966
+ const sha2562 = projected(source, changes, budget);
54967
+ if (!changes.has(source.path) && sha2562 !== source.sha256)
54968
+ throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
54969
+ return { ...source, sha256: sha2562 };
54956
54970
  }) };
54957
54971
  }
54958
54972
  function assertProjectDiscovery(agent, directories, home, canonical4 = resolve15, reviewed) {
@@ -55064,6 +55078,19 @@ function resolveAgentDiscovery(options) {
55064
55078
  }
55065
55079
  }
55066
55080
  }
55081
+ if (options.reviewed === undefined && options.retainedReview !== undefined) {
55082
+ const retained = options.retainedReview;
55083
+ if (retained.agent !== agent || retained.method !== "reviewed")
55084
+ unresolved("invalid retained review");
55085
+ verifyAgentDiscovery(retained);
55086
+ if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
55087
+ unresolved("retained review is missing the full agent configuration source");
55088
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
55089
+ unresolved("retained settings witness names another configuration source");
55090
+ if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual7(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
55091
+ unresolved("retained review is missing current runtime discovery coverage");
55092
+ return retained;
55093
+ }
55067
55094
  if (options.reviewed && (options.reviewed.version !== 1 || !Array.isArray(options.reviewed.agents) || options.reviewed.agents.some((item) => !item || typeof item !== "object") || new Set(options.reviewed.agents.map((item) => item.agent)).size !== options.reviewed.agents.length))
55068
55095
  throw new Error("Invalid --discovery-inputs version or agents");
55069
55096
  const review = options.reviewed?.agents.find((item) => item.agent === agent);
@@ -84538,7 +84565,7 @@ var init_execution_grants = __esm(() => {
84538
84565
  });
84539
84566
 
84540
84567
  // src/lib/execution-grant-client.ts
84541
- import { isDeepStrictEqual as isDeepStrictEqual7 } from "util";
84568
+ import { isDeepStrictEqual as isDeepStrictEqual8 } from "util";
84542
84569
  function id(value) {
84543
84570
  if (!grantIdentifier(value))
84544
84571
  throw new Error("Invalid execution grant profile or revision identifier.");
@@ -84590,7 +84617,7 @@ async function saveExecutionGrantPolicy(profileId, grants, revision) {
84590
84617
  headers: revision === undefined ? { "If-None-Match": "*" } : { "If-Match": `"${revision}"` },
84591
84618
  body: JSON.stringify({ grants: snapshot2 })
84592
84619
  }));
84593
- if (policy.profileId !== profileId || policy.previousRevision !== (revision ?? null) || !isDeepStrictEqual7(policy.grants, snapshot2)) {
84620
+ if (policy.profileId !== profileId || policy.previousRevision !== (revision ?? null) || !isDeepStrictEqual8(policy.grants, snapshot2)) {
84594
84621
  throw new Error("The API did not preserve the reviewed execution grant policy; read it before retrying.");
84595
84622
  }
84596
84623
  return policy;
@@ -84601,7 +84628,7 @@ async function resolveExecutionGrant(input2) {
84601
84628
  if (!grantRecord(value) || Object.keys(value).length !== 3 || !grantIdentifier(value.policyRevision) || !grantIdentifier(value.grantId) || !grantRecord(value.bindings)) {
84602
84629
  throw new SkillSelectionError("INVALID_EXECUTION_GRANT", "The Skills API returned an invalid execution grant.");
84603
84630
  }
84604
- if (!isDeepStrictEqual7(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual7(value.bindings.consumer, snapshot2.consumer)) {
84631
+ if (!isDeepStrictEqual8(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual8(value.bindings.consumer, snapshot2.consumer)) {
84605
84632
  throw new SkillSelectionError("EXECUTION_GRANT_SCOPE_MISMATCH", "The Skills API returned a grant for a different selection or consumer.");
84606
84633
  }
84607
84634
  return value;
@@ -108595,7 +108622,7 @@ var init_codex_hook_rpc = __esm(() => {
108595
108622
  // src/lib/codex-hook-trust-identity.ts
108596
108623
  import { realpathSync as realpathSync13 } from "fs";
108597
108624
  import { dirname as dirname30, isAbsolute as isAbsolute25, join as join56 } from "path";
108598
- import { isDeepStrictEqual as isDeepStrictEqual8 } from "util";
108625
+ import { isDeepStrictEqual as isDeepStrictEqual9 } from "util";
108599
108626
  function inspectRecordedSkillsCli(command, expected) {
108600
108627
  need4(isAbsolute25(command) && !command.includes("\x00"), "RECONCILE_RECORDED_COMMAND_REQUIRED");
108601
108628
  const resolved = realpathSync13(command);
@@ -108605,7 +108632,7 @@ function inspectRecordedSkillsCli(command, expected) {
108605
108632
  need4(pkg.name === "@hasna/skills" && /^\d+\.\d+\.\d+$/.test(pkg.version) && pkg.bin?.skills === "bin/index.js" && resolved === join56(dirname30(manifest.file), "bin/index.js"), "SKILLS_PACKAGE_MISMATCH");
108606
108633
  need4((cli.stat.mode & 0o100n) !== 0n, "SKILLS_COMMAND_NOT_EXECUTABLE");
108607
108634
  const receipt = { path: resolved, version: pkg.version, sha256: cli.sha256, manifestSha256: manifest.sha256 };
108608
- need4(isDeepStrictEqual8(receipt, expected), "RECONCILE_SKILLS_BINDING_CHANGED");
108635
+ need4(isDeepStrictEqual9(receipt, expected), "RECONCILE_SKILLS_BINDING_CHANGED");
108609
108636
  return { receipt, recheck() {
108610
108637
  need4(realpathSync13(command) === resolved, "SKILLS_COMMAND_CHANGED");
108611
108638
  unchanged2(cli);
@@ -108641,19 +108668,19 @@ var init_codex_hook_trust_identity = __esm(() => {
108641
108668
  });
108642
108669
 
108643
108670
  // src/lib/codex-hook-policy-recovery.ts
108644
- import { isDeepStrictEqual as isDeepStrictEqual9 } from "util";
108671
+ import { isDeepStrictEqual as isDeepStrictEqual10 } from "util";
108645
108672
  function reviewedClaudeDiscoveryChange(beforeText, currentText, options) {
108646
108673
  need4(typeof options.reason === "string" && options.reason.trim().length > 0 && options.reason.length <= 512 && !/[\x00-\x1f\x7f]/.test(options.reason), "RECONCILE_RECOVERY_REASON_REQUIRED");
108647
108674
  need4(options.reviewedPlanDigest === undefined || /^[a-f0-9]{64}$/.test(options.reviewedPlanDigest), "RECONCILE_RECOVERY_PLAN_CHANGED");
108648
108675
  const before2 = parseManagedSkillPolicy(beforeText), current = parseManagedSkillPolicy(currentText);
108649
108676
  const oldDiscovery = before2.bridge?.discovery?.claude, newDiscovery = current.bridge?.discovery?.claude;
108650
108677
  need4(oldDiscovery?.agent === "claude" && newDiscovery?.agent === "claude" && oldDiscovery.method === "reviewed" && newDiscovery.method === "reviewed", "RECONCILE_POLICY_CHANGED");
108651
- const changes = ["roots", "sources"].filter((key) => !isDeepStrictEqual9(oldDiscovery[key], newDiscovery[key])).map((key) => ({ path: `bridge.discovery.claude.${key}`, before: oldDiscovery[key], after: newDiscovery[key] }));
108678
+ const changes = ["roots", "sources"].filter((key) => !isDeepStrictEqual10(oldDiscovery[key], newDiscovery[key])).map((key) => ({ path: `bridge.discovery.claude.${key}`, before: oldDiscovery[key], after: newDiscovery[key] }));
108652
108679
  need4(changes.length > 0, "RECONCILE_POLICY_CHANGED");
108653
108680
  const compared = structuredClone(current);
108654
108681
  compared.bridge.discovery.claude.roots = oldDiscovery.roots;
108655
108682
  compared.bridge.discovery.claude.sources = oldDiscovery.sources;
108656
- need4(isDeepStrictEqual9(before2, compared), "RECONCILE_POLICY_CHANGED");
108683
+ need4(isDeepStrictEqual10(before2, compared), "RECONCILE_POLICY_CHANGED");
108657
108684
  verifyAgentDiscovery(newDiscovery);
108658
108685
  return { reason: options.reason, changes };
108659
108686
  }
@@ -108669,7 +108696,7 @@ import { createHash as createHash27, randomUUID as randomUUID10 } from "crypto";
108669
108696
  import { execFileSync } from "child_process";
108670
108697
  import { dirname as dirname31, isAbsolute as isAbsolute26, join as join57, resolve as resolve23 } from "path";
108671
108698
  import { homedir as homedir12 } from "os";
108672
- import { isDeepStrictEqual as isDeepStrictEqual10 } from "util";
108699
+ import { isDeepStrictEqual as isDeepStrictEqual11 } from "util";
108673
108700
  function verifyReleaseArtifact(proof, skillsCli) {
108674
108701
  need4(isAbsolute26(proof.packageTarPath) && !proof.packageTarPath.includes("\x00"), "RECONCILE_RELEASE_PROVENANCE_PATH");
108675
108702
  const tarPath = realpathSync14(proof.packageTarPath);
@@ -108711,7 +108738,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108711
108738
  const configSha256 = intent.configSha256 ?? configBefore.sha256, configVersion = intent.configVersion ?? intent.expectedVersion;
108712
108739
  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 };
108713
108740
  need4(makePlanDigest(digestInput) === intent.planDigest, "RECONCILE_PLAN_CHANGED");
108714
- need4(isDeepStrictEqual10(intent.hooks, admitted.filter((hook) => !hook.enabled || hook.trustStatus !== "trusted")), "RECONCILE_INTENT_HOOKS_CHANGED");
108741
+ need4(isDeepStrictEqual11(intent.hooks, admitted.filter((hook) => !hook.enabled || hook.trustStatus !== "trusted")), "RECONCILE_INTENT_HOOKS_CHANGED");
108715
108742
  need4(configBefore.sha256 === (intent.beforeSha256 ?? intent.configSha256) && hooksBefore.sha256 === intent.hooksSha256 && policyBefore.sha256 === intent.policySha256, "RECONCILE_JOURNAL_CHANGED");
108716
108743
  const policyCurrent = snapshot(join57(dataDir3, "agent-policy.json"), true);
108717
108744
  const { claudeDiscoveryRecovery: claudeRecovery, nativeHookAdditionsRecovery: additionsRecovery } = options;
@@ -108724,7 +108751,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108724
108751
  if (policyCurrent.sha256 !== policyBefore.sha256 && !claudeRecovery) {
108725
108752
  need4(options.supersedeBinding === true, "RECONCILE_POLICY_CHANGED");
108726
108753
  const oldPolicy = JSON.parse(policyBefore.text), currentPolicy = JSON.parse(policyCurrent.text);
108727
- need4(isDeepStrictEqual10(withoutCodexCommand(oldPolicy), withoutCodexCommand(currentPolicy)), "RECONCILE_POLICY_CHANGED");
108754
+ need4(isDeepStrictEqual11(withoutCodexCommand(oldPolicy), withoutCodexCommand(currentPolicy)), "RECONCILE_POLICY_CHANGED");
108728
108755
  supersededBinding = true;
108729
108756
  }
108730
108757
  if (supersededBinding || recovery) {
@@ -108760,7 +108787,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108760
108787
  need4(state?.enabled === true && typeof state.trusted_hash === "string" && /^sha256:[a-f0-9]{64}$/.test(state.trusted_hash), "RECONCILE_CONFIG_DRIFT");
108761
108788
  }
108762
108789
  } else
108763
- need4(isDeepStrictEqual10(expectedConfig, current), "RECONCILE_CONFIG_DRIFT");
108790
+ need4(isDeepStrictEqual11(expectedConfig, current), "RECONCILE_CONFIG_DRIFT");
108764
108791
  const configDiscoveryRecovery = recovery && binding.discovery?.codex ? createCodexHookDiscoveryRecovery(binding.discovery.codex, configBefore, currentConfig, intent.hooks, additions?.trustProof) : undefined;
108765
108792
  assertManagedAgentBridge("codex", { home, dataDir: dataDir3, projectDir: home, codexDiscoveryRecovery: configDiscoveryRecovery });
108766
108793
  const recoveryCli = recovery ? bindSkillsCli("skills", options.reviewedSkillsCli) : undefined;
@@ -108769,14 +108796,14 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108769
108796
  const skillsCli = recovery ? inspectRecordedSkillsCli(binding.commands.codex, intent.skillsCli) : bindSkillsCli(binding.commands.codex, options.reviewedSkillsCli);
108770
108797
  if (supersededBinding)
108771
108798
  verifyReleaseArtifact(options.releaseProof, skillsCli);
108772
- const skillsBindingChanged = !isDeepStrictEqual10(skillsCli.receipt, intent.skillsCli);
108799
+ const skillsBindingChanged = !isDeepStrictEqual11(skillsCli.receipt, intent.skillsCli);
108773
108800
  need4(!skillsBindingChanged || supersededBinding, "RECONCILE_SKILLS_BINDING_CHANGED");
108774
108801
  if (!additions && hooksCurrent.sha256 !== intent.hooksSha256 && hooksCurrent.sha256 !== hooksBefore.sha256) {
108775
108802
  need4(supersededBinding, "RECONCILE_HOOKS_CHANGED");
108776
108803
  const currentDeclarations = JSON.parse(hooksCurrent.text).hooks;
108777
108804
  for (const [event, [nativeEvent]] of Object.entries(eventNames)) {
108778
108805
  const command = renderAgentHookCommand(binding.commands.codex, "codex", binding.profiles.codex, event);
108779
- need4(isDeepStrictEqual10(currentDeclarations?.[event], [{ hooks: [{ type: "command", command, timeout: 15 }] }]), "RECONCILE_BINDING_HOOK_CHANGED");
108806
+ need4(isDeepStrictEqual11(currentDeclarations?.[event], [{ hooks: [{ type: "command", command, timeout: 15 }] }]), "RECONCILE_BINDING_HOOK_CHANGED");
108780
108807
  }
108781
108808
  }
108782
108809
  rpc = await connect({ command: options.codexCommand ?? "codex", home, codexHome });
@@ -108799,9 +108826,9 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108799
108826
  delete identity4.command;
108800
108827
  return identity4;
108801
108828
  };
108802
- need4(isDeepStrictEqual10(stable(actual), stable(expected)), "RECONCILE_NATIVE_IDENTITY_CHANGED");
108829
+ need4(isDeepStrictEqual11(stable(actual), stable(expected)), "RECONCILE_NATIVE_IDENTITY_CHANGED");
108803
108830
  if (!plannedKeys.has(expected.key))
108804
- need4(isDeepStrictEqual10(actual, expected), "RECONCILE_UNRELATED_HOOK_CHANGED");
108831
+ need4(isDeepStrictEqual11(actual, expected), "RECONCILE_UNRELATED_HOOK_CHANGED");
108805
108832
  }
108806
108833
  for (const wanted of intent.hooks) {
108807
108834
  const matches2 = nativeHooks.filter((h2) => h2.key === wanted.key || h2.key === wanted.key.replace(join57(home, ".codex/hooks.json"), hooksPath));
@@ -108818,12 +108845,12 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108818
108845
  }
108819
108846
  const config2 = await rpc.request("config/read", { includeLayers: true, cwd: home });
108820
108847
  const layers = config2?.layers?.filter((layer) => layer.name?.type === "user" && layer.name.file === nativeConfigPath && !layer.name.profile);
108821
- need4(layers?.length === 1 && !layers[0].disabledReason && isDeepStrictEqual10(layers[0].config, current), "RECONCILE_NATIVE_CONFIG_MISMATCH");
108848
+ need4(layers?.length === 1 && !layers[0].disabledReason && isDeepStrictEqual11(layers[0].config, current), "RECONCILE_NATIVE_CONFIG_MISMATCH");
108822
108849
  const finalDiscovery = await rpc.request("hooks/list", { cwds: [home] });
108823
- 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 || isDeepStrictEqual10(finalDiscovery.data[0].hooks.map(safeNativeHook), nativeHooks.map(safeNativeHook))), "RECONCILE_NATIVE_STATE_CHANGED");
108850
+ 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 || isDeepStrictEqual11(finalDiscovery.data[0].hooks.map(safeNativeHook), nativeHooks.map(safeNativeHook))), "RECONCILE_NATIVE_STATE_CHANGED");
108824
108851
  const finalConfig = await rpc.request("config/read", { includeLayers: true, cwd: home });
108825
108852
  const finalLayers = finalConfig?.layers?.filter((layer) => layer.name?.type === "user" && layer.name.file === nativeConfigPath && !layer.name.profile);
108826
- need4(finalLayers?.length === 1 && !finalLayers[0].disabledReason && isDeepStrictEqual10(finalLayers[0].config, current), "RECONCILE_NATIVE_CONFIG_CHANGED");
108853
+ need4(finalLayers?.length === 1 && !finalLayers[0].disabledReason && isDeepStrictEqual11(finalLayers[0].config, current), "RECONCILE_NATIVE_CONFIG_CHANGED");
108827
108854
  if (recovery) {
108828
108855
  for (const response of [config2, finalConfig]) {
108829
108856
  const layerIndex = response.layers.findIndex((layer) => layer.name?.type === "user" && layer.name.file === nativeConfigPath && !layer.name.profile);
@@ -108881,7 +108908,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
108881
108908
  const groups = declarations[event];
108882
108909
  need4(Array.isArray(groups), "DECLARATION_CHANGED");
108883
108910
  const matches2 = groups.flatMap((group, gi) => (group.hooks ?? []).flatMap((entry, hi) => entry.command === command ? [{ group, entry, gi, hi }] : []));
108884
- need4(matches2.length === 1 && isDeepStrictEqual10(matches2[0].group, { hooks: [{ type: "command", command, timeout: 15 }] }), "DECLARATION_CHANGED");
108911
+ need4(matches2.length === 1 && isDeepStrictEqual11(matches2[0].group, { hooks: [{ type: "command", command, timeout: 15 }] }), "DECLARATION_CHANGED");
108885
108912
  expected.push({ event: nativeEvent, key: `${join57(home, ".codex/hooks.json")}:${label}:${matches2[0].gi}:${matches2[0].hi}`, command });
108886
108913
  }
108887
108914
  codexTrustTextWitness(configFile.text, expected.map((h) => h.key));
@@ -108905,7 +108932,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
108905
108932
  const discovered = await list2(), admitted = admit(discovered);
108906
108933
  const config2 = await rpc.request("config/read", { includeLayers: true, cwd: home });
108907
108934
  const layers = config2?.layers?.filter((layer) => layer.name?.type === "user" && layer.name.file === nativeConfigPath && !layer.name.profile);
108908
- need4(layers?.length === 1 && /^sha256:[a-f0-9]{64}$/.test(layers[0].version) && !layers[0].disabledReason && isDeepStrictEqual10(layers[0].config, original), "NATIVE_CONFIG_MISMATCH");
108935
+ need4(layers?.length === 1 && /^sha256:[a-f0-9]{64}$/.test(layers[0].version) && !layers[0].disabledReason && isDeepStrictEqual11(layers[0].config, original), "NATIVE_CONFIG_MISMATCH");
108909
108936
  need4(config2.config && config2.config.features?.hooks !== false, "NATIVE_HOOKS_DISABLED");
108910
108937
  need4(config2.layers.slice(0, config2.layers.indexOf(layers[0])).every((layer) => layer.config?.hooks === undefined && layer.config?.features?.hooks === undefined), "NATIVE_CONFIG_OVERRIDDEN");
108911
108938
  const keys3 = admitted.map((h) => h.key), planned = admitted.filter((h) => !h.enabled || h.trustStatus !== "trusted");
@@ -108944,7 +108971,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
108944
108971
  save(join57(journal, "hooks.before.json"), hooksFile.bytes);
108945
108972
  save(join57(journal, "policy.before.json"), policyFile.bytes);
108946
108973
  save(join57(journal, "intent.json"), json2({ ...digestInput, planDigest, hooks: planned, nativeHooks: discovered.map(safeNativeHook) }));
108947
- need4(isDeepStrictEqual10(admit(await list2()), admitted), "NATIVE_DISCOVERY_CHANGED");
108974
+ need4(isDeepStrictEqual11(admit(await list2()), admitted), "NATIVE_DISCOVERY_CHANGED");
108948
108975
  checkInputs();
108949
108976
  let result2;
108950
108977
  try {
@@ -108958,7 +108985,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
108958
108985
  expectedConfig.hooks.state ??= {};
108959
108986
  for (const h of planned)
108960
108987
  expectedConfig.hooks.state[h.key] = { ...expectedConfig.hooks.state[h.key], enabled: true, trusted_hash: h.currentHash };
108961
- need4(isDeepStrictEqual10(structure3(after2.text), expectedConfig) && codexTrustTextWitness(after2.text, keys3) === preservationText, "PRESERVATION_FAILED");
108988
+ need4(isDeepStrictEqual11(structure3(after2.text), expectedConfig) && codexTrustTextWitness(after2.text, keys3) === preservationText, "PRESERVATION_FAILED");
108962
108989
  const savedConfig = snapshot(join57(journal, "config.before.toml"), true);
108963
108990
  need4(savedConfig.sha256 === configFile.sha256, "JOURNAL_CHANGED");
108964
108991
  const discoveryTransition = binding.discovery?.codex ? createCodexHookDiscoveryRecovery(binding.discovery.codex, savedConfig, after2, planned) : undefined;
@@ -108966,8 +108993,8 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
108966
108993
  unchanged2(hooksFile);
108967
108994
  assertManagedAgentBridge("codex", { home, dataDir: dataDir3, projectDir: home, codexDiscoveryRecovery: discoveryTransition });
108968
108995
  const current = await list2(), confirmed = admit(current);
108969
- need4(confirmed.every((h) => h.enabled && h.trustStatus === "trusted") && isDeepStrictEqual10(confirmed.map((h) => [h.key, h.currentHash]), admitted.map((h) => [h.key, h.currentHash])), "NATIVE_NOT_ELIGIBLE");
108970
- need4(isDeepStrictEqual10(current.filter((h) => !keys3.includes(h.key)), discovered.filter((h) => !keys3.includes(h.key))), "UNRELATED_HOOK_CHANGED");
108996
+ need4(confirmed.every((h) => h.enabled && h.trustStatus === "trusted") && isDeepStrictEqual11(confirmed.map((h) => [h.key, h.currentHash]), admitted.map((h) => [h.key, h.currentHash])), "NATIVE_NOT_ELIGIBLE");
108997
+ need4(isDeepStrictEqual11(current.filter((h) => !keys3.includes(h.key)), discovered.filter((h) => !keys3.includes(h.key))), "UNRELATED_HOOK_CHANGED");
108971
108998
  unchanged2(after2);
108972
108999
  unchanged2(savedConfig);
108973
109000
  unchanged2(hooksFile);
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.9.15",
20022
+ version: "0.9.16",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
@@ -48834,6 +48834,7 @@ var init_codex_hook_discovery_recovery = __esm(() => {
48834
48834
  import { createHash as createHash14 } from "crypto";
48835
48835
  import { lstatSync as lstatSync16, readFileSync as readFileSync14, readdirSync as readdirSync6, realpathSync as realpathSync3 } from "fs";
48836
48836
  import { basename as basename7, dirname as dirname14, isAbsolute as isAbsolute13, join as join23, resolve as resolve13, sep as sep4 } from "path";
48837
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
48837
48838
  function parseConfig(text2, path, toml = false) {
48838
48839
  try {
48839
48840
  const value = toml ? Bun.TOML.parse(text2) : JSON.parse(text2);
@@ -49008,6 +49009,19 @@ function resolveAgentDiscovery(options) {
49008
49009
  }
49009
49010
  }
49010
49011
  }
49012
+ if (options.reviewed === undefined && options.retainedReview !== undefined) {
49013
+ const retained = options.retainedReview;
49014
+ if (retained.agent !== agent || retained.method !== "reviewed")
49015
+ unresolved("invalid retained review");
49016
+ verifyAgentDiscovery(retained);
49017
+ if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
49018
+ unresolved("retained review is missing the full agent configuration source");
49019
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
49020
+ unresolved("retained settings witness names another configuration source");
49021
+ if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual2(saved, source))) || [...roots].some((root) => !retained.roots.includes(root)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
49022
+ unresolved("retained review is missing current runtime discovery coverage");
49023
+ return retained;
49024
+ }
49011
49025
  if (options.reviewed && (options.reviewed.version !== 1 || !Array.isArray(options.reviewed.agents) || options.reviewed.agents.some((item) => !item || typeof item !== "object") || new Set(options.reviewed.agents.map((item) => item.agent)).size !== options.reviewed.agents.length))
49012
49026
  throw new Error("Invalid --discovery-inputs version or agents");
49013
49027
  const review = options.reviewed?.agents.find((item) => item.agent === agent);
@@ -57344,7 +57358,7 @@ var init_execution_grants = __esm(() => {
57344
57358
  });
57345
57359
 
57346
57360
  // src/lib/execution-grant-client.ts
57347
- import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
57361
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
57348
57362
  function id(value) {
57349
57363
  if (!grantIdentifier(value))
57350
57364
  throw new Error("Invalid execution grant profile or revision identifier.");
@@ -57387,7 +57401,7 @@ async function resolveExecutionGrant(input2) {
57387
57401
  if (!grantRecord(value) || Object.keys(value).length !== 3 || !grantIdentifier(value.policyRevision) || !grantIdentifier(value.grantId) || !grantRecord(value.bindings)) {
57388
57402
  throw new SkillSelectionError("INVALID_EXECUTION_GRANT", "The Skills API returned an invalid execution grant.");
57389
57403
  }
57390
- if (!isDeepStrictEqual2(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual2(value.bindings.consumer, snapshot2.consumer)) {
57404
+ if (!isDeepStrictEqual3(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual3(value.bindings.consumer, snapshot2.consumer)) {
57391
57405
  throw new SkillSelectionError("EXECUTION_GRANT_SCOPE_MISMATCH", "The Skills API returned a grant for a different selection or consumer.");
57392
57406
  }
57393
57407
  return value;
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.9.15",
10
+ version: "0.9.16",
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.9.15",
24125
+ version: "0.9.16",
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.9.15",
24128
+ version: "0.9.16",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
package/dist/index.js CHANGED
@@ -9093,6 +9093,7 @@ function assertAgentPolicyCollections(policy) {
9093
9093
  import { createHash as createHash16 } from "crypto";
9094
9094
  import { lstatSync as lstatSync20, readFileSync as readFileSync16, readdirSync as readdirSync9, realpathSync as realpathSync4 } from "fs";
9095
9095
  import { basename as basename8, dirname as dirname16, isAbsolute as isAbsolute14, join as join26, resolve as resolve14, sep as sep5 } from "path";
9096
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
9096
9097
 
9097
9098
  // node_modules/yaml/dist/index.js
9098
9099
  var composer = require_composer();
@@ -9610,7 +9611,7 @@ import { isAbsolute as isAbsolute11, join as join23, resolve as resolve11 } from
9610
9611
  // package.json
9611
9612
  var package_default = {
9612
9613
  name: "@hasna/skills",
9613
- version: "0.9.15",
9614
+ version: "0.9.16",
9614
9615
  description: "Skills library for AI coding agents",
9615
9616
  type: "module",
9616
9617
  bin: {
@@ -13369,7 +13370,15 @@ function planAgentIntegration(options) {
13369
13370
  validateProfile(profileId2);
13370
13371
  return [agent, { command, profileId: profileId2 }];
13371
13372
  }));
13372
- const discoveries = [...new Set(options.agents)].map((agent) => resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, canonical: (path) => canonicalAgentPath(path, aliases) }));
13373
+ const retainedAgents = [];
13374
+ const discoveries = [...new Set(options.agents)].map((agent) => {
13375
+ const retainedReview = options.discoveryInputs === undefined && policy.bridge?.discovery?.[agent]?.method === "reviewed" ? policy.bridge.discovery[agent] : undefined;
13376
+ if (retainedReview) {
13377
+ assertManagedAgentBridge(agent, { home, dataDir: dataDir2, projectDir: options.projectDir ?? home });
13378
+ retainedAgents.push(agent);
13379
+ }
13380
+ return resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, retainedReview, canonical: (path) => canonicalAgentPath(path, aliases) });
13381
+ });
13373
13382
  const nativeSkills = inventoryNativeSkills(home, { includeVendor: true, guardHermes: options.agents.includes("hermes"), agents: options.agents, projectDir: options.projectDir, agentRoots: discoveries.flatMap((binding) => binding.roots.map((path) => ({ agent: binding.agent, path }))), allowRootAliases: options.allowRootAliases });
13374
13383
  const changes = [];
13375
13384
  for (const agent of [...new Set(options.agents)]) {
@@ -13454,7 +13463,7 @@ function planAgentIntegration(options) {
13454
13463
  if (JSON.stringify(policy) !== JSON.stringify(nextPolicy))
13455
13464
  changes.push({ path: policyPath, before: previousPolicy, after: serializedPolicy });
13456
13465
  recheckRootAliases(aliases);
13457
- return { dataDir: dataDir2, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {} };
13466
+ return { dataDir: dataDir2, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {}, ...retainedAgents.length ? { retainedReviewChecks: { home, projectDir: options.projectDir ?? home, agents: retainedAgents } } : {} };
13458
13467
  }
13459
13468
  function planClaudeStopHookUpdate(options) {
13460
13469
  return planClaudeHookEventsUpdate({ ...options, events: ["Stop"] });
@@ -13573,6 +13582,8 @@ function applyAgentIntegration(plan) {
13573
13582
  parseManagedSkillPolicy(change.after);
13574
13583
  const aliases = plan.rootAliases ?? [];
13575
13584
  recheckRootAliases(aliases);
13585
+ for (const agent of plan.retainedReviewChecks?.agents ?? [])
13586
+ assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
13576
13587
  for (const agent of plan.managedAgentChecks?.agents ?? [])
13577
13588
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
13578
13589
  for (const binding of plan.discoveryBefore ?? []) {
@@ -16573,7 +16584,10 @@ function rebindAgentDiscovery(binding, changes) {
16573
16584
  const budget = discoveryByteBudget();
16574
16585
  return { ...binding, sources: binding.sources.map((source) => {
16575
16586
  assertMarketplaceBinding(binding, source);
16576
- return { ...source, sha256: projected(source, changes, budget) };
16587
+ const sha2562 = projected(source, changes, budget);
16588
+ if (!changes.has(source.path) && sha2562 !== source.sha256)
16589
+ throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
16590
+ return { ...source, sha256: sha2562 };
16577
16591
  }) };
16578
16592
  }
16579
16593
  function captureDiscoveryByteSources(paths) {
@@ -16697,6 +16711,19 @@ function resolveAgentDiscovery(options) {
16697
16711
  }
16698
16712
  }
16699
16713
  }
16714
+ if (options.reviewed === undefined && options.retainedReview !== undefined) {
16715
+ const retained = options.retainedReview;
16716
+ if (retained.agent !== agent || retained.method !== "reviewed")
16717
+ unresolved("invalid retained review");
16718
+ verifyAgentDiscovery(retained);
16719
+ if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
16720
+ unresolved("retained review is missing the full agent configuration source");
16721
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
16722
+ unresolved("retained settings witness names another configuration source");
16723
+ if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual3(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
16724
+ unresolved("retained review is missing current runtime discovery coverage");
16725
+ return retained;
16726
+ }
16700
16727
  if (options.reviewed && (options.reviewed.version !== 1 || !Array.isArray(options.reviewed.agents) || options.reviewed.agents.some((item) => !item || typeof item !== "object") || new Set(options.reviewed.agents.map((item) => item.agent)).size !== options.reviewed.agents.length))
16701
16728
  throw new Error("Invalid --discovery-inputs version or agents");
16702
16729
  const review = options.reviewed?.agents.find((item) => item.agent === agent);
@@ -46,5 +46,6 @@ export declare function resolveAgentDiscovery(options: {
46
46
  home: string;
47
47
  agent: IntegrationAgent;
48
48
  reviewed?: ReviewedDiscoveryInputs;
49
+ retainedReview?: AgentDiscoveryBinding;
49
50
  canonical?: (path: string) => string;
50
51
  }): AgentDiscoveryBinding;
@@ -56,6 +56,11 @@ export interface AgentIntegrationPlan {
56
56
  home: string;
57
57
  agents: IntegrationAgent[];
58
58
  };
59
+ retainedReviewChecks?: {
60
+ home: string;
61
+ projectDir: string;
62
+ agents: IntegrationAgent[];
63
+ };
59
64
  }
60
65
  /** Parse the reviewed exact-target binding used by native migration. Paths are
61
66
  * deliberately project-relative so a manifest cannot silently select another
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.9.15",
32376
+ version: "0.9.16",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -37704,6 +37704,7 @@ function isOwnedCliBridge(path, expectedPaths) {
37704
37704
  import { createHash as createHash15 } from "crypto";
37705
37705
  import { lstatSync as lstatSync17, readFileSync as readFileSync12, readdirSync as readdirSync6, realpathSync as realpathSync3 } from "fs";
37706
37706
  import { basename as basename6, dirname as dirname13, isAbsolute as isAbsolute14, join as join20, resolve as resolve13, sep as sep3 } from "path";
37707
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
37707
37708
 
37708
37709
  // node_modules/yaml/dist/index.js
37709
37710
  var composer = require_composer();
@@ -40401,7 +40402,10 @@ function rebindAgentDiscovery(binding, changes) {
40401
40402
  const budget = discoveryByteBudget();
40402
40403
  return { ...binding, sources: binding.sources.map((source) => {
40403
40404
  assertMarketplaceBinding(binding, source);
40404
- return { ...source, sha256: projected(source, changes, budget) };
40405
+ const sha2562 = projected(source, changes, budget);
40406
+ if (!changes.has(source.path) && sha2562 !== source.sha256)
40407
+ throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
40408
+ return { ...source, sha256: sha2562 };
40405
40409
  }) };
40406
40410
  }
40407
40411
  function captureDiscoveryByteSources(paths) {
@@ -40525,6 +40529,19 @@ function resolveAgentDiscovery(options) {
40525
40529
  }
40526
40530
  }
40527
40531
  }
40532
+ if (options.reviewed === undefined && options.retainedReview !== undefined) {
40533
+ const retained = options.retainedReview;
40534
+ if (retained.agent !== agent || retained.method !== "reviewed")
40535
+ unresolved("invalid retained review");
40536
+ verifyAgentDiscovery(retained);
40537
+ if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
40538
+ unresolved("retained review is missing the full agent configuration source");
40539
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
40540
+ unresolved("retained settings witness names another configuration source");
40541
+ if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual3(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
40542
+ unresolved("retained review is missing current runtime discovery coverage");
40543
+ return retained;
40544
+ }
40528
40545
  if (options.reviewed && (options.reviewed.version !== 1 || !Array.isArray(options.reviewed.agents) || options.reviewed.agents.some((item) => !item || typeof item !== "object") || new Set(options.reviewed.agents.map((item) => item.agent)).size !== options.reviewed.agents.length))
40529
40546
  throw new Error("Invalid --discovery-inputs version or agents");
40530
40547
  const review = options.reviewed?.agents.find((item) => item.agent === agent);
@@ -40718,7 +40735,7 @@ export default async ({ directory }) => {
40718
40735
  }
40719
40736
 
40720
40737
  // src/lib/agent-codex.ts
40721
- import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
40738
+ import { isDeepStrictEqual as isDeepStrictEqual4 } from "util";
40722
40739
  var CODEX_SKILL_CONFIG_SECTIONS = /^\[\[skills\.config\]\][^\n]*(?:\n(?!\s*\[)[^\n]*)*/gm;
40723
40740
  function assertCodexPathConfigEditable(text2) {
40724
40741
  const before = Bun.TOML.parse(text2);
@@ -40728,7 +40745,7 @@ function assertCodexPathConfigEditable(text2) {
40728
40745
  delete expected.skills.config;
40729
40746
  try {
40730
40747
  const withoutSections = Bun.TOML.parse(text2.replace(CODEX_SKILL_CONFIG_SECTIONS, ""));
40731
- if (isDeepStrictEqual3(withoutSections, expected))
40748
+ if (isDeepStrictEqual4(withoutSections, expected))
40732
40749
  return;
40733
40750
  } catch {}
40734
40751
  throw new Error("Codex skill path controls require ordinary [[skills.config]] TOML tables; preserve and convert inline or dotted config arrays before running skills hook install");
@@ -40766,7 +40783,7 @@ function disableCodexBundledSkills(text2) {
40766
40783
  const candidates = new Set;
40767
40784
  const consider = (candidate2) => {
40768
40785
  try {
40769
- if (isDeepStrictEqual3(Bun.TOML.parse(candidate2), expected))
40786
+ if (isDeepStrictEqual4(Bun.TOML.parse(candidate2), expected))
40770
40787
  candidates.add(candidate2);
40771
40788
  } catch {}
40772
40789
  };
@@ -41224,7 +41241,15 @@ function planAgentIntegration(options) {
41224
41241
  validateProfile(profileId2);
41225
41242
  return [agent, { command, profileId: profileId2 }];
41226
41243
  }));
41227
- const discoveries = [...new Set(options.agents)].map((agent) => resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, canonical: (path) => canonicalAgentPath(path, aliases) }));
41244
+ const retainedAgents = [];
41245
+ const discoveries = [...new Set(options.agents)].map((agent) => {
41246
+ const retainedReview = options.discoveryInputs === undefined && policy.bridge?.discovery?.[agent]?.method === "reviewed" ? policy.bridge.discovery[agent] : undefined;
41247
+ if (retainedReview) {
41248
+ assertManagedAgentBridge(agent, { home, dataDir: dataDir2, projectDir: options.projectDir ?? home });
41249
+ retainedAgents.push(agent);
41250
+ }
41251
+ return resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, retainedReview, canonical: (path) => canonicalAgentPath(path, aliases) });
41252
+ });
41228
41253
  const nativeSkills = inventoryNativeSkills(home, { includeVendor: true, guardHermes: options.agents.includes("hermes"), agents: options.agents, projectDir: options.projectDir, agentRoots: discoveries.flatMap((binding) => binding.roots.map((path) => ({ agent: binding.agent, path }))), allowRootAliases: options.allowRootAliases });
41229
41254
  const changes = [];
41230
41255
  for (const agent of [...new Set(options.agents)]) {
@@ -41309,7 +41334,7 @@ function planAgentIntegration(options) {
41309
41334
  if (JSON.stringify(policy) !== JSON.stringify(nextPolicy))
41310
41335
  changes.push({ path: policyPath, before: previousPolicy, after: serializedPolicy });
41311
41336
  recheckRootAliases(aliases);
41312
- return { dataDir: dataDir2, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {} };
41337
+ return { dataDir: dataDir2, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {}, ...retainedAgents.length ? { retainedReviewChecks: { home, projectDir: options.projectDir ?? home, agents: retainedAgents } } : {} };
41313
41338
  }
41314
41339
  function planClaudeStopHookUpdate(options) {
41315
41340
  return planClaudeHookEventsUpdate({ ...options, events: ["Stop"] });
@@ -41428,6 +41453,8 @@ function applyAgentIntegration(plan) {
41428
41453
  parseManagedSkillPolicy(change.after);
41429
41454
  const aliases = plan.rootAliases ?? [];
41430
41455
  recheckRootAliases(aliases);
41456
+ for (const agent of plan.retainedReviewChecks?.agents ?? [])
41457
+ assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
41431
41458
  for (const agent of plan.managedAgentChecks?.agents ?? [])
41432
41459
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
41433
41460
  for (const binding of plan.discoveryBefore ?? []) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {