@cleocode/core 2026.4.35 → 2026.4.37

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 (91) hide show
  1. package/dist/config.d.ts.map +1 -1
  2. package/dist/config.js +7 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/hooks/handlers/conduit-hooks.d.ts +72 -0
  5. package/dist/hooks/handlers/conduit-hooks.d.ts.map +1 -0
  6. package/dist/hooks/handlers/conduit-hooks.js +229 -0
  7. package/dist/hooks/handlers/conduit-hooks.js.map +1 -0
  8. package/dist/hooks/handlers/index.d.ts +2 -0
  9. package/dist/hooks/handlers/index.d.ts.map +1 -1
  10. package/dist/hooks/handlers/index.js +3 -0
  11. package/dist/hooks/handlers/index.js.map +1 -1
  12. package/dist/hooks/handlers/session-hooks.d.ts +14 -0
  13. package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
  14. package/dist/hooks/handlers/session-hooks.js +33 -0
  15. package/dist/hooks/handlers/session-hooks.js.map +1 -1
  16. package/dist/hooks/handlers/task-hooks.d.ts +2 -0
  17. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  18. package/dist/hooks/handlers/task-hooks.js +14 -0
  19. package/dist/hooks/handlers/task-hooks.js.map +1 -1
  20. package/dist/index.js +54928 -46853
  21. package/dist/index.js.map +4 -4
  22. package/dist/internal.d.ts +2 -0
  23. package/dist/internal.d.ts.map +1 -1
  24. package/dist/internal.js +1 -0
  25. package/dist/internal.js.map +1 -1
  26. package/dist/memory/anthropic-key-resolver.d.ts +35 -0
  27. package/dist/memory/anthropic-key-resolver.d.ts.map +1 -0
  28. package/dist/memory/anthropic-key-resolver.js +105 -0
  29. package/dist/memory/anthropic-key-resolver.js.map +1 -0
  30. package/dist/memory/auto-extract.d.ts +38 -42
  31. package/dist/memory/auto-extract.d.ts.map +1 -1
  32. package/dist/memory/auto-extract.js +38 -57
  33. package/dist/memory/auto-extract.js.map +1 -1
  34. package/dist/memory/brain-retrieval.d.ts +6 -0
  35. package/dist/memory/brain-retrieval.d.ts.map +1 -1
  36. package/dist/memory/brain-retrieval.js +145 -13
  37. package/dist/memory/brain-retrieval.js.map +1 -1
  38. package/dist/memory/brain-search.d.ts +82 -15
  39. package/dist/memory/brain-search.d.ts.map +1 -1
  40. package/dist/memory/brain-search.js +178 -93
  41. package/dist/memory/brain-search.js.map +1 -1
  42. package/dist/memory/engine-compat.d.ts +16 -1
  43. package/dist/memory/engine-compat.d.ts.map +1 -1
  44. package/dist/memory/engine-compat.js +0 -3
  45. package/dist/memory/engine-compat.js.map +1 -1
  46. package/dist/memory/learnings.d.ts.map +1 -1
  47. package/dist/memory/learnings.js +4 -3
  48. package/dist/memory/learnings.js.map +1 -1
  49. package/dist/memory/llm-extraction.d.ts +107 -0
  50. package/dist/memory/llm-extraction.d.ts.map +1 -0
  51. package/dist/memory/llm-extraction.js +425 -0
  52. package/dist/memory/llm-extraction.js.map +1 -0
  53. package/dist/memory/memory-bridge.js +23 -11
  54. package/dist/memory/memory-bridge.js.map +1 -1
  55. package/dist/memory/observer-reflector.d.ts +157 -0
  56. package/dist/memory/observer-reflector.d.ts.map +1 -0
  57. package/dist/memory/observer-reflector.js +626 -0
  58. package/dist/memory/observer-reflector.js.map +1 -0
  59. package/dist/store/brain-schema.d.ts +131 -0
  60. package/dist/store/brain-schema.d.ts.map +1 -1
  61. package/dist/store/brain-schema.js +30 -0
  62. package/dist/store/brain-schema.js.map +1 -1
  63. package/dist/store/brain-sqlite.js +41 -1
  64. package/dist/store/brain-sqlite.js.map +1 -1
  65. package/dist/tasks/complete.d.ts.map +1 -1
  66. package/dist/tasks/complete.js +7 -8
  67. package/dist/tasks/complete.js.map +1 -1
  68. package/package.json +13 -12
  69. package/src/config.ts +7 -0
  70. package/src/hooks/handlers/__tests__/conduit-hooks.test.ts +356 -0
  71. package/src/hooks/handlers/conduit-hooks.ts +258 -0
  72. package/src/hooks/handlers/index.ts +7 -0
  73. package/src/hooks/handlers/session-hooks.ts +37 -0
  74. package/src/hooks/handlers/task-hooks.ts +14 -0
  75. package/src/internal.ts +8 -0
  76. package/src/memory/__tests__/auto-extract.test.ts +43 -114
  77. package/src/memory/__tests__/brain-automation.test.ts +16 -39
  78. package/src/memory/__tests__/brain-rrf.test.ts +431 -0
  79. package/src/memory/__tests__/llm-extraction.test.ts +342 -0
  80. package/src/memory/__tests__/observer-reflector.test.ts +475 -0
  81. package/src/memory/anthropic-key-resolver.ts +113 -0
  82. package/src/memory/auto-extract.ts +40 -72
  83. package/src/memory/brain-retrieval.ts +187 -18
  84. package/src/memory/brain-search.ts +196 -128
  85. package/src/memory/engine-compat.ts +16 -4
  86. package/src/memory/learnings.ts +4 -3
  87. package/src/memory/llm-extraction.ts +524 -0
  88. package/src/memory/memory-bridge.ts +29 -12
  89. package/src/memory/observer-reflector.ts +829 -0
  90. package/src/store/brain-schema.ts +44 -0
  91. package/src/tasks/complete.ts +7 -10
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Unit tests for the LLM-driven extraction gate.
3
+ *
4
+ * The tests inject a mocked Anthropic client via the `client` option so no
5
+ * real network calls or API keys are required. All downstream stores are
6
+ * mocked so assertions target only the extraction routing logic.
7
+ */
8
+
9
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
10
+
11
+ // ---- mocks ----------------------------------------------------------------
12
+
13
+ vi.mock('../../config.js', () => ({
14
+ loadConfig: vi.fn().mockResolvedValue({
15
+ brain: {
16
+ llmExtraction: {
17
+ enabled: true,
18
+ model: 'claude-haiku-4-5-20251001',
19
+ minImportance: 0.6,
20
+ maxExtractions: 7,
21
+ maxTranscriptChars: 60000,
22
+ },
23
+ },
24
+ }),
25
+ }));
26
+
27
+ vi.mock('../learnings.js', () => ({
28
+ storeLearning: vi.fn().mockResolvedValue({ id: 'L-mocked' }),
29
+ }));
30
+
31
+ vi.mock('../patterns.js', () => ({
32
+ storePattern: vi.fn().mockResolvedValue({ id: 'P-mocked' }),
33
+ }));
34
+
35
+ vi.mock('../decisions.js', () => ({
36
+ storeDecision: vi.fn().mockResolvedValue({ id: 'D-mocked' }),
37
+ }));
38
+
39
+ // The zod helper is dynamically imported by the module; mock the subpath so
40
+ // we can control whether structured output is available.
41
+ vi.mock('@anthropic-ai/sdk/helpers/zod', () => ({
42
+ // Return a sentinel object — the mock messages.parse below accepts any format.
43
+ zodOutputFormat: vi.fn().mockReturnValue({ _mock: 'zodOutputFormat' }),
44
+ }));
45
+
46
+ // Mock the SDK entry point so buildAnthropicClient doesn't touch the network.
47
+ // Tests that need this will inject a custom client via options.client instead.
48
+ vi.mock('@anthropic-ai/sdk', () => {
49
+ class MockAnthropic {
50
+ messages = { parse: vi.fn(), create: vi.fn() };
51
+ }
52
+ return { default: MockAnthropic };
53
+ });
54
+
55
+ // ---- imports after mocks --------------------------------------------------
56
+
57
+ import { storeDecision } from '../decisions.js';
58
+ import { storeLearning } from '../learnings.js';
59
+ import type { ExtractedMemory } from '../llm-extraction.js';
60
+ import { extractFromTranscript } from '../llm-extraction.js';
61
+ import { storePattern } from '../patterns.js';
62
+
63
+ // ---- helpers --------------------------------------------------------------
64
+
65
+ function makeClient(memories: ExtractedMemory[]) {
66
+ const parse = vi.fn().mockResolvedValue({
67
+ parsed_output: { memories },
68
+ });
69
+ const create = vi.fn();
70
+ return {
71
+ parse,
72
+ create,
73
+ client: {
74
+ messages: { parse, create },
75
+ } as unknown as Parameters<typeof extractFromTranscript>[0]['client'],
76
+ };
77
+ }
78
+
79
+ // ---- tests ----------------------------------------------------------------
80
+
81
+ beforeEach(() => {
82
+ vi.clearAllMocks();
83
+ // Ensure the buildAnthropicClient path is not reached when we inject clients.
84
+ delete process.env.ANTHROPIC_API_KEY;
85
+ });
86
+
87
+ describe('extractFromTranscript (LLM gate)', () => {
88
+ it('returns an empty report when transcript is empty', async () => {
89
+ const report = await extractFromTranscript({
90
+ projectRoot: '/mock/root',
91
+ sessionId: 'S-empty',
92
+ transcript: '',
93
+ });
94
+
95
+ expect(report.extractedCount).toBe(0);
96
+ expect(report.storedCount).toBe(0);
97
+ expect(storeLearning).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it('returns a warning and does not store when API key and client are absent', async () => {
101
+ const report = await extractFromTranscript({
102
+ projectRoot: '/mock/root',
103
+ sessionId: 'S-no-key',
104
+ transcript: 'assistant: I implemented a feature',
105
+ });
106
+
107
+ expect(report.storedCount).toBe(0);
108
+ expect(report.warnings.some((w) => w.includes('ANTHROPIC_API_KEY'))).toBe(true);
109
+ expect(storeLearning).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it('routes a learning extraction to storeLearning', async () => {
113
+ const { client } = makeClient([
114
+ {
115
+ type: 'learning',
116
+ content: 'Brain.db uses SQLite WAL mode',
117
+ importance: 0.75,
118
+ entities: ['brain.db', 'WAL'],
119
+ justification: 'Architectural fact worth retaining',
120
+ },
121
+ ]);
122
+
123
+ const report = await extractFromTranscript({
124
+ projectRoot: '/mock/root',
125
+ sessionId: 'S-learn',
126
+ transcript: 'assistant: The brain uses WAL mode',
127
+ client,
128
+ });
129
+
130
+ expect(report.extractedCount).toBe(1);
131
+ expect(report.storedCount).toBe(1);
132
+ expect(storeLearning).toHaveBeenCalledTimes(1);
133
+ expect(storeLearning).toHaveBeenCalledWith(
134
+ '/mock/root',
135
+ expect.objectContaining({
136
+ source: 'agent-llm-extracted:S-learn',
137
+ insight: 'Brain.db uses SQLite WAL mode',
138
+ }),
139
+ );
140
+ });
141
+
142
+ it('routes a pattern extraction to storePattern', async () => {
143
+ const { client } = makeClient([
144
+ {
145
+ type: 'pattern',
146
+ content: 'When writing migrations, add ensureColumns as safety net',
147
+ importance: 0.82,
148
+ entities: ['migrations', 'ensureColumns'],
149
+ justification: 'Pattern seen in T528/T531/T549',
150
+ },
151
+ ]);
152
+
153
+ const report = await extractFromTranscript({
154
+ projectRoot: '/mock/root',
155
+ sessionId: 'S-pat',
156
+ transcript: 'assistant: We add ensureColumns as a safety net',
157
+ client,
158
+ });
159
+
160
+ expect(report.storedCount).toBe(1);
161
+ expect(storePattern).toHaveBeenCalledTimes(1);
162
+ expect(storePattern).toHaveBeenCalledWith(
163
+ '/mock/root',
164
+ expect.objectContaining({
165
+ type: 'workflow',
166
+ pattern: 'When writing migrations, add ensureColumns as safety net',
167
+ source: 'agent-llm-extracted:S-pat',
168
+ impact: 'high',
169
+ }),
170
+ );
171
+ });
172
+
173
+ it('routes a decision extraction to storeDecision with split rationale', async () => {
174
+ const { client } = makeClient([
175
+ {
176
+ type: 'decision',
177
+ content: 'Use SQLite because WAL mode prevents corruption during concurrent reads',
178
+ importance: 0.9,
179
+ entities: ['SQLite', 'WAL'],
180
+ justification: 'Architectural choice',
181
+ },
182
+ ]);
183
+
184
+ const report = await extractFromTranscript({
185
+ projectRoot: '/mock/root',
186
+ sessionId: 'S-dec',
187
+ transcript: 'assistant: We chose SQLite',
188
+ client,
189
+ });
190
+
191
+ expect(report.storedCount).toBe(1);
192
+ expect(storeDecision).toHaveBeenCalledTimes(1);
193
+ expect(storeDecision).toHaveBeenCalledWith(
194
+ '/mock/root',
195
+ expect.objectContaining({
196
+ type: 'technical',
197
+ decision: 'Use SQLite',
198
+ confidence: 'high',
199
+ }),
200
+ );
201
+ });
202
+
203
+ it('routes a correction extraction to storePattern with antiPattern', async () => {
204
+ const { client } = makeClient([
205
+ {
206
+ type: 'correction',
207
+ content: 'Avoid using any type; find root cause and wire proper types',
208
+ importance: 0.85,
209
+ entities: ['TypeScript'],
210
+ justification: 'Anti-pattern documented in code-quality-rules.md',
211
+ },
212
+ ]);
213
+
214
+ const report = await extractFromTranscript({
215
+ projectRoot: '/mock/root',
216
+ sessionId: 'S-corr',
217
+ transcript: 'assistant: Avoid any type',
218
+ client,
219
+ });
220
+
221
+ expect(report.storedCount).toBe(1);
222
+ expect(storePattern).toHaveBeenCalledTimes(1);
223
+ expect(storePattern).toHaveBeenCalledWith(
224
+ '/mock/root',
225
+ expect.objectContaining({
226
+ type: 'failure',
227
+ antiPattern: expect.stringContaining('Avoid using any type'),
228
+ }),
229
+ );
230
+ });
231
+
232
+ it('routes a constraint extraction to storeLearning with high confidence', async () => {
233
+ const { client } = makeClient([
234
+ {
235
+ type: 'constraint',
236
+ content: 'All quality gates must pass before marking a task complete',
237
+ importance: 0.7,
238
+ entities: ['quality gates'],
239
+ justification: 'Required by AGENTS.md',
240
+ },
241
+ ]);
242
+
243
+ const report = await extractFromTranscript({
244
+ projectRoot: '/mock/root',
245
+ sessionId: 'S-cons',
246
+ transcript: 'assistant: gates must pass',
247
+ client,
248
+ });
249
+
250
+ expect(report.storedCount).toBe(1);
251
+ expect(storeLearning).toHaveBeenCalledTimes(1);
252
+ const call = (storeLearning as ReturnType<typeof vi.fn>).mock.calls[0];
253
+ // Constraint confidence must be at least 0.8 even if importance is 0.7.
254
+ expect(call[1].confidence).toBeGreaterThanOrEqual(0.8);
255
+ expect(call[1].actionable).toBe(true);
256
+ });
257
+
258
+ it('filters out extractions below minImportance', async () => {
259
+ const { client } = makeClient([
260
+ {
261
+ type: 'learning',
262
+ content: 'High importance fact',
263
+ importance: 0.85,
264
+ entities: [],
265
+ justification: 'Keep me',
266
+ },
267
+ {
268
+ type: 'learning',
269
+ content: 'Low importance fact',
270
+ importance: 0.3,
271
+ entities: [],
272
+ justification: 'Drop me',
273
+ },
274
+ ]);
275
+
276
+ const report = await extractFromTranscript({
277
+ projectRoot: '/mock/root',
278
+ sessionId: 'S-filter',
279
+ transcript: 'assistant: some content',
280
+ client,
281
+ });
282
+
283
+ expect(report.extractedCount).toBe(2);
284
+ expect(report.storedCount).toBe(1);
285
+ expect(report.rejectedCount).toBe(1);
286
+ expect(storeLearning).toHaveBeenCalledTimes(1);
287
+ });
288
+
289
+ it('returns empty report when LLM returns no extractions', async () => {
290
+ const { client } = makeClient([]);
291
+
292
+ const report = await extractFromTranscript({
293
+ projectRoot: '/mock/root',
294
+ sessionId: 'S-empty',
295
+ transcript: 'assistant: just greetings and chatter',
296
+ client,
297
+ });
298
+
299
+ expect(report.extractedCount).toBe(0);
300
+ expect(report.storedCount).toBe(0);
301
+ expect(storeLearning).not.toHaveBeenCalled();
302
+ expect(storePattern).not.toHaveBeenCalled();
303
+ expect(storeDecision).not.toHaveBeenCalled();
304
+ });
305
+
306
+ it('handles LLM call failures without throwing', async () => {
307
+ const parse = vi.fn().mockRejectedValue(new Error('rate limited'));
308
+ const client = {
309
+ messages: { parse, create: vi.fn() },
310
+ } as unknown as Parameters<typeof extractFromTranscript>[0]['client'];
311
+
312
+ const report = await extractFromTranscript({
313
+ projectRoot: '/mock/root',
314
+ sessionId: 'S-error',
315
+ transcript: 'assistant: content',
316
+ client,
317
+ });
318
+
319
+ expect(report.storedCount).toBe(0);
320
+ expect(report.warnings.some((w) => w.includes('extraction call failed'))).toBe(true);
321
+ });
322
+
323
+ it('respects maxTranscriptChars by clipping long transcripts', async () => {
324
+ const { client, parse } = makeClient([]);
325
+ const longTranscript = 'a'.repeat(120000);
326
+
327
+ await extractFromTranscript({
328
+ projectRoot: '/mock/root',
329
+ sessionId: 'S-clip',
330
+ transcript: longTranscript,
331
+ client,
332
+ });
333
+
334
+ expect(parse).toHaveBeenCalledTimes(1);
335
+ const body = parse.mock.calls[0][0] as {
336
+ messages: Array<{ content: string }>;
337
+ };
338
+ const userContent = body.messages[0].content;
339
+ // The clipper inserts a "[... N chars omitted ...]" marker.
340
+ expect(userContent).toContain('chars omitted');
341
+ });
342
+ });