@skill-map/cli 0.27.0 → 0.29.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 (36) hide show
  1. package/dist/cli/tutorial/sm-master.md +181 -97
  2. package/dist/cli/tutorial/sm-tutorial.md +94 -20
  3. package/dist/cli.js +1422 -1194
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +116 -99
  6. package/dist/index.js.map +1 -1
  7. package/dist/kernel/index.d.ts +903 -1004
  8. package/dist/kernel/index.js +116 -99
  9. package/dist/kernel/index.js.map +1 -1
  10. package/dist/ui/chunk-3SI3TVER.js +7 -0
  11. package/dist/ui/{chunk-4GTCV7V4.js → chunk-47OZB7LR.js} +1 -1
  12. package/dist/ui/{chunk-JMP2LDMI.js → chunk-5JBW2LUN.js} +1 -1
  13. package/dist/ui/chunk-BL7KARTN.js +317 -0
  14. package/dist/ui/chunk-DMSZOXER.js +1 -0
  15. package/dist/ui/{chunk-Y7MXGXU3.js → chunk-DZBSELHN.js} +1 -1
  16. package/dist/ui/chunk-EFKSD7PT.js +123 -0
  17. package/dist/ui/{chunk-Z2667C3S.js → chunk-FEPH4VNB.js} +1 -1
  18. package/dist/ui/{chunk-PY2R7LHN.js → chunk-FQOZBFJ5.js} +1 -1
  19. package/dist/ui/{chunk-WOLLYGGL.js → chunk-KJQEO6P3.js} +1 -1
  20. package/dist/ui/{chunk-VO6NF24F.js → chunk-LS2NXZQZ.js} +1 -1
  21. package/dist/ui/{chunk-J3YWUNFO.js → chunk-LTQTJU54.js} +1 -1
  22. package/dist/ui/{chunk-6BG7PBUN.js → chunk-NGIFGXW7.js} +1 -1
  23. package/dist/ui/{chunk-5W6J6H76.js → chunk-SBCO7ZSP.js} +1 -1
  24. package/dist/ui/chunk-VB56BUGO.js +1 -0
  25. package/dist/ui/{chunk-UXCAEDR6.js → chunk-VDQLDTTR.js} +1 -1
  26. package/dist/ui/{chunk-AD7RBRD3.js → chunk-WJLIYGWJ.js} +5 -5
  27. package/dist/ui/index.html +2 -2
  28. package/dist/ui/{main-LM44IIOO.js → main-LGW7AYEA.js} +2 -2
  29. package/dist/ui/skill-map-mark-matrix.svg +8 -0
  30. package/dist/ui/{styles-EGXMA46P.css → styles-CDN434T2.css} +1 -1
  31. package/package.json +10 -7
  32. package/dist/ui/chunk-H2J55DNK.js +0 -7
  33. package/dist/ui/chunk-LTSP2F6C.js +0 -123
  34. package/dist/ui/chunk-Q7L6LLAK.js +0 -1
  35. package/dist/ui/chunk-UAG2DUVV.js +0 -1
  36. package/dist/ui/chunk-VH5GRUT7.js +0 -255
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // cli/entry.ts
2
- import { existsSync as existsSync29 } from "fs";
2
+ import { existsSync as existsSync30 } from "fs";
3
3
  import { Builtins, Cli as Cli2 } from "clipanion";
4
4
 
5
5
  // kernel/adapters/in-memory-progress.ts
@@ -150,14 +150,6 @@ function makeHookDispatcher(hooks, emitter) {
150
150
  }
151
151
  const byTrigger = /* @__PURE__ */ new Map();
