@cleocode/core 2026.4.14 → 2026.4.16

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 (56) hide show
  1. package/dist/crypto/credentials.d.ts.map +1 -1
  2. package/dist/hooks/payload-schemas.d.ts +1 -1
  3. package/dist/hooks/payload-schemas.d.ts.map +1 -1
  4. package/dist/index.js +34095 -31432
  5. package/dist/index.js.map +4 -4
  6. package/dist/memory/brain-retrieval.d.ts +4 -0
  7. package/dist/memory/brain-retrieval.d.ts.map +1 -1
  8. package/dist/memory/embedding-local.d.ts +5 -5
  9. package/dist/memory/engine-compat.d.ts +4 -0
  10. package/dist/memory/engine-compat.d.ts.map +1 -1
  11. package/dist/memory/mental-model-injection.d.ts +52 -0
  12. package/dist/memory/mental-model-injection.d.ts.map +1 -0
  13. package/dist/memory/mental-model-queue.d.ts +75 -0
  14. package/dist/memory/mental-model-queue.d.ts.map +1 -0
  15. package/dist/orchestration/index.d.ts +2 -0
  16. package/dist/orchestration/index.d.ts.map +1 -1
  17. package/dist/paths.d.ts +65 -0
  18. package/dist/paths.d.ts.map +1 -1
  19. package/dist/store/brain-accessor.d.ts +2 -0
  20. package/dist/store/brain-accessor.d.ts.map +1 -1
  21. package/dist/store/brain-schema.d.ts +16 -0
  22. package/dist/store/brain-schema.d.ts.map +1 -1
  23. package/dist/store/nexus-validation-schemas.d.ts +1 -1
  24. package/dist/store/nexus-validation-schemas.d.ts.map +1 -1
  25. package/dist/store/validation-schemas.d.ts +1 -1
  26. package/dist/store/validation-schemas.d.ts.map +1 -1
  27. package/migrations/drizzle-brain/20260408000001_t417-agent-field/migration.sql +13 -0
  28. package/migrations/drizzle-brain/20260408000001_t417-agent-field/snapshot.json +28 -0
  29. package/package.json +15 -15
  30. package/src/__tests__/ct-master-tac-install.test.ts +168 -0
  31. package/src/crypto/credentials.ts +28 -0
  32. package/src/hooks/payload-schemas.ts +1 -1
  33. package/src/memory/__tests__/mental-model-wave-8.test.ts +355 -0
  34. package/src/memory/brain-retrieval.ts +55 -29
  35. package/src/memory/embedding-local.ts +5 -5
  36. package/src/memory/engine-compat.ts +24 -2
  37. package/src/memory/mental-model-injection.ts +87 -0
  38. package/src/memory/mental-model-queue.ts +291 -0
  39. package/src/orchestration/index.ts +2 -0
  40. package/src/paths.ts +79 -0
  41. package/src/store/brain-accessor.ts +5 -0
  42. package/src/store/brain-schema.ts +4 -0
  43. package/src/store/nexus-validation-schemas.ts +3 -3
  44. package/src/store/validation-schemas.ts +3 -3
  45. package/src/validation/protocols/cant/architecture-decision.cant +12 -2
  46. package/src/validation/protocols/cant/artifact-publish.cant +11 -1
  47. package/src/validation/protocols/cant/consensus.cant +12 -1
  48. package/src/validation/protocols/cant/contribution.cant +11 -1
  49. package/src/validation/protocols/cant/decomposition.cant +11 -1
  50. package/src/validation/protocols/cant/implementation.cant +11 -1
  51. package/src/validation/protocols/cant/provenance.cant +13 -1
  52. package/src/validation/protocols/cant/release.cant +12 -1
  53. package/src/validation/protocols/cant/research.cant +12 -1
  54. package/src/validation/protocols/cant/specification.cant +11 -1
  55. package/src/validation/protocols/cant/testing.cant +12 -1
  56. package/src/validation/protocols/cant/validation.cant +11 -1
