Skip to content

Commit 3ef6ab1

Browse files
feat: brain defaults, active-work hooks, test coverage, #83 fix
Defaults (breaking: now opt-in by default): - brain.embedding.enabled: false → true (vector search out of the box) - brain.memoryBridge.contextAware: false → true (scope-aware bridge) - brain.summarization.enabled: false → true (session summaries) Active-work capture hooks: - New work-capture-hooks.ts: onPromptSubmit/onResponseComplete handlers - Smart filtering: only mutations, skip lifecycle-covered ops - Gated behind brain.captureWork config (default: false, opt-in) - Wired into CLI dispatch (cli.ts) for provider-agnostic capture - Replaced env var gates in file-hooks.ts and mcp-hooks.ts with config-first + env var override pattern Issue #83 fix: - Added REQUIRED_SESSION_COLUMNS (provider_id, stats_json, resume_count, grade_mode) with ensureColumns() before migrateWithRetry() - Follows existing REQUIRED_TASK_COLUMNS pattern Test coverage: - New brain-automation.test.ts: 55 tests across 9 feature groups - Covers config, summarization, transcript, maintenance, embedding, queue, bridge refresh, context-aware generation 4912 tests passing, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 533018a commit 3ef6ab1

11 files changed

Lines changed: 1296 additions & 27 deletions

File tree

packages/cleo/src/dispatch/adapters/cli.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*/
1111

