@skill-map/cli 0.53.6 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/cli/tutorial/sm-tutorial/SKILL.md +22 -24
  2. package/dist/cli/tutorial/sm-tutorial/references/_core.md +8 -7
  3. package/dist/cli/tutorial/sm-tutorial/references/_manifest.yml +21 -42
  4. package/dist/cli/tutorial/sm-tutorial/references/fixtures.md +15 -7
  5. package/dist/cli/tutorial/sm-tutorial/references/part-authoring.md +2 -2
  6. package/dist/cli/tutorial/sm-tutorial/references/part-cli.md +1 -1
  7. package/dist/cli/tutorial/sm-tutorial/references/part-connect-harness.md +9 -10
  8. package/dist/cli/tutorial/sm-tutorial/references/part-daily-loop.md +563 -0
  9. package/dist/cli/tutorial/sm-tutorial/references/part-mcp.md +5 -1
  10. package/dist/cli/tutorial/sm-tutorial/references/part-plugins.md +7 -7
  11. package/dist/cli/tutorial/sm-tutorial/references/part-project-kickoff.md +24 -12
  12. package/dist/cli/tutorial/sm-tutorial/references/part-settings.md +2 -2
  13. package/dist/cli.js +1019 -718
  14. package/dist/index.js +129 -15
  15. package/dist/kernel/index.d.ts +209 -89
  16. package/dist/kernel/index.js +129 -15
  17. package/dist/migrations/001_initial.sql +6 -1
  18. package/dist/ui/chunk-CN6IOM67.js +2 -0
  19. package/dist/ui/chunk-HPKRDGLH.js +123 -0
  20. package/dist/ui/{chunk-NBXEOYS4.js → chunk-LREXXX7T.js} +1 -1
  21. package/dist/ui/{chunk-7OJPO3XD.js → chunk-RGB5FBZL.js} +31 -30
  22. package/dist/ui/{chunk-4CXAL43H.js → chunk-XAM6VKXM.js} +1 -1
  23. package/dist/ui/index.html +2 -2
  24. package/dist/ui/{main-O4DGYJ62.js → main-7YXBWYHE.js} +3 -3
  25. package/dist/ui/{styles-L6FZYH7X.css → styles-HRJG67XW.css} +1 -1
  26. package/migrations/001_initial.sql +6 -1
  27. package/package.json +2 -2
  28. package/dist/cli/tutorial/sm-tutorial/references/part-live-site.md +0 -155
  29. package/dist/cli/tutorial/sm-tutorial/references/part-maintain.md +0 -284
  30. package/dist/cli/tutorial/sm-tutorial/references/part-run-harness.md +0 -181
  31. package/dist/ui/chunk-HWQTV6ZL.js +0 -2
  32. package/dist/ui/chunk-PRP3JSUU.js +0 -123
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // cli/entry.ts
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3bc438eb-6e57-550d-aab1-392242aca915")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e21852cd-d92e-5bdf-abc7-02fffaa89502")}catch(e){}}();
4
4
  import { existsSync as existsSync33 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -246,7 +246,7 @@ function bucketByKind(kind, instance, bag) {
246
246
  // package.json
247
247
  var package_default = {
248
248
  name: "@skill-map/cli",
249
- version: "0.53.6",
249
+ version: "0.55.0",
250
250
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
251
251
  license: "MIT",
252
252
  type: "module",
@@ -796,6 +796,16 @@ function stripCodeBlocks(input) {
796
796
  const fenceless = stripFences(input);
797
797
  return stripInline(fenceless);
798
798
  }
799
+ function extractCodeRegions(input) {
800
+ if (!input) return input;
801
+ const stripped = stripCodeBlocks(input);
802
+ let out = "";
803
+ for (let i = 0; i < input.length; i++) {
804
+ const ch = input[i];
805
+ out += ch === stripped[i] ? ch === "\n" ? "\n" : " " : ch;
806
+ }
807
+ return out;
808
+ }
799
809
  function stripFences(input) {
800
810
  const out = [];
801
811
  const lines = input.split("\n");
@@ -873,7 +883,7 @@ var atDirectiveExtractor = {
873
883
  id: ID,
874
884
  pluginId: CLAUDE_PLUGIN_ID,
875
885
  kind: "extractor",
876
- 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.",
886
+ description: "Detects `@<token>` directives in a node's body using Claude Code rules, choosing the link kind by token shape. Example: a bare handle `@team` becomes a `mentions` link, while a file-flavoured token `@docs/api.md` becomes a `references` link.",
877
887
  scope: "body",
878
888
  precondition: { provider: ["claude"] },
879
889
  // eslint-disable-next-line complexity
@@ -961,7 +971,7 @@ var slashCommandExtractor = {
961
971
  id: ID2,
962
972
  pluginId: CLAUDE_PLUGIN_ID,
963
973
  kind: "extractor",
964
- 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.",
974
+ description: "Turns `/command` invocations in a node's body into arrows that point at the resolved slash command or skill, using Claude Code routing rules. Example: `/deploy` in the body draws an arrow to the `deploy` command.",
965
975
  scope: "body",
966
976
  precondition: { provider: ["claude"] },
967
977
  extract(ctx) {
@@ -1005,6 +1015,44 @@ var slashCommandExtractor = {
1005
1015
  }
1006
1016
  };
1007
1017
 
1018
+ // plugins/claude/extractors/tools-counter/index.ts
1019
+ var ID3 = "tools-counter";
1020
+ var count = {
1021
+ slot: "card.footer.left",
1022
+ icon: "pi-wrench",
1023
+ label: "tools",
1024
+ emitWhenEmpty: false,
1025
+ priority: 40
1026
+ };
1027
+ var TOOLTIP_MAX = 255;
1028
+ var toolsCounterExtractor = {
1029
+ id: ID3,
1030
+ pluginId: CLAUDE_PLUGIN_ID,
1031
+ kind: "extractor",
1032
+ description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card. Example: an agent with `tools: [Bash, Read, Grep]` shows a count of 3.",
1033
+ scope: "frontmatter",
1034
+ precondition: { kind: ["claude/agent"] },
1035
+ ui: { count },
1036
+ extract(ctx) {
1037
+ const raw = ctx.frontmatter["tools"];
1038
+ if (!Array.isArray(raw)) return;
1039
+ const names = [];
1040
+ for (const t of raw) {
1041
+ if (typeof t === "string" && t.length > 0) names.push(t);
1042
+ }
1043
+ if (names.length === 0) return;
1044
+ ctx.emitContribution(count, {
1045
+ value: names.length,
1046
+ tooltip: buildTooltip(names)
1047
+ });
1048
+ }
1049
+ };
1050
+ function buildTooltip(names) {
1051
+ const joined = names.join(" \xB7 ");
1052
+ if (joined.length <= TOOLTIP_MAX) return joined;
1053
+ return `${joined.slice(0, TOOLTIP_MAX - 1)}\u2026`;
1054
+ }
1055
+
1008
1056
  // plugins/antigravity/providers/antigravity/index.ts
1009
1057
  var antigravityProvider = {
1010
1058
  id: "antigravity",
@@ -1391,12 +1439,12 @@ var coreMarkdownProvider = {
1391
1439
  };
1392
1440
 
1393
1441
  // plugins/core/extractors/annotations/index.ts
1394
- var ID3 = "annotations";
1442
+ var ID4 = "annotations";
1395
1443
  var annotationsExtractor = {
1396
- id: ID3,
1444
+ id: ID4,
1397
1445
  pluginId: CORE_PLUGIN_ID,
1398
1446
  kind: "extractor",
1399
- description: "Turns the `supersedes` and `supersededBy` entries from a node's `.sm` sidecar into arrows between nodes in the graph.",
1447
+ description: "Turns the `supersedes` and `supersededBy` entries from a node's `.sm` sidecar into arrows between nodes in the graph. Example: `supersededBy: v1-skill.md` in a `.sm` sidecar draws an arrow to `v1-skill.md`.",
1400
1448
  scope: "frontmatter",
1401
1449
  extract(ctx) {
1402
1450
  const sourcePath = ctx.node.path;
@@ -1412,7 +1460,7 @@ var annotationsExtractor = {
1412
1460
  raw: target,
1413
1461
  candidates: [
1414
1462
  {
1415
- extractorId: ID3,
1463
+ extractorId: ID4,
1416
1464
  kind: "supersedes",
1417
1465
  target,
1418
1466
  confidence: 1,
@@ -1449,9 +1497,67 @@ function stringArray(value) {
1449
1497
  return value.filter((v) => typeof v === "string" && v.length > 0);
1450
1498
  }
1451
1499
 
1500
+ // plugins/core/extractors/backtick-path/index.ts
1501
+ import { posix as pathPosix2 } from "path";
1502
+ var ID5 = "backtick-path";
1503
+ var PATH_RE = /(?<![\w/:.-])(?:\.{1,2}\/)?[\w][\w.-]*(?:\/[\w.-]+)*\.md\b(?![\w/])/g;
1504
+ var backtickPathExtractor = {
1505
+ id: ID5,
1506
+ pluginId: CORE_PLUGIN_ID,
1507
+ kind: "extractor",
1508
+ description: "Turns relative .md paths written inside code spans and fenced blocks into arrows between nodes in the graph. Example: a backticked `references/rules.md` path draws an arrow to that file.",
1509
+ scope: "body",
1510
+ extract(ctx) {
1511
+ const seen = /* @__PURE__ */ new Set();
1512
+ const body = extractCodeRegions(ctx.body);
1513
+ const lineStarts = computeLineStarts(body);
1514
+ const sourceDir = pathPosix2.dirname(ctx.node.path);
1515
+ for (const match of body.matchAll(PATH_RE)) {
1516
+ const original = match[0];
1517
+ const resolved = resolveTarget(sourceDir, original);
1518
+ if (resolved === null) continue;
1519
+ if (seen.has(resolved)) continue;
1520
+ seen.add(resolved);
1521
+ const offset = match.index ?? 0;
1522
+ const line = lineFor(lineStarts, offset);
1523
+ ctx.emitSignal({
1524
+ source: ctx.node.path,
1525
+ scope: "body",
1526
+ range: { start: offset, end: offset + original.length, line },
1527
+ raw: original,
1528
+ candidates: [
1529
+ {
1530
+ extractorId: ID5,
1531
+ kind: "points",
1532
+ target: resolved,
1533
+ // 0.85: a strong file signal with one degree of inference,
1534
+ // the author wrote a path inside a code region rather than
1535
+ // an explicit `[text](path)` link. Whether the path resolves
1536
+ // to a real node is a separate concern (`core/reference-broken`
1537
+ // flags unresolved targets), not a confidence question.
1538
+ confidence: 0.85,
1539
+ rationale: "relative .md path inside a code region",
1540
+ trigger: {
1541
+ originalTrigger: original,
1542
+ normalizedTrigger: resolved
1543
+ }
1544
+ }
1545
+ ]
1546
+ });
1547
+ }
1548
+ }
1549
+ };
1550
+ function resolveTarget(sourceDir, raw) {
1551
+ const trimmed = raw.trim();
1552
+ if (trimmed.length === 0) return null;
1553
+ if (trimmed.startsWith("/")) return null;
1554
+ const joined = sourceDir === "." ? trimmed : `${sourceDir}/${trimmed}`;
1555
+ return pathPosix2.normalize(joined);
1556
+ }
1557
+
1452
1558
  // plugins/core/extractors/external-url-counter/index.ts
1453
- var ID4 = "external-url-counter";
1454
- var count = {
1559
+ var ID6 = "external-url-counter";
1560
+ var count2 = {
1455
1561
  slot: "card.footer.left",
1456
1562
  icon: "pi-link",
1457
1563
  label: "urls",
@@ -1461,10 +1567,10 @@ var count = {
1461
1567
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
1462
1568
  var TRAILING_PUNCT = /[.,;:!?]+$/;
1463
1569
  var externalUrlCounterExtractor = {
1464
- id: ID4,
1570
+ id: ID6,
1465
1571
  pluginId: CORE_PLUGIN_ID,
1466
1572
  kind: "extractor",
1467
- description: "Counts the distinct external URLs in a node's body and shows the count on the card.",
1573
+ description: "Counts the distinct external URLs in a node's body and shows the count on the card. Example: a body linking `https://example.com` and `https://docs.rs` shows a count of 2.",
1468
1574
  scope: "body",
1469
1575
  /**
1470
1576
  * Phase 6 / View contribution system, surface the distinct-URL
@@ -1482,7 +1588,7 @@ var externalUrlCounterExtractor = {
1482
1588
  * inherited from the footer `.sm-gnode__stat` styles cloned by
1483
1589
  * the `NodeCounter` renderer.
1484
1590
  */
1485
- ui: { count },
1591
+ ui: { count: count2 },
1486
1592
  extract(ctx) {
1487
1593
  const seen = /* @__PURE__ */ new Set();
1488
1594
  const body = stripCodeBlocks(ctx.body);
@@ -1503,7 +1609,7 @@ var externalUrlCounterExtractor = {
1503
1609
  raw: original,
1504
1610
  candidates: [
1505
1611
  {
1506
- extractorId: ID4,
1612
+ extractorId: ID6,
1507
1613
  kind: "references",
1508
1614
  target: normalized,
1509
1615
  confidence: 0.3,
@@ -1517,7 +1623,7 @@ var externalUrlCounterExtractor = {
1517
1623
  });
1518
1624
  }
1519
1625
  if (seen.size > 0) {
1520
- ctx.emitContribution(count, { value: seen.size });
1626
+ ctx.emitContribution(count2, { value: seen.size });
1521
1627
  }
1522
1628
  }
1523
1629
  };
@@ -1536,24 +1642,24 @@ function normalizeUrl(raw) {
1536
1642
  }
1537
1643
 
1538
1644
  // plugins/core/extractors/markdown-link/index.ts
1539
- import { posix as pathPosix2 } from "path";
1540
- var ID5 = "markdown-link";
1645
+ import { posix as pathPosix3 } from "path";
1646
+ var ID7 = "markdown-link";
1541
1647
  var LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
1542
1648
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
1543
1649
  var markdownLinkExtractor = {
1544
- id: ID5,
1650
+ id: ID7,
1545
1651
  pluginId: CORE_PLUGIN_ID,
1546
1652
  kind: "extractor",
1547
- description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph.",
1653
+ description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph. Example: `[the guide](docs/guide.md)` draws an arrow to `docs/guide.md`.",
1548
1654
  scope: "body",
1549
1655
  extract(ctx) {
1550
1656
  const seen = /* @__PURE__ */ new Set();
1551
1657
  const body = stripCodeBlocks(ctx.body);
1552
1658
  const lineStarts = computeLineStarts(body);
1553
- const sourceDir = pathPosix2.dirname(ctx.node.path);
1659
+ const sourceDir = pathPosix3.dirname(ctx.node.path);
1554
1660
  for (const match of body.matchAll(LINK_RE)) {
1555
1661
  const original = match[2];
1556
- const resolved = resolveTarget(sourceDir, original);
1662
+ const resolved = resolveTarget2(sourceDir, original);
1557
1663
  if (resolved === null) continue;
1558
1664
  if (seen.has(resolved)) continue;
1559
1665
  seen.add(resolved);
@@ -1566,16 +1672,18 @@ var markdownLinkExtractor = {
1566
1672
  raw: match[0],
1567
1673
  candidates: [
1568
1674
  {
1569
- extractorId: ID5,
1675
+ extractorId: ID7,
1570
1676
  kind: "references",
1571
1677
  target: resolved,
1572
- // 1.0: the `[text](path)` syntax is unambiguous. Markdown
1573
- // explicitly designates an out-link via the brackets +
1574
- // parentheses pair; there is no inference left to discount.
1575
- // Whether the path resolves to a real node is a separate
1576
- // concern (the `core/reference-broken` analyzer flags unresolved
1577
- // targets), not a confidence question.
1578
- confidence: 1,
1678
+ // 0.95: the `[text](path)` syntax is unambiguous (the spec's
1679
+ // "unambiguous syntax" tier), but NOT 1.0. `1.0` is reserved
1680
+ // for structured input and is earned post-walk by the
1681
+ // confidence-lift transform when `target` resolves to a real
1682
+ // node; an unresolved (broken) target is downgraded to the
1683
+ // broken floor (0.5) by the same transform. Emitting 1.0 here
1684
+ // would short-circuit the lift and make a broken link
1685
+ // indistinguishable from a resolved one.
1686
+ confidence: 0.95,
1579
1687
  rationale: "unambiguous markdown link syntax",
1580
1688
  trigger: {
1581
1689
  originalTrigger: original,
@@ -1587,7 +1695,7 @@ var markdownLinkExtractor = {
1587
1695
  }
1588
1696
  }
1589
1697
  };
1590
- function resolveTarget(sourceDir, raw) {
1698
+ function resolveTarget2(sourceDir, raw) {
1591
1699
  const noFragment = raw.split("#", 1)[0];
1592
1700
  const noQuery = noFragment.split("?", 1)[0];
1593
1701
  const trimmed = noQuery.trim();
@@ -1595,17 +1703,21 @@ function resolveTarget(sourceDir, raw) {
1595
1703
  if (URL_SCHEME_RE.test(trimmed)) return null;
1596
1704
  if (trimmed.startsWith("/")) return null;
1597
1705
  const joined = sourceDir === "." ? trimmed : `${sourceDir}/${trimmed}`;
1598
- return pathPosix2.normalize(joined);
1706
+ return pathPosix3.normalize(joined);
1599
1707
  }
1600
1708
 
1601
1709
  // plugins/core/extractors/mcp-tools/index.ts
1602
- var ID6 = "mcp-tools";
1710
+ var ID8 = "mcp-tools";
1603
1711
  var MCP_PATTERN = /^mcp__([a-z0-9][a-z0-9_-]*)__[a-z0-9_-]+$/i;
1604
1712
  var mcpToolsExtractor = {
1605
- id: ID6,
1713
+ id: ID8,
1606
1714
  pluginId: CORE_PLUGIN_ID,
1607
1715
  kind: "extractor",
1608
- 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.",
1716
+ description: "Turns `tools: [mcp__<server>__<tool>]` entries in a node's frontmatter into an MCP node per unique server and an arrow from the source to each one. Example: `tools: [mcp__github__create_pr]` adds an `mcp://github` node and an arrow to it.",
1717
+ // Claude-convention pattern only; per-vendor flavours and the
1718
+ // config-side MCP declaration (Phase 5b) are still pending, so the
1719
+ // extractor ships flagged as experimental in list / show / Settings.
1720
+ stability: "experimental",
1609
1721
  scope: "frontmatter",
1610
1722
  extract(ctx) {
1611
1723
  const raw = ctx.frontmatter["tools"];
@@ -1629,7 +1741,7 @@ var mcpToolsExtractor = {
1629
1741
  raw: `mcp__${server}__*`,
1630
1742
  candidates: [
1631
1743
  {
1632
- extractorId: ID6,
1744
+ extractorId: ID8,
1633
1745
  kind: "references",
1634
1746
  target: mcpPath,
1635
1747
  confidence: 0.85,
@@ -1659,44 +1771,6 @@ function collectMcpServers(tools) {
1659
1771
  return out;
1660
1772
  }
1661
1773
 
1662
- // plugins/core/extractors/tools-counter/index.ts
1663
- var ID7 = "tools-counter";
1664
- var count2 = {
1665
- slot: "card.footer.left",
1666
- icon: "pi-wrench",
1667
- label: "tools",
1668
- emitWhenEmpty: false,
1669
- priority: 40
1670
- };
1671
- var TOOLTIP_MAX = 255;
1672
- var toolsCounterExtractor = {
1673
- id: ID7,
1674
- pluginId: CORE_PLUGIN_ID,
1675
- kind: "extractor",
1676
- description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card.",
1677
- scope: "frontmatter",
1678
- precondition: { kind: ["claude/agent"] },
1679
- ui: { count: count2 },
1680
- extract(ctx) {
1681
- const raw = ctx.frontmatter["tools"];
1682
- if (!Array.isArray(raw)) return;
1683
- const names = [];
1684
- for (const t of raw) {
1685
- if (typeof t === "string" && t.length > 0) names.push(t);
1686
- }
1687
- if (names.length === 0) return;
1688
- ctx.emitContribution(count2, {
1689
- value: names.length,
1690
- tooltip: buildTooltip(names)
1691
- });
1692
- }
1693
- };
1694
- function buildTooltip(names) {
1695
- const joined = names.join(" \xB7 ");
1696
- if (joined.length <= TOOLTIP_MAX) return joined;
1697
- return `${joined.slice(0, TOOLTIP_MAX - 1)}\u2026`;
1698
- }
1699
-
1700
1774
  // plugins/core/analyzers/annotation-field-unknown/index.ts
1701
1775
  import { readFileSync } from "fs";
1702
1776
  import { dirname, resolve } from "path";
@@ -1712,12 +1786,14 @@ function applyAjvFormats(ajv) {
1712
1786
 
1713
1787
  // plugins/core/analyzers/annotation-field-unknown/text.ts
1714
1788
  var ANNOTATION_FIELD_UNKNOWN_TEXTS = {
1789
+ // Compact finding grammar: the affected node is the finding's own
1790
+ // node, so its path never appears in the message.
1715
1791
  /** Key inside `annotations:` is not in the curated catalog. */
1716
- unknownAnnotationKey: "{{path}}: sidecar annotations contain unknown key '{{key}}' (not in annotations.schema.json catalog).",
1792
+ unknownAnnotationKey: "Unknown sidecar key '{{key}}'; not in the annotations catalog.",
1717
1793
  /** Top-level key is neither reserved, nor a registered plugin namespace, nor a registered root key. */
1718
- unknownRootKey: "{{path}}: sidecar declares unknown top-level key '{{key}}'; not a reserved block, not a registered plugin namespace, not a registered root contribution.",
1794
+ unknownRootKey: "Unknown sidecar top-level key '{{key}}'; not a reserved block, a plugin namespace, or a root contribution.",
1719
1795
  /** Value under a registered plugin namespace fails the contributed schema. */
1720
- pluginNamespaceInvalid: "{{path}}: sidecar block '{{pluginId}}.{{key}}' fails the schema contributed by plugin '{{pluginId}}': {{errors}}.",
1796
+ pluginNamespaceInvalid: "Sidecar block '{{pluginId}}.{{key}}' fails the schema from plugin '{{pluginId}}': {{errors}}.",
1721
1797
  // Tooltips for the per-node view-contribution badges. Singular vs
1722
1798
  // plural keeps the count grammar correct without a sub-template.
1723
1799
  alertTooltipSingle: "This node has 1 unknown field in its sidecar. Open the inspector for details.",
@@ -1725,10 +1801,10 @@ var ANNOTATION_FIELD_UNKNOWN_TEXTS = {
1725
1801
  };
1726
1802
 
1727
1803
  // plugins/core/analyzers/annotation-field-unknown/index.ts
1728
- var ID8 = "annotation-field-unknown";
1804
+ var ID9 = "annotation-field-unknown";
1729
1805
  var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
1730
1806
  var annotationFieldUnknownAnalyzer = {
1731
- id: ID8,
1807
+ id: ID9,
1732
1808
  pluginId: CORE_PLUGIN_ID,
1733
1809
  kind: "analyzer",
1734
1810
  description: "Flags typos or unrecognized keys in sidecars (`.sm`).",
@@ -1767,7 +1843,7 @@ var annotationFieldUnknownAnalyzer = {
1767
1843
  for (const key of Object.keys(annotations)) {
1768
1844
  if (!knownAnnotationKeys.has(key)) {
1769
1845
  issues.push({
1770
- analyzerId: ID8,
1846
+ analyzerId: ID9,
1771
1847
  severity: "warn",
1772
1848
  nodeIds: [node.path],
1773
1849
  message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownAnnotationKey, {
@@ -1794,7 +1870,7 @@ var annotationFieldUnknownAnalyzer = {
1794
1870
  if (validator(value)) continue;
1795
1871
  const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
1796
1872
  issues.push({
1797
- analyzerId: ID8,
1873
+ analyzerId: ID9,
1798
1874
  severity: "warn",
1799
1875
  nodeIds: [node.path],
1800
1876
  message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.pluginNamespaceInvalid, {
@@ -1810,7 +1886,7 @@ var annotationFieldUnknownAnalyzer = {
1810
1886
  continue;
1811
1887
  }
1812
1888
  issues.push({
1813
- analyzerId: ID8,
1889
+ analyzerId: ID9,
1814
1890
  severity: "warn",
1815
1891
  nodeIds: [node.path],
1816
1892
  message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownRootKey, {
@@ -1871,14 +1947,19 @@ function collectPluginIds(contributions) {
1871
1947
 
1872
1948
  // plugins/core/analyzers/annotation-orphan/text.ts
1873
1949
  var ANNOTATION_ORPHAN_TEXTS = {
1874
- /** Sidecar `<path>.sm` has no matching `<path>.md`. */
1875
- message: "Orphan sidecar: {{sidecarPath}} has no matching markdown node at {{expectedMdPath}}."
1950
+ /**
1951
+ * Compact finding grammar: line 1 = the orphan sidecar file, line 2
1952
+ * = the diagnosis. The expected markdown path IS the finding's
1953
+ * `nodeIds[0]` (the issue files under the path the sidecar points
1954
+ * at), so it never appears in the message.
1955
+ */
1956
+ message: "{{sidecarPath}}:\nOrphan sidecar; no matching markdown node."
1876
1957
  };
1877
1958
 
1878
1959
  // plugins/core/analyzers/annotation-orphan/index.ts
1879
- var ID9 = "annotation-orphan";
1960
+ var ID10 = "annotation-orphan";
1880
1961
  var annotationOrphanAnalyzer = {
1881
- id: ID9,
1962
+ id: ID10,
1882
1963
  pluginId: CORE_PLUGIN_ID,
1883
1964
  kind: "analyzer",
1884
1965
  description: "Flags sidecars (`.sm`) whose `.md` file no longer exists.",
@@ -1890,7 +1971,7 @@ var annotationOrphanAnalyzer = {
1890
1971
  for (const orphan of orphans) {
1891
1972
  const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
1892
1973
  issues.push({
1893
- analyzerId: ID9,
1974
+ analyzerId: ID10,
1894
1975
  severity: "warn",
1895
1976
  nodeIds: [expectedMdRelative],
1896
1977
  message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
@@ -1909,12 +1990,14 @@ var annotationOrphanAnalyzer = {
1909
1990
 
1910
1991
  // plugins/core/analyzers/annotation-stale/text.ts
1911
1992
  var ANNOTATION_STALE_TEXTS = {
1993
+ // Compact finding grammar: the affected node is the finding's own
1994
+ // node, so its path never appears in the message.
1912
1995
  /** body changed since last bump */
1913
- bodyDrift: "{{path}}: sidecar `.sm` is stale (body changed since last bump).",
1996
+ bodyDrift: "Sidecar `.sm` is stale: body changed since last bump.",
1914
1997
  /** frontmatter changed since last bump */
1915
- frontmatterDrift: "{{path}}: sidecar `.sm` is stale (frontmatter changed since last bump).",
1998
+ frontmatterDrift: "Sidecar `.sm` is stale: frontmatter changed since last bump.",
1916
1999
  /** both body and frontmatter changed */
1917
- bothDrift: "{{path}}: sidecar `.sm` is stale (body and frontmatter changed since last bump).",
2000
+ bothDrift: "Sidecar `.sm` is stale: body and frontmatter changed since last bump.",
1918
2001
  // Tooltips for the `card.footer.right` clock chip emitted alongside
1919
2002
  // the issue. Lists only the drifted face(s), in-sync faces are
1920
2003
  // omitted so the operator immediately sees what's modified without
@@ -1923,15 +2006,11 @@ var ANNOTATION_STALE_TEXTS = {
1923
2006
  // a literal placeholder the operator substitutes.
1924
2007
  bodyTooltip: "Sidecar drift since last bump:\n \u2022 body\nRun `sm bump <path>` to refresh.",
1925
2008
  frontmatterTooltip: "Sidecar drift since last bump:\n \u2022 frontmatter\nRun `sm bump <path>` to refresh.",
1926
- bothTooltip: "Sidecar drift since last bump:\n \u2022 body\n \u2022 frontmatter\nRun `sm bump <path>` to refresh.",
1927
- /** Label of the inspector action button that dispatches a bump. */
1928
- bumpLabel: "Bump",
1929
- /** Tooltip shown when the bump button is disabled (the node is fresh, no drift). */
1930
- bumpDisabledReason: "No drift to bump."
2009
+ bothTooltip: "Sidecar drift since last bump:\n \u2022 body\n \u2022 frontmatter\nRun `sm bump <path>` to refresh."
1931
2010
  };
1932
2011
 
1933
2012
  // plugins/core/analyzers/annotation-stale/index.ts
1934
- var ID10 = "annotation-stale";
2013
+ var ID11 = "annotation-stale";
1935
2014
  var staleIcon = {
1936
2015
  slot: "card.footer.right",
1937
2016
  icon: "pi-clock",
@@ -1943,30 +2022,24 @@ var staleBadge = {
1943
2022
  emitWhenEmpty: false,
1944
2023
  priority: 20
1945
2024
  };
1946
- var bumpButton = {
1947
- slot: "inspector.action.button",
1948
- priority: 10
1949
- };
1950
2025
  var annotationStaleAnalyzer = {
1951
- id: ID10,
2026
+ id: ID11,
1952
2027
  pluginId: CORE_PLUGIN_ID,
1953
2028
  kind: "analyzer",
1954
2029
  description: "Marks sidecars (`.sm`) that are out of date with their `.md`.",
1955
2030
  mode: "deterministic",
1956
2031
  // The natural fix is to bump the node: refreshes the sidecar hashes,
1957
2032
  // increments `annotations.version`, and stamps the audit block. The
1958
- // inspector surfaces `core/node-bump` as the `bumpButton` contribution.
1959
- ui: { staleIcon, staleBadge, bumpButton },
2033
+ // inspector surfaces that affordance via the `core/node-bump` action's
2034
+ // own scan-time `project()` self-projection, not from this analyzer.
2035
+ ui: { staleIcon, staleBadge },
1960
2036
  evaluate(ctx) {
1961
2037
  const issues = [];
1962
2038
  for (const node of ctx.nodes) {
1963
2039
  const status = staleStatus(node.sidecar);
1964
- if (node.sidecar?.present === true) {
1965
- emitBumpButton(ctx, node.path, status !== null);
1966
- }
1967
2040
  if (status === null) continue;
1968
2041
  issues.push({
1969
- analyzerId: ID10,
2042
+ analyzerId: ID11,
1970
2043
  severity: "info",
1971
2044
  nodeIds: [node.path],
1972
2045
  message: messageFor(status, node.path),
@@ -1999,15 +2072,6 @@ function messageFor(status, path) {
1999
2072
  return tx(ANNOTATION_STALE_TEXTS.bothDrift, { path });
2000
2073
  }
2001
2074
  }
2002
- function emitBumpButton(ctx, nodePath, enabled) {
2003
- ctx.emitContribution(nodePath, bumpButton, {
2004
- actionId: "core/node-bump",
2005
- label: ANNOTATION_STALE_TEXTS.bumpLabel,
2006
- icon: "pi-arrow-up-right",
2007
- enabled,
2008
- ...enabled ? {} : { disabledReason: ANNOTATION_STALE_TEXTS.bumpDisabledReason }
2009
- });
2010
- }
2011
2075
  function tooltipFor(status) {
2012
2076
  switch (status) {
2013
2077
  case "stale-body":
@@ -2020,9 +2084,9 @@ function tooltipFor(status) {
2020
2084
  }
2021
2085
 
2022
2086
  // plugins/core/analyzers/contribution-orphan/index.ts
2023
- var ID11 = "contribution-orphan";
2087
+ var ID12 = "contribution-orphan";
2024
2088
  var contributionOrphanAnalyzer = {
2025
- id: ID11,
2089
+ id: ID12,
2026
2090
  pluginId: CORE_PLUGIN_ID,
2027
2091
  kind: "analyzer",
2028
2092
  description: "Warns about plugin data referencing nodes renamed or deleted in the latest scan.",
@@ -2041,7 +2105,7 @@ var ISSUE_COUNTER_TEXTS = {
2041
2105
  };
2042
2106
 
2043
2107
  // plugins/core/analyzers/issue-counter/index.ts
2044
- var ID12 = "issue-counter";
2108
+ var ID13 = "issue-counter";
2045
2109
  var warnCount = {
2046
2110
  slot: "card.footer.right",
2047
2111
  icon: "pi-exclamation-triangle",
@@ -2077,7 +2141,7 @@ function emitTierChips(ctx, ref, severity, counts, singleTooltip, manyTooltip) {
2077
2141
  }
2078
2142
  }
2079
2143
  var issueCounterAnalyzer = {
2080
- id: ID12,
2144
+ id: ID13,
2081
2145
  pluginId: CORE_PLUGIN_ID,
2082
2146
  kind: "analyzer",
2083
2147
  description: "Emits one aggregate severity chip per node (error + warn counts) from the live issue accumulator.",
@@ -2112,15 +2176,16 @@ var issueCounterAnalyzer = {
2112
2176
  var JOB_FILE_ORPHAN_TEXTS = {
2113
2177
  /**
2114
2178
  * `<path>.md` lives under `.skill-map/jobs/` but no `state_jobs.filePath`
2115
- * row references it. Run `sm job prune --orphan-files` to remove.
2179
+ * row references it. Compact finding grammar: the file IS the
2180
+ * finding's own node, so its path never appears in the message.
2116
2181
  */
2117
- message: "Orphan job file: {{filePath}} is not referenced by any state_jobs row. Run `sm job prune --orphan-files` to remove it."
2182
+ message: "Orphan job file; not referenced by any job. Run `sm job prune --orphan-files` to remove it."
2118
2183
  };
2119
2184
 
2120
2185
  // plugins/core/analyzers/job-file-orphan/index.ts
2121
- var ID13 = "job-file-orphan";
2186
+ var ID14 = "job-file-orphan";
2122
2187
  var jobFileOrphanAnalyzer = {
2123
- id: ID13,
2188
+ id: ID14,
2124
2189
  pluginId: CORE_PLUGIN_ID,
2125
2190
  kind: "analyzer",
2126
2191
  description: "Flags leftover job result files (no live job references them). Clean up via `sm job prune --orphan-files`.",
@@ -2131,7 +2196,7 @@ var jobFileOrphanAnalyzer = {
2131
2196
  const issues = [];
2132
2197
  for (const filePath of orphans) {
2133
2198
  issues.push({
2134
- analyzerId: ID13,
2199
+ analyzerId: ID14,
2135
2200
  severity: "warn",
2136
2201
  nodeIds: [filePath],
2137
2202
  message: tx(JOB_FILE_ORPHAN_TEXTS.message, { filePath }),
@@ -2144,14 +2209,19 @@ var jobFileOrphanAnalyzer = {
2144
2209
 
2145
2210
  // plugins/core/analyzers/link-conflict/text.ts
2146
2211
  var LINK_CONFLICT_TEXTS = {
2147
- /** `Detectors disagree on link kind for <source> → <target> (<kindList>)` */
2148
- message: "Detectors disagree on link kind for {{source}} \u2192 {{target}} ({{kindList}})"
2212
+ /**
2213
+ * Compact finding grammar: line 1 = the disputed target, line 2 =
2214
+ * the short diagnosis. The source is the finding's own node, so it
2215
+ * never appears in the message.
2216
+ */
2217
+ message: "{{target}}:\nDetectors disagree on link kind ({{kindList}})."
2149
2218
  };
2150
2219
 
2151
2220
  // plugins/core/analyzers/link-conflict/index.ts
2152
- var ID14 = "link-conflict";
2221
+ var ID15 = "link-conflict";
2222
+ var NON_CONFLICTING_KINDS = /* @__PURE__ */ new Set(["points"]);
2153
2223
  var linkConflictAnalyzer = {
2154
- id: ID14,
2224
+ id: ID15,
2155
2225
  pluginId: "core",
2156
2226
  kind: "analyzer",
2157
2227
  description: "Flags conflicting arrow meanings between extractors (e.g. `references` vs `invokes`).",
@@ -2163,6 +2233,7 @@ var linkConflictAnalyzer = {
2163
2233
  evaluate(ctx) {
2164
2234
  const groups = /* @__PURE__ */ new Map();
2165
2235
  for (const link of ctx.links) {
2236
+ if (NON_CONFLICTING_KINDS.has(link.kind)) continue;
2166
2237
  const key = `${link.source}\0${link.target}`;
2167
2238
  const bucket = groups.get(key);
2168
2239
  if (bucket) bucket.push(link);
@@ -2198,7 +2269,7 @@ var linkConflictAnalyzer = {
2198
2269
  const [source, target] = key.split("\0");
2199
2270
  const kindList = variants.map((v) => v.kind).join(" / ");
2200
2271
  issues.push({
2201
- analyzerId: ID14,
2272
+ analyzerId: ID15,
2202
2273
  severity: "warn",
2203
2274
  nodeIds: [source, target],
2204
2275
  message: tx(LINK_CONFLICT_TEXTS.message, {
@@ -2265,7 +2336,7 @@ function resolveLinkTargetToPath(link, nameIndex) {
2265
2336
  }
2266
2337
 
2267
2338
  // plugins/core/analyzers/link-counter/index.ts
2268
- var ID15 = "link-counter";
2339
+ var ID16 = "link-counter";
2269
2340
  var linksIn = {
2270
2341
  slot: "card.footer.left",
2271
2342
  icon: "pi-download",
@@ -2281,7 +2352,7 @@ var linksOut = {
2281
2352
  priority: 20
2282
2353
  };
2283
2354
  var linkCounterAnalyzer = {
2284
- id: ID15,
2355
+ id: ID16,
2285
2356
  pluginId: CORE_PLUGIN_ID,
2286
2357
  kind: "analyzer",
2287
2358
  description: "Counts incoming and outgoing links per node.",
@@ -2328,6 +2399,27 @@ function formatBreakdown(byKind, direction) {
2328
2399
  return [direction, ...lines].join("\n");
2329
2400
  }
2330
2401
 
2402
+ // kernel/util/link-lines.ts
2403
+ function linkLines(link) {
2404
+ const lines = /* @__PURE__ */ new Set();
2405
+ for (const occ of link.occurrences ?? []) {
2406
+ const line = occ.location?.line;
2407
+ if (typeof line === "number") lines.add(line);
2408
+ }
2409
+ if (lines.size === 0) {
2410
+ const line = link.location?.line;
2411
+ if (typeof line === "number") lines.add(line);
2412
+ }
2413
+ return [...lines].sort((a, b) => a - b);
2414
+ }
2415
+ function linkWhere(link, texts) {
2416
+ const lines = linkLines(link);
2417
+ if (lines.length === 0) return "";
2418
+ return tx(lines.length === 1 ? texts.single : texts.plural, {
2419
+ lines: lines.join(", ")
2420
+ });
2421
+ }
2422
+
2331
2423
  // plugins/core/analyzers/link-self-loop/text.ts
2332
2424
  var LINK_SELF_LOOP_TEXTS = {
2333
2425
  /**
@@ -2338,13 +2430,17 @@ var LINK_SELF_LOOP_TEXTS = {
2338
2430
  * the operator's intent; UI consumers MAY hide it by default and
2339
2431
  * surface a count.
2340
2432
  */
2341
- message: "{{source}} references itself via `{{trigger}}` ({{kind}}). Self-loops typically come from the file's own heading or label and are noise rather than intent. Either remove the in-body token or treat this finding as expected and acknowledged."
2433
+ message: "`{{trigger}}`:\nSelf-reference ({{kind}}{{where}}); typically the file's own heading or label. Remove the token or ignore deliberately.",
2434
+ /** Location suffix inside the kind parens, one detection site. */
2435
+ whereSingle: ", line {{lines}}",
2436
+ /** Location suffix inside the kind parens, several detection sites. */
2437
+ wherePlural: ", lines {{lines}}"
2342
2438
  };
2343
2439
 
2344
2440
  // plugins/core/analyzers/link-self-loop/index.ts
2345
- var ID16 = "link-self-loop";
2441
+ var ID17 = "link-self-loop";
2346
2442
  var linkSelfLoopAnalyzer = {
2347
- id: ID16,
2443
+ id: ID17,
2348
2444
  pluginId: CORE_PLUGIN_ID,
2349
2445
  kind: "analyzer",
2350
2446
  description: "Flags links whose source is also their own resolved target (e.g. a body heading like `# /deploy` inside the file that defines `/deploy`).",
@@ -2355,13 +2451,16 @@ var linkSelfLoopAnalyzer = {
2355
2451
  for (const link of ctx.links) {
2356
2452
  if (!isSelfLoop(link)) continue;
2357
2453
  issues.push({
2358
- analyzerId: ID16,
2454
+ analyzerId: ID17,
2359
2455
  severity: "warn",
2360
2456
  nodeIds: [link.source],
2361
2457
  message: tx(LINK_SELF_LOOP_TEXTS.message, {
2362
- source: link.source,
2363
2458
  trigger: link.trigger?.originalTrigger ?? link.target,
2364
- kind: link.kind
2459
+ kind: link.kind,
2460
+ where: linkWhere(link, {
2461
+ single: LINK_SELF_LOOP_TEXTS.whereSingle,
2462
+ plural: LINK_SELF_LOOP_TEXTS.wherePlural
2463
+ })
2365
2464
  }),
2366
2465
  data: {
2367
2466
  target: link.target,
@@ -2384,7 +2483,7 @@ function isSelfLoop(link) {
2384
2483
  }
2385
2484
 
2386
2485
  // kernel/orchestrator/node-identifiers.ts
2387
- import { posix as pathPosix3 } from "path";
2486
+ import { posix as pathPosix4 } from "path";
2388
2487
  function deriveNodeIdentifiers(node, kindDescriptor) {
2389
2488
  const sources = kindDescriptor?.identifiers;
2390
2489
  if (!sources || sources.length === 0) return [];
@@ -2408,31 +2507,40 @@ function readFrontmatterName(node) {
2408
2507
  return raw.length > 0 ? raw : null;
2409
2508
  }
2410
2509
  function readFilenameBasename(node) {
2411
- const base = pathPosix3.basename(node.path);
2510
+ const base = pathPosix4.basename(node.path);
2412
2511
  if (!base) return null;
2413
- const ext = pathPosix3.extname(base);
2512
+ const ext = pathPosix4.extname(base);
2414
2513
  const stem = ext ? base.slice(0, -ext.length) : base;
2415
2514
  return stem.length > 0 ? stem : null;
2416
2515
  }
2417
2516
  function readDirname(node) {
2418
- const dir = pathPosix3.dirname(node.path);
2517
+ const dir = pathPosix4.dirname(node.path);
2419
2518
  if (!dir || dir === "." || dir === "/") return null;
2420
- const base = pathPosix3.basename(dir);
2519
+ const base = pathPosix4.basename(dir);
2421
2520
  return base.length > 0 ? base : null;
2422
2521
  }
2423
2522
 
2424
2523
  // kernel/orchestrator/lift-resolved-link-confidence.ts
2425
2524
  var RESERVED_TARGET_CONFIDENCE = 0.1;
2525
+ var BROKEN_TARGET_CONFIDENCE = 0.5;
2426
2526
  function liftResolvedLinkConfidence(links, nodes, ctx) {
2427
2527
  if (!links.some((l) => l.confidence < 1)) return;
2428
2528
  const indexes = buildIndexes(nodes, ctx);
2429
2529
  for (const link of links) {
2430
- if (link.confidence >= 1) continue;
2431
- const resolution = resolve2(link, indexes, ctx);
2432
- if (resolution === "none") continue;
2433
- link.confidence = ctx.reservedNodePaths.has(resolution) ? RESERVED_TARGET_CONFIDENCE : 1;
2434
- link.resolvedTarget = resolution;
2530
+ if (link.confidence < 1) applyResolution(link, indexes, ctx);
2531
+ }
2532
+ }
2533
+ function applyResolution(link, indexes, ctx) {
2534
+ const resolution = resolve2(link, indexes, ctx);
2535
+ if (resolution === "none") {
2536
+ if (isGenuinelyBroken(link, indexes)) {
2537
+ link.confidence = Math.min(link.confidence, BROKEN_TARGET_CONFIDENCE);
2538
+ }
2539
+ return;
2435
2540
  }
2541
+ link.resolvedTarget = resolution;
2542
+ if (indexes.nodeByPath.get(resolution)?.virtual) return;
2543
+ link.confidence = ctx.reservedNodePaths.has(resolution) ? RESERVED_TARGET_CONFIDENCE : 1;
2436
2544
  }
2437
2545
  function buildIndexes(nodes, ctx) {
2438
2546
  const byPath3 = /* @__PURE__ */ new Set();
@@ -2449,6 +2557,12 @@ function resolve2(link, indexes, ctx) {
2449
2557
  if (indexes.byPath.has(link.target)) return link.target;
2450
2558
  return resolveByName(link, indexes, ctx);
2451
2559
  }
2560
+ function isGenuinelyBroken(link, indexes) {
2561
+ if (indexes.byPath.has(link.target)) return false;
2562
+ const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
2563
+ if (stripped !== null && indexes.byName.has(stripped)) return false;
2564
+ return true;
2565
+ }
2452
2566
  function resolveByName(link, indexes, ctx) {
2453
2567
  const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
2454
2568
  if (stripped === null) return "none";
@@ -2492,7 +2606,7 @@ var NAME_RESERVED_TEXTS = {
2492
2606
  * a runtime built-in. Same wording skill-map shipped before the
2493
2607
  * source-side link finding landed.
2494
2608
  */
2495
- message: "{{path}} shadows a built-in {{provider}} {{kind}}. The runtime ignores this file in favour of its own built-in. Rename the file or `frontmatter.name` to a non-reserved value.",
2609
+ message: "Built-in {{provider}} {{kind}}:\nShadowed by this file; the runtime uses its built-in instead. Rename the file or its `frontmatter.name`.",
2496
2610
  /**
2497
2611
  * Source-side message: emitted on the node that AUTHORED a link
2498
2612
  * whose target resolves to a reserved name. Explains WHY the link's
@@ -2500,13 +2614,17 @@ var NAME_RESERVED_TEXTS = {
2500
2614
  * the kernel saw the target match a runtime built-in and downgraded
2501
2615
  * the edge so the operator notices.
2502
2616
  */
2503
- linkMessage: "Link `{{kind}} {{target}}` resolves to a name reserved by the {{provider}} runtime ({{reservedKind}} `{{reservedPath}}`). The runtime shadows the user file, so this edge is downgraded to confidence {{confidence}} instead of 1.0. Rename the target file or its `frontmatter.name` to a non-reserved value."
2617
+ linkMessage: "{{target}}:\nResolves to a {{provider}} built-in ({{reservedKind}} `{{reservedPath}}`){{where}}; edge downgraded to confidence {{confidence}}. Rename the target file or its `frontmatter.name`.",
2618
+ /** Location suffix after the built-in parens, one detection site. */
2619
+ whereSingle: " (line {{lines}})",
2620
+ /** Location suffix after the built-in parens, several detection sites. */
2621
+ wherePlural: " (lines {{lines}})"
2504
2622
  };
2505
2623
 
2506
2624
  // plugins/core/analyzers/name-reserved/index.ts
2507
- var ID17 = "name-reserved";
2625
+ var ID18 = "name-reserved";
2508
2626
  var nameReservedAnalyzer = {
2509
- id: ID17,
2627
+ id: ID18,
2510
2628
  pluginId: CORE_PLUGIN_ID,
2511
2629
  kind: "analyzer",
2512
2630
  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.",
@@ -2522,7 +2640,7 @@ var nameReservedAnalyzer = {
2522
2640
  const node = byPath3.get(path);
2523
2641
  if (!node) continue;
2524
2642
  issues.push({
2525
- analyzerId: ID17,
2643
+ analyzerId: ID18,
2526
2644
  severity: "warn",
2527
2645
  nodeIds: [node.path],
2528
2646
  message: tx(NAME_RESERVED_TEXTS.message, {
@@ -2538,16 +2656,16 @@ var nameReservedAnalyzer = {
2538
2656
  const reservedNode = findReservedNodeForLink(link, reserved, byPath3);
2539
2657
  if (!reservedNode) continue;
2540
2658
  issues.push({
2541
- analyzerId: ID17,
2659
+ analyzerId: ID18,
2542
2660
  severity: "warn",
2543
2661
  nodeIds: [link.source],
2544
2662
  message: tx(NAME_RESERVED_TEXTS.linkMessage, {
2545
- kind: link.kind,
2546
2663
  target: link.target,
2547
2664
  provider: reservedNode.provider,
2548
2665
  reservedKind: reservedNode.kind,
2549
2666
  reservedPath: reservedNode.path,
2550
- confidence: RESERVED_TARGET_CONFIDENCE.toFixed(2)
2667
+ confidence: RESERVED_TARGET_CONFIDENCE.toFixed(2),
2668
+ where: linkWhereSuffix(link)
2551
2669
  }),
2552
2670
  data: {
2553
2671
  target: link.target,
@@ -2562,6 +2680,12 @@ var nameReservedAnalyzer = {
2562
2680
  return issues;
2563
2681
  }
2564
2682
  };
2683
+ function linkWhereSuffix(link) {
2684
+ return linkWhere(link, {
2685
+ single: NAME_RESERVED_TEXTS.whereSingle,
2686
+ plural: NAME_RESERVED_TEXTS.wherePlural
2687
+ });
2688
+ }
2565
2689
  function findReservedNodeForLink(link, reserved, byPath3) {
2566
2690
  if (reserved.has(link.target)) {
2567
2691
  const node = byPath3.get(link.target);
@@ -2613,7 +2737,7 @@ var NODE_STABILITY_TEXTS = {
2613
2737
  };
2614
2738
 
2615
2739
  // plugins/core/analyzers/node-stability/index.ts
2616
- var ID18 = "node-stability";
2740
+ var ID19 = "node-stability";
2617
2741
  var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
2618
2742
  var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
2619
2743
  var experimental = {
@@ -2635,7 +2759,7 @@ var setStabilityButton = {
2635
2759
  priority: 15
2636
2760
  };
2637
2761
  var nodeStabilityAnalyzer = {
2638
- id: ID18,
2762
+ id: ID19,
2639
2763
  pluginId: CORE_PLUGIN_ID,
2640
2764
  kind: "analyzer",
2641
2765
  description: "Reports a node's stability stage (`experimental`, `deprecated`) on the card.",
@@ -2654,7 +2778,7 @@ var nodeStabilityAnalyzer = {
2654
2778
  tooltip: EXPERIMENTAL_TOOLTIP
2655
2779
  });
2656
2780
  issues.push({
2657
- analyzerId: ID18,
2781
+ analyzerId: ID19,
2658
2782
  severity: "info",
2659
2783
  nodeIds: [node.path],
2660
2784
  message: `Node '${node.path}' is marked experimental: API may change.`,
@@ -2667,7 +2791,7 @@ var nodeStabilityAnalyzer = {
2667
2791
  severity: "warn"
2668
2792
  });
2669
2793
  issues.push({
2670
- analyzerId: ID18,
2794
+ analyzerId: ID19,
2671
2795
  severity: "warn",
2672
2796
  nodeIds: [node.path],
2673
2797
  message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
@@ -2715,17 +2839,27 @@ function emitSetStabilityButton(ctx, nodePath, current) {
2715
2839
 
2716
2840
  // plugins/core/analyzers/node-superseded/text.ts
2717
2841
  var NODE_SUPERSEDED_TEXTS = {
2718
- /** `<path> is superseded by <supersededBy>` */
2719
- message: "{{path}} is superseded by {{supersededBy}}"
2842
+ /**
2843
+ * Compact finding grammar: line 1 = the superseding artifact, line
2844
+ * 2 = what it means. The superseded node is the finding's own node,
2845
+ * so its path never appears in the message.
2846
+ */
2847
+ message: "{{supersededBy}}:\nSupersedes this node."
2720
2848
  };
2721
2849
 
2722
2850
  // plugins/core/analyzers/node-superseded/index.ts
2723
- var ID19 = "node-superseded";
2851
+ var ID20 = "node-superseded";
2724
2852
  var nodeSupersededAnalyzer = {
2725
- id: ID19,
2853
+ id: ID20,
2726
2854
  pluginId: CORE_PLUGIN_ID,
2727
2855
  kind: "analyzer",
2728
2856
  description: "Marks nodes replaced by a newer one via `supersededBy`.",
2857
+ // Part of the experimental supersession feature: ships disabled by
2858
+ // default alongside the declarer (`core/supersede` button +
2859
+ // `core/node-supersede` action). With the declarer off by default a
2860
+ // user rarely produces `supersededBy` data, so surfacing it stays
2861
+ // experimental too; the operator opts the whole family in together.
2862
+ stability: "experimental",
2729
2863
  mode: "deterministic",
2730
2864
  evaluate(ctx) {
2731
2865
  const issues = [];
@@ -2733,7 +2867,7 @@ var nodeSupersededAnalyzer = {
2733
2867
  const supersededBy = pickSupersededBy(node);
2734
2868
  if (supersededBy === null) continue;
2735
2869
  issues.push({
2736
- analyzerId: ID19,
2870
+ analyzerId: ID20,
2737
2871
  severity: "info",
2738
2872
  nodeIds: [node.path],
2739
2873
  message: tx(NODE_SUPERSEDED_TEXTS.message, {
@@ -2757,12 +2891,34 @@ function pickSupersededBy(node) {
2757
2891
  }
2758
2892
 
2759
2893
  // plugins/core/analyzers/reference-broken/index.ts
2760
- import { posix as pathPosix4, resolve as resolve3 } from "path";
2894
+ import { posix as pathPosix5, resolve as resolve3 } from "path";
2761
2895
 
2762
2896
  // plugins/core/analyzers/reference-broken/text.ts
2763
2897
  var REFERENCE_BROKEN_TEXTS = {
2764
- /** `Broken <kind> reference from <source> → <target>` */
2765
- message: "Broken {{kind}} reference from {{source}} \u2192 {{target}}",
2898
+ /**
2899
+ * Compact finding grammar: line 1 = the unresolved target, line 2 =
2900
+ * the short diagnosis plus WHERE the reference sits (`{{where}}` is
2901
+ * the pre-rendered location suffix below, or empty when the link
2902
+ * carries no line info). The source is the finding's own node, so it
2903
+ * never appears in the message.
2904
+ */
2905
+ message: "{{target}}:\nBroken {{kindLabel}}{{where}}.",
2906
+ /** Location suffix, one detection site. */
2907
+ whereSingle: " (line {{lines}})",
2908
+ /** Location suffix, several detection sites. */
2909
+ wherePlural: " (lines {{lines}})",
2910
+ /**
2911
+ * Human noun per link kind for the message above. Fallback for an
2912
+ * off-catalog kind: `<kind> link` (composed in the analyzer).
2913
+ */
2914
+ kindLabels: {
2915
+ references: "reference",
2916
+ mentions: "mention",
2917
+ invokes: "invocation",
2918
+ supersedes: "supersession",
2919
+ points: "pointer"
2920
+ },
2921
+ kindLabelFallback: "{{kind}} link",
2766
2922
  // Tooltips for the per-node view-contribution badges. Singular vs
2767
2923
  // plural keeps the count grammar correct without a sub-template.
2768
2924
  alertTooltipSingle: "This node has a broken reference. Open the inspector for details.",
@@ -2776,9 +2932,9 @@ var REFERENCE_BROKEN_TEXTS = {
2776
2932
  };
2777
2933
 
2778
2934
  // plugins/core/analyzers/reference-broken/index.ts
2779
- var ID20 = "reference-broken";
2935
+ var ID21 = "reference-broken";
2780
2936
  var referenceBrokenAnalyzer = {
2781
- id: ID20,
2937
+ id: ID21,
2782
2938
  pluginId: CORE_PLUGIN_ID,
2783
2939
  kind: "analyzer",
2784
2940
  description: "Flags arrows pointing at a node not part of the current scan.",
@@ -2815,7 +2971,7 @@ function buildIssue(link, hintCandidates = []) {
2815
2971
  trigger: link.trigger?.normalizedTrigger ?? null
2816
2972
  };
2817
2973
  const issue = {
2818
- analyzerId: ID20,
2974
+ analyzerId: ID21,
2819
2975
  // `error`, not `warn`: a link whose target is not in the scan is a
2820
2976
  // structural defect the operator must notice, and the card chip
2821
2977
  // paints `danger` (red) to match. Per the chip-vs-issue policy in
@@ -2825,33 +2981,37 @@ function buildIssue(link, hintCandidates = []) {
2825
2981
  severity: "error",
2826
2982
  nodeIds: [link.source],
2827
2983
  message: tx(REFERENCE_BROKEN_TEXTS.message, {
2828
- kind: link.kind,
2829
- source: link.source,
2830
- target: link.target
2984
+ target: link.target,
2985
+ kindLabel: REFERENCE_BROKEN_TEXTS.kindLabels[link.kind] ?? tx(REFERENCE_BROKEN_TEXTS.kindLabelFallback, { kind: link.kind }),
2986
+ where: linkWhere(link, {
2987
+ single: REFERENCE_BROKEN_TEXTS.whereSingle,
2988
+ plural: REFERENCE_BROKEN_TEXTS.wherePlural
2989
+ })
2831
2990
  }),
2832
2991
  data
2833
2992
  };
2834
- if (hintCandidates.length > 0) {
2835
- const suggestedName = (link.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
2836
- const candidatePaths = hintCandidates.map((n) => n.path);
2837
- data["hint"] = {
2838
- kind: "missing-frontmatter-name",
2839
- suggestedName,
2840
- candidates: candidatePaths
2841
- };
2842
- issue.fix = {
2843
- summary: candidatePaths.length === 1 ? tx(REFERENCE_BROKEN_TEXTS.hintSummarySingle, {
2844
- name: suggestedName,
2845
- candidate: candidatePaths[0]
2846
- }) : tx(REFERENCE_BROKEN_TEXTS.hintSummaryMany, {
2847
- name: suggestedName,
2848
- candidates: candidatePaths.join(", ")
2849
- }),
2850
- autofixable: false
2851
- };
2852
- }
2993
+ if (hintCandidates.length > 0) attachHint(issue, data, link, hintCandidates);
2853
2994
  return issue;
2854
2995
  }
2996
+ function attachHint(issue, data, link, hintCandidates) {
2997
+ const suggestedName = (link.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
2998
+ const candidatePaths = hintCandidates.map((n) => n.path);
2999
+ data["hint"] = {
3000
+ kind: "missing-frontmatter-name",
3001
+ suggestedName,
3002
+ candidates: candidatePaths
3003
+ };
3004
+ issue.fix = {
3005
+ summary: candidatePaths.length === 1 ? tx(REFERENCE_BROKEN_TEXTS.hintSummarySingle, {
3006
+ name: suggestedName,
3007
+ candidate: candidatePaths[0]
3008
+ }) : tx(REFERENCE_BROKEN_TEXTS.hintSummaryMany, {
3009
+ name: suggestedName,
3010
+ candidates: candidatePaths.join(", ")
3011
+ }),
3012
+ autofixable: false
3013
+ };
3014
+ }
2855
3015
  function resolvesViaReferencePaths(link, refIndex) {
2856
3016
  if (!isPathStyleLink(link)) return false;
2857
3017
  return refIndex.paths.has(resolve3(refIndex.cwd, link.target));
@@ -2870,8 +3030,8 @@ function indexByNormalizedName(nodes) {
2870
3030
  return out;
2871
3031
  }
2872
3032
  function basenameWithoutExt(path) {
2873
- const base = pathPosix4.basename(path);
2874
- const ext = pathPosix4.extname(base);
3033
+ const base = pathPosix5.basename(path);
3034
+ const ext = pathPosix5.extname(base);
2875
3035
  return ext ? base.slice(0, -ext.length) : base;
2876
3036
  }
2877
3037
  function indexByBasenameWithoutName(nodes) {
@@ -2917,25 +3077,29 @@ function isPathStyleLink(link) {
2917
3077
  // plugins/core/analyzers/reference-redundant/text.ts
2918
3078
  var REFERENCE_REDUNDANT_TEXTS = {
2919
3079
  /**
2920
- * Multi-form / multi-occurrence reference message. Short and direct:
2921
- * names the duplicated target + count and lists each occurrence
2922
- * (trigger + line) so the operator sees the offending spots at a
2923
- * glance. The source node is the finding's own node, so it is not
2924
- * repeated here.
3080
+ * Compact finding grammar (subject first, `\n` renders as a line
3081
+ * break in the inspector and flattens to a space in `sm check`):
3082
+ *
3083
+ * <resolvedTarget>:
3084
+ * Duplicate reference (2): `references/x.md` (124, 145).
3085
+ *
3086
+ * Occurrences are grouped BY TRIGGER: each distinct trigger text
3087
+ * appears once with its line numbers collapsed into one paren list.
3088
+ * The source node is the finding's own node, so it never appears.
2925
3089
  */
2926
- message: "Duplicate reference to {{resolvedTarget}} ({{count}} occurrences): {{occurrences}}.",
2927
- /** Inline separator between occurrences in the message. */
3090
+ message: "{{resolvedTarget}}:\nDuplicate reference ({{count}}): {{occurrences}}.",
3091
+ /** Inline separator between trigger groups in the message. */
2928
3092
  occurrenceSeparator: ", ",
2929
- /** Per-occurrence formatting (trigger + line). */
2930
- occurrence: "`{{trigger}}` ({{kind}}, line {{line}})",
2931
- /** Per-occurrence formatting when the extractor did not record a line. */
2932
- occurrenceUnknownLine: "`{{trigger}}` ({{kind}}, unknown line)"
3093
+ /** Per-trigger formatting: the trigger once, its lines grouped. */
3094
+ occurrence: "`{{trigger}}` ({{lines}})",
3095
+ /** Placeholder for an occurrence whose extractor recorded no line. */
3096
+ lineUnknown: "?"
2933
3097
  };
2934
3098
 
2935
3099
  // plugins/core/analyzers/reference-redundant/index.ts
2936
- var ID21 = "reference-redundant";
3100
+ var ID22 = "reference-redundant";
2937
3101
  var referenceRedundantAnalyzer = {
2938
- id: ID21,
3102
+ id: ID22,
2939
3103
  pluginId: CORE_PLUGIN_ID,
2940
3104
  kind: "analyzer",
2941
3105
  description: "Flags when one node references the same target through two or more different links (e.g. a markdown link plus a `references:` entry).",
@@ -2961,14 +3125,13 @@ var referenceRedundantAnalyzer = {
2961
3125
  const [source, resolvedTarget] = key.split("\0");
2962
3126
  const flat = flattenOccurrences(links);
2963
3127
  issues.push({
2964
- analyzerId: ID21,
2965
- severity: "warn",
3128
+ analyzerId: ID22,
3129
+ severity: "info",
2966
3130
  nodeIds: [source],
2967
3131
  message: tx(REFERENCE_REDUNDANT_TEXTS.message, {
2968
- source,
2969
3132
  resolvedTarget,
2970
3133
  count: flat.length,
2971
- occurrences: flat.map(formatOccurrence).join(REFERENCE_REDUNDANT_TEXTS.occurrenceSeparator)
3134
+ occurrences: formatGroupedOccurrences(flat)
2972
3135
  }),
2973
3136
  data: {
2974
3137
  target: resolvedTarget,
@@ -3015,11 +3178,19 @@ function flattenOccurrences(links) {
3015
3178
  });
3016
3179
  return out;
3017
3180
  }
3018
- function formatOccurrence(occ) {
3019
- if (occ.line === null) {
3020
- return tx(REFERENCE_REDUNDANT_TEXTS.occurrenceUnknownLine, { trigger: occ.originalTrigger, kind: occ.kind });
3021
- }
3022
- return tx(REFERENCE_REDUNDANT_TEXTS.occurrence, { trigger: occ.originalTrigger, kind: occ.kind, line: occ.line });
3181
+ function formatGroupedOccurrences(occurrences) {
3182
+ const byTrigger = /* @__PURE__ */ new Map();
3183
+ for (const occ of occurrences) {
3184
+ const bucket = byTrigger.get(occ.originalTrigger);
3185
+ if (bucket) bucket.push(occ);
3186
+ else byTrigger.set(occ.originalTrigger, [occ]);
3187
+ }
3188
+ return [...byTrigger.entries()].map(
3189
+ ([trigger, occs]) => tx(REFERENCE_REDUNDANT_TEXTS.occurrence, {
3190
+ trigger,
3191
+ lines: occs.map((o) => o.line === null ? REFERENCE_REDUNDANT_TEXTS.lineUnknown : String(o.line)).join(", ")
3192
+ })
3193
+ ).join(REFERENCE_REDUNDANT_TEXTS.occurrenceSeparator);
3023
3194
  }
3024
3195
  function buildNameIndex2(nodes) {
3025
3196
  const out = /* @__PURE__ */ new Map();
@@ -3311,12 +3482,14 @@ function existsSyncSafe(path) {
3311
3482
 
3312
3483
  // plugins/core/analyzers/schema-violation/text.ts
3313
3484
  var SCHEMA_VIOLATION_TEXTS = {
3314
- /** `Node <path> failed schema validation: <errors>` */
3315
- nodeFailure: "Node {{path}} failed schema validation: {{errors}}",
3316
- /** `Link <source> → <target> failed schema validation: <errors>` */
3317
- linkFailure: "Link {{source}} \u2192 {{target}} failed schema validation: {{errors}}",
3318
- /** `Node <path> is missing required frontmatter fields: <missing>` */
3319
- frontmatterBaseFailure: "Node {{path}} is missing required frontmatter fields: {{missing}}.",
3485
+ // Compact finding grammar: the affected node (or the link's source)
3486
+ // is the finding's own node, so its path never appears.
3487
+ /** `Schema validation failed: <errors>` */
3488
+ nodeFailure: "Schema validation failed: {{errors}}",
3489
+ /** `<target>:\nLink failed schema validation: <errors>` */
3490
+ linkFailure: "{{target}}:\nLink failed schema validation: {{errors}}",
3491
+ /** `Missing required frontmatter: <missing>.` */
3492
+ frontmatterBaseFailure: "Missing required frontmatter: {{missing}}.",
3320
3493
  /** Singular tooltip on the alert / chip when a node has exactly one validation failure. */
3321
3494
  alertTooltipSingle: "Frontmatter or schema validation failed.",
3322
3495
  /** Plural tooltip; `{{count}}` capped at 99 in the chip badge but the tooltip text shows the raw count. */
@@ -3324,9 +3497,9 @@ var SCHEMA_VIOLATION_TEXTS = {
3324
3497
  };
3325
3498
 
3326
3499
  // plugins/core/analyzers/schema-violation/index.ts
3327
- var ID22 = "schema-violation";
3500
+ var ID23 = "schema-violation";
3328
3501
  var schemaViolationAnalyzer = {
3329
- id: ID22,
3502
+ id: ID23,
3330
3503
  pluginId: CORE_PLUGIN_ID,
3331
3504
  kind: "analyzer",
3332
3505
  description: "Flags nodes or links that violate the project schemas.",
@@ -3377,7 +3550,7 @@ function collectNodeFindings(v, node, out) {
3377
3550
  const result = v.validate("node", toNodeForSchema(node));
3378
3551
  if (result.ok) return;
3379
3552
  out.push({
3380
- analyzerId: ID22,
3553
+ analyzerId: ID23,
3381
3554
  severity: "error",
3382
3555
  nodeIds: [node.path],
3383
3556
  message: tx(SCHEMA_VIOLATION_TEXTS.nodeFailure, {
@@ -3396,7 +3569,7 @@ function collectFrontmatterBaseFindings(node, out) {
3396
3569
  if (isMissingStringField(fm, "description")) missing.push("description");
3397
3570
  if (missing.length === 0) return;
3398
3571
  out.push({
3399
- analyzerId: ID22,
3572
+ analyzerId: ID23,
3400
3573
  // `warn` (not `error`) so the default `sm scan` exit code stays
3401
3574
  // 0 even when nodes are missing frontmatter base fields. Strict
3402
3575
  // mode (`sm scan --strict`) still escalates to exit 1. Matches
@@ -3418,7 +3591,7 @@ function collectLinkFindings(v, link, out) {
3418
3591
  const result = v.validate("link", toLinkForSchema(link));
3419
3592
  if (result.ok) return;
3420
3593
  out.push({
3421
- analyzerId: ID22,
3594
+ analyzerId: ID23,
3422
3595
  severity: "error",
3423
3596
  nodeIds: [link.source],
3424
3597
  message: tx(SCHEMA_VIOLATION_TEXTS.linkFailure, {
@@ -3473,7 +3646,7 @@ var SIGNAL_COLLISION_TEXTS = {
3473
3646
  * the same paragraph, the markdown-link wins and the at-directive
3474
3647
  * silently disappears without this warning).
3475
3648
  */
3476
- message: "{{loserExtractor}} detected `{{loserRaw}}` at offset {{loserRange}} but {{winnerExtractor}}'s detection at {{winnerRange}} won the overlap collision ({{reason}}). The graph shows the winning edge only; the loser is not persisted.",
3649
+ message: "`{{loserRaw}}`:\nOverlap collision: {{loserExtractor}} (at {{loserRange}}) lost to {{winnerExtractor}} (at {{winnerRange}}) by {{reason}}; only the winning edge persists.",
3477
3650
  /**
3478
3651
  * Same warn but for the rare case the resolver rejected a Signal
3479
3652
  * because the operator disabled its extractor via
@@ -3482,20 +3655,20 @@ var SIGNAL_COLLISION_TEXTS = {
3482
3655
  * resolver; documented now so the analyzer stays forward-compatible
3483
3656
  * with the upcoming filter pass.
3484
3657
  */
3485
- messageExtractorDisabled: "Extension `{{extractorId}}` is disabled; its detection `{{loserRaw}}` (offset {{loserRange}}) did not produce a Link. Re-enable the extension in Settings or via `sm plugins enable` to surface its edges.",
3658
+ messageExtractorDisabled: "`{{loserRaw}}`:\nDropped: extension `{{extractorId}}` is disabled. Re-enable it in Settings or via `sm plugins enable`.",
3486
3659
  /**
3487
3660
  * Same warn but for the future confidence floor case. Phase 4+ stub:
3488
3661
  * today the resolver materialises every winning candidate regardless
3489
3662
  * of confidence, so this template is unreachable; documented for
3490
3663
  * forward compatibility.
3491
3664
  */
3492
- messageBelowFloor: "Detection `{{loserRaw}}` (offset {{loserRange}}, confidence {{confidence}}) fell below the configured threshold {{threshold}} and was dropped."
3665
+ messageBelowFloor: "`{{loserRaw}}`:\nDropped: confidence {{confidence}} is below the threshold {{threshold}}."
3493
3666
  };
3494
3667
 
3495
3668
  // plugins/core/analyzers/signal-collision/index.ts
3496
- var ID23 = "signal-collision";
3669
+ var ID24 = "signal-collision";
3497
3670
  var signalCollisionAnalyzer = {
3498
- id: ID23,
3671
+ id: ID24,
3499
3672
  pluginId: CORE_PLUGIN_ID,
3500
3673
  kind: "analyzer",
3501
3674
  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.",
@@ -3520,7 +3693,7 @@ function makeIssue(signal) {
3520
3693
  const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3521
3694
  const winnerRange = `${winner.range.start}-${winner.range.end}`;
3522
3695
  return {
3523
- analyzerId: ID23,
3696
+ analyzerId: ID24,
3524
3697
  severity: "warn",
3525
3698
  nodeIds: [signal.source],
3526
3699
  message: tx(SIGNAL_COLLISION_TEXTS.message, {
@@ -3553,158 +3726,42 @@ function makeIssue(signal) {
3553
3726
  if (resolution.extractorDisabled) {
3554
3727
  const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3555
3728
  return {
3556
- analyzerId: ID23,
3729
+ analyzerId: ID24,
3557
3730
  severity: "warn",
3558
3731
  nodeIds: [signal.source],
3559
3732
  message: tx(SIGNAL_COLLISION_TEXTS.messageExtractorDisabled, {
3560
3733
  extractorId: resolution.extractorDisabled.extractorId,
3561
3734
  loserRaw: signal.raw,
3562
- loserRange
3563
- }),
3564
- data: {
3565
- extractorDisabled: resolution.extractorDisabled,
3566
- raw: signal.raw,
3567
- range: signal.range ?? null
3568
- }
3569
- };
3570
- }
3571
- if (resolution.belowFloor) {
3572
- const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3573
- const topCandidate = signal.candidates[0];
3574
- return {
3575
- analyzerId: ID23,
3576
- severity: "warn",
3577
- nodeIds: [signal.source],
3578
- message: tx(SIGNAL_COLLISION_TEXTS.messageBelowFloor, {
3579
- loserRaw: signal.raw,
3580
- loserRange,
3581
- confidence: topCandidate.confidence,
3582
- threshold: resolution.belowFloor.threshold
3583
- }),
3584
- data: {
3585
- belowFloor: resolution.belowFloor,
3586
- raw: signal.raw,
3587
- range: signal.range ?? null
3588
- }
3589
- };
3590
- }
3591
- return null;
3592
- }
3593
-
3594
- // plugins/core/analyzers/supersede/text.ts
3595
- var SUPERSEDE_TEXTS = {
3596
- /** Label of the inspector action button that declares supersession. */
3597
- supersedeLabel: "Supersede",
3598
- /** Tooltip shown when the supersede button is disabled (already superseded). */
3599
- supersedeDisabledReason: "Already superseded.",
3600
- /** Tooltip shown when there is no other node to supersede this one. */
3601
- supersedeNoTargetsReason: "No other node to supersede this one.",
3602
- /** Prompt label for the target node-picker (enum-pick over the live node set). */
3603
- supersedePromptLabel: "Superseded by"
3604
- };
3605
-
3606
- // plugins/core/analyzers/supersede/index.ts
3607
- var ID24 = "supersede";
3608
- var supersedeButton = {
3609
- slot: "inspector.action.button",
3610
- priority: 10
3611
- };
3612
- var supersedeAnalyzer = {
3613
- id: ID24,
3614
- pluginId: CORE_PLUGIN_ID,
3615
- kind: "analyzer",
3616
- description: 'Projects the inspector "Supersede" button (declares a node replaced by another).',
3617
- mode: "deterministic",
3618
- ui: { supersedeButton },
3619
- evaluate(ctx) {
3620
- const candidates = ctx.nodes.filter((n) => n.virtual !== true).map((n) => n.path);
3621
- for (const node of ctx.nodes) {
3622
- if (node.virtual === true) continue;
3623
- const options = candidates.filter((p) => p !== node.path).map((p) => ({ value: p, label: p }));
3624
- emitSupersedeButton(ctx, node, options);
3625
- }
3626
- return [];
3627
- }
3628
- };
3629
- function emitSupersedeButton(ctx, node, options) {
3630
- const disabledReason = resolveDisabledReason(node, options.length);
3631
- ctx.emitContribution(node.path, supersedeButton, {
3632
- actionId: "core/node-supersede",
3633
- label: SUPERSEDE_TEXTS.supersedeLabel,
3634
- icon: "pi-arrow-right-arrow-left",
3635
- enabled: disabledReason === void 0,
3636
- ...disabledReason === void 0 ? {} : { disabledReason },
3637
- prompt: {
3638
- inputType: "enum-pick",
3639
- paramKey: "supersededBy",
3640
- label: SUPERSEDE_TEXTS.supersedePromptLabel,
3641
- options
3642
- }
3643
- });
3644
- }
3645
- function resolveDisabledReason(node, optionCount) {
3646
- if (alreadySuperseded(node)) return SUPERSEDE_TEXTS.supersedeDisabledReason;
3647
- if (optionCount === 0) return SUPERSEDE_TEXTS.supersedeNoTargetsReason;
3648
- return void 0;
3649
- }
3650
- function alreadySuperseded(node) {
3651
- const sidecar = node.sidecar;
3652
- if (!sidecar || sidecar.present !== true) return false;
3653
- const ann = sidecar.annotations;
3654
- if (!ann || typeof ann !== "object" || Array.isArray(ann)) return false;
3655
- const value = ann["supersededBy"];
3656
- return typeof value === "string" && value.length > 0;
3657
- }
3658
-
3659
- // plugins/core/analyzers/tags/text.ts
3660
- var TAGS_TEXTS = {
3661
- /** Label of the inspector action button that edits the node's tags. */
3662
- editLabel: "Edit tags",
3663
- /** Prompt label for the string-list tags input. */
3664
- promptLabel: "Tags"
3665
- };
3666
-
3667
- // plugins/core/analyzers/tags/index.ts
3668
- var ID25 = "tags";
3669
- var setTagsButton = {
3670
- slot: "inspector.action.button",
3671
- priority: 15
3672
- };
3673
- var tagsAnalyzer = {
3674
- id: ID25,
3675
- pluginId: CORE_PLUGIN_ID,
3676
- kind: "analyzer",
3677
- description: `Projects the inspector "Edit tags" button (edits a node's taxonomy tags).`,
3678
- mode: "deterministic",
3679
- ui: { setTagsButton },
3680
- evaluate(ctx) {
3681
- for (const node of ctx.nodes) {
3682
- if (node.sidecar?.present !== true) continue;
3683
- emitSetTagsButton(ctx, node);
3684
- }
3685
- return [];
3735
+ loserRange
3736
+ }),
3737
+ data: {
3738
+ extractorDisabled: resolution.extractorDisabled,
3739
+ raw: signal.raw,
3740
+ range: signal.range ?? null
3741
+ }
3742
+ };
3686
3743
  }
3687
- };
3688
- function emitSetTagsButton(ctx, node) {
3689
- ctx.emitContribution(node.path, setTagsButton, {
3690
- actionId: "core/node-set-tags",
3691
- label: TAGS_TEXTS.editLabel,
3692
- icon: "pi-tags",
3693
- enabled: true,
3694
- prompt: {
3695
- inputType: "string-list",
3696
- paramKey: "tags",
3697
- label: TAGS_TEXTS.promptLabel,
3698
- defaultValue: currentTags(node)
3699
- }
3700
- });
3701
- }
3702
- function currentTags(node) {
3703
- const ann = node.sidecar?.annotations;
3704
- if (!ann || typeof ann !== "object" || Array.isArray(ann)) return [];
3705
- const value = ann["tags"];
3706
- if (!Array.isArray(value)) return [];
3707
- return value.filter((t) => typeof t === "string");
3744
+ if (resolution.belowFloor) {
3745
+ const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3746
+ const topCandidate = signal.candidates[0];
3747
+ return {
3748
+ analyzerId: ID24,
3749
+ severity: "warn",
3750
+ nodeIds: [signal.source],
3751
+ message: tx(SIGNAL_COLLISION_TEXTS.messageBelowFloor, {
3752
+ loserRaw: signal.raw,
3753
+ loserRange,
3754
+ confidence: topCandidate.confidence,
3755
+ threshold: resolution.belowFloor.threshold
3756
+ }),
3757
+ data: {
3758
+ belowFloor: resolution.belowFloor,
3759
+ raw: signal.raw,
3760
+ range: signal.range ?? null
3761
+ }
3762
+ };
3763
+ }
3764
+ return null;
3708
3765
  }
3709
3766
 
3710
3767
  // plugins/core/analyzers/trigger-collision/text.ts
@@ -3714,18 +3771,18 @@ var TRIGGER_COLLISION_TEXTS = {
3714
3771
  * cause part. Used for the advertiser-ambiguous-only, invocation-
3715
3772
  * ambiguous-only, and cross-kind-only branches.
3716
3773
  */
3717
- messageOnePart: 'Trigger "{{normalized}}" has {{part}}.',
3774
+ messageOnePart: '"{{normalized}}":\nTrigger collision: {{part}}.',
3718
3775
  /**
3719
3776
  * Top-level message when `analyzeTriggerBucket` accumulated two cause
3720
3777
  * parts (advertiser-ambiguous AND invocation-ambiguous fire together).
3721
3778
  * The joiner lives inside the template so future locales can adapt it
3722
3779
  * (e.g. `'; y '` in Spanish) without touching the rule code.
3723
3780
  */
3724
- messageTwoParts: 'Trigger "{{normalized}}" has {{first}}; and {{second}}.',
3725
- /** `<n> nodes advertise it: <list>` part, fires on the advertiser-ambiguous branch. */
3726
- partAdvertisers: "{{count}} nodes advertise it: {{paths}}",
3727
- /** `<n> distinct invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
3728
- partInvocations: "{{count}} distinct invocation forms: {{forms}}",
3781
+ messageTwoParts: '"{{normalized}}":\nTrigger collision: {{first}}; and {{second}}.',
3782
+ /** `<n> advertisers: <list>` part, fires on the advertiser-ambiguous branch. */
3783
+ partAdvertisers: "{{count}} advertisers: {{paths}}",
3784
+ /** `<n> invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
3785
+ partInvocations: "{{count}} invocation forms: {{forms}}",
3729
3786
  /** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
3730
3787
  partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
3731
3788
  /** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
@@ -3733,14 +3790,14 @@ var TRIGGER_COLLISION_TEXTS = {
3733
3790
  };
3734
3791
 
3735
3792
  // plugins/core/analyzers/trigger-collision/index.ts
3736
- var ID26 = "trigger-collision";
3793
+ var ID25 = "trigger-collision";
3737
3794
  var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
3738
3795
  "command",
3739
3796
  "skill",
3740
3797
  "agent"
3741
3798
  ]);
3742
3799
  var triggerCollisionAnalyzer = {
3743
- id: ID26,
3800
+ id: ID25,
3744
3801
  pluginId: CORE_PLUGIN_ID,
3745
3802
  kind: "analyzer",
3746
3803
  mode: "deterministic",
@@ -3838,7 +3895,7 @@ function analyzeTriggerBucket(normalized, claims) {
3838
3895
  part: parts[0]
3839
3896
  });
3840
3897
  return {
3841
- analyzerId: ID26,
3898
+ analyzerId: ID25,
3842
3899
  severity: "error",
3843
3900
  nodeIds,
3844
3901
  message,
@@ -3878,13 +3935,13 @@ var ASCII_FORMATTER_TEXTS = {
3878
3935
  };
3879
3936
 
3880
3937
  // plugins/core/formatters/ascii/index.ts
3881
- var ID27 = "ascii";
3938
+ var ID26 = "ascii";
3882
3939
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
3883
3940
  var asciiFormatter = {
3884
- id: ID27,
3941
+ id: ID26,
3885
3942
  pluginId: CORE_PLUGIN_ID,
3886
3943
  kind: "formatter",
3887
- formatId: ID27,
3944
+ formatId: ID26,
3888
3945
  description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
3889
3946
  // ASCII tree formatter, header + per-kind sections + per-issue
3890
3947
  // section. Each section iterates and renders; splitting per section
@@ -3978,13 +4035,13 @@ function renderSection(out, kind, group) {
3978
4035
  }
3979
4036
 
3980
4037
  // plugins/core/formatters/json/index.ts
3981
- var ID28 = "json";
4038
+ var ID27 = "json";
3982
4039
  var jsonFormatter = {
3983
- id: ID28,
4040
+ id: ID27,
3984
4041
  pluginId: CORE_PLUGIN_ID,
3985
4042
  kind: "formatter",
3986
4043
  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`.",
3987
- formatId: ID28,
4044
+ formatId: ID27,
3988
4045
  format(ctx) {
3989
4046
  if (ctx.scanResult !== void 0) {
3990
4047
  return JSON.stringify(ctx.scanResult);
@@ -4122,14 +4179,33 @@ function resolveSpecRoot2() {
4122
4179
  }
4123
4180
  }
4124
4181
 
4182
+ // plugins/core/actions/node-bump/text.ts
4183
+ var BUMP_TEXTS = {
4184
+ /** Label of the inspector action button that dispatches a bump. */
4185
+ bumpLabel: "Bump",
4186
+ /** Tooltip shown when the bump button is disabled (the node is fresh, no drift). */
4187
+ bumpDisabledReason: "No drift to bump."
4188
+ };
4189
+
4125
4190
  // plugins/core/actions/node-bump/index.ts
4126
- var ID29 = "node-bump";
4191
+ var ID28 = "node-bump";
4192
+ var bumpButton = {
4193
+ slot: "inspector.action.button",
4194
+ priority: 10
4195
+ };
4127
4196
  var nodeBumpAction = {
4128
- id: ID29,
4197
+ id: ID28,
4129
4198
  pluginId: CORE_PLUGIN_ID,
4130
4199
  kind: "action",
4131
4200
  description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
4132
4201
  mode: "deterministic",
4202
+ ui: { bumpButton },
4203
+ project(ctx) {
4204
+ for (const node of ctx.nodes) {
4205
+ if (node.sidecar?.present !== true) continue;
4206
+ emitBumpButton(ctx, node.path, staleStatus2(node.sidecar) !== null);
4207
+ }
4208
+ },
4133
4209
  // The runtime contract uses generic <TInput, TReport>; bump narrows
4134
4210
  // both. The cast is the standard pattern for built-ins that want
4135
4211
  // typed local I/O while staying compatible with the open generic.
@@ -4138,6 +4214,20 @@ var nodeBumpAction = {
4138
4214
  return invokeBump(input, ctx);
4139
4215
  }
4140
4216
  };
4217
+ function emitBumpButton(ctx, nodePath, enabled) {
4218
+ ctx.emitContribution(nodePath, bumpButton, {
4219
+ actionId: "core/node-bump",
4220
+ label: BUMP_TEXTS.bumpLabel,
4221
+ icon: "pi-arrow-up-right",
4222
+ enabled,
4223
+ ...enabled ? {} : { disabledReason: BUMP_TEXTS.bumpDisabledReason }
4224
+ });
4225
+ }
4226
+ function staleStatus2(overlay) {
4227
+ const status = overlay?.status;
4228
+ if (status === void 0 || status === null || status === "fresh") return null;
4229
+ return status;
4230
+ }
4141
4231
  function invokeBump(input, ctx) {
4142
4232
  const overlay = ctx.node.sidecar ?? null;
4143
4233
  const isFresh = overlay?.present === true && overlay.status === "fresh";
@@ -4184,9 +4274,9 @@ function pickCurrentVersion(overlay) {
4184
4274
 
4185
4275
  // plugins/core/actions/node-set-stability/index.ts
4186
4276
  var STABILITY_VALUES = ["experimental", "stable", "deprecated"];
4187
- var ID30 = "node-set-stability";
4277
+ var ID29 = "node-set-stability";
4188
4278
  var nodeSetStabilityAction = {
4189
- id: ID30,
4279
+ id: ID29,
4190
4280
  pluginId: CORE_PLUGIN_ID,
4191
4281
  kind: "action",
4192
4282
  description: "Sets the lifecycle stage of the current node (writes `stability` to the sidecar).",
@@ -4225,14 +4315,33 @@ function invokeSetStability(input, ctx) {
4225
4315
  return { report, writes: [write] };
4226
4316
  }
4227
4317
 
4318
+ // plugins/core/actions/node-set-tags/text.ts
4319
+ var TAGS_TEXTS = {
4320
+ /** Label of the inspector action button that edits the node's tags. */
4321
+ editLabel: "Edit tags",
4322
+ /** Prompt label for the string-list tags input. */
4323
+ promptLabel: "Tags"
4324
+ };
4325
+
4228
4326
  // plugins/core/actions/node-set-tags/index.ts
4229
- var ID31 = "node-set-tags";
4327
+ var ID30 = "node-set-tags";
4328
+ var setTagsButton = {
4329
+ slot: "inspector.action.button",
4330
+ priority: 15
4331
+ };
4230
4332
  var nodeSetTagsAction = {
4231
- id: ID31,
4333
+ id: ID30,
4232
4334
  pluginId: CORE_PLUGIN_ID,
4233
4335
  kind: "action",
4234
4336
  description: "Sets the taxonomy tags of the current node (writes `tags` to the sidecar; whole-array replace).",
4235
4337
  mode: "deterministic",
4338
+ ui: { setTagsButton },
4339
+ project(ctx) {
4340
+ for (const node of ctx.nodes) {
4341
+ if (node.sidecar?.present !== true) continue;
4342
+ emitSetTagsButton(ctx, node);
4343
+ }
4344
+ },
4236
4345
  // The runtime contract uses generic <TInput, TReport>; this narrows
4237
4346
  // both. The cast is the standard pattern for built-ins that want
4238
4347
  // typed local I/O while staying compatible with the open generic.
@@ -4241,6 +4350,27 @@ var nodeSetTagsAction = {
4241
4350
  return invokeSetTags(input, ctx);
4242
4351
  }
4243
4352
  };
4353
+ function emitSetTagsButton(ctx, node) {
4354
+ ctx.emitContribution(node.path, setTagsButton, {
4355
+ actionId: "core/node-set-tags",
4356
+ label: TAGS_TEXTS.editLabel,
4357
+ icon: "pi-tags",
4358
+ enabled: true,
4359
+ prompt: {
4360
+ inputType: "string-list",
4361
+ paramKey: "tags",
4362
+ label: TAGS_TEXTS.promptLabel,
4363
+ defaultValue: currentTags(node)
4364
+ }
4365
+ });
4366
+ }
4367
+ function currentTags(node) {
4368
+ const ann = node.sidecar?.annotations;
4369
+ if (!ann || typeof ann !== "object" || Array.isArray(ann)) return [];
4370
+ const value = ann["tags"];
4371
+ if (!Array.isArray(value)) return [];
4372
+ return value.filter((t) => typeof t === "string");
4373
+ }
4244
4374
  function invokeSetTags(input, ctx) {
4245
4375
  const tags = Array.isArray(input.tags) ? input.tags : [];
4246
4376
  const timestamp = ctx.now().toISOString();
@@ -4264,14 +4394,44 @@ function invokeSetTags(input, ctx) {
4264
4394
  return { report, writes: [write] };
4265
4395
  }
4266
4396
 
4397
+ // plugins/core/actions/node-supersede/text.ts
4398
+ var SUPERSEDE_TEXTS = {
4399
+ /** Label of the inspector action button that declares supersession. */
4400
+ supersedeLabel: "Supersede",
4401
+ /** Tooltip shown when the supersede button is disabled (already superseded). */
4402
+ supersedeDisabledReason: "Already superseded.",
4403
+ /** Tooltip shown when there is no other node to supersede this one. */
4404
+ supersedeNoTargetsReason: "No other node to supersede this one.",
4405
+ /** Prompt label for the target node-picker (enum-pick over the live node set). */
4406
+ supersedePromptLabel: "Superseded by"
4407
+ };
4408
+
4267
4409
  // plugins/core/actions/node-supersede/index.ts
4268
- var ID32 = "node-supersede";
4410
+ var ID31 = "node-supersede";
4411
+ var supersedeButton = {
4412
+ slot: "inspector.action.button",
4413
+ priority: 10
4414
+ };
4269
4415
  var nodeSupersedeAction = {
4270
- id: ID32,
4416
+ id: ID31,
4271
4417
  pluginId: CORE_PLUGIN_ID,
4272
4418
  kind: "action",
4273
4419
  description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
4420
+ // Ships disabled by default (the declarer feature is still settling its
4421
+ // node-picker UX). The button self-projection gates as a unit with the
4422
+ // invoke executor: an enabled button pointing at a disabled action
4423
+ // would error on click, so the whole action stays experimental.
4424
+ stability: "experimental",
4274
4425
  mode: "deterministic",
4426
+ ui: { supersedeButton },
4427
+ project(ctx) {
4428
+ const candidates = ctx.nodes.filter((n) => n.virtual !== true).map((n) => n.path);
4429
+ for (const node of ctx.nodes) {
4430
+ if (node.virtual === true) continue;
4431
+ const options = candidates.filter((p) => p !== node.path).map((p) => ({ value: p, label: p }));
4432
+ emitSupersedeButton(ctx, node, options);
4433
+ }
4434
+ },
4275
4435
  // The runtime contract uses generic <TInput, TReport>; supersede
4276
4436
  // narrows both. The cast is the standard pattern for built-ins that
4277
4437
  // want typed local I/O while staying compatible with the open generic.
@@ -4280,6 +4440,35 @@ var nodeSupersedeAction = {
4280
4440
  return invokeSupersede(input, ctx);
4281
4441
  }
4282
4442
  };
4443
+ function emitSupersedeButton(ctx, node, options) {
4444
+ const disabledReason = resolveDisabledReason(node, options.length);
4445
+ ctx.emitContribution(node.path, supersedeButton, {
4446
+ actionId: "core/node-supersede",
4447
+ label: SUPERSEDE_TEXTS.supersedeLabel,
4448
+ icon: "pi-arrow-right-arrow-left",
4449
+ enabled: disabledReason === void 0,
4450
+ ...disabledReason === void 0 ? {} : { disabledReason },
4451
+ prompt: {
4452
+ inputType: "enum-pick",
4453
+ paramKey: "supersededBy",
4454
+ label: SUPERSEDE_TEXTS.supersedePromptLabel,
4455
+ options
4456
+ }
4457
+ });
4458
+ }
4459
+ function resolveDisabledReason(node, optionCount) {
4460
+ if (alreadySuperseded(node)) return SUPERSEDE_TEXTS.supersedeDisabledReason;
4461
+ if (optionCount === 0) return SUPERSEDE_TEXTS.supersedeNoTargetsReason;
4462
+ return void 0;
4463
+ }
4464
+ function alreadySuperseded(node) {
4465
+ const sidecar = node.sidecar;
4466
+ if (!sidecar || sidecar.present !== true) return false;
4467
+ const ann = sidecar.annotations;
4468
+ if (!ann || typeof ann !== "object" || Array.isArray(ann)) return false;
4469
+ const value = ann["supersededBy"];
4470
+ return typeof value === "string" && value.length > 0;
4471
+ }
4283
4472
  function invokeSupersede(input, ctx) {
4284
4473
  const supersededBy = input.supersededBy;
4285
4474
  if (supersededBy === ctx.node.path) {
@@ -4785,15 +4974,16 @@ var updateCheckHook = {
4785
4974
  var claudeProvider2 = { ...claudeProvider, pluginId: "claude", version: VERSION };
4786
4975
  var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude", version: VERSION };
4787
4976
  var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude", version: VERSION };
4977
+ var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "claude", version: VERSION };
4788
4978
  var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity", version: VERSION };
4789
4979
  var openaiProvider2 = { ...openaiProvider, pluginId: "openai", version: VERSION };
4790
4980
  var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills", version: VERSION };
4791
4981
  var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core", version: VERSION };
4792
4982
  var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core", version: VERSION };
4983
+ var backtickPathExtractor2 = { ...backtickPathExtractor, pluginId: "core", version: VERSION };
4793
4984
  var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core", version: VERSION };
4794
4985
  var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core", version: VERSION };
4795
4986
  var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core", version: VERSION };
4796
- var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "core", version: VERSION };
4797
4987
  var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, pluginId: "core", version: VERSION };
4798
4988
  var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core", version: VERSION };
4799
4989
  var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core", version: VERSION };
@@ -4810,8 +5000,6 @@ var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", v
4810
5000
  var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: VERSION };
4811
5001
  var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: VERSION };
4812
5002
  var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: VERSION };
4813
- var supersedeAnalyzer2 = { ...supersedeAnalyzer, pluginId: "core", version: VERSION };
4814
- var tagsAnalyzer2 = { ...tagsAnalyzer, pluginId: "core", version: VERSION };
4815
5003
  var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: VERSION };
4816
5004
  var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: VERSION };
4817
5005
  var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: VERSION };
@@ -4827,7 +5015,8 @@ var builtInPlugins = [
4827
5015
  extensions: [
4828
5016
  claudeProvider2,
4829
5017
  atDirectiveExtractor2,
4830
- slashCommandExtractor2
5018
+ slashCommandExtractor2,
5019
+ toolsCounterExtractor2
4831
5020
  ]
4832
5021
  },
4833
5022
  {
@@ -4857,10 +5046,10 @@ var builtInPlugins = [
4857
5046
  extensions: [
4858
5047
  coreMarkdownProvider2,
4859
5048
  annotationsExtractor2,
5049
+ backtickPathExtractor2,
4860
5050
  externalUrlCounterExtractor2,
4861
5051
  markdownLinkExtractor2,
4862
5052
  mcpToolsExtractor2,
4863
- toolsCounterExtractor2,
4864
5053
  annotationFieldUnknownAnalyzer2,
4865
5054
  annotationOrphanAnalyzer2,
4866
5055
  annotationStaleAnalyzer2,
@@ -4877,8 +5066,6 @@ var builtInPlugins = [
4877
5066
  referenceRedundantAnalyzer2,
4878
5067
  schemaViolationAnalyzer2,
4879
5068
  signalCollisionAnalyzer2,
4880
- supersedeAnalyzer2,
4881
- tagsAnalyzer2,
4882
5069
  triggerCollisionAnalyzer2,
4883
5070
  asciiFormatter2,
4884
5071
  jsonFormatter2,
@@ -6287,7 +6474,7 @@ function resolveSpecRoot3() {
6287
6474
  }
6288
6475
 
6289
6476
  // cli/i18n/bump.texts.ts
6290
- var BUMP_TEXTS = {
6477
+ var BUMP_TEXTS2 = {
6291
6478
  // --- argument validation --------------------------------------------------
6292
6479
  /**
6293
6480
  * §3.1b two-line block. Mutex between the positional <node.path> and
@@ -7784,7 +7971,8 @@ var LINK_KIND_VALUES = Object.freeze([
7784
7971
  "invokes",
7785
7972
  "references",
7786
7973
  "mentions",
7787
- "supersedes"
7974
+ "supersedes",
7975
+ "points"
7788
7976
  ]);
7789
7977
  var SEVERITY_VALUES = Object.freeze([
7790
7978
  "error",
@@ -7977,6 +8165,7 @@ function rowToNode(row) {
7977
8165
  const parsed = JSON.parse(row.externalRefsJson);
7978
8166
  if (Array.isArray(parsed) && parsed.length > 0) node.externalRefs = parsed;
7979
8167
  }
8168
+ if (row.modifiedAtMs !== null) node.modifiedAtMs = row.modifiedAtMs;
7980
8169
  return node;
7981
8170
  }
7982
8171
  function rowToLink(row) {
@@ -8493,7 +8682,10 @@ function nodeToRow(node, scannedAt) {
8493
8682
  // the column stays sparse for nodes whose bodies have no http(s)
8494
8683
  // URLs at all. Round-tripped by `rowToNode` on load.
8495
8684
  externalRefsJson: node.externalRefs && node.externalRefs.length > 0 ? JSON.stringify(node.externalRefs) : null,
8496
- scannedAt
8685
+ scannedAt,
8686
+ // File mtime (Unix ms) from the walker; NULL for virtual / derived
8687
+ // nodes that carry no backing file. Round-tripped by `rowToNode`.
8688
+ modifiedAtMs: node.modifiedAtMs ?? null
8497
8689
  };
8498
8690
  }
8499
8691
  function projectAnnotationColumns(node) {
@@ -9494,10 +9686,10 @@ var BumpCommand = class extends SmCommand {
9494
9686
  );
9495
9687
  if (!persisted) {
9496
9688
  this.printer.error(
9497
- tx(BUMP_TEXTS.nodeNotFound, {
9689
+ tx(BUMP_TEXTS2.nodeNotFound, {
9498
9690
  glyph: ansi.red("\u2715"),
9499
9691
  nodePath: this.nodePath ?? "<pending>",
9500
- hint: ansi.dim(BUMP_TEXTS.nodeNotFoundHint)
9692
+ hint: ansi.dim(BUMP_TEXTS2.nodeNotFoundHint)
9501
9693
  })
9502
9694
  );
9503
9695
  return ExitCode.NotFound;
@@ -9517,27 +9709,27 @@ var BumpCommand = class extends SmCommand {
9517
9709
  const errGlyph = ansi.red("\u2715");
9518
9710
  if (this.pending && this.nodePath !== void 0) {
9519
9711
  this.printer.error(
9520
- tx(BUMP_TEXTS.nodeAndPendingMutex, {
9712
+ tx(BUMP_TEXTS2.nodeAndPendingMutex, {
9521
9713
  glyph: errGlyph,
9522
- hint: ansi.dim(BUMP_TEXTS.nodeAndPendingMutexHint)
9714
+ hint: ansi.dim(BUMP_TEXTS2.nodeAndPendingMutexHint)
9523
9715
  })
9524
9716
  );
9525
9717
  return ExitCode.Error;
9526
9718
  }
9527
9719
  if (!this.pending && this.nodePath === void 0) {
9528
9720
  this.printer.error(
9529
- tx(BUMP_TEXTS.noTargetSpecified, {
9721
+ tx(BUMP_TEXTS2.noTargetSpecified, {
9530
9722
  glyph: errGlyph,
9531
- hint: ansi.dim(BUMP_TEXTS.noTargetSpecifiedHint)
9723
+ hint: ansi.dim(BUMP_TEXTS2.noTargetSpecifiedHint)
9532
9724
  })
9533
9725
  );
9534
9726
  return ExitCode.Error;
9535
9727
  }
9536
9728
  if (this.staged && !this.pending) {
9537
9729
  this.printer.error(
9538
- tx(BUMP_TEXTS.stagedRequiresPending, {
9730
+ tx(BUMP_TEXTS2.stagedRequiresPending, {
9539
9731
  glyph: errGlyph,
9540
- hint: ansi.dim(BUMP_TEXTS.stagedRequiresPendingHint)
9732
+ hint: ansi.dim(BUMP_TEXTS2.stagedRequiresPendingHint)
9541
9733
  })
9542
9734
  );
9543
9735
  return ExitCode.Error;
@@ -9596,10 +9788,10 @@ var BumpCommand = class extends SmCommand {
9596
9788
  const node = nodes.find((n) => n.path === this.nodePath);
9597
9789
  if (!node) {
9598
9790
  this.printer.error(
9599
- tx(BUMP_TEXTS.nodeNotFound, {
9791
+ tx(BUMP_TEXTS2.nodeNotFound, {
9600
9792
  glyph: ansi.red("\u2715"),
9601
9793
  nodePath: this.nodePath,
9602
- hint: ansi.dim(BUMP_TEXTS.nodeNotFoundHint)
9794
+ hint: ansi.dim(BUMP_TEXTS2.nodeNotFoundHint)
9603
9795
  })
9604
9796
  );
9605
9797
  return ExitCode.NotFound;
@@ -9621,9 +9813,9 @@ var BumpCommand = class extends SmCommand {
9621
9813
  const errGlyph = ansi.red("\u2715");
9622
9814
  if (item.status === "error") {
9623
9815
  this.printer.error(
9624
- tx(BUMP_TEXTS.bumpFailed, {
9816
+ tx(BUMP_TEXTS2.bumpFailed, {
9625
9817
  glyph: errGlyph,
9626
- message: tx(BUMP_TEXTS.resolveAbsPathFailed, {
9818
+ message: tx(BUMP_TEXTS2.resolveAbsPathFailed, {
9627
9819
  nodePath: node.path,
9628
9820
  message: item.message
9629
9821
  })
@@ -9633,10 +9825,10 @@ var BumpCommand = class extends SmCommand {
9633
9825
  }
9634
9826
  if (item.status === "refused") {
9635
9827
  this.printer.error(
9636
- tx(BUMP_TEXTS.refusedFresh, {
9828
+ tx(BUMP_TEXTS2.refusedFresh, {
9637
9829
  glyph: errGlyph,
9638
9830
  nodePath: node.path,
9639
- hint: ansi.dim(BUMP_TEXTS.refusedFreshHint)
9831
+ hint: ansi.dim(BUMP_TEXTS2.refusedFreshHint)
9640
9832
  })
9641
9833
  );
9642
9834
  return ExitCode.Error;
@@ -9663,9 +9855,9 @@ var BumpCommand = class extends SmCommand {
9663
9855
  if (applied.error !== void 0) {
9664
9856
  if (applied.error instanceof EConsentRequiredError) throw applied.error;
9665
9857
  this.printer.error(
9666
- tx(BUMP_TEXTS.bumpFailed, {
9858
+ tx(BUMP_TEXTS2.bumpFailed, {
9667
9859
  glyph: ansi.red("\u2715"),
9668
- message: tx(BUMP_TEXTS.storeFailedDetail, {
9860
+ message: tx(BUMP_TEXTS2.storeFailedDetail, {
9669
9861
  path: applied.sidecarPath ?? sidecarPathFor(item.absPath),
9670
9862
  message: formatErrorMessage(applied.error)
9671
9863
  })
@@ -9682,11 +9874,11 @@ var BumpCommand = class extends SmCommand {
9682
9874
  const version = item.report.version ?? 1;
9683
9875
  if (item.report.createdSidecar === true) {
9684
9876
  this.printer.data(
9685
- tx(BUMP_TEXTS.bumpedCreated, { glyph: okGlyph, sidecarPath, nodePath: node.path, version })
9877
+ tx(BUMP_TEXTS2.bumpedCreated, { glyph: okGlyph, sidecarPath, nodePath: node.path, version })
9686
9878
  );
9687
9879
  } else {
9688
9880
  this.printer.data(
9689
- tx(BUMP_TEXTS.bumped, { glyph: okGlyph, nodePath: node.path, version })
9881
+ tx(BUMP_TEXTS2.bumped, { glyph: okGlyph, nodePath: node.path, version })
9690
9882
  );
9691
9883
  }
9692
9884
  return ExitCode.Ok;
@@ -9698,7 +9890,7 @@ var BumpCommand = class extends SmCommand {
9698
9890
  const stale = nodes.filter((n) => n.sidecar?.present === true && n.sidecar.status !== null && n.sidecar.status !== "fresh").sort((a, b) => a.path.localeCompare(b.path));
9699
9891
  if (stale.length === 0) return this.#renderEmptyPending();
9700
9892
  if (!this.json) {
9701
- this.printer.info(tx(BUMP_TEXTS.pendingBanner, { count: stale.length }));
9893
+ this.printer.info(tx(BUMP_TEXTS2.pendingBanner, { count: stale.length }));
9702
9894
  }
9703
9895
  const plan = computeBumpPlan(stale, { cwd, force: this.force });
9704
9896
  const outcomes = await this.#executePending(plan, cwd, ansi);
@@ -9716,19 +9908,19 @@ var BumpCommand = class extends SmCommand {
9716
9908
  const gitOk = ensureGitForStaged(cwd);
9717
9909
  if (gitOk === "no-repo") {
9718
9910
  this.printer.error(
9719
- tx(BUMP_TEXTS.notInGitRepo, {
9911
+ tx(BUMP_TEXTS2.notInGitRepo, {
9720
9912
  glyph: errGlyph,
9721
9913
  cwd,
9722
- hint: ansi.dim(BUMP_TEXTS.notInGitRepoHint)
9914
+ hint: ansi.dim(BUMP_TEXTS2.notInGitRepoHint)
9723
9915
  })
9724
9916
  );
9725
9917
  return ExitCode.NotFound;
9726
9918
  }
9727
9919
  if (gitOk === "no-binary") {
9728
9920
  this.printer.error(
9729
- tx(BUMP_TEXTS.gitBinaryMissing, {
9921
+ tx(BUMP_TEXTS2.gitBinaryMissing, {
9730
9922
  glyph: errGlyph,
9731
- hint: ansi.dim(BUMP_TEXTS.gitBinaryMissingHint)
9923
+ hint: ansi.dim(BUMP_TEXTS2.gitBinaryMissingHint)
9732
9924
  })
9733
9925
  );
9734
9926
  return ExitCode.Error;
@@ -9766,7 +9958,7 @@ var BumpCommand = class extends SmCommand {
9766
9958
  return {
9767
9959
  nodePath: item.nodePath,
9768
9960
  status: "error",
9769
- message: tx(BUMP_TEXTS.storeFailedDetail, {
9961
+ message: tx(BUMP_TEXTS2.storeFailedDetail, {
9770
9962
  path: applied.sidecarPath ?? sidecarPathFor(item.absPath),
9771
9963
  message: formatErrorMessage(applied.error)
9772
9964
  })
@@ -9786,11 +9978,11 @@ var BumpCommand = class extends SmCommand {
9786
9978
  const addErr = stageSidecar(cwd, sidecarPath);
9787
9979
  if (addErr === null || this.json) return;
9788
9980
  this.printer.warn(
9789
- tx(BUMP_TEXTS.gitAddFailed, {
9981
+ tx(BUMP_TEXTS2.gitAddFailed, {
9790
9982
  glyph: ansi.yellow("\u26A0"),
9791
9983
  path: sidecarPath,
9792
9984
  message: addErr,
9793
- hint: ansi.dim(tx(BUMP_TEXTS.gitAddFailedHint, { path: sidecarPath }))
9985
+ hint: ansi.dim(tx(BUMP_TEXTS2.gitAddFailedHint, { path: sidecarPath }))
9794
9986
  })
9795
9987
  );
9796
9988
  }
@@ -9810,7 +10002,7 @@ var BumpCommand = class extends SmCommand {
9810
10002
  this.printer.data(JSON.stringify(empty) + "\n");
9811
10003
  return ExitCode.Ok;
9812
10004
  }
9813
- this.printer.data(BUMP_TEXTS.pendingNone);
10005
+ this.printer.data(BUMP_TEXTS2.pendingNone);
9814
10006
  return ExitCode.Ok;
9815
10007
  }
9816
10008
  // Complexity is from per-status rendering (4 status values) plus
@@ -9841,24 +10033,24 @@ var BumpCommand = class extends SmCommand {
9841
10033
  for (const o of outcomes) {
9842
10034
  if (o.status === "bumped") {
9843
10035
  this.printer.data(
9844
- tx(BUMP_TEXTS.bumpedItem, {
10036
+ tx(BUMP_TEXTS2.bumpedItem, {
9845
10037
  nodePath: o.nodePath,
9846
10038
  version: o.version ?? 0,
9847
10039
  createdSuffix: o.createdSidecar === true ? " (new sidecar)" : ""
9848
10040
  })
9849
10041
  );
9850
10042
  } else if (o.status === "refused") {
9851
- this.printer.data(tx(BUMP_TEXTS.refusedItem, { nodePath: o.nodePath }));
10043
+ this.printer.data(tx(BUMP_TEXTS2.refusedItem, { nodePath: o.nodePath }));
9852
10044
  } else if (o.status === "skipped") {
9853
10045
  this.printer.data(
9854
- tx(BUMP_TEXTS.skippedItem, {
10046
+ tx(BUMP_TEXTS2.skippedItem, {
9855
10047
  nodePath: o.nodePath,
9856
10048
  reason: o.reason ?? "unknown"
9857
10049
  })
9858
10050
  );
9859
10051
  } else {
9860
10052
  this.printer.data(
9861
- tx(BUMP_TEXTS.errorItem, {
10053
+ tx(BUMP_TEXTS2.errorItem, {
9862
10054
  nodePath: o.nodePath,
9863
10055
  message: o.message ?? ""
9864
10056
  })
@@ -9866,7 +10058,7 @@ var BumpCommand = class extends SmCommand {
9866
10058
  }
9867
10059
  }
9868
10060
  this.printer.info(
9869
- tx(BUMP_TEXTS.pendingSummary, {
10061
+ tx(BUMP_TEXTS2.pendingSummary, {
9870
10062
  bumped: counts.bumped,
9871
10063
  refused: counts.refused,
9872
10064
  skipped: counts.skipped,
@@ -10802,11 +10994,13 @@ var PluginLoader = class {
10802
10994
  if (kind === "provider" && discoveredKinds) {
10803
10995
  instance["kinds"] = discoveredKinds;
10804
10996
  }
10997
+ const stability = exported["stability"];
10805
10998
  return { ok: true, extension: {
10806
10999
  kind,
10807
11000
  id: pathId2,
10808
11001
  pluginId,
10809
11002
  version: exported["version"],
11003
+ ...stability !== void 0 ? { stability } : {},
10810
11004
  entryPath: abs,
10811
11005
  module: mod,
10812
11006
  instance
@@ -10917,29 +11111,39 @@ function isPluginLocked(idOrQualified) {
10917
11111
  }
10918
11112
 
10919
11113
  // kernel/config/plugin-resolver.ts
10920
- function resolvePluginEnabled(pluginId, cfg, dbOverrides) {
11114
+ var SHIPS_DISABLED = /* @__PURE__ */ new Set([
11115
+ "experimental",
11116
+ "deprecated"
11117
+ ]);
11118
+ function installedDefaultEnabled(stability) {
11119
+ return stability === void 0 || !SHIPS_DISABLED.has(stability);
11120
+ }
11121
+ function resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault = true) {
10921
11122
  if (isPluginLocked(pluginId)) return true;
10922
11123
  if (dbOverrides.has(pluginId)) return dbOverrides.get(pluginId) === true;
10923
11124
  const settingsEntry = cfg.plugins[pluginId];
10924
11125
  if (settingsEntry?.enabled !== void 0) return settingsEntry.enabled;
10925
- return true;
11126
+ return installedDefault;
10926
11127
  }
10927
11128
  function makeEnabledResolver(cfg, dbOverrides) {
10928
- return (pluginId) => resolvePluginEnabled(pluginId, cfg, dbOverrides);
11129
+ return (pluginId, installedDefault) => resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault);
10929
11130
  }
10930
11131
 
10931
11132
  // core/runtime/plugin-runtime/resolver.ts
10932
- function defaultResolveEnabled(_id) {
10933
- return true;
11133
+ function defaultResolveEnabled(_id, installedDefault = true) {
11134
+ return installedDefault;
10934
11135
  }
10935
11136
  function isBuiltInExtensionEnabled(plugin, ext, resolveEnabled) {
10936
- return isPluginEntryEnabled(plugin, ext.id, resolveEnabled);
11137
+ return isPluginEntryEnabled(plugin, ext.id, resolveEnabled, ext.stability);
10937
11138
  }
10938
- function isPluginEntryEnabled(plugin, extId, resolveEnabled) {
10939
- return resolveEnabled(qualifiedExtensionId(plugin.id, extId));
11139
+ function isPluginEntryEnabled(plugin, extId, resolveEnabled, stability) {
11140
+ return resolveEnabled(qualifiedExtensionId(plugin.id, extId), installedDefaultEnabled(stability));
10940
11141
  }
10941
11142
  function isPluginExtensionEnabled(ext, resolveEnabled) {
10942
- return resolveEnabled(qualifiedExtensionId(ext.pluginId, ext.id));
11143
+ return resolveEnabled(
11144
+ qualifiedExtensionId(ext.pluginId, ext.id),
11145
+ installedDefaultEnabled(ext.stability)
11146
+ );
10943
11147
  }
10944
11148
  async function buildEnabledResolver(ctx) {
10945
11149
  const { effective: cfg } = loadConfig({ ...ctx });
@@ -11137,11 +11341,11 @@ async function* walkContent(roots, options) {
11137
11341
  const extensions = options.extensions;
11138
11342
  const sizeLimit = buildSizeLimit(options);
11139
11343
  for (const root of roots) {
11140
- for await (const file of walkRoot(root, root, filter, extensions, sizeLimit)) {
11141
- const relPath = relative2(root, file).split(sep3).join("/");
11344
+ for await (const entry of walkRoot(root, root, filter, extensions, sizeLimit)) {
11345
+ const relPath = relative2(root, entry.full).split(sep3).join("/");
11142
11346
  let raw;
11143
11347
  try {
11144
- raw = await readFile(file, "utf8");
11348
+ raw = await readFile(entry.full, "utf8");
11145
11349
  } catch {
11146
11350
  continue;
11147
11351
  }
@@ -11151,6 +11355,9 @@ async function* walkContent(roots, options) {
11151
11355
  body: parsed.body,
11152
11356
  frontmatterRaw: parsed.frontmatterRaw,
11153
11357
  frontmatter: parsed.frontmatter,
11358
+ // File mtime from the TOCTOU `lstat` (zero extra syscalls).
11359
+ // Threaded onto the persisted `Node` as `modifiedAtMs`.
11360
+ modifiedAtMs: entry.modifiedAtMs,
11154
11361
  // Audit L1: forward parser diagnostics (e.g. malformed YAML)
11155
11362
  // through the IRawNode surface so the orchestrator can
11156
11363
  // convert them into warn-level kernel `Issue` rows. Omitted
@@ -11191,7 +11398,7 @@ async function* walkRoot(root, current, filter, extensions, sizeLimit) {
11191
11398
  sizeLimit.onOversizedFile?.({ path: rel, bytes: s.size });
11192
11399
  continue;
11193
11400
  }
11194
- yield full;
11401
+ yield { full, modifiedAtMs: Math.round(s.mtimeMs) };
11195
11402
  } catch {
11196
11403
  }
11197
11404
  }
@@ -11273,8 +11480,8 @@ function bucketLoaded(loaded, runtime, pluginOrder) {
11273
11480
  extractor: runtime.extensions.extractors,
11274
11481
  analyzer: runtime.extensions.analyzers,
11275
11482
  formatter: runtime.extensions.formatters,
11276
- hook: runtime.extensions.hooks
11277
- // `action` intentionally absent, see docstring.
11483
+ hook: runtime.extensions.hooks,
11484
+ action: runtime.extensions.actions
11278
11485
  });
11279
11486
  runtime.manifests.push({
11280
11487
  id: ext.id,
@@ -11389,7 +11596,7 @@ async function loadPluginRuntime(opts = {}) {
11389
11596
  const loader = createPluginLoader(loaderOpts);
11390
11597
  const discovered = await loader.discoverAndLoadAll();
11391
11598
  const runtime = {
11392
- extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [] },
11599
+ extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [], actions: [] },
11393
11600
  annotationContributions: [],
11394
11601
  viewContributions: [],
11395
11602
  manifests: [],
@@ -11444,7 +11651,7 @@ function enforceRootExclusivity(catalog) {
11444
11651
  }
11445
11652
  function emptyPluginRuntime() {
11446
11653
  const runtime = {
11447
- extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [] },
11654
+ extensions: { providers: [], extractors: [], analyzers: [], formatters: [], hooks: [], actions: [] },
11448
11655
  annotationContributions: [],
11449
11656
  viewContributions: [],
11450
11657
  manifests: [],
@@ -11462,23 +11669,26 @@ function emptyPluginRuntime() {
11462
11669
  function collectRegisteredContributionKeys(composed) {
11463
11670
  const keys = /* @__PURE__ */ new Set();
11464
11671
  if (!composed) return keys;
11465
- for (const ext of [...composed.extractors, ...composed.analyzers]) {
11466
- const raw = ext.ui;
11467
- if (typeof raw !== "object" || raw === null) continue;
11468
- for (const [contributionId, value] of Object.entries(raw)) {
11469
- if (typeof value !== "object" || value === null) continue;
11470
- keys.add(`${ext.pluginId}/${ext.id}/${contributionId}`);
11471
- }
11672
+ for (const ext of [...composed.extractors, ...composed.analyzers, ...composed.actions ?? []]) {
11673
+ addContributionKeysForExtension(ext, keys);
11472
11674
  }
11473
11675
  return keys;
11474
11676
  }
11677
+ function addContributionKeysForExtension(ext, keys) {
11678
+ const raw = ext.ui;
11679
+ if (typeof raw !== "object" || raw === null) return;
11680
+ for (const [contributionId, value] of Object.entries(raw)) {
11681
+ if (typeof value !== "object" || value === null) continue;
11682
+ keys.add(`${ext.pluginId}/${ext.id}/${contributionId}`);
11683
+ }
11684
+ }
11475
11685
  function filterBuiltInManifests(manifests, resolveEnabled) {
11476
11686
  const pluginById = /* @__PURE__ */ new Map();
11477
11687
  for (const plugin of builtInPlugins) pluginById.set(plugin.id, plugin);
11478
11688
  return manifests.filter((m) => {
11479
11689
  const plugin = pluginById.get(m.pluginId);
11480
11690
  if (!plugin) return true;
11481
- return isPluginEntryEnabled(plugin, m.id, resolveEnabled);
11691
+ return isPluginEntryEnabled(plugin, m.id, resolveEnabled, m.stability);
11482
11692
  });
11483
11693
  }
11484
11694
 
@@ -11489,9 +11699,10 @@ function composeScanExtensions(opts) {
11489
11699
  const extractors = [];
11490
11700
  const analyzers = [];
11491
11701
  const hooks = [];
11702
+ const actions = [];
11492
11703
  if (!opts.noBuiltIns) {
11493
11704
  accumulateBuiltInScanExtensions(
11494
- { providers, extractors, analyzers, hooks },
11705
+ { providers, extractors, analyzers, hooks, actions },
11495
11706
  resolveEnabled
11496
11707
  );
11497
11708
  }
@@ -11507,6 +11718,9 @@ function composeScanExtensions(opts) {
11507
11718
  for (const ext of opts.pluginRuntime.extensions.hooks) {
11508
11719
  if (isPluginExtensionEnabled(ext, resolveEnabled)) hooks.push(ext);
11509
11720
  }
11721
+ for (const ext of opts.pluginRuntime.extensions.actions) {
11722
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) actions.push(ext);
11723
+ }
11510
11724
  const finalProviders = opts.killSwitches?.providers === true ? [] : providers;
11511
11725
  const finalExtractors = opts.killSwitches?.extractors === true ? [] : extractors;
11512
11726
  const finalAnalyzers = opts.killSwitches?.analyzers === true ? [] : analyzers;
@@ -11517,7 +11731,8 @@ function composeScanExtensions(opts) {
11517
11731
  providers: finalProviders,
11518
11732
  extractors: finalExtractors,
11519
11733
  analyzers: finalAnalyzers,
11520
- hooks
11734
+ hooks,
11735
+ actions
11521
11736
  };
11522
11737
  }
11523
11738
  function accumulateBuiltInScanExtensions(buckets, resolveEnabled) {
@@ -11538,6 +11753,7 @@ function accumulateBuiltInScanExtensions(buckets, resolveEnabled) {
11538
11753
  buckets.hooks.push(ext);
11539
11754
  break;
11540
11755
  case "action":
11756
+ buckets.actions.push(ext);
11541
11757
  break;
11542
11758
  case "formatter":
11543
11759
  break;
@@ -11796,7 +12012,7 @@ function renderHuman(issues, ansi) {
11796
12012
  tx(CHECK_TEXTS.issueRow, {
11797
12013
  glyph: severityGlyph(row.severity, ansi),
11798
12014
  analyzerId: ansi.dim(row.analyzerId.padEnd(analyzerWidth)),
11799
- message: trimRedundantPath(row.message, row.primary)
12015
+ message: flattenMessage(trimRedundantPath(row.message, row.primary))
11800
12016
  })
11801
12017
  );
11802
12018
  }
@@ -11850,6 +12066,9 @@ function trimRedundantPath(message, primary) {
11850
12066
  if (!message.includes(needle)) return message;
11851
12067
  return message.replace(needle, "");
11852
12068
  }
12069
+ function flattenMessage(message) {
12070
+ return message.replace(/\n+/g, " ");
12071
+ }
11853
12072
 
11854
12073
  // cli/commands/config.ts
11855
12074
  import { existsSync as existsSync16 } from "fs";
@@ -15823,8 +16042,9 @@ function buildVirtualNode(extractor, emitted, emitter) {
15823
16042
  if (emitted.frontmatter) node.frontmatter = emitted.frontmatter;
15824
16043
  return node;
15825
16044
  }
16045
+ var KNOWN_LINK_KINDS = ["invokes", "references", "mentions", "supersedes", "points"];
15826
16046
  function validateLink(extractor, link, emitter) {
15827
- const knownKinds = ["invokes", "references", "mentions", "supersedes"];
16047
+ const knownKinds = KNOWN_LINK_KINDS;
15828
16048
  if (!knownKinds.includes(link.kind)) {
15829
16049
  const qualifiedId2 = `${extractor.pluginId}/${extractor.id}`;
15830
16050
  emitter.emit(
@@ -15859,7 +16079,6 @@ function validateLink(extractor, link, emitter) {
15859
16079
  const confidence = c ?? ConfidenceTier.MEDIUM;
15860
16080
  return { ...link, confidence };
15861
16081
  }
15862
- var KNOWN_LINK_KINDS = ["invokes", "references", "mentions", "supersedes"];
15863
16082
  function validateSignal(extractor, signal, emitter) {
15864
16083
  const qualifiedId2 = qualifiedExtensionId(extractor.pluginId, extractor.id);
15865
16084
  if (!Array.isArray(signal.candidates) || signal.candidates.length === 0) {
@@ -15978,12 +16197,87 @@ function recomputeExternalRefsCount(nodes, externalLinks, cachedPaths) {
15978
16197
  });
15979
16198
  source.externalRefs = refs;
15980
16199
  }
15981
- }
15982
- var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
15983
- var VIRTUAL_NODE_SCHEME_RE = /^mcp:\/\//i;
15984
- function isExternalUrlLink(link) {
15985
- if (VIRTUAL_NODE_SCHEME_RE.test(link.target)) return false;
15986
- return EXTERNAL_URL_SCHEME_RE.test(link.target);
16200
+ }
16201
+ var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
16202
+ var VIRTUAL_NODE_SCHEME_RE = /^mcp:\/\//i;
16203
+ function isExternalUrlLink(link) {
16204
+ if (VIRTUAL_NODE_SCHEME_RE.test(link.target)) return false;
16205
+ return EXTERNAL_URL_SCHEME_RE.test(link.target);
16206
+ }
16207
+
16208
+ // kernel/orchestrator/action-projections.ts
16209
+ function runActionProjections(actions, nodes, links, emitter) {
16210
+ const contributions = [];
16211
+ const contributionErrors = [];
16212
+ const validators = loadSchemaValidators();
16213
+ for (const action of actions) {
16214
+ if (typeof action.project !== "function") continue;
16215
+ const qualifiedId2 = qualifiedExtensionId(action.pluginId, action.id);
16216
+ const declaredContributions = readDeclaredContributionRefs(action);
16217
+ const emitContribution = (nodePath, ref, payload) => {
16218
+ const declared = typeof ref === "object" && ref !== null ? declaredContributions.get(ref) : void 0;
16219
+ if (!declared) {
16220
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUndeclaredRef, {
16221
+ extractorId: qualifiedId2,
16222
+ nodePath
16223
+ });
16224
+ emitExtensionError(emitter, qualifiedId2, nodePath, {
16225
+ phase: "emitContribution",
16226
+ reason: "undeclared-contribution-ref",
16227
+ message
16228
+ });
16229
+ contributionErrors.push({
16230
+ pluginId: action.pluginId,
16231
+ extensionId: action.id,
16232
+ nodePath,
16233
+ reason: "undeclared-contribution-ref",
16234
+ message,
16235
+ emittedAt: Date.now()
16236
+ });
16237
+ return;
16238
+ }
16239
+ const result = validators.validateContributionPayload(declared.slot, payload);
16240
+ if (!result.ok) {
16241
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
16242
+ extractorId: qualifiedId2,
16243
+ contributionId: declared.id,
16244
+ nodePath,
16245
+ slot: declared.slot,
16246
+ errors: result.errors
16247
+ });
16248
+ emitExtensionError(emitter, qualifiedId2, nodePath, {
16249
+ phase: "emitContribution",
16250
+ contributionId: declared.id,
16251
+ slot: declared.slot,
16252
+ reason: result.errors,
16253
+ message
16254
+ });
16255
+ contributionErrors.push({
16256
+ pluginId: action.pluginId,
16257
+ extensionId: action.id,
16258
+ nodePath,
16259
+ reason: result.errors,
16260
+ message,
16261
+ contributionId: declared.id,
16262
+ slot: declared.slot,
16263
+ emittedAt: Date.now()
16264
+ });
16265
+ return;
16266
+ }
16267
+ contributions.push({
16268
+ pluginId: action.pluginId,
16269
+ extensionId: action.id,
16270
+ nodePath,
16271
+ contributionId: declared.id,
16272
+ slot: declared.slot,
16273
+ payload,
16274
+ emittedAt: Date.now()
16275
+ });
16276
+ };
16277
+ const ctx = { nodes, links, emitContribution };
16278
+ action.project(ctx);
16279
+ }
16280
+ return { contributions, contributionErrors };
15987
16281
  }
15988
16282
 
15989
16283
  // kernel/orchestrator/analyzers.ts
@@ -16775,6 +17069,7 @@ function buildNode(args2) {
16775
17069
  externalRefsCount: 0,
16776
17070
  frontmatter: args2.frontmatter
16777
17071
  };
17072
+ if (args2.modifiedAtMs !== void 0) node.modifiedAtMs = args2.modifiedAtMs;
16778
17073
  if (args2.encoder) {
16779
17074
  node.tokens = countTokens(args2.encoder, args2.frontmatterRaw, args2.body);
16780
17075
  }
@@ -16890,7 +17185,10 @@ function buildFreshNodeAndValidateFrontmatter(opts) {
16890
17185
  frontmatter: opts.raw.frontmatter,
16891
17186
  bodyHash: opts.bodyHash,
16892
17187
  frontmatterHash: opts.frontmatterHash,
16893
- encoder: opts.encoder
17188
+ encoder: opts.encoder,
17189
+ // Thread the walker's mtime through; `buildNode` only attaches it
17190
+ // when present, so virtual / walk()-without-stat sources stay absent.
17191
+ modifiedAtMs: opts.raw.modifiedAtMs
16894
17192
  });
16895
17193
  const frontmatterIssues = [];
16896
17194
  if (opts.raw.parseIssues && opts.raw.parseIssues.length > 0) {
@@ -17324,6 +17622,13 @@ async function runScanInternal(_kernel, options) {
17324
17622
  walked.frontmatterIssues
17325
17623
  );
17326
17624
  mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);
17625
+ const projectionResult = runActionProjections(
17626
+ exts.actions ?? [],
17627
+ walked.nodes,
17628
+ walked.internalLinks,
17629
+ emitter
17630
+ );
17631
+ mergeActionProjections(walked, projectionResult, exts.actions);
17327
17632
  const issues = analyzerResult.issues;
17328
17633
  const silenced = options.ignoreFilter ? (path) => options.ignoreFilter.ignores(path) : void 0;
17329
17634
  const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues, silenced) : [];
@@ -17432,6 +17737,16 @@ function mergeAnalyzerEmissions(walked, analyzerResult, analyzers) {
17432
17737
  }
17433
17738
  }
17434
17739
  }
17740
+ function mergeActionProjections(walked, projectionResult, actions) {
17741
+ for (const c of projectionResult.contributions) walked.contributions.push(c);
17742
+ for (const e of projectionResult.contributionErrors) walked.contributionErrors.push(e);
17743
+ for (const action of actions ?? []) {
17744
+ if (action.ui === void 0 || typeof action.project !== "function") continue;
17745
+ for (const node of walked.nodes) {
17746
+ walked.freshlyRunTuples.add(`${action.pluginId}\0${action.id}\0${node.path}`);
17747
+ }
17748
+ }
17749
+ }
17435
17750
  function buildScanStats(walked, issues, start) {
17436
17751
  return {
17437
17752
  // `filesSkipped` is "files walked but not classified by any
@@ -20336,6 +20651,17 @@ var PLUGINS_TEXTS = {
20336
20651
  qualifiedIdNotFoundHint: "Run `sm plugins list` to see what each plugin ships.",
20337
20652
  qualifiedIdUnknownPlugin: "{{glyph}} Qualified extension id references unknown plugin: {{pluginId}}\n {{hint}}\n",
20338
20653
  qualifiedIdUnknownPluginHint: "Run `sm plugins list` for known plugin ids.",
20654
+ // --- verb-shape redirects (show is extension-only; list is plugin-only) ---
20655
+ // `sm plugins show` takes a qualified `<plugin>/<ext>` id and renders a
20656
+ // single extension. A bare plugin id is the wrong granularity, redirect
20657
+ // to `sm plugins list <id>`, which renders the whole plugin.
20658
+ showBareId: '{{glyph}} `sm plugins show` needs a qualified `<plugin>/<ext>` id; "{{id}}" is a plugin.\n {{hint}}\n',
20659
+ showBareIdHint: "Run `sm plugins list {{id}}` for the plugin and its extensions, then `sm plugins show {{id}}/<ext>` for one.",
20660
+ // `sm plugins list <id>` takes a bare plugin id. A qualified
20661
+ // `<plugin>/<ext>` id targets a single extension, redirect to
20662
+ // `sm plugins show`.
20663
+ listQualifiedId: "{{glyph}} `sm plugins list` takes a plugin id, not a qualified `<plugin>/<ext>` id: {{id}}\n {{hint}}\n",
20664
+ listQualifiedIdHint: "Run `sm plugins show {{id}}` for that extension, or `sm plugins list {{pluginId}}` for the whole plugin.",
20339
20665
  // Spec § A.10, `applicableKinds` filter on Extractors. When an extractor
20340
20666
  // declares a kind that no installed Provider emits, the load succeeds
20341
20667
  // (the Provider may arrive later) but `sm plugins doctor` surfaces a
@@ -20469,7 +20795,14 @@ var PLUGINS_TEXTS = {
20469
20795
  * the visible output stay in sync.
20470
20796
  */
20471
20797
  pluginSubIndent: " ",
20472
- listTipShow: "\nTip: `sm plugins show <id>` for kinds, versions, and per-extension status.\n",
20798
+ /**
20799
+ * Lifecycle tag appended to an extension name in list / show rows
20800
+ * when the manifest declares a non-default `stability` (anything but
20801
+ * `stable`). Inherits the surrounding line's color; `stable`
20802
+ * (declared or defaulted) renders no tag.
20803
+ */
20804
+ stabilityTag: " ({{stability}})",
20805
+ listTipShow: "\nTip: `sm plugins list <id>` for a plugin's extensions (kinds, versions, per-extension status), `sm plugins show <plugin>/<ext>` for one extension.\n",
20473
20806
  /** Show command, built-in header (no version row, no path). */
20474
20807
  detailHeaderBuiltIn: " {{glyph}} {{id}} {{source}} {{count}} extension{{plural}}\n",
20475
20808
  /**
@@ -20621,18 +20954,60 @@ function extensionRowFromBuiltIn(ext, plugin, resolveEnabled) {
20621
20954
  id: ext.id,
20622
20955
  kind: ext.kind,
20623
20956
  version: ext.version,
20624
- enabled: resolveEnabled(qualifiedExtensionId(plugin.id, ext.id)),
20957
+ enabled: resolveEnabled(qualifiedExtensionId(plugin.id, ext.id), installedDefaultEnabled(ext.stability)),
20625
20958
  description: ext.description ?? ""
20626
20959
  };
20960
+ if (ext.stability !== void 0) row.stability = ext.stability;
20627
20961
  if (ext.entry !== void 0) row.entry = ext.entry;
20628
20962
  return row;
20629
20963
  }
20964
+ function withStabilityTag(name, stability) {
20965
+ if (!stability || stability === "stable") return name;
20966
+ return name + tx(PLUGINS_TEXTS.stabilityTag, { stability: sanitizeForTerminal(stability) });
20967
+ }
20630
20968
  function omitModule(key, value) {
20631
20969
  if (key !== "module") return value;
20632
20970
  if (value === null || typeof value !== "object") return value;
20633
20971
  const tag = value[Symbol.toStringTag];
20634
20972
  return tag === "Module" ? void 0 : value;
20635
20973
  }
20974
+ function pluginCatalogue(plugins) {
20975
+ const out = [];
20976
+ for (const plugin of builtInPlugins) {
20977
+ out.push({ id: plugin.id, extensionIds: plugin.extensions.map((e) => e.id) });
20978
+ }
20979
+ for (const p of plugins) {
20980
+ out.push({ id: p.id, extensionIds: p.extensions?.map((e) => e.id) ?? [] });
20981
+ }
20982
+ return out;
20983
+ }
20984
+ function parseQualifiedExtensionId(id, catalogue) {
20985
+ const [pluginId, extId, ...rest] = id.split("/");
20986
+ if (!pluginId || !extId || rest.length > 0) return { ok: false, reason: "malformed" };
20987
+ const plugin = catalogue.find((p) => p.id === pluginId);
20988
+ if (!plugin) return { ok: false, reason: "unknown-plugin", pluginId };
20989
+ if (!plugin.extensionIds.includes(extId)) {
20990
+ return { ok: false, reason: "unknown-extension", pluginId, extId };
20991
+ }
20992
+ return { ok: true, pluginId, extId };
20993
+ }
20994
+ function renderQualifiedIdError(result, rawId, ansi) {
20995
+ const glyph = ansi.red(PLUGINS_TEXTS.rowGlyphOff);
20996
+ if (result.reason === "unknown-extension") {
20997
+ return tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
20998
+ glyph,
20999
+ id: sanitizeForTerminal(rawId),
21000
+ pluginId: sanitizeForTerminal(result.pluginId ?? ""),
21001
+ extId: sanitizeForTerminal(result.extId ?? ""),
21002
+ hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdNotFoundHint)
21003
+ });
21004
+ }
21005
+ return tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
21006
+ glyph,
21007
+ pluginId: sanitizeForTerminal(result.reason === "unknown-plugin" ? result.pluginId ?? rawId : rawId),
21008
+ hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
21009
+ });
21010
+ }
20636
21011
  function wrapText(text, maxWidth) {
20637
21012
  const words = text.split(/\s+/).filter((w) => w.length > 0);
20638
21013
  if (words.length === 0) return [];
@@ -20656,14 +21031,24 @@ var PluginsListCommand = class extends SmCommand {
20656
21031
  static paths = [["plugins", "list"]];
20657
21032
  static usage = Command22.Usage({
20658
21033
  category: "Plugins",
20659
- description: "List discovered plugins and their load status.",
20660
- details: "Scans <cwd>/.skill-map/plugins (or --plugin-dir <path>). Built-in plugins (claude, core) are listed alongside user plugins."
21034
+ description: "List discovered plugins, or one plugin's extensions.",
21035
+ details: `
21036
+ No id: scans <cwd>/.skill-map/plugins (or --plugin-dir <path>) and
21037
+ lists every plugin (built-in + user) with status, one row each.
21038
+ With a bare plugin id: renders that plugin's manifest and its
21039
+ extensions (kind / version / per-extension status). A qualified
21040
+ \`<plugin>/<ext>\` id is rejected with a redirect to \`sm plugins show\`.
21041
+ `
20661
21042
  });
21043
+ id = Option21.String({ required: false });
20662
21044
  pluginDir = Option21.String("--plugin-dir", { required: false });
20663
21045
  async run() {
20664
21046
  const plugins = await loadAll({ pluginDir: this.pluginDir });
20665
21047
  const resolveEnabled = await buildResolver();
20666
21048
  const builtIns2 = builtInRows(resolveEnabled);
21049
+ if (this.id !== void 0) {
21050
+ return this.renderPluginDetailById(this.id, builtIns2, plugins);
21051
+ }
20667
21052
  if (this.json) {
20668
21053
  this.printer.data(
20669
21054
  JSON.stringify({ builtIns: builtIns2, plugins }, omitModule, 2) + "\n"
@@ -20675,24 +21060,68 @@ var PluginsListCommand = class extends SmCommand {
20675
21060
  return ExitCode.Ok;
20676
21061
  }
20677
21062
  const ansi = this.ansiFor("stdout");
20678
- this.printer.data(renderListHuman(builtIns2, plugins, resolveEnabled, ansi));
21063
+ this.printer.data(renderIndexHuman(builtIns2, plugins, resolveEnabled, ansi));
21064
+ return ExitCode.Ok;
21065
+ }
21066
+ /**
21067
+ * `sm plugins list <id>`, render one plugin's full detail. A qualified
21068
+ * `<plugin>/<ext>` id is the wrong granularity for `list` (it targets a
21069
+ * single extension), redirect to `sm plugins show`. A bare id that
21070
+ * matches no plugin is a NotFound.
21071
+ */
21072
+ renderPluginDetailById(id, builtIns2, plugins) {
21073
+ const stderrAnsi = this.ansiFor("stderr");
21074
+ if (id.includes("/")) {
21075
+ const pluginId = id.split("/")[0] ?? id;
21076
+ this.printer.error(
21077
+ tx(PLUGINS_TEXTS.listQualifiedId, {
21078
+ glyph: stderrAnsi.red(PLUGINS_TEXTS.rowGlyphOff),
21079
+ id: sanitizeForTerminal(id),
21080
+ hint: stderrAnsi.dim(
21081
+ tx(PLUGINS_TEXTS.listQualifiedIdHint, {
21082
+ id: sanitizeForTerminal(id),
21083
+ pluginId: sanitizeForTerminal(pluginId)
21084
+ })
21085
+ )
21086
+ })
21087
+ );
21088
+ return ExitCode.Error;
21089
+ }
21090
+ const builtIn = builtIns2.find((b) => b.id === id);
21091
+ const match = plugins.find((p) => p.id === id);
21092
+ if (!builtIn && !match) {
21093
+ this.printer.error(
21094
+ tx(PLUGINS_TEXTS.pluginNotFound, {
21095
+ glyph: stderrAnsi.red(PLUGINS_TEXTS.rowGlyphOff),
21096
+ id: sanitizeForTerminal(id),
21097
+ hint: stderrAnsi.dim(PLUGINS_TEXTS.pluginNotFoundHint)
21098
+ })
21099
+ );
21100
+ return ExitCode.NotFound;
21101
+ }
21102
+ if (this.json) {
21103
+ const payload = builtIn ?? match;
21104
+ this.printer.data(JSON.stringify(payload, omitModule, 2) + "\n");
21105
+ return ExitCode.Ok;
21106
+ }
21107
+ const ansi = this.ansiFor("stdout");
21108
+ const text = builtIn ? renderBuiltInDetail(builtIn, ansi) : renderPluginDetail(match, ansi);
21109
+ this.printer.data(text);
20679
21110
  return ExitCode.Ok;
20680
21111
  }
20681
21112
  };
20682
- function renderListHuman(builtIns2, plugins, resolveEnabled, ansi) {
21113
+ function renderIndexHuman(builtIns2, plugins, resolveEnabled, ansi) {
20683
21114
  const rows = [
20684
- ...builtIns2.map(builtInToListRow),
20685
- ...plugins.map((p) => pluginToListRow(p, resolveEnabled))
21115
+ ...builtIns2.map(builtInToIndexRow),
21116
+ ...plugins.map((p) => pluginToIndexRow(p, resolveEnabled))
20686
21117
  ];
20687
21118
  const idWidth = Math.max(...rows.map((r) => r.id.length));
20688
- const countWidth = Math.max(
20689
- ...rows.map((r) => String(r.names.length).length)
20690
- );
21119
+ const countWidth = Math.max(...rows.map((r) => String(r.extCount).length));
20691
21120
  const lines = [];
20692
21121
  for (const row of rows) {
20693
21122
  const glyph = row.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
20694
21123
  const idCol = row.id.padEnd(idWidth);
20695
- const countCol = String(row.names.length).padStart(countWidth);
21124
+ const countCol = String(row.extCount).padStart(countWidth);
20696
21125
  lines.push(
20697
21126
  tx(PLUGINS_TEXTS.pluginRow, {
20698
21127
  glyph,
@@ -20701,195 +21130,34 @@ function renderListHuman(builtIns2, plugins, resolveEnabled, ansi) {
20701
21130
  source: ansi.dim(row.source)
20702
21131
  })
20703
21132
  );
20704
- const indent = PLUGINS_TEXTS.pluginSubIndent;
20705
21133
  if (row.reason) {
20706
- lines.push(`${indent}${ansi.dim(row.reason)}`);
20707
- } else if (row.names.length > 0) {
20708
- for (const wrapped of wrapNames(row.names, indent, 76)) {
20709
- lines.push(`${indent}${ansi.dim(wrapped)}`);
20710
- }
21134
+ lines.push(`${PLUGINS_TEXTS.pluginSubIndent}${ansi.dim(row.reason)}`);
20711
21135
  }
20712
21136
  }
20713
21137
  return lines.join("\n") + "\n" + PLUGINS_TEXTS.listTipShow;
20714
21138
  }
20715
- function builtInToListRow(b) {
20716
- const names = b.extensions.map(
20717
- (e) => e.enabled ? e.id : `${PLUGINS_TEXTS.rowGlyphOff} ${e.id}`
20718
- );
21139
+ function builtInToIndexRow(b) {
20719
21140
  return {
20720
21141
  id: b.id,
20721
21142
  enabled: b.enabled,
20722
21143
  source: PLUGINS_TEXTS.sourceBuiltIn,
20723
- names
21144
+ extCount: b.extensions.length
20724
21145
  };
20725
21146
  }
20726
- function pluginToListRow(p, resolveEnabled) {
21147
+ function pluginToIndexRow(p, resolveEnabled) {
20727
21148
  const isLoaded = p.status === "enabled";
20728
21149
  const extensions = p.extensions ?? [];
20729
- const enabled = isLoaded ? extensions.length === 0 || extensions.some((e) => resolveEnabled(qualifiedExtensionId(p.id, e.id))) : false;
20730
- const names = extensions.map((e) => {
20731
- const safeId = sanitizeForTerminal(e.id);
20732
- return resolveEnabled(qualifiedExtensionId(p.id, e.id)) ? safeId : `${PLUGINS_TEXTS.rowGlyphOff} ${safeId}`;
20733
- });
21150
+ const extEnabled = (e) => resolveEnabled(qualifiedExtensionId(p.id, e.id), installedDefaultEnabled(e.stability));
21151
+ const enabled = isLoaded ? extensions.length === 0 || extensions.some((e) => extEnabled(e)) : false;
20734
21152
  const reason = p.status === "enabled" ? void 0 : sanitizeForTerminal(p.reason ?? "") || void 0;
20735
21153
  return {
20736
21154
  id: sanitizeForTerminal(p.id),
20737
21155
  enabled,
20738
21156
  source: PLUGINS_TEXTS.sourceUser,
20739
- names,
21157
+ extCount: extensions.length,
20740
21158
  reason
20741
21159
  };
20742
21160
  }
20743
- function wrapNames(names, indent, maxWidth) {
20744
- const out = [];
20745
- const sep8 = ", ";
20746
- let current = "";
20747
- for (const name of names) {
20748
- const candidate = current === "" ? name : `${current}${sep8}${name}`;
20749
- if (indent.length + candidate.length > maxWidth && current !== "") {
20750
- out.push(`${current},`);
20751
- current = name;
20752
- } else {
20753
- current = candidate;
20754
- }
20755
- }
20756
- if (current !== "") out.push(current);
20757
- return out;
20758
- }
20759
-
20760
- // cli/commands/plugins/show.ts
20761
- import { Command as Command23, Option as Option22 } from "clipanion";
20762
- var PluginsShowCommand = class extends SmCommand {
20763
- static paths = [["plugins", "show"]];
20764
- static usage = Command23.Usage({
20765
- category: "Plugins",
20766
- description: "Show a single plugin's manifest + loaded extensions.",
20767
- details: `
20768
- Accepts a plugin id (\`core\`, \`claude\`, \`my-plugin\`)
20769
- or a qualified extension id (\`core/<ext-id>\`,
20770
- \`<plugin>/<ext-id>\`). When given a qualified id, validates the
20771
- extension exists and renders a single-extension detail block.
20772
- The bare form renders the parent plugin's detail with per-extension
20773
- status. The same id shapes \`sm plugins enable\` and
20774
- \`sm plugins disable\` accept resolve cleanly here too.
20775
- `
20776
- });
20777
- id = Option22.String({ required: true });
20778
- pluginDir = Option22.String("--plugin-dir", { required: false });
20779
- async run() {
20780
- const plugins = await loadAll({ pluginDir: this.pluginDir });
20781
- const resolveEnabled = await buildResolver();
20782
- const builtIns2 = builtInRows(resolveEnabled);
20783
- const stderrAnsi = this.ansiFor("stderr");
20784
- const lookupResult = resolveShowLookupId(this.id, builtIns2, plugins, stderrAnsi);
20785
- if ("error" in lookupResult) {
20786
- this.printer.error(lookupResult.error);
20787
- return ExitCode.NotFound;
20788
- }
20789
- const { pluginId, extId } = lookupResult;
20790
- const builtIn = builtIns2.find((b) => b.id === pluginId);
20791
- const match = plugins.find((p) => p.id === pluginId);
20792
- if (!builtIn && !match) {
20793
- this.printer.error(
20794
- tx(PLUGINS_TEXTS.pluginNotFound, {
20795
- glyph: stderrAnsi.red("\u2715"),
20796
- id: sanitizeForTerminal(this.id),
20797
- hint: stderrAnsi.dim(PLUGINS_TEXTS.pluginNotFoundHint)
20798
- })
20799
- );
20800
- return ExitCode.NotFound;
20801
- }
20802
- if (extId !== void 0) {
20803
- return this.renderExtensionDetail({ extId, pluginId, builtIn, match });
20804
- }
20805
- if (this.json) {
20806
- const payload = builtIn ?? match;
20807
- this.printer.data(JSON.stringify(payload, omitModule, 2) + "\n");
20808
- return ExitCode.Ok;
20809
- }
20810
- const ansi = this.ansiFor("stdout");
20811
- const text = builtIn ? renderBuiltInDetail(builtIn, ansi) : renderPluginDetail(match, ansi);
20812
- this.printer.data(text);
20813
- return ExitCode.Ok;
20814
- }
20815
- /**
20816
- * Render the single-extension detail block, the path taken when the
20817
- * user supplies a qualified `<plugin>/<ext>` id. `--json` emits the
20818
- * single extension row (no surrounding plugin envelope) so tooling
20819
- * can pipe straight into `jq`; human mode renders a focused header
20820
- * plus a Kind / Version / Stability / Description / Preconditions /
20821
- * Entry field block.
20822
- */
20823
- renderExtensionDetail(args2) {
20824
- const { extId, pluginId, builtIn, match } = args2;
20825
- const ansi = this.ansiFor("stdout");
20826
- if (builtIn) {
20827
- const ext = builtIn.extensions.find((e) => e.id === extId);
20828
- if (!ext) return ExitCode.NotFound;
20829
- if (this.json) {
20830
- this.printer.data(JSON.stringify({ pluginId, ...ext }, omitModule, 2) + "\n");
20831
- return ExitCode.Ok;
20832
- }
20833
- this.printer.data(renderBuiltInExtensionDetail(pluginId, ext, ansi));
20834
- return ExitCode.Ok;
20835
- }
20836
- const userExt = match?.extensions?.find((e) => e.id === extId);
20837
- if (!userExt) return ExitCode.NotFound;
20838
- if (this.json) {
20839
- this.printer.data(JSON.stringify(userExt, omitModule, 2) + "\n");
20840
- return ExitCode.Ok;
20841
- }
20842
- this.printer.data(renderUserExtensionDetail(pluginId, userExt, ansi));
20843
- return ExitCode.Ok;
20844
- }
20845
- };
20846
- function resolveShowLookupId(id, builtIns2, plugins, ansi) {
20847
- if (!id.includes("/")) return { pluginId: id };
20848
- const parsed = parseQualifiedId(id);
20849
- if ("error" in parsed) return { error: malformedQualifiedError(id, ansi) };
20850
- const { pluginId, extId } = parsed;
20851
- const knownExts = collectKnownExtensions(pluginId, builtIns2, plugins);
20852
- if (knownExts === null) return { error: unknownPluginError(pluginId, ansi) };
20853
- if (!knownExts.includes(extId)) {
20854
- return { error: unknownExtensionError(id, pluginId, extId, ansi) };
20855
- }
20856
- return { pluginId, extId };
20857
- }
20858
- function parseQualifiedId(id) {
20859
- const [pluginId, extId, ...rest] = id.split("/");
20860
- if (!pluginId || !extId || rest.length > 0) return { error: true };
20861
- return { pluginId, extId };
20862
- }
20863
- function collectKnownExtensions(pluginId, builtIns2, plugins) {
20864
- const builtIn = builtIns2.find((b) => b.id === pluginId);
20865
- if (builtIn) return builtIn.extensions.map((e) => e.id);
20866
- const userPlugin = plugins.find((p) => p.id === pluginId);
20867
- if (userPlugin) return userPlugin.extensions?.map((e) => e.id) ?? [];
20868
- return null;
20869
- }
20870
- function malformedQualifiedError(id, ansi) {
20871
- return tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
20872
- glyph: ansi.red("\u2715"),
20873
- pluginId: sanitizeForTerminal(id),
20874
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
20875
- });
20876
- }
20877
- function unknownPluginError(pluginId, ansi) {
20878
- return tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
20879
- glyph: ansi.red("\u2715"),
20880
- pluginId: sanitizeForTerminal(pluginId),
20881
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
20882
- });
20883
- }
20884
- function unknownExtensionError(id, pluginId, extId, ansi) {
20885
- return tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
20886
- glyph: ansi.red("\u2715"),
20887
- id: sanitizeForTerminal(id),
20888
- pluginId: sanitizeForTerminal(pluginId),
20889
- extId: sanitizeForTerminal(extId),
20890
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdNotFoundHint)
20891
- });
20892
- }
20893
21161
  function kindIndex(kind) {
20894
21162
  const idx = EXTENSION_KINDS.indexOf(kind);
20895
21163
  return idx === -1 ? EXTENSION_KINDS.length : idx;
@@ -20908,7 +21176,7 @@ function renderBuiltInDetail(b, ansi) {
20908
21176
  const items = sorted.map((ext) => ({
20909
21177
  glyph: ext.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff),
20910
21178
  kind: ext.kind,
20911
- name: `${b.id}/${ext.id}`
21179
+ name: withStabilityTag(`${b.id}/${ext.id}`, ext.stability)
20912
21180
  }));
20913
21181
  return tx(PLUGINS_TEXTS.detailHeaderBuiltIn, {
20914
21182
  glyph,
@@ -20987,7 +21255,7 @@ function collectPluginExtensionItems(match, ansi) {
20987
21255
  // status header above (✕ on the row).
20988
21256
  glyph: ansi.green(PLUGINS_TEXTS.rowGlyphOk),
20989
21257
  kind: sanitizeForTerminal(ext.kind),
20990
- name: `${safePluginId}/${safeExtId}`,
21258
+ name: withStabilityTag(`${safePluginId}/${safeExtId}`, ext.stability),
20991
21259
  version: sanitizeForTerminal(ext.version)
20992
21260
  };
20993
21261
  });
@@ -21013,6 +21281,83 @@ function renderExtensionItems(items) {
21013
21281
  }
21014
21282
  return out.join("");
21015
21283
  }
21284
+
21285
+ // cli/commands/plugins/show.ts
21286
+ import { Command as Command23, Option as Option22 } from "clipanion";
21287
+ var PluginsShowCommand = class extends SmCommand {
21288
+ static paths = [["plugins", "show"]];
21289
+ static usage = Command23.Usage({
21290
+ category: "Plugins",
21291
+ description: "Show a single extension's detail.",
21292
+ details: `
21293
+ Accepts a qualified extension id (\`core/<ext-id>\`,
21294
+ \`<plugin>/<ext-id>\`) and renders a single-extension detail block
21295
+ (Kind / Version / Stability / Description / Preconditions / Entry).
21296
+ A bare plugin id is rejected with a redirect to
21297
+ \`sm plugins list <id>\`, which renders the whole plugin. The same
21298
+ qualified id shape \`sm plugins enable\` and \`sm plugins disable\`
21299
+ accept resolves cleanly here too.
21300
+ `
21301
+ });
21302
+ id = Option22.String({ required: true });
21303
+ pluginDir = Option22.String("--plugin-dir", { required: false });
21304
+ async run() {
21305
+ const plugins = await loadAll({ pluginDir: this.pluginDir });
21306
+ const resolveEnabled = await buildResolver();
21307
+ const builtIns2 = builtInRows(resolveEnabled);
21308
+ const stderrAnsi = this.ansiFor("stderr");
21309
+ if (!this.id.includes("/")) {
21310
+ this.printer.error(
21311
+ tx(PLUGINS_TEXTS.showBareId, {
21312
+ glyph: stderrAnsi.red(PLUGINS_TEXTS.rowGlyphOff),
21313
+ id: sanitizeForTerminal(this.id),
21314
+ hint: stderrAnsi.dim(
21315
+ tx(PLUGINS_TEXTS.showBareIdHint, { id: sanitizeForTerminal(this.id) })
21316
+ )
21317
+ })
21318
+ );
21319
+ return ExitCode.Error;
21320
+ }
21321
+ const parsed = parseQualifiedExtensionId(this.id, pluginCatalogue(plugins));
21322
+ if (!parsed.ok) {
21323
+ this.printer.error(renderQualifiedIdError(parsed, this.id, stderrAnsi));
21324
+ return ExitCode.NotFound;
21325
+ }
21326
+ const { pluginId, extId } = parsed;
21327
+ const builtIn = builtIns2.find((b) => b.id === pluginId);
21328
+ const match = plugins.find((p) => p.id === pluginId);
21329
+ return this.renderExtensionDetail({ extId, pluginId, builtIn, match });
21330
+ }
21331
+ /**
21332
+ * Render the single-extension detail block. `--json` emits the single
21333
+ * extension row (no surrounding plugin envelope) so tooling can pipe
21334
+ * straight into `jq`; human mode renders a focused header plus a
21335
+ * Kind / Version / Stability / Description / Preconditions / Entry
21336
+ * field block.
21337
+ */
21338
+ renderExtensionDetail(args2) {
21339
+ const { extId, pluginId, builtIn, match } = args2;
21340
+ const ansi = this.ansiFor("stdout");
21341
+ if (builtIn) {
21342
+ const ext = builtIn.extensions.find((e) => e.id === extId);
21343
+ if (!ext) return ExitCode.NotFound;
21344
+ if (this.json) {
21345
+ this.printer.data(JSON.stringify({ pluginId, ...ext }, omitModule, 2) + "\n");
21346
+ return ExitCode.Ok;
21347
+ }
21348
+ this.printer.data(renderBuiltInExtensionDetail(pluginId, ext, ansi));
21349
+ return ExitCode.Ok;
21350
+ }
21351
+ const userExt = match?.extensions?.find((e) => e.id === extId);
21352
+ if (!userExt) return ExitCode.NotFound;
21353
+ if (this.json) {
21354
+ this.printer.data(JSON.stringify(userExt, omitModule, 2) + "\n");
21355
+ return ExitCode.Ok;
21356
+ }
21357
+ this.printer.data(renderUserExtensionDetail(pluginId, userExt, ansi));
21358
+ return ExitCode.Ok;
21359
+ }
21360
+ };
21016
21361
  function renderBuiltInExtensionDetail(pluginId, ext, ansi) {
21017
21362
  const glyph = ext.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
21018
21363
  const header = tx(PLUGINS_TEXTS.detailHeaderExtensionBuiltIn, {
@@ -21021,6 +21366,7 @@ function renderBuiltInExtensionDetail(pluginId, ext, ansi) {
21021
21366
  source: ansi.dim(PLUGINS_TEXTS.sourceBuiltIn)
21022
21367
  });
21023
21368
  const meta = { kind: ext.kind };
21369
+ if (ext.stability && ext.stability !== "stable") meta.stability = ext.stability;
21024
21370
  if (ext.description) meta.description = ext.description;
21025
21371
  if (ext.entry !== void 0) meta.entry = ext.entry;
21026
21372
  return header + "\n" + renderExtensionFields(meta);
@@ -21038,7 +21384,7 @@ function renderUserExtensionDetail(pluginId, ext, ansi) {
21038
21384
  version: ext.version,
21039
21385
  entry: ext.entryPath
21040
21386
  };
21041
- if (meta.stability !== void 0) input.stability = meta.stability;
21387
+ if (ext.stability && ext.stability !== "stable") input.stability = ext.stability;
21042
21388
  if (meta.description !== void 0) input.description = meta.description;
21043
21389
  if (meta.preconditions !== void 0) input.preconditions = meta.preconditions;
21044
21390
  return header + "\n" + renderExtensionFields(input);
@@ -21048,7 +21394,6 @@ function readInstanceMeta(instance) {
21048
21394
  const obj = instance;
21049
21395
  const out = {};
21050
21396
  if (typeof obj["description"] === "string") out.description = obj["description"];
21051
- if (typeof obj["stability"] === "string") out.stability = obj["stability"];
21052
21397
  if (Array.isArray(obj["preconditions"])) {
21053
21398
  out.preconditions = obj["preconditions"].filter(
21054
21399
  (p) => typeof p === "string"
@@ -21849,61 +22194,15 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
21849
22194
  return this.toggle(false);
21850
22195
  }
21851
22196
  };
21852
- function pluginCatalogue(plugins) {
21853
- const out = [];
21854
- for (const plugin of builtInPlugins) {
21855
- out.push({
21856
- id: plugin.id,
21857
- extensionIds: plugin.extensions.map((e) => e.id)
21858
- });
21859
- }
21860
- for (const p of plugins) {
21861
- out.push({
21862
- id: p.id,
21863
- extensionIds: p.extensions?.map((e) => e.id) ?? []
21864
- });
21865
- }
21866
- return out;
21867
- }
21868
22197
  function resolveToggleTarget(id, catalogue, ansi) {
21869
22198
  return id.includes("/") ? resolveQualifiedToggle(id, catalogue, ansi) : resolveBareToggle(id, catalogue);
21870
22199
  }
21871
22200
  function resolveQualifiedToggle(id, catalogue, ansi) {
21872
- const errGlyph = ansi.red("\u2715");
21873
- const [pluginId, extId, ...rest] = id.split("/");
21874
- if (!pluginId || !extId || rest.length > 0) {
21875
- return {
21876
- error: tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
21877
- glyph: errGlyph,
21878
- pluginId: sanitizeForTerminal(id),
21879
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
21880
- })
21881
- };
21882
- }
21883
- const plugin = catalogue.find((b) => b.id === pluginId);
21884
- if (!plugin) {
21885
- return {
21886
- error: tx(PLUGINS_TEXTS.qualifiedIdUnknownPlugin, {
21887
- glyph: errGlyph,
21888
- pluginId: sanitizeForTerminal(pluginId),
21889
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdUnknownPluginHint)
21890
- })
21891
- };
21892
- }
21893
- if (!plugin.extensionIds.includes(extId)) {
21894
- return {
21895
- error: tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
21896
- glyph: errGlyph,
21897
- id: sanitizeForTerminal(id),
21898
- pluginId: sanitizeForTerminal(pluginId),
21899
- extId: sanitizeForTerminal(extId),
21900
- hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdNotFoundHint)
21901
- })
21902
- };
21903
- }
22201
+ const parsed = parseQualifiedExtensionId(id, catalogue);
22202
+ if (!parsed.ok) return { error: renderQualifiedIdError(parsed, id, ansi) };
21904
22203
  return {
21905
22204
  origin: "qualified",
21906
- keys: [qualifiedExtensionId(pluginId, extId)]
22205
+ keys: [qualifiedExtensionId(parsed.pluginId, parsed.extId)]
21907
22206
  };
21908
22207
  }
21909
22208
  function resolveBareToggle(id, catalogue) {
@@ -22283,7 +22582,7 @@ Generated by \`sm plugins create ${kind} ${pluginId}\`. Edit \`${mainFileRel}\`
22283
22582
 
22284
22583
  ## Verbs
22285
22584
 
22286
- - \`sm plugins show ${pluginId}\`: manifest + load status
22585
+ - \`sm plugins list ${pluginId}\`: manifest + extensions + load status
22287
22586
  - \`sm plugins doctor\`: full plugin diagnostic
22288
22587
  - \`sm scan\`: re-emit contributions / re-run analysis
22289
22588
 
@@ -25735,8 +26034,9 @@ function buildBuiltInItems(resolveEnabled) {
25735
26034
  id: ext.id,
25736
26035
  kind: ext.kind,
25737
26036
  version: ext.version,
25738
- enabled: resolveEnabled(qualified),
26037
+ enabled: resolveEnabled(qualified, installedDefaultEnabled(ext.stability)),
25739
26038
  ...ext.description ? { description: ext.description } : {},
26039
+ ...ext.stability ? { stability: ext.stability } : {},
25740
26040
  ...extLocked ? { locked: true } : {}
25741
26041
  };
25742
26042
  });
@@ -25790,8 +26090,9 @@ function projectExtensionRows(plugin, resolveEnabled, pluginLocked) {
25790
26090
  id: ext.id,
25791
26091
  kind: ext.kind,
25792
26092
  version: ext.version,
25793
- enabled: resolveEnabled(qualified),
26093
+ enabled: resolveEnabled(qualified, installedDefaultEnabled(ext.stability)),
25794
26094
  ...description ? { description } : {},
26095
+ ...ext.stability ? { stability: ext.stability } : {},
25795
26096
  ...extLocked ? { locked: true } : {}
25796
26097
  };
25797
26098
  });
@@ -30277,4 +30578,4 @@ function resolveBareDefault() {
30277
30578
  process.exit(ExitCode.Error);
30278
30579
  }
30279
30580
  //# sourceMappingURL=cli.js.map
30280
- //# debugId=3bc438eb-6e57-550d-aab1-392242aca915
30581
+ //# debugId=e21852cd-d92e-5bdf-abc7-02fffaa89502