@hasna/skills 0.10.83 → 0.10.88

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.
Files changed (34) hide show
  1. package/README.md +133 -15
  2. package/bin/index.js +5690 -2868
  3. package/bin/mcp.js +1688 -810
  4. package/bin/migrate.js +1 -1
  5. package/bin/server.js +1 -1
  6. package/bin/worker.js +1 -1
  7. package/dist/cli/commands/agent-integration.d.ts +35 -0
  8. package/dist/cli/commands/context.d.ts +4 -0
  9. package/dist/cli/commands/runtime-launcher.d.ts +4 -9
  10. package/dist/index.js +3021 -1530
  11. package/dist/lib/agent-adapters.d.ts +5 -2
  12. package/dist/lib/agent-discovery.d.ts +22 -4
  13. package/dist/lib/agent-hermes.d.ts +7 -1
  14. package/dist/lib/agent-integration.d.ts +31 -2
  15. package/dist/lib/agent-opencode.d.ts +4 -0
  16. package/dist/lib/claude-hook-disable.d.ts +32 -0
  17. package/dist/lib/claude-settings-witness.d.ts +28 -0
  18. package/dist/lib/codex-settings-witness.d.ts +20 -0
  19. package/dist/lib/guarded-agent-config.d.ts +3 -0
  20. package/dist/lib/hook-diagnostics.d.ts +3 -1
  21. package/dist/lib/narrowed-witness-ledger.d.ts +24 -0
  22. package/dist/lib/native-discovery-roots.d.ts +8 -0
  23. package/dist/lib/native-drift-receipt.d.ts +55 -0
  24. package/dist/lib/opencode-hook-protocol.d.ts +5 -0
  25. package/dist/lib/profile-client.d.ts +1 -0
  26. package/dist/lib/selection-cache.d.ts +47 -2
  27. package/dist/lib/session-background-renewal.d.ts +118 -0
  28. package/dist/lib/session-reconciliation.d.ts +82 -2
  29. package/dist/lib/skill-context.d.ts +39 -1
  30. package/dist/lib/skill-relevance.d.ts +95 -0
  31. package/dist/runtime-prerequisites.js +569 -165
  32. package/dist/sdk/index.js +2626 -1396
  33. package/docs/plugin-admission.md +132 -3
  34. package/package.json +1 -1
@@ -49,7 +49,7 @@ var __require = import.meta.require;
49
49
  // src/runtime-prerequisites.ts
50
50
  import { isAbsolute as isAbsolute7 } from "path";
51
51
  // package.json
