create-walle 0.1.0

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 (136) hide show
  1. package/bin/create-walle.js +134 -0
  2. package/package.json +18 -0
  3. package/template/.env.example +40 -0
  4. package/template/CLAUDE.md +12 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +167 -0
  7. package/template/bin/setup.js +100 -0
  8. package/template/claude-code-skill.md +60 -0
  9. package/template/claude-task-manager/api-prompts.js +1841 -0
  10. package/template/claude-task-manager/api-reviews.js +275 -0
  11. package/template/claude-task-manager/approval-agent.js +454 -0
  12. package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
  13. package/template/claude-task-manager/db.js +1721 -0
  14. package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
  15. package/template/claude-task-manager/git-utils.js +214 -0
  16. package/template/claude-task-manager/package-lock.json +1607 -0
  17. package/template/claude-task-manager/package.json +31 -0
  18. package/template/claude-task-manager/prompt-harvest.js +1148 -0
  19. package/template/claude-task-manager/public/css/prompts.css +880 -0
  20. package/template/claude-task-manager/public/css/reviews.css +430 -0
  21. package/template/claude-task-manager/public/css/walle.css +732 -0
  22. package/template/claude-task-manager/public/favicon.ico +0 -0
  23. package/template/claude-task-manager/public/icon.svg +37 -0
  24. package/template/claude-task-manager/public/index.html +8346 -0
  25. package/template/claude-task-manager/public/js/prompts.js +3159 -0
  26. package/template/claude-task-manager/public/js/reviews.js +1292 -0
  27. package/template/claude-task-manager/public/js/walle.js +3081 -0
  28. package/template/claude-task-manager/public/manifest.json +13 -0
  29. package/template/claude-task-manager/public/prompts.html +4353 -0
  30. package/template/claude-task-manager/public/setup.html +216 -0
  31. package/template/claude-task-manager/queue-engine.js +404 -0
  32. package/template/claude-task-manager/server-state.js +5 -0
  33. package/template/claude-task-manager/server.js +2254 -0
  34. package/template/claude-task-manager/session-utils.js +124 -0
  35. package/template/claude-task-manager/start.sh +17 -0
  36. package/template/claude-task-manager/tests/test-ai-search.js +61 -0
  37. package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
  38. package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
  39. package/template/claude-task-manager/tests/test-features-v2.js +127 -0
  40. package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
  41. package/template/claude-task-manager/tests/test-insights.js +124 -0
  42. package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
  43. package/template/claude-task-manager/tests/test-permissions.js +122 -0
  44. package/template/claude-task-manager/tests/test-pin.js +51 -0
  45. package/template/claude-task-manager/tests/test-prompts.js +164 -0
  46. package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
  47. package/template/claude-task-manager/tests/test-review.js +104 -0
  48. package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
  49. package/template/claude-task-manager/tests/test-send-final.js +30 -0
  50. package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
  51. package/template/claude-task-manager/tests/test-send-integration.js +107 -0
  52. package/template/claude-task-manager/tests/test-send-visual.js +34 -0
  53. package/template/claude-task-manager/tests/test-session-create.js +147 -0
  54. package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
  55. package/template/claude-task-manager/tests/test-url-hash.js +68 -0
  56. package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
  57. package/template/claude-task-manager/tests/test-ux-review.js +130 -0
  58. package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
  59. package/template/claude-task-manager/tests/test-zoom.js +92 -0
  60. package/template/claude-task-manager/tests/test-zoom2.js +67 -0
  61. package/template/docs/site/api/README.md +187 -0
  62. package/template/docs/site/guides/claude-code.md +58 -0
  63. package/template/docs/site/guides/configuration.md +96 -0
  64. package/template/docs/site/guides/quickstart.md +158 -0
  65. package/template/docs/site/index.md +14 -0
  66. package/template/docs/site/skills/README.md +135 -0
  67. package/template/wall-e/.dockerignore +11 -0
  68. package/template/wall-e/Dockerfile +25 -0
  69. package/template/wall-e/adapters/adapter-base.js +37 -0
  70. package/template/wall-e/adapters/ctm.js +193 -0
  71. package/template/wall-e/adapters/slack.js +56 -0
  72. package/template/wall-e/agent.js +319 -0
  73. package/template/wall-e/api-walle.js +1073 -0
  74. package/template/wall-e/brain.js +1235 -0
  75. package/template/wall-e/channels/agent-api.js +172 -0
  76. package/template/wall-e/channels/channel-base.js +14 -0
  77. package/template/wall-e/channels/imessage-channel.js +113 -0
  78. package/template/wall-e/channels/slack-channel.js +118 -0
  79. package/template/wall-e/chat.js +778 -0
  80. package/template/wall-e/decision/confidence.js +93 -0
  81. package/template/wall-e/deploy.sh +35 -0
  82. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
  83. package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
  84. package/template/wall-e/extraction/contradiction.js +168 -0
  85. package/template/wall-e/extraction/knowledge-extractor.js +190 -0
  86. package/template/wall-e/fly.toml +24 -0
  87. package/template/wall-e/loops/ingest.js +34 -0
  88. package/template/wall-e/loops/reflect.js +63 -0
  89. package/template/wall-e/loops/tasks.js +487 -0
  90. package/template/wall-e/loops/think.js +125 -0
  91. package/template/wall-e/package-lock.json +533 -0
  92. package/template/wall-e/package.json +18 -0
  93. package/template/wall-e/scripts/ingest-slack-search.js +85 -0
  94. package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
  95. package/template/wall-e/scripts/slack-backfill.js +295 -0
  96. package/template/wall-e/scripts/slack-channel-history.js +454 -0
  97. package/template/wall-e/server.js +93 -0
  98. package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
  99. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
  100. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
  101. package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
  102. package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
  103. package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
  104. package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
  105. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
  106. package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
  107. package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
  108. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
  109. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
  110. package/template/wall-e/skills/claude-code-reader.js +144 -0
  111. package/template/wall-e/skills/mcp-client.js +407 -0
  112. package/template/wall-e/skills/skill-executor.js +163 -0
  113. package/template/wall-e/skills/skill-loader.js +410 -0
  114. package/template/wall-e/skills/skill-planner.js +88 -0
  115. package/template/wall-e/skills/slack-ingest.js +329 -0
  116. package/template/wall-e/skills/slack-pull-live.js +270 -0
  117. package/template/wall-e/skills/tool-executor.js +188 -0
  118. package/template/wall-e/tests/adapter-base.test.js +20 -0
  119. package/template/wall-e/tests/adapter-ctm.test.js +122 -0
  120. package/template/wall-e/tests/adapter-slack.test.js +98 -0
  121. package/template/wall-e/tests/agent-api.test.js +256 -0
  122. package/template/wall-e/tests/api-walle.test.js +222 -0
  123. package/template/wall-e/tests/brain.test.js +602 -0
  124. package/template/wall-e/tests/channels.test.js +104 -0
  125. package/template/wall-e/tests/chat.test.js +103 -0
  126. package/template/wall-e/tests/confidence.test.js +134 -0
  127. package/template/wall-e/tests/contradiction.test.js +217 -0
  128. package/template/wall-e/tests/ingest.test.js +113 -0
  129. package/template/wall-e/tests/mcp-client.test.js +71 -0
  130. package/template/wall-e/tests/reflect.test.js +103 -0
  131. package/template/wall-e/tests/server.test.js +111 -0
  132. package/template/wall-e/tests/skills.test.js +198 -0
  133. package/template/wall-e/tests/slack-ingest.test.js +103 -0
  134. package/template/wall-e/tests/think.test.js +435 -0
  135. package/template/wall-e/tools/local-tools.js +697 -0
  136. package/template/wall-e/tools/slack-mcp.js +290 -0
