agentsmesh 0.29.0 → 0.30.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.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { AgentsMeshError, AgentsMeshErrorCode, CheckLockSyncOptions, ComputeDiffResult, ConfigNotFoundError, ConfigValidationError, DiffEntry, DiffSummary, FileSystemError, GenerateContext, GenerationError, ImportError, LintOptions, LintResult, LoadProjectContextOptions, LockAcquisitionError, LockSyncReport, ProjectContext, RemoteFetchError, TargetNotFoundError, check, computeDiff, diff, formatDiffSummary, generate, importFrom, lint, loadConfig, loadConfigFromDirectory, loadProjectContext, resolveOutputCollisions } from './engine.js';
2
2
  export { LoadCanonicalOptions, loadCanonical, loadCanonicalFiles } from './canonical.js';
3
3
  export { getAllDescriptors, getDescriptor, getTargetCatalog, registerTargetDescriptor } from './targets.js';
4
- export { A as AddLessonInput, a as AddLessonOptions, b as AddLessonResult, c as AddLessonTriggers, D as DEFAULT_RECALL_LIMIT, I as ImportLegacyOptions, d as ImportLegacyReport, L as LESSONS_PROCEDURAL_RULE, e as Lesson, f as LessonStatus, g as LessonsGraph, h as LessonsGraphSchema, i as LessonsPaths, j as LessonsQuery, M as MatchedLesson, k as MergeLessonsOptions, l as MergeLessonsResult, m as MutateOptions, R as RankOptions, n as RankReason, o as RankedLesson, S as ScaffoldLessonsResult, p as StripMarkersOptions, q as StripMarkersReport, T as Topic, r as Trigger, s as TriggerKind, U as UnknownTopicError, V as ValidationFinding, t as ValidationLevel, u as ValidationReport, v as acquireLessonsLock, w as addLesson, x as graphFilePath, y as importLegacyLessons, z as lessonsPaths, B as loadLessonsGraph, C as mergeLessons, E as mutateLessonsGraph, F as parseGraph, G as queryLessons, H as rankLessons, J as scaffoldLessons, K as serializeGraph, N as stripLegacyMarkers, O as stripMarkersInGraph, P as toRelPath, Q as tryLoadLessonsGraph, W as validateLessonsGraph } from './init-B0aI-g8W.js';
4
+ export { A as AddLessonInput, a as AddLessonOptions, b as AddLessonResult, c as AddLessonTriggers, D as DEFAULT_RECALL_LIMIT, I as ImportLegacyOptions, d as ImportLegacyReport, L as LESSONS_PROCEDURAL_RULE, e as Lesson, f as LessonStatus, g as LessonsGraph, h as LessonsGraphSchema, i as LessonsPaths, j as LessonsQuery, M as MatchedLesson, k as MergeLessonsOptions, l as MergeLessonsResult, m as MutateOptions, R as RankOptions, n as RankReason, o as RankedLesson, S as ScaffoldLessonsResult, p as StripMarkersOptions, q as StripMarkersReport, T as Topic, r as Trigger, s as TriggerKind, U as UnknownTopicError, V as ValidationFinding, t as ValidationLevel, u as ValidationReport, v as acquireLessonsLock, w as addLesson, x as graphFilePath, y as importLegacyLessons, z as lessonsPaths, B as loadLessonsGraph, C as mergeLessons, E as mutateLessonsGraph, F as parseGraph, G as queryLessons, H as rankLessons, J as scaffoldLessons, K as serializeGraph, N as stripLegacyMarkers, O as stripMarkersInGraph, P as toRelPath, Q as tryLoadLessonsGraph, W as validateLessonsGraph } from './init-PvpXanVd.js';
5
5
  export { C as CanonicalAgent, a as CanonicalCommand, b as CanonicalFiles, c as CanonicalRule, d as CanonicalSkill, H as HookEntry, e as Hooks, I as IgnorePatterns, M as McpConfig, f as McpServer, P as Permissions, S as SkillSupportingFile, g as StdioMcpServer, U as UrlMcpServer, V as ValidatedConfig } from './schema-CH_JJep8.js';
