@soleri/forge 5.14.10 → 8.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 (65) 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 +361 -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.js +39 -1
  17. package/dist/templates/activate.js.map +1 -1
  18. package/dist/templates/agents-md.d.ts +10 -1
  19. package/dist/templates/agents-md.js +76 -16
  20. package/dist/templates/agents-md.js.map +1 -1
  21. package/dist/templates/claude-md-template.js +9 -1
  22. package/dist/templates/claude-md-template.js.map +1 -1
  23. package/dist/templates/entry-point.js +83 -6
  24. package/dist/templates/entry-point.js.map +1 -1
  25. package/dist/templates/inject-claude-md.js +53 -0
  26. package/dist/templates/inject-claude-md.js.map +1 -1
  27. package/dist/templates/package-json.js +4 -1
  28. package/dist/templates/package-json.js.map +1 -1
  29. package/dist/templates/readme.js +4 -3
  30. package/dist/templates/readme.js.map +1 -1
  31. package/dist/templates/setup-script.js +109 -3
  32. package/dist/templates/setup-script.js.map +1 -1
  33. package/dist/templates/shared-rules.js +54 -17
  34. package/dist/templates/shared-rules.js.map +1 -1
  35. package/dist/templates/test-facades.js +151 -6
  36. package/dist/templates/test-facades.js.map +1 -1
  37. package/dist/types.d.ts +71 -6
  38. package/dist/types.js +39 -1
  39. package/dist/types.js.map +1 -1
  40. package/dist/utils/detect-domain-packs.d.ts +25 -0
  41. package/dist/utils/detect-domain-packs.js +104 -0
  42. package/dist/utils/detect-domain-packs.js.map +1 -0
  43. package/package.json +2 -1
  44. package/src/__tests__/detect-domain-packs.test.ts +178 -0
  45. package/src/__tests__/scaffold-filetree.test.ts +257 -0
  46. package/src/__tests__/scaffolder.test.ts +5 -3
  47. package/src/agent-schema.ts +184 -0
  48. package/src/compose-claude-md.ts +252 -0
  49. package/src/lib.ts +14 -1
  50. package/src/scaffold-filetree.ts +421 -0
  51. package/src/scaffolder.ts +299 -15
  52. package/src/templates/activate.ts +39 -0
  53. package/src/templates/agents-md.ts +78 -16
  54. package/src/templates/claude-md-template.ts +12 -1
  55. package/src/templates/entry-point.ts +90 -6
  56. package/src/templates/inject-claude-md.ts +53 -0
  57. package/src/templates/package-json.ts +4 -1
  58. package/src/templates/readme.ts +4 -3
  59. package/src/templates/setup-script.ts +110 -4
  60. package/src/templates/shared-rules.ts +55 -17
  61. package/src/templates/test-facades.ts +156 -6
  62. package/src/types.ts +44 -1
  63. package/src/utils/detect-domain-packs.ts +129 -0
  64. package/tsconfig.json +0 -1
  65. package/vitest.config.ts +1 -2
@@ -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) {
@@ -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
  }
@@ -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) |',