@soleri/forge 9.8.0 → 9.10.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.
@@ -13,7 +13,8 @@ import { fileURLToPath } from 'node:url';
13
13
  import { stringify as yamlStringify } from 'yaml';
14
14
  import type { AgentYaml, AgentYamlInput } from './agent-schema.js';
15
15
  import { AgentYamlSchema } from './agent-schema.js';
16
- import { getEngineRulesContent } from './templates/shared-rules.js';
16
+ import { getModularEngineRules } from './templates/shared-rules.js';
17
+ import type { EngineFeature } from './templates/shared-rules.js';
17
18
  import { composeClaudeMd } from './compose-claude-md.js';
18
19
  import { generateSkills } from './templates/skills.js';
19
20
  import type { AgentConfig } from './types.js';
@@ -133,7 +134,9 @@ When building a new feature, adding functionality, or creating components.
133
134
  - Link new entries to related knowledge: \`op:link_entries\`
134
135
  - Complete orchestration: \`op:orchestrate_complete\`
135
136
  `,
136
- gates: `gates:
137
+ gates: `# Workflow gates — engine reads these and enforces them during plan execution.
138
+ # Format: phase (brainstorming|pre-execution|post-task|completion), requirement, check
139
+ gates:
137
140
  - phase: brainstorming
138
141
  requirement: Requirements are clear and user has approved the approach
139
142
  check: user-approval
@@ -150,7 +153,9 @@ When building a new feature, adding functionality, or creating components.
150
153
  requirement: Knowledge captured to vault with links
151
154
  check: knowledge-captured
152
155
  `,
153
- tools: `tools:
156
+ tools: `# Workflow tools — engine merges these into plan steps.
157
+ # Format: list of operation strings (agentId_facade op:operation_name)
158
+ tools:
154
159
  - soleri_vault op:search_intelligent
155
160
  - soleri_vault op:capture_knowledge
156
161
  - soleri_links op:link_entries
@@ -191,7 +196,9 @@ When fixing bugs, resolving errors, or addressing regressions.
191
196
  - If the bug reveals a pattern or anti-pattern, capture it: \`op:capture_knowledge\`
192
197
  - Complete orchestration: \`op:orchestrate_complete\`
193
198
  `,
194
- gates: `gates:
199
+ gates: `# Workflow gates — engine reads these and enforces them during plan execution.
200
+ # Format: phase (brainstorming|pre-execution|post-task|completion), requirement, check
201
+ gates:
195
202
  - phase: pre-execution
196
203
  requirement: Root cause identified and fix plan approved
197
204
  check: plan-approved
@@ -204,7 +211,9 @@ When fixing bugs, resolving errors, or addressing regressions.
204
211
  requirement: Anti-pattern captured if applicable
205
212
  check: knowledge-captured
206
213
  `,
207
- tools: `tools:
214
+ tools: `# Workflow tools — engine merges these into plan steps.
215
+ # Format: list of operation strings (agentId_facade op:operation_name)
216
+ tools:
208
217
  - soleri_vault op:search_intelligent
209
218
  - soleri_vault op:capture_knowledge
210
219
  - soleri_plan op:create_plan
@@ -238,12 +247,16 @@ When reviewing code, auditing quality, or checking for issues.
238
247
  ### 4. Capture
239
248
  - If review reveals new patterns or anti-patterns, capture them: \`op:capture_knowledge\`
240
249
  `,
241
- gates: `gates:
250
+ gates: `# Workflow gates — engine reads these and enforces them during plan execution.
251
+ # Format: phase (brainstorming|pre-execution|post-task|completion), requirement, check
252
+ gates:
242
253
  - phase: completion
243
254
  requirement: All blocking issues addressed
244
255
  check: issues-resolved