52
- var version = "0.10.83";
52
+ var version = "0.10.88";
53
53
  var package_default = {
54
54
  name: "@hasna/skills",
55
55
  version,
@@ -191,10 +191,218 @@ var package_default = {
191
191
  };
192
192
 
193
193
  // src/lib/agent-install-selection.ts
194
- import { lstatSync as lstatSync6 } from "fs";
195
- import { dirname as dirname6, join as join7, resolve as resolve7 } from "path";
194
+ import { lstatSync as lstatSync7 } from "fs";
195
+ import { dirname as dirname7, join as join9, resolve as resolve8 } from "path";
196
196
  import { homedir as homedir2 } from "os";
197
197
 
198
+ // src/lib/opencode-hook-protocol.ts
199
+ import { createHash as createHash2 } from "crypto";
200
+
201
+ // src/lib/agent-bridge.ts
202
+ import { createHash } from "crypto";
203
+ import { lstatSync, readFileSync, readdirSync } from "fs";
204
+ import { join } from "path";
205
+ var CLI_BRIDGE_NAME = "skills-cli";
206
+ var CLI_BRIDGE_VERSION = 1;
207
+ var markdown = `---
208
+ name: skills-cli
209
+ description: Discover, load, author, and run versioned skills through the Skills CLI when a task needs reusable instructions or an executable skill.
210
+ ---
211
+
212
+ Use the Skills CLI as the skill authority for this station.
213
+
214
+ - Discover available skills with \`skills list --json\` or \`skills search <query> --json\`.
215
+ - Read selected instructions with \`skills load <slug>\`. Use \`skills sync\` to refresh the station's shared selection profile.
216
+ - Create local drafts with \`skills new <name> --kind instruction\` or \`--kind executable\` in the Skills-owned source directory. Validate with \`skills validate <name>\`; use \`skills push --help\` for explicit versioned publication.
217
+ - Run a skill only when the user's task calls for execution. Use \`skills run --help\` and select the intended local or cloud target explicitly.
218
+
219
+ This bridge contains no skill payloads. Do not copy skill content into agent-native skill directories or load other native skill copies. If a Skills command refuses authority, selection, integrity, or native drift, report that refusal and use its repair guidance; do not substitute bundled or stale local content. Explicit local draft authoring remains available.
220
+ `;
221
+ var CLI_BRIDGE_DIGEST = createHash("sha256").update(markdown).digest("hex");
222
+ var CLI_BRIDGE_FILES = Object.freeze({
223
+ "SKILL.md": markdown,
224
+ ".hasna-skills.json": `${JSON.stringify({ managedBy: "@hasna/skills", kind: "cli-bridge", version: CLI_BRIDGE_VERSION, protocol: "selection-v1", contentSha256: CLI_BRIDGE_DIGEST }, null, 2)}
225
+ `
226
+ });
227
+ function isOwnedCliBridge(path, expectedPaths) {
228
+ if (!expectedPaths.includes(path))
229
+ return false;
230
+ try {
231
+ if (!lstatSync(path).isDirectory())
232
+ return false;
233
+ if (JSON.stringify(readdirSync(path).sort()) !== JSON.stringify(Object.keys(CLI_BRIDGE_FILES).sort()))
234
+ return false;
235
+ return Object.entries(CLI_BRIDGE_FILES).every(([name, content]) => {
236
+ const file = join(path, name), stat = lstatSync(file);
237
+ return stat.isFile() && !stat.isSymbolicLink() && stat.size === Buffer.byteLength(content) && readFileSync(file, "utf8") === content;
238
+ });
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
243
+
244
+ // src/lib/opencode-hook-protocol.ts
245
+ var OPENCODE_BRIDGE_BODY_SHA256 = createHash2("sha256").update(CLI_BRIDGE_FILES["SKILL.md"].replace(/^---\n[\s\S]*?\n---\n/, "")).digest("hex");
246
+ var OPENCODE_BRIDGE_DESCRIPTION_SHA256 = createHash2("sha256").update(/^description: (.+)$/m.exec(CLI_BRIDGE_FILES["SKILL.md"])[1]).digest("hex");
247
+
248
+ // src/lib/agent-opencode.ts
249
+ function renderOpenCodePlugin(command, profile) {
250
+ return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
251
+ import { spawn } from "node:child_process";
252
+ import { createHash } from "node:crypto";
253
+ const command = ${JSON.stringify(command)};
254
+ const profile = ${JSON.stringify(profile)};
255
+ const bridgeBodySha256 = ${JSON.stringify(OPENCODE_BRIDGE_BODY_SHA256)};
256
+ const bridgeDescriptionSha256 = ${JSON.stringify(OPENCODE_BRIDGE_DESCRIPTION_SHA256)};
257
+ function digest(text) { return createHash("sha256").update(text).digest("hex"); }
258
+ function canonicalSkill(skill) {
259
+ return skill?.id === "skills-cli" && skill.name === "skills-cli" && typeof skill.path === "string"
260
+ && typeof skill.content === "string" && digest(skill.content) === bridgeBodySha256
261
+ && typeof skill.description === "string" && digest(skill.description) === bridgeDescriptionSha256
262
+ && (skill.autoinvoke === undefined || skill.autoinvoke === false);
263
+ }
264
+ function refusal() {
265
+ const error = new Error("Skills verification blocked this request. Review the OpenCode Skills hook configuration, then retry.");
266
+ error.name = "SkillsHookRefusal";
267
+ error.skillsHookRefusal = { version: 1, code: "SKILLS_HOOK_REFUSED" };
268
+ return error;
269
+ }
270
+ export default {
271
+ id: "local.hasna-skills-cli",
272
+ async setup(ctx) {
273
+ if (!ctx?.session?.hook || !ctx?.session?.get || !ctx?.skill?.transform || !ctx?.skill?.list || !ctx?.tool?.hook) throw refusal();
274
+ const registrations = [], pending = new Set(), sessions = new Set(), prompts = new Map();
275
+ let disposed = false;
276
+ function context(input) {
277
+ if (disposed) return Promise.reject(refusal());
278
+ return new Promise((resolve, reject) => {
279
+ // Availability failures may omit optional model context. Native skill
280
+ // consumption and explicit policy refusals always remain fail-closed.
281
+ const optional = input.delivery_phase === "context";
282
+ const chunks = []; let bytes = 0, settled = false, child, timer;
283
+ const finish = () => { clearTimeout(timer); pending.delete(cancel); };
284
+ const deny = () => { if (settled) return; settled = true; finish(); reject(refusal()); };
285
+ const unavailable = code => {
286
+ if (settled) return;
287
+ if (!optional || disposed) return deny();
288
+ settled = true; finish();
289
+ resolve({ delivered: false, text: "Skills context is unavailable [SKILLS_HOOK_" + code + "]. No Skills instructions were delivered by this hook. Ordinary work may continue. Do not perform actions that depend on unavailable Skills instructions, including previously loaded instructions. Explicit skills load, context and run still require verification. Do not substitute native, unmanaged or stale payloads." });
290
+ };
291
+ const stop = code => { unavailable(code); child?.kill("SIGKILL"); };
292
+ const cancel = () => { deny(); child?.kill("SIGKILL"); };
293
+ try {
294
+ child = spawn(command, ["hook", "user-prompt", "--agent", "opencode", "--selection-profile", profile], { cwd: input.cwd, stdio: ["pipe", "pipe", "pipe"] });
295
+ } catch { unavailable("SPAWN_FAILED"); return; }
296
+ pending.add(cancel);
297
+ timer = setTimeout(() => stop("TIMEOUT"), 15000);
298
+ child.stdout.on("data", chunk => { if (settled) return; bytes += chunk.length; if (bytes > 1048576) stop("OUTPUT_LIMIT"); else chunks.push(chunk); });
299
+ child.stdout.on("error", () => stop("TRANSPORT_FAILED"));
300
+ child.stderr.on("data", () => {});
301
+ child.stderr.on("error", () => stop("TRANSPORT_FAILED"));
302
+ child.stdin.on("error", () => stop("TRANSPORT_FAILED"));
303
+ child.on("error", () => unavailable("SPAWN_FAILED"));
304
+ child.on("close", code => {
305
+ if (settled) return;
306
+ if (disposed) return deny();
307
+ if (code !== 0) return unavailable("CHILD_FAILED");
308
+ let result;
309
+ try {
310
+ result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
311
+ } catch { unavailable("INVALID_RESPONSE"); return; }
312
+ if (!result || typeof result !== "object" || Array.isArray(result)) return unavailable("INVALID_RESPONSE");
313
+ if (result.decision === "block" || result.continue === false) return deny();
314
+ // hookContextOutput returns exactly {} for a successful empty
315
+ // context. It never attests native bridge verification.
316
+ const output = optional && Object.keys(result).length === 0
317
+ ? { hookEventName: input.hook_event_name, additionalContext: "" }
318
+ : result.hookSpecificOutput;
319
+ if (!output || typeof output !== "object" || Array.isArray(output) || output.hookEventName !== input.hook_event_name || typeof output.additionalContext !== "string") return unavailable("INVALID_RESPONSE");
320
+ if (input.hook_event_name === "BeforeTool" && result.nativeBridgeVerified !== true) return deny();
321
+ settled = true; finish();
322
+ resolve({ delivered: true, text: output.additionalContext });
323
+ });
324
+ try { child.stdin.end(JSON.stringify(input)); } catch { stop("TRANSPORT_FAILED"); }
325
+ });
326
+ }
327
+ async function input(sessionID, prompt, event) {
328
+ if (disposed) throw refusal();
329
+ let session;
330
+ try { session = await ctx.session.get({ sessionID }); } catch { throw refusal(); }
331
+ if (disposed || !session || session.id !== sessionID || typeof session.location?.directory !== "string" || (session.parentID !== undefined && typeof session.parentID !== "string") || session.parentID === sessionID) throw refusal();
332
+ return { hook_event_name: event ?? (!sessions.has(sessionID) ? session.parentID ? "SubagentStart" : "SessionStart" : "UserPromptSubmit"), delivery_phase: event ? "native" : "context", cwd: session.location.directory, session_id: sessionID, parent_session_id: session.parentID ?? null, prompt, restore: true };
333
+ }
334
+ async function dispose() {
335
+ if (disposed) return;
336
+ disposed = true;
337
+ for (const cancel of pending) cancel();
338
+ sessions.clear();
339
+ prompts.clear();
340
+ const results = await Promise.allSettled(registrations.reverse().map(registration => Promise.resolve().then(() => registration.dispose())));
341
+ if (results.some(result => result.status === "rejected")) throw refusal();
342
+ }
343
+ try {
344
+ registrations.push(await ctx.skill.transform(editor => {
345
+ for (const skill of editor.list()) if (!canonicalSkill(skill)) editor.remove(skill.id);
346
+ }));
347
+ async function verifyNativeBridge(sessionID) {
348
+ let skills;
349
+ try { skills = await ctx.skill.list(); } catch { throw refusal(); }
350
+ if (!Array.isArray(skills)) throw refusal();
351
+ const matches = skills.filter(skill => skill?.id === "skills-cli");
352
+ if (matches.length !== 1) throw refusal();
353
+ const skill = matches[0];
354
+ if (!canonicalSkill(skill)) throw refusal();
355
+ const metadata = { id: skill.id, name: skill.name, path: skill.path, contentSha256: digest(skill.content), descriptionSha256: digest(skill.description) };
356
+ await context({ ...await input(sessionID, "", "BeforeTool"), tool_input: { id: "skills-cli" }, native_skill: metadata });
357
+ }
358
+ registrations.push(await ctx.tool.hook("execute.before", async event => {
359
+ if (disposed) throw refusal();
360
+ if (event.tool !== "skill") return;
361
+ if (event.input?.id !== "skills-cli") throw refusal();
362
+ await verifyNativeBridge(event.sessionID);
363
+ }));
364
+ registrations.push(await ctx.session.hook("prompt", async event => {
365
+ if (disposed || typeof event.prompt?.text !== "string" || event.prompt.text.length > 128 * 1024) throw refusal();
366
+ if (event.prompt.skills !== undefined && (!Array.isArray(event.prompt.skills) || event.prompt.skills.some(skill => skill?.id !== "skills-cli"))) throw refusal();
367
+ // Attachments materialize immediately after this hook. Check the actual
368
+ // current registry object and on-disk bridge before native consumption.
369
+ if (event.prompt.skills?.length) await verifyNativeBridge(event.sessionID);
370
+ if (disposed) throw refusal();
371
+ const queued = prompts.get(event.sessionID) ?? [];
372
+ if (queued.reduce((size, entry) => size + entry.text.length + 1, 0) + event.prompt.text.length > 128 * 1024) throw refusal();
373
+ queued.push({ text: event.prompt.text });
374
+ prompts.set(event.sessionID, queued);
375
+ }));
376
+ registrations.push(await ctx.session.hook("context", async event => {
377
+ // Only prompt-hook input has owner-prompt provenance. Keep it transient
378
+ // until the one delivery-bearing CLI call, including an empty prompt.
379
+ // Checkpoints, synthetic user-role history and resumes never match.
380
+ const queued = [...(prompts.get(event.sessionID) ?? [])];
381
+ const result = await context(await input(event.sessionID, queued.map(entry => entry.text).join("\\n")));
382
+ if (disposed) throw refusal();
383
+ if (result.text) event.system.push({ type: "text", text: result.text });
384
+ // Remove only the prompt events this request consumed. New admission
385
+ // while the CLI was running belongs to the next model context.
386
+ const current = prompts.get(event.sessionID);
387
+ if (current) {
388
+ const remaining = current.filter(entry => !queued.includes(entry));
389
+ if (remaining.length) prompts.set(event.sessionID, remaining);
390
+ else prompts.delete(event.sessionID);
391
+ }
392
+ // Retry SessionStart on the next context after an unavailable child,
393
+ // but never replay prompt text already admitted to this model turn.
394
+ if (result.delivered) sessions.add(event.sessionID);
395
+ }));
396
+ return dispose;
397
+ } catch {
398
+ await dispose();
399
+ throw refusal();
400
+ }
401
+ },
402
+ };
403
+ `;
404
+ }
405
+
198
406
  // src/lib/agent-adapters.ts
199
407
  var INTEGRATION_AGENTS = ["claude", "codex", "gemini", "opencode", "cursor", "hermes", "sumi"];
200
408
  function renderAgentHookCommand(command, agent, profileId, event) {
@@ -206,12 +414,12 @@ var AGENT_ADAPTERS = {
206
414
  claude: { root: ".claude/skills", config: ".claude/settings.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
207
415
  codex: { root: ".codex/skills", config: ".codex/hooks.json", events: ["UserPromptSubmit", "SessionStart", "SubagentStart"], promptContext: true },
208
416
  gemini: { root: ".gemini/skills", config: ".gemini/settings.json", events: ["BeforeAgent", "SessionStart"], promptContext: true },
209
- opencode: { root: ".config/opencode/skills", config: ".config/opencode/opencode.json", events: ["chat.message"], promptContext: true },
417
+ opencode: { root: ".config/opencode/skills", config: ".config/opencode/opencode.json", events: ["prompt", "context"], promptContext: true },
210
418
  sumi: { root: null, config: null, events: ["prompt", "context"], promptContext: true },
211
419
  hermes: { root: ".hermes/skills", config: ".hermes/config.yaml", events: ["pre_llm_call", "pre_tool_call"], promptContext: true, promptFailureMode: "open", toolFailureMode: "supervised-child-errors-block", requiresNativeTrust: true },
212
420
  cursor: { root: ".cursor/skills", config: ".cursor/hooks.json", events: ["beforeSubmitPrompt", "sessionStart"], promptContext: false }
213
421
  };
214
- function renderOpenCodePlugin(command, profileId) {
422
+ function renderLegacyOpenCodePlugin(command, profileId) {
215
423
  return `// Managed by @hasna/skills. Regenerate with skills hook install --agent opencode.
216
424
  import { spawn } from "node:child_process";
217
425
  import { randomUUID } from "node:crypto";
@@ -261,8 +469,12 @@ export default async ({ directory }) => {
261
469
  };
262
470
  `;
263
471
  }
472
+ function isManagedOpenCodePlugin(text, command, profileId) {
473
+ return text === renderOpenCodePlugin(command, profileId) || text === renderLegacyOpenCodePlugin(command, profileId);
474
+ }
264
475
 
265
476
  // src/lib/native-discovery-roots.ts
477
+ import { dirname, join as join2, resolve } from "path";
266
478
  var NATIVE_SKILL_ROOTS = [
267
479
  ["claude", ".claude/skills"],
268
480
  ["codex", ".codex/skills"],
@@ -282,14 +494,32 @@ var NATIVE_SKILL_ROOTS = [
282
494
  ["roo", ".roo/skills"],
283
495
  ["copilot", ".github/skills"]
284
496
  ];
497
+ function nativeSumiProjectSkillRoots(home, project, resolveSumiRoots) {
498
+ return [...resolveSumiRoots(home, project), ...[".claude", ".agents"].flatMap((directory) => ["skill", "skills"].map((name) => join2(project, directory, name)))];
499
+ }
500
+ function nativeProjectAncestorDirectories(projects, canonical = resolve) {
501
+ const directories = new Set;
502
+ for (const project of projects) {
503
+ for (let path = canonical(project), depth = 0;; depth++) {
504
+ if (depth >= 100)
505
+ throw new Error("NATIVE_SKILL_DRIFT: project ancestor discovery limit exceeded");
506
+ directories.add(path);
507
+ const parent = dirname(path);
508
+ if (parent === path)
509
+ break;
510
+ path = parent;
511
+ }
512
+ }
513
+ return [...directories];
514
+ }
285
515
 
286
516
  // src/lib/managed-policy.ts
287
- import { closeSync as closeSync5, constants as constants5, fstatSync as fstatSync5, lstatSync as lstatSync5, openSync as openSync5, readSync as readSync4 } from "fs";
288
- import { join as join6 } from "path";
517
+ import { closeSync as closeSync5, constants as constants5, fstatSync as fstatSync5, lstatSync as lstatSync6, openSync as openSync5, readSync as readSync4 } from "fs";
518
+ import { join as join8 } from "path";
289
519
 
290
520
  // src/lib/config.ts
291
- import { closeSync, constants, existsSync as existsSync2, fstatSync, lstatSync, openSync, readFileSync, statSync, writeFileSync, mkdirSync } from "fs";
292
- import { join as join2, dirname } from "path";
521
+ import { closeSync, constants, existsSync as existsSync2, fstatSync, lstatSync as lstatSync2, openSync, readFileSync as readFileSync2, statSync, writeFileSync, mkdirSync } from "fs";
522
+ import { join as join4, dirname as dirname2 } from "path";
293
523
 
294
524
  // src/lib/retired-settings.ts
295
525
  var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
@@ -345,7 +575,7 @@ function assertNoRetiredConfigKeys(config, source) {
345
575
  // src/lib/app-home.ts
346
576
  import { existsSync } from "fs";
347
577
  import { homedir } from "os";
348
- import { join, resolve } from "path";
578
+ import { join as join3, resolve as resolve2 } from "path";
349
579
  import { homedir as pathsResolverHomedir } from "os";
350
580
  import { join as pathsResolverJoin } from "path";
351
581
  var PATHS_RESOLVER_DATA_ENV = "HASNA_DATA_HOME";
@@ -381,7 +611,7 @@ function effectiveHome() {
381
611
  return process.env["HOME"] || process.env["USERPROFILE"] || homedir() || "/tmp";
382
612
  }
383
613
  function legacyDataRoot() {
384
- return join(effectiveHome(), ".hasna", "skills");
614
+ return join3(effectiveHome(), ".hasna", "skills");
385
615
  }
386
616
  function resolverDataRoot(home = effectiveHome(), env) {
387
617
  return dataDir({ app: "skills", home, env });
@@ -390,13 +620,13 @@ function adoptResolverDataRoot(resolved, env = process.env) {
390
620
  const dataOverride = env.HASNA_DATA_HOME;
391
621
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
392
622
  return true;
393
- return existsSync(join(resolved, DEFAULT_SQLITE_FILENAME)) || existsSync(join(resolved, GLOBAL_CONFIG_FILENAME));
623
+ return existsSync(join3(resolved, DEFAULT_SQLITE_FILENAME)) || existsSync(join3(resolved, GLOBAL_CONFIG_FILENAME));
394
624
  }
395
625
  function exactDataRoot() {
396
626
  for (const key of [DATA_DIR_ENV, HASNA_SKILLS_HOME_ENV, SKILLS_HOME_ENV]) {
397
627
  const dir = process.env[key]?.trim();
398
628
  if (dir)
399
- return resolve(dir);
629
+ return resolve2(dir);
400
630
  }
401
631
  return;
402
632
  }
@@ -405,19 +635,19 @@ function getDataRoot() {
405
635
  if (exact)
406
636
  return exact;
407
637
  const resolved = resolverDataRoot();
408
- return adoptResolverDataRoot(resolved) ? resolve(resolved) : resolve(legacyDataRoot());
638
+ return adoptResolverDataRoot(resolved) ? resolve2(resolved) : resolve2(legacyDataRoot());
409
639
  }
410
640
  function skillsDataRootForHome(home) {
411
- const isOwnHome = resolve(home) === resolve(effectiveHome()) || resolve(home) === resolve(homedir());
641
+ const isOwnHome = resolve2(home) === resolve2(effectiveHome()) || resolve2(home) === resolve2(homedir());
412
642
  if (isOwnHome) {
413
643
  const exact = exactDataRoot();
414
644
  if (exact)
415
645
  return exact;
416
646
  const resolved2 = resolverDataRoot(home);
417
- return adoptResolverDataRoot(resolved2) ? resolve(resolved2) : resolve(join(home, ".hasna", "skills"));
647
+ return adoptResolverDataRoot(resolved2) ? resolve2(resolved2) : resolve2(join3(home, ".hasna", "skills"));
418
648
  }
419
649
  const resolved = resolverDataRoot(home, {});
420
- return adoptResolverDataRoot(resolved, {}) ? resolve(resolved) : resolve(join(home, ".hasna", "skills"));
650
+ return adoptResolverDataRoot(resolved, {}) ? resolve2(resolved) : resolve2(join3(home, ".hasna", "skills"));
421
651
  }
422
652
 
423
653
  // src/lib/config.ts
@@ -448,7 +678,7 @@ var INSTALLED_SKILLS_DIRNAME = "installed";
448
678
  var SKILLS_CACHE_DIRNAME = "skills";
449
679
  var LAYOUT_MIGRATION_RECORD = ".layout-migration.json";
450
680
  function isOwnerLayoutMigrated(appDir) {
451
- return existsSync2(join2(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD));
681
+ return existsSync2(join4(appDir, SKILLS_CACHE_DIRNAME, LAYOUT_MIGRATION_RECORD));
452
682
  }
453
683
  function getDataDir() {
454
684
  const root = getDataRoot();
@@ -462,16 +692,16 @@ function getDataDirReadOnly() {
462
692
  }
463
693
  function getConfigPath(scope) {
464
694
  if (scope === "global") {
465
- return join2(getDataDir(), "config.json");
695
+ return join4(getDataDir(), "config.json");
466
696
  }
467
- return join2(process.cwd(), "skills.config.json");
697
+ return join4(process.cwd(), "skills.config.json");
468
698
  }
469
699
  function readConfigFile(path) {
470
700
  if (!existsSync2(path))
471
701
  return {};
472
702
  let parsed;
473
703
  try {
474
- parsed = JSON.parse(readFileSync(path, "utf-8"));
704
+ parsed = JSON.parse(readFileSync2(path, "utf-8"));
475
705
  } catch {
476
706
  return {};
477
707
  }
@@ -508,7 +738,7 @@ function saveConfig(key, value, scope = "project") {
508
738
  let existing = {};
509
739
  if (existsSync2(filePath)) {
510
740
  try {
511
- existing = JSON.parse(readFileSync(filePath, "utf-8"));
741
+ existing = JSON.parse(readFileSync2(filePath, "utf-8"));
512
742
  if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
513
743
  existing = {};
514
744
  }
@@ -516,7 +746,7 @@ function saveConfig(key, value, scope = "project") {
516
746
  existing = {};
517
747
  }
518
748
  } else {
519
- const dir = dirname(filePath);
749
+ const dir = dirname2(filePath);
520
750
  if (!existsSync2(dir)) {
521
751
  mkdirSync(dir, { recursive: true });
522
752
  }
@@ -535,7 +765,7 @@ function unsetConfig(key, scope = "project") {
535
765
  return false;
536
766
  let existing;
537
767
  try {
538
- const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
768
+ const parsed = JSON.parse(readFileSync2(filePath, "utf-8"));
539
769
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
540
770
  return false;
541
771
  existing = parsed;
@@ -551,17 +781,17 @@ function unsetConfig(key, scope = "project") {
551
781
  }
552
782
 
553
783
  // src/lib/selection-cache.ts
554
- import { createHash as createHash2, randomUUID } from "crypto";
784
+ import { createHash as createHash4, randomUUID } from "crypto";
555
785
  import { spawnSync } from "child_process";
556
- import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, linkSync, lstatSync as lstatSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync3, readlinkSync, readSync, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
786
+ import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, linkSync, lstatSync as lstatSync3, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync4, readlinkSync, readSync, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
557
787
  import { hostname } from "os";
558
- import { dirname as dirname2, isAbsolute, join as join4, parse, resolve as resolve2 } from "path";
788
+ import { dirname as dirname3, isAbsolute, join as join6, parse, resolve as resolve3 } from "path";
559
789
  import { isDeepStrictEqual } from "util";
560
790
 
561
791
  // src/lib/skill-bundle.ts
562
- import { createHash } from "crypto";
563
- import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
564
- import { join as join3, relative } from "path";
792
+ import { createHash as createHash3 } from "crypto";
793
+ import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
794
+ import { join as join5, relative } from "path";
565
795
  import { createGunzip } from "zlib";
566
796
 
567
797
  // src/lib/skill-entry-path.ts
@@ -748,7 +978,7 @@ function ownBytes(view) {
748
978
  return out;
749
979
  }
750
980
  function sha256Hex(bytes) {
751
- return createHash("sha256").update(bytes).digest("hex");
981
+ return createHash3("sha256").update(bytes).digest("hex");
752
982
  }
753
983
  function collectSkillBundleEntries(dir) {
754
984
  const entries = [];
@@ -756,8 +986,8 @@ function collectSkillBundleEntries(dir) {
756
986
  return entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
757
987
  }
758
988
  function walk(root, current, out) {
759
- for (const entry of readdirSync(current, { withFileTypes: true })) {
760
- const absolute = join3(current, entry.name);
989
+ for (const entry of readdirSync2(current, { withFileTypes: true })) {
990
+ const absolute = join5(current, entry.name);
761
991
  const rel = relative(root, absolute).split("\\").join("/");
762
992
  const isRootLevel = !rel.includes("/");
763
993
  if (ANY_SEGMENT_EXCLUDES.has(entry.name.toLowerCase()))
@@ -781,7 +1011,7 @@ function walk(root, current, out) {
781
1011
  const stats = statSync2(absolute);
782
1012
  out.push({
783
1013
  path: rel,
784
- bytes: ownBytes(readFileSync2(absolute)),
1014
+ bytes: ownBytes(readFileSync3(absolute)),
785
1015
  mode: stats.mode & 64 ? 493 : 420
786
1016
  });
787
1017
  }
@@ -1001,7 +1231,7 @@ async function inspectSkillBundle(bundle, options = {}) {
1001
1231
  parser.push(chunk);
1002
1232
  bytesSinceYield += chunk.byteLength;
1003
1233
  if (bytesSinceYield >= 256 * 1024) {
1004
- await new Promise((resolve2) => setTimeout(resolve2, 0));
1234
+ await new Promise((resolve3) => setTimeout(resolve3, 0));
1005
1235
  bytesSinceYield = 0;
1006
1236
  check();
1007
1237
  }
@@ -1239,14 +1469,14 @@ class SkillSelectionError extends Error {
1239
1469
  var MAX_CACHED_PROFILE_AGE_MS = 24 * 60 * 60 * 1000;
1240
1470
  var SELECTION_LOCK_FILE = "selection.lock.json";
1241
1471
  function selectionCacheRoot(options = {}) {
1242
- return resolve2(options.cacheDir ?? join4(getDataDirReadOnly(), "selection-cache"));
1472
+ return resolve3(options.cacheDir ?? join6(getDataDirReadOnly(), "selection-cache"));
1243
1473
  }
1244
1474
  function selectionKey(selection) {
1245
1475
  validateSelection(selection);
1246
1476
  return hash([selection.authority, selection.workspaceId, selection.slug, selection.version, selection.bundleDigest]);
1247
1477
  }
1248
1478
  function hash(value) {
1249
- return createHash2("sha256").update(JSON.stringify(value)).digest("hex");
1479
+ return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
1250
1480
  }
1251
1481
  function validateSelection(selection) {
1252
1482
  if (!selection || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(selection.slug) || !isValidSkillVersion(selection.version) || !/^sha256:[a-f0-9]{64}$/.test(selection.bundleDigest) || typeof selection.workspaceId !== "string" || !selection.workspaceId.trim() || selection.authorizationEpoch !== undefined && (typeof selection.authorizationEpoch !== "string" || !/^[a-f0-9]{32}$/.test(selection.authorizationEpoch)) || typeof selection.profileRevision !== "string" || !selection.profileRevision.trim()) {
@@ -1283,25 +1513,35 @@ function validateResolvedProfile(profile, authority) {
1283
1513
  }
1284
1514
  function selectionBundlePath(selection, options = {}) {
1285
1515
  validateSelection(selection);
1286
- return join4(selectionCacheRoot(options), "objects", hash(selection.authority), hash(selection.workspaceId), `${selection.bundleDigest.slice(7)}.tar.gz`);
1516
+ return join6(selectionCacheRoot(options), "objects", hash(selection.authority), hash(selection.workspaceId), `${selection.bundleDigest.slice(7)}.tar.gz`);
1287
1517
  }
1288
1518
  function assertRegularPath(path, createParents = false) {
1289
- const absolute = resolve2(path);
1519
+ const absolute = resolve3(path);
1290
1520
  const root = parse(absolute).root;
1291
1521
  let cursor = root;
1292
1522
  const segments = absolute.slice(root.length).split("/");
1293
1523
  for (let index = 0;index < segments.length; index++) {
1294
- cursor = join4(cursor, segments[index]);
1524
+ cursor = join6(cursor, segments[index]);
1295
1525
  try {
1296
- const stat = lstatSync2(cursor);
1526
+ const stat = lstatSync3(cursor);
1297
1527
  if (stat.isSymbolicLink() || index < segments.length - 1 && !stat.isDirectory()) {
1298
1528
  throw new SkillSelectionError("UNSAFE_CACHE_PATH", "Skills cache paths must not traverse symlinks or non-directories.");
1299
1529
  }
1300
1530
  } catch (error) {
1301
1531
  if (error.code !== "ENOENT")
1302
1532
  throw error;
1303
- if (createParents && index < segments.length - 1)
1304
- mkdirSync2(cursor, { mode: 448 });
1533
+ if (createParents && index < segments.length - 1) {
1534
+ try {
1535
+ mkdirSync2(cursor, { mode: 448 });
1536
+ } catch (creationError) {
1537
+ if (creationError.code !== "EEXIST")
1538
+ throw creationError;
1539
+ }
1540
+ const parent = lstatSync3(cursor);
1541
+ if (parent.isSymbolicLink() || !parent.isDirectory()) {
1542
+ throw new SkillSelectionError("UNSAFE_CACHE_PATH", "Skills cache paths must not traverse symlinks or non-directories.");
1543
+ }
1544
+ }
1305
1545
  }
1306
1546
  }
1307
1547
  }
@@ -1343,7 +1583,7 @@ function writeSelectionJson(path, value) {
1343
1583
  }
1344
1584
  function atomicWrite(path, bytes, mode, durable = false) {
1345
1585
  assertRegularPath(path, true);
1346
- const temporary = join4(dirname2(path), `.selection-${randomUUID()}.tmp`);
1586
+ const temporary = join6(dirname3(path), `.selection-${randomUUID()}.tmp`);
1347
1587
  try {
1348
1588
  const fd = openSync2(temporary, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, mode);
1349
1589
  try {
@@ -1355,7 +1595,7 @@ function atomicWrite(path, bytes, mode, durable = false) {
1355
1595
  }
1356
1596
  renameSync(temporary, path);
1357
1597
  if (durable)
1358
- syncDirectory(dirname2(path));
1598
+ syncDirectory(dirname3(path));
1359
1599
  } finally {
1360
1600
  try {
1361
1601
  unlinkSync(temporary);
@@ -1365,6 +1605,16 @@ function atomicWrite(path, bytes, mode, durable = false) {
1365
1605
  }
1366
1606
  }
1367
1607
  }
1608
+ function readSelectionFileSnapshot(path, maximumBytes = 4096) {
1609
+ const before = lstatSync3(path, { throwIfNoEntry: false });
1610
+ if (!before)
1611
+ return null;
1612
+ const bytes = readRegularFile(path, maximumBytes);
1613
+ const after = lstatSync3(path, { throwIfNoEntry: false });
1614
+ if (!bytes || !after?.isFile() || after.dev !== before.dev || after.ino !== before.ino)
1615
+ return null;
1616
+ return { bytes, sha256: sha256Hex(bytes), mtimeMs: after.mtimeMs, dev: after.dev, ino: after.ino };
1617
+ }
1368
1618
  function readSelectionJson(path, maximumBytes = MAX_PROFILE_DOCUMENT_BYTES) {
1369
1619
  const bytes = readRegularFile(path, Math.min(maximumBytes, MAX_PROFILE_DOCUMENT_BYTES));
1370
1620
  if (!bytes)
@@ -1382,7 +1632,8 @@ async function verifiedEntries(selection, bytes, signal, deadline) {
1382
1632
  signal?.throwIfAborted();
1383
1633
  if (remaining !== undefined && remaining <= 0)
1384
1634
  throw new SkillBundleInspectionError("BUNDLE_TIMEOUT", "Bundle inspection deadline exceeded");
1385
- const bundle = await inspectSkillBundle(bytes, { signal, ...remaining === undefined ? {} : { limits: { timeoutMs: remaining } } });
1635
+ const inspection = remaining === undefined ? {} : { limits: { timeoutMs: Math.min(remaining, SKILL_BUNDLE_INSPECTION_LIMITS.timeoutMs) } };
1636
+ const bundle = await inspectSkillBundle(bytes, { signal, ...inspection });
1386
1637
  return bundle.entries;
1387
1638
  }
1388
1639
  async function readCachedSelection(selection, options = {}) {
@@ -1455,7 +1706,7 @@ async function cacheSelectionBundle(selection, response, options = {}) {
1455
1706
  return entries;
1456
1707
  }
1457
1708
  function profilePath(profileId, options) {
1458
- return join4(selectionCacheRoot(options), "profiles", `${hash(profileId)}.json`);
1709
+ return join6(selectionCacheRoot(options), "profiles", `${hash(profileId)}.json`);
1459
1710
  }
1460
1711
  function activateSelectionProfile(profile, options = {}) {
1461
1712
  validateResolvedProfile(profile);
@@ -1487,7 +1738,7 @@ function assertFreshCachedProfile(receipt, options = {}) {
1487
1738
  }
1488
1739
  }
1489
1740
  function projectSelectionLockPath(projectDir) {
1490
- return join4(resolve2(projectDir), ".skills", SELECTION_LOCK_FILE);
1741
+ return join6(resolve3(projectDir), ".skills", SELECTION_LOCK_FILE);
1491
1742
  }
1492
1743
  function readProjectSelectionSnapshot(projectDir) {
1493
1744
  const path = projectSelectionLockPath(projectDir);
@@ -1506,7 +1757,7 @@ function readProjectSelectionSnapshot(projectDir) {
1506
1757
  function sessionReceiptPath(sessionId, options = {}) {
1507
1758
  if (!sessionId.trim() || sessionId.length > MAX_SKILL_SESSION_ID_CHARS)
1508
1759
  throw new SkillSelectionError("INVALID_SESSION", `A Skills session id must contain 1\u2013${MAX_SKILL_SESSION_ID_CHARS} characters.`);
1509
- return join4(selectionCacheRoot(options), "sessions", `${hash(sessionId)}.json`);
1760
+ return join6(selectionCacheRoot(options), "sessions", `${hash(sessionId)}.json`);
1510
1761
  }
1511
1762
  function skillSessionGeneration(receipt) {
1512
1763
  return receipt.generation ?? 0;
@@ -1560,7 +1811,7 @@ var SESSION_LOCK_MIN_AGE_MS = 30000;
1560
1811
  function localBootId() {
1561
1812
  try {
1562
1813
  if (process.platform === "linux") {
1563
- const value = readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
1814
+ const value = readFileSync4("/proc/sys/kernel/random/boot_id", "utf8").trim();
1564
1815
  return /^[a-f0-9-]{36}$/i.test(value) ? value : null;
1565
1816
  }
1566
1817
  if (process.platform === "darwin") {
@@ -1578,7 +1829,7 @@ function processStart(pid) {
1578
1829
  return { state: "unknown" };
1579
1830
  if (process.platform === "linux") {
1580
1831
  try {
1581
- const stat = readFileSync3(`/proc/${pid}/stat`, "utf8");
1832
+ const stat = readFileSync4(`/proc/${pid}/stat`, "utf8");
1582
1833
  const close = stat.lastIndexOf(")");
1583
1834
  const start = close < 0 ? undefined : stat.slice(close + 2).trim().split(/\s+/)[19];
1584
1835
  return start && /^\d+$/.test(start) ? { state: "alive", start: `linux:${start}` } : { state: "unknown" };
@@ -1597,7 +1848,16 @@ function processStart(pid) {
1597
1848
  }
1598
1849
  return { state: "unknown" };
1599
1850
  }
1851
+ var cachedLocalLockIdentity;
1600
1852
  function localLockIdentity() {
1853
+ if (cachedLocalLockIdentity?.pid === process.pid)
1854
+ return { ...cachedLocalLockIdentity.identity };
1855
+ const identity = probeLocalLockIdentity();
1856
+ if (identity)
1857
+ cachedLocalLockIdentity = { pid: process.pid, identity: { ...identity } };
1858
+ return identity;
1859
+ }
1860
+ function probeLocalLockIdentity() {
1601
1861
  const bootId = localBootId(), owner = processStart(process.pid);
1602
1862
  if (!bootId || owner.state !== "alive")
1603
1863
  return null;
@@ -1615,14 +1875,14 @@ function localLockIdentity() {
1615
1875
  }
1616
1876
  function readSessionLock(sessionId, options) {
1617
1877
  const path = `${sessionReceiptPath(sessionId, options)}.write-lock`;
1618
- const before = lstatSync2(path, { throwIfNoEntry: false });
1878
+ const before = lstatSync3(path, { throwIfNoEntry: false });
1619
1879
  if (!before)
1620
1880
  return null;
1621
1881
  if (!before.isFile() || ![1, 2].includes(before.nlink) || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid()) {
1622
1882
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is not a private regular file; recovery requires review.");
1623
1883
  }
1624
1884
  const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
1625
- const after = lstatSync2(path, { throwIfNoEntry: false });
1885
+ const after = lstatSync3(path, { throwIfNoEntry: false });
1626
1886
  if (!bytes || !after?.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs || before.mode !== after.mode || before.uid !== after.uid || before.gid !== after.gid || before.nlink !== after.nlink) {
1627
1887
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock changed during inspection; recovery requires review.");
1628
1888
  }
@@ -1639,7 +1899,7 @@ function readSessionLock(sessionId, options) {
1639
1899
  if (after.nlink === 2) {
1640
1900
  stagingPath = `${path}.${marker.operationId}.prepared`;
1641
1901
  assertRegularPath(stagingPath);
1642
- const staged = lstatSync2(stagingPath, { throwIfNoEntry: false });
1902
+ const staged = lstatSync3(stagingPath, { throwIfNoEntry: false });
1643
1903
  if (marker.publication !== "hard-link-v1" || !staged?.isFile() || staged.dev !== after.dev || staged.ino !== after.ino || staged.nlink !== 2 || staged.mode !== after.mode || staged.uid !== after.uid || staged.gid !== after.gid) {
1644
1904
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has an unbound publication link; recovery requires review.");
1645
1905
  }
@@ -1658,20 +1918,20 @@ function publishSessionLock(path, marker) {
1658
1918
  fsyncSync(fd);
1659
1919
  linkSync(stagingPath, path);
1660
1920
  published = true;
1661
- syncDirectory(dirname2(path));
1662
- const staged = lstatSync2(stagingPath);
1921
+ syncDirectory(dirname3(path));
1922
+ const staged = lstatSync3(stagingPath);
1663
1923
  if (!staged.isFile() || staged.dev !== stat.dev || staged.ino !== stat.ino) {
1664
1924
  throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The initialized lock staging inode changed before publication completed.");
1665
1925
  }
1666
1926
  unlinkSync(stagingPath);
1667
- syncDirectory(dirname2(path));
1927
+ syncDirectory(dirname3(path));
1668
1928
  handedOff = true;
1669
1929
  return { path, stagingPath, fd, dev: stat.dev, ino: stat.ino };
1670
1930
  } finally {
1671
1931
  if (!handedOff) {
1672
1932
  try {
1673
1933
  for (const target of [...published ? [path] : [], stagingPath]) {
1674
- const current = lstatSync2(target, { throwIfNoEntry: false });
1934
+ const current = lstatSync3(target, { throwIfNoEntry: false });
1675
1935
  if (current?.isFile() && current.dev === stat.dev && current.ino === stat.ino)
1676
1936
  unlinkSync(target);
1677
1937
  }
@@ -1709,16 +1969,16 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
1709
1969
  writeFileSync2(guardFd, `${JSON.stringify({ schemaVersion: 1, pid: process.pid, operationId: randomUUID() })}
1710
1970
  `);
1711
1971
  fsyncSync(guardFd);
1712
- const cacheOptions = { cacheDir: dirname2(dirname2(lock.path)) };
1972
+ const cacheOptions = { cacheDir: dirname3(dirname3(lock.path)) };
1713
1973
  const current = readSessionLock(lock.marker.sessionId, cacheOptions);
1714
1974
  if (!current || current.dev !== lock.dev || current.ino !== lock.ino || current.sha256 !== lock.sha256 || current.mtimeMs !== lock.mtimeMs || !writerIsGone(current, explicitLocalHost))
1715
1975
  return null;
1716
1976
  const currentReceipt = readSkillSessionSnapshotIfExists(lock.marker.sessionId, cacheOptions);
1717
1977
  if (currentReceipt?.sha256 !== receipt?.sha256 || currentReceipt?.generation !== receipt?.generation)
1718
1978
  return null;
1719
- const directory = join4(dirname2(dirname2(lock.path)), "session-lock-recoveries", randomUUID());
1720
- const archivePath = join4(directory, "original.write-lock");
1721
- const recordPath = join4(directory, "recovery.json");
1979
+ const directory = join6(dirname3(dirname3(lock.path)), "session-lock-recoveries", randomUUID());
1980
+ const archivePath = join6(directory, "original.write-lock");
1981
+ const recordPath = join6(directory, "recovery.json");
1722
1982
  assertRegularPath(archivePath, true);
1723
1983
  archiveBytes(recordPath, Buffer.from(`${JSON.stringify({
1724
1984
  schemaVersion: 1,
@@ -1733,7 +1993,7 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
1733
1993
  })}
1734
1994
  `));
1735
1995
  syncDirectory(directory);
1736
- syncDirectory(dirname2(directory));
1996
+ syncDirectory(dirname3(directory));
1737
1997
  const finalLock = readSessionLock(lock.marker.sessionId, cacheOptions);
1738
1998
  if (!finalLock || finalLock.dev !== lock.dev || finalLock.ino !== lock.ino || finalLock.sha256 !== lock.sha256 || readSkillSessionSnapshotIfExists(lock.marker.sessionId, cacheOptions)?.sha256 !== receipt?.sha256)
1739
1999
  return null;
@@ -1743,16 +2003,16 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
1743
2003
  moved = true;
1744
2004
  const archivedPaths = [archivePath];
1745
2005
  if (finalLock.stagingPath) {
1746
- const publicationPath = join4(directory, "publication.write-lock");
2006
+ const publicationPath = join6(directory, "publication.write-lock");
1747
2007
  renameSync(finalLock.stagingPath, publicationPath);
1748
2008
  archivedPaths.push(publicationPath);
1749
2009
  }
1750
2010
  syncDirectory(directory);
1751
- syncDirectory(dirname2(lock.path));
2011
+ syncDirectory(dirname3(lock.path));
1752
2012
  for (const path of archivedPaths) {
1753
- const before = lstatSync2(path, { throwIfNoEntry: false });
2013
+ const before = lstatSync3(path, { throwIfNoEntry: false });
1754
2014
  const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
1755
- const after = lstatSync2(path, { throwIfNoEntry: false });
2015
+ const after = lstatSync3(path, { throwIfNoEntry: false });
1756
2016
  if (!before?.isFile() || before.dev !== lock.dev || before.ino !== lock.ino || before.nlink !== archivedPaths.length || before.mtimeMs !== lock.mtimeMs || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid() || !bytes || sha256Hex(bytes) !== lock.sha256 || !after?.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.mtimeMs !== after.mtimeMs || before.mode !== after.mode || before.uid !== after.uid || before.gid !== after.gid || before.nlink !== after.nlink) {
1757
2017
  throw new Error("Archived lock ownership or bytes changed");
1758
2018
  }
@@ -1765,19 +2025,68 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
1765
2025
  return archivePath;
1766
2026
  } finally {
1767
2027
  closeSync2(guardFd);
1768
- const current = lstatSync2(guardPath, { throwIfNoEntry: false });
2028
+ const current = lstatSync3(guardPath, { throwIfNoEntry: false });
1769
2029
  if (current?.isFile() && current.dev === guardStat.dev && current.ino === guardStat.ino)
1770
2030
  unlinkSync(guardPath);
1771
2031
  }
1772
2032
  }
1773
- function withSessionWriteLocks(sessionIds, options, action) {
2033
+ var SESSION_WRITE_LOCK_WAIT_MS = 2000;
2034
+ var SESSION_WRITE_LOCK_RETRY_MIN_MS = 25;
2035
+ var SESSION_WRITE_LOCK_RETRY_MAX_MS = 100;
2036
+ function lockStatIdentity(path) {
2037
+ try {
2038
+ const stat = lstatSync3(path, { throwIfNoEntry: false });
2039
+ return stat ? `${stat.dev}:${stat.ino}:${stat.mtimeMs}:${stat.ctimeMs}:${stat.nlink}:${stat.size}` : null;
2040
+ } catch {
2041
+ return null;
2042
+ }
2043
+ }
2044
+ function sleepSync(ms) {
2045
+ if (ms > 0)
2046
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
2047
+ }
2048
+ var SESSION_LOCK_INSPECTIONS = 3;
2049
+ function inspectSessionLockSettled(sessionId, options) {
2050
+ const path = `${sessionReceiptPath(sessionId, options)}.write-lock`;
2051
+ let invalid;
2052
+ for (let attempt = 0;attempt < SESSION_LOCK_INSPECTIONS; attempt++) {
2053
+ try {
2054
+ return readSessionLock(sessionId, options);
2055
+ } catch (error) {
2056
+ if (!(error instanceof SkillSelectionError) || error.code !== "SESSION_WRITE_LOCKED")
2057
+ throw error;
2058
+ const seen = lockStatIdentity(path);
2059
+ if (seen !== null && seen === invalid)
2060
+ throw error;
2061
+ invalid = seen;
2062
+ sleepSync(SESSION_WRITE_LOCK_RETRY_MIN_MS);
2063
+ }
2064
+ }
2065
+ return "changing";
2066
+ }
2067
+ function assertSessionWriteLockReadable(sessionId, options = {}) {
2068
+ for (let attempt = 0;; attempt++) {
2069
+ try {
2070
+ readSessionLock(sessionId, options);
2071
+ return;
2072
+ } catch (error) {
2073
+ if (attempt >= 2 || !(error instanceof SkillSelectionError) || error.code !== "SESSION_WRITE_LOCKED")
2074
+ throw error;
2075
+ sleepSync(SESSION_WRITE_LOCK_RETRY_MIN_MS);
2076
+ }
2077
+ }
2078
+ }
2079
+ function withSessionWriteLocks(sessionIds, options, action, waitMs = 0) {
1774
2080
  const operationId = randomUUID();
1775
2081
  const identity = localLockIdentity();
2082
+ if (!Number.isFinite(waitMs) || waitMs < 0 || waitMs > SESSION_WRITE_LOCK_WAIT_MS)
2083
+ throw new SkillSelectionError("INVALID_CONTEXT_BUDGET", "A session write lock wait must be within its supported bound.");
2084
+ const waitUntil = performance.now() + waitMs;
1776
2085
  const locks = [...new Set(sessionIds)].map((sessionId) => ({ sessionId, path: `${sessionReceiptPath(sessionId, options)}.write-lock` })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
1777
2086
  const owned = [];
1778
2087
  const assertOwned = () => {
1779
2088
  for (const lock of owned) {
1780
- const current = lstatSync2(lock.path, { throwIfNoEntry: false });
2089
+ const current = lstatSync3(lock.path, { throwIfNoEntry: false });
1781
2090
  if (!current?.isFile() || current.dev !== lock.dev || current.ino !== lock.ino) {
1782
2091
  throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "A session write lock changed during the operation; no replacement is permitted.");
1783
2092
  }
@@ -1788,11 +2097,14 @@ function withSessionWriteLocks(sessionIds, options, action) {
1788
2097
  assertRegularPath(lock.path, true);
1789
2098
  const marker = { schemaVersion: identity ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity };
1790
2099
  let acquired;
1791
- try {
1792
- acquired = publishSessionLock(lock.path, marker);
1793
- } catch (error) {
1794
- if (error.code !== "EEXIST")
1795
- throw error;
2100
+ while (!acquired) {
2101
+ try {
2102
+ acquired = publishSessionLock(lock.path, marker);
2103
+ break;
2104
+ } catch (error) {
2105
+ if (error.code !== "EEXIST")
2106
+ throw error;
2107
+ }
1796
2108
  try {
1797
2109
  const stale = readSessionLock(lock.sessionId, options);
1798
2110
  const receipt = stale && readSkillSessionSnapshotIfExists(lock.sessionId, options);
@@ -1805,14 +2117,20 @@ function withSessionWriteLocks(sessionIds, options, action) {
1805
2117
  }
1806
2118
  try {
1807
2119
  acquired = publishSessionLock(lock.path, marker);
2120
+ break;
1808
2121
  } catch (retryError) {
1809
- if (retryError.code === "EEXIST") {
1810
- if (!readSessionLock(lock.sessionId, options))
1811
- throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The session lock changed during acquisition.");
1812
- throw new SkillSelectionError("SESSION_WRITE_BUSY", "Another Skills process owns a required session write lock; retry after it completes. A surviving legacy lock requires explicit recovery review.");
1813
- }
1814
- throw retryError;
2122
+ if (retryError.code !== "EEXIST")
2123
+ throw retryError;
2124
+ }
2125
+ const held = inspectSessionLockSettled(lock.sessionId, options);
2126
+ const remaining = waitUntil - performance.now();
2127
+ if (remaining <= 0) {
2128
+ if (!held)
2129
+ throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The session lock changed during acquisition.");
2130
+ throw new SkillSelectionError("SESSION_WRITE_BUSY", "Another Skills process owns a required session write lock; retry after it completes. A surviving legacy lock requires explicit recovery review.");
1815
2131
  }
2132
+ if (held)
2133
+ sleepSync(Math.min(remaining, SESSION_WRITE_LOCK_RETRY_MIN_MS + Math.floor(Math.random() * (SESSION_WRITE_LOCK_RETRY_MAX_MS - SESSION_WRITE_LOCK_RETRY_MIN_MS))));
1816
2134
  }
1817
2135
  owned.push(acquired);
1818
2136
  }
@@ -1821,7 +2139,7 @@ function withSessionWriteLocks(sessionIds, options, action) {
1821
2139
  for (const lock of owned.reverse()) {
1822
2140
  try {
1823
2141
  for (const path of [lock.path, lock.stagingPath]) {
1824
- const current = lstatSync2(path, { throwIfNoEntry: false });
2142
+ const current = lstatSync3(path, { throwIfNoEntry: false });
1825
2143
  if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
1826
2144
  unlinkSync(path);
1827
2145
  }
@@ -1831,8 +2149,8 @@ function withSessionWriteLocks(sessionIds, options, action) {
1831
2149
  }
1832
2150
  }
1833
2151
  }
1834
- function withSessionWriteLock(sessionId, options, action) {
1835
- return withSessionWriteLocks([sessionId], options, action);
2152
+ function withSessionWriteLock(sessionId, options, action, waitMs = 0) {
2153
+ return withSessionWriteLocks([sessionId], options, action, waitMs);
1836
2154
  }
1837
2155
  function sameSnapshot(actual, expected) {
1838
2156
  return actual === null ? expected === null : expected !== null && actual.sessionId === expected.sessionId && actual.generation === expected.generation && actual.receiptSha256 === expected.receiptSha256;
@@ -1873,7 +2191,7 @@ function writeSkillSession(receipt, expected, options = {}) {
1873
2191
  parentChanged();
1874
2192
  }
1875
2193
  writeSelectionJson(sessionReceiptPath(receipt.sessionId, options), next);
1876
- });
2194
+ }, SESSION_WRITE_LOCK_WAIT_MS);
1877
2195
  }
1878
2196
  function syncDirectory(path) {
1879
2197
  assertRegularPath(path);
@@ -1909,8 +2227,8 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
1909
2227
  if (before.receipt.profile.authority !== replacement.profile.authority || before.receipt.profile.workspaceId !== replacement.profile.workspaceId) {
1910
2228
  throw new SkillSelectionError("PROFILE_IDENTITY_MISMATCH", "A session reconciliation cannot change its Skills authority or workspace.");
1911
2229
  }
1912
- const directory = join4(selectionCacheRoot(options), "session-reconciliations", randomUUID());
1913
- const archivePath = join4(directory, "original.json"), replacementPath = join4(directory, "replacement.json"), receiptPath = join4(directory, "receipt.json");
2230
+ const directory = join6(selectionCacheRoot(options), "session-reconciliations", randomUUID());
2231
+ const archivePath = join6(directory, "original.json"), replacementPath = join6(directory, "replacement.json"), receiptPath = join6(directory, "receipt.json");
1914
2232
  const afterSha256 = sha256Hex(bytes);
1915
2233
  archiveBytes(archivePath, before.bytes);
1916
2234
  archiveBytes(replacementPath, bytes);
@@ -1918,7 +2236,7 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
1918
2236
  archiveBytes(receiptPath, Buffer.from(`${JSON.stringify(receipt)}
1919
2237
  `));
1920
2238
  syncDirectory(directory);
1921
- syncDirectory(dirname2(directory));
2239
+ syncDirectory(dirname3(directory));
1922
2240
  syncDirectory(selectionCacheRoot(options));
1923
2241
  if (sha256Hex(readRegularFile(archivePath, MAX_PROFILE_DOCUMENT_BYTES)) !== before.sha256 || !sameSnapshot(readSkillSessionSnapshot(replacement.sessionId, options), expected)) {
1924
2242
  throw new SkillSelectionError("SESSION_RECEIPT_CHANGED", "The session or its preservation archive changed before replacement; retained evidence must be reviewed.");
@@ -1933,11 +2251,11 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
1933
2251
  throw new SkillSelectionError("SESSION_RECONCILIATION_INCOMPLETE", `Inspect sessions show and the preserved operation receipt at ${receiptPath} before any retry; replacement may already have committed.`);
1934
2252
  }
1935
2253
  return { archivePath, receiptPath, beforeSha256: before.sha256, afterSha256 };
1936
- });
2254
+ }, SESSION_WRITE_LOCK_WAIT_MS);
1937
2255
  }
1938
2256
 
1939
2257
  // src/lib/agent-policy-limits.ts
1940
- import { basename as basename4, isAbsolute as isAbsolute5, join as join5, resolve as resolve6, sep as sep2 } from "path";
2258
+ import { basename as basename4, isAbsolute as isAbsolute5, join as join7, resolve as resolve7, sep as sep2 } from "path";
1941
2259
 
1942
2260
  // src/lib/codex-native-compatibility.ts
1943
2261
  var MEASURED_CODEX_CAPABILITIES = {
@@ -1956,7 +2274,9 @@ var MEASURED_CODEX_CAPABILITIES = {
1956
2274
  "codex-cli 0.161.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"],
1957
2275
  "codex-cli 0.162.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"],
1958
2276
  "codex-cli 0.162.1": ["hooks", "qualified-skill-catalog", "installed-plugin-review"],
1959
- "codex-cli 0.163.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"]
2277
+ "codex-cli 0.163.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"],
2278
+ "codex-cli 0.164.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"],
2279
+ "codex-cli 0.165.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"]
1960
2280
  };
1961
2281
  function supportsCodexNativeCapability(version2, capability) {
1962
2282
  return typeof version2 === "string" && Object.hasOwn(MEASURED_CODEX_CAPABILITIES, version2) && MEASURED_CODEX_CAPABILITIES[version2].includes(capability);
@@ -1964,37 +2284,41 @@ function supportsCodexNativeCapability(version2, capability) {
1964
2284
  var SUPPORTED_CODEX_HOOK_VERSIONS = Object.freeze(Object.keys(MEASURED_CODEX_CAPABILITIES).filter((version2) => supportsCodexNativeCapability(version2, "hooks")));
1965
2285
 
1966
2286
  // src/lib/claude-marketplace-entry-witness.ts
1967
- import { createHash as createHash4 } from "crypto";
1968
- import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
2287
+ import { createHash as createHash6 } from "crypto";
2288
+ import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, resolve as resolve5 } from "path";
1969
2289
 
1970
2290
  // src/lib/claude-settings-witness.ts
1971
- import { createHash as createHash3 } from "crypto";
1972
- import { closeSync as closeSync3, constants as constants3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3, readSync as readSync2 } from "fs";
1973
- import { basename, dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
2291
+ import { createHash as createHash5 } from "crypto";
2292
+ import { closeSync as closeSync3, constants as constants3, fstatSync as fstatSync3, lstatSync as lstatSync4, openSync as openSync3, readSync as readSync2 } from "fs";
2293
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
1974
2294
  var CLAUDE_SETTINGS_WITNESS_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
1975
2295
  function need(value, reason) {
1976
2296
  if (!value)
1977
2297
  throw new Error(`Claude settings witness ${reason}`);
1978
2298
  }
1979
2299
  function absolutePath(value) {
1980
- need(typeof value === "string" && value.length > 0 && value.length <= CLAUDE_SETTINGS_WITNESS_LIMITS.pathCharacters && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute2(value) && resolve3(value) === value, "requires a normalized absolute path");
2300
+ need(typeof value === "string" && value.length > 0 && value.length <= CLAUDE_SETTINGS_WITNESS_LIMITS.pathCharacters && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute2(value) && resolve4(value) === value, "requires a normalized absolute path");
1981
2301
  }
1982
2302
  var 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;
1983
2303
  function parents(path) {
1984
2304
  const result = [];
1985
- for (let at = dirname3(path);; at = dirname3(at)) {
1986
- const stat = lstatSync3(at, { bigint: true });
2305
+ for (let at = dirname4(path);; at = dirname4(at)) {
2306
+ const stat = lstatSync4(at, { bigint: true });
1987
2307
  need(stat.isDirectory() && !stat.isSymbolicLink(), "path traverses a symlink or non-directory");
1988
2308
  result.push([at, stat]);
1989
- if (at === dirname3(at))
2309
+ if (at === dirname4(at))
1990
2310
  return result;
1991
2311
  }
1992
2312
  }
1993
2313
  function readNativeSettingsWitnessFile(path, budget, filename = "settings.json") {
1994
2314
  absolutePath(path);
1995
2315
  need(basename(path) === filename, `requires ${filename}`);
2316
+ return readNativeDiscoveryWitnessFile(path, budget);
2317
+ }
2318
+ function readNativeDiscoveryWitnessFile(path, budget) {
2319
+ absolutePath(path);
1996
2320
  need(Number.isSafeInteger(budget.remaining) && budget.remaining >= 0, "has an invalid byte budget");
1997
- const ancestors = parents(path), initial = lstatSync3(path, { bigint: true });
2321
+ const ancestors = parents(path), initial = lstatSync4(path, { bigint: true });
1998
2322
  need(initial.isFile() && !initial.isSymbolicLink() && initial.size <= BigInt(CLAUDE_SETTINGS_WITNESS_LIMITS.bytes), "is not a bounded regular file");
1999
2323
  need(initial.size <= BigInt(budget.remaining), "exceeds its aggregate byte limit");
2000
2324
  const fd = openSync3(path, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
@@ -2009,10 +2333,10 @@ function readNativeSettingsWitnessFile(path, budget, filename = "settings.json")
2009
2333
  break;
2010
2334
  length += count;
2011
2335
  }
2012
- const after = fstatSync3(fd, { bigint: true }), current = lstatSync3(path, { bigint: true });
2336
+ const after = fstatSync3(fd, { bigint: true }), current = lstatSync4(path, { bigint: true });
2013
2337
  need(current.isFile() && sameFile(opened, after) && sameFile(after, current) && BigInt(length) === opened.size, "changed during read");
2014
2338
  for (const [ancestor, before] of ancestors) {
2015
- const now = lstatSync3(ancestor, { bigint: true });
2339
+ const now = lstatSync4(ancestor, { bigint: true });
2016
2340
  need(now.isDirectory() && before.dev === now.dev && before.ino === now.ino && before.mode === now.mode, "parent changed during read");
2017
2341
  }
2018
2342
  budget.remaining -= length;
@@ -2232,7 +2556,7 @@ function settingsDigest(text, version2 = 1) {
2232
2556
  }
2233
2557
  return true;
2234
2558
  });
2235
- return createHash3("sha256").update(`hasna.skills.claude-settings.v${version2}\x00`).update(JSON.stringify(canonical(value))).digest("hex");
2559
+ return createHash5("sha256").update(`hasna.skills.claude-settings.v${version2}\x00`).update(JSON.stringify(canonical(value))).digest("hex");
2236
2560
  }
2237
2561
  function hashClaudeSettingsReplacement(text, budget) {
2238
2562
  need(typeof text === "string", "requires settings text");
@@ -2268,7 +2592,7 @@ function upgradeClaudeSettingsWitness(previous, reviewedSettingsPath) {
2268
2592
  need(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === undefined || previous.hashMode === "bytes" || 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");
2269
2593
  const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
2270
2594
  const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
2271
- const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash3("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : 2);
2595
+ const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash5("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : 2);
2272
2596
  need(beforeDigest === previous.sha256, "reviewed preimage does not match the legacy witness");
2273
2597
  const sha256 = settingsDigest(current, 3);
2274
2598
  need(settingsDigest(before, 3) === sha256, "non-preference settings changed; explicit discovery review required");
@@ -2288,12 +2612,91 @@ function upgradeClaudeSettingsWitnessV4(previous, reviewedSettingsPath) {
2288
2612
  need(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === undefined || previous.hashMode === "bytes" || previous.hashMode === "claude-settings-v1" || previous.hashMode === "claude-settings-v2" || previous.hashMode === "claude-settings-v3") && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact prior settings witness");
2289
2613
  const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
2290
2614
  const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
2291
- const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash3("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : previous.hashMode === "claude-settings-v2" ? 2 : 3);
2615
+ const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash5("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : previous.hashMode === "claude-settings-v2" ? 2 : 3);
2292
2616
  need(beforeDigest === previous.sha256, "reviewed preimage does not match the prior witness");
2293
2617
  const sha256 = settingsDigest(current, 4);
2294
2618
  need(settingsDigest(before, 4) === sha256, "non-preference settings changed; explicit discovery review required");
2295
2619
  return { path: previous.path, hashMode: "claude-settings-v4", sha256 };
2296
2620
  }
2621
+ var V5_DISCOVERY_KEY = /skill|plugin|hook|marketplace/i;
2622
+ var V5_BOUND_KEYS = new Set(["apiKeyHelper", "awsAuthRefresh", "awsCredentialExport", "otelHeadersHelper", "fileSuggestion", "outputStyle", "agent"]);
2623
+ var V5_DISCOVERY_ENV = new Set(["CLAUDE_CONFIG_DIR"]);
2624
+ var V5_SKILL_RULE = /^Skill(?:\(|$)/;
2625
+ var V5_PERMISSION_LISTS = new Set(["allow", "deny", "ask"]);
2626
+ function narrowedClaudeSettings(value) {
2627
+ const entries = [];
2628
+ for (const [key, child] of value.entries) {
2629
+ if (V5_DISCOVERY_KEY.test(key) || V5_BOUND_KEYS.has(key)) {
2630
+ entries.push([key, child]);
2631
+ continue;
2632
+ }
2633
+ if (key === "permissions") {
2634
+ if (child.kind !== "object") {
2635
+ entries.push([key, child]);
2636
+ continue;
2637
+ }
2638
+ const kept = [];
2639
+ for (const [name, rules] of child.entries) {
2640
+ if (name === "additionalDirectories") {
2641
+ kept.push([name, rules]);
2642
+ continue;
2643
+ }
2644
+ if (!V5_PERMISSION_LISTS.has(name))
2645
+ continue;
2646
+ if (rules.kind !== "array") {
2647
+ kept.push([name, rules]);
2648
+ continue;
2649
+ }
2650
+ const skillRules = rules.items.filter((rule) => rule.kind !== "string" || V5_SKILL_RULE.test(rule.value));
2651
+ if (skillRules.length)
2652
+ kept.push([name, { kind: "array", items: skillRules }]);
2653
+ }
2654
+ if (kept.length)
2655
+ entries.push([key, { kind: "object", entries: kept }]);
2656
+ continue;
2657
+ }
2658
+ if (key === "env") {
2659
+ if (child.kind !== "object") {
2660
+ entries.push([key, child]);
2661
+ continue;
2662
+ }
2663
+ const kept = child.entries.filter(([name]) => V5_DISCOVERY_ENV.has(name) || V5_DISCOVERY_KEY.test(name));
2664
+ if (kept.length)
2665
+ entries.push([key, { kind: "object", entries: kept }]);
2666
+ }
2667
+ }
2668
+ return { kind: "object", entries };
2669
+ }
2670
+ function claudeDiscoveryControlKeys(text) {
2671
+ return narrowedClaudeSettings(parseNativeJsonObject(text)).entries.map(([key]) => key);
2672
+ }
2673
+ function claudeDiscoveryEnvironmentKeys(text) {
2674
+ const environment = narrowedClaudeSettings(parseNativeJsonObject(text)).entries.find(([key]) => key === "env")?.[1];
2675
+ return environment?.kind === "object" ? environment.entries.map(([key]) => key) : [];
2676
+ }
2677
+ function settingsDigestV5(text) {
2678
+ return createHash5("sha256").update("hasna.skills.claude-settings.v5\x00").update(JSON.stringify(canonical(narrowedClaudeSettings(parse2(text))))).digest("hex");
2679
+ }
2680
+ function captureClaudeSettingsV5(path, budget = { remaining: 256 * 1024 * 1024 }) {
2681
+ return { path, hashMode: "claude-settings-v5", sha256: settingsDigestV5(readNativeSettingsWitnessFile(path, budget)) };
2682
+ }
2683
+ function hashClaudeSettingsReplacementV5(text, budget) {
2684
+ need(typeof text === "string", "requires settings text");
2685
+ const bytes = Buffer.byteLength(text);
2686
+ need(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
2687
+ budget.remaining -= bytes;
2688
+ return settingsDigestV5(text);
2689
+ }
2690
+ function upgradeClaudeSettingsWitnessV5(previous, reviewedSettingsPath) {
2691
+ need(previous && Object.keys(previous).every((key) => ["path", "hashMode", "sha256"].includes(key)) && (previous.hashMode === undefined || ["bytes", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-settings-v4"].includes(previous.hashMode)) && typeof previous.sha256 === "string" && /^[a-f0-9]{64}$/.test(previous.sha256), "requires an exact prior settings witness");
2692
+ const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
2693
+ const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
2694
+ const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash5("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : previous.hashMode === "claude-settings-v2" ? 2 : previous.hashMode === "claude-settings-v3" ? 3 : 4);
2695
+ need(beforeDigest === previous.sha256, "reviewed preimage does not match the prior witness");
2696
+ const sha256 = settingsDigestV5(current);
2697
+ need(settingsDigestV5(before) === sha256, "discovery settings changed; explicit discovery review required");
2698
+ return { path: previous.path, hashMode: "claude-settings-v5", sha256 };
2699
+ }
2297
2700
  function hashNativeJsonControls(text, excludedMetadata) {
2298
2701
  need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
2299
2702
  const value = parse2(text);
@@ -2303,7 +2706,7 @@ function hashNativeJsonControls(text, excludedMetadata) {
2303
2706
  need(child.kind === "string" && child.value.length <= 128, "has invalid version metadata");
2304
2707
  return false;
2305
2708
  });
2306
- return createHash3("sha256").update("hasna.skills.native-json-controls.v1\x00").update(JSON.stringify(canonical(value))).digest("hex");
2709
+ return createHash5("sha256").update("hasna.skills.native-json-controls.v1\x00").update(JSON.stringify(canonical(value))).digest("hex");
2307
2710
  }
2308
2711
  function hashSumiNativeJsonControls(text) {
2309
2712
  need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
@@ -2324,7 +2727,7 @@ function hashSumiNativeJsonControls(text) {
2324
2727
  });
2325
2728
  return child.entries.length > 0;
2326
2729
  });
2327
- return createHash3("sha256").update("hasna.skills.sumi-settings.v1\x00").update(JSON.stringify(value)).digest("hex");
2730
+ return createHash5("sha256").update("hasna.skills.sumi-settings.v1\x00").update(JSON.stringify(value)).digest("hex");
2328
2731
  }
2329
2732
 
2330
2733
  // src/lib/claude-marketplace-entry-witness.ts
@@ -2392,7 +2795,7 @@ var stringArray = (value) => value?.kind === "array" && value.items.every(isStri
2392
2795
  var stringObject = (value) => value?.kind === "object" && value.entries.every(([, item]) => isString(item));
2393
2796
  function claudeMarketplaceEntrySourceValid(source) {
2394
2797
  const path = source?.path;
2395
- return typeof path === "string" && path.length > 0 && path.length <= PATH_CHARACTERS && !/[\x00-\x1f\x7f]/.test(path) && isAbsolute3(path) && resolve4(path) === path && basename2(path) === "marketplace.json" && basename2(dirname4(path)) === ".claude-plugin" && typeof source.marketplace === "string" && NAME.test(source.marketplace) && !source.marketplace.includes("..") && typeof source.plugin === "string" && NAME.test(source.plugin);
2798
+ return typeof path === "string" && path.length > 0 && path.length <= PATH_CHARACTERS && !/[\x00-\x1f\x7f]/.test(path) && isAbsolute3(path) && resolve5(path) === path && basename2(path) === "marketplace.json" && basename2(dirname5(path)) === ".claude-plugin" && typeof source.marketplace === "string" && NAME.test(source.marketplace) && !source.marketplace.includes("..") && typeof source.plugin === "string" && NAME.test(source.plugin);
2396
2799
  }
2397
2800
  function canonical2(value) {
2398
2801
  if (value.kind === "string")
@@ -2505,7 +2908,7 @@ function hashClaudeMarketplaceEntry(text, selector) {
2505
2908
  ["allowCrossMarketplaceDependenciesOn", field(root, "allowCrossMarketplaceDependenciesOn") ?? nullValue]
2506
2909
  ] };
2507
2910
  const projection = { kind: "object", entries: [["marketplace", marketplace], ["plugin", entry]] };
2508
- return createHash4("sha256").update(DOMAIN).update(canonical2(projection)).digest("hex");
2911
+ return createHash6("sha256").update(DOMAIN).update(canonical2(projection)).digest("hex");
2509
2912
  }
2510
2913
  function captureClaudeMarketplaceEntry(path, marketplace, plugin, budget = { remaining: 256 * 1024 * 1024 }) {
2511
2914
  need2(claudeMarketplaceEntrySourceValid({ path, marketplace, plugin }), "requires a normalized absolute .claude-plugin/marketplace.json path and exact names");
@@ -2519,13 +2922,13 @@ function captureClaudeMarketplaceEntry(path, marketplace, plugin, budget = { rem
2519
2922
  }
2520
2923
 
2521
2924
  // src/lib/claude-plugin-cache-marker.ts
2522
- import { createHash as createHash5 } from "crypto";
2523
- import { closeSync as closeSync4, constants as constants4, fstatSync as fstatSync4, lstatSync as lstatSync4, openSync as openSync4, readSync as readSync3 } from "fs";
2524
- import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute4, resolve as resolve5, sep } from "path";
2925
+ import { createHash as createHash7 } from "crypto";
2926
+ import { closeSync as closeSync4, constants as constants4, fstatSync as fstatSync4, lstatSync as lstatSync5, openSync as openSync4, readSync as readSync3 } from "fs";
2927
+ import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute4, resolve as resolve6, sep } from "path";
2525
2928
  var CLAUDE_ORPHAN_MARKER_MODE = "claude-plugin-orphaned-at-v1";
2526
2929
  var CACHE = `${sep}.claude${sep}plugins${sep}cache${sep}`;
2527
2930
  function isClaudePluginCacheOrphanMarker(path) {
2528
- if (typeof path !== "string" || basename3(path) !== ".orphaned_at" || !isAbsolute4(path) || resolve5(path) !== path || path.includes("\x00"))
2931
+ if (typeof path !== "string" || basename3(path) !== ".orphaned_at" || !isAbsolute4(path) || resolve6(path) !== path || path.includes("\x00"))
2529
2932
  return false;
2530
2933
  const at = path.indexOf(CACHE);
2531
2934
  if (at < 0 || path.indexOf(CACHE, at + 1) >= 0)
@@ -2537,10 +2940,10 @@ function same(left, right) {
2537
2940
  return Boolean(right && right.isFile() && left.dev === right.dev && left.ino === right.ino && left.uid === right.uid && left.mode === right.mode && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs);
2538
2941
  }
2539
2942
  function safe(path) {
2540
- for (let cursor = path;; cursor = dirname5(cursor)) {
2541
- if (lstatSync4(cursor, { throwIfNoEntry: false })?.isSymbolicLink())
2943
+ for (let cursor = path;; cursor = dirname6(cursor)) {
2944
+ if (lstatSync5(cursor, { throwIfNoEntry: false })?.isSymbolicLink())
2542
2945
  throw new Error("Refusing symlink Claude orphan marker input");
2543
- if (dirname5(cursor) === cursor)
2946
+ if (dirname6(cursor) === cursor)
2544
2947
  break;
2545
2948
  }
2546
2949
  }
@@ -2550,7 +2953,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
2550
2953
  if (changes?.has(path))
2551
2954
  throw new Error("Claude orphan markers cannot intersect a planned write");
2552
2955
  safe(path);
2553
- const before = lstatSync4(path, { throwIfNoEntry: false });
2956
+ const before = lstatSync5(path, { throwIfNoEntry: false });
2554
2957
  if (!before)
2555
2958
  return null;
2556
2959
  const uid = process.getuid?.();
@@ -2571,7 +2974,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
2571
2974
  }
2572
2975
  if (!/^[1-9][0-9]{12}$/.test(bytes.subarray(0, length).toString("latin1")))
2573
2976
  throw new Error("Malformed Claude orphan marker timestamp");
2574
- const after = fstatSync4(fd), current = lstatSync4(path, { throwIfNoEntry: false });
2977
+ const after = fstatSync4(fd), current = lstatSync5(path, { throwIfNoEntry: false });
2575
2978
  safe(path);
2576
2979
  if (!same(opened, after) || !same(after, current) || length !== after.size)
2577
2980
  throw new Error("Claude orphan marker changed during read");
@@ -2580,7 +2983,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
2580
2983
  throw new Error("Raw discovery aggregate byte limit exceeded");
2581
2984
  budget.remaining -= length;
2582
2985
  }
2583
- return createHash5("sha256").update(`${CLAUDE_ORPHAN_MARKER_MODE}\x00${path}\x00file`).digest("hex");
2986
+ return createHash7("sha256").update(`${CLAUDE_ORPHAN_MARKER_MODE}\x00${path}\x00file`).digest("hex");
2584
2987
  } finally {
2585
2988
  closeSync4(fd);
2586
2989
  }
@@ -2640,7 +3043,7 @@ function assertAgentPolicyCollections(policy) {
2640
3043
  text(control.pluginId, 1024);
2641
3044
  text(control.namespace, 64);
2642
3045
  text(control.pluginParent);
2643
- requireBound(isAbsolute5(control.pluginParent) && resolve6(control.pluginParent) === control.pluginParent && /^[a-f0-9]{64}$/.test(control.manifestSha256));
3046
+ requireBound(isAbsolute5(control.pluginParent) && resolve7(control.pluginParent) === control.pluginParent && /^[a-f0-9]{64}$/.test(control.manifestSha256));
2644
3047
  if (control.remotePluginId !== undefined)
2645
3048
  requireBound(typeof control.remotePluginId === "string" && /^[A-Za-z0-9_~-]{1,1024}$/.test(control.remotePluginId));
2646
3049
  if (control.skillsOnly !== undefined)
@@ -2658,7 +3061,7 @@ function assertAgentPolicyCollections(policy) {
2658
3061
  text(control.pluginId, 1024);
2659
3062
  text(control.namespace, 64);
2660
3063
  text(control.pluginParent);
2661
- requireBound(!ids.has(control.pluginId) && isAbsolute5(control.pluginParent) && resolve6(control.pluginParent) === control.pluginParent && ["local", "git", "npm"].includes(control.sourceType) && typeof control.sourceSha256 === "string" && /^[a-f0-9]{64}$/.test(control.sourceSha256));
3064
+ requireBound(!ids.has(control.pluginId) && isAbsolute5(control.pluginParent) && resolve7(control.pluginParent) === control.pluginParent && ["local", "git", "npm"].includes(control.sourceType) && typeof control.sourceSha256 === "string" && /^[a-f0-9]{64}$/.test(control.sourceSha256));
2662
3065
  ids.add(control.pluginId);
2663
3066
  }
2664
3067
  }
@@ -2667,7 +3070,7 @@ function assertAgentPolicyCollections(policy) {
2667
3070
  requireBound(agent === "gemini" && object(executable) && Object.keys(executable).length === 3 && executable.command === "gemini");
2668
3071
  for (const path of [executable.path, executable.target]) {
2669
3072
  text(path);
2670
- requireBound(!/[\x00-\x1f\x7f-\x9f\u200e\u200f\u202a-\u202e\u2066-\u2069]/.test(path) && isAbsolute5(path) && resolve6(path) === path);
3073
+ requireBound(!/[\x00-\x1f\x7f-\x9f\u200e\u200f\u202a-\u202e\u2066-\u2069]/.test(path) && isAbsolute5(path) && resolve7(path) === path);
2671
3074
  }
2672
3075
  }
2673
3076
  if (bridge.disabledBuiltins !== undefined)
@@ -2695,11 +3098,11 @@ function assertAgentPolicyCollections(policy) {
2695
3098
  requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && Object.keys(proof).every((key) => ["roots", "parents", "directories"].includes(key)) && roots.length + parents2.length > 0 && new Set(roots).size === roots.length && new Set(parents2).size === parents2.length);
2696
3099
  for (const root of roots) {
2697
3100
  text(root);
2698
- requireBound(isAbsolute5(root) && resolve6(root) === root);
3101
+ requireBound(isAbsolute5(root) && resolve7(root) === root);
2699
3102
  }
2700
3103
  for (const parent of parents2) {
2701
3104
  text(parent);
2702
- requireBound(isAbsolute5(parent) && resolve6(parent) === parent);
3105
+ requireBound(isAbsolute5(parent) && resolve7(parent) === parent);
2703
3106
  }
2704
3107
  for (const directory of array(proof.directories, AGENT_POLICY_LIMITS.discoveryDirectories)) {
2705
3108
  requireBound(object(directory));
@@ -2719,7 +3122,7 @@ function assertAgentPolicyCollections(policy) {
2719
3122
  text(input.namespace, 64);
2720
3123
  text(input.pluginParent);
2721
3124
  text(input.sourceRoot);
2722
- requireBound(!ids.has(input.pluginId) && [input.pluginParent, input.sourceRoot].every((path) => isAbsolute5(path) && resolve6(path) === path) && typeof input.sourceSha256 === "string" && /^[a-f0-9]{64}$/.test(input.sourceSha256));
3125
+ requireBound(!ids.has(input.pluginId) && [input.pluginParent, input.sourceRoot].every((path) => isAbsolute5(path) && resolve7(path) === path) && typeof input.sourceSha256 === "string" && /^[a-f0-9]{64}$/.test(input.sourceSha256));
2723
3126
  ids.add(input.pluginId);
2724
3127
  }
2725
3128
  if (proof.directories !== undefined)
@@ -2733,14 +3136,14 @@ function assertAgentPolicyCollections(policy) {
2733
3136
  const proof = record(value.codexSemanticCache, 9), witness = record(proof.witness, 7);
2734
3137
  requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && [8, 9].includes(Object.keys(proof).length) && Object.keys(proof).every((key) => ["schema", "configPath", "hookRootAlias", "witness", "appOnlyParents", "previousPolicySha256", "supersededSources", "supersededDirectories", "reviewedHookParents"].includes(key)) && proof.schema === "hasna.codex-semantic-cache-discovery.v1" && typeof proof.previousPolicySha256 === "string" && /^[a-f0-9]{64}$/.test(proof.previousPolicySha256) && [6, 7].includes(Object.keys(witness).length) && Object.keys(witness).every((key) => ["schema", "cacheRoot", "controls", "appOnlyParents", "skills", "sha256", "hookParents"].includes(key)) && witness.schema === "hasna.codex-semantic-cache-witness.v1" && typeof witness.sha256 === "string" && /^[a-f0-9]{64}$/.test(witness.sha256));
2735
3138
  text(proof.configPath);
2736
- requireBound(isAbsolute5(proof.configPath) && resolve6(proof.configPath) === proof.configPath && basename4(proof.configPath) === "config.toml");
3139
+ requireBound(isAbsolute5(proof.configPath) && resolve7(proof.configPath) === proof.configPath && basename4(proof.configPath) === "config.toml");
2737
3140
  text(witness.cacheRoot);
2738
- requireBound(isAbsolute5(witness.cacheRoot) && resolve6(witness.cacheRoot) === witness.cacheRoot);
3141
+ requireBound(isAbsolute5(witness.cacheRoot) && resolve7(witness.cacheRoot) === witness.cacheRoot);
2739
3142
  if (proof.hookRootAlias !== null) {
2740
3143
  requireBound(object(proof.hookRootAlias) && Object.keys(proof.hookRootAlias).length === 2);
2741
3144
  for (const key of ["alias", "target"]) {
2742
3145
  text(proof.hookRootAlias[key]);
2743
- requireBound(isAbsolute5(proof.hookRootAlias[key]) && resolve6(proof.hookRootAlias[key]) === proof.hookRootAlias[key]);
3146
+ requireBound(isAbsolute5(proof.hookRootAlias[key]) && resolve7(proof.hookRootAlias[key]) === proof.hookRootAlias[key]);
2744
3147
  }
2745
3148
  }
2746
3149
  const parents2 = array(proof.appOnlyParents, 4096);
@@ -2748,7 +3151,7 @@ function assertAgentPolicyCollections(policy) {
2748
3151
  requireBound(object(parent) && Object.keys(parent).length === 5 && parent.role === "app-only");
2749
3152
  for (const key of ["pluginId", "namespace", "pluginParent", "remotePluginId"])
2750
3153
  text(parent[key]);
2751
- requireBound(isAbsolute5(parent.pluginParent) && resolve6(parent.pluginParent) === parent.pluginParent);
3154
+ requireBound(isAbsolute5(parent.pluginParent) && resolve7(parent.pluginParent) === parent.pluginParent);
2752
3155
  }
2753
3156
  if (proof.reviewedHookParents !== undefined)
2754
3157
  for (const parent of array(proof.reviewedHookParents, 4096)) {
@@ -2760,9 +3163,9 @@ function assertAgentPolicyCollections(policy) {
2760
3163
  text(parent.remotePluginId);
2761
3164
  else {
2762
3165
  text(parent.marketplaceSourcePath);
2763
- requireBound(typeof parent.marketplaceSha256 === "string" && /^[a-f0-9]{64}$/.test(parent.marketplaceSha256) && isAbsolute5(parent.marketplaceSourcePath) && resolve6(parent.marketplaceSourcePath) === parent.marketplaceSourcePath && basename4(parent.marketplaceSourcePath) === "marketplace.json");
3166
+ requireBound(typeof parent.marketplaceSha256 === "string" && /^[a-f0-9]{64}$/.test(parent.marketplaceSha256) && isAbsolute5(parent.marketplaceSourcePath) && resolve7(parent.marketplaceSourcePath) === parent.marketplaceSourcePath && basename4(parent.marketplaceSourcePath) === "marketplace.json");
2764
3167
  }
2765
- requireBound(isAbsolute5(parent.pluginParent) && resolve6(parent.pluginParent) === parent.pluginParent);
3168
+ requireBound(isAbsolute5(parent.pluginParent) && resolve7(parent.pluginParent) === parent.pluginParent);
2766
3169
  }
2767
3170
  for (const row of [...array(witness.controls, 4096), ...array(witness.appOnlyParents, 4096)]) {
2768
3171
  requireBound(object(row));
@@ -2782,9 +3185,9 @@ function assertAgentPolicyCollections(policy) {
2782
3185
  text(row.remotePluginId);
2783
3186
  else {
2784
3187
  text(row.marketplaceSourcePath);
2785
- requireBound(typeof row.marketplaceSha256 === "string" && /^[a-f0-9]{64}$/.test(row.marketplaceSha256) && isAbsolute5(row.marketplaceSourcePath) && resolve6(row.marketplaceSourcePath) === row.marketplaceSourcePath && basename4(row.marketplaceSourcePath) === "marketplace.json");
3188
+ requireBound(typeof row.marketplaceSha256 === "string" && /^[a-f0-9]{64}$/.test(row.marketplaceSha256) && isAbsolute5(row.marketplaceSourcePath) && resolve7(row.marketplaceSourcePath) === row.marketplaceSourcePath && basename4(row.marketplaceSourcePath) === "marketplace.json");
2786
3189
  }
2787
- requireBound(isAbsolute5(row.pluginParent) && resolve6(row.pluginParent) === row.pluginParent);
3190
+ requireBound(isAbsolute5(row.pluginParent) && resolve7(row.pluginParent) === row.pluginParent);
2788
3191
  for (const key of ["manifestSha256", "hooksSha256"])
2789
3192
  requireBound(typeof row[key] === "string" && /^[a-f0-9]{64}$/.test(row[key]));
2790
3193
  if (row.installation === "remote-receipt")
@@ -2817,17 +3220,17 @@ function assertAgentPolicyCollections(policy) {
2817
3220
  text(source.path);
2818
3221
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
2819
3222
  if (source.hashMode !== undefined)
2820
- requireBound(["bytes", "path-bytes", CLAUDE_ORPHAN_MARKER_MODE, "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-settings-v4", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "codex-settings-v5", "codex-settings-v6", "executable-v1", "sumi-settings-v1", "claude-marketplace-entry-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (["bytes", CLAUDE_ORPHAN_MARKER_MODE].includes(source.hashMode) || source.sha256 !== null));
3223
+ requireBound(["bytes", "path-bytes", CLAUDE_ORPHAN_MARKER_MODE, "claude-plugin-manifest-v1", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-settings-v4", "claude-settings-v5", "claude-marketplace-registry-v2", "codex-settings-v1", "codex-settings-v2", "codex-settings-v3", "codex-settings-v4", "codex-settings-v5", "codex-settings-v6", "codex-settings-v7", "executable-v1", "sumi-settings-v1", "claude-marketplace-entry-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (["bytes", CLAUDE_ORPHAN_MARKER_MODE].includes(source.hashMode) || source.sha256 !== null));
2821
3224
  if (source.hashMode === CLAUDE_ORPHAN_MARKER_MODE)
2822
3225
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && isClaudePluginCacheOrphanMarker(source.path) && source.managedPlugins === undefined && source.reviewArtifact === undefined);
2823
3226
  if (source.hashMode === "executable-v1")
2824
- requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && value.sources.some((row) => row.hashMode === "codex-settings-v6") && isAbsolute5(source.path) && resolve6(source.path) === source.path);
3227
+ requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && value.sources.some((row) => row.hashMode === "codex-settings-v6") && isAbsolute5(source.path) && resolve7(source.path) === source.path);
2825
3228
  if (source.hashMode === "codex-settings-v6") {
2826
3229
  const seen = new Set;
2827
3230
  for (const hook of array(source.hookSources, AGENT_POLICY_LIMITS.discoverySources)) {
2828
3231
  requireBound(object(hook) && Object.keys(hook).length === 2 && Object.keys(hook).every((key) => ["path", "sha256"].includes(key)));
2829
3232
  text(hook.path);
2830
- requireBound(isAbsolute5(hook.path) && resolve6(hook.path) === hook.path && !seen.has(hook.path));
3233
+ requireBound(isAbsolute5(hook.path) && resolve7(hook.path) === hook.path && !seen.has(hook.path));
2831
3234
  seen.add(hook.path);
2832
3235
  requireBound(hook.sha256 === null || typeof hook.sha256 === "string" && /^[a-f0-9]{64}$/.test(hook.sha256));
2833
3236
  requireBound(value.sources.some((row) => row.path === hook.path && row.sha256 === hook.sha256 && [undefined, "bytes"].includes(row.hashMode) && row.format === undefined && row.fields === undefined));
@@ -2835,15 +3238,15 @@ function assertAgentPolicyCollections(policy) {
2835
3238
  } else
2836
3239
  requireBound(source.hookSources === undefined);
2837
3240
  if (source.hashMode === "claude-plugin-manifest-v1")
2838
- requireBound(agent === "claude" && value.agent === "claude" && ["automatic", "reviewed"].includes(value.method) && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve6(source.path) === source.path && source.path.endsWith(join5(sep2, ".claude-plugin", "plugin.json")) && source.managedPlugins === undefined);
3241
+ requireBound(agent === "claude" && value.agent === "claude" && ["automatic", "reviewed"].includes(value.method) && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve7(source.path) === source.path && source.path.endsWith(join7(sep2, ".claude-plugin", "plugin.json")) && source.managedPlugins === undefined);
2839
3242
  if (source.hashMode === "sumi-settings-v1")
2840
- requireBound(agent === "sumi" && value.agent === "sumi" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve6(source.path) === source.path && basename4(source.path) === "sumi.json");
2841
- if (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4" || source.hashMode === "codex-settings-v5" || source.hashMode === "codex-settings-v6"))
2842
- requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve6(source.path) === source.path && basename4(source.path) === "config.toml");
2843
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4"))
2844
- requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve6(source.path) === source.path && basename4(source.path) === "settings.json");
3243
+ requireBound(agent === "sumi" && value.agent === "sumi" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve7(source.path) === source.path && basename4(source.path) === "sumi.json");
3244
+ if (source.hashMode === "codex-settings-v1" || source.hashMode === "codex-settings-v2" || (source.hashMode === "codex-settings-v3" || source.hashMode === "codex-settings-v4" || source.hashMode === "codex-settings-v5" || source.hashMode === "codex-settings-v6" || source.hashMode === "codex-settings-v7"))
3245
+ requireBound(agent === "codex" && value.agent === "codex" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve7(source.path) === source.path && basename4(source.path) === "config.toml");
3246
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || (source.hashMode === "claude-settings-v3" || source.hashMode === "claude-settings-v4" || source.hashMode === "claude-settings-v5"))
3247
+ requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve7(source.path) === source.path && basename4(source.path) === "settings.json");
2845
3248
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
2846
- requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve6(source.path) === source.path && basename4(source.path) === "known_marketplaces.json");
3249
+ requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute5(source.path) && resolve7(source.path) === source.path && basename4(source.path) === "known_marketplaces.json");
2847
3250
  if (source.hashMode === "claude-marketplace-entry-v1")
2848
3251
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && claudeMarketplaceEntrySourceValid(source));
2849
3252
  if (source.hashMode === "claude-plugin-registry") {
@@ -2862,9 +3265,9 @@ function assertAgentPolicyCollections(policy) {
2862
3265
  requireBound(agent === "codex" && value.method === "reviewed" && source.hashMode === "bytes" && object(artifact) && Object.keys(artifact).length === 6 && Object.keys(artifact).every((key) => ["version", "kind", "storeRoot", "originalPath", "sha256", "catalogSha256"].includes(key)) && artifact.version === 1 && artifact.kind === "codex-native-catalog" && artifact.sha256 === source.sha256 && /^[a-f0-9]{64}$/.test(artifact.catalogSha256));
2863
3266
  for (const path of [artifact.storeRoot, artifact.originalPath]) {
2864
3267
  text(path);
2865
- requireBound(isAbsolute5(path) && resolve6(path) === path);
3268
+ requireBound(isAbsolute5(path) && resolve7(path) === path);
2866
3269
  }
2867
- requireBound(source.path === join5(artifact.storeRoot, "agent-discovery", "artifacts", artifact.sha256, "catalog.json"));
3270
+ requireBound(source.path === join7(artifact.storeRoot, "agent-discovery", "artifacts", artifact.sha256, "catalog.json"));
2868
3271
  }
2869
3272
  if (source.format !== undefined)
2870
3273
  requireBound(["json", "toml", "yaml"].includes(source.format));
@@ -2915,10 +3318,10 @@ function serializeManagedSkillPolicy(value) {
2915
3318
  return text2;
2916
3319
  }
2917
3320
  function readManagedSkillPolicySnapshot(dataDir2 = getDataDirReadOnly()) {
2918
- const path = join6(dataDir2, "agent-policy.json");
3321
+ const path = join8(dataDir2, "agent-policy.json");
2919
3322
  let descriptor, observed = false;
2920
3323
  try {
2921
- const stat = lstatSync5(path);
3324
+ const stat = lstatSync6(path);
2922
3325
  observed = true;
2923
3326
  if (!stat.isFile() || stat.isSymbolicLink())
2924
3327
  throw refusal();
@@ -2944,7 +3347,7 @@ function readManagedSkillPolicySnapshot(dataDir2 = getDataDirReadOnly()) {
2944
3347
  return { text: text2, value: parseManagedSkillPolicy(text2) };
2945
3348
  } catch (error) {
2946
3349
  if (error.code === "ENOENT" && !observed) {
2947
- if (lstatSync5(path, { throwIfNoEntry: false }) === undefined)
3350
+ if (lstatSync6(path, { throwIfNoEntry: false }) === undefined)
2948
3351
  return null;
2949
3352
  }
2950
3353
  if (error instanceof SkillSelectionError)
@@ -2974,7 +3377,7 @@ function detectedIntegrationAgents(options = {}) {
2974
3377
  throw new Error("Invalid existing bridge agent inventory");
2975
3378
  const present = (path) => {
2976
3379
  try {
2977
- return lstatSync6(path, { throwIfNoEntry: false }) !== undefined;
3380
+ return lstatSync7(path, { throwIfNoEntry: false }) !== undefined;
2978
3381
  } catch {
2979
3382
  throw new Error("Agent presence cannot be inspected; review native configuration before hook installation");
2980
3383
  }
@@ -2984,13 +3387,13 @@ function detectedIntegrationAgents(options = {}) {
2984
3387
  return true;
2985
3388
  if (agent === "sumi") {
2986
3389
  const xdg = env.XDG_CONFIG_HOME;
2987
- const xdgSumi = xdg?.trim() ? resolve7(options.cwd ?? process.cwd(), xdg === "~" ? home : xdg.startsWith("~/") || xdg.startsWith("~\\") ? resolve7(home, xdg.slice(2)) : xdg, "sumi") : null;
2988
- return ["SUMI_HOME", "SUMI_CONFIG_DIR", "SUMI_CONFIG", "SUMI_CONFIG_CONTENT"].some((key) => env[key] !== undefined) || Bun.which("sumi-paths", { PATH: env.PATH ?? "" }) !== null || NATIVE_SKILL_ROOTS.some(([provider, root]) => provider === "sumi" && present(join7(home, dirname6(root)))) || present(join7(home, ".config", "sumi")) || xdgSumi !== null && present(xdgSumi);
3390
+ const xdgSumi = xdg?.trim() ? resolve8(options.cwd ?? process.cwd(), xdg === "~" ? home : xdg.startsWith("~/") || xdg.startsWith("~\\") ? resolve8(home, xdg.slice(2)) : xdg, "sumi") : null;
3391
+ return ["SUMI_HOME", "SUMI_CONFIG_DIR", "SUMI_CONFIG", "SUMI_CONFIG_CONTENT"].some((key) => env[key] !== undefined) || Bun.which("sumi-paths", { PATH: env.PATH ?? "" }) !== null || NATIVE_SKILL_ROOTS.some(([provider, root]) => provider === "sumi" && present(join9(home, dirname7(root)))) || present(join9(home, ".config", "sumi")) || xdgSumi !== null && present(xdgSumi);
2989
3392
  }
2990
3393
  if (agent === "hermes" && env.HERMES_HOME !== undefined)
2991
3394
  return true;
2992
3395
  const adapter = AGENT_ADAPTERS[agent];
2993
- return present(join7(home, dirname6(adapter.config))) || NATIVE_SKILL_ROOTS.some(([provider, root]) => provider === agent && dirname6(root) === `.${agent}` && present(join7(home, dirname6(root))));
3396
+ return present(join9(home, dirname7(adapter.config))) || NATIVE_SKILL_ROOTS.some(([provider, root]) => provider === agent && dirname7(root) === `.${agent}` && present(join9(home, dirname7(root))));
2994
3397
  });
2995
3398
  if (!selected.length && !options.allowEmpty)
2996
3399
  throw new Error(`No configured or detected agents; select --agent <agent> explicitly (${INTEGRATION_AGENTS.join(", ")}).`);
@@ -2999,8 +3402,8 @@ function detectedIntegrationAgents(options = {}) {
2999
3402
 
3000
3403
  // src/lib/agent-sumi.ts
3001
3404
  import { spawnSync as spawnSync2 } from "child_process";
3002
- import { accessSync, constants as constants6, existsSync as existsSync4, lstatSync as lstatSync7, realpathSync, statSync as statSync3 } from "fs";
3003
- import { delimiter, isAbsolute as isAbsolute6, join as join8, resolve as resolve8 } from "path";
3405
+ import { accessSync, constants as constants6, existsSync as existsSync4, lstatSync as lstatSync8, realpathSync, statSync as statSync3 } from "fs";
3406
+ import { delimiter, isAbsolute as isAbsolute6, join as join10, resolve as resolve9 } from "path";
3004
3407
 
3005
3408
  // src/lib/hook-diagnostics.ts
3006
3409
  var CONTEXT_ERROR_CODES = new Set([
@@ -3068,6 +3471,7 @@ var OPTIONAL_CONTEXT_FAILURES = new Set([
3068
3471
  "SESSION_RECONCILIATION_REQUIRED",
3069
3472
  "SESSION_RENEWAL_TIMEOUT",
3070
3473
  "SESSION_WRITE_BUSY",
3474
+ "SESSION_WRITE_LOCKED",
3071
3475
  "SESSION_PARENT_CHANGED",
3072
3476
  "SESSION_CONTEXT_CONFLICT",
3073
3477
  "SKILLS_HOOK_TIMEOUT"
@@ -3090,7 +3494,7 @@ function recordWithKeys(value, keys) {
3090
3494
  return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
3091
3495
  }
3092
3496
  function absolutePath2(value) {
3093
- return typeof value === "string" && value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value) && resolve8(value) === value;
3497
+ return typeof value === "string" && value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value) && resolve9(value) === value;
3094
3498
  }
3095
3499
  function absoluteInput(value) {
3096
3500
  return value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value);
@@ -3101,7 +3505,7 @@ function resolveSumiPathsCommand(pathValue) {
3101
3505
  for (const directory of pathValue.split(delimiter)) {
3102
3506
  if (!directory || /[\x00-\x1f\x7f]/.test(directory))
3103
3507
  continue;
3104
- const candidate = resolve8(directory, "sumi-paths");
3508
+ const candidate = resolve9(directory, "sumi-paths");
3105
3509
  try {
3106
3510
  accessSync(candidate, constants6.X_OK);
3107
3511
  if (statSync3(candidate).isFile())
@@ -3113,7 +3517,7 @@ function resolveSumiPathsCommand(pathValue) {
3113
3517
  function validPathPlan(value, home, cwd) {
3114
3518
  if (!recordWithKeys(value, ["schemaVersion", "kind", "home", "cwd", "roots", "legacyRoots", "configFiles", "skillRoots"]))
3115
3519
  return false;
3116
- if (value.schemaVersion !== 1 || value.kind !== "sumi-paths" || value.home !== resolve8(home) || value.cwd !== resolve8(cwd))
3520
+ if (value.schemaVersion !== 1 || value.kind !== "sumi-paths" || value.home !== resolve9(home) || value.cwd !== resolve9(cwd))
3117
3521
  return false;
3118
3522
  if (!recordWithKeys(value.roots, SUMI_PATH_KEYS) || !recordWithKeys(value.legacyRoots, SUMI_PATH_KEYS))
3119
3523
  return false;
@@ -3135,11 +3539,11 @@ function validPathPlan(value, home, cwd) {
3135
3539
  const legacyConfigRoot = value.legacyRoots.config;
3136
3540
  if (typeof configRoot !== "string" || legacyConfigRoot !== null && typeof legacyConfigRoot !== "string")
3137
3541
  return false;
3138
- if (value.configFiles.canonical !== join8(configRoot, "sumi.json") || value.skillRoots.canonical !== join8(configRoot, "skills"))
3542
+ if (value.configFiles.canonical !== join10(configRoot, "sumi.json") || value.skillRoots.canonical !== join10(configRoot, "skills"))
3139
3543
  return false;
3140
3544
  if (legacyConfigRoot === null)
3141
3545
  return value.configFiles.legacy === null && value.skillRoots.legacy === null;
3142
- return value.configFiles.legacy === join8(legacyConfigRoot, "sumi.json") && value.skillRoots.legacy === join8(legacyConfigRoot, "skills");
3546
+ return value.configFiles.legacy === join10(legacyConfigRoot, "sumi.json") && value.skillRoots.legacy === join10(legacyConfigRoot, "skills");
3143
3547
  }
3144
3548
  function sumiPathPlan(home, cwd = process.cwd(), env = process.env) {
3145
3549
  for (const key of ["SUMI_CONFIG", "SUMI_CONFIG_CONTENT"])
@@ -3157,8 +3561,8 @@ function sumiPathPlan(home, cwd = process.cwd(), env = process.env) {
3157
3561
  childEnv[key] = env[key];
3158
3562
  let child2;
3159
3563
  try {
3160
- child2 = spawnSync2(executable, ["--json", "--home", resolve8(home), "--cwd", resolve8(cwd)], {
3161
- cwd: resolve8(cwd),
3564
+ child2 = spawnSync2(executable, ["--json", "--home", resolve9(home), "--cwd", resolve9(cwd)], {
3565
+ cwd: resolve9(cwd),
3162
3566
  env: childEnv,
3163
3567
  encoding: "buffer",
3164
3568
  timeout: 3000,
@@ -3190,7 +3594,7 @@ function selectedConfigDirectory(plan) {
3190
3594
  const canonical3 = plan.roots.config, legacy = plan.legacyRoots.config;
3191
3595
  if (legacy === null)
3192
3596
  return existsSync4(canonical3) ? realpathSync(canonical3) : canonical3;
3193
- const canonicalStat = lstatSync7(canonical3, { throwIfNoEntry: false }), legacyStat = lstatSync7(legacy, { throwIfNoEntry: false });
3597
+ const canonicalStat = lstatSync8(canonical3, { throwIfNoEntry: false }), legacyStat = lstatSync8(legacy, { throwIfNoEntry: false });
3194
3598
  if (canonicalStat && legacyStat && realpathSync(canonical3) !== realpathSync(legacy))
3195
3599
  throw new Error("Sumi config roots conflict; complete native home adoption before Skills integration");
3196
3600
  const selected = canonicalStat ? canonical3 : legacyStat ? legacy : canonical3;
@@ -3206,13 +3610,13 @@ function sumiConfigDirectory(home, env = process.env, cwd = process.cwd()) {
3206
3610
  }
3207
3611
  function sumiSkillRoots(home, cwd = process.cwd(), env = process.env) {
3208
3612
  const plan = sumiPathPlan(home, cwd, env);
3209
- return [join8(selectedConfigDirectory(plan), "skills")];
3613
+ return [join10(selectedConfigDirectory(plan), "skills")];
3210
3614
  }
3211
3615
  function sumiBridgeRoot(home, cwd = process.cwd(), env = process.env) {
3212
3616
  return sumiSkillRoots(home, cwd, env)[0];
3213
3617
  }
3214
3618
  function sumiConfigPath(home, cwd = process.cwd(), env = process.env) {
3215
- return join8(sumiConfigDirectory(home, env, cwd), "sumi.json");
3619
+ return join10(sumiConfigDirectory(home, env, cwd), "sumi.json");
3216
3620
  }
3217
3621
  function renderSumiPlugin(command, profile) {
3218
3622
  return renderSumiPluginVersion(command, profile, 3);