agentsmesh 0.22.0 → 0.23.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.
package/dist/engine.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { stringify, parse } from 'yaml';
3
3
  import { basename, join, dirname, relative, win32, posix, sep, resolve, extname } from 'path';
4
- import { readFile, rm, mkdir, readdir, stat, lstat, unlink, writeFile, rename, chmod, access, realpath, mkdtemp, cp } from 'fs/promises';
5
- import { existsSync, readFileSync, constants, realpathSync, statSync } from 'fs';
4
+ import { readFile, rm, mkdir, readdir, stat, lstat, unlink, writeFile, rename, chmod, realpath, access, mkdtemp, cp } from 'fs/promises';
5
+ import { setTimeout } from 'timers/promises';
6
+ import { existsSync, readFileSync, constants, readdirSync, realpathSync, statSync } from 'fs';
6
7
  import { parse as parse$1 } from 'smol-toml';
7
8
  import { Buffer } from 'buffer';
8
9
  import { homedir, tmpdir } from 'os';
@@ -11,6 +12,7 @@ import { execFile } from 'child_process';
11
12
  import { fileURLToPath, pathToFileURL, URL } from 'url';
12
13
  import { promisify } from 'util';
13
14
  import * as tar from 'tar';
15
+ import picomatch from 'picomatch';
14
16
  import { createTwoFilesPatch } from 'diff';
15
17
 
16
18
  var __defProp = Object.defineProperty;
