@hasna/skills 0.9.12 → 0.9.14

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/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.12",
32376
+ version: "0.9.14",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -37545,10 +37545,10 @@ 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"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
37549
- if (source.hashMode === "claude-settings-v1")
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")
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
- if (source.hashMode === "claude-marketplace-registry")
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");
37553
37553
  if (source.hashMode === "claude-plugin-registry") {
37554
37554
  requireBound(agent === "claude");
@@ -39895,9 +39895,13 @@ function textField(value) {
39895
39895
  need2(value?.kind === "string" && value.value.length > 0 && !/[\x00-\x1f\x7f]/.test(value.value), "contains an invalid recognized field");
39896
39896
  return value.value;
39897
39897
  }
39898
- function normalizeRow(row) {
39899
- if (!exactKeys2(row, ["source", "installLocation", "lastUpdated"]))
39900
- return;
39898
+ function normalizeRow(row, version = 1) {
39899
+ if (!exactKeys2(row, ["source", "installLocation", "lastUpdated"])) {
39900
+ if (version !== 2 || !exactKeys2(row, ["source", "installLocation", "lastUpdated", "autoUpdate"]))
39901
+ return;
39902
+ const autoUpdate = field(row, "autoUpdate");
39903
+ need2(autoUpdate?.kind === "literal" && (autoUpdate.value === "true" || autoUpdate.value === "false"), "contains an invalid automatic-update preference");
39904
+ }
39901
39905
  const source = field(row, "source");
39902
39906
  if (source?.kind !== "object")
39903
39907
  return;
@@ -39924,6 +39928,13 @@ function captureClaudeMarketplaceRegistry(path, budget = { remaining: 256 * 1024
39924
39928
  const sha2562 = createHash11("sha256").update("hasna.skills.claude-marketplace-registry.v1\x00").update(JSON.stringify(value)).digest("hex");
39925
39929
  return { path, hashMode: "claude-marketplace-registry", sha256: sha2562 };
39926
39930
  }
39931
+ function captureClaudeMarketplaceRegistryV2(path, budget = { remaining: 256 * 1024 * 1024 }) {
39932
+ const value = parse5(readRegistry2(path, budget));
39933
+ for (const [, row] of value.entries)
39934
+ normalizeRow(row, 2);
39935
+ const sha2562 = createHash11("sha256").update("hasna.skills.claude-marketplace-registry.v2\x00").update(JSON.stringify(value)).digest("hex");
39936
+ return { path, hashMode: "claude-marketplace-registry-v2", sha256: sha2562 };
39937
+ }
39927
39938
 
39928
39939
  // src/lib/claude-settings-witness.ts
39929
39940
  import { createHash as createHash12 } from "crypto";
@@ -40115,30 +40126,59 @@ function canonical3(value) {
40115
40126
  return { kind: "array", items: value.items.map(canonical3) };
40116
40127
  return value;
40117
40128
  }
40118
- function assertClaudeStopHookReplacement(before, replacement) {
40129
+ var CLAUDE_COORDINATED_HOOK_EVENTS = ["PreToolUse", "PostToolUse", "Stop", "Notification", "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart"];
40130
+ function assertClaudeHookEventsReplacement(before, replacement, events) {
40131
+ need3(Array.isArray(events) && events.length > 0 && events.length <= CLAUDE_COORDINATED_HOOK_EVENTS.length && new Set(events).size === events.length && events.every((event) => CLAUDE_COORDINATED_HOOK_EVENTS.includes(event)), "requires explicit supported hook events");
40132
+ const label = events.join(",");
40119
40133
  function bounded(text2) {
40120
- need3(typeof text2 === "string" && Buffer.byteLength(text2) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "Stop update exceeds its byte limit");
40134
+ need3(typeof text2 === "string" && Buffer.byteLength(text2) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, `${label} update exceeds its byte limit`);
40121
40135
  return parse6(text2);
40122
40136
  }
40123
40137
  const original = bounded(before ?? "{}"), next = bounded(replacement);
40124
40138
  const hadHooks = original.entries.some(([key]) => key === "hooks");
40125
- function withoutStop(value) {
40139
+ function withoutOwnedEvents(value) {
40126
40140
  const hooks = value.entries.find(([key]) => key === "hooks")?.[1];
40127
40141
  if (hooks !== undefined) {
40128
- need3(hooks.kind === "object", "Stop update requires an object of hooks");
40129
- const stop = hooks.entries.find(([key]) => key === "Stop")?.[1];
40130
- need3(stop === undefined || stop.kind === "array", "Stop update requires a Stop array");
40131
- hooks.entries = hooks.entries.filter(([key]) => key !== "Stop");
40142
+ need3(hooks.kind === "object", `${label} update requires an object of hooks`);
40143
+ for (const event of events) {
40144
+ const selected = hooks.entries.find(([key]) => key === event)?.[1];
40145
+ need3(selected === undefined || selected.kind === "array", `${event} update requires a ${event} array`);
40146
+ }
40147
+ hooks.entries = hooks.entries.filter(([key]) => !events.includes(key));
40132
40148
  if (!hadHooks && hooks.entries.length === 0)
40133
40149
  value.entries = value.entries.filter(([key]) => key !== "hooks");
40134
40150
  }
40135
40151
  return canonical3(value);
40136
40152
  }
40137
- need3(JSON.stringify(withoutStop(original)) === JSON.stringify(withoutStop(next)), "Stop update changed settings outside hooks.Stop");
40153
+ need3(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
40154
+ }
40155
+ var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
40156
+ function inferencePreference(key, child) {
40157
+ if (key === "skipDangerousModePermissionPrompt") {
40158
+ need3(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
40159
+ return false;
40160
+ }
40161
+ if (key === "effortLevel") {
40162
+ need3(child.kind === "string" && PERSISTED_EFFORT_LEVELS.has(child.value), "contains an invalid persisted effort level");
40163
+ return true;
40164
+ }
40165
+ if (key !== "modelSettings")
40166
+ return false;
40167
+ need3(child.kind === "object", "requires an object of model preferences");
40168
+ 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
+ return true;
40171
+ const effort = preferences.entries[0][1];
40172
+ need3(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
40173
+ return false;
40174
+ });
40175
+ return child.entries.length === 0;
40138
40176
  }
40139
- function settingsDigest(text2) {
40177
+ function settingsDigest(text2, version = 1) {
40140
40178
  const value = parse6(text2);
40141
40179
  value.entries = value.entries.filter(([key, child]) => {
40180
+ if (version === 2 && inferencePreference(key, child))
40181
+ return false;
40142
40182
  if (key === "model") {
40143
40183
  need3(child.kind === "string", "contains an invalid model selection");
40144
40184
  return !BUILTIN_MODEL_SELECTIONS.has(child.value);
@@ -40153,7 +40193,7 @@ function settingsDigest(text2) {
40153
40193
  }
40154
40194
  return true;
40155
40195
  });
40156
- return createHash12("sha256").update("hasna.skills.claude-settings.v1\x00").update(JSON.stringify(canonical3(value))).digest("hex");
40196
+ return createHash12("sha256").update(`hasna.skills.claude-settings.v${version}\x00`).update(JSON.stringify(canonical3(value))).digest("hex");
40157
40197
  }
40158
40198
  function hashClaudeSettingsReplacement(text2, budget) {
40159
40199
  need3(typeof text2 === "string", "requires settings text");
@@ -40165,6 +40205,16 @@ function hashClaudeSettingsReplacement(text2, budget) {
40165
40205
  function captureClaudeSettings(path, budget = { remaining: 256 * 1024 * 1024 }) {
40166
40206
  return { path, hashMode: "claude-settings-v1", sha256: settingsDigest(readSettings(path, budget)) };
40167
40207
  }
40208
+ function captureClaudeSettingsV2(path, budget = { remaining: 256 * 1024 * 1024 }) {
40209
+ return { path, hashMode: "claude-settings-v2", sha256: settingsDigest(readSettings(path, budget), 2) };
40210
+ }
40211
+ function hashClaudeSettingsReplacementV2(text2, budget) {
40212
+ need3(typeof text2 === "string", "requires settings text");
40213
+ const bytes = Buffer.byteLength(text2);
40214
+ need3(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
40215
+ budget.remaining -= bytes;
40216
+ return settingsDigest(text2, 2);
40217
+ }
40168
40218
 
40169
40219
  // src/lib/codex-hook-discovery-recovery.ts
40170
40220
  import { createHash as createHash14 } from "crypto";
@@ -40268,18 +40318,18 @@ function read2(path, changes) {
40268
40318
  return readFileSync12(path, "utf8");
40269
40319
  }
40270
40320
  function projected(source, changes, budget = discoveryByteBudget()) {
40271
- if (source.hashMode === "claude-settings-v1") {
40321
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
40272
40322
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
40273
40323
  throw new Error("Claude settings witnesses require exact typed metadata");
40274
- const current = captureClaudeSettings(source.path, budget).sha256;
40324
+ const current = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
40275
40325
  if (current !== source.sha256)
40276
40326
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
40277
- return changes?.has(source.path) ? hashClaudeSettingsReplacement(changes.get(source.path), budget) : current;
40327
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
40278
40328
  }
40279
- if (source.hashMode === "claude-marketplace-registry") {
40329
+ if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
40280
40330
  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))
40281
40331
  throw new Error("Claude marketplace witnesses require an exact reviewed registry without synthetic changes");
40282
- const sha2562 = captureClaudeMarketplaceRegistry(source.path, budget).sha256;
40332
+ const sha2562 = (source.hashMode === "claude-marketplace-registry-v2" ? captureClaudeMarketplaceRegistryV2 : captureClaudeMarketplaceRegistry)(source.path, budget).sha256;
40283
40333
  if (sha2562 !== source.sha256)
40284
40334
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
40285
40335
  return sha2562;
@@ -40336,9 +40386,9 @@ function verifyAgentDiscovery(binding, codexRecovery) {
40336
40386
  safe4(root2);
40337
40387
  }
40338
40388
  function assertMarketplaceBinding(binding, source) {
40339
- if (source.hashMode === "claude-settings-v1" && (binding.agent !== "claude" || binding.method !== "reviewed" || basename6(source.path) !== "settings.json"))
40389
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename6(source.path) !== "settings.json"))
40340
40390
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
40341
- if (source.hashMode === "claude-marketplace-registry" && (binding.agent !== "claude" || binding.method !== "reviewed"))
40391
+ if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
40342
40392
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
40343
40393
  }
40344
40394
  function rebindAgentDiscovery(binding, changes) {
@@ -40479,7 +40529,7 @@ function resolveAgentDiscovery(options) {
40479
40529
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
40480
40530
  throw new Error("Explicit discovery reviews require full source-file hashes");
40481
40531
  verifyAgentDiscovery(supplied);
40482
- if (review.sources.some((source) => source.hashMode === "claude-settings-v1" && source.path !== canonical4(configPath)))
40532
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
40483
40533
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
40484
40534
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
40485
40535
  throw new Error("Discovery review must include the agent configuration source");
@@ -41256,6 +41306,34 @@ function planAgentIntegration(options) {
41256
41306
  return { dataDir: dataDir2, profileId, changes, nativeSkills, observedPolicy: { path: policyPath, before: previousPolicy }, discoveryBefore: discoveries, discoveryAfter, ...aliases.length ? { rootAliases: aliases } : {} };
41257
41307
  }
41258
41308
  function planClaudeStopHookUpdate(options) {
41309
+ return planClaudeHookEventsUpdate({ ...options, events: ["Stop"] });
41310
+ }
41311
+ function planClaudePreToolUseHookUpdate(options) {
41312
+ return planClaudeHookEventsUpdate({ ...options, events: ["PreToolUse"] });
41313
+ }
41314
+ function claudeSettingsConsumers(home, path, discoveries, aliases) {
41315
+ const canonical4 = (value) => canonicalAgentPath(value, aliases);
41316
+ const containsSettings = (root2) => {
41317
+ const distance = relative3(canonical4(root2), path);
41318
+ return distance === "" || distance !== ".." && !distance.startsWith(`..${sep4}`) && !isAbsolute15(distance);
41319
+ };
41320
+ return discoveries.filter((binding) => binding.agent === "claude" || binding.agent === "opencode" || canonical4(join21(home, AGENT_ADAPTERS[binding.agent].config)) === path || binding.sources.some((source) => canonical4(source.path) === path) || binding.roots.some(containsSettings) || binding.directories?.some((directory) => containsSettings(directory.path))).map((binding) => binding.agent);
41321
+ }
41322
+ function verifyCoordinatedDiscovery(binding, home, aliases) {
41323
+ try {
41324
+ if (binding.method !== "automatic" && binding.method !== "reviewed")
41325
+ throw new Error("Invalid managed discovery method");
41326
+ verifyAgentDiscovery(binding);
41327
+ if (binding.method === "automatic") {
41328
+ const current = resolveAgentDiscovery({ home, agent: binding.agent, canonical: (path) => canonicalAgentPath(path, aliases) });
41329
+ if (JSON.stringify(current) !== JSON.stringify(binding))
41330
+ throw new Error("Configured native discovery roots changed during Claude update");
41331
+ }
41332
+ } catch (error) {
41333
+ throw new Error(`NATIVE_SKILL_DRIFT: ${error.message}`);
41334
+ }
41335
+ }
41336
+ function planClaudeHookEventsUpdate(options) {
41259
41337
  const home = resolve14(options.home ?? homedir3()), dataDir2 = options.dataDir ?? getDataDirReadOnly();
41260
41338
  const policyPath = join21(dataDir2, "agent-policy.json");
41261
41339
  assertSafePath(policyPath);
@@ -41273,31 +41351,42 @@ function planClaudeStopHookUpdate(options) {
41273
41351
  const before = lstatSync18(path, { throwIfNoEntry: false }) ? new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readNativeBytes(path, 1024 * 1024)) : null;
41274
41352
  if ((before === null ? "absent" : sha2(before)) !== options.expectedSettingsSha256)
41275
41353
  throw new Error("Claude settings preimage changed");
41276
- assertClaudeStopHookReplacement(before, options.replacement);
41354
+ assertClaudeHookEventsReplacement(before, options.replacement, options.events);
41277
41355
  const agents = bridge.agents;
41278
41356
  const discoveries = Object.entries(bridge.discovery).map(([agent, binding]) => {
41279
- if (!agents.includes(agent) || binding?.agent !== agent)
41280
- throw new Error("Invalid managed discovery owner");
41357
+ if (!agents.includes(agent) || binding?.agent !== agent || typeof bridge.commands?.[agent] !== "string" || typeof bridge.profiles?.[agent] !== "string")
41358
+ throw new Error("Invalid managed discovery owner or command/profile binding");
41281
41359
  return binding;
41282
41360
  });
41283
- if (discoveries.length !== agents.length)
41284
- throw new Error("Missing managed discovery coverage");
41285
- for (const agent of agents)
41361
+ if (discoveries.length !== agents.length || !agents.includes("claude"))
41362
+ throw new Error("Missing managed Claude discovery coverage");
41363
+ for (const binding of discoveries)
41364
+ verifyCoordinatedDiscovery(binding, home, aliases);
41365
+ const consumers = claudeSettingsConsumers(home, path, discoveries, aliases);
41366
+ for (const agent of consumers)
41286
41367
  assertManagedAgentBridge(agent, { home, dataDir: dataDir2, projectDir: home });
41287
41368
  const replacements = new Map(before === options.replacement ? [] : [[path, options.replacement]]);
41288
- const discoveryAfter = discoveries.map((binding) => rebindAgentDiscovery(binding, replacements));
41369
+ const discoveryAfter = discoveries.map((binding) => {
41370
+ const next = rebindAgentDiscovery(binding, replacements);
41371
+ for (const [index, source] of binding.sources.entries()) {
41372
+ if (source.path !== path && JSON.stringify(next.sources[index]) !== JSON.stringify(source)) {
41373
+ throw new Error(`Unrelated native discovery input changed during Claude update: ${source.path}`);
41374
+ }
41375
+ }
41376
+ return JSON.stringify(next) === JSON.stringify(binding) ? binding : next;
41377
+ });
41289
41378
  const nextPolicy = { ...policy, bridge: { ...bridge, discovery: Object.fromEntries(discoveryAfter.map((binding) => [binding.agent, binding])) } };
41290
41379
  const changes = before === options.replacement ? [] : [{ path, before, after: options.replacement }];
41291
41380
  if (JSON.stringify(nextPolicy) !== JSON.stringify(policy))
41292
41381
  changes.push({ path: policyPath, before: snapshot2.text, after: serializeManagedSkillPolicy(nextPolicy) });
41293
41382
  recheckRootAliases(aliases);
41294
41383
  const nativeSkills = inventoryNativeSkills(home, {
41295
- agents,
41384
+ agents: consumers,
41296
41385
  projectDir: home,
41297
41386
  includeVendor: true,
41298
- guardHermes: agents.includes("hermes"),
41387
+ guardHermes: consumers.includes("hermes"),
41299
41388
  allowRootAliases: aliases.length > 0,
41300
- agentRoots: discoveries.flatMap((binding) => binding.roots.map((path2) => ({ agent: binding.agent, path: path2 })))
41389
+ agentRoots: discoveries.filter((binding) => consumers.includes(binding.agent)).flatMap((binding) => binding.roots.map((path2) => ({ agent: binding.agent, path: path2 })))
41301
41390
  });
41302
41391
  return {
41303
41392
  dataDir: dataDir2,
@@ -41308,7 +41397,7 @@ function planClaudeStopHookUpdate(options) {
41308
41397
  discoveryBefore: discoveries,
41309
41398
  discoveryAfter,
41310
41399
  rootAliases: aliases,
41311
- managedAgentChecks: { home, agents: [...agents] }
41400
+ managedAgentChecks: { home, agents: consumers }
41312
41401
  };
41313
41402
  }
41314
41403
  function atomicWrite2(path, content) {
@@ -41335,8 +41424,12 @@ function applyAgentIntegration(plan) {
41335
41424
  recheckRootAliases(aliases);
41336
41425
  for (const agent of plan.managedAgentChecks?.agents ?? [])
41337
41426
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
41338
- for (const binding of plan.discoveryBefore ?? [])
41339
- verifyAgentDiscovery(binding);
41427
+ for (const binding of plan.discoveryBefore ?? []) {
41428
+ if (plan.managedAgentChecks)
41429
+ verifyCoordinatedDiscovery(binding, plan.managedAgentChecks.home, aliases);
41430
+ else
41431
+ verifyAgentDiscovery(binding);
41432
+ }
41340
41433
  if (plan.observedPolicy && currentText(plan.observedPolicy.path) !== plan.observedPolicy.before)
41341
41434
  throw new Error("Agent policy changed after planning");
41342
41435
  for (const change of plan.changes)
@@ -41368,8 +41461,12 @@ function applyAgentIntegration(plan) {
41368
41461
  written.push({ path: change.path, before: change.before, after });
41369
41462
  }
41370
41463
  recheckRootAliases(aliases);
41371
- for (const binding of plan.discoveryAfter ?? [])
41372
- verifyAgentDiscovery(binding);
41464
+ for (const binding of plan.discoveryAfter ?? []) {
41465
+ if (plan.managedAgentChecks)
41466
+ verifyCoordinatedDiscovery(binding, plan.managedAgentChecks.home, aliases);
41467
+ else
41468
+ verifyAgentDiscovery(binding);
41469
+ }
41373
41470
  for (const agent of plan.managedAgentChecks?.agents ?? [])
41374
41471
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
41375
41472
  atomicWrite2(join21(backupRoot, "receipt.json"), JSON.stringify({ version: 1, changes: written.map((change) => ({ path: change.path, beforeHash: change.before === null ? null : sha2(change.before), afterHash: sha2(change.after) })), backups, ...aliases.length ? { rootAliases: aliases } : {} }) + `
@@ -55561,6 +55658,18 @@ function rowToSkill(row) {
55561
55658
  ...row.replacement_slug ? { replacementSlug: String(row.replacement_slug) } : {}
55562
55659
  };
55563
55660
  }
55661
+ function rowToPublishedSelectionState(row) {
55662
+ return {
55663
+ slug: String(row.slug),
55664
+ version: String(row.version),
55665
+ current: row.skill_exists ? {
55666
+ lifecycle: row.lifecycle === "archived" ? "archived" : "active",
55667
+ ...row.tombstoned_at ? { tombstonedAt: dateString(row.tombstoned_at) } : {}
55668
+ } : null,
55669
+ versionBundleSha256: typeof row.bundle_sha256 === "string" ? row.bundle_sha256 : null,
55670
+ bundleAvailable: Boolean(row.bundle_available)
55671
+ };
55672
+ }
55564
55673
  function rowToPin(row) {
55565
55674
  return {
55566
55675
  orgId: String(row.org_id),
@@ -56335,6 +56444,22 @@ class SqliteSkillsStore {
56335
56444
  const row = this.get("SELECT * FROM skills_versions WHERE org_id = ? AND slug = ? AND version = ? LIMIT 1", [principal.orgId, slug, version2]);
56336
56445
  return row ? rowToSkillVersion(row) : null;
56337
56446
  }
56447
+ async getPublishedSelectionStates(principal, selections2) {
56448
+ if (selections2.length > MAX_PROFILE_SELECTIONS)
56449
+ throw new RangeError("Published selection lookup exceeds the profile selection limit");
56450
+ if (!selections2.length)
56451
+ return [];
56452
+ const requested = JSON.stringify(selections2.map(({ slug, version: version2 }) => ({ slug, version: version2 })));
56453
+ return this.all(`SELECT json_extract(q.value, '$.slug') AS slug, json_extract(q.value, '$.version') AS version,
56454
+ r.slug IS NOT NULL AS skill_exists, r.lifecycle, r.tombstoned_at,
56455
+ v.bundle_sha256, b.sha256 IS NOT NULL AS bundle_available
56456
+ FROM json_each(?) q
56457
+ LEFT JOIN skills_registry r ON r.org_id = ? AND r.slug = json_extract(q.value, '$.slug')
56458
+ LEFT JOIN skills_versions v ON v.org_id = ?
56459
+ AND v.slug = json_extract(q.value, '$.slug') AND v.version = json_extract(q.value, '$.version')
56460
+ LEFT JOIN skills_bundles b ON b.org_id = ? AND b.sha256 = v.bundle_sha256
56461
+ ORDER BY CAST(q.key AS INTEGER)`, [requested, principal.orgId, principal.orgId, principal.orgId]).map(rowToPublishedSelectionState);
56462
+ }
56338
56463
  async pinSkill(principal, slug, metadata = {}) {
56339
56464
  const row = this.get(`INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
56340
56465
  VALUES (?, ?, ?, ?, ?)
@@ -57463,16 +57588,21 @@ function selected(value) {
57463
57588
  return selections2;
57464
57589
  }
57465
57590
  async function validatePublished(store, principal, selections2) {
57466
- for (const selection2 of selections2) {
57467
- const current = await store.getSkill(principal, selection2.slug);
57591
+ const states = await store.getPublishedSelectionStates(principal, selections2);
57592
+ if (states.length !== selections2.length)
57593
+ throw new SkillRequestError(503, "SELECTION_UNAVAILABLE", "Selected publication state is unavailable");
57594
+ for (const [index, selection2] of selections2.entries()) {
57595
+ const state2 = states[index];
57596
+ if (!state2 || state2.slug !== selection2.slug || state2.version !== selection2.version)
57597
+ throw new SkillRequestError(503, "SELECTION_UNAVAILABLE", "Selected publication state is unavailable");
57598
+ const current = state2.current;
57468
57599
  if (!current || current.tombstonedAt)
57469
57600
  throw new SkillRequestError(current?.tombstonedAt ? 410 : 404, "SELECTION_UNAVAILABLE", `Selected skill '${selection2.slug}' is unavailable`);
57470
57601
  if (current.lifecycle === "archived")
57471
57602
  throw new SkillRequestError(409, "SELECTION_ARCHIVED", `Selected skill '${selection2.slug}' is archived`);
57472
- const version2 = await store.getSkillVersion(principal, selection2.slug, selection2.version);
57473
- if (!version2 || `sha256:${version2.bundleSha256}` !== selection2.bundleDigest)
57603
+ if (!state2.versionBundleSha256 || `sha256:${state2.versionBundleSha256}` !== selection2.bundleDigest)
57474
57604
  throw new SkillRequestError(409, "SELECTION_VERSION_MISMATCH", `Selected version of '${selection2.slug}' does not match its published digest`);
57475
- if (!await store.getSkillBundle(principal, version2.bundleSha256))
57605
+ if (!state2.bundleAvailable)
57476
57606
  throw new SkillRequestError(503, "SELECTION_BUNDLE_UNAVAILABLE", "Selected bundle is unavailable");
57477
57607
  }
57478
57608
  }
@@ -63361,6 +63491,25 @@ class MemorySkillsStore {
63361
63491
  const found = this.versions.get(versionKey(principal.orgId, slug, version2));
63362
63492
  return found && found.orgId === principal.orgId ? found : null;
63363
63493
  }
63494
+ async getPublishedSelectionStates(principal, selections2) {
63495
+ if (selections2.length > MAX_PROFILE_SELECTIONS)
63496
+ throw new RangeError("Published selection lookup exceeds the profile selection limit");
63497
+ return selections2.map(({ slug, version: version2 }) => {
63498
+ const current = this.skills.get(skillKey(principal.orgId, slug));
63499
+ const selectedVersion = this.versions.get(versionKey(principal.orgId, slug, version2));
63500
+ const bundle2 = selectedVersion && this.bundles.get(skillKey(principal.orgId, selectedVersion.bundleSha256));
63501
+ return {
63502
+ slug,
63503
+ version: version2,
63504
+ current: current?.orgId === principal.orgId ? {
63505
+ lifecycle: current.lifecycle,
63506
+ ...current.tombstonedAt ? { tombstonedAt: current.tombstonedAt } : {}
63507
+ } : null,
63508
+ versionBundleSha256: selectedVersion?.orgId === principal.orgId ? selectedVersion.bundleSha256 : null,
63509
+ bundleAvailable: bundle2?.orgId === principal.orgId
63510
+ };
63511
+ });
63512
+ }
63364
63513
  async listSkills(principal) {
63365
63514
  return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).sort((a4, b4) => a4.slug.localeCompare(b4.slug));
63366
63515
  }
@@ -64169,6 +64318,25 @@ class PostgresSkillsStore {
64169
64318
  `;
64170
64319
  return rows[0] ? rowToSkillVersion(rows[0]) : null;
64171
64320
  }
64321
+ async getPublishedSelectionStates(principal, selections2) {
64322
+ if (selections2.length > MAX_PROFILE_SELECTIONS)
64323
+ throw new RangeError("Published selection lookup exceeds the profile selection limit");
64324
+ if (!selections2.length)
64325
+ return [];
64326
+ const requested = JSON.stringify(selections2.map(({ slug, version: version2 }) => ({ slug, version: version2 })));
64327
+ const rows = await this.sql`
64328
+ SELECT q.value->>'slug' AS slug, q.value->>'version' AS version,
64329
+ r.slug IS NOT NULL AS skill_exists, r.lifecycle, r.tombstoned_at,
64330
+ v.bundle_sha256, b.sha256 IS NOT NULL AS bundle_available
64331
+ FROM jsonb_array_elements(${requested}::text::jsonb) WITH ORDINALITY AS q(value, ordinal)
64332
+ LEFT JOIN skills_registry r ON r.org_id = ${principal.orgId} AND r.slug = q.value->>'slug'
64333
+ LEFT JOIN skills_versions v ON v.org_id = ${principal.orgId}
64334
+ AND v.slug = q.value->>'slug' AND v.version = q.value->>'version'
64335
+ LEFT JOIN skills_bundles b ON b.org_id = ${principal.orgId} AND b.sha256 = v.bundle_sha256
64336
+ ORDER BY q.ordinal
64337
+ `;
64338
+ return rows.map(rowToPublishedSelectionState);
64339
+ }
64172
64340
  async pinSkill(principal, slug, metadata = {}) {
64173
64341
  const rows = await this.sql`
64174
64342
  INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
@@ -1,4 +1,4 @@
1
- import type { ServerSkillVersion, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord } from "./types.js";
1
+ import type { ServerSkillVersion, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, PublishedSkillSelectionState } from "./types.js";
2
2
  export declare function nowIso(): string;
3
3
  /**
4
4
  * Coerce a caller-supplied row limit to a non-negative integer.
@@ -17,6 +17,7 @@ export declare function rowToRun(row: Record<string, unknown>): ServerRunRecord;
17
17
  export declare function rowToLog(row: Record<string, unknown>): ServerRunLog;
18
18
  export declare function rowToArtifact(row: Record<string, unknown>): ServerArtifact;
19
19
  export declare function rowToSkill(row: Record<string, unknown>): ServerSkillRecord;
20
+ export declare function rowToPublishedSelectionState(row: Record<string, unknown>): PublishedSkillSelectionState;
20
21
  export declare function rowToPin(row: Record<string, unknown>): ServerPin;
21
22
  export declare function rowToSkillBundle(row: Record<string, unknown>): ServerSkillBundle;
22
23
  export declare function parseJsonObject(value: unknown): Record<string, unknown>;
@@ -15,7 +15,7 @@ import { SqliteExecutionGrantStore } from "./execution-grant-store.js";
15
15
  */
16
16
  import { Database } from "bun:sqlite";
17
17
  import { SqliteSkillSelectionStore } from "./selection-store.js";
18
- import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, ServerSkillRecord, SkillLifecyclePatch, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
18
+ import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, PublishedSkillSelection, PublishedSkillSelectionState, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, ServerSkillRecord, SkillLifecyclePatch, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
19
19
  export interface SqliteStoreOptions {
20
20
  /** Apply pending migrations on open. Default true - it is what makes zero-config work. */
21
21
  migrate?: boolean;
@@ -113,6 +113,7 @@ export declare class SqliteSkillsStore implements SkillsProductStore {
113
113
  getSkillBundle(principal: ApiPrincipal, sha256: string): Promise<ServerSkillBundle | null>;
114
114
  listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
115
115
  getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
116
+ getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
116
117
  pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
117
118
  unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
118
119
  listPins(principal: ApiPrincipal): Promise<ServerPin[]>;
@@ -1,6 +1,6 @@
1
1
  import { MemoryExecutionGrantStore, PostgresExecutionGrantStore } from "./execution-grant-store.js";
2
2
  import { MemorySkillSelectionStore, PostgresSkillSelectionStore } from "./selection-store.js";
3
- import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
3
+ import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, PublishedSkillSelection, PublishedSkillSelectionState, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
4
4
  import { type SkillLifecyclePatch } from "./types.js";
5
5
  import { type SqliteStoreOptions } from "./sqlite-store.js";
6
6
  export declare function createArtifactId(): string;
@@ -79,6 +79,7 @@ export declare class MemorySkillsStore implements SkillsProductStore {
79
79
  publishSkill(input: PublishSkillInput): Promise<ServerSkillRecord>;
80
80
  listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
81
81
  getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
82
+ getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
82
83
  listSkills(principal: ApiPrincipal): Promise<ServerSkillRecord[]>;
83
84
  getSkill(principal: ApiPrincipal, slug: string): Promise<ServerSkillRecord | null>;
84
85
  setSkillLifecycle(principal: ApiPrincipal, slug: string, patch: SkillLifecyclePatch, expectedRevisionId?: string): Promise<ServerSkillRecord | null>;
@@ -180,6 +181,7 @@ export declare class PostgresSkillsStore implements SkillsProductStore {
180
181
  getSkillBundle(principal: ApiPrincipal, sha256: string): Promise<ServerSkillBundle | null>;
181
182
  listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
182
183
  getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
184
+ getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
183
185
  pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
184
186
  unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
185
187
  listPins(principal: ApiPrincipal): Promise<ServerPin[]>;
@@ -279,6 +279,17 @@ export interface ServerSkillVersion {
279
279
  publishedByUserId?: string;
280
280
  createdAt: string;
281
281
  }
282
+ /** Exact version lookup, independent of the registry's mutable current version. */
283
+ export interface PublishedSkillSelection {
284
+ slug: string;
285
+ version: string;
286
+ }
287
+ /** Metadata only: bundle availability means its tenant-owned storage row exists. */
288
+ export interface PublishedSkillSelectionState extends PublishedSkillSelection {
289
+ current: Pick<ServerSkillRecord, "lifecycle" | "tombstonedAt"> | null;
290
+ versionBundleSha256: string | null;
291
+ bundleAvailable: boolean;
292
+ }
282
293
  export interface PublishSkillInput {
283
294
  /** Internal boot-seed precondition: never revive or replace non-bundled records. */
284
295
  seedBundledOnly?: true;
@@ -441,6 +452,13 @@ export interface SkillsProductStore {
441
452
  /** Every published version of a slug, newest first (hasna/apps#1630). */
442
453
  listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
443
454
  getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
455
+ /**
456
+ * Read at most MAX_PROFILE_SELECTIONS exact selections in one database snapshot,
457
+ * scoped to the principal's org at every join. Returns one row per input in input
458
+ * order, including missing selections and duplicates. Never reads bundle bytes or
459
+ * substitutes the latest version; callers retain lifecycle/digest error policy.
460
+ */
461
+ getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
444
462
  pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
445
463
  /** False when this principal has no pin by that slug. */
446
464
  unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
@@ -286,3 +286,63 @@ cache history is bounded to 128 entries and an aggregate 256 MiB per witness.
286
286
  Primary runtime references: [command sources](https://code.claude.com/docs/en/plugin-marketplaces#command-sources),
287
287
  [plugin components](https://code.claude.com/docs/en/plugins-reference), and
288
288
  [exact command acceptance](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md#21271).
289
+
290
+ ### Explicit v2 preference reviews
291
+
292
+ `captureClaudeSettingsV2(path)` creates a `claude-settings-v2` witness. In
293
+ addition to the v1 display/model selections, it tolerates the documented
294
+ persisted effort values `low`, `medium`, `high`, and `xhigh`, both at the top
295
+ level and in known built-in model entries whose only property is `effortLevel`.
296
+ Custom model entries and entries with any other property remain fully bound.
297
+ The boolean `skipDangerousModePermissionPrompt` remains bound: Claude also uses
298
+ this saved acceptance in bypass-mode and pending project MCP approval decisions.
299
+ Permission settings, plugins, hooks, environment, ultracode and unknown
300
+ configuration remain bound. Version 0.9.14 corrects the 0.9.13 normalization of
301
+ this flag; an existing v2 review containing it must be explicitly reviewed again.
302
+ Invalid recognized preference types are refused.
303
+
304
+ `captureClaudeMarketplaceRegistryV2(path)` creates a
305
+ `claude-marketplace-registry-v2` witness. It also recognizes a registry row with
306
+ an optional boolean `autoUpdate`. That boolean remains bound; only a strictly
307
+ validated `lastUpdated` timestamp may vary. A new source or installation path,
308
+ a changed automatic-update policy, or an unknown field still requires review.
309
+
310
+ Capture either witness through the CLI:
311
+
312
+ ```sh
313
+ skills hook witness --kind claude-settings-v2 --path /canonical/home/.claude/settings.json --json
314
+ skills hook witness --kind claude-marketplace-registry-v2 --path /canonical/home/.claude/plugins/known_marketplaces.json --json
315
+ ```
316
+
317
+ Capture does not install or authorize a review. Compare the current complete
318
+ configuration with its previous review, put the chosen witness in the reviewed
319
+ discovery inputs, and preview `skills hook install --discovery-inputs <file>`
320
+ before applying. Existing byte and v1 witnesses are never silently converted.
321
+ Runtime support for the selected witness version must be installed before
322
+ policy activation. A real hook installation is a configuration change, not a
323
+ preference: its owning installer must coordinate a guarded settings/policy
324
+ update, or it needs a fresh explicit review.
325
+
326
+ The effort schema follows [Claude's model configuration documentation](https://code.claude.com/docs/en/model-config#adjust-effort-level).
327
+
328
+ Cooperating installers can use `planClaudeHookEventsUpdate` with an explicit
329
+ selection of supported Claude hook events. `planClaudePreToolUseHookUpdate` and
330
+ `planClaudeStopHookUpdate` are single-event convenience wrappers. Supported events
331
+ are `PreToolUse`, `PostToolUse`, `Stop`, `Notification`, `SessionStart`, `SessionEnd`
332
+ `UserPromptSubmit` and `SubagentStart`. The transaction requires the exact settings SHA-256
333
+ preimage and preserves every field outside the selected events, including other
334
+ hooks. Apply the returned plan through `applyAgentIntegration`; this validates
335
+ existing managed discovery and native guards, writes settings and policy with
336
+ precondition checks, and retains recovery backups. An invalid policy or existing
337
+ drift is a refusal, never permission to use an uncoordinated writer. A null plan
338
+ means no managed Skills policy exists. These APIs do not authorize the hook's
339
+ behavior or turn a configuration-changing hook into a preference.
340
+
341
+ The transaction verifies every agent's discovery sources, directories and
342
+ automatic discovery closure. Native runtime and trust checks cover Claude and
343
+ the declared consumers of the settings path, including containing roots or
344
+ directories and OpenCode's Claude compatibility surface. Unrelated agents keep
345
+ their exact witnesses and trust state; they must still pass their own consumer
346
+ checks before use. The plan's `managedAgentChecks.agents` and `nativeSkills`
347
+ describe this scoped runtime check, not a fleet health result. A path-identity
348
+ witness intersecting the write still refuses instead of adopting a new inode.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {