@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
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Soleri v7 — CLAUDE.md Composer
3
+ *
4
+ * Auto-generates CLAUDE.md from agent.yaml + instructions/ + workflows/ + skills/.
5
+ * This file is never manually edited. `soleri dev` watches and regenerates on change.
6
+ */
7
+
8
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { parse as parseYaml } from 'yaml';
11
+ import type { AgentYaml } from './agent-schema.js';
12
+
13
+ // ─── Types ────────────────────────────────────────────────────────────
14
+
15
+ export interface ComposedClaudeMd {
16
+ /** The full CLAUDE.md content */
17
+ content: string;
18
+ /** Files that contributed to this composition */
19
+ sources: string[];
20
+ }
21
+
22
+ export interface ToolEntry {
23
+ facade: string;
24
+ ops: string[];
25
+ }
26
+
27
+ // ─── Main Composer ────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Compose CLAUDE.md from an agent folder.
31
+ *
32
+ * @param agentDir - Path to the agent folder (containing agent.yaml)
33
+ * @param tools - Registered MCP tools (from engine introspection). Optional —
34
+ * if not provided, generates a placeholder table.
35
+ */
36
+ export function composeClaudeMd(agentDir: string, tools?: ToolEntry[]): ComposedClaudeMd {
37
+ const sources: string[] = [];
38
+
39
+ // 1. Read agent.yaml
40
+ const agentYamlPath = join(agentDir, 'agent.yaml');
41
+ const agentYaml = parseYaml(readFileSync(agentYamlPath, 'utf-8')) as AgentYaml;
42
+ sources.push(agentYamlPath);
43
+
44
+ const sections: string[] = [];
45
+
46
+ // 2. Agent identity block
47
+ sections.push(composeIdentityBlock(agentYaml));
48
+
49
+ // 3. Activation commands
50
+ sections.push(composeActivation(agentYaml));
51
+
52
+ // 4. Session start
53
+ sections.push(composeSessionStart(agentYaml));
54
+
55
+ // 5. Essential tools table
56
+ sections.push(composeToolsTable(agentYaml, tools));
57
+
58
+ // 6. Engine rules (from instructions/_engine.md)
59
+ const enginePath = join(agentDir, 'instructions', '_engine.md');
60
+ if (existsSync(enginePath)) {
61
+ sections.push(readFileSync(enginePath, 'utf-8').trim());
62
+ sources.push(enginePath);
63
+ }
64
+
65
+ // 7. User instructions (instructions/*.md, excluding _engine.md)
66
+ const instructionsDir = join(agentDir, 'instructions');
67
+ if (existsSync(instructionsDir)) {
68
+ const files = readdirSync(instructionsDir)
69
+ .filter((f) => f.endsWith('.md') && f !== '_engine.md')
70
+ .sort();
71
+ for (const file of files) {
72
+ const filePath = join(instructionsDir, file);
73
+ sections.push(readFileSync(filePath, 'utf-8').trim());
74
+ sources.push(filePath);
75
+ }
76
+ }
77
+
78
+ // 8. Workflow index
79
+ const workflowsDir = join(agentDir, 'workflows');
80
+ if (existsSync(workflowsDir)) {
81
+ const workflowSection = composeWorkflowIndex(workflowsDir);
82
+ if (workflowSection) {
83
+ sections.push(workflowSection);
84
+ // Add workflow prompt files to sources
85
+ const dirs = readdirSync(workflowsDir, { withFileTypes: true }).filter((d) =>
86
+ d.isDirectory(),
87
+ );
88
+ for (const dir of dirs) {
89
+ const promptPath = join(workflowsDir, dir.name, 'prompt.md');
90
+ if (existsSync(promptPath)) sources.push(promptPath);
91
+ }
92
+ }
93
+ }
94
+
95
+ // 9. Skills index
96
+ const skillsDir = join(agentDir, 'skills');
97
+ if (existsSync(skillsDir)) {
98
+ const skillsSection = composeSkillsIndex(skillsDir);
99
+ if (skillsSection) sections.push(skillsSection);
100
+ }
101
+
102
+ const content = sections.join('\n\n') + '\n';
103
+ return { content, sources };
104
+ }
105
+
106
+ // ─── Section Composers ────────────────────────────────────────────────
107
+
108
+ function composeIdentityBlock(agent: AgentYaml): string {
109
+ const lines: string[] = [
110
+ `# ${agent.name} Mode`,
111
+ '',
112
+ `## ${agent.name}`,
113
+ '',
114
+ `**Role:** ${agent.role}`,
115
+ `**Domains:** ${agent.domains.join(', ')}`,
116
+ `**Tone:** ${agent.tone}`,
117
+ '',
118
+ agent.description,
119
+ '',
120
+ '**Principles:**',
121
+ ...agent.principles.map((p) => `- ${p}`),
122
+ ];
123
+ return lines.join('\n');
124
+ }
125
+
126
+ function composeActivation(agent: AgentYaml): string {
127
+ return [
128
+ '## Activation',
129
+ '',
130
+ `**Activate:** "Hello, ${agent.name}!" → \`${agent.id}_core op:activate params:{ projectPath: "." }\``,
131
+ `**Deactivate:** "Goodbye, ${agent.name}!" → \`${agent.id}_core op:activate params:{ deactivate: true }\``,
132
+ '',
133
+ `On activation, adopt the returned persona. Stay in character until deactivated.`,
134
+ ].join('\n');
135
+ }
136
+
137
+ function composeSessionStart(agent: AgentYaml): string {
138
+ return [
139
+ '## Session Start',
140
+ '',
141
+ `On every new session: \`${agent.id}_core op:register params:{ projectPath: "." }\``,
142
+ ].join('\n');
143
+ }
144
+
145
+ function composeToolsTable(agent: AgentYaml, tools?: ToolEntry[]): string {
146
+ const lines: string[] = [
147
+ '## Essential Tools',
148
+ '',
149
+ '| Facade | Key Ops |',
150
+ '|--------|---------|',
151
+ ];
152
+
153
+ if (tools && tools.length > 0) {
154
+ for (const tool of tools) {
155
+ const opsStr = tool.ops
156
+ .slice(0, 6)
157
+ .map((o) => `\`${o}\``)
158
+ .join(', ');
159
+ const suffix = tool.ops.length > 6 ? ', ...' : '';
160
+ lines.push(`| \`${tool.facade}\` | ${opsStr}${suffix} |`);
161
+ }
162
+ } else {
163
+ // Placeholder — will be filled by engine introspection at dev time
164
+ lines.push(`| \`${agent.id}_core\` | \`health\`, \`identity\`, \`register\`, \`activate\` |`);
165
+ lines.push(
166
+ `| \`${agent.id}_vault\` | \`search_intelligent\`, \`capture_knowledge\`, \`capture_quick\` |`,
167
+ );
168
+ lines.push(`| \`${agent.id}_brain\` | \`recommend\`, \`strengths\`, \`feedback\` |`);
169
+ lines.push(
170
+ `| \`${agent.id}_planner\` | \`create_plan\`, \`approve_plan\`, \`plan_split\`, \`plan_reconcile\` |`,
171
+ );
172
+ lines.push(
173
+ `| \`${agent.id}_memory\` | \`memory_search\`, \`memory_capture\`, \`session_capture\` |`,
174
+ );
175
+ lines.push(
176
+ `| \`${agent.id}_curator\` | \`curator_groom\`, \`curator_status\`, \`curator_health\` |`,
177
+ );
178
+ lines.push(
179
+ `| \`${agent.id}_admin\` | \`admin_health\`, \`admin_tool_list\`, \`admin_diagnostic\` |`,
180
+ );
181
+
182
+ // Domain facades from packs
183
+ if (agent.packs) {
184
+ for (const pack of agent.packs) {
185
+ lines.push(`| \`${agent.id}_${pack.name}\` | \`get_patterns\`, \`search\`, \`capture\` |`);
186
+ }
187
+ }
188
+ // Domain facades from domains
189
+ for (const domain of agent.domains) {
190
+ lines.push(`| \`${agent.id}_${domain}\` | \`get_patterns\`, \`search\`, \`capture\` |`);
191
+ }
192
+ }
193
+
194
+ lines.push('');
195
+ lines.push(`> Full list: \`${agent.id}_admin op:admin_tool_list\``);
196
+
197
+ return lines.join('\n');
198
+ }
199
+
200
+ function composeWorkflowIndex(workflowsDir: string): string | null {
201
+ const dirs = readdirSync(workflowsDir, { withFileTypes: true })
202
+ .filter((d) => d.isDirectory())
203
+ .sort((a, b) => a.name.localeCompare(b.name));
204
+
205
+ if (dirs.length === 0) return null;
206
+
207
+ const lines: string[] = [
208
+ '## Available Workflows',
209
+ '',
210
+ '| Workflow | Description |',
211
+ '|----------|-------------|',
212
+ ];
213
+
214
+ for (const dir of dirs) {
215
+ const promptPath = join(workflowsDir, dir.name, 'prompt.md');
216
+ let description = dir.name;
217
+
218
+ if (existsSync(promptPath)) {
219
+ const content = readFileSync(promptPath, 'utf-8');
220
+ // Extract first non-heading, non-empty line as description
221
+ const descLine = content.split('\n').find((line) => line.trim() && !line.startsWith('#'));
222
+ if (descLine) description = descLine.trim().slice(0, 80);
223
+ }
224
+
225
+ lines.push(`| \`${dir.name}\` | ${description} |`);
226
+ }
227
+
228
+ return lines.join('\n');
229
+ }
230
+
231
+ function composeSkillsIndex(skillsDir: string): string | null {
232
+ const dirs = readdirSync(skillsDir, { withFileTypes: true })
233
+ .filter((d) => d.isDirectory())
234
+ .sort((a, b) => a.name.localeCompare(b.name));
235
+
236
+ if (dirs.length === 0) return null;
237
+
238
+ const lines: string[] = ['## Available Skills', ''];
239
+
240
+ for (const dir of dirs) {
241
+ const skillPath = join(skillsDir, dir.name, 'SKILL.md');
242
+ if (existsSync(skillPath)) {
243
+ const content = readFileSync(skillPath, 'utf-8');
244
+ // Extract description from frontmatter if present
245
+ const descMatch = content.match(/^description:\s*(.+)$/m);
246
+ const desc = descMatch ? descMatch[1].trim() : dir.name;
247
+ lines.push(`- **${dir.name}**: ${desc}`);
248
+ }
249
+ }
250
+
251
+ return lines.join('\n');
252
+ }
package/src/lib.ts CHANGED
@@ -18,9 +18,22 @@ export type {
18
18
  InstallKnowledgeResult,
19
19
  AddDomainResult,
20
20
  } from './types.js';