@@ -58,6 +58,8 @@ export interface SearchBrainCompactParams {
58
58
  tables?: Array<'decisions' | 'patterns' | 'learnings' | 'observations'>;
59
59
  dateStart?: string;
60
60
  dateEnd?: string;
61
+ /** T418: filter results to observations produced by a specific agent (Wave 8 mental models). */
62
+ agent?: string;
61
63
  }
62
64
 
63
65
  /** Result from searchBrainCompact. */
@@ -121,6 +123,8 @@ export interface ObserveBrainParams {
121
123
  project?: string;
122
124
  sourceSessionId?: string;
123
125
  sourceType?: BrainObservationSourceType;
126
+ /** T417: agent provenance — the name of the spawned agent producing this observation. */
127
+ agent?: string;
124
128
  }
125
129
 
126
130
  /** Result from observeBrain. */
@@ -149,15 +153,21 @@ export async function searchBrainCompact(
149
153
  projectRoot: string,
150
154
  params: SearchBrainCompactParams,
151
155
  ): Promise<SearchBrainCompactResult> {
152
- const { query, limit, tables, dateStart, dateEnd } = params;
156
+ const { query, limit, tables, dateStart, dateEnd, agent } = params;
153
157
 
154
158
  if (!query || !query.trim()) {
155
159
  return { results: [], total: 0, tokensEstimated: 0 };
156
160
  }
157
161
 
162
+ // T418: when agent filter is set, restrict search to observations table only
163
+ const effectiveTables =
164
+ agent !== undefined && agent !== null
165
+ ? (['observations'] as Array<'decisions' | 'patterns' | 'learnings' | 'observations'>)
166
+ : tables;
167
+
158
168
  const searchResult = await searchBrain(projectRoot, query, {
159
169
  limit: limit ?? 10,
160
- tables,
170
+ tables: effectiveTables,
161
171
  });
162
172
 
163
173
  // Project full results to compact format.
@@ -166,38 +176,45 @@ export async function searchBrainCompact(
166
176
  // We handle both naming conventions for robustness.
167
177
  let results: BrainCompactHit[] = [];
168
178
 
169
- for (const d of searchResult.decisions) {
170
- const raw = d as Record<string, unknown>;
171
- results.push({
172
- id: d.id,
173
- type: 'decision',
174
- title: d.decision.slice(0, 80),
175
- date: (d.createdAt ?? (raw['created_at'] as string)) || '',
176
- });
177
- }
179
+ if (!agent) {
180
+ for (const d of searchResult.decisions) {
181
+ const raw = d as Record<string, unknown>;
182
+ results.push({
183
+ id: d.id,
184
+ type: 'decision',
185
+ title: d.decision.slice(0, 80),
186
+ date: (d.createdAt ?? (raw['created_at'] as string)) || '',
187
+ });
188
+ }
178
189
 
179
- for (const p of searchResult.patterns) {
180
- const raw = p as Record<string, unknown>;
181
- results.push({
182
- id: p.id,
183
- type: 'pattern',
184
- title: p.pattern.slice(0, 80),
185
- date: (p.extractedAt ?? (raw['extracted_at'] as string)) || '',
186
- });
187
- }
190
+ for (const p of searchResult.patterns) {
191
+ const raw = p as Record<string, unknown>;
192
+ results.push({
193
+ id: p.id,
194
+ type: 'pattern',
195
+ title: p.pattern.slice(0, 80),
196
+ date: (p.extractedAt ?? (raw['extracted_at'] as string)) || '',
197
+ });
198
+ }
188
199
 
189
- for (const l of searchResult.learnings) {
190
- const raw = l as Record<string, unknown>;
191
- results.push({
192
- id: l.id,
193
- type: 'learning',
194
- title: l.insight.slice(0, 80),
195
- date: (l.createdAt ?? (raw['created_at'] as string)) || '',
196
- });
200
+ for (const l of searchResult.learnings) {
201
+ const raw = l as Record<string, unknown>;
202
+ results.push({
203
+ id: l.id,
204
+ type: 'learning',
205
+ title: l.insight.slice(0, 80),
206
+ date: (l.createdAt ?? (raw['created_at'] as string)) || '',
207
+ });
208
+ }
197
209
  }
198
210
 
199
211
  for (const o of searchResult.observations) {
200
212
  const raw = o as Record<string, unknown>;
213
+ // T418: apply agent post-filter when specified
214
+ if (agent) {
215
+ const rowAgent = o.agent ?? (raw['agent'] as string | null) ?? null;
216
+ if (rowAgent !== agent) continue;
217
+ }
201
218
  results.push({
202
219
  id: o.id,
203
220
  type: 'observation',
@@ -524,7 +541,15 @@ export async function observeBrain(
524
541
  projectRoot: string,
525
542
  params: ObserveBrainParams,
526
543
  ): Promise<ObserveBrainResult> {
527
- const { text, title: titleParam, type: typeParam, project, sourceSessionId, sourceType } = params;
544
+ const {
545
+ text,
546
+ title: titleParam,
547
+ type: typeParam,
548
+ project,
549
+ sourceSessionId,
550
+ sourceType,
551
+ agent,
552
+ } = params;
528
553
 
529
554
  if (!text || !text.trim()) {
530
555
  throw new Error('Observation text is required');
@@ -588,6 +613,7 @@ export async function observeBrain(
588
613
  project: project ?? null,
589
614
  sourceSessionId: validSessionId,
590
615
  sourceType: sourceType ?? 'agent',
616
+ agent: agent ?? null,
591
617
  createdAt: now,
592
618
  });
593
619
 
@@ -10,11 +10,11 @@
10
10
  * @task T136
11
11
  * @why Ship vector search out-of-the-box without external API keys
12
12
  * @what Local embedding provider using @huggingface/transformers all-MiniLM-L6-v2
13
- * @remarks Migrated from @xenova/transformers (v2) to @huggingface/transformers
14
- * (v4) the upstream rename, same author. v4 drops the deprecated
15
- * prebuild-install transitive (via sharp@0.34+) and ships with newer
16
- * onnxruntime. Public API (`pipeline`, `FeatureExtractionPipeline`) is
17
- * unchanged; Xenova-hosted models still resolve by their original names.
13
+ * @remarks Brain embeddings are a FIRST-CLASS CLEO feature — the transformers
14
+ * package is a regular dependency of `@cleocode/core`, not optional.
15
+ * Migrated from `@xenova/transformers` v2 to `@huggingface/transformers`
16
+ * v4 (upstream rename, same author) which drops the deprecated
17
+ * `prebuild-install` transitive via `sharp@0.34+`.
18
18
  */
19
19
 
20
20
  import type { EmbeddingProvider } from './brain-embedding.js';
@@ -35,6 +35,8 @@ import {
35
35
  searchLearnings,
36
36
  storeLearning,
37
37
  } from './learnings.js';
38
+ // T419: async reinforcement queue for mental-model writes (ULTRAPLAN L5)
39
+ import { isMentalModelObservation, mentalModelQueue } from './mental-model-queue.js';
38
40
  // BRAIN memory imports (T4770)
39
41
  import {
40
42
  patternStats,
@@ -412,6 +414,8 @@ export async function memoryFind(
412
414
  tables?: string[];
413
415
  dateStart?: string;
414
416
  dateEnd?: string;
417
+ /** T418: filter results to observations produced by a specific agent. */
418
+ agent?: string;
415
419
  },
416
420
  projectRoot?: string,
417
421
  ): Promise<EngineResult> {
@@ -425,6 +429,7 @@ export async function memoryFind(
425
429
  | undefined,
426
430
  dateStart: params.dateStart,
427
431
  dateEnd: params.dateEnd,
432
+ agent: params.agent,
428
433
  });
429
434
  return { success: true, data: result };
430
435
  } catch (error) {
@@ -536,19 +541,36 @@ export async function memoryObserve(
536
541
  project?: string;
537
542
  sourceSessionId?: string;
538
543
  sourceType?: string;
544
+ /** T417: agent provenance — name of the spawned agent producing this observation. */
545
+ agent?: string;
539
546
  },
540
547
  projectRoot?: string,
541
548
  ): Promise<EngineResult> {
542
549
  try {
543
550
  const root = resolveRoot(projectRoot);
544
- const result = await observeBrain(root, {
551
+ const observeParams: ObserveBrainParams = {
545
552
  text: params.text,
546
553
  title: params.title,
547
554
  type: params.type as ObserveBrainParams['type'],
548
555
  project: params.project,
549
556
  sourceSessionId: params.sourceSessionId,
550
557
  sourceType: params.sourceType as ObserveBrainParams['sourceType'],
551
- });
558
+ agent: params.agent,
559
+ };
560
+
561
+ // T419: route mental-model observations (agent-tagged, relevant type) through
562
+ // the async reinforcement queue for non-blocking writes (ULTRAPLAN L5).
563
+ // All other observations use the existing synchronous path.
564
+ let result: Awaited<ReturnType<typeof observeBrain>>;
565
+ if (isMentalModelObservation(observeParams) && observeParams.agent) {
566
+ result = await mentalModelQueue.enqueue(root, {
567
+ ...observeParams,
568
+ agent: observeParams.agent,
569
+ });
570
+ } else {
571
+ result = await observeBrain(root, observeParams);
572
+ }
573
+
552
574
  return { success: true, data: result };
553
575
  } catch (error) {
554
576
  return {
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Pure helpers for validate-on-load mental-model injection.
3
+ *
4
+ * These helpers are used by:
5
+ * - The Pi CANT bridge (cleo-cant-bridge.ts) to build the system-prompt block
6
+ * - T421 empirical tests to assert on preamble content without a real Pi runtime
7
+ *
8
+ * No I/O. Safe to call in tests without a real DB or Pi extension context.
9
+ *
10
+ * @task T420
11
+ * @epic T377
12
+ * @wave W8
13
+ */
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ /** Minimal observation shape returned by memoryFind / searchBrainCompact. */
20
+ export interface MentalModelObservation {
21
+ /** Brain DB observation ID (O- prefix). */
22
+ id: string;
23
+ /** Observation type: discovery, change, feature, decision, bugfix, refactor, etc. */
24
+ type: string;
25
+ /** Short observation title or truncated text. */
26
+ title: string;
27
+ /** ISO date string for display. */
28
+ date?: string;
29
+ }
30
+
31
+ // ============================================================================
32
+ // Constants
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Preamble text injected into the Pi system prompt when an agent has a
37
+ * `mental_model:` CANT block. The agent MUST re-evaluate each observation
38
+ * against the current project state before acting.
39
+ *
40
+ * Exported so empirical tests (T421) can assert on its presence.
41
+ */
42
+ export const VALIDATE_ON_LOAD_PREAMBLE =
43
+ '===== MENTAL MODEL (validate-on-load) =====\n' +
44
+ 'These are your prior observations, patterns, and learnings for this project.\n' +
45
+ 'Before acting, you MUST re-evaluate each entry against current project state.\n' +
46
+ 'If an entry is stale, note it and proceed with fresh understanding.';
47
+
48
+ // ============================================================================
49
+ // Pure helpers
50
+ // ============================================================================
51
+
52
+ /**
53
+ * Build the validate-on-load mental-model injection string.
54
+ *
55
+ * Pure function — no I/O, safe to call in tests without a real DB.
56
+ *
57
+ * @param agentName - Name of the spawned agent (used in the header line).
58
+ * @param observations - Prior mental-model observations to list.
59
+ * @returns System-prompt block containing the preamble and numbered observations,
60
+ * or an empty string when `observations` is empty.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const block = buildMentalModelInjection('my-agent', [
65
+ * { id: 'O-abc1', type: 'discovery', title: 'Auth uses JWT', date: '2026-04-08' },
66
+ * ]);
67
+ * // block contains VALIDATE_ON_LOAD_PREAMBLE + numbered list
68
+ * ```
69
+ */
70
+ export function buildMentalModelInjection(
71
+ agentName: string,
72
+ observations: MentalModelObservation[],
73
+ ): string {
74
+ if (observations.length === 0) return '';
75
+
76
+ const lines: string[] = ['', `// Agent: ${agentName}`, VALIDATE_ON_LOAD_PREAMBLE, ''];
77
+
78
+ for (let i = 0; i < observations.length; i++) {
79
+ const obs = observations[i];
80
+ const datePart = obs.date ? ` [${obs.date}]` : '';
81
+ lines.push(`${i + 1}. [${obs.id}] (${obs.type})${datePart}: ${obs.title}`);
82
+ }
83
+
84
+ lines.push('===== END MENTAL MODEL =====');
85
+
86
+ return lines.join('\n');
87
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Async reinforcement queue for non-blocking mental-model writes.
3
+ *
4
+ * ULTRAPLAN L5 compliance: observations tagged with an `agent` provenance and a
5
+ * mental-model-relevant type ('discovery', 'change', 'feature', 'decision') are
6
+ * routed through this queue instead of writing synchronously to brain.db. This
7
+ * decouples the hot path (agent execution) from I/O latency.
8
+ *
9
+ * The queue is drained to brain.db either:
10
+ * 1. Periodically — every {@link FLUSH_INTERVAL_MS} milliseconds via a timer.
11
+ * 2. On high watermark — when the queue exceeds {@link FLUSH_WATERMARK} entries.
12
+ * 3. On process exit — SIGINT, SIGTERM, and 'exit' hooks perform a best-effort
13
+ * synchronous flush so no observations are lost.
14
+ *
15
+ * Observations without an `agent` field continue to use the existing synchronous
16
+ * path in observeBrain() and are never routed here.
17
+ *
18
+ * @task T383/T419
19
+ * @epic T377
20
+ */
21
+
22
+ import type { ObserveBrainParams, ObserveBrainResult } from './brain-retrieval.js';
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Constants
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** Drain interval in milliseconds. */
29
+ const FLUSH_INTERVAL_MS = 5_000;
30
+
31
+ /** Drain when queue exceeds this many entries, regardless of timer. */
32
+ const FLUSH_WATERMARK = 50;
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Types
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /** Queued observation entry with its write callback. */
39
+ interface QueuedObservation {
40
+ /** Project root the observation is scoped to. */
41
+ projectRoot: string;
42
+ /** Full observation parameters, including the required `agent` field. */
43
+ params: ObserveBrainParams & { agent: string };
44
+ /** Resolve callback — called with the persisted result after flush. */
45
+ resolve: (result: ObserveBrainResult) => void;
46
+ /** Reject callback — called if the observation cannot be persisted. */
47
+ reject: (err: Error) => void;
48
+ }
49
+
50
+ /**
51
+ * Public interface for the mental-model queue singleton.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * const q = getMentalModelQueue();
56
+ * await q.enqueue(projectRoot, { text: 'Agent learned X', agent: 'my-agent', ... });
57
+ * const remaining = q.size();
58
+ * await q.flush();
59
+ * ```
60
+ */
61
+ export interface MentalModelQueue {
62
+ /**
63
+ * Enqueue a mental-model observation for async write.
64
+ *
65
+ * Returns a Promise that resolves with the persisted {@link ObserveBrainResult}
66
+ * once the batch flush runs. Non-blocking for the caller.
67
+ *
68
+ * @param projectRoot - Project root directory for the brain.db path.
69
+ * @param params - Observation parameters. MUST include `agent`.
70
+ */
71
+ enqueue(
72
+ projectRoot: string,
73
+ params: ObserveBrainParams & { agent: string },
74
+ ): Promise<ObserveBrainResult>;
75
+
76
+ /**
77
+ * Drain the queue immediately.
78
+ *
79
+ * Writes all pending observations to brain.db.
80
+ * Safe to call concurrently — duplicate calls are serialised internally.
81
+ *
82
+ * @returns The number of observations successfully drained.
83
+ */
84
+ flush(): Promise<number>;
85
+
86
+ /** Current number of pending observations in the queue. */
87
+ size(): number;
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Observation types that route through the mental-model queue.
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /**
95
+ * Observation types that are considered mental-model relevant and therefore
96
+ * eligible for async queuing when produced by a named agent.
97
+ */
98
+ const MENTAL_MODEL_TYPES = new Set<string>([
99
+ 'discovery',
100
+ 'change',
101
+ 'feature',
102
+ 'decision',
103
+ 'bugfix',
104
+ 'refactor',
105
+ ]);
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Queue implementation
109
+ // ---------------------------------------------------------------------------
110
+
111
+ /** In-memory queue of pending mental-model observations. */
112
+ const _queue: QueuedObservation[] = [];
113
+
114
+ /** Whether a flush is currently in progress (prevents re-entrant flushes). */
115
+ let _flushing = false;
116
+
117
+ /** Whether process-exit hooks have been registered. */
118
+ let _hooksRegistered = false;
119
+
120
+ /** Handle for the periodic flush timer (undefined = no active timer). */
121
+ let _timer: ReturnType<typeof setInterval> | undefined;
122
+
123
+ /**
124
+ * Drain the queue synchronously where possible, or fall back to async writes.
125
+ * Returns when all observations in the current batch have been persisted.
126
+ */
127
+ async function drainQueue(): Promise<number> {
128
+ if (_queue.length === 0) return 0;
129
+
130
+ // Snapshot current batch and clear the queue
131
+ const batch = _queue.splice(0, _queue.length);
132
+ let count = 0;
133
+
134
+ // Import observeBrain lazily to avoid circular dependencies at module load
135
+ const { observeBrain } = await import('./brain-retrieval.js');
136
+
137
+ for (const entry of batch) {
138
+ try {
139
+ const result = await observeBrain(entry.projectRoot, entry.params);
140
+ entry.resolve(result);
141
+ count++;
142
+ } catch (err) {
143
+ const error = err instanceof Error ? err : new Error(String(err));
144
+ entry.reject(error);
145
+ }
146
+ }
147
+
148
+ return count;
149
+ }
150
+
151
+ /**
152
+ * Best-effort synchronous exit flush.
153
+ * Used in 'exit' event handler where async I/O is not guaranteed.
154
+ * Falls back to fire-and-forget if the environment does not support
155
+ * synchronous-style promises (i.e., in environments where the event
156
+ * loop may already be draining).
157
+ */
158
+ function exitFlush(): void {
159
+ if (_queue.length === 0) return;
160
+ // We can't reliably await in a synchronous exit handler. The best we
161
+ // can do is trigger the drain and hope the pending writes complete.
162
+ drainQueue().catch(() => {
163
+ // Silently swallow — process is terminating anyway
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Register process-exit hooks once.
169
+ * Ensures that no observations are silently dropped on graceful shutdown.
170
+ */
171
+ function registerExitHooks(): void {
172
+ if (_hooksRegistered) return;
173
+ _hooksRegistered = true;
174
+
175
+ process.on('exit', exitFlush);
176
+
177
+ process.once('SIGINT', () => {
178
+ drainQueue()
179
+ .catch(() => {
180
+ /* best-effort */
181
+ })
182
+ .finally(() => {
183
+ process.exit(130); // 128 + SIGINT
184
+ });
185
+ });
186
+
187
+ process.once('SIGTERM', () => {
188
+ drainQueue()
189
+ .catch(() => {
190
+ /* best-effort */
191
+ })
192
+ .finally(() => {
193
+ process.exit(143); // 128 + SIGTERM
194
+ });
195
+ });
196
+ }
197
+
198
+ /**
199
+ * Start the periodic flush timer if it isn't already running.
200
+ */
201
+ function ensureTimer(): void {
202
+ if (_timer !== undefined) return;
203
+ _timer = setInterval(() => {
204
+ if (_queue.length === 0) return;
205
+ if (_flushing) return;
206
+ _flushing = true;
207
+ drainQueue()
208
+ .catch(() => {
209
+ /* best-effort */
210
+ })
211
+ .finally(() => {
212
+ _flushing = false;
213
+ });
214
+ }, FLUSH_INTERVAL_MS);
215
+ // Unref so the timer doesn't prevent process exit when queue is idle
216
+ if (typeof _timer.unref === 'function') {
217
+ _timer.unref();
218
+ }
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Public singleton
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /**
226
+ * Mental-model queue singleton.
227
+ *
228
+ * Use this instead of calling observeBrain() directly when writing agent-tagged
229
+ * observations that should be queued for async persistence (ULTRAPLAN L5).
230
+ */
231
+ export const mentalModelQueue: MentalModelQueue = {
232
+ enqueue(
233
+ projectRoot: string,
234
+ params: ObserveBrainParams & { agent: string },
235
+ ): Promise<ObserveBrainResult> {
236
+ registerExitHooks();
237
+ ensureTimer();
238
+
239
+ return new Promise<ObserveBrainResult>((resolve, reject) => {
240
+ _queue.push({ projectRoot, params, resolve, reject });
241
+
242
+ // High-watermark flush
243
+ if (_queue.length >= FLUSH_WATERMARK && !_flushing) {
244
+ _flushing = true;
245
+ drainQueue()
246
+ .catch(() => {
247
+ /* best-effort */
248
+ })
249
+ .finally(() => {
250
+ _flushing = false;
251
+ });
252
+ }
253
+ });
254
+ },
255
+
256
+ async flush(): Promise<number> {
257
+ if (_flushing) {
258
+ // Wait for the current flush to settle then flush again
259
+ await new Promise<void>((r) => setTimeout(r, 50));
260
+ }
261
+ _flushing = true;
262
+ try {
263
+ return await drainQueue();
264
+ } finally {
265
+ _flushing = false;
266
+ }
267
+ },
268
+
269
+ size(): number {
270
+ return _queue.length;
271
+ },
272
+ };
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // Helpers
276
+ // ---------------------------------------------------------------------------
277
+
278
+ /**
279
+ * Determine whether an observation should be routed through the mental-model
280
+ * queue rather than written synchronously.
281
+ *
282
+ * Returns `true` when the observation has a non-empty `agent` field AND a
283
+ * mental-model-relevant type.
284
+ *
285
+ * @param params - Observation parameters to evaluate.
286
+ */
287
+ export function isMentalModelObservation(params: ObserveBrainParams): boolean {
288
+ if (!params.agent) return false;
289
+ const type = params.type ?? 'discovery';
290
+ return MENTAL_MODEL_TYPES.has(type);
291
+ }
@@ -55,6 +55,8 @@ export interface SpawnContext {
55
55
  fullyResolved: boolean;
56
56
  unresolvedTokens: string[];
57
57
  };
58
+ /** Optional compiled CANT agent definition attached by `cleo orchestrate spawn`. */
59
+ agentDef?: unknown;
58
60
  }
59
61
 
60
62
  /** Task readiness assessment. */