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,949 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import test from "node:test";
5
- import { resetSingletons } from "../test/helpers/reset-singletons.js";
6
- const repoRoot = process.cwd();
7
- const sandboxRoot = join(repoRoot, ".test-work", `memory-eot-${process.pid}`);
8
- const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
9
- process.env.CHAPTERHOUSE_HOME = sandboxRoot;
10
- function resetSandbox() {
11
- mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
12
- rmSync(sandboxRoot, { recursive: true, force: true });
13
- mkdirSync(chapterhouseHome, { recursive: true });
14
- }
15
- async function loadModules(cacheBust = `${Date.now()}-${Math.random()}`) {
16
- const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
17
- const memoryModule = await import(new URL(`./index.js?case=${cacheBust}`, import.meta.url).href);
18
- const eotModule = await import(new URL(`./eot.js?case=${cacheBust}`, import.meta.url).href);
19
- const agentsModule = await import(new URL(`../copilot/agents.js?case=${cacheBust}`, import.meta.url).href);
20
- return { dbModule, memoryModule, eotModule, agentsModule };
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
- }
40
- function getFunction(module, name) {
41
- const value = module[name];
42
- assert.equal(typeof value, "function", `expected ${name} to be exported`);
43
- return value;
44
- }
45
- test.beforeEach(() => {
46
- process.env.CHAPTERHOUSE_HOME = sandboxRoot;
47
- delete process.env.CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED;
48
- delete process.env.CHAPTERHOUSE_MEMORY_AUTO_ACCEPT;
49
- resetSandbox();
50
- resetSingletons();
51
- });
52
- test.after(() => {
53
- delete process.env.CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED;
54
- delete process.env.CHAPTERHOUSE_MEMORY_AUTO_ACCEPT;
55
- resetSingletons();
56
- rmSync(sandboxRoot, { recursive: true, force: true });
57
- });
58
- test("runEndOfTaskMemoryHook does nothing when CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED=0", async () => {
59
- process.env.CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED = "0";
60
- const { dbModule, memoryModule, eotModule } = await loadModules("disabled");
61
- const db = dbModule.getDb();
62
- const getScope = getFunction(memoryModule, "getScope");
63
- const listObservations = getFunction(memoryModule, "listObservations");
64
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
65
- const chapterhouse = getScope("chapterhouse");
66
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
67
- const inserted = db.prepare(`
68
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
69
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-001', 'pending')
70
- `).run(chapterhouse.id, JSON.stringify({
71
- kind: "observation",
72
- payload: { content: "Disabled hooks must not persist memory." },
73
- confidence: 0.8,
74
- }));
75
- let llmCalls = 0;
76
- await runEndOfTaskMemoryHook({
77
- taskId: "task-eot-001",
78
- finalResult: "done",
79
- copilotClient: {},
80
- callLLM: async () => {
81
- llmCalls++;
82
- return JSON.stringify({ decisions: [] });
83
- },
84
- });
85
- assert.equal(llmCalls, 0);
86
- assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "Disabled hooks must not persist memory."), false);
87
- const row = db.prepare(`SELECT status FROM mem_inbox WHERE id = ?`).get(Number(inserted.lastInsertRowid));
88
- assert.equal(row.status, "pending");
89
- });
90
- test("runEndOfTaskMemoryHook accepts matching proposals, rejects others from the same task, and emits a structured summary", async () => {
91
- const { dbModule, memoryModule, eotModule } = await loadModules("accept");
92
- const db = dbModule.getDb();
93
- const getScope = getFunction(memoryModule, "getScope");
94
- const listObservations = getFunction(memoryModule, "listObservations");
95
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
96
- const chapterhouse = getScope("chapterhouse");
97
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
98
- const acceptedInsert = db.prepare(`
99
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
100
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-accept', 'pending')
101
- `).run(chapterhouse.id, JSON.stringify({
102
- kind: "observation",
103
- payload: { content: "End-of-task review should remember durable findings." },
104
- confidence: 0.9,
105
- }));
106
- const rejectedInsert = db.prepare(`
107
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
108
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-accept', 'pending')
109
- `).run(chapterhouse.id, JSON.stringify({
110
- kind: "observation",
111
- payload: { content: "Ephemeral shell chatter should be discarded." },
112
- confidence: 0.4,
113
- }));
114
- db.prepare(`
115
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
116
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-other', 'pending')
117
- `).run(chapterhouse.id, JSON.stringify({
118
- kind: "observation",
119
- payload: { content: "Other task proposals must stay untouched." },
120
- confidence: 0.8,
121
- }));
122
- const summaries = [];
123
- await runEndOfTaskMemoryHook({
124
- taskId: "task-eot-accept",
125
- finalResult: "Completed the feature and found one durable design note.",
126
- copilotClient: {},
127
- callLLM: async () => JSON.stringify({
128
- decisions: [
129
- {
130
- proposal_id: Number(acceptedInsert.lastInsertRowid),
131
- decision: "accept",
132
- reason: "Durable implementation guidance.",
133
- },
134
- {
135
- proposal_id: Number(rejectedInsert.lastInsertRowid),
136
- decision: "reject",
137
- reason: "Ephemeral output.",
138
- },
139
- ],
140
- implicit_memories: [],
141
- }),
142
- onProcessed: (summary) => {
143
- summaries.push(summary);
144
- },
145
- });
146
- assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "End-of-task review should remember durable findings."), true);
147
- const acceptedRow = db.prepare(`
148
- SELECT status, resolution_reason
149
- FROM mem_inbox
150
- WHERE id = ?
151
- `).get(Number(acceptedInsert.lastInsertRowid));
152
- const rejectedRow = db.prepare(`
153
- SELECT status, resolution_reason
154
- FROM mem_inbox
155
- WHERE id = ?
156
- `).get(Number(rejectedInsert.lastInsertRowid));
157
- const untouchedRow = db.prepare(`
158
- SELECT status
159
- FROM mem_inbox
160
- WHERE source_task_id = 'task-other'
161
- `).get();
162
- assert.equal(acceptedRow.status, "accepted");
163
- assert.equal(acceptedRow.resolution_reason, "Durable implementation guidance.");
164
- assert.equal(rejectedRow.status, "rejected");
165
- assert.equal(rejectedRow.resolution_reason, "Ephemeral output.");
166
- assert.equal(untouchedRow.status, "pending");
167
- assert.deepEqual(summaries, [{
168
- task_id: "task-eot-accept",
169
- proposals_total: 2,
170
- accepted: 1,
171
- rejected: 1,
172
- implicit_extracted: 0,
173
- auto_accept: true,
174
- }]);
175
- });
176
- test("runEndOfTaskMemoryHook accepts action_item proposals into mem_action_items", async () => {
177
- const { dbModule, memoryModule, eotModule } = await loadModules("action-item-accept");
178
- const db = dbModule.getDb();
179
- const getScope = getFunction(memoryModule, "getScope");
180
- const listActionItems = getFunction(memoryModule, "listActionItems");
181
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
182
- const chapterhouse = getScope("chapterhouse");
183
- assert.ok(chapterhouse);
184
- const inserted = db.prepare(`
185
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
186
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-action-item', 'pending')
187
- `).run(chapterhouse.id, JSON.stringify({
188
- kind: "action_item",
189
- payload: {
190
- title: "Migrate feature ideas",
191
- detail: "Move feature-ideas.md into mem_action_items.",
192
- },
193
- confidence: 0.9,
194
- }));
195
- await runEndOfTaskMemoryHook({
196
- taskId: "task-eot-action-item",
197
- finalResult: "Completed and proposed a follow-up action item.",
198
- copilotClient: {},
199
- callLLM: async () => JSON.stringify({
200
- decisions: [{
201
- proposal_id: Number(inserted.lastInsertRowid),
202
- decision: "accept",
203
- reason: "Concrete follow-up.",
204
- }],
205
- implicit_memories: [],
206
- }),
207
- });
208
- const actionItems = listActionItems({ scope_id: chapterhouse.id });
209
- const actionItem = actionItems.find((item) => item.title === "Migrate feature ideas");
210
- assert.ok(actionItem);
211
- assert.equal(actionItem.detail, "Move feature-ideas.md into mem_action_items.");
212
- assert.equal(actionItem.status, "open");
213
- assert.equal(actionItem.source, "subagent_proposal:coder");
214
- const inbox = db.prepare(`SELECT status FROM mem_inbox WHERE id = ?`).get(Number(inserted.lastInsertRowid));
215
- assert.equal(inbox.status, "accepted");
216
- });
217
- test("runEndOfTaskMemoryHook rejects invalid action_item proposals with a clear reason", async () => {
218
- const { dbModule, memoryModule, eotModule } = await loadModules("action-item-invalid");
219
- const db = dbModule.getDb();
220
- const getScope = getFunction(memoryModule, "getScope");
221
- const listActionItems = getFunction(memoryModule, "listActionItems");
222
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
223
- const chapterhouse = getScope("chapterhouse");
224
- assert.ok(chapterhouse);
225
- const inserted = db.prepare(`
226
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
227
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-action-item-invalid', 'pending')
228
- `).run(chapterhouse.id, JSON.stringify({
229
- kind: "action_item",
230
- payload: {
231
- detail: "A title is required before this can become an action item.",
232
- },
233
- confidence: 0.9,
234
- }));
235
- await runEndOfTaskMemoryHook({
236
- taskId: "task-eot-action-item-invalid",
237
- finalResult: "Completed and proposed a malformed follow-up.",
238
- copilotClient: {},
239
- callLLM: async () => JSON.stringify({
240
- decisions: [{
241
- proposal_id: Number(inserted.lastInsertRowid),
242
- decision: "accept",
243
- reason: "Concrete follow-up.",
244
- }],
245
- implicit_memories: [],
246
- }),
247
- });
248
- assert.deepEqual(listActionItems({ scope_id: chapterhouse.id }), []);
249
- const inbox = db.prepare(`
250
- SELECT status, resolution_reason, resolved_at
251
- FROM mem_inbox
252
- WHERE id = ?
253
- `).get(Number(inserted.lastInsertRowid));
254
- assert.equal(inbox.status, "rejected");
255
- assert.match(inbox.resolution_reason ?? "", /title/i);
256
- assert.ok(inbox.resolved_at);
257
- });
258
- async function runAcceptedObservationHookScenario(t, cacheBust, taskId, payload) {
259
- const warnings = [];
260
- t.mock.module("../util/logger.js", {
261
- namedExports: {
262
- childLogger: () => ({
263
- info: () => { },
264
- warn: (obj, msg) => warnings.push({ obj, msg }),
265
- error: () => { },
266
- }),
267
- },
268
- });
269
- const { dbModule, memoryModule, eotModule } = await loadModules(cacheBust);
270
- const db = dbModule.getDb();
271
- const getScope = getFunction(memoryModule, "getScope");
272
- const listObservations = getFunction(memoryModule, "listObservations");
273
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
274
- const chapterhouse = getScope("chapterhouse");
275
- assert.ok(chapterhouse);
276
- const before = listObservations({ scope_id: chapterhouse.id });
277
- const inserted = db.prepare(`
278
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
279
- VALUES (?, 'memory_proposal', ?, 'coder', ?, 'pending')
280
- `).run(chapterhouse.id, JSON.stringify({
281
- kind: "observation",
282
- payload,
283
- confidence: 0.9,
284
- }), taskId);
285
- await runEndOfTaskMemoryHook({
286
- taskId,
287
- finalResult: "Completed and reviewed an observation proposal.",
288
- copilotClient: {},
289
- callLLM: async () => JSON.stringify({
290
- decisions: [{
291
- proposal_id: Number(inserted.lastInsertRowid),
292
- decision: "accept",
293
- reason: "Durable finding.",
294
- }],
295
- implicit_memories: [],
296
- }),
297
- });
298
- const inbox = db.prepare(`
299
- SELECT status, resolution_reason
300
- FROM mem_inbox
301
- WHERE id = ?
302
- `).get(Number(inserted.lastInsertRowid));
303
- return {
304
- beforeCount: before.length,
305
- after: listObservations({ scope_id: chapterhouse.id }),
306
- warnings,
307
- inbox,
308
- };
309
- }
310
- test("runEndOfTaskMemoryHook skips accepted observation proposals with null content and warns", async (t) => {
311
- const result = await runAcceptedObservationHookScenario(t, "observation-null-content", "task-eot-observation-null-content", { content: null });
312
- assert.equal(result.after.length, result.beforeCount);
313
- assert.equal(result.after.some((row) => row.content.trim().length === 0), false);
314
- assert.equal(result.inbox.status, "accepted");
315
- assert.equal(result.inbox.resolution_reason, "Durable finding.");
316
- assert.ok(result.warnings.some((entry) => /empty content/i.test(entry.msg)));
317
- });
318
- test("runEndOfTaskMemoryHook skips accepted observation proposals with undefined content and warns", async (t) => {
319
- const result = await runAcceptedObservationHookScenario(t, "observation-undefined-content", "task-eot-observation-undefined-content", {});
320
- assert.equal(result.after.length, result.beforeCount);
321
- assert.equal(result.after.some((row) => row.content.trim().length === 0), false);
322
- assert.equal(result.inbox.status, "accepted");
323
- assert.equal(result.inbox.resolution_reason, "Durable finding.");
324
- assert.ok(result.warnings.some((entry) => /empty content/i.test(entry.msg)));
325
- });
326
- test("runEndOfTaskMemoryHook skips accepted observation proposals with empty string content and warns", async (t) => {
327
- const result = await runAcceptedObservationHookScenario(t, "observation-empty-string-content", "task-eot-observation-empty-string-content", { content: "" });
328
- assert.equal(result.after.length, result.beforeCount);
329
- assert.equal(result.after.some((row) => row.content.trim().length === 0), false);
330
- assert.equal(result.inbox.status, "accepted");
331
- assert.equal(result.inbox.resolution_reason, "Durable finding.");
332
- assert.ok(result.warnings.some((entry) => /empty content/i.test(entry.msg)));
333
- });
334
- test("runEndOfTaskMemoryHook skips accepted observation proposals with whitespace-only content and warns", async (t) => {
335
- const result = await runAcceptedObservationHookScenario(t, "observation-whitespace-content", "task-eot-observation-whitespace-content", { content: " " });
336
- assert.equal(result.after.length, result.beforeCount);
337
- assert.equal(result.after.some((row) => row.content.trim().length === 0), false);
338
- assert.equal(result.inbox.status, "accepted");
339
- assert.equal(result.inbox.resolution_reason, "Durable finding.");
340
- assert.ok(result.warnings.some((entry) => /empty content/i.test(entry.msg)));
341
- });
342
- test("runEndOfTaskMemoryHook inserts accepted observation proposals with valid content", async (t) => {
343
- const result = await runAcceptedObservationHookScenario(t, "observation-valid-content", "task-eot-observation-valid-content", { content: " Durable finding from the task. " });
344
- assert.equal(result.after.length, result.beforeCount + 1);
345
- assert.ok(result.after.some((row) => row.content === "Durable finding from the task."));
346
- assert.equal(result.inbox.status, "accepted");
347
- assert.equal(result.inbox.resolution_reason, "Durable finding.");
348
- assert.equal(result.warnings.length, 0);
349
- });
350
- test("runEndOfTaskMemoryHook rejects action_item proposals with ambiguous entity references", async () => {
351
- const { dbModule, memoryModule, eotModule } = await loadModules("action-item-ambiguous-entity");
352
- const db = dbModule.getDb();
353
- const getScope = getFunction(memoryModule, "getScope");
354
- const listActionItems = getFunction(memoryModule, "listActionItems");
355
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
356
- const chapterhouse = getScope("chapterhouse");
357
- assert.ok(chapterhouse);
358
- const inserted = db.prepare(`
359
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
360
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-action-item-ambiguous-entity', 'pending')
361
- `).run(chapterhouse.id, JSON.stringify({
362
- kind: "action_item",
363
- payload: {
364
- title: "Resolve ambiguous entity link",
365
- entity_id: 1,
366
- entity_name: "Bellonda",
367
- entity_kind: "agent",
368
- },
369
- confidence: 0.9,
370
- }));
371
- await runEndOfTaskMemoryHook({
372
- taskId: "task-eot-action-item-ambiguous-entity",
373
- finalResult: "Completed and proposed an ambiguous follow-up.",
374
- copilotClient: {},
375
- callLLM: async () => JSON.stringify({
376
- decisions: [{
377
- proposal_id: Number(inserted.lastInsertRowid),
378
- decision: "accept",
379
- reason: "Concrete follow-up.",
380
- }],
381
- implicit_memories: [],
382
- }),
383
- });
384
- assert.deepEqual(listActionItems({ scope_id: chapterhouse.id }), []);
385
- const inbox = db.prepare(`
386
- SELECT status, resolution_reason
387
- FROM mem_inbox
388
- WHERE id = ?
389
- `).get(Number(inserted.lastInsertRowid));
390
- assert.equal(inbox.status, "rejected");
391
- assert.match(inbox.resolution_reason ?? "", /entity_id.*entity_name|entity_name.*entity_id/i);
392
- });
393
- test("runEndOfTaskMemoryHook falls back to the source agent bound scope for action_item proposals without scope_slug", async () => {
394
- const home = process.env.CHAPTERHOUSE_HOME;
395
- assert.ok(home, "test home should be set");
396
- const { AGENTS_DIR: agentsDir } = await import("../paths.js");
397
- mkdirSync(agentsDir, { recursive: true });
398
- writeFileSync(join(agentsDir, "bellonda.agent.md"), [
399
- "---",
400
- "name: Bellonda",
401
- "description: Mentat of the infrastructure domain",
402
- "model: claude-sonnet-4.6",
403
- "persistent: true",
404
- "scope: infra",
405
- "---",
406
- "",
407
- "You are Bellonda.",
408
- ].join("\n"));
409
- const { dbModule, memoryModule, eotModule, agentsModule } = await loadModules("action-item-bound-scope");
410
- agentsModule.loadAgents();
411
- const db = dbModule.getDb();
412
- const getScope = getFunction(memoryModule, "getScope");
413
- const createScope = getFunction(memoryModule, "createScope");
414
- const listActionItems = getFunction(memoryModule, "listActionItems");
415
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
416
- const chapterhouse = getScope("chapterhouse");
417
- const infra = createScope({
418
- slug: "infra",
419
- title: "Infra",
420
- description: "Infra test scope",
421
- keywords: ["infra"],
422
- });
423
- assert.ok(chapterhouse);
424
- const inserted = db.prepare(`
425
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
426
- VALUES (?, 'memory_proposal', ?, 'bellonda', 'task-eot-action-item-bound-scope', 'pending')
427
- `).run(chapterhouse.id, JSON.stringify({
428
- kind: "action_item",
429
- payload: {
430
- title: "Review NAS disk alerts",
431
- },
432
- confidence: 0.9,
433
- }));
434
- await runEndOfTaskMemoryHook({
435
- taskId: "task-eot-action-item-bound-scope",
436
- finalResult: "Bellonda proposed an infra follow-up.",
437
- copilotClient: {},
438
- callLLM: async () => JSON.stringify({
439
- decisions: [{
440
- proposal_id: Number(inserted.lastInsertRowid),
441
- decision: "accept",
442
- reason: "Concrete infra follow-up.",
443
- }],
444
- implicit_memories: [],
445
- }),
446
- });
447
- assert.equal(listActionItems({ scope_id: infra.id }).some((item) => item.title === "Review NAS disk alerts"), true);
448
- assert.equal(listActionItems({ scope_id: chapterhouse.id }).some((item) => item.title === "Review NAS disk alerts"), false);
449
- });
450
- test("runEndOfTaskMemoryHook still accepts observation, decision, and entity proposals", async () => {
451
- const { dbModule, memoryModule, eotModule } = await loadModules("existing-proposal-kinds");
452
- const db = dbModule.getDb();
453
- const getScope = getFunction(memoryModule, "getScope");
454
- const listObservations = getFunction(memoryModule, "listObservations");
455
- const listDecisions = getFunction(memoryModule, "listDecisions");
456
- const listEntities = getFunction(memoryModule, "listEntities");
457
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
458
- const chapterhouse = getScope("chapterhouse");
459
- assert.ok(chapterhouse);
460
- const proposals = [
461
- { kind: "observation", payload: { content: "Existing observation proposals still persist." } },
462
- { kind: "decision", payload: { title: "Keep auto-accept", rationale: "It is required for EOT memory promotion." } },
463
- { kind: "entity", payload: { name: "Bellonda", entity_kind: "agent", summary: "Infrastructure mentat." } },
464
- ].map((envelope) => db.prepare(`
465
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
466
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-existing-kinds', 'pending')
467
- `).run(chapterhouse.id, JSON.stringify(envelope)));
468
- await runEndOfTaskMemoryHook({
469
- taskId: "task-eot-existing-kinds",
470
- finalResult: "Completed with several proposal kinds.",
471
- copilotClient: {},
472
- callLLM: async () => JSON.stringify({
473
- decisions: proposals.map((proposal) => ({
474
- proposal_id: Number(proposal.lastInsertRowid),
475
- decision: "accept",
476
- reason: "Valid durable memory.",
477
- })),
478
- implicit_memories: [],
479
- }),
480
- });
481
- assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "Existing observation proposals still persist."), true);
482
- assert.equal(listDecisions({ scope_id: chapterhouse.id }).some((row) => row.title === "Keep auto-accept" && row.rationale === "It is required for EOT memory promotion."), true);
483
- assert.equal(listEntities({ scope_id: chapterhouse.id, kind: "agent" }).some((row) => row.name === "Bellonda"), true);
484
- });
485
- test("runEndOfTaskMemoryHook accepts entity proposals with entity_kind into mem_entities", async () => {
486
- const { dbModule, memoryModule, eotModule } = await loadModules("entity-accept");
487
- const db = dbModule.getDb();
488
- const getScope = getFunction(memoryModule, "getScope");
489
- const listEntities = getFunction(memoryModule, "listEntities");
490
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
491
- const chapterhouse = getScope("chapterhouse");
492
- assert.ok(chapterhouse);
493
- const inserted = db.prepare(`
494
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
495
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-entity', 'pending')
496
- `).run(chapterhouse.id, JSON.stringify({
497
- kind: "entity",
498
- payload: {
499
- name: "truenas",
500
- entity_kind: "host",
501
- summary: "NAS host used by Bellonda.",
502
- },
503
- confidence: 0.9,
504
- }));
505
- await runEndOfTaskMemoryHook({
506
- taskId: "task-eot-entity",
507
- finalResult: "Completed and proposed a durable host entity.",
508
- copilotClient: {},
509
- callLLM: async () => JSON.stringify({
510
- decisions: [{
511
- proposal_id: Number(inserted.lastInsertRowid),
512
- decision: "accept",
513
- reason: "Durable entity.",
514
- }],
515
- implicit_memories: [],
516
- }),
517
- });
518
- const entities = listEntities({ scope_id: chapterhouse.id, kind: "host" });
519
- assert.equal(entities.some((entity) => entity.name === "truenas"
520
- && entity.kind === "host"
521
- && entity.summary === "NAS host used by Bellonda."), true);
522
- const inbox = db.prepare(`SELECT status FROM mem_inbox WHERE id = ?`).get(Number(inserted.lastInsertRowid));
523
- assert.equal(inbox.status, "accepted");
524
- });
525
- test("runEndOfTaskMemoryHook leaves reviewed proposals pending when CHAPTERHOUSE_MEMORY_AUTO_ACCEPT=0", async () => {
526
- process.env.CHAPTERHOUSE_MEMORY_AUTO_ACCEPT = "0";
527
- const { dbModule, memoryModule, eotModule } = await loadModules("pending");
528
- const db = dbModule.getDb();
529
- const getScope = getFunction(memoryModule, "getScope");
530
- const listObservations = getFunction(memoryModule, "listObservations");
531
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
532
- const chapterhouse = getScope("chapterhouse");
533
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
534
- const inserted = db.prepare(`
535
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
536
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-pending', 'pending')
537
- `).run(chapterhouse.id, JSON.stringify({
538
- kind: "observation",
539
- payload: { content: "Review can approve this, but auto-accept is disabled." },
540
- confidence: 0.9,
541
- }));
542
- await runEndOfTaskMemoryHook({
543
- taskId: "task-eot-pending",
544
- finalResult: "done",
545
- copilotClient: {},
546
- callLLM: async () => JSON.stringify({
547
- decisions: [{
548
- proposal_id: Number(inserted.lastInsertRowid),
549
- decision: "accept",
550
- reason: "Looks durable.",
551
- }],
552
- implicit_memories: [],
553
- }),
554
- });
555
- assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "Review can approve this, but auto-accept is disabled."), false);
556
- const row = db.prepare(`SELECT status FROM mem_inbox WHERE id = ?`).get(Number(inserted.lastInsertRowid));
557
- assert.equal(row.status, "pending");
558
- });
559
- test("runEndOfTaskMemoryHook rejects pending same-task proposals omitted by the reviewer", async () => {
560
- const { dbModule, memoryModule, eotModule } = await loadModules("omitted");
561
- const db = dbModule.getDb();
562
- const getScope = getFunction(memoryModule, "getScope");
563
- const listObservations = getFunction(memoryModule, "listObservations");
564
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
565
- const chapterhouse = getScope("chapterhouse");
566
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
567
- const inserted = db.prepare(`
568
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
569
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-omitted', 'pending')
570
- `).run(chapterhouse.id, JSON.stringify({
571
- kind: "observation",
572
- payload: { content: "Reviewer omissions should not silently leave proposals pending." },
573
- confidence: 0.6,
574
- }));
575
- await runEndOfTaskMemoryHook({
576
- taskId: "task-eot-omitted",
577
- finalResult: "done",
578
- copilotClient: {},
579
- callLLM: async () => JSON.stringify({
580
- decisions: [],
581
- implicit_memories: [],
582
- }),
583
- });
584
- assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "Reviewer omissions should not silently leave proposals pending."), false);
585
- const row = db.prepare(`
586
- SELECT status, resolution_reason
587
- FROM mem_inbox
588
- WHERE id = ?
589
- `).get(Number(inserted.lastInsertRowid));
590
- assert.equal(row.status, "rejected");
591
- assert.match(row.resolution_reason ?? "", /did not select/i);
592
- });
593
- test("runEndOfTaskMemoryHook can persist implicit extracted memories that were not explicitly proposed", async () => {
594
- const { memoryModule, eotModule } = await loadModules("implicit");
595
- const getScope = getFunction(memoryModule, "getScope");
596
- const listObservations = getFunction(memoryModule, "listObservations");
597
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
598
- const chapterhouse = getScope("chapterhouse");
599
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
600
- const summaries = [];
601
- await runEndOfTaskMemoryHook({
602
- taskId: "task-eot-implicit",
603
- finalResult: "The subagent discovered a durable constraint while finishing the task.",
604
- copilotClient: {},
605
- callLLM: async () => JSON.stringify({
606
- decisions: [],
607
- implicit_memories: [{
608
- kind: "observation",
609
- scope_slug: "chapterhouse",
610
- payload: {
611
- content: "The end-of-task reviewer can persist implicit durable findings from a subagent summary.",
612
- source: "implicit-extraction",
613
- },
614
- }],
615
- }),
616
- onProcessed: (summary) => {
617
- summaries.push(summary);
618
- },
619
- });
620
- assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "The end-of-task reviewer can persist implicit durable findings from a subagent summary."), true);
621
- assert.deepEqual(summaries, [{
622
- task_id: "task-eot-implicit",
623
- proposals_total: 0,
624
- accepted: 0,
625
- rejected: 0,
626
- implicit_extracted: 1,
627
- auto_accept: true,
628
- }]);
629
- });
630
- test("runEndOfTaskMemoryHook accepts implicit entity memories with entity_kind", async () => {
631
- const { memoryModule, eotModule } = await loadModules("implicit-entity");
632
- const getScope = getFunction(memoryModule, "getScope");
633
- const listEntities = getFunction(memoryModule, "listEntities");
634
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
635
- const chapterhouse = getScope("chapterhouse");
636
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
637
- await runEndOfTaskMemoryHook({
638
- taskId: "task-eot-implicit-entity",
639
- finalResult: "The subagent discovered a durable host entity while finishing the task.",
640
- copilotClient: {},
641
- callLLM: async () => JSON.stringify({
642
- decisions: [],
643
- implicit_memories: [{
644
- kind: "entity",
645
- scope_slug: "chapterhouse",
646
- payload: {
647
- name: "synology",
648
- entity_kind: "host",
649
- summary: "NAS host used by Bellonda.",
650
- },
651
- }],
652
- }),
653
- });
654
- const entities = listEntities({ scope_id: chapterhouse.id, kind: "host" });
655
- assert.equal(entities.some((entity) => entity.name === "synology"
656
- && entity.kind === "host"
657
- && entity.summary === "NAS host used by Bellonda."), true);
658
- });
659
- test("runEndOfTaskMemoryHook skips implicit observation memories with null content and warns", async (t) => {
660
- const { memoryModule, eotModule, warnings } = await loadModulesWithWarnSpy(t, "implicit-null-content");
661
- const getScope = getFunction(memoryModule, "getScope");
662
- const listObservations = getFunction(memoryModule, "listObservations");
663
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
664
- const chapterhouse = getScope("chapterhouse");
665
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
666
- const initialCount = listObservations({ scope_id: chapterhouse.id }).length;
667
- const summary = await runEndOfTaskMemoryHook({
668
- taskId: "task-eot-implicit-null",
669
- finalResult: "The reviewer attempted to persist an invalid implicit memory.",
670
- copilotClient: {},
671
- callLLM: async () => JSON.stringify({
672
- decisions: [],
673
- implicit_memories: [{
674
- kind: "observation",
675
- scope_slug: "chapterhouse",
676
- payload: {
677
- content: null,
678
- },
679
- }],
680
- }),
681
- });
682
- assert.equal(listObservations({ scope_id: chapterhouse.id }).length, initialCount);
683
- assert.equal(summary.implicit_extracted, 0);
684
- assert.equal(warnings.length, 1);
685
- });
686
- test("runEndOfTaskMemoryHook skips implicit observation memories with undefined content and warns", async (t) => {
687
- const { memoryModule, eotModule, warnings } = await loadModulesWithWarnSpy(t, "implicit-undefined-content");
688
- const getScope = getFunction(memoryModule, "getScope");
689
- const listObservations = getFunction(memoryModule, "listObservations");
690
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
691
- const chapterhouse = getScope("chapterhouse");
692
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
693
- const initialCount = listObservations({ scope_id: chapterhouse.id }).length;
694
- const summary = await runEndOfTaskMemoryHook({
695
- taskId: "task-eot-implicit-undefined",
696
- finalResult: "The reviewer attempted to persist an invalid implicit memory.",
697
- copilotClient: {},
698
- callLLM: async () => JSON.stringify({
699
- decisions: [],
700
- implicit_memories: [{
701
- kind: "observation",
702
- scope_slug: "chapterhouse",
703
- payload: {},
704
- }],
705
- }),
706
- });
707
- assert.equal(listObservations({ scope_id: chapterhouse.id }).length, initialCount);
708
- assert.equal(summary.implicit_extracted, 0);
709
- assert.equal(warnings.length, 1);
710
- });
711
- test("runEndOfTaskMemoryHook skips implicit observation memories with empty content and warns", async (t) => {
712
- const { memoryModule, eotModule, warnings } = await loadModulesWithWarnSpy(t, "implicit-empty-content");
713
- const getScope = getFunction(memoryModule, "getScope");
714
- const listObservations = getFunction(memoryModule, "listObservations");
715
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
716
- const chapterhouse = getScope("chapterhouse");
717
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
718
- const initialCount = listObservations({ scope_id: chapterhouse.id }).length;
719
- const summary = await runEndOfTaskMemoryHook({
720
- taskId: "task-eot-implicit-empty",
721
- finalResult: "The reviewer attempted to persist an invalid implicit memory.",
722
- copilotClient: {},
723
- callLLM: async () => JSON.stringify({
724
- decisions: [],
725
- implicit_memories: [{
726
- kind: "observation",
727
- scope_slug: "chapterhouse",
728
- payload: {
729
- content: " ",
730
- },
731
- }],
732
- }),
733
- });
734
- assert.equal(listObservations({ scope_id: chapterhouse.id }).length, initialCount);
735
- assert.equal(summary.implicit_extracted, 0);
736
- assert.equal(warnings.length, 1);
737
- });
738
- test("runEndOfTaskMemoryHook persists implicit observation memories with valid content", async (t) => {
739
- const { memoryModule, eotModule, warnings } = await loadModulesWithWarnSpy(t, "implicit-valid-content");
740
- const getScope = getFunction(memoryModule, "getScope");
741
- const listObservations = getFunction(memoryModule, "listObservations");
742
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
743
- const chapterhouse = getScope("chapterhouse");
744
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
745
- const summary = await runEndOfTaskMemoryHook({
746
- taskId: "task-eot-implicit-valid",
747
- finalResult: "The reviewer discovered a valid durable memory.",
748
- copilotClient: {},
749
- callLLM: async () => JSON.stringify({
750
- decisions: [],
751
- implicit_memories: [{
752
- kind: "observation",
753
- scope_slug: "chapterhouse",
754
- payload: {
755
- content: "A valid implicit memory should still be stored.",
756
- },
757
- }],
758
- }),
759
- });
760
- assert.equal(listObservations({ scope_id: chapterhouse.id }).some((row) => row.content === "A valid implicit memory should still be stored."), true);
761
- assert.equal(summary.implicit_extracted, 1);
762
- assert.equal(warnings.length, 0);
763
- });
764
- test("runEndOfTaskMemoryHook treats malformed reviewer JSON as an empty review and warns", async (t) => {
765
- const { dbModule, memoryModule, eotModule, warnings } = await loadModulesWithWarnSpy(t, "reviewer-malformed-json");
766
- const db = dbModule.getDb();
767
- const getScope = getFunction(memoryModule, "getScope");
768
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
769
- const chapterhouse = getScope("chapterhouse");
770
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
771
- const inserted = db.prepare(`
772
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
773
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-reviewer-malformed-json', 'pending')
774
- `).run(chapterhouse.id, JSON.stringify({
775
- kind: "observation",
776
- payload: { content: "Malformed reviewer JSON must not crash the hook." },
777
- confidence: 0.9,
778
- }));
779
- const summary = await runEndOfTaskMemoryHook({
780
- taskId: "task-eot-reviewer-malformed-json",
781
- finalResult: "The reviewer returned malformed JSON, so the hook should fail closed and continue.",
782
- copilotClient: {},
783
- callLLM: async () => "{ definitely-not-json",
784
- });
785
- const inbox = db.prepare(`
786
- SELECT status, resolution_reason
787
- FROM mem_inbox
788
- WHERE id = ?
789
- `).get(Number(inserted.lastInsertRowid));
790
- assert.equal(summary.accepted, 0);
791
- assert.equal(summary.rejected, 1);
792
- assert.equal(inbox.status, "rejected");
793
- assert.match(inbox.resolution_reason ?? "", /reviewer did not select/i);
794
- assert.equal(warnings.length, 1);
795
- });
796
- test("runEndOfTaskMemoryHook rejects malformed accepted proposal payload JSON and warns", async (t) => {
797
- const { dbModule, memoryModule, eotModule, warnings } = await loadModulesWithWarnSpy(t, "proposal-malformed-json");
798
- const db = dbModule.getDb();
799
- const getScope = getFunction(memoryModule, "getScope");
800
- const listObservations = getFunction(memoryModule, "listObservations");
801
- const runEndOfTaskMemoryHook = getFunction(eotModule, "runEndOfTaskMemoryHook");
802
- const chapterhouse = getScope("chapterhouse");
803
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
804
- const beforeCount = listObservations({ scope_id: chapterhouse.id }).length;
805
- const inserted = db.prepare(`
806
- INSERT INTO mem_inbox (scope_id, kind, payload, source_agent, source_task_id, status)
807
- VALUES (?, 'memory_proposal', ?, 'coder', 'task-eot-proposal-malformed-json', 'pending')
808
- `).run(chapterhouse.id, "{ definitely-not-json");
809
- const summary = await runEndOfTaskMemoryHook({
810
- taskId: "task-eot-proposal-malformed-json",
811
- finalResult: "The accepted proposal payload is malformed JSON, so the hook should reject it safely.",
812
- copilotClient: {},
813
- callLLM: async () => JSON.stringify({
814
- decisions: [{
815
- proposal_id: Number(inserted.lastInsertRowid),
816
- decision: "accept",
817
- reason: "Looks durable.",
818
- }],
819
- implicit_memories: [],
820
- }),
821
- });
822
- const inbox = db.prepare(`
823
- SELECT status, resolution_reason
824
- FROM mem_inbox
825
- WHERE id = ?
826
- `).get(Number(inserted.lastInsertRowid));
827
- assert.equal(listObservations({ scope_id: chapterhouse.id }).length, beforeCount);
828
- assert.equal(summary.accepted, 0);
829
- assert.equal(summary.rejected, 1);
830
- assert.equal(inbox.status, "rejected");
831
- assert.match(inbox.resolution_reason ?? "", /malformed/i);
832
- assert.equal(warnings.length, 1);
833
- });
834
- test("runFrictionHook does nothing by default when CHAPTERHOUSE_MEMORY_EOT_FRICTION_ENABLED is unset", async () => {
835
- const { eotModule } = await loadModules("friction-disabled-default");
836
- const runFrictionHook = getFunction(eotModule, "runFrictionHook");
837
- let llmCalls = 0;
838
- await runFrictionHook({
839
- taskId: "task-friction-disabled-default",
840
- finalResult: "A substantive final result that is definitely longer than one hundred characters to prove the friction hook still stays off by default.",
841
- copilotClient: {},
842
- callLLM: async () => {
843
- llmCalls++;
844
- return "[]";
845
- },
846
- });
847
- assert.equal(llmCalls, 0);
848
- });
849
- test("runFrictionHook skips short final results even when enabled", async () => {
850
- process.env.CHAPTERHOUSE_MEMORY_EOT_FRICTION_ENABLED = "1";
851
- const { eotModule } = await loadModules("friction-short-result");
852
- const runFrictionHook = getFunction(eotModule, "runFrictionHook");
853
- let llmCalls = 0;
854
- await runFrictionHook({
855
- taskId: "task-friction-short-result",
856
- finalResult: "too short",
857
- copilotClient: {},
858
- callLLM: async () => {
859
- llmCalls++;
860
- return "[]";
861
- },
862
- });
863
- assert.equal(llmCalls, 0);
864
- });
865
- test("runFrictionHook records action items when enabled and the task result is substantive", async () => {
866
- process.env.CHAPTERHOUSE_MEMORY_EOT_FRICTION_ENABLED = "1";
867
- const { memoryModule, eotModule } = await loadModules("friction-records-action-items");
868
- const getScope = getFunction(memoryModule, "getScope");
869
- const setActiveScope = getFunction(memoryModule, "setActiveScope");
870
- const listActionItems = getFunction(memoryModule, "listActionItems");
871
- const runFrictionHook = getFunction(eotModule, "runFrictionHook");
872
- const chapterhouse = getScope("chapterhouse");
873
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
874
- setActiveScope("chapterhouse");
875
- await runFrictionHook({
876
- taskId: "task-friction-records-action-items",
877
- finalResult: "The agent had to retry the same command several times because the tool returned a generic error with no validation detail, which made the task materially slower and harder to finish cleanly.",
878
- copilotClient: {},
879
- callLLM: async () => JSON.stringify([
880
- {
881
- title: "Improve validation feedback for memory tools",
882
- detail: "Return the rejected field name and allowed values instead of a generic failure.",
883
- source: "eot:friction",
884
- },
885
- ]),
886
- });
887
- const actionItems = listActionItems({ scope_id: chapterhouse.id });
888
- assert.equal(actionItems.length, 1);
889
- assert.equal(actionItems[0]?.title, "Improve validation feedback for memory tools");
890
- assert.equal(actionItems[0]?.detail, "Return the rejected field name and allowed values instead of a generic failure.");
891
- assert.equal(actionItems[0]?.source, "eot:friction");
892
- assert.equal(actionItems[0]?.status, "open");
893
- });
894
- test("runFrictionHook caps parsed friction items at 3", async () => {
895
- process.env.CHAPTERHOUSE_MEMORY_EOT_FRICTION_ENABLED = "1";
896
- const { memoryModule, eotModule } = await loadModules("friction-cap");
897
- const getScope = getFunction(memoryModule, "getScope");
898
- const setActiveScope = getFunction(memoryModule, "setActiveScope");
899
- const listActionItems = getFunction(memoryModule, "listActionItems");
900
- const runFrictionHook = getFunction(eotModule, "runFrictionHook");
901
- const chapterhouse = getScope("chapterhouse");
902
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
903
- setActiveScope("chapterhouse");
904
- await runFrictionHook({
905
- taskId: "task-friction-cap",
906
- finalResult: "The toolchain created several distinct sources of friction across the task, and the result is long enough that the friction hook should inspect it and write only the first three issues back into memory.",
907
- copilotClient: {},
908
- callLLM: async () => JSON.stringify([
909
- { title: "Item 1", detail: "detail 1", source: "eot:friction" },
910
- { title: "Item 2", detail: "detail 2", source: "eot:friction" },
911
- { title: "Item 3", detail: "detail 3", source: "eot:friction" },
912
- { title: "Item 4", detail: "detail 4", source: "eot:friction" },
913
- ]),
914
- });
915
- assert.deepEqual(listActionItems({ scope_id: chapterhouse.id }).map((item) => item.title).sort(), ["Item 1", "Item 2", "Item 3"]);
916
- });
917
- test("runFrictionHook treats malformed JSON as no friction items", async () => {
918
- process.env.CHAPTERHOUSE_MEMORY_EOT_FRICTION_ENABLED = "1";
919
- const { memoryModule, eotModule } = await loadModules("friction-malformed-json");
920
- const getScope = getFunction(memoryModule, "getScope");
921
- const setActiveScope = getFunction(memoryModule, "setActiveScope");
922
- const listActionItems = getFunction(memoryModule, "listActionItems");
923
- const runFrictionHook = getFunction(eotModule, "runFrictionHook");
924
- const chapterhouse = getScope("chapterhouse");
925
- assert.ok(chapterhouse, "chapterhouse scope should be seeded");
926
- setActiveScope("chapterhouse");
927
- await runFrictionHook({
928
- taskId: "task-friction-malformed-json",
929
- finalResult: "The agent hit confusing tool friction repeatedly, but the friction reviewer returned malformed JSON and the hook should safely ignore it without writing any action items.",
930
- copilotClient: {},
931
- callLLM: async () => "{not valid json",
932
- });
933
- assert.deepEqual(listActionItems({ scope_id: chapterhouse.id }), []);
934
- });
935
- test("runFrictionHook never propagates errors from the LLM call", async (t) => {
936
- process.env.CHAPTERHOUSE_MEMORY_EOT_FRICTION_ENABLED = "1";
937
- const { eotModule, warnings } = await loadModulesWithWarnSpy(t, "friction-no-throw");
938
- const runFrictionHook = getFunction(eotModule, "runFrictionHook");
939
- await assert.doesNotReject(() => runFrictionHook({
940
- taskId: "task-friction-no-throw",
941
- finalResult: "The agent encountered enough friction to trigger the hook, but the LLM call itself crashed and the hook must still fail closed without breaking end-of-task processing.",
942
- copilotClient: {},
943
- callLLM: async () => {
944
- throw new Error("boom");
945
- },
946
- }));
947
- assert.equal(warnings.length, 1);
948
- });
949
- //# sourceMappingURL=eot.test.js.map