@soleri/forge 9.14.4 → 9.16.7
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.
- package/dist/agent-schema.d.ts +2 -2
- package/dist/compose-claude-md.js +5 -2
- package/dist/compose-claude-md.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +1 -0
- package/dist/lib.js.map +1 -1
- package/dist/scaffold-filetree.js +27 -0
- package/dist/scaffold-filetree.js.map +1 -1
- package/dist/scaffolder.js +10 -0
- package/dist/scaffolder.js.map +1 -1
- package/dist/skills/soleri-agent-dev/SKILL.md +1 -0
- package/dist/skills/soleri-agent-guide/SKILL.md +1 -0
- package/dist/skills/soleri-agent-issues/SKILL.md +1 -0
- package/dist/skills/soleri-agent-mode/SKILL.md +173 -0
- package/dist/skills/soleri-agent-persona/SKILL.md +1 -0
- package/dist/skills/soleri-brain-debrief/SKILL.md +1 -0
- package/dist/skills/soleri-brainstorming/SKILL.md +1 -0
- package/dist/skills/soleri-build-skill/SKILL.md +2 -0
- package/dist/skills/soleri-code-patrol/SKILL.md +1 -0
- package/dist/skills/soleri-context-resume/SKILL.md +1 -0
- package/dist/skills/soleri-curator/SKILL.md +66 -0
- package/dist/skills/soleri-deep-review/SKILL.md +1 -0
- package/dist/skills/soleri-deliver-and-ship/SKILL.md +1 -0
- package/dist/skills/soleri-discovery-phase/SKILL.md +1 -0
- package/dist/skills/soleri-dream/SKILL.md +31 -1
- package/dist/skills/soleri-env-setup/SKILL.md +1 -0
- package/dist/skills/soleri-executing-plans/SKILL.md +1 -0
- package/dist/skills/soleri-finishing-a-development-branch/SKILL.md +1 -0
- package/dist/skills/soleri-fix-and-learn/SKILL.md +1 -0
- package/dist/skills/soleri-health-check/SKILL.md +1 -0
- package/dist/skills/soleri-intake/SKILL.md +100 -0
- package/dist/skills/soleri-knowledge-harvest/SKILL.md +1 -0
- package/dist/skills/soleri-loop/SKILL.md +69 -0
- package/dist/skills/soleri-mcp-doctor/SKILL.md +1 -0
- package/dist/skills/soleri-onboard-me/SKILL.md +1 -0
- package/dist/skills/soleri-orchestrate/SKILL.md +70 -0
- package/dist/skills/soleri-parallel-execute/SKILL.md +1 -0
- package/dist/skills/soleri-research-scout/SKILL.md +1 -0
- package/dist/skills/soleri-retrospective/SKILL.md +1 -0
- package/dist/skills/soleri-second-opinion/SKILL.md +1 -0
- package/dist/skills/soleri-subagent-driven-development/SKILL.md +1 -0
- package/dist/skills/soleri-systematic-debugging/SKILL.md +1 -0
- package/dist/skills/soleri-test-driven-development/SKILL.md +1 -0
- package/dist/skills/soleri-using-git-worktrees/SKILL.md +1 -0
- package/dist/skills/soleri-vault-capture/SKILL.md +6 -5
- package/dist/skills/soleri-vault-curate/SKILL.md +1 -0
- package/dist/skills/soleri-vault-navigator/SKILL.md +1 -0
- package/dist/skills/soleri-vault-smells/SKILL.md +1 -0
- package/dist/skills/soleri-verification-before-completion/SKILL.md +1 -0
- package/dist/skills/soleri-writing-plans/SKILL.md +6 -3
- package/dist/skills/soleri-yolo-mode/SKILL.md +1 -0
- package/dist/templates/claude-md-template.js +2 -29
- package/dist/templates/claude-md-template.js.map +1 -1
- package/dist/templates/package-json.js +2 -0
- package/dist/templates/package-json.js.map +1 -1
- package/dist/templates/setup-script.js +6 -63
- package/dist/templates/setup-script.js.map +1 -1
- package/dist/templates/shared-rules.js +11 -4
- package/dist/templates/shared-rules.js.map +1 -1
- package/dist/templates/skills.d.ts +13 -0
- package/dist/templates/skills.js +55 -3
- package/dist/templates/skills.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/__tests__/knowledge-installer.test.ts +1 -1
- package/src/__tests__/scaffold-filetree.test.ts +1 -1
- package/src/__tests__/scaffolder.test.ts +143 -111
- package/src/compose-claude-md.ts +5 -1
- package/src/lib.ts +1 -0
- package/src/scaffold-filetree.ts +33 -0
- package/src/scaffolder.ts +10 -0
- package/src/skills/soleri-agent-dev/SKILL.md +1 -0
- package/src/skills/soleri-agent-guide/SKILL.md +1 -0
- package/src/skills/soleri-agent-issues/SKILL.md +1 -0
- package/src/skills/soleri-agent-mode/SKILL.md +173 -0
- package/src/skills/soleri-agent-persona/SKILL.md +1 -0
- package/src/skills/soleri-brain-debrief/SKILL.md +1 -0
- package/src/skills/soleri-brainstorming/SKILL.md +1 -0
- package/src/skills/soleri-build-skill/SKILL.md +2 -0
- package/src/skills/soleri-code-patrol/SKILL.md +1 -0
- package/src/skills/soleri-context-resume/SKILL.md +1 -0
- package/src/skills/soleri-curator/SKILL.md +66 -0
- package/src/skills/soleri-deep-review/SKILL.md +1 -0
- package/src/skills/soleri-deliver-and-ship/SKILL.md +1 -0
- package/src/skills/soleri-discovery-phase/SKILL.md +1 -0
- package/src/skills/soleri-dream/SKILL.md +31 -1
- package/src/skills/soleri-env-setup/SKILL.md +1 -0
- package/src/skills/soleri-executing-plans/SKILL.md +1 -0
- package/src/skills/soleri-finishing-a-development-branch/SKILL.md +1 -0
- package/src/skills/soleri-fix-and-learn/SKILL.md +1 -0
- package/src/skills/soleri-health-check/SKILL.md +1 -0
- package/src/skills/soleri-intake/SKILL.md +100 -0
- package/src/skills/soleri-knowledge-harvest/SKILL.md +1 -0
- package/src/skills/soleri-loop/SKILL.md +69 -0
- package/src/skills/soleri-mcp-doctor/SKILL.md +1 -0
- package/src/skills/soleri-onboard-me/SKILL.md +1 -0
- package/src/skills/soleri-orchestrate/SKILL.md +70 -0
- package/src/skills/soleri-parallel-execute/SKILL.md +1 -0
- package/src/skills/soleri-research-scout/SKILL.md +1 -0
- package/src/skills/soleri-retrospective/SKILL.md +1 -0
- package/src/skills/soleri-second-opinion/SKILL.md +1 -0
- package/src/skills/soleri-subagent-driven-development/SKILL.md +1 -0
- package/src/skills/soleri-systematic-debugging/SKILL.md +1 -0
- package/src/skills/soleri-test-driven-development/SKILL.md +1 -0
- package/src/skills/soleri-using-git-worktrees/SKILL.md +1 -0
- package/src/skills/soleri-vault-capture/SKILL.md +6 -5
- package/src/skills/soleri-vault-curate/SKILL.md +1 -0
- package/src/skills/soleri-vault-navigator/SKILL.md +1 -0
- package/src/skills/soleri-vault-smells/SKILL.md +1 -0
- package/src/skills/soleri-verification-before-completion/SKILL.md +1 -0
- package/src/skills/soleri-writing-plans/SKILL.md +6 -3
- package/src/skills/soleri-yolo-mode/SKILL.md +1 -0
- package/src/templates/claude-md-template.ts +2 -50
- package/src/templates/package-json.ts +2 -0
- package/src/templates/setup-script.ts +6 -63
- package/src/templates/shared-rules.ts +11 -4
- package/src/templates/skills.ts +63 -3
- package/vitest.config.ts +2 -1
|
@@ -1,48 +1,73 @@
|
|
|
1
|
-
import { describe, it, expect,
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
mkdirSync,
|
|
4
|
+
rmSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
statSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
4
12
|
import { tmpdir } from 'node:os';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const SOURCE_SKILLS_DIR = join(__dirname, '..', 'skills');
|
|
5
17
|
import { scaffold, previewScaffold, listAgents } from '../scaffolder.js';
|
|
6
18
|
import type { AgentConfig } from '../types.js';
|
|
7
19
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
mkdirSync(tempDir, { recursive: true });
|
|
30
|
-
testConfig.outputDir = tempDir;
|
|
31
|
-
});
|
|
20
|
+
const baseConfig: AgentConfig = {
|
|
21
|
+
id: 'atlas',
|
|
22
|
+
name: 'Atlas',
|
|
23
|
+
role: 'Data Engineering Advisor',
|
|
24
|
+
description:
|
|
25
|
+
'Atlas provides guidance on data pipelines, ETL patterns, and data quality practices.',
|
|
26
|
+
domains: ['data-pipelines', 'data-quality', 'etl'],
|
|
27
|
+
principles: [
|
|
28
|
+
'Data quality is non-negotiable',
|
|
29
|
+
'Idempotent pipelines always',
|
|
30
|
+
'Schema evolution over breaking changes',
|
|
31
|
+
],
|
|
32
|
+
greeting: 'Atlas here. I help with data engineering patterns and best practices.',
|
|
33
|
+
outputDir: '', // set in beforeAll
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function makeTempDir(suffix: string): string {
|
|
37
|
+
const dir = join(tmpdir(), `forge-test-${suffix}-${Date.now()}`);
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
describe('Scaffolder', () => {
|
|
43
|
+
// Two scaffolds shared across ALL inner describes — base and telegram
|
|
44
|
+
let baseDir: string;
|
|
45
|
+
let baseResult: ReturnType<typeof scaffold>;
|
|
46
|
+
let telegramDir: string;
|
|
47
|
+
let telegramResult: ReturnType<typeof scaffold>;
|
|
48
|
+
|
|
49
|
+
beforeAll(() => {
|
|
50
|
+
baseDir = makeTempDir('base');
|
|
51
|
+
baseResult = scaffold({ ...baseConfig, outputDir: baseDir });
|
|
52
|
+
|
|
53
|
+
telegramDir = makeTempDir('telegram');
|
|
54
|
+
telegramResult = scaffold({ ...baseConfig, telegram: true, outputDir: telegramDir });
|
|
55
|
+
}, 60_000);
|
|
56
|
+
|
|
57
|
+
afterAll(() => {
|
|
58
|
+
rmSync(baseDir, { recursive: true, force: true });
|
|
59
|
+
rmSync(telegramDir, { recursive: true, force: true });
|
|
35
60
|
});
|
|
36
61
|
|
|
37
62
|
describe('previewScaffold', () => {
|
|
38
63
|
it('should return preview without creating files', () => {
|
|
39
|
-
const preview = previewScaffold(
|
|
64
|
+
const preview = previewScaffold({ ...baseConfig, outputDir: baseDir });
|
|
40
65
|
|
|
41
|
-
expect(preview.agentDir).toBe(join(
|
|
66
|
+
expect(preview.agentDir).toBe(join(baseDir, 'atlas'));
|
|
42
67
|
expect(preview.persona.name).toBe('Atlas');
|
|
43
68
|
expect(preview.persona.role).toBe('Data Engineering Advisor');
|
|
44
69
|
expect(preview.domains).toEqual(['data-pipelines', 'data-quality', 'etl']);
|
|
45
|
-
expect(preview.files.length).
|
|
70
|
+
expect(preview.files.length).toBe(19);
|
|
46
71
|
|
|
47
72
|
const paths = preview.files.map((f) => f.path);
|
|
48
73
|
expect(paths).toContain('README.md');
|
|
@@ -56,7 +81,7 @@ describe('Scaffolder', () => {
|
|
|
56
81
|
expect(paths).not.toContain('src/facades/data-pipelines.facade.ts');
|
|
57
82
|
|
|
58
83
|
// Should have domain facades + core facade in preview (3 domains + semantic + agent core)
|
|
59
|
-
expect(preview.facades.length).
|
|
84
|
+
expect(preview.facades.length).toBe(13);
|
|
60
85
|
expect(preview.facades[0].name).toBe('atlas_data_pipelines');
|
|
61
86
|
|
|
62
87
|
// Agent-specific facade has 5 ops
|
|
@@ -68,24 +93,29 @@ describe('Scaffolder', () => {
|
|
|
68
93
|
const vaultFacade = preview.facades.find((f) => f.name === 'atlas_vault')!;
|
|
69
94
|
expect(vaultFacade).toBeDefined();
|
|
70
95
|
|
|
71
|
-
// Should NOT create any files
|
|
72
|
-
expect(existsSync(join(
|
|
96
|
+
// Should NOT create any files beyond what beforeAll scaffolded
|
|
97
|
+
expect(existsSync(join(baseDir, 'atlas'))).toBe(true); // beforeAll created this
|
|
73
98
|
});
|
|
74
99
|
});
|
|
75
100
|
|
|
76
101
|
describe('scaffold', () => {
|
|
77
102
|
it('should create a complete agent project', () => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
expect(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
expect(baseResult.success).toBe(true);
|
|
104
|
+
expect(baseResult.agentDir).toBe(join(baseDir, 'atlas'));
|
|
105
|
+
expect(baseResult.domains).toEqual(['data-pipelines', 'data-quality', 'etl']);
|
|
106
|
+
|
|
107
|
+
// Split: base scaffold files (stable) + one SKILL.md per source skill (dynamic)
|
|
108
|
+
const skillFiles = baseResult.filesCreated.filter((f) => f.startsWith('skills/'));
|
|
109
|
+
const baseFiles = baseResult.filesCreated.filter((f) => !f.startsWith('skills/'));
|
|
110
|
+
const sourceSkillCount = readdirSync(SOURCE_SKILLS_DIR, { withFileTypes: true }).filter((e) =>
|
|
111
|
+
e.isDirectory(),
|
|
112
|
+
).length;
|
|
113
|
+
expect(skillFiles.length).toBe(sourceSkillCount);
|
|
114
|
+
expect(baseFiles.length).toBeGreaterThan(0);
|
|
84
115
|
});
|
|
85
116
|
|
|
86
117
|
it('should create expected directories (no facades/ or llm/ dirs)', () => {
|
|
87
|
-
|
|
88
|
-
const agentDir = join(tempDir, 'atlas');
|
|
118
|
+
const agentDir = join(baseDir, 'atlas');
|
|
89
119
|
|
|
90
120
|
expect(existsSync(join(agentDir, 'src', 'intelligence', 'data'))).toBe(true);
|
|
91
121
|
expect(existsSync(join(agentDir, 'src', 'identity'))).toBe(true);
|
|
@@ -96,8 +126,7 @@ describe('Scaffolder', () => {
|
|
|
96
126
|
});
|
|
97
127
|
|
|
98
128
|
it('should create valid package.json with @soleri/core ^2.0.0', () => {
|
|
99
|
-
|
|
100
|
-
const pkg = JSON.parse(readFileSync(join(tempDir, 'atlas', 'package.json'), 'utf-8'));
|
|
129
|
+
const pkg = JSON.parse(readFileSync(join(baseDir, 'atlas', 'package.json'), 'utf-8'));
|
|
101
130
|
|
|
102
131
|
expect(pkg.name).toBe('atlas');
|
|
103
132
|
expect(pkg.type).toBe('module');
|
|
@@ -110,9 +139,8 @@ describe('Scaffolder', () => {
|
|
|
110
139
|
});
|
|
111
140
|
|
|
112
141
|
it('should create persona with correct config', () => {
|
|
113
|
-
scaffold(testConfig);
|
|
114
142
|
const persona = readFileSync(
|
|
115
|
-
join(
|
|
143
|
+
join(baseDir, 'atlas', 'src', 'identity', 'persona.ts'),
|
|
116
144
|
'utf-8',
|
|
117
145
|
);
|
|
118
146
|
|
|
@@ -122,8 +150,7 @@ describe('Scaffolder', () => {
|
|
|
122
150
|
});
|
|
123
151
|
|
|
124
152
|
it('should create seeded intelligence data files', () => {
|
|
125
|
-
|
|
126
|
-
const dataDir = join(tempDir, 'atlas', 'src', 'intelligence', 'data');
|
|
153
|
+
const dataDir = join(baseDir, 'atlas', 'src', 'intelligence', 'data');
|
|
127
154
|
const files = readdirSync(dataDir);
|
|
128
155
|
|
|
129
156
|
expect(files).toContain('data-pipelines.json');
|
|
@@ -138,8 +165,7 @@ describe('Scaffolder', () => {
|
|
|
138
165
|
});
|
|
139
166
|
|
|
140
167
|
it('should create entry point using runtime factories from @soleri/core', () => {
|
|
141
|
-
|
|
142
|
-
const entry = readFileSync(join(tempDir, 'atlas', 'src', 'index.ts'), 'utf-8');
|
|
168
|
+
const entry = readFileSync(join(baseDir, 'atlas', 'src', 'index.ts'), 'utf-8');
|
|
143
169
|
|
|
144
170
|
// v5.0 runtime factory pattern
|
|
145
171
|
expect(entry).toContain('createAgentRuntime');
|
|
@@ -159,16 +185,14 @@ describe('Scaffolder', () => {
|
|
|
159
185
|
});
|
|
160
186
|
|
|
161
187
|
it('should create .mcp.json for client config', () => {
|
|
162
|
-
|
|
163
|
-
const mcp = JSON.parse(readFileSync(join(tempDir, 'atlas', '.mcp.json'), 'utf-8'));
|
|
188
|
+
const mcp = JSON.parse(readFileSync(join(baseDir, 'atlas', '.mcp.json'), 'utf-8'));
|
|
164
189
|
|
|
165
190
|
expect(mcp.mcpServers.atlas).toBeDefined();
|
|
166
191
|
expect(mcp.mcpServers.atlas.command).toBe('node');
|
|
167
192
|
});
|
|
168
193
|
|
|
169
194
|
it('should create activation files', () => {
|
|
170
|
-
|
|
171
|
-
const activationDir = join(tempDir, 'atlas', 'src', 'activation');
|
|
195
|
+
const activationDir = join(baseDir, 'atlas', 'src', 'activation');
|
|
172
196
|
const files = readdirSync(activationDir);
|
|
173
197
|
|
|
174
198
|
expect(files).toContain('claude-md-content.ts');
|
|
@@ -177,8 +201,7 @@ describe('Scaffolder', () => {
|
|
|
177
201
|
});
|
|
178
202
|
|
|
179
203
|
it('should create activation files with correct content', () => {
|
|
180
|
-
|
|
181
|
-
const activationDir = join(tempDir, 'atlas', 'src', 'activation');
|
|
204
|
+
const activationDir = join(baseDir, 'atlas', 'src', 'activation');
|
|
182
205
|
|
|
183
206
|
const claudeMd = readFileSync(join(activationDir, 'claude-md-content.ts'), 'utf-8');
|
|
184
207
|
expect(claudeMd).toContain('atlas:mode');
|
|
@@ -195,8 +218,7 @@ describe('Scaffolder', () => {
|
|
|
195
218
|
});
|
|
196
219
|
|
|
197
220
|
it('should create README.md with agent-specific content', () => {
|
|
198
|
-
|
|
199
|
-
const readme = readFileSync(join(tempDir, 'atlas', 'README.md'), 'utf-8');
|
|
221
|
+
const readme = readFileSync(join(baseDir, 'atlas', 'README.md'), 'utf-8');
|
|
200
222
|
|
|
201
223
|
expect(readme).toContain('# Atlas');
|
|
202
224
|
expect(readme).toContain('Data Engineering Advisor');
|
|
@@ -205,8 +227,7 @@ describe('Scaffolder', () => {
|
|
|
205
227
|
});
|
|
206
228
|
|
|
207
229
|
it('should create executable setup.sh', () => {
|
|
208
|
-
|
|
209
|
-
const setupPath = join(tempDir, 'atlas', 'scripts', 'setup.sh');
|
|
230
|
+
const setupPath = join(baseDir, 'atlas', 'scripts', 'setup.sh');
|
|
210
231
|
const setup = readFileSync(setupPath, 'utf-8');
|
|
211
232
|
|
|
212
233
|
expect(setup).toContain('AGENT_NAME="atlas"');
|
|
@@ -220,9 +241,8 @@ describe('Scaffolder', () => {
|
|
|
220
241
|
});
|
|
221
242
|
|
|
222
243
|
it('should generate facade tests using runtime factories', () => {
|
|
223
|
-
scaffold(testConfig);
|
|
224
244
|
const facadesTest = readFileSync(
|
|
225
|
-
join(
|
|
245
|
+
join(baseDir, 'atlas', 'src', '__tests__', 'facades.test.ts'),
|
|
226
246
|
'utf-8',
|
|
227
247
|
);
|
|
228
248
|
|
|
@@ -247,18 +267,17 @@ describe('Scaffolder', () => {
|
|
|
247
267
|
});
|
|
248
268
|
|
|
249
269
|
it('should fail if directory already exists', () => {
|
|
250
|
-
|
|
251
|
-
const
|
|
270
|
+
// beforeAll already scaffolded to baseDir — attempt on same dir should fail
|
|
271
|
+
const duplicate = scaffold({ ...baseConfig, outputDir: baseDir });
|
|
252
272
|
|
|
253
|
-
expect(
|
|
254
|
-
expect(
|
|
273
|
+
expect(duplicate.success).toBe(false);
|
|
274
|
+
expect(duplicate.summary).toContain('already exists');
|
|
255
275
|
});
|
|
256
276
|
});
|
|
257
277
|
|
|
258
278
|
describe('skills', () => {
|
|
259
|
-
it('should create skills directory with
|
|
260
|
-
|
|
261
|
-
const skillsDir = join(tempDir, 'atlas', 'skills');
|
|
279
|
+
it('should create skills directory with SKILL.md files', () => {
|
|
280
|
+
const skillsDir = join(baseDir, 'atlas', 'skills');
|
|
262
281
|
|
|
263
282
|
expect(existsSync(skillsDir)).toBe(true);
|
|
264
283
|
|
|
@@ -266,7 +285,10 @@ describe('Scaffolder', () => {
|
|
|
266
285
|
.filter((e) => e.isDirectory())
|
|
267
286
|
.map((e) => e.name);
|
|
268
287
|
|
|
269
|
-
|
|
288
|
+
const expectedCount = readdirSync(SOURCE_SKILLS_DIR, { withFileTypes: true }).filter((e) =>
|
|
289
|
+
e.isDirectory(),
|
|
290
|
+
).length;
|
|
291
|
+
expect(skillDirs.length).toBe(expectedCount);
|
|
270
292
|
|
|
271
293
|
// Verify each skill dir has a SKILL.md
|
|
272
294
|
for (const dir of skillDirs) {
|
|
@@ -275,8 +297,7 @@ describe('Scaffolder', () => {
|
|
|
275
297
|
});
|
|
276
298
|
|
|
277
299
|
it('should include core expected skill names', () => {
|
|
278
|
-
|
|
279
|
-
const skillsDir = join(tempDir, 'atlas', 'skills');
|
|
300
|
+
const skillsDir = join(baseDir, 'atlas', 'skills');
|
|
280
301
|
const skillDirs = readdirSync(skillsDir).sort();
|
|
281
302
|
|
|
282
303
|
// Check essential skills exist (not an exhaustive list — skills are added over time)
|
|
@@ -293,8 +314,7 @@ describe('Scaffolder', () => {
|
|
|
293
314
|
});
|
|
294
315
|
|
|
295
316
|
it('should have YAML frontmatter in all skills', () => {
|
|
296
|
-
|
|
297
|
-
const skillsDir = join(tempDir, 'atlas', 'skills');
|
|
317
|
+
const skillsDir = join(baseDir, 'atlas', 'skills');
|
|
298
318
|
const skillDirs = readdirSync(skillsDir);
|
|
299
319
|
|
|
300
320
|
for (const dir of skillDirs) {
|
|
@@ -305,8 +325,7 @@ describe('Scaffolder', () => {
|
|
|
305
325
|
});
|
|
306
326
|
|
|
307
327
|
it('should substitute YOUR_AGENT_core with agent ID in all skills', () => {
|
|
308
|
-
|
|
309
|
-
const skillsDir = join(tempDir, 'atlas', 'skills');
|
|
328
|
+
const skillsDir = join(baseDir, 'atlas', 'skills');
|
|
310
329
|
const allSkills = readdirSync(skillsDir);
|
|
311
330
|
|
|
312
331
|
for (const name of allSkills) {
|
|
@@ -320,8 +339,7 @@ describe('Scaffolder', () => {
|
|
|
320
339
|
});
|
|
321
340
|
|
|
322
341
|
it('should have valid content in superpowers-adapted skills', () => {
|
|
323
|
-
|
|
324
|
-
const skillsDir = join(tempDir, 'atlas', 'skills');
|
|
342
|
+
const skillsDir = join(baseDir, 'atlas', 'skills');
|
|
325
343
|
const superpowersSkills = ['soleri-brainstorming', 'soleri-executing-plans'];
|
|
326
344
|
|
|
327
345
|
for (const name of superpowersSkills) {
|
|
@@ -334,8 +352,7 @@ describe('Scaffolder', () => {
|
|
|
334
352
|
});
|
|
335
353
|
|
|
336
354
|
it('should have no YOUR_AGENT_core placeholder remaining in any skill', () => {
|
|
337
|
-
|
|
338
|
-
const skillsDir = join(tempDir, 'atlas', 'skills');
|
|
355
|
+
const skillsDir = join(baseDir, 'atlas', 'skills');
|
|
339
356
|
const allSkills = readdirSync(skillsDir);
|
|
340
357
|
|
|
341
358
|
for (const name of allSkills) {
|
|
@@ -345,22 +362,46 @@ describe('Scaffolder', () => {
|
|
|
345
362
|
});
|
|
346
363
|
|
|
347
364
|
it('should include skills in preview', () => {
|
|
348
|
-
const preview = previewScaffold(
|
|
365
|
+
const preview = previewScaffold({ ...baseConfig, outputDir: baseDir });
|
|
349
366
|
const paths = preview.files.map((f) => f.path);
|
|
350
367
|
expect(paths).toContain('skills/');
|
|
351
368
|
});
|
|
352
369
|
|
|
353
370
|
it('should mention skills in scaffold summary', () => {
|
|
354
|
-
|
|
355
|
-
|
|
371
|
+
expect(baseResult.summary).toContain('built-in skills');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should sync generated skills into .claude/skills/', () => {
|
|
375
|
+
const agentDir = join(baseDir, 'atlas');
|
|
376
|
+
const claudeSkillsDir = join(agentDir, '.claude', 'skills');
|
|
377
|
+
|
|
378
|
+
expect(existsSync(claudeSkillsDir)).toBe(true);
|
|
379
|
+
|
|
380
|
+
// Project-local sync uses symlinks, so check for both directories and symlinks
|
|
381
|
+
const syncedSkills = readdirSync(claudeSkillsDir, { withFileTypes: true })
|
|
382
|
+
.filter((e) => {
|
|
383
|
+
if (e.isDirectory()) return true;
|
|
384
|
+
// lstat to detect symlinks (readdirSync follows symlinks for isDirectory())
|
|
385
|
+
try {
|
|
386
|
+
return lstatSync(join(claudeSkillsDir, e.name)).isSymbolicLink();
|
|
387
|
+
} catch {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
.map((e) => e.name);
|
|
392
|
+
|
|
393
|
+
expect(syncedSkills.length).toBeGreaterThan(0);
|
|
394
|
+
|
|
395
|
+
// Every synced skill should have a SKILL.md (existsSync follows symlinks)
|
|
396
|
+
for (const name of syncedSkills) {
|
|
397
|
+
expect(existsSync(join(claudeSkillsDir, name, 'SKILL.md'))).toBe(true);
|
|
398
|
+
}
|
|
356
399
|
});
|
|
357
400
|
});
|
|
358
401
|
|
|
359
402
|
describe('listAgents', () => {
|
|
360
403
|
it('should list scaffolded agents', () => {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const agents = listAgents(tempDir);
|
|
404
|
+
const agents = listAgents(baseDir);
|
|
364
405
|
expect(agents).toHaveLength(1);
|
|
365
406
|
expect(agents[0].id).toBe('atlas');
|
|
366
407
|
expect(agents[0].domains).toEqual(['data-pipelines', 'data-quality', 'etl']);
|
|
@@ -373,14 +414,8 @@ describe('Scaffolder', () => {
|
|
|
373
414
|
});
|
|
374
415
|
|
|
375
416
|
describe('telegram scaffolding', () => {
|
|
376
|
-
const telegramConfig: AgentConfig = {
|
|
377
|
-
...testConfig,
|
|
378
|
-
telegram: true,
|
|
379
|
-
};
|
|
380
|
-
|
|
381
417
|
it('should include telegram files in preview', () => {
|
|
382
|
-
|
|
383
|
-
const preview = previewScaffold(telegramConfig);
|
|
418
|
+
const preview = previewScaffold({ ...baseConfig, telegram: true, outputDir: telegramDir });
|
|
384
419
|
const paths = preview.files.map((f) => f.path);
|
|
385
420
|
expect(paths).toContain('src/telegram-bot.ts');
|
|
386
421
|
expect(paths).toContain('src/telegram-config.ts');
|
|
@@ -389,27 +424,28 @@ describe('Scaffolder', () => {
|
|
|
389
424
|
});
|
|
390
425
|
|
|
391
426
|
it('should not include telegram files without flag', () => {
|
|
392
|
-
const preview = previewScaffold(
|
|
427
|
+
const preview = previewScaffold({ ...baseConfig, outputDir: baseDir });
|
|
393
428
|
const paths = preview.files.map((f) => f.path);
|
|
394
429
|
expect(paths).not.toContain('src/telegram-bot.ts');
|
|
395
430
|
});
|
|
396
431
|
|
|
397
432
|
it('should generate telegram source files', () => {
|
|
398
|
-
telegramConfig.outputDir = tempDir;
|
|
399
|
-
const result = scaffold(telegramConfig);
|
|
400
433
|
// Build may fail (grammy not installed) but files should be created
|
|
401
|
-
expect(
|
|
402
|
-
expect(
|
|
403
|
-
expect(
|
|
404
|
-
expect(
|
|
434
|
+
expect(telegramResult.filesCreated).toContain('src/telegram-bot.ts');
|
|
435
|
+
expect(telegramResult.filesCreated).toContain('src/telegram-config.ts');
|
|
436
|
+
expect(telegramResult.filesCreated).toContain('src/telegram-agent.ts');
|
|
437
|
+
expect(telegramResult.filesCreated).toContain('src/telegram-supervisor.ts');
|
|
405
438
|
|
|
406
439
|
// Verify file contents reference the agent
|
|
407
|
-
const botContent = readFileSync(
|
|
440
|
+
const botContent = readFileSync(
|
|
441
|
+
join(telegramDir, 'atlas', 'src', 'telegram-bot.ts'),
|
|
442
|
+
'utf-8',
|
|
443
|
+
);
|
|
408
444
|
expect(botContent).toContain('Atlas');
|
|
409
445
|
expect(botContent).toContain('grammy');
|
|
410
446
|
|
|
411
447
|
const configContent = readFileSync(
|
|
412
|
-
join(
|
|
448
|
+
join(telegramDir, 'atlas', 'src', 'telegram-config.ts'),
|
|
413
449
|
'utf-8',
|
|
414
450
|
);
|
|
415
451
|
expect(configContent).toContain('.atlas');
|
|
@@ -417,20 +453,16 @@ describe('Scaffolder', () => {
|
|
|
417
453
|
});
|
|
418
454
|
|
|
419
455
|
it('should include grammy dependency in package.json', () => {
|
|
420
|
-
|
|
421
|
-
scaffold(telegramConfig);
|
|
422
|
-
const pkg = JSON.parse(readFileSync(join(tempDir, 'atlas', 'package.json'), 'utf-8'));
|
|
456
|
+
const pkg = JSON.parse(readFileSync(join(telegramDir, 'atlas', 'package.json'), 'utf-8'));
|
|
423
457
|
expect(pkg.dependencies.grammy).toBeDefined();
|
|
424
458
|
expect(pkg.scripts['telegram:start']).toBeDefined();
|
|
425
459
|
expect(pkg.scripts['telegram:dev']).toBeDefined();
|
|
426
460
|
});
|
|
427
461
|
|
|
428
462
|
it('should not include grammy without telegram flag', () => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
expect(pkg.dependencies.grammy).toBeUndefined();
|
|
433
|
-
}
|
|
463
|
+
// baseResult was scaffolded without telegram flag — use it for this assertion
|
|
464
|
+
const pkg = JSON.parse(readFileSync(join(baseDir, 'atlas', 'package.json'), 'utf-8'));
|
|
465
|
+
expect(pkg.dependencies.grammy).toBeUndefined();
|
|
434
466
|
});
|
|
435
467
|
});
|
|
436
468
|
});
|
package/src/compose-claude-md.ts
CHANGED
|
@@ -407,7 +407,11 @@ function composeWorkflowIndex(workflowsDir: string): string | null {
|
|
|
407
407
|
const content = readFileSync(promptPath, 'utf-8');
|
|
408
408
|
// Extract first non-heading, non-empty line as description
|
|
409
409
|
const descLine = content.split('\n').find((line) => line.trim() && !line.startsWith('#'));
|
|
410
|
-
if (descLine)
|
|
410
|
+
if (descLine) {
|
|
411
|
+
const trimmed = descLine.trim();
|
|
412
|
+
description =
|
|
413
|
+
trimmed.length > 120 ? trimmed.slice(0, 117).replace(/\s+\S*$/, '') + '...' : trimmed;
|
|
414
|
+
}
|
|
411
415
|
}
|
|
412
416
|
|
|
413
417
|
lines.push(`| \`${dir.name}\` | ${description} |`);
|
package/src/lib.ts
CHANGED
|
@@ -42,6 +42,7 @@ export {
|
|
|
42
42
|
} from './templates/shared-rules.js';
|
|
43
43
|
export type { EngineFeature } from './templates/shared-rules.js';
|
|
44
44
|
export { generateInjectClaudeMd } from './templates/inject-claude-md.js';
|
|
45
|
+
export { generateAgentsMd } from './templates/agents-md.js';
|
|
45
46
|
export { generateSkills } from './templates/skills.js';
|
|
46
47
|
export { generateTelegramBot } from './templates/telegram-bot.js';
|
|
47
48
|
export { generateTelegramAgent } from './templates/telegram-agent.js';
|
package/src/scaffold-filetree.ts
CHANGED
|
@@ -581,6 +581,36 @@ See [Hook Packs documentation](https://soleri.dev/docs/guides/pack-authoring/) f
|
|
|
581
581
|
};
|
|
582
582
|
writeFile(agentDir, '.mcp.json', JSON.stringify(mcpJson, null, 2) + '\n', filesCreated);
|
|
583
583
|
|
|
584
|
+
// ─── 3a. Write settings.local.json (Claude Code hooks + pre-approved permissions) ─────
|
|
585
|
+
const settingsLocal = {
|
|
586
|
+
permissions: {
|
|
587
|
+
allow: [
|
|
588
|
+
// Pre-approve all facades for this agent so users skip approval prompts
|
|
589
|
+
// on routine ops (session_start, vault search, orchestrate_plan, etc.)
|
|
590
|
+
`mcp__${config.id}__*`,
|
|
591
|
+
],
|
|
592
|
+
},
|
|
593
|
+
hooks: {
|
|
594
|
+
Stop: [
|
|
595
|
+
{
|
|
596
|
+
matcher: '',
|
|
597
|
+
hooks: [
|
|
598
|
+
{
|
|
599
|
+
type: 'command',
|
|
600
|
+
command: 'soleri brain close-orphans --max-age 1h',
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
},
|
|
604
|
+
],
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
writeFile(
|
|
608
|
+
agentDir,
|
|
609
|
+
'settings.local.json',
|
|
610
|
+
JSON.stringify(settingsLocal, null, 2) + '\n',
|
|
611
|
+
filesCreated,
|
|
612
|
+
);
|
|
613
|
+
|
|
584
614
|
// ─── 3b. Write .opencode.json (OpenCode uses "mcp" not "mcpServers", type "local" not "stdio", command as array) ──
|
|
585
615
|
const opencodeJson = {
|
|
586
616
|
$schema: 'https://opencode.ai/config.json',
|
|
@@ -604,6 +634,9 @@ See [Hook Packs documentation](https://soleri.dev/docs/guides/pack-authoring/) f
|
|
|
604
634
|
'AGENTS.md',
|
|
605
635
|
'instructions/_engine.md',
|
|
606
636
|
'',
|
|
637
|
+
'# Local config — machine-specific, do not commit',
|
|
638
|
+
'settings.local.json',
|
|
639
|
+
'',
|
|
607
640
|
'# OS',
|
|
608
641
|
'.DS_Store',
|
|
609
642
|
'',
|
package/src/scaffolder.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { generateTelegramConfig } from './templates/telegram-config.js';
|
|
|
37
37
|
import { generateTelegramAgent } from './templates/telegram-agent.js';
|
|
38
38
|
import { generateTelegramSupervisor } from './templates/telegram-supervisor.js';
|
|
39
39
|
import { detectInstalledDomainPacks } from './utils/detect-domain-packs.js';
|
|
40
|
+
import { syncSkillsToClaudeCode } from '@soleri/core';
|
|
40
41
|
|
|
41
42
|
function getSetupTarget(config: AgentConfig): SetupTarget {
|
|
42
43
|
return config.setupTarget ?? 'claude';
|
|
@@ -454,6 +455,15 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
|
|
|
454
455
|
filesCreated.push(path);
|
|
455
456
|
}
|
|
456
457
|
|
|
458
|
+
// Sync generated skills into .claude/skills/ for immediate Claude Code discovery
|
|
459
|
+
try {
|
|
460
|
+
const agentSkillsDir = join(agentDir, 'skills');
|
|
461
|
+
syncSkillsToClaudeCode([agentSkillsDir], config.name, { projectRoot: agentDir });
|
|
462
|
+
} catch (err) {
|
|
463
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
464
|
+
console.error(`[forge] Warning: Failed to sync skills to .claude/skills/: ${msg}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
457
467
|
const totalOps = config.domains.length * 5 + 214; // 5 per domain + 209 semantic + 5 agent-specific
|
|
458
468
|
|
|
459
469
|
// Auto-build: install dependencies and compile before registering MCP
|