@skill-map/cli 0.51.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 (53) 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 +1253 -564
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +335 -208
  18. package/dist/kernel/index.d.ts +320 -15
  19. package/dist/kernel/index.js +335 -208
  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-TXTY24G4.js +2204 -0
  33. package/dist/ui/chunk-UBQUCSQ4.js +1 -0
  34. package/dist/ui/chunk-WFLPMCK4.js +392 -0
  35. package/dist/ui/chunk-WHZVGOS3.js +5 -0
  36. package/dist/ui/chunk-YQFIXHKM.js +123 -0
  37. package/dist/ui/index.html +2 -2
  38. package/dist/ui/main-OYITFJ7B.js +4 -0
  39. package/dist/ui/{styles-RG7Y33BT.css → styles-Q4NCOJQY.css} +1 -1
  40. package/migrations/001_initial.sql +36 -0
  41. package/package.json +10 -8
  42. package/dist/cli/tutorial/sm-master/SKILL.md +0 -688
  43. package/dist/cli/tutorial/sm-master/references/fixture-templates.md +0 -212
  44. package/dist/ui/chunk-2GXE52AJ.js +0 -123
  45. package/dist/ui/chunk-AEA5GIA7.js +0 -1
  46. package/dist/ui/chunk-KHRNVLJW.js +0 -1
  47. package/dist/ui/chunk-OZTRR4M7.js +0 -2312
  48. package/dist/ui/chunk-Q5YJKCTP.js +0 -1066
  49. package/dist/ui/chunk-RCT3JSFL.js +0 -1
  50. package/dist/ui/chunk-VBTLX7GH.js +0 -1110
  51. package/dist/ui/chunk-VJ57LHDR.js +0 -4
  52. package/dist/ui/chunk-WMGW2UAL.js +0 -2
  53. package/dist/ui/main-N7D2YBEX.js +0 -4
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]="1a42a4ca-6765-5a66-abc9-6b57464bc9c6")}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.51.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,65 +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
- severity: "warn",
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
- severity: "warn",
1977
+ tooltip: tooltipFor(status)
1978
+ });
1979
+ ctx.emitContribution(node.path, staleBadge, {
1980
+ icon: "pi-clock",
1979
1981
  tooltip: tooltipFor(status)
1980
1982
  });
1981
1983
  }
1982
1984
  return issues;
1983
1985
  }
