@soleri/forge 9.7.2 → 9.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/agent-schema.d.ts +177 -6
  2. package/dist/agent-schema.js +58 -0
  3. package/dist/agent-schema.js.map +1 -1
  4. package/dist/compose-claude-md.js +56 -3
  5. package/dist/compose-claude-md.js.map +1 -1
  6. package/dist/domain-manager.d.ts +1 -0
  7. package/dist/domain-manager.js +57 -1
  8. package/dist/domain-manager.js.map +1 -1
  9. package/dist/knowledge-installer.d.ts +2 -0
  10. package/dist/knowledge-installer.js +107 -1
  11. package/dist/knowledge-installer.js.map +1 -1
  12. package/dist/lib.d.ts +1 -1
  13. package/dist/lib.js +1 -1
  14. package/dist/lib.js.map +1 -1
  15. package/dist/scaffold-filetree.d.ts +12 -0
  16. package/dist/scaffold-filetree.js +356 -10
  17. package/dist/scaffold-filetree.js.map +1 -1
  18. package/dist/scaffolder.js +12 -0
  19. package/dist/scaffolder.js.map +1 -1
  20. package/dist/skills/subagent-driven-development/SKILL.md +87 -20
  21. package/dist/templates/setup-script.js +71 -0
  22. package/dist/templates/setup-script.js.map +1 -1
  23. package/dist/templates/shared-rules.js +163 -6
  24. package/dist/templates/shared-rules.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/__tests__/domain-manager.test.ts +140 -0
  27. package/src/__tests__/scaffold-filetree.test.ts +326 -1
  28. package/src/__tests__/scaffolder.test.ts +7 -5
  29. package/src/__tests__/shared-rules.test.ts +48 -0
  30. package/src/agent-schema.ts +66 -0
  31. package/src/compose-claude-md.ts +63 -3
  32. package/src/domain-manager.ts +74 -1
  33. package/src/knowledge-installer.ts +124 -1
  34. package/src/lib.ts +6 -1
  35. package/src/scaffold-filetree.ts +404 -10
  36. package/src/scaffolder.ts +17 -0
  37. package/src/skills/subagent-driven-development/SKILL.md +87 -20
  38. package/src/templates/setup-script.ts +71 -0
  39. package/src/templates/shared-rules.ts +166 -6
