agentsmesh 0.28.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';
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-CzaoYJlG.js';
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-ItDY3NR0.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
+ 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
+ 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
  }
@@ -21595,7 +21614,14 @@ var extendSourceSchema = z.object({
21595
21614
  features: z.array(featureSchema),
21596
21615
  /** Repo-relative POSIX path for discovery (skill packs, nested .agentsmesh). */
21597
21616
  path: z.string().optional(),
21598
- pick: extendPickSchema.optional()
21617
+ pick: extendPickSchema.optional(),
21618
+ /**
21619
+ * Per-entry consent for "elevated" artifacts (`hooks`, `permissions`, `mcp`)
21620
+ * from a remote source. These control local code execution at generate time,
21621
+ * so for non-local extends they are stripped by default and only merged for
21622
+ * the artifacts listed here. Local extends are trusted and ignore this field.
21623
+ */
21624
+ accept: z.array(z.enum(["hooks", "permissions", "mcp"])).optional()
21599
21625
  });
21600
21626
  var collaborationSchema = z.object({
21601
21627
  strategy: z.enum(["merge", "lock", "last-wins"]).default("merge"),
@@ -22304,7 +22330,9 @@ async function resolveExtendPaths(config, configDir, options = {}) {
22304
22330
  as: ext.as,
22305
22331
  version: fetched.version,
22306
22332
  path: ext.path,
22307
- pick: ext.pick
22333
+ pick: ext.pick,
22334
+ isRemote: true,
22335
+ accept: ext.accept
22308
22336
  });
22309
22337
  continue;
22310
22338
  }
@@ -22321,7 +22349,9 @@ async function resolveExtendPaths(config, configDir, options = {}) {
22321
22349
  target: ext.target,
22322
22350
  as: ext.as,
22323
22351
  path: ext.path,
22324
- pick: ext.pick
22352
+ pick: ext.pick,
22353
+ isRemote: false,
22354
+ accept: ext.accept
22325
22355
  });
22326
22356
  }
22327
22357
  return result;
@@ -23770,6 +23800,47 @@ function applyExtendPick(canonical, features, pick, extendName) {
23770
23800
  return next;
23771
23801
  }
23772
23802
 
23803
+ // src/install/core/elevated-artifacts.ts
23804
+ function stripUntrustedElevatedArtifacts(canonical, options) {
23805
+ if (options.sourceKind === "local") {
23806
+ return { canonical, stripped: [] };
23807
+ }
23808
+ const stripped = [];
23809
+ let hooks = canonical.hooks;
23810
+ let permissions = canonical.permissions;
23811
+ let mcp = canonical.mcp;
23812
+ if (hooks !== null && !options.acceptHooks) {
23813
+ hooks = null;
23814
+ stripped.push("hooks");
23815
+ }
23816
+ if (permissions !== null && !options.acceptPermissions) {
23817
+ permissions = null;
23818
+ stripped.push("permissions");
23819
+ }
23820
+ if (mcp !== null && !options.acceptMcp) {
23821
+ mcp = null;
23822
+ stripped.push("mcp");
23823
+ }
23824
+ if (stripped.length === 0) {
23825
+ return { canonical, stripped: [] };
23826
+ }
23827
+ return {
23828
+ canonical: { ...canonical, hooks, permissions, mcp },
23829
+ stripped
23830
+ };
23831
+ }
23832
+
23833
+ // src/canonical/extends/extend-elevated.ts
23834
+ function gateExtendElevatedArtifacts(canonical, ext) {
23835
+ const accept = ext.accept ?? [];
23836
+ return stripUntrustedElevatedArtifacts(canonical, {
23837
+ sourceKind: ext.isRemote ? "git" : "local",
23838
+ acceptHooks: accept.includes("hooks"),
23839
+ acceptPermissions: accept.includes("permissions"),
23840
+ acceptMcp: accept.includes("mcp")
23841
+ });
23842
+ }
23843
+
23773
23844
  // src/install/pack/pack-reader.ts
23774
23845
  init_fs();
23775
23846
  var MANUAL_INSTALL_AS = ["rules", "commands", "agents", "skills"];
