@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.
- package/dist/agent-schema.d.ts +11 -0
- package/dist/agent-schema.js +8 -0
- package/dist/agent-schema.js.map +1 -1
- package/dist/compose-claude-md.d.ts +11 -0
- package/dist/compose-claude-md.js +104 -0
- package/dist/compose-claude-md.js.map +1 -1
- package/dist/lib.d.ts +2 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/scaffold-filetree.js +28 -11
- package/dist/scaffold-filetree.js.map +1 -1
- package/dist/skills/subagent-driven-development/SKILL.md +87 -20
- package/dist/templates/inject-claude-md.js +50 -5
- package/dist/templates/inject-claude-md.js.map +1 -1
- package/dist/templates/section-parser.d.ts +33 -0
- package/dist/templates/section-parser.js +75 -0
- package/dist/templates/section-parser.js.map +1 -0
- package/dist/templates/shared-rules.d.ts +14 -0
- package/dist/templates/shared-rules.js +192 -11
- package/dist/templates/shared-rules.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/compose-claude-md.test.ts +89 -0
- package/src/__tests__/scaffold-filetree.test.ts +93 -0
- package/src/__tests__/scaffolder.test.ts +7 -5
- package/src/__tests__/shared-rules.test.ts +99 -1
- package/src/agent-schema.ts +8 -0
- package/src/compose-claude-md.ts +124 -0
- package/src/lib.ts +7 -1
- package/src/scaffold-filetree.ts +34 -11
- package/src/skills/subagent-driven-development/SKILL.md +87 -20
- package/src/templates/inject-claude-md.ts +50 -5
- package/src/templates/section-parser.ts +97 -0
- package/src/templates/shared-rules.ts +203 -11
package/src/scaffold-filetree.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
59
|
+
### Step 2: Decompose & Route
|
|
31
60
|
|
|
32
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
98
|
+
**Minimal (default):**
|
|
52
99
|
|
|
53
100
|
```
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
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
|
-
'
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
+
}
|