@hasna/skills 0.9.1 → 0.9.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/README.md CHANGED
@@ -271,6 +271,15 @@ Vendor file restoration uses an exclusive hard link to preserve concurrent files
271
271
  Do not retry an interrupted operation until its journal and both paths have been
272
272
  reconciled; a failed final journal write may leave the earlier durable intent.
273
273
 
274
+ For a reviewed migration that must retire one exact native copy, pass
275
+ `--target-manifest <file>`. The JSON manifest uses schema
276
+ `hasna.skills-native-migration-targets.v1` and binds each target to an agent,
277
+ an existing absolute `projectRoot`, a normalized project-relative `path`, and
278
+ the complete directory `treeSha256`. Exact-target mode refuses broad selector
279
+ flags, protected or vendor entries, missing or ambiguous paths, symlinks,
280
+ digest drift, and writes a manifest digest into the recovery journal. Every
281
+ inventory entry outside the manifest remains untouched.
282
+
274
283
  `skills hook agents --json` reports the supported adapters and coverage limits.
275
284
  Claude and Codex have lifecycle context hooks; Gemini uses `BeforeAgent`, and
276
285
  OpenCode uses its awaited message plugin. Cursor receives selected context at
@@ -758,7 +767,7 @@ of app folders, and `XDG_CONFIG_HOME` is not consulted at all.
758
767
  | `skills sessions show <id> --json` | | Inspect one session's exact profile revision and receipt hash without loading payloads |
759
768
  | `skills sessions reconcile <id> --from-profile <id> --from-revision <rev> --receipt-sha256 <sha> --selection-profile <id> --profile-revision <rev>` | | Plan an explicit migration of one live session; `--apply --plan-digest <digest> --plan-issued-at <time> --plan-expires-at <time>` (within five minutes) preserves its old receipt and applies the reviewed replacement |
760
769
  | `skills hook agents --json` | | Report maintained adapters and explicit coverage limits |
761
- | `skills migrate native` | | Inventory native copies; `--apply` archives managed copies, with explicit `--include-unmanaged` and `--include-vendor` retirement options |
770
+ | `skills migrate native` | | Inventory native copies; `--apply` archives managed copies, with explicit `--include-unmanaged`, `--include-vendor`, or reviewed `--target-manifest` selection |
762
771
  | `skills pull --all --selection-profile <id>` | | With CLI loading active, refresh the selected profile into the verified cache |
763
772
  | `skills sync --selection-profile <id> [--check] [--station <id>]` | | Sync or check the selected profile/cache; optionally record station state |
764
773
  | `skills station-state <id>` | | Read a station's sync receipt in the authenticated workspace |
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.9.1",
36871
+ version: "0.9.2",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -55358,6 +55358,82 @@ function treeHash(root2) {
55358
55358
  visit2(root2);
55359
55359
  return hash2.digest("hex");
55360
55360
  }
