@hasna/skills 0.10.31 → 0.10.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -623,6 +623,18 @@ on this same host. The exact lock bytes and a metadata record remain under
623
623
  by recovery. A live, recent, malformed or different-host lock still blocks the
624
624
  write. A recovery guard serializes competing recoverers.
625
625
 
626
+ Writers gather identity before publishing the lock. A private, fully written and
627
+ synced staging inode is linked into the live path atomically without replacing
628
+ an existing lock; the staging name is then removed before the protected write.
629
+ The writer keeps its descriptor open through lock release. An interruption at
630
+ the brief two-link publication boundary leaves a complete marker and its exact
631
+ operation-bound staging witness. Recovery accepts that pair only when both names
632
+ identify the same private inode and the existing stale-owner checks pass.
633
+ Readers from 0.10.31 and earlier refuse this interrupted two-link state; inspect
634
+ and recover it with an updated installation. Ordinary held locks have one link.
635
+ This prevents new empty locks; it does not recover historical empty or malformed
636
+ locks, whose ownership remains unknown and which must stay protected.
637
+
626
638
  Older PID-only locks require a local review because they do not identify their
627
639
  host. On the host that created the lock, inspect the receipt and lock, verify
628
640
  the recorded PID belongs to no live writer, then apply the exact reviewed lock:
@@ -1136,6 +1148,32 @@ semantic version. If discovery or verification fails, it exits nonzero and
1136
1148
  explains that installation may already have completed. It does not automatically
1137
1149
  reinstall or change your PATH.
1138
1150
 
1151
+ ### Exact-version runtime updates
1152
+
1153
+ `skills self-update --version 0.10.34` installs a verified package into a retained
1154
+ copyfile runtime, preserves configuration and launcher preimages, and keeps the
1155
+ previous runtime available for receipt-bound rollback.
1156
+
1157
+ To require a minimum dependency age, pass a positive integer number of days and
1158
+ repeat the exclusion option for package names or package globs:
1159
+
1160
+ ```bash
1161
+ skills self-update --version 0.10.34 --min-release-age 7 \
1162
+ --min-release-age-exclude '@hasna/*' \
1163
+ --min-release-age-exclude '@hasna-internal/*' \
1164
+ --min-release-age-exclude '@openai/*' \
1165
+ --min-release-age-exclude '@anthropic-ai/*' \
1166
+ --min-release-age-exclude openai
1167
+ ```
1168
+
1169
+ The policy reaches both dependency lock resolution and `npm ci` through private
1170
+ npm configuration. Exemptions match each dependency's own package identity;
1171
+ unrelated transitive dependencies retain the minimum age. These options require
1172
+ an exact version and npm 11.19.0 or newer with both age capabilities. They are
1173
+ refused for rollback, alias adoption or the no-argument updater. Existing ambient
1174
+ npm settings and credentials are not forwarded. Omitting the policy preserves
1175
+ existing exact-version update behavior.
1176
+
1139
1177
  ### Local environment assignments
1140
1178
 
1141
1179
  `skills env-check --set 'KEY=value'` writes one literal value to the current