@@ -45,7 +47,7 @@ function validateCapabilityImplementations(descriptor31, capabilities17, ctx, pa
45
47
  function validateDescriptor(value) {
46
48
  return targetDescriptorSchema.parse(value);
47
49
  }
48
- var capabilityLevelSchema, capabilitiesSchema, generatorsSchema, pathResolversSchema, layoutSchema, globalSupportSchema, legacyGlobalKeys, generatorRequirements, settingsBackedFeatures, conversionDefaultsSchema, metadataSchema, targetDescriptorSchemaBase, targetDescriptorSchema;
50
+ var capabilityLevelSchema, capabilitiesSchema, generatorsSchema, pathResolversSchema, layoutSchema, globalSupportSchema, legacyGlobalKeys, generatorRequirements, settingsBackedFeatures, conversionDefaultsSchema, metadataSchema, nativePickStrategySchema, nativeInstallSchema, targetDescriptorSchemaBase, targetDescriptorSchema;
49
51
  var init_target_descriptor_schema = __esm({
50
52
  "src/targets/catalog/target-descriptor.schema.ts"() {
51
53
  capabilityLevelSchema = z.union([
@@ -119,6 +121,22 @@ var init_target_descriptor_schema = __esm({
119
121
  officialUrl: z.string().min(1),
120
122
  shortDescription: z.string().min(1)
121
123
  }).passthrough();
124
+ nativePickStrategySchema = z.discriminatedUnion("kind", [
125
+ z.object({ kind: z.literal("basename"), suffix: z.string().min(1) }),
126
+ z.object({ kind: z.literal("skillDir") }),
127
+ z.object({ kind: z.literal("firstSegment") })
128
+ ]);
129
+ nativeInstallSchema = z.object({
130
+ pickPaths: z.array(
131
+ z.object({
132
+ prefix: z.string().min(1),
133
+ feature: z.enum(["commands", "rules", "agents", "skills"]),
134
+ strategy: nativePickStrategySchema
135
+ })
136
+ ).optional(),
137
+ inferPick: z.function().optional(),
138
+ dialectHints: z.array(z.object({ frontmatterKey: z.string().min(1) })).optional()
139
+ }).strict();
122
140
  targetDescriptorSchemaBase = z.object({
123
141
  id: z.string().regex(/^[a-z][a-z0-9-]*$/, "Target id must be lowercase with hyphens"),
124
142
  metadata: metadataSchema,
@@ -130,6 +148,7 @@ var init_target_descriptor_schema = __esm({
130
148
  globalSupport: globalSupportSchema.optional(),
131
149
  buildImportPaths: z.function(),
132
150
  detectionPaths: z.array(z.string()),
151
+ nativeInstall: nativeInstallSchema.optional(),
133
152
  excludeFromStarterInit: z.boolean().optional(),
134
153
  conversionDefaults: conversionDefaultsSchema.optional(),
135
154
  emitScopedSettings: z.function().optional(),
@@ -630,15 +649,19 @@ var init_errors = __esm({
630
649
  LockAcquisitionError = class extends AgentsMeshError {
631
650
  lockPath;
632
651
  holder;
652
+ /** Human-readable lock name surfaced in the message, e.g. "lessons lock". */
653
+ label;
633
654
  constructor(lockPath, holder, options) {
655
+ const label = options?.label ?? "lock";
634
656
  super(
635
657
  "AM_LOCK_ACQUISITION_FAILED",
636
- `Could not acquire generate lock at ${lockPath}: currently held by ${holder}. Wait for the other process to finish, or remove ${lockPath} manually if you are sure no agentsmesh process is running.`,
658
+ `Could not acquire ${label} at ${lockPath}: currently held by ${holder}. Wait for the other process to finish, or remove ${lockPath} manually if you are sure no agentsmesh process is running.`,
637
659
  options
638
660
  );
639
661
  this.name = "LockAcquisitionError";
640
662
  this.lockPath = lockPath;
641
663
  this.holder = holder;
664
+ this.label = label;
642
665
  }
643
666
  };
644
667
  FileSystemError = class extends AgentsMeshError {
@@ -803,6 +826,27 @@ var init_fs_traverse = __esm({
803
826
  MAX_SEGMENT_REPETITIONS = 3;
804
827
  }
805
828
  });
829
+ async function renameWithRetry(from, to, options = {}) {
830
+ const attempts = options.attempts ?? 5;
831
+ const delayMs = options.delayMs ?? 50;
832
+ for (let attempt = 0; ; attempt++) {
833
+ try {
834
+ await rename(from, to);
835
+ return;
836
+ } catch (err) {
837
+ const code = err.code;
838
+ const transient = code !== void 0 && TRANSIENT_RENAME_CODES.has(code);
839
+ if (!transient || attempt >= attempts - 1) throw err;
840
+ await setTimeout(delayMs * 2 ** attempt);
841
+ }
842
+ }
843
+ }
844
+ var TRANSIENT_RENAME_CODES;
845
+ var init_rename_retry = __esm({
846
+ "src/utils/filesystem/rename-retry.ts"() {
847
+ TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY", "ENOTEMPTY", "EEXIST"]);
848
+ }
849
+ });
806
850
  async function readFileSafe(path) {
807
851
  try {
808
852
  const data = await readFile(path, "utf-8");
@@ -889,6 +933,7 @@ var init_fs = __esm({
889
933
  init_fs_text_encoding();
890
934
  init_fs_traverse();
891
935
  init_fs_text_encoding();
936
+ init_rename_retry();
892
937
  }
893
938
  });
894
939
  function escapeRegExp(value) {
@@ -897,19 +942,24 @@ function escapeRegExp(value) {
897
942
  function managedBlockPattern(start, end) {
898
943
  return new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`, "g");
899
944
  }
900
- function replaceManagedBlock(content, start, end, block) {
901
- const pattern = managedBlockPattern(start, end);
902
- if (pattern.test(content)) {
903
- return content.replace(pattern, block).trim();
904
- }
905
- const trimmed = content.trim();
906
- return trimmed ? `${trimmed}
907
-
908
- ${block}` : block;
909
- }
910
945
  function stripManagedBlock(content, start, end) {
911
946
  return content.replace(managedBlockPattern(start, end), "").trim();
912
947
  }
948
+ function splitFrontmatterPrefix(content) {
949
+ if (content.indexOf("---") !== 0) return { prefix: "", body: content.trim() };
950
+ const close = content.indexOf("---", 3);
951
+ if (close === -1) return { prefix: "", body: content.trim() };
952
+ return { prefix: content.slice(0, close + 3), body: content.slice(close + 3).trim() };
953
+ }
954
+ function insertAtBodyTop(content, block) {
955
+ const { prefix, body } = splitFrontmatterPrefix(content);
956
+ const placed = body ? `${block}
957
+
958
+ ${body}` : block;
959
+ return prefix ? `${prefix}
960
+
961
+ ${placed}` : placed;
962
+ }
913
963
  function ruleSource(source) {
914
964
  const normalized = source.replace(/\\/g, "/");
915
965
  const meshIndex = normalized.lastIndexOf(".agentsmesh/");
@@ -1011,31 +1061,9 @@ var init_managed_blocks = __esm({
1011
1061
  });
1012
1062
 
1013
1063
  // src/targets/projection/root-instruction-paragraph.ts
1014
- function normalizeWhitespace(value) {
1015
- return value.replace(/\s+/g, " ").trim();
1016
- }
1017
1064
  function appendAgentsmeshRootInstructionParagraph(content) {
1018
- const trimmed = content.trim();
1019
- if (trimmed.includes(ROOT_CONTRACT_START) && trimmed.includes(ROOT_CONTRACT_END)) {
1020
- return replaceManagedBlock(
1021
- trimmed,
1022
- ROOT_CONTRACT_START,
1023
- ROOT_CONTRACT_END,
1024
- AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH
1025
- );
1026
- }
1027
- const norm = normalizeWhitespace(trimmed);
1028
- if (norm.includes(normalizeWhitespace(AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH))) {
1029
- return trimmed;
1030
- }
1031
- for (const legacy of LEGACY_FORMS) {
1032
- if (norm.includes(normalizeWhitespace(legacy))) {
1033
- return trimmed.replace(legacy, AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH);
1034
- }
1035
- }
1036
- return trimmed ? `${trimmed}
1037
-
1038
- ${AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH}` : AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH;
1065
+ const withoutPrior = stripAgentsmeshRootInstructionParagraph(content);
1066
+ return insertAtBodyTop(withoutPrior, AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH);
1039
1067
  }
1040
1068
  function stripAgentsmeshRootInstructionParagraph(content) {
1041
1069
  let result = stripManagedBlock(content, ROOT_CONTRACT_START, ROOT_CONTRACT_END);
@@ -2009,9 +2037,8 @@ function shouldRewritePathToken(fullContent, start, end, matchText, rewriteBareP
2009
2037
  const before = fullContent[start - 1];
2010
2038
  const after = fullContent[end];
2011
2039
  if (isMarkdownReferenceDefinitionDestination(fullContent, start, candidateEnd)) return true;
2012
- if (before === "'" && after === "'" || before === '"' && after === '"' || before === "`" && after === "`") {
2013
- return true;
2014
- }
2040
+ if (before === "`" && after === "`") return true;
2041
+ if (before === "'" && after === "'" || before === '"' && after === '"') return false;
2015
2042
  if (before === "<" && after === ">") return true;
2016
2043
  if (before === "[" && after === "]") {
2017
2044
  if (!rewriteBarePathTokens && !isRootRelativePathToken(normalizedCandidate) && markdownBracketLabelDuplicatesDestination(fullContent, start, matchText)) {
@@ -3338,12 +3365,12 @@ var init_augment_code = __esm({
3338
3365
  });
3339
3366
 
3340
3367
  // src/targets/claude-code/constants.ts
3341
- var CLAUDE_CODE_TARGET, CLAUDE_ROOT, CLAUDE_LEGACY_ROOT, CLAUDE_RULES_DIR, CLAUDE_COMMANDS_DIR, CLAUDE_AGENTS_DIR, CLAUDE_SKILLS_DIR, CLAUDE_SETTINGS, CLAUDE_HOOKS_JSON, CLAUDE_OUTPUT_STYLES_DIR, CLAUDE_IGNORE, CLAUDE_MCP_JSON, CLAUDE_GLOBAL_MCP_JSON, CLAUDE_CANONICAL_RULES_DIR, CLAUDE_CANONICAL_COMMANDS_DIR, CLAUDE_CANONICAL_AGENTS_DIR, CLAUDE_CANONICAL_SKILLS_DIR, CLAUDE_CANONICAL_MCP, CLAUDE_CANONICAL_PERMISSIONS, CLAUDE_CANONICAL_HOOKS, CLAUDE_CANONICAL_IGNORE;
3368
+ var CLAUDE_CODE_TARGET, CLAUDE_ROOT, CLAUDE_NESTED_ROOT, CLAUDE_RULES_DIR, CLAUDE_COMMANDS_DIR, CLAUDE_AGENTS_DIR, CLAUDE_SKILLS_DIR, CLAUDE_SETTINGS, CLAUDE_HOOKS_JSON, CLAUDE_OUTPUT_STYLES_DIR, CLAUDE_IGNORE, CLAUDE_MCP_JSON, CLAUDE_GLOBAL_MCP_JSON, CLAUDE_CANONICAL_RULES_DIR, CLAUDE_CANONICAL_COMMANDS_DIR, CLAUDE_CANONICAL_AGENTS_DIR, CLAUDE_CANONICAL_SKILLS_DIR, CLAUDE_CANONICAL_MCP, CLAUDE_CANONICAL_PERMISSIONS, CLAUDE_CANONICAL_HOOKS, CLAUDE_CANONICAL_IGNORE;
3342
3369
  var init_constants7 = __esm({
3343
3370
  "src/targets/claude-code/constants.ts"() {
3344
3371
  CLAUDE_CODE_TARGET = "claude-code";
3345
- CLAUDE_ROOT = ".claude/CLAUDE.md";
3346
- CLAUDE_LEGACY_ROOT = "CLAUDE.md";
3372
+ CLAUDE_ROOT = "CLAUDE.md";
3373
+ CLAUDE_NESTED_ROOT = ".claude/CLAUDE.md";
3347
3374
  CLAUDE_RULES_DIR = ".claude/rules";
3348
3375
  CLAUDE_COMMANDS_DIR = ".claude/commands";
3349
3376
  CLAUDE_AGENTS_DIR = ".claude/agents";
@@ -5205,7 +5232,8 @@ var init_amp2 = __esm({
5205
5232
  markAsRoot: true
5206
5233
  }
5207
5234
  },
5208
- emitScopedSettings(canonical, _scope) {
5235
+ emitScopedSettings(canonical, _scope, enabledFeatures) {
5236
+ if (!enabledFeatures.has("mcp")) return [];
5209
5237
  if (!canonical.mcp || Object.keys(canonical.mcp.mcpServers).length === 0) return [];
5210
5238
  return [
5211
5239
  {
@@ -5957,12 +5985,12 @@ function mergeAugmentSettings(existing, newContent) {
5957
5985
  if (overlay.hooks !== void 0) base.hooks = overlay.hooks;
5958
5986
  return JSON.stringify(base, null, 2);
5959
5987
  }
5960
- function buildSettingsContent(canonical) {
5988
+ function buildSettingsContent(canonical, enabledFeatures) {
5961
5989
  const settings = {};
5962
- if (canonical.mcp && Object.keys(canonical.mcp.mcpServers).length > 0) {
5990
+ if (enabledFeatures.has("mcp") && canonical.mcp && Object.keys(canonical.mcp.mcpServers).length > 0) {
5963
5991
  settings.mcpServers = canonical.mcp.mcpServers;
5964
5992
  }
5965
- if (canonical.hooks && Object.keys(canonical.hooks).length > 0) {
5993
+ if (enabledFeatures.has("hooks") && canonical.hooks && Object.keys(canonical.hooks).length > 0) {
5966
5994
  settings.hooks = serializeHooksForSettings(canonical.hooks);
5967
5995
  }
5968
5996
  if (Object.keys(settings).length === 0) return null;
@@ -6091,8 +6119,8 @@ var init_augment_code2 = __esm({
6091
6119
  ],
6092
6120
  layout: globalLayout5
6093
6121
  },
6094
- emitScopedSettings(canonical) {
6095
- const content = buildSettingsContent(canonical);
6122
+ emitScopedSettings(canonical, _scope, enabledFeatures) {
6123
+ const content = buildSettingsContent(canonical, enabledFeatures);
6096
6124
  if (content === null) return [];
6097
6125
  return [{ path: AUGMENT_CODE_SETTINGS_FILE, content }];
6098
6126
  },
@@ -6666,7 +6694,10 @@ var init_claude_code2 = __esm({
6666
6694
  skillDir: ".claude/skills",
6667
6695
  managedOutputs: {
6668
6696
  dirs: [".claude/agents", ".claude/commands", ".claude/rules", ".claude/skills"],
6669
- files: [".claude/CLAUDE.md", ".claude/settings.json", ".claudeignore", ".mcp.json"]
6697
+ // CLAUDE_NESTED_ROOT is the pre-migration project location; listing it here lets
6698
+ // `cleanupStaleGeneratedOutputs` evict a leftover `.claude/CLAUDE.md` once generation
6699
+ // writes the root `CLAUDE.md`, so Claude Code never concatenates both into context.
6700
+ files: [CLAUDE_ROOT, CLAUDE_NESTED_ROOT, ".claude/settings.json", ".claudeignore", ".mcp.json"]
6670
6701
  },
6671
6702
  paths: {
6672
6703
  rulePath(slug, _rule) {
@@ -6681,7 +6712,7 @@ var init_claude_code2 = __esm({
6681
6712
  }
6682
6713
  };
6683
6714
  globalLayout6 = {
6684
- rootInstructionPath: CLAUDE_ROOT,
6715
+ rootInstructionPath: CLAUDE_NESTED_ROOT,
6685
6716
  skillDir: ".claude/skills",
6686
6717
  renderPrimaryRootInstruction: renderClaudeGlobalPrimaryInstructions,
6687
6718
  managedOutputs: {
@@ -6694,7 +6725,7 @@ var init_claude_code2 = __esm({
6694
6725
  ".agents/skills"
6695
6726
  ],
6696
6727
  files: [
6697
- ".claude/CLAUDE.md",
6728
+ CLAUDE_NESTED_ROOT,
6698
6729
  ".claude/settings.json",
6699
6730
  CLAUDE_GLOBAL_MCP_JSON,
6700
6731
  CLAUDE_HOOKS_JSON,
@@ -6702,6 +6733,7 @@ var init_claude_code2 = __esm({
6702
6733
  ]
6703
6734
  },
6704
6735
  rewriteGeneratedPath(path) {
6736
+ if (path === CLAUDE_ROOT) return CLAUDE_NESTED_ROOT;
6705
6737
  if (path === CLAUDE_MCP_JSON) return CLAUDE_GLOBAL_MCP_JSON;
6706
6738
  return path;
6707
6739
  },
@@ -6765,10 +6797,11 @@ var init_claude_code2 = __esm({
6765
6797
  importer: {
6766
6798
  rules: [
6767
6799
  {
6768
- // Root rule: prefer .claude/CLAUDE.md, fall back to legacy CLAUDE.md (project only).
6800
+ // Root rule: project prefers root CLAUDE.md, falls back to nested .claude/CLAUDE.md;
6801
+ // global reads the nested .claude/CLAUDE.md.
6769
6802
  feature: "rules",
6770
6803
  mode: "singleFile",
6771
- source: { project: [CLAUDE_ROOT, CLAUDE_LEGACY_ROOT], global: [CLAUDE_ROOT] },
6804
+ source: { project: [CLAUDE_ROOT, CLAUDE_NESTED_ROOT], global: [CLAUDE_NESTED_ROOT] },
6772
6805
  canonicalDir: CLAUDE_CANONICAL_RULES_DIR,
6773
6806
  canonicalRootFilename: "_root.md",
6774
6807
  markAsRoot: true
@@ -6814,7 +6847,23 @@ var init_claude_code2 = __esm({
6814
6847
  }
6815
6848
  },
6816
6849
  buildImportPaths: buildClaudeCodeImportPaths,
6817
- detectionPaths: ["CLAUDE.md", ".claude/rules", ".claude/commands"]
6850
+ detectionPaths: [CLAUDE_ROOT, CLAUDE_NESTED_ROOT, ".claude/rules", ".claude/commands"],
6851
+ nativeInstall: {
6852
+ pickPaths: [
6853
+ {
6854
+ prefix: ".claude/commands",
6855
+ feature: "commands",
6856
+ strategy: { kind: "basename", suffix: ".md" }
6857
+ },
6858
+ { prefix: ".claude/rules", feature: "rules", strategy: { kind: "basename", suffix: ".md" } },
6859
+ {
6860
+ prefix: ".claude/agents",
6861
+ feature: "agents",
6862
+ strategy: { kind: "basename", suffix: ".md" }
6863
+ },
6864
+ { prefix: ".claude/skills/", feature: "skills", strategy: { kind: "firstSegment" } }
6865
+ ]
6866
+ }
6818
6867
  };
6819
6868
  }
6820
6869
  });
@@ -7693,6 +7742,16 @@ var init_cline2 = __esm({
7693
7742
  },
7694
7743
  buildImportPaths: buildClineImportPaths,
7695
7744
  detectionPaths: [".clinerules", ".cline"],
7745
+ nativeInstall: {
7746
+ pickPaths: [
7747
+ { prefix: CLINE_SKILLS_DIR, feature: "skills", strategy: { kind: "skillDir" } },
7748
+ {
7749
+ prefix: CLINE_WORKFLOWS_DIR,
7750
+ feature: "commands",
7751
+ strategy: { kind: "basename", suffix: ".md" }
7752
+ }
7753
+ ]
7754
+ },
7696
7755
  conversionDefaults: { agentsToSkills: true }
7697
7756
  };
7698
7757
  }
@@ -8611,6 +8670,11 @@ var init_codex_cli2 = __esm({
8611
8670
  ".codex/agents",
8612
8671
  ".codex/rules"
8613
8672
  ],
8673
+ nativeInstall: {
8674
+ pickPaths: [
8675
+ { prefix: ".codex", feature: "rules", strategy: { kind: "basename", suffix: ".md" } }
8676
+ ]
8677
+ },
8614
8678
  excludeFromStarterInit: true,
8615
8679
  conversionDefaults: { commandsToSkills: true, agentsToSkills: false }
8616
8680
  };
@@ -9114,6 +9178,21 @@ var init_continue2 = __esm({
9114
9178
  },
9115
9179
  buildImportPaths: buildContinueImportPaths,
9116
9180
  detectionPaths: [".continue/rules", ".continue/skills", ".continue/mcpServers"],
9181
+ nativeInstall: {
9182
+ pickPaths: [
9183
+ {
9184
+ prefix: ".continue/rules",
9185
+ feature: "rules",
9186
+ strategy: { kind: "basename", suffix: ".md" }
9187
+ },
9188
+ {
9189
+ prefix: ".continue/prompts",
9190
+ feature: "commands",
9191
+ strategy: { kind: "basename", suffix: ".md" }
9192
+ },
9193
+ { prefix: ".continue/skills", feature: "skills", strategy: { kind: "skillDir" } }
9194
+ ]
9195
+ },
9117
9196
  conversionDefaults: { agentsToSkills: true }
9118
9197
  };
9119
9198
  }
@@ -9481,6 +9560,80 @@ var init_importer10 = __esm({
9481
9560
  init_copilot2();
9482
9561
  }
9483
9562
  });
9563
+ async function skillNamesFromNativeSkillDir(scanRoot) {
9564
+ const files = await readDirRecursive(scanRoot);
9565
+ const names = /* @__PURE__ */ new Set();
9566
+ for (const f of files) {
9567
+ if (basename(f) === "SKILL.md") {
9568
+ names.add(basename(dirname(f)));
9569
+ continue;
9570
+ }
9571
+ const rel2 = relative(scanRoot, f).replace(/\\/g, "/");
9572
+ if (!rel2.includes("/") && f.toLowerCase().endsWith(".md")) {
9573
+ names.add(basename(f, ".md"));
9574
+ }
9575
+ }
9576
+ return [...names].filter(Boolean).sort();
9577
+ }
9578
+ var init_native_skill_scan = __esm({
9579
+ "src/install/native/native-skill-scan.ts"() {
9580
+ init_fs();
9581
+ }
9582
+ });
9583
+ async function inferCopilotPickFromPath(repoRoot, posixPath) {
9584
+ const scan = join(repoRoot, ...posixPath.split("/"));
9585
+ if (posixPath.startsWith(COPILOT_PROMPTS_DIR)) {
9586
+ const files = await readDirRecursive(scan);
9587
+ const commands = [
9588
+ ...new Set(
9589
+ files.filter((f) => f.toLowerCase().endsWith(".prompt.md")).map((f) => basename(f, ".prompt.md"))
9590
+ )
9591
+ ].sort();
9592
+ return commands.length ? { commands } : {};
9593
+ }
9594
+ if (posixPath.startsWith(".github/copilot") && !posixPath.includes("copilot-instructions.md")) {
9595
+ const files = await readDirRecursive(scan);
9596
+ const rules = [
9597
+ ...new Set(
9598
+ files.filter((f) => f.includes(".instructions.md")).map((f) => basename(f).replace(/\.instructions\.md$/i, ""))
9599
+ )
9600
+ ].sort();
9601
+ return rules.length ? { rules } : {};
9602
+ }
9603
+ if (posixPath.startsWith(".github/instructions")) {
9604
+ const files = await readDirRecursive(scan);
9605
+ const names = /* @__PURE__ */ new Set();
9606
+ for (const f of files) {
9607
+ const b = basename(f);
9608
+ if (b.toLowerCase().endsWith(".instructions.md"))
9609
+ names.add(b.replace(/\.instructions\.md$/i, ""));
9610
+ else if (b.toLowerCase().endsWith(".md")) names.add(basename(f, ".md"));
9611
+ }
9612
+ const rules = [...names].sort();
9613
+ return rules.length ? { rules } : {};
9614
+ }
9615
+ if (posixPath.startsWith(".github/skills")) {
9616
+ const skills = await skillNamesFromNativeSkillDir(scan);
9617
+ return skills.length ? { skills } : {};
9618
+ }
9619
+ if (posixPath.startsWith(".github/agents")) {
9620
+ const files = await readDirRecursive(scan);
9621
+ const agents = [
9622
+ ...new Set(
9623
+ files.filter((f) => f.toLowerCase().endsWith(".agent.md")).map((f) => basename(f, ".agent.md"))
9624
+ )
9625
+ ].sort();
9626
+ return agents.length ? { agents } : {};
9627
+ }
9628
+ return {};
9629
+ }
9630
+ var init_native_path_pick_infer_copilot = __esm({
9631
+ "src/install/native/native-path-pick-infer-copilot.ts"() {
9632
+ init_fs();
9633
+ init_constants29();
9634
+ init_native_skill_scan();
9635
+ }
9636
+ });
9484
9637
  function pruneUndefined3(record) {
9485
9638
  for (const key of Object.keys(record)) {
9486
9639
  if (record[key] === void 0) delete record[key];
@@ -9770,6 +9923,7 @@ var init_copilot2 = __esm({
9770
9923
  init_generator11();
9771
9924
  init_constants29();
9772
9925
  init_importer10();
9926
+ init_native_path_pick_infer_copilot();
9773
9927
  init_import_mappers4();
9774
9928
  init_linter10();
9775
9929
  init_import_map_builders();
@@ -9984,7 +10138,8 @@ var init_copilot2 = __esm({
9984
10138
  ".github/skills",
9985
10139
  ".github/agents",
9986
10140
  ".github/hooks"
9987
- ]
10141
+ ],
10142
+ nativeInstall: { inferPick: inferCopilotPickFromPath }
9988
10143
  };
9989
10144
  }
9990
10145
  });
@@ -11497,6 +11652,23 @@ var init_cursor2 = __esm({
11497
11652
  },
11498
11653
  buildImportPaths: buildCursorImportPaths,
11499
11654
  detectionPaths: [".cursor/rules", ".cursor/mcp.json"],
11655
+ nativeInstall: {
11656
+ pickPaths: [
11657
+ { prefix: ".cursor/rules", feature: "rules", strategy: { kind: "basename", suffix: ".mdc" } },
11658
+ {
11659
+ prefix: ".cursor/commands",
11660
+ feature: "commands",
11661
+ strategy: { kind: "basename", suffix: ".md" }
11662
+ },
11663
+ {
11664
+ prefix: ".cursor/agents",
11665
+ feature: "agents",
11666
+ strategy: { kind: "basename", suffix: ".md" }
11667
+ },
11668
+ { prefix: ".cursor/skills", feature: "skills", strategy: { kind: "skillDir" } }
11669
+ ],
11670
+ dialectHints: [{ frontmatterKey: "alwaysApply" }]
11671
+ },
11500
11672
  preservesManualActivation: true
11501
11673
  };
11502
11674
  }
@@ -12258,18 +12430,18 @@ function mapHookEvent2(event) {
12258
12430
  return null;
12259
12431
  }
12260
12432
  }
12261
- function generateGeminiSettingsFiles(canonical) {
12433
+ function generateGeminiSettingsFiles(canonical, enabledFeatures) {
12262
12434
  const settings = {};
12263
12435
  let hasAnyNativeSettings = false;
12264
- if (canonical.mcp && Object.keys(canonical.mcp.mcpServers).length > 0) {
12436
+ if (enabledFeatures.has("mcp") && canonical.mcp && Object.keys(canonical.mcp.mcpServers).length > 0) {
12265
12437
  settings.mcpServers = canonical.mcp.mcpServers;
12266
12438
  hasAnyNativeSettings = true;
12267
12439
  }
12268
- if (canonical.agents.length > 0) {
12440
+ if (enabledFeatures.has("agents") && canonical.agents.length > 0) {
12269
12441
  settings.experimental = { enableAgents: true };
12270
12442
  hasAnyNativeSettings = true;
12271
12443
  }
12272
- if (canonical.hooks) {
12444
+ if (enabledFeatures.has("hooks") && canonical.hooks) {
12273
12445
  const hookEntries = Object.entries(canonical.hooks).flatMap(([event, entries]) => {
12274
12446
  const mappedEvent = mapHookEvent2(event);
12275
12447
  if (!mappedEvent || !Array.isArray(entries)) return [];
@@ -12885,6 +13057,36 @@ var init_importer15 = __esm({
12885
13057
  init_importer_skills_agents();
12886
13058
  }
12887
13059
  });
13060
+ function isUnderGeminiCommands(pathInRepoPosix) {
13061
+ const p = pathInRepoPosix.replace(/^\/+|\/+$/g, "");
13062
+ return p === ".gemini/commands" || p.startsWith(".gemini/commands/");
13063
+ }
13064
+ async function inferGeminiCommandNamesFromFiles(repoRoot, pathInRepoPosix) {
13065
+ const commandsRoot = join(repoRoot, ...GEMINI_COMMANDS_DIR.split("/"));
13066
+ const scanDir = join(repoRoot, ...pathInRepoPosix.split("/"));
13067
+ const files = await readDirRecursive(scanDir);
13068
+ const names = [];
13069
+ for (const f of files) {
13070
+ if (!/\.(toml|md)$/i.test(f)) continue;
13071
+ const rel2 = relative(commandsRoot, f).replace(/\\/g, "/");
13072
+ if (rel2.startsWith("..") || rel2 === "") continue;
13073
+ const noExt = rel2.replace(/\.(toml|md)$/i, "");
13074
+ const name = noExt.split("/").filter(Boolean).join(":");
13075
+ if (name) names.push(name);
13076
+ }
13077
+ return [...new Set(names)].sort();
13078
+ }
13079
+ async function inferGeminiPick(repoRoot, posixPath) {
13080
+ if (!isUnderGeminiCommands(posixPath)) return {};
13081
+ const commands = await inferGeminiCommandNamesFromFiles(repoRoot, posixPath);
13082
+ return commands.length ? { commands } : {};
13083
+ }
13084
+ var init_gemini_install_commands = __esm({
13085
+ "src/install/native/gemini-install-commands.ts"() {
13086
+ init_fs();
13087
+ init_constants30();
13088
+ }
13089
+ });
12888
13090
  async function mapGeminiRuleFile(relativePath, destDir, normalizeTo) {
12889
13091
  const relativeMdPath = relativePath.replace(/\\/g, "/");
12890
13092
  const destPath = join(destDir, relativeMdPath);
@@ -12996,12 +13198,12 @@ var init_lint12 = __esm({
12996
13198
  });
12997
13199
 
12998
13200
  // src/targets/gemini-cli/scoped-settings-emit.ts
12999
- function emitScopedGeminiSettings(canonical, scope) {
13201
+ function emitScopedGeminiSettings(canonical, scope, enabledFeatures) {
13000
13202
  if (scope === "project") {
13001
13203
  const caps = getTargetCapabilities("gemini-cli", scope);
13002
13204
  if (caps?.ignore.flavor !== "settings-embedded") return [];
13003
13205
  }
13004
- return generateGeminiSettingsFiles(canonical);
13206
+ return generateGeminiSettingsFiles(canonical, enabledFeatures);
13005
13207
  }
13006
13208
  var init_scoped_settings_emit = __esm({
13007
13209
  "src/targets/gemini-cli/scoped-settings-emit.ts"() {
@@ -13019,6 +13221,7 @@ var init_gemini_cli2 = __esm({
13019
13221
  init_policies_generator();
13020
13222
  init_constants30();
13021
13223
  init_importer15();
13224
+ init_gemini_install_commands();
13022
13225
  init_import_mappers6();
13023
13226
  init_linter15();
13024
13227
  init_import_map_builders();
@@ -13214,6 +13417,7 @@ var init_gemini_cli2 = __esm({
13214
13417
  },
13215
13418
  buildImportPaths: buildGeminiCliImportPaths,
13216
13419
  detectionPaths: ["GEMINI.md", ".gemini"],
13420
+ nativeInstall: { inferPick: inferGeminiPick },
13217
13421
  conversionDefaults: { agentsToSkills: false }
13218
13422
  };
13219
13423
  }
@@ -14055,7 +14259,23 @@ var init_junie2 = __esm({
14055
14259
  ".junie/skills",
14056
14260
  ".junie/mcp/mcp.json",
14057
14261
  ".aiignore"
14058
- ]
14262
+ ],
14263
+ nativeInstall: {
14264
+ pickPaths: [
14265
+ {
14266
+ prefix: ".junie/commands",
14267
+ feature: "commands",
14268
+ strategy: { kind: "basename", suffix: ".md" }
14269
+ },
14270
+ { prefix: ".junie/rules", feature: "rules", strategy: { kind: "basename", suffix: ".md" } },
14271
+ {
14272
+ prefix: ".junie/agents",
14273
+ feature: "agents",
14274
+ strategy: { kind: "basename", suffix: ".md" }
14275
+ },
14276
+ { prefix: ".junie/skills", feature: "skills", strategy: { kind: "skillDir" } }
14277
+ ]
14278
+ }
14059
14279
  };
14060
14280
  }
14061
14281
  });
@@ -18405,6 +18625,16 @@ var init_windsurf2 = __esm({
18405
18625
  },
18406
18626
  buildImportPaths: buildWindsurfImportPaths,
18407
18627
  detectionPaths: [".windsurfrules", ".windsurf"],
18628
+ nativeInstall: {
18629
+ pickPaths: [
18630
+ {
18631
+ prefix: ".windsurf/rules",
18632
+ feature: "rules",
18633
+ strategy: { kind: "basename", suffix: ".md" }
18634
+ }
18635
+ ],
18636
+ dialectHints: [{ frontmatterKey: "trigger" }]
18637
+ },
18408
18638
  conversionDefaults: { agentsToSkills: true }
18409
18639
  };
18410
18640
  }
@@ -18679,7 +18909,8 @@ var init_zed2 = __esm({
18679
18909
  markAsRoot: true
18680
18910
  }
18681
18911
  },
18682
- emitScopedSettings(canonical, _scope) {
18912
+ emitScopedSettings(canonical, _scope, enabledFeatures) {
18913
+ if (!enabledFeatures.has("mcp")) return [];
18683
18914
  if (!canonical.mcp || Object.keys(canonical.mcp.mcpServers).length === 0) return [];
18684
18915
  const contextServers = {};
18685
18916
  for (const [name, server] of Object.entries(canonical.mcp.mcpServers)) {
@@ -18734,7 +18965,10 @@ function getBuiltinTargetDefinition(target31) {
18734
18965
  function getTargetCapabilities(target31, scope = "project") {
18735
18966
  const descriptor31 = getBuiltinTargetDefinition(target31) ?? getDescriptor(target31);
18736
18967
  if (!descriptor31) return void 0;
18737
- const raw = scope === "global" ? descriptor31.globalSupport?.capabilities ?? descriptor31.capabilities : descriptor31.capabilities;
18968
+ if (scope === "global" && !descriptor31.globalSupport) {
18969
+ return normalizeTargetCapabilities(ALL_NONE_CAPABILITIES);
18970
+ }
18971
+ const raw = scope === "global" ? descriptor31.globalSupport.capabilities : descriptor31.capabilities;
18738
18972
  return normalizeTargetCapabilities(raw);
18739
18973
  }
18740
18974
  function getTargetDetectionPaths(target31, scope = "project") {
@@ -18790,6 +19024,7 @@ function isConversionUpgrading(descriptor31, feature, config, scope) {
18790
19024
  function getEffectiveTargetSupportLevel(target31, feature, config, scope = "project") {
18791
19025
  const baseLevel = getTargetCapabilities(target31, scope)?.[feature]?.level ?? "none";
18792
19026
  const descriptor31 = getBuiltinTargetDefinition(target31) ?? getDescriptor(target31);
19027
+ if (scope === "global" && descriptor31 && !descriptor31.globalSupport) return "none";
18793
19028
  if (baseLevel === "none" && isConversionUpgrading(descriptor31, feature, config, scope)) {
18794
19029
  return "embedded";
18795
19030
  }
@@ -18803,7 +19038,7 @@ function resolveTargetFeatureGenerator(target31, feature, config, scope = "proje
18803
19038
  const pick = PICK_FEATURE_GENERATOR[feature];
18804
19039
  return pick === null ? void 0 : pick(descriptor31.generators);
18805
19040
  }
18806
- var BUILTIN_TARGETS, _builtinTargetsMap, PICK_FEATURE_GENERATOR;
19041
+ var ALL_NONE_CAPABILITIES, BUILTIN_TARGETS, _builtinTargetsMap, PICK_FEATURE_GENERATOR;
18807
19042
  var init_builtin_targets = __esm({
18808
19043
  "src/targets/catalog/builtin-targets.ts"() {
18809
19044
  init_conversions();
@@ -18841,6 +19076,17 @@ var init_builtin_targets = __esm({
18841
19076
  init_warp2();
18842
19077
  init_windsurf2();
18843
19078
  init_zed2();
19079
+ ALL_NONE_CAPABILITIES = {
19080
+ rules: "none",
19081
+ additionalRules: "none",
19082
+ commands: "none",
19083
+ agents: "none",
19084
+ skills: "none",
19085
+ mcp: "none",
19086
+ hooks: "none",
19087
+ ignore: "none",
19088
+ permissions: "none"
19089
+ };
18844
19090
  BUILTIN_TARGETS = [
18845
19091
  descriptor,
18846
19092
  descriptor2,
@@ -19271,6 +19517,23 @@ function rewriteGeneratedReferences(results, canonical, config, projectRoot, sco
19271
19517
  init_path_helpers();
19272
19518
  init_link_rebaser_helpers();
19273
19519
 
19520
+ // src/utils/output/color.ts
19521
+ function noColorRequested() {
19522
+ const value = process.env.NO_COLOR;
19523
+ return value !== void 0 && value !== "";
19524
+ }
19525
+ function forceColorRequested() {
19526
+ const value = process.env.FORCE_COLOR;
19527
+ if (value === void 0) return void 0;
19528
+ return value !== "0" && value !== "false";
19529
+ }
19530
+ function colorEnabled(stream = process.stdout) {
19531
+ const forced = forceColorRequested();
19532
+ if (forced !== void 0) return forced;
19533
+ if (noColorRequested()) return false;
19534
+ return stream.isTTY === true;
19535
+ }
19536
+
19274
19537
  // src/utils/output/logger.ts
19275
19538
  var C = {
19276
19539
  green: "\x1B[32m",
@@ -19279,16 +19542,14 @@ var C = {
19279
19542
  cyan: "\x1B[36m",
19280
19543
  reset: "\x1B[0m"
19281
19544
  };
19282
- function out(text) {
19283
- {
19284
- process.stdout.write(text);
19285
- }
19545
+ function outStream() {
19546
+ return process.stdout;
19286
19547
  }
19287
- function noColor() {
19288
- return process.env.NO_COLOR !== void 0 && process.env.NO_COLOR !== "";
19548
+ function out(text) {
19549
+ outStream().write(text);
19289
19550
  }
19290
- function c(code, text) {
19291
- return noColor() ? text : `${code}${text}${C.reset}`;
19551
+ function c(code, text, stream) {
19552
+ return colorEnabled(stream) ? `${code}${text}${C.reset}` : text;
19292
19553
  }
19293
19554
  function pad(str, width) {
19294
19555
  const len = [...str].length;
@@ -19296,20 +19557,20 @@ function pad(str, width) {
19296
19557
  }
19297
19558
  var logger = {
19298
19559
  info(msg) {
19299
- out(c(C.cyan, msg) + "\n");
19560
+ out(c(C.cyan, msg, outStream()) + "\n");
19300
19561
  },
19301
19562
  warn(msg) {
19302
- process.stderr.write(c(C.yellow, "\u26A0 ") + msg + "\n");
19563
+ process.stderr.write(c(C.yellow, "\u26A0 ", process.stderr) + msg + "\n");
19303
19564
  },
19304
19565
  error(msg) {
19305
- process.stderr.write(c(C.red, "\u2717 ") + msg + "\n");
19566
+ process.stderr.write(c(C.red, "\u2717 ", process.stderr) + msg + "\n");
19306
19567
  },
19307
19568
  success(msg) {
19308
- out(c(C.green, "\u2713 ") + msg + "\n");
19569
+ out(c(C.green, "\u2713 ", outStream()) + msg + "\n");
19309
19570
  },
19310
19571
  debug(msg) {
19311
19572
  if (process.env.AGENTSMESH_DEBUG === "1") {
19312
- out(c(C.cyan, "[debug] ") + msg + "\n");
19573
+ out(c(C.cyan, "[debug] ", outStream()) + msg + "\n");
19313
19574
  }
19314
19575
  },
19315
19576
  table(rows) {
@@ -19804,12 +20065,12 @@ async function generateHooksFeature(results, targets, canonical, projectRoot, sc
19804
20065
  }
19805
20066
  }
19806
20067
  }
19807
- async function generateScopedSettingsFeature(results, targets, canonical, projectRoot, scope) {
20068
+ async function generateScopedSettingsFeature(results, targets, canonical, projectRoot, scope, enabledFeatures) {
19808
20069
  for (const target31 of targets) {
19809
20070
  const descriptor31 = getBuiltinTargetDefinition(target31) ?? getDescriptor(target31);
19810
20071
  const emit = descriptor31?.emitScopedSettings;
19811
20072
  if (!emit) continue;
19812
- const outputs = emit(canonical, scope);
20073
+ const outputs = emit(canonical, scope, enabledFeatures);
19813
20074
  if (outputs.length === 0) continue;
19814
20075
  for (const out2 of outputs) {
19815
20076
  await emitGeneratedOutput(results, target31, out2, projectRoot, scope, {
@@ -19910,7 +20171,14 @@ async function generate(ctx) {
19910
20171
  }
19911
20172
  }
19912
20173
  if (hasMcp || hasIgnore || hasHooks || hasAgents || hasPermissions) {
19913
- await generateScopedSettingsFeature(results, targets, canonical, projectRoot, scope);
20174
+ await generateScopedSettingsFeature(
20175
+ results,
20176
+ targets,
20177
+ canonical,
20178
+ projectRoot,
20179
+ scope,
20180
+ enabledFeatures
20181
+ );
19914
20182
  }
19915
20183
  const decoratedResults = decoratePrimaryRootInstructions(results, canonical, scope);
19916
20184
  const sharedPaths = computeSharedRootInstructionPaths(decoratedResults, scope);
@@ -20234,7 +20502,7 @@ async function fetchGitRemoteExtend(parsed, extendName, options, cacheDir, build
20234
20502
  await cloneRepo(resolveCloneUrl(parsed), stagedRepoDir);
20235
20503
  if (parsed.ref) await checkoutRef(stagedRepoDir, parsed.ref);
20236
20504
  await rm(cacheRoot, { recursive: true, force: true });
20237
- await rename(stagedRoot, cacheRoot);
20505
+ await renameWithRetry(stagedRoot, cacheRoot);
20238
20506
  return readCachedRepo(cacheRepoDir);
20239
20507
  } catch (err) {
20240
20508
  await rm(stagedRoot, { recursive: true, force: true });
@@ -22585,87 +22853,964 @@ function lintRuleScopeInversion(input) {
22585
22853
  }
22586
22854
  return out2;
22587
22855
  }
22588
- var TriggersSchema = z.object({
22589
- file_globs: z.array(z.string()),
22590
- command_patterns: z.array(z.string()),
22591
- keywords: z.array(z.string())
22592
- }).refine((t) => t.file_globs.length + t.command_patterns.length + t.keywords.length > 0, {
22593
- message: "cluster must declare at least one trigger of any type"
22594
- });
22595
- var ClusterSchema = z.object({
22596
- topic: z.string().regex(/^[a-z0-9-]+$/, "topic must be kebab-case"),
22597
- /**
22598
- * Project-relative path (forward slashes) to the cluster's markdown body.
22599
- * Conventionally `.agentsmesh/lessons/topics/<topic>.md`, but any project
22600
- * path is accepted universal across every agent target.
22601
- */
22602
- file: z.string().regex(/\.md$/, "file must be a .md path"),
22603
- summary: z.string().min(1),
22604
- triggers: TriggersSchema
22605
- });
22606
- var LessonsIndexSchema = z.object({
22607
- version: z.literal(1),
22608
- /**
22609
- * Zero clusters is valid — supports `agentsmesh init --lessons` scaffolding a
22610
- * fresh project. Topics accumulate via `distill:apply` as failures are
22611
- * captured.
22612
- */
22613
- clusters: z.array(ClusterSchema)
22614
- });
22856
+ var CURRENT_GRAPH_VERSION = 1;
22857
+ var IdSchema = z.string().regex(/^[a-z0-9-]+$/, "id must be kebab-case");
22858
+ var DateSchema = z.string().regex(
22859
+ /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z?)?$/,
22860
+ "createdAt must be ISO-8601 date or datetime"
22861
+ );
22862
+ var TopicSchema = z.object({
22863
+ summary: z.string().min(1, "topic summary must not be empty")
22864
+ }).strict();
22865
+ var TriggerKindSchema = z.enum(["file_glob", "command_pattern", "keyword"]);
22866
+ var TriggerSchema = z.object({
22867
+ kind: TriggerKindSchema,
22868
+ pattern: z.string().min(1, "trigger pattern must not be empty")
22869
+ }).strict();
22870
+ var LessonStatusSchema = z.enum(["active", "deprecated", "superseded"]);
22871
+ var LessonSchema = z.object({
22872
+ rule: z.string().min(1, "lesson rule must not be empty"),
22873
+ rationale: z.string().min(1).optional(),
22874
+ topics: z.array(IdSchema).min(1, "lesson must reference at least one topic"),
22875
+ triggers: z.array(IdSchema),
22876
+ evidence: z.array(z.string().min(1)),
22877
+ status: LessonStatusSchema,
22878
+ supersededBy: IdSchema.optional(),
22879
+ createdAt: DateSchema
22880
+ }).strict();
22881
+ var LessonsGraphSchema = z.object({
22882
+ version: z.literal(CURRENT_GRAPH_VERSION),
22883
+ lessons: z.record(IdSchema, LessonSchema),
22884
+ topics: z.record(IdSchema, TopicSchema),
22885
+ triggers: z.record(IdSchema, TriggerSchema)
22886
+ }).strict();
22887
+ function parseGraph(raw) {
22888
+ return LessonsGraphSchema.parse(raw);
22889
+ }
22890
+
22891
+ // src/lessons/graph-store.ts
22892
+ var GRAPH_REL_PATH = ".agentsmesh/lessons/lessons.json";
22893
+ function graphFilePath(projectRoot) {
22894
+ return resolve(projectRoot, GRAPH_REL_PATH);
22895
+ }
22896
+ function loadLessonsGraph(projectRoot) {
22897
+ const raw = readFileSync(graphFilePath(projectRoot), "utf8");
22898
+ return parseGraph(JSON.parse(raw));
22899
+ }
22615
22900
  var BASE_REL = ".agentsmesh/lessons";
22616
22901
  function lessonsPaths(projectRoot) {
22617
22902
  const base = join(projectRoot, BASE_REL);
22618
22903
  return {
22619
22904
  base,
22905
+ graph: join(base, "lessons.json"),
22906
+ config: join(base, "config.json"),
22620
22907
  journal: join(base, "journal.md"),
22621
22908
  index: join(base, "index.yaml"),
22622
- ledger: join(base, "distill-ledger.yaml"),
22623
- proposal: join(base, "distill-proposal.md"),
22624
22909
  topicsDir: join(base, "topics")
22625
22910
  };
22626
22911
  }
22912
+ function toRelPath(projectRoot, absolute) {
22913
+ return relative(projectRoot, absolute).split(sep).join("/");
22914
+ }
22915
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
22916
+ var MAX_FILES = 2e5;
22917
+ function listProjectFiles(projectRoot) {
22918
+ const out2 = /* @__PURE__ */ new Set();
22919
+ try {
22920
+ const stack = [projectRoot];
22921
+ while (stack.length > 0) {
22922
+ const dir = stack.pop();
22923
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
22924
+ if (entry.isDirectory()) {
22925
+ if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
22926
+ } else if (entry.isFile()) {
22927
+ out2.add(toRelPath(projectRoot, join(dir, entry.name)));
22928
+ if (out2.size > MAX_FILES) return out2;
22929
+ }
22930
+ }
22931
+ }
22932
+ } catch {
22933
+ return null;
22934
+ }
22935
+ return out2;
22936
+ }
22937
+
22938
+ // src/lessons/validate-checks.ts
22939
+ function collectDanglingRefs(graph, findings) {
22940
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
22941
+ for (const topicId of lesson.topics) {
22942
+ if (graph.topics[topicId] === void 0) {
22943
+ findings.push({
22944
+ level: "error",
22945
+ code: "DANGLING_TOPIC",
22946
+ message: `Lesson "${lessonId}" references unknown topic "${topicId}".`,
22947
+ lessonId,
22948
+ topicId
22949
+ });
22950
+ }
22951
+ }
22952
+ for (const triggerId of lesson.triggers) {
22953
+ if (graph.triggers[triggerId] === void 0) {
22954
+ findings.push({
22955
+ level: "error",
22956
+ code: "DANGLING_TRIGGER",
22957
+ message: `Lesson "${lessonId}" references unknown trigger "${triggerId}".`,
22958
+ lessonId,
22959
+ triggerId
22960
+ });
22961
+ }
22962
+ }
22963
+ if (lesson.supersededBy !== void 0 && graph.lessons[lesson.supersededBy] === void 0) {
22964
+ findings.push({
22965
+ level: "error",
22966
+ code: "DANGLING_SUPERSEDER",
22967
+ message: `Lesson "${lessonId}" supersededBy unknown lesson "${lesson.supersededBy}".`,
22968
+ lessonId
22969
+ });
22970
+ }
22971
+ }
22972
+ }
22973
+ function collectDuplicateRefs(graph, findings) {
22974
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
22975
+ for (const topicId of firstDuplicates(lesson.topics)) {
22976
+ findings.push({
22977
+ level: "error",
22978
+ code: "DUPLICATE_TOPIC_REF",
22979
+ message: `Lesson "${lessonId}" references topic "${topicId}" more than once.`,
22980
+ lessonId,
22981
+ topicId
22982
+ });
22983
+ }
22984
+ for (const triggerId of firstDuplicates(lesson.triggers)) {
22985
+ findings.push({
22986
+ level: "error",
22987
+ code: "DUPLICATE_TRIGGER_REF",
22988
+ message: `Lesson "${lessonId}" references trigger "${triggerId}" more than once.`,
22989
+ lessonId,
22990
+ triggerId
22991
+ });
22992
+ }
22993
+ }
22994
+ }
22995
+ function firstDuplicates(ids) {
22996
+ const seen = /* @__PURE__ */ new Set();
22997
+ const dup = /* @__PURE__ */ new Set();
22998
+ for (const id of ids) {
22999
+ if (seen.has(id)) dup.add(id);
23000
+ else seen.add(id);
23001
+ }
23002
+ return [...dup];
23003
+ }
23004
+ function collectStatusInvariants(graph, findings) {
23005
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
23006
+ if (lesson.status === "superseded" && lesson.supersededBy === void 0) {
23007
+ findings.push({
23008
+ level: "error",
23009
+ code: "SUPERSEDED_WITHOUT_TARGET",
23010
+ message: `Lesson "${lessonId}" has status "superseded" but no supersededBy target.`,
23011
+ lessonId
23012
+ });
23013
+ }
23014
+ if (lesson.status === "active" && lesson.supersededBy !== void 0) {
23015
+ findings.push({
23016
+ level: "error",
23017
+ code: "ACTIVE_WITH_SUPERSEDER",
23018
+ message: `Lesson "${lessonId}" has status "active" but declares supersededBy.`,
23019
+ lessonId
23020
+ });
23021
+ }
23022
+ }
23023
+ }
23024
+ function collectLifecycleInvariants(graph, findings) {
23025
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
23026
+ if (lesson.supersededBy === void 0) continue;
23027
+ if (lesson.supersededBy === lessonId) {
23028
+ findings.push({
23029
+ level: "error",
23030
+ code: "SELF_SUPERSEDED",
23031
+ message: `Lesson "${lessonId}" is superseded by itself.`,
23032
+ lessonId
23033
+ });
23034
+ continue;
23035
+ }
23036
+ const target31 = graph.lessons[lesson.supersededBy];
23037
+ if (target31 !== void 0 && target31.status !== "active") {
23038
+ findings.push({
23039
+ level: "error",
23040
+ code: "INACTIVE_SUPERSEDER",
23041
+ message: `Lesson "${lessonId}" is superseded by "${lesson.supersededBy}", which is itself ${target31.status} \u2014 the chain dead-ends with no live replacement.`,
23042
+ lessonId
23043
+ });
23044
+ }
23045
+ }
23046
+ collectSupersedeCycles(graph, findings);
23047
+ }
23048
+ function collectSupersedeCycles(graph, findings) {
23049
+ const reported = /* @__PURE__ */ new Set();
23050
+ for (const startId of Object.keys(graph.lessons)) {
23051
+ const seen = /* @__PURE__ */ new Set();
23052
+ let cur = startId;
23053
+ while (cur !== void 0) {
23054
+ if (seen.has(cur)) {
23055
+ if (cur !== startId || reported.has(cur)) break;
23056
+ reported.add(cur);
23057
+ findings.push({
23058
+ level: "error",
23059
+ code: "SUPERSEDE_CYCLE",
23060
+ message: `Lesson "${startId}" is part of a supersededBy cycle.`,
23061
+ lessonId: startId
23062
+ });
23063
+ break;
23064
+ }
23065
+ seen.add(cur);
23066
+ const next = graph.lessons[cur]?.supersededBy;
23067
+ if (next === cur) break;
23068
+ cur = next;
23069
+ }
23070
+ }
23071
+ }
23072
+ function collectReachability(graph, findings) {
23073
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
23074
+ if (lesson.status === "active" && lesson.triggers.length === 0) {
23075
+ findings.push({
23076
+ level: "warning",
23077
+ code: "UNREACHABLE_LESSON",
23078
+ message: `Active lesson "${lessonId}" has no triggers and can never be recalled.`,
23079
+ lessonId
23080
+ });
23081
+ }
23082
+ }
23083
+ }
23084
+ function collectOrphans(graph, findings) {
23085
+ const referencedTopics = /* @__PURE__ */ new Set();
23086
+ const referencedTriggers = /* @__PURE__ */ new Set();
23087
+ for (const lesson of Object.values(graph.lessons)) {
23088
+ for (const t of lesson.topics) referencedTopics.add(t);
23089
+ for (const t of lesson.triggers) referencedTriggers.add(t);
23090
+ }
23091
+ for (const topicId of Object.keys(graph.topics)) {
23092
+ if (!referencedTopics.has(topicId)) {
23093
+ findings.push({
23094
+ level: "warning",
23095
+ code: "ORPHAN_TOPIC",
23096
+ message: `Topic "${topicId}" is not referenced by any lesson.`,
23097
+ topicId
23098
+ });
23099
+ }
23100
+ }
23101
+ for (const triggerId of Object.keys(graph.triggers)) {
23102
+ if (!referencedTriggers.has(triggerId)) {
23103
+ findings.push({
23104
+ level: "warning",
23105
+ code: "ORPHAN_TRIGGER",
23106
+ message: `Trigger "${triggerId}" is not referenced by any lesson.`,
23107
+ triggerId
23108
+ });
23109
+ }
23110
+ }
23111
+ }
23112
+
23113
+ // src/lessons/ranking-text.ts
23114
+ var STOP = /* @__PURE__ */ new Set([
23115
+ "the",
23116
+ "a",
23117
+ "an",
23118
+ "to",
23119
+ "of",
23120
+ "in",
23121
+ "and",
23122
+ "or",
23123
+ "for",
23124
+ "is",
23125
+ "on",
23126
+ "at",
23127
+ "with",
23128
+ "be",
23129
+ "as",
23130
+ "it",
23131
+ "that",
23132
+ "this",
23133
+ "its",
23134
+ "must"
23135
+ ]);
23136
+ function tokenize(text) {
23137
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
23138
+ }
23139
+
23140
+ // src/lessons/keyword-signal.ts
23141
+ var MAX_RECOMMENDED_KEYWORD_TOKENS = 5;
23142
+ function isLowSignalKeyword(pattern) {
23143
+ return tokenize(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
23144
+ }
23145
+ function splitRawTokens(pattern) {
23146
+ return pattern.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
23147
+ }
23148
+ function keywordNeedleLosesTokens(pattern) {
23149
+ const raw = splitRawTokens(pattern);
23150
+ if (raw.length < 2) return false;
23151
+ return tokenize(pattern).length !== raw.length;
23152
+ }
23153
+
23154
+ // src/lessons/regex-linear/nfa-compile.ts
23155
+ var MAX_NFA_STATES = 2e3;
23156
+ var Builder = class {
23157
+ states = [];
23158
+ alloc() {
23159
+ if (this.states.length >= MAX_NFA_STATES) {
23160
+ throw new Error(`NFA state limit exceeded (${MAX_NFA_STATES}); pattern expands too large`);
23161
+ }
23162
+ this.states.push({ eps: [], asserts: [], chars: [] });
23163
+ return this.states.length - 1;
23164
+ }
23165
+ };
23166
+ function isNonLineTerminator(c2) {
23167
+ return c2 !== "\n" && c2 !== "\r" && c2 !== "\u2028" && c2 !== "\u2029";
23168
+ }
23169
+ function compileNode(b, node) {
23170
+ switch (node.k) {
23171
+ case "empty":
23172
+ case "assert": {
23173
+ const s = b.alloc();
23174
+ const e = b.alloc();
23175
+ if (node.k === "assert") b.states[s].asserts.push({ kind: node.kind, target: e });
23176
+ else b.states[s].eps.push(e);
23177
+ return { start: s, end: e };
23178
+ }
23179
+ case "char":
23180
+ case "any":
23181
+ case "class": {
23182
+ const s = b.alloc();
23183
+ const e = b.alloc();
23184
+ const test = node.k === "char" ? (c2) => c2 === node.ch : node.k === "any" ? isNonLineTerminator : node.test;
23185
+ b.states[s].chars.push({ test, target: e });
23186
+ return { start: s, end: e };
23187
+ }
23188
+ case "concat": {
23189
+ if (node.items.length === 0) return compileNode(b, { k: "empty" });
23190
+ let first = null;
23191
+ let prevEnd = -1;
23192
+ for (const item of node.items) {
23193
+ const frag = compileNode(b, item);
23194
+ if (first === null) first = frag;
23195
+ else b.states[prevEnd].eps.push(frag.start);
23196
+ prevEnd = frag.end;
23197
+ }
23198
+ return { start: first.start, end: prevEnd };
23199
+ }
23200
+ case "alt": {
23201
+ const s = b.alloc();
23202
+ const e = b.alloc();
23203
+ for (const opt of node.opts) {
23204
+ const frag = compileNode(b, opt);
23205
+ b.states[s].eps.push(frag.start);
23206
+ b.states[frag.end].eps.push(e);
23207
+ }
23208
+ return { start: s, end: e };
23209
+ }
23210
+ case "opt": {
23211
+ const s = b.alloc();
23212
+ const e = b.alloc();
23213
+ const frag = compileNode(b, node.node);
23214
+ b.states[s].eps.push(frag.start, e);
23215
+ b.states[frag.end].eps.push(e);
23216
+ return { start: s, end: e };
23217
+ }
23218
+ case "star": {
23219
+ const s = b.alloc();
23220
+ const e = b.alloc();
23221
+ const frag = compileNode(b, node.node);
23222
+ b.states[s].eps.push(frag.start, e);
23223
+ b.states[frag.end].eps.push(frag.start, e);
23224
+ return { start: s, end: e };
23225
+ }
23226
+ case "plus": {
23227
+ const e = b.alloc();
23228
+ const frag = compileNode(b, node.node);
23229
+ b.states[frag.end].eps.push(frag.start, e);
23230
+ return { start: frag.start, end: e };
23231
+ }
23232
+ }
23233
+ }
23234
+ function compileNfa(ast) {
23235
+ const b = new Builder();
23236
+ const { start, end } = compileNode(b, ast);
23237
+ return { states: b.states, start, accept: end };
23238
+ }
23239
+
23240
+ // src/lessons/regex-linear/nfa.ts
23241
+ function wordBoundary(input, pos) {
23242
+ const before = pos > 0 && /[A-Za-z0-9_]/.test(input[pos - 1]);
23243
+ const after = pos < input.length && /[A-Za-z0-9_]/.test(input[pos]);
23244
+ return before !== after;
23245
+ }
23246
+ function assertHolds(kind, input, pos) {
23247
+ switch (kind) {
23248
+ case "start":
23249
+ return pos === 0;
23250
+ case "end":
23251
+ return pos === input.length;
23252
+ case "wordB":
23253
+ return wordBoundary(input, pos);
23254
+ case "nonWordB":
23255
+ return !wordBoundary(input, pos);
23256
+ }
23257
+ }
23258
+ function buildMatcher(ast) {
23259
+ const { states, start, accept } = compileNfa(ast);
23260
+ const closure = (set, idx, input, pos, budget) => {
23261
+ const stack = [idx];
23262
+ while (stack.length > 0) {
23263
+ if (budget.remaining <= 0) return;
23264
+ const cur = stack.pop();
23265
+ if (set.has(cur)) continue;
23266
+ set.add(cur);
23267
+ budget.remaining -= 1;
23268
+ for (const t of states[cur].eps) if (!set.has(t)) stack.push(t);
23269
+ for (const a of states[cur].asserts) {
23270
+ if (assertHolds(a.kind, input, pos) && !set.has(a.target)) stack.push(a.target);
23271
+ }
23272
+ }
23273
+ };
23274
+ return {
23275
+ // The input is matched in full (no truncation — truncation would miss suffix
23276
+ // matches and let `$` falsely match an invented endpoint). Work is bounded by
23277
+ // the shared budget instead: when it runs out we report a safe non-match.
23278
+ test(input, budget) {
23279
+ const b = budget ?? { remaining: Number.POSITIVE_INFINITY };
23280
+ if (b.remaining <= 0) return false;
23281
+ let current = /* @__PURE__ */ new Set();
23282
+ for (let pos = 0; pos <= input.length; pos += 1) {
23283
+ closure(current, start, input, pos, b);
23284
+ if (current.has(accept)) return true;
23285
+ if (b.remaining <= 0) return false;
23286
+ if (pos === input.length) break;
23287
+ const ch = input[pos];
23288
+ const next = /* @__PURE__ */ new Set();
23289
+ for (const s of current) {
23290
+ b.remaining -= 1;
23291
+ for (const t of states[s].chars) {
23292
+ if (t.test(ch)) closure(next, t.target, input, pos + 1, b);
23293
+ }
23294
+ }
23295
+ current = next;
23296
+ }
23297
+ return current.has(accept);
23298
+ }
23299
+ };
23300
+ }
23301
+
23302
+ // src/lessons/regex-linear/ast.ts
23303
+ var UnsupportedRegexError = class extends Error {
23304
+ constructor(message) {
23305
+ super(message);
23306
+ this.name = "UnsupportedRegexError";
23307
+ }
23308
+ };
23309
+
23310
+ // src/lessons/regex-linear/parse-helpers.ts
23311
+ var MAX_REPEAT = 1e3;
23312
+ var isWord = (c2) => /[A-Za-z0-9_]/.test(c2);
23313
+ function expandRepeat(atom, min, max) {
23314
+ const items = [];
23315
+ for (let k = 0; k < min; k += 1) items.push(atom);
23316
+ if (max === Infinity) {
23317
+ items.push({ k: "star", node: atom });
23318
+ } else {
23319
+ for (let k = min; k < max; k += 1) items.push({ k: "opt", node: atom });
23320
+ }
23321
+ if (items.length === 0) return { k: "empty" };
23322
+ return items.length === 1 ? items[0] : { k: "concat", items };
23323
+ }
23324
+ function escapeClass(c2) {
23325
+ switch (c2) {
23326
+ case "d":
23327
+ return (x) => x >= "0" && x <= "9";
23328
+ case "D":
23329
+ return (x) => !(x >= "0" && x <= "9");
23330
+ case "w":
23331
+ return isWord;
23332
+ case "W":
23333
+ return (x) => !isWord(x);
23334
+ case "s":
23335
+ return (x) => /\s/.test(x);
23336
+ case "S":
23337
+ return (x) => !/\s/.test(x);
23338
+ default:
23339
+ return null;
23340
+ }
23341
+ }
23342
+ var HEX2 = /^[0-9a-fA-F]{2}$/;
23343
+ var HEX4 = /^[0-9a-fA-F]{4}$/;
23344
+ function readUnicodeEscape(src, i, c2) {
23345
+ if (c2 === "x") {
23346
+ const hex2 = src.slice(i, i + 2);
23347
+ return HEX2.test(hex2) ? { ch: String.fromCharCode(parseInt(hex2, 16)), len: 2 } : { ch: "x", len: 0 };
23348
+ }
23349
+ const hex = src.slice(i, i + 4);
23350
+ return HEX4.test(hex) ? { ch: String.fromCharCode(parseInt(hex, 16)), len: 4 } : { ch: "u", len: 0 };
23351
+ }
23352
+ function readControlEscape(src, i) {
23353
+ const x = src[i];
23354
+ if (x === void 0 || !/[A-Za-z]/.test(x)) {
23355
+ throw new UnsupportedRegexError("\\c must be followed by a letter");
23356
+ }
23357
+ return { ch: String.fromCharCode(x.charCodeAt(0) & 31), len: 1 };
23358
+ }
23359
+ function classEscapeChar(src, i, e) {
23360
+ if (e === "b") return { ch: "\b", len: 0 };
23361
+ if (e === "c") return readControlEscape(src, i);
23362
+ if (e === "x" || e === "u") return readUnicodeEscape(src, i, e);
23363
+ return { ch: escapeLiteral(e), len: 0 };
23364
+ }
23365
+ function escapeLiteral(c2) {
23366
+ switch (c2) {
23367
+ case "t":
23368
+ return " ";
23369
+ case "n":
23370
+ return "\n";
23371
+ case "r":
23372
+ return "\r";
23373
+ case "f":
23374
+ return "\f";
23375
+ case "v":
23376
+ return "\v";
23377
+ case "0":
23378
+ return "\0";
23379
+ default:
23380
+ return c2;
23381
+ }
23382
+ }
23383
+
23384
+ // src/lessons/regex-linear/parse.ts
23385
+ function parseRegex(src) {
23386
+ let i = 0;
23387
+ const peek = () => src[i];
23388
+ const eat = () => src[i++];
23389
+ function parseAlt() {
23390
+ const opts = [parseConcat()];
23391
+ while (peek() === "|") {
23392
+ i += 1;
23393
+ opts.push(parseConcat());
23394
+ }
23395
+ return opts.length === 1 ? opts[0] : { k: "alt", opts };
23396
+ }
23397
+ function parseConcat() {
23398
+ const items = [];
23399
+ while (i < src.length && peek() !== "|" && peek() !== ")") {
23400
+ items.push(parseQuantified());
23401
+ }
23402
+ if (items.length === 0) return { k: "empty" };
23403
+ return items.length === 1 ? items[0] : { k: "concat", items };
23404
+ }
23405
+ function parseQuantified() {
23406
+ const atom = parseAtom();
23407
+ const q = peek();
23408
+ if (q === "*" || q === "+" || q === "?") {
23409
+ i += 1;
23410
+ if (peek() === "?") i += 1;
23411
+ return q === "*" ? { k: "star", node: atom } : q === "+" ? { k: "plus", node: atom } : { k: "opt", node: atom };
23412
+ }
23413
+ if (q === "{") {
23414
+ const repeat = tryParseBrace();
23415
+ if (repeat !== null) return expandRepeat(atom, repeat.min, repeat.max);
23416
+ }
23417
+ return atom;
23418
+ }
23419
+ function tryParseBrace() {
23420
+ const m = /^\{(\d+)(,(\d*)?)?\}/.exec(src.slice(i));
23421
+ if (m === null) return null;
23422
+ i += m[0].length;
23423
+ if (peek() === "?") i += 1;
23424
+ const min = Number(m[1]);
23425
+ const max = m[2] === void 0 ? min : m[3] === "" || m[3] === void 0 ? Infinity : Number(m[3]);
23426
+ if (min > MAX_REPEAT || max !== Infinity && max > MAX_REPEAT) {
23427
+ throw new UnsupportedRegexError(`Repeat count over ${MAX_REPEAT} not supported: {${m[1]}\u2026}`);
23428
+ }
23429
+ return { min, max };
23430
+ }
23431
+ function parseAtom() {
23432
+ const c2 = peek();
23433
+ if (c2 === "(") return parseGroup();
23434
+ if (c2 === "[") return parseClass();
23435
+ if (c2 === "\\") return parseEscape();
23436
+ if (c2 === ".") {
23437
+ i += 1;
23438
+ return { k: "any" };
23439
+ }
23440
+ if (c2 === "^") {
23441
+ i += 1;
23442
+ return { k: "assert", kind: "start" };
23443
+ }
23444
+ if (c2 === "$") {
23445
+ i += 1;
23446
+ return { k: "assert", kind: "end" };
23447
+ }
23448
+ if (c2 === void 0 || c2 === "*" || c2 === "+" || c2 === "?" || c2 === ")") {
23449
+ throw new UnsupportedRegexError(`Unexpected '${c2 ?? "<end>"}' in pattern`);
23450
+ }
23451
+ i += 1;
23452
+ return { k: "char", ch: c2 };
23453
+ }
23454
+ function parseGroup() {
23455
+ i += 1;
23456
+ if (peek() === "?") {
23457
+ const c2 = src[i + 1];
23458
+ if (c2 === "=" || c2 === "!" || c2 === "<") {
23459
+ if (!(c2 === "<" && /[A-Za-z]/.test(src[i + 2] ?? ""))) {
23460
+ throw new UnsupportedRegexError("Lookaround assertions are not supported");
23461
+ }
23462
+ }
23463
+ if (c2 === ":") i += 2;
23464
+ else if (c2 === "<") {
23465
+ i += 2;
23466
+ while (i < src.length && src[i] !== ">") i += 1;
23467
+ i += 1;
23468
+ }
23469
+ }
23470
+ const inner = parseAlt();
23471
+ if (peek() !== ")") throw new UnsupportedRegexError("Unbalanced group");
23472
+ i += 1;
23473
+ return inner;
23474
+ }
23475
+ function parseEscape() {
23476
+ i += 1;
23477
+ const c2 = peek();
23478
+ if (c2 === void 0) throw new UnsupportedRegexError("Trailing backslash");
23479
+ if (/[1-9]/.test(c2) || c2 === "k")
23480
+ throw new UnsupportedRegexError("Backreferences are not supported");
23481
+ i += 1;
23482
+ if (c2 === "b") return { k: "assert", kind: "wordB" };
23483
+ if (c2 === "B") return { k: "assert", kind: "nonWordB" };
23484
+ const cls = escapeClass(c2);
23485
+ if (cls !== null) return { k: "class", test: cls };
23486
+ if (c2 === "x" || c2 === "u" || c2 === "c") {
23487
+ const { ch, len } = c2 === "c" ? readControlEscape(src, i) : readUnicodeEscape(src, i, c2);
23488
+ i += len;
23489
+ return { k: "char", ch };
23490
+ }
23491
+ return { k: "char", ch: escapeLiteral(c2) };
23492
+ }
23493
+ function parseClass() {
23494
+ i += 1;
23495
+ const negate = peek() === "^";
23496
+ if (negate) i += 1;
23497
+ const tests = [];
23498
+ while (i < src.length && peek() !== "]") {
23499
+ tests.push(parseClassMember());
23500
+ }
23501
+ if (peek() !== "]") throw new UnsupportedRegexError("Unterminated character class");
23502
+ i += 1;
23503
+ const base = (c2) => tests.some((t) => t(c2));
23504
+ return { k: "class", test: negate ? (c2) => !base(c2) : base };
23505
+ }
23506
+ function parseClassMember() {
23507
+ let lo;
23508
+ if (peek() === "\\") {
23509
+ i += 1;
23510
+ const e = eat();
23511
+ const cls = escapeClass(e);
23512
+ if (cls !== null) return cls;
23513
+ const r = classEscapeChar(src, i, e);
23514
+ i += r.len;
23515
+ lo = r.ch;
23516
+ } else {
23517
+ lo = eat();
23518
+ }
23519
+ if (peek() === "-" && src[i + 1] !== void 0 && src[i + 1] !== "]") {
23520
+ i += 1;
23521
+ let hi;
23522
+ if (peek() === "\\") {
23523
+ i += 1;
23524
+ const e2 = eat();
23525
+ const r = classEscapeChar(src, i, e2);
23526
+ i += r.len;
23527
+ hi = r.ch;
23528
+ } else {
23529
+ hi = eat();
23530
+ }
23531
+ const a = lo.codePointAt(0);
23532
+ const b = hi.codePointAt(0);
23533
+ return (c2) => {
23534
+ const p = c2.codePointAt(0);
23535
+ return p >= a && p <= b;
23536
+ };
23537
+ }
23538
+ return (c2) => c2 === lo;
23539
+ }
23540
+ const ast = parseAlt();
23541
+ if (i !== src.length) throw new UnsupportedRegexError(`Unexpected '${peek()}' at ${i}`);
23542
+ return ast;
23543
+ }
23544
+
23545
+ // src/lessons/regex-linear/index.ts
23546
+ var cache = /* @__PURE__ */ new Map();
23547
+ function compileLinearMatcher(pattern) {
23548
+ const hit = cache.get(pattern);
23549
+ if (hit !== void 0 || cache.has(pattern)) return hit ?? null;
23550
+ let matcher;
23551
+ try {
23552
+ matcher = buildMatcher(parseRegex(pattern));
23553
+ } catch {
23554
+ matcher = null;
23555
+ }
23556
+ cache.set(pattern, matcher);
23557
+ return matcher;
23558
+ }
23559
+
23560
+ // src/lessons/regex-safety.ts
23561
+ var MAX_PATTERN_LENGTH = 1e3;
23562
+ function isSafeRegexPattern(pattern) {
23563
+ if (pattern.length > MAX_PATTERN_LENGTH) return false;
23564
+ return compileLinearMatcher(pattern) !== null;
23565
+ }
23566
+
23567
+ // src/lessons/validate-quality.ts
23568
+ function collectDuplicateRules(graph, findings) {
23569
+ const byKey = /* @__PURE__ */ new Map();
23570
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
23571
+ if (lesson.status !== "active") continue;
23572
+ const key = normalizeRule(lesson.rule);
23573
+ const bucket = byKey.get(key) ?? [];
23574
+ bucket.push(lessonId);
23575
+ byKey.set(key, bucket);
23576
+ }
23577
+ for (const [key, ids] of byKey) {
23578
+ if (ids.length < 2) continue;
23579
+ const sorted = [...ids].sort();
23580
+ for (const lessonId of sorted) {
23581
+ const others = sorted.filter((other) => other !== lessonId);
23582
+ findings.push({
23583
+ level: "error",
23584
+ code: "DUPLICATE_RULE",
23585
+ message: `Lesson "${lessonId}" duplicates rule text of: ${others.join(", ")} (normalized key: "${key.slice(0, 60)}").`,
23586
+ lessonId
23587
+ });
23588
+ }
23589
+ }
23590
+ }
23591
+ function collectInvalidTriggerPatterns(graph, findings) {
23592
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23593
+ if (trigger.kind !== "command_pattern") continue;
23594
+ try {
23595
+ new RegExp(trigger.pattern);
23596
+ } catch (err) {
23597
+ findings.push({
23598
+ level: "error",
23599
+ code: "INVALID_TRIGGER_PATTERN",
23600
+ message: `Trigger "${triggerId}" has an invalid command_pattern regex (${trigger.pattern}): ${err instanceof Error ? err.message : String(err)}.`,
23601
+ triggerId
23602
+ });
23603
+ continue;
23604
+ }
23605
+ if (!isSafeRegexPattern(trigger.pattern)) {
23606
+ findings.push({
23607
+ level: "error",
23608
+ code: "UNSAFE_TRIGGER_PATTERN",
23609
+ message: `Trigger "${triggerId}" has a command_pattern regex outside the provably-linear subset (${trigger.pattern}): it can backtrack catastrophically (e.g. a quantified group like (a+)+ or (a|aa)+, adjacent repetition like a+a+, or a backreference/lookaround). Rewrite using a linear pattern.`,
23610
+ triggerId
23611
+ });
23612
+ }
23613
+ }
23614
+ }
23615
+ function collectBackslashGlobPatterns(graph, findings) {
23616
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23617
+ if (trigger.kind !== "file_glob") continue;
23618
+ if (!trigger.pattern.includes("\\")) continue;
23619
+ findings.push({
23620
+ level: "error",
23621
+ code: "BACKSLASH_GLOB_PATTERN",
23622
+ message: `Trigger "${triggerId}" has a file_glob pattern with a backslash (${trigger.pattern}); recall normalizes paths to forward slashes, so it never fires. Replace \\ with /.`,
23623
+ triggerId
23624
+ });
23625
+ }
23626
+ }
23627
+ function collectDuplicateTriggers(graph, findings) {
23628
+ const byKey = /* @__PURE__ */ new Map();
23629
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23630
+ const key = `${trigger.kind}|${trigger.pattern}`;
23631
+ const bucket = byKey.get(key) ?? [];
23632
+ bucket.push(triggerId);
23633
+ byKey.set(key, bucket);
23634
+ }
23635
+ for (const [key, ids] of byKey) {
23636
+ if (ids.length < 2) continue;
23637
+ const sorted = [...ids].sort();
23638
+ for (const triggerId of sorted) {
23639
+ const others = sorted.filter((other) => other !== triggerId);
23640
+ findings.push({
23641
+ level: "error",
23642
+ code: "DUPLICATE_TRIGGER",
23643
+ message: `Trigger "${triggerId}" duplicates (kind, pattern) of: ${others.join(", ")} (key: "${key}").`,
23644
+ triggerId
23645
+ });
23646
+ }
23647
+ }
23648
+ }
23649
+ var HIGH_FANOUT_THRESHOLD = 10;
23650
+ function collectFanout(graph, findings) {
23651
+ const fanout = /* @__PURE__ */ new Map();
23652
+ for (const lesson of Object.values(graph.lessons)) {
23653
+ if (lesson.status !== "active") continue;
23654
+ for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
23655
+ }
23656
+ let over = 0;
23657
+ let max = 0;
23658
+ for (const n of fanout.values()) {
23659
+ if (n > HIGH_FANOUT_THRESHOLD) over += 1;
23660
+ if (n > max) max = n;
23661
+ }
23662
+ if (over > 0) {
23663
+ findings.push({
23664
+ level: "warning",
23665
+ code: "HIGH_FANOUT_TRIGGERS",
23666
+ message: `${over} trigger(s) each match more than ${HIGH_FANOUT_THRESHOLD} active lessons (max ${max}); recall returns the ranked top by default \u2014 consider per-lesson trigger refinement to improve precision.`
23667
+ });
23668
+ }
23669
+ }
23670
+ function collectLowSignalKeywords(graph, findings) {
23671
+ const activeTriggerIds2 = /* @__PURE__ */ new Set();
23672
+ for (const lesson of Object.values(graph.lessons)) {
23673
+ if (lesson.status !== "active") continue;
23674
+ for (const t of lesson.triggers) activeTriggerIds2.add(t);
23675
+ }
23676
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23677
+ if (trigger.kind !== "keyword") continue;
23678
+ if (!activeTriggerIds2.has(triggerId)) continue;
23679
+ if (!isLowSignalKeyword(trigger.pattern)) continue;
23680
+ findings.push({
23681
+ level: "warning",
23682
+ code: "LOW_SIGNAL_KEYWORD",
23683
+ message: `Keyword trigger "${triggerId}" carries more than ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens (${trigger.pattern}); recall matches a keyword only as a substring of --keyword or a contiguous token-run in the file/command, so it rarely fires \u2014 use a short distinctive phrase.`,
23684
+ triggerId
23685
+ });
23686
+ }
23687
+ }
23688
+ function collectStopwordKeywords(graph, findings) {
23689
+ const activeTriggerIds2 = /* @__PURE__ */ new Set();
23690
+ for (const lesson of Object.values(graph.lessons)) {
23691
+ if (lesson.status !== "active") continue;
23692
+ for (const t of lesson.triggers) activeTriggerIds2.add(t);
23693
+ }
23694
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23695
+ if (trigger.kind !== "keyword") continue;
23696
+ if (!activeTriggerIds2.has(triggerId)) continue;
23697
+ if (tokenize(trigger.pattern).length !== 0 && !keywordNeedleLosesTokens(trigger.pattern)) {
23698
+ continue;
23699
+ }
23700
+ findings.push({
23701
+ level: "warning",
23702
+ code: "STOPWORD_KEYWORD",
23703
+ message: `Keyword trigger "${triggerId}" (${trigger.pattern}) loses tokens to stopword filtering, so its needle can never appear as a contiguous run on the mandatory --file/--cmd recall path \u2014 drop the stopwords (e.g. "state art" instead of "state of the art"), or detach it with \`lessons untrigger\`.`,
23704
+ triggerId
23705
+ });
23706
+ }
23707
+ }
23708
+ function normalizeRule(rule) {
23709
+ return rule.trim().replace(/\s+/g, " ").toLowerCase();
23710
+ }
23711
+ function activeTriggerIds(graph) {
23712
+ const ids = /* @__PURE__ */ new Set();
23713
+ for (const lesson of Object.values(graph.lessons)) {
23714
+ if (lesson.status !== "active") continue;
23715
+ for (const t of lesson.triggers) ids.add(t);
23716
+ }
23717
+ return ids;
23718
+ }
23719
+ function deadFileGlobIds(graph, knownPaths) {
23720
+ const active = activeTriggerIds(graph);
23721
+ const paths = [...knownPaths];
23722
+ const dead = /* @__PURE__ */ new Set();
23723
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23724
+ if (trigger.kind !== "file_glob") continue;
23725
+ if (!active.has(triggerId)) continue;
23726
+ const isMatch = picomatch(trigger.pattern, { dot: true });
23727
+ if (!paths.some((p) => isMatch(p))) dead.add(triggerId);
23728
+ }
23729
+ return dead;
23730
+ }
23731
+ function collectDeadFileGlobs(graph, findings, knownPaths) {
23732
+ for (const triggerId of deadFileGlobIds(graph, knownPaths)) {
23733
+ findings.push({
23734
+ level: "warning",
23735
+ code: "DEAD_FILE_GLOB",
23736
+ message: `file_glob trigger "${triggerId}" (${graph.triggers[triggerId]?.pattern ?? ""}) matches no file in the working tree \u2014 the lesson is unreachable via this trigger (a rename likely moved the path). Re-point it at the current path, or detach it with \`lessons untrigger\`, or run \`lessons prune --apply\`.`,
23737
+ triggerId
23738
+ });
23739
+ }
23740
+ }
23741
+ var RUNNER_ANCHOR = /^\^(pnpm|npm|npx|yarn|bun)\b/;
23742
+ function collectRunnerAnchoredPatterns(graph, findings) {
23743
+ const active = activeTriggerIds(graph);
23744
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23745
+ if (trigger.kind !== "command_pattern") continue;
23746
+ if (!active.has(triggerId)) continue;
23747
+ if (!RUNNER_ANCHOR.test(trigger.pattern)) continue;
23748
+ findings.push({
23749
+ level: "warning",
23750
+ code: "RUNNER_ANCHORED_PATTERN",
23751
+ message: `command_pattern trigger "${triggerId}" (${trigger.pattern}) is anchored to one runner \u2014 it won't fire for the same task via another runner (e.g. \`npx\` vs \`pnpm\`). Drop the \`^<runner>\` anchor and key on the task (e.g. \`\\bvitest\\b\`).`,
23752
+ triggerId
23753
+ });
23754
+ }
23755
+ }
23756
+
23757
+ // src/lessons/validate.ts
23758
+ function validateLessonsGraph(graph, options = {}) {
23759
+ const findings = [];
23760
+ const schemaResult = LessonsGraphSchema.safeParse(graph);
23761
+ if (!schemaResult.success) {
23762
+ findings.push({
23763
+ level: "error",
23764
+ code: "SCHEMA_INVALID",
23765
+ message: schemaResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
23766
+ });
23767
+ return { ok: false, findings };
23768
+ }
23769
+ collectDanglingRefs(graph, findings);
23770
+ collectDuplicateRefs(graph, findings);
23771
+ collectStatusInvariants(graph, findings);
23772
+ collectLifecycleInvariants(graph, findings);
23773
+ collectDuplicateRules(graph, findings);
23774
+ collectReachability(graph, findings);
23775
+ collectInvalidTriggerPatterns(graph, findings);
23776
+ collectBackslashGlobPatterns(graph, findings);
23777
+ collectDuplicateTriggers(graph, findings);
23778
+ collectOrphans(graph, findings);
23779
+ collectFanout(graph, findings);
23780
+ collectLowSignalKeywords(graph, findings);
23781
+ collectStopwordKeywords(graph, findings);
23782
+ collectRunnerAnchoredPatterns(graph, findings);
23783
+ if (options.knownPaths !== void 0) collectDeadFileGlobs(graph, findings, options.knownPaths);
23784
+ const ok = findings.every((f) => f.level !== "error");
23785
+ return { ok, findings };
23786
+ }
22627
23787
 
22628
23788
  // src/core/lint/shared/lessons.ts
22629
23789
  var LESSONS_TARGET = "lessons";
22630
- var INDEX_REL = ".agentsmesh/lessons/index.yaml";
23790
+ var GRAPH_REL = ".agentsmesh/lessons/lessons.json";
22631
23791
  var ROOT_RULE_REL = ".agentsmesh/rules/_root.md";
22632
23792
  var LESSONS_HEADING = /^## Lessons \(/m;
22633
- var RULES_HEADING = /^## Rules\b/m;
22634
23793
  function lintLessonsSubsystem(projectRoot, scope) {
22635
23794
  if (scope === "global") return [];
22636
23795
  const paths = lessonsPaths(projectRoot);
22637
- if (!existsSync(paths.index)) return [];
23796
+ if (!existsSync(paths.graph)) return [];
22638
23797
  const out2 = [];
22639
- const parsed = LessonsIndexSchema.safeParse(parse(readFileSync(paths.index, "utf8")));
22640
- if (!parsed.success) {
22641
- return [diag("error", INDEX_REL, `index.yaml is invalid: ${parsed.error.issues[0].message}`)];
23798
+ let graph;
23799
+ try {
23800
+ graph = loadLessonsGraph(projectRoot);
23801
+ } catch (err) {
23802
+ return [
23803
+ diag(
23804
+ "error",
23805
+ GRAPH_REL,
23806
+ `lessons.json failed to load: ${err instanceof Error ? err.message : String(err)}`
23807
+ )
23808
+ ];
22642
23809
  }
22643
- for (const cluster of parsed.data.clusters) {
22644
- const topicAbs = join(projectRoot, cluster.file);
22645
- if (!existsSync(topicAbs)) {
22646
- out2.push(
22647
- diag("error", cluster.file, `topic file for cluster "${cluster.topic}" does not exist.`)
22648
- );
22649
- continue;
22650
- }
22651
- if (!RULES_HEADING.test(readFileSync(topicAbs, "utf8"))) {
22652
- out2.push(
22653
- diag("warning", cluster.file, `topic "${cluster.topic}" is missing a "## Rules" section.`)
22654
- );
22655
- }
22656
- for (const pattern of cluster.triggers.command_patterns) {
22657
- try {
22658
- new RegExp(pattern);
22659
- } catch {
22660
- out2.push(
22661
- diag(
22662
- "warning",
22663
- INDEX_REL,
22664
- `cluster "${cluster.topic}" command_patterns entry is not a valid regex: ${pattern}`
22665
- )
22666
- );
22667
- }
22668
- }
23810
+ const knownPaths = listProjectFiles(projectRoot) ?? void 0;
23811
+ const report = validateLessonsGraph(graph, { knownPaths });
23812
+ for (const finding of report.findings) {
23813
+ out2.push(diag(finding.level, GRAPH_REL, `[${finding.code}] ${finding.message}`));
22669
23814
  }
22670
23815
  const rootRuleAbs = join(projectRoot, ROOT_RULE_REL);
22671
23816
  const rootRuleBody = existsSync(rootRuleAbs) ? readFileSync(rootRuleAbs, "utf8") : "";