@soleri/forge 5.14.9 → 7.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 (66) hide show
  1. package/dist/agent-schema.d.ts +323 -0
  2. package/dist/agent-schema.js +151 -0
  3. package/dist/agent-schema.js.map +1 -0
  4. package/dist/compose-claude-md.d.ts +24 -0
  5. package/dist/compose-claude-md.js +197 -0
  6. package/dist/compose-claude-md.js.map +1 -0
  7. package/dist/index.js +0 -0
  8. package/dist/lib.d.ts +12 -1
  9. package/dist/lib.js +10 -1
  10. package/dist/lib.js.map +1 -1
  11. package/dist/scaffold-filetree.d.ts +22 -0
  12. package/dist/scaffold-filetree.js +349 -0
  13. package/dist/scaffold-filetree.js.map +1 -0
  14. package/dist/scaffolder.js +261 -11
  15. package/dist/scaffolder.js.map +1 -1
  16. package/dist/templates/activate.d.ts +5 -2
  17. package/dist/templates/activate.js +136 -35
  18. package/dist/templates/activate.js.map +1 -1
  19. package/dist/templates/agents-md.d.ts +10 -1
  20. package/dist/templates/agents-md.js +76 -16
  21. package/dist/templates/agents-md.js.map +1 -1
  22. package/dist/templates/claude-md-template.js +25 -4
  23. package/dist/templates/claude-md-template.js.map +1 -1
  24. package/dist/templates/entry-point.js +84 -7
  25. package/dist/templates/entry-point.js.map +1 -1
  26. package/dist/templates/inject-claude-md.js +53 -0
  27. package/dist/templates/inject-claude-md.js.map +1 -1
  28. package/dist/templates/package-json.js +4 -1
  29. package/dist/templates/package-json.js.map +1 -1
  30. package/dist/templates/readme.js +4 -3
  31. package/dist/templates/readme.js.map +1 -1
  32. package/dist/templates/setup-script.js +109 -3
  33. package/dist/templates/setup-script.js.map +1 -1
  34. package/dist/templates/shared-rules.js +54 -17
  35. package/dist/templates/shared-rules.js.map +1 -1
  36. package/dist/templates/test-facades.js +151 -6
  37. package/dist/templates/test-facades.js.map +1 -1
  38. package/dist/types.d.ts +75 -10
  39. package/dist/types.js +40 -2
  40. package/dist/types.js.map +1 -1
  41. package/dist/utils/detect-domain-packs.d.ts +25 -0
  42. package/dist/utils/detect-domain-packs.js +104 -0
  43. package/dist/utils/detect-domain-packs.js.map +1 -0
  44. package/package.json +2 -1
  45. package/src/__tests__/detect-domain-packs.test.ts +178 -0
  46. package/src/__tests__/scaffold-filetree.test.ts +243 -0
  47. package/src/__tests__/scaffolder.test.ts +5 -3
  48. package/src/agent-schema.ts +184 -0
  49. package/src/compose-claude-md.ts +252 -0
  50. package/src/lib.ts +14 -1
  51. package/src/scaffold-filetree.ts +409 -0
  52. package/src/scaffolder.ts +299 -15
  53. package/src/templates/activate.ts +137 -39
  54. package/src/templates/agents-md.ts +78 -16
  55. package/src/templates/claude-md-template.ts +29 -4
  56. package/src/templates/entry-point.ts +91 -7
  57. package/src/templates/inject-claude-md.ts +53 -0
  58. package/src/templates/package-json.ts +4 -1
  59. package/src/templates/readme.ts +4 -3
  60. package/src/templates/setup-script.ts +110 -4
  61. package/src/templates/shared-rules.ts +55 -17
  62. package/src/templates/test-facades.ts +156 -6
  63. package/src/types.ts +45 -2
  64. package/src/utils/detect-domain-packs.ts +129 -0
  65. package/tsconfig.json +0 -1
  66. package/vitest.config.ts +1 -2
