@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
|
@@ -154,16 +154,25 @@ interface InstallKnowledgeParams {
|
|
|
154
154
|
agentPath: string;
|
|
155
155
|
bundlePath: string;
|
|
156
156
|
generateFacades?: boolean;
|
|
157
|
+
/** Agent format: 'filetree' skips package.json check and src/ patching */
|
|
158
|
+
format?: 'filetree' | 'typescript';
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
export async function installKnowledge(
|
|
160
162
|
params: InstallKnowledgeParams,
|
|
161
163
|
): Promise<InstallKnowledgeResult> {
|
|
162
|
-
const { agentPath, bundlePath, generateFacades = true } = params;
|
|
164
|
+
const { agentPath, bundlePath, generateFacades = true, format } = params;
|
|
163
165
|
const warnings: string[] = [];
|
|
164
166
|
const facadesGenerated: string[] = [];
|
|
165
167
|
const sourceFilesPatched: string[] = [];
|
|
166
168
|
|
|
169
|
+
// ── File-tree agent path ─────────────────────────────────────────
|
|
170
|
+
if (format === 'filetree') {
|
|
171
|
+
return installKnowledgeFiletree(agentPath, bundlePath);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── TypeScript agent path (existing behavior) ────────────────────
|
|
175
|
+
|
|
167
176
|
// ── Step 1: Validate agent path ──────────────────────────────────
|
|
168
177
|
|
|
169
178
|
const pkgPath = join(agentPath, 'package.json');
|
|
@@ -363,6 +372,120 @@ export async function installKnowledge(
|
|
|
363
372
|
};
|
|
364
373
|
}
|
|
365
374
|
|
|
375
|
+
// ---------- File-tree agent installer ----------
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Install knowledge bundles into a file-tree agent.
|
|
379
|
+
* Writes to {agentPath}/knowledge/ — no package.json, no src/ patching, no build step.
|
|
380
|
+
* The engine picks up knowledge bundles from this directory at runtime.
|
|
381
|
+
*/
|
|
382
|
+
async function installKnowledgeFiletree(
|
|
383
|
+
agentPath: string,
|
|
384
|
+
bundlePath: string,
|
|
385
|
+
): Promise<InstallKnowledgeResult> {
|
|
386
|
+
const warnings: string[] = [];
|
|
387
|
+
|
|
388
|
+
// Derive agentId from agent.yaml
|
|
389
|
+
let agentId = '';
|
|
390
|
+
const yamlPath = join(agentPath, 'agent.yaml');
|
|
391
|
+
if (existsSync(yamlPath)) {
|
|
392
|
+
try {
|
|
393
|
+
const raw = readFileSync(yamlPath, 'utf-8');
|
|
394
|
+
// Simple extraction — avoid importing yaml parser here
|
|
395
|
+
const idMatch = raw.match(/^id:\s*["']?([^\s"']+)/m);
|
|
396
|
+
if (idMatch) agentId = idMatch[1];
|
|
397
|
+
} catch {
|
|
398
|
+
// best-effort
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!agentId) {
|
|
402
|
+
return fail(agentPath, '', 'No agent.yaml with valid id found — is this a file-tree agent?');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Read and validate bundles
|
|
406
|
+
const bundleFiles = collectBundleFiles(bundlePath);
|
|
407
|
+
if (bundleFiles.length === 0) {
|
|
408
|
+
return fail(agentPath, agentId, `No .json bundle files found at ${bundlePath}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const bundles: Array<{ file: string; bundle: Bundle }> = [];
|
|
412
|
+
const issues: string[] = [];
|
|
413
|
+
|
|
414
|
+
for (const file of bundleFiles) {
|
|
415
|
+
try {
|
|
416
|
+
const raw = readFileSync(file, 'utf-8');
|
|
417
|
+
const parsed = JSON.parse(raw) as Bundle;
|
|
418
|
+
const fileIssues = validateBundle(parsed, file);
|
|
419
|
+
if (fileIssues.length > 0) {
|
|
420
|
+
issues.push(...fileIssues);
|
|
421
|
+
} else {
|
|
422
|
+
bundles.push({ file, bundle: parsed });
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
issues.push(
|
|
426
|
+
`${basename(file)}: invalid JSON — ${err instanceof Error ? err.message : String(err)}`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (bundles.length === 0) {
|
|
432
|
+
return fail(agentPath, agentId, `All bundles failed validation:\n${issues.join('\n')}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (issues.length > 0) {
|
|
436
|
+
warnings.push(...issues);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Determine new vs existing domains
|
|
440
|
+
const knowledgeDir = join(agentPath, 'knowledge');
|
|
441
|
+
mkdirSync(knowledgeDir, { recursive: true });
|
|
442
|
+
|
|
443
|
+
const existingFiles = readdirSync(knowledgeDir).filter((f) => f.endsWith('.json'));
|
|
444
|
+
const existingDomains = new Set(existingFiles.map((f) => f.replace(/\.json$/, '')));
|
|
445
|
+
const domainsAdded: string[] = [];
|
|
446
|
+
const domainsUpdated: string[] = [];
|
|
447
|
+
|
|
448
|
+
for (const { bundle } of bundles) {
|
|
449
|
+
if (existingDomains.has(bundle.domain)) {
|
|
450
|
+
domainsUpdated.push(bundle.domain);
|
|
451
|
+
} else {
|
|
452
|
+
domainsAdded.push(bundle.domain);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Copy bundles to knowledge/
|
|
457
|
+
for (const { file, bundle } of bundles) {
|
|
458
|
+
const dest = join(knowledgeDir, `${bundle.domain}.json`);
|
|
459
|
+
copyFileSync(file, dest);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// No facade generation, no src/ patching, no build step for file-tree agents
|
|
463
|
+
|
|
464
|
+
const entriesTotal = bundles.reduce((sum, { bundle }) => sum + bundle.entries.length, 0);
|
|
465
|
+
|
|
466
|
+
const summaryParts = [
|
|
467
|
+
`Installed ${bundles.length} bundle(s) with ${entriesTotal} entries into ${agentId} (file-tree)`,
|
|
468
|
+
];
|
|
469
|
+
if (domainsAdded.length > 0) summaryParts.push(`New domains: ${domainsAdded.join(', ')}`);
|
|
470
|
+
if (domainsUpdated.length > 0) summaryParts.push(`Updated domains: ${domainsUpdated.join(', ')}`);
|
|
471
|
+
if (warnings.length > 0) summaryParts.push(`${warnings.length} warning(s)`);
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
success: true,
|
|
475
|
+
agentPath,
|
|
476
|
+
agentId,
|
|
477
|
+
bundlesInstalled: bundles.length,
|
|
478
|
+
entriesTotal,
|
|
479
|
+
domainsAdded,
|
|
480
|
+
domainsUpdated,
|
|
481
|
+
facadesGenerated: [],
|
|
482
|
+
sourceFilesPatched: [],
|
|
483
|
+
buildOutput: '',
|
|
484
|
+
warnings,
|
|
485
|
+
summary: summaryParts.join('. ') + '.',
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
366
489
|
// ---------- Helpers ----------
|
|
367
490
|
|
|
368
491
|
function fail(agentPath: string, agentId: string, message: string): InstallKnowledgeResult {
|
package/src/lib.ts
CHANGED
|
@@ -21,7 +21,12 @@ export type {
|
|
|
21
21
|
export { AgentConfigSchema, SETUP_TARGETS, MODEL_PRESETS } from './types.js';
|
|
22
22
|
|
|
23
23
|
// ─── v7 File-Tree Agent ──────────────────────────────────────────────
|
|
24
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
scaffoldFileTree,
|
|
26
|
+
SKILLS_REGISTRY,
|
|
27
|
+
ESSENTIAL_SKILLS,
|
|
28
|
+
resolveSkillsFilter,
|
|
29
|
+
} from './scaffold-filetree.js';
|
|
25
30
|
export type { FileTreeScaffoldResult } from './scaffold-filetree.js';
|
|
26
31
|
export { AgentYamlSchema, TONES } from './agent-schema.js';
|
|
27
32
|
export type { AgentYaml, AgentYamlInput } from './agent-schema.js';
|
package/src/scaffold-filetree.ts
CHANGED
|
@@ -18,6 +18,64 @@ import { composeClaudeMd } from './compose-claude-md.js';
|
|
|
18
18
|
import { generateSkills } from './templates/skills.js';
|
|
19
19
|
import type { AgentConfig } from './types.js';
|
|
20
20
|
|
|
21
|
+
// ─── Skills Registry ─────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Skills classified as essential (always scaffolded by default) or optional
|
|
25
|
+
* (installed on demand via `soleri skills install`).
|
|
26
|
+
*/
|
|
27
|
+
export const SKILLS_REGISTRY: Record<string, 'essential' | 'optional'> = {
|
|
28
|
+
'agent-guide': 'essential',
|
|
29
|
+
'agent-persona': 'essential',
|
|
30
|
+
'vault-navigator': 'essential',
|
|
31
|
+
'vault-capture': 'essential',
|
|
32
|
+
'systematic-debugging': 'essential',
|
|
33
|
+
'writing-plans': 'essential',
|
|
34
|
+
'context-resume': 'essential',
|
|
35
|
+
// ─── Optional (installed on demand) ────────────
|
|
36
|
+
'agent-dev': 'optional',
|
|
37
|
+
'agent-issues': 'optional',
|
|
38
|
+
'brain-debrief': 'optional',
|
|
39
|
+
brainstorming: 'optional',
|
|
40
|
+
'code-patrol': 'optional',
|
|
41
|
+
'deep-review': 'optional',
|
|
42
|
+
'deliver-and-ship': 'optional',
|
|
43
|
+
'discovery-phase': 'optional',
|
|
44
|
+
'env-setup': 'optional',
|
|
45
|
+
'executing-plans': 'optional',
|
|
46
|
+
'finishing-a-development-branch': 'optional',
|
|
47
|
+
'fix-and-learn': 'optional',
|
|
48
|
+
'health-check': 'optional',
|
|
49
|
+
'knowledge-harvest': 'optional',
|
|
50
|
+
'mcp-doctor': 'optional',
|
|
51
|
+
'onboard-me': 'optional',
|
|
52
|
+
'parallel-execute': 'optional',
|
|
53
|
+
retrospective: 'optional',
|
|
54
|
+
'second-opinion': 'optional',
|
|
55
|
+
'subagent-driven-development': 'optional',
|
|
56
|
+
'test-driven-development': 'optional',
|
|
57
|
+
'using-git-worktrees': 'optional',
|
|
58
|
+
'vault-curate': 'optional',
|
|
59
|
+
'vault-smells': 'optional',
|
|
60
|
+
'verification-before-completion': 'optional',
|
|
61
|
+
'yolo-mode': 'optional',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Names of essential skills (always scaffolded when skillsFilter is 'essential'). */
|
|
65
|
+
export const ESSENTIAL_SKILLS = Object.entries(SKILLS_REGISTRY)
|
|
66
|
+
.filter(([, tier]) => tier === 'essential')
|
|
67
|
+
.map(([name]) => name);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the skill names to scaffold based on the skillsFilter config value.
|
|
71
|
+
* Returns null when all skills should be included (no filtering).
|
|
72
|
+
*/
|
|
73
|
+
export function resolveSkillsFilter(skillsFilter: 'all' | 'essential' | string[]): string[] | null {
|
|
74
|
+
if (skillsFilter === 'all') return null; // null = include all
|
|
75
|
+
if (skillsFilter === 'essential') return ESSENTIAL_SKILLS;
|
|
76
|
+
return skillsFilter; // explicit list
|
|
77
|
+
}
|
|
78
|
+
|
|
21
79
|
// ─── Types ────────────────────────────────────────────────────────────
|
|
22
80
|
|
|
23
81
|
export interface FileTreeScaffoldResult {
|
|
@@ -235,6 +293,164 @@ Before crossing a context window boundary — \`/clear\`, context compaction, or
|
|
|
235
293
|
},
|
|
236
294
|
];
|
|
237
295
|
|
|
296
|
+
// ─── Example Instruction Files ───────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
const INSTRUCTIONS_CONVENTIONS = `# Conventions
|
|
299
|
+
|
|
300
|
+
<!-- Customize this file with your project's naming conventions, coding standards, and rules. -->
|
|
301
|
+
<!-- This file is composed into CLAUDE.md automatically — your agent will follow these rules. -->
|
|
302
|
+
|
|
303
|
+
## Naming Conventions
|
|
304
|
+
|
|
305
|
+
- Use \`kebab-case\` for file and directory names
|
|
306
|
+
- Use \`camelCase\` for variables and functions
|
|
307
|
+
- Use \`PascalCase\` for classes, types, and interfaces
|
|
308
|
+
- Prefix private helpers with \`_\` (e.g., \`_validateInput\`)
|
|
309
|
+
|
|
310
|
+
## File Organization
|
|
311
|
+
|
|
312
|
+
- Source code goes in \`src/\`
|
|
313
|
+
- Tests live next to the code they test (\`*.test.ts\`)
|
|
314
|
+
- Shared utilities go in \`src/utils/\`
|
|
315
|
+
- Types and interfaces go in \`src/types/\`
|
|
316
|
+
|
|
317
|
+
## Code Standards
|
|
318
|
+
|
|
319
|
+
- Every function must have a JSDoc comment explaining its purpose
|
|
320
|
+
- Prefer \`const\` over \`let\`; never use \`var\`
|
|
321
|
+
- Maximum file length: 300 lines — split if larger
|
|
322
|
+
- No default exports — use named exports only
|
|
323
|
+
|
|
324
|
+
## What to Avoid
|
|
325
|
+
|
|
326
|
+
- Do not add new npm dependencies without approval
|
|
327
|
+
- Do not use \`any\` type — use \`unknown\` and narrow
|
|
328
|
+
- Do not commit commented-out code
|
|
329
|
+
- Do not use hardcoded values — extract to constants or config
|
|
330
|
+
`;
|
|
331
|
+
|
|
332
|
+
const INSTRUCTIONS_GETTING_STARTED = `# Getting Started with Instructions
|
|
333
|
+
|
|
334
|
+
This folder contains your agent's custom behavioral rules. Every \`.md\` file here
|
|
335
|
+
is automatically composed into \`CLAUDE.md\` when you run \`soleri dev\`.
|
|
336
|
+
|
|
337
|
+
## How It Works
|
|
338
|
+
|
|
339
|
+
1. Create a new \`.md\` file in this folder (e.g., \`api-guidelines.md\`)
|
|
340
|
+
2. Write your rules, conventions, or guidelines in Markdown
|
|
341
|
+
3. Run \`soleri dev\` — it watches for changes and regenerates \`CLAUDE.md\`
|
|
342
|
+
4. Your agent now follows these rules in every conversation
|
|
343
|
+
|
|
344
|
+
## File Naming
|
|
345
|
+
|
|
346
|
+
- Files are included in **alphabetical order** (prefix with numbers to control order)
|
|
347
|
+
- \`_engine.md\` is auto-generated by Soleri — **do not edit it manually**
|
|
348
|
+
- \`domain.md\` was generated from your agent's domain config
|
|
349
|
+
|
|
350
|
+
## Tips
|
|
351
|
+
|
|
352
|
+
- Keep each file focused on one topic (conventions, workflows, constraints)
|
|
353
|
+
- Use clear headings — your agent reads these as instructions
|
|
354
|
+
- Add "What to Avoid" sections — agents benefit from explicit anti-patterns
|
|
355
|
+
- See the [Soleri docs](https://soleri.ai/docs) for more examples
|
|
356
|
+
`;
|
|
357
|
+
|
|
358
|
+
// ─── Workspace & Routing Seeds ───────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
/** Default workspaces seeded based on agent domains. */
|
|
361
|
+
const DOMAIN_WORKSPACE_SEEDS: Record<string, { id: string; name: string; description: string }[]> =
|
|
362
|
+
{
|
|
363
|
+
// Design-related domains
|
|
364
|
+
design: [
|
|
365
|
+
{
|
|
366
|
+
id: 'design',
|
|
367
|
+
name: 'Design',
|
|
368
|
+
description: 'Design system patterns, tokens, and components',
|
|
369
|
+
},
|
|
370
|
+
{ id: 'review', name: 'Review', description: 'Design review and accessibility audits' },
|
|
371
|
+
],
|
|
372
|
+
'ui-design': [
|
|
373
|
+
{ id: 'design', name: 'Design', description: 'UI design patterns, tokens, and components' },
|
|
374
|
+
{ id: 'review', name: 'Review', description: 'Design review and accessibility audits' },
|
|
375
|
+
],
|
|
376
|
+
accessibility: [
|
|
377
|
+
{ id: 'design', name: 'Design', description: 'Accessible design patterns and tokens' },
|
|
378
|
+
{ id: 'review', name: 'Review', description: 'Accessibility audits and compliance checks' },
|
|
379
|
+
],
|
|
380
|
+
// Dev-related domains
|
|
381
|
+
architecture: [
|
|
382
|
+
{
|
|
383
|
+
id: 'planning',
|
|
384
|
+
name: 'Planning',
|
|
385
|
+
description: 'Architecture decisions and technical planning',
|
|
386
|
+
},
|
|
387
|
+
{ id: 'src', name: 'Source', description: 'Implementation code and modules' },
|
|
388
|
+
{ id: 'docs', name: 'Documentation', description: 'Technical documentation and ADRs' },
|
|
389
|
+
],
|
|
390
|
+
backend: [
|
|
391
|
+
{ id: 'planning', name: 'Planning', description: 'Backend architecture and API design' },
|
|
392
|
+
{ id: 'src', name: 'Source', description: 'Implementation code and modules' },
|
|
393
|
+
{ id: 'docs', name: 'Documentation', description: 'API documentation and guides' },
|
|
394
|
+
],
|
|
395
|
+
frontend: [
|
|
396
|
+
{
|
|
397
|
+
id: 'planning',
|
|
398
|
+
name: 'Planning',
|
|
399
|
+
description: 'Frontend architecture and component design',
|
|
400
|
+
},
|
|
401
|
+
{ id: 'src', name: 'Source', description: 'Implementation code and components' },
|
|
402
|
+
{
|
|
403
|
+
id: 'docs',
|
|
404
|
+
name: 'Documentation',
|
|
405
|
+
description: 'Component documentation and style guides',
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
security: [
|
|
409
|
+
{
|
|
410
|
+
id: 'planning',
|
|
411
|
+
name: 'Planning',
|
|
412
|
+
description: 'Security architecture and threat modeling',
|
|
413
|
+
},
|
|
414
|
+
{ id: 'src', name: 'Source', description: 'Security implementations and policies' },
|
|
415
|
+
{ id: 'docs', name: 'Documentation', description: 'Security documentation and runbooks' },
|
|
416
|
+
],
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/** Default routing entries seeded based on agent domains. */
|
|
420
|
+
const DOMAIN_ROUTING_SEEDS: Record<
|
|
421
|
+
string,
|
|
422
|
+
{ pattern: string; workspace: string; skills: string[] }[]
|
|
423
|
+
> = {
|
|
424
|
+
design: [
|
|
425
|
+
{ pattern: 'design component', workspace: 'design', skills: ['vault-navigator'] },
|
|
426
|
+
{ pattern: 'review design', workspace: 'review', skills: ['deep-review'] },
|
|
427
|
+
],
|
|
428
|
+
'ui-design': [
|
|
429
|
+
{ pattern: 'design component', workspace: 'design', skills: ['vault-navigator'] },
|
|
430
|
+
{ pattern: 'review design', workspace: 'review', skills: ['deep-review'] },
|
|
431
|
+
],
|
|
432
|
+
architecture: [
|
|
433
|
+
{ pattern: 'plan architecture', workspace: 'planning', skills: ['writing-plans'] },
|
|
434
|
+
{ pattern: 'implement feature', workspace: 'src', skills: ['test-driven-development'] },
|
|
435
|
+
{ pattern: 'write documentation', workspace: 'docs', skills: ['vault-capture'] },
|
|
436
|
+
],
|
|
437
|
+
backend: [
|
|
438
|
+
{ pattern: 'plan API', workspace: 'planning', skills: ['writing-plans'] },
|
|
439
|
+
{ pattern: 'implement endpoint', workspace: 'src', skills: ['test-driven-development'] },
|
|
440
|
+
{ pattern: 'write docs', workspace: 'docs', skills: ['vault-capture'] },
|
|
441
|
+
],
|
|
442
|
+
frontend: [
|
|
443
|
+
{ pattern: 'plan component', workspace: 'planning', skills: ['writing-plans'] },
|
|
444
|
+
{ pattern: 'implement component', workspace: 'src', skills: ['test-driven-development'] },
|
|
445
|
+
{ pattern: 'write docs', workspace: 'docs', skills: ['vault-capture'] },
|
|
446
|
+
],
|
|
447
|
+
security: [
|
|
448
|
+
{ pattern: 'threat model', workspace: 'planning', skills: ['writing-plans'] },
|
|
449
|
+
{ pattern: 'implement policy', workspace: 'src', skills: ['test-driven-development'] },
|
|
450
|
+
{ pattern: 'write runbook', workspace: 'docs', skills: ['vault-capture'] },
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
|
|
238
454
|
// ─── Main Scaffolder ──────────────────────────────────────────────────
|
|
239
455
|
|
|
240
456
|
/**
|
|
@@ -324,6 +540,13 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
|
|
|
324
540
|
'AGENTS.md',
|
|
325
541
|
'instructions/_engine.md',
|
|
326
542
|
'',
|
|
543
|
+
'# OS',
|
|
544
|
+
'.DS_Store',
|
|
545
|
+
'',
|
|
546
|
+
'# Editor / IDE state',
|
|
547
|
+
'.obsidian/',
|
|
548
|
+
'.opencode/',
|
|
549
|
+
'',
|
|
327
550
|
].join('\n'),
|
|
328
551
|
filesCreated,
|
|
329
552
|
);
|
|
@@ -332,6 +555,24 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
|
|
|
332
555
|
writeFile(agentDir, 'instructions/_engine.md', getEngineRulesContent(), filesCreated);
|
|
333
556
|
|
|
334
557
|
// ─── 6. Write user instruction files ────────────────────────
|
|
558
|
+
// Generate user.md — user-editable file with priority placement in CLAUDE.md
|
|
559
|
+
const userMdContent = [
|
|
560
|
+
'# Your Custom Rules',
|
|
561
|
+
'',
|
|
562
|
+
'Add your agent-specific rules, constraints, and preferences here.',
|
|
563
|
+
'This file gets priority placement in CLAUDE.md — it appears before engine rules.',
|
|
564
|
+
'',
|
|
565
|
+
'## Examples of what to put here:',
|
|
566
|
+
'- Project-specific conventions',
|
|
567
|
+
'- Communication preferences',
|
|
568
|
+
'- Domain expertise to emphasize',
|
|
569
|
+
'- Things to always/never do',
|
|
570
|
+
'',
|
|
571
|
+
'Delete these instructions and replace with your own content.',
|
|
572
|
+
'',
|
|
573
|
+
].join('\n');
|
|
574
|
+
writeFile(agentDir, 'instructions/user.md', userMdContent, filesCreated);
|
|
575
|
+
|
|
335
576
|
// Generate domain-specific instruction file if agent has specialized domains
|
|
336
577
|
if (config.domains.length > 0) {
|
|
337
578
|
const domainLines = [
|
|
@@ -347,6 +588,15 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
|
|
|
347
588
|
writeFile(agentDir, 'instructions/domain.md', domainLines.join('\n'), filesCreated);
|
|
348
589
|
}
|
|
349
590
|
|
|
591
|
+
// ─── 6b. Write example instruction files ─────────────────────
|
|
592
|
+
writeFile(agentDir, 'instructions/conventions.md', INSTRUCTIONS_CONVENTIONS, filesCreated);
|
|
593
|
+
writeFile(
|
|
594
|
+
agentDir,
|
|
595
|
+
'instructions/getting-started.md',
|
|
596
|
+
INSTRUCTIONS_GETTING_STARTED,
|
|
597
|
+
filesCreated,
|
|
598
|
+
);
|
|
599
|
+
|
|
350
600
|
// ─── 7. Write workflows ─────────────────────────────────────
|
|
351
601
|
for (const wf of BUILTIN_WORKFLOWS) {
|
|
352
602
|
writeFile(agentDir, `workflows/${wf.name}/prompt.md`, wf.prompt, filesCreated);
|
|
@@ -355,7 +605,11 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
|
|
|
355
605
|
}
|
|
356
606
|
|
|
357
607
|
// ─── 8. Copy bundled skills (with placeholder substitution) ─
|
|
358
|
-
const
|
|
608
|
+
const resolvedSkills = resolveSkillsFilter(config.skillsFilter);
|
|
609
|
+
const skills = generateSkills({
|
|
610
|
+
id: config.id,
|
|
611
|
+
skills: resolvedSkills ?? undefined,
|
|
612
|
+
} as AgentConfig);
|
|
359
613
|
for (const [relativePath, content] of skills) {
|
|
360
614
|
mkdirSync(join(agentDir, dirname(relativePath)), { recursive: true });
|
|
361
615
|
writeFile(agentDir, relativePath, content, filesCreated);
|
|
@@ -381,7 +635,33 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
|
|
|
381
635
|
totalSeeded += starterEntries.length;
|
|
382
636
|
}
|
|
383
637
|
|
|
384
|
-
// ───
|
|
638
|
+
// ─── 9b. Create workspace directories with CONTEXT.md ──────
|
|
639
|
+
// Resolve workspaces: use explicit config or seed from domains
|
|
640
|
+
const resolvedWorkspaces = resolveWorkspaces(config);
|
|
641
|
+
if (resolvedWorkspaces.length > 0) {
|
|
642
|
+
for (const ws of resolvedWorkspaces) {
|
|
643
|
+
const wsDir = join(agentDir, 'workspaces', ws.id);
|
|
644
|
+
mkdirSync(wsDir, { recursive: true });
|
|
645
|
+
const contextContent = [
|
|
646
|
+
`# ${ws.name}`,
|
|
647
|
+
'',
|
|
648
|
+
ws.description,
|
|
649
|
+
'',
|
|
650
|
+
'## Instructions',
|
|
651
|
+
'',
|
|
652
|
+
`<!-- Add workspace-specific instructions here for the "${ws.name}" context. -->`,
|
|
653
|
+
'',
|
|
654
|
+
].join('\n');
|
|
655
|
+
writeFile(
|
|
656
|
+
agentDir,
|
|
657
|
+
`workspaces/${ws.id}/${ws.contextFile ?? 'CONTEXT.md'}`,
|
|
658
|
+
contextContent,
|
|
659
|
+
filesCreated,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ─── 10. Generate CLAUDE.md ──────────────────────────────────
|
|
385
665
|
const { content: claudeMd } = composeClaudeMd(agentDir);
|
|
386
666
|
writeFile(agentDir, 'CLAUDE.md', claudeMd, filesCreated);
|
|
387
667
|
|
|
@@ -476,6 +756,34 @@ function buildAgentYaml(config: AgentYaml): Record<string, unknown> {
|
|
|
476
756
|
setup.model = config.setup.model;
|
|
477
757
|
if (Object.keys(setup).length > 0) yaml.setup = setup;
|
|
478
758
|
|
|
759
|
+
// Skills filter — only include if not the default ('essential')
|
|
760
|
+
if (config.skillsFilter && config.skillsFilter !== 'essential') {
|
|
761
|
+
yaml.skillsFilter = config.skillsFilter;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Workspaces
|
|
765
|
+
const resolvedWs = resolveWorkspaces(config);
|
|
766
|
+
if (resolvedWs.length > 0) {
|
|
767
|
+
yaml.workspaces = resolvedWs.map((ws) =>
|
|
768
|
+
Object.assign(
|
|
769
|
+
{ id: ws.id, name: ws.name, description: ws.description },
|
|
770
|
+
ws.contextFile !== `CONTEXT.md` ? { contextFile: ws.contextFile } : {},
|
|
771
|
+
),
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Routing
|
|
776
|
+
const resolvedRouting = resolveRouting(config);
|
|
777
|
+
if (resolvedRouting.length > 0) {
|
|
778
|
+
yaml.routing = resolvedRouting.map((r) =>
|
|
779
|
+
Object.assign(
|
|
780
|
+
{ pattern: r.pattern, workspace: r.workspace },
|
|
781
|
+
r.context.length > 0 ? { context: r.context } : {},
|
|
782
|
+
r.skills.length > 0 ? { skills: r.skills } : {},
|
|
783
|
+
),
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
479
787
|
// Packs
|
|
480
788
|
if (config.packs && config.packs.length > 0) {
|
|
481
789
|
yaml.packs = config.packs;
|
|
@@ -484,6 +792,76 @@ function buildAgentYaml(config: AgentYaml): Record<string, unknown> {
|
|
|
484
792
|
return yaml;
|
|
485
793
|
}
|
|
486
794
|
|
|
795
|
+
// ─── Workspace & Routing Helpers ─────────────────────────────────────
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Resolve workspaces: use explicit config or seed from domains.
|
|
799
|
+
* Deduplicates by workspace id.
|
|
800
|
+
*/
|
|
801
|
+
function resolveWorkspaces(
|
|
802
|
+
config: AgentYaml,
|
|
803
|
+
): { id: string; name: string; description: string; contextFile: string }[] {
|
|
804
|
+
// If explicitly defined, use those
|
|
805
|
+
if (config.workspaces && config.workspaces.length > 0) {
|
|
806
|
+
return config.workspaces.map((ws) => ({
|
|
807
|
+
id: ws.id,
|
|
808
|
+
name: ws.name,
|
|
809
|
+
description: ws.description,
|
|
810
|
+
contextFile: ws.contextFile ?? 'CONTEXT.md',
|
|
811
|
+
}));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Otherwise, seed from domains
|
|
815
|
+
const seen = new Set<string>();
|
|
816
|
+
const workspaces: { id: string; name: string; description: string; contextFile: string }[] = [];
|
|
817
|
+
|
|
818
|
+
for (const domain of config.domains) {
|
|
819
|
+
const seeds = DOMAIN_WORKSPACE_SEEDS[domain];
|
|
820
|
+
if (!seeds) continue;
|
|
821
|
+
for (const seed of seeds) {
|
|
822
|
+
if (seen.has(seed.id)) continue;
|
|
823
|
+
seen.add(seed.id);
|
|
824
|
+
workspaces.push({ ...seed, contextFile: 'CONTEXT.md' });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return workspaces;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Resolve routing entries: use explicit config or seed from domains.
|
|
833
|
+
* Deduplicates by pattern string.
|
|
834
|
+
*/
|
|
835
|
+
function resolveRouting(
|
|
836
|
+
config: AgentYaml,
|
|
837
|
+
): { pattern: string; workspace: string; context: string[]; skills: string[] }[] {
|
|
838
|
+
// If explicitly defined, use those
|
|
839
|
+
if (config.routing && config.routing.length > 0) {
|
|
840
|
+
return config.routing.map((r) => ({
|
|
841
|
+
pattern: r.pattern,
|
|
842
|
+
workspace: r.workspace,
|
|
843
|
+
context: r.context ?? [],
|
|
844
|
+
skills: r.skills ?? [],
|
|
845
|
+
}));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Otherwise, seed from domains
|
|
849
|
+
const seen = new Set<string>();
|
|
850
|
+
const routes: { pattern: string; workspace: string; context: string[]; skills: string[] }[] = [];
|
|
851
|
+
|
|
852
|
+
for (const domain of config.domains) {
|
|
853
|
+
const seeds = DOMAIN_ROUTING_SEEDS[domain];
|
|
854
|
+
if (!seeds) continue;
|
|
855
|
+
for (const seed of seeds) {
|
|
856
|
+
if (seen.has(seed.pattern)) continue;
|
|
857
|
+
seen.add(seed.pattern);
|
|
858
|
+
routes.push({ ...seed, context: [] });
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return routes;
|
|
863
|
+
}
|
|
864
|
+
|
|
487
865
|
// ─── Starter Pack Helpers ────────────────────────────────────────────
|
|
488
866
|
|
|
489
867
|
/** Domain aliases — map agent domains to starter pack directories. */
|
package/src/scaffolder.ts
CHANGED
|
@@ -163,6 +163,13 @@ export function previewScaffold(config: AgentConfig): ScaffoldPreview {
|
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
if (opencodeSetup && config.hookPacks?.length) {
|
|
167
|
+
files.push({
|
|
168
|
+
path: '.opencode/plugins/',
|
|
169
|
+
description: `OpenCode enforcement plugin (${config.hookPacks.join(', ')})`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
166
173
|
if (config.telegram) {
|
|
167
174
|
files.push(
|
|
168
175
|
{
|
|
@@ -334,6 +341,10 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
|
|
|
334
341
|
dirs.push('.claude');
|
|
335
342
|
}
|
|
336
343
|
|
|
344
|
+
if (opencodeSetup && config.hookPacks?.length) {
|
|
345
|
+
dirs.push('.opencode/plugins');
|
|
346
|
+
}
|
|
347
|
+
|
|
337
348
|
for (const dir of dirs) {
|
|
338
349
|
mkdirSync(join(agentDir, dir), { recursive: true });
|
|
339
350
|
}
|
|
@@ -570,6 +581,12 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
|
|
|
570
581
|
summaryLines.push(`${config.hookPacks.length} hook pack(s) bundled in .claude/`);
|
|
571
582
|
}
|
|
572
583
|
|
|
584
|
+
if (opencodeSetup && config.hookPacks?.length) {
|
|
585
|
+
summaryLines.push(
|
|
586
|
+
`${config.hookPacks.length} hook pack(s) bundled as OpenCode plugin in .opencode/plugins/`,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
573
590
|
for (const registration of mcpRegistrations) {
|
|
574
591
|
if (registration.result.registered) {
|
|
575
592
|
summaryLines.push(
|