1212
import { randomUUID } from 'node:crypto';
13-
import { autoRecordDispatchTokenUsage, getProjectRoot } from '@cleocode/core/internal';
13+
import { autoRecordDispatchTokenUsage, getProjectRoot, hooks } from '@cleocode/core/internal';
1414
import { type CliOutputOptions, cliError, cliOutput } from '../../cli/renderers/index.js';
1515
import { Dispatcher } from '../dispatcher.js';
1616
import { createDomainHandlers } from '../domains/index.js';
@@ -141,6 +141,22 @@ export async function dispatchFromCli(
141141
outputOpts?: CliOutputOptions,
142142
): Promise<void> {
143143
const dispatcher = getCliDispatcher();
144+
const projectRoot = getProjectRoot();
145+
const dispatchStart = Date.now();
146+
147+
// Dispatch onPromptSubmit hook (best-effort, fire-and-forget)
148+
hooks
149+
.dispatch('onPromptSubmit', projectRoot, {
150+
timestamp: new Date().toISOString(),
151+
gateway,
152+
domain,
153+
operation,
154+
source: 'cli',
155+
})
156+
.catch(() => {
157+
/* hook errors are non-fatal */
158+
});
159+
144160
const response = await dispatcher.dispatch({
145161
gateway,
146162
domain,
@@ -150,6 +166,21 @@ export async function dispatchFromCli(
150166
requestId: randomUUID(),
151167
});
152168

169+
// Dispatch onResponseComplete hook (best-effort, fire-and-forget)
170+
hooks
171+
.dispatch('onResponseComplete', projectRoot, {
172+
timestamp: new Date().toISOString(),
173+
gateway,
174+
domain,
175+
operation,
176+
success: response.success,
177+
durationMs: Date.now() - dispatchStart,
178+
errorCode: response.error?.code,
179+
})
180+
.catch(() => {
181+
/* hook errors are non-fatal */
182+
});
183+
153184
if (response.success) {
154185
await autoRecordDispatchTokenUsage({
155186
requestPayload: params,
@@ -219,12 +250,45 @@ export async function dispatchRaw(
219250
params?: Record<string, unknown>,
220251
): Promise<DispatchResponse> {
221252
const dispatcher = getCliDispatcher();
222-
return dispatcher.dispatch({
253+
const projectRoot = getProjectRoot();
254+
const dispatchStart = Date.now();
255+
256+
// Dispatch onPromptSubmit hook (best-effort, fire-and-forget)
257+
hooks
258+
.dispatch('onPromptSubmit', projectRoot, {
259+
timestamp: new Date().toISOString(),
260+
gateway,
261+
domain,
262+
operation,
263+
source: 'cli',
264+
})
265+
.catch(() => {
266+
/* hook errors are non-fatal */
267+
});
268+
269+
const response = await dispatcher.dispatch({
223270
gateway,
224271
domain,
225272
operation,
226273
params,
227274
source: 'cli',
228275
requestId: randomUUID(),
229276
});
277+
278+
// Dispatch onResponseComplete hook (best-effort, fire-and-forget)
279+
hooks
280+
.dispatch('onResponseComplete', projectRoot, {
281+
timestamp: new Date().toISOString(),
282+
gateway,
283+
domain,
284+
operation,
285+
success: response.success,
286+
durationMs: Date.now() - dispatchStart,
287+
errorCode: response.error?.code,
288+
})
289+
.catch(() => {
290+
/* hook errors are non-fatal */
291+
});
292+
293+
return response;
230294
}

packages/contracts/src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ export interface BrainConfig {
186186
captureFiles: boolean;
187187
/** Whether to capture MCP tool events (default: false). */
188188
captureMcp: boolean;
189+
/** Whether to capture active-work dispatch mutations (tasks.add, tasks.update) (default: false). */
190+
captureWork: boolean;
189191
/** Embedding provider settings. */
190192
embedding: BrainEmbeddingConfig;
191193
/** Memory bridge auto-refresh settings. */

packages/core/src/config.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,18 @@ const DEFAULTS: CleoConfig = {
8383
autoCapture: true,
8484
captureFiles: false,
8585
captureMcp: false,
86+
captureWork: false,
8687
embedding: {
87-
enabled: false,
88+
enabled: true,
8889
provider: 'local' as const,
8990
},
9091
memoryBridge: {
9192
autoRefresh: true,
92-
contextAware: false,
93+
contextAware: true,
9394
maxTokens: 2000,
9495
},
9596
summarization: {
96-
enabled: false,
97+
enabled: true,
9798
},
9899
},
99100
};

packages/core/src/hooks/handlers/file-hooks.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
* Includes 5-second dedup and path-based filtering to avoid noisy writes.
66
* Auto-registers on module load.
77
*
8-
* Disabled by default. Set CLEO_BRAIN_CAPTURE_FILES=true to enable.
8+
* Disabled by default. Enable via:
9+
* - Config: brain.captureFiles = true (checked first)
10+
* - Env: CLEO_BRAIN_CAPTURE_FILES=true (overrides config)
911
*/
1012

1113
import { isAbsolute, relative } from 'node:path';
@@ -39,10 +41,34 @@ function shouldSkipPath(relativePath: string): boolean {
3941
return SKIP_PATTERNS.some((pattern) => pattern.test(relativePath));
4042
}
4143

44+
/**
45+
* Check whether file-change capture is enabled.
46+
*
47+
* Resolution order (first truthy wins):
48+
* 1. CLEO_BRAIN_CAPTURE_FILES env var (explicit override)
49+
* 2. brain.captureFiles project config value
50+
*
51+
* Defaults to false when neither is set.
52+
*/
53+
async function isFileCaptureEnabled(projectRoot: string): Promise<boolean> {
54+
const envOverride = process.env['CLEO_BRAIN_CAPTURE_FILES'];
55+
if (envOverride !== undefined) {
56+
return envOverride === 'true';
57+
}
58+
try {
59+
const { loadConfig } = await import('../../config.js');
60+
const config = await loadConfig(projectRoot);
61+
return config.brain?.captureFiles ?? false;
62+
} catch {
63+
return false;
64+
}
65+
}
66+
4267
/**
4368
* Handle onFileChange - capture file changes to BRAIN
4469
*
45-
* Gated behind CLEO_BRAIN_CAPTURE_FILES=true env var.
70+
* Gated behind brain.captureFiles config or CLEO_BRAIN_CAPTURE_FILES env var.
71+
* Env var takes precedence over config for backward compatibility.
4672
* Deduplicates rapid writes to the same file within a 5-second window.
4773
* Filters out .cleo/ internal files and test temp directories.
4874
* Converts absolute paths to project-relative paths.
@@ -52,7 +78,7 @@ export async function handleFileChange(
5278
payload: OnFileChangePayload,
5379
): Promise<void> {
5480
// Opt-in gate: disabled by default to prevent observation noise
55-
if (process.env['CLEO_BRAIN_CAPTURE_FILES'] !== 'true') return;
81+
if (!(await isFileCaptureEnabled(projectRoot))) return;
5682

5783
// 5-second dedup
5884
const now = Date.now();

packages/core/src/hooks/handlers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import './task-hooks.js';
1111
import './error-hooks.js';
1212
import './file-hooks.js';
1313
import './mcp-hooks.js';
14+
import './work-capture-hooks.js';
1415

1516
export { handleError } from './error-hooks.js';
1617
export { handleFileChange } from './file-hooks.js';
1718
export { handlePromptSubmit, handleResponseComplete } from './mcp-hooks.js';
1819
// Re-export handler functions for explicit use
1920
export { handleSessionEnd, handleSessionStart } from './session-hooks.js';
2021
export { handleToolComplete, handleToolStart } from './task-hooks.js';
22+
export { handleWorkPromptSubmit, handleWorkResponseComplete } from './work-capture-hooks.js';

packages/core/src/hooks/handlers/mcp-hooks.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/**
22
* MCP Prompt/Response Hook Handlers - Wave 2 of T5237
33
*
4-
* Handlers for onPromptSubmit and onResponseComplete events.
5-
* By default, NO brain capture (too noisy). Brain observation is
6-
* opt-in via CLEO_BRAIN_CAPTURE_MCP=true environment variable.
4+
* Handlers for onPromptSubmit and onResponseComplete events that capture
5+
* ALL gateway operations (read and write) to BRAIN.
6+
* By default, NO brain capture (too noisy). Enable via:
7+
* - Config: brain.captureMcp = true (checked first)
8+
* - Env: CLEO_BRAIN_CAPTURE_MCP=true (overrides config)
79
* Auto-registers on module load.
810
*/
911

@@ -17,23 +19,39 @@ function isMissingBrainSchemaError(err: unknown): boolean {
1719
}
1820

1921
/**
20-
* Check if brain capture is enabled for MCP events.
21-
* Defaults to false (too noisy for normal operation).
22+
* Check whether MCP-level brain capture is enabled.
23+
*
24+
* Resolution order (first truthy wins):
25+
* 1. CLEO_BRAIN_CAPTURE_MCP env var (explicit override)
26+
* 2. brain.captureMcp project config value
27+
*
28+
* Defaults to false when neither is set (too noisy for normal operation).
2229
*/
23-
function isBrainCaptureEnabled(): boolean {
24-
return process.env['CLEO_BRAIN_CAPTURE_MCP'] === 'true';
30+
async function isBrainCaptureEnabled(projectRoot: string): Promise<boolean> {
31+
const envOverride = process.env['CLEO_BRAIN_CAPTURE_MCP'];
32+
if (envOverride !== undefined) {
33+
return envOverride === 'true';
34+
}
35+
try {
36+
const { loadConfig } = await import('../../config.js');
37+
const config = await loadConfig(projectRoot);
38+
return config.brain?.captureMcp ?? false;
39+
} catch {
40+
return false;
41+
}
2542
}
2643

2744
/**
28-
* Handle onPromptSubmit - optionally capture prompt events to BRAIN
45+
* Handle onPromptSubmit - optionally capture ALL gateway prompt events to BRAIN.
2946
*
30-
* No-op by default. Set CLEO_BRAIN_CAPTURE_MCP=true to enable.
47+
* No-op by default. Enable via brain.captureMcp config or CLEO_BRAIN_CAPTURE_MCP env.
48+
* For selective mutation-only capture, use work-capture-hooks.ts instead.
3149
*/
3250
export async function handlePromptSubmit(
3351
projectRoot: string,
3452
payload: OnPromptSubmitPayload,
3553
): Promise<void> {
36-
if (!isBrainCaptureEnabled()) return;
54+
if (!(await isBrainCaptureEnabled(projectRoot))) return;
3755

3856
const { observeBrain } = await import('../../memory/brain-retrieval.js');
3957

@@ -50,15 +68,16 @@ export async function handlePromptSubmit(
5068
}
5169

5270
/**
53-
* Handle onResponseComplete - optionally capture response events to BRAIN
71+
* Handle onResponseComplete - optionally capture ALL gateway response events to BRAIN.
5472
*
55-
* No-op by default. Set CLEO_BRAIN_CAPTURE_MCP=true to enable.
73+
* No-op by default. Enable via brain.captureMcp config or CLEO_BRAIN_CAPTURE_MCP env.
74+
* For selective mutation-only capture, use work-capture-hooks.ts instead.
5675
*/
5776
export async function handleResponseComplete(
5877
projectRoot: string,
5978
payload: OnResponseCompletePayload,
6079
): Promise<void> {
61-
if (!isBrainCaptureEnabled()) return;
80+
if (!(await isBrainCaptureEnabled(projectRoot))) return;
6281

6382
const { observeBrain } = await import('../../memory/brain-retrieval.js');
6483

0 commit comments

Comments
 (0)