6
6
  export { E as ExtraRuleOutputContext, a as ExtraRuleOutputResolver, F as FeatureLinter, G as GenerateResult, b as GeneratedOutputMerger, c as GlobalTargetSupport, I as ImportPathBuilder, d as ImportResult, L as LintDiagnostic, R as RuleLinter, S as ScopeExtrasFn, T as TargetCapabilities, e as TargetDescriptor, f as TargetGenerators, g as TargetLayout, h as TargetLayoutScope, i as TargetLintHooks, j as TargetManagedOutputs, k as TargetOutputFamily, l as TargetPathResolvers } from './target-descriptor-CaLUz7SR.js';
7
7
  import 'zod';
package/dist/index.js CHANGED
@@ -2942,6 +2942,18 @@ var init_linter = __esm({
2942
2942
  }
2943
2943
  });
2944
2944
 
2945
+ // src/core/hook-types.ts
2946
+ var BEST_EFFORT_HOOK_EVENTS;
2947
+ var init_hook_types = __esm({
2948
+ "src/core/hook-types.ts"() {
2949
+ BEST_EFFORT_HOOK_EVENTS = /* @__PURE__ */ new Set([
2950
+ "UserPromptSubmit",
2951
+ "PostToolUseFailure",
2952
+ "SessionStart"
2953
+ ]);
2954
+ }
2955
+ });
2956
+
2945
2957
  // src/core/lint/shared/helpers.ts