245
256
  `,
246
- tools: `tools:
257
+ tools: `# Workflow tools — engine merges these into plan steps.
258
+ # Format: list of operation strings (agentId_facade op:operation_name)
259
+ tools:
247
260
  - soleri_vault op:search_intelligent
248
261
  - soleri_vault op:capture_knowledge
249
262
  - soleri_brain op:recommend
@@ -275,7 +288,9 @@ Before crossing a context window boundary — \`/clear\`, context compaction, or
275
288
  - Use plan IDs to look up active plans: \`op:orchestrate_status\`
276
289
  - Continue from where the handoff left off
277
290
  `,
278
- gates: `gates:
291
+ gates: `# Workflow gates — engine reads these and enforces them during plan execution.
292
+ # Format: phase (brainstorming|pre-execution|post-task|completion), requirement, check
293
+ gates:
279
294
  - phase: pre-transition
280
295
  requirement: Handoff document generated with current state
281
296
  check: handoff-generated
@@ -284,7 +299,9 @@ Before crossing a context window boundary — \`/clear\`, context compaction, or
284
299
  requirement: New context has loaded handoff and can reference active plans
285
300
  check: context-restored
286
301
  `,
287
- tools: `tools:
302
+ tools: `# Workflow tools — engine merges these into plan steps.
303
+ # Format: list of operation strings (agentId_facade op:operation_name)
304
+ tools:
288
305
  - soleri_memory op:handoff_generate
289
306
  - soleri_memory op:session_capture
290
307
  - soleri_orchestrate op:orchestrate_status
@@ -551,8 +568,14 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
551
568
  filesCreated,
552
569
  );
553
570
 
554
- // ─── 5. Write engine rules ──────────────────────────────────
555
- writeFile(agentDir, 'instructions/_engine.md', getEngineRulesContent(), filesCreated);
571
+ // ─── 5. Write engine rules (modular — respects engine.features) ─────
572
+ const engineFeatures = config.engine?.features as EngineFeature[] | undefined;
573
+ writeFile(
574
+ agentDir,
575
+ 'instructions/_engine.md',
576
+ getModularEngineRules(engineFeatures),
577
+ filesCreated,
578
+ );
556
579
 
557
580
  // ─── 6. Write user instruction files ────────────────────────
558
581
  // Generate user.md — user-editable file with priority placement in CLAUDE.md
@@ -8,10 +8,37 @@ description: >
8
8
 
9
9
  # Subagent-Driven Development
10
10
 
11
- Decompose work into isolated units, dispatch subagents via the Agent tool, merge results back. You are the controller — you never implement, you orchestrate.
11
+ Decompose work into isolated units, dispatch subagents via the Agent tool, merge results back. You are the orchestrator — you make all decisions, subagents execute.
12
12
 
13
13
  **Announce at start:** "I'm using the subagent-driven-development skill to dispatch isolated agents."
14
14
 
15
+ ## The Orchestrator Contract
16
+
17
+ **You are the boss. Subagents are the crew.**
18
+
19
+ 1. **All decisions stay with the orchestrator.** Research the task, consult the vault, decide the approach. Subagents receive exact specs — scope, file boundaries, acceptance criteria. They execute, they don't decide.
20
+ 2. **Subagents MUST NOT create plans.** Only the orchestrator creates plans. Subagent prompts must explicitly state: "Do NOT create plans, do NOT call planning tools."
21
+ 3. **If a subagent hits ambiguity, it returns — it doesn't guess.** The orchestrator resolves, then re-dispatches.
22
+ 4. **The orchestrator reconciles all work.** After subagents return, the orchestrator reviews changes, merges, captures knowledge.
23
+
24
+ ## Hybrid Agent Routing
25
+
26
+ Not all subagents are equal. Route by complexity:
27
+
28
+ | Signal | Agent Type | Why |
29
+ | ------------------------------------- | ------------------------- | ----------------------------- |
30
+ | Single file, clear spec, no decisions | **Claude Code worker** | Fast, low overhead |
31
+ | Approach already in parent plan | **Claude Code worker** | Spec is decided |
32
+ | 3+ files, cross-cutting concerns | **Soleri agent instance** | Needs vault, brain, lifecycle |
33
+ | Unresolved design decisions | **Soleri agent instance** | Needs judgment |
34
+ | New dependencies or architecture | **Soleri agent instance** | Needs full context |
35
+
36
+ **User overrides:**
37
+
38
+ - "Use full agent for everything" → all Soleri agent instances
39
+ - "Just use workers" → all Claude Code workers
40
+ - Default: hybrid routing
41
+
15
42
  ## When to Dispatch