1984
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
+ }
1985
2011
  function tooltipFor(status) {
1986
2012
  switch (status) {
1987
2013
  case "stale-body":
@@ -2016,6 +2042,18 @@ var ISSUE_COUNTER_TEXTS = {
2016
2042
 
2017
2043
  // plugins/core/analyzers/issue-counter/index.ts
2018
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
+ };
2019
2057
  function countByTier(issues) {
2020
2058
  const errors = /* @__PURE__ */ new Map();
2021
2059
  const warns = /* @__PURE__ */ new Map();
@@ -2028,13 +2066,13 @@ function countByTier(issues) {
2028
2066
  }
2029
2067
  return { errors, warns };
2030
2068
  }
2031
- function emitTierChips(ctx, contributionId, severity, counts, singleTooltip, manyTooltip) {
2032
- for (const [nodePath, count] of counts) {
2033
- const capped = Math.min(count, 99);
2034
- 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, {
2035
2073
  value: capped,
2036
2074
  severity,
2037
- tooltip: count === 1 ? singleTooltip : tx(manyTooltip, { count })
2075
+ tooltip: count3 === 1 ? singleTooltip : tx(manyTooltip, { count: count3 })
2038
2076
  });
2039
2077
  }
2040
2078
  }
@@ -2045,33 +2083,14 @@ var issueCounterAnalyzer = {
2045
2083
  description: "Emits one aggregate severity chip per node (error + warn counts) from the live issue accumulator.",
2046
2084
  mode: "deterministic",
2047
2085
  phase: "aggregate",
2048
- ui: {
2049
- // Third in the footer-right cluster, after the drift chip
2050
- // (priority 10) and the stability badge (priority 20). The warn
2051
- // counter sits before the error counter so the operator reads
2052
- // "advisory → blocking" left-to-right.
2053
- warnCount: {
2054
- slot: "card.footer.right",
2055
- icon: "pi-exclamation-triangle",
2056
- emitWhenEmpty: false,
2057
- priority: 30
2058
- },
2059
- // Last in the cluster, the red chip pins to the right edge so the
2060
- // most severe signal anchors the row's reading position.
2061
- errorCount: {
2062
- slot: "card.footer.right",
2063
- icon: "pi-times-circle",
2064
- emitWhenEmpty: false,
2065
- priority: 40
2066
- }
2067
- },
2086
+ ui: { warnCount, errorCount },
2068
2087
  evaluate(ctx) {
2069
2088
  const accumulator = ctx.accumulatedIssues ?? [];
2070
2089
  if (accumulator.length === 0) return [];
2071
2090
  const { errors, warns } = countByTier(accumulator);
2072
2091
  emitTierChips(
2073
2092
  ctx,
2074
- "errorCount",
2093
+ errorCount,
2075
2094
  "danger",
2076
2095
  errors,
2077
2096
  ISSUE_COUNTER_TEXTS.errorTooltipSingle,
@@ -2079,7 +2098,7 @@ var issueCounterAnalyzer = {
2079
2098
  );
2080
2099
  emitTierChips(
2081
2100
  ctx,
2082
- "warnCount",
2101
+ warnCount,
2083
2102
  "warn",
2084
2103
  warns,
2085
2104
  ISSUE_COUNTER_TEXTS.warnTooltipSingle,
@@ -2247,28 +2266,27 @@ function resolveLinkTargetToPath(link, nameIndex) {
2247
2266
 
2248
2267
  // plugins/core/analyzers/link-counter/index.ts
2249
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
+ };
2250
2283
  var linkCounterAnalyzer = {
2251
2284
  id: ID15,
2252
2285
  pluginId: CORE_PLUGIN_ID,
2253
2286
  kind: "analyzer",
2254
2287
  description: "Counts incoming and outgoing links per node.",
2255
2288
  mode: "deterministic",
2256
- ui: {
2257
- linksIn: {
2258
- slot: "card.footer.left",
2259
- icon: "pi-download",
2260
- label: "incoming links",
2261
- emitWhenEmpty: false,
2262
- priority: 10
2263
- },
2264
- linksOut: {
2265
- slot: "card.footer.left",
2266
- icon: "pi-upload",
2267
- label: "outgoing links",
2268
- emitWhenEmpty: false,
2269
- priority: 20
2270
- }
2271
- },
2289
+ ui: { linksIn, linksOut },
2272
2290
  evaluate(ctx) {
2273
2291
  const nameIndex = buildNameIndex(ctx.nodes);
2274
2292
  const perTarget = /* @__PURE__ */ new Map();
@@ -2280,8 +2298,8 @@ var linkCounterAnalyzer = {
2280
2298
  bump(perSource, link.source, link.kind);
2281
2299
  }
2282
2300
  for (const node of ctx.nodes) {
2283
- emitChip(ctx, node.path, "linksIn", perTarget.get(node.path));
2284
- 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));
2285
2303
  }
2286
2304
  return [];
2287
2305
  }
@@ -2294,14 +2312,13 @@ function bump(map, key, kind) {
2294
2312
  }
2295
2313
  byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
2296
2314
  }
2297
- function emitChip(ctx, nodePath, contributionId, byKind) {
2315
+ function emitChip(ctx, nodePath, ref, direction, byKind) {
2298
2316
  if (!byKind) return;
2299
2317
  let total = 0;
2300
2318
  for (const n of byKind.values()) total += n;
2301
2319
  if (total === 0) return;
2302
2320
  const capped = Math.min(total, 99);
2303
- const direction = contributionId === "linksIn" ? "in" : "out";
2304
- ctx.emitContribution(nodePath, contributionId, {
2321
+ ctx.emitContribution(nodePath, ref, {
2305
2322
  value: capped,
2306
2323
  tooltip: formatBreakdown(byKind, direction)
2307
2324
  });
@@ -2581,42 +2598,58 @@ function normaliseId(raw) {
2581
2598
  return raw.normalize("NFD").replace(new RegExp("\\p{Mn}+", "gu"), "").toLowerCase().replace(/[-_\s]+/g, " ").replace(/ +/g, " ").trim();
2582
2599
  }
2583
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
+
2584
2615
  // plugins/core/analyzers/node-stability/index.ts
2585
2616
  var ID18 = "node-stability";
2586
2617
  var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
2587
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
+ };
2588
2637
  var nodeStabilityAnalyzer = {
2589
2638
  id: ID18,
2590
2639
  pluginId: CORE_PLUGIN_ID,
2591
2640
  kind: "analyzer",
2592
2641
  description: "Reports a node's stability stage (`experimental`, `deprecated`) on the card.",
2593
2642
  mode: "deterministic",
2594
- ui: {
2595
- // First in the footer-right cluster: stability is the node's
2596
- // declared lifecycle state, so it leads, followed by the drift
2597
- // chip and then the severity counters. It's a state badge, not a
2598
- // count, so it stays left of the numeric zone.
2599
- experimental: {
2600
- slot: "card.footer.right",
2601
- icon: "fa-solid fa-flask",
2602
- label: "experimental",
2603
- emitWhenEmpty: false,
2604
- priority: 10
2605
- },
2606
- deprecated: {
2607
- slot: "card.footer.right",
2608
- icon: "pi-ban",
2609
- label: "deprecated",
2610
- emitWhenEmpty: false,
2611
- priority: 10
2612
- }
2613
- },
2643
+ ui: { experimental, deprecated, setStabilityButton },
2614
2644
  evaluate(ctx) {
2615
2645
  const issues = [];
2616
2646
  for (const node of ctx.nodes) {
2617
2647
  const stability = readStability(node);
2648
+ if (node.sidecar?.present === true) {
2649
+ emitSetStabilityButton(ctx, node.path, stability ?? "stable");
2650
+ }
2618
2651
  if (stability === "experimental") {
2619
- ctx.emitContribution(node.path, "experimental", {
2652
+ ctx.emitContribution(node.path, experimental, {
2620
2653
  value: 0,
2621
2654
  tooltip: EXPERIMENTAL_TOOLTIP
2622
2655
  });
@@ -2628,7 +2661,7 @@ var nodeStabilityAnalyzer = {
2628
2661
  data: { stability }
2629
2662
  });
2630
2663
  } else if (stability === "deprecated") {
2631
- ctx.emitContribution(node.path, "deprecated", {
2664
+ ctx.emitContribution(node.path, deprecated, {
2632
2665
  value: 0,
2633
2666
  tooltip: DEPRECATED_TOOLTIP,
2634
2667
  severity: "warn"
@@ -2660,6 +2693,25 @@ function readLegacyMetadataStability(fm) {
2660
2693
  function isStability(value) {
2661
2694
  return value === "experimental" || value === "deprecated" || value === "stable";
2662
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
+ }
2663
2715
 
2664
2716
  // plugins/core/analyzers/node-superseded/text.ts
2665
2717
  var NODE_SUPERSEDED_TEXTS = {
@@ -3022,8 +3074,8 @@ var ALL_SLOT_NAMES = [
3022
3074
  "card.footer.left",
3023
3075
  "card.footer.right",
3024
3076
  "graph.node.alert",
3025
- "inspector.header.badge.counter",
3026
- "inspector.header.badge.tag",
3077
+ "inspector.header.badge",
3078
+ "inspector.action.button",
3027
3079
  "inspector.body.panel.breakdown",
3028
3080
  "inspector.body.panel.records",
3029
3081
  "inspector.body.panel.tree",
@@ -3131,12 +3183,12 @@ function buildSchemaValidators() {
3131
3183
  const v = validators.get(name);
3132
3184
  if (!v) throw new Error(`Unknown schema: ${name}`);
3133
3185
  if (v(data)) return { ok: true, data };
3134
- const errors = (v.errors ?? []).map(formatError).join("; ");
3186
+ const errors = formatAjvErrors(v.errors);
3135
3187
  return { ok: false, errors };
3136
3188
  },
3137
3189
  validatePluginManifest(data) {
3138
3190
  if (pluginManifestValidator(data)) return { ok: true, data };
3139
- const errors = (pluginManifestValidator.errors ?? []).map(formatError).join("; ");
3191
+ const errors = formatAjvErrors(pluginManifestValidator.errors);
3140
3192
  return { ok: false, errors };
3141
3193
  },
3142
3194
  validateContributionPayload(slot, payload) {
@@ -3145,7 +3197,7 @@ function buildSchemaValidators() {
3145
3197
  return { ok: false, errors: "unknown-slot" };
3146
3198
  }
3147
3199
  if (validator(payload)) return { ok: true };
3148
- const errors = (validator.errors ?? []).map(formatError).join("; ");
3200
+ const errors = formatAjvErrors(validator.errors);
3149
3201
  return { ok: false, errors };
3150
3202
  }
3151
3203
  };
@@ -3177,7 +3229,7 @@ function buildProviderFrontmatterValidator(providers) {
3177
3229
  const v = compiled.get(key);
3178
3230
  if (!v) return { ok: false, errors: "no-schema" };
3179
3231
  if (v(data)) return { ok: true };
3180
- const errors = (v.errors ?? []).map(formatError).join("; ");
3232
+ const errors = formatAjvErrors(v.errors);
3181
3233
  return { ok: false, errors };
3182
3234
  }
3183
3235
  };
@@ -3186,6 +3238,47 @@ function formatError(err) {
3186
3238
  const path = err.instancePath || "(root)";
3187
3239
  return `${path} ${err.message ?? err.keyword}`;
3188
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
+ }
3189
3282
  function registerProviderAuxiliarySchemas(ajv, providers) {
3190
3283
  for (const provider of providers) {
3191
3284
  if (!provider.schemas) continue;
@@ -3498,6 +3591,122 @@ function makeIssue(signal) {
3498
3591
  return null;
3499
3592
  }
3500
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
+
3501
3710
  // plugins/core/analyzers/trigger-collision/text.ts
3502
3711
  var TRIGGER_COLLISION_TEXTS = {
3503
3712
  /**
@@ -3524,14 +3733,14 @@ var TRIGGER_COLLISION_TEXTS = {
3524
3733
  };
3525
3734
 
3526
3735
  // plugins/core/analyzers/trigger-collision/index.ts
3527
- var ID24 = "trigger-collision";
3736
+ var ID26 = "trigger-collision";
3528
3737
  var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
3529
3738
  "command",
3530
3739
  "skill",
3531
3740
  "agent"
3532
3741
  ]);
3533
3742
  var triggerCollisionAnalyzer = {
3534
- id: ID24,
3743
+ id: ID26,
3535
3744
  pluginId: CORE_PLUGIN_ID,
3536
3745
  kind: "analyzer",
3537
3746
  mode: "deterministic",
@@ -3629,7 +3838,7 @@ function analyzeTriggerBucket(normalized, claims) {
3629
3838
  part: parts[0]
3630
3839
  });
3631
3840
  return {
3632
- analyzerId: ID24,
3841
+ analyzerId: ID26,
3633
3842
  severity: "error",
3634
3843
  nodeIds,
3635
3844
  message,
@@ -3669,13 +3878,13 @@ var ASCII_FORMATTER_TEXTS = {
3669
3878
  };
3670
3879
 
3671
3880
  // plugins/core/formatters/ascii/index.ts
3672
- var ID25 = "ascii";
3881
+ var ID27 = "ascii";
3673
3882
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
3674
3883
  var asciiFormatter = {
3675
- id: ID25,
3884
+ id: ID27,
3676
3885
  pluginId: CORE_PLUGIN_ID,
3677
3886
  kind: "formatter",
3678
- formatId: ID25,
3887
+ formatId: ID27,
3679
3888
  description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
3680
3889
  // ASCII tree formatter, header + per-kind sections + per-issue
3681
3890
  // section. Each section iterates and renders; splitting per section
@@ -3769,13 +3978,13 @@ function renderSection(out, kind, group) {
3769
3978
  }
3770
3979
 
3771
3980
  // plugins/core/formatters/json/index.ts
3772
- var ID26 = "json";
3981
+ var ID28 = "json";
3773
3982
  var jsonFormatter = {
3774
- id: ID26,
3983
+ id: ID28,
3775
3984
  pluginId: CORE_PLUGIN_ID,
3776
3985
  kind: "formatter",
3777
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`.",
3778
- formatId: ID26,
3987
+ formatId: ID28,
3779
3988
  format(ctx) {
3780
3989
  if (ctx.scanResult !== void 0) {
3781
3990
  return JSON.stringify(ctx.scanResult);
@@ -3914,9 +4123,9 @@ function resolveSpecRoot2() {
3914
4123
  }
3915
4124
 
3916
4125
  // plugins/core/actions/node-bump/index.ts
3917
- var ID27 = "node-bump";
4126
+ var ID29 = "node-bump";
3918
4127
  var nodeBumpAction = {
3919
- id: ID27,
4128
+ id: ID29,
3920
4129
  pluginId: CORE_PLUGIN_ID,
3921
4130
  kind: "action",
3922
4131
  description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
@@ -3973,45 +4182,166 @@ function pickCurrentVersion(overlay) {
3973
4182
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
3974
4183
  }
3975
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
+
3976
4267
  // plugins/core/actions/node-supersede/index.ts
3977
- var ID28 = "node-supersede";
4268
+ var ID32 = "node-supersede";
3978
4269
  var nodeSupersedeAction = {
3979
- id: ID28,
4270
+ id: ID32,
3980
4271
  pluginId: CORE_PLUGIN_ID,
3981
4272
  kind: "action",
3982
4273
  description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
3983
4274
  mode: "deterministic",
3984
- invoke(_input, _ctx) {
3985
- const report = { ok: true, noop: true };
3986
- 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);
3987
4281
  }
3988
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
+ }
3989
4308
 
3990
4309
  // kernel/update-check/index.ts
3991
4310
  var SEMVER_SHAPE_RE = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
3992
4311
  async function fetchLatestVersion(pkg, opts) {
3993
4312
  const controller = new AbortController();
3994
- 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
+ });
3995
4321
  try {
3996
- const url = `https://registry.npmjs.org/${pkg}/latest`;
3997
- const response = await fetch(url, {
3998
- signal: controller.signal,
3999
- headers: { accept: "application/json" }
4000
- });
4001
- if (!response.ok) {
4002
- throw new Error(`registry returned status ${response.status}`);
4003
- }
4004
- const payload = await response.json();
4005
- if (typeof payload.version !== "string" || payload.version.length === 0) {
4006
- throw new Error("registry payload missing string `version`");
4007
- }
4008
- if (!SEMVER_SHAPE_RE.test(payload.version)) {
4009
- throw new Error("registry payload `version` is not a semver-shaped string");
4010
- }
4011
- return payload.version;
4322
+ return await Promise.race([fetchVersion(pkg, controller.signal), hardCap]);
4012
4323
  } finally {
4013
- 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}`);
4014
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");
4343
+ }
4344
+ return payload.version;
4015
4345
  }
4016
4346
  function compareVersions(a, b) {
4017
4347
  const pa = parseSemver(a);
@@ -4480,10 +4810,14 @@ var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", v
4480
4810
  var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: VERSION };
4481
4811
  var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: VERSION };
4482
4812
  var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: VERSION };
4813
+ var supersedeAnalyzer2 = { ...supersedeAnalyzer, pluginId: "core", version: VERSION };
4814
+ var tagsAnalyzer2 = { ...tagsAnalyzer, pluginId: "core", version: VERSION };
4483
4815
  var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: VERSION };
4484
4816
  var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: VERSION };
4485
4817
  var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: VERSION };
4486
4818
  var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core", version: VERSION };
4819
+ var nodeSetStabilityAction2 = { ...nodeSetStabilityAction, pluginId: "core", version: VERSION };
4820
+ var nodeSetTagsAction2 = { ...nodeSetTagsAction, pluginId: "core", version: VERSION };
4487
4821
  var nodeSupersedeAction2 = { ...nodeSupersedeAction, pluginId: "core", version: VERSION };
4488
4822
  var updateCheckHook2 = { ...updateCheckHook, pluginId: "core", version: VERSION };
4489
4823
  var builtInPlugins = [
@@ -4543,10 +4877,14 @@ var builtInPlugins = [
4543
4877
  referenceRedundantAnalyzer2,
4544
4878
  schemaViolationAnalyzer2,
4545
4879
  signalCollisionAnalyzer2,
4880
+ supersedeAnalyzer2,
4881
+ tagsAnalyzer2,
4546
4882
  triggerCollisionAnalyzer2,
4547
4883
  asciiFormatter2,
4548
4884
  jsonFormatter2,
4549
4885
  nodeBumpAction2,
4886
+ nodeSetStabilityAction2,
4887
+ nodeSetTagsAction2,
4550
4888
  nodeSupersedeAction2,
4551
4889
  updateCheckHook2
4552
4890
  ]
@@ -5803,13 +6141,16 @@ function ensureSidecarWritesAllowed(opts) {
5803
6141
  default: false
5804
6142
  });
5805
6143
  if (allowed === true) return;
5806
- if (opts.confirm === true) {
6144
+ if (opts.always === true) {
5807
6145
  writeConfigValue("allowEditSmFiles", true, {
5808
6146
  target: "project-local",
5809
6147
  cwd: opts.cwd
5810
6148
  });
5811
6149
  return;
5812
6150
  }
6151
+ if (opts.confirm === true) {
6152
+ return;
6153
+ }
5813
6154
  throw new EConsentRequiredError({
5814
6155
  key: "allowEditSmFiles",
5815
6156
  hintTarget: "project-local"
@@ -6069,6 +6410,17 @@ function ensureGitForStaged(cwd) {
6069
6410
  }
6070
6411
  return "ok";
6071
6412
  }
6413
+ function resolveGitAuthorName(cwd) {
6414
+ if (!isInsideGitRepo(cwd)) return null;
6415
+ const result = spawnSync("git", ["config", "user.name"], {
6416
+ cwd,
6417
+ stdio: ["ignore", "pipe", "pipe"],
6418
+ encoding: "utf8"
6419
+ });
6420
+ if (result.error !== void 0 || result.status !== 0) return null;
6421
+ const name = (result.stdout ?? "").trim();
6422
+ return name.length > 0 ? name : null;
6423
+ }
6072
6424
  function stageSidecar(cwd, sidecarAbsPath) {
6073
6425
  const result = spawnSync("git", ["add", "--", sidecarAbsPath], {
6074
6426
  cwd,
@@ -7423,7 +7775,7 @@ async function loadPluginOverrideMap(db) {
7423
7775
  }
7424
7776
 
7425
7777
  // kernel/util/enum-parsers.ts
7426
- var STABILITY_VALUES = Object.freeze([
7778
+ var STABILITY_VALUES2 = Object.freeze([
7427
7779
  "experimental",
7428
7780
  "stable",
7429
7781
  "deprecated"
@@ -7746,6 +8098,41 @@ async function replaceAllScanContributions(trx, contributions, livePaths = /* @_
7746
8098
  await sweepPerTupleContributions(trx, contributions, freshlyRunTuples);
7747
8099
  await upsertContributionsBuffer(trx, contributions);
7748
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
+ }
7749
8136
  async function sweepOrphanContributions(trx, livePaths) {
7750
8137
  if (livePaths.size > 0) {
7751
8138
  await trx.deleteFrom("scan_contributions").where("nodePath", "not in", [...livePaths]).execute();
@@ -7955,7 +8342,7 @@ async function findNodesByTag(db, tag) {
7955
8342
  }
7956
8343
 
7957
8344
  // kernel/adapters/sqlite/scan-persistence.ts
7958
- 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 = []) {
7959
8346
  const scannedAt = validateScannedAt(result.scannedAt);
7960
8347
  const renames = [];
7961
8348
  await db.transaction().execute(async (trx) => {
@@ -7970,6 +8357,7 @@ async function persistScanResult(db, result, renameOps = [], extractorRuns = [],
7970
8357
  registeredContributionKeys,
7971
8358
  freshlyRunTuples
7972
8359
  );
8360
+ await replaceAllScanContributionErrors(trx, contributionErrors);
7973
8361
  const tagRecords = nodesToTagRecords(result.nodes);
7974
8362
  await replaceAllScanTags(trx, tagRecords, livePathsForContrib);
7975
8363
  await upsertEnrichmentLayer(trx, result, renameOps, enrichments);
@@ -8414,7 +8802,8 @@ var SqliteStorageAdapter = class {
8414
8802
  listForNode: (nodePath) => loadContributionsForNode(this.db, nodePath),
8415
8803
  listForPaths: (paths) => loadContributionsForPaths(this.db, paths),
8416
8804
  lookup: (pluginId, contributionId, nodePath, extensionId) => loadContributionLookup(this.db, pluginId, contributionId, nodePath, extensionId),
8417
- purgeByPlugin: (pluginId, extensionId) => purgeContributionsByPlugin(this.db, pluginId, extensionId)
8805
+ purgeByPlugin: (pluginId, extensionId) => purgeContributionsByPlugin(this.db, pluginId, extensionId),
8806
+ listAllErrors: () => listAllContributionErrors(this.db)
8418
8807
  };
8419
8808
  this.tags = {
8420
8809
  listForNode: (nodePath) => loadTagsForNode(this.db, nodePath),
@@ -8487,7 +8876,8 @@ async function persistScansThroughNonTx(db, result, opts) {
8487
8876
  defaults.enrichments,
8488
8877
  defaults.contributions,
8489
8878
  defaults.registeredContributionKeys,
8490
- defaults.freshlyRunTuples
8879
+ defaults.freshlyRunTuples,
8880
+ defaults.contributionErrors
8491
8881
  );
8492
8882
  }
8493
8883
  function applyPersistDefaults(opts) {
@@ -8498,6 +8888,7 @@ function applyPersistDefaults(opts) {
8498
8888
  contributions: [],
8499
8889
  registeredContributionKeys: /* @__PURE__ */ new Set(),
8500
8890
  freshlyRunTuples: /* @__PURE__ */ new Set(),
8891
+ contributionErrors: [],
8501
8892
  ...opts
8502
8893
  };
8503
8894
  }
@@ -8662,7 +9053,8 @@ function buildTxSubset(trx) {
8662
9053
  d.enrichments,
8663
9054
  d.contributions,
8664
9055
  d.registeredContributionKeys,
8665
- d.freshlyRunTuples
9056
+ d.freshlyRunTuples,
9057
+ d.contributionErrors
8666
9058
  ).then(() => void 0);
8667
9059
  }
8668
9060
  },
@@ -8990,13 +9382,14 @@ function isAllowedLstatError(err) {
8990
9382
 
8991
9383
  // cli/commands/bump-plan.ts
8992
9384
  function computeBumpPlan(nodes, options) {
9385
+ const invoker = resolveGitAuthorName(options.cwd) ?? "cli";
8993
9386
  const items = [];
8994
9387
  for (const node of nodes) {
8995
- items.push(planOne(node, options));
9388
+ items.push(planOne(node, options, invoker));
8996
9389
  }
8997
9390
  return { items };
8998
9391
  }
8999
- function planOne(node, options) {
9392
+ function planOne(node, options, invoker) {
9000
9393
  let absPath;
9001
9394
  try {
9002
9395
  assertContained(options.cwd, node.path);
@@ -9010,7 +9403,7 @@ function planOne(node, options) {
9010
9403
  }
9011
9404
  let result;
9012
9405
  try {
9013
- result = invokeBumpFor(node, absPath, options.force);
9406
+ result = invokeBumpFor(node, absPath, options.force, invoker);
9014
9407
  } catch (err) {
9015
9408
  return {
9016
9409
  nodePath: node.path,
@@ -9032,7 +9425,7 @@ function planOne(node, options) {
9032
9425
  report: result.report
9033
9426
  };
9034
9427
  }
9035
- function invokeBumpFor(node, absPath, force) {
9428
+ function invokeBumpFor(node, absPath, force, invoker) {
9036
9429
  if (!nodeBumpAction.invoke) {
9037
9430
  throw new Error("built-in bump action is missing its invoke()");
9038
9431
  }
@@ -9041,7 +9434,7 @@ function invokeBumpFor(node, absPath, force) {
9041
9434
  return nodeBumpAction.invoke(input, {
9042
9435
  node,
9043
9436
  nodeAbsolutePath: absPath,
9044
- invoker: "cli",
9437
+ invoker,
9045
9438
  now: () => /* @__PURE__ */ new Date(),
9046
9439
  settings: {}
9047
9440
  });
@@ -9156,10 +9549,11 @@ var BumpCommand = class extends SmCommand {
9156
9549
  * `EConsentRequiredError` thrown by `FilesystemSidecarStore.applyPatch`
9157
9550
  * (via `ensureSidecarWritesAllowed`), prompt the operator if stdin is
9158
9551
  * a TTY and `--yes` was not passed. On accept, flip `this.yes` to
9159
- * true and re-run `dispatch` (the second pass passes `confirm: true`
9160
- * to the store and the gate persists the flag to project-local).
9161
- * On decline or non-TTY without `--yes`, print a directed message
9162
- * 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`.
9163
9557
  */
9164
9558
  async #runWithConsent(ansi, dispatch) {
9165
9559
  try {
@@ -9258,7 +9652,11 @@ var BumpCommand = class extends SmCommand {
9258
9652
  async #applyBumpedSingle(item, node, ansi) {
9259
9653
  const ctx = defaultRuntimeContext();
9260
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`.
9261
9658
  confirm: this.yes,
9659
+ always: this.yes,
9262
9660
  cwd: ctx.cwd
9263
9661
  };
9264
9662
  const applied = await applyBumpWrites(item, consent);
@@ -9348,7 +9746,9 @@ var BumpCommand = class extends SmCommand {
9348
9746
  const store = new FilesystemSidecarStore(ensureSidecarWritesAllowed);
9349
9747
  const ctx = defaultRuntimeContext();
9350
9748
  const consent = {
9749
+ // Step 17 split: CLI accept / `--yes` persists (see #runSingle).
9351
9750
  confirm: this.yes,
9751
+ always: this.yes,
9352
9752
  cwd: ctx.cwd
9353
9753
  };
9354
9754
  const outcomes = [];
@@ -9577,16 +9977,23 @@ function readConformanceKillSwitches(env = process.env) {
9577
9977
  }
9578
9978
 
9579
9979
  // kernel/i18n/plugin-loader.texts.ts
9980
+ var SPEC_GITHUB_BASE = "https://github.com/crystian/skill-map/blob/main";
9580
9981
  var PLUGIN_LOADER_TEXTS = {
9581
9982
  invalidManifestJsonParse: "{{manifestPath}}: {{errDescription}}. Validate the JSON (e.g. `npx jsonlint plugin.json`).",
9582
- 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.`,
9583
9984
  invalidSpecCompat: 'specCompat "{{specCompat}}" is not a valid semver range. Use a range like "^1.0.0".',
9584
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.`,
9585
9986
  loadErrorFileNotFound: "extension file not found: {{relEntry}} (resolved to {{abs}}). Check plugin.json#/extensions paths.",
9586
9987
  loadErrorImportFailed: "{{relEntry}}: import failed: {{errDescription}}",
9587
9988
  loadErrorMissingKind: "{{relEntry}}: default export missing a string `kind` field. Expected one of: {{knownKindsList}}.",
9588
9989
  loadErrorUnknownKind: '{{relEntry}}: unknown extension kind "{{kindReceived}}". Expected one of: {{knownKindsList}}.',
9589
- 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}}.",
9590
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.).",
9591
9998
  disabledByConfig: "disabled by config_plugins or settings.json",
9592
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.",
@@ -9918,7 +10325,7 @@ function loadOneProviderKind(opts) {
9918
10325
  opts.pluginPath,
9919
10326
  opts.pluginId,
9920
10327
  "invalid-manifest",
9921
- `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.`
9922
10329
  ),
9923
10330
  manifest: opts.manifest
9924
10331
  }
@@ -10340,13 +10747,17 @@ var PluginLoader = class {
10340
10747
  }
10341
10748
  const extValidator = this.#options.validators.validatorForExtension(kind);
10342
10749
  if (!extValidator(manifestView)) {
10343
- 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`;
10344
10755
  return { ok: false, failure: {
10345
10756
  ...fail(
10346
10757
  pluginPath,
10347
10758
  pluginId,
10348
10759
  "invalid-manifest",
10349
- tx(PLUGIN_LOADER_TEXTS.invalidManifestExtensionShape, { relEntry, kind, errors })
10760
+ tx(PLUGIN_LOADER_TEXTS.invalidManifestExtensionShape, { relEntry, errors, docUrl })
10350
10761
  ),
10351
10762
  manifest
10352
10763
  } };
@@ -10824,6 +11235,9 @@ function collectViewContributions(pluginId, extensionId, instance, out, options
10824
11235
  const raw = instance["ui"];
10825
11236
  if (typeof raw !== "object" || raw === null) return;
10826
11237
  const exclude = options.excludeQualifiedIds;
11238
+ const pluginOrder = options.pluginOrder;
11239
+ const extOrder = instance["order"];
11240
+ const extensionOrder = typeof extOrder === "number" ? extOrder : void 0;
10827
11241
  for (const [contributionId, value] of Object.entries(raw)) {
10828
11242
  if (typeof value !== "object" || value === null) continue;
10829
11243
  const entry = value;
@@ -10842,13 +11256,15 @@ function collectViewContributions(pluginId, extensionId, instance, out, options
10842
11256
  ...typeof entry.icon === "string" ? { icon: entry.icon } : {},
10843
11257
  ...typeof entry.emptyText === "string" ? { emptyText: entry.emptyText } : {},
10844
11258
  ...typeof entry.priority === "number" ? { priority: entry.priority } : {},
11259
+ ...typeof pluginOrder === "number" ? { pluginOrder } : {},
11260
+ ...typeof extensionOrder === "number" ? { extensionOrder } : {},
10845
11261
  emitWhenEmpty: entry.emitWhenEmpty === true
10846
11262
  });
10847
11263
  }
10848
11264
  }
10849
11265
 
10850
11266
  // core/runtime/plugin-runtime/bucketing.ts
10851
- function bucketLoaded(loaded, runtime) {
11267
+ function bucketLoaded(loaded, runtime, pluginOrder) {
10852
11268
  for (const ext of loaded) {
10853
11269
  const instance = ext.instance;
10854
11270
  if (!isExtensionInstance(instance)) continue;
@@ -10869,7 +11285,9 @@ function bucketLoaded(loaded, runtime) {
10869
11285
  ...ext.entryPath ? { entry: ext.entryPath } : {}
10870
11286
  });
10871
11287
  collectAnnotationContributions(ext.pluginId, instance, runtime.annotationContributions);
10872
- collectViewContributions(ext.pluginId, ext.id, instance, runtime.viewContributions);
11288
+ collectViewContributions(ext.pluginId, ext.id, instance, runtime.viewContributions, {
11289
+ ...typeof pluginOrder === "number" ? { pluginOrder } : {}
11290
+ });
10873
11291
  }
10874
11292
  }
10875
11293
  function collectAnnotationContributions(pluginId, instance, out) {
@@ -10915,11 +11333,14 @@ function truncateTail(s, max) {
10915
11333
  // core/runtime/i18n/plugin-runtime.texts.ts
10916
11334
  var PLUGIN_RUNTIME_TEXTS = {
10917
11335
  /**
10918
- * Stderr-ready warning for one non-loaded plugin. Format keeps the
10919
- * status word and the reason scannable so a user can grep
10920
- * `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").
10921
11342
  */
10922
- warningRow: "plugin {{id}}: {{status}}, {{reason}}",
11343
+ warningRow: "plugin {{id}} ({{status}}), all extensions skipped: {{reason}}",
10923
11344
  /** Placeholder when a non-loaded plugin record carries no `reason`. */
10924
11345
  warningReasonMissing: "(no reason recorded)"
10925
11346
  };
@@ -10981,7 +11402,7 @@ async function loadPluginRuntime(opts = {}) {
10981
11402
  };
10982
11403
  for (const plugin of discovered) {
10983
11404
  if (plugin.status === "enabled") {
10984
- bucketLoaded(plugin.extensions ?? [], runtime);
11405
+ bucketLoaded(plugin.extensions ?? [], runtime, plugin.manifest?.order);
10985
11406
  continue;
10986
11407
  }
10987
11408
  if (plugin.status === "disabled") continue;
@@ -13319,8 +13740,8 @@ var DbResetCommand = class extends SmCommand {
13319
13740
  return ExitCode.Ok;
13320
13741
  }
13321
13742
  const withCounts = rows.map((r) => {
13322
- const count = db.prepare(`SELECT COUNT(*) AS c FROM "${r.name}"`).get();
13323
- 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) };
13324
13745
  });
13325
13746
  const totalRows = withCounts.reduce((acc, r) => acc + r.rowCount, 0);
13326
13747
  const lines = withCounts.map((r) => ` - ${r.name}: ${r.rowCount} row(s)`).join("\n");
@@ -15137,7 +15558,7 @@ var ORCHESTRATOR_TEXTS = {
15137
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.",
15138
15559
  extensionErrorLinkKindNotDeclared: 'Extractor "{{extractorId}}" emitted a link of kind "{{linkKind}}" outside its declared `emitsLinkKinds` set [{{declaredKinds}}]. Link dropped.',
15139
15560
  extensionErrorIssueInvalidSeverity: `Rule "{{analyzerId}}" emitted an issue with invalid severity {{severity}} (allowed: 'error' | 'warn' | 'info'). Issue dropped.`,
15140
- 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.',
15141
15562
  extensionErrorContributionPayloadInvalid: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}}; payload failed the "{{slot}}" schema: {{errors}}. Contribution dropped.',
15142
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.',
15143
15564
  runScanRootEmptyArray: "runScan: roots must contain at least one path (spec requires minItems: 1)",
@@ -15157,6 +15578,7 @@ async function runExtractorsForNode(opts) {
15157
15578
  const externalLinks = [];
15158
15579
  const enrichmentBuffer = /* @__PURE__ */ new Map();
15159
15580
  const contributions = [];
15581
+ const contributionErrors = [];
15160
15582
  const signals = [];
15161
15583
  const virtualNodes = [];
15162
15584
  const virtualNodePaths = /* @__PURE__ */ new Set();
@@ -15188,36 +15610,54 @@ async function runExtractorsForNode(opts) {
15188
15610
  });
15189
15611
  }
15190
15612
  };
15191
- const declaredContributions = readDeclaredContributions(extractor);
15192
- const emitContribution = (contributionId, payload) => {
15193
- 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;
15194
15616
  if (!declared) {
15617
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUndeclaredRef, {
15618
+ extractorId: qualifiedId2,
15619
+ nodePath: opts.node.path
15620
+ });
15195
15621
  emitExtensionError(opts.emitter, qualifiedId2, opts.node.path, {
15196
15622
  phase: "emitContribution",
15197
- contributionId,
15198
- reason: "unknown-contribution-id",
15199
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
15200
- extractorId: qualifiedId2,
15201
- contributionId,
15202
- nodePath: opts.node.path
15203
- })
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()
15204
15633
  });
15205
15634
  return;
15206
15635
  }
15207
15636
  const result = validators.validateContributionPayload(declared.slot, payload);
15208
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
+ });
15209
15645
  emitExtensionError(opts.emitter, qualifiedId2, opts.node.path, {
15210
15646
  phase: "emitContribution",
15211
- contributionId,
15647
+ contributionId: declared.id,
15212
15648
  slot: declared.slot,
15213
15649
  reason: result.errors,
15214
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
15215
- extractorId: qualifiedId2,
15216
- contributionId,
15217
- nodePath: opts.node.path,
15218
- slot: declared.slot,
15219
- errors: result.errors
15220
- })
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()
15221
15661
  });
15222
15662
  return;
15223
15663
  }
@@ -15225,7 +15665,7 @@ async function runExtractorsForNode(opts) {
15225
15665
  pluginId: extractor.pluginId,
15226
15666
  extensionId: extractor.id,
15227
15667
  nodePath: opts.node.path,
15228
- contributionId,
15668
+ contributionId: declared.id,
15229
15669
  slot: declared.slot,
15230
15670
  payload,
15231
15671
  emittedAt: Date.now()
@@ -15263,11 +15703,12 @@ async function runExtractorsForNode(opts) {
15263
15703
  externalLinks,
15264
15704
  enrichments: Array.from(enrichmentBuffer.values()),
15265
15705
  contributions,
15706
+ contributionErrors,
15266
15707
  signals,
15267
15708
  virtualNodes
15268
15709
  };
15269
15710
  }
15270
- function readDeclaredContributions(extension) {
15711
+ function readDeclaredContributionRefs(extension) {
15271
15712
  const out = /* @__PURE__ */ new Map();
15272
15713
  const raw = extension.ui;
15273
15714
  if (typeof raw !== "object" || raw === null) return out;
@@ -15275,7 +15716,7 @@ function readDeclaredContributions(extension) {
15275
15716
  if (typeof value !== "object" || value === null) continue;
15276
15717
  const slot = value.slot;
15277
15718
  if (typeof slot !== "string") continue;
15278
- out.set(id, { slot });
15719
+ out.set(value, { id, slot });
15279
15720
  }
15280
15721
  return out;
15281
15722
  }
@@ -15523,6 +15964,7 @@ function isExternalUrlLink(link) {
15523
15964
  async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, signals, seedIssues = []) {
15524
15965
  const issues = [...seedIssues];
15525
15966
  const contributions = [];
15967
+ const contributionErrors = [];
15526
15968
  const validators = loadSchemaValidators();
15527
15969
  void registeredActionIds;
15528
15970
  const analyzerOrphans = orphanSidecars.map((o) => ({
@@ -15532,36 +15974,54 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
15532
15974
  const scheduled = orderAnalyzersByPhase(analyzers);
15533
15975
  for (const analyzer of scheduled) {
15534
15976
  const qualifiedId2 = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
15535
- const declaredContributions = readDeclaredContributions(analyzer);
15536
- const emitContribution = (nodePath, contributionId, payload) => {
15537
- 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;
15538
15980
  if (!declared) {
15981
+ const message = tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUndeclaredRef, {
15982
+ extractorId: qualifiedId2,
15983
+ nodePath
15984
+ });
15539
15985
  emitExtensionError(emitter, qualifiedId2, nodePath, {
15540
15986
  phase: "emitContribution",
15541
- contributionId,
15542
- reason: "unknown-contribution-id",
15543
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
15544
- extractorId: qualifiedId2,
15545
- contributionId,
15546
- nodePath
15547
- })
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()
15548
15997
  });
15549
15998
  return;
15550
15999
  }
15551
16000
  const result = validators.validateContributionPayload(declared.slot, payload);
15552
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
+ });
15553
16009
  emitExtensionError(emitter, qualifiedId2, nodePath, {
15554
16010
  phase: "emitContribution",
15555
- contributionId,
16011
+ contributionId: declared.id,
15556
16012
  slot: declared.slot,
15557
16013
  reason: result.errors,
15558
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
15559
- extractorId: qualifiedId2,
15560
- contributionId,
15561
- nodePath,
15562
- slot: declared.slot,
15563
- errors: result.errors
15564
- })
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()
15565
16025
  });
15566
16026
  return;
15567
16027
  }
@@ -15569,7 +16029,7 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
15569
16029
  pluginId: analyzer.pluginId,
15570
16030
  extensionId: analyzer.id,
15571
16031
  nodePath,
15572
- contributionId,
16032
+ contributionId: declared.id,
15573
16033
  slot: declared.slot,
15574
16034
  payload,
15575
16035
  emittedAt: Date.now()
@@ -15602,7 +16062,7 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
15602
16062
  emitter.emit(evt);
15603
16063
  await hookDispatcher.dispatch("analyzer.completed", evt);
15604
16064
  }
15605
- return { issues, contributions };
16065
+ return { issues, contributions, contributionErrors };
15606
16066
  }
15607
16067
  function orderAnalyzersByPhase(analyzers) {
15608
16068
  return analyzers.slice().sort((a, b) => phaseRank(a) - phaseRank(b));
@@ -16340,7 +16800,7 @@ function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyH
16340
16800
  for (const parseIssue of result.issues) {
16341
16801
  issues.push({
16342
16802
  analyzerId: "invalid-sidecar",
16343
- severity: "warn",
16803
+ severity: "error",
16344
16804
  nodeIds: [nodePathForIssue],
16345
16805
  message: parseIssue.message,
16346
16806
  data: { sidecarPath: relativePathFromRoots(mdAbs, roots) }
@@ -16490,6 +16950,7 @@ async function walkAndExtract(opts) {
16490
16950
  enrichments: [...accum.enrichmentBuffer.values()],
16491
16951
  extractorRuns: accum.extractorRuns,
16492
16952
  contributions: accum.contributionsBuffer,
16953
+ contributionErrors: accum.contributionErrorsBuffer,
16493
16954
  freshlyRunTuples: accum.freshlyRunTuples,
16494
16955
  orphanSidecars,
16495
16956
  sidecarRoots: accum.sidecarRoots,
@@ -16506,6 +16967,7 @@ function createWalkAccumulators() {
16506
16967
  frontmatterIssues: [],
16507
16968
  enrichmentBuffer: /* @__PURE__ */ new Map(),
16508
16969
  contributionsBuffer: [],
16970
+ contributionErrorsBuffer: [],
16509
16971
  freshlyRunTuples: /* @__PURE__ */ new Set(),
16510
16972
  extractorRuns: [],
16511
16973
  sidecarRoots: /* @__PURE__ */ new Map()
@@ -16656,7 +17118,11 @@ function mergeExtractResult(extractResult, accum) {
16656
17118
  accum.enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
16657
17119
  }
16658
17120
  for (const c of extractResult.contributions) accum.contributionsBuffer.push(c);
16659
- 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) {
16660
17126
  if (accum.nodes.some((n) => n.path === vn.path)) continue;
16661
17127
  accum.nodes.push(vn);
16662
17128
  }
@@ -16932,6 +17398,7 @@ async function dispatchExtractorCompleted(extractors, emitter, hookDispatcher) {
16932
17398
  }
16933
17399
  function mergeAnalyzerEmissions(walked, analyzerResult, analyzers) {
16934
17400
  for (const c of analyzerResult.contributions) walked.contributions.push(c);
17401
+ for (const e of analyzerResult.contributionErrors) walked.contributionErrors.push(e);
16935
17402
  for (const analyzer of analyzers ?? []) {
16936
17403
  if (analyzer.ui === void 0) continue;
16937
17404
  for (const node of walked.nodes) {
@@ -16977,6 +17444,7 @@ function buildScanReturn(walked, issues, renameOps, stats, options, setup) {
16977
17444
  extractorRuns: walked.extractorRuns,
16978
17445
  enrichments: walked.enrichments,
16979
17446
  contributions: walked.contributions,
17447
+ contributionErrors: walked.contributionErrors,
16980
17448
  freshlyRunTuples: walked.freshlyRunTuples
16981
17449
  };
16982
17450
  }
@@ -18006,6 +18474,7 @@ async function runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanW
18006
18474
  extractorRuns: scanned.extractorRuns,
18007
18475
  enrichments: scanned.enrichments,
18008
18476
  contributions: scanned.contributions,
18477
+ contributionErrors: scanned.contributionErrors,
18009
18478
  registeredContributionKeys: collectRegisteredContributionKeys(extensions),
18010
18479
  freshlyRunTuples: scanned.freshlyRunTuples
18011
18480
  });
@@ -18862,11 +19331,11 @@ function renderStatsFailures(failures, ansi) {
18862
19331
  tx(HISTORY_TEXTS.statsSectionHeader, { title: HISTORY_TEXTS.statsSectionTitleFailures })
18863
19332
  ];
18864
19333
  const reasonWidth = Math.max(...failures.map(([reason]) => reason.length));
18865
- for (const [reason, count] of failures) {
19334
+ for (const [reason, count3] of failures) {
18866
19335
  lines.push(
18867
19336
  tx(HISTORY_TEXTS.statsFailuresRow, {
18868
19337
  reason: sanitizeForTerminal(reason).padEnd(reasonWidth),
18869
- count: ansi.red(String(count))
19338
+ count: ansi.red(String(count3))
18870
19339
  })
18871
19340
  );
18872
19341
  }
@@ -19568,14 +20037,14 @@ var OrphansReconcileCommand = class extends SmCommand {
19568
20037
  tx(ORPHANS_TEXTS.reconcileSuccessBody, { breakdown: ansi.dim(breakdown) })
19569
20038
  );
19570
20039
  if (summary.collisions.length > 0) {
19571
- const count = summary.collisions.length;
20040
+ const count3 = summary.collisions.length;
19572
20041
  this.printer.info(
19573
20042
  tx(
19574
20043
  dryRun ? ORPHANS_TEXTS.reconcileCollisionsNoteDryRun : ORPHANS_TEXTS.reconcileCollisionsNote,
19575
20044
  {
19576
20045
  glyph: ansi.yellow("\u26A0"),
19577
- count,
19578
- plural: count === 1 ? "" : "s"
20046
+ count: count3,
20047
+ plural: count3 === 1 ? "" : "s"
19579
20048
  }
19580
20049
  )
19581
20050
  );
@@ -19892,6 +20361,23 @@ var PLUGINS_TEXTS = {
19892
20361
  doctorIssuesHeader: "\n Issues ({{count}})\n",
19893
20362
  doctorIssueEntry: " {{glyph}} {{id}} {{status}}\n",
19894
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",
19895
20381
  // --- enable / disable -----------------------------------------------
19896
20382
  /**
19897
20383
  * §3.1b two-line block. Mutex between explicit ids and `--all`; the
@@ -20395,7 +20881,7 @@ function sortExtensionsCanonical(exts) {
20395
20881
  }
20396
20882
  function renderBuiltInDetail(b, ansi) {
20397
20883
  const glyph = b.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
20398
- const count = b.extensions.length;
20884
+ const count3 = b.extensions.length;
20399
20885
  const sorted = sortExtensionsCanonical(b.extensions);
20400
20886
  const items = sorted.map((ext) => ({
20401
20887
  glyph: ext.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff),
@@ -20406,8 +20892,8 @@ function renderBuiltInDetail(b, ansi) {
20406
20892
  glyph,
20407
20893
  id: b.id,
20408
20894
  source: ansi.dim(PLUGINS_TEXTS.sourceBuiltIn),
20409
- count,
20410
- plural: count === 1 ? "" : "s"
20895
+ count: count3,
20896
+ plural: count3 === 1 ? "" : "s"
20411
20897
  }) + PLUGINS_TEXTS.detailExtensionsBlock + renderExtensionItems(items);
20412
20898
  }
20413
20899
  function renderPluginDetail(match, ansi) {
@@ -20589,6 +21075,7 @@ function renderExtensionFields(meta) {
20589
21075
 
20590
21076
  // cli/commands/plugins/doctor.ts
20591
21077
  import { Command as Command24, Option as Option23 } from "clipanion";
21078
+ var CONTRIB_ERROR_SAMPLE_CAP = 3;
20592
21079
  var STATUS_ORDER = [
20593
21080
  "enabled",
20594
21081
  "disabled",
@@ -20614,6 +21101,8 @@ var PluginsDoctorCommand = class extends SmCommand {
20614
21101
  const knownKinds = collectKnownKinds(plugins);
20615
21102
  const applicableKindWarnings = collectApplicableKindWarnings(plugins, knownKinds);
20616
21103
  const unknownSlotWarnings = collectUnknownSlotWarnings(plugins, KNOWN_SLOT_NAMES);
21104
+ const contribErrors = await loadContributionErrors();
21105
+ const contribErrorGroups = groupContributionErrorsByPlugin(contribErrors);
20617
21106
  const bad = plugins.filter((p) => p.status !== "enabled" && p.status !== "disabled");
20618
21107
  const totalWarnings = applicableKindWarnings.length + unknownSlotWarnings.length;
20619
21108
  if (this.json) {
@@ -20623,23 +21112,51 @@ var PluginsDoctorCommand = class extends SmCommand {
20623
21112
  applicableKindWarnings,
20624
21113
  unknownSlotWarnings,
20625
21114
  totalWarnings,
21115
+ contribErrors,
20626
21116
  elapsedMs: this.elapsed.ms()
20627
21117
  });
20628
21118
  this.printer.data(JSON.stringify(envelope) + "\n");
20629
- return bad.length > 0 ? ExitCode.Issues : ExitCode.Ok;
20630
- }
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) {
20631
21142
  const ansi = this.ansiFor("stdout");
20632
- this.#renderSummaryHeader(counts.enabled, bad.length, totalWarnings);
20633
- this.#renderSourceBreakdown(builtIns2.length, plugins.length);
20634
- this.#renderStatusBreakdown(counts, ansi);
20635
- if (totalWarnings > 0) {
20636
- 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
+ );
20637
21153
  }
20638
- if (bad.length > 0) {
20639
- this.#renderIssues(bad, ansi);
20640
- 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);
20641
21159
  }
20642
- return ExitCode.Ok;
20643
21160
  }
20644
21161
  #renderSummaryHeader(enabled, badCount, warnings) {
20645
21162
  this.printer.data(
@@ -20676,10 +21193,10 @@ var PluginsDoctorCommand = class extends SmCommand {
20676
21193
  const statusLabelWidth = Math.max(...STATUS_ORDER.map((s) => s.length));
20677
21194
  this.printer.data(PLUGINS_TEXTS.doctorStatusHeader);
20678
21195
  for (const status of STATUS_ORDER) {
20679
- const count = counts[status];
20680
- const isProblem = status !== "enabled" && status !== "disabled" && count > 0;
21196
+ const count3 = counts[status];
21197
+ const isProblem = status !== "enabled" && status !== "disabled" && count3 > 0;
20681
21198
  const label = status.padEnd(statusLabelWidth);
20682
- const formattedCount = isProblem ? ansi.red(String(count)) : String(count);
21199
+ const formattedCount = isProblem ? ansi.red(String(count3)) : String(count3);
20683
21200
  this.printer.data(
20684
21201
  tx(PLUGINS_TEXTS.doctorStatusRow, {
20685
21202
  label: isProblem ? ansi.red(label) : label,
@@ -20742,6 +21259,42 @@ var PluginsDoctorCommand = class extends SmCommand {
20742
21259
  }
20743
21260
  }
20744
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
+ }
20745
21298
  };
20746
21299
  function countByStatus(builtIns2, plugins, resolveEnabled) {
20747
21300
  const counts = {
@@ -20938,6 +21491,15 @@ function buildDoctorJsonEnvelope(args2) {
20938
21491
  })
20939
21492
  });
20940
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
+ }));
20941
21503
  return {
20942
21504
  ok: true,
20943
21505
  kind: "plugins.doctor",
@@ -20952,10 +21514,36 @@ function buildDoctorJsonEnvelope(args2) {
20952
21514
  },
20953
21515
  issues,
20954
21516
  warnings,
21517
+ contributionErrors,
20955
21518
  elapsedMs: args2.elapsedMs
20956
21519
  };
20957
21520
  }
20958
-
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
+ }
21546
+
20959
21547
  // cli/commands/plugins/toggle.ts
20960
21548
  import { Command as Command25, Option as Option24 } from "clipanion";
20961
21549
  var TogglePluginsBase = class extends SmCommand {
@@ -21470,9 +22058,22 @@ function stub2(extId) {
21470
22058
  * Declared settings (\`settings\`):
21471
22059
  * - 'keywords' (${DEFAULT_INPUT_TYPE}) \u2192 exposed as ctx.settings.keywords
21472
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
+ *
21473
22067
  * See: spec/plugin-author-guide.md \xA7View contributions
21474
22068
  * spec/view-slots.md
21475
22069
  */
22070
+ const count = {
22071
+ slot: '${DEFAULT_SLOT}',
22072
+ icon: '\u{1F50D}',
22073
+ label: 'kw',
22074
+ emitWhenEmpty: false,
22075
+ };
22076
+
21476
22077
  export default {
21477
22078
  version: '0.1.0',
21478
22079
  description: 'Counts configured keywords per node.',
@@ -21488,14 +22089,7 @@ export default {
21488
22089
  },
21489
22090
  },
21490
22091
 
21491
- ui: {
21492
- count: {
21493
- slot: '${DEFAULT_SLOT}',
21494
- icon: '\u{1F50D}',
21495
- label: 'kw',
21496
- emitWhenEmpty: false,
21497
- },
21498
- },
22092
+ ui: { count },
21499
22093
 
21500
22094
  extract(ctx) {
21501
22095
  const keywords = (ctx.settings && ctx.settings.keywords) || ['TODO', 'FIXME'];
@@ -21505,7 +22099,7 @@ export default {
21505
22099
  total += (ctx.body.match(re) || []).length;
21506
22100
  }
21507
22101
  if (total > 0) {
21508
- ctx.emitContribution('count', { value: total });
22102
+ ctx.emitContribution(count, { value: total });
21509
22103
  }
21510
22104
  },
21511
22105
  };
@@ -21799,8 +22393,8 @@ var VIEW_SLOTS_CATALOG = [
21799
22393
  { id: "card.footer.left", summary: "Counter chip in the left footer of the card." },
21800
22394
  { id: "card.footer.right", summary: "Counter chip in the right footer of the card." },
21801
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`.' },
21802
- { id: "inspector.header.badge.counter", summary: "Counter chip in the inspector header badge cluster." },
21803
- { 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)." },
21804
22398
  { id: "inspector.body.panel.breakdown", summary: "Top-N labeled values rendered as a bar chart in the inspector body." },
21805
22399
  { id: "inspector.body.panel.records", summary: "Tabular data (rows \xD7 columns \u2264 50 \xD7 6) in the inspector body." },
21806
22400
  { id: "inspector.body.panel.tree", summary: "Recursive label/children hierarchy (depth \u2264 6, total \u2264 200) in the inspector body." },
@@ -22098,15 +22692,15 @@ var RefreshCommand = class extends SmCommand {
22098
22692
  return ExitCode.Ok;
22099
22693
  }
22100
22694
  const glyph = ansi.green("\u2713");
22101
- const count = freshEnrichments.length;
22102
- 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;
22103
22697
  if (this.stale) {
22104
22698
  const nodeCount = targetNodes.length;
22105
22699
  const nodeNoun = nodeCount === 1 ? REFRESH_TEXTS.refreshNodeNounSingular : REFRESH_TEXTS.refreshNodeNounPlural;
22106
22700
  this.printer.data(
22107
22701
  tx(REFRESH_TEXTS.refreshSuccessStale, {
22108
22702
  glyph,
22109
- count,
22703
+ count: count3,
22110
22704
  noun,
22111
22705
  nodeCount,
22112
22706
  nodeNoun
@@ -22116,7 +22710,7 @@ var RefreshCommand = class extends SmCommand {
22116
22710
  this.printer.data(
22117
22711
  tx(REFRESH_TEXTS.refreshSuccessSingle, {
22118
22712
  glyph,
22119
- count,
22713
+ count: count3,
22120
22714
  noun,
22121
22715
  nodePath: this.nodePath
22122
22716
  })
@@ -22593,7 +23187,15 @@ function createWatcherRuntime(opts) {
22593
23187
  runOptions.priorExtractorRuns = priorState.extractorRuns;
22594
23188
  }
22595
23189
  const ran = await runScanWithRenames(kernel, runOptions);
22596
- 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;
22597
23199
  await withSqlite(
22598
23200
  { databasePath: opts.dbPath },
22599
23201
  (writer) => writer.scans.persist(result, {
@@ -22601,6 +23203,7 @@ function createWatcherRuntime(opts) {
22601
23203
  extractorRuns,
22602
23204
  enrichments,
22603
23205
  contributions,
23206
+ contributionErrors,
22604
23207
  registeredContributionKeys: collectRegisteredContributionKeys(composed),
22605
23208
  freshlyRunTuples
22606
23209
  })
@@ -22929,11 +23532,11 @@ async function runWatchLoop(opts) {
22929
23532
  }
22930
23533
  void info;
22931
23534
  },
22932
- onBreakerTripped: (count, message) => {
23535
+ onBreakerTripped: (count3, message) => {
22933
23536
  context.stderr.write(
22934
23537
  tx(WATCH_TEXTS.breakerTripped, {
22935
23538
  glyph: errGlyph,
22936
- count,
23539
+ count: count3,
22937
23540
  hint: stderrAnsi.dim(tx(WATCH_TEXTS.breakerTrippedHint, { message }))
22938
23541
  })
22939
23542
  );
@@ -23475,8 +24078,8 @@ function formatScanCounts(opts) {
23475
24078
  }
23476
24079
  return parts.join(" \xB7 ");
23477
24080
  }
23478
- function countNoun(count, singular, plural) {
23479
- return count === 1 ? singular : plural;
24081
+ function countNoun(count3, singular, plural) {
24082
+ return count3 === 1 ? singular : plural;
23480
24083
  }
23481
24084
 
23482
24085
  // cli/commands/scan-compare.ts
@@ -23758,7 +24361,7 @@ import { WebSocketServer } from "ws";
23758
24361
  // server/app.ts
23759
24362
  import { Hono } from "hono";
23760
24363
  import { bodyLimit } from "hono/body-limit";
23761
- import { HTTPException as HTTPException16 } from "hono/http-exception";
24364
+ import { HTTPException as HTTPException17 } from "hono/http-exception";
23762
24365
 
23763
24366
  // core/config/service.ts
23764
24367
  var ConfigService = class {
@@ -23921,33 +24524,33 @@ var SERVER_TEXTS = {
23921
24524
  // Hono's `app.notFound` fallback, every other unmatched path funnels
23922
24525
  // here (after static + SPA fallback have had their turn).
23923
24526
  unknownPath: "Not found: {{path}}.",
23924
- // ---- sidecar bump route (routes/sidecar.ts) ------------------------------
23925
- // 409 refusal when a fresh node is bumped without `force`. Dispatch
23926
- // is via the typed `ConflictError` (`code: 'sidecar-fresh'`), so the
23927
- // `sidecar-fresh:` prefix is NOT load-bearing; it stays only for
23928
- // log-grep affinity with the CLI's bump verb.
23929
- 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.
23930
24534
  // 400 envelopes thrown by `parseBody` when the request payload is
23931
- // malformed. Each branch has its own key so the UI / log can
23932
- // disambiguate without regex on the message.
23933
- sidecarBodyNotJson: "Request body must be valid JSON.",
23934
- sidecarBodyNotObject: "Request body must be a JSON object.",
23935
- sidecarNodePathRequired: "`nodePath` is required and must be a non-empty string.",
23936
- sidecarForceMustBeBoolean: "`force` must be a boolean when present.",
23937
- sidecarConfirmMustBeBoolean: "`confirm` must be a boolean when present.",
23938
- /**
23939
- * 412 envelope when `POST /api/sidecar/bump` would create a `.sm`
23940
- * file but `allowEditSmFiles` is still false. The UI's bump
23941
- * call-path catches `code: 'confirm-required'` and opens a
23942
- * `ConfirmationService` dialog explaining `.sm` writes; on accept
23943
- * it retries with `confirm: true` in the body.
23944
- */
23945
- 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).",
23946
- // 500 envelope when the built-in bump action ships without an
23947
- // `invoke()`, should be impossible in production but the route
23948
- // throws a typed envelope rather than a bare `Error` so the global
23949
- // `app.onError` can format it.
23950
- 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}}".',
23951
24554
  // ---- POST /api/scan (manual refresh) ------------------------------------
23952
24555
  // 400, runtime cannot persist a meaningful scan because the boot
23953
24556
  // dropped half the pipeline. Same gate the `?fresh=1` GET applies.
@@ -25018,6 +25621,8 @@ function registerPluginsRoute(app, deps) {
25018
25621
  app.get("/api/plugins", async (c) => {
25019
25622
  const resolveEnabled = await buildFreshResolver2(deps);
25020
25623
  const items = listItems(deps, resolveEnabled);
25624
+ const errorsByPlugin = await loadRuntimeContributionErrors(deps);
25625
+ attachRuntimeContributionErrors(items, errorsByPlugin);
25021
25626
  return c.json(
25022
25627
  buildListEnvelope({
25023
25628
  kind: "plugins",
@@ -25192,6 +25797,41 @@ function firstVersion(extensions) {
25192
25797
  function classifyPluginSource(_pluginPath, _deps) {
25193
25798
  return "project";
25194
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
+ }
25195
25835
  async function persistAndProject(c, deps, configKey, enabled) {
25196
25836
  const overrides = await tryWithSqlite(
25197
25837
  { databasePath: deps.options.dbPath, autoBackup: false },
@@ -25808,24 +26448,39 @@ var parsePatchBody4 = makeBodyValidator(PATCH_BODY_SCHEMA3, {
25808
26448
  import { existsSync as existsSync28 } from "fs";
25809
26449
  import { HTTPException as HTTPException13 } from "hono/http-exception";
25810
26450
  function registerActiveProviderRoute(app, deps) {
25811
- app.get("/api/active-provider", (c) => {
25812
- return c.json(buildEnvelope4(deps));
26451
+ app.get("/api/active-provider", async (c) => {
26452
+ return c.json(await buildEnvelope4(deps));
25813
26453
  });
25814
26454
  app.patch("/api/active-provider", async (c) => {
25815
26455
  const body = await parsePatchBody5(c.req.raw);
25816
26456
  const result = applyLensSwitch(deps, body.activeProvider);
25817
26457
  deps.configService.reload();
25818
- return c.json({ ...buildEnvelope4(deps), switch: result });
26458
+ return c.json({ ...await buildEnvelope4(deps), switch: result });
25819
26459
  });
25820
26460
  }
25821
- function buildEnvelope4(deps) {
26461
+ async function buildEnvelope4(deps) {
25822
26462
  const r = resolveActiveProvider(deps.runtimeContext.cwd, deps.providers);
25823
26463
  return {
25824
26464
  activeProvider: r.resolved,
25825
26465
  detected: r.detected,
25826
- source: r.source
26466
+ source: r.source,
26467
+ selectable: await resolveSelectableProviders(deps)
25827
26468
  };
25828
26469
  }
26470
+ async function resolveSelectableProviders(deps) {
26471
+ const resolveEnabled = await buildFreshResolver({
26472
+ databasePath: deps.options.dbPath,
26473
+ effectiveConfig: () => deps.configService.effective(),
26474
+ fallbackResolver: deps.pluginRuntime.resolveEnabled
26475
+ });
26476
+ const selectable = /* @__PURE__ */ new Set();
26477
+ for (const provider of deps.providers) {
26478
+ if (isPluginExtensionEnabled(provider, resolveEnabled)) {
26479
+ selectable.add(provider.id);
26480
+ }
26481
+ }
26482
+ return [...selectable];
26483
+ }
25829
26484
  function applyLensSwitch(deps, newValue) {
25830
26485
  const cwd = deps.runtimeContext.cwd;
25831
26486
  try {
@@ -25866,8 +26521,168 @@ var parsePatchBody5 = makeBodyValidator(PATCH_BODY_SCHEMA4, {
25866
26521
  }
25867
26522
  });
25868
26523
 
25869
- // 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
25870
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";
25871
26686
 
25872
26687
  // server/scan-mutex.ts
25873
26688
  var inFlight = null;
@@ -26046,7 +26861,7 @@ function registerScanRoute(app, deps) {
26046
26861
  }
26047
26862
  async function runPersistedScan(c, deps) {
26048
26863
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
26049
- throw new HTTPException14(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
26864
+ throw new HTTPException16(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
26050
26865
  }
26051
26866
  const dbExists = await tryWithSqlite(
26052
26867
  { databasePath: deps.options.dbPath, autoBackup: false },
@@ -26083,7 +26898,7 @@ async function runPersistedScan(c, deps) {
26083
26898
  ...deps.options.maxNodes !== void 0 ? { maxNodes: deps.options.maxNodes } : {}
26084
26899
  });
26085
26900
  if (outcome.kind !== "ok") {
26086
- throw new HTTPException14(500, {
26901
+ throw new HTTPException16(500, {
26087
26902
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.scanGuardTrip, { existing: outcome.existing }) : outcome.message
26088
26903
  });
26089
26904
  }
@@ -26161,7 +26976,7 @@ function groupTagsByPath2(rows) {
26161
26976
  }
26162
26977
  async function runFreshScan(deps) {
26163
26978
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
26164
- throw new HTTPException14(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
26979
+ throw new HTTPException16(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
26165
26980
  }
26166
26981
  const resolveEnabledOverride = await buildBffResolverOverride(deps);
26167
26982
  const outcome = await runScanForCommand({
@@ -26196,7 +27011,7 @@ async function runFreshScan(deps) {
26196
27011
  ...deps.options.maxNodes !== void 0 ? { maxNodes: deps.options.maxNodes } : {}
26197
27012
  });
26198
27013
  if (outcome.kind !== "ok") {
26199
- throw new HTTPException14(500, {
27014
+ throw new HTTPException16(500, {
26200
27015
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.freshScanGuardTrip, { existing: outcome.existing }) : outcome.message
26201
27016
  });
26202
27017
  }
@@ -26242,141 +27057,6 @@ function emptyScanResult() {
26242
27057
  };
26243
27058
  }
26244
27059
 
26245
- // server/routes/sidecar.ts
26246
- import { HTTPException as HTTPException15 } from "hono/http-exception";
26247
- import { resolve as resolve36 } from "path";
26248
- var STATUS_FRESH = "fresh";
26249
- var ENVELOPE_KIND2 = "sidecar.bumped";
26250
- var BUMP_BODY_SCHEMA = {
26251
- type: "object",
26252
- additionalProperties: false,
26253
- required: ["nodePath"],
26254
- properties: {
26255
- nodePath: { type: "string", minLength: 1 },
26256
- force: { type: "boolean" },
26257
- confirm: { type: "boolean" }
26258
- }
26259
- };
26260
- var parseBody = makeBodyValidator(BUMP_BODY_SCHEMA, {
26261
- notJson: SERVER_TEXTS.sidecarBodyNotJson,
26262
- notObject: SERVER_TEXTS.sidecarBodyNotObject,
26263
- invalid: SERVER_TEXTS.sidecarBodyNotObject,
26264
- mapping: {
26265
- "/nodePath:required": SERVER_TEXTS.sidecarNodePathRequired,
26266
- ":type:object": SERVER_TEXTS.sidecarBodyNotObject,
26267
- "/nodePath:type:string": SERVER_TEXTS.sidecarNodePathRequired,
26268
- "/nodePath:minLength": SERVER_TEXTS.sidecarNodePathRequired,
26269
- "/force:type:boolean": SERVER_TEXTS.sidecarForceMustBeBoolean,
26270
- "/confirm:type:boolean": SERVER_TEXTS.sidecarConfirmMustBeBoolean
26271
- }
26272
- });
26273
- function registerSidecarRoutes(app, deps) {
26274
- app.post("/api/sidecar/bump", async (c) => {
26275
- const startedAt = Date.now();
26276
- const body = await parseBody(c.req.raw);
26277
- const node = await loadNode(deps, body.nodePath);
26278
- let absPath;
26279
- try {
26280
- assertContained(deps.runtimeContext.cwd, node.path);
26281
- absPath = resolve36(deps.runtimeContext.cwd, node.path);
26282
- } catch (err) {
26283
- throw new HTTPException15(400, { message: formatErrorMessage(err) });
26284
- }
26285
- const result = invokeBump2(node, absPath, body);
26286
- if (result.report.ok === false && result.report.reason === "fresh") {
26287
- throw new ConflictError({ code: "sidecar-fresh", message: SERVER_TEXTS.sidecarFreshRefusal });
26288
- }
26289
- if (result.report.ok === true && result.report.noop === true) {
26290
- const envelope2 = {
26291
- schemaVersion: "1",
26292
- kind: ENVELOPE_KIND2,
26293
- value: {
26294
- nodePath: node.path,
26295
- version: pickExistingVersion(node),
26296
- status: STATUS_FRESH
26297
- },
26298
- elapsedMs: Date.now() - startedAt
26299
- };
26300
- return c.json(envelope2);
26301
- }
26302
- const store = new FilesystemSidecarStore(ensureSidecarWritesAllowed);
26303
- try {
26304
- for (const w of result.writes ?? []) {
26305
- if (w.kind === "sidecar") {
26306
- await store.applyPatch(w.path, w.changes, {
26307
- confirm: body.confirm === true,
26308
- cwd: deps.runtimeContext.cwd
26309
- });
26310
- }
26311
- }
26312
- } catch (err) {
26313
- if (err instanceof EConsentRequiredError) throw err;
26314
- throw new HTTPException15(500, { message: formatErrorMessage(err) });
26315
- }
26316
- if (body.confirm === true) {
26317
- deps.configService.reload();
26318
- }
26319
- const newVersion = result.report.version ?? null;
26320
- const eventData = {
26321
- nodePath: node.path,
26322
- version: newVersion,
26323
- status: STATUS_FRESH
26324
- };
26325
- const wsEnvelope = {
26326
- type: ENVELOPE_KIND2,
26327
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
26328
- data: eventData
26329
- };
26330
- deps.broadcaster.broadcast(wsEnvelope);
26331
- const envelope = {
26332
- schemaVersion: "1",
26333
- kind: ENVELOPE_KIND2,
26334
- value: {
26335
- nodePath: node.path,
26336
- version: newVersion,
26337
- status: STATUS_FRESH
26338
- },
26339
- elapsedMs: Date.now() - startedAt
26340
- };
26341
- return c.json(envelope);
26342
- });
26343
- }
26344
- async function loadNode(deps, nodePath) {
26345
- const persisted = await tryWithSqlite(
26346
- { databasePath: deps.options.dbPath, autoBackup: false },
26347
- async (adapter) => adapter.scans.load()
26348
- );
26349
- const node = persisted?.nodes.find((n) => n.path === nodePath);
26350
- if (!node) {
26351
- throw new HTTPException15(404, {
26352
- message: tx(SERVER_TEXTS.nodeNotFound, { path: sanitizeForTerminal(nodePath) })
26353
- });
26354
- }
26355
- return node;
26356
- }
26357
- function invokeBump2(node, absPath, body) {
26358
- if (!nodeBumpAction.invoke) {
26359
- throw new HTTPException15(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
26360
- }
26361
- const input = {};
26362
- if (body.force === true) input.force = true;
26363
- return nodeBumpAction.invoke(input, {
26364
- node,
26365
- nodeAbsolutePath: absPath,
26366
- invoker: "ui",
26367
- now: () => /* @__PURE__ */ new Date(),
26368
- settings: {}
26369
- });
26370
- }
26371
- function pickExistingVersion(node) {
26372
- const overlay = node.sidecar;
26373
- if (!overlay || overlay.present !== true) return null;
26374
- const annotations = overlay.annotations;
26375
- if (!annotations) return null;
26376
- const v = annotations["version"];
26377
- return typeof v === "number" && Number.isFinite(v) ? v : null;
26378
- }
26379
-
26380
27060
  // server/routes/update-status.ts
26381
27061
  function registerUpdateStatusRoute(app, deps) {
26382
27062
  app.get("/api/update-status", async (c) => {
@@ -26543,13 +27223,13 @@ function attachBroadcasterRoute(app, broadcaster) {
26543
27223
 
26544
27224
  // server/app.ts
26545
27225
  var BODY_LIMIT_BYTES = 1024 * 1024;
26546
- var DbMissingError = class extends HTTPException16 {
27226
+ var DbMissingError = class extends HTTPException17 {
26547
27227
  constructor(message) {
26548
27228
  super(500, { message });
26549
27229
  this.name = "DbMissingError";
26550
27230
  }
26551
27231
  };
26552
- var BulkValidationError = class extends HTTPException16 {
27232
+ var BulkValidationError = class extends HTTPException17 {
26553
27233
  id;
26554
27234
  code;
26555
27235
  constructor(init) {
@@ -26559,7 +27239,7 @@ var BulkValidationError = class extends HTTPException16 {
26559
27239
  this.code = init.code;
26560
27240
  }
26561
27241
  };
26562
- var LoopbackGateError = class extends HTTPException16 {
27242
+ var LoopbackGateError = class extends HTTPException17 {
26563
27243
  code;
26564
27244
  constructor(init) {
26565
27245
  super(403, { message: init.message });
@@ -26567,7 +27247,7 @@ var LoopbackGateError = class extends HTTPException16 {
26567
27247
  this.code = init.code;
26568
27248
  }
26569
27249
  };
26570
- var ConflictError = class extends HTTPException16 {
27250
+ var ConflictError = class extends HTTPException17 {
26571
27251
  code;
26572
27252
  constructor(init) {
26573
27253
  super(409, { message: init.message });
@@ -26575,6 +27255,21 @@ var ConflictError = class extends HTTPException16 {
26575
27255
  this.code = init.code;
26576
27256
  }
26577
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
+ };
26578
27273
  function createApp(deps) {
26579
27274
  const app = new Hono();
26580
27275
  const configService = new ConfigService({
@@ -26588,7 +27283,7 @@ function createApp(deps) {
26588
27283
  bodyLimit({
26589
27284
  maxSize: BODY_LIMIT_BYTES,
26590
27285
  onError: () => {
26591
- 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) }) });
26592
27287
  }
26593
27288
  })
26594
27289
  );
@@ -26624,7 +27319,7 @@ function createApp(deps) {
26624
27319
  registerGraphRoute(app, routeDeps);
26625
27320
  registerConfigRoute(app, routeDeps);
26626
27321
  registerPluginsRoute(app, routeDeps);
26627
- registerSidecarRoutes(app, { ...routeDeps, broadcaster: deps.broadcaster });
27322
+ registerActionsRoutes(app, { ...routeDeps, broadcaster: deps.broadcaster, kernel: deps.kernel });
26628
27323
  registerFavoritesRoutes(app, routeDeps);
26629
27324
  registerAnnotationsRoute(app, { kernel: deps.kernel });
26630
27325
  registerContributionsRoutes(app, { ...routeDeps, kernel: deps.kernel });
@@ -26634,7 +27329,7 @@ function createApp(deps) {
26634
27329
  registerActiveProviderRoute(app, routeDeps);
26635
27330
  registerProjectIgnoreRoute(app, routeDeps);
26636
27331
  app.all("/api/*", (c) => {
26637
- throw new HTTPException16(404, {
27332
+ throw new HTTPException17(404, {
26638
27333
  message: tx(SERVER_TEXTS.unknownApiEndpoint, { path: sanitizeForTerminal(c.req.path) })
26639
27334
  });
26640
27335
  });
@@ -26642,7 +27337,7 @@ function createApp(deps) {
26642
27337
  app.use("*", createStaticHandler({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
26643
27338
  app.get("*", createSpaFallback({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
26644
27339
  app.notFound((c) => {
26645
- throw new HTTPException16(404, {
27340
+ throw new HTTPException17(404, {
26646
27341
  message: tx(SERVER_TEXTS.unknownPath, { path: sanitizeForTerminal(c.req.path) })
26647
27342
  });
26648
27343
  });
@@ -26693,18 +27388,9 @@ function formatError2(err, c) {
26693
27388
  };
26694
27389
  return c.json(envelope, 403);
26695
27390
  }
26696
- if (err instanceof ConflictError) {
26697
- const envelope = {
26698
- ok: false,
26699
- error: {
26700
- code: err.code,
26701
- message: err.message,
26702
- details: null
26703
- }
26704
- };
26705
- return c.json(envelope, 409);
26706
- }
26707
- if (err instanceof HTTPException16) {
27391
+ const conflict = formatConflict(err, c);
27392
+ if (conflict) return conflict;
27393
+ if (err instanceof HTTPException17) {
26708
27394
  const status = err.status;
26709
27395
  const envelope = {
26710
27396
  ok: false,
@@ -26740,6 +27426,23 @@ function formatError2(err, c) {
26740
27426
  }
26741
27427
  return formatInternalErrorFallThrough(err, c);
26742
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
+ }
26743
27446
  function formatInternalErrorFallThrough(err, c) {
26744
27447
  const detail = formatErrorMessage(err);
26745
27448
  const stack = err instanceof Error && typeof err.stack === "string" ? err.stack : void 0;
@@ -26965,6 +27668,8 @@ function entryFromRegistered(c) {
26965
27668
  if (c.icon !== void 0) entry.icon = c.icon;
26966
27669
  if (c.emptyText !== void 0) entry.emptyText = c.emptyText;
26967
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;
26968
27673
  return entry;
26969
27674
  }
26970
27675
 
@@ -27222,8 +27927,10 @@ async function createServer(options, extra = {}) {
27222
27927
  }
27223
27928
  async function assemblePluginRuntime(options, runtimeContext) {
27224
27929
  const pluginRuntime = options.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime({ runtimeContext });
27225
- for (const warn of pluginRuntime.warnings) {
27226
- log.warn(sanitizeForTerminal(warn));
27930
+ if (options.noWatcher) {
27931
+ for (const warn of pluginRuntime.warnings) {
27932
+ log.warn(sanitizeForTerminal(warn));
27933
+ }
27227
27934
  }
27228
27935
  const builtInProviders = options.noBuiltIns ? [] : collectBuiltInProviders();
27229
27936
  const allProviders = [...builtInProviders, ...pluginRuntime.extensions.providers];
@@ -27234,6 +27941,11 @@ async function assemblePluginRuntime(options, runtimeContext) {
27234
27941
  function assembleKernel(pluginRuntime, noBuiltIns) {
27235
27942
  const kernel = createKernel();
27236
27943
  kernel.setRegisteredAnnotationKeys(pluginRuntime.annotationContributions);
27944
+ if (!noBuiltIns) {
27945
+ for (const action of builtIns().actions) {
27946
+ kernel.registry.register(action);
27947
+ }
27948
+ }
27237
27949
  const mergedViewContributions = [...pluginRuntime.viewContributions];
27238
27950
  if (!noBuiltIns) {
27239
27951
  const userKey = new Set(
@@ -28103,13 +28815,13 @@ var ShowCommand = class extends SmCommand {
28103
28815
  }
28104
28816
  };
28105
28817
  function renderHuman2(doc, ansi) {
28106
- const { node, linksOut, linksIn, issues } = doc;
28818
+ const { node, linksOut: linksOut2, linksIn: linksIn2, issues } = doc;
28107
28819
  const out = [];
28108
28820
  out.push(renderHeader(node, ansi));
28109
28821
  out.push(renderFieldBlock(node, ansi));
28110
28822
  out.push(renderFrontmatter(node, ansi));
28111
- if (linksOut.length > 0) out.push(renderLinksSection("out", linksOut, ansi));
28112
- 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));
28113
28825
  if (issues.length > 0) out.push(renderIssuesSection(issues, node.path, ansi));
28114
28826
  return out.join("");
28115
28827
  }
@@ -28501,7 +29213,9 @@ var SidecarRefreshCommand = class extends SmCommand {
28501
29213
  frontmatterHash: node.frontmatterHash
28502
29214
  }
28503
29215
  },
28504
- { 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 }
28505
29219
  );
28506
29220
  } catch (err) {
28507
29221
  if (err instanceof EConsentRequiredError) throw err;
@@ -28782,7 +29496,9 @@ var SidecarAnnotateCommand = class extends SmCommand {
28782
29496
  await store.applyPatch(
28783
29497
  sidecarAbsPath,
28784
29498
  scaffoldSidecarObject(node),
28785
- { 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 }
28786
29502
  );
28787
29503
  } catch (err) {
28788
29504
  if (err instanceof EConsentRequiredError) throw err;
@@ -29043,11 +29759,13 @@ var TUTORIAL_TEXTS = {
29043
29759
  // the error shape: glyph + headline + dim hint.
29044
29760
  notEmpty: "{{glyph}} sm tutorial: the current directory is not empty (found {{entries}})\n {{hint}}\n",
29045
29761
  notEmptyHint: "sm tutorial seeds a self-contained scenario; run it in a fresh empty directory, or pass `--force` to use this one anyway.",
29046
- // Invalid `variant` positional argument. Goes to stderr, exit code 2.
29047
- // Mirrors the error shape: glyph + headline + dim hint enumerating the
29048
- // valid values.
29049
- invalidVariant: "{{glyph}} sm tutorial: unknown variant '{{variant}}'\n {{hint}}\n",
29050
- 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.",
29051
29769
  // I/O failure on write or on reading the bundled skill source.
29052
29770
  writeFailed: "{{glyph}} sm tutorial: failed to write {{target}}: {{message}}\n",
29053
29771
  sourceMissing: "{{glyph}} sm tutorial: could not read the bundled skill payload for {{target}} from the install.\n {{hint}}\n",
@@ -29055,47 +29773,36 @@ var TUTORIAL_TEXTS = {
29055
29773
  };
29056
29774
 
29057
29775
  // cli/commands/tutorial.ts
29058
- var VALID_VARIANTS = ["tutorial", "master"];
29059
- var DEFAULT_VARIANT = "tutorial";
29060
- var VARIANT_SPECS = {
29061
- tutorial: {
29062
- slug: "sm-tutorial",
29063
- sourceDir: ".claude/skills/sm-tutorial",
29064
- triggerEn: "run the tutorial",
29065
- triggerEs: "ejecuta el tutorial"
29066
- },
29067
- master: {
29068
- slug: "sm-master",
29069
- sourceDir: ".claude/skills/sm-master",
29070
- triggerEn: "run the master tutorial",
29071
- triggerEs: "ejecuta el tutorial maestro"
29072
- }
29073
- };
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";
29074
29780
  var TutorialCommand = class extends SmCommand {
29075
29781
  static paths = [["tutorial"]];
29076
29782
  static usage = Command37.Usage({
29077
29783
  category: "Setup",
29078
29784
  description: "Materialize an interactive tester tutorial as a Claude Code skill folder under `<cwd>/.claude/skills/`.",
29079
29785
  details: `
29080
- Drops the canonical skill directory (SKILL.md + any references/
29081
- sub-folder) under \`<cwd>/.claude/skills/sm-tutorial/\` (default)
29082
- or \`<cwd>/.claude/skills/sm-master/\` (when invoked as \`sm
29083
- tutorial master\`). Claude Code auto-discovers the skill the
29084
- next time it boots in this directory; the tester invokes it by
29085
- 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.
29086
29791
 
29087
29792
  Does NOT require an initialized .skill-map/ project. Refuses to
29088
- overwrite the target directory unless --force is passed. Valid
29089
- values for the positional argument are: tutorial (default),
29090
- master.
29793
+ overwrite the target directory unless --force is passed. Takes no
29794
+ positional argument.
29091
29795
  `,
29092
29796
  examples: [
29093
- ["Materialize the basic tutorial skill in the cwd", "$0 tutorial"],
29094
- ["Materialize the advanced tutorial skill in the cwd", "$0 tutorial master"],
29797
+ ["Materialize the tutorial skill in the cwd", "$0 tutorial"],
29095
29798
  ["Overwrite an existing target directory", "$0 tutorial --force"]
29096
29799
  ]
29097
29800
  });
29098
- 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 });
29099
29806
  // Named `forProvider`, NOT `for` (reserved word). The CLI surface stays
29100
29807
  // `--for`; selects the destination Provider whose `scaffold.skillDir`
29101
29808
  // the skill is materialised under, skipping the interactive prompt.
@@ -29111,9 +29818,16 @@ var TutorialCommand = class extends SmCommand {
29111
29818
  const stderr = this.context.stderr;
29112
29819
  const stderrAnsi = this.ansiFor("stderr");
29113
29820
  const errGlyph = stderrAnsi.red("\u2715");
29114
- const variant = this.resolveVariantArg(errGlyph, stderrAnsi);
29115
- if (variant === null) return ExitCode.Error;
29116
- 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
+ }
29117
29831
  if (!this.force && !isDirEmpty(ctx.cwd)) {
29118
29832
  this.printer.error(
29119
29833
  tx(TUTORIAL_TEXTS.notEmpty, {
@@ -29127,11 +29841,11 @@ var TutorialCommand = class extends SmCommand {
29127
29841
  const targets = listScaffoldTargets();
29128
29842
  const target = await this.resolveScaffoldTarget(targets, stderrAnsi, errGlyph);
29129
29843
  if (target === null) return ExitCode.Error;
29130
- const targetDir = join21(ctx.cwd, target.skillDir, spec.slug);
29131
- const targetDisplay = `${target.skillDir}/${spec.slug}/`;
29844
+ const targetDir = join21(ctx.cwd, target.skillDir, SKILL_SLUG);
29845
+ const targetDisplay = `${target.skillDir}/${SKILL_SLUG}/`;
29132
29846
  let sourceDir;
29133
29847
  try {
29134
- sourceDir = resolveSkillSourceDir(variant);
29848
+ sourceDir = resolveSkillSourceDir();
29135
29849
  } catch {
29136
29850
  this.printer.error(
29137
29851
  tx(TUTORIAL_TEXTS.sourceMissing, {
@@ -29166,38 +29880,18 @@ var TutorialCommand = class extends SmCommand {
29166
29880
  this.printer.data(
29167
29881
  tx(TUTORIAL_TEXTS.written, {
29168
29882
  glyph: ansi.green("\u2713"),
29169
- slug: spec.slug,
29883
+ slug: SKILL_SLUG,
29170
29884
  target: targetDisplay,
29171
29885
  provider: ansi.dim(target.label),
29172
29886
  cwd: ansi.dim(displayCwd(ctx.cwd)),
29173
29887
  enLabel: ansi.dim(TUTORIAL_TEXTS.writtenLabelEn),
29174
29888
  esLabel: ansi.dim(TUTORIAL_TEXTS.writtenLabelEs),
29175
- enTrigger: spec.triggerEn,
29176
- esTrigger: spec.triggerEs
29889
+ enTrigger: TRIGGER_EN,
29890
+ esTrigger: TRIGGER_ES
29177
29891
  })
29178
29892
  );
29179
29893
  return ExitCode.Ok;
29180
29894
  }
29181
- /**
29182
- * Validate the positional `variant` arg against the closed catalog.
29183
- * Returns the resolved variant, or `null` after printing the
29184
- * `invalidVariant` error (caller exits non-zero). Extracted from
29185
- * `run()` to keep its cyclomatic complexity within the lint budget.
29186
- */
29187
- resolveVariantArg(errGlyph, stderrAnsi) {
29188
- const rawVariant = this.variant;
29189
- if (rawVariant !== void 0 && !isTutorialVariant(rawVariant)) {
29190
- this.printer.error(
29191
- tx(TUTORIAL_TEXTS.invalidVariant, {
29192
- glyph: errGlyph,
29193
- variant: rawVariant,
29194
- hint: stderrAnsi.dim(TUTORIAL_TEXTS.invalidVariantHint)
29195
- })
29196
- );
29197
- return null;
29198
- }
29199
- return rawVariant ?? DEFAULT_VARIANT;
29200
- }
29201
29895
  /**
29202
29896
  * Resolve the destination Provider. Precedence:
29203
29897
  * 1. `--for <id>` (validated against the scaffold-capable catalog).
@@ -29257,9 +29951,6 @@ var TutorialCommand = class extends SmCommand {
29257
29951
  return picked;
29258
29952
  }
29259
29953
  };
29260
- function isTutorialVariant(value) {
29261
- return VALID_VARIANTS.includes(value);
29262
- }
29263
29954
  function toScaffoldTarget(provider) {
29264
29955
  const scaffold = provider.scaffold;
29265
29956
  if (!scaffold || !scaffold.skillDir) return null;
@@ -29326,23 +30017,21 @@ function listCwdEntries(dir) {
29326
30017
  const more = entries.length > shown.length ? ", ..." : "";
29327
30018
  return shown.join(", ") + more;
29328
30019
  }
29329
- var cachedSourceDirs = /* @__PURE__ */ new Map();
29330
- function resolveSkillSourceDir(variant) {
29331
- const cached = cachedSourceDirs.get(variant);
29332
- if (cached !== void 0) return cached;
29333
- const spec = VARIANT_SPECS[variant];
30020
+ var cachedSourceDir;
30021
+ function resolveSkillSourceDir() {
30022
+ if (cachedSourceDir !== void 0) return cachedSourceDir;
29334
30023
  const here = dirname20(fileURLToPath7(import.meta.url));
29335
30024
  const candidates = [
29336
- // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/
29337
- resolve39(here, "../../..", spec.sourceDir),
29338
- // bundled: dist/cli.js → dist/cli/tutorial/<slug> (sibling)
29339
- resolve39(here, "cli/tutorial", spec.slug),
29340
- // bundled fallback: any-depth → cli/tutorial/<slug>
29341
- 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)
29342
30031
  ];
29343
30032
  for (const candidate of candidates) {
29344
30033
  if (existsSync32(candidate) && statSync11(candidate).isDirectory()) {
29345
- cachedSourceDirs.set(variant, candidate);
30034
+ cachedSourceDir = candidate;
29346
30035
  return candidate;
29347
30036
  }
29348
30037
  }
@@ -29566,4 +30255,4 @@ function resolveBareDefault() {
29566
30255
  process.exit(ExitCode.Error);
29567
30256
  }
29568
30257
  //# sourceMappingURL=cli.js.map
29569
- //# debugId=1a42a4ca-6765-5a66-abc9-6b57464bc9c6
30258
+ //# debugId=82aaa5c6-6dae-53a6-8d4a-1a673357051c