2946
2958
  function createWarning(file, target31, message) {
2947
2959
  return {
@@ -2957,6 +2969,13 @@ function formatOxfordComma(items) {
2957
2969
  if (items.length === 2) return `${items[0]} and ${items[1]}`;
2958
2970
  return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
2959
2971
  }
2972
+ function unsupportedHookEventNames(hooks, supportedEvents) {
2973
+ if (!hooks) return [];
2974
+ const supported = new Set(supportedEvents);
2975
+ return Object.keys(hooks).filter(
2976
+ (event) => !supported.has(event) && !BEST_EFFORT_HOOK_EVENTS.has(event)
2977
+ );
2978
+ }
2960
2979
  function createUnsupportedHookWarning(event, target31, supportedEvents, options) {
2961
2980
  const by = options?.unsupportedBy ?? target31;
2962
2981
  const supported = formatOxfordComma(supportedEvents);
@@ -2968,6 +2987,7 @@ function createUnsupportedHookWarning(event, target31, supportedEvents, options)
2968
2987
  }
2969
2988
  var init_helpers = __esm({
2970
2989
  "src/core/lint/shared/helpers.ts"() {
2990
+ init_hook_types();
2971
2991
  }
2972
2992
  });
2973
2993
 
@@ -6311,8 +6331,7 @@ var init_linter5 = __esm({
6311
6331
  // src/targets/augment-code/lint.ts
6312
6332
  function lintHooks3(canonical) {
6313
6333
  if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
6314
- const supportedSet = new Set(SUPPORTED_HOOK_EVENTS);
6315
- return Object.keys(canonical.hooks).filter((event) => !supportedSet.has(event)).map(
6334
+ return unsupportedHookEventNames(canonical.hooks, SUPPORTED_HOOK_EVENTS).map(
6316
6335
  (event) => createUnsupportedHookWarning(event, "augment-code", SUPPORTED_HOOK_EVENTS, {
6317
6336
  unsupportedBy: "AugmentCode hooks"
6318
6337
  })
@@ -10317,8 +10336,7 @@ function lintCommands3(canonical) {
10317
10336
  function lintHooks5(canonical) {
10318
10337
  if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
10319
10338
  const supported = ["PreToolUse", "PostToolUse", "Notification", "UserPromptSubmit"];
10320
- const supportedSet = new Set(supported);
10321
- const diagnostics = Object.keys(canonical.hooks).filter((event) => !supportedSet.has(event)).map(
10339
+ const diagnostics = unsupportedHookEventNames(canonical.hooks, supported).map(
10322
10340
  (event) => createUnsupportedHookWarning(event, "copilot", supported, {
10323
10341
  unsupportedBy: "Copilot hooks"
10324
10342
  })
@@ -11436,7 +11454,7 @@ var init_permissions2 = __esm({
11436
11454
  // src/targets/cursor/hook-format.ts
11437
11455
  function unmappedCursorHookEvents(hooks) {
11438
11456
  return Object.keys(hooks).filter(
11439
- (event) => Array.isArray(hooks[event]) && hooks[event].length > 0 && !(event in CANONICAL_TO_CURSOR)
11457
+ (event) => Array.isArray(hooks[event]) && hooks[event].length > 0 && !(event in CANONICAL_TO_CURSOR) && !BEST_EFFORT_HOOK_EVENTS.has(event)
11440
11458
  );
11441
11459
  }
11442
11460
  function toCursorHooks(hooks) {
@@ -11484,6 +11502,7 @@ function cursorHooksToCanonical(hooks) {
11484
11502
  var CANONICAL_TO_CURSOR, CURSOR_TO_CANONICAL;
11485
11503
  var init_hook_format = __esm({
11486
11504
  "src/targets/cursor/hook-format.ts"() {
11505
+ init_hook_types();
11487
11506
  init_hook_command();
11488
11507
  CANONICAL_TO_CURSOR = {
11489
11508
  PreToolUse: "preToolUse",
@@ -13934,8 +13953,9 @@ function lintCommands6(canonical) {
13934
13953
  function lintHooks7(canonical) {
13935
13954
  if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
13936
13955
  const supported = ["PreToolUse", "PostToolUse", "Notification"];
13937
- const supportedSet = new Set(supported);
13938
- return Object.keys(canonical.hooks).filter((event) => !supportedSet.has(event)).map((event) => createUnsupportedHookWarning(event, "gemini-cli", supported));
13956
+ return unsupportedHookEventNames(canonical.hooks, supported).map(
13957
+ (event) => createUnsupportedHookWarning(event, "gemini-cli", supported)
13958
+ );
13939
13959
  }
13940
13960
  var init_lint14 = __esm({
13941
13961
  "src/targets/gemini-cli/lint.ts"() {
@@ -16187,8 +16207,7 @@ var init_linter20 = __esm({
16187
16207
  function lintHooks10(canonical) {
16188
16208
  if (!canonical.hooks || Object.keys(canonical.hooks).length === 0) return [];
16189
16209
  const supported = ["PreToolUse", "PostToolUse", "UserPromptSubmit", "SubagentStop"];
16190
- const supportedSet = new Set(supported);
16191
- return Object.keys(canonical.hooks).filter((event) => !supportedSet.has(event)).map(
16210
+ return unsupportedHookEventNames(canonical.hooks, supported).map(
16192
16211
  (event) => createUnsupportedHookWarning(event, "kiro", supported, { unsupportedBy: "Kiro hooks" })
16193
16212
  );
16194
16213
  }
@@ -24283,7 +24302,8 @@ function lintRuleScopeInversion(input) {
24283
24302
  }
24284
24303
  return out2;
24285
24304
  }
24286
- var CURRENT_GRAPH_VERSION = 1;
24305
+ var CURRENT_GRAPH_VERSION = 2;
24306
+ var VersionSchema = z.union([z.literal(1), z.literal(2)]);
24287
24307
  var MAX_RULE_LENGTH = 2e3;
24288
24308
  var IdSchema = z.string().regex(/^[a-z0-9-]+$/, "id must be kebab-case");
24289
24309
  var DateSchema = z.string().regex(
@@ -24307,10 +24327,17 @@ var LessonSchema = z.object({
24307
24327
  evidence: z.array(z.string().min(1)),
24308
24328
  status: LessonStatusSchema,
24309
24329
  supersededBy: IdSchema.optional(),
24310
- createdAt: DateSchema
24330
+ createdAt: DateSchema,
24331
+ /**
24332
+ * `'always'` marks an ALWAYS-ON lesson: a universal standard delivered on
24333
+ * every task (via the UserPromptSubmit hook / a `--always` recall) rather than
24334
+ * matched by triggers. Such a lesson needs no trigger and is excluded from
24335
+ * triggered recall. Absent = a normal triggered lesson.
24336
+ */
24337
+ scope: z.literal("always").optional()
24311
24338
  }).strict();
24312
24339
  var LessonsGraphSchema = z.object({
24313
- version: z.literal(CURRENT_GRAPH_VERSION),
24340
+ version: VersionSchema,
24314
24341
  lessons: z.record(IdSchema, LessonSchema),
24315
24342
  topics: z.record(IdSchema, TopicSchema),
24316
24343
  triggers: z.record(IdSchema, TriggerSchema)
@@ -24375,7 +24402,7 @@ var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
24375
24402
 
24376
24403
  Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
24377
24404
 
24378
- **Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command>\` and obey matches. Pure-read commands and recall itself are exempt.
24405
+ **Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command>\` and obey matches; at task start, ALSO run \`agentsmesh lessons query --keyword "<task terms>" --always\` for conceptual + universal rules no path/command names. Pure-read commands and recall itself are exempt.
24379
24406
 
24380
24407
  **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>\`.
24381
24408
 
@@ -25251,6 +25278,12 @@ function collectDeadFileGlobs(graph, findings, knownPaths) {
25251
25278
  });
25252
25279
  }
25253
25280
  }
25281
+ function fileGlobMatchCount(pattern, knownPaths) {
25282
+ const isMatch = picomatch(pattern, { dot: true });
25283
+ let n = 0;
25284
+ for (const p of knownPaths) if (isMatch(p)) n += 1;
25285
+ return n;
25286
+ }
25254
25287
  var RUNNER_ANCHOR = /^\^(pnpm|npm|npx|yarn|bun)\b/;
25255
25288
  function collectRunnerAnchoredPatterns(graph, findings) {
25256
25289
  const active = activeTriggerIds(graph);
@@ -25881,7 +25914,7 @@ var UnrecallableLessonError = class extends Error {
25881
25914
  };
25882
25915
 
25883
25916
  // src/lessons/capture-guardrails.ts
25884
- var NEAR_DUPLICATE_THRESHOLD = 0.6;
25917
+ var WIDE_GLOB_MATCH_COUNT = 40;
25885
25918
  var MAX_RECOMMENDED_TRIGGERS = 8;
25886
25919
  function isBroadGlob(pattern) {
25887
25920
  const p = pattern.trim();
@@ -25937,9 +25970,19 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
25937
25970
  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.`
25938
25971
  });
25939
25972
  }
25973
+ 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);
25974
+ if (wide.length > 0) {
25975
+ warnings.push({
25976
+ code: "WIDE_GLOB_MATCH",
25977
+ message: `Lesson "${lessonId}" has file glob(s) (${wide.join(", ")}) matching more than ${WIDE_GLOB_MATCH_COUNT} files in the working tree; narrow to the file-CLASS where the rule actually applies so it does not fire on unrelated edits.`
25978
+ });
25979
+ }
25940
25980
  }
25941
25981
  return warnings;
25942
25982
  }
25983
+
25984
+ // src/lessons/capture-near-duplicate.ts
25985
+ var NEAR_DUPLICATE_THRESHOLD = 0.6;
25943
25986
  function nearDuplicateWarning(graph, lessonId) {
25944
25987
  const subject = graph.lessons[lessonId];
25945
25988
  if (subject === void 0) return null;
@@ -26159,7 +26202,7 @@ async function importLegacyLessons(projectRoot, options) {
26159
26202
  if (options.force !== true && populated) {
26160
26203
  throw new LessonsGraphExistsError();
26161
26204
  }
26162
- g.version = 1;
26205
+ g.version = CURRENT_GRAPH_VERSION;
26163
26206
  g.lessons = lessons;
26164
26207
  g.topics = topics;
26165
26208
  g.triggers = triggers;
@@ -26310,7 +26353,7 @@ function getHostname() {
26310
26353
  return hostname();
26311
26354
  }
26312
26355
  function sleep2(ms) {
26313
- return new Promise((resolve9) => setTimeout(resolve9, ms));
26356
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
26314
26357
  }
26315
26358
 
26316
26359
  // src/lessons/lessons-lock.ts
@@ -26326,7 +26369,7 @@ async function acquireLessonsLock(projectRoot, opts = {}) {
26326
26369
 
26327
26370
  // src/lessons/mutate.ts
26328
26371
  function emptyGraph() {
26329
- return { version: 1, lessons: {}, topics: {}, triggers: {} };
26372
+ return { version: CURRENT_GRAPH_VERSION, lessons: {}, topics: {}, triggers: {} };
26330
26373
  }
26331
26374
  function findingKey(f) {
26332
26375
  return `${f.code}|${f.triggerId ?? ""}|${f.lessonId ?? ""}`;
@@ -26350,6 +26393,7 @@ async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
26350
26393
  `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.)`
26351
26394
  );
26352
26395
  }
26396
+ graph.version = CURRENT_GRAPH_VERSION;
26353
26397
  saveLessonsGraph(projectRoot, graph);
26354
26398
  return result;
26355
26399
  } finally {
@@ -26427,14 +26471,15 @@ function addLessonInto(graph, input, options) {
26427
26471
  }
26428
26472
  graph.topics[input.topic] = { summary: options.topicSummary };
26429
26473
  }
26430
- if (options.allowNoTrigger !== true) {
26474
+ const skipTriggerGates = options.allowNoTrigger === true || input.scope === "always";
26475
+ if (!skipTriggerGates) {
26431
26476
  const existingTriggers = existingId !== null ? graph.lessons[existingId]?.triggers.length ?? 0 : 0;
26432
26477
  if (countInputTriggers(input.triggers) === 0 && existingTriggers === 0) {
26433
26478
  throw new NoTriggerError();
26434
26479
  }
26435
26480
  }
26436
26481
  const { triggerIds, newTriggerIds } = mergeTriggers(graph, input.triggers);
26437
- if (options.allowNoTrigger !== true) {
26482
+ if (!skipTriggerGates) {
26438
26483
  const resultingTriggers = existingId !== null ? union(graph.lessons[existingId].triggers, triggerIds) : triggerIds;
26439
26484
  const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
26440
26485
  if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
@@ -26448,7 +26493,9 @@ function addLessonInto(graph, input, options) {
26448
26493
  topics: union(existing.topics, [input.topic]),
26449
26494
  triggers: union(existing.triggers, triggerIds),
26450
26495
  evidence: union(existing.evidence, input.evidence ?? []),
26451
- ...existing.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {}
26496
+ ...existing.rationale === void 0 && input.rationale !== void 0 ? { rationale: input.rationale } : {},
26497
+ // Re-capturing a rule with --scope always promotes it to always-on.
26498
+ ...input.scope === "always" ? { scope: "always" } : {}
26452
26499
  };
26453
26500
  return {
26454
26501
  id: existingId,
@@ -26468,7 +26515,8 @@ function addLessonInto(graph, input, options) {
26468
26515
  evidence: input.evidence === void 0 ? [] : [...input.evidence],
26469
26516
  status: "active",
26470
26517
  createdAt: input.createdAt ?? todayIso(),
26471
- ...input.rationale === void 0 ? {} : { rationale: input.rationale }
26518
+ ...input.rationale === void 0 ? {} : { rationale: input.rationale },
26519
+ ...input.scope === "always" ? { scope: "always" } : {}
26472
26520
  };
26473
26521
  const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
26474
26522
  const nearDup = nearDuplicateWarning(graph, id);
@@ -26520,14 +26568,19 @@ function captureLogPath(projectRoot) {
26520
26568
  }
26521
26569
 
26522
26570
  // src/lessons/keyword-match.ts
26523
- function splitTokens(text) {
26524
- return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
26525
- }
26526
26571
  function deriveHaystackTokens(query) {
26527
26572
  const parts = [];
26528
26573
  if (query.file !== void 0) parts.push(query.file);
26529
26574
  if (query.command !== void 0) parts.push(query.command);
26530
- return parts.length === 0 ? [] : splitTokens(parts.join(" "));
26575
+ if (parts.length === 0) return [];
26576
+ const out2 = [];
26577
+ for (const raw of parts.join(" ").split(/[^A-Za-z0-9]+/)) {
26578
+ if (raw.length === 0) continue;
26579
+ out2.push(raw.toLowerCase());
26580
+ const sub = raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(" ").filter((t) => t.length > 0);
26581
+ if (sub.length > 1) out2.push(...sub);
26582
+ }
26583
+ return out2;
26531
26584
  }
26532
26585
  function containsRun(needle, hay) {
26533
26586
  if (needle.length === 0) return false;
@@ -26560,6 +26613,7 @@ function queryLessons(graph, query) {
26560
26613
  const matched = [];
26561
26614
  for (const [id, lesson] of Object.entries(graph.lessons)) {
26562
26615
  if (lesson.status !== "active") continue;
26616
+ if (lesson.scope === "always") continue;
26563
26617
  if (lesson.triggers.some((t) => matchedTriggerIds.has(t))) {
26564
26618
  matched.push({ id, lesson });
26565
26619
  }
@@ -26605,6 +26659,7 @@ var RRF_K = 60;
26605
26659
  var SPECIFICITY_WEIGHT = 3;
26606
26660
  var TOPIC_COHERENCE_WEIGHT = 2;
26607
26661
  var BM25_WEIGHT = 1;
26662
+ var EFFECTIVENESS_WEIGHT = 1;
26608
26663
  function rankMap(items) {
26609
26664
  const sorted = [...items].sort(
26610
26665
  (a, b) => b.value !== a.value ? b.value - a.value : a.id < b.id ? -1 : 1
@@ -26641,16 +26696,20 @@ function rankLessons(graph, query, matches, options = {}) {
26641
26696
  specificity,
26642
26697
  // `id` is a matched lesson, and buildTopicCoherence keys every matched id.
26643
26698
  topicCoherence: coherence.get(id),
26644
- matchedTriggers: hitTriggers
26699
+ matchedTriggers: hitTriggers,
26700
+ // Absent from the log ⇒ neutral 1, so a lesson with no outcome data never
26701
+ // sinks below one that has merely been recorded.
26702
+ effectiveness: options.effectiveness?.get(id) ?? 1
26645
26703
  };
26646
26704
  });
26647
26705
  const bm25Ranks = rankMap(scored.map((s) => ({ id: s.id, value: s.bm25 })));
26648
26706
  const specRanks = rankMap(scored.map((s) => ({ id: s.id, value: s.specificity })));
26649
26707
  const topicRanks = rankMap(scored.map((s) => ({ id: s.id, value: s.topicCoherence })));
26708
+ const effRanks = rankMap(scored.map((s) => ({ id: s.id, value: s.effectiveness })));
26650
26709
  const ranked = scored.map((s) => ({
26651
26710
  id: s.id,
26652
26711
  lesson: s.lesson,
26653
- score: SPECIFICITY_WEIGHT / (RRF_K + specRanks.get(s.id)) + TOPIC_COHERENCE_WEIGHT / (RRF_K + topicRanks.get(s.id)) + BM25_WEIGHT / (RRF_K + bm25Ranks.get(s.id)),
26712
+ score: SPECIFICITY_WEIGHT / (RRF_K + specRanks.get(s.id)) + TOPIC_COHERENCE_WEIGHT / (RRF_K + topicRanks.get(s.id)) + BM25_WEIGHT / (RRF_K + bm25Ranks.get(s.id)) + EFFECTIVENESS_WEIGHT / (RRF_K + effRanks.get(s.id)),
26654
26713
  reason: {
26655
26714
  matchedTriggers: s.matchedTriggers,
26656
26715
  bm25: s.bm25,
@@ -26689,6 +26748,9 @@ function defaultLessonsConfig() {
26689
26748
  autoPrune: false
26690
26749
  };
26691
26750
  }
26751
+ function outcomeLogPath(projectRoot) {
26752
+ return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
26753
+ }
26692
26754
 
26693
26755
  // src/lessons/merge.ts
26694
26756
  async function mergeLessons(projectRoot, loserId, keeperId, options = {}) {
@@ -26771,18 +26833,27 @@ async function stripMarkersInGraph(projectRoot, options = {}) {
26771
26833
  return { changedIds, changedCount: changedIds.length };
26772
26834
  }
26773
26835
  var RECALL_HOOK_COMMAND = "agentsmesh lessons hook";
26774
- var RECALL_HOOK_MATCHER = "Edit|Write|Bash";
26775
- var RECALL_EVENTS = ["PreToolUse", "PostToolUse"];
26776
- function injectEvent(doc, event) {
26836
+ var RECALL_HOOK_TOOL_MATCHER = "Edit|Write|Bash";
26837
+ var RECALL_EVENTS = [
26838
+ { event: "PreToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
26839
+ { event: "PostToolUse", matcher: RECALL_HOOK_TOOL_MATCHER },
26840
+ { event: "UserPromptSubmit", matcher: "*" },
26841
+ // Capture-on-failure nudge (see capture-nudge.ts). BEST-EFFORT: only Claude
26842
+ // Code's passthrough hooks emit it; whitelist targets drop it without warning
26843
+ // (BEST_EFFORT_HOOK_EVENTS). PostToolUse is success-only, so failures need this.
26844
+ { event: "PostToolUseFailure", matcher: "*" },
26845
+ // Reset recall dedup after a context compaction/clear (see hook.ts SessionStart).
26846
+ // BEST-EFFORT: targets that can't represent SessionStart just keep dedup as-is.
26847
+ { event: "SessionStart", matcher: "*" }
26848
+ ];
26849
+ function injectEvent(doc, event, matcher) {
26777
26850
  const existing = doc.get(event);
26778
26851
  const seq = existing instanceof YAMLSeq ? existing : new YAMLSeq();
26779
26852
  const present = seq.items.some(
26780
26853
  (item) => item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND
26781
26854
  );
26782
26855
  if (present) return false;
26783
- seq.add(
26784
- doc.createNode({ matcher: RECALL_HOOK_MATCHER, type: "command", command: RECALL_HOOK_COMMAND })
26785
- );
26856
+ seq.add(doc.createNode({ matcher, type: "command", command: RECALL_HOOK_COMMAND }));
26786
26857
  doc.set(event, seq);
26787
26858
  return true;
26788
26859
  }
@@ -26791,13 +26862,32 @@ function injectRecallHook(projectRoot) {
26791
26862
  if (!existsSync(path)) return false;
26792
26863
  const doc = parseDocument(readFileSync(path, "utf8"));
26793
26864
  let changed = false;
26794
- for (const event of RECALL_EVENTS) {
26795
- if (injectEvent(doc, event)) changed = true;
26865
+ for (const { event, matcher } of RECALL_EVENTS) {
26866
+ if (injectEvent(doc, event, matcher)) changed = true;
26796
26867
  }
26797
26868
  if (changed) writeFileSync(path, String(doc), "utf8");
26798
26869
  return changed;
26799
26870
  }
26800
26871
 
26872
+ // src/lessons/merge-driver-setup.ts
26873
+ var LESSONS_MERGE_DRIVER = "agentsmesh-lessons";
26874
+ var LESSONS_GITATTRIBUTES_ENTRY = `.agentsmesh/lessons/lessons.json merge=${LESSONS_MERGE_DRIVER}`;
26875
+
26876
+ // src/utils/filesystem/gitattributes.ts
26877
+ init_fs();
26878
+ async function ensureGitattributesEntries(projectRoot, entries) {
26879
+ const path = join(projectRoot, ".gitattributes");
26880
+ const current = await readFileSafe(path) ?? "";
26881
+ const existing = new Set(
26882
+ current.split("\n").map((s) => s.trim()).filter((s) => s.length > 0 && !s.startsWith("#"))
26883
+ );
26884
+ const toAdd = entries.filter((e) => !existing.has(e.trim()));
26885
+ if (toAdd.length === 0) return false;
26886
+ const suffix = current.endsWith("\n") || current === "" ? "" : "\n";
26887
+ await writeFileAtomic(path, current + suffix + toAdd.join("\n") + "\n");
26888
+ return true;
26889
+ }
26890
+
26801
26891
  // src/utils/filesystem/gitignore.ts
26802
26892
  init_fs();
26803
26893
  async function ensureGitignoreEntries(projectRoot, entries) {
@@ -26874,23 +26964,37 @@ regression / wrong assumption / surprise and you have not captured (nor stated
26874
26964
  \`Lesson: none\`)? The task is INCOMPLETE \u2014 and the user will check. The graph
26875
26965
  \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit.
26876
26966
 
26877
- ## Recall \u2014 before each file edit and each state-changing command
26967
+ ## Recall \u2014 before each edit/command, and at task start
26878
26968
 
26879
26969
  \`agentsmesh lessons query --file <path> --cmd <command>\`, then apply every rule.
26880
26970
  Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
26881
- is the anti-pattern** \u2014 lessons are keyed to a \`file_glob\`/\`command_pattern\`.
26971
+ for a specific edit is the anti-pattern** \u2014 anchor those to \`--file\`/\`--cmd\`. But at the
26972
+ START of a task (or when planning), run \`agentsmesh lessons query --keyword "<the task's
26973
+ key terms>" --always\`: that surfaces the conceptual rules no file/command names PLUS the
26974
+ universal always-on lessons \u2014 the manual equivalent of the automatic prompt recall on
26975
+ hook-capable tools. Author a \`keyword\` trigger beside a \`file_glob\` on conceptual lessons
26976
+ so they are reachable both ways. No shell \u2192 MCP \`lessons_query\` (\`file\`/\`command\`/\`keyword\`/\`always\`).
26882
26977
 
26883
26978
  ## Capture \u2014 Gate Function (before any completion claim)
26884
26979
 
26885
26980
  1. **SELF-CRITIQUE**: any failure, correction, regression, wrong assumption,
26886
26981
  useful surprise, repeated friction, or non-obvious fix? Failing
26887
26982
  tests/lint/typecheck and user/review corrections \u2014 yours or anyone's \u2014 all count.
26983
+ PROCESS/TOOLING surprises count too (a sandbox command that silently no-op'd, a port
26984
+ already in use, a library's argument shape) \u2014 trigger those with \`--trigger-cmd\`. So
26985
+ does a review finding you REVIEWED then deliberately REJECTED: capture it as a "do NOT
26986
+ refactor" guard so the reasoning survives the session.
26888
26987
  2. **CAPTURE** a reusable imperative rule with an effective trigger (else say so):
26889
- \`agentsmesh lessons add "<rule>" --topic <id> --trigger-file <glob>\`
26988
+ \`agentsmesh lessons add "<rule>" --topic <id> --trigger-file <glob>\`. Trigger on the
26989
+ file-CLASS where it will RECUR (e.g. \`**/lib/use*Form*.ts\`), NOT the single file you
26990
+ discovered it in \u2014 and NOT a broad \`src/**\`. If the rule is a UNIVERSAL standard that
26991
+ applies to EVERY task (a comment/test/style convention no file or command names),
26992
+ capture it with \`--scope always\` instead \u2014 it needs no trigger and is delivered on
26993
+ every task.
26890
26994
  3. **RECEIPT**: emit \`Lesson: captured <id>\` or \`Lesson: none\`.
26891
26995
 
26892
- At least one _effective_ trigger is required or the capture is rejected
26893
- (\`UNRECALLABLE_LESSON\`); prefer \`--trigger-file\`. No shell \u2192 MCP \`lessons_query\`,
26996
+ At least one _effective_ trigger is required (or \`--scope always\` for a universal rule) or
26997
+ the capture is rejected (\`UNRECALLABLE_LESSON\`); prefer \`--trigger-file\`. No shell \u2192 MCP \`lessons_query\`,
26894
26998
  \`lessons_add\`, \`lessons_topics\`, \`lessons_show\`, \`lessons_deprecate\`. Run
26895
26999
  \`agentsmesh lessons --help\` for every subcommand and flag: query, add, topics, show,
26896
27000
  deprecate, merge, untrigger, strip-markers, prune, journal, validate, stats, import-md.
@@ -26936,9 +27040,21 @@ async function scaffoldLessons(projectRoot) {
26936
27040
  const recallHookInjected = injectRecallHook(projectRoot);
26937
27041
  const gitignoreUpdated = await ensureGitignoreEntries(projectRoot, [
26938
27042
  toRelPath(projectRoot, recallLogPath(projectRoot)),
26939
- toRelPath(projectRoot, captureLogPath(projectRoot))
27043
+ toRelPath(projectRoot, captureLogPath(projectRoot)),
27044
+ toRelPath(projectRoot, outcomeLogPath(projectRoot))
27045
+ ]);
27046
+ const gitattributesUpdated = await ensureGitattributesEntries(projectRoot, [
27047
+ LESSONS_GITATTRIBUTES_ENTRY
26940
27048
  ]);
26941
- return { created, updated, skipped, rootRuleUpdated, gitignoreUpdated, recallHookInjected };
27049
+ return {
27050
+ created,
27051
+ updated,
27052
+ skipped,
27053
+ rootRuleUpdated,
27054
+ gitignoreUpdated,
27055
+ gitattributesUpdated,
27056
+ recallHookInjected
27057
+ };
26942
27058
  }
26943
27059
  function seedLessonsSkill(projectRoot, created, updated, skipped) {
26944
27060
  const skillPath = join(projectRoot, ".agentsmesh/skills", LESSONS_SKILL_NAME, "SKILL.md");