@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
package/src/scaffolder.ts CHANGED
@@ -9,7 +9,13 @@ import {
9
9
  import { join, dirname } from 'node:path';
10
10
  import { homedir } from 'node:os';
11
11
  import { execFileSync } from 'node:child_process';
12
- import type { AgentConfig, ScaffoldResult, ScaffoldPreview, AgentInfo } from './types.js';
12
+ import type {
13
+ AgentConfig,
14
+ SetupTarget,
15
+ ScaffoldResult,
16
+ ScaffoldPreview,
17
+ AgentInfo,
18
+ } from './types.js';
13
19
 
14
20
  import { generatePackageJson } from './templates/package-json.js';
15
21
  import { generateTsconfig } from './templates/tsconfig.js';
@@ -29,30 +35,49 @@ import { generateTelegramBot } from './templates/telegram-bot.js';
29
35
  import { generateTelegramConfig } from './templates/telegram-config.js';
30
36
  import { generateTelegramAgent } from './templates/telegram-agent.js';
31
37
  import { generateTelegramSupervisor } from './templates/telegram-supervisor.js';
38
+ import { detectInstalledDomainPacks } from './utils/detect-domain-packs.js';
32
39
 
33
- function getSetupTarget(config: AgentConfig): 'claude' | 'codex' | 'both' {
40
+ function getSetupTarget(config: AgentConfig): SetupTarget {
34
41
  return config.setupTarget ?? 'claude';
35
42
  }
36
43
 
37
44
  function includesClaudeSetup(config: AgentConfig): boolean {
38
45
  const target = getSetupTarget(config);
39
- return target === 'claude' || target === 'both';
46
+ return target === 'claude' || target === 'both' || target === 'all';
40
47
  }
41
48
 
42
49
  function includesCodexSetup(config: AgentConfig): boolean {
43
50
  const target = getSetupTarget(config);
44
- return target === 'codex' || target === 'both';
51
+ return target === 'codex' || target === 'both' || target === 'all';
52
+ }
53
+
54
+ function includesOpencodeSetup(config: AgentConfig): boolean {
55
+ const target = getSetupTarget(config);
56
+ return target === 'opencode' || target === 'all';
45
57
  }
46
58
 
47
59
  /**
48
60
  * Preview what scaffold will create without writing anything.
49
61
  */
50
62
  export function previewScaffold(config: AgentConfig): ScaffoldPreview {
63
+ // Auto-detect domain packs if not explicitly configured
64
+ if (!config.domainPacks || config.domainPacks.length === 0) {
65
+ const detected = detectInstalledDomainPacks(config.outputDir);
66
+ if (detected.length > 0) {
67
+ config = { ...config, domainPacks: detected };
68
+ }
69
+ }
70
+
51
71
  const agentDir = join(config.outputDir, config.id);
52
72
  const claudeSetup = includesClaudeSetup(config);
53
73
  const codexSetup = includesCodexSetup(config);
54
- const setupLabel =
55
- claudeSetup && codexSetup ? 'Claude Code + Codex' : claudeSetup ? 'Claude Code' : 'Codex';
74
+ const opencodeSetup = includesOpencodeSetup(config);
75
+ const setupParts = [
76
+ ...(claudeSetup ? ['Claude Code'] : []),
77
+ ...(codexSetup ? ['Codex'] : []),
78
+ ...(opencodeSetup ? ['OpenCode'] : []),
79
+ ];
80
+ const setupLabel = setupParts.join(' + ');
56
81
 
57
82
  const files = [
58
83
  { path: 'package.json', description: 'NPM package with MCP SDK, SQLite, Zod dependencies' },
@@ -113,10 +138,20 @@ export function previewScaffold(config: AgentConfig): ScaffoldPreview {
113
138
  },
114
139
  ];
115
140
 
116
- if (codexSetup) {
141
+ if (opencodeSetup) {
142
+ files.push({
143
+ path: '.opencode.json',
144
+ description: 'OpenCode MCP server config for connecting to this agent',
145
+ });
146
+ }
147
+
148
+ if (codexSetup || opencodeSetup) {
149
+ const hosts = [...(codexSetup ? ['Codex'] : []), ...(opencodeSetup ? ['OpenCode'] : [])].join(
150
+ ' + ',
151
+ );
117
152
  files.push({
118
153
  path: 'AGENTS.md',
119
- description: 'Codex project instructions and activation workflow',
154
+ description: `${hosts} project instructions and activation workflow`,
120
155
  });
121
156
  }
122
157
 
@@ -253,8 +288,21 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
253
288
  greeting: `Hello! I'm ${config.name}, your AI assistant for ${config.role}.`,
254
289
  };
255
290
  }
291
+
292
+ // Auto-detect domain packs if not explicitly configured
293
+ if (!config.domainPacks || config.domainPacks.length === 0) {
294
+ const detected = detectInstalledDomainPacks(config.outputDir);
295
+ if (detected.length > 0) {
296
+ config = { ...config, domainPacks: detected };
297
+ console.error(
298
+ `[forge] Auto-detected ${detected.length} domain pack(s): ${detected.map((d) => d.package).join(', ')}`,
299
+ );
300
+ }
301
+ }
302
+
256
303
  const claudeSetup = includesClaudeSetup(config);
257
304
  const codexSetup = includesCodexSetup(config);
305
+ const opencodeSetup = includesOpencodeSetup(config);
258
306
  const agentDir = join(config.outputDir, config.id);
259
307
  const filesCreated: string[] = [];
260
308
 
@@ -312,7 +360,36 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
312
360
  ['scripts/setup.sh', generateSetupScript(config)],
313
361
  ];
314
362
 
315
- if (codexSetup) {
363
+ if (opencodeSetup) {
364
+ projectFiles.push([
365
+ '.opencode.json',
366
+ JSON.stringify(
367
+ {
368
+ $schema: 'https://opencode.ai/config.json',
369
+ title: config.name,
370
+ tui: { theme: 'soleri' },
371
+ mcpServers: {
372
+ [config.id]: {
373
+ type: 'stdio',
374
+ command: 'node',
375
+ args: ['dist/index.js'],
376
+ },
377
+ },
378
+ agents: {
379
+ coder: { model: config.model ?? 'claude-code-sonnet-4' },
380
+ summarizer: { model: 'claude-code-3.5-haiku' },
381
+ task: { model: 'claude-code-3.5-haiku' },
382
+ title: { model: 'claude-code-3.5-haiku' },
383
+ },
384
+ contextPaths: ['AGENTS.md'],
385
+ },
386
+ null,
387
+ 2,
388
+ ),
389
+ ]);
390
+ }
391
+
392
+ if (codexSetup || opencodeSetup) {
316
393
  projectFiles.push(['AGENTS.md', generateAgentsMd(config)]);
317
394
  }
318
395
 
@@ -386,8 +463,22 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
386
463
  buildError = err instanceof Error ? err.message : String(err);
387
464
  }
388
465
 
466
+ // Install OpenCode CLI if needed and not already available
467
+ const opencodeInstallResult = opencodeSetup ? ensureOpencodeInstalled() : undefined;
468
+
469
+ // Create launcher script so typing the agent name starts OpenCode
470
+ if (opencodeSetup && buildSuccess) {
471
+ const launcherResult = createOpencodeLauncher(config.id, agentDir);
472
+ if (launcherResult.created) {
473
+ // Launcher details added to summary below
474
+ }
475
+ }
476
+
389
477
  // Register the agent as an MCP server in selected host configs (only if build succeeded)
390
- const mcpRegistrations: Array<{ host: 'Claude Code' | 'Codex'; result: RegistrationResult }> = [];
478
+ const mcpRegistrations: Array<{
479
+ host: 'Claude Code' | 'Codex' | 'OpenCode';
480
+ result: RegistrationResult;
481
+ }> = [];
391
482
  if (claudeSetup) {
392
483
  if (buildSuccess) {
393
484
  mcpRegistrations.push({
@@ -422,13 +513,30 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
422
513
  });
423
514
  }
424
515
  }
516
+ if (opencodeSetup) {
517
+ if (buildSuccess) {
518
+ mcpRegistrations.push({
519
+ host: 'OpenCode',
520
+ result: registerOpencodeMcpServer(config.id, agentDir),
521
+ });
522
+ } else {
523
+ mcpRegistrations.push({
524
+ host: 'OpenCode',
525
+ result: {
526
+ registered: false,
527
+ path: join(homedir(), '.opencode.json'),
528
+ error: 'Skipped — build failed',
529
+ },
530
+ });
531
+ }
532
+ }
425
533
 
426
534
  const summaryLines = [
427
535
  `Created ${config.name} agent at ${agentDir}`,
428
536
  `${config.domains.length + 11} facades with ${totalOps} operations`,
429
537
  `${config.domains.length} empty knowledge domains ready for capture`,
430
538
  `Intelligence layer (Brain) — TF-IDF scoring, auto-tagging, duplicate detection`,
431
- `Activation system included say "Hello, ${config.name}!" to activate`,
539
+ `Persistent identity — ${config.name} is active from the start`,
432
540
  `1 test suite — facades (vault, brain, planner, llm tests provided by @soleri/core)`,
433
541
  `${skillFiles.length} built-in skills (TDD, debugging, planning, vault, brain debrief)`,
434
542
  ];
@@ -440,6 +548,23 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
440
548
  summaryLines.push(` Run manually: cd ${agentDir} && npm install && npm run build`);
441
549
  }
442
550
 
551
+ if (opencodeInstallResult) {
552
+ if (opencodeInstallResult.installed) {
553
+ summaryLines.push(`OpenCode CLI installed (${opencodeInstallResult.method})`);
554
+ } else if (!opencodeInstallResult.alreadyPresent && opencodeInstallResult.error) {
555
+ summaryLines.push('Warning: Failed to install OpenCode CLI');
556
+ summaryLines.push(' Install manually: npm install -g opencode-ai');
557
+ }
558
+ }
559
+
560
+ // Report launcher status
561
+ if (opencodeSetup && buildSuccess) {
562
+ const launcherPath = join('/usr', 'local', 'bin', config.id);
563
+ if (existsSync(launcherPath)) {
564
+ summaryLines.push(`Launcher created: type "${config.id}" in terminal to start OpenCode`);
565
+ }
566
+ }
567
+
443
568
  if (claudeSetup && config.hookPacks?.length) {
444
569
  summaryLines.push(`${config.hookPacks.length} hook pack(s) bundled in .claude/`);
445
570
  }
@@ -463,7 +588,10 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
463
588
  if (codexSetup) {
464
589
  nextSteps.push(' Restart Codex');
465
590
  }
466
- nextSteps.push(` Say "Hello, ${config.name}!" to activate the persona`);
591
+ if (opencodeSetup) {
592
+ nextSteps.push(' Restart OpenCode');
593
+ }
594
+ nextSteps.push(` ${config.name} identity is active from the start — no activation needed`);
467
595
  summaryLines.push(...nextSteps);
