@hasna/skills 0.9.15 → 0.9.17

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.17",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -39947,8 +39947,8 @@ function assertAgentPolicyCollections(policy) {
39947
39947
  text(source.path);
39948
39948
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
39949
39949
  if (source.hashMode !== undefined)
39950
- requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
39951
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
39950
+ requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
39951
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
39952
39952
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute2(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "settings.json");
39953
39953
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
39954
39954
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute2(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
@@ -50545,7 +50545,7 @@ function canonical2(value) {
50545
50545
  return { kind: "array", items: value.items.map(canonical2) };
50546
50546
  return value;
50547
50547
  }
50548
- function inferencePreference(key, child) {
50548
+ function inferencePreference(key, child, version) {
50549
50549
  if (key === "skipDangerousModePermissionPrompt") {
50550
50550
  need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
50551
50551
  return false;
@@ -50558,7 +50558,7 @@ function inferencePreference(key, child) {
50558
50558
  return false;
50559
50559
  need2(child.kind === "object", "requires an object of model preferences");
50560
50560
  child.entries = child.entries.filter(([model, preferences]) => {
50561
- if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
50561
+ if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
50562
50562
  return true;
50563
50563
  const effort = preferences.entries[0][1];
50564
50564
  need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
@@ -50569,7 +50569,7 @@ function inferencePreference(key, child) {
50569
50569
  function settingsDigest(text2, version = 1) {
50570
50570
  const value = parse3(text2);
50571
50571
  value.entries = value.entries.filter(([key, child]) => {
50572
- if (version === 2 && inferencePreference(key, child))
50572
+ if (version !== 1 && inferencePreference(key, child, version))
50573
50573
  return false;
50574
50574
  if (key === "model") {
50575
50575
  need2(child.kind === "string", "contains an invalid model selection");
@@ -50607,6 +50607,16 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
50607
50607
  budget.remaining -= bytes;
50608
50608
  return settingsDigest(text2, 2);
50609
50609
  }
50610
+ function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
50611
+ return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
50612
+ }
50613
+ function hashClaudeSettingsReplacementV3(text2, budget) {
50614
+ need2(typeof text2 === "string", "requires settings text");
50615
+ const bytes = Buffer.byteLength(text2);
50616
+ need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
50617
+ budget.remaining -= bytes;
50618
+ return settingsDigest(text2, 3);
50619
+ }
50610
50620
  var CLAUDE_SETTINGS_WITNESS_LIMITS, sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, BOOLEAN_PREFERENCES, ENUM_PREFERENCES, BUILTIN_MODEL_SELECTIONS, PERSISTED_EFFORT_LEVELS;
50611
50621
  var init_claude_settings_witness = __esm(() => {
50612
50622
  CLAUDE_SETTINGS_WITNESS_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
@@ -51146,7 +51156,15 @@ function planAgentIntegration(options) {
51146
51156
  validateProfile(profileId2);
51147
51157
  return [agent, { command, profileId: profileId2 }];
51148
51158
  }));
51149
- const discoveries = [...new Set(options.agents)].map((agent) => resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, canonical: (path) => canonicalAgentPath(path, aliases) }));
51159
+ const retainedAgents = [];
51160
+ const discoveries = [...new Set(options.agents)].map((agent) => {
51161
+ const retainedReview = options.discoveryInputs === undefined && policy.bridge?.discovery?.[agent]?.method === "reviewed" ? policy.bridge.discovery[agent] : undefined;
51162
+ if (retainedReview) {
51163
+ assertManagedAgentBridge(agent, { home, dataDir: dataDir3, projectDir: options.projectDir ?? home });
51164
+ retainedAgents.push(agent);
51165
+ }
51166
+ return resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, retainedReview, canonical: (path) => canonicalAgentPath(path, aliases) });
51167
+ });
51150
51168
  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
51169
  const changes = [];
51152
51170
  for (const agent of [...new Set(options.agents)]) {
@@ -51231,7 +51249,7 @@ function planAgentIntegration(options) {
51231
51249
  if (JSON.stringify(policy) !== JSON.stringify(nextPolicy))
51232
51250
  changes.push({ path: policyPath, before: previousPolicy, after: serializedPolicy });
51233
51251
  recheckRootAliases(aliases);
51234
- return { dataDir: dataDir3, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {} };
51252
+ 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
51253
  }
51236
51254
  function verifyCoordinatedDiscovery(binding, home, aliases) {
51237
51255
  try {
@@ -51269,6 +51287,8 @@ function applyAgentIntegration(plan) {
51269
51287
  parseManagedSkillPolicy(change.after);
51270
51288
  const aliases = plan.rootAliases ?? [];
51271
51289
  recheckRootAliases(aliases);
51290
+ for (const agent of plan.retainedReviewChecks?.agents ?? [])
51291
+ assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
51272
51292
  for (const agent of plan.managedAgentChecks?.agents ?? [])
51273
51293
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
51274
51294
  for (const binding of plan.discoveryBefore ?? []) {
@@ -54741,7 +54761,7 @@ function reviewNativeHookAdditions(options) {
54741
54761
  need4(Array.isArray(groups), "RECONCILE_ADDITIONS_DECLARATIONS");
54742
54762
  const key = `${options.nativeHooksPath}:${matchedEvents[event][1]}:${groups.length}:0`;
54743
54763
  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");
54764
+ 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
54765
  let trustState = { enabled: true, trusted_hash: hook.currentHash };
54746
54766
  if (review.version === 2) {
54747
54767
  const state = addition.trustState;
@@ -54769,7 +54789,7 @@ function reviewNativeHookAdditions(options) {
54769
54789
  proofs.set(trustProof, { before: options.configBefore, current: options.configCurrent, journalHooks: options.journalHooks.map(({ key, currentHash }) => ({ key, currentHash })), states: Object.freeze(states), recheck });
54770
54790
  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
54791
  }
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;
54792
+ 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
54793
  var init_codex_hook_additions_review = __esm(() => {
54774
54794
  init_codex_hook_trust_files();
54775
54795
  init_codex_hook_trust_layout();
@@ -54843,6 +54863,7 @@ var init_codex_hook_discovery_recovery = __esm(() => {
54843
54863
  import { createHash as createHash16 } from "crypto";
54844
54864
  import { lstatSync as lstatSync20, readFileSync as readFileSync17, readdirSync as readdirSync10, realpathSync as realpathSync4 } from "fs";
54845
54865
  import { basename as basename8, dirname as dirname16, isAbsolute as isAbsolute15, join as join31, resolve as resolve15, sep as sep5 } from "path";
54866
+ import { isDeepStrictEqual as isDeepStrictEqual7 } from "util";
54846
54867
  function parseConfig(text3, path, toml = false) {
54847
54868
  try {
54848
54869
  const value = toml ? Bun.TOML.parse(text3) : JSON.parse(text3);
@@ -54875,13 +54896,13 @@ function read2(path, changes) {
54875
54896
  return readFileSync17(path, "utf8");
54876
54897
  }
54877
54898
  function projected(source, changes, budget = discoveryByteBudget()) {
54878
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
54899
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
54879
54900
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
54880
54901
  throw new Error("Claude settings witnesses require exact typed metadata");
54881
- const current = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
54902
+ const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
54882
54903
  if (current !== source.sha256)
54883
54904
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
54884
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
54905
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
54885
54906
  }
54886
54907
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
54887
54908
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
@@ -54943,7 +54964,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
54943
54964
  safe4(root2);
54944
54965
  }
54945
54966
  function assertMarketplaceBinding(binding, source) {
54946
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
54967
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
54947
54968
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
54948
54969
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
54949
54970
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -54952,7 +54973,10 @@ function rebindAgentDiscovery(binding, changes) {
54952
54973
  const budget = discoveryByteBudget();
54953
54974
  return { ...binding, sources: binding.sources.map((source) => {
54954
54975
  assertMarketplaceBinding(binding, source);
54955
- return { ...source, sha256: projected(source, changes, budget) };
54976
+ const sha2562 = projected(source, changes, budget);
54977
+ if (!changes.has(source.path) && sha2562 !== source.sha256)
54978
+ throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
54979
+ return { ...source, sha256: sha2562 };
54956
54980
  }) };
54957
54981
  }
54958
54982
  function assertProjectDiscovery(agent, directories, home, canonical4 = resolve15, reviewed) {
@@ -55064,6 +55088,19 @@ function resolveAgentDiscovery(options) {
55064
55088
  }
55065
55089
  }
55066
55090
  }
55091
+ if (options.reviewed === undefined && options.retainedReview !== undefined) {
55092
+ const retained = options.retainedReview;
55093
+ if (retained.agent !== agent || retained.method !== "reviewed")
55094
+ unresolved("invalid retained review");
55095
+ verifyAgentDiscovery(retained);
55096
+ if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
55097
+ unresolved("retained review is missing the full agent configuration source");
55098
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
55099
+ unresolved("retained settings witness names another configuration source");
55100
+ 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))
55101
+ unresolved("retained review is missing current runtime discovery coverage");
55102
+ return retained;
55103
+ }
55067
55104
  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
55105
  throw new Error("Invalid --discovery-inputs version or agents");
55069
55106
  const review = options.reviewed?.agents.find((item) => item.agent === agent);
@@ -55074,7 +55111,7 @@ function resolveAgentDiscovery(options) {
55074
55111
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
55075
55112
  throw new Error("Explicit discovery reviews require full source-file hashes");
55076
55113
  verifyAgentDiscovery(supplied);
55077
- if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
55114
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
55078
55115
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
55079
55116
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
55080
55117
  throw new Error("Discovery review must include the agent configuration source");
@@ -80129,8 +80166,9 @@ Preserved original receipt: ${result2.archivePath}` : `Planned one session recon
80129
80166
  });
80130
80167
  }
80131
80168
  function reportContextError(error61, json2 = false) {
80132
- const code = error61 instanceof SkillSelectionError ? error61.code : "SKILLS_CONTEXT_FAILED";
80133
- const message = error61 instanceof SkillSelectionError ? error61.message : "Skills context could not be loaded. Check authentication and the selected profile.";
80169
+ const storageFull = error61 instanceof Error && ["ENOSPC", "EDQUOT"].includes(error61.code ?? "");
80170
+ const code = error61 instanceof SkillSelectionError ? error61.code : storageFull ? "SKILLS_STORAGE_FULL" : "SKILLS_CONTEXT_FAILED";
80171
+ const message = error61 instanceof SkillSelectionError ? error61.message : storageFull ? "Skills could not save verified local state because the filesystem is full or its quota is exhausted. Free space through your approved recoverable cleanup process, then retry the same session." : "Skills context could not be loaded. Check authentication and the selected profile.";
80134
80172
  if (json2)
80135
80173
  console.log(JSON.stringify({ error: { code, message } }));
80136
80174
  else
@@ -84538,7 +84576,7 @@ var init_execution_grants = __esm(() => {
84538
84576
  });
84539
84577
 
84540
84578
  // src/lib/execution-grant-client.ts
84541
- import { isDeepStrictEqual as isDeepStrictEqual7 } from "util";
84579
+ import { isDeepStrictEqual as isDeepStrictEqual8 } from "util";
84542
84580
  function id(value) {
84543
84581
  if (!grantIdentifier(value))
84544
84582
  throw new Error("Invalid execution grant profile or revision identifier.");
@@ -84590,7 +84628,7 @@ async function saveExecutionGrantPolicy(profileId, grants, revision) {
84590
84628
  headers: revision === undefined ? { "If-None-Match": "*" } : { "If-Match": `"${revision}"` },
84591
84629
  body: JSON.stringify({ grants: snapshot2 })
84592
84630
  }));
84593
- if (policy.profileId !== profileId || policy.previousRevision !== (revision ?? null) || !isDeepStrictEqual7(policy.grants, snapshot2)) {
84631
+ if (policy.profileId !== profileId || policy.previousRevision !== (revision ?? null) || !isDeepStrictEqual8(policy.grants, snapshot2)) {
84594
84632
  throw new Error("The API did not preserve the reviewed execution grant policy; read it before retrying.");
84595
84633
  }
84596
84634
  return policy;
@@ -84601,7 +84639,7 @@ async function resolveExecutionGrant(input2) {
84601
84639
  if (!grantRecord(value) || Object.keys(value).length !== 3 || !grantIdentifier(value.policyRevision) || !grantIdentifier(value.grantId) || !grantRecord(value.bindings)) {
84602
84640
  throw new SkillSelectionError("INVALID_EXECUTION_GRANT", "The Skills API returned an invalid execution grant.");
84603
84641
  }
84604
- if (!isDeepStrictEqual7(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual7(value.bindings.consumer, snapshot2.consumer)) {
84642
+ if (!isDeepStrictEqual8(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual8(value.bindings.consumer, snapshot2.consumer)) {
84605
84643
  throw new SkillSelectionError("EXECUTION_GRANT_SCOPE_MISMATCH", "The Skills API returned a grant for a different selection or consumer.");
84606
84644
  }
84607
84645
  return value;
@@ -108589,13 +108627,13 @@ async function connectCodexHookRpc(options) {
108589
108627
  }
108590
108628
  var SUPPORTED_CODEX_HOOK_VERSIONS;
108591
108629
  var init_codex_hook_rpc = __esm(() => {
108592
- SUPPORTED_CODEX_HOOK_VERSIONS = ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1"];
108630
+ SUPPORTED_CODEX_HOOK_VERSIONS = ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1", "codex-cli 0.156.1"];
108593
108631
  });
108594
108632
 
108595
108633
  // src/lib/codex-hook-trust-identity.ts
108596
108634
  import { realpathSync as realpathSync13 } from "fs";
108597
108635
  import { dirname as dirname30, isAbsolute as isAbsolute25, join as join56 } from "path";
108598
- import { isDeepStrictEqual as isDeepStrictEqual8 } from "util";
108636
+ import { isDeepStrictEqual as isDeepStrictEqual9 } from "util";
108599
108637
  function inspectRecordedSkillsCli(command, expected) {
108600
108638
  need4(isAbsolute25(command) && !command.includes("\x00"), "RECONCILE_RECORDED_COMMAND_REQUIRED");
108601
108639
  const resolved = realpathSync13(command);
@@ -108605,7 +108643,7 @@ function inspectRecordedSkillsCli(command, expected) {
108605
108643
  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
108644
  need4((cli.stat.mode & 0o100n) !== 0n, "SKILLS_COMMAND_NOT_EXECUTABLE");
108607
108645
  const receipt = { path: resolved, version: pkg.version, sha256: cli.sha256, manifestSha256: manifest.sha256 };
108608
- need4(isDeepStrictEqual8(receipt, expected), "RECONCILE_SKILLS_BINDING_CHANGED");
108646
+ need4(isDeepStrictEqual9(receipt, expected), "RECONCILE_SKILLS_BINDING_CHANGED");
108609
108647
  return { receipt, recheck() {
108610
108648
  need4(realpathSync13(command) === resolved, "SKILLS_COMMAND_CHANGED");
108611
108649
  unchanged2(cli);
@@ -108641,19 +108679,19 @@ var init_codex_hook_trust_identity = __esm(() => {
108641
108679
  });
108642
108680
 
108643
108681
  // src/lib/codex-hook-policy-recovery.ts
108644
- import { isDeepStrictEqual as isDeepStrictEqual9 } from "util";
108682
+ import { isDeepStrictEqual as isDeepStrictEqual10 } from "util";
108645
108683
  function reviewedClaudeDiscoveryChange(beforeText, currentText, options) {
108646
108684
  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
108685
  need4(options.reviewedPlanDigest === undefined || /^[a-f0-9]{64}$/.test(options.reviewedPlanDigest), "RECONCILE_RECOVERY_PLAN_CHANGED");
108648
108686
  const before2 = parseManagedSkillPolicy(beforeText), current = parseManagedSkillPolicy(currentText);
108649
108687
  const oldDiscovery = before2.bridge?.discovery?.claude, newDiscovery = current.bridge?.discovery?.claude;
108650
108688
  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] }));
108689
+ 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
108690
  need4(changes.length > 0, "RECONCILE_POLICY_CHANGED");
108653
108691
  const compared = structuredClone(current);
108654
108692
  compared.bridge.discovery.claude.roots = oldDiscovery.roots;
108655
108693
  compared.bridge.discovery.claude.sources = oldDiscovery.sources;
108656
- need4(isDeepStrictEqual9(before2, compared), "RECONCILE_POLICY_CHANGED");
108694
+ need4(isDeepStrictEqual10(before2, compared), "RECONCILE_POLICY_CHANGED");
108657
108695
  verifyAgentDiscovery(newDiscovery);
108658
108696
  return { reason: options.reason, changes };
108659
108697
  }
@@ -108669,7 +108707,7 @@ import { createHash as createHash27, randomUUID as randomUUID10 } from "crypto";
108669
108707
  import { execFileSync } from "child_process";
108670
108708
  import { dirname as dirname31, isAbsolute as isAbsolute26, join as join57, resolve as resolve23 } from "path";
108671
108709
  import { homedir as homedir12 } from "os";
108672
- import { isDeepStrictEqual as isDeepStrictEqual10 } from "util";
108710
+ import { isDeepStrictEqual as isDeepStrictEqual11 } from "util";
108673
108711
  function verifyReleaseArtifact(proof, skillsCli) {
108674
108712
  need4(isAbsolute26(proof.packageTarPath) && !proof.packageTarPath.includes("\x00"), "RECONCILE_RELEASE_PROVENANCE_PATH");
108675
108713
  const tarPath = realpathSync14(proof.packageTarPath);
@@ -108711,7 +108749,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108711
108749
  const configSha256 = intent.configSha256 ?? configBefore.sha256, configVersion = intent.configVersion ?? intent.expectedVersion;
108712
108750
  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
108751
  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");
108752
+ need4(isDeepStrictEqual11(intent.hooks, admitted.filter((hook) => !hook.enabled || hook.trustStatus !== "trusted")), "RECONCILE_INTENT_HOOKS_CHANGED");
108715
108753
  need4(configBefore.sha256 === (intent.beforeSha256 ?? intent.configSha256) && hooksBefore.sha256 === intent.hooksSha256 && policyBefore.sha256 === intent.policySha256, "RECONCILE_JOURNAL_CHANGED");
108716
108754
  const policyCurrent = snapshot(join57(dataDir3, "agent-policy.json"), true);
108717
108755
  const { claudeDiscoveryRecovery: claudeRecovery, nativeHookAdditionsRecovery: additionsRecovery } = options;
@@ -108724,7 +108762,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108724
108762
  if (policyCurrent.sha256 !== policyBefore.sha256 && !claudeRecovery) {
108725
108763
  need4(options.supersedeBinding === true, "RECONCILE_POLICY_CHANGED");
108726
108764
  const oldPolicy = JSON.parse(policyBefore.text), currentPolicy = JSON.parse(policyCurrent.text);
108727
- need4(isDeepStrictEqual10(withoutCodexCommand(oldPolicy), withoutCodexCommand(currentPolicy)), "RECONCILE_POLICY_CHANGED");
108765
+ need4(isDeepStrictEqual11(withoutCodexCommand(oldPolicy), withoutCodexCommand(currentPolicy)), "RECONCILE_POLICY_CHANGED");
108728
108766
  supersededBinding = true;
108729
108767
  }
108730
108768
  if (supersededBinding || recovery) {
@@ -108760,7 +108798,7 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108760
108798
  need4(state?.enabled === true && typeof state.trusted_hash === "string" && /^sha256:[a-f0-9]{64}$/.test(state.trusted_hash), "RECONCILE_CONFIG_DRIFT");
108761
108799
  }
108762
108800
  } else
108763
- need4(isDeepStrictEqual10(expectedConfig, current), "RECONCILE_CONFIG_DRIFT");
108801
+ need4(isDeepStrictEqual11(expectedConfig, current), "RECONCILE_CONFIG_DRIFT");
108764
108802
  const configDiscoveryRecovery = recovery && binding.discovery?.codex ? createCodexHookDiscoveryRecovery(binding.discovery.codex, configBefore, currentConfig, intent.hooks, additions?.trustProof) : undefined;
108765
108803
  assertManagedAgentBridge("codex", { home, dataDir: dataDir3, projectDir: home, codexDiscoveryRecovery: configDiscoveryRecovery });
108766
108804
  const recoveryCli = recovery ? bindSkillsCli("skills", options.reviewedSkillsCli) : undefined;
@@ -108769,14 +108807,14 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108769
108807
  const skillsCli = recovery ? inspectRecordedSkillsCli(binding.commands.codex, intent.skillsCli) : bindSkillsCli(binding.commands.codex, options.reviewedSkillsCli);
108770
108808
  if (supersededBinding)
108771
108809
  verifyReleaseArtifact(options.releaseProof, skillsCli);
108772
- const skillsBindingChanged = !isDeepStrictEqual10(skillsCli.receipt, intent.skillsCli);
108810
+ const skillsBindingChanged = !isDeepStrictEqual11(skillsCli.receipt, intent.skillsCli);
108773
108811
  need4(!skillsBindingChanged || supersededBinding, "RECONCILE_SKILLS_BINDING_CHANGED");
108774
108812
  if (!additions && hooksCurrent.sha256 !== intent.hooksSha256 && hooksCurrent.sha256 !== hooksBefore.sha256) {
108775
108813
  need4(supersededBinding, "RECONCILE_HOOKS_CHANGED");
108776
108814
  const currentDeclarations = JSON.parse(hooksCurrent.text).hooks;
108777
108815
  for (const [event, [nativeEvent]] of Object.entries(eventNames)) {
108778
108816
  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");
108817
+ need4(isDeepStrictEqual11(currentDeclarations?.[event], [{ hooks: [{ type: "command", command, timeout: 15 }] }]), "RECONCILE_BINDING_HOOK_CHANGED");
108780
108818
  }
108781
108819
  }
108782
108820
  rpc = await connect({ command: options.codexCommand ?? "codex", home, codexHome });
@@ -108799,9 +108837,9 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108799
108837
  delete identity4.command;
108800
108838
  return identity4;
108801
108839
  };
108802
- need4(isDeepStrictEqual10(stable(actual), stable(expected)), "RECONCILE_NATIVE_IDENTITY_CHANGED");
108840
+ need4(isDeepStrictEqual11(stable(actual), stable(expected)), "RECONCILE_NATIVE_IDENTITY_CHANGED");
108803
108841
  if (!plannedKeys.has(expected.key))
108804
- need4(isDeepStrictEqual10(actual, expected), "RECONCILE_UNRELATED_HOOK_CHANGED");
108842
+ need4(isDeepStrictEqual11(actual, expected), "RECONCILE_UNRELATED_HOOK_CHANGED");
108805
108843
  }
108806
108844
  for (const wanted of intent.hooks) {
108807
108845
  const matches2 = nativeHooks.filter((h2) => h2.key === wanted.key || h2.key === wanted.key.replace(join57(home, ".codex/hooks.json"), hooksPath));
@@ -108818,12 +108856,12 @@ async function reconcileCodexNativeHooks(options, connect = connectCodexHookRpc)
108818
108856
  }
108819
108857
  const config2 = await rpc.request("config/read", { includeLayers: true, cwd: home });
108820
108858
  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");
108859
+ need4(layers?.length === 1 && !layers[0].disabledReason && isDeepStrictEqual11(layers[0].config, current), "RECONCILE_NATIVE_CONFIG_MISMATCH");
108822
108860
  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");
108861
+ 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
108862
  const finalConfig = await rpc.request("config/read", { includeLayers: true, cwd: home });
108825
108863
  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");
108864
+ need4(finalLayers?.length === 1 && !finalLayers[0].disabledReason && isDeepStrictEqual11(finalLayers[0].config, current), "RECONCILE_NATIVE_CONFIG_CHANGED");
108827
108865
  if (recovery) {
108828
108866
  for (const response of [config2, finalConfig]) {
108829
108867
  const layerIndex = response.layers.findIndex((layer) => layer.name?.type === "user" && layer.name.file === nativeConfigPath && !layer.name.profile);
@@ -108881,7 +108919,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
108881
108919
  const groups = declarations[event];
108882
108920
  need4(Array.isArray(groups), "DECLARATION_CHANGED");
108883
108921
  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");
108922
+ need4(matches2.length === 1 && isDeepStrictEqual11(matches2[0].group, { hooks: [{ type: "command", command, timeout: 15 }] }), "DECLARATION_CHANGED");
108885
108923
  expected.push({ event: nativeEvent, key: `${join57(home, ".codex/hooks.json")}:${label}:${matches2[0].gi}:${matches2[0].hi}`, command });
108886
108924
  }
108887
108925
  codexTrustTextWitness(configFile.text, expected.map((h) => h.key));
@@ -108905,7 +108943,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
108905
108943
  const discovered = await list2(), admitted = admit(discovered);
108906
108944
  const config2 = await rpc.request("config/read", { includeLayers: true, cwd: home });
108907
108945
  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");
108946
+ 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
108947
  need4(config2.config && config2.config.features?.hooks !== false, "NATIVE_HOOKS_DISABLED");
108910
108948
  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
108949
  const keys3 = admitted.map((h) => h.key), planned = admitted.filter((h) => !h.enabled || h.trustStatus !== "trusted");
@@ -108944,7 +108982,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
108944
108982
  save(join57(journal, "hooks.before.json"), hooksFile.bytes);
108945
108983
  save(join57(journal, "policy.before.json"), policyFile.bytes);
108946
108984
  save(join57(journal, "intent.json"), json2({ ...digestInput, planDigest, hooks: planned, nativeHooks: discovered.map(safeNativeHook) }));
108947
- need4(isDeepStrictEqual10(admit(await list2()), admitted), "NATIVE_DISCOVERY_CHANGED");
108985
+ need4(isDeepStrictEqual11(admit(await list2()), admitted), "NATIVE_DISCOVERY_CHANGED");
108948
108986
  checkInputs();
108949
108987
  let result2;
108950
108988
  try {
@@ -108958,7 +108996,7 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
108958
108996
  expectedConfig.hooks.state ??= {};
108959
108997
  for (const h of planned)
108960
108998
  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");
108999
+ need4(isDeepStrictEqual11(structure3(after2.text), expectedConfig) && codexTrustTextWitness(after2.text, keys3) === preservationText, "PRESERVATION_FAILED");
108962
109000
  const savedConfig = snapshot(join57(journal, "config.before.toml"), true);
108963
109001
  need4(savedConfig.sha256 === configFile.sha256, "JOURNAL_CHANGED");
108964
109002
  const discoveryTransition = binding.discovery?.codex ? createCodexHookDiscoveryRecovery(binding.discovery.codex, savedConfig, after2, planned) : undefined;
@@ -108966,8 +109004,8 @@ async function enrollCodexNativeHooks(options = {}, connect = connectCodexHookRp
108966
109004
  unchanged2(hooksFile);
108967
109005
  assertManagedAgentBridge("codex", { home, dataDir: dataDir3, projectDir: home, codexDiscoveryRecovery: discoveryTransition });
108968
109006
  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");
109007
+ 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");
109008
+ need4(isDeepStrictEqual11(current.filter((h) => !keys3.includes(h.key)), discovered.filter((h) => !keys3.includes(h.key))), "UNRELATED_HOOK_CHANGED");
108971
109009
  unchanged2(after2);
108972
109010
  unchanged2(savedConfig);
108973
109011
  unchanged2(hooksFile);
@@ -109027,6 +109065,9 @@ function hookFailureReason(error61, profileId) {
109027
109065
  const profile = typeof profileId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(profileId) && !profileId.includes("..") ? profileId : undefined;
109028
109066
  const identity4 = [profile ? `profile=${profile}` : undefined, owned ? `stage=${error61.stage}` : undefined].filter(Boolean).join(", ");
109029
109067
  const prefix = `Skills context is unavailable [${code2}]${identity4 ? ` (${identity4})` : ""}.`;
109068
+ if (code2 === "SKILLS_STORAGE_FULL") {
109069
+ return `${prefix} The filesystem is full or its quota is exhausted. Free space through your approved recoverable cleanup process, then retry the same session. Do not delete session receipts or change credentials to bypass this failure.`;
109070
+ }
109030
109071
  if (code2 === "PROFILE_LOCK_MISMATCH") {
109031
109072
  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.`;
109032
109073
  }
@@ -109043,6 +109084,7 @@ var CONTEXT_ERROR_CODES, HookDiagnosticError;
109043
109084
  var init_hook_diagnostics = __esm(() => {
109044
109085
  CONTEXT_ERROR_CODES = new Set([
109045
109086
  "SKILLS_CONTEXT_FAILED",
109087
+ "SKILLS_STORAGE_FULL",
109046
109088
  "PROFILE_LOCK_MISMATCH",
109047
109089
  "PROFILE_IDENTITY_MISMATCH",
109048
109090
  "PROFILE_APPLIED_REPORT_FAILED",
@@ -109158,10 +109200,10 @@ function agents(value) {
109158
109200
  }
109159
109201
  function registerAgentIntegration(parent) {
109160
109202
  const hook = parent.command("hook").description("Load selected Skills context through agent lifecycle hooks");
109161
- hook.command("witness").requiredOption("--kind <kind>", "claude-settings-v2 or claude-marketplace-registry-v2").requiredOption("--path <path>", "Canonical absolute path to the reviewed settings or registry file").option("--json", "Output the discovery witness as JSON", false).description("Capture an explicit versioned review witness without installing it").action(async (options) => {
109162
- const capture2 = options.kind === "claude-settings-v2" ? captureClaudeSettingsV2 : options.kind === "claude-marketplace-registry-v2" ? captureClaudeMarketplaceRegistryV2 : null;
109203
+ hook.command("witness").requiredOption("--kind <kind>", "claude-settings-v3, claude-settings-v2 or claude-marketplace-registry-v2").requiredOption("--path <path>", "Canonical absolute path to the reviewed settings or registry file").option("--json", "Output the discovery witness as JSON", false).description("Capture an explicit versioned review witness without installing it").action(async (options) => {
109204
+ const capture2 = options.kind === "claude-settings-v3" ? captureClaudeSettingsV3 : options.kind === "claude-settings-v2" ? captureClaudeSettingsV2 : options.kind === "claude-marketplace-registry-v2" ? captureClaudeMarketplaceRegistryV2 : null;
109163
109205
  if (!capture2)
109164
- throw new Error("Unsupported witness kind; select claude-settings-v2 or claude-marketplace-registry-v2");
109206
+ throw new Error("Unsupported witness kind; select claude-settings-v3, claude-settings-v2 or claude-marketplace-registry-v2");
109165
109207
  await writeCliOutput(JSON.stringify(capture2(options.path), null, 2));
109166
109208
  });
109167
109209
  hook.command("agents").option("--json", "Output the adapter capability inventory", false).description("Show maintained native adapters and explicit coverage limits").action(async () => {
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.17",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
@@ -38126,8 +38126,8 @@ function assertAgentPolicyCollections(policy) {
38126
38126
  text(source.path);
38127
38127
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
38128
38128
  if (source.hashMode !== undefined)
38129
- requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
38130
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
38129
+ requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
38130
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
38131
38131
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve2(source.path) === source.path && basename3(source.path) === "settings.json");
38132
38132
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
38133
38133
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve2(source.path) === source.path && basename3(source.path) === "known_marketplaces.json");
@@ -47080,7 +47080,7 @@ function canonical2(value) {
47080
47080
  return { kind: "array", items: value.items.map(canonical2) };
47081
47081
  return value;
47082
47082
  }
47083
- function inferencePreference(key, child) {
47083
+ function inferencePreference(key, child, version2) {
47084
47084
  if (key === "skipDangerousModePermissionPrompt") {
47085
47085
  need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
47086
47086
  return false;
@@ -47093,7 +47093,7 @@ function inferencePreference(key, child) {
47093
47093
  return false;
47094
47094
  need2(child.kind === "object", "requires an object of model preferences");
47095
47095
  child.entries = child.entries.filter(([model, preferences]) => {
47096
- if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
47096
+ if (version2 === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
47097
47097
  return true;
47098
47098
  const effort = preferences.entries[0][1];
47099
47099
  need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
@@ -47104,7 +47104,7 @@ function inferencePreference(key, child) {
47104
47104
  function settingsDigest(text2, version2 = 1) {
47105
47105
  const value = parse8(text2);
47106
47106
  value.entries = value.entries.filter(([key, child]) => {
47107
- if (version2 === 2 && inferencePreference(key, child))
47107
+ if (version2 !== 1 && inferencePreference(key, child, version2))
47108
47108
  return false;
47109
47109
  if (key === "model") {
47110
47110
  need2(child.kind === "string", "contains an invalid model selection");
@@ -47142,6 +47142,16 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
47142
47142
  budget.remaining -= bytes;
47143
47143
  return settingsDigest(text2, 2);
47144
47144
  }
47145
+ function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
47146
+ return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
47147
+ }
47148
+ function hashClaudeSettingsReplacementV3(text2, budget) {
47149
+ need2(typeof text2 === "string", "requires settings text");
47150
+ const bytes = Buffer.byteLength(text2);
47151
+ need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
47152
+ budget.remaining -= bytes;
47153
+ return settingsDigest(text2, 3);
47154
+ }
47145
47155
  var CLAUDE_SETTINGS_WITNESS_LIMITS, sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.uid === b.uid && a.gid === b.gid && a.nlink === b.nlink && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs, BOOLEAN_PREFERENCES, ENUM_PREFERENCES, BUILTIN_MODEL_SELECTIONS, PERSISTED_EFFORT_LEVELS;
47146
47156
  var init_claude_settings_witness = __esm(() => {
47147
47157
  CLAUDE_SETTINGS_WITNESS_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
@@ -48834,6 +48844,7 @@ var init_codex_hook_discovery_recovery = __esm(() => {
48834
48844
  import { createHash as createHash14 } from "crypto";
48835
48845
  import { lstatSync as lstatSync16, readFileSync as readFileSync14, readdirSync as readdirSync6, realpathSync as realpathSync3 } from "fs";
48836
48846
  import { basename as basename7, dirname as dirname14, isAbsolute as isAbsolute13, join as join23, resolve as resolve13, sep as sep4 } from "path";
48847
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
48837
48848
  function parseConfig(text2, path, toml = false) {
48838
48849
  try {
48839
48850
  const value = toml ? Bun.TOML.parse(text2) : JSON.parse(text2);
@@ -48866,13 +48877,13 @@ function read(path, changes) {
48866
48877
  return readFileSync14(path, "utf8");
48867
48878
  }
48868
48879
  function projected(source, changes, budget = discoveryByteBudget()) {
48869
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
48880
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
48870
48881
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
48871
48882
  throw new Error("Claude settings witnesses require exact typed metadata");
48872
- const current = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
48883
+ const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
48873
48884
  if (current !== source.sha256)
48874
48885
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
48875
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
48886
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
48876
48887
  }
48877
48888
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
48878
48889
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
@@ -48934,7 +48945,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
48934
48945
  safe4(root);
48935
48946
  }
48936
48947
  function assertMarketplaceBinding(binding, source) {
48937
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename7(source.path) !== "settings.json"))
48948
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename7(source.path) !== "settings.json"))
48938
48949
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
48939
48950
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
48940
48951
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -49008,6 +49019,19 @@ function resolveAgentDiscovery(options) {
49008
49019
  }
49009
49020
  }
49010
49021
  }
49022
+ if (options.reviewed === undefined && options.retainedReview !== undefined) {
49023
+ const retained = options.retainedReview;
49024
+ if (retained.agent !== agent || retained.method !== "reviewed")
49025
+ unresolved("invalid retained review");
49026
+ verifyAgentDiscovery(retained);
49027
+ if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
49028
+ unresolved("retained review is missing the full agent configuration source");
49029
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
49030
+ unresolved("retained settings witness names another configuration source");
49031
+ 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))
49032
+ unresolved("retained review is missing current runtime discovery coverage");
49033
+ return retained;
49034
+ }
49011
49035
  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
49036
  throw new Error("Invalid --discovery-inputs version or agents");
49013
49037
  const review = options.reviewed?.agents.find((item) => item.agent === agent);
@@ -49018,7 +49042,7 @@ function resolveAgentDiscovery(options) {
49018
49042
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
49019
49043
  throw new Error("Explicit discovery reviews require full source-file hashes");
49020
49044
  verifyAgentDiscovery(supplied);
49021
- if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
49045
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
49022
49046
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
49023
49047
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
49024
49048
  throw new Error("Discovery review must include the agent configuration source");
@@ -57344,7 +57368,7 @@ var init_execution_grants = __esm(() => {
57344
57368
  });
57345
57369
 
57346
57370
  // src/lib/execution-grant-client.ts
57347
- import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
57371
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
57348
57372
  function id(value) {
57349
57373
  if (!grantIdentifier(value))
57350
57374
  throw new Error("Invalid execution grant profile or revision identifier.");
@@ -57387,7 +57411,7 @@ async function resolveExecutionGrant(input2) {
57387
57411
  if (!grantRecord(value) || Object.keys(value).length !== 3 || !grantIdentifier(value.policyRevision) || !grantIdentifier(value.grantId) || !grantRecord(value.bindings)) {
57388
57412
  throw new SkillSelectionError("INVALID_EXECUTION_GRANT", "The Skills API returned an invalid execution grant.");
57389
57413
  }
57390
- if (!isDeepStrictEqual2(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual2(value.bindings.consumer, snapshot2.consumer)) {
57414
+ if (!isDeepStrictEqual3(value.bindings.selection, snapshot2.selection) || !isDeepStrictEqual3(value.bindings.consumer, snapshot2.consumer)) {
57391
57415
  throw new SkillSelectionError("EXECUTION_GRANT_SCOPE_MISMATCH", "The Skills API returned a grant for a different selection or consumer.");
57392
57416
  }
57393
57417
  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.17",
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.17",
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.17",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
package/dist/index.d.ts CHANGED
@@ -63,7 +63,7 @@ export { inspectSkillSession, reconcileSkillSession, type SessionReconciliationI
63
63
  export { planAgentIntegration, planClaudeStopHookUpdate, planClaudePreToolUseHookUpdate, planClaudeHookEventsUpdate, applyAgentIntegration, inventoryNativeSkills, archiveNativeSkills } from "./lib/agent-integration.js";
64
64
  export { captureDiscoveryDirectories, captureDiscoveryByteSources, captureDiscoveryPathSources, type DiscoveryDirectory, type DiscoverySource } from "./lib/agent-discovery.js";
65
65
  export { captureClaudeMarketplaceRegistry, captureClaudeMarketplaceRegistryV2 } from "./lib/claude-marketplace-registry.js";
66
- export { captureClaudeSettings, captureClaudeSettingsV2 } from "./lib/claude-settings-witness.js";
66
+ export { captureClaudeSettings, captureClaudeSettingsV2, captureClaudeSettingsV3, upgradeClaudeSettingsWitness } from "./lib/claude-settings-witness.js";
67
67
  export { buildPluginProjection, pluginTreeDigest, type PluginProjectionManifest, type PluginPayloadMapping, type PluginFileWitness } from "./lib/plugin-projection.js";
68
68
  export { planPluginAdmission, admitPlugin, resolveAdmittedPlugin, pluginResolverCommand, type PluginAdmissionTarget, type PluginAdmissionBinding, type PluginAdmissionPrincipal, type PluginAdmissionPlan, type PluginAdmissionIdentity, type PluginBundleIdentity, type PluginRoutingIdentity, type PluginAdmissionReceipt } from "./lib/plugin-admission.js";
69
69
  export { captureManagedPluginRegistry, type ManagedPluginRegistrationWitness } from "./lib/plugin-discovery.js";
package/dist/index.js CHANGED
@@ -9055,8 +9055,8 @@ function assertAgentPolicyCollections(policy) {
9055
9055
  text(source.path);
9056
9056
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
9057
9057
  if (source.hashMode !== undefined)
9058
- requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
9059
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
9058
+ requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
9059
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
9060
9060
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute2(source.path) && resolve2(source.path) === source.path && basename2(source.path) === "settings.json");
9061
9061
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
9062
9062
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute2(source.path) && resolve2(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
@@ -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.17",
9614
9615
  description: "Skills library for AI coding agents",
9615
9616
  type: "module",
9616
9617
  bin: {
@@ -12873,7 +12874,7 @@ function assertClaudeHookEventsReplacement(before, replacement, events) {
12873
12874
  need2(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
12874
12875
  }
12875
12876
  var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
12876
- function inferencePreference(key, child) {
12877
+ function inferencePreference(key, child, version) {
12877
12878
  if (key === "skipDangerousModePermissionPrompt") {
12878
12879
  need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
12879
12880
  return false;
@@ -12886,7 +12887,7 @@ function inferencePreference(key, child) {
12886
12887
  return false;
12887
12888
  need2(child.kind === "object", "requires an object of model preferences");
12888
12889
  child.entries = child.entries.filter(([model, preferences]) => {
12889
- if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
12890
+ if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
12890
12891
  return true;
12891
12892
  const effort = preferences.entries[0][1];
12892
12893
  need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
@@ -12897,7 +12898,7 @@ function inferencePreference(key, child) {
12897
12898
  function settingsDigest(text2, version = 1) {
12898
12899
  const value = parse3(text2);
12899
12900
  value.entries = value.entries.filter(([key, child]) => {
12900
- if (version === 2 && inferencePreference(key, child))
12901
+ if (version !== 1 && inferencePreference(key, child, version))
12901
12902
  return false;
12902
12903
  if (key === "model") {
12903
12904
  need2(child.kind === "string", "contains an invalid model selection");
@@ -12935,6 +12936,25 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
12935
12936
  budget.remaining -= bytes;
12936
12937
  return settingsDigest(text2, 2);
12937
12938
  }
12939
+ function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
12940
+ return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
12941
+ }
12942
+ function hashClaudeSettingsReplacementV3(text2, budget) {
12943
+ need2(typeof text2 === "string", "requires settings text");
12944
+ const bytes = Buffer.byteLength(text2);
12945
+ need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
12946
+ budget.remaining -= bytes;
12947
+ return settingsDigest(text2, 3);
12948
+ }
12949
+ function upgradeClaudeSettingsWitness(previous, reviewedSettingsPath) {
12950
+ need2(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === "claude-settings-v1" || previous.hashMode === "claude-settings-v2") && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact legacy settings witness");
12951
+ const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
12952
+ const before = readSettings(reviewedSettingsPath, budget), current = readSettings(previous.path, budget);
12953
+ need2(settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : 2) === previous.sha256, "reviewed preimage does not match the legacy witness");
12954
+ const sha2562 = settingsDigest(current, 3);
12955
+ need2(settingsDigest(before, 3) === sha2562, "non-preference settings changed; explicit discovery review required");
12956
+ return { path: previous.path, hashMode: "claude-settings-v3", sha256: sha2562 };
12957
+ }
12938
12958
 
12939
12959
  // src/lib/agent-integration.ts
12940
12960
  var sha = (data) => createHash10("sha256").update(data).digest("hex");
@@ -13369,7 +13389,15 @@ function planAgentIntegration(options) {
13369
13389
  validateProfile(profileId2);
13370
13390
  return [agent, { command, profileId: profileId2 }];
13371
13391
  }));
13372
- const discoveries = [...new Set(options.agents)].map((agent) => resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, canonical: (path) => canonicalAgentPath(path, aliases) }));
13392
+ const retainedAgents = [];
13393
+ const discoveries = [...new Set(options.agents)].map((agent) => {
13394
+ const retainedReview = options.discoveryInputs === undefined && policy.bridge?.discovery?.[agent]?.method === "reviewed" ? policy.bridge.discovery[agent] : undefined;
13395
+ if (retainedReview) {
13396
+ assertManagedAgentBridge(agent, { home, dataDir: dataDir2, projectDir: options.projectDir ?? home });
13397
+ retainedAgents.push(agent);
13398
+ }
13399
+ return resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, retainedReview, canonical: (path) => canonicalAgentPath(path, aliases) });
13400
+ });
13373
13401
  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
13402
  const changes = [];
13375
13403
  for (const agent of [...new Set(options.agents)]) {
@@ -13454,7 +13482,7 @@ function planAgentIntegration(options) {
13454
13482
  if (JSON.stringify(policy) !== JSON.stringify(nextPolicy))
13455
13483
  changes.push({ path: policyPath, before: previousPolicy, after: serializedPolicy });
13456
13484
  recheckRootAliases(aliases);
13457
- return { dataDir: dataDir2, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {} };
13485
+ 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
13486
  }
13459
13487
  function planClaudeStopHookUpdate(options) {
13460
13488
  return planClaudeHookEventsUpdate({ ...options, events: ["Stop"] });
@@ -13573,6 +13601,8 @@ function applyAgentIntegration(plan) {
13573
13601
  parseManagedSkillPolicy(change.after);
13574
13602
  const aliases = plan.rootAliases ?? [];
13575
13603
  recheckRootAliases(aliases);
13604
+ for (const agent of plan.retainedReviewChecks?.agents ?? [])
13605
+ assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
13576
13606
  for (const agent of plan.managedAgentChecks?.agents ?? [])
13577
13607
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
13578
13608
  for (const binding of plan.discoveryBefore ?? []) {
@@ -16496,13 +16526,13 @@ function read2(path, changes) {
16496
16526
  return readFileSync16(path, "utf8");
16497
16527
  }
16498
16528
  function projected(source, changes, budget = discoveryByteBudget()) {
16499
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
16529
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
16500
16530
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
16501
16531
  throw new Error("Claude settings witnesses require exact typed metadata");
16502
- const current = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
16532
+ const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
16503
16533
  if (current !== source.sha256)
16504
16534
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
16505
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
16535
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
16506
16536
  }
16507
16537
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
16508
16538
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
@@ -16564,7 +16594,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
16564
16594
  safe4(root2);
16565
16595
  }
16566
16596
  function assertMarketplaceBinding(binding, source) {
16567
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
16597
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
16568
16598
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
16569
16599
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
16570
16600
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -16573,7 +16603,10 @@ function rebindAgentDiscovery(binding, changes) {
16573
16603
  const budget = discoveryByteBudget();
16574
16604
  return { ...binding, sources: binding.sources.map((source) => {
16575
16605
  assertMarketplaceBinding(binding, source);
16576
- return { ...source, sha256: projected(source, changes, budget) };
16606
+ const sha2562 = projected(source, changes, budget);
16607
+ if (!changes.has(source.path) && sha2562 !== source.sha256)
16608
+ throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
16609
+ return { ...source, sha256: sha2562 };
16577
16610
  }) };
16578
16611
  }
16579
16612
  function captureDiscoveryByteSources(paths) {
@@ -16697,6 +16730,19 @@ function resolveAgentDiscovery(options) {
16697
16730
  }
16698
16731
  }
16699
16732
  }
16733
+ if (options.reviewed === undefined && options.retainedReview !== undefined) {
16734
+ const retained = options.retainedReview;
16735
+ if (retained.agent !== agent || retained.method !== "reviewed")
16736
+ unresolved("invalid retained review");
16737
+ verifyAgentDiscovery(retained);
16738
+ if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
16739
+ unresolved("retained review is missing the full agent configuration source");
16740
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
16741
+ unresolved("retained settings witness names another configuration source");
16742
+ 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))
16743
+ unresolved("retained review is missing current runtime discovery coverage");
16744
+ return retained;
16745
+ }
16700
16746
  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
16747
  throw new Error("Invalid --discovery-inputs version or agents");
16702
16748
  const review = options.reviewed?.agents.find((item) => item.agent === agent);
@@ -16707,7 +16753,7 @@ function resolveAgentDiscovery(options) {
16707
16753
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
16708
16754
  throw new Error("Explicit discovery reviews require full source-file hashes");
16709
16755
  verifyAgentDiscovery(supplied);
16710
- if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
16756
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
16711
16757
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
16712
16758
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
16713
16759
  throw new Error("Discovery review must include the agent configuration source");
@@ -43655,6 +43701,7 @@ export {
43655
43701
  validateCron,
43656
43702
  validateBlogArticleRunOptions,
43657
43703
  uploadSkillsSnapshotFilesToS3,
43704
+ upgradeClaudeSettingsWitness,
43658
43705
  updateSkillRun,
43659
43706
  unsetConfig,
43660
43707
  unpinSkill,
@@ -43833,6 +43880,7 @@ export {
43833
43880
  captureDiscoveryPathSources,
43834
43881
  captureDiscoveryDirectories,
43835
43882
  captureDiscoveryByteSources,
43883
+ captureClaudeSettingsV3,
43836
43884
  captureClaudeSettingsV2,
43837
43885
  captureClaudeSettings,
43838
43886
  captureClaudeMarketplaceRegistryV2,
@@ -6,7 +6,7 @@ export { captureDiscoveryDirectories, type DiscoveryDirectory } from "./agent-di
6
6
  export interface DiscoverySource {
7
7
  path: string;
8
8
  sha256: string | null;
9
- hashMode?: "bytes" | "path-bytes" | "claude-plugin-registry" | "claude-marketplace-registry" | "claude-settings-v1" | "claude-settings-v2" | "claude-marketplace-registry-v2";
9
+ hashMode?: "bytes" | "path-bytes" | "claude-plugin-registry" | "claude-marketplace-registry" | "claude-settings-v1" | "claude-settings-v2" | "claude-settings-v3" | "claude-marketplace-registry-v2";
10
10
  managedPlugins?: ManagedPluginRegistrationWitness[];
11
11
  format?: "json" | "toml" | "yaml";
12
12
  fields?: string[];
@@ -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
@@ -34,3 +34,19 @@ export declare function captureClaudeSettingsV2(path: string, budget?: ClaudeSet
34
34
  sha256: string;
35
35
  };
36
36
  export declare function hashClaudeSettingsReplacementV2(text: string, budget: ClaudeSettingsWitnessBudget): string;
37
+ /** Explicit review of model-independent effort preferences; v1/v2 stay exact. */
38
+ export declare function captureClaudeSettingsV3(path: string, budget?: ClaudeSettingsWitnessBudget): {
39
+ path: string;
40
+ hashMode: "claude-settings-v3";
41
+ sha256: string;
42
+ };
43
+ export declare function hashClaudeSettingsReplacementV3(text: string, budget: ClaudeSettingsWitnessBudget): string;
44
+ /** Upgrade only with an exact semantic preimage of the prior review. This
45
+ * proves every non-preference setting is unchanged; it never refreshes drift.
46
+ * Both files are bounded regular settings.json files, read without symlinks.
47
+ */
48
+ export declare function upgradeClaudeSettingsWitness(previous: {
49
+ path: string;
50
+ hashMode: "claude-settings-v1" | "claude-settings-v2";
51
+ sha256: string;
52
+ }, reviewedSettingsPath: string): ReturnType<typeof captureClaudeSettingsV3>;
@@ -1,4 +1,4 @@
1
- export declare const SUPPORTED_CODEX_HOOK_VERSIONS: readonly ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1"];
1
+ export declare const SUPPORTED_CODEX_HOOK_VERSIONS: readonly ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1", "codex-cli 0.156.1"];
2
2
  export interface CodexHookRpc {
3
3
  version: string;
4
4
  processId?: number;
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.17",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -37545,8 +37545,8 @@ function assertAgentPolicyCollections(policy) {
37545
37545
  text(source.path);
37546
37546
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
37547
37547
  if (source.hashMode !== undefined)
37548
- requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
37549
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
37548
+ requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
37549
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
37550
37550
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "settings.json");
37551
37551
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
37552
37552
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
@@ -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();
@@ -40153,7 +40154,7 @@ function assertClaudeHookEventsReplacement(before, replacement, events) {
40153
40154
  need3(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
40154
40155
  }
40155
40156
  var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
40156
- function inferencePreference(key, child) {
40157
+ function inferencePreference(key, child, version) {
40157
40158
  if (key === "skipDangerousModePermissionPrompt") {
40158
40159
  need3(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
40159
40160
  return false;
@@ -40166,7 +40167,7 @@ function inferencePreference(key, child) {
40166
40167
  return false;
40167
40168
  need3(child.kind === "object", "requires an object of model preferences");
40168
40169
  child.entries = child.entries.filter(([model, preferences]) => {
40169
- if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
40170
+ if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
40170
40171
  return true;
40171
40172
  const effort = preferences.entries[0][1];
40172
40173
  need3(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
@@ -40177,7 +40178,7 @@ function inferencePreference(key, child) {
40177
40178
  function settingsDigest(text2, version = 1) {
40178
40179
  const value = parse6(text2);
40179
40180
  value.entries = value.entries.filter(([key, child]) => {
40180
- if (version === 2 && inferencePreference(key, child))
40181
+ if (version !== 1 && inferencePreference(key, child, version))
40181
40182
  return false;
40182
40183
  if (key === "model") {
40183
40184
  need3(child.kind === "string", "contains an invalid model selection");
@@ -40215,6 +40216,25 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
40215
40216
  budget.remaining -= bytes;
40216
40217
  return settingsDigest(text2, 2);
40217
40218
  }
40219
+ function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
40220
+ return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
40221
+ }
40222
+ function hashClaudeSettingsReplacementV3(text2, budget) {
40223
+ need3(typeof text2 === "string", "requires settings text");
40224
+ const bytes = Buffer.byteLength(text2);
40225
+ need3(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
40226
+ budget.remaining -= bytes;
40227
+ return settingsDigest(text2, 3);
40228
+ }
40229
+ function upgradeClaudeSettingsWitness(previous, reviewedSettingsPath) {
40230
+ need3(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === "claude-settings-v1" || previous.hashMode === "claude-settings-v2") && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact legacy settings witness");
40231
+ const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
40232
+ const before = readSettings(reviewedSettingsPath, budget), current = readSettings(previous.path, budget);
40233
+ need3(settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : 2) === previous.sha256, "reviewed preimage does not match the legacy witness");
40234
+ const sha2562 = settingsDigest(current, 3);
40235
+ need3(settingsDigest(before, 3) === sha2562, "non-preference settings changed; explicit discovery review required");
40236
+ return { path: previous.path, hashMode: "claude-settings-v3", sha256: sha2562 };
40237
+ }
40218
40238
 
40219
40239
  // src/lib/codex-hook-discovery-recovery.ts
40220
40240
  import { createHash as createHash14 } from "crypto";
@@ -40324,13 +40344,13 @@ function read2(path, changes) {
40324
40344
  return readFileSync12(path, "utf8");
40325
40345
  }
40326
40346
  function projected(source, changes, budget = discoveryByteBudget()) {
40327
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
40347
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
40328
40348
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
40329
40349
  throw new Error("Claude settings witnesses require exact typed metadata");
40330
- const current = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
40350
+ const current = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
40331
40351
  if (current !== source.sha256)
40332
40352
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
40333
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
40353
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
40334
40354
  }
40335
40355
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
40336
40356
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
@@ -40392,7 +40412,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
40392
40412
  safe4(root2);
40393
40413
  }
40394
40414
  function assertMarketplaceBinding(binding, source) {
40395
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename6(source.path) !== "settings.json"))
40415
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename6(source.path) !== "settings.json"))
40396
40416
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
40397
40417
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
40398
40418
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -40401,7 +40421,10 @@ function rebindAgentDiscovery(binding, changes) {
40401
40421
  const budget = discoveryByteBudget();
40402
40422
  return { ...binding, sources: binding.sources.map((source) => {
40403
40423
  assertMarketplaceBinding(binding, source);
40404
- return { ...source, sha256: projected(source, changes, budget) };
40424
+ const sha2562 = projected(source, changes, budget);
40425
+ if (!changes.has(source.path) && sha2562 !== source.sha256)
40426
+ throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
40427
+ return { ...source, sha256: sha2562 };
40405
40428
  }) };
40406
40429
  }
40407
40430
  function captureDiscoveryByteSources(paths) {
@@ -40525,6 +40548,19 @@ function resolveAgentDiscovery(options) {
40525
40548
  }
40526
40549
  }
40527
40550
  }
40551
+ if (options.reviewed === undefined && options.retainedReview !== undefined) {
40552
+ const retained = options.retainedReview;
40553
+ if (retained.agent !== agent || retained.method !== "reviewed")
40554
+ unresolved("invalid retained review");
40555
+ verifyAgentDiscovery(retained);
40556
+ if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
40557
+ unresolved("retained review is missing the full agent configuration source");
40558
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
40559
+ unresolved("retained settings witness names another configuration source");
40560
+ 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))
40561
+ unresolved("retained review is missing current runtime discovery coverage");
40562
+ return retained;
40563
+ }
40528
40564
  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
40565
  throw new Error("Invalid --discovery-inputs version or agents");
40530
40566
  const review = options.reviewed?.agents.find((item) => item.agent === agent);
@@ -40535,7 +40571,7 @@ function resolveAgentDiscovery(options) {
40535
40571
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
40536
40572
  throw new Error("Explicit discovery reviews require full source-file hashes");
40537
40573
  verifyAgentDiscovery(supplied);
40538
- if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
40574
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
40539
40575
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
40540
40576
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
40541
40577
  throw new Error("Discovery review must include the agent configuration source");
@@ -40718,7 +40754,7 @@ export default async ({ directory }) => {
40718
40754
  }
40719
40755
 
40720
40756
  // src/lib/agent-codex.ts
40721
- import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
40757
+ import { isDeepStrictEqual as isDeepStrictEqual4 } from "util";
40722
40758
  var CODEX_SKILL_CONFIG_SECTIONS = /^\[\[skills\.config\]\][^\n]*(?:\n(?!\s*\[)[^\n]*)*/gm;
40723
40759
  function assertCodexPathConfigEditable(text2) {
40724
40760
  const before = Bun.TOML.parse(text2);
@@ -40728,7 +40764,7 @@ function assertCodexPathConfigEditable(text2) {
40728
40764
  delete expected.skills.config;
40729
40765
  try {
40730
40766
  const withoutSections = Bun.TOML.parse(text2.replace(CODEX_SKILL_CONFIG_SECTIONS, ""));
40731
- if (isDeepStrictEqual3(withoutSections, expected))
40767
+ if (isDeepStrictEqual4(withoutSections, expected))
40732
40768
  return;
40733
40769
  } catch {}
40734
40770
  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 +40802,7 @@ function disableCodexBundledSkills(text2) {
40766
40802
  const candidates = new Set;
40767
40803
  const consider = (candidate2) => {
40768
40804
  try {
40769
- if (isDeepStrictEqual3(Bun.TOML.parse(candidate2), expected))
40805
+ if (isDeepStrictEqual4(Bun.TOML.parse(candidate2), expected))
40770
40806
  candidates.add(candidate2);
40771
40807
  } catch {}
40772
40808
  };
@@ -41224,7 +41260,15 @@ function planAgentIntegration(options) {
41224
41260
  validateProfile(profileId2);
41225
41261
  return [agent, { command, profileId: profileId2 }];
41226
41262
  }));
41227
- const discoveries = [...new Set(options.agents)].map((agent) => resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, canonical: (path) => canonicalAgentPath(path, aliases) }));
41263
+ const retainedAgents = [];
41264
+ const discoveries = [...new Set(options.agents)].map((agent) => {
41265
+ const retainedReview = options.discoveryInputs === undefined && policy.bridge?.discovery?.[agent]?.method === "reviewed" ? policy.bridge.discovery[agent] : undefined;
41266
+ if (retainedReview) {
41267
+ assertManagedAgentBridge(agent, { home, dataDir: dataDir2, projectDir: options.projectDir ?? home });
41268
+ retainedAgents.push(agent);
41269
+ }
41270
+ return resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, retainedReview, canonical: (path) => canonicalAgentPath(path, aliases) });
41271
+ });
41228
41272
  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
41273
  const changes = [];
41230
41274
  for (const agent of [...new Set(options.agents)]) {
@@ -41309,7 +41353,7 @@ function planAgentIntegration(options) {
41309
41353
  if (JSON.stringify(policy) !== JSON.stringify(nextPolicy))
41310
41354
  changes.push({ path: policyPath, before: previousPolicy, after: serializedPolicy });
41311
41355
  recheckRootAliases(aliases);
41312
- return { dataDir: dataDir2, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {} };
41356
+ 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
41357
  }
41314
41358
  function planClaudeStopHookUpdate(options) {
41315
41359
  return planClaudeHookEventsUpdate({ ...options, events: ["Stop"] });
@@ -41428,6 +41472,8 @@ function applyAgentIntegration(plan) {
41428
41472
  parseManagedSkillPolicy(change.after);
41429
41473
  const aliases = plan.rootAliases ?? [];
41430
41474
  recheckRootAliases(aliases);
41475
+ for (const agent of plan.retainedReviewChecks?.agents ?? [])
41476
+ assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
41431
41477
  for (const agent of plan.managedAgentChecks?.agents ?? [])
41432
41478
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
41433
41479
  for (const binding of plan.discoveryBefore ?? []) {
@@ -289,6 +289,24 @@ Primary runtime references: [command sources](https://code.claude.com/docs/en/pl
289
289
 
290
290
  ### Explicit v2 preference reviews
291
291
 
292
+ For new reviews, use `captureClaudeSettingsV3(path)` or
293
+ `skills hook witness --kind claude-settings-v3 --path <settings.json> --json`.
294
+ The v3 witness recognizes an entry containing only a valid `effortLevel`
295
+ independently of its model name. Claude can persist a preference for a newly
296
+ released model without blocking the next prompt. Entries with additional
297
+ properties, including `maxEffortLevel`, remain fully bound, as do provider
298
+ mappings, hooks, permissions, plugins, environment and unknown settings.
299
+ The top-level model selection rules remain unchanged.
300
+
301
+ Legacy v1 and v2 digests retain their meaning. `upgradeClaudeSettingsWitness`
302
+ accepts a legacy witness and a saved `settings.json` preimage. It verifies that
303
+ the preimage matches the old digest and that current settings differ only in
304
+ recognized preferences before returning a v3 witness. It refuses all other
305
+ drift. Use that witness in reviewed discovery inputs and apply the normal
306
+ guarded integration plan. Install compatible runtimes before activating v3;
307
+ when updating agents, verify their actual hook commands and native trust, not
308
+ only the executable found on `PATH`.
309
+
292
310
  `captureClaudeSettingsV2(path)` creates a `claude-settings-v2` witness. In
293
311
  addition to the v1 display/model selections, it tolerates the documented
294
312
  persisted effort values `low`, `medium`, `high`, and `xhigh`, both at the top
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.9.15",
3
+ "version": "0.9.17",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {