agentsmesh 0.33.0 → 0.34.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,9 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { stringify, parse, parseDocument, YAMLSeq, YAMLMap, isMap, Document, isSeq, isScalar, Scalar, Pair } from 'yaml';
3
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, constants, rmSync, renameSync, readdirSync, realpathSync, statSync } from 'fs';
3
4
  import { join, resolve, relative, sep, dirname, basename, win32, posix, extname } from 'path';
4
5
  import { mkdir, access, readdir, rm, readFile, writeFile, stat, lstat, unlink, rename, chmod, realpath, mkdtemp, cp } from 'fs/promises';
5
6
  import { setTimeout as setTimeout$1 } from 'timers/promises';
6
- import { readFileSync, existsSync, mkdirSync, writeFileSync, constants, rmSync, renameSync, readdirSync, realpathSync, statSync } from 'fs';
7
7
  import { parse as parse$1, stringify as stringify$1 } from 'smol-toml';
8
8
  import { Buffer } from 'buffer';
9
9
  import { homedir, hostname, tmpdir } from 'os';
@@ -94,8 +94,17 @@ var init_target_descriptor_schema = __esm({
94
94
  managedOutputsSchema = z.object({
95
95
  dirs: z.array(z.string()),
96
96
  files: z.array(z.string()),
97
- coOwnedFiles: z.array(z.string()).optional()
97
+ coOwnedFiles: z.array(z.string()).optional(),
98
+ supersededFiles: z.array(z.string()).optional()
98
99
  }).passthrough().superRefine((value, ctx) => {
100
+ for (const file of value.supersededFiles ?? []) {
101
+ if (!value.files.includes(file) && !(value.coOwnedFiles ?? []).includes(file)) continue;
102
+ ctx.addIssue({
103
+ code: "custom",
104
+ path: ["supersededFiles"],
105
+ message: `"${file}" is in managedOutputs.supersededFiles and another managedOutputs list.`
106
+ });
107
+ }
99
108
  for (const file of value.coOwnedFiles ?? []) {
100
109
  if (!value.files.includes(file)) continue;
101
110
  ctx.addIssue({
@@ -398,24 +407,28 @@ var init_target_ids = __esm({
398
407
  }
399
408
  });
400
409
  function parseFrontmatter(content) {
401
- const open = content.indexOf("---");
402
- if (open !== 0) {
403
- return { frontmatter: {}, body: content.trim() };
404
- }
405
- const close = content.indexOf("---", 3);
406
- if (close === -1) {
407
- return { frontmatter: {}, body: content.trim() };
408
- }
409
- const yamlStr = content.slice(3, close).trim();
410
- const body = content.slice(close + 3).trim();
410
+ const split = splitFrontmatter(content);
411
+ if (split === null) return { frontmatter: {}, body: content.trim() };
412
+ const yamlStr = split.yaml.trim();
411
413
  const frontmatter = yamlStr === "" ? {} : parse(yamlStr) ?? {};
412
- return { frontmatter, body };
414
+ return { frontmatter, body: split.body };
415
+ }
416
+ function splitFrontmatter(content) {
417
+ const opener = OPENER.exec(content);
418
+ if (opener === null) return null;
419
+ const yamlStart = opener[0].length;
420
+ const closer = CLOSER.exec(content.slice(yamlStart));
421
+ if (closer === null) return null;
422
+ const closeStart = yamlStart + closer.index;
423
+ const closeEnd = closeStart + closer[0].length;
424
+ return {
425
+ yaml: content.slice(yamlStart, closeStart),
426
+ body: content.slice(closeEnd).trim(),
427
+ prefix: content.slice(0, closeEnd)
428
+ };
413
429
  }
414
430
  function extractBody(content) {
415
- if (content.indexOf("---") !== 0) return content.trim();
416
- const close = content.indexOf("---", 3);
417
- if (close === -1) return content.trim();
418
- return content.slice(close + 3).trim();
431
+ return splitFrontmatter(content)?.body ?? content.trim();
419
432
  }
420
433
  function tryParseFrontmatter(content, filePath2) {
421
434
  try {
@@ -447,8 +460,11 @@ ${yamlStr}
447
460
 
448
461
  ${body}`;
449
462
  }
463
+ var OPENER, CLOSER;
450
464
  var init_markdown = __esm({
451
465
  "src/utils/text/markdown.ts"() {
466
+ OPENER = /^---[ \t]*\r?\n/;
467
+ CLOSER = /^---[ \t]*\r?$/m;
452
468
  }
453
469
  });
454
470
 
@@ -508,11 +524,60 @@ var init_command_skill = __esm({
508
524
  LEGACY_CODEX_COMMAND_SKILL_PREFIX = "ab-command-";
509
525
  }
510
526
  });
527
+ function injectEvent(doc, event, matcher) {
528
+ const existing = doc.get(event);
529
+ const seq = existing instanceof YAMLSeq ? existing : new YAMLSeq();
530
+ const present = seq.items.some(
531
+ (item) => item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND
532
+ );
533
+ if (present) return false;
534
+ seq.add(doc.createNode({ matcher, type: "command", command: RECALL_HOOK_COMMAND }));
535
+ doc.set(event, seq);
536
+ return true;
537
+ }
538
+ function injectRecallHook(projectRoot) {
539
+ const path = join(projectRoot, ".agentsmesh", "hooks.yaml");
540
+ if (!existsSync(path)) return false;
541
+ const doc = parseDocument(readFileSync(path, "utf8"));
542
+ let changed = false;
543
+ for (const { event, matcher } of RECALL_EVENTS) {
544
+ if (injectEvent(doc, event, matcher)) changed = true;
545
+ }
546
+ if (changed) writeFileSync(path, String(doc), "utf8");
547
+ return changed;
548
+ }
549
+ var RECALL_HOOK_COMMAND, RECALL_HOOK_TOOL_MATCHER, RECALL_EVENTS;
550
+ var init_recall_hook_scaffold = __esm({
551
+ "src/lessons/recall-hook-scaffold.ts"() {
552
+ RECALL_HOOK_COMMAND = "agentsmesh lessons hook";
553
+ RECALL_HOOK_TOOL_MATCHER = "Edit|Write|Bash";
554
+ RECALL_EVENTS = [
555
+ { event: "PreToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
556
+ { event: "PostToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
557
+ { event: "UserPromptSubmit", matcher: "*" },
558
+ // Capture-on-failure nudge (see capture-nudge.ts). BEST-EFFORT: only Claude
559
+ // Code's passthrough hooks emit it; whitelist targets drop it without warning
560
+ // (BEST_EFFORT_HOOK_EVENTS). PostToolUse is success-only, so failures need this.
561
+ { event: "PostToolUseFailure", matcher: "*" },
562
+ // Reset recall dedup after a context compaction/clear (see hook.ts SessionStart).
563
+ // BEST-EFFORT: targets that can't represent SessionStart just keep dedup as-is.
564
+ { event: "SessionStart", matcher: "*" }
565
+ ];
566
+ }
567
+ });
511
568
 
512
569
  // src/core/hook-types.ts
570
+ function isBestEffortHookEvent(event, entries) {
571
+ if (!BEST_EFFORT_HOOK_EVENTS.has(event)) return false;
572
+ if (!Array.isArray(entries)) return true;
573
+ return entries.every(
574
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.command === "string" && entry.command.includes(RECALL_HOOK_COMMAND)
575
+ );
576
+ }
513
577
  var BEST_EFFORT_HOOK_EVENTS;
514
578
  var init_hook_types = __esm({
515
579
  "src/core/hook-types.ts"() {
580
+ init_recall_hook_scaffold();
516
581
  BEST_EFFORT_HOOK_EVENTS = /* @__PURE__ */ new Set([
517
582
  "UserPromptSubmit",
518
583
  "PostToolUseFailure",
@@ -698,7 +763,7 @@ var init_conf_merge = __esm({
698
763
  });
699
764
 
700
765
  // src/core/errors.ts
701
- var AgentsMeshError, ConfigNotFoundError, ConfigValidationError, TargetNotFoundError, ImportError, GenerationError, RemoteFetchError, LockAcquisitionError, FileSystemError;
766
+ var AgentsMeshError, ConfigNotFoundError, ConfigValidationError, CanonicalParseError, TargetNotFoundError, ImportError, GenerationError, RemoteFetchError, LockAcquisitionError, FileSystemError;
702
767
  var init_errors = __esm({
703
768
  "src/core/errors.ts"() {
704
769
  AgentsMeshError = class extends Error {
@@ -733,6 +798,21 @@ var init_errors = __esm({
733
798
  this.issues = issues;
734
799
  }
735
800
  };
801
+ CanonicalParseError = class extends AgentsMeshError {
802
+ path;
803
+ constructor(path, cause) {
804
+ const detail = cause instanceof Error ? cause.message : String(cause);
805
+ super(
806
+ "AM_CONFIG_INVALID",
807
+ `Invalid canonical file ${path}: ${detail}. Fix the syntax and try again.`,
808
+ {
809
+ cause
810
+ }
811
+ );
812
+ this.name = "CanonicalParseError";
813
+ this.path = path;
814
+ }
815
+ };
736
816
  TargetNotFoundError = class extends AgentsMeshError {
737
817
  target;
738
818
  constructor(target34, options) {
@@ -806,6 +886,11 @@ function normalizeLineEndings(content) {
806
886
  function executableModeFor(path) {
807
887
  return EXECUTABLE_SCRIPT_EXTENSIONS.has(extname(path).toLowerCase()) ? 493 : void 0;
808
888
  }
889
+ function normalizeTextPayload(path, content) {
890
+ if (!shouldNormalizeLineEndings(path)) return content;
891
+ const withoutBom = content.startsWith(UTF8_BOM) ? content.slice(UTF8_BOM.length) : content;
892
+ return normalizeLineEndings(withoutBom);
893
+ }
809
894
  var UTF8_BOM, TEXT_EXTENSIONS, TEXT_DOTFILES, EXECUTABLE_SCRIPT_EXTENSIONS;
810
895
  var init_fs_text_encoding = __esm({
811
896
  "src/utils/filesystem/fs-text-encoding.ts"() {
@@ -1306,10 +1391,8 @@ function stripManagedBlock(content, start, end) {
1306
1391
  return content.replace(managedBlockPattern(start, end), "").trim();
1307
1392
  }
1308
1393
  function splitFrontmatterPrefix(content) {
1309
- if (content.indexOf("---") !== 0) return { prefix: "", body: content.trim() };
1310
- const close = content.indexOf("---", 3);
1311
- if (close === -1) return { prefix: "", body: content.trim() };
1312
- return { prefix: content.slice(0, close + 3), body: content.slice(close + 3).trim() };
1394
+ const split = splitFrontmatter(content);
1395
+ return split === null ? { prefix: "", body: content.trim() } : { prefix: split.prefix, body: split.body };
1313
1396
  }
1314
1397
  function insertAtBodyTop(content, block) {
1315
1398
  const { prefix, body } = splitFrontmatterPrefix(content);
@@ -1410,6 +1493,7 @@ function extractEmbeddedRules(content) {
1410
1493
  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;
1411
1494
  var init_managed_blocks = __esm({
1412
1495
  "src/targets/projection/managed-blocks.ts"() {
1496
+ init_markdown();
1413
1497
  ROOT_CONTRACT_START = "<!-- agentsmesh:root-generation-contract:start -->";
1414
1498
  ROOT_CONTRACT_END = "<!-- agentsmesh:root-generation-contract:end -->";
1415
1499
  LESSONS_CONTRACT_START = "<!-- agentsmesh:lessons-contract:start -->";
@@ -1577,10 +1661,17 @@ async function serializeImportedCommandWithFallback(destinationPath, imported, b
1577
1661
  })();
1578
1662
  const description = imported.hasDescription ? imported.description ?? "" : typeof existingFrontmatter.description === "string" ? existingFrontmatter.description : "";
1579
1663
  const allowedTools = imported.hasAllowedTools ? imported.allowedTools ?? [] : existingAllowedTools;
1664
+ const {
1665
+ description: _d,
1666
+ "allowed-tools": _a,
1667
+ allowedTools: _c,
1668
+ ...preserved
1669
+ } = existingFrontmatter;
1580
1670
  return serializeFrontmatter(
1581
1671
  {
1582
1672
  description,
1583
- "allowed-tools": allowedTools
1673
+ "allowed-tools": allowedTools,
1674
+ ...preserved
1584
1675
  },
1585
1676
  body.trim() || ""
1586
1677
  );
@@ -2478,6 +2569,26 @@ var init_link_rebaser_resolution = __esm({
2478
2569
  }
2479
2570
  });
2480
2571
 
2572
+ // src/core/reference/link-uri-encoding.ts
2573
+ function decodeLinkPath(token, role) {
2574
+ if (role !== "markdown-link-dest" || !token.includes("%")) return token;
2575
+ try {
2576
+ return decodeURIComponent(token);
2577
+ } catch {
2578
+ return token;
2579
+ }
2580
+ }
2581
+ function encodeLinkPath(path, enabled) {
2582
+ if (!enabled) return path;
2583
+ return path.split("/").map(
2584
+ (segment) => segment === "" || segment === "." || segment === ".." ? segment : encodeURIComponent(segment)
2585
+ ).join("/");
2586
+ }
2587
+ var init_link_uri_encoding = __esm({
2588
+ "src/core/reference/link-uri-encoding.ts"() {
2589
+ }
2590
+ });
2591
+
2481
2592
  // src/core/reference/link-token-guards.ts
2482
2593
  function isTildeHomeRelativePathToken(fullContent, matchOffset, matchText) {
2483
2594
  if (matchOffset >= 2 && fullContent[matchOffset - 2] === "~" && fullContent[matchOffset - 1] === "/") {
@@ -2607,10 +2718,11 @@ function rewriteFileLinks(input) {
2607
2718
  const { candidate: punctStripped, suffix } = stripTrailingPunctuation(match);
2608
2719
  if (!punctStripped) return match;
2609
2720
  const lineNumMatch = LINE_NUMBER_SUFFIX.exec(punctStripped);
2610
- const candidate = lineNumMatch ? punctStripped.slice(0, lineNumMatch.index) : punctStripped;
2721
+ const rawCandidate = lineNumMatch ? punctStripped.slice(0, lineNumMatch.index) : punctStripped;
2611
2722
  const lineNumSuffix = lineNumMatch ? lineNumMatch[0] : "";
2612
- if (!candidate) return match;
2613
- const tokenContext = getTokenContext(fullContent, offset, offset + candidate.length);
2723
+ if (!rawCandidate) return match;
2724
+ const tokenContext = getTokenContext(fullContent, offset, offset + rawCandidate.length);
2725
+ const candidate = decodeLinkPath(rawCandidate, tokenContext.role);
2614
2726
  if (tokenContext.role !== "markdown-link-dest" && WINDOWS_ABSOLUTE_PATH.test(candidate)) {
2615
2727
  return match;
2616
2728
  }
@@ -2680,7 +2792,7 @@ function rewriteFileLinks(input) {
2680
2792
  const targetTop = targetFromRoot.split("/").filter(Boolean)[0] ?? "";
2681
2793
  const tokenIsCanonicalMesh = normalizeSeparators(candidate).startsWith(".agentsmesh/");
2682
2794
  const preferRelativeProseInSameSurface = !tokenIsCanonicalMesh && !targetIsDirectory && destTop.length > 0 && destTop === targetTop && destTop.startsWith(".") && destTop !== ".agentsmesh";
2683
- const forceRelative = preferRelativeProseInSameSurface || tokenContext.role === "markdown-link-dest" || isMarkdownLinkDestinationToken(fullContent, offset, candidate);
2795
+ const forceRelative = preferRelativeProseInSameSurface || tokenContext.role === "markdown-link-dest" || isMarkdownLinkDestinationToken(fullContent, offset, rawCandidate);
2684
2796
  const rewritten = formatLinkPathForDestination(
2685
2797
  input.projectRoot,
2686
2798
  input.destinationFile,
@@ -2697,7 +2809,7 @@ function rewriteFileLinks(input) {
2697
2809
  }
2698
2810
  );
2699
2811
  if (!rewritten) return match;
2700
- return `${rewritten}${lineNumSuffix}${suffix}`;
2812
+ return `${encodeLinkPath(rewritten, candidate !== rawCandidate)}${lineNumSuffix}${suffix}`;
2701
2813
  });
2702
2814
  return { content, missing: [...missing] };
2703
2815
  }
@@ -2707,6 +2819,7 @@ var init_link_rebaser = __esm({
2707
2819
  init_link_rebaser_helpers();
2708
2820
  init_link_rebaser_output();
2709
2821
  init_link_rebaser_resolution();
2822
+ init_link_uri_encoding();
2710
2823
  init_link_token_guards();
2711
2824
  init_link_token_context();
2712
2825
  }
@@ -3590,7 +3703,7 @@ function unsupportedHookEventNames(hooks, supportedEvents) {
3590
3703
  if (!hooks) return [];
3591
3704
  const supported = new Set(supportedEvents);
3592
3705
  return Object.keys(hooks).filter(
3593
- (event) => !supported.has(event) && !BEST_EFFORT_HOOK_EVENTS.has(event)
3706
+ (event) => !supported.has(event) && !isBestEffortHookEvent(event, hooks[event])
3594
3707
  );
3595
3708
  }
3596
3709
  function createUnsupportedHookWarning(event, target34, supportedEvents, options) {
@@ -7127,6 +7240,21 @@ var init_embedded_rules = __esm({
7127
7240
  }
7128
7241
  });
7129
7242
 
7243
+ // src/canonical/features/syntax-error.ts
7244
+ function failSyntax(filePath2, cause, onParseError) {
7245
+ const error = new CanonicalParseError(filePath2, cause);
7246
+ if (onParseError !== void 0) {
7247
+ onParseError(error, filePath2);
7248
+ return null;
7249
+ }
7250
+ throw error;
7251
+ }
7252
+ var init_syntax_error = __esm({
7253
+ "src/canonical/features/syntax-error.ts"() {
7254
+ init_errors();
7255
+ }
7256
+ });
7257
+
7130
7258
  // src/canonical/features/mcp.ts
7131
7259
  function parseStringMap(raw) {
7132
7260
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -7209,14 +7337,14 @@ function stripJsonComments(text) {
7209
7337
  }
7210
7338
  return result2;
7211
7339
  }
7212
- async function parseMcp(mcpPath) {
7340
+ async function parseMcp(mcpPath, onParseError) {
7213
7341
  const content = await readFileSafe(mcpPath);
7214
7342
  if (!content) return null;
7215
7343
  let parsed;
7216
7344
  try {
7217
7345
  parsed = JSON.parse(stripJsonComments(content));
7218
- } catch {
7219
- return null;
7346
+ } catch (err) {
7347
+ return failSyntax(mcpPath, err, onParseError);
7220
7348
  }
7221
7349
  if (!parsed || typeof parsed !== "object") return null;
7222
7350
  const mcpServersRaw = parsed.mcpServers;
@@ -7231,6 +7359,7 @@ async function parseMcp(mcpPath) {
7231
7359
  }
7232
7360
  var init_mcp = __esm({
7233
7361
  "src/canonical/features/mcp.ts"() {
7362
+ init_syntax_error();
7234
7363
  init_fs();
7235
7364
  }
7236
7365
  });
@@ -9010,10 +9139,10 @@ var init_claude_code2 = __esm({
9010
9139
  skillDir: ".claude/skills",
9011
9140
  managedOutputs: {
9012
9141
  dirs: [".claude/agents", ".claude/commands", ".claude/rules", ".claude/skills"],
9013
- // CLAUDE_NESTED_ROOT is the pre-migration project location; listing it here lets
9014
- // `cleanupStaleGeneratedOutputs` evict a leftover `.claude/CLAUDE.md` once generation
9142
+ files: [CLAUDE_ROOT, ".claudeignore"],
9143
+ // CLAUDE_NESTED_ROOT is the pre-migration project location: evicted once a run
9015
9144
  // writes the root `CLAUDE.md`, so Claude Code never concatenates both into context.
9016
- files: [CLAUDE_ROOT, CLAUDE_NESTED_ROOT, ".claudeignore"],
9145
+ supersededFiles: [CLAUDE_NESTED_ROOT],
9017
9146
  // `.mcp.json` is the shared project MCP file teams hand-commit and
9018
9147
  // deepagents-cli writes too; agentsmesh owns only `mcpServers` in it.
9019
9148
  // `.claude/settings.json` is co-owned through the `SETTINGS_JSON_PATHS`
@@ -10244,9 +10373,8 @@ var init_cline2 = __esm({
10244
10373
  };
10245
10374
  }
10246
10375
  });
10247
- function ruleSlug2(source) {
10248
- return basename(source, ".md");
10249
- }
10376
+
10377
+ // src/targets/codex-cli/codex-rule-paths.ts
10250
10378
  function directoryFromGlob(glob) {
10251
10379
  let normalized = glob.trim();
10252
10380
  if (normalized.startsWith("./")) normalized = normalized.slice(2);
@@ -10267,12 +10395,12 @@ function codexRuleDirectory(rule) {
10267
10395
  const dir = directoryFromGlob(glob);
10268
10396
  if (dir) return dir;
10269
10397
  }
10270
- return ruleSlug2(rule.source);
10398
+ return null;
10271
10399
  }
10272
10400
  function codexNestedAgentsPath(rule) {
10273
10401
  const dir = codexRuleDirectory(rule);
10274
10402
  const filename = rule.codexInstructionVariant === "override" ? "AGENTS.override.md" : "AGENTS.md";
10275
- return `${dir}/${filename}`;
10403
+ return dir === null ? filename : `${dir}/${filename}`;
10276
10404
  }
10277
10405
  var GLOB_METACHAR;
10278
10406
  var init_codex_rule_paths = __esm({
@@ -10280,10 +10408,9 @@ var init_codex_rule_paths = __esm({
10280
10408
  GLOB_METACHAR = /[*?[\]]/;
10281
10409
  }
10282
10410
  });
10283
-
10284
- // src/targets/codebuff/rule-paths.ts
10285
10411
  function codebuffNestedKnowledgePath(rule) {
10286
- return `${codexRuleDirectory(rule)}/AGENTS.md`;
10412
+ const dir = codexRuleDirectory(rule) ?? basename(rule.source, ".md");
10413
+ return `${dir}/AGENTS.md`;
10287
10414
  }
10288
10415
  var init_rule_paths = __esm({
10289
10416
  "src/targets/codebuff/rule-paths.ts"() {
@@ -10894,6 +11021,9 @@ function eligibleAdvisoryRules(canonical) {
10894
11021
  return rule.targets.length === 0 || rule.targets.includes("codex-cli");
10895
11022
  });
10896
11023
  }
11024
+ function isRootEmbedded(rule) {
11025
+ return codexNestedAgentsPath(rule) === AGENTS_MD;
11026
+ }
10897
11027
  function groupByNestedPath2(rules) {
10898
11028
  const groups = /* @__PURE__ */ new Map();
10899
11029
  for (const rule of rules) {
@@ -10906,9 +11036,11 @@ function groupByNestedPath2(rules) {
10906
11036
  }
10907
11037
  function generateRules9(canonical) {
10908
11038
  const root = canonical.rules.find((r) => r.root);
11039
+ const advisory = eligibleAdvisoryRules(canonical);
10909
11040
  const outputs = [];
10910
11041
  if (root) {
10911
- outputs.push({ path: AGENTS_MD, content: root.body.trim() });
11042
+ const content = appendEmbeddedRulesBlock(root.body.trim(), advisory.filter(isRootEmbedded));
11043
+ outputs.push({ path: AGENTS_MD, content });
10912
11044
  }
10913
11045
  for (const rule of canonical.rules) {
10914
11046
  if (rule.root) continue;
@@ -10920,7 +11052,8 @@ function generateRules9(canonical) {
10920
11052
  content: toSafeCodexRulesContent(rule.body)
10921
11053
  });
10922
11054
  }
10923
- for (const [path, rules] of groupByNestedPath2(eligibleAdvisoryRules(canonical))) {
11055
+ const nested = advisory.filter((rule) => !isRootEmbedded(rule));
11056
+ for (const [path, rules] of groupByNestedPath2(nested)) {
10924
11057
  const content = rules.map((rule) => rule.body.trim()).filter((body) => body.length > 0).join("\n\n");
10925
11058
  outputs.push({ path, content });
10926
11059
  }
@@ -11745,6 +11878,23 @@ var init_linter9 = __esm({
11745
11878
  });
11746
11879
 
11747
11880
  // src/targets/codex-cli/lint.ts
11881
+ function lintAgents3(canonical) {
11882
+ const diagnostics = [];
11883
+ for (const agent of canonical.agents) {
11884
+ const dropped = CODEX_DROPPED_AGENT_FIELDS.filter(
11885
+ (field) => hasAgentValue(agent, field)
11886
+ ).sort();
11887
+ if (dropped.length === 0) continue;
11888
+ diagnostics.push(
11889
+ createWarning(
11890
+ agent.source,
11891
+ "codex-cli",
11892
+ `Codex agent TOML supports name, description, developer_instructions, model, sandbox_mode and mcp_servers; canonical ${dropped.join(", ")} are not projected to ${CODEX_AGENTS_DIR}/${agent.name}.toml.`
11893
+ )
11894
+ );
11895
+ }
11896
+ return diagnostics;
11897
+ }
11748
11898
  function lintMcp4(canonical) {
11749
11899
  if (!canonical.mcp || Object.keys(canonical.mcp.mcpServers).length === 0) return [];
11750
11900
  const diagnostics = [];
@@ -11776,11 +11926,21 @@ function lintHooks7(canonical) {
11776
11926
  (event) => createUnsupportedHookWarning(event, "codex-cli", CODEX_SUPPORTED_HOOK_EVENTS)
11777
11927
  );
11778
11928
  }
11929
+ var CODEX_DROPPED_AGENT_FIELDS;
11779
11930
  var init_lint8 = __esm({
11780
11931
  "src/targets/codex-cli/lint.ts"() {
11781
11932
  init_helpers();
11782
11933
  init_mcp_servers();
11934
+ init_agents_format();
11783
11935
  init_constants10();
11936
+ CODEX_DROPPED_AGENT_FIELDS = [
11937
+ "tools",
11938
+ "disallowedTools",
11939
+ "maxTurns",
11940
+ "hooks",
11941
+ "skills",
11942
+ "memory"
11943
+ ];
11784
11944
  }
11785
11945
  });
11786
11946
 
@@ -11855,6 +12015,7 @@ var init_codex_cli2 = __esm({
11855
12015
  generateMcp: generateMcp6,
11856
12016
  generateHooks: generateHooks6,
11857
12017
  generatePermissions: generatePermissions7,
12018
+ lint: lintAgents3,
11858
12019
  importFrom: importFromCodex
11859
12020
  };
11860
12021
  project6 = {
@@ -11902,7 +12063,7 @@ var init_codex_cli2 = __esm({
11902
12063
  },
11903
12064
  rewriteGeneratedPath(path) {
11904
12065
  if (path === AGENTS_MD) return CODEX_GLOBAL_AGENTS_MD;
11905
- if (/\/AGENTS(\.override)?\.md$/.test(path)) return null;
12066
+ if (/(^|\/)AGENTS(\.override)?\.md$/.test(path)) return null;
11906
12067
  if (path.startsWith(`${CODEX_INSTRUCTIONS_DIR}/`)) return null;
11907
12068
  return path;
11908
12069
  },
@@ -12558,7 +12719,7 @@ function hasValue(agent, field) {
12558
12719
  if (value && typeof value === "object") return Object.keys(value).length > 0;
12559
12720
  return false;
12560
12721
  }
12561
- function lintAgents3(canonical) {
12722
+ function lintAgents4(canonical) {
12562
12723
  const diagnostics = [];
12563
12724
  for (const agent of canonical.agents) {
12564
12725
  const dropped = DROPPED_FIELDS.filter((field) => hasValue(agent, field)).sort();
@@ -12726,7 +12887,7 @@ var init_continue2 = __esm({
12726
12887
  generateHooks: generateHooks7,
12727
12888
  generateIgnore: generateIgnore8,
12728
12889
  // Feature-independent lint hook: agent warnings must not hang off `rules`.
12729
- lint: lintAgents3,
12890
+ lint: lintAgents4,
12730
12891
  importFrom: importFromContinue
12731
12892
  };
12732
12893
  descriptor10 = {
@@ -12983,7 +13144,7 @@ var init_hook_format = __esm({
12983
13144
  init_hook_entry();
12984
13145
  }
12985
13146
  });
12986
- function ruleSlug3(source) {
13147
+ function ruleSlug2(source) {
12987
13148
  const name = basename(source, ".md");
12988
13149
  return name === "_root" ? "root" : name;
12989
13150
  }
@@ -13017,7 +13178,7 @@ function generateRules11(canonical) {
13017
13178
  if (rule.root) continue;
13018
13179
  if (rule.targets.length > 0 && !rule.targets.includes("copilot")) continue;
13019
13180
  if (rule.globs.length === 0) continue;
13020
- const slug = ruleSlug3(rule.source);
13181
+ const slug = ruleSlug2(rule.source);
13021
13182
  const frontmatter = {
13022
13183
  description: rule.description || void 0,
13023
13184
  applyTo: rule.globs.length === 1 ? rule.globs[0] : rule.globs
@@ -14506,13 +14667,18 @@ var init_rules2 = __esm({
14506
14667
 
14507
14668
  // src/targets/cursor/generator/commands.ts
14508
14669
  function generateCommands13(canonical) {
14509
- return canonical.commands.map((cmd) => ({
14510
- path: `${CURSOR_COMMANDS_DIR}/${cmd.name}.md`,
14511
- content: cmd.body.trim() || ""
14512
- }));
14670
+ return canonical.commands.map((cmd) => {
14671
+ const frontmatter = {};
14672
+ if (cmd.description) frontmatter.description = cmd.description;
14673
+ return {
14674
+ path: `${CURSOR_COMMANDS_DIR}/${cmd.name}.md`,
14675
+ content: serializeFrontmatter(frontmatter, cmd.body.trim() || "")
14676
+ };
14677
+ });
14513
14678
  }
14514
14679
  var init_commands = __esm({
14515
14680
  "src/targets/cursor/generator/commands.ts"() {
14681
+ init_markdown();
14516
14682
  init_constants13();
14517
14683
  }
14518
14684
  });
@@ -14607,7 +14773,7 @@ var init_permissions3 = __esm({
14607
14773
  // src/targets/cursor/hook-format.ts
14608
14774
  function unmappedCursorHookEvents(hooks) {
14609
14775
  return Object.keys(hooks).filter(
14610
- (event) => Array.isArray(hooks[event]) && hooks[event].length > 0 && !(event in CANONICAL_TO_CURSOR) && !BEST_EFFORT_HOOK_EVENTS.has(event)
14776
+ (event) => Array.isArray(hooks[event]) && hooks[event].length > 0 && !(event in CANONICAL_TO_CURSOR) && !isBestEffortHookEvent(event, hooks[event])
14611
14777
  );
14612
14778
  }
14613
14779
  function toCursorHooks(hooks) {
@@ -15045,14 +15211,11 @@ async function hasGlobalCursorArtifacts(projectRoot) {
15045
15211
  join(projectRoot, CURSOR_GLOBAL_USER_RULES),
15046
15212
  join(projectRoot, CURSOR_MCP),
15047
15213
  join(projectRoot, CURSOR_HOOKS),
15048
- join(projectRoot, CURSOR_IGNORE),
15049
- join(projectRoot, CURSOR_SKILLS_DIR),
15050
- join(projectRoot, CURSOR_AGENTS_DIR),
15051
- join(projectRoot, CURSOR_COMMANDS_DIR)
15214
+ join(projectRoot, CURSOR_IGNORE)
15052
15215
  ];
15053
15216
  for (const p of candidates) {
15054
- const stat8 = await readFileSafe(p);
15055
- if (stat8 !== null && stat8.trim() !== "") return true;
15217
+ const content = await readFileSafe(p);
15218
+ if (content !== null && content.trim() !== "") return true;
15056
15219
  }
15057
15220
  const skillFiles = await readDirRecursiveNoSymlinks(join(projectRoot, CURSOR_SKILLS_DIR));
15058
15221
  if (skillFiles.some((f) => f.endsWith(".md"))) return true;
@@ -15339,11 +15502,11 @@ function lintHooks10(canonical) {
15339
15502
  ];
15340
15503
  }
15341
15504
  function lintCommands6(canonical) {
15342
- return canonical.commands.filter((command) => command.description.length > 0 || command.allowedTools.length > 0).map(
15505
+ return canonical.commands.filter((command) => command.allowedTools.length > 0).map(
15343
15506
  (command) => createWarning(
15344
15507
  command.source,
15345
15508
  "cursor",
15346
- "Cursor command files are plain Markdown; command description and allowed-tools metadata are not projected."
15509
+ "Cursor command files project only description frontmatter; allowed-tools metadata is not projected."
15347
15510
  )
15348
15511
  );
15349
15512
  }
@@ -15676,7 +15839,7 @@ var init_mcp_merge4 = __esm({
15676
15839
  // src/targets/deepagents-cli/hooks-format.ts
15677
15840
  function unmappedDeepagentsHookEvents(hooks) {
15678
15841
  return Object.keys(hooks).filter(
15679
- (event) => Array.isArray(hooks[event]) && hooks[event].length > 0 && !(event in CANONICAL_TO_DEEPAGENTS) && !BEST_EFFORT_HOOK_EVENTS.has(event)
15842
+ (event) => Array.isArray(hooks[event]) && hooks[event].length > 0 && !(event in CANONICAL_TO_DEEPAGENTS) && !isBestEffortHookEvent(event, hooks[event])
15680
15843
  );
15681
15844
  }
15682
15845
  function toDeepagentsHooks(hooks) {
@@ -16244,7 +16407,8 @@ var init_deepagents_cli2 = __esm({
16244
16407
  }
16245
16408
  },
16246
16409
  buildImportPaths: buildDeepagentsCliImportPaths,
16247
- detectionPaths: [DEEPAGENTS_CLI_ROOT_FILE, DEEPAGENTS_CLI_MCP_FILE]
16410
+ // `.mcp.json` is co-owned with claude-code (agentsmesh writes it), so it must not enroll this target.
16411
+ detectionPaths: [DEEPAGENTS_CLI_ROOT_FILE]
16248
16412
  };
16249
16413
  }
16250
16414
  });
@@ -20222,7 +20386,8 @@ var init_layout8 = __esm({
20222
20386
  skillDir: KIMI_CODE_SKILLS_DIR,
20223
20387
  managedOutputs: {
20224
20388
  dirs: [KIMI_CODE_AGENTS_DIR, KIMI_CODE_SKILLS_DIR],
20225
- files: [KIMI_CODE_ROOT_FILE, KIMI_CODE_NESTED_ROOT_FILE],
20389
+ files: [KIMI_CODE_ROOT_FILE],
20390
+ supersededFiles: [KIMI_CODE_NESTED_ROOT_FILE],
20226
20391
  // Kimi Code's own MCP config, in the same directory as the credential-
20227
20392
  // bearing config.toml this layout already refuses to delete.
20228
20393
  coOwnedFiles: [KIMI_CODE_MCP_FILE]
@@ -20870,7 +21035,7 @@ function lintMcp9(canonical) {
20870
21035
  }
20871
21036
  return diagnostics;
20872
21037
  }
20873
- function lintAgents4(canonical) {
21038
+ function lintAgents5(canonical) {
20874
21039
  return canonical.agents.flatMap((agent) => {
20875
21040
  const dropped = DROPPED_AGENT_FIELDS.filter(([, has]) => has(agent)).map(([field]) => field);
20876
21041
  if (dropped.length === 0) return [];
@@ -20961,7 +21126,7 @@ var init_kimi_code2 = __esm({
20961
21126
  generateHooks: generateHooks14,
20962
21127
  generatePermissions: generatePermissions16,
20963
21128
  importFrom: importFromKimiCode,
20964
- lint: lintAgents4
21129
+ lint: lintAgents5
20965
21130
  };
20966
21131
  capabilities9 = {
20967
21132
  rules: "native",
@@ -23259,7 +23424,7 @@ function lintAgentFields(agent) {
23259
23424
  )
23260
23425
  ];
23261
23426
  }
23262
- function lintAgents5(canonical) {
23427
+ function lintAgents6(canonical) {
23263
23428
  const diagnostics = [];
23264
23429
  for (const agent of canonical.agents) {
23265
23430
  diagnostics.push(...lintAgentFields(agent));
@@ -23321,7 +23486,7 @@ var init_openhands2 = __esm({
23321
23486
  generatePermissions: generatePermissions19,
23322
23487
  importFrom: importFromOpenhands,
23323
23488
  // Ungated by feature, so agent-only feature sets still get the warning.
23324
- lint: lintAgents5
23489
+ lint: lintAgents6
23325
23490
  };
23326
23491
  descriptor24 = {
23327
23492
  id: OPENHANDS_TARGET,
@@ -27136,7 +27301,7 @@ var init_constants34 = __esm({
27136
27301
  WINDSURF_GLOBAL_AGENTS_SKILLS_DIR = ".agents/skills";
27137
27302
  }
27138
27303
  });
27139
- function ruleSlug4(source) {
27304
+ function ruleSlug3(source) {
27140
27305
  const name = basename(source, ".md");
27141
27306
  return name === "_root" ? "root" : name;
27142
27307
  }
@@ -27157,7 +27322,7 @@ function generateRules32(canonical) {
27157
27322
  for (const rule of canonical.rules) {
27158
27323
  if (rule.root) continue;
27159
27324
  if (rule.targets.length > 0 && !rule.targets.includes("windsurf")) continue;
27160
- const slug = ruleSlug4(rule.source);
27325
+ const slug = ruleSlug3(rule.source);
27161
27326
  const normalizedTrigger = rule.trigger || (rule.globs.length > 0 ? "glob" : void 0);
27162
27327
  const frontmatter = {
27163
27328
  description: rule.description || void 0,
@@ -27261,19 +27426,34 @@ var init_mcp4 = __esm({
27261
27426
  }
27262
27427
  });
27263
27428
 
27264
- // src/targets/windsurf/generator/hooks.ts
27429
+ // src/targets/windsurf/hook-events.ts
27265
27430
  function windsurfEventName(event) {
27266
- const explicit = {
27267
- PreToolUse: "pre_tool_use",
27268
- PostToolUse: "post_tool_use",
27269
- Notification: "notification",
27270
- UserPromptSubmit: "user_prompt_submit",
27271
- SubagentStart: "subagent_start",
27272
- SubagentStop: "subagent_stop"
27273
- };
27274
- if (explicit[event]) return explicit[event];
27275
27431
  return event.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
27276
27432
  }
27433
+ function canonicalHookEventName(event) {
27434
+ if (KNOWN_CANONICAL_HOOK_EVENTS.includes(event)) return event;
27435
+ return WINDSURF_TO_CANONICAL.get(event) ?? null;
27436
+ }
27437
+ var KNOWN_CANONICAL_HOOK_EVENTS, WINDSURF_TO_CANONICAL;
27438
+ var init_hook_events = __esm({
27439
+ "src/targets/windsurf/hook-events.ts"() {
27440
+ init_hook_types();
27441
+ KNOWN_CANONICAL_HOOK_EVENTS = [
27442
+ "PreToolUse",
27443
+ "PostToolUse",
27444
+ "Notification",
27445
+ "UserPromptSubmit",
27446
+ "SubagentStart",
27447
+ "SubagentStop",
27448
+ ...BEST_EFFORT_HOOK_EVENTS
27449
+ ];
27450
+ WINDSURF_TO_CANONICAL = new Map(
27451
+ KNOWN_CANONICAL_HOOK_EVENTS.map((event) => [windsurfEventName(event), event])
27452
+ );
27453
+ }
27454
+ });
27455
+
27456
+ // src/targets/windsurf/generator/hooks.ts
27277
27457
  function toWindsurfHooks(hooks) {
27278
27458
  const result2 = {};
27279
27459
  for (const [event, entries] of Object.entries(hooks)) {
@@ -27301,6 +27481,7 @@ var init_hooks4 = __esm({
27301
27481
  "src/targets/windsurf/generator/hooks.ts"() {
27302
27482
  init_hook_command();
27303
27483
  init_constants34();
27484
+ init_hook_events();
27304
27485
  }
27305
27486
  });
27306
27487
 
@@ -27432,6 +27613,57 @@ var init_skills_adapter5 = __esm({
27432
27613
  init_constants34();
27433
27614
  }
27434
27615
  });
27616
+ function toHookEntry(raw) {
27617
+ if (!raw || typeof raw !== "object") return null;
27618
+ const obj = raw;
27619
+ const matcher = obj.matcher;
27620
+ if (typeof matcher !== "string") return null;
27621
+ const command = getHookText(obj);
27622
+ if (!command) return null;
27623
+ const type = typeof obj.type === "string" && VALID_TYPES.includes(obj.type) ? obj.type : void 0;
27624
+ const timeout = typeof obj.timeout === "number" && Number.isFinite(obj.timeout) ? obj.timeout : void 0;
27625
+ const prompt = getHookPrompt(obj) || void 0;
27626
+ return {
27627
+ matcher,
27628
+ command,
27629
+ ...timeout !== void 0 && { timeout },
27630
+ ...type && { type },
27631
+ ...prompt && { prompt }
27632
+ };
27633
+ }
27634
+ async function parseHooks(hooksPath, onParseError) {
27635
+ const content = await readFileSafe(hooksPath);
27636
+ if (content === null) return null;
27637
+ if (!content.trim()) return {};
27638
+ let parsed;
27639
+ try {
27640
+ parsed = parse(content);
27641
+ } catch (err) {
27642
+ return failSyntax(hooksPath, err, onParseError);
27643
+ }
27644
+ if (!parsed || typeof parsed !== "object") return null;
27645
+ const result2 = {};
27646
+ const obj = parsed;
27647
+ for (const [key, val] of Object.entries(obj)) {
27648
+ if (!Array.isArray(val)) continue;
27649
+ const entries = [];
27650
+ for (const item of val) {
27651
+ const entry = toHookEntry(item);
27652
+ if (entry) entries.push(entry);
27653
+ }
27654
+ if (entries.length > 0) result2[key] = entries;
27655
+ }
27656
+ return result2;
27657
+ }
27658
+ var VALID_TYPES;
27659
+ var init_hooks5 = __esm({
27660
+ "src/canonical/features/hooks.ts"() {
27661
+ init_syntax_error();
27662
+ init_fs();
27663
+ init_hook_command();
27664
+ VALID_TYPES = ["command", "prompt"];
27665
+ }
27666
+ });
27435
27667
  async function importWindsurfHooks(projectRoot, results) {
27436
27668
  const hooksPath = join(projectRoot, WINDSURF_HOOKS_FILE);
27437
27669
  const hooksContent = await readFileSafe(hooksPath);
@@ -27439,9 +27671,10 @@ async function importWindsurfHooks(projectRoot, results) {
27439
27671
  try {
27440
27672
  const parsed = JSON.parse(hooksContent);
27441
27673
  if (!parsed.hooks || typeof parsed.hooks !== "object" || Array.isArray(parsed.hooks)) return;
27442
- const canonical = windsurfHooksToCanonical(parsed.hooks);
27443
- if (Object.keys(canonical).length === 0) return;
27444
27674
  const destPath = join(projectRoot, WINDSURF_CANONICAL_HOOKS);
27675
+ const existing = await parseHooks(destPath) ?? {};
27676
+ const canonical = windsurfHooksToCanonical(parsed.hooks, existing);
27677
+ if (Object.keys(canonical).length === 0) return;
27445
27678
  await mkdirp(dirname(destPath));
27446
27679
  await writeFileAtomic(destPath, stringify(canonical));
27447
27680
  results.push({
@@ -27453,54 +27686,63 @@ async function importWindsurfHooks(projectRoot, results) {
27453
27686
  } catch {
27454
27687
  }
27455
27688
  }
27456
- function canonicalHookEventName(event) {
27457
- const explicit = {
27458
- pre_tool_use: "PreToolUse",
27459
- post_tool_use: "PostToolUse",
27460
- notification: "Notification",
27461
- user_prompt_submit: "UserPromptSubmit",
27462
- subagent_start: "SubagentStart",
27463
- subagent_stop: "SubagentStop"
27464
- };
27465
- return explicit[event] ?? event;
27689
+ function preservedMatcher(existing, event, command) {
27690
+ const match = existing[event]?.find((entry) => entry.command === command);
27691
+ return match?.matcher ?? WILDCARD_MATCHER;
27692
+ }
27693
+ function legacyEntries(entry) {
27694
+ const matcher = typeof entry.matcher === "string" && entry.matcher.trim() ? entry.matcher : WILDCARD_MATCHER;
27695
+ const hooksList = Array.isArray(entry.hooks) ? entry.hooks : [];
27696
+ const out2 = [];
27697
+ for (const item of hooksList) {
27698
+ if (!item || typeof item !== "object") continue;
27699
+ const hook = item;
27700
+ const command = typeof hook.command === "string" ? hook.command : typeof hook.prompt === "string" ? hook.prompt : "";
27701
+ if (!command.trim()) continue;
27702
+ const canonical = {
27703
+ matcher,
27704
+ type: hook.type === "prompt" ? "prompt" : "command",
27705
+ command
27706
+ };
27707
+ if (typeof hook.timeout === "number") canonical.timeout = hook.timeout;
27708
+ out2.push(canonical);
27709
+ }
27710
+ return out2;
27466
27711
  }
27467
- function windsurfHooksToCanonical(hooks) {
27712
+ function windsurfHooksToCanonical(hooks, existing) {
27468
27713
  const result2 = {};
27469
27714
  for (const [event, entries] of Object.entries(hooks)) {
27470
27715
  if (!Array.isArray(entries)) continue;
27471
27716
  const mappedEvent = canonicalHookEventName(event);
27717
+ if (mappedEvent === null) continue;
27472
27718
  const canonicalEntries = [];
27473
27719
  for (const entry of entries) {
27474
27720
  if (!entry || typeof entry !== "object") continue;
27475
27721
  const e = entry;
27476
27722
  if (typeof e.command === "string" && e.command.trim()) {
27477
27723
  canonicalEntries.push({
27478
- matcher: ".*",
27724
+ matcher: preservedMatcher(existing, mappedEvent, e.command),
27479
27725
  type: "command",
27480
27726
  command: e.command
27481
27727
  });
27482
27728
  continue;
27483
27729
  }
27484
- const matcher = typeof e.matcher === "string" && e.matcher.trim() ? e.matcher : ".*";
27485
- const hooksList = Array.isArray(e.hooks) ? e.hooks : [];
27486
- for (const item of hooksList) {
27487
- if (!item || typeof item !== "object") continue;
27488
- const hook = item;
27489
- const command = typeof hook.command === "string" ? hook.command : typeof hook.prompt === "string" ? hook.prompt : "";
27490
- if (!command.trim()) continue;
27491
- const canonical = {
27492
- matcher,
27493
- type: hook.type === "prompt" ? "prompt" : "command",
27494
- command
27495
- };
27496
- if (typeof hook.timeout === "number") canonical.timeout = hook.timeout;
27497
- canonicalEntries.push(canonical);
27498
- }
27730
+ canonicalEntries.push(...legacyEntries(e));
27499
27731
  }
27500
27732
  if (canonicalEntries.length > 0) result2[mappedEvent] = canonicalEntries;
27501
27733
  }
27502
27734
  return result2;
27503
27735
  }
27736
+ var WILDCARD_MATCHER;
27737
+ var init_importer_hooks2 = __esm({
27738
+ "src/targets/windsurf/importer-hooks.ts"() {
27739
+ init_hooks5();
27740
+ init_fs();
27741
+ init_constants34();
27742
+ init_hook_events();
27743
+ WILDCARD_MATCHER = "*";
27744
+ }
27745
+ });
27504
27746
  async function importWindsurfMcp(projectRoot, results) {
27505
27747
  const sourceCandidates = [WINDSURF_MCP_EXAMPLE_FILE, WINDSURF_MCP_CONFIG_FILE];
27506
27748
  for (const relPath of sourceCandidates) {
@@ -27524,8 +27766,8 @@ async function importWindsurfMcp(projectRoot, results) {
27524
27766
  }
27525
27767
  }
27526
27768
  }
27527
- var init_importer_hooks_mcp = __esm({
27528
- "src/targets/windsurf/importer-hooks-mcp.ts"() {
27769
+ var init_importer_mcp = __esm({
27770
+ "src/targets/windsurf/importer-mcp.ts"() {
27529
27771
  init_fs();
27530
27772
  init_constants34();
27531
27773
  }
@@ -27675,7 +27917,8 @@ var init_importer32 = __esm({
27675
27917
  init_constants34();
27676
27918
  init_importer_workflows();
27677
27919
  init_skills_adapter5();
27678
- init_importer_hooks_mcp();
27920
+ init_importer_hooks2();
27921
+ init_importer_mcp();
27679
27922
  }
27680
27923
  });
27681
27924
 
@@ -27760,9 +28003,28 @@ function lintPermissions22(canonical) {
27760
28003
  )
27761
28004
  ];
27762
28005
  }
28006
+ function lintHooks23(canonical) {
28007
+ if (!canonical.hooks) return [];
28008
+ const diagnostics = [];
28009
+ for (const [event, entries] of Object.entries(canonical.hooks)) {
28010
+ for (const entry of entries ?? []) {
28011
+ if (WILDCARD_MATCHERS.has(entry.matcher.trim())) continue;
28012
+ diagnostics.push(
28013
+ createWarning(
28014
+ ".agentsmesh/hooks.yaml",
28015
+ "windsurf",
28016
+ `Windsurf hooks have no matcher field; ${event} hook "${entry.command}" runs on every ${event} event (matcher "${entry.matcher}" is not projected).`
28017
+ )
28018
+ );
28019
+ }
28020
+ }
28021
+ return diagnostics;
28022
+ }
28023
+ var WILDCARD_MATCHERS;
27763
28024
  var init_lint31 = __esm({
27764
28025
  "src/targets/windsurf/lint.ts"() {
27765
28026
  init_helpers();
28027
+ WILDCARD_MATCHERS = /* @__PURE__ */ new Set(["", "*", ".*"]);
27766
28028
  }
27767
28029
  });
27768
28030
 
@@ -27918,6 +28180,7 @@ var init_windsurf2 = __esm({
27918
28180
  lintRules: lintRules32,
27919
28181
  lint: {
27920
28182
  commands: lintCommands10,
28183
+ hooks: lintHooks23,
27921
28184
  mcp: lintMcp13,
27922
28185
  permissions: lintPermissions22
27923
28186
  },
@@ -29787,6 +30050,7 @@ function ruleNameFromSource(source) {
29787
30050
  }
29788
30051
 
29789
30052
  // src/core/generate/collision.ts
30053
+ init_fs_text_encoding();
29790
30054
  init_target_ids();
29791
30055
  var AGENTS_SUFFIX = "AGENTS.md";
29792
30056
  function statusRank(status) {
@@ -29895,7 +30159,7 @@ function assertNoCaseOnlyPathCollisions(results) {
29895
30159
  }
29896
30160
  }
29897
30161
  function refreshResultStatus(result2) {
29898
- const status = result2.currentContent === void 0 ? "created" : result2.currentContent !== result2.content ? "updated" : "unchanged";
30162
+ const status = result2.currentContent === void 0 ? "created" : normalizeTextPayload(result2.path, result2.currentContent) !== normalizeTextPayload(result2.path, result2.content) ? "updated" : "unchanged";
29899
30163
  return result2.status === status ? result2 : { ...result2, status };
29900
30164
  }
29901
30165
 
@@ -30300,8 +30564,46 @@ function mergeLocalConfig(project26, local) {
30300
30564
  if (Array.isArray(local.extends) && local.extends.length > 0) {
30301
30565
  merged.extends = [...project26.extends ?? [], ...local.extends];
30302
30566
  }
30567
+ if (Array.isArray(local.plugins)) {
30568
+ merged.plugins = mergeById(project26.plugins, local.plugins);
30569
+ }
30570
+ if (Array.isArray(local.pluginTargets)) {
30571
+ merged.pluginTargets = [.../* @__PURE__ */ new Set([...project26.pluginTargets, ...local.pluginTargets])];
30572
+ }
30573
+ if (typeof local.collaboration === "object" && local.collaboration !== null && !Array.isArray(local.collaboration)) {
30574
+ merged.collaboration = local.collaboration;
30575
+ }
30576
+ warnUnhandledLocalKeys(local);
30303
30577
  return merged;
30304
30578
  }
30579
+ var LOCAL_KEYS = /* @__PURE__ */ new Set([
30580
+ "version",
30581
+ "targets",
30582
+ "features",
30583
+ "overrides",
30584
+ "conversions",
30585
+ "extends",
30586
+ "plugins",
30587
+ "pluginTargets",
30588
+ "collaboration"
30589
+ ]);
30590
+ function warnUnhandledLocalKeys(local) {
30591
+ const unknown = Object.keys(local).filter((key) => !LOCAL_KEYS.has(key));
30592
+ if (unknown.length === 0) return;
30593
+ logger.warn(
30594
+ `agentsmesh.local.yaml: ignoring unknown key(s) ${unknown.join(", ")}; supported keys are ${[...LOCAL_KEYS].join(", ")}.`
30595
+ );
30596
+ }
30597
+ function mergeById(project26, local) {
30598
+ const byId = /* @__PURE__ */ new Map();
30599
+ const anonymous = [];
30600
+ for (const entry of [...project26, ...local]) {
30601
+ const id = typeof entry === "object" && entry !== null && typeof entry.id === "string" ? entry.id : void 0;
30602
+ if (id === void 0) anonymous.push(entry);
30603
+ else byId.set(id, entry);
30604
+ }
30605
+ return [...byId.values(), ...anonymous];
30606
+ }
30305
30607
  async function loadConfigFromExactDir(configDir) {
30306
30608
  const configPath = join(configDir, CONFIG_FILENAME);
30307
30609
  let config = await loadConfig(configPath);
@@ -30790,24 +31092,14 @@ async function sweepStaleCache(cacheDir, maxAgeMs) {
30790
31092
 
30791
31093
  // src/config/remote/remote-fetcher.ts
30792
31094
  var MAX_CACHE_KEY_LENGTH = 80;
31095
+ var CACHE_KEY_HASH_LENGTH = 12;
30793
31096
  function buildCacheKey(provider, identifier, ref) {
30794
31097
  const safe = (value) => value.replace(/[^a-zA-Z0-9_.-]/g, "_").replace(/^\.+/, "_");
30795
- let key;
30796
- if (provider === "github") {
30797
- const [org, repo] = identifier.split("/", 2);
30798
- if (org && repo) {
30799
- key = `${safe(org)}--${safe(repo)}--${safe(ref)}`;
30800
- } else {
30801
- key = `${safe(provider)}__${safe(identifier)}__${safe(ref)}`;
30802
- }
30803
- } else {
30804
- key = `${safe(provider)}__${safe(identifier)}__${safe(ref)}`;
30805
- }
30806
- if (key.length > MAX_CACHE_KEY_LENGTH) {
30807
- const hash = createHash("sha256").update(key).digest("hex").slice(0, 16);
30808
- key = `${key.slice(0, MAX_CACHE_KEY_LENGTH - 18)}--${hash}`;
30809
- }
30810
- return key;
31098
+ const [org, repo] = provider === "github" ? identifier.split("/", 2) : [];
31099
+ const readable = org && repo ? `${safe(org)}--${safe(repo)}--${safe(ref)}` : `${safe(provider)}__${safe(identifier)}__${safe(ref)}`;
31100
+ const hash = createHash("sha256").update(`${provider}|${identifier}|${ref}`).digest("hex").slice(0, CACHE_KEY_HASH_LENGTH);
31101
+ const maxReadable = MAX_CACHE_KEY_LENGTH - CACHE_KEY_HASH_LENGTH - 2;
31102
+ return `${readable.slice(0, maxReadable)}--${hash}`;
30811
31103
  }
30812
31104
  function getCacheDir() {
30813
31105
  const env = process.env.AGENTSMESH_CACHE;
@@ -30919,6 +31211,13 @@ async function resolveExtendPaths(config, configDir, options = {}) {
30919
31211
  return result2;
30920
31212
  }
30921
31213
 
31214
+ // src/canonical/features/empty-file.ts
31215
+ function isEmptyCanonicalFile(content, path) {
31216
+ if (content.trim() !== "") return false;
31217
+ logger.warn(`Skipping empty canonical file ${path.replaceAll("\\", "/")}`);
31218
+ return true;
31219
+ }
31220
+
30922
31221
  // src/canonical/features/rules.ts
30923
31222
  init_fs();
30924
31223
  init_markdown();
@@ -31058,7 +31357,8 @@ async function parseRules(rulesDir, opts = {}) {
31058
31357
  const rules = [];
31059
31358
  for (const path of mdFiles) {
31060
31359
  const content = await readFileSafe(path);
31061
- if (!content) continue;
31360
+ if (content === null) continue;
31361
+ if (isEmptyCanonicalFile(content, path)) continue;
31062
31362
  const parsed = parseOrSkipFrontmatter(content, path, opts.onParseError);
31063
31363
  if (!parsed) continue;
31064
31364
  const { frontmatter, body } = parsed;
@@ -31112,7 +31412,8 @@ async function parseCommands(commandsDir, opts = {}) {
31112
31412
  const commands = [];
31113
31413
  for (const path of mdFiles) {
31114
31414
  const content = await readFileSafe(path);
31115
- if (!content) continue;
31415
+ if (content === null) continue;
31416
+ if (isEmptyCanonicalFile(content, path)) continue;
31116
31417
  const parsed = parseOrSkipFrontmatter(content, path, opts.onParseError);
31117
31418
  if (!parsed) continue;
31118
31419
  const { frontmatter, body } = parsed;
@@ -31172,15 +31473,14 @@ async function parseAgents(agentsDir, opts = {}) {
31172
31473
  const agents = [];
31173
31474
  for (const path of mdFiles) {
31174
31475
  const content = await readFileSafe(path);
31175
- if (!content) continue;
31476
+ if (content === null) continue;
31477
+ if (isEmptyCanonicalFile(content, path)) continue;
31176
31478
  const parsed = parseOrSkipFrontmatter(content, path, opts.onParseError);
31177
31479
  if (!parsed) continue;
31178
31480
  const { frontmatter, body } = parsed;
31179
31481
  const name = basename(path, ".md");
31180
31482
  assertCanonicalName("agent", name);
31181
- const toolsCamel = toStrArray2(frontmatter.tools);
31182
- const toolsKebab = toStrArray2(frontmatter["tools"]);
31183
- const tools = toolsCamel.length > 0 ? toolsCamel : toolsKebab;
31483
+ const tools = toStrArray2(frontmatter.tools);
31184
31484
  const disallowedCamel = toStrArray2(frontmatter.disallowedTools);
31185
31485
  const disallowedKebab = toStrArray2(frontmatter["disallowed-tools"]);
31186
31486
  const disallowedTools = disallowedCamel.length > 0 ? disallowedCamel : disallowedKebab;
@@ -31302,20 +31602,21 @@ async function parseSkills(skillsDir, opts = {}) {
31302
31602
  init_mcp();
31303
31603
 
31304
31604
  // src/canonical/features/permissions.ts
31605
+ init_syntax_error();
31305
31606
  init_fs();
31306
31607
  function ensureStringArray(val) {
31307
31608
  if (!Array.isArray(val)) return [];
31308
31609
  return val.filter((x) => typeof x === "string");
31309
31610
  }
31310
- async function parsePermissions(permissionsPath) {
31611
+ async function parsePermissions(permissionsPath, onParseError) {
31311
31612
  const content = await readFileSafe(permissionsPath);
31312
31613
  if (content === null) return null;
31313
31614
  if (!content.trim()) return { allow: [], deny: [], ask: [] };
31314
31615
  let parsed;
31315
31616
  try {
31316
31617
  parsed = parse(content);
31317
- } catch {
31318
- return null;
31618
+ } catch (err) {
31619
+ return failSyntax(permissionsPath, err, onParseError);
31319
31620
  }
31320
31621
  if (!parsed || typeof parsed !== "object") return null;
31321
31622
  const obj = parsed;
@@ -31325,52 +31626,8 @@ async function parsePermissions(permissionsPath) {
31325
31626
  return { allow, deny, ask };
31326
31627
  }
31327
31628
 
31328
- // src/canonical/features/hooks.ts
31329
- init_fs();
31330
- init_hook_command();
31331
- var VALID_TYPES = ["command", "prompt"];
31332
- function toHookEntry(raw) {
31333
- if (!raw || typeof raw !== "object") return null;
31334
- const obj = raw;
31335
- const matcher = obj.matcher;
31336
- if (typeof matcher !== "string") return null;
31337
- const command = getHookText(obj);
31338
- if (!command) return null;
31339
- const type = typeof obj.type === "string" && VALID_TYPES.includes(obj.type) ? obj.type : void 0;
31340
- const timeout = typeof obj.timeout === "number" && Number.isFinite(obj.timeout) ? obj.timeout : void 0;
31341
- const prompt = getHookPrompt(obj) || void 0;
31342
- return {
31343
- matcher,
31344
- command,
31345
- ...timeout !== void 0 && { timeout },
31346
- ...type && { type },
31347
- ...prompt && { prompt }
31348
- };
31349
- }
31350
- async function parseHooks(hooksPath) {
31351
- const content = await readFileSafe(hooksPath);
31352
- if (content === null) return null;
31353
- if (!content.trim()) return {};
31354
- let parsed;
31355
- try {
31356
- parsed = parse(content);
31357
- } catch {
31358
- return null;
31359
- }
31360
- if (!parsed || typeof parsed !== "object") return null;
31361
- const result2 = {};
31362
- const obj = parsed;
31363
- for (const [key, val] of Object.entries(obj)) {
31364
- if (!Array.isArray(val)) continue;
31365
- const entries = [];
31366
- for (const item of val) {
31367
- const entry = toHookEntry(item);
31368
- if (entry) entries.push(entry);
31369
- }
31370
- if (entries.length > 0) result2[key] = entries;
31371
- }
31372
- return result2;
31373
- }
31629
+ // src/canonical/load/loader.ts
31630
+ init_hooks5();
31374
31631
 
31375
31632
  // src/canonical/features/ignore.ts
31376
31633
  init_fs();
@@ -31397,9 +31654,9 @@ async function loadCanonicalFiles(canonicalDirOrProjectRoot, opts = {}) {
31397
31654
  parseCommands(join(canonicalDir, "commands"), opts),
31398
31655
  parseAgents(join(canonicalDir, "agents"), opts),
31399
31656
  parseSkills(join(canonicalDir, "skills"), opts),
31400
- parseMcp(join(canonicalDir, "mcp.json")),
31401
- parsePermissions(join(canonicalDir, "permissions.yaml")),
31402
- parseHooks(join(canonicalDir, "hooks.yaml")),
31657
+ parseMcp(join(canonicalDir, "mcp.json"), opts.onParseError),
31658
+ parsePermissions(join(canonicalDir, "permissions.yaml"), opts.onParseError),
31659
+ parseHooks(join(canonicalDir, "hooks.yaml"), opts.onParseError),
31403
31660
  parseIgnore(join(canonicalDir, "ignore"))
31404
31661
  ]);
31405
31662
  return {
@@ -31413,13 +31670,13 @@ async function loadCanonicalFiles(canonicalDirOrProjectRoot, opts = {}) {
31413
31670
  ignore
31414
31671
  };
31415
31672
  }
31416
- function ruleSlug5(r) {
31673
+ function ruleSlug4(r) {
31417
31674
  return basename(r.source, ".md");
31418
31675
  }
31419
31676
  function mergeCanonicalFiles(base, overlay) {
31420
- const baseRuleMap = new Map(base.rules.map((r) => [ruleSlug5(r), r]));
31677
+ const baseRuleMap = new Map(base.rules.map((r) => [ruleSlug4(r), r]));
31421
31678
  for (const r of overlay.rules) {
31422
- baseRuleMap.set(ruleSlug5(r), r);
31679
+ baseRuleMap.set(ruleSlug4(r), r);
31423
31680
  }
31424
31681
  const baseCmdMap = new Map(base.commands.map((c2) => [c2.name, c2]));
31425
31682
  for (const c2 of overlay.commands) {
@@ -32302,6 +32559,7 @@ function gateExtendElevatedArtifacts(canonical, ext) {
32302
32559
  });
32303
32560
  }
32304
32561
  init_mcp();
32562
+ init_hooks5();
32305
32563
 
32306
32564
  // src/install/pack/pack-reader.ts
32307
32565
  init_fs();
@@ -33067,88 +33325,6 @@ function collectOrphans(graph, findings) {
33067
33325
  }
33068
33326
  }
33069
33327
 
33070
- // src/lessons/ranking-text.ts
33071
- var K1 = 1.5;
33072
- var B = 0.75;
33073
- var STOP = /* @__PURE__ */ new Set([
33074
- "the",
33075
- "a",
33076
- "an",
33077
- "to",
33078
- "of",
33079
- "in",
33080
- "and",
33081
- "or",
33082
- "for",
33083
- "is",
33084
- "on",
33085
- "at",
33086
- "with",
33087
- "be",
33088
- "as",
33089
- "it",
33090
- "that",
33091
- "this",
33092
- "its",
33093
- "must"
33094
- ]);
33095
- function tokenize(text) {
33096
- return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
33097
- }
33098
- function queryTerms(query) {
33099
- const parts = [];
33100
- if (query.keyword !== void 0) parts.push(query.keyword);
33101
- if (query.file !== void 0) parts.push(query.file);
33102
- if (query.command !== void 0) parts.push(query.command);
33103
- return tokenize(parts.join(" "));
33104
- }
33105
- function buildCorpus(graph) {
33106
- const docs = [];
33107
- const df = /* @__PURE__ */ new Map();
33108
- let total = 0;
33109
- let n = 0;
33110
- for (const lesson of Object.values(graph.lessons)) {
33111
- if (lesson.status !== "active") continue;
33112
- const toks = tokenize(lesson.rule);
33113
- n += 1;
33114
- total += toks.length;
33115
- docs.push(toks.length);
33116
- for (const t of new Set(toks)) df.set(t, (df.get(t) ?? 0) + 1);
33117
- }
33118
- const N = Math.max(n, 1);
33119
- const idf = /* @__PURE__ */ new Map();
33120
- for (const [t, f] of df) idf.set(t, Math.log(1 + (N - f + 0.5) / (f + 0.5)));
33121
- return { idf, avgdl: total / N || 1 };
33122
- }
33123
- function bm25(terms, ruleText, corpus) {
33124
- const toks = tokenize(ruleText);
33125
- const dl = toks.length || 1;
33126
- const tf = /* @__PURE__ */ new Map();
33127
- for (const t of toks) tf.set(t, (tf.get(t) ?? 0) + 1);
33128
- let score = 0;
33129
- for (const t of new Set(terms)) {
33130
- const f = tf.get(t) ?? 0;
33131
- if (f === 0) continue;
33132
- const idf = corpus.idf.get(t);
33133
- score += idf * (f * (K1 + 1)) / (f + K1 * (1 - B + B * dl / corpus.avgdl));
33134
- }
33135
- return score;
33136
- }
33137
-
33138
- // src/lessons/keyword-signal.ts
33139
- var MAX_RECOMMENDED_KEYWORD_TOKENS = 5;
33140
- function isLowSignalKeyword(pattern) {
33141
- return tokenize(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
33142
- }
33143
- function splitRawTokens(pattern) {
33144
- return pattern.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
33145
- }
33146
- function keywordNeedleLosesTokens(pattern) {
33147
- const raw = splitRawTokens(pattern);
33148
- if (raw.length < 2) return false;
33149
- return tokenize(pattern).length !== raw.length;
33150
- }
33151
-
33152
33328
  // src/lessons/regex-linear/nfa-compile.ts
33153
33329
  var MAX_NFA_STATES = 2e3;
33154
33330
  var Builder = class {
@@ -33669,6 +33845,93 @@ function collectFanout(graph, findings) {
33669
33845
  });
33670
33846
  }
33671
33847
  }
33848
+ function normalizeRule(rule) {
33849
+ return rule.trim().replace(/\s+/g, " ").toLowerCase();
33850
+ }
33851
+
33852
+ // src/lessons/ranking-text.ts
33853
+ var K1 = 1.5;
33854
+ var B = 0.75;
33855
+ var STOP = /* @__PURE__ */ new Set([
33856
+ "the",
33857
+ "a",
33858
+ "an",
33859
+ "to",
33860
+ "of",
33861
+ "in",
33862
+ "and",
33863
+ "or",
33864
+ "for",
33865
+ "is",
33866
+ "on",
33867
+ "at",
33868
+ "with",
33869
+ "be",
33870
+ "as",
33871
+ "it",
33872
+ "that",
33873
+ "this",
33874
+ "its",
33875
+ "must"
33876
+ ]);
33877
+ function tokenize(text) {
33878
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
33879
+ }
33880
+ function queryTerms(query) {
33881
+ const parts = [];
33882
+ if (query.keyword !== void 0) parts.push(query.keyword);
33883
+ if (query.file !== void 0) parts.push(query.file);
33884
+ if (query.command !== void 0) parts.push(query.command);
33885
+ return tokenize(parts.join(" "));
33886
+ }
33887
+ function buildCorpus(graph) {
33888
+ const docs = [];
33889
+ const df = /* @__PURE__ */ new Map();
33890
+ let total = 0;
33891
+ let n = 0;
33892
+ for (const lesson of Object.values(graph.lessons)) {
33893
+ if (lesson.status !== "active") continue;
33894
+ const toks = tokenize(lesson.rule);
33895
+ n += 1;
33896
+ total += toks.length;
33897
+ docs.push(toks.length);
33898
+ for (const t of new Set(toks)) df.set(t, (df.get(t) ?? 0) + 1);
33899
+ }
33900
+ const N = Math.max(n, 1);
33901
+ const idf = /* @__PURE__ */ new Map();
33902
+ for (const [t, f] of df) idf.set(t, Math.log(1 + (N - f + 0.5) / (f + 0.5)));
33903
+ return { idf, avgdl: total / N || 1 };
33904
+ }
33905
+ function bm25(terms, ruleText, corpus) {
33906
+ const toks = tokenize(ruleText);
33907
+ const dl = toks.length || 1;
33908
+ const tf = /* @__PURE__ */ new Map();
33909
+ for (const t of toks) tf.set(t, (tf.get(t) ?? 0) + 1);
33910
+ let score = 0;
33911
+ for (const t of new Set(terms)) {
33912
+ const f = tf.get(t) ?? 0;
33913
+ if (f === 0) continue;
33914
+ const idf = corpus.idf.get(t);
33915
+ score += idf * (f * (K1 + 1)) / (f + K1 * (1 - B + B * dl / corpus.avgdl));
33916
+ }
33917
+ return score;
33918
+ }
33919
+
33920
+ // src/lessons/keyword-signal.ts
33921
+ var MAX_RECOMMENDED_KEYWORD_TOKENS = 5;
33922
+ function isLowSignalKeyword(pattern) {
33923
+ return tokenize(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
33924
+ }
33925
+ function splitRawTokens(pattern) {
33926
+ return pattern.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
33927
+ }
33928
+ function keywordNeedleLosesTokens(pattern) {
33929
+ const raw = splitRawTokens(pattern);
33930
+ if (raw.length < 2) return false;
33931
+ return tokenize(pattern).length !== raw.length;
33932
+ }
33933
+
33934
+ // src/lessons/validate-keywords.ts
33672
33935
  function collectLowSignalKeywords(graph, findings) {
33673
33936
  const activeTriggerIds2 = /* @__PURE__ */ new Set();
33674
33937
  for (const lesson of Object.values(graph.lessons)) {
@@ -33682,7 +33945,7 @@ function collectLowSignalKeywords(graph, findings) {
33682
33945
  findings.push({
33683
33946
  level: "warning",
33684
33947
  code: "LOW_SIGNAL_KEYWORD",
33685
- 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.`,
33948
+ message: `Keyword trigger "${triggerId}" carries more than ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens (${trigger.pattern}); recall matches a keyword only as a contiguous token-run in --keyword or the file/command, so it rarely fires \u2014 use a short distinctive phrase.`,
33686
33949
  triggerId
33687
33950
  });
33688
33951
  }
@@ -33707,9 +33970,62 @@ function collectStopwordKeywords(graph, findings) {
33707
33970
  });
33708
33971
  }
33709
33972
  }
33710
- function normalizeRule(rule) {
33711
- return rule.trim().replace(/\s+/g, " ").toLowerCase();
33973
+
33974
+ // src/lessons/glob-breadth.ts
33975
+ var WILDCARD = /[*?[\]]/;
33976
+ function globNarrowness(pattern) {
33977
+ const segments = pattern.replaceAll("\\", "/").split("/").filter((segment) => segment !== "" && segment !== ".");
33978
+ if (segments.length === 0) return 0;
33979
+ let literal = 0;
33980
+ let globstars = 0;
33981
+ for (const segment of segments) {
33982
+ if (segment === "**") globstars += 1;
33983
+ else if (!WILDCARD.test(segment)) literal += 1;
33984
+ }
33985
+ return literal / (segments.length + globstars);
33986
+ }
33987
+ var BROAD_GLOB_NARROWNESS = 0.34;
33988
+ function isBroadFileGlob(pattern) {
33989
+ return globNarrowness(pattern) < BROAD_GLOB_NARROWNESS;
33990
+ }
33991
+
33992
+ // src/lessons/command-pattern-breadth.ts
33993
+ var COMMAND_PROBE_CORPUS = [
33994
+ "git status",
33995
+ 'git commit -m "wip"',
33996
+ "pnpm test",
33997
+ "npx vitest run src/x.test.ts",
33998
+ "ls -la",
33999
+ "cat README.md",
34000
+ "rm -rf dist",
34001
+ "mkdir -p build/out",
34002
+ "node scripts/build.js",
34003
+ "docker compose up -d",
34004
+ "curl -s https://example.com",
34005
+ "echo hello > out.txt",
34006
+ "sed -i 's/a/b/' file.txt",
34007
+ "pnpm lint --fix",
34008
+ "python3 -m pytest",
34009
+ "cargo build --release",
34010
+ "make",
34011
+ "npm install --global typescript",
34012
+ "cp a.txt b.txt",
34013
+ "grep -rn TODO src"
34014
+ ];
34015
+ var BROAD_HIT_RATIO = 0.5;
34016
+ var PROBE_BUDGET = 1e5;
34017
+ function isBroadCommandPattern(pattern) {
34018
+ const matcher = getCommandMatcher(pattern);
34019
+ if (matcher === null) return false;
34020
+ if (matcher.test("", { remaining: PROBE_BUDGET })) return true;
34021
+ let hits = 0;
34022
+ for (const command of COMMAND_PROBE_CORPUS) {
34023
+ if (matcher.test(command, { remaining: PROBE_BUDGET })) hits += 1;
34024
+ }
34025
+ return hits > COMMAND_PROBE_CORPUS.length * BROAD_HIT_RATIO;
33712
34026
  }
34027
+
34028
+ // src/lessons/validate-liveness.ts
33713
34029
  function activeTriggerIds(graph) {
33714
34030
  const ids = /* @__PURE__ */ new Set();
33715
34031
  for (const lesson of Object.values(graph.lessons)) {
@@ -33761,6 +34077,34 @@ function collectRunnerAnchoredPatterns(graph, findings) {
33761
34077
  });
33762
34078
  }
33763
34079
  }
34080
+ function collectBroadFileGlobs(graph, findings) {
34081
+ const active = activeTriggerIds(graph);
34082
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
34083
+ if (trigger.kind !== "file_glob") continue;
34084
+ if (!active.has(triggerId)) continue;
34085
+ if (!isBroadFileGlob(trigger.pattern)) continue;
34086
+ findings.push({
34087
+ level: "warning",
34088
+ code: "BROAD_FILE_GLOB",
34089
+ message: `file_glob trigger "${triggerId}" (${trigger.pattern}) matches most of the repository, so it outranks nothing and crowds the recall budget. Narrow it to the directory or file class the rule is really about, or detach it with \`lessons untrigger\`.`,
34090
+ triggerId
34091
+ });
34092
+ }
34093
+ }
34094
+ function collectBroadCommandPatterns(graph, findings) {
34095
+ const active = activeTriggerIds(graph);
34096
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
34097
+ if (trigger.kind !== "command_pattern") continue;
34098
+ if (!active.has(triggerId)) continue;
34099
+ if (!isBroadCommandPattern(trigger.pattern)) continue;
34100
+ findings.push({
34101
+ level: "warning",
34102
+ code: "BROAD_COMMAND_PATTERN",
34103
+ message: `command_pattern trigger "${triggerId}" (${trigger.pattern}) matches nearly every command, so the lesson fires on every recall. Key it on the action (e.g. \`\\bgit commit\\b\`), or detach it with \`lessons untrigger\`.`,
34104
+ triggerId
34105
+ });
34106
+ }
34107
+ }
33764
34108
 
