@soleri/forge 4.2.2 → 5.0.1

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 +37 -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 +182 -456
  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 +23 -15
  72. package/src/patching.ts +44 -12
  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 +182 -458
  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,208 @@ 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 (37)
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
+ // Brain Intelligence ops (11)
164
+ expect(opNames).toContain('brain_session_context');
165
+ expect(opNames).toContain('brain_strengths');
166
+ expect(opNames).toContain('brain_global_patterns');
167
+ expect(opNames).toContain('brain_recommend');
168
+ expect(opNames).toContain('brain_build_intelligence');
169
+ expect(opNames).toContain('brain_export');
170
+ expect(opNames).toContain('brain_import');
171
+ expect(opNames).toContain('brain_extract_knowledge');
172
+ expect(opNames).toContain('brain_archive_sessions');
173
+ expect(opNames).toContain('brain_promote_proposals');
174
+ expect(opNames).toContain('brain_lifecycle');
175
+ // Agent-specific ops (5)
78
176
  expect(opNames).toContain('health');
79
177
  expect(opNames).toContain('identity');
80
178
  expect(opNames).toContain('activate');
81
179
  expect(opNames).toContain('inject_claude_md');
82
180
  expect(opNames).toContain('setup');
83
- expect(opNames).toContain('register');
84
- expect(opNames).toContain('llm_status');
181
+ // Total: 42
182
+ expect(facade.ops.length).toBe(42);
85
183
  });
86
184
 
