@skill-map/cli 0.67.0 → 0.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/cli/tutorial/sm-tutorial/SKILL.md +30 -23
  2. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/agents-hub/providers/agent-skills/en/agents-hub.md +2 -0
  3. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/agents-hub/providers/agent-skills/es/agents-hub.md +2 -0
  4. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/content-editor-style/providers/agent-skills/en/content-editor-style.md +1 -0
  5. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/content-editor-style/providers/agent-skills/es/content-editor-style.md +1 -0
  6. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/agent-skills/en/todo-bullet-guideline.md +1 -0
  7. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/agent-skills/en/todo-bullet-guideline2.md +1 -0
  8. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/agent-skills/en/todo-bullet-skill.md +1 -0
  9. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/agent-skills/es/todo-bullet-guideline.md +1 -0
  10. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/agent-skills/es/todo-bullet-guideline2.md +1 -0
  11. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/agent-skills/es/todo-bullet-skill.md +1 -0
  12. package/dist/cli/tutorial/sm-tutorial/fixtures-data/manifest.json +9 -4
  13. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/harness/providers/agent-skills/en/__PROVIDER__/skills/publish/SKILL.md +15 -0
  14. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/harness/providers/agent-skills/es/__PROVIDER__/skills/publish/SKILL.md +16 -0
  15. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/harness/providers/codex/en/__PROVIDER__/skills/publish/SKILL.md +15 -0
  16. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/harness/providers/codex/es/__PROVIDER__/skills/publish/SKILL.md +16 -0
  17. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/master/providers/agent-skills/en/__PROVIDER__/skills/master-agent/SKILL.md +13 -0
  18. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/master/providers/agent-skills/es/__PROVIDER__/skills/master-agent/SKILL.md +14 -0
  19. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/master/providers/codex/en/.codex/agents/master-agent.toml +10 -0
  20. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/master/providers/codex/es/.codex/agents/master-agent.toml +10 -0
  21. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/portfolio/providers/agent-skills/en/AGENTS.md +7 -0
  22. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/portfolio/providers/agent-skills/en/__PROVIDER__/skills/content-editor/SKILL.md +20 -0
  23. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/portfolio/providers/agent-skills/es/AGENTS.md +7 -0
  24. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/portfolio/providers/agent-skills/es/__PROVIDER__/skills/content-editor/SKILL.md +20 -0
  25. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/portfolio/providers/agent-skills/shared/CLAUDE.md +1 -0
  26. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/portfolio/providers/codex/en/.codex/agents/content-editor.toml +19 -0
  27. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/portfolio/providers/codex/es/.codex/agents/content-editor.toml +19 -0
  28. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/prologue/providers/codex/en/.codex/agents/demo-agent.toml +13 -0
  29. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/prologue/providers/codex/en/__PROVIDER__/skills/demo-command/SKILL.md +11 -0
  30. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/prologue/providers/codex/es/.codex/agents/demo-agent.toml +13 -0
  31. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/prologue/providers/codex/es/__PROVIDER__/skills/demo-command/SKILL.md +12 -0
  32. package/dist/cli/tutorial/sm-tutorial/references/_core.md +102 -49
  33. package/dist/cli/tutorial/sm-tutorial/references/_manifest.json +168 -20
  34. package/dist/cli/tutorial/sm-tutorial/references/_manifest.yml +85 -19
  35. package/dist/cli/tutorial/sm-tutorial/references/fixtures.md +6 -7
  36. package/dist/cli/tutorial/sm-tutorial/references/part-authoring.md +2 -2
  37. package/dist/cli/tutorial/sm-tutorial/references/part-basic-daily.md +241 -0
  38. package/dist/cli/tutorial/sm-tutorial/references/part-basic-fundamentals.md +351 -0
  39. package/dist/cli/tutorial/sm-tutorial/references/part-basic-kickoff.md +285 -0
  40. package/dist/cli/tutorial/sm-tutorial/references/part-cli.md +1 -1
  41. package/dist/cli/tutorial/sm-tutorial/references/part-daily-loop.md +62 -99
  42. package/dist/cli/tutorial/sm-tutorial/references/part-fundamentals.md +35 -34
  43. package/dist/cli/tutorial/sm-tutorial/references/part-mcp.md +3 -6
  44. package/dist/cli/tutorial/sm-tutorial/references/part-plugins.md +1 -1
  45. package/dist/cli/tutorial/sm-tutorial/references/part-project-kickoff.md +198 -26
  46. package/dist/cli/tutorial/sm-tutorial/references/part-settings.md +19 -15
  47. package/dist/cli/tutorial/sm-tutorial/scripts/fixtures.js +85 -22
  48. package/dist/cli/tutorial/sm-tutorial/scripts/lib/paths.js +74 -4
  49. package/dist/cli/tutorial/sm-tutorial/scripts/state.js +22 -6
  50. package/dist/cli.js +409 -168
  51. package/dist/conformance/index.js +42 -2
  52. package/dist/index.js +43 -30
  53. package/dist/kernel/index.d.ts +28 -5
  54. package/dist/kernel/index.js +43 -30
  55. package/dist/ui/chunk-22EQLC23.js +1845 -0
  56. package/dist/ui/chunk-3ANNEMV4.js +499 -0
  57. package/dist/ui/{chunk-5BJGO7GH.js → chunk-3U4QZKU2.js} +4 -4
  58. package/dist/ui/chunk-3ZAHOYQ7.js +1 -0
  59. package/dist/ui/{chunk-56CBK7LB.js → chunk-6FGV5O5J.js} +1 -1
  60. package/dist/ui/chunk-7WMT2LX4.js +1 -0
  61. package/dist/ui/{chunk-276RLZR4.js → chunk-BSIR3ADF.js} +14 -14
  62. package/dist/ui/{chunk-FC22ZJQZ.js → chunk-CG25RHMO.js} +1 -1
  63. package/dist/ui/chunk-EFSC6SOL.js +3 -0
  64. package/dist/ui/chunk-EJVWTBMV.js +4 -0
  65. package/dist/ui/chunk-EZI3BXQN.js +1 -0
  66. package/dist/ui/{chunk-JZ2YF7EL.js → chunk-GUPPOK7U.js} +8 -8
  67. package/dist/ui/{chunk-CJURGJTN.js → chunk-HLALESGR.js} +1 -1
  68. package/dist/ui/chunk-I3I4KHR5.js +2 -0
  69. package/dist/ui/{chunk-BOVJVOLH.js → chunk-I6ED2OW7.js} +1 -1
  70. package/dist/ui/chunk-JKPG5PO7.js +375 -0
  71. package/dist/ui/chunk-K3ZRQNN5.js +2 -0
  72. package/dist/ui/chunk-KHDWXSGR.js +1 -0
  73. package/dist/ui/{chunk-HEK4PH5A.js → chunk-KMHXNOFZ.js} +1 -1
  74. package/dist/ui/chunk-KWT7E2RJ.js +16 -0
  75. package/dist/ui/{chunk-WHZVGOS3.js → chunk-MQSU6EFZ.js} +1 -1
  76. package/dist/ui/{chunk-43S72FTV.js → chunk-OGEE252A.js} +1 -1
  77. package/dist/ui/{chunk-J4J42HJ4.js → chunk-PU5OP5RN.js} +1 -1
  78. package/dist/ui/{chunk-UTRZTB6V.js → chunk-QVG7J2MP.js} +1 -1
  79. package/dist/ui/chunk-TLMV4LOQ.js +3 -0
  80. package/dist/ui/chunk-TQBXK5JN.js +1 -0
  81. package/dist/ui/chunk-Z7SOKILO.js +2 -0
  82. package/dist/ui/{chunk-WCABR6TI.js → chunk-ZRJ5ZCFR.js} +1 -1
  83. package/dist/ui/index.html +2 -2
  84. package/dist/ui/main-R7BIU4HU.js +4 -0
  85. package/dist/ui/styles-VEGETYWD.css +1 -0
  86. package/package.json +17 -18
  87. package/dist/cli/tutorial/sm-tutorial/references/part-connect-harness.md +0 -173
  88. package/dist/ui/chunk-34ZZDYNQ.js +0 -1
  89. package/dist/ui/chunk-444BFYGR.js +0 -3
  90. package/dist/ui/chunk-44VNNUSQ.js +0 -2
  91. package/dist/ui/chunk-4SG4352Z.js +0 -7
  92. package/dist/ui/chunk-5ITZXW3A.js +0 -1
  93. package/dist/ui/chunk-7ANZW2OI.js +0 -499
  94. package/dist/ui/chunk-BJ6X6WBO.js +0 -4
  95. package/dist/ui/chunk-CZSLV6YD.js +0 -1
  96. package/dist/ui/chunk-DLYJHLJX.js +0 -2
  97. package/dist/ui/chunk-LGFABCIA.js +0 -16
  98. package/dist/ui/chunk-LPDD2DHE.js +0 -369
  99. package/dist/ui/chunk-P3SNMV4X.js +0 -2
  100. package/dist/ui/chunk-S4S5ZMXJ.js +0 -3
  101. package/dist/ui/chunk-VHEFRMK3.js +0 -1
  102. package/dist/ui/chunk-X6TRIDBI.js +0 -1845
  103. package/dist/ui/main-V77F2KZX.js +0 -4
  104. package/dist/ui/styles-I4ULXD3V.css +0 -1
  105. /package/dist/ui/{chunk-Y2Z26SRI.js → chunk-5RNLC6V4.js} +0 -0
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // cli/entry.ts
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="8b8b0da7-bdb9-55ed-8dcd-0f983f8be006")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a842c024-162c-5a3a-85db-1daea4b6735f")}catch(e){}}();
4
4
  import { existsSync as existsSync34 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -250,7 +250,7 @@ function bucketByKind(kind, instance, bag) {
250
250
  // package.json
251
251
  var package_default = {
252
252
  name: "@skill-map/cli",
253
- version: "0.67.0",
253
+ version: "0.68.1",
254
254
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
255
255
  license: "MIT",
256
256
  type: "module",
@@ -323,39 +323,38 @@ var package_default = {
323
323
  clean: "rm -rf dist coverage"
324
324
  },
325
325
  dependencies: {
326
- "@hono/node-server": "2.0.1",
327
- "@sentry/node": "10.55.0",
326
+ "@hono/node-server": "2.0.6",
327
+ "@sentry/node": "10.61.0",
328
328
  "@skill-map/spec": "workspace:*",
329
- ajv: "8.18.0",
329
+ ajv: "8.20.0",
330
330
  "ajv-formats": "3.0.1",
331
331
  chokidar: "5.0.0",
332
332
  clipanion: "4.0.0-rc.4",
333
- hono: "4.12.18",
333
+ hono: "4.12.27",
334
334
  ignore: "7.0.5",
335
335
  "js-tiktoken": "1.0.21",
336
- "js-yaml": "4.1.1",
337
- kysely: "0.28.17",
338
- "posthog-node": "5.35.6",
339
- semver: "7.7.4",
340
- "smol-toml": "1.6.1",
336
+ "js-yaml": "5.1.0",
337
+ kysely: "0.29.2",
338
+ "posthog-node": "5.38.5",
339
+ semver: "7.8.5",
340
+ "smol-toml": "1.7.0",
341
341
  typanion: "3.14.0",
342
342
  ws: "8.21.0"
343
343
  },
344
344
  devDependencies: {
345
345
  "@eslint/js": "10.0.1",
346
346
  "@stylistic/eslint-plugin": "5.10.0",
347
- "@types/js-yaml": "4.0.9",
348
- "@types/node": "24.12.2",
347
+ "@types/node": "26.0.1",
349
348
  "@types/semver": "7.7.1",
350
349
  "@types/ws": "8.18.1",
351
350
  c8: "11.0.0",
352
- eslint: "10.2.1",
353
- "eslint-plugin-import-x": "4.16.2",
351
+ eslint: "10.5.0",
352
+ "eslint-plugin-import-x": "4.17.0",
354
353
  "json-schema-to-typescript": "15.0.4",
355
354
  tsup: "8.5.1",
356
- tsx: "4.22.3",
357
- typescript: "5.9.3",
358
- "typescript-eslint": "8.59.1"
355
+ tsx: "4.22.4",
356
+ typescript: "6.0.3",
357
+ "typescript-eslint": "8.62.0"
359
358
  },
360
359
  engines: {
361
360
  node: ">=24.0"
@@ -850,9 +849,9 @@ function stripFences(input) {
850
849
  }
851
850
  continue;
852
851
  }
853
- const open2 = FENCE_RE.exec(line);
854
- if (open2?.groups) {
855
- openFence = open2.groups["fence"];
852
+ const open3 = FENCE_RE.exec(line);
853
+ if (open3?.groups) {
854
+ openFence = open3.groups["fence"];
856
855
  out.push(blank(line));
857
856
  continue;
858
857
  }
@@ -1247,12 +1246,14 @@ var agentSkillsProvider = {
1247
1246
  // Provider-owned.
1248
1247
  detect: { markers: [".agents"] },
1249
1248
  // Authoring target for `sm tutorial`: the open standard discovers skills
1250
- // under `.agents/skills/<name>/SKILL.md`. The same path is consumed by
1251
- // Antigravity (adopted the standard rather than a `.gemini/` layout) and
1252
- // OpenAI Codex (skills mirror the open standard), so `aka` surfaces both
1253
- // names in the destination prompt to orient testers on those agents.
1254
- // `aka` is display-only, `--for` still matches the `agent-skills` id.
1255
- scaffold: { skillDir: ".agents/skills", aka: ["Google's Antigravity", "OpenAI's Codex"] },
1249
+ // under `.agents/skills/<name>/SKILL.md`. `aka` lists Antigravity, which
1250
+ // shares this territory AND the BASIC tutorial track (skill + markdown,
1251
+ // references), so a tester on Antigravity scaffolds here. OpenAI Codex
1252
+ // also reads `.agents/skills/`, but Codex is a RICH-track lens (it has the
1253
+ // `agent` kind, slash and `@`), so advertising it under this basic row
1254
+ // would hand it the wrong book; Codex is surfaced once a Codex rich
1255
+ // scaffold target lands. `aka` is display-only, `--for` matches the id.
1256
+ scaffold: { skillDir: ".agents/skills", aka: ["Google's Antigravity"] },
1256
1257
  read: COMMONS_READ,
1257
1258
  kinds: COMMONS_KINDS,
1258
1259
  resolution: COMMONS_RESOLUTION,
@@ -1473,6 +1474,12 @@ var codexProvider = {
1473
1474
  // open-standard project, not necessarily a Codex one. A genuine Codex
1474
1475
  // project is identified by `.codex/`.
1475
1476
  detect: { markers: [".codex"] },
1477
+ // Tutorial scaffold target (rich track). Codex skills adopt the open
1478
+ // `.agents/skills/` layout, but the codex LENS resolves off `.codex/`,
1479
+ // which the open territory does not carry, so `sm tutorial --for codex`
1480
+ // also drops a `.codex/` marker (the home its TOML agents land under) to
1481
+ // disambiguate from the `agent-skills` lens that shares `.agents/skills/`.
1482
+ scaffold: { skillDir: ".agents/skills", marker: ".codex" },
1476
1483
  // Vendor provider: Codex CLI only reads its own territory (its `.codex/`
1477
1484
  // agents plus the open `.agents/skills/` skills it adopted). Gating the
1478
1485
  // classifier behind the active lens keeps the walker from claiming Codex
@@ -3656,7 +3663,7 @@ import { existsSync, readFileSync as readFileSync3 } from "fs";
3656
3663
  import { dirname as dirname3, resolve as resolve4 } from "path";
3657
3664
  import { createRequire as createRequire3 } from "module";
3658
3665
  import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
3659
- import yaml from "js-yaml";
3666
+ import { load as yamlLoad, JSON_SCHEMA } from "js-yaml";
3660
3667
 
3661
3668
  // kernel/util/strip-prototype-pollution.ts
3662
3669
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
@@ -3697,7 +3704,7 @@ function readSidecarFor(mdAbsolutePath) {
3697
3704
  }
3698
3705
  let parsedYaml;
3699
3706
  try {
3700
- parsedYaml = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
3707
+ parsedYaml = yamlLoad(raw, { schema: JSON_SCHEMA });
3701
3708
  } catch (err) {
3702
3709
  return {
3703
3710
  parsed: null,
@@ -5983,7 +5990,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
5983
5990
  import { dirname as dirname6, resolve as resolve8 } from "path";
5984
5991
  import { createRequire as createRequire4 } from "module";
5985
5992
  import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
5986
- import yaml2 from "js-yaml";
5993
+ import { dump as yamlDump, load as yamlLoad2, CORE_SCHEMA, JSON_SCHEMA as JSON_SCHEMA2 } from "js-yaml";
5987
5994
  var FilesystemSidecarStore = class {
5988
5995
  /**
5989
5996
  * Path-keyed in-process lock chain. Each path maps to the tail of a
@@ -6035,11 +6042,13 @@ var FilesystemSidecarStore = class {
6035
6042
  `sidecar patch produces a schema-invalid result at ${sidecarAbsPath}: ${errors}`
6036
6043
  );
6037
6044
  }
6038
- const yamlText = yaml2.dump(merged, {
6045
+ const yamlText = yamlDump(merged, {
6039
6046
  sortKeys: true,
6040
6047
  lineWidth: -1,
6041
6048
  noRefs: true,
6042
- noCompatMode: true
6049
+ // js-yaml v5: CORE_SCHEMA reproduces the old `noCompatMode: true`
6050
+ // output (YAML 1.2, no 1.1-compat quoting of yes/no/on/off).
6051
+ schema: CORE_SCHEMA
6043
6052
  });
6044
6053
  atomicWriteFile(sidecarAbsPath, yamlText);
6045
6054
  }
@@ -6069,7 +6078,7 @@ function isPlainObject4(value) {
6069
6078
  function readSidecarObject(sidecarAbsPath) {
6070
6079
  if (!existsSync6(sidecarAbsPath)) return {};
6071
6080
  const raw = readFileSync7(sidecarAbsPath, "utf8");
6072
- const parsed = yaml2.load(raw, { schema: yaml2.JSON_SCHEMA });
6081
+ const parsed = yamlLoad2(raw, { schema: JSON_SCHEMA2 });
6073
6082
  if (parsed === null || parsed === void 0) return {};
6074
6083
  if (!isPlainObject4(parsed)) {
6075
6084
  throw new Error(
@@ -6410,7 +6419,7 @@ var SmCommand = class extends Command {
6410
6419
  };
6411
6420
 
6412
6421
  // core/sqlite/with-sqlite.ts
6413
- import { existsSync as existsSync11 } from "fs";
6422
+ import { existsSync as existsSync12 } from "fs";
6414
6423
 
6415
6424
  // kernel/adapters/sqlite/storage-adapter.ts
6416
6425
  import { mkdirSync as mkdirSync4 } from "fs";
@@ -9158,6 +9167,8 @@ function createSqliteStorage(options) {
9158
9167
  }
9159
9168
 
9160
9169
  // core/sqlite/db-version-check.ts
9170
+ import { existsSync as existsSync11 } from "fs";
9171
+ import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
9161
9172
  async function detectDbVersionSkew(db, currentVersion) {
9162
9173
  const meta = await db.selectFrom("scan_meta").select(["scannedByVersion"]).executeTakeFirst();
9163
9174
  if (!meta) return { kind: "no-meta" };
@@ -9187,6 +9198,20 @@ function classifyVersionSkew(dbVersion, currentVersion) {
9187
9198
  }
9188
9199
  return { kind: "warn-older", dbVersion, currentVersion };
9189
9200
  }
9201
+ function readScannedByVersion(dbPath) {
9202
+ if (dbPath === ":memory:" || !existsSync11(dbPath)) return null;
9203
+ let raw = null;
9204
+ try {
9205
+ raw = new DatabaseSync5(dbPath, { readOnly: true });
9206
+ const row = raw.prepare("SELECT scanned_by_version AS v FROM scan_meta LIMIT 1").get();
9207
+ const v = row?.v;
9208
+ return typeof v === "string" && v.length > 0 ? v : null;
9209
+ } catch {
9210
+ return null;
9211
+ } finally {
9212
+ raw?.close();
9213
+ }
9214
+ }
9190
9215
  function parseVersionTriple(input) {
9191
9216
  if (typeof input !== "string" || input.length === 0) return null;
9192
9217
  const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(input.trim());
@@ -9358,7 +9383,7 @@ async function withSqlite(options, fn) {
9358
9383
  }
9359
9384
  }
9360
9385
  async function tryWithSqlite(options, fn) {
9361
- if (options.databasePath !== ":memory:" && !existsSync11(options.databasePath)) {
9386
+ if (options.databasePath !== ":memory:" && !existsSync12(options.databasePath)) {
9362
9387
  return null;
9363
9388
  }
9364
9389
  return withSqlite(options, fn);
@@ -9962,6 +9987,15 @@ var CHECK_TEXTS = {
9962
9987
  noIssues: "{{glyph}} No issues.\n",
9963
9988
  /** Header summary line: `sm check: 10 warnings · 0 errors`. */
9964
9989
  summaryHeader: "sm check: {{summary}}\n\n",
9990
+ /**
9991
+ * Summary fragments joined by ` · `, each colored at the call site.
9992
+ * `{{plural}}` is `''` / `'s'` resolved by count, matching the
9993
+ * `{{plural}}`-slot pattern used across the other verbs (info has no
9994
+ * plural form).
9995
+ */
9996
+ summaryErrorFragment: "{{count}} error{{plural}}",
9997
+ summaryWarningFragment: "{{count}} warning{{plural}}",
9998
+ summaryInfoFragment: "{{count}} info",
9965
9999
  /** Section heading: one per file with at least one issue. */
9966
10000
  fileSection: " {{file}}\n",
9967
10001
  /**
@@ -10031,6 +10065,20 @@ var PLUGIN_LOADER_TEXTS = {
10031
10065
  invalidManifestExtensionShape: "{{relEntry}}: {{errors}}. See {{docUrl}}.",
10032
10066
  importExceededTimeout: "import exceeded {{timeoutMs}}ms; likely a top-level side effect (network call, infinite loop, large blocking work). Move side effects into the runtime methods (`detect` / `evaluate` / `render` / etc.).",
10033
10067
  disabledByConfig: "disabled by config_plugins or settings.json",
10068
+ /**
10069
+ * Reason stamped on a project-local disk plugin discovered but not
10070
+ * imported because the operator never granted local trust. Distinct
10071
+ * from `disabledByConfig` (an explicit toggle-off): this id has no
10072
+ * `config_plugins` override at all, so its code stays unexecuted until
10073
+ * `sm plugins enable` records local intent.
10074
+ */
10075
+ untrustedNotLoaded: "not loaded: project-local plugin is untrusted until enabled. Run `sm plugins enable {{pluginId}}` to load it.",
10076
+ /**
10077
+ * One-time aggregate notice the runtime emits when project-local
10078
+ * plugins were found on disk but left unloaded for lack of trust. The
10079
+ * `{{count}}` plugins ride the scan without executing any code.
10080
+ */
10081
+ untrustedPluginsFoundNotice: "{{count}} project-local plugin(s) found in .skill-map/plugins/ but not loaded (untrusted). Their code did NOT run. Review with `sm plugins list`, then enable any you trust with `sm plugins enable <id>`.",
10034
10082
  invalidManifestDirMismatch: "directory name '{{dirName}}' does not match manifest id '{{manifestId}}'. Rename the directory to match the id, or update the manifest id to match the directory.",
10035
10083
  idCollision: "Plugin '{{id}}' at {{pathA}} collides with the plugin at {{pathB}}. Rename one and rerun.",
10036
10084
  loadErrorPluginIdMismatch: "{{relEntry}}: extension declares pluginId '{{declared}}' but its plugin.json declares id '{{manifestId}}'. Remove the explicit pluginId from the extension; the loader injects it from plugin.json#/id.",
@@ -10050,7 +10098,7 @@ var PLUGIN_LOADER_TEXTS = {
10050
10098
 
10051
10099
  // kernel/adapters/plugin-loader/index.ts
10052
10100
  import { createRequire as createRequire5 } from "module";
10053
- import { existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
10101
+ import { existsSync as existsSync14, readFileSync as readFileSync13, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
10054
10102
  import { join as join8, resolve as resolve17 } from "path";
10055
10103
  import { pathToFileURL } from "url";
10056
10104
  import semver from "semver";
@@ -10153,7 +10201,7 @@ function stripFunctionsAndPluginId(input) {
10153
10201
 
10154
10202
  // kernel/adapters/plugin-loader/validation.ts
10155
10203
  import * as nodeFs from "fs";
10156
- import { existsSync as existsSync12 } from "fs";
10204
+ import { existsSync as existsSync13 } from "fs";
10157
10205
  import { dirname as dirname10, join as join7 } from "path";
10158
10206
  import { Ajv2020 as Ajv20205 } from "ajv/dist/2020.js";
10159
10207
 
@@ -10279,7 +10327,7 @@ function validateActionFileConventions(pluginPath, pluginId, manifest, relEntry,
10279
10327
  const reportSchemaPath = join7(actionDir, "report.schema.json");
10280
10328
  const promptPath = join7(actionDir, "prompt.md");
10281
10329
  const mode = isRecord(manifestView) && typeof manifestView["mode"] === "string" ? manifestView["mode"] : "deterministic";
10282
- if (!existsSync12(reportSchemaPath)) {
10330
+ if (!existsSync13(reportSchemaPath)) {
10283
10331
  return {
10284
10332
  ...fail(
10285
10333
  pluginPath,
@@ -10290,7 +10338,7 @@ function validateActionFileConventions(pluginPath, pluginId, manifest, relEntry,
10290
10338
  manifest
10291
10339
  };
10292
10340
  }
10293
- const promptExists = existsSync12(promptPath);
10341
+ const promptExists = existsSync13(promptPath);
10294
10342
  if (mode === "probabilistic" && !promptExists) {
10295
10343
  return {
10296
10344
  ...fail(
@@ -10504,11 +10552,11 @@ var PluginLoader = class {
10504
10552
  discoverPaths() {
10505
10553
  const out = [];
10506
10554
  for (const root of this.#options.searchPaths) {
10507
- if (!existsSync13(root)) continue;
10555
+ if (!existsSync14(root)) continue;
10508
10556
  for (const entry of readdirSync5(root, { withFileTypes: true })) {
10509
10557
  if (!entry.isDirectory()) continue;
10510
10558
  const candidate = join8(root, entry.name);
10511
- if (existsSync13(join8(candidate, "plugin.json"))) {
10559
+ if (existsSync14(join8(candidate, "plugin.json"))) {
10512
10560
  out.push(resolve17(candidate));
10513
10561
  }
10514
10562
  }
@@ -10550,15 +10598,8 @@ var PluginLoader = class {
10550
10598
  const manifestResult = this.#parseAndValidateManifest(pluginPath, pluginId);
10551
10599
  if (!manifestResult.ok) return manifestResult.failure;
10552
10600
  const manifest = manifestResult.manifest;
10553
- if (this.#options.resolveEnabled && !this.#options.resolveEnabled(pluginId)) {
10554
- return {
10555
- path: pluginPath,
10556
- id: pluginId,
10557
- status: "disabled",
10558
- manifest,
10559
- reason: PLUGIN_LOADER_TEXTS.disabledByConfig
10560
- };
10561
- }
10601
+ const gated = this.#preImportGate(pluginPath, pluginId, manifest);
10602
+ if (gated) return gated;
10562
10603
  const loaded = [];
10563
10604
  for (const relEntry of discoverExtensionEntries(pluginPath)) {
10564
10605
  const result = await this.#loadAndValidateExtensionEntry(pluginPath, pluginId, manifest, relEntry);
@@ -10581,6 +10622,39 @@ var PluginLoader = class {
10581
10622
  ...storageSchemasResult.schemas ? { storageSchemas: storageSchemasResult.schemas } : {}
10582
10623
  };
10583
10624
  }
10625
+ /**
10626
+ * Pre-import gates run AFTER the JSON manifest parse (safe) and BEFORE
10627
+ * any extension `import()` (executes code). Returns a short-circuit
10628
+ * `disabled` discovery (manifest kept, code NOT imported) when a gate
10629
+ * refuses, or `null` to proceed to the import loop:
10630
+ *
10631
+ * - enable resolution: the per-config `resolveEnabled` plugin gate.
10632
+ * - import trust: the security boundary, refuse to execute a
10633
+ * project-local plugin the operator has not locally trusted; the
10634
+ * plugin stays discoverable in `sm plugins list` without running.
10635
+ */
10636
+ #preImportGate(pluginPath, pluginId, manifest) {
10637
+ if (this.#options.resolveEnabled && !this.#options.resolveEnabled(pluginId)) {
10638
+ return {
10639
+ path: pluginPath,
10640
+ id: pluginId,
10641
+ status: "disabled",
10642
+ manifest,
10643
+ reason: PLUGIN_LOADER_TEXTS.disabledByConfig
10644
+ };
10645
+ }
10646
+ if (this.#options.resolveImportTrust && !this.#options.resolveImportTrust(pluginId)) {
10647
+ return {
10648
+ path: pluginPath,
10649
+ id: pluginId,
10650
+ status: "disabled",
10651
+ untrusted: true,
10652
+ manifest,
10653
+ reason: tx(PLUGIN_LOADER_TEXTS.untrustedNotLoaded, { pluginId })
10654
+ };
10655
+ }
10656
+ return null;
10657
+ }
10584
10658
  /**
10585
10659
  * Phase 1 of `loadOne`, read `plugin.json`, AJV-validate the manifest,
10586
10660
  * enforce the directory-name == pluginId structural rule, and check
@@ -10669,7 +10743,7 @@ var PluginLoader = class {
10669
10743
  } };
10670
10744
  }
10671
10745
  const abs = resolve17(pluginPath, relEntry);
10672
- if (!existsSync13(abs)) {
10746
+ if (!existsSync14(abs)) {
10673
10747
  return { ok: false, failure: {
10674
10748
  ...fail(
10675
10749
  pluginPath,
@@ -10873,7 +10947,7 @@ function discoverExtensionEntries(pluginPath) {
10873
10947
  }
10874
10948
  function collectKindEntries(pluginPath, kindDir, out) {
10875
10949
  const kindAbs = resolve17(pluginPath, kindDir);
10876
- if (!existsSync13(kindAbs)) return;
10950
+ if (!existsSync14(kindAbs)) return;
10877
10951
  let entries;
10878
10952
  try {
10879
10953
  entries = readdirSync5(kindAbs);
@@ -10900,7 +10974,7 @@ function isDirectorySafe2(path) {
10900
10974
  }
10901
10975
  function findIndexCandidate(entryAbs) {
10902
10976
  for (const candidate of INDEX_CANDIDATES) {
10903
- if (existsSync13(resolve17(entryAbs, candidate))) return candidate;
10977
+ if (existsSync14(resolve17(entryAbs, candidate))) return candidate;
10904
10978
  }
10905
10979
  return null;
10906
10980
  }
@@ -10967,6 +11041,17 @@ function resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault = tru
10967
11041
  function makeEnabledResolver(cfg, dbOverrides) {
10968
11042
  return (pluginId, installedDefault) => resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault);
10969
11043
  }
11044
+ function makeImportTrustResolver(dbOverrides) {
11045
+ return (pluginId) => {
11046
+ if (isPluginLocked(pluginId)) return true;
11047
+ const prefix = `${pluginId}/`;
11048
+ for (const [key, enabled] of dbOverrides) {
11049
+ if (!enabled) continue;
11050
+ if (key === pluginId || key.startsWith(prefix)) return true;
11051
+ }
11052
+ return false;
11053
+ };
11054
+ }
10970
11055
 
10971
11056
  // core/runtime/plugin-runtime/resolver.ts
10972
11057
  function defaultResolveEnabled(_id, installedDefault = true) {
@@ -10984,7 +11069,7 @@ function isPluginExtensionEnabled(ext, resolveEnabled) {
10984
11069
  installedDefaultEnabled(ext.stability)
10985
11070
  );
10986
11071
  }
10987
- async function buildEnabledResolver(ctx) {
11072
+ async function buildResolverInputs(ctx) {
10988
11073
  const { effective: cfg } = loadConfig({ ...ctx });
10989
11074
  const dbPath = resolveDbPath({
10990
11075
  db: void 0,
@@ -10994,7 +11079,7 @@ async function buildEnabledResolver(ctx) {
10994
11079
  { databasePath: dbPath, autoBackup: false },
10995
11080
  (adapter) => adapter.pluginConfig.loadOverrideMap()
10996
11081
  ) ?? /* @__PURE__ */ new Map();
10997
- return makeEnabledResolver(cfg, dbOverrides);
11082
+ return { resolveEnabled: makeEnabledResolver(cfg, dbOverrides), dbOverrides };
10998
11083
  }
10999
11084
 
11000
11085
  // kernel/scan/walk-content.ts
@@ -11002,7 +11087,7 @@ import { readFile, readdir, lstat } from "fs/promises";
11002
11087
  import { isAbsolute as isAbsolute4, join as join9, relative as relative2, resolve as resolve19, sep as sep3 } from "path";
11003
11088
 
11004
11089
  // kernel/scan/ignore.ts
11005
- import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
11090
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
11006
11091
  import { dirname as dirname11, resolve as resolve18 } from "path";
11007
11092
  import { fileURLToPath as fileURLToPath2 } from "url";
11008
11093
  import ignoreFactory from "ignore";
@@ -11033,7 +11118,7 @@ function loadBundledIgnoreText() {
11033
11118
  }
11034
11119
  function readIgnoreFileText(scopeRoot) {
11035
11120
  const path = resolve18(scopeRoot, ".skillmapignore");
11036
- if (!existsSync14(path)) return void 0;
11121
+ if (!existsSync15(path)) return void 0;
11037
11122
  try {
11038
11123
  return readFileSync14(path, "utf8");
11039
11124
  } catch {
@@ -11068,7 +11153,7 @@ function readDefaultsFromDisk() {
11068
11153
  resolve18(here, "config/defaults/skillmapignore")
11069
11154
  ];
11070
11155
  for (const candidate of candidates) {
11071
- if (existsSync14(candidate)) {
11156
+ if (existsSync15(candidate)) {
11072
11157
  try {
11073
11158
  return readFileSync14(candidate, "utf8");
11074
11159
  } catch {
@@ -11079,7 +11164,7 @@ function readDefaultsFromDisk() {
11079
11164
  }
11080
11165
 
11081
11166
  // plugins/core/parsers/frontmatter-yaml/index.ts
11082
- import yaml3 from "js-yaml";
11167
+ import { load as yamlLoad3, JSON_SCHEMA as JSON_SCHEMA3 } from "js-yaml";
11083
11168
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
11084
11169
  var frontmatterYamlParser = {
11085
11170
  id: "frontmatter-yaml",
@@ -11091,7 +11176,7 @@ var frontmatterYamlParser = {
11091
11176
  let parsed = {};
11092
11177
  const issues = [];
11093
11178
  try {
11094
- const doc = yaml3.load(frontmatterRaw, { schema: yaml3.JSON_SCHEMA });
11179
+ const doc = yamlLoad3(frontmatterRaw, { schema: JSON_SCHEMA3 });
11095
11180
  if (doc && typeof doc === "object" && !Array.isArray(doc)) {
11096
11181
  parsed = stripPrototypePollution(doc);
11097
11182
  }
@@ -11531,8 +11616,11 @@ async function loadPluginRuntime(opts = {}) {
11531
11616
  const searchPaths = resolveSearchPaths(opts, ctx);
11532
11617
  const validators = loadSchemaValidators();
11533
11618
  let resolveEnabled;
11619
+ let dbOverrides;
11534
11620
  try {
11535
- resolveEnabled = await buildEnabledResolver(ctx);
11621
+ const inputs = await buildResolverInputs(ctx);
11622
+ resolveEnabled = inputs.resolveEnabled;
11623
+ dbOverrides = inputs.dbOverrides;
11536
11624
  } catch {
11537
11625
  }
11538
11626
  const loaderOpts = {
@@ -11541,6 +11629,9 @@ async function loadPluginRuntime(opts = {}) {
11541
11629
  specVersion: installedSpecVersion()
11542
11630
  };
11543
11631
  if (resolveEnabled) loaderOpts.resolveEnabled = resolveEnabled;
11632
+ if (!opts.pluginDir) {
11633
+ loaderOpts.resolveImportTrust = makeImportTrustResolver(dbOverrides ?? /* @__PURE__ */ new Map());
11634
+ }
11544
11635
  const loader = createPluginLoader(loaderOpts);
11545
11636
  const discovered = await loader.discoverAndLoadAll();
11546
11637
  const runtime = {
@@ -11563,6 +11654,12 @@ async function loadPluginRuntime(opts = {}) {
11563
11654
  if (plugin.status === "disabled") continue;
11564
11655
  runtime.warnings.push(formatWarning(plugin));
11565
11656
  }
11657
+ const untrustedCount = discovered.filter((p) => p.untrusted === true).length;
11658
+ if (untrustedCount > 0) {
11659
+ runtime.warnings.push(
11660
+ tx(PLUGIN_LOADER_TEXTS.untrustedPluginsFoundNotice, { count: untrustedCount })
11661
+ );
11662
+ }
11566
11663
  enforceRootExclusivity(runtime.annotationContributions);
11567
11664
  return runtime;
11568
11665
  }
@@ -12001,15 +12098,27 @@ function groupRowsByFile(rows) {
12001
12098
  function formatSummary(counts, ansi) {
12002
12099
  const parts = [];
12003
12100
  if (counts.error > 0) {
12004
- parts.push(ansi.red(`${counts.error} error${counts.error === 1 ? "" : "s"}`));
12101
+ parts.push(
12102
+ ansi.red(
12103
+ tx(CHECK_TEXTS.summaryErrorFragment, {
12104
+ count: counts.error,
12105
+ plural: counts.error === 1 ? "" : "s"
12106
+ })
12107
+ )
12108
+ );
12005
12109
  }
12006
12110
  if (counts.warn > 0) {
12007
12111
  parts.push(
12008
- ansi.yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`)
12112
+ ansi.yellow(
12113
+ tx(CHECK_TEXTS.summaryWarningFragment, {
12114
+ count: counts.warn,
12115
+ plural: counts.warn === 1 ? "" : "s"
12116
+ })
12117
+ )
12009
12118
  );
12010
12119
  }
12011
12120
  if (counts.info > 0) {
12012
- parts.push(ansi.cyan(`${counts.info} info`));
12121
+ parts.push(ansi.cyan(tx(CHECK_TEXTS.summaryInfoFragment, { count: counts.info })));
12013
12122
  }
12014
12123
  return parts.join(" \xB7 ");
12015
12124
  }
@@ -12034,11 +12143,11 @@ function flattenMessage(message) {
12034
12143
  }
12035
12144
 
12036
12145
  // cli/commands/config.ts
12037
- import { existsSync as existsSync16 } from "fs";
12146
+ import { existsSync as existsSync17 } from "fs";
12038
12147
  import { Command as Command4, Option as Option4 } from "clipanion";
12039
12148
 
12040
12149
  // kernel/scan/detect-providers.ts
12041
- import { existsSync as existsSync15 } from "fs";
12150
+ import { existsSync as existsSync16 } from "fs";
12042
12151
  import { join as join10 } from "path";
12043
12152
  function detectProvidersFromFilesystem(cwd, providers) {
12044
12153
  const seen = /* @__PURE__ */ new Set();
@@ -12055,7 +12164,7 @@ function isDetectableUnderCwd(cwd, provider) {
12055
12164
  if (!installedDefaultEnabled(provider.stability)) return false;
12056
12165
  const markers = provider.detect?.markers;
12057
12166
  if (!markers || markers.length === 0) return false;
12058
- return markers.some((marker) => existsSync15(join10(cwd, marker)));
12167
+ return markers.some((marker) => existsSync16(join10(cwd, marker)));
12059
12168
  }
12060
12169
 
12061
12170
  // core/config/active-provider.ts
@@ -12083,7 +12192,7 @@ function relativeIfBelow(path, cwd) {
12083
12192
  }
12084
12193
 
12085
12194
  // cli/util/scan-zone-drop.ts
12086
- import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
12195
+ import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
12087
12196
 
12088
12197
  // cli/commands/db/shared.ts
12089
12198
  var SAFE_SQL_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
@@ -12095,7 +12204,7 @@ function assertSafeIdentifier(name) {
12095
12204
 
12096
12205
  // cli/util/scan-zone-drop.ts
12097
12206
  function dropScanZone(dbPath) {
12098
- const db = new DatabaseSync5(dbPath);
12207
+ const db = new DatabaseSync6(dbPath);
12099
12208
  try {
12100
12209
  const rows = db.prepare(
12101
12210
  "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'scan\\_%' ESCAPE '\\'"
@@ -12686,7 +12795,7 @@ var ConfigSetCommand = class extends SmCommand {
12686
12795
  announceLensSwitch(cwd, ansi) {
12687
12796
  const dbPath = resolveDbPath({ db: void 0, cwd });
12688
12797
  const okGlyph = ansi.green("\u2713");
12689
- if (!existsSync16(dbPath)) {
12798
+ if (!existsSync17(dbPath)) {
12690
12799
  this.printer.info(tx(CONFIG_TEXTS.lensSwitchedNoDb, { glyph: okGlyph }));
12691
12800
  return;
12692
12801
  }
@@ -12726,7 +12835,7 @@ var ConfigResetCommand = class extends SmCommand {
12726
12835
  const path = targetSettingsPath2(target, ctx.cwd);
12727
12836
  const ansi = this.ansiFor("stdout");
12728
12837
  const okGlyph = ansi.green("\u2713");
12729
- if (!existsSync16(path)) {
12838
+ if (!existsSync17(path)) {
12730
12839
  this.printer.data(
12731
12840
  tx(CONFIG_TEXTS.unsetNoOverride, {
12732
12841
  glyph: okGlyph,
@@ -12801,16 +12910,17 @@ var CONFIG_COMMANDS = [
12801
12910
  ];
12802
12911
 
12803
12912
  // cli/commands/conformance.ts
12804
- import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
12913
+ import { existsSync as existsSync20, readFileSync as readFileSync16 } from "fs";
12805
12914
  import { dirname as dirname13, resolve as resolve23 } from "path";
12806
12915
  import { fileURLToPath as fileURLToPath4 } from "url";
12807
12916
  import { Command as Command5, Option as Option5 } from "clipanion";
12808
12917
 
12809
12918
  // conformance/index.ts
12810
12919
  import { spawnSync as spawnSync2 } from "child_process";
12811
- import { cpSync, existsSync as existsSync17, mkdtempSync, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync, statSync as statSync3 } from "fs";
12920
+ import { cpSync, existsSync as existsSync18, mkdtempSync, readdirSync as readdirSync6, readFileSync as readFileSync15, rmSync, statSync as statSync3 } from "fs";
12812
12921
  import { tmpdir } from "os";
12813
12922
  import { isAbsolute as isAbsolute6, join as join11, relative as relative3, resolve as resolve21 } from "path";
12923
+ import { DatabaseSync as DatabaseSync7 } from "node:sqlite";
12814
12924
 
12815
12925
  // conformance/i18n/runner.texts.ts
12816
12926
  var CONFORMANCE_RUNNER_TEXTS = {
@@ -12902,6 +13012,11 @@ function runConformanceCase(options) {
12902
13012
  if (c.fixture) {
12903
13013
  replaceFixture(scope, fixturesRoot, c.fixture);
12904
13014
  }
13015
+ grantFixturePluginTrust(scope, options.binary, {
13016
+ ...pickSafeEnv(process.env),
13017
+ ...options.env,
13018
+ ...setupEnv
13019
+ });
12905
13020
  const argv = [c.invoke.verb];
12906
13021
  if (c.invoke.sub) argv.push(c.invoke.sub);
12907
13022
  if (c.invoke.args) argv.push(...c.invoke.args);
@@ -12971,6 +13086,40 @@ function replaceFixture(scope, fixturesRoot, fixture) {
12971
13086
  const src = join11(fixturesRoot, fixture);
12972
13087
  cpSync(src, scope, { recursive: true });
12973
13088
  }
13089
+ function grantFixturePluginTrust(scope, binary, env) {
13090
+ const pluginsDir = join11(scope, KERNEL_SKILL_MAP_DIR, "plugins");
13091
+ if (!existsSync18(pluginsDir)) return;
13092
+ const ids = readdirSync6(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync18(join11(pluginsDir, e.name, "plugin.json"))).map((e) => e.name);
13093
+ if (ids.length === 0) return;
13094
+ const dbPath = join11(scope, KERNEL_SKILL_MAP_DIR, "skill-map.db");
13095
+ if (!existsSync18(dbPath)) {
13096
+ spawnSync2(process.execPath, [binary, "init", "--no-scan"], { cwd: scope, env, encoding: "utf8" });
13097
+ }
13098
+ if (!hasConfigPluginsTable(dbPath)) {
13099
+ spawnSync2(process.execPath, [binary, "scan"], { cwd: scope, env, encoding: "utf8" });
13100
+ }
13101
+ const db = new DatabaseSync7(dbPath);
13102
+ try {
13103
+ const stmt = db.prepare(
13104
+ "INSERT INTO config_plugins (plugin_id, enabled, updated_at) VALUES (?, 1, 0) ON CONFLICT(plugin_id) DO UPDATE SET enabled = 1"
13105
+ );
13106
+ for (const id of ids) stmt.run(id);
13107
+ } finally {
13108
+ db.close();
13109
+ }
13110
+ }
13111
+ function hasConfigPluginsTable(dbPath) {
13112
+ if (!existsSync18(dbPath)) return false;
13113
+ const db = new DatabaseSync7(dbPath, { readOnly: true });
13114
+ try {
13115
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'config_plugins'").get();
13116
+ return row !== void 0;
13117
+ } catch {
13118
+ return false;
13119
+ } finally {
13120
+ db.close();
13121
+ }
13122
+ }
12974
13123
  function assertContained2(root, rel, label) {
12975
13124
  if (isAbsolute6(rel)) {
12976
13125
  throw new Error(
@@ -13005,7 +13154,7 @@ function evaluateAssertion(a, ctx) {
13005
13154
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
13006
13155
  }
13007
13156
  const abs = resolve21(ctx.scope, a.path);
13008
- return existsSync17(abs) ? { ok: true, type: a.type } : {
13157
+ return existsSync18(abs) ? { ok: true, type: a.type } : {
13009
13158
  ok: false,
13010
13159
  type: a.type,
13011
13160
  reason: tx(CONFORMANCE_RUNNER_TEXTS.fileNotFound, { path: a.path })
@@ -13020,7 +13169,7 @@ function evaluateAssertion(a, ctx) {
13020
13169
  }
13021
13170
  const fixturePath = join11(ctx.fixturesRoot, a.fixture);
13022
13171
  const targetPath = resolve21(ctx.scope, a.path);
13023
- if (!existsSync17(targetPath)) {
13172
+ if (!existsSync18(targetPath)) {
13024
13173
  return {
13025
13174
  ok: false,
13026
13175
  type: a.type,
@@ -13201,7 +13350,7 @@ var CONFORMANCE_TEXTS = {
13201
13350
  };
13202
13351
 
13203
13352
  // cli/util/conformance-scopes.ts
13204
- import { existsSync as existsSync18, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
13353
+ import { existsSync as existsSync19, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
13205
13354
  import { dirname as dirname12, resolve as resolve22 } from "path";
13206
13355
  import { createRequire as createRequire6 } from "module";
13207
13356
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -13221,7 +13370,7 @@ function resolveCliWorkspaceRoot() {
13221
13370
  let cursor = here;
13222
13371
  for (let depth = 0; depth < 6; depth += 1) {
13223
13372
  const candidate = resolve22(cursor, "plugins");
13224
- if (existsSync18(candidate) && statSync4(candidate).isDirectory()) {
13373
+ if (existsSync19(candidate) && statSync4(candidate).isDirectory()) {
13225
13374
  return cursor;
13226
13375
  }
13227
13376
  const parent = dirname12(cursor);
@@ -13241,7 +13390,7 @@ function collectProviderScopes(specRoot) {
13241
13390
  return out;
13242
13391
  }
13243
13392
  const pluginsRoot = resolve22(workspaceRoot, "plugins");
13244
- if (!existsSync18(pluginsRoot)) return out;
13393
+ if (!existsSync19(pluginsRoot)) return out;
13245
13394
  for (const pluginEntry of readdirSync7(pluginsRoot)) {
13246
13395
  const pluginDir = resolve22(pluginsRoot, pluginEntry);
13247
13396
  if (!isDir(pluginDir)) continue;
@@ -13253,7 +13402,7 @@ function collectProviderScopes(specRoot) {
13253
13402
  }
13254
13403
  function isDir(path) {
13255
13404
  try {
13256
- return existsSync18(path) && statSync4(path).isDirectory();
13405
+ return existsSync19(path) && statSync4(path).isDirectory();
13257
13406
  } catch {
13258
13407
  return false;
13259
13408
  }
@@ -13263,10 +13412,10 @@ function collectPluginProviderScopes(providersRoot, specRoot, out) {
13263
13412
  const providerDir = resolve22(providersRoot, entry);
13264
13413
  if (!isDir(providerDir)) continue;
13265
13414
  const conformanceDir = resolve22(providerDir, "conformance");
13266
- if (!existsSync18(conformanceDir)) continue;
13415
+ if (!existsSync19(conformanceDir)) continue;
13267
13416
  const casesDir = resolve22(conformanceDir, "cases");
13268
13417
  const fixturesDir = resolve22(conformanceDir, "fixtures");
13269
- if (!existsSync18(casesDir) || !existsSync18(fixturesDir)) continue;
13418
+ if (!existsSync19(casesDir) || !existsSync19(fixturesDir)) continue;
13270
13419
  out.push({
13271
13420
  id: `provider:${entry}`,
13272
13421
  kind: "provider",
@@ -13304,7 +13453,7 @@ function selectConformanceScopes(scope) {
13304
13453
  return [match];
13305
13454
  }
13306
13455
  function listCaseFiles(scope) {
13307
- if (!existsSync18(scope.casesDir)) return [];
13456
+ if (!existsSync19(scope.casesDir)) return [];
13308
13457
  return readdirSync7(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve22(scope.casesDir, entry));
13309
13458
  }
13310
13459
 
@@ -13323,7 +13472,7 @@ function resolveBinary() {
13323
13472
  let cursor = here;
13324
13473
  for (let depth = 0; depth < 6; depth += 1) {
13325
13474
  const candidate = resolve23(cursor, "bin", "sm.js");
13326
- if (existsSync19(candidate)) return candidate;
13475
+ if (existsSync20(candidate)) return candidate;
13327
13476
  const parent = dirname13(cursor);
13328
13477
  if (parent === cursor) break;
13329
13478
  cursor = parent;
@@ -13389,7 +13538,7 @@ var ConformanceRunCommand = class extends SmCommand {
13389
13538
  return ExitCode.Error;
13390
13539
  }
13391
13540
  const binary = resolveBinary();
13392
- if (!existsSync19(binary)) {
13541
+ if (!existsSync20(binary)) {
13393
13542
  if (this.json) {
13394
13543
  this.#emitJsonError(
13395
13544
  "internal",
@@ -13607,6 +13756,20 @@ var DB_TEXTS = {
13607
13756
  */
13608
13757
  restoreSourceNotFound: "{{glyph}} Backup not found: {{sourcePath}}\n {{hint}}\n",
13609
13758
  restoreSourceNotFoundHint: "Run `sm db backup` first, or pick an existing file (the default backups directory is `.skill-map/backups/`).",
13759
+ /**
13760
+ * Source exists but is not a SQLite database (missing the header).
13761
+ * Restoring it would swap a non-DB into place; refuse with exit 2.
13762
+ */
13763
+ restoreSourceNotSqlite: "{{glyph}} Not a SQLite database: {{sourcePath}}\n {{hint}}\n",
13764
+ restoreSourceNotSqliteHint: "The file is missing the SQLite header. Pick a real backup (created by `sm db backup`).",
13765
+ /**
13766
+ * Source is a valid DB but was written by a CLI this binary cannot
13767
+ * read forward (newer minor or different major). Refuse with exit 2.
13768
+ */
13769
+ restoreSourceVersionSkew: "{{glyph}} Refusing to restore {{sourcePath}}\n {{detail}}\n {{hint}}\n",
13770
+ restoreSourceVersionNewerDetail: "It was written by skill-map {{dbVersion}}, newer than this CLI ({{currentVersion}}).",
13771
+ restoreSourceVersionMajorDetail: "It was written by skill-map {{dbVersion}}, a different major than this CLI ({{currentVersion}}).",
13772
+ restoreSourceVersionSkewHint: "Upgrade `sm` to a matching version before restoring this backup.",
13610
13773
  restoreConfirm: "Restore {{sourcePath}} over {{target}}? This overwrites the current DB.",
13611
13774
  restoreDone: "{{glyph}} Restored {{sourcePath}} \u2192 {{target}}\n",
13612
13775
  // --- shared ----------------------------------------------------------
@@ -13734,6 +13897,39 @@ async function statOrNull(path) {
13734
13897
  }
13735
13898
  }
13736
13899
 
13900
+ // core/sqlite/restore-validation.ts
13901
+ import { open } from "fs/promises";
13902
+ var SQLITE_MAGIC_PREFIX = "SQLite format 3";
13903
+ async function validateRestorableDb(sourcePath, currentVersion) {
13904
+ if (!await hasSqliteHeader(sourcePath)) {
13905
+ return { ok: false, reason: "not-sqlite" };
13906
+ }
13907
+ const dbVersion = readScannedByVersion(sourcePath);
13908
+ if (dbVersion === null) return { ok: true };
13909
+ const outcome = classifyVersionSkew(dbVersion, currentVersion);
13910
+ if (outcome.kind === "error-newer") {
13911
+ return { ok: false, reason: "version-newer", dbVersion, currentVersion };
13912
+ }
13913
+ if (outcome.kind === "error-major") {
13914
+ return { ok: false, reason: "version-major", dbVersion, currentVersion };
13915
+ }
13916
+ return { ok: true };
13917
+ }
13918
+ async function hasSqliteHeader(sourcePath) {
13919
+ let handle = null;
13920
+ try {
13921
+ handle = await open(sourcePath, "r");
13922
+ const buf = Buffer.alloc(16);
13923
+ const { bytesRead } = await handle.read(buf, 0, 16, 0);
13924
+ if (bytesRead === 0) return true;
13925
+ return buf.toString("latin1", 0, 15) === SQLITE_MAGIC_PREFIX && buf[15] === 0;
13926
+ } catch {
13927
+ return false;
13928
+ } finally {
13929
+ await handle?.close();
13930
+ }
13931
+ }
13932
+
13737
13933
  // cli/commands/db/restore.ts
13738
13934
  async function chmodOwnerOnlyBestEffort(target) {
13739
13935
  try {
@@ -13774,19 +13970,13 @@ var DbRestoreCommand = class extends SmCommand {
13774
13970
  );
13775
13971
  return ExitCode.NotFound;
13776
13972
  }
13973
+ const validation = await validateRestorableDb(sourcePath, VERSION);
13974
+ if (!validation.ok) {
13975
+ this.printValidationError(validation, sourcePath);
13976
+ return ExitCode.Error;
13977
+ }
13777
13978
  if (this.dryRun) {
13778
- this.printer.data(DB_TEXTS.dryRunHeader);
13779
- const sourceBytes = sourceStat.size;
13780
- const targetClause = await pathExists(target) ? DB_TEXTS.dryRunRestoreTargetExistsClause : DB_TEXTS.dryRunRestoreTargetMissingClause;
13781
- this.printer.data(
13782
- tx(DB_TEXTS.dryRunRestoreWouldOverwrite, {
13783
- sourcePath,
13784
- sourceBytes,
13785
- target,
13786
- targetClause
13787
- })
13788
- );
13789
- return ExitCode.Ok;
13979
+ return this.previewRestore(sourcePath, sourceStat.size, target);
13790
13980
  }
13791
13981
  if (!this.yes) {
13792
13982
  const ok = await confirm(tx(DB_TEXTS.restoreConfirm, { sourcePath, target }), {
@@ -13815,21 +14005,65 @@ var DbRestoreCommand = class extends SmCommand {
13815
14005
  );
13816
14006
  return ExitCode.Ok;
13817
14007
  }
14008
+ /**
14009
+ * `--dry-run` preview: report the source size and whether the target
14010
+ * would be created or overwritten, without copying or unlinking. The
14011
+ * validation gate has already run, so this only describes the swap.
14012
+ */
14013
+ async previewRestore(sourcePath, sourceBytes, target) {
14014
+ this.printer.data(DB_TEXTS.dryRunHeader);
14015
+ const targetClause = await pathExists(target) ? DB_TEXTS.dryRunRestoreTargetExistsClause : DB_TEXTS.dryRunRestoreTargetMissingClause;
14016
+ this.printer.data(
14017
+ tx(DB_TEXTS.dryRunRestoreWouldOverwrite, {
14018
+ sourcePath,
14019
+ sourceBytes,
14020
+ target,
14021
+ targetClause
14022
+ })
14023
+ );
14024
+ return ExitCode.Ok;
14025
+ }
14026
+ /** Render a `validateRestorableDb` rejection to stderr. */
14027
+ printValidationError(validation, sourcePath) {
14028
+ const ansi = this.ansiFor("stderr");
14029
+ if (validation.reason === "not-sqlite") {
14030
+ this.printer.error(
14031
+ tx(DB_TEXTS.restoreSourceNotSqlite, {
14032
+ glyph: ansi.red("\u2715"),
14033
+ sourcePath,
14034
+ hint: ansi.dim(DB_TEXTS.restoreSourceNotSqliteHint)
14035
+ })
14036
+ );
14037
+ return;
14038
+ }
14039
+ const detailTemplate = validation.reason === "version-newer" ? DB_TEXTS.restoreSourceVersionNewerDetail : DB_TEXTS.restoreSourceVersionMajorDetail;
14040
+ this.printer.error(
14041
+ tx(DB_TEXTS.restoreSourceVersionSkew, {
14042
+ glyph: ansi.red("\u2715"),
14043
+ sourcePath,
14044
+ detail: tx(detailTemplate, {
14045
+ dbVersion: validation.dbVersion,
14046
+ currentVersion: validation.currentVersion
14047
+ }),
14048
+ hint: ansi.dim(DB_TEXTS.restoreSourceVersionSkewHint)
14049
+ })
14050
+ );
14051
+ }
13818
14052
  };
13819
14053
 
13820
14054
  // cli/commands/db/reset.ts
13821
- import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
14055
+ import { DatabaseSync as DatabaseSync8 } from "node:sqlite";
13822
14056
  import { Command as Command8, Option as Option8 } from "clipanion";
13823
14057
 
13824
14058
  // core/sqlite/db-files.ts
13825
- import { existsSync as existsSync20 } from "fs";
14059
+ import { existsSync as existsSync21 } from "fs";
13826
14060
  import { rm as rm2 } from "fs/promises";
13827
14061
  var DB_FILE_SUFFIXES = ["", "-wal", "-shm"];
13828
14062
  async function removeDbFiles(dbPath) {
13829
14063
  if (dbPath === ":memory:") return;
13830
14064
  for (const suffix of DB_FILE_SUFFIXES) {
13831
14065
  const p = `${dbPath}${suffix}`;
13832
- if (existsSync20(p)) await rm2(p);
14066
+ if (existsSync21(p)) await rm2(p);
13833
14067
  }
13834
14068
  }
13835
14069
 
@@ -13915,7 +14149,7 @@ var DbResetCommand = class extends SmCommand {
13915
14149
  return ExitCode.Error;
13916
14150
  }
13917
14151
  }
13918
- const db = new DatabaseSync6(path);
14152
+ const db = new DatabaseSync8(path);
13919
14153
  try {
13920
14154
  const rows = db.prepare(
13921
14155
  "SELECT name FROM sqlite_master WHERE type='table' AND (name LIKE 'scan\\_%' ESCAPE '\\'" + (this.state ? " OR name LIKE 'state\\_%' ESCAPE '\\'" : "") + ")"
@@ -14058,7 +14292,7 @@ var DbBrowserCommand = class extends SmCommand {
14058
14292
  };
14059
14293
 
14060
14294
  // cli/commands/db/dump.ts
14061
- import { DatabaseSync as DatabaseSync7 } from "node:sqlite";
14295
+ import { DatabaseSync as DatabaseSync9 } from "node:sqlite";
14062
14296
  import { Command as Command11, Option as Option10 } from "clipanion";
14063
14297
  var DbDumpCommand = class extends SmCommand {
14064
14298
  static paths = [["db", "dump"]];
@@ -14100,7 +14334,7 @@ var DbDumpCommand = class extends SmCommand {
14100
14334
  }
14101
14335
  };
14102
14336
  function dumpDatabaseToStream(dbPath, out, tables) {
14103
- const db = new DatabaseSync7(dbPath, { readOnly: true });
14337
+ const db = new DatabaseSync9(dbPath, { readOnly: true });
14104
14338
  try {
14105
14339
  out.write("PRAGMA foreign_keys=OFF;\n");
14106
14340
  out.write("BEGIN TRANSACTION;\n");
@@ -14175,6 +14409,10 @@ function tryParseNonNegativeInt(raw) {
14175
14409
  }
14176
14410
  return parsed;
14177
14411
  }
14412
+ function tryParsePositiveInt(raw) {
14413
+ const parsed = tryParseNonNegativeInt(raw);
14414
+ return parsed === null || parsed === 0 ? null : parsed;
14415
+ }
14178
14416
  function parsePositiveIntegerOption(raw, label, stderr) {
14179
14417
  const parsed = tryParseNonNegativeInt(raw);
14180
14418
  if (parsed === null || parsed === 0) {
@@ -14435,7 +14673,7 @@ var DB_COMMANDS = [
14435
14673
  ];
14436
14674
 
14437
14675
  // cli/commands/example.ts
14438
- import { cpSync as cpSync2, existsSync as existsSync21, statSync as statSync5 } from "fs";
14676
+ import { cpSync as cpSync2, existsSync as existsSync22, statSync as statSync5 } from "fs";
14439
14677
  import { dirname as dirname16, relative as relative5, resolve as resolve27 } from "path";
14440
14678
  import { fileURLToPath as fileURLToPath5 } from "url";
14441
14679
  import { Command as Command13, Option as Option12 } from "clipanion";
@@ -14739,7 +14977,7 @@ function resolveExampleSourceDir() {
14739
14977
  resolve27(here, "../cli/example")
14740
14978
  ];
14741
14979
  for (const candidate of candidates) {
14742
- if (existsSync21(candidate) && statSync5(candidate).isDirectory()) {
14980
+ if (existsSync22(candidate) && statSync5(candidate).isDirectory()) {
14743
14981
  cachedSourceDir = candidate;
14744
14982
  return candidate;
14745
14983
  }
@@ -14751,7 +14989,7 @@ function resolveExampleSourceDir() {
14751
14989
  function isExamplePayloadEntry(sourceRoot, src) {
14752
14990
  const rel = relative5(sourceRoot, src);
14753
14991
  if (rel === "") return true;
14754
- return rel.split(/[\\/]/)[0] !== ".skill-map";
14992
+ return rel.split(/[\\/]/)[0] !== SKILL_MAP_DIR;
14755
14993
  }
14756
14994
 
14757
14995
  // cli/commands/export.ts
@@ -16083,7 +16321,7 @@ import { join as join16 } from "path";
16083
16321
  import { Command as Command18, Option as Option17 } from "clipanion";
16084
16322
 
16085
16323
  // kernel/orchestrator/index.ts
16086
- import { existsSync as existsSync24, statSync as statSync7 } from "fs";
16324
+ import { existsSync as existsSync25, statSync as statSync7 } from "fs";
16087
16325
  import { isAbsolute as isAbsolute10, resolve as resolve32 } from "path";
16088
16326
  import { Tiktoken as Tiktoken2 } from "js-tiktoken/lite";
16089
16327
 
@@ -17453,7 +17691,7 @@ function computeDriftStatus(args2) {
17453
17691
  }
17454
17692
 
17455
17693
  // kernel/sidecar/discover-orphans.ts
17456
- import { existsSync as existsSync22, readdirSync as readdirSync8, statSync as statSync6 } from "fs";
17694
+ import { existsSync as existsSync23, readdirSync as readdirSync8, statSync as statSync6 } from "fs";
17457
17695
  import { join as join13, relative as relative6, sep as sep4 } from "path";
17458
17696
  function discoverOrphanSidecars(roots, shouldSkip) {
17459
17697
  const out = [];
@@ -17481,7 +17719,7 @@ function walk2(root, current, shouldSkip, out) {
17481
17719
  if (!entry.isFile()) continue;
17482
17720
  if (!entry.name.endsWith(".sm")) continue;
17483
17721
  const expectedMd = `${full.slice(0, -".sm".length)}.md`;
17484
- if (existsSync22(expectedMd) && safeIsFile(expectedMd)) continue;
17722
+ if (existsSync23(expectedMd) && safeIsFile(expectedMd)) continue;
17485
17723
  out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
17486
17724
  }
17487
17725
  }
@@ -17495,10 +17733,10 @@ function safeIsFile(path) {
17495
17733
 
17496
17734
  // kernel/orchestrator/node-build.ts
17497
17735
  import { createHash as createHash2 } from "crypto";
17498
- import { existsSync as existsSync23 } from "fs";
17736
+ import { existsSync as existsSync24 } from "fs";
17499
17737
  import { isAbsolute as isAbsolute8, resolve as resolvePath } from "path";
17500
17738
  import "js-tiktoken/lite";
17501
- import yaml4 from "js-yaml";
17739
+ import { dump as yamlDump2, CORE_SCHEMA as CORE_SCHEMA2 } from "js-yaml";
17502
17740
 
17503
17741
  // kernel/orchestrator/frontmatter.ts
17504
17742
  function validateFrontmatter(providerFrontmatter, provider, kind, frontmatter, path, strict) {
@@ -17591,22 +17829,22 @@ function canonicalFrontmatter(parsed, raw) {
17591
17829
  if (!hasParsedKeys && hasRawText) {
17592
17830
  return raw;
17593
17831
  }
17594
- return yaml4.dump(parsed, {
17832
+ return yamlDump2(parsed, {
17595
17833
  sortKeys: true,
17596
17834
  lineWidth: -1,
17597
17835
  noRefs: true,
17598
- noCompatMode: true
17836
+ schema: CORE_SCHEMA2
17599
17837
  });
17600
17838
  }
17601
17839
  function canonicalSidecarAnnotations(annotations) {
17602
17840
  if (!annotations || typeof annotations !== "object" || Array.isArray(annotations)) {
17603
- return yaml4.dump({}, { sortKeys: true, lineWidth: -1, noRefs: true, noCompatMode: true });
17841
+ return yamlDump2({}, { sortKeys: true, lineWidth: -1, noRefs: true, schema: CORE_SCHEMA2 });
17604
17842
  }
17605
- return yaml4.dump(annotations, {
17843
+ return yamlDump2(annotations, {
17606
17844
  sortKeys: true,
17607
17845
  lineWidth: -1,
17608
17846
  noRefs: true,
17609
- noCompatMode: true
17847
+ schema: CORE_SCHEMA2
17610
17848
  });
17611
17849
  }
17612
17850
  function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyHash, liveFrontmatterHash) {
@@ -17660,11 +17898,11 @@ function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyH
17660
17898
  }
17661
17899
  function resolveAbsoluteMdPath(relativePath2, roots) {
17662
17900
  if (isAbsolute8(relativePath2)) {
17663
- return existsSync23(relativePath2) ? relativePath2 : null;
17901
+ return existsSync24(relativePath2) ? relativePath2 : null;
17664
17902
  }
17665
17903
  for (const root of roots) {
17666
17904
  const candidate = resolvePath(root, relativePath2);
17667
- if (existsSync23(candidate)) return candidate;
17905
+ if (existsSync24(candidate)) return candidate;
17668
17906
  }
17669
17907
  return null;
17670
17908
  }
@@ -18477,7 +18715,7 @@ function validateRoots(roots) {
18477
18715
  throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
18478
18716
  }
18479
18717
  for (const root of roots) {
18480
- if (!existsSync24(root) || !statSync7(root).isDirectory()) {
18718
+ if (!existsSync25(root) || !statSync7(root).isDirectory()) {
18481
18719
  throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
18482
18720
  }
18483
18721
  }
@@ -18486,7 +18724,7 @@ function resolveActiveProviderOption(optionValue, roots, providers) {
18486
18724
  if (optionValue !== void 0) return optionValue;
18487
18725
  for (const root of roots) {
18488
18726
  const absRoot = isAbsolute10(root) ? root : resolve32(root);
18489
- if (!existsSync24(absRoot)) continue;
18727
+ if (!existsSync25(absRoot)) continue;
18490
18728
  const detected = detectProvidersFromFilesystem(absRoot, providers)[0] ?? null;
18491
18729
  if (detected !== null) return detected;
18492
18730
  }
@@ -19232,9 +19470,7 @@ async function promptForLens(detected, stdin, stderr, warnGlyph) {
19232
19470
  }
19233
19471
 
19234
19472
  // core/sqlite/db-drift-reset.ts
19235
- import { existsSync as existsSync25 } from "fs";
19236
19473
  import { createInterface as createInterface5 } from "readline";
19237
- import { DatabaseSync as DatabaseSync8 } from "node:sqlite";
19238
19474
 
19239
19475
  // core/sqlite/i18n/db-drift.texts.ts
19240
19476
  var DB_DRIFT_TEXTS = {
@@ -19283,20 +19519,6 @@ function detectDriftReason(dbPath, currentVersion) {
19283
19519
  }
19284
19520
  return classifyFingerprint(dbPath).kind === "drift" ? "schema" : null;
19285
19521
  }
19286
- function readScannedByVersion(dbPath) {
19287
- if (dbPath === ":memory:" || !existsSync25(dbPath)) return null;
19288
- let raw = null;
19289
- try {
19290
- raw = new DatabaseSync8(dbPath, { readOnly: true });
19291
- const row = raw.prepare("SELECT scanned_by_version AS v FROM scan_meta LIMIT 1").get();
19292
- const v = row?.v;
19293
- return typeof v === "string" && v.length > 0 ? v : null;
19294
- } catch {
19295
- return null;
19296
- } finally {
19297
- raw?.close();
19298
- }
19299
- }
19300
19522
  function reasonText(reason) {
19301
19523
  return reason === "version" ? DB_DRIFT_TEXTS.driftReasonVersion : DB_DRIFT_TEXTS.driftReasonSchema;
19302
19524
  }
@@ -25241,8 +25463,8 @@ function parseBreakerLimit(raw, stderr, noColor) {
25241
25463
  }
25242
25464
  function parseMaxScanLimit(raw, stderr, noColor) {
25243
25465
  if (raw === void 0) return void 0;
25244
- const n = Number(raw);
25245
- if (!Number.isInteger(n) || n < 1) {
25466
+ const n = tryParsePositiveInt(raw);
25467
+ if (n === null) {
25246
25468
  const stderrTty = stderr;
25247
25469
  const ansi = ansiFor({ isTTY: stderrTty.isTTY === true, noColorFlag: noColor });
25248
25470
  stderr.write(
@@ -25258,8 +25480,8 @@ function parseMaxScanLimit(raw, stderr, noColor) {
25258
25480
  }
25259
25481
  function parseMaxNodesLimit(raw, stderr, noColor) {
25260
25482
  if (raw === void 0) return void 0;
25261
- const n = Number(raw);
25262
- if (!Number.isInteger(n) || n < 1) {
25483
+ const n = tryParsePositiveInt(raw);
25484
+ if (n === null) {
25263
25485
  const stderrTty = stderr;
25264
25486
  const ansi = ansiFor({ isTTY: stderrTty.isTTY === true, noColorFlag: noColor });
25265
25487
  stderr.write(
@@ -25426,8 +25648,8 @@ var ScanCommand = class extends SmCommand {
25426
25648
  */
25427
25649
  parseIntegerFlag(raw, invalidTemplate, invalidHint) {
25428
25650
  if (raw === void 0) return { kind: "ok", value: void 0 };
25429
- const n = Number(raw);
25430
- if (!Number.isInteger(n) || n < 1) {
25651
+ const n = tryParsePositiveInt(raw);
25652
+ if (n === null) {
25431
25653
  const ansi = this.ansiFor("stderr");
25432
25654
  this.printer.info(
25433
25655
  tx(invalidTemplate, {
@@ -26403,7 +26625,7 @@ function originGuarded(path) {
26403
26625
  }
26404
26626
 
26405
26627
  // server/security-headers.ts
26406
- var DEFAULT_CSP = "frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
26628
+ var DEFAULT_CSP = "frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'";
26407
26629
  function createSecurityHeaders() {
26408
26630
  return async (c, next) => {
26409
26631
  await next();
@@ -27054,7 +27276,7 @@ import { HTTPException as HTTPException8 } from "hono/http-exception";
27054
27276
 
27055
27277
  // server/node-body.ts
27056
27278
  import { constants as fsConstants2 } from "fs";
27057
- import { open } from "fs/promises";
27279
+ import { open as open2 } from "fs/promises";
27058
27280
  import { isAbsolute as isAbsolute14, resolve as resolvePath2, relative as relativePath, sep as sep8 } from "path";
27059
27281
  async function readNodeBody(cwd, relPath) {
27060
27282
  if (isAbsolute14(relPath)) return null;
@@ -27067,7 +27289,7 @@ async function readNodeBody(cwd, relPath) {
27067
27289
  let raw;
27068
27290
  let handle = null;
27069
27291
  try {
27070
- handle = await open(absFile, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
27292
+ handle = await open2(absFile, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
27071
27293
  raw = await handle.readFile("utf-8");
27072
27294
  } catch (err) {
27073
27295
  if (isExpectedFsError(err)) return null;
@@ -28526,7 +28748,7 @@ var parsePatchBody5 = makeBodyValidator(PATCH_BODY_SCHEMA4, {
28526
28748
 
28527
28749
  // server/routes/actions.ts
28528
28750
  import { HTTPException as HTTPException16 } from "hono/http-exception";
28529
- import { resolve as resolve41 } from "path";
28751
+ import { relative as relative10, resolve as resolve41 } from "path";
28530
28752
 
28531
28753
  // server/routes/node-loader.ts
28532
28754
  import { HTTPException as HTTPException15 } from "hono/http-exception";
@@ -28658,7 +28880,18 @@ function invokeAction(action, absPath, node, body, cwd) {
28658
28880
  };
28659
28881
  return invoke(body.input ?? {}, ctx);
28660
28882
  }
28883
+ function assertSidecarWritesContained(writes, cwd) {
28884
+ for (const w of writes ?? []) {
28885
+ if (w.kind !== "sidecar") continue;
28886
+ try {
28887
+ assertContained(cwd, relative10(cwd, w.path));
28888
+ } catch (err) {
28889
+ throw new HTTPException16(400, { message: formatErrorMessage(err) });
28890
+ }
28891
+ }
28892
+ }
28661
28893
  async function materializeWrites(writes, body, cwd) {
28894
+ assertSidecarWritesContained(writes, cwd);
28662
28895
  const store = new FilesystemSidecarStore(ensureSidecarWritesAllowed);
28663
28896
  try {
28664
28897
  for (const w of writes ?? []) {
@@ -30203,6 +30436,12 @@ function listenAsync(fetchCallback, wss, host, port) {
30203
30436
  fetch: fetchCallback,
30204
30437
  hostname: host,
30205
30438
  port,
30439
+ // `wss` IS a structural `WebSocketServerLike` at runtime (created
30440
+ // with `{ noServer: true }` above). The cast bridges a pure
30441
+ // type-skew: @hono/node-server 2.0.6 declares
30442
+ // `WebSocketServerLike.options.noServer` as `boolean` while
30443
+ // @types/ws declares it `boolean | undefined`, which our
30444
+ // `exactOptionalPropertyTypes: true` tsconfig rejects.
30206
30445
  websocket: { server: wss }
30207
30446
  },
30208
30447
  () => {
@@ -30520,7 +30759,7 @@ var ServeCommand = class extends SmCommand {
30520
30759
  );
30521
30760
  return ExitCode.Error;
30522
30761
  }
30523
- const maxScanResult = parseMaxScan(this.maxScan);
30762
+ const maxScanResult = parseMaxIntFlag(this.maxScan);
30524
30763
  if (!maxScanResult.ok) {
30525
30764
  this.printer.info(
30526
30765
  tx(SERVE_TEXTS.maxScanInvalid, {
@@ -30531,7 +30770,7 @@ var ServeCommand = class extends SmCommand {
30531
30770
  );
30532
30771
  return ExitCode.Error;
30533
30772
  }
30534
- const maxNodesResult = parseMaxNodes(this.maxNodes);
30773
+ const maxNodesResult = parseMaxIntFlag(this.maxNodes);
30535
30774
  if (!maxNodesResult.ok) {
30536
30775
  this.printer.info(
30537
30776
  tx(SERVE_TEXTS.maxNodesInvalid, {
@@ -30660,16 +30899,10 @@ function parseDebounce(raw) {
30660
30899
  if (parsed === null) return { ok: false, value: raw };
30661
30900
  return { ok: true, value: parsed };
30662
30901
  }
30663
- function parseMaxScan(raw) {
30902
+ function parseMaxIntFlag(raw) {
30664
30903
  if (raw === void 0) return { ok: true, value: void 0 };
30665
- const n = Number(raw);
30666
- if (!Number.isInteger(n) || n < 1) return { ok: false, value: raw };
30667
- return { ok: true, value: n };
30668
- }
30669
- function parseMaxNodes(raw) {
30670
- if (raw === void 0) return { ok: true, value: void 0 };
30671
- const n = Number(raw);
30672
- if (!Number.isInteger(n) || n < 1) return { ok: false, value: raw };
30904
+ const n = tryParsePositiveInt(raw);
30905
+ if (n === null) return { ok: false, value: raw };
30673
30906
  return { ok: true, value: n };
30674
30907
  }
30675
30908
  function resolveUiDist(ctx, raw) {
@@ -31829,13 +32062,15 @@ var TUTORIAL_TEXTS = {
31829
32062
  writtenLabelEn: "English",
31830
32063
  writtenLabelEs: "Espa\xF1ol",
31831
32064
  // Destination-provider prompt (interactive stdin, no `--for`). Header
31832
- // uses a yellow `?` glyph; options are a numbered list of provider
31833
- // label (with any `aka` agents in parentheses) + skill directory, with
31834
- // a `(default)` marker on the first option (Claude). The input line
31835
- // accepts a number, a provider id, or an empty answer (which takes the
31836
- // default).
32065
+ // uses a yellow `?` glyph; options are a numbered list of the provider's
32066
+ // vendor name (for a provider with an `aka`, the aka vendor leads and the
32067
+ // provider label follows in parentheses), with a `(default)` marker on
32068
+ // the first option (Claude). The destination folder is deliberately NOT
32069
+ // shown: several providers share `.agents/skills`, so the folder does not
32070
+ // identify the lens. The input line accepts a number, a provider id, or
32071
+ // an empty answer (which takes the default).
31837
32072
  promptHeader: "{{glyph}} Which agent should host the tutorial skill?",
31838
- promptOption: " {{index}}) {{label}}: {{skillDir}}{{marker}}",
32073
+ promptOption: " {{index}}) {{label}}{{marker}}",
31839
32074
  promptDefaultMarker: " (default)",
31840
32075
  promptInput: " Enter the number or provider id [default {{index}}]: ",
31841
32076
  // Prompt answer matched neither an index nor an id. Goes to stderr,
@@ -31966,9 +32201,7 @@ var TutorialCommand = class extends SmCommand {
31966
32201
  return ExitCode.Error;
31967
32202
  }
31968
32203
  try {
31969
- rmSync2(targetDir, { recursive: true, force: true });
31970
- mkdirSync6(dirname21(targetDir), { recursive: true });
31971
- cpSync3(sourceDir, targetDir, { recursive: true });
32204
+ materializeSkillFolder(sourceDir, targetDir, ctx.cwd, target.marker);
31972
32205
  } catch (err) {
31973
32206
  this.printer.error(
31974
32207
  tx(TUTORIAL_TEXTS.writeFailed, {
@@ -32059,6 +32292,14 @@ var TutorialCommand = class extends SmCommand {
32059
32292
  return picked;
32060
32293
  }
32061
32294
  };
32295
+ function materializeSkillFolder(sourceDir, targetDir, cwd, marker) {
32296
+ rmSync2(targetDir, { recursive: true, force: true });
32297
+ mkdirSync6(dirname21(targetDir), { recursive: true });
32298
+ cpSync3(sourceDir, targetDir, { recursive: true });
32299
+ if (marker !== void 0) {
32300
+ mkdirSync6(join21(cwd, marker), { recursive: true });
32301
+ }
32302
+ }
32062
32303
  function toScaffoldTarget(provider, includeExperimental) {
32063
32304
  const scaffold = provider.scaffold;
32064
32305
  if (!scaffold || !scaffold.skillDir) return null;
@@ -32067,6 +32308,7 @@ function toScaffoldTarget(provider, includeExperimental) {
32067
32308
  id: provider.id,
32068
32309
  label: provider.presentation.label,
32069
32310
  skillDir: scaffold.skillDir,
32311
+ ...scaffold.marker !== void 0 ? { marker: scaffold.marker } : {},
32070
32312
  aka: scaffold.aka ?? []
32071
32313
  };
32072
32314
  }
@@ -32079,7 +32321,7 @@ function listScaffoldTargets(includeExperimental = false) {
32079
32321
  return out;
32080
32322
  }
32081
32323
  function labelWithAka(target) {
32082
- return target.aka.length > 0 ? `${target.label} (${target.aka.join(", ")})` : target.label;
32324
+ return target.aka.length > 0 ? `${target.aka.join(", ")} (${target.label})` : target.label;
32083
32325
  }
32084
32326
  function renderTargetLines(targets, def, glyph) {
32085
32327
  const lines = [tx(TUTORIAL_TEXTS.promptHeader, { glyph })];
@@ -32089,7 +32331,6 @@ function renderTargetLines(targets, def, glyph) {
32089
32331
  tx(TUTORIAL_TEXTS.promptOption, {
32090
32332
  index: i + 1,
32091
32333
  label: labelWithAka(t),
32092
- skillDir: `${t.skillDir}/`,
32093
32334
  marker: t.id === def.id ? TUTORIAL_TEXTS.promptDefaultMarker : ""
32094
32335
  })
32095
32336
  );
@@ -32375,4 +32616,4 @@ function resolveBareDefault() {
32375
32616
  process.exit(ExitCode.Error);
32376
32617
  }
32377
32618
  //# sourceMappingURL=cli.js.map
32378
- //# debugId=8b8b0da7-bdb9-55ed-8dcd-0f983f8be006
32619
+ //# debugId=a842c024-162c-5a3a-85db-1daea4b6735f