agentsmesh 0.41.0 → 0.42.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.d.ts CHANGED
@@ -42,6 +42,11 @@ interface RecallOptions {
42
42
  * compact/clear, which is the exact signal a wall-clock TTL only approximates.
43
43
  */
44
44
  readonly ttlMs?: number;
45
+ /**
46
+ * Migrate a legacy store first (default true). The hook passes false: the
47
+ * migration deletes files, so it only runs from a command the user runs.
48
+ */
49
+ readonly autoMigrate?: boolean;
45
50
  }
46
51
  interface RecallResult {
47
52
  /** Relevance-ranked, capped lessons (compact metadata lives on each entry). */
package/dist/lessons.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  import { createHash, randomUUID } from 'crypto';
3
- import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, statSync, renameSync, readdirSync, chmodSync, realpathSync, accessSync, constants, appendFileSync, rmdirSync, unlinkSync, openSync, readSync, closeSync } from 'fs';
4
- import { resolve, join, relative, sep, dirname, posix, basename, extname } from 'path';
3
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, statSync, renameSync, readdirSync, realpathSync, chmodSync, accessSync, constants, appendFileSync, lstatSync, rmdirSync, unlinkSync, openSync, readSync, closeSync } from 'fs';
4
+ import { join, relative, sep, resolve, dirname, posix, basename, extname } from 'path';
5
+ import { mkdir, writeFile, rm, readFile, lstat, open, readdir, rmdir, unlink, stat, rename, realpath } from 'fs/promises';
6
+ import { hostname, tmpdir } from 'os';
5
7
  import picomatch from 'picomatch';
6
8
  import { execFile, spawnSync } from 'child_process';
7
- import { hostname, tmpdir } from 'os';
8
- import { mkdir, writeFile, rm, readFile, lstat, open, readdir, rmdir, unlink, stat, rename, realpath } from 'fs/promises';
9
9
  import { setTimeout } from 'timers/promises';
10
10
  import { promisify } from 'util';
