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.
Files changed (132) hide show
  1. package/dist/api/route-coverage.test.js +1 -3
  2. package/dist/api/server.js +0 -2
  3. package/dist/api/server.test.js +0 -281
  4. package/dist/config.js +3 -85
  5. package/dist/config.test.js +5 -123
  6. package/dist/copilot/agents.js +13 -10
  7. package/dist/copilot/agents.test.js +10 -11
  8. package/dist/copilot/memory-coordinator.js +12 -227
  9. package/dist/copilot/memory-coordinator.test.js +31 -250
  10. package/dist/copilot/orchestrator.js +8 -66
  11. package/dist/copilot/orchestrator.test.js +9 -467
  12. package/dist/copilot/skills.js +15 -1
  13. package/dist/copilot/system-message.js +9 -15
  14. package/dist/copilot/system-message.test.js +9 -22
  15. package/dist/copilot/tools/index.js +3 -3
  16. package/dist/copilot/tools-deps.js +1 -1
  17. package/dist/copilot/tools.agent.test.js +6 -0
  18. package/dist/copilot/tools.inventory.test.js +1 -14
  19. package/dist/daemon.js +7 -9
  20. package/dist/memory/assets.js +33 -0
  21. package/dist/memory/domains.js +58 -0
  22. package/dist/memory/domains.test.js +47 -0
  23. package/dist/memory/git.js +66 -0
  24. package/dist/memory/git.test.js +32 -0
  25. package/dist/memory/history.js +19 -0
  26. package/dist/memory/hottier.js +32 -0
  27. package/dist/memory/hottier.test.js +33 -0
  28. package/dist/memory/index.js +5 -13
  29. package/dist/memory/instructions.js +17 -0
  30. package/dist/memory/manager.js +92 -0
  31. package/dist/memory/markdown.js +78 -0
  32. package/dist/memory/markdown.test.js +42 -0
  33. package/dist/memory/mutex.js +18 -0
  34. package/dist/memory/path-guard.js +26 -0
  35. package/dist/memory/path-guard.test.js +27 -0
  36. package/dist/memory/paths.js +12 -0
  37. package/dist/memory/reconcile.js +75 -0
  38. package/dist/memory/reconcile.test.js +50 -0
  39. package/dist/memory/scaffold.js +37 -0
  40. package/dist/memory/scaffold.test.js +52 -0
  41. package/dist/memory/tools/commit-wrapper.js +32 -0
  42. package/dist/memory/tools/domains.js +73 -0
  43. package/dist/memory/tools/domains.test.js +66 -0
  44. package/dist/memory/tools/git.js +52 -0
  45. package/dist/memory/tools/index.js +25 -0
  46. package/dist/memory/tools/read.js +101 -0
  47. package/dist/memory/tools/read.test.js +69 -0
  48. package/dist/memory/tools/search.js +103 -0
  49. package/dist/memory/tools/search.test.js +63 -0
  50. package/dist/memory/tools/sessions.js +45 -0
  51. package/dist/memory/tools/sessions.test.js +74 -0
  52. package/dist/memory/tools/shared.js +7 -0
  53. package/dist/memory/tools/write.js +116 -0
  54. package/dist/memory/tools/write.test.js +107 -0
  55. package/dist/memory/walk.js +39 -0
  56. package/dist/store/repositories/sessions.js +40 -0
  57. package/dist/wiki/consolidation.js +3 -31
  58. package/dist/wiki/consolidation.test.js +0 -19
  59. package/memory-assets/domain-skill.md +38 -0
  60. package/memory-assets/seed/cog-meta/improvements.md +8 -0
  61. package/memory-assets/seed/cog-meta/patterns.md +5 -0
  62. package/memory-assets/seed/cog-meta/reflect-cursor.md +4 -0
  63. package/memory-assets/seed/cog-meta/scenario-calibration.md +14 -0
  64. package/memory-assets/seed/cog-meta/self-observations.md +4 -0
  65. package/memory-assets/seed/domains.yml +19 -0
  66. package/memory-assets/seed/glacier/index.md +6 -0
  67. package/memory-assets/seed/hot-memory.md +5 -0
  68. package/memory-assets/seed/link-index.md +6 -0
  69. package/memory-assets/system-instructions.md +214 -0
  70. package/memory-assets/templates/action-items.md +8 -0
  71. package/memory-assets/templates/entities.md +4 -0
  72. package/memory-assets/templates/generic.md +2 -0
  73. package/memory-assets/templates/hot-memory.md +4 -0
  74. package/memory-assets/templates/observations.md +4 -0
  75. package/package.json +2 -1
  76. package/skills/system/evolve/SKILL.md +131 -0
  77. package/skills/system/foresight/SKILL.md +116 -0
  78. package/skills/system/history/SKILL.md +58 -0
  79. package/skills/system/housekeeping/SKILL.md +185 -0
  80. package/skills/system/reflect/SKILL.md +214 -0
  81. package/skills/system/scenario/SKILL.md +198 -0
  82. package/skills/system/setup/SKILL.md +113 -0
  83. package/web/dist/assets/{WikiEdit-CGRxNazp.js → WikiEdit-BTsiBfbC.js} +2 -2
  84. package/web/dist/assets/{WikiEdit-CGRxNazp.js.map → WikiEdit-BTsiBfbC.js.map} +1 -1
  85. package/web/dist/assets/{WikiGraph-eVWNhZS3.js → WikiGraph-COOZbUeH.js} +2 -2
  86. package/web/dist/assets/{WikiGraph-eVWNhZS3.js.map → WikiGraph-COOZbUeH.js.map} +1 -1
  87. package/web/dist/assets/{index-gAvLNEvJ.js → index-aCcfpaLM.js} +101 -101
  88. package/web/dist/assets/index-aCcfpaLM.js.map +1 -0
  89. package/web/dist/index.html +1 -1
  90. package/dist/api/routes/memory.js +0 -475
  91. package/dist/api/routes/memory.test.js +0 -108
  92. package/dist/copilot/tools/memory.js +0 -678
  93. package/dist/copilot/tools.memory.test.js +0 -590
  94. package/dist/memory/action-items.js +0 -100
  95. package/dist/memory/action-items.test.js +0 -83
  96. package/dist/memory/active-scope.js +0 -78
  97. package/dist/memory/active-scope.test.js +0 -80
  98. package/dist/memory/checkpoint-prompt.js +0 -71
  99. package/dist/memory/checkpoint.js +0 -274
  100. package/dist/memory/checkpoint.test.js +0 -275
  101. package/dist/memory/decisions.js +0 -54
  102. package/dist/memory/decisions.test.js +0 -92
  103. package/dist/memory/entities.js +0 -70
  104. package/dist/memory/entities.test.js +0 -65
  105. package/dist/memory/eot.js +0 -459
  106. package/dist/memory/eot.test.js +0 -949
  107. package/dist/memory/hooks.js +0 -149
  108. package/dist/memory/hooks.test.js +0 -325
  109. package/dist/memory/hot-tier.js +0 -283
  110. package/dist/memory/hot-tier.test.js +0 -275
  111. package/dist/memory/housekeeping-scheduler.js +0 -187
  112. package/dist/memory/housekeeping-scheduler.test.js +0 -236
  113. package/dist/memory/housekeeping.js +0 -497
  114. package/dist/memory/housekeeping.test.js +0 -410
  115. package/dist/memory/inbox.js +0 -83
  116. package/dist/memory/inbox.test.js +0 -178
  117. package/dist/memory/migration.js +0 -244
  118. package/dist/memory/migration.test.js +0 -108
  119. package/dist/memory/observations.js +0 -46
  120. package/dist/memory/observations.test.js +0 -86
  121. package/dist/memory/recall.js +0 -269
  122. package/dist/memory/recall.test.js +0 -265
  123. package/dist/memory/reflect.js +0 -273
  124. package/dist/memory/reflect.test.js +0 -256
  125. package/dist/memory/scope-lock.js +0 -26
  126. package/dist/memory/scope-lock.test.js +0 -118
  127. package/dist/memory/scopes.js +0 -89
  128. package/dist/memory/scopes.test.js +0 -176
  129. package/dist/memory/tiering.js +0 -223
  130. package/dist/memory/tiering.test.js +0 -323
  131. package/dist/memory/types.js +0 -2
  132. package/web/dist/assets/index-gAvLNEvJ.js.map +0 -1
@@ -1,590 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import test from "node:test";
6
- async function loadModules() {
7
- const nonce = `${Date.now()}-${Math.random()}`;
8
- const toolsModule = await import(new URL(`./tools.js?case=${nonce}`, import.meta.url).href);
9
- const agentsModule = await import(new URL("./agents.js", import.meta.url).href);
10
- const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
11
- return { toolsModule, agentsModule, dbModule };
12
- }
13
- function findTool(tools, name) {
14
- const tool = tools.find((entry) => entry.name === name);
15
- assert.ok(tool, `${name} tool should be registered`);
16
- return tool;
17
- }
18
- test.beforeEach(() => {
19
- process.env.CHAPTERHOUSE_HOME = mkdtempSync(join(tmpdir(), "chapterhouse-tools-memory-"));
20
- });
21
- test.afterEach(async () => {
22
- const home = process.env.CHAPTERHOUSE_HOME;
23
- if (home) {
24
- const dbModule = await import("../store/db.js");
25
- dbModule.closeDb();
26
- rmSync(home, { recursive: true, force: true });
27
- }
28
- });
29
- test("memory_set_scope invalidates the orchestrator session after scheduling the scope-change checkpoint", async (t) => {
30
- const events = [];
31
- t.mock.module("./orchestrator.js", {
32
- namedExports: {
33
- getCurrentSourceChannel: () => "web",
34
- getCurrentActivityCallback: () => undefined,
35
- getCurrentActiveProjectRules: () => null,
36
- getCurrentAuthenticatedUser: () => undefined,
37
- getLastAuthenticatedUser: () => undefined,
38
- getCurrentAuthorizationHeader: () => undefined,
39
- getCurrentSessionKey: () => "session-test",
40
- maybeScheduleScopeChangeCheckpoint: (sessionKey, previousScope, nextScope) => {
41
- events.push(`checkpoint:${sessionKey}:${previousScope?.slug ?? "null"}->${nextScope?.slug ?? "null"}`);
42
- },
43
- resetCheckpointSessionState: (sessionKey) => {
44
- events.push(`reset:${sessionKey}`);
45
- },
46
- invalidateOrchestratorSession: (sessionKey) => {
47
- events.push(`invalidate:${sessionKey}`);
48
- },
49
- sendToAgentSession: async () => "",
50
- switchSessionModel: async () => { },
51
- },
52
- });
53
- const { toolsModule } = await loadModules();
54
- const tools = toolsModule.createTools({
55
- client: { async listModels() { return []; } },
56
- onAgentTaskComplete: () => { },
57
- });
58
- const memoryRemember = findTool(tools, "memory_remember");
59
- const memorySetScope = findTool(tools, "memory_set_scope");
60
- const remembered = await memoryRemember.handler({
61
- content: "Scope changes should refresh hot-tier memory on the next turn.",
62
- scope: "chapterhouse",
63
- kind: "observation",
64
- }, {});
65
- assert.equal(remembered.ok, true);
66
- events.length = 0;
67
- const result = await memorySetScope.handler({ slug: "chapterhouse" }, {});
68
- assert.equal(result.active_scope?.slug, "chapterhouse");
69
- assert.deepEqual(events, [
70
- "checkpoint:session-test:null->chapterhouse",
71
- "invalidate:session-test",
72
- "reset:session-test",
73
- ]);
74
- events.length = 0;
75
- const unchangedResult = await memorySetScope.handler({ slug: "chapterhouse" }, {});
76
- assert.equal(unchangedResult.active_scope?.slug, "chapterhouse");
77
- assert.deepEqual(events, []);
78
- });
79
- test("memory tools remember, recall, set scope, and enforce orchestrator-only writes", async () => {
80
- const { toolsModule, agentsModule, dbModule } = await loadModules();
81
- const tools = toolsModule.createTools({
82
- client: { async listModels() { return []; } },
83
- onAgentTaskComplete: () => { },
84
- });
85
- const memoryRemember = findTool(tools, "memory_remember");
86
- const memoryRecall = findTool(tools, "memory_recall");
87
- const memorySetScope = findTool(tools, "memory_set_scope");
88
- const memoryCreateScope = findTool(tools, "memory_create_scope");
89
- const bindToolsToAgent = agentsModule.bindToolsToAgent;
90
- assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
91
- const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
92
- const coderTools = bindToolsToAgent("coder", tools);
93
- const chapterhouseRemember = findTool(chapterhouseTools, "memory_remember");
94
- const chapterhouseRecall = findTool(chapterhouseTools, "memory_recall");
95
- const chapterhouseSetScope = findTool(chapterhouseTools, "memory_set_scope");
96
- const remembered = await chapterhouseRemember.handler({
97
- content: "Use SQLite FTS5 to recall scoped memory entries.",
98
- scope: "chapterhouse",
99
- kind: "observation",
100
- }, {});
101
- assert.equal(typeof remembered, "object");
102
- assert.equal(remembered.ok, true);
103
- const db = dbModule.getDb();
104
- const row = db.prepare(`
105
- SELECT id, content
106
- FROM mem_observations
107
- WHERE content = ?
108
- `).get("Use SQLite FTS5 to recall scoped memory entries.");
109
- assert.ok(row, "memory_remember should write an observation row");
110
- const ftsHits = db.prepare(`
111
- SELECT rowid
112
- FROM mem_observations_fts
113
- WHERE mem_observations_fts MATCH 'SQLite'
114
- `).all();
115
- assert.equal(ftsHits.some((hit) => hit.rowid === row.id), true);
116
- const recalled = await chapterhouseRecall.handler({
117
- query: "SQLite FTS5 scoped memory",
118
- scope: "chapterhouse",
119
- limit: 10,
120
- }, {});
121
- assert.equal(typeof recalled, "object");
122
- assert.equal((recalled.hits ?? []).some((hit) => hit.id === row.id), true);
123
- const scopeResult = await chapterhouseSetScope.handler({ slug: "chapterhouse" }, {});
124
- assert.equal(scopeResult.active_scope?.slug, "chapterhouse");
125
- const createdScope = await memoryCreateScope.handler({
126
- slug: "docs-site",
127
- title: "Docs Site",
128
- description: "Documentation publishing and content workflows",
129
- }, {});
130
- assert.deepEqual(createdScope, {
131
- ok: true,
132
- scope: {
133
- slug: "docs-site",
134
- title: "Docs Site",
135
- description: "Documentation publishing and content workflows",
136
- active: true,
137
- },
138
- });
139
- const duplicateScope = await memoryCreateScope.handler({
140
- slug: "docs-site",
141
- title: "Docs Site Again",
142
- }, {});
143
- assert.match(String(duplicateScope), /already exists/i);
144
- const invalidScope = await memoryCreateScope.handler({
145
- slug: "Docs Site",
146
- title: "Docs Site",
147
- }, {});
148
- assert.match(String(invalidScope), /kebab-case/i);
149
- const implicitScopeRemember = await chapterhouseRemember.handler({
150
- content: "Implicit active-scope writes should route to chapterhouse.",
151
- kind: "observation",
152
- }, {});
153
- assert.equal(implicitScopeRemember.ok, true);
154
- const coderVisibleTools = agentsModule.filterToolsForAgent({
155
- slug: "coder",
156
- name: "Coder",
157
- description: "Software engineer",
158
- model: "gpt-5.4",
159
- systemMessage: "test",
160
- }, tools);
161
- assert.equal(coderVisibleTools.some((tool) => tool.name === "memory_remember"), false);
162
- assert.equal(coderVisibleTools.some((tool) => tool.name === "memory_set_scope"), false);
163
- assert.equal(coderVisibleTools.some((tool) => tool.name === "memory_recall"), true);
164
- const coderRemember = findTool(coderTools, "memory_remember");
165
- const rejected = await coderRemember.handler({
166
- content: "Subagents should not be able to write memory directly.",
167
- scope: "chapterhouse",
168
- kind: "observation",
169
- }, {});
170
- assert.match(String(rejected), /orchestrator-only|memory_propose/i);
171
- const coderRecall = findTool(coderVisibleTools, "memory_recall");
172
- const coderRecalled = await coderRecall.handler({
173
- query: "SQLite",
174
- scope: "chapterhouse",
175
- limit: 10,
176
- }, {});
177
- assert.equal(typeof coderRecalled, "object");
178
- assert.equal((coderRecalled.hits ?? []).some((hit) => hit.id === row.id), true);
179
- assert.ok(memoryRemember && memoryRecall && memorySetScope && memoryCreateScope);
180
- });
181
- test("memory_propose queues pending proposals, defaults scope from the active scope, and captures delegated task context", async () => {
182
- const { toolsModule, agentsModule, dbModule } = await loadModules();
183
- const tools = toolsModule.createTools({
184
- client: { async listModels() { return []; } },
185
- onAgentTaskComplete: () => { },
186
- });
187
- const bindToolsToAgent = agentsModule.bindToolsToAgent;
188
- assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
189
- const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
190
- const coderTools = bindToolsToAgent("coder", tools, "task-propose-001");
191
- const memorySetScope = findTool(chapterhouseTools, "memory_set_scope");
192
- const memoryPropose = findTool(coderTools, "memory_propose");
193
- await memorySetScope.handler({ slug: "chapterhouse" }, {});
194
- const proposed = await memoryPropose.handler({
195
- kind: "observation",
196
- payload: {
197
- content: "Subagents can queue durable observations for orchestrator review.",
198
- source: "final task summary",
199
- },
200
- confidence: 0.9,
201
- reason: "The user explicitly asked for the new proposal path.",
202
- }, {});
203
- assert.equal(proposed.status, "queued");
204
- assert.equal(typeof proposed.proposal_id, "number");
205
- const db = dbModule.getDb();
206
- const row = db.prepare(`
207
- SELECT kind, status, source_agent, source_task_id, payload
208
- FROM mem_inbox
209
- WHERE id = ?
210
- `).get(proposed.proposal_id);
211
- assert.ok(row, "memory_propose should insert a mem_inbox row");
212
- assert.equal(row.kind, "memory_proposal");
213
- assert.equal(row.status, "pending");
214
- assert.equal(row.source_agent, "coder");
215
- assert.equal(row.source_task_id, "task-propose-001");
216
- const payload = JSON.parse(row.payload);
217
- assert.equal(payload.kind, "observation");
218
- assert.equal(payload.scope_slug, "chapterhouse");
219
- assert.equal(payload.confidence, 0.9);
220
- assert.equal(payload.reason, "The user explicitly asked for the new proposal path.");
221
- assert.equal(payload.payload.content, "Subagents can queue durable observations for orchestrator review.");
222
- });
223
- test("memory_propose from a persistent agent honors an explicit scope_slug", async () => {
224
- const home = process.env.CHAPTERHOUSE_HOME;
225
- assert.ok(home, "test home should be set");
226
- const { AGENTS_DIR: agentsDir } = await import("../paths.js");
227
- mkdirSync(agentsDir, { recursive: true });
228
- writeFileSync(join(agentsDir, "bellonda.agent.md"), [
229
- "---",
230
- "name: Bellonda",
231
- "description: Mentat of the infrastructure domain",
232
- "model: claude-sonnet-4.6",
233
- "persistent: true",
234
- "scope: infra",
235
- "---",
236
- "",
237
- "You are Bellonda.",
238
- ].join("\n"));
239
- const { toolsModule, agentsModule, dbModule } = await loadModules();
240
- agentsModule.loadAgents();
241
- const tools = toolsModule.createTools({
242
- client: { async listModels() { return []; } },
243
- onAgentTaskComplete: () => { },
244
- });
245
- const bindToolsToAgent = agentsModule.bindToolsToAgent;
246
- assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
247
- const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
248
- const bellondaTools = bindToolsToAgent("bellonda", tools, "task-persistent-scope-001");
249
- await findTool(chapterhouseTools, "memory_set_scope").handler({ slug: "chapterhouse" }, {});
250
- const memoryModule = await import("../memory/index.js");
251
- const proposed = await memoryModule.withActiveScope("infra", () => findTool(bellondaTools, "memory_propose").handler({
252
- kind: "observation",
253
- scope_slug: "chapterhouse",
254
- payload: {
255
- content: "Persistent agents can explicitly route proposals to another scope when needed.",
256
- },
257
- }, {}));
258
- assert.equal(proposed.status, "queued");
259
- const row = dbModule.getDb().prepare(`
260
- SELECT source_agent, source_task_id, payload
261
- FROM mem_inbox
262
- WHERE id = ?
263
- `).get(proposed.proposal_id);
264
- assert.ok(row, "memory_propose should insert a mem_inbox row");
265
- assert.equal(row.source_agent, "bellonda");
266
- assert.equal(row.source_task_id, "task-persistent-scope-001");
267
- const payload = JSON.parse(row.payload);
268
- assert.equal(payload.scope_slug, "chapterhouse");
269
- });
270
- test("memory_propose ignores a missing subagent bound scope and falls back to the active scope", async () => {
271
- const home = process.env.CHAPTERHOUSE_HOME;
272
- assert.ok(home, "test home should be set");
273
- const { AGENTS_DIR: agentsDir } = await import("../paths.js");
274
- mkdirSync(agentsDir, { recursive: true });
275
- writeFileSync(join(agentsDir, "hwi.agent.md"), [
276
- "---",
277
- "name: Hwi",
278
- "description: Mentat of Brian's personal scope",
279
- "model: claude-sonnet-4.6",
280
- "persistent: true",
281
- "scope: brian",
282
- "---",
283
- "",
284
- "You are Hwi.",
285
- ].join("\n"));
286
- const { toolsModule, agentsModule, dbModule } = await loadModules();
287
- agentsModule.loadAgents();
288
- const tools = toolsModule.createTools({
289
- client: { async listModels() { return []; } },
290
- onAgentTaskComplete: () => { },
291
- });
292
- const bindToolsToAgent = agentsModule.bindToolsToAgent;
293
- assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
294
- const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
295
- const hwiTools = bindToolsToAgent("hwi", tools, "task-hwi-action-item");
296
- await findTool(chapterhouseTools, "memory_set_scope").handler({ slug: "chapterhouse" }, {});
297
- const proposed = await findTool(hwiTools, "memory_propose").handler({
298
- kind: "action_item",
299
- payload: {
300
- title: "Follow up with Brian",
301
- },
302
- }, {});
303
- assert.equal(proposed.status, "queued");
304
- const row = dbModule.getDb().prepare(`
305
- SELECT source_agent, payload
306
- FROM mem_inbox
307
- WHERE id = ?
308
- `).get(proposed.proposal_id);
309
- assert.ok(row, "memory_propose should insert a mem_inbox row");
310
- assert.equal(row.source_agent, "hwi");
311
- const payload = JSON.parse(row.payload);
312
- assert.equal(payload.scope_slug, "chapterhouse");
313
- });
314
- test("memory_propose accepts entity proposals with entity_kind and queues the full payload", async () => {
315
- const { toolsModule, agentsModule, dbModule } = await loadModules();
316
- const tools = toolsModule.createTools({
317
- client: { async listModels() { return []; } },
318
- onAgentTaskComplete: () => { },
319
- });
320
- const bindToolsToAgent = agentsModule.bindToolsToAgent;
321
- assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
322
- const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
323
- const coderTools = bindToolsToAgent("coder", tools, "task-entity-propose");
324
- await findTool(chapterhouseTools, "memory_set_scope").handler({ slug: "chapterhouse" }, {});
325
- const proposed = await findTool(coderTools, "memory_propose").handler({
326
- kind: "entity",
327
- payload: {
328
- name: "truenas",
329
- entity_kind: "host",
330
- summary: "NAS host used by Bellonda.",
331
- },
332
- confidence: 0.8,
333
- }, {});
334
- assert.equal(proposed.status, "queued");
335
- const row = dbModule.getDb().prepare(`
336
- SELECT payload
337
- FROM mem_inbox
338
- WHERE id = ?
339
- `).get(proposed.proposal_id);
340
- assert.ok(row, "memory_propose should insert a mem_inbox row");
341
- const payload = JSON.parse(row.payload);
342
- assert.equal(payload.kind, "entity");
343
- assert.equal(payload.scope_slug, "chapterhouse");
344
- assert.deepEqual(payload.payload, {
345
- name: "truenas",
346
- entity_kind: "host",
347
- summary: "NAS host used by Bellonda.",
348
- });
349
- });
350
- test("action item memory tools add, list, complete, drop, and snooze action items", async () => {
351
- const { toolsModule, agentsModule } = await loadModules();
352
- const tools = toolsModule.createTools({
353
- client: { async listModels() { return []; } },
354
- onAgentTaskComplete: () => { },
355
- });
356
- const bindToolsToAgent = agentsModule.bindToolsToAgent;
357
- assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
358
- const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
359
- const coderTools = bindToolsToAgent("coder", tools);
360
- const memoryAddActionItem = findTool(chapterhouseTools, "memory_add_action_item");
361
- const memoryListActionItems = findTool(coderTools, "memory_list_action_items");
362
- const memoryCompleteActionItem = findTool(chapterhouseTools, "memory_complete_action_item");
363
- const memoryDropActionItem = findTool(chapterhouseTools, "memory_drop_action_item");
364
- const memorySnoozeActionItem = findTool(chapterhouseTools, "memory_snooze_action_item");
365
- const visibleToCoder = agentsModule.filterToolsForAgent({
366
- slug: "coder",
367
- name: "Coder",
368
- description: "Software engineer",
369
- model: "gpt-5.4",
370
- systemMessage: "test",
371
- }, tools);
372
- assert.equal(visibleToCoder.some((tool) => tool.name === "memory_list_action_items"), true);
373
- assert.equal(visibleToCoder.some((tool) => tool.name === "memory_add_action_item"), false);
374
- const added = await memoryAddActionItem.handler({
375
- scope: "chapterhouse",
376
- title: "Migrate feature ideas",
377
- detail: "Move the parked feature-ideas.md page into mem_action_items.",
378
- due_at: "2026-05-15T12:00:00.000Z",
379
- entity_name: "Chapterhouse",
380
- entity_kind: "project",
381
- source: "test",
382
- }, {});
383
- assert.equal(added.ok, true);
384
- const addedId = added.id;
385
- const listed = await memoryListActionItems.handler({ scope: "chapterhouse" }, {});
386
- assert.equal((listed.action_items).some((item) => item.id === addedId), true);
387
- const completed = await memoryCompleteActionItem.handler({ id: addedId, resolution_reason: "Done." }, {});
388
- assert.equal(completed.ok, true);
389
- assert.equal(completed.status, "done");
390
- const afterComplete = await memoryListActionItems.handler({ scope: "chapterhouse" }, {});
391
- assert.equal((afterComplete.action_items).some((item) => item.id === addedId), false);
392
- const dropped = await memoryAddActionItem.handler({ scope: "chapterhouse", title: "Drop me" }, {});
393
- const droppedResult = await memoryDropActionItem.handler({
394
- id: dropped.id,
395
- reason: "No longer needed.",
396
- }, {});
397
- assert.equal(droppedResult.status, "dropped");
398
- const snoozed = await memoryAddActionItem.handler({ scope: "chapterhouse", title: "Snooze me" }, {});
399
- const snoozedResult = await memorySnoozeActionItem.handler({
400
- id: snoozed.id,
401
- snooze_until: "2999-01-01T00:00:00.000Z",
402
- }, {});
403
- assert.equal(snoozedResult.status, "snoozed");
404
- });
405
- test("memory_propose accepts action_item proposals with a resolvable payload shape", async () => {
406
- const { toolsModule, agentsModule, dbModule } = await loadModules();
407
- const tools = toolsModule.createTools({
408
- client: { async listModels() { return []; } },
409
- onAgentTaskComplete: () => { },
410
- });
411
- const bindToolsToAgent = agentsModule.bindToolsToAgent;
412
- assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
413
- const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
414
- const coderTools = bindToolsToAgent("coder", tools, "task-action-propose");
415
- await findTool(chapterhouseTools, "memory_set_scope").handler({ slug: "chapterhouse" }, {});
416
- const proposed = await findTool(coderTools, "memory_propose").handler({
417
- kind: "action_item",
418
- payload: {
419
- title: "Remind infra about high disk usage",
420
- detail: "Next time disk exceeds 85%, notify Bellonda.",
421
- due_at: "2026-05-15T12:00:00.000Z",
422
- source: "test",
423
- },
424
- confidence: 0.8,
425
- }, {});
426
- assert.equal(proposed.status, "queued");
427
- const row = dbModule.getDb().prepare(`
428
- SELECT payload
429
- FROM mem_inbox
430
- WHERE id = ?
431
- `).get(proposed.proposal_id);
432
- const payload = JSON.parse(row.payload);
433
- assert.equal(payload.kind, "action_item");
434
- assert.equal(payload.scope_slug, "chapterhouse");
435
- assert.equal(payload.payload.title, "Remind infra about high disk usage");
436
- assert.equal(payload.payload.detail, "Next time disk exceeds 85%, notify Bellonda.");
437
- });
438
- test("memory_propose rejects action_item proposals with blank titles before queuing", async () => {
439
- const { toolsModule, dbModule } = await loadModules();
440
- const tools = toolsModule.createTools({
441
- client: { async listModels() { return []; } },
442
- onAgentTaskComplete: () => { },
443
- });
444
- const memoryPropose = findTool(tools, "memory_propose");
445
- const result = await memoryPropose.handler({
446
- kind: "action_item",
447
- payload: {
448
- title: " ",
449
- },
450
- }, {});
451
- assert.match(String(result), /title/i);
452
- const queued = dbModule.getDb().prepare(`
453
- SELECT COUNT(*) AS count
454
- FROM mem_inbox
455
- `).get();
456
- assert.equal(queued.count, 0);
457
- });
458
- test("memory_propose rejects invalid proposal kinds", async () => {
459
- const { toolsModule } = await loadModules();
460
- const tools = toolsModule.createTools({
461
- client: { async listModels() { return []; } },
462
- onAgentTaskComplete: () => { },
463
- });
464
- const memoryPropose = findTool(tools, "memory_propose");
465
- const result = await memoryPropose.handler({
466
- kind: "pattern",
467
- payload: { content: "invalid kind" },
468
- }, {});
469
- assert.match(String(result), /observation|decision|entity|action_item/i);
470
- });
471
- test("memory_housekeep is orchestrator-only and returns housekeeping summaries", async () => {
472
- const { toolsModule, agentsModule, dbModule } = await loadModules();
473
- const tools = toolsModule.createTools({
474
- client: { async listModels() { return []; } },
475
- onAgentTaskComplete: () => { },
476
- });
477
- const bindToolsToAgent = agentsModule.bindToolsToAgent;
478
- assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
479
- const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
480
- const coderTools = bindToolsToAgent("coder", tools);
481
- const memorySetScope = findTool(chapterhouseTools, "memory_set_scope");
482
- const memoryRemember = findTool(chapterhouseTools, "memory_remember");
483
- const memoryHousekeep = findTool(chapterhouseTools, "memory_housekeep");
484
- const coderHousekeep = findTool(coderTools, "memory_housekeep");
485
- await memorySetScope.handler({ slug: "chapterhouse" }, {});
486
- const remembered = await memoryRemember.handler({
487
- content: "Very old low-confidence memory should be archived by housekeeping.",
488
- scope: "chapterhouse",
489
- kind: "observation",
490
- }, {});
491
- const observationId = remembered.id;
492
- const db = dbModule.getDb();
493
- db.prepare(`
494
- UPDATE mem_observations
495
- SET confidence = 0.1, created_at = datetime('now', '-31 days')
496
- WHERE id = ?
497
- `).run(observationId);
498
- const denied = await coderHousekeep.handler({ passes: ["decay"] }, {});
499
- assert.match(String(denied), /orchestrator-only/i);
500
- const visibleToCoder = agentsModule.filterToolsForAgent({
501
- slug: "coder",
502
- name: "Coder",
503
- description: "Software engineer",
504
- model: "gpt-5.4",
505
- systemMessage: "test",
506
- }, tools);
507
- assert.equal(visibleToCoder.some((tool) => tool.name === "memory_housekeep"), false);
508
- const result = await memoryHousekeep.handler({
509
- scope_slug: "chapterhouse",
510
- passes: ["decay"],
511
- }, {});
512
- assert.equal(result.ok, true);
513
- assert.deepEqual(result.scope_ids.length, 1);
514
- assert.equal(result.summaries[0]?.pass, "decayPass");
515
- assert.equal(result.summaries[0]?.modified, 1);
516
- });
517
- test("memory_promote and memory_demote are orchestrator-only manual tier controls", async () => {
518
- const { toolsModule, agentsModule, dbModule } = await loadModules();
519
- const tools = toolsModule.createTools({
520
- client: { async listModels() { return []; } },
521
- onAgentTaskComplete: () => { },
522
- });
523
- const bindToolsToAgent = agentsModule.bindToolsToAgent;
524
- assert.equal(typeof bindToolsToAgent, "function", "bindToolsToAgent should be exported");
525
- const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
526
- const coderTools = bindToolsToAgent("coder", tools);
527
- const memoryRemember = findTool(chapterhouseTools, "memory_remember");
528
- const memoryPromote = findTool(chapterhouseTools, "memory_promote");
529
- const memoryDemote = findTool(chapterhouseTools, "memory_demote");
530
- const coderPromote = findTool(coderTools, "memory_promote");
531
- const coderDemote = findTool(coderTools, "memory_demote");
532
- const remembered = await memoryRemember.handler({
533
- content: "Manual tier controls should change this observation.",
534
- scope: "chapterhouse",
535
- kind: "observation",
536
- }, {});
537
- const observationId = remembered.id;
538
- const db = dbModule.getDb();
539
- assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(observationId).tier, "warm");
540
- const deniedPromote = await coderPromote.handler({ table: "observation", id: observationId, reason: "test" }, {});
541
- const deniedDemote = await coderDemote.handler({ table: "observation", id: observationId, reason: "test" }, {});
542
- assert.match(String(deniedPromote), /orchestrator-only/i);
543
- assert.match(String(deniedDemote), /orchestrator-only/i);
544
- assert.equal(agentsModule.filterToolsForAgent({
545
- slug: "coder",
546
- name: "Coder",
547
- description: "Software engineer",
548
- model: "gpt-5.4",
549
- systemMessage: "test",
550
- }, tools).some((tool) => tool.name === "memory_promote" || tool.name === "memory_demote"), false);
551
- const promoted = await memoryPromote.handler({ table: "observation", id: observationId, reason: "actively relevant" }, {});
552
- assert.equal(promoted.ok, true);
553
- assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(observationId).tier, "hot");
554
- const demoted = await memoryDemote.handler({ table: "observation", id: observationId, reason: "no longer active", tier: "cold" }, {});
555
- assert.equal(demoted.ok, true);
556
- assert.equal(db.prepare(`SELECT tier FROM mem_observations WHERE id = ?`).get(observationId).tier, "cold");
557
- });
558
- test("memory_remember dual-writes decisions to the wiki timeline when an entity is provided", async () => {
559
- const { toolsModule, agentsModule, dbModule } = await loadModules();
560
- const tools = toolsModule.createTools({
561
- client: { async listModels() { return []; } },
562
- onAgentTaskComplete: () => { },
563
- });
564
- const wikiFs = await import(new URL("../wiki/fs.js", import.meta.url).href);
565
- wikiFs.ensureWikiStructure();
566
- wikiFs.writePage("pages/projects/project-x/index.md", "---\ntitle: Project X\nsummary: Project X summary\nupdated: 2026-05-14\ntags: []\n---\n\n# Project X\n\n## Summary\n\nCurrent state.\n");
567
- const bindToolsToAgent = agentsModule.bindToolsToAgent;
568
- const chapterhouseTools = bindToolsToAgent("chapterhouse", tools);
569
- const memoryRemember = findTool(chapterhouseTools, "memory_remember");
570
- const result = await memoryRemember.handler({
571
- scope: "chapterhouse",
572
- kind: "decision",
573
- entity_name: "project-x",
574
- entity_kind: "project",
575
- title: "Ship append-only decisions",
576
- content: "Store the decision in SQLite and mirror it into the project timeline.",
577
- }, {});
578
- assert.equal(result.ok, true);
579
- const db = dbModule.getDb();
580
- const decision = db.prepare(`SELECT title, rationale FROM mem_decisions WHERE title = ?`).get("Ship append-only decisions");
581
- assert.deepEqual(decision, {
582
- title: "Ship append-only decisions",
583
- rationale: "Store the decision in SQLite and mirror it into the project timeline.",
584
- });
585
- const page = wikiFs.readPage("pages/projects/project-x/index.md") ?? "";
586
- assert.match(page, /## Timeline/);
587
- assert.match(page, /Ship append-only decisions/);
588
- assert.match(page, /Store the decision in SQLite and mirror it into the project timeline\./);
589
- });
590
- //# sourceMappingURL=tools.memory.test.js.map