@skill-map/cli 0.66.0 → 0.68.0

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 (102) hide show
  1. package/dist/cli/tutorial/sm-tutorial/SKILL.md +16 -8
  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 -1
  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 +101 -48
  33. package/dist/cli/tutorial/sm-tutorial/references/_manifest.json +174 -0
  34. package/dist/cli/tutorial/sm-tutorial/references/_manifest.yml +90 -0
  35. package/dist/cli/tutorial/sm-tutorial/references/part-basic-connect.md +166 -0
  36. package/dist/cli/tutorial/sm-tutorial/references/part-basic-daily.md +267 -0
  37. package/dist/cli/tutorial/sm-tutorial/references/part-basic-fundamentals.md +350 -0
  38. package/dist/cli/tutorial/sm-tutorial/references/part-basic-kickoff.md +140 -0
  39. package/dist/cli/tutorial/sm-tutorial/references/part-connect-harness.md +6 -4
  40. package/dist/cli/tutorial/sm-tutorial/references/part-daily-loop.md +21 -26
  41. package/dist/cli/tutorial/sm-tutorial/references/part-fundamentals.md +10 -5
  42. package/dist/cli/tutorial/sm-tutorial/references/part-mcp.md +3 -6
  43. package/dist/cli/tutorial/sm-tutorial/references/part-project-kickoff.md +29 -14
  44. package/dist/cli/tutorial/sm-tutorial/references/part-settings.md +17 -13
  45. package/dist/cli/tutorial/sm-tutorial/scripts/fixtures.js +85 -22
  46. package/dist/cli/tutorial/sm-tutorial/scripts/lib/paths.js +74 -4
  47. package/dist/cli/tutorial/sm-tutorial/scripts/state.js +22 -6
  48. package/dist/cli.js +527 -251
  49. package/dist/conformance/index.js +42 -2
  50. package/dist/index.js +43 -30
  51. package/dist/kernel/index.d.ts +28 -5
  52. package/dist/kernel/index.js +43 -30
  53. package/dist/ui/chunk-3ANNEMV4.js +499 -0
  54. package/dist/ui/chunk-3GDWM5VM.js +2 -0
  55. package/dist/ui/{chunk-5BJGO7GH.js → chunk-3U4QZKU2.js} +4 -4
  56. package/dist/ui/chunk-3ZAHOYQ7.js +1 -0
  57. package/dist/ui/chunk-4F53HBGG.js +1845 -0
  58. package/dist/ui/{chunk-56CBK7LB.js → chunk-6FGV5O5J.js} +1 -1
  59. package/dist/ui/chunk-7WMT2LX4.js +1 -0
  60. package/dist/ui/chunk-BJUBDHJR.js +3 -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-KHDWXSGR.js +1 -0
  72. package/dist/ui/{chunk-HEK4PH5A.js → chunk-KMHXNOFZ.js} +1 -1
  73. package/dist/ui/chunk-KWT7E2RJ.js +16 -0
  74. package/dist/ui/{chunk-WHZVGOS3.js → chunk-MQSU6EFZ.js} +1 -1
  75. package/dist/ui/{chunk-43S72FTV.js → chunk-OGEE252A.js} +1 -1
  76. package/dist/ui/{chunk-J4J42HJ4.js → chunk-PU5OP5RN.js} +1 -1
  77. package/dist/ui/{chunk-UTRZTB6V.js → chunk-QVG7J2MP.js} +1 -1
  78. package/dist/ui/chunk-TQBXK5JN.js +1 -0
  79. package/dist/ui/chunk-Z7SOKILO.js +2 -0
  80. package/dist/ui/{chunk-WCABR6TI.js → chunk-ZRJ5ZCFR.js} +1 -1
  81. package/dist/ui/index.html +2 -2
  82. package/dist/ui/main-ZYRIR6DB.js +4 -0
  83. package/dist/ui/styles-VEGETYWD.css +1 -0
  84. package/package.json +17 -18
  85. package/dist/ui/chunk-34ZZDYNQ.js +0 -1
  86. package/dist/ui/chunk-44VNNUSQ.js +0 -2
  87. package/dist/ui/chunk-4SG4352Z.js +0 -7
  88. package/dist/ui/chunk-5ITZXW3A.js +0 -1
  89. package/dist/ui/chunk-7ANZW2OI.js +0 -499
  90. package/dist/ui/chunk-BJ6X6WBO.js +0 -4
  91. package/dist/ui/chunk-CZSLV6YD.js +0 -1
  92. package/dist/ui/chunk-DLYJHLJX.js +0 -2
  93. package/dist/ui/chunk-LGFABCIA.js +0 -16
  94. package/dist/ui/chunk-LPDD2DHE.js +0 -369
  95. package/dist/ui/chunk-MSKP5A4B.js +0 -3
  96. package/dist/ui/chunk-P3SNMV4X.js +0 -2
  97. package/dist/ui/chunk-S4S5ZMXJ.js +0 -3
  98. package/dist/ui/chunk-VHEFRMK3.js +0 -1
  99. package/dist/ui/chunk-X6TRIDBI.js +0 -1845
  100. package/dist/ui/main-NCNZHLLJ.js +0 -4
  101. package/dist/ui/styles-I4ULXD3V.css +0 -1
  102. /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]="4ec5542f-63ef-5e2c-ab32-58a6c8be5dec")}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]="bde63370-f02c-51bb-92b1-0c58f46b6aee")}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.66.0",
253
+ version: "0.68.0",
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
  }
