@hasna/skills 0.10.83 → 0.10.89

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 (35) hide show
  1. package/README.md +166 -15
  2. package/bin/index.js +5829 -2913
  3. package/bin/mcp.js +1766 -835
  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 +55 -0
  8. package/dist/cli/commands/context.d.ts +8 -0
  9. package/dist/cli/commands/runtime-launcher.d.ts +4 -9
  10. package/dist/index.js +3039 -1531
  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/selection-resolver.d.ts +13 -1
  28. package/dist/lib/session-background-renewal.d.ts +118 -0
  29. package/dist/lib/session-reconciliation.d.ts +82 -2
  30. package/dist/lib/skill-context.d.ts +50 -1
  31. package/dist/lib/skill-relevance.d.ts +95 -0
  32. package/dist/runtime-prerequisites.js +570 -165
  33. package/dist/sdk/index.js +2629 -1396
  34. package/docs/plugin-admission.md +132 -3
  35. 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.89";
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,10 @@ 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"],
2280
+ "codex-cli 0.166.0": ["hooks", "qualified-skill-catalog", "installed-plugin-review"]
1960
2281
  };
1961
2282
  function supportsCodexNativeCapability(version2, capability) {
1962
2283
  return typeof version2 === "string" && Object.hasOwn(MEASURED_CODEX_CAPABILITIES, version2) && MEASURED_CODEX_CAPABILITIES[version2].includes(capability);
@@ -1964,37 +2285,41 @@ function supportsCodexNativeCapability(version2, capability) {
1964
2285
  var SUPPORTED_CODEX_HOOK_VERSIONS = Object.freeze(Object.keys(MEASURED_CODEX_CAPABILITIES).filter((version2) => supportsCodexNativeCapability(version2, "hooks")));
1965
2286
 
1966
2287
  // 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";
2288
+ import { createHash as createHash6 } from "crypto";
2289
+ import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, resolve as resolve5 } from "path";
1969
2290
 
1970
2291
  // 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";
2292
+ import { createHash as createHash5 } from "crypto";
2293
+ import { closeSync as closeSync3, constants as constants3, fstatSync as fstatSync3, lstatSync as lstatSync4, openSync as openSync3, readSync as readSync2 } from "fs";
2294
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
1974
2295
  var CLAUDE_SETTINGS_WITNESS_LIMITS = Object.freeze({ bytes: 1024 * 1024, rows: 1024, depth: 32, nodes: 65536, stringCharacters: 16384, pathCharacters: 4096 });
1975
2296
  function need(value, reason) {
1976
2297
  if (!value)
1977
2298
  throw new Error(`Claude settings witness ${reason}`);
1978
2299
  }
1979
2300
  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");
2301
+ 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
2302
  }
1982
2303
  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
2304
  function parents(path) {
1984
2305
  const result = [];
1985
- for (let at = dirname3(path);; at = dirname3(at)) {
1986
- const stat = lstatSync3(at, { bigint: true });
2306
+ for (let at = dirname4(path);; at = dirname4(at)) {
2307
+ const stat = lstatSync4(at, { bigint: true });
1987
2308
  need(stat.isDirectory() && !stat.isSymbolicLink(), "path traverses a symlink or non-directory");
1988
2309
  result.push([at, stat]);
1989
- if (at === dirname3(at))
2310
+ if (at === dirname4(at))
1990
2311
  return result;
1991
2312
  }
1992
2313
  }
1993
2314
  function readNativeSettingsWitnessFile(path, budget, filename = "settings.json") {
1994
2315
  absolutePath(path);
1995
2316
  need(basename(path) === filename, `requires ${filename}`);
2317
+ return readNativeDiscoveryWitnessFile(path, budget);
2318
+ }
2319
+ function readNativeDiscoveryWitnessFile(path, budget) {
2320
+ absolutePath(path);
1996
2321
  need(Number.isSafeInteger(budget.remaining) && budget.remaining >= 0, "has an invalid byte budget");
1997
- const ancestors = parents(path), initial = lstatSync3(path, { bigint: true });
2322
+ const ancestors = parents(path), initial = lstatSync4(path, { bigint: true });
1998
2323
  need(initial.isFile() && !initial.isSymbolicLink() && initial.size <= BigInt(CLAUDE_SETTINGS_WITNESS_LIMITS.bytes), "is not a bounded regular file");
1999
2324
  need(initial.size <= BigInt(budget.remaining), "exceeds its aggregate byte limit");
2000
2325
  const fd = openSync3(path, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
@@ -2009,10 +2334,10 @@ function readNativeSettingsWitnessFile(path, budget, filename = "settings.json")
2009
2334
  break;
2010
2335
  length += count;
2011
2336
  }
2012
- const after = fstatSync3(fd, { bigint: true }), current = lstatSync3(path, { bigint: true });
2337
+ const after = fstatSync3(fd, { bigint: true }), current = lstatSync4(path, { bigint: true });
2013
2338
  need(current.isFile() && sameFile(opened, after) && sameFile(after, current) && BigInt(length) === opened.size, "changed during read");
2014
2339
  for (const [ancestor, before] of ancestors) {
2015
- const now = lstatSync3(ancestor, { bigint: true });
2340
+ const now = lstatSync4(ancestor, { bigint: true });
2016
2341
  need(now.isDirectory() && before.dev === now.dev && before.ino === now.ino && before.mode === now.mode, "parent changed during read");
2017
2342
  }
2018
2343
  budget.remaining -= length;
@@ -2232,7 +2557,7 @@ function settingsDigest(text, version2 = 1) {
2232
2557
  }
2233
2558
  return true;
2234
2559
  });
