@soleri/forge 4.2.1 → 5.0.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 (78) hide show
  1. package/dist/domain-manager.d.ts +2 -2
  2. package/dist/domain-manager.js +35 -16
  3. package/dist/domain-manager.js.map +1 -1
  4. package/dist/index.js +0 -0
  5. package/dist/knowledge-installer.js +18 -12
  6. package/dist/knowledge-installer.js.map +1 -1
  7. package/dist/patching.d.ts +15 -6
  8. package/dist/patching.js +39 -12
  9. package/dist/patching.js.map +1 -1
  10. package/dist/scaffolder.js +18 -28
  11. package/dist/scaffolder.js.map +1 -1
  12. package/dist/templates/brain.d.ts +6 -0
  13. package/dist/templates/brain.js +478 -0
  14. package/dist/templates/brain.js.map +1 -0
  15. package/dist/templates/core-facade.js +95 -47
  16. package/dist/templates/core-facade.js.map +1 -1
  17. package/dist/templates/entry-point.d.ts +4 -0
  18. package/dist/templates/entry-point.js +146 -89
  19. package/dist/templates/entry-point.js.map +1 -1
  20. package/dist/templates/facade-factory.d.ts +1 -0
  21. package/dist/templates/facade-factory.js +63 -0
  22. package/dist/templates/facade-factory.js.map +1 -0
  23. package/dist/templates/facade-types.d.ts +1 -0
  24. package/dist/templates/facade-types.js +46 -0
  25. package/dist/templates/facade-types.js.map +1 -0
  26. package/dist/templates/intelligence-loader.d.ts +1 -0
  27. package/dist/templates/intelligence-loader.js +43 -0
  28. package/dist/templates/intelligence-loader.js.map +1 -0
  29. package/dist/templates/intelligence-types.d.ts +1 -0
  30. package/dist/templates/intelligence-types.js +24 -0
  31. package/dist/templates/intelligence-types.js.map +1 -0
  32. package/dist/templates/llm-key-pool.d.ts +7 -0
  33. package/dist/templates/llm-key-pool.js +211 -0
  34. package/dist/templates/llm-key-pool.js.map +1 -0
  35. package/dist/templates/llm-types.d.ts +5 -0
  36. package/dist/templates/llm-types.js +161 -0
  37. package/dist/templates/llm-types.js.map +1 -0
  38. package/dist/templates/llm-utils.d.ts +5 -0
  39. package/dist/templates/llm-utils.js +260 -0
  40. package/dist/templates/llm-utils.js.map +1 -0
  41. package/dist/templates/package-json.js +3 -1
  42. package/dist/templates/package-json.js.map +1 -1
  43. package/dist/templates/planner.d.ts +5 -0
  44. package/dist/templates/planner.js +150 -0
  45. package/dist/templates/planner.js.map +1 -0
  46. package/dist/templates/test-brain.d.ts +6 -0
  47. package/dist/templates/test-brain.js +474 -0
  48. package/dist/templates/test-brain.js.map +1 -0
  49. package/dist/templates/test-facades.d.ts +1 -1
  50. package/dist/templates/test-facades.js +170 -429
  51. package/dist/templates/test-facades.js.map +1 -1
  52. package/dist/templates/test-llm.d.ts +7 -0
  53. package/dist/templates/test-llm.js +574 -0
  54. package/dist/templates/test-llm.js.map +1 -0
  55. package/dist/templates/test-loader.d.ts +5 -0
  56. package/dist/templates/test-loader.js +146 -0
  57. package/dist/templates/test-loader.js.map +1 -0
  58. package/dist/templates/test-planner.d.ts +5 -0
  59. package/dist/templates/test-planner.js +271 -0
  60. package/dist/templates/test-planner.js.map +1 -0
  61. package/dist/templates/test-vault.d.ts +5 -0
  62. package/dist/templates/test-vault.js +380 -0
  63. package/dist/templates/test-vault.js.map +1 -0
  64. package/dist/templates/vault.d.ts +5 -0
  65. package/dist/templates/vault.js +263 -0
  66. package/dist/templates/vault.js.map +1 -0
  67. package/dist/types.d.ts +2 -2
  68. package/package.json +1 -1
  69. package/src/__tests__/scaffolder.test.ts +52 -109
  70. package/src/domain-manager.ts +34 -15
  71. package/src/knowledge-installer.ts +22 -15
  72. package/src/patching.ts +49 -11
  73. package/src/scaffolder.ts +18 -29
  74. package/src/templates/entry-point.ts +146 -91
  75. package/src/templates/package-json.ts +3 -1
  76. package/src/templates/test-facades.ts +170 -431
  77. package/src/templates/core-facade.ts +0 -517
  78. package/src/templates/llm-client.ts +0 -301
@@ -2,29 +2,27 @@ import type { AgentConfig } from '../types.js';
2
2
 
3
3
  /**
4
4
  * Generates facade integration tests for a new agent.
5
- * Tests all domain facades and the core facade.
5
+ * Uses runtime factories from @soleri/core tests both core ops and domain ops.
6
6
  */
