Skip to content

Commit b5903ff

Browse files
acreegerclaude
andauthored
Refactor: Apply template substitution to agent frontmatter and extract swarm defaults to agent files (#963)
* feat(agents): apply template substitution to agent frontmatter, move swarm defaults to agent files - Move Handlebars template substitution before frontmatter parsing in AgentManager.loadAgents() so model/effort fields can use conditionals - Add {{#if SWARM_MODE}} conditionals to 7 agent templates for model/effort - Remove hardcoded defaultSwarmModels/defaultSwarmEfforts maps from SwarmSetupService - Add static effort: high to wave-verifier and framework-detector agents - Fix Object.assign mutation of caller's templateVariables (use local copy) - Update tests, docs, and agent CLAUDE.md Fixes #962 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(docs): simplify agent model and effort level descriptions in iloom-commands * docs: remove implementation details from user-facing docs Remove internal details (env var names, file paths, agent rendering mechanics, confidence thresholds, subagent_type parameters) from iloom-commands.md. Keep behavior descriptions, replace mechanisms with user-visible outcomes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(agents): wrap entire effort line in conditional when no else branch Move Handlebars conditional to wrap the full `effort:` key-value line instead of just the value. Prevents `effort: ` (empty value) in non-swarm mode. Updated all 7 agent templates and CLAUDE.md example. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(agents): change complexity-evaluator swarm effort from low to high No reason to use low effort on a fast model (haiku). Also wrap entire effort line in conditionals when there's no else branch to avoid empty `effort: ` values in non-swarm mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: docs cleanup, complexity-evaluator effort, integration tests - Restore swarmModel explanation and Important note in docs - Remove implementation details from user-facing docs - Change complexity-evaluator swarm effort from low to high - Wrap effort lines in full conditionals (no empty values) - Add AgentManager integration tests with real template files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 544aac4 commit b5903ff

16 files changed

Lines changed: 468 additions & 116 deletions

docs/iloom-commands.md

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ The override follows a two-level model:
9999

100100
**Effort Override:**
101101

102-
The `--effort` flag controls Claude's reasoning depth via the `CLAUDE_CODE_EFFORT_LEVEL` environment variable:
102+
The `--effort` flag controls Claude's reasoning depth:
103103
- `low` - Quick, straightforward implementation with minimal overhead
104104
- `medium` - Balanced approach with standard implementation and testing
105105
- `high` - Comprehensive implementation with extensive testing and documentation
@@ -626,15 +626,13 @@ When `il spin` detects an epic loom (created via `il start --epic` or by confirm
626626

627627
1. **Fetches/refreshes child data** - Re-fetches child issue details and dependency map from the issue tracker
628628
2. **Creates child worktrees** - One worktree per child issue, branched off the epic branch, with dependencies installed
629-
3. **Renders swarm agents** - Writes swarm-mode agent templates to `.claude/agents/` in the epic worktree
630-
4. **Renders swarm worker agent** - Writes the iloom workflow as a custom agent type to `.claude/agents/iloom-swarm-worker.md`
631-
5. **Copies agents to child worktrees** - Copies `.claude/agents/` from the epic worktree to each child worktree so workers can resolve agent files locally
632-
6. **Launches orchestrator** - Starts Claude with agent teams enabled and `bypassPermissions` mode
629+
3. **Configures swarm agents** - Sets up the orchestrator and worker agents for the epic and all child worktrees
630+
4. **Launches orchestrator** - Starts the swarm orchestrator session
633631

634632
The orchestrator then:
635633
- Analyzes the dependency DAG to identify initially unblocked issues
636-
- Spawns parallel agents for all unblocked child issues simultaneously
637-
- Each agent uses the `iloom-swarm-worker` custom agent type, receiving the full iloom workflow as its system prompt
634+
- Spawns parallel worker agents for all unblocked child issues simultaneously
635+
- Each worker receives full issue context and implements its assigned issue autonomously
638636
- Completed work is rebased and fast-forward merged into the epic branch for clean linear history
639637
- Newly unblocked issues are spawned as their dependencies complete
640638
- Failed children are isolated and do not block unrelated issues
@@ -653,7 +651,7 @@ il spin --skip-cleanup
653651

654652
After all child agents complete and their work is merged into the epic branch, the orchestrator automatically runs a full code review using the `iloom-code-reviewer` agent. This catches cross-cutting issues that individual child agents miss because they each only see their own changes, not the integrated result.
655653

656-
If the review finds any issues (confidence score 80+), a fix agent is spawned to address them before the final commit. The review is non-blocking -- if the reviewer or fix agent fails, the swarm continues to finalization without interruption. Only a single review-fix pass is performed (no re-review loops).
654+
If the review finds issues, they are automatically fixed before the final commit. The review is non-blocking -- if the reviewer or fix agent fails, the swarm continues to finalization without interruption. Only a single review-fix pass is performed (no re-review loops).
657655

658656
Post-swarm review is enabled by default. To disable it, set `spin.postSwarmReview` to `false` in your settings:
659657

@@ -1908,12 +1906,9 @@ Only sibling dependencies (between child issues of the same epic) are included.
19081906

19091907
Each child agent runs in complete isolation:
19101908

1911-
1. The orchestrator spawns the agent with `subagent_type: "iloom-swarm-worker"`, passing the child's issue number, title, worktree path, and issue body in the Task prompt
1912-
2. The agent's system prompt contains the full iloom issue workflow adapted for swarm mode (high-authority instructions)
1913-
3. The agent implements the issue autonomously in its own worktree (branched off the epic branch)
1914-
4. On completion, the agent reports back to the orchestrator with status and summary
1915-
1916-
The orchestrator uses `bypassPermissions` mode and Claude's agent teams feature, both set automatically.
1909+
1. The orchestrator spawns a worker for each child issue, passing its issue number, title, worktree path, and issue body
1910+
2. The worker implements the issue autonomously in its own worktree (branched off the epic branch)
1911+
3. On completion, the worker reports back to the orchestrator with status and summary
19171912

19181913
**Worker Model Configuration:**
19191914

@@ -1965,9 +1960,9 @@ With the configuration above:
19651960

19661961
| Agent | Non-swarm mode | Swarm mode |
19671962
|-------|---------------|------------|
1968-
| `iloom-issue-implementer` | `opus` | `sonnet` (swarmModel) |
1969-
| `iloom-issue-complexity-evaluator` | `haiku` | `haiku` (swarmModel) |
1970-
| `iloom-issue-analyzer` | `.md` default | `opus` (Balanced mode default) |
1963+
| `iloom-issue-implementer` | `opus` (settings) | `sonnet` (swarmModel) |
1964+
| `iloom-issue-complexity-evaluator` | `haiku` (settings) | `haiku` (swarmModel) |
1965+
| `iloom-issue-analyzer` | `opus` (default) | `opus` (default) |
19711966

19721967
**Example using the `--set` flag:**
19731968

@@ -2077,13 +2072,13 @@ Example settings for each mode:
20772072
}
20782073
```
20792074

2080-
These modes use `swarmModel` on phase agents (not `model`), so non-swarm behavior is preserved. When agents run outside of swarm mode, their base `model` setting is used. Mode settings merge with existing agent settings — only the `swarmModel` (and worker `model`) fields are overwritten.
2075+
These modes only affect swarm behavior — non-swarm sessions continue using each agent's base `model` setting.
20812076

20822077
To configure, run `il init` — you'll be asked during setup, or you can change it later in the advanced configuration section.
20832078

20842079
### Effort Configuration
20852080

2086-
Effort levels control Claude's reasoning depth. iloom propagates effort to Claude Code via the `CLAUDE_CODE_EFFORT_LEVEL` environment variable for top-level sessions and via per-agent `effort:` frontmatter for agent-level overrides.
2081+
Effort levels control Claude's reasoning depth.
20872082

20882083
**Valid effort levels:** `low`, `medium`, `high`, `max`
20892084

@@ -2106,7 +2101,7 @@ Configure default effort levels for spin and plan commands in `.iloom/settings.j
21062101

21072102
**Swarm Orchestrator Effort:**
21082103

2109-
Set the effort level for the swarm orchestrator using `spin.swarmEffort`. This defaults to `medium` when not configured (matching the current hardcoded behavior):
2104+
Set the effort level for the swarm orchestrator using `spin.swarmEffort`. This defaults to `medium` when not configured:
21102105

21112106
```json
21122107
{
@@ -2148,7 +2143,7 @@ When no user configuration is provided, swarm agents use these defaults:
21482143
| `iloom-issue-implementer` | `medium` |
21492144
| `iloom-issue-enhancer` | `medium` |
21502145
| `iloom-code-reviewer` | `medium` |
2151-
| `iloom-issue-complexity-evaluator` | `low` |
2146+
| `iloom-issue-complexity-evaluator` | `high` |
21522147

21532148
**Effort Resolution Order:**
21542149

@@ -2159,8 +2154,6 @@ Effort is resolved with the following priority (highest first):
21592154
3. Settings (`spin.effort` / `plan.effort`)
21602155
4. No effort set (defers to Claude Code default)
21612156

2162-
For per-agent effort, Claude Code resolves: agent frontmatter `effort:` > `CLAUDE_CODE_EFFORT_LEVEL` env var > session default.
2163-
21642157
**Example using the `--set` flag:**
21652158

21662159
```bash
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { describe, it, expect } from 'vitest'
2+
import path from 'path'
3+
import { AgentManager } from './AgentManager.js'
4+
import { PromptTemplateManager } from './PromptTemplateManager.js'
5+
6+
/**
7+
* Integration test for AgentManager + PromptTemplateManager + real agent templates.
8+
*
9+
* These tests read the actual agent markdown files from templates/agents/,
10+
* run real Handlebars substitution, and verify the end-to-end result of
11+
* frontmatter parsing with template variables.
12+
*/
13+
14+
// Resolve the real templates/agents/ directory relative to the project root
15+
const PROJECT_ROOT = path.resolve(import.meta.dirname, '..', '..')
16+
const AGENTS_DIR = path.join(PROJECT_ROOT, 'templates', 'agents')
17+
const PROMPTS_DIR = path.join(PROJECT_ROOT, 'templates', 'prompts')
18+
19+
// All agent names expected in the templates/agents/ directory
20+
const ALL_AGENT_NAMES = [
21+
'iloom-artifact-reviewer',
22+
'iloom-code-reviewer',
23+
'iloom-framework-detector',
24+
'iloom-issue-analyze-and-plan',
25+
'iloom-issue-analyzer',
26+
'iloom-issue-complexity-evaluator',
27+
'iloom-issue-enhancer',
28+
'iloom-issue-implementer',
29+
'iloom-issue-planner',
30+
'iloom-wave-verifier',
31+
]
32+
33+
describe('AgentManager integration (real templates)', () => {
34+
// Use real PromptTemplateManager and real agent files
35+
const templateManager = new PromptTemplateManager(PROMPTS_DIR)
36+
37+
describe('loadAgents with SWARM_MODE=true', () => {
38+
it('should load all agents successfully', async () => {
39+
const manager = new AgentManager(AGENTS_DIR, templateManager)
40+
const agents = await manager.loadAgents(undefined, { SWARM_MODE: true })
41+
42+
const loadedNames = Object.keys(agents).sort()
43+
expect(loadedNames).toEqual(ALL_AGENT_NAMES)
44+
})
45+
46+
it('should resolve swarm-mode model overrides from Handlebars conditionals', async () => {
47+
const manager = new AgentManager(AGENTS_DIR, templateManager)
48+
const agents = await manager.loadAgents(undefined, { SWARM_MODE: true })
49+
50+
// Agents with conditional model: sonnet in swarm mode
51+
expect(agents['iloom-issue-implementer'].model).toBe('sonnet')
52+
expect(agents['iloom-issue-planner'].model).toBe('sonnet')
53+
expect(agents['iloom-issue-enhancer'].model).toBe('sonnet')
54+
expect(agents['iloom-code-reviewer'].model).toBe('sonnet')
55+
56+
// Agents with unconditional model (always opus regardless of mode)
57+
expect(agents['iloom-issue-analyzer'].model).toBe('opus')
58+
expect(agents['iloom-issue-analyze-and-plan'].model).toBe('opus')
59+
expect(agents['iloom-wave-verifier'].model).toBe('opus')
60+
expect(agents['iloom-framework-detector'].model).toBe('opus')
61+
expect(agents['iloom-artifact-reviewer'].model).toBe('opus')
62+
expect(agents['iloom-issue-complexity-evaluator'].model).toBe('haiku')
63+
})
64+
65+
it('should resolve swarm-mode effort defaults from Handlebars conditionals', async () => {
66+
const manager = new AgentManager(AGENTS_DIR, templateManager)
67+
const agents = await manager.loadAgents(undefined, { SWARM_MODE: true })
68+
69+
// Agents with conditional effort: {{#if SWARM_MODE}}effort: <level>{{/if}}
70+
expect(agents['iloom-issue-analyzer'].effort).toBe('high')
71+
expect(agents['iloom-issue-planner'].effort).toBe('high')
72+
expect(agents['iloom-issue-implementer'].effort).toBe('medium')
73+
expect(agents['iloom-issue-enhancer'].effort).toBe('medium')
74+
expect(agents['iloom-code-reviewer'].effort).toBe('medium')
75+
expect(agents['iloom-issue-complexity-evaluator'].effort).toBe('high')
76+
expect(agents['iloom-issue-analyze-and-plan'].effort).toBe('high')
77+
78+
// Agents with unconditional effort (always set regardless of SWARM_MODE)
79+
expect(agents['iloom-wave-verifier'].effort).toBe('high')
80+
expect(agents['iloom-framework-detector'].effort).toBe('high')
81+
82+
// Agents with no effort field at all
83+
expect(agents['iloom-artifact-reviewer'].effort).toBeUndefined()
84+
})
85+
86+
it('should have non-empty prompts for all agents', async () => {
87+
const manager = new AgentManager(AGENTS_DIR, templateManager)
88+
const agents = await manager.loadAgents(undefined, { SWARM_MODE: true })
89+
90+
for (const [name, config] of Object.entries(agents)) {
91+
expect(config.prompt.length, `${name} should have a non-empty prompt`).toBeGreaterThan(0)
92+
}
93+
})
94+
95+
it('should have non-empty descriptions for all agents', async () => {
96+
const manager = new AgentManager(AGENTS_DIR, templateManager)
97+
const agents = await manager.loadAgents(undefined, { SWARM_MODE: true })
98+
99+
for (const [name, config] of Object.entries(agents)) {
100+
expect(config.description.length, `${name} should have a non-empty description`).toBeGreaterThan(0)
101+
}
102+
})
103+
})
104+
105+
describe('loadAgents with SWARM_MODE=false (non-swarm)', () => {
106+
it('should load all agents successfully', async () => {
107+
const manager = new AgentManager(AGENTS_DIR, templateManager)
108+
const agents = await manager.loadAgents(undefined, { SWARM_MODE: false })
109+
110+
const loadedNames = Object.keys(agents).sort()
111+
expect(loadedNames).toEqual(ALL_AGENT_NAMES)
112+
})
113+
114+
it('should resolve non-swarm model defaults', async () => {
115+
const manager = new AgentManager(AGENTS_DIR, templateManager)
116+
const agents = await manager.loadAgents(undefined, { SWARM_MODE: false })
117+
118+
// Agents with conditional model resolve to opus in non-swarm mode
119+
expect(agents['iloom-issue-implementer'].model).toBe('opus')
120+
expect(agents['iloom-issue-planner'].model).toBe('opus')
121+
expect(agents['iloom-issue-enhancer'].model).toBe('opus')
122+
expect(agents['iloom-code-reviewer'].model).toBe('opus')
123+
124+
// Agents with unconditional model are the same regardless of mode
125+
expect(agents['iloom-issue-analyzer'].model).toBe('opus')
126+
expect(agents['iloom-issue-analyze-and-plan'].model).toBe('opus')
127+
expect(agents['iloom-wave-verifier'].model).toBe('opus')
128+
expect(agents['iloom-framework-detector'].model).toBe('opus')
129+
expect(agents['iloom-artifact-reviewer'].model).toBe('opus')
130+
expect(agents['iloom-issue-complexity-evaluator'].model).toBe('haiku')
131+
})
132+
133+
it('should have undefined effort for agents that only set effort in swarm mode', async () => {
134+
const manager = new AgentManager(AGENTS_DIR, templateManager)
135+
const agents = await manager.loadAgents(undefined, { SWARM_MODE: false })
136+
137+
// These agents use {{#if SWARM_MODE}}effort: <level>{{/if}} - resolves to empty/undefined
138+
expect(agents['iloom-issue-analyzer'].effort).toBeUndefined()
139+
expect(agents['iloom-issue-planner'].effort).toBeUndefined()
140+
expect(agents['iloom-issue-implementer'].effort).toBeUndefined()
141+
expect(agents['iloom-issue-enhancer'].effort).toBeUndefined()
142+
expect(agents['iloom-code-reviewer'].effort).toBeUndefined()
143+
expect(agents['iloom-issue-complexity-evaluator'].effort).toBeUndefined()
144+
expect(agents['iloom-issue-analyze-and-plan'].effort).toBeUndefined()
145+
146+
// These agents have unconditional effort - always present
147+
expect(agents['iloom-wave-verifier'].effort).toBe('high')
148+
expect(agents['iloom-framework-detector'].effort).toBe('high')
149+
150+
// No effort field at all
151+
expect(agents['iloom-artifact-reviewer'].effort).toBeUndefined()
152+
})
153+
})
154+
155+
describe('loadAgents without templateVariables (no substitution)', () => {
156+
it('should load all agents successfully even without template variables', async () => {
157+
const manager = new AgentManager(AGENTS_DIR, templateManager)
158+
const agents = await manager.loadAgents()
159+
160+
const loadedNames = Object.keys(agents).sort()
161+
expect(loadedNames).toEqual(ALL_AGENT_NAMES)
162+
})
163+
})
164+
165+
describe('swarm vs non-swarm model differences', () => {
166+
it('should produce different models for swarm-conditional agents', async () => {
167+
const manager = new AgentManager(AGENTS_DIR, templateManager)
168+
169+
const swarmAgents = await manager.loadAgents(undefined, { SWARM_MODE: true })
170+
const nonSwarmAgents = await manager.loadAgents(undefined, { SWARM_MODE: false })
171+
172+
// Agents that change model between swarm and non-swarm
173+
const conditionalModelAgents = [
174+
'iloom-issue-implementer',
175+
'iloom-issue-planner',
176+
'iloom-issue-enhancer',
177+
'iloom-code-reviewer',
178+
]
179+
180+
for (const name of conditionalModelAgents) {
181+
expect(
182+
swarmAgents[name].model,
183+
`${name} swarm model should be sonnet`,
184+
).toBe('sonnet')
185+
expect(
186+
nonSwarmAgents[name].model,
187+
`${name} non-swarm model should be opus`,
188+
).toBe('opus')
189+
}
190+
})
191+
192+
it('should produce different effort levels for swarm-conditional agents', async () => {
193+
const manager = new AgentManager(AGENTS_DIR, templateManager)
194+
195+
const swarmAgents = await manager.loadAgents(undefined, { SWARM_MODE: true })
196+
const nonSwarmAgents = await manager.loadAgents(undefined, { SWARM_MODE: false })
197+
198+
// Agents that only have effort in swarm mode
199+
const conditionalEffortAgents = [
200+
'iloom-issue-analyzer',
201+
'iloom-issue-planner',
202+
'iloom-issue-implementer',
203+
'iloom-issue-enhancer',
204+
'iloom-code-reviewer',
205+
'iloom-issue-complexity-evaluator',
206+
'iloom-issue-analyze-and-plan',
207+
]
208+
209+
for (const name of conditionalEffortAgents) {
210+
expect(
211+
swarmAgents[name].effort,
212+
`${name} should have effort in swarm mode`,
213+
).toBeDefined()
214+
expect(
215+
nonSwarmAgents[name].effort,
216+
`${name} should NOT have effort in non-swarm mode`,
217+
).toBeUndefined()
218+
}
219+
})
220+
})
221+
})

0 commit comments

Comments
 (0)