@@ -1008,12 +1007,16 @@ var slashCommandExtractor = {
1008
1007
  kind: "extractor",
1009
1008
  description: "Turns `/command` invocations in a node's body into arrows that point at the resolved slash command or skill, using Claude Code routing rules. Example: `/deploy` in the body draws an arrow to the `deploy` command.",
1010
1009
  scope: "body",
1011
- // Also authorised under the codex lens so a Codex agent's prompt body
1012
- // (the TOML `developer_instructions` field) has its `/command` tokens parsed for
1013
- // pipeline parity with the claude body. codex declares no `invokes`
1014
- // resolution today, so these signals stay unresolved (no spurious edges)
1015
- // until Codex slash commands land in Phase 6b.
1016
- precondition: { provider: ["claude", "codex"] },
1010
+ // Also authorised under the codex and antigravity lenses, which share the
1011
+ // `/command` grammar. Under codex a sub-agent's prompt body (the TOML
1012
+ // `developer_instructions` field) has its `/command` tokens parsed for
1013
+ // pipeline parity; codex resolves them to its open-standard skills
1014
+ // (`invokes: ['skill']`). Under antigravity a workflow / skill / AGENTS.md
1015
+ // body's `/name` tokens resolve to BOTH skills and workflows
1016
+ // (`invokes: ['skill', 'workflow']`), since Antigravity invokes either by
1017
+ // the same slash. A lens that declares no `invokes` resolution leaves the
1018
+ // signals unresolved (no spurious edges).
1019
+ precondition: { provider: ["claude", "codex", "antigravity"] },
1017
1020
  extract(ctx) {
1018
1021
  const seen = /* @__PURE__ */ new Set();
1019
1022
  const body = stripCodeAndHtml(ctx.body);
@@ -1093,6 +1096,27 @@ function buildTooltip(names) {
1093
1096
  return `${joined.slice(0, TOOLTIP_MAX - 1)}\u2026`;
1094
1097
  }
1095
1098
 
1099
+ // plugins/antigravity/providers/antigravity/schemas/workflow.schema.json
1100
+ var workflow_schema_default = {
1101
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1102
+ $id: "https://skill-map.ai/providers/antigravity/v1/frontmatter/workflow.schema.json",
1103
+ title: "FrontmatterAntigravityWorkflow",
1104
+ description: "Frontmatter shape for nodes classified as `workflow` by the Google Antigravity Provider. Workflows are standalone Markdown files under `.agent/workflows/<name>.md` (note the SINGULAR `.agent`, distinct from the open-standard `.agents/skills/` plural that Antigravity adopted for skills). A workflow file is YAML frontmatter followed by a numbered list of markdown steps; the agent runs it when invoked as the slash command `/<name>`, where the handle is ALWAYS the filename stem. Per the documented Antigravity workflow contract (https://antigravity.google/docs/rules-workflows) `description` is the ONLY frontmatter field: workflows have NO `name` field (unlike skills, where `name` is an optional override), so this schema requires `description` and declares no other property. The step body may carry execution-control comments, the shipped runtime documents `// turbo` (per-step) and `// turbo-all` (file-wide); those are body markup, not frontmatter, so they are not modelled here. `additionalProperties: true` lets any future frontmatter key flow through unchanged.",
1105
+ allOf: [
1106
+ { $ref: "https://skill-map.ai/spec/v0/frontmatter/base.schema.json" }
1107
+ ],
1108
+ type: "object",
1109
+ additionalProperties: true,
1110
+ required: ["description"],
1111
+ properties: {
1112
+ description: {
1113
+ type: "string",
1114
+ maxLength: 1024,
1115
+ description: "What the workflow does and when to run it. Antigravity surfaces it in the slash-command picker and uses it for smart detection; skill-map mirrors it on the node card. Required by the Antigravity workflow contract (the only documented workflow frontmatter field). Max 1024 chars, non-empty (minLength from the spec base)."
1116
+ }
1117
+ }
1118
+ };
1119
+
1096
1120
  // plugins/agent-skills/providers/agent-skills/schemas/skill.schema.json
1097
1121
  var skill_schema_default2 = {
1098
1122
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -1222,12 +1246,14 @@ var agentSkillsProvider = {
1222
1246
  // Provider-owned.
1223
1247
  detect: { markers: [".agents"] },
1224
1248
  // Authoring target for `sm tutorial`: the open standard discovers skills
1225
- // under `.agents/skills/<name>/SKILL.md`. The same path is consumed by
1226
- // Antigravity (adopted the standard rather than a `.gemini/` layout) and
1227
- // OpenAI Codex (skills mirror the open standard), so `aka` surfaces both
1228
- // names in the destination prompt to orient testers on those agents.
1229
- // `aka` is display-only, `--for` still matches the `agent-skills` id.
1230
- 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"] },
1231
1257
  read: COMMONS_READ,
1232
1258
  kinds: COMMONS_KINDS,
1233
1259
  resolution: COMMONS_RESOLUTION,
@@ -1239,11 +1265,43 @@ var agentSkillsProvider = {
1239
1265
  };
1240
1266
 
1241
1267
  // plugins/antigravity/providers/antigravity/index.ts
1268
+ var ANTIGRAVITY_RESERVED_SLASH_VERBS = [
1269
+ // Inherited open-standard base (universal cross-agent slash commands).
1270
+ ...COMMONS_RESERVED_NAMES["skill"] ?? [],
1271
+ // Antigravity-specific verbs (not part of the open-standard base).
1272
+ "artifact",
1273
+ "branch",
1274
+ "btw",
1275
+ "changelog",
1276
+ "context",
1277
+ "conversation",
1278
+ "copy",
1279
+ "credits",
1280
+ "diff",
1281
+ "fast",
1282
+ "fork",
1283
+ "goal",
1284
+ "grill-me",
1285
+ "keybindings",
1286
+ "new",
1287
+ "open",
1288
+ "planning",
1289
+ "quota",
1290
+ "rename",
1291
+ "rewind",
1292
+ "schedule",
1293
+ "settings",
1294
+ "skills",
1295
+ "switch",
1296
+ "tasks",
1297
+ "title",
1298
+ "undo"
1299
+ ];
1242
1300
  var antigravityProvider = {
1243
1301
  id: "antigravity",
1244
1302
  pluginId: ANTIGRAVITY_PLUGIN_ID,
1245
1303
  kind: "provider",
1246
- description: "Declares the Google Antigravity runtime and its reserved built-in names.",
1304
+ description: "Classifies `.agent/workflows/*.md` as Antigravity workflows and `.agents/skills/*/SKILL.md` as skills (open standard); declares the Antigravity runtime identity and its reserved built-in names.",
1247
1305
  // Provider identity for the active-lens dropdown, the topbar lens chip,
1248
1306
  // and the per-node provider chip. Antigravity violet, distinct from the
1249
1307
  // other vendor palettes.
@@ -1252,92 +1310,71 @@ var antigravityProvider = {
1252
1310
  color: "#7c3aed",
1253
1311
  colorDark: "#a78bfa"
1254
1312
  },
1255
- // No `detect` block: Antigravity has no vendor-specific workspace marker
1256
- // (it adopted the open-standard `.agents/`, owned by `agent-skills`), so
1257
- // it is never auto-suggested. The lens is set manually via
1258
- // `sm config set activeProvider antigravity`.
1259
- // Vendor provider: marked gated for the day Antigravity grows its own
1260
- // on-disk kind beyond the open standard. Today `kinds: {}` and
1261
- // `classify` returns `null` for every path, so the flag is inert; the
1262
- // declaration anticipates the migration moment so we don't have to
1263
- // remember to flip it then.
1313
+ // Auto-detect marker: Antigravity's workflows live under `.agent/workflows/`
1314
+ // (SINGULAR `.agent`), its one vendor-specific on-disk territory. Skills
1315
+ // live under the shared open-standard `.agents/` (owned by `agent-skills`),
1316
+ // so they are deliberately NOT a marker here. Now that antigravity ships
1317
+ // `beta` (enabled by default), this marker is live: a `.agent/workflows/`
1318
+ // project auto-detects the antigravity lens.
1319
+ detect: { markers: [".agent/workflows"] },
1320
+ // Vendor provider: Antigravity declares its own `workflow` kind
1321
+ // (`.agent/workflows/*.md`) on top of the open-standard skills it adopts.
1322
+ // Gating the classifier behind the active lens keeps the walker from
1323
+ // claiming Antigravity workflows under another lens, where the Antigravity
1324
+ // runtime would never resolve them anyway.
1264
1325
  gatedByActiveLens: true,
1265
- // Not yet ready for end users: ships disabled by default (the operator
1266
- // opts in via `sm plugins enable` / Settings / the tutorial's
1267
- // `--experimental` flow). Replaces the retired `comingSoon` flag.
1268
- stability: "experimental",
1269
- // Adopt the open-standard `.agents/skills/` layout by REUSING the
1270
- // `agent-skills` classifier + kind + read config (composition at the
1271
- // manifest level, not a kernel rule). Under the antigravity lens the
1272
- // walker classifies `.agents/skills/<name>/SKILL.md` as
1273
- // `{ provider: 'antigravity', kind: 'skill' }`, so the reservedNames
1274
- // below apply via SELF scope. `agent-skills` itself is gated to its own
1275
- // lens, so it never competes here (under the antigravity lens it does
1276
- // not participate). This is why there is no cross-provider lens-scope
1277
- // rule in the kernel any more.
1326
+ // Beta: ships ENABLED by default (auto-detects `.agent/workflows/`,
1327
+ // selectable as the active lens) with a maturity badge, the same posture
1328
+ // as codex, since the workflow kind + slash wiring are freshly landed.
1329
+ // Promote to `stable` (drop the field) once it has real-world mileage.
1330
+ stability: "beta",
1331
+ // `.md` + YAML frontmatter covers BOTH families (skills and workflows);
1332
+ // a single read rule suffices because the parser/extension are identical,
1333
+ // `classify()` below routes each path to its kind. (Codex needs a
1334
+ // multi-rule `read` only because it mixes `.toml` + `.md`.)
1278
1335
  read: COMMONS_READ,
1279
- kinds: COMMONS_KINDS,
1280
- resolution: COMMONS_RESOLUTION,
1281
- classify: classifyCommonsPath,
1282
- // Built-in slash-command catalog, captured verbatim from `agy /help`
1283
- // (Antigravity CLI v1.0.3). The universal cross-agent verbs (`help`,
1284
- // `config`, `mcp`, `model`, `clear`, `exit`, ...) come from the
1285
- // open-standard base catalog (`COMMONS_RESERVED_NAMES`, owned by
1286
- // `agent-skills` and inherited here by composition); this block adds ONLY
1287
- // Antigravity's OWN runtime-specific verbs on top, so the neutral
1288
- // standard never carries `agy`-specific commands. The earlier provisional
1289
- // list mirrored Gemini CLI; `agy` dropped Gemini-only verbs (`vim`,
1290
- // `theme`, `terminal-setup`, `setup-github`, `bashes`, `shells`,
1291
- // `policies`, `extensions`, `?`, `dir`, ...) and added agent-first ones.
1292
- // Both the primary verbs and the 8 documented aliases (`new`, `settings`,
1293
- // `quit`, `branch`, `switch`, `conversation`, `undo`, `quota`) are
1294
- // reserved: a user skill named after either is silently shadowed by the
1295
- // built-in once the catalog activates.
1296
- //
1297
- // Declared under the `skill` kind (NOT `command`): Antigravity has no
1298
- // vendor-specific command directory, its user slash-commands are skills
1299
- // (`.agents/skills/<name>/SKILL.md`). Because the antigravity lens now
1300
- // classifies those files itself (inherited classifier above), a user
1301
- // `.agents/skills/goal/SKILL.md` is flagged by SELF scope because `/goal`
1302
- // is built-in.
1303
- //
1304
- // **Reconciliation marker**: re-capture from `agy /help` on each major
1305
- // Antigravity CLI release, bump the cited version above, and move any
1306
- // verb that becomes universal across agents down into
1307
- // `COMMONS_RESERVED_NAMES`.
1336
+ // Two kinds: the open-standard `skill` (inherited from `agent-skills` by
1337
+ // manifest composition, so under the antigravity lens
1338
+ // `.agents/skills/<name>/SKILL.md` classifies as
1339
+ // `{ provider: 'antigravity', kind: 'skill' }` and the reservedNames below
1340
+ // apply via SELF scope) and the OWN `workflow` (`.agent/workflows/*.md`).
1341
+ // `agent-skills` itself is gated to its own lens, so it never competes here.
1342
+ kinds: {
1343
+ ...COMMONS_KINDS,
1344
+ workflow: {
1345
+ schema: "./schemas/workflow.schema.json",
1346
+ schemaJson: workflow_schema_default,
1347
+ ui: {
1348
+ label: "Workflows",
1349
+ // Antigravity violet, so a workflow node reads as Antigravity's own
1350
+ // (skills keep the normalised cross-provider green of COMMONS_KINDS).
1351
+ color: "#7c3aed",
1352
+ colorDark: "#a78bfa",
1353
+ icon: { kind: "pi", id: "pi-sitemap" }
1354
+ },
1355
+ // The handle is ALWAYS the filename stem (`/<name>`): Antigravity
1356
+ // workflows have no `name` frontmatter field (unlike skills), so there
1357
+ // is no override source, only `filename-basename`.
1358
+ identifiers: ["filename-basename"]
1359
+ }
1360
+ },
1361
+ // `/<name>` slash invocations resolve to BOTH skills and workflows: under
1362
+ // the antigravity lens a `/deploy` links to either `.agents/skills/deploy`
1363
+ // or `.agent/workflows/deploy.md`. Overrides the open-standard default
1364
+ // (`invokes: ['skill']`) to add the own `workflow` kind.
1365
+ resolution: { invokes: ["skill", "workflow"] },
1366
+ classify(path) {
1367
+ if (/^\.agent\/workflows\/[^/]+\.md$/.test(path.toLowerCase())) return "workflow";
1368
+ return classifyCommonsPath(path);
1369
+ },
1370
+ // Reserved-name catalog (`ANTIGRAVITY_RESERVED_SLASH_VERBS`, defined above).
1371
+ // Applied to BOTH `skill` and `workflow`: Antigravity invokes either kind
1372
+ // by the same `/<name>` slash, so a user file of either kind named after a
1373
+ // built-in is silently shadowed. `core/name-reserved` tests each node
1374
+ // against `reservedNames[node.kind]`, so both keys must carry the catalog.
1308
1375
  reservedNames: {
1309
- skill: [
1310
- // Inherited open-standard base (universal cross-agent slash commands).
1311
- ...COMMONS_RESERVED_NAMES["skill"] ?? [],
1312
- // Antigravity-specific verbs (not part of the open-standard base).
1313
- "artifact",
1314
- "branch",
1315
- "btw",
1316
- "changelog",
1317
- "context",
1318
- "conversation",
1319
- "copy",
1320
- "credits",
1321
- "diff",
1322
- "fast",
1323
- "fork",
1324
- "goal",
1325
- "grill-me",
1326
- "keybindings",
1327
- "new",
1328
- "open",
1329
- "planning",
1330
- "quota",
1331
- "rename",
1332
- "rewind",
1333
- "schedule",
1334
- "settings",
1335
- "skills",
1336
- "switch",
1337
- "tasks",
1338
- "title",
1339
- "undo"
1340
- ]
1376
+ skill: ANTIGRAVITY_RESERVED_SLASH_VERBS,
1377
+ workflow: ANTIGRAVITY_RESERVED_SLASH_VERBS
1341
1378
  }
1342
1379
  };
1343
1380
 
@@ -1437,6 +1474,12 @@ var codexProvider = {
1437
1474
  // open-standard project, not necessarily a Codex one. A genuine Codex
1438
1475
  // project is identified by `.codex/`.
1439
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" },
1440
1483
  // Vendor provider: Codex CLI only reads its own territory (its `.codex/`
1441
1484
  // agents plus the open `.agents/skills/` skills it adopted). Gating the
1442
1485
  // classifier behind the active lens keeps the walker from claiming Codex
@@ -3620,7 +3663,7 @@ import { existsSync, readFileSync as readFileSync3 } from "fs";
3620
3663
  import { dirname as dirname3, resolve as resolve4 } from "path";
3621
3664
  import { createRequire as createRequire3 } from "module";
3622
3665
  import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
3623
- import yaml from "js-yaml";
3666
+ import { load as yamlLoad, JSON_SCHEMA } from "js-yaml";
3624
3667
 
3625
3668
  // kernel/util/strip-prototype-pollution.ts
3626
3669
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
@@ -3661,7 +3704,7 @@ function readSidecarFor(mdAbsolutePath) {
3661
3704
  }
3662
3705
  let parsedYaml;
3663
3706
  try {
3664
- parsedYaml = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
3707
+ parsedYaml = yamlLoad(raw, { schema: JSON_SCHEMA });
3665
3708
  } catch (err) {
3666
3709
  return {
3667
3710
  parsed: null,
@@ -4520,7 +4563,7 @@ var builtInPlugins = [
4520
4563
  },
4521
4564
  {
4522
4565
  id: "antigravity",
4523
- description: "Google Antigravity CLI platform integration (replaces the retired Gemini CLI). Antigravity adopted the open-standard `.agents/` layout, so skills are classified by the neutral `agent-skills` provider; this plugin contributes the Antigravity runtime identity and a seed list of reserved built-in names.",
4566
+ description: "Google Antigravity CLI platform integration (replaces the retired Gemini CLI). Classifies Antigravity workflows under `.agent/workflows/*.md` (its own kind) and adopts the open-standard `.agents/skills/` layout for skills; contributes the Antigravity runtime identity and reserved built-in names.",
4524
4567
  extensions: [
4525
4568
  antigravityProvider2
4526
4569
  ]
@@ -5947,7 +5990,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
5947
5990
  import { dirname as dirname6, resolve as resolve8 } from "path";
5948
5991
  import { createRequire as createRequire4 } from "module";
5949
5992
  import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
5950
- import yaml2 from "js-yaml";
5993
+ import { dump as yamlDump, load as yamlLoad2, CORE_SCHEMA, JSON_SCHEMA as JSON_SCHEMA2 } from "js-yaml";
5951
5994
  var FilesystemSidecarStore = class {
5952
5995
  /**
5953
5996
  * Path-keyed in-process lock chain. Each path maps to the tail of a
@@ -5999,11 +6042,13 @@ var FilesystemSidecarStore = class {
5999
6042
  `sidecar patch produces a schema-invalid result at ${sidecarAbsPath}: ${errors}`
6000
6043
  );
6001
6044
  }
6002
- const yamlText = yaml2.dump(merged, {
6045
+ const yamlText = yamlDump(merged, {
6003
6046
  sortKeys: true,
6004
6047
  lineWidth: -1,
6005
6048
  noRefs: true,
6006
- 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
6007
6052
  });
6008
6053
  atomicWriteFile(sidecarAbsPath, yamlText);
6009
6054
  }
@@ -6033,7 +6078,7 @@ function isPlainObject4(value) {
6033
6078
  function readSidecarObject(sidecarAbsPath) {
6034
6079
  if (!existsSync6(sidecarAbsPath)) return {};
6035
6080
  const raw = readFileSync7(sidecarAbsPath, "utf8");
6036
- const parsed = yaml2.load(raw, { schema: yaml2.JSON_SCHEMA });
6081
+ const parsed = yamlLoad2(raw, { schema: JSON_SCHEMA2 });
6037
6082
  if (parsed === null || parsed === void 0) return {};
6038
6083
  if (!isPlainObject4(parsed)) {
6039
6084
  throw new Error(
@@ -6374,7 +6419,7 @@ var SmCommand = class extends Command {
6374
6419
  };
6375
6420
 
6376
6421
  // core/sqlite/with-sqlite.ts
6377
- import { existsSync as existsSync11 } from "fs";
6422
+ import { existsSync as existsSync12 } from "fs";
6378
6423
 
6379
6424
  // kernel/adapters/sqlite/storage-adapter.ts
6380
6425
  import { mkdirSync as mkdirSync4 } from "fs";
@@ -9122,6 +9167,8 @@ function createSqliteStorage(options) {
9122
9167
  }
9123
9168
 
9124
9169
  // core/sqlite/db-version-check.ts
9170
+ import { existsSync as existsSync11 } from "fs";
9171
+ import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
9125
9172
  async function detectDbVersionSkew(db, currentVersion) {
9126
9173
  const meta = await db.selectFrom("scan_meta").select(["scannedByVersion"]).executeTakeFirst();
9127
9174
  if (!meta) return { kind: "no-meta" };
@@ -9151,6 +9198,20 @@ function classifyVersionSkew(dbVersion, currentVersion) {
9151
9198
  }
9152
9199
  return { kind: "warn-older", dbVersion, currentVersion };
9153
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
+ }
9154
9215
  function parseVersionTriple(input) {
9155
9216
  if (typeof input !== "string" || input.length === 0) return null;
9156
9217
  const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(input.trim());
@@ -9322,7 +9383,7 @@ async function withSqlite(options, fn) {
9322
9383
  }
9323
9384
  }
9324
9385
  async function tryWithSqlite(options, fn) {
9325
- if (options.databasePath !== ":memory:" && !existsSync11(options.databasePath)) {
9386
+ if (options.databasePath !== ":memory:" && !existsSync12(options.databasePath)) {
9326
9387
  return null;
9327
9388
  }
9328
9389
  return withSqlite(options, fn);
@@ -9926,6 +9987,15 @@ var CHECK_TEXTS = {
9926
9987
  noIssues: "{{glyph}} No issues.\n",
9927
9988
  /** Header summary line: `sm check: 10 warnings · 0 errors`. */
9928
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",
9929
9999
  /** Section heading: one per file with at least one issue. */
9930
10000
  fileSection: " {{file}}\n",
9931
10001
  /**
@@ -9995,6 +10065,20 @@ var PLUGIN_LOADER_TEXTS = {
9995
10065
  invalidManifestExtensionShape: "{{relEntry}}: {{errors}}. See {{docUrl}}.",
9996
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.).",
9997
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>`.",
9998
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.",
9999
10083
  idCollision: "Plugin '{{id}}' at {{pathA}} collides with the plugin at {{pathB}}. Rename one and rerun.",
10000
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.",
@@ -10014,7 +10098,7 @@ var PLUGIN_LOADER_TEXTS = {
10014
10098
 
10015
10099
  // kernel/adapters/plugin-loader/index.ts
10016
10100
  import { createRequire as createRequire5 } from "module";
10017
- 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";
10018
10102
  import { join as join8, resolve as resolve17 } from "path";
10019
10103
  import { pathToFileURL } from "url";
10020
10104
  import semver from "semver";
@@ -10117,7 +10201,7 @@ function stripFunctionsAndPluginId(input) {
10117
10201
 
10118
10202
  // kernel/adapters/plugin-loader/validation.ts
10119
10203
  import * as nodeFs from "fs";
10120
- import { existsSync as existsSync12 } from "fs";
10204
+ import { existsSync as existsSync13 } from "fs";
10121
10205
  import { dirname as dirname10, join as join7 } from "path";
10122
10206
  import { Ajv2020 as Ajv20205 } from "ajv/dist/2020.js";
10123
10207
 
@@ -10243,7 +10327,7 @@ function validateActionFileConventions(pluginPath, pluginId, manifest, relEntry,
10243
10327
  const reportSchemaPath = join7(actionDir, "report.schema.json");
10244
10328
  const promptPath = join7(actionDir, "prompt.md");
10245
10329
  const mode = isRecord(manifestView) && typeof manifestView["mode"] === "string" ? manifestView["mode"] : "deterministic";
10246
- if (!existsSync12(reportSchemaPath)) {
10330
+ if (!existsSync13(reportSchemaPath)) {
10247
10331
  return {
10248
10332
  ...fail(
10249
10333
  pluginPath,
@@ -10254,7 +10338,7 @@ function validateActionFileConventions(pluginPath, pluginId, manifest, relEntry,
10254
10338
  manifest
10255
10339
  };
10256
10340
  }
10257
- const promptExists = existsSync12(promptPath);
10341
+ const promptExists = existsSync13(promptPath);
10258
10342
  if (mode === "probabilistic" && !promptExists) {
10259
10343
  return {
10260
10344
  ...fail(
@@ -10468,11 +10552,11 @@ var PluginLoader = class {
10468
10552
  discoverPaths() {
10469
10553
  const out = [];
10470
10554
  for (const root of this.#options.searchPaths) {
10471
- if (!existsSync13(root)) continue;
10555
+ if (!existsSync14(root)) continue;
10472
10556
  for (const entry of readdirSync5(root, { withFileTypes: true })) {
10473
10557
  if (!entry.isDirectory()) continue;
10474
10558
  const candidate = join8(root, entry.name);
10475
- if (existsSync13(join8(candidate, "plugin.json"))) {
10559
+ if (existsSync14(join8(candidate, "plugin.json"))) {
10476
10560
  out.push(resolve17(candidate));
10477
10561
  }
10478
10562
  }
@@ -10514,15 +10598,8 @@ var PluginLoader = class {
10514
10598
  const manifestResult = this.#parseAndValidateManifest(pluginPath, pluginId);
10515
10599
  if (!manifestResult.ok) return manifestResult.failure;
10516
10600
  const manifest = manifestResult.manifest;
10517
- if (this.#options.resolveEnabled && !this.#options.resolveEnabled(pluginId)) {
10518
- return {
10519
- path: pluginPath,
10520
- id: pluginId,
10521
- status: "disabled",
10522
- manifest,
10523
- reason: PLUGIN_LOADER_TEXTS.disabledByConfig
10524
- };
10525
- }
10601
+ const gated = this.#preImportGate(pluginPath, pluginId, manifest);
10602
+ if (gated) return gated;
10526
10603
  const loaded = [];
10527
10604
  for (const relEntry of discoverExtensionEntries(pluginPath)) {
10528
10605
  const result = await this.#loadAndValidateExtensionEntry(pluginPath, pluginId, manifest, relEntry);
@@ -10545,6 +10622,39 @@ var PluginLoader = class {
10545
10622
  ...storageSchemasResult.schemas ? { storageSchemas: storageSchemasResult.schemas } : {}
10546
10623
  };
10547
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
+ }
10548
10658
  /**
10549
10659
  * Phase 1 of `loadOne`, read `plugin.json`, AJV-validate the manifest,
10550
10660
  * enforce the directory-name == pluginId structural rule, and check
@@ -10633,7 +10743,7 @@ var PluginLoader = class {
10633
10743
  } };
10634
10744
  }
10635
10745
  const abs = resolve17(pluginPath, relEntry);
10636
- if (!existsSync13(abs)) {
10746
+ if (!existsSync14(abs)) {
10637
10747
  return { ok: false, failure: {
10638
10748
  ...fail(
10639
10749
  pluginPath,
@@ -10837,7 +10947,7 @@ function discoverExtensionEntries(pluginPath) {
10837
10947
  }
10838
10948
  function collectKindEntries(pluginPath, kindDir, out) {
10839
10949
  const kindAbs = resolve17(pluginPath, kindDir);
10840
- if (!existsSync13(kindAbs)) return;
10950
+ if (!existsSync14(kindAbs)) return;
10841
10951
  let entries;
10842
10952
  try {
10843
10953
  entries = readdirSync5(kindAbs);
@@ -10864,7 +10974,7 @@ function isDirectorySafe2(path) {
10864
10974
  }
10865
10975
  function findIndexCandidate(entryAbs) {
10866
10976
  for (const candidate of INDEX_CANDIDATES) {
10867
- if (existsSync13(resolve17(entryAbs, candidate))) return candidate;
10977
+ if (existsSync14(resolve17(entryAbs, candidate))) return candidate;
10868
10978
  }
10869
10979
  return null;
10870
10980
  }
@@ -10931,6 +11041,17 @@ function resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault = tru
10931
11041
  function makeEnabledResolver(cfg, dbOverrides) {
10932
11042
  return (pluginId, installedDefault) => resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault);
10933
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
+ }
10934
11055
 
10935
11056
  // core/runtime/plugin-runtime/resolver.ts
10936
11057
  function defaultResolveEnabled(_id, installedDefault = true) {
@@ -10948,7 +11069,7 @@ function isPluginExtensionEnabled(ext, resolveEnabled) {
10948
11069
  installedDefaultEnabled(ext.stability)
10949
11070
  );
10950
11071
  }
10951
- async function buildEnabledResolver(ctx) {
11072
+ async function buildResolverInputs(ctx) {
10952
11073
  const { effective: cfg } = loadConfig({ ...ctx });
10953
11074
  const dbPath = resolveDbPath({
10954
11075
  db: void 0,
@@ -10958,7 +11079,7 @@ async function buildEnabledResolver(ctx) {
10958
11079
  { databasePath: dbPath, autoBackup: false },
10959
11080
  (adapter) => adapter.pluginConfig.loadOverrideMap()
10960
11081
  ) ?? /* @__PURE__ */ new Map();
10961
- return makeEnabledResolver(cfg, dbOverrides);
11082
+ return { resolveEnabled: makeEnabledResolver(cfg, dbOverrides), dbOverrides };
10962
11083
  }
10963
11084
 
10964
11085
  // kernel/scan/walk-content.ts
@@ -10966,7 +11087,7 @@ import { readFile, readdir, lstat } from "fs/promises";
10966
11087
  import { isAbsolute as isAbsolute4, join as join9, relative as relative2, resolve as resolve19, sep as sep3 } from "path";
10967
11088
 
10968
11089
  // kernel/scan/ignore.ts
10969
- import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
11090
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
10970
11091
  import { dirname as dirname11, resolve as resolve18 } from "path";
10971
11092
  import { fileURLToPath as fileURLToPath2 } from "url";
10972
11093
  import ignoreFactory from "ignore";
@@ -10997,7 +11118,7 @@ function loadBundledIgnoreText() {
10997
11118
  }
10998
11119
  function readIgnoreFileText(scopeRoot) {
10999
11120
  const path = resolve18(scopeRoot, ".skillmapignore");
11000
- if (!existsSync14(path)) return void 0;
11121
+ if (!existsSync15(path)) return void 0;
11001
11122
  try {
11002
11123
  return readFileSync14(path, "utf8");
11003
11124
  } catch {
@@ -11032,7 +11153,7 @@ function readDefaultsFromDisk() {
11032
11153
  resolve18(here, "config/defaults/skillmapignore")
11033
11154
  ];
11034
11155
  for (const candidate of candidates) {
11035
- if (existsSync14(candidate)) {
11156
+ if (existsSync15(candidate)) {
11036
11157
  try {
11037
11158
  return readFileSync14(candidate, "utf8");
11038
11159
  } catch {
@@ -11043,7 +11164,7 @@ function readDefaultsFromDisk() {
11043
11164
  }
11044
11165
 
11045
11166
  // plugins/core/parsers/frontmatter-yaml/index.ts
11046
- import yaml3 from "js-yaml";
11167
+ import { load as yamlLoad3, JSON_SCHEMA as JSON_SCHEMA3 } from "js-yaml";
11047
11168
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
11048
11169
  var frontmatterYamlParser = {
11049
11170
  id: "frontmatter-yaml",
@@ -11055,7 +11176,7 @@ var frontmatterYamlParser = {
11055
11176
  let parsed = {};
11056
11177
  const issues = [];
11057
11178
  try {
11058
- const doc = yaml3.load(frontmatterRaw, { schema: yaml3.JSON_SCHEMA });
11179
+ const doc = yamlLoad3(frontmatterRaw, { schema: JSON_SCHEMA3 });
11059
11180
  if (doc && typeof doc === "object" && !Array.isArray(doc)) {
11060
11181
  parsed = stripPrototypePollution(doc);
11061
11182
  }
@@ -11495,8 +11616,11 @@ async function loadPluginRuntime(opts = {}) {
11495
11616
  const searchPaths = resolveSearchPaths(opts, ctx);
11496
11617
  const validators = loadSchemaValidators();
11497
11618
  let resolveEnabled;
11619
+ let dbOverrides;
11498
11620
  try {
11499
- resolveEnabled = await buildEnabledResolver(ctx);
11621
+ const inputs = await buildResolverInputs(ctx);
11622
+ resolveEnabled = inputs.resolveEnabled;
11623
+ dbOverrides = inputs.dbOverrides;
11500
11624
  } catch {
11501
11625
  }
11502
11626
  const loaderOpts = {
@@ -11505,6 +11629,9 @@ async function loadPluginRuntime(opts = {}) {
11505
11629
  specVersion: installedSpecVersion()
11506
11630
  };
11507
11631
  if (resolveEnabled) loaderOpts.resolveEnabled = resolveEnabled;
11632
+ if (!opts.pluginDir) {
11633
+ loaderOpts.resolveImportTrust = makeImportTrustResolver(dbOverrides ?? /* @__PURE__ */ new Map());
11634
+ }
11508
11635
  const loader = createPluginLoader(loaderOpts);
11509
11636
  const discovered = await loader.discoverAndLoadAll();
11510
11637
  const runtime = {
@@ -11527,6 +11654,12 @@ async function loadPluginRuntime(opts = {}) {
11527
11654
  if (plugin.status === "disabled") continue;
11528
11655
  runtime.warnings.push(formatWarning(plugin));
11529
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
+ }
11530
11663
  enforceRootExclusivity(runtime.annotationContributions);
11531
11664
  return runtime;
11532
11665
  }
@@ -11965,15 +12098,27 @@ function groupRowsByFile(rows) {
11965
12098
  function formatSummary(counts, ansi) {
11966
12099
  const parts = [];
11967
12100
  if (counts.error > 0) {
11968
- 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
+ );
11969
12109
  }
11970
12110
  if (counts.warn > 0) {
11971
12111
  parts.push(
11972
- 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
+ )
11973
12118
  );
11974
12119
  }
11975
12120
  if (counts.info > 0) {
11976
- parts.push(ansi.cyan(`${counts.info} info`));
12121
+ parts.push(ansi.cyan(tx(CHECK_TEXTS.summaryInfoFragment, { count: counts.info })));
11977
12122
  }
11978
12123
  return parts.join(" \xB7 ");
11979
12124
  }
@@ -11998,11 +12143,11 @@ function flattenMessage(message) {
11998
12143
  }
11999
12144
 
12000
12145
  // cli/commands/config.ts
12001
- import { existsSync as existsSync16 } from "fs";
12146
+ import { existsSync as existsSync17 } from "fs";
12002
12147
  import { Command as Command4, Option as Option4 } from "clipanion";
12003
12148
 
12004
12149
  // kernel/scan/detect-providers.ts
12005
- import { existsSync as existsSync15 } from "fs";
12150
+ import { existsSync as existsSync16 } from "fs";
12006
12151
  import { join as join10 } from "path";
12007
12152
  function detectProvidersFromFilesystem(cwd, providers) {
12008
12153
  const seen = /* @__PURE__ */ new Set();
@@ -12019,7 +12164,7 @@ function isDetectableUnderCwd(cwd, provider) {
12019
12164
  if (!installedDefaultEnabled(provider.stability)) return false;
12020
12165
  const markers = provider.detect?.markers;
12021
12166
  if (!markers || markers.length === 0) return false;
12022
- return markers.some((marker) => existsSync15(join10(cwd, marker)));
12167
+ return markers.some((marker) => existsSync16(join10(cwd, marker)));
12023
12168
  }
12024
12169
 
12025
12170
  // core/config/active-provider.ts
@@ -12047,7 +12192,7 @@ function relativeIfBelow(path, cwd) {
12047
12192
  }
12048
12193
 
12049
12194
  // cli/util/scan-zone-drop.ts
12050
- import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
12195
+ import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
12051
12196
 
12052
12197
  // cli/commands/db/shared.ts
12053
12198
  var SAFE_SQL_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
@@ -12059,7 +12204,7 @@ function assertSafeIdentifier(name) {
12059
12204
 
12060
12205
  // cli/util/scan-zone-drop.ts
12061
12206
  function dropScanZone(dbPath) {
12062
- const db = new DatabaseSync5(dbPath);
12207
+ const db = new DatabaseSync6(dbPath);
12063
12208
  try {
12064
12209
  const rows = db.prepare(
12065
12210
  "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'scan\\_%' ESCAPE '\\'"
@@ -12650,7 +12795,7 @@ var ConfigSetCommand = class extends SmCommand {
12650
12795
  announceLensSwitch(cwd, ansi) {
12651
12796
  const dbPath = resolveDbPath({ db: void 0, cwd });
12652
12797
  const okGlyph = ansi.green("\u2713");
12653
- if (!existsSync16(dbPath)) {
12798
+ if (!existsSync17(dbPath)) {
12654
12799
  this.printer.info(tx(CONFIG_TEXTS.lensSwitchedNoDb, { glyph: okGlyph }));
12655
12800
  return;
12656
12801
  }
@@ -12690,7 +12835,7 @@ var ConfigResetCommand = class extends SmCommand {
12690
12835
  const path = targetSettingsPath2(target, ctx.cwd);
12691
12836
  const ansi = this.ansiFor("stdout");
12692
12837
  const okGlyph = ansi.green("\u2713");
12693
- if (!existsSync16(path)) {
12838
+ if (!existsSync17(path)) {
12694
12839
  this.printer.data(
12695
12840
  tx(CONFIG_TEXTS.unsetNoOverride, {
12696
12841
  glyph: okGlyph,
@@ -12765,16 +12910,17 @@ var CONFIG_COMMANDS = [
12765
12910
  ];
12766
12911
 
12767
12912
  // cli/commands/conformance.ts
12768
- import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
12913
+ import { existsSync as existsSync20, readFileSync as readFileSync16 } from "fs";
12769
12914
  import { dirname as dirname13, resolve as resolve23 } from "path";
12770
12915
  import { fileURLToPath as fileURLToPath4 } from "url";
12771
12916
  import { Command as Command5, Option as Option5 } from "clipanion";
12772
12917
 
12773
12918
  // conformance/index.ts
12774
12919
  import { spawnSync as spawnSync2 } from "child_process";
12775
- 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";
12776
12921
  import { tmpdir } from "os";
12777
12922
  import { isAbsolute as isAbsolute6, join as join11, relative as relative3, resolve as resolve21 } from "path";
12923
+ import { DatabaseSync as DatabaseSync7 } from "node:sqlite";
12778
12924
 
12779
12925
  // conformance/i18n/runner.texts.ts
12780
12926
  var CONFORMANCE_RUNNER_TEXTS = {
@@ -12866,6 +13012,11 @@ function runConformanceCase(options) {
12866
13012
  if (c.fixture) {
12867
13013
  replaceFixture(scope, fixturesRoot, c.fixture);
12868
13014
  }
13015
+ grantFixturePluginTrust(scope, options.binary, {
13016
+ ...pickSafeEnv(process.env),
13017
+ ...options.env,
13018
+ ...setupEnv
13019
+ });
12869
13020
  const argv = [c.invoke.verb];
12870
13021
  if (c.invoke.sub) argv.push(c.invoke.sub);
12871
13022
  if (c.invoke.args) argv.push(...c.invoke.args);
@@ -12935,6 +13086,40 @@ function replaceFixture(scope, fixturesRoot, fixture) {
12935
13086
  const src = join11(fixturesRoot, fixture);
12936
13087
  cpSync(src, scope, { recursive: true });
12937
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
+ }
12938
13123
  function assertContained2(root, rel, label) {
12939
13124
  if (isAbsolute6(rel)) {
12940
13125
  throw new Error(
@@ -12969,7 +13154,7 @@ function evaluateAssertion(a, ctx) {
12969
13154
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
12970
13155
  }
12971
13156
  const abs = resolve21(ctx.scope, a.path);
12972
- return existsSync17(abs) ? { ok: true, type: a.type } : {
13157
+ return existsSync18(abs) ? { ok: true, type: a.type } : {
12973
13158
  ok: false,
12974
13159
  type: a.type,
12975
13160
  reason: tx(CONFORMANCE_RUNNER_TEXTS.fileNotFound, { path: a.path })
@@ -12984,7 +13169,7 @@ function evaluateAssertion(a, ctx) {
12984
13169
  }
12985
13170
  const fixturePath = join11(ctx.fixturesRoot, a.fixture);
12986
13171
  const targetPath = resolve21(ctx.scope, a.path);
12987
- if (!existsSync17(targetPath)) {
13172
+ if (!existsSync18(targetPath)) {
12988
13173
  return {
12989
13174
  ok: false,
12990
13175
  type: a.type,
@@ -13165,7 +13350,7 @@ var CONFORMANCE_TEXTS = {
13165
13350
  };
13166
13351
 
13167
13352
  // cli/util/conformance-scopes.ts
13168
- 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";
13169
13354
  import { dirname as dirname12, resolve as resolve22 } from "path";
13170
13355
  import { createRequire as createRequire6 } from "module";
13171
13356
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -13185,7 +13370,7 @@ function resolveCliWorkspaceRoot() {
13185
13370
  let cursor = here;
13186
13371
  for (let depth = 0; depth < 6; depth += 1) {
13187
13372
  const candidate = resolve22(cursor, "plugins");
13188
- if (existsSync18(candidate) && statSync4(candidate).isDirectory()) {
13373
+ if (existsSync19(candidate) && statSync4(candidate).isDirectory()) {
13189
13374
  return cursor;
13190
13375
  }
13191
13376
  const parent = dirname12(cursor);
@@ -13205,7 +13390,7 @@ function collectProviderScopes(specRoot) {
13205
13390
  return out;
13206
13391
  }
13207
13392
  const pluginsRoot = resolve22(workspaceRoot, "plugins");
13208
- if (!existsSync18(pluginsRoot)) return out;
13393
+ if (!existsSync19(pluginsRoot)) return out;
13209
13394
  for (const pluginEntry of readdirSync7(pluginsRoot)) {
13210
13395
  const pluginDir = resolve22(pluginsRoot, pluginEntry);
13211
13396
  if (!isDir(pluginDir)) continue;
@@ -13217,7 +13402,7 @@ function collectProviderScopes(specRoot) {
13217
13402
  }
13218
13403
  function isDir(path) {
13219
13404
  try {
13220
- return existsSync18(path) && statSync4(path).isDirectory();
13405
+ return existsSync19(path) && statSync4(path).isDirectory();
13221
13406
  } catch {
13222
13407
  return false;
13223
13408
  }
@@ -13227,10 +13412,10 @@ function collectPluginProviderScopes(providersRoot, specRoot, out) {
13227
13412
  const providerDir = resolve22(providersRoot, entry);
13228
13413
  if (!isDir(providerDir)) continue;
13229
13414
  const conformanceDir = resolve22(providerDir, "conformance");
13230
- if (!existsSync18(conformanceDir)) continue;
13415
+ if (!existsSync19(conformanceDir)) continue;
13231
13416
  const casesDir = resolve22(conformanceDir, "cases");
13232
13417
  const fixturesDir = resolve22(conformanceDir, "fixtures");
13233
- if (!existsSync18(casesDir) || !existsSync18(fixturesDir)) continue;
13418
+ if (!existsSync19(casesDir) || !existsSync19(fixturesDir)) continue;
13234
13419
  out.push({
13235
13420
  id: `provider:${entry}`,
13236
13421
  kind: "provider",
@@ -13268,7 +13453,7 @@ function selectConformanceScopes(scope) {
13268
13453
  return [match];
13269
13454
  }
13270
13455
  function listCaseFiles(scope) {
13271
- if (!existsSync18(scope.casesDir)) return [];
13456
+ if (!existsSync19(scope.casesDir)) return [];
13272
13457
  return readdirSync7(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve22(scope.casesDir, entry));
13273
13458
  }
13274
13459
 
@@ -13287,7 +13472,7 @@ function resolveBinary() {
13287
13472
  let cursor = here;
13288
13473
  for (let depth = 0; depth < 6; depth += 1) {
13289
13474
  const candidate = resolve23(cursor, "bin", "sm.js");
13290
- if (existsSync19(candidate)) return candidate;
13475
+ if (existsSync20(candidate)) return candidate;
13291
13476
  const parent = dirname13(cursor);
13292
13477
  if (parent === cursor) break;
13293
13478
  cursor = parent;
@@ -13353,7 +13538,7 @@ var ConformanceRunCommand = class extends SmCommand {
13353
13538
  return ExitCode.Error;
13354
13539
  }
13355
13540
  const binary = resolveBinary();
13356
- if (!existsSync19(binary)) {
13541
+ if (!existsSync20(binary)) {
13357
13542
  if (this.json) {
13358
13543
  this.#emitJsonError(
13359
13544
  "internal",
@@ -13571,6 +13756,20 @@ var DB_TEXTS = {
13571
13756
  */
13572
13757
  restoreSourceNotFound: "{{glyph}} Backup not found: {{sourcePath}}\n {{hint}}\n",
13573
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.",
13574
13773
  restoreConfirm: "Restore {{sourcePath}} over {{target}}? This overwrites the current DB.",
13575
13774
  restoreDone: "{{glyph}} Restored {{sourcePath}} \u2192 {{target}}\n",
13576
13775
  // --- shared ----------------------------------------------------------
@@ -13698,6 +13897,39 @@ async function statOrNull(path) {
13698
13897
  }
13699
13898
  }
13700
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
+
13701
13933
  // cli/commands/db/restore.ts
13702
13934
  async function chmodOwnerOnlyBestEffort(target) {
13703
13935
  try {
@@ -13738,19 +13970,13 @@ var DbRestoreCommand = class extends SmCommand {
13738
13970
  );
13739
13971
  return ExitCode.NotFound;
13740
13972
  }
13973
+ const validation = await validateRestorableDb(sourcePath, VERSION);
13974
+ if (!validation.ok) {
13975
+ this.printValidationError(validation, sourcePath);
13976
+ return ExitCode.Error;
13977
+ }
13741
13978
  if (this.dryRun) {
13742
- this.printer.data(DB_TEXTS.dryRunHeader);
13743
- const sourceBytes = sourceStat.size;
13744
- const targetClause = await pathExists(target) ? DB_TEXTS.dryRunRestoreTargetExistsClause : DB_TEXTS.dryRunRestoreTargetMissingClause;
13745
- this.printer.data(
13746
- tx(DB_TEXTS.dryRunRestoreWouldOverwrite, {
13747
- sourcePath,
13748
- sourceBytes,
13749
- target,
13750
- targetClause
13751
- })
13752
- );
13753
- return ExitCode.Ok;
13979
+ return this.previewRestore(sourcePath, sourceStat.size, target);
13754
13980
  }
13755
13981
  if (!this.yes) {
13756
13982
  const ok = await confirm(tx(DB_TEXTS.restoreConfirm, { sourcePath, target }), {
@@ -13779,21 +14005,65 @@ var DbRestoreCommand = class extends SmCommand {
13779
14005
  );
13780
14006
  return ExitCode.Ok;
13781
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
+ }
13782
14052
  };
13783
14053
 
13784
14054
  // cli/commands/db/reset.ts
13785
- import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
14055
+ import { DatabaseSync as DatabaseSync8 } from "node:sqlite";
13786
14056
  import { Command as Command8, Option as Option8 } from "clipanion";
13787
14057
 
13788
14058
  // core/sqlite/db-files.ts
13789
- import { existsSync as existsSync20 } from "fs";
14059
+ import { existsSync as existsSync21 } from "fs";
13790
14060
  import { rm as rm2 } from "fs/promises";
13791
14061
  var DB_FILE_SUFFIXES = ["", "-wal", "-shm"];
13792
14062
  async function removeDbFiles(dbPath) {
13793
14063
  if (dbPath === ":memory:") return;
13794
14064
  for (const suffix of DB_FILE_SUFFIXES) {
13795
14065
  const p = `${dbPath}${suffix}`;
13796
- if (existsSync20(p)) await rm2(p);
14066
+ if (existsSync21(p)) await rm2(p);
13797
14067
  }
13798
14068
  }
13799
14069
 
@@ -13879,7 +14149,7 @@ var DbResetCommand = class extends SmCommand {
13879
14149
  return ExitCode.Error;
13880
14150
  }
13881
14151
  }
13882
- const db = new DatabaseSync6(path);
14152
+ const db = new DatabaseSync8(path);
13883
14153
  try {
13884
14154
  const rows = db.prepare(
13885
14155
  "SELECT name FROM sqlite_master WHERE type='table' AND (name LIKE 'scan\\_%' ESCAPE '\\'" + (this.state ? " OR name LIKE 'state\\_%' ESCAPE '\\'" : "") + ")"
@@ -14022,7 +14292,7 @@ var DbBrowserCommand = class extends SmCommand {
14022
14292
  };
14023
14293
 
14024
14294
  // cli/commands/db/dump.ts
14025
- import { DatabaseSync as DatabaseSync7 } from "node:sqlite";
14295
+ import { DatabaseSync as DatabaseSync9 } from "node:sqlite";
14026
14296
  import { Command as Command11, Option as Option10 } from "clipanion";
14027
14297
  var DbDumpCommand = class extends SmCommand {
14028
14298
  static paths = [["db", "dump"]];
@@ -14064,7 +14334,7 @@ var DbDumpCommand = class extends SmCommand {
14064
14334
  }
14065
14335
  };
14066
14336
  function dumpDatabaseToStream(dbPath, out, tables) {
14067
- const db = new DatabaseSync7(dbPath, { readOnly: true });
14337
+ const db = new DatabaseSync9(dbPath, { readOnly: true });
14068
14338
  try {
14069
14339
  out.write("PRAGMA foreign_keys=OFF;\n");
14070
14340
  out.write("BEGIN TRANSACTION;\n");
@@ -14139,6 +14409,10 @@ function tryParseNonNegativeInt(raw) {
14139
14409
  }
14140
14410
  return parsed;
14141
14411
  }
14412
+ function tryParsePositiveInt(raw) {
14413
+ const parsed = tryParseNonNegativeInt(raw);
14414
+ return parsed === null || parsed === 0 ? null : parsed;
14415
+ }
14142
14416
  function parsePositiveIntegerOption(raw, label, stderr) {
14143
14417
  const parsed = tryParseNonNegativeInt(raw);
14144
14418
  if (parsed === null || parsed === 0) {
@@ -14399,7 +14673,7 @@ var DB_COMMANDS = [
14399
14673
  ];
14400
14674
 
14401
14675
  // cli/commands/example.ts
14402
- 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";
14403
14677
  import { dirname as dirname16, relative as relative5, resolve as resolve27 } from "path";
14404
14678
  import { fileURLToPath as fileURLToPath5 } from "url";
14405
14679
  import { Command as Command13, Option as Option12 } from "clipanion";
@@ -14703,7 +14977,7 @@ function resolveExampleSourceDir() {
14703
14977
  resolve27(here, "../cli/example")
14704
14978
  ];
14705
14979
  for (const candidate of candidates) {
14706
- if (existsSync21(candidate) && statSync5(candidate).isDirectory()) {
14980
+ if (existsSync22(candidate) && statSync5(candidate).isDirectory()) {
14707
14981
  cachedSourceDir = candidate;
14708
14982
  return candidate;
14709
14983
  }
@@ -14715,7 +14989,7 @@ function resolveExampleSourceDir() {
14715
14989
  function isExamplePayloadEntry(sourceRoot, src) {
14716
14990
  const rel = relative5(sourceRoot, src);
14717
14991
  if (rel === "") return true;
14718
- return rel.split(/[\\/]/)[0] !== ".skill-map";
14992
+ return rel.split(/[\\/]/)[0] !== SKILL_MAP_DIR;
14719
14993
  }
14720
14994
 
14721
14995
  // cli/commands/export.ts
@@ -16047,7 +16321,7 @@ import { join as join16 } from "path";
16047
16321
  import { Command as Command18, Option as Option17 } from "clipanion";
16048
16322
 
16049
16323
  // kernel/orchestrator/index.ts
16050
- import { existsSync as existsSync24, statSync as statSync7 } from "fs";
16324
+ import { existsSync as existsSync25, statSync as statSync7 } from "fs";
16051
16325
  import { isAbsolute as isAbsolute10, resolve as resolve32 } from "path";
16052
16326
  import { Tiktoken as Tiktoken2 } from "js-tiktoken/lite";
16053
16327
 
@@ -17417,7 +17691,7 @@ function computeDriftStatus(args2) {
17417
17691
  }
17418
17692
 
17419
17693
  // kernel/sidecar/discover-orphans.ts
17420
- 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";
17421
17695
  import { join as join13, relative as relative6, sep as sep4 } from "path";
17422
17696
  function discoverOrphanSidecars(roots, shouldSkip) {
17423
17697
  const out = [];
@@ -17445,7 +17719,7 @@ function walk2(root, current, shouldSkip, out) {
17445
17719
  if (!entry.isFile()) continue;
17446
17720
  if (!entry.name.endsWith(".sm")) continue;
17447
17721
  const expectedMd = `${full.slice(0, -".sm".length)}.md`;
17448
- if (existsSync22(expectedMd) && safeIsFile(expectedMd)) continue;
17722
+ if (existsSync23(expectedMd) && safeIsFile(expectedMd)) continue;
17449
17723
  out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
17450
17724
  }
17451
17725
  }
@@ -17459,10 +17733,10 @@ function safeIsFile(path) {
17459
17733
 
17460
17734
  // kernel/orchestrator/node-build.ts
17461
17735
  import { createHash as createHash2 } from "crypto";
17462
- import { existsSync as existsSync23 } from "fs";
17736
+ import { existsSync as existsSync24 } from "fs";
17463
17737
  import { isAbsolute as isAbsolute8, resolve as resolvePath } from "path";
17464
17738
  import "js-tiktoken/lite";
17465
- import yaml4 from "js-yaml";
17739
+ import { dump as yamlDump2, CORE_SCHEMA as CORE_SCHEMA2 } from "js-yaml";
17466
17740
 
17467
17741
  // kernel/orchestrator/frontmatter.ts
17468
17742
  function validateFrontmatter(providerFrontmatter, provider, kind, frontmatter, path, strict) {
@@ -17555,22 +17829,22 @@ function canonicalFrontmatter(parsed, raw) {
17555
17829
  if (!hasParsedKeys && hasRawText) {
17556
17830
  return raw;
17557
17831
  }
17558
- return yaml4.dump(parsed, {
17832
+ return yamlDump2(parsed, {
17559
17833
  sortKeys: true,
17560
17834
  lineWidth: -1,
17561
17835
  noRefs: true,
17562
- noCompatMode: true
17836
+ schema: CORE_SCHEMA2
17563
17837
  });
17564
17838
  }
17565
17839
  function canonicalSidecarAnnotations(annotations) {
17566
17840
  if (!annotations || typeof annotations !== "object" || Array.isArray(annotations)) {
17567
- return yaml4.dump({}, { sortKeys: true, lineWidth: -1, noRefs: true, noCompatMode: true });
17841
+ return yamlDump2({}, { sortKeys: true, lineWidth: -1, noRefs: true, schema: CORE_SCHEMA2 });
17568
17842
  }
17569
- return yaml4.dump(annotations, {
17843
+ return yamlDump2(annotations, {
17570
17844
  sortKeys: true,
17571
17845
  lineWidth: -1,
17572
17846
  noRefs: true,
17573
- noCompatMode: true
17847
+ schema: CORE_SCHEMA2
17574
17848
  });
17575
17849
  }
17576
17850
  function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyHash, liveFrontmatterHash) {
@@ -17624,11 +17898,11 @@ function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyH
17624
17898
  }
17625
17899
  function resolveAbsoluteMdPath(relativePath2, roots) {
17626
17900
  if (isAbsolute8(relativePath2)) {
17627
- return existsSync23(relativePath2) ? relativePath2 : null;
17901
+ return existsSync24(relativePath2) ? relativePath2 : null;
17628
17902
  }
17629
17903
  for (const root of roots) {
17630
17904
  const candidate = resolvePath(root, relativePath2);
17631
- if (existsSync23(candidate)) return candidate;
17905
+ if (existsSync24(candidate)) return candidate;
17632
17906
  }
17633
17907
  return null;
17634
17908
  }
@@ -18441,7 +18715,7 @@ function validateRoots(roots) {
18441
18715
  throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
18442
18716
  }
18443
18717
  for (const root of roots) {
18444
- if (!existsSync24(root) || !statSync7(root).isDirectory()) {
18718
+ if (!existsSync25(root) || !statSync7(root).isDirectory()) {
18445
18719
  throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
18446
18720
  }
18447
18721
  }
@@ -18450,7 +18724,7 @@ function resolveActiveProviderOption(optionValue, roots, providers) {
18450
18724
  if (optionValue !== void 0) return optionValue;
18451
18725
  for (const root of roots) {
18452
18726
  const absRoot = isAbsolute10(root) ? root : resolve32(root);
18453
- if (!existsSync24(absRoot)) continue;
18727
+ if (!existsSync25(absRoot)) continue;
18454
18728
  const detected = detectProvidersFromFilesystem(absRoot, providers)[0] ?? null;
18455
18729
  if (detected !== null) return detected;
18456
18730
  }
@@ -19196,9 +19470,7 @@ async function promptForLens(detected, stdin, stderr, warnGlyph) {
19196
19470
  }
19197
19471
 
19198
19472
  // core/sqlite/db-drift-reset.ts
19199
- import { existsSync as existsSync25 } from "fs";
19200
19473
  import { createInterface as createInterface5 } from "readline";
19201
- import { DatabaseSync as DatabaseSync8 } from "node:sqlite";
19202
19474
 
19203
19475
  // core/sqlite/i18n/db-drift.texts.ts
19204
19476
  var DB_DRIFT_TEXTS = {
@@ -19247,20 +19519,6 @@ function detectDriftReason(dbPath, currentVersion) {
19247
19519
  }
19248
19520
  return classifyFingerprint(dbPath).kind === "drift" ? "schema" : null;
19249
19521
  }
19250
- function readScannedByVersion(dbPath) {
19251
- if (dbPath === ":memory:" || !existsSync25(dbPath)) return null;
19252
- let raw = null;
19253
- try {
19254
- raw = new DatabaseSync8(dbPath, { readOnly: true });
19255
- const row = raw.prepare("SELECT scanned_by_version AS v FROM scan_meta LIMIT 1").get();
19256
- const v = row?.v;
19257
- return typeof v === "string" && v.length > 0 ? v : null;
19258
- } catch {
19259
- return null;
19260
- } finally {
19261
- raw?.close();
19262
- }
19263
- }
19264
19522
  function reasonText(reason) {
19265
19523
  return reason === "version" ? DB_DRIFT_TEXTS.driftReasonVersion : DB_DRIFT_TEXTS.driftReasonSchema;
19266
19524
  }
@@ -25205,8 +25463,8 @@ function parseBreakerLimit(raw, stderr, noColor) {
25205
25463
  }
25206
25464
  function parseMaxScanLimit(raw, stderr, noColor) {
25207
25465
  if (raw === void 0) return void 0;
25208
- const n = Number(raw);
25209
- if (!Number.isInteger(n) || n < 1) {
25466
+ const n = tryParsePositiveInt(raw);
25467
+ if (n === null) {
25210
25468
  const stderrTty = stderr;
25211
25469
  const ansi = ansiFor({ isTTY: stderrTty.isTTY === true, noColorFlag: noColor });
25212
25470
  stderr.write(
@@ -25222,8 +25480,8 @@ function parseMaxScanLimit(raw, stderr, noColor) {
25222
25480
  }
25223
25481
  function parseMaxNodesLimit(raw, stderr, noColor) {
25224
25482
  if (raw === void 0) return void 0;
25225
- const n = Number(raw);
25226
- if (!Number.isInteger(n) || n < 1) {
25483
+ const n = tryParsePositiveInt(raw);
25484
+ if (n === null) {
25227
25485
  const stderrTty = stderr;
25228
25486
  const ansi = ansiFor({ isTTY: stderrTty.isTTY === true, noColorFlag: noColor });
25229
25487
  stderr.write(
@@ -25390,8 +25648,8 @@ var ScanCommand = class extends SmCommand {
25390
25648
  */
25391
25649
  parseIntegerFlag(raw, invalidTemplate, invalidHint) {
25392
25650
  if (raw === void 0) return { kind: "ok", value: void 0 };
25393
- const n = Number(raw);
25394
- if (!Number.isInteger(n) || n < 1) {
25651
+ const n = tryParsePositiveInt(raw);
25652
+ if (n === null) {
25395
25653
  const ansi = this.ansiFor("stderr");
25396
25654
  this.printer.info(
25397
25655
  tx(invalidTemplate, {
@@ -26367,7 +26625,7 @@ function originGuarded(path) {
26367
26625
  }
26368
26626
 
26369
26627
  // server/security-headers.ts
26370
- 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'";
26371
26629
  function createSecurityHeaders() {
26372
26630
  return async (c, next) => {
26373
26631
  await next();
@@ -27018,7 +27276,7 @@ import { HTTPException as HTTPException8 } from "hono/http-exception";
27018
27276
 
27019
27277
  // server/node-body.ts
27020
27278
  import { constants as fsConstants2 } from "fs";
27021
- import { open } from "fs/promises";
27279
+ import { open as open2 } from "fs/promises";
27022
27280
  import { isAbsolute as isAbsolute14, resolve as resolvePath2, relative as relativePath, sep as sep8 } from "path";
27023
27281
  async function readNodeBody(cwd, relPath) {
27024
27282
  if (isAbsolute14(relPath)) return null;
@@ -27031,7 +27289,7 @@ async function readNodeBody(cwd, relPath) {
27031
27289
  let raw;
27032
27290
  let handle = null;
27033
27291
  try {
27034
- handle = await open(absFile, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
27292
+ handle = await open2(absFile, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
27035
27293
  raw = await handle.readFile("utf-8");
27036
27294
  } catch (err) {
27037
27295
  if (isExpectedFsError(err)) return null;
@@ -28490,7 +28748,7 @@ var parsePatchBody5 = makeBodyValidator(PATCH_BODY_SCHEMA4, {
28490
28748
 
28491
28749
  // server/routes/actions.ts
28492
28750
  import { HTTPException as HTTPException16 } from "hono/http-exception";
28493
- import { resolve as resolve41 } from "path";
28751
+ import { relative as relative10, resolve as resolve41 } from "path";
28494
28752
 
28495
28753
  // server/routes/node-loader.ts
28496
28754
  import { HTTPException as HTTPException15 } from "hono/http-exception";
@@ -28622,7 +28880,18 @@ function invokeAction(action, absPath, node, body, cwd) {
28622
28880
  };
28623
28881
  return invoke(body.input ?? {}, ctx);
28624
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
+ }
28625
28893
  async function materializeWrites(writes, body, cwd) {
28894
+ assertSidecarWritesContained(writes, cwd);
28626
28895
  const store = new FilesystemSidecarStore(ensureSidecarWritesAllowed);
28627
28896
  try {
28628
28897
  for (const w of writes ?? []) {
@@ -30167,6 +30436,12 @@ function listenAsync(fetchCallback, wss, host, port) {
30167
30436
  fetch: fetchCallback,
30168
30437
  hostname: host,
30169
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.
30170
30445
  websocket: { server: wss }
30171
30446
  },
30172
30447
  () => {
@@ -30484,7 +30759,7 @@ var ServeCommand = class extends SmCommand {
30484
30759
  );
30485
30760
  return ExitCode.Error;
30486
30761
  }
30487
- const maxScanResult = parseMaxScan(this.maxScan);
30762
+ const maxScanResult = parseMaxIntFlag(this.maxScan);
30488
30763
  if (!maxScanResult.ok) {
30489
30764
  this.printer.info(
30490
30765
  tx(SERVE_TEXTS.maxScanInvalid, {
@@ -30495,7 +30770,7 @@ var ServeCommand = class extends SmCommand {
30495
30770
  );
30496
30771
  return ExitCode.Error;
30497
30772
  }
30498
- const maxNodesResult = parseMaxNodes(this.maxNodes);
30773
+ const maxNodesResult = parseMaxIntFlag(this.maxNodes);
30499
30774
  if (!maxNodesResult.ok) {
30500
30775
  this.printer.info(
30501
30776
  tx(SERVE_TEXTS.maxNodesInvalid, {
@@ -30624,16 +30899,10 @@ function parseDebounce(raw) {
30624
30899
  if (parsed === null) return { ok: false, value: raw };
30625
30900
  return { ok: true, value: parsed };
30626
30901
  }
30627
- function parseMaxScan(raw) {
30628
- if (raw === void 0) return { ok: true, value: void 0 };
30629
- const n = Number(raw);
30630
- if (!Number.isInteger(n) || n < 1) return { ok: false, value: raw };
30631
- return { ok: true, value: n };
30632
- }
30633
- function parseMaxNodes(raw) {
30902
+ function parseMaxIntFlag(raw) {
30634
30903
  if (raw === void 0) return { ok: true, value: void 0 };
30635
- const n = Number(raw);
30636
- 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 };
30637
30906
  return { ok: true, value: n };
30638
30907
  }
30639
30908
  function resolveUiDist(ctx, raw) {
@@ -31930,9 +32199,7 @@ var TutorialCommand = class extends SmCommand {
31930
32199
  return ExitCode.Error;
31931
32200
  }
31932
32201
  try {
31933
- rmSync2(targetDir, { recursive: true, force: true });
31934
- mkdirSync6(dirname21(targetDir), { recursive: true });
31935
- cpSync3(sourceDir, targetDir, { recursive: true });
32202
+ materializeSkillFolder(sourceDir, targetDir, ctx.cwd, target.marker);
31936
32203
  } catch (err) {
31937
32204
  this.printer.error(
31938
32205
  tx(TUTORIAL_TEXTS.writeFailed, {
@@ -32023,6 +32290,14 @@ var TutorialCommand = class extends SmCommand {
32023
32290
  return picked;
32024
32291
  }
32025
32292
  };
32293
+ function materializeSkillFolder(sourceDir, targetDir, cwd, marker) {
32294
+ rmSync2(targetDir, { recursive: true, force: true });
32295
+ mkdirSync6(dirname21(targetDir), { recursive: true });
32296
+ cpSync3(sourceDir, targetDir, { recursive: true });
32297
+ if (marker !== void 0) {
32298
+ mkdirSync6(join21(cwd, marker), { recursive: true });
32299
+ }
32300
+ }
32026
32301
  function toScaffoldTarget(provider, includeExperimental) {
32027
32302
  const scaffold = provider.scaffold;
32028
32303
  if (!scaffold || !scaffold.skillDir) return null;
@@ -32031,6 +32306,7 @@ function toScaffoldTarget(provider, includeExperimental) {
32031
32306
  id: provider.id,
32032
32307
  label: provider.presentation.label,
32033
32308
  skillDir: scaffold.skillDir,
32309
+ ...scaffold.marker !== void 0 ? { marker: scaffold.marker } : {},
32034
32310
  aka: scaffold.aka ?? []
32035
32311
  };
32036
32312
  }
@@ -32339,4 +32615,4 @@ function resolveBareDefault() {
32339
32615
  process.exit(ExitCode.Error);
32340
32616
  }
32341
32617
  //# sourceMappingURL=cli.js.map
32342
- //# debugId=4ec5542f-63ef-5e2c-ab32-58a6c8be5dec
32618
+ //# debugId=bde63370-f02c-51bb-92b1-0c58f46b6aee