33765
34109
  // src/lessons/validate.ts
33766
34110
  function validateLessonsGraph(graph, options = {}) {
@@ -33788,6 +34132,8 @@ function validateLessonsGraph(graph, options = {}) {
33788
34132
  collectLowSignalKeywords(graph, findings);
33789
34133
  collectStopwordKeywords(graph, findings);
33790
34134
  collectRunnerAnchoredPatterns(graph, findings);
34135
+ collectBroadCommandPatterns(graph, findings);
34136
+ collectBroadFileGlobs(graph, findings);
33791
34137
  if (options.knownPaths !== void 0) collectDeadFileGlobs(graph, findings, options.knownPaths);
33792
34138
  const ok = findings.every((f) => f.level !== "error");
33793
34139
  return { ok, findings };
@@ -33839,13 +34185,13 @@ function diag(level, file, message) {
33839
34185
 
33840
34186
  // src/core/lint/linter.ts
33841
34187
  var EXCLUDE_DIRS = ["node_modules", ".git", "dist", "coverage", ".agentsmesh"];
34188
+ function isExcludedProjectPath(rel2) {
34189
+ const posix9 = rel2.replaceAll("\\", "/");
34190
+ return EXCLUDE_DIRS.some((d) => posix9.includes(`/${d}/`) || posix9.startsWith(`${d}/`));
34191
+ }
33842
34192
  async function getProjectFiles(projectRoot) {
33843
34193
  const all = await readDirRecursive(projectRoot);
33844
- const filtered = all.filter((p) => {
33845
- const rel2 = relative(projectRoot, p);
33846
- return !EXCLUDE_DIRS.some((d) => rel2.includes(`/${d}/`) || rel2.startsWith(`${d}/`));
33847
- });
33848
- return filtered.map((p) => relative(projectRoot, p));
34194
+ return all.filter((p) => !isExcludedProjectPath(relative(projectRoot, p))).map((p) => relative(projectRoot, p));
33849
34195
  }
33850
34196
  async function runLint(config, canonical, projectRoot, targetFilter, options = {}) {
33851
34197
  const scope = options.scope ?? "project";
@@ -34107,6 +34453,7 @@ async function diffOutputChecksums(rootBase, lockOutputs) {
34107
34453
  // src/core/generate/stale-cleanup.ts
34108
34454
  init_fs();
34109
34455
  init_builtin_targets();
34456
+ init_registry();
34110
34457
  async function listFiles2(root, base = root) {
34111
34458
  const entries = await readdir(root, { withFileTypes: true });
34112
34459
  const files = [];
@@ -34127,6 +34474,11 @@ function retainedDirs(inactiveTargets, scope) {
34127
34474
  }
34128
34475
  return dirs;
34129
34476
  }
34477
+ function primaryEmitted(target34, scope, expected) {
34478
+ const descriptor34 = getBuiltinTargetDefinition(target34) ?? getDescriptor(target34);
34479
+ const primary = getTargetLayout(target34, scope)?.rootInstructionPath ?? descriptor34?.generators.primaryRootInstructionPath;
34480
+ return primary !== void 0 && expected.has(primary);
34481
+ }
34130
34482
  async function findStaleGeneratedOutputs(args) {
34131
34483
  const expected = new Set(args.expectedPaths);
34132
34484
  const stale = /* @__PURE__ */ new Set();
@@ -34138,7 +34490,13 @@ async function findStaleGeneratedOutputs(args) {
34138
34490
  const managed = getTargetManagedOutputs(target34, scope);
34139
34491
  if (!managed) continue;
34140
34492
  for (const file of managed.coOwnedFiles ?? []) coOwned.add(file);
34141
- for (const file of managed.files) stale.add(file);
34493
+ for (const file of managed.files) {
34494
+ if (generated !== null && !generated.has(file)) continue;
34495
+ stale.add(file);
34496
+ }
34497
+ if (primaryEmitted(target34, scope, expected)) {
34498
+ for (const file of managed.supersededFiles ?? []) stale.add(file);
34499
+ }
34142
34500
  for (const dir of managed.dirs) {
34143
34501
  if (retained.has(dir)) continue;
34144
34502
  const absDir = join(args.projectRoot, dir);
@@ -34578,6 +34936,23 @@ function todayIso() {
34578
34936
  }
34579
34937
 
34580
34938
  // src/lessons/add-errors.ts
34939
+ var EmptyRuleError = class extends Error {
34940
+ code = "EMPTY_RULE";
34941
+ constructor() {
34942
+ super("Lesson rule must not be empty \u2014 pass one imperative sentence.");
34943
+ this.name = "EmptyRuleError";
34944
+ }
34945
+ };
34946
+ var BroadCommandPatternError = class extends Error {
34947
+ constructor(pattern) {
34948
+ super(
34949
+ `Command pattern ${JSON.stringify(pattern)} matches nearly every command, so it would fire on every recall. Key it on the action instead \u2014 a word-bounded program + subcommand (e.g. "\\bgit commit\\b", "\\brm\\b").`
34950
+ );
34951
+ this.pattern = pattern;
34952
+ this.name = "BroadCommandPatternError";
34953
+ }
34954
+ code = "BROAD_COMMAND_PATTERN";
34955
+ };
34581
34956
  var UnknownTopicError = class extends Error {
34582
34957
  constructor(topic) {
34583
34958
  super(`Unknown topic: ${topic}. Pass allowNewTopic + topicSummary to create it.`);
@@ -34617,6 +34992,77 @@ var UnrecallableLessonError = class extends Error {
34617
34992
  code = "UNRECALLABLE_LESSON";
34618
34993
  };
34619
34994
 
34995
+ // src/lessons/trigger-effectiveness.ts
34996
+ function ineffectiveTriggers(graph, triggerIds) {
34997
+ const out2 = [];
34998
+ for (const id of triggerIds) {
34999
+ const trigger = graph.triggers[id];
35000
+ if (trigger === void 0) continue;
35001
+ const reason = ineffectiveReason(trigger.kind, trigger.pattern);
35002
+ if (reason !== null) out2.push({ id, kind: trigger.kind, pattern: trigger.pattern, reason });
35003
+ }
35004
+ return out2;
35005
+ }
35006
+ function ineffectiveReason(kind, pattern) {
35007
+ if (kind === "keyword") {
35008
+ if (tokenize(pattern).length === 0) {
35009
+ return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
35010
+ }
35011
+ if (keywordNeedleLosesTokens(pattern)) {
35012
+ return "keyword contains stopwords/short words, so its needle can never appear as a contiguous run on the mandatory --file/--cmd recall path";
35013
+ }
35014
+ return null;
35015
+ }
35016
+ if (kind === "command_pattern") {
35017
+ let valid = true;
35018
+ try {
35019
+ new RegExp(pattern);
35020
+ } catch {
35021
+ valid = false;
35022
+ }
35023
+ if (!valid) {
35024
+ return "invalid regex \u2014 recall compiles it with new RegExp and swallows the throw as a non-match, so it never fires";
35025
+ }
35026
+ if (!isSafeRegexPattern(pattern)) {
35027
+ return "regex is outside the provably-linear engine \u2014 recall skips it (ReDoS guard), so it never fires";
35028
+ }
35029
+ return null;
35030
+ }
35031
+ return null;
35032
+ }
35033
+ function blockingDeadTriggers(graph, triggerIds) {
35034
+ return ineffectiveTriggers(graph, triggerIds).filter((t) => t.kind !== "command_pattern");
35035
+ }
35036
+
35037
+ // src/lessons/add-gates.ts
35038
+ function assertRuleShape(rule) {
35039
+ const trimmed = rule.trim();
35040
+ if (trimmed.length === 0) throw new EmptyRuleError();
35041
+ if (trimmed.length > MAX_RULE_LENGTH) throw new RuleTooLongError(trimmed.length, MAX_RULE_LENGTH);
35042
+ return trimmed;
35043
+ }
35044
+ function skipsTriggerGates(input, options) {
35045
+ return options.allowNoTrigger === true || input.scope === "always";
35046
+ }
35047
+ function countInputTriggers(triggers) {
35048
+ return (triggers.files?.length ?? 0) + (triggers.commands?.length ?? 0) + (triggers.keywords?.length ?? 0);
35049
+ }
35050
+ function assertTriggerInputs(input, options, existingTriggerCount) {
35051
+ if (!skipsTriggerGates(input, options) && countInputTriggers(input.triggers) === 0 && existingTriggerCount === 0) {
35052
+ throw new NoTriggerError();
35053
+ }
35054
+ if (options.allowNoTrigger !== true) {
35055
+ const broad = (input.triggers.commands ?? []).find(isBroadCommandPattern);
35056
+ if (broad !== void 0) throw new BroadCommandPatternError(broad);
35057
+ }
35058
+ }
35059
+ function assertRecallable(graph, resultingTriggers) {
35060
+ const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
35061
+ if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
35062
+ throw new UnrecallableLessonError(blockingDead);
35063
+ }
35064
+ }
35065
+
34620
35066
  // src/lessons/capture-guardrails.ts
34621
35067
  var WIDE_GLOB_MATCH_COUNT = 40;
34622
35068
  var MAX_RECOMMENDED_TRIGGERS = 8;
@@ -34655,7 +35101,7 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
34655
35101
  if (lowSignal.length > 0) {
34656
35102
  warnings.push({
34657
35103
  code: "LOW_SIGNAL_KEYWORD",
34658
- 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.`
35104
+ message: `Lesson "${lessonId}" has long keyword trigger(s) (${lowSignal.join(", ")}); recall matches a keyword only as a contiguous token-run in --keyword or the file/command, so a pattern past ${MAX_RECOMMENDED_KEYWORD_TOKENS} tokens rarely fires \u2014 use a short distinctive phrase.`
34659
35105
  });
34660
35106
  }
34661
35107
  const stopworded = triggers.filter((t) => t.kind === "keyword" && keywordNeedleLosesTokens(t.pattern)).map((t) => t.pattern);
@@ -34716,7 +35162,7 @@ function jaccard(a, b) {
34716
35162
 
34717
35163
  // src/utils/filesystem/process-lock.ts
34718
35164
  init_errors();
34719
- var DEFAULT_STALE_MS = 6e4;
35165
+ var DEFAULT_STALE_MS = 6 * 60 * 60 * 1e3;
34720
35166
  var DEFAULT_RETRIES = 30;
34721
35167
  var DEFAULT_RETRY_DELAY_MS = 200;
34722
35168
  var YOUNG_LOCK_GRACE_MS = 2e3;
@@ -34801,10 +35247,9 @@ async function inspectLock(lockPath) {
34801
35247
  }
34802
35248
  function isStale(meta, staleMs) {
34803
35249
  if (!meta) return true;
34804
- const age = Date.now() - meta.started;
34805
- if (age > staleMs) return true;
34806
- if (meta.hostname && meta.hostname !== getHostname()) return false;
34807
- return !isProcessAlive(meta.pid);
35250
+ const sameHost = !meta.hostname || meta.hostname === getHostname();
35251
+ if (sameHost && !isProcessAlive(meta.pid)) return true;
35252
+ return Date.now() - meta.started > staleMs;
34808
35253
  }
34809
35254
  function isProcessAlive(pid) {
34810
35255
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -34881,52 +35326,7 @@ async function mutateLessonsGraph(projectRoot, mutator, options = {}) {
34881
35326
  return mutateLessonsGraphLocked(projectRoot, mutator, options);
34882
35327
  }
34883
35328
 
34884
- // src/lessons/trigger-effectiveness.ts
34885
- function ineffectiveTriggers(graph, triggerIds) {
34886
- const out2 = [];
34887
- for (const id of triggerIds) {
34888
- const trigger = graph.triggers[id];
34889
- if (trigger === void 0) continue;
34890
- const reason = ineffectiveReason(trigger.kind, trigger.pattern);
34891
- if (reason !== null) out2.push({ id, kind: trigger.kind, pattern: trigger.pattern, reason });
34892
- }
34893
- return out2;
34894
- }
34895
- function ineffectiveReason(kind, pattern) {
34896
- if (kind === "keyword") {
34897
- if (tokenize(pattern).length === 0) {
34898
- return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
34899
- }
34900
- if (keywordNeedleLosesTokens(pattern)) {
34901
- return "keyword contains stopwords/short words, so its needle can never appear as a contiguous run on the mandatory --file/--cmd recall path";
34902
- }
34903
- return null;
34904
- }
34905
- if (kind === "command_pattern") {
34906
- let valid = true;
34907
- try {
34908
- new RegExp(pattern);
34909
- } catch {
34910
- valid = false;
34911
- }
34912
- if (!valid) {
34913
- return "invalid regex \u2014 recall compiles it with new RegExp and swallows the throw as a non-match, so it never fires";
34914
- }
34915
- if (!isSafeRegexPattern(pattern)) {
34916
- return "regex is outside the provably-linear engine \u2014 recall skips it (ReDoS guard), so it never fires";
34917
- }
34918
- return null;
34919
- }
34920
- return null;
34921
- }
34922
- function blockingDeadTriggers(graph, triggerIds) {
34923
- return ineffectiveTriggers(graph, triggerIds).filter((t) => t.kind !== "command_pattern");
34924
- }
34925
-
34926
35329
  // src/lessons/add.ts
34927
- function countInputTriggers(triggers) {
34928
- return (triggers.files?.length ?? 0) + (triggers.commands?.length ?? 0) + (triggers.keywords?.length ?? 0);
34929
- }
34930
35330
  async function addLesson(projectRoot, input, options = {}) {
34931
35331
  return mutateLessonsGraph(projectRoot, (graph) => addLessonInto(graph, input, options), {
34932
35332
  retries: options.retries
@@ -34934,10 +35334,7 @@ async function addLesson(projectRoot, input, options = {}) {
34934
35334
  }
34935
35335
  function addLessonInto(graph, input, options) {
34936
35336
  const ruleKey2 = normalizeRule2(input.rule);
34937
- const trimmedRule = input.rule.trim();
34938
- if (trimmedRule.length > MAX_RULE_LENGTH) {
34939
- throw new RuleTooLongError(trimmedRule.length, MAX_RULE_LENGTH);
34940
- }
35337
+ const trimmedRule = assertRuleShape(input.rule);
34941
35338
  const existingId = findExistingLessonByRule(graph, ruleKey2);
34942
35339
  const isNewTopic = graph.topics[input.topic] === void 0;
34943
35340
  if (isNewTopic) {
@@ -34947,29 +35344,23 @@ function addLessonInto(graph, input, options) {
34947
35344
  }
34948
35345
  graph.topics[input.topic] = { summary: options.topicSummary };
34949
35346
  }
34950
- const skipTriggerGates = options.allowNoTrigger === true || input.scope === "always";
34951
- if (!skipTriggerGates) {
34952
- const existingTriggers = existingId !== null ? graph.lessons[existingId]?.triggers.length ?? 0 : 0;
34953
- if (countInputTriggers(input.triggers) === 0 && existingTriggers === 0) {
34954
- throw new NoTriggerError();
34955
- }
34956
- }
35347
+ const existing = existingId !== null ? graph.lessons[existingId] : void 0;
35348
+ assertTriggerInputs(input, options, existing?.triggers.length ?? 0);
34957
35349
  const { triggerIds, newTriggerIds } = mergeTriggers(graph, input.triggers);
34958
- if (!skipTriggerGates) {
34959
- const resultingTriggers = existingId !== null ? union(graph.lessons[existingId].triggers, triggerIds) : triggerIds;
34960
- const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
34961
- if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
34962
- throw new UnrecallableLessonError(blockingDead);
34963
- }
35350
+ if (!skipsTriggerGates(input, options)) {
35351
+ assertRecallable(
35352
+ graph,
35353
+ existing === void 0 ? triggerIds : union(existing.triggers, triggerIds)
35354
+ );
34964
35355
  }
34965
35356
  if (existingId !== null) {
34966
- const existing = graph.lessons[existingId];
35357
+ const existing2 = graph.lessons[existingId];
34967
35358
  graph.lessons[existingId] = {
34968
- ...existing,
34969
- topics: union(existing.topics, [input.topic]),
34970
- triggers: union(existing.triggers, triggerIds),
34971
- evidence: union(existing.evidence, input.evidence ?? []),
34972
- ...existing.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
35359
+ ...existing2,
35360
+ topics: union(existing2.topics, [input.topic]),
35361
+ triggers: union(existing2.triggers, triggerIds),
35362
+ evidence: union(existing2.evidence, input.evidence ?? []),
35363
+ ...existing2.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
34973
35364
  // Re-capturing a rule with --scope always promotes it to always-on.
34974
35365
  ...input.scope === "always" ? { scope: "always" } : {}
34975
35366
  };
@@ -35150,14 +35541,16 @@ function deriveHaystackTokens(query) {
35150
35541
  if (query.command !== void 0) parts.push(query.command);
35151
35542
  if (parts.length === 0) return [];
35152
35543
  const out2 = [];
35153
- for (const raw of parts.join(" ").split(/[^A-Za-z0-9]+/)) {
35154
- if (raw.length === 0) continue;
35544
+ for (const raw of splitTokens(parts.join(" "))) {
35155
35545
  out2.push(raw.toLowerCase());
35156
35546
  const sub = raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(" ").filter((t) => t.length > 0);
35157
35547
  if (sub.length > 1) out2.push(...sub);
35158
35548
  }
35159
35549
  return out2;
35160
35550
  }
35551
+ function splitTokens(text) {
35552
+ return text.split(/[^A-Za-z0-9]+/).filter((t) => t.length > 0);
35553
+ }
35161
35554
  function containsRun(needle, hay) {
35162
35555
  if (needle.length === 0) return false;
35163
35556
  for (let i = 0; i + needle.length <= hay.length; i += 1) {
@@ -35173,10 +35566,11 @@ function containsRun(needle, hay) {
35173
35566
  return false;
35174
35567
  }
35175
35568
  function keywordMatches(pattern, query) {
35176
- if (query.keyword !== void 0 && query.keyword.toLowerCase().includes(pattern.toLowerCase())) {
35569
+ const needle = tokenize(pattern);
35570
+ if (query.keyword !== void 0 && containsRun(needle, splitTokens(query.keyword.toLowerCase()))) {
35177
35571
  return true;
35178
35572
  }
35179
- return containsRun(tokenize(pattern), deriveHaystackTokens(query));
35573
+ return containsRun(needle, deriveHaystackTokens(query));
35180
35574
  }
35181
35575
 
35182
35576
  // src/lessons/query.ts
@@ -35242,6 +35636,15 @@ function buildFanout(graph) {
35242
35636
  }
35243
35637
  return fanout;
35244
35638
  }
35639
+ var KEYWORD_NARROWNESS = 0.4;
35640
+ function buildNarrowness(graph) {
35641
+ const narrowness = /* @__PURE__ */ new Map();
35642
+ for (const [id, trigger] of Object.entries(graph.triggers)) {
35643
+ if (trigger.kind === "file_glob") narrowness.set(id, globNarrowness(trigger.pattern));
35644
+ else narrowness.set(id, trigger.kind === "keyword" ? KEYWORD_NARROWNESS : 1);
35645
+ }
35646
+ return narrowness;
35647
+ }
35245
35648
  function buildTopicCoherence(matches) {
35246
35649
  const topicCount = /* @__PURE__ */ new Map();
35247
35650
  for (const { lesson } of matches) {
@@ -35260,7 +35663,7 @@ function buildTopicCoherence(matches) {
35260
35663
  var DEFAULT_RECALL_LIMIT = 10;
35261
35664
  var DEFAULT_RECALL_MAX_TOKENS = 400;
35262
35665
  var RRF_K = 60;
35263
- var SPECIFICITY_WEIGHT = 3;
35666
+ var SPECIFICITY_WEIGHT = 5;
35264
35667
  var TOPIC_COHERENCE_WEIGHT = 2;
35265
35668
  var BM25_WEIGHT = 1;
35266
35669
  var EFFECTIVENESS_WEIGHT = 1;
@@ -35287,12 +35690,15 @@ function rankLessons(graph, query, matches, options = {}) {
35287
35690
  const terms = queryTerms(query);
35288
35691
  const corpus = buildCorpus(graph);
35289
35692
  const fanout = buildFanout(graph);
35693
+ const narrowness = buildNarrowness(graph);
35290
35694
  const coherence = buildTopicCoherence(matches);
35291
35695
  const matchedTriggerIds = collectMatchedTriggerIds(graph, query);
35292
35696
  const scored = matches.map(({ id, lesson }) => {
35293
35697
  const hitTriggers = lesson.triggers.filter((t) => matchedTriggerIds.has(t));
35294
35698
  let specificity = 0;
35295
- for (const t of hitTriggers) specificity = Math.max(specificity, 1 / fanout.get(t));
35699
+ for (const t of hitTriggers) {
35700
+ specificity = Math.max(specificity, (narrowness.get(t) ?? 1) / fanout.get(t));
35701
+ }
35296
35702
  return {
35297
35703
  id,
35298
35704
  lesson,
@@ -35404,9 +35810,9 @@ var LINE_REFS = String.raw`L\d+(?:\s*,\s*L\d+)*`;
35404
35810
  var LINE_REF_PATTERNS = [
35405
35811
  new RegExp(String.raw`\s*\bSee\s+${LINE_REFS}\.?`, "g"),
35406
35812
  // " See L128." / " See L140, L149"
35407
- new RegExp(String.raw`\s*\((?:${LINE_REFS})\)\.?`, "g"),
35813
+ new RegExp(String.raw`\s*\((?:${LINE_REFS})\)`, "g"),
35408
35814
  // " (L174)" / " (L92, L163)"
35409
- new RegExp(String.raw`\s*\[(?:${LINE_REFS})\]\.?`, "g")
35815
+ new RegExp(String.raw`\s*\[(?:${LINE_REFS})\]`, "g")
35410
35816
  // " [L161, L208]"
35411
35817
  ];
35412
35818
  var ALSO_RELEVANT_PATTERN = /\s*\(also relevant[^)]*\)\s*/g;
@@ -35440,42 +35846,7 @@ async function stripMarkersInGraph(projectRoot, options = {}) {
35440
35846
  });
35441
35847
  return { changedIds, changedCount: changedIds.length };
35442
35848
  }
35443
- var RECALL_HOOK_COMMAND = "agentsmesh lessons hook";
35444
- var RECALL_HOOK_TOOL_MATCHER = "Edit|Write|Bash";
35445
- var RECALL_EVENTS = [
35446
- { event: "PreToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
35447
- { event: "PostToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
35448
- { event: "UserPromptSubmit", matcher: "*" },
35449
- // Capture-on-failure nudge (see capture-nudge.ts). BEST-EFFORT: only Claude
35450
- // Code's passthrough hooks emit it; whitelist targets drop it without warning
35451
- // (BEST_EFFORT_HOOK_EVENTS). PostToolUse is success-only, so failures need this.
35452
- { event: "PostToolUseFailure", matcher: "*" },
35453
- // Reset recall dedup after a context compaction/clear (see hook.ts SessionStart).
35454
- // BEST-EFFORT: targets that can't represent SessionStart just keep dedup as-is.
35455
- { event: "SessionStart", matcher: "*" }
35456
- ];
35457
- function injectEvent(doc, event, matcher) {
35458
- const existing = doc.get(event);
35459
- const seq = existing instanceof YAMLSeq ? existing : new YAMLSeq();
35460
- const present = seq.items.some(
35461
- (item) => item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND
35462
- );
35463
- if (present) return false;
35464
- seq.add(doc.createNode({ matcher, type: "command", command: RECALL_HOOK_COMMAND }));
35465
- doc.set(event, seq);
35466
- return true;
35467
- }
35468
- function injectRecallHook(projectRoot) {
35469
- const path = join(projectRoot, ".agentsmesh", "hooks.yaml");
35470
- if (!existsSync(path)) return false;
35471
- const doc = parseDocument(readFileSync(path, "utf8"));
35472
- let changed = false;
35473
- for (const { event, matcher } of RECALL_EVENTS) {
35474
- if (injectEvent(doc, event, matcher)) changed = true;
35475
- }
35476
- if (changed) writeFileSync(path, String(doc), "utf8");
35477
- return changed;
35478
- }
35849
+ init_recall_hook_scaffold();
35479
35850
 
35480
35851
  // src/lessons/merge-driver-setup.ts
35481
35852
  var LESSONS_MERGE_DRIVER = "agentsmesh-lessons";