@skill-map/cli 0.52.0 → 0.53.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 (50) hide show
  1. package/dist/cli/tutorial/sm-tutorial/SKILL.md +239 -1659
  2. package/dist/cli/tutorial/sm-tutorial/references/_core.md +332 -0
  3. package/dist/cli/tutorial/sm-tutorial/references/_manifest.yml +175 -0
  4. package/dist/cli/tutorial/sm-tutorial/references/fixtures.md +251 -0
  5. package/dist/cli/tutorial/{sm-master/references/tour-authoring.md → sm-tutorial/references/part-authoring.md} +14 -15
  6. package/dist/cli/tutorial/sm-tutorial/references/part-cli.md +267 -0
  7. package/dist/cli/tutorial/sm-tutorial/references/part-connect-harness.md +180 -0
  8. package/dist/cli/tutorial/sm-tutorial/references/part-fundamentals.md +424 -0
  9. package/dist/cli/tutorial/sm-tutorial/references/part-live-site.md +156 -0
  10. package/dist/cli/tutorial/sm-tutorial/references/part-maintain.md +286 -0
  11. package/dist/cli/tutorial/sm-tutorial/references/part-mcp.md +78 -0
  12. package/dist/cli/tutorial/{sm-master/references/tour-plugins.md → sm-tutorial/references/part-plugins.md} +11 -11
  13. package/dist/cli/tutorial/sm-tutorial/references/part-project-kickoff.md +186 -0
  14. package/dist/cli/tutorial/{sm-master/references/tour-settings.md → sm-tutorial/references/part-settings.md} +22 -24
  15. package/dist/cli.js +1213 -550
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +334 -207
  18. package/dist/kernel/index.d.ts +320 -15
  19. package/dist/kernel/index.js +334 -207
  20. package/dist/migrations/001_initial.sql +36 -0
  21. package/dist/ui/chunk-EQ72PEHT.js +1 -0
  22. package/dist/ui/chunk-GBKHMJ4B.js +1110 -0
  23. package/dist/ui/chunk-GEI6INVH.js +515 -0
  24. package/dist/ui/chunk-JXRIGHET.js +552 -0
  25. package/dist/ui/{chunk-WQMZOINB.js → chunk-K2MAVAHG.js} +1 -1
  26. package/dist/ui/{chunk-BV323KTK.js → chunk-KHARMPTZ.js} +1 -1
  27. package/dist/ui/chunk-L4NIF75A.js +2 -0
  28. package/dist/ui/chunk-LCOYSPKE.js +1 -0
  29. package/dist/ui/chunk-OFDQMBSJ.js +1 -0
  30. package/dist/ui/chunk-P2DAPRK7.js +2 -0
  31. package/dist/ui/chunk-Q2A6FWC7.js +4 -0
  32. package/dist/ui/{chunk-ZNDMBION.js → chunk-TXTY24G4.js} +28 -30
  33. package/dist/ui/chunk-UBQUCSQ4.js +1 -0
  34. package/dist/ui/chunk-WFLPMCK4.js +392 -0
  35. package/dist/ui/chunk-YQFIXHKM.js +123 -0
  36. package/dist/ui/index.html +2 -2
  37. package/dist/ui/{main-2DWVSRRX.js → main-OYITFJ7B.js} +3 -3
  38. package/dist/ui/{styles-QBTVKEVX.css → styles-Q4NCOJQY.css} +1 -1
  39. package/migrations/001_initial.sql +36 -0
  40. package/package.json +10 -8
  41. package/dist/cli/tutorial/sm-master/SKILL.md +0 -688
  42. package/dist/cli/tutorial/sm-master/references/fixture-templates.md +0 -212
  43. package/dist/ui/chunk-5MCXQKRN.js +0 -1066
  44. package/dist/ui/chunk-6B5EAHIM.js +0 -1110
  45. package/dist/ui/chunk-AEA5GIA7.js +0 -1
  46. package/dist/ui/chunk-AQN27TN2.js +0 -123
  47. package/dist/ui/chunk-CAJ7ZI44.js +0 -1
  48. package/dist/ui/chunk-E2XO4JVD.js +0 -1
  49. package/dist/ui/chunk-VJ57LHDR.js +0 -4
  50. package/dist/ui/chunk-WMGW2UAL.js +0 -2
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]="91ef81c7-e785-5818-839a-1f18ea093b57")}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]="82aaa5c6-6dae-53a6-8d4a-1a673357051c")}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.52.0",
249
+ version: "0.53.0",
250
250
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
251
251
  license: "MIT",
252
252
  type: "module",
@@ -304,15 +304,16 @@ var package_default = {
304
304
  prebuild: "pnpm build-built-ins",
305
305
  validate: "pnpm validate:compile && pnpm validate:test",
306
306
  "validate:compile": "pnpm typecheck && pnpm lint && pnpm build && pnpm built-ins:check && pnpm view-catalog:check",
307
- "validate:test": "pnpm test:ci",
307
+ "validate:test": "pnpm test:ci && pnpm conformance",
308
+ conformance: "SKILL_MAP_TELEMETRY=0 SM_NO_UPDATE_CHECK=1 node --import tsx cli/entry.ts conformance run",
308
309
  pretest: "tsup",
309
310
  "pretest:coverage": "tsup",
310
311
  "pretest:coverage:html": "tsup",
311
- test: "tsc --noEmit && SKILL_MAP_TELEMETRY=0 node --import tsx --test --test-reporter=./scripts/test-reporter.js --test-reporter-destination=stdout '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
312
- "test:ci": "FORCE_COLOR=1 SKILL_MAP_TELEMETRY=0 node --import tsx --test --test-reporter=./scripts/test-reporter.js --test-reporter-destination=stdout '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
313
- "test:spec": "SKILL_MAP_TELEMETRY=0 node --import tsx --test --test-reporter=spec '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
314
- "test:coverage": "tsc --noEmit && SKILL_MAP_TELEMETRY=0 SKILL_MAP_SKIP_BENCHMARK=1 node --experimental-default-config-file --import tsx --test --experimental-test-coverage '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
315
- "test:coverage:html": "tsc --noEmit && SKILL_MAP_TELEMETRY=0 SKILL_MAP_SKIP_BENCHMARK=1 c8 node --import tsx --test '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
312
+ test: "tsc --noEmit && SKILL_MAP_TELEMETRY=0 SM_NO_UPDATE_CHECK=1 node --import tsx --test --test-reporter=./scripts/test-reporter.js --test-reporter-destination=stdout '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
313
+ "test:ci": "FORCE_COLOR=1 SKILL_MAP_TELEMETRY=0 SM_NO_UPDATE_CHECK=1 node --import tsx --test --test-reporter=./scripts/test-reporter.js --test-reporter-destination=stdout '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
314
+ "test:spec": "SKILL_MAP_TELEMETRY=0 SM_NO_UPDATE_CHECK=1 node --import tsx --test --test-reporter=spec '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
315
+ "test:coverage": "tsc --noEmit && SKILL_MAP_TELEMETRY=0 SM_NO_UPDATE_CHECK=1 SKILL_MAP_SKIP_BENCHMARK=1 node --experimental-default-config-file --import tsx --test --experimental-test-coverage '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
316
+ "test:coverage:html": "tsc --noEmit && SKILL_MAP_TELEMETRY=0 SM_NO_UPDATE_CHECK=1 SKILL_MAP_SKIP_BENCHMARK=1 c8 node --import tsx --test '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
316
317
  clean: "rm -rf dist coverage"
317
318
  },
