@soleri/cli 1.8.0 → 1.9.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 (43) hide show
  1. package/code-reviewer/.claude/hookify.focus-ring-required.local.md +21 -0
  2. package/code-reviewer/.claude/hookify.no-ai-attribution.local.md +18 -0
  3. package/code-reviewer/.claude/hookify.no-any-types.local.md +18 -0
  4. package/code-reviewer/.claude/hookify.no-console-log.local.md +21 -0
  5. package/code-reviewer/.claude/hookify.no-important.local.md +18 -0
  6. package/code-reviewer/.claude/hookify.no-inline-styles.local.md +21 -0
  7. package/code-reviewer/.claude/hookify.semantic-html.local.md +18 -0
  8. package/code-reviewer/.claude/hookify.ux-touch-targets.local.md +18 -0
  9. package/code-reviewer/.mcp.json +11 -0
  10. package/code-reviewer/README.md +346 -0
  11. package/code-reviewer/package-lock.json +4484 -0
  12. package/code-reviewer/package.json +45 -0
  13. package/code-reviewer/scripts/copy-assets.js +15 -0
  14. package/code-reviewer/scripts/setup.sh +130 -0
  15. package/code-reviewer/skills/brainstorming/SKILL.md +170 -0
  16. package/code-reviewer/skills/code-patrol/SKILL.md +176 -0
  17. package/code-reviewer/skills/context-resume/SKILL.md +143 -0
  18. package/code-reviewer/skills/executing-plans/SKILL.md +201 -0
  19. package/code-reviewer/skills/fix-and-learn/SKILL.md +164 -0
  20. package/code-reviewer/skills/health-check/SKILL.md +225 -0
  21. package/code-reviewer/skills/second-opinion/SKILL.md +142 -0
  22. package/code-reviewer/skills/systematic-debugging/SKILL.md +230 -0
  23. package/code-reviewer/skills/verification-before-completion/SKILL.md +170 -0
  24. package/code-reviewer/skills/writing-plans/SKILL.md +207 -0
  25. package/code-reviewer/src/__tests__/facades.test.ts +598 -0
  26. package/code-reviewer/src/activation/activate.ts +125 -0
  27. package/code-reviewer/src/activation/claude-md-content.ts +217 -0
  28. package/code-reviewer/src/activation/inject-claude-md.ts +113 -0
  29. package/code-reviewer/src/extensions/index.ts +47 -0
  30. package/code-reviewer/src/extensions/ops/example.ts +28 -0
  31. package/code-reviewer/src/identity/persona.ts +62 -0
  32. package/code-reviewer/src/index.ts +278 -0
  33. package/code-reviewer/src/intelligence/data/architecture.json +5 -0
  34. package/code-reviewer/src/intelligence/data/code-review.json +5 -0
  35. package/code-reviewer/tsconfig.json +30 -0
  36. package/code-reviewer/vitest.config.ts +23 -0
  37. package/dist/prompts/archetypes.js +35 -5
  38. package/dist/prompts/archetypes.js.map +1 -1
  39. package/dist/prompts/create-wizard.js +7 -11
  40. package/dist/prompts/create-wizard.js.map +1 -1
  41. package/dist/prompts/playbook.js +121 -25
  42. package/dist/prompts/playbook.js.map +1 -1
  43. package/package.json +1 -1