16
43
 
17
44
  | Signal | Dispatch? |
@@ -25,47 +52,87 @@ Decompose work into isolated units, dispatch subagents via the Agent tool, merge
25
52
 
26
53
  ## The Process
27
54
 
28
- ### Step 1: Decompose
55
+ ### Step 1: Research & Decide (Orchestrator only)
56
+
57
+ Read all relevant files. Consult the vault for patterns. Make every design decision. Define the exact spec for each subagent task: files to touch, approach to use, acceptance criteria.
29
58
 
30
- Break work into discrete units. For each, determine: files involved, dependencies on other units, conflict risk. Only units with no file overlap and no inter-dependency qualify for dispatch.
59
+ ### Step 2: Decompose & Route
31
60
 
32
- ### Step 2: Dispatch with Worktree Isolation
61
+ Break work into discrete units. For each, determine: files involved, dependencies on other units, conflict risk, complexity. Assign agent type per the routing table.
62
+
63
+ ### Step 3: Dispatch
64
+
65
+ Present the dispatch table to the user:
33
66
 
34
67
  ```
35
- Agent(prompt: "<task prompt>", isolation: "worktree")
68
+ ## Dispatching N tasks in parallel
69
+
70
+ | # | Task | Agent | Why |
71
+ |---|------|-------|-----|
72
+ | 1 | Description | Worker / Instance | Routing reason |
36
73
  ```
37
74
 
38
- Each subagent prompt must include: (1) task scope, (2) file boundaries, (3) acceptance criteria, (4) rules — no commits, no out-of-scope changes, run tests before reporting.
75
+ Each subagent prompt must include:
76
+
77
+ - Task scope and file boundaries
78
+ - Acceptance criteria
79
+ - "Do NOT create plans. Do NOT make design decisions. Execute this spec exactly."
80
+ - For Soleri instances: "Activate, execute, run orchestrate_complete when done."
39
81
 
40
82
  Launch all independent subagents in a **single message** so they run in parallel.
83
+ Use `isolation: "worktree"` for file-modifying tasks.
41
84
 
42
- ### Step 3: Review and Merge
85
+ ### Step 4: Review and Merge
43
86
 
44
87
  For each returning subagent:
45
88
 
46
89
  1. **Review** — read actual file changes (do not trust self-reports alone), verify tests pass, check scope compliance
47
90
  2. **Merge** — `git merge` or `git cherry-pick` from the worktree branch, one at a time
48
91
  3. **Test** — run the full suite after each merge; only proceed if green
49
- 4. **Conflicts** — resolve manually, re-run tests, capture as anti-pattern for future planning
92
+ 4. **Conflicts** — resolve manually, re-run tests, capture as anti-pattern
93
+
94
+ ### Step 5: Reconcile & Report
95
+
96
+ After all merges, report to the user:
50
97
 
51
- After all merges, capture learnings:
98
+ **Minimal (default):**
52
99
 