87
185
  it('search should query across all domains with ranked results', async () => {
88
- vault.seed([
186
+ runtime.vault.seed([
187
+ makeEntry({ id: 'c1', domain: 'alpha', title: 'Alpha pattern', tags: ['a'] }),
188
+ makeEntry({ id: 'c2', domain: 'beta', title: 'Beta pattern', tags: ['b'] }),
189
+ ]);
190
+ runtime = createAgentRuntime({ agentId: '${config.id}', vaultPath: ':memory:', plansPath: join(plannerDir, 'plans2.json') });
191
+ runtime.vault.seed([
89
192
  makeEntry({ id: 'c1', domain: 'alpha', title: 'Alpha pattern', tags: ['a'] }),
90
193
  makeEntry({ id: 'c2', domain: 'beta', title: 'Beta pattern', tags: ['b'] }),
91
194
  ]);
92
- brain = new Brain(vault);
93
- const facade = makeCoreFacade();
195
+ const facade = buildCoreFacade();
94
196
  const searchOp = facade.ops.find((o) => o.name === 'search')!;
95
197
  const results = (await searchOp.handler({ query: 'pattern' })) as Array<{ entry: unknown; score: number; breakdown: unknown }>;
96
198
  expect(Array.isArray(results)).toBe(true);
97
199
  expect(results.length).toBe(2);
98
200
  expect(results[0].score).toBeGreaterThan(0);
99
- expect(results[0].breakdown).toBeDefined();
100
201
  });
101
202
 
102
203
  it('vault_stats should return counts', async () => {
103
- vault.seed([
204
+ runtime.vault.seed([
104
205
  makeEntry({ id: 'vs1', domain: 'd1', tags: ['x'] }),
105
206
  makeEntry({ id: 'vs2', domain: 'd2', tags: ['y'] }),
106
207
  ]);
107
- const facade = makeCoreFacade();
208
+ const facade = buildCoreFacade();
108
209
  const statsOp = facade.ops.find((o) => o.name === 'vault_stats')!;
109
210
  const stats = (await statsOp.handler({})) as { totalEntries: number };
110
211
  expect(stats.totalEntries).toBe(2);
111
212
  });
112
213
 
113
214
  it('health should return ok status', async () => {
114
- const facade = makeCoreFacade();
215
+ const facade = buildCoreFacade();
115
216
  const healthOp = facade.ops.find((o) => o.name === 'health')!;
116
217
  const health = (await healthOp.handler({})) as { status: string };
117
218
  expect(health.status).toBe('ok');
118
219
  });
119
220
 
120
221
  it('identity should return persona', async () => {
121
- const facade = makeCoreFacade();
222
+ const facade = buildCoreFacade();
122
223
  const identityOp = facade.ops.find((o) => o.name === 'identity')!;
123
224
  const persona = (await identityOp.handler({})) as { name: string; role: string };
124
225
  expect(persona.name).toBe('${escapeQuotes(config.name)}');
125
226
  expect(persona.role).toBe('${escapeQuotes(config.role)}');
126
227
  });
127
228
 
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
229
  it('activate should return persona and setup status', async () => {
140
- const facade = makeCoreFacade();
230
+ const facade = buildCoreFacade();
141
231
  const activateOp = facade.ops.find((o) => o.name === 'activate')!;
142
232
  const result = (await activateOp.handler({ projectPath: '/tmp/nonexistent-test' })) as {
143
233
  activated: boolean;
144
234
  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
235
  };
149
236
  expect(result.activated).toBe(true);
150
237
  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
238
  });
157
239
 
158
240
  it('activate with deactivate flag should return deactivation', async () => {
159
- const facade = makeCoreFacade();
241
+ const facade = buildCoreFacade();
160
242
  const activateOp = facade.ops.find((o) => o.name === 'activate')!;
161
243
  const result = (await activateOp.handler({ deactivate: true })) as { deactivated: boolean; message: string };
162
244
  expect(result.deactivated).toBe(true);
@@ -167,7 +249,7 @@ ${domainDescribes}
167
249
  const tempDir = join(tmpdir(), 'forge-inject-test-' + Date.now());
168
250
  mkdirSync(tempDir, { recursive: true });
169
251
  try {
170
- const facade = makeCoreFacade();
252
+ const facade = buildCoreFacade();
171
253
  const injectOp = facade.ops.find((o) => o.name === 'inject_claude_md')!;
172
254
  const result = (await injectOp.handler({ projectPath: tempDir })) as {
173
255
  injected: boolean;
@@ -184,180 +266,21 @@ ${domainDescribes}
184
266
  }
185
267
  });
186
268
 
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
269
  it('setup should return project and global CLAUDE.md status', async () => {
203
- const facade = makeCoreFacade();
270
+ const facade = buildCoreFacade();
204
271
  const setupOp = facade.ops.find((o) => o.name === 'setup')!;
205
272
  const result = (await setupOp.handler({ projectPath: '/tmp/nonexistent-test' })) as {
206
273
  agent: { name: string };
207
- claude_md: {
208
- project: { exists: boolean; has_agent_section: boolean };
209
- global: { exists: boolean; has_agent_section: boolean };
210
- };
211
274
  vault: { entries: number };
212
275
  recommendations: string[];
213
276
  };
214
277
  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
278
  expect(result.vault.entries).toBe(0);
218
279
  expect(result.recommendations.length).toBeGreaterThan(0);
219
280
  });
220
281
 
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
282
  it('create_plan should create a draft plan', async () => {
360
- const facade = makeCoreFacade();
283
+ const facade = buildCoreFacade();
361
284
  const createOp = facade.ops.find((o) => o.name === 'create_plan')!;
362
285
  const result = (await createOp.handler({
363
286
  objective: 'Add caching',
@@ -369,217 +292,34 @@ ${domainDescribes}
369
292
  expect(result.plan.tasks).toHaveLength(1);
370
293
  });
371
294
 
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
295
  it('brain_stats should return intelligence stats', async () => {
488
- const facade = makeCoreFacade();
296
+ const facade = buildCoreFacade();
489
297
  const statsOp = facade.ops.find((o) => o.name === 'brain_stats')!;
490
298
  const result = (await statsOp.handler({})) as {
491
299
  vocabularySize: number;
492
300
  feedbackCount: number;
493
- weights: { semantic: number; severity: number; recency: number; tagOverlap: number; domainMatch: number };
494
301
  };
495
302
  expect(result.vocabularySize).toBe(0);
496
303
  expect(result.feedbackCount).toBe(0);
497
- expect(result.weights.semantic).toBeCloseTo(0.40, 2);
498
- expect(result.weights.severity).toBeCloseTo(0.15, 2);
499
304
  });
500
305
 
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);
306
+ it('curator_status should return table counts', async () => {
307
+ const facade = buildCoreFacade();
308
+ const statusOp = facade.ops.find((o) => o.name === 'curator_status')!;
309
+ const result = (await statusOp.handler({})) as { initialized: boolean };
310
+ expect(result.initialized).toBe(true);
509
311
  });
510
312
 
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
- });
526
-
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);
543
- });
544
-
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'] }),
313
+ it('curator_health_audit should return score', async () => {
314
+ runtime.vault.seed([
315
+ makeEntry({ id: 'ha1', type: 'pattern', tags: ['a', 'b'] }),
316
+ makeEntry({ id: 'ha2', type: 'anti-pattern', tags: ['c', 'd'] }),
548
317
  ]);
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);
556
- });
557
-
558
- // ─── Cognee ops (graceful degradation without Docker) ─────────
559
-
560
- it('cognee_status should report not configured when cognee is undefined', async () => {
561
- const facade = makeCoreFacade();
562
- const op = facade.ops.find((o) => o.name === 'cognee_status')!;
563
- expect(op).toBeDefined();
564
- const result = (await op.handler({})) as { available: boolean };
565
- expect(result.available).toBe(false);
566
- });
567
-
568
- it('cognee_sync should return helpful message when cognee is unavailable', async () => {
569
- const facade = makeCoreFacade();
570
- const op = facade.ops.find((o) => o.name === 'cognee_sync')!;
571
- expect(op).toBeDefined();
572
- const result = (await op.handler({})) as { synced: boolean; message: string };
573
- expect(result.synced).toBe(false);
574
- expect(result.message).toContain('Cognee not available');
575
- });
576
-
577
- it('graph_search should return empty results when cognee is unavailable', async () => {
578
- const facade = makeCoreFacade();
579
- const op = facade.ops.find((o) => o.name === 'graph_search')!;
580
- expect(op).toBeDefined();
581
- const result = (await op.handler({ query: 'test' })) as { results: unknown[] };
582
- expect(result.results).toEqual([]);
318
+ runtime.curator.groomAll();
319
+ const facade = buildCoreFacade();
320
+ const healthOp = facade.ops.find((o) => o.name === 'curator_health_audit')!;
321
+ const result = (await healthOp.handler({})) as { score: number };
322
+ expect(result.score).toBeGreaterThan(0);
583
323
  });
584
324
  });
