@delorenj/skillex 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -8620,7 +8620,7 @@ async function buildScope(scope, options, forbiddenRoots, verifySet = verifySele
8620
8620
  if (!owned4)
8621
8621
  refuse3(root, "Whole-root switching requires explicit ownership of the existing root.");
8622
8622
  if (owned4.kind === "link") return { identity: owned4 };
8623
- const children = {};
8623
+ const children2 = {};
8624
8624
  for (const name of awaitableNames) {
8625
8625
  const child = links[join25(root, name)];
8626
8626
  if (!child)
@@ -8628,9 +8628,9 @@ async function buildScope(scope, options, forbiddenRoots, verifySet = verifySele
8628
8628
  join25(root, name),
8629
8629
  "Foreign content prevents a whole-root switch; it will not be removed."
8630
8630
  );
8631
- children[name] = child;
8631
+ children2[name] = child;
8632
8632
  }
8633
- return { identity: owned4, children };
8633
+ return { identity: owned4, children: children2 };
8634
8634
  };
8635
8635
  const awaitableNames = rootEntry?.info.isDirectory() ? await readdir16(physicalPath(root, recovery)) : [];
8636
8636
  if (!rootEntry) {
@@ -8969,7 +8969,7 @@ async function publishPrepared(work, staged, destination, node, directories) {
8969
8969
  async function stageNode(work, operation2, path) {
8970
8970
  if (operation2.children !== void 0) {
8971
8971
  await mkdir7(path, { mode: 493 });
8972
- const children = {};
8972
+ const children2 = {};
8973
8973
  for (const [name, target2] of operation2.children) {
8974
8974
  const destination = join25(operation2.path, name);
8975
8975
  if (!await canonicalTarget(work, name, target2))
@@ -8978,11 +8978,11 @@ async function stageNode(work, operation2, path) {
8978
8978
  await symlink4(relative9(dirname13(destination), target2), child);
8979
8979
  const found2 = await entry2(child);
8980
8980
  if (!found2) refuse3(child, "Prepared activation link disappeared.");
8981
- children[name] = identity(found2);
8981
+ children2[name] = identity(found2);
8982
8982
  }
8983
8983
  const found = await entry2(path);
8984
8984
  if (!found) refuse3(path, "Prepared activation directory disappeared.");
8985
- return { identity: identity(found), children };
8985
+ return { identity: identity(found), children: children2 };
8986
8986
  }
8987
8987
  if (operation2.target !== void 0) {
8988
8988
  if (dirname13(operation2.path) === work.plan.activationRoot && !await canonicalTarget(work, basename5(operation2.path), operation2.target))
@@ -11417,7 +11417,7 @@ async function captureMigrationNode(path) {
11417
11417
  mode: Number(before.mode & 0o7777n),
11418
11418
  ...before.isSymbolicLink() ? { raw: await readlink6(path) } : {}
11419
11419
  };
11420
- const children = {};
11420
+ const children2 = {};
11421
11421
  let hash5 = null;
11422
11422
  if (before.isDirectory()) {
11423
11423
  const names2 = await readdir22(path, { encoding: "buffer" });
@@ -11434,7 +11434,7 @@ async function captureMigrationNode(path) {
11434
11434
  const child = await captureMigrationNode(join31(path, name));
11435
11435
  if (!child)
11436
11436
  migrationRefusal(join31(path, name), "Content disappeared during migration inventory.");
11437
- Object.defineProperty(children, name, { value: child, enumerable: true });
11437
+ Object.defineProperty(children2, name, { value: child, enumerable: true });
11438
11438
  }
11439
11439
  } else if (before.isFile()) {
11440
11440
  const handle = await open10(
@@ -11473,7 +11473,7 @@ async function captureMigrationNode(path) {
11473
11473
  const after = await lstat20(path, { bigint: true });
11474
11474
  if (after.dev !== before.dev || after.ino !== before.ino || after.mode !== before.mode || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs)
11475
11475
  migrationRefusal(path, "Content changed while its migration inventory was being captured.");
11476
- return { identity: identity5, hash: hash5, children };
11476
+ return { identity: identity5, hash: hash5, children: children2 };
11477
11477
  }
11478
11478
  function migrationDigest(node) {
11479
11479
  const project = (item2) => ({
@@ -12574,16 +12574,16 @@ async function prepare2(target2) {
12574
12574
  shared,
12575
12575
  "The old alias points at one skill definition rather than a collection root."
12576
12576
  );
12577
- const children = {};
12577
+ const children2 = {};
12578
12578
  const claimNames = [];
12579
12579
  const proofs = [{ path: shared, node: contents }];
12580
12580
  for (const name of Object.keys(contents.children)) {
12581
12581
  const path = join34(target2.root, name);
12582
12582
  const mapped2 = await canonicalMapping(target2, path);
12583
12583
  const output2 = mapped2?.name ?? name;
12584
- if (Object.hasOwn(children, output2))
12584
+ if (Object.hasOwn(children2, output2))
12585
12585
  migrationRefusal(path, "Two original children map to the same resulting name.");
12586
- Object.defineProperty(children, output2, {
12586
+ Object.defineProperty(children2, output2, {
12587
12587
  value: mapped2?.target ?? join34(shared, name),
12588
12588
  enumerable: true
12589
12589
  });
@@ -12598,7 +12598,7 @@ async function prepare2(target2) {
12598
12598
  [
12599
12599
  mapped2 ? "The mapped canonical content is verified." : "The original child remains reachable through a foreign forwarding link; its shared target is untouched."
12600
12600
  ],
12601
- children[output2]
12601
+ children2[output2]
12602
12602
  )
12603
12603
  );
12604
12604
  }
@@ -12607,7 +12607,7 @@ async function prepare2(target2) {
12607
12607
  "Replace only the old root symlink with a real directory; preserve every inventoried child and leave the shared target untouched."
12608
12608
  ]),
12609
12609
  previous: root,
12610
- children,
12610
+ children: children2,
12611
12611
  claimRoot: true,
12612
12612
  claimNames,
12613
12613
  proofs
@@ -18229,14 +18229,14 @@ async function normalizeScope(options = {}) {
18229
18229
  // src/core/profile-sync.ts
18230
18230
  import { randomUUID as randomUUID11 } from "node:crypto";
18231
18231
  import { constants as constants13 } from "node:fs";
18232
- import { mkdir as mkdir12, open as open17, readdir as readdir29, realpath as realpath32, rename as rename11, symlink as symlink7, unlink as unlink12 } from "node:fs/promises";
18232
+ import { mkdir as mkdir12, open as open17, readdir as readdir30, realpath as realpath32, rename as rename11, symlink as symlink7, unlink as unlink12 } from "node:fs/promises";
18233
18233
  import { dirname as dirname27, join as join44, resolve as resolve25 } from "node:path";
18234
18234
  import { isDeepStrictEqual as isDeepStrictEqual10 } from "node:util";
18235
18235
 
18236
18236
  // src/core/profile-policy.ts
18237
18237
  import { constants as constants12 } from "node:fs";
18238
- import { open as open16, readFile as readFile18, stat as stat8 } from "node:fs/promises";
18239
- import { join as join43 } from "node:path";
18238
+ import { open as open16, readdir as readdir29, readFile as readFile18, stat as stat8 } from "node:fs/promises";
18239
+ import { basename as basename13, join as join43 } from "node:path";
18240
18240
  import { parseDocument as parseDocument4 } from "yaml";
18241
18241
  var metadataFiles = /* @__PURE__ */ new Set([
18242
18242
  ".usage.json",
@@ -18248,9 +18248,48 @@ var metadataFiles = /* @__PURE__ */ new Set([
18248
18248
  ".curator_ledger.jsonl"
18249
18249
  ]);
18250
18250
  var metadataDirectories = /* @__PURE__ */ new Set([".curator_backups", ".locks"]);
18251
- function hermesMetadata(item2) {
18251
+ var hubFiles = /* @__PURE__ */ new Set(["audit.log", "taps.json", "lock.json"]);
18252
+ async function children(path) {
18253
+ try {
18254
+ return await readdir29(path, { withFileTypes: true });
18255
+ } catch {
18256
+ return null;
18257
+ }
18258
+ }
18259
+ async function categoryStub(item2) {
18260
+ if (item2.kind !== "directory" || item2.shadows) return false;
18261
+ const entries = await children(item2.path);
18262
+ return entries?.length === 1 && entries[0]?.name === "DESCRIPTION.md" && entries[0].isFile();
18263
+ }
18264
+ async function idleHub(item2) {
18265
+ if (item2.kind !== "directory" || item2.name !== ".hub") return false;
18266
+ const entries = await children(item2.path);
18267
+ if (!entries) return false;
18268
+ for (const child of entries) {
18269
+ const path = join43(item2.path, child.name);
18270
+ if (child.isFile() && hubFiles.has(child.name)) {
18271
+ if (child.name !== "lock.json") continue;
18272
+ try {
18273
+ const lock = JSON.parse(await readFile18(path, "utf8"));
18274
+ const installed = lock?.installed;
18275
+ if (!installed || typeof installed !== "object" || Object.keys(installed).length)
18276
+ return false;
18277
+ } catch {
18278
+ return false;
18279
+ }
18280
+ } else if (child.isDirectory() && child.name === "quarantine") {
18281
+ if ((await children(path))?.length !== 0) return false;
18282
+ } else if (child.isDirectory() && child.name === "index-cache") {
18283
+ const cached2 = await children(path);
18284
+ if (!cached2 || cached2.some((entry3) => !entry3.isFile())) return false;
18285
+ } else return false;
18286
+ }
18287
+ return true;
18288
+ }
18289
+ async function hermesMetadata(item2) {
18252
18290
  if (item2.kind === "file") return metadataFiles.has(item2.name);
18253
- if (item2.kind === "directory") return metadataDirectories.has(item2.name);
18291
+ if (item2.kind === "directory")
18292
+ return metadataDirectories.has(item2.name) || await idleHub(item2) || await categoryStub(item2);
18254
18293
  return false;
18255
18294
  }
18256
18295
  function refuse5(path, message) {
@@ -18285,11 +18324,38 @@ async function profileInheritsGlobal(profile) {
18285
18324
  refuse5(path, "Profile skills.inherit_global must be boolean.");
18286
18325
  return inherit !== false;
18287
18326
  }
18288
- async function assertStrictProfile(profile, options, local) {
18327
+ var learnedScanLimit = 2e4;
18328
+ async function learnedSkills(item2) {
18329
+ if (item2.kind !== "directory" || item2.name.startsWith(".")) return null;
18330
+ const skills = [];
18331
+ const pending = [item2.path];
18332
+ let seen = 0;
18333
+ while (pending.length) {
18334
+ const directory2 = pending.pop();
18335
+ const entries = await children(directory2);
18336
+ if (!entries) return null;
18337
+ for (const child of entries) {
18338
+ if (++seen > learnedScanLimit) return null;
18339
+ if (child.isDirectory()) pending.push(join43(directory2, child.name));
18340
+ else if (!child.isFile()) return null;
18341
+ else if (child.name === "SKILL.md") skills.push(basename13(directory2));
18342
+ }
18343
+ }
18344
+ return skills;
18345
+ }
18346
+ async function assertStrictProfile(profile, options, local, desired = /* @__PURE__ */ new Set()) {
18289
18347
  if (!await strictProfile(profile, options)) return false;
18290
- for (const item2 of local)
18291
- if (!hermesMetadata(item2))
18292
- refuse5(item2.path, `Skillex-only profile contains unowned entry ${item2.name}.`);
18348
+ for (const item2 of local) {
18349
+ if (await hermesMetadata(item2)) continue;
18350
+ const learned = await learnedSkills(item2);
18351
+ if (!learned) refuse5(item2.path, `Skillex-only profile contains unowned entry ${item2.name}.`);
18352
+ const clash = [item2.name, ...learned].find((name) => desired.has(name));
18353
+ if (clash !== void 0)
18354
+ refuse5(
18355
+ item2.path,
18356
+ `Agent-written skill ${clash} in ${item2.name} shares its name with a selected skill.`
18357
+ );
18358
+ }
18293
18359
  const path = join43(profile.root, "config.yaml");
18294
18360
  const config = await entry2(path);
18295
18361
  if (config) {
@@ -18461,7 +18527,7 @@ async function observations(profile, state) {
18461
18527
  "Use the explicit migrate workflow to replace this whole-root alias or conflicting file while preserving profile content."
18462
18528
  );
18463
18529
  if (root)
18464
- for (const name of (await readdir29(profile.skillsRoot)).sort()) {
18530
+ for (const name of (await readdir30(profile.skillsRoot)).sort()) {
18465
18531
  const path = join44(profile.skillsRoot, name);
18466
18532
  const actual = await entry2(path);
18467
18533
  if (!actual) continue;
@@ -18534,7 +18600,12 @@ async function buildWork(profile, state, options) {
18534
18600
  );
18535
18601
  state = await readProfileState(profile, receiptOptions(profile, options, projection.scopes));
18536
18602
  const observed = await observations(profile, state);
18537
- await assertStrictProfile(profile, options, observed.local);
18603
+ await assertStrictProfile(
18604
+ profile,
18605
+ options,
18606
+ observed.local,
18607
+ new Set(projection.desired.map((desired) => desired.name))
18608
+ );
18538
18609
  const links = { ...state.data.links };
18539
18610
  const changes = [];
18540
18611
  if (!observed.root) changes.push({ action: "mkdir", path: profile.skillsRoot });
@@ -19010,7 +19081,12 @@ async function runProfileSync(name, options) {
19010
19081
  });
19011
19082
  }
19012
19083
  await assertProfileIdentity(selected);
19013
- if (await assertStrictProfile(selected, options, (await observations(selected, state)).local))
19084
+ if (await assertStrictProfile(
19085
+ selected,
19086
+ options,
19087
+ (await observations(selected, state)).local,
19088
+ new Set(work.projection.desired.map((desired) => desired.name))
19089
+ ))
19014
19090
  await publishStrictPolicy(selected);
19015
19091
  const currentProfile = await discoverProfile(name, options);
19016
19092
  if (currentProfile.root !== selected.root || currentProfile.skillsRoot !== selected.skillsRoot)
@@ -19652,7 +19728,7 @@ import { randomUUID as randomUUID13 } from "node:crypto";
19652
19728
  import { constants as constants14 } from "node:fs";
19653
19729
  import { mkdir as mkdir13, open as open19, realpath as realpath34, rename as rename13, symlink as symlink8 } from "node:fs/promises";
19654
19730
  import { homedir as homedir16 } from "node:os";
19655
- import { basename as basename13, dirname as dirname29, join as join46, parse as parse6, resolve as resolve27 } from "node:path";
19731
+ import { basename as basename14, dirname as dirname29, join as join46, parse as parse6, resolve as resolve27 } from "node:path";
19656
19732
  import { isDeepStrictEqual as isDeepStrictEqual12 } from "node:util";
19657
19733
  import { valid as valid3 } from "semver";
19658
19734
  var binding5 = { namespace: "migrations", targetParts: [".agents", "root-adoption"] };
@@ -19852,7 +19928,7 @@ async function guard2(context2, journal) {
19852
19928
  function paths2(snapshot3, id) {
19853
19929
  const prefix2 = join46(
19854
19930
  dirname29(snapshot3.path),
19855
- `${basename13(snapshot3.path, ".json")}.root-cutover-${id}`
19931
+ `${basename14(snapshot3.path, ".json")}.root-cutover-${id}`
19856
19932
  );
19857
19933
  return { archive: `${prefix2}-old`, stage: `${prefix2}-new` };
19858
19934
  }
@@ -19964,8 +20040,8 @@ function journalData(snapshot3, context2) {
19964
20040
  )))
19965
20041
  refuse7(snapshot3.path, "The staged root does not match its canonical inventory.");
19966
20042
  for (const stage2 of raw.preservedStages) {
19967
- const prefix2 = `${basename13(snapshot3.path, ".json")}.root-cutover-`;
19968
- if (typeof stage2 !== "string" || dirname29(stage2) !== dirname29(snapshot3.path) || !basename13(stage2).startsWith(prefix2) || !uuid.test(basename13(stage2).slice(prefix2.length, -4)) || !stage2.endsWith("-new"))
20043
+ const prefix2 = `${basename14(snapshot3.path, ".json")}.root-cutover-`;
20044
+ if (typeof stage2 !== "string" || dirname29(stage2) !== dirname29(snapshot3.path) || !basename14(stage2).startsWith(prefix2) || !uuid.test(basename14(stage2).slice(prefix2.length, -4)) || !stage2.endsWith("-new"))
19969
20045
  refuse7(snapshot3.path, "A preserved stage has an invalid quarantine path.");
19970
20046
  }
19971
20047
  const data2 = raw;
@@ -20006,7 +20082,7 @@ async function ownerFor2(context2, allowed) {
20006
20082
  ]) {
20007
20083
  if (allowed && (path === context2.root || dirname29(path) === context2.root)) {
20008
20084
  const candidates = allowed.map(
20009
- (node) => path === context2.root ? node : node.children[basename13(path)]
20085
+ (node) => path === context2.root ? node : node.children[basename14(path)]
20010
20086
  );
20011
20087
  if (!candidates.some(
20012
20088
  (node) => node && ["link", "directory"].includes(node.identity.kind) && isDeepStrictEqual12(nodeIdentity(node), expected)
@@ -20669,7 +20745,7 @@ async function adoptRoot(options = {}) {
20669
20745
  }
20670
20746
 
20671
20747
  // src/core/sets.ts
20672
- import { readdir as readdir30 } from "node:fs/promises";
20748
+ import { readdir as readdir31 } from "node:fs/promises";
20673
20749
  import { join as join47 } from "node:path";
20674
20750
  function validateName(name) {
20675
20751
  parseManifest({ sets: [{ name }] }, "set name");
@@ -20692,7 +20768,7 @@ async function listSets(options = {}) {
20692
20768
  const failures = [];
20693
20769
  if (await inspectPath(path)) {
20694
20770
  await requireDirectory(path, "E_SET_MISSING");
20695
- for (const entry3 of (await readdir30(path, { withFileTypes: true })).sort(
20771
+ for (const entry3 of (await readdir31(path, { withFileTypes: true })).sort(
20696
20772
  (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
20697
20773
  )) {
20698
20774
  if (entry3.name.startsWith(".") || entry3.name.startsWith("_") || !entry3.isDirectory() && !entry3.isSymbolicLink())
@@ -20834,7 +20910,7 @@ async function removeSetSkills(name, names2, options = {}) {
20834
20910
  }
20835
20911
 
20836
20912
  // src/core/vendor-inspect.ts
20837
- import { readdir as readdir31 } from "node:fs/promises";
20913
+ import { readdir as readdir32 } from "node:fs/promises";
20838
20914
  import { join as join48 } from "node:path";
20839
20915
 
20840
20916
  // src/core/vendor-git.ts
@@ -21126,8 +21202,8 @@ async function readVendorGitSource(source, checkoutRoot, options = {}) {
21126
21202
  checkout
21127
21203
  ).trim();
21128
21204
  const base = await treeAt(checkout, rootTree, source.subdir, options);
21129
- const children = await treeEntries(checkout, base, options);
21130
- const declarations = source.membership === "explicit" ? source.skills : children.filter(
21205
+ const children2 = await treeEntries(checkout, base, options);
21206
+ const declarations = source.membership === "explicit" ? source.skills : children2.filter(
21131
21207
  (entry3) => !entry3.name.startsWith(".") && !entry3.name.startsWith("_") && (entry3.type === "tree" || entry3.mode === "120000" || entry3.mode === "160000")
21132
21208
  ).map((entry3) => ({ name: entry3.name, dir: entry3.name }));
21133
21209
  const selected = declarations.filter(
@@ -21199,7 +21275,7 @@ async function recordedInventory(manifest, observation, options) {
21199
21275
  const recorded = /* @__PURE__ */ new Map();
21200
21276
  const failed2 = /* @__PURE__ */ new Set();
21201
21277
  const catalog = join48(manifest.registry.root, "all-skills");
21202
- for (const entry3 of (await readdir31(catalog, { withFileTypes: true })).sort(
21278
+ for (const entry3 of (await readdir32(catalog, { withFileTypes: true })).sort(
21203
21279
  (a, b) => a.name.localeCompare(b.name)
21204
21280
  )) {
21205
21281
  checkVendorSignal(options);
@@ -21672,7 +21748,7 @@ async function inspectVendorStatus(options = {}) {
21672
21748
 
21673
21749
  // src/core/vendor-sync.ts
21674
21750
  import { randomUUID as randomUUID14 } from "node:crypto";
21675
- import { chmod as chmod4, lstat as lstat27, mkdir as mkdir14, open as open20, readdir as readdir32, rename as rename14 } from "node:fs/promises";
21751
+ import { chmod as chmod4, lstat as lstat27, mkdir as mkdir14, open as open20, readdir as readdir33, rename as rename14 } from "node:fs/promises";
21676
21752
  import { dirname as dirname30, join as join49 } from "node:path";
21677
21753
  function refuse8(code, message, path, fix) {
21678
21754
  fail(code, message, { path, fix }, ExitCode.REFUSED);
@@ -21899,7 +21975,7 @@ async function prepare6(options, extractedAt) {
21899
21975
  }
21900
21976
  }
21901
21977
  if (options.prune) {
21902
- for (const name of (await readdir32(catalog)).filter(isSkillName).sort()) {
21978
+ for (const name of (await readdir33(catalog)).filter(isSkillName).sort()) {
21903
21979
  if (names2.has(name)) continue;
21904
21980
  const path = join49(catalog, name);
21905
21981
  if (!(await vendorLstat(path))?.isDirectory()) continue;
@@ -2,5 +2,11 @@ import type { ProfileLocalEntry, ProfileLocation, ProfileOptions } from "./profi
2
2
  export declare function strictProfile(profile: ProfileLocation, options: ProfileOptions): Promise<boolean>;
3
3
  /** Persist a profile's selection boundary independently of the caller's HOME. */
4
4
  export declare function profileInheritsGlobal(profile: ProfileLocation): Promise<boolean>;
5
- export declare function assertStrictProfile(profile: ProfileLocation, options: ProfileOptions, local: readonly ProfileLocalEntry[]): Promise<boolean>;
5
+ /**
6
+ * Strict (Skillex-only) policy: Skillex owns every link it projects. Hermes bookkeeping and the
7
+ * skills a desk's own agent wrote may sit beside those links (operator decision 2026-11-06,
8
+ * SKRILL-35); a learned skill named like a selected one, a foreign link, a stray file, an
9
+ * archive or a hub install is still refused. `desired` names the skills Skillex will project.
10
+ */
11
+ export declare function assertStrictProfile(profile: ProfileLocation, options: ProfileOptions, local: readonly ProfileLocalEntry[], desired?: ReadonlySet<string>): Promise<boolean>;
6
12
  export declare function publishStrictPolicy(profile: ProfileLocation): Promise<void>;
package/dist/index.js CHANGED
@@ -8618,7 +8618,7 @@ async function buildScope(scope, options, forbiddenRoots, verifySet = verifySele
8618
8618
  if (!owned4)
8619
8619
  refuse3(root, "Whole-root switching requires explicit ownership of the existing root.");
8620
8620
  if (owned4.kind === "link") return { identity: owned4 };
8621
- const children = {};
8621
+ const children2 = {};
8622
8622
  for (const name of awaitableNames) {
8623
8623
  const child = links[join25(root, name)];
8624
8624
  if (!child)
@@ -8626,9 +8626,9 @@ async function buildScope(scope, options, forbiddenRoots, verifySet = verifySele
8626
8626
  join25(root, name),
8627
8627
  "Foreign content prevents a whole-root switch; it will not be removed."
8628
8628
  );
8629
- children[name] = child;
8629
+ children2[name] = child;
8630
8630
  }
8631
- return { identity: owned4, children };
8631
+ return { identity: owned4, children: children2 };
8632
8632
  };
8633
8633
  const awaitableNames = rootEntry?.info.isDirectory() ? await readdir16(physicalPath(root, recovery)) : [];
8634
8634
  if (!rootEntry) {
@@ -8967,7 +8967,7 @@ async function publishPrepared(work, staged, destination, node, directories) {
8967
8967
  async function stageNode(work, operation2, path) {
8968
8968
  if (operation2.children !== void 0) {
8969
8969
  await mkdir7(path, { mode: 493 });
8970
- const children = {};
8970
+ const children2 = {};
8971
8971
  for (const [name, target2] of operation2.children) {
8972
8972
  const destination = join25(operation2.path, name);
8973
8973
  if (!await canonicalTarget(work, name, target2))
@@ -8976,11 +8976,11 @@ async function stageNode(work, operation2, path) {
8976
8976
  await symlink4(relative9(dirname13(destination), target2), child);
8977
8977
  const found2 = await entry2(child);
8978
8978
  if (!found2) refuse3(child, "Prepared activation link disappeared.");
8979
- children[name] = identity(found2);
8979
+ children2[name] = identity(found2);
8980
8980
  }
8981
8981
  const found = await entry2(path);
8982
8982
  if (!found) refuse3(path, "Prepared activation directory disappeared.");
8983
- return { identity: identity(found), children };
8983
+ return { identity: identity(found), children: children2 };
8984
8984
  }
8985
8985
  if (operation2.target !== void 0) {
8986
8986
  if (dirname13(operation2.path) === work.plan.activationRoot && !await canonicalTarget(work, basename5(operation2.path), operation2.target))
@@ -11415,7 +11415,7 @@ async function captureMigrationNode(path) {
11415
11415
  mode: Number(before.mode & 0o7777n),
11416
11416
  ...before.isSymbolicLink() ? { raw: await readlink6(path) } : {}
11417
11417
  };
11418
- const children = {};
11418
+ const children2 = {};
11419
11419
  let hash5 = null;
11420
11420
  if (before.isDirectory()) {
11421
11421
  const names2 = await readdir22(path, { encoding: "buffer" });
@@ -11432,7 +11432,7 @@ async function captureMigrationNode(path) {
11432
11432
  const child = await captureMigrationNode(join31(path, name));
11433
11433
  if (!child)
11434
11434
  migrationRefusal(join31(path, name), "Content disappeared during migration inventory.");
11435
- Object.defineProperty(children, name, { value: child, enumerable: true });
11435
+ Object.defineProperty(children2, name, { value: child, enumerable: true });
11436
11436
  }
11437
11437
  } else if (before.isFile()) {
11438
11438
  const handle = await open10(
@@ -11471,7 +11471,7 @@ async function captureMigrationNode(path) {
11471
11471
  const after = await lstat20(path, { bigint: true });
11472
11472
  if (after.dev !== before.dev || after.ino !== before.ino || after.mode !== before.mode || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs)
11473
11473
  migrationRefusal(path, "Content changed while its migration inventory was being captured.");
11474
- return { identity: identity4, hash: hash5, children };
11474
+ return { identity: identity4, hash: hash5, children: children2 };
11475
11475
  }
11476
11476
  function migrationDigest(node) {
11477
11477
  const project = (item2) => ({
@@ -12572,16 +12572,16 @@ async function prepare2(target2) {
12572
12572
  shared,
12573
12573
  "The old alias points at one skill definition rather than a collection root."
12574
12574
  );
12575
- const children = {};
12575
+ const children2 = {};
12576
12576
  const claimNames = [];
12577
12577
  const proofs = [{ path: shared, node: contents }];
12578
12578
  for (const name of Object.keys(contents.children)) {
12579
12579
  const path = join34(target2.root, name);
12580
12580
  const mapped2 = await canonicalMapping(target2, path);
12581
12581
  const output = mapped2?.name ?? name;
12582
- if (Object.hasOwn(children, output))
12582
+ if (Object.hasOwn(children2, output))
12583
12583
  migrationRefusal(path, "Two original children map to the same resulting name.");
12584
- Object.defineProperty(children, output, {
12584
+ Object.defineProperty(children2, output, {
12585
12585
  value: mapped2?.target ?? join34(shared, name),
12586
12586
  enumerable: true
12587
12587
  });
@@ -12596,7 +12596,7 @@ async function prepare2(target2) {
12596
12596
  [
12597
12597
  mapped2 ? "The mapped canonical content is verified." : "The original child remains reachable through a foreign forwarding link; its shared target is untouched."
12598
12598
  ],
12599
- children[output]
12599
+ children2[output]
12600
12600
  )
12601
12601
  );
12602
12602
  }
@@ -12605,7 +12605,7 @@ async function prepare2(target2) {
12605
12605
  "Replace only the old root symlink with a real directory; preserve every inventoried child and leave the shared target untouched."
12606
12606
  ]),
12607
12607
  previous: root,
12608
- children,
12608
+ children: children2,
12609
12609
  claimRoot: true,
12610
12610
  claimNames,
12611
12611
  proofs
@@ -18227,14 +18227,14 @@ async function normalizeScope(options = {}) {
18227
18227
  // src/core/profile-sync.ts
18228
18228
  import { randomUUID as randomUUID11 } from "node:crypto";
18229
18229
  import { constants as constants13 } from "node:fs";
18230
- import { mkdir as mkdir12, open as open17, readdir as readdir29, realpath as realpath32, rename as rename11, symlink as symlink7, unlink as unlink12 } from "node:fs/promises";
18230
+ import { mkdir as mkdir12, open as open17, readdir as readdir30, realpath as realpath32, rename as rename11, symlink as symlink7, unlink as unlink12 } from "node:fs/promises";
18231
18231
  import { dirname as dirname27, join as join44, resolve as resolve25 } from "node:path";
18232
18232
  import { isDeepStrictEqual as isDeepStrictEqual10 } from "node:util";
18233
18233
 
18234
18234
  // src/core/profile-policy.ts
18235
18235
  import { constants as constants12 } from "node:fs";
18236
- import { open as open16, readFile as readFile18, stat as stat8 } from "node:fs/promises";
18237
- import { join as join43 } from "node:path";
18236
+ import { open as open16, readdir as readdir29, readFile as readFile18, stat as stat8 } from "node:fs/promises";
18237
+ import { basename as basename13, join as join43 } from "node:path";
18238
18238
  import { parseDocument as parseDocument4 } from "yaml";
18239
18239
  var metadataFiles = /* @__PURE__ */ new Set([
18240
18240
  ".usage.json",
@@ -18246,9 +18246,48 @@ var metadataFiles = /* @__PURE__ */ new Set([
18246
18246
  ".curator_ledger.jsonl"
18247
18247
  ]);
18248
18248
  var metadataDirectories = /* @__PURE__ */ new Set([".curator_backups", ".locks"]);
18249
- function hermesMetadata(item2) {
18249
+ var hubFiles = /* @__PURE__ */ new Set(["audit.log", "taps.json", "lock.json"]);
18250
+ async function children(path) {
18251
+ try {
18252
+ return await readdir29(path, { withFileTypes: true });
18253
+ } catch {
18254
+ return null;
18255
+ }
18256
+ }
18257
+ async function categoryStub(item2) {
18258
+ if (item2.kind !== "directory" || item2.shadows) return false;
18259
+ const entries = await children(item2.path);
18260
+ return entries?.length === 1 && entries[0]?.name === "DESCRIPTION.md" && entries[0].isFile();
18261
+ }
18262
+ async function idleHub(item2) {
18263
+ if (item2.kind !== "directory" || item2.name !== ".hub") return false;
18264
+ const entries = await children(item2.path);
18265
+ if (!entries) return false;
18266
+ for (const child of entries) {
18267
+ const path = join43(item2.path, child.name);
18268
+ if (child.isFile() && hubFiles.has(child.name)) {
18269
+ if (child.name !== "lock.json") continue;
18270
+ try {
18271
+ const lock = JSON.parse(await readFile18(path, "utf8"));
18272
+ const installed = lock?.installed;
18273
+ if (!installed || typeof installed !== "object" || Object.keys(installed).length)
18274
+ return false;
18275
+ } catch {
18276
+ return false;
18277
+ }
18278
+ } else if (child.isDirectory() && child.name === "quarantine") {
18279
+ if ((await children(path))?.length !== 0) return false;
18280
+ } else if (child.isDirectory() && child.name === "index-cache") {
18281
+ const cached2 = await children(path);
18282
+ if (!cached2 || cached2.some((entry3) => !entry3.isFile())) return false;
18283
+ } else return false;
18284
+ }
18285
+ return true;
18286
+ }
18287
+ async function hermesMetadata(item2) {
18250
18288
  if (item2.kind === "file") return metadataFiles.has(item2.name);
18251
- if (item2.kind === "directory") return metadataDirectories.has(item2.name);
18289
+ if (item2.kind === "directory")
18290
+ return metadataDirectories.has(item2.name) || await idleHub(item2) || await categoryStub(item2);
18252
18291
  return false;
18253
18292
  }
18254
18293
  function refuse5(path, message) {
@@ -18283,11 +18322,38 @@ async function profileInheritsGlobal(profile) {
18283
18322
  refuse5(path, "Profile skills.inherit_global must be boolean.");
18284
18323
  return inherit !== false;
18285
18324
  }
18286
- async function assertStrictProfile(profile, options, local) {
18325
+ var learnedScanLimit = 2e4;
18326
+ async function learnedSkills(item2) {
18327
+ if (item2.kind !== "directory" || item2.name.startsWith(".")) return null;
18328
+ const skills = [];
18329
+ const pending = [item2.path];
18330
+ let seen = 0;
18331
+ while (pending.length) {
18332
+ const directory2 = pending.pop();
18333
+ const entries = await children(directory2);
18334
+ if (!entries) return null;
18335
+ for (const child of entries) {
18336
+ if (++seen > learnedScanLimit) return null;
18337
+ if (child.isDirectory()) pending.push(join43(directory2, child.name));
18338
+ else if (!child.isFile()) return null;
18339
+ else if (child.name === "SKILL.md") skills.push(basename13(directory2));
18340
+ }
18341
+ }
18342
+ return skills;
18343
+ }
18344
+ async function assertStrictProfile(profile, options, local, desired = /* @__PURE__ */ new Set()) {
18287
18345
  if (!await strictProfile(profile, options)) return false;
18288
- for (const item2 of local)
18289
- if (!hermesMetadata(item2))
18290
- refuse5(item2.path, `Skillex-only profile contains unowned entry ${item2.name}.`);
18346
+ for (const item2 of local) {
18347
+ if (await hermesMetadata(item2)) continue;
18348
+ const learned = await learnedSkills(item2);
18349
+ if (!learned) refuse5(item2.path, `Skillex-only profile contains unowned entry ${item2.name}.`);
18350
+ const clash = [item2.name, ...learned].find((name) => desired.has(name));
18351
+ if (clash !== void 0)
18352
+ refuse5(
18353
+ item2.path,
18354
+ `Agent-written skill ${clash} in ${item2.name} shares its name with a selected skill.`
18355
+ );
18356
+ }
18291
18357
  const path = join43(profile.root, "config.yaml");
18292
18358
  const config = await entry2(path);
18293
18359
  if (config) {
@@ -18459,7 +18525,7 @@ async function observations(profile, state) {
18459
18525
  "Use the explicit migrate workflow to replace this whole-root alias or conflicting file while preserving profile content."
18460
18526
  );
18461
18527
  if (root)
18462
- for (const name of (await readdir29(profile.skillsRoot)).sort()) {
18528
+ for (const name of (await readdir30(profile.skillsRoot)).sort()) {
18463
18529
  const path = join44(profile.skillsRoot, name);
18464
18530
  const actual = await entry2(path);
18465
18531
  if (!actual) continue;
@@ -18532,7 +18598,12 @@ async function buildWork(profile, state, options) {
18532
18598
  );
18533
18599
  state = await readProfileState(profile, receiptOptions(profile, options, projection.scopes));
18534
18600
  const observed = await observations(profile, state);
18535
- await assertStrictProfile(profile, options, observed.local);
18601
+ await assertStrictProfile(
18602
+ profile,
18603
+ options,
18604
+ observed.local,
18605
+ new Set(projection.desired.map((desired) => desired.name))
18606
+ );
18536
18607
  const links = { ...state.data.links };
18537
18608
  const changes = [];
18538
18609
  if (!observed.root) changes.push({ action: "mkdir", path: profile.skillsRoot });
@@ -19008,7 +19079,12 @@ async function runProfileSync(name, options) {
19008
19079
  });
19009
19080
  }
19010
19081
  await assertProfileIdentity(selected);
19011
- if (await assertStrictProfile(selected, options, (await observations(selected, state)).local))
19082
+ if (await assertStrictProfile(
19083
+ selected,
19084
+ options,
19085
+ (await observations(selected, state)).local,
19086
+ new Set(work.projection.desired.map((desired) => desired.name))
19087
+ ))
19012
19088
  await publishStrictPolicy(selected);
19013
19089
  const currentProfile = await discoverProfile(name, options);
19014
19090
  if (currentProfile.root !== selected.root || currentProfile.skillsRoot !== selected.skillsRoot)
@@ -19650,7 +19726,7 @@ import { randomUUID as randomUUID13 } from "node:crypto";
19650
19726
  import { constants as constants14 } from "node:fs";
19651
19727
  import { mkdir as mkdir13, open as open19, realpath as realpath34, rename as rename13, symlink as symlink8 } from "node:fs/promises";
19652
19728
  import { homedir as homedir16 } from "node:os";
19653
- import { basename as basename13, dirname as dirname29, join as join46, parse as parse6, resolve as resolve27 } from "node:path";
19729
+ import { basename as basename14, dirname as dirname29, join as join46, parse as parse6, resolve as resolve27 } from "node:path";
19654
19730
  import { isDeepStrictEqual as isDeepStrictEqual12 } from "node:util";
19655
19731
  import { valid as valid3 } from "semver";
19656
19732
  var binding5 = { namespace: "migrations", targetParts: [".agents", "root-adoption"] };
@@ -19850,7 +19926,7 @@ async function guard2(context2, journal) {
19850
19926
  function paths2(snapshot3, id) {
19851
19927
  const prefix2 = join46(
19852
19928
  dirname29(snapshot3.path),
19853
- `${basename13(snapshot3.path, ".json")}.root-cutover-${id}`
19929
+ `${basename14(snapshot3.path, ".json")}.root-cutover-${id}`
19854
19930
  );
19855
19931
  return { archive: `${prefix2}-old`, stage: `${prefix2}-new` };
19856
19932
  }
@@ -19962,8 +20038,8 @@ function journalData(snapshot3, context2) {
19962
20038
  )))
19963
20039
  refuse7(snapshot3.path, "The staged root does not match its canonical inventory.");
19964
20040
  for (const stage2 of raw.preservedStages) {
19965
- const prefix2 = `${basename13(snapshot3.path, ".json")}.root-cutover-`;
19966
- if (typeof stage2 !== "string" || dirname29(stage2) !== dirname29(snapshot3.path) || !basename13(stage2).startsWith(prefix2) || !uuid.test(basename13(stage2).slice(prefix2.length, -4)) || !stage2.endsWith("-new"))
20041
+ const prefix2 = `${basename14(snapshot3.path, ".json")}.root-cutover-`;
20042
+ if (typeof stage2 !== "string" || dirname29(stage2) !== dirname29(snapshot3.path) || !basename14(stage2).startsWith(prefix2) || !uuid.test(basename14(stage2).slice(prefix2.length, -4)) || !stage2.endsWith("-new"))
19967
20043
  refuse7(snapshot3.path, "A preserved stage has an invalid quarantine path.");
19968
20044
  }
19969
20045
  const data2 = raw;
@@ -20004,7 +20080,7 @@ async function ownerFor2(context2, allowed) {
20004
20080
  ]) {
20005
20081
  if (allowed && (path === context2.root || dirname29(path) === context2.root)) {
20006
20082
  const candidates = allowed.map(
20007
- (node) => path === context2.root ? node : node.children[basename13(path)]
20083
+ (node) => path === context2.root ? node : node.children[basename14(path)]
20008
20084
  );
20009
20085
  if (!candidates.some(
20010
20086
  (node) => node && ["link", "directory"].includes(node.identity.kind) && isDeepStrictEqual12(nodeIdentity(node), expected)
@@ -20667,7 +20743,7 @@ async function adoptRoot(options = {}) {
20667
20743
  }
20668
20744
 
20669
20745
  // src/core/sets.ts
20670
- import { readdir as readdir30 } from "node:fs/promises";
20746
+ import { readdir as readdir31 } from "node:fs/promises";
20671
20747
  import { join as join47 } from "node:path";
20672
20748
  function validateName(name) {
20673
20749
  parseManifest({ sets: [{ name }] }, "set name");
@@ -20690,7 +20766,7 @@ async function listSets(options = {}) {
20690
20766
  const failures = [];
20691
20767
  if (await inspectPath(path)) {
20692
20768
  await requireDirectory(path, "E_SET_MISSING");
20693
- for (const entry3 of (await readdir30(path, { withFileTypes: true })).sort(
20769
+ for (const entry3 of (await readdir31(path, { withFileTypes: true })).sort(
20694
20770
  (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
20695
20771
  )) {
20696
20772
  if (entry3.name.startsWith(".") || entry3.name.startsWith("_") || !entry3.isDirectory() && !entry3.isSymbolicLink())
@@ -20832,7 +20908,7 @@ async function removeSetSkills(name, names2, options = {}) {
20832
20908
  }
20833
20909
 
20834
20910
  // src/core/vendor-inspect.ts
20835
- import { readdir as readdir31 } from "node:fs/promises";
20911
+ import { readdir as readdir32 } from "node:fs/promises";
20836
20912
  import { join as join48 } from "node:path";
20837
20913
 
20838
20914
  // src/core/vendor-git.ts
@@ -21124,8 +21200,8 @@ async function readVendorGitSource(source, checkoutRoot, options = {}) {
21124
21200
  checkout
21125
21201
  ).trim();
21126
21202
  const base = await treeAt(checkout, rootTree, source.subdir, options);
21127
- const children = await treeEntries(checkout, base, options);
21128
- const declarations = source.membership === "explicit" ? source.skills : children.filter(
21203
+ const children2 = await treeEntries(checkout, base, options);
21204
+ const declarations = source.membership === "explicit" ? source.skills : children2.filter(
21129
21205
  (entry3) => !entry3.name.startsWith(".") && !entry3.name.startsWith("_") && (entry3.type === "tree" || entry3.mode === "120000" || entry3.mode === "160000")
21130
21206
  ).map((entry3) => ({ name: entry3.name, dir: entry3.name }));
21131
21207
  const selected = declarations.filter(
@@ -21197,7 +21273,7 @@ async function recordedInventory(manifest, observation, options) {
21197
21273
  const recorded = /* @__PURE__ */ new Map();
21198
21274
  const failed2 = /* @__PURE__ */ new Set();
21199
21275
  const catalog = join48(manifest.registry.root, "all-skills");
21200
- for (const entry3 of (await readdir31(catalog, { withFileTypes: true })).sort(
21276
+ for (const entry3 of (await readdir32(catalog, { withFileTypes: true })).sort(
21201
21277
  (a, b) => a.name.localeCompare(b.name)
21202
21278
  )) {
21203
21279
  checkVendorSignal(options);
@@ -21670,7 +21746,7 @@ async function inspectVendorStatus(options = {}) {
21670
21746
 
21671
21747
  // src/core/vendor-sync.ts
21672
21748
  import { randomUUID as randomUUID14 } from "node:crypto";
21673
- import { chmod as chmod4, lstat as lstat27, mkdir as mkdir14, open as open20, readdir as readdir32, rename as rename14 } from "node:fs/promises";
21749
+ import { chmod as chmod4, lstat as lstat27, mkdir as mkdir14, open as open20, readdir as readdir33, rename as rename14 } from "node:fs/promises";
21674
21750
  import { dirname as dirname30, join as join49 } from "node:path";
21675
21751
  function refuse8(code, message, path, fix) {
21676
21752
  fail(code, message, { path, fix }, ExitCode.REFUSED);
@@ -21897,7 +21973,7 @@ async function prepare6(options, extractedAt) {
21897
21973
  }
21898
21974
  }
21899
21975
  if (options.prune) {
21900
- for (const name of (await readdir32(catalog)).filter(isSkillName).sort()) {
21976
+ for (const name of (await readdir33(catalog)).filter(isSkillName).sort()) {
21901
21977
  if (names2.has(name)) continue;
21902
21978
  const path = join49(catalog, name);
21903
21979
  if (!(await vendorLstat(path))?.isDirectory()) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/skillex",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Canonical skill catalog and activation management for agentic CLIs.",
5
5
  "type": "module",
6
6
  "license": "MIT",