2235
- return createHash3("sha256").update(`hasna.skills.claude-settings.v${version2}\x00`).update(JSON.stringify(canonical(value))).digest("hex");
2560
+ return createHash5("sha256").update(`hasna.skills.claude-settings.v${version2}\x00`).update(JSON.stringify(canonical(value))).digest("hex");
2236
2561
  }
2237
2562
  function hashClaudeSettingsReplacement(text, budget) {
2238
2563
  need(typeof text === "string", "requires settings text");
@@ -2268,7 +2593,7 @@ function upgradeClaudeSettingsWitness(previous, reviewedSettingsPath) {
2268
2593
  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
2594
  const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
2270
2595
  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);
2596
+ const beforeDigest = previous.hashMode === undefined || previous.hashMode === "bytes" ? createHash5("sha256").update(before).digest("hex") : settingsDigest(before, previous.hashMode === "claude-settings-v1" ? 1 : 2);
2272
2597
  need(beforeDigest === previous.sha256, "reviewed preimage does not match the legacy witness");
2273
2598
  const sha256 = settingsDigest(current, 3);
2274
2599
  need(settingsDigest(before, 3) === sha256, "non-preference settings changed; explicit discovery review required");
@@ -2288,12 +2613,91 @@ function upgradeClaudeSettingsWitnessV4(previous, reviewedSettingsPath) {
2288
2613
  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
2614
  const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
2290
2615
  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);
2616
+ 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
2617
  need(beforeDigest === previous.sha256, "reviewed preimage does not match the prior witness");
2293
2618
  const sha256 = settingsDigest(current, 4);
2294
2619
  need(settingsDigest(before, 4) === sha256, "non-preference settings changed; explicit discovery review required");
2295
2620
  return { path: previous.path, hashMode: "claude-settings-v4", sha256 };
2296
2621
  }
2622
+ var V5_DISCOVERY_KEY = /skill|plugin|hook|marketplace/i;
2623
+ var V5_BOUND_KEYS = new Set(["apiKeyHelper", "awsAuthRefresh", "awsCredentialExport", "otelHeadersHelper", "fileSuggestion", "outputStyle", "agent"]);
2624
+ var V5_DISCOVERY_ENV = new Set(["CLAUDE_CONFIG_DIR"]);
2625
+ var V5_SKILL_RULE = /^Skill(?:\(|$)/;
2626
+ var V5_PERMISSION_LISTS = new Set(["allow", "deny", "ask"]);
2627
+ function narrowedClaudeSettings(value) {
2628
+ const entries = [];
2629
+ for (const [key, child] of value.entries) {
2630
+ if (V5_DISCOVERY_KEY.test(key) || V5_BOUND_KEYS.has(key)) {
2631
+ entries.push([key, child]);
2632
+ continue;
2633
+ }
2634
+ if (key === "permissions") {
2635
+ if (child.kind !== "object") {
2636
+ entries.push([key, child]);
2637
+ continue;
2638
+ }
2639
+ const kept = [];
2640
+ for (const [name, rules] of child.entries) {
2641
+ if (name === "additionalDirectories") {
2642
+ kept.push([name, rules]);
2643
+ continue;
2644
+ }
2645
+ if (!V5_PERMISSION_LISTS.has(name))
2646
+ continue;
2647
+ if (rules.kind !== "array") {
2648
+ kept.push([name, rules]);
2649
+ continue;
2650
+ }
2651
+ const skillRules = rules.items.filter((rule) => rule.kind !== "string" || V5_SKILL_RULE.test(rule.value));
2652
+ if (skillRules.length)
2653
+ kept.push([name, { kind: "array", items: skillRules }]);
2654
+ }
2655
+ if (kept.length)
2656
+ entries.push([key, { kind: "object", entries: kept }]);
2657
+ continue;
2658
+ }
2659
+ if (key === "env") {
2660
+ if (child.kind !== "object") {
2661
+ entries.push([key, child]);
2662
+ continue;
2663
+ }
2664
+ const kept = child.entries.filter(([name]) => V5_DISCOVERY_ENV.has(name) || V5_DISCOVERY_KEY.test(name));
2665
+ if (kept.length)
2666
+ entries.push([key, { kind: "object", entries: kept }]);
2667
+ }
2668
+ }
2669
+ return { kind: "object", entries };
2670
+ }
2671
+ function claudeDiscoveryControlKeys(text) {
2672
+ return narrowedClaudeSettings(parseNativeJsonObject(text)).entries.map(([key]) => key);
2673
+ }
2674
+ function claudeDiscoveryEnvironmentKeys(text) {
2675
+ const environment = narrowedClaudeSettings(parseNativeJsonObject(text)).entries.find(([key]) => key === "env")?.[1];
2676
+ return environment?.kind === "object" ? environment.entries.map(([key]) => key) : [];
2677
+ }
2678
+ function settingsDigestV5(text) {
2679
+ return createHash5("sha256").update("hasna.skills.claude-settings.v5\x00").update(JSON.stringify(canonical(narrowedClaudeSettings(parse2(text))))).digest("hex");
2680
+ }
2681
+ function captureClaudeSettingsV5(path, budget = { remaining: 256 * 1024 * 1024 }) {
2682
+ return { path, hashMode: "claude-settings-v5", sha256: settingsDigestV5(readNativeSettingsWitnessFile(path, budget)) };
2683
+ }
2684
+ function hashClaudeSettingsReplacementV5(text, budget) {
2685
+ need(typeof text === "string", "requires settings text");
2686
+ const bytes = Buffer.byteLength(text);
2687
+ need(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
2688
+ budget.remaining -= bytes;
2689
+ return settingsDigestV5(text);
2690
+ }
2691
+ function upgradeClaudeSettingsWitnessV5(previous, reviewedSettingsPath) {
2692
+ 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");
2693
+ const budget = { remaining: 2 * CLAUDE_SETTINGS_WITNESS_LIMITS.bytes };
2694
+ const before = readNativeSettingsWitnessFile(reviewedSettingsPath, budget), current = readNativeSettingsWitnessFile(previous.path, budget);
2695
+ 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);
2696
+ need(beforeDigest === previous.sha256, "reviewed preimage does not match the prior witness");
2697
+ const sha256 = settingsDigestV5(current);
2698
+ need(settingsDigestV5(before) === sha256, "discovery settings changed; explicit discovery review required");
2699
+ return { path: previous.path, hashMode: "claude-settings-v5", sha256 };
2700
+ }
2297
2701
  function hashNativeJsonControls(text, excludedMetadata) {
2298
2702
  need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
2299
2703
  const value = parse2(text);
@@ -2303,7 +2707,7 @@ function hashNativeJsonControls(text, excludedMetadata) {
2303
2707
  need(child.kind === "string" && child.value.length <= 128, "has invalid version metadata");
2304
2708
  return false;
2305
2709
  });
2306
- return createHash3("sha256").update("hasna.skills.native-json-controls.v1\x00").update(JSON.stringify(canonical(value))).digest("hex");
2710
+ return createHash5("sha256").update("hasna.skills.native-json-controls.v1\x00").update(JSON.stringify(canonical(value))).digest("hex");
2307
2711
  }
2308
2712
  function hashSumiNativeJsonControls(text) {
2309
2713
  need(Buffer.byteLength(text) <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes, "exceeds its byte limit");
@@ -2324,7 +2728,7 @@ function hashSumiNativeJsonControls(text) {
2324
2728
  });
2325
2729
  return child.entries.length > 0;
2326
2730
  });
2327
- return createHash3("sha256").update("hasna.skills.sumi-settings.v1\x00").update(JSON.stringify(value)).digest("hex");
2731
+ return createHash5("sha256").update("hasna.skills.sumi-settings.v1\x00").update(JSON.stringify(value)).digest("hex");
2328
2732
  }
2329
2733
 
2330
2734
  // src/lib/claude-marketplace-entry-witness.ts
@@ -2392,7 +2796,7 @@ var stringArray = (value) => value?.kind === "array" && value.items.every(isStri
2392
2796
  var stringObject = (value) => value?.kind === "object" && value.entries.every(([, item]) => isString(item));
2393
2797
  function claudeMarketplaceEntrySourceValid(source) {
2394
2798
  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);
2799
+ 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
2800
  }
2397
2801
  function canonical2(value) {
2398
2802
  if (value.kind === "string")
@@ -2505,7 +2909,7 @@ function hashClaudeMarketplaceEntry(text, selector) {
2505
2909
  ["allowCrossMarketplaceDependenciesOn", field(root, "allowCrossMarketplaceDependenciesOn") ?? nullValue]
2506
2910
  ] };
2507
2911
  const projection = { kind: "object", entries: [["marketplace", marketplace], ["plugin", entry]] };
2508
- return createHash4("sha256").update(DOMAIN).update(canonical2(projection)).digest("hex");
2912
+ return createHash6("sha256").update(DOMAIN).update(canonical2(projection)).digest("hex");
2509
2913
  }