53
100
  ```
54
- YOUR_AGENT_core op:capture_quick params:{
55
- title: "subagent dispatch outcome",
56
- description: "<which tasks parallelized well, which conflicted>"
57
- }
101
+ N/N complete. M patterns captured to vault.
102
+ → Decisions: [any design decisions the orchestrator made]
103
+ ```
104
+
105
+ **Detailed (on request):**
106
+
58
107
  ```
108
+ | # | Task | Agent | Status | Knowledge |
109
+ |---|------|-------|--------|-----------|
110
+ | 1 | Desc | Worker | Done ✓ | — |
111
+ | 2 | Desc | Instance | Done ✓ | 2 patterns |
112
+ ```
113
+
114
+ Capture learnings to vault. Run `orchestrate_complete` for the parent plan.
115
+
116
+ ## Worktree Cleanup Guarantee
117
+
118
+ Three layers — nothing accumulates:
119
+
120
+ 1. **Per-task:** `finally` block in dispatcher removes worktree after each task
121
+ 2. **Per-batch:** `cleanupAll()` runs after all subagents complete
122
+ 3. **Per-session:** `SessionStart` hook prunes orphaned worktrees
59
123
 
60
124
  ## Anti-Patterns
61
125
 
62
- | Anti-Pattern | Why It Fails |
63
- | -------------------------------------------- | --------------------------------------------- |
64
- | Dispatching for a 5-line fix | Startup overhead exceeds the work |
65
- | Parallel dispatch of dependent tasks | Second agent works on stale assumptions |
66
- | Skipping worktree isolation for nearby files | Silent overwrites between agents |
67
- | Trusting self-reports without reading code | Agents miss edge cases or misunderstand scope |
68
- | Dispatching 10+ agents at once | Review bottleneck shifts to the controller |
126
+ | Anti-Pattern | Why It Fails |
127
+ | -------------------------------------------- | --------------------------------------------------- |
128
+ | Subagent creating its own plan | Stale plans accumulate, lifecycle never completes |
129
+ | Subagent making design decisions | Inconsistent approaches, orchestrator loses control |
130
+ | Dispatching for a 5-line fix | Startup overhead exceeds the work |
131
+ | Parallel dispatch of dependent tasks | Second agent works on stale assumptions |
132
+ | Skipping worktree isolation for nearby files | Silent overwrites between agents |
133
+ | Trusting self-reports without reading code | Agents miss edge cases or misunderstand scope |
134
+ | Dispatching 10+ agents at once | Review bottleneck shifts to the controller |
135
+ | Not cleaning up worktrees after merge | Disk bloat, stale branch accumulation |
69
136
 
70
137
  ## Merge Strategy
71
138
 
