Skip to content
Draft
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ tmp/
.vscode/

# Added by iloom CLI
.iloom/settings.local.json
.iloom/settings.local.json
.claude/settings.local.json
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ This example shows how to configure a project-wide default (e.g., GitHub remote)
}
```

### Repository Guidance (ILOOM.md)

Drop an optional `ILOOM.md` at your repository root to inject project-specific guidance (review priorities, conventions, feature-flag guardrails) into every iloom agent prompt — see [ILOOM.md — Repository Guidance](docs/iloom-commands.md#iloommd--repository-guidance) for details.

### Multi-Language/Framework Project Support

iloom supports projects in any programming language through `.iloom/package.iloom.json`. This file defines scripts using raw shell commands instead of npm scripts.
Expand Down
76 changes: 76 additions & 0 deletions docs/iloom-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Complete documentation for all iloom CLI commands, options, and flags.
- [il feedback](#il-feedback)
- [il contribute](#il-contribute)
- [il telemetry](#il-telemetry)
- [ILOOM.md — Repository Guidance](#iloommd--repository-guidance)

---

Expand Down Expand Up @@ -2096,6 +2097,81 @@ For details on what data is and is not collected, see the [Telemetry section in

---

## ILOOM.md — Repository Guidance

`ILOOM.md` is an optional per-repository file you can drop at the root of your project to steer iloom's behavior without forking the CLI or its prompt templates. Its contents are injected into every agent prompt iloom renders — so you can tune review priorities, enforce conventions, or hand AI agents project-specific guardrails that live alongside your code and evolve with it.

**Purpose:**

- Steer iloom behavior (analyzers, planners, implementers, reviewers) without forking the CLI or its prompt templates
- Keep project conventions version-controlled alongside the code that needs them
- Let different repos use the same iloom binary with different guidance

**File location:**

- `./ILOOM.md` at the repository root (same directory as `.iloom/`, `package.json`, etc.)
- Tracked in git like any other source file

**When iloom reads it:**

Every command that renders a prompt template loads `ILOOM.md` at invocation time and injects its contents into the rendered prompt. This applies to `il start`, `il spin`, `il plan`, `il enhance`, `il init`, `il summary`, and all swarm-mode and agent-driven flows.

**Silent no-op when absent:**

If `ILOOM.md` does not exist, iloom silently omits the Repository Guidance section from all prompts. There is no warning, no error, and no configuration required — it is purely opt-in. Existing projects are unaffected.

**Example `ILOOM.md`:**

```markdown
# Repository Guidance for iloom Agents

## Review Priorities

When reviewing code, apply these priorities in order:

1. **Accessibility first** — all UI changes must preserve ARIA labels and keyboard navigation. Flag any missing `aria-*` attributes as blocking.
2. **No direct DOM mutation** outside `src/dom/` — we use the abstraction layer everywhere else.
3. **Error handling** — never swallow errors. Always rethrow with context or log at `error` level.

## Theming Conventions

- Use design tokens from `src/theme/tokens.ts`. Never hardcode hex colors in components.
- Dark mode is the default; light mode tokens live in `src/theme/light.ts`.
- Component variants go through `variants()` — do not branch on `theme.mode` inside components.

## Feature Flag Guardrails

All new features must be gated behind a flag in `src/flags/registry.ts`:

- Flags default to `false` until explicitly launched
- Never remove a flag in the same PR that adds the feature
- Flag removal requires a separate PR after at least one release cycle

## Testing

- Prefer integration tests in `tests/integration/` over mock-heavy unit tests
- Do not mock our own modules — only mock external HTTP / filesystem / shell boundaries
- Snapshot tests are allowed only for stable, rarely-changing output (CLI help text, etc.)
```

**How it appears in prompts:**

When `ILOOM.md` is present, agents see a clearly labeled section near the top of their prompt:

```
## Repository Guidance (from ILOOM.md)

The repository maintainers have provided the following guidance via ILOOM.md. Apply it throughout your work.

