agentsmesh 0.30.2 → 0.32.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 { createHash } from 'crypto';
3
- import picomatch from 'picomatch';
4
2
  import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, renameSync, readdirSync, realpathSync, appendFileSync, statSync } from 'fs';
5
- import { resolve, join, relative, sep, dirname, basename, extname } from 'path';
3
+ import { resolve, dirname, join, relative, sep, basename, extname } from 'path';
6
4
  import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
5
+ import { createHash } from 'crypto';
6
+ import picomatch from 'picomatch';
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 {
@@ -1781,239 +1626,185 @@ function addLessonInto(graph, input, options) {
1781
1626
  const id = makeLessonId(graph, input.topic, ruleKey);
1782
1627
  graph.lessons[id] = {
1783
1628
  rule: trimmedRule,
1784
- topics: [input.topic],
1785
- triggers: triggerIds,
1786
- evidence: input.evidence === void 0 ? [] : [...input.evidence],
1787
- status: "active",
1788
- createdAt: input.createdAt ?? todayIso(),
1789
- ...input.rationale === void 0 ? {} : { rationale: input.rationale },
1790
- ...input.scope === "always" ? { scope: "always" } : {}
1791
- };
1792
- const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
1793
- const nearDup = nearDuplicateWarning(graph, id);
1794
- return {
1795
- id,
1796
- isNewLesson: true,
1797
- isNewTopic,
1798
- newTriggerIds,
1799
- warnings: nearDup === null ? warnings : [...warnings, nearDup]
1800
- };
1801
- }
1802
- function findExistingLessonByRule(graph, ruleKey) {
1803
- for (const [id, lesson] of Object.entries(graph.lessons)) {
1804
- if (lesson.status !== "active") continue;
1805
- if (normalizeRule(lesson.rule) === ruleKey) return id;
1806
- }
1807
- return null;
1808
- }
1809
-
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();
1872
- }
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 };
1883
- }
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];
1900
- }
1901
- function isEmptyPrunePlan(plan) {
1902
- return plan.removedTriggerIds.length === 0 && plan.removedTopicIds.length === 0 && plan.trimmedLessons.length === 0 && plan.removedDeadGlobs.length === 0;
1903
- }
1904
-
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;
1915
- }
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);
1629
+ topics: [input.topic],
1630
+ triggers: triggerIds,
1631
+ evidence: input.evidence === void 0 ? [] : [...input.evidence],
1632
+ status: "active",
1633
+ createdAt: input.createdAt ?? todayIso(),
1634
+ ...input.rationale === void 0 ? {} : { rationale: input.rationale },
1635
+ ...input.scope === "always" ? { scope: "always" } : {}
1636
+ };
1637
+ const warnings = inspectCapturedLesson(graph, id, options.knownPaths);
1638
+ const nearDup = nearDuplicateWarning(graph, id);
1639
+ return {
1640
+ id,
1641
+ isNewLesson: true,
1642
+ isNewTopic,
1643
+ newTriggerIds,
1644
+ warnings: nearDup === null ? warnings : [...warnings, nearDup]
1645
+ };
1951
1646
  }
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 {
1960
- }
1647
+ function findExistingLessonByRule(graph, ruleKey) {
1648
+ for (const [id, lesson] of Object.entries(graph.lessons)) {
1649
+ if (lesson.status !== "active") continue;
1650
+ if (normalizeRule(lesson.rule) === ruleKey) return id;
1961
1651
  }
1962
- return out;
1652
+ return null;
1963
1653
  }
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;
1654
+
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);
1675
+ }
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
+ };
1971
1685
  }
1972
- function recallLogPath(projectRoot) {
1973
- return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
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
+ };
1974
1697
  }
1975
- function isTelemetryEnabled(env = process.env) {
1976
- return env[TELEMETRY_ENV] === "1";
1698
+ function toRelPath(projectRoot, absolute) {
1699
+ return relative(projectRoot, absolute).split(sep).join("/");
1977
1700
  }
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
1701
+ var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
1702
+
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>\` 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.
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";
1717
+ }
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
+ });
1764
+ }
1765
+ }
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
- });
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}`;
1998
1796
  }
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
- );
1797
+ async function maybeAutoMigrateLessons(projectRoot) {
1798
+ if (existsSync(graphFilePath(projectRoot))) return false;
1799
+ const paths = lessonsPaths(projectRoot);
1800
+ if (!existsSync(paths.index)) return false;
1801
+ try {
1802
+ await importLegacyLessons(projectRoot, { migratedAt: todayIso2() });
1803
+ return true;
1804
+ } catch (err) {
1805
+ if (err instanceof LessonsGraphExistsError) return false;
1806
+ throw err;
1807
+ }
2017
1808
  }