7
7
  export function generateFacadesTest(config: AgentConfig): string {
8
- const domainImports = config.domains
9
- .map((d) => {
10
- const fn = `create${pascalCase(d)}Facade`;
11
- return `import { ${fn} } from '../facades/${d}.facade.js';`;
12
- })
13
- .join('\n');
14
-
15
8
  const domainDescribes = config.domains
16
9
  .map((d) => generateDomainDescribe(config.id, d))
17
10
  .join('\n\n');
18
11
 
19
12
  return `import { describe, it, expect, beforeEach, afterEach } from 'vitest';
20
- import { Vault, Brain, Planner, KeyPool } from '@soleri/core';
21
- import type { IntelligenceEntry } from '@soleri/core';
13
+ import {
14
+ createAgentRuntime,
15
+ createCoreOps,
16
+ createDomainFacade,
17
+ } from '@soleri/core';
18
+ import type { AgentRuntime, IntelligenceEntry, OpDefinition, FacadeConfig } from '@soleri/core';
19
+ import { z } from 'zod';
22
20
  import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
23
21
  import { join } from 'node:path';
24
22
  import { tmpdir } from 'node:os';
25
- ${domainImports}
26
- import { createCoreFacade } from '../facades/core.facade.js';
27
- import { LLMClient } from '../llm/llm-client.js';
23
+ import { PERSONA } from '../identity/persona.js';
24
+ import { activateAgent, deactivateAgent } from '../activation/activate.js';
25
+ import { injectClaudeMd, injectClaudeMdGlobal, hasAgentMarker } from '../activation/inject-claude-md.js';
28
26
 
29
27
  function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntry {
30
28
  return {
@@ -39,124 +37,196 @@ function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntr
39
37
  }
40
38
 
41
39
  describe('Facades', () => {
42
- let vault: Vault;
43
- let brain: Brain;
40
+ let runtime: AgentRuntime;
44
41
  let plannerDir: string;
45
- let planner: Planner;
46
- let openaiKeyPool: KeyPool;
47
- let anthropicKeyPool: KeyPool;
48
- let llmClient: LLMClient;
49
- const makeCoreFacade = () =>
50
- createCoreFacade(vault, planner, brain, undefined, llmClient, openaiKeyPool, anthropicKeyPool);
51
42
 
52
43
  beforeEach(() => {
53
- vault = new Vault(':memory:');
54
- brain = new Brain(vault);
55
44
  plannerDir = join(tmpdir(), 'forge-planner-test-' + Date.now());
56
45
  mkdirSync(plannerDir, { recursive: true });
57
- planner = new Planner(join(plannerDir, 'plans.json'));
58
- openaiKeyPool = new KeyPool({ keys: [] });
59
- anthropicKeyPool = new KeyPool({ keys: [] });
60
- llmClient = new LLMClient(openaiKeyPool, anthropicKeyPool);
46
+ runtime = createAgentRuntime({
47
+ agentId: '${config.id}',
48
+ vaultPath: ':memory:',
49
+ plansPath: join(plannerDir, 'plans.json'),
50
+ });
61
51
  });
62
52
 
63
53
  afterEach(() => {
64
- vault.close();
54
+ runtime.close();
65
55
  rmSync(plannerDir, { recursive: true, force: true });
66
56
  });
67
57
 
68
58
  ${domainDescribes}
69
59
 
70
60
  describe('${config.id}_core', () => {
61
+ function buildCoreFacade(): FacadeConfig {
62
+ const coreOps = createCoreOps(runtime);
63
+ const agentOps: OpDefinition[] = [
64
+ {
65
+ name: 'health',
66
+ description: 'Health check',
67
+ auth: 'read',
68
+ handler: async () => {
69
+ const stats = runtime.vault.stats();
70
+ return {
71
+ status: 'ok',
72
+ agent: { name: PERSONA.name, role: PERSONA.role },
73
+ vault: { entries: stats.totalEntries, domains: Object.keys(stats.byDomain) },
74
+ };
75
+ },
76
+ },
77
+ {
78
+ name: 'identity',
79
+ description: 'Agent identity',
80
+ auth: 'read',
81
+ handler: async () => PERSONA,
82
+ },
83
+ {
84
+ name: 'activate',
85
+ description: 'Activate agent',
86
+ auth: 'read',
87
+ schema: z.object({
88
+ projectPath: z.string().optional().default('.'),
89
+ deactivate: z.boolean().optional(),
90
+ }),
91
+ handler: async (params) => {
92
+ if (params.deactivate) return deactivateAgent();
93
+ return activateAgent(runtime.vault, (params.projectPath as string) ?? '.', runtime.planner);
94
+ },
95
+ },
96
+ {
97
+ name: 'inject_claude_md',
98
+ description: 'Inject CLAUDE.md',
99
+ auth: 'write',
100
+ schema: z.object({
101
+ projectPath: z.string().optional().default('.'),
102
+ global: z.boolean().optional(),
103
+ }),
104
+ handler: async (params) => {
105
+ if (params.global) return injectClaudeMdGlobal();
106
+ return injectClaudeMd((params.projectPath as string) ?? '.');
107
+ },
108
+ },
109
+ {
110
+ name: 'setup',
111
+ description: 'Setup status',
112
+ auth: 'read',
113
+ schema: z.object({ projectPath: z.string().optional().default('.') }),
114
+ handler: async (params) => {
115
+ const { existsSync: exists } = await import('node:fs');
116
+ const { join: joinPath } = await import('node:path');
117
+ const { homedir } = await import('node:os');
118
+ const pp = (params.projectPath as string) ?? '.';
119
+ const projectClaudeMd = joinPath(pp, 'CLAUDE.md');
120
+ const globalClaudeMd = joinPath(homedir(), '.claude', 'CLAUDE.md');
121
+ const stats = runtime.vault.stats();
122
+ const recommendations: string[] = [];
123
+ if (!hasAgentMarker(globalClaudeMd) && !hasAgentMarker(projectClaudeMd)) {
124
+ recommendations.push('No CLAUDE.md configured');
125
+ }
126
+ if (stats.totalEntries === 0) {
127
+ recommendations.push('Vault is empty');
128
+ }
129
+ if (recommendations.length === 0) {
130
+ recommendations.push('${config.name} is fully set up and ready!');
131
+ }
132
+ return {
133
+ agent: { name: PERSONA.name, role: PERSONA.role },
134
+ claude_md: {
135
+ project: { exists: exists(projectClaudeMd), has_agent_section: hasAgentMarker(projectClaudeMd) },
136
+ global: { exists: exists(globalClaudeMd), has_agent_section: hasAgentMarker(globalClaudeMd) },
137
+ },
138
+ vault: { entries: stats.totalEntries, domains: Object.keys(stats.byDomain) },
139
+ recommendations,
140
+ };
141
+ },
142
+ },
143
+ ];
144
+ return {
145
+ name: '${config.id}_core',
146
+ description: 'Core operations',
147
+ ops: [...coreOps, ...agentOps],
148
+ };
149
+ }
150
+
71
151
  it('should create core facade with expected ops', () => {
72
- const facade = makeCoreFacade();
152
+ const facade = buildCoreFacade();
73
153
  expect(facade.name).toBe('${config.id}_core');
74
154
  const opNames = facade.ops.map((o) => o.name);
155
+ // Core ops (26)
75
156
  expect(opNames).toContain('search');
76
157
  expect(opNames).toContain('vault_stats');
77
158
  expect(opNames).toContain('list_all');
159
+ expect(opNames).toContain('register');
160
+ expect(opNames).toContain('llm_status');
161
+ expect(opNames).toContain('curator_status');
162
+ expect(opNames).toContain('curator_health_audit');
163
+ // Agent-specific ops (5)
78
164
  expect(opNames).toContain('health');
79
165
  expect(opNames).toContain('identity');
80
166
  expect(opNames).toContain('activate');
81
167
  expect(opNames).toContain('inject_claude_md');
82
168
  expect(opNames).toContain('setup');
83
- expect(opNames).toContain('register');
84
- expect(opNames).toContain('llm_status');
169
+ // Total: 31
170
+ expect(facade.ops.length).toBe(31);
85
171
  });
86
172
 
87
173
  it('search should query across all domains with ranked results', async () => {
88
- vault.seed([
174
+ runtime.vault.seed([
175
+ makeEntry({ id: 'c1', domain: 'alpha', title: 'Alpha pattern', tags: ['a'] }),
176
+ makeEntry({ id: 'c2', domain: 'beta', title: 'Beta pattern', tags: ['b'] }),
177
+ ]);
178
+ runtime = createAgentRuntime({ agentId: '${config.id}', vaultPath: ':memory:', plansPath: join(plannerDir, 'plans2.json') });
179
+ runtime.vault.seed([
89
180
  makeEntry({ id: 'c1', domain: 'alpha', title: 'Alpha pattern', tags: ['a'] }),
90
181
  makeEntry({ id: 'c2', domain: 'beta', title: 'Beta pattern', tags: ['b'] }),
91
182
  ]);
92
- brain = new Brain(vault);
93
- const facade = makeCoreFacade();
183
+ const facade = buildCoreFacade();
94
184
  const searchOp = facade.ops.find((o) => o.name === 'search')!;
95
185
  const results = (await searchOp.handler({ query: 'pattern' })) as Array<{ entry: unknown; score: number; breakdown: unknown }>;
96
186
  expect(Array.isArray(results)).toBe(true);
97
187
  expect(results.length).toBe(2);
98
188
  expect(results[0].score).toBeGreaterThan(0);
99
- expect(results[0].breakdown).toBeDefined();
100
189
  });
101
190
 
102
191
  it('vault_stats should return counts', async () => {
103
- vault.seed([
192
+ runtime.vault.seed([
104
193
  makeEntry({ id: 'vs1', domain: 'd1', tags: ['x'] }),
105
194
  makeEntry({ id: 'vs2', domain: 'd2', tags: ['y'] }),
106
195
  ]);
107
- const facade = makeCoreFacade();
196
+ const facade = buildCoreFacade();
108
197
  const statsOp = facade.ops.find((o) => o.name === 'vault_stats')!;
109
198
  const stats = (await statsOp.handler({})) as { totalEntries: number };
110
199
  expect(stats.totalEntries).toBe(2);
111
200
  });
112
201
 
113
202
  it('health should return ok status', async () => {
114
- const facade = makeCoreFacade();
203
+ const facade = buildCoreFacade();
115
204
  const healthOp = facade.ops.find((o) => o.name === 'health')!;
116
205
  const health = (await healthOp.handler({})) as { status: string };
117
206
  expect(health.status).toBe('ok');
118
207
  });
119
208
 
120
209
  it('identity should return persona', async () => {
121
- const facade = makeCoreFacade();
210
+ const facade = buildCoreFacade();
122
211
  const identityOp = facade.ops.find((o) => o.name === 'identity')!;
123
212
  const persona = (await identityOp.handler({})) as { name: string; role: string };
124
213
  expect(persona.name).toBe('${escapeQuotes(config.name)}');
125
214
  expect(persona.role).toBe('${escapeQuotes(config.role)}');
126
215
  });
127
216
 
128
- it('list_all should support domain filter', async () => {
129
- vault.seed([
130
- makeEntry({ id: 'la1', domain: 'alpha', tags: ['a'] }),
131
- makeEntry({ id: 'la2', domain: 'beta', tags: ['b'] }),
132
- ]);
133
- const facade = makeCoreFacade();
134
- const listOp = facade.ops.find((o) => o.name === 'list_all')!;
135
- const filtered = (await listOp.handler({ domain: 'alpha' })) as unknown[];
136
- expect(filtered).toHaveLength(1);
137
- });
138
-
139
217
  it('activate should return persona and setup status', async () => {
140
- const facade = makeCoreFacade();
218
+ const facade = buildCoreFacade();
141
219
  const activateOp = facade.ops.find((o) => o.name === 'activate')!;
142
220
  const result = (await activateOp.handler({ projectPath: '/tmp/nonexistent-test' })) as {
143
221
  activated: boolean;
144
222
  persona: { name: string; role: string };
145
- guidelines: string[];
146
- tool_recommendations: unknown[];
147
- setup_status: { claude_md_injected: boolean; global_claude_md_injected: boolean; vault_has_entries: boolean };
148
223
  };
149
224
  expect(result.activated).toBe(true);
150
225
  expect(result.persona.name).toBe('${escapeQuotes(config.name)}');
151
- expect(result.persona.role).toBe('${escapeQuotes(config.role)}');
152
- expect(result.guidelines.length).toBeGreaterThan(0);
153
- expect(result.tool_recommendations.length).toBeGreaterThan(0);
154
- expect(result.setup_status.claude_md_injected).toBe(false);
155
- expect(typeof result.setup_status.global_claude_md_injected).toBe('boolean');
156
226
  });
157
227
 
158
228
  it('activate with deactivate flag should return deactivation', async () => {
159
- const facade = makeCoreFacade();
229
+ const facade = buildCoreFacade();
160
230
  const activateOp = facade.ops.find((o) => o.name === 'activate')!;
161
231
  const result = (await activateOp.handler({ deactivate: true })) as { deactivated: boolean; message: string };
162
232
  expect(result.deactivated).toBe(true);
@@ -167,7 +237,7 @@ ${domainDescribes}
167
237
  const tempDir = join(tmpdir(), 'forge-inject-test-' + Date.now());
168
238
  mkdirSync(tempDir, { recursive: true });
169
239
  try {
170
- const facade = makeCoreFacade();
240
+ const facade = buildCoreFacade();
171
241
  const injectOp = facade.ops.find((o) => o.name === 'inject_claude_md')!;
172
242
  const result = (await injectOp.handler({ projectPath: tempDir })) as {
173
243
  injected: boolean;
@@ -184,180 +254,21 @@ ${domainDescribes}
184
254
  }
185
255
  });
186
256
 
187
- it('inject_claude_md with global flag should target ~/.claude/CLAUDE.md', async () => {
188
- // We test the global path resolution by checking the returned path
189
- // contains .claude/CLAUDE.md (actual write may or may not happen depending on env)
190
- const facade = makeCoreFacade();
191
- const injectOp = facade.ops.find((o) => o.name === 'inject_claude_md')!;
192
- const result = (await injectOp.handler({ global: true })) as {
193
- injected: boolean;
194
- path: string;
195
- action: string;
196
- };
197
- expect(result.injected).toBe(true);
198
- expect(result.path).toContain('.claude');
199
- expect(result.path).toContain('CLAUDE.md');
200
- });
201
-
202
257
  it('setup should return project and global CLAUDE.md status', async () => {
203
- const facade = makeCoreFacade();
258
+ const facade = buildCoreFacade();
204
259
  const setupOp = facade.ops.find((o) => o.name === 'setup')!;
205
260
  const result = (await setupOp.handler({ projectPath: '/tmp/nonexistent-test' })) as {
206
261
  agent: { name: string };
207
- claude_md: {
208
- project: { exists: boolean; has_agent_section: boolean };
209
- global: { exists: boolean; has_agent_section: boolean };
210
- };
211
262
  vault: { entries: number };
212
263
  recommendations: string[];
213
264
  };
214
265
  expect(result.agent.name).toBe('${escapeQuotes(config.name)}');
215
- expect(result.claude_md.project.exists).toBe(false);
216
- expect(typeof result.claude_md.global.exists).toBe('boolean');
217
266
  expect(result.vault.entries).toBe(0);
218
267
  expect(result.recommendations.length).toBeGreaterThan(0);
219
268
  });
220
269
 
221
- it('register should track new project', async () => {
222
- const facade = makeCoreFacade();
223
- const registerOp = facade.ops.find((o) => o.name === 'register')!;
224
- const result = (await registerOp.handler({ projectPath: '/tmp/reg-test-project', name: 'reg-test' })) as {
225
- project: { path: string; name: string; sessionCount: number };
226
- is_new: boolean;
227
- message: string;
228
- vault: { entries: number };
229
- };
230
- expect(result.is_new).toBe(true);
231
- expect(result.project.name).toBe('reg-test');
232
- expect(result.project.sessionCount).toBe(1);
233
- expect(result.message).toContain('Welcome');
234
- });
235
-
236
- it('register should increment session count for returning project', async () => {
237
- const facade = makeCoreFacade();
238
- const registerOp = facade.ops.find((o) => o.name === 'register')!;
239
- await registerOp.handler({ projectPath: '/tmp/reg-test-returning', name: 'returning' });
240
- const result = (await registerOp.handler({ projectPath: '/tmp/reg-test-returning', name: 'returning' })) as {
241
- project: { sessionCount: number };
242
- is_new: boolean;
243
- message: string;
244
- };
245
- expect(result.is_new).toBe(false);
246
- expect(result.project.sessionCount).toBe(2);
247
- expect(result.message).toContain('Welcome back');
248
- });
249
-
250
- it('memory_capture should store a memory', async () => {
251
- const facade = makeCoreFacade();
252
- const captureOp = facade.ops.find((o) => o.name === 'memory_capture')!;
253
- const result = (await captureOp.handler({
254
- projectPath: '/test',
255
- type: 'lesson',
256
- context: 'Testing facades',
257
- summary: 'Facade tests work great',
258
- topics: ['testing'],
259
- filesModified: [],
260
- toolsUsed: [],
261
- })) as { captured: boolean; memory: { id: string; type: string } };
262
- expect(result.captured).toBe(true);
263
- expect(result.memory.id).toMatch(/^mem-/);
264
- expect(result.memory.type).toBe('lesson');
265
- });
266
-
267
- it('memory_search should find captured memories', async () => {
268
- const facade = makeCoreFacade();
269
- const captureOp = facade.ops.find((o) => o.name === 'memory_capture')!;
270
- await captureOp.handler({
271
- projectPath: '/test',
272
- type: 'lesson',
273
- context: 'Database optimization',
274
- summary: 'Learned about index strategies for PostgreSQL',
275
- topics: ['database'],
276
- filesModified: [],
277
- toolsUsed: [],
278
- });
279
- const searchOp = facade.ops.find((o) => o.name === 'memory_search')!;
280
- const results = (await searchOp.handler({ query: 'index strategies' })) as Array<{ summary: string }>;
281
- expect(results.length).toBeGreaterThan(0);
282
- expect(results[0].summary).toContain('index strategies');
283
- });
284
-
285
- it('memory_list should return all memories with stats', async () => {
286
- const facade = makeCoreFacade();
287
- const captureOp = facade.ops.find((o) => o.name === 'memory_capture')!;
288
- await captureOp.handler({
289
- projectPath: '/test',
290
- type: 'session',
291
- context: 'ctx',
292
- summary: 'Session summary',
293
- topics: [],
294
- filesModified: [],
295
- toolsUsed: [],
296
- });
297
- const listOp = facade.ops.find((o) => o.name === 'memory_list')!;
298
- const result = (await listOp.handler({})) as {
299
- memories: unknown[];
300
- stats: { total: number };
301
- };
302
- expect(result.memories).toHaveLength(1);
303
- expect(result.stats.total).toBe(1);
304
- });
305
-
306
- it('session_capture should store a session memory', async () => {
307
- const facade = makeCoreFacade();
308
- const sessionOp = facade.ops.find((o) => o.name === 'session_capture')!;
309
- const result = (await sessionOp.handler({
310
- projectPath: '/tmp/test-session',
311
- summary: 'Worked on vault memory system',
312
- topics: ['vault', 'memory'],
313
- filesModified: ['vault.ts'],
314
- toolsUsed: ['Edit', 'Bash'],
315
- })) as { captured: boolean; memory: { type: string; summary: string }; message: string };
316
- expect(result.captured).toBe(true);
317
- expect(result.memory.type).toBe('session');
318
- expect(result.memory.summary).toBe('Worked on vault memory system');
319
- expect(result.message).toContain('saved');
320
- });
321
-
322
- it('export should return vault entries as bundles', async () => {
323
- vault.seed([
324
- makeEntry({ id: 'exp1', domain: 'security', tags: ['auth'] }),
325
- makeEntry({ id: 'exp2', domain: 'security', tags: ['xss'] }),
326
- makeEntry({ id: 'exp3', domain: 'api-design', tags: ['rest'] }),
327
- ]);
328
- const facade = makeCoreFacade();
329
- const exportOp = facade.ops.find((o) => o.name === 'export')!;
330
- const result = (await exportOp.handler({})) as {
331
- exported: boolean;
332
- bundles: Array<{ domain: string; entries: unknown[] }>;
333
- totalEntries: number;
334
- domains: string[];
335
- };
336
- expect(result.exported).toBe(true);
337
- expect(result.totalEntries).toBe(3);
338
- expect(result.domains).toContain('security');
339
- expect(result.domains).toContain('api-design');
340
- expect(result.bundles.find((b) => b.domain === 'security')!.entries).toHaveLength(2);
341
- });
342
-
343
- it('export should filter by domain', async () => {
344
- vault.seed([
345
- makeEntry({ id: 'exp-d1', domain: 'security', tags: ['auth'] }),
346
- makeEntry({ id: 'exp-d2', domain: 'api-design', tags: ['rest'] }),
347
- ]);
348
- const facade = makeCoreFacade();
349
- const exportOp = facade.ops.find((o) => o.name === 'export')!;
350
- const result = (await exportOp.handler({ domain: 'security' })) as {
351
- bundles: Array<{ domain: string; entries: unknown[] }>;
352
- totalEntries: number;
353
- };
354
- expect(result.totalEntries).toBe(1);
355
- expect(result.bundles).toHaveLength(1);
356
- expect(result.bundles[0].domain).toBe('security');
357
- });
358
-
359
270
  it('create_plan should create a draft plan', async () => {
360
- const facade = makeCoreFacade();
271
+ const facade = buildCoreFacade();
361
272
  const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
362
273
  const result = (await createOp.handler({
363
274
  objective: 'Add caching',
@@ -369,190 +280,34 @@ ${domainDescribes}
369
280
  expect(result.plan.tasks).toHaveLength(1);
370
281
  });
371
282
 
372
- it('get_plan should return a plan by id', async () => {
373
- const facade = makeCoreFacade();
374
- const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
375
- const created = (await createOp.handler({ objective: 'Test', scope: 'test' })) as { plan: { id: string } };
376
- const getOp = facade.ops.find((o) => o.name === 'get_plan')!;
377
- const result = (await getOp.handler({ planId: created.plan.id })) as { id: string; objective: string };
378
- expect(result.id).toBe(created.plan.id);
379
- expect(result.objective).toBe('Test');
380
- });
381
-
382
- it('get_plan without id should return active plans', async () => {
383
- const facade = makeCoreFacade();
384
- const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
385
- await createOp.handler({ objective: 'Plan A', scope: 'a' });
386
- const getOp = facade.ops.find((o) => o.name === 'get_plan')!;
387
- const result = (await getOp.handler({})) as { active: unknown[]; executing: unknown[] };
388
- expect(result.active).toHaveLength(1);
389
- expect(result.executing).toHaveLength(0);
390
- });
391
-
392
- it('approve_plan should approve and optionally start execution', async () => {
393
- const facade = makeCoreFacade();
394
- const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
395
- const created = (await createOp.handler({ objective: 'Approve', scope: 'test' })) as { plan: { id: string } };
396
- const approveOp = facade.ops.find((o) => o.name === 'approve_plan')!;
397
- const result = (await approveOp.handler({ planId: created.plan.id, startExecution: true })) as {
398
- approved: boolean;
399
- executing: boolean;
400
- plan: { status: string };
401
- };
402
- expect(result.approved).toBe(true);
403
- expect(result.executing).toBe(true);
404
- expect(result.plan.status).toBe('executing');
405
- });
406
-
407
- it('update_task should update task status', async () => {
408
- const facade = makeCoreFacade();
409
- const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
410
- const created = (await createOp.handler({
411
- objective: 'Task update',
412
- scope: 'test',
413
- tasks: [{ title: 'T1', description: 'Do thing' }],
414
- })) as { plan: { id: string } };
415
- const approveOp = facade.ops.find((o) => o.name === 'approve_plan')!;
416
- await approveOp.handler({ planId: created.plan.id, startExecution: true });
417
- const updateOp = facade.ops.find((o) => o.name === 'update_task')!;
418
- const result = (await updateOp.handler({
419
- planId: created.plan.id,
420
- taskId: 'task-1',
421
- status: 'completed',
422
- })) as { updated: boolean; task: { status: string } };
423
- expect(result.updated).toBe(true);
424
- expect(result.task!.status).toBe('completed');
425
- });
426
-
427
- it('complete_plan should complete with task summary', async () => {
428
- const facade = makeCoreFacade();
429
- const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
430
- const created = (await createOp.handler({
431
- objective: 'Complete me',
432
- scope: 'test',
433
- tasks: [
434
- { title: 'T1', description: 'Do thing' },
435
- { title: 'T2', description: 'Another' },
436
- ],
437
- })) as { plan: { id: string } };
438
- const approveOp = facade.ops.find((o) => o.name === 'approve_plan')!;
439
- await approveOp.handler({ planId: created.plan.id, startExecution: true });
440
- const updateOp = facade.ops.find((o) => o.name === 'update_task')!;
441
- await updateOp.handler({ planId: created.plan.id, taskId: 'task-1', status: 'completed' });
442
- await updateOp.handler({ planId: created.plan.id, taskId: 'task-2', status: 'skipped' });
443
- const completeOp = facade.ops.find((o) => o.name === 'complete_plan')!;
444
- const result = (await completeOp.handler({ planId: created.plan.id })) as {
445
- completed: boolean;
446
- taskSummary: { completed: number; skipped: number; total: number };
447
- };
448
- expect(result.completed).toBe(true);
449
- expect(result.taskSummary.completed).toBe(1);
450
- expect(result.taskSummary.skipped).toBe(1);
451
- expect(result.taskSummary.total).toBe(2);
452
- });
453
-
454
- it('should have brain ops in core facade', () => {
455
- const facade = makeCoreFacade();
456
- const opNames = facade.ops.map((o) => o.name);
457
- expect(opNames).toContain('record_feedback');
458
- expect(opNames).toContain('rebuild_vocabulary');
459
- expect(opNames).toContain('brain_stats');
460
- });
461
-
462
- it('record_feedback should record search feedback', async () => {
463
- const facade = makeCoreFacade();
464
- const feedbackOp = facade.ops.find((o) => o.name === 'record_feedback')!;
465
- const result = (await feedbackOp.handler({
466
- query: 'test query',
467
- entryId: 'test-entry',
468
- action: 'accepted',
469
- })) as { recorded: boolean; query: string; action: string };
470
- expect(result.recorded).toBe(true);
471
- expect(result.query).toBe('test query');
472
- expect(result.action).toBe('accepted');
473
- });
474
-
475
- it('rebuild_vocabulary should rebuild and return size', async () => {
476
- vault.seed([
477
- makeEntry({ id: 'rv1', title: 'Rebuild vocab test', description: 'Testing vocabulary rebuild.', tags: ['rebuild'] }),
478
- ]);
479
- brain = new Brain(vault);
480
- const facade = makeCoreFacade();
481
- const rebuildOp = facade.ops.find((o) => o.name === 'rebuild_vocabulary')!;
482
- const result = (await rebuildOp.handler({})) as { rebuilt: boolean; vocabularySize: number };
483
- expect(result.rebuilt).toBe(true);
484
- expect(result.vocabularySize).toBeGreaterThan(0);
485
- });
486
-
487
283
  it('brain_stats should return intelligence stats', async () => {
488
- const facade = makeCoreFacade();
284
+ const facade = buildCoreFacade();
489
285
  const statsOp = facade.ops.find((o) => o.name === 'brain_stats')!;
490
286
  const result = (await statsOp.handler({})) as {
491
287
  vocabularySize: number;
492
288
  feedbackCount: number;
493
- weights: { semantic: number; severity: number; recency: number; tagOverlap: number; domainMatch: number };
494
289
  };
495
290
  expect(result.vocabularySize).toBe(0);
496
291
  expect(result.feedbackCount).toBe(0);
497
- expect(result.weights.semantic).toBeCloseTo(0.40, 2);
498
- expect(result.weights.severity).toBeCloseTo(0.15, 2);
499
- });
500
-
501
- it('brain_stats should reflect feedback count after recording', async () => {
502
- const facade = makeCoreFacade();
503
- const feedbackOp = facade.ops.find((o) => o.name === 'record_feedback')!;
504
- await feedbackOp.handler({ query: 'q1', entryId: 'e1', action: 'accepted' });
505
- await feedbackOp.handler({ query: 'q2', entryId: 'e2', action: 'dismissed' });
506
- const statsOp = facade.ops.find((o) => o.name === 'brain_stats')!;
507
- const result = (await statsOp.handler({})) as { feedbackCount: number };
508
- expect(result.feedbackCount).toBe(2);
509
- });
510
-
511
- it('llm_status should return provider availability', async () => {
512
- const facade = makeCoreFacade();
513
- const llmStatusOp = facade.ops.find((o) => o.name === 'llm_status')!;
514
- const result = (await llmStatusOp.handler({})) as {
515
- providers: {
516
- openai: { available: boolean; keyPool: unknown };
517
- anthropic: { available: boolean; keyPool: unknown };
518
- };
519
- routes: unknown[];
520
- };
521
- expect(result.providers.openai.available).toBe(false);
522
- expect(result.providers.anthropic.available).toBe(false);
523
- expect(result.providers.openai.keyPool).toBeNull();
524
- expect(Array.isArray(result.routes)).toBe(true);
525
292
  });
526
293
 
527
- it('llm_status should reflect key pool when keys present', async () => {
528
- const oPool = new KeyPool({ keys: ['sk-test'] });
529
- const aPool = new KeyPool({ keys: ['sk-ant-test'] });
530
- const client = new LLMClient(oPool, aPool);
531
- const facade = createCoreFacade(vault, planner, brain, undefined, client, oPool, aPool);
532
- const llmStatusOp = facade.ops.find((o) => o.name === 'llm_status')!;
533
- const result = (await llmStatusOp.handler({})) as {
534
- providers: {
535
- openai: { available: boolean; keyPool: { poolSize: number } };
536
- anthropic: { available: boolean; keyPool: { poolSize: number } };
537
- };
538
- };
539
- expect(result.providers.openai.available).toBe(true);
540
- expect(result.providers.openai.keyPool.poolSize).toBe(1);
541
- expect(result.providers.anthropic.available).toBe(true);
542
- expect(result.providers.anthropic.keyPool.poolSize).toBe(1);
294
+ it('curator_status should return table counts', async () => {
295
+ const facade = buildCoreFacade();
296
+ const statusOp = facade.ops.find((o) => o.name === 'curator_status')!;
297
+ const result = (await statusOp.handler({})) as { initialized: boolean };
298
+ expect(result.initialized).toBe(true);
543
299
  });
544
300
 
545
- it('search through brain should return ranked results with breakdowns', async () => {
546
- vault.seed([
547
- makeEntry({ id: 'bs-1', domain: 'security', title: 'Authentication token handling', severity: 'critical', description: 'Always validate JWT tokens.', tags: ['auth', 'jwt'] }),
301
+ it('curator_health_audit should return score', async () => {
302
+ runtime.vault.seed([
303
+ makeEntry({ id: 'ha1', type: 'pattern', tags: ['a', 'b'] }),
304
+ makeEntry({ id: 'ha2', type: 'anti-pattern', tags: ['c', 'd'] }),
548
305
  ]);
549
- brain = new Brain(vault);
550
- const facade = makeCoreFacade();
551
- const searchOp = facade.ops.find((o) => o.name === 'search')!;
552
- const results = (await searchOp.handler({ query: 'authentication token' })) as Array<{ entry: { id: string }; score: number; breakdown: { semantic: number; total: number } }>;
553
- expect(results.length).toBeGreaterThan(0);
554
- expect(results[0].score).toBeGreaterThan(0);
555
- expect(results[0].breakdown.total).toBe(results[0].score);
306
+ runtime.curator.groomAll();
307
+ const facade = buildCoreFacade();
308
+ const healthOp = facade.ops.find((o) => o.name === 'curator_health_audit')!;
309
+ const result = (await healthOp.handler({})) as { score: number };
310
+ expect(result.score).toBeGreaterThan(0);
556
311
  });
557
312
  });
558
313
  });
@@ -561,11 +316,14 @@ ${domainDescribes}
561
316
 
562
317
  function generateDomainDescribe(agentId: string, domain: string): string {
563
318
  const facadeName = `${agentId}_${domain.replace(/-/g, '_')}`;
564
- const factoryFn = `create${pascalCase(domain)}Facade`;
565
319
 
566
320
  return ` describe('${facadeName}', () => {
321
+ function buildDomainFacade(): FacadeConfig {
322
+ return createDomainFacade(runtime, '${agentId}', '${domain}');
323
+ }
324
+
567
325
  it('should create facade with expected ops', () => {
568
- const facade = ${factoryFn}(vault, brain);
326
+ const facade = buildDomainFacade();
569
327
  expect(facade.name).toBe('${facadeName}');
570
328
  const opNames = facade.ops.map((o) => o.name);
571
329
  expect(opNames).toContain('get_patterns');
@@ -576,83 +334,64 @@ function generateDomainDescribe(agentId: string, domain: string): string {
576
334
  });
577
335
 
578
336
  it('get_patterns should return entries for ${domain}', async () => {
579
- vault.seed([
337
+ runtime.vault.seed([
580
338
  makeEntry({ id: '${domain}-gp1', domain: '${domain}', tags: ['test'] }),
581
339
  makeEntry({ id: 'other-gp1', domain: 'other-domain', tags: ['test'] }),
582
340
  ]);
583
- const facade = ${factoryFn}(vault, brain);
341
+ const facade = buildDomainFacade();
584
342
  const op = facade.ops.find((o) => o.name === 'get_patterns')!;
585
343
  const results = (await op.handler({})) as IntelligenceEntry[];
586
344
  expect(results.every((e) => e.domain === '${domain}')).toBe(true);
587
345
  });
588
346
 
589
347
  it('search should scope to ${domain} with ranked results', async () => {
590
- vault.seed([
348
+ runtime.vault.seed([
591
349
  makeEntry({ id: '${domain}-s1', domain: '${domain}', title: 'Domain specific pattern', tags: ['find-me'] }),
592
350
  makeEntry({ id: 'other-s1', domain: 'other', title: 'Other domain pattern', tags: ['nope'] }),
593
351
  ]);
594
- brain = new Brain(vault);
595
- const facade = ${factoryFn}(vault, brain);
352
+ runtime.brain.rebuildVocabulary();
353
+ const facade = buildDomainFacade();
596
354
  const op = facade.ops.find((o) => o.name === 'search')!;
597
- const results = (await op.handler({ query: 'pattern' })) as Array<{ entry: IntelligenceEntry; score: number; breakdown: unknown }>;
355
+ const results = (await op.handler({ query: 'pattern' })) as Array<{ entry: IntelligenceEntry; score: number }>;
598
356
  expect(results.every((r) => r.entry.domain === '${domain}')).toBe(true);
599
- if (results.length > 0) {
600
- expect(results[0].score).toBeGreaterThan(0);
601
- expect(results[0].breakdown).toBeDefined();
602
- }
603
357
  });
604
358
 
605
- it('capture should add entry with ${domain} domain and auto-tags', async () => {
606
- const facade = ${factoryFn}(vault, brain);
359
+ it('capture should add entry with ${domain} domain', async () => {
360
+ const facade = buildDomainFacade();
607
361
  const captureOp = facade.ops.find((o) => o.name === 'capture')!;
608
362
  const result = (await captureOp.handler({
609
363
  id: '${domain}-cap1',
610
364
  type: 'pattern',
611
- title: 'Captured Pattern for Intelligence',
365
+ title: 'Captured Pattern',
612
366
  severity: 'warning',
613
- description: 'A captured pattern for testing the brain intelligence layer.',
367
+ description: 'A captured pattern.',
614
368
  tags: ['captured'],
615
- })) as { captured: boolean; autoTags: string[] };
369
+ })) as { captured: boolean };
616
370
  expect(result.captured).toBe(true);
617
- expect(result.autoTags).toBeDefined();
618
- const entry = vault.get('${domain}-cap1');
371
+ const entry = runtime.vault.get('${domain}-cap1');
619
372
  expect(entry).not.toBeNull();
620
373
  expect(entry!.domain).toBe('${domain}');
621
374
  });
622
375
 
623
376
  it('get_entry should return specific entry', async () => {
624
- vault.seed([makeEntry({ id: '${domain}-ge1', domain: '${domain}', tags: ['test'] })]);
625
- const facade = ${factoryFn}(vault, brain);
377
+ runtime.vault.seed([makeEntry({ id: '${domain}-ge1', domain: '${domain}', tags: ['test'] })]);
378
+ const facade = buildDomainFacade();
626
379
  const op = facade.ops.find((o) => o.name === 'get_entry')!;
627
380
  const result = (await op.handler({ id: '${domain}-ge1' })) as IntelligenceEntry;
628
381
  expect(result.id).toBe('${domain}-ge1');
629
382
  });
630
383
 
631
- it('get_entry should return error for missing entry', async () => {
632
- const facade = ${factoryFn}(vault, brain);
633
- const op = facade.ops.find((o) => o.name === 'get_entry')!;
634
- const result = (await op.handler({ id: 'nonexistent' })) as { error: string };
635
- expect(result.error).toBeDefined();
636
- });
637
-
638
384
  it('remove should delete entry', async () => {
639
- vault.seed([makeEntry({ id: '${domain}-rm1', domain: '${domain}', tags: ['test'] })]);
640
- const facade = ${factoryFn}(vault, brain);
385
+ runtime.vault.seed([makeEntry({ id: '${domain}-rm1', domain: '${domain}', tags: ['test'] })]);
386
+ const facade = buildDomainFacade();
641
387
  const op = facade.ops.find((o) => o.name === 'remove')!;
642
388
  const result = (await op.handler({ id: '${domain}-rm1' })) as { removed: boolean };
643
389
  expect(result.removed).toBe(true);
644
- expect(vault.get('${domain}-rm1')).toBeNull();
390
+ expect(runtime.vault.get('${domain}-rm1')).toBeNull();
645
391
  });
646
392
  });`;
647
393
  }
648
394
 
649
- function pascalCase(s: string): string {
650
- return s
651
- .split(/[-_\s]+/)
652
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
653
- .join('');
654
- }
655
-
656
395
  function escapeQuotes(s: string): string {
657
396
  return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
658
397
  }