<contents of ILOOM.md>
```

**Telemetry:**

iloom emits a single `iloom_md.loaded` telemetry event per process, indicating whether `ILOOM.md` was present and a coarse size bucket (`empty` / `small` / `medium` / `large`). The file's contents are never transmitted.

---

## Global Flags

Some flags work across multiple commands:
Expand Down
6 changes: 6 additions & 0 deletions src/commands/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AgentManager } from '../lib/AgentManager.js'
import { SettingsManager } from '../lib/SettingsManager.js'
import { gatherDiagnosticInfo, formatDiagnosticsAsMarkdown } from '../utils/diagnostics.js'
import { capitalizeFirstLetter } from '../utils/text.js'
import { loadPromptExtensions } from '../lib/PromptExtensions.js'

// Hardcoded target repository for feedback
const FEEDBACK_REPOSITORY = 'iloom-ai/iloom-cli'
Expand Down Expand Up @@ -60,6 +61,11 @@ export class FeedbackCommand {
const diagnostics = await gatherDiagnosticInfo()
const diagnosticsMarkdown = formatDiagnosticsAsMarkdown(diagnostics)

// Preload repository-local prompt extensions (ILOOM.md) so ILOOM_MD_CONTENT
// propagates into any templates rendered downstream by IssueEnhancementService.
// Use process.cwd() so linked-worktree users see their branch-local ILOOM.md.
await loadPromptExtensions(process.cwd())

// Step 3: Create enhanced issue body with marker and diagnostics
const userBody = body ?? description
const enhancedBody = `${diagnosticsMarkdown}
Expand Down
6 changes: 6 additions & 0 deletions src/commands/ignite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { SwarmSetupService } from '../lib/SwarmSetupService.js'
import type { LoomMetadata } from '../lib/MetadataManager.js'
import { TelemetryService } from '../lib/TelemetryService.js'
import { detectProjectLanguage } from '../utils/language-detector.js'
import { loadPromptExtensions } from '../lib/PromptExtensions.js'

