@skill-map/cli 0.39.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/cli/tutorial/sm-master/references/tour-authoring.md +0 -4
  2. package/dist/cli/tutorial/sm-tutorial/SKILL.md +279 -26
  3. package/dist/cli.js +732 -549
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +28 -13
  7. package/dist/index.js.map +1 -1
  8. package/dist/kernel/index.d.ts +44 -34
  9. package/dist/kernel/index.js +28 -13
  10. package/dist/kernel/index.js.map +1 -1
  11. package/dist/ui/chunk-4HTOYDCM.js +809 -0
  12. package/dist/ui/chunk-4X4GYACU.js +123 -0
  13. package/dist/ui/chunk-7Q3IO77R.js +317 -0
  14. package/dist/ui/chunk-FL6RV2IG.js +2 -0
  15. package/dist/ui/chunk-HGNE4UVQ.js +1 -0
  16. package/dist/ui/chunk-IS5ULQSF.js +1 -0
  17. package/dist/ui/chunk-KVWYVO6I.js +1 -0
  18. package/dist/ui/chunk-N4XX4WPE.js +2190 -0
  19. package/dist/ui/chunk-P7TXZKUX.js +2 -0
  20. package/dist/ui/chunk-UVVXMEZT.js +1025 -0
  21. package/dist/ui/chunk-W2JMLJCF.js +135 -0
  22. package/dist/ui/{chunk-NHI5UPNK.js → chunk-YWWD62BR.js} +1 -1
  23. package/dist/ui/chunk-Z3C2OSRL.js +107 -0
  24. package/dist/ui/chunk-ZAEGBMF7.js +90 -0
  25. package/dist/ui/index.html +2 -2
  26. package/dist/ui/main-F7N5RV4Y.js +3 -0
  27. package/dist/ui/{styles-2WO3KNOY.css → styles-6H4GSOHY.css} +1 -1
  28. package/package.json +2 -2
  29. package/dist/ui/chunk-AALYQ3RG.js +0 -965
  30. package/dist/ui/chunk-JECPBFFX.js +0 -135
  31. package/dist/ui/chunk-LGLLRAJ6.js +0 -61
  32. package/dist/ui/chunk-PVVKVGJ6.js +0 -500
  33. package/dist/ui/chunk-SKA7ZFUX.js +0 -1
  34. package/dist/ui/chunk-VDMXHCXR.js +0 -1
  35. package/dist/ui/chunk-VZIYRREA.js +0 -2588
  36. package/dist/ui/chunk-XRDZZC5F.js +0 -123
  37. package/dist/ui/chunk-YPO2DTMO.js +0 -317
  38. package/dist/ui/chunk-ZYIKNMFV.js +0 -107
  39. package/dist/ui/main-TYWMNAII.js +0 -2
