agentsmesh 0.21.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/index.js CHANGED
@@ -1,16 +1,18 @@
1
1
  import { z } from 'zod';
2
- import { stringify, parse } from 'yaml';
3
- import { join, basename, dirname, relative, win32, posix, sep, extname, resolve } from 'path';
4
- import { access, readdir, readFile, realpath, stat, rm, mkdir, lstat, unlink, writeFile, rename, chmod, mkdtemp, cp } from 'fs/promises';
5
- import { constants, existsSync, realpathSync, statSync, readFileSync } from 'fs';
2
+ import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
3
+ import { join, resolve, relative, sep, dirname, basename, win32, posix, extname } from 'path';
4
+ import { mkdir, access, readdir, rm, readFile, realpath, stat, writeFile, lstat, unlink, rename, chmod, mkdtemp, cp } from 'fs/promises';
5
+ import { setTimeout as setTimeout$1 } from 'timers/promises';
6
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, constants, rmSync, renameSync, readdirSync, realpathSync, statSync } from 'fs';
6
7
  import { parse as parse$1 } from 'smol-toml';
7
8
  import { Buffer } from 'buffer';
8
- import { homedir, tmpdir } from 'os';
9
+ import { homedir, hostname, tmpdir } from 'os';
9
10
  import { createHash } from 'crypto';
10
11
  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$1(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/");
@@ -997,11 +1047,13 @@ function extractEmbeddedRules(content) {
997
1047
  });
998
1048
  return { rootContent: rootContent.trim(), rules };
999
1049
  }
1000
- var ROOT_CONTRACT_START, ROOT_CONTRACT_END, EMBEDDED_RULES_START, EMBEDDED_RULES_END, EMBEDDED_RULE_END, EMBEDDED_RULE_START_PREFIX, EMBEDDED_RULE_START_SUFFIX;
1050
+ var ROOT_CONTRACT_START, ROOT_CONTRACT_END, LESSONS_CONTRACT_START, LESSONS_CONTRACT_END, EMBEDDED_RULES_START, EMBEDDED_RULES_END, EMBEDDED_RULE_END, EMBEDDED_RULE_START_PREFIX, EMBEDDED_RULE_START_SUFFIX;
1001
1051
  var init_managed_blocks = __esm({
1002
1052
  "src/targets/projection/managed-blocks.ts"() {
1003
1053
  ROOT_CONTRACT_START = "<!-- agentsmesh:root-generation-contract:start -->";
1004
1054
  ROOT_CONTRACT_END = "<!-- agentsmesh:root-generation-contract:end -->";
1055
+ LESSONS_CONTRACT_START = "<!-- agentsmesh:lessons-contract:start -->";
1056
+ LESSONS_CONTRACT_END = "<!-- agentsmesh:lessons-contract:end -->";
1005
1057
  EMBEDDED_RULES_START = "<!-- agentsmesh:embedded-rules:start -->";
1006
1058
  EMBEDDED_RULES_END = "<!-- agentsmesh:embedded-rules:end -->";
1007
1059
  EMBEDDED_RULE_END = "<!-- agentsmesh:embedded-rule:end -->";
@@ -1011,31 +1063,9 @@ var init_managed_blocks = __esm({
1011
1063
  });
1012
1064
 
1013
1065
  // src/targets/projection/root-instruction-paragraph.ts
1014
- function normalizeWhitespace(value) {
1015
- return value.replace(/\s+/g, " ").trim();
1016
- }
1017
1066
  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;
1067
+ const withoutPrior = stripAgentsmeshRootInstructionParagraph(content);
1068
+ return insertAtBodyTop(withoutPrior, AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH);
1039
1069
  }
1040
1070
  function stripAgentsmeshRootInstructionParagraph(content) {
1041
1071
  let result = stripManagedBlock(content, ROOT_CONTRACT_START, ROOT_CONTRACT_END);
@@ -1049,7 +1079,7 @@ ${legacy}`, "");
1049
1079
  }
1050
1080
  return result.trim();
1051
1081
  }
1052
- var ROOT_INSTRUCTION_BODY_V1, ROOT_INSTRUCTION_BODY_V2, ROOT_INSTRUCTION_BODY_V3, ROOT_INSTRUCTION_BODY_V4, ROOT_INSTRUCTION_BODY_V5, ROOT_INSTRUCTION_BODY_V6, ROOT_INSTRUCTION_BODY_V7, ROOT_INSTRUCTION_BODY_V8, ROOT_INSTRUCTION_BODY, LEGACY_AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH, LEGACY_AGENTSMESH_ROOT_INSTRUCTION_SECTION, AGENTSMESH_CONTRACT_WITH_V1_BODY, AGENTSMESH_CONTRACT_WITH_V2_BODY, AGENTSMESH_CONTRACT_WITH_V3_BODY, AGENTSMESH_CONTRACT_WITH_V4_BODY, AGENTSMESH_CONTRACT_WITH_V5_BODY, AGENTSMESH_CONTRACT_WITH_V6_BODY, AGENTSMESH_CONTRACT_WITH_V7_BODY, AGENTSMESH_CONTRACT_WITH_V8_BODY, AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH, LEGACY_FORMS;
1082
+ var ROOT_INSTRUCTION_BODY_V1, ROOT_INSTRUCTION_BODY_V2, ROOT_INSTRUCTION_BODY_V3, ROOT_INSTRUCTION_BODY_V4, ROOT_INSTRUCTION_BODY_V5, ROOT_INSTRUCTION_BODY_V6, ROOT_INSTRUCTION_BODY_V7, ROOT_INSTRUCTION_BODY_V8, ROOT_INSTRUCTION_BODY_V9, ROOT_INSTRUCTION_BODY_V10, ROOT_INSTRUCTION_BODY, LEGACY_AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH, LEGACY_AGENTSMESH_ROOT_INSTRUCTION_SECTION, AGENTSMESH_CONTRACT_WITH_V1_BODY, AGENTSMESH_CONTRACT_WITH_V2_BODY, AGENTSMESH_CONTRACT_WITH_V3_BODY, AGENTSMESH_CONTRACT_WITH_V4_BODY, AGENTSMESH_CONTRACT_WITH_V5_BODY, AGENTSMESH_CONTRACT_WITH_V6_BODY, AGENTSMESH_CONTRACT_WITH_V7_BODY, AGENTSMESH_CONTRACT_WITH_V8_BODY, AGENTSMESH_CONTRACT_WITH_V9_BODY, AGENTSMESH_CONTRACT_WITH_V10_BODY, AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH, LEGACY_FORMS;
1053
1083
  var init_root_instruction_paragraph = __esm({
1054
1084
  "src/targets/projection/root-instruction-paragraph.ts"() {
1055
1085
  init_managed_blocks();
@@ -1061,7 +1091,9 @@ var init_root_instruction_paragraph = __esm({
1061
1091
  ROOT_INSTRUCTION_BODY_V6 = "Create canonical files in `.agentsmesh`: `rules/_root.md` and `rules/*.md` are Markdown rules; `commands/*.md`, `agents/*.md`, and `skills/*/SKILL.md` plus supporting files use Claude-style frontmatter Markdown; `mcp.json` is MCP JSON; `hooks.yaml` and `permissions.yaml` are YAML; `ignore` is gitignore-style text. Then run `agentsmesh generate`.";
1062
1092
  ROOT_INSTRUCTION_BODY_V7 = "`.agentsmesh` is the only folder you edit or add these files in: `rules/_root.md` and `rules/*.md` are Markdown rules; `commands/*.md`, `agents/*.md`, and `skills/*/SKILL.md` plus supporting files use Claude-style frontmatter Markdown; `mcp.json` is MCP JSON; `hooks.yaml` and `permissions.yaml` are YAML; `ignore` is gitignore-style text. Do not edit generated tool files; run `agentsmesh generate`.";
1063
1093
  ROOT_INSTRUCTION_BODY_V8 = "`agentsmesh.yaml` selects targets/features (`agentsmesh.local.yaml` overrides locally), and `.agentsmesh` is the only place to add or edit canonical items: `rules/_root.md`, `rules/*.md`, `commands/*.md`, `agents/*.md`, `skills/*/SKILL.md` plus supporting files, `mcp.json`, `hooks.yaml`, `permissions.yaml`, and `ignore`; if missing run `agentsmesh init`, use `agentsmesh import --from <tool>` for native configs, `agentsmesh install <source>` or `install --sync` for reusable packs, then run `agentsmesh generate`. Use `diff`, `lint`, `check`, `watch`, `matrix`, and `merge` as needed; never edit generated tool files.";
1064
- ROOT_INSTRUCTION_BODY = "`agentsmesh.yaml` selects targets/features (`agentsmesh.local.yaml` overrides locally), and `.agentsmesh` is the only place to add or edit canonical items: `rules/_root.md`, `rules/*.md`, `commands/*.md`, `agents/*.md`, `skills/*/SKILL.md` plus supporting files, `mcp.json`, `hooks.yaml`, `permissions.yaml`, and `ignore`; if missing run `agentsmesh init`, use `agentsmesh import --from <tool>` for native configs, `agentsmesh install <source>` or `install --sync` for reusable packs, then run `agentsmesh generate`. Use `diff`, `lint`, `check`, `watch`, `matrix`, `merge`, and `refresh` as needed; never edit generated tool files.";
1094
+ ROOT_INSTRUCTION_BODY_V9 = "`agentsmesh.yaml` selects targets/features (`agentsmesh.local.yaml` overrides locally), and `.agentsmesh` is the only place to add or edit canonical items: `rules/_root.md`, `rules/*.md`, `commands/*.md`, `agents/*.md`, `skills/*/SKILL.md` plus supporting files, `mcp.json`, `hooks.yaml`, `permissions.yaml`, and `ignore`; if missing run `agentsmesh init`, use `agentsmesh import --from <tool>` for native configs, `agentsmesh install <source>` or `install --sync` for reusable packs, then run `agentsmesh generate`. Use `diff`, `lint`, `check`, `watch`, `matrix`, `merge`, and `refresh` as needed; never edit generated tool files.";
1095
+ ROOT_INSTRUCTION_BODY_V10 = "**MUST follow when changing any rule, agent, command, skill, hook, MCP server, permission, or ignore pattern.** `agentsmesh.yaml` selects targets/features (`agentsmesh.local.yaml` overrides locally), and `.agentsmesh` is the only place to add or edit canonical items: `rules/_root.md`, `rules/*.md`, `commands/*.md`, `agents/*.md`, `skills/*/SKILL.md` plus supporting files, `mcp.json`, `hooks.yaml`, `permissions.yaml`, and `ignore`; if missing run `agentsmesh init`, use `agentsmesh import --from <tool>` for native configs, `agentsmesh install <source>` or `install --sync` for reusable packs, then run `agentsmesh generate`. Use `diff`, `lint`, `check`, `watch`, `matrix`, `merge`, and `refresh` as needed; never edit generated tool files.";
1096
+ ROOT_INSTRUCTION_BODY = "**NEVER edit generated files** (`.claude/`, `.cursor/`, `AGENTS.md`, `.github/copilot-instructions.md`, and similar target outputs) \u2014 `agentsmesh generate` overwrites them. **All changes MUST go through `.agentsmesh` first**: edit `rules/_root.md`, `rules/*.md`, `commands/*.md`, `agents/*.md`, `skills/*/SKILL.md` plus supporting files, `mcp.json`, `hooks.yaml`, `permissions.yaml`, and `ignore`; `agentsmesh.yaml` selects targets/features (`agentsmesh.local.yaml` overrides locally); if missing run `agentsmesh init`, use `agentsmesh import --from <tool>` for native configs, `agentsmesh install <source>` or `install --sync` for reusable packs, then run `agentsmesh generate`. Use `diff`, `lint`, `check`, `watch`, `matrix`, `merge`, and `refresh` as needed.";
1065
1097
  LEGACY_AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH = ROOT_INSTRUCTION_BODY_V1;
1066
1098
  LEGACY_AGENTSMESH_ROOT_INSTRUCTION_SECTION = `## Project-Specific Rules
1067
1099
 
@@ -1090,12 +1122,20 @@ ${ROOT_INSTRUCTION_BODY_V7}`;
1090
1122
  AGENTSMESH_CONTRACT_WITH_V8_BODY = `## AgentsMesh Generation Contract
1091
1123
 
1092
1124
  ${ROOT_INSTRUCTION_BODY_V8}`;
1125
+ AGENTSMESH_CONTRACT_WITH_V9_BODY = `## AgentsMesh Generation Contract
1126
+
1127
+ ${ROOT_INSTRUCTION_BODY_V9}`;
1128
+ AGENTSMESH_CONTRACT_WITH_V10_BODY = `## AgentsMesh Generation Contract
1129
+
1130
+ ${ROOT_INSTRUCTION_BODY_V10}`;
1093
1131
  AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH = `${ROOT_CONTRACT_START}
1094
1132
  ## AgentsMesh Generation Contract
1095
1133
 
1096
1134
  ${ROOT_INSTRUCTION_BODY}
1097
1135
  ${ROOT_CONTRACT_END}`;
1098
1136
  LEGACY_FORMS = [
1137
+ AGENTSMESH_CONTRACT_WITH_V10_BODY,
1138
+ AGENTSMESH_CONTRACT_WITH_V9_BODY,
1099
1139
  AGENTSMESH_CONTRACT_WITH_V8_BODY,
1100
1140
  AGENTSMESH_CONTRACT_WITH_V7_BODY,
1101
1141
  AGENTSMESH_CONTRACT_WITH_V6_BODY,
@@ -1999,9 +2039,8 @@ function shouldRewritePathToken(fullContent, start, end, matchText, rewriteBareP
1999
2039
  const before = fullContent[start - 1];
2000
2040
  const after = fullContent[end];
2001
2041
  if (isMarkdownReferenceDefinitionDestination(fullContent, start, candidateEnd)) return true;
2002
- if (before === "'" && after === "'" || before === '"' && after === '"' || before === "`" && after === "`") {
2003
- return true;
2004
- }
2042
+ if (before === "`" && after === "`") return true;
2043
+ if (before === "'" && after === "'" || before === '"' && after === '"') return false;
2005
2044
  if (before === "<" && after === ">") return true;
2006
2045
  if (before === "[" && after === "]") {
2007
2046
  if (!rewriteBarePathTokens && !isRootRelativePathToken(normalizedCandidate) && markdownBracketLabelDuplicatesDestination(fullContent, start, matchText)) {
@@ -3328,12 +3367,12 @@ var init_augment_code = __esm({
3328
3367
  });
3329
3368
 
3330
3369
  // src/targets/claude-code/constants.ts
3331
- 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;
3370
+ 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;
3332
3371
  var init_constants7 = __esm({
3333
3372
  "src/targets/claude-code/constants.ts"() {
3334
3373
  CLAUDE_CODE_TARGET = "claude-code";
3335
- CLAUDE_ROOT = ".claude/CLAUDE.md";
3336
- CLAUDE_LEGACY_ROOT = "CLAUDE.md";
3374
+ CLAUDE_ROOT = "CLAUDE.md";
3375
+ CLAUDE_NESTED_ROOT = ".claude/CLAUDE.md";
3337
3376
  CLAUDE_RULES_DIR = ".claude/rules";
3338
3377
  CLAUDE_COMMANDS_DIR = ".claude/commands";
3339
3378
  CLAUDE_AGENTS_DIR = ".claude/agents";
@@ -5195,7 +5234,8 @@ var init_amp2 = __esm({
5195
5234
  markAsRoot: true
5196
5235
  }
5197
5236
  },
5198
- emitScopedSettings(canonical, _scope) {
5237
+ emitScopedSettings(canonical, _scope, enabledFeatures) {
5238
+ if (!enabledFeatures.has("mcp")) return [];
5199
5239
  if (!canonical.mcp || Object.keys(canonical.mcp.mcpServers).length === 0) return [];
5200
5240
  return [
5201
5241
  {
@@ -5947,12 +5987,12 @@ function mergeAugmentSettings(existing, newContent) {
5947
5987
  if (overlay.hooks !== void 0) base.hooks = overlay.hooks;
5948
5988
  return JSON.stringify(base, null, 2);
5949
5989
  }
5950
- function buildSettingsContent(canonical) {
5990
+ function buildSettingsContent(canonical, enabledFeatures) {
5951
5991
  const settings = {};
5952
- if (canonical.mcp && Object.keys(canonical.mcp.mcpServers).length > 0) {
5992
+ if (enabledFeatures.has("mcp") && canonical.mcp && Object.keys(canonical.mcp.mcpServers).length > 0) {
5953
5993
  settings.mcpServers = canonical.mcp.mcpServers;
5954
5994
  }
5955
- if (canonical.hooks && Object.keys(canonical.hooks).length > 0) {
5995
+ if (enabledFeatures.has("hooks") && canonical.hooks && Object.keys(canonical.hooks).length > 0) {
5956
5996
  settings.hooks = serializeHooksForSettings(canonical.hooks);
5957
5997
  }
5958
5998
  if (Object.keys(settings).length === 0) return null;
@@ -6081,8 +6121,8 @@ var init_augment_code2 = __esm({
6081
6121
  ],
6082
6122
  layout: globalLayout5
6083
6123
  },
6084
- emitScopedSettings(canonical) {
6085
- const content = buildSettingsContent(canonical);
6124
+ emitScopedSettings(canonical, _scope, enabledFeatures) {
6125
+ const content = buildSettingsContent(canonical, enabledFeatures);
6086
6126
  if (content === null) return [];
6087
6127
  return [{ path: AUGMENT_CODE_SETTINGS_FILE, content }];
6088
6128
  },
@@ -6656,7 +6696,10 @@ var init_claude_code2 = __esm({
6656
6696
  skillDir: ".claude/skills",
6657
6697
  managedOutputs: {
6658
6698
  dirs: [".claude/agents", ".claude/commands", ".claude/rules", ".claude/skills"],
6659
- files: [".claude/CLAUDE.md", ".claude/settings.json", ".claudeignore", ".mcp.json"]
6699
+ // CLAUDE_NESTED_ROOT is the pre-migration project location; listing it here lets
6700
+ // `cleanupStaleGeneratedOutputs` evict a leftover `.claude/CLAUDE.md` once generation
6701
+ // writes the root `CLAUDE.md`, so Claude Code never concatenates both into context.
6702
+ files: [CLAUDE_ROOT, CLAUDE_NESTED_ROOT, ".claude/settings.json", ".claudeignore", ".mcp.json"]
6660
6703
  },
6661
6704
  paths: {
6662
6705
  rulePath(slug, _rule) {
@@ -6671,7 +6714,7 @@ var init_claude_code2 = __esm({
6671
6714
  }
6672
6715
  };
6673
6716
  globalLayout6 = {
6674
- rootInstructionPath: CLAUDE_ROOT,
6717
+ rootInstructionPath: CLAUDE_NESTED_ROOT,
6675
6718
  skillDir: ".claude/skills",
6676
6719
  renderPrimaryRootInstruction: renderClaudeGlobalPrimaryInstructions,
6677
6720
  managedOutputs: {
@@ -6684,7 +6727,7 @@ var init_claude_code2 = __esm({
6684
6727
  ".agents/skills"
6685
6728
  ],
6686
6729
  files: [
6687
- ".claude/CLAUDE.md",
6730
+ CLAUDE_NESTED_ROOT,
6688
6731
  ".claude/settings.json",
6689
6732
  CLAUDE_GLOBAL_MCP_JSON,
6690
6733
  CLAUDE_HOOKS_JSON,
@@ -6692,6 +6735,7 @@ var init_claude_code2 = __esm({
6692
6735
  ]
6693
6736
  },
6694
6737
  rewriteGeneratedPath(path) {
6738
+ if (path === CLAUDE_ROOT) return CLAUDE_NESTED_ROOT;
6695
6739
  if (path === CLAUDE_MCP_JSON) return CLAUDE_GLOBAL_MCP_JSON;
6696
6740
  return path;
6697
6741
  },
@@ -6755,10 +6799,11 @@ var init_claude_code2 = __esm({
6755
6799
  importer: {
6756
6800
  rules: [
6757
6801
  {
6758
- // Root rule: prefer .claude/CLAUDE.md, fall back to legacy CLAUDE.md (project only).
6802
+ // Root rule: project prefers root CLAUDE.md, falls back to nested .claude/CLAUDE.md;
6803
+ // global reads the nested .claude/CLAUDE.md.
6759
6804
  feature: "rules",
6760
6805
  mode: "singleFile",
6761
- source: { project: [CLAUDE_ROOT, CLAUDE_LEGACY_ROOT], global: [CLAUDE_ROOT] },
6806
+ source: { project: [CLAUDE_ROOT, CLAUDE_NESTED_ROOT], global: [CLAUDE_NESTED_ROOT] },
6762
6807
  canonicalDir: CLAUDE_CANONICAL_RULES_DIR,
6763
6808
  canonicalRootFilename: "_root.md",
6764
6809
  markAsRoot: true
@@ -6804,7 +6849,23 @@ var init_claude_code2 = __esm({
6804
6849
  }
6805
6850
  },
6806
6851
  buildImportPaths: buildClaudeCodeImportPaths,
6807
- detectionPaths: ["CLAUDE.md", ".claude/rules", ".claude/commands"]
6852
+ detectionPaths: [CLAUDE_ROOT, CLAUDE_NESTED_ROOT, ".claude/rules", ".claude/commands"],
6853
+ nativeInstall: {
6854
+ pickPaths: [
6855
+ {
6856
+ prefix: ".claude/commands",
6857
+ feature: "commands",
6858
+ strategy: { kind: "basename", suffix: ".md" }
6859
+ },
6860
+ { prefix: ".claude/rules", feature: "rules", strategy: { kind: "basename", suffix: ".md" } },
6861
+ {
6862
+ prefix: ".claude/agents",
6863
+ feature: "agents",
6864
+ strategy: { kind: "basename", suffix: ".md" }
6865
+ },
6866
+ { prefix: ".claude/skills/", feature: "skills", strategy: { kind: "firstSegment" } }
6867
+ ]
6868
+ }
6808
6869
  };
6809
6870
  }
6810
6871
  });
@@ -7683,6 +7744,16 @@ var init_cline2 = __esm({
7683
7744
  },
7684
7745
  buildImportPaths: buildClineImportPaths,
7685
7746
  detectionPaths: [".clinerules", ".cline"],
7747
+ nativeInstall: {
7748
+ pickPaths: [
7749
+ { prefix: CLINE_SKILLS_DIR, feature: "skills", strategy: { kind: "skillDir" } },
7750
+ {
7751
+ prefix: CLINE_WORKFLOWS_DIR,
7752
+ feature: "commands",
7753
+ strategy: { kind: "basename", suffix: ".md" }
7754
+ }
7755
+ ]
7756
+ },
7686
7757
  conversionDefaults: { agentsToSkills: true }
7687
7758
  };
7688
7759
  }
@@ -8601,6 +8672,11 @@ var init_codex_cli2 = __esm({
8601
8672
  ".codex/agents",
8602
8673
  ".codex/rules"
8603
8674
  ],
8675
+ nativeInstall: {
8676
+ pickPaths: [
8677
+ { prefix: ".codex", feature: "rules", strategy: { kind: "basename", suffix: ".md" } }
8678
+ ]
8679
+ },
8604
8680
  excludeFromStarterInit: true,
8605
8681
  conversionDefaults: { commandsToSkills: true, agentsToSkills: false }
8606
8682
  };
@@ -9104,6 +9180,21 @@ var init_continue2 = __esm({
9104
9180
  },
9105
9181
  buildImportPaths: buildContinueImportPaths,
9106
9182
  detectionPaths: [".continue/rules", ".continue/skills", ".continue/mcpServers"],
9183
+ nativeInstall: {
9184
+ pickPaths: [
9185
+ {
9186
+ prefix: ".continue/rules",
9187
+ feature: "rules",
9188
+ strategy: { kind: "basename", suffix: ".md" }
9189
+ },
9190
+ {
9191
+ prefix: ".continue/prompts",
9192
+ feature: "commands",
9193
+ strategy: { kind: "basename", suffix: ".md" }
9194
+ },
9195
+ { prefix: ".continue/skills", feature: "skills", strategy: { kind: "skillDir" } }
9196
+ ]
9197
+ },
9107
9198
  conversionDefaults: { agentsToSkills: true }
9108
9199
  };
9109
9200
  }
@@ -9471,6 +9562,80 @@ var init_importer10 = __esm({
9471
9562
  init_copilot2();
9472
9563
  }
9473
9564
  });
9565
+ async function skillNamesFromNativeSkillDir(scanRoot) {
9566
+ const files = await readDirRecursive(scanRoot);
9567
+ const names = /* @__PURE__ */ new Set();
9568
+ for (const f of files) {
9569
+ if (basename(f) === "SKILL.md") {
9570
+ names.add(basename(dirname(f)));
9571
+ continue;
9572
+ }
9573
+ const rel2 = relative(scanRoot, f).replace(/\\/g, "/");
9574
+ if (!rel2.includes("/") && f.toLowerCase().endsWith(".md")) {
9575
+ names.add(basename(f, ".md"));
9576
+ }
9577
+ }
9578
+ return [...names].filter(Boolean).sort();
9579
+ }
9580
+ var init_native_skill_scan = __esm({
9581
+ "src/install/native/native-skill-scan.ts"() {
9582
+ init_fs();
9583
+ }
9584
+ });
9585
+ async function inferCopilotPickFromPath(repoRoot, posixPath) {
9586
+ const scan = join(repoRoot, ...posixPath.split("/"));
9587
+ if (posixPath.startsWith(COPILOT_PROMPTS_DIR)) {
9588
+ const files = await readDirRecursive(scan);
9589
+ const commands = [
9590
+ ...new Set(
9591
+ files.filter((f) => f.toLowerCase().endsWith(".prompt.md")).map((f) => basename(f, ".prompt.md"))
9592
+ )
9593
+ ].sort();
9594
+ return commands.length ? { commands } : {};
9595
+ }
9596
+ if (posixPath.startsWith(".github/copilot") && !posixPath.includes("copilot-instructions.md")) {
9597
+ const files = await readDirRecursive(scan);
9598
+ const rules = [
9599
+ ...new Set(
9600
+ files.filter((f) => f.includes(".instructions.md")).map((f) => basename(f).replace(/\.instructions\.md$/i, ""))
9601
+ )
9602
+ ].sort();
9603
+ return rules.length ? { rules } : {};
9604
+ }
9605
+ if (posixPath.startsWith(".github/instructions")) {
9606
+ const files = await readDirRecursive(scan);
9607
+ const names = /* @__PURE__ */ new Set();
9608
+ for (const f of files) {
9609
+ const b = basename(f);
9610
+ if (b.toLowerCase().endsWith(".instructions.md"))
9611
+ names.add(b.replace(/\.instructions\.md$/i, ""));
9612
+ else if (b.toLowerCase().endsWith(".md")) names.add(basename(f, ".md"));
9613
+ }
9614
+ const rules = [...names].sort();
9615
+ return rules.length ? { rules } : {};
9616
+ }
9617
+ if (posixPath.startsWith(".github/skills")) {
9618
+ const skills = await skillNamesFromNativeSkillDir(scan);
9619
+ return skills.length ? { skills } : {};
9620
+ }
9621
+ if (posixPath.startsWith(".github/agents")) {
9622
+ const files = await readDirRecursive(scan);
9623
+ const agents = [
9624
+ ...new Set(
9625
+ files.filter((f) => f.toLowerCase().endsWith(".agent.md")).map((f) => basename(f, ".agent.md"))
9626
+ )
9627
+ ].sort();
9628
+ return agents.length ? { agents } : {};
9629
+ }
9630
+ return {};
9631
+ }
9632
+ var init_native_path_pick_infer_copilot = __esm({
9633
+ "src/install/native/native-path-pick-infer-copilot.ts"() {
9634
+ init_fs();
9635
+ init_constants29();
9636
+ init_native_skill_scan();
9637
+ }
9638
+ });
9474
9639
  function pruneUndefined3(record) {
9475
9640
  for (const key of Object.keys(record)) {
9476
9641
  if (record[key] === void 0) delete record[key];
@@ -9760,6 +9925,7 @@ var init_copilot2 = __esm({
9760
9925
  init_generator11();
9761
9926
  init_constants29();
9762
9927
  init_importer10();
9928
+ init_native_path_pick_infer_copilot();
9763
9929
  init_import_mappers4();
9764
9930
  init_linter10();
9765
9931
  init_import_map_builders();
@@ -9974,7 +10140,8 @@ var init_copilot2 = __esm({
9974
10140
  ".github/skills",
9975
10141
  ".github/agents",
9976
10142
  ".github/hooks"
9977
- ]
10143
+ ],
10144
+ nativeInstall: { inferPick: inferCopilotPickFromPath }
9978
10145
  };
9979
10146
  }
9980
10147
  });
@@ -11004,8 +11171,8 @@ async function hasGlobalCursorArtifacts(projectRoot) {
11004
11171
  join(projectRoot, CURSOR_COMMANDS_DIR)
11005
11172
  ];
11006
11173
  for (const p of candidates) {
11007
- const stat8 = await readFileSafe(p);
11008
- if (stat8 !== null && stat8.trim() !== "") return true;
11174
+ const stat9 = await readFileSafe(p);
11175
+ if (stat9 !== null && stat9.trim() !== "") return true;
11009
11176
  }
11010
11177
  const skillFiles = await readDirRecursive(join(projectRoot, CURSOR_SKILLS_DIR));
11011
11178
  if (skillFiles.some((f) => f.endsWith(".md"))) return true;
@@ -11487,6 +11654,23 @@ var init_cursor2 = __esm({
11487
11654
  },
11488
11655
  buildImportPaths: buildCursorImportPaths,
11489
11656
  detectionPaths: [".cursor/rules", ".cursor/mcp.json"],
11657
+ nativeInstall: {
11658
+ pickPaths: [
11659
+ { prefix: ".cursor/rules", feature: "rules", strategy: { kind: "basename", suffix: ".mdc" } },
11660
+ {
11661
+ prefix: ".cursor/commands",
11662
+ feature: "commands",
11663
+ strategy: { kind: "basename", suffix: ".md" }
11664
+ },
11665
+ {
11666
+ prefix: ".cursor/agents",
11667
+ feature: "agents",
11668
+ strategy: { kind: "basename", suffix: ".md" }
11669
+ },
11670
+ { prefix: ".cursor/skills", feature: "skills", strategy: { kind: "skillDir" } }
11671
+ ],
11672
+ dialectHints: [{ frontmatterKey: "alwaysApply" }]
11673
+ },
11490
11674
  preservesManualActivation: true
11491
11675
  };
11492
11676
  }
@@ -12248,18 +12432,18 @@ function mapHookEvent2(event) {
12248
12432
  return null;
12249
12433
  }
12250
12434
  }
12251
- function generateGeminiSettingsFiles(canonical) {
12435
+ function generateGeminiSettingsFiles(canonical, enabledFeatures) {
12252
12436
  const settings = {};
12253
12437
  let hasAnyNativeSettings = false;
12254
- if (canonical.mcp && Object.keys(canonical.mcp.mcpServers).length > 0) {
12438
+ if (enabledFeatures.has("mcp") && canonical.mcp && Object.keys(canonical.mcp.mcpServers).length > 0) {
12255
12439
  settings.mcpServers = canonical.mcp.mcpServers;
12256
12440
  hasAnyNativeSettings = true;
12257
12441
  }
12258
- if (canonical.agents.length > 0) {
12442
+ if (enabledFeatures.has("agents") && canonical.agents.length > 0) {
12259
12443
  settings.experimental = { enableAgents: true };
12260
12444
  hasAnyNativeSettings = true;
12261
12445
  }
12262
- if (canonical.hooks) {
12446
+ if (enabledFeatures.has("hooks") && canonical.hooks) {
12263
12447
  const hookEntries = Object.entries(canonical.hooks).flatMap(([event, entries]) => {
12264
12448
  const mappedEvent = mapHookEvent2(event);
12265
12449
  if (!mappedEvent || !Array.isArray(entries)) return [];
@@ -12875,6 +13059,36 @@ var init_importer15 = __esm({
12875
13059
  init_importer_skills_agents();
12876
13060
  }
12877
13061
  });
13062
+ function isUnderGeminiCommands(pathInRepoPosix) {
13063
+ const p = pathInRepoPosix.replace(/^\/+|\/+$/g, "");
13064
+ return p === ".gemini/commands" || p.startsWith(".gemini/commands/");
13065
+ }
13066
+ async function inferGeminiCommandNamesFromFiles(repoRoot, pathInRepoPosix) {
13067
+ const commandsRoot = join(repoRoot, ...GEMINI_COMMANDS_DIR.split("/"));
13068
+ const scanDir = join(repoRoot, ...pathInRepoPosix.split("/"));
13069
+ const files = await readDirRecursive(scanDir);
13070
+ const names = [];
13071
+ for (const f of files) {
13072
+ if (!/\.(toml|md)$/i.test(f)) continue;
13073
+ const rel2 = relative(commandsRoot, f).replace(/\\/g, "/");
13074
+ if (rel2.startsWith("..") || rel2 === "") continue;
13075
+ const noExt = rel2.replace(/\.(toml|md)$/i, "");
13076
+ const name = noExt.split("/").filter(Boolean).join(":");
13077
+ if (name) names.push(name);
13078
+ }
13079
+ return [...new Set(names)].sort();
13080
+ }
13081
+ async function inferGeminiPick(repoRoot, posixPath) {
13082
+ if (!isUnderGeminiCommands(posixPath)) return {};
13083
+ const commands = await inferGeminiCommandNamesFromFiles(repoRoot, posixPath);
13084
+ return commands.length ? { commands } : {};
13085
+ }
13086
+ var init_gemini_install_commands = __esm({
13087
+ "src/install/native/gemini-install-commands.ts"() {
13088
+ init_fs();
13089
+ init_constants30();
13090
+ }
13091
+ });
12878
13092
  async function mapGeminiRuleFile(relativePath, destDir, normalizeTo) {
12879
13093
  const relativeMdPath = relativePath.replace(/\\/g, "/");
12880
13094
  const destPath = join(destDir, relativeMdPath);
@@ -12986,12 +13200,12 @@ var init_lint12 = __esm({
12986
13200
  });
12987
13201
 
12988
13202
  // src/targets/gemini-cli/scoped-settings-emit.ts
12989
- function emitScopedGeminiSettings(canonical, scope) {
13203
+ function emitScopedGeminiSettings(canonical, scope, enabledFeatures) {
12990
13204
  if (scope === "project") {
12991
13205
  const caps = getTargetCapabilities("gemini-cli", scope);
12992
13206
  if (caps?.ignore.flavor !== "settings-embedded") return [];
12993
13207
  }
12994
- return generateGeminiSettingsFiles(canonical);
13208
+ return generateGeminiSettingsFiles(canonical, enabledFeatures);
12995
13209
  }
12996
13210
  var init_scoped_settings_emit = __esm({
12997
13211
  "src/targets/gemini-cli/scoped-settings-emit.ts"() {
@@ -13009,6 +13223,7 @@ var init_gemini_cli2 = __esm({
13009
13223
  init_policies_generator();
13010
13224
  init_constants30();
13011
13225
  init_importer15();
13226
+ init_gemini_install_commands();
13012
13227
  init_import_mappers6();
13013
13228
  init_linter15();
13014
13229
  init_import_map_builders();
@@ -13204,6 +13419,7 @@ var init_gemini_cli2 = __esm({
13204
13419
  },
13205
13420
  buildImportPaths: buildGeminiCliImportPaths,
13206
13421
  detectionPaths: ["GEMINI.md", ".gemini"],
13422
+ nativeInstall: { inferPick: inferGeminiPick },
13207
13423
  conversionDefaults: { agentsToSkills: false }
13208
13424
  };
13209
13425
  }
@@ -14045,7 +14261,23 @@ var init_junie2 = __esm({
14045
14261
  ".junie/skills",
14046
14262
  ".junie/mcp/mcp.json",
14047
14263
  ".aiignore"
14048
- ]
14264
+ ],
14265
+ nativeInstall: {
14266
+ pickPaths: [
14267
+ {
14268
+ prefix: ".junie/commands",
14269
+ feature: "commands",
14270
+ strategy: { kind: "basename", suffix: ".md" }
14271
+ },
14272
+ { prefix: ".junie/rules", feature: "rules", strategy: { kind: "basename", suffix: ".md" } },
14273
+ {
14274
+ prefix: ".junie/agents",
14275
+ feature: "agents",
14276
+ strategy: { kind: "basename", suffix: ".md" }
14277
+ },
14278
+ { prefix: ".junie/skills", feature: "skills", strategy: { kind: "skillDir" } }
14279
+ ]
14280
+ }
14049
14281
  };
14050
14282
  }
14051
14283
  });
@@ -15900,9 +16132,18 @@ function generateIgnore12(canonical) {
15900
16132
  if (!canonical.ignore || canonical.ignore.length === 0) return [];
15901
16133
  return [{ path: QWEN_IGNORE, content: canonical.ignore.join("\n") }];
15902
16134
  }
16135
+ function renderQwenGlobalInstructions(canonical) {
16136
+ const root = canonical.rules.find((rule) => rule.root);
16137
+ const nonRootRules = canonical.rules.filter((rule) => {
16138
+ if (rule.root) return false;
16139
+ return rule.targets.length === 0 || rule.targets.includes(QWEN_CODE_TARGET);
16140
+ });
16141
+ return appendEmbeddedRulesBlock(root?.body.trim() ?? "", nonRootRules);
16142
+ }
15903
16143
  var init_generator26 = __esm({
15904
16144
  "src/targets/qwen-code/generator.ts"() {
15905
16145
  init_markdown();
16146
+ init_managed_blocks();
15906
16147
  init_constants21();
15907
16148
  }
15908
16149
  });
@@ -15985,6 +16226,7 @@ var init_qwen_code2 = __esm({
15985
16226
  };
15986
16227
  globalLayout22 = {
15987
16228
  rootInstructionPath: QWEN_GLOBAL_ROOT,
16229
+ renderPrimaryRootInstruction: renderQwenGlobalInstructions,
15988
16230
  skillDir: QWEN_GLOBAL_SKILLS_DIR,
15989
16231
  managedOutputs: {
15990
16232
  dirs: [QWEN_GLOBAL_COMMANDS_DIR, QWEN_GLOBAL_AGENTS_DIR, QWEN_GLOBAL_SKILLS_DIR],
@@ -18385,6 +18627,16 @@ var init_windsurf2 = __esm({
18385
18627
  },
18386
18628
  buildImportPaths: buildWindsurfImportPaths,
18387
18629
  detectionPaths: [".windsurfrules", ".windsurf"],
18630
+ nativeInstall: {
18631
+ pickPaths: [
18632
+ {
18633
+ prefix: ".windsurf/rules",
18634
+ feature: "rules",
18635
+ strategy: { kind: "basename", suffix: ".md" }
18636
+ }
18637
+ ],
18638
+ dialectHints: [{ frontmatterKey: "trigger" }]
18639
+ },
18388
18640
  conversionDefaults: { agentsToSkills: true }
18389
18641
  };
18390
18642
  }
@@ -18659,7 +18911,8 @@ var init_zed2 = __esm({
18659
18911
  markAsRoot: true
18660
18912
  }
18661
18913
  },
18662
- emitScopedSettings(canonical, _scope) {
18914
+ emitScopedSettings(canonical, _scope, enabledFeatures) {
18915
+ if (!enabledFeatures.has("mcp")) return [];
18663
18916
  if (!canonical.mcp || Object.keys(canonical.mcp.mcpServers).length === 0) return [];
18664
18917
  const contextServers = {};
18665
18918
  for (const [name, server] of Object.entries(canonical.mcp.mcpServers)) {
@@ -18714,7 +18967,10 @@ function getBuiltinTargetDefinition(target31) {
18714
18967
  function getTargetCapabilities(target31, scope = "project") {
18715
18968
  const descriptor31 = getBuiltinTargetDefinition(target31) ?? getDescriptor(target31);
18716
18969
  if (!descriptor31) return void 0;
18717
- const raw = scope === "global" ? descriptor31.globalSupport?.capabilities ?? descriptor31.capabilities : descriptor31.capabilities;
18970
+ if (scope === "global" && !descriptor31.globalSupport) {
18971
+ return normalizeTargetCapabilities(ALL_NONE_CAPABILITIES);
18972
+ }
18973
+ const raw = scope === "global" ? descriptor31.globalSupport.capabilities : descriptor31.capabilities;
18718
18974
  return normalizeTargetCapabilities(raw);
18719
18975
  }
18720
18976
  function getTargetDetectionPaths(target31, scope = "project") {
@@ -18770,6 +19026,7 @@ function isConversionUpgrading(descriptor31, feature, config, scope) {
18770
19026
  function getEffectiveTargetSupportLevel(target31, feature, config, scope = "project") {
18771
19027
  const baseLevel = getTargetCapabilities(target31, scope)?.[feature]?.level ?? "none";
18772
19028
  const descriptor31 = getBuiltinTargetDefinition(target31) ?? getDescriptor(target31);
19029
+ if (scope === "global" && descriptor31 && !descriptor31.globalSupport) return "none";
18773
19030
  if (baseLevel === "none" && isConversionUpgrading(descriptor31, feature, config, scope)) {
18774
19031
  return "embedded";
18775
19032
  }
@@ -18783,7 +19040,7 @@ function resolveTargetFeatureGenerator(target31, feature, config, scope = "proje
18783
19040
  const pick = PICK_FEATURE_GENERATOR[feature];
18784
19041
  return pick === null ? void 0 : pick(descriptor31.generators);
18785
19042
  }
18786
- var BUILTIN_TARGETS, _builtinTargetsMap, PICK_FEATURE_GENERATOR;
19043
+ var ALL_NONE_CAPABILITIES, BUILTIN_TARGETS, _builtinTargetsMap, PICK_FEATURE_GENERATOR;
18787
19044
  var init_builtin_targets = __esm({
18788
19045
  "src/targets/catalog/builtin-targets.ts"() {
18789
19046
  init_conversions();
@@ -18821,6 +19078,17 @@ var init_builtin_targets = __esm({
18821
19078
  init_warp2();
18822
19079
  init_windsurf2();
18823
19080
  init_zed2();
19081
+ ALL_NONE_CAPABILITIES = {
19082
+ rules: "none",
19083
+ additionalRules: "none",
19084
+ commands: "none",
19085
+ agents: "none",
19086
+ skills: "none",
19087
+ mcp: "none",
19088
+ hooks: "none",
19089
+ ignore: "none",
19090
+ permissions: "none"
19091
+ };
18824
19092
  BUILTIN_TARGETS = [
18825
19093
  descriptor,
18826
19094
  descriptor2,
@@ -19251,6 +19519,23 @@ function rewriteGeneratedReferences(results, canonical, config, projectRoot, sco
19251
19519
  init_path_helpers();
19252
19520
  init_link_rebaser_helpers();
19253
19521
 
19522
+ // src/utils/output/color.ts
19523
+ function noColorRequested() {
19524
+ const value = process.env.NO_COLOR;
19525
+ return value !== void 0 && value !== "";
19526
+ }
19527
+ function forceColorRequested() {
19528
+ const value = process.env.FORCE_COLOR;
19529
+ if (value === void 0) return void 0;
19530
+ return value !== "0" && value !== "false";
19531
+ }
19532
+ function colorEnabled(stream = process.stdout) {
19533
+ const forced = forceColorRequested();
19534
+ if (forced !== void 0) return forced;
19535
+ if (noColorRequested()) return false;
19536
+ return stream.isTTY === true;
19537
+ }
19538
+
19254
19539
  // src/utils/output/logger.ts
19255
19540
  var C = {
19256
19541
  green: "\x1B[32m",
@@ -19259,16 +19544,14 @@ var C = {
19259
19544
  cyan: "\x1B[36m",
19260
19545
  reset: "\x1B[0m"
19261
19546
  };
19262
- function out(text) {
19263
- {
19264
- process.stdout.write(text);
19265
- }
19547
+ function outStream() {
19548
+ return process.stdout;
19266
19549
  }
19267
- function noColor() {
19268
- return process.env.NO_COLOR !== void 0 && process.env.NO_COLOR !== "";
19550
+ function out(text) {
19551
+ outStream().write(text);
19269
19552
  }
19270
- function c(code, text) {
19271
- return noColor() ? text : `${code}${text}${C.reset}`;
19553
+ function c(code, text, stream) {
19554
+ return colorEnabled(stream) ? `${code}${text}${C.reset}` : text;
19272
19555
  }
19273
19556
  function pad(str, width) {
19274
19557
  const len = [...str].length;
@@ -19276,20 +19559,20 @@ function pad(str, width) {
19276
19559
  }
19277
19560
  var logger = {
19278
19561
  info(msg) {
19279
- out(c(C.cyan, msg) + "\n");
19562
+ out(c(C.cyan, msg, outStream()) + "\n");
19280
19563
  },
19281
19564
  warn(msg) {
19282
- process.stderr.write(c(C.yellow, "\u26A0 ") + msg + "\n");
19565
+ process.stderr.write(c(C.yellow, "\u26A0 ", process.stderr) + msg + "\n");
19283
19566
  },
19284
19567
  error(msg) {
19285
- process.stderr.write(c(C.red, "\u2717 ") + msg + "\n");
19568
+ process.stderr.write(c(C.red, "\u2717 ", process.stderr) + msg + "\n");
19286
19569
  },
19287
19570
  success(msg) {
19288
- out(c(C.green, "\u2713 ") + msg + "\n");
19571
+ out(c(C.green, "\u2713 ", outStream()) + msg + "\n");
19289
19572
  },
19290
19573
  debug(msg) {
19291
19574
  if (process.env.AGENTSMESH_DEBUG === "1") {
19292
- out(c(C.cyan, "[debug] ") + msg + "\n");
19575
+ out(c(C.cyan, "[debug] ", outStream()) + msg + "\n");
19293
19576
  }
19294
19577
  },
19295
19578
  table(rows) {
@@ -19784,12 +20067,12 @@ async function generateHooksFeature(results, targets, canonical, projectRoot, sc
19784
20067
  }
19785
20068
  }
19786
20069
  }
19787
- async function generateScopedSettingsFeature(results, targets, canonical, projectRoot, scope) {
20070
+ async function generateScopedSettingsFeature(results, targets, canonical, projectRoot, scope, enabledFeatures) {
19788
20071
  for (const target31 of targets) {
19789
20072
  const descriptor31 = getBuiltinTargetDefinition(target31) ?? getDescriptor(target31);
19790
20073
  const emit = descriptor31?.emitScopedSettings;
19791
20074
  if (!emit) continue;
19792
- const outputs = emit(canonical, scope);
20075
+ const outputs = emit(canonical, scope, enabledFeatures);
19793
20076
  if (outputs.length === 0) continue;
19794
20077
  for (const out2 of outputs) {
19795
20078
  await emitGeneratedOutput(results, target31, out2, projectRoot, scope, {
@@ -19890,7 +20173,14 @@ async function generate(ctx) {
19890
20173
  }
19891
20174
  }
19892
20175
  if (hasMcp || hasIgnore || hasHooks || hasAgents || hasPermissions) {
19893
- await generateScopedSettingsFeature(results, targets, canonical, projectRoot, scope);
20176
+ await generateScopedSettingsFeature(
20177
+ results,
20178
+ targets,
20179
+ canonical,
20180
+ projectRoot,
20181
+ scope,
20182
+ enabledFeatures
20183
+ );
19894
20184
  }
19895
20185
  const decoratedResults = decoratePrimaryRootInstructions(results, canonical, scope);
19896
20186
  const sharedPaths = computeSharedRootInstructionPaths(decoratedResults, scope);
@@ -20214,7 +20504,7 @@ async function fetchGitRemoteExtend(parsed, extendName, options, cacheDir, build
20214
20504
  await cloneRepo(resolveCloneUrl(parsed), stagedRepoDir);
20215
20505
  if (parsed.ref) await checkoutRef(stagedRepoDir, parsed.ref);
20216
20506
  await rm(cacheRoot, { recursive: true, force: true });
20217
- await rename(stagedRoot, cacheRoot);
20507
+ await renameWithRetry(stagedRoot, cacheRoot);
20218
20508
  return readCachedRepo(cacheRepoDir);
20219
20509
  } catch (err) {
20220
20510
  await rm(stagedRoot, { recursive: true, force: true });
@@ -22565,430 +22855,2694 @@ function lintRuleScopeInversion(input) {
22565
22855
  }
22566
22856
  return out2;
22567
22857
  }
22858
+ var CURRENT_GRAPH_VERSION = 1;
22859
+ var MAX_RULE_LENGTH = 2e3;
22860
+ var IdSchema = z.string().regex(/^[a-z0-9-]+$/, "id must be kebab-case");
22861
+ var DateSchema = z.string().regex(
22862
+ /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z?)?$/,
22863
+ "createdAt must be ISO-8601 date or datetime"
22864
+ );
22865
+ var TopicSchema = z.object({
22866
+ summary: z.string().min(1, "topic summary must not be empty")
22867
+ }).strict();
22868
+ var TriggerKindSchema = z.enum(["file_glob", "command_pattern", "keyword"]);
22869
+ var TriggerSchema = z.object({
22870
+ kind: TriggerKindSchema,
22871
+ pattern: z.string().min(1, "trigger pattern must not be empty")
22872
+ }).strict();
22873
+ var LessonStatusSchema = z.enum(["active", "deprecated", "superseded"]);
22874
+ var LessonSchema = z.object({
22875
+ rule: z.string().min(1, "lesson rule must not be empty"),
22876
+ rationale: z.string().min(1).optional(),
22877
+ topics: z.array(IdSchema).min(1, "lesson must reference at least one topic"),
22878
+ triggers: z.array(IdSchema),
22879
+ evidence: z.array(z.string().min(1)),
22880
+ status: LessonStatusSchema,
22881
+ supersededBy: IdSchema.optional(),
22882
+ createdAt: DateSchema
22883
+ }).strict();
22884
+ var LessonsGraphSchema = z.object({
22885
+ version: z.literal(CURRENT_GRAPH_VERSION),
22886
+ lessons: z.record(IdSchema, LessonSchema),
22887
+ topics: z.record(IdSchema, TopicSchema),
22888
+ triggers: z.record(IdSchema, TriggerSchema)
22889
+ }).strict();
22890
+ function parseGraph(raw) {
22891
+ return LessonsGraphSchema.parse(raw);
22892
+ }
22568
22893
 
22569
- // src/core/lint/linter.ts
22570
- var EXCLUDE_DIRS = ["node_modules", ".git", "dist", "coverage", ".agentsmesh"];
22571
- async function getProjectFiles(projectRoot) {
22572
- const all = await readDirRecursive(projectRoot);
22573
- const filtered = all.filter((p) => {
22574
- const rel2 = relative(projectRoot, p);
22575
- return !EXCLUDE_DIRS.some((d) => rel2.includes(`/${d}/`) || rel2.startsWith(`${d}/`));
22576
- });
22577
- return filtered.map((p) => relative(projectRoot, p));
22894
+ // src/lessons/graph-store.ts
22895
+ var GRAPH_REL_PATH = ".agentsmesh/lessons/lessons.json";
22896
+ function graphFilePath(projectRoot) {
22897
+ return resolve(projectRoot, GRAPH_REL_PATH);
22578
22898
  }
22579
- async function runLint(config, canonical, projectRoot, targetFilter, options = {}) {
22580
- const scope = options.scope ?? "project";
22581
- const allTargets = [...config.targets, ...config.pluginTargets ?? []];
22582
- const targets = targetFilter ? allTargets.filter((t) => targetFilter.includes(t)) : allTargets;
22583
- const hasRules = config.features.includes("rules");
22584
- const hasCommands = config.features.includes("commands");
22585
- const hasMcp = config.features.includes("mcp");
22586
- const hasPermissions = config.features.includes("permissions");
22587
- const hasHooks = config.features.includes("hooks");
22588
- const diagnostics = [];
22589
- const projectFiles = scope === "global" ? [] : await getProjectFiles(projectRoot);
22590
- for (const target31 of targets) {
22591
- const fullDesc = getDescriptor(target31);
22592
- const descriptor31 = isBuiltinTargetId(target31) ? getTargetCatalogEntry(target31) : fullDesc;
22593
- if (descriptor31?.capabilities) {
22594
- diagnostics.push(
22595
- ...lintSilentFeatureDrops({
22596
- target: target31,
22597
- capabilities: descriptor31.capabilities,
22598
- canonical,
22599
- enabledFeatures: config.features
22600
- })
22601
- );
22602
- }
22603
- if (hasHooks) {
22604
- diagnostics.push(
22605
- ...lintHookScriptReferences({
22606
- target: target31,
22607
- canonical,
22608
- hasScriptProjection: fullDesc?.postProcessHookOutputs !== void 0
22609
- })
22610
- );
22899
+ function loadLessonsGraph(projectRoot) {
22900
+ const raw = readFileSync(graphFilePath(projectRoot), "utf8");
22901
+ return parseGraph(JSON.parse(raw));
22902
+ }
22903
+ function tryLoadLessonsGraph(projectRoot) {
22904
+ if (!existsSync(graphFilePath(projectRoot))) return null;
22905
+ return loadLessonsGraph(projectRoot);
22906
+ }
22907
+ function saveLessonsGraph(projectRoot, graph) {
22908
+ const path = graphFilePath(projectRoot);
22909
+ mkdirSync(dirname(path), { recursive: true });
22910
+ const tmp = `${path}.${process.pid}.tmp`;
22911
+ writeFileSync(tmp, serializeGraph(graph), "utf8");
22912
+ renameSync(tmp, path);
22913
+ }
22914
+ function serializeGraph(graph) {
22915
+ return `${JSON.stringify(canonicalize2(graph), null, 2)}
22916
+ `;
22917
+ }
22918
+ function canonicalize2(value) {
22919
+ if (value === null) return null;
22920
+ if (Array.isArray(value)) return value.map(canonicalize2);
22921
+ if (typeof value === "object") {
22922
+ const entries = Object.entries(value).sort(
22923
+ ([a], [b]) => a < b ? -1 : 1
22924
+ );
22925
+ const out2 = {};
22926
+ for (const [k, v] of entries) out2[k] = canonicalize2(v);
22927
+ return out2;
22928
+ }
22929
+ return value;
22930
+ }
22931
+ var BASE_REL = ".agentsmesh/lessons";
22932
+ function lessonsPaths(projectRoot) {
22933
+ const base = join(projectRoot, BASE_REL);
22934
+ return {
22935
+ base,
22936
+ graph: join(base, "lessons.json"),
22937
+ config: join(base, "config.json"),
22938
+ journal: join(base, "journal.md"),
22939
+ index: join(base, "index.yaml"),
22940
+ topicsDir: join(base, "topics")
22941
+ };
22942
+ }
22943
+ function toRelPath(projectRoot, absolute) {
22944
+ return relative(projectRoot, absolute).split(sep).join("/");
22945
+ }
22946
+ var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING REQUIREMENT \u2014 MUST run both, no exceptions; the user will check)
22947
+
22948
+ Graph \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit. Manual: the \`lessons\` skill.
22949
+
22950
+ **Recall \u2014 MUST run before every file edit and every state-changing command** (build/test/install/migrate/git-write): \`agentsmesh lessons query --file <path> --cmd <command>\`, then apply every rule. Pure-read commands (cat/ls/grep/git-log) and the recall query itself are exempt.
22951
+
22952
+ **Capture \u2014 MUST run immediately after any failure** (a failing test/CI/lint/typecheck, a code review, a user correction, a regression, or a wrong assumption \u2014 yours or anyone's): \`agentsmesh lessons add "<rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`.
22953
+
22954
+ No shell? Use the \`lessons_query\` / \`lessons_add\` MCP tools. Skip either and the system does not exist.`;
22955
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
22956
+ var MAX_FILES = 2e5;
22957
+ function listProjectFiles(projectRoot) {
22958
+ const out2 = /* @__PURE__ */ new Set();
22959
+ try {
22960
+ const stack = [projectRoot];
22961
+ while (stack.length > 0) {
22962
+ const dir = stack.pop();
22963
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
22964
+ if (entry.isDirectory()) {
22965
+ if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
22966
+ } else if (entry.isFile()) {
22967
+ out2.add(toRelPath(projectRoot, join(dir, entry.name)));
22968
+ if (out2.size > MAX_FILES) return out2;
22969
+ }
22970
+ }
22611
22971
  }
22612
- if (hasRules) {
22613
- diagnostics.push(
22614
- ...lintRuleScopeInversion({
22615
- target: target31,
22616
- canonical,
22617
- preservesManualActivation: fullDesc?.preservesManualActivation === true
22618
- })
22619
- );
22972
+ } catch {
22973
+ return null;
22974
+ }
22975
+ return out2;
22976
+ }
22977
+
22978
+ // src/lessons/validate-checks.ts
22979
+ function collectDanglingRefs(graph, findings) {
22980
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
22981
+ for (const topicId of lesson.topics) {
22982
+ if (graph.topics[topicId] === void 0) {
22983
+ findings.push({
22984
+ level: "error",
22985
+ code: "DANGLING_TOPIC",
22986
+ message: `Lesson "${lessonId}" references unknown topic "${topicId}".`,
22987
+ lessonId,
22988
+ topicId
22989
+ });
22990
+ }
22620
22991
  }
22621
- if (hasRules && descriptor31?.lintRules) {
22622
- diagnostics.push(...descriptor31.lintRules(canonical, projectRoot, projectFiles, { scope }));
22992
+ for (const triggerId of lesson.triggers) {
22993
+ if (graph.triggers[triggerId] === void 0) {
22994
+ findings.push({
22995
+ level: "error",
22996
+ code: "DANGLING_TRIGGER",
22997
+ message: `Lesson "${lessonId}" references unknown trigger "${triggerId}".`,
22998
+ lessonId,
22999
+ triggerId
23000
+ });
23001
+ }
22623
23002
  }
22624
- if (fullDesc?.generators.lint) {
22625
- diagnostics.push(...fullDesc.generators.lint(canonical));
23003
+ if (lesson.supersededBy !== void 0 && graph.lessons[lesson.supersededBy] === void 0) {
23004
+ findings.push({
23005
+ level: "error",
23006
+ code: "DANGLING_SUPERSEDER",
23007
+ message: `Lesson "${lessonId}" supersededBy unknown lesson "${lesson.supersededBy}".`,
23008
+ lessonId
23009
+ });
22626
23010
  }
22627
- const lintOpts = { scope };
22628
- if (hasCommands && descriptor31?.lint?.commands) {
22629
- diagnostics.push(...descriptor31.lint.commands(canonical, lintOpts));
23011
+ }
23012
+ }
23013
+ function collectDuplicateRefs(graph, findings) {
23014
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
23015
+ for (const topicId of firstDuplicates(lesson.topics)) {
23016
+ findings.push({
23017
+ level: "error",
23018
+ code: "DUPLICATE_TOPIC_REF",
23019
+ message: `Lesson "${lessonId}" references topic "${topicId}" more than once.`,
23020
+ lessonId,
23021
+ topicId
23022
+ });
22630
23023
  }
22631
- if (hasMcp && descriptor31?.lint?.mcp) {
22632
- diagnostics.push(...descriptor31.lint.mcp(canonical, lintOpts));
23024
+ for (const triggerId of firstDuplicates(lesson.triggers)) {
23025
+ findings.push({
23026
+ level: "error",
23027
+ code: "DUPLICATE_TRIGGER_REF",
23028
+ message: `Lesson "${lessonId}" references trigger "${triggerId}" more than once.`,
23029
+ lessonId,
23030
+ triggerId
23031
+ });
22633
23032
  }
22634
- if (hasPermissions && descriptor31?.lint?.permissions) {
22635
- diagnostics.push(...descriptor31.lint.permissions(canonical, lintOpts));
23033
+ }
23034
+ }
23035
+ function firstDuplicates(ids) {
23036
+ const seen = /* @__PURE__ */ new Set();
23037
+ const dup = /* @__PURE__ */ new Set();
23038
+ for (const id of ids) {
23039
+ if (seen.has(id)) dup.add(id);
23040
+ else seen.add(id);
23041
+ }
23042
+ return [...dup];
23043
+ }
23044
+ function collectStatusInvariants(graph, findings) {
23045
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
23046
+ if (lesson.status === "superseded" && lesson.supersededBy === void 0) {
23047
+ findings.push({
23048
+ level: "error",
23049
+ code: "SUPERSEDED_WITHOUT_TARGET",
23050
+ message: `Lesson "${lessonId}" has status "superseded" but no supersededBy target.`,
23051
+ lessonId
23052
+ });
22636
23053
  }
22637
- if (hasHooks && descriptor31?.lint?.hooks) {
22638
- diagnostics.push(...descriptor31.lint.hooks(canonical, lintOpts));
23054
+ if (lesson.status === "active" && lesson.supersededBy !== void 0) {
23055
+ findings.push({
23056
+ level: "error",
23057
+ code: "ACTIVE_WITH_SUPERSEDER",
23058
+ message: `Lesson "${lessonId}" has status "active" but declares supersededBy.`,
23059
+ lessonId
23060
+ });
22639
23061
  }
22640
23062
  }
22641
- const hasErrors = diagnostics.some((d) => d.level === "error");
22642
- return { diagnostics, hasErrors };
22643
23063
  }
22644
- function computeDiff(results) {
22645
- const diffs = [];
22646
- const summary = { new: 0, updated: 0, unchanged: 0, deleted: 0 };
22647
- for (const r of results) {
22648
- if (r.status === "unchanged") {
22649
- summary.unchanged++;
23064
+ function collectLifecycleInvariants(graph, findings) {
23065
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
23066
+ if (lesson.supersededBy === void 0) continue;
23067
+ if (lesson.supersededBy === lessonId) {
23068
+ findings.push({
23069
+ level: "error",
23070
+ code: "SELF_SUPERSEDED",
23071
+ message: `Lesson "${lessonId}" is superseded by itself.`,
23072
+ lessonId
23073
+ });
22650
23074
  continue;
22651
23075
  }
22652
- if (r.status === "created") {
22653
- summary.new++;
22654
- const patch = createTwoFilesPatch(
22655
- `${r.path} (current)`,
22656
- `${r.path} (generated)`,
22657
- "",
22658
- r.content,
22659
- void 0,
22660
- void 0,
22661
- { context: 3 }
23076
+ const target31 = graph.lessons[lesson.supersededBy];
23077
+ if (target31 !== void 0 && target31.status !== "active") {
23078
+ findings.push({
23079
+ level: "error",
23080
+ code: "INACTIVE_SUPERSEDER",
23081
+ message: `Lesson "${lessonId}" is superseded by "${lesson.supersededBy}", which is itself ${target31.status} \u2014 the chain dead-ends with no live replacement.`,
23082
+ lessonId
23083
+ });
23084
+ }
23085
+ }
23086
+ collectSupersedeCycles(graph, findings);
23087
+ }
23088
+ function collectSupersedeCycles(graph, findings) {
23089
+ const reported = /* @__PURE__ */ new Set();
23090
+ for (const startId of Object.keys(graph.lessons)) {
23091
+ const seen = /* @__PURE__ */ new Set();
23092
+ let cur = startId;
23093
+ while (cur !== void 0) {
23094
+ if (seen.has(cur)) {
23095
+ if (cur !== startId || reported.has(cur)) break;
23096
+ reported.add(cur);
23097
+ findings.push({
23098
+ level: "error",
23099
+ code: "SUPERSEDE_CYCLE",
23100
+ message: `Lesson "${startId}" is part of a supersededBy cycle.`,
23101
+ lessonId: startId
23102
+ });
23103
+ break;
23104
+ }
23105
+ seen.add(cur);
23106
+ const next = graph.lessons[cur]?.supersededBy;
23107
+ if (next === cur) break;
23108
+ cur = next;
23109
+ }
23110
+ }
23111
+ }
23112
+ function collectReachability(graph, findings) {
23113
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
23114
+ if (lesson.status === "active" && lesson.triggers.length === 0) {
23115
+ findings.push({
23116
+ level: "warning",
23117
+ code: "UNREACHABLE_LESSON",
23118
+ message: `Active lesson "${lessonId}" has no triggers and can never be recalled.`,
23119
+ lessonId
23120
+ });
23121
+ }
23122
+ }
23123
+ }
23124
+ function collectOrphans(graph, findings) {
23125
+ const referencedTopics = /* @__PURE__ */ new Set();
23126
+ const referencedTriggers = /* @__PURE__ */ new Set();
23127
+ for (const lesson of Object.values(graph.lessons)) {
23128
+ for (const t of lesson.topics) referencedTopics.add(t);
23129
+ for (const t of lesson.triggers) referencedTriggers.add(t);
23130
+ }
23131
+ for (const topicId of Object.keys(graph.topics)) {
23132
+ if (!referencedTopics.has(topicId)) {
23133
+ findings.push({
23134
+ level: "warning",
23135
+ code: "ORPHAN_TOPIC",
23136
+ message: `Topic "${topicId}" is not referenced by any lesson.`,
23137
+ topicId
23138
+ });
23139
+ }
23140
+ }
23141
+ for (const triggerId of Object.keys(graph.triggers)) {
23142
+ if (!referencedTriggers.has(triggerId)) {
23143
+ findings.push({
23144
+ level: "warning",
23145
+ code: "ORPHAN_TRIGGER",
23146
+ message: `Trigger "${triggerId}" is not referenced by any lesson.`,
23147
+ triggerId
23148
+ });
23149
+ }
23150
+ }
23151
+ }
23152
+
23153
+ // src/lessons/ranking-text.ts
23154
+ var K1 = 1.5;
23155
+ var B = 0.75;
23156
+ var STOP = /* @__PURE__ */ new Set([
23157
+ "the",
23158
+ "a",
23159
+ "an",
23160
+ "to",
23161
+ "of",
23162
+ "in",
23163
+ "and",
23164
+ "or",
23165
+ "for",
23166
+ "is",
23167
+ "on",
23168
+ "at",
23169
+ "with",
23170
+ "be",
23171
+ "as",
23172
+ "it",
23173
+ "that",
23174
+ "this",
23175
+ "its",
23176
+ "must"
23177
+ ]);
23178
+ function tokenize(text) {
23179
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
23180
+ }
23181
+ function queryTerms(query) {
23182
+ const parts = [];
23183
+ if (query.keyword !== void 0) parts.push(query.keyword);
23184
+ if (query.file !== void 0) parts.push(query.file);
23185
+ if (query.command !== void 0) parts.push(query.command);
23186
+ return tokenize(parts.join(" "));
23187
+ }
23188
+ function buildCorpus(graph) {
23189
+ const docs = [];
23190
+ const df = /* @__PURE__ */ new Map();
23191
+ let total = 0;
23192
+ let n = 0;
23193
+ for (const lesson of Object.values(graph.lessons)) {
23194
+ if (lesson.status !== "active") continue;
23195
+ const toks = tokenize(lesson.rule);
23196
+ n += 1;
23197
+ total += toks.length;
23198
+ docs.push(toks.length);
23199
+ for (const t of new Set(toks)) df.set(t, (df.get(t) ?? 0) + 1);
23200
+ }
23201
+ const N = Math.max(n, 1);
23202
+ const idf = /* @__PURE__ */ new Map();
23203
+ for (const [t, f] of df) idf.set(t, Math.log(1 + (N - f + 0.5) / (f + 0.5)));
23204
+ return { idf, avgdl: total / N || 1 };
23205
+ }
23206
+ function bm25(terms, ruleText, corpus) {
23207
+ const toks = tokenize(ruleText);
23208
+ const dl = toks.length || 1;
23209
+ const tf = /* @__PURE__ */ new Map();
23210
+ for (const t of toks) tf.set(t, (tf.get(t) ?? 0) + 1);
23211
+ let score = 0;
23212
+ for (const t of new Set(terms)) {
23213
+ const f = tf.get(t) ?? 0;
23214
+ if (f === 0) continue;
23215
+ const idf = corpus.idf.get(t);
23216
+ score += idf * (f * (K1 + 1)) / (f + K1 * (1 - B + B * dl / corpus.avgdl));
23217
+ }
23218
+ return score;
23219
+ }
23220
+
23221
+ // src/lessons/keyword-signal.ts
23222
+ var MAX_RECOMMENDED_KEYWORD_TOKENS = 5;
23223
+ function isLowSignalKeyword(pattern) {
23224
+ return tokenize(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
23225
+ }
23226
+ function splitRawTokens(pattern) {
23227
+ return pattern.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
23228
+ }
23229
+ function keywordNeedleLosesTokens(pattern) {
23230
+ const raw = splitRawTokens(pattern);
23231
+ if (raw.length < 2) return false;
23232
+ return tokenize(pattern).length !== raw.length;
23233
+ }
23234
+
23235
+ // src/lessons/regex-linear/nfa-compile.ts
23236
+ var MAX_NFA_STATES = 2e3;
23237
+ var Builder = class {
23238
+ states = [];
23239
+ alloc() {
23240
+ if (this.states.length >= MAX_NFA_STATES) {
23241
+ throw new Error(`NFA state limit exceeded (${MAX_NFA_STATES}); pattern expands too large`);
23242
+ }
23243
+ this.states.push({ eps: [], asserts: [], chars: [] });
23244
+ return this.states.length - 1;
23245
+ }
23246
+ };
23247
+ function isNonLineTerminator(c2) {
23248
+ return c2 !== "\n" && c2 !== "\r" && c2 !== "\u2028" && c2 !== "\u2029";
23249
+ }
23250
+ function compileNode(b, node) {
23251
+ switch (node.k) {
23252
+ case "empty":
23253
+ case "assert": {
23254
+ const s = b.alloc();
23255
+ const e = b.alloc();
23256
+ if (node.k === "assert") b.states[s].asserts.push({ kind: node.kind, target: e });
23257
+ else b.states[s].eps.push(e);
23258
+ return { start: s, end: e };
23259
+ }
23260
+ case "char":
23261
+ case "any":
23262
+ case "class": {
23263
+ const s = b.alloc();
23264
+ const e = b.alloc();
23265
+ const test = node.k === "char" ? (c2) => c2 === node.ch : node.k === "any" ? isNonLineTerminator : node.test;
23266
+ b.states[s].chars.push({ test, target: e });
23267
+ return { start: s, end: e };
23268
+ }
23269
+ case "concat": {
23270
+ if (node.items.length === 0) return compileNode(b, { k: "empty" });
23271
+ let first = null;
23272
+ let prevEnd = -1;
23273
+ for (const item of node.items) {
23274
+ const frag = compileNode(b, item);
23275
+ if (first === null) first = frag;
23276
+ else b.states[prevEnd].eps.push(frag.start);
23277
+ prevEnd = frag.end;
23278
+ }
23279
+ return { start: first.start, end: prevEnd };
23280
+ }
23281
+ case "alt": {
23282
+ const s = b.alloc();
23283
+ const e = b.alloc();
23284
+ for (const opt of node.opts) {
23285
+ const frag = compileNode(b, opt);
23286
+ b.states[s].eps.push(frag.start);
23287
+ b.states[frag.end].eps.push(e);
23288
+ }
23289
+ return { start: s, end: e };
23290
+ }
23291
+ case "opt": {
23292
+ const s = b.alloc();
23293
+ const e = b.alloc();
23294
+ const frag = compileNode(b, node.node);
23295
+ b.states[s].eps.push(frag.start, e);
23296
+ b.states[frag.end].eps.push(e);
23297
+ return { start: s, end: e };
23298
+ }
23299
+ case "star": {
23300
+ const s = b.alloc();
23301
+ const e = b.alloc();
23302
+ const frag = compileNode(b, node.node);
23303
+ b.states[s].eps.push(frag.start, e);
23304
+ b.states[frag.end].eps.push(frag.start, e);
23305
+ return { start: s, end: e };
23306
+ }
23307
+ case "plus": {
23308
+ const e = b.alloc();
23309
+ const frag = compileNode(b, node.node);
23310
+ b.states[frag.end].eps.push(frag.start, e);
23311
+ return { start: frag.start, end: e };
23312
+ }
23313
+ }
23314
+ }
23315
+ function compileNfa(ast) {
23316
+ const b = new Builder();
23317
+ const { start, end } = compileNode(b, ast);
23318
+ return { states: b.states, start, accept: end };
23319
+ }
23320
+
23321
+ // src/lessons/regex-linear/nfa.ts
23322
+ function wordBoundary(input, pos) {
23323
+ const before = pos > 0 && /[A-Za-z0-9_]/.test(input[pos - 1]);
23324
+ const after = pos < input.length && /[A-Za-z0-9_]/.test(input[pos]);
23325
+ return before !== after;
23326
+ }
23327
+ function assertHolds(kind, input, pos) {
23328
+ switch (kind) {
23329
+ case "start":
23330
+ return pos === 0;
23331
+ case "end":
23332
+ return pos === input.length;
23333
+ case "wordB":
23334
+ return wordBoundary(input, pos);
23335
+ case "nonWordB":
23336
+ return !wordBoundary(input, pos);
23337
+ }
23338
+ }
23339
+ function buildMatcher(ast) {
23340
+ const { states, start, accept } = compileNfa(ast);
23341
+ const closure = (set, idx, input, pos, budget) => {
23342
+ const stack = [idx];
23343
+ while (stack.length > 0) {
23344
+ if (budget.remaining <= 0) return;
23345
+ const cur = stack.pop();
23346
+ if (set.has(cur)) continue;
23347
+ set.add(cur);
23348
+ budget.remaining -= 1;
23349
+ for (const t of states[cur].eps) if (!set.has(t)) stack.push(t);
23350
+ for (const a of states[cur].asserts) {
23351
+ if (assertHolds(a.kind, input, pos) && !set.has(a.target)) stack.push(a.target);
23352
+ }
23353
+ }
23354
+ };
23355
+ return {
23356
+ // The input is matched in full (no truncation — truncation would miss suffix
23357
+ // matches and let `$` falsely match an invented endpoint). Work is bounded by
23358
+ // the shared budget instead: when it runs out we report a safe non-match.
23359
+ test(input, budget) {
23360
+ const b = budget ?? { remaining: Number.POSITIVE_INFINITY };
23361
+ if (b.remaining <= 0) return false;
23362
+ let current = /* @__PURE__ */ new Set();
23363
+ for (let pos = 0; pos <= input.length; pos += 1) {
23364
+ closure(current, start, input, pos, b);
23365
+ if (current.has(accept)) return true;
23366
+ if (b.remaining <= 0) return false;
23367
+ if (pos === input.length) break;
23368
+ const ch = input[pos];
23369
+ const next = /* @__PURE__ */ new Set();
23370
+ for (const s of current) {
23371
+ b.remaining -= 1;
23372
+ for (const t of states[s].chars) {
23373
+ if (t.test(ch)) closure(next, t.target, input, pos + 1, b);
23374
+ }
23375
+ }
23376
+ current = next;
23377
+ }
23378
+ return current.has(accept);
23379
+ }
23380
+ };
23381
+ }
23382
+
23383
+ // src/lessons/regex-linear/ast.ts
23384
+ var UnsupportedRegexError = class extends Error {
23385
+ constructor(message) {
23386
+ super(message);
23387
+ this.name = "UnsupportedRegexError";
23388
+ }
23389
+ };
23390
+
23391
+ // src/lessons/regex-linear/parse-helpers.ts
23392
+ var MAX_REPEAT = 1e3;
23393
+ var isWord = (c2) => /[A-Za-z0-9_]/.test(c2);
23394
+ function expandRepeat(atom, min, max) {
23395
+ const items = [];
23396
+ for (let k = 0; k < min; k += 1) items.push(atom);
23397
+ if (max === Infinity) {
23398
+ items.push({ k: "star", node: atom });
23399
+ } else {
23400
+ for (let k = min; k < max; k += 1) items.push({ k: "opt", node: atom });
23401
+ }
23402
+ if (items.length === 0) return { k: "empty" };
23403
+ return items.length === 1 ? items[0] : { k: "concat", items };
23404
+ }
23405
+ function escapeClass(c2) {
23406
+ switch (c2) {
23407
+ case "d":
23408
+ return (x) => x >= "0" && x <= "9";
23409
+ case "D":
23410
+ return (x) => !(x >= "0" && x <= "9");
23411
+ case "w":
23412
+ return isWord;
23413
+ case "W":
23414
+ return (x) => !isWord(x);
23415
+ case "s":
23416
+ return (x) => /\s/.test(x);
23417
+ case "S":
23418
+ return (x) => !/\s/.test(x);
23419
+ default:
23420
+ return null;
23421
+ }
23422
+ }
23423
+ var HEX2 = /^[0-9a-fA-F]{2}$/;
23424
+ var HEX4 = /^[0-9a-fA-F]{4}$/;
23425
+ function readUnicodeEscape(src, i, c2) {
23426
+ if (c2 === "x") {
23427
+ const hex2 = src.slice(i, i + 2);
23428
+ return HEX2.test(hex2) ? { ch: String.fromCharCode(parseInt(hex2, 16)), len: 2 } : { ch: "x", len: 0 };
23429
+ }
23430
+ const hex = src.slice(i, i + 4);
23431
+ return HEX4.test(hex) ? { ch: String.fromCharCode(parseInt(hex, 16)), len: 4 } : { ch: "u", len: 0 };
23432
+ }
23433
+ function readControlEscape(src, i) {
23434
+ const x = src[i];
23435
+ if (x === void 0 || !/[A-Za-z]/.test(x)) {
23436
+ throw new UnsupportedRegexError("\\c must be followed by a letter");
23437
+ }
23438
+ return { ch: String.fromCharCode(x.charCodeAt(0) & 31), len: 1 };
23439
+ }
23440
+ function classEscapeChar(src, i, e) {
23441
+ if (e === "b") return { ch: "\b", len: 0 };
23442
+ if (e === "c") return readControlEscape(src, i);
23443
+ if (e === "x" || e === "u") return readUnicodeEscape(src, i, e);
23444
+ return { ch: escapeLiteral(e), len: 0 };
23445
+ }
23446
+ function escapeLiteral(c2) {
23447
+ switch (c2) {
23448
+ case "t":
23449
+ return " ";
23450
+ case "n":
23451
+ return "\n";
23452
+ case "r":
23453
+ return "\r";
23454
+ case "f":
23455
+ return "\f";
23456
+ case "v":
23457
+ return "\v";
23458
+ case "0":
23459
+ return "\0";
23460
+ default:
23461
+ return c2;
23462
+ }
23463
+ }
23464
+
23465
+ // src/lessons/regex-linear/parse.ts
23466
+ function parseRegex(src) {
23467
+ let i = 0;
23468
+ const peek = () => src[i];
23469
+ const eat = () => src[i++];
23470
+ function parseAlt() {
23471
+ const opts = [parseConcat()];
23472
+ while (peek() === "|") {
23473
+ i += 1;
23474
+ opts.push(parseConcat());
23475
+ }
23476
+ return opts.length === 1 ? opts[0] : { k: "alt", opts };
23477
+ }
23478
+ function parseConcat() {
23479
+ const items = [];
23480
+ while (i < src.length && peek() !== "|" && peek() !== ")") {
23481
+ items.push(parseQuantified());
23482
+ }
23483
+ if (items.length === 0) return { k: "empty" };
23484
+ return items.length === 1 ? items[0] : { k: "concat", items };
23485
+ }
23486
+ function parseQuantified() {
23487
+ const atom = parseAtom();
23488
+ const q = peek();
23489
+ if (q === "*" || q === "+" || q === "?") {
23490
+ i += 1;
23491
+ if (peek() === "?") i += 1;
23492
+ return q === "*" ? { k: "star", node: atom } : q === "+" ? { k: "plus", node: atom } : { k: "opt", node: atom };
23493
+ }
23494
+ if (q === "{") {
23495
+ const repeat = tryParseBrace();
23496
+ if (repeat !== null) return expandRepeat(atom, repeat.min, repeat.max);
23497
+ }
23498
+ return atom;
23499
+ }
23500
+ function tryParseBrace() {
23501
+ const m = /^\{(\d+)(,(\d*)?)?\}/.exec(src.slice(i));
23502
+ if (m === null) return null;
23503
+ i += m[0].length;
23504
+ if (peek() === "?") i += 1;
23505
+ const min = Number(m[1]);
23506
+ const max = m[2] === void 0 ? min : m[3] === "" || m[3] === void 0 ? Infinity : Number(m[3]);
23507
+ if (min > MAX_REPEAT || max !== Infinity && max > MAX_REPEAT) {
23508
+ throw new UnsupportedRegexError(`Repeat count over ${MAX_REPEAT} not supported: {${m[1]}\u2026}`);
23509
+ }
23510
+ return { min, max };
23511
+ }
23512
+ function parseAtom() {
23513
+ const c2 = peek();
23514
+ if (c2 === "(") return parseGroup();
23515
+ if (c2 === "[") return parseClass();
23516
+ if (c2 === "\\") return parseEscape();
23517
+ if (c2 === ".") {
23518
+ i += 1;
23519
+ return { k: "any" };
23520
+ }
23521
+ if (c2 === "^") {
23522
+ i += 1;
23523
+ return { k: "assert", kind: "start" };
23524
+ }
23525
+ if (c2 === "$") {
23526
+ i += 1;
23527
+ return { k: "assert", kind: "end" };
23528
+ }
23529
+ if (c2 === void 0 || c2 === "*" || c2 === "+" || c2 === "?" || c2 === ")") {
23530
+ throw new UnsupportedRegexError(`Unexpected '${c2 ?? "<end>"}' in pattern`);
23531
+ }
23532
+ i += 1;
23533
+ return { k: "char", ch: c2 };
23534
+ }
23535
+ function parseGroup() {
23536
+ i += 1;
23537
+ if (peek() === "?") {
23538
+ const c2 = src[i + 1];
23539
+ if (c2 === "=" || c2 === "!" || c2 === "<") {
23540
+ if (!(c2 === "<" && /[A-Za-z]/.test(src[i + 2] ?? ""))) {
23541
+ throw new UnsupportedRegexError("Lookaround assertions are not supported");
23542
+ }
23543
+ }
23544
+ if (c2 === ":") i += 2;
23545
+ else if (c2 === "<") {
23546
+ i += 2;
23547
+ while (i < src.length && src[i] !== ">") i += 1;
23548
+ i += 1;
23549
+ }
23550
+ }
23551
+ const inner = parseAlt();
23552
+ if (peek() !== ")") throw new UnsupportedRegexError("Unbalanced group");
23553
+ i += 1;
23554
+ return inner;
23555
+ }
23556
+ function parseEscape() {
23557
+ i += 1;
23558
+ const c2 = peek();
23559
+ if (c2 === void 0) throw new UnsupportedRegexError("Trailing backslash");
23560
+ if (/[1-9]/.test(c2) || c2 === "k")
23561
+ throw new UnsupportedRegexError("Backreferences are not supported");
23562
+ i += 1;
23563
+ if (c2 === "b") return { k: "assert", kind: "wordB" };
23564
+ if (c2 === "B") return { k: "assert", kind: "nonWordB" };
23565
+ const cls = escapeClass(c2);
23566
+ if (cls !== null) return { k: "class", test: cls };
23567
+ if (c2 === "x" || c2 === "u" || c2 === "c") {
23568
+ const { ch, len } = c2 === "c" ? readControlEscape(src, i) : readUnicodeEscape(src, i, c2);
23569
+ i += len;
23570
+ return { k: "char", ch };
23571
+ }
23572
+ return { k: "char", ch: escapeLiteral(c2) };
23573
+ }
23574
+ function parseClass() {
23575
+ i += 1;
23576
+ const negate = peek() === "^";
23577
+ if (negate) i += 1;
23578
+ const tests = [];
23579
+ while (i < src.length && peek() !== "]") {
23580
+ tests.push(parseClassMember());
23581
+ }
23582
+ if (peek() !== "]") throw new UnsupportedRegexError("Unterminated character class");
23583
+ i += 1;
23584
+ const base = (c2) => tests.some((t) => t(c2));
23585
+ return { k: "class", test: negate ? (c2) => !base(c2) : base };
23586
+ }
23587
+ function parseClassMember() {
23588
+ let lo;
23589
+ if (peek() === "\\") {
23590
+ i += 1;
23591
+ const e = eat();
23592
+ const cls = escapeClass(e);
23593
+ if (cls !== null) return cls;
23594
+ const r = classEscapeChar(src, i, e);
23595
+ i += r.len;
23596
+ lo = r.ch;
23597
+ } else {
23598
+ lo = eat();
23599
+ }
23600
+ if (peek() === "-" && src[i + 1] !== void 0 && src[i + 1] !== "]") {
23601
+ i += 1;
23602
+ let hi;
23603
+ if (peek() === "\\") {
23604
+ i += 1;
23605
+ const e2 = eat();
23606
+ const r = classEscapeChar(src, i, e2);
23607
+ i += r.len;
23608
+ hi = r.ch;
23609
+ } else {
23610
+ hi = eat();
23611
+ }
23612
+ const a = lo.codePointAt(0);
23613
+ const b = hi.codePointAt(0);
23614
+ return (c2) => {
23615
+ const p = c2.codePointAt(0);
23616
+ return p >= a && p <= b;
23617
+ };
23618
+ }
23619
+ return (c2) => c2 === lo;
23620
+ }
23621
+ const ast = parseAlt();
23622
+ if (i !== src.length) throw new UnsupportedRegexError(`Unexpected '${peek()}' at ${i}`);
23623
+ return ast;
23624
+ }
23625
+
23626
+ // src/lessons/regex-linear/index.ts
23627
+ var cache = /* @__PURE__ */ new Map();
23628
+ function compileLinearMatcher(pattern) {
23629
+ const hit = cache.get(pattern);
23630
+ if (hit !== void 0 || cache.has(pattern)) return hit ?? null;
23631
+ let matcher;
23632
+ try {
23633
+ matcher = buildMatcher(parseRegex(pattern));
23634
+ } catch {
23635
+ matcher = null;
23636
+ }
23637
+ cache.set(pattern, matcher);
23638
+ return matcher;
23639
+ }
23640
+
23641
+ // src/lessons/regex-safety.ts
23642
+ var MAX_PATTERN_LENGTH = 1e3;
23643
+ function isSafeRegexPattern(pattern) {
23644
+ if (pattern.length > MAX_PATTERN_LENGTH) return false;
23645
+ return compileLinearMatcher(pattern) !== null;
23646
+ }
23647
+ function getCommandMatcher(pattern) {
23648
+ if (pattern.length > MAX_PATTERN_LENGTH) return null;
23649
+ return compileLinearMatcher(pattern);
23650
+ }
23651
+
23652
+ // src/lessons/validate-quality.ts
23653
+ function collectDuplicateRules(graph, findings) {
23654
+ const byKey = /* @__PURE__ */ new Map();
23655
+ for (const [lessonId, lesson] of Object.entries(graph.lessons)) {
23656
+ if (lesson.status !== "active") continue;
23657
+ const key = normalizeRule(lesson.rule);
23658
+ const bucket = byKey.get(key) ?? [];
23659
+ bucket.push(lessonId);
23660
+ byKey.set(key, bucket);
23661
+ }
23662
+ for (const [key, ids] of byKey) {
23663
+ if (ids.length < 2) continue;
23664
+ const sorted = [...ids].sort();
23665
+ for (const lessonId of sorted) {
23666
+ const others = sorted.filter((other) => other !== lessonId);
23667
+ findings.push({
23668
+ level: "error",
23669
+ code: "DUPLICATE_RULE",
23670
+ message: `Lesson "${lessonId}" duplicates rule text of: ${others.join(", ")} (normalized key: "${key.slice(0, 60)}").`,
23671
+ lessonId
23672
+ });
23673
+ }
23674
+ }
23675
+ }
23676
+ function collectInvalidTriggerPatterns(graph, findings) {
23677
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23678
+ if (trigger.kind !== "command_pattern") continue;
23679
+ try {
23680
+ new RegExp(trigger.pattern);
23681
+ } catch (err) {
23682
+ findings.push({
23683
+ level: "error",
23684
+ code: "INVALID_TRIGGER_PATTERN",
23685
+ message: `Trigger "${triggerId}" has an invalid command_pattern regex (${trigger.pattern}): ${err instanceof Error ? err.message : String(err)}.`,
23686
+ triggerId
23687
+ });
23688
+ continue;
23689
+ }
23690
+ if (!isSafeRegexPattern(trigger.pattern)) {
23691
+ findings.push({
23692
+ level: "error",
23693
+ code: "UNSAFE_TRIGGER_PATTERN",
23694
+ 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.`,
23695
+ triggerId
23696
+ });
23697
+ }
23698
+ }
23699
+ }
23700
+ function collectBackslashGlobPatterns(graph, findings) {
23701
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23702
+ if (trigger.kind !== "file_glob") continue;
23703
+ if (!trigger.pattern.includes("\\")) continue;
23704
+ findings.push({
23705
+ level: "error",
23706
+ code: "BACKSLASH_GLOB_PATTERN",
23707
+ 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 /.`,
23708
+ triggerId
23709
+ });
23710
+ }
23711
+ }
23712
+ function collectDuplicateTriggers(graph, findings) {
23713
+ const byKey = /* @__PURE__ */ new Map();
23714
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23715
+ const key = `${trigger.kind}|${trigger.pattern}`;
23716
+ const bucket = byKey.get(key) ?? [];
23717
+ bucket.push(triggerId);
23718
+ byKey.set(key, bucket);
23719
+ }
23720
+ for (const [key, ids] of byKey) {
23721
+ if (ids.length < 2) continue;
23722
+ const sorted = [...ids].sort();
23723
+ for (const triggerId of sorted) {
23724
+ const others = sorted.filter((other) => other !== triggerId);
23725
+ findings.push({
23726
+ level: "error",
23727
+ code: "DUPLICATE_TRIGGER",
23728
+ message: `Trigger "${triggerId}" duplicates (kind, pattern) of: ${others.join(", ")} (key: "${key}").`,
23729
+ triggerId
23730
+ });
23731
+ }
23732
+ }
23733
+ }
23734
+ var HIGH_FANOUT_THRESHOLD = 10;
23735
+ function collectFanout(graph, findings) {
23736
+ const fanout = /* @__PURE__ */ new Map();
23737
+ for (const lesson of Object.values(graph.lessons)) {
23738
+ if (lesson.status !== "active") continue;
23739
+ for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
23740
+ }
23741
+ let over = 0;
23742
+ let max = 0;
23743
+ for (const n of fanout.values()) {
23744
+ if (n > HIGH_FANOUT_THRESHOLD) over += 1;
23745
+ if (n > max) max = n;
23746
+ }
23747
+ if (over > 0) {
23748
+ findings.push({
23749
+ level: "warning",
23750
+ code: "HIGH_FANOUT_TRIGGERS",
23751
+ 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.`
23752
+ });
23753
+ }
23754
+ }
23755
+ function collectLowSignalKeywords(graph, findings) {
23756
+ const activeTriggerIds2 = /* @__PURE__ */ new Set();
23757
+ for (const lesson of Object.values(graph.lessons)) {
23758
+ if (lesson.status !== "active") continue;
23759
+ for (const t of lesson.triggers) activeTriggerIds2.add(t);
23760
+ }
23761
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23762
+ if (trigger.kind !== "keyword") continue;
23763
+ if (!activeTriggerIds2.has(triggerId)) continue;
23764
+ if (!isLowSignalKeyword(trigger.pattern)) continue;
23765
+ findings.push({
23766
+ level: "warning",
23767
+ code: "LOW_SIGNAL_KEYWORD",
23768
+ 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.`,
23769
+ triggerId
23770
+ });
23771
+ }
23772
+ }
23773
+ function collectStopwordKeywords(graph, findings) {
23774
+ const activeTriggerIds2 = /* @__PURE__ */ new Set();
23775
+ for (const lesson of Object.values(graph.lessons)) {
23776
+ if (lesson.status !== "active") continue;
23777
+ for (const t of lesson.triggers) activeTriggerIds2.add(t);
23778
+ }
23779
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23780
+ if (trigger.kind !== "keyword") continue;
23781
+ if (!activeTriggerIds2.has(triggerId)) continue;
23782
+ if (tokenize(trigger.pattern).length !== 0 && !keywordNeedleLosesTokens(trigger.pattern)) {
23783
+ continue;
23784
+ }
23785
+ findings.push({
23786
+ level: "warning",
23787
+ code: "STOPWORD_KEYWORD",
23788
+ 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\`.`,
23789
+ triggerId
23790
+ });
23791
+ }
23792
+ }
23793
+ function normalizeRule(rule) {
23794
+ return rule.trim().replace(/\s+/g, " ").toLowerCase();
23795
+ }
23796
+ function activeTriggerIds(graph) {
23797
+ const ids = /* @__PURE__ */ new Set();
23798
+ for (const lesson of Object.values(graph.lessons)) {
23799
+ if (lesson.status !== "active") continue;
23800
+ for (const t of lesson.triggers) ids.add(t);
23801
+ }
23802
+ return ids;
23803
+ }
23804
+ function deadFileGlobIds(graph, knownPaths) {
23805
+ const active = activeTriggerIds(graph);
23806
+ const paths = [...knownPaths];
23807
+ const dead = /* @__PURE__ */ new Set();
23808
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23809
+ if (trigger.kind !== "file_glob") continue;
23810
+ if (!active.has(triggerId)) continue;
23811
+ const isMatch = picomatch(trigger.pattern, { dot: true });
23812
+ if (!paths.some((p) => isMatch(p))) dead.add(triggerId);
23813
+ }
23814
+ return dead;
23815
+ }
23816
+ function collectDeadFileGlobs(graph, findings, knownPaths) {
23817
+ for (const triggerId of deadFileGlobIds(graph, knownPaths)) {
23818
+ findings.push({
23819
+ level: "warning",
23820
+ code: "DEAD_FILE_GLOB",
23821
+ 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\`.`,
23822
+ triggerId
23823
+ });
23824
+ }
23825
+ }
23826
+ var RUNNER_ANCHOR = /^\^(pnpm|npm|npx|yarn|bun)\b/;
23827
+ function collectRunnerAnchoredPatterns(graph, findings) {
23828
+ const active = activeTriggerIds(graph);
23829
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
23830
+ if (trigger.kind !== "command_pattern") continue;
23831
+ if (!active.has(triggerId)) continue;
23832
+ if (!RUNNER_ANCHOR.test(trigger.pattern)) continue;
23833
+ findings.push({
23834
+ level: "warning",
23835
+ code: "RUNNER_ANCHORED_PATTERN",
23836
+ 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\`).`,
23837
+ triggerId
23838
+ });
23839
+ }
23840
+ }
23841
+
23842
+ // src/lessons/validate.ts
23843
+ function validateLessonsGraph(graph, options = {}) {
23844
+ const findings = [];
23845
+ const schemaResult = LessonsGraphSchema.safeParse(graph);
23846
+ if (!schemaResult.success) {
23847
+ findings.push({
23848
+ level: "error",
23849
+ code: "SCHEMA_INVALID",
23850
+ message: schemaResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
23851
+ });
23852
+ return { ok: false, findings };
23853
+ }
23854
+ collectDanglingRefs(graph, findings);
23855
+ collectDuplicateRefs(graph, findings);
23856
+ collectStatusInvariants(graph, findings);
23857
+ collectLifecycleInvariants(graph, findings);
23858
+ collectDuplicateRules(graph, findings);
23859
+ collectReachability(graph, findings);
23860
+ collectInvalidTriggerPatterns(graph, findings);
23861
+ collectBackslashGlobPatterns(graph, findings);
23862
+ collectDuplicateTriggers(graph, findings);
23863
+ collectOrphans(graph, findings);
23864
+ collectFanout(graph, findings);
23865
+ collectLowSignalKeywords(graph, findings);
23866
+ collectStopwordKeywords(graph, findings);
23867
+ collectRunnerAnchoredPatterns(graph, findings);
23868
+ if (options.knownPaths !== void 0) collectDeadFileGlobs(graph, findings, options.knownPaths);
23869
+ const ok = findings.every((f) => f.level !== "error");
23870
+ return { ok, findings };
23871
+ }
23872
+
23873
+ // src/core/lint/shared/lessons.ts
23874
+ var LESSONS_TARGET = "lessons";
23875
+ var GRAPH_REL = ".agentsmesh/lessons/lessons.json";
23876
+ var ROOT_RULE_REL = ".agentsmesh/rules/_root.md";
23877
+ var LESSONS_HEADING = /^## Lessons \(/m;
23878
+ function lintLessonsSubsystem(projectRoot, scope) {
23879
+ if (scope === "global") return [];
23880
+ const paths = lessonsPaths(projectRoot);
23881
+ if (!existsSync(paths.graph)) return [];
23882
+ const out2 = [];
23883
+ let graph;
23884
+ try {
23885
+ graph = loadLessonsGraph(projectRoot);
23886
+ } catch (err) {
23887
+ return [
23888
+ diag(
23889
+ "error",
23890
+ GRAPH_REL,
23891
+ `lessons.json failed to load: ${err instanceof Error ? err.message : String(err)}`
23892
+ )
23893
+ ];
23894
+ }
23895
+ const knownPaths = listProjectFiles(projectRoot) ?? void 0;
23896
+ const report = validateLessonsGraph(graph, { knownPaths });
23897
+ for (const finding of report.findings) {
23898
+ out2.push(diag(finding.level, GRAPH_REL, `[${finding.code}] ${finding.message}`));
23899
+ }
23900
+ const rootRuleAbs = join(projectRoot, ROOT_RULE_REL);
23901
+ const rootRuleBody = existsSync(rootRuleAbs) ? readFileSync(rootRuleAbs, "utf8") : "";
23902
+ if (!LESSONS_HEADING.test(rootRuleBody)) {
23903
+ out2.push(
23904
+ diag(
23905
+ "warning",
23906
+ ROOT_RULE_REL,
23907
+ 'lessons procedural rule ("## Lessons (...)") is missing from _root.md \u2014 recall/capture enforcement will not fire.'
23908
+ )
23909
+ );
23910
+ }
23911
+ return out2;
23912
+ }
23913
+ function diag(level, file, message) {
23914
+ return { level, file, target: LESSONS_TARGET, message };
23915
+ }
23916
+
23917
+ // src/core/lint/linter.ts
23918
+ var EXCLUDE_DIRS = ["node_modules", ".git", "dist", "coverage", ".agentsmesh"];
23919
+ async function getProjectFiles(projectRoot) {
23920
+ const all = await readDirRecursive(projectRoot);
23921
+ const filtered = all.filter((p) => {
23922
+ const rel2 = relative(projectRoot, p);
23923
+ return !EXCLUDE_DIRS.some((d) => rel2.includes(`/${d}/`) || rel2.startsWith(`${d}/`));
23924
+ });
23925
+ return filtered.map((p) => relative(projectRoot, p));
23926
+ }
23927
+ async function runLint(config, canonical, projectRoot, targetFilter, options = {}) {
23928
+ const scope = options.scope ?? "project";
23929
+ const allTargets = [...config.targets, ...config.pluginTargets ?? []];
23930
+ const targets = targetFilter ? allTargets.filter((t) => targetFilter.includes(t)) : allTargets;
23931
+ const hasRules = config.features.includes("rules");
23932
+ const hasCommands = config.features.includes("commands");
23933
+ const hasMcp = config.features.includes("mcp");
23934
+ const hasPermissions = config.features.includes("permissions");
23935
+ const hasHooks = config.features.includes("hooks");
23936
+ const diagnostics = [...lintLessonsSubsystem(projectRoot, scope)];
23937
+ const projectFiles = scope === "global" ? [] : await getProjectFiles(projectRoot);
23938
+ for (const target31 of targets) {
23939
+ const fullDesc = getDescriptor(target31);
23940
+ const descriptor31 = isBuiltinTargetId(target31) ? getTargetCatalogEntry(target31) : fullDesc;
23941
+ if (descriptor31?.capabilities) {
23942
+ diagnostics.push(
23943
+ ...lintSilentFeatureDrops({
23944
+ target: target31,
23945
+ capabilities: descriptor31.capabilities,
23946
+ canonical,
23947
+ enabledFeatures: config.features
23948
+ })
22662
23949
  );
22663
- diffs.push({ path: r.path, patch });
23950
+ }
23951
+ if (hasHooks) {
23952
+ diagnostics.push(
23953
+ ...lintHookScriptReferences({
23954
+ target: target31,
23955
+ canonical,
23956
+ hasScriptProjection: fullDesc?.postProcessHookOutputs !== void 0
23957
+ })
23958
+ );
23959
+ }
23960
+ if (hasRules) {
23961
+ diagnostics.push(
23962
+ ...lintRuleScopeInversion({
23963
+ target: target31,
23964
+ canonical,
23965
+ preservesManualActivation: fullDesc?.preservesManualActivation === true
23966
+ })
23967
+ );
23968
+ }
23969
+ if (hasRules && descriptor31?.lintRules) {
23970
+ diagnostics.push(...descriptor31.lintRules(canonical, projectRoot, projectFiles, { scope }));
23971
+ }
23972
+ if (fullDesc?.generators.lint) {
23973
+ diagnostics.push(...fullDesc.generators.lint(canonical));
23974
+ }
23975
+ const lintOpts = { scope };
23976
+ if (hasCommands && descriptor31?.lint?.commands) {
23977
+ diagnostics.push(...descriptor31.lint.commands(canonical, lintOpts));
23978
+ }
23979
+ if (hasMcp && descriptor31?.lint?.mcp) {
23980
+ diagnostics.push(...descriptor31.lint.mcp(canonical, lintOpts));
23981
+ }
23982
+ if (hasPermissions && descriptor31?.lint?.permissions) {
23983
+ diagnostics.push(...descriptor31.lint.permissions(canonical, lintOpts));
23984
+ }
23985
+ if (hasHooks && descriptor31?.lint?.hooks) {
23986
+ diagnostics.push(...descriptor31.lint.hooks(canonical, lintOpts));
23987
+ }
23988
+ }
23989
+ const hasErrors = diagnostics.some((d) => d.level === "error");
23990
+ return { diagnostics, hasErrors };
23991
+ }
23992
+ function computeDiff(results) {
23993
+ const diffs = [];
23994
+ const summary = { new: 0, updated: 0, unchanged: 0, deleted: 0 };
23995
+ for (const r of results) {
23996
+ if (r.status === "unchanged") {
23997
+ summary.unchanged++;
23998
+ continue;
23999
+ }
24000
+ if (r.status === "created") {
24001
+ summary.new++;
24002
+ const patch = createTwoFilesPatch(
24003
+ `${r.path} (current)`,
24004
+ `${r.path} (generated)`,
24005
+ "",
24006
+ r.content,
24007
+ void 0,
24008
+ void 0,
24009
+ { context: 3 }
24010
+ );
24011
+ diffs.push({ path: r.path, patch });
24012
+ continue;
24013
+ }
24014
+ if (r.status === "updated" && r.currentContent !== void 0) {
24015
+ summary.updated++;
24016
+ const patch = createTwoFilesPatch(
24017
+ `${r.path} (current)`,
24018
+ `${r.path} (generated)`,
24019
+ r.currentContent,
24020
+ r.content,
24021
+ void 0,
24022
+ void 0,
24023
+ { context: 3 }
24024
+ );
24025
+ diffs.push({ path: r.path, patch });
24026
+ continue;
24027
+ }
24028
+ }
24029
+ return { diffs, summary };
24030
+ }
24031
+ function formatDiffSummary(summary) {
24032
+ return `${summary.new} files would be created, ${summary.updated} updated, ${summary.unchanged} unchanged, ${summary.deleted} deleted`;
24033
+ }
24034
+
24035
+ // src/config/core/lock.ts
24036
+ init_fs();
24037
+
24038
+ // src/utils/crypto/hash.ts
24039
+ init_fs_text_encoding();
24040
+ function hashContent(content) {
24041
+ return createHash("sha256").update(content, "utf8").digest("hex");
24042
+ }
24043
+ async function hashFile(path) {
24044
+ try {
24045
+ const bytes = await readFile(path);
24046
+ return createHash("sha256").update(bytes).digest("hex");
24047
+ } catch (err) {
24048
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
24049
+ return null;
24050
+ }
24051
+ throw err;
24052
+ }
24053
+ }
24054
+
24055
+ // src/config/core/lock.ts
24056
+ var LOCK_FILENAME = ".lock";
24057
+ var CANONICAL_PATTERNS = [
24058
+ (r) => r.startsWith("rules/") && r.endsWith(".md"),
24059
+ (r) => r.startsWith("commands/") && r.endsWith(".md"),
24060
+ (r) => r.startsWith("agents/") && r.endsWith(".md"),
24061
+ (r) => r.match(/^skills\/[^/]+\/.+$/) !== null,
24062
+ (r) => r === "mcp.json",
24063
+ (r) => r === "permissions.yaml",
24064
+ (r) => r === "hooks.yaml",
24065
+ (r) => r === "ignore"
24066
+ ];
24067
+ function isCanonical(relPath) {
24068
+ if (relPath.startsWith("packs/")) return false;
24069
+ return CANONICAL_PATTERNS.some((p) => p(relPath));
24070
+ }
24071
+ var FEATURE_PATTERNS = {
24072
+ rules: (path) => path.startsWith("rules/"),
24073
+ commands: (path) => path.startsWith("commands/"),
24074
+ agents: (path) => path.startsWith("agents/"),
24075
+ skills: (path) => /^skills\/[^/]+\/.+$/.test(path),
24076
+ mcp: (path) => path === "mcp.json",
24077
+ permissions: (path) => path === "permissions.yaml",
24078
+ hooks: (path) => path === "hooks.yaml",
24079
+ ignore: (path) => path === "ignore"
24080
+ };
24081
+ async function readLock(abDir) {
24082
+ const lockPath = join(abDir, LOCK_FILENAME);
24083
+ const content = await readFileSafe(lockPath);
24084
+ if (content === null) return null;
24085
+ try {
24086
+ const raw = parse(content);
24087
+ if (!raw || typeof raw !== "object") return null;
24088
+ return {
24089
+ generatedAt: String(raw.generated_at ?? ""),
24090
+ generatedBy: String(raw.generated_by ?? ""),
24091
+ libVersion: String(raw.lib_version ?? ""),
24092
+ checksums: raw.checksums && typeof raw.checksums === "object" ? raw.checksums : {},
24093
+ extends: raw.extends && typeof raw.extends === "object" ? raw.extends : {},
24094
+ packs: raw.packs && typeof raw.packs === "object" ? raw.packs : {}
24095
+ };
24096
+ } catch {
24097
+ return null;
24098
+ }
24099
+ }
24100
+ async function buildChecksums(abDir) {
24101
+ if (!await exists(abDir)) return {};
24102
+ const files = await readDirRecursive(abDir);
24103
+ const result = {};
24104
+ for (const fullPath of files) {
24105
+ const rel2 = relative(abDir, fullPath).replace(/\\/g, "/");
24106
+ if (rel2 === LOCK_FILENAME) continue;
24107
+ if (!isCanonical(rel2)) continue;
24108
+ const h = await hashFile(fullPath);
24109
+ if (h !== null) {
24110
+ result[rel2] = h.startsWith("sha256:") ? h : `sha256:${h}`;
24111
+ }
24112
+ }
24113
+ return result;
24114
+ }
24115
+ function detectLockedFeatureViolations(lockChecksums, currentChecksums, lockFeatures) {
24116
+ if (lockFeatures.length === 0) return [];
24117
+ const matchers = lockFeatures.map((feature) => FEATURE_PATTERNS[feature]).filter((matcher) => matcher !== void 0);
24118
+ if (matchers.length === 0) return [];
24119
+ const allPaths = /* @__PURE__ */ new Set([...Object.keys(lockChecksums), ...Object.keys(currentChecksums)]);
24120
+ const violations = [];
24121
+ for (const path of allPaths) {
24122
+ if (!matchers.some((matcher) => matcher(path))) continue;
24123
+ if (lockChecksums[path] !== currentChecksums[path]) violations.push(path);
24124
+ }
24125
+ return violations;
24126
+ }
24127
+ async function buildExtendChecksums(resolvedExtends) {
24128
+ const result = {};
24129
+ for (const ext of resolvedExtends) {
24130
+ if (ext.version !== void 0) {
24131
+ result[ext.name] = ext.version;
24132
+ continue;
24133
+ }
24134
+ const abDir = join(ext.resolvedPath, ".agentsmesh");
24135
+ const checksums = await buildChecksums(abDir);
24136
+ const fingerprint = Object.keys(checksums).sort().map((p) => `${p}:${checksums[p]}`).join("\n");
24137
+ const h = hashContent(fingerprint);
24138
+ const hex = h.startsWith("sha256:") ? h : `sha256:${h}`;
24139
+ result[ext.name] = `local:${hex}`;
24140
+ }
24141
+ return result;
24142
+ }
24143
+
24144
+ // src/core/check/lock-sync.ts
24145
+ async function checkLockSync(opts) {
24146
+ const { config, configDir, canonicalDir } = opts;
24147
+ const lock = await readLock(canonicalDir);
24148
+ if (lock === null) {
24149
+ return {
24150
+ inSync: false,
24151
+ hasLock: false,
24152
+ modified: [],
24153
+ added: [],
24154
+ removed: [],
24155
+ extendsModified: [],
24156
+ lockedViolations: []
24157
+ };
24158
+ }
24159
+ const current = await buildChecksums(canonicalDir);
24160
+ const resolvedExtends = await resolveExtendPaths(config, configDir);
24161
+ const currentExtends = resolvedExtends.length > 0 ? await buildExtendChecksums(resolvedExtends) : {};
24162
+ const lockPaths = new Set(Object.keys(lock.checksums));
24163
+ const currentPaths = new Set(Object.keys(current));
24164
+ const modified = [];
24165
+ const added = [];
24166
+ const removed = [];
24167
+ for (const path of lockPaths) {
24168
+ const c2 = current[path];
24169
+ if (c2 === void 0) {
24170
+ removed.push(path);
24171
+ } else if (c2 !== lock.checksums[path]) {
24172
+ modified.push(path);
24173
+ }
24174
+ }
24175
+ for (const path of currentPaths) {
24176
+ if (!lockPaths.has(path)) {
24177
+ added.push(path);
24178
+ }
24179
+ }
24180
+ const extendNames = /* @__PURE__ */ new Set([...Object.keys(lock.extends), ...Object.keys(currentExtends)]);
24181
+ const extendsModified = [];
24182
+ for (const name of extendNames) {
24183
+ if (currentExtends[name] !== lock.extends[name]) {
24184
+ extendsModified.push(name);
24185
+ }
24186
+ }
24187
+ const lockedViolations = detectLockedFeatureViolations(
24188
+ lock.checksums,
24189
+ current,
24190
+ config.collaboration?.lock_features ?? []
24191
+ );
24192
+ const inSync = modified.length === 0 && added.length === 0 && removed.length === 0 && extendsModified.length === 0;
24193
+ return {
24194
+ inSync,
24195
+ hasLock: true,
24196
+ modified,
24197
+ added,
24198
+ removed,
24199
+ extendsModified,
24200
+ lockedViolations
24201
+ };
24202
+ }
24203
+
24204
+ // src/public/engine.ts
24205
+ init_registry();
24206
+ init_target_ids();
24207
+ async function importFrom(target31, opts) {
24208
+ const descriptor31 = getDescriptor(target31);
24209
+ if (!descriptor31) {
24210
+ throw new TargetNotFoundError(target31, {
24211
+ supported: [...TARGET_IDS, ...getAllDescriptors().map((d) => d.id)]
24212
+ });
24213
+ }
24214
+ return descriptor31.generators.importFrom(opts.root, { scope: opts.scope ?? "project" });
24215
+ }
24216
+ async function loadConfig2(projectRoot) {
24217
+ return loadConfigFromDir(projectRoot);
24218
+ }
24219
+ async function loadConfigFromDirectory(configDir) {
24220
+ return loadConfigFromExactDir(configDir);
24221
+ }
24222
+ async function loadProjectContext(projectRoot, options = {}) {
24223
+ const scope = options.scope ?? "project";
24224
+ const { config, context } = await loadScopedConfig(projectRoot, scope);
24225
+ await bootstrapPlugins(config, projectRoot);
24226
+ const { canonical } = await loadCanonicalWithExtends(
24227
+ config,
24228
+ context.configDir,
24229
+ { refreshRemoteCache: options.refreshRemoteCache === true },
24230
+ context.canonicalDir
24231
+ );
24232
+ return {
24233
+ config,
24234
+ canonical,
24235
+ projectRoot: context.rootBase,
24236
+ scope,
24237
+ configDir: context.configDir,
24238
+ canonicalDir: context.canonicalDir
24239
+ };
24240
+ }
24241
+ async function lint(opts) {
24242
+ const filter = opts.targetFilter ? [...opts.targetFilter] : void 0;
24243
+ return runLint(opts.config, opts.canonical, opts.projectRoot, filter, {
24244
+ scope: opts.scope
24245
+ });
24246
+ }
24247
+ function computeDiff2(results) {
24248
+ return computeDiff([...results]);
24249
+ }
24250
+ async function diff(ctx) {
24251
+ const results = await generate(ctx);
24252
+ const computed = computeDiff(results);
24253
+ return { ...computed, results };
24254
+ }
24255
+ async function check(opts) {
24256
+ return checkLockSync(opts);
24257
+ }
24258
+
24259
+ // src/public/canonical.ts
24260
+ init_errors();
24261
+ async function loadCanonical(projectRoot, options = {}) {
24262
+ if (options.includeExtends === false) {
24263
+ return loadCanonicalFiles(options.canonicalDir ?? projectRoot);
24264
+ }
24265
+ if (options.config === void 0 && options.configDir !== void 0 || options.config !== void 0 && options.configDir === void 0) {
24266
+ throw new Error("loadCanonical options require both config and configDir, or neither.");
24267
+ }
24268
+ try {
24269
+ const loaded = options.config !== void 0 && options.configDir !== void 0 ? { config: options.config, configDir: options.configDir } : await loadConfigFromDir(projectRoot);
24270
+ const { canonical } = await loadCanonicalWithExtends(
24271
+ loaded.config,
24272
+ loaded.configDir,
24273
+ { refreshRemoteCache: options.refreshRemoteCache === true },
24274
+ options.canonicalDir
24275
+ );
24276
+ return canonical;
24277
+ } catch (err) {
24278
+ if (err instanceof ConfigNotFoundError && options.config === void 0 && options.configDir === void 0) {
24279
+ return loadCanonicalFiles(options.canonicalDir ?? projectRoot);
24280
+ }
24281
+ throw err;
24282
+ }
24283
+ }
24284
+
24285
+ // src/public/targets.ts
24286
+ init_builtin_targets();
24287
+ init_registry();
24288
+ init_descriptor_import_runner();
24289
+ function copyCapabilities(capabilities17) {
24290
+ return Object.freeze({ ...capabilities17 });
24291
+ }
24292
+ function copyGenerators(generators) {
24293
+ return Object.freeze({ ...generators });
24294
+ }
24295
+ function copyOutputFamily(family) {
24296
+ return Object.freeze({
24297
+ ...family,
24298
+ explicitPaths: family.explicitPaths === void 0 ? void 0 : Object.freeze([...family.explicitPaths])
24299
+ });
24300
+ }
24301
+ function copyManagedOutputs(outputs) {
24302
+ return Object.freeze({
24303
+ dirs: Object.freeze([...outputs.dirs]),
24304
+ files: Object.freeze([...outputs.files])
24305
+ });
24306
+ }
24307
+ function copyLayout(layout) {
24308
+ return Object.freeze({
24309
+ ...layout,
24310
+ outputFamilies: layout.outputFamilies === void 0 ? void 0 : Object.freeze(layout.outputFamilies.map(copyOutputFamily)),
24311
+ managedOutputs: layout.managedOutputs === void 0 ? void 0 : copyManagedOutputs(layout.managedOutputs),
24312
+ paths: Object.freeze({ ...layout.paths })
24313
+ });
24314
+ }
24315
+ function copyGlobalSupport(globalSupport) {
24316
+ return Object.freeze({
24317
+ ...globalSupport,
24318
+ capabilities: copyCapabilities(globalSupport.capabilities),
24319
+ detectionPaths: Object.freeze([...globalSupport.detectionPaths]),
24320
+ layout: copyLayout(globalSupport.layout)
24321
+ });
24322
+ }
24323
+ function copyTargetDescriptor(descriptor31) {
24324
+ return Object.freeze({
24325
+ ...descriptor31,
24326
+ generators: copyGenerators(descriptor31.generators),
24327
+ capabilities: copyCapabilities(descriptor31.capabilities),
24328
+ globalSupport: descriptor31.globalSupport === void 0 ? void 0 : copyGlobalSupport(descriptor31.globalSupport),
24329
+ lint: descriptor31.lint === void 0 ? void 0 : Object.freeze({ ...descriptor31.lint }),
24330
+ project: copyLayout(descriptor31.project),
24331
+ supportsConversion: descriptor31.supportsConversion === void 0 ? void 0 : Object.freeze({ ...descriptor31.supportsConversion }),
24332
+ detectionPaths: Object.freeze([...descriptor31.detectionPaths]),
24333
+ sharedArtifacts: descriptor31.sharedArtifacts === void 0 ? void 0 : Object.freeze({ ...descriptor31.sharedArtifacts })
24334
+ });
24335
+ }
24336
+ function getTargetCatalog() {
24337
+ return Object.freeze(BUILTIN_TARGETS.map(copyTargetDescriptor));
24338
+ }
24339
+ function normalizeRule2(rule) {
24340
+ return rule.trim().replace(/\s+/g, " ").toLowerCase();
24341
+ }
24342
+ function union(base, extra) {
24343
+ const out2 = [...base];
24344
+ for (const item of extra) if (!out2.includes(item)) out2.push(item);
24345
+ return out2;
24346
+ }
24347
+ function mergeTriggers(graph, spec) {
24348
+ const requested = [
24349
+ // Normalize `\` → `/` so a Windows-shaped glob matches: recall relativizes
24350
+ // every `--file` to forward slashes (normalizeRecallFile), so a backslash
24351
+ // pattern stored raw would silently never fire. Normalizing here also lets
24352
+ // a backslash pattern dedupe against the forward-slash node it equals.
24353
+ ...(spec.files ?? []).map(
24354
+ (p) => ({ kind: "file_glob", pattern: p.replaceAll("\\", "/") })
24355
+ ),
24356
+ ...(spec.commands ?? []).map((p) => ({ kind: "command_pattern", pattern: p })),
24357
+ ...(spec.keywords ?? []).map((p) => ({ kind: "keyword", pattern: p }))
24358
+ ];
24359
+ const reverseLookup = /* @__PURE__ */ new Map();
24360
+ for (const [id, trigger] of Object.entries(graph.triggers)) {
24361
+ reverseLookup.set(triggerKey(trigger), id);
24362
+ }
24363
+ const triggerIds = [];
24364
+ const newTriggerIds = [];
24365
+ for (const spec2 of requested) {
24366
+ const key = triggerKey(spec2);
24367
+ const existing = reverseLookup.get(key);
24368
+ if (existing !== void 0) {
24369
+ if (!triggerIds.includes(existing)) triggerIds.push(existing);
22664
24370
  continue;
22665
24371
  }
22666
- if (r.status === "updated" && r.currentContent !== void 0) {
22667
- summary.updated++;
22668
- const patch = createTwoFilesPatch(
22669
- `${r.path} (current)`,
22670
- `${r.path} (generated)`,
22671
- r.currentContent,
22672
- r.content,
22673
- void 0,
22674
- void 0,
22675
- { context: 3 }
22676
- );
22677
- diffs.push({ path: r.path, patch });
24372
+ const id = makeTriggerId(spec2);
24373
+ graph.triggers[id] = { kind: spec2.kind, pattern: spec2.pattern };
24374
+ reverseLookup.set(key, id);
24375
+ triggerIds.push(id);
24376
+ newTriggerIds.push(id);
24377
+ }
24378
+ return { triggerIds, newTriggerIds };
24379
+ }
24380
+ function triggerKey(t) {
24381
+ return `${t.kind}|${t.pattern}`;
24382
+ }
24383
+ var TRIGGER_PREFIX = {
24384
+ file_glob: "glob",
24385
+ command_pattern: "cmd",
24386
+ keyword: "kw"
24387
+ };
24388
+ function makeTriggerId(spec) {
24389
+ const hash = createHash("sha1").update(triggerKey(spec)).digest("hex").slice(0, 8);
24390
+ return `t-${TRIGGER_PREFIX[spec.kind]}-${hash}`;
24391
+ }
24392
+ function makeLessonId(graph, topic, ruleKey) {
24393
+ const slug = ruleToSlug(ruleKey);
24394
+ const base = slug.length > 0 ? `${topic}-${slug}` : `${topic}-${createHash("sha1").update(ruleKey).digest("hex").slice(0, 8)}`;
24395
+ let candidate = base;
24396
+ let i = 2;
24397
+ while (graph.lessons[candidate] !== void 0) {
24398
+ candidate = `${base}-${i}`;
24399
+ i += 1;
24400
+ }
24401
+ return candidate;
24402
+ }
24403
+ function ruleToSlug(rule) {
24404
+ const words = rule.replace(/[^a-z0-9 ]+/g, " ").split(/\s+/).filter((w) => w.length > 0).slice(0, 5);
24405
+ return words.join("-").slice(0, 40).replace(/-+$/, "");
24406
+ }
24407
+ function todayIso() {
24408
+ const now = /* @__PURE__ */ new Date();
24409
+ const y = now.getUTCFullYear();
24410
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
24411
+ const d = String(now.getUTCDate()).padStart(2, "0");
24412
+ return `${y}-${m}-${d}`;
24413
+ }
24414
+
24415
+ // src/lessons/add-errors.ts
24416
+ var UnknownTopicError = class extends Error {
24417
+ constructor(topic) {
24418
+ super(`Unknown topic: ${topic}. Pass allowNewTopic + topicSummary to create it.`);
24419
+ this.topic = topic;
24420
+ this.name = "UnknownTopicError";
24421
+ }
24422
+ code = "UNKNOWN_TOPIC";
24423
+ };
24424
+ var RuleTooLongError = class extends Error {
24425
+ constructor(length, max) {
24426
+ super(
24427
+ `Lesson rule is ${length} characters (max ${max}). A rule should be one imperative sentence \u2014 trim it to the essential instruction, or split it into separate lessons.`
24428
+ );
24429
+ this.length = length;
24430
+ this.max = max;
24431
+ this.name = "RuleTooLongError";
24432
+ }
24433
+ code = "OVERSIZED_RULE";
24434
+ };
24435
+ var NoTriggerError = class extends Error {
24436
+ code = "NO_TRIGGER";
24437
+ constructor() {
24438
+ super(
24439
+ "A lesson needs at least one trigger to be recallable. Pass --trigger-file <glob> (preferred), --trigger-cmd <regex>, or --trigger-kw <text>."
24440
+ );
24441
+ this.name = "NoTriggerError";
24442
+ }
24443
+ };
24444
+ var UnrecallableLessonError = class extends Error {
24445
+ constructor(deadTriggers) {
24446
+ super(
24447
+ "This capture would create a lesson with no effective trigger \u2014 every trigger is dead on the mandatory --file/--cmd recall path, so the lesson could never be recalled there:\n" + deadTriggers.map((t) => ` \u2022 ${t.kind} "${t.pattern}" \u2014 ${t.reason}`).join("\n") + '\nFix: add a precise --trigger-file <glob> (preferred) or a valid --trigger-cmd <regex>; for a keyword, drop the stopwords (e.g. "state art" not "state of the art").'
24448
+ );
24449
+ this.deadTriggers = deadTriggers;
24450
+ this.name = "UnrecallableLessonError";
24451
+ }
24452
+ code = "UNRECALLABLE_LESSON";
24453
+ };
24454
+
24455
+ // src/lessons/capture-guardrails.ts
24456
+ var NEAR_DUPLICATE_THRESHOLD = 0.6;
24457
+ var MAX_RECOMMENDED_TRIGGERS = 8;
24458
+ function isBroadGlob(pattern) {
24459
+ const p = pattern.trim();
24460
+ if (p === "*" || p === "**") return true;
24461
+ if (!p.includes("**")) return false;
24462
+ const basename69 = p.slice(p.lastIndexOf("/") + 1);
24463
+ return basename69.startsWith("*");
24464
+ }
24465
+ function inspectCapturedLesson(graph, lessonId, knownPaths) {
24466
+ const lesson = graph.lessons[lessonId];
24467
+ if (lesson === void 0) return [];
24468
+ const warnings = [];
24469
+ if (lesson.triggers.length > MAX_RECOMMENDED_TRIGGERS) {
24470
+ warnings.push({
24471
+ code: "OVERSIZED_LESSON_TRIGGERS",
24472
+ message: `Lesson "${lessonId}" has ${lesson.triggers.length} triggers (recommended \u2264 ${MAX_RECOMMENDED_TRIGGERS}); broad trigger sets fire on too many edits and dilute recall \u2014 prefer a few specific triggers.`
24473
+ });
24474
+ }
24475
+ const triggers = lesson.triggers.map((id) => graph.triggers[id]).filter((t) => t !== void 0);
24476
+ const broad = triggers.filter((t) => t.kind === "file_glob" && isBroadGlob(t.pattern)).map((t) => t.pattern);
24477
+ if (broad.length > 0) {
24478
+ warnings.push({
24479
+ code: "BROAD_GLOB_TRIGGER",
24480
+ message: `Lesson "${lessonId}" has broad file glob(s) (${broad.join(", ")}) that match large swaths of the tree; prefer a path specific to the lesson.`
24481
+ });
24482
+ }
24483
+ if (triggers.length > 0 && triggers.every((t) => t.kind === "keyword")) {
24484
+ warnings.push({
24485
+ code: "KEYWORD_ONLY_LESSON",
24486
+ message: `Lesson "${lessonId}" has only keyword triggers; mandatory --file/--cmd recall surfaces these only when the keyword appears as a path/command token, so it fires less reliably \u2014 add a file_glob or command_pattern trigger for precise recall.`
24487
+ });
24488
+ }
24489
+ const lowSignal = triggers.filter((t) => t.kind === "keyword" && isLowSignalKeyword(t.pattern)).map((t) => t.pattern);
24490
+ if (lowSignal.length > 0) {
24491
+ warnings.push({
24492
+ code: "LOW_SIGNAL_KEYWORD",
24493
+ message: `Lesson "${lessonId}" has long keyword trigger(s) (${lowSignal.join(", ")}); recall matches a keyword only as a substring of --keyword or a contiguous token-run in the file/command, so a pattern past ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens rarely fires \u2014 use a short distinctive phrase.`
24494
+ });
24495
+ }
24496
+ const stopworded = triggers.filter((t) => t.kind === "keyword" && keywordNeedleLosesTokens(t.pattern)).map((t) => t.pattern);
24497
+ if (stopworded.length > 0) {
24498
+ warnings.push({
24499
+ code: "STOPWORD_KEYWORD",
24500
+ message: `Lesson "${lessonId}" has keyword trigger(s) containing stopwords/short words (${stopworded.join(", ")}); recall filters them from the pattern but NOT from the file/command text, so the phrase can never match contiguously on the --file/--cmd path \u2014 drop the stopwords (e.g. "state art" instead of "state of the art").`
24501
+ });
24502
+ }
24503
+ if (knownPaths !== void 0) {
24504
+ const dead = deadFileGlobIds(graph, knownPaths);
24505
+ const deadHere = lesson.triggers.filter((id) => dead.has(id)).map((id) => graph.triggers[id]?.pattern).filter((p) => p !== void 0);
24506
+ if (deadHere.length > 0) {
24507
+ warnings.push({
24508
+ code: "DEAD_GLOB",
24509
+ message: `Lesson "${lessonId}" has file_glob trigger(s) (${deadHere.join(", ")}) that match no file in the working tree \u2014 likely a rename. Re-point them at the current path, or the lesson is unreachable via those globs.`
24510
+ });
24511
+ }
24512
+ }
24513
+ return warnings;
24514
+ }
24515
+ function nearDuplicateWarning(graph, lessonId) {
24516
+ const subject = graph.lessons[lessonId];
24517
+ if (subject === void 0) return null;
24518
+ const subjectTokens = new Set(tokenize(subject.rule));
24519
+ if (subjectTokens.size === 0) return null;
24520
+ let best = null;
24521
+ for (const [id, other] of Object.entries(graph.lessons)) {
24522
+ if (id === lessonId || other.status !== "active") continue;
24523
+ const otherTokens = new Set(tokenize(other.rule));
24524
+ if (otherTokens.size === 0) continue;
24525
+ const score = jaccard(subjectTokens, otherTokens);
24526
+ if (score >= NEAR_DUPLICATE_THRESHOLD && (best === null || score > best.score)) {
24527
+ best = { id, score };
24528
+ }
24529
+ }
24530
+ if (best === null) return null;
24531
+ return {
24532
+ code: "NEAR_DUPLICATE_LESSON",
24533
+ message: `Lesson "${lessonId}" closely resembles active lesson "${best.id}" (~${Math.round(best.score * 100)}% token overlap); consider updating "${best.id}" instead of adding a paraphrase (recall would surface both).`
24534
+ };
24535
+ }
24536
+ function jaccard(a, b) {
24537
+ let intersection = 0;
24538
+ for (const t of a) if (b.has(t)) intersection += 1;
24539
+ return intersection / (a.size + b.size - intersection);
24540
+ }
24541
+ var LegacyTriggersSchema = z.object({
24542
+ file_globs: z.array(z.string()),
24543
+ command_patterns: z.array(z.string()),
24544
+ keywords: z.array(z.string())
24545
+ }).refine((t) => t.file_globs.length + t.command_patterns.length + t.keywords.length > 0, {
24546
+ message: "cluster must declare at least one trigger of any type"
24547
+ });
24548
+ var LegacyClusterSchema = z.object({
24549
+ topic: z.string().regex(/^[a-z0-9-]+$/),
24550
+ file: z.string().regex(/\.md$/),
24551
+ summary: z.string().min(1),
24552
+ triggers: LegacyTriggersSchema
24553
+ });
24554
+ var LegacyIndexSchema = z.object({
24555
+ version: z.literal(1),
24556
+ clusters: z.array(LegacyClusterSchema)
24557
+ });
24558
+ function collectClusterTriggerIds(cluster, triggersById, triggerIdByKey) {
24559
+ const specs = [
24560
+ ...cluster.triggers.file_globs.map((p) => ({ kind: "file_glob", pattern: p })),
24561
+ ...cluster.triggers.command_patterns.map(
24562
+ (p) => ({ kind: "command_pattern", pattern: p })
24563
+ ),
24564
+ ...cluster.triggers.keywords.map((p) => ({ kind: "keyword", pattern: p }))
24565
+ ];
24566
+ const ids = [];
24567
+ for (const spec of specs) {
24568
+ const key = `${spec.kind}|${spec.pattern}`;
24569
+ let id = triggerIdByKey.get(key);
24570
+ if (id === void 0) {
24571
+ id = makeTriggerId2(spec);
24572
+ triggerIdByKey.set(key, id);
24573
+ triggersById.set(id, { kind: spec.kind, pattern: spec.pattern });
24574
+ }
24575
+ if (!ids.includes(id)) ids.push(id);
24576
+ }
24577
+ return ids;
24578
+ }
24579
+ var TRIGGER_PREFIX2 = {
24580
+ file_glob: "glob",
24581
+ command_pattern: "cmd",
24582
+ keyword: "kw"
24583
+ };
24584
+ function makeTriggerId2(spec) {
24585
+ const hash = createHash("sha1").update(`${spec.kind}|${spec.pattern}`).digest("hex").slice(0, 8);
24586
+ return `t-${TRIGGER_PREFIX2[spec.kind]}-${hash}`;
24587
+ }
24588
+ var RULE_HEADING_RE = /^##\s+Rules\b.*$/i;
24589
+ var NEXT_HEADING_RE = /^##\s+/;
24590
+ var RULE_LINE_RE = /^(\d+)\.\s+(.+?)\s*$/;
24591
+ var EVIDENCE_TAIL_RE = /\s*\(Evidence:?\s+([^)]+)\)\s*$/;
24592
+ var EVIDENCE_REF_RE = /L\d+/g;
24593
+ function parseRulesSection(markdown) {
24594
+ const lines = markdown.split(/\r?\n/);
24595
+ let inRules = false;
24596
+ const rules = [];
24597
+ for (const line of lines) {
24598
+ if (!inRules) {
24599
+ if (RULE_HEADING_RE.test(line)) inRules = true;
22678
24600
  continue;
22679
24601
  }
24602
+ if (NEXT_HEADING_RE.test(line)) break;
24603
+ const m = RULE_LINE_RE.exec(line);
24604
+ if (m === null) continue;
24605
+ const ruleIndex = Number(m[1]);
24606
+ let body = m[2];
24607
+ const evidence = [];
24608
+ let tail = EVIDENCE_TAIL_RE.exec(body);
24609
+ while (tail !== null) {
24610
+ const refs = tail[1];
24611
+ const matches = refs.match(EVIDENCE_REF_RE);
24612
+ if (matches !== null) evidence.unshift(...matches);
24613
+ body = body.slice(0, tail.index).trimEnd();
24614
+ tail = EVIDENCE_TAIL_RE.exec(body);
24615
+ }
24616
+ rules.push({ index: ruleIndex, body, evidence });
22680
24617
  }
22681
- return { diffs, summary };
24618
+ return rules;
22682
24619
  }
22683
- function formatDiffSummary(summary) {
22684
- return `${summary.new} files would be created, ${summary.updated} updated, ${summary.unchanged} unchanged, ${summary.deleted} deleted`;
24620
+ var LEGACY_ARTIFACT_REL = [
24621
+ "index.yaml",
24622
+ "journal.md",
24623
+ "journal.legacy.md",
24624
+ "topics",
24625
+ "distill-ledger.yaml",
24626
+ "distill-proposal.md"
24627
+ ];
24628
+ function deleteLegacyArtifacts(baseDir) {
24629
+ const deleted = [];
24630
+ for (const rel2 of LEGACY_ARTIFACT_REL) {
24631
+ const abs = join(baseDir, rel2);
24632
+ if (!existsSync(abs)) continue;
24633
+ rmSync(abs, { recursive: true, force: true });
24634
+ deleted.push(abs);
24635
+ }
24636
+ return deleted;
24637
+ }
24638
+
24639
+ // src/lessons/import-legacy-merge.ts
24640
+ async function mergeLegacy(projectRoot, paths, specs, summaryByTopic, options) {
24641
+ let addedLessons = 0;
24642
+ const addedTriggers = /* @__PURE__ */ new Set();
24643
+ const touchedTopics = /* @__PURE__ */ new Set();
24644
+ await mutateLessonsGraphLocked(projectRoot, (g) => {
24645
+ addedLessons = 0;
24646
+ addedTriggers.clear();
24647
+ touchedTopics.clear();
24648
+ for (const spec of specs) {
24649
+ const result = addLessonInto(g, spec, {
24650
+ allowNewTopic: true,
24651
+ topicSummary: summaryByTopic.get(spec.topic),
24652
+ // Legacy lessons may predate the ≥1-trigger requirement; recover them
24653
+ // as-is rather than dropping historical knowledge.
24654
+ allowNoTrigger: true
24655
+ });
24656
+ if (result.isNewLesson) addedLessons += 1;
24657
+ for (const t of result.newTriggerIds) addedTriggers.add(t);
24658
+ touchedTopics.add(spec.topic);
24659
+ }
24660
+ });
24661
+ const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
24662
+ return {
24663
+ wroteGraphPath: paths.graph,
24664
+ deletedPaths,
24665
+ topicCount: touchedTopics.size,
24666
+ lessonCount: addedLessons,
24667
+ triggerCount: addedTriggers.size
24668
+ };
22685
24669
  }
22686
24670
 
22687
- // src/config/core/lock.ts
22688
- init_fs();
24671
+ // src/lessons/import-legacy.ts
24672
+ var LessonsGraphExistsError = class extends Error {
24673
+ code = "LESSONS_GRAPH_EXISTS";
24674
+ constructor() {
24675
+ super("importLegacyLessons: a non-empty lessons.json already exists; pass force to overwrite.");
24676
+ this.name = "LessonsGraphExistsError";
24677
+ }
24678
+ };
24679
+ async function importLegacyLessons(projectRoot, options) {
24680
+ const paths = lessonsPaths(projectRoot);
24681
+ const indexRaw = readFileSync(paths.index, "utf8");
24682
+ const index = LegacyIndexSchema.parse(parse(indexRaw));
24683
+ const topics = {};
24684
+ const triggersById = /* @__PURE__ */ new Map();
24685
+ const triggerIdByKey = /* @__PURE__ */ new Map();
24686
+ const lessons = {};
24687
+ const specs = [];
24688
+ const summaryByTopic = /* @__PURE__ */ new Map();
24689
+ for (const cluster of index.clusters) {
24690
+ topics[cluster.topic] = { summary: cluster.summary };
24691
+ summaryByTopic.set(cluster.topic, cluster.summary);
24692
+ const clusterTriggerIds = collectClusterTriggerIds(cluster, triggersById, triggerIdByKey);
24693
+ const topicFile = join(projectRoot, cluster.file);
24694
+ if (!existsSync(topicFile)) {
24695
+ throw new Error(
24696
+ `importLegacyLessons: declared topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
24697
+ );
24698
+ }
24699
+ const topicMarkdown = readFileSync(topicFile, "utf8");
24700
+ for (const { index: ruleIndex, body, evidence } of parseRulesSection(topicMarkdown)) {
24701
+ const lessonEvidence = [
24702
+ `legacy:${cluster.file}#rule-${ruleIndex}`,
24703
+ ...evidence.map((e) => `legacy:${e}`)
24704
+ ];
24705
+ lessons[`${cluster.topic}-rule-${ruleIndex}`] = {
24706
+ rule: body,
24707
+ topics: [cluster.topic],
24708
+ triggers: clusterTriggerIds,
24709
+ evidence: lessonEvidence,
24710
+ status: "active",
24711
+ createdAt: options.migratedAt
24712
+ };
24713
+ specs.push({
24714
+ rule: body,
24715
+ topic: cluster.topic,
24716
+ triggers: {
24717
+ files: cluster.triggers.file_globs,
24718
+ commands: cluster.triggers.command_patterns,
24719
+ keywords: cluster.triggers.keywords
24720
+ },
24721
+ evidence: lessonEvidence,
24722
+ createdAt: options.migratedAt
24723
+ });
24724
+ }
24725
+ }
24726
+ if (options.merge === true)
24727
+ return mergeLegacy(projectRoot, paths, specs, summaryByTopic, options);
24728
+ const triggers = Object.fromEntries(triggersById.entries());
24729
+ await mutateLessonsGraphLocked(projectRoot, (g) => {
24730
+ const populated = Object.keys(g.lessons).length > 0 || Object.keys(g.topics).length > 0 || Object.keys(g.triggers).length > 0;
24731
+ if (options.force !== true && populated) {
24732
+ throw new LessonsGraphExistsError();
24733
+ }
24734
+ g.version = 1;
24735
+ g.lessons = lessons;
24736
+ g.topics = topics;
24737
+ g.triggers = triggers;
24738
+ });
24739
+ const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
24740
+ return {
24741
+ wroteGraphPath: paths.graph,
24742
+ deletedPaths,
24743
+ topicCount: Object.keys(topics).length,
24744
+ lessonCount: Object.keys(lessons).length,
24745
+ triggerCount: triggersById.size
24746
+ };
24747
+ }
22689
24748
 
22690
- // src/utils/crypto/hash.ts
22691
- init_fs_text_encoding();
22692
- function hashContent(content) {
22693
- return createHash("sha256").update(content, "utf8").digest("hex");
24749
+ // src/lessons/auto-migrate.ts
24750
+ function todayIso2() {
24751
+ const now = /* @__PURE__ */ new Date();
24752
+ const y = now.getUTCFullYear();
24753
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
24754
+ const d = String(now.getUTCDate()).padStart(2, "0");
24755
+ return `${y}-${m}-${d}`;
22694
24756
  }
22695
- async function hashFile(path) {
24757
+ async function maybeAutoMigrateLessons(projectRoot) {
24758
+ if (existsSync(graphFilePath(projectRoot))) return false;
24759
+ const paths = lessonsPaths(projectRoot);
24760
+ if (!existsSync(paths.index)) return false;
22696
24761
  try {
22697
- const bytes = await readFile(path);
22698
- return createHash("sha256").update(bytes).digest("hex");
24762
+ await importLegacyLessons(projectRoot, { migratedAt: todayIso2() });
24763
+ return true;
22699
24764
  } catch (err) {
22700
- if (err instanceof Error && "code" in err && err.code === "ENOENT") {
22701
- return null;
22702
- }
24765
+ if (err instanceof LessonsGraphExistsError) return false;
22703
24766
  throw err;
22704
24767
  }
22705
24768
  }
22706
24769
 
22707
- // src/config/core/lock.ts
22708
- var LOCK_FILENAME = ".lock";
22709
- var CANONICAL_PATTERNS = [
22710
- (r) => r.startsWith("rules/") && r.endsWith(".md"),
22711
- (r) => r.startsWith("commands/") && r.endsWith(".md"),
22712
- (r) => r.startsWith("agents/") && r.endsWith(".md"),
22713
- (r) => r.match(/^skills\/[^/]+\/.+$/) !== null,
22714
- (r) => r === "mcp.json",
22715
- (r) => r === "permissions.yaml",
22716
- (r) => r === "hooks.yaml",
22717
- (r) => r === "ignore"
22718
- ];
22719
- function isCanonical(relPath) {
22720
- if (relPath.startsWith("packs/")) return false;
22721
- return CANONICAL_PATTERNS.some((p) => p(relPath));
24770
+ // src/utils/filesystem/process-lock.ts
24771
+ init_errors();
24772
+ var DEFAULT_STALE_MS = 6e4;
24773
+ var DEFAULT_RETRIES = 30;
24774
+ var DEFAULT_RETRY_DELAY_MS = 200;
24775
+ var YOUNG_LOCK_GRACE_MS = 2e3;
24776
+ async function acquireProcessLock(lockPath, opts = {}) {
24777
+ const retries = opts.retries ?? DEFAULT_RETRIES;
24778
+ const delay = opts.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
24779
+ const stale = opts.staleMs ?? DEFAULT_STALE_MS;
24780
+ await mkdir(dirname(lockPath), { recursive: true });
24781
+ let attempt = 0;
24782
+ while (true) {
24783
+ const acquired = await tryAcquire(lockPath);
24784
+ if (acquired) return acquired;
24785
+ const existing = await inspectLock(lockPath);
24786
+ if (existing !== "young" && isStale(existing, stale)) {
24787
+ await rm(lockPath, { recursive: true, force: true }).catch(() => {
24788
+ });
24789
+ continue;
24790
+ }
24791
+ if (attempt >= retries) {
24792
+ const holder = existing === "young" ? null : existing;
24793
+ throw new LockAcquisitionError(lockPath, describeHolder(holder), { label: opts.label });
24794
+ }
24795
+ attempt++;
24796
+ await sleep2(delay);
24797
+ }
22722
24798
  }
22723
- var FEATURE_PATTERNS = {
22724
- rules: (path) => path.startsWith("rules/"),
22725
- commands: (path) => path.startsWith("commands/"),
22726
- agents: (path) => path.startsWith("agents/"),
22727
- skills: (path) => /^skills\/[^/]+\/.+$/.test(path),
22728
- mcp: (path) => path === "mcp.json",
22729
- permissions: (path) => path === "permissions.yaml",
22730
- hooks: (path) => path === "hooks.yaml",
22731
- ignore: (path) => path === "ignore"
22732
- };
22733
- async function readLock(abDir) {
22734
- const lockPath = join(abDir, LOCK_FILENAME);
22735
- const content = await readFileSafe(lockPath);
22736
- if (content === null) return null;
24799
+ async function tryAcquire(lockPath) {
22737
24800
  try {
22738
- const raw = parse(content);
22739
- if (!raw || typeof raw !== "object") return null;
22740
- return {
22741
- generatedAt: String(raw.generated_at ?? ""),
22742
- generatedBy: String(raw.generated_by ?? ""),
22743
- libVersion: String(raw.lib_version ?? ""),
22744
- checksums: raw.checksums && typeof raw.checksums === "object" ? raw.checksums : {},
22745
- extends: raw.extends && typeof raw.extends === "object" ? raw.extends : {},
22746
- packs: raw.packs && typeof raw.packs === "object" ? raw.packs : {}
22747
- };
24801
+ await mkdir(lockPath, { recursive: false });
24802
+ } catch (err) {
24803
+ if (err.code === "EEXIST") return null;
24804
+ throw err;
24805
+ }
24806
+ const metadataPath = join(lockPath, "holder.json");
24807
+ const metadata = {
24808
+ pid: process.pid,
24809
+ started: Date.now(),
24810
+ hostname: getHostname()
24811
+ };
24812
+ await writeFile(metadataPath, JSON.stringify(metadata), "utf-8");
24813
+ let released = false;
24814
+ const cleanup = () => {
24815
+ if (released) return;
24816
+ released = true;
24817
+ try {
24818
+ rmSync(lockPath, { recursive: true, force: true });
24819
+ } catch {
24820
+ }
24821
+ };
24822
+ const signalHandler = (signal) => {
24823
+ cleanup();
24824
+ process.kill(process.pid, signal);
24825
+ };
24826
+ process.once("SIGINT", signalHandler);
24827
+ process.once("SIGTERM", signalHandler);
24828
+ process.once("exit", cleanup);
24829
+ return async () => {
24830
+ if (released) return;
24831
+ released = true;
24832
+ process.off("SIGINT", signalHandler);
24833
+ process.off("SIGTERM", signalHandler);
24834
+ process.off("exit", cleanup);
24835
+ await rm(lockPath, { recursive: true, force: true }).catch(() => {
24836
+ });
24837
+ };
24838
+ }
24839
+ async function inspectLock(lockPath) {
24840
+ try {
24841
+ const raw = await readFile(join(lockPath, "holder.json"), "utf-8");
24842
+ const parsed = JSON.parse(raw);
24843
+ if (!isLockMetadata(parsed)) return null;
24844
+ return parsed;
22748
24845
  } catch {
24846
+ try {
24847
+ const info = await stat(lockPath);
24848
+ const ageMs = Date.now() - info.mtimeMs;
24849
+ if (ageMs < YOUNG_LOCK_GRACE_MS) return "young";
24850
+ } catch {
24851
+ }
22749
24852
  return null;
22750
24853
  }
22751
24854
  }
22752
- async function buildChecksums(abDir) {
22753
- if (!await exists(abDir)) return {};
22754
- const files = await readDirRecursive(abDir);
22755
- const result = {};
22756
- for (const fullPath of files) {
22757
- const rel2 = relative(abDir, fullPath).replace(/\\/g, "/");
22758
- if (rel2 === LOCK_FILENAME) continue;
22759
- if (!isCanonical(rel2)) continue;
22760
- const h = await hashFile(fullPath);
22761
- if (h !== null) {
22762
- result[rel2] = h.startsWith("sha256:") ? h : `sha256:${h}`;
22763
- }
24855
+ function isStale(meta, staleMs) {
24856
+ if (!meta) return true;
24857
+ const age = Date.now() - meta.started;
24858
+ if (age > staleMs) return true;
24859
+ if (meta.hostname && meta.hostname !== getHostname()) return false;
24860
+ return !isProcessAlive(meta.pid);
24861
+ }
24862
+ function isProcessAlive(pid) {
24863
+ if (!Number.isInteger(pid) || pid <= 0) return false;
24864
+ try {
24865
+ process.kill(pid, 0);
24866
+ return true;
24867
+ } catch (err) {
24868
+ return err.code === "EPERM";
22764
24869
  }
22765
- return result;
22766
24870
  }
22767
- function detectLockedFeatureViolations(lockChecksums, currentChecksums, lockFeatures) {
22768
- if (lockFeatures.length === 0) return [];
22769
- const matchers = lockFeatures.map((feature) => FEATURE_PATTERNS[feature]).filter((matcher) => matcher !== void 0);
22770
- if (matchers.length === 0) return [];
22771
- const allPaths = /* @__PURE__ */ new Set([...Object.keys(lockChecksums), ...Object.keys(currentChecksums)]);
22772
- const violations = [];
22773
- for (const path of allPaths) {
22774
- if (!matchers.some((matcher) => matcher(path))) continue;
22775
- if (lockChecksums[path] !== currentChecksums[path]) violations.push(path);
24871
+ function describeHolder(meta) {
24872
+ if (!meta) return "unknown (unreadable lock metadata)";
24873
+ const host = meta.hostname ? `${meta.hostname}:` : "";
24874
+ return `${host}pid ${meta.pid} (running ${Date.now() - meta.started}ms)`;
24875
+ }
24876
+ function isLockMetadata(value) {
24877
+ if (typeof value !== "object" || value === null) return false;
24878
+ const v = value;
24879
+ return typeof v.pid === "number" && typeof v.started === "number";
24880
+ }
24881
+ function getHostname() {
24882
+ return hostname();
24883
+ }
24884
+ function sleep2(ms) {
24885
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
24886
+ }
24887
+
24888
+ // src/lessons/lessons-lock.ts
24889
+ var LESSONS_LOCK_FILENAME = ".lessons.lock";
24890
+ function lessonsLockPath(projectRoot) {
24891
+ return resolve(projectRoot, ".agentsmesh/lessons", LESSONS_LOCK_FILENAME);
24892
+ }
24893
+ async function acquireLessonsLock(projectRoot, opts = {}) {
24894
+ const lockPath = lessonsLockPath(projectRoot);
24895
+ await mkdir(dirname(lockPath), { recursive: true });
24896
+ return acquireProcessLock(lockPath, { ...opts, label: "lessons lock" });
24897
+ }
24898
+
24899
+ // src/lessons/mutate.ts
24900
+ function emptyGraph() {
24901
+ return { version: 1, lessons: {}, topics: {}, triggers: {} };
24902
+ }
24903
+ async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
24904
+ const release = await acquireLessonsLock(projectRoot, { retries: options.retries });
24905
+ try {
24906
+ const graph = tryLoadLessonsGraph(projectRoot) ?? emptyGraph();
24907
+ const result = await mutator(graph);
24908
+ const report = validateLessonsGraph(graph);
24909
+ if (!report.ok) {
24910
+ const errors = report.findings.filter((f) => f.level === "error").map((f) => `${f.code}: ${f.message}`).join("; ");
24911
+ throw new Error(`mutateLessonsGraph: refusing to write an invalid graph \u2014 ${errors}`);
24912
+ }
24913
+ saveLessonsGraph(projectRoot, graph);
24914
+ return result;
24915
+ } finally {
24916
+ await release();
22776
24917
  }
22777
- return violations;
22778
24918
  }
22779
- async function buildExtendChecksums(resolvedExtends) {
22780
- const result = {};
22781
- for (const ext of resolvedExtends) {
22782
- if (ext.version !== void 0) {
22783
- result[ext.name] = ext.version;
22784
- continue;
24919
+ async function mutateLessonsGraph(projectRoot, mutator, options = {}) {
24920
+ await maybeAutoMigrateLessons(projectRoot);
24921
+ return mutateLessonsGraphLocked(projectRoot, mutator, options);
24922
+ }
24923
+
24924
+ // src/lessons/trigger-effectiveness.ts
24925
+ function ineffectiveTriggers(graph, triggerIds) {
24926
+ const out2 = [];
24927
+ for (const id of triggerIds) {
24928
+ const trigger = graph.triggers[id];
24929
+ if (trigger === void 0) continue;
24930
+ const reason = ineffectiveReason(trigger.kind, trigger.pattern);
24931
+ if (reason !== null) out2.push({ id, kind: trigger.kind, pattern: trigger.pattern, reason });
24932
+ }
24933
+ return out2;
24934
+ }
24935
+ function ineffectiveReason(kind, pattern) {
24936
+ if (kind === "keyword") {
24937
+ if (tokenize(pattern).length === 0) {
24938
+ return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
24939
+ }
24940
+ if (keywordNeedleLosesTokens(pattern)) {
24941
+ return "keyword contains stopwords/short words, so its needle can never appear as a contiguous run on the mandatory --file/--cmd recall path";
24942
+ }
24943
+ return null;
24944
+ }
24945
+ if (kind === "command_pattern") {
24946
+ let valid = true;
24947
+ try {
24948
+ new RegExp(pattern);
24949
+ } catch {
24950
+ valid = false;
24951
+ }
24952
+ if (!valid) {
24953
+ return "invalid regex \u2014 recall compiles it with new RegExp and swallows the throw as a non-match, so it never fires";
24954
+ }
24955
+ if (!isSafeRegexPattern(pattern)) {
24956
+ return "regex is outside the provably-linear engine \u2014 recall skips it (ReDoS guard), so it never fires";
22785
24957
  }
22786
- const abDir = join(ext.resolvedPath, ".agentsmesh");
22787
- const checksums = await buildChecksums(abDir);
22788
- const fingerprint = Object.keys(checksums).sort().map((p) => `${p}:${checksums[p]}`).join("\n");
22789
- const h = hashContent(fingerprint);
22790
- const hex = h.startsWith("sha256:") ? h : `sha256:${h}`;
22791
- result[ext.name] = `local:${hex}`;
24958
+ return null;
22792
24959
  }
22793
- return result;
24960
+ return null;
24961
+ }
24962
+ function blockingDeadTriggers(graph, triggerIds) {
24963
+ return ineffectiveTriggers(graph, triggerIds).filter((t) => t.kind !== "command_pattern");
22794
24964
  }
22795
24965
 
22796
- // src/core/check/lock-sync.ts
22797
- async function checkLockSync(opts) {
22798
- const { config, configDir, canonicalDir } = opts;
22799
- const lock = await readLock(canonicalDir);
22800
- if (lock === null) {
24966
+ // src/lessons/add.ts
24967
+ function countInputTriggers(triggers) {
24968
+ return (triggers.files?.length ?? 0) + (triggers.commands?.length ?? 0) + (triggers.keywords?.length ?? 0);
24969
+ }
24970
+ async function addLesson(projectRoot, input, options = {}) {
24971
+ return mutateLessonsGraph(projectRoot, (graph) => addLessonInto(graph, input, options), {
24972
+ retries: options.retries
24973
+ });
24974
+ }
24975
+ function addLessonInto(graph, input, options) {
24976
+ const ruleKey = normalizeRule2(input.rule);
24977
+ const trimmedRule = input.rule.trim();
24978
+ if (trimmedRule.length > MAX_RULE_LENGTH) {
24979
+ throw new RuleTooLongError(trimmedRule.length, MAX_RULE_LENGTH);
24980
+ }
24981
+ const existingId = findExistingLessonByRule(graph, ruleKey);
24982
+ const isNewTopic = graph.topics[input.topic] === void 0;
24983
+ if (isNewTopic) {
24984
+ if (options.allowNewTopic !== true) throw new UnknownTopicError(input.topic);
24985
+ if (options.topicSummary === void 0 || options.topicSummary.length === 0) {
24986
+ throw new Error(`addLesson: new topic "${input.topic}" requires topicSummary.`);
24987
+ }
24988
+ graph.topics[input.topic] = { summary: options.topicSummary };
24989
+ }
24990
+ if (options.allowNoTrigger !== true) {
24991
+ const existingTriggers = existingId !== null ? graph.lessons[existingId]?.triggers.length ?? 0 : 0;
24992
+ if (countInputTriggers(input.triggers) === 0 && existingTriggers === 0) {
24993
+ throw new NoTriggerError();
24994
+ }
24995
+ }
24996
+ const { triggerIds, newTriggerIds } = mergeTriggers(graph, input.triggers);
24997
+ if (options.allowNoTrigger !== true) {
24998
+ const resultingTriggers = existingId !== null ? union(graph.lessons[existingId].triggers, triggerIds) : triggerIds;
24999
+ const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
25000
+ if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
25001
+ throw new UnrecallableLessonError(blockingDead);
25002
+ }
25003
+ }
25004
+ if (existingId !== null) {
25005
+ const existing = graph.lessons[existingId];
25006
+ graph.lessons[existingId] = {
25007
+ ...existing,
25008
+ topics: union(existing.topics, [input.topic]),
25009
+ triggers: union(existing.triggers, triggerIds),
25010
+ evidence: union(existing.evidence, input.evidence ?? []),
25011
+ ...existing.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {}
25012
+ };
22801
25013
  return {
22802
- inSync: false,
22803
- hasLock: false,
22804
- modified: [],
22805
- added: [],
22806
- removed: [],
22807
- extendsModified: [],
22808
- lockedViolations: []
25014
+ id: existingId,
25015
+ isNewLesson: false,
25016
+ isNewTopic,
25017
+ newTriggerIds,
25018
+ // Near-duplicate detection is meaningless on an upsert (the lesson IS the
25019
+ // match), so only DEAD_GLOB/hygiene warnings apply here.
25020
+ warnings: inspectCapturedLesson(graph, existingId, options.knownPaths)
22809
25021
  };
22810
25022
  }
22811
- const current = await buildChecksums(canonicalDir);
22812
- const resolvedExtends = await resolveExtendPaths(config, configDir);
22813
- const currentExtends = resolvedExtends.length > 0 ? await buildExtendChecksums(resolvedExtends) : {};
22814
- const lockPaths = new Set(Object.keys(lock.checksums));
22815
- const currentPaths = new Set(Object.keys(current));
22816
- const modified = [];
22817
- const added = [];
22818
- const removed = [];
22819
- for (const path of lockPaths) {
22820
- const c2 = current[path];
22821
- if (c2 === void 0) {
22822
- removed.push(path);
22823
- } else if (c2 !== lock.checksums[path]) {
22824
- modified.push(path);
22825
- }
25023
+ const id = makeLessonId(graph, input.topic, ruleKey);
25024
+ graph.lessons[id] = {
25025
+ rule: trimmedRule,
25026
+ topics: [input.topic],
25027
+ triggers: triggerIds,
25028
+ evidence: input.evidence === void 0 ? [] : [...input.evidence],
25029
+ status: "active",
25030
+ createdAt: input.createdAt ?? todayIso(),
25031
+ ...input.rationale === void 0 ? {} : { rationale: input.rationale }
25032
+ };
25033
+ const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
25034
+ const nearDup = nearDuplicateWarning(graph, id);
25035
+ return {
25036
+ id,
25037
+ isNewLesson: true,
25038
+ isNewTopic,
25039
+ newTriggerIds,
25040
+ warnings: nearDup === null ? warnings : [...warnings, nearDup]
25041
+ };
25042
+ }
25043
+ function findExistingLessonByRule(graph, ruleKey) {
25044
+ for (const [id, lesson] of Object.entries(graph.lessons)) {
25045
+ if (lesson.status !== "active") continue;
25046
+ if (normalizeRule2(lesson.rule) === ruleKey) return id;
22826
25047
  }
22827
- for (const path of currentPaths) {
22828
- if (!lockPaths.has(path)) {
22829
- added.push(path);
22830
- }
25048
+ return null;
25049
+ }
25050
+
25051
+ // src/lessons/ranking-signals.ts
25052
+ function buildFanout(graph) {
25053
+ const fanout = /* @__PURE__ */ new Map();
25054
+ for (const lesson of Object.values(graph.lessons)) {
25055
+ if (lesson.status !== "active") continue;
25056
+ for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
22831
25057
  }
22832
- const extendNames = /* @__PURE__ */ new Set([...Object.keys(lock.extends), ...Object.keys(currentExtends)]);
22833
- const extendsModified = [];
22834
- for (const name of extendNames) {
22835
- if (currentExtends[name] !== lock.extends[name]) {
22836
- extendsModified.push(name);
22837
- }
25058
+ return fanout;
25059
+ }
25060
+ function buildTopicCoherence(matches) {
25061
+ const topicCount = /* @__PURE__ */ new Map();
25062
+ for (const { lesson } of matches) {
25063
+ for (const t of lesson.topics) topicCount.set(t, (topicCount.get(t) ?? 0) + 1);
22838
25064
  }
22839
- const lockedViolations = detectLockedFeatureViolations(
22840
- lock.checksums,
22841
- current,
22842
- config.collaboration?.lock_features ?? []
22843
- );
22844
- const inSync = modified.length === 0 && added.length === 0 && removed.length === 0 && extendsModified.length === 0;
22845
- return {
22846
- inSync,
22847
- hasLock: true,
22848
- modified,
22849
- added,
22850
- removed,
22851
- extendsModified,
22852
- lockedViolations
22853
- };
25065
+ const coherence = /* @__PURE__ */ new Map();
25066
+ for (const { id, lesson } of matches) {
25067
+ let best = 0;
25068
+ for (const t of lesson.topics) best = Math.max(best, topicCount.get(t));
25069
+ coherence.set(id, best);
25070
+ }
25071
+ return coherence;
25072
+ }
25073
+ function recallLogPath(projectRoot) {
25074
+ return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
22854
25075
  }
22855
25076
 
22856
- // src/public/engine.ts
22857
- init_registry();
22858
- init_target_ids();
22859
- async function importFrom(target31, opts) {
22860
- const descriptor31 = getDescriptor(target31);
22861
- if (!descriptor31) {
22862
- throw new TargetNotFoundError(target31, {
22863
- supported: [...TARGET_IDS, ...getAllDescriptors().map((d) => d.id)]
22864
- });
25077
+ // src/lessons/capture-telemetry.ts
25078
+ function captureLogPath(projectRoot) {
25079
+ return join(lessonsPaths(projectRoot).base, "capture-log.jsonl");
25080
+ }
25081
+
25082
+ // src/lessons/keyword-match.ts
25083
+ function splitTokens(text) {
25084
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
25085
+ }
25086
+ function deriveHaystackTokens(query) {
25087
+ const parts = [];
25088
+ if (query.file !== void 0) parts.push(query.file);
25089
+ if (query.command !== void 0) parts.push(query.command);
25090
+ return parts.length === 0 ? [] : splitTokens(parts.join(" "));
25091
+ }
25092
+ function containsRun(needle, hay) {
25093
+ if (needle.length === 0) return false;
25094
+ for (let i = 0; i + needle.length <= hay.length; i += 1) {
25095
+ let hit = true;
25096
+ for (let j = 0; j < needle.length; j += 1) {
25097
+ if (hay[i + j] !== needle[j]) {
25098
+ hit = false;
25099
+ break;
25100
+ }
25101
+ }
25102
+ if (hit) return true;
22865
25103
  }
22866
- return descriptor31.generators.importFrom(opts.root, { scope: opts.scope ?? "project" });
25104
+ return false;
22867
25105
  }
22868
- async function loadConfig2(projectRoot) {
22869
- return loadConfigFromDir(projectRoot);
25106
+ function keywordMatches(pattern, query) {
25107
+ if (query.keyword !== void 0 && query.keyword.toLowerCase().includes(pattern.toLowerCase())) {
25108
+ return true;
25109
+ }
25110
+ return containsRun(tokenize(pattern), deriveHaystackTokens(query));
22870
25111
  }
22871
- async function loadConfigFromDirectory(configDir) {
22872
- return loadConfigFromExactDir(configDir);
25112
+
25113
+ // src/lessons/query.ts
25114
+ var COMMAND_MATCH_BUDGET = 5e6;
25115
+ function queryLessons(graph, query) {
25116
+ if (query.file === void 0 && query.command === void 0 && query.keyword === void 0) {
25117
+ return [];
25118
+ }
25119
+ const matchedTriggerIds = collectMatchedTriggerIds(graph, query);
25120
+ const matched = [];
25121
+ for (const [id, lesson] of Object.entries(graph.lessons)) {
25122
+ if (lesson.status !== "active") continue;
25123
+ if (lesson.triggers.some((t) => matchedTriggerIds.has(t))) {
25124
+ matched.push({ id, lesson });
25125
+ }
25126
+ }
25127
+ matched.sort((a, b) => a.id < b.id ? -1 : 1);
25128
+ return matched;
22873
25129
  }
22874
- async function loadProjectContext(projectRoot, options = {}) {
22875
- const scope = options.scope ?? "project";
22876
- const { config, context } = await loadScopedConfig(projectRoot, scope);
22877
- await bootstrapPlugins(config, projectRoot);
22878
- const { canonical } = await loadCanonicalWithExtends(
22879
- config,
22880
- context.configDir,
22881
- { refreshRemoteCache: options.refreshRemoteCache === true },
22882
- context.canonicalDir
25130
+ function collectMatchedTriggersByKind(graph, query) {
25131
+ const byKind = {
25132
+ file_glob: /* @__PURE__ */ new Set(),
25133
+ command_pattern: /* @__PURE__ */ new Set(),
25134
+ keyword: /* @__PURE__ */ new Set()
25135
+ };
25136
+ const budget = { remaining: COMMAND_MATCH_BUDGET };
25137
+ for (const [id, trigger] of Object.entries(graph.triggers)) {
25138
+ if (triggerMatches(trigger, query, budget)) byKind[trigger.kind].add(id);
25139
+ }
25140
+ return byKind;
25141
+ }
25142
+ function collectMatchedTriggerIds(graph, query) {
25143
+ const { file_glob, command_pattern, keyword } = collectMatchedTriggersByKind(graph, query);
25144
+ return /* @__PURE__ */ new Set([...file_glob, ...command_pattern, ...keyword]);
25145
+ }
25146
+ function triggerMatches(trigger, query, budget) {
25147
+ switch (trigger.kind) {
25148
+ case "file_glob":
25149
+ if (query.file === void 0) return false;
25150
+ return picomatch(trigger.pattern, { dot: true })(query.file);
25151
+ case "command_pattern": {
25152
+ if (query.command === void 0) return false;
25153
+ const matcher = getCommandMatcher(trigger.pattern);
25154
+ return matcher !== null && matcher.test(query.command, budget);
25155
+ }
25156
+ case "keyword":
25157
+ return keywordMatches(trigger.pattern, query);
25158
+ }
25159
+ }
25160
+
25161
+ // src/lessons/ranking.ts
25162
+ var DEFAULT_RECALL_LIMIT = 10;
25163
+ var DEFAULT_RECALL_MAX_TOKENS = 400;
25164
+ var RRF_K = 60;
25165
+ var SPECIFICITY_WEIGHT = 3;
25166
+ var TOPIC_COHERENCE_WEIGHT = 2;
25167
+ var BM25_WEIGHT = 1;
25168
+ function rankMap(items) {
25169
+ const sorted = [...items].sort(
25170
+ (a, b) => b.value !== a.value ? b.value - a.value : a.id < b.id ? -1 : 1
22883
25171
  );
25172
+ const ranks = /* @__PURE__ */ new Map();
25173
+ let prevValue = null;
25174
+ let prevRank = 0;
25175
+ sorted.forEach((item, i) => {
25176
+ const rank = prevValue !== null && item.value === prevValue ? prevRank : i + 1;
25177
+ ranks.set(item.id, rank);
25178
+ prevValue = item.value;
25179
+ prevRank = rank;
25180
+ });
25181
+ return ranks;
25182
+ }
25183
+ function estTokens(rule) {
25184
+ return Math.ceil(rule.length / 4);
25185
+ }
25186
+ function rankLessons(graph, query, matches, options = {}) {
25187
+ if (matches.length === 0) return [];
25188
+ const terms = queryTerms(query);
25189
+ const corpus = buildCorpus(graph);
25190
+ const fanout = buildFanout(graph);
25191
+ const coherence = buildTopicCoherence(matches);
25192
+ const matchedTriggerIds = collectMatchedTriggerIds(graph, query);
25193
+ const scored = matches.map(({ id, lesson }) => {
25194
+ const hitTriggers = lesson.triggers.filter((t) => matchedTriggerIds.has(t));
25195
+ let specificity = 0;
25196
+ for (const t of hitTriggers) specificity = Math.max(specificity, 1 / fanout.get(t));
25197
+ return {
25198
+ id,
25199
+ lesson,
25200
+ bm25: bm25(terms, lesson.rule, corpus),
25201
+ specificity,
25202
+ // `id` is a matched lesson, and buildTopicCoherence keys every matched id.
25203
+ topicCoherence: coherence.get(id),
25204
+ matchedTriggers: hitTriggers
25205
+ };
25206
+ });
25207
+ const bm25Ranks = rankMap(scored.map((s) => ({ id: s.id, value: s.bm25 })));
25208
+ const specRanks = rankMap(scored.map((s) => ({ id: s.id, value: s.specificity })));
25209
+ const topicRanks = rankMap(scored.map((s) => ({ id: s.id, value: s.topicCoherence })));
25210
+ const ranked = scored.map((s) => ({
25211
+ id: s.id,
25212
+ lesson: s.lesson,
25213
+ score: SPECIFICITY_WEIGHT / (RRF_K + specRanks.get(s.id)) + TOPIC_COHERENCE_WEIGHT / (RRF_K + topicRanks.get(s.id)) + BM25_WEIGHT / (RRF_K + bm25Ranks.get(s.id)),
25214
+ reason: {
25215
+ matchedTriggers: s.matchedTriggers,
25216
+ bm25: s.bm25,
25217
+ specificity: s.specificity,
25218
+ topicCoherence: s.topicCoherence
25219
+ }
25220
+ })).sort((a, b) => {
25221
+ if (b.score !== a.score) return b.score - a.score;
25222
+ const ca = a.lesson.createdAt;
25223
+ const cb = b.lesson.createdAt;
25224
+ if (ca !== cb) return ca < cb ? 1 : -1;
25225
+ return a.id < b.id ? -1 : 1;
25226
+ });
25227
+ return applyCaps(ranked, options);
25228
+ }
25229
+ function applyCaps(ranked, options) {
25230
+ let out2 = ranked;
25231
+ if (options.limit !== void 0 && options.limit >= 0) out2 = out2.slice(0, options.limit);
25232
+ if (options.maxTokens !== void 0 && out2.length > 0) {
25233
+ const budgeted = [out2[0]];
25234
+ let used = estTokens(out2[0].lesson.rule);
25235
+ for (const row of out2.slice(1)) {
25236
+ const cost = estTokens(row.lesson.rule);
25237
+ if (used + cost > options.maxTokens) break;
25238
+ used += cost;
25239
+ budgeted.push(row);
25240
+ }
25241
+ out2 = budgeted;
25242
+ }
25243
+ return out2;
25244
+ }
25245
+ function defaultLessonsConfig() {
22884
25246
  return {
22885
- config,
22886
- canonical,
22887
- projectRoot: context.rootBase,
22888
- scope,
22889
- configDir: context.configDir,
22890
- canonicalDir: context.canonicalDir
25247
+ recallLimit: DEFAULT_RECALL_LIMIT,
25248
+ recallMaxTokens: DEFAULT_RECALL_MAX_TOKENS,
25249
+ autoPrune: false
22891
25250
  };
22892
25251
  }
22893
- async function lint(opts) {
22894
- const filter = opts.targetFilter ? [...opts.targetFilter] : void 0;
22895
- return runLint(opts.config, opts.canonical, opts.projectRoot, filter, {
22896
- scope: opts.scope
25252
+
25253
+ // src/lessons/merge.ts
25254
+ async function mergeLessons(projectRoot, loserId, keeperId, options = {}) {
25255
+ return mutateLessonsGraph(projectRoot, (graph) => mergeInto(graph, loserId, keeperId), {
25256
+ retries: options.retries
22897
25257
  });
22898
25258
  }
22899
- function computeDiff2(results) {
22900
- return computeDiff([...results]);
22901
- }
22902
- async function diff(ctx) {
22903
- const results = await generate(ctx);
22904
- const computed = computeDiff(results);
22905
- return { ...computed, results };
22906
- }
22907
- async function check(opts) {
22908
- return checkLockSync(opts);
22909
- }
22910
-
22911
- // src/public/canonical.ts
22912
- init_errors();
22913
- async function loadCanonical(projectRoot, options = {}) {
22914
- if (options.includeExtends === false) {
22915
- return loadCanonicalFiles(options.canonicalDir ?? projectRoot);
25259
+ function mergeInto(graph, loserId, keeperId) {
25260
+ if (loserId === keeperId) {
25261
+ throw new Error(`mergeLessons: cannot merge lesson "${loserId}" into itself.`);
22916
25262
  }
22917
- if (options.config === void 0 && options.configDir !== void 0 || options.config !== void 0 && options.configDir === void 0) {
22918
- throw new Error("loadCanonical options require both config and configDir, or neither.");
25263
+ const loser = graph.lessons[loserId];
25264
+ if (loser === void 0) throw new Error(`mergeLessons: unknown lesson "${loserId}".`);
25265
+ const keeper = graph.lessons[keeperId];
25266
+ if (keeper === void 0) throw new Error(`mergeLessons: unknown lesson "${keeperId}".`);
25267
+ if (keeper.status !== "active") {
25268
+ throw new Error(`mergeLessons: keeper "${keeperId}" is not active (status: ${keeper.status}).`);
22919
25269
  }
22920
- try {
22921
- const loaded = options.config !== void 0 && options.configDir !== void 0 ? { config: options.config, configDir: options.configDir } : await loadConfigFromDir(projectRoot);
22922
- const { canonical } = await loadCanonicalWithExtends(
22923
- loaded.config,
22924
- loaded.configDir,
22925
- { refreshRemoteCache: options.refreshRemoteCache === true },
22926
- options.canonicalDir
25270
+ if (loser.status !== "active") {
25271
+ throw new Error(
25272
+ `mergeLessons: loser "${loserId}" is already ${loser.status}; nothing to merge.`
22927
25273
  );
22928
- return canonical;
22929
- } catch (err) {
22930
- if (err instanceof ConfigNotFoundError && options.config === void 0 && options.configDir === void 0) {
22931
- return loadCanonicalFiles(options.canonicalDir ?? projectRoot);
22932
- }
22933
- throw err;
22934
25274
  }
25275
+ graph.lessons[keeperId] = {
25276
+ ...keeper,
25277
+ triggers: union2(keeper.triggers, loser.triggers),
25278
+ topics: union2(keeper.topics, loser.topics),
25279
+ evidence: union2(keeper.evidence, loser.evidence)
25280
+ };
25281
+ graph.lessons[loserId] = { ...loser, status: "superseded", supersededBy: keeperId };
25282
+ return { loserId, keeperId };
25283
+ }
25284
+ function union2(base, extra) {
25285
+ const out2 = [...base];
25286
+ for (const item of extra) {
25287
+ if (!out2.includes(item)) out2.push(item);
25288
+ }
25289
+ return out2;
22935
25290
  }
22936
25291
 
22937
- // src/public/targets.ts
22938
- init_builtin_targets();
22939
- init_registry();
22940
- init_descriptor_import_runner();
22941
- function copyCapabilities(capabilities17) {
22942
- return Object.freeze({ ...capabilities17 });
25292
+ // src/lessons/strip-markers.ts
25293
+ var LINE_REFS = String.raw`L\d+(?:\s*,\s*L\d+)*`;
25294
+ var LINE_REF_PATTERNS = [
25295
+ new RegExp(String.raw`\s*\bSee\s+${LINE_REFS}\.?`, "g"),
25296
+ // " See L128." / " See L140, L149"
25297
+ new RegExp(String.raw`\s*\((?:${LINE_REFS})\)\.?`, "g"),
25298
+ // " (L174)" / " (L92, L163)"
25299
+ new RegExp(String.raw`\s*\[(?:${LINE_REFS})\]\.?`, "g")
25300
+ // " [L161, L208]"
25301
+ ];
25302
+ var ALSO_RELEVANT_PATTERN = /\s*\(also relevant[^)]*\)\s*/g;
25303
+ function stripLegacyMarkers(rule) {
25304
+ let out2 = rule;
25305
+ for (const pattern of LINE_REF_PATTERNS) out2 = out2.replace(pattern, "");
25306
+ out2 = out2.replace(ALSO_RELEVANT_PATTERN, " ");
25307
+ return out2.trim();
25308
+ }
25309
+ function applyStrip(graph) {
25310
+ const changedIds = [];
25311
+ for (const [id, lesson] of Object.entries(graph.lessons)) {
25312
+ const stripped = stripLegacyMarkers(lesson.rule);
25313
+ if (stripped === lesson.rule || stripped.length === 0) continue;
25314
+ changedIds.push(id);
25315
+ graph.lessons[id] = { ...lesson, rule: stripped };
25316
+ }
25317
+ return changedIds.sort();
25318
+ }
25319
+ async function stripMarkersInGraph(projectRoot, options = {}) {
25320
+ await maybeAutoMigrateLessons(projectRoot);
25321
+ const existing = tryLoadLessonsGraph(projectRoot);
25322
+ if (existing === null) return { changedIds: [], changedCount: 0 };
25323
+ if (options.dryRun === true) {
25324
+ const changedIds2 = applyStrip(existing);
25325
+ return { changedIds: changedIds2, changedCount: changedIds2.length };
25326
+ }
25327
+ let changedIds = [];
25328
+ await mutateLessonsGraph(projectRoot, (graph) => {
25329
+ changedIds = applyStrip(graph);
25330
+ });
25331
+ return { changedIds, changedCount: changedIds.length };
25332
+ }
25333
+ var RECALL_HOOK_COMMAND = "agentsmesh lessons hook";
25334
+ var RECALL_HOOK_MATCHER = "Edit|Write|Bash";
25335
+ function injectRecallHook(projectRoot) {
25336
+ const path = join(projectRoot, ".agentsmesh", "hooks.yaml");
25337
+ if (!existsSync(path)) return false;
25338
+ const doc = parseDocument(readFileSync(path, "utf8"));
25339
+ const existing = doc.get("PostToolUse");
25340
+ const post = existing instanceof YAMLSeq ? existing : new YAMLSeq();
25341
+ const present = post.items.some(
25342
+ (item) => item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND
25343
+ );
25344
+ if (present) return false;
25345
+ post.add(
25346
+ doc.createNode({ matcher: RECALL_HOOK_MATCHER, type: "command", command: RECALL_HOOK_COMMAND })
25347
+ );
25348
+ doc.set("PostToolUse", post);
25349
+ writeFileSync(path, String(doc), "utf8");
25350
+ return true;
22943
25351
  }
22944
- function copyGenerators(generators) {
22945
- return Object.freeze({ ...generators });
25352
+
25353
+ // src/utils/filesystem/gitignore.ts
25354
+ init_fs();
25355
+ async function ensureGitignoreEntries(projectRoot, entries) {
25356
+ const gitignorePath = join(projectRoot, ".gitignore");
25357
+ const current = await readFileSafe(gitignorePath) ?? "";
25358
+ const existing = new Set(
25359
+ current.split("\n").map((s) => s.trim()).filter((s) => s.length > 0 && !s.startsWith("#"))
25360
+ );
25361
+ const toAdd = entries.filter((e) => !isCoveredByExisting(e, existing));
25362
+ if (toAdd.length === 0) return false;
25363
+ const suffix = current.endsWith("\n") || current === "" ? "" : "\n";
25364
+ await writeFileAtomic(gitignorePath, current + suffix + toAdd.join("\n") + "\n");
25365
+ return true;
22946
25366
  }
22947
- function copyOutputFamily(family) {
22948
- return Object.freeze({
22949
- ...family,
22950
- explicitPaths: family.explicitPaths === void 0 ? void 0 : Object.freeze([...family.explicitPaths])
22951
- });
25367
+ function isCoveredByExisting(candidate, existing) {
25368
+ if (existing.has(candidate)) return true;
25369
+ let parent = candidate.replace(/\/$/, "");
25370
+ while (parent.includes("/")) {
25371
+ parent = parent.slice(0, parent.lastIndexOf("/"));
25372
+ if (parent === "") break;
25373
+ if (existing.has(parent) || existing.has(`${parent}/`) || existing.has(`${parent}/**`)) {
25374
+ return true;
25375
+ }
25376
+ }
25377
+ return false;
22952
25378
  }
22953
- function copyManagedOutputs(outputs) {
22954
- return Object.freeze({
22955
- dirs: Object.freeze([...outputs.dirs]),
22956
- files: Object.freeze([...outputs.files])
22957
- });
25379
+
25380
+ // src/targets/projection/lessons-paragraph.ts
25381
+ init_managed_blocks();
25382
+ var LESSONS_PARAGRAPH_BLOCK = `${LESSONS_CONTRACT_START}
25383
+ ${LESSONS_PROCEDURAL_RULE}
25384
+ ${LESSONS_CONTRACT_END}`;
25385
+ function appendLessonsParagraph(content) {
25386
+ const withoutPrior = stripLessonsParagraph(content);
25387
+ return insertAtBodyTop(withoutPrior, LESSONS_PARAGRAPH_BLOCK);
22958
25388
  }
22959
- function copyLayout(layout) {
22960
- return Object.freeze({
22961
- ...layout,
22962
- outputFamilies: layout.outputFamilies === void 0 ? void 0 : Object.freeze(layout.outputFamilies.map(copyOutputFamily)),
22963
- managedOutputs: layout.managedOutputs === void 0 ? void 0 : copyManagedOutputs(layout.managedOutputs),
22964
- paths: Object.freeze({ ...layout.paths })
22965
- });
25389
+ function stripLessonsParagraph(content) {
25390
+ const withoutBlock = stripManagedBlock(content, LESSONS_CONTRACT_START, LESSONS_CONTRACT_END);
25391
+ return stripRawProceduralRule(withoutBlock).trim();
22966
25392
  }
22967
- function copyGlobalSupport(globalSupport) {
22968
- return Object.freeze({
22969
- ...globalSupport,
22970
- capabilities: copyCapabilities(globalSupport.capabilities),
22971
- detectionPaths: Object.freeze([...globalSupport.detectionPaths]),
22972
- layout: copyLayout(globalSupport.layout)
22973
- });
25393
+ function stripRawProceduralRule(content) {
25394
+ return content.replace(`
25395
+
25396
+ ${LESSONS_PROCEDURAL_RULE}`, "").replace(LESSONS_PROCEDURAL_RULE, "");
22974
25397
  }
22975
- function copyTargetDescriptor(descriptor31) {
22976
- return Object.freeze({
22977
- ...descriptor31,
22978
- generators: copyGenerators(descriptor31.generators),
22979
- capabilities: copyCapabilities(descriptor31.capabilities),
22980
- globalSupport: descriptor31.globalSupport === void 0 ? void 0 : copyGlobalSupport(descriptor31.globalSupport),
22981
- lint: descriptor31.lint === void 0 ? void 0 : Object.freeze({ ...descriptor31.lint }),
22982
- project: copyLayout(descriptor31.project),
22983
- supportsConversion: descriptor31.supportsConversion === void 0 ? void 0 : Object.freeze({ ...descriptor31.supportsConversion }),
22984
- detectionPaths: Object.freeze([...descriptor31.detectionPaths]),
22985
- sharedArtifacts: descriptor31.sharedArtifacts === void 0 ? void 0 : Object.freeze({ ...descriptor31.sharedArtifacts })
22986
- });
25398
+
25399
+ // src/lessons/skill.ts
25400
+ init_markdown();
25401
+ var LESSONS_SKILL_NAME = "lessons";
25402
+ var LESSONS_SKILL_DESCRIPTION = "Full operating manual for the agentsmesh lessons system (recall + capture). Consult when running any `agentsmesh lessons` subcommand (query, add, topics, show, deprecate, merge, untrigger, strip-markers, journal, validate, stats, prune, import-md), choosing a topic or trigger flags, using the lessons MCP tools, or when unsure how to phrase or capture a lesson.";
25403
+ var LESSONS_SKILL_BODY = `# Lessons \u2014 operating manual
25404
+
25405
+ Two commands: **Recall** before you act, **Capture** after any failure. The graph
25406
+ \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit.
25407
+
25408
+ ## Recall \u2014 before each file edit and each state-changing command
25409
+
25410
+ \`agentsmesh lessons query --file <path> --cmd <command>\` (add \`--keyword <text>\` to
25411
+ match by task), then apply every rule returned. Scope is MUTATING actions: file edits
25412
+ and state-changing commands (build/test/install/migrate/git-write). Pure-read commands
25413
+ (cat/ls/grep/git-log; read-only) and the recall query itself are **exempt** \u2014 no
25414
+ infinite regress. A predicate-less query is rejected; **keyword-only recall is the
25415
+ anti-pattern** \u2014 most lessons are keyed to a \`file_glob\`/\`command_pattern\` and won't
25416
+ surface (the CLI warns). Excuses ("small edit", "I already know this", "later") all
25417
+ mean: query first \u2014 skipping recall on a mutating action is a process violation, and
25418
+ the user will check.
25419
+
25420
+ ## Capture \u2014 immediately after any failure
25421
+
25422
+ Any failure counts, not just red tests: a failing test/CI/lint/typecheck, a code
25423
+ review, a user correction, a regression, or a wrong assumption \u2014 yours or anyone's.
25424
+
25425
+ \`agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`
25426
+
25427
+ - **At least one _effective_ trigger is required.** A capture is rejected
25428
+ (\`UNRECALLABLE_LESSON\`) when EVERY trigger is dead on the mandatory \`--file\`/\`--cmd\`
25429
+ recall path \u2014 a stopword-only keyword ("state of the art"), or an invalid/ReDoS
25430
+ command regex \u2014 because the lesson could never be recalled there. Prefer
25431
+ \`--trigger-file\`: the most reliable trigger, it fires on \`--file\` recall. A keyword
25432
+ alone is discouraged (\`KEYWORD_ONLY_LESSON\`); paraphrasing an existing rule warns
25433
+ (\`NEAR_DUPLICATE_LESSON\` \u2014 update that lesson instead).
25434
+ - **One imperative sentence.** A rule over 2000 chars is rejected (\`OVERSIZED_RULE\`) \u2014
25435
+ trim it or split into separate lessons; don't paste a log/diff.
25436
+ - Widen with \`--trigger-cmd <regex>\` / \`--trigger-kw <text>\`. New area:
25437
+ \`--new-topic --topic-summary "<line>"\` (list ids with \`agentsmesh lessons topics\`).
25438
+
25439
+ ## No shell? \u2014 MCP tools
25440
+
25441
+ \`lessons_query\`, \`lessons_add\`, \`lessons_topics\`, \`lessons_show\` (inspect a topic),
25442
+ \`lessons_deprecate\` (retire). validate / prune / merge / import-md are CLI-only.
25443
+
25444
+ ## Other subcommands
25445
+
25446
+ \`agentsmesh lessons <cmd>\`: \`show\` \xB7 \`deprecate\` (\`--superseded-by\`) \xB7 \`merge\` \xB7
25447
+ \`untrigger\` \xB7 \`strip-markers\` \xB7 \`prune\` (\`--apply\`; trims over-cap triggers, GCs
25448
+ orphan triggers/topics) \xB7 \`journal\` \xB7 \`validate\` \xB7 \`stats\` \xB7 \`import-md\`. Full
25449
+ help: \`agentsmesh lessons --help\`.
25450
+
25451
+ ## Config (\`.agentsmesh/lessons/config.json\`)
25452
+
25453
+ \`recallLimit\` / \`recallMaxTokens\` (canonical recall caps; per-call overrides
25454
+ \`--top\` / \`--max-tokens\`). \`recallMaxTokens\` is approximate \u2014 \`rule.length / 4\`,
25455
+ not a real tokenizer. \`autoPrune: true\` (default off) auto-GCs structural cruft
25456
+ after each capture \u2014 orphan triggers/topics + non-stranding dead globs, the safe
25457
+ half of \`prune\`; never trims/strands an active lesson, git-reversible.
25458
+
25459
+ ## Dedup (opt-in)
25460
+
25461
+ Set \`--session <id>\` (or \`AGENTSMESH_SESSION_ID\`) and lessons already delivered this
25462
+ session are suppressed, so each recall carries only what is new (\`--no-dedup\` opts
25463
+ out). With no session id, recall is fully stateless \u2014 unchanged.`;
25464
+ var LESSONS_SKILL_FILE = serializeFrontmatter(
25465
+ { name: LESSONS_SKILL_NAME, description: LESSONS_SKILL_DESCRIPTION },
25466
+ LESSONS_SKILL_BODY
25467
+ );
25468
+
25469
+ // src/lessons/init.ts
25470
+ async function scaffoldLessons(projectRoot) {
25471
+ const paths = lessonsPaths(projectRoot);
25472
+ const created = [];
25473
+ const updated = [];
25474
+ const skipped = [];
25475
+ mkdirSync(paths.base, { recursive: true });
25476
+ await maybeAutoMigrateLessons(projectRoot);
25477
+ if (existsSync(paths.graph)) {
25478
+ skipped.push(paths.graph);
25479
+ } else {
25480
+ await mutateLessonsGraphLocked(projectRoot, () => {
25481
+ });
25482
+ created.push(paths.graph);
25483
+ }
25484
+ seedLessonsConfig(projectRoot, created, skipped);
25485
+ seedLessonsSkill(projectRoot, created, updated, skipped);
25486
+ const rootRuleUpdated = injectProceduralBlock(projectRoot);
25487
+ const recallHookInjected = injectRecallHook(projectRoot);
25488
+ const gitignoreUpdated = await ensureGitignoreEntries(projectRoot, [
25489
+ toRelPath(projectRoot, recallLogPath(projectRoot)),
25490
+ toRelPath(projectRoot, captureLogPath(projectRoot))
25491
+ ]);
25492
+ return { created, updated, skipped, rootRuleUpdated, gitignoreUpdated, recallHookInjected };
22987
25493
  }
22988
- function getTargetCatalog() {
22989
- return Object.freeze(BUILTIN_TARGETS.map(copyTargetDescriptor));
25494
+ function seedLessonsSkill(projectRoot, created, updated, skipped) {
25495
+ const skillPath = join(projectRoot, ".agentsmesh/skills", LESSONS_SKILL_NAME, "SKILL.md");
25496
+ const desired = `${LESSONS_SKILL_FILE}
25497
+ `;
25498
+ if (!existsSync(skillPath)) {
25499
+ mkdirSync(dirname(skillPath), { recursive: true });
25500
+ writeFileSync(skillPath, desired, "utf8");
25501
+ created.push(skillPath);
25502
+ return;
25503
+ }
25504
+ if (readFileSync(skillPath, "utf8") === desired) {
25505
+ skipped.push(skillPath);
25506
+ return;
25507
+ }
25508
+ writeFileSync(skillPath, desired, "utf8");
25509
+ updated.push(skillPath);
25510
+ }
25511
+ function seedLessonsConfig(projectRoot, created, skipped) {
25512
+ const configPath = lessonsPaths(projectRoot).config;
25513
+ if (existsSync(configPath)) {
25514
+ skipped.push(configPath);
25515
+ return;
25516
+ }
25517
+ mkdirSync(dirname(configPath), { recursive: true });
25518
+ writeFileSync(configPath, `${JSON.stringify(defaultLessonsConfig(), null, 2)}
25519
+ `, "utf8");
25520
+ created.push(configPath);
25521
+ }
25522
+ function injectProceduralBlock(projectRoot) {
25523
+ const rootRule = join(projectRoot, ".agentsmesh/rules/_root.md");
25524
+ if (!existsSync(rootRule)) {
25525
+ mkdirSync(dirname(rootRule), { recursive: true });
25526
+ const seeded = `---
25527
+ root: true
25528
+ description: ""
25529
+ ---
25530
+
25531
+ ${LESSONS_PARAGRAPH_BLOCK}
25532
+
25533
+ # Operational Guidelines
25534
+ `;
25535
+ writeFileSync(rootRule, seeded, "utf8");
25536
+ return true;
25537
+ }
25538
+ const current = readFileSync(rootRule, "utf8");
25539
+ const desired = `${appendLessonsParagraph(current)}
25540
+ `;
25541
+ if (desired === current) return false;
25542
+ writeFileSync(rootRule, desired, "utf8");
25543
+ return true;
22990
25544
  }
22991
25545
 
22992
- export { AgentsMeshError, ConfigNotFoundError, ConfigValidationError, FileSystemError, GenerationError, ImportError, LockAcquisitionError, RemoteFetchError, TargetNotFoundError, check, computeDiff2 as computeDiff, diff, formatDiffSummary, generate, getAllDescriptors, getDescriptor, getTargetCatalog, importFrom, lint, loadCanonical, loadCanonicalFiles, loadConfig2 as loadConfig, loadConfigFromDirectory, loadProjectContext, registerTargetDescriptor, resolveOutputCollisions };
25546
+ export { AgentsMeshError, ConfigNotFoundError, ConfigValidationError, DEFAULT_RECALL_LIMIT, FileSystemError, GenerationError, ImportError, LESSONS_PROCEDURAL_RULE, LessonsGraphSchema, LockAcquisitionError, RemoteFetchError, TargetNotFoundError, UnknownTopicError, acquireLessonsLock, addLesson, check, computeDiff2 as computeDiff, diff, formatDiffSummary, generate, getAllDescriptors, getDescriptor, getTargetCatalog, graphFilePath, importFrom, importLegacyLessons, lessonsPaths, lint, loadCanonical, loadCanonicalFiles, loadConfig2 as loadConfig, loadConfigFromDirectory, loadLessonsGraph, loadProjectContext, mergeLessons, mutateLessonsGraph, parseGraph, queryLessons, rankLessons, registerTargetDescriptor, resolveOutputCollisions, scaffoldLessons, serializeGraph, stripLegacyMarkers, stripMarkersInGraph, toRelPath, tryLoadLessonsGraph, validateLessonsGraph };
22993
25547
  //# sourceMappingURL=index.js.map
22994
25548
  //# sourceMappingURL=index.js.map