@soleri/forge 9.9.0 → 9.11.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.
@@ -11,6 +11,57 @@ import { parse as parseYaml } from 'yaml';
11
11
  import { AgentYamlSchema, type AgentYaml } from './agent-schema.js';
12
12
  import { ENGINE_MODULE_MANIFEST, CORE_KEY_OPS } from '@soleri/core/module-manifest';
13
13
 
14
+ // ─── User Custom Zone ────────────────────────────────────────────────
15
+
16
+ const USER_CUSTOM_OPEN = '<!-- user:custom -->';
17
+ const USER_CUSTOM_CLOSE = '<!-- /user:custom -->';
18
+
19
+ const DEFAULT_USER_CUSTOM_BLOCK = [
20
+ USER_CUSTOM_OPEN,
21
+ '<!-- Add your custom instructions here. This section survives regeneration. -->',
22
+ USER_CUSTOM_CLOSE,
23
+ ].join('\n');
24
+
25
+ /**
26
+ * Extract content between `<!-- user:custom -->` and `<!-- /user:custom -->` markers.
27
+ * Returns the full block (including markers) or null if not found.
28
+ */
29
+ export function extractUserCustomZone(content: string): string | null {
30
+ const openIdx = content.indexOf(USER_CUSTOM_OPEN);
31
+ if (openIdx === -1) return null;
32
+
33
+ const closeIdx = content.indexOf(USER_CUSTOM_CLOSE, openIdx);
34
+ if (closeIdx === -1) return null;
35
+
36
+ return content.slice(openIdx, closeIdx + USER_CUSTOM_CLOSE.length);
37
+ }
38
+
39
+ /**
40
+ * Inject a user:custom zone into composed CLAUDE.md content.
41
+ * Replaces the existing user:custom block if present, otherwise inserts
42
+ * before the engine-rules-ref marker (or appends at the end).
43
+ */
44
+ export function injectUserCustomZone(content: string, userZone: string): string {
45
+ // If the content already has a user:custom block, replace it
46
+ const existing = extractUserCustomZone(content);
47
+ if (existing) {
48
+ return content.replace(existing, userZone);
49
+ }
50
+
51
+ // Insert before engine-rules-ref if present
52
+ const engineRefMarker = '<!-- soleri:engine-rules-ref -->';
53
+ const engineIdx = content.indexOf(engineRefMarker);
54
+ if (engineIdx !== -1) {
55
+ return content.slice(0, engineIdx) + userZone + '\n\n' + content.slice(engineIdx);
56
+ }
57
+
58
+ // Fallback: append before the last newline
59
+ if (content.endsWith('\n')) {
60
+ return content.slice(0, -1) + '\n\n' + userZone + '\n';
61
+ }
62
+ return content + '\n\n' + userZone + '\n';
63
+ }
64
+
14
65
  // ─── Types ────────────────────────────────────────────────────────────
15
66
 