2510
2914
  function captureClaudeMarketplaceEntry(path, marketplace, plugin, budget = { remaining: 256 * 1024 * 1024 }) {
2511
2915
  need2(claudeMarketplaceEntrySourceValid({ path, marketplace, plugin }), "requires a normalized absolute .claude-plugin/marketplace.json path and exact names");
@@ -2519,13 +2923,13 @@ function captureClaudeMarketplaceEntry(path, marketplace, plugin, budget = { rem
2519
2923
  }
2520
2924
 
2521
2925
  // 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";
2926
+ import { createHash as createHash7 } from "crypto";
2927
+ import { closeSync as closeSync4, constants as constants4, fstatSync as fstatSync4, lstatSync as lstatSync5, openSync as openSync4, readSync as readSync3 } from "fs";
2928
+ import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute4, resolve as resolve6, sep } from "path";
2525
2929
  var CLAUDE_ORPHAN_MARKER_MODE = "claude-plugin-orphaned-at-v1";
2526
2930
  var CACHE = `${sep}.claude${sep}plugins${sep}cache${sep}`;
2527
2931
  function isClaudePluginCacheOrphanMarker(path) {
2528
- if (typeof path !== "string" || basename3(path) !== ".orphaned_at" || !isAbsolute4(path) || resolve5(path) !== path || path.includes("\x00"))
2932
+ if (typeof path !== "string" || basename3(path) !== ".orphaned_at" || !isAbsolute4(path) || resolve6(path) !== path || path.includes("\x00"))
2529
2933
  return false;
2530
2934
  const at = path.indexOf(CACHE);
2531
2935
  if (at < 0 || path.indexOf(CACHE, at + 1) >= 0)
@@ -2537,10 +2941,10 @@ function same(left, right) {
2537
2941
  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
2942
  }
2539
2943
  function safe(path) {
2540
- for (let cursor = path;; cursor = dirname5(cursor)) {
2541
- if (lstatSync4(cursor, { throwIfNoEntry: false })?.isSymbolicLink())
2944
+ for (let cursor = path;; cursor = dirname6(cursor)) {
2945
+ if (lstatSync5(cursor, { throwIfNoEntry: false })?.isSymbolicLink())
2542
2946
  throw new Error("Refusing symlink Claude orphan marker input");
2543
- if (dirname5(cursor) === cursor)
2947
+ if (dirname6(cursor) === cursor)
2544
2948
  break;
2545
2949
  }
2546
2950
  }
@@ -2550,7 +2954,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
2550
2954
  if (changes?.has(path))
2551
2955
  throw new Error("Claude orphan markers cannot intersect a planned write");
2552
2956
  safe(path);
2553
- const before = lstatSync4(path, { throwIfNoEntry: false });
2957
+ const before = lstatSync5(path, { throwIfNoEntry: false });
2554
2958
  if (!before)
2555
2959
  return null;
2556
2960
  const uid = process.getuid?.();
@@ -2571,7 +2975,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
2571
2975
  }
2572
2976
  if (!/^[1-9][0-9]{12}$/.test(bytes.subarray(0, length).toString("latin1")))
2573
2977
  throw new Error("Malformed Claude orphan marker timestamp");
2574
- const after = fstatSync4(fd), current = lstatSync4(path, { throwIfNoEntry: false });
2978
+ const after = fstatSync4(fd), current = lstatSync5(path, { throwIfNoEntry: false });
2575
2979
  safe(path);
2576
2980
  if (!same(opened, after) || !same(after, current) || length !== after.size)
2577
2981
  throw new Error("Claude orphan marker changed during read");
@@ -2580,7 +2984,7 @@ function hashClaudePluginCacheOrphanMarker(path, budget, changes) {
2580
2984
  throw new Error("Raw discovery aggregate byte limit exceeded");
2581
2985
  budget.remaining -= length;
2582
2986
  }
2583
- return createHash5("sha256").update(`${CLAUDE_ORPHAN_MARKER_MODE}\x00${path}\x00file`).digest("hex");
2987
+ return createHash7("sha256").update(`${CLAUDE_ORPHAN_MARKER_MODE}\x00${path}\x00file`).digest("hex");
2584
2988
  } finally {
2585
2989
  closeSync4(fd);
2586
2990
  }
@@ -2640,7 +3044,7 @@ function assertAgentPolicyCollections(policy) {
2640
3044
  text(control.pluginId, 1024);
2641
3045
  text(control.namespace, 64);
2642
3046
  text(control.pluginParent);
2643
- requireBound(isAbsolute5(control.pluginParent) && resolve6(control.pluginParent) === control.pluginParent && /^[a-f0-9]{64}$/.test(control.manifestSha256));
3047
+ requireBound(isAbsolute5(control.pluginParent) && resolve7(control.pluginParent) === control.pluginParent && /^[a-f0-9]{64}$/.test(control.manifestSha256));
2644
3048
  if (control.remotePluginId !== undefined)
2645
3049
  requireBound(typeof control.remotePluginId === "string" && /^[A-Za-z0-9_~-]{1,1024}$/.test(control.remotePluginId));
2646
3050
  if (control.skillsOnly !== undefined)
@@ -2658,7 +3062,7 @@ function assertAgentPolicyCollections(policy) {
2658
3062
  text(control.pluginId, 1024);
2659
3063
  text(control.namespace, 64);
2660
3064
  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));
3065
+ 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
3066
  ids.add(control.pluginId);
2663
3067
  }
