# AGENTS.md

Instructions for AI coding agents working on this repository.

## Project Identity

| Aspect | Value |
|--------|-------|
| **PyPI package** | `ai-agent-rules` |
| **CLI command** | `ai-agent-rules` (canonical) or `ai-rules` (alias) |
| **Python module** | `ai_rules` |

The name `ai-rules` was taken on PyPI, so the package is published as `ai-agent-rules`. Both CLI entry points work. Use `ai-agent-rules` as the canonical name; `ai-rules` is kept as a convenience alias.

**Supported agents:** Claude Code, Goose, Gemini CLI, Codex CLI, Amp, Shared (AGENTS.md, skills)

## Quick Commands

```bash
just                          # Lint, format, and type check
just test                     # Run tests
just test-unit                # Run unit tests only
just test-integration         # Run integration tests only
just test-e2e                         # Run E2E tests (subprocess, real CLI; CI: ubuntu via ci.yml, macos/windows via e2e.yml)
uv run ai-agent-rules <cmd>         # Run CLI

# GitHub installation
uv run ai-agent-rules setup --github  # Install from GitHub instead of PyPI

# Key CLI commands
uv run ai-agent-rules install        # Install symlinks
uv run ai-agent-rules status         # Check symlink status (shows diffs)
uv run ai-agent-rules upgrade        # Upgrade to latest (shows changelogs)
uv run ai-agent-rules validate       # Validate config files
uv run ai-agent-rules diff           # Show diffs between repo and installed

# Filtering flags (apply to install, status, diff, uninstall)
#   --agents <list>  Comma-separated agent IDs to target (default: all)
#   --only <list>    Comma-separated component types to target (default: all)
#                    Valid values: config, skills, settings, mcps, plugins,
#                    extensions, completions, tools, source-files
#                    Composes with --agents as an intersection

# Subcommands
uv run ai-agent-rules config show    # Show current config
uv run ai-agent-rules config edit    # Edit user config in $EDITOR
uv run ai-agent-rules override list  # List settings overrides
uv run ai-agent-rules completions install  # Install shell completions
uv run ai-agent-rules profile list   # List available profiles
uv run ai-agent-rules profile switch <name>  # Switch to different profile
```

## Tech Stack

- Python 3.14+ with strict type checking (mypy)
- **uv** for dependency management
- **Click** for CLI framework
- **Rich** for console output
- **pytest** with xdist for parallel testing
- **just** for task automation
- **ruff** for linting and formatting

## Project Structure

```
src/ai_rules/
├── cli/                # Click CLI package (commands, groups, components, helpers)
├── config.py           # Config loading, path parsing, merging, preserved fields
├── profiles.py         # Profile loading and inheritance resolution
├── state.py            # State management (active profile tracking)
├── utils.py            # Deep merge and utility functions
├── symlinks.py         # Symlink operations with backups
├── plugins.py          # Claude Code plugin management via marketplace
├── mcp.py              # MCP server management
├── skills.py           # Shared skills management for Claude Code & Goose
├── claude_extensions.py # Claude extensions (agents, commands, hooks) status
├── completions.py      # Shell completion management
├── agents/
│   ├── base.py         # Abstract Agent base class
│   ├── amp.py          # Amp agent (ampcode.com)
│   ├── claude.py       # ClaudeAgent (settings, MCPs, extensions)
│   ├── codex.py        # CodexAgent (config.toml, MCPs)
│   ├── gemini.py       # GeminiAgent (settings, MCPs)
│   ├── goose.py        # GooseAgent (config, hints, MCPs)
│   └── shared.py       # SharedAgent (AGENTS.md, shared skills)
├── bootstrap/          # GitHub install utilities
│   ├── registry.py     # Tool lifecycle registry (DEPRECATED_TOOLS, ACTIVE_TOOLS) — single source of truth
│   ├── installer.py    # Generic install/uninstall (ensure_tool_installed, ensure_tool_uninstalled)
│   ├── updater.py      # Update checking
│   └── version.py      # Version parsing
└── config/             # Source configs (bundled in package)
    ├── AGENTS.md       # Shared behavioral rules
    ├── chat_agent_hints.md  # Chat agent hints
    ├── mcps.json       # Shared MCP server definitions
    ├── amp/            # Amp configs (AGENTS.md, settings.json)
    ├── claude/         # Claude Code configs (CLAUDE.md, settings.json, mcps.json)
    ├── codex/          # Codex configs (config.toml)
    ├── gemini/         # Gemini configs (GEMINI.md, settings.json)
    ├── goose/          # Goose configs (.goosehints, config.yaml)
    ├── skills/         # **SHARED** skills (symlinked to Claude, Goose, Codex, Amp)
    │   ├── agents-md/, code-reviewer/, continue-crash/, crossfire/
    │   ├── dev-docs/, doc-writer/, pr-creator/, prompt-critique/
    │   ├── prompt-engineer/, test-writer/
    └── profiles/       # Built-in profiles (default.yaml, personal.yaml, work.yaml); fragments/ for agents_md_file content
tests/
├── fixtures/           # Test fixture files
├── unit/               # No filesystem side effects
├── integration/        # Modifies files/symlinks
└── e2e/                # Subprocess tests invoking real CLI binary (zero mocking)
```

