@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
@@ -1,35 +1,97 @@
1
1
  import type { AgentConfig } from '../types.js';
2
+ import { getEngineRulesContent } from './shared-rules.js';
2
3
 
3
4
  /**
4
- * Generate AGENTS.md content for Codex sessions.
5
+ * Generate AGENTS.md content for OpenCode (primary host).
6
+ *
7
+ * This is the full instruction file — equivalent to what CLAUDE.md gets
8
+ * via claude-md-template.ts + shared-rules.ts. OpenCode reads AGENTS.md
9
+ * as its primary instruction file, so it must contain:
10
+ * 1. Persistent identity (always-on, no activation needed)
11
+ * 2. Full facade table (all 13+ semantic facades + domains)
12
+ * 3. Engine rules (vault-first, planning, output formatting, etc.)
13
+ * 4. Session start protocol
14
+ * 5. Skills reference
5
15
  */
6
16
  export function generateAgentsMd(config: AgentConfig): string {
17
+ const bt = '`';
18
+ const tp = config.id; // tool prefix
7
19
  const principles = config.principles.map((p) => `- ${p}`).join('\n');
8
20
  const domains = config.domains.map((d) => `- ${d}`).join('\n');
9
21
 
10
- return `# AGENTS.md instructions for this project
22
+ // ─── Domain facade rows ───────────────────────────────────
23
+ const domainRows = config.domains
24
+ .map((d) => {
25
+ const toolName = `${tp}_${d.replace(/-/g, '_')}`;
26
+ return `| ${bt}${toolName}${bt} | ${bt}get_patterns${bt}, ${bt}search${bt}, ${bt}capture${bt} |`;
27
+ })
28
+ .join('\n');
11
29
 
12
- ## Agent Identity
13
- - Name: ${config.name}
14
- - Role: ${config.role}
15
- - Agent MCP prefix: \`${config.id}\`
30
+ // ─── Engine rules (strip markers — AGENTS.md embeds them inline) ───
31
+ const engineRules = getEngineRulesContent()
32
+ .replace(/<!-- soleri:engine-rules -->\n?/, '')
33
+ .replace(/<!-- \/soleri:engine-rules -->\n?/, '')
34
+ .trim();
16
35
 
17
- ## Activation
18
- - Say "Hello, ${config.name}!" to activate persona behavior via \`${config.id}_core\`.
19
- - Say "Goodbye, ${config.name}!" to deactivate.
36
+ return `# ${config.name}
20
37
 
21
- ## Domains
38
+ ## Identity
39
+
40
+ You ARE **${config.name}**. ${config.role}.
41
+
42
+ ${config.description}
43
+
44
+ This identity is permanent — not activated by greeting, not deactivated by farewell.
45
+ Adopt this persona for every message. Your MCP tool prefix is ${bt}${tp}${bt}.
46
+
47
+ **Tone:** ${config.tone ?? 'pragmatic'}
48
+
49
+ **Domains:**
22
50
  ${domains}
23
51
 
24
- ## Principles
52
+ **Principles:**
25
53
  ${principles}
26
54
 
55
+ ## Adaptive Identity
56
+
57
+ ${config.name} is not a fixed-role agent. The role above is a starting point — the agent evolves as knowledge is added.
58
+
59
+ When the user asks about your capabilities or you need to check what you've learned, use ${bt}${tp}_core op:activate${bt} to discover evolved capabilities.
60
+
61
+ ## Session Start
62
+
63
+ Do NOT call any tools automatically on session start. Just greet the user in character.
64
+ Only call ${bt}${tp}_orchestrate op:register${bt} or ${bt}${tp}_core op:activate${bt} when you actually need project context or capability discovery — not on every message.
65
+
66
+ ## Essential Tools
67
+
68
+ | Facade | Key Ops |
69
+ |--------|---------|
70
+ | ${bt}${tp}_core${bt} | ${bt}health${bt}, ${bt}search${bt}, ${bt}identity${bt}, ${bt}register${bt}, ${bt}activate${bt} |
71
+ ${domainRows}
72
+ | ${bt}${tp}_vault${bt} | ${bt}search_intelligent${bt}, ${bt}capture_knowledge${bt}, ${bt}capture_quick${bt}, ${bt}search_feedback${bt} |
73
+ | ${bt}${tp}_vault${bt} (keeper) | ${bt}knowledge_audit${bt}, ${bt}knowledge_health${bt}, ${bt}knowledge_merge${bt}, ${bt}knowledge_reorganize${bt} |
74
+ | ${bt}${tp}_vault${bt} (mgmt) | ${bt}vault_get${bt}, ${bt}vault_update${bt}, ${bt}vault_remove${bt}, ${bt}vault_tags${bt}, ${bt}vault_domains${bt}, ${bt}vault_recent${bt} |
75
+ | ${bt}${tp}_curator${bt} | ${bt}curator_status${bt}, ${bt}curator_detect_duplicates${bt}, ${bt}curator_contradictions${bt}, ${bt}curator_groom_all${bt}, ${bt}curator_consolidate${bt}, ${bt}curator_health_audit${bt} |
76
+ | ${bt}${tp}_curator${bt} (advanced) | ${bt}curator_enrich${bt}, ${bt}curator_hybrid_contradictions${bt}, ${bt}curator_entry_history${bt}, ${bt}curator_queue_stats${bt} |
77
+ | ${bt}${tp}_plan${bt} | ${bt}create_plan${bt}, ${bt}approve_plan${bt}, ${bt}plan_split${bt}, ${bt}plan_reconcile${bt}, ${bt}plan_complete_lifecycle${bt} |
78
+ | ${bt}${tp}_orchestrate${bt} | ${bt}orchestrate_plan${bt}, ${bt}orchestrate_execute${bt}, ${bt}orchestrate_complete${bt} |
79
+ | ${bt}${tp}_brain${bt} | ${bt}brain_stats${bt}, ${bt}brain_feedback${bt}, ${bt}rebuild_vocabulary${bt}, ${bt}brain_strengths${bt}, ${bt}brain_recommend${bt} |
80
+ | ${bt}${tp}_memory${bt} | ${bt}memory_search${bt}, ${bt}memory_capture${bt}, ${bt}session_capture${bt} |
81
+ | ${bt}${tp}_control${bt} | ${bt}route_intent${bt}, ${bt}morph${bt}, ${bt}get_behavior_rules${bt}, ${bt}governance_dashboard${bt}, ${bt}governance_policy${bt} |
82
+ | ${bt}${tp}_loop${bt} | ${bt}loop_start${bt}, ${bt}loop_iterate${bt}, ${bt}loop_status${bt}, ${bt}loop_cancel${bt} |
83
+ | ${bt}${tp}_cognee${bt} | ${bt}cognee_search${bt}, ${bt}cognee_graph_stats${bt}, ${bt}cognee_export_status${bt} |
84
+ | ${bt}${tp}_context${bt} | ${bt}context_extract_entities${bt}, ${bt}context_retrieve_knowledge${bt}, ${bt}context_analyze${bt} |
85
+ | ${bt}${tp}_agency${bt} | ${bt}agency_enable${bt}, ${bt}agency_status${bt}, ${bt}agency_surface_patterns${bt}, ${bt}agency_warnings${bt}, ${bt}agency_clarify${bt} |
86
+ | ${bt}${tp}_admin${bt} | ${bt}admin_health${bt}, ${bt}admin_tool_list${bt}, ${bt}admin_diagnostic${bt} |
87
+
88
+ > Full list: ${bt}${tp}_admin op:admin_tool_list${bt}
89
+
27
90
  ## Skills
28
- - Local skills live in \`skills/<skill>/SKILL.md\`.
29
- - If a user explicitly names a skill, open that \`SKILL.md\` and follow it for that turn.
30
91
 
31
- ## Setup Notes
32
- - This repository was scaffolded with Codex support.
33
- - Session model/reasoning is selected at session start and does not auto-switch per prompt.
92
+ - Local skills live in ${bt}skills/<skill>/SKILL.md${bt}.
93
+ - If a user explicitly names a skill, open that ${bt}SKILL.md${bt} and follow it for that turn.
94
+
95
+ ${engineRules}
34
96
  `;
35
97
  }
