@soleri/forge 9.7.2 → 9.8.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.
- package/dist/agent-schema.d.ts +177 -6
- package/dist/agent-schema.js +58 -0
- package/dist/agent-schema.js.map +1 -1
- package/dist/compose-claude-md.js +56 -3
- package/dist/compose-claude-md.js.map +1 -1
- package/dist/domain-manager.d.ts +1 -0
- package/dist/domain-manager.js +57 -1
- package/dist/domain-manager.js.map +1 -1
- package/dist/knowledge-installer.d.ts +2 -0
- package/dist/knowledge-installer.js +107 -1
- package/dist/knowledge-installer.js.map +1 -1
- package/dist/lib.d.ts +1 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/scaffold-filetree.d.ts +12 -0
- package/dist/scaffold-filetree.js +332 -2
- package/dist/scaffold-filetree.js.map +1 -1
- package/dist/scaffolder.js +12 -0
- package/dist/scaffolder.js.map +1 -1
- package/dist/templates/setup-script.js +71 -0
- package/dist/templates/setup-script.js.map +1 -1
- package/dist/templates/shared-rules.js +52 -6
- package/dist/templates/shared-rules.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/domain-manager.test.ts +140 -0
- package/src/__tests__/scaffold-filetree.test.ts +282 -1
- package/src/__tests__/shared-rules.test.ts +48 -0
- package/src/agent-schema.ts +66 -0
- package/src/compose-claude-md.ts +63 -3
- package/src/domain-manager.ts +74 -1
- package/src/knowledge-installer.ts +124 -1
- package/src/lib.ts +6 -1
- package/src/scaffold-filetree.ts +380 -2
- package/src/scaffolder.ts +17 -0
- package/src/templates/setup-script.ts +71 -0
- package/src/templates/shared-rules.ts +53 -6
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
9
|
-
import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { tmpdir } from 'node:os';
|
|
12
12
|
import { parse as parseYaml } from 'yaml';
|
|
@@ -197,6 +197,31 @@ describe('scaffoldFileTree', () => {
|
|
|
197
197
|
expect(gitignore).toContain('_engine.md');
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
+
it('generates conventions.md example instruction file', () => {
|
|
201
|
+
const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
|
|
202
|
+
expect(result.success).toBe(true);
|
|
203
|
+
|
|
204
|
+
const content = readFileSync(join(result.agentDir, 'instructions', 'conventions.md'), 'utf-8');
|
|
205
|
+
expect(content).toContain('# Conventions');
|
|
206
|
+
expect(content).toContain('Naming Conventions');
|
|
207
|
+
expect(content).toContain('What to Avoid');
|
|
208
|
+
expect(content).toContain('kebab-case');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('generates getting-started.md example instruction file', () => {
|
|
212
|
+
const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
|
|
213
|
+
expect(result.success).toBe(true);
|
|
214
|
+
|
|
215
|
+
const content = readFileSync(
|
|
216
|
+
join(result.agentDir, 'instructions', 'getting-started.md'),
|
|
217
|
+
'utf-8',
|
|
218
|
+
);
|
|
219
|
+
expect(content).toContain('Getting Started with Instructions');
|
|
220
|
+
expect(content).toContain('_engine.md');
|
|
221
|
+
expect(content).toContain('soleri dev');
|
|
222
|
+
expect(content).toContain('alphabetical order');
|
|
223
|
+
});
|
|
224
|
+
|
|
200
225
|
it('fails if directory already exists', () => {
|
|
201
226
|
scaffoldFileTree(MINIMAL_CONFIG, tempDir);
|
|
202
227
|
const result2 = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
|
|
@@ -252,4 +277,260 @@ describe('scaffoldFileTree', () => {
|
|
|
252
277
|
expect(result.success).toBe(true);
|
|
253
278
|
expect(result.summary).toContain('No build step needed');
|
|
254
279
|
});
|
|
280
|
+
|
|
281
|
+
it('generates user.md with placeholder content', () => {
|
|
282
|
+
const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
|
|
283
|
+
expect(result.success).toBe(true);
|
|
284
|
+
|
|
285
|
+
const userMdPath = join(result.agentDir, 'instructions', 'user.md');
|
|
286
|
+
expect(existsSync(userMdPath)).toBe(true);
|
|
287
|
+
|
|
288
|
+
const content = readFileSync(userMdPath, 'utf-8');
|
|
289
|
+
expect(content).toContain('# Your Custom Rules');
|
|
290
|
+
expect(content).toContain('priority placement in CLAUDE.md');
|
|
291
|
+
expect(content).toContain('Delete these instructions and replace with your own content.');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('includes user.md in filesCreated', () => {
|
|
295
|
+
const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
|
|
296
|
+
expect(result.success).toBe(true);
|
|
297
|
+
expect(result.filesCreated).toContain('instructions/user.md');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('places user.md content before engine rules ref in CLAUDE.md', () => {
|
|
301
|
+
const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
|
|
302
|
+
expect(result.success).toBe(true);
|
|
303
|
+
|
|
304
|
+
const claudeMd = readFileSync(join(result.agentDir, 'CLAUDE.md'), 'utf-8');
|
|
305
|
+
const userPos = claudeMd.indexOf('# Your Custom Rules');
|
|
306
|
+
const enginePos = claudeMd.indexOf('soleri:engine-rules-ref');
|
|
307
|
+
|
|
308
|
+
expect(userPos).toBeGreaterThan(-1);
|
|
309
|
+
expect(enginePos).toBeGreaterThan(-1);
|
|
310
|
+
expect(userPos).toBeLessThan(enginePos);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('does not duplicate user.md in the alphabetical instructions section', () => {
|
|
314
|
+
const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
|
|
315
|
+
expect(result.success).toBe(true);
|
|
316
|
+
|
|
317
|
+
const claudeMd = readFileSync(join(result.agentDir, 'CLAUDE.md'), 'utf-8');
|
|
318
|
+
// user.md content should appear exactly once
|
|
319
|
+
const matches = claudeMd.match(/# Your Custom Rules/g);
|
|
320
|
+
expect(matches).toHaveLength(1);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ─── Skills Filter Tests ─────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
it('default scaffold creates only essential skills (~7)', () => {
|
|
326
|
+
const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
|
|
327
|
+
expect(result.success).toBe(true);
|
|
328
|
+
|
|
329
|
+
const skillDirs = readdirSync(join(result.agentDir, 'skills'), { withFileTypes: true })
|
|
330
|
+
.filter((d) => d.isDirectory())
|
|
331
|
+
.map((d) => d.name);
|
|
332
|
+
|
|
333
|
+
// Should have ~7 essential skills, not 30+
|
|
334
|
+
expect(skillDirs.length).toBeGreaterThanOrEqual(5);
|
|
335
|
+
expect(skillDirs.length).toBeLessThanOrEqual(10);
|
|
336
|
+
|
|
337
|
+
// Essential skills should be present
|
|
338
|
+
expect(skillDirs).toContain('agent-guide');
|
|
339
|
+
expect(skillDirs).toContain('vault-navigator');
|
|
340
|
+
expect(skillDirs).toContain('vault-capture');
|
|
341
|
+
expect(skillDirs).toContain('systematic-debugging');
|
|
342
|
+
expect(skillDirs).toContain('writing-plans');
|
|
343
|
+
expect(skillDirs).toContain('context-resume');
|
|
344
|
+
expect(skillDirs).toContain('agent-persona');
|
|
345
|
+
|
|
346
|
+
// Optional skills should NOT be present
|
|
347
|
+
expect(skillDirs).not.toContain('brainstorming');
|
|
348
|
+
expect(skillDirs).not.toContain('deep-review');
|
|
349
|
+
expect(skillDirs).not.toContain('code-patrol');
|
|
350
|
+
expect(skillDirs).not.toContain('yolo-mode');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('skillsFilter: "all" creates all skills', () => {
|
|
354
|
+
const result = scaffoldFileTree(
|
|
355
|
+
{ ...MINIMAL_CONFIG, id: 'all-skills', skillsFilter: 'all' },
|
|
356
|
+
tempDir,
|
|
357
|
+
);
|
|
358
|
+
expect(result.success).toBe(true);
|
|
359
|
+
|
|
360
|
+
const skillDirs = readdirSync(join(result.agentDir, 'skills'), { withFileTypes: true })
|
|
361
|
+
.filter((d) => d.isDirectory())
|
|
362
|
+
.map((d) => d.name);
|
|
363
|
+
|
|
364
|
+
// Should have all 30+ skills
|
|
365
|
+
expect(skillDirs.length).toBeGreaterThanOrEqual(25);
|
|
366
|
+
expect(skillDirs).toContain('brainstorming');
|
|
367
|
+
expect(skillDirs).toContain('deep-review');
|
|
368
|
+
expect(skillDirs).toContain('yolo-mode');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('skillsFilter: explicit array creates exactly those skills', () => {
|
|
372
|
+
const result = scaffoldFileTree(
|
|
373
|
+
{ ...MINIMAL_CONFIG, id: 'custom-skills', skillsFilter: ['vault-navigator', 'agent-guide'] },
|
|
374
|
+
tempDir,
|
|
375
|
+
);
|
|
376
|
+
expect(result.success).toBe(true);
|
|
377
|
+
|
|
378
|
+
const skillDirs = readdirSync(join(result.agentDir, 'skills'), { withFileTypes: true })
|
|
379
|
+
.filter((d) => d.isDirectory())
|
|
380
|
+
.map((d) => d.name);
|
|
381
|
+
|
|
382
|
+
expect(skillDirs).toEqual(['agent-guide', 'vault-navigator']);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('CLAUDE.md only lists on-disk skills', () => {
|
|
386
|
+
// Default scaffold = essential only
|
|
387
|
+
const result = scaffoldFileTree({ ...MINIMAL_CONFIG, id: 'claude-md-skills' }, tempDir);
|
|
388
|
+
expect(result.success).toBe(true);
|
|
389
|
+
|
|
390
|
+
const claudeMd = readFileSync(join(result.agentDir, 'CLAUDE.md'), 'utf-8');
|
|
391
|
+
|
|
392
|
+
// Essential skills should appear
|
|
393
|
+
expect(claudeMd).toContain('vault-navigator');
|
|
394
|
+
expect(claudeMd).toContain('agent-guide');
|
|
395
|
+
|
|
396
|
+
// Optional skills should NOT appear (not on disk)
|
|
397
|
+
expect(claudeMd).not.toContain('brainstorming');
|
|
398
|
+
expect(claudeMd).not.toContain('yolo-mode');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('skillsFilter default (essential) is not written to agent.yaml', () => {
|
|
402
|
+
const result = scaffoldFileTree(MINIMAL_CONFIG, tempDir);
|
|
403
|
+
expect(result.success).toBe(true);
|
|
404
|
+
|
|
405
|
+
const content = readFileSync(join(result.agentDir, 'agent.yaml'), 'utf-8');
|
|
406
|
+
expect(content).not.toContain('skillsFilter');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('skillsFilter: "all" IS written to agent.yaml', () => {
|
|
410
|
+
const result = scaffoldFileTree(
|
|
411
|
+
{ ...MINIMAL_CONFIG, id: 'written-filter', skillsFilter: 'all' },
|
|
412
|
+
tempDir,
|
|
413
|
+
);
|
|
414
|
+
expect(result.success).toBe(true);
|
|
415
|
+
|
|
416
|
+
const content = readFileSync(join(result.agentDir, 'agent.yaml'), 'utf-8');
|
|
417
|
+
const parsed = parseYaml(content);
|
|
418
|
+
expect(parsed.skillsFilter).toBe('all');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ─── Workspace & Routing Tests ─────────────────────────────
|
|
422
|
+
|
|
423
|
+
it('creates workspace directories with CONTEXT.md when workspaces defined', () => {
|
|
424
|
+
const result = scaffoldFileTree(
|
|
425
|
+
{
|
|
426
|
+
...MINIMAL_CONFIG,
|
|
427
|
+
workspaces: [
|
|
428
|
+
{ id: 'design', name: 'Design', description: 'Design workspace' },
|
|
429
|
+
{ id: 'review', name: 'Review', description: 'Review workspace' },
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
tempDir,
|
|
433
|
+
);
|
|
434
|
+
expect(result.success).toBe(true);
|
|
435
|
+
|
|
436
|
+
// Workspace directories and CONTEXT.md files exist
|
|
437
|
+
expect(existsSync(join(result.agentDir, 'workspaces', 'design', 'CONTEXT.md'))).toBe(true);
|
|
438
|
+
expect(existsSync(join(result.agentDir, 'workspaces', 'review', 'CONTEXT.md'))).toBe(true);
|
|
439
|
+
|
|
440
|
+
// CONTEXT.md contains workspace name and description
|
|
441
|
+
const content = readFileSync(
|
|
442
|
+
join(result.agentDir, 'workspaces', 'design', 'CONTEXT.md'),
|
|
443
|
+
'utf-8',
|
|
444
|
+
);
|
|
445
|
+
expect(content).toContain('# Design');
|
|
446
|
+
expect(content).toContain('Design workspace');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('seeds default workspaces from domains when no explicit workspaces', () => {
|
|
450
|
+
const result = scaffoldFileTree(
|
|
451
|
+
{
|
|
452
|
+
...MINIMAL_CONFIG,
|
|
453
|
+
domains: ['architecture'],
|
|
454
|
+
},
|
|
455
|
+
tempDir,
|
|
456
|
+
);
|
|
457
|
+
expect(result.success).toBe(true);
|
|
458
|
+
|
|
459
|
+
// Architecture domain seeds planning, src, docs workspaces
|
|
460
|
+
expect(existsSync(join(result.agentDir, 'workspaces', 'planning', 'CONTEXT.md'))).toBe(true);
|
|
461
|
+
expect(existsSync(join(result.agentDir, 'workspaces', 'src', 'CONTEXT.md'))).toBe(true);
|
|
462
|
+
expect(existsSync(join(result.agentDir, 'workspaces', 'docs', 'CONTEXT.md'))).toBe(true);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('includes routing entries in agent.yaml', () => {
|
|
466
|
+
const result = scaffoldFileTree(
|
|
467
|
+
{
|
|
468
|
+
...MINIMAL_CONFIG,
|
|
469
|
+
workspaces: [{ id: 'src', name: 'Source', description: 'Source code' }],
|
|
470
|
+
routing: [{ pattern: 'implement feature', workspace: 'src', skills: ['tdd'] }],
|
|
471
|
+
},
|
|
472
|
+
tempDir,
|
|
473
|
+
);
|
|
474
|
+
expect(result.success).toBe(true);
|
|
475
|
+
|
|
476
|
+
const content = readFileSync(join(result.agentDir, 'agent.yaml'), 'utf-8');
|
|
477
|
+
const parsed = parseYaml(content);
|
|
478
|
+
|
|
479
|
+
expect(parsed.workspaces).toHaveLength(1);
|
|
480
|
+
expect(parsed.workspaces[0].id).toBe('src');
|
|
481
|
+
expect(parsed.routing).toHaveLength(1);
|
|
482
|
+
expect(parsed.routing[0].pattern).toBe('implement feature');
|
|
483
|
+
expect(parsed.routing[0].workspace).toBe('src');
|
|
484
|
+
expect(parsed.routing[0].skills).toEqual(['tdd']);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('creates no workspaces directory when no workspaces and no matching domains', () => {
|
|
488
|
+
const result = scaffoldFileTree(
|
|
489
|
+
{
|
|
490
|
+
...MINIMAL_CONFIG,
|
|
491
|
+
domains: ['testing', 'quality'], // no workspace seeds for these
|
|
492
|
+
},
|
|
493
|
+
tempDir,
|
|
494
|
+
);
|
|
495
|
+
expect(result.success).toBe(true);
|
|
496
|
+
|
|
497
|
+
// No workspaces directory
|
|
498
|
+
expect(existsSync(join(result.agentDir, 'workspaces'))).toBe(false);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('includes workspaces and routing sections in CLAUDE.md when defined', () => {
|
|
502
|
+
const result = scaffoldFileTree(
|
|
503
|
+
{
|
|
504
|
+
...MINIMAL_CONFIG,
|
|
505
|
+
workspaces: [{ id: 'design', name: 'Design', description: 'Design patterns' }],
|
|
506
|
+
routing: [
|
|
507
|
+
{ pattern: 'design component', workspace: 'design', skills: ['vault-navigator'] },
|
|
508
|
+
],
|
|
509
|
+
},
|
|
510
|
+
tempDir,
|
|
511
|
+
);
|
|
512
|
+
expect(result.success).toBe(true);
|
|
513
|
+
|
|
514
|
+
const claudeMd = readFileSync(join(result.agentDir, 'CLAUDE.md'), 'utf-8');
|
|
515
|
+
expect(claudeMd).toContain('## Workspaces');
|
|
516
|
+
expect(claudeMd).toContain('Design patterns');
|
|
517
|
+
expect(claudeMd).toContain('## Task Routing');
|
|
518
|
+
expect(claudeMd).toContain('design component');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('omits workspaces and routing sections from CLAUDE.md when not defined', () => {
|
|
522
|
+
// Use domains with no workspace seeds
|
|
523
|
+
const result = scaffoldFileTree(
|
|
524
|
+
{
|
|
525
|
+
...MINIMAL_CONFIG,
|
|
526
|
+
domains: ['testing', 'quality'],
|
|
527
|
+
},
|
|
528
|
+
tempDir,
|
|
529
|
+
);
|
|
530
|
+
expect(result.success).toBe(true);
|
|
531
|
+
|
|
532
|
+
const claudeMd = readFileSync(join(result.agentDir, 'CLAUDE.md'), 'utf-8');
|
|
533
|
+
expect(claudeMd).not.toContain('## Workspaces');
|
|
534
|
+
expect(claudeMd).not.toContain('## Task Routing');
|
|
535
|
+
});
|
|
255
536
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getEngineRulesContent, getEngineMarker } from '../templates/shared-rules.js';
|
|
3
|
+
|
|
4
|
+
describe('shared-rules', () => {
|
|
5
|
+
const content = getEngineRulesContent();
|
|
6
|
+
|
|
7
|
+
it('includes the engine marker', () => {
|
|
8
|
+
expect(content).toContain(`<!-- ${getEngineMarker()} -->`);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('Reconciliation Triggers', () => {
|
|
12
|
+
it('includes the Reconciliation Triggers section', () => {
|
|
13
|
+
expect(content).toContain('### Reconciliation Triggers');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('includes the explicit trigger (user says "done")', () => {
|
|
17
|
+
expect(content).toContain('**Explicit**');
|
|
18
|
+
expect(content).toMatch(/User says.*done.*ship it.*looks good/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('includes the plan-complete trigger', () => {
|
|
22
|
+
expect(content).toContain('**Plan-complete**');
|
|
23
|
+
expect(content).toContain(
|
|
24
|
+
'All tasks are complete. Want me to wrap up and capture what we learned, or is there more to fix?',
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('includes the idle trigger', () => {
|
|
29
|
+
expect(content).toContain('**Idle**');
|
|
30
|
+
expect(content).toContain(
|
|
31
|
+
"We've been idle on this plan. Ready to wrap up, or still working?",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('includes the NEVER auto-complete rule', () => {
|
|
36
|
+
expect(content).toContain('**NEVER auto-complete without asking the user.**');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('references orchestrate_status readiness field', () => {
|
|
40
|
+
expect(content).toContain('op:orchestrate_status');
|
|
41
|
+
expect(content).toContain('allTasksTerminal');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('describes orchestrate_complete as user-gated in the Non-Negotiable Rule', () => {
|
|
46
|
+
expect(content).toContain('user-gated');
|
|
47
|
+
});
|
|
48
|
+
});
|
package/src/agent-schema.ts
CHANGED
|
@@ -145,6 +145,32 @@ export const WorkflowDefinitionSchema = z.object({
|
|
|
145
145
|
verificationCriteria: z.array(z.string()).optional().default([]),
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
// ─── Workspace & Routing Schemas ─────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/** Workspace definition — scoped context area within an agent */
|
|
151
|
+
export const WorkspaceSchema = z.object({
|
|
152
|
+
/** Unique workspace identifier (kebab-case) */
|
|
153
|
+
id: z.string().min(1),
|
|
154
|
+
/** Human-readable workspace name */
|
|
155
|
+
name: z.string().min(1),
|
|
156
|
+
/** What this workspace is for */
|
|
157
|
+
description: z.string().min(1),
|
|
158
|
+
/** Context file name within the workspace directory. Default: CONTEXT.md */
|
|
159
|
+
contextFile: z.string().optional().default('CONTEXT.md'),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/** Routing entry — maps task patterns to workspaces */
|
|
163
|
+
export const RoutingEntrySchema = z.object({
|
|
164
|
+
/** Task pattern that triggers this route (e.g., "write script", "review code") */
|
|
165
|
+
pattern: z.string().min(1),
|
|
166
|
+
/** Target workspace id */
|
|
167
|
+
workspace: z.string().min(1),
|
|
168
|
+
/** Extra context files to load for this route */
|
|
169
|
+
context: z.array(z.string()).optional().default([]),
|
|
170
|
+
/** Skills to activate for this route */
|
|
171
|
+
skills: z.array(z.string()).optional().default([]),
|
|
172
|
+
});
|
|
173
|
+
|
|
148
174
|
// ─── Main Agent Schema ────────────────────────────────────────────────
|
|
149
175
|
|
|
150
176
|
/**
|
|
@@ -187,13 +213,53 @@ export const AgentYamlSchema = z.object({
|
|
|
187
213
|
/** LLM client integration settings */
|
|
188
214
|
setup: SetupConfigSchema.optional().default({}),
|
|
189
215
|
|
|
216
|
+
// ─── Skills ─────────────────────────────────────
|
|
217
|
+
/**
|
|
218
|
+
* Controls which skills are scaffolded.
|
|
219
|
+
* - 'essential' (default): ~7 core skills for a lightweight start
|
|
220
|
+
* - 'all': scaffold all available skills (backward compat)
|
|
221
|
+
* - string[]: scaffold only the named skills
|
|
222
|
+
*/
|
|
223
|
+
skillsFilter: z
|
|
224
|
+
.union([z.literal('all'), z.literal('essential'), z.array(z.string())])
|
|
225
|
+
.optional()
|
|
226
|
+
.default('essential'),
|
|
227
|
+
|
|
228
|
+
// ─── Workspaces & Routing ───────────────────────
|
|
229
|
+
/** Scoped context areas within the agent */
|
|
230
|
+
workspaces: z.array(WorkspaceSchema).optional(),
|
|
231
|
+
/** Task pattern → workspace routing table */
|
|
232
|
+
routing: z.array(RoutingEntrySchema).optional(),
|
|
233
|
+
|
|
190
234
|
// ─── Domain Packs ──────────────────────────────
|
|
191
235
|
/** npm domain packs with custom ops and knowledge */
|
|
192
236
|
packs: z.array(DomainPackSchema).optional(),
|
|
237
|
+
|
|
238
|
+
// ─── Git Initialization ────────────────────────
|
|
239
|
+
/** Git initialization configuration. If omitted, git is not initialized. */
|
|
240
|
+
git: z
|
|
241
|
+
.object({
|
|
242
|
+
/** Whether to run git init in the scaffolded agent directory */
|
|
243
|
+
init: z.boolean(),
|
|
244
|
+
/** Optional remote repository configuration */
|
|
245
|
+
remote: z
|
|
246
|
+
.object({
|
|
247
|
+
/** How to set up the remote: 'gh' creates via GitHub CLI, 'manual' uses a provided URL */
|
|
248
|
+
type: z.enum(['gh', 'manual']),
|
|
249
|
+
/** Remote URL (required for 'manual', auto-generated for 'gh') */
|
|
250
|
+
url: z.string().optional(),
|
|
251
|
+
/** Repository visibility for 'gh' type. Default: 'private' */
|
|
252
|
+
visibility: z.enum(['public', 'private']).optional().default('private'),
|
|
253
|
+
})
|
|
254
|
+
.optional(),
|
|
255
|
+
})
|
|
256
|
+
.optional(),
|
|
193
257
|
});
|
|
194
258
|
|
|
195
259
|
export type AgentYaml = z.infer<typeof AgentYamlSchema>;
|
|
196
260
|
export type AgentYamlInput = z.input<typeof AgentYamlSchema>;
|
|
261
|
+
export type Workspace = z.infer<typeof WorkspaceSchema>;
|
|
262
|
+
export type RoutingEntry = z.infer<typeof RoutingEntrySchema>;
|
|
197
263
|
export type WorkflowDefinition = z.infer<typeof WorkflowDefinitionSchema>;
|
|
198
264
|
export type WorkflowGate = z.infer<typeof WorkflowGateSchema>;
|
|
199
265
|
export type WorkflowTaskTemplate = z.infer<typeof WorkflowTaskTemplateSchema>;
|
package/src/compose-claude-md.ts
CHANGED
|
@@ -56,7 +56,26 @@ export function composeClaudeMd(agentDir: string, tools?: ToolEntry[]): Composed
|
|
|
56
56
|
// 5. Essential tools table
|
|
57
57
|
sections.push(composeToolsTable(agentYaml, tools));
|
|
58
58
|
|
|
59
|
-
// 6.
|
|
59
|
+
// 6. User custom instructions (instructions/user.md) — priority placement
|
|
60
|
+
// This file is user-editable and appears BEFORE engine rules and other instructions.
|
|
61
|
+
const userMdPath = join(agentDir, 'instructions', 'user.md');
|
|
62
|
+
if (existsSync(userMdPath)) {
|
|
63
|
+
const userContent = readFileSync(userMdPath, 'utf-8').trim();
|
|
64
|
+
if (userContent) {
|
|
65
|
+
sections.push(userContent);
|
|
66
|
+
sources.push(userMdPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 6b. Workspaces section (if defined)
|
|
71
|
+
const workspacesSection = composeWorkspacesSection(agentYaml);
|
|
72
|
+
if (workspacesSection) sections.push(workspacesSection);
|
|
73
|
+
|
|
74
|
+
// 6c. Routing table (if defined)
|
|
75
|
+
const routingSection = composeRoutingTable(agentYaml);
|
|
76
|
+
if (routingSection) sections.push(routingSection);
|
|
77
|
+
|
|
78
|
+
// 7. Engine rules — NOT inlined (they are injected once into ~/.claude/CLAUDE.md
|
|
60
79
|
// or project CLAUDE.md via `soleri install`). Including them here would
|
|
61
80
|
// triple-load the rules (~8k tokens duplicated per layer).
|
|
62
81
|
// We emit a short reference so the agent knows rules exist.
|
|
@@ -72,11 +91,11 @@ export function composeClaudeMd(agentDir: string, tools?: ToolEntry[]): Composed
|
|
|
72
91
|
sources.push(enginePath);
|
|
73
92
|
}
|
|
74
93
|
|
|
75
|
-
//
|
|
94
|
+
// 8. User instructions (instructions/*.md, excluding _engine.md and user.md)
|
|
76
95
|
const instructionsDir = join(agentDir, 'instructions');
|
|
77
96
|
if (existsSync(instructionsDir)) {
|
|
78
97
|
const files = readdirSync(instructionsDir)
|
|
79
|
-
.filter((f) => f.endsWith('.md') && f !== '_engine.md')
|
|
98
|
+
.filter((f) => f.endsWith('.md') && f !== '_engine.md' && f !== 'user.md')
|
|
80
99
|
.sort();
|
|
81
100
|
for (const file of files) {
|
|
82
101
|
const filePath = join(instructionsDir, file);
|
|
@@ -198,6 +217,47 @@ function composeToolsTable(agent: AgentYaml, tools?: ToolEntry[]): string {
|
|
|
198
217
|
return lines.join('\n');
|
|
199
218
|
}
|
|
200
219
|
|
|
220
|
+
function composeWorkspacesSection(agent: AgentYaml): string | null {
|
|
221
|
+
if (!agent.workspaces || agent.workspaces.length === 0) return null;
|
|
222
|
+
|
|
223
|
+
const lines: string[] = [
|
|
224
|
+
'## Workspaces',
|
|
225
|
+
'',
|
|
226
|
+
'Scoped context areas — each workspace has its own CONTEXT.md with task-specific instructions.',
|
|
227
|
+
'',
|
|
228
|
+
'| Workspace | Description |',
|
|
229
|
+
'|-----------|-------------|',
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
for (const ws of agent.workspaces) {
|
|
233
|
+
lines.push(`| \`${ws.id}\` | ${ws.description} |`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return lines.join('\n');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function composeRoutingTable(agent: AgentYaml): string | null {
|
|
240
|
+
if (!agent.routing || agent.routing.length === 0) return null;
|
|
241
|
+
|
|
242
|
+
const lines: string[] = [
|
|
243
|
+
'## Task Routing',
|
|
244
|
+
'',
|
|
245
|
+
'When a task matches a pattern below, navigate to the target workspace, load its CONTEXT.md, and activate the listed skills.',
|
|
246
|
+
'If no pattern matches, use the default root context.',
|
|
247
|
+
'',
|
|
248
|
+
'| Task Pattern | Route To | Context | Skills |',
|
|
249
|
+
'|--------------|----------|---------|--------|',
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
for (const route of agent.routing) {
|
|
253
|
+
const ctx = route.context.length > 0 ? route.context.join(', ') : '—';
|
|
254
|
+
const skills = route.skills.length > 0 ? route.skills.map((s) => `\`${s}\``).join(', ') : '—';
|
|
255
|
+
lines.push(`| ${route.pattern} | \`${route.workspace}\` | ${ctx} | ${skills} |`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
201
261
|
function composeWorkflowIndex(workflowsDir: string): string | null {
|
|
202
262
|
const dirs = readdirSync(workflowsDir, { withFileTypes: true })
|
|
203
263
|
.filter((d) => d.isDirectory())
|
package/src/domain-manager.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { execFileSync } from 'node:child_process';
|
|
11
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
11
12
|
import { generateDomainFacade } from './templates/domain-facade.js';
|
|
12
13
|
import { generateVaultOnlyDomainFacade } from './knowledge-installer.js';
|
|
13
14
|
import { patchIndexTs, patchClaudeMdContent } from './patching.js';
|
|
@@ -17,6 +18,7 @@ interface AddDomainParams {
|
|
|
17
18
|
agentPath: string;
|
|
18
19
|
domain: string;
|
|
19
20
|
noBuild?: boolean;
|
|
21
|
+
format?: 'filetree' | 'typescript';
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -41,9 +43,17 @@ function isV5Agent(agentPath: string): boolean {
|
|
|
41
43
|
* 6. Rebuild (unless noBuild)
|
|
42
44
|
*/
|
|
43
45
|
export async function addDomain(params: AddDomainParams): Promise<AddDomainResult> {
|
|
44
|
-
const { agentPath, domain, noBuild = false } = params;
|
|
46
|
+
const { agentPath, domain, noBuild = false, format } = params;
|
|
45
47
|
const warnings: string[] = [];
|
|
46
48
|
|
|
49
|
+
// ── File-tree agent path ──
|
|
50
|
+
|
|
51
|
+
if (format === 'filetree') {
|
|
52
|
+
return addDomainFileTree(agentPath, domain);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── TypeScript agent path (default / backward compat) ──
|
|
56
|
+
|
|
47
57
|
// ── Validate agent ──
|
|
48
58
|
|
|
49
59
|
const pkgPath = join(agentPath, 'package.json');
|
|
@@ -174,6 +184,69 @@ export async function addDomain(params: AddDomainParams): Promise<AddDomainResul
|
|
|
174
184
|
};
|
|
175
185
|
}
|
|
176
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Add a domain to a file-tree agent (agent.yaml + knowledge/).
|
|
189
|
+
* No facade generation, no src/ patching, no build step.
|
|
190
|
+
*/
|
|
191
|
+
function addDomainFileTree(agentPath: string, domain: string): AddDomainResult {
|
|
192
|
+
// ── Validate domain name ──
|
|
193
|
+
|
|
194
|
+
if (!/^[a-z][a-z0-9-]*$/.test(domain)) {
|
|
195
|
+
return fail(agentPath, domain, `Invalid domain name "${domain}" — must be kebab-case`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Read and validate agent.yaml ──
|
|
199
|
+
|
|
200
|
+
const yamlPath = join(agentPath, 'agent.yaml');
|
|
201
|
+
if (!existsSync(yamlPath)) {
|
|
202
|
+
return fail(agentPath, domain, 'No agent.yaml found — is this a file-tree agent?');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let agentYaml: Record<string, unknown>;
|
|
206
|
+
try {
|
|
207
|
+
agentYaml = parseYaml(readFileSync(yamlPath, 'utf-8')) as Record<string, unknown>;
|
|
208
|
+
} catch {
|
|
209
|
+
return fail(agentPath, domain, 'Failed to parse agent.yaml — is it valid YAML?');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const agentId = (agentYaml.id as string) ?? '';
|
|
213
|
+
if (!agentId) {
|
|
214
|
+
return fail(agentPath, domain, 'agent.yaml is missing an "id" field');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Check if domain already exists ──
|
|
218
|
+
|
|
219
|
+
const domains: string[] = Array.isArray(agentYaml.domains) ? (agentYaml.domains as string[]) : [];
|
|
220
|
+
if (domains.includes(domain)) {
|
|
221
|
+
return fail(agentPath, domain, `Domain "${domain}" already exists in agent.yaml`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Update agent.yaml domains array ──
|
|
225
|
+
|
|
226
|
+
agentYaml.domains = [...domains, domain];
|
|
227
|
+
writeFileSync(yamlPath, stringifyYaml(agentYaml), 'utf-8');
|
|
228
|
+
|
|
229
|
+
// ── Create knowledge/{domain}.json ──
|
|
230
|
+
|
|
231
|
+
const knowledgeDir = join(agentPath, 'knowledge');
|
|
232
|
+
mkdirSync(knowledgeDir, { recursive: true });
|
|
233
|
+
|
|
234
|
+
const bundlePath = join(knowledgeDir, `${domain}.json`);
|
|
235
|
+
const emptyBundle = JSON.stringify({ domain, entries: [] }, null, 2);
|
|
236
|
+
writeFileSync(bundlePath, emptyBundle, 'utf-8');
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
success: true,
|
|
240
|
+
agentPath,
|
|
241
|
+
domain,
|
|
242
|
+
agentId,
|
|
243
|
+
facadeGenerated: false,
|
|
244
|
+
buildOutput: '',
|
|
245
|
+
warnings: [],
|
|
246
|
+
summary: `Added domain "${domain}" to ${agentId} (file-tree agent)`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
177
250
|
function fail(agentPath: string, domain: string, message: string): AddDomainResult {
|
|
178
251
|
return {
|
|
179
252
|
success: false,
|