55361
+ function parseNativeMigrationTargetManifest(input) {
55362
+ const digest2 = sha(input);
55363
+ let value;
55364
+ try {
55365
+ value = JSON.parse(input.toString());
55366
+ } catch {
55367
+ throw new Error("Invalid native migration target manifest JSON");
55368
+ }
55369
+ if (!value || typeof value !== "object" || Array.isArray(value))
55370
+ throw new Error("Invalid native migration target manifest");
55371
+ const record2 = value;
55372
+ if (record2.schema !== "hasna.skills-native-migration-targets.v1" || Object.keys(record2).some((key) => !["schema", "targets"].includes(key)))
55373
+ throw new Error("Unsupported native migration target manifest schema");
55374
+ if (!Array.isArray(record2.targets) || record2.targets.length === 0 || record2.targets.length > 1000)
55375
+ throw new Error("Native migration target manifest must contain 1 to 1000 targets");
55376
+ const targets = [];
55377
+ const identities = new Set;
55378
+ for (const target of record2.targets) {
55379
+ if (!target || typeof target !== "object" || Array.isArray(target))
55380
+ throw new Error("Invalid native migration target");
55381
+ const item = target;
55382
+ if (Object.keys(item).some((key) => !["agent", "projectRoot", "path", "treeSha256"].includes(key)) || typeof item.agent !== "string" || typeof item.projectRoot !== "string" || typeof item.path !== "string" || typeof item.treeSha256 !== "string")
55383
+ throw new Error("Invalid native migration target fields");
55384
+ if (!INTEGRATION_AGENTS.includes(item.agent))
55385
+ throw new Error(`Unsupported native migration target agent: ${item.agent}`);
55386
+ const projectRoot = resolve16(item.projectRoot);
55387
+ if (!isAbsolute15(item.projectRoot) || projectRoot !== item.projectRoot || !existsSync15(projectRoot) || !lstatSync18(projectRoot).isDirectory())
55388
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${item.projectRoot}`);
55389
+ assertSafePath(projectRoot);
55390
+ if (isAbsolute15(item.path) || item.path.includes("\\") || item.path.length === 0 || item.path.includes("\x00"))
55391
+ throw new Error(`Native migration target path must be normalized and project-relative: ${item.path}`);
55392
+ const resolved = resolve16(projectRoot, item.path), normalized = relative5(projectRoot, resolved);
55393
+ if (!normalized || normalized === ".." || normalized.startsWith(`..${sep6}`) || isAbsolute15(normalized) || normalized !== item.path)
55394
+ throw new Error(`Native migration target path escapes or is not normalized: ${item.path}`);
55395
+ if (!/^[0-9a-f]{64}$/.test(item.treeSha256))
55396
+ throw new Error(`Invalid native migration tree digest for ${item.path}`);
55397
+ const identity4 = `${item.agent}\x00${projectRoot}\x00${normalized}`;
55398
+ if (identities.has(identity4))
55399
+ throw new Error(`Duplicate native migration target: ${item.path}`);
55400
+ identities.add(identity4);
55401
+ targets.push({ agent: item.agent, projectRoot, path: normalized, treeSha256: item.treeSha256 });
55402
+ }
55403
+ return { schema: "hasna.skills-native-migration-targets.v1", targets, digest: digest2 };
55404
+ }
55405
+ function readNativeMigrationTargetManifest(path) {
55406
+ return parseNativeMigrationTargetManifest(readNativeBytes(path, 4 * 1024 * 1024));
55407
+ }
55408
+ function selectNativeMigrationTargets(inventory, manifest) {
55409
+ if (manifest.schema !== "hasna.skills-native-migration-targets.v1" || !/^[0-9a-f]{64}$/.test(manifest.digest) || !Array.isArray(manifest.targets) || manifest.targets.length === 0)
55410
+ throw new Error("Invalid native migration target manifest");
55411
+ const identities = new Set;
55412
+ const selected = [];
55413
+ for (const target of manifest.targets) {
55414
+ const projectRoot = resolve16(target.projectRoot), normalized = relative5(projectRoot, resolve16(projectRoot, target.path));
55415
+ if (!isAbsolute15(target.projectRoot) || projectRoot !== target.projectRoot || !existsSync15(projectRoot) || !lstatSync18(projectRoot).isDirectory())
55416
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
55417
+ assertSafePath(projectRoot);
55418
+ if (isAbsolute15(target.path) || target.path.includes("\\") || target.path.includes("\x00") || !normalized || normalized === ".." || normalized.startsWith(`..${sep6}`) || normalized !== target.path || !/^[0-9a-f]{64}$/.test(target.treeSha256))
55419
+ throw new Error(`Invalid native migration target: ${target.path}`);
55420
+ const identity4 = `${target.agent}\x00${projectRoot}\x00${normalized}`;
55421
+ if (identities.has(identity4))
55422
+ throw new Error(`Duplicate native migration target: ${target.path}`);
55423
+ identities.add(identity4);
55424
+ const expected = resolve16(target.projectRoot, target.path);
55425
+ const matches2 = inventory.filter((entry2) => entry2.agent === target.agent && resolve16(entry2.path) === expected);
55426
+ if (matches2.length !== 1)
55427
+ throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
55428
+ const entry = matches2[0];
55429
+ if (entry.bridge || entry.system || entry.vendor)
55430
+ throw new Error(`Native migration target is protected or vendor-owned: ${entry.path}`);
55431
+ if (entry.hash !== target.treeSha256 || treeHash(entry.path) !== target.treeSha256)
55432
+ throw new Error(`Native migration target changed after review: ${entry.path}`);
55433
+ selected.push(entry);
55434
+ }
55435
+ return selected;
55436
+ }
55361
55437
  function projectAncestorDirectories(projects) {
55362
55438
  const directories = new Set;
55363
55439
  for (const project of projects) {
@@ -55846,12 +55922,13 @@ function archiveNativeSkills(inventory, options) {
55846
55922
  if (!expected || treeHash(entry.path) !== entry.hash || !isOwnedCliBridge(entry.path, [expected]))
55847
55923
  throw new Error(`Skills bridge changed after planning: ${entry.path}`);
55848
55924
  }
55849
- const selected = inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
55925
+ const selected = options.targetManifest ? selectNativeMigrationTargets(inventory, options.targetManifest) : inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
55850
55926
  for (const entry of selected)
55851
55927
  if (treeHash(entry.path) !== entry.hash)
55852
55928
  throw new Error(`Native skill changed after planning: ${entry.path}`);
55929
+ const targetManifest = options.targetManifest ? { schema: options.targetManifest.schema, digest: options.targetManifest.digest, targetCount: options.targetManifest.targets.length } : undefined;
55853
55930
  if (!selected.length)
55854
- return { entries: [], ...aliases.length ? { rootAliases: aliases } : {} };
55931
+ return { entries: [], ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
55855
55932
  const operationId = randomUUID6(), archiveRoot = join29(options.dataDir ?? getDataDir(), "migration", operationId, "native"), receiptPath = join29(archiveRoot, "receipt.json");
55856
55933
  const entries = selected.map((entry, index) => ({
55857
55934
  source: entry.vendor ? join29(entry.path, "SKILL.md") : entry.path,
@@ -55860,7 +55937,7 @@ function archiveNativeSkills(inventory, options) {
55860
55937
  ...entry.vendor ? { discoveryOnly: true } : {},
55861
55938
  status: "planned"
55862
55939
  }));
55863
- const journal = { version: 2, operationId, status: "planned", entries, ...aliases.length ? { rootAliases: aliases } : {} };
55940
+ const journal = { version: 2, operationId, status: "planned", entries, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
55864
55941
  const moved = [];
55865
55942
  const verifyArchive = (entry) => {
55866
55943
  assertSafePath(entry.archive);
@@ -55954,7 +56031,7 @@ function archiveNativeSkills(inventory, options) {
55954
56031
  } catch {}
55955
56032
  throw new Error(`Native archive failed; inspect the recovery journal at ${receiptPath} before retrying.`, { cause: error });
55956
56033
  }
55957
- return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...aliases.length ? { rootAliases: aliases } : {} };
56034
+ return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
55958
56035
  }
55959
56036
  function assertManagedAgentBridge(agent, options = {}) {
55960
56037
  const home = resolve16(options.home ?? homedir4()), dataDir3 = options.dataDir ?? getDataDirReadOnly();
@@ -108541,11 +108618,16 @@ function registerAgentIntegration(parent) {
108541
108618
  }
108542
108619
  });
108543
108620
  const migrate = parent.command("migrate").description("Preserve and retire native agent skill copies");
108544
- migrate.command("native").option("--project <directory>", "Also inventory a project and its ancestors (the current directory and its ancestors are always included)").option("--include-unmanaged", "Archive user-authored skills as well as Skills-managed copies", false).option("--include-vendor", "Retire vendor SKILL.md discovery files while preserving plugin scripts and assets", false).option("--discovery-inputs <file>", "Advanced reviewed active plugin roots and source hashes").option("--allow-root-aliases", "Allow home .claude/.codex aliases to existing directories within this home", false).option("--apply", "Move selected skills to private archives outside agent discovery roots", false).option("--json", "Output inventory and archive receipt as JSON", false).description("Inventory native skill copies; preserve complete directories before retiring them").action(async (options) => {
108621
+ migrate.command("native").option("--project <directory>", "Also inventory a project and its ancestors (the current directory and its ancestors are always included)").option("--include-unmanaged", "Archive user-authored skills as well as Skills-managed copies", false).option("--include-vendor", "Retire vendor SKILL.md discovery files while preserving plugin scripts and assets", false).option("--target-manifest <file>", "Reviewed exact native migration target manifest").option("--discovery-inputs <file>", "Advanced reviewed active plugin roots and source hashes").option("--allow-root-aliases", "Allow home .claude/.codex aliases to existing directories within this home", false).option("--apply", "Move selected skills to private archives outside agent discovery roots", false).option("--json", "Output inventory and archive receipt as JSON", false).description("Inventory native skill copies; preserve complete directories before retiring them").action(async (options) => {
108545
108622
  try {
108546
108623
  const discoveryInputs = options.discoveryInputs ? JSON.parse(readFileSync37(options.discoveryInputs, "utf8")) : undefined;
108624
+ if (options.targetManifest && (options.includeUnmanaged || options.includeVendor))
108625
+ throw new Error("--target-manifest cannot be combined with broad native migration selectors");
108626
+ const targetManifest = options.targetManifest ? readNativeMigrationTargetManifest(options.targetManifest) : undefined;
108547
108627
  const inventory = inventoryNativeSkills(undefined, { projectDirs: [process.cwd(), ...options.project ? [options.project] : []], includeVendor: options.includeVendor, configured: options.includeVendor, discoveryInputs, allowRootAliases: options.allowRootAliases });
108548
- const result2 = options.apply ? archiveNativeSkills(inventory, { includeUnmanaged: options.includeUnmanaged, includeVendor: options.includeVendor, allowRootAliases: options.allowRootAliases }) : { entries: [] };
108628
+ if (targetManifest)
108629
+ selectNativeMigrationTargets(inventory, targetManifest);
108630
+ const result2 = options.apply ? archiveNativeSkills(inventory, { includeUnmanaged: options.includeUnmanaged, includeVendor: options.includeVendor, allowRootAliases: options.allowRootAliases, targetManifest }) : { entries: [], ...targetManifest ? { targetManifest: { schema: targetManifest.schema, digest: targetManifest.digest, targetCount: targetManifest.targets.length } } : {} };
108549
108631
  if (options.json)
108550
108632
  await writeCliOutput(JSON.stringify({ applied: options.apply, inventory, ...result2 }));
108551
108633
  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.9.1",
20022
+ version: "0.9.2",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
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.9.1",
10
+ version: "0.9.2",
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.9.1",
24125
+ version: "0.9.2",
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.9.1",
24128
+ version: "0.9.2",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
package/dist/index.js CHANGED
@@ -11448,7 +11448,7 @@ import { isAbsolute as isAbsolute10, join as join20, resolve as resolve10 } from
11448
11448
  // package.json
11449
11449
  var package_default = {
11450
11450
  name: "@hasna/skills",
11451
- version: "0.9.1",
11451
+ version: "0.9.2",
11452
11452
  description: "Skills library for AI coding agents",
11453
11453
  type: "module",
11454
11454
  bin: {
@@ -15738,6 +15738,35 @@ function treeHash(root2) {
15738
15738
  visit2(root2);
15739
15739
  return hash2.digest("hex");
15740
15740
  }
15741
+ function selectNativeMigrationTargets(inventory, manifest) {
15742
+ if (manifest.schema !== "hasna.skills-native-migration-targets.v1" || !/^[0-9a-f]{64}$/.test(manifest.digest) || !Array.isArray(manifest.targets) || manifest.targets.length === 0)
15743
+ throw new Error("Invalid native migration target manifest");
15744
+ const identities = new Set;
15745
+ const selected = [];
15746
+ for (const target of manifest.targets) {
15747
+ const projectRoot = resolve15(target.projectRoot), normalized = relative5(projectRoot, resolve15(projectRoot, target.path));
15748
+ if (!isAbsolute15(target.projectRoot) || projectRoot !== target.projectRoot || !existsSync11(projectRoot) || !lstatSync18(projectRoot).isDirectory())
15749
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
15750
+ assertSafePath(projectRoot);
15751
+ if (isAbsolute15(target.path) || target.path.includes("\\") || target.path.includes("\x00") || !normalized || normalized === ".." || normalized.startsWith(`..${sep6}`) || normalized !== target.path || !/^[0-9a-f]{64}$/.test(target.treeSha256))
15752
+ throw new Error(`Invalid native migration target: ${target.path}`);
15753
+ const identity3 = `${target.agent}\x00${projectRoot}\x00${normalized}`;
15754
+ if (identities.has(identity3))
15755
+ throw new Error(`Duplicate native migration target: ${target.path}`);
15756
+ identities.add(identity3);
15757
+ const expected = resolve15(target.projectRoot, target.path);
15758
+ const matches = inventory.filter((entry2) => entry2.agent === target.agent && resolve15(entry2.path) === expected);
15759
+ if (matches.length !== 1)
15760
+ throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
15761
+ const entry = matches[0];
15762
+ if (entry.bridge || entry.system || entry.vendor)
15763
+ throw new Error(`Native migration target is protected or vendor-owned: ${entry.path}`);
15764
+ if (entry.hash !== target.treeSha256 || treeHash(entry.path) !== target.treeSha256)
15765
+ throw new Error(`Native migration target changed after review: ${entry.path}`);
15766
+ selected.push(entry);
15767
+ }
15768
+ return selected;
15769
+ }
15741
15770
  function projectAncestorDirectories(projects) {
15742
15771
  const directories = new Set;
15743
15772
  for (const project of projects) {
@@ -16226,12 +16255,13 @@ function archiveNativeSkills(inventory, options) {
16226
16255
  if (!expected || treeHash(entry.path) !== entry.hash || !isOwnedCliBridge(entry.path, [expected]))
16227
16256
  throw new Error(`Skills bridge changed after planning: ${entry.path}`);
16228
16257
  }
16229
- const selected = inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
16258
+ const selected = options.targetManifest ? selectNativeMigrationTargets(inventory, options.targetManifest) : inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
16230
16259
  for (const entry of selected)
16231
16260
  if (treeHash(entry.path) !== entry.hash)
16232
16261
  throw new Error(`Native skill changed after planning: ${entry.path}`);
16262
+ const targetManifest = options.targetManifest ? { schema: options.targetManifest.schema, digest: options.targetManifest.digest, targetCount: options.targetManifest.targets.length } : undefined;
16233
16263
  if (!selected.length)
16234
- return { entries: [], ...aliases.length ? { rootAliases: aliases } : {} };
16264
+ return { entries: [], ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
16235
16265
  const operationId = randomUUID3(), archiveRoot = join25(options.dataDir ?? getDataDir(), "migration", operationId, "native"), receiptPath = join25(archiveRoot, "receipt.json");
16236
16266
  const entries = selected.map((entry, index) => ({
16237
16267
  source: entry.vendor ? join25(entry.path, "SKILL.md") : entry.path,
@@ -16240,7 +16270,7 @@ function archiveNativeSkills(inventory, options) {
16240
16270
  ...entry.vendor ? { discoveryOnly: true } : {},
16241
16271
  status: "planned"
16242
16272
  }));
16243
- const journal = { version: 2, operationId, status: "planned", entries, ...aliases.length ? { rootAliases: aliases } : {} };
16273
+ const journal = { version: 2, operationId, status: "planned", entries, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
16244
16274
  const moved = [];
16245
16275
  const verifyArchive = (entry) => {
16246
16276
  assertSafePath(entry.archive);
@@ -16334,7 +16364,7 @@ function archiveNativeSkills(inventory, options) {
16334
16364
  } catch {}
16335
16365
  throw new Error(`Native archive failed; inspect the recovery journal at ${receiptPath} before retrying.`, { cause: error });
16336
16366
  }
16337
- return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...aliases.length ? { rootAliases: aliases } : {} };
16367
+ return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
16338
16368
  }
16339
16369
  function assertNativeExportAllowed(dataDir2 = getDataDirReadOnly()) {
16340
16370
  if (requiresCliSkillLoading(dataDir2))
@@ -22,6 +22,17 @@ export interface NativeSkillEntry {
22
22
  bridgeHome?: string;
23
23
  rootAlias?: AgentRootAlias;
24
24
  }
25
+ export interface NativeMigrationTarget {
26
+ agent: string;
27
+ projectRoot: string;
28
+ path: string;
29
+ treeSha256: string;
30
+ }
31
+ export interface NativeMigrationTargetManifest {
32
+ schema: "hasna.skills-native-migration-targets.v1";
33
+ targets: NativeMigrationTarget[];
34
+ digest: string;
35
+ }
25
36
  export interface AgentConfigChange {
26
37
  path: string;
27
38
  before: string | null;
@@ -40,6 +51,15 @@ export interface AgentIntegrationPlan {
40
51
  discoveryAfter?: AgentDiscoveryBinding[];
41
52
  rootAliases?: AgentRootAlias[];
42
53
  }
54
+ /** Parse the reviewed exact-target binding used by native migration. Paths are
55
+ * deliberately project-relative so a manifest cannot silently select another
56
+ * checkout when a project root changes. */
57
+ export declare function parseNativeMigrationTargetManifest(input: string | Buffer): NativeMigrationTargetManifest;
58
+ /** Read a manifest without following links or opening special files, with a
59
+ * finite bound so a CLI cannot block on an untrusted path. */
60
+ export declare function readNativeMigrationTargetManifest(path: string): NativeMigrationTargetManifest;
61
+ /** Validate every reviewed target before any archive directory or journal is created. */
62
+ export declare function selectNativeMigrationTargets(inventory: NativeSkillEntry[], manifest: NativeMigrationTargetManifest): NativeSkillEntry[];
43
63
  export declare function inventoryNativeSkills(home?: string, options?: {
44
64
  includeVendor?: boolean;
45
65
  guardHermes?: boolean;
@@ -75,6 +95,7 @@ export declare function archiveNativeSkills(inventory: NativeSkillEntry[], optio
75
95
  includeUnmanaged?: boolean;
76
96
  includeVendor?: boolean;
77
97
  allowRootAliases?: boolean;
98
+ targetManifest?: NativeMigrationTargetManifest;
78
99
  }): {
79
100
  entries: Array<{
80
101
  source: string;
@@ -84,6 +105,11 @@ export declare function archiveNativeSkills(inventory: NativeSkillEntry[], optio
84
105
  }>;
85
106
  receiptPath?: string;
86
107
  rootAliases?: AgentRootAlias[];
108
+ targetManifest?: {
109
+ schema: string;
110
+ digest: string;
111
+ targetCount: number;
112
+ };
87
113
  };
88
114
  /** Run before any prompt context load. Missing ownership or reappearing native
89
115
  * discovery files require repair; verified cache availability is not an override. */
package/dist/sdk/index.js CHANGED
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
32373
32373
  // package.json
32374
32374
  var package_default = {
32375
32375
  name: "@hasna/skills",
32376
- version: "0.9.1",
32376
+ version: "0.9.2",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
@@ -40661,6 +40661,35 @@ function treeHash(root2) {
40661
40661
  visit2(root2);
40662
40662
  return hash2.digest("hex");
40663
40663
  }
40664
+ function selectNativeMigrationTargets(inventory, manifest) {
40665
+ if (manifest.schema !== "hasna.skills-native-migration-targets.v1" || !/^[0-9a-f]{64}$/.test(manifest.digest) || !Array.isArray(manifest.targets) || manifest.targets.length === 0)
40666
+ throw new Error("Invalid native migration target manifest");
40667
+ const identities = new Set;
40668
+ const selected = [];
40669
+ for (const target of manifest.targets) {
40670
+ const projectRoot = resolve14(target.projectRoot), normalized = relative3(projectRoot, resolve14(projectRoot, target.path));
40671
+ if (!isAbsolute15(target.projectRoot) || projectRoot !== target.projectRoot || !existsSync6(projectRoot) || !lstatSync17(projectRoot).isDirectory())
40672
+ throw new Error(`Native migration target project root must be an existing absolute directory: ${target.projectRoot}`);
40673
+ assertSafePath(projectRoot);
40674
+ if (isAbsolute15(target.path) || target.path.includes("\\") || target.path.includes("\x00") || !normalized || normalized === ".." || normalized.startsWith(`..${sep4}`) || normalized !== target.path || !/^[0-9a-f]{64}$/.test(target.treeSha256))
40675
+ throw new Error(`Invalid native migration target: ${target.path}`);
40676
+ const identity3 = `${target.agent}\x00${projectRoot}\x00${normalized}`;
40677
+ if (identities.has(identity3))
40678
+ throw new Error(`Duplicate native migration target: ${target.path}`);
40679
+ identities.add(identity3);
40680
+ const expected = resolve14(target.projectRoot, target.path);
40681
+ const matches = inventory.filter((entry2) => entry2.agent === target.agent && resolve14(entry2.path) === expected);
40682
+ if (matches.length !== 1)
40683
+ throw new Error(`Native migration target was not found exactly once: ${target.agent} ${target.projectRoot}/${target.path}`);
40684
+ const entry = matches[0];
40685
+ if (entry.bridge || entry.system || entry.vendor)
40686
+ throw new Error(`Native migration target is protected or vendor-owned: ${entry.path}`);
40687
+ if (entry.hash !== target.treeSha256 || treeHash(entry.path) !== target.treeSha256)
40688
+ throw new Error(`Native migration target changed after review: ${entry.path}`);
40689
+ selected.push(entry);
40690
+ }
40691
+ return selected;
40692
+ }
40664
40693
  function projectAncestorDirectories(projects) {
40665
40694
  const directories = new Set;
40666
40695
  for (const project of projects) {
@@ -41149,12 +41178,13 @@ function archiveNativeSkills(inventory, options) {
41149
41178
  if (!expected || treeHash(entry.path) !== entry.hash || !isOwnedCliBridge(entry.path, [expected]))
41150
41179
  throw new Error(`Skills bridge changed after planning: ${entry.path}`);
41151
41180
  }
41152
- const selected = inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
41181
+ const selected = options.targetManifest ? selectNativeMigrationTargets(inventory, options.targetManifest) : inventory.filter((entry) => !entry.bridge && !entry.system && (entry.vendor ? options.includeVendor : entry.managed || options.includeUnmanaged));
41153
41182
  for (const entry of selected)
41154
41183
  if (treeHash(entry.path) !== entry.hash)
41155
41184
  throw new Error(`Native skill changed after planning: ${entry.path}`);
41185
+ const targetManifest = options.targetManifest ? { schema: options.targetManifest.schema, digest: options.targetManifest.digest, targetCount: options.targetManifest.targets.length } : undefined;
41156
41186
  if (!selected.length)
41157
- return { entries: [], ...aliases.length ? { rootAliases: aliases } : {} };
41187
+ return { entries: [], ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
41158
41188
  const operationId = randomUUID3(), archiveRoot = join21(options.dataDir ?? getDataDir(), "migration", operationId, "native"), receiptPath = join21(archiveRoot, "receipt.json");
41159
41189
  const entries = selected.map((entry, index) => ({
41160
41190
  source: entry.vendor ? join21(entry.path, "SKILL.md") : entry.path,
@@ -41163,7 +41193,7 @@ function archiveNativeSkills(inventory, options) {
41163
41193
  ...entry.vendor ? { discoveryOnly: true } : {},
41164
41194
  status: "planned"
41165
41195
  }));
41166
- const journal = { version: 2, operationId, status: "planned", entries, ...aliases.length ? { rootAliases: aliases } : {} };
41196
+ const journal = { version: 2, operationId, status: "planned", entries, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
41167
41197
  const moved = [];
41168
41198
  const verifyArchive = (entry) => {
41169
41199
  assertSafePath(entry.archive);
@@ -41257,7 +41287,7 @@ function archiveNativeSkills(inventory, options) {
41257
41287
  } catch {}
41258
41288
  throw new Error(`Native archive failed; inspect the recovery journal at ${receiptPath} before retrying.`, { cause: error });
41259
41289
  }
41260
- return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...aliases.length ? { rootAliases: aliases } : {} };
41290
+ return { entries: entries.map(({ status, conflict, ...entry }) => entry), receiptPath, ...targetManifest ? { targetManifest } : {}, ...aliases.length ? { rootAliases: aliases } : {} };
41261
41291
  }
41262
41292
  function assertNativeExportAllowed(dataDir2 = getDataDirReadOnly()) {
41263
41293
  if (requiresCliSkillLoading(dataDir2))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {