@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.
Files changed (48) hide show
  1. package/dist/hooks/handlers/agent-hooks.d.ts +48 -0
  2. package/dist/hooks/handlers/agent-hooks.d.ts.map +1 -0
  3. package/dist/hooks/handlers/context-hooks.d.ts +53 -0
  4. package/dist/hooks/handlers/context-hooks.d.ts.map +1 -0
  5. package/dist/hooks/handlers/error-hooks.d.ts +4 -4
  6. package/dist/hooks/handlers/error-hooks.d.ts.map +1 -1
  7. package/dist/hooks/handlers/file-hooks.d.ts +3 -3
  8. package/dist/hooks/handlers/file-hooks.d.ts.map +1 -1
  9. package/dist/hooks/handlers/index.d.ts +8 -1
  10. package/dist/hooks/handlers/index.d.ts.map +1 -1
  11. package/dist/hooks/handlers/mcp-hooks.d.ts +29 -7
  12. package/dist/hooks/handlers/mcp-hooks.d.ts.map +1 -1
  13. package/dist/hooks/handlers/session-hooks.d.ts +5 -5
  14. package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
  15. package/dist/hooks/handlers/task-hooks.d.ts +5 -5
  16. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  17. package/dist/hooks/handlers/work-capture-hooks.d.ts +7 -7
  18. package/dist/hooks/handlers/work-capture-hooks.d.ts.map +1 -1
  19. package/dist/hooks/payload-schemas.d.ts +177 -11
  20. package/dist/hooks/payload-schemas.d.ts.map +1 -1
  21. package/dist/hooks/provider-hooks.d.ts +33 -7
  22. package/dist/hooks/provider-hooks.d.ts.map +1 -1
  23. package/dist/hooks/registry.d.ts +26 -6
  24. package/dist/hooks/registry.d.ts.map +1 -1
  25. package/dist/hooks/types.d.ts +132 -38
  26. package/dist/hooks/types.d.ts.map +1 -1
  27. package/dist/index.js +335 -59
  28. package/dist/index.js.map +4 -4
  29. package/dist/sessions/snapshot.d.ts.map +1 -1
  30. package/package.json +6 -6
  31. package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +634 -0
  32. package/src/hooks/handlers/agent-hooks.ts +148 -0
  33. package/src/hooks/handlers/context-hooks.ts +156 -0
  34. package/src/hooks/handlers/error-hooks.ts +8 -5
  35. package/src/hooks/handlers/file-hooks.ts +6 -4
  36. package/src/hooks/handlers/index.ts +12 -1
  37. package/src/hooks/handlers/mcp-hooks.ts +74 -9
  38. package/src/hooks/handlers/session-hooks.ts +7 -7
  39. package/src/hooks/handlers/task-hooks.ts +7 -7
  40. package/src/hooks/handlers/work-capture-hooks.ts +12 -12
  41. package/src/hooks/payload-schemas.ts +96 -26
  42. package/src/hooks/provider-hooks.ts +50 -9
  43. package/src/hooks/registry.ts +86 -23
  44. package/src/hooks/types.ts +175 -39
  45. package/src/sessions/index.ts +4 -4
  46. package/src/sessions/snapshot.ts +4 -2
  47. package/src/store/json.ts +2 -2
  48. 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,CAoG1B;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"}
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.72",
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.8.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.72",
41
- "@cleocode/agents": "2026.3.72",
42
- "@cleocode/contracts": "2026.3.72",
43
- "@cleocode/skills": "2026.3.72"
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
+ });