318
319
  dependencies: {
@@ -344,6 +345,7 @@ var package_default = {
344
345
  c8: "11.0.0",
345
346
  eslint: "10.2.1",
346
347
  "eslint-plugin-import-x": "4.16.2",
348
+ "json-schema-to-typescript": "15.0.4",
347
349
  tsup: "8.5.1",
348
350
  tsx: "4.22.3",
349
351
  typescript: "5.9.3",
@@ -1449,6 +1451,13 @@ function stringArray(value) {
1449
1451
 
1450
1452
  // plugins/core/extractors/external-url-counter/index.ts
1451
1453
  var ID4 = "external-url-counter";
1454
+ var count = {
1455
+ slot: "card.footer.left",
1456
+ icon: "pi-link",
1457
+ label: "urls",
1458
+ emitWhenEmpty: false,
1459
+ priority: 30
1460
+ };
1452
1461
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
1453
1462
  var TRAILING_PUNCT = /[.,;:!?]+$/;
1454
1463
  var externalUrlCounterExtractor = {
@@ -1473,15 +1482,7 @@ var externalUrlCounterExtractor = {
1473
1482
  * inherited from the footer `.sm-gnode__stat` styles cloned by
1474
1483
  * the `NodeCounter` renderer.
1475
1484
  */
1476
- ui: {
1477
- count: {
1478
- slot: "card.footer.left",
1479
- icon: "pi-link",
1480
- label: "urls",
1481
- emitWhenEmpty: false,
1482
- priority: 30
1483
- }
1484
- },
1485
+ ui: { count },
1485
1486
  extract(ctx) {
1486
1487
  const seen = /* @__PURE__ */ new Set();
1487
1488
  const body = stripCodeBlocks(ctx.body);
@@ -1516,7 +1517,7 @@ var externalUrlCounterExtractor = {
1516
1517
  });
1517
1518
  }
1518
1519
  if (seen.size > 0) {
1519
- ctx.emitContribution("count", { value: seen.size });
1520
+ ctx.emitContribution(count, { value: seen.size });
1520
1521
  }
1521
1522
  }
1522
1523
  };
@@ -1660,6 +1661,13 @@ function collectMcpServers(tools) {
1660
1661
 
1661
1662
  // plugins/core/extractors/tools-counter/index.ts
1662
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
+ };
1663
1671
  var TOOLTIP_MAX = 255;
1664
1672
  var toolsCounterExtractor = {
1665
1673
  id: ID7,
@@ -1668,15 +1676,7 @@ var toolsCounterExtractor = {
1668
1676
  description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card.",
1669
1677
  scope: "frontmatter",
1670
1678
  precondition: { kind: ["claude/agent"] },
1671
- ui: {
1672
- count: {
1673
- slot: "card.footer.left",
1674
- icon: "pi-wrench",
1675
- label: "tools",
1676
- emitWhenEmpty: false,
1677
- priority: 40
1678
- }
1679
- },
1679
+ ui: { count: count2 },
1680
1680
  extract(ctx) {
1681
1681
  const raw = ctx.frontmatter["tools"];
1682
1682
  if (!Array.isArray(raw)) return;
@@ -1685,7 +1685,7 @@ var toolsCounterExtractor = {
1685
1685
  if (typeof t === "string" && t.length > 0) names.push(t);
1686
1686
  }
1687
1687
  if (names.length === 0) return;
1688
- ctx.emitContribution("count", {
1688
+ ctx.emitContribution(count2, {
1689
1689
  value: names.length,
1690
1690
  tooltip: buildTooltip(names)
1691
1691
  });
@@ -1923,64 +1923,91 @@ var ANNOTATION_STALE_TEXTS = {
1923
1923
  // a literal placeholder the operator substitutes.
1924
1924
  bodyTooltip: "Sidecar drift since last bump:\n \u2022 body\nRun `sm bump <path>` to refresh.",
1925
1925
  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."
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."
1927
1931
  };
1928
1932
 
1929
1933
  // plugins/core/analyzers/annotation-stale/index.ts
1930
1934
  var ID10 = "annotation-stale";
1935
+ var staleIcon = {
1936
+ slot: "card.footer.right",
1937
+ icon: "pi-clock",
1938
+ emitWhenEmpty: true,
1939
+ priority: 20
1940
+ };
1941
+ var staleBadge = {
1942
+ slot: "inspector.header.badge",
1943
+ emitWhenEmpty: false,
1944
+ priority: 20
1945
+ };
1946
+ var bumpButton = {
1947
+ slot: "inspector.action.button",
1948
+ priority: 10
1949
+ };
1931
1950
  var annotationStaleAnalyzer = {
1932
1951
  id: ID10,
1933
1952
  pluginId: CORE_PLUGIN_ID,
1934
1953
  kind: "analyzer",
1935
1954
  description: "Marks sidecars (`.sm`) that are out of date with their `.md`.",
1936
1955
  mode: "deterministic",
1937
- // The natural fix is to bump the node: refreshes `for` hashes,
1956
+ // The natural fix is to bump the node: refreshes the sidecar hashes,
1938
1957
  // increments `annotations.version`, and stamps the audit block. The
1939
- // UI surfaces `core/node-bump` in the node inspector under "Recommended
1940
- // for issues" whenever this analyzer fires.
1941
- ui: {
1942
- // A `pi-clock` chip in the footer-right cluster so the operator
1943
- // spots drift in the list / inspector view (and on the graph card
1944
- // body). Emitted with `value: 0` and `emitWhenEmpty: true` so the
1945
- // renderer treats it as icon-only, drift severity is binary at
1946
- // this surface (the tooltip carries the per-face detail body /
1947
- // frontmatter / both). The corner badge on `graph.node.alert` was
1948
- // dropped on purpose: a tooltip on the footer chip is enough, and
1949
- // the corner badge stacked on top of broken-ref / unknown-field
1950
- // alerts produced visual noise.
1951
- staleIcon: {
1952
- slot: "card.footer.right",
1953
- icon: "pi-clock",
1954
- emitWhenEmpty: true,
1955
- // Sits right after the stability badge and before the severity
1956
- // counters: stability is the node's declared lifecycle state,
1957
- // drift is "this node disagrees with its sidecar", then the
1958
- // warn / error counts anchor the right edge.
1959
- priority: 20
1960
- }
1961
- },
1958
+ // inspector surfaces `core/node-bump` as the `bumpButton` contribution.
1959
+ ui: { staleIcon, staleBadge, bumpButton },
1962
1960
  evaluate(ctx) {
1963
1961
  const issues = [];
1964
1962
  for (const node of ctx.nodes) {
1965
- const status = node.sidecar?.status;
1966
- if (status === void 0 || status === null) continue;
1967
- if (status === "fresh") continue;
1968
- const message = status === "stale-body" ? tx(ANNOTATION_STALE_TEXTS.bodyDrift, { path: node.path }) : status === "stale-frontmatter" ? tx(ANNOTATION_STALE_TEXTS.frontmatterDrift, { path: node.path }) : tx(ANNOTATION_STALE_TEXTS.bothDrift, { path: node.path });
1963
+ const status = staleStatus(node.sidecar);
1964
+ if (node.sidecar?.present === true) {
1965
+ emitBumpButton(ctx, node.path, status !== null);
1966
+ }
1967
+ if (status === null) continue;
1969
1968
  issues.push({
1970
1969
  analyzerId: ID10,
1971
1970
  severity: "info",
1972
1971
  nodeIds: [node.path],
1973
- message,
1972
+ message: messageFor(status, node.path),
1974
1973
  data: { status }
1975
1974
  });
1976
- ctx.emitContribution(node.path, "staleIcon", {
1975
+ ctx.emitContribution(node.path, staleIcon, {
1977
1976
  value: 0,
1978
1977
  tooltip: tooltipFor(status)
1979
1978
  });
1979
+ ctx.emitContribution(node.path, staleBadge, {
1980
+ icon: "pi-clock",
1981
+ tooltip: tooltipFor(status)
1982
+ });
1980
1983
  }
1981
1984
  return issues;
1982
1985
  }
1983
1986
  };
1987
+ function staleStatus(overlay) {
1988
+ const status = overlay?.status;
1989
+ if (status === void 0 || status === null || status === "fresh") return null;
1990
+ return status;
1991
+ }
1992
+ function messageFor(status, path) {
1993
+ switch (status) {
1994
+ case "stale-body":
1995
+ return tx(ANNOTATION_STALE_TEXTS.bodyDrift, { path });
1996
+ case "stale-frontmatter":
1997
+ return tx(ANNOTATION_STALE_TEXTS.frontmatterDrift, { path });
1998
+ case "stale-both":
1999
+ return tx(ANNOTATION_STALE_TEXTS.bothDrift, { path });
2000
+ }
2001
+ }
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
+ }
1984
2011
  function tooltipFor(status) {
1985
2012
  switch (status) {
1986
2013
  case "stale-body":
@@ -2015,6 +2042,18 @@ var ISSUE_COUNTER_TEXTS = {
2015
2042
 
2016
2043
  // plugins/core/analyzers/issue-counter/index.ts
2017
2044
  var ID12 = "issue-counter";
2045
+ var warnCount = {
2046
+ slot: "card.footer.right",
2047
+ icon: "pi-exclamation-triangle",
2048
+ emitWhenEmpty: false,
2049
+ priority: 30
2050
+ };
2051
+ var errorCount = {
2052
+ slot: "card.footer.right",
2053
+ icon: "pi-times-circle",
2054
+ emitWhenEmpty: false,
2055
+ priority: 40
2056
+ };
2018
2057
  function countByTier(issues) {
2019
2058
  const errors = /* @__PURE__ */ new Map();
2020
2059
  const warns = /* @__PURE__ */ new Map();
@@ -2027,13 +2066,13 @@ function countByTier(issues) {
2027
2066
  }
2028
2067
  return { errors, warns };
2029
2068
  }
2030
- function emitTierChips(ctx, contributionId, severity, counts, singleTooltip, manyTooltip) {
2031
- for (const [nodePath, count] of counts) {
2032
- const capped = Math.min(count, 99);
2033
- ctx.emitContribution(nodePath, contributionId, {
2069
+ function emitTierChips(ctx, ref, severity, counts, singleTooltip, manyTooltip) {
2070
+ for (const [nodePath, count3] of counts) {
2071
+ const capped = Math.min(count3, 99);
2072
+ ctx.emitContribution(nodePath, ref, {
2034
2073
  value: capped,
2035
2074
  severity,
2036
- tooltip: count === 1 ? singleTooltip : tx(manyTooltip, { count })
2075
+ tooltip: count3 === 1 ? singleTooltip : tx(manyTooltip, { count: count3 })
2037
2076
  });
2038
2077
  }
2039
2078
  }
@@ -2044,33 +2083,14 @@ var issueCounterAnalyzer = {
2044
2083
  description: "Emits one aggregate severity chip per node (error + warn counts) from the live issue accumulator.",
2045
2084
  mode: "deterministic",
2046
2085
  phase: "aggregate",
2047
- ui: {
2048
- // Third in the footer-right cluster, after the drift chip
2049
- // (priority 10) and the stability badge (priority 20). The warn
2050
- // counter sits before the error counter so the operator reads
2051
- // "advisory → blocking" left-to-right.
2052
- warnCount: {
2053
- slot: "card.footer.right",
2054
- icon: "pi-exclamation-triangle",
2055
- emitWhenEmpty: false,
2056
- priority: 30
2057
- },
2058
- // Last in the cluster, the red chip pins to the right edge so the
2059
- // most severe signal anchors the row's reading position.
2060
- errorCount: {
2061
- slot: "card.footer.right",
2062
- icon: "pi-times-circle",
2063
- emitWhenEmpty: false,
2064
- priority: 40
2065
- }
2066
- },
2086
+ ui: { warnCount, errorCount },
2067
2087
  evaluate(ctx) {
2068
2088
  const accumulator = ctx.accumulatedIssues ?? [];
2069
2089
  if (accumulator.length === 0) return [];
2070
2090
  const { errors, warns } = countByTier(accumulator);
2071
2091
  emitTierChips(
2072
2092
  ctx,
2073
- "errorCount",
2093
+ errorCount,
2074
2094
  "danger",
2075
2095
  errors,
2076
2096
  ISSUE_COUNTER_TEXTS.errorTooltipSingle,
@@ -2078,7 +2098,7 @@ var issueCounterAnalyzer = {
2078
2098
  );
2079
2099
  emitTierChips(
2080
2100
  ctx,
2081
- "warnCount",
2101
+ warnCount,
2082
2102
  "warn",
2083
2103
  warns,
2084
2104
  ISSUE_COUNTER_TEXTS.warnTooltipSingle,
@@ -2246,28 +2266,27 @@ function resolveLinkTargetToPath(link, nameIndex) {
2246
2266
 
2247
2267
  // plugins/core/analyzers/link-counter/index.ts
2248
2268
  var ID15 = "link-counter";
2269
+ var linksIn = {
2270
+ slot: "card.footer.left",
2271
+ icon: "pi-download",
2272
+ label: "incoming links",
2273
+ emitWhenEmpty: false,
2274
+ priority: 10
2275
+ };
2276
+ var linksOut = {
2277
+ slot: "card.footer.left",
2278
+ icon: "pi-upload",
2279
+ label: "outgoing links",
2280
+ emitWhenEmpty: false,
2281
+ priority: 20
2282
+ };
2249
2283
  var linkCounterAnalyzer = {
2250
2284
  id: ID15,
2251
2285
  pluginId: CORE_PLUGIN_ID,
2252
2286
  kind: "analyzer",
2253
2287
  description: "Counts incoming and outgoing links per node.",
2254
2288
  mode: "deterministic",
2255
- ui: {
2256
- linksIn: {
2257
- slot: "card.footer.left",
2258
- icon: "pi-download",
2259
- label: "incoming links",
2260
- emitWhenEmpty: false,
2261
- priority: 10
2262
- },
2263
- linksOut: {
2264
- slot: "card.footer.left",
2265
- icon: "pi-upload",
2266
- label: "outgoing links",
2267
- emitWhenEmpty: false,
2268
- priority: 20
2269
- }
2270
- },
2289
+ ui: { linksIn, linksOut },
2271
2290
  evaluate(ctx) {
2272
2291
  const nameIndex = buildNameIndex(ctx.nodes);
2273
2292
  const perTarget = /* @__PURE__ */ new Map();
@@ -2279,8 +2298,8 @@ var linkCounterAnalyzer = {
2279
2298
  bump(perSource, link.source, link.kind);
2280
2299
  }
2281
2300
  for (const node of ctx.nodes) {
2282
- emitChip(ctx, node.path, "linksIn", perTarget.get(node.path));
2283
- emitChip(ctx, node.path, "linksOut", perSource.get(node.path));
2301
+ emitChip(ctx, node.path, linksIn, "in", perTarget.get(node.path));
2302
+ emitChip(ctx, node.path, linksOut, "out", perSource.get(node.path));
2284
2303
  }
2285
2304
  return [];
2286
2305
  }
@@ -2293,14 +2312,13 @@ function bump(map, key, kind) {
2293
2312
  }
2294
2313
  byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
2295
2314
  }
2296
- function emitChip(ctx, nodePath, contributionId, byKind) {
2315
+ function emitChip(ctx, nodePath, ref, direction, byKind) {
2297
2316
  if (!byKind) return;
2298
2317
  let total = 0;
2299
2318
  for (const n of byKind.values()) total += n;
2300
2319
  if (total === 0) return;
2301
2320
  const capped = Math.min(total, 99);
2302
- const direction = contributionId === "linksIn" ? "in" : "out";
2303
- ctx.emitContribution(nodePath, contributionId, {
2321
+ ctx.emitContribution(nodePath, ref, {
2304
2322
  value: capped,
2305
2323
  tooltip: formatBreakdown(byKind, direction)
2306
2324
  });
@@ -2580,42 +2598,58 @@ function normaliseId(raw) {
2580
2598
  return raw.normalize("NFD").replace(new RegExp("\\p{Mn}+", "gu"), "").toLowerCase().replace(/[-_\s]+/g, " ").replace(/ +/g, " ").trim();
2581
2599
  }
2582
2600
 
2601
+ // plugins/core/analyzers/node-stability/text.ts
2602
+ var NODE_STABILITY_TEXTS = {
2603
+ /** Label of the inspector action button that sets the lifecycle stage. */
2604
+ setLabel: "Set stability",
2605
+ /** Prompt label for the enum-pick stability input. */
2606
+ promptLabel: "Stability",
2607
+ /** Prompt option label for the `experimental` stage. */
2608
+ optionExperimental: "Experimental",
2609
+ /** Prompt option label for the `stable` stage. */
2610
+ optionStable: "Stable",
2611
+ /** Prompt option label for the `deprecated` stage. */
2612
+ optionDeprecated: "Deprecated"
2613
+ };
2614
+
2583
2615
  // plugins/core/analyzers/node-stability/index.ts
2584
2616
  var ID18 = "node-stability";
2585
2617
  var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
2586
2618
  var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
2619
+ var experimental = {
2620
+ slot: "card.footer.right",
2621
+ icon: "fa-solid fa-flask",
2622
+ label: "experimental",
2623
+ emitWhenEmpty: false,
2624
+ priority: 10
2625
+ };
2626
+ var deprecated = {
2627
+ slot: "card.footer.right",
2628
+ icon: "pi-ban",
2629
+ label: "deprecated",
2630
+ emitWhenEmpty: false,
2631
+ priority: 10
2632
+ };
2633
+ var setStabilityButton = {
2634
+ slot: "inspector.action.button",
2635
+ priority: 15
2636
+ };
2587
2637
  var nodeStabilityAnalyzer = {
2588
2638
  id: ID18,
2589
2639
  pluginId: CORE_PLUGIN_ID,
2590
2640
  kind: "analyzer",
2591
2641
  description: "Reports a node's stability stage (`experimental`, `deprecated`) on the card.",
2592
2642
  mode: "deterministic",
2593
- ui: {
2594
- // First in the footer-right cluster: stability is the node's
2595
- // declared lifecycle state, so it leads, followed by the drift
2596
- // chip and then the severity counters. It's a state badge, not a
2597
- // count, so it stays left of the numeric zone.
2598
- experimental: {
2599
- slot: "card.footer.right",
2600
- icon: "fa-solid fa-flask",
2601
- label: "experimental",
2602
- emitWhenEmpty: false,
2603
- priority: 10
2604
- },
2605
- deprecated: {
2606
- slot: "card.footer.right",
2607
- icon: "pi-ban",
2608
- label: "deprecated",
2609
- emitWhenEmpty: false,
2610
- priority: 10
2611
- }
2612
- },
2643
+ ui: { experimental, deprecated, setStabilityButton },
2613
2644
  evaluate(ctx) {
2614
2645
  const issues = [];
2615
2646
  for (const node of ctx.nodes) {
2616
2647
  const stability = readStability(node);
2648
+ if (node.sidecar?.present === true) {
2649
+ emitSetStabilityButton(ctx, node.path, stability ?? "stable");
2650
+ }
2617
2651
  if (stability === "experimental") {
2618
- ctx.emitContribution(node.path, "experimental", {
2652
+ ctx.emitContribution(node.path, experimental, {
2619
2653
  value: 0,
2620
2654
  tooltip: EXPERIMENTAL_TOOLTIP
2621
2655
  });
@@ -2627,7 +2661,7 @@ var nodeStabilityAnalyzer = {
2627
2661
  data: { stability }
2628
2662
  });
2629
2663
  } else if (stability === "deprecated") {
2630
- ctx.emitContribution(node.path, "deprecated", {
2664
+ ctx.emitContribution(node.path, deprecated, {
2631
2665
  value: 0,
2632
2666
  tooltip: DEPRECATED_TOOLTIP,
2633
2667
  severity: "warn"
@@ -2659,6 +2693,25 @@ function readLegacyMetadataStability(fm) {
2659
2693
  function isStability(value) {
2660
2694
  return value === "experimental" || value === "deprecated" || value === "stable";
2661
2695
  }
2696
+ function emitSetStabilityButton(ctx, nodePath, current) {
2697
+ ctx.emitContribution(nodePath, setStabilityButton, {
2698
+ actionId: "core/node-set-stability",
2699
+ label: NODE_STABILITY_TEXTS.setLabel,
2700
+ icon: "pi-flag",
2701
+ enabled: true,
2702
+ prompt: {
2703
+ inputType: "enum-pick",
2704
+ paramKey: "stability",
2705
+ label: NODE_STABILITY_TEXTS.promptLabel,
2706
+ options: [
2707
+ { value: "experimental", label: NODE_STABILITY_TEXTS.optionExperimental },
2708
+ { value: "stable", label: NODE_STABILITY_TEXTS.optionStable },
2709
+ { value: "deprecated", label: NODE_STABILITY_TEXTS.optionDeprecated }
2710
+ ],
2711
+ defaultValue: current
2712
+ }
2713
+ });
2714
+ }
2662
2715
 
2663
2716
  // plugins/core/analyzers/node-superseded/text.ts
2664
2717
  var NODE_SUPERSEDED_TEXTS = {
@@ -3021,8 +3074,8 @@ var ALL_SLOT_NAMES = [
3021
3074
  "card.footer.left",
3022
3075
  "card.footer.right",
3023
3076
  "graph.node.alert",
3024
- "inspector.header.badge.counter",
3025
- "inspector.header.badge.tag",
3077
+ "inspector.header.badge",
3078
+ "inspector.action.button",
3026
3079
  "inspector.body.panel.breakdown",
3027
3080
  "inspector.body.panel.records",
3028
3081
  "inspector.body.panel.tree",
@@ -3130,12 +3183,12 @@ function buildSchemaValidators() {
3130
3183
  const v = validators.get(name);
3131
3184
  if (!v) throw new Error(`Unknown schema: ${name}`);
3132
3185
  if (v(data)) return { ok: true, data };
3133
- const errors = (v.errors ?? []).map(formatError).join("; ");
3186
+ const errors = formatAjvErrors(v.errors);
3134
3187
  return { ok: false, errors };
3135
3188
  },
3136
3189
  validatePluginManifest(data) {
3137
3190
  if (pluginManifestValidator(data)) return { ok: true, data };
3138
- const errors = (pluginManifestValidator.errors ?? []).map(formatError).join("; ");
3191
+ const errors = formatAjvErrors(pluginManifestValidator.errors);
3139
3192
  return { ok: false, errors };
3140
3193
  },
3141
3194
  validateContributionPayload(slot, payload) {
@@ -3144,7 +3197,7 @@ function buildSchemaValidators() {
3144
3197
  return { ok: false, errors: "unknown-slot" };
3145
3198
  }
3146
3199
  if (validator(payload)) return { ok: true };
3147
- const errors = (validator.errors ?? []).map(formatError).join("; ");
3200
+ const errors = formatAjvErrors(validator.errors);
3148
3201
  return { ok: false, errors };
3149
3202
  }
3150
3203
  };
@@ -3176,7 +3229,7 @@ function buildProviderFrontmatterValidator(providers) {
3176
3229
  const v = compiled.get(key);
3177
3230
  if (!v) return { ok: false, errors: "no-schema" };
3178
3231
  if (v(data)) return { ok: true };
3179
- const errors = (v.errors ?? []).map(formatError).join("; ");
3232
+ const errors = formatAjvErrors(v.errors);
3180
3233
  return { ok: false, errors };
3181
3234
  }
3182
3235
  };
@@ -3185,6 +3238,47 @@ function formatError(err) {
3185
3238
  const path = err.instancePath || "(root)";
3186
3239
  return `${path} ${err.message ?? err.keyword}`;
3187
3240
  }
3241
+ function formatAjvErrors(errors) {
3242
+ const list = errors ?? [];
3243
+ if (list.length === 0) return "";
3244
+ const byPath3 = /* @__PURE__ */ new Map();
3245
+ for (const e of list) {
3246
+ const path = e.instancePath || "(root)";
3247
+ const bucket = byPath3.get(path);
3248
+ if (bucket) bucket.push(e);
3249
+ else byPath3.set(path, [e]);
3250
+ }
3251
+ const parts = [];
3252
+ for (const [path, errs] of byPath3) parts.push(...formatPathErrors(path, errs));
3253
+ return [...new Set(parts)].join("; ");
3254
+ }
3255
+ function constBranchValues(errs) {
3256
+ let count3 = 0;
3257
+ for (const e of errs) {
3258
+ const isConst = e.keyword === "const" && typeof e.params === "object" && e.params !== null && "allowedValue" in e.params;
3259
+ if (isConst) count3 += 1;
3260
+ }
3261
+ return count3;
3262
+ }
3263
+ function formatPathErrors(path, errs) {
3264
+ if (constBranchValues(errs) >= 2) {
3265
+ const parts2 = [`${path} is not a valid value`];
3266
+ for (const e of errs) {
3267
+ if (e.keyword !== "const" && e.keyword !== "oneOf") parts2.push(formatError(e));
3268
+ }
3269
+ return parts2;
3270
+ }
3271
+ const seen = /* @__PURE__ */ new Set();
3272
+ const parts = [];
3273
+ for (const e of errs) {
3274
+ const msg = formatError(e);
3275
+ if (!seen.has(msg)) {
3276
+ seen.add(msg);
3277
+ parts.push(msg);
3278
+ }
3279
+ }
3280
+ return parts;
3281
+ }
3188
3282
  function registerProviderAuxiliarySchemas(ajv, providers) {
3189
3283
  for (const provider of providers) {
3190
3284
  if (!provider.schemas) continue;
@@ -3497,6 +3591,122 @@ function makeIssue(signal) {
3497
3591
  return null;
3498
3592
  }
3499
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 [];
3686
+ }
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");
3708
+ }
3709
+
3500
3710
  // plugins/core/analyzers/trigger-collision/text.ts
3501
3711
  var TRIGGER_COLLISION_TEXTS = {
3502
3712
  /**
@@ -3523,14 +3733,14 @@ var TRIGGER_COLLISION_TEXTS = {
3523
3733
  };
3524
3734
 
3525
3735
  // plugins/core/analyzers/trigger-collision/index.ts
3526
- var ID24 = "trigger-collision";
3736
+ var ID26 = "trigger-collision";
3527
3737
  var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
3528
3738
  "command",
3529
3739
  "skill",
3530
3740
  "agent"
3531
3741
  ]);
3532
3742
  var triggerCollisionAnalyzer = {
3533
- id: ID24,
3743
+ id: ID26,
3534
3744
  pluginId: CORE_PLUGIN_ID,
3535
3745
  kind: "analyzer",
3536
3746
  mode: "deterministic",
@@ -3628,7 +3838,7 @@ function analyzeTriggerBucket(normalized, claims) {
3628
3838
  part: parts[0]
3629
3839
  });
3630
3840
  return {
3631
- analyzerId: ID24,
3841
+ analyzerId: ID26,
3632
3842
  severity: "error",
3633
3843
  nodeIds,
3634
3844
  message,
@@ -3668,13 +3878,13 @@ var ASCII_FORMATTER_TEXTS = {
3668
3878
  };
3669
3879
 
3670
3880
  // plugins/core/formatters/ascii/index.ts
3671
- var ID25 = "ascii";
3881
+ var ID27 = "ascii";
3672
3882
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
3673
3883
  var asciiFormatter = {
3674
- id: ID25,
3884
+ id: ID27,
3675
3885
  pluginId: CORE_PLUGIN_ID,
3676
3886
  kind: "formatter",
3677
- formatId: ID25,
3887
+ formatId: ID27,
3678
3888
  description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
3679
3889
  // ASCII tree formatter, header + per-kind sections + per-issue
3680
3890
  // section. Each section iterates and renders; splitting per section
@@ -3768,13 +3978,13 @@ function renderSection(out, kind, group) {
3768
3978
  }
3769
3979
 
3770
3980
  // plugins/core/formatters/json/index.ts
3771
- var ID26 = "json";
3981
+ var ID28 = "json";
3772
3982
  var jsonFormatter = {
3773
- id: ID26,
3983
+ id: ID28,
3774
3984
  pluginId: CORE_PLUGIN_ID,
3775
3985
  kind: "formatter",
3776
3986
  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`.",
3777
- formatId: ID26,
3987
+ formatId: ID28,
3778
3988
  format(ctx) {
3779
3989
  if (ctx.scanResult !== void 0) {
3780
3990
  return JSON.stringify(ctx.scanResult);
@@ -3913,9 +4123,9 @@ function resolveSpecRoot2() {
3913
4123
  }
3914
4124
 
3915
4125
  // plugins/core/actions/node-bump/index.ts
3916
- var ID27 = "node-bump";
4126
+ var ID29 = "node-bump";
3917
4127
  var nodeBumpAction = {
3918
- id: ID27,
4128
+ id: ID29,
3919
4129
  pluginId: CORE_PLUGIN_ID,
3920
4130
  kind: "action",
3921
4131
  description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
@@ -3972,45 +4182,166 @@ function pickCurrentVersion(overlay) {
3972
4182
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
3973
4183
  }
3974
4184
 
4185
+ // plugins/core/actions/node-set-stability/index.ts
4186
+ var STABILITY_VALUES = ["experimental", "stable", "deprecated"];
4187
+ var ID30 = "node-set-stability";
4188
+ var nodeSetStabilityAction = {
4189
+ id: ID30,
4190
+ pluginId: CORE_PLUGIN_ID,
4191
+ kind: "action",
4192
+ description: "Sets the lifecycle stage of the current node (writes `stability` to the sidecar).",
4193
+ mode: "deterministic",
4194
+ // The runtime contract uses generic <TInput, TReport>; this narrows
4195
+ // both. The cast is the standard pattern for built-ins that want
4196
+ // typed local I/O while staying compatible with the open generic.
4197
+ invoke(rawInput, ctx) {
4198
+ const input = rawInput ?? {};
4199
+ return invokeSetStability(input, ctx);
4200
+ }
4201
+ };
4202
+ function invokeSetStability(input, ctx) {
4203
+ const stability = input.stability;
4204
+ if (!STABILITY_VALUES.includes(stability)) {
4205
+ return { report: { ok: false, reason: "invalid" } };
4206
+ }
4207
+ const timestamp = ctx.now().toISOString();
4208
+ const write = {
4209
+ kind: "sidecar",
4210
+ path: sidecarPathFor(ctx.nodeAbsolutePath),
4211
+ changes: {
4212
+ identity: {
4213
+ path: ctx.node.path,
4214
+ bodyHash: ctx.node.bodyHash,
4215
+ frontmatterHash: ctx.node.frontmatterHash
4216
+ },
4217
+ annotations: { stability },
4218
+ audit: {
4219
+ lastBumpedAt: timestamp,
4220
+ lastBumpedBy: ctx.invoker
4221
+ }
4222
+ }
4223
+ };
4224
+ const report = { ok: true, stability };
4225
+ return { report, writes: [write] };
4226
+ }
4227
+
4228
+ // plugins/core/actions/node-set-tags/index.ts
4229
+ var ID31 = "node-set-tags";
4230
+ var nodeSetTagsAction = {
4231
+ id: ID31,
4232
+ pluginId: CORE_PLUGIN_ID,
4233
+ kind: "action",
4234
+ description: "Sets the taxonomy tags of the current node (writes `tags` to the sidecar; whole-array replace).",
4235
+ mode: "deterministic",
4236
+ // The runtime contract uses generic <TInput, TReport>; this narrows
4237
+ // both. The cast is the standard pattern for built-ins that want
4238
+ // typed local I/O while staying compatible with the open generic.
4239
+ invoke(rawInput, ctx) {
4240
+ const input = rawInput ?? {};
4241
+ return invokeSetTags(input, ctx);
4242
+ }
4243
+ };
4244
+ function invokeSetTags(input, ctx) {
4245
+ const tags = Array.isArray(input.tags) ? input.tags : [];
4246
+ const timestamp = ctx.now().toISOString();
4247
+ const write = {
4248
+ kind: "sidecar",
4249
+ path: sidecarPathFor(ctx.nodeAbsolutePath),
4250
+ changes: {
4251
+ identity: {
4252
+ path: ctx.node.path,
4253
+ bodyHash: ctx.node.bodyHash,
4254
+ frontmatterHash: ctx.node.frontmatterHash
4255
+ },
4256
+ annotations: { tags },
4257
+ audit: {
4258
+ lastBumpedAt: timestamp,
4259
+ lastBumpedBy: ctx.invoker
4260
+ }
4261
+ }
4262
+ };
4263
+ const report = { ok: true, tags };
4264
+ return { report, writes: [write] };
4265
+ }
4266
+
3975
4267
  // plugins/core/actions/node-supersede/index.ts
3976
- var ID28 = "node-supersede";
4268
+ var ID32 = "node-supersede";
3977
4269
  var nodeSupersedeAction = {
3978
- id: ID28,
4270
+ id: ID32,
3979
4271
  pluginId: CORE_PLUGIN_ID,
3980
4272
  kind: "action",
3981
4273
  description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
3982
4274
  mode: "deterministic",
3983
- invoke(_input, _ctx) {
3984
- const report = { ok: true, noop: true };
3985
- return { report };
4275
+ // The runtime contract uses generic <TInput, TReport>; supersede
4276
+ // narrows both. The cast is the standard pattern for built-ins that
4277
+ // want typed local I/O while staying compatible with the open generic.
4278
+ invoke(rawInput, ctx) {
4279
+ const input = rawInput ?? {};
4280
+ return invokeSupersede(input, ctx);
3986
4281
  }
3987
4282
  };
4283
+ function invokeSupersede(input, ctx) {
4284
+ const supersededBy = input.supersededBy;
4285
+ if (supersededBy === ctx.node.path) {
4286
+ return { report: { ok: false, reason: "self" } };
4287
+ }
4288
+ const timestamp = ctx.now().toISOString();
4289
+ const write = {
4290
+ kind: "sidecar",
4291
+ path: sidecarPathFor(ctx.nodeAbsolutePath),
4292
+ changes: {
4293
+ identity: {
4294
+ path: ctx.node.path,
4295
+ bodyHash: ctx.node.bodyHash,
4296
+ frontmatterHash: ctx.node.frontmatterHash
4297
+ },
4298
+ annotations: { supersededBy },
4299
+ audit: {
4300
+ lastBumpedAt: timestamp,
4301
+ lastBumpedBy: ctx.invoker
4302
+ }
4303
+ }
4304
+ };
4305
+ const report = { ok: true, supersededBy };
4306
+ return { report, writes: [write] };
4307
+ }
3988
4308
 
3989
4309
  // kernel/update-check/index.ts
3990
4310
  var SEMVER_SHAPE_RE = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
3991
4311
  async function fetchLatestVersion(pkg, opts) {
3992
4312
  const controller = new AbortController();
3993
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
4313
+ const abortTimer = setTimeout(() => controller.abort(), opts.timeoutMs);
4314
+ let capTimer;
4315
+ const hardCap = new Promise((_resolve, reject) => {
4316
+ capTimer = setTimeout(
4317
+ () => reject(new Error(`update check timed out after ${opts.timeoutMs}ms`)),
4318
+ opts.timeoutMs
4319
+ );
4320
+ });
3994
4321
  try {
3995
- const url = `https://registry.npmjs.org/${pkg}/latest`;
3996
- const response = await fetch(url, {
3997
- signal: controller.signal,
3998
- headers: { accept: "application/json" }
3999
- });
4000
- if (!response.ok) {
4001
- throw new Error(`registry returned status ${response.status}`);
4002
- }
4003
- const payload = await response.json();
4004
- if (typeof payload.version !== "string" || payload.version.length === 0) {
4005
- throw new Error("registry payload missing string `version`");
4006
- }
4007
- if (!SEMVER_SHAPE_RE.test(payload.version)) {
4008
- throw new Error("registry payload `version` is not a semver-shaped string");
4009
- }
4010
- return payload.version;
4322
+ return await Promise.race([fetchVersion(pkg, controller.signal), hardCap]);
4011
4323
  } finally {
4012
- clearTimeout(timer);
4324
+ clearTimeout(abortTimer);
4325
+ if (capTimer !== void 0) clearTimeout(capTimer);
4326
+ }
4327
+ }
4328
+ async function fetchVersion(pkg, signal) {
4329
+ const url = `https://registry.npmjs.org/${pkg}/latest`;
4330
+ const response = await fetch(url, {
4331
+ signal,
4332
+ headers: { accept: "application/json" }
4333
+ });
4334
+ if (!response.ok) {
4335
+ throw new Error(`registry returned status ${response.status}`);
4336
+ }
4337
+ const payload = await response.json();
4338
+ if (typeof payload.version !== "string" || payload.version.length === 0) {
4339
+ throw new Error("registry payload missing string `version`");
4340
+ }
4341
+ if (!SEMVER_SHAPE_RE.test(payload.version)) {
4342
+ throw new Error("registry payload `version` is not a semver-shaped string");
4013
4343
  }
4344
+ return payload.version;
4014
4345
  }
4015
4346
  function compareVersions(a, b) {
4016
4347
  const pa = parseSemver(a);
@@ -4479,10 +4810,14 @@ var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", v
4479
4810
  var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: VERSION };
4480
4811
  var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: VERSION };
4481
4812
  var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: VERSION };
4813
+ var supersedeAnalyzer2 = { ...supersedeAnalyzer, pluginId: "core", version: VERSION };
4814
+ var tagsAnalyzer2 = { ...tagsAnalyzer, pluginId: "core", version: VERSION };
4482
4815
  var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: VERSION };
4483
4816
  var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: VERSION };
4484
4817
  var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: VERSION };
4485
4818
  var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core", version: VERSION };
4819
+ var nodeSetStabilityAction2 = { ...nodeSetStabilityAction, pluginId: "core", version: VERSION };
4820
+ var nodeSetTagsAction2 = { ...nodeSetTagsAction, pluginId: "core", version: VERSION };
4486
4821
  var nodeSupersedeAction2 = { ...nodeSupersedeAction, pluginId: "core", version: VERSION };
4487
4822
  var updateCheckHook2 = { ...updateCheckHook, pluginId: "core", version: VERSION };
4488
4823
  var builtInPlugins = [
@@ -4542,10 +4877,14 @@ var builtInPlugins = [
4542
4877
  referenceRedundantAnalyzer2,
4543
4878
  schemaViolationAnalyzer2,
4544
4879
  signalCollisionAnalyzer2,
4880
+ supersedeAnalyzer2,
4881
+ tagsAnalyzer2,
4545
4882
  triggerCollisionAnalyzer2,
4546
4883
  asciiFormatter2,
4547
4884
  jsonFormatter2,
4548
4885
  nodeBumpAction2,
4886
+ nodeSetStabilityAction2,
4887
+ nodeSetTagsAction2,
4549
4888
  nodeSupersedeAction2,
4550
4889
  updateCheckHook2
4551
4890
  ]
@@ -5802,13 +6141,16 @@ function ensureSidecarWritesAllowed(opts) {
5802
6141
  default: false
5803
6142
  });
5804
6143
  if (allowed === true) return;
5805
- if (opts.confirm === true) {
6144
+ if (opts.always === true) {
5806
6145
  writeConfigValue("allowEditSmFiles", true, {
5807
6146
  target: "project-local",
5808
6147
  cwd: opts.cwd
5809
6148
  });
5810
6149
  return;
5811
6150
  }
6151
+ if (opts.confirm === true) {
6152
+ return;
6153
+ }
5812
6154
  throw new EConsentRequiredError({
5813
6155
  key: "allowEditSmFiles",
5814
6156
  hintTarget: "project-local"
@@ -7433,7 +7775,7 @@ async function loadPluginOverrideMap(db) {
7433
7775
  }
7434
7776
 
7435
7777
  // kernel/util/enum-parsers.ts
7436
- var STABILITY_VALUES = Object.freeze([
7778
+ var STABILITY_VALUES2 = Object.freeze([
7437
7779
  "experimental",
7438
7780
  "stable",
7439
7781
  "deprecated"
@@ -7756,6 +8098,41 @@ async function replaceAllScanContributions(trx, contributions, livePaths = /* @_
7756
8098
  await sweepPerTupleContributions(trx, contributions, freshlyRunTuples);
7757
8099
  await upsertContributionsBuffer(trx, contributions);
7758
8100
  }
8101
+ async function replaceAllScanContributionErrors(trx, contributionErrors) {
8102
+ await trx.deleteFrom("scan_contribution_errors").execute();
8103
+ if (contributionErrors.length === 0) return;
8104
+ const CHUNK = 400;
8105
+ for (let i = 0; i < contributionErrors.length; i += CHUNK) {
8106
+ const slice = contributionErrors.slice(i, i + CHUNK);
8107
+ const rows = slice.map((e) => ({
8108
+ pluginId: e.pluginId,
8109
+ extensionId: e.extensionId,
8110
+ nodePath: e.nodePath,
8111
+ reason: e.reason,
8112
+ message: e.message,
8113
+ contributionId: e.contributionId ?? null,
8114
+ slot: e.slot ?? null,
8115
+ emittedAt: e.emittedAt
8116
+ }));
8117
+ await trx.insertInto("scan_contribution_errors").values(rows).execute();
8118
+ }
8119
+ }
8120
+ async function listAllContributionErrors(db) {
8121
+ const rows = await db.selectFrom("scan_contribution_errors").selectAll().orderBy("pluginId", "asc").orderBy("extensionId", "asc").orderBy("nodePath", "asc").orderBy("emittedAt", "asc").execute();
8122
+ return rows.map(rowToContributionError);
8123
+ }
8124
+ function rowToContributionError(row) {
8125
+ return {
8126
+ pluginId: row.pluginId,
8127
+ extensionId: row.extensionId,
8128
+ nodePath: row.nodePath,
8129
+ reason: row.reason,
8130
+ message: row.message,
8131
+ ...row.contributionId !== null ? { contributionId: row.contributionId } : {},
8132
+ ...row.slot !== null ? { slot: row.slot } : {},
8133
+ emittedAt: row.emittedAt
8134
+ };
8135
+ }
7759
8136
  async function sweepOrphanContributions(trx, livePaths) {
7760
8137
  if (livePaths.size > 0) {
7761
8138
  await trx.deleteFrom("scan_contributions").where("nodePath", "not in", [...livePaths]).execute();
@@ -7965,7 +8342,7 @@ async function findNodesByTag(db, tag) {
7965
8342
  }
7966
8343
 
7967
8344
  // kernel/adapters/sqlite/scan-persistence.ts
7968
- async function persistScanResult(db, result, renameOps = [], extractorRuns = [], enrichments = [], contributions = [], registeredContributionKeys = /* @__PURE__ */ new Set(), freshlyRunTuples = /* @__PURE__ */ new Set()) {
8345
+ async function persistScanResult(db, result, renameOps = [], extractorRuns = [], enrichments = [], contributions = [], registeredContributionKeys = /* @__PURE__ */ new Set(), freshlyRunTuples = /* @__PURE__ */ new Set(), contributionErrors = []) {
7969
8346
  const scannedAt = validateScannedAt(result.scannedAt);
7970
8347
  const renames = [];
7971
8348
  await db.transaction().execute(async (trx) => {
@@ -7980,6 +8357,7 @@ async function persistScanResult(db, result, renameOps = [], extractorRuns = [],
7980
8357
  registeredContributionKeys,
7981
8358
  freshlyRunTuples
7982
8359
  );
8360
+ await replaceAllScanContributionErrors(trx, contributionErrors);
7983
8361
  const tagRecords = nodesToTagRecords(result.nodes);
7984
8362
  await replaceAllScanTags(trx, tagRecords, livePathsForContrib);
7985
8363
  await upsertEnrichmentLayer(trx, result, renameOps, enrichments);
@@ -8424,7 +8802,8 @@ var SqliteStorageAdapter = class {
8424
8802
  listForNode: (nodePath) => loadContributionsForNode(this.db, nodePath),
8425
8803
  listForPaths: (paths) => loadContributionsForPaths(this.db, paths),
8426
8804
  lookup: (pluginId, contributionId, nodePath, extensionId) => loadContributionLookup(this.db, pluginId, contributionId, nodePath, extensionId),
8427
- purgeByPlugin: (pluginId, extensionId) => purgeContributionsByPlugin(this.db, pluginId, extensionId)
8805
+ purgeByPlugin: (pluginId, extensionId) => purgeContributionsByPlugin(this.db, pluginId, extensionId),
8806
+ listAllErrors: () => listAllContributionErrors(this.db)
8428
8807
  };
8429
8808
  this.tags = {
8430
8809
  listForNode: (nodePath) => loadTagsForNode(this.db, nodePath),
@@ -8497,7 +8876,8 @@ async function persistScansThroughNonTx(db, result, opts) {
8497
8876
  defaults.enrichments,
8498
8877
  defaults.contributions,
8499
8878
  defaults.registeredContributionKeys,
8500
- defaults.freshlyRunTuples
8879
+ defaults.freshlyRunTuples,
8880
+ defaults.contributionErrors
8501
8881
  );
8502
8882
  }
8503
8883
  function applyPersistDefaults(opts) {
@@ -8508,6 +8888,7 @@ function applyPersistDefaults(opts) {
8508
8888
  contributions: [],
8509
8889
  registeredContributionKeys: /* @__PURE__ */ new Set(),
8510
8890
  freshlyRunTuples: /* @__PURE__ */ new Set(),
8891
+ contributionErrors: [],
8511
8892
  ...opts
8512
8893
  };
8513
8894
  }
@@ -8672,7 +9053,8 @@ function buildTxSubset(trx) {
8672
9053
  d.enrichments,
8673
9054
  d.contributions,
8674
9055
  d.registeredContributionKeys,
8675
- d.freshlyRunTuples
9056
+ d.freshlyRunTuples,
9057
+ d.contributionErrors
8676
9058
  ).then(() => void 0);
8677
9059
  }
8678
9060
  },
@@ -9167,10 +9549,11 @@ var BumpCommand = class extends SmCommand {
9167
9549
  * `EConsentRequiredError` thrown by `FilesystemSidecarStore.applyPatch`
9168
9550
  * (via `ensureSidecarWritesAllowed`), prompt the operator if stdin is
9169
9551
  * a TTY and `--yes` was not passed. On accept, flip `this.yes` to
9170
- * true and re-run `dispatch` (the second pass passes `confirm: true`
9171
- * to the store and the gate persists the flag to project-local).
9172
- * On decline or non-TTY without `--yes`, print a directed message
9173
- * and return `ExitCode.Error`.
9552
+ * true and re-run `dispatch` (the second pass passes `always: true`
9553
+ * to the store and the gate persists the flag to project-local, the
9554
+ * CLI's documented "never asked again" contract, Step 17 consent
9555
+ * split). On decline or non-TTY without `--yes`, print a directed
9556
+ * message and return `ExitCode.Error`.
9174
9557
  */
9175
9558
  async #runWithConsent(ansi, dispatch) {
9176
9559
  try {
@@ -9269,7 +9652,11 @@ var BumpCommand = class extends SmCommand {
9269
9652
  async #applyBumpedSingle(item, node, ansi) {
9270
9653
  const ctx = defaultRuntimeContext();
9271
9654
  const consent = {
9655
+ // Step 17 split: the CLI's accept / `--yes` persists the grant
9656
+ // (its documented "never asked again" contract), so it threads
9657
+ // `always`, not the new one-shot `confirm`.
9272
9658
  confirm: this.yes,
9659
+ always: this.yes,
9273
9660
  cwd: ctx.cwd
9274
9661
  };
9275
9662
  const applied = await applyBumpWrites(item, consent);
@@ -9359,7 +9746,9 @@ var BumpCommand = class extends SmCommand {
9359
9746
  const store = new FilesystemSidecarStore(ensureSidecarWritesAllowed);
9360
9747
  const ctx = defaultRuntimeContext();
9361
9748
  const consent = {
9749
+ // Step 17 split: CLI accept / `--yes` persists (see #runSingle).
9362
9750
  confirm: this.yes,
9751
+ always: this.yes,
9363
9752
  cwd: ctx.cwd
9364
9753
  };
9365
9754
  const outcomes = [];
@@ -9588,16 +9977,23 @@ function readConformanceKillSwitches(env = process.env) {
9588
9977
  }
9589
9978
 
9590
9979
  // kernel/i18n/plugin-loader.texts.ts
9980
+ var SPEC_GITHUB_BASE = "https://github.com/crystian/skill-map/blob/main";
9591
9981
  var PLUGIN_LOADER_TEXTS = {
9592
9982
  invalidManifestJsonParse: "{{manifestPath}}: {{errDescription}}. Validate the JSON (e.g. `npx jsonlint plugin.json`).",
9593
- invalidManifestAjv: "{{manifestPath}}: {{errors}}. See spec/schemas/plugins-registry.schema.json#/$defs/PluginManifest.",
9983
+ invalidManifestAjv: `{{manifestPath}}: {{errors}}. See ${SPEC_GITHUB_BASE}/spec/schemas/plugins-registry.schema.json#/$defs/PluginManifest.`,
9594
9984
  invalidSpecCompat: 'specCompat "{{specCompat}}" is not a valid semver range. Use a range like "^1.0.0".',
9595
9985
  incompatibleSpec: `@skill-map/spec {{installedSpecVersion}} does not satisfy specCompat "{{specCompat}}". Either update the plugin's specCompat (and re-test) or pin sm to a compatible spec version.`,
9596
9986
  loadErrorFileNotFound: "extension file not found: {{relEntry}} (resolved to {{abs}}). Check plugin.json#/extensions paths.",
9597
9987
  loadErrorImportFailed: "{{relEntry}}: import failed: {{errDescription}}",
9598
9988
  loadErrorMissingKind: "{{relEntry}}: default export missing a string `kind` field. Expected one of: {{knownKindsList}}.",
9599
9989
  loadErrorUnknownKind: '{{relEntry}}: unknown extension kind "{{kindReceived}}". Expected one of: {{knownKindsList}}.',
9600
- invalidManifestExtensionShape: "{{relEntry}}: {{kind}} manifest invalid: {{errors}}. See spec/schemas/extensions/{{kind}}.schema.json.",
9990
+ // No "manifest invalid" framing here: the warning wrapper already
9991
+ // carries the `(invalid-manifest)` status, so this reason is just the
9992
+ // file + the specific error + the doc link. `{{docUrl}}` is chosen by
9993
+ // the caller: a bad view-slot value points to the slot catalog
9994
+ // (`spec/view-slots.md`), every other manifest-shape error to the kind
9995
+ // schema. Both are GitHub blob URLs.
9996
+ invalidManifestExtensionShape: "{{relEntry}}: {{errors}}. See {{docUrl}}.",
9601
9997
  importExceededTimeout: "import exceeded {{timeoutMs}}ms; likely a top-level side effect (network call, infinite loop, large blocking work). Move side effects into the runtime methods (`detect` / `evaluate` / `render` / etc.).",
9602
9998
  disabledByConfig: "disabled by config_plugins or settings.json",
9603
9999
  invalidManifestDirMismatch: "directory name '{{dirName}}' does not match manifest id '{{manifestId}}'. Rename the directory to match the id, or update the manifest id to match the directory.",
@@ -9929,7 +10325,7 @@ function loadOneProviderKind(opts) {
9929
10325
  opts.pluginPath,
9930
10326
  opts.pluginId,
9931
10327
  "invalid-manifest",
9932
- `Provider kind \`${opts.entry}\` (declared at \`${opts.relEntry}\`) failed validation in \`kinds/${opts.entry}/kind.json\`: ${validation.errors}. See spec/schemas/extensions/provider-kind.schema.json.`
10328
+ `Provider kind \`${opts.entry}\` (declared at \`${opts.relEntry}\`) failed validation in \`kinds/${opts.entry}/kind.json\`: ${validation.errors}. See ${SPEC_GITHUB_BASE}/spec/schemas/extensions/provider-kind.schema.json.`
9933
10329
  ),
9934
10330
  manifest: opts.manifest
9935
10331
  }
@@ -10351,13 +10747,17 @@ var PluginLoader = class {
10351
10747
  }
10352
10748
  const extValidator = this.#options.validators.validatorForExtension(kind);
10353
10749
  if (!extValidator(manifestView)) {
10354
- const errors = (extValidator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
10750
+ const errors = formatAjvErrors(extValidator.errors);
10751
+ const slotError = (extValidator.errors ?? []).some(
10752
+ (e) => (e.instancePath ?? "").endsWith("/slot")
10753
+ );
10754
+ const docUrl = slotError ? `${SPEC_GITHUB_BASE}/spec/view-slots.md` : `${SPEC_GITHUB_BASE}/spec/schemas/extensions/${kind}.schema.json`;
10355
10755
  return { ok: false, failure: {
10356
10756
  ...fail(
10357
10757
  pluginPath,
10358
10758
  pluginId,
10359
10759
  "invalid-manifest",
10360
- tx(PLUGIN_LOADER_TEXTS.invalidManifestExtensionShape, { relEntry, kind, errors })
10760
+ tx(PLUGIN_LOADER_TEXTS.invalidManifestExtensionShape, { relEntry, errors, docUrl })
10361
10761
  ),
10362
10762
  manifest
10363
10763
  } };
@@ -10835,6 +11235,9 @@ function collectViewContributions(pluginId, extensionId, instance, out, options
10835
11235
  const raw = instance["ui"];
10836
11236
  if (typeof raw !== "object" || raw === null) return;
10837
11237
  const exclude = options.excludeQualifiedIds;
11238
+ const pluginOrder = options.pluginOrder;
11239
+ const extOrder = instance["order"];
11240
+ const extensionOrder = typeof extOrder === "number" ? extOrder : void 0;
10838
11241
  for (const [contributionId, value] of Object.entries(raw)) {
10839
11242
  if (typeof value !== "object" || value === null) continue;
10840
11243
  const entry = value;
@@ -10853,13 +11256,15 @@ function collectViewContributions(pluginId, extensionId, instance, out, options
10853
11256
  ...typeof entry.icon === "string" ? { icon: entry.icon } : {},
10854
11257
  ...typeof entry.emptyText === "string" ? { emptyText: entry.emptyText } : {},
10855
11258
  ...typeof entry.priority === "number" ? { priority: entry.priority } : {},
11259
+ ...typeof pluginOrder === "number" ? { pluginOrder } : {},
11260
+ ...typeof extensionOrder === "number" ? { extensionOrder } : {},
10856
11261
  emitWhenEmpty: entry.emitWhenEmpty === true
10857
11262
  });
10858
11263
  }
10859
11264
  }
10860
11265
 
10861
11266
  // core/runtime/plugin-runtime/bucketing.ts
10862
- function bucketLoaded(loaded, runtime) {
11267
+ function bucketLoaded(loaded, runtime, pluginOrder) {
10863
11268
  for (const ext of loaded) {
10864
11269
  const instance = ext.instance;
10865
11270
  if (!isExtensionInstance(instance)) continue;
@@ -10880,7 +11285,9 @@ function bucketLoaded(loaded, runtime) {
10880
11285
  ...ext.entryPath ? { entry: ext.entryPath } : {}
10881
11286
  });
10882
11287
  collectAnnotationContributions(ext.pluginId, instance, runtime.annotationContributions);
10883
- collectViewContributions(ext.pluginId, ext.id, instance, runtime.viewContributions);
11288
+ collectViewContributions(ext.pluginId, ext.id, instance, runtime.viewContributions, {
11289
+ ...typeof pluginOrder === "number" ? { pluginOrder } : {}
11290
+ });
10884
11291
  }
10885
11292
  }
10886
11293
  function collectAnnotationContributions(pluginId, instance, out) {
@@ -10926,11 +11333,14 @@ function truncateTail(s, max) {
10926
11333
  // core/runtime/i18n/plugin-runtime.texts.ts
10927
11334
  var PLUGIN_RUNTIME_TEXTS = {
10928
11335
  /**
10929
- * Stderr-ready warning for one non-loaded plugin. Format keeps the
10930
- * status word and the reason scannable so a user can grep
10931
- * `incompatible-spec` / `invalid-manifest` / `load-error`.
11336
+ * Stderr-ready warning for one non-loaded plugin. The status in parens
11337
+ * stays greppable (`invalid-manifest` / `incompatible-spec` /
11338
+ * `load-error`); `all extensions skipped` states the consequence (the
11339
+ * loader rejects the plugin whole, aborting on the first bad
11340
+ * extension). The reason carries the specifics, so it must NOT restate
11341
+ * the status (no second "manifest invalid").
10932
11342
  */
10933
- warningRow: "plugin {{id}}: {{status}}, {{reason}}",
11343
+ warningRow: "plugin {{id}} ({{status}}), all extensions skipped: {{reason}}",
10934
11344
  /** Placeholder when a non-loaded plugin record carries no `reason`. */
10935
11345
  warningReasonMissing: "(no reason recorded)"
10936
11346
  };
@@ -10992,7 +11402,7 @@ async function loadPluginRuntime(opts = {}) {
10992
11402
  };
10993
11403
  for (const plugin of discovered) {
10994
11404
  if (plugin.status === "enabled") {
10995
- bucketLoaded(plugin.extensions ?? [], runtime);
11405
+ bucketLoaded(plugin.extensions ?? [], runtime, plugin.manifest?.order);
10996
11406
  continue;
10997
11407
  }
10998
11408
  if (plugin.status === "disabled") continue;
@@ -13330,8 +13740,8 @@ var DbResetCommand = class extends SmCommand {
13330
13740
  return ExitCode.Ok;
13331
13741
  }
13332
13742
  const withCounts = rows.map((r) => {
13333
- const count = db.prepare(`SELECT COUNT(*) AS c FROM "${r.name}"`).get();
13334
- return { name: r.name, rowCount: Number(count.c) };
13743
+ const count3 = db.prepare(`SELECT COUNT(*) AS c FROM "${r.name}"`).get();
13744
+ return { name: r.name, rowCount: Number(count3.c) };
13335
13745
  });
13336
13746
  const totalRows = withCounts.reduce((acc, r) => acc + r.rowCount, 0);
13337
13747
  const lines = withCounts.map((r) => ` - ${r.name}: ${r.rowCount} row(s)`).join("\n");
@@ -15148,7 +15558,7 @@ var ORCHESTRATOR_TEXTS = {
15148
15558
  frontmatterMalformedMissingClose: "Frontmatter in {{path}} opens with `---` but never closes (no matching `---` line at column 0 was found). The file was scanned as body-only and every metadata field was silently lost. Add a closing `---` line below the metadata block.",
15149
15559
  extensionErrorLinkKindNotDeclared: 'Extractor "{{extractorId}}" emitted a link of kind "{{linkKind}}" outside its declared `emitsLinkKinds` set [{{declaredKinds}}]. Link dropped.',
15150
15560
  extensionErrorIssueInvalidSeverity: `Rule "{{analyzerId}}" emitted an issue with invalid severity {{severity}} (allowed: 'error' | 'warn' | 'info'). Issue dropped.`,
15151
- extensionErrorContributionUnknownId: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}} but did not declare it in its `viewContributions` map. Contribution dropped.',
15561
+ extensionErrorContributionUndeclaredRef: 'Extension "{{extractorId}}" emitted a view contribution on {{nodePath}} whose object is not one declared in its `ui` map (pass the declared const by reference, do not spread or inline it). Contribution dropped.',
15152
15562
  extensionErrorContributionPayloadInvalid: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}}; payload failed the "{{slot}}" schema: {{errors}}. Contribution dropped.',
15153
15563
  extensionErrorRecommendedActionMissing: 'Analyzer "{{analyzerId}}" declares recommendedAction "{{actionId}}" but no Action is registered under that qualified id. The analyzer stays registered; the recommendation will not surface in the inspector.',
15154
15564
  runScanRootEmptyArray: "runScan: roots must contain at least one path (spec requires minItems: 1)",
@@ -15168,6 +15578,7 @@ async function runExtractorsForNode(opts) {
15168
15578
  const externalLinks = [];
15169
15579
  const enrichmentBuffer = /* @__PURE__ */ new Map();
15170
15580
  const contributions = [];
15581
+ const contributionErrors = [];
15171
15582
  const signals = [];
15172
15583
  const virtualNodes = [];
15173
15584
  const virtualNodePaths = /* @__PURE__ */ new Set();
@@ -15199,36 +15610,54 @@ async function runExtractorsForNode(opts) {
15199
15610
  });
15200
15611
  }
15201
15612
  };
15202
- const declaredContributions = readDeclaredContributions(extractor);
15203
- const emitContribution = (contributionId, payload) => {
15204
- const declared = declaredContributions.get(contributionId);
15613
+ const declaredContributions = readDeclaredContributionRefs(extractor);
15614
+ const emitContribution = (ref, payload) => {
15615
+ const declared = typeof ref === "object" && ref !== null ? declaredContributions.get(ref) : void 0;
15205
15616
  if (!declared) {
15617
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUndeclaredRef, {
15618
+ extractorId: qualifiedId2,
15619
+ nodePath: opts.node.path
15620
+ });
15206
15621
  emitExtensionError(opts.emitter, qualifiedId2, opts.node.path, {
15207
15622
  phase: "emitContribution",
15208
- contributionId,
15209
- reason: "unknown-contribution-id",
15210
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
15211
- extractorId: qualifiedId2,
15212
- contributionId,
15213
- nodePath: opts.node.path
15214
- })
15623
+ reason: "undeclared-contribution-ref",
15624
+ message
15625
+ });
15626
+ contributionErrors.push({
15627
+ pluginId: extractor.pluginId,
15628
+ extensionId: extractor.id,
15629
+ nodePath: opts.node.path,
15630
+ reason: "undeclared-contribution-ref",
15631
+ message,
15632
+ emittedAt: Date.now()
15215
15633
  });
15216
15634
  return;
15217
15635
  }
15218
15636
  const result = validators.validateContributionPayload(declared.slot, payload);
15219
15637
  if (!result.ok) {
15638
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
15639
+ extractorId: qualifiedId2,
15640
+ contributionId: declared.id,
15641
+ nodePath: opts.node.path,
15642
+ slot: declared.slot,
15643
+ errors: result.errors
15644
+ });
15220
15645
  emitExtensionError(opts.emitter, qualifiedId2, opts.node.path, {
15221
15646
  phase: "emitContribution",
15222
- contributionId,
15647
+ contributionId: declared.id,
15223
15648
  slot: declared.slot,
15224
15649
  reason: result.errors,
15225
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
15226
- extractorId: qualifiedId2,
15227
- contributionId,
15228
- nodePath: opts.node.path,
15229
- slot: declared.slot,
15230
- errors: result.errors
15231
- })
15650
+ message
15651
+ });
15652
+ contributionErrors.push({
15653
+ pluginId: extractor.pluginId,
15654
+ extensionId: extractor.id,
15655
+ nodePath: opts.node.path,
15656
+ reason: result.errors,
15657
+ message,
15658
+ contributionId: declared.id,
15659
+ slot: declared.slot,
15660
+ emittedAt: Date.now()
15232
15661
  });
15233
15662
  return;
15234
15663
  }
@@ -15236,7 +15665,7 @@ async function runExtractorsForNode(opts) {
15236
15665
  pluginId: extractor.pluginId,
15237
15666
  extensionId: extractor.id,
15238
15667
  nodePath: opts.node.path,
15239
- contributionId,
15668
+ contributionId: declared.id,
15240
15669
  slot: declared.slot,
15241
15670
  payload,
15242
15671
  emittedAt: Date.now()
@@ -15274,11 +15703,12 @@ async function runExtractorsForNode(opts) {
15274
15703
  externalLinks,
15275
15704
  enrichments: Array.from(enrichmentBuffer.values()),
15276
15705
  contributions,
15706
+ contributionErrors,
15277
15707
  signals,
15278
15708
  virtualNodes
15279
15709
  };
15280
15710
  }
15281
- function readDeclaredContributions(extension) {
15711
+ function readDeclaredContributionRefs(extension) {
15282
15712
  const out = /* @__PURE__ */ new Map();
15283
15713
  const raw = extension.ui;
15284
15714
  if (typeof raw !== "object" || raw === null) return out;
@@ -15286,7 +15716,7 @@ function readDeclaredContributions(extension) {
15286
15716
  if (typeof value !== "object" || value === null) continue;
15287
15717
  const slot = value.slot;
15288
15718
  if (typeof slot !== "string") continue;
15289
- out.set(id, { slot });
15719
+ out.set(value, { id, slot });
15290
15720
  }
15291
15721
  return out;
15292
15722
  }
@@ -15534,6 +15964,7 @@ function isExternalUrlLink(link) {
15534
15964
  async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, signals, seedIssues = []) {
15535
15965
  const issues = [...seedIssues];
15536
15966
  const contributions = [];
15967
+ const contributionErrors = [];
15537
15968
  const validators = loadSchemaValidators();
15538
15969
  void registeredActionIds;
15539
15970
  const analyzerOrphans = orphanSidecars.map((o) => ({
@@ -15543,36 +15974,54 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
15543
15974
  const scheduled = orderAnalyzersByPhase(analyzers);
15544
15975
  for (const analyzer of scheduled) {
15545
15976
  const qualifiedId2 = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
15546
- const declaredContributions = readDeclaredContributions(analyzer);
15547
- const emitContribution = (nodePath, contributionId, payload) => {
15548
- const declared = declaredContributions.get(contributionId);
15977
+ const declaredContributions = readDeclaredContributionRefs(analyzer);
15978
+ const emitContribution = (nodePath, ref, payload) => {
15979
+ const declared = typeof ref === "object" && ref !== null ? declaredContributions.get(ref) : void 0;
15549
15980
  if (!declared) {
15981
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUndeclaredRef, {
15982
+ extractorId: qualifiedId2,
15983
+ nodePath
15984
+ });
15550
15985
  emitExtensionError(emitter, qualifiedId2, nodePath, {
15551
15986
  phase: "emitContribution",
15552
- contributionId,
15553
- reason: "unknown-contribution-id",
15554
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
15555
- extractorId: qualifiedId2,
15556
- contributionId,
15557
- nodePath
15558
- })
15987
+ reason: "undeclared-contribution-ref",
15988
+ message
15989
+ });
15990
+ contributionErrors.push({
15991
+ pluginId: analyzer.pluginId,
15992
+ extensionId: analyzer.id,
15993
+ nodePath,
15994
+ reason: "undeclared-contribution-ref",
15995
+ message,
15996
+ emittedAt: Date.now()
15559
15997
  });
15560
15998
  return;
15561
15999
  }
15562
16000
  const result = validators.validateContributionPayload(declared.slot, payload);
15563
16001
  if (!result.ok) {
16002
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
16003
+ extractorId: qualifiedId2,
16004
+ contributionId: declared.id,
16005
+ nodePath,
16006
+ slot: declared.slot,
16007
+ errors: result.errors
16008
+ });
15564
16009
  emitExtensionError(emitter, qualifiedId2, nodePath, {
15565
16010
  phase: "emitContribution",
15566
- contributionId,
16011
+ contributionId: declared.id,
15567
16012
  slot: declared.slot,
15568
16013
  reason: result.errors,
15569
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
15570
- extractorId: qualifiedId2,
15571
- contributionId,
15572
- nodePath,
15573
- slot: declared.slot,
15574
- errors: result.errors
15575
- })
16014
+ message
16015
+ });
16016
+ contributionErrors.push({
16017
+ pluginId: analyzer.pluginId,
16018
+ extensionId: analyzer.id,
16019
+ nodePath,
16020
+ reason: result.errors,
16021
+ message,
16022
+ contributionId: declared.id,
16023
+ slot: declared.slot,
16024
+ emittedAt: Date.now()
15576
16025
  });
15577
16026
  return;
15578
16027
  }
@@ -15580,7 +16029,7 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
15580
16029
  pluginId: analyzer.pluginId,
15581
16030
  extensionId: analyzer.id,
15582
16031
  nodePath,
15583
- contributionId,
16032
+ contributionId: declared.id,
15584
16033
  slot: declared.slot,
15585
16034
  payload,
15586
16035
  emittedAt: Date.now()
@@ -15613,7 +16062,7 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
15613
16062
  emitter.emit(evt);
15614
16063
  await hookDispatcher.dispatch("analyzer.completed", evt);
15615
16064
  }
15616
- return { issues, contributions };
16065
+ return { issues, contributions, contributionErrors };
15617
16066
  }
15618
16067
  function orderAnalyzersByPhase(analyzers) {
15619
16068
  return analyzers.slice().sort((a, b) => phaseRank(a) - phaseRank(b));
@@ -16501,6 +16950,7 @@ async function walkAndExtract(opts) {
16501
16950
  enrichments: [...accum.enrichmentBuffer.values()],
16502
16951
  extractorRuns: accum.extractorRuns,
16503
16952
  contributions: accum.contributionsBuffer,
16953
+ contributionErrors: accum.contributionErrorsBuffer,
16504
16954
  freshlyRunTuples: accum.freshlyRunTuples,
16505
16955
  orphanSidecars,
16506
16956
  sidecarRoots: accum.sidecarRoots,
@@ -16517,6 +16967,7 @@ function createWalkAccumulators() {
16517
16967
  frontmatterIssues: [],
16518
16968
  enrichmentBuffer: /* @__PURE__ */ new Map(),
16519
16969
  contributionsBuffer: [],
16970
+ contributionErrorsBuffer: [],
16520
16971
  freshlyRunTuples: /* @__PURE__ */ new Set(),
16521
16972
  extractorRuns: [],
16522
16973
  sidecarRoots: /* @__PURE__ */ new Map()
@@ -16667,7 +17118,11 @@ function mergeExtractResult(extractResult, accum) {
16667
17118
  accum.enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
16668
17119
  }
16669
17120
  for (const c of extractResult.contributions) accum.contributionsBuffer.push(c);
16670
- for (const vn of extractResult.virtualNodes) {
17121
+ for (const e of extractResult.contributionErrors) accum.contributionErrorsBuffer.push(e);
17122
+ mergeVirtualNodes(extractResult.virtualNodes, accum);
17123
+ }
17124
+ function mergeVirtualNodes(virtualNodes, accum) {
17125
+ for (const vn of virtualNodes) {
16671
17126
  if (accum.nodes.some((n) => n.path === vn.path)) continue;
16672
17127
  accum.nodes.push(vn);
16673
17128
  }
@@ -16943,6 +17398,7 @@ async function dispatchExtractorCompleted(extractors, emitter, hookDispatcher) {
16943
17398
  }
16944
17399
  function mergeAnalyzerEmissions(walked, analyzerResult, analyzers) {
16945
17400
  for (const c of analyzerResult.contributions) walked.contributions.push(c);
17401
+ for (const e of analyzerResult.contributionErrors) walked.contributionErrors.push(e);
16946
17402
  for (const analyzer of analyzers ?? []) {
16947
17403
  if (analyzer.ui === void 0) continue;
16948
17404
  for (const node of walked.nodes) {
@@ -16988,6 +17444,7 @@ function buildScanReturn(walked, issues, renameOps, stats, options, setup) {
16988
17444
  extractorRuns: walked.extractorRuns,
16989
17445
  enrichments: walked.enrichments,
16990
17446
  contributions: walked.contributions,
17447
+ contributionErrors: walked.contributionErrors,
16991
17448
  freshlyRunTuples: walked.freshlyRunTuples
16992
17449
  };
16993
17450
  }
@@ -18017,6 +18474,7 @@ async function runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanW
18017
18474
  extractorRuns: scanned.extractorRuns,
18018
18475
  enrichments: scanned.enrichments,
18019
18476
  contributions: scanned.contributions,
18477
+ contributionErrors: scanned.contributionErrors,
18020
18478
  registeredContributionKeys: collectRegisteredContributionKeys(extensions),
18021
18479
  freshlyRunTuples: scanned.freshlyRunTuples
18022
18480
  });
@@ -18873,11 +19331,11 @@ function renderStatsFailures(failures, ansi) {
18873
19331
  tx(HISTORY_TEXTS.statsSectionHeader, { title: HISTORY_TEXTS.statsSectionTitleFailures })
18874
19332
  ];
18875
19333
  const reasonWidth = Math.max(...failures.map(([reason]) => reason.length));
18876
- for (const [reason, count] of failures) {
19334
+ for (const [reason, count3] of failures) {
18877
19335
  lines.push(
18878
19336
  tx(HISTORY_TEXTS.statsFailuresRow, {
18879
19337
  reason: sanitizeForTerminal(reason).padEnd(reasonWidth),
18880
- count: ansi.red(String(count))
19338
+ count: ansi.red(String(count3))
18881
19339
  })
18882
19340
  );
18883
19341
  }
@@ -19579,14 +20037,14 @@ var OrphansReconcileCommand = class extends SmCommand {
19579
20037
  tx(ORPHANS_TEXTS.reconcileSuccessBody, { breakdown: ansi.dim(breakdown) })
19580
20038
  );
19581
20039
  if (summary.collisions.length > 0) {
19582
- const count = summary.collisions.length;
20040
+ const count3 = summary.collisions.length;
19583
20041
  this.printer.info(
19584
20042
  tx(
19585
20043
  dryRun ? ORPHANS_TEXTS.reconcileCollisionsNoteDryRun : ORPHANS_TEXTS.reconcileCollisionsNote,
19586
20044
  {
19587
20045
  glyph: ansi.yellow("\u26A0"),
19588
- count,
19589
- plural: count === 1 ? "" : "s"
20046
+ count: count3,
20047
+ plural: count3 === 1 ? "" : "s"
19590
20048
  }
19591
20049
  )
19592
20050
  );
@@ -19903,6 +20361,23 @@ var PLUGINS_TEXTS = {
19903
20361
  doctorIssuesHeader: "\n Issues ({{count}})\n",
19904
20362
  doctorIssueEntry: " {{glyph}} {{id}} {{status}}\n",
19905
20363
  doctorIssueBody: " {{line}}\n",
20364
+ // --- runtime contribution errors (last scan) -------------------------
20365
+ /**
20366
+ * "off-shape visible" follow-up. Section heading for view
20367
+ * contributions the last scan REJECTED at emit time (undeclared ref,
20368
+ * or payload failed the slot's AJV schema). Rendered only when at
20369
+ * least one error was persisted; promotes the exit code to 1.
20370
+ * `count` is the total error row count across every plugin.
20371
+ */
20372
+ doctorContribErrorsHeader: "\n Runtime contribution errors (last scan) ({{count}})\n",
20373
+ /** Per-plugin group header: red glyph + plugin id + this plugin's error count. */
20374
+ doctorContribErrorEntry: " {{glyph}} {{pluginId}} ({{count}})\n",
20375
+ /** Sample line under a plugin group: wrapped, dimmed diagnostic message. */
20376
+ doctorContribErrorBody: " {{line}}\n",
20377
+ /** Trailing dimmed note when a plugin has more errors than the sample cap shows. */
20378
+ doctorContribErrorMore: " {{line}}\n",
20379
+ /** Body of the "more" note: the count of samples omitted under this plugin. */
20380
+ doctorContribErrorMoreText: "... and {{count}} more",
19906
20381
  // --- enable / disable -----------------------------------------------
19907
20382
  /**
19908
20383
  * §3.1b two-line block. Mutex between explicit ids and `--all`; the
@@ -20406,7 +20881,7 @@ function sortExtensionsCanonical(exts) {
20406
20881
  }
20407
20882
  function renderBuiltInDetail(b, ansi) {
20408
20883
  const glyph = b.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
20409
- const count = b.extensions.length;
20884
+ const count3 = b.extensions.length;
20410
20885
  const sorted = sortExtensionsCanonical(b.extensions);
20411
20886
  const items = sorted.map((ext) => ({
20412
20887
  glyph: ext.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff),
@@ -20417,8 +20892,8 @@ function renderBuiltInDetail(b, ansi) {
20417
20892
  glyph,
20418
20893
  id: b.id,
20419
20894
  source: ansi.dim(PLUGINS_TEXTS.sourceBuiltIn),
20420
- count,
20421
- plural: count === 1 ? "" : "s"
20895
+ count: count3,
20896
+ plural: count3 === 1 ? "" : "s"
20422
20897
  }) + PLUGINS_TEXTS.detailExtensionsBlock + renderExtensionItems(items);
20423
20898
  }
20424
20899
  function renderPluginDetail(match, ansi) {
@@ -20600,6 +21075,7 @@ function renderExtensionFields(meta) {
20600
21075
 
20601
21076
  // cli/commands/plugins/doctor.ts
20602
21077
  import { Command as Command24, Option as Option23 } from "clipanion";
21078
+ var CONTRIB_ERROR_SAMPLE_CAP = 3;
20603
21079
  var STATUS_ORDER = [
20604
21080
  "enabled",
20605
21081
  "disabled",
@@ -20625,6 +21101,8 @@ var PluginsDoctorCommand = class extends SmCommand {
20625
21101
  const knownKinds = collectKnownKinds(plugins);
20626
21102
  const applicableKindWarnings = collectApplicableKindWarnings(plugins, knownKinds);
20627
21103
  const unknownSlotWarnings = collectUnknownSlotWarnings(plugins, KNOWN_SLOT_NAMES);
21104
+ const contribErrors = await loadContributionErrors();
21105
+ const contribErrorGroups = groupContributionErrorsByPlugin(contribErrors);
20628
21106
  const bad = plugins.filter((p) => p.status !== "enabled" && p.status !== "disabled");
20629
21107
  const totalWarnings = applicableKindWarnings.length + unknownSlotWarnings.length;
20630
21108
  if (this.json) {
@@ -20634,23 +21112,51 @@ var PluginsDoctorCommand = class extends SmCommand {
20634
21112
  applicableKindWarnings,
20635
21113
  unknownSlotWarnings,
20636
21114
  totalWarnings,
21115
+ contribErrors,
20637
21116
  elapsedMs: this.elapsed.ms()
20638
21117
  });
20639
21118
  this.printer.data(JSON.stringify(envelope) + "\n");
20640
- return bad.length > 0 ? ExitCode.Issues : ExitCode.Ok;
20641
- }
21119
+ return bad.length > 0 || contribErrors.length > 0 ? ExitCode.Issues : ExitCode.Ok;
21120
+ }
21121
+ this.#renderHumanReport({
21122
+ counts,
21123
+ builtInCount: builtIns2.length,
21124
+ userCount: plugins.length,
21125
+ applicableKindWarnings,
21126
+ unknownSlotWarnings,
21127
+ totalWarnings,
21128
+ bad,
21129
+ contribErrorGroups,
21130
+ contribErrorCount: contribErrors.length
21131
+ });
21132
+ return bad.length > 0 || contribErrors.length > 0 ? ExitCode.Issues : ExitCode.Ok;
21133
+ }
21134
+ /**
21135
+ * Render the full human-mode report in section order: summary header,
21136
+ * source + status tables, then the gated warnings / issues / runtime
21137
+ * contribution-error sections. Pulled out of `run` so the verb body
21138
+ * stays a linear pipeline (load → aggregate → render → exit code)
21139
+ * under the complexity cap.
21140
+ */
21141
+ #renderHumanReport(args2) {
20642
21142
  const ansi = this.ansiFor("stdout");
20643
- this.#renderSummaryHeader(counts.enabled, bad.length, totalWarnings);
20644
- this.#renderSourceBreakdown(builtIns2.length, plugins.length);
20645
- this.#renderStatusBreakdown(counts, ansi);
20646
- if (totalWarnings > 0) {
20647
- this.#renderWarnings(applicableKindWarnings, unknownSlotWarnings, totalWarnings, ansi);
21143
+ this.#renderSummaryHeader(args2.counts.enabled, args2.bad.length, args2.totalWarnings);
21144
+ this.#renderSourceBreakdown(args2.builtInCount, args2.userCount);
21145
+ this.#renderStatusBreakdown(args2.counts, ansi);
21146
+ if (args2.totalWarnings > 0) {
21147
+ this.#renderWarnings(
21148
+ args2.applicableKindWarnings,
21149
+ args2.unknownSlotWarnings,
21150
+ args2.totalWarnings,
21151
+ ansi
21152
+ );
20648
21153
  }
20649
- if (bad.length > 0) {
20650
- this.#renderIssues(bad, ansi);
20651
- return ExitCode.Issues;
21154
+ if (args2.bad.length > 0) {
21155
+ this.#renderIssues(args2.bad, ansi);
21156
+ }
21157
+ if (args2.contribErrorCount > 0) {
21158
+ this.#renderContributionErrors(args2.contribErrorGroups, args2.contribErrorCount, ansi);
20652
21159
  }
20653
- return ExitCode.Ok;
20654
21160
  }
20655
21161
  #renderSummaryHeader(enabled, badCount, warnings) {
20656
21162
  this.printer.data(
@@ -20687,10 +21193,10 @@ var PluginsDoctorCommand = class extends SmCommand {
20687
21193
  const statusLabelWidth = Math.max(...STATUS_ORDER.map((s) => s.length));
20688
21194
  this.printer.data(PLUGINS_TEXTS.doctorStatusHeader);
20689
21195
  for (const status of STATUS_ORDER) {
20690
- const count = counts[status];
20691
- const isProblem = status !== "enabled" && status !== "disabled" && count > 0;
21196
+ const count3 = counts[status];
21197
+ const isProblem = status !== "enabled" && status !== "disabled" && count3 > 0;
20692
21198
  const label = status.padEnd(statusLabelWidth);
20693
- const formattedCount = isProblem ? ansi.red(String(count)) : String(count);
21199
+ const formattedCount = isProblem ? ansi.red(String(count3)) : String(count3);
20694
21200
  this.printer.data(
20695
21201
  tx(PLUGINS_TEXTS.doctorStatusRow, {
20696
21202
  label: isProblem ? ansi.red(label) : label,
@@ -20753,6 +21259,42 @@ var PluginsDoctorCommand = class extends SmCommand {
20753
21259
  }
20754
21260
  }
20755
21261
  }
21262
+ /**
21263
+ * "off-shape visible" follow-up. Render the last scan's runtime
21264
+ * contribution rejections grouped by plugin: one red entry per plugin
21265
+ * (id + this plugin's error count), then up to
21266
+ * `CONTRIB_ERROR_SAMPLE_CAP` wrapped sample messages, then a dimmed
21267
+ * "... and N more" note when the group overflows the cap. The full
21268
+ * set is always available via `--json`.
21269
+ */
21270
+ #renderContributionErrors(groups, total, ansi) {
21271
+ this.printer.data(tx(PLUGINS_TEXTS.doctorContribErrorsHeader, { count: total }));
21272
+ const glyph = ansi.red(PLUGINS_TEXTS.rowGlyphOff);
21273
+ for (const group of groups) {
21274
+ this.printer.data(
21275
+ tx(PLUGINS_TEXTS.doctorContribErrorEntry, {
21276
+ glyph,
21277
+ pluginId: sanitizeForTerminal(group.pluginId),
21278
+ count: group.errors.length
21279
+ })
21280
+ );
21281
+ for (const err of group.errors.slice(0, CONTRIB_ERROR_SAMPLE_CAP)) {
21282
+ for (const line of wrapText(sanitizeForTerminal(err.message), 64)) {
21283
+ this.printer.data(
21284
+ tx(PLUGINS_TEXTS.doctorContribErrorBody, { line: ansi.dim(line) })
21285
+ );
21286
+ }
21287
+ }
21288
+ const hidden = group.errors.length - CONTRIB_ERROR_SAMPLE_CAP;
21289
+ if (hidden > 0) {
21290
+ this.printer.data(
21291
+ tx(PLUGINS_TEXTS.doctorContribErrorMore, {
21292
+ line: ansi.dim(tx(PLUGINS_TEXTS.doctorContribErrorMoreText, { count: hidden }))
21293
+ })
21294
+ );
21295
+ }
21296
+ }
21297
+ }
20756
21298
  };
20757
21299
  function countByStatus(builtIns2, plugins, resolveEnabled) {
20758
21300
  const counts = {
@@ -20949,6 +21491,15 @@ function buildDoctorJsonEnvelope(args2) {
20949
21491
  })
20950
21492
  });
20951
21493
  }
21494
+ const contributionErrors = args2.contribErrors.map((e) => ({
21495
+ pluginId: sanitizeForTerminal(e.pluginId),
21496
+ extensionId: sanitizeForTerminal(e.extensionId),
21497
+ nodePath: sanitizeForTerminal(e.nodePath),
21498
+ reason: sanitizeForTerminal(e.reason),
21499
+ message: sanitizeForTerminal(e.message),
21500
+ ...e.contributionId !== void 0 ? { contributionId: sanitizeForTerminal(e.contributionId) } : {},
21501
+ ...e.slot !== void 0 ? { slot: sanitizeForTerminal(e.slot) } : {}
21502
+ }));
20952
21503
  return {
20953
21504
  ok: true,
20954
21505
  kind: "plugins.doctor",
@@ -20963,9 +21514,35 @@ function buildDoctorJsonEnvelope(args2) {
20963
21514
  },
20964
21515
  issues,
20965
21516
  warnings,
21517
+ contributionErrors,
20966
21518
  elapsedMs: args2.elapsedMs
20967
21519
  };
20968
21520
  }
21521
+ async function loadContributionErrors() {
21522
+ const ctx = defaultRuntimeContext();
21523
+ const dbPath = resolveDbPath({ db: void 0, cwd: ctx.cwd });
21524
+ try {
21525
+ const rows = await tryWithSqlite(
21526
+ { databasePath: dbPath, autoBackup: false },
21527
+ (adapter) => adapter.contributions.listAllErrors()
21528
+ );
21529
+ return rows ?? [];
21530
+ } catch {
21531
+ return [];
21532
+ }
21533
+ }
21534
+ function groupContributionErrorsByPlugin(errors) {
21535
+ const byPlugin = /* @__PURE__ */ new Map();
21536
+ for (const err of errors) {
21537
+ let group = byPlugin.get(err.pluginId);
21538
+ if (!group) {
21539
+ group = { pluginId: err.pluginId, errors: [] };
21540
+ byPlugin.set(err.pluginId, group);
21541
+ }
21542
+ group.errors.push(err);
21543
+ }
21544
+ return [...byPlugin.values()];
21545
+ }
20969
21546
 
20970
21547
  // cli/commands/plugins/toggle.ts
20971
21548
  import { Command as Command25, Option as Option24 } from "clipanion";
@@ -21481,9 +22058,22 @@ function stub2(extId) {
21481
22058
  * Declared settings (\`settings\`):
21482
22059
  * - 'keywords' (${DEFAULT_INPUT_TYPE}) \u2192 exposed as ctx.settings.keywords
21483
22060
  *
22061
+ * View contributions: declare each one as a const, list it in \`ui\` by
22062
+ * shorthand, and pass that SAME const (by reference, not a string id) to
22063
+ * \`ctx.emitContribution\`. The kernel recovers the contribution from the
22064
+ * object identity. (Write the plugin in TypeScript and add
22065
+ * \`satisfies IViewContribution\` from '@skill-map/cli' to get a typed payload.)
22066
+ *
21484
22067
  * See: spec/plugin-author-guide.md \xA7View contributions
21485
22068
  * spec/view-slots.md
21486
22069
  */
22070
+ const count = {
22071
+ slot: '${DEFAULT_SLOT}',
22072
+ icon: '\u{1F50D}',
22073
+ label: 'kw',
22074
+ emitWhenEmpty: false,
22075
+ };
22076
+
21487
22077
  export default {
21488
22078
  version: '0.1.0',
21489
22079
  description: 'Counts configured keywords per node.',
@@ -21499,14 +22089,7 @@ export default {
21499
22089
  },
21500
22090
  },
21501
22091
 
21502
- ui: {
21503
- count: {
21504
- slot: '${DEFAULT_SLOT}',
21505
- icon: '\u{1F50D}',
21506
- label: 'kw',
21507
- emitWhenEmpty: false,
21508
- },
21509
- },
22092
+ ui: { count },
21510
22093
 
21511
22094
  extract(ctx) {
21512
22095
  const keywords = (ctx.settings && ctx.settings.keywords) || ['TODO', 'FIXME'];
@@ -21516,7 +22099,7 @@ export default {
21516
22099
  total += (ctx.body.match(re) || []).length;
21517
22100
  }
21518
22101
  if (total > 0) {
21519
- ctx.emitContribution('count', { value: total });
22102
+ ctx.emitContribution(count, { value: total });
21520
22103
  }
21521
22104
  },
21522
22105
  };
@@ -21810,8 +22393,8 @@ var VIEW_SLOTS_CATALOG = [
21810
22393
  { id: "card.footer.left", summary: "Counter chip in the left footer of the card." },
21811
22394
  { id: "card.footer.right", summary: "Counter chip in the right footer of the card." },
21812
22395
  { id: "graph.node.alert", summary: 'Reserved corner badge on the graph node, special-case signals only. No core analyzer emits here; routine "this node has a problem" findings belong in `card.footer.right`.' },
21813
- { id: "inspector.header.badge.counter", summary: "Counter chip in the inspector header badge cluster." },
21814
- { id: "inspector.header.badge.tag", summary: "Qualitative tag chip in the inspector header badge cluster." },
22396
+ { id: "inspector.header.badge", summary: "Unified badge in the inspector header cluster: icon and/or label and/or count, optional severity. Multi-cardinality, priority order, modeled on card.footer.left. Replaces the retired inspector.header.badge.counter and inspector.header.badge.tag sub-slots." },
22397
+ { id: "inspector.action.button", summary: "Action button in the inspector. Click dispatches a kernel Action by qualified id via POST /api/actions/:id. Always emitted; the payload `enabled` flag carries the dynamic condition (e.g. isStale for the bump button)." },
21815
22398
  { id: "inspector.body.panel.breakdown", summary: "Top-N labeled values rendered as a bar chart in the inspector body." },
21816
22399
  { id: "inspector.body.panel.records", summary: "Tabular data (rows \xD7 columns \u2264 50 \xD7 6) in the inspector body." },
21817
22400
  { id: "inspector.body.panel.tree", summary: "Recursive label/children hierarchy (depth \u2264 6, total \u2264 200) in the inspector body." },
@@ -22109,15 +22692,15 @@ var RefreshCommand = class extends SmCommand {
22109
22692
  return ExitCode.Ok;
22110
22693
  }
22111
22694
  const glyph = ansi.green("\u2713");
22112
- const count = freshEnrichments.length;
22113
- const noun = count === 1 ? REFRESH_TEXTS.refreshNounSingular : REFRESH_TEXTS.refreshNounPlural;
22695
+ const count3 = freshEnrichments.length;
22696
+ const noun = count3 === 1 ? REFRESH_TEXTS.refreshNounSingular : REFRESH_TEXTS.refreshNounPlural;
22114
22697
  if (this.stale) {
22115
22698
  const nodeCount = targetNodes.length;
22116
22699
  const nodeNoun = nodeCount === 1 ? REFRESH_TEXTS.refreshNodeNounSingular : REFRESH_TEXTS.refreshNodeNounPlural;
22117
22700
  this.printer.data(
22118
22701
  tx(REFRESH_TEXTS.refreshSuccessStale, {
22119
22702
  glyph,
22120
- count,
22703
+ count: count3,
22121
22704
  noun,
22122
22705
  nodeCount,
22123
22706
  nodeNoun
@@ -22127,7 +22710,7 @@ var RefreshCommand = class extends SmCommand {
22127
22710
  this.printer.data(
22128
22711
  tx(REFRESH_TEXTS.refreshSuccessSingle, {
22129
22712
  glyph,
22130
- count,
22713
+ count: count3,
22131
22714
  noun,
22132
22715
  nodePath: this.nodePath
22133
22716
  })
@@ -22604,7 +23187,15 @@ function createWatcherRuntime(opts) {
22604
23187
  runOptions.priorExtractorRuns = priorState.extractorRuns;
22605
23188
  }
22606
23189
  const ran = await runScanWithRenames(kernel, runOptions);
22607
- const { result, renameOps, extractorRuns, enrichments, contributions, freshlyRunTuples } = ran;
23190
+ const {
23191
+ result,
23192
+ renameOps,
23193
+ extractorRuns,
23194
+ enrichments,
23195
+ contributions,
23196
+ contributionErrors,
23197
+ freshlyRunTuples
23198
+ } = ran;
22608
23199
  await withSqlite(
22609
23200
  { databasePath: opts.dbPath },
22610
23201
  (writer) => writer.scans.persist(result, {
@@ -22612,6 +23203,7 @@ function createWatcherRuntime(opts) {
22612
23203
  extractorRuns,
22613
23204
  enrichments,
22614
23205
  contributions,
23206
+ contributionErrors,
22615
23207
  registeredContributionKeys: collectRegisteredContributionKeys(composed),
22616
23208
  freshlyRunTuples
22617
23209
  })
@@ -22940,11 +23532,11 @@ async function runWatchLoop(opts) {
22940
23532
  }
22941
23533
  void info;
22942
23534
  },
22943
- onBreakerTripped: (count, message) => {
23535
+ onBreakerTripped: (count3, message) => {
22944
23536
  context.stderr.write(
22945
23537
  tx(WATCH_TEXTS.breakerTripped, {
22946
23538
  glyph: errGlyph,
22947
- count,
23539
+ count: count3,
22948
23540
  hint: stderrAnsi.dim(tx(WATCH_TEXTS.breakerTrippedHint, { message }))
22949
23541
  })
22950
23542
  );
@@ -23486,8 +24078,8 @@ function formatScanCounts(opts) {
23486
24078
  }
23487
24079
  return parts.join(" \xB7 ");
23488
24080
  }
23489
- function countNoun(count, singular, plural) {
23490
- return count === 1 ? singular : plural;
24081
+ function countNoun(count3, singular, plural) {
24082
+ return count3 === 1 ? singular : plural;
23491
24083
  }
23492
24084
 
23493
24085
  // cli/commands/scan-compare.ts
@@ -23769,7 +24361,7 @@ import { WebSocketServer } from "ws";
23769
24361
  // server/app.ts
23770
24362
  import { Hono } from "hono";
23771
24363
  import { bodyLimit } from "hono/body-limit";
23772
- import { HTTPException as HTTPException16 } from "hono/http-exception";
24364
+ import { HTTPException as HTTPException17 } from "hono/http-exception";
23773
24365
 
23774
24366
  // core/config/service.ts
23775
24367
  var ConfigService = class {
@@ -23932,33 +24524,33 @@ var SERVER_TEXTS = {
23932
24524
  // Hono's `app.notFound` fallback, every other unmatched path funnels
23933
24525
  // here (after static + SPA fallback have had their turn).
23934
24526
  unknownPath: "Not found: {{path}}.",
23935
- // ---- sidecar bump route (routes/sidecar.ts) ------------------------------
23936
- // 409 refusal when a fresh node is bumped without `force`. Dispatch
23937
- // is via the typed `ConflictError` (`code: 'sidecar-fresh'`), so the
23938
- // `sidecar-fresh:` prefix is NOT load-bearing; it stays only for
23939
- // log-grep affinity with the CLI's bump verb.
23940
- sidecarFreshRefusal: "sidecar-fresh: Node is fresh; pass force:true to bump anyway.",
24527
+ // ---- generic action-dispatch route (routes/actions.ts, Step 17) ----------
24528
+ //
24529
+ // POST /api/actions/:qualifiedId. Generalises the legacy bump route:
24530
+ // resolve the qualified action id off the kernel registry, invoke it,
24531
+ // materialise any sidecar writes through the same consent-gated store,
24532
+ // broadcast `action.applied`. Each error keeps its own message key so
24533
+ // the UI / log can disambiguate without regex on the message.
23941
24534
  // 400 envelopes thrown by `parseBody` when the request payload is
23942
- // malformed. Each branch has its own key so the UI / log can
23943
- // disambiguate without regex on the message.
23944
- sidecarBodyNotJson: "Request body must be valid JSON.",
23945
- sidecarBodyNotObject: "Request body must be a JSON object.",
23946
- sidecarNodePathRequired: "`nodePath` is required and must be a non-empty string.",
23947
- sidecarForceMustBeBoolean: "`force` must be a boolean when present.",
23948
- sidecarConfirmMustBeBoolean: "`confirm` must be a boolean when present.",
23949
- /**
23950
- * 412 envelope when `POST /api/sidecar/bump` would create a `.sm`
23951
- * file but `allowEditSmFiles` is still false. The UI's bump
23952
- * call-path catches `code: 'confirm-required'` and opens a
23953
- * `ConfirmationService` dialog explaining `.sm` writes; on accept
23954
- * it retries with `confirm: true` in the body.
23955
- */
23956
- sidecarConsentRequired: "consent required to write .sm sidecar files in this project. Retry with `confirm: true` to grant (writes to .skill-map/settings.local.json, gitignored).",
23957
- // 500 envelope when the built-in bump action ships without an
23958
- // `invoke()`, should be impossible in production but the route
23959
- // throws a typed envelope rather than a bare `Error` so the global
23960
- // `app.onError` can format it.
23961
- sidecarBumpInvokeMissing: "built-in bump action is missing its invoke().",
24535
+ // malformed.
24536
+ actionBodyNotJson: "Request body must be valid JSON.",
24537
+ actionBodyNotObject: "Request body must be a JSON object.",
24538
+ actionNodePathRequired: "`nodePath` is required and must be a non-empty string.",
24539
+ actionInputMustBeObject: "`input` must be an object when present.",
24540
+ actionConfirmMustBeBoolean: "`confirm` must be a boolean when present.",
24541
+ actionAlwaysMustBeBoolean: "`always` must be a boolean when present.",
24542
+ // 404 envelope when `:qualifiedId` does not resolve to a registered
24543
+ // action with an `invoke()`. Covers both "no such action" and "action
24544
+ // exists but ships no deterministic entry point" (a probabilistic
24545
+ // action that cannot be dispatched over this synchronous route). The
24546
+ // id is sanitised before interpolation.
24547
+ actionUnknown: 'No invokable action with id "{{actionId}}".',
24548
+ // 409 envelope when an action's report comes back `ok: false`. The
24549
+ // refusal `reason` (when the report carries one) becomes the envelope
24550
+ // `code`; the fallback `action-refused` covers reports that refuse
24551
+ // without naming a reason. Dispatch is via the typed
24552
+ // `ActionRefusedError`; the message is informational only.
24553
+ actionRefused: 'Action "{{actionId}}" refused to run on "{{nodePath}}".',
23962
24554
  // ---- POST /api/scan (manual refresh) ------------------------------------
23963
24555
  // 400, runtime cannot persist a meaningful scan because the boot
23964
24556
  // dropped half the pipeline. Same gate the `?fresh=1` GET applies.
@@ -25029,6 +25621,8 @@ function registerPluginsRoute(app, deps) {
25029
25621
  app.get("/api/plugins", async (c) => {
25030
25622
  const resolveEnabled = await buildFreshResolver2(deps);
25031
25623
  const items = listItems(deps, resolveEnabled);
25624
+ const errorsByPlugin = await loadRuntimeContributionErrors(deps);
25625
+ attachRuntimeContributionErrors(items, errorsByPlugin);
25032
25626
  return c.json(
25033
25627
  buildListEnvelope({
25034
25628
  kind: "plugins",
@@ -25203,6 +25797,41 @@ function firstVersion(extensions) {
25203
25797
  function classifyPluginSource(_pluginPath, _deps) {
25204
25798
  return "project";
25205
25799
  }
25800
+ async function loadRuntimeContributionErrors(deps) {
25801
+ try {
25802
+ const rows = await tryWithSqlite(
25803
+ { databasePath: deps.options.dbPath, autoBackup: false },
25804
+ (adapter) => adapter.contributions.listAllErrors()
25805
+ );
25806
+ if (rows === null) return /* @__PURE__ */ new Map();
25807
+ return groupContributionErrorsByPlugin2(rows);
25808
+ } catch {
25809
+ return /* @__PURE__ */ new Map();
25810
+ }
25811
+ }
25812
+ function groupContributionErrorsByPlugin2(rows) {
25813
+ const out = /* @__PURE__ */ new Map();
25814
+ for (const row of rows) {
25815
+ const projected = {
25816
+ extensionId: row.extensionId,
25817
+ nodePath: row.nodePath,
25818
+ reason: row.reason,
25819
+ message: row.message,
25820
+ ...row.contributionId !== void 0 ? { contributionId: row.contributionId } : {},
25821
+ ...row.slot !== void 0 ? { slot: row.slot } : {}
25822
+ };
25823
+ const list = out.get(row.pluginId);
25824
+ if (list) list.push(projected);
25825
+ else out.set(row.pluginId, [projected]);
25826
+ }
25827
+ return out;
25828
+ }
25829
+ function attachRuntimeContributionErrors(items, errorsByPlugin) {
25830
+ for (const item of items) {
25831
+ const errors = errorsByPlugin.get(item.id);
25832
+ if (errors && errors.length > 0) item.runtimeContributionErrors = errors;
25833
+ }
25834
+ }
25206
25835
  async function persistAndProject(c, deps, configKey, enabled) {
25207
25836
  const overrides = await tryWithSqlite(
25208
25837
  { databasePath: deps.options.dbPath, autoBackup: false },
@@ -25892,8 +26521,168 @@ var parsePatchBody5 = makeBodyValidator(PATCH_BODY_SCHEMA4, {
25892
26521
  }
25893
26522
  });
25894
26523
 
25895
- // server/routes/scan.ts
26524
+ // server/routes/actions.ts
26525
+ import { HTTPException as HTTPException15 } from "hono/http-exception";
26526
+ import { resolve as resolve36 } from "path";
26527
+
26528
+ // server/routes/node-loader.ts
25896
26529
  import { HTTPException as HTTPException14 } from "hono/http-exception";
26530
+ async function loadNode(deps, nodePath) {
26531
+ const persisted = await tryWithSqlite(
26532
+ { databasePath: deps.options.dbPath, autoBackup: false },
26533
+ async (adapter) => adapter.scans.load()
26534
+ );
26535
+ const node = persisted?.nodes.find((n) => n.path === nodePath);
26536
+ if (!node) {
26537
+ throw new HTTPException14(404, {
26538
+ message: tx(SERVER_TEXTS.nodeNotFound, { path: sanitizeForTerminal(nodePath) })
26539
+ });
26540
+ }
26541
+ return node;
26542
+ }
26543
+
26544
+ // server/routes/actions.ts
26545
+ var ENVELOPE_KIND2 = "action.applied";
26546
+ var REFUSED_CODE = "action-refused";
26547
+ var INVOKER_FALLBACK = "ui";
26548
+ var QUALIFIED_ID_SEGMENT2 = /^[A-Za-z0-9._-]+$/;
26549
+ var ACTION_BODY_SCHEMA = {
26550
+ type: "object",
26551
+ additionalProperties: false,
26552
+ required: ["nodePath"],
26553
+ properties: {
26554
+ nodePath: { type: "string", minLength: 1 },
26555
+ input: { type: "object" },
26556
+ confirm: { type: "boolean" },
26557
+ always: { type: "boolean" }
26558
+ }
26559
+ };
26560
+ var parseBody = makeBodyValidator(ACTION_BODY_SCHEMA, {
26561
+ notJson: SERVER_TEXTS.actionBodyNotJson,
26562
+ notObject: SERVER_TEXTS.actionBodyNotObject,
26563
+ invalid: SERVER_TEXTS.actionBodyNotObject,
26564
+ mapping: {
26565
+ "/nodePath:required": SERVER_TEXTS.actionNodePathRequired,
26566
+ ":type:object": SERVER_TEXTS.actionBodyNotObject,
26567
+ "/nodePath:type:string": SERVER_TEXTS.actionNodePathRequired,
26568
+ "/nodePath:minLength": SERVER_TEXTS.actionNodePathRequired,
26569
+ "/input:type:object": SERVER_TEXTS.actionInputMustBeObject,
26570
+ "/confirm:type:boolean": SERVER_TEXTS.actionConfirmMustBeBoolean,
26571
+ "/always:type:boolean": SERVER_TEXTS.actionAlwaysMustBeBoolean
26572
+ }
26573
+ });
26574
+ function registerActionsRoutes(app, deps) {
26575
+ app.post("/api/actions/:pluginId/:actionId", async (c) => {
26576
+ const startedAt = Date.now();
26577
+ const pluginId = parseSegment(c.req.param("pluginId"), "pluginId");
26578
+ const shortId = parseSegment(c.req.param("actionId"), "actionId");
26579
+ const actionId = qualifiedExtensionId(pluginId, shortId);
26580
+ const action = resolveInvokableAction(deps.kernel, actionId);
26581
+ const body = await parseBody(c.req.raw);
26582
+ const node = await loadNode(deps, body.nodePath);
26583
+ let absPath;
26584
+ try {
26585
+ assertContained(deps.runtimeContext.cwd, node.path);
26586
+ absPath = resolve36(deps.runtimeContext.cwd, node.path);
26587
+ } catch (err) {
26588
+ throw new HTTPException15(400, { message: formatErrorMessage(err) });
26589
+ }
26590
+ const result = invokeAction(action, absPath, node, body, deps.runtimeContext.cwd);
26591
+ const report = result.report;
26592
+ if (report.ok === false) {
26593
+ const reason = typeof report.reason === "string" && report.reason.length > 0 ? sanitizeForTerminal(report.reason) : REFUSED_CODE;
26594
+ throw new ActionRefusedError({
26595
+ code: reason,
26596
+ message: tx(SERVER_TEXTS.actionRefused, {
26597
+ actionId: sanitizeForTerminal(actionId),
26598
+ nodePath: sanitizeForTerminal(node.path)
26599
+ }),
26600
+ actionId,
26601
+ nodePath: node.path,
26602
+ report: result.report
26603
+ });
26604
+ }
26605
+ if (report.ok === true && report.noop === true) {
26606
+ return c.json(buildEnvelope5(actionId, node.path, result.report, startedAt));
26607
+ }
26608
+ await materializeWrites(result.writes, body, deps.runtimeContext.cwd);
26609
+ if (body.always === true) {
26610
+ deps.configService.reload();
26611
+ }
26612
+ const eventData = {
26613
+ actionId,
26614
+ nodePath: node.path,
26615
+ report: result.report
26616
+ };
26617
+ const wsEnvelope = {
26618
+ type: ENVELOPE_KIND2,
26619
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
26620
+ data: eventData
26621
+ };
26622
+ deps.broadcaster.broadcast(wsEnvelope);
26623
+ return c.json(buildEnvelope5(actionId, node.path, result.report, startedAt));
26624
+ });
26625
+ }
26626
+ function parseSegment(value, name) {
26627
+ if (!QUALIFIED_ID_SEGMENT2.test(value)) {
26628
+ throw new HTTPException15(400, {
26629
+ message: tx(SERVER_TEXTS.qualifiedIdMalformed, {
26630
+ name,
26631
+ value: sanitizeForTerminal(value)
26632
+ })
26633
+ });
26634
+ }
26635
+ return value;
26636
+ }
26637
+ function resolveInvokableAction(kernel, actionId) {
26638
+ const ext = kernel.registry.get("action", actionId);
26639
+ const action = ext;
26640
+ if (!action || typeof action.invoke !== "function") {
26641
+ throw new HTTPException15(404, {
26642
+ message: tx(SERVER_TEXTS.actionUnknown, { actionId: sanitizeForTerminal(actionId) })
26643
+ });
26644
+ }
26645
+ return action;
26646
+ }
26647
+ function invokeAction(action, absPath, node, body, cwd) {
26648
+ const invoke = action.invoke;
26649
+ const ctx = {
26650
+ node,
26651
+ nodeAbsolutePath: absPath,
26652
+ invoker: resolveGitAuthorName(cwd) ?? INVOKER_FALLBACK,
26653
+ now: () => /* @__PURE__ */ new Date(),
26654
+ settings: {}
26655
+ };
26656
+ return invoke(body.input ?? {}, ctx);
26657
+ }
26658
+ async function materializeWrites(writes, body, cwd) {
26659
+ const store = new FilesystemSidecarStore(ensureSidecarWritesAllowed);
26660
+ try {
26661
+ for (const w of writes ?? []) {
26662
+ if (w.kind === "sidecar") {
26663
+ await store.applyPatch(w.path, w.changes, {
26664
+ confirm: body.confirm === true,
26665
+ always: body.always === true,
26666
+ cwd
26667
+ });
26668
+ }
26669
+ }
26670
+ } catch (err) {
26671
+ if (err instanceof EConsentRequiredError) throw err;
26672
+ throw new HTTPException15(500, { message: formatErrorMessage(err) });
26673
+ }
26674
+ }
26675
+ function buildEnvelope5(actionId, nodePath, report, startedAt) {
26676
+ return {
26677
+ schemaVersion: "1",
26678
+ kind: ENVELOPE_KIND2,
26679
+ value: { actionId, nodePath, report },
26680
+ elapsedMs: Date.now() - startedAt
26681
+ };
26682
+ }
26683
+
26684
+ // server/routes/scan.ts
26685
+ import { HTTPException as HTTPException16 } from "hono/http-exception";
25897
26686
 
25898
26687
  // server/scan-mutex.ts
25899
26688
  var inFlight = null;
@@ -26072,7 +26861,7 @@ function registerScanRoute(app, deps) {
26072
26861
  }
26073
26862
  async function runPersistedScan(c, deps) {
26074
26863
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
26075
- throw new HTTPException14(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
26864
+ throw new HTTPException16(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
26076
26865
  }
26077
26866
  const dbExists = await tryWithSqlite(
26078
26867
  { databasePath: deps.options.dbPath, autoBackup: false },
@@ -26109,7 +26898,7 @@ async function runPersistedScan(c, deps) {
26109
26898
  ...deps.options.maxNodes !== void 0 ? { maxNodes: deps.options.maxNodes } : {}
26110
26899
  });
26111
26900
  if (outcome.kind !== "ok") {
26112
- throw new HTTPException14(500, {
26901
+ throw new HTTPException16(500, {
26113
26902
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.scanGuardTrip, { existing: outcome.existing }) : outcome.message
26114
26903
  });
26115
26904
  }
@@ -26187,7 +26976,7 @@ function groupTagsByPath2(rows) {
26187
26976
  }
26188
26977
  async function runFreshScan(deps) {
26189
26978
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
26190
- throw new HTTPException14(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
26979
+ throw new HTTPException16(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
26191
26980
  }
26192
26981
  const resolveEnabledOverride = await buildBffResolverOverride(deps);
26193
26982
  const outcome = await runScanForCommand({
@@ -26222,7 +27011,7 @@ async function runFreshScan(deps) {
26222
27011
  ...deps.options.maxNodes !== void 0 ? { maxNodes: deps.options.maxNodes } : {}
26223
27012
  });
26224
27013
  if (outcome.kind !== "ok") {
26225
- throw new HTTPException14(500, {
27014
+ throw new HTTPException16(500, {
26226
27015
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.freshScanGuardTrip, { existing: outcome.existing }) : outcome.message
26227
27016
  });
26228
27017
  }
@@ -26268,141 +27057,6 @@ function emptyScanResult() {
26268
27057
  };
26269
27058
  }
26270
27059
 
26271
- // server/routes/sidecar.ts
26272
- import { HTTPException as HTTPException15 } from "hono/http-exception";
26273
- import { resolve as resolve36 } from "path";
26274
- var STATUS_FRESH = "fresh";
26275
- var ENVELOPE_KIND2 = "sidecar.bumped";
26276
- var BUMP_BODY_SCHEMA = {
26277
- type: "object",
26278
- additionalProperties: false,
26279
- required: ["nodePath"],
26280
- properties: {
26281
- nodePath: { type: "string", minLength: 1 },
26282
- force: { type: "boolean" },
26283
- confirm: { type: "boolean" }
26284
- }
26285
- };
26286
- var parseBody = makeBodyValidator(BUMP_BODY_SCHEMA, {
26287
- notJson: SERVER_TEXTS.sidecarBodyNotJson,
26288
- notObject: SERVER_TEXTS.sidecarBodyNotObject,
26289
- invalid: SERVER_TEXTS.sidecarBodyNotObject,
26290
- mapping: {
26291
- "/nodePath:required": SERVER_TEXTS.sidecarNodePathRequired,
26292
- ":type:object": SERVER_TEXTS.sidecarBodyNotObject,
26293
- "/nodePath:type:string": SERVER_TEXTS.sidecarNodePathRequired,
26294
- "/nodePath:minLength": SERVER_TEXTS.sidecarNodePathRequired,
26295
- "/force:type:boolean": SERVER_TEXTS.sidecarForceMustBeBoolean,
26296
- "/confirm:type:boolean": SERVER_TEXTS.sidecarConfirmMustBeBoolean
26297
- }
26298
- });
26299
- function registerSidecarRoutes(app, deps) {
26300
- app.post("/api/sidecar/bump", async (c) => {
26301
- const startedAt = Date.now();
26302
- const body = await parseBody(c.req.raw);
26303
- const node = await loadNode(deps, body.nodePath);
26304
- let absPath;
26305
- try {
26306
- assertContained(deps.runtimeContext.cwd, node.path);
26307
- absPath = resolve36(deps.runtimeContext.cwd, node.path);
26308
- } catch (err) {
26309
- throw new HTTPException15(400, { message: formatErrorMessage(err) });
26310
- }
26311
- const result = invokeBump2(node, absPath, body, deps.runtimeContext.cwd);
26312
- if (result.report.ok === false && result.report.reason === "fresh") {
26313
- throw new ConflictError({ code: "sidecar-fresh", message: SERVER_TEXTS.sidecarFreshRefusal });
26314
- }
26315
- if (result.report.ok === true && result.report.noop === true) {
26316
- const envelope2 = {
26317
- schemaVersion: "1",
26318
- kind: ENVELOPE_KIND2,
26319
- value: {
26320
- nodePath: node.path,
26321
- version: pickExistingVersion(node),
26322
- status: STATUS_FRESH
26323
- },
26324
- elapsedMs: Date.now() - startedAt
26325
- };
26326
- return c.json(envelope2);
26327
- }
26328
- const store = new FilesystemSidecarStore(ensureSidecarWritesAllowed);
26329
- try {
26330
- for (const w of result.writes ?? []) {
26331
- if (w.kind === "sidecar") {
26332
- await store.applyPatch(w.path, w.changes, {
26333
- confirm: body.confirm === true,
26334
- cwd: deps.runtimeContext.cwd
26335
- });
26336
- }
26337
- }
26338
- } catch (err) {
26339
- if (err instanceof EConsentRequiredError) throw err;
26340
- throw new HTTPException15(500, { message: formatErrorMessage(err) });
26341
- }
26342
- if (body.confirm === true) {
26343
- deps.configService.reload();
26344
- }
26345
- const newVersion = result.report.version ?? null;
26346
- const eventData = {
26347
- nodePath: node.path,
26348
- version: newVersion,
26349
- status: STATUS_FRESH
26350
- };
26351
- const wsEnvelope = {
26352
- type: ENVELOPE_KIND2,
26353
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
26354
- data: eventData
26355
- };
26356
- deps.broadcaster.broadcast(wsEnvelope);
26357
- const envelope = {
26358
- schemaVersion: "1",
26359
- kind: ENVELOPE_KIND2,
26360
- value: {
26361
- nodePath: node.path,
26362
- version: newVersion,
26363
- status: STATUS_FRESH
26364
- },
26365
- elapsedMs: Date.now() - startedAt
26366
- };
26367
- return c.json(envelope);
26368
- });
26369
- }
26370
- async function loadNode(deps, nodePath) {
26371
- const persisted = await tryWithSqlite(
26372
- { databasePath: deps.options.dbPath, autoBackup: false },
26373
- async (adapter) => adapter.scans.load()
26374
- );
26375
- const node = persisted?.nodes.find((n) => n.path === nodePath);
26376
- if (!node) {
26377
- throw new HTTPException15(404, {
26378
- message: tx(SERVER_TEXTS.nodeNotFound, { path: sanitizeForTerminal(nodePath) })
26379
- });
26380
- }
26381
- return node;
26382
- }
26383
- function invokeBump2(node, absPath, body, cwd) {
26384
- if (!nodeBumpAction.invoke) {
26385
- throw new HTTPException15(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
26386
- }
26387
- const input = {};
26388
- if (body.force === true) input.force = true;
26389
- return nodeBumpAction.invoke(input, {
26390
- node,
26391
- nodeAbsolutePath: absPath,
26392
- invoker: resolveGitAuthorName(cwd) ?? "ui",
26393
- now: () => /* @__PURE__ */ new Date(),
26394
- settings: {}
26395
- });
26396
- }
26397
- function pickExistingVersion(node) {
26398
- const overlay = node.sidecar;
26399
- if (!overlay || overlay.present !== true) return null;
26400
- const annotations = overlay.annotations;
26401
- if (!annotations) return null;
26402
- const v = annotations["version"];
26403
- return typeof v === "number" && Number.isFinite(v) ? v : null;
26404
- }
26405
-
26406
27060
  // server/routes/update-status.ts
26407
27061
  function registerUpdateStatusRoute(app, deps) {
26408
27062
  app.get("/api/update-status", async (c) => {
@@ -26569,13 +27223,13 @@ function attachBroadcasterRoute(app, broadcaster) {
26569
27223
 
26570
27224
  // server/app.ts
26571
27225
  var BODY_LIMIT_BYTES = 1024 * 1024;
26572
- var DbMissingError = class extends HTTPException16 {
27226
+ var DbMissingError = class extends HTTPException17 {
26573
27227
  constructor(message) {
26574
27228
  super(500, { message });
26575
27229
  this.name = "DbMissingError";
26576
27230
  }
26577
27231
  };
26578
- var BulkValidationError = class extends HTTPException16 {
27232
+ var BulkValidationError = class extends HTTPException17 {
26579
27233
  id;
26580
27234
  code;
26581
27235
  constructor(init) {
@@ -26585,7 +27239,7 @@ var BulkValidationError = class extends HTTPException16 {
26585
27239
  this.code = init.code;
26586
27240
  }
26587
27241
  };
26588
- var LoopbackGateError = class extends HTTPException16 {
27242
+ var LoopbackGateError = class extends HTTPException17 {
26589
27243
  code;
26590
27244
  constructor(init) {
26591
27245
  super(403, { message: init.message });
@@ -26593,7 +27247,7 @@ var LoopbackGateError = class extends HTTPException16 {
26593
27247
  this.code = init.code;
26594
27248
  }
26595
27249
  };
26596
- var ConflictError = class extends HTTPException16 {
27250
+ var ConflictError = class extends HTTPException17 {
26597
27251
  code;
26598
27252
  constructor(init) {
26599
27253
  super(409, { message: init.message });
@@ -26601,6 +27255,21 @@ var ConflictError = class extends HTTPException16 {
26601
27255
  this.code = init.code;
26602
27256
  }
26603
27257
  };
27258
+ var ActionRefusedError = class extends HTTPException17 {
27259
+ /** Refusal code: the report's `reason` when present, else `'action-refused'`. */
27260
+ code;
27261
+ details;
27262
+ constructor(init) {
27263
+ super(409, { message: init.message });
27264
+ this.name = "ActionRefusedError";
27265
+ this.code = init.code;
27266
+ this.details = {
27267
+ actionId: init.actionId,
27268
+ nodePath: init.nodePath,
27269
+ report: init.report
27270
+ };
27271
+ }
27272
+ };
26604
27273
  function createApp(deps) {
26605
27274
  const app = new Hono();
26606
27275
  const configService = new ConfigService({
@@ -26614,7 +27283,7 @@ function createApp(deps) {
26614
27283
  bodyLimit({
26615
27284
  maxSize: BODY_LIMIT_BYTES,
26616
27285
  onError: () => {
26617
- throw new HTTPException16(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
27286
+ throw new HTTPException17(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
26618
27287
  }
26619
27288
  })
26620
27289
  );
@@ -26650,7 +27319,7 @@ function createApp(deps) {
26650
27319
  registerGraphRoute(app, routeDeps);
26651
27320
  registerConfigRoute(app, routeDeps);
26652
27321
  registerPluginsRoute(app, routeDeps);
26653
- registerSidecarRoutes(app, { ...routeDeps, broadcaster: deps.broadcaster });
27322
+ registerActionsRoutes(app, { ...routeDeps, broadcaster: deps.broadcaster, kernel: deps.kernel });
26654
27323
  registerFavoritesRoutes(app, routeDeps);
26655
27324
  registerAnnotationsRoute(app, { kernel: deps.kernel });
26656
27325
  registerContributionsRoutes(app, { ...routeDeps, kernel: deps.kernel });
@@ -26660,7 +27329,7 @@ function createApp(deps) {
26660
27329
  registerActiveProviderRoute(app, routeDeps);
26661
27330
  registerProjectIgnoreRoute(app, routeDeps);
26662
27331
  app.all("/api/*", (c) => {
26663
- throw new HTTPException16(404, {
27332
+ throw new HTTPException17(404, {
26664
27333
  message: tx(SERVER_TEXTS.unknownApiEndpoint, { path: sanitizeForTerminal(c.req.path) })
26665
27334
  });
26666
27335
  });
@@ -26668,7 +27337,7 @@ function createApp(deps) {
26668
27337
  app.use("*", createStaticHandler({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
26669
27338
  app.get("*", createSpaFallback({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
26670
27339
  app.notFound((c) => {
26671
- throw new HTTPException16(404, {
27340
+ throw new HTTPException17(404, {
26672
27341
  message: tx(SERVER_TEXTS.unknownPath, { path: sanitizeForTerminal(c.req.path) })
26673
27342
  });
26674
27343
  });
@@ -26719,18 +27388,9 @@ function formatError2(err, c) {
26719
27388
  };
26720
27389
  return c.json(envelope, 403);
26721
27390
  }
26722
- if (err instanceof ConflictError) {
26723
- const envelope = {
26724
- ok: false,
26725
- error: {
26726
- code: err.code,
26727
- message: err.message,
26728
- details: null
26729
- }
26730
- };
26731
- return c.json(envelope, 409);
26732
- }
26733
- if (err instanceof HTTPException16) {
27391
+ const conflict = formatConflict(err, c);
27392
+ if (conflict) return conflict;
27393
+ if (err instanceof HTTPException17) {
26734
27394
  const status = err.status;
26735
27395
  const envelope = {
26736
27396
  ok: false,
@@ -26766,6 +27426,23 @@ function formatError2(err, c) {
26766
27426
  }
26767
27427
  return formatInternalErrorFallThrough(err, c);
26768
27428
  }
27429
+ function formatConflict(err, c) {
27430
+ if (err instanceof ActionRefusedError) {
27431
+ const envelope = {
27432
+ ok: false,
27433
+ error: { code: err.code, message: err.message, details: err.details }
27434
+ };
27435
+ return c.json(envelope, 409);
27436
+ }
27437
+ if (err instanceof ConflictError) {
27438
+ const envelope = {
27439
+ ok: false,
27440
+ error: { code: err.code, message: err.message, details: null }
27441
+ };
27442
+ return c.json(envelope, 409);
27443
+ }
27444
+ return null;
27445
+ }
26769
27446
  function formatInternalErrorFallThrough(err, c) {
26770
27447
  const detail = formatErrorMessage(err);
26771
27448
  const stack = err instanceof Error && typeof err.stack === "string" ? err.stack : void 0;
@@ -26991,6 +27668,8 @@ function entryFromRegistered(c) {
26991
27668
  if (c.icon !== void 0) entry.icon = c.icon;
26992
27669
  if (c.emptyText !== void 0) entry.emptyText = c.emptyText;
26993
27670
  if (c.priority !== void 0) entry.priority = c.priority;
27671
+ if (c.pluginOrder !== void 0) entry.pluginOrder = c.pluginOrder;
27672
+ if (c.extensionOrder !== void 0) entry.extensionOrder = c.extensionOrder;
26994
27673
  return entry;
26995
27674
  }
26996
27675
 
@@ -27248,8 +27927,10 @@ async function createServer(options, extra = {}) {
27248
27927
  }
27249
27928
  async function assemblePluginRuntime(options, runtimeContext) {
27250
27929
  const pluginRuntime = options.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime({ runtimeContext });
27251
- for (const warn of pluginRuntime.warnings) {
27252
- log.warn(sanitizeForTerminal(warn));
27930
+ if (options.noWatcher) {
27931
+ for (const warn of pluginRuntime.warnings) {
27932
+ log.warn(sanitizeForTerminal(warn));
27933
+ }
27253
27934
  }
27254
27935
  const builtInProviders = options.noBuiltIns ? [] : collectBuiltInProviders();
27255
27936
  const allProviders = [...builtInProviders, ...pluginRuntime.extensions.providers];
@@ -27260,6 +27941,11 @@ async function assemblePluginRuntime(options, runtimeContext) {
27260
27941
  function assembleKernel(pluginRuntime, noBuiltIns) {
27261
27942
  const kernel = createKernel();
27262
27943
  kernel.setRegisteredAnnotationKeys(pluginRuntime.annotationContributions);
27944
+ if (!noBuiltIns) {
27945
+ for (const action of builtIns().actions) {
27946
+ kernel.registry.register(action);
27947
+ }
27948
+ }
27263
27949
  const mergedViewContributions = [...pluginRuntime.viewContributions];
27264
27950
  if (!noBuiltIns) {
27265
27951
  const userKey = new Set(
@@ -28129,13 +28815,13 @@ var ShowCommand = class extends SmCommand {
28129
28815
  }
28130
28816
  };
28131
28817
  function renderHuman2(doc, ansi) {
28132
- const { node, linksOut, linksIn, issues } = doc;
28818
+ const { node, linksOut: linksOut2, linksIn: linksIn2, issues } = doc;
28133
28819
  const out = [];
28134
28820
  out.push(renderHeader(node, ansi));
28135
28821
  out.push(renderFieldBlock(node, ansi));
28136
28822
  out.push(renderFrontmatter(node, ansi));
28137
- if (linksOut.length > 0) out.push(renderLinksSection("out", linksOut, ansi));
28138
- if (linksIn.length > 0) out.push(renderLinksSection("in", linksIn, ansi));
28823
+ if (linksOut2.length > 0) out.push(renderLinksSection("out", linksOut2, ansi));
28824
+ if (linksIn2.length > 0) out.push(renderLinksSection("in", linksIn2, ansi));
28139
28825
  if (issues.length > 0) out.push(renderIssuesSection(issues, node.path, ansi));
28140
28826
  return out.join("");
28141
28827
  }
@@ -28527,7 +29213,9 @@ var SidecarRefreshCommand = class extends SmCommand {
28527
29213
  frontmatterHash: node.frontmatterHash
28528
29214
  }
28529
29215
  },
28530
- { confirm: this.yes, cwd: ctx.cwd }
29216
+ // Step 17 split: CLI accept / `--yes` persists the grant, so it
29217
+ // threads `always`, not the new one-shot `confirm`.
29218
+ { confirm: this.yes, always: this.yes, cwd: ctx.cwd }
28531
29219
  );
28532
29220
  } catch (err) {
28533
29221
  if (err instanceof EConsentRequiredError) throw err;
@@ -28808,7 +29496,9 @@ var SidecarAnnotateCommand = class extends SmCommand {
28808
29496
  await store.applyPatch(
28809
29497
  sidecarAbsPath,
28810
29498
  scaffoldSidecarObject(node),
28811
- { confirm: this.yes, cwd: ctx.cwd }
29499
+ // Step 17 split: CLI accept / `--yes` persists the grant, so it
29500
+ // threads `always`, not the new one-shot `confirm`.
29501
+ { confirm: this.yes, always: this.yes, cwd: ctx.cwd }
28812
29502
  );
28813
29503
  } catch (err) {
28814
29504
  if (err instanceof EConsentRequiredError) throw err;
@@ -29069,11 +29759,13 @@ var TUTORIAL_TEXTS = {
29069
29759
  // the error shape: glyph + headline + dim hint.
29070
29760
  notEmpty: "{{glyph}} sm tutorial: the current directory is not empty (found {{entries}})\n {{hint}}\n",
29071
29761
  notEmptyHint: "sm tutorial seeds a self-contained scenario; run it in a fresh empty directory, or pass `--force` to use this one anyway.",
29072
- // Invalid `variant` positional argument. Goes to stderr, exit code 2.
29073
- // Mirrors the error shape: glyph + headline + dim hint enumerating the
29074
- // valid values.
29075
- invalidVariant: "{{glyph}} sm tutorial: unknown variant '{{variant}}'\n {{hint}}\n",
29076
- invalidVariantHint: "Valid values: tutorial (default), master.",
29762
+ // Legacy positional argument (e.g. a stale `sm tutorial master`). The
29763
+ // verb no longer takes a positional: there is a single umbrella skill
29764
+ // and the advanced walkthrough is a menu choice inside it, not a
29765
+ // separate install. Goes to stderr, exit code 2. Mirrors the error
29766
+ // shape: glyph + headline + dim hint.
29767
+ legacyPositional: "{{glyph}} sm tutorial: unexpected argument '{{arg}}'\n {{hint}}\n",
29768
+ legacyPositionalHint: "sm tutorial takes no positional argument. The master walkthrough is no longer a separate install; run `sm tutorial` and pick the advanced parts from the in-skill menu.",
29077
29769
  // I/O failure on write or on reading the bundled skill source.
29078
29770
  writeFailed: "{{glyph}} sm tutorial: failed to write {{target}}: {{message}}\n",
29079
29771
  sourceMissing: "{{glyph}} sm tutorial: could not read the bundled skill payload for {{target}} from the install.\n {{hint}}\n",
@@ -29081,47 +29773,36 @@ var TUTORIAL_TEXTS = {
29081
29773
  };
29082
29774
 
29083
29775
  // cli/commands/tutorial.ts
29084
- var VALID_VARIANTS = ["tutorial", "master"];
29085
- var DEFAULT_VARIANT = "tutorial";
29086
- var VARIANT_SPECS = {
29087
- tutorial: {
29088
- slug: "sm-tutorial",
29089
- sourceDir: ".claude/skills/sm-tutorial",
29090
- triggerEn: "run the tutorial",
29091
- triggerEs: "ejecuta el tutorial"
29092
- },
29093
- master: {
29094
- slug: "sm-master",
29095
- sourceDir: ".claude/skills/sm-master",
29096
- triggerEn: "run the master tutorial",
29097
- triggerEs: "ejecuta el tutorial maestro"
29098
- }
29099
- };
29776
+ var SKILL_SLUG = "sm-tutorial";
29777
+ var SKILL_SOURCE_DIR = ".claude/skills/sm-tutorial";
29778
+ var TRIGGER_EN = "run the tutorial";
29779
+ var TRIGGER_ES = "ejecuta el tutorial";
29100
29780
  var TutorialCommand = class extends SmCommand {
29101
29781
  static paths = [["tutorial"]];
29102
29782
  static usage = Command37.Usage({
29103
29783
  category: "Setup",
29104
29784
  description: "Materialize an interactive tester tutorial as a Claude Code skill folder under `<cwd>/.claude/skills/`.",
29105
29785
  details: `
29106
- Drops the canonical skill directory (SKILL.md + any references/
29107
- sub-folder) under \`<cwd>/.claude/skills/sm-tutorial/\` (default)
29108
- or \`<cwd>/.claude/skills/sm-master/\` (when invoked as \`sm
29109
- tutorial master\`). Claude Code auto-discovers the skill the
29110
- next time it boots in this directory; the tester invokes it by
29111
- speaking one of its trigger phrases.
29786
+ Drops the canonical skill directory (SKILL.md + its references/
29787
+ sub-folder) under \`<cwd>/.claude/skills/sm-tutorial/\`. Claude
29788
+ Code auto-discovers the skill the next time it boots in this
29789
+ directory; the tester invokes it by speaking one of its trigger
29790
+ phrases and picks the advanced parts from the in-skill menu.
29112
29791
 
29113
29792
  Does NOT require an initialized .skill-map/ project. Refuses to
29114
- overwrite the target directory unless --force is passed. Valid
29115
- values for the positional argument are: tutorial (default),
29116
- master.
29793
+ overwrite the target directory unless --force is passed. Takes no
29794
+ positional argument.
29117
29795
  `,
29118
29796
  examples: [
29119
- ["Materialize the basic tutorial skill in the cwd", "$0 tutorial"],
29120
- ["Materialize the advanced tutorial skill in the cwd", "$0 tutorial master"],
29797
+ ["Materialize the tutorial skill in the cwd", "$0 tutorial"],
29121
29798
  ["Overwrite an existing target directory", "$0 tutorial --force"]
29122
29799
  ]
29123
29800
  });
29124
- variant = Option35.String({ required: false });
29801
+ // Legacy positional catcher: the verb takes no positional argument any
29802
+ // more. Accept one so a stale `sm tutorial master` lands on a friendly
29803
+ // usage error (guarded in `run()`) instead of clipanion's generic
29804
+ // "extraneous argument" message.
29805
+ legacyPositional = Option35.String({ required: false });
29125
29806
  // Named `forProvider`, NOT `for` (reserved word). The CLI surface stays
29126
29807
  // `--for`; selects the destination Provider whose `scaffold.skillDir`
29127
29808
  // the skill is materialised under, skipping the interactive prompt.
@@ -29137,9 +29818,16 @@ var TutorialCommand = class extends SmCommand {
29137
29818
  const stderr = this.context.stderr;
29138
29819
  const stderrAnsi = this.ansiFor("stderr");
29139
29820
  const errGlyph = stderrAnsi.red("\u2715");
29140
- const variant = this.resolveVariantArg(errGlyph, stderrAnsi);
29141
- if (variant === null) return ExitCode.Error;
29142
- const spec = VARIANT_SPECS[variant];
29821
+ if (this.legacyPositional !== void 0) {
29822
+ this.printer.error(
29823
+ tx(TUTORIAL_TEXTS.legacyPositional, {
29824
+ glyph: errGlyph,
29825
+ arg: this.legacyPositional,
29826
+ hint: stderrAnsi.dim(TUTORIAL_TEXTS.legacyPositionalHint)
29827
+ })
29828
+ );
29829
+ return ExitCode.Error;
29830
+ }
29143
29831
  if (!this.force && !isDirEmpty(ctx.cwd)) {
29144
29832
  this.printer.error(
29145
29833
  tx(TUTORIAL_TEXTS.notEmpty, {
@@ -29153,11 +29841,11 @@ var TutorialCommand = class extends SmCommand {
29153
29841
  const targets = listScaffoldTargets();
29154
29842
  const target = await this.resolveScaffoldTarget(targets, stderrAnsi, errGlyph);
29155
29843
  if (target === null) return ExitCode.Error;
29156
- const targetDir = join21(ctx.cwd, target.skillDir, spec.slug);
29157
- const targetDisplay = `${target.skillDir}/${spec.slug}/`;
29844
+ const targetDir = join21(ctx.cwd, target.skillDir, SKILL_SLUG);
29845
+ const targetDisplay = `${target.skillDir}/${SKILL_SLUG}/`;
29158
29846
  let sourceDir;
29159
29847
  try {
29160
- sourceDir = resolveSkillSourceDir(variant);
29848
+ sourceDir = resolveSkillSourceDir();
29161
29849
  } catch {
29162
29850
  this.printer.error(
29163
29851
  tx(TUTORIAL_TEXTS.sourceMissing, {
@@ -29192,38 +29880,18 @@ var TutorialCommand = class extends SmCommand {
29192
29880
  this.printer.data(
29193
29881
  tx(TUTORIAL_TEXTS.written, {
29194
29882
  glyph: ansi.green("\u2713"),
29195
- slug: spec.slug,
29883
+ slug: SKILL_SLUG,
29196
29884
  target: targetDisplay,
29197
29885
  provider: ansi.dim(target.label),
29198
29886
  cwd: ansi.dim(displayCwd(ctx.cwd)),
29199
29887
  enLabel: ansi.dim(TUTORIAL_TEXTS.writtenLabelEn),
29200
29888
  esLabel: ansi.dim(TUTORIAL_TEXTS.writtenLabelEs),
29201
- enTrigger: spec.triggerEn,
29202
- esTrigger: spec.triggerEs
29889
+ enTrigger: TRIGGER_EN,
29890
+ esTrigger: TRIGGER_ES
29203
29891
  })
29204
29892
  );
29205
29893
  return ExitCode.Ok;
29206
29894
  }
29207
- /**
29208
- * Validate the positional `variant` arg against the closed catalog.
29209
- * Returns the resolved variant, or `null` after printing the
29210
- * `invalidVariant` error (caller exits non-zero). Extracted from
29211
- * `run()` to keep its cyclomatic complexity within the lint budget.
29212
- */
29213
- resolveVariantArg(errGlyph, stderrAnsi) {
29214
- const rawVariant = this.variant;
29215
- if (rawVariant !== void 0 && !isTutorialVariant(rawVariant)) {
29216
- this.printer.error(
29217
- tx(TUTORIAL_TEXTS.invalidVariant, {
29218
- glyph: errGlyph,
29219
- variant: rawVariant,
29220
- hint: stderrAnsi.dim(TUTORIAL_TEXTS.invalidVariantHint)
29221
- })
29222
- );
29223
- return null;
29224
- }
29225
- return rawVariant ?? DEFAULT_VARIANT;
29226
- }
29227
29895
  /**
29228
29896
  * Resolve the destination Provider. Precedence:
29229
29897
  * 1. `--for <id>` (validated against the scaffold-capable catalog).
@@ -29283,9 +29951,6 @@ var TutorialCommand = class extends SmCommand {
29283
29951
  return picked;
29284
29952
  }
29285
29953
  };
29286
- function isTutorialVariant(value) {
29287
- return VALID_VARIANTS.includes(value);
29288
- }
29289
29954
  function toScaffoldTarget(provider) {
29290
29955
  const scaffold = provider.scaffold;
29291
29956
  if (!scaffold || !scaffold.skillDir) return null;
@@ -29352,23 +30017,21 @@ function listCwdEntries(dir) {
29352
30017
  const more = entries.length > shown.length ? ", ..." : "";
29353
30018
  return shown.join(", ") + more;
29354
30019
  }
29355
- var cachedSourceDirs = /* @__PURE__ */ new Map();
29356
- function resolveSkillSourceDir(variant) {
29357
- const cached = cachedSourceDirs.get(variant);
29358
- if (cached !== void 0) return cached;
29359
- const spec = VARIANT_SPECS[variant];
30020
+ var cachedSourceDir;
30021
+ function resolveSkillSourceDir() {
30022
+ if (cachedSourceDir !== void 0) return cachedSourceDir;
29360
30023
  const here = dirname20(fileURLToPath7(import.meta.url));
29361
30024
  const candidates = [
29362
- // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/
29363
- resolve39(here, "../../..", spec.sourceDir),
29364
- // bundled: dist/cli.js → dist/cli/tutorial/<slug> (sibling)
29365
- resolve39(here, "cli/tutorial", spec.slug),
29366
- // bundled fallback: any-depth → cli/tutorial/<slug>
29367
- resolve39(here, "../cli/tutorial", spec.slug)
30025
+ // dev: src/cli/commands/ → repo-root .claude/skills/sm-tutorial/
30026
+ resolve39(here, "../../..", SKILL_SOURCE_DIR),
30027
+ // bundled: dist/cli.js → dist/cli/tutorial/sm-tutorial (sibling)
30028
+ resolve39(here, "cli/tutorial", SKILL_SLUG),
30029
+ // bundled fallback: any-depth → cli/tutorial/sm-tutorial
30030
+ resolve39(here, "../cli/tutorial", SKILL_SLUG)
29368
30031
  ];
29369
30032
  for (const candidate of candidates) {
29370
30033
  if (existsSync32(candidate) && statSync11(candidate).isDirectory()) {
29371
- cachedSourceDirs.set(variant, candidate);
30034
+ cachedSourceDir = candidate;
29372
30035
  return candidate;
29373
30036
  }
29374
30037
  }
@@ -29592,4 +30255,4 @@ function resolveBareDefault() {
29592
30255
  process.exit(ExitCode.Error);
29593
30256
  }
29594
30257
  //# sourceMappingURL=cli.js.map
29595
- //# debugId=91ef81c7-e785-5818-839a-1f18ea093b57
30258
+ //# debugId=82aaa5c6-6dae-53a6-8d4a-1a673357051c