@cleocode/core 2026.3.43 → 2026.3.45

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 (183) hide show
  1. package/dist/admin/export-tasks.d.ts.map +1 -1
  2. package/dist/admin/import-tasks.d.ts +10 -2
  3. package/dist/admin/import-tasks.d.ts.map +1 -1
  4. package/dist/agents/agent-schema.d.ts +358 -0
  5. package/dist/agents/agent-schema.d.ts.map +1 -0
  6. package/dist/agents/capacity.d.ts +57 -0
  7. package/dist/agents/capacity.d.ts.map +1 -0
  8. package/dist/agents/index.d.ts +17 -0
  9. package/dist/agents/index.d.ts.map +1 -0
  10. package/dist/agents/registry.d.ts +115 -0
  11. package/dist/agents/registry.d.ts.map +1 -0
  12. package/dist/agents/retry.d.ts +83 -0
  13. package/dist/agents/retry.d.ts.map +1 -0
  14. package/dist/hooks/index.d.ts +4 -1
  15. package/dist/hooks/index.d.ts.map +1 -1
  16. package/dist/hooks/payload-schemas.d.ts +214 -0
  17. package/dist/hooks/payload-schemas.d.ts.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +16937 -2371
  21. package/dist/index.js.map +4 -4
  22. package/dist/inject/index.d.ts.map +1 -1
  23. package/dist/intelligence/impact.d.ts +51 -0
  24. package/dist/intelligence/impact.d.ts.map +1 -0
  25. package/dist/intelligence/index.d.ts +15 -0
  26. package/dist/intelligence/index.d.ts.map +1 -0
  27. package/dist/intelligence/patterns.d.ts +66 -0
  28. package/dist/intelligence/patterns.d.ts.map +1 -0
  29. package/dist/intelligence/prediction.d.ts +51 -0
  30. package/dist/intelligence/prediction.d.ts.map +1 -0
  31. package/dist/intelligence/types.d.ts +221 -0
  32. package/dist/intelligence/types.d.ts.map +1 -0
  33. package/dist/internal.d.ts +12 -1
  34. package/dist/internal.d.ts.map +1 -1
  35. package/dist/issue/template-parser.d.ts +8 -2
  36. package/dist/issue/template-parser.d.ts.map +1 -1
  37. package/dist/lifecycle/pipeline.d.ts +2 -2
  38. package/dist/lifecycle/pipeline.d.ts.map +1 -1
  39. package/dist/lifecycle/state-machine.d.ts +1 -1
  40. package/dist/lifecycle/state-machine.d.ts.map +1 -1
  41. package/dist/memory/brain-lifecycle.d.ts.map +1 -1
  42. package/dist/memory/brain-retrieval.d.ts.map +1 -1
  43. package/dist/memory/brain-row-types.d.ts +40 -6
  44. package/dist/memory/brain-row-types.d.ts.map +1 -1
  45. package/dist/memory/brain-search.d.ts.map +1 -1
  46. package/dist/memory/brain-similarity.d.ts.map +1 -1
  47. package/dist/memory/claude-mem-migration.d.ts.map +1 -1
  48. package/dist/nexus/discover.d.ts.map +1 -1
  49. package/dist/nexus/index.d.ts +2 -0
  50. package/dist/nexus/index.d.ts.map +1 -1
  51. package/dist/nexus/transfer-types.d.ts +123 -0
  52. package/dist/nexus/transfer-types.d.ts.map +1 -0
  53. package/dist/nexus/transfer.d.ts +31 -0
  54. package/dist/nexus/transfer.d.ts.map +1 -0
  55. package/dist/orchestration/bootstrap.d.ts.map +1 -1
  56. package/dist/orchestration/skill-ops.d.ts +4 -4
  57. package/dist/orchestration/skill-ops.d.ts.map +1 -1
  58. package/dist/otel/index.d.ts +1 -1
  59. package/dist/otel/index.d.ts.map +1 -1
  60. package/dist/sessions/briefing.d.ts.map +1 -1
  61. package/dist/sessions/handoff.d.ts.map +1 -1
  62. package/dist/sessions/index.d.ts +1 -1
  63. package/dist/sessions/index.d.ts.map +1 -1
  64. package/dist/sessions/types.d.ts +8 -42
  65. package/dist/sessions/types.d.ts.map +1 -1
  66. package/dist/signaldock/signaldock-transport.d.ts +1 -1
  67. package/dist/signaldock/signaldock-transport.d.ts.map +1 -1
  68. package/dist/skills/injection/subagent.d.ts +3 -3
  69. package/dist/skills/injection/subagent.d.ts.map +1 -1
  70. package/dist/skills/manifests/contribution.d.ts +2 -2
  71. package/dist/skills/manifests/contribution.d.ts.map +1 -1
  72. package/dist/skills/orchestrator/spawn.d.ts +6 -6
  73. package/dist/skills/orchestrator/spawn.d.ts.map +1 -1
  74. package/dist/skills/orchestrator/startup.d.ts +1 -1
  75. package/dist/skills/orchestrator/startup.d.ts.map +1 -1
  76. package/dist/skills/orchestrator/validator.d.ts +2 -2
  77. package/dist/skills/orchestrator/validator.d.ts.map +1 -1
  78. package/dist/skills/precedence-types.d.ts +24 -1
  79. package/dist/skills/precedence-types.d.ts.map +1 -1
  80. package/dist/skills/types.d.ts +70 -4
  81. package/dist/skills/types.d.ts.map +1 -1
  82. package/dist/store/brain-sqlite.d.ts +4 -1
  83. package/dist/store/brain-sqlite.d.ts.map +1 -1
  84. package/dist/store/export.d.ts +5 -4
  85. package/dist/store/export.d.ts.map +1 -1
  86. package/dist/store/nexus-sqlite.d.ts +4 -1
  87. package/dist/store/nexus-sqlite.d.ts.map +1 -1
  88. package/dist/store/sqlite.d.ts +4 -1
  89. package/dist/store/sqlite.d.ts.map +1 -1
  90. package/dist/store/tasks-schema.d.ts +14 -4
  91. package/dist/store/tasks-schema.d.ts.map +1 -1
  92. package/dist/store/typed-query.d.ts +12 -0
  93. package/dist/store/typed-query.d.ts.map +1 -0
  94. package/dist/store/validation-schemas.d.ts +2423 -50
  95. package/dist/store/validation-schemas.d.ts.map +1 -1
  96. package/dist/system/inject-generate.d.ts.map +1 -1
  97. package/dist/validation/doctor/checks.d.ts +5 -0
  98. package/dist/validation/doctor/checks.d.ts.map +1 -1
  99. package/dist/validation/engine.d.ts +10 -10
  100. package/dist/validation/engine.d.ts.map +1 -1
  101. package/dist/validation/index.d.ts +6 -2
  102. package/dist/validation/index.d.ts.map +1 -1
  103. package/dist/validation/protocol-common.d.ts +10 -2
  104. package/dist/validation/protocol-common.d.ts.map +1 -1
  105. package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/migration.sql +84 -0
  106. package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/snapshot.json +4060 -0
  107. package/migrations/drizzle-tasks/20260320020000_agent-dimension/migration.sql +35 -0
  108. package/migrations/drizzle-tasks/20260320020000_agent-dimension/snapshot.json +4312 -0
  109. package/package.json +2 -2
  110. package/src/admin/export-tasks.ts +2 -5
  111. package/src/admin/import-tasks.ts +53 -29
  112. package/src/agents/__tests__/capacity.test.ts +219 -0
  113. package/src/agents/__tests__/registry.test.ts +457 -0
  114. package/src/agents/__tests__/retry.test.ts +289 -0
  115. package/src/agents/agent-schema.ts +107 -0
  116. package/src/agents/capacity.ts +151 -0
  117. package/src/agents/index.ts +68 -0
  118. package/src/agents/registry.ts +449 -0
  119. package/src/agents/retry.ts +255 -0
  120. package/src/hooks/index.ts +20 -1
  121. package/src/hooks/payload-schemas.ts +199 -0
  122. package/src/index.ts +69 -0
  123. package/src/inject/index.ts +14 -14
  124. package/src/intelligence/__tests__/impact.test.ts +453 -0
  125. package/src/intelligence/__tests__/patterns.test.ts +450 -0
  126. package/src/intelligence/__tests__/prediction.test.ts +418 -0
  127. package/src/intelligence/impact.ts +638 -0
  128. package/src/intelligence/index.ts +47 -0
  129. package/src/intelligence/patterns.ts +621 -0
  130. package/src/intelligence/prediction.ts +621 -0
  131. package/src/intelligence/types.ts +273 -0
  132. package/src/internal.ts +89 -2
  133. package/src/issue/template-parser.ts +65 -4
  134. package/src/lifecycle/pipeline.ts +14 -7
  135. package/src/lifecycle/state-machine.ts +6 -2
  136. package/src/memory/brain-lifecycle.ts +5 -11
  137. package/src/memory/brain-retrieval.ts +44 -38
  138. package/src/memory/brain-row-types.ts +43 -6
  139. package/src/memory/brain-search.ts +53 -32
  140. package/src/memory/brain-similarity.ts +9 -8
  141. package/src/memory/claude-mem-migration.ts +4 -3
  142. package/src/nexus/__tests__/nexus-e2e.test.ts +1481 -0
  143. package/src/nexus/__tests__/transfer.test.ts +446 -0
  144. package/src/nexus/discover.ts +1 -0
  145. package/src/nexus/index.ts +14 -0
  146. package/src/nexus/transfer-types.ts +129 -0
  147. package/src/nexus/transfer.ts +314 -0
  148. package/src/orchestration/bootstrap.ts +11 -17
  149. package/src/orchestration/skill-ops.ts +52 -32
  150. package/src/otel/index.ts +48 -4
  151. package/src/sessions/__tests__/briefing.test.ts +31 -2
  152. package/src/sessions/briefing.ts +27 -42
  153. package/src/sessions/handoff.ts +52 -86
  154. package/src/sessions/index.ts +5 -1
  155. package/src/sessions/types.ts +9 -43
  156. package/src/signaldock/signaldock-transport.ts +5 -2
  157. package/src/skills/injection/subagent.ts +10 -16
  158. package/src/skills/manifests/contribution.ts +5 -13
  159. package/src/skills/orchestrator/__tests__/spawn-tier.test.ts +44 -30
  160. package/src/skills/orchestrator/spawn.ts +18 -31
  161. package/src/skills/orchestrator/startup.ts +78 -65
  162. package/src/skills/orchestrator/validator.ts +26 -31
  163. package/src/skills/precedence-types.ts +24 -1
  164. package/src/skills/types.ts +72 -5
  165. package/src/store/__tests__/test-db-helper.d.ts +4 -4
  166. package/src/store/__tests__/test-db-helper.js +5 -16
  167. package/src/store/__tests__/test-db-helper.ts +5 -18
  168. package/src/store/brain-sqlite.ts +7 -3
  169. package/src/store/chain-schema.ts +1 -1
  170. package/src/store/export.ts +22 -12
  171. package/src/store/nexus-sqlite.ts +7 -3
  172. package/src/store/sqlite.ts +9 -3
  173. package/src/store/tasks-schema.ts +65 -8
  174. package/src/store/typed-query.ts +17 -0
  175. package/src/store/validation-schemas.ts +347 -23
  176. package/src/system/inject-generate.ts +9 -23
  177. package/src/validation/doctor/checks.ts +24 -2
  178. package/src/validation/engine.ts +11 -11
  179. package/src/validation/index.ts +131 -3
  180. package/src/validation/protocol-common.ts +54 -3
  181. package/dist/tasks/reparent.d.ts +0 -38
  182. package/dist/tasks/reparent.d.ts.map +0 -1
  183. package/src/tasks/reparent.ts +0 -134
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/core",
3
- "version": "2026.3.43",
3
+ "version": "2026.3.45",
4
4
  "description": "CLEO core business logic kernel — tasks, sessions, memory, orchestration, lifecycle, with bundled SQLite store",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,7 +36,7 @@
36
36
  "write-file-atomic": "^6.0.0",
37
37
  "yaml": "^2.8.2",
38
38
  "zod": "^3.25.76",
39
- "@cleocode/contracts": "2026.3.43"
39
+ "@cleocode/contracts": "2026.3.45"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=24.0.0"
@@ -175,15 +175,12 @@ export async function exportTasksPackage(params: ExportTasksParams): Promise<Exp
175
175
  }
176
176
 
177
177
  const projectMeta = await accessor.getMetaValue<{ name?: string }>('project');
178
- const taskData = {
179
- tasks: allTasks,
180
- project: projectMeta,
181
- } as import('@cleocode/contracts').TaskFile;
182
- const pkg = buildExportPackage(selectedTasks, taskData, {
178
+ const pkg = buildExportPackage(selectedTasks, {
183
179
  mode: exportMode,
184
180
  rootTaskIds: parsedIds.length > 0 ? parsedIds : selectedTasks.map((t) => t.id),
185
181
  includeChildren: subtreeMode,
186
182
  filters: filters.length > 0 ? filters : undefined,
183
+ projectName: projectMeta?.name,
187
184
  });
188
185
 
189
186
  const content = JSON.stringify(pkg, null, 2);
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * Extracted from CLI import-tasks command for dispatch layer access.
5
5
  *
6
- * @task T5323, T5328
6
+ * @task T5323, T5328, T046
7
7
  * @epic T4545
8
8
  */
9
9
 
10
10
  import { constants as fsConstants } from 'node:fs';
11
11
  import { access, readFile } from 'node:fs/promises';
12
12
  import type { Task, TaskStatus } from '@cleocode/contracts';
13
+ import type { ImportFromPackageOptions, ImportFromPackageResult } from '../nexus/transfer-types.js';
13
14
  import { getAccessor } from '../store/data-accessor.js';
14
15
  import type { ExportPackage } from '../store/export.js';
15
16
  import {
@@ -74,25 +75,13 @@ export interface ImportTasksResult {
74
75
  }
75
76
 
76
77
  /**
77
- * Import tasks from a cross-project export package with ID remapping.
78
+ * Import tasks from an in-memory ExportPackage with ID remapping.
79
+ * Core logic extracted from importTasksPackage for reuse by transfer engine.
78
80
  */
79
- export async function importTasksPackage(params: ImportTasksParams): Promise<ImportTasksResult> {
80
- const { file } = params;
81
-
82
- try {
83
- await access(file, fsConstants.R_OK);
84
- } catch {
85
- throw new Error(`Export file not found: ${file}`);
86
- }
87
-
88
- const content = await readFile(file, 'utf-8');
89
- let exportPkg: ExportPackage;
90
- try {
91
- exportPkg = JSON.parse(content) as ExportPackage;
92
- } catch {
93
- throw new Error(`Invalid JSON in: ${file}`);
94
- }
95
-
81
+ export async function importFromPackage(
82
+ exportPkg: ExportPackage,
83
+ options: ImportFromPackageOptions = {},
84
+ ): Promise<ImportFromPackageResult> {
96
85
  if (exportPkg._meta?.format !== 'cleo-export') {
97
86
  throw new Error(
98
87
  `Invalid export format (expected 'cleo-export', got '${exportPkg._meta?.format}')`,
@@ -103,17 +92,17 @@ export async function importTasksPackage(params: ImportTasksParams): Promise<Imp
103
92
  throw new Error('Export package contains no tasks');
104
93
  }
105
94
 
106
- const accessor = await getAccessor(params.cwd);
95
+ const accessor = await getAccessor(options.cwd);
107
96
  const { tasks: existingTasks } = await accessor.queryTasks({});
108
97
 
109
- const onConflict: OnConflict = params.onConflict ?? 'fail';
110
- const onMissingDep: OnMissingDep = params.onMissingDep ?? 'strip';
111
- const force = params.force ?? false;
112
- const parentId = params.parent;
113
- const phaseOverride = params.phase;
114
- const addLabel = params.addLabel;
115
- const resetStatus = params.resetStatus;
116
- const addProvenance = params.provenance !== false;
98
+ const onConflict: OnConflict = options.onConflict ?? 'fail';
99
+ const onMissingDep: OnMissingDep = options.onMissingDep === 'fail' ? 'fail' : 'strip';
100
+ const force = options.force ?? false;
101
+ const parentId = options.parent;
102
+ const phaseOverride = options.phase;
103
+ const addLabel = options.addLabel;
104
+ const resetStatus = options.resetStatus;
105
+ const addProvenance = options.provenance !== false;
117
106
 
118
107
  if (parentId) {
119
108
  const parentExists = existingTasks.some((t) => t.id === parentId);
@@ -195,7 +184,7 @@ export async function importTasksPackage(params: ImportTasksParams): Promise<Imp
195
184
  existingIds.add(remapped.id);
196
185
  }
197
186
 
198
- if (params.dryRun) {
187
+ if (options.dryRun) {
199
188
  return {
200
189
  imported: transformed.length,
201
190
  skipped: skipped.length,
@@ -217,3 +206,38 @@ export async function importTasksPackage(params: ImportTasksParams): Promise<Imp
217
206
  idRemap: idRemapJson,
218
207
  };
219
208
  }
209
+
210
+ /**
211
+ * Import tasks from a cross-project export package file with ID remapping.
212
+ * Thin wrapper around importFromPackage that handles file I/O.
213
+ */
214
+ export async function importTasksPackage(params: ImportTasksParams): Promise<ImportTasksResult> {
215
+ const { file } = params;
216
+
217
+ try {
218
+ await access(file, fsConstants.R_OK);
219
+ } catch {
220
+ throw new Error(`Export file not found: ${file}`);
221
+ }
222
+
223
+ const content = await readFile(file, 'utf-8');
224
+ let exportPkg: ExportPackage;
225
+ try {
226
+ exportPkg = JSON.parse(content) as ExportPackage;
227
+ } catch {
228
+ throw new Error(`Invalid JSON in: ${file}`);
229
+ }
230
+
231
+ return importFromPackage(exportPkg, {
232
+ cwd: params.cwd,
233
+ dryRun: params.dryRun,
234
+ parent: params.parent,
235
+ phase: params.phase,
236
+ addLabel: params.addLabel,
237
+ provenance: params.provenance,
238
+ resetStatus: params.resetStatus,
239
+ onConflict: params.onConflict,
240
+ onMissingDep: params.onMissingDep === 'placeholder' ? 'strip' : params.onMissingDep,
241
+ force: params.force,
242
+ });
243
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Tests for capacity tracking and load balancing.
3
+ *
4
+ * @module agents/__tests__/capacity.test
5
+ */
6
+
7
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
11
+
12
+ import {
13
+ findLeastLoadedAgent,
14
+ getAvailableCapacity,
15
+ getCapacitySummary,
16
+ isOverloaded,
17
+ updateCapacity,
18
+ } from '../capacity.js';
19
+ import { registerAgent, updateAgentStatus } from '../registry.js';
20
+
21
+ describe('Capacity Tracking', () => {
22
+ let tempDir: string;
23
+
24
+ beforeEach(async () => {
25
+ tempDir = await mkdtemp(join(tmpdir(), 'cleo-capacity-test-'));
26
+ await mkdir(join(tempDir, '.cleo'), { recursive: true });
27
+ await mkdir(join(tempDir, '.cleo', 'backups', 'operational'), { recursive: true });
28
+ });
29
+
30
+ afterEach(async () => {
31
+ try {
32
+ const { closeAllDatabases } = await import('../../store/sqlite.js');
33
+ await closeAllDatabases();
34
+ } catch {
35
+ /* module may not be loaded */
36
+ }
37
+ await Promise.race([
38
+ rm(tempDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 300 }).catch(() => {}),
39
+ new Promise<void>((resolve) => setTimeout(resolve, 8_000)),
40
+ ]);
41
+ });
42
+
43
+ // ==========================================================================
44
+ // updateCapacity
45
+ // ==========================================================================
46
+
47
+ describe('updateCapacity', () => {
48
+ it('updates capacity value', async () => {
49
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
50
+ const updated = await updateCapacity(agent.id, 0.5, tempDir);
51
+
52
+ expect(updated).not.toBeNull();
53
+ expect(parseFloat(updated!.capacity)).toBeCloseTo(0.5, 4);
54
+ });
55
+
56
+ it('returns null for non-existent agent', async () => {
57
+ const result = await updateCapacity('agt_nonexistent_abc123', 0.5, tempDir);
58
+ expect(result).toBeNull();
59
+ });
60
+
61
+ it('rejects capacity below 0', async () => {
62
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
63
+ await expect(updateCapacity(agent.id, -0.1, tempDir)).rejects.toThrow(
64
+ 'Capacity must be between 0.0 and 1.0',
65
+ );
66
+ });
67
+
68
+ it('rejects capacity above 1', async () => {
69
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
70
+ await expect(updateCapacity(agent.id, 1.5, tempDir)).rejects.toThrow(
71
+ 'Capacity must be between 0.0 and 1.0',
72
+ );
73
+ });
74
+
75
+ it('accepts boundary values', async () => {
76
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
77
+
78
+ const zero = await updateCapacity(agent.id, 0, tempDir);
79
+ expect(parseFloat(zero!.capacity)).toBeCloseTo(0, 4);
80
+
81
+ const one = await updateCapacity(agent.id, 1, tempDir);
82
+ expect(parseFloat(one!.capacity)).toBeCloseTo(1, 4);
83
+ });
84
+ });
85
+
86
+ // ==========================================================================
87
+ // getAvailableCapacity
88
+ // ==========================================================================
89
+
90
+ describe('getAvailableCapacity', () => {
91
+ it('sums capacity of active and idle agents', async () => {
92
+ const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
93
+ const a2 = await registerAgent({ agentType: 'researcher' }, tempDir);
94
+ await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
95
+ await updateAgentStatus(a2.id, { status: 'idle' }, tempDir);
96
+ await updateCapacity(a1.id, 0.7, tempDir);
97
+ await updateCapacity(a2.id, 0.3, tempDir);
98
+
99
+ const capacity = await getAvailableCapacity(tempDir);
100
+ expect(capacity).toBeCloseTo(1.0, 1);
101
+ });
102
+
103
+ it('excludes stopped and crashed agents', async () => {
104
+ const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
105
+ const a2 = await registerAgent({ agentType: 'researcher' }, tempDir);
106
+ const a3 = await registerAgent({ agentType: 'validator' }, tempDir);
107
+ await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
108
+ await updateAgentStatus(a2.id, { status: 'stopped' }, tempDir);
109
+ await updateAgentStatus(a3.id, { status: 'crashed' }, tempDir);
110
+
111
+ const capacity = await getAvailableCapacity(tempDir);
112
+ // Only a1 contributes (1.0 default)
113
+ expect(capacity).toBeCloseTo(1.0, 1);
114
+ });
115
+
116
+ it('returns 0 when no active agents', async () => {
117
+ const capacity = await getAvailableCapacity(tempDir);
118
+ expect(capacity).toBe(0);
119
+ });
120
+ });
121
+
122
+ // ==========================================================================
123
+ // findLeastLoadedAgent
124
+ // ==========================================================================
125
+
126
+ describe('findLeastLoadedAgent', () => {
127
+ it('finds agent with highest capacity', async () => {
128
+ const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
129
+ const a2 = await registerAgent({ agentType: 'executor' }, tempDir);
130
+ const a3 = await registerAgent({ agentType: 'executor' }, tempDir);
131
+ await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
132
+ await updateAgentStatus(a2.id, { status: 'active' }, tempDir);
133
+ await updateAgentStatus(a3.id, { status: 'active' }, tempDir);
134
+ await updateCapacity(a1.id, 0.2, tempDir);
135
+ await updateCapacity(a2.id, 0.8, tempDir);
136
+ await updateCapacity(a3.id, 0.5, tempDir);
137
+
138
+ const least = await findLeastLoadedAgent(undefined, tempDir);
139
+ expect(least).not.toBeNull();
140
+ expect(least!.id).toBe(a2.id);
141
+ });
142
+
143
+ it('filters by agent type', async () => {
144
+ const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
145
+ const a2 = await registerAgent({ agentType: 'researcher' }, tempDir);
146
+ await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
147
+ await updateAgentStatus(a2.id, { status: 'active' }, tempDir);
148
+ await updateCapacity(a1.id, 0.3, tempDir);
149
+ await updateCapacity(a2.id, 0.9, tempDir);
150
+
151
+ const least = await findLeastLoadedAgent('executor', tempDir);
152
+ expect(least).not.toBeNull();
153
+ expect(least!.id).toBe(a1.id);
154
+ expect(least!.agentType).toBe('executor');
155
+ });
156
+
157
+ it('returns null when no matching agents', async () => {
158
+ const result = await findLeastLoadedAgent('orchestrator', tempDir);
159
+ expect(result).toBeNull();
160
+ });
161
+ });
162
+
163
+ // ==========================================================================
164
+ // isOverloaded
165
+ // ==========================================================================
166
+
167
+ describe('isOverloaded', () => {
168
+ it('returns true when capacity below threshold', async () => {
169
+ const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
170
+ await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
171
+ await updateCapacity(a1.id, 0.05, tempDir);
172
+
173
+ expect(await isOverloaded(0.1, tempDir)).toBe(true);
174
+ });
175
+
176
+ it('returns false when capacity above threshold', async () => {
177
+ const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
178
+ await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
179
+ await updateCapacity(a1.id, 0.5, tempDir);
180
+
181
+ expect(await isOverloaded(0.1, tempDir)).toBe(false);
182
+ });
183
+
184
+ it('returns true when no active agents (0 capacity)', async () => {
185
+ expect(await isOverloaded(0.1, tempDir)).toBe(true);
186
+ });
187
+ });
188
+
189
+ // ==========================================================================
190
+ // getCapacitySummary
191
+ // ==========================================================================
192
+
193
+ describe('getCapacitySummary', () => {
194
+ it('produces correct summary', async () => {
195
+ const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
196
+ const a2 = await registerAgent({ agentType: 'researcher' }, tempDir);
197
+ await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
198
+ await updateAgentStatus(a2.id, { status: 'idle' }, tempDir);
199
+ await updateCapacity(a1.id, 0.6, tempDir);
200
+ await updateCapacity(a2.id, 0.4, tempDir);
201
+
202
+ const summary = await getCapacitySummary(0.1, tempDir);
203
+
204
+ expect(summary.totalCapacity).toBeCloseTo(1.0, 1);
205
+ expect(summary.activeAgentCount).toBe(2);
206
+ expect(summary.averageCapacity).toBeCloseTo(0.5, 1);
207
+ expect(summary.overloaded).toBe(false);
208
+ expect(summary.threshold).toBe(0.1);
209
+ });
210
+
211
+ it('reports overloaded when below threshold', async () => {
212
+ const summary = await getCapacitySummary(0.5, tempDir);
213
+
214
+ expect(summary.totalCapacity).toBe(0);
215
+ expect(summary.activeAgentCount).toBe(0);
216
+ expect(summary.overloaded).toBe(true);
217
+ });
218
+ });
219
+ });