## Key Patterns

### Agent Abstraction
All AI tools inherit from `Agent` (`agents/base.py`). To add a new tool:
1. Create `agents/<tool>.py` inheriting from `Agent`
2. Implement: `name`, `agent_id`, `get_symlinks()`
3. Register in `targets/registry.py::TARGET_CLASSES`

### Config System
- User config: `~/.ai-agent-rules-config.yaml`
- **State file**: `~/.ai-agent-rules/state.yaml` (tracks active profile, last install time)
- **Profiles**: Named collections of overrides (default, personal, work) with inheritance
  - Built-in: `config/profiles/{default,personal,work}.yaml`
  - User-defined profile files are not supported; per-machine customization uses `~/.ai-agent-rules-config.yaml`
  - Inheritance via `extends:` key (e.g., work extends default)
  - Commands: `profile list`, `profile show`, `profile current`, `profile switch`
  - `agents_md_file`: path relative to `profiles/` dir; content loaded into `agents_md` at profile load; mutually exclusive with inline `agents_md` in the same profile file; accumulates parent-first through inheritance
- `settings_overrides` for machine-specific agent settings
- Cache-based override merging for all agents with preserved fields
  - **Critical**: Preserves agent-managed fields during cache rebuild:
    - Claude: `enabledPlugins`, `hooks`
    - Codex: `projects`
    - Gemini: `ide`
    - Goose: `extensions`
