hippocamp 0.2.5 → 0.2.7
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/.github/workflows/publish.yml +25 -0
- package/AGENTS.md +7 -5
- package/README.md +39 -310
- package/assets/github-actions/hippocamp-dream.yml +2 -2
- package/assets/railway/run-dream.sh +2 -2
- package/docs/development.md +34 -0
- package/docs/dream.md +46 -0
- package/docs/how-it-works.md +119 -0
- package/docs/results.md +59 -0
- package/docs/setup.md +66 -0
- package/package.json +1 -1
- package/scripts/eval-lagoon.cjs +3 -3
- package/scripts/hippocamp-dream.cjs +2 -2
- package/scripts/hippocamp-mcp.cjs +5 -5
- package/scripts/hippocamp-memory.cjs +86 -59
- package/scripts/install-claude.cjs +1 -1
- package/scripts/install-codex.cjs +1 -1
- package/scripts/install-grok.cjs +1 -1
- package/skills/hippocamp-memory/SKILL.md +3 -3
- package/test/mcp-params.test.cjs +10 -10
- package/test/memory-efficiency.test.cjs +180 -16
- package/test/package-paths.test.cjs +12 -0
- package/test/sync-memory.test.cjs +2 -4
|
@@ -76,7 +76,6 @@ test("search uses indexed events and returns coherent evidence", async (t) => {
|
|
|
76
76
|
const projectRoot = path.join(tempRoot, "search-project");
|
|
77
77
|
|
|
78
78
|
process.env.HIPPOCAMP_GLOBAL_ROOT = lagoonRoot;
|
|
79
|
-
await fs.mkdir(projectRoot, { recursive: true });
|
|
80
79
|
t.after(async () => {
|
|
81
80
|
if (originalRoot === undefined) {
|
|
82
81
|
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
@@ -88,7 +87,7 @@ test("search uses indexed events and returns coherent evidence", async (t) => {
|
|
|
88
87
|
|
|
89
88
|
await memory.appendEvent({
|
|
90
89
|
content: "Decision:\nUse indexed search only.",
|
|
91
|
-
|
|
90
|
+
keywords: ["strict-index"],
|
|
92
91
|
date: "2026-09-21",
|
|
93
92
|
projectRoot,
|
|
94
93
|
scope: "project",
|
|
@@ -109,7 +108,7 @@ test("search uses indexed events and returns coherent evidence", async (t) => {
|
|
|
109
108
|
|
|
110
109
|
await memory.appendEvent({
|
|
111
110
|
content: `Decision:\n${"x".repeat(1300)}`,
|
|
112
|
-
|
|
111
|
+
keywords: ["oversized-metadata"],
|
|
113
112
|
date: "2026-09-21",
|
|
114
113
|
projectRoot,
|
|
115
114
|
scope: "project",
|
|
@@ -124,7 +123,9 @@ test("search uses indexed events and returns coherent evidence", async (t) => {
|
|
|
124
123
|
scope: "project",
|
|
125
124
|
});
|
|
126
125
|
|
|
127
|
-
|
|
126
|
+
// A keyword-only match still returns whole paragraphs; the oversized run is left out, never cropped.
|
|
127
|
+
assert.equal(oversized.results[0]?.heading, "Large metadata-only match");
|
|
128
|
+
assert.equal(oversized.results[0].snippet, "Decision:");
|
|
128
129
|
|
|
129
130
|
const projectMemoryRoot = path.join(lagoonRoot, "projects", "search-project");
|
|
130
131
|
await fs.writeFile(
|
|
@@ -145,7 +146,7 @@ test("search uses indexed events and returns coherent evidence", async (t) => {
|
|
|
145
146
|
const eventsRoot = path.join(projectMemoryRoot, "events");
|
|
146
147
|
await fs.writeFile(
|
|
147
148
|
path.join(eventsRoot, "2026-09-20.md"),
|
|
148
|
-
"# Events: 2026-09-20\n\n## 09:00 — Hidden fallback\n\
|
|
149
|
+
"# Events: 2026-09-20\n\n## 09:00 — Hidden fallback\n\nKeywords:\n- quasar-velvet\n\nShould not be scanned.\n",
|
|
149
150
|
"utf8",
|
|
150
151
|
);
|
|
151
152
|
|
|
@@ -174,11 +175,11 @@ test("search ranks indexed event bodies with typo tolerance", async (t) => {
|
|
|
174
175
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
175
176
|
});
|
|
176
177
|
|
|
177
|
-
for (const [title,
|
|
178
|
+
for (const [title, keywords, content] of [
|
|
178
179
|
["Deploy note", ["deploy"], "The zanzibar migration needs a rollback plan."],
|
|
179
180
|
["Unrelated note", ["billing"], "Invoices are generated monthly."],
|
|
180
181
|
]) {
|
|
181
|
-
await memory.appendEvent({ content,
|
|
182
|
+
await memory.appendEvent({ content, keywords, projectRoot, scope: "project", sync: false, title });
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
const search = await memory.searchMemory({ projectRoot, query: "zanzibr rollback", scope: "project" });
|
|
@@ -188,6 +189,42 @@ test("search ranks indexed event bodies with typo tolerance", async (t) => {
|
|
|
188
189
|
assert.equal(search.results[0].match, "body");
|
|
189
190
|
});
|
|
190
191
|
|
|
192
|
+
test("search still matches events written with legacy Cues sections and indexes", async (t) => {
|
|
193
|
+
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
194
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-legacy-cues-"));
|
|
195
|
+
const projectRoot = path.join(tempRoot, "legacy-project");
|
|
196
|
+
const eventsRoot = path.join(tempRoot, "lagoon", "projects", "legacy-project", "events");
|
|
197
|
+
|
|
198
|
+
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
199
|
+
await fs.mkdir(eventsRoot, { recursive: true });
|
|
200
|
+
t.after(async () => {
|
|
201
|
+
if (originalRoot === undefined) {
|
|
202
|
+
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
203
|
+
} else {
|
|
204
|
+
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
205
|
+
}
|
|
206
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const id = "2026-09-01T09:00:00.000Z";
|
|
210
|
+
await fs.writeFile(
|
|
211
|
+
path.join(eventsRoot, "2026-09-01.md"),
|
|
212
|
+
`# Events — 2026-09-01\n\n## ${id} — Old note\n\nCues:\n- quasar-velvet\n\nOld body.\n`,
|
|
213
|
+
"utf8",
|
|
214
|
+
);
|
|
215
|
+
await fs.writeFile(
|
|
216
|
+
path.join(eventsRoot, "2026-09-01.index.json"),
|
|
217
|
+
JSON.stringify({ version: 1, path: "2026-09-01.md", events: [{ id, heading: "Old note", cues: ["quasar-velvet"] }] }),
|
|
218
|
+
"utf8",
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const search = await memory.searchMemory({ projectRoot, query: "quasar velvet", scope: "project" });
|
|
222
|
+
|
|
223
|
+
assert.equal(search.results.length, 1);
|
|
224
|
+
assert.deepEqual(search.results[0].keywords, ["quasar-velvet"]);
|
|
225
|
+
assert.equal(search.results[0].match, "keywords");
|
|
226
|
+
});
|
|
227
|
+
|
|
191
228
|
test("search returns whole sentences from an oversized matching paragraph", async (t) => {
|
|
192
229
|
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
193
230
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-long-paragraph-"));
|
|
@@ -207,7 +244,7 @@ test("search returns whole sentences from an oversized matching paragraph", asyn
|
|
|
207
244
|
const filler = "The weekly sync covered routine updates. ".repeat(40);
|
|
208
245
|
await memory.appendEvent({
|
|
209
246
|
content: `${filler}The kestrel database moved to eu-west-3. ${filler}`,
|
|
210
|
-
|
|
247
|
+
keywords: ["infra"],
|
|
211
248
|
projectRoot,
|
|
212
249
|
scope: "project",
|
|
213
250
|
sync: false,
|
|
@@ -246,7 +283,7 @@ test("search returns every matching paragraph that fits the excerpt budget", asy
|
|
|
246
283
|
];
|
|
247
284
|
await memory.appendEvent({
|
|
248
285
|
content: paragraphs.join("\n\n"),
|
|
249
|
-
|
|
286
|
+
keywords: ["infra"],
|
|
250
287
|
projectRoot,
|
|
251
288
|
scope: "project",
|
|
252
289
|
sync: false,
|
|
@@ -282,7 +319,7 @@ test("events appended in the same millisecond keep distinct ids and bodies", asy
|
|
|
282
319
|
const timestamp = "2026-10-01T09:00:00.000Z";
|
|
283
320
|
const first = await memory.appendEvent({
|
|
284
321
|
content: "The zanzibar migration needs a rollback plan.",
|
|
285
|
-
|
|
322
|
+
keywords: ["deploy"],
|
|
286
323
|
projectRoot,
|
|
287
324
|
scope: "project",
|
|
288
325
|
sync: false,
|
|
@@ -291,7 +328,7 @@ test("events appended in the same millisecond keep distinct ids and bodies", asy
|
|
|
291
328
|
});
|
|
292
329
|
const second = await memory.appendEvent({
|
|
293
330
|
content: "Invoices are generated monthly.",
|
|
294
|
-
|
|
331
|
+
keywords: ["billing"],
|
|
295
332
|
projectRoot,
|
|
296
333
|
scope: "project",
|
|
297
334
|
sync: false,
|
|
@@ -329,7 +366,7 @@ test("parallel appends keep every event", async (t) => {
|
|
|
329
366
|
Array.from({ length: 20 }, (_, index) =>
|
|
330
367
|
memory.appendEvent({
|
|
331
368
|
content: `Parallel event ${index}.`,
|
|
332
|
-
|
|
369
|
+
keywords: ["parallel"],
|
|
333
370
|
date: "2026-10-01",
|
|
334
371
|
projectRoot,
|
|
335
372
|
scope: "project",
|
|
@@ -366,7 +403,7 @@ test("search favors events from a month named in the query", async (t) => {
|
|
|
366
403
|
for (const timestamp of ["2026-05-02T08:00:00.000Z", "2026-08-05T10:00:00.000Z", "2026-09-01T09:00:00.000Z"]) {
|
|
367
404
|
await memory.appendEvent({
|
|
368
405
|
content: "Published the Android build to Google Play production.",
|
|
369
|
-
|
|
406
|
+
keywords: ["android-release"],
|
|
370
407
|
date: timestamp.slice(0, 10),
|
|
371
408
|
projectRoot,
|
|
372
409
|
scope: "project",
|
|
@@ -407,7 +444,7 @@ test("search favors events from a relative date named in the query", async (t) =
|
|
|
407
444
|
await memory.appendEvent({
|
|
408
445
|
// The old event matches the words better, so only the date words can rank the others first.
|
|
409
446
|
content: key === "older" ? "Deploy key rotation: rotated the deploy key again." : "Rotated the key for staging.",
|
|
410
|
-
|
|
447
|
+
keywords: ["deploy-key"],
|
|
411
448
|
date: timestamp.slice(0, 10),
|
|
412
449
|
projectRoot,
|
|
413
450
|
scope: "project",
|
|
@@ -448,7 +485,7 @@ test("search returns a snippet for a long event matched only by its date", async
|
|
|
448
485
|
|
|
449
486
|
await memory.appendEvent({
|
|
450
487
|
content: Array(30).fill("The weekly sync covered routine updates.").join("\n\n"),
|
|
451
|
-
|
|
488
|
+
keywords: ["sync"],
|
|
452
489
|
date: "2026-08-05",
|
|
453
490
|
projectRoot,
|
|
454
491
|
scope: "project",
|
|
@@ -485,7 +522,7 @@ test("search returns nothing when no memory is about the question", async (t) =>
|
|
|
485
522
|
["Deploy rollback", "A failed backend deploy rolls back to the previous revision."],
|
|
486
523
|
["Billing note", "Invoices are generated monthly."],
|
|
487
524
|
]) {
|
|
488
|
-
await memory.appendEvent({ content,
|
|
525
|
+
await memory.appendEvent({ content, keywords: ["ops"], projectRoot, scope: "project", sync: false, title });
|
|
489
526
|
}
|
|
490
527
|
|
|
491
528
|
const unrelated = await memory.searchMemory({ projectRoot, query: "which kubernetes namespace does the backend deploy to", scope: "project" });
|
|
@@ -498,3 +535,130 @@ test("search returns nothing when no memory is about the question", async (t) =>
|
|
|
498
535
|
// No event is from August; the month name must not count as a missing topic word.
|
|
499
536
|
assert.ok(monthOnly.results.length > 0);
|
|
500
537
|
});
|
|
538
|
+
|
|
539
|
+
test("search keeps an event matched only by a keyword its long body never repeats", async (t) => {
|
|
540
|
+
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
541
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-keyword-only-"));
|
|
542
|
+
const projectRoot = path.join(tempRoot, "keyword-only-project");
|
|
543
|
+
|
|
544
|
+
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
545
|
+
await fs.mkdir(projectRoot, { recursive: true });
|
|
546
|
+
t.after(async () => {
|
|
547
|
+
if (originalRoot === undefined) {
|
|
548
|
+
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
549
|
+
} else {
|
|
550
|
+
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
551
|
+
}
|
|
552
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const content = [
|
|
556
|
+
"Opened three hardening PRs after the security audit.",
|
|
557
|
+
"The backend PR rotates the cron secret and checks request signatures on every scheduled job. ".repeat(8),
|
|
558
|
+
"The website PR updates the content studio dependencies and removes an unused preview route. ".repeat(8),
|
|
559
|
+
].join("\n\n");
|
|
560
|
+
|
|
561
|
+
assert.ok(content.length > 1200);
|
|
562
|
+
await memory.appendEvent({
|
|
563
|
+
content,
|
|
564
|
+
keywords: ["security-audit", "strava-oauth"],
|
|
565
|
+
projectRoot,
|
|
566
|
+
scope: "project",
|
|
567
|
+
sync: false,
|
|
568
|
+
title: "Security audit opened hardening PRs",
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const search = await memory.searchMemory({ projectRoot, query: "Strava bug", scope: "project" });
|
|
572
|
+
|
|
573
|
+
assert.equal(search.results[0]?.heading, "Security audit opened hardening PRs");
|
|
574
|
+
assert.match(search.results[0].snippet, /^Opened three hardening PRs/);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("search returns nothing when a rare word only resembles memory words", async (t) => {
|
|
578
|
+
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
579
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-lookalike-"));
|
|
580
|
+
const projectRoot = path.join(tempRoot, "lookalike-project");
|
|
581
|
+
|
|
582
|
+
process.env.HIPPOCAMP_GLOBAL_ROOT = path.join(tempRoot, "lagoon");
|
|
583
|
+
await fs.mkdir(projectRoot, { recursive: true });
|
|
584
|
+
t.after(async () => {
|
|
585
|
+
if (originalRoot === undefined) {
|
|
586
|
+
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
587
|
+
} else {
|
|
588
|
+
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
589
|
+
}
|
|
590
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
await memory.appendEvent({
|
|
594
|
+
content: "Each worker instance reads the instruct prompt and its content before a sync.",
|
|
595
|
+
keywords: ["health-sync"],
|
|
596
|
+
projectRoot,
|
|
597
|
+
scope: "project",
|
|
598
|
+
sync: false,
|
|
599
|
+
title: "Apple Health sync banner hardening",
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// "instinct" is one typo away from "instance" and "instruct" but means something else.
|
|
603
|
+
const lookalike = await memory.searchMemory({ projectRoot, query: "Instinct", scope: "project" });
|
|
604
|
+
const shared = await memory.searchMemory({ projectRoot, query: "context", scope: "project" });
|
|
605
|
+
const plural = await memory.searchMemory({ projectRoot, query: "worker instances", scope: "project" });
|
|
606
|
+
|
|
607
|
+
assert.deepEqual(lookalike.results, []);
|
|
608
|
+
assert.deepEqual(shared.results, []);
|
|
609
|
+
assert.equal(plural.results[0]?.heading, "Apple Health sync banner hardening");
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("an empty folder has no project and uses global memory", async (t) => {
|
|
613
|
+
const originalRoot = process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
614
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "hippocamp-empty-folder-"));
|
|
615
|
+
const lagoonRoot = path.join(tempRoot, "lagoon");
|
|
616
|
+
const emptyRoot = path.join(tempRoot, "billowy-glove");
|
|
617
|
+
const codeRoot = path.join(tempRoot, "job-search");
|
|
618
|
+
|
|
619
|
+
process.env.HIPPOCAMP_GLOBAL_ROOT = lagoonRoot;
|
|
620
|
+
// Only .git, like a fresh session folder some agents create.
|
|
621
|
+
await fs.mkdir(path.join(emptyRoot, ".git"), { recursive: true });
|
|
622
|
+
await fs.mkdir(codeRoot, { recursive: true });
|
|
623
|
+
await fs.writeFile(path.join(codeRoot, "notes.md"), "Job search notes.\n", "utf8");
|
|
624
|
+
t.after(async () => {
|
|
625
|
+
if (originalRoot === undefined) {
|
|
626
|
+
delete process.env.HIPPOCAMP_GLOBAL_ROOT;
|
|
627
|
+
} else {
|
|
628
|
+
process.env.HIPPOCAMP_GLOBAL_ROOT = originalRoot;
|
|
629
|
+
}
|
|
630
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const wake = await memory.wakeUp({ projectRoot: emptyRoot });
|
|
634
|
+
const event = await memory.appendEvent({
|
|
635
|
+
content: "Created the applying-to-jobs skill.",
|
|
636
|
+
keywords: ["applying-to-jobs"],
|
|
637
|
+
projectRoot: emptyRoot,
|
|
638
|
+
scope: "project",
|
|
639
|
+
sync: false,
|
|
640
|
+
title: "Job application skill created",
|
|
641
|
+
});
|
|
642
|
+
await memory.appendEvent({
|
|
643
|
+
content: "Created the applying-to-jobs tracker in Notion.",
|
|
644
|
+
keywords: ["applying-to-jobs-tracker"],
|
|
645
|
+
projectRoot: codeRoot,
|
|
646
|
+
scope: "project",
|
|
647
|
+
sync: false,
|
|
648
|
+
title: "Job tracker created",
|
|
649
|
+
});
|
|
650
|
+
const fromEmpty = await memory.searchMemory({ projectRoot: emptyRoot, query: "applying to jobs skill" });
|
|
651
|
+
const projectScope = await memory.searchMemory({ projectRoot: emptyRoot, query: "applying to jobs", scope: "project" });
|
|
652
|
+
const fromProject = await memory.searchMemory({ projectRoot: codeRoot, query: "applying to jobs skill" });
|
|
653
|
+
|
|
654
|
+
assert.equal(wake.projectSlug, null);
|
|
655
|
+
assert.equal(wake.projectMemoryRoot, lagoonRoot);
|
|
656
|
+
assert.equal(path.dirname(path.dirname(event.indexPath)), lagoonRoot);
|
|
657
|
+
// Global memory is searched once, and other projects stay out of it.
|
|
658
|
+
assert.deepEqual(fromEmpty.results.map((result) => result.heading), ["Job application skill created"]);
|
|
659
|
+
assert.deepEqual(projectScope.results.map((result) => result.heading), ["Job application skill created"]);
|
|
660
|
+
assert.equal(fromProject.results[0]?.heading, "Job application skill created");
|
|
661
|
+
assert.equal(memory.getProjectSlug(codeRoot), "job-search");
|
|
662
|
+
// A folder that does not exist keeps its name, as Dream's synthetic roots do.
|
|
663
|
+
assert.equal(memory.getProjectSlug(path.join(tempRoot, "missing")), "missing");
|
|
664
|
+
});
|
|
@@ -28,3 +28,15 @@ test("published files contain no local home directory paths", () => {
|
|
|
28
28
|
|
|
29
29
|
assert.deepEqual(matches, []);
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
test("the README demo GIF stays out of the npm package", () => {
|
|
33
|
+
// The README loads it from GitHub; .npmignore keeps the 600 kB file out of every install.
|
|
34
|
+
const output = execFileSync("npm", ["pack", "--dry-run", "--json"], {
|
|
35
|
+
cwd: path.join(__dirname, ".."),
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
shell: process.platform === "win32",
|
|
38
|
+
});
|
|
39
|
+
const [pack] = JSON.parse(output);
|
|
40
|
+
|
|
41
|
+
assert.ok(!pack.files.some((file) => file.path.endsWith(".gif")));
|
|
42
|
+
});
|
|
@@ -29,7 +29,6 @@ test("sync commits when the Lagoon path goes through a symlink", async (t) => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
await fs.mkdir(realLagoon);
|
|
32
|
-
await fs.mkdir(projectRoot);
|
|
33
32
|
await fs.symlink(realLagoon, linkedLagoon);
|
|
34
33
|
git(["init", "-q", "-b", "main"]);
|
|
35
34
|
git(["commit", "-q", "--allow-empty", "-m", "init"]);
|
|
@@ -45,7 +44,7 @@ test("sync commits when the Lagoon path goes through a symlink", async (t) => {
|
|
|
45
44
|
|
|
46
45
|
const result = await memory.appendEvent({
|
|
47
46
|
content: "Use advisory locks.",
|
|
48
|
-
|
|
47
|
+
keywords: ["locks"],
|
|
49
48
|
projectRoot,
|
|
50
49
|
scope: "project",
|
|
51
50
|
title: "Queue decision",
|
|
@@ -56,12 +55,11 @@ test("sync commits when the Lagoon path goes through a symlink", async (t) => {
|
|
|
56
55
|
|
|
57
56
|
// A symlink inside the Lagoon pointing at another project must not be synced as this one.
|
|
58
57
|
const otherProject = path.join(tempRoot, "other-project");
|
|
59
|
-
await fs.mkdir(otherProject);
|
|
60
58
|
await fs.mkdir(path.join(realLagoon, "projects", "other-project"), { recursive: true });
|
|
61
59
|
await fs.symlink(path.join(realLagoon, "projects", "linked-project", "events"), path.join(realLagoon, "projects", "other-project", "events"));
|
|
62
60
|
|
|
63
61
|
await assert.rejects(
|
|
64
|
-
memory.appendEvent({ content: "Cross write.",
|
|
62
|
+
memory.appendEvent({ content: "Cross write.", keywords: ["x"], projectRoot: otherProject, scope: "project", title: "Escape" }),
|
|
65
63
|
/escapes the memory root/,
|
|
66
64
|
);
|
|
67
65
|
});
|