agentsmesh 0.24.0 → 0.25.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,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
3
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';
4
+ import { mkdir, access, readdir, rm, readFile, writeFile, stat, lstat, unlink, rename, chmod, realpath, mkdtemp, cp } from 'fs/promises';
5
5
  import { setTimeout as setTimeout$1 } from 'timers/promises';
6
6
  import { readFileSync, existsSync, mkdirSync, writeFileSync, constants, rmSync, renameSync, readdirSync, realpathSync, statSync } from 'fs';
7
7
  import { parse as parse$1 } from 'smol-toml';
@@ -1381,7 +1381,7 @@ async function importEmbeddedSkills(projectRoot, skillsDir, fromTool, results, n
1381
1381
  toPath: `${AB_SKILLS}/${entry.name}/SKILL.md`,
1382
1382
  feature: "skills"
1383
1383
  });
1384
- const sourceFiles = await readDirRecursive(sourceSkillDir);
1384
+ const sourceFiles = await readDirRecursiveNoSymlinks(sourceSkillDir);
1385
1385
  for (const sourcePath of sourceFiles) {
1386
1386
  if (sourcePath === sourceSkillFile) continue;
1387
1387
  const relativePath = relative(sourceSkillDir, sourcePath).replace(/\\/g, "/");
@@ -2329,7 +2329,7 @@ function matchesExtension(path, extensions) {
2329
2329
  return extensions.some((extension) => path.endsWith(extension));
2330
2330
  }
2331
2331
  async function importFileDirectory(opts) {
2332
- const files = await readDirRecursive(opts.srcDir);
2332
+ const files = await readDirRecursiveNoSymlinks(opts.srcDir);
2333
2333
  const matchedFiles = files.filter((path) => matchesExtension(path, opts.extensions));
2334
2334
  const results = [];
2335
2335
  for (const srcPath of matchedFiles) {
@@ -6748,7 +6748,7 @@ var init_settings_helpers2 = __esm({
6748
6748
  async function importClaudeSkills(projectRoot, results, normalize) {
6749
6749
  const skillsBaseDir = join(projectRoot, CLAUDE_SKILLS_DIR);
6750
6750
  const destBase = join(projectRoot, CLAUDE_CANONICAL_SKILLS_DIR);
6751
- const allFiles = await readDirRecursive(skillsBaseDir);
6751
+ const allFiles = await readDirRecursiveNoSymlinks(skillsBaseDir);
6752
6752
  const skillMdFiles = allFiles.filter((f) => f.endsWith("SKILL.md"));
6753
6753
  for (const skillMdPath of skillMdFiles) {
6754
6754
  const skillDir = dirname(skillMdPath);
@@ -6767,7 +6767,7 @@ async function importClaudeSkills(projectRoot, results, normalize) {
6767
6767
  `);
6768
6768
  continue;
6769
6769
  }
6770
- const skillFiles = await readDirRecursive(skillDir);
6770
+ const skillFiles = await readDirRecursiveNoSymlinks(skillDir);
6771
6771
  for (const filePath of skillFiles) {
6772
6772
  const fileContent = await readFileSafe(filePath);
6773
6773
  if (fileContent === null) continue;
@@ -7392,7 +7392,7 @@ async function importClineRules(projectRoot, results, normalize) {
7392
7392
  feature: "rules"
7393
7393
  });
7394
7394
  } else {
7395
- const ruleFiles = await readDirRecursive(clineRulesPath);
7395
+ const ruleFiles = await readDirRecursiveNoSymlinks(clineRulesPath);
7396
7396
  const mdFiles = ruleFiles.filter((f) => f.endsWith(".md") && !f.includes("/workflows/")).sort();
7397
7397
  const first = mdFiles[0];
7398
7398
  if (first) {
@@ -7456,27 +7456,40 @@ var init_importer_rules = __esm({
7456
7456
  init_constants8();
7457
7457
  }
7458
7458
  });
7459
+ function toStringRecord2(raw) {
7460
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
7461
+ return Object.fromEntries(
7462
+ Object.entries(raw).filter((entry) => typeof entry[1] === "string")
7463
+ );
7464
+ }
7459
7465
  function mapClineServerToCanonical(raw) {
7460
7466
  if (!raw || typeof raw !== "object") return null;
7461
7467
  const obj = raw;
7462
- const command = typeof obj.command === "string" ? obj.command : "";
7463
- if (!command) return null;
7464
- const type = typeof obj.type === "string" ? obj.type : typeof obj.transportType === "string" ? obj.transportType : "stdio";
7465
- const args = Array.isArray(obj.args) ? obj.args.filter((x) => typeof x === "string") : [];
7466
- const envRaw = obj.env;
7467
- const env = envRaw !== null && typeof envRaw === "object" && !Array.isArray(envRaw) ? Object.fromEntries(
7468
- Object.entries(envRaw).filter(
7469
- (entry) => typeof entry[1] === "string"
7470
- )
7471
- ) : {};
7468
+ const transport = typeof obj.type === "string" ? obj.type : void 0;
7469
+ const transportType = typeof obj.transportType === "string" ? obj.transportType : void 0;
7470
+ const env = toStringRecord2(obj.env);
7472
7471
  const description = typeof obj.description === "string" ? obj.description : void 0;
7473
- return {
7474
- ...description !== void 0 && { description },
7475
- type,
7476
- command,
7477
- args,
7478
- env
7479
- };
7472
+ const command = typeof obj.command === "string" ? obj.command : "";
7473
+ if (command) {
7474
+ const args = Array.isArray(obj.args) ? obj.args.filter((x) => typeof x === "string") : [];
7475
+ return {
7476
+ ...description !== void 0 && { description },
7477
+ type: transport ?? transportType ?? "stdio",
7478
+ command,
7479
+ args,
7480
+ env
7481
+ };
7482
+ }
7483
+ if (typeof obj.url === "string") {
7484
+ return {
7485
+ ...description !== void 0 && { description },
7486
+ type: transport ?? transportType ?? "http",
7487
+ url: obj.url,
7488
+ headers: toStringRecord2(obj.headers),
7489
+ env
7490
+ };
7491
+ }
7492
+ return null;
7480
7493
  }
7481
7494
  async function importClineMcp(projectRoot, results) {
7482
7495
  const candidatePaths = [CLINE_MCP_SETTINGS, CLINE_MCP_SETTINGS_LEGACY].map(
@@ -7550,7 +7563,7 @@ var init_reserved = __esm({
7550
7563
  }
7551
7564
  });
7552
7565
  async function readNativeSkill(skillDir) {
7553
- const allFiles = await readDirRecursive(skillDir).catch(() => []);
7566
+ const allFiles = await readDirRecursiveNoSymlinks(skillDir).catch(() => []);
7554
7567
  const entries = [];
7555
7568
  for (const absPath of allFiles) {
7556
7569
  const relPath = relative(skillDir, absPath).replace(/\\/g, "/");
@@ -7616,7 +7629,7 @@ async function importFlatSkill(skillName, srcPath, content, options) {
7616
7629
  async function findDirectorySkills(skillsDir) {
7617
7630
  const skills = /* @__PURE__ */ new Map();
7618
7631
  try {
7619
- const allFiles = await readDirRecursive(skillsDir);
7632
+ const allFiles = await readDirRecursiveNoSymlinks(skillsDir);
7620
7633
  const skillMdFiles = allFiles.filter((f) => basename(f) === "SKILL.md");
7621
7634
  for (const skillMdPath of skillMdFiles) {
7622
7635
  const skillDir = dirname(skillMdPath);
@@ -7747,7 +7760,7 @@ function extractMeta(content, key) {
7747
7760
  return match?.[1]?.trim() ?? null;
7748
7761
  }
7749
7762
  async function loadHooksFromDir(dir, hooks) {
7750
- const files = await readDirRecursive(dir).catch(() => []);
7763
+ const files = await readDirRecursiveNoSymlinks(dir).catch(() => []);
7751
7764
  const shFiles = files.filter((f) => basename(f).endsWith(".sh") && dirname(f) === dir);
7752
7765
  for (const srcPath of shFiles) {
7753
7766
  const content = await readFileSafe(srcPath);
@@ -8482,7 +8495,7 @@ async function importCodexAgentsFromToml(projectRoot, results, normalize) {
8482
8495
  const agentsPath = join(projectRoot, CODEX_AGENTS_DIR);
8483
8496
  const agentsDestDir = join(projectRoot, CODEX_CANONICAL_AGENTS_DIR);
8484
8497
  try {
8485
- const agentFiles = await readDirRecursive(agentsPath);
8498
+ const agentFiles = await readDirRecursiveNoSymlinks(agentsPath);
8486
8499
  const tomlFiles = agentFiles.filter((f) => f.endsWith(".toml"));
8487
8500
  for (const srcPath of tomlFiles) {
8488
8501
  const content = await readFileSafe(srcPath);
@@ -8578,7 +8591,7 @@ async function importCodexNonRootRuleFiles(projectRoot, destDir, normalize) {
8578
8591
  const results = [];
8579
8592
  const codexRulesPath = join(projectRoot, CODEX_RULES_DIR);
8580
8593
  try {
8581
- const ruleFiles = await readDirRecursive(codexRulesPath);
8594
+ const ruleFiles = await readDirRecursiveNoSymlinks(codexRulesPath);
8582
8595
  const mdFiles = ruleFiles.filter((f) => f.endsWith(".md"));
8583
8596
  for (const srcPath of mdFiles) {
8584
8597
  const content = await readFileSafe(srcPath);
@@ -8735,7 +8748,7 @@ async function importCodexRules(projectRoot, results, normalize, normalizeWindsu
8735
8748
  }
8736
8749
  async function importInstructionMirrors(projectRoot, destDir, results, normalize) {
8737
8750
  try {
8738
- const files = await readDirRecursive(join(projectRoot, CODEX_INSTRUCTIONS_DIR));
8751
+ const files = await readDirRecursiveNoSymlinks(join(projectRoot, CODEX_INSTRUCTIONS_DIR));
8739
8752
  const instructionFiles = files.filter((file) => file.endsWith(".md"));
8740
8753
  const instructionsRoot = join(projectRoot, CODEX_INSTRUCTIONS_DIR);
8741
8754
  for (const srcPath of instructionFiles) {
@@ -9181,19 +9194,31 @@ function readMcpServers(content, extension) {
9181
9194
  for (const [name, value] of Object.entries(rawServers)) {
9182
9195
  if (!value || typeof value !== "object" || Array.isArray(value)) continue;
9183
9196
  const server = value;
9184
- if (typeof server.command !== "string") continue;
9185
- servers[name] = {
9186
- type: typeof server.type === "string" ? server.type : "stdio",
9187
- command: server.command,
9188
- args: toStringArray5(server.args),
9189
- env: toStringRecord(server.env),
9190
- description: typeof server.description === "string" ? server.description : void 0
9191
- };
9197
+ const description = typeof server.description === "string" ? server.description : void 0;
9198
+ if (typeof server.command === "string") {
9199
+ servers[name] = {
9200
+ type: typeof server.type === "string" ? server.type : "stdio",
9201
+ command: server.command,
9202
+ args: toStringArray5(server.args),
9203
+ env: toStringRecord(server.env),
9204
+ description
9205
+ };
9206
+ continue;
9207
+ }
9208
+ if (typeof server.url === "string") {
9209
+ servers[name] = {
9210
+ type: typeof server.type === "string" ? server.type : "http",
9211
+ url: server.url,
9212
+ headers: toStringRecord(server.headers),
9213
+ env: toStringRecord(server.env),
9214
+ description
9215
+ };
9216
+ }
9192
9217
  }
9193
9218
  return servers;
9194
9219
  }
9195
9220
  async function importMcp2(projectRoot, results) {
9196
- const files = (await readDirRecursive(join(projectRoot, CONTINUE_MCP_DIR))).filter(
9221
+ const files = (await readDirRecursiveNoSymlinks(join(projectRoot, CONTINUE_MCP_DIR))).filter(
9197
9222
  (file) => [".json", ".yaml", ".yml"].includes(extname(file))
9198
9223
  );
9199
9224
  const merged = {};
@@ -9853,7 +9878,7 @@ function extractWrapperCommand(content) {
9853
9878
  }
9854
9879
  async function importHooks(projectRoot, results) {
9855
9880
  const hooksDir = join(projectRoot, COPILOT_HOOKS_DIR);
9856
- const allFiles = await readDirRecursive(hooksDir).catch(() => []);
9881
+ const allFiles = await readDirRecursiveNoSymlinks(hooksDir).catch(() => []);
9857
9882
  const jsonFiles = allFiles.filter((file) => file.endsWith(".json"));
9858
9883
  const hooks = {};
9859
9884
  for (const srcPath of jsonFiles) {
@@ -9889,7 +9914,7 @@ async function importHooks(projectRoot, results) {
9889
9914
  }
9890
9915
  }
9891
9916
  const legacyDir = join(projectRoot, COPILOT_LEGACY_HOOKS_DIR);
9892
- const legacyFiles = await readDirRecursive(legacyDir).catch(() => []);
9917
+ const legacyFiles = await readDirRecursiveNoSymlinks(legacyDir).catch(() => []);
9893
9918
  const shFiles = legacyFiles.filter(
9894
9919
  (file) => dirname(file) === legacyDir && /^[^-]+-\d+\.sh$/i.test(basename(file))
9895
9920
  );
@@ -9966,7 +9991,7 @@ var init_importer10 = __esm({
9966
9991
  }
9967
9992
  });
9968
9993
  async function skillNamesFromNativeSkillDir(scanRoot) {
9969
- const files = await readDirRecursive(scanRoot);
9994
+ const files = await readDirRecursiveNoSymlinks(scanRoot);
9970
9995
  const names = /* @__PURE__ */ new Set();
9971
9996
  for (const f of files) {
9972
9997
  if (basename(f) === "SKILL.md") {
@@ -9988,7 +10013,7 @@ var init_native_skill_scan = __esm({
9988
10013
  async function inferCopilotPickFromPath(repoRoot, posixPath) {
9989
10014
  const scan = join(repoRoot, ...posixPath.split("/"));
9990
10015
  if (posixPath.startsWith(COPILOT_PROMPTS_DIR)) {
9991
- const files = await readDirRecursive(scan);
10016
+ const files = await readDirRecursiveNoSymlinks(scan);
9992
10017
  const commands = [
9993
10018
  ...new Set(
9994
10019
  files.filter((f) => f.toLowerCase().endsWith(".prompt.md")).map((f) => basename(f, ".prompt.md"))
@@ -9997,7 +10022,7 @@ async function inferCopilotPickFromPath(repoRoot, posixPath) {
9997
10022
  return commands.length ? { commands } : {};
9998
10023
  }
9999
10024
  if (posixPath.startsWith(".github/copilot") && !posixPath.includes("copilot-instructions.md")) {
10000
- const files = await readDirRecursive(scan);
10025
+ const files = await readDirRecursiveNoSymlinks(scan);
10001
10026
  const rules = [
10002
10027
  ...new Set(
10003
10028
  files.filter((f) => f.includes(".instructions.md")).map((f) => basename(f).replace(/\.instructions\.md$/i, ""))
@@ -10006,7 +10031,7 @@ async function inferCopilotPickFromPath(repoRoot, posixPath) {
10006
10031
  return rules.length ? { rules } : {};
10007
10032
  }
10008
10033
  if (posixPath.startsWith(".github/instructions")) {
10009
- const files = await readDirRecursive(scan);
10034
+ const files = await readDirRecursiveNoSymlinks(scan);
10010
10035
  const names = /* @__PURE__ */ new Set();
10011
10036
  for (const f of files) {
10012
10037
  const b = basename(f);
@@ -10022,7 +10047,7 @@ async function inferCopilotPickFromPath(repoRoot, posixPath) {
10022
10047
  return skills.length ? { skills } : {};
10023
10048
  }
10024
10049
  if (posixPath.startsWith(".github/agents")) {
10025
- const files = await readDirRecursive(scan);
10050
+ const files = await readDirRecursiveNoSymlinks(scan);
10026
10051
  const agents = [
10027
10052
  ...new Set(
10028
10053
  files.filter((f) => f.toLowerCase().endsWith(".agent.md")).map((f) => basename(f, ".agent.md"))
@@ -11571,7 +11596,7 @@ async function importSkills3(projectRoot, results, normalize, skillsRelDir = CUR
11571
11596
  for (const [skillName, skillDir] of directorySkills) {
11572
11597
  await importDirectorySkill(skillName, skillDir, options);
11573
11598
  }
11574
- const allFiles = await readDirRecursive(skillsDir).catch(() => []);
11599
+ const allFiles = await readDirRecursiveNoSymlinks(skillsDir).catch(() => []);
11575
11600
  const mdFiles = allFiles.filter((f) => f.endsWith(".md"));
11576
11601
  const handledPaths = new Set(
11577
11602
  Array.from(directorySkills.values()).flatMap(
@@ -11609,11 +11634,11 @@ async function hasGlobalCursorArtifacts(projectRoot) {
11609
11634
  const stat9 = await readFileSafe(p);
11610
11635
  if (stat9 !== null && stat9.trim() !== "") return true;
11611
11636
  }
11612
- const skillFiles = await readDirRecursive(join(projectRoot, CURSOR_SKILLS_DIR));
11637
+ const skillFiles = await readDirRecursiveNoSymlinks(join(projectRoot, CURSOR_SKILLS_DIR));
11613
11638
  if (skillFiles.some((f) => f.endsWith(".md"))) return true;
11614
- const agentFiles = await readDirRecursive(join(projectRoot, CURSOR_AGENTS_DIR));
11639
+ const agentFiles = await readDirRecursiveNoSymlinks(join(projectRoot, CURSOR_AGENTS_DIR));
11615
11640
  if (agentFiles.some((f) => f.endsWith(".md"))) return true;
11616
- const commandFiles = await readDirRecursive(join(projectRoot, CURSOR_COMMANDS_DIR));
11641
+ const commandFiles = await readDirRecursiveNoSymlinks(join(projectRoot, CURSOR_COMMANDS_DIR));
11617
11642
  if (commandFiles.some((f) => f.endsWith(".md"))) return true;
11618
11643
  return false;
11619
11644
  }
@@ -13201,7 +13226,7 @@ async function importGeminiPolicies(projectRoot) {
13201
13226
  const policiesDir = join(projectRoot, GEMINI_POLICIES_DIR);
13202
13227
  let policyFiles;
13203
13228
  try {
13204
- policyFiles = await readDirRecursive(policiesDir);
13229
+ policyFiles = await readDirRecursiveNoSymlinks(policiesDir);
13205
13230
  } catch {
13206
13231
  return results;
13207
13232
  }
@@ -13300,7 +13325,7 @@ var init_importer_strip = __esm({
13300
13325
  });
13301
13326
  async function importGeminiSkillsAndAgents(projectRoot, results, normalize) {
13302
13327
  const geminiSkillsPath = join(projectRoot, GEMINI_SKILLS_DIR);
13303
- const skillDirs = await readDirRecursive(geminiSkillsPath);
13328
+ const skillDirs = await readDirRecursiveNoSymlinks(geminiSkillsPath);
13304
13329
  const skillMdFiles = skillDirs.filter((f) => basename(f) === "SKILL.md");
13305
13330
  for (const srcPath of skillMdFiles) {
13306
13331
  const content = await readFileSafe(srcPath);
@@ -13339,7 +13364,7 @@ async function importGeminiSkillsAndAgents(projectRoot, results, normalize) {
13339
13364
  toPath: `${GEMINI_CANONICAL_SKILLS_DIR}/${skillName}/SKILL.md`,
13340
13365
  feature: "skills"
13341
13366
  });
13342
- const allSkillFiles = await readDirRecursive(dirname(srcPath));
13367
+ const allSkillFiles = await readDirRecursiveNoSymlinks(dirname(srcPath));
13343
13368
  for (const absPath of allSkillFiles) {
13344
13369
  if (absPath === srcPath) continue;
13345
13370
  const supportContent = await readFileSafe(absPath);
@@ -13358,7 +13383,7 @@ async function importGeminiSkillsAndAgents(projectRoot, results, normalize) {
13358
13383
  }
13359
13384
  const geminiAgentsPath = join(projectRoot, GEMINI_AGENTS_DIR);
13360
13385
  try {
13361
- const agentFiles = await readDirRecursive(geminiAgentsPath);
13386
+ const agentFiles = await readDirRecursiveNoSymlinks(geminiAgentsPath);
13362
13387
  const agentMdFiles = agentFiles.filter((f) => f.endsWith(".md"));
13363
13388
  for (const srcPath of agentMdFiles) {
13364
13389
  const content = await readFileSafe(srcPath);
@@ -13491,7 +13516,7 @@ function isUnderGeminiCommands(pathInRepoPosix) {
13491
13516
  async function inferGeminiCommandNamesFromFiles(repoRoot, pathInRepoPosix) {
13492
13517
  const commandsRoot = join(repoRoot, ...GEMINI_COMMANDS_DIR.split("/"));
13493
13518
  const scanDir = join(repoRoot, ...pathInRepoPosix.split("/"));
13494
- const files = await readDirRecursive(scanDir);
13519
+ const files = await readDirRecursiveNoSymlinks(scanDir);
13495
13520
  const names = [];
13496
13521
  for (const f of files) {
13497
13522
  if (!/\.(toml|md)$/i.test(f)) continue;
@@ -15703,7 +15728,7 @@ async function importNonRootRules(projectRoot, results, normalize) {
15703
15728
  }
15704
15729
  async function importHooks2(projectRoot, results) {
15705
15730
  const hooks = {};
15706
- for (const absPath of await readDirRecursive(join(projectRoot, KIRO_HOOKS_DIR))) {
15731
+ for (const absPath of await readDirRecursiveNoSymlinks(join(projectRoot, KIRO_HOOKS_DIR))) {
15707
15732
  if (!absPath.endsWith(".kiro.hook")) continue;
15708
15733
  const parsed = parseKiroHookFile(await readFileSafe(absPath) ?? "");
15709
15734
  if (!parsed) continue;
@@ -16055,7 +16080,7 @@ var init_generator24 = __esm({
16055
16080
  init_constants19();
16056
16081
  }
16057
16082
  });
16058
- function toStringRecord2(value) {
16083
+ function toStringRecord3(value) {
16059
16084
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
16060
16085
  const out2 = {};
16061
16086
  for (const [k, v] of Object.entries(value)) {
@@ -16081,8 +16106,8 @@ function parseOpenCodeMcp(content) {
16081
16106
  out2[name] = {
16082
16107
  type: "url",
16083
16108
  url: entry.url,
16084
- headers: toStringRecord2(entry.headers),
16085
- env: toStringRecord2(entry.environment),
16109
+ headers: toStringRecord3(entry.headers),
16110
+ env: toStringRecord3(entry.environment),
16086
16111
  ...typeof entry.description === "string" ? { description: entry.description } : {}
16087
16112
  };
16088
16113
  continue;
@@ -16096,7 +16121,7 @@ function parseOpenCodeMcp(content) {
16096
16121
  type: "stdio",
16097
16122
  command,
16098
16123
  args,
16099
- env: toStringRecord2(entry.environment),
16124
+ env: toStringRecord3(entry.environment),
16100
16125
  ...typeof entry.description === "string" ? { description: entry.description } : {}
16101
16126
  };
16102
16127
  }
@@ -18918,7 +18943,7 @@ function toStringArray8(value) {
18918
18943
  }
18919
18944
  async function importWorkflows(projectRoot, results, normalize) {
18920
18945
  const workflowsDir = join(projectRoot, WINDSURF_WORKFLOWS_DIR);
18921
- const workflowFiles = await readDirRecursive(workflowsDir);
18946
+ const workflowFiles = await readDirRecursiveNoSymlinks(workflowsDir);
18922
18947
  const workflowMdFiles = workflowFiles.filter((f) => f.endsWith(".md"));
18923
18948
  const destCommandsDir = join(projectRoot, WINDSURF_CANONICAL_COMMANDS_DIR);
18924
18949
  for (const srcPath of workflowMdFiles) {
@@ -20760,8 +20785,22 @@ function resolveOutputCollisions(results) {
20760
20785
  }
20761
20786
  deduped[existingIdx] = mergeDuplicateMetadata(existing, result);
20762
20787
  }
20788
+ assertNoCaseOnlyPathCollisions(deduped);
20763
20789
  return deduped;
20764
20790
  }
20791
+ function assertNoCaseOnlyPathCollisions(results) {
20792
+ const byLower = /* @__PURE__ */ new Map();
20793
+ for (const result of results) {
20794
+ const key = result.path.toLowerCase();
20795
+ const prior = byLower.get(key);
20796
+ if (prior !== void 0 && prior.path !== result.path) {
20797
+ throw new Error(
20798
+ `Case-only path collision: "${prior.path}" (${prior.target}) and "${result.path}" (${result.target}) resolve to the same file on case-insensitive filesystems (Windows/macOS). Rename one canonical source.`
20799
+ );
20800
+ }
20801
+ byLower.set(key, result);
20802
+ }
20803
+ }
20765
20804
  function refreshResultStatus(result) {
20766
20805
  const status = result.currentContent === void 0 ? "created" : result.currentContent !== result.content ? "updated" : "unchanged";
20767
20806
  return result.status === status ? result : { ...result, status };
@@ -21316,6 +21355,100 @@ function redactUrlSecrets(message) {
21316
21355
  }
21317
21356
  );
21318
21357
  }
21358
+ function gitProtocolOptIns() {
21359
+ const on = (v) => v === "1" || v === "true";
21360
+ return {
21361
+ http: on(process.env.AGENTSMESH_ALLOW_INSECURE_GIT),
21362
+ file: on(process.env.AGENTSMESH_ALLOW_LOCAL_GIT)
21363
+ };
21364
+ }
21365
+ function isAllowedGitProtocol(url) {
21366
+ let parsed;
21367
+ try {
21368
+ parsed = new URL(url);
21369
+ } catch {
21370
+ return false;
21371
+ }
21372
+ const { http, file } = gitProtocolOptIns();
21373
+ const allowed = ["https:", "ssh:"];
21374
+ if (http) allowed.push("http:");
21375
+ if (file) allowed.push("file:");
21376
+ return allowed.includes(parsed.protocol);
21377
+ }
21378
+ function gitAllowProtocolEnv() {
21379
+ const { http, file } = gitProtocolOptIns();
21380
+ const protos = ["https", "ssh"];
21381
+ if (http) protos.push("http");
21382
+ if (file) protos.push("file");
21383
+ return protos.join(":");
21384
+ }
21385
+ function assertAllowedGitUrl(url) {
21386
+ if (isAllowedGitProtocol(url)) return;
21387
+ throw new Error(
21388
+ `agentsmesh refuses a git remote with a disallowed transport: "${redactUrlSecrets(url)}". Allowed: https, ssh. Set AGENTSMESH_ALLOW_INSECURE_GIT=1 to permit http, AGENTSMESH_ALLOW_LOCAL_GIT=1 to permit file.`
21389
+ );
21390
+ }
21391
+ function splitSourceRef(source, prefix, defaultRef) {
21392
+ if (!source.startsWith(prefix)) return null;
21393
+ const rest = source.slice(prefix.length).trim();
21394
+ if (!rest) return null;
21395
+ const refIdx = rest.lastIndexOf("@");
21396
+ if (refIdx < 0) return [rest, defaultRef];
21397
+ const slug = rest.slice(0, refIdx).trim();
21398
+ const ref = rest.slice(refIdx + 1).trim();
21399
+ if (!slug || !ref) return null;
21400
+ return [slug, ref];
21401
+ }
21402
+ function parseGithubSource(source) {
21403
+ const parts = splitSourceRef(source, "github:", "latest");
21404
+ if (!parts) return null;
21405
+ const [slug, tag] = parts;
21406
+ const slash = slug.indexOf("/");
21407
+ if (slash < 0) return null;
21408
+ const org = slug.slice(0, slash).trim();
21409
+ const repo = slug.slice(slash + 1).trim();
21410
+ if (!org || !repo || !tag) return null;
21411
+ return { org, repo, tag };
21412
+ }
21413
+ function parseGitlabSource(source) {
21414
+ const parts = splitSourceRef(source, "gitlab:");
21415
+ if (!parts) return null;
21416
+ const [slug, ref] = parts;
21417
+ const slash = slug.lastIndexOf("/");
21418
+ if (slash < 0) return null;
21419
+ const namespace = slug.slice(0, slash).trim();
21420
+ const project31 = slug.slice(slash + 1).trim();
21421
+ if (!namespace || !project31) return null;
21422
+ return {
21423
+ namespace,
21424
+ project: project31,
21425
+ ref,
21426
+ cloneUrl: `https://gitlab.com/${namespace}/${project31}.git`
21427
+ };
21428
+ }
21429
+ function parseGitSource(source) {
21430
+ if (!source.startsWith("git+")) return null;
21431
+ const rest = source.slice(4).trim();
21432
+ if (!rest) return null;
21433
+ const hashIdx = rest.lastIndexOf("#");
21434
+ const url = (hashIdx < 0 ? rest : rest.slice(0, hashIdx)).trim();
21435
+ const ref = hashIdx < 0 ? void 0 : rest.slice(hashIdx + 1).trim();
21436
+ if (!url || hashIdx >= 0 && !ref) return null;
21437
+ if (!isAllowedGitProtocol(url)) return null;
21438
+ return { url, ref };
21439
+ }
21440
+ function parseRemoteSource(source) {
21441
+ const github = parseGithubSource(source);
21442
+ if (github) return { kind: "github", ...github };
21443
+ const gitlab = parseGitlabSource(source);
21444
+ if (gitlab) return { kind: "gitlab", ...gitlab };
21445
+ const git = parseGitSource(source);
21446
+ if (git) return { kind: "git", ...git };
21447
+ return null;
21448
+ }
21449
+ function isSupportedRemoteSource(source) {
21450
+ return parseRemoteSource(source) !== null;
21451
+ }
21319
21452
 
21320
21453
  // src/config/remote/git-remote.ts
21321
21454
  var execFileAsync = promisify(execFile);
@@ -21386,7 +21519,8 @@ function resolveCloneUrl(parsed) {
21386
21519
  }
21387
21520
  async function cloneRepo(cloneUrl, repoDir) {
21388
21521
  ensureNotFlag(cloneUrl, "clone-url");
21389
- await runGit(["clone", cloneUrl, repoDir]);
21522
+ assertAllowedGitUrl(cloneUrl);
21523
+ await runGit(["clone", "-c", "core.symlinks=false", cloneUrl, repoDir]);
21390
21524
  }
21391
21525
  async function checkoutRef(repoDir, ref) {
21392
21526
  ensureNotFlag(ref, "ref");
@@ -21400,7 +21534,10 @@ async function runGit(args, cwd) {
21400
21534
  cwd,
21401
21535
  env: {
21402
21536
  ...process.env,
21403
- GIT_TERMINAL_PROMPT: "0"
21537
+ GIT_TERMINAL_PROMPT: "0",
21538
+ // Constrain transports git may use (incl. mid-clone redirects/insteadOf)
21539
+ // to the same allowlist the clone URL itself passed.
21540
+ GIT_ALLOW_PROTOCOL: gitAllowProtocolEnv()
21404
21541
  }
21405
21542
  });
21406
21543
  return stdout.trim();
@@ -21588,80 +21725,6 @@ async function fetchGithubDefaultBranch(parsed, extendName, options, cacheDir, b
21588
21725
  }
21589
21726
  throw lastError instanceof Error ? lastError : new Error("Failed to clone GitHub default branch");
21590
21727
  }
21591
- function splitSourceRef(source, prefix, defaultRef) {
21592
- if (!source.startsWith(prefix)) return null;
21593
- const rest = source.slice(prefix.length).trim();
21594
- if (!rest) return null;
21595
- const refIdx = rest.lastIndexOf("@");
21596
- if (refIdx < 0) return [rest, defaultRef];
21597
- const slug = rest.slice(0, refIdx).trim();
21598
- const ref = rest.slice(refIdx + 1).trim();
21599
- if (!slug || !ref) return null;
21600
- return [slug, ref];
21601
- }
21602
- function parseGithubSource(source) {
21603
- const parts = splitSourceRef(source, "github:", "latest");
21604
- if (!parts) return null;
21605
- const [slug, tag] = parts;
21606
- const slash = slug.indexOf("/");
21607
- if (slash < 0) return null;
21608
- const org = slug.slice(0, slash).trim();
21609
- const repo = slug.slice(slash + 1).trim();
21610
- if (!org || !repo || !tag) return null;
21611
- return { org, repo, tag };
21612
- }
21613
- function parseGitlabSource(source) {
21614
- const parts = splitSourceRef(source, "gitlab:");
21615
- if (!parts) return null;
21616
- const [slug, ref] = parts;
21617
- const slash = slug.lastIndexOf("/");
21618
- if (slash < 0) return null;
21619
- const namespace = slug.slice(0, slash).trim();
21620
- const project31 = slug.slice(slash + 1).trim();
21621
- if (!namespace || !project31) return null;
21622
- return {
21623
- namespace,
21624
- project: project31,
21625
- ref,
21626
- cloneUrl: `https://gitlab.com/${namespace}/${project31}.git`
21627
- };
21628
- }
21629
- function parseGitSource(source) {
21630
- if (!source.startsWith("git+")) return null;
21631
- const rest = source.slice(4).trim();
21632
- if (!rest) return null;
21633
- const hashIdx = rest.lastIndexOf("#");
21634
- const url = (hashIdx < 0 ? rest : rest.slice(0, hashIdx)).trim();
21635
- const ref = hashIdx < 0 ? void 0 : rest.slice(hashIdx + 1).trim();
21636
- if (!url || hashIdx >= 0 && !ref) return null;
21637
- let parsedUrl;
21638
- try {
21639
- parsedUrl = new URL(url);
21640
- } catch {
21641
- return null;
21642
- }
21643
- const allowInsecure = process.env.AGENTSMESH_ALLOW_INSECURE_GIT === "1" || process.env.AGENTSMESH_ALLOW_INSECURE_GIT === "true";
21644
- const allowLocalGit = process.env.AGENTSMESH_ALLOW_LOCAL_GIT === "1" || process.env.AGENTSMESH_ALLOW_LOCAL_GIT === "true";
21645
- const allowed = ["https:", "ssh:"];
21646
- if (allowInsecure) allowed.push("http:");
21647
- if (allowLocalGit) allowed.push("file:");
21648
- if (!allowed.includes(parsedUrl.protocol)) {
21649
- return null;
21650
- }
21651
- return { url, ref };
21652
- }
21653
- function parseRemoteSource(source) {
21654
- const github = parseGithubSource(source);
21655
- if (github) return { kind: "github", ...github };
21656
- const gitlab = parseGitlabSource(source);
21657
- if (gitlab) return { kind: "gitlab", ...gitlab };
21658
- const git = parseGitSource(source);
21659
- if (git) return { kind: "git", ...git };
21660
- return null;
21661
- }
21662
- function isSupportedRemoteSource(source) {
21663
- return parseRemoteSource(source) !== null;
21664
- }
21665
21728
  async function sweepStaleCache(cacheDir, maxAgeMs) {
21666
21729
  const dir = cacheDir ?? getCacheDir();
21667
21730
  const threshold = Number(process.env.AGENTSMESH_CACHE_MAX_AGE_DAYS ?? 30) * 864e5;
@@ -21895,15 +21958,17 @@ function assertNoBasenameCollisions(feature, paths, stripExt) {
21895
21958
  const idx = Math.max(fwdIdx, bckIdx);
21896
21959
  const base = idx === -1 ? p : p.slice(idx + 1);
21897
21960
  const slug = base.endsWith(stripExt) ? base.slice(0, -stripExt.length) : base;
21898
- const prior = seen.get(slug);
21899
- if (prior !== void 0 && prior !== p) {
21961
+ const key = slug.toLowerCase();
21962
+ const prior = seen.get(key);
21963
+ if (prior !== void 0 && prior.path !== p) {
21964
+ const detail = prior.slug === slug ? `"${slug}"` : `"${prior.slug}" vs "${slug}" (case-insensitive)`;
21900
21965
  throw new CanonicalNameError(
21901
21966
  feature,
21902
21967
  slug,
21903
- `canonical ${feature} files collide on slug "${slug}": ${prior} vs ${p}. Rename one.`
21968
+ `canonical ${feature} files collide on slug ${detail}: ${prior.path} vs ${p}. Rename one.`
21904
21969
  );
21905
21970
  }
21906
- seen.set(slug, p);
21971
+ seen.set(key, { path: p, slug });
21907
21972
  }
21908
21973
  }
21909
21974
  var ALTERNATE_RESOURCE_FORMATS = /* @__PURE__ */ new Set([".toml", ".yaml", ".yml", ".json"]);
@@ -21937,7 +22002,7 @@ function toStrArray(v) {
21937
22002
  return [];
21938
22003
  }
21939
22004
  async function parseRules(rulesDir, opts = {}) {
21940
- const files = await readDirRecursive(rulesDir);
22005
+ const files = await readDirRecursiveNoSymlinks(rulesDir);
21941
22006
  const mdFiles = files.filter((f) => {
21942
22007
  if (!f.endsWith(".md")) return false;
21943
22008
  const name = basename(f, ".md");
@@ -21946,6 +22011,7 @@ async function parseRules(rulesDir, opts = {}) {
21946
22011
  warnIfUnrecognizedResourceFormats("rules", rulesDir, files, mdFiles, {
21947
22012
  handledByOtherReader: opts.handledByOtherReader
21948
22013
  });
22014
+ assertNoBasenameCollisions("rule", mdFiles, ".md");
21949
22015
  const rules = [];
21950
22016
  for (const path of mdFiles) {
21951
22017
  const content = await readFileSafe(path);
@@ -21994,7 +22060,7 @@ function toToolsArray2(v) {
21994
22060
  return [];
21995
22061
  }
21996
22062
  async function parseCommands(commandsDir, opts = {}) {
21997
- const files = await readDirRecursive(commandsDir);
22063
+ const files = await readDirRecursiveNoSymlinks(commandsDir);
21998
22064
  const mdFiles = files.filter((f) => f.endsWith(".md") && !basename(f).startsWith("_"));
21999
22065
  warnIfUnrecognizedResourceFormats("commands", commandsDir, files, mdFiles, {
22000
22066
  handledByOtherReader: opts.handledByOtherReader
@@ -22054,7 +22120,7 @@ function toHooks2(v) {
22054
22120
  return {};
22055
22121
  }
22056
22122
  async function parseAgents(agentsDir, opts = {}) {
22057
- const files = await readDirRecursive(agentsDir);
22123
+ const files = await readDirRecursiveNoSymlinks(agentsDir);
22058
22124
  const mdFiles = files.filter((f) => f.endsWith(".md") && !basename(f).startsWith("_"));
22059
22125
  warnIfUnrecognizedResourceFormats("agents", agentsDir, files, mdFiles, {
22060
22126
  handledByOtherReader: opts.handledByOtherReader
@@ -22638,7 +22704,7 @@ async function importEntities(kind, dir, opts) {
22638
22704
  }
22639
22705
  async function readToolNativeEntities(srcDir, targetId, kind, parseOpts = {}) {
22640
22706
  const specs = directorySpecsFor(getDescriptor(targetId)?.importer, kind);
22641
- const allFiles = await readDirRecursive(srcDir);
22707
+ const allFiles = await readDirRecursiveNoSymlinks(srcDir);
22642
22708
  const nonMdExtensions = /* @__PURE__ */ new Set();
22643
22709
  for (const spec of specs) {
22644
22710
  for (const ext of spec.extensions) {
@@ -22995,7 +23061,7 @@ async function stageSingleFile(sourcePath, destinationDir, acceptMdc) {
22995
23061
  async function stageMarkdownCollection(sourceRoot, destinationDir, acceptMdc) {
22996
23062
  const info = await stat(sourceRoot);
22997
23063
  if (info.isFile()) return stageSingleFile(sourceRoot, destinationDir, acceptMdc);
22998
- const files = (await readDirRecursive(sourceRoot)).filter(
23064
+ const files = (await readDirRecursiveNoSymlinks(sourceRoot)).filter(
22999
23065
  (file) => isAcceptedFile(file, acceptMdc) && !isBoilerplate(basename(file))
23000
23066
  );
23001
23067
  if (files.length === 0) {
@@ -23042,7 +23108,7 @@ async function stagePreferredSkills(sourceRoot, destinationDir, preferredSkillNa
23042
23108
  }
23043
23109
  const wanted = new Set(preferredSkillNames);
23044
23110
  const matches = /* @__PURE__ */ new Map();
23045
- for (const file of await readDirRecursive(sourceRoot)) {
23111
+ for (const file of await readDirRecursiveNoSymlinks(sourceRoot)) {
23046
23112
  if (!file.endsWith("/SKILL.md") && !file.endsWith("\\SKILL.md")) continue;
23047
23113
  const skillDir = dirname(file);
23048
23114
  const skillName = basename(skillDir);
@@ -23090,7 +23156,7 @@ async function stageSkills(sourceRoot, destinationDir, options = {}) {
23090
23156
  return;
23091
23157
  }
23092
23158
  await mkdirp(destinationDir);
23093
- const entries = await readDirRecursive(sourceRoot);
23159
+ const entries = await readDirRecursiveNoSymlinks(sourceRoot);
23094
23160
  const roots = /* @__PURE__ */ new Set();
23095
23161
  for (const file of entries.filter(
23096
23162
  (entry) => entry.endsWith("/SKILL.md") || entry.endsWith("\\SKILL.md")
@@ -23784,15 +23850,15 @@ function lessonsPaths(projectRoot) {
23784
23850
  function toRelPath(projectRoot, absolute) {
23785
23851
  return relative(projectRoot, absolute).split(sep).join("/");
23786
23852
  }
23787
- var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING REQUIREMENT \u2014 MUST run both, no exceptions; the user will check)
23853
+ var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
23788
23854
 
23789
- Graph \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit. Manual: the \`lessons\` skill.
23855
+ Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
23790
23856
 
23791
- **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.
23857
+ **Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command>\` and obey matches. Pure-read commands and recall itself are exempt.
23792
23858
 
23793
- **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>\`.
23859
+ **Capture:** after any failure, user correction, regression, wrong assumption, useful surprise, repeated friction, or non-obvious fix, MUST self-critique and run \`agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`.
23794
23860
 
23795
- No shell? Use the \`lessons_query\` / \`lessons_add\` MCP tools. Skip either and the system does not exist.`;
23861
+ **Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`;
23796
23862
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
23797
23863
  var MAX_FILES = 2e5;
23798
23864
  function listProjectFiles(projectRoot) {
@@ -25741,15 +25807,27 @@ async function acquireLessonsLock(projectRoot, opts = {}) {
25741
25807
  function emptyGraph() {
25742
25808
  return { version: 1, lessons: {}, topics: {}, triggers: {} };
25743
25809
  }
25810
+ function findingKey(f) {
25811
+ return `${f.code}|${f.triggerId ?? ""}|${f.lessonId ?? ""}`;
25812
+ }
25813
+ function errorSignatures(report) {
25814
+ return new Set(report.findings.filter((f) => f.level === "error").map(findingKey));
25815
+ }
25744
25816
  async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
25745
25817
  const release = await acquireLessonsLock(projectRoot, { retries: options.retries });
25746
25818
  try {
25747
25819
  const graph = tryLoadLessonsGraph(projectRoot) ?? emptyGraph();
25820
+ const baseline = errorSignatures(validateLessonsGraph(graph));
25748
25821
  const result = await mutator(graph);
25749
25822
  const report = validateLessonsGraph(graph);
25750
- if (!report.ok) {
25751
- const errors = report.findings.filter((f) => f.level === "error").map((f) => `${f.code}: ${f.message}`).join("; ");
25752
- throw new Error(`mutateLessonsGraph: refusing to write an invalid graph \u2014 ${errors}`);
25823
+ const introduced = report.findings.filter(
25824
+ (f) => f.level === "error" && !baseline.has(findingKey(f))
25825
+ );
25826
+ if (introduced.length > 0) {
25827
+ const errors = introduced.map((f) => `${f.code}: ${f.message}`).join("; ");
25828
+ throw new Error(
25829
+ `mutateLessonsGraph: refusing to write \u2014 this change introduces ${errors}. (Pre-existing graph issues are not blocking; run \`agentsmesh lessons validate\` to review and \`lessons untrigger\`/\`prune\` to repair them.)`
25830
+ );
25753
25831
  }
25754
25832
  saveLessonsGraph(projectRoot, graph);
25755
25833
  return result;
@@ -26220,6 +26298,17 @@ function isCoveredByExisting(candidate, existing) {
26220
26298
 
26221
26299
  // src/targets/projection/lessons-paragraph.ts
26222
26300
  init_managed_blocks();
26301
+ var LEGACY_RAW_FORMS = [
26302
+ `## Lessons (BLOCKING REQUIREMENT \u2014 MUST run both, no exceptions; the user will check)
26303
+
26304
+ Graph \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit. Manual: the \`lessons\` skill.
26305
+
26306
+ **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.
26307
+
26308
+ **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>\`.
26309
+
26310
+ No shell? Use the \`lessons_query\` / \`lessons_add\` MCP tools. Skip either and the system does not exist.`
26311
+ ];
26223
26312
  var LESSONS_PARAGRAPH_BLOCK = `${LESSONS_CONTRACT_START}
26224
26313
  ${LESSONS_PROCEDURAL_RULE}
26225
26314
  ${LESSONS_CONTRACT_END}`;
@@ -26232,76 +26321,66 @@ function stripLessonsParagraph(content) {
26232
26321
  return stripRawProceduralRule(withoutBlock).trim();
26233
26322
  }
26234
26323
  function stripRawProceduralRule(content) {
26235
- return content.replace(`
26324
+ return [LESSONS_PROCEDURAL_RULE, ...LEGACY_RAW_FORMS].reduce(
26325
+ (next, rule) => next.replace(`
26236
26326
 
26237
- ${LESSONS_PROCEDURAL_RULE}`, "").replace(LESSONS_PROCEDURAL_RULE, "");
26327
+ ${rule}`, "").replace(rule, ""),
26328
+ content
26329
+ );
26238
26330
  }
26239
26331
 
26240
26332
  // src/lessons/skill.ts
26241
26333
  init_markdown();
26242
26334
  var LESSONS_SKILL_NAME = "lessons";
26243
- 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.";
26244
- var LESSONS_SKILL_BODY = `# Lessons \u2014 operating manual
26335
+ var LESSONS_SKILL_DESCRIPTION = "Use when about to edit a file or run a state-changing command, or after any failure, correction, or surprising result.";
26336
+ var LESSONS_SKILL_BODY = `# Lessons \u2014 operating manual (Iron Law)
26245
26337
 
26246
- Two commands: **Recall** before you act, **Capture** after any failure. The graph
26247
- \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit.
26338
+ ## The Iron Law
26248
26339
 
26249
- ## Recall \u2014 before each file edit and each state-changing command
26250
-
26251
- \`agentsmesh lessons query --file <path> --cmd <command>\` (add \`--keyword <text>\` to
26252
- match by task), then apply every rule returned. Scope is MUTATING actions: file edits
26253
- and state-changing commands (build/test/install/migrate/git-write). Pure-read commands
26254
- (cat/ls/grep/git-log; read-only) and the recall query itself are **exempt** \u2014 no
26255
- infinite regress. A predicate-less query is rejected; **keyword-only recall is the
26256
- anti-pattern** \u2014 most lessons are keyed to a \`file_glob\`/\`command_pattern\` and won't
26257
- surface (the CLI warns). Excuses ("small edit", "I already know this", "later") all
26258
- mean: query first \u2014 skipping recall on a mutating action is a process violation, and
26259
- the user will check.
26260
-
26261
- ## Capture \u2014 immediately after any failure
26340
+ **NO MUTATION WITHOUT RECALL. NO COMPLETION WITHOUT A CAPTURE DECISION.**
26262
26341
 
26263
- Any failure counts, not just red tests: a failing test/CI/lint/typecheck, a code
26264
- review, a user correction, a regression, or a wrong assumption \u2014 yours or anyone's.
26265
-
26266
- \`agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`
26342
+ Violating the letter is violating the spirit. Edited a file or ran a state-changing
26343
+ command without recall? Process violation. Did the turn hit a failure / correction /
26344
+ regression / wrong assumption / surprise and you have not captured (nor stated
26345
+ \`Lesson: none\`)? The task is INCOMPLETE \u2014 and the user will check. The graph
26346
+ \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit.
26267
26347
 
26268
- - **At least one _effective_ trigger is required.** A capture is rejected
26269
- (\`UNRECALLABLE_LESSON\`) when EVERY trigger is dead on the mandatory \`--file\`/\`--cmd\`
26270
- recall path \u2014 a stopword-only keyword ("state of the art"), or an invalid/ReDoS
26271
- command regex \u2014 because the lesson could never be recalled there. Prefer
26272
- \`--trigger-file\`: the most reliable trigger, it fires on \`--file\` recall. A keyword
26273
- alone is discouraged (\`KEYWORD_ONLY_LESSON\`); paraphrasing an existing rule warns
26274
- (\`NEAR_DUPLICATE_LESSON\` \u2014 update that lesson instead).
26275
- - **One imperative sentence.** A rule over 2000 chars is rejected (\`OVERSIZED_RULE\`) \u2014
26276
- trim it or split into separate lessons; don't paste a log/diff.
26277
- - Widen with \`--trigger-cmd <regex>\` / \`--trigger-kw <text>\`. New area:
26278
- \`--new-topic --topic-summary "<line>"\` (list ids with \`agentsmesh lessons topics\`).
26348
+ ## Recall \u2014 before each file edit and each state-changing command
26279
26349
 
26280
- ## No shell? \u2014 MCP tools
26350
+ \`agentsmesh lessons query --file <path> --cmd <command>\`, then apply every rule.
26351
+ Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
26352
+ is the anti-pattern** \u2014 lessons are keyed to a \`file_glob\`/\`command_pattern\`.
26281
26353
 
26282
- \`lessons_query\`, \`lessons_add\`, \`lessons_topics\`, \`lessons_show\` (inspect a topic),
26283
- \`lessons_deprecate\` (retire). validate / prune / merge / import-md are CLI-only.
26354
+ ## Capture \u2014 Gate Function (before any completion claim)
26284
26355
 
26285
- ## Other subcommands
26356
+ 1. **SELF-CRITIQUE**: any failure, correction, regression, wrong assumption,
26357
+ useful surprise, repeated friction, or non-obvious fix? Failing
26358
+ tests/lint/typecheck and user/review corrections \u2014 yours or anyone's \u2014 all count.
26359
+ 2. **CAPTURE** a reusable imperative rule with an effective trigger (else say so):
26360
+ \`agentsmesh lessons add "<rule>" --topic <id> --trigger-file <glob>\`
26361
+ 3. **RECEIPT**: emit \`Lesson: captured <id>\` or \`Lesson: none\`.
26286
26362
 
26287
- \`agentsmesh lessons <cmd>\`: \`show\` \xB7 \`deprecate\` (\`--superseded-by\`) \xB7 \`merge\` \xB7
26288
- \`untrigger\` \xB7 \`strip-markers\` \xB7 \`prune\` (\`--apply\`; trims over-cap triggers, GCs
26289
- orphan triggers/topics) \xB7 \`journal\` \xB7 \`validate\` \xB7 \`stats\` \xB7 \`import-md\`. Full
26290
- help: \`agentsmesh lessons --help\`.
26363
+ At least one _effective_ trigger is required or the capture is rejected
26364
+ (\`UNRECALLABLE_LESSON\`); prefer \`--trigger-file\`. No shell \u2192 MCP \`lessons_query\`,
26365
+ \`lessons_add\`, \`lessons_topics\`, \`lessons_show\`, \`lessons_deprecate\`. Run
26366
+ \`agentsmesh lessons --help\` for every subcommand and flag: query, add, topics, show,
26367
+ deprecate, merge, untrigger, strip-markers, prune, journal, validate, stats, import-md.
26291
26368
 
26292
- ## Config (\`.agentsmesh/lessons/config.json\`)
26369
+ ### Rationalization Prevention \u2014 these excuses mean STOP
26293
26370
 
26294
- \`recallLimit\` / \`recallMaxTokens\` (canonical recall caps; per-call overrides
26295
- \`--top\` / \`--max-tokens\`). \`recallMaxTokens\` is approximate \u2014 \`rule.length / 4\`,
26296
- not a real tokenizer. \`autoPrune: true\` (default off) auto-GCs structural cruft
26297
- after each capture \u2014 orphan triggers/topics + non-stranding dead globs, the safe
26298
- half of \`prune\`; never trims/strands an active lesson, git-reversible.
26371
+ | Excuse | Reality |
26372
+ | --- | --- |
26373
+ | "Small edit / I already know this / later" | Query first \u2014 skipping recall is a process violation |
26374
+ | "Nothing reusable here" | You hit a failure/surprise \u2014 name it or capture it |
26375
+ | "My own TDD red, not a real failure" | A red you did not predict IS a lesson |
26376
+ | "I fixed one site; the twin is obvious" | Capture it \u2014 the unfixed twin is what gets missed |
26377
+ | "Different words, so the rule doesn't apply" | Spirit over letter |
26299
26378
 
26300
- ## Dedup (opt-in)
26379
+ ## Lesson gate \u2014 before final response
26301
26380
 
26302
- Set \`--session <id>\` (or \`AGENTSMESH_SESSION_ID\`) and lessons already delivered this
26303
- session are suppressed, so each recall carries only what is new (\`--no-dedup\` opts
26304
- out). With no session id, recall is fully stateless \u2014 unchanged.`;
26381
+ The final response MUST carry the receipt: \`Lesson: captured <id>\` or \`Lesson: none\`.
26382
+ No receipt = task incomplete. Do not capture one-off facts, task summaries, or project
26383
+ context \u2014 only reusable imperative rules with an effective trigger.`;
26305
26384
  var LESSONS_SKILL_FILE = serializeFrontmatter(
26306
26385
  { name: LESSONS_SKILL_NAME, description: LESSONS_SKILL_DESCRIPTION },
26307
26386
  LESSONS_SKILL_BODY