@cleocode/core 2026.3.69 → 2026.3.71

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 (129) hide show
  1. package/dist/agents/retry.d.ts.map +1 -1
  2. package/dist/agents/retry.js +23 -42
  3. package/dist/agents/retry.js.map +1 -1
  4. package/dist/cleo.d.ts +2 -300
  5. package/dist/cleo.d.ts.map +1 -1
  6. package/dist/cleo.js +2 -2
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +30 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/hooks/handlers/file-hooks.d.ts +5 -2
  11. package/dist/hooks/handlers/file-hooks.d.ts.map +1 -1
  12. package/dist/hooks/handlers/index.d.ts +2 -0
  13. package/dist/hooks/handlers/index.d.ts.map +1 -1
  14. package/dist/hooks/handlers/mcp-hooks.d.ts +11 -7
  15. package/dist/hooks/handlers/mcp-hooks.d.ts.map +1 -1
  16. package/dist/hooks/handlers/memory-bridge-refresh.d.ts +20 -0
  17. package/dist/hooks/handlers/memory-bridge-refresh.d.ts.map +1 -0
  18. package/dist/hooks/handlers/memory-bridge-refresh.js +42 -0
  19. package/dist/hooks/handlers/memory-bridge-refresh.js.map +1 -0
  20. package/dist/hooks/handlers/session-hooks.d.ts +10 -0
  21. package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
  22. package/dist/hooks/handlers/session-hooks.js +36 -0
  23. package/dist/hooks/handlers/session-hooks.js.map +1 -1
  24. package/dist/hooks/handlers/task-hooks.d.ts +4 -0
  25. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  26. package/dist/hooks/handlers/task-hooks.js +7 -0
  27. package/dist/hooks/handlers/task-hooks.js.map +1 -1
  28. package/dist/hooks/handlers/work-capture-hooks.d.ts +40 -0
  29. package/dist/hooks/handlers/work-capture-hooks.d.ts.map +1 -0
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +5069 -4678
  33. package/dist/index.js.map +4 -4
  34. package/dist/internal.d.ts +10 -2
  35. package/dist/internal.d.ts.map +1 -1
  36. package/dist/internal.js +10 -3
  37. package/dist/internal.js.map +1 -1
  38. package/dist/memory/auto-extract.d.ts +13 -0
  39. package/dist/memory/auto-extract.d.ts.map +1 -1
  40. package/dist/memory/auto-extract.js +34 -0
  41. package/dist/memory/auto-extract.js.map +1 -1
  42. package/dist/memory/brain-embedding.d.ts +13 -0
  43. package/dist/memory/brain-embedding.d.ts.map +1 -1
  44. package/dist/memory/brain-embedding.js +17 -0
  45. package/dist/memory/brain-embedding.js.map +1 -1
  46. package/dist/memory/brain-maintenance.d.ts +110 -0
  47. package/dist/memory/brain-maintenance.d.ts.map +1 -0
  48. package/dist/memory/brain-maintenance.js +98 -0
  49. package/dist/memory/brain-maintenance.js.map +1 -0
  50. package/dist/memory/brain-retrieval.d.ts +31 -5
  51. package/dist/memory/brain-retrieval.d.ts.map +1 -1
  52. package/dist/memory/brain-retrieval.js +53 -6
  53. package/dist/memory/brain-retrieval.js.map +1 -1
  54. package/dist/memory/embedding-local.d.ts +55 -0
  55. package/dist/memory/embedding-local.d.ts.map +1 -0
  56. package/dist/memory/embedding-local.js +97 -0
  57. package/dist/memory/embedding-local.js.map +1 -0
  58. package/dist/memory/embedding-queue.d.ts +90 -0
  59. package/dist/memory/embedding-queue.d.ts.map +1 -0
  60. package/dist/memory/embedding-queue.js +271 -0
  61. package/dist/memory/embedding-queue.js.map +1 -0
  62. package/dist/memory/embedding-worker.d.ts +19 -0
  63. package/dist/memory/embedding-worker.d.ts.map +1 -0
  64. package/dist/memory/embedding-worker.js +58 -0
  65. package/dist/memory/embedding-worker.js.map +1 -0
  66. package/dist/memory/memory-bridge.d.ts +21 -1
  67. package/dist/memory/memory-bridge.d.ts.map +1 -1
  68. package/dist/memory/memory-bridge.js +83 -2
  69. package/dist/memory/memory-bridge.js.map +1 -1
  70. package/dist/memory/session-memory.d.ts +26 -0
  71. package/dist/memory/session-memory.d.ts.map +1 -1
  72. package/dist/memory/session-memory.js +105 -0
  73. package/dist/memory/session-memory.js.map +1 -1
  74. package/dist/pagination.js +3 -0
  75. package/dist/pagination.js.map +1 -1
  76. package/dist/sessions/index.d.ts.map +1 -1
  77. package/dist/sessions/index.js +2 -6
  78. package/dist/sessions/index.js.map +1 -1
  79. package/dist/store/brain-sqlite.js +13 -62
  80. package/dist/store/brain-sqlite.js.map +1 -1
  81. package/dist/store/migration-manager.js +151 -0
  82. package/dist/store/migration-manager.js.map +1 -0
  83. package/dist/store/sqlite.d.ts.map +1 -1
  84. package/dist/store/sqlite.js +16 -134
  85. package/dist/store/sqlite.js.map +1 -1
  86. package/dist/tasks/add.js +27 -22
  87. package/dist/tasks/add.js.map +1 -1
  88. package/dist/tasks/complete.d.ts.map +1 -1
  89. package/dist/tasks/complete.js +13 -40
  90. package/dist/tasks/complete.js.map +1 -1
  91. package/dist/tasks/enforcement.js +12 -15
  92. package/dist/tasks/enforcement.js.map +1 -1
  93. package/dist/upgrade.js +246 -3
  94. package/dist/upgrade.js.map +1 -1
  95. package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/migration.sql +17 -17
  96. package/package.json +6 -5
  97. package/src/agents/retry.ts +30 -24
  98. package/src/cleo.ts +30 -251
  99. package/src/config.ts +18 -0
  100. package/src/hooks/handlers/file-hooks.ts +29 -3
  101. package/src/hooks/handlers/index.ts +2 -0
  102. package/src/hooks/handlers/mcp-hooks.ts +32 -13
  103. package/src/hooks/handlers/memory-bridge-refresh.ts +47 -0
  104. package/src/hooks/handlers/session-hooks.ts +38 -0
  105. package/src/hooks/handlers/task-hooks.ts +8 -0
  106. package/src/hooks/handlers/work-capture-hooks.ts +184 -0
  107. package/src/index.ts +5 -0
  108. package/src/internal.ts +28 -2
  109. package/src/memory/__tests__/brain-automation.test.ts +941 -0
  110. package/src/memory/auto-extract.ts +40 -0
  111. package/src/memory/brain-embedding.ts +18 -0
  112. package/src/memory/brain-maintenance.ts +183 -0
  113. package/src/memory/brain-retrieval.ts +85 -7
  114. package/src/memory/embedding-local.ts +107 -0
  115. package/src/memory/embedding-queue.ts +304 -0
  116. package/src/memory/embedding-worker.ts +79 -0
  117. package/src/memory/memory-bridge.ts +101 -2
  118. package/src/memory/session-memory.ts +123 -0
  119. package/src/sessions/index.ts +2 -6
  120. package/src/store/__tests__/test-db-helper.js +14 -2
  121. package/src/store/__tests__/test-db-helper.ts +4 -1
  122. package/src/store/sqlite.ts +28 -0
  123. package/src/tasks/__tests__/complete-unblocks.test.ts +4 -1
  124. package/src/tasks/__tests__/complete.test.ts +18 -6
  125. package/src/tasks/__tests__/epic-enforcement.test.ts +4 -1
  126. package/src/tasks/__tests__/update.test.ts +4 -1
  127. package/src/tasks/complete.ts +8 -8
  128. package/templates/config.template.json +19 -0
  129. package/templates/global-config.template.json +19 -0
package/src/cleo.ts CHANGED
@@ -16,26 +16,26 @@
16
16
 
17
17
  import path from 'node:path';
18
18
  import type {
19
+ AdminAPI,
20
+ AgentsAPI,
21
+ CleoInitOptions,
19
22
  DataAccessor,
20
- ExternalTask,
21
- ExternalTaskLink,
22
- ReconcileOptions,
23
- ReconcileResult,
24
- Task,
25
- TaskPriority,
26
- TaskSize,
27
- TaskStatus,
28
- TaskType,
23
+ IntelligenceAPI,
24
+ LifecycleAPI,
25
+ MemoryAPI,
26
+ NexusAPI,
27
+ OrchestrationAPI,
28
+ ReleaseAPI,
29
+ SessionsAPI,
30
+ StickyAPI,
31
+ SyncAPI,
32
+ TasksAPI,
29
33
  } from '@cleocode/contracts';
30
34
  // Admin
31
35
  import { exportTasks } from './admin/export.js';
32
- import type { ImportParams } from './admin/import.js';
33
36
  import { importTasks } from './admin/import.js';
34
37
  // Agents
35
38
  import {
36
- type AgentCapacity,
37
- type AgentHealthStatus,
38
- type AgentInstanceRow,
39
39
  checkAgentHealth,
40
40
  deregisterAgent,
41
41
  detectCrashedAgents,
@@ -43,16 +43,10 @@ import {
43
43
  heartbeat,
44
44
  isOverloaded,
45
45
  listAgentInstances,
46
- type RegisterAgentOptions,
47
46
  registerAgent,
48
47
  } from './agents/index.js';
49
48
  // Intelligence
50
- import {
51
- type BlastRadius,
52
- calculateBlastRadius,
53
- type ImpactReport,
54
- predictImpact,
55
- } from './intelligence/index.js';
49
+ import { calculateBlastRadius, predictImpact } from './intelligence/index.js';
56
50
  // Lifecycle
57
51
  import {
58
52
  checkGate,
@@ -66,7 +60,6 @@ import {
66
60
  skipStage,
67
61
  startStage,
68
62
  } from './lifecycle/index.js';
69
- import type { BrainObservationType } from './memory/brain-retrieval.js';
70
63
  // Memory
71
64
  import {
72
65
  fetchBrainEntries,
@@ -74,7 +67,6 @@ import {
74
67
  searchBrainCompact,
75
68
  timelineBrain,
76
69
  } from './memory/brain-retrieval.js';
77
- import type { HybridSearchOptions } from './memory/brain-search.js';
78
70
  import { hybridSearch, searchBrain } from './memory/brain-search.js';
79
71
  // Nexus
80
72
  import { discoverRelated, searchAcrossProjects } from './nexus/discover.js';
@@ -162,237 +154,24 @@ import { showTask } from './tasks/show.js';
162
154
  import { updateTask } from './tasks/update.js';
163
155
 
164
156
  // ============================================================================
165
- // Domain API interfaces
166
- // ============================================================================
167
-
168
- export interface TasksAPI {
169
- add(params: {
170
- title: string;
171
- description: string;
172
- parent?: string;
173
- priority?: TaskPriority;
174
- type?: TaskType;
175
- size?: TaskSize;
176
- phase?: string;
177
- labels?: string[];
178
- depends?: string[];
179
- notes?: string;
180
- }): Promise<unknown>;
181
- find(params: {
182
- query?: string;
183
- id?: string;
184
- status?: TaskStatus;
185
- limit?: number;
186
- }): Promise<unknown>;
187
- show(taskId: string): Promise<unknown>;
188
- list(params?: {
189
- status?: TaskStatus;
190
- priority?: TaskPriority;
191
- parentId?: string;
192
- phase?: string;
193
- limit?: number;
194
- }): Promise<unknown>;
195
- update(params: {
196
- taskId: string;
197
- title?: string;
198
- status?: TaskStatus;
199
- priority?: TaskPriority;
200
- description?: string;
201
- notes?: string;
202
- }): Promise<unknown>;
203
- complete(params: { taskId: string; notes?: string }): Promise<unknown>;
204
- delete(params: { taskId: string; force?: boolean }): Promise<unknown>;
205
- archive(params?: { before?: string; taskIds?: string[]; dryRun?: boolean }): Promise<unknown>;
206
- /** Start working on a specific task (sets focus). */
207
- start(taskId: string): Promise<unknown>;
208
- /** Stop working on the current task (clears focus). */
209
- stop(): Promise<{ previousTask: string | null }>;
210
- /** Get the current task work state. */
211
- current(): Promise<unknown>;
212
- }
213
-
214
- export interface SessionsAPI {
215
- start(params: {
216
- name: string;
217
- scope: string;
218
- agent?: string;
219
- startTask?: string;
220
- }): Promise<unknown>;
221
- end(params?: { note?: string }): Promise<unknown>;
222
- status(): Promise<unknown>;
223
- resume(sessionId: string): Promise<unknown>;
224
- list(params?: { status?: string; limit?: number }): Promise<unknown>;
225
- find(params?: {
226
- status?: string;
227
- scope?: string;
228
- query?: string;
229
- limit?: number;
230
- }): Promise<unknown>;
231
- show(sessionId: string): Promise<unknown>;
232
- suspend(sessionId: string, reason?: string): Promise<unknown>;
233
- briefing(params?: { maxNextTasks?: number; scope?: string }): Promise<unknown>;
234
- handoff(sessionId: string, options?: { note?: string; nextAction?: string }): Promise<unknown>;
235
- gc(maxAgeHours?: number): Promise<unknown>;
236
- recordDecision(params: {
237
- sessionId: string;
238
- taskId: string;
239
- decision: string;
240
- rationale: string;
241
- alternatives?: string[];
242
- }): Promise<unknown>;
243
- recordAssumption(params: {
244
- assumption: string;
245
- confidence: 'high' | 'medium' | 'low';
246
- sessionId?: string;
247
- taskId?: string;
248
- }): Promise<unknown>;
249
- contextDrift(params?: { sessionId?: string }): Promise<unknown>;
250
- decisionLog(params?: { sessionId?: string; taskId?: string }): Promise<unknown>;
251
- lastHandoff(scope?: { type: string; epicId?: string }): Promise<unknown>;
252
- }
253
-
254
- export interface MemoryAPI {
255
- observe(params: { text: string; title?: string; type?: BrainObservationType }): Promise<unknown>;
256
- find(params: {
257
- query: string;
258
- limit?: number;
259
- tables?: Array<'decisions' | 'patterns' | 'learnings' | 'observations'>;
260
- }): Promise<unknown>;
261
- fetch(params: { ids: string[] }): Promise<unknown>;
262
- timeline(params: { anchor: string; depthBefore?: number; depthAfter?: number }): Promise<unknown>;
263
- search(query: string, options?: { limit?: number }): Promise<unknown>;
264
- hybridSearch(query: string, options?: HybridSearchOptions): Promise<unknown>;
265
- }
266
-
267
- export interface OrchestrationAPI {
268
- start(epicId: string): Promise<unknown>;
269
- analyze(epicId: string): Promise<unknown>;
270
- readyTasks(epicId: string): Promise<unknown>;
271
- nextTask(epicId: string): Promise<unknown>;
272
- context(epicId: string): Promise<unknown>;
273
- dependencyGraph(tasks: Task[]): unknown;
274
- epicStatus(epicId: string, title: string, children: Task[]): unknown;
275
- progress(tasks: Task[]): unknown;
276
- }
277
-
278
- export interface LifecycleAPI {
279
- status(epicId: string): Promise<unknown>;
280
- startStage(epicId: string, stage: string): Promise<unknown>;
281
- completeStage(epicId: string, stage: string, artifacts?: string[]): Promise<unknown>;
282
- skipStage(epicId: string, stage: string, reason: string): Promise<unknown>;
283
- checkGate(epicId: string, targetStage: string): Promise<unknown>;
284
- history(epicId: string): Promise<unknown>;
285
- resetStage(epicId: string, stage: string, reason: string): Promise<unknown>;
286
- passGate(epicId: string, gateName: string, agent?: string): Promise<unknown>;
287
- failGate(epicId: string, gateName: string, reason?: string): Promise<unknown>;
288
- stages: readonly string[];
289
- }
290
-
291
- export interface ReleaseAPI {
292
- prepare(params: { version: string; tasks?: string[]; notes?: string }): Promise<unknown>;
293
- commit(params: { version: string }): Promise<unknown>;
294
- tag(params: { version: string }): Promise<unknown>;
295
- push(params: { version: string; remote?: string; explicitPush?: boolean }): Promise<unknown>;
296
- rollback(params: { version: string; reason?: string }): Promise<unknown>;
297
- calculateVersion(current: string, bumpType: string): string;
298
- bumpVersion(): Promise<unknown>;
299
- }
300
-
301
- export interface AdminAPI {
302
- export(params?: Record<string, unknown>): Promise<unknown>;
303
- import(params: Omit<ImportParams, 'cwd'>): Promise<unknown>;
304
- }
305
-
306
- export interface StickyAPI {
307
- add(params: {
308
- content: string;
309
- tags?: string[];
310
- priority?: string;
311
- color?: string;
312
- }): Promise<unknown>;
313
- show(stickyId: string): Promise<unknown>;
314
- list(params?: {
315
- status?: string;
316
- color?: string;
317
- priority?: string;
318
- limit?: number;
319
- }): Promise<unknown>;
320
- archive(stickyId: string): Promise<unknown>;
321
- purge(stickyId: string): Promise<unknown>;
322
- convert(params: {
323
- stickyId: string;
324
- targetType: 'task' | 'memory' | 'task_note' | 'session_note';
325
- title?: string;
326
- memoryType?: string;
327
- taskId?: string;
328
- }): Promise<unknown>;
329
- }
330
-
331
- export interface NexusAPI {
332
- init(): Promise<unknown>;
333
- register(params: { path: string; name?: string; permissions?: string }): Promise<unknown>;
334
- unregister(params: { name: string }): Promise<unknown>;
335
- list(): Promise<unknown>;
336
- show(params: { name: string }): Promise<unknown>;
337
- sync(params?: { name?: string }): Promise<unknown>;
338
- discover(params: { query: string; method?: string; limit?: number }): Promise<unknown>;
339
- search(params: { pattern: string; project?: string; limit?: number }): Promise<unknown>;
340
- setPermission(params: { name: string; level: 'read' | 'write' | 'execute' }): Promise<unknown>;
341
- sharingStatus(): Promise<unknown>;
342
- }
343
-
344
- export interface SyncAPI {
345
- /** Reconcile external tasks with CLEO as SSoT. */
346
- reconcile(params: {
347
- externalTasks: ExternalTask[];
348
- providerId: string;
349
- dryRun?: boolean;
350
- conflictPolicy?: ReconcileOptions['conflictPolicy'];
351
- defaultPhase?: string;
352
- defaultLabels?: string[];
353
- }): Promise<ReconcileResult>;
354
- /** Get all external task links for a provider. */
355
- getLinks(providerId: string): Promise<ExternalTaskLink[]>;
356
- /** Get all external task links for a CLEO task. */
357
- getTaskLinks(taskId: string): Promise<ExternalTaskLink[]>;
358
- /** Remove all external task links for a provider. */
359
- removeProviderLinks(providerId: string): Promise<number>;
360
- }
361
-
362
- export interface AgentsAPI {
363
- /** Register a new agent instance. */
364
- register(options: RegisterAgentOptions): Promise<AgentInstanceRow>;
365
- /** Deregister an agent instance. */
366
- deregister(agentId: string): Promise<AgentInstanceRow | null>;
367
- /** Get health status for a specific agent. */
368
- health(agentId: string): Promise<AgentHealthStatus | null>;
369
- /** Detect agents that have crashed (missed heartbeats). */
370
- detectCrashed(thresholdMs?: number): Promise<AgentInstanceRow[]>;
371
- /** Record a heartbeat for an agent. */
372
- recordHeartbeat(agentId: string): Promise<unknown>;
373
- /** Get capacity info for an agent. */
374
- capacity(agentId: string): Promise<AgentCapacity | null>;
375
- /** Check if system is overloaded (available capacity below threshold). */
376
- isOverloaded(threshold?: number): Promise<boolean>;
377
- /** List all agent instances with optional filters. */
378
- list(params?: { status?: string; agentType?: string }): Promise<AgentInstanceRow[]>;
379
- }
380
-
381
- export interface IntelligenceAPI {
382
- /** Predict impact of a change description on related tasks. */
383
- predictImpact(change: string): Promise<ImpactReport>;
384
- /** Calculate blast radius for a task change. */
385
- blastRadius(taskId: string): Promise<BlastRadius>;
386
- }
387
-
388
- // ============================================================================
389
- // Init options
157
+ // Domain API interfaces — re-exported from @cleocode/contracts/facade
390
158
  // ============================================================================
391
159
 
392
- export interface CleoInitOptions {
393
- store?: DataAccessor;
394
- caamp?: boolean;
395
- }
160
+ export type {
161
+ AdminAPI,
162
+ AgentsAPI,
163
+ CleoInitOptions,
164
+ IntelligenceAPI,
165
+ LifecycleAPI,
166
+ MemoryAPI,
167
+ NexusAPI,
168
+ OrchestrationAPI,
169
+ ReleaseAPI,
170
+ SessionsAPI,
171
+ StickyAPI,
172
+ SyncAPI,
173
+ TasksAPI,
174
+ } from '@cleocode/contracts';
396
175
 
397
176
  // ============================================================================
398
177
  // Cleo facade class
package/src/config.ts CHANGED
@@ -79,6 +79,24 @@ const DEFAULTS: CleoConfig = {
79
79
  agentPrefix: 'cleo-',
80
80
  privacyTier: 'private',
81
81
  },
82
+ brain: {
83
+ autoCapture: true,
84
+ captureFiles: false,
85
+ captureMcp: false,
86
+ captureWork: false,
87
+ embedding: {
88
+ enabled: true,
89
+ provider: 'local' as const,
90
+ },
91
+ memoryBridge: {
92
+ autoRefresh: true,
93
+ contextAware: true,
94
+ maxTokens: 2000,
95
+ },
96
+ summarization: {
97
+ enabled: true,
98
+ },
99
+ },
82
100
  };
83
101
 
84
102
  /** Environment variable to config path mapping. */
@@ -5,7 +5,9 @@
5
5
  * Includes 5-second dedup and path-based filtering to avoid noisy writes.
6
6
  * Auto-registers on module load.
7
7
  *
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)
9
11
  */
10
12
 
11
13
  import { isAbsolute, relative } from 'node:path';
@@ -39,10 +41,34 @@ function shouldSkipPath(relativePath: string): boolean {
39
41
  return SKIP_PATTERNS.some((pattern) => pattern.test(relativePath));
40
42
  }
41
43
 
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
+
42
67
  /**
43
68
  * Handle onFileChange - capture file changes to BRAIN
44
69
  *
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.
46
72
  * Deduplicates rapid writes to the same file within a 5-second window.
47
73
  * Filters out .cleo/ internal files and test temp directories.
48
74
  * Converts absolute paths to project-relative paths.
@@ -52,7 +78,7 @@ export async function handleFileChange(
52
78
  payload: OnFileChangePayload,
53
79
  ): Promise<void> {
54
80
  // 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;
56
82
 
57
83
  // 5-second dedup
58
84
  const now = Date.now();
@@ -11,6 +11,7 @@ import './task-hooks.js';
11
11
  import './error-hooks.js';
12
12
  import './file-hooks.js';
13
13
  import './mcp-hooks.js';
14
+ import './work-capture-hooks.js';
14
15
 
15
16
  export { handleError } from './error-hooks.js';
16
17
  export { handleFileChange } from './file-hooks.js';
@@ -18,3 +19,4 @@ export { handlePromptSubmit, handleResponseComplete } from './mcp-hooks.js';
18
19
  // Re-export handler functions for explicit use
19
20
  export { handleSessionEnd, handleSessionStart } from './session-hooks.js';
20
21
  export { handleToolComplete, handleToolStart } from './task-hooks.js';
22
+ export { handleWorkPromptSubmit, handleWorkResponseComplete } from './work-capture-hooks.js';
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * MCP Prompt/Response Hook Handlers - Wave 2 of T5237
3
3
  *
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)
7
9
  * Auto-registers on module load.
8
10
  */
9
11
 
@@ -17,23 +19,39 @@ function isMissingBrainSchemaError(err: unknown): boolean {
17
19
  }
18
20
 
19
21
  /**
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).
22
29
  */
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
+ }
25
42
  }
