agentsmesh 0.31.0 → 0.33.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/lessons.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { z } from 'zod';
2
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, statSync, renameSync, readdirSync, realpathSync, appendFileSync } from 'fs';
3
+ import { resolve, dirname, join, relative, sep, basename, extname } from 'path';
4
+ import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
2
5
  import { createHash } from 'crypto';
3
6
  import picomatch from 'picomatch';
4
- import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, renameSync, readdirSync, realpathSync, appendFileSync, statSync } from 'fs';
5
- import { resolve, join, relative, sep, dirname, basename, extname } from 'path';
6
- import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
7
7
  import { mkdir, rm, writeFile, readFile, stat, lstat, unlink, rename, chmod } from 'fs/promises';
8
8
  import { tmpdir, hostname } from 'os';
9
9
  import 'timers/promises';
@@ -52,6 +52,157 @@ var LessonsGraphSchema = z.object({
52
52
  function parseGraph(raw) {
53
53
  return LessonsGraphSchema.parse(raw);
54
54
  }
55
+ var GRAPH_REL_PATH = ".agentsmesh/lessons/lessons.json";
56
+ function graphFilePath(projectRoot) {
57
+ return resolve(projectRoot, GRAPH_REL_PATH);
58
+ }
59
+ function loadLessonsGraph(projectRoot) {
60
+ const raw = readFileSync(graphFilePath(projectRoot), "utf8");
61
+ return parseGraph(JSON.parse(raw));
62
+ }
63
+ function tryLoadLessonsGraph(projectRoot) {
64
+ if (!existsSync(graphFilePath(projectRoot))) return null;
65
+ return loadLessonsGraph(projectRoot);
66
+ }
67
+ function loadLessonsGraphResilient(projectRoot) {
68
+ const path = graphFilePath(projectRoot);
69
+ if (!existsSync(path)) return { status: "absent", graph: null };
70
+ try {
71
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
72
+ const version = parsed?.version;
73
+ if (typeof version === "number" && version > CURRENT_GRAPH_VERSION) {
74
+ return { status: "newer-version", graph: null, version };
75
+ }
76
+ return { status: "ok", graph: parseGraph(parsed) };
77
+ } catch (error) {
78
+ return {
79
+ status: "corrupt",
80
+ graph: null,
81
+ error: error instanceof Error ? error : new Error(String(error))
82
+ };
83
+ }
84
+ }
85
+ function saveLessonsGraph(projectRoot, graph) {
86
+ const path = graphFilePath(projectRoot);
87
+ mkdirSync(dirname(path), { recursive: true });
88
+ const tmp = `${path}.${process.pid}.tmp`;
89
+ writeFileSync(tmp, serializeGraph(graph), "utf8");
90
+ renameSync(tmp, path);
91
+ }
92
+ function serializeGraph(graph) {
93
+ return `${JSON.stringify(canonicalize(graph), null, 2)}
94
+ `;
95
+ }
96
+ function canonicalize(value) {
97
+ if (value === null) return null;
98
+ if (Array.isArray(value)) return value.map(canonicalize);
99
+ if (typeof value === "object") {
100
+ const entries = Object.entries(value).sort(
101
+ ([a], [b]) => a < b ? -1 : 1
102
+ );
103
+ const out = {};
104
+ for (const [k, v] of entries) out[k] = canonicalize(v);
105
+ return out;
106
+ }
107
+ return value;
108
+ }
109
+ var LegacyTriggersSchema = z.object({
110
+ file_globs: z.array(z.string()),
111
+ command_patterns: z.array(z.string()),
112
+ keywords: z.array(z.string())
113
+ }).refine((t) => t.file_globs.length + t.command_patterns.length + t.keywords.length > 0, {
114
+ message: "cluster must declare at least one trigger of any type"
115
+ });
116
+ var LegacyClusterSchema = z.object({
117
+ topic: z.string().regex(/^[a-z0-9-]+$/),
118
+ file: z.string().regex(/\.md$/),
119
+ summary: z.string().min(1),
120
+ triggers: LegacyTriggersSchema
121
+ });
122
+ var LegacyIndexSchema = z.object({
123
+ version: z.literal(1),
124
+ clusters: z.array(LegacyClusterSchema)
125
+ });
126
+ function collectClusterTriggerIds(cluster, triggersById, triggerIdByKey) {
127
+ const specs = [
128
+ ...cluster.triggers.file_globs.map((p) => ({ kind: "file_glob", pattern: p })),
129
+ ...cluster.triggers.command_patterns.map(
130
+ (p) => ({ kind: "command_pattern", pattern: p })
131
+ ),
132
+ ...cluster.triggers.keywords.map((p) => ({ kind: "keyword", pattern: p }))
133
+ ];
134
+ const ids = [];
135
+ for (const spec of specs) {
136
+ const key = `${spec.kind}|${spec.pattern}`;
137
+ let id = triggerIdByKey.get(key);
138
+ if (id === void 0) {
139
+ id = makeTriggerId(spec);
140
+ triggerIdByKey.set(key, id);
141
+ triggersById.set(id, { kind: spec.kind, pattern: spec.pattern });
142
+ }
143
+ if (!ids.includes(id)) ids.push(id);
144
+ }
145
+ return ids;
146
+ }
147
+ var TRIGGER_PREFIX = {
148
+ file_glob: "glob",
149
+ command_pattern: "cmd",
150
+ keyword: "kw"
151
+ };
152
+ function makeTriggerId(spec) {
153
+ const hash = createHash("sha1").update(`${spec.kind}|${spec.pattern}`).digest("hex").slice(0, 8);
154
+ return `t-${TRIGGER_PREFIX[spec.kind]}-${hash}`;
155
+ }
156
+ var RULE_HEADING_RE = /^##\s+Rules\b.*$/i;
157
+ var NEXT_HEADING_RE = /^##\s+/;
158
+ var RULE_LINE_RE = /^(\d+)\.\s+(.+?)\s*$/;
159
+ var EVIDENCE_TAIL_RE = /\s*\(Evidence:?\s+([^)]+)\)\s*$/;
160
+ var EVIDENCE_REF_RE = /L\d+/g;
161
+ function parseRulesSection(markdown) {
162
+ const lines = markdown.split(/\r?\n/);
163
+ let inRules = false;
164
+ const rules = [];
165
+ for (const line of lines) {
166
+ if (!inRules) {
167
+ if (RULE_HEADING_RE.test(line)) inRules = true;
168
+ continue;
169
+ }
170
+ if (NEXT_HEADING_RE.test(line)) break;
171
+ const m = RULE_LINE_RE.exec(line);
172
+ if (m === null) continue;
173
+ const ruleIndex = Number(m[1]);
174
+ let body = m[2];
175
+ const evidence = [];
176
+ let tail = EVIDENCE_TAIL_RE.exec(body);
177
+ while (tail !== null) {
178
+ const refs = tail[1];
179
+ const matches = refs.match(EVIDENCE_REF_RE);
180
+ if (matches !== null) evidence.unshift(...matches);
181
+ body = body.slice(0, tail.index).trimEnd();
182
+ tail = EVIDENCE_TAIL_RE.exec(body);
183
+ }
184
+ rules.push({ index: ruleIndex, body, evidence });
185
+ }
186
+ return rules;
187
+ }
188
+ var LEGACY_ARTIFACT_REL = [
189
+ "index.yaml",
190
+ "journal.md",
191
+ "journal.legacy.md",
192
+ "topics",
193
+ "distill-ledger.yaml",
194
+ "distill-proposal.md"
195
+ ];
196
+ function deleteLegacyArtifacts(baseDir) {
197
+ const deleted = [];
198
+ for (const rel of LEGACY_ARTIFACT_REL) {
199
+ const abs = join(baseDir, rel);
200
+ if (!existsSync(abs)) continue;
201
+ rmSync(abs, { recursive: true, force: true });
202
+ deleted.push(abs);
203
+ }
204
+ return deleted;
205
+ }
55
206
  function normalizeRule(rule) {
56
207
  return rule.trim().replace(/\s+/g, " ").toLowerCase();
57
208
  }
@@ -85,7 +236,7 @@ function mergeTriggers(graph, spec) {
85
236
  if (!triggerIds.includes(existing)) triggerIds.push(existing);
86
237
  continue;
87
238
  }
88
- const id = makeTriggerId(spec2);
239
+ const id = makeTriggerId2(spec2);
89
240
  graph.triggers[id] = { kind: spec2.kind, pattern: spec2.pattern };
90
241
  reverseLookup.set(key, id);
91
242
  triggerIds.push(id);
@@ -96,14 +247,14 @@ function mergeTriggers(graph, spec) {
96
247
  function triggerKey(t) {
97
248
  return `${t.kind}|${t.pattern}`;
98
249
  }
99
- var TRIGGER_PREFIX = {
250
+ var TRIGGER_PREFIX2 = {
100
251
  file_glob: "glob",
101
252
  command_pattern: "cmd",
102
253
  keyword: "kw"
103
254
  };
104
- function makeTriggerId(spec) {
255
+ function makeTriggerId2(spec) {
105
256
  const hash = createHash("sha1").update(triggerKey(spec)).digest("hex").slice(0, 8);
106
- return `t-${TRIGGER_PREFIX[spec.kind]}-${hash}`;
257
+ return `t-${TRIGGER_PREFIX2[spec.kind]}-${hash}`;
107
258
  }
108
259
  function makeLessonId(graph, topic, ruleKey) {
109
260
  const slug = ruleToSlug(ruleKey);
@@ -397,338 +548,32 @@ function jaccard(a, b) {
397
548
  for (const t of a) if (b.has(t)) intersection += 1;
398
549
  return intersection / (a.size + b.size - intersection);
399
550
  }
400
- var GRAPH_REL_PATH = ".agentsmesh/lessons/lessons.json";
401
- function graphFilePath(projectRoot) {
402
- return resolve(projectRoot, GRAPH_REL_PATH);
403
- }
404
- function loadLessonsGraph(projectRoot) {
405
- const raw = readFileSync(graphFilePath(projectRoot), "utf8");
406
- return parseGraph(JSON.parse(raw));
407
- }
408
- function tryLoadLessonsGraph(projectRoot) {
409
- if (!existsSync(graphFilePath(projectRoot))) return null;
410
- return loadLessonsGraph(projectRoot);
411
- }
412
- function loadLessonsGraphResilient(projectRoot) {
413
- const path = graphFilePath(projectRoot);
414
- if (!existsSync(path)) return { status: "absent", graph: null };
415
- try {
416
- const parsed = JSON.parse(readFileSync(path, "utf8"));
417
- const version = parsed?.version;
418
- if (typeof version === "number" && version > CURRENT_GRAPH_VERSION) {
419
- return { status: "newer-version", graph: null, version };
420
- }
421
- return { status: "ok", graph: parseGraph(parsed) };
422
- } catch (error) {
423
- return {
424
- status: "corrupt",
425
- graph: null,
426
- error: error instanceof Error ? error : new Error(String(error))
427
- };
551
+
552
+ // src/core/errors.ts
553
+ var AgentsMeshError = class extends Error {
554
+ code;
555
+ constructor(code, message, options) {
556
+ super(message, options);
557
+ this.name = "AgentsMeshError";
558
+ this.code = code;
428
559
  }
429
- }
430
- function saveLessonsGraph(projectRoot, graph) {
431
- const path = graphFilePath(projectRoot);
432
- mkdirSync(dirname(path), { recursive: true });
433
- const tmp = `${path}.${process.pid}.tmp`;
434
- writeFileSync(tmp, serializeGraph(graph), "utf8");
435
- renameSync(tmp, path);
436
- }
437
- function serializeGraph(graph) {
438
- return `${JSON.stringify(canonicalize(graph), null, 2)}
439
- `;
440
- }
441
- function canonicalize(value) {
442
- if (value === null) return null;
443
- if (Array.isArray(value)) return value.map(canonicalize);
444
- if (typeof value === "object") {
445
- const entries = Object.entries(value).sort(
446
- ([a], [b]) => a < b ? -1 : 1
560
+ };
561
+ var LockAcquisitionError = class extends AgentsMeshError {
562
+ lockPath;
563
+ holder;
564
+ /** Human-readable lock name surfaced in the message, e.g. "lessons lock". */
565
+ label;
566
+ constructor(lockPath, holder, options) {
567
+ const label = options?.label ?? "lock";
568
+ super(
569
+ "AM_LOCK_ACQUISITION_FAILED",
570
+ `Could not acquire ${label} at ${lockPath}: currently held by ${holder}. Wait for the other process to finish, or remove ${lockPath} manually if you are sure no agentsmesh process is running.`,
571
+ options
447
572
  );
448
- const out = {};
449
- for (const [k, v] of entries) out[k] = canonicalize(v);
450
- return out;
451
- }
452
- return value;
453
- }
454
- var LegacyTriggersSchema = z.object({
455
- file_globs: z.array(z.string()),
456
- command_patterns: z.array(z.string()),
457
- keywords: z.array(z.string())
458
- }).refine((t) => t.file_globs.length + t.command_patterns.length + t.keywords.length > 0, {
459
- message: "cluster must declare at least one trigger of any type"
460
- });
461
- var LegacyClusterSchema = z.object({
462
- topic: z.string().regex(/^[a-z0-9-]+$/),
463
- file: z.string().regex(/\.md$/),
464
- summary: z.string().min(1),
465
- triggers: LegacyTriggersSchema
466
- });
467
- var LegacyIndexSchema = z.object({
468
- version: z.literal(1),
469
- clusters: z.array(LegacyClusterSchema)
470
- });
471
- function collectClusterTriggerIds(cluster, triggersById, triggerIdByKey) {
472
- const specs = [
473
- ...cluster.triggers.file_globs.map((p) => ({ kind: "file_glob", pattern: p })),
474
- ...cluster.triggers.command_patterns.map(
475
- (p) => ({ kind: "command_pattern", pattern: p })
476
- ),
477
- ...cluster.triggers.keywords.map((p) => ({ kind: "keyword", pattern: p }))
478
- ];
479
- const ids = [];
480
- for (const spec of specs) {
481
- const key = `${spec.kind}|${spec.pattern}`;
482
- let id = triggerIdByKey.get(key);
483
- if (id === void 0) {
484
- id = makeTriggerId2(spec);
485
- triggerIdByKey.set(key, id);
486
- triggersById.set(id, { kind: spec.kind, pattern: spec.pattern });
487
- }
488
- if (!ids.includes(id)) ids.push(id);
489
- }
490
- return ids;
491
- }
492
- var TRIGGER_PREFIX2 = {
493
- file_glob: "glob",
494
- command_pattern: "cmd",
495
- keyword: "kw"
496
- };
497
- function makeTriggerId2(spec) {
498
- const hash = createHash("sha1").update(`${spec.kind}|${spec.pattern}`).digest("hex").slice(0, 8);
499
- return `t-${TRIGGER_PREFIX2[spec.kind]}-${hash}`;
500
- }
501
- var RULE_HEADING_RE = /^##\s+Rules\b.*$/i;
502
- var NEXT_HEADING_RE = /^##\s+/;
503
- var RULE_LINE_RE = /^(\d+)\.\s+(.+?)\s*$/;
504
- var EVIDENCE_TAIL_RE = /\s*\(Evidence:?\s+([^)]+)\)\s*$/;
505
- var EVIDENCE_REF_RE = /L\d+/g;
506
- function parseRulesSection(markdown) {
507
- const lines = markdown.split(/\r?\n/);
508
- let inRules = false;
509
- const rules = [];
510
- for (const line of lines) {
511
- if (!inRules) {
512
- if (RULE_HEADING_RE.test(line)) inRules = true;
513
- continue;
514
- }
515
- if (NEXT_HEADING_RE.test(line)) break;
516
- const m = RULE_LINE_RE.exec(line);
517
- if (m === null) continue;
518
- const ruleIndex = Number(m[1]);
519
- let body = m[2];
520
- const evidence = [];
521
- let tail = EVIDENCE_TAIL_RE.exec(body);
522
- while (tail !== null) {
523
- const refs = tail[1];
524
- const matches = refs.match(EVIDENCE_REF_RE);
525
- if (matches !== null) evidence.unshift(...matches);
526
- body = body.slice(0, tail.index).trimEnd();
527
- tail = EVIDENCE_TAIL_RE.exec(body);
528
- }
529
- rules.push({ index: ruleIndex, body, evidence });
530
- }
531
- return rules;
532
- }
533
- var LEGACY_ARTIFACT_REL = [
534
- "index.yaml",
535
- "journal.md",
536
- "journal.legacy.md",
537
- "topics",
538
- "distill-ledger.yaml",
539
- "distill-proposal.md"
540
- ];
541
- function deleteLegacyArtifacts(baseDir) {
542
- const deleted = [];
543
- for (const rel of LEGACY_ARTIFACT_REL) {
544
- const abs = join(baseDir, rel);
545
- if (!existsSync(abs)) continue;
546
- rmSync(abs, { recursive: true, force: true });
547
- deleted.push(abs);
548
- }
549
- return deleted;
550
- }
551
-
552
- // src/lessons/import-legacy-merge.ts
553
- async function mergeLegacy(projectRoot, paths, specs, summaryByTopic, options) {
554
- let addedLessons = 0;
555
- const addedTriggers = /* @__PURE__ */ new Set();
556
- const touchedTopics = /* @__PURE__ */ new Set();
557
- await mutateLessonsGraphLocked(projectRoot, (g) => {
558
- addedLessons = 0;
559
- addedTriggers.clear();
560
- touchedTopics.clear();
561
- for (const spec of specs) {
562
- const result = addLessonInto(g, spec, {
563
- allowNewTopic: true,
564
- topicSummary: summaryByTopic.get(spec.topic),
565
- // Legacy lessons may predate the ≥1-trigger requirement; recover them
566
- // as-is rather than dropping historical knowledge.
567
- allowNoTrigger: true
568
- });
569
- if (result.isNewLesson) addedLessons += 1;
570
- for (const t of result.newTriggerIds) addedTriggers.add(t);
571
- touchedTopics.add(spec.topic);
572
- }
573
- });
574
- const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
575
- return {
576
- wroteGraphPath: paths.graph,
577
- deletedPaths,
578
- topicCount: touchedTopics.size,
579
- lessonCount: addedLessons,
580
- triggerCount: addedTriggers.size
581
- };
582
- }
583
- var BASE_REL = ".agentsmesh/lessons";
584
- function lessonsPaths(projectRoot) {
585
- const base = join(projectRoot, BASE_REL);
586
- return {
587
- base,
588
- graph: join(base, "lessons.json"),
589
- config: join(base, "config.json"),
590
- journal: join(base, "journal.md"),
591
- index: join(base, "index.yaml"),
592
- topicsDir: join(base, "topics")
593
- };
594
- }
595
- function toRelPath(projectRoot, absolute) {
596
- return relative(projectRoot, absolute).split(sep).join("/");
597
- }
598
- var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
599
-
600
- Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
601
-
602
- **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.
603
-
604
- **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>\`.
605
-
606
- **Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`;
607
-
608
- // src/lessons/import-legacy.ts
609
- var LessonsGraphExistsError = class extends Error {
610
- code = "LESSONS_GRAPH_EXISTS";
611
- constructor() {
612
- super("importLegacyLessons: a non-empty lessons.json already exists; pass force to overwrite.");
613
- this.name = "LessonsGraphExistsError";
614
- }
615
- };
616
- async function importLegacyLessons(projectRoot, options) {
617
- const paths = lessonsPaths(projectRoot);
618
- const indexRaw = readFileSync(paths.index, "utf8");
619
- const index = LegacyIndexSchema.parse(parse(indexRaw));
620
- const topics = {};
621
- const triggersById = /* @__PURE__ */ new Map();
622
- const triggerIdByKey = /* @__PURE__ */ new Map();
623
- const lessons = {};
624
- const specs = [];
625
- const summaryByTopic = /* @__PURE__ */ new Map();
626
- for (const cluster of index.clusters) {
627
- topics[cluster.topic] = { summary: cluster.summary };
628
- summaryByTopic.set(cluster.topic, cluster.summary);
629
- const clusterTriggerIds = collectClusterTriggerIds(cluster, triggersById, triggerIdByKey);
630
- const topicFile = join(projectRoot, cluster.file);
631
- if (!existsSync(topicFile)) {
632
- throw new Error(
633
- `importLegacyLessons: declared topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
634
- );
635
- }
636
- const topicMarkdown = readFileSync(topicFile, "utf8");
637
- for (const { index: ruleIndex, body, evidence } of parseRulesSection(topicMarkdown)) {
638
- const lessonEvidence = [
639
- `legacy:${cluster.file}#rule-${ruleIndex}`,
640
- ...evidence.map((e) => `legacy:${e}`)
641
- ];
642
- lessons[`${cluster.topic}-rule-${ruleIndex}`] = {
643
- rule: body,
644
- topics: [cluster.topic],
645
- triggers: clusterTriggerIds,
646
- evidence: lessonEvidence,
647
- status: "active",
648
- createdAt: options.migratedAt
649
- };
650
- specs.push({
651
- rule: body,
652
- topic: cluster.topic,
653
- triggers: {
654
- files: cluster.triggers.file_globs,
655
- commands: cluster.triggers.command_patterns,
656
- keywords: cluster.triggers.keywords
657
- },
658
- evidence: lessonEvidence,
659
- createdAt: options.migratedAt
660
- });
661
- }
662
- }
663
- if (options.merge === true)
664
- return mergeLegacy(projectRoot, paths, specs, summaryByTopic, options);
665
- const triggers = Object.fromEntries(triggersById.entries());
666
- await mutateLessonsGraphLocked(projectRoot, (g) => {
667
- const populated = Object.keys(g.lessons).length > 0 || Object.keys(g.topics).length > 0 || Object.keys(g.triggers).length > 0;
668
- if (options.force !== true && populated) {
669
- throw new LessonsGraphExistsError();
670
- }
671
- g.version = CURRENT_GRAPH_VERSION;
672
- g.lessons = lessons;
673
- g.topics = topics;
674
- g.triggers = triggers;
675
- });
676
- const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
677
- return {
678
- wroteGraphPath: paths.graph,
679
- deletedPaths,
680
- topicCount: Object.keys(topics).length,
681
- lessonCount: Object.keys(lessons).length,
682
- triggerCount: triggersById.size
683
- };
684
- }
685
-
686
- // src/lessons/auto-migrate.ts
687
- function todayIso2() {
688
- const now = /* @__PURE__ */ new Date();
689
- const y = now.getUTCFullYear();
690
- const m = String(now.getUTCMonth() + 1).padStart(2, "0");
691
- const d = String(now.getUTCDate()).padStart(2, "0");
692
- return `${y}-${m}-${d}`;
693
- }
694
- async function maybeAutoMigrateLessons(projectRoot) {
695
- if (existsSync(graphFilePath(projectRoot))) return false;
696
- const paths = lessonsPaths(projectRoot);
697
- if (!existsSync(paths.index)) return false;
698
- try {
699
- await importLegacyLessons(projectRoot, { migratedAt: todayIso2() });
700
- return true;
701
- } catch (err) {
702
- if (err instanceof LessonsGraphExistsError) return false;
703
- throw err;
704
- }
705
- }
706
-
707
- // src/core/errors.ts
708
- var AgentsMeshError = class extends Error {
709
- code;
710
- constructor(code, message, options) {
711
- super(message, options);
712
- this.name = "AgentsMeshError";
713
- this.code = code;
714
- }
715
- };
716
- var LockAcquisitionError = class extends AgentsMeshError {
717
- lockPath;
718
- holder;
719
- /** Human-readable lock name surfaced in the message, e.g. "lessons lock". */
720
- label;
721
- constructor(lockPath, holder, options) {
722
- const label = options?.label ?? "lock";
723
- super(
724
- "AM_LOCK_ACQUISITION_FAILED",
725
- `Could not acquire ${label} at ${lockPath}: currently held by ${holder}. Wait for the other process to finish, or remove ${lockPath} manually if you are sure no agentsmesh process is running.`,
726
- options
727
- );
728
- this.name = "LockAcquisitionError";
729
- this.lockPath = lockPath;
730
- this.holder = holder;
731
- this.label = label;
573
+ this.name = "LockAcquisitionError";
574
+ this.lockPath = lockPath;
575
+ this.holder = holder;
576
+ this.label = label;
732
577
  }
733
578
  };
734
579
  var FileSystemError = class extends AgentsMeshError {
@@ -856,7 +701,7 @@ function getHostname() {
856
701
  return hostname();
857
702
  }
858
703
  function sleep(ms) {
859
- return new Promise((resolve7) => setTimeout(resolve7, ms));
704
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
860
705
  }
861
706
 
862
707
  // src/lessons/lessons-lock.ts
@@ -1807,255 +1652,159 @@ function findExistingLessonByRule(graph, ruleKey) {
1807
1652
  return null;
1808
1653
  }
1809
1654
 
1810
- // src/lessons/ranking-signals.ts
1811
- function buildFanout(graph) {
1812
- const fanout = /* @__PURE__ */ new Map();
1813
- for (const lesson of Object.values(graph.lessons)) {
1814
- if (lesson.status !== "active") continue;
1815
- for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
1816
- }
1817
- return fanout;
1818
- }
1819
- function buildTopicCoherence(matches) {
1820
- const topicCount = /* @__PURE__ */ new Map();
1821
- for (const { lesson } of matches) {
1822
- for (const t of lesson.topics) topicCount.set(t, (topicCount.get(t) ?? 0) + 1);
1823
- }
1824
- const coherence = /* @__PURE__ */ new Map();
1825
- for (const { id, lesson } of matches) {
1826
- let best = 0;
1827
- for (const t of lesson.topics) best = Math.max(best, topicCount.get(t));
1828
- coherence.set(id, best);
1829
- }
1830
- return coherence;
1831
- }
1832
-
1833
- // src/lessons/prune.ts
1834
- function planPrune(graph, options = {}) {
1835
- const cap = Math.max(1, options.cap ?? MAX_RECOMMENDED_TRIGGERS);
1836
- const fanout = buildFanout(graph);
1837
- const trimmedLessons = [];
1838
- const keptByLesson = /* @__PURE__ */ new Map();
1839
- for (const [id, lesson] of Object.entries(graph.lessons)) {
1840
- if (lesson.status !== "active") continue;
1841
- if (options.trimOverCap === false || lesson.triggers.length <= cap) {
1842
- keptByLesson.set(id, lesson.triggers);
1843
- continue;
1844
- }
1845
- const ordered = [...lesson.triggers].sort((a, b) => {
1846
- const fa = fanout.get(a);
1847
- const fb = fanout.get(b);
1848
- return fa !== fb ? fa - fb : a < b ? -1 : 1;
1849
- });
1850
- const drop = new Set(ordered.slice(cap));
1851
- const kept = lesson.triggers.filter((t) => !drop.has(t));
1852
- keptByLesson.set(id, kept);
1853
- trimmedLessons.push({ id, removedTriggers: [...drop], keptCount: kept.length });
1854
- }
1855
- const removedDeadGlobs = [];
1856
- const unreachableLessons = [];
1857
- if (options.knownPaths !== void 0) {
1858
- const dead = deadFileGlobIds(graph, options.knownPaths);
1859
- if (dead.size > 0) {
1860
- for (const [id, kept] of keptByLesson) {
1861
- const deadInLesson = kept.filter((t) => dead.has(t));
1862
- if (deadInLesson.length === 0) continue;
1863
- const remaining = kept.filter((t) => !dead.has(t));
1864
- if (remaining.length >= 1) {
1865
- keptByLesson.set(id, remaining);
1866
- removedDeadGlobs.push({ id, removedTriggers: deadInLesson, keptCount: remaining.length });
1867
- } else {
1868
- unreachableLessons.push(id);
1869
- }
1870
- }
1871
- unreachableLessons.sort();
1655
+ // src/lessons/import-legacy-merge.ts
1656
+ async function mergeLegacy(projectRoot, paths, specs, summaryByTopic, options) {
1657
+ let addedLessons = 0;
1658
+ const addedTriggers = /* @__PURE__ */ new Set();
1659
+ const touchedTopics = /* @__PURE__ */ new Set();
1660
+ await mutateLessonsGraphLocked(projectRoot, (g) => {
1661
+ addedLessons = 0;
1662
+ addedTriggers.clear();
1663
+ touchedTopics.clear();
1664
+ for (const spec of specs) {
1665
+ const result = addLessonInto(g, spec, {
1666
+ allowNewTopic: true,
1667
+ topicSummary: summaryByTopic.get(spec.topic),
1668
+ // Legacy lessons may predate the ≥1-trigger requirement; recover them
1669
+ // as-is rather than dropping historical knowledge.
1670
+ allowNoTrigger: true
1671
+ });
1672
+ if (result.isNewLesson) addedLessons += 1;
1673
+ for (const t of result.newTriggerIds) addedTriggers.add(t);
1674
+ touchedTopics.add(spec.topic);
1872
1675
  }
1873
- }
1874
- const live = /* @__PURE__ */ new Set();
1875
- for (const kept of keptByLesson.values()) for (const t of kept) live.add(t);
1876
- const removedTriggerIds = Object.keys(graph.triggers).filter((t) => !live.has(t)).sort();
1877
- const referencedTopics = /* @__PURE__ */ new Set();
1878
- for (const lesson of Object.values(graph.lessons)) {
1879
- for (const topic of lesson.topics) referencedTopics.add(topic);
1880
- }
1881
- const removedTopicIds = Object.keys(graph.topics).filter((t) => !referencedTopics.has(t)).sort();
1882
- return { removedTriggerIds, removedTopicIds, trimmedLessons, removedDeadGlobs, unreachableLessons, cap };
1676
+ });
1677
+ const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
1678
+ return {
1679
+ wroteGraphPath: paths.graph,
1680
+ deletedPaths,
1681
+ topicCount: touchedTopics.size,
1682
+ lessonCount: addedLessons,
1683
+ triggerCount: addedTriggers.size
1684
+ };
1883
1685
  }
1884
- function applyPruneToGraph(graph, plan) {
1885
- for (const trim of [...plan.trimmedLessons, ...plan.removedDeadGlobs ?? []]) {
1886
- const lesson = graph.lessons[trim.id];
1887
- if (lesson === void 0) continue;
1888
- const drop = new Set(trim.removedTriggers);
1889
- graph.lessons[trim.id] = { ...lesson, triggers: lesson.triggers.filter((t) => !drop.has(t)) };
1890
- }
1891
- for (const topicId of plan.removedTopicIds) delete graph.topics[topicId];
1892
- const dead = new Set(plan.removedTriggerIds);
1893
- if (dead.size === 0) return;
1894
- for (const [id, lesson] of Object.entries(graph.lessons)) {
1895
- if (lesson.triggers.some((t) => dead.has(t))) {
1896
- graph.lessons[id] = { ...lesson, triggers: lesson.triggers.filter((t) => !dead.has(t)) };
1897
- }
1898
- }
1899
- for (const t of dead) delete graph.triggers[t];
1686
+ var BASE_REL = ".agentsmesh/lessons";
1687
+ function lessonsPaths(projectRoot) {
1688
+ const base = join(projectRoot, BASE_REL);
1689
+ return {
1690
+ base,
1691
+ graph: join(base, "lessons.json"),
1692
+ config: join(base, "config.json"),
1693
+ journal: join(base, "journal.md"),
1694
+ index: join(base, "index.yaml"),
1695
+ topicsDir: join(base, "topics")
1696
+ };
1900
1697
  }
1901
- function isEmptyPrunePlan(plan) {
1902
- return plan.removedTriggerIds.length === 0 && plan.removedTopicIds.length === 0 && plan.trimmedLessons.length === 0 && plan.removedDeadGlobs.length === 0;
1698
+ function toRelPath(projectRoot, absolute) {
1699
+ return relative(projectRoot, absolute).split(sep).join("/");
1903
1700
  }
1701
+ var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
1904
1702
 
1905
- // src/lessons/auto-prune.ts
1906
- function isAutoPruneEnabled(projectRoot) {
1907
- const path = lessonsPaths(projectRoot).config;
1908
- if (!existsSync(path)) return false;
1909
- try {
1910
- const parsed = JSON.parse(readFileSync(path, "utf8"));
1911
- if (typeof parsed !== "object" || parsed === null) return false;
1912
- return parsed.autoPrune === true;
1913
- } catch {
1914
- return false;
1703
+ Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
1704
+
1705
+ **Recall:** before every file edit or state-changing command, MUST run \`agentsmesh lessons query --file <path> --cmd <command> --session auto\` and obey matches; at task start, ALSO run \`agentsmesh lessons query --keyword "<task terms>" --always --session auto\` for conceptual + universal rules no path/command names. Pure-read commands and recall itself are exempt.
1706
+
1707
+ **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>\`.
1708
+
1709
+ **Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`;
1710
+
1711
+ // src/lessons/import-legacy.ts
1712
+ var LessonsGraphExistsError = class extends Error {
1713
+ code = "LESSONS_GRAPH_EXISTS";
1714
+ constructor() {
1715
+ super("importLegacyLessons: a non-empty lessons.json already exists; pass force to overwrite.");
1716
+ this.name = "LessonsGraphExistsError";
1915
1717
  }
1916
- }
1917
- async function maybeAutoPrune(projectRoot, knownPaths) {
1918
- if (!isAutoPruneEnabled(projectRoot)) return null;
1919
- const preview = tryLoadLessonsGraph(projectRoot);
1920
- if (preview === null) return null;
1921
- if (isEmptyPrunePlan(planPrune(preview, { trimOverCap: false, knownPaths }))) return null;
1922
- let summary = { removedTriggers: 0, removedTopics: 0, detachedDeadGlobs: 0 };
1923
- await mutateLessonsGraph(projectRoot, (graph) => {
1924
- const plan = planPrune(graph, { trimOverCap: false, knownPaths });
1925
- if (isEmptyPrunePlan(plan)) return;
1926
- applyPruneToGraph(graph, plan);
1927
- summary = {
1928
- removedTriggers: plan.removedTriggerIds.length,
1929
- removedTopics: plan.removedTopicIds.length,
1930
- detachedDeadGlobs: plan.removedDeadGlobs.reduce((n, t) => n + t.removedTriggers.length, 0)
1931
- };
1932
- });
1933
- const total = summary.removedTriggers + summary.removedTopics + summary.detachedDeadGlobs;
1934
- return total > 0 ? summary : null;
1935
- }
1936
- function appendJsonl(path, record, opts) {
1937
- mkdirSync(dirname(path), { recursive: true });
1938
- appendFileSync(path, `${JSON.stringify(record)}
1939
- `, "utf8");
1940
- if (statSync(path).size > opts.trimTriggerBytes) capJsonl(path, opts.maxRecords);
1941
- }
1942
- function capJsonl(path, maxRecords) {
1943
- if (!existsSync(path)) return;
1944
- const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim().length > 0);
1945
- if (lines.length <= maxRecords) return;
1946
- const kept = lines.slice(lines.length - maxRecords);
1947
- const tmp = `${path}.${process.pid}.tmp`;
1948
- writeFileSync(tmp, `${kept.join("\n")}
1949
- `, "utf8");
1950
- renameSync(tmp, path);
1951
- }
1952
- function readJsonl(path) {
1953
- if (!existsSync(path)) return [];
1954
- const out = [];
1955
- for (const line of readFileSync(path, "utf8").split("\n")) {
1956
- if (line.trim().length === 0) continue;
1957
- try {
1958
- out.push(JSON.parse(line));
1959
- } catch {
1718
+ };
1719
+ async function importLegacyLessons(projectRoot, options) {
1720
+ const paths = lessonsPaths(projectRoot);
1721
+ const indexRaw = readFileSync(paths.index, "utf8");
1722
+ const index = LegacyIndexSchema.parse(parse(indexRaw));
1723
+ const topics = {};
1724
+ const triggersById = /* @__PURE__ */ new Map();
1725
+ const triggerIdByKey = /* @__PURE__ */ new Map();
1726
+ const lessons = {};
1727
+ const specs = [];
1728
+ const summaryByTopic = /* @__PURE__ */ new Map();
1729
+ for (const cluster of index.clusters) {
1730
+ topics[cluster.topic] = { summary: cluster.summary };
1731
+ summaryByTopic.set(cluster.topic, cluster.summary);
1732
+ const clusterTriggerIds = collectClusterTriggerIds(cluster, triggersById, triggerIdByKey);
1733
+ const topicFile = join(projectRoot, cluster.file);
1734
+ if (!existsSync(topicFile)) {
1735
+ throw new Error(
1736
+ `importLegacyLessons: declared topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
1737
+ );
1738
+ }
1739
+ const topicMarkdown = readFileSync(topicFile, "utf8");
1740
+ for (const { index: ruleIndex, body, evidence } of parseRulesSection(topicMarkdown)) {
1741
+ const lessonEvidence = [
1742
+ `legacy:${cluster.file}#rule-${ruleIndex}`,
1743
+ ...evidence.map((e) => `legacy:${e}`)
1744
+ ];
1745
+ lessons[`${cluster.topic}-rule-${ruleIndex}`] = {
1746
+ rule: body,
1747
+ topics: [cluster.topic],
1748
+ triggers: clusterTriggerIds,
1749
+ evidence: lessonEvidence,
1750
+ status: "active",
1751
+ createdAt: options.migratedAt
1752
+ };
1753
+ specs.push({
1754
+ rule: body,
1755
+ topic: cluster.topic,
1756
+ triggers: {
1757
+ files: cluster.triggers.file_globs,
1758
+ commands: cluster.triggers.command_patterns,
1759
+ keywords: cluster.triggers.keywords
1760
+ },
1761
+ evidence: lessonEvidence,
1762
+ createdAt: options.migratedAt
1763
+ });
1960
1764
  }
1961
1765
  }
1962
- return out;
1963
- }
1964
- var MAX_RECALL_LOG_RECORDS = 5e3;
1965
- var RECALL_LOG_TRIM_TRIGGER_BYTES = 2e6;
1966
- var TELEMETRY_ENV = "AGENTSMESH_LESSONS_TELEMETRY";
1967
- var SESSION_ENV = "AGENTSMESH_SESSION_ID";
1968
- function sessionId(env = process.env) {
1969
- const raw = env[SESSION_ENV];
1970
- return raw !== void 0 && raw.trim().length > 0 ? raw : void 0;
1971
- }
1972
- function recallLogPath(projectRoot) {
1973
- return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
1974
- }
1975
- function isTelemetryEnabled(env = process.env) {
1976
- return env[TELEMETRY_ENV] === "1";
1977
- }
1978
- function appendRecallRecord(projectRoot, record, env = process.env) {
1979
- if (!isTelemetryEnabled(env)) return;
1980
- appendJsonl(recallLogPath(projectRoot), record, {
1981
- maxRecords: MAX_RECALL_LOG_RECORDS,
1982
- trimTriggerBytes: RECALL_LOG_TRIM_TRIGGER_BYTES
1766
+ if (options.merge === true)
1767
+ return mergeLegacy(projectRoot, paths, specs, summaryByTopic, options);
1768
+ const triggers = Object.fromEntries(triggersById.entries());
1769
+ await mutateLessonsGraphLocked(projectRoot, (g) => {
1770
+ const populated = Object.keys(g.lessons).length > 0 || Object.keys(g.topics).length > 0 || Object.keys(g.triggers).length > 0;
1771
+ if (options.force !== true && populated) {
1772
+ throw new LessonsGraphExistsError();
1773
+ }
1774
+ g.version = CURRENT_GRAPH_VERSION;
1775
+ g.lessons = lessons;
1776
+ g.topics = topics;
1777
+ g.triggers = triggers;
1983
1778
  });
1779
+ const deletedPaths = options.deleteLegacy === false ? [] : deleteLegacyArtifacts(paths.base);
1780
+ return {
1781
+ wroteGraphPath: paths.graph,
1782
+ deletedPaths,
1783
+ topicCount: Object.keys(topics).length,
1784
+ lessonCount: Object.keys(lessons).length,
1785
+ triggerCount: triggersById.size
1786
+ };
1984
1787
  }
1985
1788
 
1986
- // src/lessons/capture-telemetry.ts
1987
- var MAX_CAPTURE_LOG_RECORDS = 5e3;
1988
- var CAPTURE_LOG_TRIM_TRIGGER_BYTES = 2e6;
1989
- function captureLogPath(projectRoot) {
1990
- return join(lessonsPaths(projectRoot).base, "capture-log.jsonl");
1991
- }
1992
- function appendCaptureRecord(projectRoot, record, env = process.env) {
1993
- if (!isTelemetryEnabled(env)) return;
1994
- appendJsonl(captureLogPath(projectRoot), record, {
1995
- maxRecords: MAX_CAPTURE_LOG_RECORDS,
1996
- trimTriggerBytes: CAPTURE_LOG_TRIM_TRIGGER_BYTES
1997
- });
1998
- }
1999
- function recordCapture(projectRoot, triggerKinds, result, env = process.env) {
2000
- if (!isTelemetryEnabled(env)) return;
2001
- const session = sessionId(env);
2002
- appendCaptureRecord(
2003
- projectRoot,
2004
- {
2005
- ts: (/* @__PURE__ */ new Date()).toISOString(),
2006
- isNewLesson: result?.isNewLesson ?? false,
2007
- isNewTopic: result?.isNewTopic ?? false,
2008
- newTriggerCount: result?.newTriggerIds.length ?? 0,
2009
- triggerKinds,
2010
- blocked: result === null,
2011
- warningCodes: result?.warnings.map((w) => w.code) ?? [],
2012
- ...session !== void 0 ? { session } : {},
2013
- ...result !== null ? { lessonId: result.id } : {}
2014
- },
2015
- env
2016
- );
2017
- }
2018
- function normalizeRecallFile(file, projectRoot) {
2019
- const forward = file.replaceAll("\\", "/");
2020
- const direct = relativize(projectRoot, forward);
2021
- if (!direct.startsWith("../")) return direct;
2022
- const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
2023
- return viaReal.startsWith("../") ? direct : viaReal;
2024
- }
2025
- function relativize(root, forward) {
2026
- const rel = relative(root, resolve(root, forward)).replaceAll("\\", "/");
2027
- return rel === "" ? forward.replaceAll("\\", "/") : rel;
2028
- }
2029
- function safeRealpath(path) {
2030
- try {
2031
- return realpathSync(path);
2032
- } catch {
2033
- const parent = dirname(path);
2034
- if (parent === path) return path;
2035
- return resolve(safeRealpath(parent), basename(path));
2036
- }
1789
+ // src/lessons/auto-migrate.ts
1790
+ function todayIso2() {
1791
+ const now = /* @__PURE__ */ new Date();
1792
+ const y = now.getUTCFullYear();
1793
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
1794
+ const d = String(now.getUTCDate()).padStart(2, "0");
1795
+ return `${y}-${m}-${d}`;
2037
1796
  }
2038
- var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
2039
- var MAX_FILES = 2e5;
2040
- function listProjectFiles(projectRoot) {
2041
- const out = /* @__PURE__ */ new Set();
1797
+ async function maybeAutoMigrateLessons(projectRoot) {
1798
+ if (existsSync(graphFilePath(projectRoot))) return false;
1799
+ const paths = lessonsPaths(projectRoot);
1800
+ if (!existsSync(paths.index)) return false;
2042
1801
  try {
2043
- const stack = [projectRoot];
2044
- while (stack.length > 0) {
2045
- const dir = stack.pop();
2046
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
2047
- if (entry.isDirectory()) {
2048
- if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
2049
- } else if (entry.isFile()) {
2050
- out.add(toRelPath(projectRoot, join(dir, entry.name)));
2051
- if (out.size > MAX_FILES) return out;
2052
- }
2053
- }
2054
- }
2055
- } catch {
2056
- return null;
1802
+ await importLegacyLessons(projectRoot, { migratedAt: todayIso2() });
1803
+ return true;
1804
+ } catch (err) {
1805
+ if (err instanceof LessonsGraphExistsError) return false;
1806
+ throw err;
2057
1807
  }
2058
- return out;
2059
1808
  }
2060
1809
 
2061
1810
  // src/lessons/keyword-match.ts
@@ -2142,6 +1891,171 @@ function triggerMatches(trigger, query, budget) {
2142
1891
  return keywordMatches(trigger.pattern, query);
2143
1892
  }
2144
1893
  }
1894
+ function appendJsonl(path, record, opts) {
1895
+ mkdirSync(dirname(path), { recursive: true });
1896
+ appendFileSync(path, `${JSON.stringify(record)}
1897
+ `, "utf8");
1898
+ if (statSync(path).size > opts.trimTriggerBytes) capJsonl(path, opts.maxRecords);
1899
+ }
1900
+ function capJsonl(path, maxRecords) {
1901
+ if (!existsSync(path)) return;
1902
+ const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim().length > 0);
1903
+ if (lines.length <= maxRecords) return;
1904
+ const kept = lines.slice(lines.length - maxRecords);
1905
+ const tmp = `${path}.${process.pid}.tmp`;
1906
+ writeFileSync(tmp, `${kept.join("\n")}
1907
+ `, "utf8");
1908
+ renameSync(tmp, path);
1909
+ }
1910
+ function readJsonl(path) {
1911
+ if (!existsSync(path)) return [];
1912
+ const out = [];
1913
+ for (const line of readFileSync(path, "utf8").split("\n")) {
1914
+ if (line.trim().length === 0) continue;
1915
+ try {
1916
+ out.push(JSON.parse(line));
1917
+ } catch {
1918
+ }
1919
+ }
1920
+ return out;
1921
+ }
1922
+
1923
+ // src/lessons/telemetry.ts
1924
+ var MAX_RECALL_LOG_RECORDS = 5e3;
1925
+ var RECALL_LOG_TRIM_TRIGGER_BYTES = 2e6;
1926
+ var TELEMETRY_ENV = "AGENTSMESH_LESSONS_TELEMETRY";
1927
+ var SESSION_ENV = "AGENTSMESH_SESSION_ID";
1928
+ function sessionId(env = process.env) {
1929
+ const raw = env[SESSION_ENV];
1930
+ return raw !== void 0 && raw.trim().length > 0 ? raw : void 0;
1931
+ }
1932
+ function recallLogPath(projectRoot) {
1933
+ return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
1934
+ }
1935
+ function isTelemetryEnabled(env = process.env) {
1936
+ return env[TELEMETRY_ENV] === "1";
1937
+ }
1938
+ function appendRecallRecord(projectRoot, record, env = process.env) {
1939
+ if (!isTelemetryEnabled(env)) return;
1940
+ appendJsonl(recallLogPath(projectRoot), record, {
1941
+ maxRecords: MAX_RECALL_LOG_RECORDS,
1942
+ trimTriggerBytes: RECALL_LOG_TRIM_TRIGGER_BYTES
1943
+ });
1944
+ }
1945
+
1946
+ // src/lessons/cmd-fastpath.ts
1947
+ var FASTPATH_DIR = "agentsmesh-lessons-cmdidx";
1948
+ function shortHash(value) {
1949
+ let h = 5381;
1950
+ for (let i = 0; i < value.length; i += 1) h = (h << 5) + h + value.charCodeAt(i) >>> 0;
1951
+ return h.toString(36);
1952
+ }
1953
+ function commandFastpathCachePath(projectRoot) {
1954
+ return join(tmpdir(), FASTPATH_DIR, `${shortHash(resolve(projectRoot))}.json`);
1955
+ }
1956
+ function currentGraphStamp(projectRoot) {
1957
+ try {
1958
+ const s = statSync(graphFilePath(projectRoot));
1959
+ return { mtimeMs: s.mtimeMs, size: s.size };
1960
+ } catch {
1961
+ return null;
1962
+ }
1963
+ }
1964
+ function isStringArray(v) {
1965
+ return Array.isArray(v) && v.every((x) => typeof x === "string");
1966
+ }
1967
+ function readCache(path) {
1968
+ try {
1969
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
1970
+ if (typeof parsed !== "object" || parsed === null) return null;
1971
+ const c = parsed;
1972
+ const stamp = c.stamp;
1973
+ if (typeof stamp?.mtimeMs !== "number" || typeof stamp.size !== "number" || !isStringArray(c.commandPatterns) || !isStringArray(c.keywordPatterns)) {
1974
+ return null;
1975
+ }
1976
+ return {
1977
+ stamp: { mtimeMs: stamp.mtimeMs, size: stamp.size },
1978
+ commandPatterns: c.commandPatterns,
1979
+ keywordPatterns: c.keywordPatterns
1980
+ };
1981
+ } catch {
1982
+ return null;
1983
+ }
1984
+ }
1985
+ function refreshCommandFastpath(projectRoot, graph, preReadStamp) {
1986
+ try {
1987
+ if (preReadStamp === null) return;
1988
+ const stamp = currentGraphStamp(projectRoot);
1989
+ if (stamp === null) return;
1990
+ if (stamp.mtimeMs !== preReadStamp.mtimeMs || stamp.size !== preReadStamp.size) return;
1991
+ const path = commandFastpathCachePath(projectRoot);
1992
+ const existing = readCache(path);
1993
+ if (existing !== null && existing.stamp.mtimeMs === stamp.mtimeMs && existing.stamp.size === stamp.size) {
1994
+ return;
1995
+ }
1996
+ const reachable = /* @__PURE__ */ new Set();
1997
+ for (const lesson of Object.values(graph.lessons)) {
1998
+ if (lesson.status !== "active" || lesson.scope === "always") continue;
1999
+ for (const t of lesson.triggers) reachable.add(t);
2000
+ }
2001
+ const commandPatterns = [];
2002
+ const keywordPatterns = [];
2003
+ for (const [id, trigger] of Object.entries(graph.triggers)) {
2004
+ if (!reachable.has(id)) continue;
2005
+ if (trigger.kind === "command_pattern") commandPatterns.push(trigger.pattern);
2006
+ else if (trigger.kind === "keyword") keywordPatterns.push(trigger.pattern);
2007
+ }
2008
+ const cache2 = { stamp, commandPatterns, keywordPatterns };
2009
+ mkdirSync(dirname(path), { recursive: true });
2010
+ const tmp = `${path}.${process.pid}.tmp`;
2011
+ writeFileSync(tmp, JSON.stringify(cache2), "utf8");
2012
+ renameSync(tmp, path);
2013
+ } catch {
2014
+ }
2015
+ }
2016
+ function normalizeRecallFile(file, projectRoot) {
2017
+ const forward = file.replaceAll("\\", "/");
2018
+ const direct = relativize(projectRoot, forward);
2019
+ if (!direct.startsWith("../")) return direct;
2020
+ const viaReal = relativize(safeRealpath(projectRoot), safeRealpath(resolve(projectRoot, forward)));
2021
+ return viaReal.startsWith("../") ? direct : viaReal;
2022
+ }
2023
+ function relativize(root, forward) {
2024
+ const rel = relative(root, resolve(root, forward)).replaceAll("\\", "/");
2025
+ return rel === "" ? forward.replaceAll("\\", "/") : rel;
2026
+ }
2027
+ function safeRealpath(path) {
2028
+ try {
2029
+ return realpathSync(path);
2030
+ } catch {
2031
+ const parent = dirname(path);
2032
+ if (parent === path) return path;
2033
+ return resolve(safeRealpath(parent), basename(path));
2034
+ }
2035
+ }
2036
+
2037
+ // src/lessons/ranking-signals.ts
2038
+ function buildFanout(graph) {
2039
+ const fanout = /* @__PURE__ */ new Map();
2040
+ for (const lesson of Object.values(graph.lessons)) {
2041
+ if (lesson.status !== "active") continue;
2042
+ for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
2043
+ }
2044
+ return fanout;
2045
+ }
2046
+ function buildTopicCoherence(matches) {
2047
+ const topicCount = /* @__PURE__ */ new Map();
2048
+ for (const { lesson } of matches) {
2049
+ for (const t of lesson.topics) topicCount.set(t, (topicCount.get(t) ?? 0) + 1);
2050
+ }
2051
+ const coherence = /* @__PURE__ */ new Map();
2052
+ for (const { id, lesson } of matches) {
2053
+ let best = 0;
2054
+ for (const t of lesson.topics) best = Math.max(best, topicCount.get(t));
2055
+ coherence.set(id, best);
2056
+ }
2057
+ return coherence;
2058
+ }
2145
2059
 
2146
2060
  // src/lessons/ranking.ts
2147
2061
  var DEFAULT_RECALL_LIMIT = 10;
@@ -2236,7 +2150,8 @@ function defaultLessonsConfig() {
2236
2150
  return {
2237
2151
  recallLimit: DEFAULT_RECALL_LIMIT,
2238
2152
  recallMaxTokens: DEFAULT_RECALL_MAX_TOKENS,
2239
- autoPrune: false
2153
+ autoPrune: false,
2154
+ repairTriggers: false
2240
2155
  };
2241
2156
  }
2242
2157
  function positiveInt(value) {
@@ -2297,48 +2212,116 @@ function loadEffectiveness(projectRoot) {
2297
2212
  return map;
2298
2213
  }
2299
2214
  var SEEN_DIR = "agentsmesh-lessons-seen";
2300
- function openSessionDedup(options = {}) {
2301
- if (options.disabled === true) return null;
2302
- const id = options.explicit !== void 0 && options.explicit.trim().length > 0 ? options.explicit.trim() : sessionId(options.env);
2303
- if (id === void 0) return null;
2304
- const path = seenPath(id, options.projectRoot);
2305
- return { sessionId: id, seen: loadSeen(path), path };
2306
- }
2307
- function shortHash(value) {
2215
+ function shortHash2(value) {
2308
2216
  let h = 5381;
2309
2217
  for (let i = 0; i < value.length; i += 1) h = (h << 5) + h + value.charCodeAt(i) >>> 0;
2310
2218
  return h.toString(36);
2311
2219
  }
2312
- function seenPath(id, projectRoot) {
2220
+ function seenStorePath(id, projectRoot) {
2313
2221
  const safe = id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 200);
2314
- const scoped = projectRoot === void 0 ? safe : `${safe}__${shortHash(resolve(projectRoot))}`;
2222
+ const scoped = projectRoot === void 0 ? safe : `${safe}__${shortHash2(resolve(projectRoot))}`;
2315
2223
  return join(tmpdir(), SEEN_DIR, `${scoped}.json`);
2316
2224
  }
2317
- function loadSeen(path) {
2318
- if (!existsSync(path)) return /* @__PURE__ */ new Set();
2225
+ function readSeenStore(path) {
2226
+ if (!existsSync(path)) return { ids: /* @__PURE__ */ new Set(), stamps: null };
2319
2227
  try {
2320
2228
  const parsed = JSON.parse(readFileSync(path, "utf8"));
2321
- if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
2322
- return new Set(parsed.filter((x) => typeof x === "string"));
2229
+ if (Array.isArray(parsed)) {
2230
+ const ids = new Set(parsed.filter((x) => typeof x === "string"));
2231
+ return { ids, stamps: null };
2232
+ }
2233
+ if (typeof parsed === "object" && parsed !== null) {
2234
+ const seen = parsed.seen;
2235
+ if (typeof seen === "object" && seen !== null) {
2236
+ const stamps = /* @__PURE__ */ new Map();
2237
+ for (const [id, ms] of Object.entries(seen)) {
2238
+ if (typeof ms === "number") stamps.set(id, ms);
2239
+ }
2240
+ const lastAt = parsed.lastAt;
2241
+ return {
2242
+ ids: new Set(stamps.keys()),
2243
+ stamps,
2244
+ ...typeof lastAt === "number" ? { lastAt } : {}
2245
+ };
2246
+ }
2247
+ }
2248
+ return { ids: /* @__PURE__ */ new Set(), stamps: null };
2249
+ } catch {
2250
+ return { ids: /* @__PURE__ */ new Set(), stamps: null };
2251
+ }
2252
+ }
2253
+ function writeSeenStore(path, data, lastAt) {
2254
+ try {
2255
+ mkdirSync(dirname(path), { recursive: true });
2256
+ const body = data instanceof Map ? JSON.stringify({ v: 2, lastAt: lastAt ?? Date.now(), seen: Object.fromEntries(data) }) : JSON.stringify(data);
2257
+ const tmp = `${path}.${process.pid}.tmp`;
2258
+ writeFileSync(tmp, body, "utf8");
2259
+ renameSync(tmp, path);
2323
2260
  } catch {
2324
- return /* @__PURE__ */ new Set();
2325
2261
  }
2326
2262
  }
2263
+ var AUTO_SESSION_IDLE_MS = 30 * 60 * 1e3;
2264
+ var FUTURE_TOLERANCE_MS = 6e4;
2265
+ function stampAgeMs(stamp, now = Date.now()) {
2266
+ if (stamp - now > FUTURE_TOLERANCE_MS) return Number.POSITIVE_INFINITY;
2267
+ return Math.max(0, now - stamp);
2268
+ }
2269
+ function isIdleSession(stamps, lastAt) {
2270
+ if (stamps === null || stamps.size === 0) return false;
2271
+ let newest = lastAt ?? 0;
2272
+ for (const ms of stamps.values()) if (ms > newest) newest = ms;
2273
+ return stampAgeMs(newest) > AUTO_SESSION_IDLE_MS;
2274
+ }
2275
+
2276
+ // src/lessons/seen-cache.ts
2277
+ function openSessionDedup(options = {}) {
2278
+ if (options.disabled === true) return null;
2279
+ const id = options.explicit !== void 0 && options.explicit.trim().length > 0 ? options.explicit.trim() : sessionId(options.env);
2280
+ if (id === void 0) return null;
2281
+ const path = seenStorePath(id, options.projectRoot);
2282
+ const store = readSeenStore(path);
2283
+ const stale = options.ttlMs !== void 0 && isIdleSession(store.stamps, store.lastAt);
2284
+ const stamps = stale ? null : store.stamps;
2285
+ return {
2286
+ sessionId: id,
2287
+ seen: stale ? /* @__PURE__ */ new Set() : visibleSeen(store.ids, stamps, options.ttlMs),
2288
+ path,
2289
+ stamps,
2290
+ ...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
2291
+ };
2292
+ }
2293
+ function visibleSeen(ids, stamps, ttlMs) {
2294
+ if (ttlMs === void 0) return ids;
2295
+ if (stamps === null) return /* @__PURE__ */ new Set();
2296
+ const now = Date.now();
2297
+ const fresh = /* @__PURE__ */ new Set();
2298
+ for (const [id, ms] of stamps) if (stampAgeMs(ms, now) <= ttlMs) fresh.add(id);
2299
+ return fresh;
2300
+ }
2327
2301
  function filterUnseen(dedup, matches) {
2328
2302
  return matches.filter((m) => !dedup.seen.has(m.id));
2329
2303
  }
2330
2304
  function commitSeen(dedup, returnedIds) {
2331
- if (returnedIds.length === 0) return;
2305
+ if (returnedIds.length === 0) {
2306
+ if (dedup.ttlMs !== void 0 && dedup.stamps !== null) {
2307
+ writeSeenStore(dedup.path, dedup.stamps);
2308
+ }
2309
+ return;
2310
+ }
2311
+ if (dedup.ttlMs !== void 0 || dedup.stamps !== null) {
2312
+ const now = Date.now();
2313
+ const merged = /* @__PURE__ */ new Map();
2314
+ for (const [id, ms] of dedup.stamps ?? []) {
2315
+ if (dedup.ttlMs === void 0 || now - ms <= dedup.ttlMs) merged.set(id, ms);
2316
+ }
2317
+ for (const id of returnedIds) merged.set(id, now);
2318
+ writeSeenStore(dedup.path, merged);
2319
+ return;
2320
+ }
2332
2321
  const union3 = new Set(dedup.seen);
2333
2322
  for (const id of returnedIds) union3.add(id);
2334
2323
  if (union3.size === dedup.seen.size) return;
2335
- try {
2336
- mkdirSync(dirname(dedup.path), { recursive: true });
2337
- const tmp = `${dedup.path}.${process.pid}.tmp`;
2338
- writeFileSync(tmp, JSON.stringify([...union3]), "utf8");
2339
- renameSync(tmp, dedup.path);
2340
- } catch {
2341
- }
2324
+ writeSeenStore(dedup.path, [...union3]);
2342
2325
  }
2343
2326
 
2344
2327
  // src/lessons/recall.ts
@@ -2347,6 +2330,7 @@ async function recallLessons(projectRoot, query, options = {}) {
2347
2330
  await maybeAutoMigrateLessons(projectRoot);
2348
2331
  } catch {
2349
2332
  }
2333
+ const preReadStamp = currentGraphStamp(projectRoot);
2350
2334
  const load = loadLessonsGraphResilient(projectRoot);
2351
2335
  if (load.status === "corrupt") {
2352
2336
  return { lessons: [], totalMatches: 0, suppressed: 0, corrupt: true };
@@ -2356,12 +2340,14 @@ async function recallLessons(projectRoot, query, options = {}) {
2356
2340
  }
2357
2341
  if (load.status === "absent") return { lessons: [], totalMatches: 0, suppressed: 0 };
2358
2342
  const graph = load.graph;
2343
+ refreshCommandFastpath(projectRoot, graph, preReadStamp);
2359
2344
  const matchQuery = query.file === void 0 ? query : { ...query, file: normalizeRecallFile(query.file, projectRoot) };
2360
2345
  const matches = queryLessons(graph, matchQuery);
2361
2346
  const dedup = openSessionDedup({
2362
2347
  explicit: options.sessionId,
2363
2348
  disabled: options.noDedup,
2364
- projectRoot
2349
+ projectRoot,
2350
+ ...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {}
2365
2351
  });
2366
2352
  const forRank = dedup === null ? matches : filterUnseen(dedup, matches);
2367
2353
  const cfg = loadRecallConfig(projectRoot);
@@ -2369,22 +2355,27 @@ async function recallLessons(projectRoot, query, options = {}) {
2369
2355
  limit: options.limit ?? cfg.limit,
2370
2356
  maxTokens: options.maxTokens === null ? void 0 : options.maxTokens ?? cfg.maxTokens,
2371
2357
  // Down-rank proven fire-but-fail lessons (empty ⇒ neutral, so recall is
2372
- // unchanged until the outcome log has real signal). Read from the side-channel.
2373
- effectiveness: loadEffectiveness(projectRoot)
2358
+ // unchanged until the outcome log has real signal). Read from the side-channel
2359
+ // only when something survived matching+dedup — a no-match recall must not pay
2360
+ // the (up to 2MB) outcome-log read for a ranking of nothing.
2361
+ effectiveness: forRank.length === 0 ? /* @__PURE__ */ new Map() : loadEffectiveness(projectRoot)
2374
2362
  });
2375
2363
  if (dedup !== null)
2376
2364
  commitSeen(
2377
2365
  dedup,
2378
2366
  lessons.map((l) => l.id)
2379
2367
  );
2380
- recordRecallTelemetry(projectRoot, graph, matchQuery, matches, lessons, { bypassed: false });
2368
+ recordRecallTelemetry(projectRoot, graph, matchQuery, matches, lessons, {
2369
+ bypassed: false,
2370
+ session: options.sessionId
2371
+ });
2381
2372
  return { lessons, totalMatches: matches.length, suppressed: matches.length - forRank.length };
2382
2373
  }
2383
2374
  function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, options = {}) {
2384
2375
  if (!isTelemetryEnabled()) return;
2385
2376
  const byKind = collectMatchedTriggersByKind(graph, query);
2386
2377
  const countVia = (set) => matches.filter(({ lesson }) => lesson.triggers.some((t) => set.has(t))).length;
2387
- const session = sessionId();
2378
+ const session = options.session ?? sessionId();
2388
2379
  appendRecallRecord(projectRoot, {
2389
2380
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2390
2381
  hasFile: query.file !== void 0,
@@ -2404,6 +2395,255 @@ function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, opti
2404
2395
  ...session !== void 0 ? { session } : {}
2405
2396
  });
2406
2397
  }
2398
+
2399
+ // src/lessons/prune.ts
2400
+ function planPrune(graph, options = {}) {
2401
+ const cap = Math.max(1, options.cap ?? MAX_RECOMMENDED_TRIGGERS);
2402
+ const fanout = buildFanout(graph);
2403
+ const trimmedLessons = [];
2404
+ const keptByLesson = /* @__PURE__ */ new Map();
2405
+ for (const [id, lesson] of Object.entries(graph.lessons)) {
2406
+ if (lesson.status !== "active") continue;
2407
+ if (options.trimOverCap === false || lesson.triggers.length <= cap) {
2408
+ keptByLesson.set(id, lesson.triggers);
2409
+ continue;
2410
+ }
2411
+ const ordered = [...lesson.triggers].sort((a, b) => {
2412
+ const fa = fanout.get(a);
2413
+ const fb = fanout.get(b);
2414
+ return fa !== fb ? fa - fb : a < b ? -1 : 1;
2415
+ });
2416
+ const drop = new Set(ordered.slice(cap));
2417
+ const kept = lesson.triggers.filter((t) => !drop.has(t));
2418
+ keptByLesson.set(id, kept);
2419
+ trimmedLessons.push({ id, removedTriggers: [...drop], keptCount: kept.length });
2420
+ }
2421
+ const removedDeadGlobs = [];
2422
+ const unreachableLessons = [];
2423
+ if (options.knownPaths !== void 0) {
2424
+ const dead = deadFileGlobIds(graph, options.knownPaths);
2425
+ if (dead.size > 0) {
2426
+ for (const [id, kept] of keptByLesson) {
2427
+ const deadInLesson = kept.filter((t) => dead.has(t));
2428
+ if (deadInLesson.length === 0) continue;
2429
+ const remaining = kept.filter((t) => !dead.has(t));
2430
+ if (remaining.length >= 1) {
2431
+ keptByLesson.set(id, remaining);
2432
+ removedDeadGlobs.push({ id, removedTriggers: deadInLesson, keptCount: remaining.length });
2433
+ } else {
2434
+ unreachableLessons.push(id);
2435
+ }
2436
+ }
2437
+ unreachableLessons.sort();
2438
+ }
2439
+ }
2440
+ const live = /* @__PURE__ */ new Set();
2441
+ for (const kept of keptByLesson.values()) for (const t of kept) live.add(t);
2442
+ const removedTriggerIds = Object.keys(graph.triggers).filter((t) => !live.has(t)).sort();
2443
+ const referencedTopics = /* @__PURE__ */ new Set();
2444
+ for (const lesson of Object.values(graph.lessons)) {
2445
+ for (const topic of lesson.topics) referencedTopics.add(topic);
2446
+ }
2447
+ const removedTopicIds = Object.keys(graph.topics).filter((t) => !referencedTopics.has(t)).sort();
2448
+ return { removedTriggerIds, removedTopicIds, trimmedLessons, removedDeadGlobs, unreachableLessons, cap };
2449
+ }
2450
+ function applyPruneToGraph(graph, plan) {
2451
+ for (const trim of [...plan.trimmedLessons, ...plan.removedDeadGlobs ?? []]) {
2452
+ const lesson = graph.lessons[trim.id];
2453
+ if (lesson === void 0) continue;
2454
+ const drop = new Set(trim.removedTriggers);
2455
+ graph.lessons[trim.id] = { ...lesson, triggers: lesson.triggers.filter((t) => !drop.has(t)) };
2456
+ }
2457
+ for (const topicId of plan.removedTopicIds) delete graph.topics[topicId];
2458
+ const dead = new Set(plan.removedTriggerIds);
2459
+ if (dead.size === 0) return;
2460
+ for (const [id, lesson] of Object.entries(graph.lessons)) {
2461
+ if (lesson.triggers.some((t) => dead.has(t))) {
2462
+ graph.lessons[id] = { ...lesson, triggers: lesson.triggers.filter((t) => !dead.has(t)) };
2463
+ }
2464
+ }
2465
+ for (const t of dead) delete graph.triggers[t];
2466
+ }
2467
+ function isEmptyPrunePlan(plan) {
2468
+ return plan.removedTriggerIds.length === 0 && plan.removedTopicIds.length === 0 && plan.trimmedLessons.length === 0 && plan.removedDeadGlobs.length === 0;
2469
+ }
2470
+
2471
+ // src/lessons/auto-prune.ts
2472
+ function isAutoPruneEnabled(projectRoot) {
2473
+ const path = lessonsPaths(projectRoot).config;
2474
+ if (!existsSync(path)) return false;
2475
+ try {
2476
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
2477
+ if (typeof parsed !== "object" || parsed === null) return false;
2478
+ return parsed.autoPrune === true;
2479
+ } catch {
2480
+ return false;
2481
+ }
2482
+ }
2483
+ async function maybeAutoPrune(projectRoot, knownPaths) {
2484
+ if (!isAutoPruneEnabled(projectRoot)) return null;
2485
+ const preview = tryLoadLessonsGraph(projectRoot);
2486
+ if (preview === null) return null;
2487
+ if (isEmptyPrunePlan(planPrune(preview, { trimOverCap: false, knownPaths }))) return null;
2488
+ let summary = { removedTriggers: 0, removedTopics: 0, detachedDeadGlobs: 0 };
2489
+ await mutateLessonsGraph(projectRoot, (graph) => {
2490
+ const plan = planPrune(graph, { trimOverCap: false, knownPaths });
2491
+ if (isEmptyPrunePlan(plan)) return;
2492
+ applyPruneToGraph(graph, plan);
2493
+ summary = {
2494
+ removedTriggers: plan.removedTriggerIds.length,
2495
+ removedTopics: plan.removedTopicIds.length,
2496
+ detachedDeadGlobs: plan.removedDeadGlobs.reduce((n, t) => n + t.removedTriggers.length, 0)
2497
+ };
2498
+ });
2499
+ const total = summary.removedTriggers + summary.removedTopics + summary.detachedDeadGlobs;
2500
+ return total > 0 ? summary : null;
2501
+ }
2502
+ var MAX_CAPTURE_LOG_RECORDS = 5e3;
2503
+ var CAPTURE_LOG_TRIM_TRIGGER_BYTES = 2e6;
2504
+ function captureLogPath(projectRoot) {
2505
+ return join(lessonsPaths(projectRoot).base, "capture-log.jsonl");
2506
+ }
2507
+ function appendCaptureRecord(projectRoot, record, env = process.env) {
2508
+ if (!isTelemetryEnabled(env)) return;
2509
+ appendJsonl(captureLogPath(projectRoot), record, {
2510
+ maxRecords: MAX_CAPTURE_LOG_RECORDS,
2511
+ trimTriggerBytes: CAPTURE_LOG_TRIM_TRIGGER_BYTES
2512
+ });
2513
+ }
2514
+ function recordCapture(projectRoot, triggerKinds, result, env = process.env) {
2515
+ if (!isTelemetryEnabled(env)) return;
2516
+ const session = sessionId(env);
2517
+ appendCaptureRecord(
2518
+ projectRoot,
2519
+ {
2520
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2521
+ isNewLesson: result?.isNewLesson ?? false,
2522
+ isNewTopic: result?.isNewTopic ?? false,
2523
+ newTriggerCount: result?.newTriggerIds.length ?? 0,
2524
+ triggerKinds,
2525
+ blocked: result === null,
2526
+ warningCodes: result?.warnings.map((w) => w.code) ?? [],
2527
+ ...session !== void 0 ? { session } : {},
2528
+ ...result !== null ? { lessonId: result.id } : {}
2529
+ },
2530
+ env
2531
+ );
2532
+ }
2533
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
2534
+ var MAX_FILES = 2e5;
2535
+ function listProjectFiles(projectRoot) {
2536
+ const out = /* @__PURE__ */ new Set();
2537
+ try {
2538
+ const stack = [projectRoot];
2539
+ while (stack.length > 0) {
2540
+ const dir = stack.pop();
2541
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
2542
+ if (entry.isDirectory()) {
2543
+ if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
2544
+ } else if (entry.isFile()) {
2545
+ out.add(toRelPath(projectRoot, join(dir, entry.name)));
2546
+ if (out.size > MAX_FILES) return out;
2547
+ }
2548
+ }
2549
+ }
2550
+ } catch {
2551
+ return null;
2552
+ }
2553
+ return out;
2554
+ }
2555
+ function isTriggerRepairEnabled(projectRoot) {
2556
+ const path = lessonsPaths(projectRoot).config;
2557
+ if (!existsSync(path)) return false;
2558
+ try {
2559
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
2560
+ if (typeof parsed !== "object" || parsed === null) return false;
2561
+ return parsed.repairTriggers === true;
2562
+ } catch {
2563
+ return false;
2564
+ }
2565
+ }
2566
+ function evidencePath(evidence, knownPaths) {
2567
+ for (const entry of evidence ?? []) {
2568
+ const candidate = entry.replaceAll("\\", "/").replace(/(:\d+)+$/, "").trim();
2569
+ if (knownPaths.has(candidate)) return candidate;
2570
+ }
2571
+ return void 0;
2572
+ }
2573
+ function classGlobFor(path) {
2574
+ const slash = path.lastIndexOf("/");
2575
+ const dir = slash === -1 ? "" : path.slice(0, slash + 1);
2576
+ const base = path.slice(slash + 1);
2577
+ const dot = base.lastIndexOf(".");
2578
+ return `${dir}*${dot > 0 ? base.slice(dot) : ""}`;
2579
+ }
2580
+ function repairFileGlobs(files, evidence, knownPaths, repairs) {
2581
+ const out = [];
2582
+ for (const glob of files) {
2583
+ const needsNarrow = knownPaths !== void 0 && (isBroadGlob(glob) || fileGlobMatchCount(glob, knownPaths) > WIDE_GLOB_MATCH_COUNT);
2584
+ if (!needsNarrow || evidence === void 0 || !picomatch(glob, { dot: true })(evidence)) {
2585
+ if (!out.includes(glob)) out.push(glob);
2586
+ continue;
2587
+ }
2588
+ const derived = classGlobFor(evidence);
2589
+ const derivedOk = derived !== glob && picomatch(derived, { dot: true })(evidence) && fileGlobMatchCount(derived, knownPaths) <= fileGlobMatchCount(glob, knownPaths);
2590
+ if (!derivedOk) {
2591
+ if (!out.includes(glob)) out.push(glob);
2592
+ continue;
2593
+ }
2594
+ if (!out.includes(derived)) out.push(derived);
2595
+ repairs.push({
2596
+ code: "NARROWED_GLOB",
2597
+ message: `Narrowed broad file glob "${glob}" to the evidence file's class "${derived}". Review: for general/library behavior, re-point at the file-CLASS recurrence surface (a '**/.../*Name*.ts'-style glob) where the rule will actually recur.`
2598
+ });
2599
+ }
2600
+ return out;
2601
+ }
2602
+ function repairKeywords(keywords, repairs) {
2603
+ const out = [];
2604
+ for (const kw of keywords) {
2605
+ const tokens = tokenize(kw);
2606
+ if (tokens.length === 0) {
2607
+ repairs.push({
2608
+ code: "DROPPED_KEYWORD",
2609
+ message: `Dropped keyword trigger "${kw}" \u2014 it tokenizes to nothing (stopwords/short words only) and can never fire.`
2610
+ });
2611
+ continue;
2612
+ }
2613
+ if (!out.includes(kw)) out.push(kw);
2614
+ if (!keywordNeedleLosesTokens(kw) && !isLowSignalKeyword(kw)) continue;
2615
+ const variant = tokens.slice(0, MAX_RECOMMENDED_KEYWORD_TOKENS).join(" ");
2616
+ if (variant.toLowerCase() === kw.toLowerCase() || out.includes(variant)) continue;
2617
+ out.push(variant);
2618
+ repairs.push({
2619
+ code: "KEYWORD_VARIANT_ADDED",
2620
+ message: `Keyword trigger "${kw}" cannot match on the mandatory --file/--cmd token path; added the matchable variant "${variant}" beside it (the original still matches prompt text).`
2621
+ });
2622
+ }
2623
+ return out;
2624
+ }
2625
+ function repairTriggers(input, knownPaths) {
2626
+ const repairs = [];
2627
+ const evidence = knownPaths === void 0 ? void 0 : evidencePath(input.evidence, knownPaths);
2628
+ const files = input.triggers.files === void 0 ? void 0 : repairFileGlobs(input.triggers.files, evidence, knownPaths, repairs);
2629
+ const keywords = input.triggers.keywords === void 0 ? void 0 : repairKeywords(input.triggers.keywords, repairs);
2630
+ if (repairs.length === 0) return { input, repairs };
2631
+ const total = (files?.length ?? 0) + (input.triggers.commands?.length ?? 0) + (keywords?.length ?? 0);
2632
+ if (total === 0) return { input, repairs: [] };
2633
+ return {
2634
+ input: {
2635
+ ...input,
2636
+ triggers: {
2637
+ ...files !== void 0 ? { files } : {},
2638
+ ...input.triggers.commands !== void 0 ? { commands: input.triggers.commands } : {},
2639
+ ...keywords !== void 0 ? { keywords } : {}
2640
+ }
2641
+ },
2642
+ repairs
2643
+ };
2644
+ }
2645
+
2646
+ // src/lessons/capture.ts
2407
2647
  async function captureLesson(projectRoot, input, options = {}) {
2408
2648
  await maybeAutoMigrateLessons(projectRoot);
2409
2649
  const triggerKinds = {
@@ -2412,11 +2652,14 @@ async function captureLesson(projectRoot, input, options = {}) {
2412
2652
  keyword: input.triggers.keywords?.length ?? 0
2413
2653
  };
2414
2654
  const knownPaths = options.knownPaths ?? listProjectFiles(projectRoot) ?? void 0;
2655
+ const repair = isTriggerRepairEnabled(projectRoot) ? repairTriggers(input, knownPaths) : null;
2656
+ const effective = repair === null ? input : repair.input;
2415
2657
  try {
2416
- const result = await addLesson(projectRoot, input, { ...options, knownPaths });
2417
- recordCapture(projectRoot, triggerKinds, result);
2658
+ const result = await addLesson(projectRoot, effective, { ...options, knownPaths });
2659
+ const repaired = repair === null || repair.repairs.length === 0 ? result : { ...result, warnings: [...result.warnings, ...repair.repairs] };
2660
+ recordCapture(projectRoot, triggerKinds, repaired);
2418
2661
  const autoPruned = await maybeAutoPrune(projectRoot, knownPaths);
2419
- return autoPruned === null ? result : { ...result, autoPruned };
2662
+ return autoPruned === null ? repaired : { ...repaired, autoPruned };
2420
2663
  } catch (err) {
2421
2664
  recordCapture(projectRoot, triggerKinds, null);
2422
2665
  throw err;
@@ -2736,6 +2979,17 @@ ${placed}` : placed;
2736
2979
 
2737
2980
  // src/targets/projection/lessons-paragraph.ts
2738
2981
  var LEGACY_RAW_FORMS = [
2982
+ // Pre-`--session auto` wording (2026-07): strip sentinel-less copies so a
2983
+ // project scaffolded before the dedup correlator dedups on the next scaffold.
2984
+ `## Lessons (BLOCKING)
2985
+
2986
+ Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
2987
+
2988
+ **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.
2989
+
2990
+ **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>\`.
2991
+
2992
+ **Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`,
2739
2993
  `## Lessons (BLOCKING REQUIREMENT \u2014 MUST run both, no exceptions; the user will check)
2740
2994
 
2741
2995
  Graph \`.agentsmesh/lessons/lessons.json\` is canonical \u2014 never hand-edit. Manual: the \`lessons\` skill.
@@ -2793,14 +3047,18 @@ regression / wrong assumption / surprise and you have not captured (nor stated
2793
3047
 
2794
3048
  ## Recall \u2014 before each edit/command, and at task start
2795
3049
 
2796
- \`agentsmesh lessons query --file <path> --cmd <command>\`, then apply every rule.
2797
- Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
3050
+ \`agentsmesh lessons query --file <path> --cmd <command> --session auto\`, then apply every
3051
+ rule. Pure-read commands (read-only) and the query itself are exempt. **keyword-only recall
2798
3052
  for a specific edit is the anti-pattern** \u2014 anchor those to \`--file\`/\`--cmd\`. But at the
2799
3053
  START of a task (or when planning), run \`agentsmesh lessons query --keyword "<the task's
2800
- key terms>" --always\`: that surfaces the conceptual rules no file/command names PLUS the
2801
- universal always-on lessons \u2014 the manual equivalent of the automatic prompt recall on
2802
- hook-capable tools. Author a \`keyword\` trigger beside a \`file_glob\` on conceptual lessons
2803
- so they are reachable both ways. No shell \u2192 MCP \`lessons_query\` (\`file\`/\`command\`/\`keyword\`/\`always\`).
3054
+ key terms>" --always --session auto\`: that surfaces the conceptual rules no file/command
3055
+ names PLUS the universal always-on lessons \u2014 the manual equivalent of the automatic prompt
3056
+ recall on hook-capable tools. **Always pass \`--session auto\`**: it suppresses rules already
3057
+ shown this session so repeat recalls stay quiet (without it every recall re-delivers the
3058
+ whole matched set); \`--no-dedup\` re-shows everything after a context reset. Author a
3059
+ \`keyword\` trigger beside a \`file_glob\` on conceptual lessons so they are reachable both
3060
+ ways. No shell \u2192 MCP \`lessons_query\` (\`file\`/\`command\`/\`keyword\`/\`always\`; session dedup
3061
+ is on by default there \u2014 \`no_dedup:true\` to re-show).
2804
3062
 
2805
3063
  ## Capture \u2014 Gate Function (before any completion claim)
2806
3064