@@ -23919,7 +23990,14 @@ async function loadCanonicalWithExtends(config, configDir, options = {}, canonic
23919
23990
  for (const ext of resolvedExtends) {
23920
23991
  const extCanonical = await loadCanonicalForExtend(ext);
23921
23992
  const filtered = filterCanonicalByFeatures(extCanonical, ext.features);
23922
- const picked = applyExtendPick(filtered, ext.features, ext.pick, ext.name);
23993
+ const gated = gateExtendElevatedArtifacts(filtered, ext);
23994
+ if (gated.stripped.length > 0) {
23995
+ const list = gated.stripped.join(", ");
23996
+ logger.warn(
23997
+ `[agentsmesh] Extend "${ext.name}": stripped elevated artifacts from a remote source: ${list}. Add \`accept: [${list}]\` to this extends entry in agentsmesh.yaml to keep them.`
23998
+ );
23999
+ }
24000
+ const picked = applyExtendPick(gated.canonical, ext.features, ext.pick, ext.name);
23923
24001
  merged = mergeCanonicalFiles(merged, picked);
23924
24002
  }
23925
24003
  const packsCanonical = await loadPacksCanonical(canonicalDir);
@@ -23955,7 +24033,18 @@ function canonicalize(path) {
23955
24033
  try {
23956
24034
  return realpathSync(path);
23957
24035
  } catch {
23958
- return path;
24036
+ let current = resolve(path);
24037
+ const tail = [];
24038
+ for (; ; ) {
24039
+ const parent = dirname(current);
24040
+ if (parent === current) return resolve(path);
24041
+ tail.unshift(basename(current));
24042
+ try {
24043
+ return join(realpathSync(parent), ...tail);
24044
+ } catch {
24045
+ current = parent;
24046
+ }
24047
+ }
23959
24048
  }
23960
24049
  }
23961
24050
  function assertSourceInsideProjectRoot(resolvedPath, projectRoot) {
@@ -24213,7 +24302,8 @@ function lintRuleScopeInversion(input) {
24213
24302
  }
24214
24303
  return out2;
24215
24304
  }
24216
- var CURRENT_GRAPH_VERSION = 1;
24305
+ var CURRENT_GRAPH_VERSION = 2;
24306
+ var VersionSchema = z.union([z.literal(1), z.literal(2)]);
24217
24307
  var MAX_RULE_LENGTH = 2e3;
24218
24308
  var IdSchema = z.string().regex(/^[a-z0-9-]+$/, "id must be kebab-case");
24219
24309
  var DateSchema = z.string().regex(
@@ -24237,10 +24327,17 @@ var LessonSchema = z.object({
24237
24327
  evidence: z.array(z.string().min(1)),
24238
24328
  status: LessonStatusSchema,
24239
24329
  supersededBy: IdSchema.optional(),
24240
- 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()
24241
24338
  }).strict();
24242
24339
  var LessonsGraphSchema = z.object({
24243
- version: z.literal(CURRENT_GRAPH_VERSION),
24340
+ version: VersionSchema,
24244
24341
  lessons: z.record(IdSchema, LessonSchema),
24245
24342
  topics: z.record(IdSchema, TopicSchema),
24246
24343
  triggers: z.record(IdSchema, TriggerSchema)
@@ -24305,7 +24402,7 @@ var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
24305
24402
 
24306
24403
  Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
24307
24404
 
24308
- **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.
24309
24406
 
24310
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>\`.
24311
24408
 
@@ -25181,6 +25278,12 @@ function collectDeadFileGlobs(graph, findings, knownPaths) {
25181
25278
  });
25182
25279
  }
25183
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
+ }
25184
25287
  var RUNNER_ANCHOR = /^\^(pnpm|npm|npx|yarn|bun)\b/;
25185
25288
  function collectRunnerAnchoredPatterns(graph, findings) {
25186
25289
  const active = activeTriggerIds(graph);
@@ -25811,14 +25914,14 @@ var UnrecallableLessonError = class extends Error {
25811
25914
  };
25812
25915
 
25813
25916
  // src/lessons/capture-guardrails.ts
25814
- var NEAR_DUPLICATE_THRESHOLD = 0.6;
25917
+ var WIDE_GLOB_MATCH_COUNT = 40;
25815
25918
  var MAX_RECOMMENDED_TRIGGERS = 8;
25816
25919
  function isBroadGlob(pattern) {
25817
25920
  const p = pattern.trim();
25818
25921
  if (p === "*" || p === "**") return true;
25819
25922
  if (!p.includes("**")) return false;
25820
- const basename70 = p.slice(p.lastIndexOf("/") + 1);
25821
- return basename70.startsWith("*");
25923
+ const basename71 = p.slice(p.lastIndexOf("/") + 1);
25924
+ return basename71.startsWith("*");
25822
25925
  }
25823
25926
  function inspectCapturedLesson(graph, lessonId, knownPaths) {
25824
25927
  const lesson = graph.lessons[lessonId];
@@ -25867,9 +25970,19 @@ function inspectCapturedLesson(graph, lessonId, knownPaths) {
25867
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.`
25868
25971
  });
25869
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
+ }
25870
25980
  }