2018
1809
  function normalizeRecallFile(file, projectRoot) {
2019
1810
  const forward = file.replaceAll("\\", "/");
@@ -2035,28 +1826,6 @@ function safeRealpath(path) {
2035
1826
  return resolve(safeRealpath(parent), basename(path));
2036
1827
  }
2037
1828
  }
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();
2042
- 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;
2057
- }
2058
- return out;
2059
- }
2060
1829
 
2061
1830
  // src/lessons/keyword-match.ts
2062
1831
  function deriveHaystackTokens(query) {
@@ -2143,6 +1912,29 @@ function triggerMatches(trigger, query, budget) {
2143
1912
  }
2144
1913
  }
2145
1914
 
1915
+ // src/lessons/ranking-signals.ts
1916
+ function buildFanout(graph) {
1917
+ const fanout = /* @__PURE__ */ new Map();
1918
+ for (const lesson of Object.values(graph.lessons)) {
1919
+ if (lesson.status !== "active") continue;
1920
+ for (const t of lesson.triggers) fanout.set(t, (fanout.get(t) ?? 0) + 1);
1921
+ }
1922
+ return fanout;
1923
+ }
1924
+ function buildTopicCoherence(matches) {
1925
+ const topicCount = /* @__PURE__ */ new Map();
1926
+ for (const { lesson } of matches) {
1927
+ for (const t of lesson.topics) topicCount.set(t, (topicCount.get(t) ?? 0) + 1);
1928
+ }
1929
+ const coherence = /* @__PURE__ */ new Map();
1930
+ for (const { id, lesson } of matches) {
1931
+ let best = 0;
1932
+ for (const t of lesson.topics) best = Math.max(best, topicCount.get(t));
1933
+ coherence.set(id, best);
1934
+ }
1935
+ return coherence;
1936
+ }
1937
+
2146
1938
  // src/lessons/ranking.ts
2147
1939
  var DEFAULT_RECALL_LIMIT = 10;
2148
1940
  var DEFAULT_RECALL_MAX_TOKENS = 400;
@@ -2236,7 +2028,8 @@ function defaultLessonsConfig() {
2236
2028
  return {
2237
2029
  recallLimit: DEFAULT_RECALL_LIMIT,
2238
2030
  recallMaxTokens: DEFAULT_RECALL_MAX_TOKENS,
2239
- autoPrune: false
2031
+ autoPrune: false,
2032
+ repairTriggers: false
2240
2033
  };
2241
2034
  }
2242
2035
  function positiveInt(value) {
@@ -2261,6 +2054,57 @@ function loadRecallConfig(projectRoot) {
2261
2054
  return fallback;
2262
2055
  }
2263
2056
  }
2057
+ function appendJsonl(path, record, opts) {
2058
+ mkdirSync(dirname(path), { recursive: true });
2059
+ appendFileSync(path, `${JSON.stringify(record)}
2060
+ `, "utf8");
2061
+ if (statSync(path).size > opts.trimTriggerBytes) capJsonl(path, opts.maxRecords);
2062
+ }
2063
+ function capJsonl(path, maxRecords) {
2064
+ if (!existsSync(path)) return;
2065
+ const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim().length > 0);
2066
+ if (lines.length <= maxRecords) return;
2067
+ const kept = lines.slice(lines.length - maxRecords);
2068
+ const tmp = `${path}.${process.pid}.tmp`;
2069
+ writeFileSync(tmp, `${kept.join("\n")}
2070
+ `, "utf8");
2071
+ renameSync(tmp, path);
2072
+ }
2073
+ function readJsonl(path) {
2074
+ if (!existsSync(path)) return [];
2075
+ const out = [];
2076
+ for (const line of readFileSync(path, "utf8").split("\n")) {
2077
+ if (line.trim().length === 0) continue;
2078
+ try {
2079
+ out.push(JSON.parse(line));
2080
+ } catch {
2081
+ }
2082
+ }
2083
+ return out;
2084
+ }
2085
+ var MAX_RECALL_LOG_RECORDS = 5e3;
2086
+ var RECALL_LOG_TRIM_TRIGGER_BYTES = 2e6;
2087
+ var TELEMETRY_ENV = "AGENTSMESH_LESSONS_TELEMETRY";
2088
+ var SESSION_ENV = "AGENTSMESH_SESSION_ID";
2089
+ function sessionId(env = process.env) {
2090
+ const raw = env[SESSION_ENV];
2091
+ return raw !== void 0 && raw.trim().length > 0 ? raw : void 0;
2092
+ }
2093
+ function recallLogPath(projectRoot) {
2094
+ return join(lessonsPaths(projectRoot).base, "recall-log.jsonl");
2095
+ }
2096
+ function isTelemetryEnabled(env = process.env) {
2097
+ return env[TELEMETRY_ENV] === "1";
2098
+ }
2099
+ function appendRecallRecord(projectRoot, record, env = process.env) {
2100
+ if (!isTelemetryEnabled(env)) return;
2101
+ appendJsonl(recallLogPath(projectRoot), record, {
2102
+ maxRecords: MAX_RECALL_LOG_RECORDS,
2103
+ trimTriggerBytes: RECALL_LOG_TRIM_TRIGGER_BYTES
2104
+ });
2105
+ }
2106
+
2107
+ // src/lessons/outcome-log.ts
2264
2108
  function outcomeLogPath(projectRoot) {
2265
2109
  return join(lessonsPaths(projectRoot).base, "outcome-log.jsonl");
2266
2110
  }
