chapterhouse 0.13.1 → 0.14.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/dist/api/route-coverage.test.js +1 -3
- package/dist/api/server.js +0 -2
- package/dist/api/server.test.js +0 -281
- package/dist/config.js +3 -85
- package/dist/config.test.js +5 -123
- package/dist/copilot/agents.js +13 -10
- package/dist/copilot/agents.test.js +10 -11
- package/dist/copilot/memory-coordinator.js +12 -227
- package/dist/copilot/memory-coordinator.test.js +31 -250
- package/dist/copilot/orchestrator.js +8 -66
- package/dist/copilot/orchestrator.test.js +9 -467
- package/dist/copilot/skills.js +15 -1
- package/dist/copilot/system-message.js +9 -15
- package/dist/copilot/system-message.test.js +9 -22
- package/dist/copilot/tools/index.js +3 -3
- package/dist/copilot/tools-deps.js +1 -1
- package/dist/copilot/tools.agent.test.js +6 -0
- package/dist/copilot/tools.inventory.test.js +1 -14
- package/dist/daemon.js +7 -9
- package/dist/memory/assets.js +33 -0
- package/dist/memory/domains.js +58 -0
- package/dist/memory/domains.test.js +47 -0
- package/dist/memory/git.js +66 -0
- package/dist/memory/git.test.js +32 -0
- package/dist/memory/history.js +19 -0
- package/dist/memory/hottier.js +32 -0
- package/dist/memory/hottier.test.js +33 -0
- package/dist/memory/index.js +5 -13
- package/dist/memory/instructions.js +17 -0
- package/dist/memory/manager.js +92 -0
- package/dist/memory/markdown.js +78 -0
- package/dist/memory/markdown.test.js +42 -0
- package/dist/memory/mutex.js +18 -0
- package/dist/memory/path-guard.js +26 -0
- package/dist/memory/path-guard.test.js +27 -0
- package/dist/memory/paths.js +12 -0
- package/dist/memory/reconcile.js +75 -0
- package/dist/memory/reconcile.test.js +50 -0
- package/dist/memory/scaffold.js +37 -0
- package/dist/memory/scaffold.test.js +52 -0
- package/dist/memory/tools/commit-wrapper.js +32 -0
- package/dist/memory/tools/domains.js +73 -0
- package/dist/memory/tools/domains.test.js +66 -0
- package/dist/memory/tools/git.js +52 -0
- package/dist/memory/tools/index.js +25 -0
- package/dist/memory/tools/read.js +101 -0
- package/dist/memory/tools/read.test.js +69 -0
- package/dist/memory/tools/search.js +103 -0
- package/dist/memory/tools/search.test.js +63 -0
- package/dist/memory/tools/sessions.js +45 -0
- package/dist/memory/tools/sessions.test.js +74 -0
- package/dist/memory/tools/shared.js +7 -0
- package/dist/memory/tools/write.js +116 -0
- package/dist/memory/tools/write.test.js +107 -0
- package/dist/memory/walk.js +39 -0
- package/dist/store/repositories/sessions.js +40 -0
- package/dist/wiki/consolidation.js +3 -31
- package/dist/wiki/consolidation.test.js +0 -19
- package/memory-assets/domain-skill.md +38 -0
- package/memory-assets/seed/cog-meta/improvements.md +8 -0
- package/memory-assets/seed/cog-meta/patterns.md +5 -0
- package/memory-assets/seed/cog-meta/reflect-cursor.md +4 -0
- package/memory-assets/seed/cog-meta/scenario-calibration.md +14 -0
- package/memory-assets/seed/cog-meta/self-observations.md +4 -0
- package/memory-assets/seed/domains.yml +19 -0
- package/memory-assets/seed/glacier/index.md +6 -0
- package/memory-assets/seed/hot-memory.md +5 -0
- package/memory-assets/seed/link-index.md +6 -0
- package/memory-assets/system-instructions.md +214 -0
- package/memory-assets/templates/action-items.md +8 -0
- package/memory-assets/templates/entities.md +4 -0
- package/memory-assets/templates/generic.md +2 -0
- package/memory-assets/templates/hot-memory.md +4 -0
- package/memory-assets/templates/observations.md +4 -0
- package/package.json +2 -1
- package/skills/system/evolve/SKILL.md +131 -0
- package/skills/system/foresight/SKILL.md +116 -0
- package/skills/system/history/SKILL.md +58 -0
- package/skills/system/housekeeping/SKILL.md +185 -0
- package/skills/system/reflect/SKILL.md +214 -0
- package/skills/system/scenario/SKILL.md +198 -0
- package/skills/system/setup/SKILL.md +113 -0
- package/web/dist/assets/{WikiEdit-CGRxNazp.js → WikiEdit-BTsiBfbC.js} +2 -2
- package/web/dist/assets/{WikiEdit-CGRxNazp.js.map → WikiEdit-BTsiBfbC.js.map} +1 -1
- package/web/dist/assets/{WikiGraph-eVWNhZS3.js → WikiGraph-COOZbUeH.js} +2 -2
- package/web/dist/assets/{WikiGraph-eVWNhZS3.js.map → WikiGraph-COOZbUeH.js.map} +1 -1
- package/web/dist/assets/{index-gAvLNEvJ.js → index-aCcfpaLM.js} +101 -101
- package/web/dist/assets/index-aCcfpaLM.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/dist/api/routes/memory.js +0 -475
- package/dist/api/routes/memory.test.js +0 -108
- package/dist/copilot/tools/memory.js +0 -678
- package/dist/copilot/tools.memory.test.js +0 -590
- package/dist/memory/action-items.js +0 -100
- package/dist/memory/action-items.test.js +0 -83
- package/dist/memory/active-scope.js +0 -78
- package/dist/memory/active-scope.test.js +0 -80
- package/dist/memory/checkpoint-prompt.js +0 -71
- package/dist/memory/checkpoint.js +0 -274
- package/dist/memory/checkpoint.test.js +0 -275
- package/dist/memory/decisions.js +0 -54
- package/dist/memory/decisions.test.js +0 -92
- package/dist/memory/entities.js +0 -70
- package/dist/memory/entities.test.js +0 -65
- package/dist/memory/eot.js +0 -459
- package/dist/memory/eot.test.js +0 -949
- package/dist/memory/hooks.js +0 -149
- package/dist/memory/hooks.test.js +0 -325
- package/dist/memory/hot-tier.js +0 -283
- package/dist/memory/hot-tier.test.js +0 -275
- package/dist/memory/housekeeping-scheduler.js +0 -187
- package/dist/memory/housekeeping-scheduler.test.js +0 -236
- package/dist/memory/housekeeping.js +0 -497
- package/dist/memory/housekeeping.test.js +0 -410
- package/dist/memory/inbox.js +0 -83
- package/dist/memory/inbox.test.js +0 -178
- package/dist/memory/migration.js +0 -244
- package/dist/memory/migration.test.js +0 -108
- package/dist/memory/observations.js +0 -46
- package/dist/memory/observations.test.js +0 -86
- package/dist/memory/recall.js +0 -269
- package/dist/memory/recall.test.js +0 -265
- package/dist/memory/reflect.js +0 -273
- package/dist/memory/reflect.test.js +0 -256
- package/dist/memory/scope-lock.js +0 -26
- package/dist/memory/scope-lock.test.js +0 -118
- package/dist/memory/scopes.js +0 -89
- package/dist/memory/scopes.test.js +0 -176
- package/dist/memory/tiering.js +0 -223
- package/dist/memory/tiering.test.js +0 -323
- package/dist/memory/types.js +0 -2
- package/web/dist/assets/index-gAvLNEvJ.js.map +0 -1
package/dist/memory/hooks.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { childLogger } from "../util/logger.js";
|
|
2
|
-
import { config } from "../config.js";
|
|
3
|
-
import { getActiveScope } from "./active-scope.js";
|
|
4
|
-
import { recordObservation } from "./observations.js";
|
|
5
|
-
import { getScope } from "./scopes.js";
|
|
6
|
-
const log = childLogger("memory.hooks");
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Env knob
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
function isHooksEnabled() {
|
|
11
|
-
return config.memoryHooksEnabled;
|
|
12
|
-
}
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Dispatcher
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
export class MemoryHookDispatcher {
|
|
17
|
-
handlers = new Map();
|
|
18
|
-
register(event, handler) {
|
|
19
|
-
const bucket = this.handlers.get(event) ?? [];
|
|
20
|
-
bucket.push(handler);
|
|
21
|
-
this.handlers.set(event, bucket);
|
|
22
|
-
}
|
|
23
|
-
async dispatch(event, payload) {
|
|
24
|
-
if (!isHooksEnabled()) {
|
|
25
|
-
log.debug({ event }, "memory.hooks.skip.disabled");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
const bucket = this.handlers.get(event) ?? [];
|
|
29
|
-
if (bucket.length === 0) {
|
|
30
|
-
log.debug({ event }, "memory.hooks.no_handlers");
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
log.info({ event, handlerCount: bucket.length }, "memory.hooks.dispatch");
|
|
34
|
-
for (const handler of bucket) {
|
|
35
|
-
try {
|
|
36
|
-
await handler(payload);
|
|
37
|
-
}
|
|
38
|
-
catch (err) {
|
|
39
|
-
log.error({ err, event }, "memory.hooks.handler_error");
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// Singleton dispatcher — shared across the process
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
export const hookDispatcher = new MemoryHookDispatcher();
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
// Built-in handler: git:commit → observation in active scope
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
function buildGitCommitContent(payload) {
|
|
52
|
-
const lines = [`Git commit: ${payload.message}`];
|
|
53
|
-
if (payload.stat?.trim()) {
|
|
54
|
-
lines.push("", "Changed files:", payload.stat.trim());
|
|
55
|
-
}
|
|
56
|
-
return lines.join("\n");
|
|
57
|
-
}
|
|
58
|
-
function resolveGlobalOrActiveScope() {
|
|
59
|
-
const active = getActiveScope();
|
|
60
|
-
if (active)
|
|
61
|
-
return active;
|
|
62
|
-
return getScope("global") ?? null;
|
|
63
|
-
}
|
|
64
|
-
export async function handleGitCommitHook(payload) {
|
|
65
|
-
if (!isHooksEnabled()) {
|
|
66
|
-
log.debug("memory.hooks.git_commit.disabled");
|
|
67
|
-
return { observation_id: "disabled" };
|
|
68
|
-
}
|
|
69
|
-
const scope = resolveGlobalOrActiveScope();
|
|
70
|
-
if (!scope) {
|
|
71
|
-
throw new Error("No active or global memory scope found. Create a scope before using memory hooks.");
|
|
72
|
-
}
|
|
73
|
-
const content = buildGitCommitContent(payload);
|
|
74
|
-
const observation = recordObservation({
|
|
75
|
-
scope_id: scope.id,
|
|
76
|
-
content,
|
|
77
|
-
source: "git:commit",
|
|
78
|
-
tier: "warm",
|
|
79
|
-
confidence: 1.0,
|
|
80
|
-
});
|
|
81
|
-
log.info({ scope: scope.slug, observation_id: observation.id }, "memory.hooks.git_commit.recorded");
|
|
82
|
-
return { observation_id: String(observation.id) };
|
|
83
|
-
}
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// Built-in handler: pr:merge → observation
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
function buildPrMergeContent(payload) {
|
|
88
|
-
const lines = [`PR #${payload.number} merged: ${payload.title}`];
|
|
89
|
-
if (payload.body?.trim()) {
|
|
90
|
-
lines.push("", payload.body.trim());
|
|
91
|
-
}
|
|
92
|
-
if (payload.files_changed && payload.files_changed.length > 0) {
|
|
93
|
-
lines.push("", "Files changed:");
|
|
94
|
-
for (const file of payload.files_changed) {
|
|
95
|
-
lines.push(` ${file}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return lines.join("\n");
|
|
99
|
-
}
|
|
100
|
-
export async function handlePrMergeHook(payload) {
|
|
101
|
-
if (!isHooksEnabled()) {
|
|
102
|
-
log.debug("memory.hooks.pr_merge.disabled");
|
|
103
|
-
return { observation_id: "disabled" };
|
|
104
|
-
}
|
|
105
|
-
const scope = resolveGlobalOrActiveScope();
|
|
106
|
-
if (!scope) {
|
|
107
|
-
throw new Error("No active or global memory scope found. Create a scope before using memory hooks.");
|
|
108
|
-
}
|
|
109
|
-
const content = buildPrMergeContent(payload);
|
|
110
|
-
const observation = recordObservation({
|
|
111
|
-
scope_id: scope.id,
|
|
112
|
-
content,
|
|
113
|
-
source: "pr:merge",
|
|
114
|
-
tier: "warm",
|
|
115
|
-
confidence: 1.0,
|
|
116
|
-
});
|
|
117
|
-
log.info({ scope: scope.slug, observation_id: observation.id, pr: payload.number }, "memory.hooks.pr_merge.recorded");
|
|
118
|
-
return { observation_id: String(observation.id) };
|
|
119
|
-
}
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// Built-in handler: scope:created → observation in global scope
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
hookDispatcher.register("scope:created", async (payload) => {
|
|
124
|
-
const globalScope = getScope("global");
|
|
125
|
-
if (!globalScope) {
|
|
126
|
-
log.debug("memory.hooks.scope_created.no_global_scope");
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const content = `Scope '${payload.slug}' created: ${payload.description || payload.title}`;
|
|
130
|
-
const observation = recordObservation({
|
|
131
|
-
scope_id: globalScope.id,
|
|
132
|
-
content,
|
|
133
|
-
source: "scope:created",
|
|
134
|
-
tier: "warm",
|
|
135
|
-
confidence: 1.0,
|
|
136
|
-
});
|
|
137
|
-
log.info({ scope: payload.slug, observation_id: observation.id }, "memory.hooks.scope_created.recorded");
|
|
138
|
-
});
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
// Built-in handler: memory:decision → structured log event
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
hookDispatcher.register("memory:decision", async (payload) => {
|
|
143
|
-
log.info({
|
|
144
|
-
decision_id: payload.id,
|
|
145
|
-
scope_id: payload.scope_id,
|
|
146
|
-
title: payload.title,
|
|
147
|
-
}, "memory.hooks.decision.recorded");
|
|
148
|
-
});
|
|
149
|
-
//# sourceMappingURL=hooks.js.map
|
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import { DEFAULT_API_SERVER_STARTUP_TIMEOUT_MS, getFreePort, stopChild, waitForApiServerReady, } from "../test/api-server.js";
|
|
7
|
-
const repoRoot = process.cwd();
|
|
8
|
-
const sandboxRoot = join(repoRoot, ".test-work", `memory-hooks-${process.pid}`);
|
|
9
|
-
const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
|
|
10
|
-
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
11
|
-
function resetSandbox() {
|
|
12
|
-
mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
|
|
13
|
-
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
14
|
-
mkdirSync(chapterhouseHome, { recursive: true });
|
|
15
|
-
}
|
|
16
|
-
async function loadModules(cacheBust = `${Date.now()}-${Math.random()}`) {
|
|
17
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
18
|
-
const memoryModule = await import(new URL(`./index.js?case=${cacheBust}`, import.meta.url).href);
|
|
19
|
-
const hooksModule = await import(new URL(`./hooks.js?case=${cacheBust}`, import.meta.url).href);
|
|
20
|
-
return { dbModule, memoryModule, hooksModule };
|
|
21
|
-
}
|
|
22
|
-
function getFunction(module, name) {
|
|
23
|
-
const value = module[name];
|
|
24
|
-
assert.equal(typeof value, "function", `expected ${name} to be exported`);
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
|
-
test.beforeEach(async () => {
|
|
28
|
-
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
29
|
-
delete process.env.CHAPTERHOUSE_MEMORY_HOOKS_ENABLED;
|
|
30
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
31
|
-
dbModule.closeDb();
|
|
32
|
-
resetSandbox();
|
|
33
|
-
});
|
|
34
|
-
test.after(async () => {
|
|
35
|
-
delete process.env.CHAPTERHOUSE_MEMORY_HOOKS_ENABLED;
|
|
36
|
-
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
37
|
-
dbModule.closeDb();
|
|
38
|
-
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
39
|
-
});
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// MemoryHookDispatcher — basic register/dispatch
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
test("MemoryHookDispatcher dispatches to registered handlers", async () => {
|
|
44
|
-
const { hooksModule } = await loadModules("dispatch-basic");
|
|
45
|
-
const MemoryHookDispatcher = hooksModule["MemoryHookDispatcher"];
|
|
46
|
-
assert.ok(MemoryHookDispatcher, "MemoryHookDispatcher should be exported");
|
|
47
|
-
const dispatcher = new MemoryHookDispatcher();
|
|
48
|
-
const received = [];
|
|
49
|
-
dispatcher.register("git:commit", async (payload) => { received.push(payload); });
|
|
50
|
-
await dispatcher.dispatch("git:commit", { message: "initial commit", stat: "1 file changed" });
|
|
51
|
-
assert.equal(received.length, 1);
|
|
52
|
-
assert.deepEqual(received[0], { message: "initial commit", stat: "1 file changed" });
|
|
53
|
-
});
|
|
54
|
-
test("MemoryHookDispatcher dispatches to multiple handlers for same event", async () => {
|
|
55
|
-
const { hooksModule } = await loadModules("dispatch-multi");
|
|
56
|
-
const MemoryHookDispatcher = hooksModule["MemoryHookDispatcher"];
|
|
57
|
-
const dispatcher = new MemoryHookDispatcher();
|
|
58
|
-
let callCount = 0;
|
|
59
|
-
dispatcher.register("pr:merge", async () => { callCount++; });
|
|
60
|
-
dispatcher.register("pr:merge", async () => { callCount++; });
|
|
61
|
-
await dispatcher.dispatch("pr:merge", { number: 42, title: "feat: something" });
|
|
62
|
-
assert.equal(callCount, 2);
|
|
63
|
-
});
|
|
64
|
-
test("MemoryHookDispatcher is no-op when CHAPTERHOUSE_MEMORY_HOOKS_ENABLED=false", async () => {
|
|
65
|
-
process.env.CHAPTERHOUSE_MEMORY_HOOKS_ENABLED = "false";
|
|
66
|
-
const { hooksModule } = await loadModules("dispatch-disabled-false");
|
|
67
|
-
const MemoryHookDispatcher = hooksModule["MemoryHookDispatcher"];
|
|
68
|
-
const dispatcher = new MemoryHookDispatcher();
|
|
69
|
-
let called = false;
|
|
70
|
-
dispatcher.register("git:commit", async () => { called = true; });
|
|
71
|
-
await dispatcher.dispatch("git:commit", { message: "disabled test" });
|
|
72
|
-
assert.equal(called, false);
|
|
73
|
-
});
|
|
74
|
-
test("MemoryHookDispatcher is no-op when CHAPTERHOUSE_MEMORY_HOOKS_ENABLED=0", async () => {
|
|
75
|
-
process.env.CHAPTERHOUSE_MEMORY_HOOKS_ENABLED = "0";
|
|
76
|
-
const { hooksModule } = await loadModules("dispatch-disabled-zero");
|
|
77
|
-
const MemoryHookDispatcher = hooksModule["MemoryHookDispatcher"];
|
|
78
|
-
const dispatcher = new MemoryHookDispatcher();
|
|
79
|
-
let called = false;
|
|
80
|
-
dispatcher.register("pr:merge", async () => { called = true; });
|
|
81
|
-
await dispatcher.dispatch("pr:merge", { number: 1, title: "test" });
|
|
82
|
-
assert.equal(called, false);
|
|
83
|
-
});
|
|
84
|
-
test("MemoryHookDispatcher handler errors don't propagate to caller", async () => {
|
|
85
|
-
const { hooksModule } = await loadModules("dispatch-error-swallow");
|
|
86
|
-
const MemoryHookDispatcher = hooksModule["MemoryHookDispatcher"];
|
|
87
|
-
const dispatcher = new MemoryHookDispatcher();
|
|
88
|
-
dispatcher.register("git:commit", async () => { throw new Error("handler blew up"); });
|
|
89
|
-
await assert.doesNotReject(dispatcher.dispatch("git:commit", { message: "safe?" }));
|
|
90
|
-
});
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
// handleGitCommitHook — writes observation to DB
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
test("handleGitCommitHook records an observation in the active/global scope", async () => {
|
|
95
|
-
const { dbModule, memoryModule, hooksModule } = await loadModules("git-commit-hook");
|
|
96
|
-
dbModule.getDb();
|
|
97
|
-
const getScope = getFunction(memoryModule, "getScope");
|
|
98
|
-
const listObservations = getFunction(memoryModule, "listObservations");
|
|
99
|
-
const handleGitCommitHook = getFunction(hooksModule, "handleGitCommitHook");
|
|
100
|
-
const globalScope = getScope("global");
|
|
101
|
-
assert.ok(globalScope, "global scope must exist after DB init");
|
|
102
|
-
const result = await handleGitCommitHook({ message: "feat: add hooks", stat: "src/memory/hooks.ts | 5 ++" });
|
|
103
|
-
assert.ok(result.observation_id);
|
|
104
|
-
assert.notEqual(result.observation_id, "disabled");
|
|
105
|
-
const observations = listObservations({ scope_id: globalScope.id, limit: 10 });
|
|
106
|
-
const found = observations.find((obs) => obs.source === "git:commit" && obs.content.includes("feat: add hooks"));
|
|
107
|
-
assert.ok(found, "observation should be recorded with source git:commit");
|
|
108
|
-
assert.ok(found.content.includes("Changed files:"), "content should include changed files section");
|
|
109
|
-
});
|
|
110
|
-
test("handleGitCommitHook returns disabled when CHAPTERHOUSE_MEMORY_HOOKS_ENABLED=false", async () => {
|
|
111
|
-
process.env.CHAPTERHOUSE_MEMORY_HOOKS_ENABLED = "false";
|
|
112
|
-
const { dbModule, hooksModule } = await loadModules("git-commit-hook-disabled");
|
|
113
|
-
dbModule.getDb();
|
|
114
|
-
const handleGitCommitHook = getFunction(hooksModule, "handleGitCommitHook");
|
|
115
|
-
const result = await handleGitCommitHook({ message: "should not be recorded" });
|
|
116
|
-
assert.equal(result.observation_id, "disabled");
|
|
117
|
-
});
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
// handlePrMergeHook
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
test("handlePrMergeHook records an observation with PR details", async () => {
|
|
122
|
-
const { dbModule, memoryModule, hooksModule } = await loadModules("pr-merge-hook");
|
|
123
|
-
dbModule.getDb();
|
|
124
|
-
const getScope = getFunction(memoryModule, "getScope");
|
|
125
|
-
const listObservations = getFunction(memoryModule, "listObservations");
|
|
126
|
-
const handlePrMergeHook = getFunction(hooksModule, "handlePrMergeHook");
|
|
127
|
-
const globalScope = getScope("global");
|
|
128
|
-
assert.ok(globalScope, "global scope must exist");
|
|
129
|
-
const result = await handlePrMergeHook({
|
|
130
|
-
number: 42,
|
|
131
|
-
title: "feat(memory): event hooks",
|
|
132
|
-
body: "Implements Sprint 1 Track A",
|
|
133
|
-
files_changed: ["src/memory/hooks.ts", "src/memory/index.ts"],
|
|
134
|
-
});
|
|
135
|
-
assert.ok(result.observation_id);
|
|
136
|
-
assert.notEqual(result.observation_id, "disabled");
|
|
137
|
-
const observations = listObservations({ scope_id: globalScope.id, limit: 10 });
|
|
138
|
-
const found = observations.find((obs) => obs.source === "pr:merge" && obs.content.includes("PR #42"));
|
|
139
|
-
assert.ok(found, "observation should record PR number");
|
|
140
|
-
assert.ok(found.content.includes("feat(memory): event hooks"), "content should include PR title");
|
|
141
|
-
assert.ok(found.content.includes("src/memory/hooks.ts"), "content should list changed files");
|
|
142
|
-
});
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
// scope:created hook (fires from hookDispatcher after createScope)
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
test("scope:created handler writes observation to global scope", async () => {
|
|
147
|
-
const { dbModule, memoryModule, hooksModule } = await loadModules("scope-created");
|
|
148
|
-
dbModule.getDb();
|
|
149
|
-
const getScope = getFunction(memoryModule, "getScope");
|
|
150
|
-
const listObservations = getFunction(memoryModule, "listObservations");
|
|
151
|
-
const hookDispatcher = hooksModule["hookDispatcher"];
|
|
152
|
-
assert.ok(hookDispatcher, "hookDispatcher should be exported");
|
|
153
|
-
const globalScope = getScope("global");
|
|
154
|
-
assert.ok(globalScope, "global scope must exist");
|
|
155
|
-
await hookDispatcher.dispatch("scope:created", {
|
|
156
|
-
slug: "my-project",
|
|
157
|
-
title: "My Project",
|
|
158
|
-
description: "A test project scope",
|
|
159
|
-
});
|
|
160
|
-
const observations = listObservations({ scope_id: globalScope.id, limit: 10 });
|
|
161
|
-
const found = observations.find((obs) => obs.source === "scope:created" && obs.content.includes("my-project"));
|
|
162
|
-
assert.ok(found, "scope creation observation should be written to global scope");
|
|
163
|
-
assert.ok(found.content.includes("A test project scope"), "content should include description");
|
|
164
|
-
});
|
|
165
|
-
test("scope:created hook is no-op when CHAPTERHOUSE_MEMORY_HOOKS_ENABLED=0", async () => {
|
|
166
|
-
process.env.CHAPTERHOUSE_MEMORY_HOOKS_ENABLED = "0";
|
|
167
|
-
const { dbModule, memoryModule, hooksModule } = await loadModules("scope-created-disabled");
|
|
168
|
-
dbModule.getDb();
|
|
169
|
-
const getScope = getFunction(memoryModule, "getScope");
|
|
170
|
-
const listObservations = getFunction(memoryModule, "listObservations");
|
|
171
|
-
const hookDispatcher = hooksModule["hookDispatcher"];
|
|
172
|
-
const globalScope = getScope("global");
|
|
173
|
-
assert.ok(globalScope, "global scope must exist");
|
|
174
|
-
const before = listObservations({ scope_id: globalScope.id, limit: 100 });
|
|
175
|
-
await hookDispatcher.dispatch("scope:created", { slug: "silent", title: "Silent", description: "should not persist" });
|
|
176
|
-
const after = listObservations({ scope_id: globalScope.id, limit: 100 });
|
|
177
|
-
assert.equal(after.length, before.length, "no new observations when hooks disabled");
|
|
178
|
-
});
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
// API endpoints — integration tests using a spawned server
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
async function withStartedServer(run, extraEnv = {}, timeoutMs = DEFAULT_API_SERVER_STARTUP_TIMEOUT_MS) {
|
|
183
|
-
const testRoot = join(repoRoot, ".test-work", `hooks-api-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
184
|
-
mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
|
|
185
|
-
rmSync(testRoot, { recursive: true, force: true });
|
|
186
|
-
mkdirSync(testRoot, { recursive: true });
|
|
187
|
-
const port = await getFreePort();
|
|
188
|
-
const logs = [];
|
|
189
|
-
const child = spawn(process.execPath, [
|
|
190
|
-
"--input-type=module",
|
|
191
|
-
"-e",
|
|
192
|
-
"import { startApiServer } from './dist/api/server.js'; await startApiServer();",
|
|
193
|
-
], {
|
|
194
|
-
cwd: repoRoot,
|
|
195
|
-
env: {
|
|
196
|
-
...Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith("COPILOT_"))),
|
|
197
|
-
CHAPTERHOUSE_DISABLE_DOTENV: "1",
|
|
198
|
-
CHAPTERHOUSE_HOME: testRoot,
|
|
199
|
-
API_HOST: "127.0.0.1",
|
|
200
|
-
API_PORT: String(port),
|
|
201
|
-
API_TOKEN: "hooks-test-token",
|
|
202
|
-
...extraEnv,
|
|
203
|
-
},
|
|
204
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
205
|
-
});
|
|
206
|
-
child.stdout?.on("data", (chunk) => logs.push(String(chunk)));
|
|
207
|
-
child.stderr?.on("data", (chunk) => logs.push(String(chunk)));
|
|
208
|
-
const baseUrl = `http://127.0.0.1:${port}`;
|
|
209
|
-
try {
|
|
210
|
-
await waitForApiServerReady({ child, baseUrl, logs, timeoutMs });
|
|
211
|
-
await run({ baseUrl, authHeader: "Bearer hooks-test-token", testRoot });
|
|
212
|
-
}
|
|
213
|
-
finally {
|
|
214
|
-
await stopChild(child);
|
|
215
|
-
rmSync(testRoot, { recursive: true, force: true });
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
test("POST /api/memory/hooks/git-commit returns ok with observation_id", async () => {
|
|
219
|
-
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
220
|
-
const response = await fetch(`${baseUrl}/api/memory/hooks/git-commit`, {
|
|
221
|
-
method: "POST",
|
|
222
|
-
headers: { "Content-Type": "application/json", Authorization: authHeader },
|
|
223
|
-
body: JSON.stringify({ message: "feat: test commit", stat: "1 file changed, 5 insertions" }),
|
|
224
|
-
});
|
|
225
|
-
assert.equal(response.status, 200, `Expected 200, got ${response.status}`);
|
|
226
|
-
const body = await response.json();
|
|
227
|
-
assert.equal(body.ok, true);
|
|
228
|
-
assert.ok(typeof body.observation_id === "string" && body.observation_id.length > 0, "observation_id should be returned");
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
test("POST /api/memory/hooks/git-commit validates required message field", async () => {
|
|
232
|
-
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
233
|
-
const response = await fetch(`${baseUrl}/api/memory/hooks/git-commit`, {
|
|
234
|
-
method: "POST",
|
|
235
|
-
headers: { "Content-Type": "application/json", Authorization: authHeader },
|
|
236
|
-
body: JSON.stringify({ stat: "no message field" }),
|
|
237
|
-
});
|
|
238
|
-
assert.equal(response.status, 400, `Expected 400, got ${response.status}`);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
test("POST /api/memory/hooks/pr-merge returns ok with observation_id", async () => {
|
|
242
|
-
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
243
|
-
const response = await fetch(`${baseUrl}/api/memory/hooks/pr-merge`, {
|
|
244
|
-
method: "POST",
|
|
245
|
-
headers: { "Content-Type": "application/json", Authorization: authHeader },
|
|
246
|
-
body: JSON.stringify({
|
|
247
|
-
number: 99,
|
|
248
|
-
title: "feat: test PR",
|
|
249
|
-
body: "Adds some feature",
|
|
250
|
-
files_changed: ["src/foo.ts", "src/bar.ts"],
|
|
251
|
-
}),
|
|
252
|
-
});
|
|
253
|
-
assert.equal(response.status, 200, `Expected 200, got ${response.status}`);
|
|
254
|
-
const body = await response.json();
|
|
255
|
-
assert.equal(body.ok, true);
|
|
256
|
-
assert.ok(typeof body.observation_id === "string" && body.observation_id.length > 0, "observation_id should be returned");
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
test("POST /api/memory/hooks/pr-merge validates required fields", async () => {
|
|
260
|
-
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
261
|
-
const response = await fetch(`${baseUrl}/api/memory/hooks/pr-merge`, {
|
|
262
|
-
method: "POST",
|
|
263
|
-
headers: { "Content-Type": "application/json", Authorization: authHeader },
|
|
264
|
-
body: JSON.stringify({ title: "missing number" }),
|
|
265
|
-
});
|
|
266
|
-
assert.equal(response.status, 400, `Expected 400, got ${response.status}`);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
|
-
// Auth enforcement on hook endpoints — Bug #334
|
|
271
|
-
// ---------------------------------------------------------------------------
|
|
272
|
-
test("POST /api/memory/hooks/git-commit returns 401 without auth token", async () => {
|
|
273
|
-
await withStartedServer(async ({ baseUrl }) => {
|
|
274
|
-
const response = await fetch(`${baseUrl}/api/memory/hooks/git-commit`, {
|
|
275
|
-
method: "POST",
|
|
276
|
-
headers: { "Content-Type": "application/json" },
|
|
277
|
-
body: JSON.stringify({ message: "unauthorized commit" }),
|
|
278
|
-
});
|
|
279
|
-
assert.equal(response.status, 401, `Expected 401, got ${response.status}`);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
test("POST /api/memory/hooks/git-commit returns 401 with wrong auth token", async () => {
|
|
283
|
-
await withStartedServer(async ({ baseUrl }) => {
|
|
284
|
-
const response = await fetch(`${baseUrl}/api/memory/hooks/git-commit`, {
|
|
285
|
-
method: "POST",
|
|
286
|
-
headers: { "Content-Type": "application/json", Authorization: "Bearer wrong-token" },
|
|
287
|
-
body: JSON.stringify({ message: "bad token commit" }),
|
|
288
|
-
});
|
|
289
|
-
assert.equal(response.status, 401, `Expected 401, got ${response.status}`);
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
test("POST /api/memory/hooks/pr-merge returns 401 without auth token", async () => {
|
|
293
|
-
await withStartedServer(async ({ baseUrl }) => {
|
|
294
|
-
const response = await fetch(`${baseUrl}/api/memory/hooks/pr-merge`, {
|
|
295
|
-
method: "POST",
|
|
296
|
-
headers: { "Content-Type": "application/json" },
|
|
297
|
-
body: JSON.stringify({ number: 1, title: "unauthorized PR" }),
|
|
298
|
-
});
|
|
299
|
-
assert.equal(response.status, 401, `Expected 401, got ${response.status}`);
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
test("POST /api/memory/hooks/pr-merge returns 401 with wrong auth token", async () => {
|
|
303
|
-
await withStartedServer(async ({ baseUrl }) => {
|
|
304
|
-
const response = await fetch(`${baseUrl}/api/memory/hooks/pr-merge`, {
|
|
305
|
-
method: "POST",
|
|
306
|
-
headers: { "Content-Type": "application/json", Authorization: "Bearer wrong-token" },
|
|
307
|
-
body: JSON.stringify({ number: 1, title: "bad token PR" }),
|
|
308
|
-
});
|
|
309
|
-
assert.equal(response.status, 401, `Expected 401, got ${response.status}`);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
test("POST /api/memory/hooks/git-commit is no-op when CHAPTERHOUSE_MEMORY_HOOKS_ENABLED=false", async () => {
|
|
313
|
-
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
314
|
-
const response = await fetch(`${baseUrl}/api/memory/hooks/git-commit`, {
|
|
315
|
-
method: "POST",
|
|
316
|
-
headers: { "Content-Type": "application/json", Authorization: authHeader },
|
|
317
|
-
body: JSON.stringify({ message: "should be disabled" }),
|
|
318
|
-
});
|
|
319
|
-
assert.equal(response.status, 200, `Expected 200, got ${response.status}`);
|
|
320
|
-
const body = await response.json();
|
|
321
|
-
assert.equal(body.ok, true);
|
|
322
|
-
assert.equal(body.observation_id, "disabled", "observation_id should be 'disabled' when hooks are off");
|
|
323
|
-
}, { CHAPTERHOUSE_MEMORY_HOOKS_ENABLED: "0" });
|
|
324
|
-
});
|
|
325
|
-
//# sourceMappingURL=hooks.test.js.map
|