@@ -70,10 +70,15 @@ export function generateInjectClaudeMd(config: AgentConfig): string {
70
70
  ' * `<!-- soleri:engine-rules -->`. If already present, they are updated.',
71
71
  ' * Agent block (identity + facade table) is injected under',
72
72
  ` * \`<!-- ${marker} -->\`.`,
73
+ ' *',
74
+ ' * @param skipEngineRules - If true, skip engine rules injection (used for global files)',
73
75
  ' */',
74
- 'function injectIntoFile(filePath: string): InjectResult {',
75
- ' // Step 1: Engine rules — shared across all agents',
76
- ' const engineAction = injectBlock(filePath, getEngineRulesContent(), getEngineRulesMarker());',
76
+ 'function injectIntoFile(filePath: string, skipEngineRules = false): InjectResult {',
77
+ ' // Step 1: Engine rules — shared across all agents (skip for global files)',
78
+ ' let engineAction: string = "skipped";',
79
+ ' if (!skipEngineRules) {',
80
+ ' engineAction = injectBlock(filePath, getEngineRulesContent(), getEngineRulesMarker());',
81
+ ' }',
77
82
  '',
78
83
  ' // Step 2: Agent-specific block',
79
84
  ' const agentAction = injectBlock(filePath, getClaudeMdContent(), getClaudeMdMarker());',
@@ -99,13 +104,24 @@ export function generateInjectClaudeMd(config: AgentConfig): string {
99
104
  ' * Inject into the global ~/.claude/CLAUDE.md.',
100
105
  " * Creates ~/.claude/ directory if it doesn't exist.",
101
106
  ' * This makes the activation phrase work in any project.',
107
+ ' * Engine rules are NOT injected globally — they live in project-level CLAUDE.md only.',
102
108
  ' */',
103
109
  'export function injectClaudeMdGlobal(): InjectResult {',
104
110
  " const claudeDir = join(homedir(), '.claude');",
105
111
  ' if (!existsSync(claudeDir)) {',
106
112
  ' mkdirSync(claudeDir, { recursive: true });',
107
113
  ' }',
108
- " return injectIntoFile(join(claudeDir, 'CLAUDE.md'));",
114
+ " const filePath = join(claudeDir, 'CLAUDE.md');",
115
+ '',
116
+ ' // Add header comment for new files',
117
+ ' if (!existsSync(filePath)) {',
118
+ " writeFileSync(filePath, '<!-- Global agent activation file. Engine rules live in project-level CLAUDE.md only. -->\\n', 'utf-8');",
119
+ ' }',
120
+ '',
121
+ ' // Self-heal: strip engine rules if they leaked into the global file',
122
+ ' removeBlock(filePath, getEngineRulesMarker());',
123
+ '',
124
+ ' return injectIntoFile(filePath, true);',
109
125
  '}',
110
126
  '',
111
127
  '/**',
@@ -154,6 +170,24 @@ export function generateInjectClaudeMd(config: AgentConfig): string {
154
170
  '}',
155
171
  '',
156
172
  '/**',
173
+ ' * Remove engine rules from the global ~/.claude/CLAUDE.md.',
174
+ ' * Self-healing: strips engine rules that should not be in the global file.',
175
+ ' */',
176
+ 'export function removeEngineRulesFromGlobal(): { removed: boolean; path: string } {',
177
+ " const filePath = join(homedir(), '.claude', 'CLAUDE.md');",
178
+ ' return { removed: removeBlock(filePath, getEngineRulesMarker()), path: filePath };',
179
+ '}',
180
+ '',
181
+ '/**',
182
+ ' * Remove engine rules from the global ~/.config/opencode/AGENTS.md.',
183
+ ' * Self-healing: strips engine rules that should not be in the global file.',
184
+ ' */',
185
+ 'export function removeEngineRulesFromGlobalAgentsMd(): { removed: boolean; path: string } {',
186
+ " const filePath = join(homedir(), '.config', 'opencode', 'AGENTS.md');",
187
+ ' return { removed: removeBlock(filePath, getEngineRulesMarker()), path: filePath };',
188
+ '}',
189
+ '',
190
+ '/**',
157
191
  ' * Check if the agent marker exists in a CLAUDE.md file.',
158
192
  ' */',
159
193
  'export function hasAgentMarker(filePath: string): boolean {',
@@ -186,13 +220,24 @@ export function generateInjectClaudeMd(config: AgentConfig): string {
186
220
  ' * Inject into the global ~/.config/opencode/AGENTS.md.',
187
221
  " * Creates ~/.config/opencode/ directory if it doesn't exist.",
188
222
  ' * This makes the activation phrase work in any OpenCode project.',
223
+ ' * Engine rules are NOT injected globally — they live in project-level AGENTS.md only.',
189
224
  ' */',
190
225
  'export function injectAgentsMdGlobal(): InjectResult {',
191
226
  " const opencodeDir = join(homedir(), '.config', 'opencode');",
192
227
  ' if (!existsSync(opencodeDir)) {',
193
228
  ' mkdirSync(opencodeDir, { recursive: true });',
194
229
  ' }',
195
- " return injectIntoFile(join(opencodeDir, 'AGENTS.md'));",
230
+ " const filePath = join(opencodeDir, 'AGENTS.md');",
231
+ '',
232
+ ' // Add header comment for new files',
233
+ ' if (!existsSync(filePath)) {',
234
+ " writeFileSync(filePath, '<!-- Global agent activation file. Engine rules live in project-level AGENTS.md only. -->\\n', 'utf-8');",
235
+ ' }',
236
+ '',
237
+ ' // Self-heal: strip engine rules if they leaked into the global file',
238
+ ' removeBlock(filePath, getEngineRulesMarker());',
239
+ '',
240
+ ' return injectIntoFile(filePath, true);',
196
241
  '}',
197
242
  '',
198
243
  '/**',
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Section parser for marker-delimited engine rules content.
3
+ *
4
+ * Extracts `<!-- soleri:xxx -->` sections from the engine rules markdown,
5
+ * enabling selective inclusion of feature modules.
6
+ *
7
+ * Single-pass: splits on `## ` headings, maps chunks to markers, filters.
8
+ */
9
+
10
+ export interface ParsedSection {
11
+ /** e.g. 'soleri:response-integrity' */
12
+ marker: string;
13
+ /** Full text including heading and marker comment */
14
+ content: string;
15
+ }
16
+
17
+ export interface ParsedContent {
18
+ /** Everything before the first section */
19
+ preamble: string;
20
+ /** Ordered list of parsed sections */
21
+ sections: ParsedSection[];
22
+ /** Closing marker line and anything after */
23
+ closing: string;
24
+ }
25
+
26
+ const SECTION_MARKER_RE = /<!-- (soleri:[a-z-]+) -->/;
27
+ const CLOSING_MARKER = '<!-- /soleri:engine-rules -->';
28
+
29
+ /**
30
+ * Parse marker-delimited sections from engine rules content.
31
+ *
32
+ * Strategy: split on `## ` heading boundaries, then classify each chunk
33
+ * as preamble, section (has a marker), or closing (has closing marker).
34
+ */
35
+ export function parseSections(content: string): ParsedContent {
36
+ // Split at each `## ` heading — lookahead preserves the heading in the chunk
37
+ const chunks = content.split(/(?=^## )/m);
38
+
39
+ let preamble = '';
40
+ const sections: ParsedSection[] = [];
41
+ let closing = '';
42
+ let foundFirstSection = false;
43
+
44
+ for (const chunk of chunks) {
45
+ // Check if this chunk contains the closing marker
46
+ const closingIdx = chunk.indexOf(CLOSING_MARKER);
47
+ if (closingIdx !== -1) {
48
+ // Content before closing marker belongs to last section or preamble
49
+ const beforeClosing = chunk.slice(0, closingIdx);
50
+ const afterClosing = chunk.slice(closingIdx);
51
+
52
+ if (beforeClosing.trim()) {
53
+ const markerMatch = beforeClosing.match(SECTION_MARKER_RE);
54
+ if (markerMatch && markerMatch[1] !== 'soleri:engine-rules') {
55
+ sections.push({ marker: markerMatch[1], content: beforeClosing });
56
+ foundFirstSection = true;
57
+ } else if (!foundFirstSection) {
58
+ preamble += beforeClosing;
59
+ }
60
+ }
61
+ closing = afterClosing;
62
+ continue;
63
+ }
64
+
65
+ // Check if chunk has a section marker
66
+ const markerMatch = chunk.match(SECTION_MARKER_RE);
67
+ if (markerMatch && markerMatch[1] !== 'soleri:engine-rules') {
68
+ sections.push({ marker: markerMatch[1], content: chunk });
69
+ foundFirstSection = true;
70
+ } else {
71
+ // No marker — this is preamble (before first section)
72
+ if (!foundFirstSection) {
73
+ preamble += chunk;
74
+ }
75
+ }
76
+ }
77
+
78
+ return { preamble, sections, closing };
79
+ }
80
+
81
+ /**
82
+ * Rebuild content from parsed sections, including only allowed markers.
83
+ */
84
+ export function filterSections(parsed: ParsedContent, allowedMarkers: Set<string>): string {
85
+ const parts: string[] = [parsed.preamble];
86
+
87
+ for (const section of parsed.sections) {
88
+ if (allowedMarkers.has(section.marker)) {
89
+ let text = section.content;
90
+ if (!text.endsWith('\n')) text += '\n';
91
+ parts.push(text);
92
+ }
93
+ }
94
+
95
+ parts.push(parsed.closing);
96
+ return parts.join('\n');
97
+ }