agentsmesh 0.39.0 → 0.41.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,15 +1,15 @@
1
- import { z } from 'zod';
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';
1
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, constants, rmSync, readdirSync, statSync, chmodSync, renameSync, accessSync, realpathSync, rmdirSync, unlinkSync } from 'fs';
4
2
  import { join, resolve, relative, sep, dirname, basename, extname, win32, posix } from 'path';
5
- import { mkdir, access, readdir, rm, readFile, writeFile, stat, lstat, open, realpath, rename, mkdtemp, cp } from 'fs/promises';
3
+ import { stringify, parse, parseDocument, YAMLSeq, isMap, Document, isSeq, YAMLMap, isScalar, Scalar, Pair } from 'yaml';
4
+ import { z } from 'zod';
5
+ import { access, readdir, mkdir, readFile, rm, writeFile, lstat, open, stat, realpath, rmdir, unlink, rename, mkdtemp, cp } from 'fs/promises';
6
6
  import { setTimeout } from 'timers/promises';
7
7
  import { createHash, randomUUID } from 'crypto';
8
- import picomatch2 from 'picomatch';
8
+ import picomatch from 'picomatch';
9
9
  import { parse as parse$1, stringify as stringify$1 } from 'smol-toml';
10
10
  import { Buffer } from 'buffer';
11
11
  import { homedir, hostname, tmpdir } from 'os';
12
- import { execFile } from 'child_process';
12
+ import { execFile, spawnSync } from 'child_process';
13
13
  import { fileURLToPath, pathToFileURL, URL as URL$1 } from 'url';
14
14
  import { promisify } from 'util';
15
15
  import * as tar from 'tar';
@@ -24,6 +24,104 @@ var __export = (target34, all) => {
24
24
  for (var name in all)
25
25
  __defProp(target34, name, { get: all[name], enumerable: true });
26
26
  };