package/dist/cli.js CHANGED
@@ -441,7 +441,6 @@ var claudeProvider = {
441
441
  id: "claude",
442
442
  pluginId: CLAUDE_PLUGIN_ID,
443
443
  kind: "provider",
444
- version: "1.0.0",
445
444
  description: "Classifies files under `.claude/{agents,commands,skills}` as Claude Code agents, commands, and skills.",
446
445
  // Vendor provider: Claude Code only reads its own `.claude/` territory
447
446
  // and ignores `.codex/` / Antigravity layouts at runtime. Gating the
@@ -726,7 +725,6 @@ var atDirectiveExtractor = {
726
725
  id: ID,
727
726
  pluginId: CLAUDE_PLUGIN_ID,
728
727
  kind: "extractor",
729
- version: "1.0.0",
730
728
  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.",
731
729
  scope: "body",
732
730
  precondition: { provider: ["claude"] },
@@ -815,7 +813,6 @@ var slashCommandExtractor = {
815
813
  id: ID2,
816
814
  pluginId: CLAUDE_PLUGIN_ID,
817
815
  kind: "extractor",
818
- version: "1.0.0",
819
816
  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.",
820
817
  scope: "body",
821
818
  precondition: { provider: ["claude"] },
@@ -865,7 +862,6 @@ var antigravityProvider = {
865
862
  id: "antigravity",
866
863
  pluginId: ANTIGRAVITY_PLUGIN_ID,
867
864
  kind: "provider",
868
- version: "1.0.0",
869
865
  description: "Declares the Google Antigravity runtime and its reserved built-in names.",
870
866
  // Vendor provider: marked gated for the day Antigravity grows its own
871
867
  // on-disk kind beyond the open standard. Today `kinds: {}` and
@@ -1014,7 +1010,6 @@ var openaiProvider = {
1014
1010
  id: "openai",
1015
1011
  pluginId: OPENAI_PLUGIN_ID,
1016
1012
  kind: "provider",
1017
- version: "1.0.0",
1018
1013
  description: "Classifies files under `.codex/agents/*.toml` as OpenAI Codex CLI sub-agents.",
1019
1014
  // Vendor provider: Codex CLI only reads its own `.codex/` territory.
1020
1015
  // Gating the classifier behind the active lens keeps the walker from
@@ -1072,7 +1067,6 @@ var agentSkillsProvider = {
1072
1067
  id: "agent-skills",
1073
1068
  pluginId: AGENT_SKILLS_PLUGIN_ID,
1074
1069
  kind: "provider",
1075
- version: "1.0.0",
1076
1070
  description: "Classifies files under `.agents/skills/<name>/SKILL.md` as Agent Skills.",
1077
1071
  read: { extensions: [".md"], parser: "frontmatter-yaml" },
1078
1072
  kinds: {
@@ -1120,7 +1114,6 @@ var coreMarkdownProvider = {
1120
1114
  id: "markdown",
1121
1115
  pluginId: CORE_PLUGIN_ID,
1122
1116
  kind: "provider",
1123
- version: "1.0.0",
1124
1117
  description: "Universal `.md` fallback. Claims any markdown file that no vendor-specific provider has classified.",
1125
1118
  read: { extensions: [".md"], parser: "frontmatter-yaml" },
1126
1119
  // Per spec § A.6, defaultRefreshAction values MUST be qualified
@@ -1156,10 +1149,14 @@ var coreMarkdownProvider = {
1156
1149
  },
1157
1150
  // No `resolution`: `core/markdown` is the universal fallback Provider,
1158
1151
  // it does not declare an invocation surface of its own. Mentions /
1159
- // slashes sourced from markdown bodies still resolve via OTHER
1160
- // Providers' resolution maps (the lookup keys on the source node's
1161
- // Provider id, not on `core/markdown`). Leaving the field absent keeps
1162
- // the contract narrow.
1152
+ // slashes sourced from markdown bodies are still resolved by the
1153
+ // post-walk transform, the lookup keys on the ACTIVE PROVIDER LENS
1154
+ // (per `spec/architecture.md` §Provider · resolution rules), mirroring
1155
+ // the extractor gate that authorised the emission in the first place.
1156
+ // Leaving this field absent therefore has no resolver-side impact
1157
+ // under any lens that DOES declare a resolution map; it would only
1158
+ // matter the day `markdown` itself becomes a lens (which is not on
1159
+ // the roadmap, the format is provider-agnostic by design).
1163
1160
  classify() {
1164
1161
  return "markdown";
1165
1162
  }
@@ -1171,7 +1168,6 @@ var annotationsExtractor = {
1171
1168
  id: ID3,
1172
1169
  pluginId: CORE_PLUGIN_ID,
1173
1170
  kind: "extractor",
1174
- version: "1.0.0",
1175
1171
  description: "Turns the `supersedes` and `supersededBy` entries from a node's `.sm` sidecar into arrows between nodes in the graph.",
1176
1172
  scope: "frontmatter",
1177
1173
  extract(ctx) {
@@ -1233,7 +1229,6 @@ var externalUrlCounterExtractor = {
1233
1229
  id: ID4,
1234
1230
  pluginId: CORE_PLUGIN_ID,
1235
1231
  kind: "extractor",
1236
- version: "1.0.0",
1237
1232
  description: "Counts the distinct external URLs in a node's body and shows the count on the card.",
1238
1233
  scope: "body",
1239
1234
  /**
@@ -1322,7 +1317,6 @@ var markdownLinkExtractor = {
1322
1317
  id: ID5,
1323
1318
  pluginId: CORE_PLUGIN_ID,
1324
1319
  kind: "extractor",
1325
- version: "1.0.0",
1326
1320
  description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph.",
1327
1321
  scope: "body",
1328
1322
  extract(ctx) {
@@ -1384,7 +1378,6 @@ var mcpToolsExtractor = {
1384
1378
  id: ID6,
1385
1379
  pluginId: CORE_PLUGIN_ID,
1386
1380
  kind: "extractor",
1387
- version: "1.0.0",
1388
1381
  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.",
1389
1382
  scope: "frontmatter",
1390
1383
  extract(ctx) {
@@ -1446,7 +1439,6 @@ var toolsCounterExtractor = {
1446
1439
  id: ID7,
1447
1440
  pluginId: CORE_PLUGIN_ID,
1448
1441
  kind: "extractor",
1449
- version: "1.0.0",
1450
1442
  description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card.",
1451
1443
  scope: "frontmatter",
1452
1444
  precondition: { kind: ["claude/agent"] },
@@ -1513,34 +1505,15 @@ var annotationFieldUnknownAnalyzer = {
1513
1505
  id: ID8,
1514
1506
  pluginId: CORE_PLUGIN_ID,
1515
1507
  kind: "analyzer",
1516
- version: "1.0.0",
1517
1508
  description: "Flags typos or unrecognized keys in sidecars (`.sm`).",
1518
1509
  mode: "deterministic",
1519
- ui: {
1520
- // Corner badge on the graph card; count omitted when there is a
1521
- // single unknown field (avoids a noisy "icon + 1" chip).
1522
- alert: {
1523
- slot: "graph.node.alert",
1524
- // Filled warning triangle on the corner, matches the broken-ref
1525
- // alert's "attention-grabbing solid" pattern; the footer chip
1526
- // below stays outlined for the quieter counter pairing.
1527
- icon: "fa-solid fa-triangle-exclamation",
1528
- emitWhenEmpty: false
1529
- },
1530
- // Footer chip on the card, `_counter` shape but rendered icon-only
1531
- // (the analyzer emits `value: 0` so NodeCounter hides the number
1532
- // and only the glyph shows). PrimeIcons `pi-question-circle` so the
1533
- // visual weight matches `annotation-stale`'s `pi-clock` chip
1534
- // sitting next to it on the same footer row. `emitWhenEmpty: true`
1535
- // is required: with `value: 0` the slot treats the payload as
1536
- // empty, so the manifest has to opt in to keep the emission.
1537
- chip: {
1538
- slot: "card.footer.right",
1539
- icon: "pi-question-circle",
1540
- emitWhenEmpty: true,
1541
- priority: 30
1542
- }
1543
- },
1510
+ // No `ui` declaration: the per-node icon-only chip used to surface
1511
+ // "this sidecar declares unknown keys" on `card.footer.right`, but
1512
+ // its severity (`warn`) is now folded into the aggregate counters
1513
+ // emitted by `core/issue-counter`. The detection logic stays, the
1514
+ // findings still ship as `Issue` records (visible in the inspector,
1515
+ // `sm check`, etc.).
1516
+ ui: {},
1544
1517
  // Analyzer body iterates every sidecar root and classifies each
1545
1518
  // key against three buckets (catalog / plugin namespace / unknown
1546
1519
  // root). The per-key branching IS the classification table; factoring
@@ -1623,19 +1596,7 @@ var annotationFieldUnknownAnalyzer = {
1623
1596
  bump2(node.path);
1624
1597
  }
1625
1598
  }
1626
- for (const [nodePath, count] of perNode) {
1627
- const tooltip = count === 1 ? ANNOTATION_FIELD_UNKNOWN_TEXTS.alertTooltipSingle : tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.alertTooltipMany, { count });
1628
- ctx.emitContribution(nodePath, "alert", {
1629
- icon: "fa-solid fa-triangle-exclamation",
1630
- severity: "warn",
1631
- tooltip
1632
- });
1633
- ctx.emitContribution(nodePath, "chip", {
1634
- value: 0,
1635
- severity: "warn",
1636
- tooltip
1637
- });
1638
- }
1599
+ void perNode;
1639
1600
  return issues;
1640
1601
  }
1641
1602
  };
@@ -1694,7 +1655,6 @@ var annotationOrphanAnalyzer = {
1694
1655
  id: ID9,
1695
1656
  pluginId: CORE_PLUGIN_ID,
1696
1657
  kind: "analyzer",
1697
- version: "1.0.0",
1698
1658
  description: "Flags sidecars (`.sm`) whose `.md` file no longer exists.",
1699
1659
  mode: "deterministic",
1700
1660
  evaluate(ctx) {
@@ -1746,7 +1706,6 @@ var annotationStaleAnalyzer = {
1746
1706
  id: ID10,
1747
1707
  pluginId: CORE_PLUGIN_ID,
1748
1708
  kind: "analyzer",
1749
- version: "1.0.0",
1750
1709
  description: "Marks sidecars (`.sm`) that are out of date with their `.md`.",
1751
1710
  mode: "deterministic",
1752
1711
  // The natural fix is to bump the node: refreshes `for` hashes,
@@ -1767,7 +1726,10 @@ var annotationStaleAnalyzer = {
1767
1726
  slot: "card.footer.right",
1768
1727
  icon: "pi-clock",
1769
1728
  emitWhenEmpty: true,
1770
- priority: 20
1729
+ // First in the footer-right cluster: drift is the operator's
1730
+ // entry point for "this node disagrees with its sidecar",
1731
+ // followed by stability, then the severity counters.
1732
+ priority: 10
1771
1733
  }
1772
1734
  },
1773
1735
  evaluate(ctx) {
@@ -1810,7 +1772,6 @@ var contributionOrphanAnalyzer = {
1810
1772
  id: ID11,
1811
1773
  pluginId: CORE_PLUGIN_ID,
1812
1774
  kind: "analyzer",
1813
- version: "0.0.0",
1814
1775
  description: "Warns about plugin data referencing nodes renamed or deleted in the latest scan.",
1815
1776
  mode: "deterministic",
1816
1777
  evaluate(_ctx) {
@@ -1818,6 +1779,89 @@ var contributionOrphanAnalyzer = {
1818
1779
  }
1819
1780
  };
1820
1781
 
1782
+ // plugins/core/analyzers/issue-counter/text.ts
1783
+ var ISSUE_COUNTER_TEXTS = {
1784
+ errorTooltipSingle: "1 error",
1785
+ errorTooltipMany: "{{count}} errors",
1786
+ warnTooltipSingle: "1 warning",
1787
+ warnTooltipMany: "{{count}} warnings"
1788
+ };
1789
+
1790
+ // plugins/core/analyzers/issue-counter/index.ts
1791
+ var ID12 = "issue-counter";
1792
+ function countByTier(issues) {
1793
+ const errors = /* @__PURE__ */ new Map();
1794
+ const warns = /* @__PURE__ */ new Map();
1795
+ for (const issue of issues) {
1796
+ const bucket = issue.severity === "error" ? errors : issue.severity === "warn" ? warns : null;
1797
+ if (!bucket) continue;
1798
+ for (const nodeId of issue.nodeIds) {
1799
+ bucket.set(nodeId, (bucket.get(nodeId) ?? 0) + 1);
1800
+ }
1801
+ }
1802
+ return { errors, warns };
1803
+ }
1804
+ function emitTierChips(ctx, contributionId, severity, counts, singleTooltip, manyTooltip) {
1805
+ for (const [nodePath, count] of counts) {
1806
+ const capped = Math.min(count, 99);
1807
+ ctx.emitContribution(nodePath, contributionId, {
1808
+ value: capped,
1809
+ severity,
1810
+ tooltip: count === 1 ? singleTooltip : tx(manyTooltip, { count })
1811
+ });
1812
+ }
1813
+ }
1814
+ var issueCounterAnalyzer = {
1815
+ id: ID12,
1816
+ pluginId: CORE_PLUGIN_ID,
1817
+ kind: "analyzer",
1818
+ description: "Emits one aggregate severity chip per node (error + warn counts) from the live issue accumulator.",
1819
+ mode: "deterministic",
1820
+ phase: "aggregate",
1821
+ ui: {
1822
+ // Third in the footer-right cluster, after the drift chip
1823
+ // (priority 10) and the stability badge (priority 20). The warn
1824
+ // counter sits before the error counter so the operator reads
1825
+ // "advisory → blocking" left-to-right.
1826
+ warnCount: {
1827
+ slot: "card.footer.right",
1828
+ icon: "pi-exclamation-triangle",
1829
+ emitWhenEmpty: false,
1830
+ priority: 30
1831
+ },
1832
+ // Last in the cluster, the red chip pins to the right edge so the
1833
+ // most severe signal anchors the row's reading position.
1834
+ errorCount: {
1835
+ slot: "card.footer.right",
1836
+ icon: "pi-times-circle",
1837
+ emitWhenEmpty: false,
1838
+ priority: 40
1839
+ }
1840
+ },
1841
+ evaluate(ctx) {
1842
+ const accumulator = ctx.accumulatedIssues ?? [];
1843
+ if (accumulator.length === 0) return [];
1844
+ const { errors, warns } = countByTier(accumulator);
1845
+ emitTierChips(
1846
+ ctx,
1847
+ "errorCount",
1848
+ "danger",
1849
+ errors,
1850
+ ISSUE_COUNTER_TEXTS.errorTooltipSingle,
1851
+ ISSUE_COUNTER_TEXTS.errorTooltipMany
1852
+ );
1853
+ emitTierChips(
1854
+ ctx,
1855
+ "warnCount",
1856
+ "warn",
1857
+ warns,
1858
+ ISSUE_COUNTER_TEXTS.warnTooltipSingle,
1859
+ ISSUE_COUNTER_TEXTS.warnTooltipMany
1860
+ );
1861
+ return [];
1862
+ }
1863
+ };
1864
+
1821
1865
  // plugins/core/analyzers/job-file-orphan/text.ts
1822
1866
  var JOB_FILE_ORPHAN_TEXTS = {
1823
1867
  /**
@@ -1828,12 +1872,11 @@ var JOB_FILE_ORPHAN_TEXTS = {
1828
1872
  };
1829
1873
 
1830
1874
  // plugins/core/analyzers/job-file-orphan/index.ts
1831
- var ID12 = "job-file-orphan";
1875
+ var ID13 = "job-file-orphan";
1832
1876
  var jobFileOrphanAnalyzer = {
1833
- id: ID12,
1877
+ id: ID13,
1834
1878
  pluginId: CORE_PLUGIN_ID,
1835
1879
  kind: "analyzer",
1836
- version: "1.0.0",
1837
1880
  description: "Flags leftover job result files (no live job references them). Clean up via `sm job prune --orphan-files`.",
1838
1881
  mode: "deterministic",
1839
1882
  evaluate(ctx) {
@@ -1842,7 +1885,7 @@ var jobFileOrphanAnalyzer = {
1842
1885
  const issues = [];
1843
1886
  for (const filePath of orphans) {
1844
1887
  issues.push({
1845
- analyzerId: ID12,
1888
+ analyzerId: ID13,
1846
1889
  severity: "warn",
1847
1890
  nodeIds: [filePath],
1848
1891
  message: tx(JOB_FILE_ORPHAN_TEXTS.message, { filePath }),
@@ -1860,12 +1903,11 @@ var LINK_CONFLICT_TEXTS = {
1860
1903
  };
1861
1904
 
1862
1905
  // plugins/core/analyzers/link-conflict/index.ts
1863
- var ID13 = "link-conflict";
1906
+ var ID14 = "link-conflict";
1864
1907
  var linkConflictAnalyzer = {
1865
- id: ID13,
1908
+ id: ID14,
1866
1909
  pluginId: "core",
1867
1910
  kind: "analyzer",
1868
- version: "1.0.0",
1869
1911
  description: "Flags conflicting arrow meanings between extractors (e.g. `references` vs `invokes`).",
1870
1912
  mode: "deterministic",
1871
1913
  // Bucket links by (source, target), then per-bucket detect distinct
@@ -1910,7 +1952,7 @@ var linkConflictAnalyzer = {
1910
1952
  const [source, target] = key.split("\0");
1911
1953
  const kindList = variants.map((v) => v.kind).join(" / ");
1912
1954
  issues.push({
1913
- analyzerId: ID13,
1955
+ analyzerId: ID14,
1914
1956
  severity: "warn",
1915
1957
  nodeIds: [source, target],
1916
1958
  message: tx(LINK_CONFLICT_TEXTS.message, {
@@ -1977,12 +2019,11 @@ function resolveLinkTargetToPath(link, nameIndex) {
1977
2019
  }
1978
2020
 
1979
2021
  // plugins/core/analyzers/link-counter/index.ts
1980
- var ID14 = "link-counter";
2022
+ var ID15 = "link-counter";
1981
2023
  var linkCounterAnalyzer = {
1982
- id: ID14,
2024
+ id: ID15,
1983
2025
  pluginId: CORE_PLUGIN_ID,
1984
2026
  kind: "analyzer",
1985
- version: "1.0.0",
1986
2027
  description: "Counts incoming and outgoing links per node.",
1987
2028
  mode: "deterministic",
1988
2029
  ui: {
@@ -2057,12 +2098,11 @@ var LINK_SELF_LOOP_TEXTS = {
2057
2098
  };
2058
2099
 
2059
2100
  // plugins/core/analyzers/link-self-loop/index.ts
2060
- var ID15 = "link-self-loop";
2101
+ var ID16 = "link-self-loop";
2061
2102
  var linkSelfLoopAnalyzer = {
2062
- id: ID15,
2103
+ id: ID16,
2063
2104
  pluginId: CORE_PLUGIN_ID,
2064
2105
  kind: "analyzer",
2065
- version: "1.0.0",
2066
2106
  description: "Flags links whose source is also their own resolved target (e.g. a body heading like `# /deploy` inside the file that defines `/deploy`).",
2067
2107
  mode: "deterministic",
2068
2108
  evaluate(ctx) {
@@ -2071,7 +2111,7 @@ var linkSelfLoopAnalyzer = {
2071
2111
  for (const link of ctx.links) {
2072
2112
  if (!isSelfLoop(link)) continue;
2073
2113
  issues.push({
2074
- analyzerId: ID15,
2114
+ analyzerId: ID16,
2075
2115
  severity: "warn",
2076
2116
  nodeIds: [link.source],
2077
2117
  message: tx(LINK_SELF_LOOP_TEXTS.message, {
@@ -2175,10 +2215,9 @@ function resolveByName(link, indexes, ctx) {
2175
2215
  const winner = candidates.find((c) => allowedKinds.includes(c.kind));
2176
2216
  return winner ? winner.path : "none";
2177
2217
  }
2178
- function lookupAllowedKinds(link, indexes, ctx) {
2179
- const sourceNode = indexes.nodeByPath.get(link.source);
2180
- if (!sourceNode) return void 0;
2181
- return ctx.providerResolution.get(sourceNode.provider)?.[link.kind];
2218
+ function lookupAllowedKinds(link, _indexes, ctx) {
2219
+ if (ctx.activeProvider === null) return void 0;
2220
+ return ctx.providerResolution.get(ctx.activeProvider)?.[link.kind];
2182
2221
  }
2183
2222
  function stripTriggerSigil(normalized) {
2184
2223
  if (!normalized) return null;
@@ -2221,12 +2260,11 @@ var NAME_RESERVED_TEXTS = {
2221
2260
  };
2222
2261
 
2223
2262
  // plugins/core/analyzers/name-reserved/index.ts
2224
- var ID16 = "name-reserved";
2263
+ var ID17 = "name-reserved";
2225
2264
  var nameReservedAnalyzer = {
2226
- id: ID16,
2265
+ id: ID17,
2227
2266
  pluginId: CORE_PLUGIN_ID,
2228
2267
  kind: "analyzer",
2229
- version: "1.0.0",
2230
2268
  description: "Flags two kinds of reserved-name collision: a file whose name shadows a built-in command of the active runtime, and a link that resolves to one of those reserved names.",
2231
2269
  mode: "deterministic",
2232
2270
  // eslint-disable-next-line complexity
@@ -2240,7 +2278,7 @@ var nameReservedAnalyzer = {
2240
2278
  const node = byPath3.get(path);
2241
2279
  if (!node) continue;
2242
2280
  issues.push({
2243
- analyzerId: ID16,
2281
+ analyzerId: ID17,
2244
2282
  severity: "warn",
2245
2283
  nodeIds: [node.path],
2246
2284
  message: tx(NAME_RESERVED_TEXTS.message, {
@@ -2256,7 +2294,7 @@ var nameReservedAnalyzer = {
2256
2294
  const reservedNode = findReservedNodeForLink(link, reserved, byPath3);
2257
2295
  if (!reservedNode) continue;
2258
2296
  issues.push({
2259
- analyzerId: ID16,
2297
+ analyzerId: ID17,
2260
2298
  severity: "warn",
2261
2299
  nodeIds: [link.source],
2262
2300
  message: tx(NAME_RESERVED_TEXTS.linkMessage, {
@@ -2317,30 +2355,32 @@ function normaliseId(raw) {
2317
2355
  }
2318
2356
 
2319
2357
  // plugins/core/analyzers/node-stability/index.ts
2320
- var ID17 = "node-stability";
2358
+ var ID18 = "node-stability";
2321
2359
  var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
2322
2360
  var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
2323
2361
  var nodeStabilityAnalyzer = {
2324
- id: ID17,
2362
+ id: ID18,
2325
2363
  pluginId: CORE_PLUGIN_ID,
2326
2364
  kind: "analyzer",
2327
- version: "1.0.0",
2328
2365
  description: "Reports a node's stability stage (`experimental`, `deprecated`) on the card.",
2329
2366
  mode: "deterministic",
2330
2367
  ui: {
2368
+ // Second in the footer-right cluster, after the drift chip and
2369
+ // before the severity counters. Stability is a state badge, not a
2370
+ // count, so its priority sits between the two semantic zones.
2331
2371
  experimental: {
2332
2372
  slot: "card.footer.right",
2333
2373
  icon: "fa-solid fa-flask",
2334
2374
  label: "experimental",
2335
2375
  emitWhenEmpty: false,
2336
- priority: 10
2376
+ priority: 20
2337
2377
  },
2338
2378
  deprecated: {
2339
2379
  slot: "card.footer.right",
2340
2380
  icon: "pi-ban",
2341
2381
  label: "deprecated",
2342
2382
  emitWhenEmpty: false,
2343
- priority: 10
2383
+ priority: 20
2344
2384
  }
2345
2385
  },
2346
2386
  evaluate(ctx) {
@@ -2353,7 +2393,7 @@ var nodeStabilityAnalyzer = {
2353
2393
  tooltip: EXPERIMENTAL_TOOLTIP
2354
2394
  });
2355
2395
  issues.push({
2356
- analyzerId: ID17,
2396
+ analyzerId: ID18,
2357
2397
  severity: "info",
2358
2398
  nodeIds: [node.path],
2359
2399
  message: `Node '${node.path}' is marked experimental: API may change.`,
@@ -2366,7 +2406,7 @@ var nodeStabilityAnalyzer = {
2366
2406
  severity: "warn"
2367
2407
  });
2368
2408
  issues.push({
2369
- analyzerId: ID17,
2409
+ analyzerId: ID18,
2370
2410
  severity: "warn",
2371
2411
  nodeIds: [node.path],
2372
2412
  message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
@@ -2400,12 +2440,11 @@ var NODE_SUPERSEDED_TEXTS = {
2400
2440
  };
2401
2441
 
2402
2442
  // plugins/core/analyzers/node-superseded/index.ts
2403
- var ID18 = "node-superseded";
2443
+ var ID19 = "node-superseded";
2404
2444
  var nodeSupersededAnalyzer = {
2405
- id: ID18,
2445
+ id: ID19,
2406
2446
  pluginId: CORE_PLUGIN_ID,
2407
2447
  kind: "analyzer",
2408
- version: "1.0.0",
2409
2448
  description: "Marks nodes replaced by a newer one via `supersededBy`.",
2410
2449
  mode: "deterministic",
2411
2450
  evaluate(ctx) {
@@ -2414,7 +2453,7 @@ var nodeSupersededAnalyzer = {
2414
2453
  const supersededBy = pickSupersededBy(node);
2415
2454
  if (supersededBy === null) continue;
2416
2455
  issues.push({
2417
- analyzerId: ID18,
2456
+ analyzerId: ID19,
2418
2457
  severity: "info",
2419
2458
  nodeIds: [node.path],
2420
2459
  message: tx(NODE_SUPERSEDED_TEXTS.message, {
@@ -2457,66 +2496,34 @@ var REFERENCE_BROKEN_TEXTS = {
2457
2496
  };
2458
2497
 
2459
2498
  // plugins/core/analyzers/reference-broken/index.ts
2460
- var ID19 = "reference-broken";
2499
+ var ID20 = "reference-broken";
2461
2500
  var referenceBrokenAnalyzer = {
2462
- id: ID19,
2501
+ id: ID20,
2463
2502
  pluginId: CORE_PLUGIN_ID,
2464
2503
  kind: "analyzer",
2465
- version: "1.0.0",
2466
2504
  description: "Flags arrows pointing at a node not part of the current scan.",
2467
2505
  mode: "deterministic",
2468
- ui: {
2469
- // Corner badge on the graph card; count omitted when there is a
2470
- // single broken ref (avoids a noisy "icon + 1" chip).
2471
- alert: {
2472
- slot: "graph.node.alert",
2473
- icon: "fa-solid fa-circle-xmark",
2474
- emitWhenEmpty: false
2475
- },
2476
- // Footer chip on the card. `_counter` shape, `value` always shows,
2477
- // so the operator sees "how many" at a glance. Renders OUTLINED
2478
- // (`fa-regular`) so the corner alert (filled, attention-grabbing)
2479
- // and the footer chip (quieter, paired with a number) read as two
2480
- // beats of the same signal rather than two identical glyphs.
2481
- chip: {
2482
- slot: "card.footer.right",
2483
- icon: "fa-regular fa-circle-xmark",
2484
- emitWhenEmpty: false,
2485
- priority: 40
2486
- }
2487
- },
2488
- // The resolver, the reference-paths escape hatch, the per-source
2489
- // aggregation, and the dual-slot emit (with single/plural tooltip and
2490
- // optional count) all live in one flow because they share the per-link
2491
- // loop. Splitting them would re-walk `ctx.links` three times.
2492
- // eslint-disable-next-line complexity
2506
+ // No `ui` declaration: this analyzer used to emit a per-finding
2507
+ // counter chip on `card.footer.right`, but that chip duplicated the
2508
+ // aggregate severity counters now owned by `core/issue-counter`. The
2509
+ // detection logic stays intact, only the chip emission is gone.
2510
+ ui: {},
2511
+ // The resolver, the reference-paths escape hatch, and the hint
2512
+ // index all share the per-link loop, splitting would re-walk
2513
+ // `ctx.links` once per concern. The per-source aggregation that
2514
+ // historically lived alongside (driving the now-retired chip
2515
+ // emission) moved into `core/issue-counter`.
2493
2516
  evaluate(ctx) {
2494
2517
  const byPath3 = new Set(ctx.nodes.map((n) => n.path));
2495
2518
  const byNormalizedName = indexByNormalizedName(ctx.nodes);
2496
2519
  const byBasenameWithoutName = indexByBasenameWithoutName(ctx.nodes);
2497
2520
  const refIndex = ctx.referenceablePaths && ctx.referenceablePaths.size > 0 && ctx.cwd ? { paths: ctx.referenceablePaths, cwd: ctx.cwd } : null;
2498
2521
  const issues = [];
2499
- const perNode = /* @__PURE__ */ new Map();
2500
2522
  for (const link of ctx.links) {
2501
2523
  if (isResolved(link, byPath3, byNormalizedName)) continue;
2502
2524
  if (refIndex && resolvesViaReferencePaths(link, refIndex)) continue;
2503
2525
  const candidates = findHintCandidates(link, byBasenameWithoutName);
2504
2526
  issues.push(buildIssue(link, candidates));
2505
- perNode.set(link.source, (perNode.get(link.source) ?? 0) + 1);
2506
- }
2507
- for (const [nodePath, count] of perNode) {
2508
- const tooltip = count === 1 ? REFERENCE_BROKEN_TEXTS.alertTooltipSingle : tx(REFERENCE_BROKEN_TEXTS.alertTooltipMany, { count });
2509
- const capped = Math.min(count, 99);
2510
- ctx.emitContribution(nodePath, "alert", {
2511
- icon: "fa-solid fa-circle-xmark",
2512
- severity: "danger",
2513
- tooltip
2514
- });
2515
- ctx.emitContribution(nodePath, "chip", {
2516
- value: capped,
2517
- severity: "danger",
2518
- tooltip
2519
- });
2520
2527
  }
2521
2528
  return issues;
2522
2529
  }
@@ -2528,8 +2535,14 @@ function buildIssue(link, hintCandidates = []) {
2528
2535
  trigger: link.trigger?.normalizedTrigger ?? null
2529
2536
  };
2530
2537
  const issue = {
2531
- analyzerId: ID19,
2532
- severity: "warn",
2538
+ analyzerId: ID20,
2539
+ // `error`, not `warn`: a link whose target is not in the scan is a
2540
+ // structural defect the operator must notice, and the card chip
2541
+ // paints `danger` (red) to match. Per the chip-vs-issue policy in
2542
+ // `context/view-slots.md`, a `danger` chip MUST be backed by an
2543
+ // `error` Issue so the visual signal lines up with the exit code
2544
+ // and the global error count on the card.
2545
+ severity: "error",
2533
2546
  nodeIds: [link.source],
2534
2547
  message: tx(REFERENCE_BROKEN_TEXTS.message, {
2535
2548
  kind: link.kind,
@@ -2638,12 +2651,11 @@ var REFERENCE_REDUNDANT_TEXTS = {
2638
2651
  };
2639
2652
 
2640
2653
  // plugins/core/analyzers/reference-redundant/index.ts
2641
- var ID20 = "reference-redundant";
2654
+ var ID21 = "reference-redundant";
2642
2655
  var referenceRedundantAnalyzer = {
2643
- id: ID20,
2656
+ id: ID21,
2644
2657
  pluginId: CORE_PLUGIN_ID,
2645
2658
  kind: "analyzer",
2646
- version: "1.0.0",
2647
2659
  description: "Flags when one node references the same target through two or more different links (e.g. a markdown link plus a `references:` entry).",
2648
2660
  mode: "deterministic",
2649
2661
  evaluate(ctx) {
@@ -2667,7 +2679,7 @@ var referenceRedundantAnalyzer = {
2667
2679
  const [source, resolvedTarget] = key.split("\0");
2668
2680
  const flat = flattenOccurrences(links);
2669
2681
  issues.push({
2670
- analyzerId: ID20,
2682
+ analyzerId: ID21,
2671
2683
  severity: "warn",
2672
2684
  nodeIds: [source],
2673
2685
  message: tx(REFERENCE_REDUNDANT_TEXTS.message, {
@@ -2989,35 +3001,25 @@ var SCHEMA_VIOLATION_TEXTS = {
2989
3001
  };
2990
3002
 
2991
3003
  // plugins/core/analyzers/schema-violation/index.ts
2992
- var ID21 = "schema-violation";
3004
+ var ID22 = "schema-violation";
2993
3005
  var schemaViolationAnalyzer = {
2994
- id: ID21,
3006
+ id: ID22,
2995
3007
  pluginId: CORE_PLUGIN_ID,
2996
3008
  kind: "analyzer",
2997
- version: "1.0.0",
2998
3009
  description: "Flags nodes or links that violate the project schemas.",
2999
3010
  mode: "deterministic",
3000
- ui: {
3001
- // Corner badge on the graph card; surfaces when the node body /
3002
- // frontmatter fails schema validation (parse error, missing
3003
- // `name`/`description`, malformed YAML, etc.). Same visual
3004
- // chassis as `core/reference-broken`, danger severity.
3005
- alert: {
3006
- slot: "graph.node.alert",
3007
- icon: "fa-solid fa-triangle-exclamation",
3008
- emitWhenEmpty: false
3009
- },
3010
- // Footer chip that mirrors the corner alert with the actual
3011
- // count so the operator can scan the cards and prioritise.
3012
- // Outlined (vs the filled corner alert) per the broken-ref
3013
- // pattern: two beats of the same signal.
3014
- chip: {
3015
- slot: "card.footer.right",
3016
- icon: "fa-regular fa-triangle-exclamation",
3017
- emitWhenEmpty: false,
3018
- priority: 35
3019
- }
3020
- },
3011
+ // No `ui` declaration: the per-node failure-count chip used to live
3012
+ // on `card.footer.right`, but its information is now folded into the
3013
+ // aggregate severity counters emitted by `core/issue-counter`. The
3014
+ // findings still emit as `Issue` records, so `sm check` / inspector
3015
+ // unchanged.
3016
+ ui: {},
3017
+ // Pre-existing complexity: validates every node + every link
3018
+ // against multiple schemas with per-severity aggregation. The
3019
+ // branching mirrors the schema catalog and splitting scatters the
3020
+ // validation contract. Tracked as tech-debt; surfaced when an
3021
+ // unrelated change touched the lint cache.
3022
+ // eslint-disable-next-line complexity
3021
3023
  evaluate(ctx) {
3022
3024
  const validators = loadSchemaValidators();
3023
3025
  const findings = [];
@@ -3027,26 +3029,24 @@ var schemaViolationAnalyzer = {
3027
3029
  collectNodeFindings(validators, node, findings);
3028
3030
  collectFrontmatterBaseFindings(node, findings);
3029
3031
  if (findings.length > before) {
3030
- perNode.set(node.path, (perNode.get(node.path) ?? 0) + (findings.length - before));
3032
+ let worst = "warn";
3033
+ for (let i = before; i < findings.length; i++) {
3034
+ if (findings[i].severity === "error") {
3035
+ worst = "danger";
3036
+ break;
3037
+ }
3038
+ }
3039
+ const prev = perNode.get(node.path);
3040
+ perNode.set(node.path, {
3041
+ count: (prev?.count ?? 0) + (findings.length - before),
3042
+ worst: prev?.worst === "danger" ? "danger" : worst
3043
+ });
3031
3044
  }
3032
3045
  }
3033
3046
  for (const link of ctx.links) {
3034
3047
  collectLinkFindings(validators, link, findings);
3035
3048
  }
3036
- for (const [nodePath, count] of perNode) {
3037
- const tooltip = count === 1 ? SCHEMA_VIOLATION_TEXTS.alertTooltipSingle : tx(SCHEMA_VIOLATION_TEXTS.alertTooltipMany, { count });
3038
- const capped = Math.min(count, 99);
3039
- ctx.emitContribution(nodePath, "alert", {
3040
- icon: "fa-solid fa-triangle-exclamation",
3041
- severity: "danger",
3042
- tooltip
3043
- });
3044
- ctx.emitContribution(nodePath, "chip", {
3045
- value: capped,
3046
- severity: "danger",
3047
- tooltip
3048
- });
3049
- }
3049
+ void perNode;
3050
3050
  return findings;
3051
3051
  }
3052
3052
  };
@@ -3054,7 +3054,7 @@ function collectNodeFindings(v, node, out) {
3054
3054
  const result = v.validate("node", toNodeForSchema(node));
3055
3055
  if (result.ok) return;
3056
3056
  out.push({
3057
- analyzerId: ID21,
3057
+ analyzerId: ID22,
3058
3058
  severity: "error",
3059
3059
  nodeIds: [node.path],
3060
3060
  message: tx(SCHEMA_VIOLATION_TEXTS.nodeFailure, {
@@ -3073,7 +3073,7 @@ function collectFrontmatterBaseFindings(node, out) {
3073
3073
  if (isMissingStringField(fm, "description")) missing.push("description");
3074
3074
  if (missing.length === 0) return;
3075
3075
  out.push({
3076
- analyzerId: ID21,
3076
+ analyzerId: ID22,
3077
3077
  // `warn` (not `error`) so the default `sm scan` exit code stays
3078
3078
  // 0 even when nodes are missing frontmatter base fields. Strict
3079
3079
  // mode (`sm scan --strict`) still escalates to exit 1. Matches
@@ -3095,7 +3095,7 @@ function collectLinkFindings(v, link, out) {
3095
3095
  const result = v.validate("link", toLinkForSchema(link));
3096
3096
  if (result.ok) return;
3097
3097
  out.push({
3098
- analyzerId: ID21,
3098
+ analyzerId: ID22,
3099
3099
  severity: "error",
3100
3100
  nodeIds: [link.source],
3101
3101
  message: tx(SCHEMA_VIOLATION_TEXTS.linkFailure, {
@@ -3170,12 +3170,11 @@ var SIGNAL_COLLISION_TEXTS = {
3170
3170
  };
3171
3171
 
3172
3172
  // plugins/core/analyzers/signal-collision/index.ts
3173
- var ID22 = "signal-collision";
3173
+ var ID23 = "signal-collision";
3174
3174
  var signalCollisionAnalyzer = {
3175
- id: ID22,
3175
+ id: ID23,
3176
3176
  pluginId: CORE_PLUGIN_ID,
3177
3177
  kind: "analyzer",
3178
- version: "1.0.0",
3179
3178
  description: "Reports when two extractors fight over the same span of body text, or when a candidate link is dropped (extractor disabled, confidence too low) before it reaches the graph.",
3180
3179
  mode: "deterministic",
3181
3180
  evaluate(ctx) {
@@ -3198,7 +3197,7 @@ function makeIssue(signal) {
3198
3197
  const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3199
3198
  const winnerRange = `${winner.range.start}-${winner.range.end}`;
3200
3199
  return {
3201
- analyzerId: ID22,
3200
+ analyzerId: ID23,
3202
3201
  severity: "warn",
3203
3202
  nodeIds: [signal.source],
3204
3203
  message: tx(SIGNAL_COLLISION_TEXTS.message, {
@@ -3231,7 +3230,7 @@ function makeIssue(signal) {
3231
3230
  if (resolution.extractorDisabled) {
3232
3231
  const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3233
3232
  return {
3234
- analyzerId: ID22,
3233
+ analyzerId: ID23,
3235
3234
  severity: "warn",
3236
3235
  nodeIds: [signal.source],
3237
3236
  message: tx(SIGNAL_COLLISION_TEXTS.messageExtractorDisabled, {
@@ -3250,7 +3249,7 @@ function makeIssue(signal) {
3250
3249
  const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3251
3250
  const topCandidate = signal.candidates[0];
3252
3251
  return {
3253
- analyzerId: ID22,
3252
+ analyzerId: ID23,
3254
3253
  severity: "warn",
3255
3254
  nodeIds: [signal.source],
3256
3255
  message: tx(SIGNAL_COLLISION_TEXTS.messageBelowFloor, {
@@ -3295,18 +3294,17 @@ var TRIGGER_COLLISION_TEXTS = {
3295
3294
  };
3296
3295
 
3297
3296
  // plugins/core/analyzers/trigger-collision/index.ts
3298
- var ID23 = "trigger-collision";
3297
+ var ID24 = "trigger-collision";
3299
3298
  var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
3300
3299
  "command",
3301
3300
  "skill",
3302
3301
  "agent"
3303
3302
  ]);
3304
3303
  var triggerCollisionAnalyzer = {
3305
- id: ID23,
3304
+ id: ID24,
3306
3305
  pluginId: CORE_PLUGIN_ID,
3307
3306
  kind: "analyzer",
3308
3307
  mode: "deterministic",
3309
- version: "1.0.0",
3310
3308
  description: "Flags two or more nodes that claim the same `/command` or `@agent` name.",
3311
3309
  // Two claim-collection passes (advertisement + invocation) feeding
3312
3310
  // the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
@@ -3401,7 +3399,7 @@ function analyzeTriggerBucket(normalized, claims) {
3401
3399
  part: parts[0]
3402
3400
  });
3403
3401
  return {
3404
- analyzerId: ID23,
3402
+ analyzerId: ID24,
3405
3403
  severity: "error",
3406
3404
  nodeIds,
3407
3405
  message,
@@ -3441,14 +3439,13 @@ var ASCII_FORMATTER_TEXTS = {
3441
3439
  };
3442
3440
 
3443
3441
  // plugins/core/formatters/ascii/index.ts
3444
- var ID24 = "ascii";
3442
+ var ID25 = "ascii";
3445
3443
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
3446
3444
  var asciiFormatter = {
3447
- id: ID24,
3445
+ id: ID25,
3448
3446
  pluginId: CORE_PLUGIN_ID,
3449
3447
  kind: "formatter",
3450
- formatId: ID24,
3451
- version: "1.0.0",
3448
+ formatId: ID25,
3452
3449
  description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
3453
3450
  // ASCII tree formatter, header + per-kind sections + per-issue
3454
3451
  // section. Each section iterates and renders; splitting per section
@@ -3542,14 +3539,13 @@ function renderSection(out, kind, group) {
3542
3539
  }
3543
3540
 
3544
3541
  // plugins/core/formatters/json/index.ts
3545
- var ID25 = "json";
3542
+ var ID26 = "json";
3546
3543
  var jsonFormatter = {
3547
- id: ID25,
3544
+ id: ID26,
3548
3545
  pluginId: CORE_PLUGIN_ID,
3549
3546
  kind: "formatter",
3550
- version: "1.0.0",
3551
3547
  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`.",
3552
- formatId: ID25,
3548
+ formatId: ID26,
3553
3549
  format(ctx) {
3554
3550
  if (ctx.scanResult !== void 0) {
3555
3551
  return JSON.stringify(ctx.scanResult);
@@ -3688,12 +3684,11 @@ function resolveSpecRoot2() {
3688
3684
  }
3689
3685
 
3690
3686
  // plugins/core/actions/node-bump/index.ts
3691
- var ID26 = "node-bump";
3687
+ var ID27 = "node-bump";
3692
3688
  var nodeBumpAction = {
3693
- id: ID26,
3689
+ id: ID27,
3694
3690
  pluginId: CORE_PLUGIN_ID,
3695
3691
  kind: "action",
3696
- version: "1.0.0",
3697
3692
  description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
3698
3693
  mode: "deterministic",
3699
3694
  // The runtime contract uses generic <TInput, TReport>; bump narrows
@@ -3749,12 +3744,11 @@ function pickCurrentVersion(overlay) {
3749
3744
  }
3750
3745
 
3751
3746
  // plugins/core/actions/node-supersede/index.ts
3752
- var ID27 = "node-supersede";
3747
+ var ID28 = "node-supersede";
3753
3748
  var nodeSupersedeAction = {
3754
- id: ID27,
3749
+ id: ID28,
3755
3750
  pluginId: CORE_PLUGIN_ID,
3756
3751
  kind: "action",
3757
- version: "0.0.0",
3758
3752
  description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
3759
3753
  mode: "deterministic",
3760
3754
  invoke(_input, _ctx) {
@@ -3861,7 +3855,7 @@ var UPDATE_CHECK_TEXTS = {
3861
3855
  // package.json
3862
3856
  var package_default = {
3863
3857
  name: "@skill-map/cli",
3864
- version: "0.39.0",
3858
+ version: "0.40.1",
3865
3859
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
3866
3860
  license: "MIT",
3867
3861
  type: "module",
@@ -4278,7 +4272,6 @@ var updateCheckHook = {
4278
4272
  id: "update-check",
4279
4273
  pluginId: CORE_PLUGIN_ID,
4280
4274
  kind: "hook",
4281
- version: "1.0.0",
4282
4275
  description: "Checks daily for a newer `skill-map` version on npm. Shows an `update available` banner when one is found.",
4283
4276
  triggers: ["boot"],
4284
4277
  async on(ctx) {
@@ -4294,43 +4287,43 @@ var updateCheckHook = {
4294
4287
  };
4295
4288
 
4296
4289
  // plugins/built-ins.ts
4297
- var claudeProvider2 = { ...claudeProvider, pluginId: "claude" };
4298
- var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude" };
4299
- var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude" };
4300
- var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity" };
4301
- var openaiProvider2 = { ...openaiProvider, pluginId: "openai" };
4302
- var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills" };
4303
- var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core" };
4304
- var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core" };
4305
- var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core" };
4306
- var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core" };
4307
- var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core" };
4308
- var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "core" };
4309
- var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, pluginId: "core" };
4310
- var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core" };
4311
- var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core" };
4312
- var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core" };
4313
- var jobFileOrphanAnalyzer2 = { ...jobFileOrphanAnalyzer, pluginId: "core" };
4314
- var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core" };
4315
- var linkCounterAnalyzer2 = { ...linkCounterAnalyzer, pluginId: "core" };
4316
- var linkSelfLoopAnalyzer2 = { ...linkSelfLoopAnalyzer, pluginId: "core" };
4317
- var nameReservedAnalyzer2 = { ...nameReservedAnalyzer, pluginId: "core" };
4318
- var nodeStabilityAnalyzer2 = { ...nodeStabilityAnalyzer, pluginId: "core" };
4319
- var nodeSupersededAnalyzer2 = { ...nodeSupersededAnalyzer, pluginId: "core" };
4320
- var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core" };
4321
- var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core" };
4322
- var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core" };
4323
- var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core" };
4324
- var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core" };
4325
- var asciiFormatter2 = { ...asciiFormatter, pluginId: "core" };
4326
- var jsonFormatter2 = { ...jsonFormatter, pluginId: "core" };
4327
- var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core" };
4328
- var nodeSupersedeAction2 = { ...nodeSupersedeAction, pluginId: "core" };
4329
- var updateCheckHook2 = { ...updateCheckHook, pluginId: "core" };
4290
+ var claudeProvider2 = { ...claudeProvider, pluginId: "claude", version: "0.40.1" };
4291
+ var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude", version: "0.40.1" };
4292
+ var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude", version: "0.40.1" };
4293
+ var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity", version: "0.40.1" };
4294
+ var openaiProvider2 = { ...openaiProvider, pluginId: "openai", version: "0.40.1" };
4295
+ var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills", version: "0.40.1" };
4296
+ var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core", version: "0.40.1" };
4297
+ var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core", version: "0.40.1" };
4298
+ var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core", version: "0.40.1" };
4299
+ var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core", version: "0.40.1" };
4300
+ var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core", version: "0.40.1" };
4301
+ var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "core", version: "0.40.1" };
4302
+ var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, pluginId: "core", version: "0.40.1" };
4303
+ var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core", version: "0.40.1" };
4304
+ var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core", version: "0.40.1" };
4305
+ var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core", version: "0.40.1" };
4306
+ var issueCounterAnalyzer2 = { ...issueCounterAnalyzer, pluginId: "core", version: "0.40.1" };
4307
+ var jobFileOrphanAnalyzer2 = { ...jobFileOrphanAnalyzer, pluginId: "core", version: "0.40.1" };
4308
+ var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core", version: "0.40.1" };
4309
+ var linkCounterAnalyzer2 = { ...linkCounterAnalyzer, pluginId: "core", version: "0.40.1" };
4310
+ var linkSelfLoopAnalyzer2 = { ...linkSelfLoopAnalyzer, pluginId: "core", version: "0.40.1" };
4311
+ var nameReservedAnalyzer2 = { ...nameReservedAnalyzer, pluginId: "core", version: "0.40.1" };
4312
+ var nodeStabilityAnalyzer2 = { ...nodeStabilityAnalyzer, pluginId: "core", version: "0.40.1" };
4313
+ var nodeSupersededAnalyzer2 = { ...nodeSupersededAnalyzer, pluginId: "core", version: "0.40.1" };
4314
+ var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", version: "0.40.1" };
4315
+ var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: "0.40.1" };
4316
+ var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: "0.40.1" };
4317
+ var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: "0.40.1" };
4318
+ var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: "0.40.1" };
4319
+ var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: "0.40.1" };
4320
+ var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: "0.40.1" };
4321
+ var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core", version: "0.40.1" };
4322
+ var nodeSupersedeAction2 = { ...nodeSupersedeAction, pluginId: "core", version: "0.40.1" };
4323
+ var updateCheckHook2 = { ...updateCheckHook, pluginId: "core", version: "0.40.1" };
4330
4324
  var builtInBundles = [
4331
4325
  {
4332
4326
  id: "claude",
4333
- granularity: "bundle",
4334
4327
  description: "Claude Code platform integration. Classifies files under `.claude/{agents,commands,skills}` and detects Claude-flavored slash commands and at-directives in their bodies.",
4335
4328
  extensions: [
4336
4329
  claudeProvider2,
@@ -4340,7 +4333,6 @@ var builtInBundles = [
4340
4333
  },
4341
4334
  {
4342
4335
  id: "antigravity",
4343
- granularity: "bundle",
4344
4336
  description: "Google Antigravity CLI platform integration (replaces the retired Gemini CLI). Antigravity adopted the open-standard `.agents/` layout, so skills are classified by the neutral `agent-skills` provider; this plugin contributes the Antigravity runtime identity and a seed list of reserved built-in names.",
4345
4337
  extensions: [
4346
4338
  antigravityProvider2
@@ -4348,7 +4340,6 @@ var builtInBundles = [
4348
4340
  },
4349
4341
  {
4350
4342
  id: "openai",
4351
- granularity: "bundle",
4352
4343
  description: "OpenAI Codex CLI platform integration. Classifies TOML sub-agent definitions under `.codex/agents/*.toml`.",
4353
4344
  extensions: [
4354
4345
  openaiProvider2
@@ -4356,7 +4347,6 @@ var builtInBundles = [
4356
4347
  },
4357
4348
  {
4358
4349
  id: "agent-skills",
4359
- granularity: "bundle",
4360
4350
  description: "Open-standard Agent Skills layout. Classifies skills under the vendor-neutral path `.agents/skills/<name>/SKILL.md` (adopted by Anthropic, OpenAI, Google). See agentskills.io.",
4361
4351
  extensions: [
4362
4352
  agentSkillsProvider2
@@ -4364,7 +4354,6 @@ var builtInBundles = [
4364
4354
  },
4365
4355
  {
4366
4356
  id: "core",
4367
- granularity: "extension",
4368
4357
  description: "Core extensions shared across providers: parsers, extractors, analyzers, actions, hooks, formatters, and the universal `.md` fallback provider.",
4369
4358
  extensions: [
4370
4359
  coreMarkdownProvider2,
@@ -4377,6 +4366,7 @@ var builtInBundles = [
4377
4366
  annotationOrphanAnalyzer2,
4378
4367
  annotationStaleAnalyzer2,
4379
4368
  contributionOrphanAnalyzer2,
4369
+ issueCounterAnalyzer2,
4380
4370
  jobFileOrphanAnalyzer2,
4381
4371
  linkConflictAnalyzer2,
4382
4372
  linkCounterAnalyzer2,
@@ -8933,7 +8923,16 @@ var CHECK_TEXTS = {
8933
8923
  tipLine: "\nTip: `sm refresh <node>` to revalidate a file after fixes.\n",
8934
8924
  // --- prob stub advisory ---------------------------------------------------
8935
8925
  probStubAdvisory: "sm check --include-prob: probabilistic Analyzer dispatch requires the job subsystem (Step 10). Stub: skipped {{count}} probabilistic analyzer(s): {{analyzerIds}}. Deterministic analyzers ran as usual; full dispatch lands when the job subsystem ships.\n",
8936
- probStubAdvisoryAsync: "sm check --include-prob --async: probabilistic Analyzer dispatch requires the job subsystem (Step 10). Stub: skipped {{count}} probabilistic analyzer(s): {{analyzerIds}}. The --async flag is reserved for future encoding (returns job ids without waiting once jobs land); today it is a no-op. Deterministic analyzers ran as usual.\n"
8926
+ probStubAdvisoryAsync: "sm check --include-prob --async: probabilistic Analyzer dispatch requires the job subsystem (Step 10). Stub: skipped {{count}} probabilistic analyzer(s): {{analyzerIds}}. The --async flag is reserved for future encoding (returns job ids without waiting once jobs land); today it is a no-op. Deterministic analyzers ran as usual.\n",
8927
+ /**
8928
+ * Emitted on stderr when `--analyzers` lists one or more ids the
8929
+ * loaded analyzer registry does not know. The user almost always
8930
+ * mistyped (e.g. `broken-ref` instead of `reference-broken`); listing
8931
+ * the valid ids inline lets them fix the call without a second round
8932
+ * trip through `sm plugins list`. Exit code is `ExitCode.Error` (2):
8933
+ * bad usage, not "no issues found".
8934
+ */
8935
+ unknownAnalyzerIds: "sm check: unknown analyzer id(s) in --analyzers: {{unknown}}.\nValid ids (qualified or short form accepted):\n{{known}}\n"
8937
8936
  };
8938
8937
 
8939
8938
  // cli/util/conformance-env.ts
@@ -9474,20 +9473,17 @@ var PluginLoader = class {
9474
9473
  * into a helper would scatter the return-on-failure pattern without
9475
9474
  * making the orchestration clearer.
9476
9475
  */
9477
- // eslint-disable-next-line complexity
9478
9476
  async loadOne(pluginPath) {
9479
9477
  const pluginId = pathId(pluginPath);
9480
9478
  const manifestResult = this.#parseAndValidateManifest(pluginPath, pluginId);
9481
9479
  if (!manifestResult.ok) return manifestResult.failure;
9482
9480
  const manifest = manifestResult.manifest;
9483
- const granularity = manifest.granularity ?? "extension";
9484
9481
  if (this.#options.resolveEnabled && !this.#options.resolveEnabled(pluginId)) {
9485
9482
  return {
9486
9483
  path: pluginPath,
9487
9484
  id: pluginId,
9488
9485
  status: "disabled",
9489
9486
  manifest,
9490
- granularity,
9491
9487
  reason: PLUGIN_LOADER_TEXTS.disabledByConfig
9492
9488
  };
9493
9489
  }
@@ -9509,7 +9505,6 @@ var PluginLoader = class {
9509
9505
  id: pluginId,
9510
9506
  status: "enabled",
9511
9507
  manifest,
9512
- granularity,
9513
9508
  extensions: loaded,
9514
9509
  ...storageSchemasResult.schemas ? { storageSchemas: storageSchemasResult.schemas } : {}
9515
9510
  };
@@ -9567,7 +9562,6 @@ var PluginLoader = class {
9567
9562
  id: pluginId,
9568
9563
  status: "incompatible-spec",
9569
9564
  manifest,
9570
- granularity: manifest.granularity ?? "extension",
9571
9565
  reason: tx(PLUGIN_LOADER_TEXTS.incompatibleSpec, {
9572
9566
  installedSpecVersion: this.#options.specVersion,
9573
9567
  specCompat: manifest.specCompat
@@ -9886,25 +9880,9 @@ function isBuiltInExtensionEnabled(bundle, ext, resolveEnabled) {
9886
9880
  return isBundleEntryEnabled(bundle, ext.id, resolveEnabled);
9887
9881
  }
9888
9882
  function isBundleEntryEnabled(bundle, extId, resolveEnabled) {
9889
- if (bundle.granularity === "bundle") {
9890
- if (!resolveEnabled(bundle.id)) return false;
9891
- return resolveEnabled(qualifiedExtensionId(bundle.id, extId));
9892
- }
9893
9883
  return resolveEnabled(qualifiedExtensionId(bundle.id, extId));
9894
9884
  }
9895
- function buildGranularityMap(discovered) {
9896
- const out = /* @__PURE__ */ new Map();
9897
- for (const plugin of discovered) {
9898
- out.set(plugin.id, plugin.granularity ?? "bundle");
9899
- }
9900
- return out;
9901
- }
9902
- function isPluginExtensionEnabled(ext, granularityMap, resolveEnabled) {
9903
- const granularity = granularityMap.get(ext.pluginId) ?? "bundle";
9904
- if (granularity === "bundle") {
9905
- if (!resolveEnabled(ext.pluginId)) return false;
9906
- return resolveEnabled(qualifiedExtensionId(ext.pluginId, ext.id));
9907
- }
9885
+ function isPluginExtensionEnabled(ext, resolveEnabled) {
9908
9886
  return resolveEnabled(qualifiedExtensionId(ext.pluginId, ext.id));
9909
9887
  }
9910
9888
  async function buildEnabledResolver(ctx) {
@@ -10423,7 +10401,6 @@ function filterBuiltInManifests(manifests, resolveEnabled) {
10423
10401
  // core/runtime/plugin-runtime/composer.ts
10424
10402
  function composeScanExtensions(opts) {
10425
10403
  const resolveEnabled = opts.resolveEnabled ?? opts.pluginRuntime.resolveEnabled;
10426
- const granularityMap = buildGranularityMap(opts.pluginRuntime.discovered);
10427
10404
  const providers = [];
10428
10405
  const extractors = [];
10429
10406
  const analyzers = [];
@@ -10435,16 +10412,16 @@ function composeScanExtensions(opts) {
10435
10412
  );
10436
10413
  }
10437
10414
  for (const ext of opts.pluginRuntime.extensions.providers) {
10438
- if (isPluginExtensionEnabled(ext, granularityMap, resolveEnabled)) providers.push(ext);
10415
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) providers.push(ext);
10439
10416
  }
10440
10417
  for (const ext of opts.pluginRuntime.extensions.extractors) {
10441
- if (isPluginExtensionEnabled(ext, granularityMap, resolveEnabled)) extractors.push(ext);
10418
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) extractors.push(ext);
10442
10419
  }
10443
10420
  for (const ext of opts.pluginRuntime.extensions.analyzers) {
10444
- if (isPluginExtensionEnabled(ext, granularityMap, resolveEnabled)) analyzers.push(ext);
10421
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) analyzers.push(ext);
10445
10422
  }
10446
10423
  for (const ext of opts.pluginRuntime.extensions.hooks) {
10447
- if (isPluginExtensionEnabled(ext, granularityMap, resolveEnabled)) hooks.push(ext);
10424
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) hooks.push(ext);
10448
10425
  }
10449
10426
  const finalProviders = opts.killSwitches?.providers === true ? [] : providers;
10450
10427
  const finalExtractors = opts.killSwitches?.extractors === true ? [] : extractors;
@@ -10491,7 +10468,6 @@ function accumulateBuiltInScanExtensions(buckets, resolveEnabled) {
10491
10468
  function composeFormatters(opts) {
10492
10469
  const noBuiltIns = opts.noBuiltIns ?? false;
10493
10470
  const resolveEnabled = opts.resolveEnabled ?? opts.pluginRuntime.resolveEnabled;
10494
- const granularityMap = buildGranularityMap(opts.pluginRuntime.discovered);
10495
10471
  const out = [];
10496
10472
  if (!noBuiltIns) {
10497
10473
  for (const bundle of builtInBundles) {
@@ -10503,32 +10479,24 @@ function composeFormatters(opts) {
10503
10479
  }
10504
10480
  }
10505
10481
  for (const ext of opts.pluginRuntime.extensions.formatters) {
10506
- if (isPluginExtensionEnabled(ext, granularityMap, resolveEnabled)) out.push(ext);
10482
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) out.push(ext);
10507
10483
  }
10508
10484
  return out;
10509
10485
  }
10510
10486
  function registerEnabledExtensions(kernel, pluginRuntime, options = {}) {
10511
10487
  const noBuiltIns = options.noBuiltIns === true;
10512
10488
  const resolveEnabled = options.resolveEnabled ?? pluginRuntime.resolveEnabled;
10513
- const granularityMap = buildGranularityMap(pluginRuntime.discovered);
10514
10489
  if (!noBuiltIns) {
10515
10490
  const enabledBuiltIns = filterBuiltInManifests(listBuiltIns(), resolveEnabled);
10516
10491
  for (const manifest of enabledBuiltIns) kernel.registry.register(manifest);
10517
10492
  }
10518
10493
  for (const manifest of pluginRuntime.manifests) {
10519
- if (!isPluginExtensionEnabled(manifest, granularityMap, resolveEnabled)) continue;
10494
+ if (!isPluginExtensionEnabled(manifest, resolveEnabled)) continue;
10520
10495
  kernel.registry.register(manifest);
10521
10496
  }
10522
10497
  if (kernel.setRegisteredAnnotationKeys) {
10523
10498
  const filteredAnnotations = pluginRuntime.annotationContributions.filter(
10524
- (entry) => (
10525
- // Annotation contributions live at plugin-id granularity (the
10526
- // catalog row carries `pluginId`, not `extensionId`), so the
10527
- // bundle-level toggle gates the entire row. Extension
10528
- // granularity falls through to the manifest-level filter above
10529
- // this surface is bundle-scoped by design.
10530
- resolveEnabled(entry.pluginId)
10531
- )
10499
+ (entry) => resolveEnabled(entry.pluginId)
10532
10500
  );
10533
10501
  kernel.setRegisteredAnnotationKeys(filteredAnnotations);
10534
10502
  }
@@ -10536,7 +10504,6 @@ function registerEnabledExtensions(kernel, pluginRuntime, options = {}) {
10536
10504
  const userContribs = pluginRuntime.viewContributions.filter(
10537
10505
  (entry) => isPluginExtensionEnabled(
10538
10506
  { pluginId: entry.pluginId, id: entry.extensionId },
10539
- granularityMap,
10540
10507
  resolveEnabled
10541
10508
  )
10542
10509
  );
@@ -10602,22 +10569,8 @@ var CheckCommand = class extends SmCommand {
10602
10569
  const exit = requireDbOrExit(dbPath, this.context.stderr);
10603
10570
  if (exit !== null) return exit;
10604
10571
  const analyzerFilter = parseAnalyzersFlag(this.analyzers);
10605
- if (this.includeProb) {
10606
- const probAnalyzerIds = await detectProbAnalyzerIds({
10607
- noPlugins: this.noPlugins,
10608
- analyzerFilter,
10609
- printer: this.printer
10610
- });
10611
- if (probAnalyzerIds.length > 0) {
10612
- const template = this.async ? CHECK_TEXTS.probStubAdvisoryAsync : CHECK_TEXTS.probStubAdvisory;
10613
- this.printer.info(
10614
- tx(template, {
10615
- count: probAnalyzerIds.length,
10616
- analyzerIds: probAnalyzerIds.join(", ")
10617
- })
10618
- );
10619
- }
10620
- }
10572
+ const preflight = await this.#preflightAnalyzerCatalog(analyzerFilter);
10573
+ if (preflight.exit !== null) return preflight.exit;
10621
10574
  return withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
10622
10575
  let issues = await adapter.issues.listAll();
10623
10576
  if (this.node !== void 0) {
@@ -10640,6 +10593,53 @@ var CheckCommand = class extends SmCommand {
10640
10593
  return issues.some((i) => i.severity === "error") ? ExitCode.Issues : ExitCode.Ok;
10641
10594
  });
10642
10595
  }
10596
+ /**
10597
+ * Either an explicit `--analyzers` list or `--include-prob` forces a
10598
+ * load of the live Analyzer catalog: the first needs it to validate
10599
+ * the user-supplied ids against the registry, the second to enumerate
10600
+ * registered probabilistic analyzers. Sharing a single load keeps
10601
+ * `sm check` from paying for two passes when both flags are present.
10602
+ *
10603
+ * Returns `{ exit: <code> }` to short-circuit `run()` when the
10604
+ * validation rejects an unknown id (the only path that aborts before
10605
+ * the DB read). Successful runs return `{ exit: null }`.
10606
+ */
10607
+ async #preflightAnalyzerCatalog(analyzerFilter) {
10608
+ const needsCatalog = analyzerFilter !== void 0 || this.includeProb;
10609
+ if (!needsCatalog) return { exit: null };
10610
+ const analyzers = await loadAnalyzerCatalog({
10611
+ noPlugins: this.noPlugins,
10612
+ printer: this.printer
10613
+ });
10614
+ if (analyzerFilter !== void 0) {
10615
+ const validation = validateAnalyzerFilter(analyzerFilter, analyzers);
10616
+ if (validation !== null) {
10617
+ this.printer.error(validation);
10618
+ return { exit: ExitCode.Error };
10619
+ }
10620
+ }
10621
+ if (this.includeProb) {
10622
+ this.#emitProbAdvisory(analyzers, analyzerFilter);
10623
+ }
10624
+ return { exit: null };
10625
+ }
10626
+ /**
10627
+ * Walk the loaded catalog for probabilistic analyzers honouring the
10628
+ * `--analyzers` filter and, when any survive, emit the stub stderr
10629
+ * advisory naming them. Extracted so `run()` does not carry the
10630
+ * branching for the `--include-prob` / `--async` advisory shapes.
10631
+ */
10632
+ #emitProbAdvisory(analyzers, analyzerFilter) {
10633
+ const probAnalyzerIds = detectProbAnalyzerIds(analyzers, analyzerFilter);
10634
+ if (probAnalyzerIds.length === 0) return;
10635
+ const template = this.async ? CHECK_TEXTS.probStubAdvisoryAsync : CHECK_TEXTS.probStubAdvisory;
10636
+ this.printer.info(
10637
+ tx(template, {
10638
+ count: probAnalyzerIds.length,
10639
+ analyzerIds: probAnalyzerIds.join(", ")
10640
+ })
10641
+ );
10642
+ }
10643
10643
  };
10644
10644
  function parseAnalyzersFlag(raw) {
10645
10645
  if (raw === void 0) return void 0;
@@ -10647,7 +10647,7 @@ function parseAnalyzersFlag(raw) {
10647
10647
  if (ids.length === 0) return void 0;
10648
10648
  return ids;
10649
10649
  }
10650
- async function detectProbAnalyzerIds(opts) {
10650
+ async function loadAnalyzerCatalog(opts) {
10651
10651
  const pluginRuntime = opts.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime();
10652
10652
  pluginRuntime.emitWarnings(opts.printer);
10653
10653
  const composed = composeScanExtensions({
@@ -10655,12 +10655,30 @@ async function detectProbAnalyzerIds(opts) {
10655
10655
  pluginRuntime,
10656
10656
  killSwitches: readConformanceKillSwitches()
10657
10657
  });
10658
- const analyzers = composed?.analyzers ?? [];
10658
+ return composed?.analyzers ?? [];
10659
+ }
10660
+ function validateAnalyzerFilter(filter, analyzers) {
10661
+ const knownQualified = /* @__PURE__ */ new Set();
10662
+ const knownShort = /* @__PURE__ */ new Set();
10663
+ for (const analyzer of analyzers) {
10664
+ const qualified = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
10665
+ knownQualified.add(qualified);
10666
+ knownShort.add(analyzer.id);
10667
+ }
10668
+ const unknown = filter.filter((id) => !knownQualified.has(id) && !knownShort.has(id));
10669
+ if (unknown.length === 0) return null;
10670
+ const knownList = [...knownQualified].sort().map((id) => ` ${id}`).join("\n");
10671
+ return tx(CHECK_TEXTS.unknownAnalyzerIds, {
10672
+ unknown: unknown.join(", "),
10673
+ known: knownList
10674
+ });
10675
+ }
10676
+ function detectProbAnalyzerIds(analyzers, analyzerFilter) {
10659
10677
  const probIds = [];
10660
10678
  for (const analyzer of analyzers) {
10661
10679
  if (analyzer.mode !== "probabilistic") continue;
10662
10680
  const qualified = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
10663
- if (opts.analyzerFilter && !matchesAnalyzerFilter(qualified, opts.analyzerFilter)) continue;
10681
+ if (analyzerFilter && !matchesAnalyzerFilter(qualified, analyzerFilter)) continue;
10664
10682
  probIds.push(qualified);
10665
10683
  }
10666
10684
  probIds.sort();
@@ -14733,8 +14751,8 @@ function isExternalUrlLink(link) {
14733
14751
  }
14734
14752
 
14735
14753
  // kernel/orchestrator/analyzers.ts
14736
- async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, signals) {
14737
- const issues = [];
14754
+ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, signals, seedIssues = []) {
14755
+ const issues = [...seedIssues];
14738
14756
  const contributions = [];
14739
14757
  const validators = loadSchemaValidators();
14740
14758
  void registeredActionIds;
@@ -14742,7 +14760,8 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
14742
14760
  relativePath: o.relativePath,
14743
14761
  expectedMdPath: o.expectedMdPath
14744
14762
  }));
14745
- for (const analyzer of analyzers) {
14763
+ const scheduled = orderAnalyzersByPhase(analyzers);
14764
+ for (const analyzer of scheduled) {
14746
14765
  const qualifiedId2 = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
14747
14766
  const declaredContributions = readDeclaredContributions(analyzer);
14748
14767
  const emitContribution = (nodePath, contributionId, payload) => {
@@ -14795,6 +14814,11 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
14795
14814
  annotationContributions,
14796
14815
  viewContributions,
14797
14816
  orphanJobFiles,
14817
+ // `issues` is the live accumulator, mutated by `issues.push(...)`
14818
+ // below as each analyzer's emission lands. Late-phase analyzers
14819
+ // (`core/issue-counter`) read it to compute cross-analyzer
14820
+ // aggregates. Treat as read-only on the analyzer side.
14821
+ accumulatedIssues: issues,
14798
14822
  ...referenceablePaths ? { referenceablePaths } : {},
14799
14823
  ...cwd ? { cwd } : {},
14800
14824
  ...reservedNodePaths ? { reservedNodePaths } : {},
@@ -14811,6 +14835,12 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
14811
14835
  }
14812
14836
  return { issues, contributions };
14813
14837
  }
14838
+ function orderAnalyzersByPhase(analyzers) {
14839
+ return analyzers.slice().sort((a, b) => phaseRank(a) - phaseRank(b));
14840
+ }
14841
+ function phaseRank(a) {
14842
+ return a.phase === "aggregate" ? 1 : 0;
14843
+ }
14814
14844
  function validateIssue(analyzer, issue, emitter) {
14815
14845
  const severity = issue.severity;
14816
14846
  if (severity !== "error" && severity !== "warn" && severity !== "info") {
@@ -15955,7 +15985,7 @@ async function runScanInternal(_kernel, options) {
15955
15985
  else walked.internalLinks.push(link);
15956
15986
  }
15957
15987
  walked.signals = resolved.resolvedSignals;
15958
- const postWalkCtx = buildPostWalkTransformCtx(exts.providers, walked.nodes);
15988
+ const postWalkCtx = buildPostWalkTransformCtx(exts.providers, walked.nodes, activeProviderId);
15959
15989
  walked.internalLinks = applyPostWalkTransforms(walked.internalLinks, walked.nodes, postWalkCtx);
15960
15990
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
15961
15991
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
@@ -15978,11 +16008,15 @@ async function runScanInternal(_kernel, options) {
15978
16008
  emitter,
15979
16009
  hookDispatcher,
15980
16010
  postWalkCtx.reservedNodePaths,
15981
- walked.signals
16011
+ walked.signals,
16012
+ // Seed the accumulator with orchestrator-emitted frontmatter
16013
+ // issues so the aggregate phase (`core/issue-counter`) counts
16014
+ // them on the per-node chip. The seeds are echoed back on
16015
+ // `analyzerResult.issues`, no explicit push is needed below.
16016
+ walked.frontmatterIssues
15982
16017
  );
15983
16018
  mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);
15984
16019
  const issues = analyzerResult.issues;
15985
- for (const issue of walked.frontmatterIssues) issues.push(issue);
15986
16020
  const silenced = options.ignoreFilter ? (path) => options.ignoreFilter.ignores(path) : void 0;
15987
16021
  const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues, silenced) : [];
15988
16022
  const stats = buildScanStats(walked, issues, start);
@@ -15991,14 +16025,14 @@ async function runScanInternal(_kernel, options) {
15991
16025
  await hookDispatcher.dispatch("scan.completed", scanCompletedEvent);
15992
16026
  return buildScanReturn(walked, issues, renameOps, stats, options, setup);
15993
16027
  }
15994
- function buildPostWalkTransformCtx(providers, nodes) {
16028
+ function buildPostWalkTransformCtx(providers, nodes, activeProvider) {
15995
16029
  const { kindRegistry, providerResolution, reservedNamesByProviderKind } = buildProviderIndexes(providers);
15996
16030
  const reservedNodePaths = buildReservedNodePaths(
15997
16031
  nodes,
15998
16032
  kindRegistry,
15999
16033
  reservedNamesByProviderKind
16000
16034
  );
16001
- return { kindRegistry, providerResolution, reservedNodePaths };
16035
+ return { kindRegistry, providerResolution, activeProvider, reservedNodePaths };
16002
16036
  }
16003
16037
  function buildProviderIndexes(providers) {
16004
16038
  const kindRegistry = /* @__PURE__ */ new Map();
@@ -16450,9 +16484,18 @@ var SCAN_RUNNER_TEXTS = {
16450
16484
  * markers (`.claude/`, `.codex/`, `AGENTS.md`, `.cursor/`) anywhere
16451
16485
  * under cwd or the effective scan roots. Plain-markdown projects
16452
16486
  * keep scanning fine; provider-specific extractors silently no-op
16453
- * for this scan.
16487
+ * for this scan. Follows `context/cli-output-style.md` §3.1b
16488
+ * (two-line block, glyph + dim hint):
16489
+ * - line 1: `{{glyph}}` (yellow `⚠`) + headline naming the
16490
+ * missing markers,
16491
+ * - line 2 (indent 3): `{{hint}}`, dim, names the consequence
16492
+ * and the actionable next step.
16493
+ * Both the full block AND the bare hint are catalog-side so the
16494
+ * caller can wrap the hint in `ansi.dim(...)` without splitting
16495
+ * the template manually.
16454
16496
  */
16455
- activeProviderNoMarkerWarning: "No provider markers detected (.claude/, .codex/, AGENTS.md, .cursor/). Scanning as universal markdown only; provider-specific link types (e.g. claude @-directives, /-commands) will not appear in the graph. Set `activeProvider` in .skill-map/settings.json or install a provider plugin to enable them.",
16497
+ activeProviderNoMarkerWarning: "{{glyph}} No provider markers detected (.claude/, .codex/, AGENTS.md, .cursor/).\n {{hint}}\n",
16498
+ activeProviderNoMarkerWarningHint: "Scanning as universal markdown only; provider-specific link types (e.g. claude @-directives, /-commands) will not appear. Set `activeProvider` in .skill-map/settings.json or install a provider plugin to enable them.",
16456
16499
  /**
16457
16500
  * Active-provider bootstrap: filesystem auto-detect found exactly
16458
16501
  * one marker and persisted the detected id to project settings.
@@ -16605,7 +16648,14 @@ async function bootstrapActiveProvider(opts) {
16605
16648
  }
16606
16649
  const detected = aggregateDetected(opts.cwd, opts.effectiveRoots, fromCwd.detected);
16607
16650
  if (detected.length === 0) {
16608
- opts.printer.warn(SCAN_RUNNER_TEXTS.activeProviderNoMarkerWarning);
16651
+ const warnGlyph = opts.style?.warnGlyph ?? "\u26A0";
16652
+ const dim = opts.style?.dim ?? ((s) => s);
16653
+ opts.printer.warn(
16654
+ tx(SCAN_RUNNER_TEXTS.activeProviderNoMarkerWarning, {
16655
+ glyph: warnGlyph,
16656
+ hint: dim(SCAN_RUNNER_TEXTS.activeProviderNoMarkerWarningHint)
16657
+ })
16658
+ );
16609
16659
  return { kind: "ok", activeProvider: null, source: "none" };
16610
16660
  }
16611
16661
  if (detected.length === 1) {
@@ -18814,21 +18864,6 @@ import { Command as Command22, Option as Option21 } from "clipanion";
18814
18864
  // cli/i18n/plugins.texts.ts
18815
18865
  var PLUGINS_TEXTS = {
18816
18866
  // --- enable / disable error guidance --------------------------------
18817
- // Spec § A.7, granularity validation. The CLI rejects mismatched ids
18818
- // up front (instead of silently writing a config_plugins row that the
18819
- // runtime would later ignore) so the user learns the model immediately.
18820
- /**
18821
- * Granularity-mismatch errors share a structured shape:
18822
- * ✕ <headline>
18823
- * <fix-line>
18824
- * <hint-line>
18825
- * Glyph + indent + dim hint applied at the call site so all four
18826
- * "wrong shape" advisories read the same way.
18827
- */
18828
- granularityBundleRejectsQualified: "{{glyph}} '{{bundleId}}' has granularity=bundle.\n Use `sm plugins {{verb}} {{bundleId}}` to {{verb}} the whole bundle.\n {{hint}}\n",
18829
- granularityBundleRejectsQualifiedHint: "Individual extensions inside a bundle-granularity plugin cannot be toggled.",
18830
- granularityExtensionRejectsBundleId: "{{glyph}} '{{bundleId}}' has granularity=extension.\n Use `sm plugins {{verb}} {{bundleId}}/<ext-id>` to {{verb}} a single extension.\n {{hint}}\n",
18831
- granularityExtensionRejectsBundleIdHint: "Run `sm plugins list` for the per-extension qualified ids.",
18832
18867
  pluginNotFound: "{{glyph}} Plugin not found: {{id}}\n {{hint}}\n",
18833
18868
  pluginNotFoundHint: "Run `sm plugins list` for discovered ids and the qualified extension ids.",
18834
18869
  pluginLocked: '{{glyph}} Plugin "{{id}}" is locked by the host and cannot be toggled.\n {{hint}}\n',
@@ -18859,8 +18894,14 @@ var PLUGINS_TEXTS = {
18859
18894
  // --- list verb -------------------------------------------------------
18860
18895
  listEmpty: "No plugins discovered.\n",
18861
18896
  // --- doctor verb -----------------------------------------------------
18862
- /** One-line summary that opens the human doctor output. */
18863
- doctorSummary: "plugins doctor: {{enabled}} enabled \xB7 {{issues}} issue{{issuesPlural}} \xB7 {{warnings}} warning{{warningsPlural}}\n\n",
18897
+ /**
18898
+ * One-line summary that opens the human doctor output. `enabled` is
18899
+ * the count of enabled extensions across every bundle (every
18900
+ * extension is independently toggle-able by its qualified id); the
18901
+ * value matches the row count rendered by `sm plugins list` once
18902
+ * disabled extensions are filtered out.
18903
+ */
18904
+ doctorSummary: "plugins doctor: {{enabled}} enabled extension{{enabledPlural}} \xB7 {{issues}} issue{{issuesPlural}} \xB7 {{warnings}} warning{{warningsPlural}}\n\n",
18864
18905
  /** Source breakdown row (built-in vs user). Indented 4 to match the status rows. */
18865
18906
  doctorSourceRow: " {{label}} {{count}}\n",
18866
18907
  /** Status breakdown table heading. */
@@ -18894,8 +18935,35 @@ var PLUGINS_TEXTS = {
18894
18935
  toggleNeitherIdNorAllHint: "Examples: `sm plugins {{verb}} <id1> <id2>` (explicit set), `sm plugins {{verb}} --all` (every discovered plugin).",
18895
18936
  toggleResolveError: "{{error}}",
18896
18937
  toggleAppliedSingle: "{{verbPast}}: {{id}}\n",
18897
- toggleAppliedManyHeader: "{{verbPast}}: {{count}} plugin(s)\n",
18938
+ toggleAppliedManyHeader: "{{verbPast}}: {{count}} extension(s)\n",
18898
18939
  toggleAppliedManyRow: " - {{id}}\n",
18940
+ /**
18941
+ * Macro expansion summary printed on stderr before the confirm
18942
+ * prompt (or before the `--yes` rejection). The block lists every
18943
+ * qualified extension id the bare bundle id resolves to, so the
18944
+ * user sees the exact set that would flip.
18945
+ */
18946
+ bundleMacroHeader: "sm plugins {{verb}} {{bundleId}}: this will affect {{count}} extensions:\n",
18947
+ bundleMacroRow: " - {{id}}\n",
18948
+ /**
18949
+ * Interactive prompt rendered to a TTY by the macro path. The
18950
+ * `confirm` helper appends the `[y/N]` suffix from UTIL_TEXTS.
18951
+ */
18952
+ bundleMacroConfirmPrompt: "Apply this {{verb}} to every listed extension?",
18953
+ /**
18954
+ * Stderr advisory when a TTY user answers no to the macro prompt.
18955
+ * The verb exits non-zero (ExitCode.Error) so callers can detect
18956
+ * the cancellation.
18957
+ */
18958
+ bundleMacroCancelled: "Cancelled.\n",
18959
+ /**
18960
+ * Non-TTY rejection path: pipes / CI cannot prompt, so the verb
18961
+ * refuses unless `--yes` is set. The body lines come from
18962
+ * `bundleMacroHeader` / `bundleMacroRow` above; this template adds
18963
+ * the directed re-run hint.
18964
+ */
18965
+ bundleMacroRequiresYes: "{{glyph}} Refusing to {{verb}} multiple extensions without confirmation.\n {{hint}}\n",
18966
+ bundleMacroRequiresYesHint: "Re-run with --yes to apply, or pass a qualified id `<bundle>/<extension>` for a single extension.",
18899
18967
  // --- list / show renderers ------------------------------------------
18900
18968
  rowStatusOk: "ok",
18901
18969
  rowStatusOff: "off",
@@ -18941,17 +19009,15 @@ var PLUGINS_TEXTS = {
18941
19009
  /** Extensions block heading, separated from the header by a blank line. */
18942
19010
  detailExtensionsBlock: "\n",
18943
19011
  /**
18944
- * Extension row WITH per-extension glyph (granularity=extension).
18945
- * Used by built-in `core` and any user plugin that opts in. Padding
18946
- * for {{kind}} and {{name}} is computed at render time so columns
18947
- * align inside the block.
18948
- */
18949
- detailExtensionRowGlyph: " {{glyph}} {{kind}} {{name}} v{{version}}\n",
18950
- /**
18951
- * Extension row WITHOUT per-extension glyph (granularity=bundle).
18952
- * The bundle is the only toggle; per-extension status is implicit.
19012
+ * Extension row inside the bundle detail. Every extension is
19013
+ * independently toggle-able, so every row carries its own glyph
19014
+ * (✓ / ✕). Padding for {{kind}} and {{name}} is computed at render
19015
+ * time so columns align inside the block. `{{versionSuffix}}` is
19016
+ * either ` v<x.y.z>` (user plugins) or empty (built-in bundles,
19017
+ * which inherit the CLI version and do not maintain per-extension
19018
+ * versions of their own).
18953
19019
  */
18954
- detailExtensionRowBare: " {{kind}} {{name}} v{{version}}\n",
19020
+ detailExtensionRowGlyph: " {{glyph}} {{kind}} {{name}}{{versionSuffix}}\n",
18955
19021
  detailVersionUnknown: "?",
18956
19022
  detailCompatUnknown: "?",
18957
19023
  /**
@@ -19052,26 +19118,23 @@ async function loadAll(opts) {
19052
19118
  }
19053
19119
  function builtInRows(resolveEnabled) {
19054
19120
  return sortBundlesForPresentation(builtInBundles).map((bundle) => {
19055
- const bundleEnabled = resolveEnabled(bundle.id);
19056
- const extensions = bundle.extensions.map((ext) => extensionRowFromBuiltIn(ext, bundle, bundleEnabled, resolveEnabled));
19121
+ const extensions = bundle.extensions.map((ext) => extensionRowFromBuiltIn(ext, bundle, resolveEnabled));
19057
19122
  const manifestSummary = bundle.extensions.map((ext) => `${ext.kind}:${qualifiedExtensionId(bundle.id, ext.id)}@${ext.version}`).join(", ");
19058
19123
  return {
19059
19124
  id: bundle.id,
19060
- granularity: bundle.granularity,
19061
- enabled: bundleEnabled,
19125
+ enabled: extensions.some((e) => e.enabled),
19062
19126
  description: bundle.description,
19063
19127
  extensions,
19064
19128
  manifestSummary
19065
19129
  };
19066
19130
  });
19067
19131
  }
19068
- function extensionRowFromBuiltIn(ext, bundle, bundleEnabled, resolveEnabled) {
19069
- const qualifiedEnabled = resolveEnabled(qualifiedExtensionId(bundle.id, ext.id));
19132
+ function extensionRowFromBuiltIn(ext, bundle, resolveEnabled) {
19070
19133
  const row = {
19071
19134
  id: ext.id,
19072
19135
  kind: ext.kind,
19073
19136
  version: ext.version,
19074
- enabled: bundle.granularity === "bundle" ? bundleEnabled && qualifiedEnabled : qualifiedEnabled,
19137
+ enabled: resolveEnabled(qualifiedExtensionId(bundle.id, ext.id)),
19075
19138
  description: ext.description ?? ""
19076
19139
  };
19077
19140
  if (ext.entry !== void 0) row.entry = ext.entry;
@@ -19125,14 +19188,14 @@ var PluginsListCommand = class extends SmCommand {
19125
19188
  return ExitCode.Ok;
19126
19189
  }
19127
19190
  const ansi = this.ansiFor("stdout");
19128
- this.printer.data(renderListHuman(builtIns2, plugins, ansi));
19191
+ this.printer.data(renderListHuman(builtIns2, plugins, resolveEnabled, ansi));
19129
19192
  return ExitCode.Ok;
19130
19193
  }
19131
19194
  };
19132
- function renderListHuman(builtIns2, plugins, ansi) {
19195
+ function renderListHuman(builtIns2, plugins, resolveEnabled, ansi) {
19133
19196
  const rows = [
19134
19197
  ...builtIns2.map(builtInToListRow),
19135
- ...plugins.map(pluginToListRow)
19198
+ ...plugins.map((p) => pluginToListRow(p, resolveEnabled))
19136
19199
  ];
19137
19200
  const idWidth = Math.max(...rows.map((r) => r.id.length));
19138
19201
  const countWidth = Math.max(
@@ -19163,9 +19226,9 @@ function renderListHuman(builtIns2, plugins, ansi) {
19163
19226
  return lines.join("\n") + "\n" + PLUGINS_TEXTS.listTipShow;
19164
19227
  }
19165
19228
  function builtInToListRow(b) {
19166
- const names = b.granularity === "extension" ? b.extensions.map(
19229
+ const names = b.extensions.map(
19167
19230
  (e) => e.enabled ? e.id : `${PLUGINS_TEXTS.rowGlyphOff} ${e.id}`
19168
- ) : b.extensions.map((e) => e.id);
19231
+ );
19169
19232
  return {
19170
19233
  id: b.id,
19171
19234
  enabled: b.enabled,
@@ -19173,9 +19236,14 @@ function builtInToListRow(b) {
19173
19236
  names
19174
19237
  };
19175
19238
  }
19176
- function pluginToListRow(p) {
19177
- const enabled = p.status === "enabled";
19178
- const names = p.extensions?.map((e) => sanitizeForTerminal(e.id)) ?? [];
19239
+ function pluginToListRow(p, resolveEnabled) {
19240
+ const isLoaded = p.status === "enabled";
19241
+ const extensions = p.extensions ?? [];
19242
+ const enabled = isLoaded ? extensions.length === 0 || extensions.some((e) => resolveEnabled(qualifiedExtensionId(p.id, e.id))) : false;
19243
+ const names = extensions.map((e) => {
19244
+ const safeId = sanitizeForTerminal(e.id);
19245
+ return resolveEnabled(qualifiedExtensionId(p.id, e.id)) ? safeId : `${PLUGINS_TEXTS.rowGlyphOff} ${safeId}`;
19246
+ });
19179
19247
  const reason = p.status === "enabled" ? void 0 : sanitizeForTerminal(p.reason ?? "") || void 0;
19180
19248
  return {
19181
19249
  id: sanitizeForTerminal(p.id),
@@ -19213,11 +19281,10 @@ var PluginsShowCommand = class extends SmCommand {
19213
19281
  Accepts a bundle / plugin id (\`core\`, \`claude\`, \`my-plugin\`)
19214
19282
  or a qualified extension id (\`core/<ext-id>\`,
19215
19283
  \`<plugin>/<ext-id>\`). When given a qualified id, validates the
19216
- extension exists and renders the parent bundle's detail (which
19217
- lists every extension with per-extension status for
19218
- granularity=extension bundles like \`core\`). The same id shapes
19219
- \`sm plugins enable\` and \`sm plugins disable\` accept resolve
19220
- cleanly here too.
19284
+ extension exists and renders a single-extension detail block.
19285
+ The bare form renders the parent bundle's detail with per-extension
19286
+ status. The same id shapes \`sm plugins enable\` and
19287
+ \`sm plugins disable\` accept resolve cleanly here too.
19221
19288
  `
19222
19289
  });
19223
19290
  id = Option22.String({ required: true });
@@ -19348,16 +19415,13 @@ function sortExtensionsCanonical(exts) {
19348
19415
  });
19349
19416
  }
19350
19417
  function renderBuiltInDetail(b, ansi) {
19351
- const enabled = b.enabled;
19352
- const glyph = enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
19418
+ const glyph = b.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
19353
19419
  const count = b.extensions.length;
19354
- const qualify = b.granularity === "extension";
19355
19420
  const sorted = sortExtensionsCanonical(b.extensions);
19356
19421
  const items = sorted.map((ext) => ({
19357
- glyph: b.granularity === "extension" ? ext.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff) : null,
19422
+ glyph: ext.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff),
19358
19423
  kind: ext.kind,
19359
- name: qualify ? `${b.id}/${ext.id}` : ext.id,
19360
- version: ext.version
19424
+ name: `${b.id}/${ext.id}`
19361
19425
  }));
19362
19426
  return tx(PLUGINS_TEXTS.detailHeaderBuiltIn, {
19363
19427
  glyph,
@@ -19425,15 +19489,18 @@ function renderPluginDetailFields(match) {
19425
19489
  function collectPluginExtensionItems(match, ansi) {
19426
19490
  const enabled = match.status === "enabled";
19427
19491
  if (!enabled || !match.extensions) return [];
19428
- const qualify = match.granularity === "extension";
19429
19492
  const safeBundleId = sanitizeForTerminal(match.id);
19430
19493
  const sorted = sortExtensionsCanonical(match.extensions);
19431
19494
  return sorted.map((ext) => {
19432
19495
  const safeExtId = sanitizeForTerminal(ext.id);
19433
19496
  return {
19434
- glyph: match.granularity === "extension" ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : null,
19497
+ // User plugins surfaced via `loadAll` already filter on the
19498
+ // resolver, so a reachable extension on this surface is enabled
19499
+ // by construction. The disabled path goes through the bundle
19500
+ // status header above (✕ on the row).
19501
+ glyph: ansi.green(PLUGINS_TEXTS.rowGlyphOk),
19435
19502
  kind: sanitizeForTerminal(ext.kind),
19436
- name: qualify ? `${safeBundleId}/${safeExtId}` : safeExtId,
19503
+ name: `${safeBundleId}/${safeExtId}`,
19437
19504
  version: sanitizeForTerminal(ext.version)
19438
19505
  };
19439
19506
  });
@@ -19441,29 +19508,21 @@ function collectPluginExtensionItems(match, ansi) {
19441
19508
  function renderExtensionItems(items) {
19442
19509
  if (items.length === 0) return "";
19443
19510
  const kindWidth = Math.max(...items.map((i) => i.kind.length));
19444
- const nameWidth = Math.max(...items.map((i) => i.name.length));
19511
+ const anyVersion = items.some((i) => i.version !== void 0);
19512
+ const nameWidth = anyVersion ? Math.max(...items.map((i) => i.name.length)) : 0;
19445
19513
  const out = [];
19446
19514
  for (const item of items) {
19447
19515
  const kind = item.kind.padEnd(kindWidth);
19448
- const name = item.name.padEnd(nameWidth);
19449
- if (item.glyph !== null) {
19450
- out.push(
19451
- tx(PLUGINS_TEXTS.detailExtensionRowGlyph, {
19452
- glyph: item.glyph,
19453
- kind,
19454
- name,
19455
- version: item.version
19456
- })
19457
- );
19458
- } else {
19459
- out.push(
19460
- tx(PLUGINS_TEXTS.detailExtensionRowBare, {
19461
- kind,
19462
- name,
19463
- version: item.version
19464
- })
19465
- );
19466
- }
19516
+ const name = anyVersion ? item.name.padEnd(nameWidth) : item.name;
19517
+ const versionSuffix = item.version ? ` v${item.version}` : "";
19518
+ out.push(
19519
+ tx(PLUGINS_TEXTS.detailExtensionRowGlyph, {
19520
+ glyph: item.glyph,
19521
+ kind,
19522
+ name,
19523
+ versionSuffix
19524
+ })
19525
+ );
19467
19526
  }
19468
19527
  return out.join("");
19469
19528
  }
@@ -19474,7 +19533,7 @@ function renderBuiltInExtensionDetail(bundleId, ext, ansi) {
19474
19533
  qualifiedId: sanitizeForTerminal(`${bundleId}/${ext.id}`),
19475
19534
  source: ansi.dim(PLUGINS_TEXTS.sourceBuiltIn)
19476
19535
  });
19477
- const meta = { kind: ext.kind, version: ext.version };
19536
+ const meta = { kind: ext.kind };
19478
19537
  if (ext.description) meta.description = ext.description;
19479
19538
  if (ext.entry !== void 0) meta.entry = ext.entry;
19480
19539
  return header + "\n" + renderExtensionFields(meta);
@@ -19513,7 +19572,12 @@ function readInstanceMeta(instance) {
19513
19572
  function renderExtensionFields(meta) {
19514
19573
  const fields = [];
19515
19574
  fields.push({ label: PLUGINS_TEXTS.detailFieldKind, value: sanitizeForTerminal(meta.kind) });
19516
- fields.push({ label: PLUGINS_TEXTS.detailFieldVersion, value: sanitizeForTerminal(meta.version) });
19575
+ if (meta.version) {
19576
+ fields.push({
19577
+ label: PLUGINS_TEXTS.detailFieldVersion,
19578
+ value: sanitizeForTerminal(meta.version)
19579
+ });
19580
+ }
19517
19581
  if (meta.stability) {
19518
19582
  fields.push({
19519
19583
  label: PLUGINS_TEXTS.detailFieldStability,
@@ -19567,7 +19631,7 @@ var PluginsDoctorCommand = class extends SmCommand {
19567
19631
  const plugins = await loadAll({ pluginDir: this.pluginDir });
19568
19632
  const resolveEnabled = await buildResolver();
19569
19633
  const builtIns2 = builtInRows(resolveEnabled);
19570
- const counts = countByStatus(builtIns2, plugins);
19634
+ const counts = countByStatus(builtIns2, plugins, resolveEnabled);
19571
19635
  const knownKinds = collectKnownKinds(plugins);
19572
19636
  const applicableKindWarnings = collectApplicableKindWarnings(plugins, knownKinds);
19573
19637
  const unknownSlotWarnings = collectUnknownSlotWarnings(plugins, KNOWN_SLOT_NAMES);
@@ -19602,6 +19666,7 @@ var PluginsDoctorCommand = class extends SmCommand {
19602
19666
  this.printer.data(
19603
19667
  tx(PLUGINS_TEXTS.doctorSummary, {
19604
19668
  enabled,
19669
+ enabledPlural: enabled === 1 ? "" : "s",
19605
19670
  issues: badCount,
19606
19671
  issuesPlural: badCount === 1 ? "" : "s",
19607
19672
  warnings,
@@ -19699,7 +19764,7 @@ var PluginsDoctorCommand = class extends SmCommand {
19699
19764
  }
19700
19765
  }
19701
19766
  };
19702
- function countByStatus(builtIns2, plugins) {
19767
+ function countByStatus(builtIns2, plugins, resolveEnabled) {
19703
19768
  const counts = {
19704
19769
  enabled: 0,
19705
19770
  disabled: 0,
@@ -19710,15 +19775,20 @@ function countByStatus(builtIns2, plugins) {
19710
19775
  "id-collision": 0
19711
19776
  };
19712
19777
  for (const b of builtIns2) {
19713
- if (b.granularity === "bundle") {
19714
- counts[b.enabled ? "enabled" : "disabled"]++;
19715
- } else {
19716
- for (const ext of b.extensions) {
19717
- counts[ext.enabled ? "enabled" : "disabled"]++;
19718
- }
19778
+ for (const ext of b.extensions) {
19779
+ counts[ext.enabled ? "enabled" : "disabled"]++;
19780
+ }
19781
+ }
19782
+ for (const p of plugins) {
19783
+ if (p.status !== "enabled" || !p.extensions) {
19784
+ counts[p.status]++;
19785
+ continue;
19786
+ }
19787
+ for (const ext of p.extensions) {
19788
+ const enabled = resolveEnabled(`${p.id}/${ext.id}`);
19789
+ counts[enabled ? "enabled" : "disabled"]++;
19719
19790
  }
19720
19791
  }
19721
- for (const p of plugins) counts[p.status]++;
19722
19792
  return counts;
19723
19793
  }
19724
19794
  function forEachProviderInstance(plugins, callback) {
@@ -19911,6 +19981,9 @@ function buildDoctorJsonEnvelope(args2) {
19911
19981
  import { Command as Command25, Option as Option24 } from "clipanion";
19912
19982
  var TogglePluginsBase = class extends SmCommand {
19913
19983
  all = Option24.Boolean("--all", false);
19984
+ yes = Option24.Boolean("--yes,-y", false, {
19985
+ description: "Skip the interactive confirm when a bare bundle id (or --all) fans the toggle out across multiple extensions."
19986
+ });
19914
19987
  ids = Option24.Rest({ name: "ids" });
19915
19988
  async toggle(enabled) {
19916
19989
  const verb = enabled ? "enable" : "disable";
@@ -19919,14 +19992,17 @@ var TogglePluginsBase = class extends SmCommand {
19919
19992
  if (argError !== null) return argError;
19920
19993
  const plugins = await loadAll({ pluginDir: void 0 });
19921
19994
  const catalogue = bundleCatalogue(plugins);
19922
- const targetsResult = this.#pickTargets(catalogue, verb, stderrAnsi);
19995
+ const targetsResult = this.#pickTargets(catalogue, stderrAnsi);
19923
19996
  if (typeof targetsResult === "number") return targetsResult;
19924
19997
  let targets = targetsResult;
19998
+ const macroOk = await this.#confirmMacroIfNeeded(targets, verb, stderrAnsi);
19999
+ if (!macroOk) return ExitCode.Error;
19925
20000
  const lockError = this.#applyLockGate(targets, stderrAnsi);
19926
20001
  if (typeof lockError === "number") return lockError;
19927
20002
  targets = lockError;
19928
- await this.#persistTargets(targets, enabled);
19929
- this.#renderSuccess(targets, enabled);
20003
+ const keys = expandToKeys(targets);
20004
+ await this.#persistKeys(keys, enabled);
20005
+ this.#renderSuccess(keys, enabled);
19930
20006
  return ExitCode.Ok;
19931
20007
  }
19932
20008
  /**
@@ -19959,87 +20035,166 @@ var TogglePluginsBase = class extends SmCommand {
19959
20035
  /**
19960
20036
  * Resolve `<id>...` against the catalogue or fan out via `--all`.
19961
20037
  * Returns the target list on success, or the exit code on a
19962
- * directed-error path (unknown id, granularity mismatch).
20038
+ * directed-error path (unknown id, malformed qualified id).
19963
20039
  *
19964
- * `--all` is a macro on bundle ids: every plugin / bundle the user
19965
- * can see. We deliberately do NOT expand to qualified
19966
- * <bundle>/<ext> keys, that would silently flip a granularity
19967
- * policy. For granularity=extension bundles the user already hits
19968
- * the directed error message when they try the bundle id directly,
19969
- * so `--all` skips them here too and the real "disable every core
19970
- * extension" intent is served by `--no-built-ins` on `sm scan`.
19971
- *
19972
- * Variadic mode is all-or-nothing: the first bad id aborts the
19973
- * batch before any DB write, so the user never lands in a partial
19974
- * state. Repeated ids in the same call are deduped.
20040
+ * Repeated ids in the same call are deduped at the target level
20041
+ * (`origin === 'bare'` and `origin === 'qualified'` rows stay
20042
+ * distinct so the macro-confirm path can address each correctly).
20043
+ * The first unknown id aborts the batch before any DB write so the
20044
+ * user never lands in a partial state.
19975
20045
  */
19976
- #pickTargets(catalogue, verb, ansi) {
20046
+ #pickTargets(catalogue, ansi) {
19977
20047
  if (this.all) {
19978
- return catalogue.filter((b) => b.granularity === "bundle").map((b) => b.id);
20048
+ return catalogue.map((b) => ({
20049
+ origin: "bare",
20050
+ bundleId: b.id,
20051
+ keys: b.extensionIds.map((extId) => qualifiedExtensionId(b.id, extId))
20052
+ }));
19979
20053
  }
19980
- const keys = [];
20054
+ const out = [];
20055
+ const seen = /* @__PURE__ */ new Set();
19981
20056
  for (const rawId of this.ids) {
19982
- const resolved = resolveToggleTarget(rawId, catalogue, verb, ansi);
20057
+ const resolved = resolveToggleTarget(rawId, catalogue, ansi);
19983
20058
  if ("error" in resolved) {
19984
20059
  this.printer.error(tx(PLUGINS_TEXTS.toggleResolveError, { error: resolved.error }));
19985
20060
  return ExitCode.NotFound;
19986
20061
  }
19987
- keys.push(resolved.key);
20062
+ const novelKeys = resolved.keys.filter((k) => !seen.has(k));
20063
+ if (novelKeys.length === 0) continue;
20064
+ for (const k of novelKeys) seen.add(k);
20065
+ out.push({ ...resolved, keys: novelKeys });
19988
20066
  }
19989
- return [...new Set(keys)];
20067
+ return out;
20068
+ }
20069
+ /**
20070
+ * Macro gate: when the request would fan a single user input out
20071
+ * across more than one extension (either `--all` or a bare bundle
20072
+ * id whose bundle holds ≥2 extensions), confirm the intent.
20073
+ *
20074
+ * Resolution order:
20075
+ * 1. `--yes` flag: skip the prompt entirely.
20076
+ * 2. TTY stdin: render the list + ask interactively (`[y/N]`).
20077
+ * 3. Non-TTY (CI / pipe / agent harness): refuse with a directed
20078
+ * message that names the extensions and points at `--yes`.
20079
+ *
20080
+ * Returns `true` when the verb should proceed, `false` when it
20081
+ * should abort. Single-extension targets (bare bundle id mapping to
20082
+ * one child, or qualified ids) skip the gate uniformly.
20083
+ */
20084
+ // Cyclomatic count comes from the three-stage gate (--yes shortcut,
20085
+ // TTY interactive path, non-TTY rejection) folded over the targets
20086
+ // loop. Splitting them scatters the contract without making the
20087
+ // algorithm clearer.
20088
+ // eslint-disable-next-line complexity
20089
+ async #confirmMacroIfNeeded(targets, verb, ansi) {
20090
+ const macroTargets = targets.filter((t) => requiresMacroConfirm(t));
20091
+ if (macroTargets.length === 0) return true;
20092
+ if (this.yes) return true;
20093
+ const isTty = Boolean(this.context.stdin && "isTTY" in this.context.stdin && this.context.stdin.isTTY);
20094
+ for (const target of macroTargets) {
20095
+ const bundleLabel = target.origin === "bare" ? target.bundleId ?? "--all" : "--all";
20096
+ this.printer.info(
20097
+ tx(PLUGINS_TEXTS.bundleMacroHeader, {
20098
+ verb,
20099
+ bundleId: sanitizeForTerminal(bundleLabel),
20100
+ count: target.keys.length
20101
+ })
20102
+ );
20103
+ for (const key of target.keys) {
20104
+ this.printer.info(tx(PLUGINS_TEXTS.bundleMacroRow, { id: sanitizeForTerminal(key) }));
20105
+ }
20106
+ }
20107
+ if (!isTty) {
20108
+ this.printer.error(
20109
+ tx(PLUGINS_TEXTS.bundleMacroRequiresYes, {
20110
+ glyph: ansi.red("\u2715"),
20111
+ verb,
20112
+ hint: ansi.dim(PLUGINS_TEXTS.bundleMacroRequiresYesHint)
20113
+ })
20114
+ );
20115
+ return false;
20116
+ }
20117
+ const ok = await confirm(
20118
+ tx(PLUGINS_TEXTS.bundleMacroConfirmPrompt, { verb }),
20119
+ { stdin: this.context.stdin, stderr: this.context.stderr }
20120
+ );
20121
+ if (!ok) {
20122
+ this.printer.info(PLUGINS_TEXTS.bundleMacroCancelled);
20123
+ }
20124
+ return ok;
19990
20125
  }
19991
20126
  /**
19992
20127
  * Host lock, see `src/kernel/config/locked-plugins.ts`. Bulk modes
19993
- * (`--all` or an explicit batch of >1 ids) silently skip locked
19994
- * targets so the user can still toggle the rest. Single-id mode
19995
- * surfaces a directed exit-5 message so the user knows their one
19996
- * intended target was refused.
20128
+ * (`--all`, an explicit batch of >1 targets, or a macro expansion
20129
+ * with >1 keys) silently skip locked extensions so the user can
20130
+ * still toggle the rest. Single-extension mode surfaces a directed
20131
+ * exit-5 message so the user knows their one intended target was
20132
+ * refused.
19997
20133
  */
19998
20134
  #applyLockGate(targets, ansi) {
19999
- if (this.all || this.ids.length > 1) return targets.filter((id) => !isPluginLocked(id));
20000
- const lockedHit = targets.find((id) => isPluginLocked(id));
20001
- if (!lockedHit) return targets;
20135
+ const totalKeys = targets.reduce((acc, t) => acc + t.keys.length, 0);
20136
+ const bulk = this.all || this.ids.length > 1 || totalKeys > 1;
20137
+ if (bulk) {
20138
+ return targets.map((t) => ({ ...t, keys: t.keys.filter((k) => !isPluginLocked(k)) }));
20139
+ }
20140
+ const onlyKey = targets[0]?.keys[0];
20141
+ if (!onlyKey || !isPluginLocked(onlyKey)) return targets;
20002
20142
  this.printer.error(
20003
20143
  tx(PLUGINS_TEXTS.pluginLocked, {
20004
20144
  glyph: ansi.red("\u2715"),
20005
- id: sanitizeForTerminal(lockedHit),
20145
+ id: sanitizeForTerminal(onlyKey),
20006
20146
  hint: ansi.dim(PLUGINS_TEXTS.pluginLockedHint)
20007
20147
  })
20008
20148
  );
20009
20149
  return ExitCode.NotFound;
20010
20150
  }
20011
20151
  /**
20012
- * Persist the toggle in `config_plugins`. On disable, also purge
20013
- * the plugin's `scan_contributions` rows immediately (matches the
20014
- * BFF route, see `server/routes/plugins.ts:applyChangeToAdapter`).
20015
- * `targets` carries either a bare bundle id (e.g. `claude`) or a
20016
- * qualified `<bundle>/<ext>` (e.g. `core/slash-command`); the split mirrors
20017
- * how the catalog sweep groups rows.
20152
+ * Persist every qualified id in `config_plugins`. On disable, also
20153
+ * purge the plugin's `scan_contributions` rows immediately (matches
20154
+ * the BFF route, see `server/routes/plugins.ts:applyChangeToAdapter`).
20155
+ * Every key is `<bundle>/<ext>` shape so the contribution purge can
20156
+ * split into `(pluginId, extensionId)` cleanly.
20018
20157
  */
20019
- async #persistTargets(targets, enabled) {
20158
+ async #persistKeys(keys, enabled) {
20020
20159
  const ctx = defaultRuntimeContext();
20021
20160
  const dbPath = resolveDbPath({ db: void 0, cwd: ctx.cwd });
20022
20161
  await withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
20023
- for (const id of targets) {
20162
+ for (const id of keys) {
20024
20163
  await adapter.pluginConfig.set(id, enabled);
20025
20164
  if (!enabled) await purgeContributionsFor(adapter, id);
20026
20165
  }
20027
20166
  });
20028
20167
  }
20029
- #renderSuccess(targets, enabled) {
20168
+ #renderSuccess(keys, enabled) {
20030
20169
  const verbPast = enabled ? "enabled" : "disabled";
20031
- if (targets.length === 1) {
20032
- this.printer.data(tx(PLUGINS_TEXTS.toggleAppliedSingle, { verbPast, id: targets[0] }));
20170
+ if (keys.length === 1) {
20171
+ this.printer.data(tx(PLUGINS_TEXTS.toggleAppliedSingle, { verbPast, id: keys[0] }));
20033
20172
  } else {
20034
20173
  this.printer.data(
20035
- tx(PLUGINS_TEXTS.toggleAppliedManyHeader, { verbPast, count: targets.length })
20174
+ tx(PLUGINS_TEXTS.toggleAppliedManyHeader, { verbPast, count: keys.length })
20036
20175
  );
20037
- for (const id of targets) {
20176
+ for (const id of keys) {
20038
20177
  this.printer.data(tx(PLUGINS_TEXTS.toggleAppliedManyRow, { id }));
20039
20178
  }
20040
20179
  }
20041
20180
  }
20042
20181
  };
20182
+ function requiresMacroConfirm(target) {
20183
+ if (target.origin !== "bare") return false;
20184
+ return target.keys.length >= 2;
20185
+ }
20186
+ function expandToKeys(targets) {
20187
+ const out = [];
20188
+ const seen = /* @__PURE__ */ new Set();
20189
+ for (const t of targets) {
20190
+ for (const k of t.keys) {
20191
+ if (seen.has(k)) continue;
20192
+ seen.add(k);
20193
+ out.push(k);
20194
+ }
20195
+ }
20196
+ return out;
20197
+ }
20043
20198
  async function purgeContributionsFor(adapter, id) {
20044
20199
  const slash = id.indexOf("/");
20045
20200
  if (slash < 0) {
@@ -20052,25 +20207,24 @@ var PluginsEnableCommand = class extends TogglePluginsBase {
20052
20207
  static paths = [["plugins", "enable"]];
20053
20208
  static usage = Command25.Usage({
20054
20209
  category: "Plugins",
20055
- description: "Enable one or more plugins (or --all). Persists in config_plugins.",
20210
+ description: "Enable one or more extensions (or --all). Persists in config_plugins.",
20056
20211
  details: `
20057
- Writes a row to config_plugins with enabled=1 per id. Takes
20058
- precedence over the team-shared baseline at
20212
+ Writes a row to config_plugins with enabled=1 per qualified
20213
+ extension id. Takes precedence over the team-shared baseline at
20059
20214
  settings.json#/plugins/<id>/enabled. Use sm plugins disable to
20060
20215
  flip; sm config reset plugins.<id>.enabled drops the settings.json
20061
20216
  baseline.
20062
20217
 
20063
- Accepts one or more ids in one call, e.g.
20064
- 'sm plugins enable claude antigravity openai'. Batches are
20065
- all-or-nothing: a single unknown / mismatched id aborts before
20066
- any write. Repeated ids are deduped. Locked plugins inside a
20067
- batch are silently skipped.
20218
+ Accepts qualified ids (\`claude/at-directive\`) and bare bundle
20219
+ ids (\`claude\`, which fans the toggle out across every extension
20220
+ inside the bundle). Multi-extension bundles need --yes (or an
20221
+ interactive TTY confirm) to avoid flipping 27 core extensions by
20222
+ accident. Single-extension bundles (openai, agent-skills,
20223
+ antigravity) apply without prompting.
20068
20224
 
20069
- Granularity: a bundle-granularity plugin (default for user plugins,
20070
- and the built-in 'claude' bundle) accepts only the bundle id. An
20071
- extension-granularity plugin (the built-in 'core' bundle) accepts
20072
- only qualified ids '<bundle>/<ext-id>'. Mismatches are rejected
20073
- with directed guidance.
20225
+ Batches are all-or-nothing: a single unknown id aborts before
20226
+ any write. Repeated ids are deduped. Locked extensions inside a
20227
+ batch are silently skipped.
20074
20228
  `
20075
20229
  });
20076
20230
  async run() {
@@ -20081,24 +20235,23 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
20081
20235
  static paths = [["plugins", "disable"]];
20082
20236
  static usage = Command25.Usage({
20083
20237
  category: "Plugins",
20084
- description: "Disable one or more plugins (or --all). Persists in config_plugins; does not delete files.",
20238
+ description: "Disable one or more extensions (or --all). Persists in config_plugins; does not delete files.",
20085
20239
  details: `
20086
- Writes a row to config_plugins with enabled=0 per id. Discovery
20087
- still surfaces the plugin in sm plugins list, but with
20088
- status=disabled; its extensions are not imported and the kernel
20089
- will not run them.
20090
-
20091
- Accepts one or more ids in one call, e.g.
20092
- 'sm plugins disable antigravity openai agent-skills'. Batches are
20093
- all-or-nothing: a single unknown / mismatched id aborts before
20094
- any write. Repeated ids are deduped. Locked plugins inside a
20240
+ Writes a row to config_plugins with enabled=0 per qualified
20241
+ extension id. Discovery still surfaces the plugin in
20242
+ sm plugins list, but with status=disabled; the kernel will not
20243
+ run any of its disabled extensions.
20244
+
20245
+ Accepts qualified ids (\`core/markdown-link\`) and bare bundle
20246
+ ids (\`core\`, which fans the toggle out across every extension
20247
+ inside the bundle). Multi-extension bundles need --yes (or an
20248
+ interactive TTY confirm) to avoid flipping 27 core extensions by
20249
+ accident. Single-extension bundles (openai, agent-skills,
20250
+ antigravity) apply without prompting.
20251
+
20252
+ Batches are all-or-nothing: a single unknown id aborts before
20253
+ any write. Repeated ids are deduped. Locked extensions inside a
20095
20254
  batch are silently skipped.
20096
-
20097
- Granularity: a bundle-granularity plugin (default for user plugins,
20098
- and the built-in 'claude' bundle) accepts only the bundle id. An
20099
- extension-granularity plugin (the built-in 'core' bundle) accepts
20100
- only qualified ids '<bundle>/<ext-id>'. Mismatches are rejected
20101
- with directed guidance.
20102
20255
  `
20103
20256
  });
20104
20257
  async run() {
@@ -20110,23 +20263,21 @@ function bundleCatalogue(plugins) {
20110
20263
  for (const bundle of builtInBundles) {
20111
20264
  out.push({
20112
20265
  id: bundle.id,
20113
- granularity: bundle.granularity,
20114
20266
  extensionIds: bundle.extensions.map((e) => e.id)
20115
20267
  });
20116
20268
  }
20117
20269
  for (const p of plugins) {
20118
20270
  out.push({
20119
20271
  id: p.id,
20120
- granularity: p.granularity ?? "bundle",
20121
20272
  extensionIds: p.extensions?.map((e) => e.id) ?? []
20122
20273
  });
20123
20274
  }
20124
20275
  return out;
20125
20276
  }
20126
- function resolveToggleTarget(id, catalogue, verb, ansi) {
20127
- return id.includes("/") ? resolveQualifiedToggle(id, catalogue, verb, ansi) : resolveBareToggle(id, catalogue, verb, ansi);
20277
+ function resolveToggleTarget(id, catalogue, ansi) {
20278
+ return id.includes("/") ? resolveQualifiedToggle(id, catalogue, ansi) : resolveBareToggle(id, catalogue);
20128
20279
  }
20129
- function resolveQualifiedToggle(id, catalogue, verb, ansi) {
20280
+ function resolveQualifiedToggle(id, catalogue, ansi) {
20130
20281
  const errGlyph = ansi.red("\u2715");
20131
20282
  const [bundleId, extId, ...rest] = id.split("/");
20132
20283
  if (!bundleId || !extId || rest.length > 0) {
@@ -20148,17 +20299,6 @@ function resolveQualifiedToggle(id, catalogue, verb, ansi) {
20148
20299
  })
20149
20300
  };
20150
20301
  }
20151
- if (bundle.granularity === "bundle") {
20152
- return {
20153
- error: tx(PLUGINS_TEXTS.granularityBundleRejectsQualified, {
20154
- glyph: errGlyph,
20155
- bundleId: sanitizeForTerminal(bundleId),
20156
- extId: sanitizeForTerminal(extId),
20157
- verb,
20158
- hint: ansi.dim(PLUGINS_TEXTS.granularityBundleRejectsQualifiedHint)
20159
- })
20160
- };
20161
- }
20162
20302
  if (!bundle.extensionIds.includes(extId)) {
20163
20303
  return {
20164
20304
  error: tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
@@ -20170,31 +20310,27 @@ function resolveQualifiedToggle(id, catalogue, verb, ansi) {
20170
20310
  })
20171
20311
  };
20172
20312
  }
20173
- return { key: qualifiedExtensionId(bundleId, extId) };
20313
+ return {
20314
+ origin: "qualified",
20315
+ keys: [qualifiedExtensionId(bundleId, extId)]
20316
+ };
20174
20317
  }
20175
- function resolveBareToggle(id, catalogue, verb, ansi) {
20176
- const errGlyph = ansi.red("\u2715");
20318
+ function resolveBareToggle(id, catalogue) {
20177
20319
  const bundle = catalogue.find((b) => b.id === id);
20178
20320
  if (!bundle) {
20179
20321
  return {
20180
20322
  error: tx(PLUGINS_TEXTS.pluginNotFound, {
20181
- glyph: errGlyph,
20323
+ glyph: "\u2715",
20182
20324
  id: sanitizeForTerminal(id),
20183
- hint: ansi.dim(PLUGINS_TEXTS.pluginNotFoundHint)
20184
- })
20185
- };
20186
- }
20187
- if (bundle.granularity === "extension") {
20188
- return {
20189
- error: tx(PLUGINS_TEXTS.granularityExtensionRejectsBundleId, {
20190
- glyph: errGlyph,
20191
- bundleId: sanitizeForTerminal(id),
20192
- verb,
20193
- hint: ansi.dim(PLUGINS_TEXTS.granularityExtensionRejectsBundleIdHint)
20325
+ hint: PLUGINS_TEXTS.pluginNotFoundHint
20194
20326
  })
20195
20327
  };
20196
20328
  }
20197
- return { key: bundle.id };
20329
+ return {
20330
+ origin: "bare",
20331
+ bundleId: bundle.id,
20332
+ keys: bundle.extensionIds.map((extId) => qualifiedExtensionId(bundle.id, extId))
20333
+ };
20198
20334
  }
20199
20335
 
20200
20336
  // cli/commands/plugins/create.ts
@@ -20245,7 +20381,6 @@ var PluginsCreateCommand = class extends SmCommand {
20245
20381
  version: "0.1.0",
20246
20382
  specCompat: `^${specVersion}`,
20247
20383
  catalogCompat: "^1.0.0",
20248
- granularity: "bundle",
20249
20384
  description: "Generated by `sm plugins create`. Edit to taste.",
20250
20385
  settings: {
20251
20386
  keywords: {
@@ -20361,7 +20496,7 @@ var VIEW_SLOTS_CATALOG = [
20361
20496
  { id: "card.subtitle.left", summary: "Single non-negative integer in the card subtitle row." },
20362
20497
  { id: "card.footer.left", summary: "Counter chip in the left footer of the card." },
20363
20498
  { id: "card.footer.right", summary: "Counter chip in the right footer of the card." },
20364
- { id: "graph.node.alert", summary: "Corner badge decoration on the graph node (alert / status)." },
20499
+ { id: "graph.node.alert", summary: 'Reserved corner badge on the graph node, special-case signals only. No core analyzer emits here; routine "this node has a problem" findings belong in `card.footer.right`.' },
20365
20500
  { id: "inspector.header.badge.counter", summary: "Counter chip in the inspector header badge cluster." },
20366
20501
  { id: "inspector.header.badge.tag", summary: "Qualitative tag chip in the inspector header badge cluster." },
20367
20502
  { id: "inspector.body.panel.breakdown", summary: "Top-N labeled values rendered as a bar chart in the inspector body." },
@@ -21659,13 +21794,12 @@ var ScanCommand = class extends SmCommand {
21659
21794
  const ansi = this.ansiFor("stdout");
21660
21795
  const cwd = defaultRuntimeContext().cwd;
21661
21796
  const hasErrors = exitCode2 === ExitCode.Issues;
21662
- const issuesCount = result.stats.issuesCount;
21663
- const glyph = hasErrors ? ansi.red("\u2715") : ansi.green("\u2713");
21797
+ const severityCounts = countBySeverity(result.issues);
21798
+ const glyph = hasErrors ? " " : ansi.green("\u2713");
21664
21799
  const counts = formatScanCounts({
21665
21800
  nodes: result.stats.nodesCount,
21666
21801
  links: result.stats.linksCount,
21667
- issues: issuesCount,
21668
- hasErrors,
21802
+ severities: severityCounts,
21669
21803
  ansi
21670
21804
  });
21671
21805
  const duration = ansi.dim(`in ${result.stats.durationMs}ms`);
@@ -21712,11 +21846,49 @@ var ScanCommand = class extends SmCommand {
21712
21846
  return exitCode2;
21713
21847
  }
21714
21848
  };
21849
+ function countBySeverity(issues) {
21850
+ const buckets = {
21851
+ error: /* @__PURE__ */ new Set(),
21852
+ warn: /* @__PURE__ */ new Set(),
21853
+ info: /* @__PURE__ */ new Set()
21854
+ };
21855
+ for (const i of issues) {
21856
+ const tier = i.severity;
21857
+ const bucket = buckets[tier];
21858
+ if (!bucket) continue;
21859
+ fillSeverityBucket(bucket, i.nodeIds);
21860
+ }
21861
+ return { errors: buckets.error.size, warns: buckets.warn.size, info: buckets.info.size };
21862
+ }
21863
+ function fillSeverityBucket(bucket, nodeIds) {
21864
+ const ids = nodeIds ?? [];
21865
+ if (ids.length === 0) {
21866
+ bucket.add("");
21867
+ return;
21868
+ }
21869
+ for (const id of ids) bucket.add(id);
21870
+ }
21715
21871
  function formatScanCounts(opts) {
21716
- const { nodes, links, issues, hasErrors, ansi } = opts;
21717
- const issuesText = `${issues} ${plural(issues, "issue")}`;
21718
- const issuesColored = issues === 0 ? ansi.dim(issuesText) : hasErrors ? ansi.red(issuesText) : ansi.yellow(issuesText);
21719
- return `${nodes} ${plural(nodes, "node")} \xB7 ${links} ${plural(links, "link")} \xB7 ${issuesColored}`;
21872
+ const { nodes, links, severities, ansi } = opts;
21873
+ const parts = [
21874
+ `${nodes} ${plural(nodes, "node")}`,
21875
+ `${links} ${plural(links, "link")}`
21876
+ ];
21877
+ const total = severities.errors + severities.warns + severities.info;
21878
+ if (total === 0) {
21879
+ parts.push(ansi.dim("0 issues"));
21880
+ } else {
21881
+ if (severities.errors > 0) {
21882
+ parts.push(ansi.red(`${severities.errors} ${plural(severities.errors, "error")}`));
21883
+ }
21884
+ if (severities.warns > 0) {
21885
+ parts.push(ansi.yellow(`${severities.warns} ${plural(severities.warns, "warning")}`));
21886
+ }
21887
+ if (severities.info > 0) {
21888
+ parts.push(ansi.dim(`${severities.info} info`));
21889
+ }
21890
+ }
21891
+ return parts.join(" \xB7 ");
21720
21892
  }
21721
21893
  function plural(count, word) {
21722
21894
  return count === 1 ? word : `${word}s`;
@@ -22199,10 +22371,10 @@ var SERVER_TEXTS = {
22199
22371
  pluginsBodyNotJson: "Request body must be valid JSON.",
22200
22372
  pluginsBodyNotObject: "Request body must be a JSON object.",
22201
22373
  pluginsEnabledRequired: "`enabled` is required and must be a boolean.",
22202
- // 400, granularity mismatch. Two flavours so the message is useful
22203
- // when the operator hits the wrong route by hand.
22204
- pluginsGranularityExtensionExpected: 'Plugin "{{id}}" has granularity:"extension"; toggle individual extensions via PATCH /api/plugins/{{id}}/extensions/<extensionId>.',
22205
- pluginsGranularityBundleExpected: 'Plugin "{{id}}" has granularity:"bundle"; toggle the whole bundle via PATCH /api/plugins/{{id}}.',
22374
+ // 400, cascade route rejects qualified ids: the bare-id PATCH is the
22375
+ // bundle macro endpoint. Anything containing `/` needs the dedicated
22376
+ // per-extension route below.
22377
+ pluginsCascadeRouteQualifiedRejected: 'Plugin id "{{id}}" contains "/"; toggle individual extensions via PATCH /api/plugins/<bundle>/extensions/<extensionId>.',
22206
22378
  // 404, unknown plugin / extension.
22207
22379
  pluginsUnknown: 'No plugin with id "{{id}}".',
22208
22380
  pluginsExtensionUnknown: 'Plugin "{{bundleId}}" has no extension named "{{extensionId}}".',
@@ -23209,7 +23381,7 @@ function registerPluginsRoute(app, deps) {
23209
23381
  const id = c.req.param("id");
23210
23382
  if (id.includes("/")) {
23211
23383
  throw new HTTPException8(400, {
23212
- message: tx(SERVER_TEXTS.pluginsGranularityExtensionExpected, { id })
23384
+ message: tx(SERVER_TEXTS.pluginsCascadeRouteQualifiedRejected, { id })
23213
23385
  });
23214
23386
  }
23215
23387
  const handle = findHandle(id, deps);
@@ -23218,18 +23390,15 @@ function registerPluginsRoute(app, deps) {
23218
23390
  message: tx(SERVER_TEXTS.pluginsUnknown, { id })
23219
23391
  });
23220
23392
  }
23221
- if (granularityOf(handle) !== "bundle") {
23222
- throw new HTTPException8(400, {
23223
- message: tx(SERVER_TEXTS.pluginsGranularityExtensionExpected, { id })
23224
- });
23225
- }
23226
23393
  if (isPluginLocked(id)) {
23227
23394
  throw new HTTPException8(403, {
23228
23395
  message: tx(SERVER_TEXTS.pluginsLocked, { id })
23229
23396
  });
23230
23397
  }
23231
23398
  const body = await parsePatchBody(c.req.raw);
23232
- return await persistAndProject(c, deps, id, body.enabled);
23399
+ const childIds = bundleExtensionIds(handle).map((extId) => qualifiedExtensionId(id, extId));
23400
+ const writable = childIds.filter((q) => !isPluginLocked(q));
23401
+ return await persistManyAndProject(c, deps, writable, body.enabled);
23233
23402
  });
23234
23403
  app.patch("/api/plugins/:bundleId/extensions/:extensionId", async (c) => {
23235
23404
  const bundleId = c.req.param("bundleId");
@@ -23278,7 +23447,6 @@ function listItems(deps, resolveEnabled) {
23278
23447
  }
23279
23448
  function buildBuiltInItems(resolveEnabled) {
23280
23449
  return sortBundlesForPresentation(builtInBundles).map((bundle) => {
23281
- const bundleEnabled = resolveEnabled(bundle.id);
23282
23450
  const bundleLocked = isPluginLocked(bundle.id);
23283
23451
  const extensions = bundle.extensions.map((ext) => {
23284
23452
  const qualified = qualifiedExtensionId(bundle.id, ext.id);
@@ -23292,6 +23460,7 @@ function buildBuiltInItems(resolveEnabled) {
23292
23460
  ...extLocked ? { locked: true } : {}
23293
23461
  };
23294
23462
  });
23463
+ const bundleEnabled = extensions.some((e) => e.enabled);
23295
23464
  return {
23296
23465
  id: bundle.id,
23297
23466
  version: firstVersion(bundle.extensions),
@@ -23299,7 +23468,6 @@ function buildBuiltInItems(resolveEnabled) {
23299
23468
  status: bundleEnabled ? "enabled" : "disabled",
23300
23469
  reason: null,
23301
23470
  source: "built-in",
23302
- granularity: bundle.granularity,
23303
23471
  description: bundle.description,
23304
23472
  ...extensions.length > 0 ? { extensions } : {},
23305
23473
  ...bundleLocked ? { locked: true } : {}
@@ -23310,9 +23478,8 @@ function buildDiscoveredItems(discovered, deps, resolveEnabled) {
23310
23478
  return discovered.map((plugin) => buildDiscoveredItem(plugin, deps, resolveEnabled));
23311
23479
  }
23312
23480
  function buildDiscoveredItem(plugin, deps, resolveEnabled) {
23313
- const granularity = plugin.granularity ?? "bundle";
23314
23481
  const bundleLocked = isPluginLocked(plugin.id);
23315
- const extensions = projectExtensionRows(plugin, granularity, resolveEnabled, bundleLocked);
23482
+ const extensions = projectExtensionRows(plugin, resolveEnabled, bundleLocked);
23316
23483
  const optional = optionalDiscoveredFields(plugin, extensions);
23317
23484
  return {
23318
23485
  id: plugin.id,
@@ -23321,7 +23488,6 @@ function buildDiscoveredItem(plugin, deps, resolveEnabled) {
23321
23488
  status: projectStatus(plugin, resolveEnabled),
23322
23489
  reason: plugin.reason ?? null,
23323
23490
  source: classifyPluginSource(plugin.path, deps),
23324
- granularity,
23325
23491
  ...optional,
23326
23492
  ...bundleLocked ? { locked: true } : {},
23327
23493
  ...plugin.status === "disabled" ? { startsAsDisabled: true } : {}
@@ -23334,7 +23500,7 @@ function optionalDiscoveredFields(plugin, extensions) {
23334
23500
  if (extensions) out.extensions = extensions;
23335
23501
  return out;
23336
23502
  }
23337
- function projectExtensionRows(plugin, _granularity, resolveEnabled, bundleLocked) {
23503
+ function projectExtensionRows(plugin, resolveEnabled, bundleLocked) {
23338
23504
  if (!plugin.extensions || plugin.extensions.length === 0) return void 0;
23339
23505
  return plugin.extensions.map((ext) => {
23340
23506
  const description = readInstanceDescription(ext.instance);
@@ -23383,6 +23549,18 @@ async function persistAndProject(c, deps, configKey, enabled) {
23383
23549
  );
23384
23550
  return projectListResponse(c, deps, overrides);
23385
23551
  }
23552
+ async function persistManyAndProject(c, deps, keys, enabled) {
23553
+ const overrides = await tryWithSqlite(
23554
+ { databasePath: deps.options.dbPath, autoBackup: false },
23555
+ async (adapter) => {
23556
+ for (const key of keys) {
23557
+ await applyChangeToAdapter(adapter, key, enabled);
23558
+ }
23559
+ return await adapter.pluginConfig.loadOverrideMap();
23560
+ }
23561
+ );
23562
+ return projectListResponse(c, deps, overrides);
23563
+ }
23386
23564
  async function applyChangeToAdapter(adapter, configKey, enabled) {
23387
23565
  await adapter.pluginConfig.set(configKey, enabled);
23388
23566
  if (enabled) return;
@@ -23426,13 +23604,6 @@ function validateBulkChange(change, deps) {
23426
23604
  message: tx(SERVER_TEXTS.pluginsUnknown, { id: change.id })
23427
23605
  };
23428
23606
  }
23429
- if (granularityOf(handle2) !== "bundle") {
23430
- return {
23431
- status: 400,
23432
- code: "bad-query",
23433
- message: tx(SERVER_TEXTS.pluginsGranularityExtensionExpected, { id: change.id })
23434
- };
23435
- }
23436
23607
  if (isPluginLocked(change.id)) {
23437
23608
  return {
23438
23609
  status: 403,
@@ -23473,13 +23644,22 @@ async function persistBulkAndProject(c, deps, changes) {
23473
23644
  { databasePath: deps.options.dbPath, autoBackup: false },
23474
23645
  async (adapter) => {
23475
23646
  for (const change of changes) {
23476
- await applyChangeToAdapter(adapter, change.id, change.enabled);
23647
+ const writeKeys = expandBulkChangeKeys(change, deps);
23648
+ for (const key of writeKeys) {
23649
+ await applyChangeToAdapter(adapter, key, change.enabled);
23650
+ }
23477
23651
  }
23478
23652
  return await adapter.pluginConfig.loadOverrideMap();
23479
23653
  }
23480
23654
  );
23481
23655
  return projectListResponse(c, deps, overrides);
23482
23656
  }
23657
+ function expandBulkChangeKeys(change, deps) {
23658
+ if (change.id.includes("/")) return [change.id];
23659
+ const handle = findHandle(change.id, deps);
23660
+ if (!handle) return [];
23661
+ return bundleExtensionIds(handle).map((extId) => qualifiedExtensionId(change.id, extId)).filter((q) => !isPluginLocked(q));
23662
+ }
23483
23663
  async function buildFreshResolver2(deps) {
23484
23664
  return buildFreshResolver({
23485
23665
  databasePath: deps.options.dbPath,
@@ -23497,8 +23677,11 @@ function findHandle(id, deps) {
23497
23677
  if (discovered) return { kind: "discovered", plugin: discovered };
23498
23678
  return null;
23499
23679
  }
23500
- function granularityOf(handle) {
23501
- return handle.kind === "built-in" ? handle.bundle.granularity : handle.plugin.granularity ?? "bundle";
23680
+ function bundleExtensionIds(handle) {
23681
+ if (handle.kind === "built-in") {
23682
+ return handle.bundle.extensions.map((e) => e.id);
23683
+ }
23684
+ return (handle.plugin.extensions ?? []).map((e) => e.id);
23502
23685
  }
23503
23686
  function hasExtension(handle, extensionId) {
23504
23687
  if (handle.kind === "built-in") {