chapterhouse 0.7.0 → 0.8.1
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/agents/korg.agent.md +65 -0
- package/dist/api/korg.js +34 -0
- package/dist/api/korg.test.js +42 -0
- package/dist/api/server.js +238 -2
- package/dist/api/server.test.js +199 -0
- package/dist/config.js +28 -0
- package/dist/config.test.js +20 -0
- package/dist/copilot/agents.js +3 -4
- package/dist/copilot/agents.test.js +12 -1
- package/dist/copilot/orchestrator.js +12 -1
- package/dist/copilot/orchestrator.test.js +3 -7
- package/dist/copilot/system-message.js +12 -10
- package/dist/copilot/system-message.test.js +6 -1
- package/dist/copilot/tools.js +193 -375
- package/dist/copilot/tools.memory.test.js +32 -0
- package/dist/copilot/tools.wiki.test.js +80 -59
- package/dist/copilot/turn-event-log-env.test.js +11 -15
- package/dist/daemon.js +19 -0
- package/dist/memory/decisions.js +6 -5
- package/dist/memory/entities.js +20 -9
- package/dist/memory/eot.js +30 -8
- package/dist/memory/eot.test.js +220 -6
- package/dist/memory/hooks.js +151 -0
- package/dist/memory/hooks.test.js +325 -0
- package/dist/memory/hot-tier.js +37 -0
- package/dist/memory/hot-tier.test.js +30 -0
- package/dist/memory/housekeeping-scheduler.js +35 -0
- package/dist/memory/housekeeping-scheduler.test.js +50 -0
- package/dist/memory/inbox.js +10 -0
- package/dist/memory/index.js +3 -1
- package/dist/memory/migration.js +244 -0
- package/dist/memory/migration.test.js +108 -0
- package/dist/memory/reflect.js +273 -0
- package/dist/memory/reflect.test.js +254 -0
- package/dist/paths.js +31 -11
- package/dist/store/db.js +187 -4
- package/dist/store/db.test.js +66 -2
- package/dist/test/helpers/reset-singletons.js +8 -0
- package/dist/test/helpers/reset-singletons.test.js +37 -0
- package/dist/test/setup-env.js +9 -1
- package/dist/wiki/consolidation.js +641 -0
- package/dist/wiki/consolidation.test.js +143 -0
- package/dist/wiki/frontmatter.js +48 -0
- package/dist/wiki/frontmatter.test.js +42 -0
- package/dist/wiki/fs.js +22 -13
- package/dist/wiki/index-manager.js +305 -330
- package/dist/wiki/index-manager.test.js +265 -144
- package/dist/wiki/ingest.js +347 -0
- package/dist/wiki/ingest.test.js +111 -0
- package/dist/wiki/links.js +151 -0
- package/dist/wiki/links.test.js +176 -0
- package/dist/wiki/log-manager.js +8 -5
- package/dist/wiki/log-manager.test.js +4 -0
- package/dist/wiki/migrate-topics.test.js +16 -6
- package/dist/wiki/scheduler.js +118 -0
- package/dist/wiki/scheduler.test.js +64 -0
- package/dist/wiki/timeline.js +51 -0
- package/dist/wiki/timeline.test.js +65 -0
- package/dist/wiki/topic-structure.js +1 -1
- package/package.json +1 -1
- package/skills/pkb-ideas/SKILL.md +78 -0
- package/skills/pkb-ideas/_meta.json +4 -0
- package/skills/pkb-org/SKILL.md +82 -0
- package/skills/pkb-org/_meta.json +4 -0
- package/skills/pkb-people/SKILL.md +74 -0
- package/skills/pkb-people/_meta.json +4 -0
- package/skills/pkb-research/SKILL.md +83 -0
- package/skills/pkb-research/_meta.json +4 -0
- package/skills/pkb-source/SKILL.md +38 -0
- package/skills/pkb-source/_meta.json +4 -0
- package/skills/wiki-conventions/SKILL.md +5 -5
- package/web/dist/assets/{index-DuKYxMIR.css → index-5kz9aRU9.css} +1 -1
- package/web/dist/assets/{index-DytB69KC.js → index-BbX9RKf3.js} +91 -89
- package/web/dist/assets/index-BbX9RKf3.js.map +1 -0
- package/web/dist/index.html +2 -2
- package/dist/wiki/context.js +0 -138
- package/dist/wiki/fix.js +0 -335
- package/dist/wiki/fix.test.js +0 -350
- package/dist/wiki/lint.js +0 -451
- package/dist/wiki/lint.test.js +0 -329
- package/web/dist/assets/index-DytB69KC.js.map +0 -1
package/dist/memory/eot.test.js
CHANGED
|
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import test from "node:test";
|
|
5
|
+
import { resetSingletons } from "../test/helpers/reset-singletons.js";
|
|
5
6
|
const repoRoot = process.cwd();
|
|
6
7
|
const sandboxRoot = join(repoRoot, ".test-work", `memory-eot-${process.pid}`);
|
|
7
8
|
const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
|
|
@@ -18,24 +19,40 @@ async function loadModules(cacheBust = `${Date.now()}-${Math.random()}`) {
|
|
|
18
19
|
const agentsModule = await import(new URL(`../copilot/agents.js?case=${cacheBust}`, import.meta.url).href);
|
|
19
20
|
return { dbModule, memoryModule, eotModule, agentsModule };
|
|
20
21
|
}
|
|
22
|
+
async function loadModulesWithWarnSpy(t, cacheBust) {
|
|
23
|
+
const warnings = [];
|
|
24
|
+
t.mock.module("../util/logger.js", {
|
|
25
|
+
namedExports: {
|
|
26
|
+
childLogger: () => ({
|
|
27
|
+
info: () => { },
|
|
28
|
+
warn: (...args) => {
|
|
29
|
+
warnings.push(args);
|
|
30
|
+
},
|
|
31
|
+
error: () => { },
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
36
|
+
const memoryModule = await import(new URL(`./index.js?case=${cacheBust}`, import.meta.url).href);
|
|
37
|
+
const eotModule = await import(new URL(`./eot.js?case=${cacheBust}`, import.meta.url).href);
|
|
38
|
+
return { dbModule, memoryModule, eotModule, warnings };
|
|
39
|
+
}
|
|
21
40
|
function getFunction(module, name) {
|
|
22
41
|
const value = module[name];
|
|
23
42
|
assert.equal(typeof value, "function", `expected ${name} to be exported`);
|
|
24
43
|
return value;
|
|
25
44
|
}
|
|
26
|
-
test.beforeEach(
|
|
45
|
+
test.beforeEach(() => {
|
|
27
46
|
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
28
47
|
delete process.env.CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED;
|
|
29
48
|
delete process.env.CHAPTERHOUSE_MEMORY_AUTO_ACCEPT;
|
|
30
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
31
|
-
dbModule.closeDb();
|
|
32
49
|
resetSandbox();
|
|
50
|
+
resetSingletons();
|
|
33
51
|
});
|
|
34
|
-
test.after(
|
|
52
|
+
test.after(() => {
|
|
35
53
|
delete process.env.CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED;
|
|
36
54
|
delete process.env.CHAPTERHOUSE_MEMORY_AUTO_ACCEPT;
|
|
37
|
-
|
|
38
|
-
dbModule.closeDb();
|
|
55
|
+
resetSingletons();
|
|
39
56
|
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
40
57
|
});
|
|
41
58
|
test("runEndOfTaskMemoryHook does nothing when CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED=0", async () => {
|
|
@@ -239,6 +256,98 @@ test("runEndOfTaskMemoryHook rejects invalid action_item proposals with a clear
|
|
|
239
256
|
assert.match(inbox.resolution_reason ?? "", /title/i);
|
|
240
257
|
assert.ok(inbox.resolved_at);
|
|
241
258
|
});
|
|
259
|
+
async function runAcceptedObservationHookScenario(t, cacheBust, taskId, payload) {
|
|
260
|
+
const warnings = [];
|
|
261
|
+
t.mock.module("../util/logger.js", {
|
|
262
|
+
namedExports: {
|
|
263
|
+
childLogger: () => ({
|
|
264
|
+
info: () => { },
|
|
265
|
+
warn: (obj, msg) => warnings.push({ obj, msg }),
|
|
266
|
+
error: () => { },
|
|
267
|
+
}),
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
const { dbModule, memoryModule, eotModule } = await loadModules(cacheBust);
|
|
271
|
+
const db = dbModule.getDb();
|
|
272
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
273
|
+
const listObservations = getFunction(memoryModule, "listObservations");
|
|
274
|
+
const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
|
|
275
|
+
const chapterhouse = getScope("chapterhouse");
|
|
276
|
+
assert.ok(chapterhouse);
|
|
277
|
+
const before = listObservations({ scope_id: chapterhouse.id });
|
|
278
|
+
const inserted = db.prepare(`
|
|
279
|
+
INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
|
|
280
|
+
VALUES (?, 'memory_proposal', ?, 'coder', ?, 'pending')
|
|
281
|
+
`).run(chapterhouse.id, JSON.stringify({
|
|
282
|
+
kind: "observation",
|
|
283
|
+
payload,
|
|
284
|
+
confidence: 0.9,
|
|
285
|
+
}), taskId);
|
|
286
|
+
await runEndOfTaskMemoryHook({
|
|
287
|
+
taskId,
|
|
288
|
+
finalResult: "Completed and reviewed an observation proposal.",
|
|
289
|
+
copilotClient: {},
|
|
290
|
+
callLLM: async () => JSON.stringify({
|
|
291
|
+
decisions: [{
|
|
292
|
+
proposal_id: Number(inserted.lastInsertRowid),
|
|
293
|
+
decision: "accept",
|
|
294
|
+
reason: "Durable finding.",
|
|
295
|
+
}],
|
|
296
|
+
implicit_memories: [],
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
const inbox = db.prepare(`
|
|
300
|
+
SELECT status, resolution_reason
|
|
301
|
+
FROM mem_inbox
|
|
302
|
+
WHERE id = ?
|
|
303
|
+
`).get(Number(inserted.lastInsertRowid));
|
|
304
|
+
return {
|
|
305
|
+
beforeCount: before.length,
|
|
306
|
+
after: listObservations({ scope_id: chapterhouse.id }),
|
|
307
|
+
warnings,
|
|
308
|
+
inbox,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
test("runEndOfTaskMemoryHook skips accepted observation proposals with null content and warns", async (t) => {
|
|
312
|
+
const result = await runAcceptedObservationHookScenario(t, "observation-null-content", "task-eot-observation-null-content", { content: null });
|
|
313
|
+
assert.equal(result.after.length, result.beforeCount);
|
|
314
|
+
assert.equal(result.after.some((row) => row.content.trim().length === 0), false);
|
|
315
|
+
assert.equal(result.inbox.status, "accepted");
|
|
316
|
+
assert.equal(result.inbox.resolution_reason, "Durable finding.");
|
|
317
|
+
assert.ok(result.warnings.some((entry) => /empty content/i.test(entry.msg)));
|
|
318
|
+
});
|
|
319
|
+
test("runEndOfTaskMemoryHook skips accepted observation proposals with undefined content and warns", async (t) => {
|
|
320
|
+
const result = await runAcceptedObservationHookScenario(t, "observation-undefined-content", "task-eot-observation-undefined-content", {});
|
|
321
|
+
assert.equal(result.after.length, result.beforeCount);
|
|
322
|
+
assert.equal(result.after.some((row) => row.content.trim().length === 0), false);
|
|
323
|
+
assert.equal(result.inbox.status, "accepted");
|
|
324
|
+
assert.equal(result.inbox.resolution_reason, "Durable finding.");
|
|
325
|
+
assert.ok(result.warnings.some((entry) => /empty content/i.test(entry.msg)));
|
|
326
|
+
});
|
|
327
|
+
test("runEndOfTaskMemoryHook skips accepted observation proposals with empty string content and warns", async (t) => {
|
|
328
|
+
const result = await runAcceptedObservationHookScenario(t, "observation-empty-string-content", "task-eot-observation-empty-string-content", { content: "" });
|
|
329
|
+
assert.equal(result.after.length, result.beforeCount);
|
|
330
|
+
assert.equal(result.after.some((row) => row.content.trim().length === 0), false);
|
|
331
|
+
assert.equal(result.inbox.status, "accepted");
|
|
332
|
+
assert.equal(result.inbox.resolution_reason, "Durable finding.");
|
|
333
|
+
assert.ok(result.warnings.some((entry) => /empty content/i.test(entry.msg)));
|
|
334
|
+
});
|
|
335
|
+
test("runEndOfTaskMemoryHook skips accepted observation proposals with whitespace-only content and warns", async (t) => {
|
|
336
|
+
const result = await runAcceptedObservationHookScenario(t, "observation-whitespace-content", "task-eot-observation-whitespace-content", { content: " " });
|
|
337
|
+
assert.equal(result.after.length, result.beforeCount);
|
|
338
|
+
assert.equal(result.after.some((row) => row.content.trim().length === 0), false);
|
|
339
|
+
assert.equal(result.inbox.status, "accepted");
|
|
340
|
+
assert.equal(result.inbox.resolution_reason, "Durable finding.");
|
|
341
|
+
assert.ok(result.warnings.some((entry) => /empty content/i.test(entry.msg)));
|
|
342
|
+
});
|
|
343
|
+
test("runEndOfTaskMemoryHook inserts accepted observation proposals with valid content", async (t) => {
|
|
344
|
+
const result = await runAcceptedObservationHookScenario(t, "observation-valid-content", "task-eot-observation-valid-content", { content: " Durable finding from the task. " });
|
|
345
|
+
assert.equal(result.after.length, result.beforeCount + 1);
|
|
346
|
+
assert.ok(result.after.some((row) => row.content === "Durable finding from the task."));
|
|
347
|
+
assert.equal(result.inbox.status, "accepted");
|
|
348
|
+
assert.equal(result.inbox.resolution_reason, "Durable finding.");
|
|
349
|
+
assert.equal(result.warnings.length, 0);
|
|
350
|
+
});
|
|
242
351
|
test("runEndOfTaskMemoryHook rejects action_item proposals with ambiguous entity references", async () => {
|
|
243
352
|
const { dbModule, memoryModule, eotModule } = await loadModules("action-item-ambiguous-entity");
|
|
244
353
|
const db = dbModule.getDb();
|
|
@@ -549,4 +658,109 @@ test("runEndOfTaskMemoryHook accepts implicit entity memories with entity_kind",
|
|
|
549
658
|
&& entity.kind === "host"
|
|
550
659
|
&& entity.summary === "NAS host used by Bellonda."), true);
|
|
551
660
|
});
|
|
661
|
+
test("runEndOfTaskMemoryHook skips implicit observation memories with null content and warns", async (t) => {
|
|
662
|
+
const { memoryModule, eotModule, warnings } = await loadModulesWithWarnSpy(t, "implicit-null-content");
|
|
663
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
664
|
+
const listObservations = getFunction(memoryModule, "listObservations");
|
|
665
|
+
const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
|
|
666
|
+
const chapterhouse = getScope("chapterhouse");
|
|
667
|
+
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
668
|
+
const initialCount = listObservations({ scope_id: chapterhouse.id }).length;
|
|
669
|
+
const summary = await runEndOfTaskMemoryHook({
|
|
670
|
+
taskId: "task-eot-implicit-null",
|
|
671
|
+
finalResult: "The reviewer attempted to persist an invalid implicit memory.",
|
|
672
|
+
copilotClient: {},
|
|
673
|
+
callLLM: async () => JSON.stringify({
|
|
674
|
+
decisions: [],
|
|
675
|
+
implicit_memories: [{
|
|
676
|
+
kind: "observation",
|
|
677
|
+
scope_slug: "chapterhouse",
|
|
678
|
+
payload: {
|
|
679
|
+
content: null,
|
|
680
|
+
},
|
|
681
|
+
}],
|
|
682
|
+
}),
|
|
683
|
+
});
|
|
684
|
+
assert.equal(listObservations({ scope_id: chapterhouse.id }).length, initialCount);
|
|
685
|
+
assert.equal(summary.implicit_extracted, 0);
|
|
686
|
+
assert.equal(warnings.length, 1);
|
|
687
|
+
});
|
|
688
|
+
test("runEndOfTaskMemoryHook skips implicit observation memories with undefined content and warns", async (t) => {
|
|
689
|
+
const { memoryModule, eotModule, warnings } = await loadModulesWithWarnSpy(t, "implicit-undefined-content");
|
|
690
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
691
|
+
const listObservations = getFunction(memoryModule, "listObservations");
|
|
692
|
+
const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
|
|
693
|
+
const chapterhouse = getScope("chapterhouse");
|
|
694
|
+
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
695
|
+
const initialCount = listObservations({ scope_id: chapterhouse.id }).length;
|
|
696
|
+
const summary = await runEndOfTaskMemoryHook({
|
|
697
|
+
taskId: "task-eot-implicit-undefined",
|
|
698
|
+
finalResult: "The reviewer attempted to persist an invalid implicit memory.",
|
|
699
|
+
copilotClient: {},
|
|
700
|
+
callLLM: async () => JSON.stringify({
|
|
701
|
+
decisions: [],
|
|
702
|
+
implicit_memories: [{
|
|
703
|
+
kind: "observation",
|
|
704
|
+
scope_slug: "chapterhouse",
|
|
705
|
+
payload: {},
|
|
706
|
+
}],
|
|
707
|
+
}),
|
|
708
|
+
});
|
|
709
|
+
assert.equal(listObservations({ scope_id: chapterhouse.id }).length, initialCount);
|
|
710
|
+
assert.equal(summary.implicit_extracted, 0);
|
|
711
|
+
assert.equal(warnings.length, 1);
|
|
712
|
+
});
|
|
713
|
+
test("runEndOfTaskMemoryHook skips implicit observation memories with empty content and warns", async (t) => {
|
|
714
|
+
const { memoryModule, eotModule, warnings } = await loadModulesWithWarnSpy(t, "implicit-empty-content");
|
|
715
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
716
|
+
const listObservations = getFunction(memoryModule, "listObservations");
|
|
717
|
+
const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
|
|
718
|
+
const chapterhouse = getScope("chapterhouse");
|
|
719
|
+
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
720
|
+
const initialCount = listObservations({ scope_id: chapterhouse.id }).length;
|
|
721
|
+
const summary = await runEndOfTaskMemoryHook({
|
|
722
|
+
taskId: "task-eot-implicit-empty",
|
|
723
|
+
finalResult: "The reviewer attempted to persist an invalid implicit memory.",
|
|
724
|
+
copilotClient: {},
|
|
725
|
+
callLLM: async () => JSON.stringify({
|
|
726
|
+
decisions: [],
|
|
727
|
+
implicit_memories: [{
|
|
728
|
+
kind: "observation",
|
|
729
|
+
scope_slug: "chapterhouse",
|
|
730
|
+
payload: {
|
|
731
|
+
content: " ",
|
|
732
|
+
},
|
|
733
|
+
}],
|
|
734
|
+
}),
|
|
735
|
+
});
|
|
736
|
+
assert.equal(listObservations({ scope_id: chapterhouse.id }).length, initialCount);
|
|
737
|
+
assert.equal(summary.implicit_extracted, 0);
|
|
738
|
+
assert.equal(warnings.length, 1);
|
|
739
|
+
});
|
|
740
|
+
test("runEndOfTaskMemoryHook persists implicit observation memories with valid content", async (t) => {
|
|
741
|
+
const { memoryModule, eotModule, warnings } = await loadModulesWithWarnSpy(t, "implicit-valid-content");
|
|
742
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
743
|
+
const listObservations = getFunction(memoryModule, "listObservations");
|
|
744
|
+
const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
|
|
745
|
+
const chapterhouse = getScope("chapterhouse");
|
|
746
|
+
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
747
|
+
const summary = await runEndOfTaskMemoryHook({
|
|
748
|
+
taskId: "task-eot-implicit-valid",
|
|
749
|
+
finalResult: "The reviewer discovered a valid durable memory.",
|
|
750
|
+
copilotClient: {},
|
|
751
|
+
callLLM: async () => JSON.stringify({
|
|
752
|
+
decisions: [],
|
|
753
|
+
implicit_memories: [{
|
|
754
|
+
kind: "observation",
|
|
755
|
+
scope_slug: "chapterhouse",
|
|
756
|
+
payload: {
|
|
757
|
+
content: "A valid implicit memory should still be stored.",
|
|
758
|
+
},
|
|
759
|
+
}],
|
|
760
|
+
}),
|
|
761
|
+
});
|
|
762
|
+
assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "A valid implicit memory should still be stored."), true);
|
|
763
|
+
assert.equal(summary.implicit_extracted, 1);
|
|
764
|
+
assert.equal(warnings.length, 0);
|
|
765
|
+
});
|
|
552
766
|
//# sourceMappingURL=eot.test.js.map
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { childLogger } from "../util/logger.js";
|
|
2
|
+
import { getActiveScope } from "./active-scope.js";
|
|
3
|
+
import { recordObservation } from "./observations.js";
|
|
4
|
+
import { getScope } from "./scopes.js";
|
|
5
|
+
const log = childLogger("memory.hooks");
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Env knob
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
function isHooksEnabled() {
|
|
10
|
+
const raw = process.env.CHAPTERHOUSE_MEMORY_HOOKS_ENABLED?.trim();
|
|
11
|
+
if (raw === "false" || raw === "0")
|
|
12
|
+
return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Dispatcher
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
export class MemoryHookDispatcher {
|
|
19
|
+
handlers = new Map();
|
|
20
|
+
register(event, handler) {
|
|
21
|
+
const bucket = this.handlers.get(event) ?? [];
|
|
22
|
+
bucket.push(handler);
|
|
23
|
+
this.handlers.set(event, bucket);
|
|
24
|
+
}
|
|
25
|
+
async dispatch(event, payload) {
|
|
26
|
+
if (!isHooksEnabled()) {
|
|
27
|
+
log.debug({ event }, "memory.hooks.skip.disabled");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const bucket = this.handlers.get(event) ?? [];
|
|
31
|
+
if (bucket.length === 0) {
|
|
32
|
+
log.debug({ event }, "memory.hooks.no_handlers");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
log.info({ event, handlerCount: bucket.length }, "memory.hooks.dispatch");
|
|
36
|
+
for (const handler of bucket) {
|
|
37
|
+
try {
|
|
38
|
+
await handler(payload);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
log.error({ err, event }, "memory.hooks.handler_error");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Singleton dispatcher — shared across the process
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
export const hookDispatcher = new MemoryHookDispatcher();
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Built-in handler: git:commit → observation in active scope
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
function buildGitCommitContent(payload) {
|
|
54
|
+
const lines = [`Git commit: ${payload.message}`];
|
|
55
|
+
if (payload.stat?.trim()) {
|
|
56
|
+
lines.push("", "Changed files:", payload.stat.trim());
|
|
57
|
+
}
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
function resolveGlobalOrActiveScope() {
|
|
61
|
+
const active = getActiveScope();
|
|
62
|
+
if (active)
|
|
63
|
+
return active;
|
|
64
|
+
return getScope("global") ?? null;
|
|
65
|
+
}
|
|
66
|
+
export async function handleGitCommitHook(payload) {
|
|
67
|
+
if (!isHooksEnabled()) {
|
|
68
|
+
log.debug("memory.hooks.git_commit.disabled");
|
|
69
|
+
return { observation_id: "disabled" };
|
|
70
|
+
}
|
|
71
|
+
const scope = resolveGlobalOrActiveScope();
|
|
72
|
+
if (!scope) {
|
|
73
|
+
throw new Error("No active or global memory scope found. Create a scope before using memory hooks.");
|
|
74
|
+
}
|
|
75
|
+
const content = buildGitCommitContent(payload);
|
|
76
|
+
const observation = recordObservation({
|
|
77
|
+
scope_id: scope.id,
|
|
78
|
+
content,
|
|
79
|
+
source: "git:commit",
|
|
80
|
+
tier: "warm",
|
|
81
|
+
confidence: 1.0,
|
|
82
|
+
});
|
|
83
|
+
log.info({ scope: scope.slug, observation_id: observation.id }, "memory.hooks.git_commit.recorded");
|
|
84
|
+
return { observation_id: String(observation.id) };
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Built-in handler: pr:merge → observation
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
function buildPrMergeContent(payload) {
|
|
90
|
+
const lines = [`PR #${payload.number} merged: ${payload.title}`];
|
|
91
|
+
if (payload.body?.trim()) {
|
|
92
|
+
lines.push("", payload.body.trim());
|
|
93
|
+
}
|
|
94
|
+
if (payload.files_changed && payload.files_changed.length > 0) {
|
|
95
|
+
lines.push("", "Files changed:");
|
|
96
|
+
for (const file of payload.files_changed) {
|
|
97
|
+
lines.push(` ${file}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
export async function handlePrMergeHook(payload) {
|
|
103
|
+
if (!isHooksEnabled()) {
|
|
104
|
+
log.debug("memory.hooks.pr_merge.disabled");
|
|
105
|
+
return { observation_id: "disabled" };
|
|
106
|
+
}
|
|
107
|
+
const scope = resolveGlobalOrActiveScope();
|
|
108
|
+
if (!scope) {
|
|
109
|
+
throw new Error("No active or global memory scope found. Create a scope before using memory hooks.");
|
|
110
|
+
}
|
|
111
|
+
const content = buildPrMergeContent(payload);
|
|
112
|
+
const observation = recordObservation({
|
|
113
|
+
scope_id: scope.id,
|
|
114
|
+
content,
|
|
115
|
+
source: "pr:merge",
|
|
116
|
+
tier: "warm",
|
|
117
|
+
confidence: 1.0,
|
|
118
|
+
});
|
|
119
|
+
log.info({ scope: scope.slug, observation_id: observation.id, pr: payload.number }, "memory.hooks.pr_merge.recorded");
|
|
120
|
+
return { observation_id: String(observation.id) };
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Built-in handler: scope:created → observation in global scope
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
hookDispatcher.register("scope:created", async (payload) => {
|
|
126
|
+
const globalScope = getScope("global");
|
|
127
|
+
if (!globalScope) {
|
|
128
|
+
log.debug("memory.hooks.scope_created.no_global_scope");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const content = `Scope '${payload.slug}' created: ${payload.description || payload.title}`;
|
|
132
|
+
const observation = recordObservation({
|
|
133
|
+
scope_id: globalScope.id,
|
|
134
|
+
content,
|
|
135
|
+
source: "scope:created",
|
|
136
|
+
tier: "warm",
|
|
137
|
+
confidence: 1.0,
|
|
138
|
+
});
|
|
139
|
+
log.info({ scope: payload.slug, observation_id: observation.id }, "memory.hooks.scope_created.recorded");
|
|
140
|
+
});
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Built-in handler: memory:decision → structured log event
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
hookDispatcher.register("memory:decision", async (payload) => {
|
|
145
|
+
log.info({
|
|
146
|
+
decision_id: payload.id,
|
|
147
|
+
scope_id: payload.scope_id,
|
|
148
|
+
title: payload.title,
|
|
149
|
+
}, "memory.hooks.decision.recorded");
|
|
150
|
+
});
|
|
151
|
+
//# sourceMappingURL=hooks.js.map
|