21
- export { AgentConfigSchema, SETUP_TARGETS } from './types.js';
21
+ export { AgentConfigSchema, SETUP_TARGETS, MODEL_PRESETS } from './types.js';
22
+
23
+ // ─── v7 File-Tree Agent ──────────────────────────────────────────────
24
+ export { scaffoldFileTree } from './scaffold-filetree.js';
25
+ export type { FileTreeScaffoldResult } from './scaffold-filetree.js';
26
+ export { AgentYamlSchema, TONES } from './agent-schema.js';
27
+ export type { AgentYaml, AgentYamlInput } from './agent-schema.js';
28
+ export { composeClaudeMd } from './compose-claude-md.js';
29
+ export type { ComposedClaudeMd, ToolEntry } from './compose-claude-md.js';
22
30
  export { generateExtensionsIndex, generateExampleOp } from './templates/extensions.js';
23
31
  export { generateClaudeMdTemplate } from './templates/claude-md-template.js';
24
32
  export { getEngineRulesContent, getEngineMarker } from './templates/shared-rules.js';
25
33
  export { generateInjectClaudeMd } from './templates/inject-claude-md.js';
26
34
  export { generateSkills } from './templates/skills.js';
35
+ export { generateTelegramBot } from './templates/telegram-bot.js';
36
+ export { generateTelegramAgent } from './templates/telegram-agent.js';
37
+ export { generateTelegramConfig } from './templates/telegram-config.js';
38
+ export { generateTelegramSupervisor } from './templates/telegram-supervisor.js';
39
+ export { generateEntryPoint } from './templates/entry-point.js';
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Soleri v7 — File-Tree Agent Scaffolder
3
+ *
4
+ * Generates a folder tree with plain files (YAML, Markdown, JSON).
5
+ * No TypeScript, no package.json, no build step.
6
+ *
7
+ * Replaces the old scaffold() that generated TypeScript projects.
8
+ */
9
+
10
+ import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { stringify as yamlStringify } from 'yaml';
13
+ import type { AgentYaml, AgentYamlInput } from './agent-schema.js';
14
+ import { AgentYamlSchema } from './agent-schema.js';
15
+ import { getEngineRulesContent } from './templates/shared-rules.js';
16
+ import { composeClaudeMd } from './compose-claude-md.js';
17
+
18
+ // ─── Types ────────────────────────────────────────────────────────────
19
+
20
+ export interface FileTreeScaffoldResult {
21
+ success: boolean;
22
+ agentDir: string;
23
+ filesCreated: string[];
24
+ summary: string;
25
+ }
26
+
27
+ // ─── Built-in Workflows ───────────────────────────────────────────────
28
+
29
+ interface WorkflowTemplate {
30
+ name: string;
31
+ prompt: string;
32
+ gates: string;
33
+ tools: string;
34
+ }
35
+
36
+ const BUILTIN_WORKFLOWS: WorkflowTemplate[] = [
37
+ {
38
+ name: 'feature-dev',
39
+ prompt: `# Feature Development
40
+
41
+ ## When to Use
42
+ When building a new feature, adding functionality, or creating components.
43
+
44
+ ## Steps
45
+
46
+ ### 1. Understand
47
+ - Search vault for existing patterns: \`op:search_intelligent\`
48
+ - Read relevant source code
49
+ - Clarify requirements with user if ambiguous
50
+
51
+ ### 2. Plan
52
+ - Create structured plan: \`op:orchestrate_plan\`
53
+ - Present plan to user, wait for approval
54
+ - Do NOT write code before approval
55
+
56
+ ### 3. Test First
57
+ - Write failing tests that define expected behavior
58
+ - Run tests to confirm they fail (RED)
59
+
60
+ ### 4. Implement
61
+ - Write minimum code to pass tests (GREEN)
62
+ - Follow vault patterns, avoid known anti-patterns
63
+ - Use semantic tokens, not hardcoded values
64
+
65
+ ### 5. Refactor
66
+ - Clean up without changing behavior
67
+ - Extract reusable patterns
68
+ - Ensure all tests still pass
69
+
70
+ ### 6. Capture & Ship
71
+ - Capture learned patterns: \`op:capture_knowledge\`
72
+ - Link new entries to related knowledge: \`op:link_entries\`
73
+ - Complete orchestration: \`op:orchestrate_complete\`
74
+ `,
75
+ gates: `gates:
76
+ - phase: brainstorming
77
+ requirement: Requirements are clear and user has approved the approach
78
+ check: user-approval
79
+
80
+ - phase: pre-execution
81
+ requirement: Plan created via orchestrator and approved by user
82
+ check: plan-approved
83
+
84
+ - phase: post-task
85
+ requirement: All tests pass and code compiles
86
+ check: tests-pass
87
+
88
+ - phase: completion
89
+ requirement: Knowledge captured to vault with links
90
+ check: knowledge-captured
91
+ `,
92
+ tools: `tools:
93
+ - soleri_vault op:search_intelligent
94
+ - soleri_vault op:capture_knowledge
95
+ - soleri_vault op:link_entries
96
+ - soleri_planner op:create_plan
97
+ - soleri_planner op:approve_plan
98
+ - soleri_brain op:recommend
99
+ `,
100
+ },
101
+ {
102
+ name: 'bug-fix',
103
+ prompt: `# Bug Fix
104
+
105
+ ## When to Use
106
+ When fixing bugs, resolving errors, or addressing regressions.
107
+
108
+ ## Steps
109
+
110
+ ### 1. Reproduce
111
+ - Understand the reported issue
112
+ - Search vault for similar past bugs: \`op:search_intelligent\`
113
+ - Identify the root cause, not just the symptom
114
+
115
+ ### 2. Plan Fix
116
+ - Create a plan: \`op:orchestrate_plan\`
117
+ - Identify affected files and potential side effects
118
+ - Wait for user approval
119
+
120
+ ### 3. Write Regression Test
121
+ - Write a test that reproduces the bug (RED)
122
+ - Confirm it fails for the right reason
123
+
124
+ ### 4. Fix
125
+ - Apply the minimal fix
126
+ - Run the regression test — must pass (GREEN)
127
+ - Run full test suite — no new failures
128
+
129
+ ### 5. Capture
130
+ - If the bug reveals a pattern or anti-pattern, capture it: \`op:capture_knowledge\`
131
+ - Complete orchestration: \`op:orchestrate_complete\`
132
+ `,
133
+ gates: `gates:
134
+ - phase: pre-execution
135
+ requirement: Root cause identified and fix plan approved
136
+ check: plan-approved
137
+
138
+ - phase: post-task
139
+ requirement: Regression test passes and no new failures
140
+ check: tests-pass
141
+
142
+ - phase: completion
143
+ requirement: Anti-pattern captured if applicable
144
+ check: knowledge-captured
145
+ `,
146
+ tools: `tools:
147
+ - soleri_vault op:search_intelligent
148
+ - soleri_vault op:capture_knowledge
149
+ - soleri_planner op:create_plan
150
+ - soleri_brain op:recommend
151
+ `,
152
+ },
153
+ {
154
+ name: 'code-review',
155
+ prompt: `# Code Review
156
+
157
+ ## When to Use
158
+ When reviewing code, auditing quality, or checking for issues.
159
+
160
+ ## Steps
161
+
162
+ ### 1. Context
163
+ - Search vault for relevant patterns and anti-patterns: \`op:search_intelligent\`
164
+ - Understand the intent of the changes
165
+
166
+ ### 2. Review
167
+ - Check for correctness, readability, and maintainability
168
+ - Verify test coverage
169
+ - Check for security issues
170
+ - Validate accessibility if applicable
171
+
172
+ ### 3. Feedback
173
+ - Provide actionable, specific feedback
174
+ - Reference vault patterns where applicable
175
+ - Distinguish blocking issues from suggestions
176
+
177
+ ### 4. Capture
178
+ - If review reveals new patterns or anti-patterns, capture them: \`op:capture_knowledge\`
179
+ `,
180
+ gates: `gates:
181
+ - phase: completion
182
+ requirement: All blocking issues addressed
183
+ check: issues-resolved
184
+ `,
185
+ tools: `tools:
186
+ - soleri_vault op:search_intelligent
187
+ - soleri_vault op:capture_knowledge
188
+ - soleri_brain op:recommend
189
+ `,
190
+ },
191
+ ];
192
+
193
+ // ─── Main Scaffolder ──────────────────────────────────────────────────
194
+
195
+ /**
196
+ * Scaffold a file-tree agent.
197
+ *
198
+ * Creates a folder with agent.yaml, .mcp.json, instructions/, workflows/,
199
+ * and auto-generates CLAUDE.md. No TypeScript, no build step.
200
+ */
201
+ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): FileTreeScaffoldResult {
202
+ // Validate config
203
+ const parseResult = AgentYamlSchema.safeParse(input);
204
+ if (!parseResult.success) {
205
+ return {
206
+ success: false,
207
+ agentDir: join(outputDir, input.id ?? 'unknown'),
208
+ filesCreated: [],
209
+ summary: `Invalid config: ${parseResult.error.message}`,
210
+ };
211
+ }
212
+
213
+ const config = parseResult.data;
214
+ const agentDir = join(outputDir, config.id);
215
+ const filesCreated: string[] = [];
216
+
217
+ // Check for existing directory
218
+ if (existsSync(agentDir)) {
219
+ return {
220
+ success: false,
221
+ agentDir,
222
+ filesCreated: [],
223
+ summary: `Directory already exists: ${agentDir}. Choose a different ID or remove it.`,
224
+ };
225
+ }
226
+
227
+ // ─── 1. Create directory structure ──────────────────────────
228
+ const dirs = ['', 'instructions', 'workflows', 'knowledge', 'skills', 'hooks', 'data'];
229
+
230
+ // Add workflow subdirectories
231
+ for (const wf of BUILTIN_WORKFLOWS) {
232
+ dirs.push(`workflows/${wf.name}`);
233
+ }
234
+
235
+ for (const dir of dirs) {
236
+ mkdirSync(join(agentDir, dir), { recursive: true });
237
+ }
238
+
239
+ // ─── 2. Write agent.yaml ────────────────────────────────────
240
+ const agentYamlContent = yamlStringify(buildAgentYaml(config), {
241
+ lineWidth: 100,
242
+ singleQuote: true,
243
+ });
244
+ writeFile(agentDir, 'agent.yaml', agentYamlContent, filesCreated);
245
+
246
+ // ─── 3. Write .mcp.json ─────────────────────────────────────
247
+ const mcpJson = {
248
+ mcpServers: {
249
+ 'soleri-engine': {
250
+ command: 'npx',
251
+ args: ['@soleri/engine', '--agent', './agent.yaml'],
252
+ },
253
+ },
254
+ };
255
+ writeFile(agentDir, '.mcp.json', JSON.stringify(mcpJson, null, 2) + '\n', filesCreated);
256
+
257
+ // ─── 4. Write .gitignore ────────────────────────────────────
258
+ writeFile(
259
+ agentDir,
260
+ '.gitignore',
261
+ [
262
+ '# Auto-generated — do not commit',
263
+ 'CLAUDE.md',
264
+ 'AGENTS.md',
265
+ 'instructions/_engine.md',
266
+ '',
267
+ ].join('\n'),
268
+ filesCreated,
269
+ );
270
+
271
+ // ─── 5. Write engine rules ──────────────────────────────────
272
+ writeFile(agentDir, 'instructions/_engine.md', getEngineRulesContent(), filesCreated);
273
+
274
+ // ─── 6. Write user instruction files ────────────────────────
275
+ // Generate domain-specific instruction file if agent has specialized domains
276
+ if (config.domains.length > 0) {
277
+ const domainLines = [
278
+ '# Domain Knowledge',
279
+ '',
280
+ `This agent specializes in: ${config.domains.join(', ')}.`,
281
+ '',
282
+ '## Principles',
283
+ '',
284
+ ...config.principles.map((p) => `- ${p}`),
285
+ '',
286
+ ];
287
+ writeFile(agentDir, 'instructions/domain.md', domainLines.join('\n'), filesCreated);
288
+ }
289
+
290
+ // ─── 7. Write workflows ─────────────────────────────────────
291
+ for (const wf of BUILTIN_WORKFLOWS) {
292
+ writeFile(agentDir, `workflows/${wf.name}/prompt.md`, wf.prompt, filesCreated);
293
+ writeFile(agentDir, `workflows/${wf.name}/gates.yaml`, wf.gates, filesCreated);
294
+ writeFile(agentDir, `workflows/${wf.name}/tools.yaml`, wf.tools, filesCreated);
295
+ }
296
+
297
+ // ─── 8. Write empty knowledge bundle ────────────────────────
298
+ for (const domain of config.domains) {
299
+ const bundle = {
300
+ domain,
301
+ version: '1.0.0',
302
+ entries: [],
303
+ };
304
+ writeFile(
305
+ agentDir,
306
+ `knowledge/${domain}.json`,
307
+ JSON.stringify(bundle, null, 2) + '\n',
308
+ filesCreated,
309
+ );
310
+ }
311
+
312
+ // ─── 9. Generate CLAUDE.md ──────────────────────────────────
313
+ const { content: claudeMd } = composeClaudeMd(agentDir);
314
+ writeFile(agentDir, 'CLAUDE.md', claudeMd, filesCreated);
315
+
316
+ // ─── 10. Summary ────────────────────────────────────────────
317
+ const summary = [
318
+ `Agent "${config.name}" scaffolded at ${agentDir}`,
319
+ '',
320
+ ` Files: ${filesCreated.length}`,
321
+ ` Domains: ${config.domains.join(', ')}`,
322
+ ` Workflows: ${BUILTIN_WORKFLOWS.map((w) => w.name).join(', ')}`,
323
+ '',
324
+ 'Next steps:',
325
+ ` 1. cd ${config.id}`,
326
+ ' 2. Review agent.yaml and customize instructions/',
327
+ ' 3. Run: soleri install (registers MCP server)',
328
+ ' 4. Run: soleri dev (watches files, auto-regenerates CLAUDE.md)',
329
+ '',
330
+ 'No build step needed — this agent is ready to use.',
331
+ ].join('\n');
332
+
333
+ return {
334
+ success: true,
335
+ agentDir,
336
+ filesCreated,
337
+ summary,
338
+ };
339
+ }
340
+
341
+ // ─── Helpers ──────────────────────────────────────────────────────────
342
+
343
+ function writeFile(
344
+ agentDir: string,
345
+ relativePath: string,
346
+ content: string,
347
+ filesCreated: string[],
348
+ ): void {
349
+ const fullPath = join(agentDir, relativePath);
350
+ const dir = join(
351
+ agentDir,
352
+ relativePath.includes('/') ? relativePath.split('/').slice(0, -1).join('/') : '',
353
+ );
354
+ if (!existsSync(dir)) {
355
+ mkdirSync(dir, { recursive: true });
356
+ }
357
+ writeFileSync(fullPath, content, 'utf-8');
358
+ filesCreated.push(relativePath);
359
+ }
360
+
361
+ /**
362
+ * Build a clean agent.yaml object for serialization.
363
+ * Strips defaults and empty optionals for cleaner output.
364
+ */
365
+ function buildAgentYaml(config: AgentYaml): Record<string, unknown> {
366
+ const yaml: Record<string, unknown> = {
367
+ id: config.id,
368
+ name: config.name,
369
+ role: config.role,
370
+ description: config.description,
371
+ domains: config.domains,
372
+ principles: config.principles,
373
+ };
374
+
375
+ if (config.tone && config.tone !== 'pragmatic') {
376
+ yaml.tone = config.tone;
377
+ }
378
+
379
+ if (config.greeting) {
380
+ yaml.greeting = config.greeting;
381
+ }
382
+
383
+ // Engine config — only include non-defaults
384
+ const engine: Record<string, unknown> = {};
385
+ if (config.engine?.vault) engine.vault = config.engine.vault;
386
+ if (config.engine?.learning === false) engine.learning = false;
387
+ if (config.engine?.cognee === true) engine.cognee = true;
388
+ if (Object.keys(engine).length > 0) yaml.engine = engine;
389
+
390
+ // Vaults
391
+ if (config.vaults && config.vaults.length > 0) {
392
+ yaml.vaults = config.vaults;
393
+ }
394
+
395
+ // Setup — only include non-defaults
396
+ const setup: Record<string, unknown> = {};
397
+ if (config.setup?.target && config.setup.target !== 'opencode')
398
+ setup.target = config.setup.target;
399
+ if (config.setup?.model && config.setup.model !== 'claude-code-sonnet-4')
400
+ setup.model = config.setup.model;
401
+ if (Object.keys(setup).length > 0) yaml.setup = setup;
402
+
403
+ // Packs
404
+ if (config.packs && config.packs.length > 0) {
405
+ yaml.packs = config.packs;
406
+ }
407
+
408
+ return yaml;
409
+ }