16
67
  export interface ComposedClaudeMd {
@@ -37,6 +88,15 @@ export interface ToolEntry {
37
88
  export function composeClaudeMd(agentDir: string, tools?: ToolEntry[]): ComposedClaudeMd {
38
89
  const sources: string[] = [];
39
90
 
91
+ // 0. Read existing CLAUDE.md to preserve user:custom zone
92
+ const existingClaudeMdPath = join(agentDir, 'CLAUDE.md');
93
+ let preservedUserZone: string | null = null;
94
+ let existingContent: string | null = null;
95
+ if (existsSync(existingClaudeMdPath)) {
96
+ existingContent = readFileSync(existingClaudeMdPath, 'utf-8');
97
+ preservedUserZone = extractUserCustomZone(existingContent);
98
+ }
99
+
40
100
  // 1. Read agent.yaml
41
101
  const agentYamlPath = join(agentDir, 'agent.yaml');
42
102
  const agentYaml = AgentYamlSchema.parse(parseYaml(readFileSync(agentYamlPath, 'utf-8')));
@@ -128,10 +188,74 @@ export function composeClaudeMd(agentDir: string, tools?: ToolEntry[]): Composed
128
188
  if (skillsSection) sections.push(skillsSection);
129
189
  }
130
190
 
191
+ // 10. Inject user:custom zone (preserved from old file, or default empty block)
192
+ const userZone = preservedUserZone ?? DEFAULT_USER_CUSTOM_BLOCK;
193
+ sections.splice(findUserCustomInsertIndex(sections), 0, userZone);
194
+
131
195
  const content = sections.join('\n\n') + '\n';
196
+
197
+ // 11. Detect orphaned content in old file (compare against newly composed content)
198
+ if (existingContent) {
199
+ detectOrphanedContent(existingContent, content);
200
+ }
201
+
132
202
  return { content, sources };
133
203
  }
134
204
 
205
+ // ─── User Custom Zone Helpers ────────────────────────────────────────
206
+
207
+ /**
208
+ * Find the insertion index for the user:custom zone in the sections array.
209
+ * Target: after user instructions (instructions/user.md content), before engine-rules-ref.
210
+ */
211
+ function findUserCustomInsertIndex(sections: string[]): number {
212
+ // Look for the engine-rules-ref marker
213
+ const engineRefIdx = sections.findIndex((s) => s.includes('<!-- soleri:engine-rules-ref -->'));
214
+ if (engineRefIdx !== -1) return engineRefIdx;
215
+
216
+ // Fallback: insert before the last section
217
+ return sections.length;
218
+ }
219
+
220
+ /**
221
+ * Detect content in existing CLAUDE.md that was manually added outside of
222
+ * the user:custom zone. Compares the old file's headings against a freshly
223
+ * composed version — any heading in the old file that doesn't appear in
224
+ * the new composition (and isn't inside user:custom) is orphaned.
225
+ *
226
+ * @param existingContent - The current CLAUDE.md content before regeneration
227
+ * @param newContent - The freshly composed CLAUDE.md content
228
+ */
229
+ function detectOrphanedContent(existingContent: string, newContent: string): void {
230
+ // Extract headings from old file (excluding those inside user:custom zone)
231
+ const oldHeadings = extractHeadingsOutsideUserCustom(existingContent);
232
+ if (oldHeadings.length === 0) return;
233
+
234
+ // Extract headings from new composed content
235
+ const newHeadingSet = new Set(newContent.match(/^#{1,3} .+$/gm)?.map((h) => h.trim()) ?? []);
236
+
237
+ // Find headings in old that don't exist in new
238
+ const orphaned = oldHeadings.filter((h) => !newHeadingSet.has(h));
239
+
240
+ if (orphaned.length > 0) {
241
+ process.stderr.write(
242
+ '\u26a0 CLAUDE.md contains content outside managed sections. ' +
243
+ 'Move to instructions/ or <!-- user:custom --> to preserve across regeneration.\n',
244
+ );
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Extract markdown headings from content, excluding any inside the user:custom zone.
250
+ */
251
+ function extractHeadingsOutsideUserCustom(content: string): string[] {
252
+ // Remove user:custom zone content first
253
+ const userZone = extractUserCustomZone(content);
254
+ const cleaned = userZone ? content.replace(userZone, '') : content;
255
+
256
+ return cleaned.match(/^#{1,3} .+$/gm)?.map((h) => h.trim()) ?? [];
257
+ }
258
+
135
259
  // ─── Section Composers ────────────────────────────────────────────────
136
260
 
137
261
  function composeIdentityBlock(agent: AgentYaml): string {
package/src/lib.ts CHANGED
@@ -34,7 +34,13 @@ export { composeClaudeMd } from './compose-claude-md.js';
34
34
  export type { ComposedClaudeMd, ToolEntry } from './compose-claude-md.js';
35
35
  export { generateExtensionsIndex, generateExampleOp } from './templates/extensions.js';
36
36
  export { generateClaudeMdTemplate } from './templates/claude-md-template.js';
37
- export { getEngineRulesContent, getEngineMarker } from './templates/shared-rules.js';
37
+ export {
38
+ getEngineRulesContent,
39
+ getEngineMarker,
40
+ getModularEngineRules,
41
+ ENGINE_FEATURES,
42
+ } from './templates/shared-rules.js';
43
+ export type { EngineFeature } from './templates/shared-rules.js';
38
44
  export { generateInjectClaudeMd } from './templates/inject-claude-md.js';
39
45
  export { generateSkills } from './templates/skills.js';
40
46
  export { generateTelegramBot } from './templates/telegram-bot.js';
@@ -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';
@@ -567,8 +568,14 @@ export function scaffoldFileTree(input: AgentYamlInput, outputDir: string): File
567
568
  filesCreated,
568
569
  );
569
570
 
570
- // ─── 5. Write engine rules ──────────────────────────────────
571
- 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
+ );
572
579
 
573
580
  // ─── 6. Write user instruction files ────────────────────────
574
581
  // Generate user.md — user-editable file with priority placement in CLAUDE.md
@@ -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
+ }
@@ -9,6 +9,8 @@
9
9
  * Uses op:name syntax — the active agent provides the tool prefix.
10
10
  */
11
11
 
12
+ import { parseSections, filterSections } from './section-parser.js';
13
+
12
14
  const ENGINE_MARKER = 'soleri:engine-rules';
13
15
 
14
16
  export function getEngineMarker(): string {
@@ -20,6 +22,83 @@ export function getEngineRulesContent(): string {
20
22
  return ENGINE_RULES_LINES.join('\n');
21
23
  }
22
24
 
25
+ // ─── Modular Engine Rules ────────────────────────────────────────────
26
+
27
+ /** Feature modules that can be selectively included in engine rules. */
28
+ export type EngineFeature = 'vault' | 'planning' | 'brain' | 'advanced';
29
+
30
+ /** All available feature modules. */
31
+ export const ENGINE_FEATURES: readonly EngineFeature[] = [
32
+ 'vault',
33
+ 'planning',
34
+ 'brain',
35
+ 'advanced',
36
+ ] as const;
37
+
38
+ /**
39
+ * Section markers grouped by module.
40
+ *
41
+ * 'core' sections are always included.
42
+ * Feature modules are included only when requested (or when no filter is specified).
43
+ */
44
+ const MODULE_SECTIONS: Record<'core' | EngineFeature, readonly string[]> = {
45
+ core: [
46
+ 'soleri:what-is-soleri',
47
+ 'soleri:response-integrity',
48
+ 'soleri:tool-schema-validation',
49
+ 'soleri:memory-quality',
50
+ 'soleri:output-formatting',
51
+ 'soleri:clean-commits',
52
+ 'soleri:intent-detection',
53
+ 'soleri:overlay-mode',
54
+ 'soleri:session',
55
+ 'soleri:getting-started',
56
+ 'soleri:cli',
57
+ 'soleri:persona-self-update',
58
+ 'soleri:workspace-routing',
59
+ ],
60
+ vault: [
61
+ 'soleri:vault-protocol',
62
+ 'soleri:knowledge-capture',
63
+ 'soleri:tool-advocacy',
64
+ 'soleri:cross-project',
65
+ ],
66
+ planning: [
67
+ 'soleri:planning',
68
+ 'soleri:workflow-overrides',
69
+ 'soleri:yolo-mode',
70
+ 'soleri:task-routing',
71
+ 'soleri:validation-loop',
72
+ 'soleri:verification-protocol',
73
+ ],
74
+ brain: ['soleri:brain', 'soleri:model-routing'],
75
+ advanced: ['soleri:subagent-identity'],
76
+ };
77
+
78
+ /**
79
+ * Returns engine rules with only selected feature modules included.
80
+ *
81
+ * Core rules are ALWAYS included. Feature modules are included when:
82
+ * - `features` is undefined/empty → ALL modules included (backward compatible)
83
+ * - `features` is specified → only listed modules + core
84
+ *
85
+ * @param features - Feature modules to include. Omit for all.
86
+ */
87
+ export function getModularEngineRules(features?: EngineFeature[]): string {
88
+ if (!features || features.length === 0) {
89
+ return getEngineRulesContent();
90
+ }
91
+
92
+ const allowedMarkers = new Set<string>(MODULE_SECTIONS.core);
93
+ for (const feature of features) {
94
+ const sections = MODULE_SECTIONS[feature];
95
+ if (sections) for (const m of sections) allowedMarkers.add(m);
96
+ }
97
+
98
+ const parsed = parseSections(getEngineRulesContent());
99
+ return filterSections(parsed, allowedMarkers);
100
+ }
101
+
23
102
  const ENGINE_RULES_LINES: string[] = [
24
103
  `<!-- ${ENGINE_MARKER} -->`,
25
104
  '',
@@ -383,6 +462,8 @@ const ENGINE_RULES_LINES: string[] = [
383
462
  '- Knowledge to vault (patterns learned, decisions made)',
384
463
  '- Session summary (what was done, files changed)',
385
464
  "- Brain feedback (what worked, what didn't)",
465
+ '- Evidence report — git diff vs plan tasks (accuracy score, verdicts per task)',
466
+ '- Fix-trail quality signals — clean first-try tasks strengthen brain patterns, high-rework tasks (2+ fix iterations) flag anti-patterns',
386
467
  '',
387
468
  'Without completion, the knowledge trail is lost. The code is in git, but the WHY disappears.',
388
469
  '',
@@ -659,6 +740,7 @@ const ENGINE_RULES_LINES: string[] = [
659
740
  '|----------------|---------|',
660
741
  '| Engine + CLI | `npx @soleri/cli@latest upgrade` or `soleri upgrade` |',
661
742
  '| Agent templates | `soleri agent refresh` (regenerates CLAUDE.md from latest engine) |',
743
+ '| Any agent by path | `soleri agent refresh --path ~/projects/my-agent` (from any directory) |',
662
744
  '| Knowledge packs | `soleri pack update` |',
663
745
  '| Check for updates | `soleri agent status` or `soleri agent update --check` |',
664
746
  '',
@@ -683,28 +765,28 @@ const ENGINE_RULES_LINES: string[] = [
683
765
  '',
684
766
  '### How Your CLAUDE.md is Built',
685
767
  '',
686
- 'Your CLAUDE.md is **auto-generated** — never edit it manually. It gets regenerated by `composeClaudeMd()` in these scenarios:',
687
- '',
688
- '| Trigger | How |',
689
- '|---------|-----|',
690
- '| `soleri dev` | Hot-reloads and regenerates on file changes |',
691
- '| `soleri agent refresh` | Explicitly regenerates from latest templates |',
692
- '| `soleri agent update` | After engine update, regenerates to pick up new rules |',
693
- '| Scaffold (`create-soleri`) | Generates initial CLAUDE.md for new agents |',
768
+ 'Your CLAUDE.md is **auto-generated** — never edit it manually (except inside `<!-- user:custom -->` markers). Regenerated by `soleri dev`, `soleri agent refresh`, `soleri agent update`, and on scaffold.',
694
769
  '',
695
770
  'The composition pipeline assembles CLAUDE.md from:',
696
771
  '',
697
772
  '1. **Agent identity** — from `agent.yaml`',
698
773
  '2. **Custom instructions** — from `instructions/user.md` (priority placement, before engine rules)',
699
- '3. **Engine rules** — from `@soleri/forge` shared rules (this section)',
774
+ '3. **Engine rules** — modular, controlled by `engine.features` in `agent.yaml`',
700
775
  '4. **User instructions** — from `instructions/*.md` (alphabetically sorted, excluding `user.md` and `_engine.md`)',
701
776
  '5. **Tools table** — from engine registration',
702
777
  '6. **Workflow index** — from `workflows/`',
703
778
  '7. **Skills index** — from `skills/`',
704
779
  '',
705
- '`instructions/user.md` is the recommended place for your most important agent-specific rules it appears early in CLAUDE.md for maximum influence on model behavior. Other `instructions/*.md` files are included after engine rules.',
780
+ '**Modular engine rules:** The `engine.features` array in `agent.yaml` controls which rule modules are included. Available: `vault`, `planning`, `brain`, `advanced`. Core rules are always included. Default (no features specified) = all modules.',
781
+ '',
782
+ '### What Survives Regeneration',
706
783
  '',
707
- 'When the engine updates (`@soleri/core` or `@soleri/forge`), running `soleri agent refresh` regenerates CLAUDE.md with the latest shared rules. Your own `instructions/*.md` files are where agent-specific behavior lives — those survive regeneration.',
784
+ '| Source | Survives? |',
785
+ '|--------|-----------|',
786
+ '| `instructions/*.md` | Yes — re-read on every regen |',
787
+ '| `<!-- user:custom -->` zone in CLAUDE.md | Yes — extracted and re-injected |',
788
+ '| `agent.yaml` | Drives regen (source of truth) |',
789
+ '| Manual CLAUDE.md edits outside markers | No — overwritten, warning logged |',
708
790
  '',
709
791
  '### System Requirements',
710
792
  '',
@@ -723,10 +805,10 @@ const ENGINE_RULES_LINES: string[] = [
723
805
  '',
724
806
  '| Command | What it does |',
725
807
  '|---------|-------------|',
726
- '| `soleri agent status` | Health check — version, packs, vault, update availability |',
727
- '| `soleri agent update` | Update engine to latest compatible version (`--check` for dry run) |',
728
- '| `soleri agent refresh` | Regenerate AGENTS.md/CLAUDE.md from latest forge templates (`--dry-run` to preview) |',
729
- '| `soleri agent diff` | Show drift between current templates and latest engine |',
808
+ '| `soleri agent status` | Health check — version, packs, vault, update availability (`--path <dir>`) |',
809
+ '| `soleri agent update` | Update engine to latest compatible version (`--check`, `--path <dir>`) |',
810
+ '| `soleri agent refresh` | Regenerate AGENTS.md/CLAUDE.md from latest forge templates (`--dry-run`, `--path <dir>`) |',
811
+ '| `soleri agent diff` | Show drift between current templates and latest engine (`--path <dir>`) |',
730
812
  '| `soleri doctor` | Full system health and project status check |',
731
813
  '| `soleri dev` | Run agent in development mode (stdio MCP) |',
732
814
  '| `soleri test` | Run agent tests (`--watch`, `--coverage`) |',