@@ -0,0 +1,598 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import {
3
+ createAgentRuntime,
4
+ createSemanticFacades,
5
+ createDomainFacade,
6
+ } from '@soleri/core';
7
+ import type { AgentRuntime, IntelligenceEntry, OpDefinition, FacadeConfig } from '@soleri/core';
8
+ import { z } from 'zod';
9
+ import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { tmpdir } from 'node:os';
12
+ import { PERSONA } from '../identity/persona.js';
13
+ import { activateAgent, deactivateAgent } from '../activation/activate.js';
14
+ import { injectClaudeMd, injectClaudeMdGlobal, hasAgentMarker } from '../activation/inject-claude-md.js';
15
+
16
+ function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntry {
17
+ return {
18
+ id: overrides.id ?? 'test-1',
19
+ type: overrides.type ?? 'pattern',
20
+ domain: overrides.domain ?? 'testing',
21
+ title: overrides.title ?? 'Test Pattern',
22
+ severity: overrides.severity ?? 'warning',
23
+ description: overrides.description ?? 'A test pattern.',
24
+ tags: overrides.tags ?? ['testing'],
25
+ };
26
+ }
27
+
28
+ describe('Facades', () => {
29
+ let runtime: AgentRuntime;
30
+ let plannerDir: string;
31
+
32
+ beforeEach(() => {
33
+ plannerDir = join(tmpdir(), 'forge-planner-test-' + Date.now());
34
+ mkdirSync(plannerDir, { recursive: true });
35
+ runtime = createAgentRuntime({
36
+ agentId: 'code-reviewer',
37
+ vaultPath: ':memory:',
38
+ plansPath: join(plannerDir, 'plans.json'),
39
+ });
40
+ });
41
+
42
+ afterEach(() => {
43
+ runtime.close();
44
+ rmSync(plannerDir, { recursive: true, force: true });
45
+ });
46
+
47
+ describe('code-reviewer_code_review', () => {
48
+ function buildDomainFacade(): FacadeConfig {
49
+ return createDomainFacade(runtime, 'code-reviewer', 'code-review');
50
+ }
51
+
52
+ it('should create facade with expected ops', () => {
53
+ const facade = buildDomainFacade();
54
+ expect(facade.name).toBe('code-reviewer_code_review');
55
+ const opNames = facade.ops.map((o) => o.name);
56
+ expect(opNames).toContain('get_patterns');
57
+ expect(opNames).toContain('search');
58
+ expect(opNames).toContain('get_entry');
59
+ expect(opNames).toContain('capture');
60
+ expect(opNames).toContain('remove');
61
+ });
62
+
63
+ it('get_patterns should return entries for code-review', async () => {
64
+ runtime.vault.seed([
65
+ makeEntry({ id: 'code-review-gp1', domain: 'code-review', tags: ['test'] }),
66
+ makeEntry({ id: 'other-gp1', domain: 'other-domain', tags: ['test'] }),
67
+ ]);
68
+ const facade = buildDomainFacade();
69
+ const op = facade.ops.find((o) => o.name === 'get_patterns')!;
70
+ const results = (await op.handler({})) as IntelligenceEntry[];
71
+ expect(results.every((e) => e.domain === 'code-review')).toBe(true);
72
+ });
73
+
74
+ it('search should scope to code-review with ranked results', async () => {
75
+ runtime.vault.seed([
76
+ makeEntry({ id: 'code-review-s1', domain: 'code-review', title: 'Domain specific pattern', tags: ['find-me'] }),
77
+ makeEntry({ id: 'other-s1', domain: 'other', title: 'Other domain pattern', tags: ['nope'] }),
78
+ ]);
79
+ runtime.brain.rebuildVocabulary();
80
+ const facade = buildDomainFacade();
81
+ const op = facade.ops.find((o) => o.name === 'search')!;
82
+ const results = (await op.handler({ query: 'pattern' })) as Array<{ entry: IntelligenceEntry; score: number }>;
83
+ expect(results.every((r) => r.entry.domain === 'code-review')).toBe(true);
84
+ });
85
+
86
+ it('capture should add entry with code-review domain', async () => {
87
+ const facade = buildDomainFacade();
88
+ const captureOp = facade.ops.find((o) => o.name === 'capture')!;
89
+ const result = (await captureOp.handler({
90
+ id: 'code-review-cap1',
91
+ type: 'pattern',
92
+ title: 'Captured Pattern',
93
+ severity: 'warning',
94
+ description: 'A captured pattern.',
95
+ tags: ['captured'],
96
+ })) as { captured: boolean; governance?: { action: string } };
97
+ expect(result.captured).toBe(true);
98
+ const entry = runtime.vault.get('code-review-cap1');
99
+ expect(entry).not.toBeNull();
100
+ expect(entry!.domain).toBe('code-review');
101
+ });
102
+
103
+ it('get_entry should return specific entry', async () => {
104
+ runtime.vault.seed([makeEntry({ id: 'code-review-ge1', domain: 'code-review', tags: ['test'] })]);
105
+ const facade = buildDomainFacade();
106
+ const op = facade.ops.find((o) => o.name === 'get_entry')!;
107
+ const result = (await op.handler({ id: 'code-review-ge1' })) as IntelligenceEntry;
108
+ expect(result.id).toBe('code-review-ge1');
109
+ });
110
+
111
+ it('remove should delete entry', async () => {
112
+ runtime.vault.seed([makeEntry({ id: 'code-review-rm1', domain: 'code-review', tags: ['test'] })]);
113
+ const facade = buildDomainFacade();
114
+ const op = facade.ops.find((o) => o.name === 'remove')!;
115
+ const result = (await op.handler({ id: 'code-review-rm1' })) as { removed: boolean };
116
+ expect(result.removed).toBe(true);
117
+ expect(runtime.vault.get('code-review-rm1')).toBeNull();
118
+ });
119
+ });
120
+
121
+ describe('code-reviewer_architecture', () => {
122
+ function buildDomainFacade(): FacadeConfig {
123
+ return createDomainFacade(runtime, 'code-reviewer', 'architecture');
124
+ }
125
+
126
+ it('should create facade with expected ops', () => {
127
+ const facade = buildDomainFacade();
128
+ expect(facade.name).toBe('code-reviewer_architecture');
129
+ const opNames = facade.ops.map((o) => o.name);
130
+ expect(opNames).toContain('get_patterns');
131
+ expect(opNames).toContain('search');
132
+ expect(opNames).toContain('get_entry');
133
+ expect(opNames).toContain('capture');
134
+ expect(opNames).toContain('remove');
135
+ });
136
+
137
+ it('get_patterns should return entries for architecture', async () => {
138
+ runtime.vault.seed([
139
+ makeEntry({ id: 'architecture-gp1', domain: 'architecture', tags: ['test'] }),
140
+ makeEntry({ id: 'other-gp1', domain: 'other-domain', tags: ['test'] }),
141
+ ]);
142
+ const facade = buildDomainFacade();
143
+ const op = facade.ops.find((o) => o.name === 'get_patterns')!;
144
+ const results = (await op.handler({})) as IntelligenceEntry[];
145
+ expect(results.every((e) => e.domain === 'architecture')).toBe(true);
146
+ });
147
+
148
+ it('search should scope to architecture with ranked results', async () => {
149
+ runtime.vault.seed([
150
+ makeEntry({ id: 'architecture-s1', domain: 'architecture', title: 'Domain specific pattern', tags: ['find-me'] }),
151
+ makeEntry({ id: 'other-s1', domain: 'other', title: 'Other domain pattern', tags: ['nope'] }),
152
+ ]);
153
+ runtime.brain.rebuildVocabulary();
154
+ const facade = buildDomainFacade();
155
+ const op = facade.ops.find((o) => o.name === 'search')!;
156
+ const results = (await op.handler({ query: 'pattern' })) as Array<{ entry: IntelligenceEntry; score: number }>;
157
+ expect(results.every((r) => r.entry.domain === 'architecture')).toBe(true);
158
+ });
159
+
160
+ it('capture should add entry with architecture domain', async () => {
161
+ const facade = buildDomainFacade();
162
+ const captureOp = facade.ops.find((o) => o.name === 'capture')!;
163
+ const result = (await captureOp.handler({
164
+ id: 'architecture-cap1',
165
+ type: 'pattern',
166
+ title: 'Captured Pattern',
167
+ severity: 'warning',
168
+ description: 'A captured pattern.',
169
+ tags: ['captured'],
170
+ })) as { captured: boolean; governance?: { action: string } };
171
+ expect(result.captured).toBe(true);
172
+ const entry = runtime.vault.get('architecture-cap1');
173
+ expect(entry).not.toBeNull();
174
+ expect(entry!.domain).toBe('architecture');
175
+ });
176
+
177
+ it('get_entry should return specific entry', async () => {
178
+ runtime.vault.seed([makeEntry({ id: 'architecture-ge1', domain: 'architecture', tags: ['test'] })]);
179
+ const facade = buildDomainFacade();
180
+ const op = facade.ops.find((o) => o.name === 'get_entry')!;
181
+ const result = (await op.handler({ id: 'architecture-ge1' })) as IntelligenceEntry;
182
+ expect(result.id).toBe('architecture-ge1');
183
+ });
184
+
185
+ it('remove should delete entry', async () => {
186
+ runtime.vault.seed([makeEntry({ id: 'architecture-rm1', domain: 'architecture', tags: ['test'] })]);
187
+ const facade = buildDomainFacade();
188
+ const op = facade.ops.find((o) => o.name === 'remove')!;
189
+ const result = (await op.handler({ id: 'architecture-rm1' })) as { removed: boolean };
190
+ expect(result.removed).toBe(true);
191
+ expect(runtime.vault.get('architecture-rm1')).toBeNull();
192
+ });
193
+ });
194
+
195
+ // ─── Semantic Facades ────────────────────────────────────────
196
+ describe('semantic facades', () => {
197
+ function buildSemanticFacades(): FacadeConfig[] {
198
+ return createSemanticFacades(runtime, 'code-reviewer');
199
+ }
200
+
201
+ it('should create 10 semantic facades', () => {
202
+ const facades = buildSemanticFacades();
203
+ expect(facades).toHaveLength(10);
204
+ const names = facades.map(f => f.name);
205
+ expect(names).toContain('code-reviewer_vault');
206
+ expect(names).toContain('code-reviewer_plan');
207
+ expect(names).toContain('code-reviewer_brain');
208
+ expect(names).toContain('code-reviewer_memory');
209
+ expect(names).toContain('code-reviewer_admin');
210
+ expect(names).toContain('code-reviewer_curator');
211
+ expect(names).toContain('code-reviewer_loop');
212
+ expect(names).toContain('code-reviewer_orchestrate');
213
+ expect(names).toContain('code-reviewer_control');
214
+ expect(names).toContain('code-reviewer_cognee');
215
+ });
216
+
217
+ it('total ops across all facades should be 209', () => {
218
+ const facades = buildSemanticFacades();
219
+ const totalOps = facades.reduce((sum, f) => sum + f.ops.length, 0);
220
+ expect(totalOps).toBe(209);
221
+ });
222
+ });
223
+
224
+ describe('code-reviewer_vault', () => {
225
+ function getFacade(): FacadeConfig {
226
+ return createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_vault')!;
227
+ }
228
+
229
+ it('should contain vault ops', () => {
230
+ const opNames = getFacade().ops.map(o => o.name);
231
+ expect(opNames).toContain('search');
232
+ expect(opNames).toContain('vault_stats');
233
+ expect(opNames).toContain('list_all');
234
+ expect(opNames).toContain('export');
235
+ expect(opNames).toContain('vault_get');
236
+ expect(opNames).toContain('vault_import');
237
+ expect(opNames).toContain('capture_knowledge');
238
+ expect(opNames).toContain('intake_ingest_book');
239
+ });
240
+
241
+ it('search should query across all domains', async () => {
242
+ runtime.vault.seed([
243
+ makeEntry({ id: 'c1', domain: 'alpha', title: 'Alpha pattern', tags: ['a'] }),
244
+ makeEntry({ id: 'c2', domain: 'beta', title: 'Beta pattern', tags: ['b'] }),
245
+ ]);
246
+ runtime = createAgentRuntime({ agentId: 'code-reviewer', vaultPath: ':memory:', plansPath: join(plannerDir, 'plans2.json') });
247
+ runtime.vault.seed([
248
+ makeEntry({ id: 'c1', domain: 'alpha', title: 'Alpha pattern', tags: ['a'] }),
249
+ makeEntry({ id: 'c2', domain: 'beta', title: 'Beta pattern', tags: ['b'] }),
250
+ ]);
251
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_vault')!;
252
+ const searchOp = facade.ops.find(o => o.name === 'search')!;
253
+ const results = (await searchOp.handler({ query: 'pattern' })) as Array<{ entry: unknown; score: number }>;
254
+ expect(Array.isArray(results)).toBe(true);
255
+ expect(results.length).toBe(2);
256
+ });
257
+
258
+ it('vault_stats should return counts', async () => {
259
+ runtime.vault.seed([
260
+ makeEntry({ id: 'vs1', domain: 'd1', tags: ['x'] }),
261
+ makeEntry({ id: 'vs2', domain: 'd2', tags: ['y'] }),
262
+ ]);
263
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_vault')!;
264
+ const statsOp = facade.ops.find(o => o.name === 'vault_stats')!;
265
+ const stats = (await statsOp.handler({})) as { totalEntries: number };
266
+ expect(stats.totalEntries).toBe(2);
267
+ });
268
+ });
269
+
270
+ describe('code-reviewer_plan', () => {
271
+ it('should contain planning ops', () => {
272
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_plan')!;
273
+ const opNames = facade.ops.map(o => o.name);
274
+ expect(opNames).toContain('create_plan');
275
+ expect(opNames).toContain('get_plan');
276
+ expect(opNames).toContain('approve_plan');
277
+ expect(opNames).toContain('plan_iterate');
278
+ expect(opNames).toContain('plan_grade');
279
+ });
280
+
281
+ it('create_plan should create a draft plan', async () => {
282
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_plan')!;
283
+ const createOp = facade.ops.find(o => o.name === 'create_plan')!;
284
+ const result = (await createOp.handler({
285
+ objective: 'Add caching',
286
+ scope: 'api layer',
287
+ tasks: [{ title: 'Add Redis', description: 'Set up Redis client' }],
288
+ })) as { created: boolean; plan: { status: string } };
289
+ expect(result.created).toBe(true);
290
+ expect(result.plan.status).toBe('draft');
291
+ });
292
+ });
293
+
294
+ describe('code-reviewer_brain', () => {
295
+ it('should contain brain ops', () => {
296
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_brain')!;
297
+ const opNames = facade.ops.map(o => o.name);
298
+ expect(opNames).toContain('brain_stats');
299
+ expect(opNames).toContain('brain_strengths');
300
+ expect(opNames).toContain('brain_build_intelligence');
301
+ expect(opNames).toContain('brain_lifecycle');
302
+ expect(opNames).toContain('brain_decay_report');
303
+ });
304
+
305
+ it('brain_stats should return intelligence stats', async () => {
306
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_brain')!;
307
+ const statsOp = facade.ops.find(o => o.name === 'brain_stats')!;
308
+ const result = (await statsOp.handler({})) as { vocabularySize: number };
309
+ expect(result.vocabularySize).toBe(0);
310
+ });
311
+ });
312
+
313
+ describe('code-reviewer_memory', () => {
314
+ it('should contain memory ops', () => {
315
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_memory')!;
316
+ const opNames = facade.ops.map(o => o.name);
317
+ expect(opNames).toContain('memory_search');
318
+ expect(opNames).toContain('memory_capture');
319
+ expect(opNames).toContain('memory_promote_to_global');
320
+ });
321
+ });
322
+
323
+ describe('code-reviewer_admin', () => {
324
+ it('should contain admin ops', () => {
325
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_admin')!;
326
+ const opNames = facade.ops.map(o => o.name);
327
+ expect(opNames).toContain('admin_health');
328
+ expect(opNames).toContain('admin_tool_list');
329
+ expect(opNames).toContain('llm_rotate');
330
+ expect(opNames).toContain('render_prompt');
331
+ });
332
+ });
333
+
334
+ describe('code-reviewer_curator', () => {
335
+ it('should contain curator ops', () => {
336
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_curator')!;
337
+ const opNames = facade.ops.map(o => o.name);
338
+ expect(opNames).toContain('curator_status');
339
+ expect(opNames).toContain('curator_health_audit');
340
+ expect(opNames).toContain('curator_hybrid_contradictions');
341
+ });
342
+
343
+ it('curator_status should return initialized', async () => {
344
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_curator')!;
345
+ const statusOp = facade.ops.find(o => o.name === 'curator_status')!;
346
+ const result = (await statusOp.handler({})) as { initialized: boolean };
347
+ expect(result.initialized).toBe(true);
348
+ });
349
+ });
350
+
351
+ describe('code-reviewer_loop', () => {
352
+ it('should contain loop ops', () => {
353
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_loop')!;
354
+ const opNames = facade.ops.map(o => o.name);
355
+ expect(opNames).toContain('loop_start');
356
+ expect(opNames).toContain('loop_iterate');
357
+ expect(opNames).toContain('loop_cancel');
358
+ });
359
+ });
360
+
361
+ describe('code-reviewer_orchestrate', () => {
362
+ it('should contain orchestrate ops', () => {
363
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_orchestrate')!;
364
+ const opNames = facade.ops.map(o => o.name);
365
+ expect(opNames).toContain('register');
366
+ expect(opNames).toContain('orchestrate_plan');
367
+ expect(opNames).toContain('project_get');
368
+ expect(opNames).toContain('playbook_list');
369
+ });
370
+ });
371
+
372
+ describe('code-reviewer_control', () => {
373
+ it('should contain control and governance ops', () => {
374
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_control')!;
375
+ const opNames = facade.ops.map(o => o.name);
376
+ expect(opNames).toContain('get_identity');
377
+ expect(opNames).toContain('route_intent');
378
+ expect(opNames).toContain('governance_policy');
379
+ expect(opNames).toContain('governance_dashboard');
380
+ });
381
+
382
+ it('governance_policy should return default policy', async () => {
383
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_control')!;
384
+ const policyOp = facade.ops.find(o => o.name === 'governance_policy')!;
385
+ const result = (await policyOp.handler({ action: 'get', projectPath: '/test' })) as {
386
+ projectPath: string;
387
+ quotas: { maxEntriesTotal: number };
388
+ };
389
+ expect(result.projectPath).toBe('/test');
390
+ expect(result.quotas.maxEntriesTotal).toBe(500);
391
+ });
392
+ });
393
+
394
+ describe('code-reviewer_cognee', () => {
395
+ it('should contain cognee ops', () => {
396
+ const facade = createSemanticFacades(runtime, 'code-reviewer').find(f => f.name === 'code-reviewer_cognee')!;
397
+ const opNames = facade.ops.map(o => o.name);
398
+ expect(opNames).toContain('cognee_status');
399
+ expect(opNames).toContain('cognee_search');
400
+ expect(opNames).toContain('cognee_sync_status');
401
+ });
402
+ });
403
+
404
+ describe('code-reviewer_core (agent-specific)', () => {
405
+ function buildAgentFacade(): FacadeConfig {
406
+ const agentOps: OpDefinition[] = [
407
+ {
408
+ name: 'health',
409
+ description: 'Health check',
410
+ auth: 'read',
411
+ handler: async () => {
412
+ const stats = runtime.vault.stats();
413
+ return {
414
+ status: 'ok',
415
+ agent: { name: PERSONA.name, role: PERSONA.role },
416
+ vault: { entries: stats.totalEntries, domains: Object.keys(stats.byDomain) },
417
+ };
418
+ },
419
+ },
420
+ {
421
+ name: 'identity',
422
+ description: 'Agent identity',
423
+ auth: 'read',
424
+ handler: async () => PERSONA,
425
+ },
426
+ {
427
+ name: 'activate',
428
+ description: 'Activate agent',
429
+ auth: 'read',
430
+ schema: z.object({
431
+ projectPath: z.string().optional().default('.'),
432
+ deactivate: z.boolean().optional(),
433
+ }),
434
+ handler: async (params) => {
435
+ if (params.deactivate) return deactivateAgent();
436
+ return activateAgent(runtime.vault, (params.projectPath as string) ?? '.', runtime.planner);
437
+ },
438
+ },
439
+ {
440
+ name: 'inject_claude_md',
441
+ description: 'Inject CLAUDE.md',
442
+ auth: 'write',
443
+ schema: z.object({
444
+ projectPath: z.string().optional().default('.'),
445
+ global: z.boolean().optional(),
446
+ }),
447
+ handler: async (params) => {
448
+ if (params.global) return injectClaudeMdGlobal();
449
+ return injectClaudeMd((params.projectPath as string) ?? '.');
450
+ },
451
+ },
452
+ {
453
+ name: 'setup',
454
+ description: 'Setup status',
455
+ auth: 'read',
456
+ schema: z.object({ projectPath: z.string().optional().default('.') }),
457
+ handler: async (params) => {
458
+ const { existsSync: exists } = await import('node:fs');
459
+ const { join: joinPath } = await import('node:path');
460
+ const { homedir } = await import('node:os');
461
+ const pp = (params.projectPath as string) ?? '.';
462
+ const projectClaudeMd = joinPath(pp, 'CLAUDE.md');
463
+ const globalClaudeMd = joinPath(homedir(), '.claude', 'CLAUDE.md');
464
+ const stats = runtime.vault.stats();
465
+ const recommendations: string[] = [];
466
+ if (!hasAgentMarker(globalClaudeMd) && !hasAgentMarker(projectClaudeMd)) {
467
+ recommendations.push('No CLAUDE.md configured');
468
+ }
469
+ if (stats.totalEntries === 0) {
470
+ recommendations.push('Vault is empty');
471
+ }
472
+ // Check hook status
473
+ const { readdirSync } = await import('node:fs');
474
+ const agentClaudeDir = joinPath(__dirname, '..', '.claude');
475
+ const globalClaudeDir = joinPath(homedir(), '.claude');
476
+ const hookStatus = { agent: [] as string[], global: [] as string[], missing: [] as string[] };
477
+ if (exists(agentClaudeDir)) {
478
+ try {
479
+ const agentHooks = readdirSync(agentClaudeDir)
480
+ .filter((f: string) => f.startsWith('hookify.') && f.endsWith('.local.md'))
481
+ .map((f: string) => f.replace('hookify.', '').replace('.local.md', ''));
482
+ hookStatus.agent = agentHooks;
483
+ for (const hook of agentHooks) {
484
+ if (exists(joinPath(globalClaudeDir, `hookify.${hook}.local.md`))) {
485
+ hookStatus.global.push(hook);
486
+ } else {
487
+ hookStatus.missing.push(hook);
488
+ }
489
+ }
490
+ } catch { /* ignore */ }
491
+ }
492
+ if (hookStatus.missing.length > 0) {
493
+ recommendations.push(`${hookStatus.missing.length} hook(s) not installed globally — run scripts/setup.sh`);
494
+ }
495
+ if (recommendations.length === 0) {
496
+ recommendations.push('Code Reviewer is fully set up and ready!');
497
+ }
498
+ return {
499
+ agent: { name: PERSONA.name, role: PERSONA.role },
500
+ claude_md: {
501
+ project: { exists: exists(projectClaudeMd), has_agent_section: hasAgentMarker(projectClaudeMd) },
502
+ global: { exists: exists(globalClaudeMd), has_agent_section: hasAgentMarker(globalClaudeMd) },
503
+ },
504
+ vault: { entries: stats.totalEntries, domains: Object.keys(stats.byDomain) },
505
+ hooks: hookStatus,
506
+ recommendations,
507
+ };
508
+ },
509
+ },
510
+ ];
511
+ return {
512
+ name: 'code-reviewer_core',
513
+ description: 'Agent-specific operations',
514
+ ops: agentOps,
515
+ };
516
+ }
517
+
518
+ it('agent ops should not appear in semantic facades', () => {
519
+ const facades = createSemanticFacades(runtime, 'code-reviewer');
520
+ const allOps = facades.flatMap(f => f.ops.map(o => o.name));
521
+ expect(allOps).not.toContain('health');
522
+ expect(allOps).not.toContain('identity');
523
+ expect(allOps).not.toContain('activate');
524
+ expect(allOps).not.toContain('inject_claude_md');
525
+ expect(allOps).not.toContain('setup');
526
+ });
527
+
528
+ it('health should return ok status', async () => {
529
+ const facade = buildAgentFacade();
530
+ const healthOp = facade.ops.find((o) => o.name === 'health')!;
531
+ const health = (await healthOp.handler({})) as { status: string };
532
+ expect(health.status).toBe('ok');
533
+ });
534
+
535
+ it('identity should return persona', async () => {
536
+ const facade = buildAgentFacade();
537
+ const identityOp = facade.ops.find((o) => o.name === 'identity')!;
538
+ const persona = (await identityOp.handler({})) as { name: string; role: string };
539
+ expect(persona.name).toBe('Code Reviewer');
540
+ expect(persona.role).toBe('Catches bugs, enforces code patterns, and reviews pull requests before merge');
541
+ });
542
+
543
+ it('activate should return persona and setup status', async () => {
544
+ const facade = buildAgentFacade();
545
+ const activateOp = facade.ops.find((o) => o.name === 'activate')!;
546
+ const result = (await activateOp.handler({ projectPath: '/tmp/nonexistent-test' })) as {
547
+ activated: boolean;
548
+ persona: { name: string; role: string };
549
+ };
550
+ expect(result.activated).toBe(true);
551
+ expect(result.persona.name).toBe('Code Reviewer');
552
+ });
553
+
554
+ it('activate with deactivate flag should return deactivation', async () => {
555
+ const facade = buildAgentFacade();
556
+ const activateOp = facade.ops.find((o) => o.name === 'activate')!;
557
+ const result = (await activateOp.handler({ deactivate: true })) as { deactivated: boolean; message: string };
558
+ expect(result.deactivated).toBe(true);
559
+ expect(result.message).toBeDefined();
560
+ });
561
+
562
+ it('inject_claude_md should create CLAUDE.md in temp dir', async () => {
563
+ const tempDir = join(tmpdir(), 'forge-inject-test-' + Date.now());
564
+ mkdirSync(tempDir, { recursive: true });
565
+ try {
566
+ const facade = buildAgentFacade();
567
+ const injectOp = facade.ops.find((o) => o.name === 'inject_claude_md')!;
568
+ const result = (await injectOp.handler({ projectPath: tempDir })) as {
569
+ injected: boolean;
570
+ path: string;
571
+ action: string;
572
+ };
573
+ expect(result.injected).toBe(true);
574
+ expect(result.action).toBe('created');
575
+ expect(existsSync(result.path)).toBe(true);
576
+ const content = readFileSync(result.path, 'utf-8');
577
+ expect(content).toContain('code-reviewer:mode');
578
+ } finally {
579
+ rmSync(tempDir, { recursive: true, force: true });
580
+ }
581
+ });
582
+
583
+ it('setup should return project and global CLAUDE.md status', async () => {
584
+ const facade = buildAgentFacade();
585
+ const setupOp = facade.ops.find((o) => o.name === 'setup')!;
586
+ const result = (await setupOp.handler({ projectPath: '/tmp/nonexistent-test' })) as {
587
+ agent: { name: string };
588
+ claude_md: { project: { exists: boolean; has_agent_section: boolean }; global: { exists: boolean; has_agent_section: boolean } };
589
+ vault: { entries: number };
590
+ hooks: { agent: string[]; global: string[]; missing: string[] };
591
+ recommendations: string[];
592
+ };
593
+ expect(result.agent.name).toBe('Code Reviewer');
594
+ expect(result.vault.entries).toBe(0);
595
+ expect(result.recommendations.length).toBeGreaterThan(0);
596
+ });
597
+ });
598
+ });