agentsmesh 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.js CHANGED
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  import { stringify, parse } from 'yaml';
3
3
  import { basename, join, dirname, relative, win32, posix, sep, resolve, extname } from 'path';
4
4
  import { readFile, rm, mkdir, readdir, stat, lstat, unlink, writeFile, rename, chmod, access, realpath, mkdtemp, cp } from 'fs/promises';
5
- import { constants, existsSync, realpathSync, statSync, readFileSync } from 'fs';
5
+ import { existsSync, readFileSync, constants, realpathSync, statSync } from 'fs';
6
6
  import { parse as parse$1 } from 'smol-toml';
7
7
  import { Buffer } from 'buffer';
8
8
  import { homedir, tmpdir } from 'os';
@@ -768,6 +768,33 @@ async function readDirRecursive(dir, visited, branchSegments) {
768
768
  );
769
769
  }
770
770
  }
771
+ async function readDirRecursiveNoSymlinks(dir, branchSegments) {
772
+ const currentBranchSegments = branchSegments ?? [basename(dir)];
773
+ try {
774
+ const entries = await readdir(dir, { withFileTypes: true });
775
+ const files = [];
776
+ for (const ent of entries) {
777
+ if (ent.isSymbolicLink()) continue;
778
+ const full = join(dir, ent.name);
779
+ if (ent.isDirectory()) {
780
+ const nextSegments = [...currentBranchSegments, ent.name];
781
+ if (shouldSkipRecursiveBranch(nextSegments)) continue;
782
+ files.push(...await readDirRecursiveNoSymlinks(full, nextSegments));
783
+ } else if (ent.isFile()) {
784
+ files.push(full);
785
+ }
786
+ }
787
+ return files;
788
+ } catch (err) {
789
+ const e = err;
790
+ if (e.code === "ENOENT" || e.code === "ENOTDIR" || e.code === "EACCES") return [];
791
+ throw new FileSystemError(
792
+ dir,
793
+ `Failed to read directory ${dir}: ${e.message}. Check permissions.`,
794
+ { cause: err, errnoCode: e.code }
795
+ );
796
+ }
797
+ }
771
798
  var MAX_RECURSIVE_DEPTH, MAX_SEGMENT_REPETITIONS;