package/bin/index.js CHANGED
@@ -36868,7 +36868,7 @@ var package_default;
36868
36868
  var init_package = __esm(() => {
36869
36869
  package_default = {
36870
36870
  name: "@hasna/skills",
36871
- version: "0.10.31",
36871
+ version: "0.10.34",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -48139,7 +48139,7 @@ var init_profile_limits = __esm(() => {
48139
48139
  // src/lib/selection-cache.ts
48140
48140
  import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
48141
48141
  import { spawnSync } from "child_process";
48142
- import { closeSync as closeSync2, constants as constants3, fstatSync as fstatSync2, fsyncSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync9, readlinkSync as readlinkSync2, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
48142
+ import { closeSync as closeSync2, constants as constants3, fstatSync as fstatSync2, fsyncSync, linkSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync9, readlinkSync as readlinkSync2, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
48143
48143
  import { hostname } from "os";
48144
48144
  import { dirname as dirname7, join as join15, parse as parse2, resolve as resolve8 } from "path";
48145
48145
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
@@ -48487,7 +48487,7 @@ function readSessionLock(sessionId, options) {
48487
48487
  const before2 = lstatSync7(path, { throwIfNoEntry: false });
48488
48488
  if (!before2)
48489
48489
  return null;
48490
- if (!before2.isFile() || before2.nlink !== 1 || (before2.mode & 63) !== 0 || process.getuid && before2.uid !== process.getuid()) {
48490
+ if (!before2.isFile() || ![1, 2].includes(before2.nlink) || (before2.mode & 63) !== 0 || process.getuid && before2.uid !== process.getuid()) {
48491
48491
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is not a private regular file; recovery requires review.");
48492
48492
  }
48493
48493
  const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
@@ -48501,10 +48501,54 @@ function readSessionLock(sessionId, options) {
48501
48501
  } catch {
48502
48502
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is unreadable; recovery requires review.");
48503
48503
  }
48504
- if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
48504
+ if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.publication !== undefined && marker.publication !== "hard-link-v1" || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
48505
48505
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has invalid ownership metadata; recovery requires review.");
48506
48506
  }
48507
- return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after2.dev, ino: after2.ino, mtimeMs: after2.mtimeMs };
48507
+ let stagingPath;
48508
+ if (after2.nlink === 2) {
48509
+ stagingPath = `${path}.${marker.operationId}.prepared`;
48510
+ assertRegularPath(stagingPath);
48511
+ const staged = lstatSync7(stagingPath, { throwIfNoEntry: false });
48512
+ if (marker.publication !== "hard-link-v1" || !staged?.isFile() || staged.dev !== after2.dev || staged.ino !== after2.ino || staged.nlink !== 2 || staged.mode !== after2.mode || staged.uid !== after2.uid || staged.gid !== after2.gid) {
48513
+ throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has an unbound publication link; recovery requires review.");
48514
+ }
48515
+ }
48516
+ return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after2.dev, ino: after2.ino, mtimeMs: after2.mtimeMs, stagingPath };
48517
+ }
48518
+ function publishSessionLock(path, marker) {
48519
+ const stagingPath = `${path}.${marker.operationId}.prepared`;
48520
+ const bytes = Buffer.from(`${JSON.stringify({ ...marker, publication: "hard-link-v1" })}
48521
+ `);
48522
+ const fd = openSync2(stagingPath, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
48523
+ const stat = fstatSync2(fd);
48524
+ let published = false, handedOff = false;
48525
+ try {
48526
+ writeFileSync3(fd, bytes);
48527
+ fsyncSync(fd);
48528
+ linkSync(stagingPath, path);
48529
+ published = true;
48530
+ syncDirectory(dirname7(path));
48531
+ const staged = lstatSync7(stagingPath);
48532
+ if (!staged.isFile() || staged.dev !== stat.dev || staged.ino !== stat.ino) {
48533
+ throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The initialized lock staging inode changed before publication completed.");
48534
+ }
48535
+ unlinkSync(stagingPath);
48536
+ syncDirectory(dirname7(path));
48537
+ handedOff = true;
48538
+ return { path, stagingPath, fd, dev: stat.dev, ino: stat.ino };
48539
+ } finally {
48540
+ if (!handedOff) {
48541
+ try {
48542
+ for (const target of [...published ? [path] : [], stagingPath]) {
48543
+ const current = lstatSync7(target, { throwIfNoEntry: false });
48544
+ if (current?.isFile() && current.dev === stat.dev && current.ino === stat.ino)
48545
+ unlinkSync(target);
48546
+ }
48547
+ } finally {
48548
+ closeSync2(fd);
48549
+ }
48550
+ }
48551
+ }
48508
48552
  }
48509
48553
  function writerIsGone(lock, explicitLocalHost) {
48510
48554
  if (Date.now() - lock.mtimeMs < SESSION_LOCK_MIN_AGE_MS)
@@ -48593,10 +48637,21 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
48593
48637
  try {
48594
48638
  renameSync(lock.path, archivePath);
48595
48639
  moved = true;
48640
+ const archivedPaths = [archivePath];
48641
+ if (finalLock.stagingPath) {
48642
+ const publicationPath = join15(directory, "publication.write-lock");
48643
+ renameSync(finalLock.stagingPath, publicationPath);
48644
+ archivedPaths.push(publicationPath);
48645
+ }
48596
48646
  syncDirectory(directory);
48597
48647
  syncDirectory(dirname7(lock.path));
48598
- if (sha256Hex(readRegularFile(archivePath, SESSION_LOCK_MAX_BYTES)) !== lock.sha256) {
48599
- throw new Error("Archived lock bytes changed");
48648
+ for (const path of archivedPaths) {
48649
+ const before2 = lstatSync7(path, { throwIfNoEntry: false });
48650
+ const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
48651
+ const after2 = lstatSync7(path, { throwIfNoEntry: false });
48652
+ if (!before2?.isFile() || before2.dev !== lock.dev || before2.ino !== lock.ino || before2.nlink !== archivedPaths.length || before2.mtimeMs !== lock.mtimeMs || (before2.mode & 63) !== 0 || process.getuid && before2.uid !== process.getuid() || !bytes || sha256Hex(bytes) !== lock.sha256 || !after2?.isFile() || before2.dev !== after2.dev || before2.ino !== after2.ino || before2.mtimeMs !== after2.mtimeMs || before2.mode !== after2.mode || before2.uid !== after2.uid || before2.gid !== after2.gid || before2.nlink !== after2.nlink) {
48653
+ throw new Error("Archived lock ownership or bytes changed");
48654
+ }
48600
48655
  }
48601
48656
  } catch (error) {
48602
48657
  if (moved)
@@ -48623,6 +48678,7 @@ function recoverSessionWriteLock(sessionId, reviewDigest, options = {}) {
48623
48678
  }
48624
48679
  function withSessionWriteLocks(sessionIds, options, action) {
48625
48680
  const operationId = randomUUID3();
48681
+ const identity4 = localLockIdentity();
48626
48682
  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);
48627
48683
  const owned = [];
48628
48684
  const assertOwned = () => {
@@ -48636,9 +48692,10 @@ function withSessionWriteLocks(sessionIds, options, action) {
48636
48692
  try {
48637
48693
  for (const lock of locks) {
48638
48694
  assertRegularPath(lock.path, true);
48639
- let fd;
48695
+ const marker = { schemaVersion: identity4 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity4 };
48696
+ let acquired;
48640
48697
  try {
48641
- fd = openSync2(lock.path, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
48698
+ acquired = publishSessionLock(lock.path, marker);
48642
48699
  } catch (error) {
48643
48700
  if (error.code !== "EEXIST")
48644
48701
  throw error;
@@ -48653,7 +48710,7 @@ function withSessionWriteLocks(sessionIds, options, action) {
48653
48710
  throw recoveryError;
48654
48711
  }
48655
48712
  try {
48656
- fd = openSync2(lock.path, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
48713
+ acquired = publishSessionLock(lock.path, marker);
48657
48714
  } catch (retryError) {
48658
48715
  if (retryError.code === "EEXIST") {
48659
48716
  if (!readSessionLock(lock.sessionId, options))
@@ -48663,19 +48720,20 @@ function withSessionWriteLocks(sessionIds, options, action) {
48663
48720
  throw retryError;
48664
48721
  }
48665
48722
  }
48666
- const stat = fstatSync2(fd);
48667
- owned.push({ path: lock.path, fd, dev: stat.dev, ino: stat.ino });
48668
- const identity4 = localLockIdentity();
48669
- writeFileSync3(fd, `${JSON.stringify({ schemaVersion: identity4 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity4 })}
48670
- `);
48723
+ owned.push(acquired);
48671
48724
  }
48672
48725
  return action(assertOwned);
48673
48726
  } finally {
48674
48727
  for (const lock of owned.reverse()) {
48675
- closeSync2(lock.fd);
48676
- const current = lstatSync7(lock.path, { throwIfNoEntry: false });
48677
- if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
48678
- unlinkSync(lock.path);
48728
+ try {
48729
+ for (const path of [lock.path, lock.stagingPath]) {
48730
+ const current = lstatSync7(path, { throwIfNoEntry: false });
48731
+ if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
48732
+ unlinkSync(path);
48733
+ }
48734
+ } finally {
48735
+ closeSync2(lock.fd);
48736
+ }
48679
48737
  }
48680
48738
  }
48681
48739
  }
@@ -52538,7 +52596,7 @@ var init_agent_sumi = __esm(() => {
52538
52596
  // src/lib/agent-integration.ts
52539
52597
  import { createHash as createHash12, randomUUID as randomUUID5 } from "crypto";
52540
52598
  import { isDeepStrictEqual as isDeepStrictEqual5 } from "util";
52541
- import { existsSync as existsSync13, lstatSync as lstatSync16, statSync as statSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync13, readdirSync as readdirSync8, opendirSync as opendirSync3, readlinkSync as readlinkSync4, realpathSync as realpathSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, chmodSync, openSync as openSync8, closeSync as closeSync8, fsyncSync as fsyncSync2, fstatSync as fstatSync8, readSync as readSync6, constants as constants8, linkSync } from "fs";
52599
+ import { existsSync as existsSync13, lstatSync as lstatSync16, statSync as statSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync13, readdirSync as readdirSync8, opendirSync as opendirSync3, readlinkSync as readlinkSync4, realpathSync as realpathSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, chmodSync, openSync as openSync8, closeSync as closeSync8, fsyncSync as fsyncSync2, fstatSync as fstatSync8, readSync as readSync6, constants as constants8, linkSync as linkSync2 } from "fs";
52542
52600
  import { dirname as dirname11, join as join23, resolve as resolve12, relative as relative5, isAbsolute as isAbsolute11, sep as sep5 } from "path";
52543
52601
  import { homedir as homedir3 } from "os";
52544
52602
  function hasNoDarwinAcl(fd) {
@@ -53592,7 +53650,7 @@ function archiveNativeSkills(inventory, options) {
53592
53650
  continue;
53593
53651
  }
53594
53652
  if (entry.discoveryOnly) {
53595
- linkSync(entry.archive, entry.source);
53653
+ linkSync2(entry.archive, entry.source);
53596
53654
  syncArchiveDirectory(dirname11(entry.source));
53597
53655
  unlinkSync3(entry.archive);
53598
53656
  } else {
@@ -55575,7 +55633,7 @@ var init_plugin_projection = __esm(() => {
55575
55633
 
55576
55634
  // src/lib/plugin-projection-store.ts
55577
55635
  import { createHash as createHash14, randomUUID as randomUUID6 } from "crypto";
55578
- import { constants as constants9, closeSync as closeSync9, fchmodSync, fstatSync as fstatSync9, fsyncSync as fsyncSync3, linkSync as linkSync2, lstatSync as lstatSync18, mkdirSync as mkdirSync9, openSync as openSync9, opendirSync as opendirSync4, readSync as readSync7, renameSync as renameSync5, rmdirSync as rmdirSync3, rmSync as rmSync4, unlinkSync as unlinkSync4, writeSync as writeSync2 } from "fs";
55636
+ import { constants as constants9, closeSync as closeSync9, fchmodSync, fstatSync as fstatSync9, fsyncSync as fsyncSync3, linkSync as linkSync3, lstatSync as lstatSync18, mkdirSync as mkdirSync9, openSync as openSync9, opendirSync as opendirSync4, readSync as readSync7, renameSync as renameSync5, rmdirSync as rmdirSync3, rmSync as rmSync4, unlinkSync as unlinkSync4, writeSync as writeSync2 } from "fs";
55579
55637
  import { dirname as dirname13, isAbsolute as isAbsolute12, join as join30, parse as parse5, resolve as resolve13 } from "path";
55580
55638
  function stat(path) {
55581
55639
  try {
@@ -55887,7 +55945,7 @@ function writePluginJsonImmutable(path, value) {
55887
55945
  written = stat(temporary);
55888
55946
  verifyAncestors(chain, true);
55889
55947
  try {
55890
- linkSync2(temporary, path);
55948
+ linkSync3(temporary, path);
55891
55949
  } catch (error) {
55892
55950
  if (!(error && typeof error === "object" && ("code" in error) && error.code === "EEXIST"))
55893
55951
  throw error;
@@ -57265,13 +57323,13 @@ function resolveAgentDiscovery(options) {
57265
57323
  unresolved("JSONC configuration requires a supported format adapter");
57266
57324
  for (const root2 of [directory, join33(home, ".claude"), join33(home, ".agents")])
57267
57325
  for (const name of ["skill", "skills"])
57268
- roots.add(join33(root2, name));
57326
+ roots.add(canonical6(join33(root2, name)));
57269
57327
  if (config.skills !== undefined && (!Array.isArray(config.skills) || config.skills.some((path) => typeof path !== "string")))
57270
57328
  unresolved("skills must be a string array");
57271
57329
  for (const value of config.skills ?? []) {
57272
57330
  if (typeof value !== "string" || /[\0$]/.test(value) || !value || !(isAbsolute17(value) || value.startsWith("~/")))
57273
57331
  unresolved("remote or relative skill sources need a dedicated discovery adapter");
57274
- roots.add(value.startsWith("~/") ? join33(home, value.slice(2)) : value);
57332
+ roots.add(canonical6(value.startsWith("~/") ? join33(home, value.slice(2)) : value));
57275
57333
  }
57276
57334
  for (const key of ["plugin", "plugins"])
57277
57335
  if (config[key] !== undefined && !Array.isArray(config[key]))
@@ -109695,16 +109753,47 @@ function assertConfigUnchanged(configs) {
109695
109753
  }
109696
109754
  }
109697
109755
  }
109698
- async function runNpm(args2, cwd2, home, staging) {
109756
+ function releaseAgePolicy(options) {
109757
+ if (options.minReleaseAgeExclude !== undefined && !Array.isArray(options.minReleaseAgeExclude))
109758
+ throw new Error("MIN_RELEASE_AGE_EXCLUDE_INVALID");
109759
+ if (options.minReleaseAge === undefined) {
109760
+ if (options.minReleaseAgeExclude?.length)
109761
+ throw new Error("MIN_RELEASE_AGE_REQUIRED_FOR_EXCLUSIONS");
109762
+ return;
109763
+ }
109764
+ if (!Number.isSafeInteger(options.minReleaseAge) || options.minReleaseAge < 1)
109765
+ throw new Error("MIN_RELEASE_AGE_INVALID");
109766
+ const exclusions = options.minReleaseAgeExclude ?? [];
109767
+ if (!Array.isArray(exclusions) || exclusions.some((pattern) => typeof pattern !== "string" || pattern.length > 214 || !/^(?:@[a-z0-9*?][a-z0-9._*?-]*\/)?[a-z0-9*?][a-z0-9._*?-]*$/.test(pattern))) {
109768
+ throw new Error("MIN_RELEASE_AGE_EXCLUDE_INVALID");
109769
+ }
109770
+ return { minReleaseAge: options.minReleaseAge, minReleaseAgeExclude: [...new Set(exclusions)] };
109771
+ }
109772
+ async function npmReleaseAgeCapability(cwd2, env3) {
109773
+ try {
109774
+ const versionProbe = Bun.spawn(["npm", "--version"], { cwd: cwd2, env: env3, stdin: "ignore", stdout: "pipe", stderr: "ignore" });
109775
+ const [versionText, versionStatus] = await Promise.all([new Response(versionProbe.stdout).text(), versionProbe.exited]);
109776
+ const version2 = versionText.trim();
109777
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version2);
109778
+ if (versionStatus !== 0 || !match || Number(match[1]) < 11 || Number(match[1]) === 11 && Number(match[2]) < 19)
109779
+ throw new Error("NPM_RELEASE_AGE_UNSUPPORTED");
109780
+ const configProbe = Bun.spawn(["npm", "config", "list", "--json"], { cwd: cwd2, env: env3, stdin: "ignore", stdout: "pipe", stderr: "ignore" });
109781
+ const [configText, configStatus] = await Promise.all([new Response(configProbe.stdout).text(), configProbe.exited]);
109782
+ const config2 = object9(JSON.parse(configText), "NPM_RELEASE_AGE_UNSUPPORTED");
109783
+ if (configStatus !== 0 || !Object.hasOwn(config2, "min-release-age") || !Object.hasOwn(config2, "min-release-age-exclude") || !Array.isArray(config2["min-release-age-exclude"]))
109784
+ throw new Error("NPM_RELEASE_AGE_UNSUPPORTED");
109785
+ return version2;
109786
+ } catch {
109787
+ throw new Error("NPM_RELEASE_AGE_UNSUPPORTED");
109788
+ }
109789
+ }
109790
+ async function runNpm(args2, cwd2, home, staging, policy) {
109699
109791
  const userNpmrc = join56(staging, "npm-user.npmrc"), globalNpmrc = join56(staging, "npm-global.npmrc");
109700
- if (!existsSync34(userNpmrc))
109701
- writeFileSync23(userNpmrc, `registry=${REGISTRY_ORIGIN}/
109792
+ const baseConfig = `registry=${REGISTRY_ORIGIN}/
109702
109793
  ignore-scripts=true
109703
- `, { mode: 384, flag: "wx" });
109794
+ `;
109704
109795
  if (!existsSync34(globalNpmrc))
109705
- writeFileSync23(globalNpmrc, `registry=${REGISTRY_ORIGIN}/
109706
- ignore-scripts=true
109707
- `, { mode: 384, flag: "wx" });
109796
+ writeFileSync23(globalNpmrc, baseConfig, { mode: 384, flag: "wx" });
109708
109797
  const env3 = {
109709
109798
  PATH: process.env.PATH ?? "",
109710
109799
  HOME: home,
@@ -109718,6 +109807,19 @@ ignore-scripts=true
109718
109807
  NPM_CONFIG_REGISTRY: `${REGISTRY_ORIGIN}/`,
109719
109808
  NPM_CONFIG_IGNORE_SCRIPTS: "true"
109720
109809
  };
109810
+ const capabilityNpmrc = join56(staging, "npm-capability.npmrc");
109811
+ if (policy && !existsSync34(capabilityNpmrc))
109812
+ writeFileSync23(capabilityNpmrc, baseConfig, { mode: 384, flag: "wx" });
109813
+ if (policy && readFileSync37(capabilityNpmrc, "utf8") !== baseConfig)
109814
+ throw new Error("NPM_ISOLATED_CONFIG_DRIFT");
109815
+ const npmVersion = policy ? await npmReleaseAgeCapability(cwd2, { ...env3, NPM_CONFIG_USERCONFIG: capabilityNpmrc }) : undefined;
109816
+ const config2 = baseConfig + (policy ? `min-release-age=${policy.minReleaseAge}
109817
+ ${policy.minReleaseAgeExclude.map((pattern) => `min-release-age-exclude[]=${pattern}
109818
+ `).join("")}` : "");
109819
+ if (!existsSync34(userNpmrc))
109820
+ writeFileSync23(userNpmrc, config2, { mode: 384, flag: "wx" });
109821
+ if (readFileSync37(userNpmrc, "utf8") !== config2 || readFileSync37(globalNpmrc, "utf8") !== baseConfig)
109822
+ throw new Error("NPM_ISOLATED_CONFIG_DRIFT");
109721
109823
  const proc = Bun.spawn(["npm", ...args2, "--registry", `${REGISTRY_ORIGIN}/`, "--ignore-scripts", "--no-audit", "--no-fund"], {
109722
109824
  cwd: cwd2,
109723
109825
  env: env3,
@@ -109728,6 +109830,7 @@ ignore-scripts=true
109728
109830
  const exitCode = await proc.exited;
109729
109831
  if (exitCode !== 0)
109730
109832
  throw new Error("DEPENDENCY_GRAPH_INSTALL_FAILED");
109833
+ return npmVersion;
109731
109834
  }
109732
109835
  function comparePackageTrees(expectedRoot, installedRoot) {
109733
109836
  const expected = readTree(expectedRoot, false);
@@ -110106,6 +110209,7 @@ async function updateCopyfileRuntime(version2, options = {}) {
110106
110209
  throw new Error("EXACT_VERSION_REQUIRED");
110107
110210
  if (!STABLE_SEMVER_PATTERN.test(version2))
110108
110211
  throw new Error("EXACT_STABLE_VERSION_REQUIRED");
110212
+ const policy = releaseAgePolicy(options);
110109
110213
  const home = resolve24(options.homeDir ?? process.env.HOME ?? "");
110110
110214
  if (!home || !existsSync34(home))
110111
110215
  throw new Error("HOME_NOT_FOUND");
@@ -110143,8 +110247,10 @@ async function updateCopyfileRuntime(version2, options = {}) {
110143
110247
  mkdirSync24(stagePackage, { mode: 448 });
110144
110248
  copyFileSync(artifact.tarballPath, join56(stagePackage, "verified.tgz"));
110145
110249
  writeFileSync23(join56(stagePackage, "package.json"), JSON.stringify({ private: true, dependencies: { [PACKAGE_NAME]: "file:./verified.tgz" } }, null, 2), { mode: 384, flag: "wx" });
110146
- await runNpm(["install", "--package-lock-only", "--prefix", stagePackage], stagePackage, home, stagePath);
110147
- await runNpm(["ci", "--prefix", stagePackage], stagePackage, home, stagePath);
110250
+ const npmVersion = await runNpm(["install", "--package-lock-only", "--prefix", stagePackage], stagePackage, home, stagePath, policy);
110251
+ const ciNpmVersion = await runNpm(["ci", "--prefix", stagePackage], stagePackage, home, stagePath, policy);
110252
+ if (npmVersion !== ciNpmVersion)
110253
+ throw new Error("NPM_VERSION_DRIFT_DURING_UPDATE");
110148
110254
  const lockBytes = readFileSync37(join56(stagePackage, "package-lock.json"));
110149
110255
  const installedPackage = join56(stagePackage, "node_modules", "@hasna", "skills");
110150
110256
  const installed = readPackage(join56(installedPackage, "package.json"));
@@ -110185,6 +110291,7 @@ async function updateCopyfileRuntime(version2, options = {}) {
110185
110291
  targetPackageRoot,
110186
110292
  configs: configPreimages,
110187
110293
  preimageSha256,
110294
+ ...policy ? { dependencyPolicy: { ...policy, npmVersion } } : {},
110188
110295
  launchers,
110189
110296
  switchedLaunchers: [],
110190
110297
  rollbackCompletedLaunchers: []
@@ -110223,7 +110330,7 @@ async function updateCopyfileRuntime(version2, options = {}) {
110223
110330
  }
110224
110331
  receipt.state = "switched";
110225
110332
  persistReceipt(join56(finalPath, "rollout-receipt.json"), receipt);
110226
- return { updated: true, version: version2, currentVersion: layout.currentVersion, receiptId: id2, runtimeRoot: finalPath, tarballSha256: artifact.sha256, tarballIntegrity: artifact.integrity, runtimeTreeSha256: stagedTree.digest, launcherCount: launchers.length, configCount: configPreimages.length };
110333
+ return { updated: true, version: version2, currentVersion: layout.currentVersion, receiptId: id2, runtimeRoot: finalPath, tarballSha256: artifact.sha256, tarballIntegrity: artifact.integrity, runtimeTreeSha256: stagedTree.digest, launcherCount: launchers.length, configCount: configPreimages.length, ...policy ? { dependencyPolicy: { ...policy, npmVersion } } : {} };
110227
110334
  } catch (error61) {
110228
110335
  if (switched.length > 0 || moved) {
110229
110336
  let rollbackFailed = false;
@@ -110447,7 +110554,12 @@ function registerRuntime(parent) {
110447
110554
  });
110448
110555
  const setup = parent.command("setup").description("Point this CLI at a Skills API server, or register agent integrations").option("--api-url <url>", "Skills API origin to send remote work to").option("--global", "Accepted for compatibility; the API origin is always per-user", false).option("--json", "Output setup result as JSON", false).action(async (options) => handleSetup(options));
110449
110556
  setup.command("agents").option("--json", "Output registration result as JSON", false).description("Register the Skills MCP server with all supported agents").action(async (options) => handleMcp({ register: "all", json: options.json }));
110450
- parent.command("self-update").description("Update @hasna/skills to the latest version").option("--json", "Output result as JSON", false).option("--version <version>", "Install this exact @hasna/skills version as a staged copyfile runtime").option("--rollback <receipt-id>", "Restore launcher links from an exact copyfile rollout receipt").option("--adopt-aliases", "Switch verified legacy @hasna/skills aliases to the active copyfile runtime").option("--rollback-aliases <receipt-id>", "Restore aliases from an exact adoption receipt").action(async (options) => {
110557
+ parent.command("self-update").description("Update @hasna/skills to the latest version").option("--json", "Output result as JSON", false).option("--version <version>", "Install this exact @hasna/skills version as a staged copyfile runtime").option("--min-release-age <days>", "Require a positive integer dependency release age in days for an exact-version update").option("--min-release-age-exclude <pattern>", "Exclude matching package identities from the release-age policy (repeatable)", (pattern, prior) => [...prior, pattern], []).option("--rollback <receipt-id>", "Restore launcher links from an exact copyfile rollout receipt").option("--adopt-aliases", "Switch verified legacy @hasna/skills aliases to the active copyfile runtime").option("--rollback-aliases <receipt-id>", "Restore aliases from an exact adoption receipt").action(async (options) => {
110558
+ if ((options.minReleaseAge !== undefined || options.minReleaseAgeExclude.length > 0) && (!options.version || options.rollback || options.adoptAliases || options.rollbackAliases)) {
110559
+ console.error(JSON.stringify({ error: "RELEASE_AGE_POLICY_REQUIRES_EXACT_VERSION" }));
110560
+ process.exitCode = 1;
110561
+ return;
110562
+ }
110451
110563
  if ([options.version, options.rollback, options.adoptAliases, options.rollbackAliases].filter(Boolean).length > 1) {
110452
110564
  console.error(JSON.stringify({ error: "SELF_UPDATE_OPERATIONS_ARE_MUTUALLY_EXCLUSIVE" }));
110453
110565
  process.exitCode = 1;
@@ -110483,7 +110595,8 @@ function registerRuntime(parent) {
110483
110595
  }
110484
110596
  if (options.version) {
110485
110597
  try {
110486
- const result2 = await updateCopyfileRuntime(options.version);
110598
+ const minReleaseAge = options.minReleaseAge === undefined ? undefined : /^\d+$/.test(options.minReleaseAge) ? Number(options.minReleaseAge) : NaN;
110599
+ const result2 = await updateCopyfileRuntime(options.version, { minReleaseAge, minReleaseAgeExclude: options.minReleaseAgeExclude });
110487
110600
  if (options.json)
110488
110601
  console.log(JSON.stringify(result2));
110489
110602
  else
package/bin/mcp.js CHANGED
@@ -20019,7 +20019,7 @@ var package_default;
20019
20019
  var init_package = __esm(() => {
20020
20020
  package_default = {
20021
20021
  name: "@hasna/skills",
20022
- version: "0.10.31",
20022
+ version: "0.10.34",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
@@ -46227,7 +46227,7 @@ var init_profile_limits = __esm(() => {
46227
46227
  // src/lib/selection-cache.ts
46228
46228
  import { createHash as createHash6, randomUUID } from "crypto";
46229
46229
  import { spawnSync as spawnSync2 } from "child_process";
46230
- import { closeSync as closeSync6, constants as constants3, fstatSync as fstatSync6, fsyncSync, lstatSync as lstatSync8, mkdirSync as mkdirSync3, openSync as openSync6, readFileSync as readFileSync10, readlinkSync as readlinkSync2, readSync as readSync3, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
46230
+ import { closeSync as closeSync6, constants as constants3, fstatSync as fstatSync6, fsyncSync, linkSync, lstatSync as lstatSync8, mkdirSync as mkdirSync3, openSync as openSync6, readFileSync as readFileSync10, readlinkSync as readlinkSync2, readSync as readSync3, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
46231
46231
  import { hostname as hostname3 } from "os";
46232
46232
  import { dirname as dirname8, join as join14, parse as parse7, resolve as resolve7 } from "path";
46233
46233
  import { isDeepStrictEqual } from "util";
@@ -46563,7 +46563,7 @@ function readSessionLock(sessionId, options) {
46563
46563
  const before = lstatSync8(path, { throwIfNoEntry: false });
46564
46564
  if (!before)
46565
46565
  return null;
46566
- if (!before.isFile() || before.nlink !== 1 || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid()) {
46566
+ if (!before.isFile() || ![1, 2].includes(before.nlink) || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid()) {
46567
46567
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is not a private regular file; recovery requires review.");
46568
46568
  }
46569
46569
  const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
@@ -46577,10 +46577,54 @@ function readSessionLock(sessionId, options) {
46577
46577
  } catch {
46578
46578
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is unreadable; recovery requires review.");
46579
46579
  }
46580
- if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
46580
+ if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.publication !== undefined && marker.publication !== "hard-link-v1" || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
46581
46581
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has invalid ownership metadata; recovery requires review.");
46582
46582
  }
46583
- return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after.dev, ino: after.ino, mtimeMs: after.mtimeMs };
46583
+ let stagingPath;
46584
+ if (after.nlink === 2) {
46585
+ stagingPath = `${path}.${marker.operationId}.prepared`;
46586
+ assertRegularPath(stagingPath);
46587
+ const staged = lstatSync8(stagingPath, { throwIfNoEntry: false });
46588
+ 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) {
46589
+ throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has an unbound publication link; recovery requires review.");
46590
+ }
46591
+ }
46592
+ return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after.dev, ino: after.ino, mtimeMs: after.mtimeMs, stagingPath };
46593
+ }
46594
+ function publishSessionLock(path, marker) {
46595
+ const stagingPath = `${path}.${marker.operationId}.prepared`;
46596
+ const bytes = Buffer.from(`${JSON.stringify({ ...marker, publication: "hard-link-v1" })}
46597
+ `);
46598
+ const fd = openSync6(stagingPath, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
46599
+ const stat = fstatSync6(fd);
46600
+ let published = false, handedOff = false;
46601
+ try {
46602
+ writeFileSync3(fd, bytes);
46603
+ fsyncSync(fd);
46604
+ linkSync(stagingPath, path);
46605
+ published = true;
46606
+ syncDirectory(dirname8(path));
46607
+ const staged = lstatSync8(stagingPath);
46608
+ if (!staged.isFile() || staged.dev !== stat.dev || staged.ino !== stat.ino) {
46609
+ throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The initialized lock staging inode changed before publication completed.");
46610
+ }
46611
+ unlinkSync(stagingPath);
46612
+ syncDirectory(dirname8(path));
46613
+ handedOff = true;
46614
+ return { path, stagingPath, fd, dev: stat.dev, ino: stat.ino };
46615
+ } finally {
46616
+ if (!handedOff) {
46617
+ try {
46618
+ for (const target of [...published ? [path] : [], stagingPath]) {
46619
+ const current = lstatSync8(target, { throwIfNoEntry: false });
46620
+ if (current?.isFile() && current.dev === stat.dev && current.ino === stat.ino)
46621
+ unlinkSync(target);
46622
+ }
46623
+ } finally {
46624
+ closeSync6(fd);
46625
+ }
46626
+ }
46627
+ }
46584
46628
  }
46585
46629
  function writerIsGone(lock, explicitLocalHost) {
46586
46630
  if (Date.now() - lock.mtimeMs < SESSION_LOCK_MIN_AGE_MS)
@@ -46642,10 +46686,21 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
46642
46686
  try {
46643
46687
  renameSync(lock.path, archivePath);
46644
46688
  moved = true;
46689
+ const archivedPaths = [archivePath];
46690
+ if (finalLock.stagingPath) {
46691
+ const publicationPath = join14(directory, "publication.write-lock");
46692
+ renameSync(finalLock.stagingPath, publicationPath);
46693
+ archivedPaths.push(publicationPath);
46694
+ }
46645
46695
  syncDirectory(directory);
46646
46696
  syncDirectory(dirname8(lock.path));
46647
- if (sha256Hex(readRegularFile(archivePath, SESSION_LOCK_MAX_BYTES)) !== lock.sha256) {
46648
- throw new Error("Archived lock bytes changed");
46697
+ for (const path of archivedPaths) {
46698
+ const before = lstatSync8(path, { throwIfNoEntry: false });
46699
+ const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
46700
+ const after = lstatSync8(path, { throwIfNoEntry: false });
46701
+ 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) {
46702
+ throw new Error("Archived lock ownership or bytes changed");
46703
+ }
46649
46704
  }
46650
46705
  } catch (error61) {
46651
46706
  if (moved)
@@ -46662,6 +46717,7 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
46662
46717
  }
46663
46718
  function withSessionWriteLocks(sessionIds, options, action) {
46664
46719
  const operationId = randomUUID();
46720
+ const identity3 = localLockIdentity();
46665
46721
  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);
46666
46722
  const owned = [];
46667
46723
  const assertOwned = () => {
@@ -46675,9 +46731,10 @@ function withSessionWriteLocks(sessionIds, options, action) {
46675
46731
  try {
46676
46732
  for (const lock of locks) {
46677
46733
  assertRegularPath(lock.path, true);
46678
- let fd;
46734
+ const marker = { schemaVersion: identity3 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity3 };
46735
+ let acquired;
46679
46736
  try {
46680
- fd = openSync6(lock.path, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
46737
+ acquired = publishSessionLock(lock.path, marker);
46681
46738
  } catch (error61) {
46682
46739
  if (error61.code !== "EEXIST")
46683
46740
  throw error61;
@@ -46692,7 +46749,7 @@ function withSessionWriteLocks(sessionIds, options, action) {
46692
46749
  throw recoveryError;
46693
46750
  }
46694
46751
  try {
46695
- fd = openSync6(lock.path, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL | constants3.O_NOFOLLOW, 384);
46752
+ acquired = publishSessionLock(lock.path, marker);
46696
46753
  } catch (retryError) {
46697
46754
  if (retryError.code === "EEXIST") {
46698
46755
  if (!readSessionLock(lock.sessionId, options))
@@ -46702,19 +46759,20 @@ function withSessionWriteLocks(sessionIds, options, action) {
46702
46759
  throw retryError;
46703
46760
  }
46704
46761
  }
46705
- const stat = fstatSync6(fd);
46706
- owned.push({ path: lock.path, fd, dev: stat.dev, ino: stat.ino });
46707
- const identity3 = localLockIdentity();
46708
- writeFileSync3(fd, `${JSON.stringify({ schemaVersion: identity3 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity3 })}
46709
- `);
46762
+ owned.push(acquired);
46710
46763
  }
46711
46764
  return action(assertOwned);
46712
46765
  } finally {
46713
46766
  for (const lock of owned.reverse()) {
46714
- closeSync6(lock.fd);
46715
- const current = lstatSync8(lock.path, { throwIfNoEntry: false });
46716
- if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
46717
- unlinkSync(lock.path);
46767
+ try {
46768
+ for (const path of [lock.path, lock.stagingPath]) {
46769
+ const current = lstatSync8(path, { throwIfNoEntry: false });
46770
+ if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
46771
+ unlinkSync(path);
46772
+ }
46773
+ } finally {
46774
+ closeSync6(lock.fd);
46775
+ }
46718
46776
  }
46719
46777
  }
46720
46778
  }
@@ -48897,7 +48955,7 @@ var init_plugin_projection = __esm(() => {
48897
48955
  });
48898
48956
 
48899
48957
  // src/lib/plugin-projection-store.ts
48900
- import { constants as constants7, closeSync as closeSync10, fchmodSync, fstatSync as fstatSync10, fsyncSync as fsyncSync2, linkSync, lstatSync as lstatSync14, mkdirSync as mkdirSync6, openSync as openSync10, opendirSync as opendirSync2, readSync as readSync6, renameSync as renameSync2, rmdirSync as rmdirSync2, rmSync as rmSync2, unlinkSync as unlinkSync3, writeSync as writeSync2 } from "fs";
48958
+ import { constants as constants7, closeSync as closeSync10, fchmodSync, fstatSync as fstatSync10, fsyncSync as fsyncSync2, linkSync as linkSync2, lstatSync as lstatSync14, mkdirSync as mkdirSync6, openSync as openSync10, opendirSync as opendirSync2, readSync as readSync6, renameSync as renameSync2, rmdirSync as rmdirSync2, rmSync as rmSync2, unlinkSync as unlinkSync3, writeSync as writeSync2 } from "fs";
48901
48959
  import { dirname as dirname11, isAbsolute as isAbsolute9, join as join22, parse as parse9, resolve as resolve11 } from "path";
48902
48960
  function stat(path) {
48903
48961
  try {
@@ -49956,13 +50014,13 @@ function resolveAgentDiscovery(options) {
49956
50014
  unresolved("JSONC configuration requires a supported format adapter");
49957
50015
  for (const root of [directory, join25(home, ".claude"), join25(home, ".agents")])
49958
50016
  for (const name of ["skill", "skills"])
49959
- roots.add(join25(root, name));
50017
+ roots.add(canonical6(join25(root, name)));
49960
50018
  if (config2.skills !== undefined && (!Array.isArray(config2.skills) || config2.skills.some((path) => typeof path !== "string")))
49961
50019
  unresolved("skills must be a string array");
49962
50020
  for (const value of config2.skills ?? []) {
49963
50021
  if (typeof value !== "string" || /[\0$]/.test(value) || !value || !(isAbsolute13(value) || value.startsWith("~/")))
49964
50022
  unresolved("remote or relative skill sources need a dedicated discovery adapter");
49965
- roots.add(value.startsWith("~/") ? join25(home, value.slice(2)) : value);
50023
+ roots.add(canonical6(value.startsWith("~/") ? join25(home, value.slice(2)) : value));
49966
50024
  }
49967
50025
  for (const key of ["plugin", "plugins"])
49968
50026
  if (config2[key] !== undefined && !Array.isArray(config2[key]))
package/bin/migrate.js CHANGED
@@ -7,7 +7,7 @@ import { join as join5 } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@hasna/skills",
10
- version: "0.10.31",
10
+ version: "0.10.34",
11
11
  description: "Skills library for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
package/bin/server.js CHANGED
@@ -24122,7 +24122,7 @@ var init_dist_es9 = __esm(() => {
24122
24122
  // package.json
24123
24123
  var package_default = {
24124
24124
  name: "@hasna/skills",
24125
- version: "0.10.31",
24125
+ version: "0.10.34",
24126
24126
  description: "Skills library for AI coding agents",
24127
24127
  type: "module",
24128
24128
  bin: {
package/bin/worker.js CHANGED
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24125
24125
  // package.json
24126
24126
  var package_default = {
24127
24127
  name: "@hasna/skills",
24128
- version: "0.10.31",
24128
+ version: "0.10.34",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
@@ -26,6 +26,8 @@ export declare function updateCopyfileRuntime(version: string, options?: {
26
26
  pathValue?: string;
27
27
  registryOrigin?: string;
28
28
  fetcher?: typeof fetch;
29
+ minReleaseAge?: number;
30
+ minReleaseAgeExclude?: string[];
29
31
  onLauncherSwitched?: (path: string) => void;
30
32
  }): Promise<JsonObject>;
31
33
  export declare function rollbackCopyfileRuntime(receiptId: string, options?: {
package/dist/index.js CHANGED
@@ -9665,7 +9665,7 @@ import { isAbsolute as isAbsolute13, join as join26, resolve as resolve13 } from
9665
9665
  // src/lib/selection-cache.ts
9666
9666
  import { createHash as createHash6, randomUUID } from "crypto";
9667
9667
  import { spawnSync } from "child_process";
9668
- import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync8, readlinkSync as readlinkSync2, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
9668
+ import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, linkSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync8, readlinkSync as readlinkSync2, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
9669
9669
  import { hostname } from "os";
9670
9670
  import { dirname as dirname7, join as join11, parse as parse2, resolve as resolve7 } from "path";
9671
9671
  import { isDeepStrictEqual } from "util";
@@ -10655,7 +10655,7 @@ function readSessionLock(sessionId, options) {
10655
10655
  const before = lstatSync7(path, { throwIfNoEntry: false });
10656
10656
  if (!before)
10657
10657
  return null;
10658
- if (!before.isFile() || before.nlink !== 1 || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid()) {
10658
+ if (!before.isFile() || ![1, 2].includes(before.nlink) || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid()) {
10659
10659
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is not a private regular file; recovery requires review.");
10660
10660
  }
10661
10661
  const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
@@ -10669,10 +10669,54 @@ function readSessionLock(sessionId, options) {
10669
10669
  } catch {
10670
10670
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is unreadable; recovery requires review.");
10671
10671
  }
10672
- if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
10672
+ if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.publication !== undefined && marker.publication !== "hard-link-v1" || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
10673
10673
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has invalid ownership metadata; recovery requires review.");
10674
10674
  }
10675
- return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after.dev, ino: after.ino, mtimeMs: after.mtimeMs };
10675
+ let stagingPath;
10676
+ if (after.nlink === 2) {
10677
+ stagingPath = `${path}.${marker.operationId}.prepared`;
10678
+ assertRegularPath(stagingPath);
10679
+ const staged = lstatSync7(stagingPath, { throwIfNoEntry: false });
10680
+ 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) {
10681
+ throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has an unbound publication link; recovery requires review.");
10682
+ }
10683
+ }
10684
+ return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after.dev, ino: after.ino, mtimeMs: after.mtimeMs, stagingPath };
10685
+ }
10686
+ function publishSessionLock(path, marker) {
10687
+ const stagingPath = `${path}.${marker.operationId}.prepared`;
10688
+ const bytes = Buffer.from(`${JSON.stringify({ ...marker, publication: "hard-link-v1" })}
10689
+ `);
10690
+ const fd = openSync2(stagingPath, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
10691
+ const stat = fstatSync2(fd);
10692
+ let published = false, handedOff = false;
10693
+ try {
10694
+ writeFileSync3(fd, bytes);
10695
+ fsyncSync(fd);
10696
+ linkSync(stagingPath, path);
10697
+ published = true;
10698
+ syncDirectory(dirname7(path));
10699
+ const staged = lstatSync7(stagingPath);
10700
+ if (!staged.isFile() || staged.dev !== stat.dev || staged.ino !== stat.ino) {
10701
+ throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The initialized lock staging inode changed before publication completed.");
10702
+ }
10703
+ unlinkSync(stagingPath);
10704
+ syncDirectory(dirname7(path));
10705
+ handedOff = true;
10706
+ return { path, stagingPath, fd, dev: stat.dev, ino: stat.ino };
10707
+ } finally {
10708
+ if (!handedOff) {
10709
+ try {
10710
+ for (const target of [...published ? [path] : [], stagingPath]) {
10711
+ const current = lstatSync7(target, { throwIfNoEntry: false });
10712
+ if (current?.isFile() && current.dev === stat.dev && current.ino === stat.ino)
10713
+ unlinkSync(target);
10714
+ }
10715
+ } finally {
10716
+ closeSync2(fd);
10717
+ }
10718
+ }
10719
+ }
10676
10720
  }
10677
10721
  function writerIsGone(lock, explicitLocalHost) {
10678
10722
  if (Date.now() - lock.mtimeMs < SESSION_LOCK_MIN_AGE_MS)
@@ -10734,10 +10778,21 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
10734
10778
  try {
10735
10779
  renameSync(lock.path, archivePath);
10736
10780
  moved = true;
10781
+ const archivedPaths = [archivePath];
10782
+ if (finalLock.stagingPath) {
10783
+ const publicationPath = join11(directory, "publication.write-lock");
10784
+ renameSync(finalLock.stagingPath, publicationPath);
10785
+ archivedPaths.push(publicationPath);
10786
+ }
10737
10787
  syncDirectory(directory);
10738
10788
  syncDirectory(dirname7(lock.path));
10739
- if (sha256Hex(readRegularFile(archivePath, SESSION_LOCK_MAX_BYTES)) !== lock.sha256) {
10740
- throw new Error("Archived lock bytes changed");
10789
+ for (const path of archivedPaths) {
10790
+ const before = lstatSync7(path, { throwIfNoEntry: false });
10791
+ const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
10792
+ const after = lstatSync7(path, { throwIfNoEntry: false });
10793
+ 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) {
10794
+ throw new Error("Archived lock ownership or bytes changed");
10795
+ }
10741
10796
  }
10742
10797
  } catch (error) {
10743
10798
  if (moved)
@@ -10754,6 +10809,7 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
10754
10809
  }
10755
10810
  function withSessionWriteLocks(sessionIds, options, action) {
10756
10811
  const operationId = randomUUID();
10812
+ const identity3 = localLockIdentity();
10757
10813
  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);
10758
10814
  const owned = [];
10759
10815
  const assertOwned = () => {
@@ -10767,9 +10823,10 @@ function withSessionWriteLocks(sessionIds, options, action) {
10767
10823
  try {
10768
10824
  for (const lock of locks) {
10769
10825
  assertRegularPath(lock.path, true);
10770
- let fd;
10826
+ const marker = { schemaVersion: identity3 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity3 };
10827
+ let acquired;
10771
10828
  try {
10772
- fd = openSync2(lock.path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
10829
+ acquired = publishSessionLock(lock.path, marker);
10773
10830
  } catch (error) {
10774
10831
  if (error.code !== "EEXIST")
10775
10832
  throw error;
@@ -10784,7 +10841,7 @@ function withSessionWriteLocks(sessionIds, options, action) {
10784
10841
  throw recoveryError;
10785
10842
  }
10786
10843
  try {
10787
- fd = openSync2(lock.path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
10844
+ acquired = publishSessionLock(lock.path, marker);
10788
10845
  } catch (retryError) {
10789
10846
  if (retryError.code === "EEXIST") {
10790
10847
  if (!readSessionLock(lock.sessionId, options))
@@ -10794,19 +10851,20 @@ function withSessionWriteLocks(sessionIds, options, action) {
10794
10851
  throw retryError;
10795
10852
  }
10796
10853
  }
10797
- const stat = fstatSync2(fd);
10798
- owned.push({ path: lock.path, fd, dev: stat.dev, ino: stat.ino });
10799
- const identity3 = localLockIdentity();
10800
- writeFileSync3(fd, `${JSON.stringify({ schemaVersion: identity3 ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity3 })}
10801
- `);
10854
+ owned.push(acquired);
10802
10855
  }
10803
10856
  return action(assertOwned);
10804
10857
  } finally {
10805
10858
  for (const lock of owned.reverse()) {
10806
- closeSync2(lock.fd);
10807
- const current = lstatSync7(lock.path, { throwIfNoEntry: false });
10808
- if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
10809
- unlinkSync(lock.path);
10859
+ try {
10860
+ for (const path of [lock.path, lock.stagingPath]) {
10861
+ const current = lstatSync7(path, { throwIfNoEntry: false });
10862
+ if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
10863
+ unlinkSync(path);
10864
+ }
10865
+ } finally {
10866
+ closeSync2(lock.fd);
10867
+ }
10810
10868
  }
10811
10869
  }
10812
10870
  }
@@ -10917,7 +10975,7 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
10917
10975
  // package.json
10918
10976
  var package_default = {
10919
10977
  name: "@hasna/skills",
10920
- version: "0.10.31",
10978
+ version: "0.10.34",
10921
10979
  description: "Skills library for AI coding agents",
10922
10980
  type: "module",
10923
10981
  bin: {
@@ -14152,7 +14210,7 @@ function upgradeCodexSettingsWitnessV3(previous, reviewedSettingsPath) {
14152
14210
  // src/lib/agent-integration.ts
14153
14211
  import { createHash as createHash12, randomUUID as randomUUID2 } from "crypto";
14154
14212
  import { isDeepStrictEqual as isDeepStrictEqual4 } from "util";
14155
- import { existsSync as existsSync9, lstatSync as lstatSync16, statSync as statSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync12, readdirSync as readdirSync8, opendirSync as opendirSync3, readlinkSync as readlinkSync4, realpathSync as realpathSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, chmodSync, openSync as openSync8, closeSync as closeSync8, fsyncSync as fsyncSync2, fstatSync as fstatSync8, readSync as readSync6, constants as constants7, linkSync } from "fs";
14213
+ import { existsSync as existsSync9, lstatSync as lstatSync16, statSync as statSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync12, readdirSync as readdirSync8, opendirSync as opendirSync3, readlinkSync as readlinkSync4, realpathSync as realpathSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, chmodSync, openSync as openSync8, closeSync as closeSync8, fsyncSync as fsyncSync2, fstatSync as fstatSync8, readSync as readSync6, constants as constants7, linkSync as linkSync2 } from "fs";
14156
14214
  import { dirname as dirname11, join as join18, resolve as resolve11, relative as relative5, isAbsolute as isAbsolute11, sep as sep5 } from "path";
14157
14215
  import { homedir as homedir2 } from "os";
14158
14216
 
@@ -15741,7 +15799,7 @@ function archiveNativeSkills(inventory, options) {
15741
15799
  continue;
15742
15800
  }
15743
15801
  if (entry.discoveryOnly) {
15744
- linkSync(entry.archive, entry.source);
15802
+ linkSync2(entry.archive, entry.source);
15745
15803
  syncArchiveDirectory(dirname11(entry.source));
15746
15804
  unlinkSync3(entry.archive);
15747
15805
  } else {
@@ -17321,7 +17379,7 @@ function buildPluginProjection(entries) {
17321
17379
 
17322
17380
  // src/lib/plugin-projection-store.ts
17323
17381
  import { createHash as createHash14, randomUUID as randomUUID3 } from "crypto";
17324
- import { constants as constants8, closeSync as closeSync9, fchmodSync, fstatSync as fstatSync9, fsyncSync as fsyncSync3, linkSync as linkSync2, lstatSync as lstatSync18, mkdirSync as mkdirSync8, openSync as openSync9, opendirSync as opendirSync4, readSync as readSync7, renameSync as renameSync4, rmdirSync as rmdirSync3, rmSync as rmSync3, unlinkSync as unlinkSync4, writeSync as writeSync2 } from "fs";
17382
+ import { constants as constants8, closeSync as closeSync9, fchmodSync, fstatSync as fstatSync9, fsyncSync as fsyncSync3, linkSync as linkSync3, lstatSync as lstatSync18, mkdirSync as mkdirSync8, openSync as openSync9, opendirSync as opendirSync4, readSync as readSync7, renameSync as renameSync4, rmdirSync as rmdirSync3, rmSync as rmSync3, unlinkSync as unlinkSync4, writeSync as writeSync2 } from "fs";
17325
17383
  import { dirname as dirname13, isAbsolute as isAbsolute12, join as join25, parse as parse5, resolve as resolve12 } from "path";
17326
17384
  var READ_FLAGS = constants8.O_RDONLY | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
17327
17385
  var WRITE_FLAGS = constants8.O_WRONLY | constants8.O_CREAT | constants8.O_EXCL | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
@@ -17637,7 +17695,7 @@ function writePluginJsonImmutable(path, value) {
17637
17695
  written = stat(temporary);
17638
17696
  verifyAncestors(chain, true);
17639
17697
  try {
17640
- linkSync2(temporary, path);
17698
+ linkSync3(temporary, path);
17641
17699
  } catch (error) {
17642
17700
  if (!(error && typeof error === "object" && ("code" in error) && error.code === "EEXIST"))
17643
17701
  throw error;
@@ -18737,13 +18795,13 @@ function resolveAgentDiscovery(options) {
18737
18795
  unresolved("JSONC configuration requires a supported format adapter");
18738
18796
  for (const root2 of [directory, join28(home, ".claude"), join28(home, ".agents")])
18739
18797
  for (const name of ["skill", "skills"])
18740
- roots.add(join28(root2, name));
18798
+ roots.add(canonical6(join28(root2, name)));
18741
18799
  if (config.skills !== undefined && (!Array.isArray(config.skills) || config.skills.some((path) => typeof path !== "string")))
18742
18800
  unresolved("skills must be a string array");
18743
18801
  for (const value of config.skills ?? []) {
18744
18802
  if (typeof value !== "string" || /[\0$]/.test(value) || !value || !(isAbsolute16(value) || value.startsWith("~/")))
18745
18803
  unresolved("remote or relative skill sources need a dedicated discovery adapter");
18746
- roots.add(value.startsWith("~/") ? join28(home, value.slice(2)) : value);
18804
+ roots.add(canonical6(value.startsWith("~/") ? join28(home, value.slice(2)) : value));
18747
18805
  }
18748
18806
  for (const key of ["plugin", "plugins"])
18749
18807
  if (config[key] !== undefined && !Array.isArray(config[key]))
package/dist/sdk/index.js CHANGED
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
32373
32373
  // package.json
32374
32374
  var package_default = {
32375
32375
  name: "@hasna/skills",
32376
- version: "0.10.31",
32376
+ version: "0.10.34",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -33976,7 +33976,7 @@ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
33976
33976
  // src/lib/selection-cache.ts
33977
33977
  import { createHash as createHash3, randomUUID } from "crypto";
33978
33978
  import { spawnSync as spawnSync2 } from "child_process";
33979
- import { closeSync as closeSync5, constants as constants2, fstatSync as fstatSync5, fsyncSync, lstatSync as lstatSync3, mkdirSync as mkdirSync2, openSync as openSync5, readFileSync as readFileSync6, readlinkSync, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
33979
+ import { closeSync as closeSync5, constants as constants2, fstatSync as fstatSync5, fsyncSync, linkSync, lstatSync as lstatSync3, mkdirSync as mkdirSync2, openSync as openSync5, readFileSync as readFileSync6, readlinkSync, readSync as readSync2, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
33980
33980
  import { hostname } from "os";
33981
33981
  import { dirname as dirname3, join as join7, parse, resolve as resolve2 } from "path";
33982
33982
  import { isDeepStrictEqual } from "util";
@@ -35259,7 +35259,7 @@ function readSessionLock(sessionId, options) {
35259
35259
  const before = lstatSync3(path, { throwIfNoEntry: false });
35260
35260
  if (!before)
35261
35261
  return null;
35262
- if (!before.isFile() || before.nlink !== 1 || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid()) {
35262
+ if (!before.isFile() || ![1, 2].includes(before.nlink) || (before.mode & 63) !== 0 || process.getuid && before.uid !== process.getuid()) {
35263
35263
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is not a private regular file; recovery requires review.");
35264
35264
  }
35265
35265
  const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
@@ -35273,10 +35273,54 @@ function readSessionLock(sessionId, options) {
35273
35273
  } catch {
35274
35274
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock is unreadable; recovery requires review.");
35275
35275
  }
35276
- if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
35276
+ if (!marker || ![1, 2].includes(marker.schemaVersion) || marker.sessionId !== sessionId || !/^[a-f0-9-]{36}$/i.test(marker.operationId) || !Number.isSafeInteger(marker.pid) || marker.pid <= 0 || marker.publication !== undefined && marker.publication !== "hard-link-v1" || marker.schemaVersion === 2 && (!marker.host || !marker.bootId || !marker.processStart || process.platform === "linux" && !marker.pidNamespace)) {
35277
35277
  throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has invalid ownership metadata; recovery requires review.");
35278
35278
  }
35279
- return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after.dev, ino: after.ino, mtimeMs: after.mtimeMs };
35279
+ let stagingPath;
35280
+ if (after.nlink === 2) {
35281
+ stagingPath = `${path}.${marker.operationId}.prepared`;
35282
+ assertRegularPath(stagingPath);
35283
+ const staged = lstatSync3(stagingPath, { throwIfNoEntry: false });
35284
+ 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) {
35285
+ throw new SkillSelectionError("SESSION_WRITE_LOCKED", "The session lock has an unbound publication link; recovery requires review.");
35286
+ }
35287
+ }
35288
+ return { path, bytes, marker, sha256: sha256Hex(bytes), dev: after.dev, ino: after.ino, mtimeMs: after.mtimeMs, stagingPath };
35289
+ }
35290
+ function publishSessionLock(path, marker) {
35291
+ const stagingPath = `${path}.${marker.operationId}.prepared`;
35292
+ const bytes = Buffer.from(`${JSON.stringify({ ...marker, publication: "hard-link-v1" })}
35293
+ `);
35294
+ const fd = openSync5(stagingPath, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
35295
+ const stat = fstatSync5(fd);
35296
+ let published = false, handedOff = false;
35297
+ try {
35298
+ writeFileSync2(fd, bytes);
35299
+ fsyncSync(fd);
35300
+ linkSync(stagingPath, path);
35301
+ published = true;
35302
+ syncDirectory(dirname3(path));
35303
+ const staged = lstatSync3(stagingPath);
35304
+ if (!staged.isFile() || staged.dev !== stat.dev || staged.ino !== stat.ino) {
35305
+ throw new SkillSelectionError("SESSION_WRITE_LOCK_CHANGED", "The initialized lock staging inode changed before publication completed.");
35306
+ }
35307
+ unlinkSync(stagingPath);
35308
+ syncDirectory(dirname3(path));
35309
+ handedOff = true;
35310
+ return { path, stagingPath, fd, dev: stat.dev, ino: stat.ino };
35311
+ } finally {
35312
+ if (!handedOff) {
35313
+ try {
35314
+ for (const target of [...published ? [path] : [], stagingPath]) {
35315
+ const current = lstatSync3(target, { throwIfNoEntry: false });
35316
+ if (current?.isFile() && current.dev === stat.dev && current.ino === stat.ino)
35317
+ unlinkSync(target);
35318
+ }
35319
+ } finally {
35320
+ closeSync5(fd);
35321
+ }
35322
+ }
35323
+ }
35280
35324
  }
35281
35325
  function writerIsGone(lock, explicitLocalHost) {
35282
35326
  if (Date.now() - lock.mtimeMs < SESSION_LOCK_MIN_AGE_MS)
@@ -35338,10 +35382,21 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
35338
35382
  try {
35339
35383
  renameSync(lock.path, archivePath);
35340
35384
  moved = true;
35385
+ const archivedPaths = [archivePath];
35386
+ if (finalLock.stagingPath) {
35387
+ const publicationPath = join7(directory, "publication.write-lock");
35388
+ renameSync(finalLock.stagingPath, publicationPath);
35389
+ archivedPaths.push(publicationPath);
35390
+ }
35341
35391
  syncDirectory(directory);
35342
35392
  syncDirectory(dirname3(lock.path));
35343
- if (sha256Hex(readRegularFile(archivePath, SESSION_LOCK_MAX_BYTES)) !== lock.sha256) {
35344
- throw new Error("Archived lock bytes changed");
35393
+ for (const path of archivedPaths) {
35394
+ const before = lstatSync3(path, { throwIfNoEntry: false });
35395
+ const bytes = readRegularFile(path, SESSION_LOCK_MAX_BYTES);
35396
+ const after = lstatSync3(path, { throwIfNoEntry: false });
35397
+ 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) {
35398
+ throw new Error("Archived lock ownership or bytes changed");
35399
+ }
35345
35400
  }
35346
35401
  } catch (error) {
35347
35402
  if (moved)
@@ -35358,6 +35413,7 @@ function archiveSessionLock(lock, receipt, explicitLocalHost) {
35358
35413
  }
35359
35414
  function withSessionWriteLocks(sessionIds, options, action) {
35360
35415
  const operationId = randomUUID();
35416
+ const identity = localLockIdentity();
35361
35417
  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);
35362
35418
  const owned = [];
35363
35419
  const assertOwned = () => {
@@ -35371,9 +35427,10 @@ function withSessionWriteLocks(sessionIds, options, action) {
35371
35427
  try {
35372
35428
  for (const lock of locks) {
35373
35429
  assertRegularPath(lock.path, true);
35374
- let fd;
35430
+ const marker = { schemaVersion: identity ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity };
35431
+ let acquired;
35375
35432
  try {
35376
- fd = openSync5(lock.path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
35433
+ acquired = publishSessionLock(lock.path, marker);
35377
35434
  } catch (error) {
35378
35435
  if (error.code !== "EEXIST")
35379
35436
  throw error;
@@ -35388,7 +35445,7 @@ function withSessionWriteLocks(sessionIds, options, action) {
35388
35445
  throw recoveryError;
35389
35446
  }
35390
35447
  try {
35391
- fd = openSync5(lock.path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
35448
+ acquired = publishSessionLock(lock.path, marker);
35392
35449
  } catch (retryError) {
35393
35450
  if (retryError.code === "EEXIST") {
35394
35451
  if (!readSessionLock(lock.sessionId, options))
@@ -35398,19 +35455,20 @@ function withSessionWriteLocks(sessionIds, options, action) {
35398
35455
  throw retryError;
35399
35456
  }
35400
35457
  }
35401
- const stat = fstatSync5(fd);
35402
- owned.push({ path: lock.path, fd, dev: stat.dev, ino: stat.ino });
35403
- const identity = localLockIdentity();
35404
- writeFileSync2(fd, `${JSON.stringify({ schemaVersion: identity ? 2 : 1, operationId, sessionId: lock.sessionId, pid: process.pid, ...identity })}
35405
- `);
35458
+ owned.push(acquired);
35406
35459
  }
35407
35460
  return action(assertOwned);
35408
35461
  } finally {
35409
35462
  for (const lock of owned.reverse()) {
35410
- closeSync5(lock.fd);
35411
- const current = lstatSync3(lock.path, { throwIfNoEntry: false });
35412
- if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
35413
- unlinkSync(lock.path);
35463
+ try {
35464
+ for (const path of [lock.path, lock.stagingPath]) {
35465
+ const current = lstatSync3(path, { throwIfNoEntry: false });
35466
+ if (current?.isFile() && current.dev === lock.dev && current.ino === lock.ino)
35467
+ unlinkSync(path);
35468
+ }
35469
+ } finally {
35470
+ closeSync5(lock.fd);
35471
+ }
35414
35472
  }
35415
35473
  }
35416
35474
  }
@@ -39115,7 +39173,7 @@ var NATIVE_SKILL_ROOTS = [
39115
39173
  // src/lib/agent-integration.ts
39116
39174
  import { createHash as createHash18, randomUUID as randomUUID3 } from "crypto";
39117
39175
  import { isDeepStrictEqual as isDeepStrictEqual6 } from "util";
39118
- import { existsSync as existsSync7, lstatSync as lstatSync20, statSync as statSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync14, readdirSync as readdirSync8, opendirSync as opendirSync5, readlinkSync as readlinkSync4, realpathSync as realpathSync8, renameSync as renameSync3, rmdirSync as rmdirSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4, chmodSync, openSync as openSync16, closeSync as closeSync16, fsyncSync as fsyncSync4, fstatSync as fstatSync16, readSync as readSync11, constants as constants12, linkSync as linkSync2 } from "fs";
39176
+ import { existsSync as existsSync7, lstatSync as lstatSync20, statSync as statSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync14, readdirSync as readdirSync8, opendirSync as opendirSync5, readlinkSync as readlinkSync4, realpathSync as realpathSync8, renameSync as renameSync3, rmdirSync as rmdirSync3, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4, chmodSync, openSync as openSync16, closeSync as closeSync16, fsyncSync as fsyncSync4, fstatSync as fstatSync16, readSync as readSync11, constants as constants12, linkSync as linkSync3 } from "fs";
39119
39177
  import { dirname as dirname15, join as join23, resolve as resolve16, relative as relative4, isAbsolute as isAbsolute17, sep as sep5 } from "path";
39120
39178
  import { homedir as homedir3 } from "os";
39121
39179
 
@@ -40116,7 +40174,7 @@ function buildPluginProjection(entries) {
40116
40174
 
40117
40175
  // src/lib/plugin-projection-store.ts
40118
40176
  import { createHash as createHash13, randomUUID as randomUUID2 } from "crypto";
40119
- import { constants as constants8, closeSync as closeSync12, fchmodSync, fstatSync as fstatSync12, fsyncSync as fsyncSync2, linkSync, lstatSync as lstatSync13, mkdirSync as mkdirSync4, openSync as openSync12, opendirSync as opendirSync3, readSync as readSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, rmSync, unlinkSync as unlinkSync3, writeSync as writeSync2 } from "fs";
40177
+ import { constants as constants8, closeSync as closeSync12, fchmodSync, fstatSync as fstatSync12, fsyncSync as fsyncSync2, linkSync as linkSync2, lstatSync as lstatSync13, mkdirSync as mkdirSync4, openSync as openSync12, opendirSync as opendirSync3, readSync as readSync7, renameSync as renameSync2, rmdirSync as rmdirSync2, rmSync, unlinkSync as unlinkSync3, writeSync as writeSync2 } from "fs";
40120
40178
  import { dirname as dirname10, isAbsolute as isAbsolute11, join as join16, parse as parse5, resolve as resolve10 } from "path";
40121
40179
  var READ_FLAGS = constants8.O_RDONLY | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
40122
40180
  var WRITE_FLAGS = constants8.O_WRONLY | constants8.O_CREAT | constants8.O_EXCL | constants8.O_NOFOLLOW | constants8.O_NONBLOCK;
@@ -40432,7 +40490,7 @@ function writePluginJsonImmutable(path, value) {
40432
40490
  written = stat(temporary);
40433
40491
  verifyAncestors(chain, true);
40434
40492
  try {
40435
- linkSync(temporary, path);
40493
+ linkSync2(temporary, path);
40436
40494
  } catch (error) {
40437
40495
  if (!(error && typeof error === "object" && ("code" in error) && error.code === "EEXIST"))
40438
40496
  throw error;
@@ -42076,13 +42134,13 @@ function resolveAgentDiscovery(options) {
42076
42134
  unresolved("JSONC configuration requires a supported format adapter");
42077
42135
  for (const root2 of [directory, join22(home, ".claude"), join22(home, ".agents")])
42078
42136
  for (const name of ["skill", "skills"])
42079
- roots.add(join22(root2, name));
42137
+ roots.add(canonical6(join22(root2, name)));
42080
42138
  if (config.skills !== undefined && (!Array.isArray(config.skills) || config.skills.some((path) => typeof path !== "string")))
42081
42139
  unresolved("skills must be a string array");
42082
42140
  for (const value of config.skills ?? []) {
42083
42141
  if (typeof value !== "string" || /[\0$]/.test(value) || !value || !(isAbsolute16(value) || value.startsWith("~/")))
42084
42142
  unresolved("remote or relative skill sources need a dedicated discovery adapter");
42085
- roots.add(value.startsWith("~/") ? join22(home, value.slice(2)) : value);
42143
+ roots.add(canonical6(value.startsWith("~/") ? join22(home, value.slice(2)) : value));
42086
42144
  }
42087
42145
  for (const key of ["plugin", "plugins"])
42088
42146
  if (config[key] !== undefined && !Array.isArray(config[key]))
@@ -43720,7 +43778,7 @@ function archiveNativeSkills(inventory, options) {
43720
43778
  continue;
43721
43779
  }
43722
43780
  if (entry.discoveryOnly) {
43723
- linkSync2(entry.archive, entry.source);
43781
+ linkSync3(entry.archive, entry.source);
43724
43782
  syncArchiveDirectory(dirname15(entry.source));
43725
43783
  unlinkSync4(entry.archive);
43726
43784
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.31",
3
+ "version": "0.10.34",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {