@soleri/core 2.0.2 → 2.4.0

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 (226) hide show
  1. package/dist/brain/brain.d.ts +14 -50
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +207 -16
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +86 -0
  6. package/dist/brain/intelligence.d.ts.map +1 -0
  7. package/dist/brain/intelligence.js +771 -0
  8. package/dist/brain/intelligence.js.map +1 -0
  9. package/dist/brain/types.d.ts +197 -0
  10. package/dist/brain/types.d.ts.map +1 -0
  11. package/dist/brain/types.js +2 -0
  12. package/dist/brain/types.js.map +1 -0
  13. package/dist/cognee/client.d.ts +35 -0
  14. package/dist/cognee/client.d.ts.map +1 -0
  15. package/dist/cognee/client.js +291 -0
  16. package/dist/cognee/client.js.map +1 -0
  17. package/dist/cognee/types.d.ts +46 -0
  18. package/dist/cognee/types.d.ts.map +1 -0
  19. package/dist/cognee/types.js +3 -0
  20. package/dist/cognee/types.js.map +1 -0
  21. package/dist/control/identity-manager.d.ts +22 -0
  22. package/dist/control/identity-manager.d.ts.map +1 -0
  23. package/dist/control/identity-manager.js +233 -0
  24. package/dist/control/identity-manager.js.map +1 -0
  25. package/dist/control/intent-router.d.ts +32 -0
  26. package/dist/control/intent-router.d.ts.map +1 -0
  27. package/dist/control/intent-router.js +242 -0
  28. package/dist/control/intent-router.js.map +1 -0
  29. package/dist/control/types.d.ts +68 -0
  30. package/dist/control/types.d.ts.map +1 -0
  31. package/dist/control/types.js +9 -0
  32. package/dist/control/types.js.map +1 -0
  33. package/dist/curator/curator.d.ts +29 -0
  34. package/dist/curator/curator.d.ts.map +1 -1
  35. package/dist/curator/curator.js +142 -5
  36. package/dist/curator/curator.js.map +1 -1
  37. package/dist/facades/types.d.ts +1 -1
  38. package/dist/governance/governance.d.ts +42 -0
  39. package/dist/governance/governance.d.ts.map +1 -0
  40. package/dist/governance/governance.js +488 -0
  41. package/dist/governance/governance.js.map +1 -0
  42. package/dist/governance/index.d.ts +3 -0
  43. package/dist/governance/index.d.ts.map +1 -0
  44. package/dist/governance/index.js +2 -0
  45. package/dist/governance/index.js.map +1 -0
  46. package/dist/governance/types.d.ts +102 -0
  47. package/dist/governance/types.d.ts.map +1 -0
  48. package/dist/governance/types.js +3 -0
  49. package/dist/governance/types.js.map +1 -0
  50. package/dist/index.d.ts +35 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +32 -1
  53. package/dist/index.js.map +1 -1
  54. package/dist/llm/llm-client.d.ts.map +1 -1
  55. package/dist/llm/llm-client.js +9 -2
  56. package/dist/llm/llm-client.js.map +1 -1
  57. package/dist/logging/logger.d.ts +37 -0
  58. package/dist/logging/logger.d.ts.map +1 -0
  59. package/dist/logging/logger.js +145 -0
  60. package/dist/logging/logger.js.map +1 -0
  61. package/dist/logging/types.d.ts +19 -0
  62. package/dist/logging/types.d.ts.map +1 -0
  63. package/dist/logging/types.js +2 -0
  64. package/dist/logging/types.js.map +1 -0
  65. package/dist/loop/loop-manager.d.ts +49 -0
  66. package/dist/loop/loop-manager.d.ts.map +1 -0
  67. package/dist/loop/loop-manager.js +105 -0
  68. package/dist/loop/loop-manager.js.map +1 -0
  69. package/dist/loop/types.d.ts +35 -0
  70. package/dist/loop/types.d.ts.map +1 -0
  71. package/dist/loop/types.js +8 -0
  72. package/dist/loop/types.js.map +1 -0
  73. package/dist/planning/gap-analysis.d.ts +29 -0
  74. package/dist/planning/gap-analysis.d.ts.map +1 -0
  75. package/dist/planning/gap-analysis.js +265 -0
  76. package/dist/planning/gap-analysis.js.map +1 -0
  77. package/dist/planning/gap-types.d.ts +29 -0
  78. package/dist/planning/gap-types.d.ts.map +1 -0
  79. package/dist/planning/gap-types.js +28 -0
  80. package/dist/planning/gap-types.js.map +1 -0
  81. package/dist/planning/planner.d.ts +150 -1
  82. package/dist/planning/planner.d.ts.map +1 -1
  83. package/dist/planning/planner.js +365 -2
  84. package/dist/planning/planner.js.map +1 -1
  85. package/dist/project/project-registry.d.ts +79 -0
  86. package/dist/project/project-registry.d.ts.map +1 -0
  87. package/dist/project/project-registry.js +276 -0
  88. package/dist/project/project-registry.js.map +1 -0
  89. package/dist/project/types.d.ts +28 -0
  90. package/dist/project/types.d.ts.map +1 -0
  91. package/dist/project/types.js +5 -0
  92. package/dist/project/types.js.map +1 -0
  93. package/dist/runtime/admin-extra-ops.d.ts +13 -0
  94. package/dist/runtime/admin-extra-ops.d.ts.map +1 -0
  95. package/dist/runtime/admin-extra-ops.js +284 -0
  96. package/dist/runtime/admin-extra-ops.js.map +1 -0
  97. package/dist/runtime/admin-ops.d.ts +15 -0
  98. package/dist/runtime/admin-ops.d.ts.map +1 -0
  99. package/dist/runtime/admin-ops.js +322 -0
  100. package/dist/runtime/admin-ops.js.map +1 -0
  101. package/dist/runtime/capture-ops.d.ts +15 -0
  102. package/dist/runtime/capture-ops.d.ts.map +1 -0
  103. package/dist/runtime/capture-ops.js +345 -0
  104. package/dist/runtime/capture-ops.js.map +1 -0
  105. package/dist/runtime/core-ops.d.ts +7 -3
  106. package/dist/runtime/core-ops.d.ts.map +1 -1
  107. package/dist/runtime/core-ops.js +646 -15
  108. package/dist/runtime/core-ops.js.map +1 -1
  109. package/dist/runtime/curator-extra-ops.d.ts +9 -0
  110. package/dist/runtime/curator-extra-ops.d.ts.map +1 -0
  111. package/dist/runtime/curator-extra-ops.js +59 -0
  112. package/dist/runtime/curator-extra-ops.js.map +1 -0
  113. package/dist/runtime/domain-ops.d.ts.map +1 -1
  114. package/dist/runtime/domain-ops.js +59 -13
  115. package/dist/runtime/domain-ops.js.map +1 -1
  116. package/dist/runtime/grading-ops.d.ts +14 -0
  117. package/dist/runtime/grading-ops.d.ts.map +1 -0
  118. package/dist/runtime/grading-ops.js +105 -0
  119. package/dist/runtime/grading-ops.js.map +1 -0
  120. package/dist/runtime/loop-ops.d.ts +13 -0
  121. package/dist/runtime/loop-ops.d.ts.map +1 -0
  122. package/dist/runtime/loop-ops.js +179 -0
  123. package/dist/runtime/loop-ops.js.map +1 -0
  124. package/dist/runtime/memory-cross-project-ops.d.ts +12 -0
  125. package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -0
  126. package/dist/runtime/memory-cross-project-ops.js +165 -0
  127. package/dist/runtime/memory-cross-project-ops.js.map +1 -0
  128. package/dist/runtime/memory-extra-ops.d.ts +13 -0
  129. package/dist/runtime/memory-extra-ops.d.ts.map +1 -0
  130. package/dist/runtime/memory-extra-ops.js +173 -0
  131. package/dist/runtime/memory-extra-ops.js.map +1 -0
  132. package/dist/runtime/orchestrate-ops.d.ts +17 -0
  133. package/dist/runtime/orchestrate-ops.d.ts.map +1 -0
  134. package/dist/runtime/orchestrate-ops.js +240 -0
  135. package/dist/runtime/orchestrate-ops.js.map +1 -0
  136. package/dist/runtime/planning-extra-ops.d.ts +17 -0
  137. package/dist/runtime/planning-extra-ops.d.ts.map +1 -0
  138. package/dist/runtime/planning-extra-ops.js +300 -0
  139. package/dist/runtime/planning-extra-ops.js.map +1 -0
  140. package/dist/runtime/project-ops.d.ts +15 -0
  141. package/dist/runtime/project-ops.d.ts.map +1 -0
  142. package/dist/runtime/project-ops.js +181 -0
  143. package/dist/runtime/project-ops.js.map +1 -0
  144. package/dist/runtime/runtime.d.ts.map +1 -1
  145. package/dist/runtime/runtime.js +48 -1
  146. package/dist/runtime/runtime.js.map +1 -1
  147. package/dist/runtime/types.d.ts +23 -0
  148. package/dist/runtime/types.d.ts.map +1 -1
  149. package/dist/runtime/vault-extra-ops.d.ts +9 -0
  150. package/dist/runtime/vault-extra-ops.d.ts.map +1 -0
  151. package/dist/runtime/vault-extra-ops.js +195 -0
  152. package/dist/runtime/vault-extra-ops.js.map +1 -0
  153. package/dist/telemetry/telemetry.d.ts +48 -0
  154. package/dist/telemetry/telemetry.d.ts.map +1 -0
  155. package/dist/telemetry/telemetry.js +87 -0
  156. package/dist/telemetry/telemetry.js.map +1 -0
  157. package/dist/vault/vault.d.ts +94 -0
  158. package/dist/vault/vault.d.ts.map +1 -1
  159. package/dist/vault/vault.js +340 -1
  160. package/dist/vault/vault.js.map +1 -1
  161. package/package.json +1 -1
  162. package/src/__tests__/admin-extra-ops.test.ts +420 -0
  163. package/src/__tests__/admin-ops.test.ts +271 -0
  164. package/src/__tests__/brain-intelligence.test.ts +828 -0
  165. package/src/__tests__/brain.test.ts +396 -27
  166. package/src/__tests__/capture-ops.test.ts +509 -0
  167. package/src/__tests__/cognee-client.test.ts +524 -0
  168. package/src/__tests__/core-ops.test.ts +341 -49
  169. package/src/__tests__/curator-extra-ops.test.ts +359 -0
  170. package/src/__tests__/curator.test.ts +126 -31
  171. package/src/__tests__/domain-ops.test.ts +111 -9
  172. package/src/__tests__/governance.test.ts +522 -0
  173. package/src/__tests__/grading-ops.test.ts +340 -0
  174. package/src/__tests__/identity-manager.test.ts +243 -0
  175. package/src/__tests__/intent-router.test.ts +222 -0
  176. package/src/__tests__/logger.test.ts +200 -0
  177. package/src/__tests__/loop-ops.test.ts +398 -0
  178. package/src/__tests__/memory-cross-project-ops.test.ts +246 -0
  179. package/src/__tests__/memory-extra-ops.test.ts +352 -0
  180. package/src/__tests__/orchestrate-ops.test.ts +284 -0
  181. package/src/__tests__/planner.test.ts +331 -0
  182. package/src/__tests__/planning-extra-ops.test.ts +548 -0
  183. package/src/__tests__/project-ops.test.ts +367 -0
  184. package/src/__tests__/runtime.test.ts +13 -11
  185. package/src/__tests__/vault-extra-ops.test.ts +407 -0
  186. package/src/brain/brain.ts +308 -72
  187. package/src/brain/intelligence.ts +1230 -0
  188. package/src/brain/types.ts +214 -0
  189. package/src/cognee/client.ts +352 -0
  190. package/src/cognee/types.ts +62 -0
  191. package/src/control/identity-manager.ts +354 -0
  192. package/src/control/intent-router.ts +326 -0
  193. package/src/control/types.ts +102 -0
  194. package/src/curator/curator.ts +265 -15
  195. package/src/governance/governance.ts +698 -0
  196. package/src/governance/index.ts +18 -0
  197. package/src/governance/types.ts +111 -0
  198. package/src/index.ts +128 -3
  199. package/src/llm/llm-client.ts +18 -24
  200. package/src/logging/logger.ts +154 -0
  201. package/src/logging/types.ts +21 -0
  202. package/src/loop/loop-manager.ts +130 -0
  203. package/src/loop/types.ts +44 -0
  204. package/src/planning/gap-analysis.ts +506 -0
  205. package/src/planning/gap-types.ts +58 -0
  206. package/src/planning/planner.ts +478 -2
  207. package/src/project/project-registry.ts +358 -0
  208. package/src/project/types.ts +31 -0
  209. package/src/runtime/admin-extra-ops.ts +307 -0
  210. package/src/runtime/admin-ops.ts +329 -0
  211. package/src/runtime/capture-ops.ts +385 -0
  212. package/src/runtime/core-ops.ts +747 -26
  213. package/src/runtime/curator-extra-ops.ts +71 -0
  214. package/src/runtime/domain-ops.ts +65 -13
  215. package/src/runtime/grading-ops.ts +121 -0
  216. package/src/runtime/loop-ops.ts +194 -0
  217. package/src/runtime/memory-cross-project-ops.ts +192 -0
  218. package/src/runtime/memory-extra-ops.ts +186 -0
  219. package/src/runtime/orchestrate-ops.ts +272 -0
  220. package/src/runtime/planning-extra-ops.ts +327 -0
  221. package/src/runtime/project-ops.ts +196 -0
  222. package/src/runtime/runtime.ts +54 -1
  223. package/src/runtime/types.ts +23 -0
  224. package/src/runtime/vault-extra-ops.ts +225 -0
  225. package/src/telemetry/telemetry.ts +118 -0
  226. package/src/vault/vault.ts +412 -1
@@ -0,0 +1,352 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdirSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { createAgentRuntime } from '../runtime/runtime.js';
6
+ import { createMemoryExtraOps } from '../runtime/memory-extra-ops.js';
7
+ import type { AgentRuntime } from '../runtime/types.js';
8
+ import type { OpDefinition } from '../facades/types.js';
9
+
10
+ describe('createMemoryExtraOps', () => {
11
+ let runtime: AgentRuntime;
12
+ let ops: OpDefinition[];
13
+ let plannerDir: string;
14
+
15
+ beforeEach(() => {
16
+ plannerDir = join(tmpdir(), 'memory-extra-ops-test-' + Date.now());
17
+ mkdirSync(plannerDir, { recursive: true });
18
+ runtime = createAgentRuntime({
19
+ agentId: 'test-memory-extra',
20
+ vaultPath: ':memory:',
21
+ plansPath: join(plannerDir, 'plans.json'),
22
+ });
23
+ ops = createMemoryExtraOps(runtime);
24
+ });
25
+
26
+ afterEach(() => {
27
+ runtime.close();
28
+ rmSync(plannerDir, { recursive: true, force: true });
29
+ });
30
+
31
+ function findOp(name: string): OpDefinition {
32
+ const op = ops.find((o) => o.name === name);
33
+ if (!op) throw new Error(`Op "${name}" not found`);
34
+ return op;
35
+ }
36
+
37
+ /** Helper to create a test memory and return its ID */
38
+ function captureTestMemory(overrides?: {
39
+ projectPath?: string;
40
+ type?: 'session' | 'lesson' | 'preference';
41
+ summary?: string;
42
+ topics?: string[];
43
+ }) {
44
+ return runtime.vault.captureMemory({
45
+ projectPath: overrides?.projectPath ?? '/test/project',
46
+ type: overrides?.type ?? 'lesson',
47
+ context: 'Test context',
48
+ summary: overrides?.summary ?? 'Test memory summary',
49
+ topics: overrides?.topics ?? ['testing'],
50
+ filesModified: ['file.ts'],
51
+ toolsUsed: ['tool1'],
52
+ });
53
+ }
54
+
55
+ it('should return 8 ops', () => {
56
+ expect(ops.length).toBe(8);
57
+ });
58
+
59
+ it('should have all expected op names', () => {
60
+ const names = ops.map((o) => o.name);
61
+ expect(names).toContain('memory_delete');
62
+ expect(names).toContain('memory_stats');
63
+ expect(names).toContain('memory_export');
64
+ expect(names).toContain('memory_import');
65
+ expect(names).toContain('memory_prune');
66
+ expect(names).toContain('memory_deduplicate');
67
+ expect(names).toContain('memory_topics');
68
+ expect(names).toContain('memory_by_project');
69
+ });
70
+
71
+ // ─── memory_delete ──────────────────────────────────────────────
72
+
73
+ it('memory_delete should delete an existing memory', async () => {
74
+ const mem = captureTestMemory();
75
+ const result = (await findOp('memory_delete').handler({ memoryId: mem.id })) as {
76
+ deleted: boolean;
77
+ memoryId: string;
78
+ };
79
+ expect(result.deleted).toBe(true);
80
+ expect(result.memoryId).toBe(mem.id);
81
+ expect(runtime.vault.getMemory(mem.id)).toBeNull();
82
+ });
83
+
84
+ it('memory_delete should return error for non-existent memory', async () => {
85
+ const result = (await findOp('memory_delete').handler({ memoryId: 'non-existent' })) as {
86
+ deleted: boolean;
87
+ error: string;
88
+ };
89
+ expect(result.deleted).toBe(false);
90
+ expect(result.error).toContain('not found');
91
+ });
92
+
93
+ // ─── memory_stats ──────────────────────────────────────────────
94
+
95
+ it('memory_stats should return detailed statistics', async () => {
96
+ captureTestMemory({ type: 'lesson', projectPath: '/proj-a' });
97
+ captureTestMemory({ type: 'session', projectPath: '/proj-a' });
98
+ captureTestMemory({ type: 'lesson', projectPath: '/proj-b' });
99
+
100
+ const result = (await findOp('memory_stats').handler({})) as {
101
+ total: number;
102
+ byType: Record<string, number>;
103
+ byProject: Record<string, number>;
104
+ oldest: number | null;
105
+ newest: number | null;
106
+ archivedCount: number;
107
+ };
108
+ expect(result.total).toBe(3);
109
+ expect(result.byType['lesson']).toBe(2);
110
+ expect(result.byType['session']).toBe(1);
111
+ expect(result.byProject['/proj-a']).toBe(2);
112
+ expect(result.byProject['/proj-b']).toBe(1);
113
+ expect(result.oldest).toBeTypeOf('number');
114
+ expect(result.newest).toBeTypeOf('number');
115
+ expect(result.archivedCount).toBe(0);
116
+ });
117
+
118
+ it('memory_stats should filter by projectPath', async () => {
119
+ captureTestMemory({ projectPath: '/proj-a' });
120
+ captureTestMemory({ projectPath: '/proj-b' });
121
+
122
+ const result = (await findOp('memory_stats').handler({ projectPath: '/proj-a' })) as {
123
+ total: number;
124
+ };
125
+ expect(result.total).toBe(1);
126
+ });
127
+
128
+ // ─── memory_export ──────────────────────────────────────────────
129
+
130
+ it('memory_export should export all memories', async () => {
131
+ captureTestMemory({ summary: 'Export test 1' });
132
+ captureTestMemory({ summary: 'Export test 2' });
133
+
134
+ const result = (await findOp('memory_export').handler({})) as {
135
+ exported: boolean;
136
+ count: number;
137
+ memories: unknown[];
138
+ };
139
+ expect(result.exported).toBe(true);
140
+ expect(result.count).toBe(2);
141
+ expect(result.memories.length).toBe(2);
142
+ });
143
+
144
+ it('memory_export should filter by project', async () => {
145
+ captureTestMemory({ projectPath: '/proj-a', summary: 'A' });
146
+ captureTestMemory({ projectPath: '/proj-b', summary: 'B' });
147
+
148
+ const result = (await findOp('memory_export').handler({ projectPath: '/proj-a' })) as {
149
+ count: number;
150
+ };
151
+ expect(result.count).toBe(1);
152
+ });
153
+
154
+ it('memory_export should filter by type', async () => {
155
+ captureTestMemory({ type: 'lesson', summary: 'Lesson' });
156
+ captureTestMemory({ type: 'session', summary: 'Session' });
157
+
158
+ const result = (await findOp('memory_export').handler({ type: 'lesson' })) as {
159
+ count: number;
160
+ };
161
+ expect(result.count).toBe(1);
162
+ });
163
+
164
+ // ─── memory_import ──────────────────────────────────────────────
165
+
166
+ it('memory_import should import new memories', async () => {
167
+ const result = (await findOp('memory_import').handler({
168
+ memories: [
169
+ {
170
+ id: 'import-1',
171
+ projectPath: '/imported',
172
+ type: 'lesson',
173
+ context: 'Imported context',
174
+ summary: 'Imported memory',
175
+ topics: ['imported'],
176
+ filesModified: [],
177
+ toolsUsed: [],
178
+ createdAt: Math.floor(Date.now() / 1000),
179
+ archivedAt: null,
180
+ },
181
+ ],
182
+ })) as { imported: number; skipped: number; total: number };
183
+ expect(result.imported).toBe(1);
184
+ expect(result.skipped).toBe(0);
185
+ expect(result.total).toBe(1);
186
+
187
+ const mem = runtime.vault.getMemory('import-1');
188
+ expect(mem).not.toBeNull();
189
+ expect(mem!.summary).toBe('Imported memory');
190
+ });
191
+
192
+ it('memory_import should skip duplicates', async () => {
193
+ const mem = captureTestMemory();
194
+
195
+ const result = (await findOp('memory_import').handler({
196
+ memories: [
197
+ {
198
+ id: mem.id,
199
+ projectPath: mem.projectPath,
200
+ type: mem.type,
201
+ context: mem.context,
202
+ summary: mem.summary,
203
+ topics: mem.topics,
204
+ filesModified: mem.filesModified,
205
+ toolsUsed: mem.toolsUsed,
206
+ createdAt: mem.createdAt,
207
+ archivedAt: null,
208
+ },
209
+ ],
210
+ })) as { imported: number; skipped: number };
211
+ expect(result.imported).toBe(0);
212
+ expect(result.skipped).toBe(1);
213
+ });
214
+
215
+ // ─── memory_prune ──────────────────────────────────────────────
216
+
217
+ it('memory_prune should delete old memories', async () => {
218
+ // Insert a memory with an old timestamp directly via db
219
+ const db = runtime.vault.getDb();
220
+ const oldTimestamp = Math.floor(Date.now() / 1000) - 100 * 86400; // 100 days ago
221
+ db.prepare(
222
+ `INSERT INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used, created_at)
223
+ VALUES ('old-mem', '/test', 'lesson', 'old', 'Old memory', '[]', '[]', '[]', ?)`,
224
+ ).run(oldTimestamp);
225
+
226
+ // Also capture a fresh memory
227
+ captureTestMemory({ summary: 'Fresh memory' });
228
+
229
+ const result = (await findOp('memory_prune').handler({ olderThanDays: 30 })) as {
230
+ pruned: number;
231
+ olderThanDays: number;
232
+ };
233
+ expect(result.pruned).toBe(1);
234
+ expect(result.olderThanDays).toBe(30);
235
+
236
+ // Fresh memory should remain
237
+ const remaining = runtime.vault.listMemories({});
238
+ expect(remaining.length).toBe(1);
239
+ expect(remaining[0].summary).toBe('Fresh memory');
240
+ });
241
+
242
+ it('memory_prune should not prune recent memories', async () => {
243
+ captureTestMemory({ summary: 'Recent' });
244
+
245
+ const result = (await findOp('memory_prune').handler({ olderThanDays: 1 })) as {
246
+ pruned: number;
247
+ };
248
+ expect(result.pruned).toBe(0);
249
+ });
250
+
251
+ // ─── memory_deduplicate ─────────────────────────────────────────
252
+
253
+ it('memory_deduplicate should remove duplicates', async () => {
254
+ captureTestMemory({ summary: 'Duplicate summary', projectPath: '/proj', type: 'lesson' });
255
+ captureTestMemory({ summary: 'Duplicate summary', projectPath: '/proj', type: 'lesson' });
256
+ captureTestMemory({ summary: 'Unique summary', projectPath: '/proj', type: 'lesson' });
257
+
258
+ const result = (await findOp('memory_deduplicate').handler({})) as {
259
+ removed: number;
260
+ groups: Array<{ kept: string; removed: string[] }>;
261
+ };
262
+ expect(result.removed).toBe(1);
263
+ expect(result.groups.length).toBe(1);
264
+ expect(result.groups[0].removed.length).toBe(1);
265
+
266
+ const remaining = runtime.vault.listMemories({});
267
+ expect(remaining.length).toBe(2);
268
+ });
269
+
270
+ it('memory_deduplicate should return 0 when no duplicates', async () => {
271
+ captureTestMemory({ summary: 'Unique 1' });
272
+ captureTestMemory({ summary: 'Unique 2' });
273
+
274
+ const result = (await findOp('memory_deduplicate').handler({})) as { removed: number };
275
+ expect(result.removed).toBe(0);
276
+ });
277
+
278
+ // ─── memory_topics ──────────────────────────────────────────────
279
+
280
+ it('memory_topics should list unique topics with counts', async () => {
281
+ captureTestMemory({ topics: ['react', 'testing'] });
282
+ captureTestMemory({ topics: ['react', 'hooks'] });
283
+ captureTestMemory({ topics: ['testing'] });
284
+
285
+ const result = (await findOp('memory_topics').handler({})) as {
286
+ count: number;
287
+ topics: Array<{ topic: string; count: number }>;
288
+ };
289
+ expect(result.count).toBe(3); // react, testing, hooks
290
+ // Sorted by frequency descending
291
+ expect(result.topics[0].topic).toBe('react');
292
+ expect(result.topics[0].count).toBe(2);
293
+ expect(result.topics[1].topic).toBe('testing');
294
+ expect(result.topics[1].count).toBe(2);
295
+ expect(result.topics[2].topic).toBe('hooks');
296
+ expect(result.topics[2].count).toBe(1);
297
+ });
298
+
299
+ it('memory_topics should return empty when no memories', async () => {
300
+ const result = (await findOp('memory_topics').handler({})) as { count: number };
301
+ expect(result.count).toBe(0);
302
+ });
303
+
304
+ // ─── memory_by_project ──────────────────────────────────────────
305
+
306
+ it('memory_by_project should group memories by project', async () => {
307
+ captureTestMemory({ projectPath: '/proj-a', summary: 'A1' });
308
+ captureTestMemory({ projectPath: '/proj-a', summary: 'A2' });
309
+ captureTestMemory({ projectPath: '/proj-b', summary: 'B1' });
310
+
311
+ const result = (await findOp('memory_by_project').handler({})) as {
312
+ count: number;
313
+ projects: Array<{
314
+ project: string;
315
+ count: number;
316
+ memories: Array<{ summary: string }>;
317
+ }>;
318
+ };
319
+ expect(result.count).toBe(2);
320
+ const projA = result.projects.find((p) => p.project === '/proj-a');
321
+ const projB = result.projects.find((p) => p.project === '/proj-b');
322
+ expect(projA).toBeDefined();
323
+ expect(projA!.count).toBe(2);
324
+ expect(projA!.memories.length).toBe(2);
325
+ expect(projB).toBeDefined();
326
+ expect(projB!.count).toBe(1);
327
+ });
328
+
329
+ it('memory_by_project should return counts only when includeMemories=false', async () => {
330
+ captureTestMemory({ projectPath: '/proj-a', summary: 'A1' });
331
+
332
+ const result = (await findOp('memory_by_project').handler({ includeMemories: false })) as {
333
+ count: number;
334
+ projects: Array<{ project: string; count: number; memories?: unknown }>;
335
+ };
336
+ expect(result.count).toBe(1);
337
+ expect(result.projects[0].memories).toBeUndefined();
338
+ });
339
+
340
+ // ─── Auth levels ────────────────────────────────────────────────
341
+
342
+ it('should have correct auth levels', () => {
343
+ expect(findOp('memory_delete').auth).toBe('write');
344
+ expect(findOp('memory_stats').auth).toBe('read');
345
+ expect(findOp('memory_export').auth).toBe('read');
346
+ expect(findOp('memory_import').auth).toBe('write');
347
+ expect(findOp('memory_prune').auth).toBe('admin');
348
+ expect(findOp('memory_deduplicate').auth).toBe('admin');
349
+ expect(findOp('memory_topics').auth).toBe('read');
350
+ expect(findOp('memory_by_project').auth).toBe('read');
351
+ });
352
+ });
@@ -0,0 +1,284 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdirSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { createAgentRuntime } from '../runtime/runtime.js';
6
+ import { createOrchestrateOps } from '../runtime/orchestrate-ops.js';
7
+ import type { AgentRuntime } from '../runtime/types.js';
8
+ import type { OpDefinition } from '../facades/types.js';
9
+
10
+ describe('createOrchestrateOps', () => {
11
+ let runtime: AgentRuntime;
12
+ let ops: OpDefinition[];
13
+ let plannerDir: string;
14
+
15
+ beforeEach(() => {
16
+ plannerDir = join(tmpdir(), 'orchestrate-ops-test-' + Date.now());
17
+ mkdirSync(plannerDir, { recursive: true });
18
+ runtime = createAgentRuntime({
19
+ agentId: 'test-orchestrate',
20
+ vaultPath: ':memory:',
21
+ plansPath: join(plannerDir, 'plans.json'),
22
+ });
23
+ ops = createOrchestrateOps(runtime);
24
+ });
25
+
26
+ afterEach(() => {
27
+ runtime.close();
28
+ rmSync(plannerDir, { recursive: true, force: true });
29
+ });
30
+
31
+ function findOp(name: string): OpDefinition {
32
+ const op = ops.find((o) => o.name === name);
33
+ if (!op) throw new Error(`Op "${name}" not found`);
34
+ return op;
35
+ }
36
+
37
+ it('should return 5 ops', () => {
38
+ expect(ops.length).toBe(5);
39
+ });
40
+
41
+ it('should have all expected op names', () => {
42
+ const names = ops.map((o) => o.name);
43
+ expect(names).toContain('orchestrate_plan');
44
+ expect(names).toContain('orchestrate_execute');
45
+ expect(names).toContain('orchestrate_complete');
46
+ expect(names).toContain('orchestrate_status');
47
+ expect(names).toContain('orchestrate_quick_capture');
48
+ });
49
+
50
+ it('should assign correct auth levels', () => {
51
+ expect(findOp('orchestrate_plan').auth).toBe('write');
52
+ expect(findOp('orchestrate_execute').auth).toBe('write');
53
+ expect(findOp('orchestrate_complete').auth).toBe('write');
54
+ expect(findOp('orchestrate_status').auth).toBe('read');
55
+ expect(findOp('orchestrate_quick_capture').auth).toBe('write');
56
+ });
57
+
58
+ // ─── orchestrate_plan ───────────────────────────────────────────
59
+
60
+ describe('orchestrate_plan', () => {
61
+ it('should create a plan with empty recommendations when brain has no data', async () => {
62
+ const op = findOp('orchestrate_plan');
63
+ const result = (await op.handler({
64
+ objective: 'Build a new button component',
65
+ scope: 'src/components/Button',
66
+ domain: 'component',
67
+ })) as { plan: { id: string; objective: string; decisions: string[] }; recommendations: unknown[] };
68
+
69
+ expect(result.plan).toBeDefined();
70
+ expect(result.plan.objective).toBe('Build a new button component');
71
+ expect(result.recommendations).toEqual([]);
72
+ expect(result.plan.decisions).toEqual([]);
73
+ });
74
+
75
+ it('should create a plan with tasks when provided', async () => {
76
+ const op = findOp('orchestrate_plan');
77
+ const result = (await op.handler({
78
+ objective: 'Refactor auth module',
79
+ scope: 'src/auth',
80
+ tasks: [
81
+ { title: 'Extract interfaces', description: 'Pull out shared types' },
82
+ { title: 'Add tests', description: 'Cover edge cases' },
83
+ ],
84
+ })) as { plan: { tasks: Array<{ title: string }> } };
85
+
86
+ expect(result.plan.tasks).toHaveLength(2);
87
+ expect(result.plan.tasks[0].title).toBe('Extract interfaces');
88
+ expect(result.plan.tasks[1].title).toBe('Add tests');
89
+ });
90
+
91
+ it('should work without optional domain parameter', async () => {
92
+ const op = findOp('orchestrate_plan');
93
+ const result = (await op.handler({
94
+ objective: 'Quick fix',
95
+ scope: 'all',
96
+ })) as { plan: { id: string } };
97
+
98
+ expect(result.plan.id).toBeDefined();
99
+ });
100
+ });
101
+
102
+ // ─── orchestrate_execute ────────────────────────────────────────
103
+
104
+ describe('orchestrate_execute', () => {
105
+ it('should start plan execution and open a brain session', async () => {
106
+ // Create and approve a plan first
107
+ const plan = runtime.planner.create({
108
+ objective: 'Test execution',
109
+ scope: 'test',
110
+ });
111
+ runtime.planner.approve(plan.id);
112
+
113
+ const op = findOp('orchestrate_execute');
114
+ const result = (await op.handler({
115
+ planId: plan.id,
116
+ domain: 'testing',
117
+ context: 'Running orchestration test',
118
+ })) as { plan: { status: string }; session: { id: string; domain: string | null } };
119
+
120
+ expect(result.plan.status).toBe('executing');
121
+ expect(result.session).toBeDefined();
122
+ expect(result.session.id).toBeDefined();
123
+ expect(result.session.domain).toBe('testing');
124
+ });
125
+
126
+ it('should throw when plan is not approved', async () => {
127
+ const plan = runtime.planner.create({
128
+ objective: 'Not approved',
129
+ scope: 'test',
130
+ });
131
+
132
+ const op = findOp('orchestrate_execute');
133
+ await expect(op.handler({ planId: plan.id })).rejects.toThrow(/must be 'approved'/);
134
+ });
135
+ });
136
+
137
+ // ─── orchestrate_complete ───────────────────────────────────────
138
+
139
+ describe('orchestrate_complete', () => {
140
+ it('should complete plan, end session, and extract knowledge', async () => {
141
+ // Full lifecycle: create -> approve -> execute -> complete
142
+ const plan = runtime.planner.create({
143
+ objective: 'Full lifecycle test',
144
+ scope: 'test',
145
+ });
146
+ runtime.planner.approve(plan.id);
147
+ runtime.planner.startExecution(plan.id);
148
+
149
+ // Start a brain session
150
+ const session = runtime.brainIntelligence.lifecycle({
151
+ action: 'start',
152
+ domain: 'testing',
153
+ planId: plan.id,
154
+ });
155
+
156
+ const op = findOp('orchestrate_complete');
157
+ const result = (await op.handler({
158
+ planId: plan.id,
159
+ sessionId: session.id,
160
+ outcome: 'completed',
161
+ toolsUsed: ['tool1', 'tool2', 'tool1', 'tool1'],
162
+ filesModified: ['a.ts', 'b.ts'],
163
+ })) as {
164
+ plan: { status: string };
165
+ session: { endedAt: string | null; planOutcome: string | null };
166
+ extraction: unknown;
167
+ };
168
+
169
+ expect(result.plan.status).toBe('completed');
170
+ expect(result.session.endedAt).toBeDefined();
171
+ expect(result.session.planOutcome).toBe('completed');
172
+ // extraction may or may not produce proposals depending on heuristics
173
+ });
174
+
175
+ it('should handle abandoned outcome', async () => {
176
+ const plan = runtime.planner.create({ objective: 'Abandoned test', scope: 'test' });
177
+ runtime.planner.approve(plan.id);
178
+ runtime.planner.startExecution(plan.id);
179
+
180
+ const session = runtime.brainIntelligence.lifecycle({
181
+ action: 'start',
182
+ domain: 'testing',
183
+ });
184
+
185
+ const op = findOp('orchestrate_complete');
186
+ const result = (await op.handler({
187
+ planId: plan.id,
188
+ sessionId: session.id,
189
+ outcome: 'abandoned',
190
+ })) as { plan: { status: string }; session: { planOutcome: string | null } };
191
+
192
+ expect(result.plan.status).toBe('completed');
193
+ expect(result.session.planOutcome).toBe('abandoned');
194
+ });
195
+ });
196
+
197
+ // ─── orchestrate_status ─────────────────────────────────────────
198
+
199
+ describe('orchestrate_status', () => {
200
+ it('should return combined status', async () => {
201
+ const op = findOp('orchestrate_status');
202
+ const result = (await op.handler({})) as {
203
+ activePlans: unknown[];
204
+ sessionContext: { recentSessions: unknown[] };
205
+ vaultStats: { totalEntries: number };
206
+ recommendations: unknown[];
207
+ brainStats: { sessions: number };
208
+ };
209
+
210
+ expect(result.activePlans).toBeDefined();
211
+ expect(Array.isArray(result.activePlans)).toBe(true);
212
+ expect(result.sessionContext).toBeDefined();
213
+ expect(result.vaultStats).toBeDefined();
214
+ expect(result.recommendations).toBeDefined();
215
+ expect(result.brainStats).toBeDefined();
216
+ });
217
+
218
+ it('should include active plans in status', async () => {
219
+ runtime.planner.create({ objective: 'Active plan 1', scope: 'test' });
220
+ runtime.planner.create({ objective: 'Active plan 2', scope: 'test' });
221
+
222
+ const op = findOp('orchestrate_status');
223
+ const result = (await op.handler({})) as {
224
+ activePlans: Array<{ objective: string }>;
225
+ };
226
+
227
+ expect(result.activePlans).toHaveLength(2);
228
+ });
229
+
230
+ it('should respect sessionLimit parameter', async () => {
231
+ const op = findOp('orchestrate_status');
232
+ const result = (await op.handler({ sessionLimit: 2 })) as {
233
+ sessionContext: { recentSessions: unknown[] };
234
+ };
235
+
236
+ // No sessions exist, but the limit should be respected
237
+ expect(result.sessionContext.recentSessions).toBeDefined();
238
+ });
239
+ });
240
+
241
+ // ─── orchestrate_quick_capture ──────────────────────────────────
242
+
243
+ describe('orchestrate_quick_capture', () => {
244
+ it('should create, end, and extract in one call', async () => {
245
+ const op = findOp('orchestrate_quick_capture');
246
+ const result = (await op.handler({
247
+ domain: 'component',
248
+ context: 'Built a new date picker with keyboard navigation',
249
+ toolsUsed: ['validate_component_code', 'check_contrast'],
250
+ filesModified: ['src/DatePicker.tsx', 'src/DatePicker.test.tsx'],
251
+ outcome: 'completed',
252
+ })) as {
253
+ session: { id: string; endedAt: string | null; domain: string | null };
254
+ extraction: unknown;
255
+ };
256
+
257
+ expect(result.session).toBeDefined();
258
+ expect(result.session.endedAt).toBeDefined();
259
+ expect(result.session.domain).toBe('component');
260
+ });
261
+
262
+ it('should work with minimal params', async () => {
263
+ const op = findOp('orchestrate_quick_capture');
264
+ const result = (await op.handler({
265
+ domain: 'misc',
266
+ context: 'Fixed a typo',
267
+ })) as { session: { id: string } };
268
+
269
+ expect(result.session).toBeDefined();
270
+ expect(result.session.id).toBeDefined();
271
+ });
272
+
273
+ it('should handle abandoned outcome', async () => {
274
+ const op = findOp('orchestrate_quick_capture');
275
+ const result = (await op.handler({
276
+ domain: 'refactor',
277
+ context: 'Started refactor but rolled back',
278
+ outcome: 'abandoned',
279
+ })) as { session: { planOutcome: string | null } };
280
+
281
+ expect(result.session.planOutcome).toBe('abandoned');
282
+ });
283
+ });
284
+ });