772
799
  var init_fs_traverse = __esm({
773
800
  "src/utils/filesystem/fs-traverse.ts"() {
@@ -1022,7 +1049,7 @@ ${legacy}`, "");
1022
1049
  }
1023
1050
  return result.trim();
1024
1051
  }
1025
- 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;
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_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;
1026
1053
  var init_root_instruction_paragraph = __esm({
1027
1054
  "src/targets/projection/root-instruction-paragraph.ts"() {
1028
1055
  init_managed_blocks();
@@ -1034,7 +1061,9 @@ var init_root_instruction_paragraph = __esm({
1034
1061
  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`.";
1035
1062
  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`.";
1036
1063
  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.";
1037
- 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.";
1064
+ 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.";
1065
+ 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.";
1066
+ 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.";
1038
1067
  LEGACY_AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH = ROOT_INSTRUCTION_BODY_V1;
1039
1068
  LEGACY_AGENTSMESH_ROOT_INSTRUCTION_SECTION = `## Project-Specific Rules
1040
1069
 
@@ -1063,12 +1092,20 @@ ${ROOT_INSTRUCTION_BODY_V7}`;
1063
1092
  AGENTSMESH_CONTRACT_WITH_V8_BODY = `## AgentsMesh Generation Contract
1064
1093
 
1065
1094
  ${ROOT_INSTRUCTION_BODY_V8}`;
1095
+ AGENTSMESH_CONTRACT_WITH_V9_BODY = `## AgentsMesh Generation Contract
1096
+
1097
+ ${ROOT_INSTRUCTION_BODY_V9}`;
1098
+ AGENTSMESH_CONTRACT_WITH_V10_BODY = `## AgentsMesh Generation Contract
1099
+
1100
+ ${ROOT_INSTRUCTION_BODY_V10}`;
1066
1101
  AGENTSMESH_ROOT_INSTRUCTION_PARAGRAPH = `${ROOT_CONTRACT_START}
1067
1102
  ## AgentsMesh Generation Contract
1068
1103
 
1069
1104
  ${ROOT_INSTRUCTION_BODY}
1070
1105
  ${ROOT_CONTRACT_END}`;
1071
1106
  LEGACY_FORMS = [
1107
+ AGENTSMESH_CONTRACT_WITH_V10_BODY,
1108
+ AGENTSMESH_CONTRACT_WITH_V9_BODY,
1072
1109
  AGENTSMESH_CONTRACT_WITH_V8_BODY,
1073
1110
  AGENTSMESH_CONTRACT_WITH_V7_BODY,
1074
1111
  AGENTSMESH_CONTRACT_WITH_V6_BODY,
@@ -15873,9 +15910,18 @@ function generateIgnore12(canonical) {
15873
15910
  if (!canonical.ignore || canonical.ignore.length === 0) return [];
15874
15911
  return [{ path: QWEN_IGNORE, content: canonical.ignore.join("\n") }];
15875
15912
  }
15913
+ function renderQwenGlobalInstructions(canonical) {
15914
+ const root = canonical.rules.find((rule) => rule.root);
15915
+ const nonRootRules = canonical.rules.filter((rule) => {
15916
+ if (rule.root) return false;
15917
+ return rule.targets.length === 0 || rule.targets.includes(QWEN_CODE_TARGET);
15918
+ });
15919
+ return appendEmbeddedRulesBlock(root?.body.trim() ?? "", nonRootRules);
15920
+ }
15876
15921
  var init_generator26 = __esm({
15877
15922
  "src/targets/qwen-code/generator.ts"() {
15878
15923
  init_markdown();
15924
+ init_managed_blocks();
15879
15925
  init_constants21();
15880
15926
  }
15881
15927
  });
@@ -15958,6 +16004,7 @@ var init_qwen_code2 = __esm({
15958
16004
  };
15959
16005
  globalLayout22 = {
15960
16006
  rootInstructionPath: QWEN_GLOBAL_ROOT,
16007
+ renderPrimaryRootInstruction: renderQwenGlobalInstructions,
15961
16008
  skillDir: QWEN_GLOBAL_SKILLS_DIR,
15962
16009
  managedOutputs: {
15963
16010
  dirs: [QWEN_GLOBAL_COMMANDS_DIR, QWEN_GLOBAL_AGENTS_DIR, QWEN_GLOBAL_SKILLS_DIR],
@@ -20147,6 +20194,19 @@ init_fs();
20147
20194
 
20148
20195
  // src/config/remote/git-remote.ts
20149
20196
  init_fs();
20197
+
20198
+ // src/utils/output/redact-url-secrets.ts
20199
+ var URL_WITH_CREDENTIALS = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^/@\s"'<>]+)@([^\s"'<>]+)/g;
20200
+ function redactUrlSecrets(message) {
20201
+ return message.replace(
20202
+ URL_WITH_CREDENTIALS,
20203
+ (_full, scheme, _userinfo, rest) => {
20204
+ return `${scheme}***@${rest}`;
20205
+ }
20206
+ );
20207
+ }
20208
+
20209
+ // src/config/remote/git-remote.ts
20150
20210
  var execFileAsync = promisify(execFile);
20151
20211
  var REPO_DIRNAME = "repo";
20152
20212
  function ensureNotFlag(value, kind) {
@@ -20180,12 +20240,13 @@ async function fetchGitRemoteExtend(parsed, extendName, options, cacheDir, build
20180
20240
  await rm(stagedRoot, { recursive: true, force: true });
20181
20241
  const allowFallback = options.allowOfflineFallback !== false;
20182
20242
  if (allowFallback && await hasCachedRepo(cacheRepoDir)) {
20243
+ const rawMsg = err instanceof Error ? err.message : String(err);
20183
20244
  console.warn(
20184
- `[agentsmesh] Remote fetch failed for ${extendName}; using cached version. Error: ${err instanceof Error ? err.message : String(err)}`
20245
+ `[agentsmesh] Remote fetch failed for ${extendName}; using cached version. Error: ${redactUrlSecrets(rawMsg)}`
20185
20246
  );
20186
20247
  return readCachedRepo(cacheRepoDir);
20187
20248
  }
20188
- throw err;
20249
+ throw err instanceof Error ? Object.assign(new Error(redactUrlSecrets(err.message)), { cause: err.cause }) : err;
20189
20250
  }
20190
20251
  }
20191
20252
  async function readCachedRepo(repoDir) {
@@ -20346,13 +20407,14 @@ async function fetchGithubRemoteExtend(parsed, extendName, options, cacheDir, bu
20346
20407
  if (allowFallback && await exists(extractDir)) {
20347
20408
  const topDir2 = await findExtractTopDir(extractDir);
20348
20409
  if (topDir2) {
20410
+ const rawMsg = err instanceof Error ? err.message : String(err);
20349
20411
  console.warn(
20350
- `[agentsmesh] Network failed for ${extendName}; using cached version. Error: ${err instanceof Error ? err.message : String(err)}`
20412
+ `[agentsmesh] Network failed for ${extendName}; using cached version. Error: ${redactUrlSecrets(rawMsg)}`
20351
20413
  );
20352
20414
  return { resolvedPath: join(extractDir, topDir2), version: tag };
20353
20415
  }
20354
20416
  }
20355
- throw err;
20417
+ throw err instanceof Error ? Object.assign(new Error(redactUrlSecrets(err.message)), { cause: err.cause }) : err;
20356
20418
  }
20357
20419
  await rm(extractDir, { recursive: true, force: true });
20358
20420
  await mkdir(extractDir, { recursive: true });
@@ -20363,12 +20425,14 @@ async function fetchGithubRemoteExtend(parsed, extendName, options, cacheDir, bu
20363
20425
  file: tarPath,
20364
20426
  cwd: extractDir,
20365
20427
  strict: true,
20428
+ // Allowlist entry types instead of denylist: only `File` and `Directory`
20429
+ // can be extracted. Hardlinks (`Link`), symlinks (`SymbolicLink`), FIFOs,
20430
+ // character/block devices, and any future/exotic tar entry type are
20431
+ // rejected. A denylist would silently let an unknown variant through.
20366
20432
  filter: (entryPath, entry) => {
20367
20433
  if (isZipSlipPath(entryPath)) return false;
20368
- if (entry && "type" in entry && (entry.type === "Link" || entry.type === "SymbolicLink")) {
20369
- return false;
20370
- }
20371
- return true;
20434
+ const type = entry && "type" in entry ? entry.type : void 0;
20435
+ return type === "File" || type === "Directory";
20372
20436
  }
20373
20437
  });
20374
20438
  } finally {
@@ -20466,8 +20530,11 @@ function parseGitSource(source) {
20466
20530
  return null;
20467
20531
  }
20468
20532
  const allowInsecure = process.env.AGENTSMESH_ALLOW_INSECURE_GIT === "1" || process.env.AGENTSMESH_ALLOW_INSECURE_GIT === "true";
20469
- const allowedProtocols = allowInsecure ? ["https:", "http:", "ssh:", "file:"] : ["https:", "ssh:", "file:"];
20470
- if (!allowedProtocols.includes(parsedUrl.protocol)) {
20533
+ const allowLocalGit = process.env.AGENTSMESH_ALLOW_LOCAL_GIT === "1" || process.env.AGENTSMESH_ALLOW_LOCAL_GIT === "true";
20534
+ const allowed = ["https:", "ssh:"];
20535
+ if (allowInsecure) allowed.push("http:");
20536
+ if (allowLocalGit) allowed.push("file:");
20537
+ if (!allowed.includes(parsedUrl.protocol)) {
20471
20538
  return null;
20472
20539
  }
20473
20540
  return { url, ref };
@@ -20926,6 +20993,7 @@ async function parseAgents(agentsDir, opts = {}) {
20926
20993
 
20927
20994
  // src/canonical/features/skills.ts
20928
20995
  init_fs();
20996
+ init_fs_traverse();
20929
20997
  init_markdown();
20930
20998
  init_boilerplate_filter();
20931
20999
  async function readContent(path) {
@@ -20944,7 +21012,7 @@ function sanitizeSkillName(raw) {
20944
21012
  return raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
20945
21013
  }
20946
21014
  async function listSupportingFiles(skillDir) {
20947
- const files = await readDirRecursive(skillDir);
21015
+ const files = await readDirRecursiveNoSymlinks(skillDir);
20948
21016
  const result = [];
20949
21017
  for (const absPath of files) {
20950
21018
  const raw = absPath.slice(skillDir.length + 1);
@@ -22517,6 +22585,104 @@ function lintRuleScopeInversion(input) {
22517
22585
  }
22518
22586
  return out2;
22519
22587
  }
22588
+ var TriggersSchema = z.object({
22589
+ file_globs: z.array(z.string()),
22590
+ command_patterns: z.array(z.string()),
22591
+ keywords: z.array(z.string())
22592
+ }).refine((t) => t.file_globs.length + t.command_patterns.length + t.keywords.length > 0, {
22593
+ message: "cluster must declare at least one trigger of any type"
22594
+ });
22595
+ var ClusterSchema = z.object({
22596
+ topic: z.string().regex(/^[a-z0-9-]+$/, "topic must be kebab-case"),
22597
+ /**
22598
+ * Project-relative path (forward slashes) to the cluster's markdown body.
22599
+ * Conventionally `.agentsmesh/lessons/topics/<topic>.md`, but any project
22600
+ * path is accepted — universal across every agent target.
22601
+ */
22602
+ file: z.string().regex(/\.md$/, "file must be a .md path"),
22603
+ summary: z.string().min(1),
22604
+ triggers: TriggersSchema
22605
+ });
22606
+ var LessonsIndexSchema = z.object({
22607
+ version: z.literal(1),
22608
+ /**
22609
+ * Zero clusters is valid — supports `agentsmesh init --lessons` scaffolding a
22610
+ * fresh project. Topics accumulate via `distill:apply` as failures are
22611
+ * captured.
22612
+ */
22613
+ clusters: z.array(ClusterSchema)
22614
+ });
22615
+ var BASE_REL = ".agentsmesh/lessons";
22616
+ function lessonsPaths(projectRoot) {
22617
+ const base = join(projectRoot, BASE_REL);
22618
+ return {
22619
+ base,
22620
+ journal: join(base, "journal.md"),
22621
+ index: join(base, "index.yaml"),
22622
+ ledger: join(base, "distill-ledger.yaml"),
22623
+ proposal: join(base, "distill-proposal.md"),
22624
+ topicsDir: join(base, "topics")
22625
+ };
22626
+ }
22627
+
22628
+ // src/core/lint/shared/lessons.ts
22629
+ var LESSONS_TARGET = "lessons";
22630
+ var INDEX_REL = ".agentsmesh/lessons/index.yaml";
22631
+ var ROOT_RULE_REL = ".agentsmesh/rules/_root.md";
22632
+ var LESSONS_HEADING = /^## Lessons \(/m;
22633
+ var RULES_HEADING = /^## Rules\b/m;
22634
+ function lintLessonsSubsystem(projectRoot, scope) {
22635
+ if (scope === "global") return [];
22636
+ const paths = lessonsPaths(projectRoot);
22637
+ if (!existsSync(paths.index)) return [];
22638
+ const out2 = [];
22639
+ const parsed = LessonsIndexSchema.safeParse(parse(readFileSync(paths.index, "utf8")));
22640
+ if (!parsed.success) {
22641
+ return [diag("error", INDEX_REL, `index.yaml is invalid: ${parsed.error.issues[0].message}`)];
22642
+ }
22643
+ for (const cluster of parsed.data.clusters) {
22644
+ const topicAbs = join(projectRoot, cluster.file);
22645
+ if (!existsSync(topicAbs)) {
22646
+ out2.push(
22647
+ diag("error", cluster.file, `topic file for cluster "${cluster.topic}" does not exist.`)
22648
+ );
22649
+ continue;
22650
+ }
22651
+ if (!RULES_HEADING.test(readFileSync(topicAbs, "utf8"))) {
22652
+ out2.push(
22653
+ diag("warning", cluster.file, `topic "${cluster.topic}" is missing a "## Rules" section.`)
22654
+ );
22655
+ }
22656
+ for (const pattern of cluster.triggers.command_patterns) {
22657
+ try {
22658
+ new RegExp(pattern);
22659
+ } catch {
22660
+ out2.push(
22661
+ diag(
22662
+ "warning",
22663
+ INDEX_REL,
22664
+ `cluster "${cluster.topic}" command_patterns entry is not a valid regex: ${pattern}`
22665
+ )
22666
+ );
22667
+ }
22668
+ }
22669
+ }
22670
+ const rootRuleAbs = join(projectRoot, ROOT_RULE_REL);
22671
+ const rootRuleBody = existsSync(rootRuleAbs) ? readFileSync(rootRuleAbs, "utf8") : "";
22672
+ if (!LESSONS_HEADING.test(rootRuleBody)) {
22673
+ out2.push(
22674
+ diag(
22675
+ "warning",
22676
+ ROOT_RULE_REL,
22677
+ 'lessons procedural rule ("## Lessons (...)") is missing from _root.md \u2014 recall/capture enforcement will not fire.'
22678
+ )
22679
+ );
22680
+ }
22681
+ return out2;
22682
+ }
22683
+ function diag(level, file, message) {
22684
+ return { level, file, target: LESSONS_TARGET, message };
22685
+ }
22520
22686
 
22521
22687
  // src/core/lint/linter.ts
22522
22688
  var EXCLUDE_DIRS = ["node_modules", ".git", "dist", "coverage", ".agentsmesh"];
@@ -22537,7 +22703,7 @@ async function runLint(config, canonical, projectRoot, targetFilter, options = {
22537
22703
  const hasMcp = config.features.includes("mcp");
22538
22704
  const hasPermissions = config.features.includes("permissions");
22539
22705
  const hasHooks = config.features.includes("hooks");
22540
- const diagnostics = [];
22706
+ const diagnostics = [...lintLessonsSubsystem(projectRoot, scope)];
22541
22707
  const projectFiles = scope === "global" ? [] : await getProjectFiles(projectRoot);
22542
22708
  for (const target31 of targets) {
22543
22709
  const fullDesc = getDescriptor(target31);