@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,178 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { detectInstalledDomainPacks } from '../utils/detect-domain-packs.js';
6
+
7
+ function createTempDir(): string {
8
+ const dir = join(
9
+ tmpdir(),
10
+ `soleri-forge-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
11
+ );
12
+ mkdirSync(dir, { recursive: true });
13
+ return dir;
14
+ }
15
+
16
+ describe('detectInstalledDomainPacks', () => {
17
+ let tempDir: string;
18
+
19
+ beforeEach(() => {
20
+ tempDir = createTempDir();
21
+ });
22
+
23
+ afterEach(() => {
24
+ try {
25
+ rmSync(tempDir, { recursive: true, force: true });
26
+ } catch {
27
+ // best-effort cleanup
28
+ }
29
+ });
30
+
31
+ it('returns empty array when no node_modules exists', () => {
32
+ const result = detectInstalledDomainPacks(tempDir);
33
+ expect(result).toEqual([]);
34
+ });
35
+
36
+ it('returns empty array when @soleri scope exists but has no domain-* packages', () => {
37
+ const scope = join(tempDir, 'node_modules', '@soleri', 'core');
38
+ mkdirSync(scope, { recursive: true });
39
+ writeFileSync(
40
+ join(scope, 'package.json'),
41
+ JSON.stringify({ name: '@soleri/core', version: '1.0.0' }),
42
+ );
43
+
44
+ const result = detectInstalledDomainPacks(tempDir);
45
+ expect(result).toEqual([]);
46
+ });
47
+
48
+ it('detects a domain pack with soleri-domain-pack keyword', () => {
49
+ const packDir = join(tempDir, 'node_modules', '@soleri', 'domain-design');
50
+ mkdirSync(packDir, { recursive: true });
51
+ writeFileSync(
52
+ join(packDir, 'package.json'),
53
+ JSON.stringify({
54
+ name: '@soleri/domain-design',
55
+ version: '2.1.0',
56
+ keywords: ['soleri-domain-pack'],
57
+ }),
58
+ );
59
+
60
+ const result = detectInstalledDomainPacks(tempDir);
61
+ expect(result).toEqual([
62
+ { name: 'design', package: '@soleri/domain-design', version: '2.1.0' },
63
+ ]);
64
+ });
65
+
66
+ it('detects a domain pack with a main entry point', () => {
67
+ const packDir = join(tempDir, 'node_modules', '@soleri', 'domain-security');
68
+ const distDir = join(packDir, 'dist');
69
+ mkdirSync(distDir, { recursive: true });
70
+ writeFileSync(
71
+ join(packDir, 'package.json'),
72
+ JSON.stringify({
73
+ name: '@soleri/domain-security',
74
+ version: '1.0.0',
75
+ main: 'dist/index.js',
76
+ }),
77
+ );
78
+ writeFileSync(join(distDir, 'index.js'), 'module.exports = {};');
79
+
80
+ const result = detectInstalledDomainPacks(tempDir);
81
+ expect(result).toEqual([
82
+ { name: 'security', package: '@soleri/domain-security', version: '1.0.0' },
83
+ ]);
84
+ });
85
+
86
+ it('detects multiple domain packs', () => {
87
+ for (const [suffix, ver] of [
88
+ ['design', '2.0.0'],
89
+ ['security', '1.0.0'],
90
+ ['analytics', '0.5.0'],
91
+ ]) {
92
+ const packDir = join(tempDir, 'node_modules', '@soleri', `domain-${suffix}`);
93
+ mkdirSync(packDir, { recursive: true });
94
+ writeFileSync(
95
+ join(packDir, 'package.json'),
96
+ JSON.stringify({
97
+ name: `@soleri/domain-${suffix}`,
98
+ version: ver,
99
+ keywords: ['soleri-domain-pack'],
100
+ }),
101
+ );
102
+ }
103
+
104
+ const result = detectInstalledDomainPacks(tempDir);
105
+ expect(result).toHaveLength(3);
106
+ expect(result.map((r) => r.name).sort()).toEqual(['analytics', 'design', 'security']);
107
+ });
108
+
109
+ it('skips packages without valid package.json', () => {
110
+ const packDir = join(tempDir, 'node_modules', '@soleri', 'domain-broken');
111
+ mkdirSync(packDir, { recursive: true });
112
+ writeFileSync(join(packDir, 'package.json'), '{ invalid json }');
113
+
114
+ const result = detectInstalledDomainPacks(tempDir);
115
+ expect(result).toEqual([]);
116
+ });
117
+
118
+ it('skips packages missing name or version', () => {
119
+ const packDir = join(tempDir, 'node_modules', '@soleri', 'domain-incomplete');
120
+ mkdirSync(packDir, { recursive: true });
121
+ writeFileSync(
122
+ join(packDir, 'package.json'),
123
+ JSON.stringify({ name: '@soleri/domain-incomplete' }), // no version
124
+ );
125
+
126
+ const result = detectInstalledDomainPacks(tempDir);
127
+ expect(result).toEqual([]);
128
+ });
129
+
130
+ it('skips packages that have no entry point and no keyword', () => {
131
+ const packDir = join(tempDir, 'node_modules', '@soleri', 'domain-empty');
132
+ mkdirSync(packDir, { recursive: true });
133
+ writeFileSync(
134
+ join(packDir, 'package.json'),
135
+ JSON.stringify({ name: '@soleri/domain-empty', version: '1.0.0' }),
136
+ );
137
+
138
+ const result = detectInstalledDomainPacks(tempDir);
139
+ expect(result).toEqual([]);
140
+ });
141
+
142
+ it('walks up to find node_modules in a parent directory', () => {
143
+ // Create node_modules in tempDir but search from a subdirectory
144
+ const packDir = join(tempDir, 'node_modules', '@soleri', 'domain-design');
145
+ mkdirSync(packDir, { recursive: true });
146
+ writeFileSync(
147
+ join(packDir, 'package.json'),
148
+ JSON.stringify({
149
+ name: '@soleri/domain-design',
150
+ version: '1.0.0',
151
+ keywords: ['soleri-domain-pack'],
152
+ }),
153
+ );
154
+
155
+ const subDir = join(tempDir, 'projects', 'my-agent');
156
+ mkdirSync(subDir, { recursive: true });
157
+
158
+ const result = detectInstalledDomainPacks(subDir);
159
+ expect(result).toEqual([
160
+ { name: 'design', package: '@soleri/domain-design', version: '1.0.0' },
161
+ ]);
162
+ });
163
+
164
+ it('detects domain pack with dist/index.js entry point (no main field)', () => {
165
+ const packDir = join(tempDir, 'node_modules', '@soleri', 'domain-testing');
166
+ mkdirSync(join(packDir, 'dist'), { recursive: true });
167
+ writeFileSync(
168
+ join(packDir, 'package.json'),
169
+ JSON.stringify({ name: '@soleri/domain-testing', version: '1.0.0' }),
170
+ );
171
+ writeFileSync(join(packDir, 'dist', 'index.js'), 'export default {};');
172
+
173
+ const result = detectInstalledDomainPacks(tempDir);
174
+ expect(result).toEqual([
175
+ { name: 'testing', package: '@soleri/domain-testing', version: '1.0.0' },
176
+ ]);
177
+ });
178
+ });
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Tests for the file-tree scaffolder.
3
+ *
4
+ * Validates that scaffoldFileTree() produces a valid agent folder
5
+ * with all expected files and no TypeScript output.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
9
+ import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { tmpdir } from 'node:os';
12
+ import { parse as parseYaml } from 'yaml';
13
+ import { scaffoldFileTree } from '../scaffold-filetree.js';
14
+
15
+ let tempDir: string;
16
+
17
+ beforeEach(() => {
18
+ tempDir = join(tmpdir(), `soleri-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
19
+ mkdirSync(tempDir, { recursive: true });
20
+ });
21
+
22
+ afterEach(() => {
23
+ rmSync(tempDir, { recursive: true, force: true });
24
+ });
25
+
26
+ const MINIMAL_CONFIG = {
27
+ id: 'test-agent',
28
+ name: 'Test Agent',
29
+ role: 'Testing Advisor',
30
+ description: 'A test agent for validating the file-tree scaffolder output.',
31
+ domains: ['testing', 'quality'],
32
+ principles: ['Test everything', 'Fail fast'],
33
+ };
34
+
35
+ describe('scaffoldFileTree', () => {
36
+ it('creates agent directory with all expected files', () => {
37
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
38
+
39
+ expect(result.success).toBe(true);
40
+ expect(result.agentDir).toBe(join(tempDir, 'test-agent'));
41
+ expect(result.filesCreated.length).toBeGreaterThan(10);
42
+
43
+ // Core files exist
44
+ expect(existsSync(join(result.agentDir, 'agent.yaml'))).toBe(true);
45
+ expect(existsSync(join(result.agentDir, '.mcp.json'))).toBe(true);
46
+ expect(existsSync(join(result.agentDir, '.gitignore'))).toBe(true);
47
+ expect(existsSync(join(result.agentDir, 'CLAUDE.md'))).toBe(true);
48
+
49
+ // Directories exist
50
+ expect(existsSync(join(result.agentDir, 'instructions'))).toBe(true);
51
+ expect(existsSync(join(result.agentDir, 'workflows'))).toBe(true);
52
+ expect(existsSync(join(result.agentDir, 'knowledge'))).toBe(true);
53
+ expect(existsSync(join(result.agentDir, 'skills'))).toBe(true);
54
+ expect(existsSync(join(result.agentDir, 'hooks'))).toBe(true);
55
+ expect(existsSync(join(result.agentDir, 'data'))).toBe(true);
56
+ });
57
+
58
+ it('generates NO TypeScript files', () => {
59
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
60
+ expect(result.success).toBe(true);
61
+
62
+ // No .ts files anywhere
63
+ const tsFiles = result.filesCreated.filter((f) => f.endsWith('.ts'));
64
+ expect(tsFiles).toEqual([]);
65
+
66
+ // No package.json
67
+ expect(existsSync(join(result.agentDir, 'package.json'))).toBe(false);
68
+
69
+ // No tsconfig.json
70
+ expect(existsSync(join(result.agentDir, 'tsconfig.json'))).toBe(false);
71
+
72
+ // No src/ directory
73
+ expect(existsSync(join(result.agentDir, 'src'))).toBe(false);
74
+
75
+ // No node_modules
76
+ expect(existsSync(join(result.agentDir, 'node_modules'))).toBe(false);
77
+ });
78
+
79
+ it('generates valid agent.yaml', () => {
80
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
81
+ expect(result.success).toBe(true);
82
+
83
+ const content = readFileSync(join(result.agentDir, 'agent.yaml'), 'utf-8');
84
+ const parsed = parseYaml(content);
85
+
86
+ expect(parsed.id).toBe('test-agent');
87
+ expect(parsed.name).toBe('Test Agent');
88
+ expect(parsed.role).toBe('Testing Advisor');
89
+ expect(parsed.domains).toEqual(['testing', 'quality']);
90
+ expect(parsed.principles).toEqual(['Test everything', 'Fail fast']);
91
+ });
92
+
93
+ it('generates valid .mcp.json pointing to soleri-engine', () => {
94
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
95
+ expect(result.success).toBe(true);
96
+
97
+ const content = readFileSync(join(result.agentDir, '.mcp.json'), 'utf-8');
98
+ const parsed = JSON.parse(content);
99
+
100
+ expect(parsed.mcpServers['soleri-engine']).toBeDefined();
101
+ expect(parsed.mcpServers['soleri-engine'].command).toBe('npx');
102
+ expect(parsed.mcpServers['soleri-engine'].args).toContain('@soleri/engine');
103
+ expect(parsed.mcpServers['soleri-engine'].args).toContain('./agent.yaml');
104
+ });
105
+
106
+ it('generates engine rules in instructions/_engine.md', () => {
107
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
108
+ expect(result.success).toBe(true);
109
+
110
+ const content = readFileSync(join(result.agentDir, 'instructions', '_engine.md'), 'utf-8');
111
+ expect(content).toContain('soleri:engine-rules');
112
+ expect(content).toContain('Vault as Source of Truth');
113
+ expect(content).toContain('Planning');
114
+ expect(content).toContain('Clean Commits');
115
+ });
116
+
117
+ it('generates domain instruction file', () => {
118
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
119
+ expect(result.success).toBe(true);
120
+
121
+ const content = readFileSync(join(result.agentDir, 'instructions', 'domain.md'), 'utf-8');
122
+ expect(content).toContain('testing, quality');
123
+ expect(content).toContain('Test everything');
124
+ });
125
+
126
+ it('generates workflow folders with prompt, gates, and tools', () => {
127
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
128
+ expect(result.success).toBe(true);
129
+
130
+ // Check feature-dev workflow
131
+ const featureDevDir = join(result.agentDir, 'workflows', 'feature-dev');
132
+ expect(existsSync(join(featureDevDir, 'prompt.md'))).toBe(true);
133
+ expect(existsSync(join(featureDevDir, 'gates.yaml'))).toBe(true);
134
+ expect(existsSync(join(featureDevDir, 'tools.yaml'))).toBe(true);
135
+
136
+ const prompt = readFileSync(join(featureDevDir, 'prompt.md'), 'utf-8');
137
+ expect(prompt).toContain('Feature Development');
138
+ expect(prompt).toContain('op:search_intelligent');
139
+
140
+ // Check bug-fix workflow
141
+ expect(existsSync(join(result.agentDir, 'workflows', 'bug-fix', 'prompt.md'))).toBe(true);
142
+
143
+ // Check code-review workflow
144
+ expect(existsSync(join(result.agentDir, 'workflows', 'code-review', 'prompt.md'))).toBe(true);
145
+ });
146
+
147
+ it('generates knowledge bundles per domain', () => {
148
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
149
+ expect(result.success).toBe(true);
150
+
151
+ // One bundle per domain
152
+ expect(existsSync(join(result.agentDir, 'knowledge', 'testing.json'))).toBe(true);
153
+ expect(existsSync(join(result.agentDir, 'knowledge', 'quality.json'))).toBe(true);
154
+
155
+ const bundle = JSON.parse(
156
+ readFileSync(join(result.agentDir, 'knowledge', 'testing.json'), 'utf-8'),
157
+ );
158
+ expect(bundle.domain).toBe('testing');
159
+ expect(bundle.version).toBe('1.0.0');
160
+ expect(bundle.entries).toEqual([]);
161
+ });
162
+
163
+ it('generates CLAUDE.md with correct agent identity', () => {
164
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
165
+ expect(result.success).toBe(true);
166
+
167
+ const claudeMd = readFileSync(join(result.agentDir, 'CLAUDE.md'), 'utf-8');
168
+ expect(claudeMd).toContain('# Test Agent Mode');
169
+ expect(claudeMd).toContain('**Role:** Testing Advisor');
170
+ expect(claudeMd).toContain('test-agent_core op:activate');
171
+ expect(claudeMd).toContain('test-agent_vault');
172
+ expect(claudeMd).toContain('Available Workflows');
173
+ expect(claudeMd).toContain('feature-dev');
174
+ });
175
+
176
+ it('.gitignore excludes auto-generated files', () => {
177
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
178
+ expect(result.success).toBe(true);
179
+
180
+ const gitignore = readFileSync(join(result.agentDir, '.gitignore'), 'utf-8');
181
+ expect(gitignore).toContain('CLAUDE.md');
182
+ expect(gitignore).toContain('AGENTS.md');
183
+ expect(gitignore).toContain('_engine.md');
184
+ });
185
+
186
+ it('fails if directory already exists', () => {
187
+ scaffoldFileTree(MINIMAL_CONFIG, tempDir);
188
+ const result2 = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
189
+
190
+ expect(result2.success).toBe(false);
191
+ expect(result2.summary).toContain('already exists');
192
+ });
193
+
194
+ it('fails on invalid config', () => {
195
+ const result = scaffoldFileTree(
196
+ { id: 'INVALID_ID', name: '', role: '', description: '', domains: [], principles: [] } as any,
197
+ tempDir,
198
+ );
199
+ expect(result.success).toBe(false);
200
+ expect(result.summary).toContain('Invalid config');
201
+ });
202
+
203
+ it('omits default values from agent.yaml for clean output', () => {
204
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
205
+ expect(result.success).toBe(true);
206
+
207
+ const content = readFileSync(join(result.agentDir, 'agent.yaml'), 'utf-8');
208
+
209
+ // tone: pragmatic is the default — should NOT appear
210
+ expect(content).not.toContain('tone:');
211
+ // setup.target: opencode is the default — should NOT appear
212
+ expect(content).not.toContain('target:');
213
+ // engine.learning: true is the default — should NOT appear
214
+ expect(content).not.toContain('learning:');
215
+ });
216
+
217
+ it('includes non-default values in agent.yaml', () => {
218
+ const result = scaffoldFileTree(
219
+ {
220
+ ...MINIMAL_CONFIG,
221
+ tone: 'precise',
222
+ setup: { target: 'claude', model: 'claude-code-opus-4' },
223
+ engine: { cognee: true },
224
+ },
225
+ tempDir,
226
+ );
227
+ expect(result.success).toBe(true);
228
+
229
+ const content = readFileSync(join(result.agentDir, 'agent.yaml'), 'utf-8');
230
+ const parsed = parseYaml(content);
231
+
232
+ expect(parsed.tone).toBe('precise');
233
+ expect(parsed.setup.target).toBe('claude');
234
+ expect(parsed.setup.model).toBe('claude-code-opus-4');
235
+ expect(parsed.engine.cognee).toBe(true);
236
+ });
237
+
238
+ it('summary says no build step needed', () => {
239
+ const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
240
+ expect(result.success).toBe(true);
241
+ expect(result.summary).toContain('No build step needed');
242
+ });
243
+ });
@@ -99,7 +99,7 @@ describe('Scaffolder', () => {
99
99
  scaffold(testConfig);
100
100
  const pkg = JSON.parse(readFileSync(join(tempDir, 'atlas', 'package.json'), 'utf-8'));
101
101
 
102
- expect(pkg.name).toBe('atlas-mcp');
102
+ expect(pkg.name).toBe('atlas');
103
103
  expect(pkg.type).toBe('module');
104
104
  expect(pkg.dependencies['@modelcontextprotocol/sdk']).toBeDefined();
105
105
  expect(pkg.dependencies['@soleri/core']).toBe('^2.0.0');
@@ -121,7 +121,7 @@ describe('Scaffolder', () => {
121
121
  expect(persona).toContain('Data quality is non-negotiable');
122
122
  });
123
123
 
124
- it('should create empty intelligence data files', () => {
124
+ it('should create seeded intelligence data files', () => {
125
125
  scaffold(testConfig);
126
126
  const dataDir = join(tempDir, 'atlas', 'src', 'intelligence', 'data');
127
127
  const files = readdirSync(dataDir);
@@ -132,7 +132,9 @@ describe('Scaffolder', () => {
132
132
 
133
133
  const bundle = JSON.parse(readFileSync(join(dataDir, 'data-pipelines.json'), 'utf-8'));
134
134
  expect(bundle.domain).toBe('data-pipelines');
135
- expect(bundle.entries).toEqual([]);
135
+ expect(bundle.entries.length).toBe(1);
136
+ expect(bundle.entries[0].id).toBe('data-pipelines-seed');
137
+ expect(bundle.entries[0].tags).toContain('seed');
136
138
  });
137
139
 
138
140
  it('should create entry point using runtime factories from @soleri/core', () => {
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Soleri v7 — File-Tree Agent Schema
3
+ *
4
+ * Defines the agent.yaml format for file-tree agents.
5
+ * This replaces the old AgentConfigSchema that generated TypeScript projects.
6
+ *
7
+ * An agent is a folder. This schema defines agent.yaml — the single source
8
+ * of truth for identity and engine configuration.
9
+ */
10
+
11
+ import { z } from 'zod';
12
+
13
+ // ─── Constants ────────────────────────────────────────────────────────
14
+
15
+ /** Communication tones */
16
+ export const TONES = ['precise', 'mentor', 'pragmatic'] as const;
17
+ export type Tone = (typeof TONES)[number];
18
+
19
+ /** Where to set up client integration */
20
+ export const SETUP_TARGETS = ['claude', 'codex', 'opencode', 'both', 'all'] as const;
21
+ export type SetupTarget = (typeof SETUP_TARGETS)[number];
22
+
23
+ // ─── Sub-Schemas ──────────────────────────────────────────────────────
24
+
25
+ /** External vault connection */
26
+ const VaultConnectionSchema = z.object({
27
+ /** Display name for this vault */
28
+ name: z.string().min(1),
29
+ /** Absolute path to vault SQLite database */
30
+ path: z.string().min(1),
31
+ /** Search priority (0–1). Higher = results ranked higher. Default: 0.5 */
32
+ priority: z.number().min(0).max(1).optional().default(0.5),
33
+ });
34
+
35
+ /** Domain pack reference */
36
+ const DomainPackSchema = z.object({
37
+ /** Domain name (e.g., "design", "code-review") */
38
+ name: z.string().min(1),
39
+ /** npm package name (e.g., "@soleri/domain-design") */
40
+ package: z.string().min(1),
41
+ /** Semver version constraint (optional) */
42
+ version: z.string().optional(),
43
+ });
44
+
45
+ /** Engine configuration */
46
+ const EngineConfigSchema = z.object({
47
+ /** Path to agent's vault SQLite database. Default: ~/.{id}/vault.db */
48
+ vault: z.string().optional(),
49
+ /** Enable brain/learning loop. Default: true */
50
+ learning: z.boolean().optional().default(true),
51
+ /** Enable Cognee vector search. Default: false */
52
+ cognee: z.boolean().optional().default(false),
53
+ });
54
+
55
+ /** Client setup configuration */
56
+ const SetupConfigSchema = z.object({
57
+ /** Target client for MCP registration */
58
+ target: z.enum(SETUP_TARGETS).optional().default('opencode'),
59
+ /** Primary model for the client */
60
+ model: z.string().optional().default('claude-code-sonnet-4'),
61
+ });
62
+
63
+ // ─── Workflow Sub-Schemas ─────────────────────────────────────────────
64
+
65
+ /** Gate phases in a workflow */
66
+ export const GATE_PHASES = ['brainstorming', 'pre-execution', 'post-task', 'completion'] as const;
67
+ export type GatePhase = (typeof GATE_PHASES)[number];
68
+
69
+ /** Workflow gate definition (maps to gates.yaml) */
70
+ export const WorkflowGateSchema = z.object({
71
+ phase: z.enum(GATE_PHASES),
72
+ requirement: z.string().min(1),
73
+ check: z.string().min(1),
74
+ });
75
+
76
+ /** Task template ordering */
77
+ export const TASK_ORDERS = ['before-implementation', 'after-implementation', 'parallel'] as const;
78
+
79
+ /** Task template types */
80
+ export const TASK_TYPES = [
81
+ 'implementation',
82
+ 'test',
83
+ 'story',
84
+ 'documentation',
85
+ 'verification',
86
+ ] as const;
87
+
88
+ /** Workflow task template (injected during plan generation) */
89
+ export const WorkflowTaskTemplateSchema = z.object({
90
+ taskType: z.enum(TASK_TYPES),
91
+ titleTemplate: z.string().min(1),
92
+ acceptanceCriteria: z.array(z.string()).optional().default([]),
93
+ tools: z.array(z.string()).optional().default([]),
94
+ order: z.enum(TASK_ORDERS),
95
+ });
96
+
97
+ /** Workflow intent types */
98
+ export const INTENTS = ['BUILD', 'FIX', 'REVIEW', 'PLAN', 'IMPROVE', 'DELIVER'] as const;
99
+ export type Intent = (typeof INTENTS)[number];
100
+
101
+ /** Workflow definition (maps to workflow folder contents) */
102
+ export const WorkflowDefinitionSchema = z.object({
103
+ /** Unique workflow ID (derived from folder name if not specified) */
104
+ id: z.string().optional(),
105
+ /** generic or domain tier */
106
+ tier: z.enum(['generic', 'domain']).optional().default('generic'),
107
+ /** Human-readable title */
108
+ title: z.string().min(1),
109
+ /** When to activate this workflow */
110
+ trigger: z.string().optional(),
111
+ /** What this workflow does */
112
+ description: z.string().optional(),
113
+ /** Numbered step-by-step process (from prompt.md, parsed at runtime) */
114
+ steps: z.string().optional(),
115
+ /** Success criteria */
116
+ expectedOutcome: z.string().optional(),
117
+ /** ID of generic workflow this domain workflow extends */
118
+ extends: z.string().optional(),
119
+ /** Domain filtering: skip UI playbooks for backend tasks */
120
+ domain: z.enum(['ui', 'backend', 'any']).optional().default('any'),
121
+ /** Intents that trigger this workflow */
122
+ matchIntents: z.array(z.enum(INTENTS)).optional().default([]),
123
+ /** Keywords in plan text that trigger this workflow */
124
+ matchKeywords: z.array(z.string()).optional().default([]),
125
+ /** Lifecycle checkpoints */
126
+ gates: z.array(WorkflowGateSchema).optional().default([]),
127
+ /** Task templates injected during plan generation */
128
+ taskTemplates: z.array(WorkflowTaskTemplateSchema).optional().default([]),
129
+ /** Tools auto-added to plan's tool chain */
130
+ toolInjections: z.array(z.string()).optional().default([]),
131
+ /** Completion gate validation rules */
132
+ verificationCriteria: z.array(z.string()).optional().default([]),
133
+ });
134
+
135
+ // ─── Main Agent Schema ────────────────────────────────────────────────
136
+
137
+ /**
138
+ * agent.yaml schema — the single source of truth for a file-tree agent.
139
+ *
140
+ * This is what `soleri create` generates and what the engine reads on startup.
141
+ */
142
+ export const AgentYamlSchema = z.object({
143
+ // ─── Identity (required) ────────────────────────
144
+ /** Agent identifier — kebab-case, used for directories and tool prefixes */
145
+ id: z.string().regex(/^[a-z][a-z0-9-]*$/, 'Must be kebab-case (e.g., "gaudi", "my-agent")'),
146
+ /** Human-readable display name */
147
+ name: z.string().min(1).max(50),
148
+ /** One-line role description */
149
+ role: z.string().min(1).max(100),
150
+ /** Longer description of capabilities */
151
+ description: z.string().min(10).max(500),
152
+ /** Knowledge domains (1–20) */
153
+ domains: z.array(z.string().min(1)).min(1).max(20),
154
+ /** Core principles (1–10) */
155
+ principles: z.array(z.string().min(1)).min(1).max(10),
156
+
157
+ // ─── Personality (optional) ─────────────────────
158
+ /** Communication tone */
159
+ tone: z.enum(TONES).optional().default('pragmatic'),
160
+ /** Greeting message (auto-generated if omitted) */
161
+ greeting: z.string().min(10).max(300).optional(),
162
+
163
+ // ─── Engine ─────────────────────────────────────
164
+ /** Knowledge engine configuration */
165
+ engine: EngineConfigSchema.optional().default({}),
166
+
167
+ // ─── Vault Connections ──────────────────────────
168
+ /** Link to external vaults for shared knowledge */
169
+ vaults: z.array(VaultConnectionSchema).optional(),
170
+
171
+ // ─── Client Setup ──────────────────────────────
172
+ /** LLM client integration settings */
173
+ setup: SetupConfigSchema.optional().default({}),
174
+
175
+ // ─── Domain Packs ──────────────────────────────
176
+ /** npm domain packs with custom ops and knowledge */
177
+ packs: z.array(DomainPackSchema).optional(),
178
+ });
179
+
180
+ export type AgentYaml = z.infer<typeof AgentYamlSchema>;
181
+ export type AgentYamlInput = z.input<typeof AgentYamlSchema>;
182
+ export type WorkflowDefinition = z.infer<typeof WorkflowDefinitionSchema>;
183
+ export type WorkflowGate = z.infer<typeof WorkflowGateSchema>;
184
+ export type WorkflowTaskTemplate = z.infer<typeof WorkflowTaskTemplateSchema>;