@@ -0,0 +1,435 @@
1
+ const { describe, it } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+
4
+ const { buildExtractionPrompt, parseExtractionResponse } = require('../extraction/knowledge-extractor.js');
5
+
6
+ // ── Task 9: Knowledge Extractor ─────────────────────────────────────────
7
+
8
+ describe('buildExtractionPrompt', () => {
9
+ const memories = [
10
+ { id: 'm1', source: 'slack', timestamp: '2026-03-22T10:00:00Z', content: 'I prefer SQLite for small projects' },
11
+ { id: 'm2', source: 'email', timestamp: '2026-03-22T11:00:00Z', content: 'Alice is my colleague on the infra team' },
12
+ ];
13
+
14
+ it('includes owner name in prompt', () => {
15
+ const prompt = buildExtractionPrompt(memories, 'Test Owner');
16
+ assert.ok(prompt.includes('Test Owner'), 'prompt should include owner name');
17
+ });
18
+
19
+ it('includes memory content', () => {
20
+ const prompt = buildExtractionPrompt(memories, 'Test Owner');
21
+ assert.ok(prompt.includes('I prefer SQLite for small projects'), 'prompt should include memory content');
22
+ assert.ok(prompt.includes('Alice is my colleague on the infra team'), 'prompt should include all memory content');
23
+ });
24
+
25
+ it('includes memory IDs', () => {
26
+ const prompt = buildExtractionPrompt(memories, 'Test Owner');
27
+ assert.ok(prompt.includes('m1'), 'prompt should include memory id m1');
28
+ assert.ok(prompt.includes('m2'), 'prompt should include memory id m2');
29
+ });
30
+ });
31
+
32
+ describe('parseExtractionResponse', () => {
33
+ const validResponse = JSON.stringify([
34
+ { category: 'technical', subject: 'Test Owner', predicate: 'prefers', object: 'SQLite', confidence: 0.85, source_memory_ids: ['m1'] },
35
+ { category: 'relationship', subject: 'Alice', predicate: 'is', object: 'colleague', confidence: 0.6, source_memory_ids: ['m2'] },
36
+ ]);
37
+
38
+ it('parses valid JSON array and returns knowledge entries', () => {
39
+ const result = parseExtractionResponse(validResponse);
40
+ assert.equal(result.length, 2);
41
+ assert.equal(result[0].category, 'technical');
42
+ assert.equal(result[0].subject, 'Test Owner');
43
+ assert.equal(result[0].predicate, 'prefers');
44
+ assert.equal(result[0].object, 'SQLite');
45
+ assert.equal(result[0].confidence, 0.85);
46
+ assert.deepEqual(result[0].source_memory_ids, ['m1']);
47
+ assert.equal(result[1].category, 'relationship');
48
+ });
49
+
50
+ it('returns empty array for invalid JSON', () => {
51
+ const result = parseExtractionResponse('not valid json');
52
+ assert.deepEqual(result, []);
53
+ });
54
+
55
+ it('handles JSON wrapped in markdown code fences', () => {
56
+ const wrapped = '```json\n' + validResponse + '\n```';
57
+ const result = parseExtractionResponse(wrapped);
58
+ assert.equal(result.length, 2);
59
+ assert.equal(result[0].category, 'technical');
60
+ assert.equal(result[1].subject, 'Alice');
61
+ });
62
+
63
+ it('filters entries missing required fields', () => {
64
+ const partial = JSON.stringify([
65
+ { category: 'technical', subject: 'Test Owner', predicate: 'prefers', object: 'SQLite', confidence: 0.85, source_memory_ids: ['m1'] },
66
+ { category: 'technical', subject: 'Test Owner' }, // missing predicate and object
67
+ ]);
68
+ const result = parseExtractionResponse(partial);
69
+ assert.equal(result.length, 1);
70
+ assert.equal(result[0].object, 'SQLite');
71
+ });
72
+ });
73
+
74
+ // ── Task 10: Think Loop ─────────────────────────────────────────────────
75
+
76
+ const { before, after } = require('node:test');
77
+ const path = require('path');
78
+ const fs = require('fs');
79
+ const os = require('os');
80
+ const brain = require('../brain');
81
+ const think = require('../loops/think');
82
+
83
+ describe('think loop', () => {
84
+ const ownerName = 'TestOwner';
85
+ let tmpDir;
86
+
87
+ const mockExtract = async (memories, ownerName) => [
88
+ {
89
+ category: 'technical',
90
+ subject: ownerName,
91
+ predicate: 'prefers',
92
+ object: 'early returns over nested conditionals',
93
+ confidence: 0.85,
94
+ source_memory_ids: memories.map(m => m.id),
95
+ },
96
+ ];
97
+
98
+ before(() => {
99
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'walle-think-test-'));
100
+ brain.initDb(path.join(tmpDir, 'test.db'));
101
+ brain.setOwner('name', ownerName);
102
+ });
103
+
104
+ after(() => {
105
+ brain.closeDb();
106
+ fs.rmSync(tmpDir, { recursive: true, force: true });
107
+ });
108
+
109
+ it('processes pending memories and returns correct counts', async () => {
110
+ brain.insertMemory({
111
+ source: 'test', memory_type: 'observation',
112
+ content: 'I always use early returns', timestamp: '2026-03-22T10:00:00Z',
113
+ });
114
+ brain.insertMemory({
115
+ source: 'test', memory_type: 'observation',
116
+ content: 'Nested conditionals are hard to read', timestamp: '2026-03-22T10:01:00Z',
117
+ });
118
+
119
+ const result = await think.runOnce({ extractFn: mockExtract });
120
+ assert.equal(result.memoriesProcessed, 2);
121
+ assert.equal(result.knowledgeExtracted, 1);
122
+ });
123
+
124
+ it('stores extracted knowledge in the brain', () => {
125
+ const entries = brain.findKnowledge({ subject: ownerName });
126
+ assert.ok(entries.length > 0, 'should have knowledge entries for owner');
127
+ const match = entries.find(e => e.predicate === 'prefers' && e.object === 'early returns over nested conditionals');
128
+ assert.ok(match, 'should find the extracted knowledge entry');
129
+ });
130
+
131
+ it('marks processed memories as done', () => {
132
+ const done = brain.listMemories({ extractionStatus: 'done' });
133
+ assert.equal(done.length, 2, 'both memories should be marked done');
134
+ });
135
+
136
+ it('marks memories as failed when extractFn throws', async () => {
137
+ brain.insertMemory({
138
+ source: 'test', memory_type: 'observation',
139
+ content: 'This will fail extraction', timestamp: '2026-03-22T11:00:00Z',
140
+ });
141
+
142
+ const result = await think.runOnce({
143
+ extractFn: () => { throw new Error('API down'); },
144
+ });
145
+
146
+ assert.equal(result.memoriesProcessed, 1);
147
+ assert.equal(result.knowledgeExtracted, 0);
148
+
149
+ const failed = brain.listMemories({ extractionStatus: 'failed' });
150
+ assert.equal(failed.length, 1, 'the memory should be marked failed');
151
+ });
152
+
153
+ it('returns zeros when no pending memories exist', async () => {
154
+ const result = await think.runOnce({ extractFn: mockExtract });
155
+ assert.equal(result.memoriesProcessed, 0);
156
+ assert.equal(result.knowledgeExtracted, 0);
157
+ });
158
+ });
159
+
160
+ // ── Task 3: Contradiction Integration ────────────────────────────────────
161
+
162
+ describe('think loop - contradictions', () => {
163
+ const ownerName = 'ContraOwner';
164
+ let tmpDir;
165
+
166
+ before(() => {
167
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'walle-contra-test-'));
168
+ brain.initDb(path.join(tmpDir, 'test.db'));
169
+ brain.setOwner('name', ownerName);
170
+ });
171
+
172
+ after(() => {
173
+ brain.closeDb();
174
+ fs.rmSync(tmpDir, { recursive: true, force: true });
175
+ });
176
+
177
+ it('creates pending question and supersedes old knowledge when contradiction detected', async () => {
178
+ // Insert existing knowledge that will be contradicted
179
+ const oldK = brain.insertKnowledge({
180
+ category: 'preference',
181
+ subject: ownerName,
182
+ predicate: 'prefers_db',
183
+ object: 'MySQL',
184
+ confidence: 0.8,
185
+ });
186
+
187
+ // Insert a pending memory
188
+ brain.insertMemory({
189
+ source: 'test', memory_type: 'observation',
190
+ content: 'Actually I now prefer SQLite', timestamp: '2026-03-22T12:00:00Z',
191
+ });
192
+
193
+ const mockExtract = async (memories) => [{
194
+ category: 'preference',
195
+ subject: ownerName,
196
+ predicate: 'prefers_db',
197
+ object: 'SQLite',
198
+ confidence: 0.9,
199
+ source_memory_ids: memories.map(m => m.id),
200
+ }];
201
+
202
+ const mockDetectFn = (existing, entries) => {
203
+ // Find the MySQL entry in existing knowledge
204
+ const mysqlEntry = existing.find(e => e.object === 'MySQL');
205
+ if (!mysqlEntry) return [];
206
+ const newEntry = entries.find(e => e.object === 'SQLite');
207
+ if (!newEntry) return [];
208
+ return [{
209
+ old_id: mysqlEntry.id,
210
+ new_subject: newEntry.subject,
211
+ new_predicate: newEntry.predicate,
212
+ new_object: newEntry.object,
213
+ explanation: 'Changed database preference from MySQL to SQLite',
214
+ }];
215
+ };
216
+
217
+ await think.runOnce({ extractFn: mockExtract, detectFn: mockDetectFn });
218
+
219
+ // Verify a pending question was created
220
+ const questions = brain.listQuestions({ question_type: 'contradiction' });
221
+ assert.ok(questions.length > 0, 'should have created a contradiction question');
222
+ assert.equal(questions[0].question_type, 'contradiction');
223
+ assert.ok(questions[0].question.includes('MySQL'), 'question should mention the contradiction');
224
+
225
+ // Verify old knowledge was superseded
226
+ const oldKnowledge = brain.getKnowledge(oldK.id);
227
+ assert.equal(oldKnowledge.status, 'superseded', 'old knowledge should be superseded');
228
+ assert.ok(oldKnowledge.superseded_by, 'should have superseded_by set');
229
+ });
230
+
231
+ it('inserts knowledge normally when no contradictions found', async () => {
232
+ brain.insertMemory({
233
+ source: 'test', memory_type: 'observation',
234
+ content: 'I like hiking', timestamp: '2026-03-22T13:00:00Z',
235
+ });
236
+
237
+ const mockExtract = async (memories) => [{
238
+ category: 'hobby',
239
+ subject: ownerName,
240
+ predicate: 'enjoys',
241
+ object: 'hiking',
242
+ confidence: 0.7,
243
+ source_memory_ids: memories.map(m => m.id),
244
+ }];
245
+
246
+ const mockDetectFn = () => [];
247
+
248
+ const questionsBefore = brain.listQuestions({ question_type: 'contradiction' }).length;
249
+
250
+ await think.runOnce({ extractFn: mockExtract, detectFn: mockDetectFn });
251
+
252
+ const questionsAfter = brain.listQuestions({ question_type: 'contradiction' }).length;
253
+ assert.equal(questionsAfter, questionsBefore, 'no new contradiction questions should be created');
254
+
255
+ const hiking = brain.findKnowledge({ subject: ownerName, predicate: 'enjoys' });
256
+ assert.ok(hiking.length > 0, 'hiking knowledge should be inserted');
257
+ });
258
+
259
+ it('contradiction creates question with question_type=contradiction', async () => {
260
+ // Insert existing knowledge
261
+ const oldK = brain.insertKnowledge({
262
+ category: 'work',
263
+ subject: ownerName,
264
+ predicate: 'works_at',
265
+ object: 'CompanyA',
266
+ confidence: 0.9,
267
+ });
268
+
269
+ brain.insertMemory({
270
+ source: 'test', memory_type: 'observation',
271
+ content: 'I now work at CompanyB', timestamp: '2026-03-22T14:00:00Z',
272
+ });
273
+
274
+ const mockExtract = async (memories) => [{
275
+ category: 'work',
276
+ subject: ownerName,
277
+ predicate: 'works_at',
278
+ object: 'CompanyB',
279
+ confidence: 0.9,
280
+ source_memory_ids: memories.map(m => m.id),
281
+ }];
282
+
283
+ const mockDetectFn = (existing, entries) => {
284
+ const old = existing.find(e => e.object === 'CompanyA');
285
+ const ne = entries.find(e => e.object === 'CompanyB');
286
+ if (!old || !ne) return [];
287
+ return [{
288
+ old_id: old.id,
289
+ new_subject: ne.subject,
290
+ new_predicate: ne.predicate,
291
+ new_object: ne.object,
292
+ explanation: 'Changed employer from CompanyA to CompanyB',
293
+ }];
294
+ };
295
+
296
+ await think.runOnce({ extractFn: mockExtract, detectFn: mockDetectFn });
297
+
298
+ const questions = brain.listQuestions({ question_type: 'contradiction' });
299
+ const companyQ = questions.find(q => q.question.includes('CompanyA') || q.question.includes('CompanyB'));
300
+ assert.ok(companyQ, 'should find the contradiction question');
301
+ assert.equal(companyQ.question_type, 'contradiction');
302
+ assert.equal(companyQ.status, 'pending');
303
+ });
304
+ });
305
+
306
+ // ── Task 4: Pending Question Self-Resolution ─────────────────────────────
307
+
308
+ describe('think loop - self-resolution', () => {
309
+ const ownerName = 'ResolveOwner';
310
+ let tmpDir;
311
+
312
+ before(() => {
313
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'walle-resolve-test-'));
314
+ brain.initDb(path.join(tmpDir, 'test.db'));
315
+ brain.setOwner('name', ownerName);
316
+ });
317
+
318
+ after(() => {
319
+ brain.closeDb();
320
+ fs.rmSync(tmpDir, { recursive: true, force: true });
321
+ });
322
+
323
+ it('resolves pending question when enough new memories exist', async () => {
324
+ // Insert a pending question (created_at will be "now")
325
+ const q = brain.insertQuestion({
326
+ question_type: 'contradiction',
327
+ question: 'Is SQLite or MySQL preferred?',
328
+ context: JSON.stringify({ old_id: 'fake-old', new_entry: {} }),
329
+ });
330
+
331
+ // Insert 5 memories with timestamps after the question was created
332
+ const futureTime = new Date(Date.now() + 60000).toISOString();
333
+ for (let i = 0; i < 5; i++) {
334
+ brain.insertMemory({
335
+ source: 'test', memory_type: 'observation',
336
+ content: `Memory supporting SQLite preference ${i}`,
337
+ timestamp: futureTime,
338
+ });
339
+ }
340
+
341
+ const mockSelfResolve = async (question, memories, owner) => {
342
+ if (memories.length > 3) {
343
+ return {
344
+ resolved: true,
345
+ answer: 'SQLite is preferred',
346
+ evidence: ['mem-1', 'mem-2'],
347
+ };
348
+ }
349
+ return { resolved: false };
350
+ };
351
+
352
+ // No pending extraction memories (they all got inserted above with pending status),
353
+ // but we need to mark them done so they don't go through extraction
354
+ const pendingMems = brain.listMemories({ extractionStatus: 'pending' });
355
+ for (const m of pendingMems) {
356
+ brain.updateMemoryExtraction(m.id, 'done');
357
+ }
358
+
359
+ await think.runOnce({
360
+ extractFn: async () => [],
361
+ selfResolveFn: mockSelfResolve,
362
+ });
363
+
364
+ const resolved = brain.listQuestions({ status: 'inferred' });
365
+ const match = resolved.find(r => r.id === q.id);
366
+ assert.ok(match, 'question should be resolved');
367
+ assert.equal(match.status, 'inferred');
368
+ assert.equal(match.answer, 'SQLite is preferred');
369
+ assert.equal(match.resolution_type, 'inferred');
370
+ });
371
+
372
+ it('keeps question pending with insufficient memories', async () => {
373
+ // Insert a question with a far-future created_at so no memories qualify
374
+ // We do this by inserting directly with a future timestamp
375
+ const db = brain.getDb();
376
+ const { v4: uuidv4 } = require('uuid');
377
+ const qId = uuidv4();
378
+ db.prepare(`
379
+ INSERT INTO pending_questions (id, question_type, question, status, created_at)
380
+ VALUES (?, ?, ?, 'pending', ?)
381
+ `).run(qId, 'preference', 'What editor does the user prefer?', '2099-01-01T00:00:00Z');
382
+
383
+ let selfResolveCalled = false;
384
+ const mockSelfResolve = async () => {
385
+ selfResolveCalled = true;
386
+ return { resolved: true, answer: 'should not be called' };
387
+ };
388
+
389
+ await think.runOnce({
390
+ extractFn: async () => [],
391
+ selfResolveFn: mockSelfResolve,
392
+ });
393
+
394
+ assert.equal(selfResolveCalled, false, 'selfResolveFn should not be called with insufficient memories');
395
+ const question = brain.listQuestions({ status: 'pending' }).find(q => q.id === qId);
396
+ assert.ok(question, 'question should still be pending');
397
+ assert.equal(question.status, 'pending');
398
+ });
399
+
400
+ it('keeps question pending when selfResolveFn says not resolved', async () => {
401
+ const q = brain.insertQuestion({
402
+ question_type: 'contradiction',
403
+ question: 'Unresolvable question',
404
+ });
405
+
406
+ // Insert enough memories to trigger self-resolution attempt
407
+ const futureTime = new Date(Date.now() + 120000).toISOString();
408
+ for (let i = 0; i < 5; i++) {
409
+ brain.insertMemory({
410
+ source: 'test', memory_type: 'observation',
411
+ content: `Unrelated memory ${i}`,
412
+ timestamp: futureTime,
413
+ });
414
+ }
415
+
416
+ // Mark all pending memories done
417
+ const pendingMems = brain.listMemories({ extractionStatus: 'pending' });
418
+ for (const m of pendingMems) {
419
+ brain.updateMemoryExtraction(m.id, 'done');
420
+ }
421
+
422
+ const mockSelfResolve = async () => {
423
+ return { resolved: false };
424
+ };
425
+
426
+ await think.runOnce({
427
+ extractFn: async () => [],
428
+ selfResolveFn: mockSelfResolve,
429
+ });
430
+
431
+ const questions = brain.listQuestions({ status: 'pending' });
432
+ const match = questions.find(r => r.id === q.id);
433
+ assert.ok(match, 'question should still be pending');
434
+ });
435
+ });