@@ -29,9 +29,10 @@ export function generateClaudeMdTemplate(config: AgentConfig): string {
29
29
  // ─── Identity ──────────────────────────────────────────
30
30
  `## ${config.name}`,
31
31
  '',
32
- `**Role:** ${config.role}`,
33
- `**Domains:** ${config.domains.join(', ')}`,
32
+ `**Origin role:** ${config.role}`,
33
+ `**Initial domains:** ${config.domains.join(', ')}`,
34
34
  `**Tone:** ${config.tone ?? 'pragmatic'}`,
35
+ ...(config.sharedVaultPath ? [`**Shared vault:** \`${config.sharedVaultPath}\``] : []),
35
36
  '',
36
37
  config.description,
37
38
  '',
@@ -39,19 +40,33 @@ export function generateClaudeMdTemplate(config: AgentConfig): string {
39
40
  ...config.principles.map((p) => `- ${p}`),
40
41
  '',
41
42
 
43
+ // ─── Adaptive Identity ──────────────────────────────────
44
+ '## Adaptive Identity',
45
+ '',
46
+ `${config.name} is not a fixed-role agent. The origin role above is a starting point — the agent evolves as knowledge is added.`,
47
+ '',
48
+ '**On activation**, the agent discovers its current capabilities by checking:',
49
+ '- Vault domains (what knowledge actually exists)',
50
+ '- Installed packs (what was added after creation)',
51
+ '- Identity updates (any role changes via `op:update_identity`)',
52
+ '',
53
+ '**Use the `current` field** from the activation response — not `origin` — to determine how to present yourself.',
54
+ 'If you have grown beyond your origin role, say so. Your greeting and expertise should reflect what you actually know.',
55
+ '',
56
+
42
57
  // ─── Activation ────────────────────────────────────────
43
58
  '## Activation',
44
59
  '',
45
60
  `**Activate:** "Hello, ${config.name}!" → ${bt}${toolPrefix}_core op:activate params:{ projectPath: "." }${bt}`,
46
61
  `**Deactivate:** "Goodbye, ${config.name}!" → ${bt}${toolPrefix}_core op:activate params:{ deactivate: true }${bt}`,
47
62
  '',
48
- 'On activation, adopt the returned persona. Stay in character until deactivated.',
63
+ 'On activation, read the `current` field to discover your evolved role, then adopt that persona for the session.',
49
64
  '',
50
65
 
51
66
  // ─── Session Start ─────────────────────────────────────
52
67
  '## Session Start',
53
68
  '',
54
- `On every new session: ${bt}${toolPrefix}_core op:register params:{ projectPath: "." }${bt}`,
69
+ `On every new session: ${bt}${toolPrefix}_orchestrate op:register params:{ projectPath: "." }${bt}`,
55
70
  '',
56
71
  ];
57
72
 
@@ -73,6 +88,16 @@ export function generateClaudeMdTemplate(config: AgentConfig): string {
73
88
  );
74
89
  }
