@cleocode/core 2026.3.72 → 2026.3.73
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/hooks/handlers/agent-hooks.d.ts +48 -0
- package/dist/hooks/handlers/agent-hooks.d.ts.map +1 -0
- package/dist/hooks/handlers/context-hooks.d.ts +53 -0
- package/dist/hooks/handlers/context-hooks.d.ts.map +1 -0
- package/dist/hooks/handlers/error-hooks.d.ts +4 -4
- package/dist/hooks/handlers/error-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/file-hooks.d.ts +3 -3
- package/dist/hooks/handlers/file-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/index.d.ts +8 -1
- package/dist/hooks/handlers/index.d.ts.map +1 -1
- package/dist/hooks/handlers/mcp-hooks.d.ts +29 -7
- package/dist/hooks/handlers/mcp-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/session-hooks.d.ts +5 -5
- package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/task-hooks.d.ts +5 -5
- package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/work-capture-hooks.d.ts +7 -7
- package/dist/hooks/handlers/work-capture-hooks.d.ts.map +1 -1
- package/dist/hooks/payload-schemas.d.ts +177 -11
- package/dist/hooks/payload-schemas.d.ts.map +1 -1
- package/dist/hooks/provider-hooks.d.ts +33 -7
- package/dist/hooks/provider-hooks.d.ts.map +1 -1
- package/dist/hooks/registry.d.ts +26 -6
- package/dist/hooks/registry.d.ts.map +1 -1
- package/dist/hooks/types.d.ts +132 -38
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/index.js +335 -59
- package/dist/index.js.map +4 -4
- package/dist/sessions/snapshot.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +634 -0
- package/src/hooks/handlers/agent-hooks.ts +148 -0
- package/src/hooks/handlers/context-hooks.ts +156 -0
- package/src/hooks/handlers/error-hooks.ts +8 -5
- package/src/hooks/handlers/file-hooks.ts +6 -4
- package/src/hooks/handlers/index.ts +12 -1
- package/src/hooks/handlers/mcp-hooks.ts +74 -9
- package/src/hooks/handlers/session-hooks.ts +7 -7
- package/src/hooks/handlers/task-hooks.ts +7 -7
- package/src/hooks/handlers/work-capture-hooks.ts +12 -12
- package/src/hooks/payload-schemas.ts +96 -26
- package/src/hooks/provider-hooks.ts +50 -9
- package/src/hooks/registry.ts +86 -23
- package/src/hooks/types.ts +175 -39
- package/src/sessions/index.ts +4 -4
- package/src/sessions/snapshot.ts +4 -2
- package/src/store/json.ts +2 -2
- package/src/task-work/index.ts +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/sessions/snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAMhE,qEAAqE;AACrE,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC/B,qBAAqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAmB;IAClC,sBAAsB;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,4CAA4C;AAC5C,MAAM,WAAW,mBAAmB;IAClC,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,6BAA6B;IAC7B,OAAO,EAAE,WAAW,CAAC;IACrB,0CAA0C;IAC1C,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,wDAAwD;IACxD,YAAY,EAAE,mBAAmB,EAAE,CAAC;IACpC,mDAAmD;IACnD,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACvC,oDAAoD;IACpD,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,yCAAyC;AACzC,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gEAAgE;IAChE,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAMD;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,gBAAqB,EAC9B,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/sessions/snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAG9D,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAMhE,qEAAqE;AACrE,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC/B,qBAAqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAmB;IAClC,sBAAsB;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,4CAA4C;AAC5C,MAAM,WAAW,mBAAmB;IAClC,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,6BAA6B;IAC7B,OAAO,EAAE,WAAW,CAAC;IACrB,0CAA0C;IAC1C,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,wDAAwD;IACxD,YAAY,EAAE,mBAAmB,EAAE,CAAC;IACpC,mDAAmD;IACnD,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACvC,oDAAoD;IACpD,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,yCAAyC;AACzC,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gEAAgE;IAChE,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAMD;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,gBAAqB,EAC9B,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC,eAAe,CAAC,CAsG1B;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAClC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,eAAe,EACzB,OAAO,GAAE,cAAmB,EAC5B,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC,OAAO,CAAC,CAkFlB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/core",
|
|
3
|
-
"version": "2026.3.
|
|
3
|
+
"version": "2026.3.73",
|
|
4
4
|
"description": "CLEO core business logic kernel — tasks, sessions, memory, orchestration, lifecycle, with bundled SQLite store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@cleocode/caamp": "^1.
|
|
26
|
+
"@cleocode/caamp": "^1.9.1",
|
|
27
27
|
"@cleocode/lafs-protocol": "^1.8.0",
|
|
28
28
|
"@xenova/transformers": "^2.17.2",
|
|
29
29
|
"ajv": "^8.18.0",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"write-file-atomic": "^6.0.0",
|
|
38
38
|
"yaml": "^2.8.2",
|
|
39
39
|
"zod": "^3.25.76",
|
|
40
|
-
"@cleocode/adapters": "2026.3.
|
|
41
|
-
"@cleocode/agents": "2026.3.
|
|
42
|
-
"@cleocode/contracts": "2026.3.
|
|
43
|
-
"@cleocode/skills": "2026.3.
|
|
40
|
+
"@cleocode/adapters": "2026.3.73",
|
|
41
|
+
"@cleocode/agents": "2026.3.73",
|
|
42
|
+
"@cleocode/contracts": "2026.3.73",
|
|
43
|
+
"@cleocode/skills": "2026.3.73"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
46
46
|
"node": ">=24.0.0"
|
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E integration tests: hook automation fires across lifecycle events
|
|
3
|
+
*
|
|
4
|
+
* Verifies that brain automation hooks actually dispatch and call observeBrain
|
|
5
|
+
* with the correct payloads for all lifecycle event types. Tests use mock
|
|
6
|
+
* adapters and direct handler invocation to exercise the full handler logic
|
|
7
|
+
* without requiring a real SQLite database.
|
|
8
|
+
*
|
|
9
|
+
* @task T168
|
|
10
|
+
* @epic T134
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Mock setup — must come before any handler imports.
|
|
17
|
+
// vi.mock is hoisted; factory fns must use vi.fn() inline (not outer vars).
|
|
18
|
+
// After imports, use vi.mocked() to get typed refs to the mock instances.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
vi.mock('../../../memory/brain-retrieval.js', () => ({
|
|
22
|
+
observeBrain: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock('../../../config.js', () => ({
|
|
26
|
+
loadConfig: vi.fn(),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('../memory-bridge-refresh.js', () => ({
|
|
30
|
+
maybeRefreshMemoryBridge: vi.fn(),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Handler imports — after mock setup
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
import * as configModule from '../../../config.js';
|
|
38
|
+
import * as brainRetrieval from '../../../memory/brain-retrieval.js';
|
|
39
|
+
import { handleSubagentStart, handleSubagentStop } from '../agent-hooks.js';
|
|
40
|
+
import { handlePostCompact, handlePreCompact } from '../context-hooks.js';
|
|
41
|
+
import { handleSystemNotification } from '../mcp-hooks.js';
|
|
42
|
+
import * as bridgeRefresh from '../memory-bridge-refresh.js';
|
|
43
|
+
import { handleSessionEnd, handleSessionStart } from '../session-hooks.js';
|
|
44
|
+
import { handleToolComplete, handleToolStart } from '../task-hooks.js';
|
|
45
|
+
import { handleWorkPromptSubmit, handleWorkResponseComplete } from '../work-capture-hooks.js';
|
|
46
|
+
|
|
47
|
+
// Typed mock refs — assigned after imports resolve
|
|
48
|
+
const observeBrainMock = vi.mocked(brainRetrieval.observeBrain);
|
|
49
|
+
const loadConfigMock = vi.mocked(configModule.loadConfig);
|
|
50
|
+
const maybeRefreshMemoryBridgeMock = vi.mocked(bridgeRefresh.maybeRefreshMemoryBridge);
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Shared config factories
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/** Returns a minimal CleoConfig with brain.autoCapture and brain.captureWork enabled. */
|
|
57
|
+
function makeConfig(
|
|
58
|
+
overrides: {
|
|
59
|
+
autoCapture?: boolean;
|
|
60
|
+
captureWork?: boolean;
|
|
61
|
+
captureFiles?: boolean;
|
|
62
|
+
captureMcp?: boolean;
|
|
63
|
+
autoRefresh?: boolean;
|
|
64
|
+
} = {},
|
|
65
|
+
): Record<string, unknown> {
|
|
66
|
+
return {
|
|
67
|
+
brain: {
|
|
68
|
+
autoCapture: overrides.autoCapture ?? true,
|
|
69
|
+
captureWork: overrides.captureWork ?? false,
|
|
70
|
+
captureFiles: overrides.captureFiles ?? false,
|
|
71
|
+
captureMcp: overrides.captureMcp ?? false,
|
|
72
|
+
memoryBridge: { autoRefresh: overrides.autoRefresh ?? false },
|
|
73
|
+
embedding: { enabled: false, provider: 'local' },
|
|
74
|
+
summarization: { enabled: false },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const PROJECT_ROOT = '/tmp/e2e-test-project';
|
|
80
|
+
const TIMESTAMP = '2026-03-24T00:00:00.000Z';
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Test suite
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
describe('hook automation E2E', () => {
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
observeBrainMock.mockReset().mockResolvedValue(undefined);
|
|
89
|
+
loadConfigMock.mockReset().mockResolvedValue(makeConfig());
|
|
90
|
+
maybeRefreshMemoryBridgeMock.mockReset().mockResolvedValue(undefined);
|
|
91
|
+
// Clear work-capture env var
|
|
92
|
+
delete process.env['CLEO_BRAIN_CAPTURE_WORK'];
|
|
93
|
+
delete process.env['CLEO_BRAIN_CAPTURE_MCP'];
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
delete process.env['CLEO_BRAIN_CAPTURE_WORK'];
|
|
98
|
+
delete process.env['CLEO_BRAIN_CAPTURE_MCP'];
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// -------------------------------------------------------------------------
|
|
102
|
+
// 1. SessionStart dispatches and brain handler fires (bridge refresh)
|
|
103
|
+
// -------------------------------------------------------------------------
|
|
104
|
+
describe('SessionStart', () => {
|
|
105
|
+
it('fires brain observation on session start', async () => {
|
|
106
|
+
await handleSessionStart(PROJECT_ROOT, {
|
|
107
|
+
timestamp: TIMESTAMP,
|
|
108
|
+
sessionId: 'ses-e2e-1',
|
|
109
|
+
name: 'E2E Session',
|
|
110
|
+
scope: 'T168',
|
|
111
|
+
agent: 'claude-sonnet',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
116
|
+
PROJECT_ROOT,
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
title: 'Session start: E2E Session',
|
|
119
|
+
type: 'discovery',
|
|
120
|
+
sourceSessionId: 'ses-e2e-1',
|
|
121
|
+
sourceType: 'agent',
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('triggers memory bridge refresh after session start', async () => {
|
|
127
|
+
await handleSessionStart(PROJECT_ROOT, {
|
|
128
|
+
timestamp: TIMESTAMP,
|
|
129
|
+
sessionId: 'ses-e2e-refresh',
|
|
130
|
+
name: 'Bridge Refresh Test',
|
|
131
|
+
scope: 'global',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(maybeRefreshMemoryBridgeMock).toHaveBeenCalledWith(PROJECT_ROOT);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// -------------------------------------------------------------------------
|
|
139
|
+
// 2. SessionEnd dispatches and brain handler fires (summarization + bridge)
|
|
140
|
+
// -------------------------------------------------------------------------
|
|
141
|
+
describe('SessionEnd', () => {
|
|
142
|
+
it('fires brain observation on session end', async () => {
|
|
143
|
+
await handleSessionEnd(PROJECT_ROOT, {
|
|
144
|
+
timestamp: TIMESTAMP,
|
|
145
|
+
sessionId: 'ses-e2e-2',
|
|
146
|
+
duration: 1800,
|
|
147
|
+
tasksCompleted: ['T166', 'T168'],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
151
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
152
|
+
PROJECT_ROOT,
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
title: 'Session end: ses-e2e-2',
|
|
155
|
+
type: 'change',
|
|
156
|
+
sourceSessionId: 'ses-e2e-2',
|
|
157
|
+
sourceType: 'agent',
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('includes task list in session end observation text', async () => {
|
|
163
|
+
await handleSessionEnd(PROJECT_ROOT, {
|
|
164
|
+
timestamp: TIMESTAMP,
|
|
165
|
+
sessionId: 'ses-e2e-tasks',
|
|
166
|
+
duration: 600,
|
|
167
|
+
tasksCompleted: ['T100', 'T101'],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const callText = observeBrainMock.mock.calls[0][1].text as string;
|
|
171
|
+
expect(callText).toContain('T100');
|
|
172
|
+
expect(callText).toContain('T101');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('triggers memory bridge refresh after session end', async () => {
|
|
176
|
+
await handleSessionEnd(PROJECT_ROOT, {
|
|
177
|
+
timestamp: TIMESTAMP,
|
|
178
|
+
sessionId: 'ses-e2e-bridge',
|
|
179
|
+
duration: 300,
|
|
180
|
+
tasksCompleted: [],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(maybeRefreshMemoryBridgeMock).toHaveBeenCalledWith(PROJECT_ROOT);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// -------------------------------------------------------------------------
|
|
188
|
+
// 3. PreToolUse dispatches and brain handler fires (observation created)
|
|
189
|
+
// -------------------------------------------------------------------------
|
|
190
|
+
describe('PreToolUse', () => {
|
|
191
|
+
it('fires brain observation when a tool starts', async () => {
|
|
192
|
+
await handleToolStart(PROJECT_ROOT, {
|
|
193
|
+
timestamp: TIMESTAMP,
|
|
194
|
+
taskId: 'T168',
|
|
195
|
+
taskTitle: 'E2E integration tests',
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
199
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
200
|
+
PROJECT_ROOT,
|
|
201
|
+
expect.objectContaining({
|
|
202
|
+
text: 'Started work on T168: E2E integration tests',
|
|
203
|
+
title: 'Task start: T168',
|
|
204
|
+
type: 'change',
|
|
205
|
+
sourceType: 'agent',
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// -------------------------------------------------------------------------
|
|
212
|
+
// 4. PostToolUse dispatches and brain handler fires (completion observation)
|
|
213
|
+
// -------------------------------------------------------------------------
|
|
214
|
+
describe('PostToolUse', () => {
|
|
215
|
+
it('fires brain observation when a tool completes', async () => {
|
|
216
|
+
await handleToolComplete(PROJECT_ROOT, {
|
|
217
|
+
timestamp: TIMESTAMP,
|
|
218
|
+
taskId: 'T168',
|
|
219
|
+
taskTitle: 'E2E integration tests',
|
|
220
|
+
status: 'done',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
224
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
225
|
+
PROJECT_ROOT,
|
|
226
|
+
expect.objectContaining({
|
|
227
|
+
text: 'Task T168 completed with status: done',
|
|
228
|
+
title: 'Task complete: T168',
|
|
229
|
+
type: 'change',
|
|
230
|
+
sourceType: 'agent',
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('triggers memory bridge refresh after tool completes', async () => {
|
|
236
|
+
await handleToolComplete(PROJECT_ROOT, {
|
|
237
|
+
timestamp: TIMESTAMP,
|
|
238
|
+
taskId: 'T168',
|
|
239
|
+
taskTitle: 'E2E integration tests',
|
|
240
|
+
status: 'done',
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(maybeRefreshMemoryBridgeMock).toHaveBeenCalledWith(PROJECT_ROOT);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// -------------------------------------------------------------------------
|
|
248
|
+
// 5. PromptSubmit dispatches for mutations only (work-capture filter)
|
|
249
|
+
// -------------------------------------------------------------------------
|
|
250
|
+
describe('PromptSubmit (work-capture)', () => {
|
|
251
|
+
it('captures mutate operations in CAPTURE_OPERATIONS set', async () => {
|
|
252
|
+
process.env['CLEO_BRAIN_CAPTURE_WORK'] = 'true';
|
|
253
|
+
|
|
254
|
+
await handleWorkPromptSubmit(PROJECT_ROOT, {
|
|
255
|
+
timestamp: TIMESTAMP,
|
|
256
|
+
gateway: 'mutate',
|
|
257
|
+
domain: 'tasks',
|
|
258
|
+
operation: 'add',
|
|
259
|
+
source: 'agent-alpha',
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
263
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
264
|
+
PROJECT_ROOT,
|
|
265
|
+
expect.objectContaining({
|
|
266
|
+
title: 'Work intent: tasks.add',
|
|
267
|
+
type: 'discovery',
|
|
268
|
+
sourceType: 'agent',
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// -----------------------------------------------------------------------
|
|
274
|
+
// 6. PromptSubmit skips queries (smart filtering)
|
|
275
|
+
// -----------------------------------------------------------------------
|
|
276
|
+
it('skips query gateway operations', async () => {
|
|
277
|
+
process.env['CLEO_BRAIN_CAPTURE_WORK'] = 'true';
|
|
278
|
+
|
|
279
|
+
await handleWorkPromptSubmit(PROJECT_ROOT, {
|
|
280
|
+
timestamp: TIMESTAMP,
|
|
281
|
+
gateway: 'query',
|
|
282
|
+
domain: 'tasks',
|
|
283
|
+
operation: 'find',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('skips mutate operations NOT in CAPTURE_OPERATIONS (e.g. tasks.complete)', async () => {
|
|
290
|
+
process.env['CLEO_BRAIN_CAPTURE_WORK'] = 'true';
|
|
291
|
+
|
|
292
|
+
await handleWorkPromptSubmit(PROJECT_ROOT, {
|
|
293
|
+
timestamp: TIMESTAMP,
|
|
294
|
+
gateway: 'mutate',
|
|
295
|
+
domain: 'tasks',
|
|
296
|
+
operation: 'complete',
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// -------------------------------------------------------------------------
|
|
304
|
+
// 7. ResponseComplete dispatches for successes only
|
|
305
|
+
// -------------------------------------------------------------------------
|
|
306
|
+
describe('ResponseComplete (work-capture)', () => {
|
|
307
|
+
it('captures successful mutate operations in CAPTURE_OPERATIONS', async () => {
|
|
308
|
+
process.env['CLEO_BRAIN_CAPTURE_WORK'] = 'true';
|
|
309
|
+
|
|
310
|
+
await handleWorkResponseComplete(PROJECT_ROOT, {
|
|
311
|
+
timestamp: TIMESTAMP,
|
|
312
|
+
gateway: 'mutate',
|
|
313
|
+
domain: 'tasks',
|
|
314
|
+
operation: 'add',
|
|
315
|
+
success: true,
|
|
316
|
+
durationMs: 42,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
320
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
321
|
+
PROJECT_ROOT,
|
|
322
|
+
expect.objectContaining({
|
|
323
|
+
title: 'Work done: tasks.add',
|
|
324
|
+
type: 'change',
|
|
325
|
+
sourceType: 'agent',
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('skips failed operations', async () => {
|
|
331
|
+
process.env['CLEO_BRAIN_CAPTURE_WORK'] = 'true';
|
|
332
|
+
|
|
333
|
+
await handleWorkResponseComplete(PROJECT_ROOT, {
|
|
334
|
+
timestamp: TIMESTAMP,
|
|
335
|
+
gateway: 'mutate',
|
|
336
|
+
domain: 'tasks',
|
|
337
|
+
operation: 'add',
|
|
338
|
+
success: false,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// -------------------------------------------------------------------------
|
|
346
|
+
// 8. SubagentStart creates brain observation
|
|
347
|
+
// -------------------------------------------------------------------------
|
|
348
|
+
describe('SubagentStart', () => {
|
|
349
|
+
it('creates brain observation when subagent spawns', async () => {
|
|
350
|
+
await handleSubagentStart(PROJECT_ROOT, {
|
|
351
|
+
timestamp: TIMESTAMP,
|
|
352
|
+
agentId: 'agent-worker-1',
|
|
353
|
+
role: 'implementer',
|
|
354
|
+
taskId: 'T166',
|
|
355
|
+
sessionId: 'ses-e2e-1',
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
359
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
360
|
+
PROJECT_ROOT,
|
|
361
|
+
expect.objectContaining({
|
|
362
|
+
title: 'Subagent start: agent-worker-1',
|
|
363
|
+
type: 'discovery',
|
|
364
|
+
sourceType: 'agent',
|
|
365
|
+
sourceSessionId: 'ses-e2e-1',
|
|
366
|
+
}),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const callText = observeBrainMock.mock.calls[0][1].text as string;
|
|
370
|
+
expect(callText).toContain('agent-worker-1');
|
|
371
|
+
expect(callText).toContain('role=implementer');
|
|
372
|
+
expect(callText).toContain('task=T166');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('creates observation with minimal payload (no role or task)', async () => {
|
|
376
|
+
await handleSubagentStart(PROJECT_ROOT, {
|
|
377
|
+
timestamp: TIMESTAMP,
|
|
378
|
+
agentId: 'agent-minimal',
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
382
|
+
const callText = observeBrainMock.mock.calls[0][1].text as string;
|
|
383
|
+
expect(callText).toContain('agent-minimal');
|
|
384
|
+
expect(callText).not.toContain('role=');
|
|
385
|
+
expect(callText).not.toContain('task=');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('creates observation for SubagentStop with completion status', async () => {
|
|
389
|
+
await handleSubagentStop(PROJECT_ROOT, {
|
|
390
|
+
timestamp: TIMESTAMP,
|
|
391
|
+
agentId: 'agent-worker-1',
|
|
392
|
+
status: 'complete',
|
|
393
|
+
taskId: 'T166',
|
|
394
|
+
summary: 'All handlers wired',
|
|
395
|
+
sessionId: 'ses-e2e-1',
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
399
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
400
|
+
PROJECT_ROOT,
|
|
401
|
+
expect.objectContaining({
|
|
402
|
+
title: 'Subagent stop: agent-worker-1',
|
|
403
|
+
type: 'change',
|
|
404
|
+
sourceType: 'agent',
|
|
405
|
+
sourceSessionId: 'ses-e2e-1',
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const callText = observeBrainMock.mock.calls[0][1].text as string;
|
|
410
|
+
expect(callText).toContain('status=complete');
|
|
411
|
+
expect(callText).toContain('task=T166');
|
|
412
|
+
expect(callText).toContain('All handlers wired');
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// -------------------------------------------------------------------------
|
|
417
|
+
// 9. PreCompact creates context snapshot observation
|
|
418
|
+
// -------------------------------------------------------------------------
|
|
419
|
+
describe('PreCompact', () => {
|
|
420
|
+
it('creates context snapshot observation before compaction', async () => {
|
|
421
|
+
await handlePreCompact(PROJECT_ROOT, {
|
|
422
|
+
timestamp: TIMESTAMP,
|
|
423
|
+
tokensBefore: 80000,
|
|
424
|
+
reason: 'context-limit',
|
|
425
|
+
sessionId: 'ses-e2e-1',
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
429
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
430
|
+
PROJECT_ROOT,
|
|
431
|
+
expect.objectContaining({
|
|
432
|
+
title: 'Pre-compaction context snapshot',
|
|
433
|
+
type: 'discovery',
|
|
434
|
+
sourceType: 'agent',
|
|
435
|
+
sourceSessionId: 'ses-e2e-1',
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const callText = observeBrainMock.mock.calls[0][1].text as string;
|
|
440
|
+
expect(callText).toContain('80,000');
|
|
441
|
+
expect(callText).toContain('context-limit');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('creates PostCompact record after compaction', async () => {
|
|
445
|
+
await handlePostCompact(PROJECT_ROOT, {
|
|
446
|
+
timestamp: TIMESTAMP,
|
|
447
|
+
tokensBefore: 80000,
|
|
448
|
+
tokensAfter: 20000,
|
|
449
|
+
success: true,
|
|
450
|
+
sessionId: 'ses-e2e-1',
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
454
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
455
|
+
PROJECT_ROOT,
|
|
456
|
+
expect.objectContaining({
|
|
457
|
+
title: 'Post-compaction record',
|
|
458
|
+
type: 'change',
|
|
459
|
+
sourceType: 'agent',
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const callText = observeBrainMock.mock.calls[0][1].text as string;
|
|
464
|
+
expect(callText).toContain('succeeded');
|
|
465
|
+
expect(callText).toContain('80,000');
|
|
466
|
+
expect(callText).toContain('20,000');
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// -------------------------------------------------------------------------
|
|
471
|
+
// 10. Config gating: handler skips when brain.autoCapture=false
|
|
472
|
+
// -------------------------------------------------------------------------
|
|
473
|
+
describe('config gating', () => {
|
|
474
|
+
it('SubagentStart skips when brain.autoCapture=false', async () => {
|
|
475
|
+
loadConfigMock.mockResolvedValue(makeConfig({ autoCapture: false }));
|
|
476
|
+
|
|
477
|
+
await handleSubagentStart(PROJECT_ROOT, {
|
|
478
|
+
timestamp: TIMESTAMP,
|
|
479
|
+
agentId: 'agent-gated',
|
|
480
|
+
role: 'tester',
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('PreCompact skips when brain.autoCapture=false', async () => {
|
|
487
|
+
loadConfigMock.mockResolvedValue(makeConfig({ autoCapture: false }));
|
|
488
|
+
|
|
489
|
+
await handlePreCompact(PROJECT_ROOT, {
|
|
490
|
+
timestamp: TIMESTAMP,
|
|
491
|
+
tokensBefore: 50000,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('PostCompact skips when brain.autoCapture=false', async () => {
|
|
498
|
+
loadConfigMock.mockResolvedValue(makeConfig({ autoCapture: false }));
|
|
499
|
+
|
|
500
|
+
await handlePostCompact(PROJECT_ROOT, {
|
|
501
|
+
timestamp: TIMESTAMP,
|
|
502
|
+
success: true,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('SubagentStop skips when brain.autoCapture=false', async () => {
|
|
509
|
+
loadConfigMock.mockResolvedValue(makeConfig({ autoCapture: false }));
|
|
510
|
+
|
|
511
|
+
await handleSubagentStop(PROJECT_ROOT, {
|
|
512
|
+
timestamp: TIMESTAMP,
|
|
513
|
+
agentId: 'agent-gated',
|
|
514
|
+
status: 'complete',
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('work-capture skips when captureWork=false and env not set', async () => {
|
|
521
|
+
// No env override + captureWork=false in config
|
|
522
|
+
loadConfigMock.mockResolvedValue(makeConfig({ captureWork: false }));
|
|
523
|
+
|
|
524
|
+
await handleWorkPromptSubmit(PROJECT_ROOT, {
|
|
525
|
+
timestamp: TIMESTAMP,
|
|
526
|
+
gateway: 'mutate',
|
|
527
|
+
domain: 'tasks',
|
|
528
|
+
operation: 'add',
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// -------------------------------------------------------------------------
|
|
536
|
+
// 11. Dedup: PostToolUse doesn't double-capture what session-hooks handles
|
|
537
|
+
// -------------------------------------------------------------------------
|
|
538
|
+
describe('dedup (no double-capture)', () => {
|
|
539
|
+
it('PostToolUse and SessionEnd are separate calls — no overlap', async () => {
|
|
540
|
+
// Simulate a session ending after a task completes
|
|
541
|
+
await handleToolComplete(PROJECT_ROOT, {
|
|
542
|
+
timestamp: TIMESTAMP,
|
|
543
|
+
taskId: 'T168',
|
|
544
|
+
taskTitle: 'E2E tests',
|
|
545
|
+
status: 'done',
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
observeBrainMock.mockClear();
|
|
549
|
+
|
|
550
|
+
await handleSessionEnd(PROJECT_ROOT, {
|
|
551
|
+
timestamp: TIMESTAMP,
|
|
552
|
+
sessionId: 'ses-dedup',
|
|
553
|
+
duration: 300,
|
|
554
|
+
tasksCompleted: ['T168'],
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Each handler fires exactly once — no double-capture for the same event
|
|
558
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
559
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
560
|
+
PROJECT_ROOT,
|
|
561
|
+
expect.objectContaining({ title: 'Session end: ses-dedup' }),
|
|
562
|
+
);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('work-capture and mcp-hooks register on same event but use different config keys', async () => {
|
|
566
|
+
// work-capture is keyed on captureWork; mcp-hooks keyed on captureMcp
|
|
567
|
+
// When both are disabled (default), neither fires
|
|
568
|
+
await handleWorkPromptSubmit(PROJECT_ROOT, {
|
|
569
|
+
timestamp: TIMESTAMP,
|
|
570
|
+
gateway: 'mutate',
|
|
571
|
+
domain: 'tasks',
|
|
572
|
+
operation: 'add',
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// -------------------------------------------------------------------------
|
|
580
|
+
// 12. Notification — system notification captured as observation
|
|
581
|
+
// -------------------------------------------------------------------------
|
|
582
|
+
describe('Notification (system)', () => {
|
|
583
|
+
it('captures message-bearing system notifications', async () => {
|
|
584
|
+
await handleSystemNotification(PROJECT_ROOT, {
|
|
585
|
+
timestamp: TIMESTAMP,
|
|
586
|
+
message: 'CLEO session limit approaching (80% used)',
|
|
587
|
+
sessionId: 'ses-e2e-1',
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
expect(observeBrainMock).toHaveBeenCalledTimes(1);
|
|
591
|
+
expect(observeBrainMock).toHaveBeenCalledWith(
|
|
592
|
+
PROJECT_ROOT,
|
|
593
|
+
expect.objectContaining({
|
|
594
|
+
type: 'discovery',
|
|
595
|
+
sourceType: 'agent',
|
|
596
|
+
sourceSessionId: 'ses-e2e-1',
|
|
597
|
+
}),
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
const callText = observeBrainMock.mock.calls[0][1].text as string;
|
|
601
|
+
expect(callText).toContain('CLEO session limit approaching');
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('skips file-change notifications (handled by file-hooks)', async () => {
|
|
605
|
+
await handleSystemNotification(PROJECT_ROOT, {
|
|
606
|
+
timestamp: TIMESTAMP,
|
|
607
|
+
filePath: 'src/core/tasks.ts',
|
|
608
|
+
changeType: 'write',
|
|
609
|
+
message: 'some extra message',
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('skips notifications with no message and no filePath', async () => {
|
|
616
|
+
await handleSystemNotification(PROJECT_ROOT, {
|
|
617
|
+
timestamp: TIMESTAMP,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('skips when brain.autoCapture=false', async () => {
|
|
624
|
+
loadConfigMock.mockResolvedValue(makeConfig({ autoCapture: false }));
|
|
625
|
+
|
|
626
|
+
await handleSystemNotification(PROJECT_ROOT, {
|
|
627
|
+
timestamp: TIMESTAMP,
|
|
628
|
+
message: 'Should be skipped',
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
expect(observeBrainMock).not.toHaveBeenCalled();
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
});
|