468
596
 
469
597
  return {
@@ -498,7 +626,10 @@ export function listAgents(parentDir: string): AgentInfo[] {
498
626
 
499
627
  try {
500
628
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
501
- if (!pkg.name?.endsWith('-mcp')) continue;
629
+ // Accept both old format (name-mcp) and new format (name)
630
+ const hasMcpSuffix = pkg.name?.endsWith('-mcp');
631
+ const hasIntelligenceDir = existsSync(join(dir, 'src', 'intelligence', 'data'));
632
+ if (!hasMcpSuffix && !hasIntelligenceDir) continue;
502
633
 
503
634
  const dataDir = join(dir, 'src', 'intelligence', 'data');
504
635
  let domains: string[] = [];
@@ -512,7 +643,7 @@ export function listAgents(parentDir: string): AgentInfo[] {
512
643
 
513
644
  agents.push({
514
645
  id: name,
515
- name: pkg.name.replace('-mcp', ''),
646
+ name: hasMcpSuffix ? pkg.name.replace('-mcp', '') : pkg.name,
516
647
  role: pkg.description || '',
517
648
  path: dir,
518
649
  domains,
@@ -614,12 +745,165 @@ args = ["${join(agentDir, 'dist', 'index.js')}"]
614
745
  }
615
746
  }
616
747
 
748
+ /**
749
+ * Create a launcher script at /usr/local/bin/<agentId> that starts OpenCode
750
+ * in the agent's project directory. Typing the agent name in terminal → OpenCode starts.
751
+ */
752
+ function createOpencodeLauncher(
753
+ agentId: string,
754
+ agentDir: string,
755
+ ): { created: boolean; path: string; error?: string } {
756
+ const launcherPath = join('/usr', 'local', 'bin', agentId);
757
+ const script = [
758
+ '#!/usr/bin/env bash',
759
+ `# Soleri agent launcher — starts OpenCode with ${agentId} MCP agent`,
760
+ `# Set terminal title to agent name`,
761
+ `printf '\\033]0;${agentId}\\007'`,
762
+ `cd "${agentDir}" || exit 1`,
763
+ 'exec opencode "$@"',
764
+ '',
765
+ ].join('\n');
766
+
767
+ try {
768
+ writeFileSync(launcherPath, script, { mode: 0o755 });
769
+ return { created: true, path: launcherPath };
770
+ } catch {
771
+ // /usr/local/bin may need sudo — try via agent's scripts/ directory instead
772
+ const localLauncher = join(agentDir, 'scripts', agentId);
773
+ try {
774
+ writeFileSync(localLauncher, script, { mode: 0o755 });
775
+ // Try to symlink to /usr/local/bin
776
+ try {
777
+ const { symlinkSync, unlinkSync } = require('node:fs') as typeof import('node:fs');
778
+ if (existsSync(launcherPath)) unlinkSync(launcherPath);
779
+ symlinkSync(localLauncher, launcherPath);
780
+ return { created: true, path: launcherPath };
781
+ } catch {
782
+ return { created: true, path: localLauncher };
783
+ }
784
+ } catch (err) {
785
+ return {
786
+ created: false,
787
+ path: launcherPath,
788
+ error: err instanceof Error ? err.message : String(err),
789
+ };
790
+ }
791
+ }
792
+ }
793
+
794
+ /**
795
+ * Ensure OpenCode CLI is installed (Soleri fork with title branding support).
796
+ * Tries `go install` from the fork first, falls back to upstream npm package.
797
+ */
798
+ function ensureOpencodeInstalled(): {
799
+ alreadyPresent: boolean;
800
+ installed: boolean;
801
+ method?: string;
802
+ error?: string;
803
+ } {
804
+ // Check if already available
805
+ try {
806
+ execFileSync('opencode', ['--version'], { stdio: 'pipe', timeout: 10_000 });
807
+ return { alreadyPresent: true, installed: false };
808
+ } catch {
809
+ // Not installed — proceed to install
810
+ }
811
+
812
+ // Try Go install from Soleri fork (supports title branding)
813
+ try {
814
+ execFileSync('go', ['version'], { stdio: 'pipe', timeout: 5_000 });
815
+ execFileSync('go', ['install', 'github.com/adrozdenko/opencode@latest'], {
816
+ stdio: 'pipe',
817
+ timeout: 120_000,
818
+ });
819
+ return {
820
+ alreadyPresent: false,
821
+ installed: true,
822
+ method: 'go install github.com/adrozdenko/opencode@latest',
823
+ };
824
+ } catch {
825
+ // Go not available or install failed — fall back to npm
826
+ }
827
+
828
+ // Fallback: upstream npm package (no title branding)
829
+ try {
830
+ execFileSync('npm', ['install', '-g', 'opencode-ai'], {
831
+ stdio: 'pipe',
832
+ timeout: 60_000,
833
+ });
834
+ return {
835
+ alreadyPresent: false,
836
+ installed: true,
837
+ method: 'npm install -g opencode-ai (upstream — title branding requires Go)',
838
+ };
839
+ } catch (err) {
840
+ return {
841
+ alreadyPresent: false,
842
+ installed: false,
843
+ error: err instanceof Error ? err.message : String(err),
844
+ };
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Register the agent as an MCP server in ~/.opencode.json.
850
+ * Idempotent — updates existing entry if present.
851
+ */
852
+ function registerOpencodeMcpServer(agentId: string, agentDir: string): RegistrationResult {
853
+ const opencodeConfigPath = join(homedir(), '.opencode.json');
854
+
855
+ try {
856
+ let config: Record<string, unknown> = {};
857
+
858
+ if (existsSync(opencodeConfigPath)) {
859
+ // Strip single-line comments before parsing (JSONC support)
860
+ const raw = readFileSync(opencodeConfigPath, 'utf-8');
861
+ const stripped = raw.replace(/^\s*\/\/.*$/gm, '');
862
+ try {
863
+ config = JSON.parse(stripped);
864
+ } catch {
865
+ config = {};
866
+ }
867
+ }
868
+
869
+ if (!config.mcpServers || typeof config.mcpServers !== 'object') {
870
+ config.mcpServers = {};
871
+ }
872
+
873
+ const servers = config.mcpServers as Record<string, unknown>;
874
+ servers[agentId] = {
875
+ type: 'stdio',
876
+ command: 'node',
877
+ args: [join(agentDir, 'dist', 'index.js')],
878
+ };
879
+
880
+ writeFileSync(opencodeConfigPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
881
+ return { registered: true, path: opencodeConfigPath };
882
+ } catch (err) {
883
+ return {
884
+ registered: false,
885
+ path: opencodeConfigPath,
886
+ error: err instanceof Error ? err.message : String(err),
887
+ };
888
+ }
889
+ }
890
+
617
891
  function generateEmptyBundle(domain: string): string {
618
892
  return JSON.stringify(
619
893
  {
620
894
  domain,
621
895
  version: '1.0.0',
622
- entries: [],
896
+ entries: [
897
+ {
898
+ id: `${domain}-seed`,
899
+ type: 'pattern',
900
+ domain,
901
+ title: `${domain.replace(/-/g, ' ')} domain seed`,
902
+ severity: 'suggestion',
903
+ description: `Seed entry for the ${domain.replace(/-/g, ' ')} domain. Replace or remove once real knowledge is captured.`,
904
+ tags: [domain, 'seed'],
905
+ },
906
+ ],
623
907
  },
624
908
  null,
625
909
  2,
@@ -2,26 +2,15 @@ import type { AgentConfig } from '../types.js';
2
2
 
3
3
  /**
4
4
  * Generates src/activation/activate.ts for a new agent.
5
- * Provides the activate/deactivate system that returns full context
6
- * to Claudepersona, guidelines, tool recommendations, setup status.
5
+ *
6
+ * Activation is ADAPTIVE it discovers current capabilities at runtime
7
+ * (vault domains, installed packs, identity changes) rather than returning
8
+ * a frozen snapshot from scaffold time. PERSONA is the birth config;
9
+ * the activation response reflects what the agent has become.
7
10
  *
8
11
  * Uses array-joined pattern because generated code contains template literals.
9
12
  */
10
13
  export function generateActivate(config: AgentConfig): string {
11
- const toolPrefix = config.id; // keep hyphens — matches MCP tool registration
12
- const _marker = `${config.id}:mode`;
13
-
14
- // Build tool recommendations from config domains
15
- const toolRecLines: string[] = [];
16
- for (const d of config.domains) {
17
- const toolName = `${toolPrefix}_${d.replace(/-/g, '_')}`;
18
- toolRecLines.push(` { intent: 'search ${d}', facade: '${toolName}', op: 'search' },`);
19
- toolRecLines.push(
20
- ` { intent: '${d} patterns', facade: '${toolName}', op: 'get_patterns' },`,
21
- );
22
- toolRecLines.push(` { intent: 'capture ${d}', facade: '${toolName}', op: 'capture' },`);
23
- }
24
-
25
14
  // Build behavioral guidelines from config principles
26
15
  const guidelineLines = config.principles
27
16
  .map((p) => {
@@ -30,23 +19,34 @@ export function generateActivate(config: AgentConfig): string {
30
19
  })
31
20
  .join('\n');
32
21
 
22
+ // Static domain list (from scaffold time) — used as baseline
23
+ const configDomains = JSON.stringify(config.domains);
24
+
33
25
  return [
34
26
  "import { join } from 'node:path';",
27
+ "import { existsSync, readFileSync } from 'node:fs';",
35
28
  "import { homedir } from 'node:os';",
36
29
  "import { PERSONA } from '../identity/persona.js';",
37
30
  "import { hasAgentMarker, removeClaudeMdGlobal } from './inject-claude-md.js';",
38
- "import type { Vault, Planner, Plan } from '@soleri/core';",
31
+ "import type { AgentRuntime } from '@soleri/core';",
39
32
  '',
40
33
  'export interface ActivationResult {',
41
34
  ' activated: boolean;',
42
- ' persona: {',
35
+ ' origin: {',
43
36
  ' name: string;',
44
37
  ' role: string;',
45
38
  ' description: string;',
39
+ ' };',
40
+ ' current: {',
41
+ ' role: string;',
46
42
  ' greeting: string;',
43
+ ' domains: string[];',
44
+ ' capabilities: Array<{ domain: string; entries: number }>;',
45
+ ' installed_packs: Array<{ id: string; type: string }>;',
46
+ ' what_you_can_do: string[];',
47
+ ' growth_suggestions: string[];',
47
48
  ' };',
48
49
  ' guidelines: string[];',
49
- ' tool_recommendations: Array<{ intent: string; facade: string; op: string }>;',
50
50
  ' session_instruction: string;',
51
51
  ' setup_status: {',
52
52
  ' claude_md_injected: boolean;',
@@ -67,20 +67,115 @@ export function generateActivate(config: AgentConfig): string {
67
67
  '}',
68
68
  '',
69
69
  '/**',
70
- ` * Activate ${config.name} — returns full context for Claude to adopt the persona.`,
70
+ ` * Activate ${config.name} — discovers current capabilities and returns adaptive context.`,
71
+ ' *',
72
+ ' * PERSONA is the birth config. The activation response reflects what the agent',
73
+ ' * has become through installed packs, captured knowledge, and identity updates.',
71
74
  ' */',
72
- 'export function activateAgent(vault: Vault, projectPath: string, planner?: Planner): ActivationResult {',
73
- ' // Check CLAUDE.md injection status (project-level and global)',
75
+ 'export function activateAgent(runtime: AgentRuntime, projectPath: string): ActivationResult {',
76
+ ' const { vault, planner, identityManager } = runtime;',
77
+ '',
78
+ ' // ─── Setup status ──────────────────────────────────────────',
74
79
  " const projectClaudeMd = join(projectPath, 'CLAUDE.md');",
75
80
  " const globalClaudeMd = join(homedir(), '.claude', 'CLAUDE.md');",
76
81
  ' const claudeMdInjected = hasAgentMarker(projectClaudeMd);',
77
82
  ' const globalClaudeMdInjected = hasAgentMarker(globalClaudeMd);',
78
83
  '',
79
- ' // Check vault status',
84
+ ' // ─── Vault stats — what the agent actually knows ──────────',
80
85
  ' const stats = vault.stats();',
81
86
  ' const vaultHasEntries = stats.totalEntries > 0;',
82
87
  '',
83
- " // Build next steps based on what's missing",
88
+ ' // ─── Discover domains ─────────────────────────────────────',
89
+ ` const configuredDomains: string[] = ${configDomains};`,
90
+ ' const vaultDomains = Object.keys(stats.byDomain);',
91
+ '',
92
+ ' // Merge configured + vault-discovered domains (dedup)',
93
+ ' const allDomains = [...new Set([...configuredDomains, ...vaultDomains])];',
94
+ '',
95
+ ' // Build capability map — entries per domain',
96
+ ' const capabilities = allDomains.map((d) => ({',
97
+ ' domain: d,',
98
+ ' entries: stats.byDomain[d] ?? 0,',
99
+ ' }));',
100
+ '',
101
+ ' // ─── Discover installed packs ─────────────────────────────',
102
+ ' const installedPacks: Array<{ id: string; type: string }> = [];',
103
+ ' try {',
104
+ " const lockPath = join(projectPath, 'soleri.lock');",
105
+ ' if (existsSync(lockPath)) {',
106
+ " const lockData = JSON.parse(readFileSync(lockPath, 'utf-8'));",
107
+ ' if (lockData.packs) {',
108
+ ' for (const [id, entry] of Object.entries(lockData.packs)) {',
109
+ ' installedPacks.push({ id, type: (entry as Record<string, string>).type ?? "unknown" });',
110
+ ' }',
111
+ ' }',
112
+ ' }',
113
+ ' } catch {',
114
+ ' // Lock file missing or corrupt — proceed without pack info',
115
+ ' }',
116
+ '',
117
+ ' // ─── Dynamic role — based on what the agent actually covers ─',
118
+ " const currentIdentity = identityManager.getIdentity('" + config.id + "');",
119
+ ' const newDomains = allDomains.filter((d) => !configuredDomains.includes(d));',
120
+ ' let currentRole = currentIdentity?.role ?? PERSONA.role;',
121
+ '',
122
+ ' // If the agent has grown beyond its birth domains, reflect that',
123
+ ' if (newDomains.length > 0) {',
124
+ ' const formatted = newDomains.map((d) => d.replace(/-/g, " ")).join(", ");',
125
+ ' currentRole = `${PERSONA.role} (also covering ${formatted})`;',
126
+ ' }',
127
+ '',
128
+ ' // ─── Dynamic greeting ─────────────────────────────────────',
129
+ " let greeting = `Hello! I'm ${PERSONA.name}.`;",
130
+ ' if (allDomains.length > configuredDomains.length) {',
131
+ " greeting += ` I started as a ${PERSONA.role} and have expanded to also cover ${newDomains.map((d) => d.replace(/-/g, ' ')).join(', ')}.`;",
132
+ ' } else {',
133
+ ' greeting += ` ${PERSONA.role} ready to help.`;',
134
+ ' }',
135
+ ' if (stats.totalEntries > 0) {',
136
+ ' const domainSummary = capabilities',
137
+ ' .filter((c) => c.entries > 0)',
138
+ ' .map((c) => `${c.entries} ${c.domain.replace(/-/g, " ")}`)',
139
+ ' .join(", ");',
140
+ ' greeting += ` Vault: ${stats.totalEntries} entries (${domainSummary}).`;',
141
+ ' }',
142
+ '',
143
+ ' // ─── Capability self-awareness ────────────────────────────',
144
+ ' const whatYouCanDo: string[] = [',
145
+ ' "Search and traverse a connected knowledge graph (vault) before every decision",',
146
+ ' "Create structured plans with approval gates and drift reconciliation",',
147
+ ' "Learn from sessions — brain tracks pattern strengths and recommends approaches",',
148
+ ' "Remember across conversations and projects (cross-project memory)",',
149
+ ' "Capture knowledge as typed entries with Zettelkasten links",',
150
+ ' "Run iterative validation loops until quality targets are met",',
151
+ ' "Orchestrate multi-step workflows: plan → execute → capture",',
152
+ ' ];',
153
+ '',
154
+ ' // Add domain-specific capabilities',
155
+ ' for (const cap of capabilities) {',
156
+ ' if (cap.entries > 0) {',
157
+ ' whatYouCanDo.push(`${cap.domain.replace(/-/g, " ")}: ${cap.entries} patterns and knowledge entries`);',
158
+ ' }',
159
+ ' }',
160
+ '',
161
+ ' // Add pack-specific capabilities',
162
+ ' for (const pack of installedPacks) {',
163
+ ' whatYouCanDo.push(`Pack "${pack.id}" (${pack.type}) installed — provides domain-specific intelligence`);',
164
+ ' }',
165
+ '',
166
+ ' const growthSuggestions: string[] = [];',
167
+ ' if (stats.totalEntries < 10) {',
168
+ ' growthSuggestions.push("Vault has few entries — start capturing patterns to build your knowledge base");',
169
+ ' }',
170
+ ' if (installedPacks.length === 0) {',
171
+ ' growthSuggestions.push("No packs installed — try: soleri pack install <name> to add domain intelligence");',
172
+ ' growthSuggestions.push("Available starter packs: soleri pack available");',
173
+ ' }',
174
+ ' if (allDomains.length <= 1) {',
175
+ ' growthSuggestions.push("Only one domain configured — add more with: soleri add-domain <name>");',
176
+ ' }',
177
+ '',
178
+ ' // ─── Next steps ───────────────────────────────────────────',
84
179
  ' const nextSteps: string[] = [];',
85
180
  ' if (!globalClaudeMdInjected && !claudeMdInjected) {',
86
181
  ` nextSteps.push('No CLAUDE.md configured — run inject_claude_md with global: true for all projects, or without for this project only');`,
@@ -88,10 +183,7 @@ export function generateActivate(config: AgentConfig): string {
88
183
  ` nextSteps.push('Global CLAUDE.md not configured — run inject_claude_md with global: true to enable activation in all projects');`,
89
184
  ' }',
90
185
  ' if (!vaultHasEntries) {',
91
- " nextSteps.push('Vault is empty — start capturing knowledge with the domain capture ops');",
92
- ' }',
93
- ' if (nextSteps.length === 0) {',
94
- ` nextSteps.push('All set! ${config.name} is fully integrated.');`,
186
+ " nextSteps.push('Vault is empty — start capturing knowledge with the domain capture ops, or install a knowledge pack with soleri pack install');",
95
187
  ' }',
96
188
  '',
97
189
  ' // Check for executing plans',
@@ -105,25 +197,32 @@ export function generateActivate(config: AgentConfig): string {
105
197
  ' nextSteps.unshift(`${executingPlans.length} plan(s) in progress — use get_plan to review`);',
106
198
  ' }',
107
199
  '',
200
+ ' if (nextSteps.length === 0) {',
201
+ ` nextSteps.push('All set! ${config.name} is ready.');`,
202
+ ' }',
203
+ '',
108
204
  ' return {',
109
205
  ' activated: true,',
110
- ' persona: {',
206
+ ' origin: {',
111
207
  ' name: PERSONA.name,',
112
208
  ' role: PERSONA.role,',
113
209
  ' description: PERSONA.description,',
114
- ' greeting: PERSONA.greeting,',
210
+ ' },',
211
+ ' current: {',
212
+ ' role: currentRole,',
213
+ ' greeting,',
214
+ ' domains: allDomains,',
215
+ ' capabilities,',
216
+ ' installed_packs: installedPacks,',
217
+ ' what_you_can_do: whatYouCanDo,',
218
+ ' growth_suggestions: growthSuggestions,',
115
219
  ' },',
116
220
  ' guidelines: [',
117
221
  guidelineLines,
118
222
  ' ],',
119
- ' tool_recommendations: [',
120
- ` { intent: 'health check', facade: '${toolPrefix}_core', op: 'health' },`,
121
- ` { intent: 'search all', facade: '${toolPrefix}_core', op: 'search' },`,
122
- ` { intent: 'vault stats', facade: '${toolPrefix}_core', op: 'vault_stats' },`,
123
- ...toolRecLines,
124
- ' ],',
125
- ` session_instruction: 'You are now ' + PERSONA.name + ', a ' + PERSONA.role + '. Stay in character for the ENTIRE session. ' +`,
126
- ` 'Reference patterns from the knowledge vault. Provide concrete examples. Flag anti-patterns with severity.',`,
223
+ " session_instruction: `You are ${PERSONA.name}. Your origin role is ${PERSONA.role}, but you have grown — your current capabilities span: ${allDomains.join(', ')}. ` +",
224
+ " 'Adapt your expertise to match your actual knowledge. ' +",
225
+ " 'Reference patterns from the knowledge vault. Provide concrete examples. Flag anti-patterns with severity.',",
127
226
  ' setup_status: {',
128
227
  ' claude_md_injected: claudeMdInjected,',
129
228
  ' global_claude_md_injected: globalClaudeMdInjected,',
@@ -139,7 +238,6 @@ export function generateActivate(config: AgentConfig): string {
139
238
  ` * Deactivate ${config.name} — drops persona and cleans up CLAUDE.md sections.`,
140
239
  ' */',
141
240
  'export function deactivateAgent(): DeactivationResult {',
142
- ' // Remove agent sections from global CLAUDE.md on deactivation',
143
241
  ' const globalResult = removeClaudeMdGlobal();',
144
242
  ' return {',
145
243
  ' deactivated: true,',