585
325
  });
@@ -588,11 +328,14 @@ ${domainDescribes}
588
328
 
589
329
  function generateDomainDescribe(agentId: string, domain: string): string {
590
330
  const facadeName = `${agentId}_${domain.replace(/-/g, '_')}`;
591
- const factoryFn = `create${pascalCase(domain)}Facade`;
592
331
 
593
332
  return ` describe('${facadeName}', () => {
333
+ function buildDomainFacade(): FacadeConfig {
334
+ return createDomainFacade(runtime, '${agentId}', '${domain}');
335
+ }
336
+
594
337
  it('should create facade with expected ops', () => {
595
- const facade = ${factoryFn}(vault, brain);
338
+ const facade = buildDomainFacade();
596
339
  expect(facade.name).toBe('${facadeName}');
597
340
  const opNames = facade.ops.map((o) => o.name);
598
341
  expect(opNames).toContain('get_patterns');
@@ -603,83 +346,64 @@ function generateDomainDescribe(agentId: string, domain: string): string {
603
346
  });
604
347
 
605
348
  it('get_patterns should return entries for ${domain}', async () => {
606
- vault.seed([
349
+ runtime.vault.seed([
607
350
  makeEntry({ id: '${domain}-gp1', domain: '${domain}', tags: ['test'] }),
608
351
  makeEntry({ id: 'other-gp1', domain: 'other-domain', tags: ['test'] }),
609
352
  ]);
610
- const facade = ${factoryFn}(vault, brain);
353
+ const facade = buildDomainFacade();
611
354
  const op = facade.ops.find((o) => o.name === 'get_patterns')!;
612
355
  const results = (await op.handler({})) as IntelligenceEntry[];
613
356
  expect(results.every((e) => e.domain === '${domain}')).toBe(true);
614
357
  });
615
358
 
616
359
  it('search should scope to ${domain} with ranked results', async () => {
617
- vault.seed([
360
+ runtime.vault.seed([
618
361
  makeEntry({ id: '${domain}-s1', domain: '${domain}', title: 'Domain specific pattern', tags: ['find-me'] }),
619
362
  makeEntry({ id: 'other-s1', domain: 'other', title: 'Other domain pattern', tags: ['nope'] }),
620
363
  ]);
621
- brain = new Brain(vault);
622
- const facade = ${factoryFn}(vault, brain);
364
+ runtime.brain.rebuildVocabulary();
365
+ const facade = buildDomainFacade();
623
366
  const op = facade.ops.find((o) => o.name === 'search')!;
624
- const results = (await op.handler({ query: 'pattern' })) as Array<{ entry: IntelligenceEntry; score: number; breakdown: unknown }>;
367
+ const results = (await op.handler({ query: 'pattern' })) as Array<{ entry: IntelligenceEntry; score: number }>;
625
368
  expect(results.every((r) => r.entry.domain === '${domain}')).toBe(true);
626
- if (results.length > 0) {
627
- expect(results[0].score).toBeGreaterThan(0);
628
- expect(results[0].breakdown).toBeDefined();
629
- }
630
369
  });
631
370
 
632
- it('capture should add entry with ${domain} domain and auto-tags', async () => {
633
- const facade = ${factoryFn}(vault, brain);
371
+ it('capture should add entry with ${domain} domain', async () => {
372
+ const facade = buildDomainFacade();
634
373
  const captureOp = facade.ops.find((o) => o.name === 'capture')!;
635
374
  const result = (await captureOp.handler({
636
375
  id: '${domain}-cap1',
637
376
  type: 'pattern',
638
- title: 'Captured Pattern for Intelligence',
377
+ title: 'Captured Pattern',
639
378
  severity: 'warning',
640
- description: 'A captured pattern for testing the brain intelligence layer.',
379
+ description: 'A captured pattern.',
641
380
  tags: ['captured'],
642
- })) as { captured: boolean; autoTags: string[] };
381
+ })) as { captured: boolean };
643
382
  expect(result.captured).toBe(true);
644
- expect(result.autoTags).toBeDefined();
645
- const entry = vault.get('${domain}-cap1');
383
+ const entry = runtime.vault.get('${domain}-cap1');
646
384
  expect(entry).not.toBeNull();
647
385
  expect(entry!.domain).toBe('${domain}');
648
386
  });
649
387
 
650
388
  it('get_entry should return specific entry', async () => {
651
- vault.seed([makeEntry({ id: '${domain}-ge1', domain: '${domain}', tags: ['test'] })]);
652
- const facade = ${factoryFn}(vault, brain);
389
+ runtime.vault.seed([makeEntry({ id: '${domain}-ge1', domain: '${domain}', tags: ['test'] })]);
390
+ const facade = buildDomainFacade();
653
391
  const op = facade.ops.find((o) => o.name === 'get_entry')!;
654
392
  const result = (await op.handler({ id: '${domain}-ge1' })) as IntelligenceEntry;
655
393
  expect(result.id).toBe('${domain}-ge1');
656
394
  });
657
395
 
658
- it('get_entry should return error for missing entry', async () => {
659
- const facade = ${factoryFn}(vault, brain);
660
- const op = facade.ops.find((o) => o.name === 'get_entry')!;
661
- const result = (await op.handler({ id: 'nonexistent' })) as { error: string };
662
- expect(result.error).toBeDefined();
663
- });
664
-
665
396
  it('remove should delete entry', async () => {
666
- vault.seed([makeEntry({ id: '${domain}-rm1', domain: '${domain}', tags: ['test'] })]);
667
- const facade = ${factoryFn}(vault, brain);
397
+ runtime.vault.seed([makeEntry({ id: '${domain}-rm1', domain: '${domain}', tags: ['test'] })]);
398
+ const facade = buildDomainFacade();
668
399
  const op = facade.ops.find((o) => o.name === 'remove')!;
669
400
  const result = (await op.handler({ id: '${domain}-rm1' })) as { removed: boolean };
670
401
  expect(result.removed).toBe(true);
671
- expect(vault.get('${domain}-rm1')).toBeNull();
402
+ expect(runtime.vault.get('${domain}-rm1')).toBeNull();
672
403
  });
673
404
  });`;
674
405
  }
675
406
 
676
- function pascalCase(s: string): string {
677
- return s
678
- .split(/[-_\s]+/)
679
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
680
- .join('');
681
- }
682
-
683
407
  function escapeQuotes(s: string): string {
684
408
  return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
685
409
  }