@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.
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/hooks/handlers/conduit-hooks.d.ts +72 -0
- package/dist/hooks/handlers/conduit-hooks.d.ts.map +1 -0
- package/dist/hooks/handlers/conduit-hooks.js +229 -0
- package/dist/hooks/handlers/conduit-hooks.js.map +1 -0
- package/dist/hooks/handlers/index.d.ts +2 -0
- package/dist/hooks/handlers/index.d.ts.map +1 -1
- package/dist/hooks/handlers/index.js +3 -0
- package/dist/hooks/handlers/index.js.map +1 -1
- package/dist/hooks/handlers/session-hooks.d.ts +14 -0
- package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/session-hooks.js +33 -0
- package/dist/hooks/handlers/session-hooks.js.map +1 -1
- package/dist/hooks/handlers/task-hooks.d.ts +2 -0
- package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/task-hooks.js +14 -0
- package/dist/hooks/handlers/task-hooks.js.map +1 -1
- package/dist/index.js +54928 -46853
- package/dist/index.js.map +4 -4
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -0
- package/dist/internal.js.map +1 -1
- package/dist/memory/anthropic-key-resolver.d.ts +35 -0
- package/dist/memory/anthropic-key-resolver.d.ts.map +1 -0
- package/dist/memory/anthropic-key-resolver.js +105 -0
- package/dist/memory/anthropic-key-resolver.js.map +1 -0
- package/dist/memory/auto-extract.d.ts +38 -42
- package/dist/memory/auto-extract.d.ts.map +1 -1
- package/dist/memory/auto-extract.js +38 -57
- package/dist/memory/auto-extract.js.map +1 -1
- package/dist/memory/brain-retrieval.d.ts +6 -0
- package/dist/memory/brain-retrieval.d.ts.map +1 -1
- package/dist/memory/brain-retrieval.js +145 -13
- package/dist/memory/brain-retrieval.js.map +1 -1
- package/dist/memory/brain-search.d.ts +82 -15
- package/dist/memory/brain-search.d.ts.map +1 -1
- package/dist/memory/brain-search.js +178 -93
- package/dist/memory/brain-search.js.map +1 -1
- package/dist/memory/engine-compat.d.ts +16 -1
- package/dist/memory/engine-compat.d.ts.map +1 -1
- package/dist/memory/engine-compat.js +0 -3
- package/dist/memory/engine-compat.js.map +1 -1
- package/dist/memory/learnings.d.ts.map +1 -1
- package/dist/memory/learnings.js +4 -3
- package/dist/memory/learnings.js.map +1 -1
- package/dist/memory/llm-extraction.d.ts +107 -0
- package/dist/memory/llm-extraction.d.ts.map +1 -0
- package/dist/memory/llm-extraction.js +425 -0
- package/dist/memory/llm-extraction.js.map +1 -0
- package/dist/memory/memory-bridge.js +23 -11
- package/dist/memory/memory-bridge.js.map +1 -1
- package/dist/memory/observer-reflector.d.ts +157 -0
- package/dist/memory/observer-reflector.d.ts.map +1 -0
- package/dist/memory/observer-reflector.js +626 -0
- package/dist/memory/observer-reflector.js.map +1 -0
- package/dist/store/brain-schema.d.ts +131 -0
- package/dist/store/brain-schema.d.ts.map +1 -1
- package/dist/store/brain-schema.js +30 -0
- package/dist/store/brain-schema.js.map +1 -1
- package/dist/store/brain-sqlite.js +41 -1
- package/dist/store/brain-sqlite.js.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +7 -8
- package/dist/tasks/complete.js.map +1 -1
- package/package.json +13 -12
- package/src/config.ts +7 -0
- package/src/hooks/handlers/__tests__/conduit-hooks.test.ts +356 -0
- package/src/hooks/handlers/conduit-hooks.ts +258 -0
- package/src/hooks/handlers/index.ts +7 -0
- package/src/hooks/handlers/session-hooks.ts +37 -0
- package/src/hooks/handlers/task-hooks.ts +14 -0
- package/src/internal.ts +8 -0
- package/src/memory/__tests__/auto-extract.test.ts +43 -114
- package/src/memory/__tests__/brain-automation.test.ts +16 -39
- package/src/memory/__tests__/brain-rrf.test.ts +431 -0
- package/src/memory/__tests__/llm-extraction.test.ts +342 -0
- package/src/memory/__tests__/observer-reflector.test.ts +475 -0
- package/src/memory/anthropic-key-resolver.ts +113 -0
- package/src/memory/auto-extract.ts +40 -72
- package/src/memory/brain-retrieval.ts +187 -18
- package/src/memory/brain-search.ts +196 -128
- package/src/memory/engine-compat.ts +16 -4
- package/src/memory/learnings.ts +4 -3
- package/src/memory/llm-extraction.ts +524 -0
- package/src/memory/memory-bridge.ts +29 -12
- package/src/memory/observer-reflector.ts +829 -0
- package/src/store/brain-schema.ts +44 -0
- 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
|
|
2
|
+
* Unit tests for the auto-extract module.
|
|
3
3
|
*
|
|
4
|
-
* extractTaskCompletionMemory and extractSessionEndMemory
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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('../
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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('
|
|
87
|
-
it('
|
|
88
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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('
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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('
|
|
126
|
-
|
|
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
|
-
|
|
93
|
+
extractFromTranscript('/mock/root', 'S-103', 'meaningful content'),
|
|
130
94
|
).resolves.toBeUndefined();
|
|
131
95
|
});
|
|
132
|
-
});
|
|
133
96
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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('
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
});
|