Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 16 additions & 23 deletions docs/iloom-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ The override follows a two-level model:

**Effort Override:**

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

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

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

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.

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).
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).

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

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

Each child agent runs in complete isolation:

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
2. The agent's system prompt contains the full iloom issue workflow adapted for swarm mode (high-authority instructions)
3. The agent implements the issue autonomously in its own worktree (branched off the epic branch)
4. On completion, the agent reports back to the orchestrator with status and summary

The orchestrator uses `bypassPermissions` mode and Claude's agent teams feature, both set automatically.
1. The orchestrator spawns a worker for each child issue, passing its issue number, title, worktree path, and issue body
2. The worker implements the issue autonomously in its own worktree (branched off the epic branch)
3. On completion, the worker reports back to the orchestrator with status and summary

**Worker Model Configuration:**

Expand Down Expand Up @@ -1965,9 +1960,9 @@ With the configuration above:

| Agent | Non-swarm mode | Swarm mode |
|-------|---------------|------------|
| `iloom-issue-implementer` | `opus` | `sonnet` (swarmModel) |
| `iloom-issue-complexity-evaluator` | `haiku` | `haiku` (swarmModel) |
| `iloom-issue-analyzer` | `.md` default | `opus` (Balanced mode default) |
| `iloom-issue-implementer` | `opus` (settings) | `sonnet` (swarmModel) |
| `iloom-issue-complexity-evaluator` | `haiku` (settings) | `haiku` (swarmModel) |
| `iloom-issue-analyzer` | `opus` (default) | `opus` (default) |

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

