@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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent (Subagent) Lifecycle Hook Handlers
|
|
3
|
+
*
|
|
4
|
+
* Captures SubagentStart and SubagentStop events to BRAIN so that
|
|
5
|
+
* multi-agent orchestration runs leave an auditable trail of which
|
|
6
|
+
* subagents were spawned, what tasks they were assigned, and how
|
|
7
|
+
* they completed.
|
|
8
|
+
*
|
|
9
|
+
* Gated behind brain.autoCapture config. Never throws — all errors are
|
|
10
|
+
* swallowed so that brain capture never blocks agent orchestration.
|
|
11
|
+
*
|
|
12
|
+
* Auto-registers on module load.
|
|
13
|
+
*
|
|
14
|
+
* @task T166
|
|
15
|
+
* @epic T134
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { hooks } from '../registry.js';
|
|
19
|
+
import type { SubagentStartPayload, SubagentStopPayload } from '../types.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Shared helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
function isMissingBrainSchemaError(err: unknown): boolean {
|
|
26
|
+
if (!(err instanceof Error)) return false;
|
|
27
|
+
const message = String(err.message || '').toLowerCase();
|
|
28
|
+
return message.includes('no such table') && message.includes('brain_');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check whether brain auto-capture is enabled.
|
|
33
|
+
*
|
|
34
|
+
* Resolution order (first truthy wins):
|
|
35
|
+
* 1. brain.autoCapture project config value (via loadConfig cascade)
|
|
36
|
+
*
|
|
37
|
+
* Defaults to false when config is unreadable.
|
|
38
|
+
*
|
|
39
|
+
* @param projectRoot - Absolute path to the project root directory.
|
|
40
|
+
*/
|
|
41
|
+
async function isAutoCaptureEnabled(projectRoot: string): Promise<boolean> {
|
|
42
|
+
try {
|
|
43
|
+
const { loadConfig } = await import('../../config.js');
|
|
44
|
+
const config = await loadConfig(projectRoot);
|
|
45
|
+
return config.brain?.autoCapture ?? false;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Handlers
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handle SubagentStart — log subagent spawn as a BRAIN observation.
|
|
57
|
+
*
|
|
58
|
+
* Records the agent ID, role, and task assignment so orchestrators can
|
|
59
|
+
* trace which agents were active in a given session.
|
|
60
|
+
*
|
|
61
|
+
* Gated behind brain.autoCapture config. Never throws.
|
|
62
|
+
*
|
|
63
|
+
* @param projectRoot - Absolute path to the project root directory.
|
|
64
|
+
* @param payload - SubagentStart event payload.
|
|
65
|
+
*
|
|
66
|
+
* @task T166
|
|
67
|
+
* @epic T134
|
|
68
|
+
*/
|
|
69
|
+
export async function handleSubagentStart(
|
|
70
|
+
projectRoot: string,
|
|
71
|
+
payload: SubagentStartPayload,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
if (!(await isAutoCaptureEnabled(projectRoot))) return;
|
|
74
|
+
|
|
75
|
+
const { observeBrain } = await import('../../memory/brain-retrieval.js');
|
|
76
|
+
|
|
77
|
+
const rolePart = payload.role ? ` role=${payload.role}` : '';
|
|
78
|
+
const taskPart = payload.taskId ? ` task=${payload.taskId}` : '';
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await observeBrain(projectRoot, {
|
|
82
|
+
text: `Subagent spawned: ${payload.agentId}${rolePart}${taskPart}`,
|
|
83
|
+
title: `Subagent start: ${payload.agentId}`,
|
|
84
|
+
type: 'discovery',
|
|
85
|
+
sourceSessionId: payload.sessionId,
|
|
86
|
+
sourceType: 'agent',
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (!isMissingBrainSchemaError(err)) throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Handle SubagentStop — log subagent completion result as a BRAIN observation.
|
|
95
|
+
*
|
|
96
|
+
* Records the agent ID, completion status, assigned task, and optional
|
|
97
|
+
* summary reference so orchestrators can audit subagent outcomes.
|
|
98
|
+
*
|
|
99
|
+
* Gated behind brain.autoCapture config. Never throws.
|
|
100
|
+
*
|
|
101
|
+
* @param projectRoot - Absolute path to the project root directory.
|
|
102
|
+
* @param payload - SubagentStop event payload.
|
|
103
|
+
*
|
|
104
|
+
* @task T166
|
|
105
|
+
* @epic T134
|
|
106
|
+
*/
|
|
107
|
+
export async function handleSubagentStop(
|
|
108
|
+
projectRoot: string,
|
|
109
|
+
payload: SubagentStopPayload,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
if (!(await isAutoCaptureEnabled(projectRoot))) return;
|
|
112
|
+
|
|
113
|
+
const { observeBrain } = await import('../../memory/brain-retrieval.js');
|
|
114
|
+
|
|
115
|
+
const statusPart = payload.status ? ` status=${payload.status}` : '';
|
|
116
|
+
const taskPart = payload.taskId ? ` task=${payload.taskId}` : '';
|
|
117
|
+
const summaryPart = payload.summary ? `\nSummary: ${payload.summary}` : '';
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await observeBrain(projectRoot, {
|
|
121
|
+
text: `Subagent completed: ${payload.agentId}${statusPart}${taskPart}${summaryPart}`,
|
|
122
|
+
title: `Subagent stop: ${payload.agentId}`,
|
|
123
|
+
type: 'change',
|
|
124
|
+
sourceSessionId: payload.sessionId,
|
|
125
|
+
sourceType: 'agent',
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (!isMissingBrainSchemaError(err)) throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Auto-registration
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
hooks.register({
|
|
137
|
+
id: 'brain-subagent-start',
|
|
138
|
+
event: 'SubagentStart',
|
|
139
|
+
handler: handleSubagentStart,
|
|
140
|
+
priority: 100,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
hooks.register({
|
|
144
|
+
id: 'brain-subagent-stop',
|
|
145
|
+
event: 'SubagentStop',
|
|
146
|
+
handler: handleSubagentStop,
|
|
147
|
+
priority: 100,
|
|
148
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Compaction Hook Handlers
|
|
3
|
+
*
|
|
4
|
+
* Captures PreCompact and PostCompact events to BRAIN so that agents
|
|
5
|
+
* can later observe when context compactions occurred and what state
|
|
6
|
+
* preceded them. This is especially useful for long sessions where
|
|
7
|
+
* context is compacted multiple times.
|
|
8
|
+
*
|
|
9
|
+
* PreCompact saves a session context snapshot before compaction begins.
|
|
10
|
+
* PostCompact records that compaction occurred and the resulting token counts.
|
|
11
|
+
*
|
|
12
|
+
* Gated behind brain.autoCapture config. Never throws — all errors are
|
|
13
|
+
* swallowed so brain capture never blocks context compaction.
|
|
14
|
+
*
|
|
15
|
+
* Auto-registers on module load.
|
|
16
|
+
*
|
|
17
|
+
* @task T166
|
|
18
|
+
* @epic T134
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { hooks } from '../registry.js';
|
|
22
|
+
import type { PostCompactPayload, PreCompactPayload } from '../types.js';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Shared helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
function isMissingBrainSchemaError(err: unknown): boolean {
|
|
29
|
+
if (!(err instanceof Error)) return false;
|
|
30
|
+
const message = String(err.message || '').toLowerCase();
|
|
31
|
+
return message.includes('no such table') && message.includes('brain_');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check whether brain auto-capture is enabled.
|
|
36
|
+
*
|
|
37
|
+
* Resolution order (first truthy wins):
|
|
38
|
+
* 1. brain.autoCapture project config value (via loadConfig cascade)
|
|
39
|
+
*
|
|
40
|
+
* Defaults to false when config is unreadable.
|
|
41
|
+
*
|
|
42
|
+
* @param projectRoot - Absolute path to the project root directory.
|
|
43
|
+
*/
|
|
44
|
+
async function isAutoCaptureEnabled(projectRoot: string): Promise<boolean> {
|
|
45
|
+
try {
|
|
46
|
+
const { loadConfig } = await import('../../config.js');
|
|
47
|
+
const config = await loadConfig(projectRoot);
|
|
48
|
+
return config.brain?.autoCapture ?? false;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Handlers
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handle PreCompact — snapshot current session memory context to BRAIN.
|
|
60
|
+
*
|
|
61
|
+
* Fires immediately before context compaction begins. Records the token
|
|
62
|
+
* count and compaction reason so the brain retains context about what
|
|
63
|
+
* was in scope before compaction.
|
|
64
|
+
*
|
|
65
|
+
* Gated behind brain.autoCapture config. Never throws.
|
|
66
|
+
*
|
|
67
|
+
* @param projectRoot - Absolute path to the project root directory.
|
|
68
|
+
* @param payload - PreCompact event payload.
|
|
69
|
+
*
|
|
70
|
+
* @task T166
|
|
71
|
+
* @epic T134
|
|
72
|
+
*/
|
|
73
|
+
export async function handlePreCompact(
|
|
74
|
+
projectRoot: string,
|
|
75
|
+
payload: PreCompactPayload,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
if (!(await isAutoCaptureEnabled(projectRoot))) return;
|
|
78
|
+
|
|
79
|
+
const { observeBrain } = await import('../../memory/brain-retrieval.js');
|
|
80
|
+
|
|
81
|
+
const tokensPart =
|
|
82
|
+
payload.tokensBefore != null ? ` (~${payload.tokensBefore.toLocaleString()} tokens)` : '';
|
|
83
|
+
const reasonPart = payload.reason ? ` Reason: ${payload.reason}` : '';
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await observeBrain(projectRoot, {
|
|
87
|
+
text: `Context compaction about to begin${tokensPart}.${reasonPart}`,
|
|
88
|
+
title: 'Pre-compaction context snapshot',
|
|
89
|
+
type: 'discovery',
|
|
90
|
+
sourceSessionId: payload.sessionId,
|
|
91
|
+
sourceType: 'agent',
|
|
92
|
+
});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (!isMissingBrainSchemaError(err)) throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handle PostCompact — record compaction completion to BRAIN.
|
|
100
|
+
*
|
|
101
|
+
* Fires immediately after context compaction completes. Records the
|
|
102
|
+
* before/after token counts so agents can correlate observations made
|
|
103
|
+
* before compaction with those made after.
|
|
104
|
+
*
|
|
105
|
+
* Gated behind brain.autoCapture config. Never throws.
|
|
106
|
+
*
|
|
107
|
+
* @param projectRoot - Absolute path to the project root directory.
|
|
108
|
+
* @param payload - PostCompact event payload.
|
|
109
|
+
*
|
|
110
|
+
* @task T166
|
|
111
|
+
* @epic T134
|
|
112
|
+
*/
|
|
113
|
+
export async function handlePostCompact(
|
|
114
|
+
projectRoot: string,
|
|
115
|
+
payload: PostCompactPayload,
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
if (!(await isAutoCaptureEnabled(projectRoot))) return;
|
|
118
|
+
|
|
119
|
+
const { observeBrain } = await import('../../memory/brain-retrieval.js');
|
|
120
|
+
|
|
121
|
+
const statusPart = payload.success ? 'succeeded' : 'failed';
|
|
122
|
+
const beforePart =
|
|
123
|
+
payload.tokensBefore != null ? ` before=${payload.tokensBefore.toLocaleString()}` : '';
|
|
124
|
+
const afterPart =
|
|
125
|
+
payload.tokensAfter != null ? ` after=${payload.tokensAfter.toLocaleString()}` : '';
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await observeBrain(projectRoot, {
|
|
129
|
+
text: `Context compaction ${statusPart}${beforePart}${afterPart}`,
|
|
130
|
+
title: 'Post-compaction record',
|
|
131
|
+
type: 'change',
|
|
132
|
+
sourceSessionId: payload.sessionId,
|
|
133
|
+
sourceType: 'agent',
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (!isMissingBrainSchemaError(err)) throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Auto-registration
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
hooks.register({
|
|
145
|
+
id: 'brain-pre-compact',
|
|
146
|
+
event: 'PreCompact',
|
|
147
|
+
handler: handlePreCompact,
|
|
148
|
+
priority: 100,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
hooks.register({
|
|
152
|
+
id: 'brain-post-compact',
|
|
153
|
+
event: 'PostCompact',
|
|
154
|
+
handler: handlePostCompact,
|
|
155
|
+
priority: 100,
|
|
156
|
+
});
|
|
@@ -7,17 +7,20 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { hooks } from '../registry.js';
|
|
10
|
-
import type {
|
|
10
|
+
import type { PostToolUseFailurePayload } from '../types.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Handle
|
|
13
|
+
* Handle PostToolUseFailure - capture operation errors to BRAIN
|
|
14
14
|
*
|
|
15
15
|
* Includes infinite-loop guard: if the payload has _fromHook marker,
|
|
16
|
-
* the handler skips to prevent
|
|
16
|
+
* the handler skips to prevent PostToolUseFailure -> observeBrain -> PostToolUseFailure loops.
|
|
17
17
|
* Additionally, ALL observeBrain errors are silently suppressed to prevent
|
|
18
18
|
* re-entrant hook firing.
|
|
19
19
|
*/
|
|
20
|
-
export async function handleError(
|
|
20
|
+
export async function handleError(
|
|
21
|
+
projectRoot: string,
|
|
22
|
+
payload: PostToolUseFailurePayload,
|
|
23
|
+
): Promise<void> {
|
|
21
24
|
// Infinite-loop guard: skip if this error originated from a hook
|
|
22
25
|
if (payload.metadata?.['_fromHook']) return;
|
|
23
26
|
|
|
@@ -42,7 +45,7 @@ export async function handleError(projectRoot: string, payload: OnErrorPayload):
|
|
|
42
45
|
// Register handler on module load
|
|
43
46
|
hooks.register({
|
|
44
47
|
id: 'brain-error',
|
|
45
|
-
event: '
|
|
48
|
+
event: 'PostToolUseFailure',
|
|
46
49
|
handler: handleError,
|
|
47
50
|
priority: 100,
|
|
48
51
|
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { isAbsolute, relative } from 'node:path';
|
|
14
14
|
import { hooks } from '../registry.js';
|
|
15
|
-
import type {
|
|
15
|
+
import type { NotificationPayload } from '../types.js';
|
|
16
16
|
|
|
17
17
|
function isMissingBrainSchemaError(err: unknown): boolean {
|
|
18
18
|
if (!(err instanceof Error)) return false;
|
|
@@ -65,7 +65,7 @@ async function isFileCaptureEnabled(projectRoot: string): Promise<boolean> {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* Handle
|
|
68
|
+
* Handle Notification (file-change variant) - capture file changes to BRAIN
|
|
69
69
|
*
|
|
70
70
|
* Gated behind brain.captureFiles config or CLEO_BRAIN_CAPTURE_FILES env var.
|
|
71
71
|
* Env var takes precedence over config for backward compatibility.
|
|
@@ -75,8 +75,10 @@ async function isFileCaptureEnabled(projectRoot: string): Promise<boolean> {
|
|
|
75
75
|
*/
|
|
76
76
|
export async function handleFileChange(
|
|
77
77
|
projectRoot: string,
|
|
78
|
-
payload:
|
|
78
|
+
payload: NotificationPayload,
|
|
79
79
|
): Promise<void> {
|
|
80
|
+
// Only handle file-change notifications
|
|
81
|
+
if (!payload.filePath || !payload.changeType) return;
|
|
80
82
|
// Opt-in gate: disabled by default to prevent observation noise
|
|
81
83
|
if (!(await isFileCaptureEnabled(projectRoot))) return;
|
|
82
84
|
|
|
@@ -111,7 +113,7 @@ export async function handleFileChange(
|
|
|
111
113
|
// Register handler on module load
|
|
112
114
|
hooks.register({
|
|
113
115
|
id: 'brain-file-change',
|
|
114
|
-
event: '
|
|
116
|
+
event: 'Notification',
|
|
115
117
|
handler: handleFileChange,
|
|
116
118
|
priority: 100,
|
|
117
119
|
});
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Barrel export for all hook handlers. Importing this module will
|
|
5
5
|
* auto-register all handlers with the hook registry.
|
|
6
|
+
*
|
|
7
|
+
* @task T166
|
|
8
|
+
* @epic T134
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
// Import handlers to trigger auto-registration on module load
|
|
@@ -12,10 +15,18 @@ import './error-hooks.js';
|
|
|
12
15
|
import './file-hooks.js';
|
|
13
16
|
import './mcp-hooks.js';
|
|
14
17
|
import './work-capture-hooks.js';
|
|
18
|
+
import './agent-hooks.js';
|
|
19
|
+
import './context-hooks.js';
|
|
15
20
|
|
|
21
|
+
export { handleSubagentStart, handleSubagentStop } from './agent-hooks.js';
|
|
22
|
+
export { handlePostCompact, handlePreCompact } from './context-hooks.js';
|
|
16
23
|
export { handleError } from './error-hooks.js';
|
|
17
24
|
export { handleFileChange } from './file-hooks.js';
|
|
18
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
handlePromptSubmit,
|
|
27
|
+
handleResponseComplete,
|
|
28
|
+
handleSystemNotification,
|
|
29
|
+
} from './mcp-hooks.js';
|
|
19
30
|
// Re-export handler functions for explicit use
|
|
20
31
|
export { handleSessionEnd, handleSessionStart } from './session-hooks.js';
|
|
21
32
|
export { handleToolComplete, handleToolStart } from './task-hooks.js';
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP Prompt/Response Hook Handlers - Wave 2 of T5237
|
|
3
3
|
*
|
|
4
|
-
* Handlers for onPromptSubmit
|
|
5
|
-
* ALL gateway operations (read and write) to BRAIN.
|
|
4
|
+
* Handlers for onPromptSubmit, onResponseComplete, and system Notification
|
|
5
|
+
* events that capture ALL gateway operations (read and write) to BRAIN.
|
|
6
6
|
* By default, NO brain capture (too noisy). Enable via:
|
|
7
7
|
* - Config: brain.captureMcp = true (checked first)
|
|
8
8
|
* - Env: CLEO_BRAIN_CAPTURE_MCP=true (overrides config)
|
|
9
9
|
* Auto-registers on module load.
|
|
10
|
+
*
|
|
11
|
+
* Note: File-change Notification events (those with filePath + changeType) are
|
|
12
|
+
* handled by file-hooks.ts. This module handles message-bearing system
|
|
13
|
+
* notifications (Notification payloads with a message field).
|
|
14
|
+
*
|
|
15
|
+
* @task T166
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
import { hooks } from '../registry.js';
|
|
13
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
NotificationPayload,
|
|
21
|
+
PromptSubmitPayload,
|
|
22
|
+
ResponseCompletePayload,
|
|
23
|
+
} from '../types.js';
|
|
14
24
|
|
|
15
25
|
function isMissingBrainSchemaError(err: unknown): boolean {
|
|
16
26
|
if (!(err instanceof Error)) return false;
|
|
@@ -42,14 +52,14 @@ async function isBrainCaptureEnabled(projectRoot: string): Promise<boolean> {
|
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
/**
|
|
45
|
-
* Handle
|
|
55
|
+
* Handle PromptSubmit - optionally capture ALL gateway prompt events to BRAIN.
|
|
46
56
|
*
|
|
47
57
|
* No-op by default. Enable via brain.captureMcp config or CLEO_BRAIN_CAPTURE_MCP env.
|
|
48
58
|
* For selective mutation-only capture, use work-capture-hooks.ts instead.
|
|
49
59
|
*/
|
|
50
60
|
export async function handlePromptSubmit(
|
|
51
61
|
projectRoot: string,
|
|
52
|
-
payload:
|
|
62
|
+
payload: PromptSubmitPayload,
|
|
53
63
|
): Promise<void> {
|
|
54
64
|
if (!(await isBrainCaptureEnabled(projectRoot))) return;
|
|
55
65
|
|
|
@@ -68,14 +78,14 @@ export async function handlePromptSubmit(
|
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
/**
|
|
71
|
-
* Handle
|
|
81
|
+
* Handle ResponseComplete - optionally capture ALL gateway response events to BRAIN.
|
|
72
82
|
*
|
|
73
83
|
* No-op by default. Enable via brain.captureMcp config or CLEO_BRAIN_CAPTURE_MCP env.
|
|
74
84
|
* For selective mutation-only capture, use work-capture-hooks.ts instead.
|
|
75
85
|
*/
|
|
76
86
|
export async function handleResponseComplete(
|
|
77
87
|
projectRoot: string,
|
|
78
|
-
payload:
|
|
88
|
+
payload: ResponseCompletePayload,
|
|
79
89
|
): Promise<void> {
|
|
80
90
|
if (!(await isBrainCaptureEnabled(projectRoot))) return;
|
|
81
91
|
|
|
@@ -93,17 +103,72 @@ export async function handleResponseComplete(
|
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Handle Notification — capture system notifications as BRAIN observations.
|
|
108
|
+
*
|
|
109
|
+
* Only fires for Notification payloads that carry a message field (i.e. system
|
|
110
|
+
* notifications). File-change notifications (filePath + changeType) are
|
|
111
|
+
* handled exclusively by file-hooks.ts and are skipped here to avoid
|
|
112
|
+
* double-capture.
|
|
113
|
+
*
|
|
114
|
+
* Gated behind brain.autoCapture config. Never throws.
|
|
115
|
+
*
|
|
116
|
+
* @param projectRoot - Absolute path to the project root directory.
|
|
117
|
+
* @param payload - Notification event payload.
|
|
118
|
+
*
|
|
119
|
+
* @task T166
|
|
120
|
+
*/
|
|
121
|
+
export async function handleSystemNotification(
|
|
122
|
+
projectRoot: string,
|
|
123
|
+
payload: NotificationPayload,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
// File-change notifications are handled by file-hooks.ts
|
|
126
|
+
if (payload.filePath || payload.changeType) return;
|
|
127
|
+
// Only handle message-bearing system notifications
|
|
128
|
+
if (!payload.message) return;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const { loadConfig } = await import('../../config.js');
|
|
132
|
+
const config = await loadConfig(projectRoot);
|
|
133
|
+
if (!config.brain?.autoCapture) return;
|
|
134
|
+
} catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { observeBrain } = await import('../../memory/brain-retrieval.js');
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await observeBrain(projectRoot, {
|
|
142
|
+
text: `System notification: ${payload.message}`,
|
|
143
|
+
title: `Notification: ${payload.message.slice(0, 60)}`,
|
|
144
|
+
type: 'discovery',
|
|
145
|
+
sourceSessionId: payload.sessionId,
|
|
146
|
+
sourceType: 'agent',
|
|
147
|
+
});
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (!isMissingBrainSchemaError(err)) throw err;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
96
153
|
// Register handlers on module load
|
|
97
154
|
hooks.register({
|
|
98
155
|
id: 'brain-prompt-submit',
|
|
99
|
-
event: '
|
|
156
|
+
event: 'PromptSubmit',
|
|
100
157
|
handler: handlePromptSubmit,
|
|
101
158
|
priority: 100,
|
|
102
159
|
});
|
|
103
160
|
|
|
104
161
|
hooks.register({
|
|
105
162
|
id: 'brain-response-complete',
|
|
106
|
-
event: '
|
|
163
|
+
event: 'ResponseComplete',
|
|
107
164
|
handler: handleResponseComplete,
|
|
108
165
|
priority: 100,
|
|
109
166
|
});
|
|
167
|
+
|
|
168
|
+
// Lower priority (90) so file-hooks.ts (100) runs first for Notification events
|
|
169
|
+
hooks.register({
|
|
170
|
+
id: 'brain-system-notification',
|
|
171
|
+
event: 'Notification',
|
|
172
|
+
handler: handleSystemNotification,
|
|
173
|
+
priority: 90,
|
|
174
|
+
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { hooks } from '../registry.js';
|
|
13
|
-
import type {
|
|
13
|
+
import type { SessionEndPayload, SessionStartPayload } from '../types.js';
|
|
14
14
|
import { maybeRefreshMemoryBridge } from './memory-bridge-refresh.js';
|
|
15
15
|
|
|
16
16
|
function isMissingBrainSchemaError(err: unknown): boolean {
|
|
@@ -20,14 +20,14 @@ function isMissingBrainSchemaError(err: unknown): boolean {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Handle
|
|
23
|
+
* Handle SessionStart - capture initial session context
|
|
24
24
|
*
|
|
25
25
|
* T138: Refresh memory bridge on session start.
|
|
26
26
|
* T139: Regenerate bridge with session scope context.
|
|
27
27
|
*/
|
|
28
28
|
export async function handleSessionStart(
|
|
29
29
|
projectRoot: string,
|
|
30
|
-
payload:
|
|
30
|
+
payload: SessionStartPayload,
|
|
31
31
|
): Promise<void> {
|
|
32
32
|
const { observeBrain } = await import('../../memory/brain-retrieval.js');
|
|
33
33
|
|
|
@@ -48,14 +48,14 @@ export async function handleSessionStart(
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Handle
|
|
51
|
+
* Handle SessionEnd - capture session summary
|
|
52
52
|
*
|
|
53
53
|
* T138: Refresh memory bridge after session ends.
|
|
54
54
|
* T144: Extract transcript observations via cross-provider adapter.
|
|
55
55
|
*/
|
|
56
56
|
export async function handleSessionEnd(
|
|
57
57
|
projectRoot: string,
|
|
58
|
-
payload:
|
|
58
|
+
payload: SessionEndPayload,
|
|
59
59
|
): Promise<void> {
|
|
60
60
|
const { observeBrain } = await import('../../memory/brain-retrieval.js');
|
|
61
61
|
|
|
@@ -107,14 +107,14 @@ export async function handleSessionEnd(
|
|
|
107
107
|
// Register handlers on module load
|
|
108
108
|
hooks.register({
|
|
109
109
|
id: 'brain-session-start',
|
|
110
|
-
event: '
|
|
110
|
+
event: 'SessionStart',
|
|
111
111
|
handler: handleSessionStart,
|
|
112
112
|
priority: 100,
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
hooks.register({
|
|
116
116
|
id: 'brain-session-end',
|
|
117
|
-
event: '
|
|
117
|
+
event: 'SessionEnd',
|
|
118
118
|
handler: handleSessionEnd,
|
|
119
119
|
priority: 100,
|
|
120
120
|
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { hooks } from '../registry.js';
|
|
11
|
-
import type {
|
|
11
|
+
import type { PostToolUsePayload, PreToolUsePayload } from '../types.js';
|
|
12
12
|
import { maybeRefreshMemoryBridge } from './memory-bridge-refresh.js';
|
|
13
13
|
|
|
14
14
|
function isMissingBrainSchemaError(err: unknown): boolean {
|
|
@@ -18,11 +18,11 @@ function isMissingBrainSchemaError(err: unknown): boolean {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Handle
|
|
21
|
+
* Handle PreToolUse (maps to task.start in CLEO, canonical: was onToolStart)
|
|
22
22
|
*/
|
|
23
23
|
export async function handleToolStart(
|
|
24
24
|
projectRoot: string,
|
|
25
|
-
payload:
|
|
25
|
+
payload: PreToolUsePayload,
|
|
26
26
|
): Promise<void> {
|
|
27
27
|
const { observeBrain } = await import('../../memory/brain-retrieval.js');
|
|
28
28
|
|
|
@@ -39,13 +39,13 @@ export async function handleToolStart(
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Handle
|
|
42
|
+
* Handle PostToolUse (maps to task.complete in CLEO, canonical: was onToolComplete)
|
|
43
43
|
*
|
|
44
44
|
* T138: Refresh memory bridge after task completion.
|
|
45
45
|
*/
|
|
46
46
|
export async function handleToolComplete(
|
|
47
47
|
projectRoot: string,
|
|
48
|
-
payload:
|
|
48
|
+
payload: PostToolUsePayload,
|
|
49
49
|
): Promise<void> {
|
|
50
50
|
const { observeBrain } = await import('../../memory/brain-retrieval.js');
|
|
51
51
|
|
|
@@ -67,14 +67,14 @@ export async function handleToolComplete(
|
|
|
67
67
|
// Register handlers
|
|
68
68
|
hooks.register({
|
|
69
69
|
id: 'brain-tool-start',
|
|
70
|
-
event: '
|
|
70
|
+
event: 'PreToolUse',
|
|
71
71
|
handler: handleToolStart,
|
|
72
72
|
priority: 100,
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
hooks.register({
|
|
76
76
|
id: 'brain-tool-complete',
|
|
77
|
-
event: '
|
|
77
|
+
event: 'PostToolUse',
|
|
78
78
|
handler: handleToolComplete,
|
|
79
79
|
priority: 100,
|
|
80
80
|
});
|