- **Plugin management**: Declarative plugin installs from profiles (`plugins`, `marketplaces` keys)
  - Auto-uninstalls orphaned plugins (previously managed by ai-agent-rules, removed from config)
  - Tracks managed plugins in `~/.claude/plugins/ai-agent-rules-managed.json`
  - Warns about manually-installed plugins not in config (doesn't auto-remove)
- Agent-specific hints (CLAUDE.md, .goosehints) use `@~/AGENTS.md` to reference main file (token-saving)
- **Managed-file provenance header** — every deployed non-JSON config source starts with `This file is managed by ai-agent-rules. Do not edit manually.` plus the repo URL (`#` comments for YAML/TOML, `<!-- -->` for markdown, `#` lines inside frontmatter for `SKILL.md`). JSON cannot carry comments, so JSON configs are exempt. Enforced by `TestManagedFileProvenance` in `tests/unit/test_config_files.py`

## Testing

```bash
uv run pytest -m unit           # Unit only
uv run pytest -m integration    # Integration only
uv run pytest -m agents         # Agent tests only
uv run pytest -m bootstrap      # Bootstrap tests only
uv run pytest -m completions    # Shell completion tests only
uv run pytest -m config         # Config tests only
uv run pytest -m state          # State management tests only
just test-e2e                   # E2E only — runs real CLI as subprocess, no mocking
                                # E2E isolation: tmp_path, HOME/USERPROFILE/APPDATA/XDG all redirected
                                # Use run_cli / run_cli_with_config fixtures (see tests/e2e/conftest.py)
```

## Code Style

- Run `just` before committing (handles linting, formatting, type checks)
- **pathlib.Path** not string paths
- **rich.console** for CLI output
- **Conventional commits** (`feat:`, `fix:`, `chore:`, `refactor:`, `docs:`)

## Common Gotchas

1. **Array path notation** - Setting paths use brackets for arrays: `hooks.SubagentStop[0].command`

2. **Mocking HOME in tests** - Must patch both or tests fail subtly:
   ```python
   monkeypatch.setenv("HOME", str(home))
   monkeypatch.setattr(Path, "home", staticmethod(lambda: home))
   ```

3. **Package index config** - `uvx pip` and `uv tool` use different index configs:
   - `uvx pip` → pip config (PIP_INDEX_URL)
   - `uv tool` → uv config (UV_DEFAULT_INDEX/UV_INDEX_URL)
   - Solution: Pass explicit `--index-url` to pip, `--default-index` to uv

4. **Local development vs installed tool** - **CRITICAL**: Always use `uv run ai-agent-rules` when developing locally:
   - **Local dev (from repo)**: `uv run ai-agent-rules <command>` → runs YOUR local code changes directly
   - **Installed tool (any directory)**: `ai-rules <command>` → runs installed version from `~/.local/share/uv/tools/`
   - Running `ai-rules` without `uv run` will NOT reflect your local changes
   - **NEVER use editable install** (`uv pip install -e .`) - risks conflicts with installed version, unnecessary complexity

5. **Package data dotfiles** - Dotfiles require explicit glob pattern:
   - `pyproject.toml`: `ai_rules = ["config/**/*", "config/**/.*"]`
   - Second pattern needed for `.goosehints` and other dotfiles to be included in wheel

6. **GitHub installs** - `setup --github` installs from HEAD of main branch, not tags:
   - Update checks use GitHub API tags
   - Useful for pre-release features before PyPI publish

7. **Preserved fields in settings.json** - `enabledPlugins`, `hooks` managed by Claude Code or user:
   - ai-agent-rules preserves these fields during cache rebuilds
   - Defined in the `preserved_fields` property of each agent class (e.g., `claude.py`, `codex.py`)
   - Tracking file: `~/.claude/ai-agent-rules-managed-fields.json` (tracks ai-agent-rules contributions)
   - Cleanup: When ai-agent-rules removes a hook from source, it's removed from user settings
   - User hooks preserved (e.g., custom UserPromptSubmit hooks won't be removed)

8. **Upgrade shows changelogs** - `upgrade` command fetches CHANGELOG.md from GitHub:
   - Displays version notes between current and latest
   - Fetches from `https://raw.githubusercontent.com/{repo}/main/CHANGELOG.md`
   - Fails silently on network errors (still proceeds with upgrade)
   - Auto-forces install (no double prompt)

9. **Optional tool registry** — Adding/retiring an optional tool requires ONE registry entry in `bootstrap/registry.py`. Never add per-tool functions:
   ```python
   # ✅ Add to DEPRECATED_TOOLS to retire a tool (cleanup on next install)
   DeprecatedToolSpec(
       tool_id="foo", package_name="foo-pkg", command_name="foo", is_mcp=False
   )
   # ✅ Add to ACTIVE_TOOLS to manage a new tool
   ActiveToolSpec(
       tool_id="bar", command_name="bar-cli", get_install_spec=lambda: BarTool.INSTALL_SPEC
   )
   # ❌ Do NOT add ensure_foo_installed() / ensure_foo_uninstalled() functions
   ```
   - `DeprecatedToolSpec.is_still_in_use` — True means "skip cleanup" (user still wants it)
   - `ActiveToolSpec.is_configured` — True means "install it" (different semantics!)
   - `ActiveToolSpec.get_install_spec` is a `Callable[[], ToolSpec]` — always call it: `active.get_install_spec()`. Storing the result at module level causes circular imports.
   - `ensure_tool_installed(..., skip_update_check=True)` in `_install_active_tools` — install is a fast presence-check; upgrade checks belong in the `upgrade` command

10. **Profile inheritance is additive-only** — `deep_merge` can add or overwrite keys but CANNOT remove them.
    If a parent profile sets a key, every child inherits it unless the child explicitly overrides it.
    Inheritance chain: `default` → `personal` → `work`.
    When adding a setting to a parent profile, check all child profiles and add explicit overrides where the setting should NOT apply.

11. **Gemini skill directory** - Gemini discovers skills from `~/.agents/skills/` (the Codex directory) via a built-in alias:
   - Do NOT add a `~/.gemini/skills/` directory — it causes "Skill conflict detected" warnings in headless invocations
   - This is why `AGENT_SKILLS_DIRS` in `config.py` intentionally excludes Gemini

12. **Response-style rules are duplicated on purpose** - `config/AGENTS.md` (coding agents) and `config/chat_agent_hints.md` (copy/pasted into chat apps) share a response-style core but cannot share includes — the chat file must stay self-contained. When editing response-style rules in one, check whether the other needs the same change.

## Skills

**Skills:** Explore `config/skills/*/SKILL.md` for available skills (15 total: agents-md, code-reviewer, continue-crash, crossfire, dev-docs, doc-writer, kb, pr-creator, prompt-critique, prompt-engineer, rebase, research, session-search, test-writer, worktree-cleanup).
- **SHARED across agents** - symlinked to `~/.claude/skills/`, `~/.config/goose/skills/`, `~/.config/agents/skills/` (Amp), `~/.agents/skills/` (Codex)
- Managed by SharedAgent (displays under "Shared:" in status)
- To add a skill: Create subdir in `config/skills/` with `SKILL.md`
- **Skill versioning**: Each SKILL.md has a `version` field in its frontmatter. Bump it when changing a skill's behavior — patch (1.0.x) for tweaks/fixes, minor (1.x.0) for new capabilities, major (x.0.0) for breaking prompt changes
  - **Ship checklist (do NOT skip):** any PR that changes a skill's behavior MUST bump that skill's `version` in the SAME PR. Verify the bump is present before marking the PR ready for review — a behavior change with an unchanged version is an incomplete PR.

## Key Files by Task

| Task | Files |
|------|-------|
| Add CLI command | `cli/commands/` (one module per command) or `cli/groups/` (subcommand groups), then register in `cli/__init__.py::_register_commands` |
| Add/retire optional tool | `bootstrap/registry.py` — one entry in `ACTIVE_TOOLS` or `DEPRECATED_TOOLS` only; no other files needed |
| Add skill | Create subdir in `config/skills/` with `SKILL.md` (shared) |
| Config loading | `config.py` (Config class, load_config) |
| Profile management | `profiles.py`, `state.py`, `cli/groups/profile.py` |
| State management | `state.py` (ProfileState class) |
| Symlink behavior | `symlinks.py` (create_symlink, remove_symlink) |
| Config files component | `cli/components/config.py` (ConfigComponent — install/status/diff for symlinked config files) |
| Settings cache component | `cli/components/settings.py` (SettingsComponent — build/rebuild merged settings cache) |
| Shell completions | `completions.py`, `cli/groups/completions.py` |
| New agent | `agents/base.py`, `agents/<new>.py`, `targets/registry.py::TARGET_CLASSES` |
| Plugin management | `plugins.py`, each `agents/*.py` defines `preserved_fields` |
| MCP management | `mcp.py` (MCPManager base + subclasses: Claude, Goose, Codex, Gemini, Amp) |
| Skills management | `skills.py` (SkillManager), `agents/shared.py` |
| Preserved fields tracking | `config.py` (ManagedFieldsTracker), each `agents/*.py` defines `preserved_fields` |
| Upgrade checking | `bootstrap/updater.py`, `bootstrap/installer.py` |
| GitHub install support | `bootstrap/installer.py` (GITHUB_REPO_URL) |
