@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
@@ -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>;
@@ -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. Engine rules NOT inlined (they are injected once into ~/.claude/CLAUDE.md
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
- // 7. User instructions (instructions/*.md, excluding _engine.md)
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())
@@ -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,
@@ -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 { scaffoldFileTree } from './scaffold-filetree.js';
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';