@cleocode/core 2026.3.58 → 2026.3.60

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 (153) hide show
  1. package/dist/agents/agent-registry.d.ts +206 -0
  2. package/dist/agents/agent-registry.d.ts.map +1 -0
  3. package/dist/agents/agent-registry.js +288 -0
  4. package/dist/agents/agent-registry.js.map +1 -0
  5. package/dist/agents/agent-schema.js +5 -0
  6. package/dist/agents/agent-schema.js.map +1 -1
  7. package/dist/agents/execution-learning.js +474 -0
  8. package/dist/agents/execution-learning.js.map +1 -0
  9. package/dist/agents/health-monitor.d.ts +161 -0
  10. package/dist/agents/health-monitor.d.ts.map +1 -0
  11. package/dist/agents/health-monitor.js +217 -0
  12. package/dist/agents/health-monitor.js.map +1 -0
  13. package/dist/agents/index.d.ts +3 -1
  14. package/dist/agents/index.d.ts.map +1 -1
  15. package/dist/agents/index.js +9 -1
  16. package/dist/agents/index.js.map +1 -1
  17. package/dist/agents/retry.d.ts +57 -4
  18. package/dist/agents/retry.d.ts.map +1 -1
  19. package/dist/agents/retry.js +57 -4
  20. package/dist/agents/retry.js.map +1 -1
  21. package/dist/backfill/index.d.ts +27 -0
  22. package/dist/backfill/index.d.ts.map +1 -1
  23. package/dist/backfill/index.js +229 -0
  24. package/dist/backfill/index.js.map +1 -0
  25. package/dist/bootstrap.d.ts +2 -1
  26. package/dist/bootstrap.d.ts.map +1 -1
  27. package/dist/bootstrap.js +135 -28
  28. package/dist/bootstrap.js.map +1 -1
  29. package/dist/cleo.d.ts +40 -0
  30. package/dist/cleo.d.ts.map +1 -1
  31. package/dist/config.js +83 -0
  32. package/dist/config.js.map +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1036 -536
  36. package/dist/index.js.map +4 -4
  37. package/dist/intelligence/adaptive-validation.js +497 -0
  38. package/dist/intelligence/adaptive-validation.js.map +1 -0
  39. package/dist/intelligence/impact.d.ts +34 -1
  40. package/dist/intelligence/impact.d.ts.map +1 -1
  41. package/dist/intelligence/impact.js +176 -0
  42. package/dist/intelligence/impact.js.map +1 -1
  43. package/dist/intelligence/index.d.ts +2 -2
  44. package/dist/intelligence/index.d.ts.map +1 -1
  45. package/dist/intelligence/index.js +6 -1
  46. package/dist/intelligence/index.js.map +1 -1
  47. package/dist/intelligence/types.d.ts +60 -0
  48. package/dist/intelligence/types.d.ts.map +1 -1
  49. package/dist/internal.d.ts +5 -4
  50. package/dist/internal.d.ts.map +1 -1
  51. package/dist/internal.js +11 -2
  52. package/dist/internal.js.map +1 -1
  53. package/dist/lib/index.d.ts +10 -0
  54. package/dist/lib/index.d.ts.map +1 -0
  55. package/dist/lib/index.js +10 -0
  56. package/dist/lib/index.js.map +1 -0
  57. package/dist/lib/retry.d.ts +128 -0
  58. package/dist/lib/retry.d.ts.map +1 -0
  59. package/dist/lib/retry.js +152 -0
  60. package/dist/lib/retry.js.map +1 -0
  61. package/dist/nexus/sharing/index.d.ts +48 -2
  62. package/dist/nexus/sharing/index.d.ts.map +1 -1
  63. package/dist/nexus/sharing/index.js +110 -1
  64. package/dist/nexus/sharing/index.js.map +1 -1
  65. package/dist/scaffold.d.ts.map +1 -1
  66. package/dist/scaffold.js +22 -2
  67. package/dist/scaffold.js.map +1 -1
  68. package/dist/sessions/session-enforcement.js +4 -0
  69. package/dist/sessions/session-enforcement.js.map +1 -1
  70. package/dist/stats/index.js +2 -0
  71. package/dist/stats/index.js.map +1 -1
  72. package/dist/stats/workflow-telemetry.d.ts +15 -0
  73. package/dist/stats/workflow-telemetry.d.ts.map +1 -1
  74. package/dist/stats/workflow-telemetry.js +400 -0
  75. package/dist/stats/workflow-telemetry.js.map +1 -0
  76. package/dist/store/brain-schema.js +4 -1
  77. package/dist/store/brain-schema.js.map +1 -1
  78. package/dist/store/converters.js +2 -0
  79. package/dist/store/converters.js.map +1 -1
  80. package/dist/store/cross-db-cleanup.d.ts +35 -0
  81. package/dist/store/cross-db-cleanup.d.ts.map +1 -1
  82. package/dist/store/cross-db-cleanup.js +169 -0
  83. package/dist/store/cross-db-cleanup.js.map +1 -0
  84. package/dist/store/db-helpers.js +2 -0
  85. package/dist/store/db-helpers.js.map +1 -1
  86. package/dist/store/migration-sqlite.js +5 -0
  87. package/dist/store/migration-sqlite.js.map +1 -1
  88. package/dist/store/sqlite-data-accessor.js +20 -28
  89. package/dist/store/sqlite-data-accessor.js.map +1 -1
  90. package/dist/store/sqlite.js +13 -2
  91. package/dist/store/sqlite.js.map +1 -1
  92. package/dist/store/task-store.js +4 -0
  93. package/dist/store/task-store.js.map +1 -1
  94. package/dist/store/tasks-schema.js +50 -20
  95. package/dist/store/tasks-schema.js.map +1 -1
  96. package/dist/tasks/add.js +87 -3
  97. package/dist/tasks/add.js.map +1 -1
  98. package/dist/tasks/complete.d.ts.map +1 -1
  99. package/dist/tasks/complete.js +15 -4
  100. package/dist/tasks/complete.js.map +1 -1
  101. package/dist/tasks/enforcement.d.ts.map +1 -1
  102. package/dist/tasks/enforcement.js +8 -1
  103. package/dist/tasks/enforcement.js.map +1 -1
  104. package/dist/tasks/epic-enforcement.d.ts +61 -0
  105. package/dist/tasks/epic-enforcement.d.ts.map +1 -1
  106. package/dist/tasks/epic-enforcement.js +294 -0
  107. package/dist/tasks/epic-enforcement.js.map +1 -0
  108. package/dist/tasks/index.js +1 -1
  109. package/dist/tasks/index.js.map +1 -1
  110. package/dist/tasks/pipeline-stage.d.ts +70 -1
  111. package/dist/tasks/pipeline-stage.d.ts.map +1 -1
  112. package/dist/tasks/pipeline-stage.js +248 -0
  113. package/dist/tasks/pipeline-stage.js.map +1 -0
  114. package/dist/tasks/update.js +28 -0
  115. package/dist/tasks/update.js.map +1 -1
  116. package/package.json +5 -5
  117. package/schemas/config.schema.json +37 -1547
  118. package/src/__tests__/sharing.test.ts +24 -0
  119. package/src/agents/__tests__/agent-registry.test.ts +351 -0
  120. package/src/agents/__tests__/health-monitor.test.ts +332 -0
  121. package/src/agents/agent-registry.ts +394 -0
  122. package/src/agents/health-monitor.ts +279 -0
  123. package/src/agents/index.ts +24 -1
  124. package/src/agents/retry.ts +57 -4
  125. package/src/backfill/index.ts +27 -0
  126. package/src/bootstrap.ts +171 -30
  127. package/src/cleo.ts +103 -2
  128. package/src/config.ts +3 -3
  129. package/src/index.ts +1 -0
  130. package/src/intelligence/__tests__/impact.test.ts +165 -1
  131. package/src/intelligence/impact.ts +203 -0
  132. package/src/intelligence/index.ts +3 -0
  133. package/src/intelligence/types.ts +76 -0
  134. package/src/internal.ts +20 -0
  135. package/src/lib/__tests__/retry.test.ts +321 -0
  136. package/src/lib/index.ts +16 -0
  137. package/src/lib/retry.ts +224 -0
  138. package/src/nexus/sharing/index.ts +142 -2
  139. package/src/scaffold.ts +24 -2
  140. package/src/stats/workflow-telemetry.ts +15 -0
  141. package/src/store/__tests__/session-store.test.ts +43 -7
  142. package/src/store/__tests__/task-store.test.ts +1 -1
  143. package/src/store/__tests__/test-db-helper.ts +7 -3
  144. package/src/store/cross-db-cleanup.ts +35 -0
  145. package/src/tasks/__tests__/epic-enforcement.test.ts +9 -4
  146. package/src/tasks/__tests__/minimal-test.test.ts +2 -2
  147. package/src/tasks/__tests__/update.test.ts +25 -25
  148. package/src/tasks/complete.ts +11 -6
  149. package/src/tasks/enforcement.ts +6 -3
  150. package/src/tasks/epic-enforcement.ts +61 -0
  151. package/src/tasks/pipeline-stage.ts +70 -1
  152. package/templates/config.template.json +5 -116
  153. package/templates/global-config.template.json +2 -44
@@ -153,5 +153,29 @@ describe('sharing', () => {
153
153
  expect(status.tracked).toContain('adrs/ADR-001.md');
154
154
  expect(status.ignored).toContain('tasks.db');
155
155
  });
156
+
157
+ it('returns safe defaults for git sync fields when .cleo/.git does not exist', async () => {
158
+ const status = await getSharingStatus(tempDir);
159
+
160
+ expect(status.hasGit).toBe(false);
161
+ expect(status.remotes).toEqual([]);
162
+ expect(status.pendingChanges).toBe(false);
163
+ expect(status.lastSync).toBeNull();
164
+ });
165
+
166
+ it('reports hasGit=true when .cleo/.git/HEAD exists', async () => {
167
+ // Simulate an initialized .cleo/.git repo (HEAD file is the sentinel)
168
+ const cleoDir = join(tempDir, '.cleo');
169
+ await mkdir(join(cleoDir, '.git'), { recursive: true });
170
+ await writeFile(join(cleoDir, '.git', 'HEAD'), 'ref: refs/heads/main\n');
171
+
172
+ const status = await getSharingStatus(tempDir);
173
+
174
+ expect(status.hasGit).toBe(true);
175
+ // With no actual git repo content, git commands will fail gracefully
176
+ expect(Array.isArray(status.remotes)).toBe(true);
177
+ expect(typeof status.pendingChanges).toBe('boolean');
178
+ expect(status.lastSync === null || typeof status.lastSync === 'string').toBe(true);
179
+ });
156
180
  });