Expand Down Expand Up @@ -2077,13 +2072,13 @@ Example settings for each mode:
}
```

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.
These modes only affect swarm behavior — non-swarm sessions continue using each agent's base `model` setting.

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

### Effort Configuration

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.
Effort levels control Claude's reasoning depth.

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

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

**Swarm Orchestrator Effort:**

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

```json
{
Expand Down Expand Up @@ -2148,7 +2143,7 @@ When no user configuration is provided, swarm agents use these defaults:
| `iloom-issue-implementer` | `medium` |
| `iloom-issue-enhancer` | `medium` |
| `iloom-code-reviewer` | `medium` |
| `iloom-issue-complexity-evaluator` | `low` |
| `iloom-issue-complexity-evaluator` | `high` |

**Effort Resolution Order:**

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

For per-agent effort, Claude Code resolves: agent frontmatter `effort:` > `CLAUDE_CODE_EFFORT_LEVEL` env var > session default.

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

```bash
Expand Down
221 changes: 221 additions & 0 deletions src/lib/AgentManager.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { describe, it, expect } from 'vitest'
import path from 'path'
import { AgentManager } from './AgentManager.js'
import { PromptTemplateManager } from './PromptTemplateManager.js'

/**
* Integration test for AgentManager + PromptTemplateManager + real agent templates.
*
* These tests read the actual agent markdown files from templates/agents/,
* run real Handlebars substitution, and verify the end-to-end result of
* frontmatter parsing with template variables.
*/

// Resolve the real templates/agents/ directory relative to the project root
const PROJECT_ROOT = path.resolve(import.meta.dirname, '..', '..')
const AGENTS_DIR = path.join(PROJECT_ROOT, 'templates', 'agents')
const PROMPTS_DIR = path.join(PROJECT_ROOT, 'templates', 'prompts')

// All agent names expected in the templates/agents/ directory
const ALL_AGENT_NAMES = [
'iloom-artifact-reviewer',
'iloom-code-reviewer',
'iloom-framework-detector',
'iloom-issue-analyze-and-plan',
'iloom-issue-analyzer',
'iloom-issue-complexity-evaluator',
'iloom-issue-enhancer',
'iloom-issue-implementer',
'iloom-issue-planner',
'iloom-wave-verifier',
]

describe('AgentManager integration (real templates)', () => {
// Use real PromptTemplateManager and real agent files
const templateManager = new PromptTemplateManager(PROMPTS_DIR)

describe('loadAgents with SWARM_MODE=true', () => {
it('should load all agents successfully', async () => {
const manager = new AgentManager(AGENTS_DIR, templateManager)
const agents = await manager.loadAgents(undefined, { SWARM_MODE: true })

const loadedNames = Object.keys(agents).sort()
expect(loadedNames).toEqual(ALL_AGENT_NAMES)
})

it('should resolve swarm-mode model overrides from Handlebars conditionals', async () => {
const manager = new AgentManager(AGENTS_DIR, templateManager)
const agents = await manager.loadAgents(undefined, { SWARM_MODE: true })

// Agents with conditional model: sonnet in swarm mode
expect(agents['iloom-issue-implementer'].model).toBe('sonnet')
expect(agents['iloom-issue-planner'].model).toBe('sonnet')
expect(agents['iloom-issue-enhancer'].model).toBe('sonnet')
expect(agents['iloom-code-reviewer'].model).toBe('sonnet')

// Agents with unconditional model (always opus regardless of mode)
expect(agents['iloom-issue-analyzer'].model).toBe('opus')
expect(agents['iloom-issue-analyze-and-plan'].model).toBe('opus')
expect(agents['iloom-wave-verifier'].model).toBe('opus')
expect(agents['iloom-framework-detector'].model).toBe('opus')
expect(agents['iloom-artifact-reviewer'].model).toBe('opus')
expect(agents['iloom-issue-complexity-evaluator'].model).toBe('haiku')
})

it('should resolve swarm-mode effort defaults from Handlebars conditionals', async () => {
const manager = new AgentManager(AGENTS_DIR, templateManager)
const agents = await manager.loadAgents(undefined, { SWARM_MODE: true })

// Agents with conditional effort: {{#if SWARM_MODE}}effort: <level>{{/if}}
expect(agents['iloom-issue-analyzer'].effort).toBe('high')
expect(agents['iloom-issue-planner'].effort).toBe('high')
expect(agents['iloom-issue-implementer'].effort).toBe('medium')
expect(agents['iloom-issue-enhancer'].effort).toBe('medium')
expect(agents['iloom-code-reviewer'].effort).toBe('medium')
expect(agents['iloom-issue-complexity-evaluator'].effort).toBe('high')
expect(agents['iloom-issue-analyze-and-plan'].effort).toBe('high')

// Agents with unconditional effort (always set regardless of SWARM_MODE)
expect(agents['iloom-wave-verifier'].effort).toBe('high')
expect(agents['iloom-framework-detector'].effort).toBe('high')

// Agents with no effort field at all
expect(agents['iloom-artifact-reviewer'].effort).toBeUndefined()
})

it('should have non-empty prompts for all agents', async () => {
const manager = new AgentManager(AGENTS_DIR, templateManager)
const agents = await manager.loadAgents(undefined, { SWARM_MODE: true })

for (const [name, config] of Object.entries(agents)) {
expect(config.prompt.length, `${name} should have a non-empty prompt`).toBeGreaterThan(0)
}
})

it('should have non-empty descriptions for all agents', async () => {
const manager = new AgentManager(AGENTS_DIR, templateManager)
const agents = await manager.loadAgents(undefined, { SWARM_MODE: true })

for (const [name, config] of Object.entries(agents)) {
expect(config.description.length, `${name} should have a non-empty description`).toBeGreaterThan(0)
}
})
})

describe('loadAgents with SWARM_MODE=false (non-swarm)', () => {
it('should load all agents successfully', async () => {
const manager = new AgentManager(AGENTS_DIR, templateManager)
const agents = await manager.loadAgents(undefined, { SWARM_MODE: false })

const loadedNames = Object.keys(agents).sort()
expect(loadedNames).toEqual(ALL_AGENT_NAMES)
})

it('should resolve non-swarm model defaults', async () => {
const manager = new AgentManager(AGENTS_DIR, templateManager)
const agents = await manager.loadAgents(undefined, { SWARM_MODE: false })

// Agents with conditional model resolve to opus in non-swarm mode
expect(agents['iloom-issue-implementer'].model).toBe('opus')
expect(agents['iloom-issue-planner'].model).toBe('opus')
expect(agents['iloom-issue-enhancer'].model).toBe('opus')
expect(agents['iloom-code-reviewer'].model).toBe('opus')

// Agents with unconditional model are the same regardless of mode
expect(agents['iloom-issue-analyzer'].model).toBe('opus')
expect(agents['iloom-issue-analyze-and-plan'].model).toBe('opus')
expect(agents['iloom-wave-verifier'].model).toBe('opus')
expect(agents['iloom-framework-detector'].model).toBe('opus')
expect(agents['iloom-artifact-reviewer'].model).toBe('opus')
expect(agents['iloom-issue-complexity-evaluator'].model).toBe('haiku')
})

it('should have undefined effort for agents that only set effort in swarm mode', async () => {
const manager = new AgentManager(AGENTS_DIR, templateManager)
const agents = await manager.loadAgents(undefined, { SWARM_MODE: false })

// These agents use {{#if SWARM_MODE}}effort: <level>{{/if}} - resolves to empty/undefined
expect(agents['iloom-issue-analyzer'].effort).toBeUndefined()
expect(agents['iloom-issue-planner'].effort).toBeUndefined()
expect(agents['iloom-issue-implementer'].effort).toBeUndefined()
expect(agents['iloom-issue-enhancer'].effort).toBeUndefined()
expect(agents['iloom-code-reviewer'].effort).toBeUndefined()
expect(agents['iloom-issue-complexity-evaluator'].effort).toBeUndefined()
expect(agents['iloom-issue-analyze-and-plan'].effort).toBeUndefined()

// These agents have unconditional effort - always present
expect(agents['iloom-wave-verifier'].effort).toBe('high')
expect(agents['iloom-framework-detector'].effort).toBe('high')

// No effort field at all
expect(agents['iloom-artifact-reviewer'].effort).toBeUndefined()
})
})

describe('loadAgents without templateVariables (no substitution)', () => {
it('should load all agents successfully even without template variables', async () => {
const manager = new AgentManager(AGENTS_DIR, templateManager)
const agents = await manager.loadAgents()

const loadedNames = Object.keys(agents).sort()
expect(loadedNames).toEqual(ALL_AGENT_NAMES)
})
})

describe('swarm vs non-swarm model differences', () => {
it('should produce different models for swarm-conditional agents', async () => {
const manager = new AgentManager(AGENTS_DIR, templateManager)

const swarmAgents = await manager.loadAgents(undefined, { SWARM_MODE: true })
const nonSwarmAgents = await manager.loadAgents(undefined, { SWARM_MODE: false })

// Agents that change model between swarm and non-swarm
const conditionalModelAgents = [
'iloom-issue-implementer',
'iloom-issue-planner',
'iloom-issue-enhancer',
'iloom-code-reviewer',
]

for (const name of conditionalModelAgents) {
expect(
swarmAgents[name].model,
`${name} swarm model should be sonnet`,
).toBe('sonnet')
expect(
nonSwarmAgents[name].model,
`${name} non-swarm model should be opus`,
).toBe('opus')
}
})

it('should produce different effort levels for swarm-conditional agents', async () => {
const manager = new AgentManager(AGENTS_DIR, templateManager)

const swarmAgents = await manager.loadAgents(undefined, { SWARM_MODE: true })
const nonSwarmAgents = await manager.loadAgents(undefined, { SWARM_MODE: false })

// Agents that only have effort in swarm mode
const conditionalEffortAgents = [
'iloom-issue-analyzer',
'iloom-issue-planner',
'iloom-issue-implementer',
'iloom-issue-enhancer',
'iloom-code-reviewer',
'iloom-issue-complexity-evaluator',
'iloom-issue-analyze-and-plan',
]

for (const name of conditionalEffortAgents) {
expect(
swarmAgents[name].effort,
`${name} should have effort in swarm mode`,
).toBeDefined()
expect(
nonSwarmAgents[name].effort,
`${name} should NOT have effort in non-swarm mode`,
).toBeUndefined()
}
})
})
})
Loading
Loading