@skill-map/cli 0.54.0 → 0.56.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 (41) hide show
  1. package/dist/cli/tutorial/sm-tutorial/SKILL.md +22 -24
  2. package/dist/cli/tutorial/sm-tutorial/references/_core.md +8 -7
  3. package/dist/cli/tutorial/sm-tutorial/references/_manifest.yml +21 -42
  4. package/dist/cli/tutorial/sm-tutorial/references/fixtures.md +15 -7
  5. package/dist/cli/tutorial/sm-tutorial/references/part-authoring.md +2 -2
  6. package/dist/cli/tutorial/sm-tutorial/references/part-cli.md +1 -1
  7. package/dist/cli/tutorial/sm-tutorial/references/part-connect-harness.md +9 -10
  8. package/dist/cli/tutorial/sm-tutorial/references/part-daily-loop.md +563 -0
  9. package/dist/cli/tutorial/sm-tutorial/references/part-mcp.md +5 -1
  10. package/dist/cli/tutorial/sm-tutorial/references/part-plugins.md +7 -7
  11. package/dist/cli/tutorial/sm-tutorial/references/part-project-kickoff.md +24 -12
  12. package/dist/cli/tutorial/sm-tutorial/references/part-settings.md +2 -2
  13. package/dist/cli.js +1785 -1102
  14. package/dist/index.js +148 -14
  15. package/dist/kernel/index.d.ts +229 -97
  16. package/dist/kernel/index.js +148 -14
  17. package/dist/migrations/001_initial.sql +5 -0
  18. package/dist/ui/chunk-4ITL7E6U.js +1 -0
  19. package/dist/ui/chunk-DWBJCNC7.js +2 -0
  20. package/dist/ui/{chunk-CXTU4HQV.js → chunk-GHOVZAAV.js} +1 -1
  21. package/dist/ui/{chunk-GBKHMJ4B.js → chunk-H6O2DYVT.js} +13 -13
  22. package/dist/ui/chunk-HDKR6XHG.js +917 -0
  23. package/dist/ui/{chunk-GEI6INVH.js → chunk-JA4Z74I3.js} +1 -1
  24. package/dist/ui/chunk-RS3ANRT5.js +1 -0
  25. package/dist/ui/chunk-VUNP5KNI.js +3 -0
  26. package/dist/ui/chunk-W3Z3CZL4.js +1844 -0
  27. package/dist/ui/chunk-YHJL5LP3.js +913 -0
  28. package/dist/ui/index.html +2 -2
  29. package/dist/ui/{main-HP3MOLI2.js → main-PL3BEVQI.js} +3 -3
  30. package/dist/ui/{styles-4SNVM34O.css → styles-RHEEXRHQ.css} +1 -1
  31. package/migrations/001_initial.sql +5 -0
  32. package/package.json +2 -2
  33. package/dist/cli/tutorial/sm-tutorial/references/part-live-site.md +0 -155
  34. package/dist/cli/tutorial/sm-tutorial/references/part-maintain.md +0 -284
  35. package/dist/cli/tutorial/sm-tutorial/references/part-run-harness.md +0 -181
  36. package/dist/ui/chunk-4CXAL43H.js +0 -1
  37. package/dist/ui/chunk-BUNPMGDX.js +0 -2205
  38. package/dist/ui/chunk-DSNBKMYU.js +0 -2
  39. package/dist/ui/chunk-JXRIGHET.js +0 -552
  40. package/dist/ui/chunk-MVRQGDZJ.js +0 -123
  41. package/dist/ui/chunk-WFLPMCK4.js +0 -392
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]="3118bb11-aad2-50b2-b70d-ed15c2f72a97")}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]="49fdc2af-2694-5cd0-ac19-33f8d1dffa9e")}catch(e){}}();
4
4
  import { existsSync as existsSync33 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -197,6 +197,10 @@ function matchesFilter(hook, event) {
197
197
  function buildHookContext(_hook, trigger, event) {
198
198
  const data = event.data ?? {};
199
199
  const ctx = {
200
+ // `settings` is always populated (possibly empty) so hooks can read
201
+ // `ctx.settings.<id>` without a presence check. The composer
202
+ // populated `resolvedSettings` on each composed hook.
203
+ settings: _hook.resolvedSettings ?? {},
200
204
  event: {
201
205
  type: trigger,
202
206
  timestamp: event.timestamp,
@@ -246,7 +250,7 @@ function bucketByKind(kind, instance, bag) {
246
250
  // package.json
247
251
  var package_default = {
248
252
  name: "@skill-map/cli",
249
- version: "0.54.0",
253
+ version: "0.56.0",
250
254
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
251
255
  license: "MIT",
252
256
  type: "module",
@@ -567,6 +571,41 @@ var ANTIGRAVITY_PLUGIN_ID = "antigravity";
567
571
  var AGENT_SKILLS_PLUGIN_ID = "agent-skills";
568
572
 
569
573
  // plugins/claude/providers/claude/index.ts
574
+ var RESERVED_SLASH_NAMES = [
575
+ "help",
576
+ "clear",
577
+ "compact",
578
+ "cost",
579
+ "init",
580
+ "model",
581
+ "agents",
582
+ "login",
583
+ "logout",
584
+ "mcp",
585
+ "memory",
586
+ "config",
587
+ "doctor",
588
+ "permissions",
589
+ "add-dir",
590
+ "bug",
591
+ "pr-comments",
592
+ "release-notes",
593
+ "review",
594
+ "terminal-setup",
595
+ "vim",
596
+ "output-style",
597
+ "hooks",
598
+ "install-github-app",
599
+ "migrate-installer",
600
+ "upgrade",
601
+ "resume",
602
+ "exit",
603
+ "quit",
604
+ "security-review",
605
+ "statusline",
606
+ "usage",
607
+ "feedback"
608
+ ];
570
609
  var claudeProvider = {
571
610
  id: "claude",
572
611
  pluginId: CLAUDE_PLUGIN_ID,
@@ -718,53 +757,18 @@ var claudeProvider = {
718
757
  // declaring one of these (e.g. `.claude/commands/help.md`) is
719
758
  // silently shadowed: the runtime runs the built-in instead.
720
759
  //
721
- // - `command`: canonical list of built-in slash commands documented
722
- // at https://docs.claude.com/en/docs/claude-code/slash-commands.
723
- // The list will drift as Anthropic adds / removes commands;
724
- // updates ship via a kernel patch + changeset (the catalog is
725
- // considered API surface that users rely on the analyzer to
726
- // reflect).
727
- // - `agent`: built-in agents Anthropic ships with the CLI. Smaller
760
+ // - `command` / `skill`: both reserve `RESERVED_SLASH_NAMES` (defined
761
+ // above). Per the resolution matrix they share the `/` invocation
762
+ // namespace, so a built-in slash command shadows a user node of
763
+ // EITHER kind that claims its name (a `skill` named `help` is as
764
+ // unreachable as a `command` named `help`). The catalog is API
765
+ // surface users rely on the analyzer to reflect.
766
+ // - `agent`: built-in agents Anthropic ships with the CLI, invoked
767
+ // through the `@` / Task namespace (separate from `/`). Smaller
728
768
  // surface than commands today.
729
- // - `skill`: no built-in skills today (skills are user-defined and
730
- // discovered from disk); the key is omitted on purpose, defaulting
731
- // to no reserved names for the kind.
732
769
  reservedNames: {
733
- command: [
734
- "help",
735
- "clear",
736
- "compact",
737
- "cost",
738
- "init",
739
- "model",
740
- "agents",
741
- "login",
742
- "logout",
743
- "mcp",
744
- "memory",
745
- "config",
746
- "doctor",
747
- "permissions",
748
- "add-dir",
749
- "bug",
750
- "pr-comments",
751
- "release-notes",
752
- "review",
753
- "terminal-setup",
754
- "vim",
755
- "output-style",
756
- "hooks",
757
- "install-github-app",
758
- "migrate-installer",
759
- "upgrade",
760
- "resume",
761
- "exit",
762
- "quit",
763
- "security-review",
764
- "statusline",
765
- "usage",
766
- "feedback"
767
- ],
770
+ command: RESERVED_SLASH_NAMES,
771
+ skill: RESERVED_SLASH_NAMES,
768
772
  agent: [
769
773
  "general-purpose",
770
774
  "output-style-setup",
@@ -883,7 +887,7 @@ var atDirectiveExtractor = {
883
887
  id: ID,
884
888
  pluginId: CLAUDE_PLUGIN_ID,
885
889
  kind: "extractor",
886
- description: "Detects `@<token>` directives in a node's body using Claude Code rules. A bare handle (e.g. `@team`) becomes a `mentions` link; a file-flavoured token (e.g. `@docs/api.md`, `@./readme.md`) becomes a `references` link.",
890
+ description: "Detects `@<token>` directives in a node's body using Claude Code rules, choosing the link kind by token shape. Example: a bare handle `@team` becomes a `mentions` link, while a file-flavoured token `@docs/api.md` becomes a `references` link.",
887
891
  scope: "body",
888
892
  precondition: { provider: ["claude"] },
889
893
  // eslint-disable-next-line complexity
@@ -971,7 +975,7 @@ var slashCommandExtractor = {
971
975
  id: ID2,
972
976
  pluginId: CLAUDE_PLUGIN_ID,
973
977
  kind: "extractor",
974
- 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.",
978
+ 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.",
975
979
  scope: "body",
976
980
  precondition: { provider: ["claude"] },
977
981
  extract(ctx) {
@@ -1029,7 +1033,7 @@ var toolsCounterExtractor = {
1029
1033
  id: ID3,
1030
1034
  pluginId: CLAUDE_PLUGIN_ID,
1031
1035
  kind: "extractor",
1032
- description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card.",
1036
+ description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card. Example: an agent with `tools: [Bash, Read, Grep]` shows a count of 3.",
1033
1037
  scope: "frontmatter",
1034
1038
  precondition: { kind: ["claude/agent"] },
1035
1039
  ui: { count },
@@ -1444,7 +1448,7 @@ var annotationsExtractor = {
1444
1448
  id: ID4,
1445
1449
  pluginId: CORE_PLUGIN_ID,
1446
1450
  kind: "extractor",
1447
- description: "Turns the `supersedes` and `supersededBy` entries from a node's `.sm` sidecar into arrows between nodes in the graph.",
1451
+ description: "Turns the `supersedes` and `supersededBy` entries from a node's `.sm` sidecar into arrows between nodes in the graph. Example: `supersededBy: v1-skill.md` in a `.sm` sidecar draws an arrow to `v1-skill.md`.",
1448
1452
  scope: "frontmatter",
1449
1453
  extract(ctx) {
1450
1454
  const sourcePath = ctx.node.path;
@@ -1500,12 +1504,12 @@ function stringArray(value) {
1500
1504
  // plugins/core/extractors/backtick-path/index.ts
1501
1505
  import { posix as pathPosix2 } from "path";
1502
1506
  var ID5 = "backtick-path";
1503
- var PATH_RE = /(?<![\w/:.-])(?:\.{1,2}\/)?[\w.-]+(?:\/[\w.-]+)+\.md\b(?![\w/])/g;
1507
+ var PATH_RE = /(?<![\w/:.-])(?:\.{1,2}\/)?[\w][\w.-]*(?:\/[\w.-]+)*\.md\b(?![\w/])/g;
1504
1508
  var backtickPathExtractor = {
1505
1509
  id: ID5,
1506
1510
  pluginId: CORE_PLUGIN_ID,
1507
1511
  kind: "extractor",
1508
- description: "Turns relative .md paths written inside code spans and fenced blocks into arrows between nodes in the graph.",
1512
+ description: "Turns relative .md paths written inside code spans and fenced blocks into arrows between nodes in the graph. Example: a backticked `references/rules.md` path draws an arrow to that file.",
1509
1513
  scope: "body",
1510
1514
  extract(ctx) {
1511
1515
  const seen = /* @__PURE__ */ new Set();
@@ -1564,14 +1568,31 @@ var count2 = {
1564
1568
  emitWhenEmpty: false,
1565
1569
  priority: 30
1566
1570
  };
1571
+ var SETTING_IGNORED_DOMAINS = "ignored-domains";
1572
+ var settings = {
1573
+ [SETTING_IGNORED_DOMAINS]: {
1574
+ type: "string-list",
1575
+ label: "Ignored domains",
1576
+ description: "Hostnames to exclude from the external-URL count (e.g. internal mirrors, link shorteners).",
1577
+ default: []
1578
+ }
1579
+ };
1567
1580
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
1568
1581
  var TRAILING_PUNCT = /[.,;:!?]+$/;
1569
1582
  var externalUrlCounterExtractor = {
1570
1583
  id: ID6,
1571
1584
  pluginId: CORE_PLUGIN_ID,
1572
1585
  kind: "extractor",
1573
- description: "Counts the distinct external URLs in a node's body and shows the count on the card.",
1586
+ description: "Counts the distinct external URLs in a node's body and shows the count on the card. Example: a body linking `https://example.com` and `https://docs.rs` shows a count of 2.",
1574
1587
  scope: "body",
1588
+ /**
1589
+ * Operator-configurable hostnames to exclude from the count. A URL
1590
+ * whose normalized `hostname` is in this list is skipped entirely:
1591
+ * no Signal, no contribution increment. Default empty (count every
1592
+ * external URL). The operator sets it via
1593
+ * `sm plugins config core/external-url-counter ignored-domains '["…"]'`.
1594
+ */
1595
+ settings,
1575
1596
  /**
1576
1597
  * Phase 6 / View contribution system, surface the distinct-URL
1577
1598
  * count as a card-footer-left chip alongside the in/out link
@@ -1591,6 +1612,7 @@ var externalUrlCounterExtractor = {
1591
1612
  ui: { count: count2 },
1592
1613
  extract(ctx) {
1593
1614
  const seen = /* @__PURE__ */ new Set();
1615
+ const ignoredDomains = readIgnoredDomains(ctx.settings[SETTING_IGNORED_DOMAINS]);
1594
1616
  const body = stripCodeBlocks(ctx.body);
1595
1617
  const lineStarts = computeLineStarts(body);
1596
1618
  for (const match of body.matchAll(URL_RE)) {
@@ -1598,8 +1620,9 @@ var externalUrlCounterExtractor = {
1598
1620
  if (original.length === 0) continue;
1599
1621
  const normalized = normalizeUrl(original);
1600
1622
  if (normalized === null) continue;
1601
- if (seen.has(normalized)) continue;
1602
- seen.add(normalized);
1623
+ if (ignoredDomains.has(normalized.host)) continue;
1624
+ if (seen.has(normalized.href)) continue;
1625
+ seen.add(normalized.href);
1603
1626
  const offset = match.index ?? 0;
1604
1627
  const line = lineFor(lineStarts, offset);
1605
1628
  ctx.emitSignal({
@@ -1611,12 +1634,12 @@ var externalUrlCounterExtractor = {
1611
1634
  {
1612
1635
  extractorId: ID6,
1613
1636
  kind: "references",
1614
- target: normalized,
1637
+ target: normalized.href,
1615
1638
  confidence: 0.3,
1616
1639
  rationale: "external URL pseudo-link, counted then dropped",
1617
1640
  trigger: {
1618
1641
  originalTrigger: original,
1619
- normalizedTrigger: normalized
1642
+ normalizedTrigger: normalized.href
1620
1643
  }
1621
1644
  }
1622
1645
  ]
@@ -1630,12 +1653,20 @@ var externalUrlCounterExtractor = {
1630
1653
  function stripTrailingPunctuation(raw) {
1631
1654
  return raw.replace(TRAILING_PUNCT, "");
1632
1655
  }
1656
+ function readIgnoredDomains(value) {
1657
+ if (!Array.isArray(value)) return /* @__PURE__ */ new Set();
1658
+ const out = /* @__PURE__ */ new Set();
1659
+ for (const entry of value) {
1660
+ if (typeof entry === "string" && entry.length > 0) out.add(entry.toLowerCase());
1661
+ }
1662
+ return out;
1663
+ }
1633
1664
  function normalizeUrl(raw) {
1634
1665
  try {
1635
1666
  const url = new URL(raw);
1636
1667
  url.hostname = url.hostname.toLowerCase();
1637
1668
  url.hash = "";
1638
- return url.href;
1669
+ return { href: url.href, host: url.hostname };
1639
1670
  } catch {
1640
1671
  return null;
1641
1672
  }
@@ -1650,7 +1681,7 @@ var markdownLinkExtractor = {
1650
1681
  id: ID7,
1651
1682
  pluginId: CORE_PLUGIN_ID,
1652
1683
  kind: "extractor",
1653
- description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph.",
1684
+ description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph. Example: `[the guide](docs/guide.md)` draws an arrow to `docs/guide.md`.",
1654
1685
  scope: "body",
1655
1686
  extract(ctx) {
1656
1687
  const seen = /* @__PURE__ */ new Set();
@@ -1675,13 +1706,15 @@ var markdownLinkExtractor = {
1675
1706
  extractorId: ID7,
1676
1707
  kind: "references",
1677
1708
  target: resolved,
1678
- // 1.0: the `[text](path)` syntax is unambiguous. Markdown
1679
- // explicitly designates an out-link via the brackets +
1680
- // parentheses pair; there is no inference left to discount.
1681
- // Whether the path resolves to a real node is a separate
1682
- // concern (the `core/reference-broken` analyzer flags unresolved
1683
- // targets), not a confidence question.
1684
- confidence: 1,
1709
+ // 0.95: the `[text](path)` syntax is unambiguous (the spec's
1710
+ // "unambiguous syntax" tier), but NOT 1.0. `1.0` is reserved
1711
+ // for structured input and is earned post-walk by the
1712
+ // confidence-lift transform when `target` resolves to a real
1713
+ // node; an unresolved (broken) target is downgraded to the
1714
+ // broken floor (0.5) by the same transform. Emitting 1.0 here
1715
+ // would short-circuit the lift and make a broken link
1716
+ // indistinguishable from a resolved one.
1717
+ confidence: 0.95,
1685
1718
  rationale: "unambiguous markdown link syntax",
1686
1719
  trigger: {
1687
1720
  originalTrigger: original,
@@ -1711,7 +1744,7 @@ var mcpToolsExtractor = {
1711
1744
  id: ID8,
1712
1745
  pluginId: CORE_PLUGIN_ID,
1713
1746
  kind: "extractor",
1714
- description: "Turns `tools: [mcp__<server>__<tool>]` entries in a node's frontmatter into an MCP node per unique server and an arrow from the source to each one.",
1747
+ description: "Turns `tools: [mcp__<server>__<tool>]` entries in a node's frontmatter into an MCP node per unique server and an arrow from the source to each one. Example: `tools: [mcp__github__create_pr]` adds an `mcp://github` node and an arrow to it.",
1715
1748
  // Claude-convention pattern only; per-vendor flavours and the
1716
1749
  // config-side MCP declaration (Phase 5b) are still pending, so the
1717
1750
  // extractor ships flagged as experimental in list / show / Settings.
@@ -2004,11 +2037,7 @@ var ANNOTATION_STALE_TEXTS = {
2004
2037
  // a literal placeholder the operator substitutes.
2005
2038
  bodyTooltip: "Sidecar drift since last bump:\n \u2022 body\nRun `sm bump <path>` to refresh.",
2006
2039
  frontmatterTooltip: "Sidecar drift since last bump:\n \u2022 frontmatter\nRun `sm bump <path>` to refresh.",
2007
- bothTooltip: "Sidecar drift since last bump:\n \u2022 body\n \u2022 frontmatter\nRun `sm bump <path>` to refresh.",
2008
- /** Label of the inspector action button that dispatches a bump. */
2009
- bumpLabel: "Bump",
2010
- /** Tooltip shown when the bump button is disabled (the node is fresh, no drift). */
2011
- bumpDisabledReason: "No drift to bump."
2040
+ bothTooltip: "Sidecar drift since last bump:\n \u2022 body\n \u2022 frontmatter\nRun `sm bump <path>` to refresh."
2012
2041
  };
2013
2042
 
2014
2043
  // plugins/core/analyzers/annotation-stale/index.ts
@@ -2024,10 +2053,6 @@ var staleBadge = {
2024
2053
  emitWhenEmpty: false,
2025
2054
  priority: 20
2026
2055
  };
2027
- var bumpButton = {
2028
- slot: "inspector.action.button",
2029
- priority: 10
2030
- };
2031
2056
  var annotationStaleAnalyzer = {
2032
2057
  id: ID11,
2033
2058
  pluginId: CORE_PLUGIN_ID,
@@ -2036,15 +2061,13 @@ var annotationStaleAnalyzer = {
2036
2061
  mode: "deterministic",
2037
2062
  // The natural fix is to bump the node: refreshes the sidecar hashes,
2038
2063
  // increments `annotations.version`, and stamps the audit block. The
2039
- // inspector surfaces `core/node-bump` as the `bumpButton` contribution.
2040
- ui: { staleIcon, staleBadge, bumpButton },
2064
+ // inspector surfaces that affordance via the `core/node-bump` action's
2065
+ // own scan-time `project()` self-projection, not from this analyzer.
2066
+ ui: { staleIcon, staleBadge },
2041
2067
  evaluate(ctx) {
2042
2068
  const issues = [];
2043
2069
  for (const node of ctx.nodes) {
2044
2070
  const status = staleStatus(node.sidecar);
2045
- if (node.sidecar?.present === true) {
2046
- emitBumpButton(ctx, node.path, status !== null);
2047
- }
2048
2071
  if (status === null) continue;
2049
2072
  issues.push({
2050
2073
  analyzerId: ID11,
@@ -2080,15 +2103,6 @@ function messageFor(status, path) {
2080
2103
  return tx(ANNOTATION_STALE_TEXTS.bothDrift, { path });
2081
2104
  }
2082
2105
  }
2083
- function emitBumpButton(ctx, nodePath, enabled) {
2084
- ctx.emitContribution(nodePath, bumpButton, {
2085
- actionId: "core/node-bump",
2086
- label: ANNOTATION_STALE_TEXTS.bumpLabel,
2087
- icon: "pi-arrow-up-right",
2088
- enabled,
2089
- ...enabled ? {} : { disabledReason: ANNOTATION_STALE_TEXTS.bumpDisabledReason }
2090
- });
2091
- }
2092
2106
  function tooltipFor(status) {
2093
2107
  switch (status) {
2094
2108
  case "stale-body":
@@ -2239,7 +2253,7 @@ var ID15 = "link-conflict";
2239
2253
  var NON_CONFLICTING_KINDS = /* @__PURE__ */ new Set(["points"]);
2240
2254
  var linkConflictAnalyzer = {
2241
2255
  id: ID15,
2242
- pluginId: "core",
2256
+ pluginId: CORE_PLUGIN_ID,
2243
2257
  kind: "analyzer",
2244
2258
  description: "Flags conflicting arrow meanings between extractors (e.g. `references` vs `invokes`).",
2245
2259
  mode: "deterministic",
@@ -2304,52 +2318,30 @@ function rankConfidence(c) {
2304
2318
  return c;
2305
2319
  }
2306
2320
 
2307
- // kernel/util/trigger-resolve.ts
2308
- function buildNameIndex(nodes) {
2309
- const out = /* @__PURE__ */ new Map();
2310
- indexByCanonicalName(nodes, out);
2311
- fillIndexWithPathBasename(nodes, out);
2312
- return out;
2313
- }
2314
- function indexByCanonicalName(nodes, out) {
2315
- for (const node of nodes) {
2316
- const raw = canonicalName(node);
2317
- if (raw === null) continue;
2318
- const key = normalizeTrigger(raw);
2319
- if (!out.has(key)) out.set(key, node.path);
2320
- }
2321
+ // kernel/util/link-lines.ts
2322
+ function isSelfLoop(link) {
2323
+ if (link.source === link.target) return true;
2324
+ if (link.resolvedTarget && link.source === link.resolvedTarget) return true;
2325
+ return false;
2321
2326
  }
2322
- function fillIndexWithPathBasename(nodes, out) {
2323
- for (const node of nodes) {
2324
- if (canonicalName(node) !== null) continue;
2325
- const derived = pathBasenameForLink(node.path);
2326
- if (derived.length === 0) continue;
2327
- const key = normalizeTrigger(derived);
2328
- if (!out.has(key)) out.set(key, node.path);
2327
+ function linkLines(link) {
2328
+ const lines = /* @__PURE__ */ new Set();
2329
+ for (const occ of link.occurrences ?? []) {
2330
+ const line = occ.location?.line;
2331
+ if (typeof line === "number") lines.add(line);
2329
2332
  }
2330
- }
2331
- function canonicalName(node) {
2332
- const raw = node.frontmatter?.["name"];
2333
- if (typeof raw !== "string" || raw.length === 0) return null;
2334
- return raw;
2335
- }
2336
- function pathBasenameForLink(path) {
2337
- const segments = path.split("/").filter((s) => s.length > 0);
2338
- if (segments.length === 0) return path;
2339
- const last = segments[segments.length - 1];
2340
- if (last === "SKILL.md" && segments.length >= 2) {
2341
- return segments[segments.length - 2];
2333
+ if (lines.size === 0) {
2334
+ const line = link.location?.line;
2335
+ if (typeof line === "number") lines.add(line);
2342
2336
  }
2343
- return last.replace(/\.md$/, "");
2337
+ return [...lines].sort((a, b) => a - b);
2344
2338
  }
2345
- function resolveLinkTargetToPath(link, nameIndex) {
2346
- const raw = link.target;
2347
- const sigil = raw.charAt(0);
2348
- if (sigil !== "/" && sigil !== "@") return raw;
2349
- const normalizedTrigger = link.trigger?.normalizedTrigger;
2350
- const normalized = typeof normalizedTrigger === "string" ? normalizedTrigger.replace(/^[/@]/, "").trim() : normalizeTrigger(raw.slice(1));
2351
- const resolved = nameIndex.get(normalized);
2352
- return resolved ?? raw;
2339
+ function linkWhere(link, texts) {
2340
+ const lines = linkLines(link);
2341
+ if (lines.length === 0) return "";
2342
+ return tx(lines.length === 1 ? texts.single : texts.plural, {
2343
+ lines: lines.join(", ")
2344
+ });
2353
2345
  }
2354
2346
 
2355
2347
  // plugins/core/analyzers/link-counter/index.ts
@@ -2376,13 +2368,12 @@ var linkCounterAnalyzer = {
2376
2368
  mode: "deterministic",
2377
2369
  ui: { linksIn, linksOut },
2378
2370
  evaluate(ctx) {
2379
- const nameIndex = buildNameIndex(ctx.nodes);
2380
2371
  const perTarget = /* @__PURE__ */ new Map();
2381
2372
  const perSource = /* @__PURE__ */ new Map();
2382
2373
  for (const link of ctx.links) {
2383
- const resolvedTarget = resolveLinkTargetToPath(link, nameIndex);
2384
- if (link.source === link.target || link.source === resolvedTarget) continue;
2385
- bump(perTarget, resolvedTarget, link.kind);
2374
+ if (isSelfLoop(link)) continue;
2375
+ const target = link.resolvedTarget ?? link.target;
2376
+ bump(perTarget, target, link.kind);
2386
2377
  bump(perSource, link.source, link.kind);
2387
2378
  }
2388
2379
  for (const node of ctx.nodes) {
@@ -2416,27 +2407,6 @@ function formatBreakdown(byKind, direction) {
2416
2407
  return [direction, ...lines].join("\n");
2417
2408
  }
2418
2409
 
2419
- // kernel/util/link-lines.ts
2420
- function linkLines(link) {
2421
- const lines = /* @__PURE__ */ new Set();
2422
- for (const occ of link.occurrences ?? []) {
2423
- const line = occ.location?.line;
2424
- if (typeof line === "number") lines.add(line);
2425
- }
2426
- if (lines.size === 0) {
2427
- const line = link.location?.line;
2428
- if (typeof line === "number") lines.add(line);
2429
- }
2430
- return [...lines].sort((a, b) => a - b);
2431
- }
2432
- function linkWhere(link, texts) {
2433
- const lines = linkLines(link);
2434
- if (lines.length === 0) return "";
2435
- return tx(lines.length === 1 ? texts.single : texts.plural, {
2436
- lines: lines.join(", ")
2437
- });
2438
- }
2439
-
2440
2410
  // plugins/core/analyzers/link-self-loop/text.ts
2441
2411
  var LINK_SELF_LOOP_TEXTS = {
2442
2412
  /**
@@ -2482,22 +2452,13 @@ var linkSelfLoopAnalyzer = {
2482
2452
  data: {
2483
2453
  target: link.target,
2484
2454
  resolvedTarget: link.resolvedTarget ?? link.target,
2485
- kind: link.kind,
2486
- // Mark explicitly so UI / downstream consumers can read this
2487
- // single field instead of re-computing the `source === target`
2488
- // predicate themselves.
2489
- selfLoop: true
2455
+ kind: link.kind
2490
2456
  }
2491
2457
  });
2492
2458
  }
2493
2459
  return issues;
2494
2460
  }
2495
2461
  };
2496
- function isSelfLoop(link) {
2497
- if (link.source === link.target) return true;
2498
- if (link.resolvedTarget && link.source === link.resolvedTarget) return true;
2499
- return false;
2500
- }
2501
2462
 
2502
2463
  // kernel/orchestrator/node-identifiers.ts
2503
2464
  import { posix as pathPosix4 } from "path";
@@ -2539,16 +2500,34 @@ function readDirname(node) {
2539
2500
 
2540
2501
  // kernel/orchestrator/lift-resolved-link-confidence.ts
2541
2502
  var RESERVED_TARGET_CONFIDENCE = 0.1;
2503
+ var BROKEN_TARGET_CONFIDENCE = 0.5;
2542
2504
  function liftResolvedLinkConfidence(links, nodes, ctx) {
2543
2505
  if (!links.some((l) => l.confidence < 1)) return;
2544
2506
  const indexes = buildIndexes(nodes, ctx);
2545
2507
  for (const link of links) {
2546
- if (link.confidence >= 1) continue;
2547
- const resolution = resolve2(link, indexes, ctx);
2548
- if (resolution === "none") continue;
2549
- link.confidence = ctx.reservedNodePaths.has(resolution) ? RESERVED_TARGET_CONFIDENCE : 1;
2550
- link.resolvedTarget = resolution;
2508
+ if (link.confidence < 1) applyResolution(link, indexes, ctx);
2509
+ }
2510
+ }
2511
+ function collectBrokenLinks(links, nodes, ctx) {
2512
+ const broken = /* @__PURE__ */ new Set();
2513
+ if (links.length === 0) return broken;
2514
+ const indexes = buildIndexes(nodes, ctx);
2515
+ for (const link of links) {
2516
+ if (isGenuinelyBroken(link, indexes)) broken.add(link);
2517
+ }
2518
+ return broken;
2519
+ }
2520
+ function applyResolution(link, indexes, ctx) {
2521
+ const resolution = resolve2(link, indexes, ctx);
2522
+ if (resolution === "none") {
2523
+ if (isGenuinelyBroken(link, indexes)) {
2524
+ link.confidence = Math.min(link.confidence, BROKEN_TARGET_CONFIDENCE);
2525
+ }
2526
+ return;
2551
2527
  }
2528
+ link.resolvedTarget = resolution;
2529
+ if (indexes.nodeByPath.get(resolution)?.virtual) return;
2530
+ link.confidence = ctx.reservedNodePaths.has(resolution) ? RESERVED_TARGET_CONFIDENCE : 1;
2552
2531
  }
2553
2532
  function buildIndexes(nodes, ctx) {
2554
2533
  const byPath3 = /* @__PURE__ */ new Set();
@@ -2565,6 +2544,12 @@ function resolve2(link, indexes, ctx) {
2565
2544
  if (indexes.byPath.has(link.target)) return link.target;
2566
2545
  return resolveByName(link, indexes, ctx);
2567
2546
  }
2547
+ function isGenuinelyBroken(link, indexes) {
2548
+ if (indexes.byPath.has(link.target)) return false;
2549
+ const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
2550
+ if (stripped !== null && indexes.byName.has(stripped)) return false;
2551
+ return true;
2552
+ }
2568
2553
  function resolveByName(link, indexes, ctx) {
2569
2554
  const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
2570
2555
  if (stripped === null) return "none";
@@ -2608,7 +2593,7 @@ var NAME_RESERVED_TEXTS = {
2608
2593
  * a runtime built-in. Same wording skill-map shipped before the
2609
2594
  * source-side link finding landed.
2610
2595
  */
2611
- message: "Built-in {{provider}} {{kind}}:\nShadowed by this file; the runtime uses its built-in instead. Rename the file or its `frontmatter.name`.",
2596
+ message: "Name collision: this {{kind}} name is already used by the {{provider}} runtime built-in, which shadows this file. Rename the file or its `frontmatter.name`.",
2612
2597
  /**
2613
2598
  * Source-side message: emitted on the node that AUTHORED a link
2614
2599
  * whose target resolves to a reserved name. Explains WHY the link's
@@ -2616,7 +2601,7 @@ var NAME_RESERVED_TEXTS = {
2616
2601
  * the kernel saw the target match a runtime built-in and downgraded
2617
2602
  * the edge so the operator notices.
2618
2603
  */
2619
- linkMessage: "{{target}}:\nResolves to a {{provider}} built-in ({{reservedKind}} `{{reservedPath}}`){{where}}; edge downgraded to confidence {{confidence}}. Rename the target file or its `frontmatter.name`.",
2604
+ linkMessage: "{{target}}:\nName collision: resolves to a {{provider}} built-in ({{reservedKind}} `{{reservedPath}}`){{where}}; the built-in wins, so this edge drops to confidence {{confidence}}. Rename the target file or its `frontmatter.name`.",
2620
2605
  /** Location suffix after the built-in parens, one detection site. */
2621
2606
  whereSingle: " (line {{lines}})",
2622
2607
  /** Location suffix after the built-in parens, several detection sites. */
@@ -2655,7 +2640,9 @@ var nameReservedAnalyzer = {
2655
2640
  }
2656
2641
  for (const link of ctx.links) {
2657
2642
  if (link.confidence !== RESERVED_TARGET_CONFIDENCE) continue;
2658
- const reservedNode = findReservedNodeForLink(link, reserved, byPath3);
2643
+ const reservedPath = link.resolvedTarget;
2644
+ if (!reservedPath || !reserved.has(reservedPath)) continue;
2645
+ const reservedNode = byPath3.get(reservedPath);
2659
2646
  if (!reservedNode) continue;
2660
2647
  issues.push({
2661
2648
  analyzerId: ID18,
@@ -2688,41 +2675,6 @@ function linkWhereSuffix(link) {
2688
2675
  plural: NAME_RESERVED_TEXTS.wherePlural
2689
2676
  });
2690
2677
  }
2691
- function findReservedNodeForLink(link, reserved, byPath3) {
2692
- if (reserved.has(link.target)) {
2693
- const node = byPath3.get(link.target);
2694
- if (node) return node;
2695
- }
2696
- const trigger = link.trigger?.normalizedTrigger;
2697
- if (!trigger) return null;
2698
- const stripped = trigger.replace(/^[/@]/, "").trim();
2699
- if (stripped.length === 0) return null;
2700
- for (const path of reserved) {
2701
- const node = byPath3.get(path);
2702
- if (!node) continue;
2703
- if (matchesNodeIdentifier(node, stripped)) return node;
2704
- }
2705
- return null;
2706
- }
2707
- function matchesNodeIdentifier(node, stripped) {
2708
- const candidates = [];
2709
- const fmName = node.frontmatter?.["name"];
2710
- if (typeof fmName === "string" && fmName.length > 0) candidates.push(normaliseId(fmName));
2711
- const basename = node.path.split("/").pop() ?? "";
2712
- if (basename) {
2713
- const stem = basename.replace(/\.[^.]+$/, "");
2714
- if (stem) candidates.push(normaliseId(stem));
2715
- }
2716
- const segs = node.path.split("/");
2717
- if (segs.length >= 2) {
2718
- const dirBase = segs[segs.length - 2];
2719
- if (dirBase) candidates.push(normaliseId(dirBase));
2720
- }
2721
- return candidates.includes(stripped);
2722
- }
2723
- function normaliseId(raw) {
2724
- return raw.normalize("NFD").replace(new RegExp("\\p{Mn}+", "gu"), "").toLowerCase().replace(/[-_\s]+/g, " ").replace(/ +/g, " ").trim();
2725
- }
2726
2678
 
2727
2679
  // plugins/core/analyzers/node-stability/text.ts
2728
2680
  var NODE_STABILITY_TEXTS = {
@@ -2856,6 +2808,12 @@ var nodeSupersededAnalyzer = {
2856
2808
  pluginId: CORE_PLUGIN_ID,
2857
2809
  kind: "analyzer",
2858
2810
  description: "Marks nodes replaced by a newer one via `supersededBy`.",
2811
+ // Part of the experimental supersession feature: ships disabled by
2812
+ // default alongside the declarer (`core/supersede` button +
2813
+ // `core/node-supersede` action). With the declarer off by default a
2814
+ // user rarely produces `supersededBy` data, so surfacing it stays
2815
+ // experimental too; the operator opts the whole family in together.
2816
+ stability: "experimental",
2859
2817
  mode: "deterministic",
2860
2818
  evaluate(ctx) {
2861
2819
  const issues = [];
@@ -2887,7 +2845,7 @@ function pickSupersededBy(node) {
2887
2845
  }
2888
2846
 
2889
2847
  // plugins/core/analyzers/reference-broken/index.ts
2890
- import { posix as pathPosix5, resolve as resolve3 } from "path";
2848
+ import { resolve as resolve3 } from "path";
2891
2849
 
2892
2850
  // plugins/core/analyzers/reference-broken/text.ts
2893
2851
  var REFERENCE_BROKEN_TEXTS = {
@@ -2918,13 +2876,7 @@ var REFERENCE_BROKEN_TEXTS = {
2918
2876
  // Tooltips for the per-node view-contribution badges. Singular vs
2919
2877
  // plural keeps the count grammar correct without a sub-template.
2920
2878
  alertTooltipSingle: "This node has a broken reference. Open the inspector for details.",
2921
- alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details.",
2922
- // Fix-summary copy when the broken trigger has a same-named file on
2923
- // disk that does not advertise `name:` in its frontmatter. Two
2924
- // variants for single vs multiple candidates; same template family
2925
- // as the alert tooltips above.
2926
- hintSummarySingle: "Add `name: {{name}}` to the frontmatter of {{candidate}} so this reference resolves.",
2927
- hintSummaryMany: "Add `name: {{name}}` to the frontmatter of one of these files so this reference resolves: {{candidates}}."
2879
+ alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details."
2928
2880
  };
2929
2881
 
2930
2882
  // plugins/core/analyzers/reference-broken/index.ts
@@ -2940,33 +2892,31 @@ var referenceBrokenAnalyzer = {
2940
2892
  // aggregate severity counters now owned by `core/issue-counter`. The
2941
2893
  // detection logic stays intact, only the chip emission is gone.
2942
2894
  ui: {},
2943
- // The resolver, the reference-paths escape hatch, and the hint
2944
- // index all share the per-link loop, splitting would re-walk
2945
- // `ctx.links` once per concern. The per-source aggregation that
2946
- // historically lived alongside (driving the now-retired chip
2947
- // emission) moved into `core/issue-counter`.
2895
+ // Pure projector of the orchestrator's genuinely-broken verdict
2896
+ // (`ctx.brokenLinks`, computed once from the same name index the
2897
+ // confidence lift uses), with the reference-paths escape hatch layered
2898
+ // on top. The per-source aggregation that historically lived alongside
2899
+ // (driving the now-retired chip emission) moved into
2900
+ // `core/issue-counter`.
2948
2901
  evaluate(ctx) {
2949
- const byPath3 = new Set(ctx.nodes.map((n) => n.path));
2950
- const byNormalizedName = indexByNormalizedName(ctx.nodes);
2951
- const byBasenameWithoutName = indexByBasenameWithoutName(ctx.nodes);
2952
- const refIndex = ctx.referenceablePaths && ctx.referenceablePaths.size > 0 && ctx.cwd ? { paths: ctx.referenceablePaths, cwd: ctx.cwd } : null;
2902
+ const broken = ctx.brokenLinks;
2903
+ if (!broken || broken.size === 0) return [];
2904
+ const refIndex = buildReferenceIndex(ctx);
2953
2905
  const issues = [];
2954
2906
  for (const link of ctx.links) {
2955
- if (isResolved(link, byPath3, byNormalizedName)) continue;
2907
+ if (!broken.has(link)) continue;
2956
2908
  if (refIndex && resolvesViaReferencePaths(link, refIndex)) continue;
2957
- const candidates = findHintCandidates(link, byBasenameWithoutName);
2958
- issues.push(buildIssue(link, candidates));
2909
+ issues.push(buildIssue(link));
2959
2910
  }
2960
2911
  return issues;
2961
2912
  }
2962
2913
  };
2963
- function buildIssue(link, hintCandidates = []) {
2964
- const data = {
2965
- target: link.target,
2966
- kind: link.kind,
2967
- trigger: link.trigger?.normalizedTrigger ?? null
2968
- };
2969
- const issue = {
2914
+ function buildReferenceIndex(ctx) {
2915
+ if (!ctx.referenceablePaths || ctx.referenceablePaths.size === 0 || !ctx.cwd) return null;
2916
+ return { paths: ctx.referenceablePaths, cwd: ctx.cwd };
2917
+ }
2918
+ function buildIssue(link) {
2919
+ return {
2970
2920
  analyzerId: ID21,
2971
2921
  // `error`, not `warn`: a link whose target is not in the scan is a
2972
2922
  // structural defect the operator must notice, and the card chip
@@ -2984,86 +2934,17 @@ function buildIssue(link, hintCandidates = []) {
2984
2934
  plural: REFERENCE_BROKEN_TEXTS.wherePlural
2985
2935
  })
2986
2936
  }),
2987
- data
2988
- };
2989
- if (hintCandidates.length > 0) attachHint(issue, data, link, hintCandidates);
2990
- return issue;
2991
- }
2992
- function attachHint(issue, data, link, hintCandidates) {
2993
- const suggestedName = (link.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
2994
- const candidatePaths = hintCandidates.map((n) => n.path);
2995
- data["hint"] = {
2996
- kind: "missing-frontmatter-name",
2997
- suggestedName,
2998
- candidates: candidatePaths
2999
- };
3000
- issue.fix = {
3001
- summary: candidatePaths.length === 1 ? tx(REFERENCE_BROKEN_TEXTS.hintSummarySingle, {
3002
- name: suggestedName,
3003
- candidate: candidatePaths[0]
3004
- }) : tx(REFERENCE_BROKEN_TEXTS.hintSummaryMany, {
3005
- name: suggestedName,
3006
- candidates: candidatePaths.join(", ")
3007
- }),
3008
- autofixable: false
2937
+ data: {
2938
+ target: link.target,
2939
+ kind: link.kind,
2940
+ trigger: link.trigger?.normalizedTrigger ?? null
2941
+ }
3009
2942
  };
3010
2943
  }
3011
2944
  function resolvesViaReferencePaths(link, refIndex) {
3012
2945
  if (!isPathStyleLink(link)) return false;
3013
2946
  return refIndex.paths.has(resolve3(refIndex.cwd, link.target));
3014
2947
  }
3015
- function indexByNormalizedName(nodes) {
3016
- const out = /* @__PURE__ */ new Map();
3017
- for (const node of nodes) {
3018
- const raw = node.frontmatter?.["name"];
3019
- const name = typeof raw === "string" ? raw : "";
3020
- if (!name) continue;
3021
- const key = normalizeTrigger(name);
3022
- const bucket = out.get(key) ?? [];
3023
- bucket.push(node);
3024
- out.set(key, bucket);
3025
- }
3026
- return out;
3027
- }
3028
- function basenameWithoutExt(path) {
3029
- const base = pathPosix5.basename(path);
3030
- const ext = pathPosix5.extname(base);
3031
- return ext ? base.slice(0, -ext.length) : base;
3032
- }
3033
- function indexByBasenameWithoutName(nodes) {
3034
- const out = /* @__PURE__ */ new Map();
3035
- for (const node of nodes) {
3036
- const raw = node.frontmatter?.["name"];
3037
- const name = typeof raw === "string" ? raw : "";
3038
- if (name) continue;
3039
- const bare = basenameWithoutExt(node.path);
3040
- if (!bare) continue;
3041
- const key = normalizeTrigger(bare);
3042
- if (!key) continue;
3043
- const bucket = out.get(key) ?? [];
3044
- bucket.push(node);
3045
- out.set(key, bucket);
3046
- }
3047
- return out;
3048
- }
3049
- function findHintCandidates(link, idx) {
3050
- const normalized = link.trigger?.normalizedTrigger;
3051
- if (!normalized) return [];
3052
- const sigil = normalized.charAt(0);
3053
- if (sigil !== "/" && sigil !== "@") return [];
3054
- const withoutSigil = normalized.slice(1).trim();
3055
- if (!withoutSigil) return [];
3056
- return idx.get(withoutSigil) ?? [];
3057
- }
3058
- function isResolved(link, byPath3, byNormalizedName) {
3059
- const normalized = link.trigger?.normalizedTrigger;
3060
- if (normalized) {
3061
- const withoutSigil = normalized.replace(/^[/@]/, "").trim();
3062
- if (byNormalizedName.has(withoutSigil)) return true;
3063
- }
3064
- if (byPath3.has(link.target)) return true;
3065
- return false;
3066
- }
3067
2948
  function isPathStyleLink(link) {
3068
2949
  const sigil = link.trigger?.normalizedTrigger?.charAt(0);
3069
2950
  if (sigil === "/" || sigil === "@") return false;
@@ -3102,12 +2983,9 @@ var referenceRedundantAnalyzer = {
3102
2983
  mode: "deterministic",
3103
2984
  evaluate(ctx) {
3104
2985
  if (ctx.links.length === 0) return [];
3105
- const byPath3 = /* @__PURE__ */ new Map();
3106
- for (const node of ctx.nodes) byPath3.set(node.path, node);
3107
- const byName = buildNameIndex2(ctx.nodes);
3108
2986
  const groups = /* @__PURE__ */ new Map();
3109
2987
  for (const link of ctx.links) {
3110
- const resolved = resolveTargetPath(link, byPath3, byName);
2988
+ const resolved = link.resolvedTarget;
3111
2989
  if (!resolved) continue;
3112
2990
  const key = `${link.source}\0${resolved}`;
3113
2991
  const bucket = groups.get(key);
@@ -3188,45 +3066,6 @@ function formatGroupedOccurrences(occurrences) {
3188
3066
  })
3189
3067
  ).join(REFERENCE_REDUNDANT_TEXTS.occurrenceSeparator);
3190
3068
  }
3191
- function buildNameIndex2(nodes) {
3192
- const out = /* @__PURE__ */ new Map();
3193
- for (const node of nodes) {
3194
- for (const candidate of collectIdentifiers(node)) {
3195
- const normalised = normalizeTrigger(candidate);
3196
- if (!normalised) continue;
3197
- const bucket = out.get(normalised);
3198
- if (bucket) bucket.push(node.path);
3199
- else out.set(normalised, [node.path]);
3200
- }
3201
- }
3202
- return out;
3203
- }
3204
- function collectIdentifiers(node) {
3205
- const out = [];
3206
- const fmName = node.frontmatter?.["name"];
3207
- if (typeof fmName === "string" && fmName.length > 0) out.push(fmName);
3208
- const segs = node.path.split("/");
3209
- const last = segs[segs.length - 1] ?? "";
3210
- if (last) {
3211
- const stem = last.replace(/\.[^.]+$/, "");
3212
- if (stem) out.push(stem);
3213
- }
3214
- if (segs.length >= 2) {
3215
- const dirBase = segs[segs.length - 2];
3216
- if (dirBase) out.push(dirBase);
3217
- }
3218
- return out;
3219
- }
3220
- function resolveTargetPath(link, byPath3, byName) {
3221
- if (byPath3.has(link.target)) return link.target;
3222
- const trigger = link.trigger?.normalizedTrigger;
3223
- if (!trigger) return null;
3224
- const stripped = trigger.replace(/^[/@]/, "").trim();
3225
- if (!stripped) return null;
3226
- const candidates = byName.get(stripped);
3227
- if (!candidates || candidates.length === 0) return null;
3228
- return candidates[0] ?? null;
3229
- }
3230
3069
 
3231
3070
  // kernel/adapters/schema-validators.ts
3232
3071
  import { readFileSync as readFileSync2 } from "fs";
@@ -3760,198 +3599,82 @@ function makeIssue(signal) {
3760
3599
  return null;
3761
3600
  }
3762
3601
 
3763
- // plugins/core/analyzers/supersede/text.ts
3764
- var SUPERSEDE_TEXTS = {
3765
- /** Label of the inspector action button that declares supersession. */
3766
- supersedeLabel: "Supersede",
3767
- /** Tooltip shown when the supersede button is disabled (already superseded). */
3768
- supersedeDisabledReason: "Already superseded.",
3769
- /** Tooltip shown when there is no other node to supersede this one. */
3770
- supersedeNoTargetsReason: "No other node to supersede this one.",
3771
- /** Prompt label for the target node-picker (enum-pick over the live node set). */
3772
- supersedePromptLabel: "Superseded by"
3602
+ // plugins/core/analyzers/trigger-collision/text.ts
3603
+ var TRIGGER_COLLISION_TEXTS = {
3604
+ /**
3605
+ * Top-level message when `analyzeTriggerBucket` accumulated exactly one
3606
+ * cause part. Used for the advertiser-ambiguous-only, invocation-
3607
+ * ambiguous-only, and cross-kind-only branches.
3608
+ */
3609
+ messageOnePart: '"{{normalized}}":\nTrigger collision: {{part}}.',
3610
+ /**
3611
+ * Top-level message when `analyzeTriggerBucket` accumulated two cause
3612
+ * parts (advertiser-ambiguous AND invocation-ambiguous fire together).
3613
+ * The joiner lives inside the template so future locales can adapt it
3614
+ * (e.g. `'; y '` in Spanish) without touching the rule code.
3615
+ */
3616
+ messageTwoParts: '"{{normalized}}":\nTrigger collision: {{first}}; and {{second}}.',
3617
+ /** `<n> advertisers: <list>` part, fires on the advertiser-ambiguous branch. */
3618
+ partAdvertisers: "{{count}} advertisers: {{paths}}",
3619
+ /** `<n> invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
3620
+ partInvocations: "{{count}} invocation forms: {{forms}}",
3621
+ /** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
3622
+ partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
3623
+ /** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
3624
+ partNonCanonicalPlural: "non-canonical invocations {{forms}} against advertiser {{advertiser}}"
3773
3625
  };
3774
3626
 
3775
- // plugins/core/analyzers/supersede/index.ts
3776
- var ID25 = "supersede";
3777
- var supersedeButton = {
3778
- slot: "inspector.action.button",
3779
- priority: 10
3780
- };
3781
- var supersedeAnalyzer = {
3627
+ // plugins/core/analyzers/trigger-collision/index.ts
3628
+ var ID25 = "trigger-collision";
3629
+ var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
3630
+ "command",
3631
+ "skill",
3632
+ "agent"
3633
+ ]);
3634
+ var triggerCollisionAnalyzer = {
3782
3635
  id: ID25,
3783
3636
  pluginId: CORE_PLUGIN_ID,
3784
3637
  kind: "analyzer",
3785
- description: 'Projects the inspector "Supersede" button (declares a node replaced by another).',
3786
3638
  mode: "deterministic",
3787
- ui: { supersedeButton },
3639
+ description: "Flags two or more nodes that claim the same `/command` or `@agent` name.",
3640
+ // Two claim-collection passes (advertisement + invocation) feeding
3641
+ // the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
3642
+ // eslint-disable-next-line complexity
3788
3643
  evaluate(ctx) {
3789
- const candidates = ctx.nodes.filter((n) => n.virtual !== true).map((n) => n.path);
3644
+ const buckets = /* @__PURE__ */ new Map();
3645
+ const push = (key, claim) => {
3646
+ const bucket = buckets.get(key) ?? [];
3647
+ bucket.push(claim);
3648
+ buckets.set(key, bucket);
3649
+ };
3790
3650
  for (const node of ctx.nodes) {
3791
- if (node.virtual === true) continue;
3792
- const options = candidates.filter((p) => p !== node.path).map((p) => ({ value: p, label: p }));
3793
- emitSupersedeButton(ctx, node, options);
3651
+ if (!ADVERTISING_KINDS.has(node.kind)) continue;
3652
+ const raw = node.frontmatter?.["name"];
3653
+ if (typeof raw !== "string" || raw.length === 0) continue;
3654
+ const normalized = `/${normalizeTrigger(raw)}`;
3655
+ if (normalized === "/") continue;
3656
+ push(normalized, {
3657
+ kind: "advertiser",
3658
+ token: node.path,
3659
+ nodeId: node.path,
3660
+ canonicalForm: `/${raw}`
3661
+ });
3794
3662
  }
3795
- return [];
3796
- }
3797
- };
3798
- function emitSupersedeButton(ctx, node, options) {
3799
- const disabledReason = resolveDisabledReason(node, options.length);
3800
- ctx.emitContribution(node.path, supersedeButton, {
3801
- actionId: "core/node-supersede",
3802
- label: SUPERSEDE_TEXTS.supersedeLabel,
3803
- icon: "pi-arrow-right-arrow-left",
3804
- enabled: disabledReason === void 0,
3805
- ...disabledReason === void 0 ? {} : { disabledReason },
3806
- prompt: {
3807
- inputType: "enum-pick",
3808
- paramKey: "supersededBy",
3809
- label: SUPERSEDE_TEXTS.supersedePromptLabel,
3810
- options
3811
- }
3812
- });
3813
- }
3814
- function resolveDisabledReason(node, optionCount) {
3815
- if (alreadySuperseded(node)) return SUPERSEDE_TEXTS.supersedeDisabledReason;
3816
- if (optionCount === 0) return SUPERSEDE_TEXTS.supersedeNoTargetsReason;
3817
- return void 0;
3818
- }
3819
- function alreadySuperseded(node) {
3820
- const sidecar = node.sidecar;
3821
- if (!sidecar || sidecar.present !== true) return false;
3822
- const ann = sidecar.annotations;
3823
- if (!ann || typeof ann !== "object" || Array.isArray(ann)) return false;
3824
- const value = ann["supersededBy"];
3825
- return typeof value === "string" && value.length > 0;
3826
- }
3827
-
3828
- // plugins/core/analyzers/tags/text.ts
3829
- var TAGS_TEXTS = {
3830
- /** Label of the inspector action button that edits the node's tags. */
3831
- editLabel: "Edit tags",
3832
- /** Prompt label for the string-list tags input. */
3833
- promptLabel: "Tags"
3834
- };
3835
-
3836
- // plugins/core/analyzers/tags/index.ts
3837
- var ID26 = "tags";
3838
- var setTagsButton = {
3839
- slot: "inspector.action.button",
3840
- priority: 15
3841
- };
3842
- var tagsAnalyzer = {
3843
- id: ID26,
3844
- pluginId: CORE_PLUGIN_ID,
3845
- kind: "analyzer",
3846
- description: `Projects the inspector "Edit tags" button (edits a node's taxonomy tags).`,
3847
- mode: "deterministic",
3848
- ui: { setTagsButton },
3849
- evaluate(ctx) {
3850
- for (const node of ctx.nodes) {
3851
- if (node.sidecar?.present !== true) continue;
3852
- emitSetTagsButton(ctx, node);
3853
- }
3854
- return [];
3855
- }
3856
- };
3857
- function emitSetTagsButton(ctx, node) {
3858
- ctx.emitContribution(node.path, setTagsButton, {
3859
- actionId: "core/node-set-tags",
3860
- label: TAGS_TEXTS.editLabel,
3861
- icon: "pi-tags",
3862
- enabled: true,
3863
- prompt: {
3864
- inputType: "string-list",
3865
- paramKey: "tags",
3866
- label: TAGS_TEXTS.promptLabel,
3867
- defaultValue: currentTags(node)
3868
- }
3869
- });
3870
- }
3871
- function currentTags(node) {
3872
- const ann = node.sidecar?.annotations;
3873
- if (!ann || typeof ann !== "object" || Array.isArray(ann)) return [];
3874
- const value = ann["tags"];
3875
- if (!Array.isArray(value)) return [];
3876
- return value.filter((t) => typeof t === "string");
3877
- }
3878
-
3879
- // plugins/core/analyzers/trigger-collision/text.ts
3880
- var TRIGGER_COLLISION_TEXTS = {
3881
- /**
3882
- * Top-level message when `analyzeTriggerBucket` accumulated exactly one
3883
- * cause part. Used for the advertiser-ambiguous-only, invocation-
3884
- * ambiguous-only, and cross-kind-only branches.
3885
- */
3886
- messageOnePart: '"{{normalized}}":\nTrigger collision: {{part}}.',
3887
- /**
3888
- * Top-level message when `analyzeTriggerBucket` accumulated two cause
3889
- * parts (advertiser-ambiguous AND invocation-ambiguous fire together).
3890
- * The joiner lives inside the template so future locales can adapt it
3891
- * (e.g. `'; y '` in Spanish) without touching the rule code.
3892
- */
3893
- messageTwoParts: '"{{normalized}}":\nTrigger collision: {{first}}; and {{second}}.',
3894
- /** `<n> advertisers: <list>` part, fires on the advertiser-ambiguous branch. */
3895
- partAdvertisers: "{{count}} advertisers: {{paths}}",
3896
- /** `<n> invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
3897
- partInvocations: "{{count}} invocation forms: {{forms}}",
3898
- /** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
3899
- partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
3900
- /** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
3901
- partNonCanonicalPlural: "non-canonical invocations {{forms}} against advertiser {{advertiser}}"
3902
- };
3903
-
3904
- // plugins/core/analyzers/trigger-collision/index.ts
3905
- var ID27 = "trigger-collision";
3906
- var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
3907
- "command",
3908
- "skill",
3909
- "agent"
3910
- ]);
3911
- var triggerCollisionAnalyzer = {
3912
- id: ID27,
3913
- pluginId: CORE_PLUGIN_ID,
3914
- kind: "analyzer",
3915
- mode: "deterministic",
3916
- description: "Flags two or more nodes that claim the same `/command` or `@agent` name.",
3917
- // Two claim-collection passes (advertisement + invocation) feeding
3918
- // the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
3919
- // eslint-disable-next-line complexity
3920
- evaluate(ctx) {
3921
- const buckets = /* @__PURE__ */ new Map();
3922
- const push = (key, claim) => {
3923
- const bucket = buckets.get(key) ?? [];
3924
- bucket.push(claim);
3925
- buckets.set(key, bucket);
3926
- };
3927
- for (const node of ctx.nodes) {
3928
- if (!ADVERTISING_KINDS.has(node.kind)) continue;
3929
- const raw = node.frontmatter?.["name"];
3930
- if (typeof raw !== "string" || raw.length === 0) continue;
3931
- const normalized = `/${normalizeTrigger(raw)}`;
3932
- if (normalized === "/") continue;
3933
- push(normalized, {
3934
- kind: "advertiser",
3935
- token: node.path,
3936
- nodeId: node.path,
3937
- canonicalForm: `/${raw}`
3938
- });
3939
- }
3940
- for (const link of ctx.links) {
3941
- const normalized = link.trigger?.normalizedTrigger;
3942
- if (!normalized) continue;
3943
- push(normalized, {
3944
- kind: "invocation",
3945
- token: link.target,
3946
- nodeId: link.source
3947
- });
3948
- }
3949
- const issues = [];
3950
- for (const [normalized, claims] of buckets) {
3951
- const issue = analyzeTriggerBucket(normalized, claims);
3952
- if (issue) issues.push(issue);
3953
- }
3954
- return issues;
3663
+ for (const link of ctx.links) {
3664
+ const normalized = link.trigger?.normalizedTrigger;
3665
+ if (!normalized) continue;
3666
+ push(normalized, {
3667
+ kind: "invocation",
3668
+ token: link.target,
3669
+ nodeId: link.source
3670
+ });
3671
+ }
3672
+ const issues = [];
3673
+ for (const [normalized, claims] of buckets) {
3674
+ const issue = analyzeTriggerBucket(normalized, claims);
3675
+ if (issue) issues.push(issue);
3676
+ }
3677
+ return issues;
3955
3678
  }
3956
3679
  };
3957
3680
  function analyzeTriggerBucket(normalized, claims) {
@@ -4007,7 +3730,7 @@ function analyzeTriggerBucket(normalized, claims) {
4007
3730
  part: parts[0]
4008
3731
  });
4009
3732
  return {
4010
- analyzerId: ID27,
3733
+ analyzerId: ID25,
4011
3734
  severity: "error",
4012
3735
  nodeIds,
4013
3736
  message,
@@ -4047,13 +3770,13 @@ var ASCII_FORMATTER_TEXTS = {
4047
3770
  };
4048
3771
 
4049
3772
  // plugins/core/formatters/ascii/index.ts
4050
- var ID28 = "ascii";
3773
+ var ID26 = "ascii";
4051
3774
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
4052
3775
  var asciiFormatter = {
4053
- id: ID28,
3776
+ id: ID26,
4054
3777
  pluginId: CORE_PLUGIN_ID,
4055
3778
  kind: "formatter",
4056
- formatId: ID28,
3779
+ formatId: ID26,
4057
3780
  description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
4058
3781
  // ASCII tree formatter, header + per-kind sections + per-issue
4059
3782
  // section. Each section iterates and renders; splitting per section
@@ -4147,13 +3870,13 @@ function renderSection(out, kind, group) {
4147
3870
  }
4148
3871
 
4149
3872
  // plugins/core/formatters/json/index.ts
4150
- var ID29 = "json";
3873
+ var ID27 = "json";
4151
3874
  var jsonFormatter = {
4152
- id: ID29,
3875
+ id: ID27,
4153
3876
  pluginId: CORE_PLUGIN_ID,
4154
3877
  kind: "formatter",
4155
3878
  description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json`). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
4156
- formatId: ID29,
3879
+ formatId: ID27,
4157
3880
  format(ctx) {
4158
3881
  if (ctx.scanResult !== void 0) {
4159
3882
  return JSON.stringify(ctx.scanResult);
@@ -4291,14 +4014,33 @@ function resolveSpecRoot2() {
4291
4014
  }
4292
4015
  }
4293
4016
 
4017
+ // plugins/core/actions/node-bump/text.ts
4018
+ var BUMP_TEXTS = {
4019
+ /** Label of the inspector action button that dispatches a bump. */
4020
+ bumpLabel: "Bump",
4021
+ /** Tooltip shown when the bump button is disabled (the node is fresh, no drift). */
4022
+ bumpDisabledReason: "No drift to bump."
4023
+ };
4024
+
4294
4025
  // plugins/core/actions/node-bump/index.ts
4295
- var ID30 = "node-bump";
4026
+ var ID28 = "node-bump";
4027
+ var bumpButton = {
4028
+ slot: "inspector.action.button",
4029
+ priority: 10
4030
+ };
4296
4031
  var nodeBumpAction = {
4297
- id: ID30,
4032
+ id: ID28,
4298
4033
  pluginId: CORE_PLUGIN_ID,
4299
4034
  kind: "action",
4300
4035
  description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
4301
4036
  mode: "deterministic",
4037
+ ui: { bumpButton },
4038
+ project(ctx) {
4039
+ for (const node of ctx.nodes) {
4040
+ if (node.sidecar?.present !== true) continue;
4041
+ emitBumpButton(ctx, node.path, staleStatus2(node.sidecar) !== null);
4042
+ }
4043
+ },
4302
4044
  // The runtime contract uses generic <TInput, TReport>; bump narrows
4303
4045
  // both. The cast is the standard pattern for built-ins that want
4304
4046
  // typed local I/O while staying compatible with the open generic.
@@ -4307,6 +4049,20 @@ var nodeBumpAction = {
4307
4049
  return invokeBump(input, ctx);
4308
4050
  }
4309
4051
  };
4052
+ function emitBumpButton(ctx, nodePath, enabled) {
4053
+ ctx.emitContribution(nodePath, bumpButton, {
4054
+ actionId: "core/node-bump",
4055
+ label: BUMP_TEXTS.bumpLabel,
4056
+ icon: "pi-arrow-up-right",
4057
+ enabled,
4058
+ ...enabled ? {} : { disabledReason: BUMP_TEXTS.bumpDisabledReason }
4059
+ });
4060
+ }
4061
+ function staleStatus2(overlay) {
4062
+ const status = overlay?.status;
4063
+ if (status === void 0 || status === null || status === "fresh") return null;
4064
+ return status;
4065
+ }
4310
4066
  function invokeBump(input, ctx) {
4311
4067
  const overlay = ctx.node.sidecar ?? null;
4312
4068
  const isFresh = overlay?.present === true && overlay.status === "fresh";
@@ -4353,9 +4109,9 @@ function pickCurrentVersion(overlay) {
4353
4109
 
4354
4110
  // plugins/core/actions/node-set-stability/index.ts
4355
4111
  var STABILITY_VALUES = ["experimental", "stable", "deprecated"];
4356
- var ID31 = "node-set-stability";
4112
+ var ID29 = "node-set-stability";
4357
4113
  var nodeSetStabilityAction = {
4358
- id: ID31,
4114
+ id: ID29,
4359
4115
  pluginId: CORE_PLUGIN_ID,
4360
4116
  kind: "action",
4361
4117
  description: "Sets the lifecycle stage of the current node (writes `stability` to the sidecar).",
@@ -4394,14 +4150,33 @@ function invokeSetStability(input, ctx) {
4394
4150
  return { report, writes: [write] };
4395
4151
  }
4396
4152
 
4153
+ // plugins/core/actions/node-set-tags/text.ts
4154
+ var TAGS_TEXTS = {
4155
+ /** Label of the inspector action button that edits the node's tags. */
4156
+ editLabel: "Edit tags",
4157
+ /** Prompt label for the string-list tags input. */
4158
+ promptLabel: "Tags"
4159
+ };
4160
+
4397
4161
  // plugins/core/actions/node-set-tags/index.ts
4398
- var ID32 = "node-set-tags";
4162
+ var ID30 = "node-set-tags";
4163
+ var setTagsButton = {
4164
+ slot: "inspector.action.button",
4165
+ priority: 15
4166
+ };
4399
4167
  var nodeSetTagsAction = {
4400
- id: ID32,
4168
+ id: ID30,
4401
4169
  pluginId: CORE_PLUGIN_ID,
4402
4170
  kind: "action",
4403
4171
  description: "Sets the taxonomy tags of the current node (writes `tags` to the sidecar; whole-array replace).",
4404
4172
  mode: "deterministic",
4173
+ ui: { setTagsButton },
4174
+ project(ctx) {
4175
+ for (const node of ctx.nodes) {
4176
+ if (node.sidecar?.present !== true) continue;
4177
+ emitSetTagsButton(ctx, node);
4178
+ }
4179
+ },
4405
4180
  // The runtime contract uses generic <TInput, TReport>; this narrows
4406
4181
  // both. The cast is the standard pattern for built-ins that want
4407
4182
  // typed local I/O while staying compatible with the open generic.
@@ -4410,6 +4185,27 @@ var nodeSetTagsAction = {
4410
4185
  return invokeSetTags(input, ctx);
4411
4186
  }
4412
4187
  };
4188
+ function emitSetTagsButton(ctx, node) {
4189
+ ctx.emitContribution(node.path, setTagsButton, {
4190
+ actionId: "core/node-set-tags",
4191
+ label: TAGS_TEXTS.editLabel,
4192
+ icon: "pi-tags",
4193
+ enabled: true,
4194
+ prompt: {
4195
+ inputType: "string-list",
4196
+ paramKey: "tags",
4197
+ label: TAGS_TEXTS.promptLabel,
4198
+ defaultValue: currentTags(node)
4199
+ }
4200
+ });
4201
+ }
4202
+ function currentTags(node) {
4203
+ const ann = node.sidecar?.annotations;
4204
+ if (!ann || typeof ann !== "object" || Array.isArray(ann)) return [];
4205
+ const value = ann["tags"];
4206
+ if (!Array.isArray(value)) return [];
4207
+ return value.filter((t) => typeof t === "string");
4208
+ }
4413
4209
  function invokeSetTags(input, ctx) {
4414
4210
  const tags = Array.isArray(input.tags) ? input.tags : [];
4415
4211
  const timestamp = ctx.now().toISOString();
@@ -4432,23 +4228,82 @@ function invokeSetTags(input, ctx) {
4432
4228
  const report = { ok: true, tags };
4433
4229
  return { report, writes: [write] };
4434
4230
  }
4435
-
4436
- // plugins/core/actions/node-supersede/index.ts
4437
- var ID33 = "node-supersede";
4438
- var nodeSupersedeAction = {
4439
- id: ID33,
4440
- pluginId: CORE_PLUGIN_ID,
4441
- kind: "action",
4442
- description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
4443
- mode: "deterministic",
4444
- // The runtime contract uses generic <TInput, TReport>; supersede
4445
- // narrows both. The cast is the standard pattern for built-ins that
4446
- // want typed local I/O while staying compatible with the open generic.
4447
- invoke(rawInput, ctx) {
4448
- const input = rawInput ?? {};
4449
- return invokeSupersede(input, ctx);
4450
- }
4451
- };
4231
+
4232
+ // plugins/core/actions/node-supersede/text.ts
4233
+ var SUPERSEDE_TEXTS = {
4234
+ /** Label of the inspector action button that declares supersession. */
4235
+ supersedeLabel: "Supersede",
4236
+ /** Tooltip shown when the supersede button is disabled (already superseded). */
4237
+ supersedeDisabledReason: "Already superseded.",
4238
+ /** Tooltip shown when there is no other node to supersede this one. */
4239
+ supersedeNoTargetsReason: "No other node to supersede this one.",
4240
+ /** Prompt label for the target node-picker (enum-pick over the live node set). */
4241
+ supersedePromptLabel: "Superseded by"
4242
+ };
4243
+
4244
+ // plugins/core/actions/node-supersede/index.ts
4245
+ var ID31 = "node-supersede";
4246
+ var supersedeButton = {
4247
+ slot: "inspector.action.button",
4248
+ priority: 10
4249
+ };
4250
+ var nodeSupersedeAction = {
4251
+ id: ID31,
4252
+ pluginId: CORE_PLUGIN_ID,
4253
+ kind: "action",
4254
+ description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
4255
+ // Ships disabled by default (the declarer feature is still settling its
4256
+ // node-picker UX). The button self-projection gates as a unit with the
4257
+ // invoke executor: an enabled button pointing at a disabled action
4258
+ // would error on click, so the whole action stays experimental.
4259
+ stability: "experimental",
4260
+ mode: "deterministic",
4261
+ ui: { supersedeButton },
4262
+ project(ctx) {
4263
+ const candidates = ctx.nodes.filter((n) => n.virtual !== true).map((n) => n.path);
4264
+ for (const node of ctx.nodes) {
4265
+ if (node.virtual === true) continue;
4266
+ const options = candidates.filter((p) => p !== node.path).map((p) => ({ value: p, label: p }));
4267
+ emitSupersedeButton(ctx, node, options);
4268
+ }
4269
+ },
4270
+ // The runtime contract uses generic <TInput, TReport>; supersede
4271
+ // narrows both. The cast is the standard pattern for built-ins that
4272
+ // want typed local I/O while staying compatible with the open generic.
4273
+ invoke(rawInput, ctx) {
4274
+ const input = rawInput ?? {};
4275
+ return invokeSupersede(input, ctx);
4276
+ }
4277
+ };
4278
+ function emitSupersedeButton(ctx, node, options) {
4279
+ const disabledReason = resolveDisabledReason(node, options.length);
4280
+ ctx.emitContribution(node.path, supersedeButton, {
4281
+ actionId: "core/node-supersede",
4282
+ label: SUPERSEDE_TEXTS.supersedeLabel,
4283
+ icon: "pi-arrow-right-arrow-left",
4284
+ enabled: disabledReason === void 0,
4285
+ ...disabledReason === void 0 ? {} : { disabledReason },
4286
+ prompt: {
4287
+ inputType: "enum-pick",
4288
+ paramKey: "supersededBy",
4289
+ label: SUPERSEDE_TEXTS.supersedePromptLabel,
4290
+ options
4291
+ }
4292
+ });
4293
+ }
4294
+ function resolveDisabledReason(node, optionCount) {
4295
+ if (alreadySuperseded(node)) return SUPERSEDE_TEXTS.supersedeDisabledReason;
4296
+ if (optionCount === 0) return SUPERSEDE_TEXTS.supersedeNoTargetsReason;
4297
+ return void 0;
4298
+ }
4299
+ function alreadySuperseded(node) {
4300
+ const sidecar = node.sidecar;
4301
+ if (!sidecar || sidecar.present !== true) return false;
4302
+ const ann = sidecar.annotations;
4303
+ if (!ann || typeof ann !== "object" || Array.isArray(ann)) return false;
4304
+ const value = ann["supersededBy"];
4305
+ return typeof value === "string" && value.length > 0;
4306
+ }
4452
4307
  function invokeSupersede(input, ctx) {
4453
4308
  const supersededBy = input.supersededBy;
4454
4309
  if (supersededBy === ctx.node.path) {
@@ -4769,11 +4624,11 @@ function validateOrDefault(parsed) {
4769
4624
  if (!result.ok) return defaultSettings();
4770
4625
  return result.data;
4771
4626
  }
4772
- function backfillSubObjects(settings) {
4627
+ function backfillSubObjects(settings2) {
4773
4628
  return {
4774
- ...settings,
4775
- updateCheck: settings.updateCheck ?? {},
4776
- telemetry: settings.telemetry ?? {}
4629
+ ...settings2,
4630
+ updateCheck: settings2.updateCheck ?? {},
4631
+ telemetry: settings2.telemetry ?? {}
4777
4632
  };
4778
4633
  }
4779
4634
  function writeUserSettings(patch) {
@@ -4793,24 +4648,24 @@ function writeUserSettings(patch) {
4793
4648
  }
4794
4649
  }
4795
4650
  function isUpdateCheckEnabled() {
4796
- const settings = readUserSettings();
4797
- return settings.updateCheck?.enabled !== false;
4651
+ const settings2 = readUserSettings();
4652
+ return settings2.updateCheck?.enabled !== false;
4798
4653
  }
4799
4654
  function isErrorTelemetryEnabled() {
4800
- const settings = readUserSettings();
4801
- return settings.telemetry?.errorsEnabled === true;
4655
+ const settings2 = readUserSettings();
4656
+ return settings2.telemetry?.errorsEnabled === true;
4802
4657
  }
4803
4658
  function isUsageCliTelemetryEnabled() {
4804
- const settings = readUserSettings();
4805
- return settings.telemetry?.usageCliEnabled === true;
4659
+ const settings2 = readUserSettings();
4660
+ return settings2.telemetry?.usageCliEnabled === true;
4806
4661
  }
4807
4662
  function isUsageUiTelemetryEnabled() {
4808
- const settings = readUserSettings();
4809
- return settings.telemetry?.usageUiEnabled === true;
4663
+ const settings2 = readUserSettings();
4664
+ return settings2.telemetry?.usageUiEnabled === true;
4810
4665
  }
4811
4666
  function readAnonymousId() {
4812
- const settings = readUserSettings();
4813
- return settings.telemetry?.anonymousId ?? null;
4667
+ const settings2 = readUserSettings();
4668
+ return settings2.telemetry?.anonymousId ?? null;
4814
4669
  }
4815
4670
  function ensureAnonymousId(generate = () => randomUUID()) {
4816
4671
  const existing = readAnonymousId();
@@ -4820,12 +4675,12 @@ function ensureAnonymousId(generate = () => randomUUID()) {
4820
4675
  return id;
4821
4676
  }
4822
4677
  function hasTelemetryPromptBeenShown() {
4823
- const settings = readUserSettings();
4824
- return typeof settings.telemetry?.promptedAt === "number";
4678
+ const settings2 = readUserSettings();
4679
+ return typeof settings2.telemetry?.promptedAt === "number";
4825
4680
  }
4826
4681
  function hasSeenFirstRun() {
4827
- const settings = readUserSettings();
4828
- return typeof settings.telemetry?.firstRunAt === "number";
4682
+ const settings2 = readUserSettings();
4683
+ return typeof settings2.telemetry?.firstRunAt === "number";
4829
4684
  }
4830
4685
  function mergeSettings(current, patch) {
4831
4686
  const merged = {
@@ -4980,8 +4835,6 @@ var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", v
4980
4835
  var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: VERSION };
4981
4836
  var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: VERSION };
4982
4837
  var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: VERSION };
4983
- var supersedeAnalyzer2 = { ...supersedeAnalyzer, pluginId: "core", version: VERSION };
4984
- var tagsAnalyzer2 = { ...tagsAnalyzer, pluginId: "core", version: VERSION };
4985
4838
  var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: VERSION };
4986
4839
  var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: VERSION };
4987
4840
  var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: VERSION };
@@ -5048,8 +4901,6 @@ var builtInPlugins = [
5048
4901
  referenceRedundantAnalyzer2,
5049
4902
  schemaViolationAnalyzer2,
5050
4903
  signalCollisionAnalyzer2,
5051
- supersedeAnalyzer2,
5052
- tagsAnalyzer2,
5053
4904
  triggerCollisionAnalyzer2,
5054
4905
  asciiFormatter2,
5055
4906
  jsonFormatter2,
@@ -6239,6 +6090,10 @@ function removeConfigValue(key, opts) {
6239
6090
  writeJsonAtomic(path, merged);
6240
6091
  return true;
6241
6092
  }
6093
+ function getValueSource(key, opts) {
6094
+ const loaded = loadConfigForScope(opts);
6095
+ return loaded.sources.get(key);
6096
+ }
6242
6097
  function loadConfigForScope(opts) {
6243
6098
  return loadConfig({
6244
6099
  cwd: opts.cwd,
@@ -6458,7 +6313,7 @@ function resolveSpecRoot3() {
6458
6313
  }
6459
6314
 
6460
6315
  // cli/i18n/bump.texts.ts
6461
- var BUMP_TEXTS = {
6316
+ var BUMP_TEXTS2 = {
6462
6317
  // --- argument validation --------------------------------------------------
6463
6318
  /**
6464
6319
  * §3.1b two-line block. Mutex between the positional <node.path> and
@@ -8149,6 +8004,7 @@ function rowToNode(row) {
8149
8004
  const parsed = JSON.parse(row.externalRefsJson);
8150
8005
  if (Array.isArray(parsed) && parsed.length > 0) node.externalRefs = parsed;
8151
8006
  }
8007
+ if (row.modifiedAtMs !== null) node.modifiedAtMs = row.modifiedAtMs;
8152
8008
  return node;
8153
8009
  }
8154
8010
  function rowToLink(row) {
@@ -8665,7 +8521,10 @@ function nodeToRow(node, scannedAt) {
8665
8521
  // the column stays sparse for nodes whose bodies have no http(s)
8666
8522
  // URLs at all. Round-tripped by `rowToNode` on load.
8667
8523
  externalRefsJson: node.externalRefs && node.externalRefs.length > 0 ? JSON.stringify(node.externalRefs) : null,
8668
- scannedAt
8524
+ scannedAt,
8525
+ // File mtime (Unix ms) from the walker; NULL for virtual / derived
8526
+ // nodes that carry no backing file. Round-tripped by `rowToNode`.
8527
+ modifiedAtMs: node.modifiedAtMs ?? null
8669
8528
  };
8670
8529
  }
8671
8530
  function projectAnnotationColumns(node) {
@@ -9666,10 +9525,10 @@ var BumpCommand = class extends SmCommand {
9666
9525
  );
9667
9526
  if (!persisted) {
9668
9527
  this.printer.error(
9669
- tx(BUMP_TEXTS.nodeNotFound, {
9528
+ tx(BUMP_TEXTS2.nodeNotFound, {
9670
9529
  glyph: ansi.red("\u2715"),
9671
9530
  nodePath: this.nodePath ?? "<pending>",
9672
- hint: ansi.dim(BUMP_TEXTS.nodeNotFoundHint)
9531
+ hint: ansi.dim(BUMP_TEXTS2.nodeNotFoundHint)
9673
9532
  })
9674
9533
  );
9675
9534
  return ExitCode.NotFound;
@@ -9689,27 +9548,27 @@ var BumpCommand = class extends SmCommand {
9689
9548
  const errGlyph = ansi.red("\u2715");
9690
9549
  if (this.pending && this.nodePath !== void 0) {
9691
9550
  this.printer.error(
9692
- tx(BUMP_TEXTS.nodeAndPendingMutex, {
9551
+ tx(BUMP_TEXTS2.nodeAndPendingMutex, {
9693
9552
  glyph: errGlyph,
9694
- hint: ansi.dim(BUMP_TEXTS.nodeAndPendingMutexHint)
9553
+ hint: ansi.dim(BUMP_TEXTS2.nodeAndPendingMutexHint)
9695
9554
  })
9696
9555
  );
9697
9556
  return ExitCode.Error;
9698
9557
  }
9699
9558
  if (!this.pending && this.nodePath === void 0) {
9700
9559
  this.printer.error(
9701
- tx(BUMP_TEXTS.noTargetSpecified, {
9560
+ tx(BUMP_TEXTS2.noTargetSpecified, {
9702
9561
  glyph: errGlyph,
9703
- hint: ansi.dim(BUMP_TEXTS.noTargetSpecifiedHint)
9562
+ hint: ansi.dim(BUMP_TEXTS2.noTargetSpecifiedHint)
9704
9563
  })
9705
9564
  );
9706
9565
  return ExitCode.Error;
9707
9566
  }
9708
9567
  if (this.staged && !this.pending) {
9709
9568
  this.printer.error(
9710
- tx(BUMP_TEXTS.stagedRequiresPending, {
9569
+ tx(BUMP_TEXTS2.stagedRequiresPending, {
9711
9570
  glyph: errGlyph,
9712
- hint: ansi.dim(BUMP_TEXTS.stagedRequiresPendingHint)
9571
+ hint: ansi.dim(BUMP_TEXTS2.stagedRequiresPendingHint)
9713
9572
  })
9714
9573
  );
9715
9574
  return ExitCode.Error;
@@ -9768,10 +9627,10 @@ var BumpCommand = class extends SmCommand {
9768
9627
  const node = nodes.find((n) => n.path === this.nodePath);
9769
9628
  if (!node) {
9770
9629
  this.printer.error(
9771
- tx(BUMP_TEXTS.nodeNotFound, {
9630
+ tx(BUMP_TEXTS2.nodeNotFound, {
9772
9631
  glyph: ansi.red("\u2715"),
9773
9632
  nodePath: this.nodePath,
9774
- hint: ansi.dim(BUMP_TEXTS.nodeNotFoundHint)
9633
+ hint: ansi.dim(BUMP_TEXTS2.nodeNotFoundHint)
9775
9634
  })
9776
9635
  );
9777
9636
  return ExitCode.NotFound;
@@ -9793,9 +9652,9 @@ var BumpCommand = class extends SmCommand {
9793
9652
  const errGlyph = ansi.red("\u2715");
9794
9653
  if (item.status === "error") {
9795
9654
  this.printer.error(
9796
- tx(BUMP_TEXTS.bumpFailed, {
9655
+ tx(BUMP_TEXTS2.bumpFailed, {
9797
9656
  glyph: errGlyph,
9798
- message: tx(BUMP_TEXTS.resolveAbsPathFailed, {
9657
+ message: tx(BUMP_TEXTS2.resolveAbsPathFailed, {
9799
9658
  nodePath: node.path,
9800
9659
  message: item.message
9801
9660
  })
@@ -9805,10 +9664,10 @@ var BumpCommand = class extends SmCommand {
9805
9664
  }
9806
9665
  if (item.status === "refused") {
9807
9666
  this.printer.error(
9808
- tx(BUMP_TEXTS.refusedFresh, {
9667
+ tx(BUMP_TEXTS2.refusedFresh, {
9809
9668
  glyph: errGlyph,
9810
9669
  nodePath: node.path,
9811
- hint: ansi.dim(BUMP_TEXTS.refusedFreshHint)
9670
+ hint: ansi.dim(BUMP_TEXTS2.refusedFreshHint)
9812
9671
  })
9813
9672
  );
9814
9673
  return ExitCode.Error;
@@ -9835,9 +9694,9 @@ var BumpCommand = class extends SmCommand {
9835
9694
  if (applied.error !== void 0) {
9836
9695
  if (applied.error instanceof EConsentRequiredError) throw applied.error;
9837
9696
  this.printer.error(
9838
- tx(BUMP_TEXTS.bumpFailed, {
9697
+ tx(BUMP_TEXTS2.bumpFailed, {
9839
9698
  glyph: ansi.red("\u2715"),
9840
- message: tx(BUMP_TEXTS.storeFailedDetail, {
9699
+ message: tx(BUMP_TEXTS2.storeFailedDetail, {
9841
9700
  path: applied.sidecarPath ?? sidecarPathFor(item.absPath),
9842
9701
  message: formatErrorMessage(applied.error)
9843
9702
  })
@@ -9854,11 +9713,11 @@ var BumpCommand = class extends SmCommand {
9854
9713
  const version = item.report.version ?? 1;
9855
9714
  if (item.report.createdSidecar === true) {
9856
9715
  this.printer.data(
9857
- tx(BUMP_TEXTS.bumpedCreated, { glyph: okGlyph, sidecarPath, nodePath: node.path, version })
9716
+ tx(BUMP_TEXTS2.bumpedCreated, { glyph: okGlyph, sidecarPath, nodePath: node.path, version })
9858
9717
  );
9859
9718
  } else {
9860
9719
  this.printer.data(
9861
- tx(BUMP_TEXTS.bumped, { glyph: okGlyph, nodePath: node.path, version })
9720
+ tx(BUMP_TEXTS2.bumped, { glyph: okGlyph, nodePath: node.path, version })
9862
9721
  );
9863
9722
  }
9864
9723
  return ExitCode.Ok;
@@ -9870,7 +9729,7 @@ var BumpCommand = class extends SmCommand {
9870
9729
  const stale = nodes.filter((n) => n.sidecar?.present === true && n.sidecar.status !== null && n.sidecar.status !== "fresh").sort((a, b) => a.path.localeCompare(b.path));
9871
9730
  if (stale.length === 0) return this.#renderEmptyPending();
9872
9731
  if (!this.json) {
9873
- this.printer.info(tx(BUMP_TEXTS.pendingBanner, { count: stale.length }));
9732
+ this.printer.info(tx(BUMP_TEXTS2.pendingBanner, { count: stale.length }));
9874
9733
  }
9875
9734
  const plan = computeBumpPlan(stale, { cwd, force: this.force });
9876
9735
  const outcomes = await this.#executePending(plan, cwd, ansi);
@@ -9888,19 +9747,19 @@ var BumpCommand = class extends SmCommand {
9888
9747
  const gitOk = ensureGitForStaged(cwd);
9889
9748
  if (gitOk === "no-repo") {
9890
9749
  this.printer.error(
9891
- tx(BUMP_TEXTS.notInGitRepo, {
9750
+ tx(BUMP_TEXTS2.notInGitRepo, {
9892
9751
  glyph: errGlyph,
9893
9752
  cwd,
9894
- hint: ansi.dim(BUMP_TEXTS.notInGitRepoHint)
9753
+ hint: ansi.dim(BUMP_TEXTS2.notInGitRepoHint)
9895
9754
  })
9896
9755
  );
9897
9756
  return ExitCode.NotFound;
9898
9757
  }
9899
9758
  if (gitOk === "no-binary") {
9900
9759
  this.printer.error(
9901
- tx(BUMP_TEXTS.gitBinaryMissing, {
9760
+ tx(BUMP_TEXTS2.gitBinaryMissing, {
9902
9761
  glyph: errGlyph,
9903
- hint: ansi.dim(BUMP_TEXTS.gitBinaryMissingHint)
9762
+ hint: ansi.dim(BUMP_TEXTS2.gitBinaryMissingHint)
9904
9763
  })
9905
9764
  );
9906
9765
  return ExitCode.Error;
@@ -9938,7 +9797,7 @@ var BumpCommand = class extends SmCommand {
9938
9797
  return {
9939
9798
  nodePath: item.nodePath,
9940
9799
  status: "error",
9941
- message: tx(BUMP_TEXTS.storeFailedDetail, {
9800
+ message: tx(BUMP_TEXTS2.storeFailedDetail, {
9942
9801
  path: applied.sidecarPath ?? sidecarPathFor(item.absPath),
9943
9802
  message: formatErrorMessage(applied.error)
9944
9803
  })
@@ -9958,11 +9817,11 @@ var BumpCommand = class extends SmCommand {
9958
9817
  const addErr = stageSidecar(cwd, sidecarPath);
9959
9818
  if (addErr === null || this.json) return;
9960
9819
  this.printer.warn(
9961
- tx(BUMP_TEXTS.gitAddFailed, {
9820
+ tx(BUMP_TEXTS2.gitAddFailed, {
9962
9821
  glyph: ansi.yellow("\u26A0"),
9963
9822
  path: sidecarPath,
9964
9823
  message: addErr,
9965
- hint: ansi.dim(tx(BUMP_TEXTS.gitAddFailedHint, { path: sidecarPath }))
9824
+ hint: ansi.dim(tx(BUMP_TEXTS2.gitAddFailedHint, { path: sidecarPath }))
9966
9825
  })
9967
9826
  );
9968
9827
  }
@@ -9982,7 +9841,7 @@ var BumpCommand = class extends SmCommand {
9982
9841
  this.printer.data(JSON.stringify(empty) + "\n");
9983
9842
  return ExitCode.Ok;
9984
9843
  }
9985
- this.printer.data(BUMP_TEXTS.pendingNone);
9844
+ this.printer.data(BUMP_TEXTS2.pendingNone);
9986
9845
  return ExitCode.Ok;
9987
9846
  }
9988
9847
  // Complexity is from per-status rendering (4 status values) plus
@@ -10013,24 +9872,24 @@ var BumpCommand = class extends SmCommand {
10013
9872
  for (const o of outcomes) {
10014
9873
  if (o.status === "bumped") {
10015
9874
  this.printer.data(
10016
- tx(BUMP_TEXTS.bumpedItem, {
9875
+ tx(BUMP_TEXTS2.bumpedItem, {
10017
9876
  nodePath: o.nodePath,
10018
9877
  version: o.version ?? 0,
10019
9878
  createdSuffix: o.createdSidecar === true ? " (new sidecar)" : ""
10020
9879
  })
10021
9880
  );
10022
9881
  } else if (o.status === "refused") {
10023
- this.printer.data(tx(BUMP_TEXTS.refusedItem, { nodePath: o.nodePath }));
9882
+ this.printer.data(tx(BUMP_TEXTS2.refusedItem, { nodePath: o.nodePath }));
10024
9883
  } else if (o.status === "skipped") {
10025
9884
  this.printer.data(
10026
- tx(BUMP_TEXTS.skippedItem, {
9885
+ tx(BUMP_TEXTS2.skippedItem, {
10027
9886
  nodePath: o.nodePath,
10028
9887
  reason: o.reason ?? "unknown"
10029
9888
  })
10030
9889
  );
10031
9890
  } else {
10032
9891
  this.printer.data(
10033
- tx(BUMP_TEXTS.errorItem, {
9892
+ tx(BUMP_TEXTS2.errorItem, {
10034
9893
  nodePath: o.nodePath,
10035
9894
  message: o.message ?? ""
10036
9895
  })
@@ -10038,7 +9897,7 @@ var BumpCommand = class extends SmCommand {
10038
9897
  }
10039
9898
  }
10040
9899
  this.printer.info(
10041
- tx(BUMP_TEXTS.pendingSummary, {
9900
+ tx(BUMP_TEXTS2.pendingSummary, {
10042
9901
  bumped: counts.bumped,
10043
9902
  refused: counts.refused,
10044
9903
  skipped: counts.skipped,
@@ -11091,29 +10950,39 @@ function isPluginLocked(idOrQualified) {
11091
10950
  }
11092
10951
 
11093
10952
  // kernel/config/plugin-resolver.ts
11094
- function resolvePluginEnabled(pluginId, cfg, dbOverrides) {
10953
+ var SHIPS_DISABLED = /* @__PURE__ */ new Set([
10954
+ "experimental",
10955
+ "deprecated"
10956
+ ]);
10957
+ function installedDefaultEnabled(stability) {
10958
+ return stability === void 0 || !SHIPS_DISABLED.has(stability);
10959
+ }
10960
+ function resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault = true) {
11095
10961
  if (isPluginLocked(pluginId)) return true;
11096
10962
  if (dbOverrides.has(pluginId)) return dbOverrides.get(pluginId) === true;
11097
10963
  const settingsEntry = cfg.plugins[pluginId];
11098
10964
  if (settingsEntry?.enabled !== void 0) return settingsEntry.enabled;
11099
- return true;
10965
+ return installedDefault;
11100
10966
  }
11101
10967
  function makeEnabledResolver(cfg, dbOverrides) {
11102
- return (pluginId) => resolvePluginEnabled(pluginId, cfg, dbOverrides);
10968
+ return (pluginId, installedDefault) => resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault);
11103
10969
  }
11104
10970
 
11105
10971
  // core/runtime/plugin-runtime/resolver.ts
11106
- function defaultResolveEnabled(_id) {
11107
- return true;
10972
+ function defaultResolveEnabled(_id, installedDefault = true) {
10973
+ return installedDefault;
11108
10974
  }
11109
10975
  function isBuiltInExtensionEnabled(plugin, ext, resolveEnabled) {
11110
- return isPluginEntryEnabled(plugin, ext.id, resolveEnabled);
10976
+ return isPluginEntryEnabled(plugin, ext.id, resolveEnabled, ext.stability);
11111
10977
  }
11112
- function isPluginEntryEnabled(plugin, extId, resolveEnabled) {
11113
- return resolveEnabled(qualifiedExtensionId(plugin.id, extId));
10978
+ function isPluginEntryEnabled(plugin, extId, resolveEnabled, stability) {
10979
+ return resolveEnabled(qualifiedExtensionId(plugin.id, extId), installedDefaultEnabled(stability));
11114
10980
  }
11115
10981
  function isPluginExtensionEnabled(ext, resolveEnabled) {
11116
- return resolveEnabled(qualifiedExtensionId(ext.pluginId, ext.id));
10982
+ return resolveEnabled(
10983
+ qualifiedExtensionId(ext.pluginId, ext.id),
10984
+ installedDefaultEnabled(ext.stability)
10985
+ );
11117
10986
  }
11118
10987
  async function buildEnabledResolver(ctx) {
11119
10988
  const { effective: cfg } = loadConfig({ ...ctx });
@@ -11311,11 +11180,11 @@ async function* walkContent(roots, options) {
11311
11180
  const extensions = options.extensions;
11312
11181
  const sizeLimit = buildSizeLimit(options);
11313
11182
  for (const root of roots) {
11314
- for await (const file of walkRoot(root, root, filter, extensions, sizeLimit)) {
11315
- const relPath = relative2(root, file).split(sep3).join("/");
11183
+ for await (const entry of walkRoot(root, root, filter, extensions, sizeLimit)) {
11184
+ const relPath = relative2(root, entry.full).split(sep3).join("/");
11316
11185
  let raw;
11317
11186
  try {
11318
- raw = await readFile(file, "utf8");
11187
+ raw = await readFile(entry.full, "utf8");
11319
11188
  } catch {
11320
11189
  continue;
11321
11190
  }
@@ -11325,6 +11194,9 @@ async function* walkContent(roots, options) {
11325
11194
  body: parsed.body,
11326
11195
  frontmatterRaw: parsed.frontmatterRaw,
11327
11196
  frontmatter: parsed.frontmatter,
11197
+ // File mtime from the TOCTOU `lstat` (zero extra syscalls).
11198
+ // Threaded onto the persisted `Node` as `modifiedAtMs`.
11199
+ modifiedAtMs: entry.modifiedAtMs,
11328
11200
  // Audit L1: forward parser diagnostics (e.g. malformed YAML)
11329
11201
  // through the IRawNode surface so the orchestrator can
11330
11202
  // convert them into warn-level kernel `Issue` rows. Omitted
@@ -11365,7 +11237,7 @@ async function* walkRoot(root, current, filter, extensions, sizeLimit) {
11365
11237
  sizeLimit.onOversizedFile?.({ path: rel, bytes: s.size });
11366
11238
  continue;
11367
11239
  }
11368
- yield full;
11240
+ yield { full, modifiedAtMs: Math.round(s.mtimeMs) };
11369
11241
  } catch {
11370
11242
  }
11371
11243
  }
@@ -11447,8 +11319,8 @@ function bucketLoaded(loaded, runtime, pluginOrder) {
11447
11319
  extractor: runtime.extensions.extractors,
11448
11320
  analyzer: runtime.extensions.analyzers,
11449
11321
  formatter: runtime.extensions.formatters,
11450
- hook: runtime.extensions.hooks
11451
- // `action` intentionally absent, see docstring.
11322
+ hook: runtime.extensions.hooks,
11323
+ action: runtime.extensions.actions
11452
11324
  });
11453
11325
  runtime.manifests.push({
11454
11326
  id: ext.id,
@@ -11563,7 +11435,7 @@ async function loadPluginRuntime(opts = {}) {
11563
11435
  const loader = createPluginLoader(loaderOpts);
11564
11436
  const discovered = await loader.discoverAndLoadAll();
11565
11437
  const runtime = {
11566
- extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [] },
11438
+ extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [], actions: [] },
11567
11439
  annotationContributions: [],
11568
11440
  viewContributions: [],
11569
11441
  manifests: [],
@@ -11618,7 +11490,7 @@ function enforceRootExclusivity(catalog) {
11618
11490
  }
11619
11491
  function emptyPluginRuntime() {
11620
11492
  const runtime = {
11621
- extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [] },
11493
+ extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [], actions: [] },
11622
11494
  annotationContributions: [],
11623
11495
  viewContributions: [],
11624
11496
  manifests: [],
@@ -11636,50 +11508,59 @@ function emptyPluginRuntime() {
11636
11508
  function collectRegisteredContributionKeys(composed) {
11637
11509
  const keys = /* @__PURE__ */ new Set();
11638
11510
  if (!composed) return keys;
11639
- for (const ext of [...composed.extractors, ...composed.analyzers]) {
11640
- const raw = ext.ui;
11641
- if (typeof raw !== "object" || raw === null) continue;
11642
- for (const [contributionId, value] of Object.entries(raw)) {
11643
- if (typeof value !== "object" || value === null) continue;
11644
- keys.add(`${ext.pluginId}/${ext.id}/${contributionId}`);
11645
- }
11511
+ for (const ext of [...composed.extractors, ...composed.analyzers, ...composed.actions ?? []]) {
11512
+ addContributionKeysForExtension(ext, keys);
11646
11513
  }
11647
11514
  return keys;
11648
11515
  }
11516
+ function addContributionKeysForExtension(ext, keys) {
11517
+ const raw = ext.ui;
11518
+ if (typeof raw !== "object" || raw === null) return;
11519
+ for (const [contributionId, value] of Object.entries(raw)) {
11520
+ if (typeof value !== "object" || value === null) continue;
11521
+ keys.add(`${ext.pluginId}/${ext.id}/${contributionId}`);
11522
+ }
11523
+ }
11649
11524
  function filterBuiltInManifests(manifests, resolveEnabled) {
11650
11525
  const pluginById = /* @__PURE__ */ new Map();
11651
11526
  for (const plugin of builtInPlugins) pluginById.set(plugin.id, plugin);
11652
11527
  return manifests.filter((m) => {
11653
11528
  const plugin = pluginById.get(m.pluginId);
11654
11529
  if (!plugin) return true;
11655
- return isPluginEntryEnabled(plugin, m.id, resolveEnabled);
11530
+ return isPluginEntryEnabled(plugin, m.id, resolveEnabled, m.stability);
11656
11531
  });
11657
11532
  }
11658
11533
 
11659
11534
  // core/runtime/plugin-runtime/composer.ts
11660
11535
  function composeScanExtensions(opts) {
11661
11536
  const resolveEnabled = opts.resolveEnabled ?? opts.pluginRuntime.resolveEnabled;
11537
+ const resolveSettings = opts.resolveSettings;
11662
11538
  const providers = [];
11663
11539
  const extractors = [];
11664
11540
  const analyzers = [];
11665
11541
  const hooks = [];
11542
+ const actions = [];
11666
11543
  if (!opts.noBuiltIns) {
11667
11544
  accumulateBuiltInScanExtensions(
11668
- { providers, extractors, analyzers, hooks },
11669
- resolveEnabled
11545
+ { providers, extractors, analyzers, hooks, actions },
11546
+ resolveEnabled,
11547
+ resolveSettings
11670
11548
  );
11671
11549
  }
11672
11550
  for (const ext of opts.pluginRuntime.extensions.providers) {
11673
- if (isPluginExtensionEnabled(ext, resolveEnabled)) providers.push(ext);
11551
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) providers.push(withResolvedSettings(ext, resolveSettings));
11674
11552
  }
11675
11553
  for (const ext of opts.pluginRuntime.extensions.extractors) {
11676
- if (isPluginExtensionEnabled(ext, resolveEnabled)) extractors.push(ext);
11554
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) extractors.push(withResolvedSettings(ext, resolveSettings));
11677
11555
  }
11678
11556
  for (const ext of opts.pluginRuntime.extensions.analyzers) {
11679
- if (isPluginExtensionEnabled(ext, resolveEnabled)) analyzers.push(ext);
11557
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) analyzers.push(withResolvedSettings(ext, resolveSettings));
11680
11558
  }
11681
11559
  for (const ext of opts.pluginRuntime.extensions.hooks) {
11682
- if (isPluginExtensionEnabled(ext, resolveEnabled)) hooks.push(ext);
11560
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) hooks.push(withResolvedSettings(ext, resolveSettings));
11561
+ }
11562
+ for (const ext of opts.pluginRuntime.extensions.actions) {
11563
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) actions.push(ext);
11683
11564
  }
11684
11565
  const finalProviders = opts.killSwitches?.providers === true ? [] : providers;
11685
11566
  const finalExtractors = opts.killSwitches?.extractors === true ? [] : extractors;
@@ -11691,27 +11572,33 @@ function composeScanExtensions(opts) {
11691
11572
  providers: finalProviders,
11692
11573
  extractors: finalExtractors,
11693
11574
  analyzers: finalAnalyzers,
11694
- hooks
11575
+ hooks,
11576
+ actions
11695
11577
  };
11696
11578
  }
11697
- function accumulateBuiltInScanExtensions(buckets, resolveEnabled) {
11579
+ function withResolvedSettings(ext, resolveSettings) {
11580
+ if (!resolveSettings) return ext;
11581
+ return { ...ext, resolvedSettings: resolveSettings(ext) };
11582
+ }
11583
+ function accumulateBuiltInScanExtensions(buckets, resolveEnabled, resolveSettings) {
11698
11584
  for (const plugin of builtInPlugins) {
11699
11585
  for (const ext of plugin.extensions) {
11700
11586
  if (!isBuiltInExtensionEnabled(plugin, ext, resolveEnabled)) continue;
11701
11587
  switch (ext.kind) {
11702
11588
  case "provider":
11703
- buckets.providers.push(ext);
11589
+ buckets.providers.push(withResolvedSettings(ext, resolveSettings));
11704
11590
  break;
11705
11591
  case "extractor":
11706
- buckets.extractors.push(ext);
11592
+ buckets.extractors.push(withResolvedSettings(ext, resolveSettings));
11707
11593
  break;
11708
11594
  case "analyzer":
11709
- buckets.analyzers.push(ext);
11595
+ buckets.analyzers.push(withResolvedSettings(ext, resolveSettings));
11710
11596
  break;
11711
11597
  case "hook":
11712
- buckets.hooks.push(ext);
11598
+ buckets.hooks.push(withResolvedSettings(ext, resolveSettings));
11713
11599
  break;
11714
11600
  case "action":
11601
+ buckets.actions.push(ext);
11715
11602
  break;
11716
11603
  case "formatter":
11717
11604
  break;
@@ -11726,18 +11613,19 @@ function accumulateBuiltInScanExtensions(buckets, resolveEnabled) {
11726
11613
  function composeFormatters(opts) {
11727
11614
  const noBuiltIns = opts.noBuiltIns ?? false;
11728
11615
  const resolveEnabled = opts.resolveEnabled ?? opts.pluginRuntime.resolveEnabled;
11616
+ const resolveSettings = opts.resolveSettings;
11729
11617
  const out = [];
11730
11618
  if (!noBuiltIns) {
11731
11619
  for (const plugin of builtInPlugins) {
11732
11620
  for (const ext of plugin.extensions) {
11733
11621
  if (ext.kind !== "formatter") continue;
11734
11622
  if (!isBuiltInExtensionEnabled(plugin, ext, resolveEnabled)) continue;
11735
- out.push(ext);
11623
+ out.push(withResolvedSettings(ext, resolveSettings));
11736
11624
  }
11737
11625
  }
11738
11626
  }
11739
11627
  for (const ext of opts.pluginRuntime.extensions.formatters) {
11740
- if (isPluginExtensionEnabled(ext, resolveEnabled)) out.push(ext);
11628
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) out.push(withResolvedSettings(ext, resolveSettings));
11741
11629
  }
11742
11630
  return out;
11743
11631
  }
@@ -14924,6 +14812,9 @@ var GraphCommand = class extends SmCommand {
14924
14812
  nodes: scan.nodes,
14925
14813
  links: scan.links,
14926
14814
  issues: scan.issues,
14815
+ // Resolved settings of the formatter (empty when the formatter
14816
+ // declares none, or when the composer did not populate them).
14817
+ settings: formatter.resolvedSettings ?? {},
14927
14818
  // Pass the full persisted scan so format-specific renderers
14928
14819
  // that mirror a `ScanResult` envelope (today: built-in `json`)
14929
14820
  // can emit it verbatim without re-deriving fields like
@@ -15935,12 +15826,12 @@ function emitExtensionError(emitter, qualifiedId2, nodePath, data) {
15935
15826
  }
15936
15827
  function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, emitSignal, emitNode, store) {
15937
15828
  const scope = extractor.scope ?? "both";
15938
- const settings = extractor.resolvedSettings ?? {};
15829
+ const settings2 = extractor.resolvedSettings ?? {};
15939
15830
  return {
15940
15831
  node,
15941
15832
  body: scope === "frontmatter" ? "" : body,
15942
15833
  frontmatter: scope === "body" ? {} : frontmatter,
15943
- settings,
15834
+ settings: settings2,
15944
15835
  emitLink,
15945
15836
  enrichNode,
15946
15837
  emitContribution,
@@ -16163,8 +16054,83 @@ function isExternalUrlLink(link) {
16163
16054
  return EXTERNAL_URL_SCHEME_RE.test(link.target);
16164
16055
  }
16165
16056
 
16057
+ // kernel/orchestrator/action-projections.ts
16058
+ function runActionProjections(actions, nodes, links, emitter) {
16059
+ const contributions = [];
16060
+ const contributionErrors = [];
16061
+ const validators = loadSchemaValidators();
16062
+ for (const action of actions) {
16063
+ if (typeof action.project !== "function") continue;
16064
+ const qualifiedId2 = qualifiedExtensionId(action.pluginId, action.id);
16065
+ const declaredContributions = readDeclaredContributionRefs(action);
16066
+ const emitContribution = (nodePath, ref, payload) => {
16067
+ const declared = typeof ref === "object" && ref !== null ? declaredContributions.get(ref) : void 0;
16068
+ if (!declared) {
16069
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUndeclaredRef, {
16070
+ extractorId: qualifiedId2,
16071
+ nodePath
16072
+ });
16073
+ emitExtensionError(emitter, qualifiedId2, nodePath, {
16074
+ phase: "emitContribution",
16075
+ reason: "undeclared-contribution-ref",
16076
+ message
16077
+ });
16078
+ contributionErrors.push({
16079
+ pluginId: action.pluginId,
16080
+ extensionId: action.id,
16081
+ nodePath,
16082
+ reason: "undeclared-contribution-ref",
16083
+ message,
16084
+ emittedAt: Date.now()
16085
+ });
16086
+ return;
16087
+ }
16088
+ const result = validators.validateContributionPayload(declared.slot, payload);
16089
+ if (!result.ok) {
16090
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
16091
+ extractorId: qualifiedId2,
16092
+ contributionId: declared.id,
16093
+ nodePath,
16094
+ slot: declared.slot,
16095
+ errors: result.errors
16096
+ });
16097
+ emitExtensionError(emitter, qualifiedId2, nodePath, {
16098
+ phase: "emitContribution",
16099
+ contributionId: declared.id,
16100
+ slot: declared.slot,
16101
+ reason: result.errors,
16102
+ message
16103
+ });
16104
+ contributionErrors.push({
16105
+ pluginId: action.pluginId,
16106
+ extensionId: action.id,
16107
+ nodePath,
16108
+ reason: result.errors,
16109
+ message,
16110
+ contributionId: declared.id,
16111
+ slot: declared.slot,
16112
+ emittedAt: Date.now()
16113
+ });
16114
+ return;
16115
+ }
16116
+ contributions.push({
16117
+ pluginId: action.pluginId,
16118
+ extensionId: action.id,
16119
+ nodePath,
16120
+ contributionId: declared.id,
16121
+ slot: declared.slot,
16122
+ payload,
16123
+ emittedAt: Date.now()
16124
+ });
16125
+ };
16126
+ const ctx = { nodes, links, emitContribution };
16127
+ action.project(ctx);
16128
+ }
16129
+ return { contributions, contributionErrors };
16130
+ }
16131
+
16166
16132
  // kernel/orchestrator/analyzers.ts
16167
- async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, signals, seedIssues = []) {
16133
+ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, brokenLinks, signals, seedIssues = []) {
16168
16134
  const issues = [...seedIssues];
16169
16135
  const contributions = [];
16170
16136
  const contributionErrors = [];
@@ -16241,6 +16207,10 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16241
16207
  const emitted = await analyzer.evaluate({
16242
16208
  nodes,
16243
16209
  links: internalLinks,
16210
+ // `settings` is always populated (possibly empty) so analyzers can
16211
+ // read `ctx.settings.<id>` without a presence check. The composer
16212
+ // populated `resolvedSettings` on each composed analyzer.
16213
+ settings: analyzer.resolvedSettings ?? {},
16244
16214
  orphanSidecars: analyzerOrphans,
16245
16215
  sidecarRoots,
16246
16216
  annotationContributions,
@@ -16254,6 +16224,7 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16254
16224
  ...referenceablePaths ? { referenceablePaths } : {},
16255
16225
  ...cwd ? { cwd } : {},
16256
16226
  ...reservedNodePaths ? { reservedNodePaths } : {},
16227
+ ...brokenLinks ? { brokenLinks } : {},
16257
16228
  ...signals && signals.length > 0 ? { signals } : {},
16258
16229
  emitContribution
16259
16230
  });
@@ -16952,6 +16923,7 @@ function buildNode(args2) {
16952
16923
  externalRefsCount: 0,
16953
16924
  frontmatter: args2.frontmatter
16954
16925
  };
16926
+ if (args2.modifiedAtMs !== void 0) node.modifiedAtMs = args2.modifiedAtMs;
16955
16927
  if (args2.encoder) {
16956
16928
  node.tokens = countTokens(args2.encoder, args2.frontmatterRaw, args2.body);
16957
16929
  }
@@ -17067,7 +17039,10 @@ function buildFreshNodeAndValidateFrontmatter(opts) {
17067
17039
  frontmatter: opts.raw.frontmatter,
17068
17040
  bodyHash: opts.bodyHash,
17069
17041
  frontmatterHash: opts.frontmatterHash,
17070
- encoder: opts.encoder
17042
+ encoder: opts.encoder,
17043
+ // Thread the walker's mtime through; `buildNode` only attaches it
17044
+ // when present, so virtual / walk()-without-stat sources stay absent.
17045
+ modifiedAtMs: opts.raw.modifiedAtMs
17071
17046
  });
17072
17047
  const frontmatterIssues = [];
17073
17048
  if (opts.raw.parseIssues && opts.raw.parseIssues.length > 0) {
@@ -17472,6 +17447,7 @@ async function runScanInternal(_kernel, options) {
17472
17447
  walked.signals = resolved.resolvedSignals;
17473
17448
  const postWalkCtx = buildPostWalkTransformCtx(exts.providers, walked.nodes, activeProviderId);
17474
17449
  walked.internalLinks = applyPostWalkTransforms(walked.internalLinks, walked.nodes, postWalkCtx);
17450
+ const brokenLinks = collectBrokenLinks(walked.internalLinks, walked.nodes, postWalkCtx);
17475
17451
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
17476
17452
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
17477
17453
  await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
@@ -17493,6 +17469,7 @@ async function runScanInternal(_kernel, options) {
17493
17469
  emitter,
17494
17470
  hookDispatcher,
17495
17471
  postWalkCtx.reservedNodePaths,
17472
+ brokenLinks,
17496
17473
  walked.signals,
17497
17474
  // Seed the accumulator with orchestrator-emitted frontmatter
17498
17475
  // issues so the aggregate phase (`core/issue-counter`) counts
@@ -17501,6 +17478,13 @@ async function runScanInternal(_kernel, options) {
17501
17478
  walked.frontmatterIssues
17502
17479
  );
17503
17480
  mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);
17481
+ const projectionResult = runActionProjections(
17482
+ exts.actions ?? [],
17483
+ walked.nodes,
17484
+ walked.internalLinks,
17485
+ emitter
17486
+ );
17487
+ mergeActionProjections(walked, projectionResult, exts.actions);
17504
17488
  const issues = analyzerResult.issues;
17505
17489
  const silenced = options.ignoreFilter ? (path) => options.ignoreFilter.ignores(path) : void 0;
17506
17490
  const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues, silenced) : [];
@@ -17609,6 +17593,16 @@ function mergeAnalyzerEmissions(walked, analyzerResult, analyzers) {
17609
17593
  }
17610
17594
  }
17611
17595
  }
17596
+ function mergeActionProjections(walked, projectionResult, actions) {
17597
+ for (const c of projectionResult.contributions) walked.contributions.push(c);
17598
+ for (const e of projectionResult.contributionErrors) walked.contributionErrors.push(e);
17599
+ for (const action of actions ?? []) {
17600
+ if (action.ui === void 0 || typeof action.project !== "function") continue;
17601
+ for (const node of walked.nodes) {
17602
+ walked.freshlyRunTuples.add(`${action.pluginId}\0${action.id}\0${node.path}`);
17603
+ }
17604
+ }
17605
+ }
17612
17606
  function buildScanStats(walked, issues, start) {
17613
17607
  return {
17614
17608
  // `filesSkipped` is "files walked but not classified by any
@@ -17920,6 +17914,159 @@ function findOrphanJobFiles(jobsDir, referencedPaths) {
17920
17914
  return { orphanFilePaths: orphans, referencedCount: referencedPaths.size };
17921
17915
  }
17922
17916
 
17917
+ // core/config/plugin-settings.ts
17918
+ var defaultWarn = (message) => log.warn(message);
17919
+ function resolveExtensionSettings(manifest, config, onWarn = defaultWarn) {
17920
+ const declarations = manifest.settings;
17921
+ if (!declarations || Object.keys(declarations).length === 0) return {};
17922
+ const overrides = readSettingsOverrides(config, manifest.pluginId, manifest.id);
17923
+ const resolved = {};
17924
+ for (const [settingId, declaration] of Object.entries(declarations)) {
17925
+ const outcome = resolveOneSetting(manifest, settingId, declaration, overrides, onWarn);
17926
+ if (outcome.hasValue) resolved[settingId] = outcome.value;
17927
+ }
17928
+ return resolved;
17929
+ }
17930
+ function resolveOneSetting(manifest, settingId, declaration, overrides, onWarn) {
17931
+ const fallback = declarationDefault(declaration);
17932
+ const toFallback = () => fallback !== void 0 ? { hasValue: true, value: fallback } : { hasValue: false };
17933
+ if (!Object.prototype.hasOwnProperty.call(overrides, settingId)) return toFallback();
17934
+ const candidate = overrides[settingId];
17935
+ const check = validateSettingValue(declaration, candidate);
17936
+ if (check.ok) return { hasValue: true, value: candidate };
17937
+ onWarn(
17938
+ `Setting '${settingId}' for extension '${manifest.pluginId}/${manifest.id}' is invalid (${check.reason}); falling back to the declared default.`
17939
+ );
17940
+ return toFallback();
17941
+ }
17942
+ function buildSettingsResolver(config, onWarn = defaultWarn) {
17943
+ return (ext) => resolveExtensionSettings(ext, config, onWarn);
17944
+ }
17945
+ function declarationDefault(declaration) {
17946
+ return "default" in declaration ? declaration.default : void 0;
17947
+ }
17948
+ function readSettingsOverrides(config, pluginId, extId) {
17949
+ const entry = config.plugins?.[pluginId];
17950
+ const ext = entry?.extensions?.[extId];
17951
+ const settings2 = ext?.settings;
17952
+ if (settings2 && typeof settings2 === "object" && !Array.isArray(settings2)) {
17953
+ return settings2;
17954
+ }
17955
+ return {};
17956
+ }
17957
+ var OK = { ok: true, reason: "" };
17958
+ function fail2(reason) {
17959
+ return { ok: false, reason };
17960
+ }
17961
+ function validateSettingValue(declaration, value) {
17962
+ switch (declaration.type) {
17963
+ case "string-list":
17964
+ return validateStringList(value, declaration.min, declaration.max, declaration.itemMaxLength);
17965
+ case "single-string":
17966
+ return validateSingleString(value, declaration.minLength, declaration.maxLength, declaration.pattern);
17967
+ case "boolean-flag":
17968
+ return typeof value === "boolean" ? OK : fail2("expected a boolean");
17969
+ case "integer":
17970
+ return validateInteger(value, declaration.min, declaration.max);
17971
+ case "number":
17972
+ return validateNumber(value, declaration.min, declaration.max);
17973
+ case "enum-pick":
17974
+ return validateEnumPick(value, declaration.options.map((o) => o.value));
17975
+ case "enum-multipick":
17976
+ return validateEnumMultipick(value, declaration.options.map((o) => o.value), declaration.min, declaration.max);
17977
+ case "path-glob":
17978
+ return validatePathGlob(value, declaration.multiple === true);
17979
+ case "regex":
17980
+ return validateRegex(value, declaration.flags);
17981
+ case "secret":
17982
+ return typeof value === "string" ? OK : fail2("expected a string");
17983
+ case "key-value-list":
17984
+ return validateKeyValueList(value, declaration.min, declaration.max);
17985
+ default: {
17986
+ const _exhaustive = declaration;
17987
+ return fail2(`unknown input-type: ${String(_exhaustive.type)}`);
17988
+ }
17989
+ }
17990
+ }
17991
+ function isStringArray(value) {
17992
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
17993
+ }
17994
+ function validateStringList(value, min, max, itemMaxLength) {
17995
+ if (!isStringArray(value)) return fail2("expected an array of strings");
17996
+ if (min !== void 0 && value.length < min) return fail2(`expected at least ${min} item(s)`);
17997
+ if (max !== void 0 && value.length > max) return fail2(`expected at most ${max} item(s)`);
17998
+ const cap = itemMaxLength ?? 256;
17999
+ if (value.some((item) => item.length > cap)) return fail2(`item exceeds ${cap} characters`);
18000
+ return OK;
18001
+ }
18002
+ function validateSingleString(value, minLength, maxLength, pattern) {
18003
+ if (typeof value !== "string") return fail2("expected a string");
18004
+ if (minLength !== void 0 && value.length < minLength) return fail2(`expected at least ${minLength} characters`);
18005
+ if (maxLength !== void 0 && value.length > maxLength) return fail2(`expected at most ${maxLength} characters`);
18006
+ return matchesPattern(value, pattern);
18007
+ }
18008
+ function matchesPattern(value, pattern) {
18009
+ if (pattern === void 0) return OK;
18010
+ let re;
18011
+ try {
18012
+ re = new RegExp(pattern);
18013
+ } catch {
18014
+ return OK;
18015
+ }
18016
+ return re.test(value) ? OK : fail2(`does not match pattern ${pattern}`);
18017
+ }
18018
+ function validateInteger(value, min, max) {
18019
+ if (typeof value !== "number" || !Number.isInteger(value)) return fail2("expected an integer");
18020
+ if (!Number.isSafeInteger(value)) return fail2("integer out of safe range");
18021
+ if (min !== void 0 && value < min) return fail2(`expected >= ${min}`);
18022
+ if (max !== void 0 && value > max) return fail2(`expected <= ${max}`);
18023
+ return OK;
18024
+ }
18025
+ function validateNumber(value, min, max) {
18026
+ if (typeof value !== "number" || !Number.isFinite(value)) return fail2("expected a finite number");
18027
+ if (min !== void 0 && value < min) return fail2(`expected >= ${min}`);
18028
+ if (max !== void 0 && value > max) return fail2(`expected <= ${max}`);
18029
+ return OK;
18030
+ }
18031
+ function validateEnumPick(value, allowed) {
18032
+ if (typeof value !== "string") return fail2("expected a string");
18033
+ if (!allowed.includes(value)) return fail2(`expected one of: ${allowed.join(", ")}`);
18034
+ return OK;
18035
+ }
18036
+ function validateEnumMultipick(value, allowed, min, max) {
18037
+ if (!isStringArray(value)) return fail2("expected an array of strings");
18038
+ const allowedSet = new Set(allowed);
18039
+ if (value.some((v) => !allowedSet.has(v))) return fail2(`every entry must be one of: ${allowed.join(", ")}`);
18040
+ if (min !== void 0 && value.length < min) return fail2(`expected at least ${min} selection(s)`);
18041
+ if (max !== void 0 && value.length > max) return fail2(`expected at most ${max} selection(s)`);
18042
+ return OK;
18043
+ }
18044
+ function validatePathGlob(value, multiple) {
18045
+ if (multiple) {
18046
+ return isStringArray(value) ? OK : fail2("expected an array of glob strings");
18047
+ }
18048
+ return typeof value === "string" ? OK : fail2("expected a glob string");
18049
+ }
18050
+ function validateRegex(value, flags) {
18051
+ if (typeof value !== "string") return fail2("expected a string");
18052
+ try {
18053
+ new RegExp(value, flags ?? "");
18054
+ } catch (err) {
18055
+ return fail2(`is not a compilable regex (${err instanceof Error ? err.message : String(err)})`);
18056
+ }
18057
+ return OK;
18058
+ }
18059
+ function validateKeyValueList(value, min, max) {
18060
+ if (!Array.isArray(value)) return fail2("expected an array of { key, value } entries");
18061
+ const wellShaped = value.every(
18062
+ (entry) => entry !== null && typeof entry === "object" && typeof entry.key === "string" && typeof entry.value === "string"
18063
+ );
18064
+ if (!wellShaped) return fail2("every entry must be { key: string, value: string }");
18065
+ if (min !== void 0 && value.length < min) return fail2(`expected at least ${min} entr(y/ies)`);
18066
+ if (max !== void 0 && value.length > max) return fail2(`expected at most ${max} entr(y/ies)`);
18067
+ return OK;
18068
+ }
18069
+
17923
18070
  // core/runtime/i18n/progress-emitter.texts.ts
17924
18071
  var PROGRESS_EMITTER_TEXTS = {
17925
18072
  /**
@@ -18420,10 +18567,10 @@ async function runScanForCommand(opts) {
18420
18567
  const dbPath = resolveDbPath({ db: void 0, ...ctx });
18421
18568
  const kernel = createKernel();
18422
18569
  const pluginRuntime = await preparePluginRuntime(opts, opts.printer);
18423
- const extensions = registerExtensions(kernel, pluginRuntime, opts);
18424
18570
  const scanInputs = loadScanInputs(opts, ctx);
18425
18571
  if ("kind" in scanInputs) return scanInputs;
18426
18572
  const { cfg, ignoreFilter, strict, effectiveRoots } = scanInputs;
18573
+ const extensions = registerExtensions(kernel, pluginRuntime, opts, cfg);
18427
18574
  let referenceablePaths;
18428
18575
  if (cfg.scan.referencePaths.length > 0) {
18429
18576
  const walk3 = walkReferencePaths(cfg.scan.referencePaths, ctx.cwd);
@@ -18519,10 +18666,11 @@ async function preparePluginRuntime(opts, printer) {
18519
18666
  pluginRuntime.emitWarnings(printer);
18520
18667
  return pluginRuntime;
18521
18668
  }
18522
- function registerExtensions(kernel, pluginRuntime, opts) {
18669
+ function registerExtensions(kernel, pluginRuntime, opts, cfg) {
18523
18670
  const composeOpts = {
18524
18671
  noBuiltIns: opts.noBuiltIns,
18525
- pluginRuntime
18672
+ pluginRuntime,
18673
+ resolveSettings: buildSettingsResolver(cfg)
18526
18674
  };
18527
18675
  if (opts.killSwitches) composeOpts.killSwitches = opts.killSwitches;
18528
18676
  if (opts.resolveEnabledOverride) composeOpts.resolveEnabled = opts.resolveEnabledOverride;
@@ -20066,7 +20214,7 @@ async function findActiveOrphanIssues(adapter, predicate) {
20066
20214
  (issue) => ORPHAN_RULE_IDS.includes(issue.analyzerId) && predicate(issue)
20067
20215
  );
20068
20216
  }
20069
- function isStringArray(v) {
20217
+ function isStringArray2(v) {
20070
20218
  return Array.isArray(v) && v.every((s) => typeof s === "string");
20071
20219
  }
20072
20220
  var OrphansCommand = class extends SmCommand {
@@ -20437,7 +20585,7 @@ var OrphansUndoRenameCommand = class extends SmCommand {
20437
20585
  return { ok: false, exitCode: ExitCode.NotFound };
20438
20586
  }
20439
20587
  const dataCandidates = issue.data ? issue.data["candidates"] : void 0;
20440
- if (!isStringArray(dataCandidates) || !dataCandidates.includes(this.from)) {
20588
+ if (!isStringArray2(dataCandidates) || !dataCandidates.includes(this.from)) {
20441
20589
  this.printer.error(
20442
20590
  tx(ORPHANS_TEXTS.undoAmbiguousNotInCandidates, { glyph: errGlyph, from: this.from })
20443
20591
  );
@@ -20513,6 +20661,17 @@ var PLUGINS_TEXTS = {
20513
20661
  qualifiedIdNotFoundHint: "Run `sm plugins list` to see what each plugin ships.",
20514
20662
  qualifiedIdUnknownPlugin: "{{glyph}} Qualified extension id references unknown plugin: {{pluginId}}\n {{hint}}\n",
20515
20663
  qualifiedIdUnknownPluginHint: "Run `sm plugins list` for known plugin ids.",
20664
+ // --- verb-shape redirects (show is extension-only; list is plugin-only) ---
20665
+ // `sm plugins show` takes a qualified `<plugin>/<ext>` id and renders a
20666
+ // single extension. A bare plugin id is the wrong granularity, redirect
20667
+ // to `sm plugins list <id>`, which renders the whole plugin.
20668
+ showBareId: '{{glyph}} `sm plugins show` needs a qualified `<plugin>/<ext>` id; "{{id}}" is a plugin.\n {{hint}}\n',
20669
+ showBareIdHint: "Run `sm plugins list {{id}}` for the plugin and its extensions, then `sm plugins show {{id}}/<ext>` for one.",
20670
+ // `sm plugins list <id>` takes a bare plugin id. A qualified
20671
+ // `<plugin>/<ext>` id targets a single extension, redirect to
20672
+ // `sm plugins show`.
20673
+ listQualifiedId: "{{glyph}} `sm plugins list` takes a plugin id, not a qualified `<plugin>/<ext>` id: {{id}}\n {{hint}}\n",
20674
+ listQualifiedIdHint: "Run `sm plugins show {{id}}` for that extension, or `sm plugins list {{pluginId}}` for the whole plugin.",
20516
20675
  // Spec § A.10, `applicableKinds` filter on Extractors. When an extractor
20517
20676
  // declares a kind that no installed Provider emits, the load succeeds
20518
20677
  // (the Provider may arrive later) but `sm plugins doctor` surfaces a
@@ -20653,7 +20812,7 @@ var PLUGINS_TEXTS = {
20653
20812
  * (declared or defaulted) renders no tag.
20654
20813
  */
20655
20814
  stabilityTag: " ({{stability}})",
20656
- listTipShow: "\nTip: `sm plugins show <id>` for kinds, versions, and per-extension status.\n",
20815
+ listTipShow: "\nTip: `sm plugins list <id>` for a plugin's extensions (kinds, versions, per-extension status), `sm plugins show <plugin>/<ext>` for one extension.\n",
20657
20816
  /** Show command, built-in header (no version row, no path). */
20658
20817
  detailHeaderBuiltIn: " {{glyph}} {{id}} {{source}} {{count}} extension{{plural}}\n",
20659
20818
  /**
@@ -20805,7 +20964,7 @@ function extensionRowFromBuiltIn(ext, plugin, resolveEnabled) {
20805
20964
  id: ext.id,
20806
20965
  kind: ext.kind,
20807
20966
  version: ext.version,
20808
- enabled: resolveEnabled(qualifiedExtensionId(plugin.id, ext.id)),
20967
+ enabled: resolveEnabled(qualifiedExtensionId(plugin.id, ext.id), installedDefaultEnabled(ext.stability)),
20809
20968
  description: ext.description ?? ""
20810
20969
  };
20811
20970
  if (ext.stability !== void 0) row.stability = ext.stability;
@@ -20822,6 +20981,43 @@ function omitModule(key, value) {
20822
20981
  const tag = value[Symbol.toStringTag];
20823
20982
  return tag === "Module" ? void 0 : value;
20824
20983
  }
20984
+ function pluginCatalogue(plugins) {
20985
+ const out = [];
20986
+ for (const plugin of builtInPlugins) {
20987
+ out.push({ id: plugin.id, extensionIds: plugin.extensions.map((e) => e.id) });
20988
+ }
20989
+ for (const p of plugins) {
20990
+ out.push({ id: p.id, extensionIds: p.extensions?.map((e) => e.id) ?? [] });
20991
+ }
20992
+ return out;
20993
+ }
20994
+ function parseQualifiedExtensionId(id, catalogue) {
20995
+ const [pluginId, extId, ...rest] = id.split("/");
20996
+ if (!pluginId || !extId || rest.length > 0) return { ok: false, reason: "malformed" };
20997
+ const plugin = catalogue.find((p) => p.id === pluginId);
20998
+ if (!plugin) return { ok: false, reason: "unknown-plugin", pluginId };
20999
+ if (!plugin.extensionIds.includes(extId)) {
21000
+ return { ok: false, reason: "unknown-extension", pluginId, extId };
21001
+ }
21002
+ return { ok: true, pluginId, extId };
21003
+ }
21004
+ function renderQualifiedIdError(result, rawId, ansi) {
21005
+ const glyph = ansi.red(PLUGINS_TEXTS.rowGlyphOff);
21006
+ if (result.reason === "unknown-extension") {
21007
+ return tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
21008
+ glyph,
21009
+ id: sanitizeForTerminal(rawId),
21010
+ pluginId: sanitizeForTerminal(result.pluginId ?? ""),
21011
+ extId: sanitizeForTerminal(result.extId ?? ""),
21012
+ hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdNotFoundHint)
21013
+ });
21014
+ }
21015
+ return tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
21016
+ glyph,
21017
+ pluginId: sanitizeForTerminal(result.reason === "unknown-plugin" ? result.pluginId ?? rawId : rawId),
21018
+ hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
21019
+ });
21020
+ }
20825
21021
  function wrapText(text, maxWidth) {
20826
21022
  const words = text.split(/\s+/).filter((w) => w.length > 0);
20827
21023
  if (words.length === 0) return [];
@@ -20845,14 +21041,24 @@ var PluginsListCommand = class extends SmCommand {
20845
21041
  static paths = [["plugins", "list"]];
20846
21042
  static usage = Command22.Usage({
20847
21043
  category: "Plugins",
20848
- description: "List discovered plugins and their load status.",
20849
- details: "Scans <cwd>/.skill-map/plugins (or --plugin-dir <path>). Built-in plugins (claude, core) are listed alongside user plugins."
21044
+ description: "List discovered plugins, or one plugin's extensions.",
21045
+ details: `
21046
+ No id: scans <cwd>/.skill-map/plugins (or --plugin-dir <path>) and
21047
+ lists every plugin (built-in + user) with status, one row each.
21048
+ With a bare plugin id: renders that plugin's manifest and its
21049
+ extensions (kind / version / per-extension status). A qualified
21050
+ \`<plugin>/<ext>\` id is rejected with a redirect to \`sm plugins show\`.
21051
+ `
20850
21052
  });
21053
+ id = Option21.String({ required: false });
20851
21054
  pluginDir = Option21.String("--plugin-dir", { required: false });
20852
21055
  async run() {
20853
21056
  const plugins = await loadAll({ pluginDir: this.pluginDir });
20854
21057
  const resolveEnabled = await buildResolver();
20855
21058
  const builtIns2 = builtInRows(resolveEnabled);
21059
+ if (this.id !== void 0) {
21060
+ return this.renderPluginDetailById(this.id, builtIns2, plugins);
21061
+ }
20856
21062
  if (this.json) {
20857
21063
  this.printer.data(
20858
21064
  JSON.stringify({ builtIns: builtIns2, plugins }, omitModule, 2) + "\n"
@@ -20864,134 +21070,45 @@ var PluginsListCommand = class extends SmCommand {
20864
21070
  return ExitCode.Ok;
20865
21071
  }
20866
21072
  const ansi = this.ansiFor("stdout");
20867
- this.printer.data(renderListHuman(builtIns2, plugins, resolveEnabled, ansi));
21073
+ this.printer.data(renderIndexHuman(builtIns2, plugins, resolveEnabled, ansi));
20868
21074
  return ExitCode.Ok;
20869
21075
  }
20870
- };
20871
- function renderListHuman(builtIns2, plugins, resolveEnabled, ansi) {
20872
- const rows = [
20873
- ...builtIns2.map(builtInToListRow),
20874
- ...plugins.map((p) => pluginToListRow(p, resolveEnabled))
20875
- ];
20876
- const idWidth = Math.max(...rows.map((r) => r.id.length));
20877
- const countWidth = Math.max(
20878
- ...rows.map((r) => String(r.names.length).length)
20879
- );
20880
- const lines = [];
20881
- for (const row of rows) {
20882
- const glyph = row.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
20883
- const idCol = row.id.padEnd(idWidth);
20884
- const countCol = String(row.names.length).padStart(countWidth);
20885
- lines.push(
20886
- tx(PLUGINS_TEXTS.pluginRow, {
20887
- glyph,
20888
- id: idCol,
20889
- count: ` ${countCol}`,
20890
- source: ansi.dim(row.source)
20891
- })
20892
- );
20893
- const indent = PLUGINS_TEXTS.pluginSubIndent;
20894
- if (row.reason) {
20895
- lines.push(`${indent}${ansi.dim(row.reason)}`);
20896
- } else if (row.names.length > 0) {
20897
- for (const wrapped of wrapNames(row.names, indent, 76)) {
20898
- lines.push(`${indent}${ansi.dim(wrapped)}`);
20899
- }
20900
- }
20901
- }
20902
- return lines.join("\n") + "\n" + PLUGINS_TEXTS.listTipShow;
20903
- }
20904
- function builtInToListRow(b) {
20905
- const names = b.extensions.map((e) => {
20906
- const name = withStabilityTag(e.id, e.stability);
20907
- return e.enabled ? name : `${PLUGINS_TEXTS.rowGlyphOff} ${name}`;
20908
- });
20909
- return {
20910
- id: b.id,
20911
- enabled: b.enabled,
20912
- source: PLUGINS_TEXTS.sourceBuiltIn,
20913
- names
20914
- };
20915
- }
20916
- function pluginToListRow(p, resolveEnabled) {
20917
- const isLoaded = p.status === "enabled";
20918
- const extensions = p.extensions ?? [];
20919
- const enabled = isLoaded ? extensions.length === 0 || extensions.some((e) => resolveEnabled(qualifiedExtensionId(p.id, e.id))) : false;
20920
- const names = extensions.map((e) => {
20921
- const safeId = withStabilityTag(sanitizeForTerminal(e.id), e.stability);
20922
- return resolveEnabled(qualifiedExtensionId(p.id, e.id)) ? safeId : `${PLUGINS_TEXTS.rowGlyphOff} ${safeId}`;
20923
- });
20924
- const reason = p.status === "enabled" ? void 0 : sanitizeForTerminal(p.reason ?? "") || void 0;
20925
- return {
20926
- id: sanitizeForTerminal(p.id),
20927
- enabled,
20928
- source: PLUGINS_TEXTS.sourceUser,
20929
- names,
20930
- reason
20931
- };
20932
- }
20933
- function wrapNames(names, indent, maxWidth) {
20934
- const out = [];
20935
- const sep8 = ", ";
20936
- let current = "";
20937
- for (const name of names) {
20938
- const candidate = current === "" ? name : `${current}${sep8}${name}`;
20939
- if (indent.length + candidate.length > maxWidth && current !== "") {
20940
- out.push(`${current},`);
20941
- current = name;
20942
- } else {
20943
- current = candidate;
20944
- }
20945
- }
20946
- if (current !== "") out.push(current);
20947
- return out;
20948
- }
20949
-
20950
- // cli/commands/plugins/show.ts
20951
- import { Command as Command23, Option as Option22 } from "clipanion";
20952
- var PluginsShowCommand = class extends SmCommand {
20953
- static paths = [["plugins", "show"]];
20954
- static usage = Command23.Usage({
20955
- category: "Plugins",
20956
- description: "Show a single plugin's manifest + loaded extensions.",
20957
- details: `
20958
- Accepts a plugin id (\`core\`, \`claude\`, \`my-plugin\`)
20959
- or a qualified extension id (\`core/<ext-id>\`,
20960
- \`<plugin>/<ext-id>\`). When given a qualified id, validates the
20961
- extension exists and renders a single-extension detail block.
20962
- The bare form renders the parent plugin's detail with per-extension
20963
- status. The same id shapes \`sm plugins enable\` and
20964
- \`sm plugins disable\` accept resolve cleanly here too.
20965
- `
20966
- });
20967
- id = Option22.String({ required: true });
20968
- pluginDir = Option22.String("--plugin-dir", { required: false });
20969
- async run() {
20970
- const plugins = await loadAll({ pluginDir: this.pluginDir });
20971
- const resolveEnabled = await buildResolver();
20972
- const builtIns2 = builtInRows(resolveEnabled);
21076
+ /**
21077
+ * `sm plugins list <id>`, render one plugin's full detail. A qualified
21078
+ * `<plugin>/<ext>` id is the wrong granularity for `list` (it targets a
21079
+ * single extension), redirect to `sm plugins show`. A bare id that
21080
+ * matches no plugin is a NotFound.
21081
+ */
21082
+ renderPluginDetailById(id, builtIns2, plugins) {
20973
21083
  const stderrAnsi = this.ansiFor("stderr");
20974
- const lookupResult = resolveShowLookupId(this.id, builtIns2, plugins, stderrAnsi);
20975
- if ("error" in lookupResult) {
20976
- this.printer.error(lookupResult.error);
20977
- return ExitCode.NotFound;
21084
+ if (id.includes("/")) {
21085
+ const pluginId = id.split("/")[0] ?? id;
21086
+ this.printer.error(
21087
+ tx(PLUGINS_TEXTS.listQualifiedId, {
21088
+ glyph: stderrAnsi.red(PLUGINS_TEXTS.rowGlyphOff),
21089
+ id: sanitizeForTerminal(id),
21090
+ hint: stderrAnsi.dim(
21091
+ tx(PLUGINS_TEXTS.listQualifiedIdHint, {
21092
+ id: sanitizeForTerminal(id),
21093
+ pluginId: sanitizeForTerminal(pluginId)
21094
+ })
21095
+ )
21096
+ })
21097
+ );
21098
+ return ExitCode.Error;
20978
21099
  }
20979
- const { pluginId, extId } = lookupResult;
20980
- const builtIn = builtIns2.find((b) => b.id === pluginId);
20981
- const match = plugins.find((p) => p.id === pluginId);
21100
+ const builtIn = builtIns2.find((b) => b.id === id);
21101
+ const match = plugins.find((p) => p.id === id);
20982
21102
  if (!builtIn && !match) {
20983
21103
  this.printer.error(
20984
21104
  tx(PLUGINS_TEXTS.pluginNotFound, {
20985
- glyph: stderrAnsi.red("\u2715"),
20986
- id: sanitizeForTerminal(this.id),
21105
+ glyph: stderrAnsi.red(PLUGINS_TEXTS.rowGlyphOff),
21106
+ id: sanitizeForTerminal(id),
20987
21107
  hint: stderrAnsi.dim(PLUGINS_TEXTS.pluginNotFoundHint)
20988
21108
  })
20989
21109
  );
20990
21110
  return ExitCode.NotFound;
20991
21111
  }
20992
- if (extId !== void 0) {
20993
- return this.renderExtensionDetail({ extId, pluginId, builtIn, match });
20994
- }
20995
21112
  if (this.json) {
20996
21113
  const payload = builtIn ?? match;
20997
21114
  this.printer.data(JSON.stringify(payload, omitModule, 2) + "\n");
@@ -21002,83 +21119,54 @@ var PluginsShowCommand = class extends SmCommand {
21002
21119
  this.printer.data(text);
21003
21120
  return ExitCode.Ok;
21004
21121
  }
21005
- /**
21006
- * Render the single-extension detail block, the path taken when the
21007
- * user supplies a qualified `<plugin>/<ext>` id. `--json` emits the
21008
- * single extension row (no surrounding plugin envelope) so tooling
21009
- * can pipe straight into `jq`; human mode renders a focused header
21010
- * plus a Kind / Version / Stability / Description / Preconditions /
21011
- * Entry field block.
21012
- */
21013
- renderExtensionDetail(args2) {
21014
- const { extId, pluginId, builtIn, match } = args2;
21015
- const ansi = this.ansiFor("stdout");
21016
- if (builtIn) {
21017
- const ext = builtIn.extensions.find((e) => e.id === extId);
21018
- if (!ext) return ExitCode.NotFound;
21019
- if (this.json) {
21020
- this.printer.data(JSON.stringify({ pluginId, ...ext }, omitModule, 2) + "\n");
21021
- return ExitCode.Ok;
21022
- }
21023
- this.printer.data(renderBuiltInExtensionDetail(pluginId, ext, ansi));
21024
- return ExitCode.Ok;
21025
- }
21026
- const userExt = match?.extensions?.find((e) => e.id === extId);
21027
- if (!userExt) return ExitCode.NotFound;
21028
- if (this.json) {
21029
- this.printer.data(JSON.stringify(userExt, omitModule, 2) + "\n");
21030
- return ExitCode.Ok;
21122
+ };
21123
+ function renderIndexHuman(builtIns2, plugins, resolveEnabled, ansi) {
21124
+ const rows = [
21125
+ ...builtIns2.map(builtInToIndexRow),
21126
+ ...plugins.map((p) => pluginToIndexRow(p, resolveEnabled))
21127
+ ];
21128
+ const idWidth = Math.max(...rows.map((r) => r.id.length));
21129
+ const countWidth = Math.max(...rows.map((r) => String(r.extCount).length));
21130
+ const lines = [];
21131
+ for (const row of rows) {
21132
+ const glyph = row.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
21133
+ const idCol = row.id.padEnd(idWidth);
21134
+ const countCol = String(row.extCount).padStart(countWidth);
21135
+ lines.push(
21136
+ tx(PLUGINS_TEXTS.pluginRow, {
21137
+ glyph,
21138
+ id: idCol,
21139
+ count: ` ${countCol}`,
21140
+ source: ansi.dim(row.source)
21141
+ })
21142
+ );
21143
+ if (row.reason) {
21144
+ lines.push(`${PLUGINS_TEXTS.pluginSubIndent}${ansi.dim(row.reason)}`);
21031
21145
  }
21032
- this.printer.data(renderUserExtensionDetail(pluginId, userExt, ansi));
21033
- return ExitCode.Ok;
21034
21146
  }
21035
- };
21036
- function resolveShowLookupId(id, builtIns2, plugins, ansi) {
21037
- if (!id.includes("/")) return { pluginId: id };
21038
- const parsed = parseQualifiedId(id);
21039
- if ("error" in parsed) return { error: malformedQualifiedError(id, ansi) };
21040
- const { pluginId, extId } = parsed;
21041
- const knownExts = collectKnownExtensions(pluginId, builtIns2, plugins);
21042
- if (knownExts === null) return { error: unknownPluginError(pluginId, ansi) };
21043
- if (!knownExts.includes(extId)) {
21044
- return { error: unknownExtensionError(id, pluginId, extId, ansi) };
21045
- }
21046
- return { pluginId, extId };
21047
- }
21048
- function parseQualifiedId(id) {
21049
- const [pluginId, extId, ...rest] = id.split("/");
21050
- if (!pluginId || !extId || rest.length > 0) return { error: true };
21051
- return { pluginId, extId };
21052
- }
21053
- function collectKnownExtensions(pluginId, builtIns2, plugins) {
21054
- const builtIn = builtIns2.find((b) => b.id === pluginId);
21055
- if (builtIn) return builtIn.extensions.map((e) => e.id);
21056
- const userPlugin = plugins.find((p) => p.id === pluginId);
21057
- if (userPlugin) return userPlugin.extensions?.map((e) => e.id) ?? [];
21058
- return null;
21059
- }
21060
- function malformedQualifiedError(id, ansi) {
21061
- return tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
21062
- glyph: ansi.red("\u2715"),
21063
- pluginId: sanitizeForTerminal(id),
21064
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
21065
- });
21147
+ return lines.join("\n") + "\n" + PLUGINS_TEXTS.listTipShow;
21066
21148
  }
21067
- function unknownPluginError(pluginId, ansi) {
21068
- return tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
21069
- glyph: ansi.red("\u2715"),
21070
- pluginId: sanitizeForTerminal(pluginId),
21071
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
21072
- });
21149
+ function builtInToIndexRow(b) {
21150
+ return {
21151
+ id: b.id,
21152
+ enabled: b.enabled,
21153
+ source: PLUGINS_TEXTS.sourceBuiltIn,
21154
+ extCount: b.extensions.length
21155
+ };
21073
21156
  }
21074
- function unknownExtensionError(id, pluginId, extId, ansi) {
21075
- return tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
21076
- glyph: ansi.red("\u2715"),
21077
- id: sanitizeForTerminal(id),
21078
- pluginId: sanitizeForTerminal(pluginId),
21079
- extId: sanitizeForTerminal(extId),
21080
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdNotFoundHint)
21081
- });
21157
+ function pluginToIndexRow(p, resolveEnabled) {
21158
+ const isLoaded = p.status === "enabled";
21159
+ const extensions = p.extensions ?? [];
21160
+ const extEnabled = (e) => resolveEnabled(qualifiedExtensionId(p.id, e.id), installedDefaultEnabled(e.stability));
21161
+ const enabled = isLoaded ? extensions.length === 0 || extensions.some((e) => extEnabled(e)) : false;
21162
+ const reason = p.status === "enabled" ? void 0 : sanitizeForTerminal(p.reason ?? "") || void 0;
21163
+ return {
21164
+ id: sanitizeForTerminal(p.id),
21165
+ enabled,
21166
+ source: PLUGINS_TEXTS.sourceUser,
21167
+ extCount: extensions.length,
21168
+ reason
21169
+ };
21082
21170
  }
21083
21171
  function kindIndex(kind) {
21084
21172
  const idx = EXTENSION_KINDS.indexOf(kind);
@@ -21203,6 +21291,83 @@ function renderExtensionItems(items) {
21203
21291
  }
21204
21292
  return out.join("");
21205
21293
  }
21294
+
21295
+ // cli/commands/plugins/show.ts
21296
+ import { Command as Command23, Option as Option22 } from "clipanion";
21297
+ var PluginsShowCommand = class extends SmCommand {
21298
+ static paths = [["plugins", "show"]];
21299
+ static usage = Command23.Usage({
21300
+ category: "Plugins",
21301
+ description: "Show a single extension's detail.",
21302
+ details: `
21303
+ Accepts a qualified extension id (\`core/<ext-id>\`,
21304
+ \`<plugin>/<ext-id>\`) and renders a single-extension detail block
21305
+ (Kind / Version / Stability / Description / Preconditions / Entry).
21306
+ A bare plugin id is rejected with a redirect to
21307
+ \`sm plugins list <id>\`, which renders the whole plugin. The same
21308
+ qualified id shape \`sm plugins enable\` and \`sm plugins disable\`
21309
+ accept resolves cleanly here too.
21310
+ `
21311
+ });
21312
+ id = Option22.String({ required: true });
21313
+ pluginDir = Option22.String("--plugin-dir", { required: false });
21314
+ async run() {
21315
+ const plugins = await loadAll({ pluginDir: this.pluginDir });
21316
+ const resolveEnabled = await buildResolver();
21317
+ const builtIns2 = builtInRows(resolveEnabled);
21318
+ const stderrAnsi = this.ansiFor("stderr");
21319
+ if (!this.id.includes("/")) {
21320
+ this.printer.error(
21321
+ tx(PLUGINS_TEXTS.showBareId, {
21322
+ glyph: stderrAnsi.red(PLUGINS_TEXTS.rowGlyphOff),
21323
+ id: sanitizeForTerminal(this.id),
21324
+ hint: stderrAnsi.dim(
21325
+ tx(PLUGINS_TEXTS.showBareIdHint, { id: sanitizeForTerminal(this.id) })
21326
+ )
21327
+ })
21328
+ );
21329
+ return ExitCode.Error;
21330
+ }
21331
+ const parsed = parseQualifiedExtensionId(this.id, pluginCatalogue(plugins));
21332
+ if (!parsed.ok) {
21333
+ this.printer.error(renderQualifiedIdError(parsed, this.id, stderrAnsi));
21334
+ return ExitCode.NotFound;
21335
+ }
21336
+ const { pluginId, extId } = parsed;
21337
+ const builtIn = builtIns2.find((b) => b.id === pluginId);
21338
+ const match = plugins.find((p) => p.id === pluginId);
21339
+ return this.renderExtensionDetail({ extId, pluginId, builtIn, match });
21340
+ }
21341
+ /**
21342
+ * Render the single-extension detail block. `--json` emits the single
21343
+ * extension row (no surrounding plugin envelope) so tooling can pipe
21344
+ * straight into `jq`; human mode renders a focused header plus a
21345
+ * Kind / Version / Stability / Description / Preconditions / Entry
21346
+ * field block.
21347
+ */
21348
+ renderExtensionDetail(args2) {
21349
+ const { extId, pluginId, builtIn, match } = args2;
21350
+ const ansi = this.ansiFor("stdout");
21351
+ if (builtIn) {
21352
+ const ext = builtIn.extensions.find((e) => e.id === extId);
21353
+ if (!ext) return ExitCode.NotFound;
21354
+ if (this.json) {
21355
+ this.printer.data(JSON.stringify({ pluginId, ...ext }, omitModule, 2) + "\n");
21356
+ return ExitCode.Ok;
21357
+ }
21358
+ this.printer.data(renderBuiltInExtensionDetail(pluginId, ext, ansi));
21359
+ return ExitCode.Ok;
21360
+ }
21361
+ const userExt = match?.extensions?.find((e) => e.id === extId);
21362
+ if (!userExt) return ExitCode.NotFound;
21363
+ if (this.json) {
21364
+ this.printer.data(JSON.stringify(userExt, omitModule, 2) + "\n");
21365
+ return ExitCode.Ok;
21366
+ }
21367
+ this.printer.data(renderUserExtensionDetail(pluginId, userExt, ansi));
21368
+ return ExitCode.Ok;
21369
+ }
21370
+ };
21206
21371
  function renderBuiltInExtensionDetail(pluginId, ext, ansi) {
21207
21372
  const glyph = ext.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
21208
21373
  const header = tx(PLUGINS_TEXTS.detailHeaderExtensionBuiltIn, {
@@ -22039,61 +22204,15 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
22039
22204
  return this.toggle(false);
22040
22205
  }
22041
22206
  };
22042
- function pluginCatalogue(plugins) {
22043
- const out = [];
22044
- for (const plugin of builtInPlugins) {
22045
- out.push({
22046
- id: plugin.id,
22047
- extensionIds: plugin.extensions.map((e) => e.id)
22048
- });
22049
- }
22050
- for (const p of plugins) {
22051
- out.push({
22052
- id: p.id,
22053
- extensionIds: p.extensions?.map((e) => e.id) ?? []
22054
- });
22055
- }
22056
- return out;
22057
- }
22058
22207
  function resolveToggleTarget(id, catalogue, ansi) {
22059
22208
  return id.includes("/") ? resolveQualifiedToggle(id, catalogue, ansi) : resolveBareToggle(id, catalogue);
22060
22209
  }
22061
22210
  function resolveQualifiedToggle(id, catalogue, ansi) {
22062
- const errGlyph = ansi.red("\u2715");
22063
- const [pluginId, extId, ...rest] = id.split("/");
22064
- if (!pluginId || !extId || rest.length > 0) {
22065
- return {
22066
- error: tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
22067
- glyph: errGlyph,
22068
- pluginId: sanitizeForTerminal(id),
22069
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
22070
- })
22071
- };
22072
- }
22073
- const plugin = catalogue.find((b) => b.id === pluginId);
22074
- if (!plugin) {
22075
- return {
22076
- error: tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
22077
- glyph: errGlyph,
22078
- pluginId: sanitizeForTerminal(pluginId),
22079
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
22080
- })
22081
- };
22082
- }
22083
- if (!plugin.extensionIds.includes(extId)) {
22084
- return {
22085
- error: tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
22086
- glyph: errGlyph,
22087
- id: sanitizeForTerminal(id),
22088
- pluginId: sanitizeForTerminal(pluginId),
22089
- extId: sanitizeForTerminal(extId),
22090
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdNotFoundHint)
22091
- })
22092
- };
22093
- }
22211
+ const parsed = parseQualifiedExtensionId(id, catalogue);
22212
+ if (!parsed.ok) return { error: renderQualifiedIdError(parsed, id, ansi) };
22094
22213
  return {
22095
22214
  origin: "qualified",
22096
- keys: [qualifiedExtensionId(pluginId, extId)]
22215
+ keys: [qualifiedExtensionId(parsed.pluginId, parsed.extId)]
22097
22216
  };
22098
22217
  }
22099
22218
  function resolveBareToggle(id, catalogue) {
@@ -22473,7 +22592,7 @@ Generated by \`sm plugins create ${kind} ${pluginId}\`. Edit \`${mainFileRel}\`
22473
22592
 
22474
22593
  ## Verbs
22475
22594
 
22476
- - \`sm plugins show ${pluginId}\`: manifest + load status
22595
+ - \`sm plugins list ${pluginId}\`: manifest + extensions + load status
22477
22596
  - \`sm plugins doctor\`: full plugin diagnostic
22478
22597
  - \`sm scan\`: re-emit contributions / re-run analysis
22479
22598
 
@@ -22620,11 +22739,12 @@ var INPUT_TYPES_CATALOG = [
22620
22739
  { id: "single-string", summary: "Single text input." },
22621
22740
  { id: "boolean-flag", summary: "On/off toggle." },
22622
22741
  { id: "integer", summary: "Integer with optional bounds." },
22742
+ { id: "number", summary: "Decimal number with optional bounds." },
22623
22743
  { id: "enum-pick", summary: "Pick one from a closed set." },
22624
22744
  { id: "enum-multipick", summary: "Pick zero or more from a closed set." },
22625
22745
  { id: "path-glob", summary: "Glob pattern (single or multiple)." },
22626
22746
  { id: "regex", summary: "ECMAScript regex pattern body." },
22627
- { id: "secret", summary: "Sensitive string (encrypted at rest)." },
22747
+ { id: "secret", summary: "Sensitive string, forced into project-local storage (gitignored), not encrypted." },
22628
22748
  { id: "key-value-list", summary: "Editable mapping of strings to strings." }
22629
22749
  ];
22630
22750
 
@@ -22645,57 +22765,426 @@ var PluginsSlotsListCommand = class extends SmCommand {
22645
22765
  2
22646
22766
  ) + "\n"
22647
22767
  );
22648
- return ExitCode.Ok;
22768
+ return ExitCode.Ok;
22769
+ }
22770
+ const ansi = this.ansiFor("stdout");
22771
+ const idWidth = Math.max(
22772
+ ...VIEW_SLOTS_CATALOG.map((c) => c.id.length),
22773
+ ...INPUT_TYPES_CATALOG.map((t) => t.id.length)
22774
+ );
22775
+ this.printer.data(
22776
+ tx(PLUGINS_TEXTS.slotsListHeaderViewSlots, { count: VIEW_SLOTS_CATALOG.length })
22777
+ );
22778
+ for (const c of VIEW_SLOTS_CATALOG) {
22779
+ this.printer.data(
22780
+ ` ${c.id.padEnd(idWidth)} ${ansi.dim(c.summary)}
22781
+ `
22782
+ );
22783
+ }
22784
+ this.printer.data(
22785
+ tx(PLUGINS_TEXTS.slotsListHeaderInputTypes, { count: INPUT_TYPES_CATALOG.length })
22786
+ );
22787
+ for (const t of INPUT_TYPES_CATALOG) {
22788
+ this.printer.data(
22789
+ ` ${t.id.padEnd(idWidth)} ${ansi.dim(t.summary)}
22790
+ `
22791
+ );
22792
+ }
22793
+ this.printer.data(
22794
+ tx(PLUGINS_TEXTS.slotsListTipFooter, {
22795
+ tip: ansi.dim(PLUGINS_TEXTS.slotsListTipText)
22796
+ })
22797
+ );
22798
+ return ExitCode.Ok;
22799
+ }
22800
+ };
22801
+
22802
+ // cli/commands/plugins/upgrade.ts
22803
+ import { Command as Command28, Option as Option26 } from "clipanion";
22804
+ var PluginsUpgradeCommand = class extends SmCommand {
22805
+ static paths = [["plugins", "upgrade"]];
22806
+ static usage = Command28.Usage({
22807
+ category: "Plugins",
22808
+ description: "Apply catalog migrations to plugin manifests.",
22809
+ details: "No migrations registered against catalog v1.0.0 yet; this verb is a no-op today. The structure exists so future slot renames / deprecations land without spec churn."
22810
+ });
22811
+ pluginId = Option26.String({ required: false, name: "plugin-id" });
22812
+ async run() {
22813
+ this.printer.data(
22814
+ "sm plugins upgrade: no migrations registered for catalog v1.0.0.\n All loaded plugins are catalog-current.\n Run `sm plugins doctor` to surface any incompatible-catalog status.\n"
22815
+ );
22816
+ return ExitCode.Ok;
22817
+ }
22818
+ };
22819
+
22820
+ // cli/commands/plugins/config.ts
22821
+ import { Command as Command29, Option as Option27 } from "clipanion";
22822
+
22823
+ // cli/i18n/plugins-config.texts.ts
22824
+ var PLUGINS_CONFIG_TEXTS = {
22825
+ // --- id-shape redirects ----------------------------------------------
22826
+ // `sm plugins config` operates on one extension. A bare plugin id is
22827
+ // the wrong granularity, redirect to `sm plugins list <id>`.
22828
+ bareId: '{{glyph}} `sm plugins config` needs a qualified `<plugin>/<ext>` id; "{{id}}" is a plugin.\n {{hint}}\n',
22829
+ bareIdHint: "Run `sm plugins list {{id}}` to see the extensions, then `sm plugins config {{id}}/<ext>`.",
22830
+ // --- no declared settings --------------------------------------------
22831
+ noSettings: '{{glyph}} Extension "{{id}}" declares no configurable settings.\n {{hint}}\n',
22832
+ noSettingsHint: "Run `sm plugins show {{id}}` to inspect the extension.",
22833
+ unknownSetting: '{{glyph}} Unknown setting "{{settingId}}" for extension "{{id}}".\n {{hint}}\n',
22834
+ unknownSettingHint: "Declared settings: {{declared}}.",
22835
+ // --- coercion / validation -------------------------------------------
22836
+ coerceFailed: '{{glyph}} Could not parse "{{value}}" as type {{type}} for setting "{{settingId}}".\n {{hint}}\n',
22837
+ coerceFailedHint: "{{detail}}",
22838
+ validationFailed: '{{glyph}} Invalid value for setting "{{settingId}}" ({{type}}): {{reason}}.\n',
22839
+ writeFailed: '{{glyph}} Failed to write setting "{{settingId}}": {{message}}\n',
22840
+ // --- table view (no settingId) ---------------------------------------
22841
+ /** Section header above the settings table. */
22842
+ tableHeader: " Settings for {{id}}\n",
22843
+ /** One table row: setting id, effective value, source layer tag. */
22844
+ tableRow: " {{settingId}} {{value}}{{sourceTag}}\n",
22845
+ /** Dim suffix showing which layer set the effective value. */
22846
+ tableSourceTag: " [{{source}}]",
22847
+ /** Redaction placeholder for `secret`-typed values in any output. */
22848
+ redacted: "<redacted>",
22849
+ // --- write / reset receipts ------------------------------------------
22850
+ setWritten: "{{glyph}} Set {{settingId}} = {{value}} for {{id}}{{wroteTag}}\n",
22851
+ setWroteTag: " (wrote {{path}})",
22852
+ resetRemoved: "{{glyph}} Cleared {{settingId}} for {{id}}; falls back to the declared default{{wroteTag}}\n",
22853
+ resetNoOverride: "{{glyph}} No override set for {{settingId}} on {{id}}; nothing to clear.\n",
22854
+ // --- re-scan footer ---------------------------------------------------
22855
+ rescanFooter: "{{hint}}\n",
22856
+ rescanFooterText: "Settings are read once per scan; run `sm scan` to apply."
22857
+ };
22858
+
22859
+ // cli/commands/plugins/config.ts
22860
+ var PluginsConfigCommand = class extends SmCommand {
22861
+ static paths = [["plugins", "config"]];
22862
+ static usage = Command29.Usage({
22863
+ category: "Plugins",
22864
+ description: "Read or write an extension's declared settings.",
22865
+ details: `
22866
+ Operates on a single extension by its qualified \`<plugin>/<ext>\`
22867
+ id. With no settingId it prints a table of each declared setting,
22868
+ its effective value, and the config layer that set it. With a
22869
+ settingId + value it coerces the shell string to the declared
22870
+ input-type, validates it, and writes
22871
+ \`plugins.<plugin>.extensions.<ext>.settings.<settingId>\` to
22872
+ settings.json (or settings.local.json for \`secret\` settings).
22873
+ \`--reset\` removes the override so the manifest default applies.
22874
+ Secret values are shown as <redacted>. Run \`sm scan\` to apply.
22875
+ `
22876
+ });
22877
+ id = Option27.String({ required: true });
22878
+ settingId = Option27.String({ required: false });
22879
+ value = Option27.String({ required: false });
22880
+ reset = Option27.Boolean("--reset", false, {
22881
+ description: "Remove the override for <settingId> so the manifest default applies."
22882
+ });
22883
+ pluginDir = Option27.String("--plugin-dir", { required: false });
22884
+ // Read-only when listing; the write / reset paths emit their own
22885
+ // receipt. `sm config` exempts the config family from "done in <…>";
22886
+ // mirror that here for the read path. The write path keeps the line.
22887
+ emitElapsed = true;
22888
+ // CLI orchestrator: each branch is one validation gate (bare id /
22889
+ // unknown extension / no settings / unknown setting) or a mode
22890
+ // dispatch (table vs set vs reset). Splitting per branch scatters the
22891
+ // gate from the value it gates.
22892
+ // eslint-disable-next-line complexity
22893
+ async run() {
22894
+ const ctx = defaultRuntimeContext();
22895
+ const stderrAnsi = this.ansiFor("stderr");
22896
+ if (!this.id.includes("/")) {
22897
+ this.printer.error(
22898
+ tx(PLUGINS_CONFIG_TEXTS.bareId, {
22899
+ glyph: stderrAnsi.red("\u2715"),
22900
+ id: sanitizeForTerminal(this.id),
22901
+ hint: stderrAnsi.dim(
22902
+ tx(PLUGINS_CONFIG_TEXTS.bareIdHint, { id: sanitizeForTerminal(this.id) })
22903
+ )
22904
+ })
22905
+ );
22906
+ return ExitCode.Error;
22907
+ }
22908
+ const plugins = await loadAll({ pluginDir: this.pluginDir });
22909
+ const parsed = parseQualifiedExtensionId(this.id, pluginCatalogue(plugins));
22910
+ if (!parsed.ok) {
22911
+ this.printer.error(renderQualifiedIdError(parsed, this.id, stderrAnsi));
22912
+ return ExitCode.NotFound;
22913
+ }
22914
+ const { pluginId, extId } = parsed;
22915
+ const declarations = resolveDeclaredSettings(pluginId, extId, plugins);
22916
+ if (!declarations || Object.keys(declarations).length === 0) {
22917
+ this.printer.error(
22918
+ tx(PLUGINS_CONFIG_TEXTS.noSettings, {
22919
+ glyph: stderrAnsi.red("\u2715"),
22920
+ id: sanitizeForTerminal(this.id),
22921
+ hint: stderrAnsi.dim(
22922
+ tx(PLUGINS_CONFIG_TEXTS.noSettingsHint, { id: sanitizeForTerminal(this.id) })
22923
+ )
22924
+ })
22925
+ );
22926
+ return ExitCode.NotFound;
22927
+ }
22928
+ if (this.settingId === void 0) {
22929
+ return this.renderTable(pluginId, extId, declarations, ctx.cwd);
22930
+ }
22931
+ const declaration = declarations[this.settingId];
22932
+ if (!declaration) {
22933
+ this.printer.error(
22934
+ tx(PLUGINS_CONFIG_TEXTS.unknownSetting, {
22935
+ glyph: stderrAnsi.red("\u2715"),
22936
+ settingId: sanitizeForTerminal(this.settingId),
22937
+ id: sanitizeForTerminal(this.id),
22938
+ hint: stderrAnsi.dim(
22939
+ tx(PLUGINS_CONFIG_TEXTS.unknownSettingHint, {
22940
+ declared: Object.keys(declarations).map((k) => `'${k}'`).join(", ")
22941
+ })
22942
+ )
22943
+ })
22944
+ );
22945
+ return ExitCode.NotFound;
22946
+ }
22947
+ if (this.reset) {
22948
+ return this.resetSetting(pluginId, extId, this.settingId, declaration, ctx.cwd);
22949
+ }
22950
+ if (this.value === void 0) {
22951
+ return this.renderTable(pluginId, extId, { [this.settingId]: declaration }, ctx.cwd);
22952
+ }
22953
+ return this.writeSetting(pluginId, extId, this.settingId, declaration, this.value, ctx.cwd);
22954
+ }
22955
+ /**
22956
+ * Render the settings table (human) or the resolved set (`--json`).
22957
+ * Effective values come from the kernel settings resolver so the table
22958
+ * shows exactly what `ctx.settings.<id>` would see at scan time
22959
+ * (default overlaid by the config override, validated). Secret values
22960
+ * are redacted.
22961
+ */
22962
+ renderTable(pluginId, extId, declarations, cwd) {
22963
+ const { effective } = loadConfig({ cwd });
22964
+ const resolved = resolveExtensionSettings({ pluginId, id: extId, settings: declarations }, effective, () => {
22965
+ });
22966
+ if (this.json) {
22967
+ const payload = {};
22968
+ for (const [settingId, declaration] of Object.entries(declarations)) {
22969
+ payload[settingId] = declaration.type === "secret" ? PLUGINS_CONFIG_TEXTS.redacted : resolved[settingId] ?? null;
22970
+ }
22971
+ this.printer.data(JSON.stringify(payload) + "\n");
22972
+ return ExitCode.Ok;
22973
+ }
22974
+ const ansi = this.ansiFor("stdout");
22975
+ const lines = [tx(PLUGINS_CONFIG_TEXTS.tableHeader, { id: `${pluginId}/${extId}` })];
22976
+ const idWidth = Math.max(...Object.keys(declarations).map((k) => k.length));
22977
+ for (const [settingId, declaration] of Object.entries(declarations)) {
22978
+ const display = declaration.type === "secret" ? PLUGINS_CONFIG_TEXTS.redacted : formatValue2(resolved[settingId]);
22979
+ const dotKey = settingDotKey(pluginId, extId, settingId);
22980
+ const source = getValueSource(dotKey, { cwd });
22981
+ const sourceTag = source ? ansi.dim(tx(PLUGINS_CONFIG_TEXTS.tableSourceTag, { source: layerLabel(source) })) : "";
22982
+ lines.push(
22983
+ tx(PLUGINS_CONFIG_TEXTS.tableRow, {
22984
+ settingId: settingId.padEnd(idWidth),
22985
+ value: sanitizeForTerminal(display),
22986
+ sourceTag
22987
+ })
22988
+ );
22989
+ }
22990
+ this.printer.data(lines.join(""));
22991
+ return ExitCode.Ok;
22992
+ }
22993
+ writeSetting(pluginId, extId, settingId, declaration, raw, cwd) {
22994
+ const stderrAnsi = this.ansiFor("stderr");
22995
+ const errGlyph = stderrAnsi.red("\u2715");
22996
+ const coerced = coerceCliValue(declaration, raw);
22997
+ if (!coerced.ok) {
22998
+ this.printer.error(
22999
+ tx(PLUGINS_CONFIG_TEXTS.coerceFailed, {
23000
+ glyph: errGlyph,
23001
+ value: sanitizeForTerminal(raw),
23002
+ type: declaration.type,
23003
+ settingId: sanitizeForTerminal(settingId),
23004
+ hint: stderrAnsi.dim(tx(PLUGINS_CONFIG_TEXTS.coerceFailedHint, { detail: coerced.reason }))
23005
+ })
23006
+ );
23007
+ return ExitCode.Error;
23008
+ }
23009
+ let invalidReason = null;
23010
+ resolveExtensionSettings(
23011
+ { pluginId, id: extId, settings: { [settingId]: declaration } },
23012
+ { plugins: { [pluginId]: { extensions: { [extId]: { settings: { [settingId]: coerced.value } } } } } },
23013
+ (message) => {
23014
+ invalidReason = message;
23015
+ }
23016
+ );
23017
+ if (invalidReason !== null) {
23018
+ this.printer.error(
23019
+ tx(PLUGINS_CONFIG_TEXTS.validationFailed, {
23020
+ glyph: errGlyph,
23021
+ settingId: sanitizeForTerminal(settingId),
23022
+ type: declaration.type,
23023
+ reason: invalidReason
23024
+ })
23025
+ );
23026
+ return ExitCode.Error;
23027
+ }
23028
+ const target = declaration.type === "secret" ? "project-local" : "project";
23029
+ const dotKey = settingDotKey(pluginId, extId, settingId);
23030
+ try {
23031
+ writeConfigValue(dotKey, coerced.value, { target, cwd });
23032
+ } catch (err) {
23033
+ return this.renderWriteError(err, settingId);
22649
23034
  }
22650
23035
  const ansi = this.ansiFor("stdout");
22651
- const idWidth = Math.max(
22652
- ...VIEW_SLOTS_CATALOG.map((c) => c.id.length),
22653
- ...INPUT_TYPES_CATALOG.map((t) => t.id.length)
22654
- );
23036
+ const display = declaration.type === "secret" ? PLUGINS_CONFIG_TEXTS.redacted : formatValue2(coerced.value);
23037
+ const path = target === "project-local" ? defaultLocalSettingsPath(cwd) : defaultSettingsPath(cwd);
22655
23038
  this.printer.data(
22656
- tx(PLUGINS_TEXTS.slotsListHeaderViewSlots, { count: VIEW_SLOTS_CATALOG.length })
23039
+ tx(PLUGINS_CONFIG_TEXTS.setWritten, {
23040
+ glyph: ansi.green("\u2713"),
23041
+ settingId,
23042
+ value: sanitizeForTerminal(display),
23043
+ id: `${pluginId}/${extId}`,
23044
+ wroteTag: ansi.dim(tx(PLUGINS_CONFIG_TEXTS.setWroteTag, { path: relativeIfBelow(path, cwd) }))
23045
+ })
22657
23046
  );
22658
- for (const c of VIEW_SLOTS_CATALOG) {
23047
+ this.printRescanFooter();
23048
+ return ExitCode.Ok;
23049
+ }
23050
+ resetSetting(pluginId, extId, settingId, declaration, cwd) {
23051
+ const target = declaration.type === "secret" ? "project-local" : "project";
23052
+ const dotKey = settingDotKey(pluginId, extId, settingId);
23053
+ const ansi = this.ansiFor("stdout");
23054
+ let removed;
23055
+ try {
23056
+ removed = removeConfigValue(dotKey, { target, cwd });
23057
+ } catch (err) {
23058
+ return this.renderWriteError(err, settingId);
23059
+ }
23060
+ if (!removed) {
22659
23061
  this.printer.data(
22660
- ` ${c.id.padEnd(idWidth)} ${ansi.dim(c.summary)}
22661
- `
23062
+ tx(PLUGINS_CONFIG_TEXTS.resetNoOverride, {
23063
+ glyph: ansi.green("\u2713"),
23064
+ settingId,
23065
+ id: `${pluginId}/${extId}`
23066
+ })
22662
23067
  );
23068
+ return ExitCode.Ok;
22663
23069
  }
23070
+ const path = target === "project-local" ? defaultLocalSettingsPath(cwd) : defaultSettingsPath(cwd);
22664
23071
  this.printer.data(
22665
- tx(PLUGINS_TEXTS.slotsListHeaderInputTypes, { count: INPUT_TYPES_CATALOG.length })
23072
+ tx(PLUGINS_CONFIG_TEXTS.resetRemoved, {
23073
+ glyph: ansi.green("\u2713"),
23074
+ settingId,
23075
+ id: `${pluginId}/${extId}`,
23076
+ wroteTag: ansi.dim(tx(PLUGINS_CONFIG_TEXTS.setWroteTag, { path: relativeIfBelow(path, cwd) }))
23077
+ })
22666
23078
  );
22667
- for (const t of INPUT_TYPES_CATALOG) {
22668
- this.printer.data(
22669
- ` ${t.id.padEnd(idWidth)} ${ansi.dim(t.summary)}
22670
- `
23079
+ this.printRescanFooter();
23080
+ return ExitCode.Ok;
23081
+ }
23082
+ renderWriteError(err, settingId) {
23083
+ const ansi = this.ansiFor("stderr");
23084
+ const glyph = ansi.red("\u2715");
23085
+ if (err instanceof ForbiddenSegmentError) {
23086
+ this.printer.error(
23087
+ tx(PLUGINS_CONFIG_TEXTS.writeFailed, { glyph, settingId, message: err.message })
23088
+ );
23089
+ return ExitCode.Error;
23090
+ }
23091
+ if (err instanceof ProjectLocalOnlyKeyError) {
23092
+ this.printer.error(
23093
+ tx(PLUGINS_CONFIG_TEXTS.writeFailed, { glyph, settingId, message: err.message })
23094
+ );
23095
+ return ExitCode.Error;
23096
+ }
23097
+ if (err instanceof ConfigValidationError) {
23098
+ this.printer.error(
23099
+ tx(PLUGINS_CONFIG_TEXTS.writeFailed, { glyph, settingId, message: err.errors })
22671
23100
  );
23101
+ return ExitCode.Error;
22672
23102
  }
23103
+ throw err;
23104
+ }
23105
+ printRescanFooter() {
23106
+ if (this.json) return;
23107
+ const ansi = this.ansiFor("stdout");
22673
23108
  this.printer.data(
22674
- tx(PLUGINS_TEXTS.slotsListTipFooter, {
22675
- tip: ansi.dim(PLUGINS_TEXTS.slotsListTipText)
23109
+ tx(PLUGINS_CONFIG_TEXTS.rescanFooter, {
23110
+ hint: ansi.dim(PLUGINS_CONFIG_TEXTS.rescanFooterText)
22676
23111
  })
22677
23112
  );
22678
- return ExitCode.Ok;
22679
23113
  }
22680
23114
  };
22681
-
22682
- // cli/commands/plugins/upgrade.ts
22683
- import { Command as Command28, Option as Option26 } from "clipanion";
22684
- var PluginsUpgradeCommand = class extends SmCommand {
22685
- static paths = [["plugins", "upgrade"]];
22686
- static usage = Command28.Usage({
22687
- category: "Plugins",
22688
- description: "Apply catalog migrations to plugin manifests.",
22689
- details: "No migrations registered against catalog v1.0.0 yet; this verb is a no-op today. The structure exists so future slot renames / deprecations land without spec churn."
22690
- });
22691
- pluginId = Option26.String({ required: false, name: "plugin-id" });
22692
- async run() {
22693
- this.printer.data(
22694
- "sm plugins upgrade: no migrations registered for catalog v1.0.0.\n All loaded plugins are catalog-current.\n Run `sm plugins doctor` to surface any incompatible-catalog status.\n"
22695
- );
22696
- return ExitCode.Ok;
23115
+ function resolveDeclaredSettings(pluginId, extId, plugins) {
23116
+ for (const plugin of builtInPlugins) {
23117
+ if (plugin.id !== pluginId) continue;
23118
+ for (const ext of plugin.extensions) {
23119
+ if (ext.id === extId) return ext.settings;
23120
+ }
23121
+ }
23122
+ const match = plugins.find((p) => p.id === pluginId);
23123
+ const userExt = match?.extensions?.find((e) => e.id === extId);
23124
+ return readInstanceSettings(userExt?.instance);
23125
+ }
23126
+ function readInstanceSettings(instance) {
23127
+ if (instance === null || typeof instance !== "object") return void 0;
23128
+ const settings2 = instance.settings;
23129
+ if (settings2 && typeof settings2 === "object" && !Array.isArray(settings2)) {
23130
+ return settings2;
23131
+ }
23132
+ return void 0;
23133
+ }
23134
+ function coerceCliValue(declaration, raw) {
23135
+ switch (declaration.type) {
23136
+ case "integer":
23137
+ case "number": {
23138
+ const n = Number(raw);
23139
+ if (raw.trim() === "" || Number.isNaN(n)) return { ok: false, reason: "expected a numeric value" };
23140
+ return { ok: true, value: n };
23141
+ }
23142
+ case "boolean-flag": {
23143
+ if (raw === "true") return { ok: true, value: true };
23144
+ if (raw === "false") return { ok: true, value: false };
23145
+ return { ok: false, reason: "expected `true` or `false`" };
23146
+ }
23147
+ case "string-list":
23148
+ case "enum-multipick":
23149
+ case "key-value-list":
23150
+ return parseJsonValue(raw);
23151
+ case "path-glob":
23152
+ return declaration.multiple === true ? parseJsonValue(raw) : { ok: true, value: raw };
23153
+ case "single-string":
23154
+ case "enum-pick":
23155
+ case "regex":
23156
+ case "secret":
23157
+ return { ok: true, value: raw };
23158
+ default: {
23159
+ const _exhaustive = declaration;
23160
+ return { ok: false, reason: `unknown input-type: ${String(_exhaustive.type)}` };
23161
+ }
23162
+ }
23163
+ }
23164
+ function parseJsonValue(raw) {
23165
+ try {
23166
+ return { ok: true, value: JSON.parse(raw) };
23167
+ } catch {
23168
+ return { ok: false, reason: 'expected a JSON value (e.g. ["a","b"])' };
22697
23169
  }
23170
+ }
23171
+ function settingDotKey(pluginId, extId, settingId) {
23172
+ return `plugins.${pluginId}.extensions.${extId}.settings.${settingId}`;
23173
+ }
23174
+ function formatValue2(value) {
23175
+ if (value === void 0 || value === null) return "null";
23176
+ if (Array.isArray(value) || typeof value === "object") return JSON.stringify(value);
23177
+ return String(value);
23178
+ }
23179
+ var LAYER_LABEL = {
23180
+ defaults: "default",
23181
+ project: "settings.json",
23182
+ "project-local": "settings.local.json",
23183
+ override: "override"
22698
23184
  };
23185
+ function layerLabel(layer) {
23186
+ return LAYER_LABEL[layer];
23187
+ }
22699
23188
 
22700
23189
  // cli/commands/plugins.ts
22701
23190
  var PLUGIN_COMMANDS = [
@@ -22706,13 +23195,14 @@ var PLUGIN_COMMANDS = [
22706
23195
  PluginsDisableCommand,
22707
23196
  PluginsCreateCommand,
22708
23197
  PluginsSlotsListCommand,
22709
- PluginsUpgradeCommand
23198
+ PluginsUpgradeCommand,
23199
+ PluginsConfigCommand
22710
23200
  ];
22711
23201
 
22712
23202
  // cli/commands/refresh.ts
22713
23203
  import { readFile as readFile4 } from "fs/promises";
22714
23204
  import { resolve as resolve34 } from "path";
22715
- import { Command as Command29, Option as Option27 } from "clipanion";
23205
+ import { Command as Command30, Option as Option28 } from "clipanion";
22716
23206
 
22717
23207
  // cli/i18n/refresh.texts.ts
22718
23208
  var REFRESH_TEXTS = {
@@ -22768,7 +23258,7 @@ var REFRESH_TEXTS = {
22768
23258
  // cli/commands/refresh.ts
22769
23259
  var RefreshCommand = class extends SmCommand {
22770
23260
  static paths = [["refresh"]];
22771
- static usage = Command29.Usage({
23261
+ static usage = Command30.Usage({
22772
23262
  category: "Scan",
22773
23263
  description: "Refresh enrichment rows: granular (single node) or batch (every stale row).",
22774
23264
  details: `
@@ -22790,11 +23280,11 @@ var RefreshCommand = class extends SmCommand {
22790
23280
  ["Refresh every node with stale enrichments", "$0 refresh --stale"]
22791
23281
  ]
22792
23282
  });
22793
- nodePath = Option27.String({ name: "node", required: false });
22794
- stale = Option27.Boolean("--stale", false, {
23283
+ nodePath = Option28.String({ name: "node", required: false });
23284
+ stale = Option28.Boolean("--stale", false, {
22795
23285
  description: "Refresh every node carrying a stale enrichment row (no-op in this revision; reserved for future Action-prob enrichments)."
22796
23286
  });
22797
- noPlugins = Option27.Boolean("--no-plugins", false, {
23287
+ noPlugins = Option28.Boolean("--no-plugins", false, {
22798
23288
  description: "Skip drop-in plugin discovery; use only the built-in extractor set."
22799
23289
  });
22800
23290
  // The remaining cyclomatic count comes from CLI ergonomics that don't
@@ -22828,9 +23318,11 @@ var RefreshCommand = class extends SmCommand {
22828
23318
  const pluginRuntime = this.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime();
22829
23319
  pluginRuntime.emitWarnings(this.printer);
22830
23320
  listBuiltIns();
23321
+ const refreshCfg = loadConfig({ cwd: ctx.cwd }).effective;
22831
23322
  const composed = composeScanExtensions({
22832
23323
  noBuiltIns: false,
22833
23324
  pluginRuntime,
23325
+ resolveSettings: buildSettingsResolver(refreshCfg),
22834
23326
  killSwitches: readConformanceKillSwitches()
22835
23327
  });
22836
23328
  const allExtractors = composed?.extractors ?? [];
@@ -23096,7 +23588,7 @@ var IntentionalFailCommand = class extends SmCommand {
23096
23588
  };
23097
23589
 
23098
23590
  // cli/commands/scan.ts
23099
- import { Command as Command31, Option as Option29 } from "clipanion";
23591
+ import { Command as Command32, Option as Option30 } from "clipanion";
23100
23592
 
23101
23593
  // kernel/util/format-bytes.ts
23102
23594
  var UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
@@ -23248,7 +23740,7 @@ var SCAN_TEXTS = {
23248
23740
  };
23249
23741
 
23250
23742
  // cli/commands/watch.ts
23251
- import { Command as Command30, Option as Option28 } from "clipanion";
23743
+ import { Command as Command31, Option as Option29 } from "clipanion";
23252
23744
 
23253
23745
  // core/watcher/runtime.ts
23254
23746
  import { dirname as dirname18 } from "path";
@@ -23370,7 +23862,8 @@ function createWatcherRuntime(opts) {
23370
23862
  const composeOpts = {
23371
23863
  noBuiltIns: opts.noBuiltIns,
23372
23864
  pluginRuntime,
23373
- resolveEnabled: resolveEnabledOverride
23865
+ resolveEnabled: resolveEnabledOverride,
23866
+ resolveSettings: buildSettingsResolver(cfg)
23374
23867
  };
23375
23868
  if (opts.killSwitches) composeOpts.killSwitches = opts.killSwitches;
23376
23869
  const composed = composeScanExtensions(composeOpts);
@@ -23787,7 +24280,7 @@ async function runWatchLoop(opts) {
23787
24280
  }
23788
24281
  var WatchCommand = class extends SmCommand {
23789
24282
  static paths = [["watch"]];
23790
- static usage = Command30.Usage({
24283
+ static usage = Command31.Usage({
23791
24284
  category: "Scan",
23792
24285
  description: "Watch roots and run an incremental scan after each debounced batch of filesystem events.",
23793
24286
  details: `
@@ -23811,21 +24304,21 @@ var WatchCommand = class extends SmCommand {
23811
24304
  ["Stream ScanResult per batch as ndjson", "$0 watch --json"]
23812
24305
  ]
23813
24306
  });
23814
- roots = Option28.Rest({ name: "roots" });
23815
- noTokens = Option28.Boolean("--no-tokens", false, {
24307
+ roots = Option29.Rest({ name: "roots" });
24308
+ noTokens = Option29.Boolean("--no-tokens", false, {
23816
24309
  description: "Skip per-node token counts (cl100k_base BPE)."
23817
24310
  });
23818
- strict = Option28.Boolean("--strict", false, {
24311
+ strict = Option29.Boolean("--strict", false, {
23819
24312
  description: "Promote frontmatter-validation findings from warn to error inside each batch. Does not change the watcher exit code."
23820
24313
  });
23821
- noPlugins = Option28.Boolean("--no-plugins", false, {
24314
+ noPlugins = Option29.Boolean("--no-plugins", false, {
23822
24315
  description: "Skip drop-in plugin discovery for the watcher session."
23823
24316
  });
23824
- maxConsecutiveFailures = Option28.String("--max-consecutive-failures", {
24317
+ maxConsecutiveFailures = Option29.String("--max-consecutive-failures", {
23825
24318
  required: false,
23826
24319
  description: "Shut down with exit 2 after N consecutive batch failures (default 5; 0 disables the breaker)."
23827
24320
  });
23828
- maxNodes = Option28.String("--max-nodes", {
24321
+ maxNodes = Option29.String("--max-nodes", {
23829
24322
  required: false,
23830
24323
  description: "Per-batch override of scan.maxNodes (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. When a batch hits the cap, additional files are dropped and the UI surfaces the persistent oversized banner. Validation: integer >= 1."
23831
24324
  });
@@ -23892,7 +24385,7 @@ function parseMaxNodesLimit(raw, stderr, noColor) {
23892
24385
  // cli/commands/scan.ts
23893
24386
  var ScanCommand = class extends SmCommand {
23894
24387
  static paths = [["scan"]];
23895
- static usage = Command31.Usage({
24388
+ static usage = Command32.Usage({
23896
24389
  category: "Scan",
23897
24390
  description: "Scan roots for markdown nodes, run extractors and analyzers.",
23898
24391
  details: `
@@ -23927,35 +24420,35 @@ var ScanCommand = class extends SmCommand {
23927
24420
  ["What would the next incremental scan persist?", "$0 scan --changed -n --json"]
23928
24421
  ]
23929
24422
  });
23930
- roots = Option29.Rest({ name: "roots" });
23931
- noBuiltIns = Option29.Boolean("--no-built-ins", false, {
24423
+ roots = Option30.Rest({ name: "roots" });
24424
+ noBuiltIns = Option30.Boolean("--no-built-ins", false, {
23932
24425
  description: "Skip the built-in extension set. Yields a zero-filled ScanResult (kernel-empty-boot parity); skips DB persistence."
23933
24426
  });
23934
- noPlugins = Option29.Boolean("--no-plugins", false, {
24427
+ noPlugins = Option30.Boolean("--no-plugins", false, {
23935
24428
  description: "Skip drop-in plugin discovery. Only the built-in set runs. Combine with --no-built-ins for a fully empty pipeline."
23936
24429
  });
23937
- noTokens = Option29.Boolean("--no-tokens", false, {
24430
+ noTokens = Option30.Boolean("--no-tokens", false, {
23938
24431
  description: "Skip per-node token counts (cl100k_base BPE). Leaves node.tokens undefined; spec-valid since the field is optional."
23939
24432
  });
23940
- dryRun = Option29.Boolean("-n,--dry-run", false, {
24433
+ dryRun = Option30.Boolean("-n,--dry-run", false, {
23941
24434
  description: "Run the scan in memory and skip every DB write. Combined with --changed, still opens the DB read-side to load the prior snapshot."
23942
24435
  });
23943
- changed = Option29.Boolean("--changed", false, {
24436
+ changed = Option30.Boolean("--changed", false, {
23944
24437
  description: "Incremental scan: reuse unchanged nodes from the persisted prior snapshot. Degrades to a full scan if no prior snapshot exists."
23945
24438
  });
23946
- allowEmpty = Option29.Boolean("--allow-empty", false, {
24439
+ allowEmpty = Option30.Boolean("--allow-empty", false, {
23947
24440
  description: "Allow a zero-result scan to wipe an already-populated DB (replace-all replace by zero rows). Off by default to avoid the typo-trap where an invalid root silently clears your data."
23948
24441
  });
23949
- strict = Option29.Boolean("--strict", false, {
24442
+ strict = Option30.Boolean("--strict", false, {
23950
24443
  description: "Promote frontmatter-validation findings from warn to error (exit code 1 on any violation). Overrides scan.strict from config when both are set."
23951
24444
  });
23952
- watch = Option29.Boolean("--watch", false, {
24445
+ watch = Option30.Boolean("--watch", false, {
23953
24446
  description: "Long-running mode: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`."
23954
24447
  });
23955
- yes = Option29.Boolean("--yes", false, {
24448
+ yes = Option30.Boolean("--yes", false, {
23956
24449
  description: "Non-interactive mode. For ambiguous activeProvider auto-detect, multiple provider markers (.claude/, .codex/, AGENTS.md, .cursor/) under the scan tree exit non-zero instead of prompting; set the lens manually via `sm config set activeProvider <id>` and re-run. Also auto-confirms the pre-1.0 schema-drift rebuild (when the DB was written by a different skill-map major.minor it is deleted and regenerated) instead of prompting."
23957
24450
  });
23958
- maxNodes = Option29.String("--max-nodes", {
24451
+ maxNodes = Option30.String("--max-nodes", {
23959
24452
  required: false,
23960
24453
  description: "Per-invocation override of `scan.maxNodes` (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. When the walker hits the cap, additional files are dropped and the scan is marked oversized in scan_meta (the UI raises a persistent banner pointing at the .skillmapignore editor in Settings \u2192 Project). Validation: integer >= 1."
23961
24454
  });
@@ -24296,10 +24789,10 @@ function countNoun(count3, singular, plural) {
24296
24789
 
24297
24790
  // cli/commands/scan-compare.ts
24298
24791
  import { access, readFile as readFile5 } from "fs/promises";
24299
- import { Command as Command32, Option as Option30 } from "clipanion";
24792
+ import { Command as Command33, Option as Option31 } from "clipanion";
24300
24793
  var ScanCompareCommand = class extends SmCommand {
24301
24794
  static paths = [["scan", "compare-with"]];
24302
- static usage = Command32.Usage({
24795
+ static usage = Command33.Usage({
24303
24796
  category: "Scan",
24304
24797
  description: "Run a fresh scan in memory and emit a delta against the saved ScanResult dump at <dump>. Read-only.",
24305
24798
  details: `
@@ -24327,15 +24820,15 @@ var ScanCompareCommand = class extends SmCommand {
24327
24820
  ["JSON output for tooling", "$0 scan compare-with baseline.json --json"]
24328
24821
  ]
24329
24822
  });
24330
- dump = Option30.String({ required: true });
24331
- roots = Option30.Rest({ name: "roots" });
24332
- noTokens = Option30.Boolean("--no-tokens", false, {
24823
+ dump = Option31.String({ required: true });
24824
+ roots = Option31.Rest({ name: "roots" });
24825
+ noTokens = Option31.Boolean("--no-tokens", false, {
24333
24826
  description: "Skip per-node token counts during the fresh scan."
24334
24827
  });
24335
- strict = Option30.Boolean("--strict", false, {
24828
+ strict = Option31.Boolean("--strict", false, {
24336
24829
  description: "Promote layered-config warnings and frontmatter-validation findings from warn to error."
24337
24830
  });
24338
- noPlugins = Option30.Boolean("--no-plugins", false, {
24831
+ noPlugins = Option31.Boolean("--no-plugins", false, {
24339
24832
  description: "Skip drop-in plugin discovery."
24340
24833
  });
24341
24834
  // Cyclomatic count comes from CLI ergonomics: 3 distinct try/catch
@@ -24377,6 +24870,7 @@ var ScanCompareCommand = class extends SmCommand {
24377
24870
  const composedExtensions = composeScanExtensions({
24378
24871
  noBuiltIns: false,
24379
24872
  pluginRuntime,
24873
+ resolveSettings: buildSettingsResolver(cfg),
24380
24874
  killSwitches: readConformanceKillSwitches()
24381
24875
  });
24382
24876
  let current;
@@ -24540,7 +25034,7 @@ function renderDeltaIssues(issues) {
24540
25034
  // cli/commands/serve.ts
24541
25035
  import { spawn as spawn2 } from "child_process";
24542
25036
  import { existsSync as existsSync31 } from "fs";
24543
- import { Command as Command33, Option as Option31 } from "clipanion";
25037
+ import { Command as Command34, Option as Option32 } from "clipanion";
24544
25038
 
24545
25039
  // kernel/util/dev-mode.ts
24546
25040
  import { sep as sep6 } from "path";
@@ -24801,8 +25295,22 @@ var SERVER_TEXTS = {
24801
25295
  // The single-id variants above still apply for per-entry validation
24802
25296
  // (unknown id, granularity mismatch, lock); these cover the
24803
25297
  // body-shape level.
24804
- pluginsChangesRequired: "Request body must include a `changes` array of `{ id, enabled }` entries.",
24805
- pluginsChangeMalformed: "Each entry in `changes` must have a string `id` and a boolean `enabled`.",
25298
+ pluginsChangesRequired: "Request body must include a `changes` array of `{ id, enabled?, settings? }` entries.",
25299
+ pluginsChangeMalformed: "Each entry in `changes` must have a string `id` plus at least one of `enabled` (boolean) or `settings` (object).",
25300
+ // 400, a bulk change carries `settings` against a bare plugin id.
25301
+ // Settings are per-extension, so they require a qualified
25302
+ // `<plugin>/<ext>` id; a bare plugin id is the wrong granularity.
25303
+ pluginsSettingsRequireQualifiedId: 'Settings can only be written on a qualified `<plugin>/<ext>` id, not the bare plugin id "{{id}}".',
25304
+ // 400, a bulk change carries `settings` for an extension that declares
25305
+ // none in its manifest.
25306
+ pluginsSettingsNoneDeclared: 'Extension "{{pluginId}}/{{extensionId}}" declares no configurable settings.',
25307
+ // 400, a `settings` entry names a settingId the extension does not
25308
+ // declare, or its value fails the declared input-type's rules. The
25309
+ // `{{reason}}` is the resolver's per-type validation message.
25310
+ pluginsSettingsInvalid: 'Invalid setting "{{settingId}}" for "{{pluginId}}/{{extensionId}}": {{reason}}.',
25311
+ // 500, a settings write failed at persist time (AJV revalidation of
25312
+ // the merged config file rejected the result).
25313
+ pluginsSettingsPersistFailed: 'Failed to persist settings for "{{id}}": {{message}}.',
24806
25314
  // ---- preferences route (routes/preferences.ts) --------------------------
24807
25315
  //
24808
25316
  // GET / PATCH /api/preferences. The PATCH body is shaped
@@ -25316,13 +25824,15 @@ function registerGraphRoute(app, deps) {
25316
25824
  }
25317
25825
  function renderGraphPayload(formatter, loaded) {
25318
25826
  const scan = loaded ?? { nodes: [], links: [], issues: [] };
25827
+ const settings2 = formatter.resolvedSettings ?? {};
25319
25828
  if (loaded === null) {
25320
- return formatter.format({ nodes: scan.nodes, links: scan.links, issues: scan.issues });
25829
+ return formatter.format({ nodes: scan.nodes, links: scan.links, issues: scan.issues, settings: settings2 });
25321
25830
  }
25322
25831
  return formatter.format({
25323
25832
  nodes: scan.nodes,
25324
25833
  links: scan.links,
25325
25834
  issues: scan.issues,
25835
+ settings: settings2,
25326
25836
  scanResult: loaded
25327
25837
  });
25328
25838
  }
@@ -25782,6 +26292,72 @@ function normalizeArrayIndices(path) {
25782
26292
  return path.replace(/\/\d+(?=\/|$)/g, "/*");
25783
26293
  }
25784
26294
 
26295
+ // server/routes/plugins-settings.ts
26296
+ function readManifestSettings(manifestLike) {
26297
+ if (manifestLike === null || typeof manifestLike !== "object") return void 0;
26298
+ const settings2 = manifestLike.settings;
26299
+ if (settings2 && typeof settings2 === "object" && !Array.isArray(settings2)) {
26300
+ return settings2;
26301
+ }
26302
+ return void 0;
26303
+ }
26304
+ function projectExtensionSettings(pluginId, extId, declarations, config) {
26305
+ if (!declarations || Object.keys(declarations).length === 0) return {};
26306
+ const settings2 = Object.entries(declarations).map(
26307
+ ([id, declaration]) => ({ ...declaration, id })
26308
+ );
26309
+ const ref = { pluginId, id: extId, settings: declarations };
26310
+ const resolved = resolveExtensionSettings(ref, config, () => {
26311
+ });
26312
+ const settingValues = {};
26313
+ const secretSet = [];
26314
+ for (const [id, declaration] of Object.entries(declarations)) {
26315
+ if (declaration.type === "secret") {
26316
+ if (Object.prototype.hasOwnProperty.call(resolved, id)) secretSet.push(id);
26317
+ continue;
26318
+ }
26319
+ if (Object.prototype.hasOwnProperty.call(resolved, id)) {
26320
+ settingValues[id] = resolved[id];
26321
+ }
26322
+ }
26323
+ return {
26324
+ settings: settings2,
26325
+ settingValues,
26326
+ ...secretSet.length > 0 ? { secretSettingsSet: secretSet } : {}
26327
+ };
26328
+ }
26329
+ function validateSettingsPatch(pluginId, extId, declarations, patch) {
26330
+ for (const [settingId, value] of Object.entries(patch)) {
26331
+ const declaration = declarations?.[settingId];
26332
+ if (!declaration) {
26333
+ return {
26334
+ settingId,
26335
+ reason: `extension "${pluginId}/${extId}" declares no setting "${settingId}"`
26336
+ };
26337
+ }
26338
+ let invalidReason = null;
26339
+ resolveExtensionSettings(
26340
+ { pluginId, id: extId, settings: { [settingId]: declaration } },
26341
+ { plugins: { [pluginId]: { extensions: { [extId]: { settings: { [settingId]: value } } } } } },
26342
+ (message) => {
26343
+ invalidReason = message;
26344
+ }
26345
+ );
26346
+ if (invalidReason !== null) {
26347
+ return { settingId, reason: invalidReason };
26348
+ }
26349
+ }
26350
+ return null;
26351
+ }
26352
+ function persistSettingsPatch(pluginId, extId, declarations, patch, cwd) {
26353
+ for (const [settingId, value] of Object.entries(patch)) {
26354
+ const declaration = declarations?.[settingId];
26355
+ const target = declaration?.type === "secret" ? "project-local" : "project";
26356
+ const dotKey = `plugins.${pluginId}.extensions.${extId}.settings.${settingId}`;
26357
+ writeConfigValue(dotKey, value, { target, cwd });
26358
+ }
26359
+ }
26360
+
25785
26361
  // server/routes/plugins.ts
25786
26362
  var SINGLE_PATCH_BODY_SCHEMA = {
25787
26363
  type: "object",
@@ -25811,10 +26387,18 @@ var BULK_PATCH_BODY_SCHEMA = {
25811
26387
  items: {
25812
26388
  type: "object",
25813
26389
  additionalProperties: false,
25814
- required: ["id", "enabled"],
26390
+ // `id` is the only mandatory field; a change carries `enabled`,
26391
+ // `settings`, or both. `minProperties: 2` enforces at least one
26392
+ // of the two beyond `id` (an `{ id }`-only entry is a no-op the
26393
+ // client should not send). Per-setting type validation runs in
26394
+ // code against the manifest, so `settings` is a permissive
26395
+ // object here (the body schema only fixes its container shape).
26396
+ required: ["id"],
26397
+ minProperties: 2,
25815
26398
  properties: {
25816
26399
  id: { type: "string", minLength: 1 },
25817
- enabled: { type: "boolean" }
26400
+ enabled: { type: "boolean" },
26401
+ settings: { type: "object" }
25818
26402
  }
25819
26403
  }
25820
26404
  }
@@ -25826,7 +26410,13 @@ var parseBulkPatchBody = makeBodyValidator(BULK_PATCH_BODY_SCHEMA, {
25826
26410
  invalid: SERVER_TEXTS.pluginsChangeMalformed,
25827
26411
  mapping: {
25828
26412
  "/changes:required": SERVER_TEXTS.pluginsChangesRequired,
25829
- "/changes:type:array": SERVER_TEXTS.pluginsChangesRequired
26413
+ "/changes:type:array": SERVER_TEXTS.pluginsChangesRequired,
26414
+ // A change with neither `enabled` nor `settings` (just `id`) trips
26415
+ // `minProperties`; surface the same "malformed entry" message.
26416
+ "/changes/*:minProperties": SERVER_TEXTS.pluginsChangeMalformed,
26417
+ "/changes/*:type:object": SERVER_TEXTS.pluginsChangeMalformed,
26418
+ "/changes/*/settings:type:object": SERVER_TEXTS.pluginsChangeMalformed,
26419
+ "/changes/*/enabled:type:boolean": SERVER_TEXTS.pluginsChangeMalformed
25830
26420
  }
25831
26421
  });
25832
26422
  function registerPluginsRoute(app, deps) {
@@ -25910,25 +26500,33 @@ function registerPluginsRoute(app, deps) {
25910
26500
  });
25911
26501
  }
25912
26502
  function listItems(deps, resolveEnabled) {
26503
+ const config = deps.configService.effective();
25913
26504
  return [
25914
- ...deps.options.noBuiltIns ? [] : buildBuiltInItems(resolveEnabled),
25915
- ...buildDiscoveredItems(deps.pluginRuntime.discovered, deps, resolveEnabled)
26505
+ ...deps.options.noBuiltIns ? [] : buildBuiltInItems(resolveEnabled, config),
26506
+ ...buildDiscoveredItems(deps.pluginRuntime.discovered, deps, resolveEnabled, config)
25916
26507
  ];
25917
26508
  }
25918
- function buildBuiltInItems(resolveEnabled) {
26509
+ function buildBuiltInItems(resolveEnabled, config) {
25919
26510
  return sortPluginsForPresentation(builtInPlugins).map((plugin) => {
25920
26511
  const pluginLocked = isPluginLocked(plugin.id);
25921
26512
  const extensions = plugin.extensions.map((ext) => {
25922
26513
  const qualified = qualifiedExtensionId(plugin.id, ext.id);
25923
26514
  const extLocked = pluginLocked || isPluginLocked(qualified);
26515
+ const settings2 = projectExtensionSettings(
26516
+ plugin.id,
26517
+ ext.id,
26518
+ readManifestSettings(ext),
26519
+ config
26520
+ );
25924
26521
  return {
25925
26522
  id: ext.id,
25926
26523
  kind: ext.kind,
25927
26524
  version: ext.version,
25928
- enabled: resolveEnabled(qualified),
26525
+ enabled: resolveEnabled(qualified, installedDefaultEnabled(ext.stability)),
25929
26526
  ...ext.description ? { description: ext.description } : {},
25930
26527
  ...ext.stability ? { stability: ext.stability } : {},
25931
- ...extLocked ? { locked: true } : {}
26528
+ ...extLocked ? { locked: true } : {},
26529
+ ...settings2
25932
26530
  };
25933
26531
  });
25934
26532
  const pluginEnabled = extensions.some((e) => e.enabled);
@@ -25945,12 +26543,12 @@ function buildBuiltInItems(resolveEnabled) {
25945
26543
  };
25946
26544
  });
25947
26545
  }
25948
- function buildDiscoveredItems(discovered, deps, resolveEnabled) {
25949
- return discovered.map((plugin) => buildDiscoveredItem(plugin, deps, resolveEnabled));
26546
+ function buildDiscoveredItems(discovered, deps, resolveEnabled, config) {
26547
+ return discovered.map((plugin) => buildDiscoveredItem(plugin, deps, resolveEnabled, config));
25950
26548
  }
25951
- function buildDiscoveredItem(plugin, deps, resolveEnabled) {
26549
+ function buildDiscoveredItem(plugin, deps, resolveEnabled, config) {
25952
26550
  const pluginLocked = isPluginLocked(plugin.id);
25953
- const extensions = projectExtensionRows(plugin, resolveEnabled, pluginLocked);
26551
+ const extensions = projectExtensionRows(plugin, resolveEnabled, pluginLocked, config);
25954
26552
  const optional = optionalDiscoveredFields(plugin, extensions);
25955
26553
  return {
25956
26554
  id: plugin.id,
@@ -25971,20 +26569,27 @@ function optionalDiscoveredFields(plugin, extensions) {
25971
26569
  if (extensions) out.extensions = extensions;
25972
26570
  return out;
25973
26571
  }
25974
- function projectExtensionRows(plugin, resolveEnabled, pluginLocked) {
26572
+ function projectExtensionRows(plugin, resolveEnabled, pluginLocked, config) {
25975
26573
  if (!plugin.extensions || plugin.extensions.length === 0) return void 0;
25976
26574
  return plugin.extensions.map((ext) => {
25977
26575
  const description = readInstanceDescription(ext.instance);
25978
26576
  const qualified = qualifiedExtensionId(plugin.id, ext.id);
25979
26577
  const extLocked = pluginLocked || isPluginLocked(qualified);
26578
+ const settings2 = projectExtensionSettings(
26579
+ plugin.id,
26580
+ ext.id,
26581
+ readManifestSettings(ext.instance),
26582
+ config
26583
+ );
25980
26584
  return {
25981
26585
  id: ext.id,
25982
26586
  kind: ext.kind,
25983
26587
  version: ext.version,
25984
- enabled: resolveEnabled(qualified),
26588
+ enabled: resolveEnabled(qualified, installedDefaultEnabled(ext.stability)),
25985
26589
  ...description ? { description } : {},
25986
26590
  ...ext.stability ? { stability: ext.stability } : {},
25987
- ...extLocked ? { locked: true } : {}
26591
+ ...extLocked ? { locked: true } : {},
26592
+ ...settings2
25988
26593
  };
25989
26594
  });
25990
26595
  }
@@ -26102,25 +26707,35 @@ function projectListResponse(c, deps, overrides) {
26102
26707
  );
26103
26708
  }
26104
26709
  function validateBulkChange(change, deps) {
26105
- const slash = change.id.indexOf("/");
26106
- if (slash < 0) {
26107
- const handle2 = findHandle(change.id, deps);
26108
- if (!handle2) {
26109
- return {
26110
- status: 404,
26111
- code: "not-found",
26112
- message: tx(SERVER_TEXTS.pluginsUnknown, { id: change.id })
26113
- };
26114
- }
26115
- if (isPluginLocked(change.id)) {
26116
- return {
26117
- status: 403,
26118
- code: "locked",
26119
- message: tx(SERVER_TEXTS.pluginsLocked, { id: change.id })
26120
- };
26121
- }
26122
- return null;
26710
+ return change.id.includes("/") ? validateQualifiedBulkChange(change, deps) : validateBareBulkChange(change, deps);
26711
+ }
26712
+ function validateBareBulkChange(change, deps) {
26713
+ if (change.settings !== void 0) {
26714
+ return {
26715
+ status: 400,
26716
+ code: "bad-query",
26717
+ message: tx(SERVER_TEXTS.pluginsSettingsRequireQualifiedId, { id: change.id })
26718
+ };
26719
+ }
26720
+ const handle = findHandle(change.id, deps);
26721
+ if (!handle) {
26722
+ return {
26723
+ status: 404,
26724
+ code: "not-found",
26725
+ message: tx(SERVER_TEXTS.pluginsUnknown, { id: change.id })
26726
+ };
26727
+ }
26728
+ if (isPluginLocked(change.id)) {
26729
+ return {
26730
+ status: 403,
26731
+ code: "locked",
26732
+ message: tx(SERVER_TEXTS.pluginsLocked, { id: change.id })
26733
+ };
26123
26734
  }
26735
+ return null;
26736
+ }
26737
+ function validateQualifiedBulkChange(change, deps) {
26738
+ const slash = change.id.indexOf("/");
26124
26739
  const pluginId = change.id.slice(0, slash);
26125
26740
  const extensionId = change.id.slice(slash + 1);
26126
26741
  const handle = findHandle(pluginId, deps);
@@ -26145,13 +26760,49 @@ function validateBulkChange(change, deps) {
26145
26760
  message: tx(SERVER_TEXTS.pluginsExtensionLocked, { pluginId, extensionId })
26146
26761
  };
26147
26762
  }
26763
+ if (change.settings !== void 0) {
26764
+ return validateChangeSettings(handle, pluginId, extensionId, change.settings);
26765
+ }
26766
+ return null;
26767
+ }
26768
+ function validateChangeSettings(handle, pluginId, extensionId, patch) {
26769
+ const declarations = handleExtensionSettings(handle, extensionId);
26770
+ if (!declarations || Object.keys(declarations).length === 0) {
26771
+ return {
26772
+ status: 400,
26773
+ code: "bad-query",
26774
+ message: tx(SERVER_TEXTS.pluginsSettingsNoneDeclared, { pluginId, extensionId })
26775
+ };
26776
+ }
26777
+ const failure = validateSettingsPatch(pluginId, extensionId, declarations, patch);
26778
+ if (failure !== null) {
26779
+ return {
26780
+ status: 400,
26781
+ code: "bad-query",
26782
+ message: tx(SERVER_TEXTS.pluginsSettingsInvalid, {
26783
+ settingId: failure.settingId,
26784
+ pluginId,
26785
+ extensionId,
26786
+ reason: failure.reason
26787
+ })
26788
+ };
26789
+ }
26148
26790
  return null;
26149
26791
  }
26792
+ function handleExtensionSettings(handle, extensionId) {
26793
+ if (handle.kind === "built-in") {
26794
+ const ext2 = handle.plugin.extensions.find((e) => e.id === extensionId);
26795
+ return readManifestSettings(ext2);
26796
+ }
26797
+ const ext = (handle.plugin.extensions ?? []).find((e) => e.id === extensionId);
26798
+ return readManifestSettings(ext?.instance);
26799
+ }
26150
26800
  async function persistBulkAndProject(c, deps, changes) {
26151
26801
  const overrides = await tryWithSqlite(
26152
26802
  { databasePath: deps.options.dbPath, autoBackup: false },
26153
26803
  async (adapter) => {
26154
26804
  for (const change of changes) {
26805
+ if (change.enabled === void 0) continue;
26155
26806
  const writeKeys = expandBulkChangeKeys(change, deps);
26156
26807
  for (const key of writeKeys) {
26157
26808
  await applyChangeToAdapter(adapter, key, change.enabled);
@@ -26160,8 +26811,40 @@ async function persistBulkAndProject(c, deps, changes) {
26160
26811
  return await adapter.pluginConfig.loadOverrideMap();
26161
26812
  }
26162
26813
  );
26814
+ if (overrides === null) {
26815
+ throw new DbMissingError(
26816
+ tx(SERVER_TEXTS.pluginsDbMissing, { path: deps.options.dbPath })
26817
+ );
26818
+ }
26819
+ const settingsTouched = persistBulkSettings(deps, changes);
26820
+ if (settingsTouched) deps.configService.reload();
26163
26821
  return projectListResponse(c, deps, overrides);
26164
26822
  }
26823
+ function persistBulkSettings(deps, changes) {
26824
+ const cwd = deps.runtimeContext.cwd;
26825
+ let touched = false;
26826
+ for (const change of changes) {
26827
+ if (change.settings === void 0) continue;
26828
+ const slash = change.id.indexOf("/");
26829
+ if (slash < 0) continue;
26830
+ const pluginId = change.id.slice(0, slash);
26831
+ const extensionId = change.id.slice(slash + 1);
26832
+ const handle = findHandle(pluginId, deps);
26833
+ const declarations = handle ? handleExtensionSettings(handle, extensionId) : void 0;
26834
+ try {
26835
+ persistSettingsPatch(pluginId, extensionId, declarations, change.settings, cwd);
26836
+ } catch (err) {
26837
+ throw new HTTPException9(500, {
26838
+ message: tx(SERVER_TEXTS.pluginsSettingsPersistFailed, {
26839
+ id: change.id,
26840
+ message: err instanceof Error ? err.message : String(err)
26841
+ })
26842
+ });
26843
+ }
26844
+ if (Object.keys(change.settings).length > 0) touched = true;
26845
+ }
26846
+ return touched;
26847
+ }
26165
26848
  function expandBulkChangeKeys(change, deps) {
26166
26849
  if (change.id.includes("/")) return [change.id];
26167
26850
  const handle = findHandle(change.id, deps);
@@ -28513,7 +29196,7 @@ function formatCwdPath(cwd) {
28513
29196
  // cli/commands/serve.ts
28514
29197
  var ServeCommand = class extends SmCommand {
28515
29198
  static paths = [["serve"]];
28516
- static usage = Command33.Usage({
29199
+ static usage = Command34.Usage({
28517
29200
  category: "Setup",
28518
29201
  description: "Start the Hono BFF (single-port: REST + WebSocket + SPA bundle).",
28519
29202
  details: `
@@ -28537,18 +29220,18 @@ var ServeCommand = class extends SmCommand {
28537
29220
  ["Point at a pre-built UI bundle", "$0 serve --ui-dist ./ui/dist/browser"]
28538
29221
  ]
28539
29222
  });
28540
- port = Option31.String("--port", {
29223
+ port = Option32.String("--port", {
28541
29224
  required: false,
28542
29225
  description: "Listening port (default 4242). 0 = OS-assigned."
28543
29226
  });
28544
- host = Option31.String("--host", {
29227
+ host = Option32.String("--host", {
28545
29228
  required: false,
28546
29229
  description: "Listening host (default 127.0.0.1). Loopback-only enforced when --dev-cors is set."
28547
29230
  });
28548
- noBuiltIns = Option31.Boolean("--no-built-ins", false, {
29231
+ noBuiltIns = Option32.Boolean("--no-built-ins", false, {
28549
29232
  description: "Skip built-in plugin registration (parity with sm scan --no-built-ins)."
28550
29233
  });
28551
- noPlugins = Option31.Boolean("--no-plugins", false, {
29234
+ noPlugins = Option32.Boolean("--no-plugins", false, {
28552
29235
  description: "Skip drop-in plugin discovery."
28553
29236
  });
28554
29237
  // `Option.Boolean('--open', true)`, Clipanion's parser auto-derives
@@ -28558,31 +29241,31 @@ var ServeCommand = class extends SmCommand {
28558
29241
  // two registrations for the same flag and rejects the invocation
28559
29242
  // with "Ambiguous Syntax Error". Same convention shipped by every
28560
29243
  // other `--no-...` flag in the CLI tree.
28561
- open = Option31.Boolean("--open", true, {
29244
+ open = Option32.Boolean("--open", true, {
28562
29245
  description: "Auto-open the SPA in the user's default browser after listen. --no-open opts out."
28563
29246
  });
28564
- devCors = Option31.Boolean("--dev-cors", false, {
29247
+ devCors = Option32.Boolean("--dev-cors", false, {
28565
29248
  description: "Enable permissive CORS for the Angular dev-server proxy workflow."
28566
29249
  });
28567
29250
  // `--ui-dist` is intentionally undocumented in the Usage block above
28568
29251
  // (the demo build pipeline + tests rely on it; everyday users never
28569
29252
  // need it). Clipanion still exposes it on the parser; the Usage
28570
29253
  // omission is the "hidden" contract per the 14.1 brief.
28571
- uiDist = Option31.String("--ui-dist", { required: false, hidden: true });
28572
- noUi = Option31.Boolean("--no-ui", false, {
29254
+ uiDist = Option32.String("--ui-dist", { required: false, hidden: true });
29255
+ noUi = Option32.Boolean("--no-ui", false, {
28573
29256
  description: "Don't serve the Angular UI bundle. Use this when running the BFF alongside `ui:dev` (Angular dev server with HMR). The root `/` then renders an inline placeholder pointing the user at the dev server."
28574
29257
  });
28575
- noWatcher = Option31.Boolean("--no-watcher", false, {
29258
+ noWatcher = Option32.Boolean("--no-watcher", false, {
28576
29259
  description: "Disable the chokidar-fed scan-and-broadcast loop. Use only for CI / read-only deployments."
28577
29260
  });
28578
- yes = Option31.Boolean("--yes", false, {
29261
+ yes = Option32.Boolean("--yes", false, {
28579
29262
  description: "Skip the interactive prompt and rebuild the local cache when the on-disk DB has drifted (version skew or an inline schema change). Non-TTY invocations rebuild without asking regardless of this flag."
28580
29263
  });
28581
29264
  // `--watcher-debounce-ms` is undocumented sugar for advanced users
28582
29265
  // who want to tighten / relax the watcher's batching window without
28583
29266
  // editing settings.json. Hidden flag, the Usage block omits it.
28584
- watcherDebounceMs = Option31.String("--watcher-debounce-ms", { required: false, hidden: true });
28585
- maxNodes = Option31.String("--max-nodes", {
29267
+ watcherDebounceMs = Option32.String("--watcher-debounce-ms", { required: false, hidden: true });
29268
+ maxNodes = Option32.String("--max-nodes", {
28586
29269
  required: false,
28587
29270
  description: "Per-invocation override of scan.maxNodes (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. Applies to every scan the server runs (initial watcher pass, debounced batches, POST /api/scan, GET /api/scan?fresh=1). Same flag is honoured on the bare `sm` invocation, which routes to `sm serve`."
28588
29271
  });
@@ -28926,7 +29609,7 @@ function tryOpenBrowser(url, stderr, warnGlyph) {
28926
29609
  }
28927
29610
 
28928
29611
  // cli/commands/show.ts
28929
- import { Command as Command34, Option as Option32 } from "clipanion";
29612
+ import { Command as Command35, Option as Option33 } from "clipanion";
28930
29613
 
28931
29614
  // cli/i18n/show.texts.ts
28932
29615
  var SHOW_TEXTS = {
@@ -28977,7 +29660,7 @@ var SHOW_TEXTS = {
28977
29660
  // cli/commands/show.ts
28978
29661
  var ShowCommand = class extends SmCommand {
28979
29662
  static paths = [["show"]];
28980
- static usage = Command34.Usage({
29663
+ static usage = Command35.Usage({
28981
29664
  category: "Browse",
28982
29665
  description: "Node detail: weight, frontmatter, links, issues.",
28983
29666
  details: `
@@ -28993,7 +29676,7 @@ var ShowCommand = class extends SmCommand {
28993
29676
  ["Machine-readable detail", "$0 show .claude/agents/architect.md --json"]
28994
29677
  ]
28995
29678
  });
28996
- nodePath = Option32.String({ required: true });
29679
+ nodePath = Option33.String({ required: true });
28997
29680
  async run() {
28998
29681
  const dbPath = resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
28999
29682
  const exit = requireDbOrExit(dbPath, this.context.stderr);
@@ -29235,7 +29918,7 @@ function rankConfidenceForGrouping(c) {
29235
29918
  // cli/commands/sidecar.ts
29236
29919
  import { unlink as unlink3 } from "fs/promises";
29237
29920
  import { resolve as resolve38 } from "path";
29238
- import { Command as Command35, Option as Option33 } from "clipanion";
29921
+ import { Command as Command36, Option as Option34 } from "clipanion";
29239
29922
 
29240
29923
  // cli/i18n/sidecar.texts.ts
29241
29924
  var SIDECAR_TEXTS = {
@@ -29316,7 +29999,7 @@ async function runWithSidecarConsent(bag, ansi, dispatch) {
29316
29999
  }
29317
30000
  var SidecarRefreshCommand = class extends SmCommand {
29318
30001
  static paths = [["sidecar", "refresh"]];
29319
- static usage = Command35.Usage({
30002
+ static usage = Command36.Usage({
29320
30003
  category: "Actions",
29321
30004
  description: "Refresh a sidecar's `for.{bodyHash, frontmatterHash}` to match the live node. Does NOT bump the version.",
29322
30005
  details: `
@@ -29333,8 +30016,8 @@ var SidecarRefreshCommand = class extends SmCommand {
29333
30016
  ["Refresh a node's sidecar hashes", "$0 sidecar refresh .claude/agents/architect.md"]
29334
30017
  ]
29335
30018
  });
29336
- nodePath = Option33.String({ required: true });
29337
- yes = Option33.Boolean("--yes", false, {
30019
+ nodePath = Option34.String({ required: true });
30020
+ yes = Option34.Boolean("--yes", false, {
29338
30021
  description: "Confirm writing .sm sidecar files in this project (sets allowEditSmFiles=true on first run)."
29339
30022
  });
29340
30023
  async run() {
@@ -29456,7 +30139,7 @@ var SidecarRefreshCommand = class extends SmCommand {
29456
30139
  };
29457
30140
  var SidecarPruneCommand = class extends SmCommand {
29458
30141
  static paths = [["sidecar", "prune"]];
29459
- static usage = Command35.Usage({
30142
+ static usage = Command36.Usage({
29460
30143
  category: "Actions",
29461
30144
  description: "Delete orphan .sm files (sidecars whose accompanying .md no longer exists).",
29462
30145
  details: `
@@ -29478,8 +30161,8 @@ var SidecarPruneCommand = class extends SmCommand {
29478
30161
  ["Delete every orphan .sm file (non-interactive)", "$0 sidecar prune --yes"]
29479
30162
  ]
29480
30163
  });
29481
- dryRun = Option33.Boolean("-n,--dry-run", false);
29482
- yes = Option33.Boolean("--yes,--force", false, {
30164
+ dryRun = Option34.Boolean("-n,--dry-run", false);
30165
+ yes = Option34.Boolean("--yes,--force", false, {
29483
30166
  description: "Skip the interactive confirmation prompt. Required for non-interactive callers (CI, pre-commit hooks)."
29484
30167
  });
29485
30168
  // Complexity is from per-orphan handling, empty-set / dry-run /
@@ -29599,7 +30282,7 @@ var SidecarPruneCommand = class extends SmCommand {
29599
30282
  };
29600
30283
  var SidecarAnnotateCommand = class extends SmCommand {
29601
30284
  static paths = [["sidecar", "annotate"]];
29602
- static usage = Command35.Usage({
30285
+ static usage = Command36.Usage({
29603
30286
  category: "Actions",
29604
30287
  description: "Scaffold an empty `<basename>.sm` next to a node ready for editing.",
29605
30288
  details: `
@@ -29617,9 +30300,9 @@ var SidecarAnnotateCommand = class extends SmCommand {
29617
30300
  ["Overwrite an existing one", "$0 sidecar annotate .claude/agents/architect.md --force"]
29618
30301
  ]
29619
30302
  });
29620
- nodePath = Option33.String({ required: true });
29621
- force = Option33.Boolean("--force", false);
29622
- yes = Option33.Boolean("--yes", false, {
30303
+ nodePath = Option34.String({ required: true });
30304
+ force = Option34.Boolean("--force", false);
30305
+ yes = Option34.Boolean("--yes", false, {
29623
30306
  description: "Confirm writing .sm sidecar files in this project (sets allowEditSmFiles=true on first run)."
29624
30307
  });
29625
30308
  async run() {
@@ -29758,7 +30441,7 @@ var SIDECAR_COMMANDS = [
29758
30441
  ];
29759
30442
 
29760
30443
  // cli/commands/stubs.ts
29761
- import { Command as Command36, Option as Option34 } from "clipanion";
30444
+ import { Command as Command37, Option as Option35 } from "clipanion";
29762
30445
 
29763
30446
  // cli/i18n/stubs.texts.ts
29764
30447
  var STUBS_TEXTS = {
@@ -29784,7 +30467,7 @@ var StubCommand = class extends SmCommand {
29784
30467
  };
29785
30468
  var DoctorCommand = class extends StubCommand {
29786
30469
  static paths = [["doctor"]];
29787
- static usage = Command36.Usage({
30470
+ static usage = Command37.Usage({
29788
30471
  category: "Setup",
29789
30472
  description: planned("Diagnostic report: DB integrity, pending migrations, orphan rows, plugin status, runner availability.")
29790
30473
  });
@@ -29792,18 +30475,18 @@ var DoctorCommand = class extends StubCommand {
29792
30475
  };
29793
30476
  var FindingsCommand = class extends StubCommand {
29794
30477
  static paths = [["findings"]];
29795
- static usage = Command36.Usage({
30478
+ static usage = Command37.Usage({
29796
30479
  category: "Browse",
29797
30480
  description: planned("Probabilistic findings: injection, stale summaries, low confidence.")
29798
30481
  });
29799
- kind = Option34.String("--kind", { required: false });
29800
- since = Option34.String("--since", { required: false });
29801
- threshold = Option34.String("--threshold", { required: false });
30482
+ kind = Option35.String("--kind", { required: false });
30483
+ since = Option35.String("--since", { required: false });
30484
+ threshold = Option35.String("--threshold", { required: false });
29802
30485
  verbName = "findings";
29803
30486
  };
29804
30487
  var ActionsListCommand = class extends StubCommand {
29805
30488
  static paths = [["actions", "list"]];
29806
- static usage = Command36.Usage({
30489
+ static usage = Command37.Usage({
29807
30490
  category: "Jobs",
29808
30491
  description: planned("Registered action types (manifest view).")
29809
30492
  });
@@ -29811,103 +30494,103 @@ var ActionsListCommand = class extends StubCommand {
29811
30494
  };
29812
30495
  var ActionsShowCommand = class extends StubCommand {
29813
30496
  static paths = [["actions", "show"]];
29814
- static usage = Command36.Usage({
30497
+ static usage = Command37.Usage({
29815
30498
  category: "Jobs",
29816
30499
  description: planned("Full action manifest, including preconditions and expected duration.")
29817
30500
  });
29818
- id = Option34.String({ required: true });
30501
+ id = Option35.String({ required: true });
29819
30502
  verbName = "actions show";
29820
30503
  };
29821
30504
  var JobSubmitCommand = class extends StubCommand {
29822
30505
  static paths = [["job", "submit"]];
29823
- static usage = Command36.Usage({
30506
+ static usage = Command37.Usage({
29824
30507
  category: "Jobs",
29825
30508
  description: planned("Enqueue a single job or fan out to every matching node (--all).")
29826
30509
  });
29827
- action = Option34.String({ required: true });
29828
- node = Option34.String("-n", { required: false });
29829
- all = Option34.Boolean("--all", false);
30510
+ action = Option35.String({ required: true });
30511
+ node = Option35.String("-n", { required: false });
30512
+ all = Option35.Boolean("--all", false);
29830
30513
  // CLI flag stays `--run`; field name is `runFlag` per the
29831
30514
  // shadow-avoidance convention documented on `SmCommand`.
29832
- runFlag = Option34.Boolean("--run", false);
29833
- force = Option34.Boolean("--force", false);
29834
- ttl = Option34.String("--ttl", { required: false });
29835
- priority = Option34.String("--priority", { required: false });
30515
+ runFlag = Option35.Boolean("--run", false);
30516
+ force = Option35.Boolean("--force", false);
30517
+ ttl = Option35.String("--ttl", { required: false });
30518
+ priority = Option35.String("--priority", { required: false });
29836
30519
  verbName = "job submit";
29837
30520
  };
29838
30521
  var JobListCommand = class extends StubCommand {
29839
30522
  static paths = [["job", "list"]];
29840
- static usage = Command36.Usage({ category: "Jobs", description: planned("List jobs.") });
29841
- status = Option34.String("--status", { required: false });
29842
- action = Option34.String("--action", { required: false });
29843
- node = Option34.String("--node", { required: false });
30523
+ static usage = Command37.Usage({ category: "Jobs", description: planned("List jobs.") });
30524
+ status = Option35.String("--status", { required: false });
30525
+ action = Option35.String("--action", { required: false });
30526
+ node = Option35.String("--node", { required: false });
29844
30527
  verbName = "job list";
29845
30528
  };
29846
30529
  var JobShowCommand = class extends StubCommand {
29847
30530
  static paths = [["job", "show"]];
29848
- static usage = Command36.Usage({ category: "Jobs", description: planned("Job detail: state, claim time, TTL, runner, content hash.") });
29849
- id = Option34.String({ required: true });
30531
+ static usage = Command37.Usage({ category: "Jobs", description: planned("Job detail: state, claim time, TTL, runner, content hash.") });
30532
+ id = Option35.String({ required: true });
29850
30533
  verbName = "job show";
29851
30534
  };
29852
30535
  var JobPreviewCommand = class extends StubCommand {
29853
30536
  static paths = [["job", "preview"]];
29854
- static usage = Command36.Usage({ category: "Jobs", description: planned("Render the job MD file without executing.") });
29855
- id = Option34.String({ required: true });
30537
+ static usage = Command37.Usage({ category: "Jobs", description: planned("Render the job MD file without executing.") });
30538
+ id = Option35.String({ required: true });
29856
30539
  verbName = "job preview";
29857
30540
  };
29858
30541
  var JobClaimCommand = class extends StubCommand {
29859
30542
  static paths = [["job", "claim"]];
29860
- static usage = Command36.Usage({
30543
+ static usage = Command37.Usage({
29861
30544
  category: "Jobs",
29862
30545
  description: planned("Atomic primitive: return next queued job id, mark it running.")
29863
30546
  });
29864
- filter = Option34.String("--filter", { required: false });
30547
+ filter = Option35.String("--filter", { required: false });
29865
30548
  verbName = "job claim";
29866
30549
  };
29867
30550
  var JobRunCommand = class extends StubCommand {
29868
30551
  static paths = [["job", "run"]];
29869
- static usage = Command36.Usage({
30552
+ static usage = Command37.Usage({
29870
30553
  category: "Jobs",
29871
30554
  description: planned("Full CLI-runner loop: claim + spawn + record.")
29872
30555
  });
29873
- all = Option34.Boolean("--all", false);
29874
- max = Option34.String("--max", { required: false });
30556
+ all = Option35.Boolean("--all", false);
30557
+ max = Option35.String("--max", { required: false });
29875
30558
  verbName = "job run";
29876
30559
  };
29877
30560
  var JobStatusCommand = class extends StubCommand {
29878
30561
  static paths = [["job", "status"]];
29879
- static usage = Command36.Usage({
30562
+ static usage = Command37.Usage({
29880
30563
  category: "Jobs",
29881
30564
  description: planned("Counts (per status) or single-job status.")
29882
30565
  });
29883
- id = Option34.String({ required: false });
30566
+ id = Option35.String({ required: false });
29884
30567
  verbName = "job status";
29885
30568
  };
29886
30569
  var JobCancelCommand = class extends StubCommand {
29887
30570
  static paths = [["job", "cancel"]];
29888
- static usage = Command36.Usage({
30571
+ static usage = Command37.Usage({
29889
30572
  category: "Jobs",
29890
30573
  description: planned("Force a running job to failed with reason user-cancelled.")
29891
30574
  });
29892
- id = Option34.String({ required: false });
29893
- all = Option34.Boolean("--all", false);
30575
+ id = Option35.String({ required: false });
30576
+ all = Option35.Boolean("--all", false);
29894
30577
  verbName = "job cancel";
29895
30578
  };
29896
30579
  var RecordCommand = class extends StubCommand {
29897
30580
  static paths = [["record"]];
29898
- static usage = Command36.Usage({
30581
+ static usage = Command37.Usage({
29899
30582
  category: "Jobs",
29900
30583
  description: planned("Close a running job with success or failure. Nonce is the sole credential.")
29901
30584
  });
29902
- id = Option34.String("--id", { required: true });
29903
- nonce = Option34.String("--nonce", { required: true });
29904
- status = Option34.String("--status", { required: true });
29905
- report = Option34.String("--report", { required: false });
29906
- tokensIn = Option34.String("--tokens-in", { required: false });
29907
- tokensOut = Option34.String("--tokens-out", { required: false });
29908
- durationMs = Option34.String("--duration-ms", { required: false });
29909
- model = Option34.String("--model", { required: false });
29910
- error = Option34.String("--error", { required: false });
30585
+ id = Option35.String("--id", { required: true });
30586
+ nonce = Option35.String("--nonce", { required: true });
30587
+ status = Option35.String("--status", { required: true });
30588
+ report = Option35.String("--report", { required: false });
30589
+ tokensIn = Option35.String("--tokens-in", { required: false });
30590
+ tokensOut = Option35.String("--tokens-out", { required: false });
30591
+ durationMs = Option35.String("--duration-ms", { required: false });
30592
+ model = Option35.String("--model", { required: false });
30593
+ error = Option35.String("--error", { required: false });
29911
30594
  verbName = "record";
29912
30595
  };
29913
30596
  var STUB_COMMANDS = [
@@ -29931,7 +30614,7 @@ import { cpSync as cpSync2, existsSync as existsSync32, mkdirSync as mkdirSync6,
29931
30614
  import { dirname as dirname20, join as join21, resolve as resolve39 } from "path";
29932
30615
  import { createInterface as createInterface5 } from "readline";
29933
30616
  import { fileURLToPath as fileURLToPath7 } from "url";
29934
- import { Command as Command37, Option as Option35 } from "clipanion";
30617
+ import { Command as Command38, Option as Option36 } from "clipanion";
29935
30618
 
29936
30619
  // cli/i18n/tutorial.texts.ts
29937
30620
  var TUTORIAL_TEXTS = {
@@ -29993,7 +30676,7 @@ var TRIGGER_EN = "run the tutorial";
29993
30676
  var TRIGGER_ES = "ejecuta el tutorial";
29994
30677
  var TutorialCommand = class extends SmCommand {
29995
30678
  static paths = [["tutorial"]];
29996
- static usage = Command37.Usage({
30679
+ static usage = Command38.Usage({
29997
30680
  category: "Setup",
29998
30681
  description: "Materialize an interactive tester tutorial as a Claude Code skill folder under `<cwd>/.claude/skills/`.",
29999
30682
  details: `
@@ -30016,15 +30699,15 @@ var TutorialCommand = class extends SmCommand {
30016
30699
  // more. Accept one so a stale `sm tutorial master` lands on a friendly
30017
30700
  // usage error (guarded in `run()`) instead of clipanion's generic
30018
30701
  // "extraneous argument" message.
30019
- legacyPositional = Option35.String({ required: false });
30702
+ legacyPositional = Option36.String({ required: false });
30020
30703
  // Named `forProvider`, NOT `for` (reserved word). The CLI surface stays
30021
30704
  // `--for`; selects the destination Provider whose `scaffold.skillDir`
30022
30705
  // the skill is materialised under, skipping the interactive prompt.
30023
- forProvider = Option35.String("--for", {
30706
+ forProvider = Option36.String("--for", {
30024
30707
  required: false,
30025
30708
  description: "Destination provider id (e.g. claude, agent-skills). Skips the prompt."
30026
30709
  });
30027
- force = Option35.Boolean("--force", false, {
30710
+ force = Option36.Boolean("--force", false, {
30028
30711
  description: "Overwrite an existing target directory without prompting."
30029
30712
  });
30030
30713
  async run() {
@@ -30255,7 +30938,7 @@ function resolveSkillSourceDir() {
30255
30938
  }
30256
30939
 
30257
30940
  // cli/commands/version.ts
30258
- import { Command as Command38 } from "clipanion";
30941
+ import { Command as Command39 } from "clipanion";
30259
30942
 
30260
30943
  // cli/i18n/version.texts.ts
30261
30944
  var VERSION_TEXTS = {
@@ -30270,7 +30953,7 @@ var VERSION_TEXTS = {
30270
30953
  // cli/commands/version.ts
30271
30954
  var VersionCommand = class extends SmCommand {
30272
30955
  static paths = [["version"]];
30273
- static usage = Command38.Usage({
30956
+ static usage = Command39.Usage({
30274
30957
  category: "Introspection",
30275
30958
  description: "Print the CLI / kernel / spec / runtime / db-schema version matrix."
30276
30959
  });
@@ -30469,4 +31152,4 @@ function resolveBareDefault() {
30469
31152
  process.exit(ExitCode.Error);
30470
31153
  }
30471
31154
  //# sourceMappingURL=cli.js.map
30472
- //# debugId=3118bb11-aad2-50b2-b70d-ed15c2f72a97
31155
+ //# debugId=49fdc2af-2694-5cd0-ac19-33f8d1dffa9e