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