agentsmesh 0.33.0 → 0.35.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,74 @@ var init_command_skill = __esm({
508
524
  LEGACY_CODEX_COMMAND_SKILL_PREFIX = "ab-command-";
509
525
  }
510
526
  });
527
+ function removeEvent(doc, event) {
528
+ const existing = doc.get(event);
529
+ if (!(existing instanceof YAMLSeq)) return false;
530
+ const kept = existing.items.filter(
531
+ (item) => !(item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND)
532
+ );
533
+ if (kept.length === existing.items.length) return false;
534
+ if (kept.length === 0) doc.delete(event);
535
+ else existing.items = kept;
536
+ return true;
537
+ }
538
+ function injectEvent(doc, event, matcher) {
539
+ const existing = doc.get(event);
540
+ const seq = existing instanceof YAMLSeq ? existing : new YAMLSeq();
541
+ const present = seq.items.some(
542
+ (item) => item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND
543
+ );
544
+ if (present) return false;
545
+ seq.add(doc.createNode({ matcher, type: "command", command: RECALL_HOOK_COMMAND }));
546
+ doc.set(event, seq);
547
+ return true;
548
+ }
549
+ function injectRecallHook(projectRoot) {
550
+ const path = join(projectRoot, ".agentsmesh", "hooks.yaml");
551
+ if (!existsSync(path)) return false;
552
+ const doc = parseDocument(readFileSync(path, "utf8"));
553
+ let changed = false;
554
+ for (const { event, matcher } of RECALL_EVENTS) {
555
+ if (injectEvent(doc, event, matcher)) changed = true;
556
+ }
557
+ for (const event of RETIRED_EVENTS) {
558
+ if (removeEvent(doc, event)) changed = true;
559
+ }
560
+ if (changed) writeFileSync(path, String(doc), "utf8");
561
+ return changed;
562
+ }
563
+ var RECALL_HOOK_COMMAND, RECALL_HOOK_TOOL_MATCHER, RECALL_EVENTS, RETIRED_EVENTS;
564
+ var init_recall_hook_scaffold = __esm({
565
+ "src/lessons/recall-hook-scaffold.ts"() {
566
+ RECALL_HOOK_COMMAND = "agentsmesh lessons hook";
567
+ RECALL_HOOK_TOOL_MATCHER = "Edit|Write|Bash";
568
+ RECALL_EVENTS = [
569
+ { event: "PreToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
570
+ { event: "UserPromptSubmit", matcher: "*" },
571
+ // Capture-on-failure nudge (see capture-nudge.ts). BEST-EFFORT: only Claude
572
+ // Code's passthrough hooks emit it; whitelist targets drop it without warning
573
+ // (BEST_EFFORT_HOOK_EVENTS). PostToolUse is success-only, so failures need this.
574
+ { event: "PostToolUseFailure", matcher: "*" },
575
+ // Reset recall dedup after a context compaction/clear (see hook.ts SessionStart).
576
+ // BEST-EFFORT: targets that can't represent SessionStart just keep dedup as-is.
577
+ { event: "SessionStart", matcher: "*" }
578
+ ];
579
+ RETIRED_EVENTS = ["PostToolUse"];
580
+ }
581
+ });
511
582
 
512
583
  // src/core/hook-types.ts
584
+ function isBestEffortHookEvent(event, entries) {
585
+ if (!BEST_EFFORT_HOOK_EVENTS.has(event)) return false;
586
+ if (!Array.isArray(entries)) return true;
587
+ return entries.every(
588
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.command === "string" && entry.command.includes(RECALL_HOOK_COMMAND)
589
+ );
590
+ }
513
591
  var BEST_EFFORT_HOOK_EVENTS;
514
592
  var init_hook_types = __esm({
515
593
  "src/core/hook-types.ts"() {
594
+ init_recall_hook_scaffold();
516
595
  BEST_EFFORT_HOOK_EVENTS = /* @__PURE__ */ new Set([
517
596
  "UserPromptSubmit",
518
597
  "PostToolUseFailure",
@@ -698,7 +777,7 @@ var init_conf_merge = __esm({
698
777
  });
699
778
 
700
779
  // src/core/errors.ts
701
- var AgentsMeshError, ConfigNotFoundError, ConfigValidationError, TargetNotFoundError, ImportError, GenerationError, RemoteFetchError, LockAcquisitionError, FileSystemError;
780
+ var AgentsMeshError, ConfigNotFoundError, ConfigValidationError, CanonicalParseError, TargetNotFoundError, ImportError, GenerationError, RemoteFetchError, LockAcquisitionError, FileSystemError;
702
781
  var init_errors = __esm({
703
782
  "src/core/errors.ts"() {
704
783
  AgentsMeshError = class extends Error {
@@ -733,6 +812,21 @@ var init_errors = __esm({
733
812
  this.issues = issues;
734
813
  }
735
814
  };
815
+ CanonicalParseError = class extends AgentsMeshError {
816
+ path;
817
+ constructor(path, cause) {
818
+ const detail = cause instanceof Error ? cause.message : String(cause);
819
+ super(
820
+ "AM_CONFIG_INVALID",
821
+ `Invalid canonical file ${path}: ${detail}. Fix the syntax and try again.`,
822
+ {
823
+ cause
824
+ }
825
+ );
826
+ this.name = "CanonicalParseError";
827
+ this.path = path;
828
+ }
829
+ };
736
830
  TargetNotFoundError = class extends AgentsMeshError {
737
831
  target;
738
832
  constructor(target34, options) {
@@ -806,6 +900,11 @@ function normalizeLineEndings(content) {
806
900
  function executableModeFor(path) {
807
901
  return EXECUTABLE_SCRIPT_EXTENSIONS.has(extname(path).toLowerCase()) ? 493 : void 0;
808
902
  }
903
+ function normalizeTextPayload(path, content) {
904
+ if (!shouldNormalizeLineEndings(path)) return content;
905
+ const withoutBom = content.startsWith(UTF8_BOM) ? content.slice(UTF8_BOM.length) : content;
906
+ return normalizeLineEndings(withoutBom);
907
+ }
809
908
  var UTF8_BOM, TEXT_EXTENSIONS, TEXT_DOTFILES, EXECUTABLE_SCRIPT_EXTENSIONS;
810
909
  var init_fs_text_encoding = __esm({
811
910
  "src/utils/filesystem/fs-text-encoding.ts"() {
@@ -1306,10 +1405,8 @@ function stripManagedBlock(content, start, end) {
1306
1405
  return content.replace(managedBlockPattern(start, end), "").trim();
1307
1406
  }
1308
1407
  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() };
1408
+ const split = splitFrontmatter(content);
1409
+ return split === null ? { prefix: "", body: content.trim() } : { prefix: split.prefix, body: split.body };
1313
1410
  }
1314
1411
  function insertAtBodyTop(content, block) {
1315
1412
  const { prefix, body } = splitFrontmatterPrefix(content);
@@ -1410,6 +1507,7 @@ function extractEmbeddedRules(content) {
1410
1507
  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
1508
  var init_managed_blocks = __esm({
1412
1509
  "src/targets/projection/managed-blocks.ts"() {
1510
+ init_markdown();
1413
1511
  ROOT_CONTRACT_START = "<!-- agentsmesh:root-generation-contract:start -->";
1414
1512
  ROOT_CONTRACT_END = "<!-- agentsmesh:root-generation-contract:end -->";
1415
1513
  LESSONS_CONTRACT_START = "<!-- agentsmesh:lessons-contract:start -->";
@@ -1577,10 +1675,17 @@ async function serializeImportedCommandWithFallback(destinationPath, imported, b
1577
1675
  })();
1578
1676
  const description = imported.hasDescription ? imported.description ?? "" : typeof existingFrontmatter.description === "string" ? existingFrontmatter.description : "";
1579
1677
  const allowedTools = imported.hasAllowedTools ? imported.allowedTools ?? [] : existingAllowedTools;
1678
+ const {
1679
+ description: _d,
1680
+ "allowed-tools": _a,
1681
+ allowedTools: _c,
1682
+ ...preserved
1683
+ } = existingFrontmatter;
1580
1684
  return serializeFrontmatter(
1581
1685
  {
1582
1686
  description,
1583
- "allowed-tools": allowedTools
1687
+ "allowed-tools": allowedTools,
1688
+ ...preserved
1584
1689
  },
1585
1690
  body.trim() || ""
1586
1691
  );
@@ -2478,6 +2583,26 @@ var init_link_rebaser_resolution = __esm({
2478
2583
  }
2479
2584
  });
2480
2585
 
2586
+ // src/core/reference/link-uri-encoding.ts
2587
+ function decodeLinkPath(token, role) {
2588
+ if (role !== "markdown-link-dest" || !token.includes("%")) return token;
2589
+ try {
2590
+ return decodeURIComponent(token);
2591
+ } catch {
2592
+ return token;
2593
+ }
2594
+ }
2595
+ function encodeLinkPath(path, enabled) {
2596
+ if (!enabled) return path;
2597
+ return path.split("/").map(
2598
+ (segment) => segment === "" || segment === "." || segment === ".." ? segment : encodeURIComponent(segment)
2599
+ ).join("/");
2600
+ }
2601
+ var init_link_uri_encoding = __esm({
2602
+ "src/core/reference/link-uri-encoding.ts"() {
2603
+ }
2604
+ });
2605
+
2481
2606
  // src/core/reference/link-token-guards.ts
2482
2607
  function isTildeHomeRelativePathToken(fullContent, matchOffset, matchText) {
2483
2608
  if (matchOffset >= 2 && fullContent[matchOffset - 2] === "~" && fullContent[matchOffset - 1] === "/") {
@@ -2607,10 +2732,11 @@ function rewriteFileLinks(input) {
2607
2732
  const { candidate: punctStripped, suffix } = stripTrailingPunctuation(match);
2608
2733
  if (!punctStripped) return match;
2609
2734
  const lineNumMatch = LINE_NUMBER_SUFFIX.exec(punctStripped);
2610
- const candidate = lineNumMatch ? punctStripped.slice(0, lineNumMatch.index) : punctStripped;
2735
+ const rawCandidate = lineNumMatch ? punctStripped.slice(0, lineNumMatch.index) : punctStripped;
2611
2736
  const lineNumSuffix = lineNumMatch ? lineNumMatch[0] : "";
2612
- if (!candidate) return match;
2613
- const tokenContext = getTokenContext(fullContent, offset, offset + candidate.length);
2737
+ if (!rawCandidate) return match;
2738
+ const tokenContext = getTokenContext(fullContent, offset, offset + rawCandidate.length);
2739
+ const candidate = decodeLinkPath(rawCandidate, tokenContext.role);
2614
2740
  if (tokenContext.role !== "markdown-link-dest" && WINDOWS_ABSOLUTE_PATH.test(candidate)) {
2615
2741
  return match;
2616
2742
  }
@@ -2680,7 +2806,7 @@ function rewriteFileLinks(input) {
2680
2806
  const targetTop = targetFromRoot.split("/").filter(Boolean)[0] ?? "";
2681
2807
  const tokenIsCanonicalMesh = normalizeSeparators(candidate).startsWith(".agentsmesh/");
2682
2808
  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);
2809
+ const forceRelative = preferRelativeProseInSameSurface || tokenContext.role === "markdown-link-dest" || isMarkdownLinkDestinationToken(fullContent, offset, rawCandidate);
2684
2810
  const rewritten = formatLinkPathForDestination(
2685
2811
  input.projectRoot,
2686
2812
  input.destinationFile,
@@ -2697,7 +2823,7 @@ function rewriteFileLinks(input) {
2697
2823
  }
2698
2824
  );
2699
2825
  if (!rewritten) return match;
2700
- return `${rewritten}${lineNumSuffix}${suffix}`;
2826
+ return `${encodeLinkPath(rewritten, candidate !== rawCandidate)}${lineNumSuffix}${suffix}`;
2701
2827
  });
2702
2828
  return { content, missing: [...missing] };
2703
2829
  }
@@ -2707,6 +2833,7 @@ var init_link_rebaser = __esm({
2707
2833
  init_link_rebaser_helpers();
2708
2834
  init_link_rebaser_output();
2709
2835
  init_link_rebaser_resolution();
2836
+ init_link_uri_encoding();
2710
2837
  init_link_token_guards();
2711
2838
  init_link_token_context();
2712
2839
  }
@@ -3590,7 +3717,7 @@ function unsupportedHookEventNames(hooks, supportedEvents) {
3590
3717
  if (!hooks) return [];
3591
3718
  const supported = new Set(supportedEvents);
3592
3719
  return Object.keys(hooks).filter(
3593
- (event) => !supported.has(event) && !BEST_EFFORT_HOOK_EVENTS.has(event)
3720
+ (event) => !supported.has(event) && !isBestEffortHookEvent(event, hooks[event])
3594
3721
  );
3595
3722
  }
3596
3723
  function createUnsupportedHookWarning(event, target34, supportedEvents, options) {
@@ -7127,6 +7254,21 @@ var init_embedded_rules = __esm({
7127
7254
  }
7128
7255
  });
7129
7256
 
7257
+ // src/canonical/features/syntax-error.ts
7258
+ function failSyntax(filePath2, cause, onParseError) {
7259
+ const error = new CanonicalParseError(filePath2, cause);
7260
+ if (onParseError !== void 0) {
7261
+ onParseError(error, filePath2);
7262
+ return null;
7263
+ }
7264
+ throw error;
7265
+ }
7266
+ var init_syntax_error = __esm({
7267
+ "src/canonical/features/syntax-error.ts"() {
7268
+ init_errors();
7269
+ }
7270
+ });
7271
+
7130
7272
  // src/canonical/features/mcp.ts
7131
7273
  function parseStringMap(raw) {
7132
7274
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -7209,14 +7351,14 @@ function stripJsonComments(text) {
7209
7351
  }
7210
7352
  return result2;
7211
7353
  }
7212
- async function parseMcp(mcpPath) {
7354
+ async function parseMcp(mcpPath, onParseError) {
7213
7355
  const content = await readFileSafe(mcpPath);
7214
7356
  if (!content) return null;
7215
7357
  let parsed;
7216
7358
  try {
7217
7359
  parsed = JSON.parse(stripJsonComments(content));
7218
- } catch {
7219
- return null;
7360
+ } catch (err) {
7361
+ return failSyntax(mcpPath, err, onParseError);
7220
7362
  }
7221
7363
  if (!parsed || typeof parsed !== "object") return null;
7222
7364
  const mcpServersRaw = parsed.mcpServers;
@@ -7231,6 +7373,7 @@ async function parseMcp(mcpPath) {
7231
7373
  }
7232
7374
  var init_mcp = __esm({
7233
7375
  "src/canonical/features/mcp.ts"() {
7376
+ init_syntax_error();
7234
7377
  init_fs();
7235
7378
  }
7236
7379
  });
@@ -9010,10 +9153,10 @@ var init_claude_code2 = __esm({
9010
9153
  skillDir: ".claude/skills",
9011
9154
  managedOutputs: {
9012
9155
  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
9156
+ files: [CLAUDE_ROOT, ".claudeignore"],
9157
+ // CLAUDE_NESTED_ROOT is the pre-migration project location: evicted once a run
9015
9158
  // writes the root `CLAUDE.md`, so Claude Code never concatenates both into context.
9016
- files: [CLAUDE_ROOT, CLAUDE_NESTED_ROOT, ".claudeignore"],
9159
+ supersededFiles: [CLAUDE_NESTED_ROOT],
9017
9160
  // `.mcp.json` is the shared project MCP file teams hand-commit and
9018
9161
  // deepagents-cli writes too; agentsmesh owns only `mcpServers` in it.
9019
9162
  // `.claude/settings.json` is co-owned through the `SETTINGS_JSON_PATHS`
@@ -10244,9 +10387,8 @@ var init_cline2 = __esm({
10244
10387
  };
10245
10388
  }
10246
10389
  });
10247
- function ruleSlug2(source) {
10248
- return basename(source, ".md");
10249
- }
10390
+
10391
+ // src/targets/codex-cli/codex-rule-paths.ts
10250
10392
  function directoryFromGlob(glob) {
10251
10393
  let normalized = glob.trim();
10252
10394
  if (normalized.startsWith("./")) normalized = normalized.slice(2);
@@ -10267,12 +10409,12 @@ function codexRuleDirectory(rule) {
10267
10409
  const dir = directoryFromGlob(glob);
10268
10410
  if (dir) return dir;
10269
10411
  }
10270
- return ruleSlug2(rule.source);
10412
+ return null;
10271
10413
  }
10272
10414
  function codexNestedAgentsPath(rule) {
10273
10415
  const dir = codexRuleDirectory(rule);
10274
10416
  const filename = rule.codexInstructionVariant === "override" ? "AGENTS.override.md" : "AGENTS.md";
10275
- return `${dir}/${filename}`;
10417
+ return dir === null ? filename : `${dir}/${filename}`;
10276
10418
  }
10277
10419
  var GLOB_METACHAR;
10278
10420
  var init_codex_rule_paths = __esm({
@@ -10280,10 +10422,9 @@ var init_codex_rule_paths = __esm({
10280
10422
  GLOB_METACHAR = /[*?[\]]/;
10281
10423
  }
10282
10424
  });
10283
-
10284
- // src/targets/codebuff/rule-paths.ts
10285
10425
  function codebuffNestedKnowledgePath(rule) {
10286
- return `${codexRuleDirectory(rule)}/AGENTS.md`;
10426
+ const dir = codexRuleDirectory(rule) ?? basename(rule.source, ".md");
10427
+ return `${dir}/AGENTS.md`;
10287
10428
  }
10288
10429
  var init_rule_paths = __esm({
10289
10430
  "src/targets/codebuff/rule-paths.ts"() {
@@ -10894,6 +11035,9 @@ function eligibleAdvisoryRules(canonical) {
10894
11035
  return rule.targets.length === 0 || rule.targets.includes("codex-cli");
10895
11036
  });
10896
11037
  }
11038
+ function isRootEmbedded(rule) {
11039
+ return codexNestedAgentsPath(rule) === AGENTS_MD;
11040
+ }
10897
11041
  function groupByNestedPath2(rules) {
10898
11042
  const groups = /* @__PURE__ */ new Map();
10899
11043
  for (const rule of rules) {
@@ -10906,9 +11050,11 @@ function groupByNestedPath2(rules) {
10906
11050
  }
10907
11051
  function generateRules9(canonical) {
10908
11052
  const root = canonical.rules.find((r) => r.root);
11053
+ const advisory = eligibleAdvisoryRules(canonical);
10909
11054
  const outputs = [];
10910
11055
  if (root) {
10911
- outputs.push({ path: AGENTS_MD, content: root.body.trim() });
11056
+ const content = appendEmbeddedRulesBlock(root.body.trim(), advisory.filter(isRootEmbedded));
11057
+ outputs.push({ path: AGENTS_MD, content });
10912
11058
  }
10913
11059
  for (const rule of canonical.rules) {
10914
11060
  if (rule.root) continue;
@@ -10920,7 +11066,8 @@ function generateRules9(canonical) {
10920
11066
  content: toSafeCodexRulesContent(rule.body)
10921
11067
  });
10922
11068
  }
10923
- for (const [path, rules] of groupByNestedPath2(eligibleAdvisoryRules(canonical))) {
11069
+ const nested = advisory.filter((rule) => !isRootEmbedded(rule));
11070
+ for (const [path, rules] of groupByNestedPath2(nested)) {
10924
11071
  const content = rules.map((rule) => rule.body.trim()).filter((body) => body.length > 0).join("\n\n");
10925
11072
  outputs.push({ path, content });
10926
11073
  }
@@ -11745,6 +11892,23 @@ var init_linter9 = __esm({
11745
11892
  });
11746
11893
 
11747
11894
  // src/targets/codex-cli/lint.ts
11895
+ function lintAgents3(canonical) {
11896
+ const diagnostics = [];
11897
+ for (const agent of canonical.agents) {
11898
+ const dropped = CODEX_DROPPED_AGENT_FIELDS.filter(
11899
+ (field) => hasAgentValue(agent, field)
11900
+ ).sort();
11901
+ if (dropped.length === 0) continue;
11902
+ diagnostics.push(
11903
+ createWarning(
11904
+ agent.source,
11905
+ "codex-cli",
11906
+ `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.`
11907
+ )
11908
+ );
11909
+ }
11910
+ return diagnostics;
11911
+ }
11748
11912
  function lintMcp4(canonical) {
11749
11913
  if (!canonical.mcp || Object.keys(canonical.mcp.mcpServers).length === 0) return [];
11750
11914
  const diagnostics = [];
@@ -11776,11 +11940,21 @@ function lintHooks7(canonical) {
11776
11940
  (event) => createUnsupportedHookWarning(event, "codex-cli", CODEX_SUPPORTED_HOOK_EVENTS)
11777
11941
  );
11778
11942
  }
11943
+ var CODEX_DROPPED_AGENT_FIELDS;
11779
11944
  var init_lint8 = __esm({
11780
11945
  "src/targets/codex-cli/lint.ts"() {
11781
11946
  init_helpers();
11782
11947
  init_mcp_servers();
11948
+ init_agents_format();
11783
11949
  init_constants10();
11950
+ CODEX_DROPPED_AGENT_FIELDS = [
11951
+ "tools",
11952
+ "disallowedTools",
11953
+ "maxTurns",
11954
+ "hooks",
11955
+ "skills",
11956
+ "memory"
11957
+ ];
11784
11958
  }
11785
11959
  });
11786
11960
 
@@ -11855,6 +12029,7 @@ var init_codex_cli2 = __esm({
11855
12029
  generateMcp: generateMcp6,
11856
12030
  generateHooks: generateHooks6,
11857
12031
  generatePermissions: generatePermissions7,
12032
+ lint: lintAgents3,
11858
12033
  importFrom: importFromCodex
11859
12034
  };
11860
12035
  project6 = {
@@ -11902,7 +12077,7 @@ var init_codex_cli2 = __esm({
11902
12077
  },
11903
12078
  rewriteGeneratedPath(path) {
11904
12079
  if (path === AGENTS_MD) return CODEX_GLOBAL_AGENTS_MD;
11905
- if (/\/AGENTS(\.override)?\.md$/.test(path)) return null;
12080
+ if (/(^|\/)AGENTS(\.override)?\.md$/.test(path)) return null;
11906
12081
  if (path.startsWith(`${CODEX_INSTRUCTIONS_DIR}/`)) return null;
11907
12082
  return path;
11908
12083
  },
@@ -12558,7 +12733,7 @@ function hasValue(agent, field) {
12558
12733
  if (value && typeof value === "object") return Object.keys(value).length > 0;
12559
12734
  return false;
12560
12735
  }
12561
- function lintAgents3(canonical) {
12736
+ function lintAgents4(canonical) {
12562
12737
  const diagnostics = [];
12563
12738
  for (const agent of canonical.agents) {
12564
12739
  const dropped = DROPPED_FIELDS.filter((field) => hasValue(agent, field)).sort();
@@ -12726,7 +12901,7 @@ var init_continue2 = __esm({
12726
12901
  generateHooks: generateHooks7,
12727
12902
  generateIgnore: generateIgnore8,
12728
12903
  // Feature-independent lint hook: agent warnings must not hang off `rules`.
12729
- lint: lintAgents3,
12904
+ lint: lintAgents4,
12730
12905
  importFrom: importFromContinue
12731
12906
  };
12732
12907
  descriptor10 = {
@@ -12983,7 +13158,7 @@ var init_hook_format = __esm({
12983
13158
  init_hook_entry();
12984
13159
  }
12985
13160
  });
12986
- function ruleSlug3(source) {
13161
+ function ruleSlug2(source) {
12987
13162
  const name = basename(source, ".md");
12988
13163
  return name === "_root" ? "root" : name;
12989
13164
  }
@@ -13017,7 +13192,7 @@ function generateRules11(canonical) {
13017
13192
  if (rule.root) continue;
13018
13193
  if (rule.targets.length > 0 && !rule.targets.includes("copilot")) continue;
13019
13194
  if (rule.globs.length === 0) continue;
13020
- const slug = ruleSlug3(rule.source);
13195
+ const slug = ruleSlug2(rule.source);
13021
13196
  const frontmatter = {
13022
13197
  description: rule.description || void 0,
13023
13198
  applyTo: rule.globs.length === 1 ? rule.globs[0] : rule.globs
@@ -14506,13 +14681,18 @@ var init_rules2 = __esm({
14506
14681
 
14507
14682
  // src/targets/cursor/generator/commands.ts
14508
14683
  function generateCommands13(canonical) {
14509
- return canonical.commands.map((cmd) => ({
14510
- path: `${CURSOR_COMMANDS_DIR}/${cmd.name}.md`,
14511
- content: cmd.body.trim() || ""
14512
- }));
14684
+ return canonical.commands.map((cmd) => {
14685
+ const frontmatter = {};
14686
+ if (cmd.description) frontmatter.description = cmd.description;
14687
+ return {
14688
+ path: `${CURSOR_COMMANDS_DIR}/${cmd.name}.md`,
14689
+ content: serializeFrontmatter(frontmatter, cmd.body.trim() || "")
14690
+ };
14691
+ });
14513
14692
  }
14514
14693
  var init_commands = __esm({
14515
14694
  "src/targets/cursor/generator/commands.ts"() {
14695
+ init_markdown();
14516
14696
  init_constants13();
14517
14697
  }
14518
14698
  });
@@ -14607,7 +14787,7 @@ var init_permissions3 = __esm({
14607
14787
  // src/targets/cursor/hook-format.ts
14608
14788
  function unmappedCursorHookEvents(hooks) {
14609
14789
  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)
14790
+ (event) => Array.isArray(hooks[event]) && hooks[event].length > 0 && !(event in CANONICAL_TO_CURSOR) && !isBestEffortHookEvent(event, hooks[event])
14611
14791
  );
14612
14792
  }
14613
14793
  function toCursorHooks(hooks) {
@@ -15045,14 +15225,11 @@ async function hasGlobalCursorArtifacts(projectRoot) {
15045
15225
  join(projectRoot, CURSOR_GLOBAL_USER_RULES),
15046
15226
  join(projectRoot, CURSOR_MCP),
15047
15227
  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)
15228
+ join(projectRoot, CURSOR_IGNORE)
15052
15229
  ];
15053
15230
  for (const p of candidates) {
15054
- const stat8 = await readFileSafe(p);
15055
- if (stat8 !== null && stat8.trim() !== "") return true;
15231
+ const content = await readFileSafe(p);
15232
+ if (content !== null && content.trim() !== "") return true;
15056
15233
  }
15057
15234
  const skillFiles = await readDirRecursiveNoSymlinks(join(projectRoot, CURSOR_SKILLS_DIR));
15058
15235
  if (skillFiles.some((f) => f.endsWith(".md"))) return true;
@@ -15339,11 +15516,11 @@ function lintHooks10(canonical) {
15339
15516
  ];
15340
15517
  }
15341
15518
  function lintCommands6(canonical) {
15342
- return canonical.commands.filter((command) => command.description.length > 0 || command.allowedTools.length > 0).map(
15519
+ return canonical.commands.filter((command) => command.allowedTools.length > 0).map(
15343
15520
  (command) => createWarning(
15344
15521
  command.source,
15345
15522
  "cursor",
15346
- "Cursor command files are plain Markdown; command description and allowed-tools metadata are not projected."
15523
+ "Cursor command files project only description frontmatter; allowed-tools metadata is not projected."
15347
15524
  )
15348
15525
  );
15349
15526
  }
@@ -15676,7 +15853,7 @@ var init_mcp_merge4 = __esm({
15676
15853
  // src/targets/deepagents-cli/hooks-format.ts
15677
15854
  function unmappedDeepagentsHookEvents(hooks) {
15678
15855
  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)
15856
+ (event) => Array.isArray(hooks[event]) && hooks[event].length > 0 && !(event in CANONICAL_TO_DEEPAGENTS) && !isBestEffortHookEvent(event, hooks[event])
15680
15857
  );
15681
15858
  }
15682
15859
  function toDeepagentsHooks(hooks) {
@@ -16244,7 +16421,8 @@ var init_deepagents_cli2 = __esm({
16244
16421
  }
16245
16422
  },
16246
16423
  buildImportPaths: buildDeepagentsCliImportPaths,
16247
- detectionPaths: [DEEPAGENTS_CLI_ROOT_FILE, DEEPAGENTS_CLI_MCP_FILE]
16424
+ // `.mcp.json` is co-owned with claude-code (agentsmesh writes it), so it must not enroll this target.
16425
+ detectionPaths: [DEEPAGENTS_CLI_ROOT_FILE]
16248
16426
  };
16249
16427
  }
16250
16428
  });
@@ -20222,7 +20400,8 @@ var init_layout8 = __esm({
20222
20400
  skillDir: KIMI_CODE_SKILLS_DIR,
20223
20401
  managedOutputs: {
20224
20402
  dirs: [KIMI_CODE_AGENTS_DIR, KIMI_CODE_SKILLS_DIR],
20225
- files: [KIMI_CODE_ROOT_FILE, KIMI_CODE_NESTED_ROOT_FILE],
20403
+ files: [KIMI_CODE_ROOT_FILE],
20404
+ supersededFiles: [KIMI_CODE_NESTED_ROOT_FILE],
20226
20405
  // Kimi Code's own MCP config, in the same directory as the credential-
20227
20406
  // bearing config.toml this layout already refuses to delete.
20228
20407
  coOwnedFiles: [KIMI_CODE_MCP_FILE]
@@ -20870,7 +21049,7 @@ function lintMcp9(canonical) {
20870
21049
  }
20871
21050
  return diagnostics;
20872
21051
  }
20873
- function lintAgents4(canonical) {
21052
+ function lintAgents5(canonical) {
20874
21053
  return canonical.agents.flatMap((agent) => {
20875
21054
  const dropped = DROPPED_AGENT_FIELDS.filter(([, has]) => has(agent)).map(([field]) => field);
20876
21055
  if (dropped.length === 0) return [];
@@ -20961,7 +21140,7 @@ var init_kimi_code2 = __esm({
20961
21140
  generateHooks: generateHooks14,
20962
21141
  generatePermissions: generatePermissions16,
20963
21142
  importFrom: importFromKimiCode,
20964
- lint: lintAgents4
21143
+ lint: lintAgents5
20965
21144
  };
20966
21145
  capabilities9 = {
20967
21146
  rules: "native",
@@ -23259,7 +23438,7 @@ function lintAgentFields(agent) {
23259
23438
  )
23260
23439
  ];
23261
23440
  }
23262
- function lintAgents5(canonical) {
23441
+ function lintAgents6(canonical) {
23263
23442
  const diagnostics = [];
23264
23443
  for (const agent of canonical.agents) {
23265
23444
  diagnostics.push(...lintAgentFields(agent));
@@ -23321,7 +23500,7 @@ var init_openhands2 = __esm({
23321
23500
  generatePermissions: generatePermissions19,
23322
23501
  importFrom: importFromOpenhands,
23323
23502
  // Ungated by feature, so agent-only feature sets still get the warning.
23324
- lint: lintAgents5
23503
+ lint: lintAgents6
23325
23504
  };
23326
23505
  descriptor24 = {
23327
23506
  id: OPENHANDS_TARGET,
@@ -27136,7 +27315,7 @@ var init_constants34 = __esm({
27136
27315
  WINDSURF_GLOBAL_AGENTS_SKILLS_DIR = ".agents/skills";
27137
27316
  }
27138
27317
  });
27139
- function ruleSlug4(source) {
27318
+ function ruleSlug3(source) {
27140
27319
  const name = basename(source, ".md");
27141
27320
  return name === "_root" ? "root" : name;
27142
27321
  }
@@ -27157,7 +27336,7 @@ function generateRules32(canonical) {
27157
27336
  for (const rule of canonical.rules) {
27158
27337
  if (rule.root) continue;
27159
27338
  if (rule.targets.length > 0 && !rule.targets.includes("windsurf")) continue;
27160
- const slug = ruleSlug4(rule.source);
27339
+ const slug = ruleSlug3(rule.source);
27161
27340
  const normalizedTrigger = rule.trigger || (rule.globs.length > 0 ? "glob" : void 0);
27162
27341
  const frontmatter = {
27163
27342
  description: rule.description || void 0,
@@ -27261,19 +27440,34 @@ var init_mcp4 = __esm({
27261
27440
  }
27262
27441
  });
27263
27442
 
27264
- // src/targets/windsurf/generator/hooks.ts
27443
+ // src/targets/windsurf/hook-events.ts
27265
27444
  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
27445
  return event.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
27276
27446
  }
27447
+ function canonicalHookEventName(event) {
27448
+ if (KNOWN_CANONICAL_HOOK_EVENTS.includes(event)) return event;
27449
+ return WINDSURF_TO_CANONICAL.get(event) ?? null;
27450
+ }
27451
+ var KNOWN_CANONICAL_HOOK_EVENTS, WINDSURF_TO_CANONICAL;
27452
+ var init_hook_events = __esm({
27453
+ "src/targets/windsurf/hook-events.ts"() {
27454
+ init_hook_types();
27455
+ KNOWN_CANONICAL_HOOK_EVENTS = [
27456
+ "PreToolUse",
27457
+ "PostToolUse",
27458
+ "Notification",
27459
+ "UserPromptSubmit",
27460
+ "SubagentStart",
27461
+ "SubagentStop",
27462
+ ...BEST_EFFORT_HOOK_EVENTS
27463
+ ];
27464
+ WINDSURF_TO_CANONICAL = new Map(
27465
+ KNOWN_CANONICAL_HOOK_EVENTS.map((event) => [windsurfEventName(event), event])
27466
+ );
27467
+ }
27468
+ });
27469
+
27470
+ // src/targets/windsurf/generator/hooks.ts
27277
27471
  function toWindsurfHooks(hooks) {
27278
27472
  const result2 = {};
27279
27473
  for (const [event, entries] of Object.entries(hooks)) {
@@ -27301,6 +27495,7 @@ var init_hooks4 = __esm({
27301
27495
  "src/targets/windsurf/generator/hooks.ts"() {
27302
27496
  init_hook_command();
27303
27497
  init_constants34();
27498
+ init_hook_events();
27304
27499
  }
27305
27500
  });
27306
27501
 
@@ -27432,6 +27627,57 @@ var init_skills_adapter5 = __esm({
27432
27627
  init_constants34();
27433
27628
  }
27434
27629
  });
27630
+ function toHookEntry(raw) {
27631
+ if (!raw || typeof raw !== "object") return null;
27632
+ const obj = raw;
27633
+ const matcher = obj.matcher;
27634
+ if (typeof matcher !== "string") return null;
27635
+ const command = getHookText(obj);
27636
+ if (!command) return null;
27637
+ const type = typeof obj.type === "string" && VALID_TYPES.includes(obj.type) ? obj.type : void 0;
27638
+ const timeout = typeof obj.timeout === "number" && Number.isFinite(obj.timeout) ? obj.timeout : void 0;
27639
+ const prompt = getHookPrompt(obj) || void 0;
27640
+ return {
27641
+ matcher,
27642
+ command,
27643
+ ...timeout !== void 0 && { timeout },
27644
+ ...type && { type },
27645
+ ...prompt && { prompt }
27646
+ };
27647
+ }
27648
+ async function parseHooks(hooksPath, onParseError) {
27649
+ const content = await readFileSafe(hooksPath);
27650
+ if (content === null) return null;
27651
+ if (!content.trim()) return {};
27652
+ let parsed;
27653
+ try {
27654
+ parsed = parse(content);
27655
+ } catch (err) {
27656
+ return failSyntax(hooksPath, err, onParseError);
27657
+ }
27658
+ if (!parsed || typeof parsed !== "object") return null;
27659
+ const result2 = {};
27660
+ const obj = parsed;
27661
+ for (const [key, val] of Object.entries(obj)) {
27662
+ if (!Array.isArray(val)) continue;
27663
+ const entries = [];
27664
+ for (const item of val) {
27665
+ const entry = toHookEntry(item);
27666
+ if (entry) entries.push(entry);
27667
+ }
27668
+ if (entries.length > 0) result2[key] = entries;
27669
+ }
27670
+ return result2;
27671
+ }
27672
+ var VALID_TYPES;
27673
+ var init_hooks5 = __esm({
27674
+ "src/canonical/features/hooks.ts"() {
27675
+ init_syntax_error();
27676
+ init_fs();
27677
+ init_hook_command();
27678
+ VALID_TYPES = ["command", "prompt"];
27679
+ }
27680
+ });
27435
27681
  async function importWindsurfHooks(projectRoot, results) {
27436
27682
  const hooksPath = join(projectRoot, WINDSURF_HOOKS_FILE);
27437
27683
  const hooksContent = await readFileSafe(hooksPath);
@@ -27439,9 +27685,10 @@ async function importWindsurfHooks(projectRoot, results) {
27439
27685
  try {
27440
27686
  const parsed = JSON.parse(hooksContent);
27441
27687
  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
27688
  const destPath = join(projectRoot, WINDSURF_CANONICAL_HOOKS);
27689
+ const existing = await parseHooks(destPath) ?? {};
27690
+ const canonical = windsurfHooksToCanonical(parsed.hooks, existing);
27691
+ if (Object.keys(canonical).length === 0) return;
27445
27692
  await mkdirp(dirname(destPath));
27446
27693
  await writeFileAtomic(destPath, stringify(canonical));
27447
27694
  results.push({
@@ -27453,54 +27700,63 @@ async function importWindsurfHooks(projectRoot, results) {
27453
27700
  } catch {
27454
27701
  }
27455
27702
  }
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;
27703
+ function preservedMatcher(existing, event, command) {
27704
+ const match = existing[event]?.find((entry) => entry.command === command);
27705
+ return match?.matcher ?? WILDCARD_MATCHER;
27466
27706
  }
27467
- function windsurfHooksToCanonical(hooks) {
27707
+ function legacyEntries(entry) {
27708
+ const matcher = typeof entry.matcher === "string" && entry.matcher.trim() ? entry.matcher : WILDCARD_MATCHER;
27709
+ const hooksList = Array.isArray(entry.hooks) ? entry.hooks : [];
27710
+ const out2 = [];
27711
+ for (const item of hooksList) {
27712
+ if (!item || typeof item !== "object") continue;
27713
+ const hook = item;
27714
+ const command = typeof hook.command === "string" ? hook.command : typeof hook.prompt === "string" ? hook.prompt : "";
27715
+ if (!command.trim()) continue;
27716
+ const canonical = {
27717
+ matcher,
27718
+ type: hook.type === "prompt" ? "prompt" : "command",
27719
+ command
27720
+ };
27721
+ if (typeof hook.timeout === "number") canonical.timeout = hook.timeout;
27722
+ out2.push(canonical);
27723
+ }
27724
+ return out2;
27725
+ }
27726
+ function windsurfHooksToCanonical(hooks, existing) {
27468
27727
  const result2 = {};
27469
27728
  for (const [event, entries] of Object.entries(hooks)) {
27470
27729
  if (!Array.isArray(entries)) continue;
27471
27730
  const mappedEvent = canonicalHookEventName(event);
27731
+ if (mappedEvent === null) continue;
27472
27732
  const canonicalEntries = [];
27473
27733
  for (const entry of entries) {
27474
27734
  if (!entry || typeof entry !== "object") continue;
27475
27735
  const e = entry;
27476
27736
  if (typeof e.command === "string" && e.command.trim()) {
27477
27737
  canonicalEntries.push({
27478
- matcher: ".*",
27738
+ matcher: preservedMatcher(existing, mappedEvent, e.command),
27479
27739
  type: "command",
27480
27740
  command: e.command
27481
27741
  });
27482
27742
  continue;
27483
27743
  }
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
- }
27744
+ canonicalEntries.push(...legacyEntries(e));
27499
27745
  }
27500
27746
  if (canonicalEntries.length > 0) result2[mappedEvent] = canonicalEntries;
27501
27747
  }
27502
27748
  return result2;
27503
27749
  }
27750
+ var WILDCARD_MATCHER;
27751
+ var init_importer_hooks2 = __esm({
27752
+ "src/targets/windsurf/importer-hooks.ts"() {
27753
+ init_hooks5();
27754
+ init_fs();
27755
+ init_constants34();
27756
+ init_hook_events();
27757
+ WILDCARD_MATCHER = "*";
27758
+ }
27759
+ });
27504
27760
  async function importWindsurfMcp(projectRoot, results) {
27505
27761
  const sourceCandidates = [WINDSURF_MCP_EXAMPLE_FILE, WINDSURF_MCP_CONFIG_FILE];
27506
27762
  for (const relPath of sourceCandidates) {
@@ -27524,8 +27780,8 @@ async function importWindsurfMcp(projectRoot, results) {
27524
27780
  }
27525
27781
  }
27526
27782
  }
27527
- var init_importer_hooks_mcp = __esm({
27528
- "src/targets/windsurf/importer-hooks-mcp.ts"() {
27783
+ var init_importer_mcp = __esm({
27784
+ "src/targets/windsurf/importer-mcp.ts"() {
27529
27785
  init_fs();
27530
27786
  init_constants34();
27531
27787
  }
@@ -27675,7 +27931,8 @@ var init_importer32 = __esm({
27675
27931
  init_constants34();
27676
27932
  init_importer_workflows();
27677
27933
  init_skills_adapter5();
27678
- init_importer_hooks_mcp();
27934
+ init_importer_hooks2();
27935
+ init_importer_mcp();
27679
27936
  }
27680
27937
  });
27681
27938
 
@@ -27760,9 +28017,28 @@ function lintPermissions22(canonical) {
27760
28017
  )
27761
28018
  ];
27762
28019
  }
28020
+ function lintHooks23(canonical) {
28021
+ if (!canonical.hooks) return [];
28022
+ const diagnostics = [];
28023
+ for (const [event, entries] of Object.entries(canonical.hooks)) {
28024
+ for (const entry of entries ?? []) {
28025
+ if (WILDCARD_MATCHERS.has(entry.matcher.trim())) continue;
28026
+ diagnostics.push(
28027
+ createWarning(
28028
+ ".agentsmesh/hooks.yaml",
28029
+ "windsurf",
28030
+ `Windsurf hooks have no matcher field; ${event} hook "${entry.command}" runs on every ${event} event (matcher "${entry.matcher}" is not projected).`
28031
+ )
28032
+ );
28033
+ }
28034
+ }
28035
+ return diagnostics;
28036
+ }
28037
+ var WILDCARD_MATCHERS;
27763
28038
  var init_lint31 = __esm({
27764
28039
  "src/targets/windsurf/lint.ts"() {
27765
28040
  init_helpers();
28041
+ WILDCARD_MATCHERS = /* @__PURE__ */ new Set(["", "*", ".*"]);
27766
28042
  }
27767
28043
  });
27768
28044
 
@@ -27918,6 +28194,7 @@ var init_windsurf2 = __esm({
27918
28194
  lintRules: lintRules32,
27919
28195
  lint: {
27920
28196
  commands: lintCommands10,
28197
+ hooks: lintHooks23,
27921
28198
  mcp: lintMcp13,
27922
28199
  permissions: lintPermissions22
27923
28200
  },
@@ -29787,6 +30064,7 @@ function ruleNameFromSource(source) {
29787
30064
  }
29788
30065
 
29789
30066
  // src/core/generate/collision.ts
30067
+ init_fs_text_encoding();
29790
30068
  init_target_ids();
29791
30069
  var AGENTS_SUFFIX = "AGENTS.md";
29792
30070
  function statusRank(status) {
@@ -29895,7 +30173,7 @@ function assertNoCaseOnlyPathCollisions(results) {
29895
30173
  }
29896
30174
  }
29897
30175
  function refreshResultStatus(result2) {
29898
- const status = result2.currentContent === void 0 ? "created" : result2.currentContent !== result2.content ? "updated" : "unchanged";
30176
+ const status = result2.currentContent === void 0 ? "created" : normalizeTextPayload(result2.path, result2.currentContent) !== normalizeTextPayload(result2.path, result2.content) ? "updated" : "unchanged";
29899
30177
  return result2.status === status ? result2 : { ...result2, status };
29900
30178
  }
29901
30179
 
@@ -30300,8 +30578,46 @@ function mergeLocalConfig(project26, local) {
30300
30578
  if (Array.isArray(local.extends) && local.extends.length > 0) {
30301
30579
  merged.extends = [...project26.extends ?? [], ...local.extends];
30302
30580
  }
30581
+ if (Array.isArray(local.plugins)) {
30582
+ merged.plugins = mergeById(project26.plugins, local.plugins);
30583
+ }
30584
+ if (Array.isArray(local.pluginTargets)) {
30585
+ merged.pluginTargets = [.../* @__PURE__ */ new Set([...project26.pluginTargets, ...local.pluginTargets])];
30586
+ }
30587
+ if (typeof local.collaboration === "object" && local.collaboration !== null && !Array.isArray(local.collaboration)) {
30588
+ merged.collaboration = local.collaboration;
30589
+ }
30590
+ warnUnhandledLocalKeys(local);
30303
30591
  return merged;
30304
30592
  }
30593
+ var LOCAL_KEYS = /* @__PURE__ */ new Set([
30594
+ "version",
30595
+ "targets",
30596
+ "features",
30597
+ "overrides",
30598
+ "conversions",
30599
+ "extends",
30600
+ "plugins",
30601
+ "pluginTargets",
30602
+ "collaboration"
30603
+ ]);
30604
+ function warnUnhandledLocalKeys(local) {
30605
+ const unknown = Object.keys(local).filter((key) => !LOCAL_KEYS.has(key));
30606
+ if (unknown.length === 0) return;
30607
+ logger.warn(
30608
+ `agentsmesh.local.yaml: ignoring unknown key(s) ${unknown.join(", ")}; supported keys are ${[...LOCAL_KEYS].join(", ")}.`
30609
+ );
30610
+ }
30611
+ function mergeById(project26, local) {
30612
+ const byId = /* @__PURE__ */ new Map();
30613
+ const anonymous = [];
30614
+ for (const entry of [...project26, ...local]) {
30615
+ const id = typeof entry === "object" && entry !== null && typeof entry.id === "string" ? entry.id : void 0;
30616
+ if (id === void 0) anonymous.push(entry);
30617
+ else byId.set(id, entry);
30618
+ }
30619
+ return [...byId.values(), ...anonymous];
30620
+ }
30305
30621
  async function loadConfigFromExactDir(configDir) {
30306
30622
  const configPath = join(configDir, CONFIG_FILENAME);
30307
30623
  let config = await loadConfig(configPath);
@@ -30790,24 +31106,14 @@ async function sweepStaleCache(cacheDir, maxAgeMs) {
30790
31106
 
30791
31107
  // src/config/remote/remote-fetcher.ts
30792
31108
  var MAX_CACHE_KEY_LENGTH = 80;
31109
+ var CACHE_KEY_HASH_LENGTH = 12;
30793
31110
  function buildCacheKey(provider, identifier, ref) {
30794
31111
  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;
31112
+ const [org, repo] = provider === "github" ? identifier.split("/", 2) : [];
31113
+ const readable = org && repo ? `${safe(org)}--${safe(repo)}--${safe(ref)}` : `${safe(provider)}__${safe(identifier)}__${safe(ref)}`;
31114
+ const hash = createHash("sha256").update(`${provider}|${identifier}|${ref}`).digest("hex").slice(0, CACHE_KEY_HASH_LENGTH);
31115
+ const maxReadable = MAX_CACHE_KEY_LENGTH - CACHE_KEY_HASH_LENGTH - 2;
31116
+ return `${readable.slice(0, maxReadable)}--${hash}`;
30811
31117
  }
30812
31118
  function getCacheDir() {
30813
31119
  const env = process.env.AGENTSMESH_CACHE;
@@ -30919,6 +31225,13 @@ async function resolveExtendPaths(config, configDir, options = {}) {
30919
31225
  return result2;
30920
31226
  }
30921
31227
 
31228
+ // src/canonical/features/empty-file.ts
31229
+ function isEmptyCanonicalFile(content, path) {
31230
+ if (content.trim() !== "") return false;
31231
+ logger.warn(`Skipping empty canonical file ${path.replaceAll("\\", "/")}`);
31232
+ return true;
31233
+ }
31234
+
30922
31235
  // src/canonical/features/rules.ts
30923
31236
  init_fs();
30924
31237
  init_markdown();
@@ -31058,7 +31371,8 @@ async function parseRules(rulesDir, opts = {}) {
31058
31371
  const rules = [];
31059
31372
  for (const path of mdFiles) {
31060
31373
  const content = await readFileSafe(path);
31061
- if (!content) continue;
31374
+ if (content === null) continue;
31375
+ if (isEmptyCanonicalFile(content, path)) continue;
31062
31376
  const parsed = parseOrSkipFrontmatter(content, path, opts.onParseError);
31063
31377
  if (!parsed) continue;
31064
31378
  const { frontmatter, body } = parsed;
@@ -31112,7 +31426,8 @@ async function parseCommands(commandsDir, opts = {}) {
31112
31426
  const commands = [];
31113
31427
  for (const path of mdFiles) {
31114
31428
  const content = await readFileSafe(path);
31115
- if (!content) continue;
31429
+ if (content === null) continue;
31430
+ if (isEmptyCanonicalFile(content, path)) continue;
31116
31431
  const parsed = parseOrSkipFrontmatter(content, path, opts.onParseError);
31117
31432
  if (!parsed) continue;
31118
31433
  const { frontmatter, body } = parsed;
@@ -31172,15 +31487,14 @@ async function parseAgents(agentsDir, opts = {}) {
31172
31487
  const agents = [];
31173
31488
  for (const path of mdFiles) {
31174
31489
  const content = await readFileSafe(path);
31175
- if (!content) continue;
31490
+ if (content === null) continue;
31491
+ if (isEmptyCanonicalFile(content, path)) continue;
31176
31492
  const parsed = parseOrSkipFrontmatter(content, path, opts.onParseError);
31177
31493
  if (!parsed) continue;
31178
31494
  const { frontmatter, body } = parsed;
31179
31495
  const name = basename(path, ".md");
31180
31496
  assertCanonicalName("agent", name);
31181
- const toolsCamel = toStrArray2(frontmatter.tools);
31182
- const toolsKebab = toStrArray2(frontmatter["tools"]);
31183
- const tools = toolsCamel.length > 0 ? toolsCamel : toolsKebab;
31497
+ const tools = toStrArray2(frontmatter.tools);
31184
31498
  const disallowedCamel = toStrArray2(frontmatter.disallowedTools);
31185
31499
  const disallowedKebab = toStrArray2(frontmatter["disallowed-tools"]);
31186
31500
  const disallowedTools = disallowedCamel.length > 0 ? disallowedCamel : disallowedKebab;
@@ -31302,20 +31616,21 @@ async function parseSkills(skillsDir, opts = {}) {
31302
31616
  init_mcp();
31303
31617
 
31304
31618
  // src/canonical/features/permissions.ts
31619
+ init_syntax_error();
31305
31620
  init_fs();
31306
31621
  function ensureStringArray(val) {
31307
31622
  if (!Array.isArray(val)) return [];
31308
31623
  return val.filter((x) => typeof x === "string");
31309
31624
  }
31310
- async function parsePermissions(permissionsPath) {
31625
+ async function parsePermissions(permissionsPath, onParseError) {
31311
31626
  const content = await readFileSafe(permissionsPath);
31312
31627
  if (content === null) return null;
31313
31628
  if (!content.trim()) return { allow: [], deny: [], ask: [] };
31314
31629
  let parsed;
31315
31630
  try {
31316
31631
  parsed = parse(content);
31317
- } catch {
31318
- return null;
31632
+ } catch (err) {
31633
+ return failSyntax(permissionsPath, err, onParseError);
31319
31634
  }
31320
31635
  if (!parsed || typeof parsed !== "object") return null;
31321
31636
  const obj = parsed;
@@ -31325,52 +31640,8 @@ async function parsePermissions(permissionsPath) {
31325
31640
  return { allow, deny, ask };
31326
31641
  }
31327
31642
 
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
- }
31643
+ // src/canonical/load/loader.ts
31644
+ init_hooks5();
31374
31645
 
31375
31646
  // src/canonical/features/ignore.ts
31376
31647
  init_fs();
@@ -31397,9 +31668,9 @@ async function loadCanonicalFiles(canonicalDirOrProjectRoot, opts = {}) {
31397
31668
  parseCommands(join(canonicalDir, "commands"), opts),
31398
31669
  parseAgents(join(canonicalDir, "agents"), opts),
31399
31670
  parseSkills(join(canonicalDir, "skills"), opts),
31400
- parseMcp(join(canonicalDir, "mcp.json")),
31401
- parsePermissions(join(canonicalDir, "permissions.yaml")),
31402
- parseHooks(join(canonicalDir, "hooks.yaml")),
31671
+ parseMcp(join(canonicalDir, "mcp.json"), opts.onParseError),
31672
+ parsePermissions(join(canonicalDir, "permissions.yaml"), opts.onParseError),
31673
+ parseHooks(join(canonicalDir, "hooks.yaml"), opts.onParseError),
31403
31674
  parseIgnore(join(canonicalDir, "ignore"))
31404
31675
  ]);
31405
31676
  return {
@@ -31413,13 +31684,13 @@ async function loadCanonicalFiles(canonicalDirOrProjectRoot, opts = {}) {
31413
31684
  ignore
31414
31685
  };
31415
31686
  }
31416
- function ruleSlug5(r) {
31687
+ function ruleSlug4(r) {
31417
31688
  return basename(r.source, ".md");
31418
31689
  }
31419
31690
  function mergeCanonicalFiles(base, overlay) {
31420
- const baseRuleMap = new Map(base.rules.map((r) => [ruleSlug5(r), r]));
31691
+ const baseRuleMap = new Map(base.rules.map((r) => [ruleSlug4(r), r]));
31421
31692
  for (const r of overlay.rules) {
31422
- baseRuleMap.set(ruleSlug5(r), r);
31693
+ baseRuleMap.set(ruleSlug4(r), r);
31423
31694
  }
31424
31695
  const baseCmdMap = new Map(base.commands.map((c2) => [c2.name, c2]));
31425
31696
  for (const c2 of overlay.commands) {
@@ -32302,6 +32573,7 @@ function gateExtendElevatedArtifacts(canonical, ext) {
32302
32573
  });
32303
32574
  }
32304
32575
  init_mcp();
32576
+ init_hooks5();
32305
32577
 
32306
32578
  // src/install/pack/pack-reader.ts
32307
32579
  init_fs();
@@ -33067,88 +33339,6 @@ function collectOrphans(graph, findings) {
33067
33339
  }
33068
33340
  }
33069
33341
 
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
33342
  // src/lessons/regex-linear/nfa-compile.ts
33153
33343
  var MAX_NFA_STATES = 2e3;
33154
33344
  var Builder = class {
@@ -33669,6 +33859,93 @@ function collectFanout(graph, findings) {
33669
33859
  });
33670
33860
  }
33671
33861
  }
33862
+ function normalizeRule(rule) {
33863
+ return rule.trim().replace(/\s+/g, " ").toLowerCase();
33864
+ }
33865
+
33866
+ // src/lessons/ranking-text.ts
33867
+ var K1 = 1.5;
33868
+ var B = 0.75;
33869
+ var STOP = /* @__PURE__ */ new Set([
33870
+ "the",
33871
+ "a",
33872
+ "an",
33873
+ "to",
33874
+ "of",
33875
+ "in",
33876
+ "and",
33877
+ "or",
33878
+ "for",
33879
+ "is",
33880
+ "on",
33881
+ "at",
33882
+ "with",
33883
+ "be",
33884
+ "as",
33885
+ "it",
33886
+ "that",
33887
+ "this",
33888
+ "its",
33889
+ "must"
33890
+ ]);
33891
+ function tokenize(text) {
33892
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
33893
+ }
33894
+ function queryTerms(query) {
33895
+ const parts = [];
33896
+ if (query.keyword !== void 0) parts.push(query.keyword);
33897
+ if (query.file !== void 0) parts.push(query.file);
33898
+ if (query.command !== void 0) parts.push(query.command);
33899
+ return tokenize(parts.join(" "));
33900
+ }
33901
+ function buildCorpus(graph) {
33902
+ const docs = [];
33903
+ const df = /* @__PURE__ */ new Map();
33904
+ let total = 0;
33905
+ let n = 0;
33906
+ for (const lesson of Object.values(graph.lessons)) {
33907
+ if (lesson.status !== "active") continue;
33908
+ const toks = tokenize(lesson.rule);
33909
+ n += 1;
33910
+ total += toks.length;
33911
+ docs.push(toks.length);
33912
+ for (const t of new Set(toks)) df.set(t, (df.get(t) ?? 0) + 1);
33913
+ }
33914
+ const N = Math.max(n, 1);
33915
+ const idf = /* @__PURE__ */ new Map();
33916
+ for (const [t, f] of df) idf.set(t, Math.log(1 + (N - f + 0.5) / (f + 0.5)));
33917
+ return { idf, avgdl: total / N || 1 };
33918
+ }
33919
+ function bm25(terms, ruleText, corpus) {
33920
+ const toks = tokenize(ruleText);
33921
+ const dl = toks.length || 1;
33922
+ const tf = /* @__PURE__ */ new Map();
33923
+ for (const t of toks) tf.set(t, (tf.get(t) ?? 0) + 1);
33924
+ let score = 0;
33925
+ for (const t of new Set(terms)) {
33926
+ const f = tf.get(t) ?? 0;
33927
+ if (f === 0) continue;
33928
+ const idf = corpus.idf.get(t);
33929
+ score += idf * (f * (K1 + 1)) / (f + K1 * (1 - B + B * dl / corpus.avgdl));
33930
+ }
33931
+ return score;
33932
+ }
33933
+
33934
+ // src/lessons/keyword-signal.ts
33935
+ var MAX_RECOMMENDED_KEYWORD_TOKENS = 5;
33936
+ function isLowSignalKeyword(pattern) {
33937
+ return tokenize(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
33938
+ }
33939
+ function splitRawTokens(pattern) {
33940
+ return pattern.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
33941
+ }
33942
+ function keywordNeedleLosesTokens(pattern) {
33943
+ const raw = splitRawTokens(pattern);
33944
+ if (raw.length < 2) return false;
33945
+ return tokenize(pattern).length !== raw.length;
33946
+ }
33947
+
33948
+ // src/lessons/validate-keywords.ts
33672
33949
  function collectLowSignalKeywords(graph, findings) {
33673
33950
  const activeTriggerIds2 = /* @__PURE__ */ new Set();
33674
33951
  for (const lesson of Object.values(graph.lessons)) {
@@ -33682,7 +33959,7 @@ function collectLowSignalKeywords(graph, findings) {
33682
33959
  findings.push({
33683
33960
  level: "warning",
33684
33961
  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.`,
33962
+ 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
33963
  triggerId
33687
33964
  });
33688
33965
  }
@@ -33707,9 +33984,62 @@ function collectStopwordKeywords(graph, findings) {
33707
33984
  });
33708
33985
  }
33709
33986
  }
33710
- function normalizeRule(rule) {
33711
- return rule.trim().replace(/\s+/g, " ").toLowerCase();
33987
+
33988
+ // src/lessons/glob-breadth.ts
33989
+ var WILDCARD = /[*?[\]]/;
33990
+ function globNarrowness(pattern) {
33991
+ const segments = pattern.replaceAll("\\", "/").split("/").filter((segment) => segment !== "" && segment !== ".");
33992
+ if (segments.length === 0) return 0;
33993
+ let literal = 0;
33994
+ let globstars = 0;
33995
+ for (const segment of segments) {
33996
+ if (segment === "**") globstars += 1;
33997
+ else if (!WILDCARD.test(segment)) literal += 1;
33998
+ }
33999
+ return literal / (segments.length + globstars);
34000
+ }
34001
+ var BROAD_GLOB_NARROWNESS = 0.34;
34002
+ function isBroadFileGlob(pattern) {
34003
+ return globNarrowness(pattern) < BROAD_GLOB_NARROWNESS;
34004
+ }
34005
+
34006
+ // src/lessons/command-pattern-breadth.ts
34007
+ var COMMAND_PROBE_CORPUS = [
34008
+ "git status",
34009
+ 'git commit -m "wip"',
34010
+ "pnpm test",
34011
+ "npx vitest run src/x.test.ts",
34012
+ "ls -la",
34013
+ "cat README.md",
34014
+ "rm -rf dist",
34015
+ "mkdir -p build/out",
34016
+ "node scripts/build.js",
34017
+ "docker compose up -d",
34018
+ "curl -s https://example.com",
34019
+ "echo hello > out.txt",
34020
+ "sed -i 's/a/b/' file.txt",
34021
+ "pnpm lint --fix",
34022
+ "python3 -m pytest",
34023
+ "cargo build --release",
34024
+ "make",
34025
+ "npm install --global typescript",
34026
+ "cp a.txt b.txt",
34027
+ "grep -rn TODO src"
34028
+ ];
34029
+ var BROAD_HIT_RATIO = 0.5;
34030
+ var PROBE_BUDGET = 1e5;
34031
+ function isBroadCommandPattern(pattern) {
34032
+ const matcher = getCommandMatcher(pattern);
34033
+ if (matcher === null) return false;
34034
+ if (matcher.test("", { remaining: PROBE_BUDGET })) return true;
34035
+ let hits = 0;
34036
+ for (const command of COMMAND_PROBE_CORPUS) {
34037
+ if (matcher.test(command, { remaining: PROBE_BUDGET })) hits += 1;
34038
+ }
34039
+ return hits > COMMAND_PROBE_CORPUS.length * BROAD_HIT_RATIO;
33712
34040
  }
34041
+
34042
+ // src/lessons/validate-liveness.ts
33713
34043
  function activeTriggerIds(graph) {
33714
34044
  const ids = /* @__PURE__ */ new Set();
33715
34045
  for (const lesson of Object.values(graph.lessons)) {
@@ -33761,6 +34091,34 @@ function collectRunnerAnchoredPatterns(graph, findings) {
33761
34091
  });
33762
34092
  }
33763
34093
  }
34094
+ function collectBroadFileGlobs(graph, findings) {
34095
+ const active = activeTriggerIds(graph);
34096
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
34097
+ if (trigger.kind !== "file_glob") continue;
34098
+ if (!active.has(triggerId)) continue;
34099
+ if (!isBroadFileGlob(trigger.pattern)) continue;
34100
+ findings.push({
34101
+ level: "warning",
34102
+ code: "BROAD_FILE_GLOB",
34103
+ 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\`.`,
34104
+ triggerId
34105
+ });
34106
+ }
34107
+ }
34108
+ function collectBroadCommandPatterns(graph, findings) {
34109
+ const active = activeTriggerIds(graph);
34110
+ for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
34111
+ if (trigger.kind !== "command_pattern") continue;
34112
+ if (!active.has(triggerId)) continue;
34113
+ if (!isBroadCommandPattern(trigger.pattern)) continue;
34114
+ findings.push({
34115
+ level: "warning",
34116
+ code: "BROAD_COMMAND_PATTERN",
34117
+ 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\`.`,
34118
+ triggerId
34119
+ });
34120
+ }
34121
+ }
33764
34122
 
33765
34123
  // src/lessons/validate.ts
33766
34124
  function validateLessonsGraph(graph, options = {}) {
@@ -33788,6 +34146,8 @@ function validateLessonsGraph(graph, options = {}) {
33788
34146
  collectLowSignalKeywords(graph, findings);
33789
34147
  collectStopwordKeywords(graph, findings);
33790
34148
  collectRunnerAnchoredPatterns(graph, findings);
34149
+ collectBroadCommandPatterns(graph, findings);
34150
+ collectBroadFileGlobs(graph, findings);
33791
34151
  if (options.knownPaths !== void 0) collectDeadFileGlobs(graph, findings, options.knownPaths);
33792
34152
  const ok = findings.every((f) => f.level !== "error");
33793
34153
  return { ok, findings };
@@ -33839,13 +34199,13 @@ function diag(level, file, message) {
33839
34199
 
33840
34200
  // src/core/lint/linter.ts
33841
34201
  var EXCLUDE_DIRS = ["node_modules", ".git", "dist", "coverage", ".agentsmesh"];
34202
+ function isExcludedProjectPath(rel2) {
34203
+ const posix9 = rel2.replaceAll("\\", "/");
34204
+ return EXCLUDE_DIRS.some((d) => posix9.includes(`/${d}/`) || posix9.startsWith(`${d}/`));
34205
+ }
33842
34206
  async function getProjectFiles(projectRoot) {
33843
34207
  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));
34208
+ return all.filter((p) => !isExcludedProjectPath(relative(projectRoot, p))).map((p) => relative(projectRoot, p));
33849
34209
  }
33850
34210
  async function runLint(config, canonical, projectRoot, targetFilter, options = {}) {
33851
34211
  const scope = options.scope ?? "project";
@@ -34107,6 +34467,7 @@ async function diffOutputChecksums(rootBase, lockOutputs) {
34107
34467
  // src/core/generate/stale-cleanup.ts
34108
34468
  init_fs();
34109
34469
  init_builtin_targets();
34470
+ init_registry();
34110
34471
  async function listFiles2(root, base = root) {
34111
34472
  const entries = await readdir(root, { withFileTypes: true });
34112
34473
  const files = [];
@@ -34127,6 +34488,11 @@ function retainedDirs(inactiveTargets, scope) {
34127
34488
  }
34128
34489
  return dirs;
34129
34490
  }
34491
+ function primaryEmitted(target34, scope, expected) {
34492
+ const descriptor34 = getBuiltinTargetDefinition(target34) ?? getDescriptor(target34);
34493
+ const primary = getTargetLayout(target34, scope)?.rootInstructionPath ?? descriptor34?.generators.primaryRootInstructionPath;
34494
+ return primary !== void 0 && expected.has(primary);
34495
+ }
34130
34496
  async function findStaleGeneratedOutputs(args) {
34131
34497
  const expected = new Set(args.expectedPaths);
34132
34498
  const stale = /* @__PURE__ */ new Set();
@@ -34138,7 +34504,13 @@ async function findStaleGeneratedOutputs(args) {
34138
34504
  const managed = getTargetManagedOutputs(target34, scope);
34139
34505
  if (!managed) continue;
34140
34506
  for (const file of managed.coOwnedFiles ?? []) coOwned.add(file);
34141
- for (const file of managed.files) stale.add(file);
34507
+ for (const file of managed.files) {
34508
+ if (generated !== null && !generated.has(file)) continue;
34509
+ stale.add(file);
34510
+ }
34511
+ if (primaryEmitted(target34, scope, expected)) {
34512
+ for (const file of managed.supersededFiles ?? []) stale.add(file);
34513
+ }
34142
34514
  for (const dir of managed.dirs) {
34143
34515
  if (retained.has(dir)) continue;
34144
34516
  const absDir = join(args.projectRoot, dir);
@@ -34578,6 +34950,23 @@ function todayIso() {
34578
34950
  }
34579
34951
 
34580
34952
  // src/lessons/add-errors.ts
34953
+ var EmptyRuleError = class extends Error {
34954
+ code = "EMPTY_RULE";
34955
+ constructor() {
34956
+ super("Lesson rule must not be empty \u2014 pass one imperative sentence.");
34957
+ this.name = "EmptyRuleError";
34958
+ }
34959
+ };
34960
+ var BroadCommandPatternError = class extends Error {
34961
+ constructor(pattern) {
34962
+ super(
34963
+ `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").`
34964
+ );
34965
+ this.pattern = pattern;
34966
+ this.name = "BroadCommandPatternError";
34967
+ }
34968
+ code = "BROAD_COMMAND_PATTERN";
34969
+ };
34581
34970
  var UnknownTopicError = class extends Error {
34582
34971
  constructor(topic) {
34583
34972
  super(`Unknown topic: ${topic}. Pass allowNewTopic + topicSummary to create it.`);
@@ -34617,6 +35006,77 @@ var UnrecallableLessonError = class extends Error {
34617
35006
  code = "UNRECALLABLE_LESSON";
34618
35007
  };
34619
35008
 
35009
+ // src/lessons/trigger-effectiveness.ts
35010
+ function ineffectiveTriggers(graph, triggerIds) {
35011
+ const out2 = [];
35012
+ for (const id of triggerIds) {
35013
+ const trigger = graph.triggers[id];
35014
+ if (trigger === void 0) continue;
35015
+ const reason = ineffectiveReason(trigger.kind, trigger.pattern);
35016
+ if (reason !== null) out2.push({ id, kind: trigger.kind, pattern: trigger.pattern, reason });
35017
+ }
35018
+ return out2;
35019
+ }
35020
+ function ineffectiveReason(kind, pattern) {
35021
+ if (kind === "keyword") {
35022
+ if (tokenize(pattern).length === 0) {
35023
+ return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
35024
+ }
35025
+ if (keywordNeedleLosesTokens(pattern)) {
35026
+ return "keyword contains stopwords/short words, so its needle can never appear as a contiguous run on the mandatory --file/--cmd recall path";
35027
+ }
35028
+ return null;
35029
+ }
35030
+ if (kind === "command_pattern") {
35031
+ let valid = true;
35032
+ try {
35033
+ new RegExp(pattern);
35034
+ } catch {
35035
+ valid = false;
35036
+ }
35037
+ if (!valid) {
35038
+ return "invalid regex \u2014 recall compiles it with new RegExp and swallows the throw as a non-match, so it never fires";
35039
+ }
35040
+ if (!isSafeRegexPattern(pattern)) {
35041
+ return "regex is outside the provably-linear engine \u2014 recall skips it (ReDoS guard), so it never fires";
35042
+ }
35043
+ return null;
35044
+ }
35045
+ return null;
35046
+ }
35047
+ function blockingDeadTriggers(graph, triggerIds) {
35048
+ return ineffectiveTriggers(graph, triggerIds).filter((t) => t.kind !== "command_pattern");
35049
+ }
35050
+
35051
+ // src/lessons/add-gates.ts
35052
+ function assertRuleShape(rule) {
35053
+ const trimmed = rule.trim();
35054
+ if (trimmed.length === 0) throw new EmptyRuleError();
35055
+ if (trimmed.length > MAX_RULE_LENGTH) throw new RuleTooLongError(trimmed.length, MAX_RULE_LENGTH);
35056
+ return trimmed;
35057
+ }
35058
+ function skipsTriggerGates(input, options) {
35059
+ return options.allowNoTrigger === true || input.scope === "always";
35060
+ }
35061
+ function countInputTriggers(triggers) {
35062
+ return (triggers.files?.length ?? 0) + (triggers.commands?.length ?? 0) + (triggers.keywords?.length ?? 0);
35063
+ }
35064
+ function assertTriggerInputs(input, options, existingTriggerCount) {
35065
+ if (!skipsTriggerGates(input, options) && countInputTriggers(input.triggers) === 0 && existingTriggerCount === 0) {
35066
+ throw new NoTriggerError();
35067
+ }
35068
+ if (options.allowNoTrigger !== true) {
35069
+ const broad = (input.triggers.commands ?? []).find(isBroadCommandPattern);
35070
+ if (broad !== void 0) throw new BroadCommandPatternError(broad);
35071
+ }
35072
+ }
35073
+ function assertRecallable(graph, resultingTriggers) {
35074
+ const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
35075
+ if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
35076
+ throw new UnrecallableLessonError(blockingDead);
35077
+ }
35078
+ }
35079
+
34620
35080
  // src/lessons/capture-guardrails.ts
34621
35081
  var WIDE_GLOB_MATCH_COUNT = 40;
34622
35082
  var MAX_RECOMMENDED_TRIGGERS = 8;
@@ -34655,7 +35115,7 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
34655
35115
  if (lowSignal.length > 0) {
34656
35116
  warnings.push({
34657
35117
  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.`
35118
+ 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
35119
  });
34660
35120
  }
34661
35121
  const stopworded = triggers.filter((t) => t.kind === "keyword" && keywordNeedleLosesTokens(t.pattern)).map((t) => t.pattern);
@@ -34716,7 +35176,7 @@ function jaccard(a, b) {
34716
35176
 
34717
35177
  // src/utils/filesystem/process-lock.ts
34718
35178
  init_errors();
34719
- var DEFAULT_STALE_MS = 6e4;
35179
+ var DEFAULT_STALE_MS = 6 * 60 * 60 * 1e3;
34720
35180
  var DEFAULT_RETRIES = 30;
34721
35181
  var DEFAULT_RETRY_DELAY_MS = 200;
34722
35182
  var YOUNG_LOCK_GRACE_MS = 2e3;
@@ -34801,10 +35261,9 @@ async function inspectLock(lockPath) {
34801
35261
  }
34802
35262
  function isStale(meta, staleMs) {
34803
35263
  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);
35264
+ const sameHost = !meta.hostname || meta.hostname === getHostname();
35265
+ if (sameHost && !isProcessAlive(meta.pid)) return true;
35266
+ return Date.now() - meta.started > staleMs;
34808
35267
  }
34809
35268
  function isProcessAlive(pid) {
34810
35269
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -34881,52 +35340,7 @@ async function mutateLessonsGraph(projectRoot, mutator, options = {}) {
34881
35340
  return mutateLessonsGraphLocked(projectRoot, mutator, options);
34882
35341
  }
34883
35342
 
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
35343
  // src/lessons/add.ts
34927
- function countInputTriggers(triggers) {
34928
- return (triggers.files?.length ?? 0) + (triggers.commands?.length ?? 0) + (triggers.keywords?.length ?? 0);
34929
- }
34930
35344
  async function addLesson(projectRoot, input, options = {}) {
34931
35345
  return mutateLessonsGraph(projectRoot, (graph) => addLessonInto(graph, input, options), {
34932
35346
  retries: options.retries
@@ -34934,10 +35348,7 @@ async function addLesson(projectRoot, input, options = {}) {
34934
35348
  }
34935
35349
  function addLessonInto(graph, input, options) {
34936
35350
  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
- }
35351
+ const trimmedRule = assertRuleShape(input.rule);
34941
35352
  const existingId = findExistingLessonByRule(graph, ruleKey2);
34942
35353
  const isNewTopic = graph.topics[input.topic] === void 0;
34943
35354
  if (isNewTopic) {
@@ -34947,29 +35358,23 @@ function addLessonInto(graph, input, options) {
34947
35358
  }
34948
35359
  graph.topics[input.topic] = { summary: options.topicSummary };
34949
35360
  }
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
- }
35361
+ const existing = existingId !== null ? graph.lessons[existingId] : void 0;
35362
+ assertTriggerInputs(input, options, existing?.triggers.length ?? 0);
34957
35363
  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
- }
35364
+ if (!skipsTriggerGates(input, options)) {
35365
+ assertRecallable(
35366
+ graph,
35367
+ existing === void 0 ? triggerIds : union(existing.triggers, triggerIds)
35368
+ );
34964
35369
  }
34965
35370
  if (existingId !== null) {
34966
- const existing = graph.lessons[existingId];
35371
+ const existing2 = graph.lessons[existingId];
34967
35372
  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 } : {},
35373
+ ...existing2,
35374
+ topics: union(existing2.topics, [input.topic]),
35375
+ triggers: union(existing2.triggers, triggerIds),
35376
+ evidence: union(existing2.evidence, input.evidence ?? []),
35377
+ ...existing2.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
34973
35378
  // Re-capturing a rule with --scope always promotes it to always-on.
34974
35379
  ...input.scope === "always" ? { scope: "always" } : {}
34975
35380
  };
@@ -35150,14 +35555,16 @@ function deriveHaystackTokens(query) {
35150
35555
  if (query.command !== void 0) parts.push(query.command);
35151
35556
  if (parts.length === 0) return [];
35152
35557
  const out2 = [];
35153
- for (const raw of parts.join(" ").split(/[^A-Za-z0-9]+/)) {
35154
- if (raw.length === 0) continue;
35558
+ for (const raw of splitTokens(parts.join(" "))) {
35155
35559
  out2.push(raw.toLowerCase());
35156
35560
  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
35561
  if (sub.length > 1) out2.push(...sub);
35158
35562
  }
35159
35563
  return out2;
35160
35564
  }
35565
+ function splitTokens(text) {
35566
+ return text.split(/[^A-Za-z0-9]+/).filter((t) => t.length > 0);
35567
+ }
35161
35568
  function containsRun(needle, hay) {
35162
35569
  if (needle.length === 0) return false;
35163
35570
  for (let i = 0; i + needle.length <= hay.length; i += 1) {
@@ -35173,10 +35580,11 @@ function containsRun(needle, hay) {
35173
35580
  return false;
35174
35581
  }
35175
35582
  function keywordMatches(pattern, query) {
35176
- if (query.keyword !== void 0 && query.keyword.toLowerCase().includes(pattern.toLowerCase())) {
35583
+ const needle = tokenize(pattern);
35584
+ if (query.keyword !== void 0 && containsRun(needle, splitTokens(query.keyword.toLowerCase()))) {
35177
35585
  return true;
35178
35586
  }
35179
- return containsRun(tokenize(pattern), deriveHaystackTokens(query));
35587
+ return containsRun(needle, deriveHaystackTokens(query));
35180
35588
  }
35181
35589
 
35182
35590
  // src/lessons/query.ts
@@ -35242,6 +35650,15 @@ function buildFanout(graph) {
35242
35650
  }
35243
35651
  return fanout;
35244
35652
  }
35653
+ var KEYWORD_NARROWNESS = 0.4;
35654
+ function buildNarrowness(graph) {
35655
+ const narrowness = /* @__PURE__ */ new Map();
35656
+ for (const [id, trigger] of Object.entries(graph.triggers)) {
35657
+ if (trigger.kind === "file_glob") narrowness.set(id, globNarrowness(trigger.pattern));
35658
+ else narrowness.set(id, trigger.kind === "keyword" ? KEYWORD_NARROWNESS : 1);
35659
+ }
35660
+ return narrowness;
35661
+ }
35245
35662
  function buildTopicCoherence(matches) {
35246
35663
  const topicCount = /* @__PURE__ */ new Map();
35247
35664
  for (const { lesson } of matches) {
@@ -35260,7 +35677,7 @@ function buildTopicCoherence(matches) {
35260
35677
  var DEFAULT_RECALL_LIMIT = 10;
35261
35678
  var DEFAULT_RECALL_MAX_TOKENS = 400;
35262
35679
  var RRF_K = 60;
35263
- var SPECIFICITY_WEIGHT = 3;
35680
+ var SPECIFICITY_WEIGHT = 5;
35264
35681
  var TOPIC_COHERENCE_WEIGHT = 2;
35265
35682
  var BM25_WEIGHT = 1;
35266
35683
  var EFFECTIVENESS_WEIGHT = 1;
@@ -35287,12 +35704,15 @@ function rankLessons(graph, query, matches, options = {}) {
35287
35704
  const terms = queryTerms(query);
35288
35705
  const corpus = buildCorpus(graph);
35289
35706
  const fanout = buildFanout(graph);
35707
+ const narrowness = buildNarrowness(graph);
35290
35708
  const coherence = buildTopicCoherence(matches);
35291
35709
  const matchedTriggerIds = collectMatchedTriggerIds(graph, query);
35292
35710
  const scored = matches.map(({ id, lesson }) => {
35293
35711
  const hitTriggers = lesson.triggers.filter((t) => matchedTriggerIds.has(t));
35294
35712
  let specificity = 0;
35295
- for (const t of hitTriggers) specificity = Math.max(specificity, 1 / fanout.get(t));
35713
+ for (const t of hitTriggers) {
35714
+ specificity = Math.max(specificity, (narrowness.get(t) ?? 1) / fanout.get(t));
35715
+ }
35296
35716
  return {
35297
35717
  id,
35298
35718
  lesson,
@@ -35350,7 +35770,8 @@ function defaultLessonsConfig() {
35350
35770
  recallLimit: DEFAULT_RECALL_LIMIT,
35351
35771
  recallMaxTokens: DEFAULT_RECALL_MAX_TOKENS,
35352
35772
  autoPrune: false,
35353
- repairTriggers: false
35773
+ repairTriggers: false,
35774
+ telemetry: false
35354
35775
  };
35355
35776
  }
35356
35777
  function outcomeLogPath(projectRoot) {
@@ -35404,9 +35825,9 @@ var LINE_REFS = String.raw`L\d+(?:\s*,\s*L\d+)*`;
35404
35825
  var LINE_REF_PATTERNS = [
35405
35826
  new RegExp(String.raw`\s*\bSee\s+${LINE_REFS}\.?`, "g"),
35406
35827
  // " See L128." / " See L140, L149"
35407
- new RegExp(String.raw`\s*\((?:${LINE_REFS})\)\.?`, "g"),
35828
+ new RegExp(String.raw`\s*\((?:${LINE_REFS})\)`, "g"),
35408
35829
  // " (L174)" / " (L92, L163)"
35409
- new RegExp(String.raw`\s*\[(?:${LINE_REFS})\]\.?`, "g")
35830
+ new RegExp(String.raw`\s*\[(?:${LINE_REFS})\]`, "g")
35410
35831
  // " [L161, L208]"
35411
35832
  ];
35412
35833
  var ALSO_RELEVANT_PATTERN = /\s*\(also relevant[^)]*\)\s*/g;
@@ -35440,42 +35861,7 @@ async function stripMarkersInGraph(projectRoot, options = {}) {
35440
35861
  });
35441
35862
  return { changedIds, changedCount: changedIds.length };
35442
35863
  }
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
- }
35864
+ init_recall_hook_scaffold();
35479
35865
 
35480
35866
  // src/lessons/merge-driver-setup.ts
35481
35867
  var LESSONS_MERGE_DRIVER = "agentsmesh-lessons";