27
+ function agentsmeshInvocation(projectRoot) {
28
+ return dependsOnAgentsmesh(projectRoot) ? LOCAL_FIRST : BARE;
29
+ }
30
+ function dependsOnAgentsmesh(projectRoot) {
31
+ try {
32
+ const manifest = JSON.parse(
33
+ readFileSync(join(projectRoot, "package.json"), "utf8")
34
+ );
35
+ return DEPENDENCY_FIELDS.some((field) => manifest?.[field]?.agentsmesh !== void 0);
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+ var LOCAL_FIRST, BARE, DEPENDENCY_FIELDS;
41
+ var init_cli_invocation = __esm({
42
+ "src/lessons/cli-invocation.ts"() {
43
+ LOCAL_FIRST = "npx --no --offline agentsmesh";
44
+ BARE = "agentsmesh";
45
+ DEPENDENCY_FIELDS = ["dependencies", "devDependencies", "optionalDependencies"];
46
+ }
47
+ });
48
+ function recallHookCommand(projectRoot) {
49
+ return `${agentsmeshInvocation(projectRoot)} ${RECALL_SUBCOMMAND}`;
50
+ }
51
+ function isRecallHookCommand(command) {
52
+ return typeof command === "string" && command.includes(RECALL_HOOK_COMMAND);
53
+ }
54
+ function isManagedRecallCommand(command) {
55
+ return typeof command === "string" && MANAGED_COMMAND.test(command.trim());
56
+ }
57
+ function isManaged(item) {
58
+ return item instanceof YAMLMap && isManagedRecallCommand(item.get("command"));
59
+ }
60
+ function removeEvent(doc, event) {
61
+ const existing = doc.get(event);
62
+ if (!(existing instanceof YAMLSeq)) return false;
63
+ const kept = existing.items.filter((item) => !isManaged(item));
64
+ if (kept.length === existing.items.length) return false;
65
+ if (kept.length === 0) doc.delete(event);
66
+ else existing.items = kept;
67
+ return true;
68
+ }
69
+ function upsertEvent(doc, event, matcher, command) {
70
+ const existing = doc.get(event);
71
+ const seq = existing instanceof YAMLSeq ? existing : new YAMLSeq();
72
+ const [first, ...extra] = seq.items.filter(isManaged);
73
+ if (first === void 0) {
74
+ seq.add(doc.createNode({ matcher, type: "command", command }));
75
+ doc.set(event, seq);
76
+ return true;
77
+ }
78
+ let changed = extra.length > 0;
79
+ if (changed) seq.items = seq.items.filter((item) => !extra.includes(item));
80
+ const desired = { matcher, type: "command", command };
81
+ for (const [key, value] of Object.entries(desired)) {
82
+ if (first.get(key) === value) continue;
83
+ first.set(key, value);
84
+ changed = true;
85
+ }
86
+ return changed;
87
+ }
88
+ function injectRecallHook(projectRoot) {
89
+ const path = join(projectRoot, ".agentsmesh", "hooks.yaml");
90
+ if (!existsSync(path)) return false;
91
+ const doc = parseDocument(readFileSync(path, "utf8"));
92
+ const command = recallHookCommand(projectRoot);
93
+ let changed = false;
94
+ for (const { event, matcher } of RECALL_EVENTS) {
95
+ if (upsertEvent(doc, event, matcher, command)) changed = true;
96
+ }
97
+ for (const event of RETIRED_EVENTS) {
98
+ if (removeEvent(doc, event)) changed = true;
99
+ }
100
+ if (changed) writeFileSync(path, String(doc), "utf8");
101
+ return changed;
102
+ }
103
+ var RECALL_SUBCOMMAND, RECALL_HOOK_COMMAND, RECALL_HOOK_TOOL_MATCHER, MANAGED_COMMAND, RECALL_EVENTS, RETIRED_EVENTS;
104
+ var init_recall_hook_scaffold = __esm({
105
+ "src/lessons/recall-hook-scaffold.ts"() {
106
+ init_cli_invocation();
107
+ RECALL_SUBCOMMAND = "lessons hook";
108
+ RECALL_HOOK_COMMAND = `agentsmesh ${RECALL_SUBCOMMAND}`;
109
+ RECALL_HOOK_TOOL_MATCHER = "Edit|Write|NotebookEdit|Bash|PowerShell";
110
+ MANAGED_COMMAND = new RegExp(`^(?:npx(?: --?[\\w-]+)* )?${RECALL_HOOK_COMMAND}$`);
111
+ RECALL_EVENTS = [
112
+ { event: "PreToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
113
+ { event: "UserPromptSubmit", matcher: "*" },
114
+ // Capture-on-failure nudge (see capture-nudge.ts). BEST-EFFORT: targets with
115
+ // no failure event drop it without warning (BEST_EFFORT_HOOK_EVENTS).
116
+ // PostToolUse is success-only, so failures need this.
117
+ { event: "PostToolUseFailure", matcher: "*" },
118
+ // Reset recall dedup after a context compaction/clear (see hook.ts SessionStart).
119
+ // BEST-EFFORT: targets that can't represent SessionStart just keep dedup as-is.
120
+ { event: "SessionStart", matcher: "*" }
121
+ ];
122
+ RETIRED_EVENTS = ["PostToolUse"];
123
+ }
124
+ });
27
125
  function capabilityLevel(capability) {
28
126
  return typeof capability === "string" ? capability : capability.level;
29
127
  }
@@ -183,6 +281,7 @@ var init_target_descriptor_schema = __esm({
183
281
  emitScopedSettings: z.function().optional(),
184
282
  mergeGeneratedOutputContent: z.function().optional(),
185
283
  postProcessHookOutputs: z.function().optional(),
284
+ hookContextEvents: z.array(z.string()).optional(),
186
285
  preservesManualActivation: z.boolean().optional()
187
286
  }).passthrough();
188
287
  targetDescriptorSchema = targetDescriptorSchemaBase.superRefine((value, ctx) => {
@@ -578,61 +677,6 @@ var init_guards = __esm({
578
677
  "src/utils/types/guards.ts"() {
579
678
  }
580
679
  });
581
- function removeEvent(doc, event) {
582
- const existing = doc.get(event);
583
- if (!(existing instanceof YAMLSeq)) return false;
584
- const kept = existing.items.filter(
585
- (item) => !(item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND)
586
- );
587
- if (kept.length === existing.items.length) return false;
588
- if (kept.length === 0) doc.delete(event);
589
- else existing.items = kept;
590
- return true;
591
- }
592
- function injectEvent(doc, event, matcher) {
593
- const existing = doc.get(event);
594
- const seq = existing instanceof YAMLSeq ? existing : new YAMLSeq();
595
- const present = seq.items.some(
596
- (item) => item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND
597
- );
598
- if (present) return false;
599
- seq.add(doc.createNode({ matcher, type: "command", command: RECALL_HOOK_COMMAND }));
600
- doc.set(event, seq);
601
- return true;
602
- }
603
- function injectRecallHook(projectRoot) {
604
- const path = join(projectRoot, ".agentsmesh", "hooks.yaml");
605
- if (!existsSync(path)) return false;
606
- const doc = parseDocument(readFileSync(path, "utf8"));
607
- let changed = false;
608
- for (const { event, matcher } of RECALL_EVENTS) {
609
- if (injectEvent(doc, event, matcher)) changed = true;
610
- }
611
- for (const event of RETIRED_EVENTS) {
612
- if (removeEvent(doc, event)) changed = true;
613
- }
614
- if (changed) writeFileSync(path, String(doc), "utf8");
615
- return changed;
616
- }
617
- var RECALL_HOOK_COMMAND, RECALL_HOOK_TOOL_MATCHER, RECALL_EVENTS, RETIRED_EVENTS;
618
- var init_recall_hook_scaffold = __esm({
619
- "src/lessons/recall-hook-scaffold.ts"() {
620
- RECALL_HOOK_COMMAND = "agentsmesh lessons hook";
621
- RECALL_HOOK_TOOL_MATCHER = "Edit|Write|Bash";
622
- RECALL_EVENTS = [
623
- { event: "PreToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
624
- { event: "UserPromptSubmit", matcher: "*" },
625
- // Capture-on-failure nudge (see capture-nudge.ts). BEST-EFFORT: only Claude
626
- // Code's passthrough hooks emit it; whitelist targets drop it without warning
627
- // (BEST_EFFORT_HOOK_EVENTS). PostToolUse is success-only, so failures need this.
628
- { event: "PostToolUseFailure", matcher: "*" },
629
- // Reset recall dedup after a context compaction/clear (see hook.ts SessionStart).
630
- // BEST-EFFORT: targets that can't represent SessionStart just keep dedup as-is.
631
- { event: "SessionStart", matcher: "*" }
632
- ];
633
- RETIRED_EVENTS = ["PostToolUse"];
634
- }
635
- });
636
680
 
637
681
  // src/core/hook-types.ts
638
682
  function isBestEffortHookEvent(event, entries) {
@@ -960,8 +1004,10 @@ function executableModeFor(path) {
960
1004
  }
961
1005
  function normalizeTextPayload(path, content) {
962
1006
  if (!shouldNormalizeLineEndings(path)) return content;
963
- const withoutBom = content.startsWith(UTF8_BOM) ? content.slice(UTF8_BOM.length) : content;
964
- return normalizeLineEndings(withoutBom);
1007
+ return normalizeLineEndings(stripBom(content));
1008
+ }
1009
+ function stripBom(text) {
1010
+ return text.startsWith(UTF8_BOM) ? text.slice(UTF8_BOM.length) : text;
965
1011
  }
966
1012
  var UTF8_BOM, TEXT_EXTENSIONS, TEXT_DOTFILES, BINARY_EXTENSIONS, EXECUTABLE_SCRIPT_EXTENSIONS;
967
1013
  var init_fs_text_encoding = __esm({
@@ -2161,6 +2207,15 @@ var init_constants = __esm({
2161
2207
  }
2162
2208
  });
2163
2209
 
2210
+ // src/targets/catalog/ignore-output.ts
2211
+ function ignoreOutput(path) {
2212
+ return (canonical) => canonical.ignore.length === 0 ? [] : [{ path, content: canonical.ignore.join("\n") }];
2213
+ }
2214
+ var init_ignore_output = __esm({
2215
+ "src/targets/catalog/ignore-output.ts"() {
2216
+ }
2217
+ });
2218
+
2164
2219
  // src/targets/aider/generator.ts
2165
2220
  function buildAiderConventions(canonical) {
2166
2221
  const root = canonical.rules.find((rule) => rule.root);
@@ -2190,11 +2245,7 @@ function generateAgents(canonical) {
2190
2245
  content: serializeProjectedAgentSkill(agent)
2191
2246
  }));
2192
2247
  }
2193
- function generateIgnore(canonical) {
2194
- if (canonical.ignore.length === 0) return [];
2195
- return [{ path: AIDER_IGNORE, content: canonical.ignore.join("\n") }];
2196
- }
2197
- var generateMcp, generatePermissions;
2248
+ var generateIgnore, generateMcp, generatePermissions;
2198
2249
  var init_generator = __esm({
2199
2250
  "src/targets/aider/generator.ts"() {
2200
2251
  init_no_outputs();
@@ -2203,6 +2254,8 @@ var init_generator = __esm({
2203
2254
  init_projected_agent_skill();
2204
2255
  init_command_skill();
2205
2256
  init_constants();
2257
+ init_ignore_output();
2258
+ generateIgnore = ignoreOutput(AIDER_IGNORE);
2206
2259
  generateMcp = NO_OUTPUTS;
2207
2260
  generatePermissions = NO_OUTPUTS;
2208
2261
  }
@@ -2918,6 +2971,9 @@ var init_link_token_context = __esm({
2918
2971
  });
2919
2972
 
2920
2973
  // src/core/reference/link-rebaser.ts
2974
+ function isSlashCommandToken(normalizedToken) {
2975
+ return /^\/[A-Za-z][A-Za-z0-9-]*$/.test(normalizedToken);
2976
+ }
2921
2977
  function rewriteFileLinks(input) {
2922
2978
  const missing = /* @__PURE__ */ new Set();
2923
2979
  const protectedRefRanges = protectedRanges(input.content);
@@ -2968,6 +3024,7 @@ function rewriteFileLinks(input) {
2968
3024
  if (targetIsDirectory && !normalizedCandidate.includes("/") && !normalizedCandidate.includes("\\")) {
2969
3025
  return match;
2970
3026
  }
3027
+ if (isSlashCommandToken(normalizedCandidate)) return match;
2971
3028
  if (resolvedBeforeTranslate === null) {
2972
3029
  let normTok = normalizeSeparators(punctStripped);
2973
3030
  if (normTok.startsWith("agentsmesh/")) {
@@ -3727,7 +3784,7 @@ var init_importer = __esm({
3727
3784
  }
3728
3785
  });
3729
3786
  function globFilter(files, pattern) {
3730
- const isMatch = picomatch2(pattern, OPTIONS);
3787
+ const isMatch = picomatch(pattern, OPTIONS);
3731
3788
  return files.filter((file) => isMatch(file));
3732
3789
  }
3733
3790
  var OPTIONS;
@@ -7092,10 +7149,6 @@ function generateAgents4(canonical) {
7092
7149
  content: serializeAntigravityAgent(agent)
7093
7150
  }));
7094
7151
  }
7095
- function generateIgnore3(canonical) {
7096
- if (canonical.ignore.length === 0) return [];
7097
- return [{ path: ANTIGRAVITY_IGNORE_FILE, content: canonical.ignore.join("\n") }];
7098
- }
7099
7152
  function renderAntigravityGlobalInstructions(canonical) {
7100
7153
  const root = canonical.rules.find((rule) => rule.root);
7101
7154
  const nonRootRules = canonical.rules.filter((rule) => {
@@ -7110,7 +7163,7 @@ function generateHooks2(canonical) {
7110
7163
  if (Object.keys(hooks).length === 0) return [];
7111
7164
  return [{ path: ANTIGRAVITY_HOOKS_FILE, content: JSON.stringify(hooks, null, 2) }];
7112
7165
  }
7113
- var generatePermissions3;
7166
+ var generateIgnore3, generatePermissions3;
7114
7167
  var init_generator4 = __esm({
7115
7168
  "src/targets/antigravity/generator.ts"() {
7116
7169
  init_no_outputs();
@@ -7119,6 +7172,8 @@ var init_generator4 = __esm({
7119
7172
  init_hooks_format2();
7120
7173
  init_agents_format();
7121
7174
  init_constants5();
7175
+ init_ignore_output();
7176
+ generateIgnore3 = ignoreOutput(ANTIGRAVITY_IGNORE_FILE);
7122
7177
  generatePermissions3 = NO_OUTPUTS;
7123
7178
  }
7124
7179
  });
@@ -8712,16 +8767,14 @@ function generateHooks3(canonical) {
8712
8767
  const content = JSON.stringify({ hooks: claudeHooks }, null, 2);
8713
8768
  return [{ path: CLAUDE_SETTINGS, content }];
8714
8769
  }
8715
- function generateIgnore5(canonical) {
8716
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
8717
- const content = canonical.ignore.join("\n");
8718
- return [{ path: CLAUDE_IGNORE, content }];
8719
- }
8770
+ var generateIgnore5;
8720
8771
  var init_generator6 = __esm({
8721
8772
  "src/targets/claude-code/generator.ts"() {
8722
8773
  init_markdown();
8723
8774
  init_constants7();
8724
8775
  init_hooks_format2();
8776
+ init_ignore_output();
8777
+ generateIgnore5 = ignoreOutput(CLAUDE_IGNORE);
8725
8778
  }
8726
8779
  });
8727
8780
  function ruleSectionTitle(rule) {
@@ -10540,11 +10593,7 @@ function generateMcp5(canonical) {
10540
10593
  if (!canonical.mcp || Object.keys(canonical.mcp.mcpServers).length === 0) return [];
10541
10594
  return [{ path: CODEBUFF_MCP_FILE, content: serializeCodebuffMcp(canonical.mcp) }];
10542
10595
  }
10543
- function generateIgnore7(canonical) {
10544
- if (canonical.ignore.length === 0) return [];
10545
- return [{ path: CODEBUFF_IGNORE_FILE, content: canonical.ignore.join("\n") }];
10546
- }
10547
- var generateAgents8, generateHooks5, generatePermissions6;
10596
+ var generateIgnore7, generateAgents8, generateHooks5, generatePermissions6;
10548
10597
  var init_generator8 = __esm({
10549
10598
  "src/targets/codebuff/generator.ts"() {
10550
10599
  init_no_outputs();
@@ -10554,6 +10603,8 @@ var init_generator8 = __esm({
10554
10603
  init_nested_rules();
10555
10604
  init_mcp_format2();
10556
10605
  init_constants9();
10606
+ init_ignore_output();
10607
+ generateIgnore7 = ignoreOutput(CODEBUFF_IGNORE_FILE);
10557
10608
  generateAgents8 = NO_OUTPUTS;
10558
10609
  generateHooks5 = NO_OUTPUTS;
10559
10610
  generatePermissions6 = NO_OUTPUTS;
@@ -13066,53 +13117,37 @@ var init_mcp_generator = __esm({
13066
13117
  }
13067
13118
  });
13068
13119
 
13069
- // src/targets/copilot/hook-entry.ts
13070
- function hasHookCommand2(entry) {
13071
- return hasHookCommand(entry);
13072
- }
13073
- var init_hook_entry = __esm({
13074
- "src/targets/copilot/hook-entry.ts"() {
13075
- init_hook_command();
13076
- }
13077
- });
13078
-
13079
13120
  // src/targets/copilot/hook-format.ts
13080
- function mapHookEvent(event) {
13081
- switch (event) {
13082
- case "PreToolUse":
13083
- return "preToolUse";
13084
- case "PostToolUse":
13085
- return "postToolUse";
13086
- case "Notification":
13087
- return "notification";
13088
- case "UserPromptSubmit":
13089
- return "userPromptSubmitted";
13090
- default:
13091
- return null;
13092
- }
13121
+ function copilotHookGroups(hooks) {
13122
+ return Object.entries(hooks ?? {}).flatMap(([event, entries]) => {
13123
+ const copilotEvent = CANONICAL_TO_COPILOT.get(event);
13124
+ if (!copilotEvent || !Array.isArray(entries)) return [];
13125
+ const kept = entries.filter(
13126
+ (entry) => typeof entry === "object" && entry !== null && hasHookCommand(entry)
13127
+ );
13128
+ return kept.length > 0 ? [{ event, copilotEvent, entries: kept }] : [];
13129
+ });
13130
+ }
13131
+ function wrapperScriptName(event, index) {
13132
+ return `${event.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase()}-${index}.sh`;
13093
13133
  }
13094
13134
  function buildCopilotHooksObject(hooks) {
13095
- if (!hooks) return null;
13096
- const result2 = Object.fromEntries(
13097
- Object.entries(hooks).flatMap(([event, entries]) => {
13098
- const mappedEvent = mapHookEvent(event);
13099
- if (!mappedEvent || !Array.isArray(entries)) return [];
13100
- const mappedEntries = entries.filter(
13101
- (entry) => typeof entry === "object" && entry !== null && hasHookCommand2(entry)
13102
- ).map((entry, index) => {
13103
- const safePhase = event.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();
13135
+ const groups = copilotHookGroups(hooks);
13136
+ if (groups.length === 0) return null;
13137
+ return Object.fromEntries(
13138
+ groups.map(({ event, copilotEvent, entries }) => [
13139
+ copilotEvent,
13140
+ entries.map((entry, index) => {
13104
13141
  const hook = {
13105
13142
  type: "command",
13106
- bash: `./scripts/${safePhase}-${index}.sh`
13143
+ bash: `./scripts/${wrapperScriptName(event, index)}`
13107
13144
  };
13108
13145
  if (entry.matcher && entry.matcher !== "*") hook.matcher = entry.matcher;
13109
13146
  if (entry.timeout !== void 0) hook.timeoutSec = Math.ceil(entry.timeout / 1e3);
13110
13147
  return hook;
13111
- });
13112
- return mappedEntries.length > 0 ? [[mappedEvent, mappedEntries]] : [];
13113
- })
13148
+ })
13149
+ ])
13114
13150
  );
13115
- return Object.keys(result2).length > 0 ? result2 : null;
13116
13151
  }
13117
13152
  function generateHooks8(canonical) {
13118
13153
  const hooks = buildCopilotHooksObject(canonical.hooks);
@@ -13124,10 +13159,24 @@ function generateHooks8(canonical) {
13124
13159
  }
13125
13160
  ];
13126
13161
  }
13162
+ var COPILOT_HOOK_CONTEXT_EVENTS, CANONICAL_TO_COPILOT;
13127
13163
  var init_hook_format = __esm({
13128
13164
  "src/targets/copilot/hook-format.ts"() {
13129
13165
  init_constants32();
13130
- init_hook_entry();
13166
+ init_hook_command();
13167
+ COPILOT_HOOK_CONTEXT_EVENTS = [
13168
+ "SessionStart",
13169
+ "PostToolUse",
13170
+ "PostToolUseFailure"
13171
+ ];
13172
+ CANONICAL_TO_COPILOT = /* @__PURE__ */ new Map([
13173
+ ["PreToolUse", "preToolUse"],
13174
+ ["PostToolUse", "postToolUse"],
13175
+ ["PostToolUseFailure", "postToolUseFailure"],
13176
+ ["Notification", "notification"],
13177
+ ["UserPromptSubmit", "userPromptSubmitted"],
13178
+ ["SessionStart", "sessionStart"]
13179
+ ]);
13131
13180
  }
13132
13181
  });
13133
13182
  function ruleSlug2(source) {
@@ -13233,18 +13282,7 @@ var init_generator12 = __esm({
13233
13282
  }
13234
13283
  });
13235
13284
  function mapCopilotHookEvent(event) {
13236
- switch (event) {
13237
- case "preToolUse":
13238
- return "PreToolUse";
13239
- case "postToolUse":
13240
- return "PostToolUse";
13241
- case "notification":
13242
- return "Notification";
13243
- case "userPromptSubmitted":
13244
- return "UserPromptSubmit";
13245
- default:
13246
- return null;
13247
- }
13285
+ return COPILOT_TO_CANONICAL.get(event) ?? null;
13248
13286
  }
13249
13287
  function extractMatcher(comment) {
13250
13288
  if (typeof comment !== "string") return "*";
@@ -13332,11 +13370,16 @@ async function importHooks(projectRoot, results, options = {}) {
13332
13370
  feature: "hooks"
13333
13371
  });
13334
13372
  }
13373
+ var COPILOT_TO_CANONICAL;
13335
13374
  var init_hook_parser = __esm({
13336
13375
  "src/targets/copilot/hook-parser.ts"() {
13337
13376
  init_canonical_paths();
13338
13377
  init_fs();
13339
13378
  init_constants32();
13379
+ init_hook_format();
13380
+ COPILOT_TO_CANONICAL = new Map(
13381
+ [...CANONICAL_TO_COPILOT].map(([canonical, copilot]) => [copilot, canonical])
13382
+ );
13340
13383
  }
13341
13384
  });
13342
13385
  async function importSkills2(projectRoot, results, normalize, skillsDirRel = COPILOT_SKILLS_DIR) {
@@ -13573,7 +13616,7 @@ function lintCommands4(canonical) {
13573
13616
  }
13574
13617
  function lintHooks9(canonical) {
13575
13618
  if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
13576
- const supported = ["PreToolUse", "PostToolUse", "Notification", "UserPromptSubmit"];
13619
+ const supported = [...CANONICAL_TO_COPILOT.keys()];
13577
13620
  const diagnostics = unsupportedHookEventNames(canonical.hooks, supported).map(
13578
13621
  (event) => createUnsupportedHookWarning(event, "copilot", supported, {
13579
13622
  unsupportedBy: "Copilot hooks"
@@ -13596,11 +13639,9 @@ function lintHooks9(canonical) {
13596
13639
  var init_lint10 = __esm({
13597
13640
  "src/targets/copilot/lint.ts"() {
13598
13641
  init_helpers();
13642
+ init_hook_format();
13599
13643
  }
13600
13644
  });
13601
- function safePhaseName(phase) {
13602
- return phase.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();
13603
- }
13604
13645
  function toRepoRelative(projectRoot, sourcePath) {
13605
13646
  const repoRelative = relative(projectRoot, sourcePath).replace(/\\/g, "/");
13606
13647
  if (!repoRelative || repoRelative.startsWith("../")) return null;
@@ -13628,9 +13669,6 @@ async function buildAssetOutput(projectRoot, command, hooksDirRel) {
13628
13669
  rewrittenCommand: rewriteWrapperCommand(command, repoRelative)
13629
13670
  };
13630
13671
  }
13631
- function wrapperPath(event, index, hooksDirRel) {
13632
- return `${hooksDirRel}/scripts/${safePhaseName(event)}-${index}.sh`;
13633
- }
13634
13672
  function safeShellLine2(value) {
13635
13673
  return value.replace(/[\r\n]+/g, " ");
13636
13674
  }
@@ -13645,15 +13683,13 @@ function buildWrapper(command, matcher) {
13645
13683
  ].join("\n");
13646
13684
  }
13647
13685
  async function addHookScriptAssets(projectRoot, canonical, outputs, hooksDirRel = COPILOT_HOOKS_DIR) {
13648
- if (!canonical.hooks) return outputs;
13686
+ const groups = copilotHookGroups(canonical.hooks);
13687
+ if (groups.length === 0) return outputs;
13649
13688
  const wrapperOutputs = [];
13650
13689
  const assetOutputs = /* @__PURE__ */ new Map();
13651
- for (const [event, entries] of Object.entries(canonical.hooks)) {
13652
- if (!Array.isArray(entries)) continue;
13653
- let index = 0;
13654
- for (const entry of entries) {
13655
- if (!hasHookCommand2(entry)) continue;
13656
- const scriptPath = wrapperPath(event, index, hooksDirRel);
13690
+ for (const { event, entries } of groups) {
13691
+ for (const [index, entry] of entries.entries()) {
13692
+ const scriptPath = `${hooksDirRel}/scripts/${wrapperScriptName(event, index)}`;
13657
13693
  let command = entry.command;
13658
13694
  const asset = await buildAssetOutput(projectRoot, entry.command, hooksDirRel);
13659
13695
  if (asset) {
@@ -13667,7 +13703,6 @@ async function addHookScriptAssets(projectRoot, canonical, outputs, hooksDirRel
13667
13703
  'set -eu\nHOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\n'
13668
13704
  );
13669
13705
  wrapperOutputs.push({ path: scriptPath, content: wrapper });
13670
- index++;
13671
13706
  }
13672
13707
  }
13673
13708
  return [...outputs, ...wrapperOutputs, ...assetOutputs.values()];
@@ -13677,7 +13712,7 @@ var init_hook_assets = __esm({
13677
13712
  "src/targets/copilot/hook-assets.ts"() {
13678
13713
  init_fs();
13679
13714
  init_constants32();
13680
- init_hook_entry();
13715
+ init_hook_format();
13681
13716
  SCRIPT_PREFIX_RE = /^(?<prefix>\s*(?:(?:bash|sh|zsh)\s+)?)["']?(?<path>(?:\.\.\/|\.\/|[^/\s"'`]+\/)[^\s"'`]+)["']?(?<suffix>(?:\s.*)?)$/;
13682
13717
  }
13683
13718
  });
@@ -13995,6 +14030,7 @@ var init_copilot2 = __esm({
13995
14030
  init_command_prompt();
13996
14031
  init_lint10();
13997
14032
  init_hook_assets();
14033
+ init_hook_format();
13998
14034
  init_scope_extras2();
13999
14035
  init_importer_spec2();
14000
14036
  init_capabilities6();
@@ -14125,6 +14161,7 @@ var init_copilot2 = __esm({
14125
14161
  permissions: lintPermissions7
14126
14162
  },
14127
14163
  postProcessHookOutputs: async (projectRoot, canonical, outputs) => addHookScriptAssets(projectRoot, canonical, [...outputs]),
14164
+ hookContextEvents: COPILOT_HOOK_CONTEXT_EVENTS,
14128
14165
  mergeGeneratedOutputContent: mergeCopilotMcpJson,
14129
14166
  project: project7,
14130
14167
  globalSupport: {
@@ -14212,11 +14249,6 @@ function generatePermissions8(canonical) {
14212
14249
  });
14213
14250
  return [{ path: CRUSH_CONFIG_FILE, content: JSON.stringify(crushConfig, null, 2) }];
14214
14251
  }
14215
- function generateIgnore9(canonical) {
14216
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
14217
- const content = canonical.ignore.join("\n");
14218
- return [{ path: CRUSH_IGNORE, content }];
14219
- }
14220
14252
  function buildCrushHooksFromCanonical(canonical) {
14221
14253
  if (!canonical.hooks) return {};
14222
14254
  const result2 = {};
@@ -14234,7 +14266,7 @@ function buildCrushHooksFromCanonical(canonical) {
14234
14266
  }
14235
14267
  return result2;
14236
14268
  }
14237
- var generateRules12;
14269
+ var generateRules12, generateIgnore9;
14238
14270
  var init_generator13 = __esm({
14239
14271
  "src/targets/crush/generator.ts"() {
14240
14272
  init_managed_blocks();
@@ -14243,7 +14275,9 @@ var init_generator13 = __esm({
14243
14275
  init_command_skill();
14244
14276
  init_constants12();
14245
14277
  init_config_format();
14278
+ init_ignore_output();
14246
14279
  generateRules12 = (canonical) => embeddedRootRule(canonical, CRUSH_TARGET, CRUSH_ROOT_FILE);
14280
+ generateIgnore9 = ignoreOutput(CRUSH_IGNORE);
14247
14281
  }
14248
14282
  });
14249
14283
  async function importFromCrush(projectRoot, options = {}) {
@@ -14779,7 +14813,7 @@ function cursorHooksToCanonical(hooks) {
14779
14813
  }
14780
14814
  return result2;
14781
14815
  }
14782
- var CANONICAL_TO_CURSOR, CURSOR_TO_CANONICAL;
14816
+ var CANONICAL_TO_CURSOR, CURSOR_HOOK_CONTEXT_EVENTS, CURSOR_TO_CANONICAL;
14783
14817
  var init_hook_format2 = __esm({
14784
14818
  "src/targets/cursor/hook-format.ts"() {
14785
14819
  init_hook_types();
@@ -14787,6 +14821,7 @@ var init_hook_format2 = __esm({
14787
14821
  CANONICAL_TO_CURSOR = {
14788
14822
  PreToolUse: "preToolUse",
14789
14823
  PostToolUse: "postToolUse",
14824
+ PostToolUseFailure: "postToolUseFailure",
14790
14825
  UserPromptSubmit: "beforeSubmitPrompt",
14791
14826
  SubagentStart: "subagentStart",
14792
14827
  SubagentStop: "subagentStop",
@@ -14795,6 +14830,11 @@ var init_hook_format2 = __esm({
14795
14830
  SessionEnd: "sessionEnd",
14796
14831
  PreCompact: "preCompact"
14797
14832
  };
14833
+ CURSOR_HOOK_CONTEXT_EVENTS = [
14834
+ "SessionStart",
14835
+ "PostToolUse",
14836
+ "PostToolUseFailure"
14837
+ ];
14798
14838
  CURSOR_TO_CANONICAL = new Map(
14799
14839
  Object.entries(CANONICAL_TO_CURSOR).map(([canonical, cursor]) => [cursor, canonical])
14800
14840
  );
@@ -14803,7 +14843,7 @@ var init_hook_format2 = __esm({
14803
14843
 
14804
14844
  // src/targets/cursor/generator/hooks.ts
14805
14845
  function generateHooks10(canonical) {
14806
- if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
14846
+ if (!canonical.hooks) return [];
14807
14847
  const cursorHooks = toCursorHooks(canonical.hooks);
14808
14848
  if (Object.keys(cursorHooks).length === 0) return [];
14809
14849
  const content = JSON.stringify({ version: 1, hooks: cursorHooks }, null, 2);
@@ -14817,14 +14857,12 @@ var init_hooks3 = __esm({
14817
14857
  });
14818
14858
 
14819
14859
  // src/targets/cursor/generator/ignore.ts
14820
- function generateIgnore10(canonical) {
14821
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
14822
- const content = canonical.ignore.join("\n");
14823
- return [{ path: CURSOR_IGNORE, content }];
14824
- }
14860
+ var generateIgnore10;
14825
14861
  var init_ignore = __esm({
14826
14862
  "src/targets/cursor/generator/ignore.ts"() {
14827
14863
  init_constants13();
14864
+ init_ignore_output();
14865
+ generateIgnore10 = ignoreOutput(CURSOR_IGNORE);
14828
14866
  }
14829
14867
  });
14830
14868
 
@@ -15520,6 +15558,7 @@ var init_cursor2 = __esm({
15520
15558
  init_linter13();
15521
15559
  init_import_map_builders();
15522
15560
  init_lint12();
15561
+ init_hook_format2();
15523
15562
  target13 = {
15524
15563
  name: "cursor",
15525
15564
  primaryRootInstructionPath: CURSOR_GENERAL_RULE,
@@ -15641,6 +15680,7 @@ var init_cursor2 = __esm({
15641
15680
  permissions: "native"
15642
15681
  },
15643
15682
  emptyImportMessage: "No Cursor config found (AGENTS.md or .cursor/rules/*.mdc; with --global: ~/.cursor/{rules/*.mdc,AGENTS.md,mcp.json,hooks.json,cursorignore,skills/,agents/,commands/} and legacy ~/.agentsmesh-exports/cursor/user-rules.md).",
15683
+ hookContextEvents: CURSOR_HOOK_CONTEXT_EVENTS,
15644
15684
  lintRules: lintRules13,
15645
15685
  lint: {
15646
15686
  commands: lintCommands6,
@@ -16902,25 +16942,127 @@ var init_skills3 = __esm({
16902
16942
  }
16903
16943
  });
16904
16944
 
16905
- // src/targets/gemini-cli/generator/settings.ts
16906
- function mapHookEvent2(event) {
16907
- switch (event) {
16908
- case "PreToolUse":
16909
- return "BeforeTool";
16910
- case "PostToolUse":
16911
- return "AfterTool";
16912
- case "Notification":
16913
- return "Notification";
16914
- case "SubagentStart":
16915
- return "BeforeAgent";
16916
- case "SubagentStop":
16917
- return "AfterAgent";
16918
- case "SessionStart":
16919
- return "SessionStart";
16920
- default:
16921
- return null;
16945
+ // src/targets/gemini-cli/hook-map.ts
16946
+ function geminiHookEvent(event) {
16947
+ return CANONICAL_TO_GEMINI.get(event) ?? null;
16948
+ }
16949
+ function mapGeminiHookEvent(event) {
16950
+ return GEMINI_TO_CANONICAL.get(event) ?? null;
16951
+ }
16952
+ function splitNames(list, separator) {
16953
+ return list.split(separator).map((name) => name.trim()).filter((name) => name.length > 0);
16954
+ }
16955
+ function toGeminiMatcher(geminiEvent, matcher) {
16956
+ if (!TOOL_EVENTS.has(geminiEvent) || !NAME_LIST.test(matcher)) return matcher;
16957
+ const tools = unique(
16958
+ splitNames(matcher, /[|,]/).flatMap((name) => CANONICAL_TO_GEMINI_TOOLS.get(name) ?? [name])
16959
+ );
16960
+ return tools.length > 0 ? `^(?:${tools.join("|")})$` : matcher;
16961
+ }
16962
+ function fromGeminiMatcher(geminiEvent, matcher) {
16963
+ if (!TOOL_EVENTS.has(geminiEvent)) return matcher;
16964
+ const list = ANCHORED_LIST.exec(matcher)?.[1] ?? matcher;
16965
+ if (!NAME_LIST.test(list) || list.includes(",")) return matcher;
16966
+ return unique(
16967
+ splitNames(list, /\|/).map((name) => GEMINI_TO_CANONICAL_TOOLS.get(name) ?? name)
16968
+ ).join("|");
16969
+ }
16970
+ var GEMINI_HOOK_CONTEXT_EVENTS, CANONICAL_TO_GEMINI, GEMINI_TO_CANONICAL, TOOL_EVENTS, CANONICAL_TO_GEMINI_TOOLS, GEMINI_TO_CANONICAL_TOOLS, NAME_LIST, ANCHORED_LIST, unique;
16971
+ var init_hook_map = __esm({
16972
+ "src/targets/gemini-cli/hook-map.ts"() {
16973
+ GEMINI_HOOK_CONTEXT_EVENTS = [
16974
+ "UserPromptSubmit",
16975
+ "SessionStart",
16976
+ "PostToolUse"
16977
+ ];
16978
+ CANONICAL_TO_GEMINI = /* @__PURE__ */ new Map([
16979
+ ["PreToolUse", "BeforeTool"],
16980
+ ["PostToolUse", "AfterTool"],
16981
+ ["Notification", "Notification"],
16982
+ // BeforeAgent fires after the user submits a prompt and carries `prompt`.
16983
+ ["UserPromptSubmit", "BeforeAgent"],
16984
+ // Older mapping, kept so existing SubagentStart hooks still reach Gemini.
16985
+ ["SubagentStart", "BeforeAgent"],
16986
+ ["SubagentStop", "AfterAgent"],
16987
+ ["SessionStart", "SessionStart"]
16988
+ ]);
16989
+ GEMINI_TO_CANONICAL = new Map([
16990
+ ...[...CANONICAL_TO_GEMINI].map(([canonical, gemini]) => [gemini, canonical]),
16991
+ // BeforeAgent is shared with SubagentStart; import it as the prompt event.
16992
+ ["BeforeAgent", "UserPromptSubmit"],
16993
+ // Legacy lowercase names.
16994
+ ["preToolUse", "PreToolUse"],
16995
+ ["postToolUse", "PostToolUse"],
16996
+ ["notification", "Notification"]
16997
+ ]);
16998
+ TOOL_EVENTS = /* @__PURE__ */ new Set(["BeforeTool", "AfterTool"]);
16999
+ CANONICAL_TO_GEMINI_TOOLS = /* @__PURE__ */ new Map([
17000
+ ["Bash", ["run_shell_command"]],
17001
+ ["PowerShell", ["run_shell_command"]],
17002
+ ["Edit", ["replace"]],
17003
+ ["MultiEdit", ["replace"]],
17004
+ ["Write", ["write_file"]],
17005
+ ["NotebookEdit", []],
17006
+ ["Read", ["read_file", "read_many_files"]],
17007
+ ["Grep", ["grep_search"]],
17008
+ ["Glob", ["glob"]],
17009
+ ["LS", ["list_directory"]],
17010
+ ["WebFetch", ["web_fetch"]],
17011
+ ["WebSearch", ["google_web_search"]],
17012
+ ["TodoWrite", ["write_todos"]]
17013
+ ]);
17014
+ GEMINI_TO_CANONICAL_TOOLS = /* @__PURE__ */ new Map([
17015
+ ["run_shell_command", "Bash"],
17016
+ ["replace", "Edit"],
17017
+ ["write_file", "Write"],
17018
+ ["read_file", "Read"],
17019
+ ["read_many_files", "Read"],
17020
+ ["grep_search", "Grep"],
17021
+ ["search_file_content", "Grep"],
17022
+ ["glob", "Glob"],
17023
+ ["list_directory", "LS"],
17024
+ ["web_fetch", "WebFetch"],
17025
+ ["google_web_search", "WebSearch"],
17026
+ ["write_todos", "TodoWrite"]
17027
+ ]);
17028
+ NAME_LIST = /^[\w\s,|-]+$/;
17029
+ ANCHORED_LIST = /^\^\(\?:(.*)\)\$$/;
17030
+ unique = (values) => [...new Set(values)];
16922
17031
  }
17032
+ });
17033
+
17034
+ // src/targets/gemini-cli/generator/hooks.ts
17035
+ function buildGeminiHooks(hooks) {
17036
+ const result2 = {};
17037
+ for (const [event, entries] of Object.entries(hooks ?? {})) {
17038
+ const geminiEvent = geminiHookEvent(event);
17039
+ if (!geminiEvent || !Array.isArray(entries)) continue;
17040
+ for (const entry of entries) {
17041
+ if (typeof entry !== "object" || entry === null || !hasHookCommand(entry)) continue;
17042
+ const list = result2[geminiEvent] ??= [];
17043
+ list.push({
17044
+ matcher: typeof entry.matcher === "string" ? toGeminiMatcher(geminiEvent, entry.matcher) : entry.matcher,
17045
+ hooks: [
17046
+ {
17047
+ name: `${geminiEvent}-${list.length + 1}`,
17048
+ type: "command",
17049
+ command: getHookCommand(entry),
17050
+ timeout: entry.timeout
17051
+ }
17052
+ ]
17053
+ });
17054
+ }
17055
+ }
17056
+ return Object.keys(result2).length > 0 ? result2 : null;
16923
17057
  }
17058
+ var init_hooks4 = __esm({
17059
+ "src/targets/gemini-cli/generator/hooks.ts"() {
17060
+ init_hook_command();
17061
+ init_hook_map();
17062
+ }
17063
+ });
17064
+
17065
+ // src/targets/gemini-cli/generator/settings.ts
16924
17066
  function generateGeminiSettingsFiles(canonical, enabledFeatures) {
16925
17067
  const settings = {};
16926
17068
  let hasAnyNativeSettings = false;
@@ -16932,29 +17074,10 @@ function generateGeminiSettingsFiles(canonical, enabledFeatures) {
16932
17074
  settings.experimental = { enableAgents: true };
16933
17075
  hasAnyNativeSettings = true;
16934
17076
  }
16935
- if (enabledFeatures.has("hooks") && canonical.hooks) {
16936
- const hookEntries = Object.entries(canonical.hooks).flatMap(([event, entries]) => {
16937
- const mappedEvent = mapHookEvent2(event);
16938
- if (!mappedEvent || !Array.isArray(entries)) return [];
16939
- const mappedEntries = entries.filter(
16940
- (entry) => typeof entry === "object" && entry !== null && hasHookCommand(entry)
16941
- ).map((entry, index) => ({
16942
- matcher: entry.matcher,
16943
- hooks: [
16944
- {
16945
- name: `${mappedEvent}-${index + 1}`,
16946
- type: "command",
16947
- command: getHookCommand(entry),
16948
- timeout: entry.timeout
16949
- }
16950
- ]
16951
- }));
16952
- return mappedEntries.length > 0 ? [[mappedEvent, mappedEntries]] : [];
16953
- });
16954
- if (hookEntries.length > 0) {
16955
- settings.hooks = Object.fromEntries(hookEntries);
16956
- hasAnyNativeSettings = true;
16957
- }
17077
+ const hooks = enabledFeatures.has("hooks") ? buildGeminiHooks(canonical.hooks) : null;
17078
+ if (hooks) {
17079
+ settings.hooks = hooks;
17080
+ hasAnyNativeSettings = true;
16958
17081
  }
16959
17082
  if (hasAnyNativeSettings) {
16960
17083
  settings.context = { fileName: [GEMINI_ROOT, GEMINI_COMPAT_AGENTS] };
@@ -16964,19 +17087,18 @@ function generateGeminiSettingsFiles(canonical, enabledFeatures) {
16964
17087
  }
16965
17088
  var init_settings2 = __esm({
16966
17089
  "src/targets/gemini-cli/generator/settings.ts"() {
16967
- init_hook_command();
16968
17090
  init_constants33();
17091
+ init_hooks4();
16969
17092
  }
16970
17093
  });
16971
17094
 
16972
17095
  // src/targets/gemini-cli/generator/ignore.ts
16973
- function generateIgnore13(canonical) {
16974
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
16975
- return [{ path: GEMINI_IGNORE, content: canonical.ignore.join("\n") }];
16976
- }
17096
+ var generateIgnore13;
16977
17097
  var init_ignore2 = __esm({
16978
17098
  "src/targets/gemini-cli/generator/ignore.ts"() {
16979
17099
  init_constants33();
17100
+ init_ignore_output();
17101
+ generateIgnore13 = ignoreOutput(GEMINI_IGNORE);
16980
17102
  }
16981
17103
  });
16982
17104
 
@@ -17255,27 +17377,6 @@ var init_layout6 = __esm({
17255
17377
  };
17256
17378
  }
17257
17379
  });
17258
- function mapGeminiHookEvent(event) {
17259
- switch (event) {
17260
- case "BeforeTool":
17261
- case "preToolUse":
17262
- return "PreToolUse";
17263
- case "AfterTool":
17264
- case "postToolUse":
17265
- return "PostToolUse";
17266
- case "Notification":
17267
- case "notification":
17268
- return "Notification";
17269
- case "BeforeAgent":
17270
- return "SubagentStart";
17271
- case "AfterAgent":
17272
- return "SubagentStop";
17273
- case "SessionStart":
17274
- return "SessionStart";
17275
- default:
17276
- return null;
17277
- }
17278
- }
17279
17380
  function parseFlexibleFrontmatter(content) {
17280
17381
  const yamlOpen = content.indexOf("---");
17281
17382
  const tomlOpen = content.indexOf("+++");
@@ -17349,7 +17450,7 @@ async function importGeminiSettings(projectRoot, results) {
17349
17450
  (entry) => entry.hooks.filter(
17350
17451
  (hook) => hook !== null && typeof hook === "object" && hasHookCommand(hook)
17351
17452
  ).map((hook) => ({
17352
- matcher: entry.matcher,
17453
+ matcher: fromGeminiMatcher(event, entry.matcher),
17353
17454
  command: getHookCommand(hook),
17354
17455
  type: "command",
17355
17456
  timeout: typeof hook.timeout === "number" ? hook.timeout : void 0
@@ -17359,7 +17460,7 @@ async function importGeminiSettings(projectRoot, results) {
17359
17460
  const legacyMapped = value.filter(
17360
17461
  (entry) => entry !== null && typeof entry === "object" && typeof entry.matcher === "string" && hasHookCommand(entry)
17361
17462
  ).map((entry) => ({
17362
- matcher: entry.matcher,
17463
+ matcher: fromGeminiMatcher(event, entry.matcher),
17363
17464
  command: getHookCommand(entry),
17364
17465
  type: "command"
17365
17466
  }));
@@ -17388,7 +17489,7 @@ var init_format_helpers_settings = __esm({
17388
17489
  init_hook_command();
17389
17490
  init_fs();
17390
17491
  init_constants33();
17391
- init_format_helpers_shared();
17492
+ init_hook_map();
17392
17493
  }
17393
17494
  });
17394
17495
  async function importGeminiIgnore(projectRoot, results) {
@@ -17884,14 +17985,7 @@ function lintPermissions10(canonical, options) {
17884
17985
  }
17885
17986
  function lintHooks12(canonical) {
17886
17987
  if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
17887
- const supported = [
17888
- "PreToolUse",
17889
- "PostToolUse",
17890
- "Notification",
17891
- "SubagentStart",
17892
- "SubagentStop",
17893
- "SessionStart"
17894
- ];
17988
+ const supported = [...CANONICAL_TO_GEMINI.keys()];
17895
17989
  return unsupportedHookEventNames(canonical.hooks, supported).map(
17896
17990
  (event) => createUnsupportedHookWarning(event, "gemini-cli", supported)
17897
17991
  );
@@ -17899,6 +17993,7 @@ function lintHooks12(canonical) {
17899
17993
  var init_lint15 = __esm({
17900
17994
  "src/targets/gemini-cli/lint.ts"() {
17901
17995
  init_helpers();
17996
+ init_hook_map();
17902
17997
  }
17903
17998
  });
17904
17999
 
@@ -17935,6 +18030,7 @@ var init_gemini_cli2 = __esm({
17935
18030
  init_import_map_builders();
17936
18031
  init_lint15();
17937
18032
  init_scoped_settings_emit();
18033
+ init_hook_map();
17938
18034
  init_settings();
17939
18035
  target16 = {
17940
18036
  name: "gemini-cli",
@@ -17985,6 +18081,7 @@ var init_gemini_cli2 = __esm({
17985
18081
  permissions: lintPermissions10
17986
18082
  },
17987
18083
  emitScopedSettings: emitScopedGeminiSettings,
18084
+ hookContextEvents: GEMINI_HOOK_CONTEXT_EVENTS,
17988
18085
  mergeGeneratedOutputContent(existing, pending, newContent, resolvedPath) {
17989
18086
  const base = pending?.content ?? existing;
17990
18087
  if (base !== null && resolvedPath === GEMINI_SETTINGS) {
@@ -18077,14 +18174,10 @@ function generateAgents17(canonical) {
18077
18174
  content: serializeProjectedAgentSkill(agent)
18078
18175
  }));
18079
18176
  }
18080
- function generateIgnore14(canonical) {
18081
- if (canonical.ignore.length === 0) return [];
18082
- return [{ path: GOOSE_IGNORE, content: canonical.ignore.join("\n") }];
18083
- }
18084
18177
  function generateHooks12(canonical) {
18085
18178
  return buildWrappedCommandHooks(canonical, GOOSE_HOOKS_FILE);
18086
18179
  }
18087
- var generateRules17, generatePermissions12;
18180
+ var generateRules17, generateIgnore14, generatePermissions12;
18088
18181
  var init_generator20 = __esm({
18089
18182
  "src/targets/goose/generator.ts"() {
18090
18183
  init_managed_blocks();
@@ -18094,7 +18187,9 @@ var init_generator20 = __esm({
18094
18187
  init_command_skill();
18095
18188
  init_wrapped_command_hooks();
18096
18189
  init_constants16();
18190
+ init_ignore_output();
18097
18191
  generateRules17 = (canonical) => embeddedRootRule(canonical, GOOSE_TARGET, GOOSE_ROOT_FILE);
18192
+ generateIgnore14 = ignoreOutput(GOOSE_IGNORE);
18098
18193
  generatePermissions12 = NO_OUTPUTS;
18099
18194
  }
18100
18195
  });
@@ -18999,10 +19094,6 @@ function generateAgents18(canonical) {
18999
19094
  };
19000
19095
  });
19001
19096
  }
19002
- function generateIgnore16(canonical) {
19003
- if (canonical.ignore.length === 0) return [];
19004
- return [{ path: JUNIE_IGNORE, content: canonical.ignore.join("\n") }];
19005
- }
19006
19097
  function generateSkills17(canonical) {
19007
19098
  return generateEmbeddedSkills(canonical, JUNIE_SKILLS_DIR);
19008
19099
  }
@@ -19014,6 +19105,7 @@ function renderJunieGlobalInstructions(canonical) {
19014
19105
  });
19015
19106
  return appendEmbeddedRulesBlock(root?.body.trim() ?? "", nonRootRules);
19016
19107
  }
19108
+ var generateIgnore16;
19017
19109
  var init_generator22 = __esm({
19018
19110
  "src/targets/junie/generator.ts"() {
19019
19111
  init_mcp_servers();
@@ -19021,7 +19113,9 @@ var init_generator22 = __esm({
19021
19113
  init_managed_blocks();
19022
19114
  init_markdown();
19023
19115
  init_constants18();
19116
+ init_ignore_output();
19024
19117
  init_global_config2();
19118
+ generateIgnore16 = ignoreOutput(JUNIE_IGNORE);
19025
19119
  }
19026
19120
  });
19027
19121
 
@@ -19437,10 +19531,6 @@ function generateMcp15(canonical) {
19437
19531
  }
19438
19532
  ];
19439
19533
  }
19440
- function generateIgnore17(canonical) {
19441
- if (canonical.ignore.length === 0) return [];
19442
- return [{ path: KILO_CODE_IGNORE, content: canonical.ignore.join("\n") }];
19443
- }
19444
19534
  function generatePermissions15(canonical) {
19445
19535
  if (!canonical.permissions) return [];
19446
19536
  const { allow, deny } = canonical.permissions;
@@ -19455,11 +19545,14 @@ function generatePermissions15(canonical) {
19455
19545
  function generateSkills18(canonical) {
19456
19546
  return generateEmbeddedSkills(canonical, KILO_CODE_SKILLS_DIR);
19457
19547
  }
19548
+ var generateIgnore17;
19458
19549
  var init_generator23 = __esm({
19459
19550
  "src/targets/kilo-code/generator.ts"() {
19460
19551
  init_embedded_skill();
19461
19552
  init_markdown();
19462
19553
  init_constants19();
19554
+ init_ignore_output();
19555
+ generateIgnore17 = ignoreOutput(KILO_CODE_IGNORE);
19463
19556
  }
19464
19557
  });
19465
19558
  var kiloNonRootRuleMapper, kiloCommandMapper, kiloAgentMapper;
@@ -21204,11 +21297,7 @@ function buildKiroAgentOutputs(canonical, rules = []) {
21204
21297
  function generateAgents21(canonical) {
21205
21298
  return buildKiroAgentOutputs(canonical);
21206
21299
  }
21207
- function generateIgnore18(canonical) {
21208
- if (canonical.ignore.length === 0) return [];
21209
- return [{ path: KIRO_IGNORE, content: canonical.ignore.join("\n") }];
21210
- }
21211
- var generatePermissions17;
21300
+ var generateIgnore18, generatePermissions17;
21212
21301
  var init_generator25 = __esm({
21213
21302
  "src/targets/kiro/generator.ts"() {
21214
21303
  init_no_outputs();
@@ -21217,6 +21306,8 @@ var init_generator25 = __esm({
21217
21306
  init_markdown();
21218
21307
  init_hook_format3();
21219
21308
  init_constants21();
21309
+ init_ignore_output();
21310
+ generateIgnore18 = ignoreOutput(KIRO_IGNORE);
21220
21311
  generatePermissions17 = NO_OUTPUTS;
21221
21312
  }
21222
21313
  });
@@ -23859,10 +23950,6 @@ function generateMcp19(canonical) {
23859
23950
  const content = JSON.stringify({ mcpServers: canonical.mcp.mcpServers }, null, 2);
23860
23951
  return [{ path: QWEN_SETTINGS, content }];
23861
23952
  }
23862
- function generateIgnore21(canonical) {
23863
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
23864
- return [{ path: QWEN_IGNORE, content: canonical.ignore.join("\n") }];
23865
- }
23866
23953
  function generateHooks18(canonical) {
23867
23954
  if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
23868
23955
  const hooks = buildClaudeHooksObjectFromCanonical(canonical);
@@ -23880,11 +23967,14 @@ function generatePermissions21(canonical) {
23880
23967
  if (ask.length > 0) permissions.ask = ask;
23881
23968
  return [{ path: QWEN_SETTINGS, content: JSON.stringify({ permissions }, null, 2) }];
23882
23969
  }
23970
+ var generateIgnore21;
23883
23971
  var init_generator29 = __esm({
23884
23972
  "src/targets/qwen-code/generator.ts"() {
23885
23973
  init_markdown();
23886
23974
  init_hooks_format2();
23887
23975
  init_constants25();
23976
+ init_ignore_output();
23977
+ generateIgnore21 = ignoreOutput(QWEN_IGNORE);
23888
23978
  }
23889
23979
  });
23890
23980
 
@@ -25582,10 +25672,6 @@ function generateMcp23(canonical) {
25582
25672
  }
25583
25673
  ];
25584
25674
  }
25585
- function generateIgnore24(canonical) {
25586
- if (canonical.ignore.length === 0) return [];
25587
- return [{ path: TRAE_IGNORE, content: canonical.ignore.join("\n") }];
25588
- }
25589
25675
  function generateHooks20(canonical) {
25590
25676
  if (!canonical.hooks) return [];
25591
25677
  const hooks = {};
@@ -25606,12 +25692,15 @@ function generateHooks20(canonical) {
25606
25692
  if (Object.keys(hooks).length === 0) return [];
25607
25693
  return [{ path: TRAE_HOOKS_FILE, content: JSON.stringify({ version: 1, hooks }, null, 2) }];
25608
25694
  }
25695
+ var generateIgnore24;
25609
25696
  var init_generator33 = __esm({
25610
25697
  "src/targets/trae/generator.ts"() {
25611
25698
  init_embedded_skill();
25612
25699
  init_markdown();
25613
25700
  init_hook_command();
25614
25701
  init_constants29();
25702
+ init_ignore_output();
25703
+ generateIgnore24 = ignoreOutput(TRAE_IGNORE);
25615
25704
  }
25616
25705
  });
25617
25706
 
@@ -26318,11 +26407,7 @@ function generateMcp24(canonical, ctx) {
26318
26407
  const content = JSON.stringify({ mcpServers: canonical.mcp.mcpServers }, null, 2);
26319
26408
  return [{ path, content }];
26320
26409
  }
26321
- function generateIgnore25(canonical) {
26322
- if (canonical.ignore.length === 0) return [];
26323
- return [{ path: WARP_IGNORE_FILE, content: canonical.ignore.join("\n") }];
26324
- }
26325
- var generateRules31, generatePermissions24, generateHooks21;
26410
+ var generateRules31, generatePermissions24, generateHooks21, generateIgnore25;
26326
26411
  var init_generator34 = __esm({
26327
26412
  "src/targets/warp/generator.ts"() {
26328
26413
  init_managed_blocks();
@@ -26331,9 +26416,11 @@ var init_generator34 = __esm({
26331
26416
  init_projected_agent_skill();
26332
26417
  init_command_skill();
26333
26418
  init_constants30();
26419
+ init_ignore_output();
26334
26420
  generateRules31 = (canonical) => embeddedRootRule(canonical, WARP_TARGET, WARP_ROOT_FILE);
26335
26421
  generatePermissions24 = NO_OUTPUTS;
26336
26422
  generateHooks21 = NO_OUTPUTS;
26423
+ generateIgnore25 = ignoreOutput(WARP_IGNORE_FILE);
26337
26424
  }
26338
26425
  });
26339
26426
 
@@ -26958,13 +27045,12 @@ var init_rules4 = __esm({
26958
27045
  });
26959
27046
 
26960
27047
  // src/targets/windsurf/generator/ignore.ts
26961
- function generateIgnore26(canonical) {
26962
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
26963
- return [{ path: CODEIUM_IGNORE, content: canonical.ignore.join("\n") }];
26964
- }
27048
+ var generateIgnore26;
26965
27049
  var init_ignore3 = __esm({
26966
27050
  "src/targets/windsurf/generator/ignore.ts"() {
26967
27051
  init_constants34();
27052
+ init_ignore_output();
27053
+ generateIgnore26 = ignoreOutput(CODEIUM_IGNORE);
26968
27054
  }
26969
27055
  });
26970
27056
 
@@ -27030,7 +27116,7 @@ function canonicalHookEventName(event) {
27030
27116
  if (KNOWN_CANONICAL_HOOK_EVENTS.includes(event)) return event;
27031
27117
  return WINDSURF_TO_CANONICAL.get(event) ?? null;
27032
27118
  }
27033
- var KNOWN_CANONICAL_HOOK_EVENTS, WINDSURF_TO_CANONICAL;
27119
+ var KNOWN_CANONICAL_HOOK_EVENTS, WINDSURF_HOOK_CONTEXT_EVENTS, WINDSURF_TO_CANONICAL;
27034
27120
  var init_hook_events = __esm({
27035
27121
  "src/targets/windsurf/hook-events.ts"() {
27036
27122
  init_hook_types();
@@ -27043,6 +27129,7 @@ var init_hook_events = __esm({
27043
27129
  "SubagentStop",
27044
27130
  ...BEST_EFFORT_HOOK_EVENTS
27045
27131
  ];
27132
+ WINDSURF_HOOK_CONTEXT_EVENTS = [];
27046
27133
  WINDSURF_TO_CANONICAL = new Map(
27047
27134
  KNOWN_CANONICAL_HOOK_EVENTS.map((event) => [windsurfEventName(event), event])
27048
27135
  );
@@ -27068,12 +27155,12 @@ function toWindsurfHooks(hooks) {
27068
27155
  return result2;
27069
27156
  }
27070
27157
  function generateHooks22(canonical) {
27071
- if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
27158
+ if (!canonical.hooks) return [];
27072
27159
  const hooks = toWindsurfHooks(canonical.hooks);
27073
27160
  if (Object.keys(hooks).length === 0) return [];
27074
27161
  return [{ path: WINDSURF_HOOKS_FILE, content: JSON.stringify({ hooks }, null, 2) }];
27075
27162
  }
27076
- var init_hooks4 = __esm({
27163
+ var init_hooks5 = __esm({
27077
27164
  "src/targets/windsurf/generator/hooks.ts"() {
27078
27165
  init_hook_command();
27079
27166
  init_constants34();
@@ -27125,7 +27212,7 @@ var init_generator35 = __esm({
27125
27212
  init_workflows();
27126
27213
  init_agents4();
27127
27214
  init_mcp4();
27128
- init_hooks4();
27215
+ init_hooks5();
27129
27216
  init_skills4();
27130
27217
  init_permissions6();
27131
27218
  }
@@ -27244,7 +27331,7 @@ async function parseHooks(hooksPath, onParseError) {
27244
27331
  return result2;
27245
27332
  }
27246
27333
  var VALID_TYPES;
27247
- var init_hooks5 = __esm({
27334
+ var init_hooks6 = __esm({
27248
27335
  "src/canonical/features/hooks.ts"() {
27249
27336
  init_syntax_error();
27250
27337
  init_fs();
@@ -27325,7 +27412,7 @@ var WILDCARD_MATCHER;
27325
27412
  var init_importer_hooks2 = __esm({
27326
27413
  "src/targets/windsurf/importer-hooks.ts"() {
27327
27414
  init_canonical_paths();
27328
- init_hooks5();
27415
+ init_hooks6();
27329
27416
  init_fs();
27330
27417
  init_constants34();
27331
27418
  init_hook_events();
@@ -27626,6 +27713,7 @@ var init_windsurf2 = __esm({
27626
27713
  init_merge14();
27627
27714
  init_linter32();
27628
27715
  init_lint31();
27716
+ init_hook_events();
27629
27717
  init_import_map_builders();
27630
27718
  init_conversions();
27631
27719
  init_projected_agent_skill();
@@ -27757,6 +27845,7 @@ var init_windsurf2 = __esm({
27757
27845
  },
27758
27846
  emptyImportMessage: "No Windsurf config found (.windsurfrules, .windsurf/rules, .windsurfignore, or .codeiumignore).",
27759
27847
  supportsConversion: { agents: true },
27848
+ hookContextEvents: WINDSURF_HOOK_CONTEXT_EVENTS,
27760
27849
  lintRules: lintRules32,
27761
27850
  lint: {
27762
27851
  commands: lintCommands10,
@@ -28901,6 +28990,23 @@ var init_builtin_targets = __esm({
28901
28990
  }
28902
28991
  });
28903
28992
 
28993
+ // src/targets/catalog/recall-hook-targets.ts
28994
+ init_recall_hook_scaffold();
28995
+ init_builtin_targets();
28996
+ init_registry();
28997
+ function withTargetRecallHooks(canonical, target34) {
28998
+ const descriptor34 = getBuiltinTargetDefinition(target34) ?? getDescriptor(target34);
28999
+ const contextEvents = descriptor34?.hookContextEvents;
29000
+ if (contextEvents === void 0 || canonical.hooks === null) return canonical;
29001
+ const hooks = {};
29002
+ for (const [event, entries] of Object.entries(canonical.hooks)) {
29003
+ if (!Array.isArray(entries)) continue;
29004
+ const kept = contextEvents.includes(event) ? entries : entries.filter((entry) => !isRecallHookCommand(entry?.command));
29005
+ if (kept.length > 0) hooks[event] = kept;
29006
+ }
29007
+ return { ...canonical, hooks };
29008
+ }
29009
+
28904
29010
  // src/core/generate/engine.ts
28905
29011
  init_builtin_targets();
28906
29012
  init_registry();
@@ -29803,11 +29909,12 @@ async function generateHooksFeature(results, targets, canonical, projectRoot, sc
29803
29909
  const gen = resolveTargetFeatureGenerator(target34, "hooks", config, scope) ?? getDescriptor(target34)?.generators.generateHooks;
29804
29910
  if (!gen) continue;
29805
29911
  const ctx = featureContext(target34, "hooks", scope);
29806
- let outputs = [...gen(canonical, ctx)];
29912
+ const projected = withTargetRecallHooks(canonical, target34);
29913
+ let outputs = [...gen(projected, ctx)];
29807
29914
  const descriptor34 = getBuiltinTargetDefinition(target34) ?? getDescriptor(target34);
29808
29915
  const post = descriptor34?.postProcessHookOutputs;
29809
29916
  if (post) {
29810
- outputs = [...await post(projectRoot, canonical, outputs)];
29917
+ outputs = [...await post(projectRoot, projected, outputs)];
29811
29918
  }
29812
29919
  const options = outputMergeOptions(target34);
29813
29920
  for (const out2 of outputs) {
@@ -29820,7 +29927,7 @@ async function generateScopedSettingsFeature(results, targets, canonical, projec
29820
29927
  const descriptor34 = getBuiltinTargetDefinition(target34) ?? getDescriptor(target34);
29821
29928
  const emit = descriptor34?.emitScopedSettings;
29822
29929
  if (!emit) continue;
29823
- const outputs = emit(canonical, scope, enabledFeatures);
29930
+ const outputs = emit(withTargetRecallHooks(canonical, target34), scope, enabledFeatures);
29824
29931
  if (outputs.length === 0) continue;
29825
29932
  const options = outputMergeOptions(target34);
29826
29933
  for (const out2 of outputs) {
@@ -29863,7 +29970,12 @@ async function generate(ctx) {
29863
29970
  const descriptor34 = getBuiltinTargetDefinition(target34) ?? getDescriptor(target34);
29864
29971
  const scopeExtras = descriptor34?.globalSupport?.scopeExtras;
29865
29972
  if (scopeExtras) {
29866
- const extras = await scopeExtras(canonical, projectRoot, scope, enabledFeatures);
29973
+ const extras = await scopeExtras(
29974
+ withTargetRecallHooks(canonical, target34),
29975
+ projectRoot,
29976
+ scope,
29977
+ enabledFeatures
29978
+ );
29867
29979
  await emitScopeExtras(results, target34, extras, projectRoot);
29868
29980
  }
29869
29981
  }
@@ -31170,7 +31282,7 @@ async function parsePermissions(permissionsPath, onParseError) {
31170
31282
  }
31171
31283
 
31172
31284
  // src/canonical/load/loader.ts
31173
- init_hooks5();
31285
+ init_hooks6();
31174
31286
 
31175
31287
  // src/canonical/features/ignore.ts
31176
31288
  init_fs();
@@ -31219,10 +31331,10 @@ function ruleSlug4(r) {
31219
31331
  function mergeByKey(base, overlay, key) {
31220
31332
  return [...new Map([...base, ...overlay].map((item) => [key(item), item])).values()];
31221
31333
  }
31222
- function mergeCanonicalFiles(base, overlay) {
31334
+ function mergeCanonicalFiles(base, overlay, options = {}) {
31223
31335
  const mcp = mergeMcp7(base.mcp, overlay.mcp);
31224
31336
  const permissions = mergePermissions2(base.permissions, overlay.permissions);
31225
- const hooks = mergeHooks5(base.hooks, overlay.hooks);
31337
+ const hooks = options.hooks === "combine" ? combineHooks(base.hooks, overlay.hooks) : overrideHooks(base.hooks, overlay.hooks);
31226
31338
  const ignore = mergeUniqueStrings(base.ignore, overlay.ignore);
31227
31339
  return {
31228
31340
  rules: mergeByKey(base.rules, overlay.rules, ruleSlug4),
@@ -31261,17 +31373,31 @@ function mergeUniqueStrings(base, overlay) {
31261
31373
  }
31262
31374
  return merged;
31263
31375
  }
31264
- function mergeHooks5(base, overlay) {
31376
+ function overrideHooks(base, overlay) {
31265
31377
  if (!base && !overlay) return null;
31266
31378
  const result2 = {};
31267
- const keys = /* @__PURE__ */ new Set([...Object.keys(base ?? {}), ...Object.keys(overlay ?? {})]);
31268
- for (const k of keys) {
31379
+ for (const k of hookEvents(base, overlay)) {
31269
31380
  const o = overlay?.[k];
31270
- const b = base?.[k];
31271
- result2[k] = o !== void 0 && o.length > 0 ? o : b ?? [];
31381
+ result2[k] = o !== void 0 && o.length > 0 ? o : base?.[k] ?? [];
31382
+ }
31383
+ return result2;
31384
+ }
31385
+ function combineHooks(first, then) {
31386
+ if (!first && !then) return null;
31387
+ const result2 = {};
31388
+ for (const k of hookEvents(first, then)) {
31389
+ const kept = first?.[k] ?? [];
31390
+ const defined = new Set(kept.map(hookKey));
31391
+ result2[k] = [...kept, ...(then?.[k] ?? []).filter((entry) => !defined.has(hookKey(entry)))];
31272
31392
  }
31273
31393
  return result2;
31274
31394
  }
31395
+ function hookEvents(a, b) {
31396
+ return [.../* @__PURE__ */ new Set([...Object.keys(a ?? {}), ...Object.keys(b ?? {})])];
31397
+ }
31398
+ function hookKey(entry) {
31399
+ return JSON.stringify([entry.type ?? "command", entry.matcher, entry.command]);
31400
+ }
31275
31401
 
31276
31402
  // src/config/resolve/native-format-detector.ts
31277
31403
  init_fs();
@@ -32042,7 +32168,7 @@ function gateExtendElevatedArtifacts(canonical, ext) {
32042
32168
  });
32043
32169
  }
32044
32170
  init_mcp();
32045
- init_hooks5();
32171
+ init_hooks6();
32046
32172
 
32047
32173
  // src/install/pack/pack-reader.ts
32048
32174
  init_fs();
@@ -32129,7 +32255,7 @@ async function loadPacksCanonical(abDir) {
32129
32255
  const canonical = await loadPackCanonical(packDir);
32130
32256
  const filtered = filterCanonicalByFeatures(canonical, meta.features);
32131
32257
  const picked = applyExtendPick(filtered, meta.features, meta.pick, meta.name);
32132
- merged = mergeCanonicalFiles(merged, picked);
32258
+ merged = mergeCanonicalFiles(merged, picked, { hooks: "combine" });
32133
32259
  }
32134
32260
  return merged;
32135
32261
  }
@@ -32180,9 +32306,10 @@ async function loadCanonicalWithExtends(config, configDir, options = {}, canonic
32180
32306
  merged = mergeCanonicalFiles(merged, picked);
32181
32307
  }
32182
32308
  const packsCanonical = await loadPacksCanonical(canonicalDir);
32183
- merged = mergeCanonicalFiles(merged, packsCanonical);
32309
+ merged = mergeCanonicalFiles(merged, packsCanonical, { hooks: "combine" });
32184
32310
  const localCanonical = await loadCanonicalFiles(canonicalDir);
32185
32311
  merged = mergeCanonicalFiles(merged, localCanonical);
32312
+ merged = { ...merged, hooks: combineHooks(merged.hooks, packsCanonical.hooks) };
32186
32313
  return { canonical: merged, resolvedExtends };
32187
32314
  }
32188
32315
 
@@ -32461,6 +32588,9 @@ function lintRuleScopeInversion(input) {
32461
32588
  }
32462
32589
  return out2;
32463
32590
  }
32591
+
32592
+ // src/lessons/graph-store.ts
32593
+ init_fs_text_encoding();
32464
32594
  var CURRENT_GRAPH_VERSION = 2;
32465
32595
  var VersionSchema = z.union([z.literal(1), z.literal(2)]);
32466
32596
  var MAX_RULE_LENGTH = 2e3;
@@ -32504,25 +32634,54 @@ var LessonsGraphSchema = z.object({
32504
32634
  function parseGraph(raw) {
32505
32635
  return LessonsGraphSchema.parse(raw);
32506
32636
  }
32637
+ function emptyGraph() {
32638
+ return { version: CURRENT_GRAPH_VERSION, lessons: {}, topics: {}, triggers: {} };
32639
+ }
32507
32640
 
32508
32641
  // src/lessons/graph-store.ts
32509
- var GRAPH_REL_PATH = ".agentsmesh/lessons/lessons.json";
32642
+ var LESSONS_GRAPH_PATH = ".agentsmesh/lessons/lessons.json";
32510
32643
  function graphFilePath(projectRoot) {
32511
- return resolve(projectRoot, GRAPH_REL_PATH);
32644
+ return resolve(projectRoot, LESSONS_GRAPH_PATH);
32512
32645
  }
32513
32646
  function loadLessonsGraph(projectRoot) {
32514
32647
  const raw = readFileSync(graphFilePath(projectRoot), "utf8");
32515
- return parseGraph(JSON.parse(raw));
32648
+ return parseGraph(JSON.parse(stripBom(raw)));
32516
32649
  }
32517
32650
  function tryLoadLessonsGraph(projectRoot) {
32518
32651
  if (!existsSync(graphFilePath(projectRoot))) return null;
32519
32652
  return loadLessonsGraph(projectRoot);
32520
32653
  }
32654
+ var LessonsGraphReadOnlyError = class extends Error {
32655
+ constructor() {
32656
+ super(
32657
+ `${LESSONS_GRAPH_PATH} is read-only, so nothing was saved. Make it writable (chmod u+w ${LESSONS_GRAPH_PATH}) to change lessons.`
32658
+ );
32659
+ this.name = "LessonsGraphReadOnlyError";
32660
+ }
32661
+ };
32662
+ function fileMode(path) {
32663
+ try {
32664
+ return statSync(path).mode & 511;
32665
+ } catch {
32666
+ return void 0;
32667
+ }
32668
+ }
32669
+ function isWritable(path) {
32670
+ try {
32671
+ accessSync(path, constants.W_OK);
32672
+ return true;
32673
+ } catch {
32674
+ return false;
32675
+ }
32676
+ }
32521
32677
  function saveLessonsGraph(projectRoot, graph) {
32522
32678
  const path = graphFilePath(projectRoot);
32523
32679
  mkdirSync(dirname(path), { recursive: true });
32680
+ const mode = fileMode(path);
32681
+ if (mode !== void 0 && !isWritable(path)) throw new LessonsGraphReadOnlyError();
32524
32682
  const tmp = `${path}.${process.pid}.tmp`;
32525
32683
  writeFileSync(tmp, serializeGraph(graph), "utf8");
32684
+ if (mode !== void 0) chmodSync(tmp, mode);
32526
32685
  renameSync(tmp, path);
32527
32686
  }
32528
32687
  function serializeGraph(graph) {
@@ -32566,9 +32725,76 @@ Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Man
32566
32725
  **Capture:** after any failure, user correction, regression, wrong assumption, useful surprise, repeated friction, or non-obvious fix, MUST self-critique and run \`agentsmesh lessons add "<imperative rule>" --topic <id> --trigger-file <glob> --evidence <sha|lesson-id>\`.
32567
32726
 
32568
32727
  **Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`;
32728
+ var MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
32729
+ function runGit2(cwd, args, timeoutMs) {
32730
+ const r = spawnSync("git", [...args], {
32731
+ cwd,
32732
+ encoding: "utf8",
32733
+ maxBuffer: MAX_OUTPUT_BYTES,
32734
+ timeout: timeoutMs,
32735
+ windowsHide: true
32736
+ });
32737
+ const status = r.error === void 0 ? r.status ?? -1 : -1;
32738
+ return { status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
32739
+ }
32740
+
32741
+ // src/lessons/git-path-history.ts
32742
+ var GIT_SCAN_TIMEOUT_MS = 3e3;
32743
+ var cache = /* @__PURE__ */ new Map();
32744
+ function readGitPathHistory(projectRoot) {
32745
+ if (!cache.has(projectRoot)) cache.set(projectRoot, scanGitPathHistory(projectRoot));
32746
+ return cache.get(projectRoot) ?? null;
32747
+ }
32748
+ var LOG_ARGS = [
32749
+ "log",
32750
+ "HEAD",
32751
+ "--relative",
32752
+ "-M",
32753
+ "--diff-filter=DR",
32754
+ "--name-status",
32755
+ "-z",
32756
+ "--no-color",
32757
+ "--no-show-signature",
32758
+ "--pretty=format:"
32759
+ ];
32760
+ function scanGitPathHistory(projectRoot, timeoutMs = GIT_SCAN_TIMEOUT_MS) {
32761
+ const tracked = runGit2(projectRoot, ["ls-files", "-z"], timeoutMs);
32762
+ if (tracked.status !== 0) return null;
32763
+ const log = runGit2(projectRoot, LOG_ARGS, timeoutMs);
32764
+ if (log.status !== 0) return null;
32765
+ return {
32766
+ tracked: new Set(tracked.stdout.split("\0").filter(Boolean)),
32767
+ ...parseRemovals(log.stdout)
32768
+ };
32769
+ }
32770
+ function parseRemovals(out2) {
32771
+ const deleted = /* @__PURE__ */ new Set();
32772
+ const renamedAway = /* @__PURE__ */ new Set();
32773
+ const tokens = out2.split("\0");
32774
+ for (let i = 0; i < tokens.length; i += 1) {
32775
+ const status = tokens[i] ?? "";
32776
+ const path = tokens[i + 1] ?? "";
32777
+ if (status.startsWith("D")) {
32778
+ if (path !== "") deleted.add(path);
32779
+ i += 1;
32780
+ } else if (status.startsWith("R")) {
32781
+ if (path !== "") renamedAway.add(path);
32782
+ i += 2;
32783
+ }
32784
+ }
32785
+ return { deleted, renamedAway };
32786
+ }
32787
+
32788
+ // src/lessons/project-files.ts
32569
32789
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
32570
32790
  var MAX_FILES = 2e5;
32571
- function listProjectFiles(projectRoot) {
32791
+ function projectFilesOf(paths2, gitHistory) {
32792
+ return Object.assign(new Set(paths2), { gitHistory });
32793
+ }
32794
+ function gitHistoryOf(paths2) {
32795
+ return paths2.gitHistory?.() ?? null;
32796
+ }
32797
+ function listProjectFiles(projectRoot, maxFiles = MAX_FILES) {
32572
32798
  const out2 = /* @__PURE__ */ new Set();
32573
32799
  try {
32574
32800
  const stack = [projectRoot];
@@ -32579,14 +32805,14 @@ function listProjectFiles(projectRoot) {
32579
32805
  if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
32580
32806
  } else if (entry.isFile()) {
32581
32807
  out2.add(toRelPath(projectRoot, join(dir, entry.name)));
32582
- if (out2.size > MAX_FILES) return out2;
32808
+ if (out2.size > maxFiles) return null;
32583
32809
  }
32584
32810
  }
32585
32811
  }
32586
32812
  } catch {
32587
32813
  return null;
32588
32814
  }
32589
- return out2;
32815
+ return projectFilesOf(out2, () => readGitPathHistory(projectRoot));
32590
32816
  }
32591
32817
 
32592
32818
  // src/lessons/validate-checks.ts
@@ -32763,6 +32989,265 @@ function collectOrphans(graph, findings) {
32763
32989
  }
32764
32990
  }
32765
32991
  }
32992
+
32993
+ // src/lessons/glob-expand.ts
32994
+ var MARK = "\0";
32995
+ var LED = "";
32996
+ var MAX_EXPANSIONS = 64;
32997
+ function fail(reason) {
32998
+ throw new Error(reason);
32999
+ }
33000
+ function skipClass(s, i) {
33001
+ const end = s.indexOf("]", i + 1);
33002
+ return end === -1 ? i + 1 : end + 1;
33003
+ }
33004
+ function markLedStars(body) {
33005
+ let out2 = "";
33006
+ let depth = 0;
33007
+ for (let i = 0; i < body.length; ) {
33008
+ const c2 = body[i];
33009
+ if (c2 === "[") {
33010
+ const end = skipClass(body, i);
33011
+ out2 += body.slice(i, end);
33012
+ i = end;
33013
+ continue;
33014
+ }
33015
+ if (c2 === "{") depth += 1;
33016
+ if (c2 === "}" && depth > 0) depth -= 1;
33017
+ const prev = body[i - 1];
33018
+ if (c2 === "*" && prev === "." && depth > 0) fail(".* inside {\u2026} is not supported");
33019
+ const segmentStart = i === 0 || prev === "/";
33020
+ const afterLeadingDot = prev === "." && (i === 1 || body[i - 2] === "/");
33021
+ const led = c2 === "*" && body[i + 1] !== "*" && prev !== "*" && (segmentStart || afterLeadingDot);
33022
+ out2 += led ? LED : c2;
33023
+ i += 1;
33024
+ }
33025
+ return out2;
33026
+ }
33027
+ function expandBraces(s) {
33028
+ let open2 = -1;
33029
+ for (let i = 0; i < s.length && open2 === -1; ) {
33030
+ if (s[i] === "[") i = skipClass(s, i);
33031
+ else if (s[i] === "}") fail("unbalanced }");
33032
+ else if (s[i] === "{") open2 = i;
33033
+ else i += 1;
33034
+ }
33035
+ if (open2 === -1) return [s];
33036
+ const options = [];
33037
+ let depth = 0;
33038
+ let start = open2 + 1;
33039
+ let close = -1;
33040
+ for (let i = open2 + 1; i < s.length && close === -1; ) {
33041
+ const c2 = s[i];
33042
+ if (c2 === "[") {
33043
+ i = skipClass(s, i);
33044
+ continue;
33045
+ }
33046
+ if (c2 === "{") depth += 1;
33047
+ else if (c2 === "}" && depth > 0) depth -= 1;
33048
+ else if (c2 === "}") close = i;
33049
+ else if (c2 === "," && depth === 0) {
33050
+ options.push(s.slice(start, i));
33051
+ start = i + 1;
33052
+ }
33053
+ i += 1;
33054
+ }
33055
+ if (close === -1) fail("unclosed {");
33056
+ if (options.length === 0)
33057
+ fail("brace groups need a comma, e.g. {a,b} (ranges are not supported)");
33058
+ options.push(s.slice(start, close));
33059
+ const suffixes = expandBraces(s.slice(close + 1));
33060
+ const out2 = [];
33061
+ for (const option of options) {
33062
+ for (const head of expandBraces(option)) {
33063
+ for (const tail of suffixes) {
33064
+ out2.push(s.slice(0, open2) + MARK + head + MARK + tail);
33065
+ if (out2.length > MAX_EXPANSIONS) fail(`more than ${MAX_EXPANSIONS} brace expansions`);
33066
+ }
33067
+ }
33068
+ }
33069
+ return out2;
33070
+ }
33071
+
33072
+ // src/lessons/glob-parse.ts
33073
+ var MAX_GLOB_LENGTH = 256;
33074
+ function parseGlob(pattern) {
33075
+ try {
33076
+ return parseOrThrow(pattern);
33077
+ } catch (err) {
33078
+ return err instanceof Error ? err.message : String(err);
33079
+ }
33080
+ }
33081
+ function parseOrThrow(pattern) {
33082
+ if (pattern.length > MAX_GLOB_LENGTH) fail(`longer than ${MAX_GLOB_LENGTH} characters`);
33083
+ if (pattern.includes("\\")) fail("backslash escapes are not supported (use / as separator)");
33084
+ if (/["\u0000-\u001f]/.test(pattern)) fail("quotes and control characters are not supported");
33085
+ if (/[()|]/.test(pattern.replace(/\[[^\]/]*\]/g, ""))) {
33086
+ fail("extglobs and (\u2026)/| groups are not supported (use {a,b})");
33087
+ }
33088
+ if (/[[\]{}]\+/.test(pattern)) fail("+ after a class or brace is a regex quantifier");
33089
+ const negated = pattern.startsWith("!");
33090
+ let body = negated ? pattern.slice(1) : pattern;
33091
+ if (body.startsWith("./")) body = body.slice(2);
33092
+ if (body.startsWith("!") || body.startsWith("./")) fail("use a single leading ! and ./");
33093
+ if (body === "") fail("empty pattern");
33094
+ const fastPath = !negated && (body === "*.*" || body === "**/*.*");
33095
+ if (fastPath) body = `${body.slice(0, -1)}?*`;
33096
+ return { negated, alternatives: expandBraces(markLedStars(body)).map(parseAlternative) };
33097
+ }
33098
+ function parseAlternative(alt) {
33099
+ const raw = alt.split("/");
33100
+ const out2 = [];
33101
+ raw.forEach((segment, i) => {
33102
+ if (segment.includes("**")) {
33103
+ if (segment !== "**") fail("** must be a whole segment, outside {\u2026} (use * in a segment)");
33104
+ if (out2.at(-1)?.k === "globstar") return;
33105
+ const before = raw[i - 1] ?? "";
33106
+ const trailing = raw.slice(i + 1).every((s) => s === "**");
33107
+ const min1 = trailing && (before.endsWith("*") || before.endsWith(LED));
33108
+ out2.push({ k: "globstar", min1 });
33109
+ return;
33110
+ }
33111
+ const tokens = tokenize(segment, alt);
33112
+ const guarded = segment.includes(LED);
33113
+ const literal = tokens.every((t) => t.k === "lit") ? tokens.map((t) => t.k === "lit" ? t.ch : "").join("") : null;
33114
+ const matchesEmpty = segment !== "" && !guarded && tokens.every((t) => t.k === "star");
33115
+ out2.push({ k: "segment", tokens, literal, guarded, matchesEmpty });
33116
+ });
33117
+ return out2;
33118
+ }
33119
+ function tokenize(segment, alt) {
33120
+ const tokens = [];
33121
+ for (let i = 0; i < segment.length; i += 1) {
33122
+ const c2 = segment[i];
33123
+ if (c2 === MARK) continue;
33124
+ if (c2 === "*" || c2 === LED) tokens.push({ k: "star" });
33125
+ else if (c2 === "?") tokens.push({ k: "one" });
33126
+ else if (c2 === "[") {
33127
+ const end = segment.indexOf("]", i + 1);
33128
+ if (end === -1) {
33129
+ if (alt.includes("]")) fail("a [...] class cannot span /");
33130
+ tokens.push({ k: "lit", ch: c2 });
33131
+ continue;
33132
+ }
33133
+ tokens.push(parseClass(segment.slice(i + 1, end)));
33134
+ i = end;
33135
+ } else tokens.push({ k: "lit", ch: c2 });
33136
+ }
33137
+ return tokens;
33138
+ }
33139
+ var CLASS_SPECIAL = /[-*+?.^${}(|)[\]]/;
33140
+ function parseClass(body) {
33141
+ if (body === "") fail("empty [] class");
33142
+ if (body.startsWith("!")) fail("[!...] is not a negation here; use [^...]");
33143
+ if (body.includes("[")) fail("POSIX [:classes:] and nested [ are not supported");
33144
+ const negated = body.startsWith("^");
33145
+ const members = negated ? body.slice(1) : body;
33146
+ if (members === "") fail("empty [^] class");
33147
+ const ranges = [];
33148
+ for (let i = 0; i < members.length; i += 1) {
33149
+ const lo = members[i];
33150
+ const hi = members[i + 2];
33151
+ if (members[i + 1] === "-" && hi !== void 0) {
33152
+ if (hi < lo) fail(`class range out of order: ${lo}-${hi}`);
33153
+ ranges.push([lo, hi]);
33154
+ i += 2;
33155
+ } else ranges.push([lo, lo]);
33156
+ }
33157
+ const inSet = (c2) => ranges.some(([lo, hi]) => c2 >= lo && c2 <= hi);
33158
+ const literal = CLASS_SPECIAL.test(body) ? null : `[${body}]`;
33159
+ return { k: "class", test: negated ? (c2) => c2 !== "/" && !inSet(c2) : inSet, literal };
33160
+ }
33161
+ function normalizeRecallFile(file, projectRoot) {
33162
+ const forward = file.replaceAll("\\", "/");
33163
+ const direct = relativize(projectRoot, forward);
33164
+ if (!direct.startsWith("../")) return direct;
33165
+ const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
33166
+ return viaReal.startsWith("../") ? direct : viaReal;
33167
+ }
33168
+ function relativize(root, forward) {
33169
+ const rel2 = relative(root, resolve(root, forward)).replaceAll("\\", "/");
33170
+ return rel2 === "" ? forward.replaceAll("\\", "/") : rel2;
33171
+ }
33172
+ function safeRealpath(path) {
33173
+ try {
33174
+ return realpathSync(path);
33175
+ } catch {
33176
+ const parent = dirname(path);
33177
+ if (parent === path) return path;
33178
+ return resolve(safeRealpath(parent), basename(path));
33179
+ }
33180
+ }
33181
+
33182
+ // src/lessons/trigger-file-glob.ts
33183
+ var ABSOLUTE = /^(?:[A-Za-z]:)?\//;
33184
+ var GLOB_CHARS = /[*?[{]/;
33185
+ var CODES = {
33186
+ outside: "TRIGGER_FILE_OUTSIDE_PROJECT",
33187
+ root: "TRIGGER_FILE_IS_PROJECT_ROOT",
33188
+ folder: "TRIGGER_FILE_IS_DIRECTORY",
33189
+ unsafe: "UNSAFE_GLOB_PATTERN"
33190
+ };
33191
+ function problemMessage(given, problem, detail) {
33192
+ switch (problem) {
33193
+ case "outside":
33194
+ return `--trigger-file ${given} points outside the project root. File triggers match project-relative paths, so it would never fire \u2014 pass a glob relative to the project root (e.g. "src/**/*.ts").`;
33195
+ case "root":
33196
+ return `--trigger-file ${given} is the project root itself, and file triggers match files. Pass a glob such as "src/**/*.ts".`;
33197
+ case "folder":
33198
+ return `--trigger-file ${given} is a folder, and file triggers match files. Use ${JSON.stringify(`${detail}/**`)} to match every file in it.`;
33199
+ case "unsafe":
33200
+ return `--trigger-file ${given} is outside the safe glob subset: ${detail}. Use only *, **, ?, [...] and {a,b}.`;
33201
+ }
33202
+ }
33203
+ var TriggerFileGlobError = class extends Error {
33204
+ constructor(pattern, problem = "outside", detail = "") {
33205
+ super(problemMessage(JSON.stringify(pattern), problem, detail));
33206
+ this.pattern = pattern;
33207
+ this.name = "TriggerFileGlobError";
33208
+ this.code = CODES[problem];
33209
+ }
33210
+ code;
33211
+ };
33212
+ function sameFolder(a, b) {
33213
+ try {
33214
+ return realpathSync(a) === realpathSync(b);
33215
+ } catch {
33216
+ return false;
33217
+ }
33218
+ }
33219
+ function isFolder(projectRoot, path) {
33220
+ if (GLOB_CHARS.test(path)) return false;
33221
+ return statSync(join(projectRoot, path), { throwIfNoEntry: false })?.isDirectory() === true;
33222
+ }
33223
+ function projectRelativeGlob(pattern, projectRoot) {
33224
+ const forward = pattern.trim().replaceAll("\\", "/");
33225
+ let rel2 = forward;
33226
+ if (ABSOLUTE.test(forward)) {
33227
+ const root = projectRoot.replaceAll("\\", "/").replace(/\/+$/, "");
33228
+ rel2 = forward.startsWith(`${root}/`) || forward === root ? forward.slice(root.length + 1) : normalizeRecallFile(forward, projectRoot);
33229
+ if (ABSOLUTE.test(rel2)) {
33230
+ throw new TriggerFileGlobError(
33231
+ pattern,
33232
+ sameFolder(forward, projectRoot) ? "root" : "outside"
33233
+ );
33234
+ }
33235
+ }
33236
+ const normalized = posix.normalize(rel2 === "" ? "." : rel2);
33237
+ if (normalized === ".." || normalized.startsWith("../")) {
33238
+ throw new TriggerFileGlobError(pattern, "outside");
33239
+ }
33240
+ const path = normalized.replace(/\/+$/, "");
33241
+ if (path === "." || path === "") throw new TriggerFileGlobError(pattern, "root");
33242
+ if (path !== normalized || isFolder(projectRoot, path)) {
33243
+ throw new TriggerFileGlobError(pattern, "folder", path);
33244
+ }
33245
+ const unsafe = parseGlob(path);
33246
+ if (typeof unsafe === "string") throw new TriggerFileGlobError(pattern, "unsafe", unsafe);
33247
+ return path;
33248
+ }
33249
+
33250
+ // src/lessons/add-helpers.ts
32766
33251
  function normalizeRule(rule) {
32767
33252
  return rule.trim().replace(/\s+/g, " ").toLowerCase();
32768
33253
  }
@@ -32771,14 +33256,50 @@ function union(base, extra) {
32771
33256
  for (const item of extra) if (!out2.includes(item)) out2.push(item);
32772
33257
  return out2;
32773
33258
  }
32774
- function mergeTriggers(graph, spec) {
33259
+ function upsertLesson(before, input, triggerIds) {
33260
+ return {
33261
+ ...before,
33262
+ topics: union(before.topics, [input.topic]),
33263
+ triggers: union(before.triggers, triggerIds),
33264
+ evidence: union(before.evidence, input.evidence ?? []),
33265
+ ...before.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
33266
+ ...input.scope === "always" ? { scope: "always" } : {}
33267
+ };
33268
+ }
33269
+ function describeUpsert(before, after) {
33270
+ const added = (base, next) => next.filter((item) => !base.includes(item));
33271
+ const topics = added(before.topics, after.topics);
33272
+ const triggers = added(before.triggers, after.triggers);
33273
+ const evidence = added(before.evidence, after.evidence);
33274
+ const changes = [];
33275
+ if (after.scope === "always" && before.scope !== "always") changes.push("scope set to always");
33276
+ if (topics.length > 0) changes.push(`topic added: ${topics.join(", ")}`);
33277
+ if (triggers.length > 0) {
33278
+ changes.push(`trigger${triggers.length === 1 ? "" : "s"} attached: ${triggers.join(", ")}`);
33279
+ }
33280
+ if (evidence.length > 0) changes.push(`evidence added: ${evidence.join(", ")}`);
33281
+ if (before.rationale === void 0 && after.rationale !== void 0) {
33282
+ changes.push("rationale added");
33283
+ }
33284
+ return changes;
33285
+ }
33286
+ function findExistingLessonByRule(graph, ruleKey2) {
33287
+ for (const [id, lesson] of Object.entries(graph.lessons)) {
33288
+ if (lesson.status !== "active") continue;
33289
+ if (normalizeRule(lesson.rule) === ruleKey2) return id;
33290
+ }
33291
+ return null;
33292
+ }
33293
+ function mergeTriggers(graph, spec, projectRoot) {
32775
33294
  const requested = [
32776
- // Normalize `\` → `/` so a Windows-shaped glob matches: recall relativizes
32777
- // every `--file` to forward slashes (normalizeRecallFile), so a backslash
32778
- // pattern stored raw would silently never fire. Normalizing here also lets
32779
- // a backslash pattern dedupe against the forward-slash node it equals.
33295
+ // Recall matches forward-slash, project-relative paths (normalizeRecallFile),
33296
+ // so a backslash or absolute pattern stored raw would silently never fire.
33297
+ // Normalizing here also dedupes it against the node it equals.
32780
33298
  ...(spec.files ?? []).map(
32781
- (p) => ({ kind: "file_glob", pattern: p.replaceAll("\\", "/") })
33299
+ (p) => ({
33300
+ kind: "file_glob",
33301
+ pattern: projectRoot === void 0 ? p.replaceAll("\\", "/") : projectRelativeGlob(p, projectRoot)
33302
+ })
32782
33303
  ),
32783
33304
  ...(spec.commands ?? []).map((p) => ({ kind: "command_pattern", pattern: p })),
32784
33305
  ...(spec.keywords ?? []).map((p) => ({ kind: "keyword", pattern: p }))
@@ -32835,6 +33356,132 @@ function todayIso() {
32835
33356
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
32836
33357
  }
32837
33358
 
33359
+ // src/lessons/glob-dp.ts
33360
+ var isDotSegment = (s) => s === "." || s === "..";
33361
+ function matchSegments(alt, segments, work) {
33362
+ const n = segments.length;
33363
+ let next = new Uint8Array(n + 1);
33364
+ next[n] = 1;
33365
+ for (let i = alt.length - 1; i >= 0; i -= 1) {
33366
+ const seg = alt[i];
33367
+ const cur = new Uint8Array(n + 1);
33368
+ for (let j = n; j >= 0; j -= 1) {
33369
+ if (--work.remaining <= 0) return false;
33370
+ if (seg.k === "globstar") {
33371
+ const eat = j < n && !isDotSegment(segments[j]) && (cur[j + 1] === 1 || next[j + 1] === 1);
33372
+ cur[j] = !seg.min1 && next[j] === 1 || eat ? 1 : 0;
33373
+ } else if (j < n && next[j + 1] === 1) {
33374
+ const text = segments[j];
33375
+ const ok = seg.literal !== null ? text === seg.literal : matchSegment(seg, text, j < n - 1, work);
33376
+ cur[j] = ok ? 1 : 0;
33377
+ }
33378
+ }
33379
+ if (seg.k === "globstar" && i >= 1 && tailMatchesNothing(alt, i + 1)) cur[n] = 1;
33380
+ next = cur;
33381
+ }
33382
+ return next[0] === 1;
33383
+ }
33384
+ function tailMatchesNothing(alt, from) {
33385
+ const empty = alt[from];
33386
+ const rest = alt[from + 1];
33387
+ if (empty?.k !== "segment" || !empty.matchesEmpty) return false;
33388
+ return rest === void 0 || rest.k === "globstar" && !rest.min1 && from + 2 === alt.length;
33389
+ }
33390
+ function matchSegment(seg, text, followedBySlash, work) {
33391
+ if (seg.guarded && (isDotSegment(text) || text === "" && !followedBySlash)) return false;
33392
+ const tokens = seg.tokens;
33393
+ const len = text.length;
33394
+ let next = new Uint8Array(len + 1);
33395
+ next[len] = 1;
33396
+ for (let k = tokens.length - 1; k >= 0; k -= 1) {
33397
+ const tok = tokens[k];
33398
+ const cur = new Uint8Array(len + 1);
33399
+ work.remaining -= len + 1;
33400
+ if (work.remaining <= 0) return false;
33401
+ for (let c2 = len; c2 >= 0; c2 -= 1) {
33402
+ cur[c2] = tokenMatches(tok, text, c2, next, cur) ? 1 : 0;
33403
+ }
33404
+ next = cur;
33405
+ }
33406
+ return next[0] === 1;
33407
+ }
33408
+ function tokenMatches(tok, text, c2, next, cur) {
33409
+ const ch = text[c2];
33410
+ switch (tok.k) {
33411
+ case "star":
33412
+ return next[c2] === 1 || ch !== void 0 && cur[c2 + 1] === 1;
33413
+ case "one":
33414
+ return ch !== void 0 && next[c2 + 1] === 1;
33415
+ case "lit":
33416
+ return ch === tok.ch && next[c2 + 1] === 1;
33417
+ case "class":
33418
+ return ch !== void 0 && tok.test(ch) && next[c2 + 1] === 1 || tok.literal !== null && text.startsWith(tok.literal, c2) && next[c2 + tok.literal.length] === 1;
33419
+ }
33420
+ }
33421
+
33422
+ // src/lessons/glob-safety.ts
33423
+ var MAX_GLOB_PATH_LENGTH = 4096;
33424
+ var MATCH_WORK_LIMIT = 2e5;
33425
+ var CACHE_LIMIT = 2e3;
33426
+ var cache2 = /* @__PURE__ */ new Map();
33427
+ function unsafeGlobFinding(triggerId, trigger) {
33428
+ if (trigger.kind !== "file_glob" || trigger.pattern.includes("\\")) return null;
33429
+ const reason = parseGlob(trigger.pattern);
33430
+ if (typeof reason !== "string") return null;
33431
+ return {
33432
+ level: "error",
33433
+ code: "UNSAFE_GLOB_PATTERN",
33434
+ message: `Trigger "${triggerId}" has a file_glob outside the safe glob subset (${trigger.pattern.slice(0, 120)}): ${reason}. Recall treats it as a non-match. Use only *, **, ?, [...] and {a,b}.`,
33435
+ triggerId
33436
+ };
33437
+ }
33438
+ function getGlobMatcher(pattern) {
33439
+ const hit = cache2.get(pattern);
33440
+ if (hit !== void 0) return hit;
33441
+ if (cache2.size >= CACHE_LIMIT) cache2.clear();
33442
+ const composed = pattern.normalize("NFC");
33443
+ const parsed = parseGlob(composed);
33444
+ const matcher = typeof parsed === "string" ? null : build(composed, parsed);
33445
+ cache2.set(pattern, matcher);
33446
+ return matcher;
33447
+ }
33448
+ function build(pattern, parsed) {
33449
+ const alts = parsed.alternatives.map(prepare);
33450
+ return {
33451
+ test(rawPath, budget) {
33452
+ const path = rawPath.normalize("NFC");
33453
+ if (path === pattern) return true;
33454
+ if (path === "" || path.length > MAX_GLOB_PATH_LENGTH) return false;
33455
+ const live = alts.filter((a) => mayMatch(a, path));
33456
+ if (live.length === 0) return parsed.negated;
33457
+ const limit = Math.min(MATCH_WORK_LIMIT, budget?.remaining ?? MATCH_WORK_LIMIT);
33458
+ const work = { remaining: limit };
33459
+ const segments = path.split("/");
33460
+ const hit = live.some((a) => matchSegments(a.alt, segments, work));
33461
+ if (budget !== void 0) budget.remaining -= limit - work.remaining;
33462
+ return work.remaining > 0 && hit !== parsed.negated;
33463
+ }
33464
+ };
33465
+ }
33466
+ function mayMatch({ head, tail }, path) {
33467
+ if (!path.endsWith(tail)) return false;
33468
+ if (head === null) return true;
33469
+ return path.startsWith(head) && (path.length === head.length || path[head.length] === "/");
33470
+ }
33471
+ function prepare(alt) {
33472
+ const first = alt[0];
33473
+ const last = alt[alt.length - 1];
33474
+ let tail = "";
33475
+ if (last?.k === "segment" && !last.matchesEmpty) {
33476
+ for (let k = last.tokens.length - 1; k >= 0; k -= 1) {
33477
+ const tok = last.tokens[k];
33478
+ if (tok.k !== "lit") break;
33479
+ tail = tok.ch + tail;
33480
+ }
33481
+ }
33482
+ return { alt, head: first?.k === "segment" ? first.literal : null, tail };
33483
+ }
33484
+
32838
33485
  // src/lessons/regex-linear/nfa-compile.ts
32839
33486
  var MAX_NFA_STATES = 2e3;
32840
33487
  var Builder = class {
@@ -33026,6 +33673,9 @@ function escapeClass(c2) {
33026
33673
  var HEX2 = /^[0-9a-fA-F]{2}$/;
33027
33674
  var HEX4 = /^[0-9a-fA-F]{4}$/;
33028
33675
  function readUnicodeEscape(src, i, c2) {
33676
+ if (c2 === "u" && src[i] === "{") {
33677
+ throw new UnsupportedRegexError("\\u{\u2026} code point escapes are not supported; use \\uHHHH");
33678
+ }
33029
33679
  if (c2 === "x") {
33030
33680
  const hex2 = src.slice(i, i + 2);
33031
33681
  return HEX2.test(hex2) ? { ch: String.fromCharCode(parseInt(hex2, 16)), len: 2 } : { ch: "x", len: 0 };
@@ -33115,7 +33765,7 @@ function parseRegex(src) {
33115
33765
  function parseAtom() {
33116
33766
  const c2 = peek();
33117
33767
  if (c2 === "(") return parseGroup2();
33118
- if (c2 === "[") return parseClass();
33768
+ if (c2 === "[") return parseClass2();
33119
33769
  if (c2 === "\\") return parseEscape();
33120
33770
  if (c2 === ".") {
33121
33771
  i += 1;
@@ -33174,7 +33824,7 @@ function parseRegex(src) {
33174
33824
  }
33175
33825
  return { k: "char", ch: escapeLiteral(c2) };
33176
33826
  }
33177
- function parseClass() {
33827
+ function parseClass2() {
33178
33828
  i += 1;
33179
33829
  const negate = peek() === "^";
33180
33830
  if (negate) i += 1;
@@ -33227,17 +33877,17 @@ function parseRegex(src) {
33227
33877
  }
33228
33878
 
33229
33879
  // src/lessons/regex-linear/index.ts
33230
- var cache = /* @__PURE__ */ new Map();
33880
+ var cache3 = /* @__PURE__ */ new Map();
33231
33881
  function compileLinearMatcher(pattern) {
33232
- const hit = cache.get(pattern);
33233
- if (hit !== void 0 || cache.has(pattern)) return hit ?? null;
33882
+ const hit = cache3.get(pattern);
33883
+ if (hit !== void 0 || cache3.has(pattern)) return hit ?? null;
33234
33884
  let matcher;
33235
33885
  try {
33236
33886
  matcher = buildMatcher(parseRegex(pattern));
33237
33887
  } catch {
33238
33888
  matcher = null;
33239
33889
  }
33240
- cache.set(pattern, matcher);
33890
+ cache3.set(pattern, matcher);
33241
33891
  return matcher;
33242
33892
  }
33243
33893
 
@@ -33278,6 +33928,8 @@ function collectDuplicateRules(graph, findings) {
33278
33928
  }
33279
33929
  function collectInvalidTriggerPatterns(graph, findings) {
33280
33930
  for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
33931
+ const globFinding = unsafeGlobFinding(triggerId, trigger);
33932
+ if (globFinding !== null) findings.push(globFinding);
33281
33933
  if (trigger.kind !== "command_pattern") continue;
33282
33934
  try {
33283
33935
  new RegExp(trigger.pattern);
@@ -33285,7 +33937,7 @@ function collectInvalidTriggerPatterns(graph, findings) {
33285
33937
  findings.push({
33286
33938
  level: "error",
33287
33939
  code: "INVALID_TRIGGER_PATTERN",
33288
- message: `Trigger "${triggerId}" has an invalid command_pattern regex (${trigger.pattern}): ${err instanceof Error ? err.message : String(err)}.`,
33940
+ message: `Trigger "${triggerId}" has an invalid command_pattern regex (${trigger.pattern}): ${regexSyntaxReason(err)}.`,
33289
33941
  triggerId
33290
33942
  });
33291
33943
  continue;
@@ -33294,12 +33946,16 @@ function collectInvalidTriggerPatterns(graph, findings) {
33294
33946
  findings.push({
33295
33947
  level: "error",
33296
33948
  code: "UNSAFE_TRIGGER_PATTERN",
33297
- message: `Trigger "${triggerId}" has a command_pattern regex outside the provably-linear subset (${trigger.pattern}): it can backtrack catastrophically (e.g. a quantified group like (a+)+ or (a|aa)+, adjacent repetition like a+a+, or a backreference/lookaround). Rewrite using a linear pattern.`,
33949
+ message: `Trigger "${triggerId}" has a command_pattern regex the linear matcher cannot run (${trigger.pattern}). It does not support backreferences (\\1, \\k<name>), lookarounds ((?=x), (?!x), (?<=x), (?<!x)), or patterns too large to run, such as (a{1000}){10}. Nested quantifiers such as (a+)+ are fine. Rewrite the pattern without the unsupported part.`,
33298
33950
  triggerId
33299
33951
  });
33300
33952
  }
33301
33953
  }
33302
33954
  }
33955
+ function regexSyntaxReason(err) {
33956
+ const text = err instanceof Error ? err.message : String(err);
33957
+ return text.replace(/^Invalid regular expression: \/[\s\S]*\/[a-z]*: /, "");
33958
+ }
33303
33959
  function collectBackslashGlobPatterns(graph, findings) {
33304
33960
  for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
33305
33961
  if (trigger.kind !== "file_glob") continue;
@@ -33380,7 +34036,11 @@ function collectTriggerSetCollisions(graph, findings) {
33380
34036
 
33381
34037
  // src/lessons/glob-breadth.ts
33382
34038
  var WILDCARD = /[*?[\]]/;
34039
+ function isNegatedGlob(pattern) {
34040
+ return pattern.startsWith("!");
34041
+ }
33383
34042
  function globNarrowness(pattern) {
34043
+ if (isNegatedGlob(pattern)) return 0;
33384
34044
  const segments = pattern.replaceAll("\\", "/").split("/").filter((segment) => segment !== "" && segment !== ".");
33385
34045
  if (segments.length === 0) return 0;
33386
34046
  let literal = 0;
@@ -33431,6 +34091,15 @@ function isBroadCommandPattern(pattern) {
33431
34091
  }
33432
34092
  return hits > COMMAND_PROBE_CORPUS.length * BROAD_HIT_RATIO;
33433
34093
  }
34094
+ function missingGlobState(pattern, history) {
34095
+ const matcher = getGlobMatcher(pattern);
34096
+ if (history === null || matcher === null) return "pending";
34097
+ const matchesAny = (paths2) => [...paths2].some((p) => matcher.test(p));
34098
+ if (matchesAny(history.tracked)) return "live";
34099
+ if (matchesAny(history.renamedAway)) return "dead";
34100
+ if (!picomatch.scan(pattern).isGlob && matchesAny(history.deleted)) return "dead";
34101
+ return "pending";
34102
+ }
33434
34103
 
33435
34104
  // src/lessons/validate-liveness.ts
33436
34105
  function activeTriggerIds(graph) {
@@ -33441,32 +34110,42 @@ function activeTriggerIds(graph) {
33441
34110
  }
33442
34111
  return ids;
33443
34112
  }
33444
- function deadFileGlobIds(graph, knownPaths) {
33445
- const active = activeTriggerIds(graph);
34113
+ function fileGlobLiveness(graph, knownPaths, triggerIds) {
34114
+ const active = triggerIds === void 0 ? activeTriggerIds(graph) : new Set(triggerIds);
33446
34115
  const paths2 = [...knownPaths];
33447
- const dead = /* @__PURE__ */ new Set();
34116
+ const missing = [];
33448
34117
  for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
33449
- if (trigger.kind !== "file_glob") continue;
33450
- if (!active.has(triggerId)) continue;
33451
- const isMatch = picomatch2(trigger.pattern, { dot: true });
33452
- if (!paths2.some((p) => isMatch(p))) dead.add(triggerId);
34118
+ if (trigger.kind !== "file_glob" || !active.has(triggerId)) continue;
34119
+ const matcher = getGlobMatcher(trigger.pattern);
34120
+ if (matcher === null) continue;
34121
+ if (!paths2.some((p) => matcher.test(p))) missing.push([triggerId, trigger.pattern]);
34122
+ }
34123
+ const dead = /* @__PURE__ */ new Set();
34124
+ const pending = /* @__PURE__ */ new Set();
34125
+ if (missing.length === 0) return { dead, pending };
34126
+ const history = gitHistoryOf(knownPaths);
34127
+ for (const [triggerId, pattern] of missing) {
34128
+ const state = missingGlobState(pattern, history);
34129
+ if (state === "dead") dead.add(triggerId);
34130
+ else if (state === "pending") pending.add(triggerId);
33453
34131
  }
33454
- return dead;
34132
+ return { dead, pending };
33455
34133
  }
33456
34134
  function collectDeadFileGlobs(graph, findings, knownPaths) {
33457
- for (const triggerId of deadFileGlobIds(graph, knownPaths)) {
34135
+ for (const triggerId of fileGlobLiveness(graph, knownPaths).dead) {
33458
34136
  findings.push({
33459
34137
  level: "warning",
33460
34138
  code: "DEAD_FILE_GLOB",
33461
- message: `file_glob trigger "${triggerId}" (${graph.triggers[triggerId]?.pattern ?? ""}) matches no file in the working tree \u2014 the lesson is unreachable via this trigger (a rename likely moved the path). Re-point it at the current path, or detach it with \`lessons untrigger\`, or run \`lessons prune --apply\`.`,
34139
+ message: `file_glob trigger "${triggerId}" (${graph.triggers[triggerId]?.pattern ?? ""}) matches no file, and git history shows its path was renamed or deleted \u2014 the lesson is unreachable via this trigger. Re-point it at the current path, or detach it with \`lessons untrigger\`, or run \`lessons prune --apply\`.`,
33462
34140
  triggerId
33463
34141
  });
33464
34142
  }
33465
34143
  }
33466
34144
  function fileGlobMatchCount(pattern, knownPaths) {
33467
- const isMatch = picomatch2(pattern, { dot: true });
34145
+ const matcher = getGlobMatcher(pattern);
34146
+ if (matcher === null) return 0;
33468
34147
  let n = 0;
33469
- for (const p of knownPaths) if (isMatch(p)) n += 1;
34148
+ for (const p of knownPaths) if (matcher.test(p)) n += 1;
33470
34149
  return n;
33471
34150
  }
33472
34151
  var RUNNER_ANCHOR = /^\^(pnpm|npm|npx|yarn|bun)\b/;
@@ -33538,7 +34217,7 @@ var STOP = /* @__PURE__ */ new Set([
33538
34217
  "its",
33539
34218
  "must"
33540
34219
  ]);
33541
- function tokenize(text) {
34220
+ function tokenize2(text) {
33542
34221
  return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
33543
34222
  }
33544
34223
  function queryTerms(query) {
@@ -33546,7 +34225,7 @@ function queryTerms(query) {
33546
34225
  if (query.keyword !== void 0) parts.push(query.keyword);
33547
34226
  if (query.file !== void 0) parts.push(query.file);
33548
34227
  if (query.command !== void 0) parts.push(query.command);
33549
- return tokenize(parts.join(" "));
34228
+ return tokenize2(parts.join(" "));
33550
34229
  }
33551
34230
  function buildCorpus(graph) {
33552
34231
  const docs = [];
@@ -33555,7 +34234,7 @@ function buildCorpus(graph) {
33555
34234
  let n = 0;
33556
34235
  for (const lesson of Object.values(graph.lessons)) {
33557
34236
  if (lesson.status !== "active") continue;
33558
- const toks = tokenize(lesson.rule);
34237
+ const toks = tokenize2(lesson.rule);
33559
34238
  n += 1;
33560
34239
  total += toks.length;
33561
34240
  docs.push(toks.length);
@@ -33567,7 +34246,7 @@ function buildCorpus(graph) {
33567
34246
  return { idf, avgdl: total / N || 1 };
33568
34247
  }
33569
34248
  function bm25(terms, ruleText, corpus) {
33570
- const toks = tokenize(ruleText);
34249
+ const toks = tokenize2(ruleText);
33571
34250
  const dl = toks.length || 1;
33572
34251
  const tf = /* @__PURE__ */ new Map();
33573
34252
  for (const t of toks) tf.set(t, (tf.get(t) ?? 0) + 1);
@@ -33584,7 +34263,7 @@ function bm25(terms, ruleText, corpus) {
33584
34263
  // src/lessons/keyword-signal.ts
33585
34264
  var MAX_RECOMMENDED_KEYWORD_TOKENS = 5;
33586
34265
  function isLowSignalKeyword(pattern) {
33587
- return tokenize(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
34266
+ return tokenize2(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
33588
34267
  }
33589
34268
  function splitRawTokens(pattern) {
33590
34269
  return pattern.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
@@ -33592,7 +34271,7 @@ function splitRawTokens(pattern) {
33592
34271
  function keywordNeedleLosesTokens(pattern) {
33593
34272
  const raw = splitRawTokens(pattern);
33594
34273
  if (raw.length < 2) return false;
33595
- return tokenize(pattern).length !== raw.length;
34274
+ return tokenize2(pattern).length !== raw.length;
33596
34275
  }
33597
34276
 
33598
34277
  // src/lessons/validate-keywords.ts
@@ -33615,7 +34294,7 @@ function collectStopwordKeywords(graph, findings) {
33615
34294
  for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
33616
34295
  if (trigger.kind !== "keyword") continue;
33617
34296
  if (!active.has(triggerId)) continue;
33618
- if (tokenize(trigger.pattern).length !== 0 && !keywordNeedleLosesTokens(trigger.pattern)) {
34297
+ if (tokenize2(trigger.pattern).length !== 0 && !keywordNeedleLosesTokens(trigger.pattern)) {
33619
34298
  continue;
33620
34299
  }
33621
34300
  findings.push({
@@ -33663,7 +34342,6 @@ function validateLessonsGraph(graph, options = {}) {
33663
34342
 
33664
34343
  // src/core/lint/shared/lessons.ts
33665
34344
  var LESSONS_TARGET = "lessons";
33666
- var GRAPH_REL = ".agentsmesh/lessons/lessons.json";
33667
34345
  var ROOT_RULE_REL = ".agentsmesh/rules/_root.md";
33668
34346
  var LESSONS_HEADING = /^## Lessons \(/m;
33669
34347
  function lintLessonsSubsystem(projectRoot, scope) {
@@ -33678,7 +34356,7 @@ function lintLessonsSubsystem(projectRoot, scope) {
33678
34356
  return [
33679
34357
  diag(
33680
34358
  "error",
33681
- GRAPH_REL,
34359
+ LESSONS_GRAPH_PATH,
33682
34360
  `lessons.json failed to load: ${err instanceof Error ? err.message : String(err)}`
33683
34361
  )
33684
34362
  ];
@@ -33686,7 +34364,7 @@ function lintLessonsSubsystem(projectRoot, scope) {
33686
34364
  const knownPaths = listProjectFiles(projectRoot) ?? void 0;
33687
34365
  const report = validateLessonsGraph(graph, { knownPaths });
33688
34366
  for (const finding of report.findings) {
33689
- out2.push(diag(finding.level, GRAPH_REL, `[${finding.code}] ${finding.message}`));
34367
+ out2.push(diag(finding.level, LESSONS_GRAPH_PATH, `[${finding.code}] ${finding.message}`));
33690
34368
  }
33691
34369
  const rootRuleAbs = join(projectRoot, ROOT_RULE_REL);
33692
34370
  const rootRuleBody = existsSync(rootRuleAbs) ? readFileSync(rootRuleAbs, "utf8") : "";
@@ -33708,8 +34386,8 @@ function diag(level, file, message) {
33708
34386
  // src/core/lint/linter.ts
33709
34387
  var EXCLUDE_DIRS = ["node_modules", ".git", "dist", "coverage", ".agentsmesh"];
33710
34388
  function isExcludedProjectPath(rel2) {
33711
- const posix8 = rel2.replaceAll("\\", "/");
33712
- return EXCLUDE_DIRS.some((d) => posix8.includes(`/${d}/`) || posix8.startsWith(`${d}/`));
34389
+ const posix11 = rel2.replaceAll("\\", "/");
34390
+ return EXCLUDE_DIRS.some((d) => posix11.includes(`/${d}/`) || posix11.startsWith(`${d}/`));
33713
34391
  }
33714
34392
  async function getProjectFiles(projectRoot) {
33715
34393
  const all = await readDirRecursive(projectRoot);
@@ -34102,14 +34780,22 @@ async function findUntrackedManagedDirFiles(args) {
34102
34780
  return [...found].sort();
34103
34781
  }
34104
34782
 
34783
+ // src/lessons/conflict-markers.ts
34784
+ function hasConflictMarkers(text) {
34785
+ return /^(?:<{7}|>{7})(?: |\r?$)/m.test(text);
34786
+ }
34787
+
34105
34788
  // src/core/check/lock-sync.ts
34789
+ init_fs();
34106
34790
  async function checkLockSync(opts) {
34107
34791
  const { config, configDir, canonicalDir, rootBase, scope = "project" } = opts;
34108
34792
  const lock = await readLock(canonicalDir);
34109
34793
  if (lock === null) {
34794
+ const text = await readFileSafe(join(canonicalDir, ".lock"));
34110
34795
  return {
34111
34796
  inSync: false,
34112
34797
  hasLock: false,
34798
+ lockConflict: text !== null && hasConflictMarkers(text),
34113
34799
  canonicalDrift: false,
34114
34800
  outputDrift: false,
34115
34801
  modified: [],
@@ -34179,6 +34865,7 @@ async function checkLockSync(opts) {
34179
34865
  return {
34180
34866
  inSync,
34181
34867
  hasLock: true,
34868
+ lockConflict: false,
34182
34869
  canonicalDrift,
34183
34870
  outputDrift,
34184
34871
  modified,
@@ -34454,6 +35141,27 @@ var UnknownTopicError = class extends Error {
34454
35141
  }
34455
35142
  code = "UNKNOWN_TOPIC";
34456
35143
  };
35144
+ var InvalidTopicIdError = class extends Error {
35145
+ constructor(topic) {
35146
+ const slug = topic.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
35147
+ super(
35148
+ `Topic id ${JSON.stringify(topic)} must be kebab-case (lowercase letters, digits and -)` + (slug.length > 0 ? `, e.g. ${JSON.stringify(slug)}.` : ".")
35149
+ );
35150
+ this.topic = topic;
35151
+ this.name = "InvalidTopicIdError";
35152
+ }
35153
+ code = "INVALID_TOPIC_ID";
35154
+ };
35155
+ var TopicSummaryRequiredError = class extends Error {
35156
+ constructor(topic) {
35157
+ super(
35158
+ `New topic ${JSON.stringify(topic)} needs a one-line summary (--topic-summary on the CLI, topic_summary over MCP).`
35159
+ );
35160
+ this.topic = topic;
35161
+ this.name = "TopicSummaryRequiredError";
35162
+ }
35163
+ code = "TOPIC_SUMMARY_REQUIRED";
35164
+ };
34457
35165
  var RuleTooLongError = class extends Error {
34458
35166
  constructor(length, max) {
34459
35167
  super(
@@ -34484,6 +35192,9 @@ var UnrecallableLessonError = class extends Error {
34484
35192
  }
34485
35193
  code = "UNRECALLABLE_LESSON";
34486
35194
  };
35195
+ function codePointLength(text) {
35196
+ return [...text].length;
35197
+ }
34487
35198
 
34488
35199
  // src/lessons/trigger-effectiveness.ts
34489
35200
  function ineffectiveTriggers(graph, triggerIds) {
@@ -34498,7 +35209,7 @@ function ineffectiveTriggers(graph, triggerIds) {
34498
35209
  }
34499
35210
  function ineffectiveReason(kind, pattern) {
34500
35211
  if (kind === "keyword") {
34501
- if (tokenize(pattern).length === 0) {
35212
+ if (tokenize2(pattern).length === 0) {
34502
35213
  return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
34503
35214
  }
34504
35215
  if (keywordNeedleLosesTokens(pattern)) {
@@ -34531,9 +35242,19 @@ function blockingDeadTriggers(graph, triggerIds) {
34531
35242
  function assertRuleShape(rule) {
34532
35243
  const trimmed = rule.trim();
34533
35244
  if (trimmed.length === 0) throw new EmptyRuleError();
34534
- if (trimmed.length > MAX_RULE_LENGTH) throw new RuleTooLongError(trimmed.length, MAX_RULE_LENGTH);
35245
+ const length = codePointLength(trimmed);
35246
+ if (length > MAX_RULE_LENGTH) throw new RuleTooLongError(length, MAX_RULE_LENGTH);
34535
35247
  return trimmed;
34536
35248
  }
35249
+ function ensureTopic(graph, topic, options) {
35250
+ if (!/^[a-z0-9-]+$/.test(topic)) throw new InvalidTopicIdError(topic);
35251
+ if (graph.topics[topic] !== void 0) return false;
35252
+ if (options.allowNewTopic !== true) throw new UnknownTopicError(topic);
35253
+ const summary = options.topicSummary?.trim() ?? "";
35254
+ if (summary.length === 0) throw new TopicSummaryRequiredError(topic);
35255
+ graph.topics[topic] = { summary };
35256
+ return true;
35257
+ }
34537
35258
  function skipsTriggerGates(input, options) {
34538
35259
  return options.allowNoTrigger === true || input.scope === "always";
34539
35260
  }
@@ -34549,10 +35270,30 @@ function assertTriggerInputs(input, options, existingTriggerCount) {
34549
35270
  if (broad !== void 0) throw new BroadCommandPatternError(broad);
34550
35271
  }
34551
35272
  }
34552
- function assertRecallable(graph, resultingTriggers) {
35273
+ function dropDeadCommandTriggers(graph, merged, options) {
35274
+ if (options.allowNoTrigger === true) return { ...merged, dropped: [] };
35275
+ const dropped = ineffectiveTriggers(graph, merged.triggerIds).filter(
35276
+ (t) => t.kind === "command_pattern"
35277
+ );
35278
+ const dead = new Set(dropped.map((t) => t.id));
35279
+ for (const id of merged.newTriggerIds) if (dead.has(id)) delete graph.triggers[id];
35280
+ return {
35281
+ triggerIds: merged.triggerIds.filter((id) => !dead.has(id)),
35282
+ newTriggerIds: merged.newTriggerIds.filter((id) => !dead.has(id)),
35283
+ dropped
35284
+ };
35285
+ }
35286
+ function deadCommandWarning(trigger) {
35287
+ return {
35288
+ code: "DEAD_COMMAND_PATTERN",
35289
+ message: `Dropped command trigger ${JSON.stringify(trigger.pattern)} (not saved): ${trigger.reason}.`
35290
+ };
35291
+ }
35292
+ function assertRecallable(graph, resultingTriggers, dropped) {
34553
35293
  const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
34554
- if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
34555
- throw new UnrecallableLessonError(blockingDead);
35294
+ const dead = [...dropped, ...blockingDead];
35295
+ if (dead.length > 0 && blockingDead.length === resultingTriggers.length) {
35296
+ throw new UnrecallableLessonError(dead);
34556
35297
  }
34557
35298
  }
34558
35299
 
@@ -34561,7 +35302,7 @@ var WIDE_GLOB_MATCH_COUNT = 40;
34561
35302
  var MAX_RECOMMENDED_TRIGGERS = 8;
34562
35303
  function isBroadGlob(pattern) {
34563
35304
  const p = pattern.trim();
34564
- if (p === "*" || p === "**") return true;
35305
+ if (p === "*" || p === "**" || isNegatedGlob(p)) return true;
34565
35306
  if (!p.includes("**")) return false;
34566
35307
  const basename78 = p.slice(p.lastIndexOf("/") + 1);
34567
35308
  return basename78.startsWith("*");
@@ -34605,12 +35346,19 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
34605
35346
  });
34606
35347
  }
34607
35348
  if (knownPaths !== void 0) {
34608
- const dead = deadFileGlobIds(graph, knownPaths);
34609
- const deadHere = lesson.triggers.filter((id) => dead.has(id)).map((id) => graph.triggers[id]?.pattern).filter((p) => p !== void 0);
35349
+ const { dead, pending } = fileGlobLiveness(graph, knownPaths, lesson.triggers);
35350
+ const deadHere = patternsIn(graph, lesson.triggers, dead);
34610
35351
  if (deadHere.length > 0) {
34611
35352
  warnings.push({
34612
35353
  code: "DEAD_GLOB",
34613
- message: `Lesson "${lessonId}" has file_glob trigger(s) (${deadHere.join(", ")}) that match no file in the working tree \u2014 likely a rename. Re-point them at the current path, or the lesson is unreachable via those globs.`
35354
+ message: `Lesson "${lessonId}" has file_glob trigger(s) (${deadHere.join(", ")}) that match no file, and git history shows the path was renamed or deleted \u2014 likely a rename. Re-point them at the current path, or the lesson is unreachable via those globs.`
35355
+ });
35356
+ }
35357
+ const pendingHere = patternsIn(graph, lesson.triggers, pending);
35358
+ if (pendingHere.length > 0) {
35359
+ warnings.push({
35360
+ code: "PENDING_GLOB",
35361
+ message: `Lesson "${lessonId}" has file_glob trigger(s) (${pendingHere.join(", ")}) whose path does not exist yet \u2014 the trigger will fire once it does, so it is kept. If the path is a typo, re-point it.`
34614
35362
  });
34615
35363
  }
34616
35364
  const wide = triggers.filter((t) => t.kind === "file_glob" && !isBroadGlob(t.pattern)).filter((t) => fileGlobMatchCount(t.pattern, knownPaths) > WIDE_GLOB_MATCH_COUNT).map((t) => t.pattern);
@@ -34623,18 +35371,21 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
34623
35371
  }
34624
35372
  return warnings;
34625
35373
  }
35374
+ function patternsIn(graph, ids, keep) {
35375
+ return ids.filter((id) => keep.has(id)).map((id) => graph.triggers[id]?.pattern).filter((p) => p !== void 0);
35376
+ }
34626
35377
 
34627
35378
  // src/lessons/capture-near-duplicate.ts
34628
35379
  var NEAR_DUPLICATE_THRESHOLD = 0.6;
34629
35380
  function nearDuplicateWarning(graph, lessonId) {
34630
35381
  const subject = graph.lessons[lessonId];
34631
35382
  if (subject === void 0) return null;
34632
- const subjectTokens = new Set(tokenize(subject.rule));
35383
+ const subjectTokens = new Set(tokenize2(subject.rule));
34633
35384
  if (subjectTokens.size === 0) return null;
34634
35385
  let best = null;
34635
35386
  for (const [id, other] of Object.entries(graph.lessons)) {
34636
35387
  if (id === lessonId || other.status !== "active") continue;
34637
- const otherTokens = new Set(tokenize(other.rule));
35388
+ const otherTokens = new Set(tokenize2(other.rule));
34638
35389
  if (otherTokens.size === 0) continue;
34639
35390
  const score = jaccard(subjectTokens, otherTokens);
34640
35391
  if (score >= NEAR_DUPLICATE_THRESHOLD && (best === null || score > best.score)) {
@@ -34652,63 +35403,375 @@ function jaccard(a, b) {
34652
35403
  for (const t of a) if (b.has(t)) intersection += 1;
34653
35404
  return intersection / (a.size + b.size - intersection);
34654
35405
  }
35406
+ var LEFTOVER = /\.(?:\d+\.tmp|[\w-]+\.stale)$/;
35407
+ var LEFTOVER_AGE_MS = 6e4;
35408
+ function sweepLessonsLeftovers(projectRoot, now = Date.now()) {
35409
+ const dir = lessonsPaths(projectRoot).base;
35410
+ let names;
35411
+ try {
35412
+ names = readdirSync(dir);
35413
+ } catch {
35414
+ return;
35415
+ }
35416
+ for (const name of names) {
35417
+ if (!LEFTOVER.test(name)) continue;
35418
+ const path = join(dir, name);
35419
+ try {
35420
+ if (now - statSync(path).mtimeMs > LEFTOVER_AGE_MS)
35421
+ rmSync(path, { recursive: true, force: true });
35422
+ } catch {
35423
+ }
35424
+ }
35425
+ }
34655
35426
 
34656
35427
  // src/utils/filesystem/process-lock.ts
34657
35428
  init_errors();
35429
+ var execFileAsync2 = promisify(execFile);
35430
+ var PS_TIMEOUT_MS = 2e3;
35431
+ var self;
35432
+ async function processIdentity(pid, platform = process.platform) {
35433
+ if (!Number.isInteger(pid) || pid <= 0 || platform === "win32") return null;
35434
+ try {
35435
+ return platform === "linux" ? await linuxIdentity(pid) : await psIdentity(pid);
35436
+ } catch {
35437
+ return null;
35438
+ }
35439
+ }
35440
+ function selfIdentity() {
35441
+ self ??= processIdentity(process.pid);
35442
+ return self;
35443
+ }
35444
+ function linuxStartIdentity(stat7, bootId) {
35445
+ const start = stat7.slice(stat7.lastIndexOf(")") + 2).split(" ")[19];
35446
+ const boot = bootId.trim();
35447
+ if (start === void 0 || !/^\d+$/.test(start) || boot === "") return null;
35448
+ return `${boot}:${start}`;
35449
+ }
35450
+ async function linuxIdentity(pid) {
35451
+ const [stat7, bootId] = await Promise.all([
35452
+ readFile(`/proc/${pid}/stat`, "utf-8"),
35453
+ readFile("/proc/sys/kernel/random/boot_id", "utf-8")
35454
+ ]);
35455
+ return linuxStartIdentity(stat7, bootId);
35456
+ }
35457
+ async function psIdentity(pid) {
35458
+ const { stdout } = await execFileAsync2("ps", ["-o", "lstart=", "-p", String(pid)], {
35459
+ env: { ...process.env, LC_ALL: "C", TZ: "UTC" },
35460
+ timeout: PS_TIMEOUT_MS
35461
+ });
35462
+ const start = stdout.trim();
35463
+ return start === "" ? null : start;
35464
+ }
35465
+ var TRANSIENT_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
35466
+ var ATTEMPTS = 5;
35467
+ var BASE_DELAY_MS = 25;
35468
+ new Int32Array(new SharedArrayBuffer(4));
35469
+ function isTransientFsError(err) {
35470
+ const code = err?.code;
35471
+ return typeof code === "string" && TRANSIENT_CODES.has(code);
35472
+ }
35473
+ async function retryTransient(op) {
35474
+ for (let attempt = 1; ; attempt++) {
35475
+ try {
35476
+ return await op();
35477
+ } catch (err) {
35478
+ if (!isTransientFsError(err) || attempt >= ATTEMPTS) throw err;
35479
+ await setTimeout(BASE_DELAY_MS * 2 ** (attempt - 1));
35480
+ }
35481
+ }
35482
+ }
35483
+
35484
+ // src/utils/filesystem/process-lock-ops.ts
35485
+ init_rename_retry();
35486
+ var HOLDER_FILE = "holder.json";
35487
+ var OWNER_PREFIX = "owner-";
35488
+ var YOUNG_LOCK_GRACE_MS = 2e3;
35489
+ var PID_REUSE_PROBE_AFTER_MS = 2e3;
35490
+ function ownerPath(lockPath, token) {
35491
+ return join(lockPath, `${OWNER_PREFIX}${token}`);
35492
+ }
35493
+ function holderPath(lockPath) {
35494
+ return join(lockPath, HOLDER_FILE);
35495
+ }
35496
+ function errorCode(err) {
35497
+ return err?.code;
35498
+ }
35499
+ var LockPathNotFolderError = class extends Error {
35500
+ constructor(lockPath) {
35501
+ super(
35502
+ `${lockPath.replaceAll("\\", "/")} is a file, but agentsmesh keeps its lock there as a folder. Delete it and run the command again.`
35503
+ );
35504
+ this.name = "LockPathNotFolderError";
35505
+ }
35506
+ };
35507
+ async function ownerTokens(dir) {
35508
+ try {
35509
+ const entries = await readdir(dir);
35510
+ return entries.filter((e) => e.startsWith(OWNER_PREFIX)).map((e) => e.slice(OWNER_PREFIX.length));
35511
+ } catch (err) {
35512
+ if (errorCode(err) === "ENOENT") return null;
35513
+ if (errorCode(err) === "ENOTDIR") throw new LockPathNotFolderError(dir);
35514
+ throw err;
35515
+ }
35516
+ }
35517
+ async function readHolderRaw(dir) {
35518
+ return readFile(holderPath(dir), "utf-8").catch(() => null);
35519
+ }
35520
+ function holderToken(raw) {
35521
+ return raw === null ? void 0 : parseMetadata(raw)?.token;
35522
+ }
35523
+ async function inspectLock(lockPath) {
35524
+ const tokens = await ownerTokens(lockPath);
35525
+ if (tokens === null) return { kind: "gone" };
35526
+ const raw = await readHolderRaw(lockPath);
35527
+ const meta = raw === null ? null : parseMetadata(raw);
35528
+ const [only] = tokens;
35529
+ if (meta && tokens.length === 1 && only !== void 0 && meta.token === only) {
35530
+ return { kind: "held", token: only, meta };
35531
+ }
35532
+ if (meta && raw !== null && tokens.length === 0 && meta.token === void 0) {
35533
+ return { kind: "legacy", meta, raw };
35534
+ }
35535
+ const age = await dirAgeMs(lockPath);
35536
+ if (age === null) return { kind: "gone" };
35537
+ const young = age < YOUNG_LOCK_GRACE_MS && age >= -3e5;
35538
+ return young ? { kind: "young" } : { kind: "orphan", tokens, raw };
35539
+ }
35540
+ async function isStale(meta, staleMs, cache4) {
35541
+ const age = Date.now() - meta.started;
35542
+ if (age > staleMs || age < -3e5) return true;
35543
+ if (meta.hostname && meta.hostname !== hostname()) return false;
35544
+ if (!isProcessAlive(meta.pid)) return true;
35545
+ if (meta.procStart === void 0 || age < PID_REUSE_PROBE_AFTER_MS) return false;
35546
+ return pidReused(meta.pid, meta.procStart, cache4);
35547
+ }
35548
+ function describeHolder(state) {
35549
+ if (state.kind !== "held" && state.kind !== "legacy") return "unknown (unreadable lock metadata)";
35550
+ const { meta } = state;
35551
+ const host = meta.hostname ? `${meta.hostname}:` : "";
35552
+ return `${host}pid ${meta.pid} (running ${Math.max(0, Date.now() - meta.started)}ms)`;
35553
+ }
35554
+ async function dirAgeMs(lockPath) {
35555
+ try {
35556
+ return Date.now() - (await stat(lockPath)).mtimeMs;
35557
+ } catch (err) {
35558
+ if (errorCode(err) === "ENOENT") return null;
35559
+ throw err;
35560
+ }
35561
+ }
35562
+ function pidReused(pid, recorded, cache4) {
35563
+ const key = `${pid}:${recorded}`;
35564
+ let verdict = cache4.get(key);
35565
+ if (!verdict) {
35566
+ verdict = processIdentity(pid).then((current) => current !== null && current !== recorded);
35567
+ cache4.set(key, verdict);
35568
+ }
35569
+ return verdict;
35570
+ }
35571
+ function isProcessAlive(pid) {
35572
+ if (!Number.isInteger(pid) || pid <= 0) return false;
35573
+ try {
35574
+ process.kill(pid, 0);
35575
+ return true;
35576
+ } catch (err) {
35577
+ return errorCode(err) === "EPERM";
35578
+ }
35579
+ }
35580
+ function parseMetadata(raw) {
35581
+ let value;
35582
+ try {
35583
+ value = JSON.parse(raw);
35584
+ } catch {
35585
+ return null;
35586
+ }
35587
+ if (typeof value !== "object" || value === null) return null;
35588
+ const v = value;
35589
+ if (typeof v.pid !== "number" || typeof v.started !== "number") return null;
35590
+ const optionalText = (x) => x === void 0 || typeof x === "string";
35591
+ const textOk = [v.hostname, v.token, v.procStart].every(optionalText);
35592
+ return textOk ? value : null;
35593
+ }
35594
+
35595
+ // src/utils/filesystem/process-lock-ops.ts
35596
+ async function tryAcquire(lockPath, meta) {
35597
+ try {
35598
+ await mkdir(lockPath);
35599
+ } catch (err) {
35600
+ if (errorCode(err) === "EEXIST") return false;
35601
+ throw err;
35602
+ }
35603
+ const owner = ownerPath(lockPath, meta.token);
35604
+ let writingHolder = false;
35605
+ try {
35606
+ await mkdir(owner);
35607
+ if ((await ownerTokens(lockPath))?.length !== 1) return await backOff(lockPath, owner);
35608
+ writingHolder = true;
35609
+ await writeFile(holderPath(lockPath), JSON.stringify(meta), { encoding: "utf-8", flag: "wx" });
35610
+ } catch (err) {
35611
+ const code = errorCode(err);
35612
+ if (code === "ENOENT" || code === "EEXIST") return backOff(lockPath, owner);
35613
+ if (writingHolder) await rm(holderPath(lockPath), { force: true }).catch(() => {
35614
+ });
35615
+ await backOff(lockPath, owner);
35616
+ throw err;
35617
+ }
35618
+ if (existsSync(owner)) return true;
35619
+ await teardown(lockPath, [meta.token]);
35620
+ return false;
35621
+ }
35622
+ function releaseOwnedSync(lockPath, token) {
35623
+ try {
35624
+ rmdirSync(ownerPath(lockPath, token));
35625
+ } catch {
35626
+ return;
35627
+ }
35628
+ try {
35629
+ if (holderToken(readFileSync(holderPath(lockPath), "utf-8")) === token) {
35630
+ unlinkSync(holderPath(lockPath));
35631
+ }
35632
+ } catch {
35633
+ }
35634
+ try {
35635
+ rmdirSync(lockPath);
35636
+ } catch {
35637
+ }
35638
+ }
35639
+ async function evict(lockPath, state) {
35640
+ if (state.kind === "held") return evictOwners(lockPath, [state.token]);
35641
+ if (state.kind === "orphan" && state.tokens.length > 0) {
35642
+ return evictOwners(lockPath, state.tokens);
35643
+ }
35644
+ if (state.kind === "legacy" || state.kind === "orphan") return dropUnowned(lockPath, state.raw);
35645
+ }
35646
+ async function evictOwners(lockPath, tokens) {
35647
+ const removed = [];
35648
+ for (const token of tokens) {
35649
+ if (await removeOwner(lockPath, token)) removed.push(token);
35650
+ }
35651
+ if (removed.length > 0) await teardown(lockPath, removed);
35652
+ }
35653
+ async function removeOwner(lockPath, token) {
35654
+ try {
35655
+ await retryTransient(() => rmdir(ownerPath(lockPath, token)));
35656
+ return true;
35657
+ } catch (err) {
35658
+ if (errorCode(err) === "ENOENT") return false;
35659
+ throw err;
35660
+ }
35661
+ }
35662
+ async function teardown(lockPath, tokens) {
35663
+ const token = holderToken(await readHolderRaw(lockPath));
35664
+ if (token !== void 0 && tokens.includes(token)) {
35665
+ await unlink(holderPath(lockPath)).catch(() => {
35666
+ });
35667
+ }
35668
+ await rmdir(lockPath).catch(() => {
35669
+ });
35670
+ }
35671
+ async function dropUnowned(lockPath, judgedRaw) {
35672
+ const aside = `${lockPath}.${randomUUID()}.stale`;
35673
+ try {
35674
+ await renameWithRetry(lockPath, aside);
35675
+ } catch (err) {
35676
+ if (errorCode(err) === "ENOENT") return;
35677
+ throw err;
35678
+ }
35679
+ const owners = await ownerTokens(aside);
35680
+ if (owners?.length !== 0 || await readHolderRaw(aside) !== judgedRaw) {
35681
+ return putBack(aside, lockPath);
35682
+ }
35683
+ try {
35684
+ await rm(aside, { recursive: true, force: true });
35685
+ } catch (err) {
35686
+ await putBack(aside, lockPath);
35687
+ throw err;
35688
+ }
35689
+ }
35690
+ async function putBack(aside, lockPath) {
35691
+ await rename(aside, lockPath).catch(() => {
35692
+ });
35693
+ }
35694
+ async function backOff(lockPath, owner) {
35695
+ await rmdir(owner).catch(() => {
35696
+ });
35697
+ await rmdir(lockPath).catch(() => {
35698
+ });
35699
+ return false;
35700
+ }
35701
+
35702
+ // src/utils/filesystem/process-lock.ts
34658
35703
  var DEFAULT_STALE_MS = 6 * 60 * 60 * 1e3;
34659
35704
  var DEFAULT_RETRIES = 30;
34660
35705
  var DEFAULT_RETRY_DELAY_MS = 200;
34661
- var YOUNG_LOCK_GRACE_MS = 2e3;
35706
+ var MAX_IMMEDIATE_RETRIES = 100;
35707
+ var MAX_TRANSIENT_ERRORS = 5;
34662
35708
  async function acquireProcessLock(lockPath, opts = {}) {
34663
35709
  const retries = opts.retries ?? DEFAULT_RETRIES;
34664
- const delay = opts.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
34665
- const stale = opts.staleMs ?? DEFAULT_STALE_MS;
35710
+ const staleMs = opts.staleMs ?? DEFAULT_STALE_MS;
34666
35711
  await mkdir(dirname(lockPath), { recursive: true });
35712
+ const procStart = await selfIdentity();
35713
+ const probes = /* @__PURE__ */ new Map();
34667
35714
  let attempt = 0;
35715
+ let immediate = 0;
35716
+ const waitingSince = Date.now();
35717
+ let noticed = false;
35718
+ let transient = 0;
34668
35719
  while (true) {
34669
- const acquired = await tryAcquire(lockPath);
34670
- if (acquired) return acquired;
34671
- const existing = await inspectLock(lockPath);
34672
- if (existing !== "young" && isStale(existing, stale)) {
34673
- await rm(lockPath, { recursive: true, force: true });
35720
+ let state;
35721
+ try {
35722
+ const holder = newHolder(procStart);
35723
+ if (await tryAcquire(lockPath, holder)) return holdLock(lockPath, holder.token);
35724
+ state = await inspectLock(lockPath);
35725
+ transient = 0;
35726
+ } catch (err) {
35727
+ if (!isTransientFsError(err) || ++transient >= MAX_TRANSIENT_ERRORS) throw err;
35728
+ await setTimeout(lockRetryDelayMs(transient, opts));
35729
+ continue;
35730
+ }
35731
+ if (immediate < MAX_IMMEDIATE_RETRIES && await clearedNow(lockPath, state, staleMs, probes)) {
35732
+ immediate++;
34674
35733
  continue;
34675
35734
  }
34676
35735
  if (attempt >= retries) {
34677
- const holder = existing === "young" ? null : existing;
34678
- throw new LockAcquisitionError(lockPath, describeHolder(holder), { label: opts.label });
35736
+ throw new LockAcquisitionError(lockPath, describeHolder(state), { label: opts.label });
34679
35737
  }
34680
35738
  attempt++;
34681
- await setTimeout(delay);
35739
+ immediate = 0;
35740
+ if (!noticed && opts.onWait && Date.now() - waitingSince >= (opts.waitNoticeMs ?? 2e3)) {
35741
+ noticed = true;
35742
+ opts.onWait(describeHolder(state));
35743
+ }
35744
+ await setTimeout(lockRetryDelayMs(attempt, opts));
34682
35745
  }
34683
35746
  }
34684
- async function tryAcquire(lockPath) {
34685
- try {
34686
- await mkdir(lockPath, { recursive: false });
34687
- } catch (err) {
34688
- if (err.code === "EEXIST") return null;
34689
- throw err;
34690
- }
34691
- const metadataPath = join(lockPath, "holder.json");
34692
- const metadata = {
35747
+ function lockRetryDelayMs(attempt, opts, random = Math.random) {
35748
+ const base = opts.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
35749
+ const cap2 = Math.max(base, opts.maxRetryDelayMs ?? base);
35750
+ const delay = Math.min(cap2, base * 2 ** (attempt - 1));
35751
+ return opts.jitter ? delay * (0.5 + random() * 0.5) : delay;
35752
+ }
35753
+ async function clearedNow(lockPath, state, staleMs, probes) {
35754
+ if (state.kind === "gone") return true;
35755
+ if (state.kind === "young") return false;
35756
+ if (state.kind !== "orphan" && !await isStale(state.meta, staleMs, probes)) return false;
35757
+ await evict(lockPath, state);
35758
+ return true;
35759
+ }
35760
+ function newHolder(procStart) {
35761
+ return {
34693
35762
  pid: process.pid,
34694
35763
  started: Date.now(),
34695
- hostname: getHostname()
35764
+ hostname: hostname(),
35765
+ token: randomUUID(),
35766
+ ...procStart === null ? {} : { procStart }
34696
35767
  };
34697
- try {
34698
- await writeFile(metadataPath, JSON.stringify(metadata), "utf-8");
34699
- } catch (error) {
34700
- await rm(lockPath, { recursive: true, force: true }).catch(() => {
34701
- });
34702
- throw error;
34703
- }
35768
+ }
35769
+ function holdLock(lockPath, token) {
34704
35770
  let released = false;
34705
35771
  const cleanup = () => {
34706
35772
  if (released) return;
34707
35773
  released = true;
34708
- try {
34709
- rmSync(lockPath, { recursive: true, force: true });
34710
- } catch {
34711
- }
35774
+ releaseOwnedSync(lockPath, token);
34712
35775
  };
34713
35776
  const signalHandler = (signal) => {
34714
35777
  cleanup();
@@ -34717,76 +35780,69 @@ async function tryAcquire(lockPath) {
34717
35780
  process.once("SIGINT", signalHandler);
34718
35781
  process.once("SIGTERM", signalHandler);
34719
35782
  process.once("exit", cleanup);
34720
- return async () => {
35783
+ const release = async () => {
34721
35784
  if (released) return;
34722
35785
  released = true;
34723
35786
  process.off("SIGINT", signalHandler);
34724
35787
  process.off("SIGTERM", signalHandler);
34725
35788
  process.off("exit", cleanup);
34726
- await rm(lockPath, { recursive: true, force: true }).catch(() => {
35789
+ await evictOwners(lockPath, [token]).catch(() => {
34727
35790
  });
34728
35791
  };
34729
- }
34730
- async function inspectLock(lockPath) {
34731
- try {
34732
- const raw = await readFile(join(lockPath, "holder.json"), "utf-8");
34733
- const parsed = JSON.parse(raw);
34734
- if (!isLockMetadata(parsed)) return null;
34735
- return parsed;
34736
- } catch {
34737
- try {
34738
- const info = await stat(lockPath);
34739
- const ageMs = Date.now() - info.mtimeMs;
34740
- if (ageMs < YOUNG_LOCK_GRACE_MS) return "young";
34741
- } catch {
34742
- }
34743
- return null;
34744
- }
34745
- }
34746
- function isStale(meta, staleMs) {
34747
- if (!meta) return true;
34748
- const sameHost = !meta.hostname || meta.hostname === getHostname();
34749
- if (sameHost && !isProcessAlive(meta.pid)) return true;
34750
- return Date.now() - meta.started > staleMs;
34751
- }
34752
- function isProcessAlive(pid) {
34753
- if (!Number.isInteger(pid) || pid <= 0) return false;
34754
- try {
34755
- process.kill(pid, 0);
34756
- return true;
34757
- } catch (err) {
34758
- return err.code === "EPERM";
34759
- }
34760
- }
34761
- function describeHolder(meta) {
34762
- if (!meta) return "unknown (unreadable lock metadata)";
34763
- const host = meta.hostname ? `${meta.hostname}:` : "";
34764
- return `${host}pid ${meta.pid} (running ${Date.now() - meta.started}ms)`;
34765
- }
34766
- function isLockMetadata(value) {
34767
- if (typeof value !== "object" || value === null) return false;
34768
- const v = value;
34769
- return typeof v.pid === "number" && typeof v.started === "number";
34770
- }
34771
- function getHostname() {
34772
- return hostname();
35792
+ const isHeld = async () => !released && existsSync(ownerPath(lockPath, token));
35793
+ return Object.assign(release, { isHeld });
34773
35794
  }
34774
35795
 
34775
35796
  // src/lessons/lessons-lock.ts
34776
35797
  var LESSONS_LOCK_FILENAME = ".lessons.lock";
35798
+ var LESSONS_LOCK_OPTIONS = Object.freeze({
35799
+ retries: 500,
35800
+ retryDelayMs: 25,
35801
+ maxRetryDelayMs: 250,
35802
+ jitter: true,
35803
+ staleMs: 6e4
35804
+ });
34777
35805
  function lessonsLockPath(projectRoot) {
34778
35806
  return resolve(projectRoot, ".agentsmesh/lessons", LESSONS_LOCK_FILENAME);
34779
35807
  }
34780
35808
  async function acquireLessonsLock(projectRoot, opts = {}) {
34781
- const lockPath = lessonsLockPath(projectRoot);
34782
- await mkdir(dirname(lockPath), { recursive: true });
34783
- return acquireProcessLock(lockPath, { ...opts, label: "lessons lock" });
35809
+ return acquireProcessLock(lessonsLockPath(projectRoot), {
35810
+ retries: opts.retries ?? LESSONS_LOCK_OPTIONS.retries,
35811
+ retryDelayMs: opts.retryDelayMs ?? LESSONS_LOCK_OPTIONS.retryDelayMs,
35812
+ maxRetryDelayMs: opts.maxRetryDelayMs ?? LESSONS_LOCK_OPTIONS.maxRetryDelayMs,
35813
+ jitter: opts.jitter ?? LESSONS_LOCK_OPTIONS.jitter,
35814
+ staleMs: opts.staleMs ?? LESSONS_LOCK_OPTIONS.staleMs,
35815
+ label: "lessons lock",
35816
+ waitNoticeMs: opts.waitNoticeMs,
35817
+ onWait: opts.onWait ?? ((holder) => logger.warn(
35818
+ `Waiting for the lessons lock, held by ${holder}; a lock older than ${LESSONS_LOCK_OPTIONS.staleMs / 1e3} s is taken over.`
35819
+ ))
35820
+ });
35821
+ }
35822
+ var LessonsLockLostError = class extends Error {
35823
+ constructor() {
35824
+ super(
35825
+ `lost the lessons lock while writing (the process was paused longer than the ${LESSONS_LOCK_OPTIONS.staleMs / 1e3} s stale window?); nothing was saved \u2014 retry the command`
35826
+ );
35827
+ this.name = "LessonsLockLostError";
35828
+ }
35829
+ };
35830
+ async function assertLessonsLockHeld(lock) {
35831
+ if (!await lock.isHeld()) throw new LessonsLockLostError();
34784
35832
  }
34785
35833
 
34786
35834
  // src/lessons/mutate.ts
34787
- function emptyGraph() {
34788
- return { version: CURRENT_GRAPH_VERSION, lessons: {}, topics: {}, triggers: {} };
34789
- }
35835
+ var LessonsWriteRefusedError = class extends Error {
35836
+ findings;
35837
+ constructor(findings) {
35838
+ const errors = findings.map((f) => `${f.code}: ${f.message.replace(/[.\s]+$/, "")}`).join("; ");
35839
+ super(
35840
+ `Refused to save the lessons graph: this change would add ${errors}. Nothing was written.`
35841
+ );
35842
+ this.name = "LessonsWriteRefusedError";
35843
+ this.findings = findings;
35844
+ }
35845
+ };
34790
35846
  function findingKey(f) {
34791
35847
  return `${f.code}|${f.triggerId ?? ""}|${f.lessonId ?? ""}`;
34792
35848
  }
@@ -34796,6 +35852,7 @@ function errorSignatures(report) {
34796
35852
  async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
34797
35853
  const release = await acquireLessonsLock(projectRoot, { retries: options.retries });
34798
35854
  try {
35855
+ sweepLessonsLeftovers(projectRoot);
34799
35856
  const graph = tryLoadLessonsGraph(projectRoot) ?? emptyGraph();
34800
35857
  const baseline = errorSignatures(validateLessonsGraph(graph));
34801
35858
  const result2 = await mutator(graph);
@@ -34804,12 +35861,10 @@ async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
34804
35861
  (f) => f.level === "error" && !baseline.has(findingKey(f))
34805
35862
  );
34806
35863
  if (introduced.length > 0) {
34807
- const errors = introduced.map((f) => `${f.code}: ${f.message}`).join("; ");
34808
- throw new Error(
34809
- `mutateLessonsGraph: refusing to write \u2014 this change introduces ${errors}. (Pre-existing graph issues are not blocking; run \`agentsmesh lessons validate\` to review and \`lessons untrigger\`/\`prune\` to repair them.)`
34810
- );
35864
+ throw new LessonsWriteRefusedError(introduced);
34811
35865
  }
34812
35866
  graph.version = CURRENT_GRAPH_VERSION;
35867
+ await assertLessonsLockHeld(release);
34813
35868
  saveLessonsGraph(projectRoot, graph);
34814
35869
  return result2;
34815
35870
  } finally {
@@ -34823,50 +35878,41 @@ async function mutateLessonsGraph(projectRoot, mutator, options = {}) {
34823
35878
 
34824
35879
  // src/lessons/add.ts
34825
35880
  async function addLesson(projectRoot, input, options = {}) {
34826
- return mutateLessonsGraph(projectRoot, (graph) => addLessonInto(graph, input, options), {
34827
- retries: options.retries
34828
- });
35881
+ return mutateLessonsGraph(
35882
+ projectRoot,
35883
+ (graph) => addLessonInto(graph, input, { ...options, projectRoot }),
35884
+ { retries: options.retries }
35885
+ );
34829
35886
  }
34830
35887
  function addLessonInto(graph, input, options) {
34831
35888
  const ruleKey2 = normalizeRule(input.rule);
34832
35889
  const trimmedRule = assertRuleShape(input.rule);
34833
35890
  const existingId = findExistingLessonByRule(graph, ruleKey2);
34834
- const isNewTopic = graph.topics[input.topic] === void 0;
34835
- if (isNewTopic) {
34836
- if (options.allowNewTopic !== true) throw new UnknownTopicError(input.topic);
34837
- if (options.topicSummary === void 0 || options.topicSummary.length === 0) {
34838
- throw new Error(`addLesson: new topic "${input.topic}" requires topicSummary.`);
34839
- }
34840
- graph.topics[input.topic] = { summary: options.topicSummary };
34841
- }
35891
+ const isNewTopic = ensureTopic(graph, input.topic, options);
34842
35892
  const existing = existingId !== null ? graph.lessons[existingId] : void 0;
34843
35893
  assertTriggerInputs(input, options, existing?.triggers.length ?? 0);
34844
- const { triggerIds, newTriggerIds } = mergeTriggers(graph, input.triggers);
35894
+ const merged = mergeTriggers(graph, input.triggers, options.projectRoot);
35895
+ const { triggerIds, newTriggerIds, dropped } = dropDeadCommandTriggers(graph, merged, options);
34845
35896
  if (!skipsTriggerGates(input, options)) {
34846
- assertRecallable(
34847
- graph,
34848
- existing === void 0 ? triggerIds : union(existing.triggers, triggerIds)
34849
- );
35897
+ const resulting = existing === void 0 ? triggerIds : union(existing.triggers, triggerIds);
35898
+ assertRecallable(graph, resulting, dropped);
34850
35899
  }
34851
- if (existingId !== null) {
34852
- const existing2 = graph.lessons[existingId];
34853
- graph.lessons[existingId] = {
34854
- ...existing2,
34855
- topics: union(existing2.topics, [input.topic]),
34856
- triggers: union(existing2.triggers, triggerIds),
34857
- evidence: union(existing2.evidence, input.evidence ?? []),
34858
- ...existing2.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
34859
- // Re-capturing a rule with --scope always promotes it to always-on.
34860
- ...input.scope === "always" ? { scope: "always" } : {}
34861
- };
35900
+ const droppedWarnings = dropped.map(deadCommandWarning);
35901
+ if (existingId !== null && existing !== void 0) {
35902
+ const updated = upsertLesson(existing, input, triggerIds);
35903
+ graph.lessons[existingId] = updated;
34862
35904
  return {
34863
35905
  id: existingId,
34864
35906
  isNewLesson: false,
34865
35907
  isNewTopic,
34866
35908
  newTriggerIds,
35909
+ changes: describeUpsert(existing, updated),
34867
35910
  // Near-duplicate detection is meaningless on an upsert (the lesson IS the
34868
35911
  // match), so only DEAD_GLOB/hygiene warnings apply here.
34869
- warnings: inspectCapturedLesson(graph, existingId, options.knownPaths)
35912
+ warnings: [
35913
+ ...inspectCapturedLesson(graph, existingId, options.knownPaths),
35914
+ ...droppedWarnings
35915
+ ]
34870
35916
  };
34871
35917
  }
34872
35918
  const id = makeLessonId(graph, input.topic, ruleKey2);
@@ -34874,29 +35920,26 @@ function addLessonInto(graph, input, options) {
34874
35920
  rule: trimmedRule,
34875
35921
  topics: [input.topic],
34876
35922
  triggers: triggerIds,
34877
- evidence: input.evidence === void 0 ? [] : [...input.evidence],
35923
+ evidence: [...new Set(input.evidence ?? [])],
34878
35924
  status: "active",
34879
35925
  createdAt: input.createdAt ?? todayIso(),
34880
35926
  ...input.rationale === void 0 ? {} : { rationale: input.rationale },
34881
35927
  ...input.scope === "always" ? { scope: "always" } : {}
34882
35928
  };
34883
- const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
34884
35929
  const nearDup = nearDuplicateWarning(graph, id);
34885
35930
  return {
34886
35931
  id,
34887
35932
  isNewLesson: true,
34888
35933
  isNewTopic,
34889
35934
  newTriggerIds,
34890
- warnings: nearDup === null ? warnings : [...warnings, nearDup]
35935
+ changes: [],
35936
+ warnings: [
35937
+ ...inspectCapturedLesson(graph, id, options.knownPaths),
35938
+ ...nearDup === null ? [] : [nearDup],
35939
+ ...droppedWarnings
35940
+ ]
34891
35941
  };
34892
35942
  }
34893
- function findExistingLessonByRule(graph, ruleKey2) {
34894
- for (const [id, lesson] of Object.entries(graph.lessons)) {
34895
- if (lesson.status !== "active") continue;
34896
- if (normalizeRule(lesson.rule) === ruleKey2) return id;
34897
- }
34898
- return null;
34899
- }
34900
35943
 
34901
35944
  // src/lessons/import-legacy-merge.ts
34902
35945
  async function mergeLegacy(projectRoot, paths2, specs, summaryByTopic, options) {
@@ -34929,19 +35972,33 @@ async function mergeLegacy(projectRoot, paths2, specs, summaryByTopic, options)
34929
35972
  triggerCount: addedTriggers.size
34930
35973
  };
34931
35974
  }
34932
-
34933
- // src/lessons/import-legacy.ts
34934
- var LessonsGraphExistsError = class extends Error {
34935
- code = "LESSONS_GRAPH_EXISTS";
34936
- constructor() {
34937
- super("importLegacyLessons: a non-empty lessons.json already exists; pass force to overwrite.");
34938
- this.name = "LessonsGraphExistsError";
35975
+ var LESSONS_DIR = ".agentsmesh/lessons";
35976
+ var LegacyTopicPathError = class extends Error {
35977
+ code = "LEGACY_TOPIC_PATH_OUTSIDE";
35978
+ constructor(file) {
35979
+ super(
35980
+ `Legacy topic file path is outside .agentsmesh/lessons/: ${file}. Refusing to migrate (legacy artifacts left intact).`
35981
+ );
35982
+ this.name = "LegacyTopicPathError";
34939
35983
  }
34940
35984
  };
34941
- async function importLegacyLessons(projectRoot, options) {
34942
- const paths2 = lessonsPaths(projectRoot);
34943
- const indexRaw = readFileSync(paths2.index, "utf8");
34944
- const index = LegacyIndexSchema.parse(parse(indexRaw));
35985
+ async function resolveLegacyTopicPath(projectRoot, file) {
35986
+ const forward = file.replaceAll("\\", "/");
35987
+ const normalized = posix.normalize(forward);
35988
+ const relative27 = !/^[A-Za-z]:/.test(forward) && !forward.startsWith("/") && normalized.startsWith(`${LESSONS_DIR}/`);
35989
+ if (!relative27) throw new LegacyTopicPathError(file);
35990
+ const target34 = join(projectRoot, normalized);
35991
+ try {
35992
+ await assertPathInsideRoot(join(projectRoot, LESSONS_DIR), target34);
35993
+ } catch {
35994
+ throw new LegacyTopicPathError(file);
35995
+ }
35996
+ return target34;
35997
+ }
35998
+ async function readLegacySource(projectRoot, migratedAt) {
35999
+ const index = LegacyIndexSchema.parse(
36000
+ parse(readFileSync(lessonsPaths(projectRoot).index, "utf8"))
36001
+ );
34945
36002
  const topics = {};
34946
36003
  const triggersById = /* @__PURE__ */ new Map();
34947
36004
  const triggerIdByKey = /* @__PURE__ */ new Map();
@@ -34952,14 +36009,15 @@ async function importLegacyLessons(projectRoot, options) {
34952
36009
  topics[cluster.topic] = { summary: cluster.summary };
34953
36010
  summaryByTopic.set(cluster.topic, cluster.summary);
34954
36011
  const clusterTriggerIds = collectClusterTriggerIds(cluster, triggersById, triggerIdByKey);
34955
- const topicFile = join(projectRoot, cluster.file);
36012
+ const topicFile = await resolveLegacyTopicPath(projectRoot, cluster.file);
34956
36013
  if (!existsSync(topicFile)) {
34957
36014
  throw new Error(
34958
- `importLegacyLessons: declared topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
36015
+ `Legacy topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
34959
36016
  );
34960
36017
  }
34961
- const topicMarkdown = readFileSync(topicFile, "utf8");
34962
- for (const { index: ruleIndex, body, evidence } of parseRulesSection(topicMarkdown)) {
36018
+ for (const { index: ruleIndex, body, evidence } of parseRulesSection(
36019
+ readFileSync(topicFile, "utf8")
36020
+ )) {
34963
36021
  const lessonEvidence = [
34964
36022
  `legacy:${cluster.file}#rule-${ruleIndex}`,
34965
36023
  ...evidence.map((e) => `legacy:${e}`)
@@ -34970,7 +36028,7 @@ async function importLegacyLessons(projectRoot, options) {
34970
36028
  triggers: clusterTriggerIds,
34971
36029
  evidence: lessonEvidence,
34972
36030
  status: "active",
34973
- createdAt: options.migratedAt
36031
+ createdAt: migratedAt
34974
36032
  };
34975
36033
  specs.push({
34976
36034
  rule: body,
@@ -34981,22 +36039,43 @@ async function importLegacyLessons(projectRoot, options) {
34981
36039
  keywords: cluster.triggers.keywords
34982
36040
  },
34983
36041
  evidence: lessonEvidence,
34984
- createdAt: options.migratedAt
36042
+ createdAt: migratedAt
34985
36043
  });
34986
36044
  }
34987
36045
  }
34988
- if (options.merge === true)
36046
+ return { topics, triggers: Object.fromEntries(triggersById), lessons, specs, summaryByTopic };
36047
+ }
36048
+
36049
+ // src/lessons/import-legacy.ts
36050
+ var LessonsGraphExistsError = class extends Error {
36051
+ code = "LESSONS_GRAPH_EXISTS";
36052
+ constructor() {
36053
+ super(
36054
+ "A non-empty lessons.json already exists. Pass --force to overwrite it, or --merge to add the legacy lessons to it."
36055
+ );
36056
+ this.name = "LessonsGraphExistsError";
36057
+ }
36058
+ };
36059
+ async function importLegacyLessons(projectRoot, options) {
36060
+ const paths2 = lessonsPaths(projectRoot);
36061
+ if (options.merge === true) {
36062
+ const { specs, summaryByTopic } = await readLegacySource(projectRoot, options.migratedAt);
34989
36063
  return mergeLegacy(projectRoot, paths2, specs, summaryByTopic, options);
34990
- const triggers = Object.fromEntries(triggersById.entries());
34991
- await mutateLessonsGraphLocked(projectRoot, (g) => {
36064
+ }
36065
+ const { topics, lessons, triggers } = await mutateLessonsGraphLocked(projectRoot, async (g) => {
36066
+ if (options.requireAbsentGraph === true && existsSync(paths2.graph)) {
36067
+ throw new LessonsGraphExistsError();
36068
+ }
34992
36069
  const populated = Object.keys(g.lessons).length > 0 || Object.keys(g.topics).length > 0 || Object.keys(g.triggers).length > 0;
34993
36070
  if (options.force !== true && populated) {
34994
36071
  throw new LessonsGraphExistsError();
34995
36072
  }
36073
+ const source = await readLegacySource(projectRoot, options.migratedAt);
34996
36074
  g.version = CURRENT_GRAPH_VERSION;
34997
- g.lessons = lessons;
34998
- g.topics = topics;
34999
- g.triggers = triggers;
36075
+ g.lessons = source.lessons;
36076
+ g.topics = source.topics;
36077
+ g.triggers = source.triggers;
36078
+ return source;
35000
36079
  });
35001
36080
  const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths2.base);
35002
36081
  return {
@@ -35004,7 +36083,7 @@ async function importLegacyLessons(projectRoot, options) {
35004
36083
  deletedPaths,
35005
36084
  topicCount: Object.keys(topics).length,
35006
36085
  lessonCount: Object.keys(lessons).length,
35007
- triggerCount: triggersById.size
36086
+ triggerCount: Object.keys(triggers).length
35008
36087
  };
35009
36088
  }
35010
36089
 
@@ -35014,13 +36093,14 @@ async function maybeAutoMigrateLessons(projectRoot) {
35014
36093
  const paths2 = lessonsPaths(projectRoot);
35015
36094
  if (!existsSync(paths2.index)) return false;
35016
36095
  try {
35017
- await importLegacyLessons(projectRoot, { migratedAt: todayIso() });
36096
+ await importLegacyLessons(projectRoot, { migratedAt: todayIso(), requireAbsentGraph: true });
35018
36097
  return true;
35019
36098
  } catch (err) {
35020
36099
  if (err instanceof LessonsGraphExistsError) return false;
35021
36100
  throw err;
35022
36101
  }
35023
36102
  }
36103
+ new Int32Array(new SharedArrayBuffer(4));
35024
36104
 
35025
36105
  // src/lessons/keyword-match.ts
35026
36106
  function deriveHaystackTokens(query) {
@@ -35054,7 +36134,7 @@ function containsRun(needle, hay) {
35054
36134
  return false;
35055
36135
  }
35056
36136
  function keywordMatches(pattern, query) {
35057
- const needle = tokenize(pattern);
36137
+ const needle = tokenize2(pattern);
35058
36138
  if (query.keyword !== void 0 && containsRun(needle, splitTokens(query.keyword.toLowerCase()))) {
35059
36139
  return true;
35060
36140
  }
@@ -35063,6 +36143,7 @@ function keywordMatches(pattern, query) {
35063
36143
 
35064
36144
  // src/lessons/query.ts
35065
36145
  var COMMAND_MATCH_BUDGET = 5e6;
36146
+ var GLOB_MATCH_BUDGET = 2e6;
35066
36147
  function queryLessons(graph, query) {
35067
36148
  if (query.file === void 0 && query.command === void 0 && query.keyword === void 0) {
35068
36149
  return [];
@@ -35085,9 +36166,12 @@ function collectMatchedTriggersByKind(graph, query) {
35085
36166
  command_pattern: /* @__PURE__ */ new Set(),
35086
36167
  keyword: /* @__PURE__ */ new Set()
35087
36168
  };
35088
- const budget = { remaining: COMMAND_MATCH_BUDGET };
36169
+ const budgets = {
36170
+ command: { remaining: COMMAND_MATCH_BUDGET },
36171
+ glob: { remaining: GLOB_MATCH_BUDGET }
36172
+ };
35089
36173
  for (const [id, trigger] of Object.entries(graph.triggers)) {
35090
- if (triggerMatches(trigger, query, budget)) byKind[trigger.kind].add(id);
36174
+ if (triggerMatches(trigger, query, budgets)) byKind[trigger.kind].add(id);
35091
36175
  }
35092
36176
  return byKind;
35093
36177
  }
@@ -35095,21 +36179,29 @@ function collectMatchedTriggerIds(graph, query) {
35095
36179
  const { file_glob, command_pattern, keyword } = collectMatchedTriggersByKind(graph, query);
35096
36180
  return /* @__PURE__ */ new Set([...file_glob, ...command_pattern, ...keyword]);
35097
36181
  }
35098
- function triggerMatches(trigger, query, budget) {
36182
+ function triggerMatches(trigger, query, budgets) {
35099
36183
  switch (trigger.kind) {
35100
- case "file_glob":
36184
+ case "file_glob": {
35101
36185
  if (query.file === void 0) return false;
35102
- return picomatch2(trigger.pattern, { dot: true })(query.file);
36186
+ const matcher = getGlobMatcher(trigger.pattern);
36187
+ return matcher !== null && matcher.test(query.file, budgets.glob);
36188
+ }
35103
36189
  case "command_pattern": {
35104
36190
  if (query.command === void 0) return false;
35105
36191
  const matcher = getCommandMatcher(trigger.pattern);
35106
- return matcher !== null && matcher.test(query.command, budget);
36192
+ return matcher !== null && matcher.test(query.command, budgets.command);
35107
36193
  }
35108
36194
  case "keyword":
35109
36195
  return keywordMatches(trigger.pattern, query);
35110
36196
  }
35111
36197
  }
35112
36198
 
36199
+ // src/lessons/telemetry.ts
36200
+ init_fs_text_encoding();
36201
+
36202
+ // src/lessons/log-record-guards.ts
36203
+ init_guards();
36204
+
35113
36205
  // src/lessons/telemetry.ts
35114
36206
  function recallLogPath(projectRoot) {
35115
36207
  return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
@@ -35246,14 +36338,20 @@ function applyCaps(ranked, options) {
35246
36338
  }
35247
36339
  return out2;
35248
36340
  }
36341
+
36342
+ // src/lessons/recall-config.ts
36343
+ init_fs_text_encoding();
35249
36344
  function defaultLessonsConfig() {
35250
36345
  return {
35251
36346
  recallLimit: DEFAULT_RECALL_LIMIT,
35252
36347
  recallMaxTokens: DEFAULT_RECALL_MAX_TOKENS,
35253
36348
  autoPrune: false,
35254
- telemetry: false
36349
+ telemetry: false,
36350
+ outcomeLog: true
35255
36351
  };
35256
36352
  }
36353
+
36354
+ // src/lessons/outcome-log.ts
35257
36355
  function outcomeLogPath(projectRoot) {
35258
36356
  return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
35259
36357
  }
@@ -35269,19 +36367,17 @@ async function mergeLessons(projectRoot, loserId, keeperId, options = {}) {
35269
36367
  }
35270
36368
  function mergeInto(graph, loserId, keeperId) {
35271
36369
  if (loserId === keeperId) {
35272
- throw new Error(`mergeLessons: cannot merge lesson "${loserId}" into itself.`);
36370
+ throw new Error(`Cannot merge lesson "${loserId}" into itself.`);
35273
36371
  }
35274
36372
  const loser = graph.lessons[loserId];
35275
- if (loser === void 0) throw new Error(`mergeLessons: unknown lesson "${loserId}".`);
36373
+ if (loser === void 0) throw new Error(`Unknown lesson "${loserId}".`);
35276
36374
  const keeper = graph.lessons[keeperId];
35277
- if (keeper === void 0) throw new Error(`mergeLessons: unknown lesson "${keeperId}".`);
36375
+ if (keeper === void 0) throw new Error(`Unknown lesson "${keeperId}".`);
35278
36376
  if (keeper.status !== "active") {
35279
- throw new Error(`mergeLessons: keeper "${keeperId}" is not active (status: ${keeper.status}).`);
36377
+ throw new Error(`Keeper "${keeperId}" is not active (status: ${keeper.status}).`);
35280
36378
  }
35281
36379
  if (loser.status !== "active") {
35282
- throw new Error(
35283
- `mergeLessons: loser "${loserId}" is already ${loser.status}; nothing to merge.`
35284
- );
36380
+ throw new Error(`Loser "${loserId}" is already ${loser.status}; nothing to merge.`);
35285
36381
  }
35286
36382
  graph.lessons[keeperId] = {
35287
36383
  ...keeper,
@@ -35336,9 +36432,123 @@ async function stripMarkersInGraph(projectRoot, options = {}) {
35336
36432
  }
35337
36433
  init_recall_hook_scaffold();
35338
36434
 
36435
+ // src/lessons/merge-driver-setup.ts
36436
+ init_cli_invocation();
36437
+ function commandProgram(command) {
36438
+ const m = /^\s*(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(command);
36439
+ return m?.[1] ?? m?.[2] ?? m?.[3] ?? "";
36440
+ }
36441
+ function isTransientBinDir(dir) {
36442
+ const parts = dir.split(/[\\/]+/).filter((p) => p !== "");
36443
+ const [parent, last] = parts.slice(-2);
36444
+ return parts.includes("_npx") || parent === "node_modules" && last === ".bin";
36445
+ }
36446
+ function commandLauncherExists(command, env = process.env, platform = process.platform) {
36447
+ const program = commandProgram(command);
36448
+ if (program === "") return false;
36449
+ if (program.includes("/") || program.includes("\\")) return existsSync(program);
36450
+ const win = platform === "win32";
36451
+ const exts = win ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
36452
+ const dirs = (env.PATH ?? env.Path ?? "").split(win ? ";" : ":").filter((d) => d !== "" && !isTransientBinDir(d));
36453
+ return dirs.some((dir) => exts.some((ext) => existsSync(join(dir, program + ext))));
36454
+ }
36455
+ function localBinExists(fromDir, name) {
36456
+ for (let dir = resolve(fromDir); ; dir = dirname(dir)) {
36457
+ const bin = join(dir, "node_modules", ".bin");
36458
+ if (existsSync(join(bin, name)) || existsSync(join(bin, `${name}.cmd`))) return true;
36459
+ if (dirname(dir) === dir) return false;
36460
+ }
36461
+ }
36462
+
35339
36463
  // src/lessons/merge-driver-setup.ts
35340
36464
  var LESSONS_MERGE_DRIVER = "agentsmesh-lessons";
35341
- var LESSONS_GITATTRIBUTES_ENTRY = `.agentsmesh/lessons/lessons.json merge=${LESSONS_MERGE_DRIVER}`;
36465
+ var LESSONS_GITATTRIBUTES_ENTRY = `${LESSONS_GRAPH_PATH} merge=${LESSONS_MERGE_DRIVER}`;
36466
+ var DRIVER_KEY = `merge.${LESSONS_MERGE_DRIVER}.driver`;
36467
+ var NAME_KEY = `merge.${LESSONS_MERGE_DRIVER}.name`;
36468
+ var DRIVER_NAME = "agentsmesh lessons union";
36469
+ function lessonsMergeDriverCommand(invocation) {
36470
+ return `${invocation.replaceAll("\\", "/")} lessons merge-driver %O %A %B`;
36471
+ }
36472
+ var NPX_INVOCATION = "npx --no --offline agentsmesh";
36473
+ var NPX_COMMAND = lessonsMergeDriverCommand(NPX_INVOCATION);
36474
+ var BARE_COMMAND = lessonsMergeDriverCommand("agentsmesh");
36475
+ var OWN_COMMANDS = /* @__PURE__ */ new Set([BARE_COMMAND, NPX_COMMAND]);
36476
+ function launchable(command, env) {
36477
+ const npxMissing = command === NPX_COMMAND && !commandLauncherExists(NPX_COMMAND, env);
36478
+ return npxMissing && commandLauncherExists(BARE_COMMAND, env) ? BARE_COMMAND : command;
36479
+ }
36480
+ function configValue(git, root, key) {
36481
+ const r = git(root, ["config", "--get", key]);
36482
+ return r.status === 0 ? r.stdout.trim() : null;
36483
+ }
36484
+ function launchProblem(git, projectRoot, command, env) {
36485
+ if (!commandLauncherExists(command, env)) {
36486
+ return `\`${commandProgram(command)}\` is not installed on PATH (npx and package-script bin folders do not count), so git could not start the driver; install agentsmesh globally or as a project devDependency`;
36487
+ }
36488
+ if (command !== NPX_COMMAND) return null;
36489
+ const top = git(projectRoot, ["rev-parse", "--show-toplevel"]);
36490
+ const repoRoot = top.status === 0 ? top.stdout.trim() : projectRoot;
36491
+ if (localBinExists(repoRoot, "agentsmesh") || commandLauncherExists("agentsmesh", env)) {
36492
+ return null;
36493
+ }
36494
+ return `git runs the driver from the repository root (${repoRoot.replaceAll("\\", "/")}), where \`${NPX_INVOCATION}\` cannot find agentsmesh; add agentsmesh to the devDependencies of the root package.json and install, or install agentsmesh globally`;
36495
+ }
36496
+ function ensureLessonsMergeDriver(projectRoot, options = {}) {
36497
+ const git = options.git ?? runGit2;
36498
+ const env = options.env ?? process.env;
36499
+ const command = launchable(
36500
+ lessonsMergeDriverCommand(options.invocation ?? agentsmeshInvocation(projectRoot)),
36501
+ env
36502
+ );
36503
+ const attr = git(projectRoot, ["check-attr", "merge", "--", LESSONS_GRAPH_PATH]);
36504
+ if (attr.status !== 0 || !attr.stdout.trim().endsWith(`: merge: ${LESSONS_MERGE_DRIVER}`)) {
36505
+ return { status: "skipped", command };
36506
+ }
36507
+ const existing = configValue(git, projectRoot, DRIVER_KEY);
36508
+ if (existing !== null && existing !== command && !OWN_COMMANDS.has(existing)) {
36509
+ return { status: "custom", command, existing };
36510
+ }
36511
+ const reason = existing === command ? null : launchProblem(git, projectRoot, command, env);
36512
+ if (reason !== null) return { status: "failed", command, reason };
36513
+ const writes = [];
36514
+ if (existing !== command) writes.push([DRIVER_KEY, command]);
36515
+ if (configValue(git, projectRoot, NAME_KEY) === null) writes.push([NAME_KEY, DRIVER_NAME]);
36516
+ for (const [key, value] of writes) {
36517
+ const r = git(projectRoot, ["config", "--local", key, value]);
36518
+ if (r.status !== 0) {
36519
+ const detail = r.stderr.trim() || `exit ${r.status}`;
36520
+ const reason2 = `git config failed (${detail}); run: git config ${DRIVER_KEY} "${command}"`;
36521
+ return { status: "failed", command, reason: reason2 };
36522
+ }
36523
+ }
36524
+ if (existing === command) return { status: "unchanged", command };
36525
+ return { status: existing === null ? "configured" : "updated", command };
36526
+ }
36527
+
36528
+ // src/lessons/recall-hook-hint.ts
36529
+ init_recall_hook_scaffold();
36530
+ var RECALL_HOOK_TEAM_HINT = "Lessons recall hooks call a global agentsmesh: teammates without a global install will not get lesson recall; add agentsmesh as a devDependency and re-run 'agentsmesh init --lessons'.";
36531
+ function recallHookTeamHint(projectRoot) {
36532
+ const wired = wiredRecallCommands(projectRoot);
36533
+ if (wired.length === 0) return null;
36534
+ const expected = recallHookCommand(projectRoot);
36535
+ const stale = wired.find((command) => command !== expected);
36536
+ if (stale !== void 0) {
36537
+ return `Lessons recall hooks run \`${stale}\`, but this project now calls \`${expected}\`; re-run 'agentsmesh init --lessons' to update them.`;
36538
+ }
36539
+ if (!existsSync(join(projectRoot, "package.json"))) return null;
36540
+ return expected === RECALL_HOOK_COMMAND ? RECALL_HOOK_TEAM_HINT : null;
36541
+ }
36542
+ function wiredRecallCommands(projectRoot) {
36543
+ try {
36544
+ const hooks = parse(
36545
+ readFileSync(join(projectRoot, ".agentsmesh", "hooks.yaml"), "utf8")
36546
+ );
36547
+ return Object.values(hooks ?? {}).filter(Array.isArray).flat().map((entry) => entry?.command).filter(isManagedRecallCommand).map((command) => command.trim());
36548
+ } catch {
36549
+ return [];
36550
+ }
36551
+ }
35342
36552
 
35343
36553
  // src/utils/filesystem/gitattributes.ts
35344
36554
  init_fs();
@@ -35479,7 +36689,8 @@ At least one _effective_ trigger is required (or \`--scope always\` for a univer
35479
36689
  the capture is rejected (\`UNRECALLABLE_LESSON\`); prefer \`--trigger-file\`. No shell \u2192 MCP \`lessons_query\`,
35480
36690
  \`lessons_add\`, \`lessons_topics\`, \`lessons_show\`, \`lessons_deprecate\`. Run
35481
36691
  \`agentsmesh lessons --help\` for every subcommand and flag: query, add, topics, show,
35482
- deprecate, merge, untrigger, strip-markers, prune, journal, validate, stats, import-md.
36692
+ deprecate, merge, untrigger, strip-markers, prune, journal, validate, resolve, stats, import-md.
36693
+ A git merge conflict in \`lessons.json\` \u2192 run \`agentsmesh lessons resolve\`; never hand-edit it.
35483
36694
 
35484
36695
  ### Rationalization Prevention \u2014 these excuses mean STOP
35485
36696
 
@@ -35523,11 +36734,14 @@ async function scaffoldLessons(projectRoot) {
35523
36734
  const gitignoreUpdated = await ensureGitignoreEntries(projectRoot, [
35524
36735
  toRelPath(projectRoot, recallLogPath(projectRoot)),
35525
36736
  toRelPath(projectRoot, captureLogPath(projectRoot)),
35526
- toRelPath(projectRoot, outcomeLogPath(projectRoot))
36737
+ toRelPath(projectRoot, outcomeLogPath(projectRoot)),
36738
+ `${toRelPath(projectRoot, lessonsLockPath(projectRoot))}/`,
36739
+ `${toRelPath(projectRoot, paths2.base)}/*.tmp`
35527
36740
  ]);
35528
36741
  const gitattributesUpdated = await ensureGitattributesEntries(projectRoot, [
35529
36742
  LESSONS_GITATTRIBUTES_ENTRY
35530
36743
  ]);
36744
+ const mergeDriver = ensureLessonsMergeDriver(projectRoot);
35531
36745
  return {
35532
36746
  created,
35533
36747
  updated,
@@ -35535,7 +36749,9 @@ async function scaffoldLessons(projectRoot) {
35535
36749
  rootRuleUpdated,
35536
36750
  gitignoreUpdated,
35537
36751
  gitattributesUpdated,
35538
- recallHookInjected
36752
+ recallHookInjected,
36753
+ mergeDriver,
36754
+ recallHookTeamHint: recallHookTeamHint(projectRoot)
35539
36755
  };
35540
36756
  }
35541
36757
  function seedLessonsSkill(projectRoot, created, updated, skipped) {