# Plan: User-level AGENTS.md support

## Goal

Allow users to place an `AGENTS.md` file in their config directory (`~/.config/swival/AGENTS.md`) that gets prepended to any project-level `AGENTS.md`. This mirrors the existing global-vs-project config pattern (`config.toml`).

## Current behavior

- `load_instructions()` in `swival/agent.py:322-353` loads `CLAUDE.md` and `AGENTS.md` from `<base_dir>` only.
- Each file is read, truncated to 10k chars, and wrapped in XML tags (`<project-instructions>` / `<agent-instructions>`).
- The `--no-instructions` flag skips all instruction loading.
- The global config dir is resolved by `_global_config_dir()` in `swival/config.py:95-100`.

## Call-site audit

All instruction loading is highly centralized. `load_instructions()` is called from exactly one production site: inside `build_system_prompt()` at `agent.py:1191`. `build_system_prompt()` itself has exactly two call sites:

1. **CLI path** — `_run_main()` at `agent.py:1278` (covers both single-shot and REPL modes)
2. **Library path** — `Session._setup()` at `session.py:167` (covers `.run()` and `.ask()`)

REPL commands (`/init`, `/clear`, `/continue`) do not rebuild the system prompt — they reuse the one already in the message list. No other code reads AGENTS.md or CLAUDE.md directly.

Threading `config_dir` into `build_system_prompt()` is therefore sufficient to cover all paths. Both call sites already have access to config state and can resolve the config dir.

## Design

### Loading order

The final `<agent-instructions>` block will contain:

1. User-level `AGENTS.md` from `~/.config/swival/AGENTS.md` (or `$XDG_CONFIG_HOME/swival/AGENTS.md`)
2. Project-level `AGENTS.md` from `<base_dir>/AGENTS.md`

Both are optional. If neither exists, nothing is emitted (same as today).

### Char limit

The combined content shares the existing `MAX_INSTRUCTIONS_CHARS = 10_000` limit for the `<agent-instructions>` block. The user-level file is read first; remaining budget goes to the project file.

This is a deliberate choice: a large user file can starve project instructions. The rationale is that the user controls both files and can size them accordingly, and the alternative (separate 10k budgets per level) would double the worst-case context consumption. This tradeoff must be documented in the README and covered by a dedicated regression test.

### XML tagging

Both levels go inside a single `<agent-instructions>` block, with comment markers to identify provenance:

```
<agent-instructions>
<!-- user: /home/user/.config/swival/AGENTS.md -->
...user AGENTS.md content...

<!-- project: /home/user/myproject/AGENTS.md -->
...project AGENTS.md content...
</agent-instructions>
```

When only one level is present, only that level's comment and content appear — no empty markers.

When both are present, they are separated by exactly one blank line (`\n\n`).

### File provenance tracking

`load_instructions()` returns a list of loaded filenames. All paths in this list must be **absolute/resolved** (e.g. `/home/user/.config/swival/AGENTS.md`, not `AGENTS.md` or `~/.config/swival/AGENTS.md`). This keeps diagnostics and verbose output unambiguous, especially for nested project layouts.

### Flag behavior

- `--no-instructions` skips both user-level and project-level instruction files (CLAUDE.md and AGENTS.md).
- No new CLI flag needed. Users who don't want user-level agent instructions simply don't create the file.

### Error handling

An unreadable user-level AGENTS.md (permission denied, I/O error) is handled identically to the existing project-level behavior: catch `OSError`, skip the file, and continue. In verbose mode, print a warning naming the file and the error. No crash, no abort.

## Changes

### 1. `swival/config.py` — expose config dir

Rename `_global_config_dir` to `global_config_dir` (drop the underscore) and update the one internal caller (`load_config`).

### 2. `swival/agent.py` — update `load_instructions()`

New signature:

```python
def load_instructions(base_dir: str, config_dir: Path | None, verbose: bool) -> tuple[str, list[str]]:
```

Logic changes:

- Read `<config_dir>/AGENTS.md` if `config_dir` is not None. Apply existing OSError/encoding handling.
- Read `<base_dir>/AGENTS.md` as before.
- Concatenate into a single `<agent-instructions>` block with provenance comments (absolute paths) and `\n\n` separator.
- Shared char budget: user-level content counted first, project-level gets remainder. If user-level alone exceeds limit, it is truncated and project-level is skipped entirely with a truncation notice.
- Return absolute paths in the filenames list.

### 3. `swival/agent.py` — update `build_system_prompt()`

Add `config_dir: Path | None` parameter. Pass it through to `load_instructions()`.

### 4. `swival/config.py` — return `config_dir` from `load_config()`

`load_config()` already calls `global_config_dir()` internally to find the global TOML and returns a `dict`. Add a `"config_dir"` key to that dict holding the resolved `Path`. This avoids changing the return type or tuple arity, and all existing callers that don't need the value simply ignore the key.

```python
# in load_config(), before returning:
cfg["config_dir"] = global_config_dir()
return cfg
```

Downstream access: `config["config_dir"]` → `Path`.

### 5. `swival/agent.py` — update `_run_main()` call site

Read `config_dir` from the config dict (populated by step 4) and pass it to `build_system_prompt()`.

### 6. `swival/session.py` — update `Session._setup()` call site

Same: read `config_dir` from the config dict rather than calling `global_config_dir()` independently.

### 7. Tests — `tests/test_instructions.py`

New test cases:

| Case | Assertion |
|------|-----------|
| User-level only | Content appears in `<agent-instructions>`, provenance comment shows absolute config dir path |
| Project-level only | Behavior identical to today (regression) |
| Both present | User-level first, project-level second, separated by `\n\n`, both provenance comments present |
| Neither present | No `<agent-instructions>` block emitted |
| User-level exceeds 10k | Truncated with notice, project-level skipped entirely |
| Combined exceeds 10k | User-level intact, project-level truncated |
| `--no-instructions` | Both levels skipped |
| Unreadable user-level file (monkeypatch `Path.open` to raise `PermissionError`) | Skipped without crash, project-level still loads |
| Empty user-level file | No crash, project-level loads normally, no empty comment block |
| Boundary formatting | Assert exact `<!-- user: ... -->`, `<!-- project: ... -->`, and `\n\n` separator text |

### 8. Documentation

Update README to document:

- `~/.config/swival/AGENTS.md` is loaded before project-level `AGENTS.md`
- Shared 10k char budget (user-level has priority)
- `--no-instructions` disables both

## Files touched

| File | Change |
|------|--------|
| `swival/config.py` | Rename `_global_config_dir` → `global_config_dir`, include resolved path in `load_config()` output |
| `swival/agent.py` | Update `load_instructions()`, `build_system_prompt()`, `_run_main()` |
| `swival/session.py` | Read `config_dir` from config dict, pass to `build_system_prompt()` in `_setup()` |
| `tests/test_instructions.py` | 10 new test cases (unreadable file tested via monkeypatch, not chmod) |
| `README.md` | Document user-level AGENTS.md |

## Not in scope

- User-level `CLAUDE.md` — could follow the same pattern later but is a separate decision.
- Per-directory AGENTS.md walking (ancestor directories) — intentionally kept simple.
- Merging or deduplication of instructions across levels — just concatenation.