152
152
  for (const hook of hooks) {
153
- if (hook.mode === "probabilistic") {
154
- const qualifiedId2 = qualifiedExtensionId(hook.pluginId, hook.id);
155
- log.warn(
156
- `Probabilistic hook ${qualifiedId2} deferred to job subsystem (future job subsystem). The hook is registered but will not dispatch in-scan.`,
157
- { hookId: qualifiedId2, mode: "probabilistic" }
158
- );
159
- continue;
160
- }
161
153
  for (const trig of hook.triggers) {
162
154
  const bucket = byTrigger.get(trig);
163
155
  if (bucket) bucket.push(hook);
@@ -249,7 +241,7 @@ function bucketByKind(kind, instance, bag) {
249
241
  }
250
242
  }
251
243
 
252
- // built-in-plugins/providers/claude/schemas/skill.schema.json
244
+ // plugins/claude/providers/claude/schemas/skill.schema.json
253
245
  var skill_schema_default = {
254
246
  $schema: "https://json-schema.org/draft/2020-12/schema",
255
247
  $id: "https://skill-map.dev/providers/claude/v1/frontmatter/skill.schema.json",
@@ -263,7 +255,7 @@ var skill_schema_default = {
263
255
  properties: {}
264
256
  };
265
257
 
266
- // built-in-plugins/providers/claude/schemas/skill-base.schema.json
258
+ // plugins/claude/providers/claude/schemas/skill-base.schema.json
267
259
  var skill_base_schema_default = {
268
260
  $schema: "https://json-schema.org/draft/2020-12/schema",
269
261
  $id: "https://skill-map.dev/providers/claude/v1/frontmatter/skill-base.schema.json",
@@ -342,7 +334,7 @@ var skill_base_schema_default = {
342
334
  }
343
335
  };
344
336
 
345
- // built-in-plugins/providers/claude/schemas/agent.schema.json
337
+ // plugins/claude/providers/claude/schemas/agent.schema.json
346
338
  var agent_schema_default = {
347
339
  $schema: "https://json-schema.org/draft/2020-12/schema",
348
340
  $id: "https://skill-map.dev/providers/claude/v1/frontmatter/agent.schema.json",
@@ -423,7 +415,7 @@ var agent_schema_default = {
423
415
  }
424
416
  };
425
417
 
426
- // built-in-plugins/providers/claude/schemas/command.schema.json
418
+ // plugins/claude/providers/claude/schemas/command.schema.json
427
419
  var command_schema_default = {
428
420
  $schema: "https://json-schema.org/draft/2020-12/schema",
429
421
  $id: "https://skill-map.dev/providers/claude/v1/frontmatter/command.schema.json",
@@ -437,14 +429,13 @@ var command_schema_default = {
437
429
  properties: {}
438
430
  };
439
431
 
440
- // built-in-plugins/providers/claude/index.ts
432
+ // plugins/claude/providers/claude/index.ts
441
433
  var claudeProvider = {
442
434
  id: "claude",
443
435
  pluginId: "claude",
444
436
  kind: "provider",
445
437
  version: "1.0.0",
446
438
  description: "Walks Claude Code scope conventions (.claude/{agents,commands,skills}).",
447
- stability: "stable",
448
439
  // Declarative discovery: `.md` files parsed via the kernel's
449
440
  // `frontmatter-yaml` built-in. Equals the kernel's default but stated
450
441
  // explicitly so the Provider doubles as a copy-paste template for
@@ -474,7 +465,6 @@ var claudeProvider = {
474
465
  agent: {
475
466
  schema: "./schemas/agent.schema.json",
476
467
  schemaJson: agent_schema_default,
477
- defaultRefreshAction: "claude/summarize-agent",
478
468
  ui: {
479
469
  label: "Agents",
480
470
  color: "#3b82f6",
@@ -485,7 +475,6 @@ var claudeProvider = {
485
475
  command: {
486
476
  schema: "./schemas/command.schema.json",
487
477
  schemaJson: command_schema_default,
488
- defaultRefreshAction: "claude/summarize-command",
489
478
  ui: {
490
479
  label: "Commands",
491
480
  color: "#f59e0b",
@@ -499,7 +488,6 @@ var claudeProvider = {
499
488
  skill: {
500
489
  schema: "./schemas/skill.schema.json",
501
490
  schemaJson: skill_schema_default,
502
- defaultRefreshAction: "claude/summarize-skill",
503
491
  ui: {
504
492
  label: "Skills",
505
493
  color: "#10b981",
@@ -523,7 +511,7 @@ var claudeProvider = {
523
511
  }
524
512
  };
525
513
 
526
- // built-in-plugins/providers/gemini/schemas/agent.schema.json
514
+ // plugins/gemini/providers/gemini/schemas/agent.schema.json
527
515
  var agent_schema_default2 = {
528
516
  $schema: "https://json-schema.org/draft/2020-12/schema",
529
517
  $id: "https://skill-map.dev/providers/gemini/v1/frontmatter/agent.schema.json",
@@ -577,7 +565,7 @@ var agent_schema_default2 = {
577
565
  }
578
566
  };
579
567
 
580
- // built-in-plugins/providers/gemini/schemas/skill.schema.json
568
+ // plugins/gemini/providers/gemini/schemas/skill.schema.json
581
569
  var skill_schema_default2 = {
582
570
  $schema: "https://json-schema.org/draft/2020-12/schema",
583
571
  $id: "https://skill-map.dev/providers/gemini/v1/frontmatter/skill.schema.json",
@@ -591,14 +579,13 @@ var skill_schema_default2 = {
591
579
  properties: {}
592
580
  };
593
581
 
594
- // built-in-plugins/providers/gemini/index.ts
582
+ // plugins/gemini/providers/gemini/index.ts
595
583
  var geminiProvider = {
596
584
  id: "gemini",
597
585
  pluginId: "gemini",
598
586
  kind: "provider",
599
587
  version: "1.0.0",
600
588
  description: "Walks Gemini CLI scope conventions (.gemini/{agents,skills}).",
601
- stability: "stable",
602
589
  read: { extensions: [".md"], parser: "frontmatter-yaml" },
603
590
  // Per spec § A.6, defaultRefreshAction values MUST be qualified
604
591
  // action ids. The summarize-* actions are not yet implemented as
@@ -616,7 +603,6 @@ var geminiProvider = {
616
603
  agent: {
617
604
  schema: "./schemas/agent.schema.json",
618
605
  schemaJson: agent_schema_default2,
619
- defaultRefreshAction: "gemini/summarize-agent",
620
606
  ui: {
621
607
  label: "Agents",
622
608
  color: "#3b82f6",
@@ -627,7 +613,6 @@ var geminiProvider = {
627
613
  skill: {
628
614
  schema: "./schemas/skill.schema.json",
629
615
  schemaJson: skill_schema_default2,
630
- defaultRefreshAction: "gemini/summarize-skill",
631
616
  ui: {
632
617
  label: "Skills",
633
618
  color: "#10b981",
@@ -644,7 +629,7 @@ var geminiProvider = {
644
629
  }
645
630
  };
646
631
 
647
- // built-in-plugins/providers/agent-skills/schemas/skill.schema.json
632
+ // plugins/agent-skills/providers/agent-skills/schemas/skill.schema.json
648
633
  var skill_schema_default3 = {
649
634
  $schema: "https://json-schema.org/draft/2020-12/schema",
650
635
  $id: "https://skill-map.dev/providers/agent-skills/v1/frontmatter/skill.schema.json",
@@ -658,20 +643,18 @@ var skill_schema_default3 = {
658
643
  properties: {}
659
644
  };
660
645
 
661
- // built-in-plugins/providers/agent-skills/index.ts
646
+ // plugins/agent-skills/providers/agent-skills/index.ts
662
647
  var agentSkillsProvider = {
663
648
  id: "agent-skills",
664
649
  pluginId: "agent-skills",
665
650
  kind: "provider",
666
651
  version: "1.0.0",
667
652
  description: "Agent Skills open standard. Vendor-neutral path `.agents/skills/<name>/SKILL.md` (Anthropic, OpenAI, Google). See agentskills.io.",
668
- stability: "stable",
669
653
  read: { extensions: [".md"], parser: "frontmatter-yaml" },
670
654
  kinds: {
671
655
  skill: {
672
656
  schema: "./schemas/skill.schema.json",
673
657
  schemaJson: skill_schema_default3,
674
- defaultRefreshAction: "agent-skills/summarize-skill",
675
658
  ui: {
676
659
  label: "Skills",
677
660
  color: "#10b981",
@@ -686,7 +669,7 @@ var agentSkillsProvider = {
686
669
  }
687
670
  };
688
671
 
689
- // built-in-plugins/providers/core-markdown/schemas/markdown.schema.json
672
+ // plugins/core/providers/core-markdown/schemas/markdown.schema.json
690
673
  var markdown_schema_default = {
691
674
  $schema: "https://json-schema.org/draft/2020-12/schema",
692
675
  $id: "https://skill-map.dev/providers/core/v1/frontmatter/markdown.schema.json",
@@ -699,14 +682,13 @@ var markdown_schema_default = {
699
682
  additionalProperties: true
700
683
  };
701
684
 
702
- // built-in-plugins/providers/core-markdown/index.ts
685
+ // plugins/core/providers/core-markdown/index.ts
703
686
  var coreMarkdownProvider = {
704
687
  id: "markdown",
705
688
  pluginId: "core",
706
689
  kind: "provider",
707
690
  version: "1.0.0",
708
691
  description: "Universal `.md` fallback. Claims any markdown file no vendor-specific Provider classifies.",
709
- stability: "stable",
710
692
  read: { extensions: [".md"], parser: "frontmatter-yaml" },
711
693
  // Per spec § A.6, defaultRefreshAction values MUST be qualified
712
694
  // action ids. The summarize-markdown action is not yet implemented
@@ -723,7 +705,6 @@ var coreMarkdownProvider = {
723
705
  markdown: {
724
706
  schema: "./schemas/markdown.schema.json",
725
707
  schemaJson: markdown_schema_default,
726
- defaultRefreshAction: "core/summarize-markdown",
727
708
  ui: {
728
709
  label: "Markdown",
729
710
  color: "#5b908c",
@@ -740,7 +721,7 @@ var coreMarkdownProvider = {
740
721
  }
741
722
  };
742
723
 
743
- // built-in-plugins/extractors/annotations/index.ts
724
+ // plugins/core/extractors/annotations/index.ts
744
725
  var ID = "annotations";
745
726
  var annotationsExtractor = {
746
727
  id: ID,
@@ -748,9 +729,6 @@ var annotationsExtractor = {
748
729
  kind: "extractor",
749
730
  version: "1.0.0",
750
731
  description: "Turns the `supersedes` and `supersededBy` entries you write in a node's `.sm` sidecar into the arrows (edges) shown between nodes in the graph.",
751
- stability: "stable",
752
- emitsLinkKinds: ["supersedes"],
753
- defaultConfidence: "high",
754
732
  scope: "frontmatter",
755
733
  extract(ctx) {
756
734
  const sourcePath = ctx.node.path;
@@ -807,53 +785,15 @@ function normalizeTrigger(source) {
807
785
  return out.trim();
808
786
  }
809
787
 
810
- // built-in-plugins/extractors/slash/index.ts
811
- var ID2 = "slash";
812
- var SLASH_RE = /(?<![A-Za-z0-9_/.:?#])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
813
- var slashExtractor = {
814
- id: ID2,
815
- pluginId: "core",
816
- kind: "extractor",
817
- version: "1.0.0",
818
- description: "Detects `/command` invocations in a node's body and turns each one into an arrow between nodes in the graph.",
819
- stability: "stable",
820
- emitsLinkKinds: ["invokes"],
821
- defaultConfidence: "medium",
822
- scope: "body",
823
- extract(ctx) {
824
- const seen = /* @__PURE__ */ new Set();
825
- for (const match of ctx.body.matchAll(SLASH_RE)) {
826
- const original = match[1];
827
- const normalized = normalizeTrigger(original);
828
- if (seen.has(normalized)) continue;
829
- seen.add(normalized);
830
- ctx.emitLink({
831
- source: ctx.node.path,
832
- target: original,
833
- kind: "invokes",
834
- confidence: "medium",
835
- sources: [ID2],
836
- trigger: {
837
- originalTrigger: original,
838
- normalizedTrigger: normalized
839
- }
840
- });
841
- }
842
- }
843
- };
844
-
845
- // built-in-plugins/extractors/at-directive/index.ts
846
- var ID3 = "at-directive";
788
+ // plugins/core/extractors/at-directive/index.ts
789
+ var ID2 = "at-directive";
847
790
  var AT_RE = /(?:^|[^A-Za-z0-9_@])(@[a-z0-9][a-z0-9_-]*(?:[/:][a-z0-9][a-z0-9_-]*)?)/gi;
848
791
  var atDirectiveExtractor = {
849
- id: ID3,
792
+ id: ID2,
850
793
  pluginId: "core",
851
794
  kind: "extractor",
852
795
  version: "1.0.0",
853
796
  description: "Detects `@agent-name` mentions in a node's body and turns each one into an arrow between nodes in the graph.",
854
- stability: "stable",
855
- emitsLinkKinds: ["mentions"],
856
- defaultConfidence: "medium",
857
797
  scope: "body",
858
798
  extract(ctx) {
859
799
  const seen = /* @__PURE__ */ new Set();
@@ -867,7 +807,7 @@ var atDirectiveExtractor = {
867
807
  target: original,
868
808
  kind: "mentions",
869
809
  confidence: "medium",
870
- sources: [ID3],
810
+ sources: [ID2],
871
811
  trigger: {
872
812
  originalTrigger: original,
873
813
  normalizedTrigger: normalized
@@ -877,19 +817,16 @@ var atDirectiveExtractor = {
877
817
  }
878
818
  };
879
819
 
880
- // built-in-plugins/extractors/external-url-counter/index.ts
881
- var ID4 = "external-url-counter";
820
+ // plugins/core/extractors/external-url-counter/index.ts
821
+ var ID3 = "external-url-counter";
882
822
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
883
823
  var TRAILING_PUNCT = /[.,;:!?]+$/;
884
824
  var externalUrlCounterExtractor = {
885
- id: ID4,
825
+ id: ID3,
886
826
  pluginId: "core",
887
827
  kind: "extractor",
888
828
  version: "1.0.0",
889
829
  description: "Counts the distinct external URLs in a node's body and shows the total on the card.",
890
- stability: "stable",
891
- emitsLinkKinds: ["references"],
892
- defaultConfidence: "low",
893
830
  scope: "body",
894
831
  /**
895
832
  * Phase 6 / View contribution system, surface the distinct-URL
@@ -907,7 +844,7 @@ var externalUrlCounterExtractor = {
907
844
  * inherited from the footer `.sm-gnode__stat` styles cloned by
908
845
  * the `NodeCounter` renderer.
909
846
  */
910
- viewContributions: {
847
+ ui: {
911
848
  count: {
912
849
  slot: "card.footer.left",
913
850
  icon: "pi-link",
@@ -932,7 +869,7 @@ var externalUrlCounterExtractor = {
932
869
  target: normalized,
933
870
  kind: "references",
934
871
  confidence: "low",
935
- sources: [ID4],
872
+ sources: [ID3],
936
873
  trigger: {
937
874
  originalTrigger: original,
938
875
  normalizedTrigger: normalized
@@ -977,20 +914,17 @@ function lineFor(lineStarts, offset) {
977
914
  return lo + 1;
978
915
  }
979
916
 
980
- // built-in-plugins/extractors/markdown-link/index.ts
917
+ // plugins/core/extractors/markdown-link/index.ts
981
918
  import { posix as pathPosix } from "path";
982
- var ID5 = "markdown-link";
919
+ var ID4 = "markdown-link";
983
920
  var LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
984
921
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
985
922
  var markdownLinkExtractor = {
986
- id: ID5,
923
+ id: ID4,
987
924
  pluginId: "core",
988
925
  kind: "extractor",
989
926
  version: "1.0.0",
990
927
  description: "Detects markdown links (`[text](path)`) in a node's body and turns each one into an arrow between nodes in the graph.",
991
- stability: "stable",
992
- emitsLinkKinds: ["references"],
993
- defaultConfidence: "high",
994
928
  scope: "body",
995
929
  extract(ctx) {
996
930
  const seen = /* @__PURE__ */ new Set();
@@ -1008,7 +942,7 @@ var markdownLinkExtractor = {
1008
942
  target: resolved,
1009
943
  kind: "references",
1010
944
  confidence: "high",
1011
- sources: [ID5],
945
+ sources: [ID4],
1012
946
  trigger: {
1013
947
  originalTrigger: original,
1014
948
  normalizedTrigger: resolved
@@ -1047,99 +981,50 @@ function lineFor2(lineStarts, offset) {
1047
981
  return lo + 1;
1048
982
  }
1049
983
 
1050
- // built-in-plugins/analyzers/stability/index.ts
1051
- var ID6 = "stability";
1052
- var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
1053
- var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
1054
- var stabilityAnalyzer = {
1055
- id: ID6,
984
+ // plugins/core/extractors/slash/index.ts
985
+ var ID5 = "slash";
986
+ var SLASH_RE = /(?<![A-Za-z0-9_/.:?#])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
987
+ var slashExtractor = {
988
+ id: ID5,
1056
989
  pluginId: "core",
1057
- kind: "analyzer",
990
+ kind: "extractor",
1058
991
  version: "1.0.0",
1059
- description: "Reports node lifecycle stage (`experimental`, `deprecated`) on the card.",
1060
- stability: "stable",
1061
- mode: "deterministic",
1062
- viewContributions: {
1063
- experimental: {
1064
- slot: "card.footer.right",
1065
- icon: "fa-solid fa-flask",
1066
- label: "experimental",
1067
- emitWhenEmpty: false,
1068
- priority: 10
1069
- },
1070
- deprecated: {
1071
- slot: "card.footer.right",
1072
- icon: "pi-ban",
1073
- label: "deprecated",
1074
- emitWhenEmpty: false,
1075
- priority: 10
1076
- }
1077
- },
1078
- evaluate(ctx) {
1079
- const issues = [];
1080
- for (const node of ctx.nodes) {
1081
- const stability = readStability(node);
1082
- if (stability === "experimental") {
1083
- ctx.emitContribution(node.path, "experimental", {
1084
- value: 0,
1085
- tooltip: EXPERIMENTAL_TOOLTIP
1086
- });
1087
- issues.push({
1088
- analyzerId: ID6,
1089
- severity: "info",
1090
- nodeIds: [node.path],
1091
- message: `Node '${node.path}' is marked experimental: API may change.`,
1092
- data: { stability }
1093
- });
1094
- } else if (stability === "deprecated") {
1095
- ctx.emitContribution(node.path, "deprecated", {
1096
- value: 0,
1097
- tooltip: DEPRECATED_TOOLTIP,
1098
- severity: "warn"
1099
- });
1100
- issues.push({
1101
- analyzerId: ID6,
1102
- severity: "warn",
1103
- nodeIds: [node.path],
1104
- message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
1105
- data: { stability }
1106
- });
1107
- }
992
+ description: "Detects `/command` invocations in a node's body and turns each one into an arrow between nodes in the graph.",
993
+ scope: "body",
994
+ extract(ctx) {
995
+ const seen = /* @__PURE__ */ new Set();
996
+ for (const match of ctx.body.matchAll(SLASH_RE)) {
997
+ const original = match[1];
998
+ const normalized = normalizeTrigger(original);
999
+ if (seen.has(normalized)) continue;
1000
+ seen.add(normalized);
1001
+ ctx.emitLink({
1002
+ source: ctx.node.path,
1003
+ target: original,
1004
+ kind: "invokes",
1005
+ confidence: "medium",
1006
+ sources: [ID5],
1007
+ trigger: {
1008
+ originalTrigger: original,
1009
+ normalizedTrigger: normalized
1010
+ }
1011
+ });
1108
1012
  }
1109
- return issues;
1110
1013
  }
1111
1014
  };
1112
- function readStability(node) {
1113
- const fromAnn = node.sidecar?.annotations?.["stability"];
1114
- if (isStability(fromAnn)) return fromAnn;
1115
- const legacy = readLegacyMetadataStability(node.frontmatter);
1116
- return isStability(legacy) ? legacy : null;
1117
- }
1118
- function readLegacyMetadataStability(fm) {
1119
- if (!fm) return void 0;
1120
- const meta = fm["metadata"];
1121
- if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
1122
- return meta["stability"];
1123
- }
1124
- function isStability(value) {
1125
- return value === "experimental" || value === "deprecated" || value === "stable";
1126
- }
1127
1015
 
1128
- // built-in-plugins/extractors/tools-count/index.ts
1129
- var ID7 = "tools-count";
1016
+ // plugins/core/extractors/tools-count/index.ts
1017
+ var ID6 = "tools-count";
1130
1018
  var TOOLTIP_MAX = 255;
1131
1019
  var toolsCountExtractor = {
1132
- id: ID7,
1020
+ id: ID6,
1133
1021
  pluginId: "core",
1134
1022
  kind: "extractor",
1135
1023
  version: "1.0.0",
1136
1024
  description: "Counts the tools an agent declares in its frontmatter and shows the total on the agent card.",
1137
- stability: "stable",
1138
- emitsLinkKinds: [],
1139
- defaultConfidence: "high",
1140
1025
  scope: "frontmatter",
1141
- applicableKinds: ["agent"],
1142
- viewContributions: {
1026
+ precondition: { kind: ["claude/agent"] },
1027
+ ui: {
1143
1028
  count: {
1144
1029
  slot: "card.footer.left",
1145
1030
  icon: "pi-wrench",
@@ -1168,155 +1053,132 @@ function buildTooltip(names) {
1168
1053
  return `${joined.slice(0, TOOLTIP_MAX - 1)}\u2026`;
1169
1054
  }
1170
1055
 
1171
- // built-in-plugins/i18n/trigger-collision.texts.ts
1172
- var TRIGGER_COLLISION_TEXTS = {
1173
- /**
1174
- * Top-level message when `analyzeTriggerBucket` accumulated exactly one
1175
- * cause part. Used for the advertiser-ambiguous-only, invocation-
1176
- * ambiguous-only, and cross-kind-only branches.
1177
- */
1178
- messageOnePart: 'Trigger "{{normalized}}" has {{part}}.',
1179
- /**
1180
- * Top-level message when `analyzeTriggerBucket` accumulated two cause
1181
- * parts (advertiser-ambiguous AND invocation-ambiguous fire together).
1182
- * The joiner lives inside the template so future locales can adapt it
1183
- * (e.g. `'; y '` in Spanish) without touching the rule code.
1184
- */
1185
- messageTwoParts: 'Trigger "{{normalized}}" has {{first}}; and {{second}}.',
1186
- /** `<n> nodes advertise it: <list>` part, fires on the advertiser-ambiguous branch. */
1187
- partAdvertisers: "{{count}} nodes advertise it: {{paths}}",
1188
- /** `<n> distinct invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
1189
- partInvocations: "{{count}} distinct invocation forms: {{forms}}",
1190
- /** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
1191
- partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
1192
- /** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
1193
- partNonCanonicalPlural: "non-canonical invocations {{forms}} against advertiser {{advertiser}}"
1056
+ // plugins/core/analyzers/annotation-orphan/text.ts
1057
+ var ANNOTATION_ORPHAN_TEXTS = {
1058
+ /** Sidecar `<path>.sm` has no matching `<path>.md`. */
1059
+ message: "Orphan sidecar: {{sidecarPath}} has no matching markdown node at {{expectedMdPath}}."
1194
1060
  };
1195
1061
 
1196
- // built-in-plugins/analyzers/trigger-collision/index.ts
1197
- var ID8 = "trigger-collision";
1198
- var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
1199
- "command",
1200
- "skill",
1201
- "agent"
1202
- ]);
1203
- var triggerCollisionAnalyzer = {
1204
- id: ID8,
1062
+ // plugins/core/analyzers/annotation-orphan/index.ts
1063
+ var ID7 = "annotation-orphan";
1064
+ var annotationOrphanAnalyzer = {
1065
+ id: ID7,
1205
1066
  pluginId: "core",
1206
1067
  kind: "analyzer",
1207
- mode: "deterministic",
1208
1068
  version: "1.0.0",
1209
- description: "Detects and flags two or more nodes claiming the same `/command` or `@agent` name.",
1210
- stability: "stable",
1211
- // Two claim-collection passes (advertisement + invocation) feeding
1212
- // the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
1213
- // eslint-disable-next-line complexity
1069
+ description: "Detects and flags sidecars (`.sm`) whose `.md` no longer exists.",
1070
+ mode: "deterministic",
1214
1071
  evaluate(ctx) {
1215
- const buckets = /* @__PURE__ */ new Map();
1216
- const push = (key, claim) => {
1217
- const bucket = buckets.get(key) ?? [];
1218
- bucket.push(claim);
1219
- buckets.set(key, bucket);
1220
- };
1221
- for (const node of ctx.nodes) {
1222
- if (!ADVERTISING_KINDS.has(node.kind)) continue;
1223
- const raw = node.frontmatter?.["name"];
1224
- if (typeof raw !== "string" || raw.length === 0) continue;
1225
- const normalized = `/${normalizeTrigger(raw)}`;
1226
- if (normalized === "/") continue;
1227
- push(normalized, {
1228
- kind: "advertiser",
1229
- token: node.path,
1230
- nodeId: node.path,
1231
- canonicalForm: `/${raw}`
1232
- });
1233
- }
1234
- for (const link2 of ctx.links) {
1235
- const normalized = link2.trigger?.normalizedTrigger;
1236
- if (!normalized) continue;
1237
- push(normalized, {
1238
- kind: "invocation",
1239
- token: link2.target,
1240
- nodeId: link2.source
1241
- });
1242
- }
1072
+ const orphans = ctx.orphanSidecars;
1073
+ if (!orphans || orphans.length === 0) return [];
1243
1074
  const issues = [];
1244
- for (const [normalized, claims] of buckets) {
1245
- const issue = analyzeTriggerBucket(normalized, claims);
1246
- if (issue) issues.push(issue);
1075
+ for (const orphan of orphans) {
1076
+ const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
1077
+ issues.push({
1078
+ analyzerId: ID7,
1079
+ severity: "warn",
1080
+ nodeIds: [expectedMdRelative],
1081
+ message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
1082
+ sidecarPath: orphan.relativePath,
1083
+ expectedMdPath: orphan.expectedMdPath
1084
+ }),
1085
+ data: {
1086
+ sidecarPath: orphan.relativePath,
1087
+ expectedMdPath: orphan.expectedMdPath
1088
+ }
1089
+ });
1247
1090
  }
1248
1091
  return issues;
1249
1092
  }
1250
1093
  };
1251
- function analyzeTriggerBucket(normalized, claims) {
1252
- const advertiserPaths = [
1253
- ...new Set(claims.filter((c) => c.kind === "advertiser").map((c) => c.token))
1254
- ].sort();
1255
- const invocationTargets = [
1256
- ...new Set(claims.filter((c) => c.kind === "invocation").map((c) => c.token))
1257
- ].sort();
1258
- const advertisers = claims.filter(
1259
- (c) => c.kind === "advertiser"
1260
- );
1261
- const advertiserAmbiguous = advertiserPaths.length >= 2;
1262
- const invocationAmbiguous = invocationTargets.length >= 2;
1263
- const canonicalForms = new Set(advertisers.map((a) => a.canonicalForm));
1264
- const nonCanonicalInvocations = invocationTargets.filter((t) => !canonicalForms.has(t));
1265
- const crossKindAmbiguous = advertiserPaths.length === 1 && nonCanonicalInvocations.length >= 1;
1266
- if (!advertiserAmbiguous && !invocationAmbiguous && !crossKindAmbiguous) {
1267
- return null;
1268
- }
1269
- const nodeIds = [...new Set(claims.map((c) => c.nodeId))].sort();
1270
- const parts = [];
1271
- if (advertiserAmbiguous) {
1272
- parts.push(
1273
- tx(TRIGGER_COLLISION_TEXTS.partAdvertisers, {
1274
- count: advertiserPaths.length,
1275
- paths: advertiserPaths.join(", ")
1276
- })
1277
- );
1094
+
1095
+ // plugins/core/analyzers/annotation-stale/text.ts
1096
+ var ANNOTATION_STALE_TEXTS = {
1097
+ /** body changed since last bump */
1098
+ bodyDrift: "{{path}}: sidecar `.sm` is stale (body changed since last bump).",
1099
+ /** frontmatter changed since last bump */
1100
+ frontmatterDrift: "{{path}}: sidecar `.sm` is stale (frontmatter changed since last bump).",
1101
+ /** both body and frontmatter changed */
1102
+ bothDrift: "{{path}}: sidecar `.sm` is stale (body and frontmatter changed since last bump).",
1103
+ // Tooltips for the `card.footer.right` clock chip emitted alongside
1104
+ // the issue. Lists only the drifted face(s), in-sync faces are
1105
+ // omitted so the operator immediately sees what's modified without
1106
+ // scanning prose. No `{{path}}` placeholder, the chip already sits
1107
+ // on the affected node. The hint `sm bump <path>` keeps `<path>` as
1108
+ // a literal placeholder the operator substitutes.
1109
+ bodyTooltip: "Sidecar drift since last bump:\n \u2022 body\nRun `sm bump <path>` to refresh.",
1110
+ frontmatterTooltip: "Sidecar drift since last bump:\n \u2022 frontmatter\nRun `sm bump <path>` to refresh.",
1111
+ bothTooltip: "Sidecar drift since last bump:\n \u2022 body\n \u2022 frontmatter\nRun `sm bump <path>` to refresh."
1112
+ };
1113
+
1114
+ // plugins/core/analyzers/annotation-stale/index.ts
1115
+ var ID8 = "annotation-stale";
1116
+ var annotationStaleAnalyzer = {
1117
+ id: ID8,
1118
+ pluginId: "core",
1119
+ kind: "analyzer",
1120
+ version: "1.0.0",
1121
+ description: "Detects and marks sidecars (`.sm`) out of date of their `.md`.",
1122
+ mode: "deterministic",
1123
+ // The natural fix is to bump the node: refreshes `for` hashes,
1124
+ // increments `annotations.version`, and stamps the audit block. The
1125
+ // UI surfaces `core/bump` in the node inspector under "Recommended
1126
+ // for issues" whenever this analyzer fires.
1127
+ ui: {
1128
+ // A `pi-clock` chip in the footer-right cluster so the operator
1129
+ // spots drift in the list / inspector view (and on the graph card
1130
+ // body). Emitted with `value: 0` and `emitWhenEmpty: true` so the
1131
+ // renderer treats it as icon-only, drift severity is binary at
1132
+ // this surface (the tooltip carries the per-face detail body /
1133
+ // frontmatter / both). The corner badge on `graph.node.alert` was
1134
+ // dropped on purpose: a tooltip on the footer chip is enough, and
1135
+ // the corner badge stacked on top of broken-ref / unknown-field
1136
+ // alerts produced visual noise.
1137
+ staleIcon: {
1138
+ slot: "card.footer.right",
1139
+ icon: "pi-clock",
1140
+ emitWhenEmpty: true,
1141
+ priority: 20
1142
+ }
1143
+ },
1144
+ evaluate(ctx) {
1145
+ const issues = [];
1146
+ for (const node of ctx.nodes) {
1147
+ const status = node.sidecar?.status;
1148
+ if (status === void 0 || status === null) continue;
1149
+ if (status === "fresh") continue;
1150
+ 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 });
1151
+ issues.push({
1152
+ analyzerId: ID8,
1153
+ severity: "warn",
1154
+ nodeIds: [node.path],
1155
+ message,
1156
+ data: { status }
1157
+ });
1158
+ ctx.emitContribution(node.path, "staleIcon", {
1159
+ value: 0,
1160
+ severity: "warn",
1161
+ tooltip: tooltipFor(status)
1162
+ });
1163
+ }
1164
+ return issues;
1278
1165
  }
1279
- if (invocationAmbiguous) {
1280
- parts.push(
1281
- tx(TRIGGER_COLLISION_TEXTS.partInvocations, {
1282
- count: invocationTargets.length,
1283
- forms: invocationTargets.join(", ")
1284
- })
1285
- );
1286
- } else if (crossKindAmbiguous) {
1287
- const template = nonCanonicalInvocations.length > 1 ? TRIGGER_COLLISION_TEXTS.partNonCanonicalPlural : TRIGGER_COLLISION_TEXTS.partNonCanonicalSingular;
1288
- parts.push(
1289
- tx(template, {
1290
- forms: nonCanonicalInvocations.join(", "),
1291
- advertiser: advertiserPaths[0]
1292
- })
1293
- );
1166
+ };
1167
+ function tooltipFor(status) {
1168
+ switch (status) {
1169
+ case "stale-body":
1170
+ return ANNOTATION_STALE_TEXTS.bodyTooltip;
1171
+ case "stale-frontmatter":
1172
+ return ANNOTATION_STALE_TEXTS.frontmatterTooltip;
1173
+ case "stale-both":
1174
+ return ANNOTATION_STALE_TEXTS.bothTooltip;
1294
1175
  }
1295
- const message = parts.length === 2 ? tx(TRIGGER_COLLISION_TEXTS.messageTwoParts, {
1296
- normalized,
1297
- first: parts[0],
1298
- second: parts[1]
1299
- }) : tx(TRIGGER_COLLISION_TEXTS.messageOnePart, {
1300
- normalized,
1301
- part: parts[0]
1302
- });
1303
- return {
1304
- analyzerId: ID8,
1305
- severity: "error",
1306
- nodeIds,
1307
- message,
1308
- data: {
1309
- normalizedTrigger: normalized,
1310
- invocationTargets,
1311
- advertiserPaths
1312
- }
1313
- };
1314
1176
  }
1315
1177
 
1316
- // built-in-plugins/analyzers/broken-ref/index.ts
1178
+ // plugins/core/analyzers/broken-ref/index.ts
1317
1179
  import { resolve } from "path";
1318
1180
 
1319
- // built-in-plugins/i18n/broken-ref.texts.ts
1181
+ // plugins/core/analyzers/broken-ref/text.ts
1320
1182
  var BROKEN_REF_TEXTS = {
1321
1183
  /** `Broken <kind> reference from <source> → <target>` */
1322
1184
  message: "Broken {{kind}} reference from {{source}} \u2192 {{target}}",
@@ -1326,7 +1188,7 @@ var BROKEN_REF_TEXTS = {
1326
1188
  alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details."
1327
1189
  };
1328
1190
 
1329
- // built-in-plugins/analyzers/broken-ref/index.ts
1191
+ // plugins/core/analyzers/broken-ref/index.ts
1330
1192
  var ID9 = "broken-ref";
1331
1193
  var brokenRefAnalyzer = {
1332
1194
  id: ID9,
@@ -1334,9 +1196,8 @@ var brokenRefAnalyzer = {
1334
1196
  kind: "analyzer",
1335
1197
  version: "1.0.0",
1336
1198
  description: "Detects and flags arrows pointing at a node not part of the current scan.",
1337
- stability: "stable",
1338
1199
  mode: "deterministic",
1339
- viewContributions: {
1200
+ ui: {
1340
1201
  // Corner badge on the graph card; count omitted when there is a
1341
1202
  // single broken ref (avoids a noisy "icon + 1" chip).
1342
1203
  alert: {
@@ -1439,66 +1300,69 @@ function isPathStyleLink(link2) {
1439
1300
  return true;
1440
1301
  }
1441
1302
 
1442
- // built-in-plugins/i18n/superseded.texts.ts
1443
- var SUPERSEDED_TEXTS = {
1444
- /** `<path> is superseded by <supersededBy>` */
1445
- message: "{{path}} is superseded by {{supersededBy}}"
1303
+ // plugins/core/analyzers/contribution-orphan/index.ts
1304
+ var ID10 = "contribution-orphan";
1305
+ var contributionOrphanAnalyzer = {
1306
+ id: ID10,
1307
+ pluginId: "core",
1308
+ kind: "analyzer",
1309
+ version: "0.0.0",
1310
+ description: "Detects and warns about plugin data referencing nodes renamed or deleted in the latest scan.",
1311
+ mode: "deterministic",
1312
+ evaluate(_ctx) {
1313
+ return [];
1314
+ }
1446
1315
  };
1447
1316
 
1448
- // built-in-plugins/analyzers/superseded/index.ts
1449
- var ID10 = "superseded";
1450
- var supersededAnalyzer = {
1451
- id: ID10,
1317
+ // plugins/core/analyzers/job-orphan-file/text.ts
1318
+ var JOB_ORPHAN_FILE_TEXTS = {
1319
+ /**
1320
+ * `<path>.md` lives under `.skill-map/jobs/` but no `state_jobs.filePath`
1321
+ * row references it. Run `sm job prune --orphan-files` to remove.
1322
+ */
1323
+ message: "Orphan job file: {{filePath}} is not referenced by any state_jobs row. Run `sm job prune --orphan-files` to remove it."
1324
+ };
1325
+
1326
+ // plugins/core/analyzers/job-orphan-file/index.ts
1327
+ var ID11 = "job-orphan-file";
1328
+ var jobOrphanFileAnalyzer = {
1329
+ id: ID11,
1452
1330
  pluginId: "core",
1453
1331
  kind: "analyzer",
1454
1332
  version: "1.0.0",
1455
- description: "Detects and marks nodes replaced by a newer one via `supersededBy`.",
1456
- stability: "stable",
1333
+ description: "Detects and flags leftover job result files (no live job references them). Cleanup via `sm job prune --orphan-files`.",
1457
1334
  mode: "deterministic",
1458
1335
  evaluate(ctx) {
1336
+ const orphans = ctx.orphanJobFiles;
1337
+ if (!orphans || orphans.length === 0) return [];
1459
1338
  const issues = [];
1460
- for (const node of ctx.nodes) {
1461
- const supersededBy = pickSupersededBy(node);
1462
- if (supersededBy === null) continue;
1339
+ for (const filePath of orphans) {
1463
1340
  issues.push({
1464
- analyzerId: ID10,
1465
- severity: "info",
1466
- nodeIds: [node.path],
1467
- message: tx(SUPERSEDED_TEXTS.message, {
1468
- path: node.path,
1469
- supersededBy
1470
- }),
1471
- data: { supersededBy }
1341
+ analyzerId: ID11,
1342
+ severity: "warn",
1343
+ nodeIds: [filePath],
1344
+ message: tx(JOB_ORPHAN_FILE_TEXTS.message, { filePath }),
1345
+ data: { filePath }
1472
1346
  });
1473
1347
  }
1474
1348
  return issues;
1475
1349
  }
1476
1350
  };
1477
- function pickSupersededBy(node) {
1478
- const sidecar = node.sidecar;
1479
- if (!sidecar || sidecar.present !== true) return null;
1480
- const ann = sidecar.annotations;
1481
- if (!ann || typeof ann !== "object" || Array.isArray(ann)) return null;
1482
- const value = ann["supersededBy"];
1483
- if (typeof value !== "string" || value.length === 0) return null;
1484
- return value;
1485
- }
1486
1351
 
1487
- // built-in-plugins/i18n/link-conflict.texts.ts
1352
+ // plugins/core/analyzers/link-conflict/text.ts
1488
1353
  var LINK_CONFLICT_TEXTS = {
1489
1354
  /** `Detectors disagree on link kind for <source> → <target> (<kindList>)` */
1490
1355
  message: "Detectors disagree on link kind for {{source}} \u2192 {{target}} ({{kindList}})"
1491
1356
  };
1492
1357
 
1493
- // built-in-plugins/analyzers/link-conflict/index.ts
1494
- var ID11 = "link-conflict";
1358
+ // plugins/core/analyzers/link-conflict/index.ts
1359
+ var ID12 = "link-conflict";
1495
1360
  var linkConflictAnalyzer = {
1496
- id: ID11,
1361
+ id: ID12,
1497
1362
  pluginId: "core",
1498
1363
  kind: "analyzer",
1499
1364
  version: "1.0.0",
1500
1365
  description: 'Detects and flags conflicting arrow meanings between extractors (e.g. "references" vs "invokes").',
1501
- stability: "stable",
1502
1366
  mode: "deterministic",
1503
1367
  // Bucket links by (source, target), then per-bucket detect distinct
1504
1368
  // kinds. The branching is intrinsic to the per-bucket conflict
@@ -1542,7 +1406,7 @@ var linkConflictAnalyzer = {
1542
1406
  const [source, target] = key.split("\0");
1543
1407
  const kindList = variants.map((v) => v.kind).join(" / ");
1544
1408
  issues.push({
1545
- analyzerId: ID11,
1409
+ analyzerId: ID12,
1546
1410
  severity: "warn",
1547
1411
  nodeIds: [source, target],
1548
1412
  message: tx(LINK_CONFLICT_TEXTS.message, {
@@ -1567,168 +1431,386 @@ function rankConfidence(c) {
1567
1431
  }
1568
1432
  }
1569
1433
 
1570
- // built-in-plugins/i18n/annotation-stale.texts.ts
1571
- var ANNOTATION_STALE_TEXTS = {
1572
- /** body changed since last bump */
1573
- bodyDrift: "{{path}}: sidecar `.sm` is stale (body changed since last bump).",
1574
- /** frontmatter changed since last bump */
1575
- frontmatterDrift: "{{path}}: sidecar `.sm` is stale (frontmatter changed since last bump).",
1576
- /** both body and frontmatter changed */
1577
- bothDrift: "{{path}}: sidecar `.sm` is stale (body and frontmatter changed since last bump).",
1578
- // Tooltips for the `card.footer.right` clock chip emitted alongside
1579
- // the issue. Lists only the drifted face(s), in-sync faces are
1580
- // omitted so the operator immediately sees what's modified without
1581
- // scanning prose. No `{{path}}` placeholder, the chip already sits
1582
- // on the affected node. The hint `sm bump <path>` keeps `<path>` as
1583
- // a literal placeholder the operator substitutes.
1584
- bodyTooltip: "Sidecar drift since last bump:\n \u2022 body\nRun `sm bump <path>` to refresh.",
1585
- frontmatterTooltip: "Sidecar drift since last bump:\n \u2022 frontmatter\nRun `sm bump <path>` to refresh.",
1586
- bothTooltip: "Sidecar drift since last bump:\n \u2022 body\n \u2022 frontmatter\nRun `sm bump <path>` to refresh."
1587
- };
1434
+ // kernel/util/trigger-resolve.ts
1435
+ function buildNameIndex(nodes) {
1436
+ const out = /* @__PURE__ */ new Map();
1437
+ indexByCanonicalName(nodes, out);
1438
+ fillIndexWithPathBasename(nodes, out);
1439
+ return out;
1440
+ }
1441
+ function indexByCanonicalName(nodes, out) {
1442
+ for (const node of nodes) {
1443
+ const raw = canonicalName(node);
1444
+ if (raw === null) continue;
1445
+ const key = normalizeTrigger(raw);
1446
+ if (!out.has(key)) out.set(key, node.path);
1447
+ }
1448
+ }
1449
+ function fillIndexWithPathBasename(nodes, out) {
1450
+ for (const node of nodes) {
1451
+ if (canonicalName(node) !== null) continue;
1452
+ const derived = pathBasenameForLink(node.path);
1453
+ if (derived.length === 0) continue;
1454
+ const key = normalizeTrigger(derived);
1455
+ if (!out.has(key)) out.set(key, node.path);
1456
+ }
1457
+ }
1458
+ function canonicalName(node) {
1459
+ const raw = node.frontmatter?.["name"];
1460
+ if (typeof raw !== "string" || raw.length === 0) return null;
1461
+ return raw;
1462
+ }
1463
+ function pathBasenameForLink(path) {
1464
+ const segments = path.split("/").filter((s) => s.length > 0);
1465
+ if (segments.length === 0) return path;
1466
+ const last = segments[segments.length - 1];
1467
+ if (last === "SKILL.md" && segments.length >= 2) {
1468
+ return segments[segments.length - 2];
1469
+ }
1470
+ return last.replace(/\.md$/, "");
1471
+ }
1472
+ function resolveLinkTargetToPath(link2, nameIndex) {
1473
+ const raw = link2.target;
1474
+ const sigil = raw.charAt(0);
1475
+ if (sigil !== "/" && sigil !== "@") return raw;
1476
+ const normalizedTrigger = link2.trigger?.normalizedTrigger;
1477
+ const normalized = typeof normalizedTrigger === "string" ? normalizedTrigger.replace(/^[/@]/, "").trim() : normalizeTrigger(raw.slice(1));
1478
+ const resolved = nameIndex.get(normalized);
1479
+ return resolved ?? raw;
1480
+ }
1588
1481
 
1589
- // built-in-plugins/analyzers/annotation-stale/index.ts
1590
- var ID12 = "annotation-stale";
1591
- var annotationStaleAnalyzer = {
1592
- id: ID12,
1482
+ // plugins/core/analyzers/link-counts/index.ts
1483
+ var ID13 = "link-counts";
1484
+ var linkCountsAnalyzer = {
1485
+ id: ID13,
1593
1486
  pluginId: "core",
1594
1487
  kind: "analyzer",
1595
1488
  version: "1.0.0",
1596
- description: "Detects and marks sidecars (`.sm`) out of date of their `.md`.",
1597
- stability: "stable",
1489
+ description: "Counts incoming and outgoing links per node.",
1598
1490
  mode: "deterministic",
1599
- // The natural fix is to bump the node: refreshes `for` hashes,
1600
- // increments `annotations.version`, and stamps the audit block. The
1601
- // UI surfaces `core/bump` in the node inspector under "Recommended
1602
- // for issues" whenever this analyzer fires.
1603
- recommendedActions: ["core/bump"],
1604
- viewContributions: {
1605
- // A `pi-clock` chip in the footer-right cluster so the operator
1606
- // spots drift in the list / inspector view (and on the graph card
1607
- // body). Emitted with `value: 0` and `emitWhenEmpty: true` so the
1608
- // renderer treats it as icon-only, drift severity is binary at
1609
- // this surface (the tooltip carries the per-face detail body /
1610
- // frontmatter / both). The corner badge on `graph.node.alert` was
1611
- // dropped on purpose: a tooltip on the footer chip is enough, and
1612
- // the corner badge stacked on top of broken-ref / unknown-field
1613
- // alerts produced visual noise.
1614
- staleIcon: {
1615
- slot: "card.footer.right",
1616
- icon: "pi-clock",
1617
- emitWhenEmpty: true,
1491
+ ui: {
1492
+ linksIn: {
1493
+ slot: "card.footer.left",
1494
+ icon: "pi-download",
1495
+ label: "incoming links",
1496
+ emitWhenEmpty: false,
1497
+ priority: 10
1498
+ },
1499
+ linksOut: {
1500
+ slot: "card.footer.left",
1501
+ icon: "pi-upload",
1502
+ label: "outgoing links",
1503
+ emitWhenEmpty: false,
1618
1504
  priority: 20
1619
1505
  }
1620
1506
  },
1507
+ evaluate(ctx) {
1508
+ const nameIndex = buildNameIndex(ctx.nodes);
1509
+ const perTarget = /* @__PURE__ */ new Map();
1510
+ const perSource = /* @__PURE__ */ new Map();
1511
+ for (const link2 of ctx.links) {
1512
+ const resolvedTarget = resolveLinkTargetToPath(link2, nameIndex);
1513
+ bump(perTarget, resolvedTarget, link2.kind);
1514
+ bump(perSource, link2.source, link2.kind);
1515
+ }
1516
+ for (const node of ctx.nodes) {
1517
+ emitChip(ctx, node.path, "linksIn", perTarget.get(node.path));
1518
+ emitChip(ctx, node.path, "linksOut", perSource.get(node.path));
1519
+ }
1520
+ return [];
1521
+ }
1522
+ };
1523
+ function bump(map, key, kind) {
1524
+ let byKind = map.get(key);
1525
+ if (!byKind) {
1526
+ byKind = /* @__PURE__ */ new Map();
1527
+ map.set(key, byKind);
1528
+ }
1529
+ byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
1530
+ }
1531
+ function emitChip(ctx, nodePath, contributionId, byKind) {
1532
+ if (!byKind) return;
1533
+ let total = 0;
1534
+ for (const n of byKind.values()) total += n;
1535
+ if (total === 0) return;
1536
+ const capped = Math.min(total, 99);
1537
+ const direction = contributionId === "linksIn" ? "in" : "out";
1538
+ ctx.emitContribution(nodePath, contributionId, {
1539
+ value: capped,
1540
+ tooltip: formatBreakdown(byKind, direction)
1541
+ });
1542
+ }
1543
+ function formatBreakdown(byKind, direction) {
1544
+ const lines = [...byKind.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([kind, n]) => `${kind}: ${n}`);
1545
+ return [direction, ...lines].join("\n");
1546
+ }
1547
+
1548
+ // plugins/core/analyzers/stability/index.ts
1549
+ var ID14 = "stability";
1550
+ var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
1551
+ var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
1552
+ var stabilityAnalyzer = {
1553
+ id: ID14,
1554
+ pluginId: "core",
1555
+ kind: "analyzer",
1556
+ version: "1.0.0",
1557
+ description: "Reports node lifecycle stage (`experimental`, `deprecated`) on the card.",
1558
+ mode: "deterministic",
1559
+ ui: {
1560
+ experimental: {
1561
+ slot: "card.footer.right",
1562
+ icon: "fa-solid fa-flask",
1563
+ label: "experimental",
1564
+ emitWhenEmpty: false,
1565
+ priority: 10
1566
+ },
1567
+ deprecated: {
1568
+ slot: "card.footer.right",
1569
+ icon: "pi-ban",
1570
+ label: "deprecated",
1571
+ emitWhenEmpty: false,
1572
+ priority: 10
1573
+ }
1574
+ },
1621
1575
  evaluate(ctx) {
1622
1576
  const issues = [];
1623
1577
  for (const node of ctx.nodes) {
1624
- const status = node.sidecar?.status;
1625
- if (status === void 0 || status === null) continue;
1626
- if (status === "fresh") continue;
1627
- 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 });
1628
- issues.push({
1629
- analyzerId: ID12,
1630
- severity: "warn",
1631
- nodeIds: [node.path],
1632
- message,
1633
- data: { status }
1634
- });
1635
- ctx.emitContribution(node.path, "staleIcon", {
1636
- value: 0,
1637
- severity: "warn",
1638
- tooltip: tooltipFor(status)
1639
- });
1578
+ const stability = readStability(node);
1579
+ if (stability === "experimental") {
1580
+ ctx.emitContribution(node.path, "experimental", {
1581
+ value: 0,
1582
+ tooltip: EXPERIMENTAL_TOOLTIP
1583
+ });
1584
+ issues.push({
1585
+ analyzerId: ID14,
1586
+ severity: "info",
1587
+ nodeIds: [node.path],
1588
+ message: `Node '${node.path}' is marked experimental: API may change.`,
1589
+ data: { stability }
1590
+ });
1591
+ } else if (stability === "deprecated") {
1592
+ ctx.emitContribution(node.path, "deprecated", {
1593
+ value: 0,
1594
+ tooltip: DEPRECATED_TOOLTIP,
1595
+ severity: "warn"
1596
+ });
1597
+ issues.push({
1598
+ analyzerId: ID14,
1599
+ severity: "warn",
1600
+ nodeIds: [node.path],
1601
+ message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
1602
+ data: { stability }
1603
+ });
1604
+ }
1640
1605
  }
1641
1606
  return issues;
1642
1607
  }
1643
1608
  };
1644
- function tooltipFor(status) {
1645
- switch (status) {
1646
- case "stale-body":
1647
- return ANNOTATION_STALE_TEXTS.bodyTooltip;
1648
- case "stale-frontmatter":
1649
- return ANNOTATION_STALE_TEXTS.frontmatterTooltip;
1650
- case "stale-both":
1651
- return ANNOTATION_STALE_TEXTS.bothTooltip;
1652
- }
1609
+ function readStability(node) {
1610
+ const fromAnn = node.sidecar?.annotations?.["stability"];
1611
+ if (isStability(fromAnn)) return fromAnn;
1612
+ const legacy = readLegacyMetadataStability(node.frontmatter);
1613
+ return isStability(legacy) ? legacy : null;
1614
+ }
1615
+ function readLegacyMetadataStability(fm) {
1616
+ if (!fm) return void 0;
1617
+ const meta = fm["metadata"];
1618
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
1619
+ return meta["stability"];
1620
+ }
1621
+ function isStability(value) {
1622
+ return value === "experimental" || value === "deprecated" || value === "stable";
1653
1623
  }
1654
1624
 
1655
- // built-in-plugins/i18n/annotation-orphan.texts.ts
1656
- var ANNOTATION_ORPHAN_TEXTS = {
1657
- /** Sidecar `<path>.sm` has no matching `<path>.md`. */
1658
- message: "Orphan sidecar: {{sidecarPath}} has no matching markdown node at {{expectedMdPath}}."
1625
+ // plugins/core/analyzers/superseded/text.ts
1626
+ var SUPERSEDED_TEXTS = {
1627
+ /** `<path> is superseded by <supersededBy>` */
1628
+ message: "{{path}} is superseded by {{supersededBy}}"
1659
1629
  };
1660
1630
 
1661
- // built-in-plugins/analyzers/annotation-orphan/index.ts
1662
- var ID13 = "annotation-orphan";
1663
- var annotationOrphanAnalyzer = {
1664
- id: ID13,
1631
+ // plugins/core/analyzers/superseded/index.ts
1632
+ var ID15 = "superseded";
1633
+ var supersededAnalyzer = {
1634
+ id: ID15,
1665
1635
  pluginId: "core",
1666
1636
  kind: "analyzer",
1667
1637
  version: "1.0.0",
1668
- description: "Detects and flags sidecars (`.sm`) whose `.md` no longer exists.",
1669
- stability: "stable",
1638
+ description: "Detects and marks nodes replaced by a newer one via `supersededBy`.",
1670
1639
  mode: "deterministic",
1671
1640
  evaluate(ctx) {
1672
- const orphans = ctx.orphanSidecars;
1673
- if (!orphans || orphans.length === 0) return [];
1674
1641
  const issues = [];
1675
- for (const orphan of orphans) {
1676
- const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
1642
+ for (const node of ctx.nodes) {
1643
+ const supersededBy = pickSupersededBy(node);
1644
+ if (supersededBy === null) continue;
1677
1645
  issues.push({
1678
- analyzerId: ID13,
1679
- severity: "warn",
1680
- nodeIds: [expectedMdRelative],
1681
- message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
1682
- sidecarPath: orphan.relativePath,
1683
- expectedMdPath: orphan.expectedMdPath
1646
+ analyzerId: ID15,
1647
+ severity: "info",
1648
+ nodeIds: [node.path],
1649
+ message: tx(SUPERSEDED_TEXTS.message, {
1650
+ path: node.path,
1651
+ supersededBy
1684
1652
  }),
1685
- data: {
1686
- sidecarPath: orphan.relativePath,
1687
- expectedMdPath: orphan.expectedMdPath
1688
- }
1653
+ data: { supersededBy }
1689
1654
  });
1690
1655
  }
1691
1656
  return issues;
1692
1657
  }
1693
1658
  };
1659
+ function pickSupersededBy(node) {
1660
+ const sidecar = node.sidecar;
1661
+ if (!sidecar || sidecar.present !== true) return null;
1662
+ const ann = sidecar.annotations;
1663
+ if (!ann || typeof ann !== "object" || Array.isArray(ann)) return null;
1664
+ const value = ann["supersededBy"];
1665
+ if (typeof value !== "string" || value.length === 0) return null;
1666
+ return value;
1667
+ }
1694
1668
 
1695
- // built-in-plugins/i18n/job-orphan-file.texts.ts
1696
- var JOB_ORPHAN_FILE_TEXTS = {
1669
+ // plugins/core/analyzers/trigger-collision/text.ts
1670
+ var TRIGGER_COLLISION_TEXTS = {
1697
1671
  /**
1698
- * `<path>.md` lives under `.skill-map/jobs/` but no `state_jobs.filePath`
1699
- * row references it. Run `sm job prune --orphan-files` to remove.
1672
+ * Top-level message when `analyzeTriggerBucket` accumulated exactly one
1673
+ * cause part. Used for the advertiser-ambiguous-only, invocation-
1674
+ * ambiguous-only, and cross-kind-only branches.
1700
1675
  */
1701
- message: "Orphan job file: {{filePath}} is not referenced by any state_jobs row. Run `sm job prune --orphan-files` to remove it."
1676
+ messageOnePart: 'Trigger "{{normalized}}" has {{part}}.',
1677
+ /**
1678
+ * Top-level message when `analyzeTriggerBucket` accumulated two cause
1679
+ * parts (advertiser-ambiguous AND invocation-ambiguous fire together).
1680
+ * The joiner lives inside the template so future locales can adapt it
1681
+ * (e.g. `'; y '` in Spanish) without touching the rule code.
1682
+ */
1683
+ messageTwoParts: 'Trigger "{{normalized}}" has {{first}}; and {{second}}.',
1684
+ /** `<n> nodes advertise it: <list>` part, fires on the advertiser-ambiguous branch. */
1685
+ partAdvertisers: "{{count}} nodes advertise it: {{paths}}",
1686
+ /** `<n> distinct invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
1687
+ partInvocations: "{{count}} distinct invocation forms: {{forms}}",
1688
+ /** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
1689
+ partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
1690
+ /** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
1691
+ partNonCanonicalPlural: "non-canonical invocations {{forms}} against advertiser {{advertiser}}"
1702
1692
  };
1703
1693
 
1704
- // built-in-plugins/analyzers/job-orphan-file/index.ts
1705
- var ID14 = "job-orphan-file";
1706
- var jobOrphanFileAnalyzer = {
1707
- id: ID14,
1694
+ // plugins/core/analyzers/trigger-collision/index.ts
1695
+ var ID16 = "trigger-collision";
1696
+ var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
1697
+ "command",
1698
+ "skill",
1699
+ "agent"
1700
+ ]);
1701
+ var triggerCollisionAnalyzer = {
1702
+ id: ID16,
1708
1703
  pluginId: "core",
1709
1704
  kind: "analyzer",
1710
- version: "1.0.0",
1711
- description: "Detects and flags leftover job result files (no live job references them). Cleanup via `sm job prune --orphan-files`.",
1712
- stability: "stable",
1713
1705
  mode: "deterministic",
1706
+ version: "1.0.0",
1707
+ description: "Detects and flags two or more nodes claiming the same `/command` or `@agent` name.",
1708
+ // Two claim-collection passes (advertisement + invocation) feeding
1709
+ // the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
1710
+ // eslint-disable-next-line complexity
1714
1711
  evaluate(ctx) {
1715
- const orphans = ctx.orphanJobFiles;
1716
- if (!orphans || orphans.length === 0) return [];
1717
- const issues = [];
1718
- for (const filePath of orphans) {
1719
- issues.push({
1720
- analyzerId: ID14,
1721
- severity: "warn",
1722
- nodeIds: [filePath],
1723
- message: tx(JOB_ORPHAN_FILE_TEXTS.message, { filePath }),
1724
- data: { filePath }
1712
+ const buckets = /* @__PURE__ */ new Map();
1713
+ const push = (key, claim) => {
1714
+ const bucket = buckets.get(key) ?? [];
1715
+ bucket.push(claim);
1716
+ buckets.set(key, bucket);
1717
+ };
1718
+ for (const node of ctx.nodes) {
1719
+ if (!ADVERTISING_KINDS.has(node.kind)) continue;
1720
+ const raw = node.frontmatter?.["name"];
1721
+ if (typeof raw !== "string" || raw.length === 0) continue;
1722
+ const normalized = `/${normalizeTrigger(raw)}`;
1723
+ if (normalized === "/") continue;
1724
+ push(normalized, {
1725
+ kind: "advertiser",
1726
+ token: node.path,
1727
+ nodeId: node.path,
1728
+ canonicalForm: `/${raw}`
1729
+ });
1730
+ }
1731
+ for (const link2 of ctx.links) {
1732
+ const normalized = link2.trigger?.normalizedTrigger;
1733
+ if (!normalized) continue;
1734
+ push(normalized, {
1735
+ kind: "invocation",
1736
+ token: link2.target,
1737
+ nodeId: link2.source
1725
1738
  });
1726
1739
  }
1740
+ const issues = [];
1741
+ for (const [normalized, claims] of buckets) {
1742
+ const issue = analyzeTriggerBucket(normalized, claims);
1743
+ if (issue) issues.push(issue);
1744
+ }
1727
1745
  return issues;
1728
1746
  }
1729
1747
  };
1748
+ function analyzeTriggerBucket(normalized, claims) {
1749
+ const advertiserPaths = [
1750
+ ...new Set(claims.filter((c) => c.kind === "advertiser").map((c) => c.token))
1751
+ ].sort();
1752
+ const invocationTargets = [
1753
+ ...new Set(claims.filter((c) => c.kind === "invocation").map((c) => c.token))
1754
+ ].sort();
1755
+ const advertisers = claims.filter(
1756
+ (c) => c.kind === "advertiser"
1757
+ );
1758
+ const advertiserAmbiguous = advertiserPaths.length >= 2;
1759
+ const invocationAmbiguous = invocationTargets.length >= 2;
1760
+ const canonicalForms = new Set(advertisers.map((a) => a.canonicalForm));
1761
+ const nonCanonicalInvocations = invocationTargets.filter((t) => !canonicalForms.has(t));
1762
+ const crossKindAmbiguous = advertiserPaths.length === 1 && nonCanonicalInvocations.length >= 1;
1763
+ if (!advertiserAmbiguous && !invocationAmbiguous && !crossKindAmbiguous) {
1764
+ return null;
1765
+ }
1766
+ const nodeIds = [...new Set(claims.map((c) => c.nodeId))].sort();
1767
+ const parts = [];
1768
+ if (advertiserAmbiguous) {
1769
+ parts.push(
1770
+ tx(TRIGGER_COLLISION_TEXTS.partAdvertisers, {
1771
+ count: advertiserPaths.length,
1772
+ paths: advertiserPaths.join(", ")
1773
+ })
1774
+ );
1775
+ }
1776
+ if (invocationAmbiguous) {
1777
+ parts.push(
1778
+ tx(TRIGGER_COLLISION_TEXTS.partInvocations, {
1779
+ count: invocationTargets.length,
1780
+ forms: invocationTargets.join(", ")
1781
+ })
1782
+ );
1783
+ } else if (crossKindAmbiguous) {
1784
+ const template = nonCanonicalInvocations.length > 1 ? TRIGGER_COLLISION_TEXTS.partNonCanonicalPlural : TRIGGER_COLLISION_TEXTS.partNonCanonicalSingular;
1785
+ parts.push(
1786
+ tx(template, {
1787
+ forms: nonCanonicalInvocations.join(", "),
1788
+ advertiser: advertiserPaths[0]
1789
+ })
1790
+ );
1791
+ }
1792
+ const message = parts.length === 2 ? tx(TRIGGER_COLLISION_TEXTS.messageTwoParts, {
1793
+ normalized,
1794
+ first: parts[0],
1795
+ second: parts[1]
1796
+ }) : tx(TRIGGER_COLLISION_TEXTS.messageOnePart, {
1797
+ normalized,
1798
+ part: parts[0]
1799
+ });
1800
+ return {
1801
+ analyzerId: ID16,
1802
+ severity: "error",
1803
+ nodeIds,
1804
+ message,
1805
+ data: {
1806
+ normalizedTrigger: normalized,
1807
+ invocationTargets,
1808
+ advertiserPaths
1809
+ }
1810
+ };
1811
+ }
1730
1812
 
1731
- // built-in-plugins/analyzers/unknown-field/index.ts
1813
+ // plugins/core/analyzers/unknown-field/index.ts
1732
1814
  import { readFileSync } from "fs";
1733
1815
  import { dirname, resolve as resolve2 } from "path";
1734
1816
  import { createRequire } from "module";
@@ -1741,7 +1823,7 @@ function applyAjvFormats(ajv) {
1741
1823
  addFormats(ajv);
1742
1824
  }
1743
1825
 
1744
- // built-in-plugins/i18n/unknown-field.texts.ts
1826
+ // plugins/core/analyzers/unknown-field/text.ts
1745
1827
  var UNKNOWN_FIELD_TEXTS = {
1746
1828
  /** Key inside `annotations:` is not in the curated catalog. */
1747
1829
  unknownAnnotationKey: "{{path}}: sidecar annotations contain unknown key '{{key}}' (not in annotations.schema.json catalog).",
@@ -1755,18 +1837,17 @@ var UNKNOWN_FIELD_TEXTS = {
1755
1837
  alertTooltipMany: "This node has {{count}} unknown fields in its sidecar. Open the inspector for details."
1756
1838
  };
1757
1839
 
1758
- // built-in-plugins/analyzers/unknown-field/index.ts
1759
- var ID15 = "unknown-field";
1840
+ // plugins/core/analyzers/unknown-field/index.ts
1841
+ var ID17 = "unknown-field";
1760
1842
  var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
1761
1843
  var unknownFieldAnalyzer = {
1762
- id: ID15,
1844
+ id: ID17,
1763
1845
  pluginId: "core",
1764
1846
  kind: "analyzer",
1765
1847
  version: "1.0.0",
1766
1848
  description: "Detects and flags typos or unrecognized keys in sidecars (`.sm`).",
1767
- stability: "stable",
1768
1849
  mode: "deterministic",
1769
- viewContributions: {
1850
+ ui: {
1770
1851
  // Corner badge on the graph card; count omitted when there is a
1771
1852
  // single unknown field (avoids a noisy "icon + 1" chip).
1772
1853
  alert: {
@@ -1818,7 +1899,7 @@ var unknownFieldAnalyzer = {
1818
1899
  for (const key of Object.keys(annotations)) {
1819
1900
  if (!knownAnnotationKeys.has(key)) {
1820
1901
  issues.push({
1821
- analyzerId: ID15,
1902
+ analyzerId: ID17,
1822
1903
  severity: "warn",
1823
1904
  nodeIds: [node.path],
1824
1905
  message: tx(UNKNOWN_FIELD_TEXTS.unknownAnnotationKey, {
@@ -1845,7 +1926,7 @@ var unknownFieldAnalyzer = {
1845
1926
  if (validator(value)) continue;
1846
1927
  const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
1847
1928
  issues.push({
1848
- analyzerId: ID15,
1929
+ analyzerId: ID17,
1849
1930
  severity: "warn",
1850
1931
  nodeIds: [node.path],
1851
1932
  message: tx(UNKNOWN_FIELD_TEXTS.pluginNamespaceInvalid, {
@@ -1861,7 +1942,7 @@ var unknownFieldAnalyzer = {
1861
1942
  continue;
1862
1943
  }
1863
1944
  issues.push({
1864
- analyzerId: ID15,
1945
+ analyzerId: ID17,
1865
1946
  severity: "warn",
1866
1947
  nodeIds: [node.path],
1867
1948
  message: tx(UNKNOWN_FIELD_TEXTS.unknownRootKey, {
@@ -1925,178 +2006,12 @@ function indexRootContributions(contributions) {
1925
2006
  if (entry.location === "root") out.add(entry.key);
1926
2007
  }
1927
2008
  return out;
1928
- }
1929
- function collectPluginIds(contributions) {
1930
- const out = /* @__PURE__ */ new Set();
1931
- for (const entry of contributions) out.add(entry.pluginId);
1932
- return out;
1933
- }
1934
-
1935
- // built-in-plugins/analyzers/contribution-orphan/index.ts
1936
- var ID16 = "contribution-orphan";
1937
- var contributionOrphanAnalyzer = {
1938
- id: ID16,
1939
- pluginId: "core",
1940
- kind: "analyzer",
1941
- version: "0.0.0",
1942
- description: "Detects and warns about plugin data referencing nodes renamed or deleted in the latest scan.",
1943
- stability: "experimental",
1944
- mode: "deterministic",
1945
- evaluate(_ctx) {
1946
- return [];
1947
- }
1948
- };
1949
-
1950
- // kernel/util/safe-text.ts
1951
- var ANSI_ESCAPE_RE = /[›][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
1952
- var C0_CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
1953
- function sanitizeForTerminal(text) {
1954
- return text.replace(ANSI_ESCAPE_RE, "").replace(C0_CONTROL_RE, "");
1955
- }
1956
-
1957
- // built-in-plugins/i18n/ascii.texts.ts
1958
- var ASCII_FORMATTER_TEXTS = {
1959
- /** Header line: `skill-map graph: N nodes, M links, K issues`. */
1960
- header: "skill-map graph: {{nodes}} nodes, {{links}} links, {{issues}} issues",
1961
- /** Per-node-kind section header: `## <kind> (<count>)`. */
1962
- kindSectionHeader: "## {{kind}} ({{count}})",
1963
- /** Plain node bullet: `- <path>`. */
1964
- nodeBullet: "- {{path}}",
1965
- /** Node bullet with title suffix: `- <path>: "<title>"`. */
1966
- nodeBulletWithTitle: '- {{path}}: "{{title}}"',
1967
- /** `## links (<count>)` section header. */
1968
- linksSectionHeader: "## links ({{count}})",
1969
- /** Link bullet: `- <source> --<kind>--> <target> [<confidence>]`. */
1970
- linkBullet: "- {{source}} --{{kind}}--> {{target}} [{{confidence}}]",
1971
- /** `## issues (<count>)` section header. */
1972
- issuesSectionHeader: "## issues ({{count}})",
1973
- /** Issue bullet: `- [<severity>] <analyzerId>: <message>`. */
1974
- issueBullet: "- [{{severity}}] {{analyzerId}}: {{message}}"
1975
- };
1976
-
1977
- // built-in-plugins/formatters/ascii/index.ts
1978
- var ID17 = "ascii";
1979
- var KIND_ORDER = ["agent", "command", "skill", "markdown"];
1980
- var asciiFormatter = {
1981
- id: ID17,
1982
- pluginId: "core",
1983
- kind: "formatter",
1984
- version: "1.0.0",
1985
- description: "Renders the scan as plain text, grouped by kind, arrows, and issues. Used by `sm scan --format=ascii`.",
1986
- stability: "stable",
1987
- formatId: "ascii",
1988
- // ASCII tree formatter, header + per-kind sections + per-issue
1989
- // section. Each section iterates and renders; splitting per section
1990
- // would multiply the for-loop boilerplate.
1991
- // eslint-disable-next-line complexity
1992
- format(ctx) {
1993
- const out = [];
1994
- out.push(
1995
- tx(ASCII_FORMATTER_TEXTS.header, {
1996
- nodes: ctx.nodes.length,
1997
- links: ctx.links.length,
1998
- issues: ctx.issues.length
1999
- }),
2000
- ""
2001
- );
2002
- const byKind = /* @__PURE__ */ new Map();
2003
- for (const node of ctx.nodes) {
2004
- if (!byKind.has(node.kind)) byKind.set(node.kind, []);
2005
- byKind.get(node.kind).push(node);
2006
- }
2007
- const renderedKinds = /* @__PURE__ */ new Set();
2008
- for (const kind of KIND_ORDER) {
2009
- const group = byKind.get(kind);
2010
- if (!group || group.length === 0) continue;
2011
- renderSection(out, kind, group);
2012
- renderedKinds.add(kind);
2013
- }
2014
- const extraKinds = [...byKind.keys()].filter((k) => !renderedKinds.has(k)).sort();
2015
- for (const kind of extraKinds) {
2016
- const group = byKind.get(kind);
2017
- if (!group || group.length === 0) continue;
2018
- renderSection(out, kind, group);
2019
- }
2020
- if (ctx.links.length > 0) {
2021
- out.push(tx(ASCII_FORMATTER_TEXTS.linksSectionHeader, { count: ctx.links.length }));
2022
- const sorted = [...ctx.links].sort((a, b) => {
2023
- const aKey = `${a.source}\0${a.kind}\0${a.target}`;
2024
- const bKey = `${b.source}\0${b.kind}\0${b.target}`;
2025
- return aKey.localeCompare(bKey);
2026
- });
2027
- for (const link2 of sorted) {
2028
- out.push(
2029
- tx(ASCII_FORMATTER_TEXTS.linkBullet, {
2030
- source: sanitizeForTerminal(link2.source),
2031
- kind: sanitizeForTerminal(link2.kind),
2032
- target: sanitizeForTerminal(link2.target),
2033
- confidence: link2.confidence
2034
- })
2035
- );
2036
- }
2037
- out.push("");
2038
- }
2039
- if (ctx.issues.length > 0) {
2040
- out.push(tx(ASCII_FORMATTER_TEXTS.issuesSectionHeader, { count: ctx.issues.length }));
2041
- for (const issue of ctx.issues) {
2042
- out.push(
2043
- tx(ASCII_FORMATTER_TEXTS.issueBullet, {
2044
- severity: issue.severity,
2045
- analyzerId: sanitizeForTerminal(issue.analyzerId),
2046
- message: sanitizeForTerminal(issue.message)
2047
- })
2048
- );
2049
- }
2050
- out.push("");
2051
- }
2052
- return out.join("\n");
2053
- }
2054
- };
2055
- function pickTitle(node) {
2056
- const name = node.frontmatter?.["name"];
2057
- return typeof name === "string" && name.length > 0 ? name : null;
2058
- }
2059
- function renderSection(out, kind, group) {
2060
- const sorted = [...group].sort((a, b) => a.path.localeCompare(b.path));
2061
- out.push(
2062
- tx(ASCII_FORMATTER_TEXTS.kindSectionHeader, {
2063
- kind: sanitizeForTerminal(kind),
2064
- count: sorted.length
2065
- })
2066
- );
2067
- for (const node of sorted) {
2068
- const title = pickTitle(node);
2069
- out.push(
2070
- title ? tx(ASCII_FORMATTER_TEXTS.nodeBulletWithTitle, {
2071
- path: sanitizeForTerminal(node.path),
2072
- title: sanitizeForTerminal(title)
2073
- }) : tx(ASCII_FORMATTER_TEXTS.nodeBullet, { path: sanitizeForTerminal(node.path) })
2074
- );
2075
- }
2076
- out.push("");
2077
- }
2078
-
2079
- // built-in-plugins/formatters/json/index.ts
2080
- var ID18 = "json";
2081
- var jsonFormatter = {
2082
- id: ID18,
2083
- pluginId: "core",
2084
- kind: "formatter",
2085
- version: "1.0.0",
2086
- description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json` when the full ScanResult is available). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
2087
- stability: "stable",
2088
- formatId: ID18,
2089
- format(ctx) {
2090
- if (ctx.scanResult !== void 0) {
2091
- return JSON.stringify(ctx.scanResult);
2092
- }
2093
- return JSON.stringify({
2094
- nodes: ctx.nodes,
2095
- links: ctx.links,
2096
- issues: ctx.issues
2097
- });
2098
- }
2099
- };
2009
+ }
2010
+ function collectPluginIds(contributions) {
2011
+ const out = /* @__PURE__ */ new Set();
2012
+ for (const entry of contributions) out.add(entry.pluginId);
2013
+ return out;
2014
+ }
2100
2015
 
2101
2016
  // kernel/adapters/schema-validators.ts
2102
2017
  import { readFileSync as readFileSync2 } from "fs";
@@ -2138,6 +2053,7 @@ var SCHEMA_FILES = {
2138
2053
  "conformance-case": "schemas/conformance-case.schema.json",
2139
2054
  "history-stats": "schemas/history-stats.schema.json",
2140
2055
  "extension-provider": "schemas/extensions/provider.schema.json",
2056
+ "extension-provider-kind": "schemas/extensions/provider-kind.schema.json",
2141
2057
  "extension-extractor": "schemas/extensions/extractor.schema.json",
2142
2058
  "extension-analyzer": "schemas/extensions/analyzer.schema.json",
2143
2059
  "extension-action": "schemas/extensions/action.schema.json",
@@ -2304,7 +2220,7 @@ function existsSyncSafe(path) {
2304
2220
  }
2305
2221
  }
2306
2222
 
2307
- // built-in-plugins/i18n/validate-all.texts.ts
2223
+ // plugins/core/analyzers/validate-all/text.ts
2308
2224
  var VALIDATE_ALL_TEXTS = {
2309
2225
  /** `Node <path> failed schema validation: <errors>` */
2310
2226
  nodeFailure: "Node {{path}} failed schema validation: {{errors}}",
@@ -2318,17 +2234,16 @@ var VALIDATE_ALL_TEXTS = {
2318
2234
  alertTooltipMany: "{{count}} schema validation issues on this node."
2319
2235
  };
2320
2236
 
2321
- // built-in-plugins/analyzers/validate-all/index.ts
2322
- var ID19 = "validate-all";
2237
+ // plugins/core/analyzers/validate-all/index.ts
2238
+ var ID18 = "validate-all";
2323
2239
  var validateAllAnalyzer = {
2324
- id: ID19,
2240
+ id: ID18,
2325
2241
  pluginId: "core",
2326
2242
  kind: "analyzer",
2327
2243
  version: "1.0.0",
2328
2244
  description: "Detects and flags nodes or links violating the project schemas.",
2329
- stability: "stable",
2330
2245
  mode: "deterministic",
2331
- viewContributions: {
2246
+ ui: {
2332
2247
  // Corner badge on the graph card; surfaces when the node body /
2333
2248
  // frontmatter fails schema validation (parse error, missing
2334
2249
  // `name`/`description`, malformed YAML, etc.). Same visual
@@ -2385,7 +2300,7 @@ function collectNodeFindings(v, node, out) {
2385
2300
  const result = v.validate("node", toNodeForSchema(node));
2386
2301
  if (result.ok) return;
2387
2302
  out.push({
2388
- analyzerId: ID19,
2303
+ analyzerId: ID18,
2389
2304
  severity: "error",
2390
2305
  nodeIds: [node.path],
2391
2306
  message: tx(VALIDATE_ALL_TEXTS.nodeFailure, {
@@ -2404,7 +2319,7 @@ function collectFrontmatterBaseFindings(node, out) {
2404
2319
  if (isMissingStringField(fm, "description")) missing.push("description");
2405
2320
  if (missing.length === 0) return;
2406
2321
  out.push({
2407
- analyzerId: ID19,
2322
+ analyzerId: ID18,
2408
2323
  // `warn` (not `error`) so the default `sm scan` exit code stays
2409
2324
  // 0 even when nodes are missing frontmatter base fields. Strict
2410
2325
  // mode (`sm scan --strict`) still escalates to exit 1. Matches
@@ -2426,7 +2341,7 @@ function collectLinkFindings(v, link2, out) {
2426
2341
  const result = v.validate("link", toLinkForSchema(link2));
2427
2342
  if (result.ok) return;
2428
2343
  out.push({
2429
- analyzerId: ID19,
2344
+ analyzerId: ID18,
2430
2345
  severity: "error",
2431
2346
  nodeIds: [link2.source],
2432
2347
  message: tx(VALIDATE_ALL_TEXTS.linkFailure, {
@@ -2466,120 +2381,154 @@ function toLinkForSchema(link2) {
2466
2381
  };
2467
2382
  }
2468
2383
 
2469
- // kernel/util/trigger-resolve.ts
2470
- function buildNameIndex(nodes) {
2471
- const out = /* @__PURE__ */ new Map();
2472
- indexByCanonicalName(nodes, out);
2473
- fillIndexWithPathBasename(nodes, out);
2474
- return out;
2475
- }
2476
- function indexByCanonicalName(nodes, out) {
2477
- for (const node of nodes) {
2478
- const raw = canonicalName(node);
2479
- if (raw === null) continue;
2480
- const key = normalizeTrigger(raw);
2481
- if (!out.has(key)) out.set(key, node.path);
2482
- }
2384
+ // kernel/util/safe-text.ts
2385
+ var ANSI_ESCAPE_RE = /[›][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
2386
+ var C0_CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
2387
+ function sanitizeForTerminal(text) {
2388
+ return text.replace(ANSI_ESCAPE_RE, "").replace(C0_CONTROL_RE, "");
2483
2389
  }
2484
- function fillIndexWithPathBasename(nodes, out) {
2485
- for (const node of nodes) {
2486
- if (canonicalName(node) !== null) continue;
2487
- const derived = pathBasenameForLink(node.path);
2488
- if (derived.length === 0) continue;
2489
- const key = normalizeTrigger(derived);
2490
- if (!out.has(key)) out.set(key, node.path);
2390
+
2391
+ // plugins/core/formatters/ascii/text.ts
2392
+ var ASCII_FORMATTER_TEXTS = {
2393
+ /** Header line: `skill-map graph: N nodes, M links, K issues`. */
2394
+ header: "skill-map graph: {{nodes}} nodes, {{links}} links, {{issues}} issues",
2395
+ /** Per-node-kind section header: `## <kind> (<count>)`. */
2396
+ kindSectionHeader: "## {{kind}} ({{count}})",
2397
+ /** Plain node bullet: `- <path>`. */
2398
+ nodeBullet: "- {{path}}",
2399
+ /** Node bullet with title suffix: `- <path>: "<title>"`. */
2400
+ nodeBulletWithTitle: '- {{path}}: "{{title}}"',
2401
+ /** `## links (<count>)` section header. */
2402
+ linksSectionHeader: "## links ({{count}})",
2403
+ /** Link bullet: `- <source> --<kind>--> <target> [<confidence>]`. */
2404
+ linkBullet: "- {{source}} --{{kind}}--> {{target}} [{{confidence}}]",
2405
+ /** `## issues (<count>)` section header. */
2406
+ issuesSectionHeader: "## issues ({{count}})",
2407
+ /** Issue bullet: `- [<severity>] <analyzerId>: <message>`. */
2408
+ issueBullet: "- [{{severity}}] {{analyzerId}}: {{message}}"
2409
+ };
2410
+
2411
+ // plugins/core/formatters/ascii/index.ts
2412
+ var ID19 = "ascii";
2413
+ var KIND_ORDER = ["agent", "command", "skill", "markdown"];
2414
+ var asciiFormatter = {
2415
+ id: ID19,
2416
+ pluginId: "core",
2417
+ kind: "formatter",
2418
+ formatId: ID19,
2419
+ version: "1.0.0",
2420
+ description: "Renders the scan as plain text, grouped by kind, arrows, and issues. Used by `sm scan --format=ascii`.",
2421
+ // ASCII tree formatter, header + per-kind sections + per-issue
2422
+ // section. Each section iterates and renders; splitting per section
2423
+ // would multiply the for-loop boilerplate.
2424
+ // eslint-disable-next-line complexity
2425
+ format(ctx) {
2426
+ const out = [];
2427
+ out.push(
2428
+ tx(ASCII_FORMATTER_TEXTS.header, {
2429
+ nodes: ctx.nodes.length,
2430
+ links: ctx.links.length,
2431
+ issues: ctx.issues.length
2432
+ }),
2433
+ ""
2434
+ );
2435
+ const byKind = /* @__PURE__ */ new Map();
2436
+ for (const node of ctx.nodes) {
2437
+ if (!byKind.has(node.kind)) byKind.set(node.kind, []);
2438
+ byKind.get(node.kind).push(node);
2439
+ }
2440
+ const renderedKinds = /* @__PURE__ */ new Set();
2441
+ for (const kind of KIND_ORDER) {
2442
+ const group = byKind.get(kind);
2443
+ if (!group || group.length === 0) continue;
2444
+ renderSection(out, kind, group);
2445
+ renderedKinds.add(kind);
2446
+ }
2447
+ const extraKinds = [...byKind.keys()].filter((k) => !renderedKinds.has(k)).sort();
2448
+ for (const kind of extraKinds) {
2449
+ const group = byKind.get(kind);
2450
+ if (!group || group.length === 0) continue;
2451
+ renderSection(out, kind, group);
2452
+ }
2453
+ if (ctx.links.length > 0) {
2454
+ out.push(tx(ASCII_FORMATTER_TEXTS.linksSectionHeader, { count: ctx.links.length }));
2455
+ const sorted = [...ctx.links].sort((a, b) => {
2456
+ const aKey = `${a.source}\0${a.kind}\0${a.target}`;
2457
+ const bKey = `${b.source}\0${b.kind}\0${b.target}`;
2458
+ return aKey.localeCompare(bKey);
2459
+ });
2460
+ for (const link2 of sorted) {
2461
+ out.push(
2462
+ tx(ASCII_FORMATTER_TEXTS.linkBullet, {
2463
+ source: sanitizeForTerminal(link2.source),
2464
+ kind: sanitizeForTerminal(link2.kind),
2465
+ target: sanitizeForTerminal(link2.target),
2466
+ confidence: link2.confidence
2467
+ })
2468
+ );
2469
+ }
2470
+ out.push("");
2471
+ }
2472
+ if (ctx.issues.length > 0) {
2473
+ out.push(tx(ASCII_FORMATTER_TEXTS.issuesSectionHeader, { count: ctx.issues.length }));
2474
+ for (const issue of ctx.issues) {
2475
+ out.push(
2476
+ tx(ASCII_FORMATTER_TEXTS.issueBullet, {
2477
+ severity: issue.severity,
2478
+ analyzerId: sanitizeForTerminal(issue.analyzerId),
2479
+ message: sanitizeForTerminal(issue.message)
2480
+ })
2481
+ );
2482
+ }
2483
+ out.push("");
2484
+ }
2485
+ return out.join("\n");
2491
2486
  }
2487
+ };
2488
+ function pickTitle(node) {
2489
+ const name = node.frontmatter?.["name"];
2490
+ return typeof name === "string" && name.length > 0 ? name : null;
2492
2491
  }
2493
- function canonicalName(node) {
2494
- const raw = node.frontmatter?.["name"];
2495
- if (typeof raw !== "string" || raw.length === 0) return null;
2496
- return raw;
2497
- }
2498
- function pathBasenameForLink(path) {
2499
- const segments = path.split("/").filter((s) => s.length > 0);
2500
- if (segments.length === 0) return path;
2501
- const last = segments[segments.length - 1];
2502
- if (last === "SKILL.md" && segments.length >= 2) {
2503
- return segments[segments.length - 2];
2492
+ function renderSection(out, kind, group) {
2493
+ const sorted = [...group].sort((a, b) => a.path.localeCompare(b.path));
2494
+ out.push(
2495
+ tx(ASCII_FORMATTER_TEXTS.kindSectionHeader, {
2496
+ kind: sanitizeForTerminal(kind),
2497
+ count: sorted.length
2498
+ })
2499
+ );
2500
+ for (const node of sorted) {
2501
+ const title = pickTitle(node);
2502
+ out.push(
2503
+ title ? tx(ASCII_FORMATTER_TEXTS.nodeBulletWithTitle, {
2504
+ path: sanitizeForTerminal(node.path),
2505
+ title: sanitizeForTerminal(title)
2506
+ }) : tx(ASCII_FORMATTER_TEXTS.nodeBullet, { path: sanitizeForTerminal(node.path) })
2507
+ );
2504
2508
  }
2505
- return last.replace(/\.md$/, "");
2506
- }
2507
- function resolveLinkTargetToPath(link2, nameIndex) {
2508
- const raw = link2.target;
2509
- const sigil = raw.charAt(0);
2510
- if (sigil !== "/" && sigil !== "@") return raw;
2511
- const normalizedTrigger = link2.trigger?.normalizedTrigger;
2512
- const normalized = typeof normalizedTrigger === "string" ? normalizedTrigger.replace(/^[/@]/, "").trim() : normalizeTrigger(raw.slice(1));
2513
- const resolved = nameIndex.get(normalized);
2514
- return resolved ?? raw;
2509
+ out.push("");
2515
2510
  }
2516
2511
 
2517
- // built-in-plugins/analyzers/link-counts/index.ts
2518
- var ID20 = "link-counts";
2519
- var linkCountsAnalyzer = {
2512
+ // plugins/core/formatters/json/index.ts
2513
+ var ID20 = "json";
2514
+ var jsonFormatter = {
2520
2515
  id: ID20,
2521
2516
  pluginId: "core",
2522
- kind: "analyzer",
2517
+ kind: "formatter",
2523
2518
  version: "1.0.0",
2524
- description: "Counts incoming and outgoing links per node.",
2525
- stability: "stable",
2526
- mode: "deterministic",
2527
- viewContributions: {
2528
- linksIn: {
2529
- slot: "card.footer.left",
2530
- icon: "pi-download",
2531
- label: "incoming links",
2532
- emitWhenEmpty: false,
2533
- priority: 10
2534
- },
2535
- linksOut: {
2536
- slot: "card.footer.left",
2537
- icon: "pi-upload",
2538
- label: "outgoing links",
2539
- emitWhenEmpty: false,
2540
- priority: 20
2541
- }
2542
- },
2543
- evaluate(ctx) {
2544
- const nameIndex = buildNameIndex(ctx.nodes);
2545
- const perTarget = /* @__PURE__ */ new Map();
2546
- const perSource = /* @__PURE__ */ new Map();
2547
- for (const link2 of ctx.links) {
2548
- const resolvedTarget = resolveLinkTargetToPath(link2, nameIndex);
2549
- bump(perTarget, resolvedTarget, link2.kind);
2550
- bump(perSource, link2.source, link2.kind);
2551
- }
2552
- for (const node of ctx.nodes) {
2553
- emitChip(ctx, node.path, "linksIn", perTarget.get(node.path));
2554
- emitChip(ctx, node.path, "linksOut", perSource.get(node.path));
2519
+ description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json` when the full ScanResult is available). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
2520
+ formatId: ID20,
2521
+ format(ctx) {
2522
+ if (ctx.scanResult !== void 0) {
2523
+ return JSON.stringify(ctx.scanResult);
2555
2524
  }
2556
- return [];
2525
+ return JSON.stringify({
2526
+ nodes: ctx.nodes,
2527
+ links: ctx.links,
2528
+ issues: ctx.issues
2529
+ });
2557
2530
  }
2558
2531
  };
2559
- function bump(map, key, kind) {
2560
- let byKind = map.get(key);
2561
- if (!byKind) {
2562
- byKind = /* @__PURE__ */ new Map();
2563
- map.set(key, byKind);
2564
- }
2565
- byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
2566
- }
2567
- function emitChip(ctx, nodePath, contributionId, byKind) {
2568
- if (!byKind) return;
2569
- let total = 0;
2570
- for (const n of byKind.values()) total += n;
2571
- if (total === 0) return;
2572
- const capped = Math.min(total, 99);
2573
- const direction = contributionId === "linksIn" ? "in" : "out";
2574
- ctx.emitContribution(nodePath, contributionId, {
2575
- value: capped,
2576
- tooltip: formatBreakdown(byKind, direction)
2577
- });
2578
- }
2579
- function formatBreakdown(byKind, direction) {
2580
- const lines = [...byKind.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([kind, n]) => `${kind}: ${n}`);
2581
- return [direction, ...lines].join("\n");
2582
- }
2583
2532
 
2584
2533
  // kernel/sidecar/parse.ts
2585
2534
  import { existsSync, readFileSync as readFileSync3 } from "fs";
@@ -2706,7 +2655,7 @@ function resolveSpecRoot2() {
2706
2655
  }
2707
2656
  }
2708
2657
 
2709
- // built-in-plugins/actions/bump/index.ts
2658
+ // plugins/core/actions/bump/index.ts
2710
2659
  var ID21 = "bump";
2711
2660
  var PLUGIN_ID = "core";
2712
2661
  var bumpAction = {
@@ -2715,9 +2664,7 @@ var bumpAction = {
2715
2664
  kind: "action",
2716
2665
  version: "1.0.0",
2717
2666
  description: "Marks a node as updated: bumps version, refreshes sidecar hashes, records the timestamp.",
2718
- stability: "stable",
2719
2667
  mode: "deterministic",
2720
- reportSchemaRef: "https://skill-map.dev/spec/v0/bump-report.schema.json",
2721
2668
  // The runtime contract uses generic <TInput, TReport>; bump narrows
2722
2669
  // both. The cast is the standard pattern for built-ins that want
2723
2670
  // typed local I/O while staying compatible with the open generic.
@@ -2770,7 +2717,7 @@ function pickCurrentVersion(overlay) {
2770
2717
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
2771
2718
  }
2772
2719
 
2773
- // built-in-plugins/actions/mark-superseded/index.ts
2720
+ // plugins/core/actions/mark-superseded/index.ts
2774
2721
  var ID22 = "mark-superseded";
2775
2722
  var PLUGIN_ID2 = "core";
2776
2723
  var markSupersededAction = {
@@ -2779,9 +2726,7 @@ var markSupersededAction = {
2779
2726
  kind: "action",
2780
2727
  version: "0.0.0",
2781
2728
  description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar). Paired with the `core/superseded` analyzer.",
2782
- stability: "experimental",
2783
2729
  mode: "deterministic",
2784
- reportSchemaRef: "https://skill-map.dev/spec/v0/report-base-deterministic.schema.json",
2785
2730
  invoke(_input, _ctx) {
2786
2731
  const report = { ok: true, noop: true };
2787
2732
  return { report };
@@ -2886,7 +2831,7 @@ var UPDATE_CHECK_TEXTS = {
2886
2831
  // package.json
2887
2832
  var package_default = {
2888
2833
  name: "@skill-map/cli",
2889
- version: "0.27.0",
2834
+ version: "0.29.0",
2890
2835
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
2891
2836
  license: "MIT",
2892
2837
  type: "module",
@@ -2939,16 +2884,19 @@ var package_default = {
2939
2884
  "lint:fix": "eslint . --fix",
2940
2885
  reference: "node scripts/build-reference.js",
2941
2886
  "reference:check": "node scripts/build-reference.js --check",
2887
+ "build-built-ins": "node ../scripts/generate-built-ins.js",
2888
+ "built-ins:check": "node ../scripts/generate-built-ins.js --check",
2889
+ prebuild: "pnpm build-built-ins",
2942
2890
  validate: "pnpm validate:compile && pnpm validate:test",
2943
- "validate:compile": "pnpm typecheck && pnpm lint && pnpm build && pnpm reference:check",
2891
+ "validate:compile": "pnpm typecheck && pnpm lint && pnpm build && pnpm reference:check && pnpm built-ins:check",
2944
2892
  "validate:test": "pnpm test:ci",
2945
2893
  pretest: "tsup",
2946
2894
  "pretest:coverage": "tsup",
2947
2895
  "pretest:coverage:html": "tsup",
2948
- test: "tsc --noEmit && node --import tsx --test --test-reporter=spec 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
2949
- "test:ci": "node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
2950
- "test:coverage": "tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 node --experimental-default-config-file --import tsx --test --experimental-test-coverage 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
2951
- "test:coverage:html": "tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 c8 node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
2896
+ test: "tsc --noEmit && 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'",
2897
+ "test:ci": "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'",
2898
+ "test:coverage": "tsc --noEmit && 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'",
2899
+ "test:coverage:html": "tsc --noEmit && 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'",
2952
2900
  clean: "rm -rf dist coverage"
2953
2901
  },
2954
2902
  dependencies: {
@@ -3224,7 +3172,7 @@ function writeBanner(opts, latestVersion) {
3224
3172
  isTTY: opts.stderr.isTTY === true,
3225
3173
  noColorFlag: opts.noColorFlag
3226
3174
  });
3227
- const labelRaw = ` \u2B06 ${UPDATE_CHECK_TEXTS.availableHeader} `;
3175
+ const labelRaw = ` \u2B07 ${UPDATE_CHECK_TEXTS.availableHeader} `;
3228
3176
  const fillCount = Math.max(0, BANNER_WIDTH - 2 - labelRaw.length);
3229
3177
  const header = ansi.cyan("\u250C\u2500") + ansi.bold(ansi.cyan(labelRaw)) + ansi.cyan("\u2500".repeat(fillCount));
3230
3178
  const versionLine = `${ansi.cyan("\u2502")} ${VERSION} \u2192 ${latestVersion}`;
@@ -3237,15 +3185,13 @@ ${footer}
3237
3185
  `);
3238
3186
  }
3239
3187
 
3240
- // built-in-plugins/hooks/update-check/index.ts
3188
+ // plugins/core/hooks/update-check/index.ts
3241
3189
  var updateCheckHook = {
3242
3190
  id: "update-check",
3243
3191
  pluginId: "core",
3244
3192
  kind: "hook",
3245
3193
  version: "1.0.0",
3246
3194
  description: "Checks daily for a newer skill-map version on npm. Shows an `update available` banner when one is found.",
3247
- stability: "stable",
3248
- mode: "deterministic",
3249
3195
  triggers: ["boot"],
3250
3196
  async on(ctx) {
3251
3197
  const payload = ctx.event.data ?? {};
@@ -3259,14 +3205,41 @@ var updateCheckHook = {
3259
3205
  }
3260
3206
  };
3261
3207
 
3262
- // built-in-plugins/built-ins.ts
3208
+ // plugins/built-ins.ts
3209
+ var claudeProvider2 = { ...claudeProvider, pluginId: "claude" };
3210
+ var geminiProvider2 = { ...geminiProvider, pluginId: "gemini" };
3211
+ var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills" };
3212
+ var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core" };
3213
+ var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core" };
3214
+ var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "core" };
3215
+ var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core" };
3216
+ var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core" };
3217
+ var slashExtractor2 = { ...slashExtractor, pluginId: "core" };
3218
+ var toolsCountExtractor2 = { ...toolsCountExtractor, pluginId: "core" };
3219
+ var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core" };
3220
+ var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core" };
3221
+ var brokenRefAnalyzer2 = { ...brokenRefAnalyzer, pluginId: "core" };
3222
+ var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core" };
3223
+ var jobOrphanFileAnalyzer2 = { ...jobOrphanFileAnalyzer, pluginId: "core" };
3224
+ var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core" };
3225
+ var linkCountsAnalyzer2 = { ...linkCountsAnalyzer, pluginId: "core" };
3226
+ var stabilityAnalyzer2 = { ...stabilityAnalyzer, pluginId: "core" };
3227
+ var supersededAnalyzer2 = { ...supersededAnalyzer, pluginId: "core" };
3228
+ var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core" };
3229
+ var unknownFieldAnalyzer2 = { ...unknownFieldAnalyzer, pluginId: "core" };
3230
+ var validateAllAnalyzer2 = { ...validateAllAnalyzer, pluginId: "core" };
3231
+ var asciiFormatter2 = { ...asciiFormatter, pluginId: "core" };
3232
+ var jsonFormatter2 = { ...jsonFormatter, pluginId: "core" };
3233
+ var bumpAction2 = { ...bumpAction, pluginId: "core" };
3234
+ var markSupersededAction2 = { ...markSupersededAction, pluginId: "core" };
3235
+ var updateCheckHook2 = { ...updateCheckHook, pluginId: "core" };
3263
3236
  var builtInBundles = [
3264
3237
  {
3265
3238
  id: "claude",
3266
3239
  granularity: "bundle",
3267
3240
  description: "Claude Code platform integration. Classifies files under `.claude/{agents,commands,skills}` and parses Claude-flavored frontmatter.",
3268
3241
  extensions: [
3269
- claudeProvider
3242
+ claudeProvider2
3270
3243
  ]
3271
3244
  },
3272
3245
  {
@@ -3274,7 +3247,7 @@ var builtInBundles = [
3274
3247
  granularity: "bundle",
3275
3248
  description: "Gemini CLI platform integration. Classifies files under `.gemini/{agents,skills}` and parses Gemini-flavored frontmatter.",
3276
3249
  extensions: [
3277
- geminiProvider
3250
+ geminiProvider2
3278
3251
  ]
3279
3252
  },
3280
3253
  {
@@ -3282,7 +3255,7 @@ var builtInBundles = [
3282
3255
  granularity: "bundle",
3283
3256
  description: "Agent Skills open standard. Vendor-neutral path `.agents/skills/<name>/SKILL.md` (Anthropic, OpenAI, Google). See agentskills.io.",
3284
3257
  extensions: [
3285
- agentSkillsProvider
3258
+ agentSkillsProvider2
3286
3259
  ]
3287
3260
  },
3288
3261
  {
@@ -3290,39 +3263,30 @@ var builtInBundles = [
3290
3263
  granularity: "extension",
3291
3264
  description: "Core extensions shared across providers: extractors, analyzers, formatters, the bump action, and the universal `.md` fallback Provider.",
3292
3265
  extensions: [
3293
- // Provider FIRST within the core bundle so the kindRegistry
3294
- // composer picks it up alongside other providers; orchestration
3295
- // ordering (vendor providers first, core/markdown LAST) is
3296
- // enforced by the bundle list above (claude / gemini /
3297
- // agent-skills precede core). Within the core bundle, the
3298
- // provider's slot among extractors / analyzers / formatter is
3299
- // irrelevant, the orchestrator buckets by kind before
3300
- // iterating, so this list defines registration order, not
3301
- // execution order.
3302
- coreMarkdownProvider,
3303
- annotationsExtractor,
3304
- atDirectiveExtractor,
3305
- externalUrlCounterExtractor,
3306
- markdownLinkExtractor,
3307
- slashExtractor,
3308
- toolsCountExtractor,
3309
- triggerCollisionAnalyzer,
3310
- brokenRefAnalyzer,
3311
- supersededAnalyzer,
3312
- linkConflictAnalyzer,
3313
- annotationStaleAnalyzer,
3314
- annotationOrphanAnalyzer,
3315
- jobOrphanFileAnalyzer,
3316
- stabilityAnalyzer,
3317
- unknownFieldAnalyzer,
3318
- contributionOrphanAnalyzer,
3319
- asciiFormatter,
3320
- jsonFormatter,
3321
- validateAllAnalyzer,
3322
- linkCountsAnalyzer,
3323
- bumpAction,
3324
- markSupersededAction,
3325
- updateCheckHook
3266
+ coreMarkdownProvider2,
3267
+ annotationsExtractor2,
3268
+ atDirectiveExtractor2,
3269
+ externalUrlCounterExtractor2,
3270
+ markdownLinkExtractor2,
3271
+ slashExtractor2,
3272
+ toolsCountExtractor2,
3273
+ annotationOrphanAnalyzer2,
3274
+ annotationStaleAnalyzer2,
3275
+ brokenRefAnalyzer2,
3276
+ contributionOrphanAnalyzer2,
3277
+ jobOrphanFileAnalyzer2,
3278
+ linkConflictAnalyzer2,
3279
+ linkCountsAnalyzer2,
3280
+ stabilityAnalyzer2,
3281
+ supersededAnalyzer2,
3282
+ triggerCollisionAnalyzer2,
3283
+ unknownFieldAnalyzer2,
3284
+ validateAllAnalyzer2,
3285
+ asciiFormatter2,
3286
+ jsonFormatter2,
3287
+ bumpAction2,
3288
+ markSupersededAction2,
3289
+ updateCheckHook2
3326
3290
  ]
3327
3291
  }
3328
3292
  ];
@@ -3331,8 +3295,8 @@ function builtIns() {
3331
3295
  providers: [],
3332
3296
  extractors: [],
3333
3297
  analyzers: [],
3334
- actions: [],
3335
3298
  formatters: [],
3299
+ actions: [],
3336
3300
  hooks: []
3337
3301
  };
3338
3302
  for (const bundle of builtInBundles) {
@@ -3356,8 +3320,8 @@ function bucketBuiltIn(ext, out) {
3356
3320
  provider: out.providers,
3357
3321
  extractor: out.extractors,
3358
3322
  analyzer: out.analyzers,
3359
- action: out.actions,
3360
3323
  formatter: out.formatters,
3324
+ action: out.actions,
3361
3325
  hook: out.hooks
3362
3326
  });
3363
3327
  }
@@ -3366,12 +3330,9 @@ function toExtensionRow(x) {
3366
3330
  id: x.id,
3367
3331
  pluginId: x.pluginId,
3368
3332
  kind: x.kind,
3369
- version: x.version
3333
+ version: x.version,
3334
+ description: x.description ?? ""
3370
3335
  };
3371
- if (x.description !== void 0) row.description = x.description;
3372
- if (x.stability !== void 0) row.stability = x.stability;
3373
- if (x.preconditions !== void 0) row.preconditions = x.preconditions;
3374
- if (x.entry !== void 0) row.entry = x.entry;
3375
3336
  return row;
3376
3337
  }
3377
3338
 
@@ -4131,14 +4092,14 @@ function readJsonObjectOrEmpty(path) {
4131
4092
  }
4132
4093
  return {};
4133
4094
  }
4134
- function writeFileAtomicExclusive(path, content) {
4095
+ function writeFileAtomicExclusive(path, content, mode = 384) {
4135
4096
  const tmp = `${path}.tmp.${process.pid}.${randomBytes(8).toString("hex")}`;
4136
4097
  let fd = null;
4137
4098
  try {
4138
4099
  fd = openSync(
4139
4100
  tmp,
4140
4101
  fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,
4141
- 384
4102
+ mode
4142
4103
  );
4143
4104
  writeSync(fd, content);
4144
4105
  closeSync(fd);
@@ -7124,7 +7085,8 @@ function invokeBumpFor(node, absPath, force) {
7124
7085
  node,
7125
7086
  nodeAbsolutePath: absPath,
7126
7087
  invoker: "cli",
7127
- now: () => /* @__PURE__ */ new Date()
7088
+ now: () => /* @__PURE__ */ new Date(),
7089
+ settings: {}
7128
7090
  });
7129
7091
  }
7130
7092
 
@@ -7644,8 +7606,8 @@ var PLUGIN_LOADER_TEXTS = {
7644
7606
 
7645
7607
  // kernel/adapters/plugin-loader/index.ts
7646
7608
  import { createRequire as createRequire5 } from "module";
7647
- import { existsSync as existsSync11, readFileSync as readFileSync11, readdirSync as readdirSync3 } from "fs";
7648
- import { join as join7, resolve as resolve16 } from "path";
7609
+ import { existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
7610
+ import { join as join8, resolve as resolve16 } from "path";
7649
7611
  import { pathToFileURL } from "url";
7650
7612
  import semver from "semver";
7651
7613
 
@@ -7682,7 +7644,7 @@ function applyIdCollisions(plugins) {
7682
7644
  const buckets = /* @__PURE__ */ new Map();
7683
7645
  for (const p of plugins) {
7684
7646
  if (!p.manifest) continue;
7685
- const id = p.manifest.id;
7647
+ const id = p.id;
7686
7648
  const bucket = buckets.get(id);
7687
7649
  if (bucket) bucket.push(p);
7688
7650
  else buckets.set(id, [p]);
@@ -7733,39 +7695,22 @@ function extractDefault(mod) {
7733
7695
  if (!isRecord(mod)) return mod;
7734
7696
  return "default" in mod ? mod["default"] : mod;
7735
7697
  }
7698
+ var LOADER_INJECTED_KEYS = /* @__PURE__ */ new Set(["pluginId", "id", "kind", "kinds", "formatId"]);
7736
7699
  function stripFunctionsAndPluginId(input) {
7737
- if (!isRecord(input)) return input;
7738
- const out = {};
7739
- for (const [k, v] of Object.entries(input)) {
7740
- if (typeof v === "function") continue;
7741
- if (k === "pluginId") continue;
7742
- if (k === "kinds" && isRecord(v)) {
7743
- out[k] = stripKindsRuntimeFields(v);
7744
- continue;
7745
- }
7746
- out[k] = v;
7747
- }
7748
- return out;
7749
- }
7750
- function stripKindsRuntimeFields(kinds) {
7751
- const out = {};
7752
- for (const [kind, entry] of Object.entries(kinds)) {
7753
- if (!isRecord(entry)) {
7754
- out[kind] = entry;
7755
- continue;
7756
- }
7757
- const cleaned = {};
7758
- for (const [k, v] of Object.entries(entry)) {
7759
- if (k === "schemaJson") continue;
7760
- if (typeof v === "function") continue;
7761
- cleaned[k] = v;
7762
- }
7763
- out[kind] = cleaned;
7700
+ if (!isRecord(input)) return input;
7701
+ const out = {};
7702
+ for (const [k, v] of Object.entries(input)) {
7703
+ if (typeof v === "function") continue;
7704
+ if (LOADER_INJECTED_KEYS.has(k)) continue;
7705
+ out[k] = v;
7764
7706
  }
7765
7707
  return out;
7766
7708
  }
7767
7709
 
7768
7710
  // kernel/adapters/plugin-loader/validation.ts
7711
+ import * as nodeFs from "fs";
7712
+ import { existsSync as existsSync11 } from "fs";
7713
+ import { dirname as dirname9, join as join7 } from "path";
7769
7714
  import { Ajv2020 as Ajv20205 } from "ajv/dist/2020.js";
7770
7715
 
7771
7716
  // kernel/extensions/hook.ts
@@ -7793,76 +7738,73 @@ var KNOWN_KINDS = /* @__PURE__ */ new Set([
7793
7738
  ]);
7794
7739
  var KNOWN_KINDS_LIST = [...KNOWN_KINDS].join(" / ");
7795
7740
  var HOOKABLE_TRIGGERS_LIST = HOOK_TRIGGERS.join(", ");
7796
- function validateAnnotationContributions(pluginPath, manifest, relEntry, manifestView) {
7741
+ function validateAnnotationContributions(pluginPath, pluginId, manifest, relEntry, manifestView) {
7797
7742
  if (!isRecord(manifestView)) return null;
7798
- const raw = manifestView["annotationContributions"];
7743
+ const raw = manifestView["annotation"];
7799
7744
  if (raw === void 0) return null;
7800
7745
  if (!isRecord(raw)) return null;
7801
- for (const [key, value] of Object.entries(raw)) {
7802
- if (!isRecord(value)) continue;
7803
- const location = value["location"] ?? "namespaced";
7804
- const ownership = value["ownership"] ?? "shared";
7805
- if (location === "root" && ownership !== "exclusive") {
7806
- return {
7807
- ...fail(
7808
- pluginPath,
7809
- manifest.id,
7810
- "invalid-manifest",
7811
- tx(PLUGIN_LOADER_TEXTS.invalidManifestRootSharedAnnotation, {
7812
- relEntry,
7813
- key,
7814
- ownership
7815
- })
7816
- ),
7817
- manifest
7818
- };
7819
- }
7820
- const schema = value["schema"];
7821
- if (!isRecord(schema)) {
7822
- return {
7823
- ...fail(
7824
- pluginPath,
7825
- manifest.id,
7826
- "invalid-manifest",
7827
- tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {
7828
- relEntry,
7829
- key,
7830
- errDescription: "schema must be an object literal"
7831
- })
7832
- ),
7833
- manifest
7834
- };
7835
- }
7836
- try {
7837
- const ajv = new Ajv20205({ strict: false, allErrors: true, allowUnionTypes: true });
7838
- applyAjvFormats(ajv);
7839
- ajv.compile(schema);
7840
- } catch (err) {
7841
- return {
7842
- ...fail(
7843
- pluginPath,
7844
- manifest.id,
7845
- "invalid-manifest",
7846
- tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {
7847
- relEntry,
7848
- key,
7849
- errDescription: describe(err)
7850
- })
7851
- ),
7852
- manifest
7853
- };
7854
- }
7746
+ const location = raw["location"] ?? "namespaced";
7747
+ const ownership = raw["ownership"] ?? "shared";
7748
+ if (location === "root" && ownership !== "exclusive") {
7749
+ return {
7750
+ ...fail(
7751
+ pluginPath,
7752
+ pluginId,
7753
+ "invalid-manifest",
7754
+ tx(PLUGIN_LOADER_TEXTS.invalidManifestRootSharedAnnotation, {
7755
+ relEntry,
7756
+ key: "<annotation>",
7757
+ ownership
7758
+ })
7759
+ ),
7760
+ manifest
7761
+ };
7762
+ }
7763
+ const schema = raw["schema"];
7764
+ if (!isRecord(schema)) {
7765
+ return {
7766
+ ...fail(
7767
+ pluginPath,
7768
+ pluginId,
7769
+ "invalid-manifest",
7770
+ tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {
7771
+ relEntry,
7772
+ key: "<annotation>",
7773
+ errDescription: "schema must be an object literal"
7774
+ })
7775
+ ),
7776
+ manifest
7777
+ };
7778
+ }
7779
+ try {
7780
+ const ajv = new Ajv20205({ strict: false, allErrors: true, allowUnionTypes: true });
7781
+ applyAjvFormats(ajv);
7782
+ ajv.compile(schema);
7783
+ } catch (err) {
7784
+ return {
7785
+ ...fail(
7786
+ pluginPath,
7787
+ pluginId,
7788
+ "invalid-manifest",
7789
+ tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {
7790
+ relEntry,
7791
+ key: "<annotation>",
7792
+ errDescription: describe(err)
7793
+ })
7794
+ ),
7795
+ manifest
7796
+ };
7855
7797
  }
7856
7798
  return null;
7857
7799
  }
7858
- function validateHookTriggers(pluginPath, manifest, relEntry, exported, manifestView) {
7800
+ function validateHookTriggers(pluginPath, pluginId, manifest, relEntry, exported, manifestView) {
7859
7801
  const triggers = manifestView["triggers"];
7860
7802
  const hookId = exported["id"] ?? "?";
7861
7803
  if (!Array.isArray(triggers) || triggers.length === 0) {
7862
7804
  return {
7863
7805
  ...fail(
7864
7806
  pluginPath,
7865
- manifest.id,
7807
+ pluginId,
7866
7808
  "invalid-manifest",
7867
7809
  tx(PLUGIN_LOADER_TEXTS.invalidManifestHookEmptyTriggers, { hookId })
7868
7810
  ),
@@ -7874,7 +7816,7 @@ function validateHookTriggers(pluginPath, manifest, relEntry, exported, manifest
7874
7816
  return {
7875
7817
  ...fail(
7876
7818
  pluginPath,
7877
- manifest.id,
7819
+ pluginId,
7878
7820
  "invalid-manifest",
7879
7821
  tx(PLUGIN_LOADER_TEXTS.invalidManifestHookUnknownTrigger, {
7880
7822
  hookId,
@@ -7888,9 +7830,135 @@ function validateHookTriggers(pluginPath, manifest, relEntry, exported, manifest
7888
7830
  }
7889
7831
  return null;
7890
7832
  }
7833
+ function validateActionFileConventions(pluginPath, pluginId, manifest, relEntry, entryAbsPath, manifestView) {
7834
+ const actionDir = dirname9(entryAbsPath);
7835
+ const reportSchemaPath = join7(actionDir, "report.schema.json");
7836
+ const promptPath = join7(actionDir, "prompt.md");
7837
+ const mode = isRecord(manifestView) && typeof manifestView["mode"] === "string" ? manifestView["mode"] : "deterministic";
7838
+ if (!existsSync11(reportSchemaPath)) {
7839
+ return {
7840
+ ...fail(
7841
+ pluginPath,
7842
+ pluginId,
7843
+ "load-error",
7844
+ `Action at \`${relEntry}\` is missing \`report.schema.json\` in its folder (structure-as-truth: every Action carries a report schema by convention).`
7845
+ ),
7846
+ manifest
7847
+ };
7848
+ }
7849
+ const promptExists = existsSync11(promptPath);
7850
+ if (mode === "probabilistic" && !promptExists) {
7851
+ return {
7852
+ ...fail(
7853
+ pluginPath,
7854
+ pluginId,
7855
+ "load-error",
7856
+ `Probabilistic Action at \`${relEntry}\` is missing \`prompt.md\` in its folder (structure-as-truth: probabilistic Actions carry a prompt template by convention).`
7857
+ ),
7858
+ manifest
7859
+ };
7860
+ }
7861
+ if (mode === "deterministic" && promptExists) {
7862
+ return {
7863
+ ...fail(
7864
+ pluginPath,
7865
+ pluginId,
7866
+ "invalid-manifest",
7867
+ `Deterministic Action at \`${relEntry}\` carries an unexpected \`prompt.md\` (delete the file or switch \`mode\` to \`'probabilistic'\`).`
7868
+ ),
7869
+ manifest
7870
+ };
7871
+ }
7872
+ return null;
7873
+ }
7874
+ function discoverProviderKinds(pluginPath, pluginId, manifest, relEntry, validatorForKind) {
7875
+ const kindsRoot = join7(pluginPath, "kinds");
7876
+ let entries;
7877
+ try {
7878
+ entries = nodeFs.readdirSync(kindsRoot);
7879
+ } catch {
7880
+ return { ok: true, kinds: {} };
7881
+ }
7882
+ const out = {};
7883
+ for (const entry of entries.sort()) {
7884
+ if (entry.startsWith(".")) continue;
7885
+ const kindDir = join7(kindsRoot, entry);
7886
+ if (!isDirectorySafe(kindDir, nodeFs.statSync)) continue;
7887
+ const result = loadOneProviderKind({
7888
+ pluginPath,
7889
+ pluginId,
7890
+ manifest,
7891
+ relEntry,
7892
+ entry,
7893
+ kindDir,
7894
+ validatorForKind
7895
+ });
7896
+ if (!result.ok) return result;
7897
+ out[entry] = result.kind;
7898
+ }
7899
+ return { ok: true, kinds: out };
7900
+ }
7901
+ function loadOneProviderKind(opts) {
7902
+ const schemaJson = readJsonFile(join7(opts.kindDir, "schema.json"));
7903
+ if ("error" in schemaJson) {
7904
+ return providerKindFailure(opts, "load-error", "schema.json", schemaJson.error);
7905
+ }
7906
+ const kindJson = readJsonFile(join7(opts.kindDir, "kind.json"));
7907
+ if ("error" in kindJson) {
7908
+ return providerKindFailure(opts, "invalid-manifest", "kind.json", kindJson.error);
7909
+ }
7910
+ const validation = opts.validatorForKind(kindJson.value);
7911
+ if (!validation.ok) {
7912
+ return {
7913
+ ok: false,
7914
+ failure: {
7915
+ ...fail(
7916
+ opts.pluginPath,
7917
+ opts.pluginId,
7918
+ "invalid-manifest",
7919
+ `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.`
7920
+ ),
7921
+ manifest: opts.manifest
7922
+ }
7923
+ };
7924
+ }
7925
+ const ui = isRecord(kindJson.value) ? kindJson.value["ui"] : void 0;
7926
+ return {
7927
+ ok: true,
7928
+ kind: { schema: `./kinds/${opts.entry}/schema.json`, schemaJson: schemaJson.value, ui }
7929
+ };
7930
+ }
7931
+ function readJsonFile(path) {
7932
+ try {
7933
+ return { value: JSON.parse(nodeFs.readFileSync(path, "utf8")) };
7934
+ } catch (err) {
7935
+ return { error: describe(err) };
7936
+ }
7937
+ }
7938
+ function providerKindFailure(opts, status, fileName, errDescription) {
7939
+ return {
7940
+ ok: false,
7941
+ failure: {
7942
+ ...fail(
7943
+ opts.pluginPath,
7944
+ opts.pluginId,
7945
+ status,
7946
+ `Provider kind \`${opts.entry}\` (declared at \`${opts.relEntry}\`) is missing or has an unparseable \`kinds/${opts.entry}/${fileName}\` (${errDescription}).`
7947
+ ),
7948
+ manifest: opts.manifest
7949
+ }
7950
+ };
7951
+ }
7952
+ function isDirectorySafe(path, statSync11) {
7953
+ try {
7954
+ return statSync11(path).isDirectory();
7955
+ } catch {
7956
+ return false;
7957
+ }
7958
+ }
7891
7959
 
7892
7960
  // kernel/adapters/plugin-loader/storage-schemas.ts
7893
- import { readFileSync as readFileSync10 } from "fs";
7961
+ import { readFileSync as readFileSync11 } from "fs";
7894
7962
  import { resolve as resolve15 } from "path";
7895
7963
  import { Ajv2020 as Ajv20206 } from "ajv/dist/2020.js";
7896
7964
 
@@ -7898,7 +7966,7 @@ import { Ajv2020 as Ajv20206 } from "ajv/dist/2020.js";
7898
7966
  var KV_SCHEMA_KEY = "__kv__";
7899
7967
 
7900
7968
  // kernel/adapters/plugin-loader/storage-schemas.ts
7901
- function loadStorageSchemas(pluginPath, manifest) {
7969
+ function loadStorageSchemas(pluginPath, pluginId, manifest) {
7902
7970
  const storage = manifest.storage;
7903
7971
  if (!storage) return { ok: true };
7904
7972
  if (storage.mode === "kv") {
@@ -7908,7 +7976,7 @@ function loadStorageSchemas(pluginPath, manifest) {
7908
7976
  const reason = tx(
7909
7977
  compiled.phase === "read" ? PLUGIN_LOADER_TEXTS.loadErrorStorageKvSchemaRead : PLUGIN_LOADER_TEXTS.loadErrorStorageKvSchemaCompile,
7910
7978
  {
7911
- pluginId: manifest.id,
7979
+ pluginId,
7912
7980
  schemaPath: storage.schema,
7913
7981
  errDescription: compiled.errDescription
7914
7982
  }
@@ -7935,7 +8003,7 @@ function loadStorageSchemas(pluginPath, manifest) {
7935
8003
  const reason = tx(
7936
8004
  compiled.phase === "read" ? PLUGIN_LOADER_TEXTS.loadErrorStorageSchemaRead : PLUGIN_LOADER_TEXTS.loadErrorStorageSchemaCompile,
7937
8005
  {
7938
- pluginId: manifest.id,
8006
+ pluginId,
7939
8007
  table,
7940
8008
  schemaPath: relPath,
7941
8009
  errDescription: compiled.errDescription
@@ -7958,7 +8026,7 @@ function compilePluginSchema(pluginPath, relPath) {
7958
8026
  const abs = resolve15(pluginPath, relPath);
7959
8027
  let raw;
7960
8028
  try {
7961
- raw = JSON.parse(readFileSync10(abs, "utf8"));
8029
+ raw = JSON.parse(readFileSync11(abs, "utf8"));
7962
8030
  } catch (err) {
7963
8031
  return { ok: false, phase: "read", errDescription: describe(err) };
7964
8032
  }
@@ -7992,11 +8060,11 @@ var PluginLoader = class {
7992
8060
  discoverPaths() {
7993
8061
  const out = [];
7994
8062
  for (const root of this.#options.searchPaths) {
7995
- if (!existsSync11(root)) continue;
7996
- for (const entry of readdirSync3(root, { withFileTypes: true })) {
8063
+ if (!existsSync12(root)) continue;
8064
+ for (const entry of readdirSync4(root, { withFileTypes: true })) {
7997
8065
  if (!entry.isDirectory()) continue;
7998
- const candidate = join7(root, entry.name);
7999
- if (existsSync11(join7(candidate, "plugin.json"))) {
8066
+ const candidate = join8(root, entry.name);
8067
+ if (existsSync12(join8(candidate, "plugin.json"))) {
8000
8068
  out.push(resolve16(candidate));
8001
8069
  }
8002
8070
  }
@@ -8006,7 +8074,7 @@ var PluginLoader = class {
8006
8074
  /**
8007
8075
  * Full pass, discover every plugin, attempt to load each, then apply
8008
8076
  * the cross-root id-collision pass over the results. Two plugins that
8009
- * survived their individual load with the same `manifest.id` both get
8077
+ * survived their individual load with the same `pluginId` both get
8010
8078
  * downgraded to status `id-collision` (no precedence, the spec is
8011
8079
  * explicit that "no extension is privileged"). Plugins that already
8012
8080
  * failed their individual load (`invalid-manifest` /
@@ -8026,61 +8094,69 @@ var PluginLoader = class {
8026
8094
  /**
8027
8095
  * Load a single plugin from its directory. Never throws, a failure is
8028
8096
  * reported via the returned status.
8097
+ *
8098
+ * Cyclomatic count covers the four sequential phases (manifest parse,
8099
+ * enabled resolution, per-extension load loop, storage output-schemas
8100
+ * compile) plus their failure short-circuits. Splitting each phase
8101
+ * into a helper would scatter the return-on-failure pattern without
8102
+ * making the orchestration clearer.
8029
8103
  */
8030
8104
  // eslint-disable-next-line complexity
8031
8105
  async loadOne(pluginPath) {
8032
- const manifestResult = this.#parseAndValidateManifest(pluginPath);
8106
+ const pluginId = pathId(pluginPath);
8107
+ const manifestResult = this.#parseAndValidateManifest(pluginPath, pluginId);
8033
8108
  if (!manifestResult.ok) return manifestResult.failure;
8034
8109
  const manifest = manifestResult.manifest;
8035
- if (this.#options.resolveEnabled && !this.#options.resolveEnabled(manifest.id)) {
8110
+ const granularity = manifest.granularity ?? "extension";
8111
+ if (this.#options.resolveEnabled && !this.#options.resolveEnabled(pluginId)) {
8036
8112
  return {
8037
8113
  path: pluginPath,
8038
- id: manifest.id,
8114
+ id: pluginId,
8039
8115
  status: "disabled",
8040
8116
  manifest,
8041
- granularity: manifest.granularity ?? "bundle",
8117
+ granularity,
8042
8118
  reason: PLUGIN_LOADER_TEXTS.disabledByConfig
8043
8119
  };
8044
8120
  }
8045
8121
  const loaded = [];
8046
- for (const relEntry of manifest.extensions) {
8047
- const result = await this.#loadAndValidateExtensionEntry(pluginPath, manifest, relEntry);
8122
+ for (const relEntry of discoverExtensionEntries(pluginPath)) {
8123
+ const result = await this.#loadAndValidateExtensionEntry(pluginPath, pluginId, manifest, relEntry);
8048
8124
  if (!result.ok) return result.failure;
8049
8125
  loaded.push(result.extension);
8050
8126
  }
8051
- const storageSchemasResult = loadStorageSchemas(pluginPath, manifest);
8127
+ const storageSchemasResult = loadStorageSchemas(pluginPath, pluginId, manifest);
8052
8128
  if (!storageSchemasResult.ok) {
8053
8129
  return {
8054
- ...fail(pluginPath, manifest.id, "load-error", storageSchemasResult.reason),
8130
+ ...fail(pluginPath, pluginId, "load-error", storageSchemasResult.reason),
8055
8131
  manifest
8056
8132
  };
8057
8133
  }
8058
8134
  return {
8059
8135
  path: pluginPath,
8060
- id: manifest.id,
8136
+ id: pluginId,
8061
8137
  status: "enabled",
8062
8138
  manifest,
8063
- granularity: manifest.granularity ?? "bundle",
8139
+ granularity,
8064
8140
  extensions: loaded,
8065
8141
  ...storageSchemasResult.schemas ? { storageSchemas: storageSchemasResult.schemas } : {}
8066
8142
  };
8067
8143
  }
8068
8144
  /**
8069
8145
  * Phase 1 of `loadOne`, read `plugin.json`, AJV-validate the manifest,
8070
- * enforce the directory-name == manifest.id structural rule, and check
8146
+ * enforce the directory-name == pluginId structural rule, and check
8071
8147
  * specCompat (range syntax + satisfies the installed spec version).
8072
8148
  * Returns either the validated manifest or an `IDiscoveredPlugin` with
8073
8149
  * the appropriate failure status.
8074
8150
  */
8075
- #parseAndValidateManifest(pluginPath) {
8076
- const manifestPath = join7(pluginPath, "plugin.json");
8151
+ #parseAndValidateManifest(pluginPath, pluginId) {
8152
+ const manifestPath = join8(pluginPath, "plugin.json");
8077
8153
  let raw;
8078
8154
  try {
8079
- raw = JSON.parse(readFileSync11(manifestPath, "utf8"));
8155
+ raw = JSON.parse(readFileSync12(manifestPath, "utf8"));
8080
8156
  } catch (err) {
8081
8157
  return { ok: false, failure: fail(
8082
8158
  pluginPath,
8083
- pathId(pluginPath),
8159
+ pluginId,
8084
8160
  "invalid-manifest",
8085
8161
  tx(PLUGIN_LOADER_TEXTS.invalidManifestJsonParse, {
8086
8162
  manifestPath,
@@ -8092,7 +8168,7 @@ var PluginLoader = class {
8092
8168
  if (!manifestResult.ok) {
8093
8169
  return { ok: false, failure: fail(
8094
8170
  pluginPath,
8095
- pathId(pluginPath),
8171
+ pluginId,
8096
8172
  "invalid-manifest",
8097
8173
  tx(PLUGIN_LOADER_TEXTS.invalidManifestAjv, {
8098
8174
  manifestPath,
@@ -8101,26 +8177,11 @@ var PluginLoader = class {
8101
8177
  ) };
8102
8178
  }
8103
8179
  const manifest = manifestResult.data;
8104
- const dirName = pathId(pluginPath);
8105
- if (dirName !== manifest.id) {
8106
- return { ok: false, failure: {
8107
- ...fail(
8108
- pluginPath,
8109
- manifest.id,
8110
- "invalid-manifest",
8111
- tx(PLUGIN_LOADER_TEXTS.invalidManifestDirMismatch, {
8112
- dirName,
8113
- manifestId: manifest.id
8114
- })
8115
- ),
8116
- manifest
8117
- } };
8118
- }
8119
8180
  if (!semver.validRange(manifest.specCompat)) {
8120
8181
  return { ok: false, failure: {
8121
8182
  ...fail(
8122
8183
  pluginPath,
8123
- manifest.id,
8184
+ pluginId,
8124
8185
  "invalid-manifest",
8125
8186
  tx(PLUGIN_LOADER_TEXTS.invalidSpecCompat, { specCompat: manifest.specCompat })
8126
8187
  ),
@@ -8130,10 +8191,10 @@ var PluginLoader = class {
8130
8191
  if (!semver.satisfies(this.#options.specVersion, manifest.specCompat, { includePrerelease: true })) {
8131
8192
  return { ok: false, failure: {
8132
8193
  path: pluginPath,
8133
- id: manifest.id,
8194
+ id: pluginId,
8134
8195
  status: "incompatible-spec",
8135
8196
  manifest,
8136
- granularity: manifest.granularity ?? "bundle",
8197
+ granularity: manifest.granularity ?? "extension",
8137
8198
  reason: tx(PLUGIN_LOADER_TEXTS.incompatibleSpec, {
8138
8199
  installedSpecVersion: this.#options.specVersion,
8139
8200
  specCompat: manifest.specCompat
@@ -8156,12 +8217,12 @@ var PluginLoader = class {
8156
8217
  // splitting per sub-check would multiply the discriminated-union
8157
8218
  // boilerplate without making the validation pipeline clearer.
8158
8219
  // eslint-disable-next-line complexity
8159
- async #loadAndValidateExtensionEntry(pluginPath, manifest, relEntry) {
8220
+ async #loadAndValidateExtensionEntry(pluginPath, pluginId, manifest, relEntry) {
8160
8221
  if (!isInsidePlugin(pluginPath, relEntry)) {
8161
8222
  return { ok: false, failure: {
8162
8223
  ...fail(
8163
8224
  pluginPath,
8164
- manifest.id,
8225
+ pluginId,
8165
8226
  "invalid-manifest",
8166
8227
  tx(PLUGIN_LOADER_TEXTS.loadErrorPathEscapesPlugin, { relEntry, pluginPath })
8167
8228
  ),
@@ -8169,11 +8230,11 @@ var PluginLoader = class {
8169
8230
  } };
8170
8231
  }
8171
8232
  const abs = resolve16(pluginPath, relEntry);
8172
- if (!existsSync11(abs)) {
8233
+ if (!existsSync12(abs)) {
8173
8234
  return { ok: false, failure: {
8174
8235
  ...fail(
8175
8236
  pluginPath,
8176
- manifest.id,
8237
+ pluginId,
8177
8238
  "load-error",
8178
8239
  tx(PLUGIN_LOADER_TEXTS.loadErrorFileNotFound, { relEntry, abs })
8179
8240
  ),
@@ -8187,7 +8248,7 @@ var PluginLoader = class {
8187
8248
  return { ok: false, failure: {
8188
8249
  ...fail(
8189
8250
  pluginPath,
8190
- manifest.id,
8251
+ pluginId,
8191
8252
  "load-error",
8192
8253
  tx(PLUGIN_LOADER_TEXTS.loadErrorImportFailed, {
8193
8254
  relEntry,
@@ -8198,11 +8259,11 @@ var PluginLoader = class {
8198
8259
  } };
8199
8260
  }
8200
8261
  const exported = extractDefault(mod);
8201
- if (!isRecord(exported) || typeof exported["kind"] !== "string") {
8262
+ if (!isRecord(exported)) {
8202
8263
  return { ok: false, failure: {
8203
8264
  ...fail(
8204
8265
  pluginPath,
8205
- manifest.id,
8266
+ pluginId,
8206
8267
  "load-error",
8207
8268
  tx(PLUGIN_LOADER_TEXTS.loadErrorMissingKind, {
8208
8269
  relEntry,
@@ -8212,16 +8273,32 @@ var PluginLoader = class {
8212
8273
  manifest
8213
8274
  } };
8214
8275
  }
8215
- const kind = exported["kind"];
8216
- if (!KNOWN_KINDS.has(kind)) {
8276
+ const [pathKindDir, pathId2] = relEntry.split("/");
8277
+ const kindFromPath = pathKindDir && pathKindDir.endsWith("s") ? pathKindDir.slice(0, -1) : void 0;
8278
+ if (!kindFromPath || !KNOWN_KINDS.has(kindFromPath)) {
8217
8279
  return { ok: false, failure: {
8218
8280
  ...fail(
8219
8281
  pluginPath,
8220
- manifest.id,
8221
- "load-error",
8282
+ pluginId,
8283
+ "invalid-manifest",
8222
8284
  tx(PLUGIN_LOADER_TEXTS.loadErrorUnknownKind, {
8223
8285
  relEntry,
8224
- kindReceived: String(exported["kind"]),
8286
+ kindReceived: String(pathKindDir ?? "(missing)"),
8287
+ knownKindsList: KNOWN_KINDS_LIST
8288
+ })
8289
+ ),
8290
+ manifest
8291
+ } };
8292
+ }
8293
+ const kind = kindFromPath;
8294
+ if (!pathId2) {
8295
+ return { ok: false, failure: {
8296
+ ...fail(
8297
+ pluginPath,
8298
+ pluginId,
8299
+ "invalid-manifest",
8300
+ tx(PLUGIN_LOADER_TEXTS.loadErrorMissingKind, {
8301
+ relEntry,
8225
8302
  knownKindsList: KNOWN_KINDS_LIST
8226
8303
  })
8227
8304
  ),
@@ -8229,16 +8306,16 @@ var PluginLoader = class {
8229
8306
  } };
8230
8307
  }
8231
8308
  const declaredPluginId = exported["pluginId"];
8232
- if (typeof declaredPluginId === "string" && declaredPluginId !== manifest.id) {
8309
+ if (typeof declaredPluginId === "string" && declaredPluginId !== pluginId) {
8233
8310
  return { ok: false, failure: {
8234
8311
  ...fail(
8235
8312
  pluginPath,
8236
- manifest.id,
8313
+ pluginId,
8237
8314
  "invalid-manifest",
8238
8315
  tx(PLUGIN_LOADER_TEXTS.loadErrorPluginIdMismatch, {
8239
8316
  relEntry,
8240
8317
  declared: declaredPluginId,
8241
- manifestId: manifest.id
8318
+ manifestId: pluginId
8242
8319
  })
8243
8320
  ),
8244
8321
  manifest
@@ -8246,7 +8323,7 @@ var PluginLoader = class {
8246
8323
  }
8247
8324
  const manifestView = stripFunctionsAndPluginId(exported);
8248
8325
  if (kind === "hook") {
8249
- const hookFailure = validateHookTriggers(pluginPath, manifest, relEntry, exported, manifestView);
8326
+ const hookFailure = validateHookTriggers(pluginPath, pluginId, manifest, relEntry, exported, manifestView);
8250
8327
  if (hookFailure) return { ok: false, failure: hookFailure };
8251
8328
  }
8252
8329
  const extValidator = this.#options.validators.validatorForExtension(kind);
@@ -8255,7 +8332,7 @@ var PluginLoader = class {
8255
8332
  return { ok: false, failure: {
8256
8333
  ...fail(
8257
8334
  pluginPath,
8258
- manifest.id,
8335
+ pluginId,
8259
8336
  "invalid-manifest",
8260
8337
  tx(PLUGIN_LOADER_TEXTS.invalidManifestExtensionShape, { relEntry, kind, errors })
8261
8338
  ),
@@ -8264,16 +8341,49 @@ var PluginLoader = class {
8264
8341
  }
8265
8342
  const contribFailure = validateAnnotationContributions(
8266
8343
  pluginPath,
8344
+ pluginId,
8267
8345
  manifest,
8268
8346
  relEntry,
8269
8347
  manifestView
8270
8348
  );
8271
8349
  if (contribFailure) return { ok: false, failure: contribFailure };
8272
- const instance = isRecord(exported) ? { ...exported, pluginId: manifest.id } : exported;
8350
+ if (kind === "action") {
8351
+ const actionFailure = validateActionFileConventions(
8352
+ pluginPath,
8353
+ pluginId,
8354
+ manifest,
8355
+ relEntry,
8356
+ abs,
8357
+ manifestView
8358
+ );
8359
+ if (actionFailure) return { ok: false, failure: actionFailure };
8360
+ }
8361
+ let discoveredKinds;
8362
+ if (kind === "provider") {
8363
+ const kindsResult = discoverProviderKinds(
8364
+ pluginPath,
8365
+ pluginId,
8366
+ manifest,
8367
+ relEntry,
8368
+ (data) => {
8369
+ const v = this.#options.validators.validate("extension-provider-kind", data);
8370
+ if (v.ok) return { ok: true, errors: "" };
8371
+ return { ok: false, errors: v.errors };
8372
+ }
8373
+ );
8374
+ if (!kindsResult.ok) return { ok: false, failure: kindsResult.failure };
8375
+ if (Object.keys(kindsResult.kinds).length > 0) discoveredKinds = kindsResult.kinds;
8376
+ }
8377
+ const instance = { ...exported, pluginId, id: pathId2, kind };
8378
+ if (kind === "formatter") instance["formatId"] = pathId2;
8379
+ if (kind === "provider" && discoveredKinds) {
8380
+ const inlineKinds = isRecord(exported["kinds"]) ? exported["kinds"] : {};
8381
+ instance["kinds"] = { ...inlineKinds, ...discoveredKinds };
8382
+ }
8273
8383
  return { ok: true, extension: {
8274
8384
  kind,
8275
- id: exported["id"],
8276
- pluginId: manifest.id,
8385
+ id: pathId2,
8386
+ pluginId,
8277
8387
  version: exported["version"],
8278
8388
  entryPath: abs,
8279
8389
  module: mod,
@@ -8281,11 +8391,64 @@ var PluginLoader = class {
8281
8391
  } };
8282
8392
  }
8283
8393
  };
8394
+ var KIND_DIR_NAMES = [
8395
+ "providers",
8396
+ "extractors",
8397
+ "analyzers",
8398
+ "actions",
8399
+ "formatters",
8400
+ "hooks"
8401
+ ];
8402
+ var INDEX_CANDIDATES = [
8403
+ "index.js",
8404
+ "index.mjs",
8405
+ "index.ts"
8406
+ ];
8407
+ function discoverExtensionEntries(pluginPath) {
8408
+ const out = [];
8409
+ for (const kindDir of KIND_DIR_NAMES) {
8410
+ collectKindEntries(pluginPath, kindDir, out);
8411
+ }
8412
+ return out;
8413
+ }
8414
+ function collectKindEntries(pluginPath, kindDir, out) {
8415
+ const kindAbs = resolve16(pluginPath, kindDir);
8416
+ if (!existsSync12(kindAbs)) return;
8417
+ let entries;
8418
+ try {
8419
+ entries = readdirSync4(kindAbs);
8420
+ } catch {
8421
+ return;
8422
+ }
8423
+ entries.sort();
8424
+ for (const entry of entries) {
8425
+ if (entry.startsWith(".")) continue;
8426
+ const entryAbs = resolve16(kindAbs, entry);
8427
+ if (!isDirectorySafe2(entryAbs)) continue;
8428
+ const candidate = findIndexCandidate(entryAbs);
8429
+ if (candidate !== null) {
8430
+ out.push(`${kindDir}/${entry}/${candidate}`);
8431
+ }
8432
+ }
8433
+ }
8434
+ function isDirectorySafe2(path) {
8435
+ try {
8436
+ return statSync2(path).isDirectory();
8437
+ } catch {
8438
+ return false;
8439
+ }
8440
+ }
8441
+ function findIndexCandidate(entryAbs) {
8442
+ for (const candidate of INDEX_CANDIDATES) {
8443
+ if (existsSync12(resolve16(entryAbs, candidate))) return candidate;
8444
+ }
8445
+ return null;
8446
+ }
8284
8447
  function installedSpecVersion() {
8285
8448
  const require2 = createRequire5(import.meta.url);
8286
8449
  const indexPath = require2.resolve("@skill-map/spec/index.json");
8287
8450
  const pkgPath = resolve16(indexPath, "..", "package.json");
8288
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
8451
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
8289
8452
  return pkg.version;
8290
8453
  }
8291
8454
 
@@ -8382,11 +8545,11 @@ async function buildEnabledResolver(ctx) {
8382
8545
 
8383
8546
  // kernel/scan/walk-content.ts
8384
8547
  import { readFile, readdir, lstat } from "fs/promises";
8385
- import { join as join8, relative as relative2, sep as sep2 } from "path";
8548
+ import { join as join9, relative as relative2, sep as sep2 } from "path";
8386
8549
 
8387
8550
  // kernel/scan/ignore.ts
8388
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
8389
- import { dirname as dirname9, resolve as resolve17 } from "path";
8551
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
8552
+ import { dirname as dirname10, resolve as resolve17 } from "path";
8390
8553
  import { fileURLToPath as fileURLToPath2 } from "url";
8391
8554
  import ignoreFactory from "ignore";
8392
8555
  function buildIgnoreFilter(opts = {}) {
@@ -8416,9 +8579,9 @@ function loadBundledIgnoreText() {
8416
8579
  }
8417
8580
  function readIgnoreFileText(scopeRoot) {
8418
8581
  const path = resolve17(scopeRoot, ".skillmapignore");
8419
- if (!existsSync12(path)) return void 0;
8582
+ if (!existsSync13(path)) return void 0;
8420
8583
  try {
8421
- return readFileSync12(path, "utf8");
8584
+ return readFileSync13(path, "utf8");
8422
8585
  } catch {
8423
8586
  return void 0;
8424
8587
  }
@@ -8442,7 +8605,7 @@ function loadDefaultsText() {
8442
8605
  return cachedDefaults;
8443
8606
  }
8444
8607
  function readDefaultsFromDisk() {
8445
- const here = dirname9(fileURLToPath2(import.meta.url));
8608
+ const here = dirname10(fileURLToPath2(import.meta.url));
8446
8609
  const candidates = [
8447
8610
  resolve17(here, "../../config/defaults/skillmapignore"),
8448
8611
  // src/kernel/scan/ → src/config/defaults/
@@ -8451,9 +8614,9 @@ function readDefaultsFromDisk() {
8451
8614
  resolve17(here, "config/defaults/skillmapignore")
8452
8615
  ];
8453
8616
  for (const candidate of candidates) {
8454
- if (existsSync12(candidate)) {
8617
+ if (existsSync13(candidate)) {
8455
8618
  try {
8456
- return readFileSync12(candidate, "utf8");
8619
+ return readFileSync13(candidate, "utf8");
8457
8620
  } catch {
8458
8621
  }
8459
8622
  }
@@ -8461,7 +8624,7 @@ function readDefaultsFromDisk() {
8461
8624
  return "";
8462
8625
  }
8463
8626
 
8464
- // built-in-plugins/parsers/frontmatter-yaml/index.ts
8627
+ // plugins/core/parsers/frontmatter-yaml/index.ts
8465
8628
  import yaml3 from "js-yaml";
8466
8629
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
8467
8630
  var frontmatterYamlParser = {
@@ -8496,7 +8659,7 @@ function sanitiseParseErrorMessage(err) {
8496
8659
  return raw.replace(/[-]+/g, " ").replace(/\s+/g, " ").trim();
8497
8660
  }
8498
8661
 
8499
- // built-in-plugins/parsers/plain/index.ts
8662
+ // plugins/core/parsers/plain/index.ts
8500
8663
  var plainParser = {
8501
8664
  id: "plain",
8502
8665
  parse(raw, _path) {
@@ -8559,7 +8722,7 @@ async function* walkRoot(root, current, filter, extensions) {
8559
8722
  }
8560
8723
  for (const entry of entries) {
8561
8724
  const name = entry.name;
8562
- const full = join8(current, name);
8725
+ const full = join9(current, name);
8563
8726
  const rel = relative2(root, full).split(sep2).join("/");
8564
8727
  if (filter.ignores(rel)) continue;
8565
8728
  if (entry.isSymbolicLink()) continue;
@@ -8605,7 +8768,7 @@ function resolveProviderWalk(provider) {
8605
8768
  // kernel/extensions/collect-view-contributions.ts
8606
8769
  function collectViewContributions(pluginId, extensionId, instance, out, options = {}) {
8607
8770
  if (typeof instance !== "object" || instance === null) return;
8608
- const raw = instance["viewContributions"];
8771
+ const raw = instance["ui"];
8609
8772
  if (typeof raw !== "object" || raw === null) return;
8610
8773
  const exclude = options.excludeQualifiedIds;
8611
8774
  for (const [contributionId, value] of Object.entries(raw)) {
@@ -8649,6 +8812,7 @@ function bucketLoaded(loaded, bundle) {
8649
8812
  pluginId: ext.pluginId,
8650
8813
  kind: ext.kind,
8651
8814
  version: ext.version,
8815
+ description: instance.description ?? "",
8652
8816
  ...ext.entryPath ? { entry: ext.entryPath } : {}
8653
8817
  });
8654
8818
  collectAnnotationContributions(ext.pluginId, instance, bundle.annotationContributions);
@@ -8656,21 +8820,25 @@ function bucketLoaded(loaded, bundle) {
8656
8820
  }
8657
8821
  }
8658
8822
  function collectAnnotationContributions(pluginId, instance, out) {
8659
- if (typeof instance !== "object" || instance === null) return;
8660
- const raw = instance["annotationContributions"];
8661
- if (typeof raw !== "object" || raw === null) return;
8662
- for (const [key, value] of Object.entries(raw)) {
8663
- if (typeof value !== "object" || value === null) continue;
8664
- const entry = value;
8665
- if (typeof entry.schema !== "object" || entry.schema === null) continue;
8666
- out.push({
8667
- pluginId,
8668
- key,
8669
- location: entry.location ?? "namespaced",
8670
- ownership: entry.ownership ?? "shared",
8671
- schema: entry.schema
8672
- });
8673
- }
8823
+ const row = tryReadAnnotationRow(pluginId, instance);
8824
+ if (row !== null) out.push(row);
8825
+ }
8826
+ function tryReadAnnotationRow(pluginId, instance) {
8827
+ if (typeof instance !== "object" || instance === null) return null;
8828
+ const inst = instance;
8829
+ const raw = inst["annotation"];
8830
+ if (typeof raw !== "object" || raw === null) return null;
8831
+ const entry = raw;
8832
+ if (typeof entry.schema !== "object" || entry.schema === null) return null;
8833
+ const extId = inst["id"];
8834
+ if (typeof extId !== "string" || extId.length === 0) return null;
8835
+ return {
8836
+ pluginId,
8837
+ key: extId,
8838
+ location: entry.location ?? "namespaced",
8839
+ ownership: entry.ownership ?? "shared",
8840
+ schema: entry.schema
8841
+ };
8674
8842
  }
8675
8843
  function isExtensionInstance(v) {
8676
8844
  return typeof v === "object" && v !== null && typeof v["id"] === "string" && typeof v["kind"] === "string" && typeof v["version"] === "string";
@@ -8821,7 +8989,7 @@ function collectRegisteredContributionKeys(composed) {
8821
8989
  const keys = /* @__PURE__ */ new Set();
8822
8990
  if (!composed) return keys;
8823
8991
  for (const ext of [...composed.extractors, ...composed.analyzers]) {
8824
- const raw = ext.viewContributions;
8992
+ const raw = ext.ui;
8825
8993
  if (typeof raw !== "object" || raw === null) continue;
8826
8994
  for (const [contributionId, value] of Object.entries(raw)) {
8827
8995
  if (typeof value !== "object" || value === null) continue;
@@ -9162,7 +9330,7 @@ function trimRedundantPath(message, primary) {
9162
9330
  }
9163
9331
 
9164
9332
  // cli/commands/config.ts
9165
- import { existsSync as existsSync13 } from "fs";
9333
+ import { existsSync as existsSync14 } from "fs";
9166
9334
  import { Command as Command4, Option as Option4 } from "clipanion";
9167
9335
 
9168
9336
  // cli/util/path-display.ts
@@ -9690,7 +9858,7 @@ var ConfigResetCommand = class extends SmCommand {
9690
9858
  const path = targetSettingsPath2(target, ctx.cwd);
9691
9859
  const ansi = this.ansiFor("stdout");
9692
9860
  const okGlyph = ansi.green("\u2713");
9693
- if (!existsSync13(path)) {
9861
+ if (!existsSync14(path)) {
9694
9862
  this.printer.data(
9695
9863
  tx(CONFIG_TEXTS.unsetNoOverride, {
9696
9864
  glyph: okGlyph,
@@ -9765,16 +9933,16 @@ var CONFIG_COMMANDS = [
9765
9933
  ];
9766
9934
 
9767
9935
  // cli/commands/conformance.ts
9768
- import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
9769
- import { dirname as dirname11, resolve as resolve21 } from "path";
9936
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
9937
+ import { dirname as dirname12, resolve as resolve21 } from "path";
9770
9938
  import { fileURLToPath as fileURLToPath4 } from "url";
9771
9939
  import { Command as Command5, Option as Option5 } from "clipanion";
9772
9940
 
9773
9941
  // conformance/index.ts
9774
9942
  import { spawnSync as spawnSync2 } from "child_process";
9775
- import { cpSync, existsSync as existsSync14, mkdtempSync, readdirSync as readdirSync4, readFileSync as readFileSync13, rmSync, statSync } from "fs";
9943
+ import { cpSync, existsSync as existsSync15, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync, statSync as statSync3 } from "fs";
9776
9944
  import { tmpdir } from "os";
9777
- import { isAbsolute as isAbsolute5, join as join9, relative as relative3, resolve as resolve19 } from "path";
9945
+ import { isAbsolute as isAbsolute5, join as join10, relative as relative3, resolve as resolve19 } from "path";
9778
9946
 
9779
9947
  // conformance/i18n/runner.texts.ts
9780
9948
  var CONFORMANCE_RUNNER_TEXTS = {
@@ -9808,11 +9976,11 @@ function disableEnv(setup) {
9808
9976
  return env;
9809
9977
  }
9810
9978
  function runConformanceCase(options) {
9811
- const raw = readFileSync13(options.casePath, "utf8");
9979
+ const raw = readFileSync14(options.casePath, "utf8");
9812
9980
  const c = JSON.parse(raw);
9813
- const fixturesRoot = options.fixturesRoot ?? join9(options.specRoot, "conformance", "fixtures");
9981
+ const fixturesRoot = options.fixturesRoot ?? join10(options.specRoot, "conformance", "fixtures");
9814
9982
  const safeId = c.id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32);
9815
- const scope = mkdtempSync(join9(tmpdir(), `sm-conformance-${safeId}-`));
9983
+ const scope = mkdtempSync(join10(tmpdir(), `sm-conformance-${safeId}-`));
9816
9984
  const setupEnv = disableEnv(c.setup);
9817
9985
  try {
9818
9986
  const priorFailure = runPriorScansSetup(c, options, scope, fixturesRoot, setupEnv);
@@ -9882,11 +10050,11 @@ function runPriorScansSetup(c, options, scope, fixturesRoot, setupEnv) {
9882
10050
  }
9883
10051
  function replaceFixture(scope, fixturesRoot, fixture) {
9884
10052
  assertContained2(fixturesRoot, fixture, "fixture");
9885
- for (const entry of readdirSync4(scope)) {
10053
+ for (const entry of readdirSync5(scope)) {
9886
10054
  if (entry === KERNEL_SKILL_MAP_DIR) continue;
9887
- rmSync(join9(scope, entry), { recursive: true, force: true });
10055
+ rmSync(join10(scope, entry), { recursive: true, force: true });
9888
10056
  }
9889
- const src = join9(fixturesRoot, fixture);
10057
+ const src = join10(fixturesRoot, fixture);
9890
10058
  cpSync(src, scope, { recursive: true });
9891
10059
  }
9892
10060
  function assertContained2(root, rel, label) {
@@ -9923,7 +10091,7 @@ function evaluateAssertion(a, ctx) {
9923
10091
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
9924
10092
  }
9925
10093
  const abs = resolve19(ctx.scope, a.path);
9926
- return existsSync14(abs) ? { ok: true, type: a.type } : {
10094
+ return existsSync15(abs) ? { ok: true, type: a.type } : {
9927
10095
  ok: false,
9928
10096
  type: a.type,
9929
10097
  reason: tx(CONFORMANCE_RUNNER_TEXTS.fileNotFound, { path: a.path })
@@ -9936,17 +10104,17 @@ function evaluateAssertion(a, ctx) {
9936
10104
  } catch (err) {
9937
10105
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
9938
10106
  }
9939
- const fixturePath = join9(ctx.fixturesRoot, a.fixture);
10107
+ const fixturePath = join10(ctx.fixturesRoot, a.fixture);
9940
10108
  const targetPath = resolve19(ctx.scope, a.path);
9941
- if (!existsSync14(targetPath)) {
10109
+ if (!existsSync15(targetPath)) {
9942
10110
  return {
9943
10111
  ok: false,
9944
10112
  type: a.type,
9945
10113
  reason: tx(CONFORMANCE_RUNNER_TEXTS.targetNotFound, { path: a.path })
9946
10114
  };
9947
10115
  }
9948
- const needle = readFileSync13(fixturePath);
9949
- const haystack = readFileSync13(targetPath);
10116
+ const needle = readFileSync14(fixturePath);
10117
+ const haystack = readFileSync14(targetPath);
9950
10118
  return haystack.includes(needle) ? { ok: true, type: a.type } : {
9951
10119
  ok: false,
9952
10120
  type: a.type,
@@ -10119,15 +10287,15 @@ var CONFORMANCE_TEXTS = {
10119
10287
  };
10120
10288
 
10121
10289
  // cli/util/conformance-scopes.ts
10122
- import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
10123
- import { dirname as dirname10, resolve as resolve20 } from "path";
10290
+ import { existsSync as existsSync16, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
10291
+ import { dirname as dirname11, resolve as resolve20 } from "path";
10124
10292
  import { createRequire as createRequire6 } from "module";
10125
10293
  import { fileURLToPath as fileURLToPath3 } from "url";
10126
10294
  function resolveSpecRoot4() {
10127
10295
  const require2 = createRequire6(import.meta.url);
10128
10296
  try {
10129
10297
  const indexPath = require2.resolve("@skill-map/spec/index.json");
10130
- return dirname10(indexPath);
10298
+ return dirname11(indexPath);
10131
10299
  } catch {
10132
10300
  throw new Error(
10133
10301
  "@skill-map/spec not resolvable: ensure the workspace is linked or the package is installed."
@@ -10135,19 +10303,19 @@ function resolveSpecRoot4() {
10135
10303
  }
10136
10304
  }
10137
10305
  function resolveCliWorkspaceRoot() {
10138
- const here = dirname10(fileURLToPath3(import.meta.url));
10306
+ const here = dirname11(fileURLToPath3(import.meta.url));
10139
10307
  let cursor = here;
10140
10308
  for (let depth = 0; depth < 6; depth += 1) {
10141
- const candidate = resolve20(cursor, "built-in-plugins", "providers");
10142
- if (existsSync15(candidate) && statSync2(candidate).isDirectory()) {
10309
+ const candidate = resolve20(cursor, "plugins");
10310
+ if (existsSync16(candidate) && statSync4(candidate).isDirectory()) {
10143
10311
  return cursor;
10144
10312
  }
10145
- const parent = dirname10(cursor);
10313
+ const parent = dirname11(cursor);
10146
10314
  if (parent === cursor) break;
10147
10315
  cursor = parent;
10148
10316
  }
10149
10317
  throw new Error(
10150
- `sm conformance: built-in Provider conformance assets not found (expected a 'built-in-plugins/providers/' directory above ${here}). The bundled CLI may not yet copy the assets; run from the source workspace, or rebuild after enabling the asset-copy step.`
10318
+ `sm conformance: built-in Provider conformance assets not found (expected a 'plugins/' directory above ${here}). The bundled CLI may not yet copy the assets; run from the source workspace, or rebuild after enabling the asset-copy step.`
10151
10319
  );
10152
10320
  }
10153
10321
  function collectProviderScopes(specRoot) {
@@ -10158,16 +10326,33 @@ function collectProviderScopes(specRoot) {
10158
10326
  } catch {
10159
10327
  return out;
10160
10328
  }
10161
- const providersRoot = resolve20(workspaceRoot, "built-in-plugins", "providers");
10162
- if (!existsSync15(providersRoot)) return out;
10163
- for (const entry of readdirSync5(providersRoot)) {
10329
+ const pluginsRoot = resolve20(workspaceRoot, "plugins");
10330
+ if (!existsSync16(pluginsRoot)) return out;
10331
+ for (const bundleEntry of readdirSync6(pluginsRoot)) {
10332
+ const bundleDir = resolve20(pluginsRoot, bundleEntry);
10333
+ if (!isDir(bundleDir)) continue;
10334
+ const providersRoot = resolve20(bundleDir, "providers");
10335
+ if (!isDir(providersRoot)) continue;
10336
+ collectBundleProviderScopes(providersRoot, specRoot, out);
10337
+ }
10338
+ return out;
10339
+ }
10340
+ function isDir(path) {
10341
+ try {
10342
+ return existsSync16(path) && statSync4(path).isDirectory();
10343
+ } catch {
10344
+ return false;
10345
+ }
10346
+ }
10347
+ function collectBundleProviderScopes(providersRoot, specRoot, out) {
10348
+ for (const entry of readdirSync6(providersRoot)) {
10164
10349
  const providerDir = resolve20(providersRoot, entry);
10165
- if (!statSync2(providerDir).isDirectory()) continue;
10350
+ if (!isDir(providerDir)) continue;
10166
10351
  const conformanceDir = resolve20(providerDir, "conformance");
10167
- if (!existsSync15(conformanceDir)) continue;
10352
+ if (!existsSync16(conformanceDir)) continue;
10168
10353
  const casesDir = resolve20(conformanceDir, "cases");
10169
10354
  const fixturesDir = resolve20(conformanceDir, "fixtures");
10170
- if (!existsSync15(casesDir) || !existsSync15(fixturesDir)) continue;
10355
+ if (!existsSync16(casesDir) || !existsSync16(fixturesDir)) continue;
10171
10356
  out.push({
10172
10357
  id: `provider:${entry}`,
10173
10358
  kind: "provider",
@@ -10177,7 +10362,6 @@ function collectProviderScopes(specRoot) {
10177
10362
  specRoot
10178
10363
  });
10179
10364
  }
10180
- return out;
10181
10365
  }
10182
10366
  function specScope(specRoot) {
10183
10367
  return {
@@ -10206,8 +10390,8 @@ function selectConformanceScopes(scope) {
10206
10390
  return [match];
10207
10391
  }
10208
10392
  function listCaseFiles(scope) {
10209
- if (!existsSync15(scope.casesDir)) return [];
10210
- return readdirSync5(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve20(scope.casesDir, entry));
10393
+ if (!existsSync16(scope.casesDir)) return [];
10394
+ return readdirSync6(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve20(scope.casesDir, entry));
10211
10395
  }
10212
10396
 
10213
10397
  // cli/commands/conformance.ts
@@ -10221,12 +10405,12 @@ function formatAssertionFailureDetail(type, reason) {
10221
10405
  });
10222
10406
  }
10223
10407
  function resolveBinary() {
10224
- const here = dirname11(fileURLToPath4(import.meta.url));
10408
+ const here = dirname12(fileURLToPath4(import.meta.url));
10225
10409
  let cursor = here;
10226
10410
  for (let depth = 0; depth < 6; depth += 1) {
10227
10411
  const candidate = resolve21(cursor, "bin", "sm.js");
10228
- if (existsSync16(candidate)) return candidate;
10229
- const parent = dirname11(cursor);
10412
+ if (existsSync17(candidate)) return candidate;
10413
+ const parent = dirname12(cursor);
10230
10414
  if (parent === cursor) break;
10231
10415
  cursor = parent;
10232
10416
  }
@@ -10291,7 +10475,7 @@ var ConformanceRunCommand = class extends SmCommand {
10291
10475
  return ExitCode.Error;
10292
10476
  }
10293
10477
  const binary = resolveBinary();
10294
- if (!existsSync16(binary)) {
10478
+ if (!existsSync17(binary)) {
10295
10479
  if (this.json) {
10296
10480
  this.#emitJsonError(
10297
10481
  "internal",
@@ -10465,7 +10649,7 @@ function projectAssertionFailures(assertions) {
10465
10649
  }
10466
10650
  function readCaseId(casePath) {
10467
10651
  try {
10468
- const raw = readFileSync14(casePath, "utf8");
10652
+ const raw = readFileSync15(casePath, "utf8");
10469
10653
  const parsed = JSON.parse(raw);
10470
10654
  if (typeof parsed.id === "string") return parsed.id;
10471
10655
  } catch {
@@ -10483,7 +10667,7 @@ function writeStreamSnippet(stream, header, text) {
10483
10667
  var CONFORMANCE_COMMANDS = [ConformanceRunCommand];
10484
10668
 
10485
10669
  // cli/commands/db/backup.ts
10486
- import { dirname as dirname12, join as join10, resolve as resolve22 } from "path";
10670
+ import { dirname as dirname13, join as join11, resolve as resolve22 } from "path";
10487
10671
  import { Command as Command6, Option as Option6 } from "clipanion";
10488
10672
 
10489
10673
  // cli/i18n/db.texts.ts
@@ -10569,7 +10753,7 @@ var DbBackupCommand = class extends SmCommand {
10569
10753
  const exit = requireDbOrExit(path, this.context.stderr);
10570
10754
  if (exit !== null) return exit;
10571
10755
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10572
- const outPath = this.out ? resolve22(this.out) : join10(dirname12(path), "backups", `${ts}.db`);
10756
+ const outPath = this.out ? resolve22(this.out) : join11(dirname13(path), "backups", `${ts}.db`);
10573
10757
  await withSqlite({ databasePath: path, autoMigrate: false }, async (storage) => {
10574
10758
  storage.migrations.writeBackup(outPath);
10575
10759
  });
@@ -10586,7 +10770,7 @@ var DbBackupCommand = class extends SmCommand {
10586
10770
 
10587
10771
  // cli/commands/db/restore.ts
10588
10772
  import { chmod, copyFile, mkdir, rm } from "fs/promises";
10589
- import { dirname as dirname13, resolve as resolve23 } from "path";
10773
+ import { dirname as dirname14, resolve as resolve23 } from "path";
10590
10774
  import { Command as Command7, Option as Option7 } from "clipanion";
10591
10775
 
10592
10776
  // cli/util/fs.ts
@@ -10669,7 +10853,7 @@ var DbRestoreCommand = class extends SmCommand {
10669
10853
  return ExitCode.Error;
10670
10854
  }
10671
10855
  }
10672
- await mkdir(dirname13(target), { recursive: true });
10856
+ await mkdir(dirname14(target), { recursive: true });
10673
10857
  await copyFile(sourcePath, target);
10674
10858
  await chmodOwnerOnlyBestEffort(target);
10675
10859
  for (const sidecar of [`${target}-wal`, `${target}-shm`]) {
@@ -11018,7 +11202,7 @@ function formatSqlValue(value) {
11018
11202
 
11019
11203
  // cli/commands/db/migrate.ts
11020
11204
  import { mkdir as mkdir2 } from "fs/promises";
11021
- import { dirname as dirname14 } from "path";
11205
+ import { dirname as dirname15 } from "path";
11022
11206
  import { Command as Command12, Option as Option11 } from "clipanion";
11023
11207
 
11024
11208
  // cli/i18n/option-validators.texts.ts
@@ -11105,7 +11289,7 @@ var DbMigrateCommand = class extends SmCommand {
11105
11289
  return ExitCode.Error;
11106
11290
  }
11107
11291
  const path = resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
11108
- if (path !== ":memory:") await mkdir2(dirname14(path), { recursive: true });
11292
+ if (path !== ":memory:") await mkdir2(dirname15(path), { recursive: true });
11109
11293
  const adapter = createSqliteStorage({
11110
11294
  databasePath: path,
11111
11295
  autoMigrate: false
@@ -11758,7 +11942,7 @@ var GraphCommand = class extends SmCommand {
11758
11942
  };
11759
11943
 
11760
11944
  // cli/commands/help.ts
11761
- import { readFileSync as readFileSync15 } from "fs";
11945
+ import { readFileSync as readFileSync16 } from "fs";
11762
11946
  import { createRequire as createRequire7 } from "module";
11763
11947
  import { resolve as resolve25 } from "path";
11764
11948
  import { Command as Command15, Option as Option14 } from "clipanion";
@@ -11981,7 +12165,7 @@ function resolveSpecVersion() {
11981
12165
  const req = createRequire7(import.meta.url);
11982
12166
  const indexPath = req.resolve("@skill-map/spec/index.json");
11983
12167
  const pkgPath = resolve25(indexPath, "..", "package.json");
11984
- const pkg = JSON.parse(readFileSync15(pkgPath, "utf8"));
12168
+ const pkg = JSON.parse(readFileSync16(pkgPath, "utf8"));
11985
12169
  return pkg.version;
11986
12170
  } catch {
11987
12171
  return "unknown";
@@ -12233,13 +12417,13 @@ function registeredVerbPaths(cli2) {
12233
12417
  // cli/commands/hooks.ts
12234
12418
  import {
12235
12419
  chmodSync,
12236
- existsSync as existsSync17,
12420
+ existsSync as existsSync18,
12237
12421
  mkdirSync as mkdirSync5,
12238
- readFileSync as readFileSync16,
12239
- statSync as statSync3,
12422
+ readFileSync as readFileSync17,
12423
+ statSync as statSync5,
12240
12424
  writeFileSync as writeFileSync2
12241
12425
  } from "fs";
12242
- import { dirname as dirname15, resolve as resolve26 } from "path";
12426
+ import { dirname as dirname16, resolve as resolve26 } from "path";
12243
12427
  import { Command as Command16, Option as Option15 } from "clipanion";
12244
12428
 
12245
12429
  // cli/i18n/hooks.texts.ts
@@ -12332,7 +12516,7 @@ var HooksInstallCommand = class extends SmCommand {
12332
12516
  }
12333
12517
  const hooksDir = resolve26(repoRoot, ".git", "hooks");
12334
12518
  const hookPath = resolve26(hooksDir, "pre-commit");
12335
- const existing = existsSync17(hookPath) ? readFileSync16(hookPath, "utf8") : null;
12519
+ const existing = existsSync18(hookPath) ? readFileSync17(hookPath, "utf8") : null;
12336
12520
  const planned2 = computePlannedHookContent(existing);
12337
12521
  if (planned2.kind === "already-installed") {
12338
12522
  this.printer.info(tx(HOOKS_TEXTS.alreadyInstalled, { glyph: okGlyph, hookPath }));
@@ -12358,7 +12542,7 @@ var HooksInstallCommand = class extends SmCommand {
12358
12542
  return ExitCode.Ok;
12359
12543
  }
12360
12544
  try {
12361
- if (!existsSync17(hooksDir)) mkdirSync5(hooksDir, { recursive: true });
12545
+ if (!existsSync18(hooksDir)) mkdirSync5(hooksDir, { recursive: true });
12362
12546
  writeFileSync2(hookPath, planned2.content, { encoding: "utf8" });
12363
12547
  ensureExecutableBit(hookPath);
12364
12548
  } catch (err) {
@@ -12389,8 +12573,8 @@ var HooksInstallCommand = class extends SmCommand {
12389
12573
  function findGitRepoRoot(cwd) {
12390
12574
  let current = cwd;
12391
12575
  while (true) {
12392
- if (existsSync17(resolve26(current, ".git"))) return current;
12393
- const parent = dirname15(current);
12576
+ if (existsSync18(resolve26(current, ".git"))) return current;
12577
+ const parent = dirname16(current);
12394
12578
  if (parent === current) return null;
12395
12579
  current = parent;
12396
12580
  }
@@ -12404,18 +12588,18 @@ function computePlannedHookContent(existing) {
12404
12588
  return { kind: "chained", content: existing + sep6 + "\n" + SKILL_MAP_BLOCK };
12405
12589
  }
12406
12590
  function ensureExecutableBit(path) {
12407
- const mode = statSync3(path).mode;
12591
+ const mode = statSync5(path).mode;
12408
12592
  chmodSync(path, mode | 73);
12409
12593
  }
12410
12594
  var HOOKS_COMMANDS = [HooksInstallCommand];
12411
12595
 
12412
12596
  // cli/commands/init.ts
12413
12597
  import { mkdir as mkdir3, readFile as readFile2, writeFile } from "fs/promises";
12414
- import { join as join14 } from "path";
12598
+ import { join as join15 } from "path";
12415
12599
  import { Command as Command17, Option as Option16 } from "clipanion";
12416
12600
 
12417
12601
  // kernel/orchestrator/index.ts
12418
- import { existsSync as existsSync20, statSync as statSync5 } from "fs";
12602
+ import { existsSync as existsSync21, statSync as statSync7 } from "fs";
12419
12603
  import { Tiktoken as Tiktoken2 } from "js-tiktoken/lite";
12420
12604
  import cl100k_base from "js-tiktoken/ranks/cl100k_base";
12421
12605
 
@@ -12533,7 +12717,7 @@ async function runExtractorsForNode(opts) {
12533
12717
  }
12534
12718
  function readDeclaredContributions(extension) {
12535
12719
  const out = /* @__PURE__ */ new Map();
12536
- const raw = extension.viewContributions;
12720
+ const raw = extension.ui;
12537
12721
  if (typeof raw !== "object" || raw === null) return out;
12538
12722
  for (const [id, value] of Object.entries(raw)) {
12539
12723
  if (typeof value !== "object" || value === null) continue;
@@ -12554,11 +12738,13 @@ function emitExtensionError(emitter, qualifiedId2, nodePath, data) {
12554
12738
  );
12555
12739
  }
12556
12740
  function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, store) {
12557
- const scope = extractor.scope;
12741
+ const scope = extractor.scope ?? "both";
12742
+ const settings = extractor.resolvedSettings ?? {};
12558
12743
  return {
12559
12744
  node,
12560
12745
  body: scope === "frontmatter" ? "" : body,
12561
12746
  frontmatter: scope === "body" ? {} : frontmatter,
12747
+ settings,
12562
12748
  emitLink,
12563
12749
  enrichNode,
12564
12750
  emitContribution,
@@ -12566,25 +12752,26 @@ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enr
12566
12752
  };
12567
12753
  }
12568
12754
  function validateLink(extractor, link2, emitter) {
12569
- if (!extractor.emitsLinkKinds.includes(link2.kind)) {
12755
+ const knownKinds = ["invokes", "references", "mentions", "supersedes"];
12756
+ if (!knownKinds.includes(link2.kind)) {
12570
12757
  const qualifiedId2 = `${extractor.pluginId}/${extractor.id}`;
12571
12758
  emitter.emit(
12572
12759
  makeEvent("extension.error", {
12573
12760
  kind: "link-kind-not-declared",
12574
12761
  extensionId: qualifiedId2,
12575
12762
  linkKind: link2.kind,
12576
- declaredKinds: extractor.emitsLinkKinds,
12763
+ declaredKinds: knownKinds,
12577
12764
  link: { source: link2.source, target: link2.target, kind: link2.kind },
12578
12765
  message: tx(ORCHESTRATOR_TEXTS.extensionErrorLinkKindNotDeclared, {
12579
12766
  extractorId: qualifiedId2,
12580
12767
  linkKind: link2.kind,
12581
- declaredKinds: extractor.emitsLinkKinds.join(", ")
12768
+ declaredKinds: knownKinds.join(", ")
12582
12769
  })
12583
12770
  })
12584
12771
  );
12585
12772
  return null;
12586
12773
  }
12587
- const confidence = link2.confidence ?? extractor.defaultConfidence;
12774
+ const confidence = link2.confidence ?? "medium";
12588
12775
  return { ...link2, confidence };
12589
12776
  }
12590
12777
  function dedupeLinks(links) {
@@ -12642,7 +12829,7 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
12642
12829
  const issues = [];
12643
12830
  const contributions = [];
12644
12831
  const validators = loadSchemaValidators();
12645
- validateRecommendedActions(analyzers, registeredActionIds, emitter);
12832
+ void registeredActionIds;
12646
12833
  const analyzerOrphans = orphanSidecars.map((o) => ({
12647
12834
  relativePath: o.relativePath,
12648
12835
  expectedMdPath: o.expectedMdPath
@@ -12714,27 +12901,6 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
12714
12901
  }
12715
12902
  return { issues, contributions };
12716
12903
  }
12717
- function validateRecommendedActions(analyzers, registeredActionIds, emitter) {
12718
- for (const analyzer of analyzers) {
12719
- const refs = analyzer.recommendedActions;
12720
- if (refs === void 0 || refs.length === 0) continue;
12721
- const analyzerId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
12722
- for (const actionId of refs) {
12723
- if (registeredActionIds.has(actionId)) continue;
12724
- emitter.emit(
12725
- makeEvent("extension.error", {
12726
- kind: "recommended-action-missing",
12727
- extensionId: analyzerId,
12728
- actionId,
12729
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorRecommendedActionMissing, {
12730
- analyzerId,
12731
- actionId
12732
- })
12733
- })
12734
- );
12735
- }
12736
- }
12737
- }
12738
12904
  function validateIssue(analyzer, issue, emitter) {
12739
12905
  const severity = issue.severity;
12740
12906
  if (severity !== "error" && severity !== "warn" && severity !== "info") {
@@ -12812,9 +12978,15 @@ function originatingNodeOf(link2, priorNodePaths) {
12812
12978
  return link2.source;
12813
12979
  }
12814
12980
  function computeCacheDecision(opts) {
12815
- const applicableExtractors = opts.extractors.filter(
12816
- (ex) => ex.applicableKinds === void 0 || ex.applicableKinds.includes(opts.kind)
12817
- );
12981
+ const applicableExtractors = opts.extractors.filter((ex) => {
12982
+ const kinds = ex.precondition?.kind;
12983
+ if (!kinds || kinds.length === 0) return true;
12984
+ return kinds.some((qualified) => {
12985
+ const slashIdx = qualified.indexOf("/");
12986
+ const kindOnly = slashIdx === -1 ? qualified : qualified.slice(slashIdx + 1);
12987
+ return kindOnly === opts.kind;
12988
+ });
12989
+ });
12818
12990
  const applicableQualifiedIds = new Set(
12819
12991
  applicableExtractors.map((ex) => qualifiedExtensionId(ex.pluginId, ex.id))
12820
12992
  );
@@ -13005,6 +13177,7 @@ function flagAmbiguousRenames(opts) {
13005
13177
  function flagOrphans(opts) {
13006
13178
  for (const fromPath of opts.deletedPaths) {
13007
13179
  if (opts.claimedDeleted.has(fromPath)) continue;
13180
+ if (opts.silenced?.(fromPath)) continue;
13008
13181
  opts.issues.push({
13009
13182
  analyzerId: "orphan",
13010
13183
  severity: "info",
@@ -13014,7 +13187,7 @@ function flagOrphans(opts) {
13014
13187
  });
13015
13188
  }
13016
13189
  }
13017
- function detectRenamesAndOrphans(prior, current, issues) {
13190
+ function detectRenamesAndOrphans(prior, current, issues, silenced) {
13018
13191
  const priorByPath = /* @__PURE__ */ new Map();
13019
13192
  for (const n of prior.nodes) priorByPath.set(n.path, n);
13020
13193
  const currentByPath = /* @__PURE__ */ new Map();
@@ -13048,7 +13221,12 @@ function detectRenamesAndOrphans(prior, current, issues) {
13048
13221
  issues
13049
13222
  }));
13050
13223
  flagAmbiguousRenames({ newPaths, candidatesByNew, claimedDeleted, claimedNew, issues });
13051
- flagOrphans({ deletedPaths, claimedDeleted, issues });
13224
+ flagOrphans({
13225
+ deletedPaths,
13226
+ claimedDeleted,
13227
+ issues,
13228
+ ...silenced ? { silenced } : {}
13229
+ });
13052
13230
  return ops;
13053
13231
  }
13054
13232
 
@@ -13063,8 +13241,8 @@ function computeDriftStatus(args2) {
13063
13241
  }
13064
13242
 
13065
13243
  // kernel/sidecar/discover-orphans.ts
13066
- import { existsSync as existsSync18, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
13067
- import { join as join11, relative as relative4, sep as sep3 } from "path";
13244
+ import { existsSync as existsSync19, readdirSync as readdirSync7, statSync as statSync6 } from "fs";
13245
+ import { join as join12, relative as relative4, sep as sep3 } from "path";
13068
13246
  function discoverOrphanSidecars(roots, shouldSkip) {
13069
13247
  const out = [];
13070
13248
  for (const root of roots) {
@@ -13075,12 +13253,12 @@ function discoverOrphanSidecars(roots, shouldSkip) {
13075
13253
  function walk(root, current, shouldSkip, out) {
13076
13254
  let entries;
13077
13255
  try {
13078
- entries = readdirSync6(current, { withFileTypes: true, encoding: "utf8" });
13256
+ entries = readdirSync7(current, { withFileTypes: true, encoding: "utf8" });
13079
13257
  } catch {
13080
13258
  return;
13081
13259
  }
13082
13260
  for (const entry of entries) {
13083
- const full = join11(current, entry.name);
13261
+ const full = join12(current, entry.name);
13084
13262
  const rel = relative4(root, full).split(sep3).join("/");
13085
13263
  if (shouldSkip(rel)) continue;
13086
13264
  if (entry.isSymbolicLink()) continue;
@@ -13091,13 +13269,13 @@ function walk(root, current, shouldSkip, out) {
13091
13269
  if (!entry.isFile()) continue;
13092
13270
  if (!entry.name.endsWith(".sm")) continue;
13093
13271
  const expectedMd = `${full.slice(0, -".sm".length)}.md`;
13094
- if (existsSync18(expectedMd) && safeIsFile(expectedMd)) continue;
13272
+ if (existsSync19(expectedMd) && safeIsFile(expectedMd)) continue;
13095
13273
  out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
13096
13274
  }
13097
13275
  }
13098
13276
  function safeIsFile(path) {
13099
13277
  try {
13100
- return statSync4(path).isFile();
13278
+ return statSync6(path).isFile();
13101
13279
  } catch {
13102
13280
  return false;
13103
13281
  }
@@ -13105,7 +13283,7 @@ function safeIsFile(path) {
13105
13283
 
13106
13284
  // kernel/orchestrator/node-build.ts
13107
13285
  import { createHash } from "crypto";
13108
- import { existsSync as existsSync19 } from "fs";
13286
+ import { existsSync as existsSync20 } from "fs";
13109
13287
  import { isAbsolute as isAbsolute6, resolve as resolvePath } from "path";
13110
13288
  import "js-tiktoken/lite";
13111
13289
  import yaml4 from "js-yaml";
@@ -13269,11 +13447,11 @@ function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyH
13269
13447
  }
13270
13448
  function resolveAbsoluteMdPath(relativePath2, roots) {
13271
13449
  if (isAbsolute6(relativePath2)) {
13272
- return existsSync19(relativePath2) ? relativePath2 : null;
13450
+ return existsSync20(relativePath2) ? relativePath2 : null;
13273
13451
  }
13274
13452
  for (const root of roots) {
13275
13453
  const candidate = resolvePath(root, relativePath2);
13276
- if (existsSync19(candidate)) return candidate;
13454
+ if (existsSync20(candidate)) return candidate;
13277
13455
  }
13278
13456
  return null;
13279
13457
  }
@@ -13386,6 +13564,9 @@ function buildWalkContext(opts) {
13386
13564
  async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextIndex) {
13387
13565
  const bodyHash = sha256(raw.body);
13388
13566
  const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
13567
+ if (Array.isArray(provider.roots) && provider.roots.length > 0) {
13568
+ if (!matchesAnyRoot(raw.path, provider.roots)) return false;
13569
+ }
13389
13570
  const kind = provider.classify(raw.path, raw.frontmatter);
13390
13571
  if (kind === null) {
13391
13572
  return false;
@@ -13554,6 +13735,26 @@ function recordExtractorRuns(nodePath, ctx, accum) {
13554
13735
  });
13555
13736
  }
13556
13737
  }
13738
+ function matchesAnyRoot(relPath, roots) {
13739
+ for (const r of roots) {
13740
+ if (matchesOneRoot(relPath, r)) return true;
13741
+ }
13742
+ return false;
13743
+ }
13744
+ function matchesOneRoot(relPath, pattern) {
13745
+ if (pattern.endsWith("/**")) return matchesDeepGlob(relPath, pattern.slice(0, -3));
13746
+ if (pattern.endsWith("/*")) return matchesShallowGlob(relPath, pattern.slice(0, -2));
13747
+ return relPath === pattern;
13748
+ }
13749
+ function matchesDeepGlob(relPath, prefix) {
13750
+ if (prefix.length === 0) return true;
13751
+ return relPath === prefix || relPath.startsWith(`${prefix}/`);
13752
+ }
13753
+ function matchesShallowGlob(relPath, prefix) {
13754
+ if (!relPath.startsWith(`${prefix}/`)) return false;
13755
+ const tail = relPath.slice(prefix.length + 1);
13756
+ return tail.length > 0 && !tail.includes("/");
13757
+ }
13557
13758
 
13558
13759
  // kernel/orchestrator/index.ts
13559
13760
  var SCANNED_BY = {
@@ -13622,7 +13823,8 @@ async function runScanInternal(_kernel, options) {
13622
13823
  mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);
13623
13824
  const issues = analyzerResult.issues;
13624
13825
  for (const issue of walked.frontmatterIssues) issues.push(issue);
13625
- const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues) : [];
13826
+ const silenced = options.ignoreFilter ? (path) => options.ignoreFilter.ignores(path) : void 0;
13827
+ const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues, silenced) : [];
13626
13828
  const stats = buildScanStats(walked, issues, start);
13627
13829
  const scanCompletedEvent = makeEvent("scan.completed", { stats });
13628
13830
  emitter.emit(scanCompletedEvent);
@@ -13665,7 +13867,7 @@ async function dispatchExtractorCompleted(extractors, emitter, hookDispatcher) {
13665
13867
  function mergeAnalyzerEmissions(walked, analyzerResult, analyzers) {
13666
13868
  for (const c of analyzerResult.contributions) walked.contributions.push(c);
13667
13869
  for (const analyzer of analyzers ?? []) {
13668
- if (analyzer.viewContributions === void 0) continue;
13870
+ if (analyzer.ui === void 0) continue;
13669
13871
  for (const node of walked.nodes) {
13670
13872
  walked.freshlyRunTuples.add(`${analyzer.pluginId}\0${analyzer.id}\0${node.path}`);
13671
13873
  }
@@ -13712,7 +13914,7 @@ function validateRoots(roots) {
13712
13914
  throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
13713
13915
  }
13714
13916
  for (const root of roots) {
13715
- if (!existsSync20(root) || !statSync5(root).isDirectory()) {
13917
+ if (!existsSync21(root) || !statSync7(root).isDirectory()) {
13716
13918
  throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
13717
13919
  }
13718
13920
  }
@@ -13940,16 +14142,16 @@ function createKernel() {
13940
14142
  }
13941
14143
 
13942
14144
  // kernel/jobs/orphan-files.ts
13943
- import { readdirSync as readdirSync7, statSync as statSync6 } from "fs";
13944
- import { join as join12, resolve as resolve28 } from "path";
14145
+ import { readdirSync as readdirSync8, statSync as statSync8 } from "fs";
14146
+ import { join as join13, resolve as resolve28 } from "path";
13945
14147
  function findOrphanJobFiles(jobsDir, referencedPaths) {
13946
14148
  let entries;
13947
14149
  try {
13948
- const stat2 = statSync6(jobsDir);
14150
+ const stat2 = statSync8(jobsDir);
13949
14151
  if (!stat2.isDirectory()) {
13950
14152
  return { orphanFilePaths: [], referencedCount: referencedPaths.size };
13951
14153
  }
13952
- entries = readdirSync7(jobsDir, { withFileTypes: true });
14154
+ entries = readdirSync8(jobsDir, { withFileTypes: true });
13953
14155
  } catch {
13954
14156
  return { orphanFilePaths: [], referencedCount: referencedPaths.size };
13955
14157
  }
@@ -13959,7 +14161,7 @@ function findOrphanJobFiles(jobsDir, referencedPaths) {
13959
14161
  if (!entry.isFile()) continue;
13960
14162
  const name = entry.name;
13961
14163
  if (!name.endsWith(".md")) continue;
13962
- const abs = resolve28(join12(jobsDir, name));
14164
+ const abs = resolve28(join13(jobsDir, name));
13963
14165
  if (!referencedPaths.has(abs)) orphans.push(abs);
13964
14166
  }
13965
14167
  orphans.sort();
@@ -14033,9 +14235,9 @@ var SCAN_RUNNER_TEXTS = {
14033
14235
  };
14034
14236
 
14035
14237
  // core/runtime/reference-paths-walker.ts
14036
- import { readdirSync as readdirSync8, statSync as statSync7 } from "fs";
14238
+ import { readdirSync as readdirSync9, statSync as statSync9 } from "fs";
14037
14239
  import { homedir as osHomedir2 } from "os";
14038
- import { isAbsolute as isAbsolute7, join as join13, resolve as resolve29 } from "path";
14240
+ import { isAbsolute as isAbsolute7, join as join14, resolve as resolve29 } from "path";
14039
14241
  var REFERENCE_WALK_MAX_FILES = 5e4;
14040
14242
  var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
14041
14243
  "node_modules",
@@ -14043,7 +14245,7 @@ var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
14043
14245
  SKILL_MAP_DIR
14044
14246
  ]);
14045
14247
  function resolveScanPath(raw, cwd) {
14046
- if (raw.startsWith("~/")) return resolve29(join13(osHomedir2(), raw.slice(2)));
14248
+ if (raw.startsWith("~/")) return resolve29(join14(osHomedir2(), raw.slice(2)));
14047
14249
  if (raw === "~") return resolve29(osHomedir2());
14048
14250
  if (isAbsolute7(raw)) return resolve29(raw);
14049
14251
  return resolve29(cwd, raw);
@@ -14068,14 +14270,14 @@ function walkInto(dir, out) {
14068
14270
  if (out.size >= REFERENCE_WALK_MAX_FILES) return true;
14069
14271
  let entries;
14070
14272
  try {
14071
- entries = readdirSync8(dir, { withFileTypes: true });
14273
+ entries = readdirSync9(dir, { withFileTypes: true });
14072
14274
  } catch {
14073
14275
  return false;
14074
14276
  }
14075
14277
  for (const entry of entries) {
14076
14278
  if (out.size >= REFERENCE_WALK_MAX_FILES) return true;
14077
14279
  if (entry.isSymbolicLink()) continue;
14078
- const full = join13(dir, entry.name);
14280
+ const full = join14(dir, entry.name);
14079
14281
  if (entry.isDirectory()) {
14080
14282
  if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
14081
14283
  if (walkInto(full, out)) return true;
@@ -14087,7 +14289,7 @@ function walkInto(dir, out) {
14087
14289
  }
14088
14290
  function safeStat(path) {
14089
14291
  try {
14090
- return statSync7(path);
14292
+ return statSync9(path);
14091
14293
  } catch {
14092
14294
  return null;
14093
14295
  }
@@ -14399,7 +14601,7 @@ var InitCommand = class extends SmCommand {
14399
14601
  async run() {
14400
14602
  const ctx = defaultRuntimeContext();
14401
14603
  const scopeRoot = ctx.cwd;
14402
- const skillMapDir = join14(scopeRoot, SKILL_MAP_DIR);
14604
+ const skillMapDir = join15(scopeRoot, SKILL_MAP_DIR);
14403
14605
  const settingsPath = defaultSettingsPath(scopeRoot);
14404
14606
  const localPath = defaultLocalSettingsPath(scopeRoot);
14405
14607
  const ignorePath = defaultIgnoreFilePath(scopeRoot);
@@ -14439,13 +14641,13 @@ var InitCommand = class extends SmCommand {
14439
14641
  writeFileAtomicExclusive(localPath, "{}\n");
14440
14642
  }
14441
14643
  if (!await pathExists(ignorePath) || this.force) {
14442
- writeFileAtomicExclusive(ignorePath, loadBundledIgnoreText());
14644
+ writeFileAtomicExclusive(ignorePath, loadBundledIgnoreText(), 420);
14443
14645
  }
14444
14646
  const ansi = this.ansiFor("stdout");
14445
14647
  const okGlyph = ansi.green("\u2713");
14446
14648
  const updated = await ensureGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
14447
14649
  if (updated) {
14448
- const gitignorePath = join14(scopeRoot, ".gitignore");
14650
+ const gitignorePath = join15(scopeRoot, ".gitignore");
14449
14651
  printer.info(
14450
14652
  GITIGNORE_ENTRIES.length === 1 ? tx(INIT_TEXTS.gitignoreUpdatedSingular, { glyph: okGlyph, path: gitignorePath }) : tx(INIT_TEXTS.gitignoreUpdatedPlural, {
14451
14653
  glyph: okGlyph,
@@ -14484,7 +14686,7 @@ async function dryRunFileMessage(path) {
14484
14686
  }
14485
14687
  async function writeDryRunGitignorePlan(printer, scopeRoot) {
14486
14688
  const wouldAdd = await previewGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
14487
- const gitignorePath = join14(scopeRoot, ".gitignore");
14689
+ const gitignorePath = join15(scopeRoot, ".gitignore");
14488
14690
  if (wouldAdd.length === 0) {
14489
14691
  printer.info(tx(INIT_TEXTS.dryRunWouldLeaveGitignoreUnchanged, { path: gitignorePath }));
14490
14692
  } else if (wouldAdd.length === 1) {
@@ -14559,7 +14761,7 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, ansi) {
14559
14761
  return hasErrors ? ExitCode.Issues : ExitCode.Ok;
14560
14762
  }
14561
14763
  async function previewGitignoreEntries(scopeRoot, entries) {
14562
- const path = join14(scopeRoot, ".gitignore");
14764
+ const path = join15(scopeRoot, ".gitignore");
14563
14765
  const body = await pathExists(path) ? await readFile2(path, "utf8") : "";
14564
14766
  const present = new Set(
14565
14767
  body.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"))
@@ -14567,7 +14769,7 @@ async function previewGitignoreEntries(scopeRoot, entries) {
14567
14769
  return entries.filter((entry) => !present.has(entry));
14568
14770
  }
14569
14771
  async function ensureGitignoreEntries(scopeRoot, entries) {
14570
- const path = join14(scopeRoot, ".gitignore");
14772
+ const path = join15(scopeRoot, ".gitignore");
14571
14773
  let body = "";
14572
14774
  if (await pathExists(path)) {
14573
14775
  body = await readFile2(path, "utf8");
@@ -16222,7 +16424,7 @@ var PLUGINS_TEXTS = {
16222
16424
  * Success block printed after scaffolding. Follows the no-em-dash rule
16223
16425
  * across every line.
16224
16426
  */
16225
- createSuccess: "Created {{targetDir}}\nNext:\n - Edit {{pluginId}}/extensions/extractor.js (the extract() body)\n - Run sm scan to see the contribution surface\n - sm plugins slots list: browse other slots\n",
16427
+ createSuccess: "Created {{targetDir}}\nNext:\n - Edit {{pluginId}}/extractors/{{pluginId}}-extractor/index.js (the extract() body)\n - Run sm scan to see the contribution surface\n - sm plugins slots list: browse other slots\n",
16226
16428
  // --- slots list verb -------------------------------------------------
16227
16429
  /** Section header for the view-slots catalogue. */
16228
16430
  slotsListHeaderViewSlots: " View slots ({{count}})\n",
@@ -16282,11 +16484,9 @@ function extensionRowFromBuiltIn(ext, bundle, bundleEnabled, resolveEnabled) {
16282
16484
  id: ext.id,
16283
16485
  kind: ext.kind,
16284
16486
  version: ext.version,
16285
- enabled: bundle.granularity === "bundle" ? bundleEnabled : resolveEnabled(qualifiedExtensionId(bundle.id, ext.id))
16487
+ enabled: bundle.granularity === "bundle" ? bundleEnabled : resolveEnabled(qualifiedExtensionId(bundle.id, ext.id)),
16488
+ description: ext.description ?? ""
16286
16489
  };
16287
- if (ext.description !== void 0) row.description = ext.description;
16288
- if (ext.stability !== void 0) row.stability = ext.stability;
16289
- if (ext.preconditions !== void 0) row.preconditions = ext.preconditions;
16290
16490
  if (ext.entry !== void 0) row.entry = ext.entry;
16291
16491
  return row;
16292
16492
  }
@@ -16549,12 +16749,24 @@ function unknownExtensionError(id, bundleId, extId, ansi) {
16549
16749
  hint: ansi.dim(PLUGINS_TEXTS.qualifiedIdNotFoundHint)
16550
16750
  });
16551
16751
  }
16752
+ function kindIndex(kind) {
16753
+ const idx = EXTENSION_KINDS.indexOf(kind);
16754
+ return idx === -1 ? EXTENSION_KINDS.length : idx;
16755
+ }
16756
+ function sortExtensionsCanonical(exts) {
16757
+ return [...exts].sort((a, b) => {
16758
+ const k = kindIndex(a.kind) - kindIndex(b.kind);
16759
+ if (k !== 0) return k;
16760
+ return a.id.localeCompare(b.id);
16761
+ });
16762
+ }
16552
16763
  function renderBuiltInDetail(b, ansi) {
16553
16764
  const enabled = b.enabled;
16554
16765
  const glyph = enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
16555
16766
  const count = b.extensions.length;
16556
16767
  const qualify = b.granularity === "extension";
16557
- const items = b.extensions.map((ext) => ({
16768
+ const sorted = sortExtensionsCanonical(b.extensions);
16769
+ const items = sorted.map((ext) => ({
16558
16770
  glyph: b.granularity === "extension" ? ext.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff) : null,
16559
16771
  kind: ext.kind,
16560
16772
  name: qualify ? `${b.id}/${ext.id}` : ext.id,
@@ -16628,7 +16840,8 @@ function collectPluginExtensionItems(match, ansi) {
16628
16840
  if (!enabled || !match.extensions) return [];
16629
16841
  const qualify = match.granularity === "extension";
16630
16842
  const safeBundleId = sanitizeForTerminal(match.id);
16631
- return match.extensions.map((ext) => {
16843
+ const sorted = sortExtensionsCanonical(match.extensions);
16844
+ return sorted.map((ext) => {
16632
16845
  const safeExtId = sanitizeForTerminal(ext.id);
16633
16846
  return {
16634
16847
  glyph: match.granularity === "extension" ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : null,
@@ -16675,9 +16888,7 @@ function renderBuiltInExtensionDetail(bundleId, ext, ansi) {
16675
16888
  source: ansi.dim(PLUGINS_TEXTS.sourceBuiltIn)
16676
16889
  });
16677
16890
  const meta = { kind: ext.kind, version: ext.version };
16678
- if (ext.stability !== void 0) meta.stability = ext.stability;
16679
- if (ext.description !== void 0) meta.description = ext.description;
16680
- if (ext.preconditions !== void 0) meta.preconditions = ext.preconditions;
16891
+ if (ext.description) meta.description = ext.description;
16681
16892
  if (ext.entry !== void 0) meta.entry = ext.entry;
16682
16893
  return header + "\n" + renderExtensionFields(meta);
16683
16894
  }
@@ -16978,11 +17189,12 @@ function collectBuiltInApplicableKindWarnings(out, knownKinds) {
16978
17189
  for (const ext of bundle.extensions) {
16979
17190
  if (ext.kind !== "extractor") continue;
16980
17191
  const extractor = ext;
16981
- if (!extractor.applicableKinds) continue;
17192
+ const kinds = extractor.precondition?.kind;
17193
+ if (!kinds || kinds.length === 0) continue;
16982
17194
  appendUnknownKindWarnings(
16983
17195
  out,
16984
17196
  qualifiedExtensionId(bundle.id, extractor.id),
16985
- extractor.applicableKinds,
17197
+ kinds,
16986
17198
  knownKinds
16987
17199
  );
16988
17200
  }
@@ -16992,19 +17204,24 @@ function collectUserApplicableKindWarnings(out, plugins, knownKinds) {
16992
17204
  for (const p of plugins) {
16993
17205
  if (p.status !== "enabled" || !p.extensions) continue;
16994
17206
  for (const ext of p.extensions) {
16995
- if (ext.kind !== "extractor") continue;
16996
- const inst = extensionInstance(ext);
16997
- if (!inst) continue;
16998
- const ak = inst["applicableKinds"];
16999
- if (!Array.isArray(ak)) continue;
17000
- appendUnknownKindWarnings(
17001
- out,
17002
- qualifiedExtensionId(ext.pluginId, ext.id),
17003
- ak,
17004
- knownKinds
17005
- );
17006
- }
17007
- }
17207
+ collectKindsFromExtension(ext, knownKinds, out);
17208
+ }
17209
+ }
17210
+ }
17211
+ function collectKindsFromExtension(ext, knownKinds, out) {
17212
+ if (ext.kind !== "extractor") return;
17213
+ const inst = extensionInstance(ext);
17214
+ if (!inst) return;
17215
+ const pre = inst["precondition"];
17216
+ if (!pre || typeof pre !== "object") return;
17217
+ const kinds = pre.kind;
17218
+ if (!Array.isArray(kinds)) return;
17219
+ appendUnknownKindWarnings(
17220
+ out,
17221
+ qualifiedExtensionId(ext.pluginId, ext.id),
17222
+ kinds,
17223
+ knownKinds
17224
+ );
17008
17225
  }
17009
17226
  function appendUnknownKindWarnings(out, extractorQualifiedId, applicableKinds, knownKinds) {
17010
17227
  for (const k of applicableKinds) {
@@ -17357,8 +17574,8 @@ function resolveBareToggle(id, catalogue, verb, ansi) {
17357
17574
  }
17358
17575
 
17359
17576
  // cli/commands/plugins/create.ts
17360
- import { existsSync as existsSync21, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "fs";
17361
- import { join as join15, resolve as resolve31 } from "path";
17577
+ import { existsSync as existsSync22, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "fs";
17578
+ import { join as join16, resolve as resolve31 } from "path";
17362
17579
  import { Command as Command26, Option as Option25 } from "clipanion";
17363
17580
  var PluginsCreateCommand = class extends SmCommand {
17364
17581
  static paths = [["plugins", "create"]];
@@ -17384,8 +17601,8 @@ var PluginsCreateCommand = class extends SmCommand {
17384
17601
  }
17385
17602
  const ctx = defaultRuntimeContext();
17386
17603
  const baseDir = defaultProjectPluginsDir(ctx);
17387
- const targetDir = this.at ? resolve31(this.at) : join15(baseDir, this.pluginId);
17388
- if (existsSync21(targetDir) && !this.force) {
17604
+ const targetDir = this.at ? resolve31(this.at) : join16(baseDir, this.pluginId);
17605
+ if (existsSync22(targetDir) && !this.force) {
17389
17606
  this.printer.error(
17390
17607
  tx(PLUGINS_TEXTS.createRefuseOverwrite, {
17391
17608
  glyph: errGlyph,
@@ -17394,14 +17611,15 @@ var PluginsCreateCommand = class extends SmCommand {
17394
17611
  );
17395
17612
  return ExitCode.Error;
17396
17613
  }
17397
- mkdirSync6(join15(targetDir, "extensions"), { recursive: true });
17614
+ const extractorName = `${this.pluginId}-extractor`;
17615
+ mkdirSync6(join16(targetDir, "extractors", extractorName), { recursive: true });
17398
17616
  const specVersion = installedSpecVersion();
17399
17617
  const manifest = {
17400
17618
  id: this.pluginId,
17401
17619
  version: "0.1.0",
17402
17620
  specCompat: `^${specVersion}`,
17403
17621
  catalogCompat: "^1.0.0",
17404
- extensions: ["./extensions/extractor.js"],
17622
+ granularity: "bundle",
17405
17623
  description: "Generated by `sm plugins create`. Edit to taste.",
17406
17624
  settings: {
17407
17625
  keywords: {
@@ -17414,14 +17632,14 @@ var PluginsCreateCommand = class extends SmCommand {
17414
17632
  }
17415
17633
  };
17416
17634
  writeFileSync3(
17417
- join15(targetDir, "plugin.json"),
17635
+ join16(targetDir, "plugin.json"),
17418
17636
  JSON.stringify(manifest, null, 2) + "\n"
17419
17637
  );
17420
17638
  writeFileSync3(
17421
- join15(targetDir, "extensions", "extractor.js"),
17422
- scaffolderExtractorStub(this.pluginId)
17639
+ join16(targetDir, "extractors", extractorName, "index.js"),
17640
+ scaffolderExtractorStub(extractorName)
17423
17641
  );
17424
- writeFileSync3(join15(targetDir, "README.md"), scaffolderReadme(this.pluginId));
17642
+ writeFileSync3(join16(targetDir, "README.md"), scaffolderReadme(this.pluginId));
17425
17643
  this.printer.data(
17426
17644
  tx(PLUGINS_TEXTS.createSuccess, {
17427
17645
  targetDir: sanitizeForTerminal(targetDir),
@@ -17431,7 +17649,7 @@ var PluginsCreateCommand = class extends SmCommand {
17431
17649
  return ExitCode.Ok;
17432
17650
  }
17433
17651
  };
17434
- function scaffolderExtractorStub(pluginId) {
17652
+ function scaffolderExtractorStub(extractorId) {
17435
17653
  return `/**
17436
17654
  * Generated by \`sm plugins create\`. Edit the extract() body.
17437
17655
  *
@@ -17440,6 +17658,11 @@ function scaffolderExtractorStub(pluginId) {
17440
17658
  * splitting into a named export will surface as \`load-error: default
17441
17659
  * export missing a string \\\`kind\\\` field\`.
17442
17660
  *
17661
+ * Folder convention: this file lives at
17662
+ * \`extractors/${extractorId}/index.js\`. The bundle's plugin.json#/id
17663
+ * provides the qualified id \`<plugin-id>/${extractorId}\`; the loader
17664
+ * injects \`pluginId\` automatically, do NOT hardcode it here.
17665
+ *
17443
17666
  * Declared view contributions (in plugin.json):
17444
17667
  * - 'count' \u2192 slot \`card.footer.left\` (renders as a chip
17445
17668
  * in the left footer of the node card)
@@ -17451,13 +17674,11 @@ function scaffolderExtractorStub(pluginId) {
17451
17674
  * spec/view-slots.md
17452
17675
  */
17453
17676
  export default {
17454
- id: '${pluginId}-extractor',
17455
- pluginId: '${pluginId}',
17677
+ id: '${extractorId}',
17456
17678
  kind: 'extractor',
17457
17679
  version: '0.1.0',
17458
17680
  description: 'Counts configured keywords per node.',
17459
17681
  stability: 'experimental',
17460
- mode: 'deterministic',
17461
17682
  emitsLinkKinds: [],
17462
17683
  defaultConfidence: 'high',
17463
17684
  scope: 'body',
@@ -17924,9 +18145,15 @@ var RefreshCommand = class extends SmCommand {
17924
18145
  continue;
17925
18146
  }
17926
18147
  const fm = node.frontmatter ?? {};
17927
- const applicable = allExtractors.filter(
17928
- (ex) => ex.applicableKinds === void 0 || ex.applicableKinds.includes(node.kind)
17929
- );
18148
+ const applicable = allExtractors.filter((ex) => {
18149
+ const kinds = ex.precondition?.kind;
18150
+ if (!kinds || kinds.length === 0) return true;
18151
+ return kinds.some((qualified) => {
18152
+ const slashIdx = qualified.indexOf("/");
18153
+ const kindOnly = slashIdx === -1 ? qualified : qualified.slice(slashIdx + 1);
18154
+ return kindOnly === node.kind;
18155
+ });
18156
+ });
17930
18157
  for (const extractor of applicable) {
17931
18158
  const records = await runExtractorForEnrichment(extractor, node, body, fm);
17932
18159
  for (const record of records) nodeEnrichments.push(record);
@@ -18036,7 +18263,7 @@ var SCAN_TEXTS = {
18036
18263
  import { Command as Command30, Option as Option28 } from "clipanion";
18037
18264
 
18038
18265
  // core/watcher/runtime.ts
18039
- import { dirname as dirname16 } from "path";
18266
+ import { dirname as dirname17 } from "path";
18040
18267
 
18041
18268
  // core/runtime/fresh-resolver.ts
18042
18269
  async function buildFreshResolver(deps) {
@@ -18261,7 +18488,7 @@ function createWatcherRuntime(opts) {
18261
18488
  roots: [
18262
18489
  cwd,
18263
18490
  // parent of `.skillmapignore`
18264
- dirname16(settingsPath)
18491
+ dirname17(settingsPath)
18265
18492
  // parent of `.skill-map/settings.json`
18266
18493
  ],
18267
18494
  cwd,
@@ -18817,7 +19044,7 @@ function plural(count, word) {
18817
19044
  }
18818
19045
 
18819
19046
  // cli/commands/scan-compare.ts
18820
- import { existsSync as existsSync22, readFileSync as readFileSync17 } from "fs";
19047
+ import { existsSync as existsSync23, readFileSync as readFileSync18 } from "fs";
18821
19048
  import { Command as Command32, Option as Option30 } from "clipanion";
18822
19049
  var ScanCompareCommand = class extends SmCommand {
18823
19050
  static paths = [["scan", "compare-with"]];
@@ -18929,12 +19156,12 @@ var ScanCompareCommand = class extends SmCommand {
18929
19156
  }
18930
19157
  };
18931
19158
  function loadAndValidateDump(path) {
18932
- if (!existsSync22(path)) {
19159
+ if (!existsSync23(path)) {
18933
19160
  throw new Error(tx(SCAN_TEXTS.compareDumpNotFound, { path }));
18934
19161
  }
18935
19162
  let raw;
18936
19163
  try {
18937
- raw = readFileSync17(path, "utf8");
19164
+ raw = readFileSync18(path, "utf8");
18938
19165
  } catch (err) {
18939
19166
  const message = formatErrorMessage(err);
18940
19167
  throw new Error(tx(SCAN_TEXTS.compareDumpReadFailed, { path, message }), { cause: err });
@@ -19059,7 +19286,7 @@ function renderDeltaIssues(issues) {
19059
19286
 
19060
19287
  // cli/commands/serve.ts
19061
19288
  import { spawn as spawn2 } from "child_process";
19062
- import { existsSync as existsSync26 } from "fs";
19289
+ import { existsSync as existsSync27 } from "fs";
19063
19290
  import { Command as Command33, Option as Option31 } from "clipanion";
19064
19291
 
19065
19292
  // cli/util/browser-launch.ts
@@ -19712,7 +19939,7 @@ function contentTypeFor(format) {
19712
19939
  }
19713
19940
 
19714
19941
  // server/health.ts
19715
- import { existsSync as existsSync23 } from "fs";
19942
+ import { existsSync as existsSync24 } from "fs";
19716
19943
  var FALLBACK_SCHEMA_VERSION = "1";
19717
19944
  function buildHealth(deps) {
19718
19945
  return {
@@ -19720,7 +19947,7 @@ function buildHealth(deps) {
19720
19947
  schemaVersion: FALLBACK_SCHEMA_VERSION,
19721
19948
  specVersion: deps.specVersion,
19722
19949
  implVersion: VERSION,
19723
- db: existsSync23(deps.dbPath) ? "present" : "missing",
19950
+ db: existsSync24(deps.dbPath) ? "present" : "missing",
19724
19951
  cwd: deps.cwd,
19725
19952
  dbPath: deps.dbPath
19726
19953
  };
@@ -21084,7 +21311,8 @@ function invokeBump2(node, absPath, body) {
21084
21311
  node,
21085
21312
  nodeAbsolutePath: absPath,
21086
21313
  invoker: "ui",
21087
- now: () => /* @__PURE__ */ new Date()
21314
+ now: () => /* @__PURE__ */ new Date(),
21315
+ settings: {}
21088
21316
  });
21089
21317
  }
21090
21318
  function pickExistingVersion(node) {
@@ -21121,9 +21349,9 @@ function registerUpdateStatusRoute(app, deps) {
21121
21349
  }
21122
21350
 
21123
21351
  // server/static.ts
21124
- import { existsSync as existsSync24 } from "fs";
21352
+ import { existsSync as existsSync25 } from "fs";
21125
21353
  import { readFile as readFile5 } from "fs/promises";
21126
- import { extname, join as join16 } from "path";
21354
+ import { extname, join as join17 } from "path";
21127
21355
  import { serveStatic } from "@hono/node-server/serve-static";
21128
21356
  var INDEX_HTML = "index.html";
21129
21357
  var PLACEHOLDER_HTML = `<!doctype html>
@@ -21175,8 +21403,8 @@ function createSpaFallback(opts) {
21175
21403
  return async (c, _next) => {
21176
21404
  if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.notFound();
21177
21405
  if (opts.uiDist === null) return htmlResponse(c, placeholder);
21178
- const indexPath = join16(opts.uiDist, INDEX_HTML);
21179
- if (!existsSync24(indexPath)) return htmlResponse(c, placeholder);
21406
+ const indexPath = join17(opts.uiDist, INDEX_HTML);
21407
+ if (!existsSync25(indexPath)) return htmlResponse(c, placeholder);
21180
21408
  return fileResponse(c, indexPath);
21181
21409
  };
21182
21410
  }
@@ -21745,10 +21973,10 @@ function validateNoUi(noUi, uiDist) {
21745
21973
  }
21746
21974
 
21747
21975
  // server/paths.ts
21748
- import { existsSync as existsSync25, statSync as statSync8 } from "fs";
21749
- import { dirname as dirname17, isAbsolute as isAbsolute9, join as join17, resolve as resolve34 } from "path";
21976
+ import { existsSync as existsSync26, statSync as statSync10 } from "fs";
21977
+ import { dirname as dirname18, isAbsolute as isAbsolute9, join as join18, resolve as resolve34 } from "path";
21750
21978
  import { fileURLToPath as fileURLToPath5 } from "url";
21751
- var DEFAULT_UI_REL = join17("ui", "dist", "ui", "browser");
21979
+ var DEFAULT_UI_REL = join18("ui", "dist", "ui", "browser");
21752
21980
  var PACKAGE_UI_REL = "ui";
21753
21981
  var INDEX_HTML2 = "index.html";
21754
21982
  function resolveDefaultUiDist(ctx) {
@@ -21760,10 +21988,10 @@ function resolveExplicitUiDist(ctx, raw) {
21760
21988
  return isAbsolute9(raw) ? raw : resolve34(ctx.cwd, raw);
21761
21989
  }
21762
21990
  function isUiBundleDir(path) {
21763
- if (!existsSync25(path)) return false;
21991
+ if (!existsSync26(path)) return false;
21764
21992
  try {
21765
- if (!statSync8(path).isDirectory()) return false;
21766
- return existsSync25(join17(path, INDEX_HTML2));
21993
+ if (!statSync10(path).isDirectory()) return false;
21994
+ return existsSync26(join18(path, INDEX_HTML2));
21767
21995
  } catch {
21768
21996
  return false;
21769
21997
  }
@@ -21771,7 +21999,7 @@ function isUiBundleDir(path) {
21771
21999
  function resolvePackageBundledUi() {
21772
22000
  let here;
21773
22001
  try {
21774
- here = dirname17(fileURLToPath5(import.meta.url));
22002
+ here = dirname18(fileURLToPath5(import.meta.url));
21775
22003
  } catch {
21776
22004
  return null;
21777
22005
  }
@@ -21780,11 +22008,11 @@ function resolvePackageBundledUi() {
21780
22008
  function resolvePackageBundledUiFrom(here) {
21781
22009
  let current = here;
21782
22010
  for (let i = 0; i < 8; i++) {
21783
- const candidate = join17(current, PACKAGE_UI_REL);
22011
+ const candidate = join18(current, PACKAGE_UI_REL);
21784
22012
  if (isUiBundleDir(candidate)) return candidate;
21785
- const distHere = join17(current, "dist", PACKAGE_UI_REL);
22013
+ const distHere = join18(current, "dist", PACKAGE_UI_REL);
21786
22014
  if (isUiBundleDir(distHere)) return distHere;
21787
- const parent = dirname17(current);
22015
+ const parent = dirname18(current);
21788
22016
  if (parent === current) return null;
21789
22017
  current = parent;
21790
22018
  }
@@ -21793,9 +22021,9 @@ function resolvePackageBundledUiFrom(here) {
21793
22021
  function walkUpForUi(startDir) {
21794
22022
  let current = resolve34(startDir);
21795
22023
  for (let i = 0; i < 64; i++) {
21796
- const candidate = join17(current, DEFAULT_UI_REL);
22024
+ const candidate = join18(current, DEFAULT_UI_REL);
21797
22025
  if (isUiBundleDir(candidate)) return candidate;
21798
- const parent = dirname17(current);
22026
+ const parent = dirname18(current);
21799
22027
  if (parent === current) return null;
21800
22028
  current = parent;
21801
22029
  }
@@ -22211,7 +22439,7 @@ var ServeCommand = class extends SmCommand {
22211
22439
  return ExitCode.Error;
22212
22440
  }
22213
22441
  const dbPath = resolveDbPath({ db: this.db, ...runtimeCtx });
22214
- if (this.db !== void 0 && !existsSync26(dbPath)) {
22442
+ if (this.db !== void 0 && !existsSync27(dbPath)) {
22215
22443
  this.printer.info(
22216
22444
  tx(SERVE_TEXTS.dbNotFound, { path: sanitizeForTerminal(dbPath) })
22217
22445
  );
@@ -22707,7 +22935,7 @@ function rankConfidenceForGrouping(c) {
22707
22935
  }
22708
22936
 
22709
22937
  // cli/commands/sidecar.ts
22710
- import { existsSync as existsSync27, unlinkSync as unlinkSync2 } from "fs";
22938
+ import { existsSync as existsSync28, unlinkSync as unlinkSync2 } from "fs";
22711
22939
  import { resolve as resolve35 } from "path";
22712
22940
  import { Command as Command35, Option as Option33 } from "clipanion";
22713
22941
 
@@ -23148,7 +23376,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23148
23376
  return ExitCode.Error;
23149
23377
  }
23150
23378
  const sidecarAbsPath = sidecarPathFor(absPath);
23151
- if (existsSync27(sidecarAbsPath) && this.force !== true) {
23379
+ if (existsSync28(sidecarAbsPath) && this.force !== true) {
23152
23380
  this.printer.error(
23153
23381
  tx(SIDECAR_TEXTS.annotateExists, {
23154
23382
  glyph: errGlyph,
@@ -23158,7 +23386,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23158
23386
  );
23159
23387
  return ExitCode.Error;
23160
23388
  }
23161
- if (existsSync27(sidecarAbsPath) && this.force === true) {
23389
+ if (existsSync28(sidecarAbsPath) && this.force === true) {
23162
23390
  try {
23163
23391
  unlinkSync2(sidecarAbsPath);
23164
23392
  } catch (err) {
@@ -23388,9 +23616,9 @@ var STUB_COMMANDS = [
23388
23616
  ];
23389
23617
 
23390
23618
  // cli/commands/tutorial.ts
23391
- import { existsSync as existsSync28, readFileSync as readFileSync18 } from "fs";
23619
+ import { existsSync as existsSync29, readFileSync as readFileSync19 } from "fs";
23392
23620
  import { writeFile as writeFile2 } from "fs/promises";
23393
- import { dirname as dirname18, join as join18, resolve as resolve36 } from "path";
23621
+ import { dirname as dirname19, join as join19, resolve as resolve36 } from "path";
23394
23622
  import { fileURLToPath as fileURLToPath6 } from "url";
23395
23623
  import { Command as Command37, Option as Option35 } from "clipanion";
23396
23624
 
@@ -23487,7 +23715,7 @@ var TutorialCommand = class extends SmCommand {
23487
23715
  }
23488
23716
  const variant = rawVariant ?? DEFAULT_VARIANT;
23489
23717
  const spec = VARIANT_SPECS[variant];
23490
- const target = join18(ctx.cwd, spec.filename);
23718
+ const target = join19(ctx.cwd, spec.filename);
23491
23719
  if (await pathExists(target) && !this.force) {
23492
23720
  this.printer.error(
23493
23721
  tx(TUTORIAL_TEXTS.alreadyExists, {
@@ -23561,7 +23789,7 @@ function loadBundledTutorialText(variant) {
23561
23789
  }
23562
23790
  function readTutorialFromDisk(variant) {
23563
23791
  const spec = VARIANT_SPECS[variant];
23564
- const here = dirname18(fileURLToPath6(import.meta.url));
23792
+ const here = dirname19(fileURLToPath6(import.meta.url));
23565
23793
  const candidates = [
23566
23794
  // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/SKILL.md
23567
23795
  resolve36(here, "../../..", spec.sourcePath),
@@ -23571,8 +23799,8 @@ function readTutorialFromDisk(variant) {
23571
23799
  resolve36(here, "../cli/tutorial", spec.bundledName)
23572
23800
  ];
23573
23801
  for (const candidate of candidates) {
23574
- if (existsSync28(candidate)) {
23575
- return readFileSync18(candidate, "utf8");
23802
+ if (existsSync29(candidate)) {
23803
+ return readFileSync19(candidate, "utf8");
23576
23804
  }
23577
23805
  }
23578
23806
  throw new Error(`SKILL.md not found in any candidate location (last tried: ${candidates[candidates.length - 1]})`);
@@ -23745,7 +23973,7 @@ await lifecycleDispatcher.dispatch(
23745
23973
  process.exit(exitCode);
23746
23974
  function resolveBareDefault() {
23747
23975
  const ctx = defaultRuntimeContext();
23748
- if (existsSync29(defaultProjectDbPath(ctx))) {
23976
+ if (existsSync30(defaultProjectDbPath(ctx))) {
23749
23977
  return ["serve"];
23750
23978
  }
23751
23979
  process.stderr.write(tx(ENTRY_TEXTS.bareNoProject, { cwd: ctx.cwd }));