26
43
 
27
44
  /**
28
- * Handle onPromptSubmit - optionally capture prompt events to BRAIN
45
+ * Handle onPromptSubmit - optionally capture ALL gateway prompt events to BRAIN.
29
46
  *
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.
31
49
  */
32
50
  export async function handlePromptSubmit(
33
51
  projectRoot: string,
34
52
  payload: OnPromptSubmitPayload,
35
53
  ): Promise<void> {
36
- if (!isBrainCaptureEnabled()) return;
54
+ if (!(await isBrainCaptureEnabled(projectRoot))) return;
37
55
 
38
56
  const { observeBrain } = await import('../../memory/brain-retrieval.js');
39
57
 
@@ -50,15 +68,16 @@ export async function handlePromptSubmit(
50
68
  }
51
69
 
52
70
  /**
53
- * Handle onResponseComplete - optionally capture response events to BRAIN
71
+ * Handle onResponseComplete - optionally capture ALL gateway response events to BRAIN.
54
72
  *
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.
56
75
  */
57
76
  export async function handleResponseComplete(
58
77
  projectRoot: string,
59
78
  payload: OnResponseCompletePayload,
60
79
  ): Promise<void> {
61
- if (!isBrainCaptureEnabled()) return;
80
+ if (!(await isBrainCaptureEnabled(projectRoot))) return;
62
81
 
63
82
  const { observeBrain } = await import('../../memory/brain-retrieval.js');
64
83
 
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared memory bridge refresh helper for hook handlers.
3
+ *
4
+ * Provides a config-gated, debounced wrapper around refreshMemoryBridge().
5
+ * Prevents rapid regeneration of .cleo/memory-bridge.md when multiple
6
+ * lifecycle events fire in quick succession (e.g. session end + task complete).
7
+ *
8
+ * Debounce: max one refresh per 30 seconds across all callers in the process.
9
+ *
10
+ * @task T138
11
+ * @epic T134
12
+ */
13
+
14
+ /** Debounce window in milliseconds. */
15
+ const DEBOUNCE_MS = 30_000;
16
+
17
+ /** Timestamp of the last successful refresh call (module-level singleton). */
18
+ let lastRefreshTime = 0;
19
+
20
+ /**
21
+ * Refresh the memory bridge if autoRefresh is enabled and the debounce window
22
+ * has elapsed. Reads config via loadConfig() (cascaded). Never throws.
23
+ *
24
+ * @param projectRoot - Absolute path to the project root directory.
25
+ */
26
+ export async function maybeRefreshMemoryBridge(projectRoot: string): Promise<void> {
27
+ try {
28
+ const { loadConfig } = await import('../../config.js');
29
+ const config = await loadConfig(projectRoot);
30
+
31
+ if (!config.brain?.memoryBridge?.autoRefresh) {
32
+ return;
33
+ }
34
+
35
+ const now = Date.now();
36
+ if (now - lastRefreshTime < DEBOUNCE_MS) {
37
+ return;
38
+ }
39
+
40
+ lastRefreshTime = now;
41
+
42
+ const { refreshMemoryBridge } = await import('../../memory/memory-bridge.js');
43
+ await refreshMemoryBridge(projectRoot);
44
+ } catch {
45
+ // Best-effort: never block lifecycle events
46
+ }
47
+ }
@@ -3,10 +3,15 @@
3
3
  *
4
4
  * Handlers that capture session lifecycle events to BRAIN via memory.observe.
5
5
  * Auto-registers on module load.
6
+ *
7
+ * T138: Triggers memory bridge refresh on session start and end.
8
+ * T139: Regenerates bridge with session scope on start.
9
+ * T144: Extracts transcript observations on session end.
6
10
  */
7
11
 
8
12
  import { hooks } from '../registry.js';
9
13
  import type { OnSessionEndPayload, OnSessionStartPayload } from '../types.js';
14
+ import { maybeRefreshMemoryBridge } from './memory-bridge-refresh.js';
10
15
 
11
16
  function isMissingBrainSchemaError(err: unknown): boolean {
12
17
  if (!(err instanceof Error)) return false;
@@ -16,6 +21,9 @@ function isMissingBrainSchemaError(err: unknown): boolean {
16
21
 
17
22
  /**
18
23
  * Handle onSessionStart - capture initial session context
24
+ *
25
+ * T138: Refresh memory bridge on session start.
26
+ * T139: Regenerate bridge with session scope context.
19
27
  */
20
28
  export async function handleSessionStart(
21
29
  projectRoot: string,
@@ -34,10 +42,16 @@ export async function handleSessionStart(
34
42
  } catch (err) {
35
43
  if (!isMissingBrainSchemaError(err)) throw err;
36
44
  }
45
+
46
+ // T138/T139: Refresh memory bridge after session starts (best-effort)
47
+ await maybeRefreshMemoryBridge(projectRoot);
37
48
  }
38
49
 
39
50
  /**
40
51
  * Handle onSessionEnd - capture session summary
52
+ *
53
+ * T138: Refresh memory bridge after session ends.
54
+ * T144: Extract transcript observations via cross-provider adapter.
41
55
  */
42
56
  export async function handleSessionEnd(
43
57
  projectRoot: string,
@@ -64,6 +78,30 @@ export async function handleSessionEnd(
64
78
  } catch {
65
79
  // Grading must never block session end
66
80
  }
81
+
82
+ // T144: Cross-provider transcript extraction (best-effort)
83
+ try {
84
+ const { loadConfig } = await import('../../config.js');
85
+ const config = await loadConfig(projectRoot);
86
+ if (config.brain?.autoCapture) {
87
+ const { AdapterManager } = await import('../../adapters/index.js');
88
+ const manager = AdapterManager.getInstance(projectRoot);
89
+ const activeAdapter = manager.getActive();
90
+ const hookProvider = activeAdapter?.hooks;
91
+ if (hookProvider && typeof hookProvider.getTranscript === 'function') {
92
+ const transcript = await hookProvider.getTranscript(payload.sessionId, projectRoot);
93
+ if (transcript) {
94
+ const { extractFromTranscript } = await import('../../memory/auto-extract.js');
95
+ await extractFromTranscript(projectRoot, payload.sessionId, transcript);
96
+ }
97
+ }
98
+ }
99
+ } catch {
100
+ // Graceful no-op: transcript extraction must never block session end
101
+ }
102
+
103
+ // T138: Refresh memory bridge after session ends (best-effort)
104
+ await maybeRefreshMemoryBridge(projectRoot);
67
105
  }
68
106
 
69
107
  // Register handlers on module load
@@ -3,10 +3,13 @@
3
3
  *
4
4
  * Handlers that capture task lifecycle events to BRAIN via memory.observe.
5
5
  * Auto-registers on module load.
6
+ *
7
+ * T138: Triggers memory bridge refresh after task completion.
6
8
  */
7
9
 
8
10
  import { hooks } from '../registry.js';
9
11
  import type { OnToolCompletePayload, OnToolStartPayload } from '../types.js';
12
+ import { maybeRefreshMemoryBridge } from './memory-bridge-refresh.js';
10
13
 
11
14
  function isMissingBrainSchemaError(err: unknown): boolean {
12
15
  if (!(err instanceof Error)) return false;
@@ -37,6 +40,8 @@ export async function handleToolStart(
37
40
 
38
41
  /**
39
42
  * Handle onToolComplete (maps to task.complete in CLEO)
43
+ *
44
+ * T138: Refresh memory bridge after task completion.
40
45
  */
41
46
  export async function handleToolComplete(
42
47
  projectRoot: string,
@@ -54,6 +59,9 @@ export async function handleToolComplete(
54
59
  } catch (err) {
55
60
  if (!isMissingBrainSchemaError(err)) throw err;
56
61
  }
62
+
63
+ // T138: Refresh memory bridge after task completes (best-effort)
64
+ await maybeRefreshMemoryBridge(projectRoot);
57
65
  }
58
66
 
59
67
  // Register handlers