agentsmesh 0.40.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
  }
@@ -3731,7 +3784,7 @@ var init_importer = __esm({
3731
3784
  }
3732
3785
  });
3733
3786
  function globFilter(files, pattern) {
3734
- const isMatch = picomatch2(pattern, OPTIONS);
3787
+ const isMatch = picomatch(pattern, OPTIONS);
3735
3788
  return files.filter((file) => isMatch(file));
3736
3789
  }
3737
3790
  var OPTIONS;
@@ -7096,10 +7149,6 @@ function generateAgents4(canonical) {
7096
7149
  content: serializeAntigravityAgent(agent)
7097
7150
  }));
7098
7151
  }
7099
- function generateIgnore3(canonical) {
7100
- if (canonical.ignore.length === 0) return [];
7101
- return [{ path: ANTIGRAVITY_IGNORE_FILE, content: canonical.ignore.join("\n") }];
7102
- }
7103
7152
  function renderAntigravityGlobalInstructions(canonical) {
7104
7153
  const root = canonical.rules.find((rule) => rule.root);
7105
7154
  const nonRootRules = canonical.rules.filter((rule) => {
@@ -7114,7 +7163,7 @@ function generateHooks2(canonical) {
7114
7163
  if (Object.keys(hooks).length === 0) return [];
7115
7164
  return [{ path: ANTIGRAVITY_HOOKS_FILE, content: JSON.stringify(hooks, null, 2) }];
7116
7165
  }
7117
- var generatePermissions3;
7166
+ var generateIgnore3, generatePermissions3;
7118
7167
  var init_generator4 = __esm({
7119
7168
  "src/targets/antigravity/generator.ts"() {
7120
7169
  init_no_outputs();
@@ -7123,6 +7172,8 @@ var init_generator4 = __esm({
7123
7172
  init_hooks_format2();
7124
7173
  init_agents_format();
7125
7174
  init_constants5();
7175
+ init_ignore_output();
7176
+ generateIgnore3 = ignoreOutput(ANTIGRAVITY_IGNORE_FILE);
7126
7177
  generatePermissions3 = NO_OUTPUTS;
7127
7178
  }
7128
7179
  });
@@ -8716,16 +8767,14 @@ function generateHooks3(canonical) {
8716
8767
  const content = JSON.stringify({ hooks: claudeHooks }, null, 2);
8717
8768
  return [{ path: CLAUDE_SETTINGS, content }];
8718
8769
  }
8719
- function generateIgnore5(canonical) {
8720
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
8721
- const content = canonical.ignore.join("\n");
8722
- return [{ path: CLAUDE_IGNORE, content }];
8723
- }
8770
+ var generateIgnore5;
8724
8771
  var init_generator6 = __esm({
8725
8772
  "src/targets/claude-code/generator.ts"() {
8726
8773
  init_markdown();
8727
8774
  init_constants7();
8728
8775
  init_hooks_format2();
8776
+ init_ignore_output();
8777
+ generateIgnore5 = ignoreOutput(CLAUDE_IGNORE);
8729
8778
  }
8730
8779
  });
8731
8780
  function ruleSectionTitle(rule) {
@@ -10544,11 +10593,7 @@ function generateMcp5(canonical) {
10544
10593
  if (!canonical.mcp || Object.keys(canonical.mcp.mcpServers).length === 0) return [];
10545
10594
  return [{ path: CODEBUFF_MCP_FILE, content: serializeCodebuffMcp(canonical.mcp) }];
10546
10595
  }
10547
- function generateIgnore7(canonical) {
10548
- if (canonical.ignore.length === 0) return [];
10549
- return [{ path: CODEBUFF_IGNORE_FILE, content: canonical.ignore.join("\n") }];
10550
- }
10551
- var generateAgents8, generateHooks5, generatePermissions6;
10596
+ var generateIgnore7, generateAgents8, generateHooks5, generatePermissions6;
10552
10597
  var init_generator8 = __esm({
10553
10598
  "src/targets/codebuff/generator.ts"() {
10554
10599
  init_no_outputs();
@@ -10558,6 +10603,8 @@ var init_generator8 = __esm({
10558
10603
  init_nested_rules();
10559
10604
  init_mcp_format2();
10560
10605
  init_constants9();
10606
+ init_ignore_output();
10607
+ generateIgnore7 = ignoreOutput(CODEBUFF_IGNORE_FILE);
10561
10608
  generateAgents8 = NO_OUTPUTS;
10562
10609
  generateHooks5 = NO_OUTPUTS;
10563
10610
  generatePermissions6 = NO_OUTPUTS;
@@ -13070,53 +13117,37 @@ var init_mcp_generator = __esm({
13070
13117
  }
13071
13118
  });
13072
13119
 
13073
- // src/targets/copilot/hook-entry.ts
13074
- function hasHookCommand2(entry) {
13075
- return hasHookCommand(entry);
13076
- }
13077
- var init_hook_entry = __esm({
13078
- "src/targets/copilot/hook-entry.ts"() {
13079
- init_hook_command();
13080
- }
13081
- });
13082
-
13083
13120
  // src/targets/copilot/hook-format.ts
13084
- function mapHookEvent(event) {
13085
- switch (event) {
13086
- case "PreToolUse":
13087
- return "preToolUse";
13088
- case "PostToolUse":
13089
- return "postToolUse";
13090
- case "Notification":
13091
- return "notification";
13092
- case "UserPromptSubmit":
13093
- return "userPromptSubmitted";
13094
- default:
13095
- return null;
13096
- }
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`;
13097
13133
  }
13098
13134
  function buildCopilotHooksObject(hooks) {
13099
- if (!hooks) return null;
13100
- const result2 = Object.fromEntries(
13101
- Object.entries(hooks).flatMap(([event, entries]) => {
13102
- const mappedEvent = mapHookEvent(event);
13103
- if (!mappedEvent || !Array.isArray(entries)) return [];
13104
- const mappedEntries = entries.filter(
13105
- (entry) => typeof entry === "object" && entry !== null && hasHookCommand2(entry)
13106
- ).map((entry, index) => {
13107
- 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) => {
13108
13141
  const hook = {
13109
13142
  type: "command",
13110
- bash: `./scripts/${safePhase}-${index}.sh`
13143
+ bash: `./scripts/${wrapperScriptName(event, index)}`
13111
13144
  };
13112
13145
  if (entry.matcher && entry.matcher !== "*") hook.matcher = entry.matcher;
13113
13146
  if (entry.timeout !== void 0) hook.timeoutSec = Math.ceil(entry.timeout / 1e3);
13114
13147
  return hook;
13115
- });
13116
- return mappedEntries.length > 0 ? [[mappedEvent, mappedEntries]] : [];
13117
- })
13148
+ })
13149
+ ])
13118
13150
  );
13119
- return Object.keys(result2).length > 0 ? result2 : null;
13120
13151
  }
13121
13152
  function generateHooks8(canonical) {
13122
13153
  const hooks = buildCopilotHooksObject(canonical.hooks);
@@ -13128,10 +13159,24 @@ function generateHooks8(canonical) {
13128
13159
  }
13129
13160
  ];
13130
13161
  }
13162
+ var COPILOT_HOOK_CONTEXT_EVENTS, CANONICAL_TO_COPILOT;
13131
13163
  var init_hook_format = __esm({
13132
13164
  "src/targets/copilot/hook-format.ts"() {
13133
13165
  init_constants32();
13134
- 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
+ ]);
13135
13180
  }
13136
13181
  });
13137
13182
  function ruleSlug2(source) {
@@ -13237,18 +13282,7 @@ var init_generator12 = __esm({
13237
13282
  }
13238
13283
  });
13239
13284
  function mapCopilotHookEvent(event) {
13240
- switch (event) {
13241
- case "preToolUse":
13242
- return "PreToolUse";
13243
- case "postToolUse":
13244
- return "PostToolUse";
13245
- case "notification":
13246
- return "Notification";
13247
- case "userPromptSubmitted":
13248
- return "UserPromptSubmit";
13249
- default:
13250
- return null;
13251
- }
13285
+ return COPILOT_TO_CANONICAL.get(event) ?? null;
13252
13286
  }
13253
13287
  function extractMatcher(comment) {
13254
13288
  if (typeof comment !== "string") return "*";
@@ -13336,11 +13370,16 @@ async function importHooks(projectRoot, results, options = {}) {
13336
13370
  feature: "hooks"
13337
13371
  });
13338
13372
  }
13373
+ var COPILOT_TO_CANONICAL;
13339
13374
  var init_hook_parser = __esm({
13340
13375
  "src/targets/copilot/hook-parser.ts"() {
13341
13376
  init_canonical_paths();
13342
13377
  init_fs();
13343
13378
  init_constants32();
13379
+ init_hook_format();
13380
+ COPILOT_TO_CANONICAL = new Map(
13381
+ [...CANONICAL_TO_COPILOT].map(([canonical, copilot]) => [copilot, canonical])
13382
+ );
13344
13383
  }
13345
13384
  });
13346
13385
  async function importSkills2(projectRoot, results, normalize, skillsDirRel = COPILOT_SKILLS_DIR) {
@@ -13577,7 +13616,7 @@ function lintCommands4(canonical) {
13577
13616
  }
13578
13617
  function lintHooks9(canonical) {
13579
13618
  if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
13580
- const supported = ["PreToolUse", "PostToolUse", "Notification", "UserPromptSubmit"];
13619
+ const supported = [...CANONICAL_TO_COPILOT.keys()];
13581
13620
  const diagnostics = unsupportedHookEventNames(canonical.hooks, supported).map(
13582
13621
  (event) => createUnsupportedHookWarning(event, "copilot", supported, {
13583
13622
  unsupportedBy: "Copilot hooks"
@@ -13600,11 +13639,9 @@ function lintHooks9(canonical) {
13600
13639
  var init_lint10 = __esm({
13601
13640
  "src/targets/copilot/lint.ts"() {
13602
13641
  init_helpers();
13642
+ init_hook_format();
13603
13643
  }
13604
13644
  });
13605
- function safePhaseName(phase) {
13606
- return phase.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase();
13607
- }
13608
13645
  function toRepoRelative(projectRoot, sourcePath) {
13609
13646
  const repoRelative = relative(projectRoot, sourcePath).replace(/\\/g, "/");
13610
13647
  if (!repoRelative || repoRelative.startsWith("../")) return null;
@@ -13632,9 +13669,6 @@ async function buildAssetOutput(projectRoot, command, hooksDirRel) {
13632
13669
  rewrittenCommand: rewriteWrapperCommand(command, repoRelative)
13633
13670
  };
13634
13671
  }
13635
- function wrapperPath(event, index, hooksDirRel) {
13636
- return `${hooksDirRel}/scripts/${safePhaseName(event)}-${index}.sh`;
13637
- }
13638
13672
  function safeShellLine2(value) {
13639
13673
  return value.replace(/[\r\n]+/g, " ");
13640
13674
  }
@@ -13649,15 +13683,13 @@ function buildWrapper(command, matcher) {
13649
13683
  ].join("\n");
13650
13684
  }
13651
13685
  async function addHookScriptAssets(projectRoot, canonical, outputs, hooksDirRel = COPILOT_HOOKS_DIR) {
13652
- if (!canonical.hooks) return outputs;
13686
+ const groups = copilotHookGroups(canonical.hooks);
13687
+ if (groups.length === 0) return outputs;
13653
13688
  const wrapperOutputs = [];
13654
13689
  const assetOutputs = /* @__PURE__ */ new Map();
13655
- for (const [event, entries] of Object.entries(canonical.hooks)) {
13656
- if (!Array.isArray(entries)) continue;
13657
- let index = 0;
13658
- for (const entry of entries) {
13659
- if (!hasHookCommand2(entry)) continue;
13660
- 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)}`;
13661
13693
  let command = entry.command;
13662
13694
  const asset = await buildAssetOutput(projectRoot, entry.command, hooksDirRel);
13663
13695
  if (asset) {
@@ -13671,7 +13703,6 @@ async function addHookScriptAssets(projectRoot, canonical, outputs, hooksDirRel
13671
13703
  'set -eu\nHOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\n'
13672
13704
  );
13673
13705
  wrapperOutputs.push({ path: scriptPath, content: wrapper });
13674
- index++;
13675
13706
  }
13676
13707
  }
13677
13708
  return [...outputs, ...wrapperOutputs, ...assetOutputs.values()];
@@ -13681,7 +13712,7 @@ var init_hook_assets = __esm({
13681
13712
  "src/targets/copilot/hook-assets.ts"() {
13682
13713
  init_fs();
13683
13714
  init_constants32();
13684
- init_hook_entry();
13715
+ init_hook_format();
13685
13716
  SCRIPT_PREFIX_RE = /^(?<prefix>\s*(?:(?:bash|sh|zsh)\s+)?)["']?(?<path>(?:\.\.\/|\.\/|[^/\s"'`]+\/)[^\s"'`]+)["']?(?<suffix>(?:\s.*)?)$/;
13686
13717
  }
13687
13718
  });
@@ -13999,6 +14030,7 @@ var init_copilot2 = __esm({
13999
14030
  init_command_prompt();
14000
14031
  init_lint10();
14001
14032
  init_hook_assets();
14033
+ init_hook_format();
14002
14034
  init_scope_extras2();
14003
14035
  init_importer_spec2();
14004
14036
  init_capabilities6();
@@ -14129,6 +14161,7 @@ var init_copilot2 = __esm({
14129
14161
  permissions: lintPermissions7
14130
14162
  },
14131
14163
  postProcessHookOutputs: async (projectRoot, canonical, outputs) => addHookScriptAssets(projectRoot, canonical, [...outputs]),
14164
+ hookContextEvents: COPILOT_HOOK_CONTEXT_EVENTS,
14132
14165
  mergeGeneratedOutputContent: mergeCopilotMcpJson,
14133
14166
  project: project7,
14134
14167
  globalSupport: {
@@ -14216,11 +14249,6 @@ function generatePermissions8(canonical) {
14216
14249
  });
14217
14250
  return [{ path: CRUSH_CONFIG_FILE, content: JSON.stringify(crushConfig, null, 2) }];
14218
14251
  }
14219
- function generateIgnore9(canonical) {
14220
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
14221
- const content = canonical.ignore.join("\n");
14222
- return [{ path: CRUSH_IGNORE, content }];
14223
- }
14224
14252
  function buildCrushHooksFromCanonical(canonical) {
14225
14253
  if (!canonical.hooks) return {};
14226
14254
  const result2 = {};
@@ -14238,7 +14266,7 @@ function buildCrushHooksFromCanonical(canonical) {
14238
14266
  }
14239
14267
  return result2;
14240
14268
  }
14241
- var generateRules12;
14269
+ var generateRules12, generateIgnore9;
14242
14270
  var init_generator13 = __esm({
14243
14271
  "src/targets/crush/generator.ts"() {
14244
14272
  init_managed_blocks();
@@ -14247,7 +14275,9 @@ var init_generator13 = __esm({
14247
14275
  init_command_skill();
14248
14276
  init_constants12();
14249
14277
  init_config_format();
14278
+ init_ignore_output();
14250
14279
  generateRules12 = (canonical) => embeddedRootRule(canonical, CRUSH_TARGET, CRUSH_ROOT_FILE);
14280
+ generateIgnore9 = ignoreOutput(CRUSH_IGNORE);
14251
14281
  }
14252
14282
  });
14253
14283
  async function importFromCrush(projectRoot, options = {}) {
@@ -14783,7 +14813,7 @@ function cursorHooksToCanonical(hooks) {
14783
14813
  }
14784
14814
  return result2;
14785
14815
  }
14786
- var CANONICAL_TO_CURSOR, CURSOR_TO_CANONICAL;
14816
+ var CANONICAL_TO_CURSOR, CURSOR_HOOK_CONTEXT_EVENTS, CURSOR_TO_CANONICAL;
14787
14817
  var init_hook_format2 = __esm({
14788
14818
  "src/targets/cursor/hook-format.ts"() {
14789
14819
  init_hook_types();
@@ -14791,6 +14821,7 @@ var init_hook_format2 = __esm({
14791
14821
  CANONICAL_TO_CURSOR = {
14792
14822
  PreToolUse: "preToolUse",
14793
14823
  PostToolUse: "postToolUse",
14824
+ PostToolUseFailure: "postToolUseFailure",
14794
14825
  UserPromptSubmit: "beforeSubmitPrompt",
14795
14826
  SubagentStart: "subagentStart",
14796
14827
  SubagentStop: "subagentStop",
@@ -14799,6 +14830,11 @@ var init_hook_format2 = __esm({
14799
14830
  SessionEnd: "sessionEnd",
14800
14831
  PreCompact: "preCompact"
14801
14832
  };
14833
+ CURSOR_HOOK_CONTEXT_EVENTS = [
14834
+ "SessionStart",
14835
+ "PostToolUse",
14836
+ "PostToolUseFailure"
14837
+ ];
14802
14838
  CURSOR_TO_CANONICAL = new Map(
14803
14839
  Object.entries(CANONICAL_TO_CURSOR).map(([canonical, cursor]) => [cursor, canonical])
14804
14840
  );
@@ -14807,7 +14843,7 @@ var init_hook_format2 = __esm({
14807
14843
 
14808
14844
  // src/targets/cursor/generator/hooks.ts
14809
14845
  function generateHooks10(canonical) {
14810
- if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
14846
+ if (!canonical.hooks) return [];
14811
14847
  const cursorHooks = toCursorHooks(canonical.hooks);
14812
14848
  if (Object.keys(cursorHooks).length === 0) return [];
14813
14849
  const content = JSON.stringify({ version: 1, hooks: cursorHooks }, null, 2);
@@ -14821,14 +14857,12 @@ var init_hooks3 = __esm({
14821
14857
  });
14822
14858
 
14823
14859
  // src/targets/cursor/generator/ignore.ts
14824
- function generateIgnore10(canonical) {
14825
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
14826
- const content = canonical.ignore.join("\n");
14827
- return [{ path: CURSOR_IGNORE, content }];
14828
- }
14860
+ var generateIgnore10;
14829
14861
  var init_ignore = __esm({
14830
14862
  "src/targets/cursor/generator/ignore.ts"() {
14831
14863
  init_constants13();
14864
+ init_ignore_output();
14865
+ generateIgnore10 = ignoreOutput(CURSOR_IGNORE);
14832
14866
  }
14833
14867
  });
14834
14868
 
@@ -15524,6 +15558,7 @@ var init_cursor2 = __esm({
15524
15558
  init_linter13();
15525
15559
  init_import_map_builders();
15526
15560
  init_lint12();
15561
+ init_hook_format2();
15527
15562
  target13 = {
15528
15563
  name: "cursor",
15529
15564
  primaryRootInstructionPath: CURSOR_GENERAL_RULE,
@@ -15645,6 +15680,7 @@ var init_cursor2 = __esm({
15645
15680
  permissions: "native"
15646
15681
  },
15647
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,
15648
15684
  lintRules: lintRules13,
15649
15685
  lint: {
15650
15686
  commands: lintCommands6,
@@ -16906,25 +16942,127 @@ var init_skills3 = __esm({
16906
16942
  }
16907
16943
  });
16908
16944
 
16909
- // src/targets/gemini-cli/generator/settings.ts
16910
- function mapHookEvent2(event) {
16911
- switch (event) {
16912
- case "PreToolUse":
16913
- return "BeforeTool";
16914
- case "PostToolUse":
16915
- return "AfterTool";
16916
- case "Notification":
16917
- return "Notification";
16918
- case "SubagentStart":
16919
- return "BeforeAgent";
16920
- case "SubagentStop":
16921
- return "AfterAgent";
16922
- case "SessionStart":
16923
- return "SessionStart";
16924
- default:
16925
- 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)];
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
+ }
16926
17055
  }
17056
+ return Object.keys(result2).length > 0 ? result2 : null;
16927
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
16928
17066
  function generateGeminiSettingsFiles(canonical, enabledFeatures) {
16929
17067
  const settings = {};
16930
17068
  let hasAnyNativeSettings = false;
@@ -16936,29 +17074,10 @@ function generateGeminiSettingsFiles(canonical, enabledFeatures) {
16936
17074
  settings.experimental = { enableAgents: true };
16937
17075
  hasAnyNativeSettings = true;
16938
17076
  }
16939
- if (enabledFeatures.has("hooks") && canonical.hooks) {
16940
- const hookEntries = Object.entries(canonical.hooks).flatMap(([event, entries]) => {
16941
- const mappedEvent = mapHookEvent2(event);
16942
- if (!mappedEvent || !Array.isArray(entries)) return [];
16943
- const mappedEntries = entries.filter(
16944
- (entry) => typeof entry === "object" && entry !== null && hasHookCommand(entry)
16945
- ).map((entry, index) => ({
16946
- matcher: entry.matcher,
16947
- hooks: [
16948
- {
16949
- name: `${mappedEvent}-${index + 1}`,
16950
- type: "command",
16951
- command: getHookCommand(entry),
16952
- timeout: entry.timeout
16953
- }
16954
- ]
16955
- }));
16956
- return mappedEntries.length > 0 ? [[mappedEvent, mappedEntries]] : [];
16957
- });
16958
- if (hookEntries.length > 0) {
16959
- settings.hooks = Object.fromEntries(hookEntries);
16960
- hasAnyNativeSettings = true;
16961
- }
17077
+ const hooks = enabledFeatures.has("hooks") ? buildGeminiHooks(canonical.hooks) : null;
17078
+ if (hooks) {
17079
+ settings.hooks = hooks;
17080
+ hasAnyNativeSettings = true;
16962
17081
  }
16963
17082
  if (hasAnyNativeSettings) {
16964
17083
  settings.context = { fileName: [GEMINI_ROOT, GEMINI_COMPAT_AGENTS] };
@@ -16968,19 +17087,18 @@ function generateGeminiSettingsFiles(canonical, enabledFeatures) {
16968
17087
  }
16969
17088
  var init_settings2 = __esm({
16970
17089
  "src/targets/gemini-cli/generator/settings.ts"() {
16971
- init_hook_command();
16972
17090
  init_constants33();
17091
+ init_hooks4();
16973
17092
  }
16974
17093
  });
16975
17094
 
16976
17095
  // src/targets/gemini-cli/generator/ignore.ts
16977
- function generateIgnore13(canonical) {
16978
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
16979
- return [{ path: GEMINI_IGNORE, content: canonical.ignore.join("\n") }];
16980
- }
17096
+ var generateIgnore13;
16981
17097
  var init_ignore2 = __esm({
16982
17098
  "src/targets/gemini-cli/generator/ignore.ts"() {
16983
17099
  init_constants33();
17100
+ init_ignore_output();
17101
+ generateIgnore13 = ignoreOutput(GEMINI_IGNORE);
16984
17102
  }
16985
17103
  });
16986
17104
 
@@ -17259,27 +17377,6 @@ var init_layout6 = __esm({
17259
17377
  };
17260
17378
  }
17261
17379
  });
17262
- function mapGeminiHookEvent(event) {
17263
- switch (event) {
17264
- case "BeforeTool":
17265
- case "preToolUse":
17266
- return "PreToolUse";
17267
- case "AfterTool":
17268
- case "postToolUse":
17269
- return "PostToolUse";
17270
- case "Notification":
17271
- case "notification":
17272
- return "Notification";
17273
- case "BeforeAgent":
17274
- return "SubagentStart";
17275
- case "AfterAgent":
17276
- return "SubagentStop";
17277
- case "SessionStart":
17278
- return "SessionStart";
17279
- default:
17280
- return null;
17281
- }
17282
- }
17283
17380
  function parseFlexibleFrontmatter(content) {
17284
17381
  const yamlOpen = content.indexOf("---");
17285
17382
  const tomlOpen = content.indexOf("+++");
@@ -17353,7 +17450,7 @@ async function importGeminiSettings(projectRoot, results) {
17353
17450
  (entry) => entry.hooks.filter(
17354
17451
  (hook) => hook !== null && typeof hook === "object" && hasHookCommand(hook)
17355
17452
  ).map((hook) => ({
17356
- matcher: entry.matcher,
17453
+ matcher: fromGeminiMatcher(event, entry.matcher),
17357
17454
  command: getHookCommand(hook),
17358
17455
  type: "command",
17359
17456
  timeout: typeof hook.timeout === "number" ? hook.timeout : void 0
@@ -17363,7 +17460,7 @@ async function importGeminiSettings(projectRoot, results) {
17363
17460
  const legacyMapped = value.filter(
17364
17461
  (entry) => entry !== null && typeof entry === "object" && typeof entry.matcher === "string" && hasHookCommand(entry)
17365
17462
  ).map((entry) => ({
17366
- matcher: entry.matcher,
17463
+ matcher: fromGeminiMatcher(event, entry.matcher),
17367
17464
  command: getHookCommand(entry),
17368
17465
  type: "command"
17369
17466
  }));
@@ -17392,7 +17489,7 @@ var init_format_helpers_settings = __esm({
17392
17489
  init_hook_command();
17393
17490
  init_fs();
17394
17491
  init_constants33();
17395
- init_format_helpers_shared();
17492
+ init_hook_map();
17396
17493
  }
17397
17494
  });
17398
17495
  async function importGeminiIgnore(projectRoot, results) {
@@ -17888,14 +17985,7 @@ function lintPermissions10(canonical, options) {
17888
17985
  }
17889
17986
  function lintHooks12(canonical) {
17890
17987
  if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
17891
- const supported = [
17892
- "PreToolUse",
17893
- "PostToolUse",
17894
- "Notification",
17895
- "SubagentStart",
17896
- "SubagentStop",
17897
- "SessionStart"
17898
- ];
17988
+ const supported = [...CANONICAL_TO_GEMINI.keys()];
17899
17989
  return unsupportedHookEventNames(canonical.hooks, supported).map(
17900
17990
  (event) => createUnsupportedHookWarning(event, "gemini-cli", supported)
17901
17991
  );
@@ -17903,6 +17993,7 @@ function lintHooks12(canonical) {
17903
17993
  var init_lint15 = __esm({
17904
17994
  "src/targets/gemini-cli/lint.ts"() {
17905
17995
  init_helpers();
17996
+ init_hook_map();
17906
17997
  }
17907
17998
  });
17908
17999
 
@@ -17939,6 +18030,7 @@ var init_gemini_cli2 = __esm({
17939
18030
  init_import_map_builders();
17940
18031
  init_lint15();
17941
18032
  init_scoped_settings_emit();
18033
+ init_hook_map();
17942
18034
  init_settings();
17943
18035
  target16 = {
17944
18036
  name: "gemini-cli",
@@ -17989,6 +18081,7 @@ var init_gemini_cli2 = __esm({
17989
18081
  permissions: lintPermissions10
17990
18082
  },
17991
18083
  emitScopedSettings: emitScopedGeminiSettings,
18084
+ hookContextEvents: GEMINI_HOOK_CONTEXT_EVENTS,
17992
18085
  mergeGeneratedOutputContent(existing, pending, newContent, resolvedPath) {
17993
18086
  const base = pending?.content ?? existing;
17994
18087
  if (base !== null && resolvedPath === GEMINI_SETTINGS) {
@@ -18081,14 +18174,10 @@ function generateAgents17(canonical) {
18081
18174
  content: serializeProjectedAgentSkill(agent)
18082
18175
  }));
18083
18176
  }
18084
- function generateIgnore14(canonical) {
18085
- if (canonical.ignore.length === 0) return [];
18086
- return [{ path: GOOSE_IGNORE, content: canonical.ignore.join("\n") }];
18087
- }
18088
18177
  function generateHooks12(canonical) {
18089
18178
  return buildWrappedCommandHooks(canonical, GOOSE_HOOKS_FILE);
18090
18179
  }
18091
- var generateRules17, generatePermissions12;
18180
+ var generateRules17, generateIgnore14, generatePermissions12;
18092
18181
  var init_generator20 = __esm({
18093
18182
  "src/targets/goose/generator.ts"() {
18094
18183
  init_managed_blocks();
@@ -18098,7 +18187,9 @@ var init_generator20 = __esm({
18098
18187
  init_command_skill();
18099
18188
  init_wrapped_command_hooks();
18100
18189
  init_constants16();
18190
+ init_ignore_output();
18101
18191
  generateRules17 = (canonical) => embeddedRootRule(canonical, GOOSE_TARGET, GOOSE_ROOT_FILE);
18192
+ generateIgnore14 = ignoreOutput(GOOSE_IGNORE);
18102
18193
  generatePermissions12 = NO_OUTPUTS;
18103
18194
  }
18104
18195
  });
@@ -19003,10 +19094,6 @@ function generateAgents18(canonical) {
19003
19094
  };
19004
19095
  });
19005
19096
  }
19006
- function generateIgnore16(canonical) {
19007
- if (canonical.ignore.length === 0) return [];
19008
- return [{ path: JUNIE_IGNORE, content: canonical.ignore.join("\n") }];
19009
- }
19010
19097
  function generateSkills17(canonical) {
19011
19098
  return generateEmbeddedSkills(canonical, JUNIE_SKILLS_DIR);
19012
19099
  }
@@ -19018,6 +19105,7 @@ function renderJunieGlobalInstructions(canonical) {
19018
19105
  });
19019
19106
  return appendEmbeddedRulesBlock(root?.body.trim() ?? "", nonRootRules);
19020
19107
  }
19108
+ var generateIgnore16;
19021
19109
  var init_generator22 = __esm({
19022
19110
  "src/targets/junie/generator.ts"() {
19023
19111
  init_mcp_servers();
@@ -19025,7 +19113,9 @@ var init_generator22 = __esm({
19025
19113
  init_managed_blocks();
19026
19114
  init_markdown();
19027
19115
  init_constants18();
19116
+ init_ignore_output();
19028
19117
  init_global_config2();
19118
+ generateIgnore16 = ignoreOutput(JUNIE_IGNORE);
19029
19119
  }
19030
19120
  });
19031
19121
 
@@ -19441,10 +19531,6 @@ function generateMcp15(canonical) {
19441
19531
  }
19442
19532
  ];
19443
19533
  }
19444
- function generateIgnore17(canonical) {
19445
- if (canonical.ignore.length === 0) return [];
19446
- return [{ path: KILO_CODE_IGNORE, content: canonical.ignore.join("\n") }];
19447
- }
19448
19534
  function generatePermissions15(canonical) {
19449
19535
  if (!canonical.permissions) return [];
19450
19536
  const { allow, deny } = canonical.permissions;
@@ -19459,11 +19545,14 @@ function generatePermissions15(canonical) {
19459
19545
  function generateSkills18(canonical) {
19460
19546
  return generateEmbeddedSkills(canonical, KILO_CODE_SKILLS_DIR);
19461
19547
  }
19548
+ var generateIgnore17;
19462
19549
  var init_generator23 = __esm({
19463
19550
  "src/targets/kilo-code/generator.ts"() {
19464
19551
  init_embedded_skill();
19465
19552
  init_markdown();
19466
19553
  init_constants19();
19554
+ init_ignore_output();
19555
+ generateIgnore17 = ignoreOutput(KILO_CODE_IGNORE);
19467
19556
  }
19468
19557
  });
19469
19558
  var kiloNonRootRuleMapper, kiloCommandMapper, kiloAgentMapper;
@@ -21208,11 +21297,7 @@ function buildKiroAgentOutputs(canonical, rules = []) {
21208
21297
  function generateAgents21(canonical) {
21209
21298
  return buildKiroAgentOutputs(canonical);
21210
21299
  }
21211
- function generateIgnore18(canonical) {
21212
- if (canonical.ignore.length === 0) return [];
21213
- return [{ path: KIRO_IGNORE, content: canonical.ignore.join("\n") }];
21214
- }
21215
- var generatePermissions17;
21300
+ var generateIgnore18, generatePermissions17;
21216
21301
  var init_generator25 = __esm({
21217
21302
  "src/targets/kiro/generator.ts"() {
21218
21303
  init_no_outputs();
@@ -21221,6 +21306,8 @@ var init_generator25 = __esm({
21221
21306
  init_markdown();
21222
21307
  init_hook_format3();
21223
21308
  init_constants21();
21309
+ init_ignore_output();
21310
+ generateIgnore18 = ignoreOutput(KIRO_IGNORE);
21224
21311
  generatePermissions17 = NO_OUTPUTS;
21225
21312
  }
21226
21313
  });
@@ -23863,10 +23950,6 @@ function generateMcp19(canonical) {
23863
23950
  const content = JSON.stringify({ mcpServers: canonical.mcp.mcpServers }, null, 2);
23864
23951
  return [{ path: QWEN_SETTINGS, content }];
23865
23952
  }
23866
- function generateIgnore21(canonical) {
23867
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
23868
- return [{ path: QWEN_IGNORE, content: canonical.ignore.join("\n") }];
23869
- }
23870
23953
  function generateHooks18(canonical) {
23871
23954
  if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
23872
23955
  const hooks = buildClaudeHooksObjectFromCanonical(canonical);
@@ -23884,11 +23967,14 @@ function generatePermissions21(canonical) {
23884
23967
  if (ask.length > 0) permissions.ask = ask;
23885
23968
  return [{ path: QWEN_SETTINGS, content: JSON.stringify({ permissions }, null, 2) }];
23886
23969
  }
23970
+ var generateIgnore21;
23887
23971
  var init_generator29 = __esm({
23888
23972
  "src/targets/qwen-code/generator.ts"() {
23889
23973
  init_markdown();
23890
23974
  init_hooks_format2();
23891
23975
  init_constants25();
23976
+ init_ignore_output();
23977
+ generateIgnore21 = ignoreOutput(QWEN_IGNORE);
23892
23978
  }
23893
23979
  });
23894
23980
 
@@ -25586,10 +25672,6 @@ function generateMcp23(canonical) {
25586
25672
  }
25587
25673
  ];
25588
25674
  }
25589
- function generateIgnore24(canonical) {
25590
- if (canonical.ignore.length === 0) return [];
25591
- return [{ path: TRAE_IGNORE, content: canonical.ignore.join("\n") }];
25592
- }
25593
25675
  function generateHooks20(canonical) {
25594
25676
  if (!canonical.hooks) return [];
25595
25677
  const hooks = {};
@@ -25610,12 +25692,15 @@ function generateHooks20(canonical) {
25610
25692
  if (Object.keys(hooks).length === 0) return [];
25611
25693
  return [{ path: TRAE_HOOKS_FILE, content: JSON.stringify({ version: 1, hooks }, null, 2) }];
25612
25694
  }
25695
+ var generateIgnore24;
25613
25696
  var init_generator33 = __esm({
25614
25697
  "src/targets/trae/generator.ts"() {
25615
25698
  init_embedded_skill();
25616
25699
  init_markdown();
25617
25700
  init_hook_command();
25618
25701
  init_constants29();
25702
+ init_ignore_output();
25703
+ generateIgnore24 = ignoreOutput(TRAE_IGNORE);
25619
25704
  }
25620
25705
  });
25621
25706
 
@@ -26322,11 +26407,7 @@ function generateMcp24(canonical, ctx) {
26322
26407
  const content = JSON.stringify({ mcpServers: canonical.mcp.mcpServers }, null, 2);
26323
26408
  return [{ path, content }];
26324
26409
  }
26325
- function generateIgnore25(canonical) {
26326
- if (canonical.ignore.length === 0) return [];
26327
- return [{ path: WARP_IGNORE_FILE, content: canonical.ignore.join("\n") }];
26328
- }
26329
- var generateRules31, generatePermissions24, generateHooks21;
26410
+ var generateRules31, generatePermissions24, generateHooks21, generateIgnore25;
26330
26411
  var init_generator34 = __esm({
26331
26412
  "src/targets/warp/generator.ts"() {
26332
26413
  init_managed_blocks();
@@ -26335,9 +26416,11 @@ var init_generator34 = __esm({
26335
26416
  init_projected_agent_skill();
26336
26417
  init_command_skill();
26337
26418
  init_constants30();
26419
+ init_ignore_output();
26338
26420
  generateRules31 = (canonical) => embeddedRootRule(canonical, WARP_TARGET, WARP_ROOT_FILE);
26339
26421
  generatePermissions24 = NO_OUTPUTS;
26340
26422
  generateHooks21 = NO_OUTPUTS;
26423
+ generateIgnore25 = ignoreOutput(WARP_IGNORE_FILE);
26341
26424
  }
26342
26425
  });
26343
26426
 
@@ -26962,13 +27045,12 @@ var init_rules4 = __esm({
26962
27045
  });
26963
27046
 
26964
27047
  // src/targets/windsurf/generator/ignore.ts
26965
- function generateIgnore26(canonical) {
26966
- if (!canonical.ignore || canonical.ignore.length === 0) return [];
26967
- return [{ path: CODEIUM_IGNORE, content: canonical.ignore.join("\n") }];
26968
- }
27048
+ var generateIgnore26;
26969
27049
  var init_ignore3 = __esm({
26970
27050
  "src/targets/windsurf/generator/ignore.ts"() {
26971
27051
  init_constants34();
27052
+ init_ignore_output();
27053
+ generateIgnore26 = ignoreOutput(CODEIUM_IGNORE);
26972
27054
  }
26973
27055
  });
26974
27056
 
@@ -27034,7 +27116,7 @@ function canonicalHookEventName(event) {
27034
27116
  if (KNOWN_CANONICAL_HOOK_EVENTS.includes(event)) return event;
27035
27117
  return WINDSURF_TO_CANONICAL.get(event) ?? null;
27036
27118
  }
27037
- var KNOWN_CANONICAL_HOOK_EVENTS, WINDSURF_TO_CANONICAL;
27119
+ var KNOWN_CANONICAL_HOOK_EVENTS, WINDSURF_HOOK_CONTEXT_EVENTS, WINDSURF_TO_CANONICAL;
27038
27120
  var init_hook_events = __esm({
27039
27121
  "src/targets/windsurf/hook-events.ts"() {
27040
27122
  init_hook_types();
@@ -27047,6 +27129,7 @@ var init_hook_events = __esm({
27047
27129
  "SubagentStop",
27048
27130
  ...BEST_EFFORT_HOOK_EVENTS
27049
27131
  ];
27132
+ WINDSURF_HOOK_CONTEXT_EVENTS = [];
27050
27133
  WINDSURF_TO_CANONICAL = new Map(
27051
27134
  KNOWN_CANONICAL_HOOK_EVENTS.map((event) => [windsurfEventName(event), event])
27052
27135
  );
@@ -27072,12 +27155,12 @@ function toWindsurfHooks(hooks) {
27072
27155
  return result2;
27073
27156
  }
27074
27157
  function generateHooks22(canonical) {
27075
- if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
27158
+ if (!canonical.hooks) return [];
27076
27159
  const hooks = toWindsurfHooks(canonical.hooks);
27077
27160
  if (Object.keys(hooks).length === 0) return [];
27078
27161
  return [{ path: WINDSURF_HOOKS_FILE, content: JSON.stringify({ hooks }, null, 2) }];
27079
27162
  }
27080
- var init_hooks4 = __esm({
27163
+ var init_hooks5 = __esm({
27081
27164
  "src/targets/windsurf/generator/hooks.ts"() {
27082
27165
  init_hook_command();
27083
27166
  init_constants34();
@@ -27129,7 +27212,7 @@ var init_generator35 = __esm({
27129
27212
  init_workflows();
27130
27213
  init_agents4();
27131
27214
  init_mcp4();
27132
- init_hooks4();
27215
+ init_hooks5();
27133
27216
  init_skills4();
27134
27217
  init_permissions6();
27135
27218
  }
@@ -27248,7 +27331,7 @@ async function parseHooks(hooksPath, onParseError) {
27248
27331
  return result2;
27249
27332
  }
27250
27333
  var VALID_TYPES;
27251
- var init_hooks5 = __esm({
27334
+ var init_hooks6 = __esm({
27252
27335
  "src/canonical/features/hooks.ts"() {
27253
27336
  init_syntax_error();
27254
27337
  init_fs();
@@ -27329,7 +27412,7 @@ var WILDCARD_MATCHER;
27329
27412
  var init_importer_hooks2 = __esm({
27330
27413
  "src/targets/windsurf/importer-hooks.ts"() {
27331
27414
  init_canonical_paths();
27332
- init_hooks5();
27415
+ init_hooks6();
27333
27416
  init_fs();
27334
27417
  init_constants34();
27335
27418
  init_hook_events();
@@ -27630,6 +27713,7 @@ var init_windsurf2 = __esm({
27630
27713
  init_merge14();
27631
27714
  init_linter32();
27632
27715
  init_lint31();
27716
+ init_hook_events();
27633
27717
  init_import_map_builders();
27634
27718
  init_conversions();
27635
27719
  init_projected_agent_skill();
@@ -27761,6 +27845,7 @@ var init_windsurf2 = __esm({
27761
27845
  },
27762
27846
  emptyImportMessage: "No Windsurf config found (.windsurfrules, .windsurf/rules, .windsurfignore, or .codeiumignore).",
27763
27847
  supportsConversion: { agents: true },
27848
+ hookContextEvents: WINDSURF_HOOK_CONTEXT_EVENTS,
27764
27849
  lintRules: lintRules32,
27765
27850
  lint: {
27766
27851
  commands: lintCommands10,
@@ -28905,6 +28990,23 @@ var init_builtin_targets = __esm({
28905
28990
  }
28906
28991
  });
28907
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
+
28908
29010
  // src/core/generate/engine.ts
28909
29011
  init_builtin_targets();
28910
29012
  init_registry();
@@ -29807,11 +29909,12 @@ async function generateHooksFeature(results, targets, canonical, projectRoot, sc
29807
29909
  const gen = resolveTargetFeatureGenerator(target34, "hooks", config, scope) ?? getDescriptor(target34)?.generators.generateHooks;
29808
29910
  if (!gen) continue;
29809
29911
  const ctx = featureContext(target34, "hooks", scope);
29810
- let outputs = [...gen(canonical, ctx)];
29912
+ const projected = withTargetRecallHooks(canonical, target34);
29913
+ let outputs = [...gen(projected, ctx)];
29811
29914
  const descriptor34 = getBuiltinTargetDefinition(target34) ?? getDescriptor(target34);
29812
29915
  const post = descriptor34?.postProcessHookOutputs;
29813
29916
  if (post) {
29814
- outputs = [...await post(projectRoot, canonical, outputs)];
29917
+ outputs = [...await post(projectRoot, projected, outputs)];
29815
29918
  }
29816
29919
  const options = outputMergeOptions(target34);
29817
29920
  for (const out2 of outputs) {
@@ -29824,7 +29927,7 @@ async function generateScopedSettingsFeature(results, targets, canonical, projec
29824
29927
  const descriptor34 = getBuiltinTargetDefinition(target34) ?? getDescriptor(target34);
29825
29928
  const emit = descriptor34?.emitScopedSettings;
29826
29929
  if (!emit) continue;
29827
- const outputs = emit(canonical, scope, enabledFeatures);
29930
+ const outputs = emit(withTargetRecallHooks(canonical, target34), scope, enabledFeatures);
29828
29931
  if (outputs.length === 0) continue;
29829
29932
  const options = outputMergeOptions(target34);
29830
29933
  for (const out2 of outputs) {
@@ -29867,7 +29970,12 @@ async function generate(ctx) {
29867
29970
  const descriptor34 = getBuiltinTargetDefinition(target34) ?? getDescriptor(target34);
29868
29971
  const scopeExtras = descriptor34?.globalSupport?.scopeExtras;
29869
29972
  if (scopeExtras) {
29870
- const extras = await scopeExtras(canonical, projectRoot, scope, enabledFeatures);
29973
+ const extras = await scopeExtras(
29974
+ withTargetRecallHooks(canonical, target34),
29975
+ projectRoot,
29976
+ scope,
29977
+ enabledFeatures
29978
+ );
29871
29979
  await emitScopeExtras(results, target34, extras, projectRoot);
29872
29980
  }
29873
29981
  }
@@ -31174,7 +31282,7 @@ async function parsePermissions(permissionsPath, onParseError) {
31174
31282
  }
31175
31283
 
31176
31284
  // src/canonical/load/loader.ts
31177
- init_hooks5();
31285
+ init_hooks6();
31178
31286
 
31179
31287
  // src/canonical/features/ignore.ts
31180
31288
  init_fs();
@@ -31223,10 +31331,10 @@ function ruleSlug4(r) {
31223
31331
  function mergeByKey(base, overlay, key) {
31224
31332
  return [...new Map([...base, ...overlay].map((item) => [key(item), item])).values()];
31225
31333
  }
31226
- function mergeCanonicalFiles(base, overlay) {
31334
+ function mergeCanonicalFiles(base, overlay, options = {}) {
31227
31335
  const mcp = mergeMcp7(base.mcp, overlay.mcp);
31228
31336
  const permissions = mergePermissions2(base.permissions, overlay.permissions);
31229
- const hooks = mergeHooks5(base.hooks, overlay.hooks);
31337
+ const hooks = options.hooks === "combine" ? combineHooks(base.hooks, overlay.hooks) : overrideHooks(base.hooks, overlay.hooks);
31230
31338
  const ignore = mergeUniqueStrings(base.ignore, overlay.ignore);
31231
31339
  return {
31232
31340
  rules: mergeByKey(base.rules, overlay.rules, ruleSlug4),
@@ -31265,17 +31373,31 @@ function mergeUniqueStrings(base, overlay) {
31265
31373
  }
31266
31374
  return merged;
31267
31375
  }
31268
- function mergeHooks5(base, overlay) {
31376
+ function overrideHooks(base, overlay) {
31269
31377
  if (!base && !overlay) return null;
31270
31378
  const result2 = {};
31271
- const keys = /* @__PURE__ */ new Set([...Object.keys(base ?? {}), ...Object.keys(overlay ?? {})]);
31272
- for (const k of keys) {
31379
+ for (const k of hookEvents(base, overlay)) {
31273
31380
  const o = overlay?.[k];
31274
- const b = base?.[k];
31275
- 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)))];
31276
31392
  }
31277
31393
  return result2;
31278
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
+ }
31279
31401
 
31280
31402
  // src/config/resolve/native-format-detector.ts
31281
31403
  init_fs();
@@ -32046,7 +32168,7 @@ function gateExtendElevatedArtifacts(canonical, ext) {
32046
32168
  });
32047
32169
  }
32048
32170
  init_mcp();
32049
- init_hooks5();
32171
+ init_hooks6();
32050
32172
 
32051
32173
  // src/install/pack/pack-reader.ts
32052
32174
  init_fs();
@@ -32133,7 +32255,7 @@ async function loadPacksCanonical(abDir) {
32133
32255
  const canonical = await loadPackCanonical(packDir);
32134
32256
  const filtered = filterCanonicalByFeatures(canonical, meta.features);
32135
32257
  const picked = applyExtendPick(filtered, meta.features, meta.pick, meta.name);
32136
- merged = mergeCanonicalFiles(merged, picked);
32258
+ merged = mergeCanonicalFiles(merged, picked, { hooks: "combine" });
32137
32259
  }
32138
32260
  return merged;
32139
32261
  }
@@ -32184,9 +32306,10 @@ async function loadCanonicalWithExtends(config, configDir, options = {}, canonic
32184
32306
  merged = mergeCanonicalFiles(merged, picked);
32185
32307
  }
32186
32308
  const packsCanonical = await loadPacksCanonical(canonicalDir);
32187
- merged = mergeCanonicalFiles(merged, packsCanonical);
32309
+ merged = mergeCanonicalFiles(merged, packsCanonical, { hooks: "combine" });
32188
32310
  const localCanonical = await loadCanonicalFiles(canonicalDir);
32189
32311
  merged = mergeCanonicalFiles(merged, localCanonical);
32312
+ merged = { ...merged, hooks: combineHooks(merged.hooks, packsCanonical.hooks) };
32190
32313
  return { canonical: merged, resolvedExtends };
32191
32314
  }
32192
32315
 
@@ -32465,6 +32588,9 @@ function lintRuleScopeInversion(input) {
32465
32588
  }
32466
32589
  return out2;
32467
32590
  }
32591
+
32592
+ // src/lessons/graph-store.ts
32593
+ init_fs_text_encoding();
32468
32594
  var CURRENT_GRAPH_VERSION = 2;
32469
32595
  var VersionSchema = z.union([z.literal(1), z.literal(2)]);
32470
32596
  var MAX_RULE_LENGTH = 2e3;
@@ -32508,25 +32634,54 @@ var LessonsGraphSchema = z.object({
32508
32634
  function parseGraph(raw) {
32509
32635
  return LessonsGraphSchema.parse(raw);
32510
32636
  }
32637
+ function emptyGraph() {
32638
+ return { version: CURRENT_GRAPH_VERSION, lessons: {}, topics: {}, triggers: {} };
32639
+ }
32511
32640
 
32512
32641
  // src/lessons/graph-store.ts
32513
- var GRAPH_REL_PATH = ".agentsmesh/lessons/lessons.json";
32642
+ var LESSONS_GRAPH_PATH = ".agentsmesh/lessons/lessons.json";
32514
32643
  function graphFilePath(projectRoot) {
32515
- return resolve(projectRoot, GRAPH_REL_PATH);
32644
+ return resolve(projectRoot, LESSONS_GRAPH_PATH);
32516
32645
  }
32517
32646
  function loadLessonsGraph(projectRoot) {
32518
32647
  const raw = readFileSync(graphFilePath(projectRoot), "utf8");
32519
- return parseGraph(JSON.parse(raw));
32648
+ return parseGraph(JSON.parse(stripBom(raw)));
32520
32649
  }
32521
32650
  function tryLoadLessonsGraph(projectRoot) {
32522
32651
  if (!existsSync(graphFilePath(projectRoot))) return null;
32523
32652
  return loadLessonsGraph(projectRoot);
32524
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
+ }
32525
32677
  function saveLessonsGraph(projectRoot, graph) {
32526
32678
  const path = graphFilePath(projectRoot);
32527
32679
  mkdirSync(dirname(path), { recursive: true });
32680
+ const mode = fileMode(path);
32681
+ if (mode !== void 0 && !isWritable(path)) throw new LessonsGraphReadOnlyError();
32528
32682
  const tmp = `${path}.${process.pid}.tmp`;
32529
32683
  writeFileSync(tmp, serializeGraph(graph), "utf8");
32684
+ if (mode !== void 0) chmodSync(tmp, mode);
32530
32685
  renameSync(tmp, path);
32531
32686
  }
32532
32687
  function serializeGraph(graph) {
@@ -32570,9 +32725,76 @@ Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Man
32570
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>\`.
32571
32726
 
32572
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
32573
32789
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
32574
32790
  var MAX_FILES = 2e5;
32575
- 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) {
32576
32798
  const out2 = /* @__PURE__ */ new Set();
32577
32799
  try {
32578
32800
  const stack = [projectRoot];
@@ -32583,14 +32805,14 @@ function listProjectFiles(projectRoot) {
32583
32805
  if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
32584
32806
  } else if (entry.isFile()) {
32585
32807
  out2.add(toRelPath(projectRoot, join(dir, entry.name)));
32586
- if (out2.size > MAX_FILES) return out2;
32808
+ if (out2.size > maxFiles) return null;
32587
32809
  }
32588
32810
  }
32589
32811
  }
32590
32812
  } catch {
32591
32813
  return null;
32592
32814
  }
32593
- return out2;
32815
+ return projectFilesOf(out2, () => readGitPathHistory(projectRoot));
32594
32816
  }
32595
32817
 
32596
32818
  // src/lessons/validate-checks.ts
@@ -32767,6 +32989,265 @@ function collectOrphans(graph, findings) {
32767
32989
  }
32768
32990
  }
32769
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
32770
33251
  function normalizeRule(rule) {
32771
33252
  return rule.trim().replace(/\s+/g, " ").toLowerCase();
32772
33253
  }
@@ -32775,14 +33256,50 @@ function union(base, extra) {
32775
33256
  for (const item of extra) if (!out2.includes(item)) out2.push(item);
32776
33257
  return out2;
32777
33258
  }
32778
- 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) {
32779
33294
  const requested = [
32780
- // Normalize `\` → `/` so a Windows-shaped glob matches: recall relativizes
32781
- // every `--file` to forward slashes (normalizeRecallFile), so a backslash
32782
- // pattern stored raw would silently never fire. Normalizing here also lets
32783
- // 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.
32784
33298
  ...(spec.files ?? []).map(
32785
- (p) => ({ kind: "file_glob", pattern: p.replaceAll("\\", "/") })
33299
+ (p) => ({
33300
+ kind: "file_glob",
33301
+ pattern: projectRoot === void 0 ? p.replaceAll("\\", "/") : projectRelativeGlob(p, projectRoot)
33302
+ })
32786
33303
  ),
32787
33304
  ...(spec.commands ?? []).map((p) => ({ kind: "command_pattern", pattern: p })),
32788
33305
  ...(spec.keywords ?? []).map((p) => ({ kind: "keyword", pattern: p }))
@@ -32839,6 +33356,132 @@ function todayIso() {
32839
33356
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
32840
33357
  }
32841
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
+
32842
33485
  // src/lessons/regex-linear/nfa-compile.ts
32843
33486
  var MAX_NFA_STATES = 2e3;
32844
33487
  var Builder = class {
@@ -33030,6 +33673,9 @@ function escapeClass(c2) {
33030
33673
  var HEX2 = /^[0-9a-fA-F]{2}$/;
33031
33674
  var HEX4 = /^[0-9a-fA-F]{4}$/;
33032
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
+ }
33033
33679
  if (c2 === "x") {
33034
33680
  const hex2 = src.slice(i, i + 2);
33035
33681
  return HEX2.test(hex2) ? { ch: String.fromCharCode(parseInt(hex2, 16)), len: 2 } : { ch: "x", len: 0 };
@@ -33119,7 +33765,7 @@ function parseRegex(src) {
33119
33765
  function parseAtom() {
33120
33766
  const c2 = peek();
33121
33767
  if (c2 === "(") return parseGroup2();
33122
- if (c2 === "[") return parseClass();
33768
+ if (c2 === "[") return parseClass2();
33123
33769
  if (c2 === "\\") return parseEscape();
33124
33770
  if (c2 === ".") {
33125
33771
  i += 1;
@@ -33178,7 +33824,7 @@ function parseRegex(src) {
33178
33824
  }
33179
33825
  return { k: "char", ch: escapeLiteral(c2) };
33180
33826
  }
33181
- function parseClass() {
33827
+ function parseClass2() {
33182
33828
  i += 1;
33183
33829
  const negate = peek() === "^";
33184
33830
  if (negate) i += 1;
@@ -33231,17 +33877,17 @@ function parseRegex(src) {
33231
33877
  }
33232
33878
 
33233
33879
  // src/lessons/regex-linear/index.ts
33234
- var cache = /* @__PURE__ */ new Map();
33880
+ var cache3 = /* @__PURE__ */ new Map();
33235
33881
  function compileLinearMatcher(pattern) {
33236
- const hit = cache.get(pattern);
33237
- 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;
33238
33884
  let matcher;
33239
33885
  try {
33240
33886
  matcher = buildMatcher(parseRegex(pattern));
33241
33887
  } catch {
33242
33888
  matcher = null;
33243
33889
  }
33244
- cache.set(pattern, matcher);
33890
+ cache3.set(pattern, matcher);
33245
33891
  return matcher;
33246
33892
  }
33247
33893
 
@@ -33282,6 +33928,8 @@ function collectDuplicateRules(graph, findings) {
33282
33928
  }
33283
33929
  function collectInvalidTriggerPatterns(graph, findings) {
33284
33930
  for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
33931
+ const globFinding = unsafeGlobFinding(triggerId, trigger);
33932
+ if (globFinding !== null) findings.push(globFinding);
33285
33933
  if (trigger.kind !== "command_pattern") continue;
33286
33934
  try {
33287
33935
  new RegExp(trigger.pattern);
@@ -33289,7 +33937,7 @@ function collectInvalidTriggerPatterns(graph, findings) {
33289
33937
  findings.push({
33290
33938
  level: "error",
33291
33939
  code: "INVALID_TRIGGER_PATTERN",
33292
- 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)}.`,
33293
33941
  triggerId
33294
33942
  });
33295
33943
  continue;
@@ -33298,12 +33946,16 @@ function collectInvalidTriggerPatterns(graph, findings) {
33298
33946
  findings.push({
33299
33947
  level: "error",
33300
33948
  code: "UNSAFE_TRIGGER_PATTERN",
33301
- 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.`,
33302
33950
  triggerId
33303
33951
  });
33304
33952
  }
33305
33953
  }
33306
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
+ }
33307
33959
  function collectBackslashGlobPatterns(graph, findings) {
33308
33960
  for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
33309
33961
  if (trigger.kind !== "file_glob") continue;
@@ -33384,7 +34036,11 @@ function collectTriggerSetCollisions(graph, findings) {
33384
34036
 
33385
34037
  // src/lessons/glob-breadth.ts
33386
34038
  var WILDCARD = /[*?[\]]/;
34039
+ function isNegatedGlob(pattern) {
34040
+ return pattern.startsWith("!");
34041
+ }
33387
34042
  function globNarrowness(pattern) {
34043
+ if (isNegatedGlob(pattern)) return 0;
33388
34044
  const segments = pattern.replaceAll("\\", "/").split("/").filter((segment) => segment !== "" && segment !== ".");
33389
34045
  if (segments.length === 0) return 0;
33390
34046
  let literal = 0;
@@ -33435,6 +34091,15 @@ function isBroadCommandPattern(pattern) {
33435
34091
  }
33436
34092
  return hits > COMMAND_PROBE_CORPUS.length * BROAD_HIT_RATIO;
33437
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
+ }
33438
34103
 
33439
34104
  // src/lessons/validate-liveness.ts
33440
34105
  function activeTriggerIds(graph) {
@@ -33445,32 +34110,42 @@ function activeTriggerIds(graph) {
33445
34110
  }
33446
34111
  return ids;
33447
34112
  }
33448
- function deadFileGlobIds(graph, knownPaths) {
33449
- const active = activeTriggerIds(graph);
34113
+ function fileGlobLiveness(graph, knownPaths, triggerIds) {
34114
+ const active = triggerIds === void 0 ? activeTriggerIds(graph) : new Set(triggerIds);
33450
34115
  const paths2 = [...knownPaths];
33451
- const dead = /* @__PURE__ */ new Set();
34116
+ const missing = [];
33452
34117
  for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
33453
- if (trigger.kind !== "file_glob") continue;
33454
- if (!active.has(triggerId)) continue;
33455
- const isMatch = picomatch2(trigger.pattern, { dot: true });
33456
- 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);
33457
34131
  }
33458
- return dead;
34132
+ return { dead, pending };
33459
34133
  }
33460
34134
  function collectDeadFileGlobs(graph, findings, knownPaths) {
33461
- for (const triggerId of deadFileGlobIds(graph, knownPaths)) {
34135
+ for (const triggerId of fileGlobLiveness(graph, knownPaths).dead) {
33462
34136
  findings.push({
33463
34137
  level: "warning",
33464
34138
  code: "DEAD_FILE_GLOB",
33465
- 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\`.`,
33466
34140
  triggerId
33467
34141
  });
33468
34142
  }
33469
34143
  }
33470
34144
  function fileGlobMatchCount(pattern, knownPaths) {
33471
- const isMatch = picomatch2(pattern, { dot: true });
34145
+ const matcher = getGlobMatcher(pattern);
34146
+ if (matcher === null) return 0;
33472
34147
  let n = 0;
33473
- for (const p of knownPaths) if (isMatch(p)) n += 1;
34148
+ for (const p of knownPaths) if (matcher.test(p)) n += 1;
33474
34149
  return n;
33475
34150
  }
33476
34151
  var RUNNER_ANCHOR = /^\^(pnpm|npm|npx|yarn|bun)\b/;
@@ -33542,7 +34217,7 @@ var STOP = /* @__PURE__ */ new Set([
33542
34217
  "its",
33543
34218
  "must"
33544
34219
  ]);
33545
- function tokenize(text) {
34220
+ function tokenize2(text) {
33546
34221
  return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP.has(t));
33547
34222
  }
33548
34223
  function queryTerms(query) {
@@ -33550,7 +34225,7 @@ function queryTerms(query) {
33550
34225
  if (query.keyword !== void 0) parts.push(query.keyword);
33551
34226
  if (query.file !== void 0) parts.push(query.file);
33552
34227
  if (query.command !== void 0) parts.push(query.command);
33553
- return tokenize(parts.join(" "));
34228
+ return tokenize2(parts.join(" "));
33554
34229
  }
33555
34230
  function buildCorpus(graph) {
33556
34231
  const docs = [];
@@ -33559,7 +34234,7 @@ function buildCorpus(graph) {
33559
34234
  let n = 0;
33560
34235
  for (const lesson of Object.values(graph.lessons)) {
33561
34236
  if (lesson.status !== "active") continue;
33562
- const toks = tokenize(lesson.rule);
34237
+ const toks = tokenize2(lesson.rule);
33563
34238
  n += 1;
33564
34239
  total += toks.length;
33565
34240
  docs.push(toks.length);
@@ -33571,7 +34246,7 @@ function buildCorpus(graph) {
33571
34246
  return { idf, avgdl: total / N || 1 };
33572
34247
  }
33573
34248
  function bm25(terms, ruleText, corpus) {
33574
- const toks = tokenize(ruleText);
34249
+ const toks = tokenize2(ruleText);
33575
34250
  const dl = toks.length || 1;
33576
34251
  const tf = /* @__PURE__ */ new Map();
33577
34252
  for (const t of toks) tf.set(t, (tf.get(t) ?? 0) + 1);
@@ -33588,7 +34263,7 @@ function bm25(terms, ruleText, corpus) {
33588
34263
  // src/lessons/keyword-signal.ts
33589
34264
  var MAX_RECOMMENDED_KEYWORD_TOKENS = 5;
33590
34265
  function isLowSignalKeyword(pattern) {
33591
- return tokenize(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
34266
+ return tokenize2(pattern).length > MAX_RECOMMENDED_KEYWORD_TOKENS;
33592
34267
  }
33593
34268
  function splitRawTokens(pattern) {
33594
34269
  return pattern.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
@@ -33596,7 +34271,7 @@ function splitRawTokens(pattern) {
33596
34271
  function keywordNeedleLosesTokens(pattern) {
33597
34272
  const raw = splitRawTokens(pattern);
33598
34273
  if (raw.length < 2) return false;
33599
- return tokenize(pattern).length !== raw.length;
34274
+ return tokenize2(pattern).length !== raw.length;
33600
34275
  }
33601
34276
 
33602
34277
  // src/lessons/validate-keywords.ts
@@ -33619,7 +34294,7 @@ function collectStopwordKeywords(graph, findings) {
33619
34294
  for (const [triggerId, trigger] of Object.entries(graph.triggers)) {
33620
34295
  if (trigger.kind !== "keyword") continue;
33621
34296
  if (!active.has(triggerId)) continue;
33622
- if (tokenize(trigger.pattern).length !== 0 && !keywordNeedleLosesTokens(trigger.pattern)) {
34297
+ if (tokenize2(trigger.pattern).length !== 0 && !keywordNeedleLosesTokens(trigger.pattern)) {
33623
34298
  continue;
33624
34299
  }
33625
34300
  findings.push({
@@ -33667,7 +34342,6 @@ function validateLessonsGraph(graph, options = {}) {
33667
34342
 
33668
34343
  // src/core/lint/shared/lessons.ts
33669
34344
  var LESSONS_TARGET = "lessons";
33670
- var GRAPH_REL = ".agentsmesh/lessons/lessons.json";
33671
34345
  var ROOT_RULE_REL = ".agentsmesh/rules/_root.md";
33672
34346
  var LESSONS_HEADING = /^## Lessons \(/m;
33673
34347
  function lintLessonsSubsystem(projectRoot, scope) {
@@ -33682,7 +34356,7 @@ function lintLessonsSubsystem(projectRoot, scope) {
33682
34356
  return [
33683
34357
  diag(
33684
34358
  "error",
33685
- GRAPH_REL,
34359
+ LESSONS_GRAPH_PATH,
33686
34360
  `lessons.json failed to load: ${err instanceof Error ? err.message : String(err)}`
33687
34361
  )
33688
34362
  ];
@@ -33690,7 +34364,7 @@ function lintLessonsSubsystem(projectRoot, scope) {
33690
34364
  const knownPaths = listProjectFiles(projectRoot) ?? void 0;
33691
34365
  const report = validateLessonsGraph(graph, { knownPaths });
33692
34366
  for (const finding of report.findings) {
33693
- out2.push(diag(finding.level, GRAPH_REL, `[${finding.code}] ${finding.message}`));
34367
+ out2.push(diag(finding.level, LESSONS_GRAPH_PATH, `[${finding.code}] ${finding.message}`));
33694
34368
  }
33695
34369
  const rootRuleAbs = join(projectRoot, ROOT_RULE_REL);
33696
34370
  const rootRuleBody = existsSync(rootRuleAbs) ? readFileSync(rootRuleAbs, "utf8") : "";
@@ -33712,8 +34386,8 @@ function diag(level, file, message) {
33712
34386
  // src/core/lint/linter.ts
33713
34387
  var EXCLUDE_DIRS = ["node_modules", ".git", "dist", "coverage", ".agentsmesh"];
33714
34388
  function isExcludedProjectPath(rel2) {
33715
- const posix8 = rel2.replaceAll("\\", "/");
33716
- 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}/`));
33717
34391
  }
33718
34392
  async function getProjectFiles(projectRoot) {
33719
34393
  const all = await readDirRecursive(projectRoot);
@@ -34106,14 +34780,22 @@ async function findUntrackedManagedDirFiles(args) {
34106
34780
  return [...found].sort();
34107
34781
  }
34108
34782
 
34783
+ // src/lessons/conflict-markers.ts
34784
+ function hasConflictMarkers(text) {
34785
+ return /^(?:<{7}|>{7})(?: |\r?$)/m.test(text);
34786
+ }
34787
+
34109
34788
  // src/core/check/lock-sync.ts
34789
+ init_fs();
34110
34790
  async function checkLockSync(opts) {
34111
34791
  const { config, configDir, canonicalDir, rootBase, scope = "project" } = opts;
34112
34792
  const lock = await readLock(canonicalDir);
34113
34793
  if (lock === null) {
34794
+ const text = await readFileSafe(join(canonicalDir, ".lock"));
34114
34795
  return {
34115
34796
  inSync: false,
34116
34797
  hasLock: false,
34798
+ lockConflict: text !== null && hasConflictMarkers(text),
34117
34799
  canonicalDrift: false,
34118
34800
  outputDrift: false,
34119
34801
  modified: [],
@@ -34183,6 +34865,7 @@ async function checkLockSync(opts) {
34183
34865
  return {
34184
34866
  inSync,
34185
34867
  hasLock: true,
34868
+ lockConflict: false,
34186
34869
  canonicalDrift,
34187
34870
  outputDrift,
34188
34871
  modified,
@@ -34458,6 +35141,27 @@ var UnknownTopicError = class extends Error {
34458
35141
  }
34459
35142
  code = "UNKNOWN_TOPIC";
34460
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
+ };
34461
35165
  var RuleTooLongError = class extends Error {
34462
35166
  constructor(length, max) {
34463
35167
  super(
@@ -34488,6 +35192,9 @@ var UnrecallableLessonError = class extends Error {
34488
35192
  }
34489
35193
  code = "UNRECALLABLE_LESSON";
34490
35194
  };
35195
+ function codePointLength(text) {
35196
+ return [...text].length;
35197
+ }
34491
35198
 
34492
35199
  // src/lessons/trigger-effectiveness.ts
34493
35200
  function ineffectiveTriggers(graph, triggerIds) {
@@ -34502,7 +35209,7 @@ function ineffectiveTriggers(graph, triggerIds) {
34502
35209
  }
34503
35210
  function ineffectiveReason(kind, pattern) {
34504
35211
  if (kind === "keyword") {
34505
- if (tokenize(pattern).length === 0) {
35212
+ if (tokenize2(pattern).length === 0) {
34506
35213
  return "keyword has no matchable token after stopword filtering \u2014 it cannot fire on the mandatory --file/--cmd recall path";
34507
35214
  }
34508
35215
  if (keywordNeedleLosesTokens(pattern)) {
@@ -34535,9 +35242,19 @@ function blockingDeadTriggers(graph, triggerIds) {
34535
35242
  function assertRuleShape(rule) {
34536
35243
  const trimmed = rule.trim();
34537
35244
  if (trimmed.length === 0) throw new EmptyRuleError();
34538
- 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);
34539
35247
  return trimmed;
34540
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
+ }
34541
35258
  function skipsTriggerGates(input, options) {
34542
35259
  return options.allowNoTrigger === true || input.scope === "always";
34543
35260
  }
@@ -34553,10 +35270,30 @@ function assertTriggerInputs(input, options, existingTriggerCount) {
34553
35270
  if (broad !== void 0) throw new BroadCommandPatternError(broad);
34554
35271
  }
34555
35272
  }
34556
- 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) {
34557
35293
  const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
34558
- if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
34559
- throw new UnrecallableLessonError(blockingDead);
35294
+ const dead = [...dropped, ...blockingDead];
35295
+ if (dead.length > 0 && blockingDead.length === resultingTriggers.length) {
35296
+ throw new UnrecallableLessonError(dead);
34560
35297
  }
34561
35298
  }
34562
35299
 
@@ -34565,7 +35302,7 @@ var WIDE_GLOB_MATCH_COUNT = 40;
34565
35302
  var MAX_RECOMMENDED_TRIGGERS = 8;
34566
35303
  function isBroadGlob(pattern) {
34567
35304
  const p = pattern.trim();
34568
- if (p === "*" || p === "**") return true;
35305
+ if (p === "*" || p === "**" || isNegatedGlob(p)) return true;
34569
35306
  if (!p.includes("**")) return false;
34570
35307
  const basename78 = p.slice(p.lastIndexOf("/") + 1);
34571
35308
  return basename78.startsWith("*");
@@ -34609,12 +35346,19 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
34609
35346
  });
34610
35347
  }
34611
35348
  if (knownPaths !== void 0) {
34612
- const dead = deadFileGlobIds(graph, knownPaths);
34613
- 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);
34614
35351
  if (deadHere.length > 0) {
34615
35352
  warnings.push({
34616
35353
  code: "DEAD_GLOB",
34617
- 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.`
34618
35362
  });
34619
35363
  }
34620
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);
@@ -34627,18 +35371,21 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
34627
35371
  }
34628
35372
  return warnings;
34629
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
+ }
34630
35377
 
34631
35378
  // src/lessons/capture-near-duplicate.ts
34632
35379
  var NEAR_DUPLICATE_THRESHOLD = 0.6;
34633
35380
  function nearDuplicateWarning(graph, lessonId) {
34634
35381
  const subject = graph.lessons[lessonId];
34635
35382
  if (subject === void 0) return null;
34636
- const subjectTokens = new Set(tokenize(subject.rule));
35383
+ const subjectTokens = new Set(tokenize2(subject.rule));
34637
35384
  if (subjectTokens.size === 0) return null;
34638
35385
  let best = null;
34639
35386
  for (const [id, other] of Object.entries(graph.lessons)) {
34640
35387
  if (id === lessonId || other.status !== "active") continue;
34641
- const otherTokens = new Set(tokenize(other.rule));
35388
+ const otherTokens = new Set(tokenize2(other.rule));
34642
35389
  if (otherTokens.size === 0) continue;
34643
35390
  const score = jaccard(subjectTokens, otherTokens);
34644
35391
  if (score >= NEAR_DUPLICATE_THRESHOLD && (best === null || score > best.score)) {
@@ -34656,63 +35403,375 @@ function jaccard(a, b) {
34656
35403
  for (const t of a) if (b.has(t)) intersection += 1;
34657
35404
  return intersection / (a.size + b.size - intersection);
34658
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
+ }
34659
35426
 
34660
35427
  // src/utils/filesystem/process-lock.ts
34661
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
34662
35703
  var DEFAULT_STALE_MS = 6 * 60 * 60 * 1e3;
34663
35704
  var DEFAULT_RETRIES = 30;
34664
35705
  var DEFAULT_RETRY_DELAY_MS = 200;
34665
- var YOUNG_LOCK_GRACE_MS = 2e3;
35706
+ var MAX_IMMEDIATE_RETRIES = 100;
35707
+ var MAX_TRANSIENT_ERRORS = 5;
34666
35708
  async function acquireProcessLock(lockPath, opts = {}) {
34667
35709
  const retries = opts.retries ?? DEFAULT_RETRIES;
34668
- const delay = opts.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
34669
- const stale = opts.staleMs ?? DEFAULT_STALE_MS;
35710
+ const staleMs = opts.staleMs ?? DEFAULT_STALE_MS;
34670
35711
  await mkdir(dirname(lockPath), { recursive: true });
35712
+ const procStart = await selfIdentity();
35713
+ const probes = /* @__PURE__ */ new Map();
34671
35714
  let attempt = 0;
35715
+ let immediate = 0;
35716
+ const waitingSince = Date.now();
35717
+ let noticed = false;
35718
+ let transient = 0;
34672
35719
  while (true) {
34673
- const acquired = await tryAcquire(lockPath);
34674
- if (acquired) return acquired;
34675
- const existing = await inspectLock(lockPath);
34676
- if (existing !== "young" && isStale(existing, stale)) {
34677
- 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++;
34678
35733
  continue;
34679
35734
  }
34680
35735
  if (attempt >= retries) {
34681
- const holder = existing === "young" ? null : existing;
34682
- throw new LockAcquisitionError(lockPath, describeHolder(holder), { label: opts.label });
35736
+ throw new LockAcquisitionError(lockPath, describeHolder(state), { label: opts.label });
34683
35737
  }
34684
35738
  attempt++;
34685
- 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));
34686
35745
  }
34687
35746
  }
34688
- async function tryAcquire(lockPath) {
34689
- try {
34690
- await mkdir(lockPath, { recursive: false });
34691
- } catch (err) {
34692
- if (err.code === "EEXIST") return null;
34693
- throw err;
34694
- }
34695
- const metadataPath = join(lockPath, "holder.json");
34696
- 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 {
34697
35762
  pid: process.pid,
34698
35763
  started: Date.now(),
34699
- hostname: getHostname()
35764
+ hostname: hostname(),
35765
+ token: randomUUID(),
35766
+ ...procStart === null ? {} : { procStart }
34700
35767
  };
34701
- try {
34702
- await writeFile(metadataPath, JSON.stringify(metadata), "utf-8");
34703
- } catch (error) {
34704
- await rm(lockPath, { recursive: true, force: true }).catch(() => {
34705
- });
34706
- throw error;
34707
- }
35768
+ }
35769
+ function holdLock(lockPath, token) {
34708
35770
  let released = false;
34709
35771
  const cleanup = () => {
34710
35772
  if (released) return;
34711
35773
  released = true;
34712
- try {
34713
- rmSync(lockPath, { recursive: true, force: true });
34714
- } catch {
34715
- }
35774
+ releaseOwnedSync(lockPath, token);
34716
35775
  };
34717
35776
  const signalHandler = (signal) => {
34718
35777
  cleanup();
@@ -34721,76 +35780,69 @@ async function tryAcquire(lockPath) {
34721
35780
  process.once("SIGINT", signalHandler);
34722
35781
  process.once("SIGTERM", signalHandler);
34723
35782
  process.once("exit", cleanup);
34724
- return async () => {
35783
+ const release = async () => {
34725
35784
  if (released) return;
34726
35785
  released = true;
34727
35786
  process.off("SIGINT", signalHandler);
34728
35787
  process.off("SIGTERM", signalHandler);
34729
35788
  process.off("exit", cleanup);
34730
- await rm(lockPath, { recursive: true, force: true }).catch(() => {
35789
+ await evictOwners(lockPath, [token]).catch(() => {
34731
35790
  });
34732
35791
  };
34733
- }
34734
- async function inspectLock(lockPath) {
34735
- try {
34736
- const raw = await readFile(join(lockPath, "holder.json"), "utf-8");
34737
- const parsed = JSON.parse(raw);
34738
- if (!isLockMetadata(parsed)) return null;
34739
- return parsed;
34740
- } catch {
34741
- try {
34742
- const info = await stat(lockPath);
34743
- const ageMs = Date.now() - info.mtimeMs;
34744
- if (ageMs < YOUNG_LOCK_GRACE_MS) return "young";
34745
- } catch {
34746
- }
34747
- return null;
34748
- }
34749
- }
34750
- function isStale(meta, staleMs) {
34751
- if (!meta) return true;
34752
- const sameHost = !meta.hostname || meta.hostname === getHostname();
34753
- if (sameHost && !isProcessAlive(meta.pid)) return true;
34754
- return Date.now() - meta.started > staleMs;
34755
- }
34756
- function isProcessAlive(pid) {
34757
- if (!Number.isInteger(pid) || pid <= 0) return false;
34758
- try {
34759
- process.kill(pid, 0);
34760
- return true;
34761
- } catch (err) {
34762
- return err.code === "EPERM";
34763
- }
34764
- }
34765
- function describeHolder(meta) {
34766
- if (!meta) return "unknown (unreadable lock metadata)";
34767
- const host = meta.hostname ? `${meta.hostname}:` : "";
34768
- return `${host}pid ${meta.pid} (running ${Date.now() - meta.started}ms)`;
34769
- }
34770
- function isLockMetadata(value) {
34771
- if (typeof value !== "object" || value === null) return false;
34772
- const v = value;
34773
- return typeof v.pid === "number" && typeof v.started === "number";
34774
- }
34775
- function getHostname() {
34776
- return hostname();
35792
+ const isHeld = async () => !released && existsSync(ownerPath(lockPath, token));
35793
+ return Object.assign(release, { isHeld });
34777
35794
  }
34778
35795
 
34779
35796
  // src/lessons/lessons-lock.ts
34780
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
+ });
34781
35805
  function lessonsLockPath(projectRoot) {
34782
35806
  return resolve(projectRoot, ".agentsmesh/lessons", LESSONS_LOCK_FILENAME);
34783
35807
  }
34784
35808
  async function acquireLessonsLock(projectRoot, opts = {}) {
34785
- const lockPath = lessonsLockPath(projectRoot);
34786
- await mkdir(dirname(lockPath), { recursive: true });
34787
- 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();
34788
35832
  }
34789
35833
 
34790
35834
  // src/lessons/mutate.ts
34791
- function emptyGraph() {
34792
- return { version: CURRENT_GRAPH_VERSION, lessons: {}, topics: {}, triggers: {} };
34793
- }
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
+ };
34794
35846
  function findingKey(f) {
34795
35847
  return `${f.code}|${f.triggerId ?? ""}|${f.lessonId ?? ""}`;
34796
35848
  }
@@ -34800,6 +35852,7 @@ function errorSignatures(report) {
34800
35852
  async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
34801
35853
  const release = await acquireLessonsLock(projectRoot, { retries: options.retries });
34802
35854
  try {
35855
+ sweepLessonsLeftovers(projectRoot);
34803
35856
  const graph = tryLoadLessonsGraph(projectRoot) ?? emptyGraph();
34804
35857
  const baseline = errorSignatures(validateLessonsGraph(graph));
34805
35858
  const result2 = await mutator(graph);
@@ -34808,12 +35861,10 @@ async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
34808
35861
  (f) => f.level === "error" && !baseline.has(findingKey(f))
34809
35862
  );
34810
35863
  if (introduced.length > 0) {
34811
- const errors = introduced.map((f) => `${f.code}: ${f.message}`).join("; ");
34812
- throw new Error(
34813
- `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.)`
34814
- );
35864
+ throw new LessonsWriteRefusedError(introduced);
34815
35865
  }
34816
35866
  graph.version = CURRENT_GRAPH_VERSION;
35867
+ await assertLessonsLockHeld(release);
34817
35868
  saveLessonsGraph(projectRoot, graph);
34818
35869
  return result2;
34819
35870
  } finally {
@@ -34827,50 +35878,41 @@ async function mutateLessonsGraph(projectRoot, mutator, options = {}) {
34827
35878
 
34828
35879
  // src/lessons/add.ts
34829
35880
  async function addLesson(projectRoot, input, options = {}) {
34830
- return mutateLessonsGraph(projectRoot, (graph) => addLessonInto(graph, input, options), {
34831
- retries: options.retries
34832
- });
35881
+ return mutateLessonsGraph(
35882
+ projectRoot,
35883
+ (graph) => addLessonInto(graph, input, { ...options, projectRoot }),
35884
+ { retries: options.retries }
35885
+ );
34833
35886
  }
34834
35887
  function addLessonInto(graph, input, options) {
34835
35888
  const ruleKey2 = normalizeRule(input.rule);
34836
35889
  const trimmedRule = assertRuleShape(input.rule);
34837
35890
  const existingId = findExistingLessonByRule(graph, ruleKey2);
34838
- const isNewTopic = graph.topics[input.topic] === void 0;
34839
- if (isNewTopic) {
34840
- if (options.allowNewTopic !== true) throw new UnknownTopicError(input.topic);
34841
- if (options.topicSummary === void 0 || options.topicSummary.length === 0) {
34842
- throw new Error(`addLesson: new topic "${input.topic}" requires topicSummary.`);
34843
- }
34844
- graph.topics[input.topic] = { summary: options.topicSummary };
34845
- }
35891
+ const isNewTopic = ensureTopic(graph, input.topic, options);
34846
35892
  const existing = existingId !== null ? graph.lessons[existingId] : void 0;
34847
35893
  assertTriggerInputs(input, options, existing?.triggers.length ?? 0);
34848
- 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);
34849
35896
  if (!skipsTriggerGates(input, options)) {
34850
- assertRecallable(
34851
- graph,
34852
- existing === void 0 ? triggerIds : union(existing.triggers, triggerIds)
34853
- );
35897
+ const resulting = existing === void 0 ? triggerIds : union(existing.triggers, triggerIds);
35898
+ assertRecallable(graph, resulting, dropped);
34854
35899
  }
34855
- if (existingId !== null) {
34856
- const existing2 = graph.lessons[existingId];
34857
- graph.lessons[existingId] = {
34858
- ...existing2,
34859
- topics: union(existing2.topics, [input.topic]),
34860
- triggers: union(existing2.triggers, triggerIds),
34861
- evidence: union(existing2.evidence, input.evidence ?? []),
34862
- ...existing2.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
34863
- // Re-capturing a rule with --scope always promotes it to always-on.
34864
- ...input.scope === "always" ? { scope: "always" } : {}
34865
- };
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;
34866
35904
  return {
34867
35905
  id: existingId,
34868
35906
  isNewLesson: false,
34869
35907
  isNewTopic,
34870
35908
  newTriggerIds,
35909
+ changes: describeUpsert(existing, updated),
34871
35910
  // Near-duplicate detection is meaningless on an upsert (the lesson IS the
34872
35911
  // match), so only DEAD_GLOB/hygiene warnings apply here.
34873
- warnings: inspectCapturedLesson(graph, existingId, options.knownPaths)
35912
+ warnings: [
35913
+ ...inspectCapturedLesson(graph, existingId, options.knownPaths),
35914
+ ...droppedWarnings
35915
+ ]
34874
35916
  };
34875
35917
  }
34876
35918
  const id = makeLessonId(graph, input.topic, ruleKey2);
@@ -34878,29 +35920,26 @@ function addLessonInto(graph, input, options) {
34878
35920
  rule: trimmedRule,
34879
35921
  topics: [input.topic],
34880
35922
  triggers: triggerIds,
34881
- evidence: input.evidence === void 0 ? [] : [...input.evidence],
35923
+ evidence: [...new Set(input.evidence ?? [])],
34882
35924
  status: "active",
34883
35925
  createdAt: input.createdAt ?? todayIso(),
34884
35926
  ...input.rationale === void 0 ? {} : { rationale: input.rationale },
34885
35927
  ...input.scope === "always" ? { scope: "always" } : {}
34886
35928
  };
34887
- const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
34888
35929
  const nearDup = nearDuplicateWarning(graph, id);
34889
35930
  return {
34890
35931
  id,
34891
35932
  isNewLesson: true,
34892
35933
  isNewTopic,
34893
35934
  newTriggerIds,
34894
- warnings: nearDup === null ? warnings : [...warnings, nearDup]
35935
+ changes: [],
35936
+ warnings: [
35937
+ ...inspectCapturedLesson(graph, id, options.knownPaths),
35938
+ ...nearDup === null ? [] : [nearDup],
35939
+ ...droppedWarnings
35940
+ ]
34895
35941
  };
34896
35942
  }
34897
- function findExistingLessonByRule(graph, ruleKey2) {
34898
- for (const [id, lesson] of Object.entries(graph.lessons)) {
34899
- if (lesson.status !== "active") continue;
34900
- if (normalizeRule(lesson.rule) === ruleKey2) return id;
34901
- }
34902
- return null;
34903
- }
34904
35943
 
34905
35944
  // src/lessons/import-legacy-merge.ts
34906
35945
  async function mergeLegacy(projectRoot, paths2, specs, summaryByTopic, options) {
@@ -34933,19 +35972,33 @@ async function mergeLegacy(projectRoot, paths2, specs, summaryByTopic, options)
34933
35972
  triggerCount: addedTriggers.size
34934
35973
  };
34935
35974
  }
34936
-
34937
- // src/lessons/import-legacy.ts
34938
- var LessonsGraphExistsError = class extends Error {
34939
- code = "LESSONS_GRAPH_EXISTS";
34940
- constructor() {
34941
- super("importLegacyLessons: a non-empty lessons.json already exists; pass force to overwrite.");
34942
- 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";
34943
35983
  }
34944
35984
  };
34945
- async function importLegacyLessons(projectRoot, options) {
34946
- const paths2 = lessonsPaths(projectRoot);
34947
- const indexRaw = readFileSync(paths2.index, "utf8");
34948
- 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
+ );
34949
36002
  const topics = {};
34950
36003
  const triggersById = /* @__PURE__ */ new Map();
34951
36004
  const triggerIdByKey = /* @__PURE__ */ new Map();
@@ -34956,14 +36009,15 @@ async function importLegacyLessons(projectRoot, options) {
34956
36009
  topics[cluster.topic] = { summary: cluster.summary };
34957
36010
  summaryByTopic.set(cluster.topic, cluster.summary);
34958
36011
  const clusterTriggerIds = collectClusterTriggerIds(cluster, triggersById, triggerIdByKey);
34959
- const topicFile = join(projectRoot, cluster.file);
36012
+ const topicFile = await resolveLegacyTopicPath(projectRoot, cluster.file);
34960
36013
  if (!existsSync(topicFile)) {
34961
36014
  throw new Error(
34962
- `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).`
34963
36016
  );
34964
36017
  }
34965
- const topicMarkdown = readFileSync(topicFile, "utf8");
34966
- for (const { index: ruleIndex, body, evidence } of parseRulesSection(topicMarkdown)) {
36018
+ for (const { index: ruleIndex, body, evidence } of parseRulesSection(
36019
+ readFileSync(topicFile, "utf8")
36020
+ )) {
34967
36021
  const lessonEvidence = [
34968
36022
  `legacy:${cluster.file}#rule-${ruleIndex}`,
34969
36023
  ...evidence.map((e) => `legacy:${e}`)
@@ -34974,7 +36028,7 @@ async function importLegacyLessons(projectRoot, options) {
34974
36028
  triggers: clusterTriggerIds,
34975
36029
  evidence: lessonEvidence,
34976
36030
  status: "active",
34977
- createdAt: options.migratedAt
36031
+ createdAt: migratedAt
34978
36032
  };
34979
36033
  specs.push({
34980
36034
  rule: body,
@@ -34985,22 +36039,43 @@ async function importLegacyLessons(projectRoot, options) {
34985
36039
  keywords: cluster.triggers.keywords
34986
36040
  },
34987
36041
  evidence: lessonEvidence,
34988
- createdAt: options.migratedAt
36042
+ createdAt: migratedAt
34989
36043
  });
34990
36044
  }
34991
36045
  }
34992
- 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);
34993
36063
  return mergeLegacy(projectRoot, paths2, specs, summaryByTopic, options);
34994
- const triggers = Object.fromEntries(triggersById.entries());
34995
- 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
+ }
34996
36069
  const populated = Object.keys(g.lessons).length > 0 || Object.keys(g.topics).length > 0 || Object.keys(g.triggers).length > 0;
34997
36070
  if (options.force !== true && populated) {
34998
36071
  throw new LessonsGraphExistsError();
34999
36072
  }
36073
+ const source = await readLegacySource(projectRoot, options.migratedAt);
35000
36074
  g.version = CURRENT_GRAPH_VERSION;
35001
- g.lessons = lessons;
35002
- g.topics = topics;
35003
- g.triggers = triggers;
36075
+ g.lessons = source.lessons;
36076
+ g.topics = source.topics;
36077
+ g.triggers = source.triggers;
36078
+ return source;
35004
36079
  });
35005
36080
  const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths2.base);
35006
36081
  return {
@@ -35008,7 +36083,7 @@ async function importLegacyLessons(projectRoot, options) {
35008
36083
  deletedPaths,
35009
36084
  topicCount: Object.keys(topics).length,
35010
36085
  lessonCount: Object.keys(lessons).length,
35011
- triggerCount: triggersById.size
36086
+ triggerCount: Object.keys(triggers).length
35012
36087
  };
35013
36088
  }
35014
36089
 
@@ -35018,13 +36093,14 @@ async function maybeAutoMigrateLessons(projectRoot) {
35018
36093
  const paths2 = lessonsPaths(projectRoot);
35019
36094
  if (!existsSync(paths2.index)) return false;
35020
36095
  try {
35021
- await importLegacyLessons(projectRoot, { migratedAt: todayIso() });
36096
+ await importLegacyLessons(projectRoot, { migratedAt: todayIso(), requireAbsentGraph: true });
35022
36097
  return true;
35023
36098
  } catch (err) {
35024
36099
  if (err instanceof LessonsGraphExistsError) return false;
35025
36100
  throw err;
35026
36101
  }
35027
36102
  }
36103
+ new Int32Array(new SharedArrayBuffer(4));
35028
36104
 
35029
36105
  // src/lessons/keyword-match.ts
35030
36106
  function deriveHaystackTokens(query) {
@@ -35058,7 +36134,7 @@ function containsRun(needle, hay) {
35058
36134
  return false;
35059
36135
  }
35060
36136
  function keywordMatches(pattern, query) {
35061
- const needle = tokenize(pattern);
36137
+ const needle = tokenize2(pattern);
35062
36138
  if (query.keyword !== void 0 && containsRun(needle, splitTokens(query.keyword.toLowerCase()))) {
35063
36139
  return true;
35064
36140
  }
@@ -35067,6 +36143,7 @@ function keywordMatches(pattern, query) {
35067
36143
 
35068
36144
  // src/lessons/query.ts
35069
36145
  var COMMAND_MATCH_BUDGET = 5e6;
36146
+ var GLOB_MATCH_BUDGET = 2e6;
35070
36147
  function queryLessons(graph, query) {
35071
36148
  if (query.file === void 0 && query.command === void 0 && query.keyword === void 0) {
35072
36149
  return [];
@@ -35089,9 +36166,12 @@ function collectMatchedTriggersByKind(graph, query) {
35089
36166
  command_pattern: /* @__PURE__ */ new Set(),
35090
36167
  keyword: /* @__PURE__ */ new Set()
35091
36168
  };
35092
- const budget = { remaining: COMMAND_MATCH_BUDGET };
36169
+ const budgets = {
36170
+ command: { remaining: COMMAND_MATCH_BUDGET },
36171
+ glob: { remaining: GLOB_MATCH_BUDGET }
36172
+ };
35093
36173
  for (const [id, trigger] of Object.entries(graph.triggers)) {
35094
- if (triggerMatches(trigger, query, budget)) byKind[trigger.kind].add(id);
36174
+ if (triggerMatches(trigger, query, budgets)) byKind[trigger.kind].add(id);
35095
36175
  }
35096
36176
  return byKind;
35097
36177
  }
@@ -35099,21 +36179,29 @@ function collectMatchedTriggerIds(graph, query) {
35099
36179
  const { file_glob, command_pattern, keyword } = collectMatchedTriggersByKind(graph, query);
35100
36180
  return /* @__PURE__ */ new Set([...file_glob, ...command_pattern, ...keyword]);
35101
36181
  }
35102
- function triggerMatches(trigger, query, budget) {
36182
+ function triggerMatches(trigger, query, budgets) {
35103
36183
  switch (trigger.kind) {
35104
- case "file_glob":
36184
+ case "file_glob": {
35105
36185
  if (query.file === void 0) return false;
35106
- 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
+ }
35107
36189
  case "command_pattern": {
35108
36190
  if (query.command === void 0) return false;
35109
36191
  const matcher = getCommandMatcher(trigger.pattern);
35110
- return matcher !== null && matcher.test(query.command, budget);
36192
+ return matcher !== null && matcher.test(query.command, budgets.command);
35111
36193
  }
35112
36194
  case "keyword":
35113
36195
  return keywordMatches(trigger.pattern, query);
35114
36196
  }
35115
36197
  }
35116
36198
 
36199
+ // src/lessons/telemetry.ts
36200
+ init_fs_text_encoding();
36201
+
36202
+ // src/lessons/log-record-guards.ts
36203
+ init_guards();
36204
+
35117
36205
  // src/lessons/telemetry.ts
35118
36206
  function recallLogPath(projectRoot) {
35119
36207
  return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
@@ -35250,14 +36338,20 @@ function applyCaps(ranked, options) {
35250
36338
  }
35251
36339
  return out2;
35252
36340
  }
36341
+
36342
+ // src/lessons/recall-config.ts
36343
+ init_fs_text_encoding();
35253
36344
  function defaultLessonsConfig() {
35254
36345
  return {
35255
36346
  recallLimit: DEFAULT_RECALL_LIMIT,
35256
36347
  recallMaxTokens: DEFAULT_RECALL_MAX_TOKENS,
35257
36348
  autoPrune: false,
35258
- telemetry: false
36349
+ telemetry: false,
36350
+ outcomeLog: true
35259
36351
  };
35260
36352
  }
36353
+
36354
+ // src/lessons/outcome-log.ts
35261
36355
  function outcomeLogPath(projectRoot) {
35262
36356
  return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
35263
36357
  }
@@ -35273,19 +36367,17 @@ async function mergeLessons(projectRoot, loserId, keeperId, options = {}) {
35273
36367
  }
35274
36368
  function mergeInto(graph, loserId, keeperId) {
35275
36369
  if (loserId === keeperId) {
35276
- throw new Error(`mergeLessons: cannot merge lesson "${loserId}" into itself.`);
36370
+ throw new Error(`Cannot merge lesson "${loserId}" into itself.`);
35277
36371
  }
35278
36372
  const loser = graph.lessons[loserId];
35279
- if (loser === void 0) throw new Error(`mergeLessons: unknown lesson "${loserId}".`);
36373
+ if (loser === void 0) throw new Error(`Unknown lesson "${loserId}".`);
35280
36374
  const keeper = graph.lessons[keeperId];
35281
- if (keeper === void 0) throw new Error(`mergeLessons: unknown lesson "${keeperId}".`);
36375
+ if (keeper === void 0) throw new Error(`Unknown lesson "${keeperId}".`);
35282
36376
  if (keeper.status !== "active") {
35283
- throw new Error(`mergeLessons: keeper "${keeperId}" is not active (status: ${keeper.status}).`);
36377
+ throw new Error(`Keeper "${keeperId}" is not active (status: ${keeper.status}).`);
35284
36378
  }
35285
36379
  if (loser.status !== "active") {
35286
- throw new Error(
35287
- `mergeLessons: loser "${loserId}" is already ${loser.status}; nothing to merge.`
35288
- );
36380
+ throw new Error(`Loser "${loserId}" is already ${loser.status}; nothing to merge.`);
35289
36381
  }
35290
36382
  graph.lessons[keeperId] = {
35291
36383
  ...keeper,
@@ -35340,9 +36432,123 @@ async function stripMarkersInGraph(projectRoot, options = {}) {
35340
36432
  }
35341
36433
  init_recall_hook_scaffold();
35342
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
+
35343
36463
  // src/lessons/merge-driver-setup.ts
35344
36464
  var LESSONS_MERGE_DRIVER = "agentsmesh-lessons";
35345
- 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
+ }
35346
36552
 
35347
36553
  // src/utils/filesystem/gitattributes.ts
35348
36554
  init_fs();
@@ -35483,7 +36689,8 @@ At least one _effective_ trigger is required (or \`--scope always\` for a univer
35483
36689
  the capture is rejected (\`UNRECALLABLE_LESSON\`); prefer \`--trigger-file\`. No shell \u2192 MCP \`lessons_query\`,
35484
36690
  \`lessons_add\`, \`lessons_topics\`, \`lessons_show\`, \`lessons_deprecate\`. Run
35485
36691
  \`agentsmesh lessons --help\` for every subcommand and flag: query, add, topics, show,
35486
- 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.
35487
36694
 
35488
36695
  ### Rationalization Prevention \u2014 these excuses mean STOP
35489
36696
 
@@ -35527,11 +36734,14 @@ async function scaffoldLessons(projectRoot) {
35527
36734
  const gitignoreUpdated = await ensureGitignoreEntries(projectRoot, [
35528
36735
  toRelPath(projectRoot, recallLogPath(projectRoot)),
35529
36736
  toRelPath(projectRoot, captureLogPath(projectRoot)),
35530
- toRelPath(projectRoot, outcomeLogPath(projectRoot))
36737
+ toRelPath(projectRoot, outcomeLogPath(projectRoot)),
36738
+ `${toRelPath(projectRoot, lessonsLockPath(projectRoot))}/`,
36739
+ `${toRelPath(projectRoot, paths2.base)}/*.tmp`
35531
36740
  ]);
35532
36741
  const gitattributesUpdated = await ensureGitattributesEntries(projectRoot, [
35533
36742
  LESSONS_GITATTRIBUTES_ENTRY
35534
36743
  ]);
36744
+ const mergeDriver = ensureLessonsMergeDriver(projectRoot);
35535
36745
  return {
35536
36746
  created,
35537
36747
  updated,
@@ -35539,7 +36749,9 @@ async function scaffoldLessons(projectRoot) {
35539
36749
  rootRuleUpdated,
35540
36750
  gitignoreUpdated,
35541
36751
  gitattributesUpdated,
35542
- recallHookInjected
36752
+ recallHookInjected,
36753
+ mergeDriver,
36754
+ recallHookTeamHint: recallHookTeamHint(projectRoot)
35543
36755
  };
35544
36756
  }
35545
36757
  function seedLessonsSkill(projectRoot, created, updated, skipped) {