@@ -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 {
@@ -75,7 +133,9 @@ When building a new feature, adding functionality, or creating components.
75
133
  - Link new entries to related knowledge: \`op:link_entries\`
76
134
  - Complete orchestration: \`op:orchestrate_complete\`
77
135
  `,
78
- gates: `gates:
136
+ gates: `# Workflow gates — engine reads these and enforces them during plan execution.
137
+ # Format: phase (brainstorming|pre-execution|post-task|completion), requirement, check
138
+ gates:
79
139
  - phase: brainstorming
80
140
  requirement: Requirements are clear and user has approved the approach
81
141
  check: user-approval
@@ -92,7 +152,9 @@ When building a new feature, adding functionality, or creating components.
92
152
  requirement: Knowledge captured to vault with links
93
153
  check: knowledge-captured
94
154
  `,
95
- tools: `tools:
155
+ tools: `# Workflow tools — engine merges these into plan steps.
156
+ # Format: list of operation strings (agentId_facade op:operation_name)
157
+ tools:
96
158
  - soleri_vault op:search_intelligent
97
159
  - soleri_vault op:capture_knowledge
98
160
  - soleri_links op:link_entries
@@ -133,7 +195,9 @@ When fixing bugs, resolving errors, or addressing regressions.
133
195
  - If the bug reveals a pattern or anti-pattern, capture it: \`op:capture_knowledge\`
134
196
  - Complete orchestration: \`op:orchestrate_complete\`
135
197
  `,
136
- gates: `gates:
198
+ gates: `# Workflow gates — engine reads these and enforces them during plan execution.
199
+ # Format: phase (brainstorming|pre-execution|post-task|completion), requirement, check
200
+ gates:
137
201
  - phase: pre-execution
138
202
  requirement: Root cause identified and fix plan approved
139
203
  check: plan-approved
@@ -146,7 +210,9 @@ When fixing bugs, resolving errors, or addressing regressions.
146
210
  requirement: Anti-pattern captured if applicable
147
211
  check: knowledge-captured
148
212
  `,
149
- tools: `tools:
213
+ tools: `# Workflow tools — engine merges these into plan steps.
214
+ # Format: list of operation strings (agentId_facade op:operation_name)
215
+ tools:
150
216
  - soleri_vault op:search_intelligent
151
217
  - soleri_vault op:capture_knowledge
152
218
  - soleri_plan op:create_plan
@@ -180,12 +246,16 @@ When reviewing code, auditing quality, or checking for issues.
180
246
  ### 4. Capture
181
247
  - If review reveals new patterns or anti-patterns, capture them: \`op:capture_knowledge\`
182
248
  `,
183
- gates: `gates:
249
+ gates: `# Workflow gates — engine reads these and enforces them during plan execution.
250
+ # Format: phase (brainstorming|pre-execution|post-task|completion), requirement, check
251
+ gates:
184
252
  - phase: completion
185
253
  requirement: All blocking issues addressed
186
254
  check: issues-resolved
187
255
  `,
188
- tools: `tools:
256
+ tools: `# Workflow tools — engine merges these into plan steps.
257
+ # Format: list of operation strings (agentId_facade op:operation_name)
258
+ tools:
189
259
  - soleri_vault op:search_intelligent
190
260
  - soleri_vault op:capture_knowledge
191
261
  - soleri_brain op:recommend
@@ -217,7 +287,9 @@ Before crossing a context window boundary — \`/clear\`, context compaction, or
217
287
  - Use plan IDs to look up active plans: \`op:orchestrate_status\`
218
288
  - Continue from where the handoff left off
219
289
  `,
220
- gates: `gates:
290
+ gates: `# Workflow gates — engine reads these and enforces them during plan execution.
291
+ # Format: phase (brainstorming|pre-execution|post-task|completion), requirement, check
292
+ gates:
221
293
  - phase: pre-transition
222
294
  requirement: Handoff document generated with current state
223
295
  check: handoff-generated
@@ -226,7 +298,9 @@ Before crossing a context window boundary — \`/clear\`, context compaction, or
226
298
  requirement: New context has loaded handoff and can reference active plans
227
299
  check: context-restored
228
300
  `,
229
- tools: `tools:
301
+ tools: `# Workflow tools — engine merges these into plan steps.
302
+ # Format: list of operation strings (agentId_facade op:operation_name)
303
+ tools:
230
304
  - soleri_memory op:handoff_generate
231
305
  - soleri_memory op:session_capture
232
306
  - soleri_orchestrate op:orchestrate_status
@@ -235,6 +309,164 @@ Before crossing a context window boundary — \`/clear\`, context compaction, or
235
309
  },
236
310
  ];
237
311
 
312
+ // ─── Example Instruction Files ───────────────────────────────────────
313
+
314
+ const INSTRUCTIONS_CONVENTIONS = `# Conventions
315
+
316
+ <!-- Customize this file with your project's naming conventions, coding standards, and rules. -->
317
+ <!-- This file is composed into CLAUDE.md automatically — your agent will follow these rules. -->
318
+
319
+ ## Naming Conventions
320
+
321
+ - Use \`kebab-case\` for file and directory names
322
+ - Use \`camelCase\` for variables and functions
323
+ - Use \`PascalCase\` for classes, types, and interfaces
324
+ - Prefix private helpers with \`_\` (e.g., \`_validateInput\`)
325
+
326
+ ## File Organization
327
+
328
+ - Source code goes in \`src/\`
329
+ - Tests live next to the code they test (\`*.test.ts\`)
330
+ - Shared utilities go in \`src/utils/\`
331
+ - Types and interfaces go in \`src/types/\`
332
+
333
+ ## Code Standards
334
+
335
+ - Every function must have a JSDoc comment explaining its purpose
336
+ - Prefer \`const\` over \`let\`; never use \`var\`
337
+ - Maximum file length: 300 lines — split if larger
338
+ - No default exports — use named exports only
339
+
340
+ ## What to Avoid
341
+
342
+ - Do not add new npm dependencies without approval
343
+ - Do not use \`any\` type — use \`unknown\` and narrow
344
+ - Do not commit commented-out code
345
+ - Do not use hardcoded values — extract to constants or config
346
+ `;
347
+
348
+ const INSTRUCTIONS_GETTING_STARTED = `# Getting Started with Instructions
349
+
350
+ This folder contains your agent's custom behavioral rules. Every \`.md\` file here
351
+ is automatically composed into \`CLAUDE.md\` when you run \`soleri dev\`.
352
+
353
+ ## How It Works
354
+
355
+ 1. Create a new \`.md\` file in this folder (e.g., \`api-guidelines.md\`)
356
+ 2. Write your rules, conventions, or guidelines in Markdown
357
+ 3. Run \`soleri dev\` — it watches for changes and regenerates \`CLAUDE.md\`
358
+ 4. Your agent now follows these rules in every conversation
359
+
360
+ ## File Naming
361
+
362
+ - Files are included in **alphabetical order** (prefix with numbers to control order)
363
+ - \`_engine.md\` is auto-generated by Soleri — **do not edit it manually**
364
+ - \`domain.md\` was generated from your agent's domain config
365
+
366
+ ## Tips
367
+
368
+ - Keep each file focused on one topic (conventions, workflows, constraints)
369
+ - Use clear headings — your agent reads these as instructions
370
+ - Add "What to Avoid" sections — agents benefit from explicit anti-patterns
371
+ - See the [Soleri docs](https://soleri.ai/docs) for more examples
372
+ `;
373
+
374
+ // ─── Workspace & Routing Seeds ───────────────────────────────────────
375
+
376
+ /** Default workspaces seeded based on agent domains. */
377
+ const DOMAIN_WORKSPACE_SEEDS: Record<string, { id: string; name: string; description: string }[]> =
378
+ {
379
+ // Design-related domains
380
+ design: [
381
+ {
382
+ id: 'design',
383
+ name: 'Design',
384
+ description: 'Design system patterns, tokens, and components',
385
+ },
386
+ { id: 'review', name: 'Review', description: 'Design review and accessibility audits' },
387
+ ],
388
+ 'ui-design': [
389
+ { id: 'design', name: 'Design', description: 'UI design patterns, tokens, and components' },
390
+ { id: 'review', name: 'Review', description: 'Design review and accessibility audits' },
391
+ ],
392
+ accessibility: [
393
+ { id: 'design', name: 'Design', description: 'Accessible design patterns and tokens' },
394
+ { id: 'review', name: 'Review', description: 'Accessibility audits and compliance checks' },
395
+ ],
396
+ // Dev-related domains
397
+ architecture: [
398
+ {
399
+ id: 'planning',
400
+ name: 'Planning',
401
+ description: 'Architecture decisions and technical planning',
402
+ },
403
+ { id: 'src', name: 'Source', description: 'Implementation code and modules' },
404
+ { id: 'docs', name: 'Documentation', description: 'Technical documentation and ADRs' },
405
+ ],
406
+ backend: [
407
+ { id: 'planning', name: 'Planning', description: 'Backend architecture and API design' },
408
+ { id: 'src', name: 'Source', description: 'Implementation code and modules' },
409
+ { id: 'docs', name: 'Documentation', description: 'API documentation and guides' },
410
+ ],
411
+ frontend: [
412
+ {
413
+ id: 'planning',
414
+ name: 'Planning',
415
+ description: 'Frontend architecture and component design',
416
+ },
417
+ { id: 'src', name: 'Source', description: 'Implementation code and components' },
418
+ {
419
+ id: 'docs',
420
+ name: 'Documentation',
421
+ description: 'Component documentation and style guides',
422
+ },
423
+ ],
424
+ security: [
425
+ {
426
+ id: 'planning',
427
+ name: 'Planning',
428
+ description: 'Security architecture and threat modeling',
429
+ },
430
+ { id: 'src', name: 'Source', description: 'Security implementations and policies' },
431
+ { id: 'docs', name: 'Documentation', description: 'Security documentation and runbooks' },
432
+ ],
433
+ };
434
+
435
+ /** Default routing entries seeded based on agent domains. */
436
+ const DOMAIN_ROUTING_SEEDS: Record<
437
+ string,
438
+ { pattern: string; workspace: string; skills: string[] }[]
439
+ > = {
440
+ design: [
441
+ { pattern: 'design component', workspace: 'design', skills: ['vault-navigator'] },
442
+ { pattern: 'review design', workspace: 'review', skills: ['deep-review'] },
443
+ ],
444
+ 'ui-design': [
445
+ { pattern: 'design component', workspace: 'design', skills: ['vault-navigator'] },
446
+ { pattern: 'review design', workspace: 'review', skills: ['deep-review'] },
447
+ ],
448
+ architecture: [
449
+ { pattern: 'plan architecture', workspace: 'planning', skills: ['writing-plans'] },
450
+ { pattern: 'implement feature', workspace: 'src', skills: ['test-driven-development'] },
451
+ { pattern: 'write documentation', workspace: 'docs', skills: ['vault-capture'] },
452
+ ],
453
+ backend: [
454
+ { pattern: 'plan API', workspace: 'planning', skills: ['writing-plans'] },
455
+ { pattern: 'implement endpoint', workspace: 'src', skills: ['test-driven-development'] },
456
+ { pattern: 'write docs', workspace: 'docs', skills: ['vault-capture'] },
457
+ ],
458
+ frontend: [
459
+ { pattern: 'plan component', workspace: 'planning', skills: ['writing-plans'] },
460
+ { pattern: 'implement component', workspace: 'src', skills: ['test-driven-development'] },
461
+ { pattern: 'write docs', workspace: 'docs', skills: ['vault-capture'] },
462
+ ],
463
+ security: [
464
+ { pattern: 'threat model', workspace: 'planning', skills: ['writing-plans'] },
465
+ { pattern: 'implement policy', workspace: 'src', skills: ['test-driven-development'] },
466
+ { pattern: 'write runbook', workspace: 'docs', skills: ['vault-capture'] },
467
+ ],
468
+ };
469
+
238
470
  // ─── Main Scaffolder ──────────────────────────────────────────────────
239
471
 
240
472
  /**
@@ -324,6 +556,13 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
324
556
  'AGENTS.md',
325
557
  'instructions/_engine.md',
326
558
  '',
559
+ '# OS',
560
+ '.DS_Store',
561
+ '',
562
+ '# Editor / IDE state',
563
+ '.obsidian/',
564
+ '.opencode/',
565
+ '',
327
566
  ].join('\n'),
328
567
  filesCreated,
329
568
  );
@@ -332,6 +571,24 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
332
571
  writeFile(agentDir, 'instructions/_engine.md', getEngineRulesContent(), filesCreated);
333
572
 
334
573
  // ─── 6. Write user instruction files ────────────────────────
574
+ // Generate user.md — user-editable file with priority placement in CLAUDE.md
575
+ const userMdContent = [
576
+ '# Your Custom Rules',
577
+ '',
578
+ 'Add your agent-specific rules, constraints, and preferences here.',
579
+ 'This file gets priority placement in CLAUDE.md — it appears before engine rules.',
580
+ '',
581
+ '## Examples of what to put here:',
582
+ '- Project-specific conventions',
583
+ '- Communication preferences',
584
+ '- Domain expertise to emphasize',
585
+ '- Things to always/never do',
586
+ '',
587
+ 'Delete these instructions and replace with your own content.',
588
+ '',
589
+ ].join('\n');
590
+ writeFile(agentDir, 'instructions/user.md', userMdContent, filesCreated);
591
+
335
592
  // Generate domain-specific instruction file if agent has specialized domains
336
593
  if (config.domains.length > 0) {
337
594
  const domainLines = [
@@ -347,6 +604,15 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
347
604
  writeFile(agentDir, 'instructions/domain.md', domainLines.join('\n'), filesCreated);
348
605
  }
349
606
 
607
+ // ─── 6b. Write example instruction files ─────────────────────
608
+ writeFile(agentDir, 'instructions/conventions.md', INSTRUCTIONS_CONVENTIONS, filesCreated);
609
+ writeFile(
610
+ agentDir,
611
+ 'instructions/getting-started.md',
612
+ INSTRUCTIONS_GETTING_STARTED,
613
+ filesCreated,
614
+ );
615
+
350
616
  // ─── 7. Write workflows ─────────────────────────────────────
351
617
  for (const wf of BUILTIN_WORKFLOWS) {
352
618
  writeFile(agentDir, `workflows/${wf.name}/prompt.md`, wf.prompt, filesCreated);
@@ -355,7 +621,11 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
355
621
  }
356
622
 
357
623
  // ─── 8. Copy bundled skills (with placeholder substitution) ─
358
- const skills = generateSkills({ id: config.id } as AgentConfig);
624
+ const resolvedSkills = resolveSkillsFilter(config.skillsFilter);
625
+ const skills = generateSkills({
626
+ id: config.id,
627
+ skills: resolvedSkills ?? undefined,
628
+ } as AgentConfig);
359
629
  for (const [relativePath, content] of skills) {
360
630
  mkdirSync(join(agentDir, dirname(relativePath)), { recursive: true });
361
631
  writeFile(agentDir, relativePath, content, filesCreated);
@@ -381,7 +651,33 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
381
651
  totalSeeded += starterEntries.length;
382
652
  }
383
653
 
384
- // ─── 9. Generate CLAUDE.md ──────────────────────────────────
654
+ // ─── 9b. Create workspace directories with CONTEXT.md ──────
655
+ // Resolve workspaces: use explicit config or seed from domains
656
+ const resolvedWorkspaces = resolveWorkspaces(config);
657
+ if (resolvedWorkspaces.length > 0) {
658
+ for (const ws of resolvedWorkspaces) {
659
+ const wsDir = join(agentDir, 'workspaces', ws.id);
660
+ mkdirSync(wsDir, { recursive: true });
661
+ const contextContent = [
662
+ `# ${ws.name}`,
663
+ '',
664
+ ws.description,
665
+ '',
666
+ '## Instructions',
667
+ '',
668
+ `<!-- Add workspace-specific instructions here for the "${ws.name}" context. -->`,
669
+ '',
670
+ ].join('\n');
671
+ writeFile(
672
+ agentDir,
673
+ `workspaces/${ws.id}/${ws.contextFile ?? 'CONTEXT.md'}`,
674
+ contextContent,
675
+ filesCreated,
676
+ );
677
+ }
678
+ }
679
+
680
+ // ─── 10. Generate CLAUDE.md ──────────────────────────────────
385
681
  const { content: claudeMd } = composeClaudeMd(agentDir);
386
682
  writeFile(agentDir, 'CLAUDE.md', claudeMd, filesCreated);
387
683
 
@@ -476,6 +772,34 @@ function buildAgentYaml(config: AgentYaml): Record<string, unknown> {
476
772
  setup.model = config.setup.model;
477
773
  if (Object.keys(setup).length > 0) yaml.setup = setup;
478
774
 
775
+ // Skills filter — only include if not the default ('essential')
776
+ if (config.skillsFilter && config.skillsFilter !== 'essential') {
777
+ yaml.skillsFilter = config.skillsFilter;
778
+ }
779
+
780
+ // Workspaces
781
+ const resolvedWs = resolveWorkspaces(config);
782
+ if (resolvedWs.length > 0) {
783
+ yaml.workspaces = resolvedWs.map((ws) =>
784
+ Object.assign(
785
+ { id: ws.id, name: ws.name, description: ws.description },
786
+ ws.contextFile !== `CONTEXT.md` ? { contextFile: ws.contextFile } : {},
787
+ ),
788
+ );
789
+ }
790
+
791
+ // Routing
792
+ const resolvedRouting = resolveRouting(config);
793
+ if (resolvedRouting.length > 0) {
794
+ yaml.routing = resolvedRouting.map((r) =>
795
+ Object.assign(
796
+ { pattern: r.pattern, workspace: r.workspace },
797
+ r.context.length > 0 ? { context: r.context } : {},
798
+ r.skills.length > 0 ? { skills: r.skills } : {},
799
+ ),
800
+ );
801
+ }
802
+
479
803
  // Packs
480
804
  if (config.packs && config.packs.length > 0) {
481
805
  yaml.packs = config.packs;
@@ -484,6 +808,76 @@ function buildAgentYaml(config: AgentYaml): Record<string, unknown> {
484
808
  return yaml;
485
809
  }
486
810
 
811
+ // ─── Workspace & Routing Helpers ─────────────────────────────────────
812
+
813
+ /**
814
+ * Resolve workspaces: use explicit config or seed from domains.
815
+ * Deduplicates by workspace id.
816
+ */
817
+ function resolveWorkspaces(
818
+ config: AgentYaml,
819
+ ): { id: string; name: string; description: string; contextFile: string }[] {
820
+ // If explicitly defined, use those
821
+ if (config.workspaces && config.workspaces.length > 0) {
822
+ return config.workspaces.map((ws) => ({
823
+ id: ws.id,
824
+ name: ws.name,
825
+ description: ws.description,
826
+ contextFile: ws.contextFile ?? 'CONTEXT.md',
827
+ }));
828
+ }
829
+
830
+ // Otherwise, seed from domains
831
+ const seen = new Set<string>();
832
+ const workspaces: { id: string; name: string; description: string; contextFile: string }[] = [];
833
+
834
+ for (const domain of config.domains) {
835
+ const seeds = DOMAIN_WORKSPACE_SEEDS[domain];
836
+ if (!seeds) continue;
837
+ for (const seed of seeds) {
838
+ if (seen.has(seed.id)) continue;
839
+ seen.add(seed.id);
840
+ workspaces.push({ ...seed, contextFile: 'CONTEXT.md' });
841
+ }
842
+ }
843
+
844
+ return workspaces;
845
+ }
846
+
847
+ /**
848
+ * Resolve routing entries: use explicit config or seed from domains.
849
+ * Deduplicates by pattern string.
850
+ */
851
+ function resolveRouting(
852
+ config: AgentYaml,
853
+ ): { pattern: string; workspace: string; context: string[]; skills: string[] }[] {
854
+ // If explicitly defined, use those
855
+ if (config.routing && config.routing.length > 0) {
856
+ return config.routing.map((r) => ({
857
+ pattern: r.pattern,
858
+ workspace: r.workspace,
859
+ context: r.context ?? [],
860
+ skills: r.skills ?? [],
861
+ }));
862
+ }
863
+
864
+ // Otherwise, seed from domains
865
+ const seen = new Set<string>();
866
+ const routes: { pattern: string; workspace: string; context: string[]; skills: string[] }[] = [];
867
+
868
+ for (const domain of config.domains) {
869
+ const seeds = DOMAIN_ROUTING_SEEDS[domain];
870
+ if (!seeds) continue;
871
+ for (const seed of seeds) {
872
+ if (seen.has(seed.pattern)) continue;
873
+ seen.add(seed.pattern);
874
+ routes.push({ ...seed, context: [] });
875
+ }
876
+ }
877
+
878
+ return routes;
879
+ }
880
+
487
881
  // ─── Starter Pack Helpers ────────────────────────────────────────────
488
882
 
489
883
  /** 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(