/**
* Error thrown when the spin command is run from an invalid location
Expand Down Expand Up @@ -278,7 +279,9 @@ export class IgniteCommand {
}

// Step 2.2: Get prompt template with variable substitution
const promptExtensions = await loadPromptExtensions(context.workspacePath)
const variables = this.buildTemplateVariables(context, effectiveOneShot, draftPrNumber, draftPrUrl)
variables.ILOOM_MD_CONTENT = promptExtensions.iloomMd

// Step 2.5: Add first-time user context if needed
if (isFirstRun) {
Expand Down Expand Up @@ -953,13 +956,16 @@ export class IgniteCommand {
// Determine issue prefix for commit message trailers
const issuePrefix = providerName === 'github' ? '#' : ''

const promptExtensions = await loadPromptExtensions(epicWorktreePath)

const variables: TemplateVariables = {
EPIC_ISSUE_NUMBER: epicIssueNumber,
EPIC_WORKTREE_PATH: epicWorktreePath,
EPIC_METADATA_PATH: epicMetadataPath,
CHILD_ISSUES: JSON.stringify(childIssuesData, null, 2),
DEPENDENCY_MAP: JSON.stringify(metadata.dependencyMap, null, 2),
ISSUE_PREFIX: issuePrefix,
ILOOM_MD_CONTENT: promptExtensions.iloomMd,
...(skipCleanup && { NO_CLEANUP: true }),
}

Expand Down
7 changes: 7 additions & 0 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { SettingsMigrationManager } from '../lib/SettingsMigrationManager.js'
import { getRepoRoot, isFileGitignored } from '../utils/git.js'
import { FirstRunManager } from '../utils/FirstRunManager.js'
import { TelemetryService } from '../lib/TelemetryService.js'
import { loadPromptExtensions } from '../lib/PromptExtensions.js'

/**
* Initialize iloom configuration
Expand Down Expand Up @@ -368,6 +369,11 @@ export class InitCommand {
const hasPackageJson = existsSync(packageJsonPath)
logger.debug('Package.json detection', { packageJsonPath, hasPackageJson })

// Load repository-local prompt extensions (ILOOM.md).
// Use process.cwd() rather than getRepoRoot() so linked-worktree users
// see their branch-local ILOOM.md instead of the main checkout's.
const promptExtensions = await loadPromptExtensions(process.cwd())

// Build template variables
const variables = {
SETTINGS_SCHEMA: schemaContent,
Expand All @@ -388,6 +394,7 @@ export class InitCommand {
// Multi-language support - mutually exclusive booleans
HAS_PACKAGE_JSON: hasPackageJson,
NO_PACKAGE_JSON: !hasPackageJson,
ILOOM_MD_CONTENT: promptExtensions.iloomMd,
}

logger.debug('Building template variables', {
Expand Down
5 changes: 5 additions & 0 deletions src/commands/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { promptConfirmation, isInteractiveEnvironment } from '../utils/prompt.js
import { TelemetryService } from '../lib/TelemetryService.js'
import { StartCommand } from './start.js'
import { IgniteCommand } from './ignite.js'
import { loadPromptExtensions } from '../lib/PromptExtensions.js'

// Define provider arrays for validation and dynamic flag generation
const PLANNER_PROVIDERS = ['claude', 'gemini', 'codex'] as const
Expand Down Expand Up @@ -435,6 +436,9 @@ export class PlanCommand {

// Load plan prompt template with mode-specific variables
logger.debug('Loading plan prompt template')
// Use process.cwd() rather than getRepoRoot() so linked-worktree users
// see their branch-local ILOOM.md instead of the main checkout's.
const planPromptExtensions = await loadPromptExtensions(process.cwd())
const templateVariables: TemplateVariables = {
IS_VSCODE_MODE: isVscodeMode,
EXISTING_ISSUE_MODE: !!decompositionContext,
Expand All @@ -452,6 +456,7 @@ export class PlanCommand {
REVIEWER: effectiveReviewer,
HAS_REVIEWER: effectiveReviewer !== 'none',
AUTO_SWARM_MODE: autoSwarm ?? false,
ILOOM_MD_CONTENT: planPromptExtensions.iloomMd,
...providerFlags,
}
const architectPrompt = await this.templateManager.getPrompt('plan', templateVariables)
Expand Down
5 changes: 5 additions & 0 deletions src/lib/ClaudeService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ describe('ClaudeService', () => {
WORKSPACE_PATH: '/workspace/issue-123',
PORT: 3123,
IS_VSCODE_MODE: false,
ILOOM_MD_CONTENT: '',
})

expect(claudeUtils.launchClaudeInNewTerminalWindow).toHaveBeenCalledWith(prompt, {
Expand Down Expand Up @@ -136,6 +137,7 @@ describe('ClaudeService', () => {
WORKSPACE_PATH: '/workspace',
PORT: 3123,
IS_VSCODE_MODE: false,
ILOOM_MD_CONTENT: '',
})
})

Expand All @@ -156,6 +158,7 @@ describe('ClaudeService', () => {
ISSUE_NUMBER: 123,
WORKSPACE_PATH: '/workspace',
IS_VSCODE_MODE: false,
ILOOM_MD_CONTENT: '',
})
})
})
Expand Down Expand Up @@ -183,6 +186,7 @@ describe('ClaudeService', () => {
WORKSPACE_PATH: '/workspace/pr-456',
PORT: 3456,
IS_VSCODE_MODE: false,
ILOOM_MD_CONTENT: '',
})

// PR workflow uses acceptEdits permission mode by default
Expand Down Expand Up @@ -214,6 +218,7 @@ describe('ClaudeService', () => {
expect(mockTemplateManager.getPrompt).toHaveBeenCalledWith('regular', {
WORKSPACE_PATH: '/workspace/feature',
IS_VSCODE_MODE: false,
ILOOM_MD_CONTENT: '',
})

// Regular workflow uses acceptEdits permission mode by default
Expand Down
5 changes: 5 additions & 0 deletions src/lib/ClaudeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { detectClaudeCli, launchClaude, launchClaudeInNewTerminalWindow, ClaudeC
import { PromptTemplateManager, TemplateVariables } from './PromptTemplateManager.js'
import { SettingsManager, IloomSettings } from './SettingsManager.js'
import { logger } from '../utils/logger.js'
import { loadPromptExtensions } from './PromptExtensions.js'

export interface ClaudeWorkflowOptions {
type: 'issue' | 'pr' | 'regular'
Expand Down Expand Up @@ -69,9 +70,13 @@ export class ClaudeService {
// Settings are pre-validated at CLI startup, so no error handling needed here
this.settings ??= await this.settingsManager.loadSettings()

// Load repository-local prompt extensions (ILOOM.md)
const promptExtensions = await loadPromptExtensions(workspacePath)

// Build template variables
const variables: TemplateVariables = {
WORKSPACE_PATH: workspacePath,
ILOOM_MD_CONTENT: promptExtensions.iloomMd,
}

if (issueNumber !== undefined) {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/IssueEnhancementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { openBrowser } from '../utils/browser.js'
import { waitForKeypress } from '../utils/prompt.js'
import { getLogger } from '../utils/logger-context.js'
import { generateIssueManagementMcpConfig } from '../utils/mcp.js'
import { loadPromptExtensions } from './PromptExtensions.js'

/**
* Options for enhancing an existing issue
Expand Down Expand Up @@ -80,9 +81,12 @@ export class IssueEnhancementService {

// Load only the enhancer agent with template variables so Handlebars expressions resolve
const settings = await this.settingsManager.loadSettings()
// Use process.cwd() so linked-worktree users see their branch-local ILOOM.md.
const promptExtensions = await loadPromptExtensions(process.cwd())
const templateVariables: TemplateVariables = {
STANDARD_ISSUE_MODE: true,
DIRECT_PROMPT_MODE: true,
ILOOM_MD_CONTENT: promptExtensions.iloomMd,
}
const loadedAgents = await this.agentManager.loadAgents(
settings,
Expand Down Expand Up @@ -210,9 +214,12 @@ Press any key to open issue for editing...`

// Load only the enhancer agent with template variables so Handlebars expressions resolve
const settings = await this.settingsManager.loadSettings()
// Use process.cwd() so linked-worktree users see their branch-local ILOOM.md.
const promptExtensions = await loadPromptExtensions(process.cwd())
const templateVariables: TemplateVariables = {
ISSUE_NUMBER: issueNumber,
STANDARD_ISSUE_MODE: true,
ILOOM_MD_CONTENT: promptExtensions.iloomMd,
}
const loadedAgents = await this.agentManager.loadAgents(
settings,
Expand Down
50 changes: 50 additions & 0 deletions src/lib/PromptExtensions.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, it, expect, vi } from 'vitest'
import path from 'path'
import { PromptTemplateManager, TemplateVariables } from './PromptTemplateManager.js'

// Do NOT mock fs/promises here — this test renders real template files.
vi.mock('../utils/logger.js', () => ({
logger: {
debug: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}))

/**
* Integration test confirming the ILOOM.md guidance block renders correctly
* into a real prompt template when `ILOOM_MD_CONTENT` is populated, and is
* omitted when empty.
*/
describe('PromptTemplateManager: ILOOM.md guidance injection (integration)', () => {
const templatesDir = path.resolve(process.cwd(), 'templates', 'prompts')
const manager = new PromptTemplateManager(templatesDir)

// Minimal variables required for issue-prompt.txt to render without errors.
const baseVariables: TemplateVariables = {
WORKSPACE_PATH: '/tmp/test-workspace',
ISSUE_NUMBER: 42,
ILOOM_MD_CONTENT: '',
}

const SENTINEL = '# Project Conventions\n- Use tabs\n- Prefer composition'

it('includes the Repository Guidance section when ILOOM_MD_CONTENT is non-empty', async () => {
const result = await manager.getPrompt('issue', {
...baseVariables,
ILOOM_MD_CONTENT: SENTINEL,
})

expect(result).toContain('Repository Guidance (from ILOOM.md)')
expect(result).toContain(SENTINEL)
})

it('omits the Repository Guidance section when ILOOM_MD_CONTENT is empty', async () => {
const result = await manager.getPrompt('issue', {
...baseVariables,
ILOOM_MD_CONTENT: '',
})

expect(result).not.toContain('Repository Guidance (from ILOOM.md)')
})
})
Loading