157
181
  });
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Tests for the agent registry with capacity tracking.
3
+ *
4
+ * Covers: task-count-based capacity, specialization read/write,
5
+ * performance recording delegation, and sorted agent queries.
6
+ *
7
+ * @module agents/__tests__/agent-registry.test
8
+ * @task T041
9
+ * @epic T038
10
+ */
11
+
12
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
16
+
17
+ import {
18
+ getAgentCapacity,
19
+ getAgentSpecializations,
20
+ getAgentsByCapacity,
21
+ MAX_TASKS_PER_AGENT,
22
+ recordAgentPerformance,
23
+ updateAgentSpecializations,
24
+ } from '../agent-registry.js';
25
+ import { registerAgent, updateAgentStatus } from '../registry.js';
26
+
27
+ describe('Agent Registry (T041)', () => {
28
+ let tempDir: string;
29
+
30
+ beforeEach(async () => {
31
+ tempDir = await mkdtemp(join(tmpdir(), 'cleo-agent-registry-test-'));
32
+ await mkdir(join(tempDir, '.cleo'), { recursive: true });
33
+ await mkdir(join(tempDir, '.cleo', 'backups', 'operational'), { recursive: true });
34
+ });
35
+
36
+ afterEach(async () => {
37
+ try {
38
+ const { closeAllDatabases } = await import('../../store/sqlite.js');
39
+ await closeAllDatabases();
40
+ } catch {
41
+ /* module may not be loaded */
42
+ }
43
+ await Promise.race([
44
+ rm(tempDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 300 }).catch(() => {}),
45
+ new Promise<void>((resolve) => setTimeout(resolve, 8_000)),
46
+ ]);
47
+ });
48
+
49
+ // ==========================================================================
50
+ // MAX_TASKS_PER_AGENT constant
51
+ // ==========================================================================
52
+
53
+ describe('MAX_TASKS_PER_AGENT', () => {
54
+ it('is 5', () => {
55
+ expect(MAX_TASKS_PER_AGENT).toBe(5);
56
+ });
57
+ });
58
+
59
+ // ==========================================================================
60
+ // getAgentCapacity
61
+ // ==========================================================================
62
+
63
+ describe('getAgentCapacity', () => {
64
+ it('returns null for a non-existent agent', async () => {
65
+ const result = await getAgentCapacity('agt_nonexistent_abc123', tempDir);
66
+ expect(result).toBeNull();
67
+ });
68
+
69
+ it('returns full capacity for a newly registered idle agent', async () => {
70
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
71
+ await updateAgentStatus(agent.id, { status: 'active' }, tempDir);
72
+
73
+ const cap = await getAgentCapacity(agent.id, tempDir);
74
+ expect(cap).not.toBeNull();
75
+ expect(cap!.agentId).toBe(agent.id);
76
+ expect(cap!.maxCapacity).toBe(MAX_TASKS_PER_AGENT);
77
+ expect(cap!.activeTasks).toBe(0);
78
+ expect(cap!.remainingCapacity).toBe(MAX_TASKS_PER_AGENT);
79
+ expect(cap!.available).toBe(true);
80
+ });
81
+
82
+ it('counts the agent own task_id as 1 active task', async () => {
83
+ // We need a task row to satisfy the FK; insert one directly.
84
+ const { getDb } = await import('../../store/sqlite.js');
85
+ const { tasks: tasksTable } = await import('../../store/tasks-schema.js');
86
+ const db = await getDb(tempDir);
87
+ db.insert(tasksTable)
88
+ .values({
89
+ id: 'T-cap-001',
90
+ title: 'Capacity test task',
91
+ status: 'active',
92
+ priority: 'medium',
93
+ createdAt: new Date().toISOString(),
94
+ })
95
+ .run();
96
+
97
+ const agent = await registerAgent({ agentType: 'executor', taskId: 'T-cap-001' }, tempDir);
98
+ await updateAgentStatus(agent.id, { status: 'active' }, tempDir);
99
+
100
+ const cap = await getAgentCapacity(agent.id, tempDir);
101
+ expect(cap).not.toBeNull();
102
+ expect(cap!.activeTasks).toBe(1);
103
+ expect(cap!.remainingCapacity).toBe(MAX_TASKS_PER_AGENT - 1);
104
+ });
105
+
106
+ it('counts active child agents toward capacity', async () => {
107
+ const parent = await registerAgent({ agentType: 'orchestrator' }, tempDir);
108
+ await updateAgentStatus(parent.id, { status: 'active' }, tempDir);
109
+
110
+ // Register 3 child agents
111
+ for (let i = 0; i < 3; i++) {
112
+ const child = await registerAgent(
113
+ { agentType: 'executor', parentAgentId: parent.id },
114
+ tempDir,
115
+ );
116
+ await updateAgentStatus(child.id, { status: 'active' }, tempDir);
117
+ }
118
+
119
+ const cap = await getAgentCapacity(parent.id, tempDir);
120
+ expect(cap).not.toBeNull();
121
+ expect(cap!.activeTasks).toBe(3); // no own taskId + 3 children
122
+ expect(cap!.remainingCapacity).toBe(MAX_TASKS_PER_AGENT - 3);
123
+ expect(cap!.available).toBe(true);
124
+ });
125
+
126
+ it('returns zero remaining capacity for stopped agents', async () => {
127
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
128
+ await updateAgentStatus(agent.id, { status: 'stopped' }, tempDir);
129
+
130
+ const cap = await getAgentCapacity(agent.id, tempDir);
131
+ expect(cap).not.toBeNull();
132
+ expect(cap!.remainingCapacity).toBe(0);
133
+ expect(cap!.available).toBe(false);
134
+ });
135
+
136
+ it('returns zero remaining capacity for crashed agents', async () => {
137
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
138
+ await updateAgentStatus(agent.id, { status: 'crashed' }, tempDir);
139
+
140
+ const cap = await getAgentCapacity(agent.id, tempDir);
141
+ expect(cap).not.toBeNull();
142
+ expect(cap!.remainingCapacity).toBe(0);
143
+ expect(cap!.available).toBe(false);
144
+ });
145
+
146
+ it('excludes stopped children from active count', async () => {
147
+ const parent = await registerAgent({ agentType: 'orchestrator' }, tempDir);
148
+ await updateAgentStatus(parent.id, { status: 'active' }, tempDir);
149
+
150
+ const child = await registerAgent(
151
+ { agentType: 'executor', parentAgentId: parent.id },
152
+ tempDir,
153
+ );
154
+ // Immediately stop the child — should not count
155
+ await updateAgentStatus(child.id, { status: 'stopped' }, tempDir);
156
+
157
+ const cap = await getAgentCapacity(parent.id, tempDir);
158
+ expect(cap!.activeTasks).toBe(0);
159
+ expect(cap!.remainingCapacity).toBe(MAX_TASKS_PER_AGENT);
160
+ });
161
+ });
162
+
163
+ // ==========================================================================
164
+ // getAgentsByCapacity
165
+ // ==========================================================================
166
+
167
+ describe('getAgentsByCapacity', () => {
168
+ it('returns empty array when no active agents', async () => {
169
+ const result = await getAgentsByCapacity(undefined, tempDir);
170
+ expect(result).toEqual([]);
171
+ });
172
+
173
+ it('sorts agents by remaining capacity descending', async () => {
174
+ const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
175
+ const a2 = await registerAgent({ agentType: 'executor' }, tempDir);
176
+ const a3 = await registerAgent({ agentType: 'executor' }, tempDir);
177
+ await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
178
+ await updateAgentStatus(a2.id, { status: 'active' }, tempDir);
179
+ await updateAgentStatus(a3.id, { status: 'active' }, tempDir);
180
+
181
+ // Add 2 children to a1 — so a1 has less capacity
182
+ for (let i = 0; i < 2; i++) {
183
+ const child = await registerAgent({ agentType: 'executor', parentAgentId: a1.id }, tempDir);
184
+ await updateAgentStatus(child.id, { status: 'active' }, tempDir);
185
+ }
186
+ // Add 1 child to a2
187
+ const child2 = await registerAgent({ agentType: 'executor', parentAgentId: a2.id }, tempDir);
188
+ await updateAgentStatus(child2.id, { status: 'active' }, tempDir);
189
+ // a3 has 0 children — most capacity
190
+
191
+ const result = await getAgentsByCapacity(undefined, tempDir);
192
+
193
+ // Only parent agents (a1, a2, a3) should appear (children are 'active' too
194
+ // but they have no children themselves so they appear with full capacity).
195
+ // Filter to just a1/a2/a3 for ordering assertions:
196
+ const parents = result.filter((c) => [a1.id, a2.id, a3.id].includes(c.agentId));
197
+
198
+ // a3 (5 remaining) >= a2 (4 remaining) >= a1 (3 remaining)
199
+ expect(parents[0]!.remainingCapacity).toBeGreaterThanOrEqual(parents[1]!.remainingCapacity);
200
+ expect(parents[1]!.remainingCapacity).toBeGreaterThanOrEqual(parents[2]!.remainingCapacity);
201
+ });
202
+
203
+ it('filters by agent type', async () => {
204
+ await registerAgent({ agentType: 'executor' }, tempDir).then((a) =>
205
+ updateAgentStatus(a.id, { status: 'active' }, tempDir),
206
+ );
207
+ await registerAgent({ agentType: 'researcher' }, tempDir).then((a) =>
208
+ updateAgentStatus(a.id, { status: 'active' }, tempDir),
209
+ );
210
+
211
+ const executors = await getAgentsByCapacity('executor', tempDir);
212
+ expect(executors.every((c) => c.agentType === 'executor')).toBe(true);
213
+ });
214
+
215
+ it('excludes non-active agents', async () => {
216
+ const stopped = await registerAgent({ agentType: 'executor' }, tempDir);
217
+ await updateAgentStatus(stopped.id, { status: 'stopped' }, tempDir);
218
+
219
+ const result = await getAgentsByCapacity(undefined, tempDir);
220
+ expect(result.some((c) => c.agentId === stopped.id)).toBe(false);
221
+ });
222
+ });
223
+
224
+ // ==========================================================================
225
+ // getAgentSpecializations / updateAgentSpecializations
226
+ // ==========================================================================
227
+
228
+ describe('getAgentSpecializations', () => {
229
+ it('returns empty array for agent with no specializations', async () => {
230
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
231
+ const specs = await getAgentSpecializations(agent.id, tempDir);
232
+ expect(specs).toEqual([]);
233
+ });
234
+
235
+ it('returns empty array for non-existent agent', async () => {
236
+ const specs = await getAgentSpecializations('agt_nonexistent_abc123', tempDir);
237
+ expect(specs).toEqual([]);
238
+ });
239
+ });
240
+
241
+ describe('updateAgentSpecializations', () => {
242
+ it('stores and retrieves specializations', async () => {
243
+ const agent = await registerAgent({ agentType: 'architect' }, tempDir);
244
+ await updateAgentSpecializations(agent.id, ['typescript', 'drizzle-orm', 'sqlite'], tempDir);
245
+
246
+ const specs = await getAgentSpecializations(agent.id, tempDir);
247
+ expect(specs).toEqual(['typescript', 'drizzle-orm', 'sqlite']);
248
+ });
249
+
250
+ it('replaces existing specializations', async () => {
251
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
252
+ await updateAgentSpecializations(agent.id, ['python'], tempDir);
253
+ await updateAgentSpecializations(agent.id, ['typescript', 'testing'], tempDir);
254
+
255
+ const specs = await getAgentSpecializations(agent.id, tempDir);
256
+ expect(specs).toEqual(['typescript', 'testing']);
257
+ });
258
+
259
+ it('preserves other metadata keys', async () => {
260
+ const agent = await registerAgent(
261
+ { agentType: 'executor', metadata: { model: 'opus-4', region: 'us-east' } },
262
+ tempDir,
263
+ );
264
+ await updateAgentSpecializations(agent.id, ['research'], tempDir);
265
+
266
+ // Pull raw metadata to verify other keys survive
267
+ const { getDb } = await import('../../store/sqlite.js');
268
+ const { agentInstances: table } = await import('../agent-schema.js');
269
+ const { eq } = await import('drizzle-orm');
270
+ const db = await getDb(tempDir);
271
+ const row = await db
272
+ .select({ metadataJson: table.metadataJson })
273
+ .from(table)
274
+ .where(eq(table.id, agent.id))
275
+ .get();
276
+
277
+ const parsed = JSON.parse(row!.metadataJson ?? '{}') as Record<string, unknown>;
278
+ expect(parsed.model).toBe('opus-4');
279
+ expect(parsed.region).toBe('us-east');
280
+ expect(parsed.specializations).toEqual(['research']);
281
+ });
282
+
283
+ it('returns null for non-existent agent', async () => {
284
+ const result = await updateAgentSpecializations(
285
+ 'agt_nonexistent_abc123',
286
+ ['typescript'],
287
+ tempDir,
288
+ );
289
+ expect(result).toBeNull();
290
+ });
291
+ });
292
+
293
+ // ==========================================================================
294
+ // recordAgentPerformance
295
+ // ==========================================================================
296
+
297
+ describe('recordAgentPerformance', () => {
298
+ it('returns null for non-existent agent', async () => {
299
+ const result = await recordAgentPerformance(
300
+ 'agt_nonexistent_abc123',
301
+ { taskId: 'T001', taskType: 'task', outcome: 'success' },
302
+ tempDir,
303
+ );
304
+ expect(result).toBeNull();
305
+ });
306
+
307
+ it('records performance and returns a decision ID', async () => {
308
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
309
+ await updateAgentStatus(agent.id, { status: 'active' }, tempDir);
310
+
311
+ const decisionId = await recordAgentPerformance(
312
+ agent.id,
313
+ {
314
+ taskId: 'T041',
315
+ taskType: 'task',
316
+ outcome: 'success',
317
+ durationMs: 3000,
318
+ taskLabels: ['implementation'],
319
+ },
320
+ tempDir,
321
+ );
322
+
323
+ // brain.db may not be initialised in the test tmpDir, so we accept
324
+ // either a string ID or null (best-effort recording)
325
+ if (decisionId !== null) {
326
+ expect(typeof decisionId).toBe('string');
327
+ expect(decisionId.length).toBeGreaterThan(0);
328
+ }
329
+ });
330
+
331
+ it('records failure with error metadata', async () => {
332
+ const agent = await registerAgent({ agentType: 'executor', sessionId: undefined }, tempDir);
333
+ await updateAgentStatus(agent.id, { status: 'active' }, tempDir);
334
+
335
+ // Should not throw regardless of brain.db availability
336
+ await expect(
337
+ recordAgentPerformance(
338
+ agent.id,
339
+ {
340
+ taskId: 'T999',
341
+ taskType: 'task',
342
+ outcome: 'failure',
343
+ errorMessage: 'ECONNREFUSED',
344
+ errorType: 'retriable',
345
+ },
346
+ tempDir,
347
+ ),
348
+ ).resolves.not.toThrow();
349
+ });
350
+ });
351
+ });