75
90
 
91
+ // Domain pack facades (if any)
92
+ if (config.domainPacks?.length) {
93
+ mdLines.push('', '**Domain Pack Facades:**', '');
94
+ for (const ref of config.domainPacks) {
95
+ mdLines.push(
96
+ `| ${bt}${ref.name}${bt} (pack: ${bt}${ref.package}${bt}) | *custom ops — see ${bt}admin_tool_list${bt}* |`,
97
+ );
98
+ }
99
+ }
100
+
76
101
  // Engine facades — use actual tool names (standalone facades, NOT _core sub-groups)
77
102
  mdLines.push(
78
103
  // Vault — knowledge lifecycle, capture, search, management
@@ -1,5 +1,21 @@
1
1
  import type { AgentConfig } from '../types.js';
2
2
 
3
+ /** Generate vault connection code for linked vaults. */
4
+ function generateVaultConnections(config: AgentConfig): string {
5
+ if (!config.vaults?.length) return '';
6
+ const lines = ['', ' // ─── Linked vaults ──────────────────────────────────────────────'];
7
+ for (const v of config.vaults) {
8
+ lines.push(
9
+ ` try { runtime.vaultManager.connect('${v.name}', '${v.path}', ${v.priority ?? 0.5}); } catch { /* already connected or unavailable */ }`,
10
+ );
11
+ }
12
+ lines.push(
13
+ ` console.error(\`[\${tag}] Connected ${config.vaults.length} linked vault(s): ${config.vaults.map((v) => v.name).join(', ')}\`);`,
14
+ '',
15
+ );
16
+ return lines.join('\n');
17
+ }
18
+
3
19
  /**
4
20
  * Generate the main index.ts entry point for the agent.
5
21
  *
@@ -9,6 +25,7 @@ import type { AgentConfig } from '../types.js';
9
25
  */
10
26
  export function generateEntryPoint(config: AgentConfig): string {
11
27
  const domainsLiteral = JSON.stringify(config.domains);
28
+ const hasDomainPacks = config.domainPacks && config.domainPacks.length > 0;
12
29
 
13
30
  return `#!/usr/bin/env node
14
31
 
@@ -24,12 +41,14 @@ import {
24
41
  registerAllFacades,
25
42
  seedDefaultPlaybooks,
26
43
  wrapWithMiddleware,
44
+ CapabilityRegistry,
45
+ loadAllFlows,
27
46
  } from '@soleri/core';
28
- import type { OpDefinition, AgentExtensions } from '@soleri/core';
47
+ import type { OpDefinition, AgentExtensions } from '@soleri/core';${hasDomainPacks ? `\nimport { loadDomainPacksFromConfig, createPackRuntime } from '@soleri/core';` : ''}
29
48
  import { z } from 'zod';
30
49
  import { PERSONA, getPersonaPrompt } from './identity/persona.js';
31
50
  import { activateAgent, deactivateAgent } from './activation/activate.js';
32
- import { injectClaudeMd, injectClaudeMdGlobal, hasAgentMarker } from './activation/inject-claude-md.js';
51
+ import { injectClaudeMd, injectClaudeMdGlobal, hasAgentMarker, injectAgentsMd, injectAgentsMdGlobal, hasAgentMarkerInAgentsMd } from './activation/inject-claude-md.js';
33
52
 
34
53
  const __dirname = dirname(fileURLToPath(import.meta.url));
35
54
 
@@ -37,11 +56,11 @@ async function main(): Promise<void> {
37
56
  // ─── Runtime — vault, brain, planner, curator, LLM, key pools ───
38
57
  const runtime = createAgentRuntime({
39
58
  agentId: '${config.id}',
40
- dataDir: join(__dirname, 'intelligence', 'data'),
59
+ dataDir: join(__dirname, 'intelligence', 'data'),${config.sharedVaultPath ? `\n sharedVaultPath: '${config.sharedVaultPath}',` : ''}${config.cognee ? `\n cognee: true,` : ''}
41
60
  });
42
61
 
43
62
  const tag = PERSONA.name.toLowerCase();
44
-
63
+ ${generateVaultConnections(config)}
45
64
  // Seed built-in playbooks (idempotent)
46
65
  const seedResult = seedDefaultPlaybooks(runtime.vault);
47
66
  if (seedResult.seeded > 0) {
@@ -102,7 +121,7 @@ async function main(): Promise<void> {
102
121
  changeReason: 'Initial identity seeded from PERSONA',
103
122
  });
104
123
  }
105
- return activateAgent(runtime.vault, (params.projectPath as string) ?? '.', runtime.planner);
124
+ return activateAgent(runtime, (params.projectPath as string) ?? '.');
106
125
  },
107
126
  },
108
127
  {
@@ -120,9 +139,24 @@ async function main(): Promise<void> {
120
139
  return injectClaudeMd((params.projectPath as string) ?? '.');
121
140
  },
122
141
  },
142
+ {
143
+ name: 'inject_agents_md',
144
+ description: 'Inject agent sections into AGENTS.md — project-level or global (~/.config/opencode/AGENTS.md). For OpenCode and Codex. Idempotent.',
145
+ auth: 'write',
146
+ schema: z.object({
147
+ projectPath: z.string().optional().default('.'),
148
+ global: z.boolean().optional().describe('If true, inject into ~/.config/opencode/AGENTS.md instead of project-level'),
149
+ }),
150
+ handler: async (params) => {
151
+ if (params.global) {
152
+ return injectAgentsMdGlobal();
153
+ }
154
+ return injectAgentsMd((params.projectPath as string) ?? '.');
155
+ },
156
+ },
123
157
  {
124
158
  name: 'setup',
125
- description: 'Check setup status — CLAUDE.md configured? Vault has entries? What to do next?',
159
+ description: 'Check setup status — CLAUDE.md configured? AGENTS.md configured? Vault has entries? What to do next?',
126
160
  auth: 'read',
127
161
  schema: z.object({
128
162
  projectPath: z.string().optional().default('.'),
@@ -135,12 +169,19 @@ async function main(): Promise<void> {
135
169
 
136
170
  const projectClaudeMd = joinPath(projectPath, 'CLAUDE.md');
137
171
  const globalClaudeMd = joinPath(homedir(), '.claude', 'CLAUDE.md');
172
+ const projectAgentsMd = joinPath(projectPath, 'AGENTS.md');
173
+ const globalAgentsMd = joinPath(homedir(), '.config', 'opencode', 'AGENTS.md');
138
174
 
139
175
  const projectExists = existsSync(projectClaudeMd);
140
176
  const projectHasAgent = hasAgentMarker(projectClaudeMd);
141
177
  const globalExists = existsSync(globalClaudeMd);
142
178
  const globalHasAgent = hasAgentMarker(globalClaudeMd);
143
179
 
180
+ const agentsMdProjectExists = existsSync(projectAgentsMd);
181
+ const agentsMdProjectHasAgent = hasAgentMarkerInAgentsMd(projectAgentsMd);
182
+ const agentsMdGlobalExists = existsSync(globalAgentsMd);
183
+ const agentsMdGlobalHasAgent = hasAgentMarkerInAgentsMd(globalAgentsMd);
184
+
144
185
  const s = runtime.vault.stats();
145
186
 
146
187
  const recommendations: string[] = [];
@@ -149,6 +190,9 @@ async function main(): Promise<void> {
149
190
  } else if (!globalHasAgent) {
150
191
  recommendations.push('Global ~/.claude/CLAUDE.md not configured — run inject_claude_md with global: true to enable in all projects');
151
192
  }
193
+ if (!agentsMdGlobalHasAgent && !agentsMdProjectHasAgent) {
194
+ recommendations.push('No AGENTS.md configured — run inject_agents_md for OpenCode/Codex support');
195
+ }
152
196
  if (s.totalEntries === 0) {
153
197
  recommendations.push('Vault is empty — add intelligence data or capture knowledge via domain facades');
154
198
  }
@@ -193,6 +237,10 @@ async function main(): Promise<void> {
193
237
  project: { exists: projectExists, has_agent_section: projectHasAgent },
194
238
  global: { exists: globalExists, has_agent_section: globalHasAgent },
195
239
  },
240
+ agents_md: {
241
+ project: { exists: agentsMdProjectExists, has_agent_section: agentsMdProjectHasAgent },
242
+ global: { exists: agentsMdGlobalExists, has_agent_section: agentsMdGlobalHasAgent },
243
+ },
196
244
  vault: { entries: s.totalEntries, domains: Object.keys(s.byDomain) },
197
245
  hooks: hookStatus,
198
246
  recommendations,
@@ -209,7 +257,43 @@ async function main(): Promise<void> {
209
257
  ops: agentOps,
210
258
  };
211
259
 
212
- const domainFacades = createDomainFacades(runtime, '${config.id}', ${domainsLiteral});
260
+ ${
261
+ hasDomainPacks
262
+ ? ` // ─── Domain packs ─────────────────────────────────────────────
263
+ const domainPacks = await loadDomainPacksFromConfig(${JSON.stringify(config.domainPacks)});
264
+ console.error(\`[\${tag}] Loaded \${domainPacks.length} domain packs\`);
265
+ for (const pack of domainPacks) {
266
+ if (pack.onActivate) await pack.onActivate(runtime);
267
+ }
268
+
269
+ // ─── Capability Registry ─────────────────────────────────────
270
+ const capabilityRegistry = new CapabilityRegistry();
271
+ const packRuntime = createPackRuntime(runtime);
272
+
273
+ // Register domain pack capabilities
274
+ for (const pack of domainPacks) {
275
+ if (pack.capabilities) {
276
+ const handlers = pack.capabilities(packRuntime);
277
+ // Use pack manifest capabilities if available, otherwise derive from handler keys
278
+ const definitions = (pack as Record<string, unknown>).manifest
279
+ ? ((pack as Record<string, unknown>).manifest as { capabilities?: Array<{ id: string; description: string; provides: string[]; requires: string[] }> }).capabilities ?? []
280
+ : [...handlers.keys()].map(id => ({ id, description: id, provides: [], requires: [] }));
281
+ capabilityRegistry.registerPack(pack.name, definitions, handlers, 50);
282
+ }
283
+ }
284
+
285
+ // Validate flows against installed capabilities
286
+ const flows = loadAllFlows();
287
+ for (const flow of flows) {
288
+ const validation = capabilityRegistry.validateFlow(flow);
289
+ if (validation.missing.length > 0) {
290
+ console.error(\`[\${tag}] Flow \${flow.id}: \${validation.missing.length} capabilities degraded (\${validation.missing.join(', ')})\`);
291
+ }
292
+ }
293
+ console.error(\`[\${tag}] Capability registry: \${capabilityRegistry.size} capabilities from \${capabilityRegistry.packCount} pack(s)\`);
294
+ `
295
+ : ''
296
+ } const domainFacades = createDomainFacades(runtime, '${config.id}', ${domainsLiteral}${hasDomainPacks ? `, domainPacks` : ''});
213
297
 
214
298
  // ─── User extensions (auto-discovered from src/extensions/) ────
215
299
  let extensions: AgentExtensions = {};
@@ -44,6 +44,10 @@ export function generateInjectClaudeMd(config: AgentConfig): string {
44
44
  ' if (existing.includes(startMarker)) {',
45
45
  ' const startIdx = existing.indexOf(startMarker);',
46
46
  ' const endIdx = existing.indexOf(endMarker);',
47
+ ' if (endIdx !== -1) {',
48
+ ' const currentBlock = existing.slice(startIdx, endIdx + endMarker.length);',
49
+ " if (currentBlock === content.trim()) return 'skipped';",
50
+ ' }',
47
51
  ' if (endIdx === -1) {',
48
52
  " const updated = existing.slice(0, startIdx) + content + '\\n' + existing.slice(startIdx + startMarker.length);",
49
53
  " writeFileSync(filePath, updated, 'utf-8');",
@@ -166,5 +170,54 @@ export function generateInjectClaudeMd(config: AgentConfig): string {
166
170
  " const content = readFileSync(filePath, 'utf-8');",
167
171
  " return content.includes('<!-- ' + getEngineRulesMarker() + ' -->');",
168
172
  '}',
173
+ '',
174
+ '// ─── AGENTS.md support (OpenCode, Codex) ──────────────────────',
175
+ '',
176
+ '/**',
177
+ ' * Inject agent sections into a project AGENTS.md.',
178
+ ' * Same engine rules + agent block as CLAUDE.md, targeting AGENTS.md instead.',
179
+ ' * OpenCode reads AGENTS.md as its primary instruction file.',
180
+ ' */',
181
+ 'export function injectAgentsMd(projectPath: string): InjectResult {',
182
+ " return injectIntoFile(join(projectPath, 'AGENTS.md'));",
183
+ '}',
184
+ '',
185
+ '/**',
186
+ ' * Inject into the global ~/.config/opencode/AGENTS.md.',
187
+ " * Creates ~/.config/opencode/ directory if it doesn't exist.",
188
+ ' * This makes the activation phrase work in any OpenCode project.',
189
+ ' */',
190
+ 'export function injectAgentsMdGlobal(): InjectResult {',
191
+ " const opencodeDir = join(homedir(), '.config', 'opencode');",
192
+ ' if (!existsSync(opencodeDir)) {',
193
+ ' mkdirSync(opencodeDir, { recursive: true });',
194
+ ' }',
195
+ " return injectIntoFile(join(opencodeDir, 'AGENTS.md'));",
196
+ '}',
197
+ '',
198
+ '/**',
199
+ ' * Remove agent section from a project AGENTS.md.',
200
+ ' */',
201
+ 'export function removeAgentsMd(projectPath: string): { removed: boolean; path: string } {',
202
+ " const filePath = join(projectPath, 'AGENTS.md');",
203
+ ' return { removed: removeBlock(filePath, getClaudeMdMarker()), path: filePath };',
204
+ '}',
205
+ '',
206
+ '/**',
207
+ ' * Remove agent section from the global ~/.config/opencode/AGENTS.md.',
208
+ ' */',
209
+ 'export function removeAgentsMdGlobal(): { removed: boolean; path: string } {',
210
+ " const filePath = join(homedir(), '.config', 'opencode', 'AGENTS.md');",
211
+ ' return { removed: removeBlock(filePath, getClaudeMdMarker()), path: filePath };',
212
+ '}',
213
+ '',
214
+ '/**',
215
+ ' * Check if the agent marker exists in an AGENTS.md file.',
216
+ ' */',
217
+ 'export function hasAgentMarkerInAgentsMd(filePath: string): boolean {',
218
+ ' if (!existsSync(filePath)) return false;',
219
+ " const content = readFileSync(filePath, 'utf-8');",
220
+ ` return content.includes('<!-- ${marker} -->');`,
221
+ '}',
169
222
  ].join('\n');
170
223
  }
@@ -2,7 +2,7 @@ import type { AgentConfig } from '../types.js';
2
2
 
3
3
  export function generatePackageJson(config: AgentConfig): string {
4
4
  const pkg = {
5
- name: `${config.id}-mcp`,
5
+ name: config.id,
6
6
  version: '1.0.0',
7
7
  description: config.description,
8
8
  type: 'module',
@@ -31,6 +31,9 @@ export function generatePackageJson(config: AgentConfig): string {
31
31
  '@soleri/core': '^2.0.0',
32
32
  zod: '^3.24.2',
33
33
  ...(config.telegram ? { grammy: '^1.35.0' } : {}),
34
+ ...(config.domainPacks?.length
35
+ ? Object.fromEntries(config.domainPacks.map((pack) => [pack.package, pack.version ?? '*']))
36
+ : {}),
34
37
  },
35
38
  optionalDependencies: {
36
39
  '@anthropic-ai/sdk': '^0.39.0',
@@ -4,6 +4,7 @@ import type { AgentConfig } from '../types.js';
4
4
  * Generate a README.md for the scaffolded agent.
5
5
  */
6
6
  export function generateReadme(config: AgentConfig): string {
7
+ const skillCount = config.skills?.length ?? 17;
7
8
  const setupTarget = config.setupTarget ?? 'claude';
8
9
  const claudeSetup = setupTarget === 'claude' || setupTarget === 'both';
9
10
  const codexSetup = setupTarget === 'codex' || setupTarget === 'both';
@@ -78,10 +79,10 @@ export function generateReadme(config: AgentConfig): string {
78
79
 
79
80
  const skillsLead =
80
81
  claudeSetup && codexSetup
81
- ? `${config.name} ships with 17 structured workflow skills. In Claude Code they are invocable via \`/<skill-name>\`; in Codex they are available via generated AGENTS.md + local skill files.`
82
+ ? `${config.name} ships with ${skillCount} structured workflow skills. In Claude Code they are invocable via \`/<skill-name>\`; in Codex they are available via generated AGENTS.md + local skill files.`
82
83
  : claudeSetup
83
- ? `${config.name} ships with 17 structured workflow skills, invocable via \`/<skill-name>\` in Claude Code:`
84
- : `${config.name} ships with 17 structured workflow skills, available via generated AGENTS.md + local skill files in Codex:`;
84
+ ? `${config.name} ships with ${skillCount} structured workflow skills, invocable via \`/<skill-name>\` in Claude Code:`
85
+ : `${config.name} ships with ${skillCount} structured workflow skills, available via generated AGENTS.md + local skill files in Codex:`;
85
86
 
86
87
  const skillsInstallNote =
87
88
  claudeSetup && codexSetup
@@ -6,10 +6,15 @@ import type { AgentConfig } from '../types.js';
6
6
  */
7
7
  export function generateSetupScript(config: AgentConfig): string {
8
8
  const setupTarget = config.setupTarget ?? 'claude';
9
- const claudeSetup = setupTarget === 'claude' || setupTarget === 'both';
10
- const codexSetup = setupTarget === 'codex' || setupTarget === 'both';
11
- const hostLabel =
12
- claudeSetup && codexSetup ? 'Claude Code + Codex' : claudeSetup ? 'Claude Code' : 'Codex';
9
+ const claudeSetup = setupTarget === 'claude' || setupTarget === 'both' || setupTarget === 'all';
10
+ const codexSetup = setupTarget === 'codex' || setupTarget === 'both' || setupTarget === 'all';
11
+ const opencodeSetup = setupTarget === 'opencode' || setupTarget === 'all';
12
+ const hostParts = [
13
+ ...(claudeSetup ? ['Claude Code'] : []),
14
+ ...(codexSetup ? ['Codex'] : []),
15
+ ...(opencodeSetup ? ['OpenCode'] : []),
16
+ ];
17
+ const hostLabel = hostParts.join(' + ');
13
18
 
14
19
  const claudeSection = claudeSetup
15
20
  ? `
@@ -195,6 +200,103 @@ if [ -d "$SKILLS_DIR" ]; then
195
200
  done
196
201
  echo "[ok] Codex skills: $skill_installed installed, $skill_skipped already present"
197
202
  fi
203
+ `
204
+ : '';
205
+
206
+ const opencodeSection = opencodeSetup
207
+ ? `
208
+ # Check and install OpenCode (Soleri fork with title branding)
209
+ if ! command -v opencode &>/dev/null; then
210
+ echo ""
211
+ INSTALLED=false
212
+ # Try Go install from Soleri fork (supports title branding)
213
+ if command -v go &>/dev/null; then
214
+ echo "Installing OpenCode (Soleri fork) via go install..."
215
+ if go install github.com/adrozdenko/opencode@latest 2>/dev/null; then
216
+ if command -v opencode &>/dev/null; then
217
+ echo "[ok] OpenCode installed from Soleri fork ($(opencode --version 2>/dev/null || echo 'installed'))"
218
+ INSTALLED=true
219
+ fi
220
+ fi
221
+ fi
222
+ # Fallback: upstream npm package (no title branding)
223
+ if [ "$INSTALLED" = false ]; then
224
+ echo "Installing OpenCode via npm (upstream — title branding requires Go)..."
225
+ npm install -g opencode-ai
226
+ if command -v opencode &>/dev/null; then
227
+ echo "[ok] OpenCode installed ($(opencode --version 2>/dev/null || echo 'unknown version'))"
228
+ else
229
+ echo ""
230
+ echo "Warning: Could not install OpenCode automatically."
231
+ echo "Install manually using one of:"
232
+ echo " go install github.com/adrozdenko/opencode@latest (recommended — includes title branding)"
233
+ echo " npm install -g opencode-ai (upstream)"
234
+ echo ""
235
+ fi
236
+ fi
237
+ else
238
+ echo "[ok] OpenCode found ($(opencode --version 2>/dev/null || echo 'installed'))"
239
+ fi
240
+
241
+ # Register MCP server with OpenCode
242
+ echo ""
243
+ echo "Registering ${config.name} with OpenCode..."
244
+ OPENCODE_CONFIG="$HOME/.opencode.json"
245
+ AGENT_DIST="$AGENT_DIR/dist/index.js"
246
+
247
+ OPENCODE_CONFIG="$OPENCODE_CONFIG" AGENT_NAME="$AGENT_NAME" AGENT_DIST="$AGENT_DIST" node <<'NODE'
248
+ const fs = require('node:fs');
249
+ const path = process.env.OPENCODE_CONFIG;
250
+ const agentName = process.env.AGENT_NAME;
251
+ const distPath = process.env.AGENT_DIST;
252
+
253
+ let config = {};
254
+ if (fs.existsSync(path)) {
255
+ try {
256
+ const raw = fs.readFileSync(path, 'utf-8');
257
+ const stripped = raw.replace(/^\\s*\\/\\/.*$/gm, '');
258
+ config = JSON.parse(stripped);
259
+ } catch {
260
+ config = {};
261
+ }
262
+ }
263
+
264
+ if (!config.mcp || typeof config.mcp !== 'object') {
265
+ config.mcp = {};
266
+ }
267
+
268
+ config.mcp[agentName] = {
269
+ type: 'local',
270
+ command: ['node', distPath],
271
+ enabled: true,
272
+ };
273
+
274
+ fs.writeFileSync(path, JSON.stringify(config, null, 2) + '\\n', 'utf-8');
275
+ NODE
276
+ echo "[ok] Registered ${config.name} as MCP server (OpenCode)"
277
+
278
+ # Create launcher script — type "${config.id}" to start OpenCode
279
+ LAUNCHER_PATH="/usr/local/bin/$AGENT_NAME"
280
+ LAUNCHER_CONTENT="#!/usr/bin/env bash
281
+ # Soleri agent launcher — starts OpenCode with $AGENT_NAME MCP agent
282
+ cd \\"$AGENT_DIR\\" || exit 1
283
+ exec opencode \\"\\$@\\""
284
+
285
+ if [ -w "/usr/local/bin" ]; then
286
+ echo "$LAUNCHER_CONTENT" > "$LAUNCHER_PATH"
287
+ chmod +x "$LAUNCHER_PATH"
288
+ echo "[ok] Launcher created: type \\"${config.id}\\" to start OpenCode"
289
+ else
290
+ echo "$LAUNCHER_CONTENT" > "$AGENT_DIR/scripts/$AGENT_NAME"
291
+ chmod +x "$AGENT_DIR/scripts/$AGENT_NAME"
292
+ if command -v sudo &>/dev/null; then
293
+ sudo ln -sf "$AGENT_DIR/scripts/$AGENT_NAME" "$LAUNCHER_PATH" 2>/dev/null && \\
294
+ echo "[ok] Launcher created: type \\"${config.id}\\" to start OpenCode" || \\
295
+ echo "Note: Run 'sudo ln -sf $AGENT_DIR/scripts/$AGENT_NAME $LAUNCHER_PATH' to enable \\"${config.id}\\" command"
296
+ else
297
+ echo "Note: Add $AGENT_DIR/scripts to PATH, or symlink $AGENT_DIR/scripts/$AGENT_NAME to /usr/local/bin/$AGENT_NAME"
298
+ fi
299
+ fi
198
300
  `
199
301
  : '';
200
302
 
@@ -207,6 +309,9 @@ fi
207
309
  ? ['echo " - Start a new Claude Code session (or restart if one is open)"']
208
310
  : []),
209
311
  ...(codexSetup ? ['echo " - Start a new Codex session (or restart if one is open)"'] : []),
312
+ ...(opencodeSetup
313
+ ? ['echo " - Start a new OpenCode session (or restart if one is open)"']
314
+ : []),
210
315
  `echo " - Say: \\"Hello, ${config.name}!\\""`,
211
316
  'echo ""',
212
317
  `echo "${config.name} is ready."`,
@@ -248,6 +353,7 @@ fi
248
353
  ${claudeSection}
249
354
  ${hookPackSection}
250
355
  ${codexSection}
356
+ ${opencodeSection}
251
357
  ${nextSteps}
252
358
  `;
253
359
  }