11
11
  import { stringify, parse, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
@@ -566,6 +566,115 @@ function executableModeFor(path) {
566
566
  function stripBom(text) {
567
567
  return text.startsWith(UTF8_BOM) ? text.slice(UTF8_BOM.length) : text;
568
568
  }
569
+ async function canonicalizePath(path) {
570
+ try {
571
+ return await realpath(path);
572
+ } catch (error) {
573
+ if (error.code !== "ENOENT") throw error;
574
+ const parent = dirname(path);
575
+ if (parent === path) return resolve(path);
576
+ return join(await canonicalizePath(parent), basename(path));
577
+ }
578
+ }
579
+ function canonicalizePathSync(path) {
580
+ try {
581
+ return realpathSync(path);
582
+ } catch (error) {
583
+ if (error.code !== "ENOENT") return null;
584
+ }
585
+ try {
586
+ lstatSync(path);
587
+ return null;
588
+ } catch {
589
+ const parent = dirname(path);
590
+ if (parent === path) return resolve(path);
591
+ const realParent = canonicalizePathSync(parent);
592
+ return realParent === null ? null : join(realParent, basename(path));
593
+ }
594
+ }
595
+ function resolvesInsideRootSync(root, target) {
596
+ const realRoot = canonicalizePathSync(resolve(root));
597
+ const realTarget = canonicalizePathSync(resolve(target));
598
+ return realRoot !== null && realTarget !== null && isPathInside(realTarget, realRoot);
599
+ }
600
+ function isPathInside(target, root) {
601
+ return target === root || target.startsWith(root.endsWith(sep) ? root : `${root}${sep}`);
602
+ }
603
+ var display = (path) => path.replaceAll("\\", "/");
604
+ async function assertPathInsideRoot(root, target) {
605
+ const rootAbs = resolve(root);
606
+ const targetAbs = resolve(target);
607
+ if (!isPathInside(targetAbs, rootAbs)) {
608
+ throw new Error(`Unsafe filesystem path: ${display(target)} is outside ${display(rootAbs)}`);
609
+ }
610
+ let realTarget;
611
+ let realRoot;
612
+ try {
613
+ [realTarget, realRoot] = await Promise.all([
614
+ canonicalizePath(targetAbs),
615
+ canonicalizePath(rootAbs)
616
+ ]);
617
+ } catch (cause) {
618
+ const detail = cause instanceof Error ? cause.message : String(cause);
619
+ throw new Error(
620
+ `Unsafe filesystem path: ${display(target)} could not be resolved (${detail})`,
621
+ { cause }
622
+ );
623
+ }
624
+ if (isPathInside(realTarget, realRoot)) return;
625
+ throw new Error(
626
+ `Unsafe filesystem path: ${display(target)} resolves to ${display(realTarget)} outside ${display(realRoot)}`
627
+ );
628
+ }
629
+ var BASE_REL = ".agentsmesh/lessons";
630
+ function lessonsPaths(projectRoot) {
631
+ const base = join(projectRoot, BASE_REL);
632
+ return {
633
+ base,
634
+ graph: join(base, "lessons.json"),
635
+ config: join(base, "config.json"),
636
+ journal: join(base, "journal.md"),
637
+ index: join(base, "index.yaml"),
638
+ topicsDir: join(base, "topics")
639
+ };
640
+ }
641
+ function toRelPath(projectRoot, absolute) {
642
+ return relative(projectRoot, absolute).split(sep).join("/");
643
+ }
644
+ var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
645
+
646
+ Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
647
+
648
+ **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.
649
+
650
+ **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>\`.
651
+
652
+ **Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`;
653
+
654
+ // src/lessons/lessons-dir-guard.ts
655
+ function lessonsDirInsideProject(projectRoot) {
656
+ return resolvesInsideRootSync(projectRoot, lessonsPaths(projectRoot).base);
657
+ }
658
+ var LessonsDirOutsideProjectError = class extends Error {
659
+ code = "LESSONS_DIR_OUTSIDE_PROJECT";
660
+ constructor(projectRoot) {
661
+ const base = lessonsPaths(projectRoot).base;
662
+ let where = "a path that does not exist";
663
+ try {
664
+ where = realpathSync(base).replaceAll("\\", "/");
665
+ } catch {
666
+ }
667
+ super(
668
+ `${base.replaceAll("\\", "/")} resolves to ${where}, outside the project ${projectRoot.replaceAll("\\", "/")}. agentsmesh only writes lessons inside the project; replace the link with a real folder.`
669
+ );
670
+ this.name = "LessonsDirOutsideProjectError";
671
+ }
672
+ };
673
+ function assertLessonsDirInsideProject(projectRoot) {
674
+ if (!lessonsDirInsideProject(projectRoot)) throw new LessonsDirOutsideProjectError(projectRoot);
675
+ }
676
+
677
+ // src/lessons/graph-store.ts
569
678
  var LESSONS_GRAPH_PATH = ".agentsmesh/lessons/lessons.json";
570
679
  function graphFilePath(projectRoot) {
571
680
  return resolve(projectRoot, LESSONS_GRAPH_PATH);
@@ -620,6 +729,7 @@ function isWritable(path) {
620
729
  }
621
730
  }
622
731
  function saveLessonsGraph(projectRoot, graph) {
732
+ assertLessonsDirInsideProject(projectRoot);
623
733
  const path = graphFilePath(projectRoot);
624
734
  mkdirSync(dirname(path), { recursive: true });
625
735
  const mode = fileMode(path);
@@ -693,42 +803,8 @@ function makeTriggerId2(spec) {
693
803
  const hash = createHash("sha1").update(`${spec.kind}|${spec.pattern}`).digest("hex").slice(0, 8);
694
804
  return `t-${TRIGGER_PREFIX2[spec.kind]}-${hash}`;
695
805
  }
696
- var RULE_HEADING_RE = /^##\s+Rules\b.*$/i;
697
- var NEXT_HEADING_RE = /^##\s+/;
698
- var RULE_LINE_RE = /^(\d+)\.\s+(.+?)\s*$/;
699
- var EVIDENCE_TAIL_RE = /\s*\(Evidence:?\s+([^)]+)\)\s*$/;
700
- var EVIDENCE_REF_RE = /L\d+/g;
701
- function parseRulesSection(markdown) {
702
- const lines = markdown.split(/\r?\n/);
703
- let inRules = false;
704
- const rules = [];
705
- for (const line of lines) {
706
- if (!inRules) {
707
- if (RULE_HEADING_RE.test(line)) inRules = true;
708
- continue;
709
- }
710
- if (NEXT_HEADING_RE.test(line)) break;
711
- const m = RULE_LINE_RE.exec(line);
712
- if (m === null) continue;
713
- const ruleIndex = Number(m[1]);
714
- let body = m[2];
715
- const evidence = [];
716
- let tail = EVIDENCE_TAIL_RE.exec(body);
717
- while (tail !== null) {
718
- const refs = tail[1];
719
- const matches = refs.match(EVIDENCE_REF_RE);
720
- if (matches !== null) evidence.unshift(...matches);
721
- body = body.slice(0, tail.index).trimEnd();
722
- tail = EVIDENCE_TAIL_RE.exec(body);
723
- }
724
- rules.push({ index: ruleIndex, body, evidence });
725
- }
726
- return rules;
727
- }
728
806
  var LEGACY_ARTIFACT_REL = [
729
807
  "index.yaml",
730
- "journal.md",
731
- "journal.legacy.md",
732
808
  "topics",
733
809
  "distill-ledger.yaml",
734
810
  "distill-proposal.md"
@@ -1681,30 +1757,6 @@ function parseRemovals(out2) {
1681
1757
  }
1682
1758
  return { deleted, renamedAway };
1683
1759
  }
1684
- var BASE_REL = ".agentsmesh/lessons";
1685
- function lessonsPaths(projectRoot) {
1686
- const base = join(projectRoot, BASE_REL);
1687
- return {
1688
- base,
1689
- graph: join(base, "lessons.json"),
1690
- config: join(base, "config.json"),
1691
- journal: join(base, "journal.md"),
1692
- index: join(base, "index.yaml"),
1693
- topicsDir: join(base, "topics")
1694
- };
1695
- }
1696
- function toRelPath(projectRoot, absolute) {
1697
- return relative(projectRoot, absolute).split(sep).join("/");
1698
- }
1699
- var LESSONS_PROCEDURAL_RULE = `## Lessons (BLOCKING)
1700
-
1701
- Graph \`.agentsmesh/lessons/lessons.json\` is canonical; never hand-edit it. Manual: \`lessons\` skill.
1702
-
1703
- **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.
1704
-
1705
- **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>\`.
1706
-
1707
- **Before final:** report \`Lesson: captured <id>\` or \`Lesson: none\`. No recall/capture gate = task incomplete. No shell: use \`lessons_query\` / \`lessons_add\`.`;
1708
1760
 
1709
1761
  // src/lessons/project-files.ts
1710
1762
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
@@ -2448,6 +2500,7 @@ function lessonsLockPath(projectRoot) {
2448
2500
  return resolve(projectRoot, ".agentsmesh/lessons", LESSONS_LOCK_FILENAME);
2449
2501
  }
2450
2502
  async function acquireLessonsLock(projectRoot, opts = {}) {
2503
+ assertLessonsDirInsideProject(projectRoot);
2451
2504
  return acquireProcessLock(lessonsLockPath(projectRoot), {
2452
2505
  retries: opts.retries ?? LESSONS_LOCK_OPTIONS.retries,
2453
2506
  retryDelayMs: opts.retryDelayMs ?? LESSONS_LOCK_OPTIONS.retryDelayMs,
@@ -2987,44 +3040,47 @@ async function mergeLegacy(projectRoot, paths, specs, summaryByTopic, options) {
2987
3040
  triggerCount: addedTriggers.size
2988
3041
  };
2989
3042
  }
2990
- async function canonicalizePath(path) {
2991
- try {
2992
- return await realpath(path);
2993
- } catch (error) {
2994
- if (error.code !== "ENOENT") throw error;
2995
- const parent = dirname(path);
2996
- if (parent === path) return resolve(path);
2997
- return join(await canonicalizePath(parent), basename(path));
2998
- }
2999
- }
3000
- function isPathInside(target, root) {
3001
- return target === root || target.startsWith(root.endsWith(sep) ? root : `${root}${sep}`);
3043
+
3044
+ // src/lessons/import-legacy-rules.ts
3045
+ var HEADING = /^#{1,6}\s/;
3046
+ var SECTION_HEADING = /^#{1,2}\s/;
3047
+ var RULES_HEADING = /^##\s+(?:Rules|Lessons)\b/i;
3048
+ var ITEM = /^\s{0,3}(?:\d+[.)]|[-*+])\s+(.+?)\s*$/;
3049
+ var EVIDENCE_TAIL = /\s*\(Evidence:?\s+([^)]+)\)\s*$/;
3050
+ var EVIDENCE_REF = /L\d+/g;
3051
+ function withEvidence(index, text) {
3052
+ let body = text;
3053
+ const evidence = [];
3054
+ let tail = EVIDENCE_TAIL.exec(body);
3055
+ while (tail !== null) {
3056
+ evidence.unshift(...tail[1].match(EVIDENCE_REF) ?? []);
3057
+ body = body.slice(0, tail.index).trimEnd();
3058
+ tail = EVIDENCE_TAIL.exec(body);
3059
+ }
3060
+ return { index, body, evidence };
3002
3061
  }
3003
- var display = (path) => path.replaceAll("\\", "/");
3004
- async function assertPathInsideRoot(root, target) {
3005
- const rootAbs = resolve(root);
3006
- const targetAbs = resolve(target);
3007
- if (!isPathInside(targetAbs, rootAbs)) {
3008
- throw new Error(`Unsafe filesystem path: ${display(target)} is outside ${display(rootAbs)}`);
3009
- }
3010
- let realTarget;
3011
- let realRoot;
3012
- try {
3013
- [realTarget, realRoot] = await Promise.all([
3014
- canonicalizePath(targetAbs),
3015
- canonicalizePath(rootAbs)
3016
- ]);
3017
- } catch (cause) {
3018
- const detail = cause instanceof Error ? cause.message : String(cause);
3019
- throw new Error(
3020
- `Unsafe filesystem path: ${display(target)} could not be resolved (${detail})`,
3021
- { cause }
3022
- );
3023
- }
3024
- if (isPathInside(realTarget, realRoot)) return;
3025
- throw new Error(
3026
- `Unsafe filesystem path: ${display(target)} resolves to ${display(realTarget)} outside ${display(realRoot)}`
3027
- );
3062
+ function parseRulesSection(markdown) {
3063
+ const items = [];
3064
+ let inRules = false;
3065
+ let open2 = false;
3066
+ let strayLine = null;
3067
+ markdown.split(/\r?\n/).forEach((line, i) => {
3068
+ if (HEADING.test(line)) {
3069
+ if (SECTION_HEADING.test(line)) inRules = RULES_HEADING.test(line);
3070
+ open2 = false;
3071
+ return;
3072
+ }
3073
+ const item = ITEM.exec(line);
3074
+ if (item !== null) {
3075
+ if (inRules) items.push(item[1]);
3076
+ else strayLine ??= i + 1;
3077
+ open2 = inRules;
3078
+ return;
3079
+ }
3080
+ if (line.trim() === "") open2 = false;
3081
+ else if (open2) items[items.length - 1] += ` ${line.trim()}`;
3082
+ });
3083
+ return { rules: items.map((text, i) => withEvidence(i + 1, text)), strayLine };
3028
3084
  }
3029
3085
 
3030
3086
  // src/lessons/import-legacy-read.ts
@@ -3071,9 +3127,13 @@ async function readLegacySource(projectRoot, migratedAt) {
3071
3127
  `Legacy topic file is missing: ${cluster.file}. Refusing to migrate (legacy artifacts left intact).`
3072
3128
  );
3073
3129
  }
3074
- for (const { index: ruleIndex, body, evidence } of parseRulesSection(
3075
- readFileSync(topicFile, "utf8")
3076
- )) {
3130
+ const parsed = parseRulesSection(readFileSync(topicFile, "utf8"));
3131
+ if (parsed.strayLine !== null) {
3132
+ throw new Error(
3133
+ `Legacy lessons were not migrated: ${cluster.file} line ${parsed.strayLine} is a list item outside a "## Rules" or "## Lessons" section. Move it under one of them or delete it, then run \`agentsmesh lessons import-md\`. Nothing was changed.`
3134
+ );
3135
+ }
3136
+ for (const { index: ruleIndex, body, evidence } of parsed.rules) {
3077
3137
  const lessonEvidence = [
3078
3138
  `legacy:${cluster.file}#rule-${ruleIndex}`,
3079
3139
  ...evidence.map((e) => `legacy:${e}`)
@@ -3477,7 +3537,7 @@ function isTelemetryEnabled(env = process.env, projectRoot) {
3477
3537
  return envOverride(env[TELEMETRY_ENV]) ?? (projectRoot !== void 0 && configFlag(projectRoot, "telemetry") === true);
3478
3538
  }
3479
3539
  function appendRecallRecord(projectRoot, record, env = process.env) {
3480
- if (!isTelemetryEnabled(env, projectRoot)) return;
3540
+ if (!isTelemetryEnabled(env, projectRoot) || !lessonsDirInsideProject(projectRoot)) return;
3481
3541
  appendJsonl(recallLogPath(projectRoot), record, {
3482
3542
  maxRecords: MAX_RECALL_LOG_RECORDS,
3483
3543
  trimTriggerBytes: RECALL_LOG_TRIM_TRIGGER_BYTES
@@ -4072,7 +4132,7 @@ var EMPTY = { ids: /* @__PURE__ */ new Set(), stamps: null };
4072
4132
  // src/lessons/recall.ts
4073
4133
  async function recallLessons(projectRoot, query, options = {}) {
4074
4134
  try {
4075
- await maybeAutoMigrateLessons(projectRoot);
4135
+ if (options.autoMigrate !== false) await maybeAutoMigrateLessons(projectRoot);
4076
4136
  } catch {
4077
4137
  }
4078
4138
  const preReadStamp = currentGraphStamp(projectRoot);
@@ -4226,7 +4286,7 @@ function captureLogPath(projectRoot) {
4226
4286
  return join(lessonsPaths(projectRoot).base, "capture-log.jsonl");
4227
4287
  }
4228
4288
  function appendCaptureRecord(projectRoot, record, env = process.env) {
4229
- if (!isTelemetryEnabled(env, projectRoot)) return;
4289
+ if (!isTelemetryEnabled(env, projectRoot) || !lessonsDirInsideProject(projectRoot)) return;
4230
4290
  appendJsonl(captureLogPath(projectRoot), record, {
4231
4291
  maxRecords: MAX_CAPTURE_LOG_RECORDS,
4232
4292
  trimTriggerBytes: CAPTURE_LOG_TRIM_TRIGGER_BYTES
@@ -4665,11 +4725,13 @@ ${yamlStr}
4665
4725
 
4666
4726
  ${body}`;
4667
4727
  }
4668
- var LESSONS_CONTRACT_START = "<!-- agentsmesh:lessons-contract:start -->";
4669
- var LESSONS_CONTRACT_END = "<!-- agentsmesh:lessons-contract:end -->";
4670
4728
  function escapeRegExp(value) {
4671
4729
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4672
4730
  }
4731
+
4732
+ // src/targets/projection/managed-blocks.ts
4733
+ var LESSONS_CONTRACT_START = "<!-- agentsmesh:lessons-contract:start -->";
4734
+ var LESSONS_CONTRACT_END = "<!-- agentsmesh:lessons-contract:end -->";
4673
4735
  function managedBlockPattern(start, end) {
4674
4736
  return new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`, "g");
4675
4737
  }
@@ -4814,6 +4876,7 @@ async function scaffoldLessons(projectRoot) {
4814
4876
  const created = [];
4815
4877
  const updated = [];
4816
4878
  const skipped = [];
4879
+ assertLessonsDirInsideProject(projectRoot);
4817
4880
  mkdirSync(paths.base, { recursive: true });
4818
4881
  await maybeAutoMigrateLessons(projectRoot);
4819
4882
  if (existsSync(paths.graph)) {