2664
3068
  }
@@ -2667,7 +3071,7 @@ function assertAgentPolicyCollections(policy) {
2667
3071
  requireBound(agent === "gemini" && object(executable) && Object.keys(executable).length === 3 && executable.command === "gemini");
2668
3072
  for (const path of [executable.path, executable.target]) {
2669
3073
  text(path);
2670
- requireBound(!/[\x00-\x1f\x7f-\x9f\u200e\u200f\u202a-\u202e\u2066-\u2069]/.test(path) && isAbsolute5(path) && resolve6(path) === path);
3074
+ requireBound(!/[\x00-\x1f\x7f-\x9f\u200e\u200f\u202a-\u202e\u2066-\u2069]/.test(path) && isAbsolute5(path) && resolve7(path) === path);
2671
3075
  }
2672
3076
  }
2673
3077
  if (bridge.disabledBuiltins !== undefined)
@@ -2695,11 +3099,11 @@ function assertAgentPolicyCollections(policy) {
2695
3099
  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
3100
  for (const root of roots) {
2697
3101
  text(root);
2698
- requireBound(isAbsolute5(root) && resolve6(root) === root);
3102
+ requireBound(isAbsolute5(root) && resolve7(root) === root);
2699
3103
  }
2700
3104
  for (const parent of parents2) {
2701
3105
  text(parent);
2702
- requireBound(isAbsolute5(parent) && resolve6(parent) === parent);
3106
+ requireBound(isAbsolute5(parent) && resolve7(parent) === parent);
2703
3107
  }
2704
3108
  for (const directory of array(proof.directories, AGENT_POLICY_LIMITS.discoveryDirectories)) {
2705
3109
  requireBound(object(directory));
@@ -2719,7 +3123,7 @@ function assertAgentPolicyCollections(policy) {
2719
3123
  text(input.namespace, 64);
2720
3124
  text(input.pluginParent);
2721
3125
  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));
3126
+ 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
3127
  ids.add(input.pluginId);
2724
3128
  }
2725
3129
  if (proof.directories !== undefined)
