@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
@@ -5,6 +5,7 @@
5
5
  * Auto-registers on module load.
6
6
  *
7
7
  * T138: Triggers memory bridge refresh after task completion.
8
+ * T554: Triggers LLM observer after task completion when observation count ≥ threshold.
8
9
  */
9
10
 
10
11
  import { hooks } from '../registry.js';
@@ -37,6 +38,7 @@ export async function handleToolStart(
37
38
  * Handle PostToolUse (maps to task.complete in CLEO, canonical: was onToolComplete)
38
39
  *
39
40
  * T138: Refresh memory bridge after task completion.
41
+ * T554: Fire-and-forget LLM observer when observation count ≥ threshold.
40
42
  */
41
43
  export async function handleToolComplete(
42
44
  projectRoot: string,
@@ -55,6 +57,18 @@ export async function handleToolComplete(
55
57
  if (!isMissingBrainSchemaError(err)) throw err;
56
58
  }
57
59
 
60
+ // T554: Fire-and-forget observer — runs after observation is stored so the
61
+ // new observation is included in the count. setImmediate ensures the task
62
+ // complete response reaches the caller before the LLM call begins.
63
+ setImmediate(async () => {
64
+ try {
65
+ const { runObserver } = await import('../../memory/observer-reflector.js');
66
+ await runObserver(projectRoot);
67
+ } catch {
68
+ // Observer errors must never surface to the task complete flow
69
+ }
70
+ });
71
+
58
72
  // T138: Refresh memory bridge after task completes (best-effort)
59
73
  await maybeRefreshMemoryBridge(projectRoot);
60
74
  }
package/src/internal.ts CHANGED
@@ -885,6 +885,14 @@ export type {
885
885
  BrainSearchHit,
886
886
  BrainTimelineNeighborRow,
887
887
  } from './memory/brain-row-types.js';
888
+ // Memory — LLM extraction gate (additional)
889
+ export type {
890
+ ExtractedMemory,
891
+ ExtractFromTranscriptOptions,
892
+ ExtractionReport,
893
+ ExtractionType,
894
+ } from './memory/llm-extraction.js';
895
+ export { extractFromTranscript as llmExtractFromTranscript } from './memory/llm-extraction.js';
888
896
  // Memory (additional)
889
897
  export {
890
898
  generateContextAwareContent,
@@ -1,11 +1,10 @@
1
1
  /**
2
- * Unit tests for auto-extract memory pipeline.
2
+ * Unit tests for the auto-extract module.
3
3
  *
4
- * extractTaskCompletionMemory and extractSessionEndMemory are disabled no-ops
5
- * per T523 CA1 specification. Tests verify the no-op contract holds.
6
- *
7
- * @task T526
8
- * @epic T523
4
+ * The legacy `extractTaskCompletionMemory` and `extractSessionEndMemory`
5
+ * functions were removed entirely (the LLM extraction gate replaced the
6
+ * keyword regex in extractFromTranscript). Only `resolveTaskDetails` and
7
+ * `extractFromTranscript` remain.
9
8
  */
10
9
 
11
10
  import type { Task } from '@cleocode/contracts';
@@ -13,35 +12,25 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
13
12
 
14
13
  // ---- mocks ----------------------------------------------------------------
15
14
 
16
- vi.mock('../learnings.js', () => ({
17
- storeLearning: vi.fn().mockResolvedValue(undefined),
18
- }));
19
-
20
- vi.mock('../patterns.js', () => ({
21
- storePattern: vi.fn().mockResolvedValue(undefined),
15
+ vi.mock('../llm-extraction.js', () => ({
16
+ extractFromTranscript: vi.fn().mockResolvedValue({
17
+ extractedCount: 0,
18
+ storedCount: 0,
19
+ mergedCount: 0,
20
+ rejectedCount: 0,
21
+ warnings: [],
22
+ }),
22
23
  }));
23
24
 
24
- vi.mock('../decisions.js', () => ({
25
- storeDecision: vi.fn().mockResolvedValue(undefined),
26
- }));
27
-
28
- // Mock getAccessor — should never be called by disabled functions
29
25
  vi.mock('../../store/data-accessor.js', () => ({
30
26
  getAccessor: vi.fn(),
31
27
  }));
32
28
 
33
29
  // ---- imports after mocks --------------------------------------------------
34
30
 
35
- import type { SessionBridgeData } from '../../sessions/session-memory-bridge.js';
36
31
  import { getAccessor } from '../../store/data-accessor.js';
37
- import {
38
- extractSessionEndMemory,
39
- extractTaskCompletionMemory,
40
- resolveTaskDetails,
41
- } from '../auto-extract.js';
42
- import { storeDecision } from '../decisions.js';
43
- import { storeLearning } from '../learnings.js';
44
- import { storePattern } from '../patterns.js';
32
+ import { extractFromTranscript, resolveTaskDetails } from '../auto-extract.js';
33
+ import { extractFromTranscript as llmExtractFromTranscript } from '../llm-extraction.js';
45
34
 
46
35
  // ---- helpers --------------------------------------------------------------
47
36
 
@@ -57,16 +46,6 @@ function makeTask(overrides: Partial<Task> & { id: string; title: string }): Tas
57
46
  } as Task;
58
47
  }
59
48
 
60
- function makeSessionData(overrides: Partial<SessionBridgeData> = {}): SessionBridgeData {
61
- return {
62
- sessionId: 'S-test-001',
63
- scope: 'test scope',
64
- tasksCompleted: [],
65
- duration: 3600,
66
- ...overrides,
67
- };
68
- }
69
-
70
49
  function setupAccessor(tasks: Task[]): void {
71
50
  (getAccessor as ReturnType<typeof vi.fn>).mockResolvedValue({
72
51
  queryTasks: vi.fn().mockResolvedValue({ tasks, total: tasks.length }),
@@ -83,96 +62,46 @@ beforeEach(() => {
83
62
  vi.clearAllMocks();
84
63
  });
85
64
 
86
- describe('extractTaskCompletionMemory', () => {
87
- it('is a no-op does not write learnings', async () => {
88
- const task = makeTask({ id: 'T001', title: 'Fix auth bug', description: 'Auth was broken' });
89
- setupAccessor([task]);
90
-
91
- await extractTaskCompletionMemory('/mock/root', task);
92
-
93
- expect(storeLearning).not.toHaveBeenCalled();
94
- });
65
+ describe('extractFromTranscript (wrapper)', () => {
66
+ it('delegates to the LLM extraction gate for non-empty input', async () => {
67
+ await extractFromTranscript('/mock/root', 'S-100', 'some transcript content');
95
68
 
96
- it('is a no-op — does not write dependency learnings', async () => {
97
- const task = makeTask({
98
- id: 'T002',
99
- title: 'Deploy feature',
100
- description: 'Deploy the thing',
101
- depends: ['T001', 'T003'],
69
+ expect(llmExtractFromTranscript).toHaveBeenCalledTimes(1);
70
+ expect(llmExtractFromTranscript).toHaveBeenCalledWith({
71
+ projectRoot: '/mock/root',
72
+ sessionId: 'S-100',
73
+ transcript: 'some transcript content',
102
74
  });
103
- setupAccessor([task]);
104
-
105
- await extractTaskCompletionMemory('/mock/root', task);
106
-
107
- expect(storeLearning).not.toHaveBeenCalled();
108
75
  });
109
76
 
110
- it('is a no-op does not write label patterns', async () => {
111
- const completedTasks = [
112
- makeTask({ id: 'T010', title: 'A', labels: ['bug'] }),
113
- makeTask({ id: 'T011', title: 'B', labels: ['bug'] }),
114
- makeTask({ id: 'T012', title: 'C', labels: ['bug'] }),
115
- ];
116
- const trigger = makeTask({ id: 'T013', title: 'D', labels: ['bug'] });
117
- setupAccessor([...completedTasks, trigger]);
118
-
119
- await extractTaskCompletionMemory('/mock/root', trigger);
77
+ it('skips the LLM call when transcript is empty', async () => {
78
+ await extractFromTranscript('/mock/root', 'S-101', '');
79
+ expect(llmExtractFromTranscript).not.toHaveBeenCalled();
80
+ });
120
81
 
121
- expect(storePattern).not.toHaveBeenCalled();
122
- expect(getAccessor).not.toHaveBeenCalled();
82
+ it('skips the LLM call when transcript is whitespace-only', async () => {
83
+ await extractFromTranscript('/mock/root', 'S-102', ' \n \t ');
84
+ expect(llmExtractFromTranscript).not.toHaveBeenCalled();
123
85
  });
124
86
 
125
- it('resolves to undefined without throwing', async () => {
126
- setupAccessor([]);
87
+ it('swallows errors from the LLM extraction gate', async () => {
88
+ (llmExtractFromTranscript as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
89
+ new Error('simulated failure'),
90
+ );
127
91
 
128
92
  await expect(
129
- extractTaskCompletionMemory('/mock/root', makeTask({ id: 'T099', title: 'X' })),
93
+ extractFromTranscript('/mock/root', 'S-103', 'meaningful content'),
130
94
  ).resolves.toBeUndefined();
131
95
  });
132
- });
133
96
 
134
- describe('extractSessionEndMemory', () => {
135
- it('is a no-op — does not write a session decision', async () => {
136
- const tasks = [
137
- makeTask({ id: 'T001', title: 'Task one' }),
138
- makeTask({ id: 'T002', title: 'Task two' }),
139
- ];
140
- const session = makeSessionData({ sessionId: 'S-001', tasksCompleted: ['T001', 'T002'] });
141
-
142
- await extractSessionEndMemory('/mock/root', session, tasks);
143
-
144
- expect(storeDecision).not.toHaveBeenCalled();
145
- });
146
-
147
- it('is a no-op — does not write per-task learnings', async () => {
148
- const tasks = [
149
- makeTask({ id: 'T001', title: 'Task one', description: 'Desc one' }),
150
- makeTask({ id: 'T002', title: 'Task two', description: 'Desc two' }),
151
- ];
152
- const session = makeSessionData({ sessionId: 'S-002', tasksCompleted: ['T001', 'T002'] });
153
-
154
- await extractSessionEndMemory('/mock/root', session, tasks);
155
-
156
- expect(storeLearning).not.toHaveBeenCalled();
157
- });
158
-
159
- it('is a no-op — does not write workflow patterns', async () => {
160
- const tasks = [
161
- makeTask({ id: 'T001', title: 'A', labels: ['feature'] }),
162
- makeTask({ id: 'T002', title: 'B', labels: ['feature'] }),
163
- ];
164
- const session = makeSessionData({ sessionId: 'S-003', tasksCompleted: ['T001', 'T002'] });
165
-
166
- await extractSessionEndMemory('/mock/root', session, tasks);
167
-
168
- expect(storePattern).not.toHaveBeenCalled();
169
- });
170
-
171
- it('resolves to undefined without throwing', async () => {
172
- const tasks = [makeTask({ id: 'T001', title: 'X' })];
173
- const session = makeSessionData({ tasksCompleted: ['T001'] });
174
-
175
- await expect(extractSessionEndMemory('/mock/root', session, tasks)).resolves.toBeUndefined();
97
+ it('returns undefined on non-string transcript input', async () => {
98
+ await expect(
99
+ extractFromTranscript('/mock/root', 'S-104', null as unknown as string),
100
+ ).resolves.toBeUndefined();
101
+ await expect(
102
+ extractFromTranscript('/mock/root', 'S-104', 123 as unknown as string),
103
+ ).resolves.toBeUndefined();
104
+ expect(llmExtractFromTranscript).not.toHaveBeenCalled();
176
105
  });
177
106
  });
178
107
 
@@ -460,53 +460,24 @@ describe('ingestStructuredSummary', () => {
460
460
  });
461
461
 
462
462
  // ============================================================================
463
- // 4. extractFromTranscript (auto-extract.ts)
463
+ // 4. extractFromTranscript (auto-extract.ts → llm-extraction.ts)
464
464
  // ============================================================================
465
465
 
466
- describe('extractFromTranscript', () => {
466
+ describe('extractFromTranscript (wrapper)', () => {
467
467
  beforeEach(() => {
468
468
  vi.clearAllMocks();
469
+ // Prevent real network calls during these tests by unsetting the API key.
470
+ delete process.env.ANTHROPIC_API_KEY;
469
471
  });
470
472
 
471
- it('extracts action-word lines from transcript', async () => {
473
+ it('returns without calling stores when transcript is empty', async () => {
472
474
  const { extractFromTranscript } = await import('../auto-extract.js');
473
475
  const { storeLearning } = await import('../learnings.js');
474
476
 
475
- const transcript = [
476
- 'user: Can you implement the auth module?',
477
- 'assistant: I will implement the auth module now.',
478
- 'assistant: I have fixed the login bug in auth.ts.',
479
- 'assistant: This is a short line.',
480
- 'user: Great, what about the tests?',
481
- 'assistant: I will add tests for the auth module.',
482
- ].join('\n');
483
-
484
- await extractFromTranscript('/mock/root', 'S-001', transcript);
485
-
486
- expect(storeLearning).toHaveBeenCalled();
487
- const calls = (storeLearning as ReturnType<typeof vi.fn>).mock.calls;
488
- // All stored learnings should come from action-word lines
489
- for (const [, learning] of calls) {
490
- expect(learning.source).toBe('transcript:S-001');
491
- expect(learning.confidence).toBe(0.6);
492
- }
493
- });
494
-
495
- it('limits to 5 learnings max', async () => {
496
- const { extractFromTranscript } = await import('../auto-extract.js');
497
- const { storeLearning } = await import('../learnings.js');
498
-
499
- // Build a transcript with 10 action lines (all > 20 chars)
500
- const lines = Array.from(
501
- { length: 10 },
502
- (_, i) => `assistant: I implemented feature number ${i + 1} successfully.`,
503
- );
504
- const transcript = lines.join('\n');
477
+ await extractFromTranscript('/mock/root', 'S-001', '');
478
+ await extractFromTranscript('/mock/root', 'S-001', ' \n ');
505
479
 
506
- await extractFromTranscript('/mock/root', 'S-002', transcript);
507
-
508
- const calls = (storeLearning as ReturnType<typeof vi.fn>).mock.calls;
509
- expect(calls.length).toBeLessThanOrEqual(5);
480
+ expect(storeLearning).not.toHaveBeenCalled();
510
481
  });
511
482
 
512
483
  it('never throws on malformed input', async () => {
@@ -531,13 +502,19 @@ describe('extractFromTranscript', () => {
531
502
  expect(result).toBeUndefined();
532
503
  });
533
504
 
534
- it('does nothing when no action lines present', async () => {
505
+ it('skips storage when ANTHROPIC_API_KEY is absent', async () => {
535
506
  const { extractFromTranscript } = await import('../auto-extract.js');
536
507
  const { storeLearning } = await import('../learnings.js');
537
508
 
538
- const transcript = ['user: Hello', 'assistant: Hi there', 'user: How are you?'].join('\n');
509
+ // No API key set the LLM gate must gracefully skip.
510
+ const transcript = [
511
+ 'user: Can you implement the auth module?',
512
+ 'assistant: I will implement the auth module now.',
513
+ 'assistant: I have fixed the login bug in auth.ts.',
514
+ ].join('\n');
539
515
 
540
516
  await extractFromTranscript('/mock/root', 'S-005', transcript);
517
+
541
518
  expect(storeLearning).not.toHaveBeenCalled();
542
519
  });
543
520
  });