@@ -2377,14 +2221,17 @@ async function recallLessons(projectRoot, query, options = {}) {
2377
2221
  dedup,
2378
2222
  lessons.map((l) => l.id)
2379
2223
  );
2380
- recordRecallTelemetry(projectRoot, graph, matchQuery, matches, lessons, { bypassed: false });
2224
+ recordRecallTelemetry(projectRoot, graph, matchQuery, matches, lessons, {
2225
+ bypassed: false,
2226
+ session: options.sessionId
2227
+ });
2381
2228
  return { lessons, totalMatches: matches.length, suppressed: matches.length - forRank.length };
2382
2229
  }
2383
2230
  function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, options = {}) {
2384
2231
  if (!isTelemetryEnabled()) return;
2385
2232
  const byKind = collectMatchedTriggersByKind(graph, query);
2386
2233
  const countVia = (set) => matches.filter(({ lesson }) => lesson.triggers.some((t) => set.has(t))).length;
2387
- const session = sessionId();
2234
+ const session = options.session ?? sessionId();
2388
2235
  appendRecallRecord(projectRoot, {
2389
2236
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2390
2237
  hasFile: query.file !== void 0,
@@ -2404,6 +2251,255 @@ function recordRecallTelemetry(projectRoot, graph, query, matches, lessons, opti
2404
2251
  ...session !== void 0 ? { session } : {}
2405
2252
  });
2406
2253
  }
2254
+
2255
+ // src/lessons/prune.ts
2256
+ function planPrune(graph, options = {}) {
2257
+ const cap = Math.max(1, options.cap ?? MAX_RECOMMENDED_TRIGGERS);
2258
+ const fanout = buildFanout(graph);
2259
+ const trimmedLessons = [];
2260
+ const keptByLesson = /* @__PURE__ */ new Map();
2261
+ for (const [id, lesson] of Object.entries(graph.lessons)) {
2262
+ if (lesson.status !== "active") continue;
2263
+ if (options.trimOverCap === false || lesson.triggers.length <= cap) {
2264
+ keptByLesson.set(id, lesson.triggers);
2265
+ continue;
2266
+ }
2267
+ const ordered = [...lesson.triggers].sort((a, b) => {
2268
+ const fa = fanout.get(a);
2269
+ const fb = fanout.get(b);
2270
+ return fa !== fb ? fa - fb : a < b ? -1 : 1;
2271
+ });
2272
+ const drop = new Set(ordered.slice(cap));
2273
+ const kept = lesson.triggers.filter((t) => !drop.has(t));
2274
+ keptByLesson.set(id, kept);
2275
+ trimmedLessons.push({ id, removedTriggers: [...drop], keptCount: kept.length });
2276
+ }
2277
+ const removedDeadGlobs = [];
2278
+ const unreachableLessons = [];
2279
+ if (options.knownPaths !== void 0) {
2280
+ const dead = deadFileGlobIds(graph, options.knownPaths);
2281
+ if (dead.size > 0) {
2282
+ for (const [id, kept] of keptByLesson) {
2283
+ const deadInLesson = kept.filter((t) => dead.has(t));
2284
+ if (deadInLesson.length === 0) continue;
2285
+ const remaining = kept.filter((t) => !dead.has(t));
2286
+ if (remaining.length >= 1) {
2287
+ keptByLesson.set(id, remaining);
2288
+ removedDeadGlobs.push({ id, removedTriggers: deadInLesson, keptCount: remaining.length });
2289
+ } else {
2290
+ unreachableLessons.push(id);
2291
+ }
2292
+ }
2293
+ unreachableLessons.sort();
2294
+ }
2295
+ }
2296
+ const live = /* @__PURE__ */ new Set();
2297
+ for (const kept of keptByLesson.values()) for (const t of kept) live.add(t);
2298
+ const removedTriggerIds = Object.keys(graph.triggers).filter((t) => !live.has(t)).sort();
2299
+ const referencedTopics = /* @__PURE__ */ new Set();
2300
+ for (const lesson of Object.values(graph.lessons)) {
2301
+ for (const topic of lesson.topics) referencedTopics.add(topic);
2302
+ }
2303
+ const removedTopicIds = Object.keys(graph.topics).filter((t) => !referencedTopics.has(t)).sort();
2304
+ return { removedTriggerIds, removedTopicIds, trimmedLessons, removedDeadGlobs, unreachableLessons, cap };
2305
+ }
2306
+ function applyPruneToGraph(graph, plan) {
2307
+ for (const trim of [...plan.trimmedLessons, ...plan.removedDeadGlobs ?? []]) {
2308
+ const lesson = graph.lessons[trim.id];
2309
+ if (lesson === void 0) continue;
2310
+ const drop = new Set(trim.removedTriggers);
2311
+ graph.lessons[trim.id] = { ...lesson, triggers: lesson.triggers.filter((t) => !drop.has(t)) };
2312
+ }
2313
+ for (const topicId of plan.removedTopicIds) delete graph.topics[topicId];
2314
+ const dead = new Set(plan.removedTriggerIds);
2315
+ if (dead.size === 0) return;
2316
+ for (const [id, lesson] of Object.entries(graph.lessons)) {
2317
+ if (lesson.triggers.some((t) => dead.has(t))) {
2318
+ graph.lessons[id] = { ...lesson, triggers: lesson.triggers.filter((t) => !dead.has(t)) };
2319
+ }
2320
+ }
2321
+ for (const t of dead) delete graph.triggers[t];
2322
+ }
2323
+ function isEmptyPrunePlan(plan) {
2324
+ return plan.removedTriggerIds.length === 0 && plan.removedTopicIds.length === 0 && plan.trimmedLessons.length === 0 && plan.removedDeadGlobs.length === 0;
2325
+ }
2326
+
2327
+ // src/lessons/auto-prune.ts
2328
+ function isAutoPruneEnabled(projectRoot) {
2329
+ const path = lessonsPaths(projectRoot).config;
2330
+ if (!existsSync(path)) return false;
2331
+ try {
2332
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
2333
+ if (typeof parsed !== "object" || parsed === null) return false;
2334
+ return parsed.autoPrune === true;
2335
+ } catch {
2336
+ return false;
2337
+ }
2338
+ }
2339
+ async function maybeAutoPrune(projectRoot, knownPaths) {
2340
+ if (!isAutoPruneEnabled(projectRoot)) return null;
2341
+ const preview = tryLoadLessonsGraph(projectRoot);
2342
+ if (preview === null) return null;
2343
+ if (isEmptyPrunePlan(planPrune(preview, { trimOverCap: false, knownPaths }))) return null;
2344
+ let summary = { removedTriggers: 0, removedTopics: 0, detachedDeadGlobs: 0 };
2345
+ await mutateLessonsGraph(projectRoot, (graph) => {
2346
+ const plan = planPrune(graph, { trimOverCap: false, knownPaths });
2347
+ if (isEmptyPrunePlan(plan)) return;
2348
+ applyPruneToGraph(graph, plan);
2349
+ summary = {
2350
+ removedTriggers: plan.removedTriggerIds.length,
2351
+ removedTopics: plan.removedTopicIds.length,
2352
+ detachedDeadGlobs: plan.removedDeadGlobs.reduce((n, t) => n + t.removedTriggers.length, 0)
2353
+ };
2354
+ });
2355
+ const total = summary.removedTriggers + summary.removedTopics + summary.detachedDeadGlobs;
2356
+ return total > 0 ? summary : null;
2357
+ }
2358
+ var MAX_CAPTURE_LOG_RECORDS = 5e3;
2359
+ var CAPTURE_LOG_TRIM_TRIGGER_BYTES = 2e6;
2360
+ function captureLogPath(projectRoot) {
2361
+ return join(lessonsPaths(projectRoot).base, "capture-log.jsonl");
2362
+ }
2363
+ function appendCaptureRecord(projectRoot, record, env = process.env) {
2364
+ if (!isTelemetryEnabled(env)) return;
2365
+ appendJsonl(captureLogPath(projectRoot), record, {
2366
+ maxRecords: MAX_CAPTURE_LOG_RECORDS,
2367
+ trimTriggerBytes: CAPTURE_LOG_TRIM_TRIGGER_BYTES
2368
+ });
2369
+ }
2370
+ function recordCapture(projectRoot, triggerKinds, result, env = process.env) {
2371
+ if (!isTelemetryEnabled(env)) return;
2372
+ const session = sessionId(env);
2373
+ appendCaptureRecord(
2374
+ projectRoot,
2375
+ {
2376
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2377
+ isNewLesson: result?.isNewLesson ?? false,
2378
+ isNewTopic: result?.isNewTopic ?? false,
2379
+ newTriggerCount: result?.newTriggerIds.length ?? 0,
2380
+ triggerKinds,
2381
+ blocked: result === null,
2382
+ warningCodes: result?.warnings.map((w) => w.code) ?? [],
2383
+ ...session !== void 0 ? { session } : {},
2384
+ ...result !== null ? { lessonId: result.id } : {}
2385
+ },
2386
+ env
2387
+ );
2388
+ }
2389
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
2390
+ var MAX_FILES = 2e5;
2391
+ function listProjectFiles(projectRoot) {
2392
+ const out = /* @__PURE__ */ new Set();
2393
+ try {
2394
+ const stack = [projectRoot];
2395
+ while (stack.length > 0) {
2396
+ const dir = stack.pop();
2397
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
2398
+ if (entry.isDirectory()) {
2399
+ if (!SKIP_DIRS.has(entry.name)) stack.push(join(dir, entry.name));
2400
+ } else if (entry.isFile()) {
2401
+ out.add(toRelPath(projectRoot, join(dir, entry.name)));
2402
+ if (out.size > MAX_FILES) return out;
2403
+ }
2404
+ }
2405
+ }
2406
+ } catch {
2407
+ return null;
2408
+ }
2409
+ return out;
2410
+ }
2411
+ function isTriggerRepairEnabled(projectRoot) {
2412
+ const path = lessonsPaths(projectRoot).config;
2413
+ if (!existsSync(path)) return false;
2414
+ try {
2415
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
2416
+ if (typeof parsed !== "object" || parsed === null) return false;
2417
+ return parsed.repairTriggers === true;
2418
+ } catch {
2419
+ return false;
2420
+ }
2421
+ }
2422
+ function evidencePath(evidence, knownPaths) {
2423
+ for (const entry of evidence ?? []) {
2424
+ const candidate = entry.replaceAll("\\", "/").replace(/(:\d+)+$/, "").trim();
2425
+ if (knownPaths.has(candidate)) return candidate;
2426
+ }
2427
+ return void 0;
2428
+ }
2429
+ function classGlobFor(path) {
2430
+ const slash = path.lastIndexOf("/");
2431
+ const dir = slash === -1 ? "" : path.slice(0, slash + 1);
2432
+ const base = path.slice(slash + 1);
2433
+ const dot = base.lastIndexOf(".");
2434
+ return `${dir}*${dot > 0 ? base.slice(dot) : ""}`;
2435
+ }
2436
+ function repairFileGlobs(files, evidence, knownPaths, repairs) {
2437
+ const out = [];
2438
+ for (const glob of files) {
2439
+ const needsNarrow = knownPaths !== void 0 && (isBroadGlob(glob) || fileGlobMatchCount(glob, knownPaths) > WIDE_GLOB_MATCH_COUNT);
2440
+ if (!needsNarrow || evidence === void 0 || !picomatch(glob, { dot: true })(evidence)) {
2441
+ if (!out.includes(glob)) out.push(glob);
2442
+ continue;
2443
+ }
2444
+ const derived = classGlobFor(evidence);
2445
+ const derivedOk = derived !== glob && picomatch(derived, { dot: true })(evidence) && fileGlobMatchCount(derived, knownPaths) <= fileGlobMatchCount(glob, knownPaths);
2446
+ if (!derivedOk) {
2447
+ if (!out.includes(glob)) out.push(glob);
2448
+ continue;
2449
+ }
2450
+ if (!out.includes(derived)) out.push(derived);
2451
+ repairs.push({
2452
+ code: "NARROWED_GLOB",
2453
+ 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.`
2454
+ });
2455
+ }
2456
+ return out;
2457
+ }
2458
+ function repairKeywords(keywords, repairs) {
2459
+ const out = [];
2460
+ for (const kw of keywords) {
2461
+ const tokens = tokenize(kw);
2462
+ if (tokens.length === 0) {
2463
+ repairs.push({
2464
+ code: "DROPPED_KEYWORD",
2465
+ message: `Dropped keyword trigger "${kw}" \u2014 it tokenizes to nothing (stopwords/short words only) and can never fire.`
2466
+ });
2467
+ continue;
2468
+ }
2469
+ if (!out.includes(kw)) out.push(kw);
2470
+ if (!keywordNeedleLosesTokens(kw) && !isLowSignalKeyword(kw)) continue;
2471
+ const variant = tokens.slice(0, MAX_RECOMMENDED_KEYWORD_TOKENS).join(" ");
2472
+ if (variant.toLowerCase() === kw.toLowerCase() || out.includes(variant)) continue;
2473
+ out.push(variant);
2474
+ repairs.push({
2475
+ code: "KEYWORD_VARIANT_ADDED",
2476
+ 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).`
2477
+ });
2478
+ }
2479
+ return out;
2480
+ }
2481
+ function repairTriggers(input, knownPaths) {
2482
+ const repairs = [];
2483
+ const evidence = knownPaths === void 0 ? void 0 : evidencePath(input.evidence, knownPaths);
2484
+ const files = input.triggers.files === void 0 ? void 0 : repairFileGlobs(input.triggers.files, evidence, knownPaths, repairs);
2485
+ const keywords = input.triggers.keywords === void 0 ? void 0 : repairKeywords(input.triggers.keywords, repairs);
2486
+ if (repairs.length === 0) return { input, repairs };
2487
+ const total = (files?.length ?? 0) + (input.triggers.commands?.length ?? 0) + (keywords?.length ?? 0);
2488
+ if (total === 0) return { input, repairs: [] };
2489
+ return {
2490
+ input: {
2491
+ ...input,
2492
+ triggers: {
2493
+ ...files !== void 0 ? { files } : {},
2494
+ ...input.triggers.commands !== void 0 ? { commands: input.triggers.commands } : {},
2495
+ ...keywords !== void 0 ? { keywords } : {}
2496
+ }
2497
+ },
2498
+ repairs
2499
+ };
2500
+ }
2501
+
2502
+ // src/lessons/capture.ts
2407
2503
  async function captureLesson(projectRoot, input, options = {}) {
2408
2504
  await maybeAutoMigrateLessons(projectRoot);
2409
2505
  const triggerKinds = {
@@ -2412,11 +2508,14 @@ async function captureLesson(projectRoot, input, options = {}) {
2412
2508
  keyword: input.triggers.keywords?.length ?? 0
2413
2509
  };
2414
2510
  const knownPaths = options.knownPaths ?? listProjectFiles(projectRoot) ?? void 0;
2511
+ const repair = isTriggerRepairEnabled(projectRoot) ? repairTriggers(input, knownPaths) : null;
2512
+ const effective = repair === null ? input : repair.input;
2415
2513
  try {
2416
- const result = await addLesson(projectRoot, input, { ...options, knownPaths });
2417
- recordCapture(projectRoot, triggerKinds, result);
2514
+ const result = await addLesson(projectRoot, effective, { ...options, knownPaths });
2515
+ const repaired = repair === null || repair.repairs.length === 0 ? result : { ...result, warnings: [...result.warnings, ...repair.repairs] };
2516
+ recordCapture(projectRoot, triggerKinds, repaired);
2418
2517
  const autoPruned = await maybeAutoPrune(projectRoot, knownPaths);
2419
- return autoPruned === null ? result : { ...result, autoPruned };
2518
+ return autoPruned === null ? repaired : { ...repaired, autoPruned };
2420
2519
  } catch (err) {
2421
2520
  recordCapture(projectRoot, triggerKinds, null);
2422
2521
  throw err;