@@ -28,6 +28,39 @@ const ENGINE_RULES_LINES: string[] = [
28
28
  "Shared behavioral rules for all Soleri agents. The active agent's facade table provides tool names.",
29
29
  '',
30
30
 
31
+ // ─── What is Soleri ─────────────────────────────────────
32
+ '## What is Soleri',
33
+ '<!-- soleri:what-is-soleri -->',
34
+ '',
35
+ 'You are powered by the **Soleri engine** — an intelligence framework that makes AI agents learn, remember, and improve over time. You are not a stateless chatbot. You are a knowledge-driven agent with:',
36
+ '',
37
+ "- **Vault** — your knowledge graph (Zettelkasten). Patterns, anti-patterns, principles you've learned. Grows with every session.",
38
+ '- **Brain** — pattern learning loop. Tracks what works (strengths) and recommends approaches based on past success.',
39
+ '- **Memory** — session history that persists across conversations and projects.',
40
+ '- **Planning** — structured workflow: plan → approve → execute → reconcile → capture knowledge.',
41
+ '- **Packs** — installable capability bundles (knowledge + skills + hooks). Add domains without code changes.',
42
+ '',
43
+ '### The 5-Step Rhythm',
44
+ '',
45
+ 'Every task follows this cycle — each iteration makes the next one better:',
46
+ '',
47
+ '1. **Search** — check vault for existing patterns before deciding anything',
48
+ '2. **Plan** — create a structured plan, get user approval',
49
+ '3. **Work** — execute with vault-informed decisions',
50
+ '4. **Capture** — persist what you learned (patterns, anti-patterns, decisions)',
51
+ '5. **Complete** — reconcile, capture knowledge, feed the brain',
52
+ '',
53
+ '### Growing Your Capabilities',
54
+ '',
55
+ 'You start with core capabilities (vault, brain, planning, memory). To add more:',
56
+ '',
57
+ '- **Install packs**: `soleri pack install <name>` — adds knowledge, skills, and hooks for a domain',
58
+ '- **Capture knowledge**: every pattern you capture makes you smarter for next time',
59
+ '- **Add domains**: `soleri add-domain <name>` — expands your expertise',
60
+ '',
61
+ 'When a user asks "what can you do?" — list your current domains and capabilities from your activation context, not a generic list.',
62
+ '',
63
+
31
64
  // ─── Response Integrity ─────────────────────────────────
32
65
  '## Response Integrity',
33
66
  '<!-- soleri:response-integrity -->',
@@ -48,14 +81,17 @@ const ENGINE_RULES_LINES: string[] = [
48
81
  "If in doubt, don't save. Less memory with high signal beats more memory with noise.",
49
82
  '',
50
83
 
51
- // ─── Vault-First Protocol ────────────────────────────────
52
- '## Vault as Source of Truth',
84
+ // ─── Vault-First Protocol (Zettelkasten) ─────────────────
85
+ '## Vault as Source of Truth (Zettelkasten)',
53
86
  '<!-- soleri:vault-protocol -->',
54
87
  '',
55
- '- **MANDATORY**: Consult the vault BEFORE every decision planning, design, architecture, patterns, problem-solving.',
56
- '- Lookup order: 1) VAULT (`op:search_intelligent`) → 2) MEMORY (`op:memory_search`) → 3) CODEBASE → 4) WEB/TRAINING.',
57
- '- If the vault has a pattern, follow it. If it has an anti-pattern, avoid it.',
58
- '- Persist lessons: call `op:capture_knowledge` don\'t just promise "I will remember this".',
88
+ 'The vault is a **Zettelkasten** a connected knowledge graph. Every knowledge operation follows Zettelkasten principles: atomic entries, typed links, dense connections.',
89
+ '',
90
+ '- **MANDATORY**: Consult the vault BEFORE every decision search + traverse the link graph.',
91
+ '- Lookup order: 1) VAULT search 2) VAULT traverse (follow links 2 hops) → 3) MEMORY → 4) CODEBASE → 5) WEB/TRAINING.',
92
+ "- **Search + Traverse**: Don't just search — traverse from the best result to discover connected knowledge and anti-patterns.",
93
+ '- Check `contradicts` links to know what to avoid. Check `sequences` links for ordering dependencies.',
94
+ '- Persist lessons: capture + link. An unlinked entry is incomplete.',
59
95
  '- Exceptions: runtime errors with stack traces → codebase first; user explicitly asks to search web.',
60
96
  '',
61
97
 
@@ -163,20 +199,25 @@ const ENGINE_RULES_LINES: string[] = [
163
199
  '```',
164
200
  '',
165
201
 
166
- // ─── Knowledge Capture ───────────────────────────────────
202
+ // ─── Knowledge Capture (Zettelkasten) ───────────────────
167
203
  '## Knowledge Capture',
168
204
  '<!-- soleri:knowledge-capture -->',
169
205
  '',
170
- "**MANDATORY**: Persist lessons, don't just promise them.",
206
+ "**MANDATORY**: Persist lessons, don't just promise them. **Always link after capturing.**",
171
207
  '',
172
208
  'When you learn something that should persist:',
173
209
  '1. **DON\'T** just say "I will remember this"',
174
210
  '2. **DO** call `op:capture_knowledge` to persist to vault',
175
- "3. **DO** update relevant files if it's a behavioral change",
211
+ '3. **DO** review `suggestedLinks` in the capture response',
212
+ '4. **DO** create links for relevant suggestions: `op:link_entries`',
213
+ "5. **DO** update relevant files if it's a behavioral change",
214
+ '',
215
+ 'An unlinked entry is an orphan — it adds noise, not knowledge.',
176
216
  '',
177
217
  '| Type | Op | Persists To |',
178
218
  '|------|-----|-------------|',
179
219
  '| Patterns/Anti-patterns | `op:capture_knowledge` | vault |',
220
+ '| Links between entries | `op:link_entries` | vault_links table |',
180
221
  '| Quick capture | `op:capture_quick` | vault |',
181
222
  '| Session summaries | `op:session_capture` | memory |',
182
223
  '',
@@ -277,12 +318,9 @@ const ENGINE_RULES_LINES: string[] = [
277
318
  '',
278
319
  '### Session Start Protocol',
279
320
  '',
280
- 'On EVERY new session:',
281
- '1. Register project: `op:register params:{ projectPath: "." }`',
282
- '2. Check activation response for `persistence.status`, `vault.connected`, `project.registered`.',
283
- '3. Check for plans awaiting reconciliation via `op:check_persistence`:',
284
- ' - `executing` → Remind user to call `op:plan_reconcile`.',
285
- ' - `reconciling` → Remind user to call `op:plan_complete_lifecycle`.',
321
+ 'Do NOT call tools automatically on session start — just greet the user in character.',
322
+ 'Call `op:register` only when you need project context for a task (not on every message).',
323
+ 'Call `op:activate` only when checking evolved capabilities or recovering session state.',
286
324
  '',
287
325
  '### Context Compaction',
288
326
  '',
@@ -302,7 +340,7 @@ const ENGINE_RULES_LINES: string[] = [
302
340
  '|---------|-------------|',
303
341
  '| `soleri agent status` | Health check — version, packs, vault, update availability |',
304
342
  '| `soleri agent update` | Update engine to latest compatible version (`--check` for dry run) |',
305
- '| `soleri agent refresh` | Regenerate CLAUDE.md from latest forge templates (`--dry-run` to preview) |',
343
+ '| `soleri agent refresh` | Regenerate AGENTS.md/CLAUDE.md from latest forge templates (`--dry-run` to preview) |',
306
344
  '| `soleri agent diff` | Show drift between current templates and latest engine |',
307
345
  '| `soleri doctor` | Full system health and project status check |',
308
346
  '| `soleri dev` | Run agent in development mode (stdio MCP) |',
@@ -343,7 +381,7 @@ const ENGINE_RULES_LINES: string[] = [
343
381
  '',
344
382
  '| Command | What it does |',
345
383
  '|---------|-------------|',
346
- '| `soleri install` | Register agent as MCP server (`--target claude\\|codex\\|both`) |',
384
+ '| `soleri install` | Register agent as MCP server (`--target opencode\\|claude\\|codex\\|all`) |',
347
385
  '| `soleri uninstall` | Remove agent MCP server entry |',
348
386
  '| `soleri governance --show` | Show vault governance policy |',
349
387
  '| `soleri governance --preset <name>` | Set policy preset (strict\\|moderate\\|permissive) |',
@@ -9,20 +9,29 @@ export function generateFacadesTest(config: AgentConfig): string {
9
9
  .map((d) => generateDomainDescribe(config.id, d))
10
10
  .join('\n\n');
11
11
 
12
+ const hasDomainPacks = config.domainPacks && config.domainPacks.length > 0;
13
+ const domainPackDescribes = hasDomainPacks
14
+ ? config.domainPacks!.map((ref) => generateDomainPackDescribe(config.id, ref)).join('\n\n')
15
+ : '';
16
+
12
17
  return `import { describe, it, expect, beforeEach, afterEach } from 'vitest';
13
18
  import {
14
19
  createAgentRuntime,
15
20
  createSemanticFacades,
16
21
  createDomainFacade,
22
+ createDomainFacades,${hasDomainPacks ? `\n loadDomainPacksFromConfig,` : ''}
17
23
  } from '@soleri/core';
18
24
  import type { AgentRuntime, IntelligenceEntry, OpDefinition, FacadeConfig } from '@soleri/core';
19
25
  import { z } from 'zod';
20
26
  import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
21
- import { join } from 'node:path';
27
+ import { dirname, join } from 'node:path';
28
+ import { fileURLToPath } from 'node:url';
22
29
  import { tmpdir } from 'node:os';
23
30
  import { PERSONA } from '../identity/persona.js';
24
31
  import { activateAgent, deactivateAgent } from '../activation/activate.js';
25
- import { injectClaudeMd, injectClaudeMdGlobal, hasAgentMarker } from '../activation/inject-claude-md.js';
32
+ import { injectClaudeMd, injectClaudeMdGlobal, hasAgentMarker, injectAgentsMd, injectAgentsMdGlobal, hasAgentMarkerInAgentsMd } from '../activation/inject-claude-md.js';
33
+
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
35
 
27
36
  function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntry {
28
37
  return {
@@ -56,7 +65,7 @@ describe('Facades', () => {
56
65
  });
57
66
 
58
67
  ${domainDescribes}
59
-
68
+ ${domainPackDescribes ? `\n${domainPackDescribes}\n` : ''}
60
69
  // ─── Semantic Facades ────────────────────────────────────────
61
70
  describe('semantic facades', () => {
62
71
  function buildSemanticFacades(): FacadeConfig[] {
@@ -103,6 +112,12 @@ ${domainDescribes}
103
112
  expect(opNames).toContain('vault_import');
104
113
  expect(opNames).toContain('capture_knowledge');
105
114
  expect(opNames).toContain('intake_ingest_book');
115
+ // Zettelkasten linking ops
116
+ expect(opNames).toContain('link_entries');
117
+ expect(opNames).toContain('get_links');
118
+ expect(opNames).toContain('traverse');
119
+ expect(opNames).toContain('suggest_links');
120
+ expect(opNames).toContain('get_orphans');
106
121
  });
107
122
 
108
123
  it('search should query across all domains', async () => {
@@ -300,7 +315,7 @@ ${domainDescribes}
300
315
  }),
301
316
  handler: async (params) => {
302
317
  if (params.deactivate) return deactivateAgent();
303
- return activateAgent(runtime.vault, (params.projectPath as string) ?? '.', runtime.planner);
318
+ return activateAgent(runtime, (params.projectPath as string) ?? '.');
304
319
  },
305
320
  },
306
321
  {
@@ -316,6 +331,19 @@ ${domainDescribes}
316
331
  return injectClaudeMd((params.projectPath as string) ?? '.');
317
332
  },
318
333
  },
334
+ {
335
+ name: 'inject_agents_md',
336
+ description: 'Inject AGENTS.md',
337
+ auth: 'write',
338
+ schema: z.object({
339
+ projectPath: z.string().optional().default('.'),
340
+ global: z.boolean().optional(),
341
+ }),
342
+ handler: async (params) => {
343
+ if (params.global) return injectAgentsMdGlobal();
344
+ return injectAgentsMd((params.projectPath as string) ?? '.');
345
+ },
346
+ },
319
347
  {
320
348
  name: 'setup',
321
349
  description: 'Setup status',
@@ -389,6 +417,7 @@ ${domainDescribes}
389
417
  expect(allOps).not.toContain('identity');
390
418
  expect(allOps).not.toContain('activate');
391
419
  expect(allOps).not.toContain('inject_claude_md');
420
+ expect(allOps).not.toContain('inject_agents_md');
392
421
  expect(allOps).not.toContain('setup');
393
422
  });
394
423
 
@@ -412,10 +441,10 @@ ${domainDescribes}
412
441
  const activateOp = facade.ops.find((o) => o.name === 'activate')!;
413
442
  const result = (await activateOp.handler({ projectPath: '/tmp/nonexistent-test' })) as {
414
443
  activated: boolean;
415
- persona: { name: string; role: string };
444
+ origin: { name: string; role: string };
416
445
  };
417
446
  expect(result.activated).toBe(true);
418
- expect(result.persona.name).toBe('${escapeQuotes(config.name)}');
447
+ expect(result.origin.name).toBe('${escapeQuotes(config.name)}');
419
448
  });
420
449
 
421
450
  it('activate with deactivate flag should return deactivation', async () => {
@@ -447,6 +476,27 @@ ${domainDescribes}
447
476
  }
448
477
  });
449
478
 
479
+ it('inject_agents_md should create AGENTS.md in temp dir', async () => {
480
+ const tempDir = join(tmpdir(), 'forge-inject-agents-test-' + Date.now());
481
+ mkdirSync(tempDir, { recursive: true });
482
+ try {
483
+ const facade = buildAgentFacade();
484
+ const injectOp = facade.ops.find((o) => o.name === 'inject_agents_md')!;
485
+ const result = (await injectOp.handler({ projectPath: tempDir })) as {
486
+ injected: boolean;
487
+ path: string;
488
+ action: string;
489
+ };
490
+ expect(result.injected).toBe(true);
491
+ expect(result.action).toBe('created');
492
+ expect(existsSync(result.path)).toBe(true);
493
+ const content = readFileSync(result.path, 'utf-8');
494
+ expect(content).toContain('${config.id}:mode');
495
+ } finally {
496
+ rmSync(tempDir, { recursive: true, force: true });
497
+ }
498
+ });
499
+
450
500
  it('setup should return project and global CLAUDE.md status', async () => {
451
501
  const facade = buildAgentFacade();
452
502
  const setupOp = facade.ops.find((o) => o.name === 'setup')!;
@@ -462,6 +512,73 @@ ${domainDescribes}
462
512
  expect(result.recommendations.length).toBeGreaterThan(0);
463
513
  });
464
514
  });
515
+
516
+ // ─── Capability Registry ───────────────────────────────────
517
+ describe('Capability Registry', () => {
518
+ it('should register and resolve capabilities', () => {
519
+ const { CapabilityRegistry } = require('@soleri/core');
520
+ const registry = new CapabilityRegistry();
521
+
522
+ // Register a test capability
523
+ const handlers = new Map();
524
+ handlers.set('test.hello', async () => ({ success: true, data: {}, produced: ['greeting'] }));
525
+ registry.registerPack(
526
+ 'test-pack',
527
+ [{ id: 'test.hello', description: 'Test', provides: ['greeting'], requires: [] }],
528
+ handlers,
529
+ );
530
+
531
+ expect(registry.has('test.hello')).toBe(true);
532
+ expect(registry.has('test.missing')).toBe(false);
533
+
534
+ const resolved = registry.resolve('test.hello');
535
+ expect(resolved.available).toBe(true);
536
+ });
537
+
538
+ it('should track multiple capabilities from one pack', () => {
539
+ const { CapabilityRegistry } = require('@soleri/core');
540
+ const registry = new CapabilityRegistry();
541
+
542
+ const handlers = new Map();
543
+ handlers.set('test.alpha', async () => ({ success: true, data: {}, produced: ['a'] }));
544
+ handlers.set('test.beta', async () => ({ success: true, data: {}, produced: ['b'] }));
545
+ registry.registerPack(
546
+ 'multi-pack',
547
+ [
548
+ { id: 'test.alpha', description: 'Alpha', provides: ['a'], requires: [] },
549
+ { id: 'test.beta', description: 'Beta', provides: ['b'], requires: [] },
550
+ ],
551
+ handlers,
552
+ );
553
+
554
+ expect(registry.size).toBe(2);
555
+ expect(registry.has('test.alpha')).toBe(true);
556
+ expect(registry.has('test.beta')).toBe(true);
557
+ });
558
+
559
+ it('should report missing capabilities in flow validation', () => {
560
+ const { CapabilityRegistry } = require('@soleri/core');
561
+ const registry = new CapabilityRegistry();
562
+
563
+ const handlers = new Map();
564
+ handlers.set('vault.search', async () => ({ success: true, data: {}, produced: ['results'] }));
565
+ registry.registerPack(
566
+ 'core',
567
+ [{ id: 'vault.search', description: 'Search vault', provides: ['results'], requires: [] }],
568
+ handlers,
569
+ );
570
+
571
+ const flow = {
572
+ steps: [
573
+ { needs: ['vault.search', 'color.validate'] },
574
+ ],
575
+ };
576
+ const validation = registry.validateFlow(flow);
577
+ expect(validation.valid).toBe(false);
578
+ expect(validation.available).toContain('vault.search');
579
+ expect(validation.missing).toContain('color.validate');
580
+ });
581
+ });
465
582
  });
466
583
  `;
467
584
  }
@@ -544,6 +661,39 @@ function generateDomainDescribe(agentId: string, domain: string): string {
544
661
  });`;
545
662
  }
546
663
 
664
+ function generateDomainPackDescribe(
665
+ agentId: string,
666
+ ref: { name: string; package: string; version?: string },
667
+ ): string {
668
+ return ` describe('domain pack: ${ref.name}', () => {
669
+ it('should load and validate the domain pack', async () => {
670
+ const packs = await loadDomainPacksFromConfig([${JSON.stringify(ref)}]);
671
+ expect(packs.length).toBe(1);
672
+ expect(packs[0].name).toBe('${ref.name}');
673
+ expect(packs[0].ops.length).toBeGreaterThan(0);
674
+ });
675
+
676
+ it('should register pack ops in domain facades', async () => {
677
+ const packs = await loadDomainPacksFromConfig([${JSON.stringify(ref)}]);
678
+ const facades = createDomainFacades(runtime, '${agentId}', ${JSON.stringify([ref.name])}, packs);
679
+ expect(facades.length).toBeGreaterThanOrEqual(1);
680
+ // Pack ops should be present
681
+ const allOps = facades.flatMap(f => f.ops.map(o => o.name));
682
+ expect(allOps.length).toBeGreaterThan(5); // More than standard 5
683
+ });
684
+
685
+ it('pack custom ops should be callable', async () => {
686
+ const packs = await loadDomainPacksFromConfig([${JSON.stringify(ref)}]);
687
+ const facades = createDomainFacades(runtime, '${agentId}', ${JSON.stringify([ref.name])}, packs);
688
+ const facade = facades[0];
689
+ // Test first custom op returns without error
690
+ const firstOp = facade.ops[0];
691
+ const result = await firstOp.handler({});
692
+ expect(result).toBeDefined();
693
+ });
694
+ });`;
695
+ }
696
+
547
697
  function escapeQuotes(s: string): string {
548
698
  return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
549
699
  }
package/src/types.ts CHANGED
@@ -4,9 +4,23 @@ import { z } from 'zod';
4
4
  const TONES = ['precise', 'mentor', 'pragmatic'] as const;
5
5
 
6
6
  /** Where to scaffold host/client integration setup. */
7
- export const SETUP_TARGETS = ['claude', 'codex', 'both'] as const;
7
+ export const SETUP_TARGETS = ['claude', 'codex', 'opencode', 'both', 'all'] as const;
8
8
  export type SetupTarget = (typeof SETUP_TARGETS)[number];
9
9
 
10
+ /** Available model presets for agent configuration */
11
+ export const MODEL_PRESETS = [
12
+ 'claude-code-sonnet-4',
13
+ 'claude-code-opus-4',
14
+ 'claude-code-3.7-sonnet',
15
+ 'claude-code-3.5-haiku',
16
+ 'claude-4-sonnet',
17
+ 'claude-4-opus',
18
+ 'gpt-4.1',
19
+ 'gpt-4.1-mini',
20
+ 'gemini-2.5',
21
+ 'gemini-2.5-flash',
22
+ ] as const;
23
+
10
24
  /** Agent configuration — everything needed to scaffold */
11
25
  export const AgentConfigSchema = z.object({
12
26
  /** Agent identifier (kebab-case, used for directory and package name) */
@@ -31,10 +45,39 @@ export const AgentConfigSchema = z.object({
31
45
  hookPacks: z.array(z.string()).optional(),
32
46
  /** Skills to include (if omitted, all skills are included for backward compat) */
33
47
  skills: z.array(z.string()).optional(),
48
+ /** Primary model for the coder agent */
49
+ model: z.string().optional().default('claude-code-sonnet-4'),
34
50
  /** AI client setup target: Claude Code, Codex, or both */
35
- setupTarget: z.enum(SETUP_TARGETS).optional().default('claude'),
51
+ setupTarget: z.enum(SETUP_TARGETS).optional().default('opencode'),
36
52
  /** Enable Telegram transport scaffolding. Default: false. */
37
53
  telegram: z.boolean().optional().default(false),
54
+ /** Enable Cognee vector search integration. Default: false. */
55
+ cognee: z.boolean().optional().default(false),
56
+ /** Domain packs — npm packages with custom ops, knowledge, rules, and skills. */
57
+ domainPacks: z
58
+ .array(
59
+ z.object({
60
+ name: z.string(),
61
+ package: z.string(),
62
+ version: z.string().optional(),
63
+ }),
64
+ )
65
+ .optional(),
66
+ /** Vault connections — link to existing vaults instead of importing knowledge. */
67
+ vaults: z
68
+ .array(
69
+ z.object({
70
+ /** Display name for this vault connection */
71
+ name: z.string(),
72
+ /** Absolute path to the vault SQLite database */
73
+ path: z.string(),
74
+ /** Search priority (0-1). Higher = results ranked higher. Default: 0.5 */
75
+ priority: z.number().min(0).max(1).optional().default(0.5),
76
+ }),
77
+ )
78
+ .optional(),
79
+ /** @deprecated Use vaults[] instead. Shorthand for a single shared vault at priority 0.6. */
80
+ sharedVaultPath: z.string().optional(),
38
81
  });
39
82
 
40
83
  export type AgentConfig = z.infer<typeof AgentConfigSchema>;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Auto-detect installed @soleri/domain-* packages.
3
+ * Scans node_modules for packages matching the pattern.
4
+ */
5
+
6
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+
9
+ export interface DetectedDomainPack {
10
+ /** Display name (e.g., 'design') */
11
+ name: string;
12
+ /** npm package name (e.g., '@soleri/domain-design') */
13
+ package: string;
14
+ /** Installed version from package.json */
15
+ version: string;
16
+ }
17
+
18
+ /**
19
+ * Scan node_modules/@soleri/ for domain-* packages and return refs
20
+ * suitable for merging into AgentConfig.domainPacks.
21
+ *
22
+ * Walks up from basePath to find the nearest node_modules with @soleri scope.
23
+ * For each `domain-*` directory found, reads its package.json to extract
24
+ * name and version, then does a lightweight structural check (the package
25
+ * must export a default or named 'pack' with name, version, domains, ops fields).
26
+ *
27
+ * @param basePath - Directory to start searching from (typically config.outputDir)
28
+ * @returns Array of detected domain pack references
29
+ */
30
+ export function detectInstalledDomainPacks(basePath: string): DetectedDomainPack[] {
31
+ const results: DetectedDomainPack[] = [];
32
+ const soleriScope = findSoleriScope(basePath);
33
+ if (!soleriScope) return results;
34
+
35
+ let entries: string[];
36
+ try {
37
+ entries = readdirSync(soleriScope);
38
+ } catch {
39
+ return results;
40
+ }
41
+
42
+ for (const entry of entries) {
43
+ if (!entry.startsWith('domain-')) continue;
44
+
45
+ const packDir = join(soleriScope, entry);
46
+ const pkgJsonPath = join(packDir, 'package.json');
47
+
48
+ if (!existsSync(pkgJsonPath)) continue;
49
+
50
+ try {
51
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as {
52
+ name?: string;
53
+ version?: string;
54
+ main?: string;
55
+ };
56
+
57
+ if (!pkg.name || !pkg.version) continue;
58
+
59
+ // Lightweight structural check: look for an index/main file that
60
+ // would be importable. We don't actually import it (that would
61
+ // execute arbitrary code at scaffold time), but we verify the
62
+ // package looks like a domain pack by checking package.json keywords
63
+ // or the presence of expected exports.
64
+ if (looksLikeDomainPack(packDir, pkg)) {
65
+ const shortName = entry.replace(/^domain-/, '');
66
+ results.push({
67
+ name: shortName,
68
+ package: pkg.name,
69
+ version: pkg.version,
70
+ });
71
+ }
72
+ } catch {
73
+ // Skip packages with invalid package.json
74
+ }
75
+ }
76
+
77
+ return results;
78
+ }
79
+
80
+ /**
81
+ * Walk up from basePath to find node_modules/@soleri directory.
82
+ */
83
+ function findSoleriScope(basePath: string): string | null {
84
+ let current = basePath;
85
+ const seen = new Set<string>();
86
+
87
+ while (current && !seen.has(current)) {
88
+ seen.add(current);
89
+ const candidate = join(current, 'node_modules', '@soleri');
90
+ if (existsSync(candidate)) {
91
+ return candidate;
92
+ }
93
+ const parent = join(current, '..');
94
+ if (parent === current) break;
95
+ current = parent;
96
+ }
97
+
98
+ return null;
99
+ }
100
+
101
+ /**
102
+ * Lightweight check that a package looks like a domain pack without importing it.
103
+ * Checks for:
104
+ * - A soleri-domain-pack keyword in package.json, OR
105
+ * - An entry point file that exists
106
+ */
107
+ function looksLikeDomainPack(
108
+ packDir: string,
109
+ pkg: { main?: string; keywords?: string[]; exports?: unknown },
110
+ ): boolean {
111
+ // Fast path: keyword-based detection
112
+ if (Array.isArray(pkg.keywords) && pkg.keywords.includes('soleri-domain-pack')) {
113
+ return true;
114
+ }
115
+
116
+ // Check that the package has an entry point (main or exports)
117
+ if (pkg.main) {
118
+ return existsSync(join(packDir, pkg.main));
119
+ }
120
+
121
+ // Check common entry points
122
+ for (const candidate of ['dist/index.js', 'index.js', 'dist/index.mjs']) {
123
+ if (existsSync(join(packDir, candidate))) {
124
+ return true;
125
+ }
126
+ }
127
+
128
+ return false;
129
+ }
package/tsconfig.json CHANGED
@@ -13,7 +13,6 @@
13
13
  "sourceMap": true,
14
14
  "forceConsistentCasingInFileNames": true,
15
15
  "skipLibCheck": true,
16
- "noEmitOnError": true,
17
16
  "composite": true
18
17
  },
19
18
  "include": ["src/**/*.ts"],
package/vitest.config.ts CHANGED
@@ -3,8 +3,7 @@ import { defineConfig } from 'vitest/config';
3
3
  export default defineConfig({
4
4
  test: {
5
5
  environment: 'node',
6
- pool: 'forks',
7
- poolOptions: { forks: { singleFork: true } },
6
+ pool: 'threads',
8
7
  testTimeout: 30_000,
9
8
  coverage: {
10
9
  provider: 'v8',