@@ -2733,14 +3137,14 @@ function assertAgentPolicyCollections(policy) {
2733
3137
  const proof = record(value.codexSemanticCache, 9), witness = record(proof.witness, 7);
2734
3138
  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
3139
  text(proof.configPath);
2736
- requireBound(isAbsolute5(proof.configPath) && resolve6(proof.configPath) === proof.configPath && basename4(proof.configPath) === "config.toml");
3140
+ requireBound(isAbsolute5(proof.configPath) && resolve7(proof.configPath) === proof.configPath && basename4(proof.configPath) === "config.toml");
2737
3141
  text(witness.cacheRoot);
2738
- requireBound(isAbsolute5(witness.cacheRoot) && resolve6(witness.cacheRoot) === witness.cacheRoot);
3142
+ requireBound(isAbsolute5(witness.cacheRoot) && resolve7(witness.cacheRoot) === witness.cacheRoot);
2739
3143
  if (proof.hookRootAlias !== null) {
2740
3144
  requireBound(object(proof.hookRootAlias) && Object.keys(proof.hookRootAlias).length === 2);
2741
3145
  for (const key of ["alias", "target"]) {
2742
3146
  text(proof.hookRootAlias[key]);
2743
- requireBound(isAbsolute5(proof.hookRootAlias[key]) && resolve6(proof.hookRootAlias[key]) === proof.hookRootAlias[key]);
3147
+ requireBound(isAbsolute5(proof.hookRootAlias[key]) && resolve7(proof.hookRootAlias[key]) === proof.hookRootAlias[key]);
2744
3148
  }
2745
3149
  }
2746
3150
  const parents2 = array(proof.appOnlyParents, 4096);
@@ -2748,7 +3152,7 @@ function assertAgentPolicyCollections(policy) {
2748
3152
  requireBound(object(parent) && Object.keys(parent).length === 5 && parent.role === "app-only");
2749
3153
  for (const key of ["pluginId", "namespace", "pluginParent", "remotePluginId"])
2750
3154
  text(parent[key]);
2751
- requireBound(isAbsolute5(parent.pluginParent) && resolve6(parent.pluginParent) === parent.pluginParent);
3155
+ requireBound(isAbsolute5(parent.pluginParent) && resolve7(parent.pluginParent) === parent.pluginParent);
2752
3156
  }
2753
3157
  if (proof.reviewedHookParents !== undefined)
2754
3158
  for (const parent of array(proof.reviewedHookParents, 4096)) {
@@ -2760,9 +3164,9 @@ function assertAgentPolicyCollections(policy) {
2760
3164
  text(parent.remotePluginId);
2761
3165
  else {
2762
3166
  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");
3167
+ 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
3168
  }
2765
- requireBound(isAbsolute5(parent.pluginParent) && resolve6(parent.pluginParent) === parent.pluginParent);
3169
+ requireBound(isAbsolute5(parent.pluginParent) && resolve7(parent.pluginParent) === parent.pluginParent);
2766
3170
  }
2767
3171
  for (const row of [...array(witness.controls, 4096), ...array(witness.appOnlyParents, 4096)]) {
2768
3172
  requireBound(object(row));
@@ -2782,9 +3186,9 @@ function assertAgentPolicyCollections(policy) {
2782
3186
  text(row.remotePluginId);
2783
3187
  else {
2784
3188
  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");
3189
+ 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
3190
  }
2787
- requireBound(isAbsolute5(row.pluginParent) && resolve6(row.pluginParent) === row.pluginParent);
3191
+ requireBound(isAbsolute5(row.pluginParent) && resolve7(row.pluginParent) === row.pluginParent);
2788
3192
  for (const key of ["manifestSha256", "hooksSha256"])
2789
3193
  requireBound(typeof row[key] === "string" && /^[a-f0-9]{64}$/.test(row[key]));
2790
3194
  if (row.installation === "remote-receipt")
@@ -2817,17 +3221,17 @@ function assertAgentPolicyCollections(policy) {
2817
3221
  text(source.path);
2818
3222
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
2819
3223
  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));
3224
+ 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
3225
  if (source.hashMode === CLAUDE_ORPHAN_MARKER_MODE)
2822
3226
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && isClaudePluginCacheOrphanMarker(source.path) && source.managedPlugins === undefined && source.reviewArtifact === undefined);
2823
3227
  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);
3228
+ 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
3229
  if (source.hashMode === "codex-settings-v6") {
2826
3230
  const seen = new Set;
2827
3231
  for (const hook of array(source.hookSources, AGENT_POLICY_LIMITS.discoverySources)) {
2828
3232
  requireBound(object(hook) && Object.keys(hook).length === 2 && Object.keys(hook).every((key) => ["path", "sha256"].includes(key)));
2829
3233
  text(hook.path);
2830
- requireBound(isAbsolute5(hook.path) && resolve6(hook.path) === hook.path && !seen.has(hook.path));
3234
+ requireBound(isAbsolute5(hook.path) && resolve7(hook.path) === hook.path && !seen.has(hook.path));
2831
3235
  seen.add(hook.path);
2832
3236
  requireBound(hook.sha256 === null || typeof hook.sha256 === "string" && /^[a-f0-9]{64}$/.test(hook.sha256));
2833
3237
  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 +3239,15 @@ function assertAgentPolicyCollections(policy) {
2835
3239
  } else
2836
3240
  requireBound(source.hookSources === undefined);
2837
3241
  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);
3242
+ 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
3243
  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");
3244
+ 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");
3245
+ 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"))
3246
+ 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");
3247
+ 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"))
3248
+ 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
3249
  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");
3250
+ 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
3251
  if (source.hashMode === "claude-marketplace-entry-v1")
2848
3252
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && claudeMarketplaceEntrySourceValid(source));
2849
3253
  if (source.hashMode === "claude-plugin-registry") {
@@ -2862,9 +3266,9 @@ function assertAgentPolicyCollections(policy) {
2862
3266
  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
3267
  for (const path of [artifact.storeRoot, artifact.originalPath]) {
2864
3268
  text(path);
2865
- requireBound(isAbsolute5(path) && resolve6(path) === path);
3269
+ requireBound(isAbsolute5(path) && resolve7(path) === path);
2866
3270
  }
2867
- requireBound(source.path === join5(artifact.storeRoot, "agent-discovery", "artifacts", artifact.sha256, "catalog.json"));
3271
+ requireBound(source.path === join7(artifact.storeRoot, "agent-discovery", "artifacts", artifact.sha256, "catalog.json"));
2868
3272
  }
2869
3273
  if (source.format !== undefined)
2870
3274
  requireBound(["json", "toml", "yaml"].includes(source.format));
@@ -2915,10 +3319,10 @@ function serializeManagedSkillPolicy(value) {
2915
3319
  return text2;
2916
3320
  }
2917
3321
  function readManagedSkillPolicySnapshot(dataDir2 = getDataDirReadOnly()) {
2918
- const path = join6(dataDir2, "agent-policy.json");
3322
+ const path = join8(dataDir2, "agent-policy.json");
2919
3323
  let descriptor, observed = false;
2920
3324
  try {
2921
- const stat = lstatSync5(path);
3325
+ const stat = lstatSync6(path);
2922
3326
  observed = true;
2923
3327
  if (!stat.isFile() || stat.isSymbolicLink())
2924
3328
  throw refusal();
@@ -2944,7 +3348,7 @@ function readManagedSkillPolicySnapshot(dataDir2 = getDataDirReadOnly()) {
2944
3348
  return { text: text2, value: parseManagedSkillPolicy(text2) };
2945
3349
  } catch (error) {
2946
3350
  if (error.code === "ENOENT" && !observed) {
2947
- if (lstatSync5(path, { throwIfNoEntry: false }) === undefined)
3351
+ if (lstatSync6(path, { throwIfNoEntry: false }) === undefined)
2948
3352
  return null;
2949
3353
  }
2950
3354
  if (error instanceof SkillSelectionError)
@@ -2974,7 +3378,7 @@ function detectedIntegrationAgents(options = {}) {
2974
3378
  throw new Error("Invalid existing bridge agent inventory");
2975
3379
  const present = (path) => {
2976
3380
  try {
2977
- return lstatSync6(path, { throwIfNoEntry: false }) !== undefined;
3381
+ return lstatSync7(path, { throwIfNoEntry: false }) !== undefined;
2978
3382
  } catch {
2979
3383
  throw new Error("Agent presence cannot be inspected; review native configuration before hook installation");
2980
3384
  }
@@ -2984,13 +3388,13 @@ function detectedIntegrationAgents(options = {}) {
2984
3388
  return true;
2985
3389
  if (agent === "sumi") {
2986
3390
  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);
3391
+ const xdgSumi = xdg?.trim() ? resolve8(options.cwd ?? process.cwd(), xdg === "~" ? home : xdg.startsWith("~/") || xdg.startsWith("~\\") ? resolve8(home, xdg.slice(2)) : xdg, "sumi") : null;
3392
+ 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
3393
  }
2990
3394
  if (agent === "hermes" && env.HERMES_HOME !== undefined)
2991
3395
  return true;
2992
3396
  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))));
3397
+ return present(join9(home, dirname7(adapter.config))) || NATIVE_SKILL_ROOTS.some(([provider, root]) => provider === agent && dirname7(root) === `.${agent}` && present(join9(home, dirname7(root))));
2994
3398
  });
2995
3399
  if (!selected.length && !options.allowEmpty)
2996
3400
  throw new Error(`No configured or detected agents; select --agent <agent> explicitly (${INTEGRATION_AGENTS.join(", ")}).`);
@@ -2999,8 +3403,8 @@ function detectedIntegrationAgents(options = {}) {
2999
3403
 
3000
3404
  // src/lib/agent-sumi.ts
3001
3405
  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";
3406
+ import { accessSync, constants as constants6, existsSync as existsSync4, lstatSync as lstatSync8, realpathSync, statSync as statSync3 } from "fs";
3407
+ import { delimiter, isAbsolute as isAbsolute6, join as join10, resolve as resolve9 } from "path";
3004
3408
 
3005
3409
  // src/lib/hook-diagnostics.ts
3006
3410
  var CONTEXT_ERROR_CODES = new Set([
@@ -3068,6 +3472,7 @@ var OPTIONAL_CONTEXT_FAILURES = new Set([
3068
3472
  "SESSION_RECONCILIATION_REQUIRED",
3069
3473
  "SESSION_RENEWAL_TIMEOUT",
3070
3474
  "SESSION_WRITE_BUSY",
3475
+ "SESSION_WRITE_LOCKED",
3071
3476
  "SESSION_PARENT_CHANGED",
3072
3477
  "SESSION_CONTEXT_CONFLICT",
3073
3478
  "SKILLS_HOOK_TIMEOUT"
@@ -3090,7 +3495,7 @@ function recordWithKeys(value, keys) {
3090
3495
  return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
3091
3496
  }
3092
3497
  function absolutePath2(value) {
3093
- return typeof value === "string" && value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value) && resolve8(value) === value;
3498
+ return typeof value === "string" && value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value) && resolve9(value) === value;
3094
3499
  }
3095
3500
  function absoluteInput(value) {
3096
3501
  return value.length > 0 && value.length <= 32768 && !/[\x00-\x1f\x7f]/.test(value) && isAbsolute6(value);
@@ -3101,7 +3506,7 @@ function resolveSumiPathsCommand(pathValue) {
3101
3506
  for (const directory of pathValue.split(delimiter)) {
3102
3507
  if (!directory || /[\x00-\x1f\x7f]/.test(directory))
3103
3508
  continue;
3104
- const candidate = resolve8(directory, "sumi-paths");
3509
+ const candidate = resolve9(directory, "sumi-paths");
3105
3510
  try {
3106
3511
  accessSync(candidate, constants6.X_OK);
3107
3512
  if (statSync3(candidate).isFile())
@@ -3113,7 +3518,7 @@ function resolveSumiPathsCommand(pathValue) {
3113
3518
  function validPathPlan(value, home, cwd) {
3114
3519
  if (!recordWithKeys(value, ["schemaVersion", "kind", "home", "cwd", "roots", "legacyRoots", "configFiles", "skillRoots"]))
3115
3520
  return false;
3116
- if (value.schemaVersion !== 1 || value.kind !== "sumi-paths" || value.home !== resolve8(home) || value.cwd !== resolve8(cwd))
3521
+ if (value.schemaVersion !== 1 || value.kind !== "sumi-paths" || value.home !== resolve9(home) || value.cwd !== resolve9(cwd))
3117
3522
  return false;
3118
3523
  if (!recordWithKeys(value.roots, SUMI_PATH_KEYS) || !recordWithKeys(value.legacyRoots, SUMI_PATH_KEYS))
3119
3524
  return false;
@@ -3135,11 +3540,11 @@ function validPathPlan(value, home, cwd) {
3135
3540
  const legacyConfigRoot = value.legacyRoots.config;
3136
3541
  if (typeof configRoot !== "string" || legacyConfigRoot !== null && typeof legacyConfigRoot !== "string")
3137
3542
  return false;
3138
- if (value.configFiles.canonical !== join8(configRoot, "sumi.json") || value.skillRoots.canonical !== join8(configRoot, "skills"))
3543
+ if (value.configFiles.canonical !== join10(configRoot, "sumi.json") || value.skillRoots.canonical !== join10(configRoot, "skills"))
3139
3544
  return false;
3140
3545
  if (legacyConfigRoot === null)
3141
3546
  return value.configFiles.legacy === null && value.skillRoots.legacy === null;
3142
- return value.configFiles.legacy === join8(legacyConfigRoot, "sumi.json") && value.skillRoots.legacy === join8(legacyConfigRoot, "skills");
3547
+ return value.configFiles.legacy === join10(legacyConfigRoot, "sumi.json") && value.skillRoots.legacy === join10(legacyConfigRoot, "skills");
3143
3548
  }
3144
3549
  function sumiPathPlan(home, cwd = process.cwd(), env = process.env) {
3145
3550
  for (const key of ["SUMI_CONFIG", "SUMI_CONFIG_CONTENT"])
@@ -3157,8 +3562,8 @@ function sumiPathPlan(home, cwd = process.cwd(), env = process.env) {
3157
3562
  childEnv[key] = env[key];
3158
3563
  let child2;
3159
3564
  try {
3160
- child2 = spawnSync2(executable, ["--json", "--home", resolve8(home), "--cwd", resolve8(cwd)], {
3161
- cwd: resolve8(cwd),
3565
+ child2 = spawnSync2(executable, ["--json", "--home", resolve9(home), "--cwd", resolve9(cwd)], {
3566
+ cwd: resolve9(cwd),
3162
3567
  env: childEnv,
3163
3568
  encoding: "buffer",
3164
3569
  timeout: 3000,
@@ -3190,7 +3595,7 @@ function selectedConfigDirectory(plan) {
3190
3595
  const canonical3 = plan.roots.config, legacy = plan.legacyRoots.config;
3191
3596
  if (legacy === null)
3192
3597
  return existsSync4(canonical3) ? realpathSync(canonical3) : canonical3;
3193
- const canonicalStat = lstatSync7(canonical3, { throwIfNoEntry: false }), legacyStat = lstatSync7(legacy, { throwIfNoEntry: false });
3598
+ const canonicalStat = lstatSync8(canonical3, { throwIfNoEntry: false }), legacyStat = lstatSync8(legacy, { throwIfNoEntry: false });
3194
3599
  if (canonicalStat && legacyStat && realpathSync(canonical3) !== realpathSync(legacy))
3195
3600
  throw new Error("Sumi config roots conflict; complete native home adoption before Skills integration");
3196
3601
  const selected = canonicalStat ? canonical3 : legacyStat ? legacy : canonical3;
@@ -3206,13 +3611,13 @@ function sumiConfigDirectory(home, env = process.env, cwd = process.cwd()) {
3206
3611
  }
3207
3612
  function sumiSkillRoots(home, cwd = process.cwd(), env = process.env) {
3208
3613
  const plan = sumiPathPlan(home, cwd, env);
3209
- return [join8(selectedConfigDirectory(plan), "skills")];
3614
+ return [join10(selectedConfigDirectory(plan), "skills")];
3210
3615
  }
3211
3616
  function sumiBridgeRoot(home, cwd = process.cwd(), env = process.env) {
3212
3617
  return sumiSkillRoots(home, cwd, env)[0];
3213
3618
  }
3214
3619
  function sumiConfigPath(home, cwd = process.cwd(), env = process.env) {
3215
- return join8(sumiConfigDirectory(home, env, cwd), "sumi.json");
3620
+ return join10(sumiConfigDirectory(home, env, cwd), "sumi.json");
3216
3621
  }
3217
3622
  function renderSumiPlugin(command, profile) {
3218
3623
  return renderSumiPluginVersion(command, profile, 3);