25871
25981
  return warnings;
25872
25982
  }
25983
+
25984
+ // src/lessons/capture-near-duplicate.ts
25985
+ var NEAR_DUPLICATE_THRESHOLD = 0.6;
25873
25986
  function nearDuplicateWarning(graph, lessonId) {
25874
25987
  const subject = graph.lessons[lessonId];
25875
25988
  if (subject === void 0) return null;
@@ -26089,7 +26202,7 @@ async function importLegacyLessons(projectRoot, options) {
26089
26202
  if (options.force !== true && populated) {
26090
26203
  throw new LessonsGraphExistsError();
26091
26204
  }
26092
- g.version = 1;
26205
+ g.version = CURRENT_GRAPH_VERSION;
26093
26206
  g.lessons = lessons;
26094
26207
  g.topics = topics;
26095
26208
  g.triggers = triggers;
@@ -26240,7 +26353,7 @@ function getHostname() {
26240
26353
  return hostname();
26241
26354
  }
26242
26355
  function sleep2(ms) {
26243
- return new Promise((resolve9) => setTimeout(resolve9, ms));
26356
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
26244
26357
  }
26245
26358
 
26246
26359
  // src/lessons/lessons-lock.ts
@@ -26256,7 +26369,7 @@ async function acquireLessonsLock(projectRoot, opts = {}) {
26256
26369
 
26257
26370
  // src/lessons/mutate.ts
26258
26371
  function emptyGraph() {
26259
- return { version: 1, lessons: {}, topics: {}, triggers: {} };
26372
+ return { version: CURRENT_GRAPH_VERSION, lessons: {}, topics: {}, triggers: {} };
26260
26373
  }
26261
26374
  function findingKey(f) {
26262
26375
  return `${f.code}|${f.triggerId ?? ""}|${f.lessonId ?? ""}`;
@@ -26280,6 +26393,7 @@ async function mutateLessonsGraphLocked(projectRoot, mutator, options = {}) {
26280
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.)`
26281
26394
  );
26282
26395
  }
26396
+ graph.version = CURRENT_GRAPH_VERSION;
26283
26397
  saveLessonsGraph(projectRoot, graph);
26284
26398
  return result;
26285
26399
  } finally {
@@ -26357,14 +26471,15 @@ function addLessonInto(graph, input, options) {
26357
26471
  }
26358
26472
  graph.topics[input.topic] = { summary: options.topicSummary };
26359
26473
  }
26360
- if (options.allowNoTrigger !== true) {
26474
+ const skipTriggerGates = options.allowNoTrigger === true || input.scope === "always";
26475
+ if (!skipTriggerGates) {
26361
26476
  const existingTriggers = existingId !== null ? graph.lessons[existingId]?.triggers.length ?? 0 : 0;
26362
26477
  if (countInputTriggers(input.triggers) === 0 && existingTriggers === 0) {
26363
26478
  throw new NoTriggerError();
26364
26479
  }
26365
26480
  }
26366
26481
  const { triggerIds, newTriggerIds } = mergeTriggers(graph, input.triggers);
26367
- if (options.allowNoTrigger !== true) {
26482
+ if (!skipTriggerGates) {
26368
26483
  const resultingTriggers = existingId !== null ? union(graph.lessons[existingId].triggers, triggerIds) : triggerIds;
26369
26484
  const blockingDead = blockingDeadTriggers(graph, resultingTriggers);
26370
26485
  if (resultingTriggers.length > 0 && blockingDead.length === resultingTriggers.length) {
@@ -26378,7 +26493,9 @@ function addLessonInto(graph, input, options) {
26378
26493
  topics: union(existing.topics, [input.topic]),
26379
26494
  triggers: union(existing.triggers, triggerIds),
26380
26495
  evidence: union(existing.evidence, input.evidence ?? []),
26381
- ...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" } : {}
26382
26499
  };
26383
26500
  return {
26384
26501
  id: existingId,
@@ -26398,7 +26515,8 @@ function addLessonInto(graph, input, options) {
26398
26515
  evidence: input.evidence === void 0 ? [] : [...input.evidence],
26399
26516
  status: "active",
26400
26517
  createdAt: input.createdAt ?? todayIso(),
26401
- ...input.rationale === void 0 ? {} : { rationale: input.rationale }
26518
+ ...input.rationale === void 0 ? {} : { rationale: input.rationale },
26519
+ ...input.scope === "always" ? { scope: "always" } : {}
26402
26520
  };
26403
26521
  const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
26404
26522
  const nearDup = nearDuplicateWarning(graph, id);
@@ -26450,14 +26568,19 @@ function captureLogPath(projectRoot) {
26450
26568
  }
26451
26569
 
26452
26570
  // src/lessons/keyword-match.ts
26453
- function splitTokens(text) {
26454
- return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
26455
- }
26456
26571
  function deriveHaystackTokens(query) {
26457
26572
  const parts = [];
26458
26573
  if (query.file !== void 0) parts.push(query.file);
26459
26574
  if (query.command !== void 0) parts.push(query.command);
26460
- 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;
26461
26584
  }
26462
26585
  function containsRun(needle, hay) {
26463
26586
  if (needle.length === 0) return false;
@@ -26490,6 +26613,7 @@ function queryLessons(graph, query) {
26490
26613
  const matched = [];
26491
26614
  for (const [id, lesson] of Object.entries(graph.lessons)) {
26492
26615
  if (lesson.status !== "active") continue;
26616
+ if (lesson.scope === "always") continue;
26493
26617
  if (lesson.triggers.some((t) => matchedTriggerIds.has(t))) {
26494
26618
  matched.push({ id, lesson });
26495
26619
  }
@@ -26535,6 +26659,7 @@ var RRF_K = 60;
26535
26659
  var SPECIFICITY_WEIGHT = 3;
26536
26660
  var TOPIC_COHERENCE_WEIGHT = 2;
26537
26661
  var BM25_WEIGHT = 1;
26662
+ var EFFECTIVENESS_WEIGHT = 1;
26538
26663
  function rankMap(items) {
26539
26664
  const sorted = [...items].sort(
26540
26665
  (a, b) => b.value !== a.value ? b.value - a.value : a.id < b.id ? -1 : 1
@@ -26571,16 +26696,20 @@ function rankLessons(graph, query, matches, options = {}) {
26571
26696
  specificity,
26572
26697
  // `id` is a matched lesson, and buildTopicCoherence keys every matched id.
26573
26698
  topicCoherence: coherence.get(id),
26574
- 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
26575
26703
  };
26576
26704
  });
26577
26705
  const bm25Ranks = rankMap(scored.map((s) => ({ id: s.id, value: s.bm25 })));
26578
26706
  const specRanks = rankMap(scored.map((s) => ({ id: s.id, value: s.specificity })));
26579
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 })));
26580
26709
  const ranked = scored.map((s) => ({
26581
26710
  id: s.id,
26582
26711
  lesson: s.lesson,
26583
- 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)),
26584
26713
  reason: {
26585
26714
  matchedTriggers: s.matchedTriggers,
26586
26715
  bm25: s.bm25,
@@ -26619,6 +26748,9 @@ function defaultLessonsConfig() {
26619
26748
  autoPrune: false
26620
26749
  };
26621
26750
  }
26751
+ function outcomeLogPath(projectRoot) {
26752
+ return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
26753
+ }
26622
26754
 
26623
26755
  // src/lessons/merge.ts
26624
26756
  async function mergeLessons(projectRoot, loserId, keeperId, options = {}) {
@@ -26701,18 +26833,27 @@ async function stripMarkersInGraph(projectRoot, options = {}) {
26701
26833
  return { changedIds, changedCount: changedIds.length };
26702
26834
  }
26703
26835
  var RECALL_HOOK_COMMAND = "agentsmesh lessons hook";
26704
- var RECALL_HOOK_MATCHER = "Edit|Write|Bash";
26705
- var RECALL_EVENTS = ["PreToolUse", "PostToolUse"];
26706
- 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) {
26707
26850
  const existing = doc.get(event);
26708
26851
  const seq = existing instanceof YAMLSeq ? existing : new YAMLSeq();
26709
26852
  const present = seq.items.some(
26710
26853
  (item) => item instanceof YAMLMap && item.get("command") === RECALL_HOOK_COMMAND
26711
26854
  );
26712
26855
  if (present) return false;
26713
- seq.add(
26714
- doc.createNode({ matcher: RECALL_HOOK_MATCHER, type: "command", command: RECALL_HOOK_COMMAND })
26715
- );
26856
+ seq.add(doc.createNode({ matcher, type: "command", command: RECALL_HOOK_COMMAND }));
26716
26857
  doc.set(event, seq);
26717
26858
  return true;
26718
26859
  }
@@ -26721,13 +26862,32 @@ function injectRecallHook(projectRoot) {
26721
26862
  if (!existsSync(path)) return false;
26722
26863
  const doc = parseDocument(readFileSync(path, "utf8"));
26723
26864
  let changed = false;
26724
- for (const event of RECALL_EVENTS) {
26725
- if (injectEvent(doc, event)) changed = true;
26865
+ for (const { event, matcher } of RECALL_EVENTS) {
26866
+ if (injectEvent(doc, event, matcher)) changed = true;
26726
26867
  }
26727
26868
  if (changed) writeFileSync(path, String(doc), "utf8");
26728
26869
  return changed;
26729
26870
  }
26730
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
+
26731
26891
  // src/utils/filesystem/gitignore.ts
26732
26892
  init_fs();
26733
26893
  async function ensureGitignoreEntries(projectRoot, entries) {
@@ -26804,23 +26964,37 @@ regression / wrong assumption / surprise and you have not captured (nor stated
26804
26964
  \`Lesson: none\`)? The task is INCOMPLETE \u2014 and the user will check. The graph
26805
26965
  \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit.
26806
26966
 
26807
- ## Recall \u2014 before each file edit and each state-changing command
26967
+ ## Recall \u2014 before each edit/command, and at task start
26808
26968
 
26809
26969
  \`agentsmesh lessons query --file <path> --cmd <command>\`, then apply every rule.
26810
26970
  Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
26811
- 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\`).
26812
26977
 
26813
26978
  ## Capture \u2014 Gate Function (before any completion claim)
26814
26979
 
26815
26980
  1. **SELF-CRITIQUE**: any failure, correction, regression, wrong assumption,
26816
26981
  useful surprise, repeated friction, or non-obvious fix? Failing
26817
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.
26818
26987
  2. **CAPTURE** a reusable imperative rule with an effective trigger (else say so):
26819
- \`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.
26820
26994
  3. **RECEIPT**: emit \`Lesson: captured <id>\` or \`Lesson: none\`.
26821
26995
 
26822
- At least one _effective_ trigger is required or the capture is rejected
26823
- (\`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\`,
26824
26998
  \`lessons_add\`, \`lessons_topics\`, \`lessons_show\`, \`lessons_deprecate\`. Run
26825
26999
  \`agentsmesh lessons --help\` for every subcommand and flag: query, add, topics, show,
26826
27000
  deprecate, merge, untrigger, strip-markers, prune, journal, validate, stats, import-md.
@@ -26866,9 +27040,21 @@ async function scaffoldLessons(projectRoot) {
26866
27040
  const recallHookInjected = injectRecallHook(projectRoot);
26867
27041
  const gitignoreUpdated = await ensureGitignoreEntries(projectRoot, [
26868
27042
  toRelPath(projectRoot, recallLogPath(projectRoot)),
26869
- toRelPath(projectRoot, captureLogPath(projectRoot))
27043
+ toRelPath(projectRoot, captureLogPath(projectRoot)),
27044
+ toRelPath(projectRoot, outcomeLogPath(projectRoot))
26870
27045
  ]);
26871
- return { created, updated, skipped, rootRuleUpdated, gitignoreUpdated, recallHookInjected };
27046
+ const gitattributesUpdated = await ensureGitattributesEntries(projectRoot, [
27047
+ LESSONS_GITATTRIBUTES_ENTRY
27048
+ ]);
27049
+ return {
27050
+ created,
27051
+ updated,
27052
+ skipped,
27053
+ rootRuleUpdated,
27054
+ gitignoreUpdated,
27055
+ gitattributesUpdated,
27056
+ recallHookInjected
27057
+ };
26872
27058
  }
26873
27059
  function seedLessonsSkill(projectRoot, created, updated, skipped) {
26874
27060
  const skillPath = join(projectRoot, ".agentsmesh/skills", LESSONS_SKILL_NAME, "SKILL.md");